@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.
@@ -0,0 +1,234 @@
1
+ import { Arc, Circle, Color, Insert, Layer, Line, LineType, LineWeightType, LwPolyline, MText, Point, TextEntity, Transparency, XY, XYZ, } from "@node-projects/acad-ts";
2
+ import { EXIT_USER_ERROR } from "../utils/exit-codes.js";
3
+ import { readAcadFile, writeAcadDocument } from "./acad.js";
4
+ import { DwgCliError } from "./errors.js";
5
+ import { writeOutput } from "./files.js";
6
+ const degreesToRadians = (degrees) => (degrees * Math.PI) / 180;
7
+ function xyz(point) {
8
+ return new XYZ(point.x, point.y, point.z ?? 0);
9
+ }
10
+ function xy(point) {
11
+ return new XY(point.x, point.y);
12
+ }
13
+ function handleMatches(handle, entityId) {
14
+ const expected = entityId.toLowerCase();
15
+ const text = String(handle ?? "").toLowerCase();
16
+ if (text === expected)
17
+ return true;
18
+ const numeric = Number(handle);
19
+ return (Number.isFinite(numeric) && numeric.toString(16).toLowerCase() === expected);
20
+ }
21
+ function modelEntities(doc) {
22
+ return [...(doc.modelSpace?.entities ?? [])];
23
+ }
24
+ function findEntity(doc, entityId) {
25
+ const entity = modelEntities(doc).find((item) => handleMatches(item.handle, entityId));
26
+ if (!entity) {
27
+ throw new DwgCliError(`Entity not found: ${entityId}`, "ENTITY_NOT_FOUND", EXIT_USER_ERROR);
28
+ }
29
+ return entity;
30
+ }
31
+ function ensureLayer(doc, name) {
32
+ const existing = doc.layers?.tryGetValue(name);
33
+ if (existing)
34
+ return existing;
35
+ const layer = new Layer(name);
36
+ doc.layers?.add(layer);
37
+ return layer;
38
+ }
39
+ function ensureLineType(doc, name) {
40
+ const existing = doc.lineTypes?.tryGetValue(name);
41
+ if (existing)
42
+ return existing;
43
+ const lineType = new LineType(name);
44
+ doc.lineTypes?.add(lineType);
45
+ return lineType;
46
+ }
47
+ function acadColor(color) {
48
+ switch (color.mode) {
49
+ case "byLayer":
50
+ return Color.byLayer;
51
+ case "byBlock":
52
+ return Color.byBlock;
53
+ case "index":
54
+ return new Color(color.index);
55
+ case "rgb":
56
+ return new Color(color.r, color.g, color.b);
57
+ }
58
+ }
59
+ function lineWeight(weight) {
60
+ if (!Object.values(LineWeightType).includes(weight)) {
61
+ throw new DwgCliError(`Unsupported line weight: ${weight}`, "INVALID_LINE_WEIGHT", EXIT_USER_ERROR);
62
+ }
63
+ return weight;
64
+ }
65
+ function applyCommonOptions(doc, entity, options) {
66
+ if (options.layer)
67
+ entity.layer = ensureLayer(doc, options.layer);
68
+ if (options.color)
69
+ entity.color = acadColor(options.color);
70
+ }
71
+ function addEntity(doc, entity) {
72
+ if (!doc.modelSpace) {
73
+ throw new DwgCliError("Drawing has no model space to edit.", "MODEL_SPACE_NOT_FOUND", EXIT_USER_ERROR);
74
+ }
75
+ doc.modelSpace.entities.add(entity);
76
+ }
77
+ function deleteEntity(entity, entityId) {
78
+ const owner = entity.owner;
79
+ if (!owner?.entities?.remove) {
80
+ throw new DwgCliError(`Entity cannot be deleted: ${entityId}`, "ENTITY_NOT_DELETABLE", EXIT_USER_ERROR);
81
+ }
82
+ owner.entities.remove(entity);
83
+ }
84
+ function setAttribute(entity, tag, value, id) {
85
+ if (!(entity instanceof Insert)) {
86
+ throw new DwgCliError(`Entity is not a block insert: ${id}`, "ENTITY_NOT_INSERT", EXIT_USER_ERROR);
87
+ }
88
+ const attr = [...entity.attributes].find((item) => item.tag.toLowerCase() === tag.toLowerCase());
89
+ if (!attr) {
90
+ throw new DwgCliError(`Attribute not found on ${id}: ${tag}`, "ATTRIBUTE_NOT_FOUND", EXIT_USER_ERROR);
91
+ }
92
+ attr.value = value;
93
+ }
94
+ function applyOperation(doc, operation) {
95
+ switch (operation.kind) {
96
+ case "addPoint": {
97
+ const point = new Point(xyz(operation.at));
98
+ applyCommonOptions(doc, point, operation);
99
+ addEntity(doc, point);
100
+ return;
101
+ }
102
+ case "addLine": {
103
+ const line = new Line(xyz(operation.from), xyz(operation.to));
104
+ applyCommonOptions(doc, line, operation);
105
+ addEntity(doc, line);
106
+ return;
107
+ }
108
+ case "addCircle": {
109
+ const circle = new Circle();
110
+ circle.center = xyz(operation.center);
111
+ circle.radius = operation.radius;
112
+ applyCommonOptions(doc, circle, operation);
113
+ addEntity(doc, circle);
114
+ return;
115
+ }
116
+ case "addArc": {
117
+ const arc = new Arc(xyz(operation.center), operation.radius, degreesToRadians(operation.startAngleDegrees), degreesToRadians(operation.endAngleDegrees));
118
+ applyCommonOptions(doc, arc, operation);
119
+ addEntity(doc, arc);
120
+ return;
121
+ }
122
+ case "addText": {
123
+ const text = new TextEntity();
124
+ text.value = operation.text;
125
+ text.insertPoint = xyz(operation.at);
126
+ if (operation.height !== undefined)
127
+ text.height = operation.height;
128
+ if (operation.rotationDegrees !== undefined) {
129
+ text.rotation = degreesToRadians(operation.rotationDegrees);
130
+ }
131
+ applyCommonOptions(doc, text, operation);
132
+ addEntity(doc, text);
133
+ return;
134
+ }
135
+ case "addMText": {
136
+ const text = new MText();
137
+ text.value = operation.text;
138
+ text.insertPoint = xyz(operation.at);
139
+ if (operation.height !== undefined)
140
+ text.height = operation.height;
141
+ if (operation.width !== undefined)
142
+ text.rectangleWidth = operation.width;
143
+ if (operation.rotationDegrees !== undefined) {
144
+ text.rotation = degreesToRadians(operation.rotationDegrees);
145
+ }
146
+ applyCommonOptions(doc, text, operation);
147
+ addEntity(doc, text);
148
+ return;
149
+ }
150
+ case "addPolyline": {
151
+ const polyline = new LwPolyline(operation.points.map(xy));
152
+ polyline.isClosed = operation.closed ?? false;
153
+ applyCommonOptions(doc, polyline, operation);
154
+ addEntity(doc, polyline);
155
+ return;
156
+ }
157
+ }
158
+ const entity = findEntity(doc, operation.entityId);
159
+ switch (operation.kind) {
160
+ case "setText":
161
+ if (!(entity instanceof TextEntity || entity instanceof MText)) {
162
+ throw new DwgCliError(`Entity is not editable text: ${operation.entityId}`, "ENTITY_NOT_TEXT", EXIT_USER_ERROR);
163
+ }
164
+ entity.value = operation.text;
165
+ return;
166
+ case "setLayer":
167
+ entity.layer = ensureLayer(doc, operation.layer);
168
+ return;
169
+ case "setColor":
170
+ entity.color = acadColor(operation.color);
171
+ return;
172
+ case "setLineType":
173
+ entity.lineType = ensureLineType(doc, operation.lineType);
174
+ return;
175
+ case "setLineWeight":
176
+ entity.lineWeight = lineWeight(operation.weight);
177
+ return;
178
+ case "setTransparency":
179
+ entity.transparency = Transparency.fromAlphaValue(operation.alpha);
180
+ return;
181
+ case "setVisibility":
182
+ entity.isInvisible = !operation.visible;
183
+ return;
184
+ case "move":
185
+ entity.applyTranslation(new XYZ(operation.dx, operation.dy, operation.dz ?? 0));
186
+ return;
187
+ case "rotate": {
188
+ const origin = operation.origin ? xyz(operation.origin) : XYZ.zero;
189
+ entity.applyTranslation(new XYZ(-origin.x, -origin.y, -origin.z));
190
+ entity.applyRotation(XYZ.axisZ, degreesToRadians(operation.angleDegrees));
191
+ entity.applyTranslation(origin);
192
+ return;
193
+ }
194
+ case "scale":
195
+ entity.applyScaling(new XYZ(operation.factor, operation.factor, operation.factor), operation.origin ? xyz(operation.origin) : XYZ.zero);
196
+ return;
197
+ case "copy": {
198
+ const clone = entity.clone();
199
+ clone.applyTranslation(new XYZ(operation.dx ?? 0, operation.dy ?? 0, operation.dz ?? 0));
200
+ addEntity(doc, clone);
201
+ return;
202
+ }
203
+ case "matchProperties": {
204
+ const source = findEntity(doc, operation.sourceId);
205
+ entity.matchProperties(source);
206
+ return;
207
+ }
208
+ case "setAttribute":
209
+ setAttribute(entity, operation.tag, operation.value, operation.entityId);
210
+ return;
211
+ case "delete":
212
+ deleteEntity(entity, operation.entityId);
213
+ return;
214
+ }
215
+ }
216
+ export function editWithAcadTs(opts) {
217
+ if (opts.operations.length === 0) {
218
+ throw new DwgCliError("No acad-ts edit operation specified.", "MISSING_EDIT_OPERATION", EXIT_USER_ERROR);
219
+ }
220
+ if (opts.input === opts.output && !opts.overwrite) {
221
+ throw new DwgCliError("Refusing to edit in place without --overwrite. Provide --output or pass --overwrite.", "EDIT_REQUIRES_OUTPUT_OR_OVERWRITE", EXIT_USER_ERROR);
222
+ }
223
+ const { bytes, doc, format } = readAcadFile(opts.input);
224
+ for (const operation of opts.operations)
225
+ applyOperation(doc, operation);
226
+ writeOutput(opts.output, writeAcadDocument(doc, format, bytes.length));
227
+ return {
228
+ input: opts.input,
229
+ output: opts.output,
230
+ backend: "acad-ts",
231
+ operations: opts.operations,
232
+ changed: opts.operations.length,
233
+ };
234
+ }
@@ -0,0 +1,6 @@
1
+ export interface AcadSvgViewResult {
2
+ input: string;
3
+ svg: string;
4
+ backend: "acad-ts";
5
+ }
6
+ export declare function renderSvgWithAcadTs(file: string): AcadSvgViewResult;
@@ -0,0 +1,22 @@
1
+ import { Color } from "@node-projects/acad-ts";
2
+ import { readAcadFile, writeAcadSvg } from "./acad.js";
3
+ // acad-ts SvgWriter currently crashes when text resolves ByLayer/ByBlock
4
+ // through an index that has no RGB entry. Normalize render-only colors here;
5
+ // the source file is untouched because view reparses the document every time.
6
+ function makeEntityColorsRenderable(doc) {
7
+ for (const entity of doc.modelSpace?.entities ?? []) {
8
+ const candidate = entity;
9
+ if (candidate.color?.isByLayer || candidate.color?.isByBlock) {
10
+ candidate.color = Color.black;
11
+ }
12
+ }
13
+ }
14
+ export function renderSvgWithAcadTs(file) {
15
+ const { doc } = readAcadFile(file);
16
+ makeEntityColorsRenderable(doc);
17
+ return {
18
+ input: file,
19
+ svg: writeAcadSvg(doc),
20
+ backend: "acad-ts",
21
+ };
22
+ }
@@ -0,0 +1,11 @@
1
+ import { type CadDocument } from "@node-projects/acad-ts";
2
+ import type { DwgFormat } from "../types.js";
3
+ export interface AcadFile {
4
+ bytes: Uint8Array;
5
+ doc: CadDocument;
6
+ format: DwgFormat;
7
+ }
8
+ export declare function parseAcadDocument(bytes: Uint8Array, format: DwgFormat): CadDocument;
9
+ export declare function readAcadFile(file: string): AcadFile;
10
+ export declare function writeAcadDocument(doc: CadDocument, format: DwgFormat, originalBytes: number): Uint8Array;
11
+ export declare function writeAcadSvg(doc: CadDocument): string;
@@ -0,0 +1,82 @@
1
+ import { DwgReader, DwgWriter, DxfReader, DxfWriter, SvgWriter, } from "@node-projects/acad-ts";
2
+ import { EXIT_USER_ERROR } from "../utils/exit-codes.js";
3
+ import { arrayBufferFor } from "./adapter.js";
4
+ import { DwgCliError } from "./errors.js";
5
+ import { readCadFile } from "./files.js";
6
+ export function parseAcadDocument(bytes, format) {
7
+ try {
8
+ return format === "DWG"
9
+ ? DwgReader.readFromStream(arrayBufferFor(bytes))
10
+ : DxfReader.readFromStream(bytes);
11
+ }
12
+ catch (error) {
13
+ throw new DwgCliError(`Could not parse ${format}: ${error.message}`, "PARSE_FAILED", EXIT_USER_ERROR);
14
+ }
15
+ }
16
+ export function readAcadFile(file) {
17
+ const { bytes, format } = readCadFile(file);
18
+ return { bytes, format, doc: parseAcadDocument(bytes, format) };
19
+ }
20
+ function writeDwg(doc, originalBytes) {
21
+ let size = Math.max(1024 * 1024, originalBytes * 4);
22
+ const maxSize = 512 * 1024 * 1024;
23
+ while (size <= maxSize) {
24
+ const buffer = new ArrayBuffer(size);
25
+ const writer = new DwgWriter(buffer, doc);
26
+ writer.write();
27
+ if (writer.bytesWritten <= size) {
28
+ return new Uint8Array(buffer).slice(0, writer.bytesWritten);
29
+ }
30
+ size *= 2;
31
+ }
32
+ throw new DwgCliError("Could not write DWG: output exceeded the maximum supported buffer size.", "ACAD_WRITE_FAILED", EXIT_USER_ERROR);
33
+ }
34
+ function writeDxf(doc) {
35
+ let content = "";
36
+ DxfWriter.writeToStream({
37
+ write(value) {
38
+ content +=
39
+ typeof value === "string" ? value : new TextDecoder().decode(value);
40
+ },
41
+ flush() { },
42
+ close() { },
43
+ }, doc);
44
+ return new TextEncoder().encode(content);
45
+ }
46
+ export function writeAcadDocument(doc, format, originalBytes) {
47
+ try {
48
+ return format === "DWG" ? writeDwg(doc, originalBytes) : writeDxf(doc);
49
+ }
50
+ catch (error) {
51
+ if (error instanceof DwgCliError)
52
+ throw error;
53
+ throw new DwgCliError(`Could not write ${format}: ${error.message}`, "ACAD_WRITE_FAILED", EXIT_USER_ERROR);
54
+ }
55
+ }
56
+ function trimNullPadding(bytes) {
57
+ const end = bytes.indexOf(0);
58
+ return end === -1 ? bytes : bytes.slice(0, end);
59
+ }
60
+ function isOutputCapacityError(error) {
61
+ return /offset is out of bounds|too small|outside the bounds/i.test(error.message);
62
+ }
63
+ export function writeAcadSvg(doc) {
64
+ let size = 1024 * 1024;
65
+ const maxSize = 256 * 1024 * 1024;
66
+ while (size <= maxSize) {
67
+ const output = new Uint8Array(size);
68
+ try {
69
+ const writer = new SvgWriter(output, doc);
70
+ writer.write();
71
+ writer.dispose();
72
+ return new TextDecoder().decode(trimNullPadding(output));
73
+ }
74
+ catch (error) {
75
+ if (!isOutputCapacityError(error) || size >= maxSize) {
76
+ throw new DwgCliError(`Could not render SVG with acad-ts: ${error.message}`, "ACAD_SVG_FAILED", EXIT_USER_ERROR);
77
+ }
78
+ size *= 2;
79
+ }
80
+ }
81
+ throw new DwgCliError("Could not render SVG with acad-ts: output exceeded the maximum supported buffer size.", "ACAD_SVG_FAILED", EXIT_USER_ERROR);
82
+ }
@@ -6,9 +6,3 @@ export declare class AcadTsReader implements DrawingReader {
6
6
  parse(_file: string, bytes: Uint8Array, format: DwgFormat): Promise<unknown>;
7
7
  thumbnail(): Promise<ThumbnailResult | null>;
8
8
  }
