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.
@@ -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
- type IfcContext = { readonly body: number; readonly styles: Map<string, number> };
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` becomes an
46
- * IfcBuildingElementProxy placed relative to its parent element and aggregated into it. Groups without `data` are only transforms, so their meshes
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(${str(guid(m))},$,${str(name)},$,$,$,$,(${ref(context)}),${ref(add(m, `IFCUNITASSIGNMENT(${refs(units)})`))})`);
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(${str(guid(m))},$,'Site',$,$,${ref(sitePlacement)},$,$,.ELEMENT.,$,$,$,$,$)`);
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(${str(guid(m))},$,'Building',$,$,${ref(buildingPlacement)},$,$,.ELEMENT.,$,$,$)`);
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(${str(guid(m))},$,'Storey',$,$,${ref(storeyPlacement)},$,$,.ELEMENT.,0.)`);
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 c: IfcContext = { body, styles: new Map() };
77
- const nodes = options.root
78
- ? [elementNode({ pos: center, rot: sceneRot, groups: scene.groups, data: { name: options.root.name } }, center, sceneRot, options.root)]
79
- : scene.groups.map((g) => elementNode(g, vec3TransRot(g.pos, center, sceneRot), vec3RotCombine(sceneRot, g.rot ?? vec3Zero)));
117
+ const top: Layering = { layerOf: options.layer, path: [], layer: undefined };
118
+ const rootGroup: Group | undefined = options.root && { pos: center, rot: sceneRot, groups: scene.groups, data: { name: options.root.name } };
119
+ const nodes = rootGroup
120
+ ? [elementNode(rootGroup, center, sceneRot, groupLayering(rootGroup, top), options.root)]
121
+ : scene.groups.map((g) =>
122
+ elementNode(g, vec3TransRot(g.pos, center, sceneRot), vec3RotCombine(sceneRot, g.rot ?? vec3Zero), groupLayering(g, top))
123
+ );
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(${str(guid(m))},$,$,$,${refs(elements)},${ref(storey)})`);
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 = new Date().toISOString().split(".")[0] ?? "1970-01-01T00:00:00";
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
- function elementNode(group: Group, pos: Vec3, rot: Vec3, elementClass = proxyClass): ElementNode | undefined {
90
- const { meshes, children } = elementContent(group, vec3Zero, vec3Zero);
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
- /** Meshes of the group and its descendants without data, relative to the element the group is at pos/rot in, and the descendants with data as elements */
95
- function elementContent(group: Group, pos: Vec3, rot: Vec3): Pick<ElementNode, "meshes" | "children"> {
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
- return Object.keys(child.data ?? {}).length > 0
104
- ? { meshes: [], children: [elementNode(child, childPos, childRot)].flatMap((node) => node ?? []) }
105
- : elementContent(child, childPos, childRot);
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(({ material, triangulation }) => faceSet(m, triangulation, material, c));
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
- function faceSet(m: MutableIfc, { points, triangles }: Triangulation, material: Material, c: IfcContext): number {
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(${str(guid(m))},$,$,$,${ref(parent)},${refs(children)})`);
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,