ARTICLE · 1158685
实战|用 AI 写 CAD 插件:CAD 批量打印(附源码 + 提示词)

痛点:出图的折磨────────────────────────────────────────────────────────────────────────────────────────────────────
今天给大家写一个批量打印的插件,我们做设计的,也好绘图的也好,难免遇到几十甚至上百张图的场景,打开图,输入打印命令,选打印机,选纸张,选打印样式,选打印范围,框选图框,预览一下,确认没问题,点确定,等它转圈,生成PDF,关闭打印对话框。然后打开第二张图,重复上面所有步骤。第三张,第四张,第五张……一百张图,很浪费时间,
如果你用的是布局空间,稍微好一点,可以批量发布。但大部分国内工程图纸都在模型空间,每张图一个图框,图纸和图纸之间没有关联,只能一张一张来。
插件效果:一键框选,直接打印────────────────────────────────────────────────────────────────────────────────────────────────────
先看效果。

以前你打开一张图、打印一张图、关闭一张图,循环一百次,现在你只需要做三步。第一步,打开批量打印工具。第二步,框选所有图框。第三步,点开始打印。
程序会自动识别你框选的每一个图框,不管是块、矩形还是四条直线拼的,都会自动提取出来。然后按你选的排序方式重新编号,在图纸上画上淡黄色的预览框和序号,让你一眼就能看出打印顺序对不对。确认没问题,点开始打印,程序就一张一张自动输出PDF,进度实时显示在界面上,打印完自动清理预览。
以前一下午的活儿,现在几分钟搞定。
打印方向可以选自动、横向、竖向,自动模式会按图框本身的宽高比智能判断,横的图框打横的,竖的图框打竖的。打印样式默认用 acad.ctb,你改过一次之后就记住你的选择了。文件名前缀默认用当前图纸的名字,比如你打开的是"办公楼一层平面图.dwg",输出的PDF就叫"办公楼一层平面图_001.pdf"、"办公楼一层平面图_002.pdf",一看就知道是哪个项目的。
这个插件到底解决了什么问题?────────────────────────────────────────────────────────────────────────────────────────────────────
从实际使用角度看,它解决了四个层次的问题。
1最表面的问题:重复劳动。
一百张图,一百次打印设置,一百次重复操作。这是纯粹的体力活儿,程序最适合干这种活。
2识别问题:图框是各式各样的。
有的人图框是块,有的人图框是矩形,有的人图框是四条直线拼的。一个通用的批量打印工具,必须能识别所有这些情况。我们的方案是框选一批实体,然后从里面自动提取"图框单元"。块参照算一个,闭合矩形算一个,端点相连的直线组算一个。这样不管你图框怎么画的,框选一下都能识别出来。
3排序问题:打印顺序得对。
工程图纸的打印顺序是有讲究的。一般要求从左到右、从上到下。但如果你框选的时候是从右往左框的,程序按框选顺序编号,那就反了。所以插件提供了八种排序方式:按选择顺序、从左到右、从右到左、从上到下、从下到上、网格逐行从左、网格逐行从右、蛇形左右交替。切换排序方式的时候,CAD里画的预览序号会立即跟着变,一眼就能看出来对不对。
4方向问题:横图竖图得分开处理。
同一个DWG里可能既有横向的A0图框,也有竖向的A0图框。如果统一按一个方向打印,要么横向的图被打成竖的(内容缩得很小),要么竖向的图被打成横的(内容被切掉)。我们的自动模式会逐张判断每个图框的宽高比,横的打横的,竖的打竖的,完全不用你操心。
交互方式:一个命令搞定────────────────────────────────────────────────────────────────────────────────────────────────────
加载好插件之后,只需要记住一个命令:ABPT。
在CAD命令行输入这个命令,回车,弹出工具窗口。窗口分四个区块。
1图框识别区:
可以选择按图层识别、按块名识别、自动识别,或者直接用框选识别。框选识别是推荐方式,点一下按钮,工具窗口自动隐藏,CAD获得焦点,你框选一批实体,回车确认,工具窗口自动回到最前面。
2识别结果区:
显示所有识别到的图框,每行一个,包含序号、来源类型、尺寸、左下角坐标、右上角坐标。左侧有复选框,可以单独取消某几张不打印。上方有排序下拉框,切换排序方式会立即重排列表并刷新CAD预览。
3输出设置区:
输出目录、打印设备、图纸尺寸、打印样式、方向、文件名前缀、偏移,全部可视化设置。设置会自动保存,下次打开时恢复上次的选择(方向和前缀除外,每次都重置为默认值)。
4底部日志区:
实时显示打印进度,每张图完成都显示文件名和耗时,最后汇总总耗时和整体速率。
没有复杂配置,没有学习成本,一个命令搞定。
核心干货:给AI的提示词────────────────────────────────────────────────────────────────────────────────────────────────────
下面是我实际用的提示词。
1立项目标
我跟AI说,我想做一个CAD批量打印工具,用C#和.NET 4.8。需求有六条。
能框选一批实体,自动识别出里面的图框。图框可能是块参照,可能是闭合矩形多段线,可能是四条端点相连的直线。
识别出来的图框要能排序,支持从左到右、从上到下这些常见方式。 在CAD图形区绘制预览,淡黄色矩形包围盒加序号,让我一眼看出打印顺序。 支持批量输出PDF,每张图一个文件,文件名用图纸名加序号。 打印方向可以选自动、横向、竖向。 界面用WPF,不要太老土的样式。
最后加一句:请给我完整的项目结构和代码,并解释一下思路。
2解决识别问题
第一版做出来之后,我发现框选识别的时候,块参照识别得挺好,但矩形和直线组识别不准。有的矩形被识别成好几个图框,有的直线组干脆识别不到。
于是我跟AI说:图框识别不够准。闭合矩形应该算一个图框,不应该拆成四条边。直线组要按端点连通性分组,一组算一个图框。另外,识别出来的图框要过滤掉被包含的小图框,只保留最外层的大图框。
3加排序和预览
识别准了之后,我又跟AI说:加一个排序功能,支持八种排序方式,切换的时候CAD预览要立即更新。排序用图框的包围盒坐标算,从左到右按左下角X升序,从上到下按左下角Y降序,网格模式先按Y分行,行内按X排序。
4加打印方向
打印能跑通之后,我发现横竖图框混在一起的时候方向不对。于是跟AI说:加打印方向选项,自动模式下要按图框的宽高比逐张判断旋转角度。横向图框输出横向PDF,竖向图框输出竖向PDF。
5优化性能和体验
最后做优化。框选的时候窗口要自动隐藏,框选完自动恢复并置顶。打印的时候不要弹进度条,要静默输出。每一步要记录耗时,方便排查慢在哪里。
写提示词的核心原则────────────────────────────────────────────────────────────────────────────────────────────────────
回头看,这次成功的关键,还是把提示词写成了需求说明书,而不是一句"帮我写个批量打印插件"。
总结出几条原则,你写任何AI辅助开发都能用。
1明确技术方向,不留余地。
错误写法是"做个CAD插件"。正确写法是"用C#语言,.NET 4.8版本,做一个CAD插件"。说清楚版本和环境,AI才不会给你一个跑不了的方案。
2把业务规则讲成判断条件。
错误写法是"让图框智能识别"。正确写法是"闭合多段线算一个图框,端点相连的直线组算一个图框,块参照算一个图框,被包含的图框要过滤掉"。说清楚什么情况下该做什么,AI才知道怎么判断。
3一次一个目标,别贪多。
错误写法是"做一个工具,能识别图框,能排序,能预览,能打印,能合并PDF,能加水印"。正确写法是"先做图框识别和排序,打印后面再说"。分批交付,每轮都验证,比一次性给一大堆更容易成功。
4反馈要具体到报错信息。
错误写法是"不行,有问题"。正确写法是"编译时报错,说找不到某个方法,文件是某某文件第几行,完整错误码是某某"。把错误码、文件、行号一起给,AI定位问题才快。
5让AI自我解释。
在你觉得方案有疑问时,加一句:请先解释你的方案思路,我确认后再给代码。这一句能省下大量返工时间。
核心源码分享────────────────────────────────────────────────────────────────────────────────────────────────────
这是一部分代码,完整代码可以领取,见文末
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
using Autodesk.AutoCAD.ApplicationServices;
using Autodesk.AutoCAD.DatabaseServices;
using Autodesk.AutoCAD.PlottingServices;
using CadBatchPlot.Models;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
namespaceCadBatchPlot.Services
{
///<summary>
/// 批量打印服务
/// 方向选项:横向/竖向/自动
/// 横向 = 全部输出横向 PDF
/// 竖向 = 全部输出竖向 PDF
/// 自动 = 按图框宽高比,横的对横,竖的对竖
///</summary>
internalstaticclassPlotService
{
publicstatic List<string> GetDevices()
{
var list = new List<string>();
try
{
var psv = PlotSettingsValidator.Current;
foreach (string s in psv.GetPlotDeviceList())
list.Add(s);
}
catch (Exception ex)
{
PlotLogger.Error("获取打印设备列表失败", ex);
}
list.Sort();
PlotLogger.Info($"获取打印设备列表完成,共 {list.Count} 个设备");
return list;
}
publicstatic List<PaperSizeItem> GetPaperSizes(string deviceName, Database db)
{
var list = new List<PaperSizeItem>();
PlotLogger.Separator($"获取纸张列表: {deviceName}");
try
{
using (var tr = db.TransactionManager.StartTransaction())
{
var modelLayoutId = GetModelLayoutId(db, tr);
if (modelLayoutId.IsNull)
{
PlotLogger.Error("找不到模型空间布局");
return list;
}
var layout = (Layout)tr.GetObject(modelLayoutId, OpenMode.ForRead);
var ps = new PlotSettings(layout.ModelType);
ps.CopyFrom(layout);
var psv = PlotSettingsValidator.Current;
psv.SetPlotConfigurationName(ps, deviceName, null);
try { psv.RefreshLists(ps); } catch { }
var mediaList = psv.GetCanonicalMediaNameList(ps);
PlotLogger.Info($"设备返回了 {mediaList.Count} 个纸张尺寸");
for (int i = 0; i < mediaList.Count; i++)
{
string canon = mediaList[i];
if (!IsEngineeringPaper(canon)) continue;
string display = "";
try { display = psv.GetLocaleMediaName(ps, i); } catch { }
if (string.IsNullOrWhiteSpace(display)) display = canon;
list.Add(new PaperSizeItem
{
Canonical = canon,
Display = display
});
}
PlotLogger.Info($"过滤后保留 {list.Count} 个工程图纸尺寸");
tr.Commit();
}
}
catch (Exception ex)
{
PlotLogger.Error($"获取纸张列表失败 (设备: {deviceName})", ex);
}
PlotLogger.Separator();
return list;
}
publicstatic List<string> GetPlotStyles()
{
var result = new List<string>();
var dirs = new List<string>();
PlotLogger.Separator("扫描打印样式文件");
string[] roots =
{
Environment.GetFolderPath(Environment.SpecialFolder.ApplicationData),
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFiles),
Environment.GetFolderPath(Environment.SpecialFolder.ProgramFilesX86),
Environment.GetFolderPath(Environment.SpecialFolder.CommonApplicationData),
};
foreach (var root in roots)
{
if (string.IsNullOrEmpty(root) || !Directory.Exists(root)) continue;
try
{
foreach (var autodesk in Directory.GetDirectories(root, "Autodesk"))
{
try
{
foreach (var ps in Directory.GetDirectories(
autodesk, "Plot Styles", SearchOption.AllDirectories))
{
dirs.Add(ps);
}
}
catch { }
}
}
catch { }
}
var seenDirs = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
foreach (var d in dirs)
{
if (!seenDirs.Add(d)) continue;
try
{
foreach (var f in Directory.GetFiles(d))
{
string ext = Path.GetExtension(f).ToLowerInvariant();
if (ext == ".ctb" || ext == ".stb")
result.Add(Path.GetFileName(f));
}
}
catch { }
}
var unique = new HashSet<string>(StringComparer.OrdinalIgnoreCase);
var final = new List<string>();
foreach (var s in result)
if (unique.Add(s)) final.Add(s);
final.Sort(StringComparer.OrdinalIgnoreCase);
if (final.Count == 0)
{
final.Add("monochrome.ctb");
final.Add("acad.ctb");
}
PlotLogger.Info($"找到 {final.Count} 个打印样式文件");
PlotLogger.Separator();
return final;
}
publicstaticintPlotFramesToPdf(
Database db,
List<FrameInfo> frames,
BatchPlotSettings settings,
string outputFolder,
Action<string> logAction)
{
if (frames == null || frames.Count == 0) return0;
var totalSw = Stopwatch.StartNew();
string dirText = settings.Orientation == "Landscape" ? "横向"
: settings.Orientation == "Portrait" ? "竖向"
: "自动(按图框方向)";
PlotLogger.Separator("打印参数");
PlotLogger.Info($"图框数量 : {frames.Count}");
PlotLogger.Info($"输出目录 : {outputFolder}");
PlotLogger.Info($"打印设备 : {settings.DeviceName}");
PlotLogger.Info($"图纸尺寸 : {settings.PaperSize}");
PlotLogger.Info($"方向选择 : {settings.Orientation} ({dirText})");
PlotLogger.Info($"打印样式 : {settings.PlotStyleFile}");
PlotLogger.Separator();
logAction?.Invoke($"开始打印 {frames.Count} 张,请稍候...");
short originalBgPlot = 0;
try
{
originalBgPlot = (short)Application.GetSystemVariable("BACKGROUNDPLOT");
Application.SetSystemVariable("BACKGROUNDPLOT", 0);
PlotLogger.Info($"BACKGROUNDPLOT 由 {originalBgPlot} 临时改为 0");
}
catch (Exception ex)
{
PlotLogger.Warn($"设置 BACKGROUNDPLOT 失败: {ex.Message}");
}
if (PlotFactory.ProcessPlotState != ProcessPlotState.NotPlotting)
{
PlotLogger.Warn("已有打印任务在进行");
logAction?.Invoke("已有打印任务在进行,请等待完成后再试。");
return0;
}
int successCount = 0;
int failCount = 0;
var perFrameTimes = new List<double>();
try
{
// ============================================================
// 阶段一:设备配置(只做一次)
// ============================================================
var configTimer = new StepTimer(" [配置] ");
PlotLogger.Info("===== 阶段一:设备配置 =====");
PlotInfo pi = null;
PlotSettings ps = null;
PlotSettingsValidator psv = null;
using (var tr = db.TransactionManager.StartTransaction())
{
var modelLayoutId = GetModelLayoutId(db, tr);
if (modelLayoutId.IsNull)
{
PlotLogger.Error("找不到模型空间布局");
return0;
}
var layout = (Layout)tr.GetObject(modelLayoutId, OpenMode.ForRead);
pi = new PlotInfo();
pi.Layout = modelLayoutId;
configTimer.Mark("创建 PlotInfo");
ps = new PlotSettings(layout.ModelType);
ps.CopyFrom(layout);
configTimer.Mark("创建 PlotSettings");
psv = PlotSettingsValidator.Current;
psv.SetPlotConfigurationName(ps, settings.DeviceName, settings.PaperSize);
configTimer.Mark("SetPlotConfigurationName");
try { psv.RefreshLists(ps); } catch { }
configTimer.Mark("RefreshLists");
psv.SetUseStandardScale(ps, true);
psv.SetStdScaleType(ps, StdScaleType.ScaleToFit);
psv.SetPlotCentered(ps, true);
configTimer.Mark("比例与居中");
// 旋转不在这里设置,改到循环内根据图框动态设置
if (!string.IsNullOrWhiteSpace(settings.PlotStyleFile))
{
try
{
psv.SetCurrentStyleSheet(ps, settings.PlotStyleFile);
configTimer.Mark("打印样式");
}
catch (Exception ex)
{
PlotLogger.Warn($" [配置] 打印样式设置失败: {ex.Message}");
}
}
tr.Commit();
configTimer.Mark("提交事务");
}
configTimer.Stop();
PlotLogger.Info($"===== 阶段一完成,耗时 {configTimer.TotalMs:F1}ms =====");
// ============================================================
// 阶段二:逐张打印
// ============================================================
var engineTimer = new StepTimer(" [引擎] ");
PlotLogger.Info("===== 阶段二:逐张打印 =====");
using (var engine = PlotFactory.CreatePublishEngine())
{
engine.BeginPlot(null, null);
engineTimer.Mark("BeginPlot");
PlotLogger.Info("PlotEngine 启动成功");
for (int i = 0; i < frames.Count; i++)
{
var frame = frames[i];
int seq = i + 1;
string fileName = $"{settings.FileNamePrefix}_{seq:D3}.pdf";
string fullPath = Path.Combine(outputFolder, fileName);
// ⭐ 逐张计算旋转
var rotation = GetRotation(settings.Orientation, frame);
string frameDir = frame.Width >= frame.Height ? "横向" : "竖向";
string outDir = rotation == PlotRotation.Degrees090 ? "横向" : "竖向";
var frameTimer = new StepTimer(" ");
PlotLogger.Info($"--- 第 {seq}/{frames.Count} 张: {fileName} ---");
PlotLogger.Info($" 图框: {frame.Width:F0} x {frame.Height:F0} ({frameDir}) -> 输出 {outDir}");
try
{
PlotSingleFrame(db, frame, settings, fullPath, engine,
pi, ps, psv, rotation, frameTimer);
frameTimer.Stop();
double sec = frameTimer.TotalMs / 1000.0;
perFrameTimes.Add(sec);
successCount++;
PlotLogger.Info($" ✔ {fileName} 完成,耗时 {frameTimer.TotalMs:F1}ms");
logAction?.Invoke($" [OK] {fileName} ({seq}/{frames.Count}) {frameTimer.TotalMs:F0}ms");
}
catch (Exception ex)
{
frameTimer.Stop();
failCount++;
PlotLogger.Error($" ✘ {fileName} 失败,耗时 {frameTimer.TotalMs:F1}ms", ex);
logAction?.Invoke($" [X] {fileName} 失败 ({frameTimer.TotalMs:F0}ms): {ex.Message}");
}
}
engine.EndPlot(null);
engineTimer.Mark("EndPlot");
PlotLogger.Info("PlotEngine 结束");
}
}
catch (Exception ex)
{
PlotLogger.Error("批量打印异常", ex);
logAction?.Invoke($"打印异常: {ex.Message}");
}
finally
{
try
{
Application.SetSystemVariable("BACKGROUNDPLOT", originalBgPlot);
PlotLogger.Info($"BACKGROUNDPLOT 已恢复为 {originalBgPlot}");
}
catch { }
}
totalSw.Stop();
double totalSec = totalSw.Elapsed.TotalSeconds;
PlotLogger.Separator("打印统计");
PlotLogger.Info($"结束时间: {DateTime.Now:yyyy-MM-dd HH:mm:ss}");
PlotLogger.Info($"总耗时 : {FormatDuration(totalSec)}");
PlotLogger.Info($"成功 : {successCount} 张,失败 {failCount} 张");
if (perFrameTimes.Count > 0)
{
double sum = 0, min = double.MaxValue, max = 0;
foreach (var t in perFrameTimes)
{
sum += t;
if (t < min) min = t;
if (t > max) max = t;
}
double avg = sum / perFrameTimes.Count;
PlotLogger.Info($"单张耗时: 最快 {min * 1000:F1}ms / 最慢 {max * 1000:F1}ms / 平均 {avg * 1000:F1}ms");
}
if (totalSec > 0 && successCount > 0)
{
PlotLogger.Info($"整体速率: {successCount / totalSec:F2} 张/秒");
}
PlotLogger.Separator();
logAction?.Invoke($"──────────────────────────────");
logAction?.Invoke($"打印完成: 成功 {successCount} 张,失败 {failCount} 张");
logAction?.Invoke($"总耗时: {FormatDuration(totalSec)}");
return successCount;
}
privatestaticvoidPlotSingleFrame(
Database db,
FrameInfo frame,
BatchPlotSettings settings,
string outputPath,
PlotEngine engine,
PlotInfo pi,
PlotSettings ps,
PlotSettingsValidator psv,
PlotRotation rotation,
StepTimer timer)
{
using (var tr = db.TransactionManager.StartTransaction())
{
timer.Mark("开始事务");
var ext = new Extents2d(
frame.MinX - settings.Offset,
frame.MinY - settings.Offset,
frame.MaxX + settings.Offset,
frame.MaxY + settings.Offset);
psv.SetPlotWindowArea(ps, ext);
timer.Mark("SetPlotWindowArea");
// ⭐ 逐张设置旋转
psv.SetPlotRotation(ps, rotation);
timer.Mark($"SetPlotRotation({rotation})");
psv.SetPlotType(ps, Autodesk.AutoCAD.DatabaseServices.PlotType.Window);
timer.Mark("SetPlotType(Window)");
pi.OverrideSettings = ps;
timer.Mark("OverrideSettings");
var piv = new PlotInfoValidator();
piv.MediaMatchingPolicy = MatchingPolicy.MatchEnabled;
piv.Validate(pi);
timer.Mark("Validate");
engine.BeginDocument(pi, db.Filename, null, 1, true, outputPath);
timer.Mark("BeginDocument");
var ppi = new PlotPageInfo();
engine.BeginPage(ppi, pi, true, null);
timer.Mark("BeginPage");
engine.BeginGenerateGraphics(null);
timer.Mark("BeginGenerateGraphics");
engine.EndGenerateGraphics(null);
timer.Mark("EndGenerateGraphics");
engine.EndPage(null);
timer.Mark("EndPage");
engine.EndDocument(null);
timer.Mark("EndDocument");
tr.Commit();
timer.Mark("提交事务");
if (!File.Exists(outputPath))
{
thrownew InvalidOperationException($"输出文件未生成: {outputPath}");
}
}
}
///<summary>
/// 根据方向选项和图框宽高比计算旋转角度
/// 纸张默认是竖向的(例如 A0 = 841x1189)
/// Degrees000 = 竖向输出
/// Degrees090 = 横向输出
///</summary>
privatestatic PlotRotation GetRotation(string orientation, FrameInfo frame)
{
if (orientation == "Landscape")
{
// 强制横向:所有图框都旋转 90 度
return PlotRotation.Degrees090;
}
if (orientation == "Portrait")
{
// 强制竖向:所有图框都不旋转
return PlotRotation.Degrees000;
}
// 自动:按图框宽高比
// 图框横向 -> Degrees090(横向输出)
// 图框竖向 -> Degrees000(竖向输出)
bool frameIsLandscape = frame.Width >= frame.Height;
return frameIsLandscape ? PlotRotation.Degrees090 : PlotRotation.Degrees000;
}
privatestaticstringFormatDuration(double totalSeconds)
{
if (totalSeconds < 60)
return$"{totalSeconds:F3} 秒";
var ts = TimeSpan.FromSeconds(totalSeconds);
if (ts.TotalHours >= 1)
return$"{(int)ts.TotalHours} 小时 {ts.Minutes} 分 {ts.Seconds} 秒";
else
return$"{ts.Minutes} 分 {ts.Seconds} 秒";
}
privatestatic ObjectId GetModelLayoutId(Database db, Transaction tr)
{
var bt = (BlockTable)tr.GetObject(db.BlockTableId, OpenMode.ForRead);
foreach (ObjectId id in bt)
{
var btr = (BlockTableRecord)tr.GetObject(id, OpenMode.ForRead);
if (btr.IsLayout)
{
var layout = (Layout)tr.GetObject(btr.LayoutId, OpenMode.ForRead);
if (layout.ModelType) return layout.ObjectId;
}
}
return ObjectId.Null;
}
privatestaticboolIsEngineeringPaper(string canonical)
{
if (string.IsNullOrEmpty(canonical)) returnfalse;
string s = canonical.ToUpperInvariant();
string[] excludeKeywords =
{
"PHOTO", "ENVELOPE", "BUSINESS", "CARD", "POSTCARD",
"CREDIT", "JAPANESE", "KAKU", "CHOU", "YOU #",
"CHOU #", "MONARCH", "STATEMENT", "EXECUTIVE",
};
foreach (var kw in excludeKeywords)
{
if (s.Contains(kw)) returnfalse;
}
returntrue;
}
}
}
AI开发插件的完整流程────────────────────────────────────────────────────────────────────────────────────────────────────
把这次的实战经验,抽象成可复用的流程。
明确需求。用一句话概括"我想让CAD做某事",然后拆解成核心功能、附加功能、交互方式。 编写提示词。包含技术方向、业务规则、禁用项、分批目标。 第一轮生成。让AI给完整项目结构和代码。 编译测试。遇到报错,把错误内容完整发给AI。 加调试日志。让AI加日志系统,看看到底哪一步没生效。 优化体验。最后再打磨界面、预览、性能这些细节。
成品领取 & 下期预告────────────────────────────────────────────────────────────────────────────────────────────────────
不想自己编译、调代码的朋友,关注公众号 「下班让AI教我做插件」,后台回复 「CAD批量打印」,直接领取编译好的 DLL 插件文件,NETLOAD 加载或者是使用我写的dll插件加载工具箱。 完整工程源码拿到手就能二次修改。
下期预告
下一篇咱们继续图纸提效工具,如果有想要的插件可以在评论区留言,我看看哪个留言最多就做哪个。
我是做插件的设计师,专注用 AI 提高 CAD 设计效率。关注我,下班一起做插件,把重复工作交给程序。