@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 CHANGED
@@ -1,5 +1,5 @@
1
1
  import * as THREE from "three";
2
- export type RhinoBridgeOptions = {
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 function threeGroupFrom3dm(base64: string, options?: RhinoBridgeOptions): Promise<THREE.Group>;
27
- export type Rhino3dmInspection = {
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 declare function inspect3dmArrayBuffer(buffer: ArrayBuffer | Uint8Array): Promise<Rhino3dmInspection>;
35
- export declare function inspect3dmBase64(base64: string): Promise<Rhino3dmInspection>;
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?: RhinoBridgeOptions): Promise<Record<string, string>>;
38
- declare function convertGeometryToThree(geometry: any): THREE.Object3D | null;
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
@@ -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,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"}
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 = "/rhino3dm.wasm";
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.NEXT_PUBLIC_RHINO3DM_WASM_URL?.trim() || process.env.RHINO3DM_WASM_URL?.trim();
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}/${normalizedPath}`;
16
+ return `${base}/${DEFAULT_BROWSER_WASM_FILENAME}`;
17
17
  }
18
- return `${DEFAULT_BROWSER_WASM_PATH.replace(/[^/]+$/, "")}${normalizedPath}`;
18
+ return DEFAULT_BROWSER_WASM_PATH;
19
19
  };
20
- let rhinoModulePromise = null;
21
- async function getRhinoModule(options) {
22
- if (!rhinoModulePromise) {
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.RHINO3DM_WASM_PATH
29
- ? pathModule.resolve(process.cwd(), process.env.RHINO3DM_WASM_PATH)
30
- : pathModule.resolve(process.cwd(), "public", "rhino3dm.wasm");
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("[rhino3dm] Failed to read WASM binary from", wasmPath, error);
57
+ console.error("[geometry] Failed to read WASM binary from", wasmPath, error);
38
58
  }
39
- rhinoModulePromise = rhino3dm({
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
- rhinoModulePromise = rhino3dm({
65
+ geometryModulePromise = rhino3dm({
46
66
  locateFile: (path) => resolveBrowserWasmUrl(path, options?.wasmUrl),
47
67
  });
48
68
  }
49
69
  }
50
- return rhinoModulePromise;
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("[rhino3dm] Unable to read user string", key, error);
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 getRhinoModule(options);
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("[rhino3dm] Failed to parse 3DM file from bytes");
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
- const countRhinoObjects = (doc) => {
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 getRhinoModule();
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 countRhinoObjects(doc);
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 getRhinoModule();
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("[rhino3dm] Failed to parse 3DM file");
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 getRhinoModule(options);
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("[rhino3dm] Failed to parse 3DM file for Brep extraction");
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
- function convertGeometryToThree(geometry) {
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("[rhino3dm] Non-meshed geometry received. Ensure server-side meshing before serialization.");
716
+ console.warn("[geometry] Non-meshed geometry received. Ensure server-side meshing before serialization.");
324
717
  return null;
325
718
  }
326
- console.warn(`[rhino3dm] Unsupported geometry type: ${typeName}`);
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("[rhino3dm] Mesh missing vertices or faces");
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("[rhino3dm] Mesh received with no vertices or faces; skipping");
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("[rhino3dm] Invalid vertex encountered; skipping mesh");
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("[rhino3dm] Mesh float array contained non-finite values; skipping mesh");
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("[rhino3dm] Face reference out of bounds; skipping mesh");
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("[rhino3dm] Mesh produced no triangle indices; skipping");
801
+ console.warn("[geometry] Mesh produced no triangle indices; skipping");
409
802
  return null;
410
803
  }
411
804
  geometry.setIndex(indexArray);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@treasuryspatial/rhino-bridge",
3
- "version": "0.1.4",
3
+ "version": "0.1.6",
4
4
  "type": "module",
5
5
  "license": "UNLICENSED",
6
6
  "main": "./dist/index.js",