dsh-cad 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (113) hide show
  1. package/LICENSE +201 -0
  2. package/README.md +190 -0
  3. package/README.zh-CN.md +173 -0
  4. package/cordis.patch.yml +3 -0
  5. package/lib/b64.js +12 -0
  6. package/lib/cad_connector/builtin-executor.js +55 -0
  7. package/lib/cad_connector/builtin.js +8 -0
  8. package/lib/cad_connector/executor.js +76 -0
  9. package/lib/cad_connector/freecad-executor.js +398 -0
  10. package/lib/cad_connector/freecad.js +8 -0
  11. package/lib/cad_connector/fusion360-executor.js +398 -0
  12. package/lib/cad_connector/fusion360.js +8 -0
  13. package/lib/cad_connector/gstarcad3d.js +8 -0
  14. package/lib/cad_connector/index.js +20 -0
  15. package/lib/cad_connector/onshape.js +8 -0
  16. package/lib/cad_connector/solidworks.js +8 -0
  17. package/lib/cad_connector/types.js +6 -0
  18. package/lib/cad_connector/zw3d.js +8 -0
  19. package/lib/client.js +4117 -0
  20. package/lib/client.js.map +7 -0
  21. package/lib/convert/dcprt.js +38 -0
  22. package/lib/convert/detect.js +22 -0
  23. package/lib/convert/dxf.js +124 -0
  24. package/lib/convert/index.js +70 -0
  25. package/lib/convert/obj.js +106 -0
  26. package/lib/convert/png.js +114 -0
  27. package/lib/convert/step-worker.mjs +61 -0
  28. package/lib/convert/step.js +66 -0
  29. package/lib/convert/stl.js +110 -0
  30. package/lib/convert/svg.js +101 -0
  31. package/lib/demo-bracket.brep +668 -0
  32. package/lib/demo-flange.brep +709 -0
  33. package/lib/demo-shaft.brep +516 -0
  34. package/lib/feature_script/dc_asm.js +17 -0
  35. package/lib/feature_script/dc_engineering.js +19 -0
  36. package/lib/feature_script/dc_prt.js +21 -0
  37. package/lib/feature_script/index.js +7 -0
  38. package/lib/image/profile.js +218 -0
  39. package/lib/index.js +77 -0
  40. package/lib/modeling/assembly.js +63 -0
  41. package/lib/modeling/bin-format.js +80 -0
  42. package/lib/modeling/bin-store.js +67 -0
  43. package/lib/modeling/client.js +117 -0
  44. package/lib/modeling/document.js +55 -0
  45. package/lib/modeling/drawing.js +324 -0
  46. package/lib/modeling/hlr.cjs +328 -0
  47. package/lib/modeling/modeling-worker.cjs +235 -0
  48. package/lib/modeling/occt-adapter.cjs +271 -0
  49. package/lib/routes.js +142 -0
  50. package/lib/store.js +68 -0
  51. package/lib/tools/cad-freecad.js +160 -0
  52. package/lib/tools/cad-fusion.js +138 -0
  53. package/lib/tools/cad-image.js +121 -0
  54. package/lib/tools/cad-info.js +92 -0
  55. package/lib/tools/cad-model.js +816 -0
  56. package/lib/tools/cad-view.js +112 -0
  57. package/lib/tools/util.js +25 -0
  58. package/lib/types/b64.d.ts +8 -0
  59. package/lib/types/cad/builtin.d.ts +3 -0
  60. package/lib/types/cad/freecad.d.ts +3 -0
  61. package/lib/types/cad/fusion360.d.ts +3 -0
  62. package/lib/types/cad/gstarcad3d.d.ts +3 -0
  63. package/lib/types/cad/index.d.ts +8 -0
  64. package/lib/types/cad/onshape.d.ts +3 -0
  65. package/lib/types/cad/solidworks.d.ts +3 -0
  66. package/lib/types/cad/types.d.ts +21 -0
  67. package/lib/types/cad/zw3d.d.ts +3 -0
  68. package/lib/types/cad_connector/builtin-executor.d.ts +9 -0
  69. package/lib/types/cad_connector/builtin.d.ts +3 -0
  70. package/lib/types/cad_connector/executor.d.ts +69 -0
  71. package/lib/types/cad_connector/freecad-executor.d.ts +55 -0
  72. package/lib/types/cad_connector/freecad.d.ts +3 -0
  73. package/lib/types/cad_connector/fusion360-executor.d.ts +11 -0
  74. package/lib/types/cad_connector/fusion360.d.ts +3 -0
  75. package/lib/types/cad_connector/gstarcad3d.d.ts +3 -0
  76. package/lib/types/cad_connector/index.d.ts +8 -0
  77. package/lib/types/cad_connector/onshape.d.ts +3 -0
  78. package/lib/types/cad_connector/solidworks.d.ts +3 -0
  79. package/lib/types/cad_connector/types.d.ts +21 -0
  80. package/lib/types/cad_connector/zw3d.d.ts +3 -0
  81. package/lib/types/convert/dcprt.d.ts +14 -0
  82. package/lib/types/convert/detect.d.ts +7 -0
  83. package/lib/types/convert/dxf.d.ts +3 -0
  84. package/lib/types/convert/index.d.ts +9 -0
  85. package/lib/types/convert/obj.d.ts +7 -0
  86. package/lib/types/convert/png.d.ts +8 -0
  87. package/lib/types/convert/step.d.ts +4 -0
  88. package/lib/types/convert/stl.d.ts +7 -0
  89. package/lib/types/convert/svg.d.ts +8 -0
  90. package/lib/types/feature_script/dc_asm.d.ts +40 -0
  91. package/lib/types/feature_script/dc_engineering.d.ts +55 -0
  92. package/lib/types/feature_script/dc_prt.d.ts +37 -0
  93. package/lib/types/feature_script/index.d.ts +7 -0
  94. package/lib/types/image/profile.d.ts +28 -0
  95. package/lib/types/index.d.ts +18 -0
  96. package/lib/types/modeling/assembly.d.ts +17 -0
  97. package/lib/types/modeling/bin-format.d.ts +42 -0
  98. package/lib/types/modeling/bin-store.d.ts +20 -0
  99. package/lib/types/modeling/client.d.ts +125 -0
  100. package/lib/types/modeling/document.d.ts +26 -0
  101. package/lib/types/modeling/drawing.d.ts +38 -0
  102. package/lib/types/routes.d.ts +27 -0
  103. package/lib/types/store.d.ts +18 -0
  104. package/lib/types/tools/cad-freecad.d.ts +10 -0
  105. package/lib/types/tools/cad-fusion.d.ts +8 -0
  106. package/lib/types/tools/cad-image.d.ts +8 -0
  107. package/lib/types/tools/cad-info.d.ts +6 -0
  108. package/lib/types/tools/cad-model.d.ts +13 -0
  109. package/lib/types/tools/cad-view.d.ts +10 -0
  110. package/lib/types/tools/util.d.ts +4 -0
  111. package/lib/types/types.d.ts +142 -0
  112. package/lib/types.js +25 -0
  113. package/package.json +91 -0
