advancedPencilSupport-demo1

This commit is contained in:
TrueZhuanjia committed 2025-12-02 17:15:56 +08:00
1 parent 6dc9ee569b
commit 1262a7b41f
16 files changed
+473 -45

No files matched your search

+163
View File
@@ -0,0 +1,163 @@
//
// PencilHandler.swift
// VoidLink
//
// Created by True砖家 on 2025/11/17.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
import UIKit
@objc class PencilHandler: UIResponder {
/// 用来处理所有 touch 事件的宿主视图
weak var streamView: UIView?
// private var tickTimer: SafeTimer
private var streamAspectRatio: Float
private var tickInterval: TimeInterval
private var manualTick: Bool
@objc init(streamView: UIView, settings: TemporarySettings) {
self.streamView = streamView
streamAspectRatio = settings.width.floatValue/settings.height.floatValue
tickInterval = TimeInterval(settings.pencilTickIntervalUs.floatValue/1000000)
manualTick = settings.pencilTickMode.intValue == PencilTickMode.ManualTick.rawValue
super.init()
}
// MARK: - Touch Events
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let event = event else { return }
for touch in touches {
let coalesced = event.coalescedTouches(for: touch) ?? []
_ = self.sendStylusEvent(touchBatch: coalesced)
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let event = event else { return }
for touch in touches {
let coalesced = event.coalescedTouches(for: touch) ?? []
_ = self.sendStylusEvent(touchBatch: coalesced)
}
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let event = event else { return }
for touch in touches {
let coalesced = event.coalescedTouches(for: touch) ?? []
_ = self.sendStylusEvent(touchBatch: coalesced)
}
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
self.touchesEnded(touches, with: event)
}
// MARK: - Core Logic
func getVideoAreaSize()-> CGSize {
guard let streamView = streamView else { return CGSize(width: 0, height: 0) }
if streamView.bounds.size.width > streamView.bounds.size.height * CGFloat(streamAspectRatio) {
return CGSizeMake(streamView.bounds.size.height * CGFloat(streamAspectRatio), streamView.bounds.size.height);
} else {
return CGSizeMake(streamView.bounds.size.width, streamView.bounds.size.width / CGFloat(streamAspectRatio));
}
}
func adjustCoordinatesForVideoArea(point: CGPoint) -> CGPoint {
guard let streamView = streamView else { return CGPoint(x: 0, y: 0) }
var x = point.x - streamView.bounds.origin.x
var y = point.y - streamView.bounds.origin.y
if x < streamView.bounds.width / 2 {
x -= 1
} else {
x += 1
}
if y < streamView.bounds.height / 2 {
y -= 1
} else {
y += 1
}
let videoSize = getVideoAreaSize() // 返回 CGSize
let videoOrigin = CGPoint(
x: streamView.bounds.width / 2 - videoSize.width / 2,
y: streamView.bounds.height / 2 - videoSize.height / 2
)
let confinedX = min(max(x, videoOrigin.x), videoOrigin.x + videoSize.width) - videoOrigin.x
let confinedY = min(max(y, videoOrigin.y), videoOrigin.y + videoSize.height) - videoOrigin.y
return CGPoint(x: confinedX, y: confinedY)
}
func getRotation(fromAzimuthAngle azimuthAngle: Float) -> UInt16 {
// iOS 的 azimuthAngle 为 0 表示指向西
// VoidLink 期望 0 表示指向北
// 所以顺时针旋转 90 度来转换
var rotationAngle = (azimuthAngle - .pi / 2) * (180.0 / .pi) // 弧度转角度
if rotationAngle < 0 {
rotationAngle += 360
}
return UInt16(rotationAngle)
}
func getTilt(fromAltitudeAngle altitudeAngle: Float) -> UInt8 {
// iOS 的 altitudeAngle 为 0 表示笔与触摸面平行
// VoidLink 期望 tilt 为 0 表示笔垂直触摸面
// 所以 tilt = 90 - altitude
let altitudeDegs = abs(Int16(altitudeAngle * (180.0 / .pi)))
return UInt8(90 - min(90, Int(altitudeDegs)))
}
private func sendStylusEvent(touchBatch: [UITouch], with event: UIEvent? = nil) -> DispatchTime {
guard let streamView = streamView, touchBatch.count>0 else { return .now() }
var tickMoment: TimeInterval = 0
var previousTimeStamp: TimeInterval = 0
let dispatchMoment: DispatchTime = .now()
var delay:TimeInterval = 0
for touch in touchBatch {
if previousTimeStamp == 0 {previousTimeStamp = touch.timestamp}
tickMoment += (manualTick ? tickInterval : touch.timestamp - previousTimeStamp)
let point = touch.location(in: streamView)
let azimuth = touch.azimuthAngle(in: streamView)
let altitude = touch.altitudeAngle
let location = self.adjustCoordinatesForVideoArea(point: point)
let videoSize = self.getVideoAreaSize()
let eventType:UInt8
switch touch.phase {
case .began:
eventType = UInt8(LI_TOUCH_EVENT_DOWN)
case .moved:
eventType = UInt8(LI_TOUCH_EVENT_MOVE)
case .ended:
eventType = UInt8(LI_TOUCH_EVENT_UP)
case .cancelled:
eventType = UInt8(LI_TOUCH_EVENT_UP)
default:
eventType = UInt8(LI_TOUCH_EVENT_MOVE)
}
delay = eventType == UInt8(LI_TOUCH_EVENT_UP) ? 0.0086 : 0
DispatchQueue.global().asyncAfter(deadline: dispatchMoment + tickMoment + delay) {
LiSendPenEvent(eventType, UInt8(LI_TOOL_TYPE_PEN), 0, Float(location.x/videoSize.width), Float(location.y/videoSize.height), Float(touch.force/touch.maximumPossibleForce)/sin(Float(altitude)), 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
}
}
return dispatchMoment + tickMoment + delay
}
}