@treasuryspatial/rhino-bridge 0.1.4
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/dist/index.d.ts +40 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +418 -0
- package/package.json +34 -0
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
export type RhinoBridgeOptions = {
|
|
3
|
+
wasmPath?: string;
|
|
4
|
+
wasmUrl?: string;
|
|
5
|
+
rotateToYUp?: boolean;
|
|
6
|
+
transform?: (object: THREE.Object3D) => void;
|
|
7
|
+
roleKey?: string;
|
|
8
|
+
roleKeys?: string[];
|
|
9
|
+
excludeRoles?: string[];
|
|
10
|
+
geometryRole?: string;
|
|
11
|
+
idKey?: string;
|
|
12
|
+
packetKey?: string;
|
|
13
|
+
layerKey?: string;
|
|
14
|
+
materialKey?: string;
|
|
15
|
+
};
|
|
16
|
+
export type LayeredGeometryOptions = {
|
|
17
|
+
mapLayerToId?: (layerName: string, attributes: any) => string | null;
|
|
18
|
+
transform?: (object: THREE.Object3D) => void;
|
|
19
|
+
roleKey?: string;
|
|
20
|
+
roleKeys?: string[];
|
|
21
|
+
excludeRoles?: string[];
|
|
22
|
+
packetKey?: string;
|
|
23
|
+
layerKey?: string;
|
|
24
|
+
materialKey?: string;
|
|
25
|
+
};
|
|
26
|
+
export declare function threeGroupFrom3dm(base64: string, options?: RhinoBridgeOptions): Promise<THREE.Group>;
|
|
27
|
+
export type Rhino3dmInspection = {
|
|
28
|
+
ok: boolean;
|
|
29
|
+
meshCount: number;
|
|
30
|
+
brepCount: number;
|
|
31
|
+
extrusionCount: number;
|
|
32
|
+
otherCount: number;
|
|
33
|
+
};
|
|
34
|
+
export declare function inspect3dmArrayBuffer(buffer: ArrayBuffer | Uint8Array): Promise<Rhino3dmInspection>;
|
|
35
|
+
export declare function inspect3dmBase64(base64: string): Promise<Rhino3dmInspection>;
|
|
36
|
+
export declare function loadLayeredGeometry(geometry3dm: string, options?: LayeredGeometryOptions): Promise<Record<string, THREE.Group>>;
|
|
37
|
+
export declare function extractBrepsByRole(base64: string, options?: RhinoBridgeOptions): Promise<Record<string, string>>;
|
|
38
|
+
declare function convertGeometryToThree(geometry: any): THREE.Object3D | null;
|
|
39
|
+
export { convertGeometryToThree };
|
|
40
|
+
//# sourceMappingURL=index.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,KAAK,MAAM,OAAO,CAAC;AA0B/B,MAAM,MAAM,kBAAkB,GAAG;IAC/B,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,WAAW,CAAC,EAAE,OAAO,CAAC;IACtB,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AA2FF,MAAM,MAAM,sBAAsB,GAAG;IACnC,YAAY,CAAC,EAAE,CAAC,SAAS,EAAE,MAAM,EAAE,UAAU,EAAE,GAAG,KAAK,MAAM,GAAG,IAAI,CAAC;IACrE,SAAS,CAAC,EAAE,CAAC,MAAM,EAAE,KAAK,CAAC,QAAQ,KAAK,IAAI,CAAC;IAC7C,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,QAAQ,CAAC,EAAE,MAAM,EAAE,CAAC;IACpB,YAAY,CAAC,EAAE,MAAM,EAAE,CAAC;IACxB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB,CAAC;AAUF,wBAAsB,iBAAiB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAqD1G;AAED,MAAM,MAAM,kBAAkB,GAAG;IAC/B,EAAE,EAAE,OAAO,CAAC;IACZ,SAAS,EAAE,MAAM,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC;IAClB,cAAc,EAAE,MAAM,CAAC;IACvB,UAAU,EAAE,MAAM,CAAC;CACpB,CAAC;AAsCF,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAQzG;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,kBAAkB,CAAC,CAGlF;AAED,wBAAsB,mBAAmB,CAAC,WAAW,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,KAAK,CAAC,KAAK,CAAC,CAAC,CAyErI;AAED,wBAAsB,kBAAkB,CAAC,MAAM,EAAE,MAAM,EAAE,OAAO,CAAC,EAAE,kBAAkB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAgDtH;AAED,iBAAS,sBAAsB,CAAC,QAAQ,EAAE,GAAG,GAAG,KAAK,CAAC,QAAQ,GAAG,IAAI,CAkBpE;AAyGD,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
|
package/dist/index.js
ADDED
|
@@ -0,0 +1,418 @@
|
|
|
1
|
+
import * as THREE from "three";
|
|
2
|
+
import rhino3dm from "rhino3dm";
|
|
3
|
+
const DEFAULT_BROWSER_WASM_PATH = "/rhino3dm.wasm";
|
|
4
|
+
const DEFAULT_ROLE_KEY = "b2:role";
|
|
5
|
+
const DEFAULT_GEOMETRY_ROLE = "geometry";
|
|
6
|
+
const DEFAULT_ID_KEY = "b2:id";
|
|
7
|
+
const trimTrailingSlashes = (value) => value.replace(/\/+$/, "");
|
|
8
|
+
const trimLeadingSlashes = (value) => value.replace(/^\/+/, "");
|
|
9
|
+
const resolveBrowserWasmUrl = (path, override) => {
|
|
10
|
+
const envBase = override?.trim() || process.env.NEXT_PUBLIC_RHINO3DM_WASM_URL?.trim() || process.env.RHINO3DM_WASM_URL?.trim();
|
|
11
|
+
const normalizedPath = trimLeadingSlashes(path);
|
|
12
|
+
if (envBase && envBase.length > 0) {
|
|
13
|
+
if (envBase.endsWith(".wasm"))
|
|
14
|
+
return envBase;
|
|
15
|
+
const base = trimTrailingSlashes(envBase);
|
|
16
|
+
return `${base}/${normalizedPath}`;
|
|
17
|
+
}
|
|
18
|
+
return `${DEFAULT_BROWSER_WASM_PATH.replace(/[^/]+$/, "")}${normalizedPath}`;
|
|
19
|
+
};
|
|
20
|
+
let rhinoModulePromise = null;
|
|
21
|
+
async function getRhinoModule(options) {
|
|
22
|
+
if (!rhinoModulePromise) {
|
|
23
|
+
if (typeof window === "undefined") {
|
|
24
|
+
const pathModule = await import("path");
|
|
25
|
+
const fs = await import("fs");
|
|
26
|
+
const wasmPath = options?.wasmPath
|
|
27
|
+
? pathModule.resolve(process.cwd(), options.wasmPath)
|
|
28
|
+
: process.env.RHINO3DM_WASM_PATH
|
|
29
|
+
? pathModule.resolve(process.cwd(), process.env.RHINO3DM_WASM_PATH)
|
|
30
|
+
: pathModule.resolve(process.cwd(), "public", "rhino3dm.wasm");
|
|
31
|
+
let wasmBinary = null;
|
|
32
|
+
try {
|
|
33
|
+
const buffer = fs.readFileSync(wasmPath);
|
|
34
|
+
wasmBinary = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
35
|
+
}
|
|
36
|
+
catch (error) {
|
|
37
|
+
console.error("[rhino3dm] Failed to read WASM binary from", wasmPath, error);
|
|
38
|
+
}
|
|
39
|
+
rhinoModulePromise = rhino3dm({
|
|
40
|
+
wasmBinary: wasmBinary ?? undefined,
|
|
41
|
+
locateFile: (path) => (wasmBinary ? wasmPath : resolveBrowserWasmUrl(path, options?.wasmUrl)),
|
|
42
|
+
});
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
rhinoModulePromise = rhino3dm({
|
|
46
|
+
locateFile: (path) => resolveBrowserWasmUrl(path, options?.wasmUrl),
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
return rhinoModulePromise;
|
|
51
|
+
}
|
|
52
|
+
const base64ToUint8Array = (base64) => {
|
|
53
|
+
const normalized = base64.replace(/\s/g, "");
|
|
54
|
+
let binary;
|
|
55
|
+
if (typeof window !== "undefined" && typeof window.atob === "function") {
|
|
56
|
+
binary = window.atob(normalized);
|
|
57
|
+
}
|
|
58
|
+
else if (typeof atob === "function") {
|
|
59
|
+
binary = atob(normalized);
|
|
60
|
+
}
|
|
61
|
+
else {
|
|
62
|
+
binary = Buffer.from(normalized, "base64").toString("binary");
|
|
63
|
+
}
|
|
64
|
+
const length = binary.length;
|
|
65
|
+
const bytes = new Uint8Array(length);
|
|
66
|
+
for (let i = 0; i < length; i += 1) {
|
|
67
|
+
bytes[i] = binary.charCodeAt(i);
|
|
68
|
+
}
|
|
69
|
+
return bytes;
|
|
70
|
+
};
|
|
71
|
+
const uint8ArrayToBase64 = (bytes) => {
|
|
72
|
+
if (typeof Buffer !== "undefined") {
|
|
73
|
+
return Buffer.from(bytes).toString("base64");
|
|
74
|
+
}
|
|
75
|
+
let binary = "";
|
|
76
|
+
const chunkSize = 0x8000;
|
|
77
|
+
for (let i = 0; i < bytes.length; i += chunkSize) {
|
|
78
|
+
const chunk = bytes.subarray(i, i + chunkSize);
|
|
79
|
+
binary += String.fromCharCode.apply(null, chunk);
|
|
80
|
+
}
|
|
81
|
+
if (typeof window !== "undefined" && typeof window.btoa === "function") {
|
|
82
|
+
return window.btoa(binary);
|
|
83
|
+
}
|
|
84
|
+
if (typeof btoa === "function") {
|
|
85
|
+
return btoa(binary);
|
|
86
|
+
}
|
|
87
|
+
throw new Error("No base64 encoder available in this environment.");
|
|
88
|
+
};
|
|
89
|
+
const getUserString = (attributes, key) => {
|
|
90
|
+
try {
|
|
91
|
+
if (attributes && typeof attributes.getUserString === "function") {
|
|
92
|
+
const value = attributes.getUserString(key);
|
|
93
|
+
if (value !== null && value !== undefined && `${value}`.length > 0) {
|
|
94
|
+
return String(value);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
catch (error) {
|
|
99
|
+
console.warn("[rhino3dm] Unable to read user string", key, error);
|
|
100
|
+
}
|
|
101
|
+
return null;
|
|
102
|
+
};
|
|
103
|
+
const resolveUserString = (attributes, keys) => {
|
|
104
|
+
for (const key of keys) {
|
|
105
|
+
const value = getUserString(attributes, key);
|
|
106
|
+
if (value)
|
|
107
|
+
return value;
|
|
108
|
+
}
|
|
109
|
+
return null;
|
|
110
|
+
};
|
|
111
|
+
export async function threeGroupFrom3dm(base64, options) {
|
|
112
|
+
const rhino = await getRhinoModule(options);
|
|
113
|
+
const bytes = base64ToUint8Array(base64);
|
|
114
|
+
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
115
|
+
const group = new THREE.Group();
|
|
116
|
+
if (!doc) {
|
|
117
|
+
console.error("[rhino3dm] Failed to parse 3DM file from bytes");
|
|
118
|
+
return group;
|
|
119
|
+
}
|
|
120
|
+
const rotation = options?.rotateToYUp === false ? null : new THREE.Matrix4().makeRotationX(-Math.PI / 2);
|
|
121
|
+
const roleKeys = options?.roleKeys ?? [];
|
|
122
|
+
const roleKey = options?.roleKey ?? DEFAULT_ROLE_KEY;
|
|
123
|
+
const roleKeyList = roleKeys.length > 0 ? [roleKey, ...roleKeys] : [roleKey];
|
|
124
|
+
const excludeRoles = options?.excludeRoles ?? [];
|
|
125
|
+
const packetKey = options?.packetKey ?? DEFAULT_ID_KEY;
|
|
126
|
+
const layerKey = options?.layerKey ?? "b2:layer";
|
|
127
|
+
const materialKey = options?.materialKey ?? "b2:material";
|
|
128
|
+
const objects = doc.objects();
|
|
129
|
+
const objectCount = objects.count ?? 0;
|
|
130
|
+
for (let i = 0; i < objectCount; i += 1) {
|
|
131
|
+
const obj = objects.get(i);
|
|
132
|
+
const attributes = typeof obj?.attributes === "function" ? obj.attributes() : null;
|
|
133
|
+
const role = attributes ? resolveUserString(attributes, roleKeyList) : null;
|
|
134
|
+
if (role && excludeRoles.includes(role)) {
|
|
135
|
+
continue;
|
|
136
|
+
}
|
|
137
|
+
const geom = obj?.geometry?.();
|
|
138
|
+
const threeObject = convertGeometryToThree(geom);
|
|
139
|
+
if (threeObject) {
|
|
140
|
+
if (rotation) {
|
|
141
|
+
threeObject.applyMatrix4(rotation);
|
|
142
|
+
}
|
|
143
|
+
const packetId = attributes ? getUserString(attributes, packetKey) : null;
|
|
144
|
+
const layer = attributes ? getUserString(attributes, layerKey) : null;
|
|
145
|
+
const material = attributes ? getUserString(attributes, materialKey) : null;
|
|
146
|
+
threeObject.userData = {
|
|
147
|
+
role: role ?? undefined,
|
|
148
|
+
packet: packetId ?? undefined,
|
|
149
|
+
id: packetId ?? undefined,
|
|
150
|
+
layer: layer ?? undefined,
|
|
151
|
+
material: material ?? undefined,
|
|
152
|
+
};
|
|
153
|
+
options?.transform?.(threeObject);
|
|
154
|
+
group.add(threeObject);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
return group;
|
|
158
|
+
}
|
|
159
|
+
const countRhinoObjects = (doc) => {
|
|
160
|
+
const objects = doc?.objects?.();
|
|
161
|
+
const total = objects?.count ?? 0;
|
|
162
|
+
let meshCount = 0;
|
|
163
|
+
let brepCount = 0;
|
|
164
|
+
let extrusionCount = 0;
|
|
165
|
+
let otherCount = 0;
|
|
166
|
+
for (let i = 0; i < total; i += 1) {
|
|
167
|
+
const obj = objects.get(i);
|
|
168
|
+
const geom = obj?.geometry?.();
|
|
169
|
+
const typeName = geom?.constructor?.name ?? "";
|
|
170
|
+
if (typeName === "Mesh") {
|
|
171
|
+
meshCount += 1;
|
|
172
|
+
continue;
|
|
173
|
+
}
|
|
174
|
+
if (typeName === "Brep") {
|
|
175
|
+
brepCount += 1;
|
|
176
|
+
continue;
|
|
177
|
+
}
|
|
178
|
+
if (typeName === "Extrusion") {
|
|
179
|
+
extrusionCount += 1;
|
|
180
|
+
continue;
|
|
181
|
+
}
|
|
182
|
+
otherCount += 1;
|
|
183
|
+
}
|
|
184
|
+
return {
|
|
185
|
+
ok: true,
|
|
186
|
+
meshCount,
|
|
187
|
+
brepCount,
|
|
188
|
+
extrusionCount,
|
|
189
|
+
otherCount,
|
|
190
|
+
};
|
|
191
|
+
};
|
|
192
|
+
export async function inspect3dmArrayBuffer(buffer) {
|
|
193
|
+
const rhino = await getRhinoModule();
|
|
194
|
+
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
195
|
+
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
196
|
+
if (!doc) {
|
|
197
|
+
return { ok: false, meshCount: 0, brepCount: 0, extrusionCount: 0, otherCount: 0 };
|
|
198
|
+
}
|
|
199
|
+
return countRhinoObjects(doc);
|
|
200
|
+
}
|
|
201
|
+
export async function inspect3dmBase64(base64) {
|
|
202
|
+
const bytes = base64ToUint8Array(base64);
|
|
203
|
+
return inspect3dmArrayBuffer(bytes);
|
|
204
|
+
}
|
|
205
|
+
export async function loadLayeredGeometry(geometry3dm, options) {
|
|
206
|
+
const rhino = await getRhinoModule();
|
|
207
|
+
const bytes = base64ToUint8Array(geometry3dm);
|
|
208
|
+
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
209
|
+
const groups = {};
|
|
210
|
+
if (!doc) {
|
|
211
|
+
console.error("[rhino3dm] Failed to parse 3DM file");
|
|
212
|
+
return groups;
|
|
213
|
+
}
|
|
214
|
+
const layerTable = doc.layers?.();
|
|
215
|
+
const layerNames = new Map();
|
|
216
|
+
if (layerTable && typeof layerTable.count === "number") {
|
|
217
|
+
for (let i = 0; i < layerTable.count; i += 1) {
|
|
218
|
+
const layer = layerTable.get(i);
|
|
219
|
+
if (!layer)
|
|
220
|
+
continue;
|
|
221
|
+
const index = typeof layer.index === "number" ? layer.index : i;
|
|
222
|
+
layerNames.set(index, layer.name ?? "");
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
const roleKeys = options?.roleKeys ?? [];
|
|
226
|
+
const roleKey = options?.roleKey ?? DEFAULT_ROLE_KEY;
|
|
227
|
+
const roleKeyList = roleKeys.length > 0 ? [roleKey, ...roleKeys] : [roleKey];
|
|
228
|
+
const excludeRoles = options?.excludeRoles ?? [DEFAULT_GEOMETRY_ROLE];
|
|
229
|
+
const packetKey = options?.packetKey ?? DEFAULT_ID_KEY;
|
|
230
|
+
const layerKey = options?.layerKey ?? "b2:layer";
|
|
231
|
+
const materialKey = options?.materialKey ?? "b2:material";
|
|
232
|
+
const objects = doc.objects();
|
|
233
|
+
const objectCount = objects.count ?? 0;
|
|
234
|
+
for (let i = 0; i < objectCount; i += 1) {
|
|
235
|
+
const obj = objects.get(i);
|
|
236
|
+
if (!obj)
|
|
237
|
+
continue;
|
|
238
|
+
const attributes = typeof obj.attributes === "function" ? obj.attributes() : null;
|
|
239
|
+
const role = attributes ? resolveUserString(attributes, roleKeyList) : null;
|
|
240
|
+
if (role && excludeRoles.includes(role)) {
|
|
241
|
+
continue;
|
|
242
|
+
}
|
|
243
|
+
const geom = obj.geometry?.();
|
|
244
|
+
const threeObject = convertGeometryToThree(geom);
|
|
245
|
+
if (!threeObject)
|
|
246
|
+
continue;
|
|
247
|
+
const packetId = attributes ? getUserString(attributes, packetKey) : null;
|
|
248
|
+
const layer = attributes ? getUserString(attributes, layerKey) : null;
|
|
249
|
+
const material = attributes ? getUserString(attributes, materialKey) : null;
|
|
250
|
+
threeObject.userData = {
|
|
251
|
+
role: role ?? undefined,
|
|
252
|
+
packet: packetId ?? undefined,
|
|
253
|
+
id: packetId ?? undefined,
|
|
254
|
+
layer: layer ?? undefined,
|
|
255
|
+
material: material ?? undefined,
|
|
256
|
+
};
|
|
257
|
+
options?.transform?.(threeObject);
|
|
258
|
+
const layerIndex = attributes?.layerIndex ?? attributes?.LayerIndex ?? -1;
|
|
259
|
+
const layerName = layerNames.get(layerIndex) ?? "";
|
|
260
|
+
const groupId = options?.mapLayerToId?.(layerName, attributes) ?? layerName;
|
|
261
|
+
if (!groupId)
|
|
262
|
+
continue;
|
|
263
|
+
if (!groups[groupId]) {
|
|
264
|
+
groups[groupId] = new THREE.Group();
|
|
265
|
+
}
|
|
266
|
+
groups[groupId].add(threeObject);
|
|
267
|
+
}
|
|
268
|
+
return groups;
|
|
269
|
+
}
|
|
270
|
+
export async function extractBrepsByRole(base64, options) {
|
|
271
|
+
const rhino = await getRhinoModule(options);
|
|
272
|
+
const bytes = base64ToUint8Array(base64);
|
|
273
|
+
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
274
|
+
const breps = {};
|
|
275
|
+
if (!doc) {
|
|
276
|
+
console.error("[rhino3dm] Failed to parse 3DM file for Brep extraction");
|
|
277
|
+
return breps;
|
|
278
|
+
}
|
|
279
|
+
const roleKeys = options?.roleKeys ?? [];
|
|
280
|
+
const roleKey = options?.roleKey ?? DEFAULT_ROLE_KEY;
|
|
281
|
+
const roleKeyList = roleKeys.length > 0 ? [roleKey, ...roleKeys] : [roleKey];
|
|
282
|
+
const geometryRole = options?.geometryRole ?? DEFAULT_GEOMETRY_ROLE;
|
|
283
|
+
const idKey = options?.idKey ?? DEFAULT_ID_KEY;
|
|
284
|
+
const objects = doc.objects();
|
|
285
|
+
const objectCount = objects.count ?? 0;
|
|
286
|
+
for (let i = 0; i < objectCount; i += 1) {
|
|
287
|
+
const obj = objects.get(i);
|
|
288
|
+
if (!obj)
|
|
289
|
+
continue;
|
|
290
|
+
const attributes = typeof obj.attributes === "function" ? obj.attributes() : null;
|
|
291
|
+
const role = attributes ? resolveUserString(attributes, roleKeyList) : null;
|
|
292
|
+
if (role !== geometryRole)
|
|
293
|
+
continue;
|
|
294
|
+
const itemId = attributes ? getUserString(attributes, idKey) : null;
|
|
295
|
+
if (!itemId)
|
|
296
|
+
continue;
|
|
297
|
+
const geom = obj.geometry?.();
|
|
298
|
+
if (!geom)
|
|
299
|
+
continue;
|
|
300
|
+
const typeName = geom.constructor?.name;
|
|
301
|
+
if (typeName !== "Brep") {
|
|
302
|
+
continue;
|
|
303
|
+
}
|
|
304
|
+
const brepDoc = new rhino.File3dm();
|
|
305
|
+
const attr = new rhino.ObjectAttributes();
|
|
306
|
+
brepDoc.objects().add(geom, attr);
|
|
307
|
+
const brepBytes = brepDoc.toByteArray();
|
|
308
|
+
if (!brepBytes)
|
|
309
|
+
continue;
|
|
310
|
+
breps[itemId] = uint8ArrayToBase64(new Uint8Array(brepBytes));
|
|
311
|
+
}
|
|
312
|
+
return breps;
|
|
313
|
+
}
|
|
314
|
+
function convertGeometryToThree(geometry) {
|
|
315
|
+
if (!geometry) {
|
|
316
|
+
return null;
|
|
317
|
+
}
|
|
318
|
+
const typeName = geometry.constructor?.name;
|
|
319
|
+
if (typeName === "Mesh") {
|
|
320
|
+
return meshToThree(geometry);
|
|
321
|
+
}
|
|
322
|
+
if (typeName === "Brep" || typeName === "Extrusion") {
|
|
323
|
+
console.warn("[rhino3dm] Non-meshed geometry received. Ensure server-side meshing before serialization.");
|
|
324
|
+
return null;
|
|
325
|
+
}
|
|
326
|
+
console.warn(`[rhino3dm] Unsupported geometry type: ${typeName}`);
|
|
327
|
+
return null;
|
|
328
|
+
}
|
|
329
|
+
function meshToThree(mesh) {
|
|
330
|
+
const vertices = mesh.vertices?.();
|
|
331
|
+
const faces = mesh.faces?.();
|
|
332
|
+
if (!vertices || !faces) {
|
|
333
|
+
console.warn("[rhino3dm] Mesh missing vertices or faces");
|
|
334
|
+
return null;
|
|
335
|
+
}
|
|
336
|
+
const vertexCount = vertices.count ?? 0;
|
|
337
|
+
const faceCount = faces.count ?? 0;
|
|
338
|
+
if (vertexCount === 0 || faceCount === 0) {
|
|
339
|
+
console.warn("[rhino3dm] Mesh received with no vertices or faces; skipping");
|
|
340
|
+
return null;
|
|
341
|
+
}
|
|
342
|
+
const geometry = new THREE.BufferGeometry();
|
|
343
|
+
const positionArray = new Float32Array(vertexCount * 3);
|
|
344
|
+
let wroteAllVertices = true;
|
|
345
|
+
if (typeof vertices.toFloatArray === "function") {
|
|
346
|
+
const flat = vertices.toFloatArray();
|
|
347
|
+
if (flat?.length === vertexCount * 3) {
|
|
348
|
+
positionArray.set(flat);
|
|
349
|
+
}
|
|
350
|
+
else {
|
|
351
|
+
wroteAllVertices = false;
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
else {
|
|
355
|
+
wroteAllVertices = false;
|
|
356
|
+
}
|
|
357
|
+
if (!wroteAllVertices) {
|
|
358
|
+
const writeVertex = (targetIndex, vertex) => {
|
|
359
|
+
if (!vertex) {
|
|
360
|
+
return false;
|
|
361
|
+
}
|
|
362
|
+
const x = typeof vertex.x === "number" ? vertex.x : typeof vertex[0] === "number" ? vertex[0] : Number.NaN;
|
|
363
|
+
const y = typeof vertex.y === "number" ? vertex.y : typeof vertex[1] === "number" ? vertex[1] : Number.NaN;
|
|
364
|
+
const z = typeof vertex.z === "number" ? vertex.z : typeof vertex[2] === "number" ? vertex[2] : Number.NaN;
|
|
365
|
+
if (!Number.isFinite(x) || !Number.isFinite(y) || !Number.isFinite(z)) {
|
|
366
|
+
return false;
|
|
367
|
+
}
|
|
368
|
+
positionArray[targetIndex * 3 + 0] = x;
|
|
369
|
+
positionArray[targetIndex * 3 + 1] = y;
|
|
370
|
+
positionArray[targetIndex * 3 + 2] = z;
|
|
371
|
+
return true;
|
|
372
|
+
};
|
|
373
|
+
for (let i = 0; i < vertexCount; i += 1) {
|
|
374
|
+
if (!writeVertex(i, vertices.get(i))) {
|
|
375
|
+
console.warn("[rhino3dm] Invalid vertex encountered; skipping mesh");
|
|
376
|
+
return null;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
379
|
+
}
|
|
380
|
+
else {
|
|
381
|
+
for (let i = 0; i < positionArray.length; i += 1) {
|
|
382
|
+
if (!Number.isFinite(positionArray[i])) {
|
|
383
|
+
console.warn("[rhino3dm] Mesh float array contained non-finite values; skipping mesh");
|
|
384
|
+
return null;
|
|
385
|
+
}
|
|
386
|
+
}
|
|
387
|
+
}
|
|
388
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positionArray, 3));
|
|
389
|
+
const indexArray = [];
|
|
390
|
+
for (let i = 0; i < faceCount; i += 1) {
|
|
391
|
+
const face = faces.get(i);
|
|
392
|
+
if (!face)
|
|
393
|
+
continue;
|
|
394
|
+
const a = face[0];
|
|
395
|
+
const b = face[1];
|
|
396
|
+
const c = face[2];
|
|
397
|
+
const d = face.length > 3 ? face[3] : undefined;
|
|
398
|
+
if (![a, b, c].every((idx) => Number.isInteger(idx) && idx >= 0 && idx < vertexCount)) {
|
|
399
|
+
console.warn("[rhino3dm] Face reference out of bounds; skipping mesh");
|
|
400
|
+
return null;
|
|
401
|
+
}
|
|
402
|
+
indexArray.push(a, b, c);
|
|
403
|
+
if (typeof d === "number" && d >= 0 && d < vertexCount) {
|
|
404
|
+
indexArray.push(a, c, d);
|
|
405
|
+
}
|
|
406
|
+
}
|
|
407
|
+
if (indexArray.length === 0) {
|
|
408
|
+
console.warn("[rhino3dm] Mesh produced no triangle indices; skipping");
|
|
409
|
+
return null;
|
|
410
|
+
}
|
|
411
|
+
geometry.setIndex(indexArray);
|
|
412
|
+
geometry.computeVertexNormals();
|
|
413
|
+
const meshObject = new THREE.Mesh(geometry, new THREE.MeshStandardMaterial({ color: 0xffffff }));
|
|
414
|
+
meshObject.castShadow = true;
|
|
415
|
+
meshObject.receiveShadow = true;
|
|
416
|
+
return meshObject;
|
|
417
|
+
}
|
|
418
|
+
export { convertGeometryToThree };
|
package/package.json
ADDED
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@treasuryspatial/rhino-bridge",
|
|
3
|
+
"version": "0.1.4",
|
|
4
|
+
"type": "module",
|
|
5
|
+
"license": "UNLICENSED",
|
|
6
|
+
"main": "./dist/index.js",
|
|
7
|
+
"types": "./dist/index.d.ts",
|
|
8
|
+
"exports": {
|
|
9
|
+
".": {
|
|
10
|
+
"types": "./dist/index.d.ts",
|
|
11
|
+
"default": "./dist/index.js"
|
|
12
|
+
}
|
|
13
|
+
},
|
|
14
|
+
"files": [
|
|
15
|
+
"dist"
|
|
16
|
+
],
|
|
17
|
+
"publishConfig": {
|
|
18
|
+
"access": "public"
|
|
19
|
+
},
|
|
20
|
+
"dependencies": {
|
|
21
|
+
"rhino3dm": "^8.17.0"
|
|
22
|
+
},
|
|
23
|
+
"peerDependencies": {
|
|
24
|
+
"three": "^0.180.0"
|
|
25
|
+
},
|
|
26
|
+
"devDependencies": {
|
|
27
|
+
"@types/node": "^20",
|
|
28
|
+
"three": "^0.180.0"
|
|
29
|
+
},
|
|
30
|
+
"scripts": {
|
|
31
|
+
"build": "tsc -b",
|
|
32
|
+
"typecheck": "tsc -b --pretty false --noEmit"
|
|
33
|
+
}
|
|
34
|
+
}
|