@mlightcad/cad-simple-viewer 1.7.4 → 1.7.6-preview.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.
Files changed (190) hide show
  1. package/README.md +5 -0
  2. package/dist/cad-simple-viewer.js +11114 -8898
  3. package/dist/cad-simple-viewer.umd.cjs +432 -88
  4. package/dist/mtext-renderer-worker.js +2987 -2815
  5. package/find.ts +7 -0
  6. package/lib/app/AcApDocManager.d.ts +49 -1
  7. package/lib/app/AcApDocManager.d.ts.map +1 -1
  8. package/lib/app/AcApDocManager.js +150 -12
  9. package/lib/app/AcApDocManager.js.map +1 -1
  10. package/lib/app/AcApOpenDatabaseOptions.d.ts +33 -4
  11. package/lib/app/AcApOpenDatabaseOptions.d.ts.map +1 -1
  12. package/lib/app/AcApOpenFileDialog.d.ts +1 -1
  13. package/lib/app/AcApOpenFileDialog.d.ts.map +1 -1
  14. package/lib/app/AcApOpenFileDialog.js +22 -92
  15. package/lib/app/AcApOpenFileDialog.js.map +1 -1
  16. package/lib/app/AcApOpenFileProfiler.d.ts +14 -0
  17. package/lib/app/AcApOpenFileProfiler.d.ts.map +1 -1
  18. package/lib/app/AcApOpenFileProfiler.js +25 -3
  19. package/lib/app/AcApOpenFileProfiler.js.map +1 -1
  20. package/lib/app/AcApOpenFileProgressController.d.ts +17 -9
  21. package/lib/app/AcApOpenFileProgressController.d.ts.map +1 -1
  22. package/lib/app/AcApOpenFileProgressController.js +41 -20
  23. package/lib/app/AcApOpenFileProgressController.js.map +1 -1
  24. package/lib/app/AcApProgress.d.ts +5 -0
  25. package/lib/app/AcApProgress.d.ts.map +1 -1
  26. package/lib/app/AcApProgress.js +10 -0
  27. package/lib/app/AcApProgress.js.map +1 -1
  28. package/lib/app/dataSource/AcApDataSource.d.ts +97 -0
  29. package/lib/app/dataSource/AcApDataSource.d.ts.map +1 -0
  30. package/lib/app/dataSource/AcApDataSource.js +2 -0
  31. package/lib/app/dataSource/AcApDataSource.js.map +1 -0
  32. package/lib/app/dataSource/AcApDataSourceManager.d.ts +70 -0
  33. package/lib/app/dataSource/AcApDataSourceManager.d.ts.map +1 -0
  34. package/lib/app/dataSource/AcApDataSourceManager.js +84 -0
  35. package/lib/app/dataSource/AcApDataSourceManager.js.map +1 -0
  36. package/lib/app/dataSource/AcApLocalDataSource.d.ts +25 -0
  37. package/lib/app/dataSource/AcApLocalDataSource.d.ts.map +1 -0
  38. package/lib/app/dataSource/AcApLocalDataSource.js +153 -0
  39. package/lib/app/dataSource/AcApLocalDataSource.js.map +1 -0
  40. package/lib/app/dataSource/AcApUrlDataSource.d.ts +17 -0
  41. package/lib/app/dataSource/AcApUrlDataSource.d.ts.map +1 -0
  42. package/lib/app/dataSource/AcApUrlDataSource.js +214 -0
  43. package/lib/app/dataSource/AcApUrlDataSource.js.map +1 -0
  44. package/lib/app/dataSource/acapBuildDataSourceMenu.d.ts +42 -0
  45. package/lib/app/dataSource/acapBuildDataSourceMenu.d.ts.map +1 -0
  46. package/lib/app/dataSource/acapBuildDataSourceMenu.js +121 -0
  47. package/lib/app/dataSource/acapBuildDataSourceMenu.js.map +1 -0
  48. package/lib/app/dataSource/acapInvokeDataSourceMenuAction.d.ts +17 -0
  49. package/lib/app/dataSource/acapInvokeDataSourceMenuAction.d.ts.map +1 -0
  50. package/lib/app/dataSource/acapInvokeDataSourceMenuAction.js +74 -0
  51. package/lib/app/dataSource/acapInvokeDataSourceMenuAction.js.map +1 -0
  52. package/lib/app/dataSource/acapOpenCenteredPopup.d.ts +13 -0
  53. package/lib/app/dataSource/acapOpenCenteredPopup.d.ts.map +1 -0
  54. package/lib/app/dataSource/acapOpenCenteredPopup.js +35 -0
  55. package/lib/app/dataSource/acapOpenCenteredPopup.js.map +1 -0
  56. package/lib/app/dataSource/acapOpenDataSourceResult.d.ts +13 -0
  57. package/lib/app/dataSource/acapOpenDataSourceResult.d.ts.map +1 -0
  58. package/lib/app/dataSource/acapOpenDataSourceResult.js +90 -0
  59. package/lib/app/dataSource/acapOpenDataSourceResult.js.map +1 -0
  60. package/lib/app/dataSource/acapRunDataSourceMenuAction.d.ts +15 -0
  61. package/lib/app/dataSource/acapRunDataSourceMenuAction.d.ts.map +1 -0
  62. package/lib/app/dataSource/acapRunDataSourceMenuAction.js +74 -0
  63. package/lib/app/dataSource/acapRunDataSourceMenuAction.js.map +1 -0
  64. package/lib/app/dataSource/acapWithDataSourceBusy.d.ts +12 -0
  65. package/lib/app/dataSource/acapWithDataSourceBusy.d.ts.map +1 -0
  66. package/lib/app/dataSource/acapWithDataSourceBusy.js +88 -0
  67. package/lib/app/dataSource/acapWithDataSourceBusy.js.map +1 -0
  68. package/lib/app/dataSource/index.d.ts +11 -0
  69. package/lib/app/dataSource/index.d.ts.map +1 -0
  70. package/lib/app/dataSource/index.js +10 -0
  71. package/lib/app/dataSource/index.js.map +1 -0
  72. package/lib/app/index.d.ts +1 -0
  73. package/lib/app/index.d.ts.map +1 -1
  74. package/lib/app/index.js +1 -0
  75. package/lib/app/index.js.map +1 -1
  76. package/lib/app/openFileProgress.d.ts +8 -1
  77. package/lib/app/openFileProgress.d.ts.map +1 -1
  78. package/lib/app/openFileProgress.js +13 -2
  79. package/lib/app/openFileProgress.js.map +1 -1
  80. package/lib/app/syncCurrentSpaceFromTileMode.d.ts +25 -0
  81. package/lib/app/syncCurrentSpaceFromTileMode.d.ts.map +1 -0
  82. package/lib/app/syncCurrentSpaceFromTileMode.js +85 -0
  83. package/lib/app/syncCurrentSpaceFromTileMode.js.map +1 -0
  84. package/lib/editor/global/AcEdUiTheme.d.ts +10 -1
  85. package/lib/editor/global/AcEdUiTheme.d.ts.map +1 -1
  86. package/lib/editor/global/AcEdUiTheme.js +35 -2
  87. package/lib/editor/global/AcEdUiTheme.js.map +1 -1
  88. package/lib/editor/input/ui/AcEdCommandLine.d.ts.map +1 -1
  89. package/lib/editor/input/ui/AcEdCommandLine.js +7 -7
  90. package/lib/editor/input/ui/AcEdCommandLine.js.map +1 -1
  91. package/lib/find/AcApFindNavigator.d.ts +59 -0
  92. package/lib/find/AcApFindNavigator.d.ts.map +1 -0
  93. package/lib/find/AcApFindNavigator.js +145 -0
  94. package/lib/find/AcApFindNavigator.js.map +1 -0
  95. package/lib/find/findDrawing.d.ts +100 -0
  96. package/lib/find/findDrawing.d.ts.map +1 -0
  97. package/lib/find/findDrawing.js +183 -0
  98. package/lib/find/findDrawing.js.map +1 -0
  99. package/lib/find/findText.d.ts +147 -0
  100. package/lib/find/findText.d.ts.map +1 -0
  101. package/lib/find/findText.js +527 -0
  102. package/lib/find/findText.js.map +1 -0
  103. package/lib/find/index.d.ts +3 -0
  104. package/lib/find/index.d.ts.map +1 -0
  105. package/lib/find/index.js +3 -0
  106. package/lib/find/index.js.map +1 -0
  107. package/lib/i18n/ar/main.d.ts +26 -0
  108. package/lib/i18n/ar/main.d.ts.map +1 -1
  109. package/lib/i18n/ar/main.js +26 -0
  110. package/lib/i18n/ar/main.js.map +1 -1
  111. package/lib/i18n/cs/main.d.ts +26 -0
  112. package/lib/i18n/cs/main.d.ts.map +1 -1
  113. package/lib/i18n/cs/main.js +26 -0
  114. package/lib/i18n/cs/main.js.map +1 -1
  115. package/lib/i18n/en/main.d.ts +26 -0
  116. package/lib/i18n/en/main.d.ts.map +1 -1
  117. package/lib/i18n/en/main.js +26 -0
  118. package/lib/i18n/en/main.js.map +1 -1
  119. package/lib/i18n/tr/main.d.ts +26 -0
  120. package/lib/i18n/tr/main.d.ts.map +1 -1
  121. package/lib/i18n/tr/main.js +26 -0
  122. package/lib/i18n/tr/main.js.map +1 -1
  123. package/lib/i18n/zh/main.d.ts +26 -0
  124. package/lib/i18n/zh/main.d.ts.map +1 -1
  125. package/lib/i18n/zh/main.js +26 -0
  126. package/lib/i18n/zh/main.js.map +1 -1
  127. package/lib/index.d.ts +2 -0
  128. package/lib/index.d.ts.map +1 -1
  129. package/lib/index.js +2 -0
  130. package/lib/index.js.map +1 -1
  131. package/lib/spatialIndex/AcTrHierarchicalSpatialIndex.d.ts +29 -7
  132. package/lib/spatialIndex/AcTrHierarchicalSpatialIndex.d.ts.map +1 -1
  133. package/lib/spatialIndex/AcTrHierarchicalSpatialIndex.js +77 -38
  134. package/lib/spatialIndex/AcTrHierarchicalSpatialIndex.js.map +1 -1
  135. package/lib/ui/AcUiFileOpenPanel.d.ts +156 -0
  136. package/lib/ui/AcUiFileOpenPanel.d.ts.map +1 -0
  137. package/lib/ui/AcUiFileOpenPanel.js +695 -0
  138. package/lib/ui/AcUiFileOpenPanel.js.map +1 -0
  139. package/lib/ui/icons.d.ts +2 -0
  140. package/lib/ui/icons.d.ts.map +1 -1
  141. package/lib/ui/icons.js +2 -0
  142. package/lib/ui/icons.js.map +1 -1
  143. package/lib/ui/index.d.ts +1 -0
  144. package/lib/ui/index.d.ts.map +1 -1
  145. package/lib/ui/index.js +1 -0
  146. package/lib/ui/index.js.map +1 -1
  147. package/lib/util/AcApTextStyleFontPreload.d.ts +42 -0
  148. package/lib/util/AcApTextStyleFontPreload.d.ts.map +1 -0
  149. package/lib/util/AcApTextStyleFontPreload.js +149 -0
  150. package/lib/util/AcApTextStyleFontPreload.js.map +1 -0
  151. package/lib/util/index.d.ts +1 -0
  152. package/lib/util/index.d.ts.map +1 -1
  153. package/lib/util/index.js +1 -0
  154. package/lib/util/index.js.map +1 -1
  155. package/lib/view/AcTrConvertQueue.d.ts +78 -0
  156. package/lib/view/AcTrConvertQueue.d.ts.map +1 -0
  157. package/lib/view/AcTrConvertQueue.js +143 -0
  158. package/lib/view/AcTrConvertQueue.js.map +1 -0
  159. package/lib/view/AcTrGroupWcsBboxAssert.d.ts +9 -0
  160. package/lib/view/AcTrGroupWcsBboxAssert.d.ts.map +1 -1
  161. package/lib/view/AcTrGroupWcsBboxAssert.js +12 -0
  162. package/lib/view/AcTrGroupWcsBboxAssert.js.map +1 -1
  163. package/lib/view/AcTrLayer.d.ts.map +1 -1
  164. package/lib/view/AcTrLayer.js +5 -1
  165. package/lib/view/AcTrLayer.js.map +1 -1
  166. package/lib/view/AcTrLayout.d.ts +5 -0
  167. package/lib/view/AcTrLayout.d.ts.map +1 -1
  168. package/lib/view/AcTrLayout.js +26 -0
  169. package/lib/view/AcTrLayout.js.map +1 -1
  170. package/lib/view/AcTrLayoutView.d.ts.map +1 -1
  171. package/lib/view/AcTrLayoutView.js +3 -1
  172. package/lib/view/AcTrLayoutView.js.map +1 -1
  173. package/lib/view/AcTrProgressiveOpenFitController.d.ts +1 -0
  174. package/lib/view/AcTrProgressiveOpenFitController.d.ts.map +1 -1
  175. package/lib/view/AcTrProgressiveOpenFitController.js +2 -1
  176. package/lib/view/AcTrProgressiveOpenFitController.js.map +1 -1
  177. package/lib/view/AcTrScene.d.ts +7 -0
  178. package/lib/view/AcTrScene.d.ts.map +1 -1
  179. package/lib/view/AcTrScene.js +78 -48
  180. package/lib/view/AcTrScene.js.map +1 -1
  181. package/lib/view/AcTrView2d.d.ts +234 -18
  182. package/lib/view/AcTrView2d.d.ts.map +1 -1
  183. package/lib/view/AcTrView2d.js +1017 -296
  184. package/lib/view/AcTrView2d.js.map +1 -1
  185. package/package.json +17 -10
  186. package/src/find/AcApFindNavigator.ts +190 -0
  187. package/src/find/findDrawing.ts +211 -0
  188. package/src/find/findText.ts +595 -0
  189. package/src/find/index.ts +2 -0
  190. package/src/ui/icons.ts +4 -0
