@bill_nee/dwg-reader 1.1.1 → 1.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.
package/lib/index.js CHANGED
@@ -1,9 +1,12 @@
1
1
  import { fileURLToPath } from "node:url";
2
+ import { spawn } from "node:child_process";
2
3
  import { LibreDwg, Dwg_File_Type, DwgCodePage } from "../vendor/libredwg-web/lib/index.js";
3
4
 
4
5
  const WASM_DIR = fileURLToPath(new URL("../vendor/libredwg-web/wasm", import.meta.url));
5
6
  const MAX_DWG_BYTES = 512 * 1024 * 1024; // 512 MB 上限,超大图纸会先于 WASM 内存耗尽给出明确报错
6
7
  const MAX_ENTITY_DETAIL = 2000;
8
+ const PYTHON_EXE = "D:\\dsh-home\\dsh-runtimes\\dsh-primary-runtime\\dependencies\\python\\python.exe";
9
+ const DXF_SCRIPT = fileURLToPath(new URL("../scripts/read_dxf.py", import.meta.url));
7
10
 
8
11
  let libreDwgPromise;
9
12
 
@@ -371,9 +374,202 @@ function buildReadDwgTool(ctx) {
371
374
  };
372
375
  }
373
376
 
377
+ function buildReadDxfTool(ctx) {
378
+ return {
379
+ name: "read_dxf",
380
+ description:
381
+ "读取 DXF 图纸文件(基于 Python ezdxf 桥接,无需安装 AutoCAD)。" +
382
+ "返回 DXF 版本、图层表、块数量、实体统计(按类型计数)与可选实体明细。",
383
+ parameters: {
384
+ type: "object",
385
+ additionalProperties: true,
386
+ properties: {
387
+ file_path: {
388
+ type: "string",
389
+ description: "DXF 文件路径(绝对路径或相对当前工作目录)。",
390
+ },
391
+ entities: {
392
+ type: "boolean",
393
+ description: "是否返回实体明细列表(默认 false,只返回统计)。",
394
+ },
395
+ entity_limit: {
396
+ type: "integer",
397
+ description: `实体明细最多返回条数(默认 200,上限 ${MAX_ENTITY_DETAIL})。`,
398
+ },
399
+ },
400
+ required: ["file_path"],
401
+ },
402
+ output: {
403
+ schema: {
404
+ type: "object",
405
+ additionalProperties: true,
406
+ properties: {
407
+ path: { type: "string", description: "解析后的文件路径。" },
408
+ fileName: { type: "string", description: "文件名。" },
409
+ fileSizeBytes: { type: "integer", description: "文件字节数。" },
410
+ format: { type: "string", description: "文件格式(固定为 dxf)。" },
411
+ dxfVersion: { type: "string", description: "DXF 版本代号(如 R2010)。" },
412
+ header: { type: "object", additionalProperties: true, description: "图头关键变量。" },
413
+ layerCount: { type: "integer", description: "图层数量。" },
414
+ layers: {
415
+ type: "array",
416
+ items: { type: "object", additionalProperties: true },
417
+ description: "图层表(名称/颜色/线型/开关状态)。",
418
+ },
419
+ blockCount: { type: "integer", description: "块定义数量。" },
420
+ entityCount: { type: "integer", description: "实体总数。" },
421
+ entityTypeCounts: {
422
+ type: "object",
423
+ additionalProperties: true,
424
+ description: "按实体类型统计数量(如 { LINE: 120, TEXT: 8 })。",
425
+ },
426
+ entities: {
427
+ type: "array",
428
+ items: { type: "object", additionalProperties: true },
429
+ description: "实体明细(仅当 entities=true 时非空)。",
430
+ },
431
+ entitiesTruncated: { type: "boolean", description: "实体明细是否被截断。" },
432
+ error: { type: "string", description: "错误信息;成功时为 null。" },
433
+ },
434
+ },
435
+ render: (args, value) => {
436
+ const lines = [];
437
+ lines.push(`<path>${value.path}</path>`);
438
+ lines.push(`<type>dxf</type>`);
439
+ if (value.error) {
440
+ lines.push(`<content>读取 DXF 失败:${value.error}</content>`);
441
+ return [{ type: "text", text: lines.join("\n") }];
442
+ }
443
+ const sizeMb = (value.fileSizeBytes / 1024 / 1024).toFixed(2);
444
+ lines.push(`<content>已解析 DXF:${value.fileName}(${sizeMb} MB)`);
445
+ lines.push(`版本:${value.dxfVersion}`);
446
+ lines.push(`图层:${value.layerCount} 个${value.layers.length ? " — " + value.layers.map((l) => l.name).join(", ") : ""}`);
447
+ lines.push(`块定义:${value.blockCount} 个;实体:${value.entityCount} 个`);
448
+ const typeLines = Object.entries(value.entityTypeCounts)
449
+ .sort((a, b) => b[1] - a[1])
450
+ .slice(0, 20)
451
+ .map(([t, n]) => `${t} ${n}`)
452
+ .join(",");
453
+ if (typeLines) lines.push(`实体构成:${typeLines}${Object.keys(value.entityTypeCounts).length > 20 ? ",…" : ""}`);
454
+ if (value.header && value.header.$ACADVER) lines.push(`图版:${value.header.$ACADVER}`);
455
+ if (value.entities && value.entities.length) {
456
+ lines.push(`实体明细(${value.entities.length}${value.entitiesTruncated ? ",已截断" : ""}):`);
457
+ const shown = value.entities.slice(0, 50);
458
+ for (const [i, e] of shown.entries()) {
459
+ lines.push(
460
+ ` ${i + 1}. ${e.type} handle=${e.handle} layer=${e.layer}` +
461
+ (e.colorIndex != null ? ` color=${e.colorIndex}` : "") +
462
+ (e.lineType != null ? ` linetype=${e.lineType}` : "")
463
+ );
464
+ }
465
+ if (value.entities.length > shown.length) lines.push(` …(其余 ${value.entities.length - shown.length} 条未显示)`);
466
+ }
467
+ lines.push(`</content>`);
468
+ return [{ type: "text", text: lines.join("\n") }];
469
+ },
470
+ presentationMeta: () => null,
471
+ },
472
+ isConcurrencySafe: () => false,
473
+ presentCall: (args) => ({
474
+ card: "generic",
475
+ title: `读取 DXF ${args.file_path || "(未指定路径)"}`,
476
+ kind: "read",
477
+ locations: [{ path: args.file_path || "" }],
478
+ }),
479
+ presentResult: (args, result) => {
480
+ if (result && result.isError) return undefined;
481
+ return {
482
+ card: "generic",
483
+ title: `DXF 解析完成:${args.file_path || ""}`,
484
+ kind: "read",
485
+ locations: [{ path: args.file_path || "" }],
486
+ };
487
+ },
488
+ async execute(args, exec) {
489
+ if (typeof args !== "object" || args === null) {
490
+ throw new Error('invalid arguments: expected an object with a "file_path" string');
491
+ }
492
+ if (typeof args.file_path !== "string" || !args.file_path.trim()) {
493
+ throw new Error('invalid arguments: "file_path" must be a non-empty string');
494
+ }
495
+ if (args.entities !== undefined && typeof args.entities !== "boolean") {
496
+ throw new Error('invalid arguments: "entities" must be a boolean');
497
+ }
498
+ if (args.entity_limit !== undefined && (typeof args.entity_limit !== "number" || !Number.isInteger(args.entity_limit))) {
499
+ throw new Error('invalid arguments: "entity_limit" must be an integer');
500
+ }
501
+
502
+ const resolveOptions = await sessionResolveOptions(ctx, exec);
503
+ const target = await ctx.fs.resolve(args.file_path.trim(), resolveOptions);
504
+ const info = await ctx.fs.stat(target, exec.signal);
505
+ const displayPath = ctx.fs.processPath(target);
506
+ if (!info) {
507
+ ctx.emit("fs/observed", target, { kind: "absent" }, exec);
508
+ throw new Error(`cannot read "${displayPath}": not found`);
509
+ }
510
+ if (info.type !== "file") {
511
+ throw new Error(`cannot read "${displayPath}": not a regular file`);
512
+ }
513
+
514
+ const pythonArgs = [
515
+ DXF_SCRIPT,
516
+ args.file_path.trim(),
517
+ String(args.entity_limit || 200),
518
+ String(args.entities === true),
519
+ ];
520
+
521
+ const result = await new Promise((resolve, reject) => {
522
+ const proc = spawn(PYTHON_EXE, pythonArgs, { stdio: ["pipe", "pipe", "pipe"] });
523
+ let stdout = "";
524
+ let stderr = "";
525
+ proc.stdout.on("data", (d) => (stdout += d.toString()));
526
+ proc.stderr.on("data", (d) => (stderr += d.toString()));
527
+ proc.on("close", (code) => {
528
+ if (code !== 0) {
529
+ reject(new Error(`Python 进程退出码 ${code}: ${stderr || stdout}`));
530
+ return;
531
+ }
532
+ try {
533
+ resolve(JSON.parse(stdout));
534
+ } catch (err) {
535
+ reject(new Error(`DXF 结果解析失败: ${err.message}; stdout=${stdout.slice(0, 200)}`));
536
+ }
537
+ });
538
+ proc.on("error", (err) => reject(new Error(`无法启动 Python 进程: ${err.message}`)));
539
+ });
540
+
541
+ if (result.error) {
542
+ throw new Error(`cannot read "${displayPath}": ${result.error}`);
543
+ }
544
+
545
+ ctx.emit("fs/observed", target, { kind: "present", version: info.version }, exec);
546
+
547
+ return toJson({
548
+ path: displayPath,
549
+ fileName: displayPath.split(/[\\/]/).pop(),
550
+ fileSizeBytes: result.fileSizeBytes ?? info.size !== undefined ? info.size : 0,
551
+ format: result.format ?? "dxf",
552
+ dxfVersion: result.dxfVersion ?? "",
553
+ header: result.header ?? {},
554
+ layerCount: result.layerCount ?? 0,
555
+ layers: result.layers ?? [],
556
+ blockCount: result.blockCount ?? 0,
557
+ entityCount: result.entityCount ?? 0,
558
+ entityTypeCounts: result.entityTypeCounts ?? {},
559
+ entities: result.entities ?? [],
560
+ entitiesTruncated: !!result.entitiesTruncated,
561
+ });
562
+ },
563
+ };
564
+ }
565
+
374
566
  export const inject = ["tools", "fs", "workingDirectory"];