@@ -0,0 +1,38 @@
1
+ import { encodeF32B64, encodeU32B64 } from '../b64.js';
2
+ import { runModelOp } from '../modeling/client.js';
3
+ import { isDcPrtDocument } from '../feature_script/dc_prt.js';
4
+ /** Convert a .dcprt buffer to indexed CadMeshes by replaying its features. */
5
+ export async function parseDcprt(buffer, fallbackName, timeoutMs = 180_000) {
6
+ let doc;
7
+ try {
8
+ doc = JSON.parse(buffer.toString('utf8'));
9
+ }
10
+ catch (error) {
11
+ throw new Error(`the DCPRT file is not valid JSON: ${error.message}`);
12
+ }
13
+ if (!isDcPrtDocument(doc)) {
14
+ throw new Error('not a DCPRT part document (missing dcprt header or features)');
15
+ }
16
+ await runModelOp({ kind: 'reset' }, timeoutMs);
17
+ for (const [index, feature] of doc.features.entries()) {
18
+ try {
19
+ await runModelOp(feature, timeoutMs);
20
+ }
21
+ catch (error) {
22
+ throw new Error(`DCPRT replay failed at feature ${index} (${feature.kind}): ${error.message}`);
23
+ }
24
+ }
25
+ const tessellated = await runModelOp({ kind: 'tessellate_all' }, timeoutMs);
26
+ if (tessellated.meshes === undefined || tessellated.meshes.length === 0) {
27
+ throw new Error('the DCPRT document replays to no bodies');
28
+ }
29
+ const names = new Map(doc.bodies.map((body) => [body.bodyId, body.name]));
30
+ return tessellated.meshes.map((mesh, index) => ({
31
+ name: names.get(mesh.bodyId) ?? mesh.name ?? `${fallbackName} #${index + 1}`,
32
+ positions: encodeF32B64(mesh.positions),
33
+ normals: mesh.normals === undefined ? undefined : encodeF32B64(mesh.normals),
34
+ indices: encodeU32B64(mesh.indices),
35
+ vertexCount: mesh.positions.length / 3,
36
+ triangleCount: mesh.indices.length / 3,
37
+ }));
38
+ }
@@ -0,0 +1,22 @@
1
+ const EXTENSION_TO_FORMAT = {
2
+ stl: 'stl',
3
+ obj: 'obj',
4
+ step: 'step',
5
+ stp: 'step',
6
+ iges: 'iges',
7
+ igs: 'iges',
8
+ brep: 'brep',
9
+ dxf: 'dxf',
10
+ svg: 'svg',
11
+ dcprt: 'dcprt',
12
+ };
13
+ export function detectFormat(fileName) {
14
+ const dot = fileName.lastIndexOf('.');
15
+ if (dot < 0)
16
+ return null;
17
+ const extension = fileName.slice(dot + 1).toLowerCase();
18
+ return EXTENSION_TO_FORMAT[extension] ?? null;
19
+ }
20
+ export function is3DFormat(format) {
21
+ return format === 'stl' || format === 'obj' || format === 'step' || format === 'iges' || format === 'brep' || format === 'dcprt';
22
+ }
@@ -0,0 +1,124 @@
1
+ /**
2
+ * DXF → CadDrawing2D conversion via dxf-parser. Maps the entity subset the
3
+ * viewport renders; unknown entity types are counted, not failed.
4
+ *
5
+ * dxf-parser ships as CJS with `module.exports = class`; load it through
6
+ * createRequire to sidestep ESM-interop typing friction.
7
+ */
8
+ import { createRequire } from 'node:module';
9
+ const require = createRequire(import.meta.url);
10
+ const DxfParser = require('dxf-parser');
11
+ /** DXF AutoCAD color index → 0xRRGGBB (subset). 0 = ByBlock/ByLayer. */ const ACI_COLORS = {
12
+ 1: 0xff0000, 2: 0xffff00, 3: 0x00ff00, 4: 0x00ffff, 5: 0x0000ff, 6: 0xff00ff,
13
+ 7: 0xffffff, 8: 0x808080, 9: 0xc0c0c0,
14
+ 30: 0xff5f00, 40: 0xff8700, 50: 0xffaf00, 60: 0xffd700, 90: 0xaf8700,
15
+ };
16
+ function aciColor(colorIndex) {
17
+ if (colorIndex === undefined || colorIndex === 0)
18
+ return undefined;
19
+ return ACI_COLORS[colorIndex];
20
+ }
21
+ function bulgedPolylinePoints(vertices) {
22
+ // Bulge arcs are approximated by the chord polyline; exact arc tessellation
23
+ // is deferred work. Straight segments pass through unchanged.
24
+ const points = [];
25
+ for (const vertex of vertices)
26
+ points.push(vertex.x, vertex.y);
27
+ return points;
28
+ }
29
+ /** Convert DXF text into a 2D drawing. */
30
+ export function parseDXF(text) {
31
+ const dxf = new DxfParser().parseSync(text) ?? { entities: [] };
32
+ const entities = [];
33
+ const layers = new Set();
34
+ let minX = Infinity;
35
+ let minY = Infinity;
36
+ let maxX = -Infinity;
37
+ let maxY = -Infinity;
38
+ const track = (x, y) => {
39
+ if (!Number.isFinite(x) || !Number.isFinite(y))
40
+ return;
41
+ minX = Math.min(minX, x);
42
+ minY = Math.min(minY, y);
43
+ maxX = Math.max(maxX, x);
44
+ maxY = Math.max(maxY, y);
45
+ };
46
+ for (const entity of dxf.entities ?? []) {
47
+ const layer = typeof entity.layer === 'string' && entity.layer !== '' ? entity.layer : undefined;
48
+ if (layer !== undefined)
49
+ layers.add(layer);
50
+ const color = aciColor(entity.colorIndex);
51
+ switch (entity.type) {
52
+ case 'LINE': {
53
+ // dxf-parser exposes LINE endpoints as a two-entry vertices array.
54
+ const line = entity;
55
+ const from = line.vertices?.[0];
56
+ const to = line.vertices?.[1];
57
+ if (from === undefined || to === undefined)
58
+ break;
59
+ entities.push({ type: 'line', x1: from.x, y1: from.y, x2: to.x, y2: to.y, layer, color });
60
+ track(from.x, from.y);
61
+ track(to.x, to.y);
62
+ break;
63
+ }
64
+ case 'CIRCLE': {
65
+ const circle = entity;
66
+ entities.push({ type: 'circle', cx: circle.center.x, cy: circle.center.y, r: circle.radius, layer, color });
67
+ track(circle.center.x - circle.radius, circle.center.y - circle.radius);
68
+ track(circle.center.x + circle.radius, circle.center.y + circle.radius);
69
+ break;
70
+ }
71
+ case 'ARC': {
72
+ const arc = entity;
73
+ entities.push({
74
+ type: 'arc', cx: arc.center.x, cy: arc.center.y, r: arc.radius,
75
+ startAngle: arc.startAngle, endAngle: arc.endAngle, layer, color,
76
+ });
77
+ track(arc.center.x - arc.radius, arc.center.y - arc.radius);
78
+ track(arc.center.x + arc.radius, arc.center.y + arc.radius);
79
+ break;
80
+ }
81
+ case 'LWPOLYLINE':
82
+ case 'POLYLINE': {
83
+ const polyline = entity;
84
+ const points = bulgedPolylinePoints(polyline.vertices ?? []);
85
+ if (points.length >= 4) {
86
+ entities.push({ type: 'polyline', points, closed: polyline.shape === true, layer, color });
87
+ for (let i = 0; i < points.length; i += 2)
88
+ track(points[i], points[i + 1]);
89
+ }
90
+ break;
91
+ }
92
+ case 'TEXT':
93
+ case 'MTEXT': {
94
+ const textEntity = entity;
95
+ const anchor = textEntity.startPoint ?? textEntity.position;
96
+ const content = textEntity.text;
97
+ if (anchor !== undefined && content !== undefined && content !== '') {
98
+ entities.push({
99
+ type: 'text', x: anchor.x, y: anchor.y, text: content,
100
+ height: textEntity.textHeight ?? 1, rotation: textEntity.rotation ?? 0, layer, color,
101
+ });
102
+ track(anchor.x, anchor.y);
103
+ }
104
+ break;
105
+ }
106
+ default:
107
+ // Unknown entity kinds are skipped; the count still reflects them.
108
+ break;
109
+ }
110
+ }
111
+ if (!Number.isFinite(minX)) {
112
+ minX = 0;
113
+ minY = 0;
114
+ maxX = 0;
115
+ maxY = 0;
116
+ }
117
+ return {
118
+ kind: '2d',
119
+ format: 'dxf',
120
+ entities,
121
+ bounds: { min: { x: minX, y: minY }, max: { x: maxX, y: maxY } },
122
+ layers: [...layers],
123
+ };
124
+ }
@@ -0,0 +1,70 @@
1
+ import { parseSTL } from './stl.js';
2
+ import { parseOBJ } from './obj.js';
3
+ import { parseDXF } from './dxf.js';
4
+ import { parseSVG } from './svg.js';
5
+ import { parseOcct } from './step.js';
6
+ import { parseDcprt } from './dcprt.js';
7
+ function meshBounds(meshes) {
8
+ let minX = Infinity;
9
+ let minY = Infinity;
10
+ let minZ = Infinity;
11
+ let maxX = -Infinity;
12
+ let maxY = -Infinity;
13
+ let maxZ = -Infinity;
14
+ // Decode each mesh's base64 positions once and scan the xyz triplets.
15
+ for (const mesh of meshes) {
16
+ const positions = Buffer.from(mesh.positions, 'base64');
17
+ for (let offset = 0; offset + 12 <= positions.length; offset += 12) {
18
+ const x = positions.readFloatLE(offset);
19
+ const y = positions.readFloatLE(offset + 4);
20
+ const z = positions.readFloatLE(offset + 8);
21
+ if (x < minX)
22
+ minX = x;
23
+ if (y < minY)
24
+ minY = y;
25
+ if (z < minZ)
26
+ minZ = z;
27
+ if (x > maxX)
28
+ maxX = x;
29
+ if (y > maxY)
30
+ maxY = y;
31
+ if (z > maxZ)
32
+ maxZ = z;
33
+ }
34
+ }
35
+ return { min: { x: minX, y: minY, z: minZ }, max: { x: maxX, y: maxY, z: maxZ } };
36
+ }
37
+ /** Scan a set of CadMeshes for their axis-aligned bounds. */
38
+ export function computeMeshBounds(meshes) {
39
+ return meshBounds(meshes);
40
+ }
41
+ /** Convert a supported CAD file buffer into a renderable scene. */
42
+ export async function convert(buffer, format, fileName) {
43
+ const stem = fileName.replace(/\.[^.]+$/, '');
44
+ switch (format) {
45
+ case 'stl': {
46
+ const meshes = [parseSTL(buffer, stem)];
47
+ return { kind: '3d', format, meshes, bounds: meshBounds(meshes), units: 'unitless' };
48
+ }
49
+ case 'obj': {
50
+ const meshes = parseOBJ(buffer.toString('utf8'), stem);
51
+ if (meshes.length === 0)
52
+ throw new Error('the OBJ file contains no faces');
53
+ return { kind: '3d', format, meshes, bounds: meshBounds(meshes), units: 'unitless' };
54
+ }
55
+ case 'step':
56
+ case 'iges':
57
+ case 'brep': {
58
+ const meshes = await parseOcct(buffer, format, stem);
59
+ return { kind: '3d', format, meshes, bounds: meshBounds(meshes), units: 'mm' };
60
+ }
61
+ case 'dcprt': {
62
+ const meshes = await parseDcprt(buffer, stem);
63
+ return { kind: '3d', format, meshes, bounds: meshBounds(meshes), units: 'mm' };
64
+ }
65
+ case 'dxf':
66
+ return parseDXF(buffer.toString('utf8'));
67
+ case 'svg':
68
+ return parseSVG(buffer.toString('utf8'));
69
+ }
70
+ }
@@ -0,0 +1,106 @@
1
+ import { encodeF32B64, encodeU32B64 } from '../b64.js';
2
+ function parseObj(text) {
3
+ const model = { positions: [], normals: [], groups: [{ name: '', faces: [] }] };
4
+ let currentGroup = model.groups[0];
5
+ for (const rawLine of text.split(/\r?\n/)) {
6
+ const line = rawLine.trim();
7
+ if (line === '' || line.startsWith('#'))
8
+ continue;
9
+ const space = line.indexOf(' ');
10
+ if (space < 0)
11
+ continue;
12
+ const keyword = line.slice(0, space);
13
+ const rest = line.slice(space + 1).trim();
14
+ if (keyword === 'v') {
15
+ const parts = rest.split(/\s+/);
16
+ model.positions.push(Number(parts[0]), Number(parts[1]), Number(parts[2]));
17
+ }
18
+ else if (keyword === 'vn') {
19
+ const parts = rest.split(/\s+/);
20
+ model.normals.push(Number(parts[0]), Number(parts[1]), Number(parts[2]));
21
+ }
22
+ else if (keyword === 'o' || keyword === 'g') {
23
+ const name = rest.split(/\s+/)[0] ?? '';
24
+ currentGroup = { name, faces: [] };
25
+ model.groups.push(currentGroup);
26
+ }
27
+ else if (keyword === 'f') {
28
+ const vertices = [];
29
+ const normalVertices = [];
30
+ for (const token of rest.split(/\s+/)) {
31
+ if (token === '')
32
+ continue;
33
+ // Forms: v, v/vt, v//vn, v/vt/vn
34
+ const segments = token.split('/');
35
+ const vertexIndex = Number(segments[0]);
36
+ if (!Number.isFinite(vertexIndex) || vertexIndex === 0)
37
+ continue;
38
+ // OBJ indices are 1-based; negatives count back from the end.
39
+ const resolved = vertexIndex > 0 ? vertexIndex - 1 : model.positions.length / 3 + vertexIndex;
40
+ vertices.push(resolved);
41
+ const normalToken = segments[2];
42
+ if (normalToken !== undefined && normalToken !== '') {
43
+ const normalIndex = Number(normalToken);
44
+ const resolvedNormal = normalIndex > 0 ? normalIndex - 1 : model.normals.length / 3 + normalIndex;
45
+ normalVertices.push(resolvedNormal);
46
+ }
47
+ }
48
+ if (vertices.length >= 3)
49
+ currentGroup.faces.push({ vertices, normalVertices });
50
+ }
51
+ }
52
+ return model;
53
+ }
54
+ /** Fan-triangulate one face loop. */
55
+ function fanTriangles(loop) {
56
+ const triangles = [];
57
+ for (let i = 1; i < loop.length - 1; i++) {
58
+ triangles.push([loop[0], loop[i], loop[i + 1]]);
59
+ }
60
+ return triangles;
61
+ }
62
+ /** Convert OBJ text into CadMeshes (one per non-empty group). */
63
+ export function parseOBJ(text, fallbackName) {
64
+ const model = parseObj(text);
65
+ const meshes = [];
66
+ for (const group of model.groups) {
67
+ if (group.faces.length === 0)
68
+ continue;
69
+ const positions = [];
70
+ const normals = [];
71
+ const indices = [];
72
+ const vertexMap = new Map();
73
+ const hasNormals = group.faces.some((face) => face.normalVertices.length === face.vertices.length);
74
+ for (const face of group.faces) {
75
+ const faceNormals = face.normalVertices.length === face.vertices.length ? face.normalVertices : null;
76
+ for (const [a, b, c] of fanTriangles(face.vertices)) {
77
+ for (const vertexIndex of [a, b, c]) {
78
+ const px = model.positions[vertexIndex * 3] ?? 0;
79
+ const py = model.positions[vertexIndex * 3 + 1] ?? 0;
80
+ const pz = model.positions[vertexIndex * 3 + 2] ?? 0;
81
+ const normalIndex = faceNormals?.[vertexIndex];
82
+ const key = normalIndex === undefined ? `${vertexIndex}` : `${vertexIndex}/${normalIndex}`;
83
+ let index = vertexMap.get(key);
84
+ if (index === undefined) {
85
+ index = positions.length / 3;
86
+ vertexMap.set(key, index);
87
+ positions.push(px, py, pz);
88
+ if (hasNormals) {
89
+ normals.push(normalIndex === undefined ? 0 : model.normals[normalIndex * 3] ?? 0, normalIndex === undefined ? 0 : model.normals[normalIndex * 3 + 1] ?? 0, normalIndex === undefined ? 0 : model.normals[normalIndex * 3 + 2] ?? 0);
90
+ }
91
+ }
92
+ indices.push(index);
93
+ }
94
+ }
95
+ }
96
+ meshes.push({
97
+ name: group.name === '' ? fallbackName : group.name,
98
+ positions: encodeF32B64(Float32Array.from(positions)),
99
+ normals: hasNormals ? encodeF32B64(Float32Array.from(normals)) : undefined,
100
+ indices: encodeU32B64(Uint32Array.from(indices)),
101
+ vertexCount: positions.length / 3,
102
+ triangleCount: indices.length / 3,
103
+ });
104
+ }
105
+ return meshes;
106
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Minimal PNG decoder for the image-profile pipeline: 8-bit non-interlaced
3
+ * grayscale / grayscale+alpha / RGB / RGBA, decoding to a luma map. Zero
4
+ * dependencies (node:zlib); unsupported variants fail with explicit errors.
5
+ */
6
+ import { inflateSync } from 'node:zlib';
7
+ const SIGNATURE = Buffer.from([0x89, 0x50, 0x4e, 0x47, 0x0d, 0x0a, 0x1a, 0x0a]);
8
+ const COLOR_CHANNELS = { 0: 1, 4: 2, 2: 3, 6: 4 };
9
+ /** Decode a PNG buffer to a luma image. */
10
+ export function decodePng(buffer) {
11
+ if (buffer.length < 8 || !buffer.subarray(0, 8).equals(SIGNATURE)) {
12
+ throw new Error('not a PNG file (bad signature)');
13
+ }
14
+ let offset = 8;
15
+ let width = 0;
16
+ let height = 0;
17
+ let bitDepth = 0;
18
+ let colorType = -1;
19
+ const idat = [];
20
+ let seenIhdr = false;
21
+ while (offset + 8 <= buffer.length) {
22
+ const length = buffer.readUInt32BE(offset);
23
+ const type = buffer.toString('ascii', offset + 4, offset + 8);
24
+ const data = buffer.subarray(offset + 8, offset + 8 + length);
25
+ if (type === 'IHDR') {
26
+ width = data.readUInt32BE(0);
27
+ height = data.readUInt32BE(4);
28
+ bitDepth = data[8];
29
+ colorType = data[9];
30
+ const compression = data[10];
31
+ const filterMethod = data[11];
32
+ const interlace = data[12];
33
+ if (bitDepth !== 8)
34
+ throw new Error(`unsupported PNG bit depth ${bitDepth} (only 8-bit)`);
35
+ if (!(colorType in COLOR_CHANNELS))
36
+ throw new Error(`unsupported PNG color type ${colorType}`);
37
+ if (compression !== 0 || filterMethod !== 0)
38
+ throw new Error('unsupported PNG compression/filter method');
39
+ if (interlace !== 0)
40
+ throw new Error('interlaced PNG is not supported');
41
+ seenIhdr = true;
42
+ }
43
+ else if (type === 'IDAT') {
44
+ idat.push(data);
45
+ }
46
+ else if (type === 'IEND') {
47
+ break;
48
+ }
49
+ offset += 12 + length;
50
+ }
51
+ if (!seenIhdr || width === 0 || height === 0)
52
+ throw new Error('the PNG has no usable IHDR chunk');
53
+ const channels = COLOR_CHANNELS[colorType];
54
+ const bytesPerPixel = channels;
55
+ const stride = width * bytesPerPixel;
56
+ const raw = inflateSync(Buffer.concat(idat));
57
+ if (raw.length < (stride + 1) * height)
58
+ throw new Error('the PNG pixel data is truncated');
59
+ const pixels = unfilter(raw, width, height, bytesPerPixel);
60
+ return { width, height, gray: toLuma(pixels, width, height, channels) };
61
+ }
62
+ function paeth(a, b, c) {
63
+ const p = a + b - c;
64
+ const pa = Math.abs(p - a);
65
+ const pb = Math.abs(p - b);
66
+ const pc = Math.abs(p - c);
67
+ if (pa <= pb && pa <= pc)
68
+ return a;
69
+ if (pb <= pc)
70
+ return b;
71
+ return c;
72
+ }
73
+ function unfilter(raw, width, height, bytesPerPixel) {
74
+ const stride = width * bytesPerPixel;
75
+ const out = new Uint8Array(stride * height);
76
+ let input = 0;
77
+ for (let y = 0; y < height; y++) {
78
+ const filter = raw[input++];
79
+ const row = y * stride;
80
+ const prior = row - stride;
81
+ for (let x = 0; x < stride; x++) {
82
+ const value = raw[input++];
83
+ const left = x >= bytesPerPixel ? out[row + x - bytesPerPixel] : 0;
84
+ const up = y > 0 ? out[prior + x] : 0;
85
+ const upLeft = y > 0 && x >= bytesPerPixel ? out[prior + x - bytesPerPixel] : 0;
86
+ let result = value;
87
+ if (filter === 1)
88
+ result = value + left;
89
+ else if (filter === 2)
90
+ result = value + up;
91
+ else if (filter === 3)
92
+ result = value + ((left + up) >> 1);
93
+ else if (filter === 4)
94
+ result = value + paeth(left, up, upLeft);
95
+ else if (filter !== 0)
96
+ throw new Error(`unknown PNG row filter ${filter}`);
97
+ out[row + x] = result & 0xff;
98
+ }
99
+ }
100
+ return out;
101
+ }
102
+ function toLuma(pixels, width, height, channels) {
103
+ const gray = new Uint8Array(width * height);
104
+ for (let i = 0; i < gray.length; i++) {
105
+ const o = i * channels;
106
+ if (channels === 1)
107
+ gray[i] = pixels[o];
108
+ else if (channels === 2)
109
+ gray[i] = pixels[o];
110
+ else
111
+ gray[i] = Math.round(0.299 * pixels[o] + 0.587 * pixels[o + 1] + 0.114 * pixels[o + 2]);
112
+ }
113
+ return gray;
114
+ }
@@ -0,0 +1,61 @@
1
+ /**
2
+ * occt-import-js worker: parses STEP/IGES/BREP in a worker thread so the
3
+ * WASM tessellation never blocks the agent's event loop. Plain JavaScript —
4
+ * this file is copied verbatim into lib/ by the build.
5
+ */
6
+ import { createRequire } from 'node:module'
7
+ import { parentPort } from 'node:worker_threads'
8
+
9
+ const require = createRequire(import.meta.url)
10
+
11
+ async function main() {
12
+ if (parentPort === null) return
13
+ const occtimportjs = require('occt-import-js')
14
+ const occt = await occtimportjs()
15
+
16
+ parentPort.on('message', (request) => {
17
+ try {
18
+ const bytes = new Uint8Array(request.buffer)
19
+ const result =
20
+ request.format === 'step' ? occt.ReadStepFile(bytes, null)
21
+ : request.format === 'iges' ? occt.ReadIgesFile(bytes, null)
22
+ : occt.ReadBrepFile(bytes, null)
23
+ if (!result.success || result.meshes === undefined || result.meshes.length === 0) {
24
+ parentPort.postMessage({ jobId: request.jobId, ok: false, error: `the ${request.format.toUpperCase()} file parsed but contains no tessellatable geometry` })
25
+ return
26
+ }
27
+ const transfers = []
28
+ const meshes = result.meshes.map((mesh) => {
29
+ const positionArray = mesh.attributes?.position?.array
30
+ if (positionArray === undefined || positionArray.length < 9) return null
31
+ const positions = Float32Array.from(positionArray)
32
+ const normalSource = mesh.attributes?.normal?.array
33
+ const normals = normalSource !== undefined && normalSource.length === positions.length ? Float32Array.from(normalSource) : null
34
+ const indexSource = mesh.index?.array
35
+ const indices = indexSource !== undefined && indexSource.length > 0
36
+ ? Uint32Array.from(indexSource)
37
+ : Uint32Array.from({ length: positions.length / 3 }, (_, i) => i)
38
+ transfers.push(positions.buffer, indices.buffer)
39
+ if (normals !== null) transfers.push(normals.buffer)
40
+ const color = mesh.color
41
+ return {
42
+ name: mesh.name && mesh.name !== '' ? mesh.name : 'part',
43
+ positions,
44
+ normals: normals === null ? undefined : normals,
45
+ indices,
46
+ color: color === undefined ? undefined
47
+ : (Math.round(color[0] * 255) << 16) | (Math.round(color[1] * 255) << 8) | Math.round(color[2] * 255),
48
+ }
49
+ }).filter((mesh) => mesh !== null)
50
+ if (meshes.length === 0) {
51
+ parentPort.postMessage({ jobId: request.jobId, ok: false, error: `the ${request.format.toUpperCase()} file contains no meshes with geometry` })
52
+ return
53
+ }
54
+ parentPort.postMessage({ jobId: request.jobId, ok: true, meshes }, transfers)
55
+ } catch (error) {
56
+ parentPort.postMessage({ jobId: request.jobId, ok: false, error: error instanceof Error ? error.message : String(error) })
57
+ }
58
+ })
59
+ }
60
+
61
+ void main()
@@ -0,0 +1,66 @@
1
+ /**
2
+ * Main-thread wrapper around the occt-import-js worker: one lazily spawned
3
+ * worker, job correlation, transferable buffers, and cooperative timeouts.
4
+ */
5
+ import { Worker } from 'node:worker_threads';
6
+ import { encodeF32B64, encodeU32B64 } from '../b64.js';
7
+ let worker = null;
8
+ let nextJobId = 1;
9
+ const pending = new Map();
10
+ function ensureWorker() {
11
+ if (worker !== null)
12
+ return worker;
13
+ const spawned = new Worker(new URL('./step-worker.mjs', import.meta.url));
14
+ spawned.on('message', (response) => {
15
+ const job = pending.get(response.jobId);
16
+ if (job === undefined)
17
+ return;
18
+ pending.delete(response.jobId);
19
+ clearTimeout(job.timer);
20
+ if (response.ok)
21
+ job.resolve(response.meshes);
22
+ else
23
+ job.reject(new Error(response.error));
24
+ });
25
+ spawned.on('error', (error) => {
26
+ failAll(error instanceof Error ? error : new Error(String(error)));
27
+ });
28
+ spawned.on('exit', (code) => {
29
+ if (code !== 0)
30
+ failAll(new Error(`the geometry worker exited unexpectedly (code ${code})`));
31
+ worker = null;
32
+ });
33
+ worker = spawned;
34
+ return spawned;
35
+ }
36
+ function failAll(error) {
37
+ for (const [jobId, job] of pending) {
38
+ pending.delete(jobId);
39
+ clearTimeout(job.timer);
40
+ job.reject(error);
41
+ }
42
+ }
43
+ /** Convert a STEP/IGES/BREP buffer to indexed CadMeshes. */
44
+ export async function parseOcct(buffer, format, fallbackName, timeoutMs = 120_000) {
45
+ const active = ensureWorker();
46
+ const jobId = nextJobId++;
47
+ const copy = new Uint8Array(buffer.byteLength);
48
+ copy.set(buffer);
49
+ const meshes = await new Promise((resolve, reject) => {
50
+ const timer = setTimeout(() => {
51
+ pending.delete(jobId);
52
+ reject(new Error(`${format.toUpperCase()} conversion timed out after ${timeoutMs}ms`));
53
+ }, timeoutMs);
54
+ pending.set(jobId, { resolve, reject, timer });
55
+ active.postMessage({ jobId, format, buffer: copy.buffer }, [copy.buffer]);
56
+ });
57
+ return meshes.map((mesh, index) => ({
58
+ name: mesh.name === '' || mesh.name === undefined ? `${fallbackName} #${index + 1}` : mesh.name,
59
+ positions: encodeF32B64(mesh.positions),
60
+ normals: mesh.normals === undefined ? undefined : encodeF32B64(mesh.normals),
61
+ indices: encodeU32B64(mesh.indices),
62
+ vertexCount: mesh.positions.length / 3,
63
+ triangleCount: mesh.indices.length / 3,
64
+ color: mesh.color,
65
+ }));
66
+ }