9
- export declare class NativeLibreDwgReader implements DrawingReader {
10
- private readonly toolDir?;
11
- constructor(toolDir?: string | undefined);
12
- parse(file: string, _bytes: Uint8Array, _format: DwgFormat): Promise<unknown>;
13
- thumbnail(): Promise<ThumbnailResult | null>;
14
- }
@@ -1,7 +1,6 @@
1
1
  import { DwgReader, DxfReader } from "@node-projects/acad-ts";
2
2
  import { EXIT_UNAVAILABLE, EXIT_USER_ERROR } from "../utils/exit-codes.js";
3
3
  import { DwgCliError } from "./errors.js";
4
- import { readJsonWithLibreDwg } from "./libredwg.js";
5
4
  function asRecord(value) {
6
5
  return value && typeof value === "object"
7
6
  ? value
@@ -151,15 +150,3 @@ export class AcadTsReader {
151
150
  throw new DwgCliError("Thumbnail extraction is not available through acad-ts.", "THUMBNAIL_UNAVAILABLE", EXIT_UNAVAILABLE);
152
151
  }
153
152
  }
154
- export class NativeLibreDwgReader {
155
- toolDir;
156
- constructor(toolDir) {
157
- this.toolDir = toolDir;
158
- }
159
- async parse(file, _bytes, _format) {
160
- return readJsonWithLibreDwg(file, { toolDir: this.toolDir }).json;
161
- }
162
- async thumbnail() {
163
- throw new DwgCliError("Thumbnail extraction is not available through native LibreDWG.", "THUMBNAIL_UNAVAILABLE", EXIT_UNAVAILABLE);
164
- }
165
- }
@@ -1,7 +1,6 @@
1
1
  import type { DrawingReader, DwgBlock, DwgDocument, DwgEntity, DwgLayer, EntityFilter, SvgResult, ThumbnailResult } from "../types.js";
