@mlightcad/cad-simple-viewer 1.7.7-preview.1 → 1.7.7
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 +190 -190
- package/find.ts +7 -7
- package/icons.ts +7 -7
- package/package.json +3 -3
- 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
package/src/find/findDrawing.ts
CHANGED
|
@@ -1,211 +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
|
-
}
|
|
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
|
+
}
|