375
567
 
376
568
  export function apply(ctx, config) {
377
- const dispose = ctx.tools.register(buildReadDwgTool(ctx));
378
- return () => dispose();
569
+ const disposeDwg = ctx.tools.register(buildReadDwgTool(ctx));
570
+ const disposeDxf = ctx.tools.register(buildReadDxfTool(ctx));
571
+ return () => {
572
+ disposeDwg();
573
+ disposeDxf();
574
+ };
379
575
  }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@bill_nee/dwg-reader",
3
- "version": "1.1.1",
4
- "description": "直接读取 AutoCAD DWG 图纸的 DSH 插件(基于 libredwg-web 0.8.2 WASM,GPL-3.0)。",
3
+ "version": "1.2.0",
4
+ "description": "直接读取 AutoCAD DWG/DXF 图纸的 DSH 插件(DWG 基于 libredwg-web 0.8.2 WASM,DXF 基于 Python ezdxf 桥接,GPL-3.0)。",
5
5
  "license": "GPL-3.0-only",
6
6
  "author": "MEB 工程造价项目助手 <meb@example.com>",
7
7
  "repository": {
@@ -21,6 +21,7 @@
21
21
  "files": [
22
22
  "lib/index.js",
23
23
  "vendor",
24
+ "scripts/*.py",
24
25
  "cordis.patch.yml",
25
26
  "locale/*.json",
26
27
  "icon.svg",
@@ -0,0 +1,104 @@
1
+ #!/usr/bin/env python3
2
+ """读取 DXF 文件并输出 JSON(供 DSH dwg-reader 插件调用)。"""
3
+ import sys
4
+ import json
5
+ import os
6
+
7
+ try:
8
+ import ezdxf
9
+ except ImportError as exc:
10
+ print(json.dumps({"error": f"ezdxf 未安装: {exc}"}))
11
+ sys.exit(1)
12
+
13
+ DXF_PATH = sys.argv[1] if len(sys.argv) > 1 else None
14
+ ENTITY_LIMIT = int(sys.argv[2]) if len(sys.argv) > 2 else 200
15
+ WANT_ENTITIES = sys.argv[3] == "true" if len(sys.argv) > 3 else False
16
+
17
+
18
+ def point_to_list(p):
19
+ if p is None:
20
+ return None
21
+ return [float(p.x), float(p.y), float(p.z)]
22
+
23
+
24
+ def main():
25
+ if not DXF_PATH or not os.path.exists(DXF_PATH):
26
+ print(json.dumps({"error": f"文件不存在: {DXF_PATH}"}))
27
+ sys.exit(1)
28
+
29
+ try:
30
+ doc = ezdxf.readfile(DXF_PATH)
31
+ except Exception as exc:
32
+ print(json.dumps({"error": f"无法读取 DXF: {exc}"}))
33
+ sys.exit(1)
34
+
35
+ # 图层
36
+ layers = []
37
+ for layer in doc.layers:
38
+ layers.append({
39
+ "name": layer.dxf.name,
40
+ "colorIndex": layer.dxf.color,
41
+ "lineType": layer.dxf.linetype,
42
+ "frozen": bool(layer.is_frozen()),
43
+ "off": bool(layer.is_off()),
44
+ "locked": bool(layer.is_locked()),
45
+ })
46
+
47
+ # 块记录
48
+ block_count = len(list(doc.blocks))
49
+
50
+ # 实体统计
51
+ msp = doc.modelspace()
52
+ entities = list(msp)
53
+ entity_type_counts = {}
54
+ entity_details = []
55
+ for ent in entities:
56
+ etype = ent.dxftype() or "UNKNOWN"
57
+ entity_type_counts[etype] = entity_type_counts.get(etype, 0) + 1
58
+ if WANT_ENTITIES and len(entity_details) < ENTITY_LIMIT:
59
+ detail = {
60
+ "type": etype,
61
+ "handle": ent.dxf.handle if hasattr(ent.dxf, 'handle') else None,
62
+ "layer": ent.dxf.layer if hasattr(ent.dxf, 'layer') else None,
63
+ "colorIndex": ent.dxf.color if hasattr(ent.dxf, 'color') else None,
64
+ "lineType": ent.dxf.linetype if hasattr(ent.dxf, 'linetype') else None,
65
+ }
66
+ if etype == "INSERT":
67
+ detail["blockName"] = ent.dxf.name if hasattr(ent.dxf, 'name') else None
68
+ detail["insertionPoint"] = point_to_list(ent.dxf.insert) if hasattr(ent.dxf, 'insert') else None
69
+ entity_details.append(detail)
70
+
71
+ # 图头
72
+ header = {}
73
+ try:
74
+ header_vars = doc.header
75
+ for key in ("$ACADVER", "$DWGCODEPAGE", "$INSUNITS", "$EXTMIN", "$EXTMAX", "$TDCREATE", "$TDUPDATE", "$CLAYER"):
76
+ if key in header_vars:
77
+ val = header_vars[key]
78
+ if key == "$EXTMIN" or key == "$EXTMAX":
79
+ header[key] = point_to_list(val) if val else None
80
+ else:
81
+ header[key] = str(val)
82
+ except Exception:
83
+ pass
84
+
85
+ result = {
86
+ "path": os.path.abspath(DXF_PATH),
87
+ "fileName": os.path.basename(DXF_PATH),
88
+ "fileSizeBytes": os.path.getsize(DXF_PATH),
89
+ "format": "dxf",
90
+ "dxfVersion": doc.dxfversion,
91
+ "header": header,
92
+ "layerCount": len(layers),
93
+ "layers": layers,
94
+ "blockCount": block_count,
95
+ "entityCount": len(entities),
96
+ "entityTypeCounts": entity_type_counts,
97
+ "entities": entity_details,
98
+ "entitiesTruncated": WANT_ENTITIES and len(entities) > ENTITY_LIMIT,
99
+ }
100
+ print(json.dumps(result, ensure_ascii=False))
101
+
102
+
103
+ if __name__ == "__main__":
104
+ main()