gis-view-webgl2 0.1.4 → 0.1.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.
Files changed (37) hide show
  1. package/README.md +10 -7
  2. package/dist/index.cjs +1 -1
  3. package/dist/index.d.ts +225 -20
  4. package/dist/index.js +1 -1
  5. package/docs/AI/345/257/271/350/257/235/346/261/207/346/200/273.md +109 -0
  6. package/docs/GisView.md +53 -53
  7. package/docs/ai/06-/344/274/232/350/257/235/347/264/242/345/274/225/344/270/216/345/244/215/346/237/245/346/226/271/346/263/225.md +91 -0
  8. package/docs/demos/ConnectDemo.vue +114 -3
  9. package/docs/demos/MinimapDemo.vue +216 -17
  10. package/docs/demos/SearchFilterDemo.vue +113 -1
  11. package/docs/demos/api-index.js +28 -20
  12. package/docs/demos/demos.js +1 -1
  13. package/docs/demos/gis-view-demo/GisViewDemoStage.vue +25 -16
  14. package/docs/demos/gis-view-demo/useGisViewDemo.js +27 -16
  15. package/docs/index.html +3 -3
  16. package/docs/internals/13-/351/271/260/347/234/274/345/233/276.md +8 -5
  17. package/docs/internals/16-01-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/212.md +18 -5
  18. package/docs/internals/16-03-/344/274/230/345/214/226/344/270/216/345/217/226/350/210/215-/344/270/213.md +11 -1
  19. package/docs/internals/18-/346/200/247/350/203/275/345/256/236/346/265/213/344/270/216WebGPU/350/257/204/344/274/260.md +73 -1
  20. package/docs/manual/03-/351/205/215/347/275/256/351/241/271.md +14 -5
  21. package/docs/manual/04-/344/272/213/344/273/266/344/270/216/345/233/236/350/260/203.md +2 -2
  22. package/docs/manual/05-/347/224/273/350/264/250/344/270/216/346/211/271/351/207/217/346/233/264/346/226/260.md +1 -1
  23. package/docs/manual/09-/351/200/232/351/201/223/344/270/216/345/244/232/345/233/236/347/272/277.md +1 -1
  24. package/docs/manual/10-/351/200/232/351/201/223/347/274/226/350/276/221/344/270/216/346/226/260/345/242/236.md +423 -3
  25. package/docs/manual/14-/350/247/206/345/233/276/344/270/216/347/255/233/351/200/211.md +99 -30
  26. package/docs/manual/16-/345/256/236/344/276/213/346/226/271/346/263/225/346/200/273/350/241/250.md +12 -7
  27. package/docs/manual/17-/345/217/202/346/225/260/351/200/237/346/237/245.md +14 -6
  28. package/docs/manual/18-/351/200/220/346/226/271/346/263/225/347/244/272/344/276/213.md +137 -6
  29. package/docs/manual/19-/350/257/273/346/225/260/344/270/216/346/213/226/346/213/275/351/242/204/350/247/210.md +4 -0
  30. package/docs/manual/20-/346/200/247/350/203/275/344/270/216/345/244/247/346/225/260/346/215/256.md +8 -3
  31. package/docs/manual/21-/350/257/264/346/230/216/344/270/216/345/270/270/350/247/201/351/227/256/351/242/230.md +6 -3
  32. package/docs/steps/README.md +6 -6
  33. package/docs/steps/selftest-demos.html +252 -9
  34. package/docs/steps/selftest-docs.html +3 -3
  35. package/docs/steps/selftest-full.html +357 -13
  36. package/docs/steps/selftest-perf.html +599 -382
  37. package/package.json +1 -1
@@ -28,6 +28,13 @@ const arrow = ref(true)
28
28
  const arrowSize = ref(12)
29
29
  const arrowPositionsText = ref('0.5')
30
30
  const circuitSpacing = ref(12)
31
+ const startPointId = ref('')
32
+ const dotsVisible = ref(true)
33
+ const dotSize = ref(5)
34
+ const startDotColor = ref('#2f6fd0')
35
+ const endDotColor = ref('#d9544d')
36
+ const dotShape = ref('circle')
37
+ const draftLayer = ref('above')
31
38
  const draft = ref(null)
32
39
  const actionText = ref('配置好线条之后点「开始连线」,依次点两个节点')
33
40
  const dataText = ref('正在创建视图…')
