@treasuryspatial/rhino-bridge 0.1.4 → 0.1.5
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 +25 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +316 -35
- package/package.json +1 -1
package/dist/index.d.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
|
-
export type
|
|
2
|
+
export type GeometryBridgeOptions = {
|
|
3
3
|
wasmPath?: string;
|
|
4
4
|
wasmUrl?: string;
|
|
5
5
|
rotateToYUp?: boolean;
|
|
@@ -12,6 +12,7 @@ export type RhinoBridgeOptions = {
|
|
|
12
12
|
packetKey?: string;
|
|
13
13
|
layerKey?: string;
|
|
14
14
|
materialKey?: string;
|
|
15
|
+
curveMode?: "auto" | "always" | "never";
|
|
15
16
|
};
|
|
16
17
|
export type LayeredGeometryOptions = {
|
|
17
18
|
mapLayerToId?: (layerName: string, attributes: any) => string | null;
|
|
@@ -23,18 +24,35 @@ export type LayeredGeometryOptions = {
|
|
|
23
24
|
layerKey?: string;
|
|
24
25
|
materialKey?: string;
|
|
25
26
|
};
|
|
26
|
-
export declare function threeGroupFrom3dm(base64: string, options?:
|
|
27
|
-
export type
|
|
27
|
+
export declare function threeGroupFrom3dm(base64: string, options?: GeometryBridgeOptions): Promise<THREE.Group>;
|
|
28
|
+
export type Geometry3dmInspection = {
|
|
28
29
|
ok: boolean;
|
|
29
30
|
meshCount: number;
|
|
30
31
|
brepCount: number;
|
|
31
32
|
extrusionCount: number;
|
|
32
33
|
otherCount: number;
|
|
33
34
|
};
|
|
34
|
-
export
|
|
35
|
-
|
|
35
|
+
export type GrasshopperTo3dmStats = {
|
|
36
|
+
decodedObjects: number;
|
|
37
|
+
meshesAdded: number;
|
|
38
|
+
brepsMeshed: number;
|
|
39
|
+
brepsSkipped: number;
|
|
40
|
+
unsupported: Record<string, number>;
|
|
41
|
+
errors: string[];
|
|
42
|
+
};
|
|
43
|
+
export declare function grasshopperResultTo3dmBase64(result: {
|
|
44
|
+
values?: any[];
|
|
45
|
+
} | null | undefined): Promise<{
|
|
46
|
+
geometry3dm: string;
|
|
47
|
+
stats: GrasshopperTo3dmStats;
|
|
48
|
+
}>;
|
|
49
|
+
export declare function inspect3dmArrayBuffer(buffer: ArrayBuffer | Uint8Array): Promise<Geometry3dmInspection>;
|
|
50
|
+
export declare function inspect3dmBase64(base64: string): Promise<Geometry3dmInspection>;
|
|
36
51
|
export declare function loadLayeredGeometry(geometry3dm: string, options?: LayeredGeometryOptions): Promise<Record<string, THREE.Group>>;
|
|
37
|
-
export declare function extractBrepsByRole(base64: string, options?:
|
|
38
|
-
|
|
52
|
+
export declare function extractBrepsByRole(base64: string, options?: GeometryBridgeOptions): Promise<Record<string, string>>;
|
|
53
|
+
type ConvertGeometryOptions = {
|
|
54
|
+
allowCurves?: boolean;
|
|
55
|
+
};
|
|
56
|
+
declare function convertGeometryToThree(geometry: any, options?: ConvertGeometryOptions): THREE.Object3D | null;
|
|
39
57
|
export { convertGeometryToThree };
|
|
40
58
|
//# sourceMappingURL=index.d.ts.map
|
package/dist/index.d.ts.map
CHANGED
|
@@ -1 +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,
|
|
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,qBAAqB,GAAG;IAClC,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;IACrB,SAAS,CAAC,EAAE,MAAM,GAAG,QAAQ,GAAG,OAAO,CAAC;CACzC,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,qBAAqB,GAAG,OAAO,CAAC,KAAK,CAAC,KAAK,CAAC,CAqE7G;AAED,MAAM,MAAM,qBAAqB,GAAG;IAClC,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;AAEF,MAAM,MAAM,qBAAqB,GAAG;IAClC,cAAc,EAAE,MAAM,CAAC;IACvB,WAAW,EAAE,MAAM,CAAC;IACpB,WAAW,EAAE,MAAM,CAAC;IACpB,YAAY,EAAE,MAAM,CAAC;IACrB,WAAW,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACpC,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAEF,wBAAsB,4BAA4B,CAChD,MAAM,EAAE;IAAE,MAAM,CAAC,EAAE,GAAG,EAAE,CAAA;CAAE,GAAG,IAAI,GAAG,SAAS,GAC5C,OAAO,CAAC;IAAE,WAAW,EAAE,MAAM,CAAC;IAAC,KAAK,EAAE,qBAAqB,CAAA;CAAE,CAAC,CAuHhE;AAsCD,wBAAsB,qBAAqB,CAAC,MAAM,EAAE,WAAW,GAAG,UAAU,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAQ5G;AAED,wBAAsB,gBAAgB,CAAC,MAAM,EAAE,MAAM,GAAG,OAAO,CAAC,qBAAqB,CAAC,CAGrF;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,qBAAqB,GAAG,OAAO,CAAC,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC,CAgDzH;AAED,KAAK,sBAAsB,GAAG;IAC5B,WAAW,CAAC,EAAE,OAAO,CAAC;CACvB,CAAC;AAgIF,iBAAS,sBAAsB,CAAC,QAAQ,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE,sBAAsB,GAAG,KAAK,CAAC,QAAQ,GAAG,IAAI,CAuCtG;AAyGD,OAAO,EAAE,sBAAsB,EAAE,CAAC"}
|
package/dist/index.js
CHANGED
|
@@ -1,53 +1,53 @@
|
|
|
1
1
|
import * as THREE from "three";
|
|
2
2
|
import rhino3dm from "rhino3dm";
|
|
3
|
-
const DEFAULT_BROWSER_WASM_PATH = "/
|
|
3
|
+
const DEFAULT_BROWSER_WASM_PATH = "/geometry.wasm";
|
|
4
|
+
const DEFAULT_BROWSER_WASM_FILENAME = DEFAULT_BROWSER_WASM_PATH.split("/").pop() ?? "geometry.wasm";
|
|
4
5
|
const DEFAULT_ROLE_KEY = "b2:role";
|
|
5
6
|
const DEFAULT_GEOMETRY_ROLE = "geometry";
|
|
6
7
|
const DEFAULT_ID_KEY = "b2:id";
|
|
7
8
|
const trimTrailingSlashes = (value) => value.replace(/\/+$/, "");
|
|
8
9
|
const trimLeadingSlashes = (value) => value.replace(/^\/+/, "");
|
|
9
10
|
const resolveBrowserWasmUrl = (path, override) => {
|
|
10
|
-
const envBase = override?.trim() || process.env.
|
|
11
|
-
const normalizedPath = trimLeadingSlashes(path);
|
|
11
|
+
const envBase = override?.trim() || process.env.NEXT_PUBLIC_GEOMETRY_WASM_URL?.trim() || process.env.GEOMETRY_WASM_URL?.trim();
|
|
12
12
|
if (envBase && envBase.length > 0) {
|
|
13
13
|
if (envBase.endsWith(".wasm"))
|
|
14
14
|
return envBase;
|
|
15
15
|
const base = trimTrailingSlashes(envBase);
|
|
16
|
-
return `${base}/${
|
|
16
|
+
return `${base}/${DEFAULT_BROWSER_WASM_FILENAME}`;
|
|
17
17
|
}
|
|
18
|
-
return
|
|
18
|
+
return DEFAULT_BROWSER_WASM_PATH;
|
|
19
19
|
};
|
|
20
|
-
let
|
|
21
|
-
async function
|
|
22
|
-
if (!
|
|
20
|
+
let geometryModulePromise = null;
|
|
21
|
+
async function getGeometryModule(options) {
|
|
22
|
+
if (!geometryModulePromise) {
|
|
23
23
|
if (typeof window === "undefined") {
|
|
24
24
|
const pathModule = await import("path");
|
|
25
25
|
const fs = await import("fs");
|
|
26
26
|
const wasmPath = options?.wasmPath
|
|
27
27
|
? pathModule.resolve(process.cwd(), options.wasmPath)
|
|
28
|
-
: process.env.
|
|
29
|
-
? pathModule.resolve(process.cwd(), process.env.
|
|
30
|
-
: pathModule.resolve(process.cwd(), "public", "
|
|
28
|
+
: process.env.GEOMETRY_WASM_PATH
|
|
29
|
+
? pathModule.resolve(process.cwd(), process.env.GEOMETRY_WASM_PATH)
|
|
30
|
+
: pathModule.resolve(process.cwd(), "public", "geometry.wasm");
|
|
31
31
|
let wasmBinary = null;
|
|
32
32
|
try {
|
|
33
33
|
const buffer = fs.readFileSync(wasmPath);
|
|
34
34
|
wasmBinary = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
35
35
|
}
|
|
36
36
|
catch (error) {
|
|
37
|
-
console.error("[
|
|
37
|
+
console.error("[geometry] Failed to read WASM binary from", wasmPath, error);
|
|
38
38
|
}
|
|
39
|
-
|
|
39
|
+
geometryModulePromise = rhino3dm({
|
|
40
40
|
wasmBinary: wasmBinary ?? undefined,
|
|
41
41
|
locateFile: (path) => (wasmBinary ? wasmPath : resolveBrowserWasmUrl(path, options?.wasmUrl)),
|
|
42
42
|
});
|
|
43
43
|
}
|
|
44
44
|
else {
|
|
45
|
-
|
|
45
|
+
geometryModulePromise = rhino3dm({
|
|
46
46
|
locateFile: (path) => resolveBrowserWasmUrl(path, options?.wasmUrl),
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
}
|
|
50
|
-
return
|
|
50
|
+
return geometryModulePromise;
|
|
51
51
|
}
|
|
52
52
|
const base64ToUint8Array = (base64) => {
|
|
53
53
|
const normalized = base64.replace(/\s/g, "");
|
|
@@ -96,7 +96,7 @@ const getUserString = (attributes, key) => {
|
|
|
96
96
|
}
|
|
97
97
|
}
|
|
98
98
|
catch (error) {
|
|
99
|
-
console.warn("[
|
|
99
|
+
console.warn("[geometry] Unable to read user string", key, error);
|
|
100
100
|
}
|
|
101
101
|
return null;
|
|
102
102
|
};
|
|
@@ -109,12 +109,12 @@ const resolveUserString = (attributes, keys) => {
|
|
|
109
109
|
return null;
|
|
110
110
|
};
|
|
111
111
|
export async function threeGroupFrom3dm(base64, options) {
|
|
112
|
-
const rhino = await
|
|
112
|
+
const rhino = await getGeometryModule(options);
|
|
113
113
|
const bytes = base64ToUint8Array(base64);
|
|
114
114
|
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
115
115
|
const group = new THREE.Group();
|
|
116
116
|
if (!doc) {
|
|
117
|
-
console.error("[
|
|
117
|
+
console.error("[geometry] Failed to parse 3DM file from bytes");
|
|
118
118
|
return group;
|
|
119
119
|
}
|
|
120
120
|
const rotation = options?.rotateToYUp === false ? null : new THREE.Matrix4().makeRotationX(-Math.PI / 2);
|
|
@@ -127,6 +127,23 @@ export async function threeGroupFrom3dm(base64, options) {
|
|
|
127
127
|
const materialKey = options?.materialKey ?? "b2:material";
|
|
128
128
|
const objects = doc.objects();
|
|
129
129
|
const objectCount = objects.count ?? 0;
|
|
130
|
+
const curveMode = options?.curveMode ?? "auto";
|
|
131
|
+
let allowCurves = curveMode === "always";
|
|
132
|
+
if (curveMode === "auto") {
|
|
133
|
+
allowCurves = true;
|
|
134
|
+
for (let i = 0; i < objectCount; i += 1) {
|
|
135
|
+
const obj = objects.get(i);
|
|
136
|
+
const geom = obj?.geometry?.();
|
|
137
|
+
const typeName = geom?.constructor?.name;
|
|
138
|
+
if (typeName === "Mesh") {
|
|
139
|
+
allowCurves = false;
|
|
140
|
+
break;
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
else if (curveMode === "never") {
|
|
145
|
+
allowCurves = false;
|
|
146
|
+
}
|
|
130
147
|
for (let i = 0; i < objectCount; i += 1) {
|
|
131
148
|
const obj = objects.get(i);
|
|
132
149
|
const attributes = typeof obj?.attributes === "function" ? obj.attributes() : null;
|
|
@@ -135,7 +152,7 @@ export async function threeGroupFrom3dm(base64, options) {
|
|
|
135
152
|
continue;
|
|
136
153
|
}
|
|
137
154
|
const geom = obj?.geometry?.();
|
|
138
|
-
const threeObject = convertGeometryToThree(geom);
|
|
155
|
+
const threeObject = convertGeometryToThree(geom, { allowCurves });
|
|
139
156
|
if (threeObject) {
|
|
140
157
|
if (rotation) {
|
|
141
158
|
threeObject.applyMatrix4(rotation);
|
|
@@ -156,7 +173,124 @@ export async function threeGroupFrom3dm(base64, options) {
|
|
|
156
173
|
}
|
|
157
174
|
return group;
|
|
158
175
|
}
|
|
159
|
-
|
|
176
|
+
export async function grasshopperResultTo3dmBase64(result) {
|
|
177
|
+
const rhino = await getGeometryModule();
|
|
178
|
+
const stats = {
|
|
179
|
+
decodedObjects: 0,
|
|
180
|
+
meshesAdded: 0,
|
|
181
|
+
brepsMeshed: 0,
|
|
182
|
+
brepsSkipped: 0,
|
|
183
|
+
unsupported: {},
|
|
184
|
+
errors: [],
|
|
185
|
+
};
|
|
186
|
+
const doc = new rhino.File3dm();
|
|
187
|
+
const values = Array.isArray(result?.values) ? result?.values ?? [] : [];
|
|
188
|
+
if (!values.length) {
|
|
189
|
+
return { geometry3dm: "", stats };
|
|
190
|
+
}
|
|
191
|
+
const addObject = (geometry, name) => {
|
|
192
|
+
try {
|
|
193
|
+
if (name && rhino.ObjectAttributes) {
|
|
194
|
+
const attributes = new rhino.ObjectAttributes();
|
|
195
|
+
attributes.name = name;
|
|
196
|
+
doc.objects().add(geometry, attributes);
|
|
197
|
+
}
|
|
198
|
+
else {
|
|
199
|
+
doc.objects().add(geometry, null);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
catch (e) {
|
|
203
|
+
stats.errors.push(`Failed to add object to 3dm: ${e?.message ?? String(e)}`);
|
|
204
|
+
}
|
|
205
|
+
};
|
|
206
|
+
const tryMeshBrep = (brep) => {
|
|
207
|
+
try {
|
|
208
|
+
const createFromBrep = rhino.Mesh?.createFromBrep;
|
|
209
|
+
if (typeof createFromBrep !== "function")
|
|
210
|
+
return null;
|
|
211
|
+
const mp = rhino.MeshingParameters?.default ?? null;
|
|
212
|
+
try {
|
|
213
|
+
const meshes = mp ? createFromBrep(brep, mp) : createFromBrep(brep);
|
|
214
|
+
return Array.isArray(meshes) ? meshes.filter(Boolean) : null;
|
|
215
|
+
}
|
|
216
|
+
catch {
|
|
217
|
+
const meshes = createFromBrep(brep);
|
|
218
|
+
return Array.isArray(meshes) ? meshes.filter(Boolean) : null;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
catch (e) {
|
|
222
|
+
stats.errors.push(`Failed to mesh Brep: ${e?.message ?? String(e)}`);
|
|
223
|
+
return null;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
for (const output of values) {
|
|
227
|
+
const paramName = String(output?.ParamName ?? "output");
|
|
228
|
+
const tree = output?.InnerTree ?? {};
|
|
229
|
+
for (const path of Object.keys(tree)) {
|
|
230
|
+
const items = Array.isArray(tree[path]) ? tree[path] : [];
|
|
231
|
+
for (let idx = 0; idx < items.length; idx += 1) {
|
|
232
|
+
const item = items[idx];
|
|
233
|
+
const itemType = String(item?.type ?? "");
|
|
234
|
+
const rawData = item?.data;
|
|
235
|
+
if (!itemType.startsWith("Rhino.Geometry"))
|
|
236
|
+
continue;
|
|
237
|
+
let json = null;
|
|
238
|
+
try {
|
|
239
|
+
if (typeof rawData === "string") {
|
|
240
|
+
json = JSON.parse(rawData);
|
|
241
|
+
}
|
|
242
|
+
else if (typeof rawData === "object") {
|
|
243
|
+
json = rawData;
|
|
244
|
+
}
|
|
245
|
+
}
|
|
246
|
+
catch (e) {
|
|
247
|
+
stats.errors.push(`Failed to parse geometry JSON (${paramName} ${path} ${idx}): ${e?.message ?? String(e)}`);
|
|
248
|
+
continue;
|
|
249
|
+
}
|
|
250
|
+
if (!json)
|
|
251
|
+
continue;
|
|
252
|
+
let decoded = null;
|
|
253
|
+
try {
|
|
254
|
+
decoded = rhino.CommonObject.decode(json);
|
|
255
|
+
}
|
|
256
|
+
catch (e) {
|
|
257
|
+
stats.errors.push(`Failed to decode geometry (${paramName} ${path} ${idx}): ${e?.message ?? String(e)}`);
|
|
258
|
+
continue;
|
|
259
|
+
}
|
|
260
|
+
if (!decoded)
|
|
261
|
+
continue;
|
|
262
|
+
stats.decodedObjects += 1;
|
|
263
|
+
const typeName = decoded?.constructor?.name ?? "unknown";
|
|
264
|
+
if (typeName === "Mesh") {
|
|
265
|
+
addObject(decoded, `${paramName}[${path}][${idx}]`);
|
|
266
|
+
stats.meshesAdded += 1;
|
|
267
|
+
continue;
|
|
268
|
+
}
|
|
269
|
+
if (typeName === "Brep") {
|
|
270
|
+
const meshes = tryMeshBrep(decoded);
|
|
271
|
+
if (meshes?.length) {
|
|
272
|
+
stats.brepsMeshed += 1;
|
|
273
|
+
for (let mi = 0; mi < meshes.length; mi += 1) {
|
|
274
|
+
addObject(meshes[mi], `${paramName}[${path}][${idx}]::mesh${mi}`);
|
|
275
|
+
stats.meshesAdded += 1;
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
else {
|
|
279
|
+
addObject(decoded, `${paramName}[${path}][${idx}]::brep`);
|
|
280
|
+
stats.brepsSkipped += 1;
|
|
281
|
+
}
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
stats.unsupported[typeName] = (stats.unsupported[typeName] ?? 0) + 1;
|
|
285
|
+
addObject(decoded, `${paramName}[${path}][${idx}]::${typeName}`);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
const bytes = doc.toByteArray();
|
|
290
|
+
const geometry3dm = bytes?.length ? uint8ArrayToBase64(new Uint8Array(bytes)) : "";
|
|
291
|
+
return { geometry3dm, stats };
|
|
292
|
+
}
|
|
293
|
+
const countGeometryObjects = (doc) => {
|
|
160
294
|
const objects = doc?.objects?.();
|
|
161
295
|
const total = objects?.count ?? 0;
|
|
162
296
|
let meshCount = 0;
|
|
@@ -190,25 +324,25 @@ const countRhinoObjects = (doc) => {
|
|
|
190
324
|
};
|
|
191
325
|
};
|
|
192
326
|
export async function inspect3dmArrayBuffer(buffer) {
|
|
193
|
-
const rhino = await
|
|
327
|
+
const rhino = await getGeometryModule();
|
|
194
328
|
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
195
329
|
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
196
330
|
if (!doc) {
|
|
197
331
|
return { ok: false, meshCount: 0, brepCount: 0, extrusionCount: 0, otherCount: 0 };
|
|
198
332
|
}
|
|
199
|
-
return
|
|
333
|
+
return countGeometryObjects(doc);
|
|
200
334
|
}
|
|
201
335
|
export async function inspect3dmBase64(base64) {
|
|
202
336
|
const bytes = base64ToUint8Array(base64);
|
|
203
337
|
return inspect3dmArrayBuffer(bytes);
|
|
204
338
|
}
|
|
205
339
|
export async function loadLayeredGeometry(geometry3dm, options) {
|
|
206
|
-
const rhino = await
|
|
340
|
+
const rhino = await getGeometryModule();
|
|
207
341
|
const bytes = base64ToUint8Array(geometry3dm);
|
|
208
342
|
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
209
343
|
const groups = {};
|
|
210
344
|
if (!doc) {
|
|
211
|
-
console.error("[
|
|
345
|
+
console.error("[geometry] Failed to parse 3DM file");
|
|
212
346
|
return groups;
|
|
213
347
|
}
|
|
214
348
|
const layerTable = doc.layers?.();
|
|
@@ -268,12 +402,12 @@ export async function loadLayeredGeometry(geometry3dm, options) {
|
|
|
268
402
|
return groups;
|
|
269
403
|
}
|
|
270
404
|
export async function extractBrepsByRole(base64, options) {
|
|
271
|
-
const rhino = await
|
|
405
|
+
const rhino = await getGeometryModule(options);
|
|
272
406
|
const bytes = base64ToUint8Array(base64);
|
|
273
407
|
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
274
408
|
const breps = {};
|
|
275
409
|
if (!doc) {
|
|
276
|
-
console.error("[
|
|
410
|
+
console.error("[geometry] Failed to parse 3DM file for Brep extraction");
|
|
277
411
|
return breps;
|
|
278
412
|
}
|
|
279
413
|
const roleKeys = options?.roleKeys ?? [];
|
|
@@ -311,7 +445,134 @@ export async function extractBrepsByRole(base64, options) {
|
|
|
311
445
|
}
|
|
312
446
|
return breps;
|
|
313
447
|
}
|
|
314
|
-
|
|
448
|
+
const extractPolylinePoints = (polyline) => {
|
|
449
|
+
if (!polyline)
|
|
450
|
+
return null;
|
|
451
|
+
const points = [];
|
|
452
|
+
const count = typeof polyline.count === "number" ? polyline.count : typeof polyline.pointCount === "number" ? polyline.pointCount : 0;
|
|
453
|
+
const getter = typeof polyline.get === "function" ? polyline.get.bind(polyline) : null;
|
|
454
|
+
const pointGetter = typeof polyline.point === "function" ? polyline.point.bind(polyline) : null;
|
|
455
|
+
for (let i = 0; i < count; i += 1) {
|
|
456
|
+
const raw = getter ? getter(i) : pointGetter ? pointGetter(i) : null;
|
|
457
|
+
if (Array.isArray(raw) && raw.length >= 3) {
|
|
458
|
+
const x = Number(raw[0]);
|
|
459
|
+
const y = Number(raw[1]);
|
|
460
|
+
const z = Number(raw[2]);
|
|
461
|
+
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
|
|
462
|
+
points.push([x, y, z]);
|
|
463
|
+
}
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
return points.length > 1 ? points : null;
|
|
467
|
+
};
|
|
468
|
+
const extractCurvePoints = (curve) => {
|
|
469
|
+
if (!curve)
|
|
470
|
+
return null;
|
|
471
|
+
try {
|
|
472
|
+
if (typeof curve.ToPolyline === "function") {
|
|
473
|
+
const polyline = curve.ToPolyline();
|
|
474
|
+
const points = extractPolylinePoints(polyline);
|
|
475
|
+
if (points)
|
|
476
|
+
return points;
|
|
477
|
+
}
|
|
478
|
+
if (typeof curve.toPolyline === "function") {
|
|
479
|
+
const polyline = curve.toPolyline();
|
|
480
|
+
const points = extractPolylinePoints(polyline);
|
|
481
|
+
if (points)
|
|
482
|
+
return points;
|
|
483
|
+
}
|
|
484
|
+
if (typeof curve.tryGetPolyline === "function") {
|
|
485
|
+
const result = curve.tryGetPolyline();
|
|
486
|
+
const success = Array.isArray(result) ? result[0] : result?.[0];
|
|
487
|
+
const polyline = Array.isArray(result) ? result[1] : result?.[1];
|
|
488
|
+
if (success && polyline) {
|
|
489
|
+
const points = extractPolylinePoints(polyline);
|
|
490
|
+
if (points)
|
|
491
|
+
return points;
|
|
492
|
+
}
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
catch {
|
|
496
|
+
// fall through to sampling
|
|
497
|
+
}
|
|
498
|
+
if (typeof curve.pointAt === "function" && Array.isArray(curve.domain) && curve.domain.length >= 2) {
|
|
499
|
+
const t0 = Number(curve.domain[0]);
|
|
500
|
+
const t1 = Number(curve.domain[1]);
|
|
501
|
+
if (Number.isFinite(t0) && Number.isFinite(t1) && t0 !== t1) {
|
|
502
|
+
const segments = 64;
|
|
503
|
+
const points = [];
|
|
504
|
+
for (let i = 0; i <= segments; i += 1) {
|
|
505
|
+
const t = t0 + ((t1 - t0) * i) / segments;
|
|
506
|
+
const raw = curve.pointAt(t);
|
|
507
|
+
if (Array.isArray(raw) && raw.length >= 3) {
|
|
508
|
+
const x = Number(raw[0]);
|
|
509
|
+
const y = Number(raw[1]);
|
|
510
|
+
const z = Number(raw[2]);
|
|
511
|
+
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
|
|
512
|
+
points.push([x, y, z]);
|
|
513
|
+
}
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
if (points.length > 1) {
|
|
517
|
+
if (curve.isClosed && points.length > 2) {
|
|
518
|
+
const first = points[0];
|
|
519
|
+
const last = points[points.length - 1];
|
|
520
|
+
if (first[0] !== last[0] || first[1] !== last[1] || first[2] !== last[2]) {
|
|
521
|
+
points.push([...first]);
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
return points;
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
}
|
|
528
|
+
return null;
|
|
529
|
+
};
|
|
530
|
+
const pointsToLine = (points) => {
|
|
531
|
+
if (!points || points.length < 2)
|
|
532
|
+
return null;
|
|
533
|
+
const positions = new Float32Array(points.length * 3);
|
|
534
|
+
points.forEach((point, index) => {
|
|
535
|
+
positions[index * 3 + 0] = point[0];
|
|
536
|
+
positions[index * 3 + 1] = point[1];
|
|
537
|
+
positions[index * 3 + 2] = point[2];
|
|
538
|
+
});
|
|
539
|
+
const geometry = new THREE.BufferGeometry();
|
|
540
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
541
|
+
const material = new THREE.LineBasicMaterial({
|
|
542
|
+
color: 0x1f2937,
|
|
543
|
+
transparent: true,
|
|
544
|
+
opacity: 1,
|
|
545
|
+
toneMapped: false,
|
|
546
|
+
depthTest: false,
|
|
547
|
+
depthWrite: false,
|
|
548
|
+
});
|
|
549
|
+
const line = new THREE.Line(geometry, material);
|
|
550
|
+
line.renderOrder = 10;
|
|
551
|
+
return line;
|
|
552
|
+
};
|
|
553
|
+
const pointsToPoints = (points) => {
|
|
554
|
+
if (!points || points.length === 0)
|
|
555
|
+
return null;
|
|
556
|
+
const positions = new Float32Array(points.length * 3);
|
|
557
|
+
points.forEach((point, index) => {
|
|
558
|
+
positions[index * 3 + 0] = point[0];
|
|
559
|
+
positions[index * 3 + 1] = point[1];
|
|
560
|
+
positions[index * 3 + 2] = point[2];
|
|
561
|
+
});
|
|
562
|
+
const geometry = new THREE.BufferGeometry();
|
|
563
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
564
|
+
const material = new THREE.PointsMaterial({
|
|
565
|
+
color: 0x1f2937,
|
|
566
|
+
size: 3,
|
|
567
|
+
sizeAttenuation: false,
|
|
568
|
+
depthTest: false,
|
|
569
|
+
depthWrite: false,
|
|
570
|
+
});
|
|
571
|
+
const pointsObject = new THREE.Points(geometry, material);
|
|
572
|
+
pointsObject.renderOrder = 10;
|
|
573
|
+
return pointsObject;
|
|
574
|
+
};
|
|
575
|
+
function convertGeometryToThree(geometry, options) {
|
|
315
576
|
if (!geometry) {
|
|
316
577
|
return null;
|
|
317
578
|
}
|
|
@@ -319,24 +580,44 @@ function convertGeometryToThree(geometry) {
|
|
|
319
580
|
if (typeName === "Mesh") {
|
|
320
581
|
return meshToThree(geometry);
|
|
321
582
|
}
|
|
583
|
+
if (options?.allowCurves) {
|
|
584
|
+
if (typeName === "Point") {
|
|
585
|
+
const location = geometry.location;
|
|
586
|
+
if (Array.isArray(location) && location.length >= 3) {
|
|
587
|
+
return pointsToPoints([[Number(location[0]), Number(location[1]), Number(location[2])]]);
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
if (typeName === "PointCloud" && typeof geometry.getPoints === "function") {
|
|
591
|
+
const points = geometry.getPoints();
|
|
592
|
+
if (Array.isArray(points) && points.length > 0) {
|
|
593
|
+
return pointsToPoints(points);
|
|
594
|
+
}
|
|
595
|
+
}
|
|
596
|
+
if (typeName === "Curve" || typeName === "PolylineCurve" || typeName === "LineCurve" || typeName === "ArcCurve" || typeName === "PolyCurve" || typeName === "NurbsCurve") {
|
|
597
|
+
const points = extractCurvePoints(geometry);
|
|
598
|
+
if (points) {
|
|
599
|
+
return pointsToLine(points);
|
|
600
|
+
}
|
|
601
|
+
}
|
|
602
|
+
}
|
|
322
603
|
if (typeName === "Brep" || typeName === "Extrusion") {
|
|
323
|
-
console.warn("[
|
|
604
|
+
console.warn("[geometry] Non-meshed geometry received. Ensure server-side meshing before serialization.");
|
|
324
605
|
return null;
|
|
325
606
|
}
|
|
326
|
-
console.warn(`[
|
|
607
|
+
console.warn(`[geometry] Unsupported geometry type: ${typeName}`);
|
|
327
608
|
return null;
|
|
328
609
|
}
|
|
329
610
|
function meshToThree(mesh) {
|
|
330
611
|
const vertices = mesh.vertices?.();
|
|
331
612
|
const faces = mesh.faces?.();
|
|
332
613
|
if (!vertices || !faces) {
|
|
333
|
-
console.warn("[
|
|
614
|
+
console.warn("[geometry] Mesh missing vertices or faces");
|
|
334
615
|
return null;
|
|
335
616
|
}
|
|
336
617
|
const vertexCount = vertices.count ?? 0;
|
|
337
618
|
const faceCount = faces.count ?? 0;
|
|
338
619
|
if (vertexCount === 0 || faceCount === 0) {
|
|
339
|
-
console.warn("[
|
|
620
|
+
console.warn("[geometry] Mesh received with no vertices or faces; skipping");
|
|
340
621
|
return null;
|
|
341
622
|
}
|
|
342
623
|
const geometry = new THREE.BufferGeometry();
|
|
@@ -372,7 +653,7 @@ function meshToThree(mesh) {
|
|
|
372
653
|
};
|
|
373
654
|
for (let i = 0; i < vertexCount; i += 1) {
|
|
374
655
|
if (!writeVertex(i, vertices.get(i))) {
|
|
375
|
-
console.warn("[
|
|
656
|
+
console.warn("[geometry] Invalid vertex encountered; skipping mesh");
|
|
376
657
|
return null;
|
|
377
658
|
}
|
|
378
659
|
}
|
|
@@ -380,7 +661,7 @@ function meshToThree(mesh) {
|
|
|
380
661
|
else {
|
|
381
662
|
for (let i = 0; i < positionArray.length; i += 1) {
|
|
382
663
|
if (!Number.isFinite(positionArray[i])) {
|
|
383
|
-
console.warn("[
|
|
664
|
+
console.warn("[geometry] Mesh float array contained non-finite values; skipping mesh");
|
|
384
665
|
return null;
|
|
385
666
|
}
|
|
386
667
|
}
|
|
@@ -396,7 +677,7 @@ function meshToThree(mesh) {
|
|
|
396
677
|
const c = face[2];
|
|
397
678
|
const d = face.length > 3 ? face[3] : undefined;
|
|
398
679
|
if (![a, b, c].every((idx) => Number.isInteger(idx) && idx >= 0 && idx < vertexCount)) {
|
|
399
|
-
console.warn("[
|
|
680
|
+
console.warn("[geometry] Face reference out of bounds; skipping mesh");
|
|
400
681
|
return null;
|
|
401
682
|
}
|
|
402
683
|
indexArray.push(a, b, c);
|
|
@@ -405,7 +686,7 @@ function meshToThree(mesh) {
|
|
|
405
686
|
}
|
|
406
687
|
}
|
|
407
688
|
if (indexArray.length === 0) {
|
|
408
|
-
console.warn("[
|
|
689
|
+
console.warn("[geometry] Mesh produced no triangle indices; skipping");
|
|
409
690
|
return null;
|
|
410
691
|
}
|
|
411
692
|
geometry.setIndex(indexArray);
|