Refactoring [skip ci]

This commit is contained in:
Ali 2023-04-19 23:47:38 +04:00
parent 61b95461d5
commit 6a548e11a6
277 changed files with 613 additions and 19657 deletions

View File

@ -20,7 +20,6 @@ swift_library(
"//submodules/Postbox:Postbox",
"//submodules/TelegramCore:TelegramCore",
"//submodules/MusicAlbumArtResources:MusicAlbumArtResources",
"//submodules/MeshAnimationCache:MeshAnimationCache",
"//submodules/Utils/RangeSet:RangeSet",
"//submodules/InAppPurchaseManager:InAppPurchaseManager",
"//submodules/TextFormat:TextFormat",

View File

@ -10,7 +10,6 @@ import AsyncDisplayKit
import Display
import DeviceLocationManager
import TemporaryCachedPeerDataManager
import MeshAnimationCache
import InAppPurchaseManager
import AnimationCache
import MultiAnimationRenderer
@ -935,7 +934,6 @@ public protocol AccountContext: AnyObject {
var currentCountriesConfiguration: Atomic<CountriesConfiguration> { get }
var cachedGroupCallContexts: AccountGroupCallContextCache { get }
var meshAnimationCache: MeshAnimationCache { get }
var animationCache: AnimationCache { get }
var animationRenderer: MultiAnimationRenderer { get }

View File

@ -95,8 +95,8 @@ public enum PeerMessagesPlaylistLocation: Equatable, SharedMediaPlaylistLocation
}
}
public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayerType? {
func extractFileMedia(_ message: Message) -> TelegramMediaFile? {
public func peerMessageMediaPlayerType(_ message: EngineMessage) -> MediaManagerPlayerType? {
func extractFileMedia(_ message: EngineMessage) -> TelegramMediaFile? {
var file: TelegramMediaFile?
for media in message.media {
if let media = media as? TelegramMediaFile {
@ -120,7 +120,7 @@ public func peerMessageMediaPlayerType(_ message: Message) -> MediaManagerPlayer
return nil
}
public func peerMessagesMediaPlaylistAndItemId(_ message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? {
public func peerMessagesMediaPlaylistAndItemId(_ message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> (SharedMediaPlaylistId, SharedMediaPlaylistItemId)? {
if isGlobalSearch && !isDownloadList {
return (PeerMessagesMediaPlaylistId.custom, PeerMessagesMediaPlaylistItemId(messageId: message.id, messageIndex: message.index))
} else if isRecentActions && !isDownloadList {

View File

@ -3,7 +3,6 @@ import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import TelegramPresentationData
@ -371,7 +370,7 @@ private final class DayComponent: Component {
private var currentSelection: DaySelection?
private(set) var timestamp: Int32?
private(set) var index: MessageIndex?
private(set) var index: EngineMessage.Index?
private var isHighlightingEnabled: Bool = false
init() {
@ -983,12 +982,12 @@ public final class CalendarMessageScreen: ViewController {
private weak var controller: CalendarMessageScreen?
private let context: AccountContext
private let peerId: PeerId
private let peerId: EnginePeer.Id
private let initialTimestamp: Int32
private let enableMessageRangeDeletion: Bool
private let canNavigateToEmptyDays: Bool
private let navigateToOffset: (Int, Int32) -> Void
private let previewDay: (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
private var presentationData: PresentationData
private var scrollView: Scroller
@ -1019,13 +1018,13 @@ public final class CalendarMessageScreen: ViewController {
init(
controller: CalendarMessageScreen,
context: AccountContext,
peerId: PeerId,
peerId: EnginePeer.Id,
calendarSource: SparseMessageCalendar,
initialTimestamp: Int32,
enableMessageRangeDeletion: Bool,
canNavigateToEmptyDays: Bool,
navigateToOffset: @escaping (Int, Int32) -> Void,
previewDay: @escaping (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
) {
self.controller = controller
self.context = context
@ -1783,9 +1782,9 @@ public final class CalendarMessageScreen: ViewController {
guard let calendarState = self.calendarState else {
return
}
var messageMap: [Message] = []
var messageMap: [EngineMessage] = []
for (_, entry) in calendarState.messagesByDay {
messageMap.append(entry.message)
messageMap.append(EngineMessage(entry.message))
}
var updatedMedia: [Int: [Int: DayMedia]] = [:]
@ -1805,7 +1804,7 @@ public final class CalendarMessageScreen: ViewController {
mediaLoop: for media in message.media {
switch media {
case _ as TelegramMediaImage, _ as TelegramMediaFile:
updatedMedia[i]![day] = DayMedia(message: EngineMessage(message), media: EngineMedia(media))
updatedMedia[i]![day] = DayMedia(message: message, media: EngineMedia(media))
break mediaLoop
default:
break
@ -1830,13 +1829,13 @@ public final class CalendarMessageScreen: ViewController {
}
private let context: AccountContext
private let peerId: PeerId
private let peerId: EnginePeer.Id
private let calendarSource: SparseMessageCalendar
private let initialTimestamp: Int32
private let enableMessageRangeDeletion: Bool
private let canNavigateToEmptyDays: Bool
private let navigateToDay: (CalendarMessageScreen, Int, Int32) -> Void
private let previewDay: (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
private let previewDay: (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
private var presentationData: PresentationData
@ -1844,13 +1843,13 @@ public final class CalendarMessageScreen: ViewController {
public init(
context: AccountContext,
peerId: PeerId,
peerId: EnginePeer.Id,
calendarSource: SparseMessageCalendar,
initialTimestamp: Int32,
enableMessageRangeDeletion: Bool,
canNavigateToEmptyDays: Bool,
navigateToDay: @escaping (CalendarMessageScreen, Int, Int32) -> Void,
previewDay: @escaping (Int32, MessageIndex?, ASDisplayNode, CGRect, ContextGesture) -> Void
previewDay: @escaping (Int32, EngineMessage.Index?, ASDisplayNode, CGRect, ContextGesture) -> Void
) {
self.context = context
self.peerId = peerId

View File

@ -3,7 +3,6 @@ import AsyncDisplayKit
import Display
import TelegramCore
import SwiftSignalKit
import Postbox
import TelegramPresentationData
import AccountContext
import PresentationDataUtils
@ -17,6 +16,26 @@ import ConfettiEffect
import TelegramUniversalVideoContent
import SolidRoundedButtonNode
private func fileSize(_ path: String, useTotalFileAllocatedSize: Bool = false) -> Int64? {
if useTotalFileAllocatedSize {
let url = URL(fileURLWithPath: path)
if let values = (try? url.resourceValues(forKeys: Set([.isRegularFileKey, .totalFileAllocatedSizeKey]))) {
if values.isRegularFile ?? false {
if let fileSize = values.totalFileAllocatedSize {
return Int64(fileSize)
}
}
}
}
var value = stat()
if stat(path, &value) == 0 {
return value.st_size
} else {
return nil
}
}
private final class ProgressEstimator {
private var averageProgressPerSecond: Double = 0.0
private var lastMeasurement: (Double, Float)?
@ -91,7 +110,7 @@ private final class ImportManager {
return self.statePromise.get()
}
init(account: Account, peerId: PeerId, mainFile: TempBoxFile, archivePath: String?, entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
init(account: Account, peerId: EnginePeer.Id, mainFile: EngineTempBox.File, archivePath: String?, entries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
self.account = account
self.archivePath = archivePath
self.entries = entries
@ -234,8 +253,8 @@ private final class ImportManager {
Logger.shared.log("ChatImportScreen", "updateState take pending entry \(entry.1)")
let unpackedFile = Signal<TempBoxFile, ImportError> { subscriber in
let tempFile = TempBox.shared.tempFile(fileName: entry.0.path)
let unpackedFile = Signal<EngineTempBox.File, ImportError> { subscriber in
let tempFile = EngineTempBox.shared.tempFile(fileName: entry.0.path)
Logger.shared.log("ChatImportScreen", "Extracting \(entry.0.path) to \(tempFile.path)...")
let startTime = CACurrentMediaTime()
if SSZipArchive.extractFileFromArchive(atPath: archivePath, filePath: entry.0.path, toPath: tempFile.path) {
@ -440,9 +459,9 @@ public final class ChatImportActivityScreen: ViewController {
if let path = getAppBundle().path(forResource: "BlankVideo", ofType: "m4v"), let size = fileSize(path) {
let decoration = ChatBubbleVideoDecoration(corners: ImageCorners(), nativeSize: CGSize(width: 100.0, height: 100.0), contentMode: .aspectFit, backgroundColor: .black)
let dummyFile = TelegramMediaFile(fileId: MediaId(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
let dummyFile = TelegramMediaFile(fileId: EngineMedia.Id(namespace: 0, id: 1), partialReference: nil, resource: LocalFileReferenceMediaResource(localFilePath: path, randomId: 12345), previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: "video/mp4", size: size, attributes: [.Video(duration: 1, size: PixelDimensions(width: 100, height: 100), flags: [])])
let videoContent = NativeVideoContent(id: .message(1, MediaId(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
let videoContent = NativeVideoContent(id: .message(1, EngineMedia.Id(namespace: 0, id: 1)), userLocation: .other, fileReference: .standalone(media: dummyFile), streamVideo: .none, loopVideo: true, enableSound: false, fetchAutomatically: true, onlyFullSizeThumbnail: false, continuePlayingWithoutSoundOnLostAudioSession: false, placeholderColor: .black, storeAfterDownload: nil)
let videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: decoration, content: videoContent, priority: .embedded)
videoNode.frame = CGRect(origin: CGPoint(), size: CGSize(width: 2.0, height: 2.0))
@ -724,9 +743,9 @@ public final class ChatImportActivityScreen: ViewController {
private let context: AccountContext
private var presentationData: PresentationData
fileprivate let cancel: () -> Void
fileprivate var peerId: PeerId
fileprivate var peerId: EnginePeer.Id
private let archivePath: String?
private let mainEntry: TempBoxFile
private let mainEntry: EngineTempBox.File
private let totalBytes: Int64
private let totalMediaBytes: Int64
private let otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]
@ -746,7 +765,7 @@ public final class ChatImportActivityScreen: ViewController {
}
}
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: PeerId, archivePath: String?, mainEntry: TempBoxFile, otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
public init(context: AccountContext, cancel: @escaping () -> Void, peerId: EnginePeer.Id, archivePath: String?, mainEntry: EngineTempBox.File, otherEntries: [(SSZipEntry, String, TelegramEngine.HistoryImport.MediaType)]) {
self.context = context
self.cancel = cancel
self.peerId = peerId
@ -818,7 +837,7 @@ public final class ChatImportActivityScreen: ViewController {
self.progressEstimator = ProgressEstimator()
self.beganCompletion = false
let resolvedPeerId: Signal<PeerId, ImportManager.ImportError>
let resolvedPeerId: Signal<EnginePeer.Id, ImportManager.ImportError>
if self.peerId.namespace == Namespaces.Peer.CloudGroup {
resolvedPeerId = self.context.engine.peers.convertGroupToSupergroup(peerId: self.peerId)
|> mapError { _ -> ImportManager.ImportError in

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import PresentationDataUtils
@ -30,8 +29,8 @@ private final class ChatListFilterPresetControllerArguments {
let updateState: ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void
let openAddIncludePeer: () -> Void
let openAddExcludePeer: () -> Void
let deleteIncludePeer: (PeerId) -> Void
let deleteExcludePeer: (PeerId) -> Void
let deleteIncludePeer: (EnginePeer.Id) -> Void
let deleteExcludePeer: (EnginePeer.Id) -> Void
let setItemIdWithRevealedOptions: (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void
let deleteIncludeCategory: (ChatListFilterIncludeCategory) -> Void
let deleteExcludeCategory: (ChatListFilterExcludeCategory) -> Void
@ -49,8 +48,8 @@ private final class ChatListFilterPresetControllerArguments {
updateState: @escaping ((ChatListFilterPresetControllerState) -> ChatListFilterPresetControllerState) -> Void,
openAddIncludePeer: @escaping () -> Void,
openAddExcludePeer: @escaping () -> Void,
deleteIncludePeer: @escaping (PeerId) -> Void,
deleteExcludePeer: @escaping (PeerId) -> Void,
deleteIncludePeer: @escaping (EnginePeer.Id) -> Void,
deleteExcludePeer: @escaping (EnginePeer.Id) -> Void,
setItemIdWithRevealedOptions: @escaping (ChatListFilterRevealedItemId?, ChatListFilterRevealedItemId?) -> Void,
deleteIncludeCategory: @escaping (ChatListFilterIncludeCategory) -> Void,
deleteExcludeCategory: @escaping (ChatListFilterExcludeCategory) -> Void,
@ -93,7 +92,7 @@ private enum ChatListFilterPresetControllerSection: Int32 {
private enum ChatListFilterPresetEntryStableId: Hashable {
case index(Int)
case peer(PeerId)
case peer(EnginePeer.Id)
case includePeerInfo
case excludePeerInfo
case includeCategory(ChatListFilterIncludeCategory)
@ -311,7 +310,7 @@ private extension ChatListFilterCategoryIcon {
}
private enum ChatListFilterRevealedItemId: Equatable {
case peer(PeerId)
case peer(EnginePeer.Id)
case includeCategory(ChatListFilterIncludeCategory)
case excludeCategory(ChatListFilterExcludeCategory)
}
@ -573,8 +572,8 @@ private struct ChatListFilterPresetControllerState: Equatable {
var excludeMuted: Bool
var excludeRead: Bool
var excludeArchived: Bool
var additionallyIncludePeers: [PeerId]
var additionallyExcludePeers: [PeerId]
var additionallyIncludePeers: [EnginePeer.Id]
var additionallyExcludePeers: [EnginePeer.Id]
var revealedItemId: ChatListFilterRevealedItemId?
var expandedSections: Set<FilterSection>
@ -825,7 +824,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
return
}
var includePeers: [PeerId] = []
var includePeers: [EnginePeer.Id] = []
for peerId in peerIds {
switch peerId {
case let .peer(id):
@ -838,7 +837,7 @@ private func internalChatListFilterAddChatsController(context: AccountContext, f
if filter.id > 1, case let .filter(_, _, _, data) = filter, data.hasSharedLinks {
let newPeers = includePeers.filter({ !(filter.data?.includePeers.peers.contains($0) ?? false) })
var removedPeers: [PeerId] = []
var removedPeers: [EnginePeer.Id] = []
if let data = filter.data {
removedPeers = data.includePeers.peers.filter({ !includePeers.contains($0) })
}
@ -951,7 +950,7 @@ private func internalChatListFilterExcludeChatsController(context: AccountContex
return
}
var excludePeers: [PeerId] = []
var excludePeers: [EnginePeer.Id] = []
for peerId in peerIds {
switch peerId {
case let .peer(id):
@ -1144,7 +1143,7 @@ func chatListFilterPresetController(context: AccountContext, currentPreset initi
sharedLinks.set(Signal<[ExportedChatFolderLink]?, NoError>.single(nil) |> then(context.engine.peers.getExportedChatFolderLinks(id: initialPreset.id)))
}
let currentPeers = Atomic<[PeerId: EngineRenderedPeer]>(value: [:])
let currentPeers = Atomic<[EnginePeer.Id: EngineRenderedPeer]>(value: [:])
let stateWithPeers = statePromise.get()
|> mapToSignal { state -> Signal<(ChatListFilterPresetControllerState, [EngineRenderedPeer], [EngineRenderedPeer]), NoError> in
let currentPeersValue = currentPeers.with { $0 }

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
@ -39,7 +38,7 @@ private enum ChatListFilterPresetListSection: Int32 {
case list
}
private func stringForUserCount(_ peers: [PeerId: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
private func stringForUserCount(_ peers: [EnginePeer.Id: SelectivePrivacyPeer], strings: PresentationStrings) -> String {
if peers.isEmpty {
return strings.PrivacyLastSeenSettings_EmpryUsersPlaceholder
} else {

View File

@ -3,7 +3,6 @@ import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import ItemListUI

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData

View File

@ -665,13 +665,9 @@ private func mappedInsertEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
}
case .hide:
switch notice {
case .chatFolderUpdates:
nodeInteraction?.hideChatFolderUpdates()
default:
break
}
@ -966,13 +962,9 @@ private func mappedUpdateEntries(context: AccountContext, nodeInteraction: ChatL
nodeInteraction?.openPasswordSetup()
case .premiumUpgrade, .premiumAnnualDiscount:
nodeInteraction?.openPremiumIntro()
case .chatFolderUpdates:
nodeInteraction?.openChatFolderUpdates()
}
case .hide:
switch notice {
case .chatFolderUpdates:
nodeInteraction?.hideChatFolderUpdates()
default:
break
}

View File

@ -210,8 +210,6 @@ class ChatListStorageInfoItemNode: ItemListRevealOptionsItemNode {
strongSelf.contentContainer.frame = CGRect(origin: CGPoint(), size: layout.contentSize)
switch item.notice {
case .chatFolderUpdates:
strongSelf.setRevealOptions((left: [], right: [ItemListRevealOption(key: 0, title: item.strings.ChatList_HideAction, icon: .none, color: item.theme.list.itemDisclosureActions.destructive.fillColor, textColor: item.theme.list.itemDisclosureActions.destructive.foregroundColor)]))
default:
strongSelf.setRevealOptions((left: [], right: []))
}

View File

@ -1,12 +1,11 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import SwiftSignalKit
import UniversalMediaPlayer
import AccountContext
private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus?, NoError> {
private func internalMessageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus?, NoError> {
guard let playerType = peerMessageMediaPlayerType(message) else {
return .single(nil)
}
@ -21,7 +20,7 @@ private func internalMessageFileMediaPlaybackStatus(context: AccountContext, fil
}
}
public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus, NoError> {
public func messageFileMediaPlaybackStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<MediaPlayerStatus, NoError> {
var duration = 0.0
if let value = file.duration {
duration = Double(value)
@ -33,7 +32,7 @@ public func messageFileMediaPlaybackStatus(context: AccountContext, file: Telegr
}
}
public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<Float, NoError> {
public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isGlobalSearch: Bool, isDownloadList: Bool) -> Signal<Float, NoError> {
guard let playerType = peerMessageMediaPlayerType(message) else {
return .never()
}
@ -45,7 +44,7 @@ public func messageFileMediaPlaybackAudioLevelEvents(context: AccountContext, fi
}
}
public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false, isDownloadList: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
public func messageFileMediaResourceStatus(context: AccountContext, file: TelegramMediaFile, message: EngineMessage, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false, isDownloadList: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
let playbackStatus = internalMessageFileMediaPlaybackStatus(context: context, file: file, message: message, isRecentActions: isRecentActions, isGlobalSearch: isGlobalSearch, isDownloadList: isDownloadList) |> map { status -> MediaPlayerPlaybackStatus? in
return status?.status
}
@ -99,7 +98,7 @@ public func messageFileMediaResourceStatus(context: AccountContext, file: Telegr
}
}
public func messageImageMediaResourceStatus(context: AccountContext, image: TelegramMediaImage, message: Message, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
public func messageImageMediaResourceStatus(context: AccountContext, image: TelegramMediaImage, message: EngineMessage, isRecentActions: Bool, isSharedMedia: Bool = false, isGlobalSearch: Bool = false) -> Signal<FileMediaResourceStatus, NoError> {
if message.flags.isSending {
return combineLatest(messageMediaImageStatus(context: context, messageId: message.id, image: image), context.account.pendingMessageManager.pendingMessageStatus(message.id) |> map { $0.0 })
|> map { resourceStatus, pendingStatus -> FileMediaResourceStatus in

View File

@ -37,13 +37,13 @@ private func instantPageBlockMedia(pageId: MediaId, block: InstantPageBlock, med
switch block {
case let .image(id, caption, _, _):
if let m = media[id] {
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: EngineMedia(m), url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
counter += 1
return result
}
case let .video(id, caption, _, _):
if let m = media[id] {
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: m, url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
let result = [InstantPageGalleryEntry(index: Int32(counter), pageId: pageId, media: InstantPageMedia(index: counter, media: EngineMedia(m), url: nil, caption: caption.text, credit: caption.credit), caption: caption.text, credit: caption.credit, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0))]
counter += 1
return result
}
@ -82,7 +82,7 @@ public func instantPageGalleryMedia(webpageId: MediaId, page: InstantPage, galle
}
if !found {
result.insert(InstantPageGalleryEntry(index: Int32(counter), pageId: webpageId, media: InstantPageMedia(index: counter, media: galleryMedia, url: nil, caption: nil, credit: nil), caption: nil, credit: nil, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0)), at: 0)
result.insert(InstantPageGalleryEntry(index: Int32(counter), pageId: webpageId, media: InstantPageMedia(index: counter, media: EngineMedia(galleryMedia), url: nil, caption: nil, credit: nil), caption: nil, credit: nil, location: InstantPageGalleryEntryLocation(position: Int32(counter), totalCount: 0)), at: 0)
}
for i in 0 ..< result.count {
@ -123,7 +123,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
if case .suggestedProfilePhoto = action.action {
isSuggested = true
}
let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer._asPeer(), message.timestamp, nil, message.id, image.immediateThumbnailData, "action", false, nil)])
let promise: Promise<[AvatarGalleryEntry]> = Promise([AvatarGalleryEntry.image(image.imageId, image.reference, image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), image.videoRepresentations.map({ VideoRepresentationWithReference(representation: $0, reference: .media(media: .message(message: MessageReference(message), media: media), resource: $0.resource)) }), peer, message.timestamp, nil, message.id, image.immediateThumbnailData, "action", false, nil)])
let sourceCorners: AvatarGalleryController.SourceCorners
if case .photoUpdated = action.action {
@ -131,7 +131,7 @@ public func chatMessageGalleryControllerData(context: AccountContext, chatLocati
} else {
sourceCorners = .round
}
let galleryController = AvatarGalleryController(context: context, peer: peer._asPeer(), sourceCorners: sourceCorners, remoteEntries: promise, isSuggested: isSuggested, skipInitial: true, replaceRootController: { controller, ready in
let galleryController = AvatarGalleryController(context: context, peer: peer, sourceCorners: sourceCorners, remoteEntries: promise, isSuggested: isSuggested, skipInitial: true, replaceRootController: { controller, ready in
})
return .chatAvatars(galleryController, image)

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import TelegramCore
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import TelegramBaseController

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Vision
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import AccountContext
@ -27,8 +26,8 @@ private final class CachedImageRecognizedContent: Codable {
}
}
private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: MessageId) -> Signal<CachedImageRecognizedContent?, NoError> {
let key = ValueBoxKey(length: 20)
private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id) -> Signal<CachedImageRecognizedContent?, NoError> {
let key = EngineDataBuffer(length: 20)
key.setInt32(0, value: messageId.namespace)
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
@ -40,8 +39,8 @@ private func cachedImageRecognizedContent(engine: TelegramEngine, messageId: Mes
}
}
private func updateCachedImageRecognizedContent(engine: TelegramEngine, messageId: MessageId, content: CachedImageRecognizedContent?) -> Signal<Never, NoError> {
let key = ValueBoxKey(length: 20)
private func updateCachedImageRecognizedContent(engine: TelegramEngine, messageId: EngineMessage.Id, content: CachedImageRecognizedContent?) -> Signal<Never, NoError> {
let key = EngineDataBuffer(length: 20)
key.setInt32(0, value: messageId.namespace)
key.setInt32(4, value: messageId.peerId.namespace._internalGetInt32Value())
key.setInt64(8, value: messageId.peerId.id._internalGetInt64Value())
@ -333,7 +332,7 @@ private func recognizeContent(in image: UIImage?) -> Signal<[RecognizedContent],
}
}
public func recognizedContent(context: AccountContext, image: @escaping () -> UIImage?, messageId: MessageId) -> Signal<[RecognizedContent], NoError> {
public func recognizedContent(context: AccountContext, image: @escaping () -> UIImage?, messageId: EngineMessage.Id) -> Signal<[RecognizedContent], NoError> {
if context.sharedContext.immediateExperimentalUISettings.disableImageContentAnalysis {
return .single([])
}

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
enum StickerVerificationStatus {
@ -74,7 +73,7 @@ public class ImportStickerPack {
let emojis: [String]
let keywords: String
let uuid: UUID
var resource: MediaResource?
var resource: EngineMediaResource?
init(content: Content, emojis: [String], keywords: String, uuid: UUID = UUID()) {
self.content = content

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramUIPreferences
@ -87,23 +86,23 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
return
}
var signals: [Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError>] = []
var signals: [Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError>] = []
for sticker in strongSelf.stickerPack.stickers {
if let resource = strongSelf.controllerNode.stickerResources[sticker.uuid] {
signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource, alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), mimeType: sticker.mimeType)
|> map { result -> (UUID, StickerVerificationStatus, MediaResource?) in
signals.append(strongSelf.context.engine.stickers.uploadSticker(peer: peer, resource: resource._asResource(), alt: sticker.emojis.first ?? "", dimensions: PixelDimensions(width: 512, height: 512), mimeType: sticker.mimeType)
|> map { result -> (UUID, StickerVerificationStatus, EngineMediaResource?) in
switch result {
case .progress:
return (sticker.uuid, .loading, nil)
case let .complete(resource, mimeType):
if ["application/x-tgsticker", "video/webm"].contains(mimeType) {
return (sticker.uuid, .verified, resource)
return (sticker.uuid, .verified, EngineMediaResource(resource))
} else {
return (sticker.uuid, .declined, nil)
}
}
}
|> `catch` { _ -> Signal<(UUID, StickerVerificationStatus, MediaResource?), NoError> in
|> `catch` { _ -> Signal<(UUID, StickerVerificationStatus, EngineMediaResource?), NoError> in
return .single((sticker.uuid, .declined, nil))
})
}
@ -115,7 +114,7 @@ public final class ImportStickerPackController: ViewController, StandalonePresen
}
var verifiedStickers = Set<UUID>()
var declinedStickers = Set<UUID>()
var uploadedStickerResources: [UUID: MediaResource] = [:]
var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
for (uuid, result, resource) in results {
switch result {
case .verified:

View File

@ -3,7 +3,6 @@ import UIKit
import Display
import AsyncDisplayKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramPresentationData
import TelegramUIPreferences
@ -52,8 +51,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
private let context: AccountContext
private var presentationData: PresentationData
private var stickerPack: ImportStickerPack?
var stickerResources: [UUID: MediaResource] = [:]
private var uploadedStickerResources: [UUID: MediaResource] = [:]
var stickerResources: [UUID: EngineMediaResource] = [:]
private var uploadedStickerResources: [UUID: EngineMediaResource] = [:]
private var stickerPackReady = true
private var containerLayout: (ContainerViewLayout, CGFloat)?
@ -623,11 +622,11 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
}
if let resource = self.uploadedStickerResources[item.stickerItem.uuid] {
if let localResource = item.stickerItem.resource {
self.context.account.postbox.mediaBox.copyResourceData(from: localResource.id, to: resource.id)
self.context.account.postbox.mediaBox.copyResourceData(from: localResource._asResource().id, to: resource._asResource().id)
}
stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
stickers.append(ImportSticker(resource: resource._asResource(), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
} else if let resource = item.stickerItem.resource {
stickers.append(ImportSticker(resource: resource, emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
stickers.append(ImportSticker(resource: resource._asResource(), emojis: item.stickerItem.emojis, dimensions: dimensions, mimeType: item.stickerItem.mimeType, keywords: item.stickerItem.keywords))
}
}
var thumbnailSticker: ImportSticker?
@ -695,23 +694,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
let context = strongSelf.context
Queue.mainQueue().after(1.0) {
var firstItem: StickerPackItem?
if let firstStickerItem = firstStickerItem, let resource = firstStickerItem.resource as? TelegramMediaResource {
var fileAttributes: [TelegramMediaFileAttribute] = []
if firstStickerItem.mimeType == "video/webm" {
fileAttributes.append(.FileName(fileName: "sticker.webm"))
fileAttributes.append(.Animated)
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
} else if firstStickerItem.mimeType == "application/x-tgsticker" {
fileAttributes.append(.FileName(fileName: "sticker.tgs"))
fileAttributes.append(.Animated)
fileAttributes.append(.Sticker(displayText: "", packReference: nil, maskData: nil))
} else {
fileAttributes.append(.FileName(fileName: "sticker.webp"))
}
fileAttributes.append(.ImageSize(size: firstStickerItem.dimensions))
firstItem = StickerPackItem(index: ItemCollectionItemIndex(index: 0, id: 0), file: TelegramMediaFile(fileId: MediaId(namespace: 0, id: 0), partialReference: nil, resource: resource, previewRepresentations: [], videoThumbnails: [], immediateThumbnailData: nil, mimeType: firstStickerItem.mimeType, size: nil, attributes: fileAttributes), indexKeys: [])
}
let firstItem: StickerPackItem? = firstStickerItem?.stickerPackItem
strongSelf.presentInGlobalOverlay?(UndoOverlayController(presentationData: strongSelf.presentationData, content: .stickersModified(title: strongSelf.presentationData.strings.StickerPackActionInfo_AddedTitle, text: strongSelf.presentationData.strings.StickerPackActionInfo_AddedText(info.title).string, undo: false, info: info, topItem: firstItem ?? items.first, context: strongSelf.context), elevatedLayout: false, action: { action in
if case .info = action {
(navigationController?.viewControllers.last as? ViewController)?.present(StickerPackScreen(context: context, mode: .settings, mainStickerPack: .id(id: info.id.id, accessHash: info.accessHash), stickerPacks: [], parentNavigationController: navigationController, actionPerformed: { _ in
@ -800,7 +783,7 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
})
}
func updateStickerPack(_ stickerPack: ImportStickerPack, verifiedStickers: Set<UUID>, declinedStickers: Set<UUID>, uploadedStickerResources: [UUID: MediaResource]) {
func updateStickerPack(_ stickerPack: ImportStickerPack, verifiedStickers: Set<UUID>, declinedStickers: Set<UUID>, uploadedStickerResources: [UUID: EngineMediaResource]) {
self.stickerPack = stickerPack
self.uploadedStickerResources = uploadedStickerResources
var updatedItems: [StickerPackPreviewGridEntry] = []
@ -813,8 +796,8 @@ final class ImportStickerPackControllerNode: ViewControllerTracingNode, UIScroll
} else {
let resource = LocalFileMediaResource(fileId: Int64.random(in: Int64.min ... Int64.max))
self.context.account.postbox.mediaBox.storeResourceData(resource.id, data: item.data)
item.resource = resource
self.stickerResources[item.uuid] = resource
item.resource = EngineMediaResource(resource)
self.stickerResources[item.uuid] = EngineMediaResource(resource)
}
var isInitiallyVerified = false
if case .image = item.content {

View File

@ -3,7 +3,6 @@ import UIKit
import SwiftSignalKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import TelegramPresentationData
import AccountContext

View File

@ -4,7 +4,6 @@ import Display
import TelegramCore
import SwiftSignalKit
import AsyncDisplayKit
import Postbox
import StickerResources
import AccountContext
import AnimatedStickerNode
@ -142,7 +141,7 @@ final class StickerPackPreviewGridItemNode: GridItemNode {
if case .video = stickerItem.content {
isVideo = true
}
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
animationNode.setup(source: AnimatedStickerResourceSource(account: account, resource: resource._asResource(), isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), mode: .direct(cachePathPrefix: nil))
}
animationNode.visibility = self.isVisibleInGrid && self.interaction?.playAnimatedStickers ?? true
} else {

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import StickerResources
@ -87,7 +86,7 @@ private final class StickerPreviewPeekContentNode: ASDisplayNode, PeekController
if case .video = item.content {
isVideo = true
}
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource, isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
self.animationNode?.setup(source: AnimatedStickerResourceSource(account: account, resource: resource._asResource(), isVideo: isVideo), width: Int(fittedDimensions.width), height: Int(fittedDimensions.height), playbackMode: .loop, mode: .direct(cachePathPrefix: nil))
}
self.animationNode?.visibility = true
}

View File

@ -1,8 +1,8 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import Postbox
import TelegramUIPreferences
import PersistentStringHash

View File

@ -1,6 +1,5 @@
import Foundation
import SwiftSignalKit
import Postbox
import TelegramCore
import AccountContext
import UrlHandling

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData
@ -20,11 +19,11 @@ public final class InstantPageArticleItem: InstantPageItem {
let contentSize: CGSize
let cover: TelegramMediaImage?
let url: String
let webpageId: MediaId
let webpageId: EngineMedia.Id
let rtl: Bool
let hasRTL: Bool
init(frame: CGRect, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, rtl: Bool, hasRTL: Bool) {
init(frame: CGRect, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: EngineMedia.Id, rtl: Bool, hasRTL: Bool) {
self.frame = frame
self.userLocation = userLocation
self.webPage = webPage
@ -73,7 +72,7 @@ public final class InstantPageArticleItem: InstantPageItem {
}
}
func layoutArticleItem(theme: InstantPageTheme, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: MediaId, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem {
func layoutArticleItem(theme: InstantPageTheme, userLocation: MediaResourceUserLocation, webPage: TelegramMediaWebpage, title: NSAttributedString, description: NSAttributedString, cover: TelegramMediaImage?, url: String, webpageId: EngineMedia.Id, boundingWidth: CGFloat, rtl: Bool) -> InstantPageArticleItem {
let inset: CGFloat = 17.0
let imageSpacing: CGFloat = 10.0
var sideInset = inset

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
@ -20,14 +19,14 @@ final class InstantPageArticleNode: ASDisplayNode, InstantPageNode {
private var imageNode: TransformImageNode?
let url: String
let webpageId: MediaId
let webpageId: EngineMedia.Id
let cover: TelegramMediaImage?
private let openUrl: (InstantPageUrlItem) -> Void
private var fetchedDisposable = MetaDisposable()
init(context: AccountContext, item: InstantPageArticleItem, webPage: TelegramMediaWebpage, strings: PresentationStrings, theme: InstantPageTheme, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: MediaId, openUrl: @escaping (InstantPageUrlItem) -> Void) {
init(context: AccountContext, item: InstantPageArticleItem, webPage: TelegramMediaWebpage, strings: PresentationStrings, theme: InstantPageTheme, contentItems: [InstantPageItem], contentSize: CGSize, cover: TelegramMediaImage?, url: String, webpageId: EngineMedia.Id, openUrl: @escaping (InstantPageUrlItem) -> Void) {
self.item = item
self.url = url
self.webpageId = webpageId

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData

View File

@ -1,7 +1,6 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import Display
@ -36,7 +35,7 @@ private func generatePauseButton(color: UIColor) -> UIImage? {
private func titleString(media: InstantPageMedia, theme: InstantPageTheme, strings: PresentationStrings) -> NSAttributedString {
let string = NSMutableAttributedString()
if let file = media.media as? TelegramMediaFile {
if case let .file(file) = media.media {
loop: for attribute in file.attributes {
if case let .Audio(isVoice, _, title, performer, _) = attribute, !isVoice {
let titleText: String = title ?? strings.MediaPlayer_UnknownTrack
@ -101,7 +100,7 @@ final class InstantPageAudioNode: ASDisplayNode, InstantPageNode {
self.scrubbingNode = MediaPlayerScrubbingNode(content: .standard(lineHeight: 3.0, lineCap: .round, scrubberHandle: .line, backgroundColor: theme.textCategories.paragraph.color.withAlphaComponent(backgroundAlpha), foregroundColor: theme.textCategories.paragraph.color, bufferingColor: theme.textCategories.paragraph.color.withAlphaComponent(0.5), chapters: []))
let playlistType: MediaManagerPlayerType
if let file = self.media.media as? TelegramMediaFile {
if case let .file(file) = self.media.media {
playlistType = file.isVoice ? .voice : .music
} else {
playlistType = .music

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData

View File

@ -555,7 +555,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
if item is InstantPageWebEmbedItem {
embedIndex += 1
}
if let imageItem = item as? InstantPageImageItem, imageItem.media.media is TelegramMediaWebpage {
if let imageItem = item as? InstantPageImageItem, case .webpage = imageItem.media.media {
embedIndex += 1
}
if item is InstantPageDetailsItem {
@ -1003,17 +1003,17 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
private func longPressMedia(_ media: InstantPageMedia) {
let controller = ContextMenuController(actions: [ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuCopy, accessibilityLabel: self.strings.Conversation_ContextMenuCopy), action: { [weak self] in
if let strongSelf = self, let image = media.media as? TelegramMediaImage {
if let strongSelf = self, case let .image(image) = media.media {
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
let _ = copyToPasteboard(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
}
}), ContextMenuAction(content: .text(title: self.strings.Conversation_LinkDialogSave, accessibilityLabel: self.strings.Conversation_LinkDialogSave), action: { [weak self] in
if let strongSelf = self, let image = media.media as? TelegramMediaImage {
if let strongSelf = self, case let .image(image) = media.media {
let media = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: image.representations, immediateThumbnailData: image.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
let _ = saveToCameraRoll(context: strongSelf.context, postbox: strongSelf.context.account.postbox, userLocation: strongSelf.sourceLocation.userLocation, mediaReference: .standalone(media: media)).start()
}
}), ContextMenuAction(content: .text(title: self.strings.Conversation_ContextMenuShare, accessibilityLabel: self.strings.Conversation_ContextMenuShare), action: { [weak self] in
if let strongSelf = self, let webPage = strongSelf.webPage, let image = media.media as? TelegramMediaImage {
if let strongSelf = self, let webPage = strongSelf.webPage, case let .image(image) = media.media {
strongSelf.present(ShareController(context: strongSelf.context, subject: .image(image.representations.map({ ImageRepresentationWithReference(representation: $0, reference: MediaResourceReference.media(media: .webPage(webPage: WebpageReference(webPage), media: image), resource: $0.resource)) }))), nil)
}
})], catchTapsOutside: true)
@ -1406,7 +1406,7 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
return
}
if let map = media.media as? TelegramMediaMap {
if case let .geo(map) = media.media {
let controllerParams = LocationViewParams(sendLiveLocation: { _ in
}, stopLiveLocation: { _ in
}, openUrl: { _ in }, openPeer: { _ in
@ -1420,12 +1420,12 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
return
}
if let file = media.media as? TelegramMediaFile, (file.isVoice || file.isMusic) {
if case let .file(file) = media.media, (file.isVoice || file.isMusic) {
var medias: [InstantPageMedia] = []
var initialIndex = 0
for item in items {
for itemMedia in item.medias {
if let itemFile = itemMedia.media as? TelegramMediaFile, (itemFile.isVoice || itemFile.isMusic) {
if case let .file(itemFile) = itemMedia.media, (itemFile.isVoice || itemFile.isMusic) {
if itemMedia.index == media.index {
initialIndex = medias.count
}
@ -1440,16 +1440,21 @@ final class InstantPageControllerNode: ASDisplayNode, UIScrollViewDelegate {
var fromPlayingVideo = false
var entries: [InstantPageGalleryEntry] = []
if media.media is TelegramMediaWebpage {
if case let .webpage(webPage) = media.media {
entries.append(InstantPageGalleryEntry(index: 0, pageId: webPage.webpageId, media: media, caption: nil, credit: nil, location: nil))
} else if let file = media.media as? TelegramMediaFile, file.isAnimated {
} else if case let .file(file) = media.media, file.isAnimated {
fromPlayingVideo = true
entries.append(InstantPageGalleryEntry(index: Int32(media.index), pageId: webPage.webpageId, media: media, caption: media.caption, credit: media.credit, location: nil))
} else {
fromPlayingVideo = true
var medias: [InstantPageMedia] = mediasFromItems(items)
medias = medias.filter {
return $0.media is TelegramMediaImage || $0.media is TelegramMediaFile
medias = medias.filter { item in
switch item.media {
case .image, .file:
return true
default:
return false
}
}
for media in medias {

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import Display

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData

View File

@ -96,9 +96,9 @@ public struct InstantPageGalleryEntry: Equatable {
}
}
if let image = self.media.media as? TelegramMediaImage {
if case let .image(image) = self.media.media {
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
} else if let file = self.media.media as? TelegramMediaFile {
} else if case let .file(file) = self.media.media {
if file.isVideo {
var indexData: GalleryItemIndexData?
if let location = self.location {
@ -122,7 +122,7 @@ public struct InstantPageGalleryEntry: Equatable {
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: representations, immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
return InstantImageGalleryItem(context: context, presentationData: presentationData, itemId: self.index, userLocation: userLocation, imageReference: .webPage(webPage: WebpageReference(webPage), media: image), caption: caption, credit: credit, location: self.location, openUrl: openUrl, openUrlOptions: openUrlOptions)
}
} else if let embedWebpage = self.media.media as? TelegramMediaWebpage, case let .Loaded(webpageContent) = embedWebpage.content {
} else if case let .webpage(embedWebpage) = self.media.media, case let .Loaded(webpageContent) = embedWebpage.content {
if webpageContent.url.hasSuffix(".m3u8") {
let content = PlatformVideoContent(id: .instantPage(embedWebpage.webpageId, embedWebpage.webpageId), userLocation: userLocation, content: .url(webpageContent.url), streamVideo: true, loopVideo: false)
return UniversalVideoGalleryItem(context: context, presentationData: presentationData, content: content, originData: nil, indexData: nil, contentInfo: .webPage(webPage, embedWebpage, { makeArguments, navigationController, present in

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
@ -43,7 +42,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
private var currentSize: CGSize?
private var fetchStatus: MediaResourceStatus?
private var fetchStatus: EngineMediaResource.FetchStatus?
private var fetchedDisposable = MetaDisposable()
private var statusDisposable = MetaDisposable()
@ -72,7 +71,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.pinchContainerNode.contentNode.addSubnode(self.imageNode)
self.addSubnode(self.pinchContainerNode)
if let image = media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
if case let .image(image) = media.media, let largest = largestImageRepresentation(image.representations) {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
@ -92,7 +91,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(largest.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.fetchStatus = status
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.updateFetchStatus()
}
}
@ -105,7 +104,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
}
} else if let file = media.media as? TelegramMediaFile {
} else if case let .file(file) = media.media {
let fileReference = FileMediaReference.webPage(webPage: WebpageReference(webPage), media: file)
if file.mimeType.hasPrefix("image/") {
if !interactive || shouldDownloadMediaAutomatically(settings: context.sharedContext.currentAutomaticMediaDownloadSettings, peerType: sourceLocation.peerType, networkType: MediaAutoDownloadNetworkType(context.account.immediateNetworkType), authorPeerId: nil, contactsPeerIds: Set(), media: file) {
@ -119,7 +118,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
self.statusNode.transitionToState(.play(.white), animated: false, completion: {})
self.pinchContainerNode.contentNode.addSubnode(self.statusNode)
}
} else if let map = media.media as? TelegramMediaMap {
} else if case let .geo(map) = media.media {
self.addSubnode(self.pinNode)
var dimensions = CGSize(width: 200.0, height: 100.0)
@ -131,7 +130,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
}
let resource = MapSnapshotMediaResource(latitude: map.latitude, longitude: map.longitude, width: Int32(dimensions.width), height: Int32(dimensions.height))
self.imageNode.setSignal(chatMapSnapshotImage(engine: context.engine, resource: resource))
} else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image {
} else if case let .webpage(webPage) = media.media, case let .Loaded(content) = webPage.content, let image = content.image {
let imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
self.imageNode.setSignal(chatMessagePhoto(postbox: context.account.postbox, userLocation: sourceLocation.userLocation, photoReference: imageReference))
self.fetchedDisposable.set(chatMessagePhotoInteractiveFetched(context: context, userLocation: sourceLocation.userLocation, photoReference: imageReference, displayAtSize: nil, storeToDownloadsPeerId: nil).start())
@ -211,7 +210,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
if self.currentSize != size || self.themeUpdated {
self.currentSize = size
self.themeUpdated = false
self.pinchContainerNode.frame = CGRect(origin: CGPoint(), size: size)
self.pinchContainerNode.update(size: size, transition: .immediate)
self.imageNode.frame = CGRect(origin: CGPoint(), size: size)
@ -219,7 +218,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
let radialStatusSize: CGFloat = 50.0
self.statusNode.frame = CGRect(x: floorToScreenPixels((size.width - radialStatusSize) / 2.0), y: floorToScreenPixels((size.height - radialStatusSize) / 2.0), width: radialStatusSize, height: radialStatusSize)
if let image = self.media.media as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
if case let .image(image) = self.media.media, let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
let boundingSize = size
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
@ -228,15 +227,15 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
apply()
self.linkIconNode.frame = CGRect(x: size.width - 38.0, y: 14.0, width: 24.0, height: 24.0)
} else if let file = self.media.media as? TelegramMediaFile, let dimensions = file.dimensions {
} else if case let .file(file) = self.media.media, let dimensions = file.dimensions {
let emptyColor = file.mimeType.hasPrefix("image/") ? self.theme.imageTintColor : nil
let imageSize = dimensions.cgSize.aspectFilled(size)
let boundingSize = size
let makeLayout = self.imageNode.asyncLayout()
let apply = makeLayout(TransformImageArguments(corners: ImageCorners(), imageSize: imageSize, boundingSize: boundingSize, intrinsicInsets: UIEdgeInsets(), emptyColor: emptyColor))
apply()
} else if self.media.media is TelegramMediaMap {
} else if case .geo = self.media.media {
for attribute in self.attributes {
if let mapAttribute = attribute as? InstantPageMapAttribute {
let imageSize = mapAttribute.dimensions.aspectFilled(size)
@ -254,7 +253,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
let (pinSize, pinApply) = makePinLayout(self.context, theme, .location(nil))
self.pinNode.frame = CGRect(origin: CGPoint(x: floor((size.width - pinSize.width) / 2.0), y: floor(size.height * 0.5 - 10.0 - pinSize.height / 2.0)), size: pinSize)
pinApply()
} else if let webPage = media.media as? TelegramMediaWebpage, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) {
} else if case let .webpage(webPage) = media.media, case let .Loaded(content) = webPage.content, let image = content.image, let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.dimensions.cgSize.aspectFilled(size)
let boundingSize = size
let radius: CGFloat = self.roundCorners ? floor(min(imageSize.width, imageSize.height) / 2.0) : 0.0
@ -290,7 +289,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
case .Local:
switch gesture {
case .tap:
if self.media.media is TelegramMediaImage && self.media.index == -1 {
if case .image = self.media.media, self.media.index == -1 {
return
}
self.openMedia(self.media)
@ -311,7 +310,7 @@ final class InstantPageImageNode: ASDisplayNode, InstantPageNode {
} else {
switch gesture {
case .tap:
if self.media.media is TelegramMediaImage && self.media.index == -1 {
if case .image = self.media.media, self.media.index == -1 {
return
}
self.openMedia(self.media)

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData

View File

@ -1,7 +1,6 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import Display
import TelegramPresentationData
import TelegramUIPreferences
@ -48,7 +47,7 @@ private func setupStyleStack(_ stack: InstantPageTextStyleStack, theme: InstantP
}
}
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [MediaId: Media], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout {
public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation: MediaResourceUserLocation, rtl: Bool, block: InstantPageBlock, boundingWidth: CGFloat, horizontalInset: CGFloat, safeInset: CGFloat, isCover: Bool, previousItems: [InstantPageItem], fillToSize: CGSize?, media: [EngineMedia.Id: EngineMedia], mediaIndexCounter: inout Int, embedIndexCounter: inout Int, detailsIndexCounter: inout Int, theme: InstantPageTheme, strings: PresentationStrings, dateTimeFormat: PresentationDateTimeFormat, webEmbedHeights: [Int : CGFloat] = [:], excludeCaptions: Bool) -> InstantPageLayout {
let layoutCaption: (InstantPageCaption, CGSize) -> ([InstantPageItem], CGSize) = { caption, contentSize in
var items: [InstantPageItem] = []
@ -373,7 +372,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
contentSize.height += verticalInset
return InstantPageLayout(origin: CGPoint(), contentSize: contentSize, items: items)
case let .image(id, caption, url, webpageId):
if let image = media[id] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
if case let .image(image) = media[id], let largest = largestImageRepresentation(image.representations) {
let imageSize = largest.dimensions
var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0))
@ -397,7 +396,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId)
}
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: image, url: mediaUrl, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: .image(image), url: mediaUrl, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
items.append(mediaItem)
contentSize.height += filledSize.height
@ -413,7 +412,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
return InstantPageLayout(origin: CGPoint(), contentSize: CGSize(), items: [])
}
case let .video(id, caption, autoplay, _):
if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions {
if case let .file(file) = media[id], let dimensions = file.dimensions {
let imageSize = dimensions
var filledSize = imageSize.cgSize.aspectFitted(CGSize(width: boundingWidth - safeInset * 2.0, height: 1200.0))
@ -433,11 +432,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var items: [InstantPageItem] = []
if autoplay {
let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: file, url: nil, caption: caption.text, credit: caption.credit), interactive: true)
let mediaItem = InstantPagePlayableVideoItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: .file(file), url: nil, caption: caption.text, credit: caption.credit), interactive: true)
items.append(mediaItem)
} else {
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: file, url: nil, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: mediaIndex, media: .file(file), url: nil, caption: caption.text, credit: caption.credit), interactive: true, roundCorners: false, fit: false)
items.append(mediaItem)
}
@ -460,11 +459,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var size = CGSize()
switch subItem {
case let .image(id, _, _, _):
if let image = media[id] as? TelegramMediaImage, let largest = largestImageRepresentation(image.representations) {
if case let .image(image) = media[id], let largest = largestImageRepresentation(image.representations) {
size = largest.dimensions.cgSize
}
case let .video(id, _, _, _):
if let file = media[id] as? TelegramMediaFile, let dimensions = file.dimensions {
if case let .file(file) = media[id], let dimensions = file.dimensions {
size = dimensions.cgSize
}
default:
@ -502,9 +501,15 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var items: [InstantPageItem] = []
if !author.isEmpty {
let avatar: TelegramMediaImage? = avatarId.flatMap { media[$0] as? TelegramMediaImage }
let avatar: TelegramMediaImage? = avatarId.flatMap { id -> TelegramMediaImage? in
if case let .image(image) = media[id] {
return image
} else {
return nil
}
}
if let avatar = avatar {
let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), webPage: webpage, media: InstantPageMedia(index: -1, media: avatar, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: true, fit: false)
let avatarItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: horizontalInset + lineInset + 1.0, y: contentSize.height - 2.0), size: CGSize(width: 50.0, height: 50.0)), webPage: webpage, media: InstantPageMedia(index: -1, media: .image(avatar), url: nil, caption: nil, credit: nil), interactive: false, roundCorners: true, fit: false)
items.append(avatarItem)
avatarInset += 62.0
@ -572,7 +577,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
for subBlock in subItems {
switch subBlock {
case let .image(id, caption, url, webpageId):
if let image = media[id] as? TelegramMediaImage, let imageSize = largestImageRepresentation(image.representations)?.dimensions {
if case let .image(image) = media[id], let imageSize = largestImageRepresentation(image.representations)?.dimensions {
let mediaIndex = mediaIndexCounter
mediaIndexCounter += 1
@ -583,7 +588,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
if let url = url {
mediaUrl = InstantPageUrlItem(url: url, webpageId: webpageId)
}
itemMedias.append(InstantPageMedia(index: mediaIndex, media: image, url: mediaUrl, caption: caption.text, credit: caption.credit))
itemMedias.append(InstantPageMedia(index: mediaIndex, media: .image(image), url: mediaUrl, caption: caption.text, credit: caption.credit))
}
break
default:
@ -626,11 +631,11 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var contentSize: CGSize
let frame = CGRect(origin: CGPoint(x: floor((boundingWidth - size.width) / 2.0), y: 0.0), size: size)
let item: InstantPageItem
if let url = url, let coverId = coverId, let image = media[coverId] as? TelegramMediaImage {
if let url = url, let coverId = coverId, case let .image(image) = media[coverId] {
let loadedContent = TelegramMediaWebpageLoadedContent(url: url, displayUrl: url, hash: 0, type: "video", websiteName: nil, title: nil, text: nil, embedUrl: url, embedType: "video", embedSize: PixelDimensions(size), duration: nil, author: nil, image: image, file: nil, attributes: [], instantPage: nil)
let content = TelegramMediaWebpageContent.Loaded(loadedContent)
item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: TelegramMediaWebpage(webpageId: MediaId(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false)
item = InstantPageImageItem(frame: frame, webPage: webpage, media: InstantPageMedia(index: embedIndex, media: .webpage(TelegramMediaWebpage(webpageId: EngineMedia.Id(namespace: Namespaces.Media.LocalWebpage, id: -1), content: content)), url: nil, caption: nil, credit: nil), attributes: [], interactive: true, roundCorners: false, fit: false)
} else {
item = InstantPageWebEmbedItem(frame: frame, url: url, html: html, enableScrolling: allowScrolling)
@ -665,7 +670,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
}
if let peer = peer {
let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: peer, safeInset: safeInset, transparent: !offset.isZero, rtl: rtl || previousItemHasRTL)
let item = InstantPagePeerReferenceItem(frame: CGRect(origin: CGPoint(x: 0.0, y: offset), size: CGSize(width: boundingWidth, height: 40.0)), initialPeer: .channel(peer), safeInset: safeInset, transparent: !offset.isZero, rtl: rtl || previousItemHasRTL)
items.append(item)
if offset.isZero {
contentSize.height += 40.0
@ -679,10 +684,10 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var contentSize = CGSize(width: boundingWidth, height: 0.0)
var items: [InstantPageItem] = []
if let file = media[audioId] as? TelegramMediaFile {
if case let .file(file) = media[audioId] {
let mediaIndex = mediaIndexCounter
mediaIndexCounter += 1
let item = InstantPageAudioItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: 48.0)), media: InstantPageMedia(index: mediaIndex, media: file, url: nil, caption: nil, credit: nil), webpage: webpage)
let item = InstantPageAudioItem(frame: CGRect(origin: CGPoint(x: 0.0, y: 0.0), size: CGSize(width: boundingWidth, height: 48.0)), media: InstantPageMedia(index: mediaIndex, media: .file(file), url: nil, caption: nil, credit: nil), webpage: webpage)
contentSize.height += item.frame.height
items.append(item)
@ -765,7 +770,9 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
for (i, article) in articles.enumerated() {
var cover: TelegramMediaImage?
if let coverId = article.photoId {
cover = media[coverId] as? TelegramMediaImage
if case let .image(image) = media[coverId] {
cover = image
}
}
var styleStack = InstantPageTextStyleStack()
@ -820,7 +827,7 @@ public func layoutInstantPageBlock(webpage: TelegramMediaWebpage, userLocation:
var contentSize = CGSize(width: boundingWidth - safeInset * 2.0, height: 0.0)
var items: [InstantPageItem] = []
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: -1, media: map, url: nil, caption: caption.text, credit: caption.credit), attributes: attributes, interactive: true, roundCorners: false, fit: false)
let mediaItem = InstantPageImageItem(frame: CGRect(origin: CGPoint(x: floor((boundingWidth - filledSize.width) / 2.0), y: 0.0), size: filledSize), webPage: webpage, media: InstantPageMedia(index: -1, media: .geo(map), url: nil, caption: caption.text, credit: caption.credit), attributes: attributes, interactive: true, roundCorners: false, fit: false)
items.append(mediaItem)
contentSize.height += filledSize.height
@ -850,12 +857,12 @@ public func instantPageLayoutForWebPage(_ webPage: TelegramMediaWebpage, userLoc
var contentSize = CGSize(width: boundingWidth, height: 0.0)
var items: [InstantPageItem] = []
var media = instantPage.media
var media = instantPage.media.mapValues(EngineMedia.init)
if let image = loadedContent.image, let id = image.id {
media[id] = image
media[id] = .image(image)
}
if let video = loadedContent.file, let id = video.id {
media[id] = video
media[id] = .file(video)
}
var mediaIndexCounter: Int = 0

View File

@ -1,15 +1,14 @@
import Foundation
import Postbox
import TelegramCore
public struct InstantPageMedia: Equatable {
public let index: Int
public let media: Media
public let media: EngineMedia
public let url: InstantPageUrlItem?
public let caption: RichText?
public let credit: RichText?
public init(index: Int, media: Media, url: InstantPageUrlItem?, caption: RichText?, credit: RichText?) {
public init(index: Int, media: EngineMedia, url: InstantPageUrlItem?, caption: RichText?, credit: RichText?) {
self.index = index
self.media = media
self.url = url
@ -18,6 +17,6 @@ public struct InstantPageMedia: Equatable {
}
public static func ==(lhs: InstantPageMedia, rhs: InstantPageMedia) -> Bool {
return lhs.index == rhs.index && lhs.media.isEqual(to: rhs.media) && lhs.url == rhs.url && lhs.caption == rhs.caption && lhs.credit == rhs.credit
return lhs.index == rhs.index && lhs.media == rhs.media && lhs.url == rhs.url && lhs.caption == rhs.caption && lhs.credit == rhs.credit
}
}

View File

@ -1,7 +1,6 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
import AccountContext
@ -22,7 +21,11 @@ struct InstantPageMediaPlaylistItemId: SharedMediaPlaylistItemId {
}
private func extractFileMedia(_ item: InstantPageMedia) -> TelegramMediaFile? {
return item.media as? TelegramMediaFile
if case let .file(file) = item.media {
return file
} else {
return nil
}
}
final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
@ -114,7 +117,7 @@ final class InstantPageMediaPlaylistItem: SharedMediaPlaylistItem {
}
struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
let webpageId: MediaId
let webpageId: EngineMedia.Id
func isEqual(to: SharedMediaPlaylistId) -> Bool {
if let to = to as? InstantPageMediaPlaylistId {
@ -125,7 +128,7 @@ struct InstantPageMediaPlaylistId: SharedMediaPlaylistId {
}
struct InstantPagePlaylistLocation: Equatable, SharedMediaPlaylistLocation {
let webpageId: MediaId
let webpageId: EngineMedia.Id
func isEqual(to: SharedMediaPlaylistLocation) -> Bool {
guard let to = to as? InstantPagePlaylistLocation else {

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData
@ -14,12 +13,12 @@ public final class InstantPagePeerReferenceItem: InstantPageItem {
public let separatesTiles: Bool = false
public let medias: [InstantPageMedia] = []
let initialPeer: Peer
let initialPeer: EnginePeer
let safeInset: CGFloat
let transparent: Bool
let rtl: Bool
init(frame: CGRect, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool) {
init(frame: CGRect, initialPeer: EnginePeer, safeInset: CGFloat, transparent: Bool, rtl: Bool) {
self.frame = frame
self.initialPeer = initialPeer
self.safeInset = safeInset

View File

@ -1,7 +1,6 @@
import Foundation
import UIKit
import TelegramCore
import Postbox
import SwiftSignalKit
import AsyncDisplayKit
import Display
@ -64,13 +63,13 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
private let activityIndicator: ActivityIndicator
private let checkNode: ASImageNode
var peer: Peer?
var peer: EnginePeer?
private var peerDisposable: Disposable?
private let joinDisposable = MetaDisposable()
private var joinState: JoinState = .none
init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, initialPeer: Peer, safeInset: CGFloat, transparent: Bool, rtl: Bool, openPeer: @escaping (EnginePeer) -> Void) {
init(context: AccountContext, strings: PresentationStrings, nameDisplayOrder: PresentationPersonNameOrder, theme: InstantPageTheme, initialPeer: EnginePeer, safeInset: CGFloat, transparent: Bool, rtl: Bool, openPeer: @escaping (EnginePeer) -> Void) {
self.context = context
self.strings = strings
self.nameDisplayOrder = nameDisplayOrder
@ -147,26 +146,26 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
let account = self.context.account
let context = self.context
let signal = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer)
|> mapToSignal({ peer -> Signal<Peer, NoError> in
let signal: Signal<EnginePeer, NoError> = actualizedPeer(postbox: account.postbox, network: account.network, peer: initialPeer._asPeer())
|> mapToSignal({ peer -> Signal<EnginePeer, NoError> in
if let peer = peer as? TelegramChannel, let username = peer.addressName, peer.accessHash == nil {
return .single(peer) |> then(context.engine.peers.resolvePeerByName(name: username)
|> mapToSignal({ updatedPeer -> Signal<Peer, NoError> in
return .single(.channel(peer)) |> then(context.engine.peers.resolvePeerByName(name: username)
|> mapToSignal({ updatedPeer -> Signal<EnginePeer, NoError> in
if let updatedPeer = updatedPeer {
return .single(updatedPeer._asPeer())
return .single(updatedPeer)
} else {
return .single(peer)
return .single(.channel(peer))
}
}))
} else {
return .single(peer)
return .single(EnginePeer(peer))
}
})
self.peerDisposable = (signal |> deliverOnMainQueue).start(next: { [weak self] peer in
if let strongSelf = self {
strongSelf.peer = peer
if let peer = peer as? TelegramChannel {
if case let .channel(peer) = peer {
var joinState = strongSelf.joinState
if case .member = peer.participationStatus {
switch joinState {
@ -210,7 +209,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
private func applyThemeAndStrings(themeUpdated: Bool) {
if let peer = self.peer {
let textColor = self.transparent ? UIColor.white : self.theme.panelPrimaryColor
self.nameNode.attributedText = NSAttributedString(string: EnginePeer(peer).displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: textColor)
self.nameNode.attributedText = NSAttributedString(string: peer.displayTitle(strings: self.strings, displayOrder: self.nameDisplayOrder), font: Font.medium(17.0), textColor: textColor)
}
let accentColor = self.transparent ? UIColor.white : self.theme.panelAccentColor
self.joinNode.setAttributedTitle(NSAttributedString(string: self.strings.Channel_JoinChannel, font: Font.medium(17.0), textColor: accentColor), for: [])
@ -300,7 +299,7 @@ final class InstantPagePeerReferenceNode: ASDisplayNode, InstantPageNode {
@objc func buttonPressed() {
if let peer = self.peer {
self.openPeer(EnginePeer(peer))
self.openPeer(peer)
}
}

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData
@ -29,7 +28,7 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
private var currentSize: CGSize?
private var fetchStatus: MediaResourceStatus?
private var fetchStatus: EngineMediaResource.FetchStatus?
private var fetchedDisposable = MetaDisposable()
private var statusDisposable = MetaDisposable()
@ -47,17 +46,19 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
self.openMedia = openMedia
var imageReference: ImageMediaReference?
if let file = media.media as? TelegramMediaFile, let presentation = smallestImageRepresentation(file.previewRepresentations) {
let image = TelegramMediaImage(imageId: MediaId(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
if case let .file(file) = media.media, let presentation = smallestImageRepresentation(file.previewRepresentations) {
let image = TelegramMediaImage(imageId: EngineMedia.Id(namespace: 0, id: 0), representations: [presentation], immediateThumbnailData: file.immediateThumbnailData, reference: nil, partialReference: nil, flags: [])
imageReference = ImageMediaReference.webPage(webPage: WebpageReference(webPage), media: image)
}
var streamVideo = false
if let file = media.media as? TelegramMediaFile {
var fileValue: TelegramMediaFile?
if case let .file(file) = media.media {
streamVideo = isMediaStreamable(media: file)
fileValue = file
}
self.videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: media.media as! TelegramMediaFile), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true)
self.videoNode = UniversalVideoNode(postbox: context.account.postbox, audioSession: context.sharedContext.mediaManager.audioSession, manager: context.sharedContext.mediaManager.universalVideoManager, decoration: GalleryVideoDecoration(), content: NativeVideoContent(id: .instantPage(webPage.webpageId, media.media.id!), userLocation: userLocation, fileReference: .webPage(webPage: WebpageReference(webPage), media: fileValue!), imageReference: imageReference, streamVideo: streamVideo ? .conservative : .none, loopVideo: true, enableSound: false, fetchAutomatically: true, placeholderColor: theme.pageBackgroundColor, storeAfterDownload: nil), priority: .embedded, autoplay: true)
self.videoNode.isUserInteractionEnabled = false
self.statusNode = RadialStatusNode(backgroundNodeColor: UIColor(white: 0.0, alpha: 0.6))
@ -66,13 +67,13 @@ final class InstantPagePlayableVideoNode: ASDisplayNode, InstantPageNode, Galler
self.addSubnode(self.videoNode)
if let file = media.media as? TelegramMediaFile {
if case let .file(file) = media.media {
self.fetchedDisposable.set(fetchedMediaResource(mediaBox: context.account.postbox.mediaBox, userLocation: userLocation, userContentType: .video, reference: AnyMediaReference.webPage(webPage: WebpageReference(webPage), media: file).resourceReference(file.resource)).start())
self.statusDisposable.set((context.account.postbox.mediaBox.resourceStatus(file.resource) |> deliverOnMainQueue).start(next: { [weak self] status in
displayLinkDispatcher.dispatch {
if let strongSelf = self {
strongSelf.fetchStatus = status
strongSelf.fetchStatus = EngineMediaResource.FetchStatus(status)
strongSelf.updateFetchStatus()
}
}

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SwiftSignalKit
import AccountContext

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import TelegramCore
import SafariServices
import TelegramPresentationData
@ -197,9 +196,9 @@ class InstantPageReferenceControllerNode: ViewControllerTracingNode, UIScrollVie
if self.contentNode == nil || self.contentNode?.frame.width != width {
self.contentNode?.removeFromSupernode()
var media: [MediaId: Media] = [:]
var media: [EngineMedia.Id: EngineMedia] = [:]
if case let .Loaded(content) = self.webPage.content, let instantPage = content.instantPage {
media = instantPage.media
media = instantPage.media.mapValues(EngineMedia.init)
}
let sideInset: CGFloat = 16.0

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import Postbox
import Display
import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import Display
import AsyncDisplayKit
import Postbox
import SwiftSignalKit
import TelegramPresentationData
import TelegramUIPreferences

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData

View File

@ -186,9 +186,9 @@ private final class InstantPageSlideshowPagerNode: ASDisplayNode, UIScrollViewDe
private func makeNodeForItem(at index: Int) -> InstantPageSlideshowItemNode {
let media = self.items[index]
let contentNode: ASDisplayNode
if let _ = media.media as? TelegramMediaImage {
if case .image = media.media {
contentNode = InstantPageImageNode(context: self.context, sourceLocation: self.sourceLocation, theme: self.theme, webPage: self.webPage, media: media, attributes: [], interactive: true, roundCorners: false, fit: false, openMedia: self.openMedia, longPressMedia: self.longPressMedia, activatePinchPreview: self.activatePinchPreview, pinchPreviewFinished: self.pinchPreviewFinished)
} else if let _ = media.media as? TelegramMediaFile {
} else if case .file = media.media {
contentNode = ASDisplayNode()
} else {
contentNode = ASDisplayNode()

View File

@ -1,7 +1,6 @@
import Foundation
import UIKit
import SwiftSignalKit
import Postbox
import TelegramCore
import TelegramUIPreferences
@ -58,7 +57,7 @@ public final class InstantPageStoredState: Codable {
}
public func instantPageStoredState(engine: TelegramEngine, webPage: TelegramMediaWebpage) -> Signal<InstantPageStoredState?, NoError> {
let key = ValueBoxKey(length: 8)
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: webPage.webpageId.id)
return engine.data.get(TelegramEngine.EngineData.Item.ItemCache.Item(collectionId: ApplicationSpecificItemCacheCollectionId.instantPageStoredState, id: key))
@ -68,7 +67,7 @@ public func instantPageStoredState(engine: TelegramEngine, webPage: TelegramMedi
}
public func updateInstantPageStoredStateInteractively(engine: TelegramEngine, webPage: TelegramMediaWebpage, state: InstantPageStoredState?) -> Signal<Never, NoError> {
let key = ValueBoxKey(length: 8)
let key = EngineDataBuffer(length: 8)
key.setInt64(0, value: webPage.webpageId.id)
if let state = state {

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import Display
import Postbox
import TelegramCore
import SwiftSignalKit
import TelegramPresentationData

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import AsyncDisplayKit
import TelegramCore
import Postbox
import Display
import TelegramPresentationData
import TelegramUIPreferences
@ -277,7 +276,7 @@ private func offestForVerticalAlignment(_ verticalAlignment: TableVerticalAlignm
}
}
func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: InstantPageTextStyleStack, theme: InstantPageTheme, bordered: Bool, striped: Bool, boundingWidth: CGFloat, horizontalInset: CGFloat, media: [MediaId: Media], webpage: TelegramMediaWebpage) -> InstantPageTableItem {
func layoutTableItem(rtl: Bool, rows: [InstantPageTableRow], styleStack: InstantPageTextStyleStack, theme: InstantPageTheme, bordered: Bool, striped: Bool, boundingWidth: CGFloat, horizontalInset: CGFloat, media: [EngineMedia.Id: EngineMedia], webpage: TelegramMediaWebpage) -> InstantPageTableItem {
if rows.count == 0 {
return InstantPageTableItem(frame: CGRect(), totalWidth: 0.0, horizontalInset: 0.0, borderWidth: 0.0, theme: theme, cells: [], rtl: rtl)
}

View File

@ -2,7 +2,6 @@ import Foundation
import UIKit
import TelegramCore
import Display
import Postbox
import AsyncDisplayKit
import TelegramPresentationData
import TelegramUIPreferences
@ -12,9 +11,9 @@ import ContextUI
public final class InstantPageUrlItem: Equatable {
public let url: String
public let webpageId: MediaId?
public let webpageId: EngineMedia.Id?
public init(url: String, webpageId: MediaId?) {
public init(url: String, webpageId: EngineMedia.Id?) {
self.url = url
self.webpageId = webpageId
}
@ -36,7 +35,7 @@ struct InstantPageTextStrikethroughItem {
struct InstantPageTextImageItem {
let frame: CGRect
let range: NSRange
let id: MediaId
let id: EngineMedia.Id
}
struct InstantPageTextAnchorItem {
@ -649,7 +648,7 @@ func attributedStringForRichText(_ text: RichText, styleStack: InstantPageTextSt
}
}
func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, horizontalInset: CGFloat = 0.0, alignment: NSTextAlignment = .natural, offset: CGPoint, media: [MediaId: Media] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, maxNumberOfLines: Int = 0, opaqueBackground: Bool = false) -> (InstantPageTextItem?, [InstantPageItem], CGSize) {
func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFloat, horizontalInset: CGFloat = 0.0, alignment: NSTextAlignment = .natural, offset: CGPoint, media: [EngineMedia.Id: EngineMedia] = [:], webpage: TelegramMediaWebpage? = nil, minimizeWidth: Bool = false, maxNumberOfLines: Int = 0, opaqueBackground: Bool = false) -> (InstantPageTextItem?, [InstantPageItem], CGSize) {
if string.length == 0 {
return (nil, [], CGSize())
}
@ -771,7 +770,7 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
extraDescent = max(extraDescent, imageFrame.maxY - (workingLineOrigin.y + fontLineHeight + minSpacing))
}
maxImageHeight = max(maxImageHeight, imageFrame.height)
lineImageItems.append(InstantPageTextImageItem(frame: imageFrame, range: range, id: MediaId(namespace: Namespaces.Media.CloudFile, id: id)))
lineImageItems.append(InstantPageTextImageItem(frame: imageFrame, range: range, id: EngineMedia.Id(namespace: Namespaces.Media.CloudFile, id: id)))
}
}
}
@ -876,8 +875,8 @@ func layoutTextItemWithString(_ string: NSAttributedString, boundingWidth: CGFlo
for line in textItem.lines {
let lineFrame = frameForLine(line, boundingWidth: boundingWidth, alignment: alignment)
for imageItem in line.imageItems {
if let image = media[imageItem.id] as? TelegramMediaFile {
let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: image, url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
if case let .image(image) = media[imageItem.id] {
let item = InstantPageImageItem(frame: imageItem.frame.offsetBy(dx: lineFrame.minX + offset.x, dy: offset.y), webPage: webpage, media: InstantPageMedia(index: -1, media: .image(image), url: nil, caption: nil, credit: nil), interactive: false, roundCorners: false, fit: false)
additionalItems.append(item)
if item.frame.minY < topInset {

View File

@ -1,6 +1,5 @@
import Foundation
import UIKit
import Postbox
import TelegramCore
import AsyncDisplayKit
import TelegramPresentationData

View File

@ -389,7 +389,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
let avatarListNode = PeerInfoAvatarListContainerNode(context: item.context)
avatarListWrapperNode.contentNode.clipsToBounds = true
avatarListNode.backgroundColor = .clear
avatarListNode.peer = peer
avatarListNode.peer = EnginePeer(peer)
avatarListNode.firstFullSizeOnly = true
avatarListNode.offsetLocation = true
avatarListNode.customCenterTapAction = { [weak self] in
@ -405,7 +405,7 @@ public class ItemListInviteRequestItemNode: ListViewItemNode, ItemListItemNode {
avatarListContainerNode.addSubnode(avatarListNode.controlsClippingOffsetNode)
avatarListWrapperNode.contentNode.addSubnode(avatarListContainerNode)
avatarListNode.update(size: targetRect.size, peer: peer, customNode: nil, additionalEntry: .single(nil), isExpanded: true, transition: .immediate)
avatarListNode.update(size: targetRect.size, peer: EnginePeer(peer), customNode: nil, additionalEntry: .single(nil), isExpanded: true, transition: .immediate)
strongSelf.offsetContainerNode.supernode?.addSubnode(avatarListWrapperNode)
strongSelf.avatarListWrapperNode = avatarListWrapperNode

View File

@ -878,7 +878,7 @@ public final class ListMessageFileItemNode: ListMessageNode {
if statusUpdated && item.displayFileInfo {
if let file = selectedMedia as? TelegramMediaFile {
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
updatedStatusSignal = messageFileMediaResourceStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
if case .Fetching = value.fetchStatus, !item.isDownloadList {
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())
@ -905,10 +905,10 @@ public final class ListMessageFileItemNode: ListMessageNode {
}
}
if isVoice {
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: message, isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
updatedPlaybackStatusSignal = messageFileMediaPlaybackStatus(context: item.context, file: file, message: EngineMessage(message), isRecentActions: false, isGlobalSearch: item.isGlobalSearchResult, isDownloadList: item.isDownloadList)
}
} else if let image = selectedMedia as? TelegramMediaImage {
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: message, isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
updatedStatusSignal = messageImageMediaResourceStatus(context: item.context, image: image, message: EngineMessage(message), isRecentActions: false, isSharedMedia: true, isGlobalSearch: item.isGlobalSearchResult || item.isDownloadList)
|> mapToSignal { value -> Signal<FileMediaResourceStatus, NoError> in
if case .Fetching = value.fetchStatus, !item.isDownloadList {
return .single(value) |> delay(0.1, queue: Queue.concurrentDefaultQueue())

View File

@ -1,156 +0,0 @@
load("@build_bazel_rules_swift//swift:swift.bzl", "swift_library")
load(
"@build_bazel_rules_apple//apple:resources.bzl",
"apple_resource_bundle",
"apple_resource_group",
)
load("//build-system/bazel-utils:plist_fragment.bzl",
"plist_fragment",
)
filegroup(
name = "LottieMeshSwiftMetalResources",
srcs = glob([
"Resources/**/*.metal",
]),
visibility = ["//visibility:public"],
)
plist_fragment(
name = "LottieMeshSwiftBundleInfoPlist",
extension = "plist",
template =
"""
<key>CFBundleIdentifier</key>
<string>org.telegram.LottieMeshSwift</string>
<key>CFBundleDevelopmentRegion</key>
<string>en</string>
<key>CFBundleName</key>
<string>LottieMeshSwift</string>
"""
)
apple_resource_bundle(
name = "LottieMeshSwiftBundle",
infoplists = [
":LottieMeshSwiftBundleInfoPlist",
],
resources = [
":LottieMeshSwiftMetalResources",
],
)
config_setting(
name = "debug_build",
values = {
"compilation_mode": "dbg",
},
)
optimization_flags = select({
":debug_build": [
"-O2",
],
"//conditions:default": [],
})
swift_optimization_flags = select({
":debug_build": [
#"-O",
],
"//conditions:default": [],
})
swift_library(
name = "LottieMeshSwift",
module_name = "LottieMeshSwift",
srcs = glob([
"Sources/**/*.swift",
]),
copts = [
"-warnings-as-errors",
] + swift_optimization_flags,
data = [
":LottieMeshSwiftBundle",
],
deps = [
":LottieMeshBinding",
"//submodules/Postbox:Postbox",
"//submodules/ManagedFile:ManagedFile",
],
visibility = [
"//visibility:public",
],
)
objc_library(
name = "LottieMeshBinding",
enable_modules = True,
module_name = "LottieMeshBinding",
srcs = glob([
"LottieMeshBinding/Sources/**/*.m",
"LottieMeshBinding/Sources/**/*.mm",
"LottieMeshBinding/Sources/**/*.h",
]),
copts = optimization_flags,
hdrs = glob([
"LottieMeshBinding/PublicHeaders/**/*.h",
]),
includes = [
"LottieMeshBinding/PublicHeaders",
],
deps = [
":LottieMesh",
],
sdk_frameworks = [
"Foundation",
],
visibility = [
"//visibility:public",
],
)
cc_library(
name = "LottieMesh",
srcs = glob([
"LottieMesh/Sources/**/*.cpp",
"LottieMesh/Sources/**/*.h",
"LottieMesh/Sources/**/*.hpp",
]),
copts = [
"-Isubmodules/LottieMeshSwift/libtess2/Include",
] + optimization_flags,
hdrs = glob([
"LottieMesh/PublicHeaders/**/*.h",
"LottieMesh/PublicHeaders/**/*.hpp",
]),
includes = [
"LottieMesh/PublicHeaders",
],
deps = [
":libtess2",
],
visibility = [
"//visibility:public",
],
)
cc_library(
name = "libtess2",
srcs = glob([
"libtess2/Sources/**/*.c",
"libtess2/Sources/**/*.h",
"libtess2/Include/**/*.h",
]),
copts = [
"-Isubmodules/LottieMeshSwift/libtess2/Include",
] + optimization_flags,
hdrs = glob([
"libtess2/Include/**/*.h",
]),
deps = [
],
visibility = [
"//visibility:public",
],
)

View File

@ -1,60 +0,0 @@
#ifndef LottieMesh_h
#define LottieMesh_h
#include <memory>
#include <vector>
#include "Point.h"
namespace MeshGenerator {
struct Path {
std::vector<Point> points;
};
struct Fill {
enum class Rule {
EvenOdd,
NonZero
};
Rule rule = Rule::EvenOdd;
explicit Fill(Rule rule_) :
rule(rule_) {
}
};
struct Stroke {
enum class LineJoin {
Miter,
Round,
Bevel
};
enum class LineCap {
Butt,
Round,
Square
};
float lineWidth = 0.0f;
LineJoin lineJoin = LineJoin::Round;
LineCap lineCap = LineCap::Round;
float miterLimit = 10.0f;
explicit Stroke(float lineWidth_, LineJoin lineJoin_, LineCap lineCap_, float miterLimit_) :
lineWidth(lineWidth_), lineJoin(lineJoin_), lineCap(lineCap_), miterLimit(miterLimit_) {
}
};
struct Mesh {
std::vector<Point> vertices;
std::vector<int> triangles;
};
std::unique_ptr<Mesh> generateMesh(std::vector<Path> const &paths, std::unique_ptr<Fill> fill, std::unique_ptr<Stroke> stroke);
}
#endif /* LottieMesh_h */

View File

@ -1,47 +0,0 @@
#ifndef Point_h
#define Point_h
#include <math.h>
#include <cmath>
namespace MeshGenerator {
struct Point {
float x = 0.0f;
float y = 0.0f;
Point(float x_, float y_) :
x(x_), y(y_) {
}
Point() : Point(0.0f, 0.0f) {
}
bool isEqual(Point const &other, float epsilon = 0.0001f) const {
return std::abs(x - other.x) <= epsilon && std::abs(y - other.y) <= epsilon;
}
float distance(Point const &other) const {
float dx = x - other.x;
float dy = y - other.y;
return sqrtf(dx * dx + dy * dy);
}
bool operator< (Point const &other) const {
if (x < other.x) {
return true;
}
if (x > other.x) {
return false;
}
return y < other.y;
}
bool operator== (Point const &other) const {
return isEqual(other);
}
};
}
#endif

View File

@ -1,86 +0,0 @@
#pragma once
#include "Vec2.h"
#include <optional>
namespace crushedpixel {
template<typename Vec2>
struct LineSegment {
LineSegment(const Vec2 &a, const Vec2 &b) :
a(a), b(b) {}
Vec2 a, b;
/**
* @return A copy of the line segment, offset by the given vector.
*/
LineSegment operator+(const Vec2 &toAdd) const {
return {Vec2Maths::add(a, toAdd), Vec2Maths::add(b, toAdd)};
}
/**
* @return A copy of the line segment, offset by the given vector.
*/
LineSegment operator-(const Vec2 &toRemove) const {
return {Vec2Maths::subtract(a, toRemove), Vec2Maths::subtract(b, toRemove)};
}
/**
* @return The line segment's normal vector.
*/
Vec2 normal() const {
auto dir = direction();
// return the direction vector
// rotated by 90 degrees counter-clockwise
return {-dir.y, dir.x};
}
/**
* @return The line segment's direction vector.
*/
Vec2 direction(bool normalized = true) const {
auto vec = Vec2Maths::subtract(b, a);
return normalized
? Vec2Maths::normalized(vec)
: vec;
}
static Vec2 intersection(const LineSegment &a, const LineSegment &b, bool infiniteLines, bool &success) {
success = true;
// calculate un-normalized direction vectors
auto r = a.direction(false);
auto s = b.direction(false);
auto originDist = Vec2Maths::subtract(b.a, a.a);
auto uNumerator = Vec2Maths::cross(originDist, r);
auto denominator = Vec2Maths::cross(r, s);
if (std::abs(denominator) < 0.0001f) {
// The lines are parallel
success = false;
return Vec2();
}
// solve the intersection positions
auto u = uNumerator / denominator;
auto t = Vec2Maths::cross(originDist, s) / denominator;
if (!infiniteLines && (t < 0 || t > 1 || u < 0 || u > 1)) {
// the intersection lies outside of the line segments
success = false;
return Vec2();
}
// calculate the intersection point
// a.a + r * t;
return Vec2Maths::add(a.a, Vec2Maths::multiply(r, t));
}
};
} // namespace crushedpixel

View File

@ -1,95 +0,0 @@
#include <LottieMesh/LottieMesh.h>
#include <LottieMesh/Point.h>
#include "Triangulation.h"
#include "tesselator.h"
#include "Polyline2D.h"
namespace MeshGenerator {
std::unique_ptr<Mesh> generateMesh(std::vector<Path> const &paths, std::unique_ptr<Fill> fill, std::unique_ptr<Stroke> stroke) {
if (stroke) {
std::unique_ptr<Mesh> mesh = std::make_unique<Mesh>();
for (const auto &path : paths) {
crushedpixel::Polyline2D::JointStyle jointStyle = crushedpixel::Polyline2D::JointStyle::ROUND;
crushedpixel::Polyline2D::EndCapStyle endCapStyle = crushedpixel::Polyline2D::EndCapStyle::SQUARE;
switch (stroke->lineJoin) {
case Stroke::LineJoin::Miter:
jointStyle = crushedpixel::Polyline2D::JointStyle::MITER;
break;
case Stroke::LineJoin::Round:
jointStyle = crushedpixel::Polyline2D::JointStyle::ROUND;
break;
case Stroke::LineJoin::Bevel:
jointStyle = crushedpixel::Polyline2D::JointStyle::BEVEL;
break;
default: {
break;
}
}
switch (stroke->lineCap) {
case Stroke::LineCap::Round: {
endCapStyle = crushedpixel::Polyline2D::EndCapStyle::ROUND;
break;
}
case Stroke::LineCap::Square: {
endCapStyle = crushedpixel::Polyline2D::EndCapStyle::SQUARE;
break;
}
case Stroke::LineCap::Butt: {
endCapStyle = crushedpixel::Polyline2D::EndCapStyle::BUTT;
break;
}
default: {
break;
}
}
auto vertices = crushedpixel::Polyline2D::create(path.points, stroke->lineWidth, jointStyle, endCapStyle);
for (const auto &vertex : vertices) {
mesh->triangles.push_back((int)mesh->vertices.size());
mesh->vertices.push_back(vertex);
}
}
assert(mesh->triangles.size() % 3 == 0);
return mesh;
} else if (fill) {
TESStesselator *tessellator = tessNewTess(NULL);
tessSetOption(tessellator, TESS_CONSTRAINED_DELAUNAY_TRIANGULATION, 1);
for (const auto &path : paths) {
tessAddContour(tessellator, 2, path.points.data(), sizeof(Point), (int)path.points.size());
}
switch (fill->rule) {
case Fill::Rule::EvenOdd: {
tessTesselate(tessellator, TESS_WINDING_ODD, TESS_POLYGONS, 3, 2, NULL);
break;
}
default: {
tessTesselate(tessellator, TESS_WINDING_NONZERO, TESS_POLYGONS, 3, 2, NULL);
break;
}
}
int vertexCount = tessGetVertexCount(tessellator);
const TESSreal *vertices = tessGetVertices(tessellator);
int indexCount = tessGetElementCount(tessellator) * 3;
const TESSindex *indices = tessGetElements(tessellator);
std::unique_ptr<Mesh> mesh = std::make_unique<Mesh>();
for (int i = 0; i < vertexCount; i++) {
mesh->vertices.push_back(Point(vertices[i * 2 + 0], vertices[i * 2 + 1]));
}
for (int i = 0; i < indexCount; i++) {
mesh->triangles.push_back(indices[i]);
}
return mesh;
} else {
return nullptr;
}
}
}

View File

@ -1,446 +0,0 @@
#pragma once
#include "LineSegment.h"
#include <vector>
#include <iterator>
#include <cassert>
namespace crushedpixel {
class Polyline2D {
public:
enum class JointStyle {
/**
* Corners are drawn with sharp joints.
* If the joint's outer angle is too large,
* the joint is drawn as beveled instead,
* to avoid the miter extending too far out.
*/
MITER,
/**
* Corners are flattened.
*/
BEVEL,
/**
* Corners are rounded off.
*/
ROUND
};
enum class EndCapStyle {
/**
* Path ends are drawn flat,
* and don't exceed the actual end point.
*/
BUTT, // lol
/**
* Path ends are drawn flat,
* but extended beyond the end point
* by half the line thickness.
*/
SQUARE,
/**
* Path ends are rounded off.
*/
ROUND,
/**
* Path ends are connected according to the JointStyle.
* When using this EndCapStyle, don't specify the common start/end point twice,
* as Polyline2D connects the first and last input point itself.
*/
JOINT
};
/**
* Creates a vector of vertices describing a solid path through the input points.
* @param points The points of the path.
* @param thickness The path's thickness.
* @param jointStyle The path's joint style.
* @param endCapStyle The path's end cap style.
* @param allowOverlap Whether to allow overlapping vertices.
* This yields better results when dealing with paths
* whose points have a distance smaller than the thickness,
* but may introduce overlapping vertices,
* which is undesirable when rendering transparent paths.
* @return The vertices describing the path.
* @tparam Vec2 The vector type to use for the vertices.
* Must have public non-const float fields "x" and "y".
* Must have a two-args constructor taking x and y values.
* See crushedpixel::Vec2 for a type that satisfies these requirements.
* @tparam InputCollection The collection type of the input points.
* Must contain elements of type Vec2.
* Must expose size() and operator[] functions.
*/
template<typename Vec2, typename InputCollection>
static std::vector<Vec2> create(const InputCollection &points, float thickness,
JointStyle jointStyle = JointStyle::MITER,
EndCapStyle endCapStyle = EndCapStyle::BUTT,
bool allowOverlap = false) {
std::vector<Vec2> vertices;
create(vertices, points, thickness, jointStyle, endCapStyle, allowOverlap);
return vertices;
}
template<typename Vec2>
static std::vector<Vec2> create(const std::vector<Vec2> &points, float thickness,
JointStyle jointStyle = JointStyle::MITER,
EndCapStyle endCapStyle = EndCapStyle::BUTT,
bool allowOverlap = false) {
std::vector<Vec2> vertices;
create<Vec2, std::vector<Vec2>>(vertices, points, thickness, jointStyle, endCapStyle, allowOverlap);
return vertices;
}
template<typename Vec2, typename InputCollection>
static size_t create(std::vector<Vec2> &vertices, const InputCollection &points, float thickness,
JointStyle jointStyle = JointStyle::MITER,
EndCapStyle endCapStyle = EndCapStyle::BUTT,
bool allowOverlap = false) {
auto numVerticesBefore = vertices.size();
create<Vec2, InputCollection>(std::back_inserter(vertices), points, thickness,
jointStyle, endCapStyle, allowOverlap);
return vertices.size() - numVerticesBefore;
}
template<typename Vec2, typename InputCollection, typename OutputIterator>
static OutputIterator create(OutputIterator vertices, const InputCollection &points, float thickness,
JointStyle jointStyle = JointStyle::MITER,
EndCapStyle endCapStyle = EndCapStyle::BUTT,
bool allowOverlap = false) {
// operate on half the thickness to make our lives easier
thickness /= 2;
// create poly segments from the points
std::vector<PolySegment<Vec2>> segments;
for (size_t i = 0; i + 1 < points.size(); i++) {
auto &point1 = points[i];
auto &point2 = points[i + 1];
// to avoid division-by-zero errors,
// only create a line segment for non-identical points
if (!Vec2Maths::equal(point1, point2)) {
segments.emplace_back(LineSegment<Vec2>(point1, point2), thickness);
}
}
if (endCapStyle == EndCapStyle::JOINT) {
// create a connecting segment from the last to the first point
auto &point1 = points[points.size() - 1];
auto &point2 = points[0];
// to avoid division-by-zero errors,
// only create a line segment for non-identical points
if (!Vec2Maths::equal(point1, point2)) {
segments.emplace_back(LineSegment<Vec2>(point1, point2), thickness);
}
}
if (segments.empty()) {
// handle the case of insufficient input points
return vertices;
}
Vec2 nextStart1{0, 0};
Vec2 nextStart2{0, 0};
Vec2 start1{0, 0};
Vec2 start2{0, 0};
Vec2 end1{0, 0};
Vec2 end2{0, 0};
// calculate the path's global start and end points
auto &firstSegment = segments[0];
auto &lastSegment = segments[segments.size() - 1];
auto pathStart1 = firstSegment.edge1.a;
auto pathStart2 = firstSegment.edge2.a;
auto pathEnd1 = lastSegment.edge1.b;
auto pathEnd2 = lastSegment.edge2.b;
// handle different end cap styles
if (endCapStyle == EndCapStyle::SQUARE) {
// extend the start/end points by half the thickness
pathStart1 = Vec2Maths::subtract(pathStart1, Vec2Maths::multiply(firstSegment.edge1.direction(), thickness));
pathStart2 = Vec2Maths::subtract(pathStart2, Vec2Maths::multiply(firstSegment.edge2.direction(), thickness));
pathEnd1 = Vec2Maths::add(pathEnd1, Vec2Maths::multiply(lastSegment.edge1.direction(), thickness));
pathEnd2 = Vec2Maths::add(pathEnd2, Vec2Maths::multiply(lastSegment.edge2.direction(), thickness));
} else if (endCapStyle == EndCapStyle::ROUND) {
// draw half circle end caps
createTriangleFan(vertices, firstSegment.center.a, firstSegment.center.a,
firstSegment.edge1.a, firstSegment.edge2.a, false);
createTriangleFan(vertices, lastSegment.center.b, lastSegment.center.b,
lastSegment.edge1.b, lastSegment.edge2.b, true);
} else if (endCapStyle == EndCapStyle::JOINT) {
// join the last (connecting) segment and the first segment
createJoint(vertices, lastSegment, firstSegment, jointStyle,
pathEnd1, pathEnd2, pathStart1, pathStart2, allowOverlap);
}
// generate mesh data for path segments
for (size_t i = 0; i < segments.size(); i++) {
auto &segment = segments[i];
// calculate start
if (i == 0) {
// this is the first segment
start1 = pathStart1;
start2 = pathStart2;
}
if (i + 1 == segments.size()) {
// this is the last segment
end1 = pathEnd1;
end2 = pathEnd2;
} else {
createJoint(vertices, segment, segments[i + 1], jointStyle,
end1, end2, nextStart1, nextStart2, allowOverlap);
}
// emit vertices
*vertices++ = start1;
*vertices++ = start2;
*vertices++ = end1;
*vertices++ = end1;
*vertices++ = start2;
*vertices++ = end2;
start1 = nextStart1;
start2 = nextStart2;
}
return vertices;
}
private:
static constexpr float pi = 3.14159265358979323846f;
/**
* The threshold for mitered joints.
* If the joint's angle is smaller than this angle,
* the joint will be drawn beveled instead.
*/
static constexpr float miterMinAngle = 0.349066; // ~20 degrees
/**
* The minimum angle of a round joint's triangles.
*/
static constexpr float roundMinAngle = 0.174533; // ~10 degrees
template<typename Vec2>
struct PolySegment {
PolySegment(const LineSegment<Vec2> &center, float thickness) :
center(center),
// calculate the segment's outer edges by offsetting
// the central line by the normal vector
// multiplied with the thickness
// center + center.normal() * thickness
edge1(center + Vec2Maths::multiply(center.normal(), thickness)),
edge2(center - Vec2Maths::multiply(center.normal(), thickness)) {}
LineSegment<Vec2> center, edge1, edge2;
};
template<typename Vec2, typename OutputIterator>
static OutputIterator createJoint(OutputIterator vertices,
const PolySegment<Vec2> &segment1, const PolySegment<Vec2> &segment2,
JointStyle jointStyle, Vec2 &end1, Vec2 &end2,
Vec2 &nextStart1, Vec2 &nextStart2,
bool allowOverlap) {
// calculate the angle between the two line segments
auto dir1 = segment1.center.direction();
auto dir2 = segment2.center.direction();
auto angle = Vec2Maths::angle(dir1, dir2);
// wrap the angle around the 180° mark if it exceeds 90°
// for minimum angle detection
auto wrappedAngle = angle;
if (wrappedAngle > pi / 2) {
wrappedAngle = pi - wrappedAngle;
}
if (jointStyle == JointStyle::MITER && wrappedAngle < miterMinAngle) {
// the minimum angle for mitered joints wasn't exceeded.
// to avoid the intersection point being extremely far out,
// thus producing an enormous joint like a rasta on 4/20,
// we render the joint beveled instead.
jointStyle = JointStyle::BEVEL;
}
if (jointStyle == JointStyle::MITER) {
// calculate each edge's intersection point
// with the next segment's central line
bool sec1Success = true;
bool sec2Success = true;
auto sec1 = LineSegment<Vec2>::intersection(segment1.edge1, segment2.edge1, true, sec1Success);
auto sec2 = LineSegment<Vec2>::intersection(segment1.edge2, segment2.edge2, true, sec2Success);
end1 = sec1Success ? sec1 : segment1.edge1.b;
end2 = sec2Success ? sec2 : segment1.edge2.b;
nextStart1 = end1;
nextStart2 = end2;
} else {
// joint style is either BEVEL or ROUND
// find out which are the inner edges for this joint
auto x1 = dir1.x;
auto x2 = dir2.x;
auto y1 = dir1.y;
auto y2 = dir2.y;
auto clockwise = x1 * y2 - x2 * y1 < 0;
const LineSegment<Vec2> *inner1, *inner2, *outer1, *outer2;
// as the normal vector is rotated counter-clockwise,
// the first edge lies to the left
// from the central line's perspective,
// and the second one to the right.
if (clockwise) {
outer1 = &segment1.edge1;
outer2 = &segment2.edge1;
inner1 = &segment1.edge2;
inner2 = &segment2.edge2;
} else {
outer1 = &segment1.edge2;
outer2 = &segment2.edge2;
inner1 = &segment1.edge1;
inner2 = &segment2.edge1;
}
// calculate the intersection point of the inner edges
bool innerSecOptSuccess = true;
auto innerSecOpt = LineSegment<Vec2>::intersection(*inner1, *inner2, allowOverlap, innerSecOptSuccess);
auto innerSec = innerSecOptSuccess
? innerSecOpt
// for parallel lines, simply connect them directly
: inner1->b;
// if there's no inner intersection, flip
// the next start position for near-180° turns
Vec2 innerStart;
if (innerSecOptSuccess) {
innerStart = innerSec;
} else if (angle > pi / 2) {
innerStart = outer1->b;
} else {
innerStart = inner1->b;
}
if (clockwise) {
end1 = outer1->b;
end2 = innerSec;
nextStart1 = outer2->a;
nextStart2 = innerStart;
} else {
end1 = innerSec;
end2 = outer1->b;
nextStart1 = innerStart;
nextStart2 = outer2->a;
}
// connect the intersection points according to the joint style
if (jointStyle == JointStyle::BEVEL) {
// simply connect the intersection points
*vertices++ = outer1->b;
*vertices++ = outer2->a;
*vertices++ = innerSec;
} else if (jointStyle == JointStyle::ROUND) {
// draw a circle between the ends of the outer edges,
// centered at the actual point
// with half the line thickness as the radius
createTriangleFan(vertices, innerSec, segment1.center.b, outer1->b, outer2->a, clockwise);
} else {
assert(false);
}
}
return vertices;
}
/**
* Creates a partial circle between two points.
* The points must be equally far away from the origin.
* @param vertices The vector to add vertices to.
* @param connectTo The position to connect the triangles to.
* @param origin The circle's origin.
* @param start The circle's starting point.
* @param end The circle's ending point.
* @param clockwise Whether the circle's rotation is clockwise.
*/
template<typename Vec2, typename OutputIterator>
static OutputIterator createTriangleFan(OutputIterator vertices, Vec2 connectTo, Vec2 origin,
Vec2 start, Vec2 end, bool clockwise) {
auto point1 = Vec2Maths::subtract(start, origin);
auto point2 = Vec2Maths::subtract(end, origin);
// calculate the angle between the two points
auto angle1 = atan2(point1.y, point1.x);
auto angle2 = atan2(point2.y, point2.x);
// ensure the outer angle is calculated
if (clockwise) {
if (angle2 > angle1) {
angle2 = angle2 - 2 * pi;
}
} else {
if (angle1 > angle2) {
angle1 = angle1 - 2 * pi;
}
}
auto jointAngle = angle2 - angle1;
// calculate the amount of triangles to use for the joint
auto numTriangles = std::max(1, (int) std::floor(std::abs(jointAngle) / roundMinAngle));
// calculate the angle of each triangle
auto triAngle = jointAngle / numTriangles;
Vec2 startPoint = start;
Vec2 endPoint;
for (int t = 0; t < numTriangles; t++) {
if (t + 1 == numTriangles) {
// it's the last triangle - ensure it perfectly
// connects to the next line
endPoint = end;
} else {
auto rot = (t + 1) * triAngle;
// rotate the original point around the origin
endPoint.x = std::cos(rot) * point1.x - std::sin(rot) * point1.y;
endPoint.y = std::sin(rot) * point1.x + std::cos(rot) * point1.y;
// re-add the rotation origin to the target point
endPoint = Vec2Maths::add(endPoint, origin);
}
// emit the triangle
*vertices++ = startPoint;
*vertices++ = endPoint;
*vertices++ = connectTo;
startPoint = endPoint;
}
return vertices;
}
};
} // namespace crushedpixel

View File

@ -1,51 +0,0 @@
#include "Triangulation.h"
#include <map>
#include <array>
#include "earcut.hpp"
namespace MeshGenerator {
std::vector<uint32_t> triangulatePolygon(std::vector<Point> const &points, std::vector<int> &indices, std::vector<std::vector<int>> const &holeIndices) {
// The index type. Defaults to uint32_t, but you can also pass uint16_t if you know that your
// data won't have more than 65536 vertices.
using N = uint32_t;
// Create array
using EarPoint = std::array<float, 2>;
std::vector<std::vector<EarPoint>> polygon;
std::map<int, int> facePointMapping;
int nextFacePointIndex = 0;
std::vector<EarPoint> facePoints;
for (auto index : indices) {
facePointMapping[nextFacePointIndex] = index;
nextFacePointIndex++;
facePoints.push_back({ points[index].x, points[index].y });
}
polygon.push_back(std::move(facePoints));
for (const auto &list : holeIndices) {
std::vector<EarPoint> holePoints;
for (auto index : list) {
facePointMapping[nextFacePointIndex] = index;
nextFacePointIndex++;
holePoints.push_back({ points[index].x, points[index].y });
}
polygon.push_back(std::move(holePoints));
}
std::vector<N> triangleIndices = mapbox::earcut<N>(polygon);
std::vector<uint32_t> mappedIndices;
for (auto index : triangleIndices) {
mappedIndices.push_back(facePointMapping[index]);
}
return mappedIndices;
}
}

View File

@ -1,14 +0,0 @@
#ifndef Triangulation_h
#define Triangulation_h
#include <vector>
#include <LottieMesh/Point.h>
namespace MeshGenerator {
std::vector<uint32_t> triangulatePolygon(std::vector<Point> const &points, std::vector<int> &indices, std::vector<std::vector<int>> const &holeIndices);
}
#endif /* Triangulation_h */

View File

@ -1,99 +0,0 @@
#pragma once
#include <cmath>
namespace crushedpixel {
/**
* A two-dimensional float vector.
* It exposes the x and y fields
* as required by the Polyline2D functions.
*/
struct Vec2 {
Vec2() :
Vec2(0, 0) {}
Vec2(float x, float y) :
x(x), y(y) {}
virtual ~Vec2() = default;
float x, y;
};
namespace Vec2Maths {
template<typename Vec2>
static bool equal(const Vec2 &a, const Vec2 &b) {
return a.x == b.x && a.y == b.y;
}
template<typename Vec2>
static Vec2 multiply(const Vec2 &a, const Vec2 &b) {
return {a.x * b.x, a.y * b.y};
}
template<typename Vec2>
static Vec2 multiply(const Vec2 &vec, float factor) {
return {vec.x * factor, vec.y * factor};
}
template<typename Vec2>
static Vec2 divide(const Vec2 &vec, float factor) {
return {vec.x / factor, vec.y / factor};
}
template<typename Vec2>
static Vec2 add(const Vec2 &a, const Vec2 &b) {
return {a.x + b.x, a.y + b.y};
}
template<typename Vec2>
static Vec2 subtract(const Vec2 &a, const Vec2 &b) {
return {a.x - b.x, a.y - b.y};
}
template<typename Vec2>
static float magnitude(const Vec2 &vec) {
return std::sqrt(vec.x * vec.x + vec.y * vec.y);
}
template<typename Vec2>
static Vec2 withLength(const Vec2 &vec, float len) {
auto mag = magnitude(vec);
auto factor = mag / len;
return divide(vec, factor);
}
template<typename Vec2>
static Vec2 normalized(const Vec2 &vec) {
return withLength(vec, 1);
}
/**
* Calculates the dot product of two vectors.
*/
template<typename Vec2>
static float dot(const Vec2 &a, const Vec2 &b) {
return a.x * b.x + a.y * b.y;
}
/**
* Calculates the cross product of two vectors.
*/
template<typename Vec2>
static float cross(const Vec2 &a, const Vec2 &b) {
return a.x * b.y - a.y * b.x;
}
/**
* Calculates the angle between two vectors.
*/
template<typename Vec2>
static float angle(const Vec2 &a, const Vec2 &b) {
return std::acos(dot(a, b) / (magnitude(a) * magnitude(b)));
}
} // namespace Vec2Maths
}

View File

@ -1,823 +0,0 @@
#pragma once
#include <algorithm>
#include <cassert>
#include <cmath>
#include <cstddef>
#include <limits>
#include <memory>
#include <utility>
#include <vector>
namespace mapbox {
namespace util {
template <std::size_t I, typename T> struct nth {
inline static typename std::tuple_element<I, T>::type
get(const T& t) { return std::get<I>(t); };
};
}
namespace detail {
template <typename N = uint32_t>
class Earcut {
public:
std::vector<N> indices;
std::size_t vertices = 0;
template <typename Polygon>
void operator()(const Polygon& points);
private:
struct Node {
Node(N index, double x_, double y_) : i(index), x(x_), y(y_) {}
Node(const Node&) = delete;
Node& operator=(const Node&) = delete;
Node(Node&&) = delete;
Node& operator=(Node&&) = delete;
const N i;
const double x;
const double y;
// previous and next vertice nodes in a polygon ring
Node* prev = nullptr;
Node* next = nullptr;
// z-order curve value
int32_t z = 0;
// previous and next nodes in z-order
Node* prevZ = nullptr;
Node* nextZ = nullptr;
// indicates whether this is a steiner point
bool steiner = false;
};
template <typename Ring> Node* linkedList(const Ring& points, const bool clockwise);
Node* filterPoints(Node* start, Node* end = nullptr);
void earcutLinked(Node* ear, int pass = 0);
bool isEar(Node* ear);
bool isEarHashed(Node* ear);
Node* cureLocalIntersections(Node* start);
void splitEarcut(Node* start);
template <typename Polygon> Node* eliminateHoles(const Polygon& points, Node* outerNode);
Node* eliminateHole(Node* hole, Node* outerNode);
Node* findHoleBridge(Node* hole, Node* outerNode);
bool sectorContainsSector(const Node* m, const Node* p);
void indexCurve(Node* start);
Node* sortLinked(Node* list);
int32_t zOrder(const double x_, const double y_);
Node* getLeftmost(Node* start);
bool pointInTriangle(double ax, double ay, double bx, double by, double cx, double cy, double px, double py) const;
bool isValidDiagonal(Node* a, Node* b);
double area(const Node* p, const Node* q, const Node* r) const;
bool equals(const Node* p1, const Node* p2);
bool intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2);
bool onSegment(const Node* p, const Node* q, const Node* r);
int sign(double val);
bool intersectsPolygon(const Node* a, const Node* b);
bool locallyInside(const Node* a, const Node* b);
bool middleInside(const Node* a, const Node* b);
Node* splitPolygon(Node* a, Node* b);
template <typename Point> Node* insertNode(std::size_t i, const Point& p, Node* last);
void removeNode(Node* p);
bool hashing;
double minX, maxX;
double minY, maxY;
double inv_size = 0;
template <typename T, typename Alloc = std::allocator<T>>
class ObjectPool {
public:
ObjectPool() { }
ObjectPool(std::size_t blockSize_) {
reset(blockSize_);
}
~ObjectPool() {
clear();
}
template <typename... Args>
T* construct(Args&&... args) {
if (currentIndex >= blockSize) {
currentBlock = alloc_traits::allocate(alloc, blockSize);
allocations.emplace_back(currentBlock);
currentIndex = 0;
}
T* object = &currentBlock[currentIndex++];
alloc_traits::construct(alloc, object, std::forward<Args>(args)...);
return object;
}
void reset(std::size_t newBlockSize) {
for (auto allocation : allocations) {
alloc_traits::deallocate(alloc, allocation, blockSize);
}
allocations.clear();
blockSize = std::max<std::size_t>(1, newBlockSize);
currentBlock = nullptr;
currentIndex = blockSize;
}
void clear() { reset(blockSize); }
private:
T* currentBlock = nullptr;
std::size_t currentIndex = 1;
std::size_t blockSize = 1;
std::vector<T*> allocations;
Alloc alloc;
typedef typename std::allocator_traits<Alloc> alloc_traits;
};
ObjectPool<Node> nodes;
};
template <typename N> template <typename Polygon>
void Earcut<N>::operator()(const Polygon& points) {
// reset
indices.clear();
vertices = 0;
if (points.empty()) return;
double x;
double y;
int threshold = 80;
std::size_t len = 0;
for (size_t i = 0; threshold >= 0 && i < points.size(); i++) {
threshold -= static_cast<int>(points[i].size());
len += points[i].size();
}
//estimate size of nodes and indices
nodes.reset(len * 3 / 2);
indices.reserve(len + points[0].size());
Node* outerNode = linkedList(points[0], true);
if (!outerNode || outerNode->prev == outerNode->next) return;
if (points.size() > 1) outerNode = eliminateHoles(points, outerNode);
// if the shape is not too simple, we'll use z-order curve hash later; calculate polygon bbox
hashing = threshold < 0;
if (hashing) {
Node* p = outerNode->next;
minX = maxX = outerNode->x;
minY = maxY = outerNode->y;
do {
x = p->x;
y = p->y;
minX = std::min<double>(minX, x);
minY = std::min<double>(minY, y);
maxX = std::max<double>(maxX, x);
maxY = std::max<double>(maxY, y);
p = p->next;
} while (p != outerNode);
// minX, minY and size are later used to transform coords into integers for z-order calculation
inv_size = std::max<double>(maxX - minX, maxY - minY);
inv_size = inv_size != .0 ? (1. / inv_size) : .0;
}
earcutLinked(outerNode);
nodes.clear();
}
// create a circular doubly linked list from polygon points in the specified winding order
template <typename N> template <typename Ring>
typename Earcut<N>::Node*
Earcut<N>::linkedList(const Ring& points, const bool clockwise) {
using Point = typename Ring::value_type;
double sum = 0;
const std::size_t len = points.size();
std::size_t i, j;
Node* last = nullptr;
// calculate original winding order of a polygon ring
for (i = 0, j = len > 0 ? len - 1 : 0; i < len; j = i++) {
const auto& p1 = points[i];
const auto& p2 = points[j];
const double p20 = util::nth<0, Point>::get(p2);
const double p10 = util::nth<0, Point>::get(p1);
const double p11 = util::nth<1, Point>::get(p1);
const double p21 = util::nth<1, Point>::get(p2);
sum += (p20 - p10) * (p11 + p21);
}
// link points into circular doubly-linked list in the specified winding order
if (clockwise == (sum > 0)) {
for (i = 0; i < len; i++) last = insertNode(vertices + i, points[i], last);
} else {
for (i = len; i-- > 0;) last = insertNode(vertices + i, points[i], last);
}
if (last && equals(last, last->next)) {
removeNode(last);
last = last->next;
}
vertices += len;
return last;
}
// eliminate colinear or duplicate points
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::filterPoints(Node* start, Node* end) {
if (!end) end = start;
Node* p = start;
bool again;
do {
again = false;
if (!p->steiner && (equals(p, p->next) || area(p->prev, p, p->next) == 0)) {
removeNode(p);
p = end = p->prev;
if (p == p->next) break;
again = true;
} else {
p = p->next;
}
} while (again || p != end);
return end;
}
// main ear slicing loop which triangulates a polygon (given as a linked list)
template <typename N>
void Earcut<N>::earcutLinked(Node* ear, int pass) {
if (!ear) return;
// interlink polygon nodes in z-order
if (!pass && hashing) indexCurve(ear);
Node* stop = ear;
Node* prev;
Node* next;
int iterations = 0;
// iterate through ears, slicing them one by one
while (ear->prev != ear->next) {
iterations++;
prev = ear->prev;
next = ear->next;
if (hashing ? isEarHashed(ear) : isEar(ear)) {
// cut off the triangle
indices.emplace_back(prev->i);
indices.emplace_back(ear->i);
indices.emplace_back(next->i);
removeNode(ear);
// skipping the next vertice leads to less sliver triangles
ear = next->next;
stop = next->next;
continue;
}
ear = next;
// if we looped through the whole remaining polygon and can't find any more ears
if (ear == stop) {
// try filtering points and slicing again
if (!pass) earcutLinked(filterPoints(ear), 1);
// if this didn't work, try curing all small self-intersections locally
else if (pass == 1) {
ear = cureLocalIntersections(filterPoints(ear));
earcutLinked(ear, 2);
// as a last resort, try splitting the remaining polygon into two
} else if (pass == 2) splitEarcut(ear);
break;
}
}
}
// check whether a polygon node forms a valid ear with adjacent nodes
template <typename N>
bool Earcut<N>::isEar(Node* ear) {
const Node* a = ear->prev;
const Node* b = ear;
const Node* c = ear->next;
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// now make sure we don't have other points inside the potential ear
Node* p = ear->next->next;
while (p != ear->prev) {
if (pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) &&
area(p->prev, p, p->next) >= 0) return false;
p = p->next;
}
return true;
}
template <typename N>
bool Earcut<N>::isEarHashed(Node* ear) {
const Node* a = ear->prev;
const Node* b = ear;
const Node* c = ear->next;
if (area(a, b, c) >= 0) return false; // reflex, can't be an ear
// triangle bbox; min & max are calculated like this for speed
const double minTX = std::min<double>(a->x, std::min<double>(b->x, c->x));
const double minTY = std::min<double>(a->y, std::min<double>(b->y, c->y));
const double maxTX = std::max<double>(a->x, std::max<double>(b->x, c->x));
const double maxTY = std::max<double>(a->y, std::max<double>(b->y, c->y));
// z-order range for the current triangle bbox;
const int32_t minZ = zOrder(minTX, minTY);
const int32_t maxZ = zOrder(maxTX, maxTY);
// first look for points inside the triangle in increasing z-order
Node* p = ear->nextZ;
while (p && p->z <= maxZ) {
if (p != ear->prev && p != ear->next &&
pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) &&
area(p->prev, p, p->next) >= 0) return false;
p = p->nextZ;
}
// then look for points in decreasing z-order
p = ear->prevZ;
while (p && p->z >= minZ) {
if (p != ear->prev && p != ear->next &&
pointInTriangle(a->x, a->y, b->x, b->y, c->x, c->y, p->x, p->y) &&
area(p->prev, p, p->next) >= 0) return false;
p = p->prevZ;
}
return true;
}
// go through all polygon nodes and cure small local self-intersections
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::cureLocalIntersections(Node* start) {
Node* p = start;
do {
Node* a = p->prev;
Node* b = p->next->next;
// a self-intersection where edge (v[i-1],v[i]) intersects (v[i+1],v[i+2])
if (!equals(a, b) && intersects(a, p, p->next, b) && locallyInside(a, b) && locallyInside(b, a)) {
indices.emplace_back(a->i);
indices.emplace_back(p->i);
indices.emplace_back(b->i);
// remove two nodes involved
removeNode(p);
removeNode(p->next);
p = start = b;
}
p = p->next;
} while (p != start);
return filterPoints(p);
}
// try splitting polygon into two and triangulate them independently
template <typename N>
void Earcut<N>::splitEarcut(Node* start) {
// look for a valid diagonal that divides the polygon into two
Node* a = start;
do {
Node* b = a->next->next;
while (b != a->prev) {
if (a->i != b->i && isValidDiagonal(a, b)) {
// split the polygon in two by the diagonal
Node* c = splitPolygon(a, b);
// filter colinear points around the cuts
a = filterPoints(a, a->next);
c = filterPoints(c, c->next);
// run earcut on each half
earcutLinked(a);
earcutLinked(c);
return;
}
b = b->next;
}
a = a->next;
} while (a != start);
}
// link every hole into the outer loop, producing a single-ring polygon without holes
template <typename N> template <typename Polygon>
typename Earcut<N>::Node*
Earcut<N>::eliminateHoles(const Polygon& points, Node* outerNode) {
const size_t len = points.size();
std::vector<Node*> queue;
for (size_t i = 1; i < len; i++) {
Node* list = linkedList(points[i], false);
if (list) {
if (list == list->next) list->steiner = true;
queue.push_back(getLeftmost(list));
}
}
std::sort(queue.begin(), queue.end(), [](const Node* a, const Node* b) {
return a->x < b->x;
});
// process holes from left to right
for (size_t i = 0; i < queue.size(); i++) {
outerNode = eliminateHole(queue[i], outerNode);
outerNode = filterPoints(outerNode, outerNode->next);
}
return outerNode;
}
// find a bridge between vertices that connects hole with an outer ring and and link it
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::eliminateHole(Node* hole, Node* outerNode) {
Node* bridge = findHoleBridge(hole, outerNode);
if (!bridge) {
return outerNode;
}
Node* bridgeReverse = splitPolygon(bridge, hole);
// filter collinear points around the cuts
Node* filteredBridge = filterPoints(bridge, bridge->next);
filterPoints(bridgeReverse, bridgeReverse->next);
// Check if input node was removed by the filtering
return outerNode == bridge ? filteredBridge : outerNode;
}
// David Eberly's algorithm for finding a bridge between hole and outer polygon
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::findHoleBridge(Node* hole, Node* outerNode) {
Node* p = outerNode;
double hx = hole->x;
double hy = hole->y;
double qx = -std::numeric_limits<double>::infinity();
Node* m = nullptr;
// find a segment intersected by a ray from the hole's leftmost Vertex to the left;
// segment's endpoint with lesser x will be potential connection Vertex
do {
if (hy <= p->y && hy >= p->next->y && p->next->y != p->y) {
double x = p->x + (hy - p->y) * (p->next->x - p->x) / (p->next->y - p->y);
if (x <= hx && x > qx) {
qx = x;
if (x == hx) {
if (hy == p->y) return p;
if (hy == p->next->y) return p->next;
}
m = p->x < p->next->x ? p : p->next;
}
}
p = p->next;
} while (p != outerNode);
if (!m) return 0;
if (hx == qx) return m; // hole touches outer segment; pick leftmost endpoint
// look for points inside the triangle of hole Vertex, segment intersection and endpoint;
// if there are no points found, we have a valid connection;
// otherwise choose the Vertex of the minimum angle with the ray as connection Vertex
const Node* stop = m;
double tanMin = std::numeric_limits<double>::infinity();
double tanCur = 0;
p = m;
double mx = m->x;
double my = m->y;
do {
if (hx >= p->x && p->x >= mx && hx != p->x &&
pointInTriangle(hy < my ? hx : qx, hy, mx, my, hy < my ? qx : hx, hy, p->x, p->y)) {
tanCur = std::abs(hy - p->y) / (hx - p->x); // tangential
if (locallyInside(p, hole) &&
(tanCur < tanMin || (tanCur == tanMin && (p->x > m->x || sectorContainsSector(m, p))))) {
m = p;
tanMin = tanCur;
}
}
p = p->next;
} while (p != stop);
return m;
}
// whether sector in vertex m contains sector in vertex p in the same coordinates
template <typename N>
bool Earcut<N>::sectorContainsSector(const Node* m, const Node* p) {
return area(m->prev, m, p->prev) < 0 && area(p->next, m, m->next) < 0;
}
// interlink polygon nodes in z-order
template <typename N>
void Earcut<N>::indexCurve(Node* start) {
assert(start);
Node* p = start;
do {
p->z = p->z ? p->z : zOrder(p->x, p->y);
p->prevZ = p->prev;
p->nextZ = p->next;
p = p->next;
} while (p != start);
p->prevZ->nextZ = nullptr;
p->prevZ = nullptr;
sortLinked(p);
}
// Simon Tatham's linked list merge sort algorithm
// http://www.chiark.greenend.org.uk/~sgtatham/algorithms/listsort.html
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::sortLinked(Node* list) {
assert(list);
Node* p;
Node* q;
Node* e;
Node* tail;
int i, numMerges, pSize, qSize;
int inSize = 1;
for (;;) {
p = list;
list = nullptr;
tail = nullptr;
numMerges = 0;
while (p) {
numMerges++;
q = p;
pSize = 0;
for (i = 0; i < inSize; i++) {
pSize++;
q = q->nextZ;
if (!q) break;
}
qSize = inSize;
while (pSize > 0 || (qSize > 0 && q)) {
if (pSize == 0) {
e = q;
q = q->nextZ;
qSize--;
} else if (qSize == 0 || !q) {
e = p;
p = p->nextZ;
pSize--;
} else if (p->z <= q->z) {
e = p;
p = p->nextZ;
pSize--;
} else {
e = q;
q = q->nextZ;
qSize--;
}
if (tail) tail->nextZ = e;
else list = e;
e->prevZ = tail;
tail = e;
}
p = q;
}
tail->nextZ = nullptr;
if (numMerges <= 1) return list;
inSize *= 2;
}
}
// z-order of a Vertex given coords and size of the data bounding box
template <typename N>
int32_t Earcut<N>::zOrder(const double x_, const double y_) {
// coords are transformed into non-negative 15-bit integer range
int32_t x = static_cast<int32_t>(32767.0 * (x_ - minX) * inv_size);
int32_t y = static_cast<int32_t>(32767.0 * (y_ - minY) * inv_size);
x = (x | (x << 8)) & 0x00FF00FF;
x = (x | (x << 4)) & 0x0F0F0F0F;
x = (x | (x << 2)) & 0x33333333;
x = (x | (x << 1)) & 0x55555555;
y = (y | (y << 8)) & 0x00FF00FF;
y = (y | (y << 4)) & 0x0F0F0F0F;
y = (y | (y << 2)) & 0x33333333;
y = (y | (y << 1)) & 0x55555555;
return x | (y << 1);
}
// find the leftmost node of a polygon ring
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::getLeftmost(Node* start) {
Node* p = start;
Node* leftmost = start;
do {
if (p->x < leftmost->x || (p->x == leftmost->x && p->y < leftmost->y))
leftmost = p;
p = p->next;
} while (p != start);
return leftmost;
}
// check if a point lies within a convex triangle
template <typename N>
bool Earcut<N>::pointInTriangle(double ax, double ay, double bx, double by, double cx, double cy, double px, double py) const {
return (cx - px) * (ay - py) - (ax - px) * (cy - py) >= 0 &&
(ax - px) * (by - py) - (bx - px) * (ay - py) >= 0 &&
(bx - px) * (cy - py) - (cx - px) * (by - py) >= 0;
}
// check if a diagonal between two polygon nodes is valid (lies in polygon interior)
template <typename N>
bool Earcut<N>::isValidDiagonal(Node* a, Node* b) {
return a->next->i != b->i && a->prev->i != b->i && !intersectsPolygon(a, b) && // dones't intersect other edges
((locallyInside(a, b) && locallyInside(b, a) && middleInside(a, b) && // locally visible
(area(a->prev, a, b->prev) != 0.0 || area(a, b->prev, b) != 0.0)) || // does not create opposite-facing sectors
(equals(a, b) && area(a->prev, a, a->next) > 0 && area(b->prev, b, b->next) > 0)); // special zero-length case
}
// signed area of a triangle
template <typename N>
double Earcut<N>::area(const Node* p, const Node* q, const Node* r) const {
return (q->y - p->y) * (r->x - q->x) - (q->x - p->x) * (r->y - q->y);
}
// check if two points are equal
template <typename N>
bool Earcut<N>::equals(const Node* p1, const Node* p2) {
return p1->x == p2->x && p1->y == p2->y;
}
// check if two segments intersect
template <typename N>
bool Earcut<N>::intersects(const Node* p1, const Node* q1, const Node* p2, const Node* q2) {
int o1 = sign(area(p1, q1, p2));
int o2 = sign(area(p1, q1, q2));
int o3 = sign(area(p2, q2, p1));
int o4 = sign(area(p2, q2, q1));
if (o1 != o2 && o3 != o4) return true; // general case
if (o1 == 0 && onSegment(p1, p2, q1)) return true; // p1, q1 and p2 are collinear and p2 lies on p1q1
if (o2 == 0 && onSegment(p1, q2, q1)) return true; // p1, q1 and q2 are collinear and q2 lies on p1q1
if (o3 == 0 && onSegment(p2, p1, q2)) return true; // p2, q2 and p1 are collinear and p1 lies on p2q2
if (o4 == 0 && onSegment(p2, q1, q2)) return true; // p2, q2 and q1 are collinear and q1 lies on p2q2
return false;
}
// for collinear points p, q, r, check if point q lies on segment pr
template <typename N>
bool Earcut<N>::onSegment(const Node* p, const Node* q, const Node* r) {
return q->x <= std::max<double>(p->x, r->x) &&
q->x >= std::min<double>(p->x, r->x) &&
q->y <= std::max<double>(p->y, r->y) &&
q->y >= std::min<double>(p->y, r->y);
}
template <typename N>
int Earcut<N>::sign(double val) {
return (0.0 < val) - (val < 0.0);
}
// check if a polygon diagonal intersects any polygon segments
template <typename N>
bool Earcut<N>::intersectsPolygon(const Node* a, const Node* b) {
const Node* p = a;
do {
if (p->i != a->i && p->next->i != a->i && p->i != b->i && p->next->i != b->i &&
intersects(p, p->next, a, b)) return true;
p = p->next;
} while (p != a);
return false;
}
// check if a polygon diagonal is locally inside the polygon
template <typename N>
bool Earcut<N>::locallyInside(const Node* a, const Node* b) {
return area(a->prev, a, a->next) < 0 ?
area(a, b, a->next) >= 0 && area(a, a->prev, b) >= 0 :
area(a, b, a->prev) < 0 || area(a, a->next, b) < 0;
}
// check if the middle Vertex of a polygon diagonal is inside the polygon
template <typename N>
bool Earcut<N>::middleInside(const Node* a, const Node* b) {
const Node* p = a;
bool inside = false;
double px = (a->x + b->x) / 2;
double py = (a->y + b->y) / 2;
do {
if (((p->y > py) != (p->next->y > py)) && p->next->y != p->y &&
(px < (p->next->x - p->x) * (py - p->y) / (p->next->y - p->y) + p->x))
inside = !inside;
p = p->next;
} while (p != a);
return inside;
}
// link two polygon vertices with a bridge; if the vertices belong to the same ring, it splits
// polygon into two; if one belongs to the outer ring and another to a hole, it merges it into a
// single ring
template <typename N>
typename Earcut<N>::Node*
Earcut<N>::splitPolygon(Node* a, Node* b) {
Node* a2 = nodes.construct(a->i, a->x, a->y);
Node* b2 = nodes.construct(b->i, b->x, b->y);
Node* an = a->next;
Node* bp = b->prev;
a->next = b;
b->prev = a;
a2->next = an;
an->prev = a2;
b2->next = a2;
a2->prev = b2;
bp->next = b2;
b2->prev = bp;
return b2;
}
// create a node and util::optionally link it with previous one (in a circular doubly linked list)
template <typename N> template <typename Point>
typename Earcut<N>::Node*
Earcut<N>::insertNode(std::size_t i, const Point& pt, Node* last) {
Node* p = nodes.construct(static_cast<N>(i), util::nth<0, Point>::get(pt), util::nth<1, Point>::get(pt));
if (!last) {
p->prev = p;
p->next = p;
} else {
assert(last);
p->next = last->next;
p->prev = last;
last->next->prev = p;
last->next = p;
}
return p;
}
template <typename N>
void Earcut<N>::removeNode(Node* p) {
p->next->prev = p->prev;
p->prev->next = p->next;
if (p->prevZ) p->prevZ->nextZ = p->nextZ;
if (p->nextZ) p->nextZ->prevZ = p->prevZ;
}
}
template <typename N = uint32_t, typename Polygon>
std::vector<N> earcut(const Polygon& poly) {
mapbox::detail::Earcut<N> earcut;
earcut(poly);
return std::move(earcut.indices);
}
}

View File

@ -1,43 +0,0 @@
#ifndef LOTTIE_MESH_BINDING_H
#define LOTTIE_MESH_BINDING_H
#import <Foundation/Foundation.h>
#import <UIKit/UIKit.h>
typedef NS_CLOSED_ENUM(NSInteger, LottieMeshFillRule) {
LottieMeshFillRuleEvenOdd,
LottieMeshFillRuleNonZero
};
@interface LottieMeshFill : NSObject
@property (nonatomic, readonly) LottieMeshFillRule fillRule;
- (instancetype _Nonnull)initWithFillRule:(LottieMeshFillRule)fillRule;
@end
@interface LottieMeshStroke : NSObject
@property (nonatomic, readonly) CGFloat lineWidth;
@property (nonatomic, readonly) CGLineJoin lineJoin;
@property (nonatomic, readonly) CGLineCap lineCap;
@property (nonatomic, readonly) CGFloat miterLimit;
- (instancetype _Nonnull)initWithLineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit;
@end
@interface LottieMeshData : NSObject
- (NSInteger)vertexCount;
- (void)getVertexAt:(NSInteger)index x:(float * _Nullable)x y:(float * _Nullable)y;
- (NSInteger)triangleCount;
- (void * _Nonnull)getTriangles;
+ (LottieMeshData * _Nullable)generateWithPath:(UIBezierPath * _Nonnull)path fill:(LottieMeshFill * _Nullable)fill stroke:(LottieMeshStroke * _Nullable)stroke;
@end
#endif

View File

@ -1,315 +0,0 @@
#import <LottieMeshBinding/LottieMeshBinding.h>
#import <LottieMesh/LottieMesh.h>
namespace {
MeshGenerator::Point bezierQuadraticPointAt(MeshGenerator::Point const &p0, MeshGenerator::Point const &p1, MeshGenerator::Point const &p2, float t) {
float x = powf((1.0 - t), 2.0) * p0.x + 2.0 * (1.0 - t) * t * p1.x + powf(t, 2.0) * p2.x;
float y = powf((1.0 - t), 2.0) * p0.y + 2.0 * (1.0 - t) * t * p1.y + powf(t, 2.0) * p2.y;
return MeshGenerator::Point(x, y);
}
float approximateBezierQuadraticLength(MeshGenerator::Point const &p0, MeshGenerator::Point const &p1, MeshGenerator::Point const &p2) {
float length = 0.0f;
float t = 0.1;
MeshGenerator::Point last = p0;
while (t < 1.01) {
auto point = bezierQuadraticPointAt(p0, p1, p2, t);
length += last.distance(point);
last = point;
t += 0.1;
}
return length;
}
void tesselateBezier(MeshGenerator::Path &path, MeshGenerator::Point const &p1, MeshGenerator::Point const &p2, MeshGenerator::Point const &p3, MeshGenerator::Point const &p4, int level) {
const float tessTol = 0.25f / 0.1f;
float x1 = p1.x;
float y1 = p1.y;
float x2 = p2.x;
float y2 = p2.y;
float x3 = p3.x;
float y3 = p3.y;
float x4 = p4.x;
float y4 = p4.y;
float x12, y12, x23, y23, x34, y34, x123, y123, x234, y234, x1234, y1234;
float dx, dy, d2, d3;
if (level > 10) {
return;
}
x12 = (x1 + x2) * 0.5f;
y12 = (y1 + y2) * 0.5f;
x23 = (x2 + x3) * 0.5f;
y23 = (y2 + y3) * 0.5f;
x34 = (x3 + x4) * 0.5f;
y34 = (y3 + y4) * 0.5f;
x123 = (x12 + x23) * 0.5f;
y123 = (y12 + y23) * 0.5f;
dx = x4 - x1;
dy = y4 - y1;
d2 = std::abs(((x2 - x4) * dy - (y2 - y4) * dx));
d3 = std::abs(((x3 - x4) * dy - (y3 - y4) * dx));
if ((d2 + d3) * (d2 + d3) < tessTol * (dx * dx + dy * dy)) {
path.points.emplace_back(x4, y4);
return;
}
x234 = (x23+x34) * 0.5f;
y234 = (y23+y34) * 0.5f;
x1234 = (x123 + x234) * 0.5f;
y1234 = (y123 + y234) * 0.5f;
tesselateBezier(path, MeshGenerator::Point(x1, y1), MeshGenerator::Point(x12, y12), MeshGenerator::Point(x123, y123), MeshGenerator::Point(x1234, y1234), level + 1);
tesselateBezier(path, MeshGenerator::Point(x1234, y1234), MeshGenerator::Point(x234, y234), MeshGenerator::Point(x34, y34), MeshGenerator::Point(x4, y4), level + 1);
}
}
@interface LottieMeshData () {
std::unique_ptr<MeshGenerator::Mesh> _mesh;
}
- (instancetype _Nonnull)initWithMesh:(std::unique_ptr<MeshGenerator::Mesh> &&)mesh;
@end
@implementation LottieMeshData
- (instancetype _Nonnull)initWithMesh:(std::unique_ptr<MeshGenerator::Mesh> &&)mesh {
self = [super init];
if (self != nil) {
_mesh = std::move(mesh);
}
return self;
}
- (NSInteger)vertexCount {
return (NSInteger)_mesh->vertices.size();
}
- (void)getVertexAt:(NSInteger)index x:(float * _Nullable)x y:(float * _Nullable)y {
MeshGenerator::Point const &point = _mesh->vertices[index];
if (x) {
*x = point.x;
}
if (y) {
*y = point.y;
}
}
- (NSInteger)triangleCount {
return (NSInteger)(_mesh->triangles.size() / 3);
}
- (void * _Nonnull)getTriangles {
return _mesh->triangles.data();
}
/*- (void)getTriangleAt:(NSInteger)index v0:(NSInteger * _Nullable)v0 v1:(NSInteger * _Nullable)v1 v2:(NSInteger * _Nullable)v2 {
if (v0) {
*v0 = (NSInteger)_mesh->triangles[index * 3 + 0];
}
if (v1) {
*v1 = (NSInteger)_mesh->triangles[index * 3 + 1];
}
if (v2) {
*v2 = (NSInteger)_mesh->triangles[index * 3 + 2];
}
}*/
+ (LottieMeshData * _Nullable)generateWithPath:(UIBezierPath * _Nonnull)path fill: (LottieMeshFill * _Nullable)fill stroke:(LottieMeshStroke * _Nullable)stroke {
float scale = 1.0f;
float flatness = 1.0;
__block MeshGenerator::Point startingPoint(0.0f, 0.0f);
__block bool hasStartingPoint = false;
__block std::vector<MeshGenerator::Path> paths;
paths.push_back(MeshGenerator::Path());
CGPathApplyWithBlock(path.CGPath, ^(const CGPathElement * _Nonnull element) {
switch (element->type) {
case kCGPathElementMoveToPoint: {
if (!paths[paths.size() - 1].points.empty()) {
if (!paths[paths.size() - 1].points[0].isEqual(paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1])) {
paths[paths.size() - 1].points.push_back(paths[paths.size() - 1].points[0]);
}
paths.push_back(MeshGenerator::Path());
}
startingPoint = MeshGenerator::Point((float)(element->points[0].x) * scale, (float)(element->points[0].y) * scale);
hasStartingPoint = true;
break;
}
case kCGPathElementAddLineToPoint: {
bool canAddPoints = false;
if (paths[paths.size() - 1].points.empty()) {
if (hasStartingPoint) {
paths[paths.size() - 1].points.push_back(startingPoint);
canAddPoints = true;
}
} else {
canAddPoints = true;
}
if (canAddPoints) {
paths[paths.size() - 1].points.push_back(MeshGenerator::Point((float)(element->points[0].x) * scale, (float)(element->points[0].y) * scale));
}
break;
}
case kCGPathElementAddQuadCurveToPoint: {
bool canAddPoints = false;
if (paths[paths.size() - 1].points.empty()) {
if (hasStartingPoint) {
paths[paths.size() - 1].points.push_back(startingPoint);
canAddPoints = true;
}
} else {
canAddPoints = true;
}
if (canAddPoints) {
float t = 0.001f;
MeshGenerator::Point p0 = paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1];
MeshGenerator::Point p1(element->points[0].x * scale, element->points[0].y * scale);
MeshGenerator::Point p2(element->points[1].x * scale, element->points[1].y * scale);
float step = 10.0f * flatness / approximateBezierQuadraticLength(p0, p1, p2);
while (t < 1.0f) {
auto point = bezierQuadraticPointAt(p0, p1, p2, t);
paths[paths.size() - 1].points.push_back(point);
t += step;
}
paths[paths.size() - 1].points.push_back(p2);
}
break;
}
case kCGPathElementAddCurveToPoint: {
bool canAddPoints = false;
if (paths[paths.size() - 1].points.empty()) {
if (hasStartingPoint) {
paths[paths.size() - 1].points.push_back(startingPoint);
canAddPoints = true;
}
} else {
canAddPoints = true;
}
if (canAddPoints) {
float t = 0.001f;
MeshGenerator::Point p0 = paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1];
MeshGenerator::Point p1(element->points[0].x * scale, element->points[0].y * scale);
MeshGenerator::Point p2(element->points[1].x * scale, element->points[1].y * scale);
MeshGenerator::Point p3(element->points[2].x * scale, element->points[2].y * scale);
tesselateBezier(paths[paths.size() - 1], p0, p1, p2, p3, 0);
}
break;
}
case kCGPathElementCloseSubpath: {
if (!paths[paths.size() - 1].points.empty()) {
if (!paths[paths.size() - 1].points[0].isEqual(paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1])) {
paths[paths.size() - 1].points.push_back(paths[paths.size() - 1].points[0]);
}
hasStartingPoint = true;
startingPoint = paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1];
paths.push_back(MeshGenerator::Path());
}
}
default: {
break;
}
}
});
if (!paths[paths.size() - 1].points.empty()) {
if (stroke == nil && !paths[paths.size() - 1].points[0].isEqual(paths[paths.size() - 1].points[paths[paths.size() - 1].points.size() - 1])) {
paths[paths.size() - 1].points.push_back(paths[paths.size() - 1].points[0]);
}
} else {
paths.pop_back();
}
std::unique_ptr<MeshGenerator::Fill> mappedFill;
if (fill) {
mappedFill = std::make_unique<MeshGenerator::Fill>(fill.fillRule == LottieMeshFillRuleEvenOdd ? MeshGenerator::Fill::Rule::EvenOdd : MeshGenerator::Fill::Rule::NonZero);
}
std::unique_ptr<MeshGenerator::Stroke> mappedStroke;
if (stroke) {
MeshGenerator::Stroke::LineJoin lineJoin;
switch (stroke.lineJoin) {
case kCGLineJoinRound:
lineJoin = MeshGenerator::Stroke::LineJoin::Round;
break;
case kCGLineJoinBevel:
lineJoin = MeshGenerator::Stroke::LineJoin::Bevel;
break;
case kCGLineJoinMiter:
lineJoin = MeshGenerator::Stroke::LineJoin::Miter;
break;
default:
lineJoin = MeshGenerator::Stroke::LineJoin::Round;
break;
}
MeshGenerator::Stroke::LineCap lineCap;
switch (stroke.lineCap) {
case kCGLineCapRound:
lineCap = MeshGenerator::Stroke::LineCap::Round;
break;
case kCGLineCapButt:
lineCap = MeshGenerator::Stroke::LineCap::Butt;
break;
case kCGLineCapSquare:
lineCap = MeshGenerator::Stroke::LineCap::Square;
break;
default:
lineCap = MeshGenerator::Stroke::LineCap::Round;
break;
}
mappedStroke = std::make_unique<MeshGenerator::Stroke>((float)stroke.lineWidth, lineJoin, lineCap, (float)stroke.miterLimit);
}
std::unique_ptr<MeshGenerator::Mesh> resultMesh = MeshGenerator::generateMesh(paths, std::move(mappedFill), std::move(mappedStroke));
if (resultMesh) {
return [[LottieMeshData alloc] initWithMesh:std::move(resultMesh)];
} else {
return nil;
}
}
@end
@implementation LottieMeshFill
- (instancetype _Nonnull)initWithFillRule:(LottieMeshFillRule)fillRule {
self = [super init];
if (self != nil) {
_fillRule = fillRule;
}
return self;
}
@end
@implementation LottieMeshStroke
- (instancetype _Nonnull)initWithLineWidth:(CGFloat)lineWidth lineJoin:(CGLineJoin)lineJoin lineCap:(CGLineCap)lineCap miterLimit:(CGFloat)miterLimit {
self = [super init];
if (self != nil) {
_lineWidth = lineWidth;
_lineJoin = lineJoin;
_lineCap = lineCap;
_miterLimit = miterLimit;
}
return self;
}
@end

View File

@ -1,82 +0,0 @@
#include <metal_stdlib>
using namespace metal;
typedef struct {
packed_float2 position;
} Vertex;
typedef struct {
packed_float2 offset;
} Offset;
typedef struct {
float4 position[[position]];
float2 localPosition[[center_no_perspective]];
} Varyings;
float2 screenSpaceToRelative(float2 point, float2 viewSize) {
float2 inverseViewSize = 1 / viewSize;
float clipX = (2.0f * point.x * inverseViewSize.x) - 2.0f;
float clipY = (2.0f * -point.y * inverseViewSize.y) + 2.0f;
return float2(clipX, clipY);
}
vertex Varyings vertexPassthrough(
constant Vertex *verticies[[buffer(0)]],
constant float2 &offset[[buffer(1)]],
unsigned int vid[[vertex_id]],
constant float4x4 &transformMatrix[[buffer(2)]],
constant int &indexOffset[[buffer(3)]]
) {
Varyings out;
constant Vertex &v = verticies[vid + indexOffset];
float2 viewSize(512.0f, 512.0f);
float4 transformedVertex = transformMatrix * float4(v.position, 0.0, 1.0);
out.position = float4(screenSpaceToRelative(float2(transformedVertex.x, transformedVertex.y) + offset, viewSize), 0.0, 1.0);
out.localPosition = float2(v.position);
return out;
}
fragment half4 fragmentPassthrough(
Varyings in[[stage_in]],
constant float4 &color[[buffer(1)]]
) {
float4 out = color;
return half4(out);
}
template<int N>
half4 mixGradientColors(float dist, constant float4 *colors, constant float *steps) {
float4 color = colors[0];
for (int i = 1; i < N; i++) {
color = mix(color, colors[i], smoothstep(steps[i - 1], steps[i], dist));
}
return half4(color);
}
#define radialGradientFunc(N) fragment half4 fragmentRadialGradient##N( \
Varyings in[[stage_in]], \
constant float2 &start[[buffer(1)]], \
constant float2 &end[[buffer(2)]], \
constant float4 *colors[[buffer(3)]], \
constant float *steps[[buffer(4)]] \
) { \
float centerDistance = distance(in.localPosition, start); \
float endDistance = distance(start, end); \
float dist = min(1.0, centerDistance / endDistance); \
return mixGradientColors<N>(dist, colors, steps); \
}
radialGradientFunc(2)
radialGradientFunc(3)
radialGradientFunc(4)
radialGradientFunc(5)
radialGradientFunc(6)
radialGradientFunc(7)
radialGradientFunc(8)
radialGradientFunc(9)
radialGradientFunc(10)

View File

@ -1,122 +0,0 @@
import Foundation
import Postbox
import ManagedFile
private let emptyMemory = malloc(1)!
public class MeshMemoryBuffer {
public internal(set) var data: Data
public internal(set) var length: Int
public init(data: Data) {
self.data = data
self.length = data.count
}
public func makeData() -> Data {
if self.data.count == self.length {
return self.data
} else {
return self.data.subdata(in: 0 ..< self.length)
}
}
}
extension WriteBuffer {
func writeInt32(_ value: Int32) {
var value = value
self.write(&value, length: 4)
}
func writeFloat(_ value: Float) {
var value: Float32 = value
self.write(&value, length: 4)
}
}
public final class MeshWriteBuffer {
let file: ManagedFile
private(set) var offset: Int = 0
public init(file: ManagedFile) {
self.file = file
}
public func write(_ data: UnsafeRawPointer, length: Int) {
let _ = self.file.write(data, count: length)
self.offset += length
}
public func writeInt8(_ value: Int8) {
var value = value
self.write(&value, length: 1)
}
public func writeInt32(_ value: Int32) {
var value = value
self.write(&value, length: 4)
}
public func writeFloat(_ value: Float) {
var value: Float32 = value
self.write(&value, length: 4)
}
public func write(_ data: Data) {
data.withUnsafeBytes { bytes in
self.write(bytes.baseAddress!, length: bytes.count)
}
}
func write(_ data: DataRange) {
data.data.withUnsafeBytes { bytes in
self.write(bytes.baseAddress!.advanced(by: data.range.lowerBound), length: data.count)
}
}
public func seek(offset: Int) {
let _ = self.file.seek(position: Int64(offset))
self.offset = offset
}
}
public final class MeshReadBuffer: MeshMemoryBuffer {
public var offset = 0
override public init(data: Data) {
super.init(data: data)
}
public func read(_ data: UnsafeMutableRawPointer, length: Int) {
self.data.copyBytes(to: data.assumingMemoryBound(to: UInt8.self), from: self.offset ..< (self.offset + length))
self.offset += length
}
func readDataRange(count: Int) -> DataRange {
let result = DataRange(data: self.data, range: self.offset ..< (self.offset + count))
self.offset += count
return result
}
public func readInt8() -> Int8 {
var result: Int8 = 0
self.read(&result, length: 1)
return result
}
public func readInt32() -> Int32 {
var result: Int32 = 0
self.read(&result, length: 4)
return result
}
public func readFloat() -> Float {
var result: Float32 = 0
self.read(&result, length: 4)
return result
}
public func skip(_ length: Int) {
self.offset += length
}
}

View File

@ -1,62 +0,0 @@
import Foundation
import UIKit
final class CapturedGeometryNode {
final class DisplayItem {
enum Display {
enum Style {
enum GradientType {
case linear
case radial
}
case color(color: UIColor, alpha: CGFloat)
case gradient(colors: [UIColor], positions: [CGFloat], start: CGPoint, end: CGPoint, type: GradientType)
}
struct Fill {
var style: Style
var fillRule: CGPathFillRule
}
struct Stroke {
var style: Style
var lineWidth: CGFloat
var lineCap: CGLineCap
var lineJoin: CGLineJoin
var miterLimit: CGFloat
}
case fill(Fill)
case stroke(Stroke)
}
let path: CGPath
let display: Display
init(path: CGPath, display: Display) {
self.path = path
self.display = display
}
}
var transform: CATransform3D
let alpha: CGFloat
let isHidden: Bool
let displayItem: DisplayItem?
let subnodes: [CapturedGeometryNode]
init(
transform: CATransform3D,
alpha: CGFloat,
isHidden: Bool,
displayItem: DisplayItem?,
subnodes: [CapturedGeometryNode]
) {
self.transform = transform
self.alpha = alpha
self.isHidden = isHidden
self.displayItem = displayItem
self.subnodes = subnodes
}
}

View File

@ -1,111 +0,0 @@
import Foundation
import QuartzCore
/**
The base class for a child layer of CompositionContainer
*/
class MyCompositionLayer {
var bounds: CGRect = CGRect()
let transformNode: LayerTransformNode
//let contentsLayer: CALayer = CALayer()
let maskLayer: MyMaskContainerLayer?
let matteType: MatteType?
var matteLayer: MyCompositionLayer? {
didSet {
//NOTE
/*if let matte = matteLayer {
if let type = matteType, type == .invert {
mask = InvertedMatteLayer(inputMatte: matte)
} else {
mask = matte
}
} else {
mask = nil
}*/
}
}
let inFrame: CGFloat
let outFrame: CGFloat
let startFrame: CGFloat
let timeStretch: CGFloat
init(layer: LayerModel, size: CGSize) {
self.transformNode = LayerTransformNode(transform: layer.transform)
if let masks = layer.masks {
maskLayer = MyMaskContainerLayer(masks: masks)
} else {
maskLayer = nil
}
self.matteType = layer.matte
self.inFrame = layer.inFrame.cgFloat
self.outFrame = layer.outFrame.cgFloat
self.timeStretch = layer.timeStretch.cgFloat
self.startFrame = layer.startTime.cgFloat
//NOTE
//self.anchorPoint = .zero
//NOTE
/*contentsLayer.anchorPoint = .zero
contentsLayer.bounds = CGRect(origin: .zero, size: size)
contentsLayer.actions = [
"opacity" : NSNull(),
"transform" : NSNull(),
"bounds" : NSNull(),
"anchorPoint" : NSNull(),
"sublayerTransform" : NSNull(),
"hidden" : NSNull()
]
addSublayer(contentsLayer)
if let maskLayer = maskLayer {
contentsLayer.mask = maskLayer
}*/
}
private(set) var isHidden = false
final func displayWithFrame(frame: CGFloat, forceUpdates: Bool) {
transformNode.updateTree(frame, forceUpdates: forceUpdates)
let layerVisible = frame.isInRangeOrEqual(inFrame, outFrame)
/// Only update contents if current time is within the layers time bounds.
if layerVisible {
displayContentsWithFrame(frame: frame, forceUpdates: forceUpdates)
maskLayer?.updateWithFrame(frame: frame, forceUpdates: forceUpdates)
}
self.isHidden = !layerVisible
//NOTE
/*contentsLayer.transform = transformNode.globalTransform
contentsLayer.opacity = transformNode.opacity
contentsLayer.isHidden = !layerVisible*/
}
func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
/// To be overridden by subclass
}
func captureGeometry() -> CapturedGeometryNode {
return CapturedGeometryNode(
transform: self.transformNode.globalTransform,
alpha: CGFloat(self.transformNode.opacity),
isHidden: self.isHidden,
displayItem: self.captureDisplayItem(),
subnodes: self.captureChildren()
)
}
func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
preconditionFailure()
}
func captureChildren() -> [CapturedGeometryNode] {
preconditionFailure()
}
}

View File

@ -1,36 +0,0 @@
import Foundation
import CoreGraphics
import QuartzCore
final class MyImageCompositionLayer: MyCompositionLayer {
var image: CGImage? = nil {
didSet {
//NOTE
/*if let image = image {
contentsLayer.contents = image
} else {
contentsLayer.contents = nil
}*/
}
}
let imageReferenceID: String
init(imageLayer: ImageLayerModel, size: CGSize) {
self.imageReferenceID = imageLayer.referenceID
super.init(layer: imageLayer, size: size)
//NOTE
//contentsLayer.masksToBounds = true
//contentsLayer.contentsGravity = CALayerContentsGravity.resize
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
preconditionFailure()
}
override func captureChildren() -> [CapturedGeometryNode] {
return []
}
}

View File

@ -1,150 +0,0 @@
import Foundation
import QuartzCore
extension MaskMode {
var usableMode: MaskMode {
switch self {
case .add:
return .add
case .subtract:
return .subtract
case .intersect:
return .intersect
case .lighten:
return .add
case .darken:
return .darken
case .difference:
return .intersect
case .none:
return .none
}
}
}
extension CGRect {
static var veryLargeRect: CGRect {
return CGRect(x: -100_000_000,
y: -100_000_000,
width: 200_000_000,
height: 200_000_000)
}
}
final class MyMaskContainerLayer {
init(masks: [Mask]) {
//NOTE
//anchorPoint = .zero
var containerLayer = CALayer()
var firstObject: Bool = true
for mask in masks {
let maskLayer = MaskLayer(mask: mask)
maskLayers.append(maskLayer)
if mask.mode.usableMode == .none {
continue
} else if mask.mode.usableMode == .add || firstObject {
firstObject = false
containerLayer.addSublayer(maskLayer)
} else {
containerLayer.mask = maskLayer
let newContainer = CALayer()
newContainer.addSublayer(containerLayer)
containerLayer = newContainer
}
}
//NOTE
//addSublayer(containerLayer)
}
fileprivate var maskLayers: [MaskLayer] = []
func updateWithFrame(frame: CGFloat, forceUpdates: Bool) {
maskLayers.forEach({ $0.updateWithFrame(frame: frame, forceUpdates: forceUpdates) })
}
}
fileprivate class MaskLayer: CALayer {
let properties: MaskNodeProperties?
let maskLayer = CAShapeLayer()
init(mask: Mask) {
self.properties = MaskNodeProperties(mask: mask)
super.init()
addSublayer(maskLayer)
anchorPoint = .zero
maskLayer.fillColor = mask.mode == .add ? CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [1, 0, 0, 1]) :
CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 1, 0, 1])
maskLayer.fillRule = CAShapeLayerFillRule.evenOdd
self.actions = [
"opacity" : NSNull()
]
}
override init(layer: Any) {
self.properties = nil
super.init(layer: layer)
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
func updateWithFrame(frame: CGFloat, forceUpdates: Bool) {
guard let properties = properties else { return }
if properties.opacity.needsUpdate(frame: frame) || forceUpdates {
properties.opacity.update(frame: frame)
self.opacity = Float(properties.opacity.value.cgFloatValue)
}
if properties.shape.needsUpdate(frame: frame) || forceUpdates {
properties.shape.update(frame: frame)
properties.expansion.update(frame: frame)
let shapePath = properties.shape.value.cgPath()
var path = shapePath
if properties.mode.usableMode == .subtract && !properties.inverted ||
(properties.mode.usableMode == .add && properties.inverted) {
/// Add a bounds rect to invert the mask
let newPath = CGMutablePath()
newPath.addRect(CGRect.veryLargeRect)
newPath.addPath(shapePath)
path = newPath
}
maskLayer.path = path
}
}
}
fileprivate class MaskNodeProperties: NodePropertyMap {
var propertyMap: [String : AnyNodeProperty]
var properties: [AnyNodeProperty]
init(mask: Mask) {
self.mode = mask.mode
self.inverted = mask.inverted
self.opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.opacity.keyframes))
self.shape = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.shape.keyframes))
self.expansion = NodeProperty(provider: KeyframeInterpolator(keyframes: mask.expansion.keyframes))
self.propertyMap = [
"Opacity" : opacity,
"Shape" : shape,
"Expansion" : expansion
]
self.properties = Array(self.propertyMap.values)
}
let mode: MaskMode
let inverted: Bool
let opacity: NodeProperty<Vector1D>
let shape: NodeProperty<BezierPath>
let expansion: NodeProperty<Vector1D>
}

View File

@ -1,15 +0,0 @@
import Foundation
final class MyNullCompositionLayer: MyCompositionLayer {
init(layer: LayerModel) {
super.init(layer: layer, size: .zero)
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
return nil
}
override func captureChildren() -> [CapturedGeometryNode] {
return []
}
}

View File

@ -1,84 +0,0 @@
import Foundation
import QuartzCore
final class MyPreCompositionLayer: MyCompositionLayer {
let frameRate: CGFloat
let remappingNode: NodeProperty<Vector1D>?
fileprivate var animationLayers: [MyCompositionLayer]
init(precomp: PreCompLayerModel,
asset: PrecompAsset,
assetLibrary: AssetLibrary?,
frameRate: CGFloat) {
self.animationLayers = []
if let keyframes = precomp.timeRemapping?.keyframes {
self.remappingNode = NodeProperty(provider: KeyframeInterpolator(keyframes: keyframes))
} else {
self.remappingNode = nil
}
self.frameRate = frameRate
super.init(layer: precomp, size: CGSize(width: precomp.width, height: precomp.height))
bounds = CGRect(origin: .zero, size: CGSize(width: precomp.width, height: precomp.height))
//NOTE
//contentsLayer.masksToBounds = true
//contentsLayer.bounds = bounds
let layers = initializeCompositionLayers(layers: asset.layers, assetLibrary: assetLibrary, frameRate: frameRate)
var imageLayers = [MyImageCompositionLayer]()
var mattedLayer: MyCompositionLayer? = nil
for layer in layers.reversed() {
layer.bounds = bounds
//NOTE
animationLayers.append(layer)
if let imageLayer = layer as? MyImageCompositionLayer {
imageLayers.append(imageLayer)
}
if let matte = mattedLayer {
/// The previous layer requires this layer to be its matte
matte.matteLayer = layer
mattedLayer = nil
continue
}
if let matte = layer.matteType,
(matte == .add || matte == .invert) {
/// We have a layer that requires a matte.
mattedLayer = layer
}
//NOTE
//contentsLayer.addSublayer(layer)
}
//NOTE
//layerImageProvider.addImageLayers(imageLayers)
}
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
let localFrame: CGFloat
if let remappingNode = remappingNode {
remappingNode.update(frame: frame)
localFrame = remappingNode.value.cgFloatValue * frameRate
} else {
localFrame = (frame - startFrame) / timeStretch
}
for animationLayer in self.animationLayers {
animationLayer.displayWithFrame(frame: localFrame, forceUpdates: forceUpdates)
}
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
return nil
}
override func captureChildren() -> [CapturedGeometryNode] {
var result: [CapturedGeometryNode] = []
for animationLayer in self.animationLayers {
result.append(animationLayer.captureGeometry())
}
return result
}
}

View File

@ -1,54 +0,0 @@
import Foundation
import CoreGraphics
/**
A CompositionLayer responsible for initializing and rendering shapes
*/
final class MyShapeCompositionLayer: MyCompositionLayer {
let rootNode: AnimatorNode?
let renderContainer: ShapeContainerLayer?
init(shapeLayer: ShapeLayerModel) {
let results = shapeLayer.items.initializeNodeTree()
let renderContainer = ShapeContainerLayer()
self.renderContainer = renderContainer
self.rootNode = results.rootNode
super.init(layer: shapeLayer, size: .zero)
//NOTE
//contentsLayer.addSublayer(renderContainer)
for container in results.renderContainers {
renderContainer.insertRenderLayer(container)
}
rootNode?.updateTree(0, forceUpdates: true)
}
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
rootNode?.updateTree(frame, forceUpdates: forceUpdates)
renderContainer?.markRenderUpdates(forFrame: frame)
}
override func captureGeometry() -> CapturedGeometryNode {
var subnodes: [CapturedGeometryNode] = []
if let renderContainer = self.renderContainer {
subnodes.append(renderContainer.captureGeometry())
}
return CapturedGeometryNode(
transform: self.transformNode.globalTransform,
alpha: CGFloat(self.transformNode.opacity),
isHidden: self.isHidden,
displayItem: nil,
subnodes: subnodes
)
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
preconditionFailure()
}
override func captureChildren() -> [CapturedGeometryNode] {
preconditionFailure()
}
}

View File

@ -1,41 +0,0 @@
import Foundation
import UIKit
final class MySolidCompositionLayer: MyCompositionLayer {
let colorProperty: NodeProperty<Color>?
let solidShape: CAShapeLayer = CAShapeLayer()
init(solid: SolidLayerModel) {
let components = solid.colorHex.hexColorComponents()
self.colorProperty = NodeProperty(provider: SingleValueProvider(Color(r: Double(components.red), g: Double(components.green), b: Double(components.blue), a: 1)))
super.init(layer: solid, size: .zero)
solidShape.path = CGPath(rect: CGRect(x: 0, y: 0, width: solid.width, height: solid.height), transform: nil)
//NOTE
//contentsLayer.addSublayer(solidShape)
}
override func displayContentsWithFrame(frame: CGFloat, forceUpdates: Bool) {
guard let colorProperty = colorProperty else { return }
colorProperty.update(frame: frame)
solidShape.fillColor = colorProperty.value.cgColorValue
}
override func captureDisplayItem() -> CapturedGeometryNode.DisplayItem? {
guard let colorProperty = colorProperty else { return nil }
guard let path = self.solidShape.path else {
return nil
}
return CapturedGeometryNode.DisplayItem(
path: path,
display: .fill(CapturedGeometryNode.DisplayItem.Display.Fill(
style: .color(color: UIColor(cgColor: colorProperty.value.cgColorValue), alpha: 1.0),
fillRule: .evenOdd
))
)
}
override func captureChildren() -> [CapturedGeometryNode] {
return []
}
}

View File

@ -1,58 +0,0 @@
//
// InvertedMatteLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/28/19.
//
import Foundation
import QuartzCore
/**
A layer that inverses the alpha output of its input layer.
WARNING: This is experimental and probably not very performant.
*/
/*final class InvertedMatteLayer: CALayer, CompositionLayerDelegate {
let inputMatte: CompositionLayer?
let wrapperLayer = CALayer()
init(inputMatte: CompositionLayer) {
self.inputMatte = inputMatte
super.init()
inputMatte.layerDelegate = self
self.anchorPoint = .zero
self.bounds = inputMatte.bounds
self.setNeedsDisplay()
}
override init(layer: Any) {
guard let layer = layer as? InvertedMatteLayer else {
fatalError("init(layer:) wrong class.")
}
self.inputMatte = nil
super.init(layer: layer)
}
func frameUpdated(frame: CGFloat) {
self.setNeedsDisplay()
self.displayIfNeeded()
}
required init?(coder aDecoder: NSCoder) {
fatalError("init(coder:) has not been implemented")
}
override func draw(in ctx: CGContext) {
guard let inputMatte = inputMatte else { return }
guard let fillColor = CGColor(colorSpace: CGColorSpaceCreateDeviceRGB(), components: [0, 0, 0, 1])
else { return }
ctx.setFillColor(fillColor)
ctx.fill(bounds)
ctx.setBlendMode(.destinationOut)
inputMatte.render(in: ctx)
}
}
*/

View File

@ -1,120 +0,0 @@
//
// LayerTransformPropertyMap.swift
// lottie-swift
//
// Created by Brandon Withrow on 2/4/19.
//
import Foundation
import CoreGraphics
import QuartzCore
final class LayerTransformProperties: NodePropertyMap {
init(transform: Transform) {
self.anchor = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.anchorPoint.keyframes))
self.scale = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.scale.keyframes))
self.rotation = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.rotation.keyframes))
self.opacity = NodeProperty(provider: KeyframeInterpolator(keyframes: transform.opacity.keyframes))
var propertyMap: [String: AnyNodeProperty] = [
"Anchor Point" : anchor,
"Scale" : scale,
"Rotation" : rotation,
"Opacity" : opacity
]
if let positionKeyframesX = transform.positionX?.keyframes,
let positionKeyframesY = transform.positionY?.keyframes {
let xPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesX))
let yPosition: NodeProperty<Vector1D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframesY))
propertyMap["X Position"] = xPosition
propertyMap["Y Position"] = yPosition
self.positionX = xPosition
self.positionY = yPosition
self.position = nil
} else if let positionKeyframes = transform.position?.keyframes {
let position: NodeProperty<Vector3D> = NodeProperty(provider: KeyframeInterpolator(keyframes: positionKeyframes))
propertyMap["Position"] = position
self.position = position
self.positionX = nil
self.positionY = nil
} else {
self.position = nil
self.positionY = nil
self.positionX = nil
}
self.properties = Array(propertyMap.values)
}
let properties: [AnyNodeProperty]
let anchor: NodeProperty<Vector3D>
let scale: NodeProperty<Vector3D>
let rotation: NodeProperty<Vector1D>
let position: NodeProperty<Vector3D>?
let positionX: NodeProperty<Vector1D>?
let positionY: NodeProperty<Vector1D>?
let opacity: NodeProperty<Vector1D>
}
class LayerTransformNode: AnimatorNode {
let outputNode: NodeOutput = PassThroughOutputNode(parent: nil)
init(transform: Transform) {
self.transformProperties = LayerTransformProperties(transform: transform)
}
let transformProperties: LayerTransformProperties
// MARK: Animator Node Protocol
var propertyMap: NodePropertyMap {
return transformProperties
}
var parentNode: AnimatorNode?
var hasLocalUpdates: Bool = false
var hasUpstreamUpdates: Bool = false
var lastUpdateFrame: CGFloat? = nil
var isEnabled: Bool = true
func shouldRebuildOutputs(frame: CGFloat) -> Bool {
return hasLocalUpdates || hasUpstreamUpdates
}
func rebuildOutputs(frame: CGFloat) {
opacity = Float(transformProperties.opacity.value.cgFloatValue) * 0.01
let position: CGPoint
if let point = transformProperties.position?.value.pointValue {
position = point
} else if let xPos = transformProperties.positionX?.value.cgFloatValue,
let yPos = transformProperties.positionY?.value.cgFloatValue {
position = CGPoint(x: xPos, y: yPos)
} else {
position = .zero
}
localTransform = CATransform3D.makeTransform(anchor: transformProperties.anchor.value.pointValue,
position: position,
scale: transformProperties.scale.value.sizeValue,
rotation: transformProperties.rotation.value.cgFloatValue,
skew: nil,
skewAxis: nil)
if let parentNode = parentNode as? LayerTransformNode {
globalTransform = CATransform3DConcat(localTransform, parentNode.globalTransform)
} else {
globalTransform = localTransform
}
}
var opacity: Float = 1
var localTransform: CATransform3D = CATransform3DIdentity
var globalTransform: CATransform3D = CATransform3DIdentity
}

View File

@ -1,107 +0,0 @@
//
// Animation.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/7/19.
//
import Foundation
public enum CoordinateSpace: Int, Codable {
case type2d
case type3d
}
/**
The `Animation` model is the top level model object in Lottie.
An `Animation` holds all of the animation data backing a Lottie Animation.
Codable, see JSON schema [here](https://github.com/airbnb/lottie-web/tree/master/docs/json).
*/
public final class Animation: Codable {
/// The version of the JSON Schema.
let version: String
/// The coordinate space of the composition.
let type: CoordinateSpace
/// The start time of the composition in frameTime.
public let startFrame: AnimationFrameTime
/// The end time of the composition in frameTime.
public let endFrame: AnimationFrameTime
/// The frame rate of the composition.
public let framerate: Double
/// The height of the composition in points.
let width: Int
/// The width of the composition in points.
let height: Int
/// The list of animation layers
let layers: [LayerModel]
/// The list of glyphs used for text rendering
let glyphs: [Glyph]?
/// The list of fonts used for text rendering
let fonts: FontList?
/// Asset Library
let assetLibrary: AssetLibrary?
/// Markers
let markers: [Marker]?
let markerMap: [String : Marker]?
/// Return all marker names, in order, or an empty list if none are specified
public var markerNames: [String] {
guard let markers = markers else { return [] }
return markers.map { $0.name }
}
enum CodingKeys : String, CodingKey {
case version = "v"
case type = "ddd"
case startFrame = "ip"
case endFrame = "op"
case framerate = "fr"
case width = "w"
case height = "h"
case layers = "layers"
case glyphs = "chars"
case fonts = "fonts"
case assetLibrary = "assets"
case markers = "markers"
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Animation.CodingKeys.self)
self.version = try container.decode(String.self, forKey: .version)
self.type = try container.decodeIfPresent(CoordinateSpace.self, forKey: .type) ?? .type2d
self.startFrame = try container.decode(AnimationFrameTime.self, forKey: .startFrame)
self.endFrame = try container.decode(AnimationFrameTime.self, forKey: .endFrame)
self.framerate = try container.decode(Double.self, forKey: .framerate)
self.width = try container.decode(Int.self, forKey: .width)
self.height = try container.decode(Int.self, forKey: .height)
self.layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers)
self.glyphs = try container.decodeIfPresent([Glyph].self, forKey: .glyphs)
self.fonts = try container.decodeIfPresent(FontList.self, forKey: .fonts)
self.assetLibrary = try container.decodeIfPresent(AssetLibrary.self, forKey: .assetLibrary)
self.markers = try container.decodeIfPresent([Marker].self, forKey: .markers)
if let markers = markers {
var markerMap: [String : Marker] = [:]
for marker in markers {
markerMap[marker.name] = marker
}
self.markerMap = markerMap
} else {
self.markerMap = nil
}
}
}

View File

@ -1,27 +0,0 @@
//
// Asset.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/9/19.
//
import Foundation
public class Asset: Codable {
/// The ID of the asset
public let id: String
private enum CodingKeys : String, CodingKey {
case id = "id"
}
required public init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: Asset.CodingKeys.self)
if let id = try? container.decode(String.self, forKey: .id) {
self.id = id
} else {
self.id = String(try container.decode(Int.self, forKey: .id))
}
}
}

View File

@ -1,48 +0,0 @@
//
// AssetLibrary.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/9/19.
//
import Foundation
final class AssetLibrary: Codable {
/// The Assets
let assets: [String : Asset]
let imageAssets: [String : ImageAsset]
let precompAssets: [String : PrecompAsset]
required init(from decoder: Decoder) throws {
var container = try decoder.unkeyedContainer()
var containerForKeys = container
var decodedAssets = [String : Asset]()
var imageAssets = [String : ImageAsset]()
var precompAssets = [String : PrecompAsset]()
while !container.isAtEnd {
let keyContainer = try containerForKeys.nestedContainer(keyedBy: PrecompAsset.CodingKeys.self)
if keyContainer.contains(.layers) {
let precompAsset = try container.decode(PrecompAsset.self)
decodedAssets[precompAsset.id] = precompAsset
precompAssets[precompAsset.id] = precompAsset
} else {
let imageAsset = try container.decode(ImageAsset.self)
decodedAssets[imageAsset.id] = imageAsset
imageAssets[imageAsset.id] = imageAsset
}
}
self.assets = decodedAssets
self.precompAssets = precompAssets
self.imageAssets = imageAssets
}
func encode(to encoder: Encoder) throws {
var container = encoder.unkeyedContainer()
try container.encode(contentsOf: Array(assets.values))
}
}

View File

@ -1,48 +0,0 @@
//
// ImageAsset.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/9/19.
//
import Foundation
public final class ImageAsset: Asset {
/// Image name
public let name: String
/// Image Directory
public let directory: String
/// Image Size
public let width: Double
public let height: Double
enum CodingKeys : String, CodingKey {
case name = "p"
case directory = "u"
case width = "w"
case height = "h"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ImageAsset.CodingKeys.self)
self.name = try container.decode(String.self, forKey: .name)
self.directory = try container.decode(String.self, forKey: .directory)
self.width = try container.decode(Double.self, forKey: .width)
self.height = try container.decode(Double.self, forKey: .height)
try super.init(from: decoder)
}
override public func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(name, forKey: .name)
try container.encode(directory, forKey: .directory)
try container.encode(width, forKey: .width)
try container.encode(height, forKey: .height)
}
}

View File

@ -1,30 +0,0 @@
//
// PrecompAsset.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/9/19.
//
import Foundation
final class PrecompAsset: Asset {
/// Layers of the precomp
let layers: [LayerModel]
enum CodingKeys : String, CodingKey {
case layers = "layers"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PrecompAsset.CodingKeys.self)
self.layers = try container.decode([LayerModel].self, ofFamily: LayerType.self, forKey: .layers)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(layers, forKey: .layers)
}
}

View File

@ -1,21 +0,0 @@
import Foundation
#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)
import UIKit
#endif
extension Bundle {
func getAnimationData(_ name: String, subdirectory: String? = nil) throws -> Data? {
// Check for files in the bundle at the given path
if let url = self.url(forResource: name, withExtension: "json", subdirectory: subdirectory) {
return try Data(contentsOf: url)
}
// Check for data assets (not available on macOS)
#if os(iOS) || os(tvOS) || os(watchOS) || targetEnvironment(macCatalyst)
let assetKey = subdirectory != nil ? "\(subdirectory ?? "")/\(name)" : name
return NSDataAsset.init(name: assetKey, bundle: self)?.data
#else
return nil
#endif
}
}

View File

@ -1,40 +0,0 @@
// From: https://medium.com/@kewindannerfjordremeczki/swift-4-0-decodable-heterogeneous-collections-ecc0e6b468cf
import Foundation
/// To support a new class family, create an enum that conforms to this protocol and contains the different types.
protocol ClassFamily: Decodable {
/// The discriminator key.
static var discriminator: Discriminator { get }
/// Returns the class type of the object corresponding to the value.
func getType() -> AnyObject.Type
}
/// Discriminator key enum used to retrieve discriminator fields in JSON payloads.
enum Discriminator: String, CodingKey {
case type = "ty"
}
extension KeyedDecodingContainer {
/// Decode a heterogeneous list of objects for a given family.
/// - Parameters:
/// - heterogeneousType: The decodable type of the list.
/// - family: The ClassFamily enum for the type family.
/// - key: The CodingKey to look up the list in the current container.
/// - Returns: The resulting list of heterogeneousType elements.
func decode<T : Decodable, U : ClassFamily>(_ heterogeneousType: [T].Type, ofFamily family: U.Type, forKey key: K) throws -> [T] {
var container = try self.nestedUnkeyedContainer(forKey: key)
var list = [T]()
var tmpContainer = container
while !container.isAtEnd {
let typeContainer = try container.nestedContainer(keyedBy: Discriminator.self)
let family: U = try typeContainer.decode(U.self, forKey: U.discriminator)
if let type = family.getType() as? T.Type {
list.append(try tmpContainer.decode(type))
}
}
return list
}
}

View File

@ -1,128 +0,0 @@
//
// Keyframe.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/7/19.
//
import Foundation
import CoreGraphics
/**
Keyframe represents a point in time and is the container for datatypes.
Note: This is a parent class and should not be used directly.
*/
final class Keyframe<T: Interpolatable> {
/// The value of the keyframe
let value: T
/// The time in frames of the keyframe.
let time: CGFloat
/// A hold keyframe freezes interpolation until the next keyframe that is not a hold.
let isHold: Bool
/// The in tangent for the time interpolation curve.
let inTangent: Vector2D?
/// The out tangent for the time interpolation curve.
let outTangent: Vector2D?
/// The spacial in tangent of the vector.
let spatialInTangent: Vector3D?
/// The spacial out tangent of the vector.
let spatialOutTangent: Vector3D?
/// Initialize a value-only keyframe with no time data.
init(_ value: T,
spatialInTangent: Vector3D? = nil,
spatialOutTangent: Vector3D? = nil) {
self.value = value
self.time = 0
self.isHold = true
self.inTangent = nil
self.outTangent = nil
self.spatialInTangent = spatialInTangent
self.spatialOutTangent = spatialOutTangent
}
/// Initialize a keyframe
init(value: T,
time: Double,
isHold: Bool,
inTangent: Vector2D?,
outTangent: Vector2D?,
spatialInTangent: Vector3D? = nil,
spatialOutTangent: Vector3D? = nil) {
self.value = value
self.time = CGFloat(time)
self.isHold = isHold
self.outTangent = outTangent
self.inTangent = inTangent
self.spatialInTangent = spatialInTangent
self.spatialOutTangent = spatialOutTangent
}
}
/**
A generic class used to parse and remap keyframe json.
Keyframe json has a couple of different variations and formats depending on the
type of keyframea and also the version of the JSON. By parsing the raw data
we can reconfigure it into a constant format.
*/
final class KeyframeData<T: Codable>: Codable {
/// The start value of the keyframe
let startValue: T?
/// The End value of the keyframe. Note: Newer versions animation json do not have this field.
let endValue: T?
/// The time in frames of the keyframe.
let time: Double?
/// A hold keyframe freezes interpolation until the next keyframe that is not a hold.
let hold: Int?
/// The in tangent for the time interpolation curve.
let inTangent: Vector2D?
/// The out tangent for the time interpolation curve.
let outTangent: Vector2D?
/// The spacial in tangent of the vector.
let spatialInTangent: Vector3D?
/// The spacial out tangent of the vector.
let spatialOutTangent:Vector3D?
init(startValue: T?,
endValue: T?,
time: Double?,
hold: Int?,
inTangent: Vector2D?,
outTangent: Vector2D?,
spatialInTangent: Vector3D?,
spatialOutTangent: Vector3D?) {
self.startValue = startValue
self.endValue = endValue
self.time = time
self.hold = hold
self.inTangent = inTangent
self.outTangent = outTangent
self.spatialInTangent = spatialInTangent
self.spatialOutTangent = spatialOutTangent
}
enum CodingKeys : String, CodingKey {
case startValue = "s"
case endValue = "e"
case time = "t"
case hold = "h"
case inTangent = "i"
case outTangent = "o"
case spatialInTangent = "ti"
case spatialOutTangent = "to"
}
var isHold: Bool {
if let hold = hold {
return hold > 0
}
return false
}
}

View File

@ -1,108 +0,0 @@
//
// KeyframeGroup.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/14/19.
//
import Foundation
/**
Used for coding/decoding a group of Keyframes by type.
Keyframe data is wrapped in a dictionary { "k" : KeyframeData }.
The keyframe data can either be an array of keyframes or, if no animation is present, the raw value.
This helper object is needed to properly decode the json.
*/
final class KeyframeGroup<T>: Codable where T: Codable, T: Interpolatable {
let keyframes: ContiguousArray<Keyframe<T>>
private enum KeyframeWrapperKey: String, CodingKey {
case keyframeData = "k"
}
init(keyframes: ContiguousArray<Keyframe<T>>) {
self.keyframes = keyframes
}
init(_ value: T) {
self.keyframes = [Keyframe(value)]
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: KeyframeWrapperKey.self)
if let keyframeData: T = try? container.decode(T.self, forKey: .keyframeData) {
/// Try to decode raw value; No keyframe data.
self.keyframes = [Keyframe<T>(keyframeData)]
} else {
/**
Decode and array of keyframes.
Body Movin and Lottie deal with keyframes in different ways.
A keyframe object in Body movin defines a span of time with a START
and an END, from the current keyframe time to the next keyframe time.
A keyframe object in Lottie defines a singular point in time/space.
This point has an in-tangent and an out-tangent.
To properly decode this we must iterate through keyframes while holding
reference to the previous keyframe.
*/
var keyframesContainer = try container.nestedUnkeyedContainer(forKey: .keyframeData)
var keyframes = ContiguousArray<Keyframe<T>>()
var previousKeyframeData: KeyframeData<T>?
while(!keyframesContainer.isAtEnd) {
// Ensure that Time and Value are present.
let keyframeData = try keyframesContainer.decode(KeyframeData<T>.self)
guard let value: T = keyframeData.startValue ?? previousKeyframeData?.endValue,
let time = keyframeData.time else {
/// Missing keyframe data. JSON must be corrupt.
throw DecodingError.dataCorruptedError(forKey: KeyframeWrapperKey.keyframeData, in: container, debugDescription: "Missing keyframe data.")
}
keyframes.append(Keyframe<T>(value: value,
time: time,
isHold: keyframeData.isHold,
inTangent: previousKeyframeData?.inTangent,
outTangent: keyframeData.outTangent,
spatialInTangent: previousKeyframeData?.spatialInTangent,
spatialOutTangent: keyframeData.spatialOutTangent))
previousKeyframeData = keyframeData
}
self.keyframes = keyframes
}
}
func encode(to encoder: Encoder) throws {
var container = encoder.container(keyedBy: KeyframeWrapperKey.self)
if keyframes.count == 1 {
let keyframe = keyframes[0]
try container.encode(keyframe.value, forKey: .keyframeData)
} else {
var keyframeContainer = container.nestedUnkeyedContainer(forKey: .keyframeData)
for i in 1..<keyframes.endIndex {
let keyframe = keyframes[i-1]
let nextKeyframe = keyframes[i]
let keyframeData = KeyframeData<T>(startValue: keyframe.value,
endValue: nextKeyframe.value,
time: Double(keyframe.time),
hold: keyframe.isHold ? 1 : nil,
inTangent: nextKeyframe.inTangent,
outTangent: keyframe.outTangent,
spatialInTangent: nil,
spatialOutTangent: nil)
try keyframeContainer.encode(keyframeData)
}
}
}
}

View File

@ -1,32 +0,0 @@
//
// ImageLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds an image.
final class ImageLayerModel: LayerModel {
/// The reference ID of the image.
let referenceID: String
private enum CodingKeys : String, CodingKey {
case referenceID = "refId"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ImageLayerModel.CodingKeys.self)
self.referenceID = try container.decode(String.self, forKey: .referenceID)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(referenceID, forKey: .referenceID)
}
}

View File

@ -1,150 +0,0 @@
//
// Layer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/7/19.
//
import Foundation
/// Used for mapping a heterogeneous list to classes for parsing.
extension LayerType: ClassFamily {
static var discriminator: Discriminator = .type
func getType() -> AnyObject.Type {
switch self {
case .precomp:
return PreCompLayerModel.self
case .solid:
return SolidLayerModel.self
case .image:
return ImageLayerModel.self
case .null:
return LayerModel.self
case .shape:
return ShapeLayerModel.self
case .text:
return TextLayerModel.self
}
}
}
public enum LayerType: Int, Codable {
case precomp
case solid
case image
case null
case shape
case text
public init(from decoder: Decoder) throws {
self = try LayerType(rawValue: decoder.singleValueContainer().decode(RawValue.self)) ?? .null
}
}
public enum MatteType: Int, Codable {
case none
case add
case invert
case unknown
}
public enum BlendMode: Int, Codable {
case normal
case multiply
case screen
case overlay
case darken
case lighten
case colorDodge
case colorBurn
case hardLight
case softLight
case difference
case exclusion
case hue
case saturation
case color
case luminosity
}
/**
A base top container for shapes, images, and other view objects.
*/
class LayerModel: Codable {
/// The readable name of the layer
let name: String
/// The index of the layer
let index: Int
/// The type of the layer.
let type: LayerType
/// The coordinate space
let coordinateSpace: CoordinateSpace
/// The in time of the layer in frames.
let inFrame: Double
/// The out time of the layer in frames.
let outFrame: Double
/// The start time of the layer in frames.
let startTime: Double
/// The transform of the layer
let transform: Transform
/// The index of the parent layer, if applicable.
let parent: Int?
/// The blending mode for the layer
let blendMode: BlendMode
/// An array of masks for the layer.
let masks: [Mask]?
/// A number that stretches time by a multiplier
let timeStretch: Double
/// The type of matte if any.
let matte: MatteType?
let hidden: Bool
private enum CodingKeys : String, CodingKey {
case name = "nm"
case index = "ind"
case type = "ty"
case coordinateSpace = "ddd"
case inFrame = "ip"
case outFrame = "op"
case startTime = "st"
case transform = "ks"
case parent = "parent"
case blendMode = "bm"
case masks = "masksProperties"
case timeStretch = "sr"
case matte = "tt"
case hidden = "hd"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: LayerModel.CodingKeys.self)
self.name = try container.decodeIfPresent(String.self, forKey: .name) ?? "Layer"
self.index = try container.decode(Int.self, forKey: .index)
self.type = try container.decode(LayerType.self, forKey: .type)
self.coordinateSpace = try container.decodeIfPresent(CoordinateSpace.self, forKey: .coordinateSpace) ?? .type2d
self.inFrame = try container.decode(Double.self, forKey: .inFrame)
self.outFrame = try container.decode(Double.self, forKey: .outFrame)
self.startTime = try container.decode(Double.self, forKey: .startTime)
self.transform = try container.decode(Transform.self, forKey: .transform)
self.parent = try container.decodeIfPresent(Int.self, forKey: .parent)
self.blendMode = try container.decodeIfPresent(BlendMode.self, forKey: .blendMode) ?? .normal
self.masks = try container.decodeIfPresent([Mask].self, forKey: .masks)
self.timeStretch = try container.decodeIfPresent(Double.self, forKey: .timeStretch) ?? 1
self.matte = try container.decodeIfPresent(MatteType.self, forKey: .matte)
self.hidden = try container.decodeIfPresent(Bool.self, forKey: .hidden) ?? false
}
}

View File

@ -1,50 +0,0 @@
//
// PreCompLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds another animation composition.
final class PreCompLayerModel: LayerModel {
/// The reference ID of the precomp.
let referenceID: String
/// A value that remaps time over time.
let timeRemapping: KeyframeGroup<Vector1D>?
/// Precomp Width
let width: Double
/// Precomp Height
let height: Double
private enum CodingKeys : String, CodingKey {
case referenceID = "refId"
case timeRemapping = "tm"
case width = "w"
case height = "h"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: PreCompLayerModel.CodingKeys.self)
self.referenceID = try container.decode(String.self, forKey: .referenceID)
self.timeRemapping = try container.decodeIfPresent(KeyframeGroup<Vector1D>.self, forKey: .timeRemapping)
self.width = try container.decode(Double.self, forKey: .width)
self.height = try container.decode(Double.self, forKey: .height)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(referenceID, forKey: .referenceID)
try container.encode(timeRemapping, forKey: .timeRemapping)
try container.encode(width, forKey: .width)
try container.encode(height, forKey: .height)
}
}

View File

@ -1,32 +0,0 @@
//
// ShapeLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds vector shape objects.
final class ShapeLayerModel: LayerModel {
/// A list of shape items.
let items: [ShapeItem]
private enum CodingKeys : String, CodingKey {
case items = "shapes"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: ShapeLayerModel.CodingKeys.self)
self.items = try container.decode([ShapeItem].self, ofFamily: ShapeType.self, forKey: .items)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(self.items, forKey: .items)
}
}

View File

@ -1,44 +0,0 @@
//
// SolidLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds a solid color.
final class SolidLayerModel: LayerModel {
/// The color of the solid in Hex // Change to value provider.
let colorHex: String
/// The Width of the color layer
let width: Double
/// The height of the color layer
let height: Double
private enum CodingKeys : String, CodingKey {
case colorHex = "sc"
case width = "sw"
case height = "sh"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: SolidLayerModel.CodingKeys.self)
self.colorHex = try container.decode(String.self, forKey: .colorHex)
self.width = try container.decode(Double.self, forKey: .width)
self.height = try container.decode(Double.self, forKey: .height)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
try container.encode(colorHex, forKey: .colorHex)
try container.encode(width, forKey: .width)
try container.encode(height, forKey: .height)
}
}

View File

@ -1,44 +0,0 @@
//
// TextLayer.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/8/19.
//
import Foundation
/// A layer that holds text.
final class TextLayerModel: LayerModel {
/// The text for the layer
let text: KeyframeGroup<TextDocument>
/// Text animators
let animators: [TextAnimator]
private enum CodingKeys : String, CodingKey {
case textGroup = "t"
}
private enum TextCodingKeys : String, CodingKey {
case text = "d"
case animators = "a"
}
required init(from decoder: Decoder) throws {
let container = try decoder.container(keyedBy: TextLayerModel.CodingKeys.self)
let textContainer = try container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: .textGroup)
self.text = try textContainer.decode(KeyframeGroup<TextDocument>.self, forKey: .text)
self.animators = try textContainer.decode([TextAnimator].self, forKey: .animators)
try super.init(from: decoder)
}
override func encode(to encoder: Encoder) throws {
try super.encode(to: encoder)
var container = encoder.container(keyedBy: CodingKeys.self)
var textContainer = container.nestedContainer(keyedBy: TextCodingKeys.self, forKey: .textGroup)
try textContainer.encode(text, forKey: .text)
try textContainer.encode(animators, forKey: .animators)
}
}

View File

@ -1,24 +0,0 @@
//
// DashPattern.swift
// lottie-swift
//
// Created by Brandon Withrow on 1/22/19.
//
import Foundation
enum DashElementType: String, Codable {
case offset = "o"
case dash = "d"
case gap = "g"
}
final class DashElement: Codable {
let type: DashElementType
let value: KeyframeGroup<Vector1D>
enum CodingKeys : String, CodingKey {
case type = "n"
case value = "v"
}
}

Some files were not shown because too many files have changed in this diff Show More