@miclivs/cadcli 0.2.0 → 0.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/README.md +47 -0
- package/dist/commands/query.d.ts +7 -0
- package/dist/commands/query.js +45 -0
- package/dist/core/adapter.js +16 -1
- package/dist/core/drawing.js +2 -1
- package/dist/core/normalize.js +4 -1
- package/dist/core/query.d.ts +17 -0
- package/dist/core/query.js +232 -0
- package/dist/core/text-normalize.d.ts +3 -0
- package/dist/core/text-normalize.js +104 -0
- package/dist/main.js +16 -0
- package/dist/types.d.ts +9 -0
- package/package.json +2 -1
- package/skills/cadcli/SKILL.md +29 -5
package/README.md
CHANGED
|
@@ -41,6 +41,8 @@ cadcli entities <file> # entities, optionally filtered
|
|
|
41
41
|
|
|
42
42
|
cadcli search <file> [query] # search IDs, types, layers, text, raw fields
|
|
43
43
|
--query "door" --type TEXT --layer A-TEXT --limit 10 --score --no-snippets
|
|
44
|
+
cadcli query <file> --schema # show SQL query tables
|
|
45
|
+
cadcli query <file> --sql "select text from texts"
|
|
44
46
|
|
|
45
47
|
cadcli view <file> [-o preview.svg] # SVG preview
|
|
46
48
|
cadcli edit <file> --set-text <text> --text-id <id> -o out.dwg
|
|
@@ -81,6 +83,51 @@ SEARCH HINTS
|
|
|
81
83
|
```
|
|
82
84
|
|
|
83
85
|
|
|
86
|
+
## Text normalization
|
|
87
|
+
|
|
88
|
+
Some CAD files store visible text through legacy code pages and SHX fonts. For example, a Hebrew DWG may store `jsr muu, 8` while AutoCAD displays it as `חדר צוות 8`. `cadcli` automatically normalizes these cases while loading the drawing, before `overview`, `search`, `entities`, `json`, and future query features consume the document.
|
|
89
|
+
|
|
90
|
+
The normalizer is route-based: document metadata such as `codePage` is combined with per-entity text style/font metadata such as `gil.shx`, `narkism$.shx`, or `heb.shx`. When the route is confident, `cadcli` rewrites the in-memory `text` field to the intended text. It does not store both raw and normalized copies in caches or output.
|
|
91
|
+
|
|
92
|
+
```txt
|
|
93
|
+
raw DWG text: jsr muu, 8
|
|
94
|
+
cadcli text: חדר צוות 8
|
|
95
|
+
```
|
|
96
|
+
|
|
97
|
+
The normalized document metadata records what happened:
|
|
98
|
+
|
|
99
|
+
```json
|
|
100
|
+
{
|
|
101
|
+
"metadata": {
|
|
102
|
+
"codePage": "ansi_1255",
|
|
103
|
+
"textNormalization": {
|
|
104
|
+
"applied": ["hebrew-keyboard"],
|
|
105
|
+
"entitiesChanged": 58
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
This makes discovery work naturally:
|
|
112
|
+
|
|
113
|
+
```bash
|
|
114
|
+
cadcli overview office.dwg
|
|
115
|
+
cadcli search office.dwg "חדר" --json
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Query
|
|
119
|
+
|
|
120
|
+
`cadcli query` runs read-only SQL over a narrow in-memory projection of the normalized drawing. It does not create a user-visible database file.
|
|
121
|
+
|
|
122
|
+
```bash
|
|
123
|
+
cadcli query office.dwg --schema
|
|
124
|
+
cadcli query office.dwg --schema --json
|
|
125
|
+
cadcli query office.dwg --sql "select id, text, layer, x, y from texts where text like 'חדר%'" --json
|
|
126
|
+
cadcli query office.dwg --file rooms.sql --json
|
|
127
|
+
```
|
|
128
|
+
|
|
129
|
+
The base tables are `summary`, `metadata`, `layers`, `blocks`, and `entities`. Convenience tables expose common drawing concepts such as `texts` and `inserts`.
|
|
130
|
+
|
|
84
131
|
## Viewing vs SVG export
|
|
85
132
|
|
|
86
133
|
`cadcli view` renders SVG through acad-ts directly from the CAD document.
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
|
+
import { queryDrawing, querySchema, querySchemaTables } from "../core/query.js";
|
|
3
|
+
import { output } from "../utils/output.js";
|
|
4
|
+
import { drawingFor, handleCommandError, userError } from "./shared.js";
|
|
5
|
+
function querySql(options) {
|
|
6
|
+
if (options.schema)
|
|
7
|
+
return "";
|
|
8
|
+
if (options.sql && options.file) {
|
|
9
|
+
throw userError("Use either --sql or --file, not both.", "QUERY_SOURCE_CONFLICT");
|
|
10
|
+
}
|
|
11
|
+
if (options.sql)
|
|
12
|
+
return options.sql;
|
|
13
|
+
if (options.file)
|
|
14
|
+
return readFileSync(options.file, "utf8");
|
|
15
|
+
throw userError("Provide --sql <query>, --file <path>, or --schema.", "MISSING_QUERY");
|
|
16
|
+
}
|
|
17
|
+
function printRows(rows) {
|
|
18
|
+
for (const row of rows) {
|
|
19
|
+
console.log(Object.values(row)
|
|
20
|
+
.map((value) => value ?? "")
|
|
21
|
+
.join("\t"));
|
|
22
|
+
}
|
|
23
|
+
}
|
|
24
|
+
export async function query(file, options) {
|
|
25
|
+
try {
|
|
26
|
+
if (options.schema) {
|
|
27
|
+
output(options, {
|
|
28
|
+
json: () => ({ tables: querySchemaTables() }),
|
|
29
|
+
human: () => console.log(querySchema()),
|
|
30
|
+
});
|
|
31
|
+
return;
|
|
32
|
+
}
|
|
33
|
+
const sql = querySql(options);
|
|
34
|
+
const doc = await drawingFor(file, options).document();
|
|
35
|
+
const result = await queryDrawing(doc, sql);
|
|
36
|
+
output(options, {
|
|
37
|
+
json: () => result,
|
|
38
|
+
human: () => printRows(result.rows),
|
|
39
|
+
quiet: () => console.log(String(result.rows.length)),
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
catch (err) {
|
|
43
|
+
handleCommandError(err);
|
|
44
|
+
}
|
|
45
|
+
}
|
package/dist/core/adapter.js
CHANGED
|
@@ -66,6 +66,14 @@ function blockName(entity) {
|
|
|
66
66
|
const block = asRecord(entity).block;
|
|
67
67
|
return valueString(asRecord(block).name);
|
|
68
68
|
}
|
|
69
|
+
function textStyle(entity) {
|
|
70
|
+
const style = asRecord(entity)._style ?? asRecord(entity).style;
|
|
71
|
+
const rec = asRecord(style);
|
|
72
|
+
return {
|
|
73
|
+
name: valueString(rec.name ?? rec._name),
|
|
74
|
+
file: valueString(rec.filename),
|
|
75
|
+
};
|
|
76
|
+
}
|
|
69
77
|
function vertices(entity) {
|
|
70
78
|
const raw = asRecord(entity).vertices;
|
|
71
79
|
if (!Array.isArray(raw))
|
|
@@ -112,8 +120,14 @@ function normalizeAcadEntity(entity) {
|
|
|
112
120
|
data.startAngle = rec.startAngle;
|
|
113
121
|
if (typeof rec.endAngle === "number")
|
|
114
122
|
data.endAngle = rec.endAngle;
|
|
115
|
-
if (text)
|
|
123
|
+
if (text) {
|
|
124
|
+
const style = textStyle(entity);
|
|
116
125
|
data.text = text;
|
|
126
|
+
if (style.name)
|
|
127
|
+
data.textStyle = style.name;
|
|
128
|
+
if (style.file)
|
|
129
|
+
data.textStyleFile = style.file;
|
|
130
|
+
}
|
|
117
131
|
if (name)
|
|
118
132
|
data.blockName = name;
|
|
119
133
|
return data;
|
|
@@ -129,6 +143,7 @@ export function normalizeAcadDocument(doc) {
|
|
|
129
143
|
const entities = items(doc.modelSpace?.entities).map(normalizeAcadEntity);
|
|
130
144
|
return {
|
|
131
145
|
version: doc.header?.versionString ?? String(doc.header?.version ?? ""),
|
|
146
|
+
codePage: doc.header?.codePage,
|
|
132
147
|
layers,
|
|
133
148
|
blocks,
|
|
134
149
|
entities,
|
package/dist/core/drawing.js
CHANGED
|
@@ -4,13 +4,14 @@ import { DwgCliError } from "./errors.js";
|
|
|
4
4
|
import { readCadFile } from "./files.js";
|
|
5
5
|
import { normalizeDocument } from "./normalize.js";
|
|
6
6
|
import { renderSvg } from "./svg.js";
|
|
7
|
+
import { normalizeTextAuto } from "./text-normalize.js";
|
|
7
8
|
function readerFor(opts) {
|
|
8
9
|
return opts.reader ?? new AcadTsReader();
|
|
9
10
|
}
|
|
10
11
|
export async function loadDrawing(file, opts = {}) {
|
|
11
12
|
const { bytes, format } = readCadFile(file);
|
|
12
13
|
const raw = await readerFor(opts).parse(file, bytes, format);
|
|
13
|
-
return normalizeDocument(file, format, raw);
|
|
14
|
+
return normalizeTextAuto(normalizeDocument(file, format, raw));
|
|
14
15
|
}
|
|
15
16
|
export async function getInfo(file, opts) {
|
|
16
17
|
return (await loadDrawing(file, opts)).summary;
|
package/dist/core/normalize.js
CHANGED
|
@@ -140,6 +140,9 @@ export function normalizeDocument(file, format, raw) {
|
|
|
140
140
|
const layers = normalizeLayers(firstArray(root, LAYER_ARRAY_FIELDS), entities);
|
|
141
141
|
const blocks = normalizeBlocks(firstArray(root, BLOCK_ARRAY_FIELDS));
|
|
142
142
|
const version = stringField(root, ["version", "headerVersion", "dwgVersion"]);
|
|
143
|
+
const metadata = {
|
|
144
|
+
codePage: stringField(root, ["codePage", "encoding"]),
|
|
145
|
+
};
|
|
143
146
|
const summary = {
|
|
144
147
|
file: basename(file),
|
|
145
148
|
format,
|
|
@@ -152,5 +155,5 @@ export function normalizeDocument(file, format, raw) {
|
|
|
152
155
|
},
|
|
153
156
|
bounds: computeBounds(entities),
|
|
154
157
|
};
|
|
155
|
-
return { summary, layers, blocks, entities, unsupported, raw };
|
|
158
|
+
return { summary, metadata, layers, blocks, entities, unsupported, raw };
|
|
156
159
|
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import type { DwgDocument } from "../types.js";
|
|
2
|
+
export interface QuerySchemaColumn {
|
|
3
|
+
name: string;
|
|
4
|
+
type: "text" | "integer" | "real";
|
|
5
|
+
}
|
|
6
|
+
export interface QuerySchemaTable {
|
|
7
|
+
name: string;
|
|
8
|
+
description: string;
|
|
9
|
+
columns: QuerySchemaColumn[];
|
|
10
|
+
}
|
|
11
|
+
export interface QueryResult {
|
|
12
|
+
columns: string[];
|
|
13
|
+
rows: Record<string, unknown>[];
|
|
14
|
+
}
|
|
15
|
+
export declare function querySchema(): string;
|
|
16
|
+
export declare function querySchemaTables(): QuerySchemaTable[];
|
|
17
|
+
export declare function queryDrawing(doc: DwgDocument, sql: string): Promise<QueryResult>;
|
|
@@ -0,0 +1,232 @@
|
|
|
1
|
+
import { EXIT_UNAVAILABLE, EXIT_USER_ERROR } from "../utils/exit-codes.js";
|
|
2
|
+
import { DwgCliError } from "./errors.js";
|
|
3
|
+
const QUERY_SCHEMA_TABLES = [
|
|
4
|
+
{
|
|
5
|
+
name: "summary",
|
|
6
|
+
description: "One-row drawing summary.",
|
|
7
|
+
columns: [
|
|
8
|
+
{ name: "file", type: "text" },
|
|
9
|
+
{ name: "format", type: "text" },
|
|
10
|
+
{ name: "version", type: "text" },
|
|
11
|
+
{ name: "entity_count", type: "integer" },
|
|
12
|
+
{ name: "layer_count", type: "integer" },
|
|
13
|
+
{ name: "block_count", type: "integer" },
|
|
14
|
+
{ name: "unsupported_count", type: "integer" },
|
|
15
|
+
],
|
|
16
|
+
},
|
|
17
|
+
{
|
|
18
|
+
name: "metadata",
|
|
19
|
+
description: "Document metadata and normalization notes.",
|
|
20
|
+
columns: [
|
|
21
|
+
{ name: "key", type: "text" },
|
|
22
|
+
{ name: "value", type: "text" },
|
|
23
|
+
],
|
|
24
|
+
},
|
|
25
|
+
{
|
|
26
|
+
name: "layers",
|
|
27
|
+
description: "Drawing layers and entity counts.",
|
|
28
|
+
columns: [
|
|
29
|
+
{ name: "name", type: "text" },
|
|
30
|
+
{ name: "entity_count", type: "integer" },
|
|
31
|
+
],
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
name: "blocks",
|
|
35
|
+
description: "Block definitions and definition entity counts.",
|
|
36
|
+
columns: [
|
|
37
|
+
{ name: "name", type: "text" },
|
|
38
|
+
{ name: "entity_count", type: "integer" },
|
|
39
|
+
],
|
|
40
|
+
},
|
|
41
|
+
{
|
|
42
|
+
name: "entities",
|
|
43
|
+
description: "All normalized model-space entities, projected narrowly.",
|
|
44
|
+
columns: [
|
|
45
|
+
{ name: "id", type: "text" },
|
|
46
|
+
{ name: "type", type: "text" },
|
|
47
|
+
{ name: "layer", type: "text" },
|
|
48
|
+
{ name: "color", type: "text" },
|
|
49
|
+
],
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
name: "texts",
|
|
53
|
+
description: "TEXT and MTEXT entities with normalized text and insertion points.",
|
|
54
|
+
columns: [
|
|
55
|
+
{ name: "id", type: "text" },
|
|
56
|
+
{ name: "type", type: "text" },
|
|
57
|
+
{ name: "layer", type: "text" },
|
|
58
|
+
{ name: "text", type: "text" },
|
|
59
|
+
{ name: "x", type: "real" },
|
|
60
|
+
{ name: "y", type: "real" },
|
|
61
|
+
{ name: "text_style", type: "text" },
|
|
62
|
+
{ name: "text_style_file", type: "text" },
|
|
63
|
+
],
|
|
64
|
+
},
|
|
65
|
+
{
|
|
66
|
+
name: "inserts",
|
|
67
|
+
description: "Placed block instances with insertion points.",
|
|
68
|
+
columns: [
|
|
69
|
+
{ name: "id", type: "text" },
|
|
70
|
+
{ name: "layer", type: "text" },
|
|
71
|
+
{ name: "block_name", type: "text" },
|
|
72
|
+
{ name: "x", type: "real" },
|
|
73
|
+
{ name: "y", type: "real" },
|
|
74
|
+
],
|
|
75
|
+
},
|
|
76
|
+
];
|
|
77
|
+
export function querySchema() {
|
|
78
|
+
return QUERY_SCHEMA_TABLES.map((table) => `${table.name}(${table.columns.map((column) => column.name).join(", ")})`).join("\n");
|
|
79
|
+
}
|
|
80
|
+
export function querySchemaTables() {
|
|
81
|
+
return QUERY_SCHEMA_TABLES;
|
|
82
|
+
}
|
|
83
|
+
async function openMemoryDatabase() {
|
|
84
|
+
try {
|
|
85
|
+
const { default: Database } = await import("better-sqlite3");
|
|
86
|
+
return new Database(":memory:");
|
|
87
|
+
}
|
|
88
|
+
catch {
|
|
89
|
+
throw new DwgCliError("cadcli query requires the optional better-sqlite3 dependency.", "QUERY_UNAVAILABLE", EXIT_UNAVAILABLE);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
function sqlValue(value) {
|
|
93
|
+
if (value === undefined || value === null)
|
|
94
|
+
return null;
|
|
95
|
+
if (typeof value === "number" || typeof value === "string")
|
|
96
|
+
return value;
|
|
97
|
+
return String(value);
|
|
98
|
+
}
|
|
99
|
+
function point(entity) {
|
|
100
|
+
const value = entity.data.insertionPoint ?? entity.data.position ?? entity.data.point;
|
|
101
|
+
if (!value || typeof value !== "object")
|
|
102
|
+
return { x: null, y: null };
|
|
103
|
+
const rec = value;
|
|
104
|
+
const x = Number(rec.x ?? rec.X);
|
|
105
|
+
const y = Number(rec.y ?? rec.Y);
|
|
106
|
+
return {
|
|
107
|
+
x: Number.isFinite(x) ? x : null,
|
|
108
|
+
y: Number.isFinite(y) ? y : null,
|
|
109
|
+
};
|
|
110
|
+
}
|
|
111
|
+
function textValue(entity) {
|
|
112
|
+
const value = entity.data.text ?? entity.data.value ?? entity.data.Text;
|
|
113
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
114
|
+
}
|
|
115
|
+
function blockName(entity) {
|
|
116
|
+
const value = entity.data.blockName ?? entity.data.block_name ?? entity.data.name;
|
|
117
|
+
return typeof value === "string" && value.length > 0 ? value : null;
|
|
118
|
+
}
|
|
119
|
+
function createSchema(db) {
|
|
120
|
+
db.exec(`
|
|
121
|
+
CREATE TABLE summary (
|
|
122
|
+
file TEXT,
|
|
123
|
+
format TEXT,
|
|
124
|
+
version TEXT,
|
|
125
|
+
entity_count INTEGER,
|
|
126
|
+
layer_count INTEGER,
|
|
127
|
+
block_count INTEGER,
|
|
128
|
+
unsupported_count INTEGER
|
|
129
|
+
);
|
|
130
|
+
CREATE TABLE metadata (key TEXT PRIMARY KEY, value TEXT);
|
|
131
|
+
CREATE TABLE layers (name TEXT PRIMARY KEY, entity_count INTEGER);
|
|
132
|
+
CREATE TABLE blocks (name TEXT PRIMARY KEY, entity_count INTEGER);
|
|
133
|
+
CREATE TABLE entities (id TEXT PRIMARY KEY, type TEXT, layer TEXT, color TEXT);
|
|
134
|
+
CREATE TABLE texts (
|
|
135
|
+
id TEXT PRIMARY KEY,
|
|
136
|
+
type TEXT,
|
|
137
|
+
layer TEXT,
|
|
138
|
+
text TEXT,
|
|
139
|
+
x REAL,
|
|
140
|
+
y REAL,
|
|
141
|
+
text_style TEXT,
|
|
142
|
+
text_style_file TEXT
|
|
143
|
+
);
|
|
144
|
+
CREATE TABLE inserts (
|
|
145
|
+
id TEXT PRIMARY KEY,
|
|
146
|
+
layer TEXT,
|
|
147
|
+
block_name TEXT,
|
|
148
|
+
x REAL,
|
|
149
|
+
y REAL
|
|
150
|
+
);
|
|
151
|
+
`);
|
|
152
|
+
}
|
|
153
|
+
function registerFunctions(db) {
|
|
154
|
+
db.function("regexp", (pattern, value) => {
|
|
155
|
+
if (typeof pattern !== "string" || value === null || value === undefined) {
|
|
156
|
+
return 0;
|
|
157
|
+
}
|
|
158
|
+
try {
|
|
159
|
+
return new RegExp(pattern, "u").test(String(value)) ? 1 : 0;
|
|
160
|
+
}
|
|
161
|
+
catch {
|
|
162
|
+
return 0;
|
|
163
|
+
}
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
function insertDocument(db, doc) {
|
|
167
|
+
db.prepare("INSERT INTO summary VALUES (?, ?, ?, ?, ?, ?, ?)").run(doc.summary.file, doc.summary.format, doc.summary.version ?? null, doc.summary.counts.entities, doc.summary.counts.layers, doc.summary.counts.blocks, doc.summary.counts.unsupported);
|
|
168
|
+
const insertMetadata = db.prepare("INSERT INTO metadata VALUES (?, ?)");
|
|
169
|
+
if (doc.metadata.codePage)
|
|
170
|
+
insertMetadata.run("code_page", doc.metadata.codePage);
|
|
171
|
+
if (doc.metadata.textNormalization) {
|
|
172
|
+
insertMetadata.run("text_normalization_applied", doc.metadata.textNormalization.applied.join(","));
|
|
173
|
+
insertMetadata.run("text_normalization_entities_changed", doc.metadata.textNormalization.entitiesChanged);
|
|
174
|
+
}
|
|
175
|
+
const insertLayer = db.prepare("INSERT INTO layers VALUES (?, ?)");
|
|
176
|
+
const insertBlock = db.prepare("INSERT INTO blocks VALUES (?, ?)");
|
|
177
|
+
const insertEntity = db.prepare("INSERT INTO entities VALUES (?, ?, ?, ?)");
|
|
178
|
+
const insertText = db.prepare("INSERT INTO texts VALUES (?, ?, ?, ?, ?, ?, ?, ?)");
|
|
179
|
+
const insertInsert = db.prepare("INSERT INTO inserts VALUES (?, ?, ?, ?, ?)");
|
|
180
|
+
db.exec("BEGIN");
|
|
181
|
+
try {
|
|
182
|
+
for (const layer of doc.layers)
|
|
183
|
+
insertLayer.run(layer.name, layer.entityCount);
|
|
184
|
+
for (const block of doc.blocks)
|
|
185
|
+
insertBlock.run(block.name, block.entityCount);
|
|
186
|
+
for (const entity of doc.entities) {
|
|
187
|
+
insertEntity.run(entity.id, entity.type, entity.layer ?? null, sqlValue(entity.color));
|
|
188
|
+
const { x, y } = point(entity);
|
|
189
|
+
const text = textValue(entity);
|
|
190
|
+
if ((entity.type === "TEXT" || entity.type === "MTEXT") && text) {
|
|
191
|
+
insertText.run(entity.id, entity.type, entity.layer ?? null, text, x, y, sqlValue(entity.data.textStyle), sqlValue(entity.data.textStyleFile));
|
|
192
|
+
}
|
|
193
|
+
if (entity.type === "INSERT") {
|
|
194
|
+
insertInsert.run(entity.id, entity.layer ?? null, blockName(entity), x, y);
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
db.exec("COMMIT");
|
|
198
|
+
}
|
|
199
|
+
catch (error) {
|
|
200
|
+
db.exec("ROLLBACK");
|
|
201
|
+
throw error;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
function ensureSelectQuery(sql) {
|
|
205
|
+
const trimmed = sql.trim();
|
|
206
|
+
if (!/^(select|with)\b/i.test(trimmed)) {
|
|
207
|
+
throw new DwgCliError("Query SQL must be a SELECT statement.", "INVALID_QUERY_SQL", EXIT_USER_ERROR);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
function ensureReadOnlyStatement(statement) {
|
|
211
|
+
if (statement.readonly === false) {
|
|
212
|
+
throw new DwgCliError("Query SQL must be read-only.", "INVALID_QUERY_SQL", EXIT_USER_ERROR);
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
export async function queryDrawing(doc, sql) {
|
|
216
|
+
ensureSelectQuery(sql);
|
|
217
|
+
const db = await openMemoryDatabase();
|
|
218
|
+
try {
|
|
219
|
+
createSchema(db);
|
|
220
|
+
registerFunctions(db);
|
|
221
|
+
insertDocument(db, doc);
|
|
222
|
+
const statement = db.prepare(sql);
|
|
223
|
+
ensureReadOnlyStatement(statement);
|
|
224
|
+
const rows = statement.all();
|
|
225
|
+
const columns = statement.columns?.().map((column) => column.name) ??
|
|
226
|
+
Object.keys(rows[0] ?? {});
|
|
227
|
+
return { columns, rows };
|
|
228
|
+
}
|
|
229
|
+
finally {
|
|
230
|
+
db.close();
|
|
231
|
+
}
|
|
232
|
+
}
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import hebrewLayout from "convert-layout/he.js";
|
|
2
|
+
const TEXT_NORMALIZATION_ROUTES = [
|
|
3
|
+
{
|
|
4
|
+
name: "hebrew-keyboard",
|
|
5
|
+
codePages: ["ansi_1255"],
|
|
6
|
+
textStyleFiles: ["gil.shx", "narkism$.shx", "heb.shx", "oron", "oron1"],
|
|
7
|
+
fromLatinKeyboard: hebrewLayout.fromEn,
|
|
8
|
+
targetScript: "hebrew",
|
|
9
|
+
},
|
|
10
|
+
];
|
|
11
|
+
function asText(value) {
|
|
12
|
+
return typeof value === "string" && value.length > 0 ? value : undefined;
|
|
13
|
+
}
|
|
14
|
+
function normalizedToken(value) {
|
|
15
|
+
return value?.trim().toLowerCase();
|
|
16
|
+
}
|
|
17
|
+
function textStyleFile(entity) {
|
|
18
|
+
return normalizedToken(asText(entity.data.textStyleFile));
|
|
19
|
+
}
|
|
20
|
+
function isTextEntity(entity) {
|
|
21
|
+
return entity.type === "TEXT" || entity.type === "MTEXT";
|
|
22
|
+
}
|
|
23
|
+
function hasTargetScript(text, script) {
|
|
24
|
+
switch (script) {
|
|
25
|
+
case "hebrew":
|
|
26
|
+
return /[\u0590-\u05ff]/u.test(text);
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
function targetScriptCount(text, script) {
|
|
30
|
+
switch (script) {
|
|
31
|
+
case "hebrew":
|
|
32
|
+
return text.match(/[\u0590-\u05ff]/gu)?.length ?? 0;
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
function mostlyLatinKeyboardText(text, targetScript) {
|
|
36
|
+
const latinLetters = text.match(/[A-Za-z]/g)?.length ?? 0;
|
|
37
|
+
return latinLetters > 0 && targetScriptCount(text, targetScript) === 0;
|
|
38
|
+
}
|
|
39
|
+
function routeMatches(route, doc, entity) {
|
|
40
|
+
const codePage = normalizedToken(doc.metadata.codePage);
|
|
41
|
+
const styleFile = textStyleFile(entity);
|
|
42
|
+
return Boolean(codePage &&
|
|
43
|
+
styleFile &&
|
|
44
|
+
route.codePages.includes(codePage) &&
|
|
45
|
+
route.textStyleFiles.includes(styleFile));
|
|
46
|
+
}
|
|
47
|
+
function routeFor(doc, entity) {
|
|
48
|
+
return TEXT_NORMALIZATION_ROUTES.find((route) => routeMatches(route, doc, entity));
|
|
49
|
+
}
|
|
50
|
+
function normalizeEntityText(doc, entity) {
|
|
51
|
+
if (!isTextEntity(entity))
|
|
52
|
+
return { entity };
|
|
53
|
+
const route = routeFor(doc, entity);
|
|
54
|
+
if (!route)
|
|
55
|
+
return { entity };
|
|
56
|
+
const text = asText(entity.data.text);
|
|
57
|
+
if (!text || !mostlyLatinKeyboardText(text, route.targetScript)) {
|
|
58
|
+
return { entity };
|
|
59
|
+
}
|
|
60
|
+
const normalized = route.fromLatinKeyboard(text);
|
|
61
|
+
if (normalized === text || !hasTargetScript(normalized, route.targetScript)) {
|
|
62
|
+
return { entity };
|
|
63
|
+
}
|
|
64
|
+
return {
|
|
65
|
+
normalizer: route.name,
|
|
66
|
+
entity: {
|
|
67
|
+
...entity,
|
|
68
|
+
data: {
|
|
69
|
+
...entity.data,
|
|
70
|
+
text: normalized,
|
|
71
|
+
},
|
|
72
|
+
},
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
export function normalizeHebrewKeyboardText(text) {
|
|
76
|
+
return hebrewLayout.fromEn(text);
|
|
77
|
+
}
|
|
78
|
+
export function normalizeTextAuto(doc) {
|
|
79
|
+
const applied = new Set();
|
|
80
|
+
let entitiesChanged = 0;
|
|
81
|
+
const entities = doc.entities.map((entity) => {
|
|
82
|
+
const result = normalizeEntityText(doc, entity);
|
|
83
|
+
if (!result.normalizer)
|
|
84
|
+
return entity;
|
|
85
|
+
entitiesChanged++;
|
|
86
|
+
applied.add(result.normalizer);
|
|
87
|
+
return result.entity;
|
|
88
|
+
});
|
|
89
|
+
if (entitiesChanged === 0)
|
|
90
|
+
return doc;
|
|
91
|
+
const unsupportedIds = new Set(doc.unsupported.map((entity) => entity.id));
|
|
92
|
+
return {
|
|
93
|
+
...doc,
|
|
94
|
+
entities,
|
|
95
|
+
unsupported: entities.filter((entity) => unsupportedIds.has(entity.id)),
|
|
96
|
+
metadata: {
|
|
97
|
+
...doc.metadata,
|
|
98
|
+
textNormalization: {
|
|
99
|
+
applied: [...applied],
|
|
100
|
+
entitiesChanged,
|
|
101
|
+
},
|
|
102
|
+
},
|
|
103
|
+
};
|
|
104
|
+
}
|
package/dist/main.js
CHANGED
|
@@ -9,6 +9,7 @@ import { info } from "./commands/info.js";
|
|
|
9
9
|
import { json } from "./commands/json.js";
|
|
10
10
|
import { layers } from "./commands/layers.js";
|
|
11
11
|
import { overview } from "./commands/overview.js";
|
|
12
|
+
import { query } from "./commands/query.js";
|
|
12
13
|
import { search } from "./commands/search.js";
|
|
13
14
|
import { svg } from "./commands/svg.js";
|
|
14
15
|
import { thumbnail } from "./commands/thumbnail.js";
|
|
@@ -27,6 +28,7 @@ Examples:
|
|
|
27
28
|
$ cadcli layers drawing.dwg --json List layers for scripts/agents
|
|
28
29
|
$ cadcli entities drawing.dwg --type LINE --limit 20
|
|
29
30
|
$ cadcli search drawing.dwg "conference" --layer A-TEXT
|
|
31
|
+
$ cadcli query drawing.dwg --sql "select text, x, y from texts"
|
|
30
32
|
$ cadcli view drawing.dwg -o drawing.svg
|
|
31
33
|
$ cadcli edit drawing.dwg --set-text "Office" --text-id 2A -o edited.dwg
|
|
32
34
|
$ cadcli edit drawing.dwg --add-line 0,0:10,0 --new-layer A-WALL -o edited.dwg
|
|
@@ -40,6 +42,7 @@ Inspecting:
|
|
|
40
42
|
blocks <file> List blocks
|
|
41
43
|
entities <file> List/filter entities
|
|
42
44
|
search <file> Search entities by text, type, layer, and raw fields
|
|
45
|
+
query <file> Query normalized CAD tables with SQL
|
|
43
46
|
|
|
44
47
|
Viewing and editing:
|
|
45
48
|
view <file> Render an SVG preview
|
|
@@ -50,6 +53,12 @@ Conversion:
|
|
|
50
53
|
svg <file> Render best-effort SVG from normalized JSON
|
|
51
54
|
thumbnail <file> Extract embedded thumbnail when available
|
|
52
55
|
|
|
56
|
+
Querying:
|
|
57
|
+
query <file> Query normalized CAD tables with SQL
|
|
58
|
+
--schema Show available query tables
|
|
59
|
+
--sql <query> Run an inline SELECT query
|
|
60
|
+
--file <path> Read a SELECT query from a file
|
|
61
|
+
|
|
53
62
|
Options:
|
|
54
63
|
--json Output as JSON
|
|
55
64
|
-q, --quiet Suppress non-essential output
|
|
@@ -119,6 +128,13 @@ program
|
|
|
119
128
|
root.query = root.query || queryWords.join(" ");
|
|
120
129
|
await search(file, root);
|
|
121
130
|
});
|
|
131
|
+
program
|
|
132
|
+
.command("query <file>")
|
|
133
|
+
.description("Query normalized CAD tables with SQL")
|
|
134
|
+
.option("--sql <query>", "SQL SELECT query to run")
|
|
135
|
+
.option("--file <path>", "Read SQL query from a file")
|
|
136
|
+
.option("--schema", "Show available query tables")
|
|
137
|
+
.action(async (file, opts, cmd) => query(file, { ...cmd.optsWithGlobals(), ...opts }));
|
|
122
138
|
program
|
|
123
139
|
.command("view <file>")
|
|
124
140
|
.description("Render an SVG preview")
|
package/dist/types.d.ts
CHANGED
|
@@ -35,8 +35,17 @@ export interface DwgSummary {
|
|
|
35
35
|
};
|
|
36
36
|
bounds?: DwgBounds;
|
|
37
37
|
}
|
|
38
|
+
export interface DwgTextNormalizationMetadata {
|
|
39
|
+
applied: string[];
|
|
40
|
+
entitiesChanged: number;
|
|
41
|
+
}
|
|
42
|
+
export interface DwgMetadata {
|
|
43
|
+
codePage?: string;
|
|
44
|
+
textNormalization?: DwgTextNormalizationMetadata;
|
|
45
|
+
}
|
|
38
46
|
export interface DwgDocument {
|
|
39
47
|
summary: DwgSummary;
|
|
48
|
+
metadata: DwgMetadata;
|
|
40
49
|
layers: DwgLayer[];
|
|
41
50
|
blocks: DwgBlock[];
|
|
42
51
|
entities: DwgEntity[];
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@miclivs/cadcli",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.3.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",
|
|
@@ -68,6 +68,7 @@
|
|
|
68
68
|
"@node-projects/acad-ts": "2.3.0",
|
|
69
69
|
"chalk": "^5.6.2",
|
|
70
70
|
"commander": "^14.0.3",
|
|
71
|
+
"convert-layout": "^0.11.1",
|
|
71
72
|
"nanoid": "^5.1.6"
|
|
72
73
|
},
|
|
73
74
|
"optionalDependencies": {
|
package/skills/cadcli/SKILL.md
CHANGED
|
@@ -5,16 +5,40 @@ description: Inspect, search, summarize, preview, and safely edit DWG/DXF CAD dr
|
|
|
5
5
|
|
|
6
6
|
# cadcli
|
|
7
7
|
|
|
8
|
-
Use `cadcli` for progressive CAD file discovery.
|
|
8
|
+
Use `cadcli` for progressive CAD file discovery. Follow this order when trying to understand a drawing:
|
|
9
|
+
|
|
10
|
+
```bash
|
|
11
|
+
cadcli info <file> --json # size, format, counts, bounds
|
|
12
|
+
cadcli overview <file> # human clues: layers, labels, blocks, hints
|
|
13
|
+
cadcli layers <file> --json # major groupings
|
|
14
|
+
cadcli blocks <file> --json # reusable object definitions
|
|
15
|
+
cadcli query <file> --schema # learn query tables before writing SQL
|
|
16
|
+
cadcli search <file> "query" --json # fuzzy discovery from overview terms
|
|
17
|
+
cadcli entities <file> --limit 20 --json # raw drilldown only when needed
|
|
18
|
+
```
|
|
19
|
+
|
|
20
|
+
When working with a drawing, build a human-readable understanding of what is in it. Do not stop at listing CAD primitives such as layers, blocks, handles, entity types, or raw coordinates. Use those details as evidence, then explain the drawing in the terms a person would use: rooms, entrances, furniture, doors, fixtures, workstations, corridors, core/service areas, labels, structural elements, mechanical/electrical items, or other project-specific objects.
|
|
21
|
+
|
|
22
|
+
Do this as evidence-based discovery. Say what the file appears to contain and cite the clues that support it: layer names, block names, text samples, entity counts, and coordinates/handles where useful. Avoid imposing generic assumptions. Different drawings encode meaning differently: one file may identify rooms through text labels, another through room-tag blocks, another through closed polylines, and another not at all. Follow the file's own conventions and translate the underlying CAD structure into concepts users can understand.
|
|
23
|
+
|
|
24
|
+
Use each command for a different job. `overview` is for clues and vocabulary. `search` is for fuzzy discovery when you have a term from the overview or the user. `query` is for tabular/repeatable questions. `entities` is for raw drilldown after you know what type or layer matters.
|
|
25
|
+
|
|
26
|
+
Always run `cadcli query <file> --schema` before writing SQL for a file. The query engine exposes narrow in-memory tables: `summary`, `metadata`, `layers`, `blocks`, `entities`, `texts`, and `inserts`.
|
|
27
|
+
|
|
28
|
+
For a question like "how many rooms are there", use this pattern:
|
|
9
29
|
|
|
10
30
|
```bash
|
|
11
|
-
cadcli info <file> --json
|
|
12
31
|
cadcli overview <file>
|
|
13
|
-
cadcli
|
|
14
|
-
cadcli
|
|
15
|
-
cadcli search <file> "query" --json
|
|
32
|
+
cadcli query <file> --schema
|
|
33
|
+
cadcli query <file> --sql "select id, text, layer, x, y from texts where text like 'Room%'" --json
|
|
16
34
|
```
|
|
17
35
|
|
|
36
|
+
Adapt the query terms to the drawing's language and labels. For Hebrew drawings, text may already be normalized by cadcli, so a room query may use `text like 'חדר%'`. For drawings where rooms are block tags rather than text labels, query `inserts` instead of `texts`.
|
|
37
|
+
|
|
38
|
+
If the drawing shows furniture or door layers, inspect their `INSERT` entities and block names. Use `cadcli query` for questions like "where are the inserted chair blocks", "which labels start with room", or "which layers have the most entities". If it shows cryptic layers like `A2` or `A3`, infer cautiously from the entity mix and repeated block/text evidence rather than from the layer name alone.
|
|
39
|
+
|
|
40
|
+
When answering the user, communicate in ordinary language. Use terms like "rooms", "furniture", "doors", "labels", "support spaces", "architectural linework", and "evidence". Do not lead with implementation details like entity types, raw JSON fields, SQL tables, or file internals unless they are needed to justify the answer or the user asks for them.
|
|
41
|
+
|
|
18
42
|
For visual checks, render an SVG preview:
|
|
19
43
|
|
|
20
44
|
```bash
|