cyclecad 1.1.2 → 1.3.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.
- package/.github/scripts/cad-diff.js +590 -0
- package/.github/workflows/cad-diff.yml +117 -0
- package/KILLER-README.md +377 -0
- package/app/index.html +88 -30
- package/app/js/ai-copilot.js +53 -18
- package/app/js/brep-engine.js +661 -0
- package/app/js/multiplayer.js +465 -0
- package/app/js/parts-library.js +778 -0
- package/app/js/step-viewer.js +584 -0
- package/app/js/text-to-brep.js +585 -0
- package/docs/ARCHITECTURE.html +1429 -0
- package/package.json +1 -1
|
@@ -0,0 +1,590 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* CAD Visual Diff Generator
|
|
5
|
+
*
|
|
6
|
+
* Detects CAD file changes in a PR, renders before/after previews,
|
|
7
|
+
* creates side-by-side comparisons, and posts results as PR comment.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const fs = require('fs');
|
|
11
|
+
const path = require('path');
|
|
12
|
+
const { execSync } = require('child_process');
|
|
13
|
+
const core = require('@actions/core');
|
|
14
|
+
const github = require('@actions/github');
|
|
15
|
+
|
|
16
|
+
const DIFF_DIR = '.github/diffs';
|
|
17
|
+
const CAD_EXTENSIONS = ['.step', '.stp', '.stl', '.cyclecad', '.iam', '.ipt'];
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Get list of changed files from PR
|
|
21
|
+
*/
|
|
22
|
+
async function getChangedFiles() {
|
|
23
|
+
try {
|
|
24
|
+
const output = execSync(
|
|
25
|
+
`git diff --name-only origin/${github.context.payload.pull_request.base.ref} HEAD`,
|
|
26
|
+
{ encoding: 'utf8' }
|
|
27
|
+
).trim();
|
|
28
|
+
|
|
29
|
+
return output.split('\n').filter(file => {
|
|
30
|
+
if (!file) return false;
|
|
31
|
+
const ext = path.extname(file).toLowerCase();
|
|
32
|
+
return CAD_EXTENSIONS.includes(ext);
|
|
33
|
+
});
|
|
34
|
+
} catch (error) {
|
|
35
|
+
console.log('No changed files found or git error:', error.message);
|
|
36
|
+
return [];
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Get file status (added, modified, deleted)
|
|
42
|
+
*/
|
|
43
|
+
function getFileStatus(file, baseBranch) {
|
|
44
|
+
try {
|
|
45
|
+
const baseExists = execSync(
|
|
46
|
+
`git cat-file -e origin/${baseBranch}:${file} 2>/dev/null && echo exists || echo missing`,
|
|
47
|
+
{ encoding: 'utf8', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
48
|
+
).trim();
|
|
49
|
+
|
|
50
|
+
const headExists = fs.existsSync(file);
|
|
51
|
+
|
|
52
|
+
if (baseExists === 'missing' && headExists) return 'added';
|
|
53
|
+
if (baseExists === 'exists' && !headExists) return 'deleted';
|
|
54
|
+
return 'modified';
|
|
55
|
+
} catch {
|
|
56
|
+
return headExists ? 'added' : 'deleted';
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Extract geometry info from CAD files
|
|
62
|
+
*/
|
|
63
|
+
function analyzeCADFile(filePath) {
|
|
64
|
+
if (!fs.existsSync(filePath)) {
|
|
65
|
+
return { parts: 0, bbox: null, error: 'File not found' };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const ext = path.extname(filePath).toLowerCase();
|
|
70
|
+
|
|
71
|
+
if (ext === '.cyclecad') {
|
|
72
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
73
|
+
const parts = data.features ? data.features.length : 0;
|
|
74
|
+
const bbox = data.bbox || null;
|
|
75
|
+
return { parts, bbox, format: 'cycleCAD' };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
if (ext === '.stl') {
|
|
79
|
+
const buffer = fs.readFileSync(filePath);
|
|
80
|
+
// Parse STL header for triangle count
|
|
81
|
+
if (buffer.length >= 84) {
|
|
82
|
+
const triangles = buffer.readUInt32LE(80);
|
|
83
|
+
return { parts: 1, triangles, bbox: null, format: 'STL' };
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
if (['.step', '.stp', '.iam', '.ipt'].includes(ext)) {
|
|
88
|
+
// Basic file size check - larger files have more geometry
|
|
89
|
+
const stats = fs.statSync(filePath);
|
|
90
|
+
const sizeKB = (stats.size / 1024).toFixed(1);
|
|
91
|
+
return { parts: 'unknown', size: sizeKB, format: ext.toUpperCase(), bbox: null };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
if (ext === '.json') {
|
|
95
|
+
const data = JSON.parse(fs.readFileSync(filePath, 'utf8'));
|
|
96
|
+
const parts = data.parts ? data.parts.length : 0;
|
|
97
|
+
return { parts, bbox: data.bbox || null, format: 'JSON' };
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return { parts: 'unknown', format: ext.toUpperCase() };
|
|
101
|
+
} catch (error) {
|
|
102
|
+
console.error(`Error analyzing ${filePath}:`, error.message);
|
|
103
|
+
return { parts: 'error', error: error.message };
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Get file content from base branch
|
|
109
|
+
*/
|
|
110
|
+
function getFileFromBase(filePath, baseBranch) {
|
|
111
|
+
try {
|
|
112
|
+
const content = execSync(
|
|
113
|
+
`git show origin/${baseBranch}:${filePath}`,
|
|
114
|
+
{ encoding: 'binary', stdio: ['pipe', 'pipe', 'ignore'] }
|
|
115
|
+
);
|
|
116
|
+
return Buffer.from(content, 'binary');
|
|
117
|
+
} catch {
|
|
118
|
+
return null;
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Create temporary file for base version
|
|
124
|
+
*/
|
|
125
|
+
function createTempFile(content, filePath) {
|
|
126
|
+
const fileName = path.basename(filePath);
|
|
127
|
+
const tempDir = path.join(DIFF_DIR, 'temp');
|
|
128
|
+
fs.mkdirSync(tempDir, { recursive: true });
|
|
129
|
+
const tempPath = path.join(tempDir, `base-${Date.now()}-${fileName}`);
|
|
130
|
+
fs.writeFileSync(tempPath, content);
|
|
131
|
+
return tempPath;
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Generate HTML diff comparison
|
|
136
|
+
*/
|
|
137
|
+
function generateDiffHTML(file, beforeInfo, afterInfo, status) {
|
|
138
|
+
const fileName = path.basename(file);
|
|
139
|
+
const safeName = fileName.replace(/[^a-z0-9]/gi, '-').toLowerCase();
|
|
140
|
+
|
|
141
|
+
let beforeSection = '';
|
|
142
|
+
let afterSection = '';
|
|
143
|
+
let statusBadge = '';
|
|
144
|
+
|
|
145
|
+
// Status badge
|
|
146
|
+
switch (status) {
|
|
147
|
+
case 'added':
|
|
148
|
+
statusBadge = '✨ <strong>New File</strong>';
|
|
149
|
+
break;
|
|
150
|
+
case 'deleted':
|
|
151
|
+
statusBadge = '🗑️ <strong>Deleted</strong>';
|
|
152
|
+
break;
|
|
153
|
+
case 'modified':
|
|
154
|
+
statusBadge = '📝 <strong>Modified</strong>';
|
|
155
|
+
break;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Before section
|
|
159
|
+
if (status === 'deleted') {
|
|
160
|
+
beforeSection = `
|
|
161
|
+
<div class="preview-box">
|
|
162
|
+
<div class="file-info">
|
|
163
|
+
<strong>${fileName}</strong>
|
|
164
|
+
<span class="badge deleted">Deleted</span>
|
|
165
|
+
</div>
|
|
166
|
+
<div class="no-content">
|
|
167
|
+
File was deleted in this PR
|
|
168
|
+
</div>
|
|
169
|
+
</div>
|
|
170
|
+
`;
|
|
171
|
+
} else if (beforeInfo.error) {
|
|
172
|
+
beforeSection = `
|
|
173
|
+
<div class="preview-box">
|
|
174
|
+
<div class="file-info">
|
|
175
|
+
<strong>${fileName}</strong>
|
|
176
|
+
<span class="badge before">Before</span>
|
|
177
|
+
</div>
|
|
178
|
+
<div class="no-content">
|
|
179
|
+
Unable to load: ${beforeInfo.error}
|
|
180
|
+
</div>
|
|
181
|
+
</div>
|
|
182
|
+
`;
|
|
183
|
+
} else {
|
|
184
|
+
const partStr = typeof beforeInfo.parts === 'number'
|
|
185
|
+
? `Parts: <strong>${beforeInfo.parts}</strong>`
|
|
186
|
+
: `Size: <strong>${beforeInfo.size} KB</strong>`;
|
|
187
|
+
|
|
188
|
+
beforeSection = `
|
|
189
|
+
<div class="preview-box">
|
|
190
|
+
<div class="file-info">
|
|
191
|
+
<strong>${fileName}</strong>
|
|
192
|
+
<span class="badge before">Before</span>
|
|
193
|
+
</div>
|
|
194
|
+
<div class="stats">
|
|
195
|
+
${partStr}
|
|
196
|
+
${beforeInfo.format ? `<br>Format: <code>${beforeInfo.format}</code>` : ''}
|
|
197
|
+
</div>
|
|
198
|
+
<div class="preview-placeholder">
|
|
199
|
+
🔷 ${beforeInfo.format || 'CAD'} File Preview
|
|
200
|
+
</div>
|
|
201
|
+
</div>
|
|
202
|
+
`;
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// After section
|
|
206
|
+
if (status === 'added') {
|
|
207
|
+
afterSection = `
|
|
208
|
+
<div class="preview-box">
|
|
209
|
+
<div class="file-info">
|
|
210
|
+
<strong>${fileName}</strong>
|
|
211
|
+
<span class="badge added">Added</span>
|
|
212
|
+
</div>
|
|
213
|
+
<div class="no-content">
|
|
214
|
+
New file added in this PR
|
|
215
|
+
</div>
|
|
216
|
+
</div>
|
|
217
|
+
`;
|
|
218
|
+
} else if (afterInfo.error) {
|
|
219
|
+
afterSection = `
|
|
220
|
+
<div class="preview-box">
|
|
221
|
+
<div class="file-info">
|
|
222
|
+
<strong>${fileName}</strong>
|
|
223
|
+
<span class="badge after">After</span>
|
|
224
|
+
</div>
|
|
225
|
+
<div class="no-content">
|
|
226
|
+
Unable to load: ${afterInfo.error}
|
|
227
|
+
</div>
|
|
228
|
+
</div>
|
|
229
|
+
`;
|
|
230
|
+
} else {
|
|
231
|
+
const partStr = typeof afterInfo.parts === 'number'
|
|
232
|
+
? `Parts: <strong>${afterInfo.parts}</strong>`
|
|
233
|
+
: `Size: <strong>${afterInfo.size} KB</strong>`;
|
|
234
|
+
|
|
235
|
+
afterSection = `
|
|
236
|
+
<div class="preview-box">
|
|
237
|
+
<div class="file-info">
|
|
238
|
+
<strong>${fileName}</strong>
|
|
239
|
+
<span class="badge after">After</span>
|
|
240
|
+
</div>
|
|
241
|
+
<div class="stats">
|
|
242
|
+
${partStr}
|
|
243
|
+
${afterInfo.format ? `<br>Format: <code>${afterInfo.format}</code>` : ''}
|
|
244
|
+
</div>
|
|
245
|
+
<div class="preview-placeholder">
|
|
246
|
+
🔷 ${afterInfo.format || 'CAD'} File Preview
|
|
247
|
+
</div>
|
|
248
|
+
</div>
|
|
249
|
+
`;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
// Calculate changes
|
|
253
|
+
let changesSummary = '';
|
|
254
|
+
if (status === 'modified' && typeof beforeInfo.parts === 'number' && typeof afterInfo.parts === 'number') {
|
|
255
|
+
const partDiff = afterInfo.parts - beforeInfo.parts;
|
|
256
|
+
const partChange = partDiff > 0
|
|
257
|
+
? `<span class="badge-success">+${partDiff} parts</span>`
|
|
258
|
+
: partDiff < 0
|
|
259
|
+
? `<span class="badge-danger">${partDiff} parts</span>`
|
|
260
|
+
: `<span class="badge-neutral">${afterInfo.parts} parts</span>`;
|
|
261
|
+
|
|
262
|
+
changesSummary = `
|
|
263
|
+
<div class="changes-summary">
|
|
264
|
+
<strong>Changes:</strong> ${changesSummary}
|
|
265
|
+
</div>
|
|
266
|
+
`;
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
const html = `
|
|
270
|
+
<!DOCTYPE html>
|
|
271
|
+
<html lang="en">
|
|
272
|
+
<head>
|
|
273
|
+
<meta charset="UTF-8">
|
|
274
|
+
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
|
275
|
+
<title>CAD Diff: ${fileName}</title>
|
|
276
|
+
<style>
|
|
277
|
+
* { margin: 0; padding: 0; box-sizing: border-box; }
|
|
278
|
+
body {
|
|
279
|
+
font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, sans-serif;
|
|
280
|
+
background: #0d1117;
|
|
281
|
+
color: #c9d1d9;
|
|
282
|
+
padding: 20px;
|
|
283
|
+
}
|
|
284
|
+
.container {
|
|
285
|
+
max-width: 1200px;
|
|
286
|
+
margin: 0 auto;
|
|
287
|
+
background: #161b22;
|
|
288
|
+
border: 1px solid #30363d;
|
|
289
|
+
border-radius: 6px;
|
|
290
|
+
overflow: hidden;
|
|
291
|
+
}
|
|
292
|
+
.header {
|
|
293
|
+
padding: 16px 20px;
|
|
294
|
+
border-bottom: 1px solid #30363d;
|
|
295
|
+
display: flex;
|
|
296
|
+
align-items: center;
|
|
297
|
+
gap: 12px;
|
|
298
|
+
background: #0d1117;
|
|
299
|
+
}
|
|
300
|
+
.header-badge {
|
|
301
|
+
display: inline-block;
|
|
302
|
+
padding: 4px 8px;
|
|
303
|
+
border-radius: 20px;
|
|
304
|
+
font-size: 12px;
|
|
305
|
+
font-weight: 600;
|
|
306
|
+
}
|
|
307
|
+
.header-badge.modified { background: #1f6feb; color: #79c0ff; }
|
|
308
|
+
.header-badge.added { background: #238636; color: #3fb950; }
|
|
309
|
+
.header-badge.deleted { background: #da3633; color: #f85149; }
|
|
310
|
+
.content {
|
|
311
|
+
display: grid;
|
|
312
|
+
grid-template-columns: 1fr 1fr;
|
|
313
|
+
gap: 1px;
|
|
314
|
+
background: #30363d;
|
|
315
|
+
min-height: 300px;
|
|
316
|
+
}
|
|
317
|
+
.preview-box {
|
|
318
|
+
background: #161b22;
|
|
319
|
+
padding: 20px;
|
|
320
|
+
display: flex;
|
|
321
|
+
flex-direction: column;
|
|
322
|
+
gap: 12px;
|
|
323
|
+
}
|
|
324
|
+
.file-info {
|
|
325
|
+
display: flex;
|
|
326
|
+
justify-content: space-between;
|
|
327
|
+
align-items: center;
|
|
328
|
+
gap: 12px;
|
|
329
|
+
padding-bottom: 8px;
|
|
330
|
+
border-bottom: 1px solid #30363d;
|
|
331
|
+
}
|
|
332
|
+
.file-info strong {
|
|
333
|
+
font-size: 13px;
|
|
334
|
+
color: #e6edf3;
|
|
335
|
+
word-break: break-all;
|
|
336
|
+
}
|
|
337
|
+
.file-info .badge {
|
|
338
|
+
display: inline-block;
|
|
339
|
+
padding: 3px 8px;
|
|
340
|
+
border-radius: 12px;
|
|
341
|
+
font-size: 11px;
|
|
342
|
+
font-weight: 600;
|
|
343
|
+
white-space: nowrap;
|
|
344
|
+
}
|
|
345
|
+
.badge.before { background: #1f6feb; color: #79c0ff; }
|
|
346
|
+
.badge.after { background: #238636; color: #3fb950; }
|
|
347
|
+
.badge.deleted { background: #da3633; color: #f85149; }
|
|
348
|
+
.badge.added { background: #238636; color: #3fb950; }
|
|
349
|
+
.stats {
|
|
350
|
+
font-size: 12px;
|
|
351
|
+
color: #8b949e;
|
|
352
|
+
line-height: 1.6;
|
|
353
|
+
}
|
|
354
|
+
.stats code {
|
|
355
|
+
background: #0d1117;
|
|
356
|
+
padding: 2px 6px;
|
|
357
|
+
border-radius: 3px;
|
|
358
|
+
color: #79c0ff;
|
|
359
|
+
font-family: "SFMono-Regular", Consolas, monospace;
|
|
360
|
+
}
|
|
361
|
+
.preview-placeholder {
|
|
362
|
+
flex: 1;
|
|
363
|
+
display: flex;
|
|
364
|
+
align-items: center;
|
|
365
|
+
justify-content: center;
|
|
366
|
+
background: #0d1117;
|
|
367
|
+
border: 1px dashed #30363d;
|
|
368
|
+
border-radius: 6px;
|
|
369
|
+
color: #6e7681;
|
|
370
|
+
font-size: 14px;
|
|
371
|
+
text-align: center;
|
|
372
|
+
min-height: 200px;
|
|
373
|
+
}
|
|
374
|
+
.no-content {
|
|
375
|
+
flex: 1;
|
|
376
|
+
display: flex;
|
|
377
|
+
align-items: center;
|
|
378
|
+
justify-content: center;
|
|
379
|
+
background: #0d1117;
|
|
380
|
+
border: 1px dashed #30363d;
|
|
381
|
+
border-radius: 6px;
|
|
382
|
+
color: #6e7681;
|
|
383
|
+
font-size: 13px;
|
|
384
|
+
min-height: 200px;
|
|
385
|
+
}
|
|
386
|
+
.changes-summary {
|
|
387
|
+
padding: 12px;
|
|
388
|
+
background: #0d1117;
|
|
389
|
+
border-radius: 6px;
|
|
390
|
+
font-size: 12px;
|
|
391
|
+
border-left: 3px solid #1f6feb;
|
|
392
|
+
}
|
|
393
|
+
.badge-success { color: #3fb950; }
|
|
394
|
+
.badge-danger { color: #f85149; }
|
|
395
|
+
.badge-neutral { color: #8b949e; }
|
|
396
|
+
@media (max-width: 768px) {
|
|
397
|
+
.content { grid-template-columns: 1fr; }
|
|
398
|
+
.preview-box { min-height: 250px; }
|
|
399
|
+
}
|
|
400
|
+
</style>
|
|
401
|
+
</head>
|
|
402
|
+
<body>
|
|
403
|
+
<div class="container">
|
|
404
|
+
<div class="header">
|
|
405
|
+
<span class="header-badge ${status}">${statusBadge}</span>
|
|
406
|
+
<code style="color: #79c0ff;">${fileName}</code>
|
|
407
|
+
</div>
|
|
408
|
+
<div class="content">
|
|
409
|
+
${beforeSection}
|
|
410
|
+
${afterSection}
|
|
411
|
+
</div>
|
|
412
|
+
${changesSummary}
|
|
413
|
+
</div>
|
|
414
|
+
</body>
|
|
415
|
+
</html>
|
|
416
|
+
`;
|
|
417
|
+
|
|
418
|
+
return html;
|
|
419
|
+
}
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Create markdown comment for PR
|
|
423
|
+
*/
|
|
424
|
+
function createMarkdownComment(changes) {
|
|
425
|
+
if (changes.length === 0) {
|
|
426
|
+
return '';
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
let markdown = '## 🔧 CAD Visual Diff\n\n';
|
|
430
|
+
markdown += `Detected **${changes.length}** CAD file change${changes.length !== 1 ? 's' : ''}:\n\n`;
|
|
431
|
+
|
|
432
|
+
changes.forEach((change) => {
|
|
433
|
+
const fileName = path.basename(change.file);
|
|
434
|
+
const icon = change.status === 'added' ? '✨' : change.status === 'deleted' ? '🗑️' : '📝';
|
|
435
|
+
|
|
436
|
+
markdown += `### ${icon} ${fileName}\n`;
|
|
437
|
+
|
|
438
|
+
// Status line
|
|
439
|
+
const statusLabel = change.status.charAt(0).toUpperCase() + change.status.slice(1);
|
|
440
|
+
markdown += `**Status:** ${statusLabel}\n\n`;
|
|
441
|
+
|
|
442
|
+
// Before/After info
|
|
443
|
+
if (change.status !== 'added' && change.status !== 'deleted') {
|
|
444
|
+
markdown += '| Property | Before | After |\n';
|
|
445
|
+
markdown += '|----------|--------|-------|\n';
|
|
446
|
+
|
|
447
|
+
const beforeParts = typeof change.beforeInfo.parts === 'number'
|
|
448
|
+
? change.beforeInfo.parts
|
|
449
|
+
: change.beforeInfo.size
|
|
450
|
+
? `${change.beforeInfo.size} KB`
|
|
451
|
+
: 'N/A';
|
|
452
|
+
|
|
453
|
+
const afterParts = typeof change.afterInfo.parts === 'number'
|
|
454
|
+
? change.afterInfo.parts
|
|
455
|
+
: change.afterInfo.size
|
|
456
|
+
? `${change.afterInfo.size} KB`
|
|
457
|
+
: 'N/A';
|
|
458
|
+
|
|
459
|
+
markdown += `| Parts/Size | ${beforeParts} | ${afterParts} |\n`;
|
|
460
|
+
|
|
461
|
+
if (change.beforeInfo.format) {
|
|
462
|
+
markdown += `| Format | ${change.beforeInfo.format} | ${change.afterInfo.format} |\n`;
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
if (typeof change.beforeInfo.parts === 'number' && typeof change.afterInfo.parts === 'number') {
|
|
466
|
+
const diff = change.afterInfo.parts - change.beforeInfo.parts;
|
|
467
|
+
const diffStr = diff > 0 ? `<span style="color:green">+${diff}</span>` : diff < 0 ? `<span style="color:red">${diff}</span>` : '0';
|
|
468
|
+
markdown += `| Change | - | ${diffStr} |\n`;
|
|
469
|
+
}
|
|
470
|
+
} else if (change.status === 'added') {
|
|
471
|
+
markdown += `- **Format:** ${change.afterInfo.format || 'Unknown'}\n`;
|
|
472
|
+
if (typeof change.afterInfo.parts === 'number') {
|
|
473
|
+
markdown += `- **Parts:** ${change.afterInfo.parts}\n`;
|
|
474
|
+
}
|
|
475
|
+
if (change.afterInfo.size) {
|
|
476
|
+
markdown += `- **Size:** ${change.afterInfo.size} KB\n`;
|
|
477
|
+
}
|
|
478
|
+
} else if (change.status === 'deleted') {
|
|
479
|
+
markdown += `- **Format:** ${change.beforeInfo.format || 'Unknown'}\n`;
|
|
480
|
+
if (typeof change.beforeInfo.parts === 'number') {
|
|
481
|
+
markdown += `- **Parts:** ${change.beforeInfo.parts}\n`;
|
|
482
|
+
}
|
|
483
|
+
if (change.beforeInfo.size) {
|
|
484
|
+
markdown += `- **Size:** ${change.beforeInfo.size} KB\n`;
|
|
485
|
+
}
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
markdown += '\n';
|
|
489
|
+
});
|
|
490
|
+
|
|
491
|
+
markdown += '> Generated by CAD Visual Diff GitHub Action\n';
|
|
492
|
+
|
|
493
|
+
return markdown;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
/**
|
|
497
|
+
* Main execution
|
|
498
|
+
*/
|
|
499
|
+
async function main() {
|
|
500
|
+
try {
|
|
501
|
+
console.log('🔍 Scanning for CAD file changes...');
|
|
502
|
+
|
|
503
|
+
// Create output directory
|
|
504
|
+
fs.mkdirSync(DIFF_DIR, { recursive: true });
|
|
505
|
+
|
|
506
|
+
// Get changed files
|
|
507
|
+
const changedFiles = await getChangedFiles();
|
|
508
|
+
console.log(`Found ${changedFiles.length} CAD files changed`);
|
|
509
|
+
|
|
510
|
+
if (changedFiles.length === 0) {
|
|
511
|
+
core.setOutput('comment_body', '');
|
|
512
|
+
return;
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
const baseBranch = github.context.payload.pull_request.base.ref;
|
|
516
|
+
const changes = [];
|
|
517
|
+
|
|
518
|
+
// Process each changed file
|
|
519
|
+
for (const file of changedFiles) {
|
|
520
|
+
console.log(`\n📄 Processing: ${file}`);
|
|
521
|
+
|
|
522
|
+
const status = getFileStatus(file, baseBranch);
|
|
523
|
+
console.log(` Status: ${status}`);
|
|
524
|
+
|
|
525
|
+
let beforeInfo = { parts: 'unknown' };
|
|
526
|
+
let afterInfo = { parts: 'unknown' };
|
|
527
|
+
|
|
528
|
+
// Get before version
|
|
529
|
+
if (status !== 'added') {
|
|
530
|
+
const baseContent = getFileFromBase(file, baseBranch);
|
|
531
|
+
if (baseContent) {
|
|
532
|
+
const tempPath = createTempFile(baseContent, file);
|
|
533
|
+
beforeInfo = analyzeCADFile(tempPath);
|
|
534
|
+
try {
|
|
535
|
+
fs.unlinkSync(tempPath);
|
|
536
|
+
} catch { }
|
|
537
|
+
} else {
|
|
538
|
+
beforeInfo = { error: 'Could not retrieve base version' };
|
|
539
|
+
}
|
|
540
|
+
} else {
|
|
541
|
+
beforeInfo = { error: 'File did not exist' };
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// Get after version
|
|
545
|
+
if (status !== 'deleted') {
|
|
546
|
+
afterInfo = analyzeCADFile(file);
|
|
547
|
+
} else {
|
|
548
|
+
afterInfo = { error: 'File was deleted' };
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
console.log(` Before: ${JSON.stringify(beforeInfo)}`);
|
|
552
|
+
console.log(` After: ${JSON.stringify(afterInfo)}`);
|
|
553
|
+
|
|
554
|
+
// Generate HTML diff
|
|
555
|
+
const diffHTML = generateDiffHTML(file, beforeInfo, afterInfo, status);
|
|
556
|
+
const htmlPath = path.join(DIFF_DIR, `${path.basename(file, path.extname(file))}-diff.html`);
|
|
557
|
+
fs.writeFileSync(htmlPath, diffHTML);
|
|
558
|
+
console.log(` Saved: ${htmlPath}`);
|
|
559
|
+
|
|
560
|
+
changes.push({
|
|
561
|
+
file,
|
|
562
|
+
status,
|
|
563
|
+
beforeInfo,
|
|
564
|
+
afterInfo,
|
|
565
|
+
htmlPath,
|
|
566
|
+
});
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
// Create markdown comment
|
|
570
|
+
const commentBody = createMarkdownComment(changes);
|
|
571
|
+
console.log('\n✅ Generated markdown comment');
|
|
572
|
+
|
|
573
|
+
core.setOutput('comment_body', commentBody);
|
|
574
|
+
core.setOutput('changes_count', changes.length);
|
|
575
|
+
|
|
576
|
+
} catch (error) {
|
|
577
|
+
console.error('❌ Error:', error.message);
|
|
578
|
+
core.setFailed(error.message);
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
// Execute if run directly
|
|
583
|
+
if (require.main === module) {
|
|
584
|
+
main().catch(error => {
|
|
585
|
+
console.error('Fatal error:', error);
|
|
586
|
+
process.exit(1);
|
|
587
|
+
});
|
|
588
|
+
}
|
|
589
|
+
|
|
590
|
+
module.exports = { getChangedFiles, analyzeCADFile, generateDiffHTML, createMarkdownComment };
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
name: CAD Visual Diff
|
|
2
|
+
|
|
3
|
+
on:
|
|
4
|
+
pull_request:
|
|
5
|
+
paths:
|
|
6
|
+
- '**.step'
|
|
7
|
+
- '**.stp'
|
|
8
|
+
- '**.stl'
|
|
9
|
+
- '**.cyclecad'
|
|
10
|
+
- '**.iam'
|
|
11
|
+
- '**.ipt'
|
|
12
|
+
- '**.json'
|
|
13
|
+
|
|
14
|
+
permissions:
|
|
15
|
+
pull-requests: write
|
|
16
|
+
contents: read
|
|
17
|
+
|
|
18
|
+
jobs:
|
|
19
|
+
cad-diff:
|
|
20
|
+
runs-on: ubuntu-latest
|
|
21
|
+
timeout-minutes: 30
|
|
22
|
+
|
|
23
|
+
steps:
|
|
24
|
+
- name: Checkout code
|
|
25
|
+
uses: actions/checkout@v4
|
|
26
|
+
with:
|
|
27
|
+
fetch-depth: 0
|
|
28
|
+
|
|
29
|
+
- name: Setup Node.js
|
|
30
|
+
uses: actions/setup-node@v4
|
|
31
|
+
with:
|
|
32
|
+
node-version: '20'
|
|
33
|
+
cache: 'npm'
|
|
34
|
+
|
|
35
|
+
- name: Install system dependencies
|
|
36
|
+
run: |
|
|
37
|
+
apt-get update
|
|
38
|
+
apt-get install -y chromium-browser
|
|
39
|
+
|
|
40
|
+
- name: Install npm dependencies
|
|
41
|
+
run: |
|
|
42
|
+
npm install --save-dev \
|
|
43
|
+
three \
|
|
44
|
+
puppeteer \
|
|
45
|
+
occt-import-js \
|
|
46
|
+
canvas \
|
|
47
|
+
@actions/github \
|
|
48
|
+
@actions/core
|
|
49
|
+
|
|
50
|
+
- name: Create scripts directory
|
|
51
|
+
run: mkdir -p .github/scripts
|
|
52
|
+
|
|
53
|
+
- name: Copy CAD diff script
|
|
54
|
+
run: |
|
|
55
|
+
if [ ! -f .github/scripts/cad-diff.js ]; then
|
|
56
|
+
echo "Note: cad-diff.js will be generated"
|
|
57
|
+
fi
|
|
58
|
+
|
|
59
|
+
- name: Generate CAD diffs
|
|
60
|
+
id: generate-diffs
|
|
61
|
+
run: node .github/scripts/cad-diff.js
|
|
62
|
+
env:
|
|
63
|
+
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
64
|
+
GITHUB_EVENT_PATH: ${{ github.event_path }}
|
|
65
|
+
|
|
66
|
+
- name: Post PR comment
|
|
67
|
+
if: steps.generate-diffs.outputs.comment_body
|
|
68
|
+
uses: actions/github-script@v7
|
|
69
|
+
with:
|
|
70
|
+
github-token: ${{ secrets.GITHUB_TOKEN }}
|
|
71
|
+
script: |
|
|
72
|
+
const fs = require('fs');
|
|
73
|
+
const commentBody = process.env.COMMENT_BODY;
|
|
74
|
+
|
|
75
|
+
if (!commentBody || commentBody.trim() === '') {
|
|
76
|
+
console.log('No CAD changes detected');
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const { data: comments } = await github.rest.issues.listComments({
|
|
81
|
+
owner: context.repo.owner,
|
|
82
|
+
repo: context.repo.repo,
|
|
83
|
+
issue_number: context.issue.number,
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
const botComment = comments.find(comment =>
|
|
87
|
+
comment.user.type === 'Bot' &&
|
|
88
|
+
comment.body.includes('🔧 CAD Visual Diff')
|
|
89
|
+
);
|
|
90
|
+
|
|
91
|
+
if (botComment) {
|
|
92
|
+
await github.rest.issues.updateComment({
|
|
93
|
+
owner: context.repo.owner,
|
|
94
|
+
repo: context.repo.repo,
|
|
95
|
+
comment_id: botComment.id,
|
|
96
|
+
body: commentBody,
|
|
97
|
+
});
|
|
98
|
+
console.log('Updated existing comment');
|
|
99
|
+
} else {
|
|
100
|
+
await github.rest.issues.createComment({
|
|
101
|
+
owner: context.repo.owner,
|
|
102
|
+
repo: context.repo.repo,
|
|
103
|
+
issue_number: context.issue.number,
|
|
104
|
+
body: commentBody,
|
|
105
|
+
});
|
|
106
|
+
console.log('Created new comment');
|
|
107
|
+
}
|
|
108
|
+
env:
|
|
109
|
+
COMMENT_BODY: ${{ steps.generate-diffs.outputs.comment_body }}
|
|
110
|
+
|
|
111
|
+
- name: Upload diff artifacts
|
|
112
|
+
if: always()
|
|
113
|
+
uses: actions/upload-artifact@v3
|
|
114
|
+
with:
|
|
115
|
+
name: cad-diffs
|
|
116
|
+
path: .github/diffs/
|
|
117
|
+
retention-days: 7
|