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,324 @@
1
+ /** Landscape paper in mm. */
2
+ const PAPER = {
3
+ A4: { width: 297, height: 210 },
4
+ A3: { width: 420, height: 297 },
5
+ };
6
+ const FRAME = 10; // inner frame inset from the paper edge
7
+ const GAP = 22; // breathing room between views
8
+ const TITLE_BLOCK = { width: 130, height: 28 };
9
+ /** GB-preferred scale series (values < 1 reduce, > 1 enlarge). */
10
+ const SCALE_SERIES = [5, 2, 1, 0.5, 0.4, 0.25, 0.2, 0.1, 0.05, 0.02, 0.01];
11
+ const COLORS = {
12
+ visible: 0x1a1d21,
13
+ hidden: 0x98a0ab,
14
+ dim: 0x2456a8,
15
+ paper: 0xb6bdc7,
16
+ text: 0x3d434c,
17
+ };
18
+ /** (x, y) bounds of a view's projected polylines; null when empty. */
19
+ function viewBounds(view) {
20
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity;
21
+ for (const polyline of [...view.visible, ...view.hidden]) {
22
+ for (let i = 0; i + 1 < polyline.length; i += 2) {
23
+ if (polyline[i] < minX)
24
+ minX = polyline[i];
25
+ if (polyline[i] > maxX)
26
+ maxX = polyline[i];
27
+ if (polyline[i + 1] < minY)
28
+ minY = polyline[i + 1];
29
+ if (polyline[i + 1] > maxY)
30
+ maxY = polyline[i + 1];
31
+ }
32
+ }
33
+ return Number.isFinite(minX) ? { minX, minY, maxX, maxY } : null;
34
+ }
35
+ function sizeOf(bounds) {
36
+ return bounds === null ? { w: 0, h: 0 } : { w: bounds.maxX - bounds.minX, h: bounds.maxY - bounds.minY };
37
+ }
38
+ /** Largest GB scale that keeps `size` inside `cell`. */
39
+ function fitScale(viewW, viewH, cellW, cellH) {
40
+ if (viewW <= 1e-9 || viewH <= 1e-9)
41
+ return 1;
42
+ const raw = Math.min(cellW / viewW, cellH / viewH);
43
+ for (const scale of SCALE_SERIES) {
44
+ if (scale <= raw)
45
+ return scale;
46
+ }
47
+ return SCALE_SERIES[SCALE_SERIES.length - 1] ?? 0.01;
48
+ }
49
+ /** Scale a view's projected polylines (layout works in paper mm). */
50
+ function scaleView(view, s) {
51
+ if (s === 1)
52
+ return view;
53
+ const scalePoly = (polyline) => polyline.map((v) => v * s);
54
+ return { name: view.name, visible: view.visible.map(scalePoly), hidden: view.hidden.map(scalePoly) };
55
+ }
56
+ /** Translation that centers a (scaled) view on (ox, oy). */
57
+ function centerTransform(view, ox, oy) {
58
+ const bounds = viewBounds(view);
59
+ if (bounds === null)
60
+ return { tx: ox, ty: oy };
61
+ return { tx: ox - (bounds.minX + bounds.maxX) / 2, ty: oy - (bounds.minY + bounds.maxY) / 2 };
62
+ }
63
+ function trim(value) {
64
+ return Number(value.toFixed(1)).toString();
65
+ }
66
+ function scaleText(scale) {
67
+ return scale >= 1 ? `${trim(scale)}:1` : `1:${trim(1 / scale)}`;
68
+ }
69
+ /**
70
+ * A linear dimension: extension lines from the measure points, a dimension
71
+ * line offset perpendicular by `offset`, arrowhead strokes, and the measured
72
+ * text centered above the dimension line. `normal` is the side the dimension
73
+ * line sits on (unit vector).
74
+ */
75
+ function linearDim(entities, p1, p2, normal, offset, text) {
76
+ const dx = p2[0] - p1[0];
77
+ const dy = p2[1] - p1[1];
78
+ const len = Math.hypot(dx, dy);
79
+ if (len < 1e-9)
80
+ return;
81
+ const ux = dx / len;
82
+ const uy = dy / len;
83
+ const [nx, ny] = normal;
84
+ const a1 = [p1[0] + nx * offset, p1[1] + ny * offset];
85
+ const a2 = [p2[0] + nx * offset, p2[1] + ny * offset];
86
+ const gap = 1.2;
87
+ const over = 2.5;
88
+ const extension = (p) => {
89
+ entities.push({
90
+ type: 'line',
91
+ x1: p[0] + nx * gap, y1: p[1] + ny * gap,
92
+ x2: p[0] + nx * (offset + over), y2: p[1] + ny * (offset + over),
93
+ layer: 'dim', color: COLORS.dim,
94
+ });
95
+ };
96
+ extension(p1);
97
+ extension(p2);
98
+ entities.push({ type: 'line', x1: a1[0], y1: a1[1], x2: a2[0], y2: a2[1], layer: 'dim', color: COLORS.dim });
99
+ // arrowheads: two strokes each, ~30° half angle, 3mm
100
+ for (const [at, sign] of [[a1, 1], [a2, -1]]) {
101
+ for (const side of [1, -1]) {
102
+ entities.push({
103
+ type: 'line',
104
+ x1: at[0], y1: at[1],
105
+ x2: at[0] + (ux * 0.87 + -uy * side * 0.5) * 3 * sign,
106
+ y2: at[1] + (uy * 0.87 + ux * side * 0.5) * 3 * sign,
107
+ layer: 'dim', color: COLORS.dim,
108
+ });
109
+ }
110
+ }
111
+ const mid = [(a1[0] + a2[0]) / 2 + nx * 1.6, (a1[1] + a2[1]) / 2 + ny * 1.6];
112
+ let rotation = (Math.atan2(uy, ux) * 180) / Math.PI;
113
+ if (rotation > 90 || rotation < -90)
114
+ rotation += 180;
115
+ entities.push({ type: 'text', x: mid[0], y: mid[1], text, height: 3.2, rotation, layer: 'dim', color: COLORS.dim });
116
+ }
117
+ /** Build the full drawing sheet (frame, title block, views, dimensions). */
118
+ export function buildDrawingSheet(input) {
119
+ const paper = PAPER[input.paper ?? 'A4'];
120
+ const { width, height } = paper;
121
+ const byName = new Map(input.views.map((view) => [view.name, view]));
122
+ const front = byName.get('front');
123
+ const top = byName.get('top');
124
+ const left = byName.get('left');
125
+ const iso = byName.get('iso');
126
+ // ── placements (dimensions collected into a side list, appended later) ────
127
+ const placed = [];
128
+ const dimEntities = [];
129
+ let scale = 1;
130
+ if (front !== undefined) {
131
+ const regionW = (width - FRAME * 2) * 0.64;
132
+ const regionH = height - FRAME * 2;
133
+ const cellW = (regionW - GAP) / 2;
134
+ const cellH = (regionH - GAP) / 2;
135
+ const pad = 20; // room for the dimension annotations inside each cell
136
+ const frontSize = sizeOf(viewBounds(front));
137
+ const leftSize = left !== undefined ? sizeOf(viewBounds(left)) : { w: 0, h: 0 };
138
+ const topSize = top !== undefined ? sizeOf(viewBounds(top)) : { w: 0, h: 0 };
139
+ const candidates = [fitScale(frontSize.w, frontSize.h, cellW - pad, cellH - pad)];
140
+ if (left !== undefined)
141
+ candidates.push(fitScale(leftSize.w, leftSize.h, cellW - pad, cellH - pad));
142
+ if (top !== undefined)
143
+ candidates.push(fitScale(topSize.w, topSize.h, cellW - pad, cellH - pad));
144
+ scale = Math.min(...candidates);
145
+ // GB first angle: 主视图 upper-left, 左视图 right of it, 俯视图 below it.
146
+ const rowY = FRAME + regionH - cellH / 2;
147
+ const frontCX = FRAME + cellW / 2 + 4;
148
+ const place = (raw, ox, oy) => {
149
+ const scaled = scaleView(raw, scale);
150
+ const { tx, ty } = centerTransform(scaled, ox, oy);
151
+ placed.push({ view: scaled, tx, ty });
152
+ };
153
+ place(front, frontCX, rowY);
154
+ if (left !== undefined)
155
+ place(left, FRAME + cellW * 1.5 + GAP, rowY);
156
+ if (top !== undefined)
157
+ place(top, frontCX, FRAME + cellH / 2);
158
+ // Overall dimensions off the scaled front/top geometry (mm on paper).
159
+ const fw = frontSize.w * scale;
160
+ const fh = frontSize.h * scale;
161
+ if (fw > 1e-9) {
162
+ linearDim(dimEntities, [frontCX - fw / 2, rowY - fh / 2], [frontCX + fw / 2, rowY - fh / 2], [0, -1], 9, trim(frontSize.w));
163
+ }
164
+ if (fh > 1e-9) {
165
+ linearDim(dimEntities, [frontCX - fw / 2, rowY - fh / 2], [frontCX - fw / 2, rowY + fh / 2], [-1, 0], 9, trim(frontSize.h));
166
+ }
167
+ if (top !== undefined && topSize.h > 1e-9) {
168
+ const topCY = FRAME + cellH / 2;
169
+ const tw = topSize.w * scale;
170
+ const td = topSize.h * scale;
171
+ linearDim(dimEntities, [frontCX + tw / 2, topCY - td / 2], [frontCX + tw / 2, topCY + td / 2], [1, 0], 9, trim(topSize.h));
172
+ }
173
+ }
174
+ // Iso: fit the right free column above the title block, independent scale.
175
+ if (iso !== undefined) {
176
+ const isoLeft = FRAME + (width - FRAME * 2) * 0.64 + GAP / 2;
177
+ const isoBottom = FRAME + TITLE_BLOCK.height + GAP / 2;
178
+ const cellW = Math.max(width - FRAME - isoLeft, 40);
179
+ const cellH = Math.max(height - FRAME - isoBottom, 40);
180
+ const isoSize = sizeOf(viewBounds(iso));
181
+ const s = fitScale(isoSize.w, isoSize.h, cellW - 10, cellH - 10);
182
+ const scaled = scaleView(iso, s);
183
+ const { tx, ty } = centerTransform(scaled, isoLeft + cellW / 2, isoBottom + cellH / 2);
184
+ placed.push({ view: scaled, tx, ty });
185
+ }
186
+ // ── entities ───────────────────────────────────────────────────────────────
187
+ const entities = [];
188
+ const frameRect = (x, y, w, h) => {
189
+ entities.push({ type: 'polyline', points: [x, y, x + w, y, x + w, y + h, x, y + h], closed: true, layer: 'paper', color: COLORS.paper });
190
+ };
191
+ frameRect(2, 2, width - 4, height - 4);
192
+ frameRect(FRAME, FRAME, width - FRAME * 2, height - FRAME * 2);
193
+ const tbX = width - FRAME - TITLE_BLOCK.width;
194
+ const tbY = FRAME;
195
+ frameRect(tbX, tbY, TITLE_BLOCK.width, TITLE_BLOCK.height);
196
+ entities.push({ type: 'line', x1: tbX, y1: tbY + TITLE_BLOCK.height / 2, x2: tbX + TITLE_BLOCK.width, y2: tbY + TITLE_BLOCK.height / 2, layer: 'paper', color: COLORS.paper });
197
+ entities.push({ type: 'line', x1: tbX + 78, y1: tbY, x2: tbX + 78, y2: tbY + TITLE_BLOCK.height, layer: 'paper', color: COLORS.paper });
198
+ const titleText = (x, y, text, size = 3.2) => {
199
+ entities.push({ type: 'text', x, y, text, height: size, layer: 'paper', color: COLORS.text });
200
+ };
201
+ titleText(tbX + 4, tbY + TITLE_BLOCK.height - 8, input.partName, 4.2);
202
+ titleText(tbX + 4, tbY + 6, `图号 ${input.drawingNo ?? '—'}`);
203
+ titleText(tbX + 44, tbY + 6, `比例 ${scaleText(scale)}`);
204
+ titleText(tbX + 82, tbY + 6, '单位 mm');
205
+ titleText(tbX + 82, tbY + TITLE_BLOCK.height / 2 - 4, `日期 ${input.date ?? ''}`);
206
+ entities.push(...dimEntities);
207
+ for (const item of placed) {
208
+ for (const polyline of item.view.visible) {
209
+ entities.push({
210
+ type: 'polyline',
211
+ points: polyline.map((v, i) => v + (i % 2 === 0 ? item.tx : item.ty)),
212
+ closed: false, layer: 'visible', color: COLORS.visible,
213
+ });
214
+ }
215
+ for (const polyline of item.view.hidden) {
216
+ entities.push({
217
+ type: 'polyline',
218
+ points: polyline.map((v, i) => v + (i % 2 === 0 ? item.tx : item.ty)),
219
+ closed: false, layer: 'hidden', color: COLORS.hidden,
220
+ });
221
+ }
222
+ }
223
+ return {
224
+ entities,
225
+ bounds: { min: { x: 0, y: 0 }, max: { x: width, y: height } },
226
+ layers: ['visible', 'hidden', 'dim', 'paper'],
227
+ width,
228
+ height,
229
+ scale,
230
+ entityCount: entities.length,
231
+ };
232
+ }
233
+ // ── export serializations ─────────────────────────────────────────────────────
234
+ const esc = (text) => text.replace(/&/g, '&amp;').replace(/</g, '&lt;').replace(/>/g, '&gt;').replace(/"/g, '&quot;');
235
+ /** Serialize the sheet to a standalone SVG (paper mm = user units, Y flipped). */
236
+ export function drawingToSvg(sheet) {
237
+ const { width, height, entities } = sheet;
238
+ const parts = [];
239
+ parts.push(`<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 ${width} ${height}" width="${width}mm" height="${height}mm">`);
240
+ parts.push(`<rect x="0" y="0" width="${width}" height="${height}" fill="#ffffff"/>`);
241
+ parts.push(`<g transform="translate(0,${height}) scale(1,-1)">`);
242
+ for (const entity of entities) {
243
+ switch (entity.type) {
244
+ case 'line':
245
+ parts.push(`<line x1="${entity.x1}" y1="${entity.y1}" x2="${entity.x2}" y2="${entity.y2}" stroke="${cssColor(entity)}" ${strokeStyle(entity)}/>`);
246
+ break;
247
+ case 'polyline': {
248
+ const points = [];
249
+ for (let i = 0; i + 1 < entity.points.length; i += 2)
250
+ points.push(`${entity.points[i]},${entity.points[i + 1]}`);
251
+ parts.push(`<polyline points="${points.join(' ')}" fill="none" stroke="${cssColor(entity)}" ${strokeStyle(entity)}/>`);
252
+ break;
253
+ }
254
+ case 'text':
255
+ parts.push(`<text x="${entity.x}" y="${entity.y}" font-size="${entity.height}" fill="${cssColor(entity)}" ` +
256
+ `transform="translate(0,${2 * entity.y}) scale(1,-1)" font-family="sans-serif">${esc(entity.text)}</text>`);
257
+ break;
258
+ default:
259
+ break;
260
+ }
261
+ }
262
+ parts.push('</g></svg>');
263
+ return parts.join('\n');
264
+ }
265
+ function cssColor(entity) {
266
+ const color = entity.color ?? 0x1a1d21;
267
+ return `#${color.toString(16).padStart(6, '0')}`;
268
+ }
269
+ function strokeStyle(entity) {
270
+ const width = entity.layer === 'hidden' || entity.layer === 'dim' ? 0.25 : 0.45;
271
+ const dash = entity.layer === 'hidden' ? ' stroke-dasharray="2,1.2"' : '';
272
+ return `stroke-width="${width}"${dash}`;
273
+ }
274
+ /** Minimal R12 ASCII DXF (LINE + TEXT entities; polylines expand to lines). */
275
+ export function drawingToDxf(sheet) {
276
+ const out = [];
277
+ const push = (code, value) => {
278
+ out.push(String(code), String(value));
279
+ };
280
+ push(0, 'SECTION');
281
+ push(2, 'ENTITIES');
282
+ for (const entity of sheet.entities) {
283
+ switch (entity.type) {
284
+ case 'line':
285
+ push(0, 'LINE');
286
+ push(8, entity.layer ?? '0');
287
+ push(10, entity.x1.toFixed(4));
288
+ push(20, entity.y1.toFixed(4));
289
+ push(30, '0');
290
+ push(11, entity.x2.toFixed(4));
291
+ push(21, entity.y2.toFixed(4));
292
+ push(31, '0');
293
+ break;
294
+ case 'polyline':
295
+ for (let i = 0; i + 3 < entity.points.length; i += 2) {
296
+ push(0, 'LINE');
297
+ push(8, entity.layer ?? '0');
298
+ push(10, entity.points[i].toFixed(4));
299
+ push(20, entity.points[i + 1].toFixed(4));
300
+ push(30, '0');
301
+ push(11, entity.points[i + 2].toFixed(4));
302
+ push(21, entity.points[i + 3].toFixed(4));
303
+ push(30, '0');
304
+ }
305
+ break;
306
+ case 'text':
307
+ push(0, 'TEXT');
308
+ push(8, entity.layer ?? '0');
309
+ push(10, entity.x.toFixed(4));
310
+ push(20, entity.y.toFixed(4));
311
+ push(30, '0');
312
+ push(40, entity.height.toFixed(3));
313
+ push(1, entity.text);
314
+ if (entity.rotation !== undefined)
315
+ push(50, entity.rotation.toFixed(2));
316
+ break;
317
+ default:
318
+ break;
319
+ }
320
+ }
321
+ push(0, 'ENDSEC');
322
+ push(0, 'EOF');
323
+ return out.join('\n');
324
+ }
@@ -0,0 +1,328 @@
1
+ /**
2
+ * Mesh-based hidden-line-removal projection (plain CJS — runs inside the
3
+ * modeling worker). The opencascade.js build exposes no HLRBRep bindings, so
4
+ * engineering-drawing views are computed from the tessellation instead:
5
+ *
6
+ * 1. candidate edges = sharp feature edges (dihedral > threshold, view
7
+ * independent) ∪ silhouette edges (one adjacent face front-facing, one
8
+ * back-facing — per view), which is what traces curved outlines;
9
+ * 2. each candidate is occlusion-tested against the projected triangle soup
10
+ * via a uniform 2D grid (depth-interpolated barycentric coverage);
11
+ * 3. surviving edges are chained into polylines and simplified (RDP), so
12
+ * the caller gets compact flat [x,y, x,y, …] paths per visibility class.
13
+ *
14
+ * Projected coordinates stay in model units (mm): u along the view's xDir,
15
+ * v = w × u (w = view direction toward the observer), so screen y is "up".
16
+ */
17
+ 'use strict'
18
+
19
+ /**
20
+ * Build the edge table keyed by quantized endpoint COORDINATES (not vertex
21
+ * indices): OCCT triangulations are per-face, so the same topological edge
22
+ * appears once per adjacent face under different indices — only coordinates
23
+ * merge them. Values accumulate adjacent face normals for the dihedral test.
24
+ */
25
+ function edgeTable(positions, indices) {
26
+ const q = (v) => Math.round(v * 1e4)
27
+ const edges = new Map()
28
+ const triCount = indices.length / 3
29
+ for (let t = 0; t < triCount; t++) {
30
+ const a = indices[t * 3]
31
+ const b = indices[t * 3 + 1]
32
+ const c = indices[t * 3 + 2]
33
+ const ax = positions[a * 3], ay = positions[a * 3 + 1], az = positions[a * 3 + 2]
34
+ const bx = positions[b * 3], by = positions[b * 3 + 1], bz = positions[b * 3 + 2]
35
+ const cx = positions[c * 3], cy = positions[c * 3 + 1], cz = positions[c * 3 + 2]
36
+ // Face normal (normalized; the winding is the orientation source of truth).
37
+ const ux = bx - ax, uy = by - ay, uz = bz - az
38
+ const vx = cx - ax, vy = cy - ay, vz = cz - az
39
+ let nx = uy * vz - uz * vy, ny = uz * vx - ux * vz, nz = ux * vy - uy * vx
40
+ const len = Math.hypot(nx, ny, nz) || 1
41
+ nx /= len; ny /= len; nz /= len
42
+ for (const [i, j] of [[a, b], [b, c], [c, a]]) {
43
+ const ka = `${q(positions[i * 3])},${q(positions[i * 3 + 1])},${q(positions[i * 3 + 2])}`
44
+ const kb = `${q(positions[j * 3])},${q(positions[j * 3 + 1])},${q(positions[j * 3 + 2])}`
45
+ const key = ka < kb ? `${ka};${kb}` : `${kb};${ka}`
46
+ let entry = edges.get(key)
47
+ if (entry === undefined) {
48
+ entry = { normals: [], x1: positions[i * 3], y1: positions[i * 3 + 1], z1: positions[i * 3 + 2], x2: positions[j * 3], y2: positions[j * 3 + 1], z2: positions[j * 3 + 2] }
49
+ edges.set(key, entry)
50
+ }
51
+ entry.normals.push([nx, ny, nz])
52
+ }
53
+ }
54
+ return edges
55
+ }
56
+
57
+ const cosThreshold = (angleDeg) => Math.cos((angleDeg * Math.PI) / 180)
58
+
59
+ /**
60
+ * Sharp + boundary feature edges, view independent. Returns flat segments
61
+ * [x1,y1,z1, x2,y2,z2].
62
+ */
63
+ function featureEdges(positions, indices, angleDeg = 25) {
64
+ return featureEdgesFrom(edgeTable(positions, indices), angleDeg)
65
+ }
66
+
67
+ function featureEdgesFrom(table, angleDeg = 25) {
68
+ const cosMin = cosThreshold(angleDeg)
69
+ const segments = []
70
+ for (const entry of table.values()) {
71
+ if (entry.normals.length === 1) {
72
+ // Boundary edge (open mesh) — always a candidate.
73
+ } else if (entry.normals.length === 2) {
74
+ const [n1, n2] = entry.normals
75
+ const dot = n1[0] * n2[0] + n1[1] * n2[1] + n1[2] * n2[2]
76
+ if (dot > cosMin) continue // smooth — not a feature edge
77
+ } else {
78
+ continue // non-manifold star: skip
79
+ }
80
+ segments.push(entry.x1, entry.y1, entry.z1, entry.x2, entry.y2, entry.z2)
81
+ }
82
+ return segments
83
+ }
84
+
85
+ /** Silhouette edges for one view: adjacent faces straddle the view plane. */
86
+ function silhouetteEdges(positions, indices, dir) {
87
+ return silhouetteEdgesFrom(edgeTable(positions, indices), dir)
88
+ }
89
+
90
+ function silhouetteEdgesFrom(table, dir) {
91
+ const segments = []
92
+ for (const entry of table.values()) {
93
+ if (entry.normals.length !== 2) continue // boundary handled by the sharp pass
94
+ const [n1, n2] = entry.normals
95
+ const d1 = n1[0] * dir[0] + n1[1] * dir[1] + n1[2] * dir[2]
96
+ const d2 = n2[0] * dir[0] + n2[1] * dir[1] + n2[2] * dir[2]
97
+ if (d1 * d2 < 0) segments.push(entry.x1, entry.y1, entry.z1, entry.x2, entry.y2, entry.z2)
98
+ }
99
+ return segments
100
+ }
101
+
102
+ /** Occlusion tester over the projected triangle soup (uniform grid). */
103
+ function createOccluderGrid(px, py, depth, indices, cellCount) {
104
+ const triCount = indices.length / 3
105
+ let minX = Infinity, minY = Infinity, maxX = -Infinity, maxY = -Infinity
106
+ for (let i = 0; i < px.length; i++) {
107
+ if (px[i] < minX) minX = px[i]
108
+ if (px[i] > maxX) maxX = px[i]
109
+ if (py[i] < minY) minY = py[i]
110
+ if (py[i] > maxY) maxY = py[i]
111
+ }
112
+ const extent = Math.max(maxX - minX, maxY - minY, 1e-9)
113
+ const cell = extent / cellCount
114
+ const grid = new Map()
115
+ const gridKey = (gx, gy) => gx * 4096 + gy
116
+ for (let t = 0; t < triCount; t++) {
117
+ const a = indices[t * 3] * 1, b = indices[t * 3 + 1], c = indices[t * 3 + 2]
118
+ const x0 = px[a], y0 = py[a], x1 = px[b], y1 = py[b], x2 = px[c], y2 = py[c]
119
+ const loX = Math.floor((Math.min(x0, x1, x2) - minX) / cell)
120
+ const hiX = Math.floor((Math.max(x0, x1, x2) - minX) / cell)
121
+ const loY = Math.floor((Math.min(y0, y1, y2) - minY) / cell)
122
+ const hiY = Math.floor((Math.max(y0, y1, y2) - minY) / cell)
123
+ for (let gy = loY; gy <= hiY; gy++) {
124
+ for (let gx = loX; gx <= hiX; gx++) {
125
+ const key = gridKey(gx, gy)
126
+ const bucket = grid.get(key)
127
+ if (bucket === undefined) grid.set(key, [t])
128
+ else bucket.push(t)
129
+ }
130
+ }
131
+ }
132
+ const EPS = extent * 1e-9
133
+ // Strict-interior margin: coverage only counts when the sample is a real
134
+ // distance inside the projected triangle. Orthographic projections make
135
+ // back faces share their boundary with the silhouette outline exactly —
136
+ // boundary-touching coverage must not hide the outline itself.
137
+ const MARGIN = 5e-3
138
+ /**
139
+ * Is (x, y) at depth `d` hidden? A triangle covers it when the projected
140
+ * point lies strictly inside and the interpolated depth is nearer by more
141
+ * than `bias` (bias ≈ half the tessellation chord sag to keep edges lying
142
+ * ON their own faces visible).
143
+ */
144
+ return (x, y, d, bias) => {
145
+ const gx = Math.floor((x - minX) / cell)
146
+ const gy = Math.floor((y - minY) / cell)
147
+ const bucket = grid.get(gridKey(gx, gy))
148
+ if (bucket === undefined) return false
149
+ for (const t of bucket) {
150
+ const a = indices[t * 3], b = indices[t * 3 + 1], c = indices[t * 3 + 2]
151
+ const x0 = px[a], y0 = py[a], x1 = px[b], y1 = py[b], x2 = px[c], y2 = py[c]
152
+ const area = (x1 - x0) * (y2 - y0) - (x2 - x0) * (y1 - y0)
153
+ if (Math.abs(area) < EPS) continue
154
+ const w0 = ((x1 - x) * (y2 - y) - (x2 - x) * (y1 - y)) / area
155
+ const w1 = ((x2 - x) * (y0 - y) - (x0 - x) * (y2 - y)) / area
156
+ const w2 = 1 - w0 - w1
157
+ if (w0 < MARGIN || w1 < MARGIN || w2 < MARGIN) continue
158
+ const frontDepth = w0 * depth[a] + w1 * depth[b] + w2 * depth[c]
159
+ if (frontDepth > d + bias) return true
160
+ }
161
+ return false
162
+ }
163
+ }
164
+
165
+ /** Ramer–Douglas–Peucker on a flat [x,y,…] polyline. */
166
+ function simplify(points, epsilon) {
167
+ const n = points.length / 2
168
+ if (n < 3) return points
169
+ const keep = new Uint8Array(n)
170
+ keep[0] = 1
171
+ keep[n - 1] = 1
172
+ const stack = [[0, n - 1]]
173
+ while (stack.length > 0) {
174
+ const [first, last] = stack.pop()
175
+ const x1 = points[first * 2], y1 = points[first * 2 + 1]
176
+ const x2 = points[last * 2], y2 = points[last * 2 + 1]
177
+ const dx = x2 - x1, dy = y2 - y1
178
+ const len = Math.hypot(dx, dy)
179
+ let maxDist = 0
180
+ let index = -1
181
+ for (let i = first + 1; i < last; i++) {
182
+ const px = points[i * 2], py = points[i * 2 + 1]
183
+ const dist = len === 0 ? Math.hypot(px - x1, py - y1) : Math.abs(dy * px - dx * py + x2 * y1 - y2 * x1) / len
184
+ if (dist > maxDist) { maxDist = dist; index = i }
185
+ }
186
+ if (maxDist > epsilon && index > 0) {
187
+ keep[index] = 1
188
+ stack.push([first, index], [index, last])
189
+ }
190
+ }
191
+ const out = []
192
+ for (let i = 0; i < n; i++) {
193
+ if (keep[i]) { out.push(points[i * 2], points[i * 2 + 1]) }
194
+ }
195
+ return out
196
+ }
197
+
198
+ /** Chain collinear-adjacent segments ([x1,y1,x2,y2] tuples) into polylines. */
199
+ function chainSegments(segments) {
200
+ const key = (x, y) => `${Math.round(x * 1000)}|${Math.round(y * 1000)}`
201
+ const map = new Map()
202
+ segments.forEach((seg, s) => {
203
+ const [x1, y1, x2, y2] = seg
204
+ for (const [k, end] of [[key(x1, y1), 0], [key(x2, y2), 1]]) {
205
+ let list = map.get(k)
206
+ if (list === undefined) { list = []; map.set(k, list) }
207
+ list.push({ s, end })
208
+ }
209
+ })
210
+ const used = new Uint8Array(segments.length)
211
+ const chains = []
212
+ for (let s = 0; s < segments.length; s++) {
213
+ if (used[s]) continue
214
+ used[s] = 1
215
+ const chain = [segments[s][0], segments[s][1], segments[s][2], segments[s][3]]
216
+ // Extend forward from the tail, then backward from the head.
217
+ for (const direction of [1, -1]) {
218
+ for (;;) {
219
+ const tailX = direction === 1 ? chain[chain.length - 2] : chain[0]
220
+ const tailY = direction === 1 ? chain[chain.length - 1] : chain[1]
221
+ const candidates = map.get(key(tailX, tailY)) ?? []
222
+ let found = null
223
+ for (const candidate of candidates) {
224
+ if (used[candidate.s]) continue
225
+ found = candidate
226
+ break
227
+ }
228
+ if (found === null) break
229
+ used[found.s] = 1
230
+ const [ex1, ey1, ex2, ey2] = segments[found.s]
231
+ // Append the far end oriented away from the join point.
232
+ const joinIsStart = key(ex1, ey1) === key(tailX, tailY)
233
+ const px = joinIsStart ? ex2 : ex1
234
+ const py = joinIsStart ? ey2 : ey1
235
+ if (direction === 1) chain.push(px, py)
236
+ else chain.unshift(px, py)
237
+ }
238
+ }
239
+ chains.push(chain)
240
+ }
241
+ return chains
242
+ }
243
+
244
+ /**
245
+ * Project one tessellated body into drawing views.
246
+ * views: [{ name, dir:[x,y,z] toward observer, xDir:[x,y,z] screen-right }].
247
+ * Returns { views: [{ name, visible: number[][] (flat polylines), hidden: number[][] }] }.
248
+ */
249
+ function projectViews({ positions, indices }, views, options = {}) {
250
+ const sharpAngle = options.sharpAngle ?? 25
251
+ const table = edgeTable(positions, indices)
252
+ const sharp = featureEdgesFrom(table, sharpAngle)
253
+ const triCount = indices.length / 3
254
+ const modelScale = (() => {
255
+ let min = Infinity, max = -Infinity
256
+ for (let i = 0; i < positions.length; i++) {
257
+ if (positions[i] < min) min = positions[i]
258
+ if (positions[i] > max) max = positions[i]
259
+ }
260
+ return Math.max(max - min, 1e-9)
261
+ })()
262
+ const bias = modelScale * (options.biasRatio ?? 0.002)
263
+ const simplifyEps = modelScale * (options.simplifyRatio ?? 0.0012)
264
+
265
+ return {
266
+ views: views.map((view) => {
267
+ const w = normalize(view.dir)
268
+ let u = normalize(view.xDir)
269
+ if (Math.abs(dot(u, w)) > 0.999) u = [1, 0, 0] // degenerate xDir fallback
270
+ const v = normalize(cross(w, u))
271
+ const count = positions.length / 3
272
+ const px = new Float64Array(count)
273
+ const py = new Float64Array(count)
274
+ const depth = new Float64Array(count)
275
+ for (let i = 0; i < count; i++) {
276
+ const x = positions[i * 3], y = positions[i * 3 + 1], z = positions[i * 3 + 2]
277
+ px[i] = x * u[0] + y * u[1] + z * u[2]
278
+ py[i] = x * v[0] + y * v[1] + z * v[2]
279
+ depth[i] = x * w[0] + y * w[1] + z * w[2]
280
+ }
281
+ const occluded = createOccluderGrid(px, py, depth, indices, 48)
282
+ const candidates = [...sharp, ...silhouetteEdgesFrom(table, w)]
283
+ const visibleSegments = []
284
+ const hiddenSegments = []
285
+ for (let s = 0; s < candidates.length / 6; s++) {
286
+ const x1 = candidates[s * 6], y1 = candidates[s * 6 + 1], z1 = candidates[s * 6 + 2]
287
+ const x2 = candidates[s * 6 + 3], y2 = candidates[s * 6 + 4], z2 = candidates[s * 6 + 5]
288
+ const p1x = x1 * u[0] + y1 * u[1] + z1 * u[2]
289
+ const p1y = x1 * v[0] + y1 * v[1] + z1 * v[2]
290
+ const p2x = x2 * u[0] + y2 * u[1] + z2 * u[2]
291
+ const p2y = x2 * v[0] + y2 * v[1] + z2 * v[2]
292
+ if (Math.abs(p1x - p2x) < 1e-9 && Math.abs(p1y - p2y) < 1e-9) continue
293
+ const d1 = x1 * w[0] + y1 * w[1] + z1 * w[2]
294
+ const d2 = x2 * w[0] + y2 * w[1] + z2 * w[2]
295
+ // Three interior samples, majority vote.
296
+ let hits = 0
297
+ for (const t of [0.25, 0.5, 0.75]) {
298
+ const sx = p1x + (p2x - p1x) * t
299
+ const sy = p1y + (p2y - p1y) * t
300
+ const sd = d1 + (d2 - d1) * t
301
+ if (occluded(sx, sy, sd, bias)) hits++
302
+ }
303
+ const target = hits >= 2 ? hiddenSegments : visibleSegments
304
+ target.push([p1x, p1y, p2x, p2y])
305
+ }
306
+ const unwrap = (segments) =>
307
+ chainSegments(segments)
308
+ .map((chain) => simplify(chain, simplifyEps))
309
+ .filter((chain) => chain.length >= 4)
310
+ return { name: view.name, visible: unwrap(visibleSegments), hidden: unwrap(hiddenSegments) }
311
+ }),
312
+ }
313
+ }
314
+
315
+ function normalize(v) {
316
+ const len = Math.hypot(v[0], v[1], v[2]) || 1
317
+ return [v[0] / len, v[1] / len, v[2] / len]
318
+ }
319
+
320
+ function cross(a, b) {
321
+ return [a[1] * b[2] - a[2] * b[1], a[2] * b[0] - a[0] * b[2], a[0] * b[1] - a[1] * b[0]]
322
+ }
323
+
324
+ function dot(a, b) {
325
+ return a[0] * b[0] + a[1] * b[1] + a[2] * b[2]
326
+ }
327
+
328
+ module.exports = { featureEdges, silhouetteEdges, projectViews }