From 424074aa4d85ee13ff5e3482ded04fbc446cf4c7 Mon Sep 17 00:00:00 2001 From: John Burwell Date: Thu, 28 May 2026 17:44:42 -0500 Subject: [PATCH] Deduplicate thumbnail renderer implementation --- .../MinecraftPackageThumbnailRenderer.swift | 185 ------------------ .../MinecraftPackageThumbnailRenderer.swift | 185 ------------------ .../project.pbxproj | 2 + 3 files changed, 2 insertions(+), 370 deletions(-) delete mode 100644 MinecraftPackagePreviewExtension/MinecraftPackageThumbnailRenderer.swift delete mode 100644 MinecraftPackageThumbnailExtension/MinecraftPackageThumbnailRenderer.swift diff --git a/MinecraftPackagePreviewExtension/MinecraftPackageThumbnailRenderer.swift b/MinecraftPackagePreviewExtension/MinecraftPackageThumbnailRenderer.swift deleted file mode 100644 index 68c73eb..0000000 --- a/MinecraftPackagePreviewExtension/MinecraftPackageThumbnailRenderer.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// MinecraftPackageThumbnailRenderer.swift -// MinecraftPackagePreviewExtension -// - -import AppKit -import Foundation - -enum MinecraftPackageThumbnailRenderer { - nonisolated static func makeThumbnail( - for inspection: MinecraftPackageInspector.InspectionResult, - size: CGSize, - scale: CGFloat = 1 - ) -> CGImage? { - let pixelSize = CGSize( - width: max(size.width * scale, 1), - height: max(size.height * scale, 1) - ) - - guard - let bitmap = NSBitmapImageRep( - bitmapDataPlanes: nil, - pixelsWide: max(Int(pixelSize.width.rounded(.up)), 1), - pixelsHigh: max(Int(pixelSize.height.rounded(.up)), 1), - bitsPerSample: 8, - samplesPerPixel: 4, - hasAlpha: true, - isPlanar: false, - colorSpaceName: .deviceRGB, - bytesPerRow: 0, - bitsPerPixel: 0 - ) - else { - return nil - } - - bitmap.size = pixelSize - - NSGraphicsContext.saveGraphicsState() - defer { NSGraphicsContext.restoreGraphicsState() } - - guard let context = NSGraphicsContext(bitmapImageRep: bitmap) else { - return nil - } - - NSGraphicsContext.current = context - let rect = CGRect(origin: .zero, size: pixelSize) - - if let iconURL = inspection.iconURL, - let iconImage = NSImage(contentsOf: iconURL) { - drawSquareThumbnail(iconImage, in: rect) - } else { - drawFallbackThumbnail(for: inspection.contentType, in: rect) - } - - context.flushGraphics() - return bitmap.cgImage - } - - nonisolated private static func drawSquareThumbnail(_ image: NSImage, in rect: CGRect) { - NSColor.clear.setFill() - rect.fill() - - let cornerRadius = min(rect.width, rect.height) * 0.10 - let clipRect = rect.insetBy(dx: max(rect.width * 0.02, 1), dy: max(rect.height * 0.02, 1)) - let clipPath = NSBezierPath( - roundedRect: clipRect, - xRadius: cornerRadius, - yRadius: cornerRadius - ) - clipPath.addClip() - - let imageSize = image.size - guard imageSize.width > 0, imageSize.height > 0 else { - image.draw(in: clipRect) - return - } - - let scale = max(clipRect.width / imageSize.width, clipRect.height / imageSize.height) - let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale) - let drawRect = CGRect( - x: clipRect.midX - drawSize.width / 2, - y: clipRect.midY - drawSize.height / 2, - width: drawSize.width, - height: drawSize.height - ) - - image.draw(in: drawRect) - - NSColor.black.withAlphaComponent(0.10).setStroke() - let borderPath = NSBezierPath( - roundedRect: clipRect.insetBy(dx: 0.5, dy: 0.5), - xRadius: max(cornerRadius - 0.5, 0), - yRadius: max(cornerRadius - 0.5, 0) - ) - borderPath.lineWidth = max(1, rect.width * 0.01) - borderPath.stroke() - } - - nonisolated private static func drawFallbackThumbnail( - for contentType: MinecraftContentType, - in rect: CGRect - ) { - let path = NSBezierPath( - roundedRect: rect, - xRadius: rect.width * 0.10, - yRadius: rect.width * 0.10 - ) - - let gradient = NSGradient(colors: backgroundColors(for: contentType)) - gradient?.draw(in: path, angle: -45) - - let badgeRect = rect.insetBy(dx: rect.width * 0.18, dy: rect.height * 0.18) - drawVoxelBadge(in: badgeRect) - - NSColor.black.withAlphaComponent(0.10).setStroke() - path.lineWidth = max(1, rect.width * 0.01) - path.stroke() - } - - nonisolated private static func drawVoxelBadge(in rect: CGRect) { - let topFace = NSBezierPath() - topFace.move(to: CGPoint(x: rect.midX, y: rect.maxY)) - topFace.line(to: CGPoint(x: rect.maxX, y: rect.maxY - rect.height * 0.18)) - topFace.line(to: CGPoint(x: rect.midX, y: rect.maxY - rect.height * 0.36)) - topFace.line(to: CGPoint(x: rect.minX, y: rect.maxY - rect.height * 0.18)) - topFace.close() - NSColor(calibratedRed: 0.78, green: 0.94, blue: 0.63, alpha: 1).setFill() - topFace.fill() - - let leftFace = NSBezierPath() - leftFace.move(to: CGPoint(x: rect.minX, y: rect.maxY - rect.height * 0.18)) - leftFace.line(to: CGPoint(x: rect.midX, y: rect.maxY - rect.height * 0.36)) - leftFace.line(to: CGPoint(x: rect.midX, y: rect.minY)) - leftFace.line(to: CGPoint(x: rect.minX, y: rect.minY + rect.height * 0.18)) - leftFace.close() - NSColor(calibratedRed: 0.34, green: 0.61, blue: 0.24, alpha: 1).setFill() - leftFace.fill() - - let rightFace = NSBezierPath() - rightFace.move(to: CGPoint(x: rect.maxX, y: rect.maxY - rect.height * 0.18)) - rightFace.line(to: CGPoint(x: rect.midX, y: rect.maxY - rect.height * 0.36)) - rightFace.line(to: CGPoint(x: rect.midX, y: rect.minY)) - rightFace.line(to: CGPoint(x: rect.maxX, y: rect.minY + rect.height * 0.18)) - rightFace.close() - NSColor(calibratedRed: 0.14, green: 0.34, blue: 0.13, alpha: 1).setFill() - rightFace.fill() - - NSColor.black.withAlphaComponent(0.18).setStroke() - [topFace, leftFace, rightFace].forEach { - $0.lineWidth = max(1, rect.width * 0.02) - $0.stroke() - } - } - - nonisolated private static func backgroundColors(for contentType: MinecraftContentType) -> [NSColor] { - switch contentType { - case .world: - return [ - NSColor(calibratedRed: 0.18, green: 0.48, blue: 0.25, alpha: 1), - NSColor(calibratedRed: 0.47, green: 0.75, blue: 0.31, alpha: 1) - ] - case .behaviorPack: - return [ - NSColor(calibratedRed: 0.58, green: 0.29, blue: 0.15, alpha: 1), - NSColor(calibratedRed: 0.86, green: 0.60, blue: 0.22, alpha: 1) - ] - case .resourcePack: - return [ - NSColor(calibratedRed: 0.12, green: 0.38, blue: 0.66, alpha: 1), - NSColor(calibratedRed: 0.31, green: 0.68, blue: 0.85, alpha: 1) - ] - case .skinPack: - return [ - NSColor(calibratedRed: 0.53, green: 0.19, blue: 0.43, alpha: 1), - NSColor(calibratedRed: 0.84, green: 0.39, blue: 0.62, alpha: 1) - ] - case .worldTemplate: - return [ - NSColor(calibratedRed: 0.23, green: 0.23, blue: 0.54, alpha: 1), - NSColor(calibratedRed: 0.53, green: 0.52, blue: 0.89, alpha: 1) - ] - } - } -} diff --git a/MinecraftPackageThumbnailExtension/MinecraftPackageThumbnailRenderer.swift b/MinecraftPackageThumbnailExtension/MinecraftPackageThumbnailRenderer.swift deleted file mode 100644 index 8e53188..0000000 --- a/MinecraftPackageThumbnailExtension/MinecraftPackageThumbnailRenderer.swift +++ /dev/null @@ -1,185 +0,0 @@ -// -// MinecraftPackageThumbnailRenderer.swift -// MinecraftPackageThumbnailExtension -// - -import AppKit -import Foundation - -enum MinecraftPackageThumbnailRenderer { - nonisolated static func makeThumbnail( - for inspection: MinecraftPackageInspector.InspectionResult, - size: CGSize, - scale: CGFloat = 1 - ) -> CGImage? { - let pixelSize = CGSize( - width: max(size.width * scale, 1), - height: max(size.height * scale, 1) - ) - - guard - let bitmap = NSBitmapImageRep( - bitmapDataPlanes: nil, - pixelsWide: max(Int(pixelSize.width.rounded(.up)), 1), - pixelsHigh: max(Int(pixelSize.height.rounded(.up)), 1), - bitsPerSample: 8, - samplesPerPixel: 4, - hasAlpha: true, - isPlanar: false, - colorSpaceName: .deviceRGB, - bytesPerRow: 0, - bitsPerPixel: 0 - ) - else { - return nil - } - - bitmap.size = pixelSize - - NSGraphicsContext.saveGraphicsState() - defer { NSGraphicsContext.restoreGraphicsState() } - - guard let context = NSGraphicsContext(bitmapImageRep: bitmap) else { - return nil - } - - NSGraphicsContext.current = context - let rect = CGRect(origin: .zero, size: pixelSize) - - if let iconURL = inspection.iconURL, - let iconImage = NSImage(contentsOf: iconURL) { - drawSquareThumbnail(iconImage, in: rect) - } else { - drawFallbackThumbnail(for: inspection.contentType, in: rect) - } - - context.flushGraphics() - return bitmap.cgImage - } - - nonisolated private static func drawSquareThumbnail(_ image: NSImage, in rect: CGRect) { - NSColor.clear.setFill() - rect.fill() - - let cornerRadius = min(rect.width, rect.height) * 0.10 - let clipRect = rect.insetBy(dx: max(rect.width * 0.02, 1), dy: max(rect.height * 0.02, 1)) - let clipPath = NSBezierPath( - roundedRect: clipRect, - xRadius: cornerRadius, - yRadius: cornerRadius - ) - clipPath.addClip() - - let imageSize = image.size - guard imageSize.width > 0, imageSize.height > 0 else { - image.draw(in: clipRect) - return - } - - let scale = max(clipRect.width / imageSize.width, clipRect.height / imageSize.height) - let drawSize = CGSize(width: imageSize.width * scale, height: imageSize.height * scale) - let drawRect = CGRect( - x: clipRect.midX - drawSize.width / 2, - y: clipRect.midY - drawSize.height / 2, - width: drawSize.width, - height: drawSize.height - ) - - image.draw(in: drawRect) - - NSColor.black.withAlphaComponent(0.10).setStroke() - let borderPath = NSBezierPath( - roundedRect: clipRect.insetBy(dx: 0.5, dy: 0.5), - xRadius: max(cornerRadius - 0.5, 0), - yRadius: max(cornerRadius - 0.5, 0) - ) - borderPath.lineWidth = max(1, rect.width * 0.01) - borderPath.stroke() - } - - nonisolated private static func drawFallbackThumbnail( - for contentType: MinecraftContentType, - in rect: CGRect - ) { - let path = NSBezierPath( - roundedRect: rect, - xRadius: rect.width * 0.10, - yRadius: rect.width * 0.10 - ) - - let gradient = NSGradient(colors: backgroundColors(for: contentType)) - gradient?.draw(in: path, angle: -45) - - let badgeRect = rect.insetBy(dx: rect.width * 0.18, dy: rect.height * 0.18) - drawVoxelBadge(in: badgeRect) - - NSColor.black.withAlphaComponent(0.10).setStroke() - path.lineWidth = max(1, rect.width * 0.01) - path.stroke() - } - - nonisolated private static func drawVoxelBadge(in rect: CGRect) { - let topFace = NSBezierPath() - topFace.move(to: CGPoint(x: rect.midX, y: rect.maxY)) - topFace.line(to: CGPoint(x: rect.maxX, y: rect.maxY - rect.height * 0.18)) - topFace.line(to: CGPoint(x: rect.midX, y: rect.maxY - rect.height * 0.36)) - topFace.line(to: CGPoint(x: rect.minX, y: rect.maxY - rect.height * 0.18)) - topFace.close() - NSColor(calibratedRed: 0.78, green: 0.94, blue: 0.63, alpha: 1).setFill() - topFace.fill() - - let leftFace = NSBezierPath() - leftFace.move(to: CGPoint(x: rect.minX, y: rect.maxY - rect.height * 0.18)) - leftFace.line(to: CGPoint(x: rect.midX, y: rect.maxY - rect.height * 0.36)) - leftFace.line(to: CGPoint(x: rect.midX, y: rect.minY)) - leftFace.line(to: CGPoint(x: rect.minX, y: rect.minY + rect.height * 0.18)) - leftFace.close() - NSColor(calibratedRed: 0.34, green: 0.61, blue: 0.24, alpha: 1).setFill() - leftFace.fill() - - let rightFace = NSBezierPath() - rightFace.move(to: CGPoint(x: rect.maxX, y: rect.maxY - rect.height * 0.18)) - rightFace.line(to: CGPoint(x: rect.midX, y: rect.maxY - rect.height * 0.36)) - rightFace.line(to: CGPoint(x: rect.midX, y: rect.minY)) - rightFace.line(to: CGPoint(x: rect.maxX, y: rect.minY + rect.height * 0.18)) - rightFace.close() - NSColor(calibratedRed: 0.14, green: 0.34, blue: 0.13, alpha: 1).setFill() - rightFace.fill() - - NSColor.black.withAlphaComponent(0.18).setStroke() - [topFace, leftFace, rightFace].forEach { - $0.lineWidth = max(1, rect.width * 0.02) - $0.stroke() - } - } - - nonisolated private static func backgroundColors(for contentType: MinecraftContentType) -> [NSColor] { - switch contentType { - case .world: - return [ - NSColor(calibratedRed: 0.18, green: 0.48, blue: 0.25, alpha: 1), - NSColor(calibratedRed: 0.47, green: 0.75, blue: 0.31, alpha: 1) - ] - case .behaviorPack: - return [ - NSColor(calibratedRed: 0.58, green: 0.29, blue: 0.15, alpha: 1), - NSColor(calibratedRed: 0.86, green: 0.60, blue: 0.22, alpha: 1) - ] - case .resourcePack: - return [ - NSColor(calibratedRed: 0.12, green: 0.38, blue: 0.66, alpha: 1), - NSColor(calibratedRed: 0.31, green: 0.68, blue: 0.85, alpha: 1) - ] - case .skinPack: - return [ - NSColor(calibratedRed: 0.53, green: 0.19, blue: 0.43, alpha: 1), - NSColor(calibratedRed: 0.84, green: 0.39, blue: 0.62, alpha: 1) - ] - case .worldTemplate: - return [ - NSColor(calibratedRed: 0.23, green: 0.23, blue: 0.54, alpha: 1), - NSColor(calibratedRed: 0.53, green: 0.52, blue: 0.89, alpha: 1) - ] - } - } -} diff --git a/World Manager for Minecraft.xcodeproj/project.pbxproj b/World Manager for Minecraft.xcodeproj/project.pbxproj index 71d8f6e..5d1c1a7 100644 --- a/World Manager for Minecraft.xcodeproj/project.pbxproj +++ b/World Manager for Minecraft.xcodeproj/project.pbxproj @@ -89,6 +89,7 @@ isa = PBXFileSystemSynchronizedBuildFileExceptionSet; membershipExceptions = ( Models/MinecraftContentItem.swift, + QuickLook/MinecraftPackageThumbnailRenderer.swift, QuickLook/MinecraftPackageTypes.swift, Services/BedrockLevelMetadataDecoder.swift, Services/MinecraftContentMetadataReader.swift, @@ -102,6 +103,7 @@ membershipExceptions = ( Models/MinecraftContentItem.swift, QuickLook/MinecraftPackageQuickLookModel.swift, + QuickLook/MinecraftPackageThumbnailRenderer.swift, QuickLook/MinecraftPackageTypes.swift, Services/BedrockLevelMetadataDecoder.swift, Services/MinecraftContentMetadataReader.swift,