@mlightcad/cad-simple-viewer 1.7.8 → 1.8.0-preview.1
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 -21
- package/README.md +204 -190
- package/dist/cad-simple-viewer-AcUiFileOpenSession-BCcqBAH9.js +2427 -0
- package/dist/cad-simple-viewer-AcUiFileOpenSession-drptdOYc.js +324 -0
- package/dist/cad-simple-viewer-file-open-panel.js +32 -0
- package/dist/cad-simple-viewer-file-open-panel.umd.cjs +1 -0
- package/dist/cad-simple-viewer.js +7938 -9803
- package/dist/cad-simple-viewer.umd.cjs +122 -444
- package/file-open-panel.ts +6 -0
- package/find.ts +7 -7
- package/icons.ts +7 -7
- package/lib/app/AcApOpenDatabaseOptions.d.ts +1 -1
- package/lib/app/AcApOpenDatabaseOptions.d.ts.map +1 -1
- package/lib/app/dataSource/AcApDataSourceIds.d.ts +5 -0
- package/lib/app/dataSource/AcApDataSourceIds.d.ts.map +1 -0
- package/lib/app/dataSource/AcApDataSourceIds.js +5 -0
- package/lib/app/dataSource/AcApDataSourceIds.js.map +1 -0
- package/lib/app/dataSource/AcApLocalDataSource.d.ts +1 -2
- package/lib/app/dataSource/AcApLocalDataSource.d.ts.map +1 -1
- package/lib/app/dataSource/AcApLocalDataSource.js +2 -2
- package/lib/app/dataSource/AcApLocalDataSource.js.map +1 -1
- package/lib/app/dataSource/AcApUrlDataSource.d.ts +1 -2
- package/lib/app/dataSource/AcApUrlDataSource.d.ts.map +1 -1
- package/lib/app/dataSource/AcApUrlDataSource.js +2 -2
- package/lib/app/dataSource/AcApUrlDataSource.js.map +1 -1
- package/lib/app/dataSource/acapBuildDataSourceMenu.js +1 -1
- package/lib/app/dataSource/acapBuildDataSourceMenu.js.map +1 -1
- package/lib/app/dataSource/index.d.ts +3 -2
- package/lib/app/dataSource/index.d.ts.map +1 -1
- package/lib/app/dataSource/index.js +3 -2
- package/lib/app/dataSource/index.js.map +1 -1
- package/lib/editor/input/handler/AcEdInputHandler.d.ts +7 -2
- package/lib/editor/input/handler/AcEdInputHandler.d.ts.map +1 -1
- package/lib/editor/input/handler/AcEdPointHandler.d.ts +8 -7
- package/lib/editor/input/handler/AcEdPointHandler.d.ts.map +1 -1
- package/lib/editor/input/handler/AcEdPointHandler.js +126 -30
- package/lib/editor/input/handler/AcEdPointHandler.js.map +1 -1
- package/lib/editor/input/ui/AcEdCommandLine.d.ts +28 -3
- package/lib/editor/input/ui/AcEdCommandLine.d.ts.map +1 -1
- package/lib/editor/input/ui/AcEdCommandLine.js +92 -26
- package/lib/editor/input/ui/AcEdCommandLine.js.map +1 -1
- package/lib/editor/input/ui/AcEdFloatingInput.d.ts +6 -0
- package/lib/editor/input/ui/AcEdFloatingInput.d.ts.map +1 -1
- package/lib/editor/input/ui/AcEdFloatingInput.js +32 -6
- package/lib/editor/input/ui/AcEdFloatingInput.js.map +1 -1
- package/lib/editor/input/ui/AcEdFloatingInputBoxes.d.ts +19 -2
- package/lib/editor/input/ui/AcEdFloatingInputBoxes.d.ts.map +1 -1
- package/lib/editor/input/ui/AcEdFloatingInputBoxes.js +23 -5
- package/lib/editor/input/ui/AcEdFloatingInputBoxes.js.map +1 -1
- package/lib/editor/input/ui/AcEdFloatingInputTypes.d.ts +18 -0
- package/lib/editor/input/ui/AcEdFloatingInputTypes.d.ts.map +1 -1
- package/lib/editor/input/ui/AcEdInputManager.d.ts +22 -0
- package/lib/editor/input/ui/AcEdInputManager.d.ts.map +1 -1
- package/lib/editor/input/ui/AcEdInputManager.js +56 -5
- package/lib/editor/input/ui/AcEdInputManager.js.map +1 -1
- package/lib/editor/input/ui/AcEdKeyboardUtil.d.ts +28 -0
- package/lib/editor/input/ui/AcEdKeyboardUtil.d.ts.map +1 -0
- package/lib/editor/input/ui/AcEdKeyboardUtil.js +59 -0
- package/lib/editor/input/ui/AcEdKeyboardUtil.js.map +1 -0
- package/lib/file-open-panel.d.ts +15 -0
- package/lib/file-open-panel.d.ts.map +1 -0
- package/lib/file-open-panel.js +33 -0
- package/lib/file-open-panel.js.map +1 -0
- package/lib/i18n/AcApI18n.d.ts.map +1 -1
- package/lib/i18n/AcApI18n.js +18 -9
- package/lib/i18n/AcApI18n.js.map +1 -1
- package/lib/i18n/ar/jig.d.ts +11 -0
- package/lib/i18n/ar/jig.d.ts.map +1 -1
- package/lib/i18n/ar/jig.js +9 -1
- package/lib/i18n/ar/jig.js.map +1 -1
- package/lib/i18n/cs/jig.d.ts +11 -0
- package/lib/i18n/cs/jig.d.ts.map +1 -1
- package/lib/i18n/cs/jig.js +11 -0
- package/lib/i18n/cs/jig.js.map +1 -1
- package/lib/i18n/en/jig.d.ts +11 -0
- package/lib/i18n/en/jig.d.ts.map +1 -1
- package/lib/i18n/en/jig.js +11 -0
- package/lib/i18n/en/jig.js.map +1 -1
- package/lib/i18n/tr/jig.d.ts +11 -0
- package/lib/i18n/tr/jig.d.ts.map +1 -1
- package/lib/i18n/tr/jig.js +11 -0
- package/lib/i18n/tr/jig.js.map +1 -1
- package/lib/i18n/zh/jig.d.ts +11 -0
- package/lib/i18n/zh/jig.d.ts.map +1 -1
- package/lib/i18n/zh/jig.js +11 -0
- package/lib/i18n/zh/jig.js.map +1 -1
- package/lib/ui/AcUiFileOpenLanding.d.ts +131 -0
- package/lib/ui/AcUiFileOpenLanding.d.ts.map +1 -0
- package/lib/ui/AcUiFileOpenLanding.js +248 -0
- package/lib/ui/AcUiFileOpenLanding.js.map +1 -0
- package/lib/ui/AcUiFileOpenPanel.d.ts +25 -5
- package/lib/ui/AcUiFileOpenPanel.d.ts.map +1 -1
- package/lib/ui/AcUiFileOpenPanel.js +25 -13
- package/lib/ui/AcUiFileOpenPanel.js.map +1 -1
- package/lib/ui/AcUiFileOpenSession.d.ts +119 -0
- package/lib/ui/AcUiFileOpenSession.d.ts.map +1 -0
- package/lib/ui/AcUiFileOpenSession.js +294 -0
- package/lib/ui/AcUiFileOpenSession.js.map +1 -0
- package/lib/ui/index.d.ts +2 -0
- package/lib/ui/index.d.ts.map +1 -1
- package/lib/ui/index.js +2 -0
- package/lib/ui/index.js.map +1 -1
- package/lib/view/AcEdViewKeyHandler.d.ts +17 -0
- package/lib/view/AcEdViewKeyHandler.d.ts.map +1 -1
- package/lib/view/AcEdViewKeyHandler.js +44 -1
- package/lib/view/AcEdViewKeyHandler.js.map +1 -1
- package/package.json +15 -5
- package/src/file-open-panel.ts +89 -0
- package/src/find/AcApFindNavigator.ts +190 -190
- package/src/find/findDrawing.ts +211 -211
- package/src/find/findText.ts +624 -624
- package/src/find/index.ts +2 -2
- package/src/ui/icons.ts +411 -411
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Subpath entry for `@mlightcad/cad-simple-viewer/file-open-panel`.
|
|
3
|
+
*
|
|
4
|
+
* Landing-page file open UI and data-source registry helpers that do **not**
|
|
5
|
+
* depend on {@link AcApDocManager}, three.js, or the command/view stack.
|
|
6
|
+
* Import this from demo / host landing pages so the full viewer stays lazy.
|
|
7
|
+
*/
|
|
8
|
+
import { AcApI18n } from './i18n/AcApI18n'
|
|
9
|
+
import arMain from './i18n/ar/main'
|
|
10
|
+
import csMain from './i18n/cs/main'
|
|
11
|
+
import enMain from './i18n/en/main'
|
|
12
|
+
import trMain from './i18n/tr/main'
|
|
13
|
+
import zhMain from './i18n/zh/main'
|
|
14
|
+
|
|
15
|
+
// Register only `main.*` strings needed by the panel (not command/jig catalogs).
|
|
16
|
+
AcApI18n.mergeLocaleMessage('en', { main: enMain })
|
|
17
|
+
AcApI18n.mergeLocaleMessage('zh', { main: zhMain })
|
|
18
|
+
AcApI18n.mergeLocaleMessage('tr', { main: trMain })
|
|
19
|
+
AcApI18n.mergeLocaleMessage('cs', { main: csMain })
|
|
20
|
+
AcApI18n.mergeLocaleMessage('ar', { main: arMain })
|
|
21
|
+
|
|
22
|
+
export {
|
|
23
|
+
AcApOpenViewMode,
|
|
24
|
+
type AcApOpenDatabaseOptions
|
|
25
|
+
} from './app/AcApOpenDatabaseOptions'
|
|
26
|
+
export {
|
|
27
|
+
acedIsCompactUiLayout,
|
|
28
|
+
ML_UI_COMPACT_MAX_WIDTH,
|
|
29
|
+
ML_UI_COMPACT_MEDIA_QUERY,
|
|
30
|
+
ML_UI_MOBILE_MAX_WIDTH,
|
|
31
|
+
ML_UI_MOBILE_MEDIA_QUERY
|
|
32
|
+
} from './editor/global/AcEdUiLayout'
|
|
33
|
+
export type {
|
|
34
|
+
AcApDataSource,
|
|
35
|
+
AcApDataSourceAccountProfile,
|
|
36
|
+
AcApDataSourceAuthState,
|
|
37
|
+
AcApDataSourceFile
|
|
38
|
+
} from './app/dataSource/AcApDataSource'
|
|
39
|
+
export {
|
|
40
|
+
ACAP_LOCAL_DATA_SOURCE_ID,
|
|
41
|
+
ACAP_URL_DATA_SOURCE_ID
|
|
42
|
+
} from './app/dataSource/AcApDataSourceIds'
|
|
43
|
+
export {
|
|
44
|
+
AcApDataSourceManager,
|
|
45
|
+
type AcApDataSourceManagerEvents
|
|
46
|
+
} from './app/dataSource/AcApDataSourceManager'
|
|
47
|
+
export {
|
|
48
|
+
acapBuildDataSourceMenu,
|
|
49
|
+
acapIsSingleLocalOpen,
|
|
50
|
+
type AcApDataSourceMenuAction,
|
|
51
|
+
type AcApDataSourceMenuItem
|
|
52
|
+
} from './app/dataSource/acapBuildDataSourceMenu'
|
|
53
|
+
export { acapInvokeDataSourceMenuAction } from './app/dataSource/acapInvokeDataSourceMenuAction'
|
|
54
|
+
export { acapOpenCenteredPopup } from './app/dataSource/acapOpenCenteredPopup'
|
|
55
|
+
export {
|
|
56
|
+
acedApplyUiTheme,
|
|
57
|
+
acedSubscribeUiTheme,
|
|
58
|
+
type AcEdApplyUiThemeOptions,
|
|
59
|
+
type AcEdUiTheme,
|
|
60
|
+
resolveUiTheme
|
|
61
|
+
} from './editor/global/AcEdUiTheme'
|
|
62
|
+
export { AcEdOpenMode } from './editor/view/AcEdOpenMode'
|
|
63
|
+
export {
|
|
64
|
+
AcApI18n,
|
|
65
|
+
type AcApLocale,
|
|
66
|
+
type AcApLocaleChangedEventArgs,
|
|
67
|
+
type AcApLocaleMessage,
|
|
68
|
+
type AcApLocaleMessages,
|
|
69
|
+
type AcApTranslateOptions
|
|
70
|
+
} from './i18n/AcApI18n'
|
|
71
|
+
export {
|
|
72
|
+
AcUiFileOpenLanding,
|
|
73
|
+
acuiAttachDataSources,
|
|
74
|
+
type AcUiFileOpenLandingOptions,
|
|
75
|
+
type AcUiFileOpenRequest
|
|
76
|
+
} from './ui/AcUiFileOpenLanding'
|
|
77
|
+
export {
|
|
78
|
+
AcUiFileOpenPanel,
|
|
79
|
+
type AcUiFileOpenPanelCardSize,
|
|
80
|
+
type AcUiFileOpenPanelOptions,
|
|
81
|
+
type AcUiFileOpenPanelResult
|
|
82
|
+
} from './ui/AcUiFileOpenPanel'
|
|
83
|
+
export {
|
|
84
|
+
AcUiFileOpenSession,
|
|
85
|
+
acuiCreateFileOpenSession,
|
|
86
|
+
type AcUiFileOpenDocManager,
|
|
87
|
+
type AcUiFileOpenSessionOptions,
|
|
88
|
+
type AcUiFileOpenSessionResult
|
|
89
|
+
} from './ui/AcUiFileOpenSession'
|
|
@@ -1,190 +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
|
-
}
|
|
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
|
+
}
|