diff --git a/Resources/Info.plist b/Resources/Info.plist index 2f8b328..70e6261 100644 --- a/Resources/Info.plist +++ b/Resources/Info.plist @@ -19,9 +19,9 @@ CFBundlePackageType APPL CFBundleShortVersionString - 0.4.0 + 0.4.1 CFBundleVersion - 5 + 6 LSMinimumSystemVersion 13.0 NSHighResolutionCapable diff --git a/Sources/GrowthLapse/VideoProcessor.swift b/Sources/GrowthLapse/VideoProcessor.swift index 36167d9..c2f8c9d 100644 --- a/Sources/GrowthLapse/VideoProcessor.swift +++ b/Sources/GrowthLapse/VideoProcessor.swift @@ -924,15 +924,16 @@ final class VideoProcessor: ObservableObject { if let pose = sample.pose { guard [pose.x, pose.y, pose.height, pose.yaw, pose.roll].allSatisfy({ $0.isFinite }), (0...1).contains(pose.x), (0...1).contains(pose.y), (0...1).contains(pose.height), - abs(pose.yaw) <= .pi, abs(pose.roll) <= .pi, - pose.pitch == nil || (pose.pitch!.isFinite && abs(pose.pitch!) <= .pi) else { return false } + // Vision angles originate as Float; Float.pi is slightly larger than Double.pi. + abs(pose.yaw) <= Double(Float.pi), abs(pose.roll) <= Double(Float.pi), + pose.pitch == nil || (pose.pitch!.isFinite && abs(pose.pitch!) <= Double(Float.pi)) else { return false } } if let color = sample.color { guard color.luminance.isFinite, (0...1).contains(color.luminance), [color.redToGreen, color.blueToGreen].compactMap({ $0 }).allSatisfy({ $0.isFinite && $0 > 0 && $0 < 10 }) else { return false } } return true - }) else { throw VideoProcessingError.invalidSettings("Ungültige gespeicherte Bildanalyse.") } + }) else { throw VideoProcessingError.invalidSettings("Ungültige gespeicherte Bildanalyse: \(clip.displayName).") } } for variant in clip.availableVariants { guard variant.sourcePath.hasPrefix("/"), variant.duration.isFinite, variant.duration > 0, diff --git a/Tests/Integration/RenderIntegration.swift b/Tests/Integration/RenderIntegration.swift index f525ded..bb70dce 100644 --- a/Tests/Integration/RenderIntegration.swift +++ b/Tests/Integration/RenderIntegration.swift @@ -196,6 +196,25 @@ struct RenderIntegration { try require(loaded.loadProject(from: GrowthLapseProject.url(for: second.outputFileURL)) != nil, "Project reload") print("PASS failure preservation, save-as, cache reuse and project reload") + // Vision's single-precision ±pi must survive project validation unchanged. + var angleProject = second + let visionPi = Double(Float.pi) + angleProject.clips[0].visualAnalysis = ClipVisualAnalysis(sourceSignature: "angle-test", samples: [ + VisualSample(time: 0, faceCount: 1, + pose: FacePose(x: 0.5, y: 0.5, height: 0.3, yaw: visionPi, + roll: -visionPi, eyesVisible: true, pitch: visionPi), color: nil, quality: 0) + ]) + let angleURL = root.appendingPathComponent("vision-angles.json") + try JSONEncoder().encode(angleProject).write(to: angleURL) + let angleLoader = VideoProcessor() + let accepted = angleLoader.loadProject(from: angleURL) + try require(accepted?.clips[0].visualAnalysis == angleProject.clips[0].visualAnalysis, + "Valid Vision Float.pi angles rejected or altered") + angleProject.clips[0].visualAnalysis!.samples[0].pose!.roll = 3.15 + try JSONEncoder().encode(angleProject).write(to: angleURL) + try require(angleLoader.loadProject(from: angleURL) == nil, "Out-of-range angle accepted") + print("PASS Vision Float.pi project round-trip and invalid-angle rejection") + // Exercise failure after normalization, while FFmpeg is producing the final video. let wrapper = root.appendingPathComponent("ffmpeg-fail") let quotedFFmpeg = "'" + tools.ffmpeg.path.replacingOccurrences(of: "'", with: "'\\''") + "'"