From 6d4f192cbeda3543f914dbb86d353eec283ec4bb Mon Sep 17 00:00:00 2001 From: TrueZhuanjia Date: Thu, 8 Jan 2026 16:51:41 +0800 Subject: [PATCH] pressure curve implementation --- VoidLink.xcodeproj/project.pbxproj | 6 +- VoidLink/AlertControllerUtil.swift | 5 +- VoidLink/Base.lproj/iPad.storyboard | 34 +- VoidLink/Database/DataManager.h | 4 +- VoidLink/Input/CustomOSC/OSCProfile.h | 3 +- VoidLink/Input/CustomOSC/OSCProfile.m | 24 +- VoidLink/Input/PencilHandler.swift | 65 +- VoidLink/Input/PressureCurve.swift | 663 ++++++++++++++++++ VoidLink/Input/StreamView.h | 2 + VoidLink/Input/StreamView.m | 45 +- VoidLink/Localization/Localizable.xcstrings | 230 ++++++ .../ViewControllers/SettingsViewController.h | 3 + .../ViewControllers/SettingsViewController.m | 19 +- .../StreamFrameViewController.m | 1 + VoidLink/VoidLink-Bridging-Header.h | 1 + VoidLink/mul.lproj/iPad.xcstrings | 132 +++- 16 files changed, 1148 insertions(+), 89 deletions(-) create mode 100644 VoidLink/Input/PressureCurve.swift diff --git a/VoidLink.xcodeproj/project.pbxproj b/VoidLink.xcodeproj/project.pbxproj index d9505b41..4011d015 100644 --- a/VoidLink.xcodeproj/project.pbxproj +++ b/VoidLink.xcodeproj/project.pbxproj @@ -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 = ""; }; 180E6E362E93BFDB0067C73A /* AlertControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = AlertControllerUtil.swift; sourceTree = ""; }; 1817B61D2E6FD04D000D938D /* SafeTimer.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = SafeTimer.swift; sourceTree = ""; }; - 182042252EFEE46700309FF6 /* touchCentered.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = touchCentered.swift; sourceTree = ""; }; 1829CE3D2E2154DF0074A99A /* SceneDelegate.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = SceneDelegate.h; sourceTree = ""; }; 1829CE3E2E2154DF0074A99A /* SceneDelegate.m */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.objc; path = SceneDelegate.m; sourceTree = ""; }; 18341A022DEB65AB0094E5F2 /* FixedTintImageView.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = FixedTintImageView.h; sourceTree = ""; }; @@ -191,6 +191,7 @@ 18A765BC2ECD51FB004784AA /* ControllerUtil.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = ControllerUtil.swift; sourceTree = ""; }; 18B317022EE67A39009B62C2 /* InertialScroller.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = InertialScroller.swift; sourceTree = ""; }; 18BF141D2E0D7A00009DEE4F /* VoidLink v1.0.xcdatamodel */ = {isa = PBXFileReference; lastKnownFileType = wrapper.xcdatamodel; path = "VoidLink v1.0.xcdatamodel"; sourceTree = ""; }; + 18D43D7E2F050240008C326F /* PressureCurve.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PressureCurve.swift; sourceTree = ""; }; 18E4BCE92E891895000A8657 /* MotionHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = MotionHandler.swift; sourceTree = ""; }; 18FCADAE2ECAC23200787A96 /* PencilHandler.swift */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.swift; path = PencilHandler.swift; sourceTree = ""; }; 693B3A9A218638CD00982F7B /* Settings.bundle */ = {isa = PBXFileReference; lastKnownFileType = "wrapper.plug-in"; path = Settings.bundle; sourceTree = ""; }; @@ -585,7 +586,6 @@ FB1A67542132419700507771 /* Moonlight TV */, FB290CF019B2C406004C83CF /* Frameworks */, FB290CEF19B2C406004C83CF /* Products */, - 182042252EFEE46700309FF6 /* touchCentered.swift */, ); sourceTree = ""; }; @@ -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 */, diff --git a/VoidLink/AlertControllerUtil.swift b/VoidLink/AlertControllerUtil.swift index 0fc2452b..26dd29f4 100644 --- a/VoidLink/AlertControllerUtil.swift +++ b/VoidLink/AlertControllerUtil.swift @@ -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 diff --git a/VoidLink/Base.lproj/iPad.storyboard b/VoidLink/Base.lproj/iPad.storyboard index bf6277f4..aff9018a 100644 --- a/VoidLink/Base.lproj/iPad.storyboard +++ b/VoidLink/Base.lproj/iPad.storyboard @@ -1898,8 +1898,8 @@ - + @@ -1920,7 +1920,7 @@ - + @@ -1930,6 +1930,26 @@ + + + + + + + + + + + + + + + @@ -2045,6 +2065,8 @@ + + @@ -2988,7 +3010,7 @@ - + @@ -2997,13 +3019,13 @@ - + - + - + diff --git a/VoidLink/Database/DataManager.h b/VoidLink/Database/DataManager.h index b5900842..7ae81342 100644 --- a/VoidLink/Database/DataManager.h +++ b/VoidLink/Database/DataManager.h @@ -79,8 +79,8 @@ typedef NS_ENUM(NSInteger, WidgetSizeTransition) { }; typedef NS_ENUM(NSInteger, PencilTickMode) { - ManualTick, - PencilTickDisabled + PencilTickDisabled, + ManualTick }; - (void) saveSettings:(Settings*)settings diff --git a/VoidLink/Input/CustomOSC/OSCProfile.h b/VoidLink/Input/CustomOSC/OSCProfile.h index 7cae79cb..e5de1564 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.h +++ b/VoidLink/Input/CustomOSC/OSCProfile.h @@ -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 *pressureCurvePoints; +@property (nonatomic, assign) bool pressureCurveEnabled; - (id) initWithName:(NSString*)name buttonStates:(NSMutableArray*)buttonStates isSelected:(BOOL)isSelected; diff --git a/VoidLink/Input/CustomOSC/OSCProfile.m b/VoidLink/Input/CustomOSC/OSCProfile.m index 81917b36..e5683545 100644 --- a/VoidLink/Input/CustomOSC/OSCProfile.m +++ b/VoidLink/Input/CustomOSC/OSCProfile.m @@ -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; } diff --git a/VoidLink/Input/PencilHandler.swift b/VoidLink/Input/PencilHandler.swift index ae9f866b..d36c7d0b 100644 --- a/VoidLink/Input/PencilHandler.swift +++ b/VoidLink/Input/PencilHandler.swift @@ -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, 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){ diff --git a/VoidLink/Input/PressureCurve.swift b/VoidLink/Input/PressureCurve.swift new file mode 100644 index 00000000..d43bb42c --- /dev/null +++ b/VoidLink/Input/PressureCurve.swift @@ -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.. 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.. [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.. 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, 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, 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, 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, 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.. 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) + } +} diff --git a/VoidLink/Input/StreamView.h b/VoidLink/Input/StreamView.h index af6a6154..ed16c839 100644 --- a/VoidLink/Input/StreamView.h +++ b/VoidLink/Input/StreamView.h @@ -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)interactionDelegate diff --git a/VoidLink/Input/StreamView.m b/VoidLink/Input/StreamView.m index 37f4d700..263f2f00 100644 --- a/VoidLink/Input/StreamView.m +++ b/VoidLink/Input/StreamView.m @@ -58,7 +58,6 @@ static const double X1_MOUSE_SPEED_DIVISOR = 2.5; int localMousePointerMode; UIResponder* touchHandler; - PencilHandler* pencilHandler; id 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; diff --git a/VoidLink/Localization/Localizable.xcstrings b/VoidLink/Localization/Localizable.xcstrings index 0e75cb6b..e45ae284 100644 --- a/VoidLink/Localization/Localizable.xcstrings +++ b/VoidLink/Localization/Localizable.xcstrings @@ -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" : { diff --git a/VoidLink/ViewControllers/SettingsViewController.h b/VoidLink/ViewControllers/SettingsViewController.h index c33be4f5..383ed3ca 100644 --- a/VoidLink/ViewControllers/SettingsViewController.h +++ b/VoidLink/ViewControllers/SettingsViewController.h @@ -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; diff --git a/VoidLink/ViewControllers/SettingsViewController.m b/VoidLink/ViewControllers/SettingsViewController.m index 7775c365..ab35b5d4 100644 --- a/VoidLink/ViewControllers/SettingsViewController.m +++ b/VoidLink/ViewControllers/SettingsViewController.m @@ -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]; } } diff --git a/VoidLink/ViewControllers/StreamFrameViewController.m b/VoidLink/ViewControllers/StreamFrameViewController.m index 6bca3197..6a9dd00a 100644 --- a/VoidLink/ViewControllers/StreamFrameViewController.m +++ b/VoidLink/ViewControllers/StreamFrameViewController.m @@ -936,6 +936,7 @@ - (void) returnToMainFrame { [_streamView clearOnScreenWidgets]; if(micHandler) [micHandler clean]; + PencilHandler.sharedInstance = nil; // Reset display mode back to default [self updatePreferredDisplayMode:NO]; diff --git a/VoidLink/VoidLink-Bridging-Header.h b/VoidLink/VoidLink-Bridging-Header.h index 278a38e7..86a73d26 100644 --- a/VoidLink/VoidLink-Bridging-Header.h +++ b/VoidLink/VoidLink-Bridging-Header.h @@ -9,4 +9,5 @@ #import "OSCProfilesManager.h" #import "opus.h" #import "opus_defines.h" +#import "ThemeManager.h" // #import "RelativeTouchHandler.h" diff --git a/VoidLink/mul.lproj/iPad.xcstrings b/VoidLink/mul.lproj/iPad.xcstrings index f62ea6ad..3a312a3e 100644 --- a/VoidLink/mul.lproj/iPad.xcstrings +++ b/VoidLink/mul.lproj/iPad.xcstrings @@ -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 週期" + } } } },