import QtQuick 2.15 import Qt3D.Core 2.15 import Qt3D.Render 2.15 import Utils 1.0 /** * MediaLibrary is an Entity that loads and manages a list of 3D media. * It also uses an internal cache to instantly reload media. */ Entity { id: root readonly property alias model: m.mediaModel property int renderMode property bool pickingEnabled: false readonly property alias count: instantiator.count // number of instantiated media delegates // For TransformGizmo in BoundingBox property DefaultCameraController sceneCameraController property Layer frontLayerComponent property var window /// Camera to consider for positioning property Camera camera: null /// True while at least one media is being loaded readonly property bool loading: { for (var i = 0; i < m.mediaModel.count; ++i) { if (m.mediaModel.get(i).status === SceneLoader.Loading) return true } return false } signal pressed(var pick) signal loadRequest(var idx) QtObject { id: m property ListModel mediaModel: ListModel { dynamicRoles: true } property var sourceToEntity: ({}) readonly property var mediaElement: ({ "source": "", "valid": true, "label": "", "visible": true, "hasBoundingBox": false, // for Meshing node only "displayBoundingBox": true, // for Meshing node only "hasTransform": false, // for SfMTransform node only "displayTransform": true, // for SfMTransform node only "section": "", "attribute": null, "entity": null, "requested": true, "vertexCount": 0, "faceCount": 0, "cameraCount": 0, "textureCount": 0, "resectionIdCount": 0, "resectionId": 0, "resectionGroups": [], "status": SceneLoader.None }) } function makeElement(values) { return Object.assign({}, JSON.parse(JSON.stringify(m.mediaElement)), values) } function ensureVisible(source) { var idx = find(source); if (idx === -1) return m.mediaModel.get(idx).visible = true loadRequest(idx) } function find(source) { for (var i = 0; i < m.mediaModel.count; ++i) { var elt = m.mediaModel.get(i) if (elt.source === source || elt.attribute === source) return i } return -1 } function load(filepath, label = undefined) { var pathStr = Filepath.urlToString(filepath) if (!Filepath.exists(pathStr)) { console.warn("Media Error: File " + pathStr + " does not exist.") return } // file already loaded, return if (m.sourceToEntity[pathStr]) { ensureVisible(pathStr) return } // add file to the internal ListModel m.mediaModel.append(makeElement({ "source": pathStr, "label": label ? label : Filepath.basename(pathStr), "section": "External" })) } function view(attribute) { if (m.sourceToEntity[attribute]) { ensureVisible(attribute) return } var attrLabel = attribute.isOutput ? "" : attribute.fullName.replace(attribute.node.name, "") var section = attribute.node.label // add file to the internal ListModel m.mediaModel.append(makeElement({ "label": section + attrLabel, "section": section, "attribute": attribute, })) } function remove(index) { // remove corresponding entry from model m.mediaModel.remove(index) } /// Get entity based on source function entity(source) { return sourceToEntity[source] } function entityAt(index) { return instantiator.objectAt(index) } function solo(index) { for (var i = 0; i < m.mediaModel.count; ++i) { m.mediaModel.setProperty(i, "visible", i === index) } } function clear() { m.mediaModel.clear() cache.clear() } // Cache that keeps in memory the last unloaded 3D media MediaCache { id: cache } NodeInstantiator { id: instantiator model: m.mediaModel delegate: Entity { id: instantiatedEntity property alias fullyInstantiated: mediaLoader.fullyInstantiated readonly property alias modelSource: mediaLoader.modelSource // Get the node property var currentNode: model.attribute ? model.attribute.node : null property string nodeType: currentNode ? currentNode.nodeType: null // Specific properties to the MESHING node (declared and initialized for every Entity anyway) property bool hasBoundingBox: { if (nodeType === "Meshing" && currentNode.attribute("useBoundingBox")) // Can have a BoundingBox return currentNode.attribute("useBoundingBox").value return false } onHasBoundingBoxChanged: model.hasBoundingBox = hasBoundingBox property bool displayBoundingBox: model.displayBoundingBox // Specific properties to the SFMTRANSFORM node (declared and initialized for every Entity anyway) property bool hasTransform: { if (nodeType === "SfMTransform" && currentNode.attribute("method")) // Can have a Transform return currentNode.attribute("method").value === "manual" return false } onHasTransformChanged: model.hasTransform = hasTransform property bool displayTransform: model.displayTransform // Create the medias MediaLoader { id: mediaLoader // whether MediaLoader has been fully instantiated by the NodeInstantiator property bool fullyInstantiated: false // explicitly store some attached model properties for outside use and ease binding readonly property var attribute: model.attribute readonly property int idx: index readonly property var modelSource: attribute || model.source readonly property bool visible: model.visible // multi-step binding to ensure MediaLoader source is properly // updated when needed, whether raw source is valid or not // raw source path property string rawSource: attribute ? attribute.value : model.source // whether dependencies are statified (applies for output/connected input attributes only) readonly property bool dependencyReady: { if (attribute) { const rootAttribute = attribute.isLink ? attribute.rootLinkParam : attribute if (rootAttribute.isOutput) return rootAttribute.node.globalStatus === "SUCCESS" } return true // is an input param without link (so no dependency) or an external file } // source based on raw source + dependency status property string currentSource: dependencyReady ? rawSource : "" // source based on currentSource + "requested" property property string finalSource: model.requested ? currentSource : "" // To use only if we want to draw the input source and not the current node output (Warning: to use with caution) // There is maybe a better way to do this to avoid overwriting bindings which should be readonly properties function drawInputSource() { rawSource = Qt.binding(() => instantiatedEntity.currentNode ? instantiatedEntity.currentNode.attribute("input").value: "") currentSource = Qt.binding(() => rawSource) finalSource = Qt.binding(() => rawSource) } camera: root.camera renderMode: root.renderMode enabled: visible // QObject.destroyed signal is not accessible // Use the object as NodeInstantiator model to be notified of its deletion NodeInstantiator { model: attribute delegate: Entity { objectName: "DestructionWatcher [" + model.toString() + "]" } onObjectRemoved: remove(index) } property bool alive: attribute ? attribute.node.alive : false onAliveChanged: { if (!alive && index >= 0) remove(index) } // 'visible' property drives media loading request onVisibleChanged: { // always request media loading if visible if (model.visible) model.requested = true // only cancel loading request if media is not valid // (a media won't be unloaded if already loaded, only hidden) else if (!model.valid) model.requested = false } function updateCacheAndModel(forceRequest) { // don't cache explicitly unloaded media if (model.requested && object && dependencyReady) { // cache current object if (cache.add(Filepath.urlToString(mediaLoader.source), object)) object = null } updateModel(forceRequest) } function updateModel(forceRequest) { // update model's source path if input is an attribute if (attribute) { model.source = rawSource } // auto-restore entity if raw source is in cache model.requested = forceRequest || (!model.valid && model.requested) || cache.contains(rawSource) model.valid = Filepath.exists(rawSource) && dependencyReady } Component.onCompleted: { // keep 'source' -> 'entity' reference m.sourceToEntity[modelSource] = instantiatedEntity // always request media loading when delegate has been created updateModel(true) // if external media failed to open, remove element from model if (!attribute && !object) remove(index) } onCurrentSourceChanged: { updateCacheAndModel(false) // Avoid the bounding box to disappear when we move it after a mesh already computed if (instantiatedEntity.hasBoundingBox && !currentSource) model.visible = true } onFinalSourceChanged: { // update media visibility // (useful if media was explicitly unloaded or hidden but loaded back from cache) model.visible = model.requested var cachedObject = cache.pop(rawSource) cached = cachedObject !== undefined if (cached) { object = cachedObject // only change cached object parent if mediaLoader has been fully instantiated // by the NodeInstantiator; otherwise re-parenting will fail silently and the object will disappear... // see "onFullyInstantiatedChanged" and parent NodeInstantiator's "onObjectAdded" if (fullyInstantiated) { object.parent = mediaLoader } } mediaLoader.source = Filepath.stringToUrl(finalSource) if (object) { // bind media info to corresponding model roles // (test for object validity to avoid error messages right after object has been deleted) var boundProperties = ["vertexCount", "faceCount", "cameraCount", "textureCount", "resectionIdCount", "resectionId", "resectionGroups"] boundProperties.forEach(function(prop) { model[prop] = Qt.binding(function() { return object ? object[prop] : 0 }) }) } else if (finalSource && status === Component.Ready) { // source was valid but no loader was created, remove element // check if component is ready to avoid removing element from the model before adding instance to the node remove(index) } } onFullyInstantiatedChanged: { // delayed reparenting of object coming from the cache if (object) object.parent = mediaLoader } onStatusChanged: { model.status = status // remove model entry for external media that failed to load if (status === SceneLoader.Error && !model.attribute) remove(index) } components: [ ObjectPicker { enabled: mediaLoader.enabled && pickingEnabled hoverEnabled: false onPressed: root.pressed(pick) } ] } // Transform: display a TransformGizmo for SfMTransform node only // note: use a NodeInstantiator to evaluate if the current node is a SfMTransform node and if the transform mode is set to Manual NodeInstantiator { id: sfmTransformGizmoInstantiator active: instantiatedEntity.hasTransform model: 1 SfMTransformGizmo { id: sfmTransformGizmoEntity sceneCameraController: root.sceneCameraController frontLayerComponent: root.frontLayerComponent window: root.window currentSfMTransformNode: instantiatedEntity.currentNode enabled: mediaLoader.visible && instantiatedEntity.displayTransform Component.onCompleted: { mediaLoader.drawInputSource() // Because we are sure we want to show the input in MANUAL mode only Scene3DHelper.addComponent(mediaLoader, sfmTransformGizmoEntity.objectTransform) // Add the transform to the media to see real-time transformations } } } // BoundingBox: display bounding box for MESHING computation // note: use a NodeInstantiator to evaluate if the current node is a MESHING node and if the checkbox is active NodeInstantiator { id: boundingBoxInstantiator active: instantiatedEntity.hasBoundingBox model: 1 MeshingBoundingBox { sceneCameraController: root.sceneCameraController frontLayerComponent: root.frontLayerComponent window: root.window currentMeshingNode: instantiatedEntity.currentNode enabled: mediaLoader.visible && instantiatedEntity.displayBoundingBox } } } onObjectAdded: { // notify object that it is now fully instantiated object.fullyInstantiated = true } onObjectRemoved: { if (m.sourceToEntity[object.modelSource]) delete m.sourceToEntity[object.modelSource] } } }