Skip to content

Commit 17afdb7

Browse files
committed
feat: implement cursor override for duplicate cursor handling in measure tool
1 parent 9adab69 commit 17afdb7

File tree

2 files changed

+127
-3
lines changed

2 files changed

+127
-3
lines changed

packages/extension/composables/key-lock.ts

Lines changed: 112 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,26 @@
1-
import { useEventListener } from '@vueuse/core'
1+
import { useEventListener, useMutationObserver } from '@vueuse/core'
22

33
import { options } from '@/ui/state'
44
import { getCanvas, setLockAltKey, setLockMetaKey } from '@/utils'
55

66
let spacePressed = false
7+
let altPressed = false
8+
let duplicateClass: string | null = null
9+
let classSnapshot = new Set<string>()
10+
const DUPLICATE_URL_SIGNATURE =
11+
'data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEAAAABACAYAAACqaXHeAAAIZElEQVR4AeyYTWxUVRTH3yD9UCEE'
12+
function extractHotspot(cursor: string): [number, number] | null {
13+
const normalized = cursor.replace(/\s+/g, ' ')
14+
const match =
15+
normalized.match(/(?:-webkit-)?image-set\([^)]*\)\s*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)/i) ??
16+
normalized.match(/\)\s*(\d+(?:\.\d+)?)\s+(\d+(?:\.\d+)?)/)
17+
if (!match) return null
18+
return [Number(match[1]), Number(match[2])]
19+
}
20+
21+
function hasDuplicateSignature(cursor: string) {
22+
return cursor.includes(DUPLICATE_URL_SIGNATURE)
23+
}
724

825
function pause() {
926
setLockMetaKey(false)
@@ -15,15 +32,15 @@ function resume() {
1532
return
1633
}
1734
setLockMetaKey(options.value.deepSelectOn)
18-
setLockAltKey(options.value.measureOn)
35+
syncAltLock()
1936
}
2037

2138
function pauseMeasure() {
2239
setLockAltKey(false)
2340
}
2441

2542
function resumeMeasure() {
26-
setLockAltKey(options.value.measureOn)
43+
syncAltLock()
2744
}
2845

2946
let resuming: number | null = null
@@ -43,21 +60,89 @@ function pauseMetaThenResume() {
4360
}
4461

4562
function keydown(e: KeyboardEvent) {
63+
if (!options.value.measureOn && e.key === 'Alt') {
64+
return
65+
}
66+
if (!altPressed && e.key === 'Alt') {
67+
altPressed = true
68+
setLockAltKey(false)
69+
reconcileCursor()
70+
}
4671
if (!spacePressed && e.key === ' ') {
4772
spacePressed = true
4873
pause()
4974
}
5075
}
5176

5277
function keyup(e: KeyboardEvent) {
78+
if (!options.value.measureOn && e.key === 'Alt') {
79+
return
80+
}
81+
if (altPressed && e.key === 'Alt') {
82+
altPressed = false
83+
syncAltLock()
84+
reconcileCursor()
85+
}
5386
if (spacePressed && e.key === ' ') {
5487
spacePressed = false
5588
resume()
5689
}
5790
}
5891

92+
function syncAltLock() {
93+
setLockAltKey(!altPressed && options.value.measureOn)
94+
}
95+
96+
function isDuplicateCursor(host: HTMLElement) {
97+
if (duplicateClass) {
98+
return host.classList.contains(duplicateClass)
99+
}
100+
const cursor = getComputedStyle(host).cursor
101+
const hotspot = extractHotspot(cursor)
102+
return hotspot != null && hotspot[0] === 8 && hotspot[1] === 8 && hasDuplicateSignature(cursor)
103+
}
104+
105+
function learnDuplicateClass(host: HTMLElement) {
106+
if (duplicateClass) return
107+
const added = Array.from(host.classList).filter((c) => !classSnapshot.has(c))
108+
if (added.length === 1) {
109+
duplicateClass = added[0]
110+
}
111+
}
112+
113+
function applyCursorCover(host: HTMLElement) {
114+
host.dataset.tpCursorOverride = ''
115+
}
116+
117+
function clearCursorCover(host: HTMLElement) {
118+
delete host.dataset.tpCursorOverride
119+
}
120+
121+
function reconcileCursor(host?: HTMLElement | null) {
122+
const target = host ?? cursorHost
123+
if (!target) return
124+
if (!options.value.measureOn || altPressed) {
125+
clearCursorCover(target)
126+
classSnapshot = new Set(target.classList)
127+
return
128+
}
129+
if (isDuplicateCursor(target)) {
130+
learnDuplicateClass(target)
131+
applyCursorCover(target)
132+
} else {
133+
clearCursorCover(target)
134+
}
135+
classSnapshot = new Set(target.classList)
136+
}
137+
138+
let cursorHost: HTMLElement | null = null
139+
59140
export function useKeyLock() {
60141
const canvas = getCanvas()
142+
cursorHost = canvas?.parentElement?.parentElement as HTMLElement | null
143+
if (cursorHost) {
144+
classSnapshot = new Set(cursorHost.classList)
145+
}
61146

62147
useEventListener(canvas, 'mouseleave', pause)
63148
useEventListener(canvas, 'mouseenter', resume)
@@ -67,10 +152,34 @@ export function useKeyLock() {
67152
useEventListener('keydown', keydown)
68153
useEventListener('keyup', keyup)
69154

155+
if (cursorHost) {
156+
useMutationObserver(
157+
cursorHost,
158+
() => {
159+
if (!options.value.measureOn) {
160+
clearCursorCover(cursorHost!)
161+
return
162+
}
163+
reconcileCursor(cursorHost)
164+
},
165+
{ attributes: true, attributeFilter: ['class'] }
166+
)
167+
}
168+
169+
reconcileCursor(cursorHost)
170+
70171
watch(
71172
() => options.value.deepSelectOn,
72173
() => {
73174
setLockMetaKey(options.value.deepSelectOn)
74175
}
75176
)
177+
178+
watch(
179+
() => options.value.measureOn,
180+
() => {
181+
syncAltLock()
182+
reconcileCursor(cursorHost)
183+
}
184+
)
76185
}

packages/extension/entrypoints/ui/style.css

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -158,3 +158,18 @@ tempad input[type="number"]:focus-visible {
158158
[class^="mode_switcher_toggle--toggleWrapper"] {
159159
display: none !important;
160160
}
161+
162+
@layer overrides {
163+
/* Cursor override for blocking duplicate cursor when measure tool is on */
164+
[data-tp-cursor-override] {
165+
cursor:
166+
-webkit-image-set(
167+
url("data:image/png;base64,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")
168+
2x,
169+
url("data:image/png;base64,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")
170+
1x
171+
)
172+
4 4,
173+
auto !important;
174+
}
175+
}

0 commit comments

Comments
 (0)