@@ -72,6 +79,10 @@ const createInitialLines = () => {
72
79
  const isChannel = computed(() => linkKind.value !== 'line')
73
80
  const circuits = computed(() => linkKind.value === 'channel-3' ? 3 : linkKind.value === 'channel-2' ? 2 : 1)
74
81
  const arrowPositions = computed(() => arrowPositionsText.value.split(',').map(value => Number(value.trim())).filter(value => Number.isFinite(value)))
82
+ const startPointLabel = computed(() => {
83
+ const point = points.find(item => String(item.id) === String(startPointId.value))
84
+ return point ? point.name ?? String(point.id) : ''
85
+ })
75
86
  const isDraftLive = computed(() => Boolean(draft.value) && ['start', 'from', 'ready', 'restart', 'pick'].includes(draft.value.phase))
76
87
  const canFinish = computed(() => draft.value?.phase === 'ready')
77
88
  const draftText = computed(() => {
@@ -110,10 +121,19 @@ const currentDraftStyle = () => ({
110
121
  color: lineColor.value,
111
122
  width: lineWidth.value,
112
123
  dot: 4,
124
+ dots: dotsVisible.value
125
+ ? {
126
+ size: dotSize.value,
127
+ shape: dotShape.value,
128
+ from: { color: startDotColor.value },
129
+ to: { color: endDotColor.value }
130
+ }
131
+ : false,
113
132
  arrow: arrow.value,
114
133
  arrowSize: arrowSize.value,
115
134
  arrowPositions: arrowPositions.value,
116
- circuitSpacing: circuitSpacing.value
135
+ circuitSpacing: circuitSpacing.value,
136
+ layer: draftLayer.value
117
137
  })
118
138
 
119
139
  /** 预览与真实落地共用同一份线条字段:颜色 / 线宽 / 方向箭头都从这里出发 */
@@ -129,6 +149,7 @@ const startDraft = () => {
129
149
  }
130
150
  view.endAddChannel()
131
151
  view.beginAddChannel({
152
+ ...(startPointId.value ? { from: startPointId.value } : {}),
132
153
  kind: isChannel.value ? 'channel' : 'line',
133
154
  circuits: circuits.value,
134
155
  routing: routing.value,
@@ -136,7 +157,9 @@ const startDraft = () => {
136
157
  channelDraftStyle: currentDraftStyle(),
137
158
  lineProps: currentLineProps()
138
159
  })
139
- actionText.value = `开始连线:先点起点,再点终点(${isChannel.value ? `通道 ${circuits.value} 回` : '独立线路'} / ${routing.value === 'orthogonal' ? '正交折线' : '直线'})`
160
+ actionText.value = startPointId.value
161
+ ? `开始连线:起点已指定为 ${startPointLabel.value},下一次点节点只选终点(${isChannel.value ? `通道 ${circuits.value} 回` : '独立线路'} / ${routing.value === 'orthogonal' ? '正交折线' : '直线'})`
162
+ : `开始连线:先点起点,再点终点(${isChannel.value ? `通道 ${circuits.value} 回` : '独立线路'} / ${routing.value === 'orthogonal' ? '正交折线' : '直线'})`
140
163
  pushLog('beginAddChannel', actionText.value)
141
164
  }
142
165
 
@@ -177,6 +200,60 @@ const commitDraft = () => {
177
200
  emit('notice', actionText.value)
178
201
  }
179
202
 
203
+ /** 直接把本次新增的线路 / 通道对象交给 commitAddChannel:不再先 addLine / addChannel */
204
+ const commitWithObject = () => {
205
+ if (!view || !canFinish.value) {
206
+ return
207
+ }
208
+ const current = view.getAddChannelDraft()
209
+ if (!current?.from || !current?.to) {
210
+ return
211
+ }
212
+ sequence += 1
213
+ const from = current.from.id
214
+ const to = current.to.id
215
+ const middle = middlePathOf(current)
216
+ const fullPath = fullPathOf(current)
217
+ let payload
218
+ if (current.kind === 'line') {
219
+ payload = {
220
+ id: `connect-object-line-${sequence}`,
221
+ name: `对象线路 ${sequence}`,
222
+ from,
223
+ to,
224
+ ...(fullPath ? { path: fullPath } : {}),
225
+ ...currentLineProps()
226
+ }
227
+ }
228
+ else {
229
+ const lineIds = Array.from({ length: current.circuits }, (_item, index) => `connect-object-line-${sequence}-${index + 1}`)
230
+ const channelId = `connect-object-channel-${sequence}`
231
+ payload = {
232
+ id: channelId,
233
+ name: `对象通道 ${sequence}`,
234
+ from,
235
+ to,
236
+ lineIds,
237
+ ...(middle ? { path: middle } : {}),
238
+ groups: [{ id: channelId, lineIds, ...(middle ? { path: middle } : {}) }]
239
+ }
240
+ }
241
+ const result = view.commitAddChannel(payload)
242
+ refreshState()
243
+ if (!result.ok) {
244
+ actionText.value = `commitAddChannel(对象) 没有落地:${result.reason ?? '未知原因'}`
245
+ }
246
+ else {
247
+ const lines = result.lines?.length ?? (result.line ? 1 : 0)
248
+ actionText.value = `commitAddChannel(对象) → ${result.kind === 'line' ? '线路对象' : '通道对象'} ${String(result.channelId ?? result.lineId ?? '')} · ${lines} 回线`
249
+ if (result.lineIds?.length) {
250
+ view.selectFeatures(result.lineIds)
251
+ }
252
+ }
253
+ pushLog('commitAddChannel(对象)', actionText.value)
254
+ emit('notice', actionText.value)
255
+ }
256
+
180
257
  const middlePathOf = current => (current.path?.length ? current.path.map(point => [point[0], point[1]]) : null)
181
258
  const fullPathOf = current => {
182
259
  const middle = middlePathOf(current)
@@ -267,7 +344,7 @@ const resetScene = () => {
267
344
  pushLog('reset', actionText.value)
268
345
  }
269
346
 
270
- watch([lineColor, lineWidth, arrow, arrowSize, arrowPositionsText, circuitSpacing], () => {
347
+ watch([lineColor, lineWidth, arrow, arrowSize, arrowPositionsText, circuitSpacing, dotsVisible, dotSize, startDotColor, endDotColor, dotShape, draftLayer], () => {
271
348
  if (view && isDraftLive.value) {
272
349
  view.updateAddChannelStyle(currentDraftStyle())
273
350
  draft.value = view.getAddChannelDraft()
@@ -333,6 +410,14 @@ onBeforeUnmount(() => {
333
410
  </select>
334
411
  </label>
335
412
  </div>
413
+ <div class="dv-row">
414
+ <label>指定起点
415
+ <select v-model="startPointId" data-demo="connect-start-point" :disabled="isDraftLive">
416
+ <option value="">点击时再选</option>
417
+ <option v-for="point in points" :key="String(point.id)" :value="String(point.id)">{{ point.name ?? point.id }}</option>
418
+ </select>
419
+ </label>
420
+ </div>
336
421
  <div class="dv-row">
337
422
  <label>线色 <input v-model="lineColor" type="color" data-demo="connect-color" /></label>
338
423
  <label>线宽 <input v-model.number="lineWidth" type="range" min="1" max="6" step="0.2" data-demo="connect-width" /> {{ lineWidth.toFixed(1) }}</label>
@@ -356,6 +441,31 @@ onBeforeUnmount(() => {
356
441
  <p class="dv-hint">草稿样式直接传给 <code>beginAddChannel({ channelDraftStyle })</code>;连线期间改颜色 / 线宽 / 箭头位置 / 回线间距会走 <code>updateAddChannelStyle()</code>,预览立即更新。正交折线会自动补一个 90 度拐点;落地线路用完整 path,通道只写中间拐点。</p>
357
442
  </div>
358
443
 
444
+ <div class="dv-group">
445
+ <h3>示例点与层级</h3>
446
+ <div class="dv-row">
447
+ <label><input v-model="dotsVisible" type="checkbox" data-demo="connect-dots-visible" /> 显示两端示例点</label>
448
+ <label>形状
449
+ <select v-model="dotShape" data-demo="connect-dot-shape">
450
+ <option value="circle">圆形</option>
451
+ <option value="square">方形</option>
452
+ </select>
453
+ </label>
454
+ <label>尺寸 <input v-model.number="dotSize" type="range" min="2" max="12" step="1" data-demo="connect-dot-size" /> {{ dotSize }}px</label>
455
+ </div>
456
+ <div class="dv-row">
457
+ <label>起点色 <input v-model="startDotColor" type="color" data-demo="connect-dot-from-color" :disabled="!dotsVisible" /></label>
458
+ <label>终点色 <input v-model="endDotColor" type="color" data-demo="connect-dot-to-color" :disabled="!dotsVisible" /></label>
459
+ <label>示例线路层级
460
+ <select v-model="draftLayer" data-demo="connect-draft-layer">
461
+ <option value="above">真实节点之上</option>
462
+ <option value="below-points">真实节点之下</option>
463
+ </select>
464
+ </label>
465
+ </div>
466
+ <p class="dv-hint">示例点始终落在被点击节点的正中心(多回线偏移不会带走它),可通过 <code>dots: false</code> 关闭,或用 <code>dots.from / dots.to</code> 分别配颜色、尺寸与形状。层级只作用于示例线 / 箭头 / <code>customDraw</code>;示例点仍画在真实节点之后,保证看得见。</p>
467
+ </div>
468
+
359
469
  <div class="dv-group">
360
470
  <h3>草稿与落地</h3>
361
471
  <div class="dv-row">
@@ -364,6 +474,7 @@ onBeforeUnmount(() => {
364
474
  </div>
365
475
  <div class="dv-row">
366
476
  <button type="button" class="dv-button" data-demo="connect-default" :disabled="!canFinish" @click="commitDraft">默认添加(commitAddChannel)</button>
477
+ <button type="button" class="dv-button" data-demo="connect-commit-object" :disabled="!canFinish" @click="commitWithObject">对象提交(通道 / 线路对象)</button>
367
478
  <button type="button" class="dv-button" data-demo="connect-add-lines" :disabled="!canFinish" @click="addWithLines">用 addLines 添加</button>
368
479
  <button type="button" class="dv-button" data-demo="connect-add-channels" :disabled="!canFinish" @click="addWithChannels">用 addChannels 添加</button>
369
480
  </div>
@@ -17,12 +17,19 @@ const enabled = ref(true)
17
17
  const size = ref('medium')
18
18
  const position = ref('bottom-right')
19
19
  const extent = ref('auto')
20
- const ruleMode = ref('voltage')
20
+ const ruleMode = ref('inherit')
21
21
  const levelMode = ref('all')
22
- const linkedSizes = ref(true)
22
+ const pointSizeScale = ref(1)
23
+ const lineWidthScale = ref(1)
24
+ const pointZoomScale = ref(0)
25
+ const lineZoomScale = ref(0)
23
26
  const channelCircuits = ref(true)
24
- const zoomable = ref(true)
27
+ const zoomMode = ref('manual')
28
+ const followZoomScale = ref(1)
25
29
  const dragAction = ref('pan')
30
+ const extentFollowContent = ref(false)
31
+ const extentFollowCamera = ref(false)
32
+ const offset = ref([0, 0])
26
33
  const maxPixelRatio = ref('2')
27
34
  const gridPlacement = ref('below-data')
28
35
  const customLayers = ref(true)
@@ -31,6 +38,7 @@ const zoomReadout = ref('—')
31
38
  const layerReadout = ref('—')
32
39
  const eventText = ref('—')
33
40
  const interactionReadout = ref('还没有手势')
41
+ const offsetReadout = ref('整图位移 0, 0')
34
42
 
35
43
  const SIZES = {
36
44
  small: [148, 104],
@@ -40,6 +48,8 @@ const SIZES = {
40
48
 
41
49
  const dataset = buildDemoData({ count: 64, channels: 3 })
42
50
  let view = null
51
+ let minimapHandle = null
52
+ let wholeMapDrag = null
43
53
  let layerPaints = { grid: 0, compass: 0, gridStage: '—' }
44
54
 
45
55
  const drawGrid = ({ context, width, height, stage }) => {
@@ -125,6 +135,9 @@ const minimapOptions = () => {
125
135
  margin: 12,
126
136
  padding: 0.05,
127
137
  extent: extent.value,
138
+ extentFollowContent: extentFollowContent.value,
139
+ extentFollowCamera: extentFollowCamera.value,
140
+ offset: [...offset.value],
128
141
  background: 'inherit',
129
142
  backgroundStyle: 'rgba(255, 255, 255, 0.94)',
130
143
  borderColor: '#c7d8eb',
@@ -140,16 +153,19 @@ const minimapOptions = () => {
140
153
  levels: levelMode.value === 'top' ? [2] : undefined,
141
154
  cascade: 'both',
142
155
  layered: true,
143
- pointSizeScale: 1,
144
- lineWidthScale: 1,
145
- pointMapZoomScale: linkedSizes.value ? 0.5 : 0,
146
- lineMapZoomScale: linkedSizes.value ? 0.5 : 0,
156
+ // 自己规则 / 沿用主图都乘这一份;与主图相机倍数、小图自身倍数都无关
157
+ pointSizeScale: Number(pointSizeScale.value),
158
+ lineWidthScale: Number(lineWidthScale.value),
159
+ // 只有小图自身放大时才按这两个指数联动;沿用主图的节点 / 线路也一样
160
+ pointZoomScale: Number(pointZoomScale.value),
161
+ lineZoomScale: Number(lineZoomScale.value),
147
162
  boxStyle: { fill: '#3678bf', fillOpacity: 0.16, stroke: '#3678bf', strokeWidth: 1, strokeStyle: 'dashed', radius: 3 },
148
163
  selectStyle: { fill: '#f0a43c', stroke: '#f0a43c' },
149
164
  interactive: true,
150
165
  onInteraction: handleMinimapInteraction,
151
166
  longPressDelay: 180,
152
- zoomable: zoomable.value,
167
+ zoomable: zoomMode.value === 'manual' ? true : zoomMode.value === 'follow' ? 'follow' : false,
168
+ followZoomScale: Number(followZoomScale.value),
153
169
  minZoom: 0.7,
154
170
  maxZoom: 6,
155
171
  zoomStep: 1.35,
@@ -168,10 +184,15 @@ const refreshReadout = () => {
168
184
  const sizeText = root ? `${Math.round(root.getBoundingClientRect().width)}×${Math.round(root.getBoundingClientRect().height)}` : '未挂载'
169
185
  readout.value = `${sizeText} · 备份 ${canvas?.width ?? 0}×${canvas?.height ?? 0} · 像素比 ${ratio ? ratio.toFixed(2) : '—'} · 范围 ${extent.value} · 自定义层 ${options.layers.length}`
170
186
  if (view) {
171
- zoomReadout.value = `小图倍数 ${view.getMinimapZoom().toFixed(2)} · ${zoomable.value ? '滚轮缩放小图' : '滚轮缩放主图'}`
187
+ zoomReadout.value = `小图倍数 ${view.getMinimapZoom().toFixed(2)} · ${zoomModeLabel.value}`
172
188
  }
189
+ offsetReadout.value = `整图位移 ${Math.round(offset.value[0])}, ${Math.round(offset.value[1])}`
173
190
  }
174
191
 
192
+ const zoomModeLabel = computed(() => zoomMode.value === 'manual'
193
+ ? '手动缩放小图'
194
+ : zoomMode.value === 'follow' ? `跟随主图(指数 ${Number(followZoomScale.value).toFixed(1)})` : '不缩放小图,滚轮缩放主图')
195
+
175
196
  const applyMinimap = () => {
176
197
  if (!view) {
177
198
  return
@@ -179,6 +200,9 @@ const applyMinimap = () => {
179
200
  view.updateOptions({ minimap: minimapOptions() })
180
201
  eventText.value = `updateOptions:${enabled.value ? '已更新鹰眼图配置' : '已卸载鹰眼图'}`
181
202
  requestAnimationFrame(refreshReadout)
203
+ if (enabled.value) {
204
+ requestAnimationFrame(installMinimapHandle)
205
+ }
182
206
  }
183
207
 
184
208
  const resetZoom = () => {
@@ -193,8 +217,108 @@ const zoomMinimap = (factor) => {
193
217
 
194
218
  const refreshZoom = () => {
195
219
  if (view) {
196
- zoomReadout.value = `小图倍数 ${view.getMinimapZoom().toFixed(2)} · ${zoomable.value ? '滚轮缩放小图' : '滚轮缩放主图'}`
220
+ zoomReadout.value = `小图倍数 ${view.getMinimapZoom().toFixed(2)} · ${zoomModeLabel.value}`
221
+ }
222
+ }
223
+
224
+ /** 拖拽演示:框选不依赖 zoomable;平移才需要手动缩放后内容有可移动余量 */
225
+ const showDragDemo = (action) => {
226
+ dragAction.value = action
227
+ zoomMode.value = action === 'box' ? 'off' : 'manual'
228
+ applyMinimap()
229
+ eventText.value = action === 'box'
230
+ ? '拖拽演示:缩放模式已关闭,在小图空白处直接拖动仍能拉出框选矩形'
231
+ : '拖拽演示:已切到手动缩放,在小图空白处拖动平移小图内容'
232
+ }
233
+
234
+ /** 把 offset 写回实例:整图拖动只改角落位移,不重画小图,也不碰主图相机 */
235
+ const setWholeMapOffset = (next) => {
236
+ const x = Number(next?.[0])
237
+ const y = Number(next?.[1])
238
+ offset.value = [Number.isFinite(x) ? x : 0, Number.isFinite(y) ? y : 0]
239
+ const options = view?.getOptions().minimap
240
+ if (options) {
241
+ view.updateOptions({ minimap: { ...options, offset: [...offset.value] } })
197
242
  }
243
+ offsetReadout.value = `整图位移 ${Math.round(offset.value[0])}, ${Math.round(offset.value[1])}`
244
+ }
245
+
246
+ const resetWholeMapOffset = () => {
247
+ setWholeMapOffset([0, 0])
248
+ eventText.value = '整图拖动:位置已复位到当前角落'
249
+ }
250
+
251
+ const beginWholeMapDrag = (event) => {
252
+ if (!view || event.button !== 0) {
253
+ return
254
+ }
255
+ event.preventDefault()
256
+ event.stopPropagation()
257
+ const current = view.getOptions().minimap?.offset
258
+ const origin = Array.isArray(current) ? [Number(current[0]) || 0, Number(current[1]) || 0] : [...offset.value]
259
+ wholeMapDrag = { pointerId: event.pointerId, clientX: event.clientX, clientY: event.clientY, origin }
260
+ minimapHandle?.classList.add('is-dragging')
261
+ eventText.value = '整图拖动:按住顶部手柄移动,松手停在当前角落'
262
+ }
263
+
264
+ const moveWholeMapDrag = (event) => {
265
+ if (!wholeMapDrag || event.pointerId !== wholeMapDrag.pointerId) {
266
+ return
267
+ }
268
+ event.preventDefault()
269
+ event.stopPropagation()
270
+ setWholeMapOffset([
271
+ wholeMapDrag.origin[0] + event.clientX - wholeMapDrag.clientX,
272
+ wholeMapDrag.origin[1] + event.clientY - wholeMapDrag.clientY
273
+ ])
274
+ }
275
+
276
+ const endWholeMapDrag = (event) => {
277
+ if (!wholeMapDrag || (event?.pointerId !== undefined && event.pointerId !== wholeMapDrag.pointerId)) {
278
+ return
279
+ }
280
+ event?.preventDefault()
281
+ event?.stopPropagation()
282
+ minimapHandle?.classList.remove('is-dragging')
283
+ wholeMapDrag = null
284
+ eventText.value = `整图拖动:已停在位移 ${Math.round(offset.value[0])}, ${Math.round(offset.value[1])}`
285
+ }
286
+
287
+ const nudgeWholeMap = (event) => {
288
+ const step = event.shiftKey ? 10 : 2
289
+ const delta = event.key === 'ArrowLeft' ? [-step, 0]
290
+ : event.key === 'ArrowRight' ? [step, 0]
291
+ : event.key === 'ArrowUp' ? [0, -step]
292
+ : event.key === 'ArrowDown' ? [0, step] : null
293
+ if (!delta) {
294
+ return
295
+ }
296
+ event.preventDefault()
297
+ event.stopPropagation()
298
+ setWholeMapOffset([offset.value[0] + delta[0], offset.value[1] + delta[1]])
299
+ eventText.value = '整图拖动:方向键也能微调整图位置(按住 Shift 每次 10px)'
300
+ }
301
+
302
+ /** 给库创建的根节点挂一个独立的拖动手柄:手柄自己 stopPropagation,不会触发小图框选 / 平移手势 */
303
+ const installMinimapHandle = () => {
304
+ if (!view || minimapHandle) {
305
+ return
306
+ }
307
+ const root = view.getMinimapElement()
308
+ if (!root) {
309
+ return
310
+ }
311
+ minimapHandle = document.createElement('div')
312
+ minimapHandle.className = 'demo-minimap-drag-handle'
313
+ minimapHandle.dataset.demo = 'minimap-drag-handle'
314
+ minimapHandle.setAttribute('role', 'button')
315
+ minimapHandle.setAttribute('tabindex', '0')
316
+ minimapHandle.setAttribute('aria-label', '拖动整张鹰眼图')
317
+ minimapHandle.title = '按住拖动整张鹰眼图;方向键可微调'
318
+ minimapHandle.textContent = '拖动整图'
319
+ minimapHandle.addEventListener('pointerdown', beginWholeMapDrag)
320
+ minimapHandle.addEventListener('keydown', nudgeWholeMap)
321
+ root.append(minimapHandle)
198
322
  }
199
323
 
200
324
  const refreshLayerReadout = () => {
@@ -229,9 +353,18 @@ onMounted(() => {
229
353
  eventText.value = `warning:${text}`
230
354
  }
231
355
  })
356
+ installMinimapHandle()
357
+ window.addEventListener('pointermove', moveWholeMapDrag, true)
358
+ window.addEventListener('pointerup', endWholeMapDrag, true)
359
+ window.addEventListener('pointercancel', endWholeMapDrag, true)
232
360
  })
233
361
 
234
362
  onBeforeUnmount(() => {
363
+ window.removeEventListener('pointermove', moveWholeMapDrag, true)
364
+ window.removeEventListener('pointerup', endWholeMapDrag, true)
365
+ window.removeEventListener('pointercancel', endWholeMapDrag, true)
366
+ minimapHandle?.remove()
367
+ minimapHandle = null
235
368
  view?.destroy()
236
369
  view = null
237
370
  })
@@ -289,7 +422,16 @@ const sizeSummary = computed(() => `${SIZES[size.value]?.[0] ?? 0}×${SIZES[size
289
422
  </select>
290
423
  </label>
291
424
  </div>
292
- <p class="dv-hint"><code>maxPixelRatio</code> 越大越清晰,也越吃填充率;高分屏按机器档位调到 2 通常就够。</p>
425
+ <div class="dv-row">
426
+ <label><input v-model="extentFollowContent" type="checkbox" data-demo="minimap-extent-content" @change="applyMinimap" /> 自动范围随内容更新(默认关)</label>
427
+ </div>
428
+ <div class="dv-row">
429
+ <label><input v-model="extentFollowCamera" type="checkbox" data-demo="minimap-extent-follow" @change="applyMinimap" /> 自动范围随相机收窄(默认关)</label>
430
+ </div>
431
+ <div class="dv-row">
432
+ <button type="button" class="dv-button" data-demo="minimap-offset-reset" @click="resetWholeMapOffset">整图位置复位</button>
433
+ </div>
434
+ <p class="dv-hint"><code>extentFollowContent</code> 默认关闭:筛选或删除外围要素不会改小图范围。按住小图顶部的「拖动整图」手柄可移动整张鹰眼图,方向键可微调;<code>offset</code> 会随其他控件一起保留。<code>maxPixelRatio</code> 越大越清晰,也越吃填充率;高分屏按机器档位调到 2 通常就够。</p>
293
435
  </div>
294
436
  <div class="dv-group">
295
437
  <h3>规则与交互</h3>
@@ -309,11 +451,24 @@ const sizeSummary = computed(() => `${SIZES[size.value]?.[0] ?? 0}×${SIZES[size
309
451
  </label>
310
452
  </div>
311
453
  <div class="dv-row">
312
- <label><input v-model="linkedSizes" type="checkbox" data-demo="minimap-linked" @change="applyMinimap" /> 尺寸跟随主图缩放</label>
454
+ <label>节点整体倍数 <input v-model.number="pointSizeScale" type="range" min="0" max="3" step="0.1" data-demo="minimap-point-size-scale" @change="applyMinimap" /> {{ Number(pointSizeScale).toFixed(1) }}</label>
455
+ <label>线路整体倍数 <input v-model.number="lineWidthScale" type="range" min="0" max="3" step="0.1" data-demo="minimap-line-width-scale" @change="applyMinimap" /> {{ Number(lineWidthScale).toFixed(1) }}</label>
456
+ </div>
457
+ <div class="dv-row">
458
+ <label>小图放大节点 <input v-model.number="pointZoomScale" type="range" min="0" max="1" step="0.1" data-demo="minimap-point-zoom-scale" @change="applyMinimap" /> {{ Number(pointZoomScale).toFixed(1) }}</label>
459
+ <label>小图放大线路 <input v-model.number="lineZoomScale" type="range" min="0" max="1" step="0.1" data-demo="minimap-line-zoom-scale" @change="applyMinimap" /> {{ Number(lineZoomScale).toFixed(1) }}</label>
460
+ </div>
461
+ <div class="dv-row">
313
462
  <label><input v-model="channelCircuits" type="checkbox" data-demo="minimap-circuits" @change="applyMinimap" /> 画全部回线</label>
314
463
  </div>
315
464
  <div class="dv-row">
316
- <label><input v-model="zoomable" type="checkbox" data-demo="minimap-zoomable" @change="applyMinimap" /> 小图自身可缩放</label>
465
+ <label>缩放模式
466
+ <select v-model="zoomMode" data-demo="minimap-zoomable" @change="applyMinimap">
467
+ <option value="off">关闭</option>
468
+ <option value="manual">手动缩放小图</option>
469
+ <option value="follow">跟随主图</option>
470
+ </select>
471
+ </label>
317
472
  <label>空白拖动
318
473
  <select v-model="dragAction" data-demo="minimap-drag-action" @change="applyMinimap">
319
474
  <option value="pan">平移小图</option>
@@ -321,7 +476,14 @@ const sizeSummary = computed(() => `${SIZES[size.value]?.[0] ?? 0}×${SIZES[size
321
476
  </select>
322
477
  </label>
323
478
  </div>
324
- <p class="dv-hint">打开「小图自身可缩放」后,滚轮缩放小图;按住 Ctrl / ⌘ + 滚轮仍然缩放主图。视口框可以拖,空白处长按跟随。</p>
479
+ <div class="dv-row">
480
+ <label>跟随指数 <input v-model.number="followZoomScale" type="range" min="0" max="2" step="0.1" data-demo="minimap-follow-scale" @change="applyMinimap" /> {{ Number(followZoomScale).toFixed(1) }}</label>
481
+ </div>
482
+ <div class="dv-row">
483
+ <button type="button" class="dv-button" data-demo="minimap-drag-box" @click="showDragDemo('box')">拖拽演示:框选</button>
484
+ <button type="button" class="dv-button" data-demo="minimap-drag-pan" @click="showDragDemo('pan')">拖拽演示:平移</button>
485
+ </div>
486
+ <p class="dv-hint">沿用主图规则时,节点 / 线路先按「小图视口 ÷ 主图视口」缩放,主图放大缩小不会改变它们的大小;滚轮或 + / - 放大鹰眼图时,再按 <code>pointZoomScale</code> / <code>lineZoomScale</code> 的指数变化。两个整体倍数对自有规则与沿用主图的要素都生效。<code>zoomable: true</code> 是小图手动缩放,<code>'follow'</code> 是整体范围跟随主图,两者互斥;<code>followZoomScale</code> 是主图倍数的指数。框选不依赖缩放开关:选「关闭」+ <code>dragAction: 'box'</code>,空白处拖动仍会直接框选。视口框可以拖,空白处长按跟随。</p>
325
487
  </div>
326
488
  <div class="dv-group">
327
489
  <h3>扩展层</h3>
@@ -341,18 +503,55 @@ const sizeSummary = computed(() => `${SIZES[size.value]?.[0] ?? 0}×${SIZES[size
341
503
  <div class="dv-group">
342
504
  <h3>小图倍数</h3>
343
505
  <div class="dv-row">
344
- <button type="button" class="dv-button" data-demo="minimap-zoom-out" @click="zoomMinimap(1 / 1.35)">-</button>
345
- <button type="button" class="dv-button dv-button--primary" data-demo="minimap-zoom-reset" @click="resetZoom">1×</button>
346
- <button type="button" class="dv-button" data-demo="minimap-zoom-in" @click="zoomMinimap(1.35)">+</button>
506
+ <button type="button" class="dv-button" data-demo="minimap-zoom-out" :disabled="zoomMode === 'follow'" @click="zoomMinimap(1 / 1.35)">-</button>
507
+ <button type="button" class="dv-button dv-button--primary" data-demo="minimap-zoom-reset" :disabled="zoomMode === 'follow'" @click="resetZoom">1×</button>
508
+ <button type="button" class="dv-button" data-demo="minimap-zoom-in" :disabled="zoomMode === 'follow'" @click="zoomMinimap(1.35)">+</button>
347
509
  </div>
348
510
  <p class="dv-readout" data-demo="minimap-zoom-readout">{{ zoomReadout }}</p>
349
511
  </div>
350
512
  <div class="dv-group">
351
513
  <h3>读数</h3>
352
514
  <p class="dv-readout" data-demo="minimap-readout">{{ readout }}</p>
515
+ <p class="dv-readout" data-demo="minimap-offset-readout">{{ offsetReadout }}</p>
353
516
  <p class="dv-readout" data-demo="minimap-event" aria-live="polite">{{ eventText }}</p>
354
517
  <p class="dv-readout" data-demo="minimap-interaction" aria-live="polite">{{ interactionReadout }}</p>
355
518
  </div>
356
519
  </aside>
357
520
  </section>
358
521
  </template>
522
+
523
+ <style scoped>
524
+ :deep(.demo-minimap-drag-handle) {
525
+ position: absolute;
526
+ z-index: 4;
527
+ top: 4px;
528
+ left: 50%;
529
+ box-sizing: border-box;
530
+ min-width: 68px;
531
+ padding: 2px 9px;
532
+ transform: translateX(-50%);
533
+ border: 1px solid rgba(54, 120, 191, 0.38);
534
+ border-radius: 999px;
535
+ background: rgba(255, 255, 255, 0.9);
536
+ box-shadow: 0 2px 7px rgba(31, 62, 105, 0.14);
537
+ color: #2f5e94;
538
+ font-size: 10px;
539
+ line-height: 16px;
540
+ text-align: center;
541
+ cursor: grab;
542
+ user-select: none;
543
+ touch-action: none;
544
+ }
545
+
546
+ :deep(.demo-minimap-drag-handle:hover),
547
+ :deep(.demo-minimap-drag-handle:focus-visible) {
548
+ border-color: #3678bf;
549
+ background: #fff;
550
+ outline: none;
551
+ }
552
+
553
+ :deep(.demo-minimap-drag-handle.is-dragging) {
554
+ cursor: grabbing;
555
+ background: #eaf2fc;
556
+ }
557
+ </style>