abstract-3d 2.11.1 → 2.12.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 +2 -0
- 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 +10 -2
- package/lib/renderers/ifc/ifc-encoding.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc-encoding.js +12 -3
- package/lib/renderers/ifc/ifc-encoding.js.map +1 -1
- package/lib/renderers/ifc/ifc.d.ts +21 -3
- package/lib/renderers/ifc/ifc.d.ts.map +1 -1
- package/lib/renderers/ifc/ifc.js +76 -31
- package/lib/renderers/ifc/ifc.js.map +1 -1
- package/lib/renderers/react/react-image-material.d.ts.map +1 -1
- package/lib/renderers/react/react-image-material.js +2 -2
- package/lib/renderers/react/react-image-material.js.map +1 -1
- package/lib/renderers/react/react-material.js +2 -2
- package/lib/renderers/react/react-material.js.map +1 -1
- package/package.json +3 -3
- package/src/abstract-3d.ts +2 -0
- package/src/renderers/ifc/ifc-encoding.ts +21 -4
- package/src/renderers/ifc/ifc.test.ts +152 -3
- package/src/renderers/ifc/ifc.ts +115 -35
- package/src/renderers/react/react-image-material.tsx +2 -2
- package/src/renderers/react/react-material.tsx +2 -2
package/src/renderers/ifc/ifc.ts
CHANGED
|
@@ -15,17 +15,42 @@ export type IfcElementClass = {
|
|
|
15
15
|
readonly predefinedType?: string;
|
|
16
16
|
};
|
|
17
17
|
|
|
18
|
+
export type IfcApplicationInfo = {
|
|
19
|
+
readonly name: string;
|
|
20
|
+
readonly version?: string;
|
|
21
|
+
/** Organization that develops the application, defaults to "Divid AB" */
|
|
22
|
+
readonly developer?: string;
|
|
23
|
+
};
|
|
24
|
+
|
|
18
25
|
export type IfcOptions = {
|
|
19
26
|
/** Name of the IFC project, defaults to "Abstract 3D" */
|
|
20
27
|
readonly name?: string;
|
|
28
|
+
/** Person exporting the file, written in the header and as the owner of every object, defaults to " " */
|
|
29
|
+
readonly author?: string;
|
|
30
|
+
/** Organization of the author, defaults to "UNKNOWN" */
|
|
31
|
+
readonly organization?: string;
|
|
32
|
+
/** Application that produced the scene, e.g. `{ name: "airCalc ACOM", version: "10.1" }`, defaults to Abstract 3D by Divid AB */
|
|
33
|
+
readonly application?: IfcApplicationInfo;
|
|
21
34
|
/**
|
|
22
35
|
* Wraps the whole scene in one element, e.g. `{ entity: "IfcUnitaryEquipment", predefinedType: "AIRHANDLER", name: "AHU" }`, otherwise the
|
|
23
36
|
* top-level groups are placed directly in the storey
|
|
24
37
|
*/
|
|
25
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;
|
|
26
44
|
};
|
|
27
45
|
|
|
28
|
-
type NodeMesh = { readonly material: Material; readonly triangulation: Triangulation };
|
|
46
|
+
type NodeMesh = { readonly material: Material; readonly triangulation: Triangulation; readonly layer: string | undefined };
|
|
47
|
+
|
|
48
|
+
/** The `data` of the groups from the root down to a group and the layer its meshes are on */
|
|
49
|
+
type Layering = {
|
|
50
|
+
readonly layerOf: IfcOptions["layer"];
|
|
51
|
+
readonly path: ReadonlyArray<Record<string, string>>;
|
|
52
|
+
readonly layer: string | undefined;
|
|
53
|
+
};
|
|
29
54
|
|
|
30
55
|
/** A group that becomes an element, groups without any triangulated geometry in them are left out */
|
|
31
56
|
type ElementNode = {
|
|
@@ -39,78 +64,126 @@ type ElementNode = {
|
|
|
39
64
|
|
|
40
65
|
const proxyClass: IfcElementClass = { entity: "IfcBuildingElementProxy" };
|
|
41
66
|
|
|
42
|
-
|
|
67
|
+
/**
|
|
68
|
+
* `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
|
|
70
|
+
*/
|
|
71
|
+
type IfcContext = {
|
|
72
|
+
readonly body: number;
|
|
73
|
+
readonly owner: (id?: string) => string;
|
|
74
|
+
readonly styles: Map<string, number>;
|
|
75
|
+
readonly layers: Map<string, Array<number>>;
|
|
76
|
+
};
|
|
43
77
|
|
|
44
78
|
/**
|
|
45
|
-
* IFC4 (Reference View) with every mesh as an IfcTriangulatedFaceSet, in millimetres. Every top-level group and every group with `data`
|
|
46
|
-
* IfcBuildingElementProxy placed relative to its parent element and aggregated into it.
|
|
47
|
-
* are merged into the parent element
|
|
79
|
+
* IFC4 (Reference View) with every mesh as an IfcTriangulatedFaceSet, in millimetres. Every top-level group and every group with a `data.id` or
|
|
80
|
+
* `data.name` becomes an IfcBuildingElementProxy placed relative to its parent element and aggregated into it. Other groups are only transforms or
|
|
81
|
+
* metadata, so their meshes are merged into the parent element. With `layer`, the face sets are assigned to an IfcPresentationLayerAssignment per
|
|
82
|
+
* layer, the presentation layer of the Reference View
|
|
48
83
|
*/
|
|
49
84
|
export function render(scene: Scene, options: IfcOptions = {}): string {
|
|
50
85
|
const name = options.name ?? "Abstract 3D";
|
|
86
|
+
const author = options.author ?? " ";
|
|
87
|
+
const organization = options.organization ?? "UNKNOWN";
|
|
88
|
+
const application = options.application ?? { name: "Abstract 3D" };
|
|
89
|
+
const now = new Date();
|
|
51
90
|
const m = ifcFile();
|
|
52
91
|
|
|
92
|
+
const ownerHistory = ownerHistoryEntity(m, author, organization, application, now);
|
|
53
93
|
const world = axisPlacement(m, vec3Zero, vec3Zero);
|
|
54
94
|
const context = add(m, `IFCGEOMETRICREPRESENTATIONCONTEXT($,'Model',3,1.E-05,${ref(world)},$)`);
|
|
55
95
|
const body = add(m, `IFCGEOMETRICREPRESENTATIONSUBCONTEXT('Body','Model',*,*,*,*,${ref(context)},$,.MODEL_VIEW.,$)`);
|
|
96
|
+
const c: IfcContext = { body, owner: (id) => `${str(guid(m, id))},${ref(ownerHistory)}`, styles: new Map(), layers: new Map() };
|
|
56
97
|
const units = [
|
|
57
98
|
add(m, "IFCSIUNIT(*,.LENGTHUNIT.,.MILLI.,.METRE.)"),
|
|
58
99
|
add(m, "IFCSIUNIT(*,.AREAUNIT.,$,.SQUARE_METRE.)"),
|
|
59
100
|
add(m, "IFCSIUNIT(*,.VOLUMEUNIT.,$,.CUBIC_METRE.)"),
|
|
60
101
|
add(m, "IFCSIUNIT(*,.PLANEANGLEUNIT.,$,.RADIAN.)"),
|
|
61
102
|
];
|
|
62
|
-
const project = add(m, `IFCPROJECT(${
|
|
103
|
+
const project = add(m, `IFCPROJECT(${c.owner()},${str(name)},$,$,$,$,(${ref(context)}),${ref(add(m, `IFCUNITASSIGNMENT(${refs(units)})`))})`);
|
|
63
104
|
|
|
64
105
|
const sitePlacement = add(m, `IFCLOCALPLACEMENT($,${ref(world)})`);
|
|
65
|
-
const site = add(m, `IFCSITE(${
|
|
106
|
+
const site = add(m, `IFCSITE(${c.owner()},'Site',$,$,${ref(sitePlacement)},$,$,.ELEMENT.,$,$,$,$,$)`);
|
|
66
107
|
const buildingPlacement = add(m, `IFCLOCALPLACEMENT(${ref(sitePlacement)},${ref(world)})`);
|
|
67
|
-
const building = add(m, `IFCBUILDING(${
|
|
108
|
+
const building = add(m, `IFCBUILDING(${c.owner()},'Building',$,$,${ref(buildingPlacement)},$,$,.ELEMENT.,$,$,$)`);
|
|
68
109
|
const storeyPlacement = add(m, `IFCLOCALPLACEMENT(${ref(buildingPlacement)},${ref(world)})`);
|
|
69
|
-
const storey = add(m, `IFCBUILDINGSTOREY(${
|
|
70
|
-
aggregate(m, project, [site]);
|
|
71
|
-
aggregate(m, site, [building]);
|
|
72
|
-
aggregate(m, building, [storey]);
|
|
110
|
+
const storey = add(m, `IFCBUILDINGSTOREY(${c.owner()},'Storey',$,$,${ref(storeyPlacement)},$,$,.ELEMENT.,0.)`);
|
|
111
|
+
aggregate(m, c, project, [site]);
|
|
112
|
+
aggregate(m, c, site, [building]);
|
|
113
|
+
aggregate(m, c, building, [storey]);
|
|
73
114
|
|
|
74
115
|
const center = scene.center_deprecated ?? vec3Zero;
|
|
75
116
|
const sceneRot = scene.rotation_deprecated ?? vec3Zero;
|
|
76
|
-
const
|
|
77
|
-
const
|
|
78
|
-
|
|
79
|
-
|
|
117
|
+
const top: Layering = { layerOf: options.layer, path: [], layer: undefined };
|
|
118
|
+
const rootGroup: Group | undefined = options.root && { pos: center, rot: sceneRot, groups: scene.groups, data: { name: options.root.name } };
|
|
119
|
+
const nodes = rootGroup
|
|
120
|
+
? [elementNode(rootGroup, center, sceneRot, groupLayering(rootGroup, top), options.root)]
|
|
121
|
+
: scene.groups.map((g) =>
|
|
122
|
+
elementNode(g, vec3TransRot(g.pos, center, sceneRot), vec3RotCombine(sceneRot, g.rot ?? vec3Zero), groupLayering(g, top))
|
|
123
|
+
);
|
|
80
124
|
const elements = nodes.flatMap((node) => node ?? []).map((node, i) => element(m, node, `${i + 1}`, storeyPlacement, c));
|
|
81
125
|
if (elements.length > 0) {
|
|
82
|
-
add(m, `IFCRELCONTAINEDINSPATIALSTRUCTURE(${
|
|
126
|
+
add(m, `IFCRELCONTAINEDINSPATIALSTRUCTURE(${c.owner()},$,$,${refs(elements)},${ref(storey)})`);
|
|
83
127
|
}
|
|
128
|
+
c.layers.forEach((items, layer) => add(m, `IFCPRESENTATIONLAYERASSIGNMENT(${str(layer)},$,${refs(items)},$)`));
|
|
84
129
|
|
|
85
|
-
const date =
|
|
86
|
-
return serialize(m, `${name}.ifc`, date);
|
|
130
|
+
const date = now.toISOString().split(".")[0] ?? "1970-01-01T00:00:00";
|
|
131
|
+
return serialize(m, { fileName: `${name}.ifc`, date, author, organization, originatingSystem: application.name });
|
|
87
132
|
}
|
|
88
133
|
|
|
89
|
-
|
|
90
|
-
|
|
134
|
+
/** The author as the owning user and the application, ADDED requires LastModifiedDate in IFC4 so it's set to the creation date */
|
|
135
|
+
function ownerHistoryEntity(m: MutableIfc, author: string, organization: string, application: IfcApplicationInfo, now: Date): number {
|
|
136
|
+
const person = add(m, `IFCPERSON(${str(author)},$,$,$,$,$,$,$)`);
|
|
137
|
+
const owningOrganization = add(m, `IFCORGANIZATION($,${str(organization)},$,$,$)`);
|
|
138
|
+
const { name, version = "", developer = "Divid AB" } = application;
|
|
139
|
+
const developerOrganization = developer === organization ? owningOrganization : add(m, `IFCORGANIZATION($,${str(developer)},$,$,$)`);
|
|
140
|
+
const user = add(m, `IFCPERSONANDORGANIZATION(${ref(person)},${ref(owningOrganization)},$)`);
|
|
141
|
+
const app = add(m, `IFCAPPLICATION(${ref(developerOrganization)},${str(version)},${str(name)},${str(name)})`);
|
|
142
|
+
const timestamp = Math.floor(now.getTime() / 1000);
|
|
143
|
+
return add(m, `IFCOWNERHISTORY(${ref(user)},${ref(app)},$,.ADDED.,${timestamp},$,$,${timestamp})`);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** `layering` is the group's own, see `groupLayering` */
|
|
147
|
+
function elementNode(group: Group, pos: Vec3, rot: Vec3, layering: Layering, elementClass = proxyClass): ElementNode | undefined {
|
|
148
|
+
const { meshes, children } = elementContent(group, vec3Zero, vec3Zero, layering);
|
|
91
149
|
return meshes.length > 0 || children.length > 0 ? { group, elementClass, pos, rot, meshes, children } : undefined;
|
|
92
150
|
}
|
|
93
151
|
|
|
94
|
-
/**
|
|
95
|
-
|
|
152
|
+
/**
|
|
153
|
+
* Meshes of the group and its descendants that aren't elements, relative to the element the group is at pos/rot in, and the descendants with a
|
|
154
|
+
* `data.id` or `data.name` as elements
|
|
155
|
+
*/
|
|
156
|
+
function elementContent(group: Group, pos: Vec3, rot: Vec3, layering: Layering): Pick<ElementNode, "meshes" | "children"> {
|
|
96
157
|
const meshes = (group.meshes ?? []).flatMap((mesh) => {
|
|
97
158
|
const triangulation = triangulate(mesh.geometry, pos, rot);
|
|
98
|
-
return triangulation ? [{ material: mesh.material, triangulation }] : [];
|
|
159
|
+
return triangulation ? [{ material: mesh.material, triangulation, layer: layering.layer }] : [];
|
|
99
160
|
});
|
|
100
161
|
const nested = (group.groups ?? []).map((child) => {
|
|
101
162
|
const childPos = vec3TransRot(child.pos, pos, rot);
|
|
102
163
|
const childRot = vec3RotCombine(rot, child.rot ?? vec3Zero);
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
:
|
|
164
|
+
const childLayering = groupLayering(child, layering);
|
|
165
|
+
return child.data?.["id"] !== undefined || child.data?.["name"] !== undefined
|
|
166
|
+
? { meshes: [], children: [elementNode(child, childPos, childRot, childLayering)].flatMap((node) => node ?? []) }
|
|
167
|
+
: elementContent(child, childPos, childRot, childLayering);
|
|
106
168
|
});
|
|
107
169
|
return { meshes: [...meshes, ...nested.flatMap((n) => n.meshes)], children: nested.flatMap((n) => n.children) };
|
|
108
170
|
}
|
|
109
171
|
|
|
172
|
+
/** Groups with `data` are added to the path and get the layer `layerOf` returns for it, if any, otherwise the layer of the parent */
|
|
173
|
+
function groupLayering(group: Group, parent: Layering): Layering {
|
|
174
|
+
const data = group.data;
|
|
175
|
+
if (data === undefined || Object.keys(data).length === 0) {
|
|
176
|
+
return parent;
|
|
177
|
+
}
|
|
178
|
+
const path = [...parent.path, data];
|
|
179
|
+
const layer = parent.layerOf?.(path);
|
|
180
|
+
return { ...parent, path, layer: layer === undefined || layer === "" ? parent.layer : layer };
|
|
181
|
+
}
|
|
182
|
+
|
|
110
183
|
/** `path` is the element's position in the tree (e.g. "2.1"), named after it when the group has no `data.name` or `data.id` since IFC requires a name */
|
|
111
184
|
function element(m: MutableIfc, node: ElementNode, path: string, parentPlacement: number, c: IfcContext): number {
|
|
112
185
|
const placement = add(m, `IFCLOCALPLACEMENT(${ref(parentPlacement)},${ref(axisPlacement(m, node.pos, node.rot))})`);
|
|
113
|
-
const items = node.meshes.map((
|
|
186
|
+
const items = node.meshes.map((mesh) => faceSet(m, mesh, c));
|
|
114
187
|
const shape =
|
|
115
188
|
items.length > 0
|
|
116
189
|
? ref(add(m, `IFCPRODUCTDEFINITIONSHAPE($,$,(${ref(add(m, `IFCSHAPEREPRESENTATION(${ref(c.body)},'Body','Tessellation',${refs(items)})`))}))`))
|
|
@@ -118,22 +191,29 @@ function element(m: MutableIfc, node: ElementNode, path: string, parentPlacement
|
|
|
118
191
|
const id = node.group.data?.["id"];
|
|
119
192
|
const name = node.group.data?.["name"] ?? id ?? `Element ${path}`;
|
|
120
193
|
const { entity, predefinedType = "NOTDEFINED" } = node.elementClass;
|
|
121
|
-
const ifcElement = add(
|
|
122
|
-
m,
|
|
123
|
-
`${entity.toUpperCase()}(${str(guid(m, id))},$,${str(name)},$,$,${ref(placement)},${shape},${str(id)},.${predefinedType}.)`
|
|
124
|
-
);
|
|
194
|
+
const ifcElement = add(m, `${entity.toUpperCase()}(${c.owner(id)},${str(name)},$,$,${ref(placement)},${shape},${str(id)},.${predefinedType}.)`);
|
|
125
195
|
aggregate(
|
|
126
196
|
m,
|
|
197
|
+
c,
|
|
127
198
|
ifcElement,
|
|
128
199
|
node.children.map((child, i) => element(m, child, `${path}.${i + 1}`, placement, c))
|
|
129
200
|
);
|
|
130
201
|
return ifcElement;
|
|
131
202
|
}
|
|
132
203
|
|
|
133
|
-
|
|
204
|
+
/** One face set per mesh, so meshes on different layers are never in the same item */
|
|
205
|
+
function faceSet(m: MutableIfc, { material, triangulation: { points, triangles }, layer }: NodeMesh, c: IfcContext): number {
|
|
134
206
|
const pointList = add(m, `IFCCARTESIANPOINTLIST3D((${points.join(",")}))`);
|
|
135
207
|
const faces = add(m, `IFCTRIANGULATEDFACESET(${ref(pointList)},$,.F.,(${triangles.map((t) => `(${t.join(",")})`).join(",")}),$)`);
|
|
136
208
|
add(m, `IFCSTYLEDITEM(${ref(faces)},(${ref(surfaceStyle(m, material, c))}),$)`);
|
|
209
|
+
if (layer !== undefined) {
|
|
210
|
+
const items = c.layers.get(layer);
|
|
211
|
+
if (items) {
|
|
212
|
+
items.push(faces);
|
|
213
|
+
} else {
|
|
214
|
+
c.layers.set(layer, [faces]);
|
|
215
|
+
}
|
|
216
|
+
}
|
|
137
217
|
return faces;
|
|
138
218
|
}
|
|
139
219
|
|
|
@@ -160,8 +240,8 @@ function axisPlacement(m: MutableIfc, pos: Vec3, rot: Vec3): number {
|
|
|
160
240
|
return add(m, `IFCAXIS2PLACEMENT3D(${ref(location)},${ref(axis)},${ref(refDirection)})`);
|
|
161
241
|
}
|
|
162
242
|
|
|
163
|
-
function aggregate(m: MutableIfc, parent: number, children: ReadonlyArray<number>): void {
|
|
243
|
+
function aggregate(m: MutableIfc, c: IfcContext, parent: number, children: ReadonlyArray<number>): void {
|
|
164
244
|
if (children.length > 0) {
|
|
165
|
-
add(m, `IFCRELAGGREGATES(${
|
|
245
|
+
add(m, `IFCRELAGGREGATES(${c.owner()},$,$,${ref(parent)},${refs(children)})`);
|
|
166
246
|
}
|
|
167
247
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { createSVG } from "abstract-image";
|
|
2
2
|
import React from "react";
|
|
3
3
|
import { suspend } from "suspend-react";
|
|
4
|
-
import { CanvasTexture, DoubleSide, PlaneGeometry, SRGBColorSpace, type Texture, TextureLoader } from "three";
|
|
4
|
+
import { CanvasTexture, DoubleSide, FrontSide, PlaneGeometry, SRGBColorSpace, type Texture, TextureLoader } from "three";
|
|
5
5
|
import type { Material, Image as A3dImage } from "../../abstract-3d.js";
|
|
6
6
|
import type { MaterialState } from "./react-material.js";
|
|
7
7
|
import { ERROR_IMG_KEY, getColor, materialDefaults, selectMat } from "./react-material.js";
|
|
@@ -59,7 +59,7 @@ export function ImageMaterial({
|
|
|
59
59
|
return (
|
|
60
60
|
<meshBasicMaterial
|
|
61
61
|
color={getColor(selected, hovered, material, selectMat)}
|
|
62
|
-
side={DoubleSide}
|
|
62
|
+
side={material.backfaceCulling ? FrontSide : DoubleSide}
|
|
63
63
|
alphaTest={(useAlphaTest ?? true) ? 0.8 : undefined}
|
|
64
64
|
map={texture}
|
|
65
65
|
{...(material.opacity !== undefined && material.opacity < 1 ? { opacity: material.opacity } : materialDefaults)}
|
|
@@ -56,11 +56,11 @@ export function ReactMaterial({
|
|
|
56
56
|
}
|
|
57
57
|
return (
|
|
58
58
|
<meshStandardMaterial
|
|
59
|
-
key={`mesh_material_standard_${mat.normal}_${mat.metalness}_${mat.opacity}_${mat.roughness}`}
|
|
59
|
+
key={`mesh_material_standard_${mat.normal}_${mat.metalness}_${mat.opacity}_${mat.roughness}_${material.backfaceCulling}`}
|
|
60
60
|
color={getColor(selected, hovered, mat, selectMat)}
|
|
61
61
|
roughness={mat.roughness}
|
|
62
62
|
metalness={mat.metalness}
|
|
63
|
-
side={DoubleSide}
|
|
63
|
+
side={material.backfaceCulling ? FrontSide : DoubleSide}
|
|
64
64
|
{...(opacity < 1 || disabled
|
|
65
65
|
? {
|
|
66
66
|
transparent: true,
|