@@ -0,0 +1,595 @@
1
+ /**
2
+ * Text search over the entities of one layout (FIND).
3
+ *
4
+ * Shared by every UI (cad-simple-ui-plugin, the full Vue cad-viewer) so that
5
+ * both behave identically. Pure TypeScript with no runtime imports, published
6
+ * as the `@mlightcad/cad-simple-viewer/find` subpath.
7
+ *
8
+ * Searched fields:
9
+ * - `TEXT` — `textString`
10
+ * - `MTEXT` — `contents` with MTEXT formatting codes removed
11
+ * - `ATTRIB` — attribute value (`textString`) of every `INSERT` in the layout
12
+ * (the tag is reported for display but is not searched)
13
+ * - `TEXT` / `MTEXT` / `ATTRIB` inside the block definition of every `INSERT`
14
+ * (nested blocks included, anonymous/dynamic blocks too): title blocks,
15
+ * legends and labels are usually drawn this way
16
+ * - constant `ATTDEF` values inside block definitions (the only ATTDEFs that
17
+ * are drawn inside a block; templates get their value from an `ATTRIB`)
18
+ * - `DIMENSION` — the text of its dimension block when that block has a
19
+ * match, otherwise the override text (`dimensionText`)
20
+ * - `MULTILEADER` — its MTEXT contents
21
+ * - `ACAD_TABLE` — the cell text (from the table block when present,
22
+ * otherwise the cell strings)
23
+ *
24
+ * Every layout is visited in one synchronous pass per search. A hit inside a
25
+ * block, dimension or table selects the top-level entity of the layout that
26
+ * contains it, and carries the WCS extents of the matched text so the view
27
+ * can zoom to the text itself instead of to the whole block. Xrefs are not
28
+ * searched.
29
+ *
30
+ * The module is structural (no `instanceof`) so it can be unit tested with
31
+ * plain objects.
32
+ */
33
+
34
+ /** Maximum number of hits returned by one search. */
35
+ export const ACAP_FIND_MAX_RESULTS = 500
36
+
37
+ /** Kind of entity that produced a hit. */
38
+ export type AcApFindHitKind =
39
+ | 'text'
40
+ | 'mtext'
41
+ | 'attribute'
42
+ /** TEXT / MTEXT inside a block definition (via an `INSERT`). */
43
+ | 'block'
44
+ | 'dimension'
45
+ | 'leader'
46
+ | 'table'
47
+
48
+ /** Options for {@link acapFindTextInLayout}. */
49
+ export interface AcApFindOptions {
50
+ /**
51
+ * When true the query must match case and accents exactly. When false
52
+ * (default) case, accents (diacritics) and runs of whitespace are ignored.
53
+ */
54
+ matchCase?: boolean
55
+ /** Maximum hits to return; defaults to {@link ACAP_FIND_MAX_RESULTS}. */
56
+ maxResults?: number
57
+ }
58
+
59
+ /** One match returned by {@link acapFindTextInLayout}. */
60
+ export interface AcApFindHit {
61
+ /**
62
+ * Top-level entity of the layout that is selected (the `INSERT` for
63
+ * attributes and block text, the `DIMENSION` / `ACAD_TABLE` for their text).
64
+ */
65
+ entityId: string
66
+ /**
67
+ * Key of the text that matched, unique within the layout: the entity id
68
+ * for top-level text, the attribute id for attributes, and
69
+ * `<entityId>/<nested id>…` for text found inside a block, dimension or
70
+ * table (the same block text appears once per `INSERT`).
71
+ */
72
+ textEntityId: string
73
+ kind: AcApFindHitKind
74
+ /** Plain text that matched (formatting codes removed, whitespace collapsed). */
75
+ text: string
76
+ /** Attribute tag, for `attribute` hits. */
77
+ tag?: string
78
+ /** Block name of the owning `INSERT`, for `attribute` hits. */
79
+ blockName?: string
80
+ layer: string
81
+ /** Insertion point of the text in WCS. */
82
+ position: { x: number; y: number }
83
+ /**
84
+ * WCS extents of the matched text, when known (text nested in a block,
85
+ * dimension or table). Used to zoom to the text itself.
86
+ */
87
+ extents?: { min: { x: number; y: number }; max: { x: number; y: number } }
88
+ }
89
+
90
+ /** Result of one search. */
91
+ export interface AcApFindResult {
92
+ hits: AcApFindHit[]
93
+ /** True when more than `maxResults` entities matched. */
94
+ truncated: boolean
95
+ }
96
+
97
+ /* eslint-disable @typescript-eslint/no-explicit-any */
98
+ type EntityLike = any
99
+ /* eslint-enable @typescript-eslint/no-explicit-any */
100
+
101
+ /** Minimal database shape used by the search. */
102
+ export interface AcApFindDatabaseLike {
103
+ tables: {
104
+ blockTable: {
105
+ /** Block definition by name; used to search text inside blocks. */
106
+ getAt?(name: string): { newIterator(): Iterable<EntityLike> } | undefined
107
+ getIdAt(id: string):
108
+ | {
109
+ newIterator(): Iterable<EntityLike>
110
+ /** Used to resolve the matched entity for zoom extents. */
111
+ getIdAt?(id: string): EntityLike
112
+ }
113
+ | undefined
114
+ }
115
+ layerTable?: {
116
+ getAt(name: string): { isOff?: boolean; isFrozen?: boolean } | undefined
117
+ }
118
+ }
119
+ }
120
+
121
+ const DIACRITICS = /[̀-ͯ]/g
122
+ const WHITESPACE = /\s+/g
123
+
124
+ /**
125
+ * Normalizes text or a query for comparison.
126
+ *
127
+ * - always: NFKC, whitespace runs → one space, trimmed
128
+ * - unless `matchCase`: accents removed, lower-cased
129
+ */
130
+ export function acapNormalizeFindText(
131
+ value: string,
132
+ matchCase = false
133
+ ): string {
134
+ let result = value.normalize('NFKC').replace(WHITESPACE, ' ').trim()
135
+ if (!matchCase) {
136
+ result = result.normalize('NFD').replace(DIACRITICS, '').toLowerCase()
137
+ }
138
+ return result
139
+ }
140
+
141
+ /**
142
+ * Converts MTEXT contents to plain text: removes formatting codes
143
+ * (`\f…;`, `\H…;`, `\C…;`, `\L \l \O \o \K \k`, braces), turns `\P` into a
144
+ * space, decodes `\~`, `\U+XXXX`, stacked fractions and `%%c %%d %%p`.
145
+ */
146
+ export function acapMTextToPlainText(contents: string): string {
147
+ const text = contents.replace(
148
+ /\\(U\+[0-9a-fA-F]{4}|[PX~]|S[^;]*;|[fFHWTQCcAaRrp][^;]*;|[LlOoKk]|[\\{}])|[{}]/g,
149
+ (_match, code: string | undefined) => {
150
+ if (!code) return '' // bare group brace
151
+ if (code.startsWith('U+'))
152
+ return String.fromCharCode(parseInt(code.slice(2), 16))
153
+ if (code === 'P' || code === 'X' || code === '~') return ' '
154
+ if (code === '\\' || code === '{' || code === '}') return code
155
+ if (code[0] === 'S') {
156
+ const stack = /^S([^;^#/]*)[\^#/]?([^;]*);$/.exec(code)
157
+ return stack ? (stack[2] ? `${stack[1]}/${stack[2]}` : stack[1]) : ''
158
+ }
159
+ return '' // font, height, colour, underline… codes
160
+ }
161
+ )
162
+ return decodeControlCodes(text).replace(WHITESPACE, ' ').trim()
163
+ }
164
+
165
+ /** Decodes `%%c`, `%%d`, `%%p` special-character codes. */
166
+ function decodeControlCodes(value: string): string {
167
+ return value
168
+ .replace(/%%[cC]/g, 'Ø')
169
+ .replace(/%%[dD]/g, '°')
170
+ .replace(/%%[pP]/g, '±')
171
+ }
172
+
173
+ function plainText(value: unknown): string {
174
+ return typeof value === 'string'
175
+ ? decodeControlCodes(value)
176
+ .normalize('NFKC')
177
+ .replace(WHITESPACE, ' ')
178
+ .trim()
179
+ : ''
180
+ }
181
+
182
+ function isLayerSearchable(
183
+ db: AcApFindDatabaseLike,
184
+ layerName: string,
185
+ cache: Map<string, boolean>
186
+ ): boolean {
187
+ const cached = cache.get(layerName)
188
+ if (cached !== undefined) return cached
189
+ const layer = db.tables.layerTable?.getAt(layerName)
190
+ const searchable = !(layer?.isOff || layer?.isFrozen)
191
+ cache.set(layerName, searchable)
192
+ return searchable
193
+ }
194
+
195
+ function pointOf(entity: EntityLike): { x: number; y: number } {
196
+ const p = entity.position ??
197
+ entity.location ??
198
+ entity.textLocation ??
199
+ entity.textPosition ?? { x: 0, y: 0 }
200
+ return { x: Number(p.x) || 0, y: Number(p.y) || 0 }
201
+ }
202
+
203
+ /** 4x4 affine matrix, column-major (`AcGeMatrix3d.elements` layout). */
204
+ type Matrix = number[]
205
+
206
+ /** `a × b` for column-major 4x4 matrices. */
207
+ function multiply(a: Matrix, b: Matrix): Matrix {
208
+ const out = new Array<number>(16)
209
+ for (let col = 0; col < 4; col++) {
210
+ for (let row = 0; row < 4; row++) {
211
+ let sum = 0
212
+ for (let k = 0; k < 4; k++) sum += a[k * 4 + row] * b[col * 4 + k]
213
+ out[col * 4 + row] = sum
214
+ }
215
+ }
216
+ return out
217
+ }
218
+
219
+ function transformPoint(
220
+ m: Matrix | undefined,
221
+ p: { x: number; y: number; z?: number }
222
+ ): { x: number; y: number } {
223
+ if (!m) return { x: p.x, y: p.y }
224
+ const z = Number(p.z) || 0
225
+ return {
226
+ x: m[0] * p.x + m[4] * p.y + m[8] * z + m[12],
227
+ y: m[1] * p.x + m[5] * p.y + m[9] * z + m[13]
228
+ }
229
+ }
230
+
231
+ function matrixOf(entity: EntityLike): Matrix | undefined {
232
+ try {
233
+ const elements = entity.blockTransform?.elements
234
+ return Array.isArray(elements) && elements.length === 16
235
+ ? (elements as number[])
236
+ : undefined
237
+ } catch {
238
+ return undefined
239
+ }
240
+ }
241
+
242
+ /** WCS box of a (possibly nested) entity, or undefined when unknown. */
243
+ function extentsOf(
244
+ entity: EntityLike,
245
+ m: Matrix | undefined
246
+ ): AcApFindHit['extents'] {
247
+ let box: { min: { x: number; y: number }; max: { x: number; y: number } }
248
+ try {
249
+ box = entity.geometricExtents
250
+ } catch {
251
+ return undefined
252
+ }
253
+ if (!box || !isFinite(box.min?.x) || !isFinite(box.max?.x)) return undefined
254
+ if (!m) return { min: { ...box.min }, max: { ...box.max } }
255
+ const corners = [
256
+ { x: box.min.x, y: box.min.y },
257
+ { x: box.max.x, y: box.min.y },
258
+ { x: box.min.x, y: box.max.y },
259
+ { x: box.max.x, y: box.max.y }
260
+ ].map(p => transformPoint(m, p))
261
+ return {
262
+ min: {
263
+ x: Math.min(...corners.map(p => p.x)),
264
+ y: Math.min(...corners.map(p => p.y))
265
+ },
266
+ max: {
267
+ x: Math.max(...corners.map(p => p.x)),
268
+ y: Math.max(...corners.map(p => p.y))
269
+ }
270
+ }
271
+ }
272
+
273
+ /** Maximum nesting of blocks inside blocks that is searched. */
274
+ const MAX_BLOCK_DEPTH = 8
275
+
276
+ /** Top-level entity of the layout that owns nested text. */
277
+ interface Owner {
278
+ entity: EntityLike
279
+ kind: Extract<AcApFindHitKind, 'block' | 'dimension' | 'table'>
280
+ /** Effective layer of the owner (layer `0` entities inherit it). */
281
+ layer: string
282
+ }
283
+
284
+ /**
285
+ * Finds text in the entities of one block table record (a layout).
286
+ *
287
+ * Entities that are invisible or sit on an off/frozen layer are skipped so
288
+ * that results always correspond to something the user can see.
289
+ *
290
+ * @param db - Database owning the layout.
291
+ * @param layoutBtrId - Block table record id of the layout (model/paper space).
292
+ * @param query - User query; blank queries return no hits.
293
+ */
294
+ export function acapFindTextInLayout(
295
+ db: AcApFindDatabaseLike,
296
+ layoutBtrId: string,
297
+ query: string,
298
+ options: AcApFindOptions = {}
299
+ ): AcApFindResult {
300
+ const matchCase = options.matchCase === true
301
+ const max = options.maxResults ?? ACAP_FIND_MAX_RESULTS
302
+ const needle = acapNormalizeFindText(query, matchCase)
303
+ const result: AcApFindResult = { hits: [], truncated: false }
304
+ if (!needle) return result
305
+
306
+ const btr = db.tables.blockTable.getIdAt(layoutBtrId)
307
+ if (!btr) return result
308
+
309
+ const layers = new Map<string, boolean>()
310
+ const matches = (text: string) =>
311
+ text !== '' && acapNormalizeFindText(text, matchCase).includes(needle)
312
+ /** Set once the result cap is exceeded; stops every loop. */
313
+ let full = false
314
+
315
+ const push = (hit: AcApFindHit) => {
316
+ if (result.hits.length >= max) {
317
+ result.truncated = true
318
+ full = true
319
+ return
320
+ }
321
+ result.hits.push(hit)
322
+ }
323
+
324
+ /**
325
+ * Hit for text nested in `owner` (block, dimension or table). `path` keeps
326
+ * the key unique when the same block is inserted several times.
327
+ */
328
+ const pushNested = (
329
+ owner: Owner,
330
+ path: string,
331
+ entity: EntityLike,
332
+ kind: AcApFindHitKind,
333
+ text: string,
334
+ m: Matrix | undefined,
335
+ extra: Partial<AcApFindHit> = {}
336
+ ) => {
337
+ const extents = extentsOf(entity, m)
338
+ push({
339
+ entityId: owner.entity.objectId,
340
+ textEntityId: `${path}/${entity.objectId}`,
341
+ kind,
342
+ text,
343
+ layer: owner.layer,
344
+ position: transformPoint(m, pointOf(entity)),
345
+ ...(extents ? { extents } : {}),
346
+ ...extra
347
+ })
348
+ }
349
+
350
+ /** Searches the entities of a block definition drawn by `owner`. */
351
+ const visitBlock = (
352
+ entities: Iterable<EntityLike>,
353
+ owner: Owner,
354
+ m: Matrix | undefined,
355
+ path: string,
356
+ depth: number,
357
+ stack: Set<string>
358
+ ) => {
359
+ for (const entity of entities) {
360
+ if (full) return
361
+ const type: string | undefined = entity.dxfTypeName
362
+ if (
363
+ type !== 'TEXT' &&
364
+ type !== 'MTEXT' &&
365
+ type !== 'INSERT' &&
366
+ type !== 'ATTRIB' &&
367
+ type !== 'ATTDEF'
368
+ ) {
369
+ continue
370
+ }
371
+ if (entity.visibility === false) continue
372
+ // Only constant ATTDEFs are drawn inside a block (their default value
373
+ // is geometry); the others get their value from an ATTRIB of the INSERT.
374
+ if (
375
+ type === 'ATTDEF' &&
376
+ (entity.isInvisible ||
377
+ !(entity.isConst || entity.isConstMTextAttribute))
378
+ ) {
379
+ continue
380
+ }
381
+ // Layer 0 inside a block inherits the owner's layer (already checked).
382
+ if (
383
+ entity.layer &&
384
+ entity.layer !== '0' &&
385
+ !isLayerSearchable(db, entity.layer, layers)
386
+ ) {
387
+ continue
388
+ }
389
+ if (type === 'INSERT') {
390
+ if (depth >= MAX_BLOCK_DEPTH) continue
391
+ const inner = matrixOf(entity)
392
+ const nested = m && inner ? multiply(m, inner) : (inner ?? m)
393
+ const nestedPath = `${path}/${entity.objectId}`
394
+ for (const attribute of entity.attributeIterator?.() ?? []) {
395
+ if (full) return
396
+ if (attribute.visibility === false || attribute.isInvisible) continue
397
+ const text = plainText(attribute.textString)
398
+ if (!matches(text)) continue
399
+ // Attributes are stored in the coordinates of the block that holds
400
+ // the nested INSERT, like the INSERT itself.
401
+ pushNested(owner, nestedPath, attribute, 'block', text, m, {
402
+ tag: attribute.tag,
403
+ blockName: entity.blockName
404
+ })
405
+ }
406
+ const name: string | undefined = entity.blockName
407
+ if (!name || stack.has(name)) continue
408
+ const def = db.tables.blockTable.getAt?.(name)
409
+ if (!def) continue
410
+ stack.add(name)
411
+ visitBlock(
412
+ def.newIterator(),
413
+ owner,
414
+ nested,
415
+ nestedPath,
416
+ depth + 1,
417
+ stack
418
+ )
419
+ stack.delete(name)
420
+ continue
421
+ }
422
+ const isMText =
423
+ type === 'MTEXT' ||
424
+ (type === 'ATTDEF' && entity.isConstMTextAttribute && entity.mtext)
425
+ const text = isMText
426
+ ? acapMTextToPlainText(
427
+ String((type === 'MTEXT' ? entity : entity.mtext).contents ?? '')
428
+ )
429
+ : plainText(entity.textString)
430
+ if (!matches(text)) continue
431
+ pushNested(
432
+ owner,
433
+ path,
434
+ entity,
435
+ owner.kind,
436
+ text,
437
+ m,
438
+ type === 'ATTDEF' ? { tag: entity.tag } : {}
439
+ )
440
+ }
441
+ }
442
+
443
+ /** Block definition drawn by a DIMENSION / ACAD_TABLE, when present. */
444
+ const ownBlock = (entity: EntityLike) => {
445
+ try {
446
+ // ACAD_TABLE resolves its anonymous block itself.
447
+ const table = entity.blockTableRecord
448
+ if (table?.newIterator)
449
+ return table as { newIterator(): Iterable<EntityLike> }
450
+ const id = entity.dimBlockId
451
+ const byId = id ? db.tables.blockTable.getIdAt(id) : undefined
452
+ if (byId) return byId
453
+ const name = entity.blockName
454
+ return name ? db.tables.blockTable.getAt?.(name) : undefined
455
+ } catch {
456
+ return undefined
457
+ }
458
+ }
459
+
460
+ for (const entity of btr.newIterator()) {
461
+ if (full) break
462
+ const type: string | undefined = entity.dxfTypeName
463
+ if (
464
+ type !== 'TEXT' &&
465
+ type !== 'MTEXT' &&
466
+ type !== 'INSERT' &&
467
+ type !== 'DIMENSION' &&
468
+ type !== 'MULTILEADER' &&
469
+ type !== 'ACAD_TABLE'
470
+ ) {
471
+ continue
472
+ }
473
+ if (entity.visibility === false) continue
474
+ if (!isLayerSearchable(db, entity.layer, layers)) continue
475
+
476
+ if (type === 'INSERT') {
477
+ for (const attribute of entity.attributeIterator?.() ?? []) {
478
+ if (full) break
479
+ if (attribute.visibility === false || attribute.isInvisible) continue
480
+ const text = plainText(attribute.textString)
481
+ if (!matches(text)) continue
482
+ push({
483
+ entityId: entity.objectId,
484
+ textEntityId: attribute.objectId,
485
+ kind: 'attribute',
486
+ text,
487
+ tag: attribute.tag,
488
+ blockName: entity.blockName,
489
+ layer: entity.layer,
490
+ position: pointOf(attribute)
491
+ })
492
+ }
493
+ const name: string | undefined = entity.blockName
494
+ const def = name ? db.tables.blockTable.getAt?.(name) : undefined
495
+ if (def && !full) {
496
+ visitBlock(
497
+ def.newIterator(),
498
+ { entity, kind: 'block', layer: entity.layer },
499
+ matrixOf(entity),
500
+ String(entity.objectId),
501
+ 1,
502
+ new Set([name as string])
503
+ )
504
+ }
505
+ continue
506
+ }
507
+
508
+ if (type === 'DIMENSION' || type === 'ACAD_TABLE') {
509
+ const kind = type === 'DIMENSION' ? 'dimension' : 'table'
510
+ const owner: Owner = { entity, kind, layer: entity.layer }
511
+ const block = ownBlock(entity)
512
+ const before = result.hits.length
513
+ if (block) {
514
+ // Dimension / table blocks are already in WCS.
515
+ visitBlock(
516
+ block.newIterator(),
517
+ owner,
518
+ undefined,
519
+ String(entity.objectId),
520
+ 1,
521
+ new Set()
522
+ )
523
+ }
524
+ if (block && result.hits.length > before) continue
525
+ // No block, or a block without a matching text: use the stored strings
526
+ // (a dimension override can differ from what the block text shows).
527
+ const texts: string[] = []
528
+ if (type === 'DIMENSION') {
529
+ const override = String(entity.dimensionText ?? '')
530
+ if (override && override !== '<>' && override !== ' ') {
531
+ texts.push(acapMTextToPlainText(override.replace('<>', '')))
532
+ }
533
+ } else {
534
+ const rows = Number(entity.numRows) || 0
535
+ const cols = Number(entity.numColumns) || 0
536
+ for (let r = 0; r < rows; r++) {
537
+ for (let c = 0; c < cols; c++) {
538
+ try {
539
+ texts.push(
540
+ acapMTextToPlainText(String(entity.textString?.(r, c) ?? ''))
541
+ )
542
+ } catch {
543
+ // Cell without text content.
544
+ }
545
+ }
546
+ }
547
+ }
548
+ for (const text of texts) {
549
+ if (full) break
550
+ if (!matches(text)) continue
551
+ push({
552
+ entityId: entity.objectId,
553
+ textEntityId: `${entity.objectId}/${result.hits.length}`,
554
+ kind,
555
+ text,
556
+ layer: entity.layer,
557
+ position: pointOf(entity)
558
+ })
559
+ }
560
+ continue
561
+ }
562
+
563
+ const text =
564
+ type === 'MTEXT' || type === 'MULTILEADER'
565
+ ? acapMTextToPlainText(String(entity.contents ?? ''))
566
+ : plainText(entity.textString)
567
+ if (!matches(text)) continue
568
+ push({
569
+ entityId: entity.objectId,
570
+ textEntityId: entity.objectId,
571
+ kind:
572
+ type === 'MTEXT' ? 'mtext' : type === 'MULTILEADER' ? 'leader' : 'text',
573
+ text,
574
+ layer: entity.layer,
575
+ position: pointOf(entity)
576
+ })
577
+ }
578
+ return result
579
+ }
580
+
581
+ /** Returns a short excerpt of `text` centred on the first match of `query`. */
582
+ export function acapFindExcerpt(
583
+ text: string,
584
+ query: string,
585
+ matchCase = false,
586
+ width = 60
587
+ ): string {
588
+ if (text.length <= width) return text
589
+ const needle = acapNormalizeFindText(query, matchCase)
590
+ // Normalization may change length (accents, NFKC); fall back to the head.
591
+ const index = acapNormalizeFindText(text, matchCase).indexOf(needle)
592
+ const start = Math.max(0, Math.min(index - 15, text.length - width))
593
+ const slice = text.slice(start, start + width)
594
+ return `${start > 0 ? '…' : ''}${slice}${start + width < text.length ? '…' : ''}`
595
+ }
@@ -0,0 +1,2 @@
1
+ export * from './findDrawing'
2
+ export * from './findText'
package/src/ui/icons.ts CHANGED
@@ -202,6 +202,10 @@ export const ICON_MEASUREMENT_PANEL = ICON_MARKUP_PANEL
202
202
  export const ICON_MARKUP_IMPORT =
203
203
  '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 20 20" fill="none" aria-hidden="true"><path stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" d="M4.5 2h6.4L15.5 6.6V16.5A1.5 1.5 0 0 1 14 18H4.5A1.5 1.5 0 0 1 3 16.5V3.5A1.5 1.5 0 0 1 4.5 2z"/><path stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" stroke-linecap="round" d="M10.9 2v4.6h4.6"/><path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M10 5.8v6M7.2 9.6 10 12.5l2.8-2.9"/></svg>'
204
204
 
205
+ /** Open drawing file toolbar icon (folder + arrow). */
206
+ export const ICON_OPEN =
207
+ '<svg xmlns="http://www.w3.org/2000/svg" width="1em" height="1em" viewBox="0 0 20 20" fill="none" aria-hidden="true"><path stroke="currentColor" stroke-width="1.5" stroke-linejoin="round" d="M2.5 5.5V15a1.5 1.5 0 0 0 1.5 1.5h12A1.5 1.5 0 0 0 17.5 15V7.5A1.5 1.5 0 0 0 16 6H9.2L7.5 4H4A1.5 1.5 0 0 0 2.5 5.5z"/><path stroke="currentColor" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" d="M10 9.2v4.8M7.6 11.6 10 14l2.4-2.4"/></svg>'
208
+
205
209
  /**
206
210
  * Export icon (mirrored document + up arrow).
207
211
  * Inspired by Noun Project “export file” 8131193 (Web Buttons by Elin Erkani).