pressure curve implementation

This commit is contained in:
TrueZhuanjia
2026-01-08 16:51:41 +08:00
parent 156b363049
commit 6d4f192cbe
16 changed files with 1148 additions and 89 deletions
+4 -2
View File
@@ -29,6 +29,7 @@
18A0EE2C2EFBCCFD00BA853A /* GraphicUtils.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A0EE2B2EFBCCFD00BA853A /* GraphicUtils.swift */; };
18A765BD2ECD51FB004784AA /* ControllerUtil.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */; };
18B317032EE67A3A009B62C2 /* InertialScroller.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18B317022EE67A39009B62C2 /* InertialScroller.swift */; };
18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18D43D7E2F050240008C326F /* PressureCurve.swift */; };
18E4BCEA2E891895000A8657 /* MotionHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18E4BCE92E891895000A8657 /* MotionHandler.swift */; };
18FCADAF2ECAC23200787A96 /* PencilHandler.swift in Sources */ = {isa = PBXBuildFile; fileRef = 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */; };
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */ = {isa = PBXBuildFile; fileRef = 840751B22BF49C050096B657 /* CustomTapGestureRecognizer.m */; };
@@ -171,7 +172,6 @@
180345E02E819877004AF36F /* libopus-sim.a */ = {isa = PBXFileReference; lastKnownFileType = archive.ar; path = "libopus-sim.a"; sourceTree = "<group>"; };
180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerUtil.swift; sourceTree = "<group>"; };
1817B61D2E6FD04D000D938D /* SafeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeTimer.swift; sourceTree = "<group>"; };
182042252EFEE46700309FF6 /* touchCentered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = touchCentered.swift; sourceTree = "<group>"; };
1829CE3D2E2154DF0074A99A /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = "<group>"; };
1829CE3E2E2154DF0074A99A /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = "<group>"; };
18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FixedTintImageView.h; sourceTree = "<group>"; };
@@ -191,6 +191,7 @@
18A765BC2ECD51FB004784AA /* ControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerUtil.swift; sourceTree = "<group>"; };
18B317022EE67A39009B62C2 /* InertialScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InertialScroller.swift; sourceTree = "<group>"; };
18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = "<group>"; };
18D43D7E2F050240008C326F /* PressureCurve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressureCurve.swift; sourceTree = "<group>"; };
18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = "<group>"; };
18FCADAE2ECAC23200787A96 /* PencilHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilHandler.swift; sourceTree = "<group>"; };
693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = "<group>"; };
@@ -585,7 +586,6 @@
FB1A67542132419700507771 /* Moonlight TV */,
FB290CF019B2C406004C83CF /* Frameworks */,
FB290CEF19B2C406004C83CF /* Products */,
182042252EFEE46700309FF6 /* touchCentered.swift */,
);
sourceTree = "<group>";
};
@@ -730,6 +730,7 @@
9819CC21254F180F008A7C8E /* AbsoluteTouchHandler.m */,
988FCD3F293B091B003050E2 /* KeyboardInputField.h */,
988FCD40293B091B003050E2 /* KeyboardInputField.m */,
18D43D7E2F050240008C326F /* PressureCurve.swift */,
18FCADAE2ECAC23200787A96 /* PencilHandler.swift */,
18416EA32EBB3DCE00AE0124 /* UITouchUtil.swift */,
18B317022EE67A39009B62C2 /* InertialScroller.swift */,
@@ -1094,6 +1095,7 @@
840751B32BF49C050096B657 /* CustomTapGestureRecognizer.m in Sources */,
187F4B282E04658A00EECC59 /* WidgetPanelStackView.m in Sources */,
FB4678FA1A55FFAD00377732 /* DiscoveryManager.m in Sources */,
18D43D7F2F050240008C326F /* PressureCurve.swift in Sources */,
180E6E372E93BFDB0067C73A /* AlertControllerUtil.swift in Sources */,
841BAC922DE35D6B00DA639E /* ThemeManager.m in Sources */,
98D5856D1C0EA79600F6CC00 /* TemporaryHost.m in Sources */,
+4 -1
View File
@@ -20,6 +20,7 @@ import UIKit
@objc public static var actionCancelled:Bool = false
@objc public static var alertController:UIAlertController = UIAlertController()
@objc public static var cancelButtonString:String = "Cancel"
@objc class func showAlert(
in viewController: UIViewController,
@@ -38,11 +39,13 @@ import UIKit
let confirmAction = UIAlertAction(title: "\(remainingSeconds)", style: .default) { _ in
actionCancelled = false
completion?()
cancelButtonString = "Cancel"
}
let cancelAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: "Cancel"), style: .cancel) { _ in
let cancelAction = UIAlertAction(title: SwiftLocalizationHelper.localizedString(forKey: cancelButtonString), style: .cancel) { _ in
actionCancelled = true
completion?()
cancelButtonString = "Cancel"
}
confirmAction.isEnabled = false
+28 -6
View File
@@ -1898,8 +1898,8 @@
<segmentedControl verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" contentHorizontalAlignment="left" contentVerticalAlignment="top" segmentControlStyle="plain" selectedSegmentIndex="0" translatesAutoresizingMaskIntoConstraints="NO" id="bVv-CW-kN8">
<rect key="frame" x="0.0" y="25.5" width="380" height="32"/>
<segments>
<segment title="Manual"/>
<segment title="Off"/>
<segment title="Manual"/>
</segments>
<color key="tintColor" red="0.6716768742" green="0.61711704730000005" blue="0.99902987480000005" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color key="selectedSegmentTintColor" systemColor="linkColor"/>
@@ -1920,7 +1920,7 @@
<color key="textColor" red="1" green="1" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<nil key="highlightedColor"/>
</label>
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" minValue="0.0" maxValue="4000" translatesAutoresizingMaskIntoConstraints="NO" id="IOG-l4-gtQ">
<slider verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" contentHorizontalAlignment="center" contentVerticalAlignment="center" value="1500" minValue="1500" maxValue="4000" translatesAutoresizingMaskIntoConstraints="NO" id="IOG-l4-gtQ">
<rect key="frame" x="0.0" y="21.5" width="380" height="34"/>
<color key="tintColor" systemColor="linkColor"/>
</slider>
@@ -1930,6 +1930,26 @@
<constraint firstAttribute="width" constant="380" id="gdZ-zL-Wl7"/>
</constraints>
</stackView>
<stackView verifyAmbiguity="off" opaque="NO" contentMode="scaleToFill" ambiguous="YES" spacing="20" translatesAutoresizingMaskIntoConstraints="NO" id="Bp6-7U-E9H">
<rect key="frame" x="3145" y="230" width="266.5" height="31"/>
<subviews>
<label verifyAmbiguity="off" opaque="NO" userInteractionEnabled="NO" contentMode="left" horizontalHuggingPriority="251" verticalHuggingPriority="251" text="Pressure Curve" textAlignment="natural" lineBreakMode="tailTruncation" baselineAdjustment="alignBaselines" minimumScaleFactor="0.64999997615814209" adjustsLetterSpacingToFitWidth="YES" adjustsFontForContentSizeCategory="YES" translatesAutoresizingMaskIntoConstraints="NO" id="b1N-z8-12w" userLabel="uiLabel">
<rect key="frame" x="0.0" y="0.0" width="116.5" height="31"/>
<fontDescription key="fontDescription" type="system" pointSize="17"/>
<nil key="textColor"/>
<nil key="highlightedColor"/>
</label>
<switch opaque="NO" contentMode="scaleToFill" horizontalHuggingPriority="750" verticalHuggingPriority="750" contentHorizontalAlignment="center" contentVerticalAlignment="center" on="YES" translatesAutoresizingMaskIntoConstraints="NO" id="c8a-2j-qVf">
<rect key="frame" x="136.5" y="0.0" width="132" height="31"/>
<constraints>
<constraint firstAttribute="width" constant="130" id="oDG-RR-5Wd"/>
</constraints>
</switch>
</subviews>
<constraints>
<constraint firstAttribute="height" constant="31" id="RRk-QS-MEj"/>
</constraints>
</stackView>
</subviews>
<color key="backgroundColor" white="0.0" alpha="0.0" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</view>
@@ -2045,6 +2065,8 @@
<outlet property="pointerVelocityDividerStack" destination="b1U-nD-gYE" id="ep1-K3-rKj"/>
<outlet property="pointerVelocityFactorStack" destination="vdk-mK-ztu" id="Cik-Rh-pGS"/>
<outlet property="pointerVelocityModeDividerSlider" destination="AcZ-2i-GI4" id="dGf-ZU-w3S"/>
<outlet property="pressureCurveStack" destination="Bp6-7U-E9H" id="tHX-hP-A78"/>
<outlet property="pressureCurveSwitch" destination="c8a-2j-qVf" id="1aS-J3-Mav"/>
<outlet property="redirectMicStack" destination="hcl-DT-peO" id="g8Y-dq-CiW"/>
<outlet property="redirectMicSwitch" destination="9Gg-3U-Oa7" id="39M-xy-hl8"/>
<outlet property="relativeTouchSlideThresholdSlider" destination="Zut-jY-AFj" id="HFz-5k-1yq"/>
@@ -2988,7 +3010,7 @@
<image name="tray.full.fill" catalog="system" width="128" height="88"/>
<image name="video.and.waveform" catalog="system" width="128" height="94"/>
<systemColor name="linkColor">
<color red="0.0" green="0.47843137254901963" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.0" green="0.47843137250000001" blue="1" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="secondarySystemGroupedBackgroundColor">
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
@@ -2997,13 +3019,13 @@
<color white="1" alpha="1" colorSpace="custom" customColorSpace="genericGamma22GrayColorSpace"/>
</systemColor>
<systemColor name="systemBrownColor">
<color red="0.67450980392156867" green="0.49803921568627452" blue="0.36862745098039218" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.63529411759999999" green="0.51764705879999995" blue="0.36862745099999999" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemGray3Color">
<color red="0.7803921568627451" green="0.7803921568627451" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.78039215689999997" green="0.78039215689999997" blue="0.80000000000000004" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
<systemColor name="systemTealColor">
<color red="0.0" green="0.76470588235294112" blue="0.81568627450980391" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
<color red="0.18823529410000001" green="0.69019607839999997" blue="0.78039215689999997" alpha="1" colorSpace="custom" customColorSpace="sRGB"/>
</systemColor>
</resources>
</document>
+2 -2
View File
@@ -79,8 +79,8 @@ typedef NS_ENUM(NSInteger, WidgetSizeTransition) {
};
typedef NS_ENUM(NSInteger, PencilTickMode) {
ManualTick,
PencilTickDisabled
PencilTickDisabled,
ManualTick
};
- (void) saveSettings:(Settings*)settings
+2 -1
View File
@@ -48,7 +48,8 @@ typedef NS_ENUM(NSInteger, MapGyroTo) {
@property (nonatomic, assign) bool reverseGyroHoldButton;
@property (nonatomic, assign) int controllerGyroSwitchHold;
@property (nonatomic, assign) int controllerGyroSwitchToggle;
@property NSArray<NSNumber *> *pressureCurvePoints;
@property (nonatomic, assign) bool pressureCurveEnabled;
- (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected;
+22 -2
View File
@@ -36,7 +36,6 @@
[encoder encodeBool:self.yawPitchToRightStick forKey:@"yawPitchToRightStick"];
[encoder encodeBool:self.rollToLeftStick forKey:@"rollToLeftStick"];
[encoder encodeBool:self.synthesizePhysicalStick forKey:@"synthesizePhysicalStick"];
[encoder encodeInt64:self.mapGyroTo forKey:@"mapGyroTo"];
[encoder encodeFloat:self.gyroSensitivityYaw forKey:@"gyroSensitivityYaw"];
[encoder encodeFloat:self.gyroSensitivityPitch forKey:@"gyroSensitivityPitch"];
[encoder encodeFloat:self.gyroSensitivityRoll forKey:@"gyroSensitivityRoll"];
@@ -50,12 +49,21 @@
[encoder encodeBool:self.reverseGyroHoldButton forKey:@"reverseGyroHoldButton"];
[encoder encodeInt:self.controllerGyroSwitchHold forKey:@"controllerGyroSwitchHold"];
[encoder encodeInt:self.controllerGyroSwitchToggle forKey:@"controllerGyroSwitchToggle"];
[encoder encodeObject:self.pressureCurvePoints forKey:@"pressureCurvePoints"];
[encoder encodeBool:self.pressureCurveEnabled forKey:@"pressureCurveEnabled"];
}
- (id) initWithCoder:(NSCoder*)decoder {
if (self = [super init]) {
self.name = [decoder decodeObjectForKey:@"name"];
self.buttonStatesEncoded = [decoder decodeObjectForKey:@"buttonStates"];
self.buttonStatesEncoded =
[decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSMutableArray class],
[NSArray class],
[NSData class],
nil]
forKey:@"buttonStates"];
self.isSelected = [decoder decodeBoolForKey:@"isSelected"];
self.mapGyroTo = [decoder containsValueForKey:@"mapGyroTo"] ? [decoder decodeInt64ForKey:@"mapGyroTo"] : mapGyroToMouse;
self.yawPitchToRightStick = [decoder containsValueForKey:@"yawPitchToRightStick"] ? [decoder decodeBoolForKey:@"yawPitchToRightStick"] : true;
@@ -74,6 +82,16 @@
self.reverseGyroHoldButton = [decoder containsValueForKey:@"reverseGyroHoldButton"] ? [decoder decodeBoolForKey:@"reverseGyroHoldButton"] : false;
self.controllerGyroSwitchHold = [decoder containsValueForKey:@"controllerGyroSwitchHold"] ? [decoder decodeIntForKey:@"controllerGyroSwitchHold"] : ControllerButtonNull;
self.controllerGyroSwitchToggle = [decoder containsValueForKey:@"controllerGyroSwitchToggle"] ? [decoder decodeIntForKey:@"controllerGyroSwitchToggle"] : ControllerButtonNull;
self.pressureCurvePoints =
[decoder containsValueForKey:@"pressureCurvePoints"]
? [decoder decodeObjectOfClasses:
[NSSet setWithObjects:
[NSArray class],
[NSNumber class],
nil]
forKey:@"pressureCurvePoints"]
: @[@0.0, @0.0, @1.0, @1.0];
self.pressureCurveEnabled = [decoder containsValueForKey:@"pressureCurveEnabled"] ? [decoder decodeBoolForKey:@"pressureCurveEnabled"] : false;
}
return self;
@@ -101,6 +119,8 @@
copy.reverseGyroHoldButton = self.reverseGyroHoldButton;
copy.controllerGyroSwitchHold = self.controllerGyroSwitchHold;
copy.controllerGyroSwitchToggle = self.controllerGyroSwitchToggle;
copy.pressureCurvePoints = [[NSMutableArray alloc] initWithArray:self.pressureCurvePoints copyItems:YES];
copy.pressureCurveEnabled = self.pressureCurveEnabled;
return copy;
}
+43 -22
View File
@@ -10,22 +10,43 @@
import UIKit
@objc class PencilHandler: UIResponder {
@objc static var sharedInstance: PencilHandler?
/// 用来处理所有 touch 事件的宿主视图
weak var streamView: UIView?
// private var tickTimer: SafeTimer
private var streamAspectRatio: Float
private var tickInterval: TimeInterval
private var manualTick: Bool
private var manualhoverFlag: Bool = false
private var pencilTickEnabled: Bool
private var pressureCurveEnabled: Bool = false
private var manualHoverFlag: Bool = false
private var autoHoverFlag: Bool = false
private var pressureLUT = PressureCurveLUT(curve: PressureCurve())
@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
pencilTickEnabled = settings.pencilTickMode.intValue != PencilTickMode.PencilTickDisabled.rawValue
super.init()
setupPressureLUT()
PencilHandler.sharedInstance = self
}
public func setupPressureLUT(){
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
let oscProfile = oscProfileMan.getSelectedProfile()
pressureCurveEnabled = oscProfile.pressureCurveEnabled
let persistedCurvePoints = PressureCurve.importCurvePoints(oscProfile.pressureCurvePoints)
let curve = PressureCurve()
curve.polylinePoints = persistedCurvePoints
curve.buildCurveSegments()
DispatchQueue.main.asyncAfter(deadline: .now()) {
self.pressureLUT = PressureCurveLUT(curve: curve)
}
}
// MARK: - Touch Events
@@ -34,16 +55,15 @@ import UIKit
guard let event = event else { return }
for touch in touches {
let coalesced = event.coalescedTouches(for: touch) ?? []
_ = self.sendStylusEvent(touchBatch: coalesced)
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
}
}
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)
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
}
}
@@ -51,7 +71,7 @@ import UIKit
guard let event = event else { return }
for touch in touches {
let coalesced = event.coalescedTouches(for: touch) ?? []
_ = self.sendStylusEvent(touchBatch: coalesced)
_ = self.sendStylusEvent(touchBatch: pencilTickEnabled ? coalesced : [touch])
}
}
@@ -102,9 +122,6 @@ import UIKit
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
@@ -113,9 +130,6 @@ import UIKit
}
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)))
}
@@ -125,13 +139,10 @@ import UIKit
var tickMoment: TimeInterval = 0
var previousTimeStamp: TimeInterval = 0
let dispatchMoment: DispatchTime = .now()
var delay:TimeInterval = 0
var dispatchMoment: DispatchTime = .now()
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
@@ -139,14 +150,15 @@ import UIKit
let videoSize = self.getVideoAreaSize()
let normalizedLocation = CGPoint(x: location.x/videoSize.width, y: location.y/videoSize.height)
let force = Float(touch.force/touch.maximumPossibleForce)/sin(Float(altitude))
let targetForce = pressureCurveEnabled ? self.pressureLUT.value(at: force) : force
let eventType:UInt8
switch touch.phase {
case .began:
eventType = UInt8(LI_TOUCH_EVENT_DOWN)
case .moved:
eventType = UInt8(manualhoverFlag ? LI_TOUCH_EVENT_HOVER : LI_TOUCH_EVENT_MOVE)
eventType = UInt8(manualHoverFlag ? LI_TOUCH_EVENT_HOVER : LI_TOUCH_EVENT_MOVE)
case .ended:
eventType = UInt8(LI_TOUCH_EVENT_UP)
case .cancelled:
@@ -158,8 +170,17 @@ import UIKit
delay = manualTick ? 0.0086 : 0.017
delay = eventType == UInt8(LI_TOUCH_EVENT_UP) ? delay : 0
if previousTimeStamp == 0 {
previousTimeStamp = touch.timestamp
tickMoment = 0
dispatchMoment = .now()
}
else {
tickMoment += (manualTick ? tickInterval : touch.timestamp - previousTimeStamp)
}
DispatchQueue.global().asyncAfter(deadline: dispatchMoment + tickMoment + delay) {
LiSendPenEvent(eventType, UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), force, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
LiSendPenEvent(eventType, UInt8(LI_TOOL_TYPE_PEN), 0, Float(normalizedLocation.x), Float(normalizedLocation.y), targetForce, 0, 0, self.getRotation(fromAzimuthAngle: Float(azimuth)), self.getTilt(fromAltitudeAngle: Float(altitude)))
}
}
@@ -167,15 +188,15 @@ import UIKit
}
@objc public func switchPencilHoever(){
manualhoverFlag = !manualhoverFlag
manualHoverFlag = !manualHoverFlag
}
@objc public func enablePencilHover(){
manualhoverFlag = true
manualHoverFlag = true
}
@objc public func disablePencilHover(){
manualhoverFlag = false
manualHoverFlag = false
}
private func attachHoverLeave(normalizedLocation:CGPoint){
+663
View File
@@ -0,0 +1,663 @@
//
// PressureCurve.swift
// VoidLink
//
// Created by True砖家 on 2026/1/5.
// Copyright © 2025 True砖家 on Bilibili. All rights reserved.
//
import UIKit
// MARK: - PressureCurve
final class PressureCurve {
var polylinePoints: [CGPoint] = [
CGPoint(x: 0, y: 0.0),
CGPoint(x: 1.0, y: 1.0)
]
private var factor: CGFloat = 0.39
private var cachedSegments: [(p0: CGPoint, c1: CGPoint, c2: CGPoint, p1: CGPoint)] = []
private var curveCutPoints: [CGPoint] {
guard polylinePoints.count >= 2 else { return polylinePoints }
var cuts: [CGPoint] = []
cuts.append(polylinePoints[0]) // 起点
for i in 0..<(polylinePoints.count - 2) {
let mid = CGPoint(
x: (polylinePoints[i+1].x + polylinePoints[i+2].x)/2,
y: (polylinePoints[i+1].y + polylinePoints[i+2].y)/2
)
if i < polylinePoints.count - 3 {
cuts.append(mid)
}
}
cuts.append(polylinePoints.last!) // 终点
return cuts
}
private func curveFactor(for middlePointCount: Int) -> CGFloat {
guard middlePointCount > 0 else { return 2.0 / 3.0 }
let n = CGFloat(middlePointCount)
return 0.30538319 * pow(n, -2.1518917) + 0.36128348
}
public func buildCurveSegments() {
let cuts = curveCutPoints
var segments: [(CGPoint, CGPoint, CGPoint, CGPoint)] = []
factor = curveFactor(for: polylinePoints.count-2)
for i in 0..<(cuts.count - 1) {
let p0 = cuts[i]
let p1 = cuts[i+1]
// segment index
let startIndex = i
let endIndex = i+1
let tangentStart: CGPoint
if startIndex < polylinePoints.count - 1 {
tangentStart = CGPoint(
x: (polylinePoints[startIndex + 1].x - polylinePoints[startIndex].x) * factor,
y: (polylinePoints[startIndex + 1].y - polylinePoints[startIndex].y) * factor
)
} else {
tangentStart = .zero
}
let tangentEnd: CGPoint
if endIndex < polylinePoints.count - 1 {
tangentEnd = CGPoint(
x: (polylinePoints[endIndex + 1].x - polylinePoints[endIndex].x) * factor,
y: (polylinePoints[endIndex + 1].y - polylinePoints[endIndex].y) * factor
)
} else {
tangentEnd = .zero
}
let c1 = CGPoint(x: p0.x + tangentStart.x, y: p0.y + tangentStart.y)
let c2 = CGPoint(x: p1.x - tangentEnd.x, y: p1.y - tangentEnd.y)
segments.append((p0: p0, c1: c1, c2: c2, p1: p1))
}
cachedSegments = segments
}
/// 采样 曲线
func sampleCurve(stepsPerSegment: Int = 50) -> [CGPoint] {
var points: [CGPoint] = []
buildCurveSegments()
for seg in cachedSegments {
let sampled = sampleCurve(p0: seg.p0, c1: seg.c1, c2: seg.c2, p1: seg.p1, steps: stepsPerSegment)
if !points.isEmpty { points.removeLast() } // 避免重复点
points.append(contentsOf: sampled)
}
return points
}
private func sampleCurve(p0: CGPoint, c1: CGPoint, c2: CGPoint, p1: CGPoint, steps: Int) -> [CGPoint] {
var result: [CGPoint] = []
for i in 0...steps {
let t = CGFloat(i)/CGFloat(steps)
let mt = 1-t
let x = mt*mt*mt*p0.x + 3*mt*mt*t*c1.x + 3*mt*t*t*c2.x + t*t*t*p1.x
let y = mt*mt*mt*p0.y + 3*mt*mt*t*c1.y + 3*mt*t*t*c2.y + t*t*t*p1.y
result.append(CGPoint(x:x, y:y))
}
return result
}
var tangentPoints: [CGPoint] { polylinePoints }
func value(at x: CGFloat) -> CGFloat {
let xClamped = min(max(x, 0), 1)
let segments = cachedSegments
for seg in segments {
if xClamped >= seg.p0.x && xClamped <= seg.p1.x {
let t = solveT(forX: xClamped,
p0: seg.p0.x,
c1: seg.c1.x,
c2: seg.c2.x,
p1: seg.p1.x)
return cubic(p0: seg.p0.y,
c1: seg.c1.y,
c2: seg.c2.y,
p1: seg.p1.y,
t: t)
}
}
return segments.last?.p1.y ?? 0
}
private func solveT(
forX x: CGFloat,
p0: CGFloat,
c1: CGFloat,
c2: CGFloat,
p1: CGFloat,
iterations: Int = 18
) -> CGFloat {
var low: CGFloat = 0
var high: CGFloat = 1
var t: CGFloat = 0.5
for _ in 0..<iterations {
t = (low + high) * 0.5
let xt = cubic(p0: p0, c1: c1, c2: c2, p1: p1, t: t)
if xt < x {
low = t
} else {
high = t
}
}
return t
}
@inline(__always)
private func cubic(
p0: CGFloat,
c1: CGFloat,
c2: CGFloat,
p1: CGFloat,
t: CGFloat
) -> CGFloat {
let mt = 1 - t
return mt*mt*mt*p0
+ 3*mt*mt*t*c1
+ 3*mt*t*t*c2
+ t*t*t*p1
}
static func exportCurvePoints(_ curve: PressureCurve) -> [NSNumber] {
var arr: [NSNumber] = []
for p in curve.polylinePoints {
arr.append(NSNumber(value: Float(p.x)))
arr.append(NSNumber(value: Float(p.y)))
}
return arr
}
static func importCurvePoints(_ numbers: [NSNumber]) -> [CGPoint] {
var pts: [CGPoint] = []
let count = numbers.count / 2
for i in 0..<count {
let x = CGFloat(numbers[i*2].floatValue)
let y = CGFloat(numbers[i*2 + 1].floatValue)
pts.append(CGPoint(x: x, y: y))
}
return pts
}
static func generateLinearCurvePoints(from pressures: [CGFloat]) -> [CGPoint] {
guard !pressures.isEmpty else { return [] }
let samples = pressures.map { min(max($0, 0), 1) }
let maxPressure = samples.max() ?? 0
if maxPressure < 1 {
let platformX = maxPressure
return [
CGPoint(x: 0, y: 0),
CGPoint(x: platformX, y: 1),
CGPoint(x: platformX, y: 1),
CGPoint(x: 1, y: 1)
]
} else {
return [
CGPoint(x: 0, y: 0),
CGPoint(x: 1, y: 1)
]
}
}
}
// MARK: - PressureCurveView
final class PressureCurveView: UIView {
let curve = PressureCurve()
private var draggingIndex: Int? = nil
private let hitRadius: CGFloat = 22
enum PressureTestStage: UInt8 {
case drawingStage
case curveStage
}
public var testStage: PressureTestStage = .curveStage
private var pressures:[CGFloat] = []
override init(frame: CGRect) {
super.init(frame: frame)
backgroundColor = .black
}
required init?(coder: NSCoder) { fatalError() }
public var showGraph: Bool = false {
didSet { setNeedsDisplay() } // 改变开关立即刷新
}
override func draw(_ rect: CGRect) {
guard let ctx = UIGraphicsGetCurrentContext() else { return }
ctx.saveGState()
// 缩放和平移
let scale: CGFloat = 0.95
let dx = (bounds.width - bounds.width * scale) / 2
let dy = (bounds.height - bounds.height * scale) / 2
ctx.translateBy(x: dx, y: dy)
ctx.scaleBy(x: scale, y: scale)
if showGraph {
drawGrid(ctx)
drawCurve(ctx)
drawTangents(ctx)
}
ctx.restoreGState()
}
private func drawGrid(_ ctx: CGContext) {
ctx.setStrokeColor(UIColor.darkGray.cgColor)
ctx.setLineWidth(1)
for i in 0...4 {
let t = CGFloat(i)/4
let x = bounds.width*t
let y = bounds.height*t
ctx.move(to: CGPoint(x:x, y:0))
ctx.addLine(to: CGPoint(x:x, y:bounds.height))
ctx.move(to: CGPoint(x:0, y:y))
ctx.addLine(to: CGPoint(x:bounds.width, y:y))
}
ctx.strokePath()
}
private func drawCurve(_ ctx: CGContext) {
ctx.setStrokeColor(UIColor.systemBlue.cgColor)
ctx.setLineWidth(3)
let points = curve.sampleCurve(stepsPerSegment: 60)
for (i,p) in points.enumerated() {
let mapped = map(p)
if i==0 { ctx.move(to:mapped) } else { ctx.addLine(to:mapped) }
}
ctx.strokePath()
}
private func drawTangents(_ ctx: CGContext) {
let pts = curve.tangentPoints
ctx.setStrokeColor(UIColor.systemPurple.cgColor)
ctx.setLineWidth(1.5)
for i in 0..<pts.count {
let p = map(pts[i])
if i==0 { ctx.move(to:p) } else { ctx.addLine(to:p) }
}
ctx.strokePath()
for p in pts {
let c = map(p)
let r: CGFloat = 4
ctx.setFillColor(UIColor.systemPurple.cgColor)
ctx.fill(CGRect(x:c.x-r, y:c.y-r, width:r*2, height:r*2))
}
}
private func map(_ p: CGPoint) -> CGPoint {
CGPoint(x:p.x*bounds.width, y:(1-p.y)*bounds.height)
}
private func unmap(_ p: CGPoint) -> CGPoint {
CGPoint(
x: p.x / bounds.width,
y: 1 - (p.y / bounds.height)
)
}
private func hitTestPoint(_ location: CGPoint) -> Int? {
let pts = curve.tangentPoints
for (i, p) in pts.enumerated() {
let c = map(p)
let dx = c.x - location.x
let dy = c.y - location.y
if dx*dx + dy*dy <= hitRadius*hitRadius {
return i
}
}
return nil
}
override func touchesBegan(_ touches: Set<UITouch>, with event: UIEvent?) {
guard let touch = touches.first else { return }
let location = touch.location(in: self)
if testStage == .drawingStage {
return
}
draggingIndex = hitTestPoint(location)
if draggingIndex == nil {
guard
let touch = touches.first
else { return }
let location = touch.location(in: self)
var point = unmap(location)
point.x = min(max(point.x, 0), 1)
point.y = min(max(point.y, 0), 1)
var insertIdx:Int = -1
for (idx, polylinePoint) in curve.polylinePoints.enumerated() {
if point.x <= polylinePoint.x {
insertIdx = idx
break
}
}
if insertIdx >= 0 {
curve.polylinePoints.insert(point, at: insertIdx)
draggingIndex = insertIdx
}
setNeedsDisplay()
}
}
override func touchesMoved(_ touches: Set<UITouch>, with event: UIEvent?) {
if testStage == .drawingStage {
let altitude = touches.first!.altitudeAngle
let force = (touches.first!.force/touches.first!.maximumPossibleForce)/sin(altitude)
pressures.append(force)
return
}
guard
let touch = touches.first,
let idx = draggingIndex
else { return }
let location = touch.location(in: self)
var point = unmap(location)
if draggingIndex == 0 {
point.x = 0
point.y = min(max(point.y, 0), 1)
}
else if draggingIndex == curve.polylinePoints.count-1 {
point.x = 1
point.y = min(max(point.y, 0), 1)
}
else {
let nextPoint = curve.polylinePoints[idx+1]
let previousPoint = curve.polylinePoints[idx-1]
point.x = min(max(point.x, previousPoint.x), nextPoint.x)
point.y = min(max(point.y, 0), 1)
}
// print("currenPoint \(point)")
curve.polylinePoints[idx] = point
setNeedsDisplay()
}
override func touchesEnded(_ touches: Set<UITouch>, with event: UIEvent?) {
if testStage == .drawingStage {
curve.polylinePoints = PressureCurve.generateLinearCurvePoints(from: pressures)
testStage = .curveStage
showGraph = true
let parentVC = parentViewController as! PressureCurveViewController
parentVC.setupNavigationBar()
parentVC.showCurveStageTips()
}
draggingIndex = nil
}
override func touchesCancelled(_ touches: Set<UITouch>, with event: UIEvent?) {
draggingIndex = nil
}
}
final class PressureCurveLUT {
static let levels = 2048
public let table: [Float]
init(curve: PressureCurve) {
curve.buildCurveSegments()
var t = [Float](repeating: 0, count: Self.levels)
let maxIndex = Float(Self.levels - 1)
for i in 0..<Self.levels {
let x = CGFloat(Float(i) / maxIndex)
let y = curve.value(at: x)
t[i] = Float(y)
}
self.table = t
}
@inline(__always)
func value(at x: Float) -> Float {
let clamped = min(max(x, 0), 1)
let index = Int((clamped * Float(Self.levels - 1)).rounded())
return table[index]
}
}
extension UIView {
var parentViewController: UIViewController? {
var responder: UIResponder? = self
while let r = responder {
if let vc = r as? UIViewController {
return vc
}
responder = r.next
}
return nil
}
}
// MARK: - ViewController
class PressureCurveViewController: UIViewController {
private var navBar = UINavigationBar()
private var container = UIView()
private var curveView = PressureCurveView()
private var minPressure: CGFloat = 0
private var maxPressure: CGFloat = 1
override func viewDidLoad() {
super.viewDidLoad()
self.view.isOpaque = false
self.view.backgroundColor = UIColor.clear // 半透明遮罩
let curveWidth: CGFloat = 400
let curveHeight: CGFloat = 400
curveView = PressureCurveView(frame: CGRect(x: 0, y: 0, width: curveWidth, height: curveHeight))
curveView.center = self.view.center
curveView.backgroundColor = .white
curveView.layer.cornerRadius = 0
curveView.layer.masksToBounds = true
container = UIView(frame: CGRect(x: 0, y: 0, width: curveWidth+10, height: curveHeight+50))
container.center = CGPoint(x: self.view.bounds.midX, y: self.view.bounds.midY-20)
container.backgroundColor = .white
container.layer.cornerRadius = 12
container.layer.masksToBounds = true
container.layer.borderWidth = 2
container.layer.borderColor = ThemeManager.separatorColor().cgColor
container.addSubview(curveView)
self.loadPersistedCurve()
self.curveView.showGraph = true
self.setupNavigationBar()
self.view.addSubview(container)
curveView.translatesAutoresizingMaskIntoConstraints = false
NSLayoutConstraint.activate([
curveView.centerXAnchor.constraint(equalTo: self.view.centerXAnchor),
curveView.centerYAnchor.constraint(equalTo: self.view.centerYAnchor),
curveView.widthAnchor.constraint(equalToConstant: curveWidth),
curveView.heightAnchor.constraint(equalToConstant: curveHeight)
])
}
override func viewDidAppear(_ animated: Bool) {
AlertControllerUtil.cancelButtonString = SwiftLocalizationHelper.localizedString(forKey: "No")
AlertControllerUtil.showAlert(
in: self,
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: SwiftLocalizationHelper.localizedString(forKey:"Reset the pen pressure curve by starting from the pen pressure test?"),
withCancel: true,
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "Yes"),
countdown: 0,
action: {},
completion: {
if AlertControllerUtil.actionCancelled {
self.curveView.showGraph = true
self.curveView.testStage = .curveStage
self.curveView.setNeedsDisplay()
self.setupNavigationBar()
self.showCurveStageTips()
}
else {
self.pressureRangeTest()
}
}
)
}
private func loadPersistedCurve() {
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
let persistedCurvePoints = PressureCurve.importCurvePoints(oscProfileMan.getSelectedProfile().pressureCurvePoints)
curveView.curve.polylinePoints = persistedCurvePoints
curveView.setNeedsDisplay()
}
public func setupNavigationBar() {
// 1. 创建 UINavigationBar
navBar.removeFromSuperview()
navBar = UINavigationBar()
navBar.translatesAutoresizingMaskIntoConstraints = false
container.addSubview(navBar)
var navItem = UINavigationItem()
if #available(iOS 13.0, *) {
let appearance = UINavigationBarAppearance()
appearance.configureWithTransparentBackground() // 透明背景
appearance.shadowColor = nil // 去掉底部细线
appearance.backgroundColor = .clear // 可以额外设置完全透明
navBar.standardAppearance = appearance
navBar.scrollEdgeAppearance = appearance
} else {
navBar.setBackgroundImage(UIImage(), for: .default)
navBar.shadowImage = UIImage() // 去掉底部线
navBar.isTranslucent = true
navBar.backgroundColor = .clear
}
// 2. 添加约束 (顶部,左右)
NSLayoutConstraint.activate([
navBar.topAnchor.constraint(equalTo: container.safeAreaLayoutGuide.topAnchor),
navBar.leadingAnchor.constraint(equalTo: container.leadingAnchor),
navBar.trailingAnchor.constraint(equalTo: container.trailingAnchor),
navBar.heightAnchor.constraint(equalToConstant: 42) // 导航栏标准高度
])
if curveView.testStage == .curveStage {
let resetButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Reset"), style: .plain, target: self, action: #selector(resetTapped))
let rangeTestButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Pressure-range-test"), style: .plain, target: self, action: #selector(pressureRangeTest))
let saveButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Save"), style: .plain, target: self, action: #selector(saveTapped))
let quitButton = UIBarButtonItem(title: SwiftLocalizationHelper.localizedString(forKey: "Exit"), style: .plain, target: self, action: #selector(exitTapped))
navItem.leftBarButtonItems = [resetButton, rangeTestButton]
navItem.rightBarButtonItems = [quitButton, saveButton]
}
if curveView.testStage == .drawingStage {
navItem = UINavigationItem(title: SwiftLocalizationHelper.localizedString(forKey: "Pressure Range Test"))
}
// 4. 将 UINavigationItem 添加到导航栏
navBar.items = [navItem]
}
public func showCurveStageTips() {
AlertControllerUtil.showAlert(
in: self,
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: SwiftLocalizationHelper.localizedString(forKey:"Adjust the pressure curve by adding or dragging the purple squares."),
withCancel: false,
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"),
countdown: 0
)
}
@objc private func resetTapped() {
curveView.curve.polylinePoints = [CGPoint(x: 0, y: 0), CGPoint(x: 1, y: 1)]
curveView.setNeedsDisplay()
}
@objc private func pressureRangeTest() {
AlertControllerUtil.showAlert(
in: self,
title: SwiftLocalizationHelper.localizedString(forKey: "Pressure Range Test"),
message: SwiftLocalizationHelper.localizedString(forKey:"Draw freely and vary pen pressure naturally."),
withCancel: false,
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"),
countdown: 0,
completion:{
self.curveView.showGraph = false
self.curveView.testStage = .drawingStage
self.curveView.setNeedsDisplay()
self.setupNavigationBar()
})
}
@objc private func saveTapped() {
let oscProfileMan = OSCProfilesManager.sharedManager(CGRectZero)
let selectedProfile = oscProfileMan.getSelectedProfile()
selectedProfile.pressureCurvePoints = PressureCurve.exportCurvePoints(curveView.curve)
oscProfileMan.replaceSelectedProfile(with: selectedProfile, overwriteDefault: true)
AlertControllerUtil.showAlert(
in: self,
title: SwiftLocalizationHelper.localizedString(forKey: "Pen Pressure Curve"),
message: SwiftLocalizationHelper.localizedString(forKey:"Pressure curve saved with current on-screen widget profile!"),
withCancel: false,
buttonTitle: SwiftLocalizationHelper.localizedString(forKey: "OK"),
countdown: 0,
completion: {
PencilHandler.sharedInstance?.setupPressureLUT()
}
)
}
@objc private func exitTapped() {
self.dismiss(animated: true)
}
}
+2
View File
@@ -39,6 +39,8 @@
@property (assign, nonatomic) CGRect originalFrame;
@property (assign, nonatomic) bool widgetToolOpened;
@property (strong, nonatomic) OnScreenControls* onScreenControls;
@property (weak, nonatomic) PencilHandler* pencilHandler;
- (void) setupStreamView:(ControllerSupport*)controllerSupport
interactionDelegate:(id<UserInteractionDelegate>)interactionDelegate
+14 -31
View File
@@ -58,7 +58,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
int localMousePointerMode;
UIResponder* touchHandler;
PencilHandler* pencilHandler;
id<UserInteractionDelegate> interactionDelegate;
NSTimer* interactionTimer;
@@ -129,8 +128,9 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
self->touchHandler = [[RelativeTouchHandler alloc] initWithView:self];
#else
pencilHandler = [[PencilHandler alloc] initWithStreamView:self settings:settings];
PencilHandler.sharedInstance = [[PencilHandler alloc] initWithStreamView:self settings:settings];
_pencilHandler = PencilHandler.sharedInstance;
// iOS uses touch Mode depending on user preference
switch (settings.touchMode.intValue) {
@@ -728,6 +728,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
CGPoint location = [self adjustCoordinatesForVideoArea:[event locationInView:self]];
CGSize videoSize = [self getVideoAreaSize];
CGFloat pressure = (event.force / event.maximumPossibleForce) / sin(event.altitudeAngle);
return LiSendPenEvent(type, LI_TOOL_TYPE_PEN, 0, location.x / videoSize.width, location.y / videoSize.height,
(event.force / event.maximumPossibleForce) / sin(event.altitudeAngle),
0.0f, 0.0f,
@@ -788,14 +790,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
if (@available(iOS 13.4, *)) { // now only pencil events are restricted
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if(settings.pencilTickMode.intValue == PencilTickDisabled){
[self sendStylusEvent:touch];
return;
}
else{
[pencilHandler touchesBegan:touches withEvent:event];
return;
}
[_pencilHandler touchesBegan:touches withEvent:event];
return;
}
}
}
@@ -950,14 +946,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if(settings.pencilTickMode.intValue == PencilTickDisabled){
[self sendStylusEvent:touch];
return;
}
else{
[pencilHandler touchesMoved:touches withEvent:event];
return;
}
[_pencilHandler touchesMoved:touches withEvent:event];
return;
}
if (@available(iOS 13.4, *)) {
UITouch *touch = [touches anyObject];
@@ -1065,14 +1055,8 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
if (@available(iOS 13.4, *)){ //now only pencil events are restricted
for (UITouch* touch in touches) {
if (touch.type == UITouchTypePencil) {
if(settings.pencilTickMode.intValue == PencilTickDisabled){
[self sendStylusEvent:touch];
return;
}
else{
[pencilHandler touchesEnded:touches withEvent:event];
return;
}
[_pencilHandler touchesEnded:touches withEvent:event];
return;
}
}
}
@@ -1450,18 +1434,17 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5;
}
- (void)switchPencilHover{
[pencilHandler switchPencilHoever];
[_pencilHandler switchPencilHoever];
}
- (void)enablePencilHover{
[pencilHandler enablePencilHover];
[_pencilHandler enablePencilHover];
}
- (void)disablePencilHover{
[pencilHandler disablePencilHover];
[_pencilHandler disablePencilHover];
}
#if !TARGET_OS_TV
- (BOOL)isMultipleTouchEnabled {
return YES;
+230
View File
@@ -697,6 +697,29 @@
}
}
},
"Adjust the pressure curve by adding or dragging the purple squares." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "通过添加或移动紫色方块点调整笔压曲线。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "透過新增或移動紫色方塊點來調整筆壓曲線。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "透過新增或移動紫色方塊點來調整筆壓曲線。"
}
}
}
},
"Alias (optional)" : {
"extractionState" : "manual",
"localizations" : {
@@ -2056,6 +2079,29 @@
}
}
},
"Draw freely and vary pen pressure naturally." : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "请随意绘制,笔压自然变化即可。"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "請隨意繪製,筆壓自然變化即可。"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "請隨意繪製,筆壓自然變化即可。"
}
}
}
},
"Drift Correction" : {
"extractionState" : "manual",
"localizations" : {
@@ -4033,6 +4079,29 @@
}
}
},
"No" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "否"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "否"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "否"
}
}
}
},
"No video received from host." : {
"extractionState" : "manual",
"localizations" : {
@@ -4626,6 +4695,29 @@
}
}
},
"Pen Pressure Curve" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "笔压曲线"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓曲線"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓曲線"
}
}
}
},
"Pencil" : {
"extractionState" : "manual",
"localizations" : {
@@ -5157,6 +5249,75 @@
}
}
},
"Pressure curve saved with current on-screen widget profile!" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "笔压曲线已随屏显控件配置保存!"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓曲線已隨螢幕小工具設定儲存!"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓曲線已隨螢幕小工具設定儲存!"
}
}
}
},
"Pressure Range Test" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "笔压范围测试"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓範圍測試"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓範圍測試"
}
}
}
},
"Pressure-range-test" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "笔压范围测试"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓範圍測試"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓範圍測試"
}
}
}
},
"Profile %@ duplicated from current layout" : {
"extractionState" : "manual",
"localizations" : {
@@ -5687,6 +5848,52 @@
}
}
},
"Reset" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "重置"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "重置"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "重置"
}
}
}
},
"Reset the pen pressure curve by starting from the pen pressure test?" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "是否从笔压范围测试开始重置笔压曲线?"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "是否從筆壓範圍測試開始重置筆壓曲線?"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "是否從筆壓範圍測試開始重置筆壓曲線?"
}
}
}
},
"Restart Required" : {
"extractionState" : "manual",
"localizations" : {
@@ -7384,6 +7591,29 @@
}
}
},
"Yes" : {
"extractionState" : "manual",
"localizations" : {
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "是"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "是"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "是"
}
}
}
},
"Your network connection isn't performing well. Reduce your video bitrate setting or try a faster connection." : {
"extractionState" : "manual",
"localizations" : {
@@ -240,6 +240,9 @@
@property (strong, nonatomic) IBOutlet UIStackView *rightStickMinOffsetStack;
@property (strong, nonatomic) IBOutlet UISlider *rightStickMinOffsetSlider;
@property (strong, nonatomic) IBOutlet UIStackView *pressureCurveStack;
@property (strong, nonatomic) IBOutlet UISwitch *pressureCurveSwitch;
@property (strong, nonatomic) IBOutlet UIStackView *testStack;
@@ -391,6 +391,8 @@ BOOL isCustomResolution(int resolutionSelected) {
[self rightStickMinOffsetSliderMoved:self.rightStickMinOffsetSlider];
[self.synthPhysicalInputSwitch setOn:oscProfile.synthesizePhysicalStick];
[self.pressureCurveSwitch setOn:oscProfile.pressureCurveEnabled];
}
- (void)saveGameProfileConfigs{
@@ -411,6 +413,7 @@ BOOL isCustomResolution(int resolutionSelected) {
&& oscProfile.reverseGyroHoldButton == self.reverseHoldButtonSwitch.isOn
&& (int16_t)(oscProfile.physicalLeftStickMinOffset) == (int16_t)self.leftStickMinOffsetSlider.value
&& (int16_t)(oscProfile.physicalRightStickMinOffset) == (int16_t)self.rightStickMinOffsetSlider.value
&& oscProfile.pressureCurveEnabled == self.pressureCurveSwitch.isOn
);
if(!configNotChanged){
@@ -426,6 +429,7 @@ BOOL isCustomResolution(int resolutionSelected) {
oscProfile.reverseGyroHoldButton = self.reverseHoldButtonSwitch.isOn;
oscProfile.physicalLeftStickMinOffset = (int16_t)self.leftStickMinOffsetSlider.value;
oscProfile.physicalRightStickMinOffset = (int16_t)self.rightStickMinOffsetSlider.value;
oscProfile.pressureCurveEnabled = self.pressureCurveSwitch.isOn;
[oscProfileMan replaceSelectedProfileWith:oscProfile overwriteDefault:YES];
}
}
@@ -850,12 +854,13 @@ BOOL isCustomResolution(int resolutionSelected) {
MenuSectionView* pencilSection = [[MenuSectionView alloc] init];
pencilSection.delegate = self;
pencilSection.sectionTitle = [LocalizationHelper localizedStringForKey:@"Pencil"];
pencilSection.identifier = @"SettingsSectionMotionControl";
pencilSection.identifier = @"SettingsSectionPencil";
if (@available(iOS 13.0, *)) {
[pencilSection setSectionWithIcon:[UIImage systemImageNamed:@"pencil.and.outline"] size:19 weight:UIImageSymbolWeightHeavy];
}
[self addSetting:self.pencilTickStack ofId:@"pencilTickStack" withInfoTag:YES withDynamicLabel:NO to:pencilSection];
[self addSetting:self.pencilTickIntervalStack ofId:@"pencilTickIntervalStack" withInfoTag:NO withDynamicLabel:YES to:pencilSection];
[self addSetting:self.pressureCurveStack ofId:@"pressureCurveStack" withInfoTag:NO withDynamicLabel:NO to:pencilSection];
[pencilSection addToParentStack:_parentStack];
}
@@ -3334,6 +3339,15 @@ BOOL isCustomResolution(int resolutionSelected) {
[self findDynamicLabelFromStack:self.pencilTickIntervalStack].text = [NSString stringWithFormat:@" %d μs ", (uint16_t)sender.value];
}
- (void)pressureCurveSwitchFlipped:(UISwitch* )sender{
if(sender.isOn && !settingsViewJustLoaded){
PressureCurveViewController* pressureCurveVC = [[PressureCurveViewController alloc] init];
pressureCurveVC.modalPresentationStyle = UIModalPresentationOverFullScreen;
self.definesPresentationContext = true;
[self presentViewController:pressureCurveVC animated:YES completion:nil];
}
}
- (void)loadPencilSettings:(TemporarySettings*) tempSettings{
if([self isIPad]){
self.pencilTickSelector.selectedSegmentIndex = tempSettings.pencilTickMode.intValue;
@@ -3343,6 +3357,9 @@ BOOL isCustomResolution(int resolutionSelected) {
[self.pencilTickIntervalSlider setValue:tempSettings.pencilTickIntervalUs.floatValue];
[self.pencilTickIntervalSlider addTarget:self action:@selector(pencilTickIntervalSliderMoved:) forControlEvents:UIControlEventValueChanged];
[self pencilTickIntervalSliderMoved:self.pencilTickIntervalSlider];
[self.pressureCurveSwitch setOn:true];
[self.pressureCurveSwitch addTarget:self action:@selector(pressureCurveSwitchFlipped:) forControlEvents:UIControlEventValueChanged];
}
}
@@ -936,6 +936,7 @@
- (void) returnToMainFrame {
[_streamView clearOnScreenWidgets];
if(micHandler) [micHandler clean];
PencilHandler.sharedInstance = nil;
// Reset display mode back to default
[self updatePreferredDisplayMode:NO];
+1
View File
@@ -9,4 +9,5 @@
#import "OSCProfilesManager.h"
#import "opus.h"
#import "opus_defines.h"
#import "ThemeManager.h"
// #import "RelativeTouchHandler.h"
+111 -21
View File
@@ -1267,6 +1267,36 @@
}
}
},
"b1N-z8-12w.text" : {
"comment" : "Class = \"UILabel\"; text = \"Pressure Curve\"; ObjectID = \"b1N-z8-12w\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Pressure Curve"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "笔压曲线"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓曲線"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆壓曲線"
}
}
}
},
"bfV-QT-eKw.text" : {
"comment" : "Class = \"UILabel\"; text = \"Use Built-in Mic\"; ObjectID = \"bfV-QT-eKw\";",
"extractionState" : "extracted_with_value",
@@ -1628,7 +1658,37 @@
}
},
"bVv-CW-kN8.segmentTitles[0]" : {
"comment" : "Class = \"UISegmentedControl\"; bVv-CW-kN8.segmentTitles[0] = \"Manual\"; ObjectID = \"bVv-CW-kN8\";",
"comment" : "Class = \"UISegmentedControl\"; bVv-CW-kN8.segmentTitles[0] = \"Off\"; ObjectID = \"bVv-CW-kN8\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
"stringUnit" : {
"state" : "new",
"value" : "Off"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "关闭"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "關閉"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "關閉"
}
}
}
},
"bVv-CW-kN8.segmentTitles[1]" : {
"comment" : "Class = \"UISegmentedControl\"; bVv-CW-kN8.segmentTitles[1] = \"Manual\"; ObjectID = \"bVv-CW-kN8\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
@@ -1636,29 +1696,23 @@
"state" : "new",
"value" : "Manual"
}
}
}
},
"bVv-CW-kN8.segmentTitles[1]" : {
"comment" : "Class = \"UISegmentedControl\"; bVv-CW-kN8.segmentTitles[1] = \"Timestamp\"; ObjectID = \"bVv-CW-kN8\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
},
"zh-Hans" : {
"stringUnit" : {
"state" : "new",
"value" : "Timestamp"
"state" : "translated",
"value" : "手动"
}
}
}
},
"bVv-CW-kN8.segmentTitles[2]" : {
"comment" : "Class = \"UISegmentedControl\"; bVv-CW-kN8.segmentTitles[2] = \"Off\"; ObjectID = \"bVv-CW-kN8\";",
"extractionState" : "extracted_with_value",
"localizations" : {
"en" : {
},
"zh-Hant" : {
"stringUnit" : {
"state" : "new",
"value" : "Off"
"state" : "translated",
"value" : "手動"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "手動"
}
}
}
@@ -4570,6 +4624,24 @@
"state" : "new",
"value" : "Pencil Tick"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "笔触Tick"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆觸Tick"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "筆觸Tick"
}
}
}
},
@@ -5338,6 +5410,24 @@
"state" : "new",
"value" : "Tick Interval"
}
},
"zh-Hans" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tick周期"
}
},
"zh-Hant" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tick 週期"
}
},
"zh-HK" : {
"stringUnit" : {
"state" : "translated",
"value" : "Tick 週期"
}
}
}
},