@miclivs/cadcli 0.1.1 → 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.
- package/README.md +23 -14
- package/dist/commands/edit.d.ts +42 -2
- package/dist/commands/edit.js +281 -7
- package/dist/commands/json.js +0 -1
- package/dist/commands/shared.d.ts +0 -1
- package/dist/commands/shared.js +0 -1
- package/dist/commands/svg.js +0 -1
- package/dist/commands/thumbnail.js +0 -1
- package/dist/commands/view.d.ts +0 -1
- package/dist/commands/view.js +5 -7
- package/dist/core/acad-edit.d.ts +142 -0
- package/dist/core/acad-edit.js +234 -0
- package/dist/core/acad-view.d.ts +6 -0
- package/dist/core/acad-view.js +22 -0
- package/dist/core/acad.d.ts +11 -0
- package/dist/core/acad.js +82 -0
- package/dist/core/adapter.d.ts +0 -6
- package/dist/core/adapter.js +0 -13
- package/dist/core/drawing.d.ts +0 -1
- package/dist/core/search.d.ts +3 -0
- package/dist/core/search.js +58 -68
- package/dist/core/sqlite-search.d.ts +3 -0
- package/dist/core/sqlite-search.js +218 -0
- package/dist/index.d.ts +2 -1
- package/dist/main.js +82 -7
- package/dist/sdk.d.ts +5 -7
- package/dist/sdk.js +6 -6
- package/package.json +6 -3
- package/scripts/apply-acad-ts-patch.mjs +41 -0
- package/skills/cadcli/SKILL.md +21 -4
- package/dist/core/libredwg.d.ts +0 -37
- package/dist/core/libredwg.js +0 -86
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { existsSync, mkdirSync, rmSync } from "node:fs";
|
|
2
|
+
import { createRequire } from "node:module";
|
|
3
|
+
import { basename, dirname, join } from "node:path";
|
|
4
|
+
import { getCacheDir } from "../store.js";
|
|
5
|
+
import { loadDrawing } from "./drawing.js";
|
|
6
|
+
import { entitySearchFields, searchMatchesFor, } from "./search.js";
|
|
7
|
+
import { computeDrawingFingerprint } from "./search-cache.js";
|
|
8
|
+
const require = createRequire(import.meta.url);
|
|
9
|
+
const SEARCH_SCHEMA_VERSION = 1;
|
|
10
|
+
function sqliteSearchDatabasePath(file, cacheDir) {
|
|
11
|
+
const root = cacheDir ?? getCacheDir();
|
|
12
|
+
const key = computeDrawingFingerprint(file);
|
|
13
|
+
return join(root, "search", `${basename(file)}-${key}.search.sqlite`);
|
|
14
|
+
}
|
|
15
|
+
async function openSqliteDatabase(path) {
|
|
16
|
+
try {
|
|
17
|
+
const Database = require("better-sqlite3");
|
|
18
|
+
return new Database(path);
|
|
19
|
+
}
|
|
20
|
+
catch {
|
|
21
|
+
return null;
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
function ensureSqliteSchema(db) {
|
|
25
|
+
db.exec(`
|
|
26
|
+
PRAGMA journal_mode = WAL;
|
|
27
|
+
CREATE TABLE IF NOT EXISTS meta (
|
|
28
|
+
key TEXT PRIMARY KEY,
|
|
29
|
+
value TEXT NOT NULL
|
|
30
|
+
);
|
|
31
|
+
CREATE TABLE IF NOT EXISTS entities (
|
|
32
|
+
id INTEGER PRIMARY KEY,
|
|
33
|
+
entity_id TEXT NOT NULL,
|
|
34
|
+
type TEXT NOT NULL,
|
|
35
|
+
layer TEXT,
|
|
36
|
+
text TEXT NOT NULL,
|
|
37
|
+
entity_json TEXT NOT NULL
|
|
38
|
+
);
|
|
39
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS entities_fts USING fts5(
|
|
40
|
+
entity_id,
|
|
41
|
+
type,
|
|
42
|
+
layer,
|
|
43
|
+
text
|
|
44
|
+
);
|
|
45
|
+
`);
|
|
46
|
+
}
|
|
47
|
+
function sqliteCacheIsValid(db, fingerprint) {
|
|
48
|
+
const row = db
|
|
49
|
+
.prepare("SELECT value FROM meta WHERE key = ?")
|
|
50
|
+
.get("fingerprint");
|
|
51
|
+
const version = db
|
|
52
|
+
.prepare("SELECT value FROM meta WHERE key = ?")
|
|
53
|
+
.get("schema_version");
|
|
54
|
+
return (row?.value === fingerprint &&
|
|
55
|
+
version?.value === String(SEARCH_SCHEMA_VERSION));
|
|
56
|
+
}
|
|
57
|
+
function resetSqliteDatabase(path) {
|
|
58
|
+
for (const suffix of ["", "-wal", "-shm"]) {
|
|
59
|
+
const target = `${path}${suffix}`;
|
|
60
|
+
if (existsSync(target))
|
|
61
|
+
rmSync(target, { force: true });
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function indexedEntities(doc) {
|
|
65
|
+
return doc.entities.map((entity, id) => ({
|
|
66
|
+
id,
|
|
67
|
+
entityId: entity.id,
|
|
68
|
+
type: entity.type,
|
|
69
|
+
layer: entity.layer,
|
|
70
|
+
text: entitySearchFields(entity).join(" "),
|
|
71
|
+
entity,
|
|
72
|
+
}));
|
|
73
|
+
}
|
|
74
|
+
function writeSqliteSearchDatabase(db, fingerprint, doc) {
|
|
75
|
+
ensureSqliteSchema(db);
|
|
76
|
+
const insertMeta = db.prepare("INSERT INTO meta (key, value) VALUES (?, ?) ON CONFLICT(key) DO UPDATE SET value = excluded.value");
|
|
77
|
+
const insertEntity = db.prepare(`
|
|
78
|
+
INSERT INTO entities (id, entity_id, type, layer, text, entity_json)
|
|
79
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
80
|
+
`);
|
|
81
|
+
const insertFts = db.prepare(`
|
|
82
|
+
INSERT INTO entities_fts (rowid, entity_id, type, layer, text)
|
|
83
|
+
VALUES (?, ?, ?, ?, ?)
|
|
84
|
+
`);
|
|
85
|
+
db.exec("BEGIN");
|
|
86
|
+
try {
|
|
87
|
+
insertMeta.run("schema_version", String(SEARCH_SCHEMA_VERSION));
|
|
88
|
+
insertMeta.run("fingerprint", fingerprint);
|
|
89
|
+
for (const entity of indexedEntities(doc)) {
|
|
90
|
+
insertEntity.run(entity.id, entity.entityId, entity.type, entity.layer ?? null, entity.text, JSON.stringify(entity.entity));
|
|
91
|
+
insertFts.run(entity.id, entity.entityId, entity.type, entity.layer ?? "", entity.text);
|
|
92
|
+
}
|
|
93
|
+
db.exec("COMMIT");
|
|
94
|
+
}
|
|
95
|
+
catch (error) {
|
|
96
|
+
db.exec("ROLLBACK");
|
|
97
|
+
db.close();
|
|
98
|
+
throw error;
|
|
99
|
+
}
|
|
100
|
+
return db;
|
|
101
|
+
}
|
|
102
|
+
async function openSqliteSearchDatabase(file, opts, loadOpts) {
|
|
103
|
+
const fingerprint = computeDrawingFingerprint(file);
|
|
104
|
+
const path = sqliteSearchDatabasePath(file, opts.cacheDir);
|
|
105
|
+
if (existsSync(path)) {
|
|
106
|
+
try {
|
|
107
|
+
const db = await openSqliteDatabase(path);
|
|
108
|
+
if (!db)
|
|
109
|
+
return null;
|
|
110
|
+
ensureSqliteSchema(db);
|
|
111
|
+
if (sqliteCacheIsValid(db, fingerprint))
|
|
112
|
+
return db;
|
|
113
|
+
db.close();
|
|
114
|
+
}
|
|
115
|
+
catch {
|
|
116
|
+
resetSqliteDatabase(path);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
resetSqliteDatabase(path);
|
|
120
|
+
mkdirSync(dirname(path), { recursive: true });
|
|
121
|
+
const db = await openSqliteDatabase(path);
|
|
122
|
+
if (!db)
|
|
123
|
+
return null;
|
|
124
|
+
return writeSqliteSearchDatabase(db, fingerprint, await loadDrawing(file, loadOpts));
|
|
125
|
+
}
|
|
126
|
+
function matchesFilterSql(opts) {
|
|
127
|
+
const clauses = [];
|
|
128
|
+
if (opts.type)
|
|
129
|
+
clauses.push("lower(e.type) = lower(:type)");
|
|
130
|
+
if (opts.layer)
|
|
131
|
+
clauses.push("lower(e.layer) = lower(:layer)");
|
|
132
|
+
return clauses.length > 0 ? ` AND ${clauses.join(" AND ")}` : "";
|
|
133
|
+
}
|
|
134
|
+
function ftsQuery(query) {
|
|
135
|
+
return query
|
|
136
|
+
.split(/\s+/)
|
|
137
|
+
.map((term) => term.trim())
|
|
138
|
+
.filter(Boolean)
|
|
139
|
+
.map((term) => `"${term.replaceAll('"', '""')}"`)
|
|
140
|
+
.join(" OR ");
|
|
141
|
+
}
|
|
142
|
+
function parseEntity(json) {
|
|
143
|
+
try {
|
|
144
|
+
const value = JSON.parse(json);
|
|
145
|
+
if (!value ||
|
|
146
|
+
typeof value.id !== "string" ||
|
|
147
|
+
typeof value.type !== "string") {
|
|
148
|
+
return null;
|
|
149
|
+
}
|
|
150
|
+
return value;
|
|
151
|
+
}
|
|
152
|
+
catch {
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
function resultScore(score) {
|
|
157
|
+
return Math.round((Number(score) || 1) * 10) / 10;
|
|
158
|
+
}
|
|
159
|
+
function rowToResult(row, query) {
|
|
160
|
+
const entity = parseEntity(row.entity_json);
|
|
161
|
+
if (!entity)
|
|
162
|
+
return null;
|
|
163
|
+
return {
|
|
164
|
+
entityId: row.entity_id,
|
|
165
|
+
type: row.type,
|
|
166
|
+
layer: row.layer ?? undefined,
|
|
167
|
+
score: resultScore(row.score),
|
|
168
|
+
matches: searchMatchesFor(entity, query),
|
|
169
|
+
entity,
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
export async function searchWithSqlite(file, opts, loadOpts) {
|
|
173
|
+
const db = await openSqliteSearchDatabase(file, opts, loadOpts);
|
|
174
|
+
if (!db)
|
|
175
|
+
return null;
|
|
176
|
+
const query = opts.query?.trim();
|
|
177
|
+
const limit = opts.limit ?? 30;
|
|
178
|
+
const filter = matchesFilterSql(opts);
|
|
179
|
+
const params = {
|
|
180
|
+
query: query ? ftsQuery(query) : "",
|
|
181
|
+
limit,
|
|
182
|
+
};
|
|
183
|
+
if (opts.type)
|
|
184
|
+
params.type = opts.type;
|
|
185
|
+
if (opts.layer)
|
|
186
|
+
params.layer = opts.layer;
|
|
187
|
+
const rows = query
|
|
188
|
+
? db
|
|
189
|
+
.prepare(`
|
|
190
|
+
SELECT e.id, e.entity_id, e.type, e.layer, e.text, e.entity_json,
|
|
191
|
+
-bm25(entities_fts, 3.0, 2.0, 2.0, 1.0) AS score
|
|
192
|
+
FROM entities_fts
|
|
193
|
+
JOIN entities e ON e.id = entities_fts.rowid
|
|
194
|
+
WHERE entities_fts MATCH :query${filter}
|
|
195
|
+
ORDER BY bm25(entities_fts, 3.0, 2.0, 2.0, 1.0)
|
|
196
|
+
LIMIT :limit
|
|
197
|
+
`)
|
|
198
|
+
.all(params)
|
|
199
|
+
: db
|
|
200
|
+
.prepare(`
|
|
201
|
+
SELECT e.id, e.entity_id, e.type, e.layer, e.text, e.entity_json,
|
|
202
|
+
1 AS score
|
|
203
|
+
FROM entities e
|
|
204
|
+
WHERE 1 = 1${filter}
|
|
205
|
+
ORDER BY e.id
|
|
206
|
+
LIMIT :limit
|
|
207
|
+
`)
|
|
208
|
+
.all(params);
|
|
209
|
+
db.close();
|
|
210
|
+
return rows
|
|
211
|
+
.map((row) => {
|
|
212
|
+
const result = rowToResult(row, query);
|
|
213
|
+
return result && opts.snippets === false
|
|
214
|
+
? { ...result, matches: [] }
|
|
215
|
+
: result;
|
|
216
|
+
})
|
|
217
|
+
.filter((result) => Boolean(result));
|
|
218
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
export type {
|
|
1
|
+
export type { AcadColor, AcadEditOperation, AcadEditResult, AcadPoint, } from "./core/acad-edit.js";
|
|
2
|
+
export type { AcadSvgViewResult } from "./core/acad-view.js";
|
|
2
3
|
export type { DrawingOverview, DrawingOverviewOptions, OverviewBlock, OverviewEntityType, OverviewLayer, OverviewText, } from "./core/overview.js";
|
|
3
4
|
export type { DwgSearchOptions, DwgSearchResult } from "./core/search.js";
|
|
4
5
|
export { Dwg } from "./sdk.js";
|
package/dist/main.js
CHANGED
|
@@ -28,7 +28,8 @@ Examples:
|
|
|
28
28
|
$ cadcli entities drawing.dwg --type LINE --limit 20
|
|
29
29
|
$ cadcli search drawing.dwg "conference" --layer A-TEXT
|
|
30
30
|
$ cadcli view drawing.dwg -o drawing.svg
|
|
31
|
-
$ cadcli edit drawing.dwg --
|
|
31
|
+
$ cadcli edit drawing.dwg --set-text "Office" --text-id 2A -o edited.dwg
|
|
32
|
+
$ cadcli edit drawing.dwg --add-line 0,0:10,0 --new-layer A-WALL -o edited.dwg
|
|
32
33
|
|
|
33
34
|
Workflow: info → overview → search/entities → view/edit
|
|
34
35
|
|
|
@@ -41,8 +42,8 @@ Inspecting:
|
|
|
41
42
|
search <file> Search entities by text, type, layer, and raw fields
|
|
42
43
|
|
|
43
44
|
Viewing and editing:
|
|
44
|
-
view <file> Render
|
|
45
|
-
edit <file> Edit DWG/DXF with
|
|
45
|
+
view <file> Render an SVG preview
|
|
46
|
+
edit <file> Edit DWG/DXF with acad-ts operations
|
|
46
47
|
|
|
47
48
|
Conversion:
|
|
48
49
|
json <file> Export normalized JSON
|
|
@@ -120,15 +121,89 @@ program
|
|
|
120
121
|
});
|
|
121
122
|
program
|
|
122
123
|
.command("view <file>")
|
|
123
|
-
.description("Render
|
|
124
|
+
.description("Render an SVG preview")
|
|
124
125
|
.option("-o, --output <path>", "Output SVG file")
|
|
125
126
|
.action(async (file, opts, cmd) => view(file, { ...cmd.optsWithGlobals(), ...opts }));
|
|
126
127
|
program
|
|
127
128
|
.command("edit <file>")
|
|
128
|
-
.description("Edit DWG/DXF with
|
|
129
|
-
.
|
|
130
|
-
.option("-
|
|
129
|
+
.description("Edit DWG/DXF with acad-ts operations")
|
|
130
|
+
.option("--set-text <text>", "Set TEXT/MTEXT content")
|
|
131
|
+
.option("--text-id <id>", "Entity id/handle for --set-text")
|
|
132
|
+
.option("--set-layer <name>", "Move an entity to a layer")
|
|
133
|
+
.option("--layer-id <id>", "Entity id/handle for --set-layer")
|
|
134
|
+
.option("--set-color <color>", "Set entity color: bylayer, byblock, index, #rrggbb, or r,g,b")
|
|
135
|
+
.option("--color-id <id>", "Entity id/handle for --set-color")
|
|
136
|
+
.option("--set-linetype <name>", "Set entity line type")
|
|
137
|
+
.option("--linetype-id <id>", "Entity id/handle for --set-linetype")
|
|
138
|
+
.option("--set-lineweight <n>", "Set entity line weight enum value")
|
|
139
|
+
.option("--lineweight-id <id>", "Entity id/handle for --set-lineweight")
|
|
140
|
+
.option("--set-transparency <alpha>", "Set transparency alpha value")
|
|
141
|
+
.option("--transparency-id <id>", "Entity id/handle for --set-transparency")
|
|
142
|
+
.option("--hide <id>", "Hide an entity")
|
|
143
|
+
.option("--show <id>", "Show an entity")
|
|
144
|
+
.option("--move <id>", "Move an entity by --dx/--dy/--dz")
|
|
145
|
+
.option("--rotate <id>", "Rotate an entity around Z by --angle degrees")
|
|
146
|
+
.option("--scale <id>", "Scale an entity by --factor")
|
|
147
|
+
.option("--copy <id>", "Copy an entity, optionally offset by --dx/--dy/--dz")
|
|
148
|
+
.option("--match-properties <id>", "Copy visual properties from --source-id")
|
|
149
|
+
.option("--source-id <id>", "Source entity id for --match-properties")
|
|
150
|
+
.option("--set-attr <tag=value>", "Set a block insert attribute")
|
|
151
|
+
.option("--insert-id <id>", "Block insert id for --set-attr")
|
|
152
|
+
.option("--dx <n>", "X delta")
|
|
153
|
+
.option("--dy <n>", "Y delta")
|
|
154
|
+
.option("--dz <n>", "Z delta")
|
|
155
|
+
.option("--angle <degrees>", "Angle in degrees")
|
|
156
|
+
.option("--origin <x,y,z>", "Origin for rotate/scale")
|
|
157
|
+
.option("--factor <n>", "Scale factor")
|
|
158
|
+
.option("--add-point <x,y,z>", "Add a POINT")
|
|
159
|
+
.option("--add-line <from:to>", "Add a LINE, e.g. 0,0:10,0")
|
|
160
|
+
.option("--add-circle <center:radius>", "Add a CIRCLE, e.g. 5,5:2")
|
|
161
|
+
.option("--add-arc <center:radius:start:end>", "Add an ARC with degree angles")
|
|
162
|
+
.option("--add-text <text>", "Add TEXT at --at")
|
|
163
|
+
.option("--add-mtext <text>", "Add MTEXT at --at")
|
|
164
|
+
.option("--points <p1;p2[;p3]>", "Add an LWPOLYLINE from x,y points")
|
|
165
|
+
.option("--closed", "Close a polyline added with --points")
|
|
166
|
+
.option("--at <x,y,z>", "Insertion point for text")
|
|
167
|
+
.option("--height <n>", "Text height")
|
|
168
|
+
.option("--width <n>", "MText width")
|
|
169
|
+
.option("--new-layer <name>", "Layer for add-* entities")
|
|
170
|
+
.option("--new-color <color>", "Color for add-* entities")
|
|
171
|
+
.option("--delete <id>", "Delete an entity")
|
|
172
|
+
.option("-o, --output <path>", "Write edited copy to this file; required unless --overwrite")
|
|
131
173
|
.option("--overwrite", "Allow editing the input file in place")
|
|
174
|
+
.addHelpText("after", `
|
|
175
|
+
|
|
176
|
+
Examples:
|
|
177
|
+
Find ids first:
|
|
178
|
+
$ cadcli entities drawing.dwg --type TEXT --json
|
|
179
|
+
$ cadcli search drawing.dwg "office" --json
|
|
180
|
+
|
|
181
|
+
Update existing entities:
|
|
182
|
+
$ cadcli edit drawing.dwg --set-text "Office" --text-id 2A -o edited.dwg
|
|
183
|
+
$ cadcli edit drawing.dwg --set-color '#ff0000' --color-id 2A -o edited.dwg
|
|
184
|
+
$ cadcli edit drawing.dwg --move 2A --dx 10 --dy 0 -o edited.dwg
|
|
185
|
+
$ cadcli edit drawing.dwg --rotate 2A --angle 90 --origin 0,0 -o edited.dwg
|
|
186
|
+
$ cadcli edit drawing.dwg --copy 2A --dx 10 --dy 0 -o edited.dwg
|
|
187
|
+
$ cadcli edit drawing.dwg --delete 2A -o edited.dwg
|
|
188
|
+
|
|
189
|
+
Add new entities:
|
|
190
|
+
$ cadcli edit drawing.dwg --add-line 0,0:10,0 --new-layer A-WALL -o edited.dwg
|
|
191
|
+
$ cadcli edit drawing.dwg --add-circle 5,5:2 -o edited.dwg
|
|
192
|
+
$ cadcli edit drawing.dwg --add-text "Label" --at 5,5 --height 2.5 -o edited.dwg
|
|
193
|
+
$ cadcli edit drawing.dwg --points '0,0;10,0;10,5' --closed -o edited.dwg
|
|
194
|
+
|
|
195
|
+
Formats:
|
|
196
|
+
points: x,y or x,y,z
|
|
197
|
+
line: from:to, e.g. 0,0:10,0
|
|
198
|
+
circle: center:radius, e.g. 5,5:2
|
|
199
|
+
arc: center:radius:startAngle:endAngle, e.g. 5,5:2:0:90
|
|
200
|
+
color: bylayer, byblock, AutoCAD color index, #rrggbb, or r,g,b
|
|
201
|
+
|
|
202
|
+
Safety:
|
|
203
|
+
cadcli refuses to modify the input file by default.
|
|
204
|
+
Use -o/--output to write an edited copy.
|
|
205
|
+
Use --overwrite only when you intentionally want to edit the input file in place.
|
|
206
|
+
`)
|
|
132
207
|
.action(async (file, opts, cmd) => edit(file, { ...cmd.optsWithGlobals(), ...opts }));
|
|
133
208
|
program
|
|
134
209
|
.command("json <file>")
|
package/dist/sdk.d.ts
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { type AcadEditOperation, type AcadEditResult } from "./core/acad-edit.js";
|
|
2
|
+
import { type AcadSvgViewResult } from "./core/acad-view.js";
|
|
1
3
|
import { type LoadOptions } from "./core/drawing.js";
|
|
2
|
-
import { type LibreDwgEditResult, type LibreDwgViewResult } from "./core/libredwg.js";
|
|
3
4
|
import { type DrawingOverview, type DrawingOverviewOptions } from "./core/overview.js";
|
|
4
5
|
import { type DwgSearchOptions, type DwgSearchResult } from "./core/search.js";
|
|
5
6
|
import type { DwgBlock, DwgDocument, DwgEntity, DwgLayer, DwgSummary, EntityFilter, SvgResult, ThumbnailResult } from "./types.js";
|
|
@@ -16,15 +17,12 @@ export declare class Dwg {
|
|
|
16
17
|
overview(opts?: DrawingOverviewOptions): Promise<DrawingOverview>;
|
|
17
18
|
json(): Promise<DwgDocument>;
|
|
18
19
|
svg(): Promise<SvgResult>;
|
|
19
|
-
view(
|
|
20
|
-
toolDir?: string;
|
|
21
|
-
}): LibreDwgViewResult;
|
|
20
|
+
view(): AcadSvgViewResult;
|
|
22
21
|
edit(opts: {
|
|
23
22
|
output: string;
|
|
24
|
-
|
|
23
|
+
operations: AcadEditOperation[];
|
|
25
24
|
overwrite?: boolean;
|
|
26
|
-
|
|
27
|
-
}): LibreDwgEditResult;
|
|
25
|
+
}): AcadEditResult;
|
|
28
26
|
thumbnail(): Promise<ThumbnailResult>;
|
|
29
27
|
static open(file: string, opts?: LoadOptions): Dwg;
|
|
30
28
|
}
|
package/dist/sdk.js
CHANGED
|
@@ -1,5 +1,6 @@
|
|
|
1
|
+
import { editWithAcadTs, } from "./core/acad-edit.js";
|
|
2
|
+
import { renderSvgWithAcadTs, } from "./core/acad-view.js";
|
|
1
3
|
import { getBlocks, getEntities, getInfo, getLayers, getThumbnail, loadDrawing, toJson, toSvg, } from "./core/drawing.js";
|
|
2
|
-
import { editWithLibreDwgFilter, renderSvgWithLibreDwg, } from "./core/libredwg.js";
|
|
3
4
|
import { getOverview, } from "./core/overview.js";
|
|
4
5
|
import { searchDrawing, } from "./core/search.js";
|
|
5
6
|
export class Dwg {
|
|
@@ -36,16 +37,15 @@ export class Dwg {
|
|
|
36
37
|
svg() {
|
|
37
38
|
return toSvg(this.file, this.opts);
|
|
38
39
|
}
|
|
39
|
-
view(
|
|
40
|
-
return
|
|
40
|
+
view() {
|
|
41
|
+
return renderSvgWithAcadTs(this.file);
|
|
41
42
|
}
|
|
42
43
|
edit(opts) {
|
|
43
|
-
return
|
|
44
|
+
return editWithAcadTs({
|
|
44
45
|
input: this.file,
|
|
45
46
|
output: opts.output,
|
|
46
|
-
|
|
47
|
+
operations: opts.operations,
|
|
47
48
|
overwrite: opts.overwrite,
|
|
48
|
-
toolDir: opts.toolDir,
|
|
49
49
|
});
|
|
50
50
|
}
|
|
51
51
|
thumbnail() {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miclivs/cadcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.2.0",
|
|
4
4
|
"description": "Agent-friendly CAD inspection, search, viewing, and editing for DWG/DXF files.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "dist/index.js",
|
|
@@ -17,11 +17,13 @@
|
|
|
17
17
|
"files": [
|
|
18
18
|
"dist",
|
|
19
19
|
"patches",
|
|
20
|
+
"scripts",
|
|
20
21
|
"skills"
|
|
21
22
|
],
|
|
22
23
|
"scripts": {
|
|
23
24
|
"build": "tsc",
|
|
24
25
|
"prepublishOnly": "npm run build",
|
|
26
|
+
"postinstall": "node scripts/apply-acad-ts-patch.mjs",
|
|
25
27
|
"ci": "bun test --coverage && bun run build && bunx biome check src/",
|
|
26
28
|
"dev": "bun run src/main.ts",
|
|
27
29
|
"build:bun": "bun build --compile src/main.ts --outfile cadcli",
|
|
@@ -47,7 +49,6 @@
|
|
|
47
49
|
"cad",
|
|
48
50
|
"cli",
|
|
49
51
|
"sdk",
|
|
50
|
-
"libredwg",
|
|
51
52
|
"svg",
|
|
52
53
|
"conversion",
|
|
53
54
|
"pi-package"
|
|
@@ -67,9 +68,11 @@
|
|
|
67
68
|
"@node-projects/acad-ts": "2.3.0",
|
|
68
69
|
"chalk": "^5.6.2",
|
|
69
70
|
"commander": "^14.0.3",
|
|
70
|
-
"minisearch": "^7.2.0",
|
|
71
71
|
"nanoid": "^5.1.6"
|
|
72
72
|
},
|
|
73
|
+
"optionalDependencies": {
|
|
74
|
+
"better-sqlite3": "^12.10.0"
|
|
75
|
+
},
|
|
73
76
|
"patchedDependencies": {
|
|
74
77
|
"@node-projects/acad-ts@2.3.0": "patches/@node-projects%2Facad-ts@2.3.0.patch"
|
|
75
78
|
}
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { readFileSync, writeFileSync } from "node:fs";
|
|
4
|
+
import { dirname, join } from "node:path";
|
|
5
|
+
import { fileURLToPath } from "node:url";
|
|
6
|
+
|
|
7
|
+
const root = dirname(dirname(fileURLToPath(import.meta.url)));
|
|
8
|
+
const targets = [
|
|
9
|
+
join(root, "node_modules"),
|
|
10
|
+
dirname(dirname(root)),
|
|
11
|
+
].map((nodeModulesRoot) =>
|
|
12
|
+
join(
|
|
13
|
+
nodeModulesRoot,
|
|
14
|
+
"@node-projects",
|
|
15
|
+
"acad-ts",
|
|
16
|
+
"dist",
|
|
17
|
+
"IO",
|
|
18
|
+
"DWG",
|
|
19
|
+
"DwgStreamReaders",
|
|
20
|
+
"DwgObjectReader.js",
|
|
21
|
+
),
|
|
22
|
+
);
|
|
23
|
+
|
|
24
|
+
const copied = "new Uint8Array(this._memoryStream)";
|
|
25
|
+
const reused = "this._memoryStream";
|
|
26
|
+
|
|
27
|
+
for (const target of targets) {
|
|
28
|
+
let source;
|
|
29
|
+
try {
|
|
30
|
+
source = readFileSync(target, "utf8");
|
|
31
|
+
} catch {
|
|
32
|
+
continue;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const matches = source.split(copied).length - 1;
|
|
36
|
+
if (matches === 0) process.exit(0);
|
|
37
|
+
|
|
38
|
+
writeFileSync(target, source.replaceAll(copied, reused));
|
|
39
|
+
console.log(`cadcli: patched acad-ts DwgObjectReader (${matches} buffer copies removed)`);
|
|
40
|
+
process.exit(0);
|
|
41
|
+
}
|
package/skills/cadcli/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: cadcli
|
|
3
|
-
description: Inspect, search, summarize, preview, and safely edit DWG/DXF CAD drawings with the cadcli CLI. Use when the user asks about CAD files, floorplans, DWG/DXF contents, layers, blocks, entities, text, previews, or
|
|
3
|
+
description: Inspect, search, summarize, preview, and safely edit DWG/DXF CAD drawings with the cadcli CLI. Use when the user asks about CAD files, floorplans, DWG/DXF contents, layers, blocks, entities, text, previews, or CAD edits.
|
|
4
4
|
---
|
|
5
5
|
|
|
6
6
|
# cadcli
|
|
@@ -15,7 +15,7 @@ cadcli entities <file> --limit 20 --json
|
|
|
15
15
|
cadcli search <file> "query" --json
|
|
16
16
|
```
|
|
17
17
|
|
|
18
|
-
For visual checks,
|
|
18
|
+
For visual checks, render an SVG preview:
|
|
19
19
|
|
|
20
20
|
```bash
|
|
21
21
|
cadcli view <file> -o preview.svg
|
|
@@ -27,8 +27,25 @@ For lightweight previews from the normalized model:
|
|
|
27
27
|
cadcli svg <file> -o preview.svg
|
|
28
28
|
```
|
|
29
29
|
|
|
30
|
-
For edits,
|
|
30
|
+
For edits, find handles first, then write an edited copy. Never overwrite by default; use `--overwrite` only when the user explicitly asks for in-place edits:
|
|
31
31
|
|
|
32
32
|
```bash
|
|
33
|
-
cadcli
|
|
33
|
+
cadcli entities <file> --type TEXT --json
|
|
34
|
+
cadcli search <file> "office" --json
|
|
35
|
+
|
|
36
|
+
cadcli edit <file> --set-text 'New label' --text-id <id> -o edited.dwg
|
|
37
|
+
cadcli edit <file> --set-layer A-TEXT --layer-id <id> -o edited.dwg
|
|
38
|
+
cadcli edit <file> --set-color '#ff0000' --color-id <id> -o edited.dwg
|
|
39
|
+
cadcli edit <file> --move <id> --dx 10 --dy 0 -o edited.dwg
|
|
40
|
+
cadcli edit <file> --rotate <id> --angle 90 --origin 0,0 -o edited.dwg
|
|
41
|
+
cadcli edit <file> --scale <id> --factor 2 -o edited.dwg
|
|
42
|
+
cadcli edit <file> --copy <id> --dx 10 --dy 0 -o edited.dwg
|
|
43
|
+
cadcli edit <file> --delete <id> -o edited.dwg
|
|
44
|
+
|
|
45
|
+
cadcli edit <file> --add-line 0,0:10,0 --new-layer A-WALL -o edited.dwg
|
|
46
|
+
cadcli edit <file> --add-circle 5,5:2 -o edited.dwg
|
|
47
|
+
cadcli edit <file> --add-text 'Label' --at 5,5 --height 2.5 -o edited.dwg
|
|
48
|
+
cadcli edit <file> --points '0,0;10,0;10,5' --closed -o edited.dwg
|
|
34
49
|
```
|
|
50
|
+
|
|
51
|
+
Edit formats: points are `x,y` or `x,y,z`; line is `from:to`; circle is `center:radius`; arc is `center:radius:startAngle:endAngle`; color is `bylayer`, `byblock`, an AutoCAD color index, `#rrggbb`, or `r,g,b`.
|
package/dist/core/libredwg.d.ts
DELETED
|
@@ -1,37 +0,0 @@
|
|
|
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;
|
package/dist/core/libredwg.js
DELETED
|
@@ -1,86 +0,0 @@
|
|
|
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
|
-
}
|