2
2
  export interface LoadOptions {
3
3
  reader?: DrawingReader;
4
- toolDir?: string;
5
4
  }
6
5
  export declare function loadDrawing(file: string, opts?: LoadOptions): Promise<DwgDocument>;
7
6
  export declare function getInfo(file: string, opts?: LoadOptions): Promise<import("../types.js").DwgSummary>;
@@ -15,4 +15,7 @@ export interface DwgSearchResult {
15
15
  matches: string[];
16
16
  entity: DwgEntity;
17
17
  }
18
+ export declare function stringifySearchValue(value: unknown): string;
19
+ export declare function entitySearchFields(entity: DwgEntity): string[];
20
+ export declare function searchMatchesFor(entity: DwgEntity, query: string | undefined): string[];
18
21
  export declare function searchDrawing(file: string, opts?: DwgSearchOptions, loadOpts?: LoadOptions): Promise<DwgSearchResult[]>;
@@ -1,48 +1,23 @@
1
- import MiniSearch from "minisearch";
2
1
  import { loadDrawing } from "./drawing.js";
3
2
  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
3
  const DEFAULT_LIMIT = 30;
7
4
  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) {
5
+ export function stringifySearchValue(value) {
31
6
  if (value === null || value === undefined)
32
7
  return "";
33
8
  if (["string", "number", "boolean", "bigint"].includes(typeof value)) {
34
9
  return String(value);
35
10
  }
36
11
  if (Array.isArray(value))
37
- return value.map(stringifyValue).join(" ");
12
+ return value.map(stringifySearchValue).join(" ");
38
13
  if (typeof value === "object") {
39
14
  return Object.entries(value)
40
- .map(([key, val]) => `${key} ${stringifyValue(val)}`)
15
+ .map(([key, val]) => `${key} ${stringifySearchValue(val)}`)
41
16
  .join(" ");
42
17
  }
43
18
  return "";
44
19
  }
45
- function textFields(entity) {
20
+ export function entitySearchFields(entity) {
46
21
  const values = [
47
22
  entity.id,
48
23
  entity.type,
@@ -52,24 +27,28 @@ function textFields(entity) {
52
27
  String(entity.data.name ?? ""),
53
28
  String(entity.data.blockName ?? ""),
54
29
  String(entity.data.block_name ?? ""),
55
- stringifyValue(entity.data),
30
+ stringifySearchValue(entity.data),
56
31
  ];
57
32
  return values.filter((value) => value.trim().length > 0);
58
33
  }
59
- function buildDocs(doc) {
34
+ function buildScanDocs(doc) {
60
35
  return doc.entities.map((entity, id) => ({
61
36
  id,
62
37
  entityId: entity.id,
63
38
  type: entity.type,
64
39
  layer: entity.layer,
65
- text: textFields(entity).join(" "),
40
+ text: entitySearchFields(entity).join(" "),
66
41
  entity,
67
42
  }));
68
43
  }
69
- function buildIndex(docs) {
70
- const index = createSearchIndex();
71
- index.addAll(docs);
72
- return index;
44
+ async function loadScanCorpus(file, opts, loadOpts) {
45
+ const fingerprint = computeDrawingFingerprint(file);
46
+ const cached = loadSearchCache(file, fingerprint, opts.cacheDir);
47
+ if (cached)
48
+ return cached.docs;
49
+ const docs = buildScanDocs(await loadDrawing(file, loadOpts));
50
+ saveSearchCache(file, { fingerprint, index: "scan", docs }, opts.cacheDir);
51
+ return docs;
73
52
  }
74
53
  function matchesFilter(doc, opts) {
75
54
  if (opts.type && doc.type.toLowerCase() !== opts.type.toLowerCase()) {
@@ -80,58 +59,69 @@ function matchesFilter(doc, opts) {
80
59
  }
81
60
  return true;
82
61
  }
83
- function matchesFor(doc, query) {
62
+ export function searchMatchesFor(entity, query) {
84
63
  if (!query)
85
64
  return [];
86
65
  const terms = query.toLowerCase().split(/\s+/).filter(Boolean);
87
66
  if (terms.length === 0)
88
67
  return [];
89
- return textFields(doc.entity)
68
+ return entitySearchFields(entity)
90
69
  .filter((chunk) => {
91
70
  const lower = chunk.toLowerCase();
92
71
  return terms.some((term) => lower.includes(term));
93
72
  })
94
73
  .slice(0, MAX_MATCHES);
95
74
  }
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
- };
75
+ function queryTerms(query) {
76
+ return query?.toLowerCase().split(/\s+/).filter(Boolean) ?? [];
77
+ }
78
+ function scanScore(doc, terms) {
79
+ if (terms.length === 0)
80
+ return 1;
81
+ const entityId = doc.entityId.toLowerCase();
82
+ const type = doc.type.toLowerCase();
83
+ const layer = doc.layer?.toLowerCase() ?? "";
84
+ const text = doc.text.toLowerCase();
85
+ let score = 0;
86
+ for (const term of terms) {
87
+ if (entityId.includes(term))
88
+ score += 3;
89
+ if (type.includes(term))
90
+ score += 2;
91
+ if (layer.includes(term))
92
+ score += 2;
93
+ if (text.includes(term))
94
+ score += 1;
104
95
  }
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 };
96
+ return score;
109
97
  }
110
- function resultScore(result) {
111
- return Math.round((Number(result.score) || 1) * 10) / 10;
98
+ function resultScore(score) {
99
+ return Math.round((score || 1) * 10) / 10;
112
100
  }
113
- export async function searchDrawing(file, opts = {}, loadOpts = {}) {
114
- const { docs, index } = await loadSearchCorpus(file, opts, loadOpts);
101
+ async function searchWithScan(file, opts = {}, loadOpts = {}) {
102
+ const docs = await loadScanCorpus(file, opts, loadOpts);
115
103
  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 }) => ({
104
+ const terms = queryTerms(query);
105
+ return docs
106
+ .filter((doc) => matchesFilter(doc, opts))
107
+ .map((doc) => ({ doc, score: scanScore(doc, terms) }))
108
+ .filter(({ score }) => terms.length === 0 || score > 0)
109
+ .map(({ doc, score }) => ({
128
110
  entityId: doc.entityId,
129
111
  type: doc.type,
130
112
  layer: doc.layer,
131
- score: resultScore(result),
132
- matches: opts.snippets === false ? [] : matchesFor(doc, query),
113
+ score: resultScore(score),
114
+ matches: opts.snippets === false ? [] : searchMatchesFor(doc.entity, query),
133
115
  entity: doc.entity,
134
116
  }))
135
117
  .sort((a, b) => b.score - a.score)
136
118
  .slice(0, opts.limit ?? DEFAULT_LIMIT);
137
119
  }
120
+ async function searchWithDiskIndex(file, opts, loadOpts) {
121
+ const { searchWithSqlite } = await import("./sqlite-search.js");
122
+ return searchWithSqlite(file, opts, loadOpts);
123
+ }
124
+ export async function searchDrawing(file, opts = {}, loadOpts = {}) {
125
+ return ((await searchWithDiskIndex(file, opts, loadOpts)) ??
126
+ searchWithScan(file, opts, loadOpts));
127
+ }
@@ -0,0 +1,3 @@
1
+ import { type LoadOptions } from "./drawing.js";
2
+ import { type DwgSearchOptions, type DwgSearchResult } from "./search.js";
3
+ export declare function searchWithSqlite(file: string, opts: DwgSearchOptions, loadOpts: LoadOptions): Promise<DwgSearchResult[] | null>;