@treasuryspatial/rhino-bridge 0.1.4 → 0.1.6
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 +37 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +428 -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,15 @@ export type RhinoBridgeOptions = {
|
|
|
12
12
|
packetKey?: string;
|
|
13
13
|
layerKey?: string;
|
|
14
14
|
materialKey?: string;
|
|
15
|
+
curveMode?: "auto" | "always" | "never";
|
|
16
|
+
zFightMitigation?: boolean | ZFightMitigationOptions;
|
|
17
|
+
};
|
|
18
|
+
export type ZFightMitigationOptions = {
|
|
19
|
+
enabled?: boolean;
|
|
20
|
+
dedupe?: boolean;
|
|
21
|
+
polygonOffset?: boolean;
|
|
22
|
+
polygonOffsetFactor?: number;
|
|
23
|
+
polygonOffsetUnits?: number;
|
|
15
24
|
};
|
|
16
25
|
export type LayeredGeometryOptions = {
|
|
17
26
|
mapLayerToId?: (layerName: string, attributes: any) => string | null;
|
|
@@ -23,18 +32,39 @@ export type LayeredGeometryOptions = {
|
|
|
23
32
|
layerKey?: string;
|
|
24
33
|
materialKey?: string;
|
|
25
34
|
};
|
|
26
|
-
export declare
|
|
27
|
-
export
|
|
35
|
+
export declare const sanitizeObject3d: (root?: THREE.Object3D | null) => void;
|
|
36
|
+
export declare const dedupeMeshesInGroup: (group?: THREE.Group | null) => void;
|
|
37
|
+
export declare const applyPolygonOffsetToGroup: (group?: THREE.Group | null, factor?: number, units?: number) => void;
|
|
38
|
+
export declare const applyZFightMitigation: (group?: THREE.Group | null, options?: boolean | ZFightMitigationOptions) => void;
|
|
39
|
+
export declare function threeGroupFrom3dm(base64: string, options?: GeometryBridgeOptions): Promise<THREE.Group>;
|
|
40
|
+
export type Geometry3dmInspection = {
|
|
28
41
|
ok: boolean;
|
|
29
42
|
meshCount: number;
|
|
30
43
|
brepCount: number;
|
|
31
44
|
extrusionCount: number;
|
|
32
45
|
otherCount: number;
|
|
33
46
|
};
|
|
34
|
-
export
|
|
35
|
-
|
|
47
|
+
export type GrasshopperTo3dmStats = {
|
|
48
|
+
decodedObjects: number;
|
|
49
|
+
meshesAdded: number;
|
|
50
|
+
brepsMeshed: number;
|
|
51
|
+
brepsSkipped: number;
|
|
52
|
+
unsupported: Record<string, number>;
|
|
53
|
+
errors: string[];
|
|
54
|
+
};
|
|
55
|
+
export declare function grasshopperResultTo3dmBase64(result: {
|
|
56
|
+
values?: any[];
|
|
57
|
+
} | null | undefined): Promise<{
|
|
58
|
+
geometry3dm: string;
|
|
59
|
+
stats: GrasshopperTo3dmStats;
|
|
60
|
+
}>;
|
|
61
|
+
export declare function inspect3dmArrayBuffer(buffer: ArrayBuffer | Uint8Array): Promise<Geometry3dmInspection>;
|
|
62
|
+
export declare function inspect3dmBase64(base64: string): Promise<Geometry3dmInspection>;
|
|
36
63
|
export declare function loadLayeredGeometry(geometry3dm: string, options?: LayeredGeometryOptions): Promise<Record<string, THREE.Group>>;
|
|
37
|
-
export declare function extractBrepsByRole(base64: string, options?:
|
|
38
|
-
|
|
64
|
+
export declare function extractBrepsByRole(base64: string, options?: GeometryBridgeOptions): Promise<Record<string, string>>;
|
|
65
|
+
type ConvertGeometryOptions = {
|
|
66
|
+
allowCurves?: boolean;
|
|
67
|
+
};
|
|
68
|
+
declare function convertGeometryToThree(geometry: any, options?: ConvertGeometryOptions): THREE.Object3D | null;
|
|
39
69
|
export { convertGeometryToThree };
|
|
40
70
|
//# 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":"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"}
|
package/dist/index.js
CHANGED
|
@@ -1,53 +1,73 @@
|
|
|
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
|
-
|
|
22
|
-
|
|
20
|
+
let geometryModulePromise = null;
|
|
21
|
+
const DEFAULT_Z_FIGHT_OPTIONS = {
|
|
22
|
+
enabled: true,
|
|
23
|
+
dedupe: true,
|
|
24
|
+
polygonOffset: true,
|
|
25
|
+
polygonOffsetFactor: 0.5,
|
|
26
|
+
polygonOffsetUnits: 0.5,
|
|
27
|
+
};
|
|
28
|
+
const resolveZFightOptions = (input) => {
|
|
29
|
+
if (input === false || input === undefined || input === null)
|
|
30
|
+
return null;
|
|
31
|
+
if (input === true)
|
|
32
|
+
return { ...DEFAULT_Z_FIGHT_OPTIONS };
|
|
33
|
+
if (input.enabled === false)
|
|
34
|
+
return null;
|
|
35
|
+
return {
|
|
36
|
+
...DEFAULT_Z_FIGHT_OPTIONS,
|
|
37
|
+
...input,
|
|
38
|
+
enabled: input.enabled ?? true,
|
|
39
|
+
};
|
|
40
|
+
};
|
|
41
|
+
async function getGeometryModule(options) {
|
|
42
|
+
if (!geometryModulePromise) {
|
|
23
43
|
if (typeof window === "undefined") {
|
|
24
44
|
const pathModule = await import("path");
|
|
25
45
|
const fs = await import("fs");
|
|
26
46
|
const wasmPath = options?.wasmPath
|
|
27
47
|
? pathModule.resolve(process.cwd(), options.wasmPath)
|
|
28
|
-
: process.env.
|
|
29
|
-
? pathModule.resolve(process.cwd(), process.env.
|
|
30
|
-
: pathModule.resolve(process.cwd(), "public", "
|
|
48
|
+
: process.env.GEOMETRY_WASM_PATH
|
|
49
|
+
? pathModule.resolve(process.cwd(), process.env.GEOMETRY_WASM_PATH)
|
|
50
|
+
: pathModule.resolve(process.cwd(), "public", "geometry.wasm");
|
|
31
51
|
let wasmBinary = null;
|
|
32
52
|
try {
|
|
33
53
|
const buffer = fs.readFileSync(wasmPath);
|
|
34
54
|
wasmBinary = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
35
55
|
}
|
|
36
56
|
catch (error) {
|
|
37
|
-
console.error("[
|
|
57
|
+
console.error("[geometry] Failed to read WASM binary from", wasmPath, error);
|
|
38
58
|
}
|
|
39
|
-
|
|
59
|
+
geometryModulePromise = rhino3dm({
|
|
40
60
|
wasmBinary: wasmBinary ?? undefined,
|
|
41
61
|
locateFile: (path) => (wasmBinary ? wasmPath : resolveBrowserWasmUrl(path, options?.wasmUrl)),
|
|
42
62
|
});
|
|
43
63
|
}
|
|
44
64
|
else {
|
|
45
|
-
|
|
65
|
+
geometryModulePromise = rhino3dm({
|
|
46
66
|
locateFile: (path) => resolveBrowserWasmUrl(path, options?.wasmUrl),
|
|
47
67
|
});
|
|
48
68
|
}
|
|
49
69
|
}
|
|
50
|
-
return
|
|
70
|
+
return geometryModulePromise;
|
|
51
71
|
}
|
|
52
72
|
const base64ToUint8Array = (base64) => {
|
|
53
73
|
const normalized = base64.replace(/\s/g, "");
|
|
@@ -96,7 +116,7 @@ const getUserString = (attributes, key) => {
|
|
|
96
116
|
}
|
|
97
117
|
}
|
|
98
118
|
catch (error) {
|
|
99
|
-
console.warn("[
|
|
119
|
+
console.warn("[geometry] Unable to read user string", key, error);
|
|
100
120
|
}
|
|
101
121
|
return null;
|
|
102
122
|
};
|
|
@@ -108,13 +128,101 @@ const resolveUserString = (attributes, keys) => {
|
|
|
108
128
|
}
|
|
109
129
|
return null;
|
|
110
130
|
};
|
|
131
|
+
export const sanitizeObject3d = (root) => {
|
|
132
|
+
if (!root || !Array.isArray(root.children))
|
|
133
|
+
return;
|
|
134
|
+
const children = root.children;
|
|
135
|
+
for (let i = children.length - 1; i >= 0; i -= 1) {
|
|
136
|
+
const child = children[i];
|
|
137
|
+
if (!child || !child.isObject3D) {
|
|
138
|
+
children.splice(i, 1);
|
|
139
|
+
continue;
|
|
140
|
+
}
|
|
141
|
+
sanitizeObject3d(child);
|
|
142
|
+
}
|
|
143
|
+
};
|
|
144
|
+
const buildGeometrySignature = (geometry) => {
|
|
145
|
+
const position = geometry.getAttribute("position");
|
|
146
|
+
if (!position || position.count === 0)
|
|
147
|
+
return null;
|
|
148
|
+
const index = geometry.getIndex();
|
|
149
|
+
const box = new THREE.Box3().setFromBufferAttribute(position);
|
|
150
|
+
const sampleCount = Math.min(position.count, 30);
|
|
151
|
+
const indexCount = index ? Math.min(index.count, 30) : 0;
|
|
152
|
+
let signature = `${position.count}|${index?.count ?? 0}|`;
|
|
153
|
+
signature += `${box.min.x.toFixed(4)},${box.min.y.toFixed(4)},${box.min.z.toFixed(4)}|`;
|
|
154
|
+
signature += `${box.max.x.toFixed(4)},${box.max.y.toFixed(4)},${box.max.z.toFixed(4)}`;
|
|
155
|
+
for (let i = 0; i < sampleCount; i += 1) {
|
|
156
|
+
signature += `|${position.getX(i).toFixed(4)},${position.getY(i).toFixed(4)},${position.getZ(i).toFixed(4)}`;
|
|
157
|
+
}
|
|
158
|
+
for (let i = 0; i < indexCount; i += 1) {
|
|
159
|
+
signature += `:${index?.getX(i) ?? 0}`;
|
|
160
|
+
}
|
|
161
|
+
return signature;
|
|
162
|
+
};
|
|
163
|
+
export const dedupeMeshesInGroup = (group) => {
|
|
164
|
+
if (!group || typeof group.traverse !== "function")
|
|
165
|
+
return;
|
|
166
|
+
sanitizeObject3d(group);
|
|
167
|
+
const seen = new Map();
|
|
168
|
+
const duplicates = [];
|
|
169
|
+
group.traverse((object) => {
|
|
170
|
+
if (!(object instanceof THREE.Mesh))
|
|
171
|
+
return;
|
|
172
|
+
const geometry = object.geometry;
|
|
173
|
+
if (!(geometry instanceof THREE.BufferGeometry))
|
|
174
|
+
return;
|
|
175
|
+
const signature = buildGeometrySignature(geometry);
|
|
176
|
+
if (!signature)
|
|
177
|
+
return;
|
|
178
|
+
const existing = seen.get(signature);
|
|
179
|
+
if (existing) {
|
|
180
|
+
duplicates.push(object);
|
|
181
|
+
}
|
|
182
|
+
else {
|
|
183
|
+
seen.set(signature, object);
|
|
184
|
+
}
|
|
185
|
+
});
|
|
186
|
+
if (duplicates.length > 0) {
|
|
187
|
+
duplicates.forEach((object) => {
|
|
188
|
+
object.parent?.remove(object);
|
|
189
|
+
});
|
|
190
|
+
}
|
|
191
|
+
};
|
|
192
|
+
export const applyPolygonOffsetToGroup = (group, factor = DEFAULT_Z_FIGHT_OPTIONS.polygonOffsetFactor, units = DEFAULT_Z_FIGHT_OPTIONS.polygonOffsetUnits) => {
|
|
193
|
+
if (!group || typeof group.traverse !== "function")
|
|
194
|
+
return;
|
|
195
|
+
group.traverse((object) => {
|
|
196
|
+
if (!(object instanceof THREE.Mesh))
|
|
197
|
+
return;
|
|
198
|
+
const materials = Array.isArray(object.material) ? object.material : [object.material];
|
|
199
|
+
materials.forEach((material) => {
|
|
200
|
+
if (!material)
|
|
201
|
+
return;
|
|
202
|
+
material.polygonOffset = true;
|
|
203
|
+
material.polygonOffsetFactor = factor;
|
|
204
|
+
material.polygonOffsetUnits = units;
|
|
205
|
+
});
|
|
206
|
+
});
|
|
207
|
+
};
|
|
208
|
+
export const applyZFightMitigation = (group, options) => {
|
|
209
|
+
const resolved = resolveZFightOptions(options);
|
|
210
|
+
if (!resolved || !group)
|
|
211
|
+
return;
|
|
212
|
+
if (resolved.dedupe) {
|
|
213
|
+
dedupeMeshesInGroup(group);
|
|
214
|
+
}
|
|
215
|
+
if (resolved.polygonOffset) {
|
|
216
|
+
applyPolygonOffsetToGroup(group, resolved.polygonOffsetFactor, resolved.polygonOffsetUnits);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
111
219
|
export async function threeGroupFrom3dm(base64, options) {
|
|
112
|
-
const rhino = await
|
|
220
|
+
const rhino = await getGeometryModule(options);
|
|
113
221
|
const bytes = base64ToUint8Array(base64);
|
|
114
222
|
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
115
223
|
const group = new THREE.Group();
|
|
116
224
|
if (!doc) {
|
|
117
|
-
console.error("[
|
|
225
|
+
console.error("[geometry] Failed to parse 3DM file from bytes");
|
|
118
226
|
return group;
|
|
119
227
|
}
|
|
120
228
|
const rotation = options?.rotateToYUp === false ? null : new THREE.Matrix4().makeRotationX(-Math.PI / 2);
|
|
@@ -127,6 +235,23 @@ export async function threeGroupFrom3dm(base64, options) {
|
|
|
127
235
|
const materialKey = options?.materialKey ?? "b2:material";
|
|
128
236
|
const objects = doc.objects();
|
|
129
237
|
const objectCount = objects.count ?? 0;
|
|
238
|
+
const curveMode = options?.curveMode ?? "auto";
|
|
239
|
+
let allowCurves = curveMode === "always";
|
|
240
|
+
if (curveMode === "auto") {
|
|
241
|
+
allowCurves = true;
|
|
242
|
+
for (let i = 0; i < objectCount; i += 1) {
|
|
243
|
+
const obj = objects.get(i);
|
|
244
|
+
const geom = obj?.geometry?.();
|
|
245
|
+
const typeName = geom?.constructor?.name;
|
|
246
|
+
if (typeName === "Mesh") {
|
|
247
|
+
allowCurves = false;
|
|
248
|
+
break;
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
else if (curveMode === "never") {
|
|
253
|
+
allowCurves = false;
|
|
254
|
+
}
|
|
130
255
|
for (let i = 0; i < objectCount; i += 1) {
|
|
131
256
|
const obj = objects.get(i);
|
|
132
257
|
const attributes = typeof obj?.attributes === "function" ? obj.attributes() : null;
|
|
@@ -135,7 +260,7 @@ export async function threeGroupFrom3dm(base64, options) {
|
|
|
135
260
|
continue;
|
|
136
261
|
}
|
|
137
262
|
const geom = obj?.geometry?.();
|
|
138
|
-
const threeObject = convertGeometryToThree(geom);
|
|
263
|
+
const threeObject = convertGeometryToThree(geom, { allowCurves });
|
|
139
264
|
if (threeObject) {
|
|
140
265
|
if (rotation) {
|
|
141
266
|
threeObject.applyMatrix4(rotation);
|
|
@@ -154,9 +279,130 @@ export async function threeGroupFrom3dm(base64, options) {
|
|
|
154
279
|
group.add(threeObject);
|
|
155
280
|
}
|
|
156
281
|
}
|
|
282
|
+
sanitizeObject3d(group);
|
|
283
|
+
if (options?.zFightMitigation) {
|
|
284
|
+
applyZFightMitigation(group, options.zFightMitigation);
|
|
285
|
+
}
|
|
157
286
|
return group;
|
|
158
287
|
}
|
|
159
|
-
|
|
288
|
+
export async function grasshopperResultTo3dmBase64(result) {
|
|
289
|
+
const rhino = await getGeometryModule();
|
|
290
|
+
const stats = {
|
|
291
|
+
decodedObjects: 0,
|
|
292
|
+
meshesAdded: 0,
|
|
293
|
+
brepsMeshed: 0,
|
|
294
|
+
brepsSkipped: 0,
|
|
295
|
+
unsupported: {},
|
|
296
|
+
errors: [],
|
|
297
|
+
};
|
|
298
|
+
const doc = new rhino.File3dm();
|
|
299
|
+
const values = Array.isArray(result?.values) ? result?.values ?? [] : [];
|
|
300
|
+
if (!values.length) {
|
|
301
|
+
return { geometry3dm: "", stats };
|
|
302
|
+
}
|
|
303
|
+
const addObject = (geometry, name) => {
|
|
304
|
+
try {
|
|
305
|
+
if (name && rhino.ObjectAttributes) {
|
|
306
|
+
const attributes = new rhino.ObjectAttributes();
|
|
307
|
+
attributes.name = name;
|
|
308
|
+
doc.objects().add(geometry, attributes);
|
|
309
|
+
}
|
|
310
|
+
else {
|
|
311
|
+
doc.objects().add(geometry, null);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
catch (e) {
|
|
315
|
+
stats.errors.push(`Failed to add object to 3dm: ${e?.message ?? String(e)}`);
|
|
316
|
+
}
|
|
317
|
+
};
|
|
318
|
+
const tryMeshBrep = (brep) => {
|
|
319
|
+
try {
|
|
320
|
+
const createFromBrep = rhino.Mesh?.createFromBrep;
|
|
321
|
+
if (typeof createFromBrep !== "function")
|
|
322
|
+
return null;
|
|
323
|
+
const mp = rhino.MeshingParameters?.default ?? null;
|
|
324
|
+
try {
|
|
325
|
+
const meshes = mp ? createFromBrep(brep, mp) : createFromBrep(brep);
|
|
326
|
+
return Array.isArray(meshes) ? meshes.filter(Boolean) : null;
|
|
327
|
+
}
|
|
328
|
+
catch {
|
|
329
|
+
const meshes = createFromBrep(brep);
|
|
330
|
+
return Array.isArray(meshes) ? meshes.filter(Boolean) : null;
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
catch (e) {
|
|
334
|
+
stats.errors.push(`Failed to mesh Brep: ${e?.message ?? String(e)}`);
|
|
335
|
+
return null;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
for (const output of values) {
|
|
339
|
+
const paramName = String(output?.ParamName ?? "output");
|
|
340
|
+
const tree = output?.InnerTree ?? {};
|
|
341
|
+
for (const path of Object.keys(tree)) {
|
|
342
|
+
const items = Array.isArray(tree[path]) ? tree[path] : [];
|
|
343
|
+
for (let idx = 0; idx < items.length; idx += 1) {
|
|
344
|
+
const item = items[idx];
|
|
345
|
+
const itemType = String(item?.type ?? "");
|
|
346
|
+
const rawData = item?.data;
|
|
347
|
+
if (!itemType.startsWith("Rhino.Geometry"))
|
|
348
|
+
continue;
|
|
349
|
+
let json = null;
|
|
350
|
+
try {
|
|
351
|
+
if (typeof rawData === "string") {
|
|
352
|
+
json = JSON.parse(rawData);
|
|
353
|
+
}
|
|
354
|
+
else if (typeof rawData === "object") {
|
|
355
|
+
json = rawData;
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
catch (e) {
|
|
359
|
+
stats.errors.push(`Failed to parse geometry JSON (${paramName} ${path} ${idx}): ${e?.message ?? String(e)}`);
|
|
360
|
+
continue;
|
|
361
|
+
}
|
|
362
|
+
if (!json)
|
|
363
|
+
continue;
|
|
364
|
+
let decoded = null;
|
|
365
|
+
try {
|
|
366
|
+
decoded = rhino.CommonObject.decode(json);
|
|
367
|
+
}
|
|
368
|
+
catch (e) {
|
|
369
|
+
stats.errors.push(`Failed to decode geometry (${paramName} ${path} ${idx}): ${e?.message ?? String(e)}`);
|
|
370
|
+
continue;
|
|
371
|
+
}
|
|
372
|
+
if (!decoded)
|
|
373
|
+
continue;
|
|
374
|
+
stats.decodedObjects += 1;
|
|
375
|
+
const typeName = decoded?.constructor?.name ?? "unknown";
|
|
376
|
+
if (typeName === "Mesh") {
|
|
377
|
+
addObject(decoded, `${paramName}[${path}][${idx}]`);
|
|
378
|
+
stats.meshesAdded += 1;
|
|
379
|
+
continue;
|
|
380
|
+
}
|
|
381
|
+
if (typeName === "Brep") {
|
|
382
|
+
const meshes = tryMeshBrep(decoded);
|
|
383
|
+
if (meshes?.length) {
|
|
384
|
+
stats.brepsMeshed += 1;
|
|
385
|
+
for (let mi = 0; mi < meshes.length; mi += 1) {
|
|
386
|
+
addObject(meshes[mi], `${paramName}[${path}][${idx}]::mesh${mi}`);
|
|
387
|
+
stats.meshesAdded += 1;
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
else {
|
|
391
|
+
addObject(decoded, `${paramName}[${path}][${idx}]::brep`);
|
|
392
|
+
stats.brepsSkipped += 1;
|
|
393
|
+
}
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
stats.unsupported[typeName] = (stats.unsupported[typeName] ?? 0) + 1;
|
|
397
|
+
addObject(decoded, `${paramName}[${path}][${idx}]::${typeName}`);
|
|
398
|
+
}
|
|
399
|
+
}
|
|
400
|
+
}
|
|
401
|
+
const bytes = doc.toByteArray();
|
|
402
|
+
const geometry3dm = bytes?.length ? uint8ArrayToBase64(new Uint8Array(bytes)) : "";
|
|
403
|
+
return { geometry3dm, stats };
|
|
404
|
+
}
|
|
405
|
+
const countGeometryObjects = (doc) => {
|
|
160
406
|
const objects = doc?.objects?.();
|
|
161
407
|
const total = objects?.count ?? 0;
|
|
162
408
|
let meshCount = 0;
|
|
@@ -190,25 +436,25 @@ const countRhinoObjects = (doc) => {
|
|
|
190
436
|
};
|
|
191
437
|
};
|
|
192
438
|
export async function inspect3dmArrayBuffer(buffer) {
|
|
193
|
-
const rhino = await
|
|
439
|
+
const rhino = await getGeometryModule();
|
|
194
440
|
const bytes = buffer instanceof Uint8Array ? buffer : new Uint8Array(buffer);
|
|
195
441
|
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
196
442
|
if (!doc) {
|
|
197
443
|
return { ok: false, meshCount: 0, brepCount: 0, extrusionCount: 0, otherCount: 0 };
|
|
198
444
|
}
|
|
199
|
-
return
|
|
445
|
+
return countGeometryObjects(doc);
|
|
200
446
|
}
|
|
201
447
|
export async function inspect3dmBase64(base64) {
|
|
202
448
|
const bytes = base64ToUint8Array(base64);
|
|
203
449
|
return inspect3dmArrayBuffer(bytes);
|
|
204
450
|
}
|
|
205
451
|
export async function loadLayeredGeometry(geometry3dm, options) {
|
|
206
|
-
const rhino = await
|
|
452
|
+
const rhino = await getGeometryModule();
|
|
207
453
|
const bytes = base64ToUint8Array(geometry3dm);
|
|
208
454
|
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
209
455
|
const groups = {};
|
|
210
456
|
if (!doc) {
|
|
211
|
-
console.error("[
|
|
457
|
+
console.error("[geometry] Failed to parse 3DM file");
|
|
212
458
|
return groups;
|
|
213
459
|
}
|
|
214
460
|
const layerTable = doc.layers?.();
|
|
@@ -268,12 +514,12 @@ export async function loadLayeredGeometry(geometry3dm, options) {
|
|
|
268
514
|
return groups;
|
|
269
515
|
}
|
|
270
516
|
export async function extractBrepsByRole(base64, options) {
|
|
271
|
-
const rhino = await
|
|
517
|
+
const rhino = await getGeometryModule(options);
|
|
272
518
|
const bytes = base64ToUint8Array(base64);
|
|
273
519
|
const doc = rhino.File3dm.fromByteArray(bytes);
|
|
274
520
|
const breps = {};
|
|
275
521
|
if (!doc) {
|
|
276
|
-
console.error("[
|
|
522
|
+
console.error("[geometry] Failed to parse 3DM file for Brep extraction");
|
|
277
523
|
return breps;
|
|
278
524
|
}
|
|
279
525
|
const roleKeys = options?.roleKeys ?? [];
|
|
@@ -311,7 +557,134 @@ export async function extractBrepsByRole(base64, options) {
|
|
|
311
557
|
}
|
|
312
558
|
return breps;
|
|
313
559
|
}
|
|
314
|
-
|
|
560
|
+
const extractPolylinePoints = (polyline) => {
|
|
561
|
+
if (!polyline)
|
|
562
|
+
return null;
|
|
563
|
+
const points = [];
|
|
564
|
+
const count = typeof polyline.count === "number" ? polyline.count : typeof polyline.pointCount === "number" ? polyline.pointCount : 0;
|
|
565
|
+
const getter = typeof polyline.get === "function" ? polyline.get.bind(polyline) : null;
|
|
566
|
+
const pointGetter = typeof polyline.point === "function" ? polyline.point.bind(polyline) : null;
|
|
567
|
+
for (let i = 0; i < count; i += 1) {
|
|
568
|
+
const raw = getter ? getter(i) : pointGetter ? pointGetter(i) : null;
|
|
569
|
+
if (Array.isArray(raw) && raw.length >= 3) {
|
|
570
|
+
const x = Number(raw[0]);
|
|
571
|
+
const y = Number(raw[1]);
|
|
572
|
+
const z = Number(raw[2]);
|
|
573
|
+
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
|
|
574
|
+
points.push([x, y, z]);
|
|
575
|
+
}
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
return points.length > 1 ? points : null;
|
|
579
|
+
};
|
|
580
|
+
const extractCurvePoints = (curve) => {
|
|
581
|
+
if (!curve)
|
|
582
|
+
return null;
|
|
583
|
+
try {
|
|
584
|
+
if (typeof curve.ToPolyline === "function") {
|
|
585
|
+
const polyline = curve.ToPolyline();
|
|
586
|
+
const points = extractPolylinePoints(polyline);
|
|
587
|
+
if (points)
|
|
588
|
+
return points;
|
|
589
|
+
}
|
|
590
|
+
if (typeof curve.toPolyline === "function") {
|
|
591
|
+
const polyline = curve.toPolyline();
|
|
592
|
+
const points = extractPolylinePoints(polyline);
|
|
593
|
+
if (points)
|
|
594
|
+
return points;
|
|
595
|
+
}
|
|
596
|
+
if (typeof curve.tryGetPolyline === "function") {
|
|
597
|
+
const result = curve.tryGetPolyline();
|
|
598
|
+
const success = Array.isArray(result) ? result[0] : result?.[0];
|
|
599
|
+
const polyline = Array.isArray(result) ? result[1] : result?.[1];
|
|
600
|
+
if (success && polyline) {
|
|
601
|
+
const points = extractPolylinePoints(polyline);
|
|
602
|
+
if (points)
|
|
603
|
+
return points;
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
catch {
|
|
608
|
+
// fall through to sampling
|
|
609
|
+
}
|
|
610
|
+
if (typeof curve.pointAt === "function" && Array.isArray(curve.domain) && curve.domain.length >= 2) {
|
|
611
|
+
const t0 = Number(curve.domain[0]);
|
|
612
|
+
const t1 = Number(curve.domain[1]);
|
|
613
|
+
if (Number.isFinite(t0) && Number.isFinite(t1) && t0 !== t1) {
|
|
614
|
+
const segments = 64;
|
|
615
|
+
const points = [];
|
|
616
|
+
for (let i = 0; i <= segments; i += 1) {
|
|
617
|
+
const t = t0 + ((t1 - t0) * i) / segments;
|
|
618
|
+
const raw = curve.pointAt(t);
|
|
619
|
+
if (Array.isArray(raw) && raw.length >= 3) {
|
|
620
|
+
const x = Number(raw[0]);
|
|
621
|
+
const y = Number(raw[1]);
|
|
622
|
+
const z = Number(raw[2]);
|
|
623
|
+
if (Number.isFinite(x) && Number.isFinite(y) && Number.isFinite(z)) {
|
|
624
|
+
points.push([x, y, z]);
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
if (points.length > 1) {
|
|
629
|
+
if (curve.isClosed && points.length > 2) {
|
|
630
|
+
const first = points[0];
|
|
631
|
+
const last = points[points.length - 1];
|
|
632
|
+
if (first[0] !== last[0] || first[1] !== last[1] || first[2] !== last[2]) {
|
|
633
|
+
points.push([...first]);
|
|
634
|
+
}
|
|
635
|
+
}
|
|
636
|
+
return points;
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
}
|
|
640
|
+
return null;
|
|
641
|
+
};
|
|
642
|
+
const pointsToLine = (points) => {
|
|
643
|
+
if (!points || points.length < 2)
|
|
644
|
+
return null;
|
|
645
|
+
const positions = new Float32Array(points.length * 3);
|
|
646
|
+
points.forEach((point, index) => {
|
|
647
|
+
positions[index * 3 + 0] = point[0];
|
|
648
|
+
positions[index * 3 + 1] = point[1];
|
|
649
|
+
positions[index * 3 + 2] = point[2];
|
|
650
|
+
});
|
|
651
|
+
const geometry = new THREE.BufferGeometry();
|
|
652
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
653
|
+
const material = new THREE.LineBasicMaterial({
|
|
654
|
+
color: 0x1f2937,
|
|
655
|
+
transparent: true,
|
|
656
|
+
opacity: 1,
|
|
657
|
+
toneMapped: false,
|
|
658
|
+
depthTest: false,
|
|
659
|
+
depthWrite: false,
|
|
660
|
+
});
|
|
661
|
+
const line = new THREE.Line(geometry, material);
|
|
662
|
+
line.renderOrder = 10;
|
|
663
|
+
return line;
|
|
664
|
+
};
|
|
665
|
+
const pointsToPoints = (points) => {
|
|
666
|
+
if (!points || points.length === 0)
|
|
667
|
+
return null;
|
|
668
|
+
const positions = new Float32Array(points.length * 3);
|
|
669
|
+
points.forEach((point, index) => {
|
|
670
|
+
positions[index * 3 + 0] = point[0];
|
|
671
|
+
positions[index * 3 + 1] = point[1];
|
|
672
|
+
positions[index * 3 + 2] = point[2];
|
|
673
|
+
});
|
|
674
|
+
const geometry = new THREE.BufferGeometry();
|
|
675
|
+
geometry.setAttribute("position", new THREE.BufferAttribute(positions, 3));
|
|
676
|
+
const material = new THREE.PointsMaterial({
|
|
677
|
+
color: 0x1f2937,
|
|
678
|
+
size: 3,
|
|
679
|
+
sizeAttenuation: false,
|
|
680
|
+
depthTest: false,
|
|
681
|
+
depthWrite: false,
|
|
682
|
+
});
|
|
683
|
+
const pointsObject = new THREE.Points(geometry, material);
|
|
684
|
+
pointsObject.renderOrder = 10;
|
|
685
|
+
return pointsObject;
|
|
686
|
+
};
|
|
687
|
+
function convertGeometryToThree(geometry, options) {
|
|
315
688
|
if (!geometry) {
|
|
316
689
|
return null;
|
|
317
690
|
}
|
|
@@ -319,24 +692,44 @@ function convertGeometryToThree(geometry) {
|
|
|
319
692
|
if (typeName === "Mesh") {
|
|
320
693
|
return meshToThree(geometry);
|
|
321
694
|
}
|
|
695
|
+
if (options?.allowCurves) {
|
|
696
|
+
if (typeName === "Point") {
|
|
697
|
+
const location = geometry.location;
|
|
698
|
+
if (Array.isArray(location) && location.length >= 3) {
|
|
699
|
+
return pointsToPoints([[Number(location[0]), Number(location[1]), Number(location[2])]]);
|
|
700
|
+
}
|
|
701
|
+
}
|
|
702
|
+
if (typeName === "PointCloud" && typeof geometry.getPoints === "function") {
|
|
703
|
+
const points = geometry.getPoints();
|
|
704
|
+
if (Array.isArray(points) && points.length > 0) {
|
|
705
|
+
return pointsToPoints(points);
|
|
706
|
+
}
|
|
707
|
+
}
|
|
708
|
+
if (typeName === "Curve" || typeName === "PolylineCurve" || typeName === "LineCurve" || typeName === "ArcCurve" || typeName === "PolyCurve" || typeName === "NurbsCurve") {
|
|
709
|
+
const points = extractCurvePoints(geometry);
|
|
710
|
+
if (points) {
|
|
711
|
+
return pointsToLine(points);
|
|
712
|
+
}
|
|
713
|
+
}
|
|
714
|
+
}
|
|
322
715
|
if (typeName === "Brep" || typeName === "Extrusion") {
|
|
323
|
-
console.warn("[
|
|
716
|
+
console.warn("[geometry] Non-meshed geometry received. Ensure server-side meshing before serialization.");
|
|
324
717
|
return null;
|
|
325
718
|
}
|
|
326
|
-
console.warn(`[
|
|
719
|
+
console.warn(`[geometry] Unsupported geometry type: ${typeName}`);
|
|
327
720
|
return null;
|
|
328
721
|
}
|
|
329
722
|
function meshToThree(mesh) {
|
|
330
723
|
const vertices = mesh.vertices?.();
|
|
331
724
|
const faces = mesh.faces?.();
|
|
332
725
|
if (!vertices || !faces) {
|
|
333
|
-
console.warn("[
|
|
726
|
+
console.warn("[geometry] Mesh missing vertices or faces");
|
|
334
727
|
return null;
|
|
335
728
|
}
|
|
336
729
|
const vertexCount = vertices.count ?? 0;
|
|
337
730
|
const faceCount = faces.count ?? 0;
|
|
338
731
|
if (vertexCount === 0 || faceCount === 0) {
|
|
339
|
-
console.warn("[
|
|
732
|
+
console.warn("[geometry] Mesh received with no vertices or faces; skipping");
|
|
340
733
|
return null;
|
|
341
734
|
}
|
|
342
735
|
const geometry = new THREE.BufferGeometry();
|
|
@@ -372,7 +765,7 @@ function meshToThree(mesh) {
|
|
|
372
765
|
};
|
|
373
766
|
for (let i = 0; i < vertexCount; i += 1) {
|
|
374
767
|
if (!writeVertex(i, vertices.get(i))) {
|
|
375
|
-
console.warn("[
|
|
768
|
+
console.warn("[geometry] Invalid vertex encountered; skipping mesh");
|
|
376
769
|
return null;
|
|
377
770
|
}
|
|
378
771
|
}
|
|
@@ -380,7 +773,7 @@ function meshToThree(mesh) {
|
|
|
380
773
|
else {
|
|
381
774
|
for (let i = 0; i < positionArray.length; i += 1) {
|
|
382
775
|
if (!Number.isFinite(positionArray[i])) {
|
|
383
|
-
console.warn("[
|
|
776
|
+
console.warn("[geometry] Mesh float array contained non-finite values; skipping mesh");
|
|
384
777
|
return null;
|
|
385
778
|
}
|
|
386
779
|
}
|
|
@@ -396,7 +789,7 @@ function meshToThree(mesh) {
|
|
|
396
789
|
const c = face[2];
|
|
397
790
|
const d = face.length > 3 ? face[3] : undefined;
|
|
398
791
|
if (![a, b, c].every((idx) => Number.isInteger(idx) && idx >= 0 && idx < vertexCount)) {
|
|
399
|
-
console.warn("[
|
|
792
|
+
console.warn("[geometry] Face reference out of bounds; skipping mesh");
|
|
400
793
|
return null;
|
|
401
794
|
}
|
|
402
795
|
indexArray.push(a, b, c);
|
|
@@ -405,7 +798,7 @@ function meshToThree(mesh) {
|
|
|
405
798
|
}
|
|
406
799
|
}
|
|
407
800
|
if (indexArray.length === 0) {
|
|
408
|
-
console.warn("[
|
|
801
|
+
console.warn("[geometry] Mesh produced no triangle indices; skipping");
|
|
409
802
|
return null;
|
|
410
803
|
}
|
|
411
804
|
geometry.setIndex(indexArray);
|