@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 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,7 @@
1
+ import type { DrawingCommandOptions } from "./shared.js";
2
+ export interface QueryOptions extends DrawingCommandOptions {
3
+ sql?: string;
4
+ file?: string;
5
+ schema?: boolean;
6
+ }
7
+ export declare function query(file: string, options: QueryOptions): Promise<void>;
@@ -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
+ }
@@ -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,
@@ -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;
@@ -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,3 @@
1
+ import type { DwgDocument } from "../types.js";
2
+ export declare function normalizeHebrewKeyboardText(text: string): string;
3
+ export declare function normalizeTextAuto(doc: DwgDocument): DwgDocument;
@@ -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.2.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": {
@@ -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. Start broad, then narrow down:
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 layers <file> --json
14
- cadcli entities <file> --limit 20 --json
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