@miclivs/cadcli 0.1.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/LICENSE +21 -0
- package/README.md +132 -0
- package/dist/commands/blocks.d.ts +4 -0
- package/dist/commands/blocks.js +22 -0
- package/dist/commands/edit.d.ts +8 -0
- package/dist/commands/edit.js +24 -0
- package/dist/commands/entities.d.ts +7 -0
- package/dist/commands/entities.js +26 -0
- package/dist/commands/info.d.ts +2 -0
- package/dist/commands/info.js +22 -0
- package/dist/commands/json.d.ts +2 -0
- package/dist/commands/json.js +20 -0
- package/dist/commands/layers.d.ts +4 -0
- package/dist/commands/layers.js +22 -0
- package/dist/commands/overview.d.ts +5 -0
- package/dist/commands/overview.js +59 -0
- package/dist/commands/search.d.ts +11 -0
- package/dist/commands/search.js +45 -0
- package/dist/commands/shared.d.ts +16 -0
- package/dist/commands/shared.js +40 -0
- package/dist/commands/svg.d.ts +2 -0
- package/dist/commands/svg.js +27 -0
- package/dist/commands/thumbnail.d.ts +2 -0
- package/dist/commands/thumbnail.js +31 -0
- package/dist/commands/view.d.ts +6 -0
- package/dist/commands/view.js +27 -0
- package/dist/core/adapter.d.ts +14 -0
- package/dist/core/adapter.js +165 -0
- package/dist/core/drawing.d.ts +14 -0
- package/dist/core/drawing.js +61 -0
- package/dist/core/errors.d.ts +5 -0
- package/dist/core/errors.js +10 -0
- package/dist/core/files.d.ts +7 -0
- package/dist/core/files.js +27 -0
- package/dist/core/libredwg.d.ts +37 -0
- package/dist/core/libredwg.js +86 -0
- package/dist/core/normalize.d.ts +3 -0
- package/dist/core/normalize.js +156 -0
- package/dist/core/overview.d.ts +35 -0
- package/dist/core/overview.js +227 -0
- package/dist/core/search-cache.d.ts +17 -0
- package/dist/core/search-cache.js +69 -0
- package/dist/core/search.d.ts +18 -0
- package/dist/core/search.js +137 -0
- package/dist/core/svg.d.ts +2 -0
- package/dist/core/svg.js +105 -0
- package/dist/index.d.ts +5 -0
- package/dist/index.js +1 -0
- package/dist/main.d.ts +2 -0
- package/dist/main.js +151 -0
- package/dist/sdk.d.ts +30 -0
- package/dist/sdk.js +57 -0
- package/dist/store.d.ts +6 -0
- package/dist/store.js +17 -0
- package/dist/types.d.ts +65 -0
- package/dist/types.js +1 -0
- package/dist/utils/exit-codes.d.ts +5 -0
- package/dist/utils/exit-codes.js +5 -0
- package/dist/utils/output.d.ts +19 -0
- package/dist/utils/output.js +26 -0
- package/package.json +76 -0
- package/patches/@node-projects%2Facad-ts@2.3.0.patch +48 -0
- package/skills/cadcli/SKILL.md +34 -0
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
import { DwgReader, DxfReader } from "@node-projects/acad-ts";
|
|
2
|
+
import { EXIT_UNAVAILABLE, EXIT_USER_ERROR } from "../utils/exit-codes.js";
|
|
3
|
+
import { DwgCliError } from "./errors.js";
|
|
4
|
+
import { readJsonWithLibreDwg } from "./libredwg.js";
|
|
5
|
+
function asRecord(value) {
|
|
6
|
+
return value && typeof value === "object"
|
|
7
|
+
? value
|
|
8
|
+
: {};
|
|
9
|
+
}
|
|
10
|
+
export function arrayBufferFor(bytes) {
|
|
11
|
+
return bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength);
|
|
12
|
+
}
|
|
13
|
+
function items(value) {
|
|
14
|
+
return value ? [...value] : [];
|
|
15
|
+
}
|
|
16
|
+
function point(value) {
|
|
17
|
+
const rec = asRecord(value);
|
|
18
|
+
const x = Number(rec.x ?? rec.X);
|
|
19
|
+
const y = Number(rec.y ?? rec.Y);
|
|
20
|
+
const z = Number(rec.z ?? rec.Z);
|
|
21
|
+
if (!Number.isFinite(x) || !Number.isFinite(y))
|
|
22
|
+
return undefined;
|
|
23
|
+
return Number.isFinite(z) ? { x, y, z } : { x, y };
|
|
24
|
+
}
|
|
25
|
+
function constructorName(value) {
|
|
26
|
+
const ctor = asRecord(value).constructor;
|
|
27
|
+
return typeof ctor === "function" && ctor.name ? ctor.name : "UNKNOWN";
|
|
28
|
+
}
|
|
29
|
+
function entityType(entity) {
|
|
30
|
+
const name = constructorName(entity);
|
|
31
|
+
const known = {
|
|
32
|
+
BlockReference: "INSERT",
|
|
33
|
+
Insert: "INSERT",
|
|
34
|
+
Line: "LINE",
|
|
35
|
+
Circle: "CIRCLE",
|
|
36
|
+
Arc: "ARC",
|
|
37
|
+
LwPolyline: "LWPOLYLINE",
|
|
38
|
+
Polyline2D: "POLYLINE",
|
|
39
|
+
Polyline3D: "POLYLINE",
|
|
40
|
+
TextEntity: "TEXT",
|
|
41
|
+
MText: "MTEXT",
|
|
42
|
+
Point: "POINT",
|
|
43
|
+
};
|
|
44
|
+
return known[name] ?? name.replace(/Entity$/, "").toUpperCase();
|
|
45
|
+
}
|
|
46
|
+
function layerName(entity) {
|
|
47
|
+
const layer = asRecord(entity).layer;
|
|
48
|
+
const name = asRecord(layer).name;
|
|
49
|
+
return name === undefined || name === null ? undefined : String(name);
|
|
50
|
+
}
|
|
51
|
+
function colorValue(entity) {
|
|
52
|
+
const color = asRecord(entity).color;
|
|
53
|
+
if (color === undefined || color === null)
|
|
54
|
+
return undefined;
|
|
55
|
+
const index = asRecord(color).index;
|
|
56
|
+
if (typeof index === "number")
|
|
57
|
+
return index;
|
|
58
|
+
return String(color);
|
|
59
|
+
}
|
|
60
|
+
function valueString(value) {
|
|
61
|
+
if (value === undefined || value === null)
|
|
62
|
+
return undefined;
|
|
63
|
+
const text = String(value).trim();
|
|
64
|
+
return text.length > 0 ? text : undefined;
|
|
65
|
+
}
|
|
66
|
+
function blockName(entity) {
|
|
67
|
+
const block = asRecord(entity).block;
|
|
68
|
+
return valueString(asRecord(block).name);
|
|
69
|
+
}
|
|
70
|
+
function vertices(entity) {
|
|
71
|
+
const raw = asRecord(entity).vertices;
|
|
72
|
+
if (!Array.isArray(raw))
|
|
73
|
+
return undefined;
|
|
74
|
+
const result = raw
|
|
75
|
+
.map((vertex) => point(asRecord(vertex).location ?? vertex))
|
|
76
|
+
.filter((vertex) => Boolean(vertex));
|
|
77
|
+
return result.length > 0 ? result : undefined;
|
|
78
|
+
}
|
|
79
|
+
function normalizeAcadEntity(entity) {
|
|
80
|
+
const rec = asRecord(entity);
|
|
81
|
+
const type = entityType(entity);
|
|
82
|
+
const data = {
|
|
83
|
+
handle: rec.handle,
|
|
84
|
+
type,
|
|
85
|
+
layer: layerName(entity),
|
|
86
|
+
color: colorValue(entity),
|
|
87
|
+
};
|
|
88
|
+
const start = point(rec.startPoint);
|
|
89
|
+
const end = point(rec.endPoint);
|
|
90
|
+
const center = point(rec.center);
|
|
91
|
+
const insertPoint = point(rec.insertPoint);
|
|
92
|
+
const location = point(rec.location);
|
|
93
|
+
const entityVertices = vertices(entity);
|
|
94
|
+
const text = valueString(rec.plainText ?? rec.value);
|
|
95
|
+
const name = blockName(entity);
|
|
96
|
+
if (start)
|
|
97
|
+
data.start = start;
|
|
98
|
+
if (end)
|
|
99
|
+
data.end = end;
|
|
100
|
+
if (center)
|
|
101
|
+
data.center = center;
|
|
102
|
+
if (insertPoint)
|
|
103
|
+
data.insertionPoint = insertPoint;
|
|
104
|
+
if (insertPoint)
|
|
105
|
+
data.position = insertPoint;
|
|
106
|
+
if (location)
|
|
107
|
+
data.point = location;
|
|
108
|
+
if (entityVertices)
|
|
109
|
+
data.vertices = entityVertices;
|
|
110
|
+
if (typeof rec.radius === "number")
|
|
111
|
+
data.radius = rec.radius;
|
|
112
|
+
if (typeof rec.startAngle === "number")
|
|
113
|
+
data.startAngle = rec.startAngle;
|
|
114
|
+
if (typeof rec.endAngle === "number")
|
|
115
|
+
data.endAngle = rec.endAngle;
|
|
116
|
+
if (text)
|
|
117
|
+
data.text = text;
|
|
118
|
+
if (name)
|
|
119
|
+
data.blockName = name;
|
|
120
|
+
return data;
|
|
121
|
+
}
|
|
122
|
+
export function normalizeAcadDocument(doc) {
|
|
123
|
+
const layers = items(doc.layers).map((layer) => ({ name: layer.name }));
|
|
124
|
+
const blocks = items(doc.blockRecords).map((block) => ({
|
|
125
|
+
name: block.name,
|
|
126
|
+
entities: items(block.entities).map((entity) => ({
|
|
127
|
+
type: entityType(entity),
|
|
128
|
+
})),
|
|
129
|
+
}));
|
|
130
|
+
const entities = items(doc.modelSpace?.entities).map(normalizeAcadEntity);
|
|
131
|
+
return {
|
|
132
|
+
version: doc.header?.versionString ?? String(doc.header?.version ?? ""),
|
|
133
|
+
layers,
|
|
134
|
+
blocks,
|
|
135
|
+
entities,
|
|
136
|
+
};
|
|
137
|
+
}
|
|
138
|
+
export class AcadTsReader {
|
|
139
|
+
async parse(_file, bytes, format) {
|
|
140
|
+
try {
|
|
141
|
+
const doc = format === "DWG"
|
|
142
|
+
? DwgReader.readFromStream(arrayBufferFor(bytes))
|
|
143
|
+
: DxfReader.readFromStream(bytes);
|
|
144
|
+
return normalizeAcadDocument(doc);
|
|
145
|
+
}
|
|
146
|
+
catch (error) {
|
|
147
|
+
throw new DwgCliError(`Could not parse ${format}: ${error.message}`, "PARSE_FAILED", EXIT_USER_ERROR);
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
async thumbnail() {
|
|
151
|
+
throw new DwgCliError("Thumbnail extraction is not available through acad-ts.", "THUMBNAIL_UNAVAILABLE", EXIT_UNAVAILABLE);
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
export class NativeLibreDwgReader {
|
|
155
|
+
toolDir;
|
|
156
|
+
constructor(toolDir) {
|
|
157
|
+
this.toolDir = toolDir;
|
|
158
|
+
}
|
|
159
|
+
async parse(file, _bytes, _format) {
|
|
160
|
+
return readJsonWithLibreDwg(file, { toolDir: this.toolDir }).json;
|
|
161
|
+
}
|
|
162
|
+
async thumbnail() {
|
|
163
|
+
throw new DwgCliError("Thumbnail extraction is not available through native LibreDWG.", "THUMBNAIL_UNAVAILABLE", EXIT_UNAVAILABLE);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
import type { DrawingReader, DwgBlock, DwgDocument, DwgEntity, DwgLayer, EntityFilter, SvgResult, ThumbnailResult } from "../types.js";
|
|
2
|
+
export interface LoadOptions {
|
|
3
|
+
reader?: DrawingReader;
|
|
4
|
+
toolDir?: string;
|
|
5
|
+
}
|
|
6
|
+
export declare function loadDrawing(file: string, opts?: LoadOptions): Promise<DwgDocument>;
|
|
7
|
+
export declare function getInfo(file: string, opts?: LoadOptions): Promise<import("../types.js").DwgSummary>;
|
|
8
|
+
export declare function getLayers(file: string, opts?: LoadOptions): Promise<DwgLayer[]>;
|
|
9
|
+
export declare function getBlocks(file: string, opts?: LoadOptions): Promise<DwgBlock[]>;
|
|
10
|
+
export declare function filterEntities(entities: DwgEntity[], filter?: EntityFilter): DwgEntity[];
|
|
11
|
+
export declare function getEntities(file: string, filter?: EntityFilter, opts?: LoadOptions): Promise<DwgEntity[]>;
|
|
12
|
+
export declare function toJson(file: string, opts?: LoadOptions): Promise<DwgDocument>;
|
|
13
|
+
export declare function toSvg(file: string, opts?: LoadOptions): Promise<SvgResult>;
|
|
14
|
+
export declare function getThumbnail(file: string, opts?: LoadOptions): Promise<ThumbnailResult>;
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { EXIT_UNAVAILABLE, EXIT_USER_ERROR } from "../utils/exit-codes.js";
|
|
2
|
+
import { AcadTsReader } from "./adapter.js";
|
|
3
|
+
import { DwgCliError } from "./errors.js";
|
|
4
|
+
import { readCadFile } from "./files.js";
|
|
5
|
+
import { normalizeDocument } from "./normalize.js";
|
|
6
|
+
import { renderSvg } from "./svg.js";
|
|
7
|
+
function readerFor(opts) {
|
|
8
|
+
return opts.reader ?? new AcadTsReader();
|
|
9
|
+
}
|
|
10
|
+
export async function loadDrawing(file, opts = {}) {
|
|
11
|
+
const { bytes, format } = readCadFile(file);
|
|
12
|
+
const raw = await readerFor(opts).parse(file, bytes, format);
|
|
13
|
+
return normalizeDocument(file, format, raw);
|
|
14
|
+
}
|
|
15
|
+
export async function getInfo(file, opts) {
|
|
16
|
+
return (await loadDrawing(file, opts)).summary;
|
|
17
|
+
}
|
|
18
|
+
export async function getLayers(file, opts) {
|
|
19
|
+
return (await loadDrawing(file, opts)).layers;
|
|
20
|
+
}
|
|
21
|
+
export async function getBlocks(file, opts) {
|
|
22
|
+
return (await loadDrawing(file, opts)).blocks;
|
|
23
|
+
}
|
|
24
|
+
export function filterEntities(entities, filter = {}) {
|
|
25
|
+
let result = entities;
|
|
26
|
+
if (filter.type)
|
|
27
|
+
result = result.filter((e) => e.type.toLowerCase() === filter.type?.toLowerCase());
|
|
28
|
+
if (filter.layer)
|
|
29
|
+
result = result.filter((e) => e.layer?.toLowerCase() === filter.layer?.toLowerCase());
|
|
30
|
+
if (filter.limit !== undefined)
|
|
31
|
+
result = result.slice(0, filter.limit);
|
|
32
|
+
return result;
|
|
33
|
+
}
|
|
34
|
+
export async function getEntities(file, filter = {}, opts) {
|
|
35
|
+
const doc = await loadDrawing(file, opts);
|
|
36
|
+
if (filter.layer &&
|
|
37
|
+
!doc.layers.some((l) => l.name.toLowerCase() === filter.layer?.toLowerCase())) {
|
|
38
|
+
throw new DwgCliError(`Layer not found: ${filter.layer}`, "LAYER_NOT_FOUND", EXIT_USER_ERROR);
|
|
39
|
+
}
|
|
40
|
+
if (filter.type &&
|
|
41
|
+
!doc.entities.some((e) => e.type.toLowerCase() === filter.type?.toLowerCase())) {
|
|
42
|
+
throw new DwgCliError(`Entity type not found: ${filter.type}`, "TYPE_NOT_FOUND", EXIT_USER_ERROR);
|
|
43
|
+
}
|
|
44
|
+
return filterEntities(doc.entities, filter);
|
|
45
|
+
}
|
|
46
|
+
export async function toJson(file, opts) {
|
|
47
|
+
return loadDrawing(file, opts);
|
|
48
|
+
}
|
|
49
|
+
export async function toSvg(file, opts) {
|
|
50
|
+
return renderSvg(await loadDrawing(file, opts));
|
|
51
|
+
}
|
|
52
|
+
export async function getThumbnail(file, opts = {}) {
|
|
53
|
+
const { bytes, format } = readCadFile(file);
|
|
54
|
+
const reader = readerFor(opts);
|
|
55
|
+
if (!reader.thumbnail)
|
|
56
|
+
throw new DwgCliError("Thumbnail extraction is not available through the configured reader.", "THUMBNAIL_UNAVAILABLE", EXIT_UNAVAILABLE);
|
|
57
|
+
const result = await reader.thumbnail(file, bytes, format);
|
|
58
|
+
if (!result)
|
|
59
|
+
throw new DwgCliError("No thumbnail was found in this drawing.", "THUMBNAIL_UNAVAILABLE", EXIT_UNAVAILABLE);
|
|
60
|
+
return result;
|
|
61
|
+
}
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
import type { DwgFormat } from "../types.js";
|
|
2
|
+
export declare function detectFormat(file: string): DwgFormat;
|
|
3
|
+
export declare function readCadFile(file: string): {
|
|
4
|
+
bytes: Uint8Array;
|
|
5
|
+
format: DwgFormat;
|
|
6
|
+
};
|
|
7
|
+
export declare function writeOutput(file: string, data: string | Uint8Array): void;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, readFileSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { dirname, extname } from "node:path";
|
|
3
|
+
import { EXIT_NOT_FOUND, EXIT_USER_ERROR } from "../utils/exit-codes.js";
|
|
4
|
+
import { DwgCliError } from "./errors.js";
|
|
5
|
+
export function detectFormat(file) {
|
|
6
|
+
const ext = extname(file).toLowerCase();
|
|
7
|
+
if (ext === ".dwg")
|
|
8
|
+
return "DWG";
|
|
9
|
+
if (ext === ".dxf")
|
|
10
|
+
return "DXF";
|
|
11
|
+
throw new DwgCliError(`Unsupported file extension: ${ext || "none"}. Expected .dwg or .dxf.`, "UNSUPPORTED_FORMAT", EXIT_USER_ERROR);
|
|
12
|
+
}
|
|
13
|
+
export function readCadFile(file) {
|
|
14
|
+
if (!existsSync(file)) {
|
|
15
|
+
throw new DwgCliError(`File not found: ${file}`, "FILE_NOT_FOUND", EXIT_NOT_FOUND);
|
|
16
|
+
}
|
|
17
|
+
return { bytes: readFileSync(file), format: detectFormat(file) };
|
|
18
|
+
}
|
|
19
|
+
export function writeOutput(file, data) {
|
|
20
|
+
try {
|
|
21
|
+
mkdirSync(dirname(file), { recursive: true });
|
|
22
|
+
writeFileSync(file, data);
|
|
23
|
+
}
|
|
24
|
+
catch (error) {
|
|
25
|
+
throw new DwgCliError(`Could not write output to ${file}: ${error.message}`, "OUTPUT_WRITE_FAILED", EXIT_USER_ERROR);
|
|
26
|
+
}
|
|
27
|
+
}
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
export type LibreDwgToolName = "dwgread" | "dwgfilter";
|
|
2
|
+
export interface LibreDwgEditResult {
|
|
3
|
+
input: string;
|
|
4
|
+
output: string;
|
|
5
|
+
expression: string;
|
|
6
|
+
backend: "LibreDWG";
|
|
7
|
+
tool: "dwgfilter";
|
|
8
|
+
stderr: string;
|
|
9
|
+
}
|
|
10
|
+
export interface LibreDwgViewResult {
|
|
11
|
+
input: string;
|
|
12
|
+
output?: string;
|
|
13
|
+
svg: string;
|
|
14
|
+
backend: "LibreDWG";
|
|
15
|
+
tool: "dwgread";
|
|
16
|
+
stderr: string;
|
|
17
|
+
}
|
|
18
|
+
export interface LibreDwgJsonResult {
|
|
19
|
+
input: string;
|
|
20
|
+
json: unknown;
|
|
21
|
+
backend: "LibreDWG";
|
|
22
|
+
tool: "dwgread";
|
|
23
|
+
stderr: string;
|
|
24
|
+
}
|
|
25
|
+
export declare function readJsonWithLibreDwg(file: string, opts?: {
|
|
26
|
+
toolDir?: string;
|
|
27
|
+
}): LibreDwgJsonResult;
|
|
28
|
+
export declare function renderSvgWithLibreDwg(file: string, opts?: {
|
|
29
|
+
toolDir?: string;
|
|
30
|
+
}): LibreDwgViewResult;
|
|
31
|
+
export declare function editWithLibreDwgFilter(opts: {
|
|
32
|
+
input: string;
|
|
33
|
+
output: string;
|
|
34
|
+
expression: string;
|
|
35
|
+
overwrite?: boolean;
|
|
36
|
+
toolDir?: string;
|
|
37
|
+
}): LibreDwgEditResult;
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
import { spawnSync } from "node:child_process";
|
|
2
|
+
import { copyFileSync, existsSync } from "node:fs";
|
|
3
|
+
import { basename, delimiter, join } from "node:path";
|
|
4
|
+
import { EXIT_ERROR, EXIT_UNAVAILABLE, EXIT_USER_ERROR, } from "../utils/exit-codes.js";
|
|
5
|
+
import { DwgCliError } from "./errors.js";
|
|
6
|
+
const WINDOWS_EXTENSIONS = [".exe", ".cmd", ".bat", ".com"];
|
|
7
|
+
function candidateNames(tool) {
|
|
8
|
+
return [tool, ...WINDOWS_EXTENSIONS.map((ext) => `${tool}${ext}`)];
|
|
9
|
+
}
|
|
10
|
+
function findTool(tool, toolDir) {
|
|
11
|
+
const dirs = toolDir ? [toolDir] : (process.env.PATH ?? "").split(delimiter);
|
|
12
|
+
for (const dir of dirs.filter(Boolean)) {
|
|
13
|
+
for (const name of candidateNames(tool)) {
|
|
14
|
+
const candidate = join(dir, name);
|
|
15
|
+
if (existsSync(candidate))
|
|
16
|
+
return candidate;
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
return undefined;
|
|
20
|
+
}
|
|
21
|
+
function requireTool(tool, toolDir) {
|
|
22
|
+
const path = findTool(tool, toolDir);
|
|
23
|
+
if (path)
|
|
24
|
+
return path;
|
|
25
|
+
throw new DwgCliError(`LibreDWG tool not found: ${tool}. Install LibreDWG and make sure ${tool} is on PATH.`, "LIBREDWG_TOOL_NOT_FOUND", EXIT_UNAVAILABLE);
|
|
26
|
+
}
|
|
27
|
+
function preview(value) {
|
|
28
|
+
const trimmed = value.trim();
|
|
29
|
+
if (trimmed.length <= 500)
|
|
30
|
+
return trimmed;
|
|
31
|
+
return `${trimmed.slice(0, 500)}…`;
|
|
32
|
+
}
|
|
33
|
+
function runLibreDwgTool(tool, args, toolDir) {
|
|
34
|
+
const path = requireTool(tool, toolDir);
|
|
35
|
+
const result = spawnSync(path, args, { encoding: "utf-8" });
|
|
36
|
+
if (result.error) {
|
|
37
|
+
throw new DwgCliError(`Could not run ${tool}: ${result.error.message}`, "LIBREDWG_RUN_FAILED", EXIT_ERROR);
|
|
38
|
+
}
|
|
39
|
+
if (result.status !== 0) {
|
|
40
|
+
const details = preview(result.stderr || result.stdout);
|
|
41
|
+
throw new DwgCliError(`${tool} failed for ${basename(args.at(-1) ?? "input")}${details ? `: ${details}` : ""}`, "LIBREDWG_RUN_FAILED", EXIT_USER_ERROR);
|
|
42
|
+
}
|
|
43
|
+
return { stdout: result.stdout, stderr: result.stderr };
|
|
44
|
+
}
|
|
45
|
+
export function readJsonWithLibreDwg(file, opts = {}) {
|
|
46
|
+
const result = runLibreDwgTool("dwgread", ["-O", "JSON", file], opts.toolDir);
|
|
47
|
+
try {
|
|
48
|
+
return {
|
|
49
|
+
input: file,
|
|
50
|
+
json: JSON.parse(result.stdout),
|
|
51
|
+
backend: "LibreDWG",
|
|
52
|
+
tool: "dwgread",
|
|
53
|
+
stderr: result.stderr,
|
|
54
|
+
};
|
|
55
|
+
}
|
|
56
|
+
catch (error) {
|
|
57
|
+
const details = preview(result.stdout);
|
|
58
|
+
throw new DwgCliError(`dwgread produced invalid JSON for ${basename(file)}: ${error.message}${details ? `; stdout: ${details}` : ""}`, "LIBREDWG_INVALID_JSON", EXIT_USER_ERROR);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
export function renderSvgWithLibreDwg(file, opts = {}) {
|
|
62
|
+
const result = runLibreDwgTool("dwgread", ["-O", "SVG", file], opts.toolDir);
|
|
63
|
+
return {
|
|
64
|
+
input: file,
|
|
65
|
+
svg: result.stdout,
|
|
66
|
+
backend: "LibreDWG",
|
|
67
|
+
tool: "dwgread",
|
|
68
|
+
stderr: result.stderr,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
export function editWithLibreDwgFilter(opts) {
|
|
72
|
+
if (opts.input === opts.output && !opts.overwrite) {
|
|
73
|
+
throw new DwgCliError("Refusing to edit in place without --overwrite. Provide --output or pass --overwrite.", "EDIT_REQUIRES_OUTPUT_OR_OVERWRITE", EXIT_USER_ERROR);
|
|
74
|
+
}
|
|
75
|
+
if (opts.input !== opts.output)
|
|
76
|
+
copyFileSync(opts.input, opts.output);
|
|
77
|
+
const result = runLibreDwgTool("dwgfilter", ["-i", opts.expression, opts.output], opts.toolDir);
|
|
78
|
+
return {
|
|
79
|
+
input: opts.input,
|
|
80
|
+
output: opts.output,
|
|
81
|
+
expression: opts.expression,
|
|
82
|
+
backend: "LibreDWG",
|
|
83
|
+
tool: "dwgfilter",
|
|
84
|
+
stderr: result.stderr,
|
|
85
|
+
};
|
|
86
|
+
}
|
|
@@ -0,0 +1,156 @@
|
|
|
1
|
+
import { basename } from "node:path";
|
|
2
|
+
const ENTITY_ARRAY_FIELDS = ["entities", "Entities", "objects", "Objects"];
|
|
3
|
+
const LAYER_ARRAY_FIELDS = ["layers", "Layers"];
|
|
4
|
+
const BLOCK_ARRAY_FIELDS = ["blocks", "Blocks", "blockHeaders"];
|
|
5
|
+
const POINT_FIELDS = [
|
|
6
|
+
"start",
|
|
7
|
+
"end",
|
|
8
|
+
"center",
|
|
9
|
+
"insertionPoint",
|
|
10
|
+
"position",
|
|
11
|
+
"point",
|
|
12
|
+
];
|
|
13
|
+
const POINT_LIST_FIELDS = ["vertices", "points"];
|
|
14
|
+
const SUPPORTED_ENTITY_TYPES = new Set([
|
|
15
|
+
"LINE",
|
|
16
|
+
"CIRCLE",
|
|
17
|
+
"ARC",
|
|
18
|
+
"LWPOLYLINE",
|
|
19
|
+
"POLYLINE",
|
|
20
|
+
"TEXT",
|
|
21
|
+
"MTEXT",
|
|
22
|
+
"POINT",
|
|
23
|
+
]);
|
|
24
|
+
function asRecord(value) {
|
|
25
|
+
return value && typeof value === "object"
|
|
26
|
+
? value
|
|
27
|
+
: {};
|
|
28
|
+
}
|
|
29
|
+
function asArray(value) {
|
|
30
|
+
return Array.isArray(value) ? value : [];
|
|
31
|
+
}
|
|
32
|
+
function firstArray(raw, names) {
|
|
33
|
+
for (const name of names) {
|
|
34
|
+
const value = raw[name];
|
|
35
|
+
if (Array.isArray(value))
|
|
36
|
+
return value;
|
|
37
|
+
}
|
|
38
|
+
return [];
|
|
39
|
+
}
|
|
40
|
+
function stringField(rec, names) {
|
|
41
|
+
for (const name of names) {
|
|
42
|
+
const value = rec[name];
|
|
43
|
+
if (value !== undefined && value !== null && String(value) !== "") {
|
|
44
|
+
return String(value);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
return undefined;
|
|
48
|
+
}
|
|
49
|
+
function getName(value, fallback) {
|
|
50
|
+
return (stringField(asRecord(value), ["name", "Name", "layerName", "LayerName"]) ??
|
|
51
|
+
fallback);
|
|
52
|
+
}
|
|
53
|
+
function getEntityType(value) {
|
|
54
|
+
return (stringField(asRecord(value), [
|
|
55
|
+
"type",
|
|
56
|
+
"Type",
|
|
57
|
+
"objectType",
|
|
58
|
+
"entityType",
|
|
59
|
+
"_type",
|
|
60
|
+
]) ?? "UNKNOWN").toUpperCase();
|
|
61
|
+
}
|
|
62
|
+
function getLayer(value) {
|
|
63
|
+
return stringField(asRecord(value), [
|
|
64
|
+
"layer",
|
|
65
|
+
"Layer",
|
|
66
|
+
"layerName",
|
|
67
|
+
"layer_name",
|
|
68
|
+
]);
|
|
69
|
+
}
|
|
70
|
+
function pointFrom(value) {
|
|
71
|
+
const rec = asRecord(value);
|
|
72
|
+
const x = Number(rec.x ?? rec.X ?? rec[0]);
|
|
73
|
+
const y = Number(rec.y ?? rec.Y ?? rec[1]);
|
|
74
|
+
return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
|
|
75
|
+
}
|
|
76
|
+
function collectEntityPoints(entity) {
|
|
77
|
+
const points = [];
|
|
78
|
+
for (const key of POINT_FIELDS) {
|
|
79
|
+
const point = pointFrom(entity.data[key]);
|
|
80
|
+
if (point)
|
|
81
|
+
points.push(point);
|
|
82
|
+
}
|
|
83
|
+
for (const key of POINT_LIST_FIELDS) {
|
|
84
|
+
for (const item of asArray(entity.data[key])) {
|
|
85
|
+
const point = pointFrom(item);
|
|
86
|
+
if (point)
|
|
87
|
+
points.push(point);
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
return points;
|
|
91
|
+
}
|
|
92
|
+
function normalizeEntity(item, index) {
|
|
93
|
+
const rec = asRecord(item);
|
|
94
|
+
return {
|
|
95
|
+
id: String(rec.id ?? rec.handle ?? rec.Handle ?? index + 1),
|
|
96
|
+
type: getEntityType(rec),
|
|
97
|
+
layer: getLayer(rec),
|
|
98
|
+
color: rec.color,
|
|
99
|
+
data: rec,
|
|
100
|
+
};
|
|
101
|
+
}
|
|
102
|
+
function normalizeLayers(rawLayers, entities) {
|
|
103
|
+
const counts = new Map();
|
|
104
|
+
for (const [index, layer] of rawLayers.entries()) {
|
|
105
|
+
counts.set(getName(layer, `Layer ${index + 1}`), 0);
|
|
106
|
+
}
|
|
107
|
+
for (const entity of entities) {
|
|
108
|
+
if (!entity.layer)
|
|
109
|
+
continue;
|
|
110
|
+
counts.set(entity.layer, (counts.get(entity.layer) ?? 0) + 1);
|
|
111
|
+
}
|
|
112
|
+
return [...counts.entries()]
|
|
113
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
114
|
+
.map(([name, entityCount]) => ({ name, entityCount }));
|
|
115
|
+
}
|
|
116
|
+
function normalizeBlocks(rawBlocks) {
|
|
117
|
+
return rawBlocks.map((block, index) => {
|
|
118
|
+
const rec = asRecord(block);
|
|
119
|
+
return {
|
|
120
|
+
name: getName(block, `Block ${index + 1}`),
|
|
121
|
+
entityCount: asArray(rec.entities ?? rec.Entities).length,
|
|
122
|
+
};
|
|
123
|
+
});
|
|
124
|
+
}
|
|
125
|
+
export function computeBounds(entities) {
|
|
126
|
+
const points = entities.flatMap(collectEntityPoints);
|
|
127
|
+
if (points.length === 0)
|
|
128
|
+
return undefined;
|
|
129
|
+
return {
|
|
130
|
+
minX: Math.min(...points.map((p) => p.x)),
|
|
131
|
+
minY: Math.min(...points.map((p) => p.y)),
|
|
132
|
+
maxX: Math.max(...points.map((p) => p.x)),
|
|
133
|
+
maxY: Math.max(...points.map((p) => p.y)),
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
export function normalizeDocument(file, format, raw) {
|
|
137
|
+
const root = asRecord(raw);
|
|
138
|
+
const entities = firstArray(root, ENTITY_ARRAY_FIELDS).map(normalizeEntity);
|
|
139
|
+
const unsupported = entities.filter((entity) => !SUPPORTED_ENTITY_TYPES.has(entity.type));
|
|
140
|
+
const layers = normalizeLayers(firstArray(root, LAYER_ARRAY_FIELDS), entities);
|
|
141
|
+
const blocks = normalizeBlocks(firstArray(root, BLOCK_ARRAY_FIELDS));
|
|
142
|
+
const version = stringField(root, ["version", "headerVersion", "dwgVersion"]);
|
|
143
|
+
const summary = {
|
|
144
|
+
file: basename(file),
|
|
145
|
+
format,
|
|
146
|
+
version,
|
|
147
|
+
counts: {
|
|
148
|
+
entities: entities.length,
|
|
149
|
+
layers: layers.length,
|
|
150
|
+
blocks: blocks.length,
|
|
151
|
+
unsupported: unsupported.length,
|
|
152
|
+
},
|
|
153
|
+
bounds: computeBounds(entities),
|
|
154
|
+
};
|
|
155
|
+
return { summary, layers, blocks, entities, unsupported, raw };
|
|
156
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { DwgDocument, DwgSummary } from "../types.js";
|
|
2
|
+
import { type LoadOptions } from "./drawing.js";
|
|
3
|
+
export interface OverviewLayer {
|
|
4
|
+
name: string;
|
|
5
|
+
entities: number;
|
|
6
|
+
types: string[];
|
|
7
|
+
keywords: string[];
|
|
8
|
+
}
|
|
9
|
+
export interface OverviewEntityType {
|
|
10
|
+
type: string;
|
|
11
|
+
count: number;
|
|
12
|
+
}
|
|
13
|
+
export interface OverviewBlock {
|
|
14
|
+
name: string;
|
|
15
|
+
definitions: number;
|
|
16
|
+
references: number;
|
|
17
|
+
}
|
|
18
|
+
export interface OverviewText {
|
|
19
|
+
keywords: string[];
|
|
20
|
+
samples: string[];
|
|
21
|
+
}
|
|
22
|
+
export interface DrawingOverview {
|
|
23
|
+
summary: DwgSummary;
|
|
24
|
+
layers: OverviewLayer[];
|
|
25
|
+
entityTypes: OverviewEntityType[];
|
|
26
|
+
blocks: OverviewBlock[];
|
|
27
|
+
text: OverviewText;
|
|
28
|
+
searchHints: string[];
|
|
29
|
+
}
|
|
30
|
+
export interface DrawingOverviewOptions {
|
|
31
|
+
keywords?: number;
|
|
32
|
+
samples?: number;
|
|
33
|
+
}
|
|
34
|
+
export declare function createDrawingOverview(doc: DwgDocument, opts?: DrawingOverviewOptions): DrawingOverview;
|
|
35
|
+
export declare function getOverview(file: string, opts?: DrawingOverviewOptions, loadOpts?: LoadOptions): Promise<DrawingOverview>;
|