@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,227 @@
|
|
|
1
|
+
import { loadDrawing } from "./drawing.js";
|
|
2
|
+
const DEFAULT_KEYWORDS = 8;
|
|
3
|
+
const DEFAULT_SAMPLES = 8;
|
|
4
|
+
const MAX_LAYER_TYPES = 6;
|
|
5
|
+
const STOP_WORDS = new Set([
|
|
6
|
+
"the",
|
|
7
|
+
"and",
|
|
8
|
+
"for",
|
|
9
|
+
"with",
|
|
10
|
+
"from",
|
|
11
|
+
"this",
|
|
12
|
+
"that",
|
|
13
|
+
"are",
|
|
14
|
+
"was",
|
|
15
|
+
"were",
|
|
16
|
+
"floor",
|
|
17
|
+
"plan",
|
|
18
|
+
"drawing",
|
|
19
|
+
]);
|
|
20
|
+
function stringifyValue(value) {
|
|
21
|
+
if (value === null || value === undefined)
|
|
22
|
+
return "";
|
|
23
|
+
if (["string", "number", "boolean", "bigint"].includes(typeof value)) {
|
|
24
|
+
return String(value);
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(value))
|
|
27
|
+
return value.map(stringifyValue).join(" ");
|
|
28
|
+
if (typeof value === "object") {
|
|
29
|
+
return Object.values(value)
|
|
30
|
+
.map(stringifyValue)
|
|
31
|
+
.join(" ");
|
|
32
|
+
}
|
|
33
|
+
return "";
|
|
34
|
+
}
|
|
35
|
+
function cleanText(text) {
|
|
36
|
+
return text
|
|
37
|
+
.replace(/https?:\/\/[^\s)>\]]+/g, " ")
|
|
38
|
+
.replace(/\b[a-f0-9]{8,}\b/gi, " ")
|
|
39
|
+
.replace(/[_-]+/g, " ");
|
|
40
|
+
}
|
|
41
|
+
function tokens(text) {
|
|
42
|
+
return (cleanText(text)
|
|
43
|
+
.toLowerCase()
|
|
44
|
+
.match(/[\p{L}\p{N}]{2,}/gu) ?? [])
|
|
45
|
+
.filter((term) => !STOP_WORDS.has(term))
|
|
46
|
+
.filter((term) => !/^\d+$/.test(term));
|
|
47
|
+
}
|
|
48
|
+
function terms(text) {
|
|
49
|
+
const words = tokens(text);
|
|
50
|
+
const result = [...words];
|
|
51
|
+
for (let index = 0; index < words.length - 1; index++) {
|
|
52
|
+
const left = words[index];
|
|
53
|
+
const right = words[index + 1];
|
|
54
|
+
if (left !== right)
|
|
55
|
+
result.push(`${left} ${right}`);
|
|
56
|
+
}
|
|
57
|
+
return result;
|
|
58
|
+
}
|
|
59
|
+
function addTerms(freq, text, weight) {
|
|
60
|
+
for (const term of terms(text)) {
|
|
61
|
+
freq.set(term, (freq.get(term) ?? 0) + weight);
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
function topTerms(freq, limit) {
|
|
65
|
+
const selected = [];
|
|
66
|
+
const suppressed = new Set();
|
|
67
|
+
const sorted = [...freq.entries()].sort((a, b) => b[1] - a[1]);
|
|
68
|
+
for (const [term] of sorted) {
|
|
69
|
+
if (selected.length >= limit)
|
|
70
|
+
break;
|
|
71
|
+
if (suppressed.has(term))
|
|
72
|
+
continue;
|
|
73
|
+
if (term.includes(" ") && (freq.get(term) ?? 0) < 2)
|
|
74
|
+
continue;
|
|
75
|
+
selected.push(term);
|
|
76
|
+
if (term.includes(" ")) {
|
|
77
|
+
for (const part of term.split(" "))
|
|
78
|
+
suppressed.add(part);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
return selected;
|
|
82
|
+
}
|
|
83
|
+
function dataText(entity) {
|
|
84
|
+
return [
|
|
85
|
+
entity.id,
|
|
86
|
+
entity.type,
|
|
87
|
+
entity.layer ?? "",
|
|
88
|
+
String(entity.data.text ?? ""),
|
|
89
|
+
String(entity.data.value ?? ""),
|
|
90
|
+
String(entity.data.name ?? ""),
|
|
91
|
+
String(entity.data.blockName ?? ""),
|
|
92
|
+
String(entity.data.block_name ?? ""),
|
|
93
|
+
stringifyValue(entity.data),
|
|
94
|
+
].join(" ");
|
|
95
|
+
}
|
|
96
|
+
function visibleText(entity) {
|
|
97
|
+
const text = entity.data.text ?? entity.data.value ?? entity.data.Text;
|
|
98
|
+
if (text === undefined || text === null)
|
|
99
|
+
return undefined;
|
|
100
|
+
const trimmed = String(text).trim();
|
|
101
|
+
return trimmed.length > 0 ? trimmed : undefined;
|
|
102
|
+
}
|
|
103
|
+
function blockName(entity) {
|
|
104
|
+
const value = entity.data.blockName ?? entity.data.block_name ?? entity.data.name;
|
|
105
|
+
if (value === undefined || value === null)
|
|
106
|
+
return undefined;
|
|
107
|
+
const name = String(value).trim();
|
|
108
|
+
return name.length > 0 ? name : undefined;
|
|
109
|
+
}
|
|
110
|
+
function frequencyBy(values) {
|
|
111
|
+
const counts = new Map();
|
|
112
|
+
for (const value of values) {
|
|
113
|
+
if (!value)
|
|
114
|
+
continue;
|
|
115
|
+
counts.set(value, (counts.get(value) ?? 0) + 1);
|
|
116
|
+
}
|
|
117
|
+
return counts;
|
|
118
|
+
}
|
|
119
|
+
function sortedCounts(counts) {
|
|
120
|
+
return [...counts.entries()].sort((a, b) => b[1] - a[1] || a[0].localeCompare(b[0]));
|
|
121
|
+
}
|
|
122
|
+
function layerKeywords(entities, documentFrequency, totalLayers, maxKeywords) {
|
|
123
|
+
const weighted = entities.map((entity) => ({
|
|
124
|
+
text: dataText(entity),
|
|
125
|
+
weight: entity.type === "TEXT" || entity.type === "MTEXT" ? 3 : 1,
|
|
126
|
+
}));
|
|
127
|
+
const freq = new Map();
|
|
128
|
+
for (const source of weighted)
|
|
129
|
+
addTerms(freq, source.text, source.weight);
|
|
130
|
+
const scored = new Map();
|
|
131
|
+
for (const [term, tf] of freq) {
|
|
132
|
+
const idf = Math.log(1 + totalLayers / (documentFrequency.get(term) ?? 1));
|
|
133
|
+
scored.set(term, tf * idf);
|
|
134
|
+
}
|
|
135
|
+
return topTerms(scored, maxKeywords);
|
|
136
|
+
}
|
|
137
|
+
function buildDocumentFrequency(layerEntities) {
|
|
138
|
+
const df = new Map();
|
|
139
|
+
for (const entities of layerEntities.values()) {
|
|
140
|
+
const seen = new Set();
|
|
141
|
+
for (const entity of entities) {
|
|
142
|
+
for (const term of terms(dataText(entity)))
|
|
143
|
+
seen.add(term);
|
|
144
|
+
}
|
|
145
|
+
for (const term of seen)
|
|
146
|
+
df.set(term, (df.get(term) ?? 0) + 1);
|
|
147
|
+
}
|
|
148
|
+
return df;
|
|
149
|
+
}
|
|
150
|
+
function uniquePush(items, value, limit) {
|
|
151
|
+
if (!value || items.length >= limit || items.includes(value))
|
|
152
|
+
return;
|
|
153
|
+
items.push(value);
|
|
154
|
+
}
|
|
155
|
+
export function createDrawingOverview(doc, opts = {}) {
|
|
156
|
+
const maxKeywords = opts.keywords ?? DEFAULT_KEYWORDS;
|
|
157
|
+
const maxSamples = opts.samples ?? DEFAULT_SAMPLES;
|
|
158
|
+
const layerEntities = new Map();
|
|
159
|
+
for (const layer of doc.layers)
|
|
160
|
+
layerEntities.set(layer.name, []);
|
|
161
|
+
for (const entity of doc.entities) {
|
|
162
|
+
const layer = entity.layer ?? "(no layer)";
|
|
163
|
+
if (!layerEntities.has(layer))
|
|
164
|
+
layerEntities.set(layer, []);
|
|
165
|
+
layerEntities.get(layer)?.push(entity);
|
|
166
|
+
}
|
|
167
|
+
const documentFrequency = buildDocumentFrequency(layerEntities);
|
|
168
|
+
const totalLayers = Math.max(layerEntities.size, 1);
|
|
169
|
+
const layers = [...layerEntities.entries()].map(([name, entities]) => {
|
|
170
|
+
const typeCounts = sortedCounts(frequencyBy(entities.map((e) => e.type)));
|
|
171
|
+
return {
|
|
172
|
+
name,
|
|
173
|
+
entities: entities.length,
|
|
174
|
+
types: typeCounts.slice(0, MAX_LAYER_TYPES).map(([type]) => type),
|
|
175
|
+
keywords: layerKeywords(entities, documentFrequency, totalLayers, maxKeywords),
|
|
176
|
+
};
|
|
177
|
+
});
|
|
178
|
+
layers.sort((a, b) => b.entities - a.entities || a.name.localeCompare(b.name));
|
|
179
|
+
const entityTypes = sortedCounts(frequencyBy(doc.entities.map((e) => e.type))).map(([type, count]) => ({ type, count }));
|
|
180
|
+
const references = frequencyBy(doc.entities.map(blockName));
|
|
181
|
+
const definitionCounts = frequencyBy(doc.blocks.map((block) => block.name));
|
|
182
|
+
const blockNames = new Set([
|
|
183
|
+
...references.keys(),
|
|
184
|
+
...definitionCounts.keys(),
|
|
185
|
+
]);
|
|
186
|
+
const blocks = [...blockNames]
|
|
187
|
+
.map((name) => ({
|
|
188
|
+
name,
|
|
189
|
+
definitions: definitionCounts.get(name) ?? 0,
|
|
190
|
+
references: references.get(name) ?? 0,
|
|
191
|
+
}))
|
|
192
|
+
.sort((a, b) => b.references - a.references ||
|
|
193
|
+
b.definitions - a.definitions ||
|
|
194
|
+
a.name.localeCompare(b.name));
|
|
195
|
+
const textFreq = new Map();
|
|
196
|
+
const textSamples = [];
|
|
197
|
+
for (const entity of doc.entities) {
|
|
198
|
+
const text = visibleText(entity);
|
|
199
|
+
if (!text)
|
|
200
|
+
continue;
|
|
201
|
+
uniquePush(textSamples, text, maxSamples);
|
|
202
|
+
addTerms(textFreq, text, 1);
|
|
203
|
+
}
|
|
204
|
+
const searchHints = [];
|
|
205
|
+
for (const term of topTerms(textFreq, maxKeywords))
|
|
206
|
+
uniquePush(searchHints, term, maxKeywords);
|
|
207
|
+
for (const layer of layers)
|
|
208
|
+
uniquePush(searchHints, layer.name, maxKeywords);
|
|
209
|
+
for (const block of blocks)
|
|
210
|
+
uniquePush(searchHints, block.name, maxKeywords);
|
|
211
|
+
for (const type of entityTypes)
|
|
212
|
+
uniquePush(searchHints, type.type, maxKeywords);
|
|
213
|
+
return {
|
|
214
|
+
summary: doc.summary,
|
|
215
|
+
layers,
|
|
216
|
+
entityTypes,
|
|
217
|
+
blocks,
|
|
218
|
+
text: {
|
|
219
|
+
keywords: topTerms(textFreq, maxKeywords),
|
|
220
|
+
samples: textSamples,
|
|
221
|
+
},
|
|
222
|
+
searchHints,
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
export async function getOverview(file, opts = {}, loadOpts = {}) {
|
|
226
|
+
return createDrawingOverview(await loadDrawing(file, loadOpts), opts);
|
|
227
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
export interface CachedSearchDoc {
|
|
2
|
+
id: number;
|
|
3
|
+
entityId: string;
|
|
4
|
+
type: string;
|
|
5
|
+
layer?: string;
|
|
6
|
+
text: string;
|
|
7
|
+
entity: unknown;
|
|
8
|
+
}
|
|
9
|
+
export interface SearchCacheData {
|
|
10
|
+
version: number;
|
|
11
|
+
fingerprint: string;
|
|
12
|
+
index: string;
|
|
13
|
+
docs: CachedSearchDoc[];
|
|
14
|
+
}
|
|
15
|
+
export declare function computeDrawingFingerprint(file: string): string;
|
|
16
|
+
export declare function loadSearchCache(file: string, fingerprint: string, cacheDir?: string): SearchCacheData | null;
|
|
17
|
+
export declare function saveSearchCache(file: string, data: Omit<SearchCacheData, "version">, cacheDir?: string): void;
|
|
@@ -0,0 +1,69 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { existsSync, mkdirSync, readFileSync, statSync, writeFileSync, } from "node:fs";
|
|
3
|
+
import { basename, dirname, join, resolve } from "node:path";
|
|
4
|
+
import { getCacheDir } from "../store.js";
|
|
5
|
+
import { stringifyJson } from "../utils/output.js";
|
|
6
|
+
const CACHE_VERSION = 1;
|
|
7
|
+
export function computeDrawingFingerprint(file) {
|
|
8
|
+
const stat = statSync(file);
|
|
9
|
+
return createHash("sha1")
|
|
10
|
+
.update(`${resolve(file)}:${stat.size}:${stat.mtimeMs}`)
|
|
11
|
+
.digest("hex");
|
|
12
|
+
}
|
|
13
|
+
function cacheFileFor(file, cacheDir) {
|
|
14
|
+
const root = cacheDir ?? getCacheDir();
|
|
15
|
+
const key = createHash("sha1").update(resolve(file)).digest("hex");
|
|
16
|
+
return join(root, "search", `${basename(file)}-${key}.search.json`);
|
|
17
|
+
}
|
|
18
|
+
function isRecord(value) {
|
|
19
|
+
return value !== null && typeof value === "object";
|
|
20
|
+
}
|
|
21
|
+
function isCachedSearchDoc(value) {
|
|
22
|
+
if (!isRecord(value))
|
|
23
|
+
return false;
|
|
24
|
+
return (typeof value.id === "number" &&
|
|
25
|
+
typeof value.entityId === "string" &&
|
|
26
|
+
typeof value.type === "string" &&
|
|
27
|
+
(value.layer === undefined || typeof value.layer === "string") &&
|
|
28
|
+
typeof value.text === "string" &&
|
|
29
|
+
value.entity !== undefined);
|
|
30
|
+
}
|
|
31
|
+
function parseSearchCache(value) {
|
|
32
|
+
if (!isRecord(value))
|
|
33
|
+
return null;
|
|
34
|
+
if (value.version !== CACHE_VERSION)
|
|
35
|
+
return null;
|
|
36
|
+
if (typeof value.fingerprint !== "string")
|
|
37
|
+
return null;
|
|
38
|
+
if (typeof value.index !== "string")
|
|
39
|
+
return null;
|
|
40
|
+
if (!Array.isArray(value.docs))
|
|
41
|
+
return null;
|
|
42
|
+
if (!value.docs.every(isCachedSearchDoc))
|
|
43
|
+
return null;
|
|
44
|
+
return {
|
|
45
|
+
version: CACHE_VERSION,
|
|
46
|
+
fingerprint: value.fingerprint,
|
|
47
|
+
index: value.index,
|
|
48
|
+
docs: value.docs,
|
|
49
|
+
};
|
|
50
|
+
}
|
|
51
|
+
export function loadSearchCache(file, fingerprint, cacheDir) {
|
|
52
|
+
const cachePath = cacheFileFor(file, cacheDir);
|
|
53
|
+
if (!existsSync(cachePath))
|
|
54
|
+
return null;
|
|
55
|
+
try {
|
|
56
|
+
const data = parseSearchCache(JSON.parse(readFileSync(cachePath, "utf-8")));
|
|
57
|
+
if (!data || data.fingerprint !== fingerprint)
|
|
58
|
+
return null;
|
|
59
|
+
return data;
|
|
60
|
+
}
|
|
61
|
+
catch {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
export function saveSearchCache(file, data, cacheDir) {
|
|
66
|
+
const cachePath = cacheFileFor(file, cacheDir);
|
|
67
|
+
mkdirSync(dirname(cachePath), { recursive: true });
|
|
68
|
+
writeFileSync(cachePath, stringifyJson({ version: CACHE_VERSION, ...data }));
|
|
69
|
+
}
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import type { DwgEntity, EntityFilter } from "../types.js";
|
|
2
|
+
import { type LoadOptions } from "./drawing.js";
|
|
3
|
+
export interface DwgSearchOptions extends EntityFilter {
|
|
4
|
+
query?: string;
|
|
5
|
+
limit?: number;
|
|
6
|
+
score?: boolean;
|
|
7
|
+
snippets?: boolean;
|
|
8
|
+
cacheDir?: string;
|
|
9
|
+
}
|
|
10
|
+
export interface DwgSearchResult {
|
|
11
|
+
entityId: string;
|
|
12
|
+
type: string;
|
|
13
|
+
layer?: string;
|
|
14
|
+
score: number;
|
|
15
|
+
matches: string[];
|
|
16
|
+
entity: DwgEntity;
|
|
17
|
+
}
|
|
18
|
+
export declare function searchDrawing(file: string, opts?: DwgSearchOptions, loadOpts?: LoadOptions): Promise<DwgSearchResult[]>;
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import MiniSearch from "minisearch";
|
|
2
|
+
import { loadDrawing } from "./drawing.js";
|
|
3
|
+
import { computeDrawingFingerprint, loadSearchCache, saveSearchCache, } from "./search-cache.js";
|
|
4
|
+
const SEARCH_FIELDS = ["entityId", "type", "layer", "text"];
|
|
5
|
+
const STORE_FIELDS = ["entityId", "type", "layer", "text"];
|
|
6
|
+
const DEFAULT_LIMIT = 30;
|
|
7
|
+
const MAX_MATCHES = 5;
|
|
8
|
+
function createSearchIndex() {
|
|
9
|
+
return new MiniSearch({
|
|
10
|
+
fields: [...SEARCH_FIELDS],
|
|
11
|
+
storeFields: [...STORE_FIELDS],
|
|
12
|
+
searchOptions: {
|
|
13
|
+
boost: { entityId: 3, type: 2, layer: 2, text: 1 },
|
|
14
|
+
fuzzy: 0.2,
|
|
15
|
+
prefix: true,
|
|
16
|
+
},
|
|
17
|
+
});
|
|
18
|
+
}
|
|
19
|
+
function loadSearchIndex(indexJson) {
|
|
20
|
+
return MiniSearch.loadJSON(indexJson, {
|
|
21
|
+
fields: [...SEARCH_FIELDS],
|
|
22
|
+
storeFields: [...STORE_FIELDS],
|
|
23
|
+
searchOptions: {
|
|
24
|
+
boost: { entityId: 3, type: 2, layer: 2, text: 1 },
|
|
25
|
+
fuzzy: 0.2,
|
|
26
|
+
prefix: true,
|
|
27
|
+
},
|
|
28
|
+
});
|
|
29
|
+
}
|
|
30
|
+
function stringifyValue(value) {
|
|
31
|
+
if (value === null || value === undefined)
|
|
32
|
+
return "";
|
|
33
|
+
if (["string", "number", "boolean", "bigint"].includes(typeof value)) {
|
|
34
|
+
return String(value);
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(value))
|
|
37
|
+
return value.map(stringifyValue).join(" ");
|
|
38
|
+
if (typeof value === "object") {
|
|
39
|
+
return Object.entries(value)
|
|
40
|
+
.map(([key, val]) => `${key} ${stringifyValue(val)}`)
|
|
41
|
+
.join(" ");
|
|
42
|
+
}
|
|
43
|
+
return "";
|
|
44
|
+
}
|
|
45
|
+
function textFields(entity) {
|
|
46
|
+
const values = [
|
|
47
|
+
entity.id,
|
|
48
|
+
entity.type,
|
|
49
|
+
entity.layer ?? "",
|
|
50
|
+
String(entity.data.text ?? ""),
|
|
51
|
+
String(entity.data.value ?? ""),
|
|
52
|
+
String(entity.data.name ?? ""),
|
|
53
|
+
String(entity.data.blockName ?? ""),
|
|
54
|
+
String(entity.data.block_name ?? ""),
|
|
55
|
+
stringifyValue(entity.data),
|
|
56
|
+
];
|
|
57
|
+
return values.filter((value) => value.trim().length > 0);
|
|
58
|
+
}
|
|
59
|
+
function buildDocs(doc) {
|
|
60
|
+
return doc.entities.map((entity, id) => ({
|
|
61
|
+
id,
|
|
62
|
+
entityId: entity.id,
|
|
63
|
+
type: entity.type,
|
|
64
|
+
layer: entity.layer,
|
|
65
|
+
text: textFields(entity).join(" "),
|
|
66
|
+
entity,
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
function buildIndex(docs) {
|
|
70
|
+
const index = createSearchIndex();
|
|
71
|
+
index.addAll(docs);
|
|
72
|
+
return index;
|
|
73
|
+
}
|
|
74
|
+
function matchesFilter(doc, opts) {
|
|
75
|
+
if (opts.type && doc.type.toLowerCase() !== opts.type.toLowerCase()) {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
if (opts.layer && doc.layer?.toLowerCase() !== opts.layer.toLowerCase()) {
|
|
79
|
+
return false;
|
|
80
|
+
}
|
|
81
|
+
return true;
|
|
82
|
+
}
|
|
83
|
+
function matchesFor(doc, query) {
|
|
84
|
+
if (!query)
|
|
85
|
+
return [];
|
|
86
|
+
const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
|
|
87
|
+
if (terms.length === 0)
|
|
88
|
+
return [];
|
|
89
|
+
return textFields(doc.entity)
|
|
90
|
+
.filter((chunk) => {
|
|
91
|
+
const lower = chunk.toLowerCase();
|
|
92
|
+
return terms.some((term) => lower.includes(term));
|
|
93
|
+
})
|
|
94
|
+
.slice(0, MAX_MATCHES);
|
|
95
|
+
}
|
|
96
|
+
async function loadSearchCorpus(file, opts, loadOpts) {
|
|
97
|
+
const fingerprint = computeDrawingFingerprint(file);
|
|
98
|
+
const cached = loadSearchCache(file, fingerprint, opts.cacheDir);
|
|
99
|
+
if (cached) {
|
|
100
|
+
return {
|
|
101
|
+
docs: cached.docs,
|
|
102
|
+
index: loadSearchIndex(cached.index),
|
|
103
|
+
};
|
|
104
|
+
}
|
|
105
|
+
const docs = buildDocs(await loadDrawing(file, loadOpts));
|
|
106
|
+
const index = buildIndex(docs);
|
|
107
|
+
saveSearchCache(file, { fingerprint, index: JSON.stringify(index), docs }, opts.cacheDir);
|
|
108
|
+
return { docs, index };
|
|
109
|
+
}
|
|
110
|
+
function resultScore(result) {
|
|
111
|
+
return Math.round((Number(result.score) || 1) * 10) / 10;
|
|
112
|
+
}
|
|
113
|
+
export async function searchDrawing(file, opts = {}, loadOpts = {}) {
|
|
114
|
+
const { docs, index } = await loadSearchCorpus(file, opts, loadOpts);
|
|
115
|
+
const query = opts.query?.trim();
|
|
116
|
+
const byId = new Map(docs.map((doc) => [doc.id, doc]));
|
|
117
|
+
const raw = query
|
|
118
|
+
? index.search(query).map((result) => ({
|
|
119
|
+
id: Number(result.id),
|
|
120
|
+
score: result.score,
|
|
121
|
+
}))
|
|
122
|
+
: docs.map((doc) => ({ id: doc.id, score: 1 }));
|
|
123
|
+
return raw
|
|
124
|
+
.map((result) => ({ result, doc: byId.get(result.id) }))
|
|
125
|
+
.filter((item) => Boolean(item.doc))
|
|
126
|
+
.filter(({ doc }) => matchesFilter(doc, opts))
|
|
127
|
+
.map(({ result, doc }) => ({
|
|
128
|
+
entityId: doc.entityId,
|
|
129
|
+
type: doc.type,
|
|
130
|
+
layer: doc.layer,
|
|
131
|
+
score: resultScore(result),
|
|
132
|
+
matches: opts.snippets === false ? [] : matchesFor(doc, query),
|
|
133
|
+
entity: doc.entity,
|
|
134
|
+
}))
|
|
135
|
+
.sort((a, b) => b.score - a.score)
|
|
136
|
+
.slice(0, opts.limit ?? DEFAULT_LIMIT);
|
|
137
|
+
}
|
package/dist/core/svg.js
ADDED
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
const DEFAULT_BOUNDS = { minX: 0, minY: 0, maxX: 100, maxY: 100 };
|
|
2
|
+
function data(entity) {
|
|
3
|
+
return entity.data;
|
|
4
|
+
}
|
|
5
|
+
function record(value) {
|
|
6
|
+
return value && typeof value === "object"
|
|
7
|
+
? value
|
|
8
|
+
: {};
|
|
9
|
+
}
|
|
10
|
+
function point(value) {
|
|
11
|
+
const obj = record(value);
|
|
12
|
+
const x = Number(obj.x ?? obj.X ?? obj[0]);
|
|
13
|
+
const y = Number(obj.y ?? obj.Y ?? obj[1]);
|
|
14
|
+
return Number.isFinite(x) && Number.isFinite(y) ? { x, y } : null;
|
|
15
|
+
}
|
|
16
|
+
function number(value) {
|
|
17
|
+
const parsed = Number(value);
|
|
18
|
+
return Number.isFinite(parsed) ? parsed : null;
|
|
19
|
+
}
|
|
20
|
+
function escapeXml(value) {
|
|
21
|
+
return value
|
|
22
|
+
.replaceAll("&", "&")
|
|
23
|
+
.replaceAll("<", "<")
|
|
24
|
+
.replaceAll(">", ">")
|
|
25
|
+
.replaceAll('"', """);
|
|
26
|
+
}
|
|
27
|
+
function y(value) {
|
|
28
|
+
return -value;
|
|
29
|
+
}
|
|
30
|
+
function renderLine(entity) {
|
|
31
|
+
const rec = data(entity);
|
|
32
|
+
const start = point(rec.start ?? rec.startPoint);
|
|
33
|
+
const end = point(rec.end ?? rec.endPoint);
|
|
34
|
+
if (!start || !end)
|
|
35
|
+
return null;
|
|
36
|
+
return `<line x1="${start.x}" y1="${y(start.y)}" x2="${end.x}" y2="${y(end.y)}" />`;
|
|
37
|
+
}
|
|
38
|
+
function renderCircle(entity) {
|
|
39
|
+
const rec = data(entity);
|
|
40
|
+
const center = point(rec.center);
|
|
41
|
+
const radius = number(rec.radius ?? rec.r);
|
|
42
|
+
if (!center || radius === null)
|
|
43
|
+
return null;
|
|
44
|
+
return `<circle cx="${center.x}" cy="${y(center.y)}" r="${radius}" />`;
|
|
45
|
+
}
|
|
46
|
+
function renderPolyline(entity) {
|
|
47
|
+
const vertices = Array.isArray(entity.data.vertices)
|
|
48
|
+
? entity.data.vertices.map(point).filter((p) => p !== null)
|
|
49
|
+
: [];
|
|
50
|
+
if (vertices.length === 0)
|
|
51
|
+
return null;
|
|
52
|
+
return `<polyline points="${vertices.map((p) => `${p.x},${y(p.y)}`).join(" ")}" />`;
|
|
53
|
+
}
|
|
54
|
+
function renderText(entity) {
|
|
55
|
+
const rec = data(entity);
|
|
56
|
+
const position = point(rec.position ?? rec.insertionPoint ?? rec.start);
|
|
57
|
+
if (!position)
|
|
58
|
+
return null;
|
|
59
|
+
const text = escapeXml(String(rec.text ?? rec.value ?? ""));
|
|
60
|
+
return `<text x="${position.x}" y="${y(position.y)}">${text}</text>`;
|
|
61
|
+
}
|
|
62
|
+
const RENDERERS = {
|
|
63
|
+
LINE: renderLine,
|
|
64
|
+
CIRCLE: renderCircle,
|
|
65
|
+
LWPOLYLINE: renderPolyline,
|
|
66
|
+
POLYLINE: renderPolyline,
|
|
67
|
+
TEXT: renderText,
|
|
68
|
+
MTEXT: renderText,
|
|
69
|
+
};
|
|
70
|
+
function rendererFor(entity) {
|
|
71
|
+
return RENDERERS[entity.type];
|
|
72
|
+
}
|
|
73
|
+
function boundsFor(doc) {
|
|
74
|
+
return doc.summary.bounds ?? DEFAULT_BOUNDS;
|
|
75
|
+
}
|
|
76
|
+
function viewBox(bounds) {
|
|
77
|
+
const width = Math.max(1, bounds.maxX - bounds.minX || DEFAULT_BOUNDS.maxX);
|
|
78
|
+
const height = Math.max(1, bounds.maxY - bounds.minY || DEFAULT_BOUNDS.maxY);
|
|
79
|
+
return `${bounds.minX} ${y(bounds.maxY)} ${width} ${height}`;
|
|
80
|
+
}
|
|
81
|
+
function metadata(unsupported, rendered) {
|
|
82
|
+
return escapeXml(JSON.stringify({ unsupported, rendered }));
|
|
83
|
+
}
|
|
84
|
+
export function renderSvg(doc) {
|
|
85
|
+
const bounds = boundsFor(doc);
|
|
86
|
+
const elements = [];
|
|
87
|
+
let unsupported = 0;
|
|
88
|
+
for (const entity of doc.entities) {
|
|
89
|
+
const renderer = rendererFor(entity);
|
|
90
|
+
const element = renderer?.(entity) ?? null;
|
|
91
|
+
if (element)
|
|
92
|
+
elements.push(element);
|
|
93
|
+
else
|
|
94
|
+
unsupported++;
|
|
95
|
+
}
|
|
96
|
+
const rendered = elements.length;
|
|
97
|
+
const svg = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox(bounds)}">
|
|
98
|
+
<g fill="none" stroke="currentColor" stroke-width="1">
|
|
99
|
+
${elements.join("\n ")}
|
|
100
|
+
</g>
|
|
101
|
+
<metadata>${metadata(unsupported, rendered)}</metadata>
|
|
102
|
+
</svg>
|
|
103
|
+
`;
|
|
104
|
+
return { svg, unsupported, rendered, bounds };
|
|
105
|
+
}
|
package/dist/index.d.ts
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export type { LibreDwgEditResult, LibreDwgJsonResult, LibreDwgViewResult, } from "./core/libredwg.js";
|
|
2
|
+
export type { DrawingOverview, DrawingOverviewOptions, OverviewBlock, OverviewEntityType, OverviewLayer, OverviewText, } from "./core/overview.js";
|
|
3
|
+
export type { DwgSearchOptions, DwgSearchResult } from "./core/search.js";
|
|
4
|
+
export { Dwg } from "./sdk.js";
|
|
5
|
+
export type { DwgBlock, DwgBounds, DwgDocument, DwgEntity, DwgFormat, DwgLayer, DwgSummary, EntityFilter, SvgResult, ThumbnailResult, } from "./types.js";
|
package/dist/index.js
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { Dwg } from "./sdk.js";
|
package/dist/main.d.ts
ADDED