@mlightcad/cad-simple-viewer 1.7.5 → 1.7.6
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/dist/cad-simple-viewer.js +9270 -8077
- package/dist/cad-simple-viewer.umd.cjs +83 -83
- package/dist/mtext-renderer-worker.js +2987 -2815
- package/find.ts +7 -0
- package/lib/app/AcApDocManager.d.ts +5 -0
- package/lib/app/AcApDocManager.d.ts.map +1 -1
- package/lib/app/AcApDocManager.js +39 -21
- package/lib/app/AcApDocManager.js.map +1 -1
- package/lib/app/AcApOpenDatabaseOptions.d.ts +8 -6
- package/lib/app/AcApOpenDatabaseOptions.d.ts.map +1 -1
- package/lib/app/AcApOpenFileProgressController.d.ts +17 -9
- package/lib/app/AcApOpenFileProgressController.d.ts.map +1 -1
- package/lib/app/AcApOpenFileProgressController.js +41 -20
- package/lib/app/AcApOpenFileProgressController.js.map +1 -1
- package/lib/app/AcApProgress.d.ts +5 -0
- package/lib/app/AcApProgress.d.ts.map +1 -1
- package/lib/app/AcApProgress.js +10 -0
- package/lib/app/AcApProgress.js.map +1 -1
- package/lib/app/openFileProgress.d.ts +8 -1
- package/lib/app/openFileProgress.d.ts.map +1 -1
- package/lib/app/openFileProgress.js +13 -2
- package/lib/app/openFileProgress.js.map +1 -1
- package/lib/app/syncCurrentSpaceFromTileMode.d.ts +25 -0
- package/lib/app/syncCurrentSpaceFromTileMode.d.ts.map +1 -0
- package/lib/app/syncCurrentSpaceFromTileMode.js +85 -0
- package/lib/app/syncCurrentSpaceFromTileMode.js.map +1 -0
- package/lib/editor/input/AcEdOsnapCenterMarks.d.ts +15 -2
- package/lib/editor/input/AcEdOsnapCenterMarks.d.ts.map +1 -1
- package/lib/editor/input/AcEdOsnapCenterMarks.js +63 -13
- package/lib/editor/input/AcEdOsnapCenterMarks.js.map +1 -1
- package/lib/editor/input/AcEdOsnapResolver.d.ts +14 -0
- package/lib/editor/input/AcEdOsnapResolver.d.ts.map +1 -1
- package/lib/editor/input/AcEdOsnapResolver.js +246 -32
- package/lib/editor/input/AcEdOsnapResolver.js.map +1 -1
- package/lib/editor/input/AcEdSelectionFilter.d.ts +6 -0
- package/lib/editor/input/AcEdSelectionFilter.d.ts.map +1 -1
- package/lib/editor/input/AcEdSelectionFilter.js +19 -2
- package/lib/editor/input/AcEdSelectionFilter.js.map +1 -1
- package/lib/editor/input/AcEditor.d.ts +6 -0
- package/lib/editor/input/AcEditor.d.ts.map +1 -1
- package/lib/editor/input/AcEditor.js +9 -0
- package/lib/editor/input/AcEditor.js.map +1 -1
- package/lib/editor/input/ui/AcEdCommandLine.d.ts +12 -0
- package/lib/editor/input/ui/AcEdCommandLine.d.ts.map +1 -1
- package/lib/editor/input/ui/AcEdCommandLine.js +54 -16
- package/lib/editor/input/ui/AcEdCommandLine.js.map +1 -1
- package/lib/editor/input/ui/AcEdInputManager.d.ts +15 -0
- package/lib/editor/input/ui/AcEdInputManager.d.ts.map +1 -1
- package/lib/editor/input/ui/AcEdInputManager.js +41 -10
- package/lib/editor/input/ui/AcEdInputManager.js.map +1 -1
- package/lib/editor/view/AcEdBaseView.d.ts +34 -0
- package/lib/editor/view/AcEdBaseView.d.ts.map +1 -1
- package/lib/editor/view/AcEdBaseView.js +35 -44
- package/lib/editor/view/AcEdBaseView.js.map +1 -1
- package/lib/editor/view/AcEdSpatialQueryResult.d.ts +7 -0
- package/lib/editor/view/AcEdSpatialQueryResult.d.ts.map +1 -1
- package/lib/editor/view/AcEdSpatialQueryResult.js.map +1 -1
- package/lib/find/AcApFindNavigator.d.ts +59 -0
- package/lib/find/AcApFindNavigator.d.ts.map +1 -0
- package/lib/find/AcApFindNavigator.js +145 -0
- package/lib/find/AcApFindNavigator.js.map +1 -0
- package/lib/find/findDrawing.d.ts +100 -0
- package/lib/find/findDrawing.d.ts.map +1 -0
- package/lib/find/findDrawing.js +183 -0
- package/lib/find/findDrawing.js.map +1 -0
- package/lib/find/findText.d.ts +147 -0
- package/lib/find/findText.d.ts.map +1 -0
- package/lib/find/findText.js +561 -0
- package/lib/find/findText.js.map +1 -0
- package/lib/find/index.d.ts +3 -0
- package/lib/find/index.d.ts.map +1 -0
- package/lib/find/index.js +3 -0
- package/lib/find/index.js.map +1 -0
- package/lib/i18n/ar/jig.d.ts +2 -0
- package/lib/i18n/ar/jig.d.ts.map +1 -1
- package/lib/i18n/ar/jig.js +1 -1
- package/lib/i18n/ar/jig.js.map +1 -1
- package/lib/i18n/cs/jig.d.ts +2 -0
- package/lib/i18n/cs/jig.d.ts.map +1 -1
- package/lib/i18n/cs/jig.js +2 -0
- package/lib/i18n/cs/jig.js.map +1 -1
- package/lib/i18n/en/jig.d.ts +2 -0
- package/lib/i18n/en/jig.d.ts.map +1 -1
- package/lib/i18n/en/jig.js +2 -0
- package/lib/i18n/en/jig.js.map +1 -1
- package/lib/i18n/tr/jig.d.ts +2 -0
- package/lib/i18n/tr/jig.d.ts.map +1 -1
- package/lib/i18n/tr/jig.js +2 -0
- package/lib/i18n/tr/jig.js.map +1 -1
- package/lib/i18n/zh/jig.d.ts +2 -0
- package/lib/i18n/zh/jig.d.ts.map +1 -1
- package/lib/i18n/zh/jig.js +2 -0
- package/lib/i18n/zh/jig.js.map +1 -1
- package/lib/index.d.ts +2 -0
- package/lib/index.d.ts.map +1 -1
- package/lib/index.js +2 -0
- package/lib/index.js.map +1 -1
- package/lib/spatialIndex/AcTrHierarchicalSpatialIndex.d.ts +29 -7
- package/lib/spatialIndex/AcTrHierarchicalSpatialIndex.d.ts.map +1 -1
- package/lib/spatialIndex/AcTrHierarchicalSpatialIndex.js +77 -38
- package/lib/spatialIndex/AcTrHierarchicalSpatialIndex.js.map +1 -1
- package/lib/view/AcTrConvertQueue.d.ts +78 -0
- package/lib/view/AcTrConvertQueue.d.ts.map +1 -0
- package/lib/view/AcTrConvertQueue.js +143 -0
- package/lib/view/AcTrConvertQueue.js.map +1 -0
- package/lib/view/AcTrGroupWcsBboxAssert.d.ts +9 -0
- package/lib/view/AcTrGroupWcsBboxAssert.d.ts.map +1 -1
- package/lib/view/AcTrGroupWcsBboxAssert.js +12 -0
- package/lib/view/AcTrGroupWcsBboxAssert.js.map +1 -1
- package/lib/view/AcTrLayer.d.ts.map +1 -1
- package/lib/view/AcTrLayer.js +5 -1
- package/lib/view/AcTrLayer.js.map +1 -1
- package/lib/view/AcTrLayout.d.ts.map +1 -1
- package/lib/view/AcTrLayout.js +15 -0
- package/lib/view/AcTrLayout.js.map +1 -1
- package/lib/view/AcTrLayoutView.d.ts.map +1 -1
- package/lib/view/AcTrLayoutView.js +3 -1
- package/lib/view/AcTrLayoutView.js.map +1 -1
- package/lib/view/AcTrProgressiveOpenFitController.d.ts +1 -0
- package/lib/view/AcTrProgressiveOpenFitController.d.ts.map +1 -1
- package/lib/view/AcTrProgressiveOpenFitController.js +2 -1
- package/lib/view/AcTrProgressiveOpenFitController.js.map +1 -1
- package/lib/view/AcTrView2d.d.ts +210 -20
- package/lib/view/AcTrView2d.d.ts.map +1 -1
- package/lib/view/AcTrView2d.js +962 -273
- package/lib/view/AcTrView2d.js.map +1 -1
- package/package.json +17 -10
- package/src/find/AcApFindNavigator.ts +190 -0
- package/src/find/findDrawing.ts +211 -0
- package/src/find/findText.ts +624 -0
- package/src/find/index.ts +2 -0
|
@@ -0,0 +1,190 @@
|
|
|
1
|
+
import {
|
|
2
|
+
acdbHostApplicationServices,
|
|
3
|
+
AcGeBox2d,
|
|
4
|
+
AcGePoint2d
|
|
5
|
+
} from '@mlightcad/data-model'
|
|
6
|
+
|
|
7
|
+
import type { AcApDocManager } from '../app/AcApDocManager'
|
|
8
|
+
import { acapFindHitBox, type AcApFindLayoutHit } from './findDrawing'
|
|
9
|
+
|
|
10
|
+
/** Options for {@link acapNavigateToFindHit}. */
|
|
11
|
+
export interface AcApFindNavigateOptions {
|
|
12
|
+
/**
|
|
13
|
+
* Activate the hit's layout when it is not the active one. Default `true`.
|
|
14
|
+
* When `false` a cross-layout hit is not navigated and `layout-mismatch`
|
|
15
|
+
* is returned.
|
|
16
|
+
*/
|
|
17
|
+
activateLayout?: boolean
|
|
18
|
+
/** Margin passed to `zoomTo`. Default `1.1`. */
|
|
19
|
+
margin?: number
|
|
20
|
+
/**
|
|
21
|
+
* Maximum time (ms) to wait for the freshly activated layout to finish
|
|
22
|
+
* converting before zooming and selecting. Default 15000.
|
|
23
|
+
*/
|
|
24
|
+
idleTimeoutMs?: number
|
|
25
|
+
/**
|
|
26
|
+
* Canvas area (CSS pixels from each edge) covered by UI such as a docked
|
|
27
|
+
* palette. The hit is framed and centred in the remaining visible part of
|
|
28
|
+
* the canvas so it never ends up hidden behind the palette.
|
|
29
|
+
*/
|
|
30
|
+
insets?: AcApFindViewInsets
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
/** Pixels of the canvas covered on each side; see {@link AcApFindNavigateOptions.insets}. */
|
|
34
|
+
export interface AcApFindViewInsets {
|
|
35
|
+
left?: number
|
|
36
|
+
right?: number
|
|
37
|
+
top?: number
|
|
38
|
+
bottom?: number
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** View methods used to keep the hit inside the uncovered canvas area. */
|
|
42
|
+
interface InsetView {
|
|
43
|
+
width?: number
|
|
44
|
+
height?: number
|
|
45
|
+
center?: { x: number; y: number }
|
|
46
|
+
worldToScreen?(p: { x: number; y: number }): { x: number; y: number }
|
|
47
|
+
screenToWorld?(p: { x: number; y: number }): { x: number; y: number }
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Zooms to `box` so that it fits, centred, in the part of the canvas that
|
|
52
|
+
* `insets` leaves visible. Falls back to a plain `zoomTo` when the view
|
|
53
|
+
* cannot convert coordinates or the visible part is too small to matter.
|
|
54
|
+
*/
|
|
55
|
+
function zoomToVisible(
|
|
56
|
+
view: InsetView & { zoomTo(box: AcGeBox2d, margin?: number): void },
|
|
57
|
+
box: { min: { x: number; y: number }; max: { x: number; y: number } },
|
|
58
|
+
margin: number,
|
|
59
|
+
insets: AcApFindViewInsets | undefined
|
|
60
|
+
) {
|
|
61
|
+
const width = Number(view.width) || 0
|
|
62
|
+
const height = Number(view.height) || 0
|
|
63
|
+
const left = Math.max(0, insets?.left ?? 0)
|
|
64
|
+
const right = Math.max(0, insets?.right ?? 0)
|
|
65
|
+
const top = Math.max(0, insets?.top ?? 0)
|
|
66
|
+
const bottom = Math.max(0, insets?.bottom ?? 0)
|
|
67
|
+
const visibleW = width - left - right
|
|
68
|
+
const visibleH = height - top - bottom
|
|
69
|
+
const usable =
|
|
70
|
+
width > 0 &&
|
|
71
|
+
height > 0 &&
|
|
72
|
+
left + right + top + bottom > 0 &&
|
|
73
|
+
visibleW >= width * 0.2 &&
|
|
74
|
+
visibleH >= height * 0.2 &&
|
|
75
|
+
!!view.worldToScreen &&
|
|
76
|
+
!!view.screenToWorld
|
|
77
|
+
if (!usable) {
|
|
78
|
+
view.zoomTo(
|
|
79
|
+
new AcGeBox2d(
|
|
80
|
+
new AcGePoint2d(box.min.x, box.min.y),
|
|
81
|
+
new AcGePoint2d(box.max.x, box.max.y)
|
|
82
|
+
),
|
|
83
|
+
margin
|
|
84
|
+
)
|
|
85
|
+
return
|
|
86
|
+
}
|
|
87
|
+
// Grow the box so that, fitted to the full canvas, its original part
|
|
88
|
+
// fits the visible part.
|
|
89
|
+
const cx = (box.min.x + box.max.x) / 2
|
|
90
|
+
const cy = (box.min.y + box.max.y) / 2
|
|
91
|
+
const halfW = ((box.max.x - box.min.x) / 2) * (width / visibleW)
|
|
92
|
+
const halfH = ((box.max.y - box.min.y) / 2) * (height / visibleH)
|
|
93
|
+
view.zoomTo(
|
|
94
|
+
new AcGeBox2d(
|
|
95
|
+
new AcGePoint2d(cx - halfW, cy - halfH),
|
|
96
|
+
new AcGePoint2d(cx + halfW, cy + halfH)
|
|
97
|
+
),
|
|
98
|
+
margin
|
|
99
|
+
)
|
|
100
|
+
// Then pan so the hit sits in the middle of the visible part.
|
|
101
|
+
const hitOnScreen = view.worldToScreen!({ x: cx, y: cy })
|
|
102
|
+
const targetX = left + visibleW / 2
|
|
103
|
+
const targetY = top + visibleH / 2
|
|
104
|
+
const newCenter = view.screenToWorld!({
|
|
105
|
+
x: width / 2 + (hitOnScreen.x - targetX),
|
|
106
|
+
y: height / 2 + (hitOnScreen.y - targetY)
|
|
107
|
+
})
|
|
108
|
+
if (isFinite(newCenter.x) && isFinite(newCenter.y)) {
|
|
109
|
+
view.center = new AcGePoint2d(newCenter.x, newCenter.y)
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Outcome of {@link acapNavigateToFindHit}.
|
|
115
|
+
*
|
|
116
|
+
* - `navigated`: layout (if needed) activated, view zoomed, entity selected
|
|
117
|
+
* - `layout-mismatch`: hit is in another layout and `activateLayout` is false
|
|
118
|
+
* - `layout-missing`: the hit's layout no longer exists in the drawing
|
|
119
|
+
* - `layout-switch-failed`: the layout manager refused / did not switch
|
|
120
|
+
* - `no-document`: no open drawing or view
|
|
121
|
+
* - `stale`: a newer navigation started while waiting; nothing was done
|
|
122
|
+
*/
|
|
123
|
+
export type AcApFindNavigateResult =
|
|
124
|
+
| 'navigated'
|
|
125
|
+
| 'layout-mismatch'
|
|
126
|
+
| 'layout-missing'
|
|
127
|
+
| 'layout-switch-failed'
|
|
128
|
+
| 'no-document'
|
|
129
|
+
| 'stale'
|
|
130
|
+
|
|
131
|
+
const navigationTokens = new WeakMap<object, number>()
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Navigates to a FIND hit: activates its layout when needed, zooms to the text
|
|
135
|
+
* and selects the owning entity.
|
|
136
|
+
*
|
|
137
|
+
* Same-layout hits are handled synchronously (before the first `await`), so
|
|
138
|
+
* callers may ignore the returned promise in that case. For a hit in another
|
|
139
|
+
* layout the layout is activated through the document's layout manager; the
|
|
140
|
+
* first-visit auto zoom of that layout is suppressed (it would otherwise
|
|
141
|
+
* override this zoom), then the function waits for the layout's entities to
|
|
142
|
+
* be converted before selecting, because selection highlighting needs the
|
|
143
|
+
* entity to be present in the scene.
|
|
144
|
+
*
|
|
145
|
+
* @param editor - Document manager that owns the active view and drawing.
|
|
146
|
+
* @param hit - Hit returned by `acapFindText`.
|
|
147
|
+
*/
|
|
148
|
+
export async function acapNavigateToFindHit(
|
|
149
|
+
editor: AcApDocManager,
|
|
150
|
+
hit: AcApFindLayoutHit,
|
|
151
|
+
options: AcApFindNavigateOptions = {}
|
|
152
|
+
): Promise<AcApFindNavigateResult> {
|
|
153
|
+
const view = editor.curView
|
|
154
|
+
const db = editor.curDocument?.database
|
|
155
|
+
if (!view || !db) return 'no-document'
|
|
156
|
+
|
|
157
|
+
const token = (navigationTokens.get(view) ?? 0) + 1
|
|
158
|
+
navigationTokens.set(view, token)
|
|
159
|
+
|
|
160
|
+
if (view.activeLayoutBtrId !== hit.layoutId) {
|
|
161
|
+
if (options.activateLayout === false) return 'layout-mismatch'
|
|
162
|
+
if (!db.tables.blockTable.getIdAt(hit.layoutId)) return 'layout-missing'
|
|
163
|
+
|
|
164
|
+
if (!view.isLayoutInitialized(hit.layoutId)) {
|
|
165
|
+
// We frame the layout ourselves; keep its first-visit auto zoom from
|
|
166
|
+
// running afterwards and overriding the zoom to the hit.
|
|
167
|
+
view.markLayoutAsInitialized(hit.layoutId)
|
|
168
|
+
}
|
|
169
|
+
acdbHostApplicationServices().layoutManager.setCurrentLayoutBtrId(
|
|
170
|
+
hit.layoutId,
|
|
171
|
+
db
|
|
172
|
+
)
|
|
173
|
+
if (view.activeLayoutBtrId !== hit.layoutId) return 'layout-switch-failed'
|
|
174
|
+
|
|
175
|
+
await view.waitUntilIdle(options.idleTimeoutMs ?? 15_000)
|
|
176
|
+
if (navigationTokens.get(view) !== token) return 'stale'
|
|
177
|
+
if (view.activeLayoutBtrId !== hit.layoutId) return 'stale'
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const box = acapFindHitBox(db, hit.layoutId, hit)
|
|
181
|
+
zoomToVisible(
|
|
182
|
+
view as unknown as Parameters<typeof zoomToVisible>[0],
|
|
183
|
+
box,
|
|
184
|
+
options.margin ?? 1.1,
|
|
185
|
+
options.insets
|
|
186
|
+
)
|
|
187
|
+
view.selectionSet.clear()
|
|
188
|
+
view.selectionSet.add(hit.entityId)
|
|
189
|
+
return 'navigated'
|
|
190
|
+
}
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Whole-drawing text search (FIND across layouts).
|
|
3
|
+
*
|
|
4
|
+
* Builds on {@link acapFindTextInLayout}: every layout of the drawing (model
|
|
5
|
+
* space and each paper space, in tab order) is searched with the same rules,
|
|
6
|
+
* and every hit is tagged with the layout it lives in so a UI can show it and
|
|
7
|
+
* navigate there.
|
|
8
|
+
*
|
|
9
|
+
* Pure TypeScript with no runtime imports (structural database shape), shared
|
|
10
|
+
* by cad-simple-ui-plugin and the Vue cad-viewer.
|
|
11
|
+
*/
|
|
12
|
+
import {
|
|
13
|
+
ACAP_FIND_MAX_RESULTS,
|
|
14
|
+
type AcApFindDatabaseLike,
|
|
15
|
+
type AcApFindHit,
|
|
16
|
+
type AcApFindOptions,
|
|
17
|
+
acapFindTextInLayout
|
|
18
|
+
} from './findText'
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Where to search.
|
|
22
|
+
*
|
|
23
|
+
* - `drawing` (default): every layout of the drawing
|
|
24
|
+
* - `layout`: only the layout that is currently active in the view
|
|
25
|
+
*/
|
|
26
|
+
export type AcApFindScope = 'drawing' | 'layout'
|
|
27
|
+
|
|
28
|
+
/** One layout of the drawing (model space or paper space). */
|
|
29
|
+
export interface AcApFindLayoutInfo {
|
|
30
|
+
/** Layout name as shown on the layout tab (e.g. `Model`, `Layout1`). */
|
|
31
|
+
name: string
|
|
32
|
+
/** Block table record id of the layout. */
|
|
33
|
+
btrId: string
|
|
34
|
+
/** Tab order; layouts are searched and listed in this order. */
|
|
35
|
+
tabOrder: number
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** A hit that also identifies the layout that contains it. */
|
|
39
|
+
export interface AcApFindLayoutHit extends AcApFindHit {
|
|
40
|
+
/** Block table record id of the layout containing the hit. */
|
|
41
|
+
layoutId: string
|
|
42
|
+
/** Name of the layout containing the hit. */
|
|
43
|
+
layoutName: string
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Result of one drawing-level search. */
|
|
47
|
+
export interface AcApFindDrawingResult {
|
|
48
|
+
hits: AcApFindLayoutHit[]
|
|
49
|
+
/** True when more than `maxResults` entities matched. */
|
|
50
|
+
truncated: boolean
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Options for {@link acapFindText}. */
|
|
54
|
+
export interface AcApFindDrawingOptions extends AcApFindOptions {
|
|
55
|
+
/** Search scope; defaults to `drawing`. */
|
|
56
|
+
scope?: AcApFindScope
|
|
57
|
+
/**
|
|
58
|
+
* Block table record id of the layout currently shown. Required for the
|
|
59
|
+
* `layout` scope; also used as the only layout when the drawing exposes no
|
|
60
|
+
* layout objects.
|
|
61
|
+
*/
|
|
62
|
+
currentLayoutId: string
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
interface LayoutLike {
|
|
66
|
+
layoutName?: string
|
|
67
|
+
tabOrder?: number
|
|
68
|
+
blockTableRecordId?: string
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Database shape used to enumerate layouts. */
|
|
72
|
+
export interface AcApFindLayoutsDatabaseLike {
|
|
73
|
+
objects?: {
|
|
74
|
+
layout?: { newIterator?(): Iterable<LayoutLike> }
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Lists the layouts of a drawing sorted by tab order (model space first in
|
|
80
|
+
* practice). Returns an empty list when the drawing has no layout objects.
|
|
81
|
+
*/
|
|
82
|
+
export function acapListFindLayouts(
|
|
83
|
+
db: AcApFindLayoutsDatabaseLike
|
|
84
|
+
): AcApFindLayoutInfo[] {
|
|
85
|
+
const table = db.objects?.layout
|
|
86
|
+
if (!table?.newIterator) return []
|
|
87
|
+
const layouts: AcApFindLayoutInfo[] = []
|
|
88
|
+
for (const layout of table.newIterator()) {
|
|
89
|
+
if (!layout.blockTableRecordId) continue
|
|
90
|
+
layouts.push({
|
|
91
|
+
name: layout.layoutName ?? '',
|
|
92
|
+
btrId: layout.blockTableRecordId,
|
|
93
|
+
tabOrder: Number(layout.tabOrder) || 0
|
|
94
|
+
})
|
|
95
|
+
}
|
|
96
|
+
return layouts.sort(
|
|
97
|
+
(a, b) => a.tabOrder - b.tabOrder || a.name.localeCompare(b.name)
|
|
98
|
+
)
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Finds text in the whole drawing or in the current layout only.
|
|
103
|
+
*
|
|
104
|
+
* Layouts are visited in tab order and the result cap is shared, so
|
|
105
|
+
* `truncated` is true as soon as any match beyond `maxResults` exists in any
|
|
106
|
+
* searched layout. Search runs on the database, not on the rendered scene, so
|
|
107
|
+
* layouts that were never opened in the view are searched too.
|
|
108
|
+
*/
|
|
109
|
+
export function acapFindText(
|
|
110
|
+
db: AcApFindDatabaseLike & AcApFindLayoutsDatabaseLike,
|
|
111
|
+
query: string,
|
|
112
|
+
options: AcApFindDrawingOptions
|
|
113
|
+
): AcApFindDrawingResult {
|
|
114
|
+
const max = options.maxResults ?? ACAP_FIND_MAX_RESULTS
|
|
115
|
+
const known = acapListFindLayouts(db)
|
|
116
|
+
const nameOf = (btrId: string) =>
|
|
117
|
+
known.find(layout => layout.btrId === btrId)?.name ?? ''
|
|
118
|
+
|
|
119
|
+
let targets: Array<{ btrId: string; name: string }>
|
|
120
|
+
if (options.scope === 'layout' || known.length === 0) {
|
|
121
|
+
targets = [
|
|
122
|
+
{ btrId: options.currentLayoutId, name: nameOf(options.currentLayoutId) }
|
|
123
|
+
]
|
|
124
|
+
} else {
|
|
125
|
+
targets = known.map(layout => ({ btrId: layout.btrId, name: layout.name }))
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
const result: AcApFindDrawingResult = { hits: [], truncated: false }
|
|
129
|
+
for (const target of targets) {
|
|
130
|
+
const found = acapFindTextInLayout(db, target.btrId, query, {
|
|
131
|
+
matchCase: options.matchCase,
|
|
132
|
+
maxResults: max - result.hits.length
|
|
133
|
+
})
|
|
134
|
+
for (const hit of found.hits) {
|
|
135
|
+
result.hits.push({
|
|
136
|
+
...hit,
|
|
137
|
+
layoutId: target.btrId,
|
|
138
|
+
layoutName: target.name
|
|
139
|
+
})
|
|
140
|
+
}
|
|
141
|
+
if (found.truncated) {
|
|
142
|
+
result.truncated = true
|
|
143
|
+
break
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
return result
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/** Axis-aligned 2D box in WCS (plain numbers; no geometry library needed). */
|
|
150
|
+
export interface AcApFindBox {
|
|
151
|
+
min: { x: number; y: number }
|
|
152
|
+
max: { x: number; y: number }
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
type XY = { x: number; y: number }
|
|
156
|
+
interface WithExtents {
|
|
157
|
+
geometricExtents?: { min: XY; max: XY }
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* View box for a hit: the text extents (`hit.extents` for nested text), widened so a short label does not
|
|
162
|
+
* zoom in absurdly far (at least ~10 text heights wide). Falls back to the
|
|
163
|
+
* hit position when the entity has no usable extents.
|
|
164
|
+
*
|
|
165
|
+
* @param layoutBtrId - Layout that contains the hit (`hit.layoutId`).
|
|
166
|
+
*/
|
|
167
|
+
export function acapFindHitBox(
|
|
168
|
+
db: AcApFindDatabaseLike,
|
|
169
|
+
layoutBtrId: string,
|
|
170
|
+
hit: AcApFindHit
|
|
171
|
+
): AcApFindBox {
|
|
172
|
+
const btr = db.tables.blockTable.getIdAt(layoutBtrId)
|
|
173
|
+
const owner = btr?.getIdAt?.(hit.entityId) as
|
|
174
|
+
| (WithExtents & {
|
|
175
|
+
attributeIterator?: () => Iterable<WithExtents & { objectId: string }>
|
|
176
|
+
})
|
|
177
|
+
| undefined
|
|
178
|
+
let target: WithExtents | undefined = owner
|
|
179
|
+
if (hit.kind === 'attribute' && owner?.attributeIterator) {
|
|
180
|
+
for (const attribute of owner.attributeIterator()) {
|
|
181
|
+
if (attribute.objectId === hit.textEntityId) {
|
|
182
|
+
target = attribute
|
|
183
|
+
break
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
let min: XY
|
|
188
|
+
let max: XY
|
|
189
|
+
try {
|
|
190
|
+
// Text nested in a block / dimension / table: the search already
|
|
191
|
+
// resolved its WCS extents (the owner's extents are the whole block).
|
|
192
|
+
const extents = hit.extents ?? target?.geometricExtents
|
|
193
|
+
if (!extents || !isFinite(extents.min.x) || !isFinite(extents.max.x)) {
|
|
194
|
+
throw new Error('no extents')
|
|
195
|
+
}
|
|
196
|
+
min = extents.min
|
|
197
|
+
max = extents.max
|
|
198
|
+
} catch {
|
|
199
|
+
min = max = hit.position
|
|
200
|
+
}
|
|
201
|
+
const cx = (min.x + max.x) / 2
|
|
202
|
+
const cy = (min.y + max.y) / 2
|
|
203
|
+
const w = max.x - min.x
|
|
204
|
+
const h = max.y - min.y
|
|
205
|
+
const width = Math.max(w * 3, h * 10, 1e-3)
|
|
206
|
+
const height = Math.max(h * 3, width / 4, 1e-3)
|
|
207
|
+
return {
|
|
208
|
+
min: { x: cx - width / 2, y: cy - height / 2 },
|
|
209
|
+
max: { x: cx + width / 2, y: cy + height / 2 }
|
|
210
|
+
}
|
|
211
|
+
}
|