| #include "rive/importers/backboard_importer.hpp" | |
| #include "rive/artboard.hpp" | |
| #include "rive/nested_artboard.hpp" | |
| #include "rive/backboard.hpp" | |
| #include "rive/file.hpp" | |
| #include "rive/assets/file_asset_referencer.hpp" | |
| #include "rive/assets/file_asset.hpp" | |
| #include "rive/constraints/scrolling/scroll_physics.hpp" | |
| #include "rive/viewmodel/viewmodel.hpp" | |
| #include "rive/viewmodel/viewmodel_instance.hpp" | |
| #include "rive/file.hpp" | |
| #include "rive/data_bind/converters/data_converter.hpp" | |
| #include "rive/data_bind/converters/data_converter_group.hpp" | |
| #include "rive/data_bind/converters/data_converter_group_item.hpp" | |
| #include "rive/data_bind/converters/data_converter_range_mapper.hpp" | |
| #include "rive/data_bind/converters/data_converter_interpolator.hpp" | |
| #include "rive/data_bind/data_bind.hpp" | |
| #include "rive/script_input_artboard.hpp" | |
| #include <algorithm> | |
| #include <unordered_set> | |
| #include <utility> | |
| using namespace rive; | |
| namespace | |
| { | |
| // Iterative Tarjan: the strongly connected component of each node. Components | |
| // complete in reverse topological order, so an edge between two components | |
| // always runs from the higher number to the lower one. Recursing could | |
| // overflow on large files. | |
| std::vector<int> stronglyConnectedComponents( | |
| const std::vector<std::vector<int>>& edges) | |
| { | |
| const size_t count = edges.size(); | |
| std::vector<int> order(count, -1); | |
| std::vector<int> lowLink(count, 0); | |
| std::vector<int> component(count, -1); | |
| std::vector<int> open; | |
| std::vector<std::pair<int, size_t>> calls; | |
| int nextOrder = 0; | |
| int nextComponent = 0; | |
| for (int root = 0; root < (int)count; root++) | |
| { | |
| if (order[root] != -1) | |
| { | |
| continue; | |
| } | |
| order[root] = lowLink[root] = nextOrder++; | |
| open.push_back(root); | |
| calls.push_back({root, 0}); | |
| while (!calls.empty()) | |
| { | |
| int current = calls.back().first; | |
| size_t next = calls.back().second++; | |
| if (next < edges[current].size()) | |
| { | |
| int to = edges[current][next]; | |
| if (order[to] == -1) | |
| { | |
| order[to] = lowLink[to] = nextOrder++; | |
| open.push_back(to); | |
| calls.push_back({to, 0}); | |
| } | |
| else if (component[to] == -1) | |
| { | |
| lowLink[current] = std::min(lowLink[current], order[to]); | |
| } | |
| continue; | |
| } | |
| calls.pop_back(); | |
| if (!calls.empty()) | |
| { | |
| int parent = calls.back().first; | |
| lowLink[parent] = std::min(lowLink[parent], lowLink[current]); | |
| } | |
| if (lowLink[current] == order[current]) | |
| { | |
| int member; | |
| do | |
| { | |
| member = open.back(); | |
| open.pop_back(); | |
| component[member] = nextComponent; | |
| } while (member != current); | |
| nextComponent++; | |
| } | |
| } | |
| } | |
| return component; | |
| } | |
| // Flags each referencer whose reference lies on a nested artboard cycle, which | |
| // would make instancing recurse without end. The editor never exports those. | |
| std::vector<bool> closesNestedArtboardCycle( | |
| const std::vector<ArtboardReferencer*>& referencers, | |
| const std::unordered_map<int, Artboard*>& artboards) | |
| { | |
| std::unordered_map<Artboard*, int> nodes; | |
| auto node = [&](Artboard* artboard) { | |
| return nodes.emplace(artboard, (int)nodes.size()).first->second; | |
| }; | |
| std::vector<std::pair<int, int>> references(referencers.size(), {-1, -1}); | |
| std::vector<std::vector<int>> edges; | |
| for (size_t i = 0; i < referencers.size(); i++) | |
| { | |
| Artboard* nesting = referencers[i]->nestingArtboard(); | |
| auto nested = artboards.find(referencers[i]->referencedArtboardId()); | |
| if (nesting == nullptr || nested == artboards.end() || | |
| nested->second == nullptr) | |
| { | |
| continue; | |
| } | |
| references[i] = {node(nesting), node(nested->second)}; | |
| edges.resize(nodes.size()); | |
| edges[references[i].first].push_back(references[i].second); | |
| } | |
| // A reference closes a cycle when both its ends share a component. | |
| auto component = stronglyConnectedComponents(edges); | |
| std::vector<bool> closesCycle(referencers.size(), false); | |
| for (size_t i = 0; i < referencers.size(); i++) | |
| { | |
| closesCycle[i] = references[i].first != -1 && | |
| component[references[i].first] == | |
| component[references[i].second]; | |
| } | |
| return closesCycle; | |
| } | |
| // Gives each bind on a file level converter (on its properties, formula tokens, | |
| // template arguments or script inputs) a clone of the converter it uses. Every | |
| // bind that uses a converter clones it, so a converter's own binds have to | |
| // resolve before anything clones the converter. Binds whose converter leads | |
| // back to the converter they sit on are left without one: cloning it would | |
| // recurse without end. The editor never exports those. Returns the binds it | |
| // handled. | |
| std::unordered_set<DataBind*> resolveConverterDataBinds( | |
| const std::vector<DataConverter*>& converters) | |
| { | |
| const size_t count = converters.size(); | |
| std::unordered_map<DataConverter*, int> indices; | |
| for (size_t i = 0; i < count; i++) | |
| { | |
| indices.emplace(converters[i], (int)i); | |
| } | |
| auto converterIndex = [&](DataBind* dataBind) { | |
| auto index = (size_t)dataBind->converterId(); | |
| return index < count ? (int)index : -1; | |
| }; | |
| std::vector<std::vector<int>> edges(count); | |
| for (size_t i = 0; i < count; i++) | |
| { | |
| for (auto dataBind : converters[i]->dataBinds()) | |
| { | |
| int to = converterIndex(dataBind); | |
| if (to != -1) | |
| { | |
| edges[i].push_back(to); | |
| } | |
| } | |
| // A group clones the converters of its items, which brings their binds. | |
| if (converters[i]->is<DataConverterGroup>()) | |
| { | |
| for (auto item : converters[i]->as<DataConverterGroup>()->items()) | |
| { | |
| auto itr = indices.find(item->converter()); | |
| if (itr != indices.end()) | |
| { | |
| edges[i].push_back(itr->second); | |
| } | |
| } | |
| } | |
| } | |
| // Lower components never use higher ones, so resolving in component order | |
| // finishes every converter before a bind clones it. | |
| auto component = stronglyConnectedComponents(edges); | |
| std::vector<int> order(count); | |
| for (size_t i = 0; i < count; i++) | |
| { | |
| order[i] = (int)i; | |
| } | |
| std::stable_sort(order.begin(), order.end(), [&](int a, int b) { | |
| return component[a] < component[b]; | |
| }); | |
| std::unordered_set<DataBind*> resolved; | |
| for (int i : order) | |
| { | |
| for (auto dataBind : converters[i]->dataBinds()) | |
| { | |
| resolved.insert(dataBind); | |
| int to = converterIndex(dataBind); | |
| // Already set when a partial import re-resolves the file's converters. | |
| if (to == -1 || component[to] == component[i] || | |
| dataBind->converter() != nullptr) | |
| { | |
| continue; | |
| } | |
| dataBind->converter(converters[to]->clone()->as<DataConverter>()); | |
| } | |
| } | |
| return resolved; | |
| } | |
| } // namespace | |
| BackboardImporter::BackboardImporter(Backboard* backboard) | |
| : m_Backboard(backboard) | |
| , m_NextArtboardId(0) | |
| {} | |
| void BackboardImporter::addArtboardReferencer(ArtboardReferencer* artboard) | |
| { | |
| m_ArtboardsReferencers.push_back(artboard); | |
| } | |
| void BackboardImporter::addFileAsset(rcp<FileAsset> asset) | |
| { | |
| m_FileAssets.push_back(asset); | |
| { | |
| // EDITOR BUG 4204 | |
| // -------------- | |
| // Ensure assetIds are unique. Due to an editor bug: | |
| // https://github.com/rive-app/rive/issues/4204 | |
| std::unordered_set<uint32_t> ids; | |
| uint32_t nextId = 1; | |
| for (auto fileAsset : m_FileAssets) | |
| { | |
| if (ids.count(fileAsset->assetId())) | |
| { | |
| fileAsset->assetId(nextId); | |
| } | |
| else | |
| { | |
| ids.insert(fileAsset->assetId()); | |
| if (fileAsset->assetId() >= nextId) | |
| { | |
| nextId = fileAsset->assetId() + 1; | |
| } | |
| } | |
| } | |
| // -------------- | |
| } | |
| } | |
| void BackboardImporter::addFileAssetReferencer(FileAssetReferencer* referencer) | |
| { | |
| m_FileAssetReferencers.push_back(referencer); | |
| } | |
| void BackboardImporter::addArtboard(Artboard* artboard) | |
| { | |
| #ifdef WITH_RIVE_TOOLS | |
| artboard->artboardId((uint16_t)m_NextArtboardId); | |
| #endif | |
| m_ArtboardLookup[m_NextArtboardId++] = artboard; | |
| } | |
| void BackboardImporter::addMissingArtboard() { m_NextArtboardId++; } | |
| StatusCode BackboardImporter::resolve() | |
| { | |
| auto closesCycle = closesNestedArtboardCycle( | |
| m_ArtboardsReferencers, | |
| m_ArtboardLookup); | |
| for (size_t i = 0; i < m_ArtboardsReferencers.size(); i++) | |
| { | |
| if (closesCycle[i]) | |
| { | |
| continue; | |
| } | |
| auto nestedArtboard = m_ArtboardsReferencers[i]; | |
| auto itr = m_ArtboardLookup.find(nestedArtboard->referencedArtboardId()); | |
| if (itr != m_ArtboardLookup.end()) | |
| { | |
| auto artboard = itr->second; | |
| if (artboard != nullptr) | |
| { | |
| nestedArtboard->referencedArtboard(artboard); | |
| } | |
| } | |
| } | |
| for (auto referencer : m_FileAssetReferencers) | |
| { | |
| auto index = (size_t)referencer->assetId(); | |
| if (index >= m_FileAssets.size()) | |
| { | |
| continue; | |
| } | |
| auto asset = m_FileAssets[index]; | |
| referencer->setAsset(asset); | |
| } | |
| for (auto converter : m_DataConverters) | |
| { | |
| if (converter->is<DataConverterRangeMapper>()) | |
| { | |
| size_t converterId = converter->as<DataConverterRangeMapper>() | |
| ->interpolatorId(); | |
| if (converterId != -1 && converterId < m_interpolators.size()) | |
| { | |
| converter->as<DataConverterRangeMapper>()->interpolator( | |
| m_interpolators[converterId]); | |
| } | |
| } | |
| else if (converter->is<DataConverterInterpolator>()) | |
| { | |
| size_t converterId = converter->as<DataConverterInterpolator>() | |
| ->interpolatorId(); | |
| if (converterId != -1 && converterId < m_interpolators.size()) | |
| { | |
| converter->as<DataConverterInterpolator>()->interpolator( | |
| m_interpolators[converterId]); | |
| } | |
| } | |
| } | |
| for (auto referencer : m_DataConverterGroupItemReferencers) | |
| { | |
| auto index = (size_t)referencer->converterId(); | |
| if (index >= m_DataConverters.size() || index < 0) | |
| { | |
| continue; | |
| } | |
| // Do not clone converters at this point because some of them are | |
| // incomplete | |
| referencer->converter(m_DataConverters[index]); | |
| } | |
| auto converterDataBinds = resolveConverterDataBinds(m_DataConverters); | |
| for (auto referencer : m_DataConverterReferencers) | |
| { | |
| if (converterDataBinds.count(referencer) != 0) | |
| { | |
| continue; | |
| } | |
| auto index = (size_t)referencer->converterId(); | |
| if (index >= m_DataConverters.size() || index < 0) | |
| { | |
| continue; | |
| } | |
| referencer->converter( | |
| m_DataConverters[index]->clone()->as<DataConverter>()); | |
| } | |
| return StatusCode::Ok; | |
| } | |
| void BackboardImporter::addDataConverter(DataConverter* dataConverter) | |
| { | |
| m_DataConverters.push_back(dataConverter); | |
| } | |
| void BackboardImporter::addDataConverterReferencer(DataBind* dataBind) | |
| { | |
| m_DataConverterReferencers.push_back(dataBind); | |
| } | |
| void BackboardImporter::addDataConverterGroupItemReferencer( | |
| DataConverterGroupItem* dataBind) | |
| { | |
| m_DataConverterGroupItemReferencers.push_back(dataBind); | |
| } | |
| void BackboardImporter::addInterpolator(KeyFrameInterpolator* interpolator) | |
| { | |
| // Since these interpolators do not belong to an artboard, we have to | |
| // initialize them | |
| interpolator->initialize(); | |
| m_interpolators.push_back(interpolator); | |
| } | |
| void BackboardImporter::seedInterpolator(KeyFrameInterpolator* interpolator) | |
| { | |
| m_interpolators.push_back(interpolator); | |
| } | |
| void BackboardImporter::addPhysics(ScrollPhysics* physics) | |
| { | |
| m_physics.push_back(physics); | |
| } | |
| void BackboardImporter::addViewModelInstance(ViewModelInstance* instance) | |
| { | |
| if (m_file == nullptr) | |
| { | |
| return; | |
| } | |
| auto viewModel = m_file->viewModel((size_t)instance->viewModelId()); | |
| if (viewModel != nullptr) | |
| { | |
| viewModel->addInstance(instance); | |
| } | |
| } | |
| void BackboardImporter::file(File* value) { m_file = value; } |