Merge remote-tracking branch 'origin/develop' into dev/nodesAndTaskManager

This commit is contained in:
Julien-Haudegond 2020-08-24 15:19:31 +02:00
commit 79e1c69d5d
39 changed files with 2060 additions and 209 deletions

View file

@ -8,13 +8,14 @@ Property = None
BaseObject = None
Variant = None
VariantList = None
JSValue = None
if meshroom.backend == meshroom.Backend.PYSIDE:
# PySide types
from .qt import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList
from .qt import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList, JSValue
elif meshroom.backend == meshroom.Backend.STANDALONE:
# Core types
from .core import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList
from .core import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList, JSValue
class _BaseModel:

View file

@ -146,3 +146,4 @@ Property = CoreProperty
BaseObject = CoreObject
Variant = object
VariantList = object
JSValue = None

View file

@ -1,4 +1,4 @@
from PySide2 import QtCore
from PySide2 import QtCore, QtQml
import shiboken2
class QObjectListModel(QtCore.QAbstractListModel):
@ -375,3 +375,4 @@ Property = QtCore.Property
BaseObject = QtCore.QObject
Variant = "QVariant"
VariantList = "QVariantList"
JSValue = QtQml.QJSValue

View file

@ -269,6 +269,7 @@ class Attribute(BaseObject):
hasOutputConnections = Property(bool, hasOutputConnections.fget, notify=hasOutputConnectionsChanged)
isDefault = Property(bool, _isDefault, notify=valueChanged)
linkParam = Property(BaseObject, getLinkParam, notify=isLinkChanged)
rootLinkParam = Property(BaseObject, lambda self: self.getLinkParam(recursive=True), notify=isLinkChanged)
node = Property(BaseObject, node.fget, constant=True)
enabledChanged = Signal()
enabled = Property(bool, getEnabled, setEnabled, notify=enabledChanged)
@ -312,8 +313,8 @@ class ListAttribute(Attribute):
self._value = value
# New value
else:
self.desc.validateValue(value)
self.extend(value)
newValue = self.desc.validateValue(value)
self.extend(newValue)
self.requestGraphUpdate()
@raiseIfLink
@ -422,10 +423,16 @@ class GroupAttribute(Attribute):
raise AttributeError(key)
def _set_value(self, exportedValue):
self.desc.validateValue(exportedValue)
# set individual child attribute values
for key, value in exportedValue.items():
self._value.get(key).value = value
value = self.desc.validateValue(exportedValue)
if isinstance(value, dict):
# set individual child attribute values
for key, v in value.items():
self._value.get(key).value = v
elif isinstance(value, (list, tuple)):
for attrDesc, v in zip(self.desc._groupDesc, value):
self._value.get(attrDesc.name).value = v
else:
raise AttributeError("Failed to set on GroupAttribute: {}".format(str(value)))
@Slot(str, result=Attribute)
def childAttribute(self, key):
@ -446,7 +453,7 @@ class GroupAttribute(Attribute):
def uid(self, uidIndex):
uids = []
for k, v in self._value.items():
if uidIndex in v.desc.uid:
if v.enabled and uidIndex in v.desc.uid:
uids.append(v.uid(uidIndex))
return hashValue(uids)

View file

@ -1,10 +1,10 @@
from meshroom.common import BaseObject, Property, Variant, VariantList
from meshroom.common import BaseObject, Property, Variant, VariantList, JSValue
from meshroom.core import pyCompatibility
from enum import Enum # available by default in python3. For python2: "pip install enum34"
import math
import os
import psutil
import ast
class Attribute(BaseObject):
"""
@ -32,12 +32,12 @@ class Attribute(BaseObject):
type = Property(str, lambda self: self.__class__.__name__, constant=True)
def validateValue(self, value):
""" Return validated/conformed 'value'.
""" Return validated/conformed 'value'. Need to be implemented in derived classes.
Raises:
ValueError: if value does not have the proper type
"""
return value
raise NotImplementedError("Attribute.validateValue is an abstract function that should be implemented in the derived class.")
def matchDescription(self, value, conform=False):
""" Returns whether the value perfectly match attribute's description.
@ -68,6 +68,14 @@ class ListAttribute(Attribute):
joinChar = Property(str, lambda self: self._joinChar, constant=True)
def validateValue(self, value):
if JSValue is not None and isinstance(value, JSValue):
# Note: we could use isArray(), property("length").toInt() to retrieve all values
raise ValueError("ListAttribute.validateValue: cannot recognize QJSValue. Please, use JSON.stringify(value) in QML.")
if isinstance(value, pyCompatibility.basestring):
# Alternative solution to set values from QML is to convert values to JSON string
# In this case, it works with all data types
value = ast.literal_eval(value)
if not isinstance(value, (list, tuple)):
raise ValueError('ListAttribute only supports list/tuple input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value)))
return value
@ -95,12 +103,25 @@ class GroupAttribute(Attribute):
groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True)
def validateValue(self, value):
""" Ensure value is a dictionary with keys compatible with the group description. """
if not isinstance(value, dict):
raise ValueError('GroupAttribute only supports dict input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value)))
invalidKeys = set(value.keys()).difference([attr.name for attr in self._groupDesc])
if invalidKeys:
raise ValueError('Value contains key that does not match group description : {}'.format(invalidKeys))
""" Ensure value is compatible with the group description and convert value if needed. """
if JSValue is not None and isinstance(value, JSValue):
# Note: we could use isArray(), property("length").toInt() to retrieve all values
raise ValueError("GroupAttribute.validateValue: cannot recognize QJSValue. Please, use JSON.stringify(value) in QML.")
if isinstance(value, pyCompatibility.basestring):
# Alternative solution to set values from QML is to convert values to JSON string
# In this case, it works with all data types
value = ast.literal_eval(value)
if isinstance(value, dict):
invalidKeys = set(value.keys()).difference([attr.name for attr in self._groupDesc])
if invalidKeys:
raise ValueError('Value contains key that does not match group description : {}'.format(invalidKeys))
elif isinstance(value, (list, tuple)):
if len(value) != len(self._groupDesc):
raise ValueError('Value contains incoherent number of values: desc size: {}, value size: {}'.format(len(self._groupDesc), len(value)))
else:
raise ValueError('GroupAttribute only supports dict/list/tuple input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value)))
return value
def matchDescription(self, value, conform=False):
@ -169,7 +190,7 @@ class BoolParam(Param):
def validateValue(self, value):
try:
return bool(int(value)) # int cast is useful to handle string values ('0', '1')
return bool(int(value)) # int cast is useful to handle string values ('0', '1')
except:
raise ValueError('BoolParam only supports bool value (param:{}, value:{}, type:{})'.format(self.name, value, type(value)))

View file

@ -223,8 +223,11 @@ class Graph(BaseObject):
def clear(self):
self.header.clear()
self._compatibilityNodes.clear()
self._nodes.clear()
self._edges.clear()
# Tell QML nodes are going to be deleted
for node in self._nodes:
node.alive = False
self._nodes.clear()
@property
def fileFeatures(self):
@ -437,6 +440,7 @@ class Graph(BaseObject):
self.removeEdge(edge.dst)
inEdges[edge.dst.getFullName()] = edge.src.getFullName()
node.alive = False
self._nodes.remove(node)
self.update()

View file

@ -470,6 +470,7 @@ class BaseNode(BaseObject):
self._position = position or Position()
self._attributes = DictModel(keyAttrName='name', parent=self)
self.attributesPerUid = defaultdict(set)
self._alive = True # for QML side to know if the node can be used or is going to be deleted
self._locked = False
self._duplicates = ListModel(parent=self) # list of nodes with the same uid
@ -566,6 +567,17 @@ class BaseNode(BaseObject):
self._position = value
self.positionChanged.emit()
@property
def alive(self):
return self._alive
@alive.setter
def alive(self, value):
if self._alive == value:
return
self._alive = value
self.aliveChanged.emit()
@property
def depth(self):
return self.graph.getDepth(self)
@ -927,6 +939,8 @@ class BaseNode(BaseObject):
globalExecModeChanged = Signal()
globalExecMode = Property(str, globalExecMode.fget, notify=globalExecModeChanged)
isComputed = Property(bool, _isComputed, notify=globalStatusChanged)
aliveChanged = Signal()
alive = Property(bool, alive.fget, alive.fset, notify=aliveChanged)
lockedChanged = Signal()
locked = Property(bool, getLocked, setLocked, notify=lockedChanged)
duplicatesChanged = Signal()

View file

@ -150,7 +150,12 @@ class LdrToHdrCalibration(desc.CommandLineNode):
if not cameraInitOutput.node.hasAttribute('viewpoints'):
if cameraInitOutput.node.hasAttribute('input'):
cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True)
viewpoints = cameraInitOutput.node.viewpoints.value
if cameraInitOutput and cameraInitOutput.node and cameraInitOutput.node.hasAttribute('viewpoints'):
viewpoints = cameraInitOutput.node.viewpoints.value
else:
# No connected CameraInit
node.nbBrackets.value = 0
return
# logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints)))
inputs = []
@ -184,7 +189,12 @@ class LdrToHdrCalibration(desc.CommandLineNode):
exposures = None
bracketSizes = set()
if len(exposureGroups) == 1:
node.nbBrackets.value = 1
if len(set(exposureGroups[0])) == 1:
# Single exposure and multiple views
node.nbBrackets.value = 1
else:
# Single view and multiple exposures
node.nbBrackets.value = len(exposureGroups[0])
else:
for expGroup in exposureGroups:
bracketSizes.add(len(expGroup))

View file

@ -180,7 +180,12 @@ class LdrToHdrMerge(desc.CommandLineNode):
if not cameraInitOutput.node.hasAttribute('viewpoints'):
if cameraInitOutput.node.hasAttribute('input'):
cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True)
viewpoints = cameraInitOutput.node.viewpoints.value
if cameraInitOutput and cameraInitOutput.node and cameraInitOutput.node.hasAttribute('viewpoints'):
viewpoints = cameraInitOutput.node.viewpoints.value
else:
# No connected CameraInit
node.nbBrackets.value = 0
return
# logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints)))
inputs = []
@ -214,7 +219,12 @@ class LdrToHdrMerge(desc.CommandLineNode):
exposures = None
bracketSizes = set()
if len(exposureGroups) == 1:
node.nbBrackets.value = 1
if len(set(exposureGroups[0])) == 1:
# Single exposure and multiple views
node.nbBrackets.value = 1
else:
# Single view and multiple exposures
node.nbBrackets.value = len(exposureGroups[0])
else:
for expGroup in exposureGroups:
bracketSizes.add(len(expGroup))

View file

@ -176,7 +176,12 @@ class LdrToHdrSampling(desc.CommandLineNode):
if not cameraInitOutput.node.hasAttribute('viewpoints'):
if cameraInitOutput.node.hasAttribute('input'):
cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True)
viewpoints = cameraInitOutput.node.viewpoints.value
if cameraInitOutput and cameraInitOutput.node and cameraInitOutput.node.hasAttribute('viewpoints'):
viewpoints = cameraInitOutput.node.viewpoints.value
else:
# No connected CameraInit
node.nbBrackets.value = 0
return
# logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints)))
inputs = []
@ -210,7 +215,12 @@ class LdrToHdrSampling(desc.CommandLineNode):
exposures = None
bracketSizes = set()
if len(exposureGroups) == 1:
node.nbBrackets.value = 1
if len(set(exposureGroups[0])) == 1:
# Single exposure and multiple views
node.nbBrackets.value = 1
else:
# Single view and multiple exposures
node.nbBrackets.value = len(exposureGroups[0])
else:
for expGroup in exposureGroups:
bracketSizes.add(len(expGroup))

View file

@ -1,4 +1,4 @@
__version__ = "5.0"
__version__ = "6.0"
from meshroom.core import desc
@ -35,6 +35,101 @@ A Graph Cut Max-Flow is applied to optimally cut the volume. This cut represents
value='',
uid=[0],
),
desc.BoolParam(
name='useBoundingBox',
label='Custom Bounding Box',
description='Edit the meshing bounding box. If enabled, it takes priority over the Estimate From SfM option. Parameters can be adjusted in advanced settings.',
value=False,
uid=[0],
group=''
),
desc.GroupAttribute(
name="boundingBox",
label="Bounding Box Settings",
description="Translation, rotation and scale of the bounding box.",
groupDesc=[
desc.GroupAttribute(
name="bboxTranslation",
label="Translation",
description="Position in space.",
groupDesc=[
desc.FloatParam(
name="x", label="x", description="X Offset",
value=0.0,
uid=[0],
range=(-20.0, 20.0, 0.01)
),
desc.FloatParam(
name="y", label="y", description="Y Offset",
value=0.0,
uid=[0],
range=(-20.0, 20.0, 0.01)
),
desc.FloatParam(
name="z", label="z", description="Z Offset",
value=0.0,
uid=[0],
range=(-20.0, 20.0, 0.01)
)
],
joinChar=","
),
desc.GroupAttribute(
name="bboxRotation",
label="Euler Rotation",
description="Rotation in Euler degrees.",
groupDesc=[
desc.FloatParam(
name="x", label="x", description="Euler X Rotation",
value=0.0,
uid=[0],
range=(-90.0, 90.0, 1)
),
desc.FloatParam(
name="y", label="y", description="Euler Y Rotation",
value=0.0,
uid=[0],
range=(-180.0, 180.0, 1)
),
desc.FloatParam(
name="z", label="z", description="Euler Z Rotation",
value=0.0,
uid=[0],
range=(-180.0, 180.0, 1)
)
],
joinChar=","
),
desc.GroupAttribute(
name="bboxScale",
label="Scale",
description="Scale of the bounding box.",
groupDesc=[
desc.FloatParam(
name="x", label="x", description="X Scale",
value=1.0,
uid=[0],
range=(0.0, 20.0, 0.01)
),
desc.FloatParam(
name="y", label="y", description="Y Scale",
value=1.0,
uid=[0],
range=(0.0, 20.0, 0.01)
),
desc.FloatParam(
name="z", label="z", description="Z Scale",
value=1.0,
uid=[0],
range=(0.0, 20.0, 0.01)
)
],
joinChar=","
)
],
joinChar=",",
enabled=lambda node: node.useBoundingBox.value,
),
desc.BoolParam(
name='estimateSpaceFromSfM',
label='Estimate Space From SfM',

View file

@ -1,4 +1,4 @@
__version__ = "2.0"
__version__ = "3.0"
from meshroom.core import desc
@ -34,12 +34,13 @@ The transformation can be based on:
label='Transformation Method',
description="Transformation method:\n"
" * transformation: Apply a given transformation\n"
" * manual: Apply the gizmo transformation (show the transformed input)\n"
" * auto_from_cameras: Use cameras\n"
" * auto_from_landmarks: Use landmarks\n"
" * from_single_camera: Use a specific camera as the origin of the coordinate system\n"
" * from_markers: Align specific markers to custom coordinates",
value='auto_from_landmarks',
values=['transformation', 'auto_from_cameras', 'auto_from_landmarks', 'from_single_camera', 'from_markers'],
values=['transformation', 'manual', 'auto_from_cameras', 'auto_from_landmarks', 'from_single_camera', 'from_markers'],
exclusive=True,
uid=[0],
),
@ -51,6 +52,76 @@ The transformation can be based on:
" * from_single_camera: Camera UID or image filename",
value='',
uid=[0],
enabled=lambda node: node.method.value == "transformation" or node.method.value == "from_single_camera",
),
desc.GroupAttribute(
name="manualTransform",
label="Manual Transform (Gizmo)",
description="Translation, rotation (Euler ZXY) and uniform scale.",
groupDesc=[
desc.GroupAttribute(
name="manualTranslation",
label="Translation",
description="Translation in space.",
groupDesc=[
desc.FloatParam(
name="x", label="x", description="X Offset",
value=0.0,
uid=[0],
range=(-20.0, 20.0, 0.01)
),
desc.FloatParam(
name="y", label="y", description="Y Offset",
value=0.0,
uid=[0],
range=(-20.0, 20.0, 0.01)
),
desc.FloatParam(
name="z", label="z", description="Z Offset",
value=0.0,
uid=[0],
range=(-20.0, 20.0, 0.01)
)
],
joinChar=","
),
desc.GroupAttribute(
name="manualRotation",
label="Euler Rotation",
description="Rotation in Euler degrees.",
groupDesc=[
desc.FloatParam(
name="x", label="x", description="Euler X Rotation",
value=0.0,
uid=[0],
range=(-90.0, 90.0, 1)
),
desc.FloatParam(
name="y", label="y", description="Euler Y Rotation",
value=0.0,
uid=[0],
range=(-180.0, 180.0, 1)
),
desc.FloatParam(
name="z", label="z", description="Euler Z Rotation",
value=0.0,
uid=[0],
range=(-180.0, 180.0, 1)
)
],
joinChar=","
),
desc.FloatParam(
name="manualScale",
label="Scale",
description="Uniform Scale.",
value=1.0,
uid=[0],
range=(0.0, 20.0, 0.01)
)
],
joinChar=",",
enabled=lambda node: node.method.value == "manual",
),
desc.ChoiceParam(
name='landmarksDescriberTypes',
@ -88,21 +159,24 @@ The transformation can be based on:
label='Scale',
description='Apply scale transformation.',
value=True,
uid=[0]
uid=[0],
enabled=lambda node: node.method.value != "manual",
),
desc.BoolParam(
name='applyRotation',
label='Rotation',
description='Apply rotation transformation.',
value=True,
uid=[0]
uid=[0],
enabled=lambda node: node.method.value != "manual",
),
desc.BoolParam(
name='applyTranslation',
label='Translation',
description='Apply translation transformation.',
value=True,
uid=[0]
uid=[0],
enabled=lambda node: node.method.value != "manual",
),
desc.ChoiceParam(
name='verboseLevel',

View file

@ -283,7 +283,7 @@ It iterates like that, adding cameras and triangulating new 2D features into 3D
label='Filter Track Forks',
description='Enable/Disable the track forks removal. A track contains a fork when incoherent matches \n'
'lead to multiple features in the same image for a single track. \n',
value=True,
value=False,
uid=[0],
),
desc.File(

View file

@ -13,7 +13,7 @@ from meshroom.core import pyCompatibility
from meshroom.ui import components
from meshroom.ui.components.clipboard import ClipboardHelper
from meshroom.ui.components.filepath import FilepathHelper
from meshroom.ui.components.scene3D import Scene3DHelper
from meshroom.ui.components.scene3D import Scene3DHelper, Transformations3DHelper
from meshroom.ui.palette import PaletteManager
from meshroom.ui.reconstruction import Reconstruction
from meshroom.ui.utils import QmlInstantEngine
@ -127,6 +127,7 @@ class MeshroomApp(QApplication):
# => expose them as context properties instead
self.engine.rootContext().setContextProperty("Filepath", FilepathHelper(parent=self))
self.engine.rootContext().setContextProperty("Scene3DHelper", Scene3DHelper(parent=self))
self.engine.rootContext().setContextProperty("Transformations3DHelper", Transformations3DHelper(parent=self))
self.engine.rootContext().setContextProperty("Clipboard", ClipboardHelper(parent=self))
# additional context properties

View file

@ -4,10 +4,13 @@ def registerTypes():
from meshroom.ui.components.clipboard import ClipboardHelper
from meshroom.ui.components.edge import EdgeMouseArea
from meshroom.ui.components.filepath import FilepathHelper
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController, Transformations3DHelper
from meshroom.ui.components.csvData import CsvData
qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea")
qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable
qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable
qmlRegisterType(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # TODO: uncreatable
qmlRegisterType(Transformations3DHelper, "Meshroom.Helpers", 1, 0, "Transformations3DHelper") # TODO: uncreatable
qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController")
qmlRegisterType(CsvData, "DataObjects", 1, 0, "CsvData")

View file

@ -0,0 +1,117 @@
from meshroom.common.qt import QObjectListModel
from PySide2.QtCore import QObject, Slot, Signal, Property
from PySide2.QtCharts import QtCharts
import csv
import os
class CsvData(QObject):
"""Store data from a CSV file."""
def __init__(self, parent=None):
"""Initialize the object without any parameter."""
super(CsvData, self).__init__(parent=parent)
self._filepath = ""
self._data = QObjectListModel(parent=self) # List of CsvColumn
self._ready = False
self.filepathChanged.connect(self.updateData)
@Slot(int, result=QObject)
def getColumn(self, index):
return self._data.at(index)
def getFilepath(self):
return self._filepath
@Slot(result=int)
def getNbColumns(self):
return len(self._data) if self._ready else 0
def setFilepath(self, filepath):
if self._filepath == filepath:
return
self.setReady(False)
self._filepath = filepath
self.filepathChanged.emit()
def setReady(self, ready):
if self._ready == ready:
return
self._ready = ready
self.readyChanged.emit()
def updateData(self):
self.setReady(False)
self._data.clear()
newColumns = self.read()
if newColumns:
self._data.setObjectList(newColumns)
self.setReady(True)
def read(self):
"""Read the CSV file and return a list containing CsvColumn objects."""
if not self._filepath or not self._filepath.lower().endswith(".csv") or not os.path.isfile(self._filepath):
return []
csvRows = []
with open(self._filepath, "r") as fp:
reader = csv.reader(fp)
for row in reader:
csvRows.append(row)
dataList = []
# Create the objects in dataList
# with the first line elements as objects' title
for elt in csvRows[0]:
dataList.append(CsvColumn(elt, parent=self._data))
# Populate the content attribute
for elt in csvRows[1:]:
for idx, value in enumerate(elt):
dataList[idx].appendValue(value)
return dataList
filepathChanged = Signal()
filepath = Property(str, getFilepath, setFilepath, notify=filepathChanged)
readyChanged = Signal()
ready = Property(bool, lambda self: self._ready, notify=readyChanged)
data = Property(QObject, lambda self: self._data, notify=readyChanged)
nbColumns = Property(int, getNbColumns, notify=readyChanged)
class CsvColumn(QObject):
"""Store content of a CSV column."""
def __init__(self, title="", parent=None):
"""Initialize the object with optional column title parameter."""
super(CsvColumn, self).__init__(parent=parent)
self._title = title
self._content = []
def appendValue(self, value):
self._content.append(value)
@Slot(result=str)
def getFirst(self):
if not self._content:
return ""
return self._content[0]
@Slot(result=str)
def getLast(self):
if not self._content:
return ""
return self._content[-1]
@Slot(QtCharts.QXYSeries)
def fillChartSerie(self, serie):
"""Fill XYSerie used for displaying QML Chart."""
if not serie:
return
serie.clear()
for index, value in enumerate(self._content):
serie.append(float(index), float(value))
title = Property(str, lambda self: self._title, constant=True)
content = Property("QStringList", lambda self: self._content, constant=True)

View file

@ -3,7 +3,7 @@ from math import acos, pi, sqrt
from PySide2.QtCore import QObject, Slot, QSize, Signal, QPointF
from PySide2.Qt3DCore import Qt3DCore
from PySide2.Qt3DRender import Qt3DRender
from PySide2.QtGui import QVector3D, QQuaternion, QVector2D
from PySide2.QtGui import QVector3D, QQuaternion, QVector2D, QVector4D, QMatrix4x4
from meshroom.ui.utils import makeProperty
@ -103,3 +103,206 @@ class TrackballController(QObject):
trackballSize = makeProperty(float, '_trackballSize', trackballSizeChanged)
rotationSpeedChanged = Signal()
rotationSpeed = makeProperty(float, '_rotationSpeed', rotationSpeedChanged)
class Transformations3DHelper(QObject):
# ---------- Exposed to QML ---------- #
@Slot(QVector4D, Qt3DRender.QCamera, QSize, result=QVector2D)
def pointFromWorldToScreen(self, point, camera, windowSize):
""" Compute the Screen point corresponding to a World Point.
Args:
point (QVector4D): point in world coordinates
camera (QCamera): camera viewing the scene
windowSize (QSize): size of the Scene3D window
Returns:
QVector2D: point in screen coordinates
"""
# Transform the point from World Coord to Normalized Device Coord
viewMatrix = camera.transform().matrix().inverted()
projectedPoint = (camera.projectionMatrix() * viewMatrix[0]).map(point)
projectedPoint2D = QVector2D(
projectedPoint.x()/projectedPoint.w(),
projectedPoint.y()/projectedPoint.w()
)
# Transform the point from Normalized Device Coord to Screen Coord
screenPoint2D = QVector2D(
int((projectedPoint2D.x() + 1) * windowSize.width() / 2),
int((projectedPoint2D.y() - 1) * windowSize.height() / -2)
)
return screenPoint2D
@Slot(Qt3DCore.QTransform, QMatrix4x4, QMatrix4x4, QMatrix4x4, QVector3D)
def relativeLocalTranslate(self, transformQtInstance, initialPosMat, initialRotMat, initialScaleMat, translateVec):
""" Translate the QTransform in its local space relatively to an initial state.
Args:
transformQtInstance (QTransform): reference to the Transform to modify
initialPosMat (QMatrix4x4): initial position matrix
initialRotMat (QMatrix4x4): initial rotation matrix
initialScaleMat (QMatrix4x4): initial scale matrix
translateVec (QVector3D): vector used for the local translation
"""
# Compute the translation transformation matrix
translationMat = QMatrix4x4()
translationMat.translate(translateVec)
# Compute the new model matrix (POSITION * ROTATION * TRANSLATE * SCALE) and set it to the Transform
mat = initialPosMat * initialRotMat * translationMat * initialScaleMat
transformQtInstance.setMatrix(mat)
@Slot(Qt3DCore.QTransform, QMatrix4x4, QQuaternion, QMatrix4x4, QVector3D, int)
def relativeLocalRotate(self, transformQtInstance, initialPosMat, initialRotQuat, initialScaleMat, axis, degree):
""" Rotate the QTransform in its local space relatively to an initial state.
Args:
transformQtInstance (QTransform): reference to the Transform to modify
initialPosMat (QMatrix4x4): initial position matrix
initialRotQuat (QQuaternion): initial rotation quaternion
initialScaleMat (QMatrix4x4): initial scale matrix
axis (QVector3D): axis to rotate around
degree (int): angle of rotation in degree
"""
# Compute the transformation quaternion from axis and angle in degrees
transformQuat = QQuaternion.fromAxisAndAngle(axis, degree)
# Compute the new rotation quaternion and then calculate the matrix
newRotQuat = initialRotQuat * transformQuat # Order is important
newRotationMat = self.quaternionToRotationMatrix(newRotQuat)
# Compute the new model matrix (POSITION * NEW_COMPUTED_ROTATION * SCALE) and set it to the Transform
mat = initialPosMat * newRotationMat * initialScaleMat
transformQtInstance.setMatrix(mat)
@Slot(Qt3DCore.QTransform, QMatrix4x4, QMatrix4x4, QMatrix4x4, QVector3D)
def relativeLocalScale(self, transformQtInstance, initialPosMat, initialRotMat, initialScaleMat, scaleVec):
""" Scale the QTransform in its local space relatively to an initial state.
Args:
transformQtInstance (QTransform): reference to the Transform to modify
initialPosMat (QMatrix4x4): initial position matrix
initialRotMat (QMatrix4x4): initial rotation matrix
initialScaleMat (QMatrix4x4): initial scale matrix
scaleVec (QVector3D): vector used for the relative scale
"""
# Make a copy of the scale matrix (otherwise, it is a reference and it does not work as expected)
scaleMat = self.copyMatrix4x4(initialScaleMat)
# Update the scale matrix copy (X then Y then Z) with the scaleVec values
scaleVecTuple = scaleVec.toTuple()
for i in range(3):
currentRow = list(scaleMat.row(i).toTuple()) # QVector3D does not implement [] operator or easy way to access value by index so this little hack is required
value = currentRow[i] + scaleVecTuple[i]
value = value if value >= 0 else -value # Make sure to have only positive scale (because negative scale can make issues with matrix decomposition)
currentRow[i] = value
scaleMat.setRow(i, QVector3D(currentRow[0], currentRow[1], currentRow[2])) # Apply the new row to the scale matrix
# Compute the new model matrix (POSITION * ROTATION * SCALE) and set it to the Transform
mat = initialPosMat * initialRotMat * scaleMat
transformQtInstance.setMatrix(mat)
@Slot(QMatrix4x4, result="QVariant")
def modelMatrixToMatrices(self, modelMat):
""" Decompose a model matrix into individual matrices.
Args:
modelMat (QMatrix4x4): model matrix to decompose
Returns:
QVariant: object containing position, rotation and scale matrices + rotation quaternion
"""
decomposition = self.decomposeModelMatrix(modelMat)
posMat = QMatrix4x4()
posMat.translate(decomposition.get("translation"))
rotMat = self.quaternionToRotationMatrix(decomposition.get("quaternion"))
scaleMat = QMatrix4x4()
scaleMat.scale(decomposition.get("scale"))
return {"position": posMat, "rotation": rotMat, "scale": scaleMat, "quaternion": decomposition.get("quaternion")}
@Slot(QVector3D, QVector3D, QVector3D, result=QMatrix4x4)
def computeModelMatrixWithEuler(self, translation, rotation, scale):
""" Compute a model matrix from three Vector3D.
Args:
translation (QVector3D): position in space (x, y, z)
rotation (QVector3D): Euler angles in degrees (x, y, z)
scale (QVector3D): scale of the object (x, y, z)
Returns:
QMatrix4x4: corresponding model matrix
"""
posMat = QMatrix4x4()
posMat.translate(translation)
quaternion = QQuaternion.fromEulerAngles(rotation)
rotMat = self.quaternionToRotationMatrix(quaternion)
scaleMat = QMatrix4x4()
scaleMat.scale(scale)
modelMat = posMat * rotMat * scaleMat
return modelMat
@Slot(QVector3D, QMatrix4x4, Qt3DRender.QCamera, QSize, result=float)
def computeScaleUnitFromModelMatrix(self, axis, modelMat, camera, windowSize):
""" Compute the length of the screen projected vector axis unit transformed by the model matrix.
Args:
axis (QVector3D): chosen axis ((1,0,0) or (0,1,0) or (0,0,1))
modelMat (QMatrix4x4): model matrix used for the transformation
camera (QCamera): camera viewing the scene
windowSize (QSize): size of the window in pixels
Returns:
float: length (in pixels)
"""
decomposition = self.decomposeModelMatrix(modelMat)
posMat = QMatrix4x4()
posMat.translate(decomposition.get("translation"))
rotMat = self.quaternionToRotationMatrix(decomposition.get("quaternion"))
unitScaleModelMat = posMat * rotMat * QMatrix4x4()
worldCenterPoint = unitScaleModelMat.map(QVector4D(0,0,0,1))
worldAxisUnitPoint = unitScaleModelMat.map(QVector4D(axis.x(),axis.y(),axis.z(),1))
screenCenter2D = self.pointFromWorldToScreen(worldCenterPoint, camera, windowSize)
screenAxisUnitPoint2D = self.pointFromWorldToScreen(worldAxisUnitPoint, camera, windowSize)
screenVector = QVector2D(screenAxisUnitPoint2D.x() - screenCenter2D.x(), -(screenAxisUnitPoint2D.y() - screenCenter2D.y()))
value = screenVector.length()
return value if (value and value > 10) else 10 # Threshold to avoid problems in extreme case
# ---------- "Private" Methods ---------- #
def copyMatrix4x4(self, mat):
""" Make a deep copy of a QMatrix4x4. """
newMat = QMatrix4x4()
for i in range(4):
newMat.setRow(i, mat.row(i))
return newMat
def decomposeModelMatrix(self, modelMat):
""" Decompose a model matrix into individual component.
Args:
modelMat (QMatrix4x4): model matrix to decompose
Returns:
QVariant: object containing translation and scale vectors + rotation quaternion
"""
translation = modelMat.column(3).toVector3D()
quaternion = QQuaternion.fromDirection(modelMat.column(2).toVector3D(), modelMat.column(1).toVector3D())
scale = QVector3D(modelMat.column(0).length(), modelMat.column(1).length(), modelMat.column(2).length())
return {"translation": translation, "quaternion": quaternion, "scale": scale}
def quaternionToRotationMatrix(self, q):
""" Return a rotation matrix from a quaternion. """
rotMat3x3 = q.toRotationMatrix()
return QMatrix4x4(
rotMat3x3(0, 0), rotMat3x3(0, 1), rotMat3x3(0, 2), 0,
rotMat3x3(1, 0), rotMat3x3(1, 1), rotMat3x3(1, 2), 0,
rotMat3x3(2, 0), rotMat3x3(2, 1), rotMat3x3(2, 2), 0,
0, 0, 0, 1
)

View file

@ -281,7 +281,11 @@ class UIGraph(QObject):
if self._graph:
self.stopExecution()
self.clear()
oldGraph = self._graph
self._graph = g
if oldGraph:
oldGraph.deleteLater()
self._graph.updated.connect(self.onGraphUpdated)
self._graph.update()
self._taskManager.update(self._graph)
@ -328,8 +332,7 @@ class UIGraph(QObject):
self.clearNodeHover()
self.clearNodeSelection()
self._taskManager.clear()
self._graph.deleteLater()
self._graph = None
self._graph.clear()
self._sortedDFSChunks.clear()
self._undoStack.clear()

View file

@ -23,15 +23,14 @@ RowLayout {
readonly property point outputAnchorPos: Qt.point(outputAnchor.x + outputAnchor.width/2,
outputAnchor.y + outputAnchor.height/2)
readonly property bool isList: attribute.type == "ListAttribute"
readonly property bool isList: attribute && attribute.type === "ListAttribute"
signal childPinCreated(var childAttribute, var pin)
signal childPinDeleted(var childAttribute, var pin)
signal pressed(var mouse)
objectName: attribute.name + "."
objectName: attribute ? attribute.name + "." : ""
layoutDirection: Qt.LeftToRight
spacing: 3
@ -58,7 +57,6 @@ RowLayout {
border.color: Colors.sysPalette.mid
color: Colors.sysPalette.base
Rectangle {
visible: inputConnectMA.containsMouse || childrenRepeater.count > 0 || attribute.isLink
radius: isList ? 0 : 2
@ -114,7 +112,7 @@ RowLayout {
readonly property string connectorType: "input"
readonly property alias attribute: root.attribute
readonly property alias nodeItem: root.nodeItem
readonly property bool isOutput: attribute.isOutput
readonly property bool isOutput: attribute && attribute.isOutput
readonly property alias isList: root.isList
property bool dragAccepted: false
anchors.verticalCenter: parent.verticalCenter
@ -175,8 +173,8 @@ RowLayout {
elide: hovered ? Text.ElideNone : Text.ElideMiddle
width: hovered ? contentWidth : parent.width
font.pointSize: 7
horizontalAlignment: attribute.isOutput ? Text.AlignRight : Text.AlignLeft
anchors.right: attribute.isOutput ? parent.right : undefined
horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft
anchors.right: attribute && attribute.isOutput ? parent.right : undefined
rightPadding: 0
color: hovered ? palette.highlight : palette.text
}

View file

@ -91,15 +91,15 @@ MessageDialog {
Label {
Layout.preferredWidth: 130
text: compatibilityNodeDelegate.node.nodeType
text: compatibilityNodeDelegate.node ? compatibilityNodeDelegate.node.nodeType : ""
}
Label {
Layout.fillWidth: true
text: compatibilityNodeDelegate.node.issueDetails
text: compatibilityNodeDelegate.node ? compatibilityNodeDelegate.node.issueDetails : ""
}
Label {
text: compatibilityNodeDelegate.node.canUpgrade ? MaterialIcons.check : MaterialIcons.clear
color: compatibilityNodeDelegate.node.canUpgrade ? "#4CAF50" : "#F44336"
text: compatibilityNodeDelegate.node && compatibilityNodeDelegate.node.canUpgrade ? MaterialIcons.check : MaterialIcons.clear
color: compatibilityNodeDelegate.node && compatibilityNodeDelegate.node.canUpgrade ? "#4CAF50" : "#F44336"
font.family: MaterialIcons.fontFamily
font.pointSize: 14
font.bold: true

View file

@ -232,11 +232,11 @@ Item {
id: edgesRepeater
// delay edges loading after nodes (edges needs attribute pins to be created)
model: nodeRepeater.loaded ? root.graph.edges : undefined
model: nodeRepeater.loaded && root.graph ? root.graph.edges : undefined
delegate: Edge {
property var src: root._attributeToDelegate[edge.src]
property var dst: root._attributeToDelegate[edge.dst]
property var src: edge ? root._attributeToDelegate[edge.src] : undefined
property var dst: edge ? root._attributeToDelegate[edge.dst] : undefined
property var srcAnchor: src.nodeItem.mapFromItem(src, src.outputAnchorPos.x, src.outputAnchorPos.y)
property var dstAnchor: dst.nodeItem.mapFromItem(dst, dst.inputAnchorPos.x, dst.inputAnchorPos.y)
@ -406,8 +406,8 @@ Item {
Repeater {
id: nodeRepeater
model: root.graph.nodes
property bool loaded: count === model.count
model: root.graph ? root.graph.nodes : undefined
property bool loaded: model ? count === model.count : false
delegate: Node {
id: nodeDelegate

View file

@ -18,7 +18,7 @@ Item {
/// Whether the node can be modified
property bool readOnly: node.locked
/// Whether the node is in compatibility mode
readonly property bool isCompatibilityNode: node.hasOwnProperty("compatibilityIssue")
readonly property bool isCompatibilityNode: node ? node.hasOwnProperty("compatibilityIssue") : false
/// Mouse related states
property bool selected: false
property bool hovered: false
@ -40,11 +40,11 @@ Item {
signal attributePinDeleted(var attribute, var pin)
// use node name as object name to simplify debugging
objectName: node.name
objectName: node ? node.name : ""
// initialize position with node coordinates
x: root.node.x
y: root.node.y
x: root.node ? root.node.x : undefined
y: root.node ? root.node.y : undefined
implicitHeight: childrenRect.height

View file

@ -76,7 +76,7 @@ Panel {
SensorDBDialog {
id: sensorDBDialog
sensorDatabase: Filepath.stringToUrl(cameraInit.attribute("sensorDatabase").value)
sensorDatabase: cameraInit ? Filepath.stringToUrl(cameraInit.attribute("sensorDatabase").value) : ""
readOnly: _reconstruction.computing
onUpdateIntrinsicsRequest: _reconstruction.rebuildIntrinsics(cameraInit)
}

View file

@ -0,0 +1,6 @@
pragma Singleton
import Meshroom.Helpers 1.0
Transformations3DHelper {
}

View file

@ -8,3 +8,4 @@ Format 1.0 format.js
# singleton Clipboard 1.0 Clipboard.qml
# singleton Filepath 1.0 Filepath.qml
# singleton Scene3DHelper 1.0 Scene3DHelper.qml
# singleton Transformations3DHelper 1.0 Transformations3DHelper.qml

View file

@ -0,0 +1,139 @@
import QtQuick 2.9
import QtQuick.Controls 2.3
import QtQuick.Layouts 1.3
import MaterialIcons 2.2
import QtPositioning 5.8
import QtLocation 5.9
import QtCharts 2.13
import Charts 1.0
import Controls 1.0
import Utils 1.0
import DataObjects 1.0
FloatingPane {
id: root
property var ldrHdrCalibrationNode: null
property color textColor: Colors.sysPalette.text
clip: true
padding: 4
CsvData {
id: csvData
filepath: ldrHdrCalibrationNode ? ldrHdrCalibrationNode.attribute("response").value : ""
}
// To avoid interaction with components in background
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
onPressed: {}
onReleased: {}
onWheel: {}
}
property bool crfReady: csvData.ready && csvData.nbColumns >= 4
onCrfReadyChanged: {
if(crfReady)
{
redCurve.clear()
greenCurve.clear()
blueCurve.clear()
csvData.getColumn(1).fillChartSerie(redCurve)
csvData.getColumn(2).fillChartSerie(greenCurve)
csvData.getColumn(3).fillChartSerie(blueCurve)
}
else
{
redCurve.clear()
greenCurve.clear()
blueCurve.clear()
}
}
Item {
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
anchors.horizontalCenterOffset: -responseChart.width/2
anchors.verticalCenterOffset: -responseChart.height/2
InteractiveChartView {
id: responseChart
width: root.width > 400 ? 400 : (root.width < 350 ? 350 : root.width)
height: width * 0.75
title: "Camera Response Function (CRF)"
legend.visible: false
antialiasing: true
ValueAxis {
id: valueAxisX
labelFormat: "%i"
titleText: "Camera Brightness"
min: crfReady ? csvData.getColumn(0).getFirst() : 0
max: crfReady ? csvData.getColumn(0).getLast() : 1
}
ValueAxis {
id: valueAxisY
titleText: "Normalized Radiance"
min: 0.0
max: 1.0
}
// We cannot use a Repeater with these Components so we need to instantiate them one by one
// Red curve
LineSeries {
id: redCurve
axisX: valueAxisX
axisY: valueAxisY
name: crfReady ? csvData.getColumn(1).title : ""
color: name.toLowerCase()
}
// Green curve
LineSeries {
id: greenCurve
axisX: valueAxisX
axisY: valueAxisY
name: crfReady ? csvData.getColumn(2).title : ""
color: name.toLowerCase()
}
// Blue curve
LineSeries {
id: blueCurve
axisX: valueAxisX
axisY: valueAxisY
name: crfReady ? csvData.getColumn(3).title : ""
color: name.toLowerCase()
}
}
Item {
id: btnContainer
anchors.bottom: responseChart.bottom
anchors.bottomMargin: 35
anchors.left: responseChart.left
anchors.leftMargin: responseChart.width * 0.15
RowLayout {
ChartViewCheckBox {
text: "ALL"
color: textColor
checkState: legend.buttonGroup.checkState
onClicked: {
const _checked = checked
for(let i = 0; i < responseChart.count; ++i) {
responseChart.series(i).visible = _checked
}
}
}
ChartViewLegend {
id: legend
chartView: responseChart
}
}
}
}
}

View file

@ -552,6 +552,18 @@ FocusScope {
featuresViewer: featuresViewerLoader.item
}
}
Loader {
id: ldrHdrCalibrationGraph
anchors.fill: parent
property var activeNode: _reconstruction.activeNodes.get('LdrToHdrCalibration').node
active: activeNode && activeNode.isComputed && displayLdrHdrCalibrationGraph.checked
sourceComponent: CameraResponseGraph {
ldrHdrCalibrationNode: activeNode
}
}
}
FloatingPane {
id: bottomToolbar
@ -628,6 +640,25 @@ FocusScope {
visible: activeNode
}
MaterialToolButton {
id: displayLdrHdrCalibrationGraph
property var activeNode: _reconstruction.activeNodes.get("LdrToHdrCalibration").node
property bool isComputed: activeNode && activeNode.isComputed
ToolTip.text: "Display Camera Response Function: " + (activeNode ? activeNode.label : "No Node")
text: MaterialIcons.timeline
font.pointSize: 11
Layout.minimumWidth: 0
checkable: true
checked: false
enabled: activeNode && activeNode.isComputed
visible: activeNode
onIsComputedChanged: {
if(!isComputed)
checked = false
}
}
Label {
id: resolutionLabel
Layout.fillWidth: true

View file

@ -0,0 +1,92 @@
import Qt3D.Core 2.0
import Qt3D.Render 2.9
import Qt3D.Input 2.0
import Qt3D.Extras 2.10
import QtQuick 2.9
Entity {
id: root
property Transform transform: Transform {}
components: [transform]
Entity {
components: [cube, greyMaterial]
CuboidMesh {
id: cube
property real edge : 1.995 // Almost 2: important to have all the cube's vertices with a unit of 1
xExtent: edge
yExtent: edge
zExtent: edge
}
PhongAlphaMaterial {
id: greyMaterial
property color base: "#fff"
ambient: base
alpha: 0.15
// Pretty convincing combination
blendFunctionArg: BlendEquation.Add
sourceRgbArg: BlendEquationArguments.SourceAlpha
sourceAlphaArg: BlendEquationArguments.OneMinusSourceAlpha
destinationRgbArg: BlendEquationArguments.DestinationColor
destinationAlphaArg: BlendEquationArguments.OneMinusSourceAlpha
}
}
Entity {
components: [edges, orangeMaterial]
PhongMaterial {
id: orangeMaterial
property color base: "#f49b2b"
ambient: base
}
GeometryRenderer {
id: edges
primitiveType: GeometryRenderer.Lines
geometry: Geometry {
Attribute {
id: boundingBoxPosition
attributeType: Attribute.VertexAttribute
vertexBaseType: Attribute.Float
vertexSize: 3
count: 24
name: defaultPositionAttributeName
buffer: Buffer {
type: Buffer.VertexBuffer
data: new Float32Array([
1.0, 1.0, 1.0,
1.0, -1.0, 1.0,
1.0, 1.0, 1.0,
1.0, 1.0, -1.0,
1.0, 1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, -1.0, -1.0,
-1.0, 1.0, -1.0,
-1.0, -1.0, -1.0,
1.0, -1.0, -1.0,
-1.0, -1.0, -1.0,
-1.0, -1.0, 1.0,
1.0, -1.0, 1.0,
1.0, -1.0, -1.0,
1.0, 1.0, -1.0,
1.0, -1.0, -1.0,
-1.0, 1.0, 1.0,
-1.0, 1.0, -1.0,
1.0, -1.0, 1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, 1.0,
-1.0, -1.0, 1.0,
-1.0, 1.0, -1.0,
1.0, 1.0, -1.0
])
}
}
boundingVolumePositionAttribute: boundingBoxPosition
}
}
}
}

View file

@ -23,6 +23,8 @@ Entity {
property alias windowSize: trackball.windowSize
property alias trackballSize: trackball.trackballSize
property bool loseMouseFocus: false // Must be changed by other entities when they want to take mouse focus
readonly property alias pressed: mouseHandler._pressed
signal mousePressed(var mouse)
signal mouseReleased(var mouse, var moved)
@ -44,7 +46,7 @@ Entity {
property point lastPosition
property point currentPosition
property bool hasMoved
sourceDevice: mouseSourceDevice
sourceDevice: loseMouseFocus ? null : mouseSourceDevice
onPressed: {
_pressed = true;
currentPosition.x = lastPosition.x = mouse.x;
@ -60,6 +62,30 @@ Entity {
onPositionChanged: {
currentPosition.x = mouse.x;
currentPosition.y = mouse.y;
const dt = 0.02
if(panning) { // translate
var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.03;
var tx = axisMX.value * root.translateSpeed * d;
var ty = axisMY.value * root.translateSpeed * d;
mouseHandler.hasMoved = true;
root.camera.translate(Qt.vector3d(-tx, -ty, 0).times(dt));
return;
}
if(moving){ // trackball rotation
trackball.rotate(mouseHandler.lastPosition, mouseHandler.currentPosition, dt);
mouseHandler.lastPosition = mouseHandler.currentPosition;
mouseHandler.hasMoved = true;
return;
}
if(zooming) { // zoom with alt + RMD
var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.1;
var tz = axisMX.value * root.translateSpeed * d;
mouseHandler.hasMoved = true;
root.camera.translate(Qt.vector3d(0, 0, tz).times(dt), Camera.DontTranslateViewCenter)
return;
}
}
onDoubleClicked: mouseDoubleClicked(mouse)
onWheel: {
@ -162,32 +188,4 @@ Entity {
}
]
}
components: [
FrameAction {
onTriggered: {
if(panning) { // translate
var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.03;
var tx = axisMX.value * root.translateSpeed * d;
var ty = axisMY.value * root.translateSpeed * d;
mouseHandler.hasMoved = true;
root.camera.translate(Qt.vector3d(-tx, -ty, 0).times(dt));
return;
}
if(moving){ // trackball rotation
trackball.rotate(mouseHandler.lastPosition, mouseHandler.currentPosition, dt);
mouseHandler.lastPosition = mouseHandler.currentPosition;
mouseHandler.hasMoved = true;
return;
}
if(zooming) { // zoom with alt + RMD
var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.1;
var tz = axisMX.value * root.translateSpeed * d;
mouseHandler.hasMoved = true;
root.camera.translate(Qt.vector3d(0, 0, tz).times(dt), Camera.DontTranslateViewCenter)
return;
}
}
}
]
}

View file

@ -0,0 +1,36 @@
import Qt3D.Core 2.0
import Qt3D.Render 2.9
import Qt3D.Input 2.0
import Qt3D.Extras 2.10
import QtQuick 2.9
import Qt3D.Logic 2.0
/**
* Wrapper for TransformGizmo.
* Must be instantiated to control an other entity.
* The goal is to instantiate the other entity inside this wrapper to gather the object and the gizmo.
* objectTranform is the component the other entity should use as a Transform.
*/
Entity {
id: root
property DefaultCameraController sceneCameraController
property Layer frontLayerComponent
property var window
property alias uniformScale: transformGizmo.uniformScale // By default, if not set, the value is: false
property TransformGizmo transformGizmo: TransformGizmo {
id: transformGizmo
camera: root.camera
windowSize: root.windowSize
frontLayerComponent: root.frontLayerComponent
window: root.window
onPickedChanged: {
sceneCameraController.loseMouseFocus = pressed // Notify the camera if the transform takes/releases the focus
}
}
readonly property Camera camera : sceneCameraController.camera
readonly property var windowSize: sceneCameraController.windowSize
readonly property alias objectTransform : transformGizmo.objectTransform // The Transform the object should use
}

View file

@ -284,6 +284,38 @@ FloatingPane {
}
}
// BoundingBox visibility (if meshing node)
MaterialToolButton {
visible: model.hasBoundingBox
enabled: model.visible
Layout.alignment: Qt.AlignTop
Layout.fillHeight: true
text: MaterialIcons.transform
font.pointSize: 10
ToolTip.text: model.displayBoundingBox ? "Hide BBox" : "Show BBox"
flat: true
opacity: model.visible ? (model.displayBoundingBox ? 1.0 : 0.6) : 0.6
onClicked: {
model.displayBoundingBox = !model.displayBoundingBox
}
}
// Transform visibility (if SfMTransform node)
MaterialToolButton {
visible: model.hasTransform
enabled: model.visible
Layout.alignment: Qt.AlignTop
Layout.fillHeight: true
text: MaterialIcons._3d_rotation
font.pointSize: 10
ToolTip.text: model.displayTransform ? "Hide Gizmo" : "Show Gizmo"
flat: true
opacity: model.visible ? (model.displayTransform ? 1.0 : 0.6) : 0.6
onClicked: {
model.displayTransform = !model.displayTransform
}
}
// Media label and info
Item {
implicitHeight: childrenRect.height

View file

@ -16,6 +16,11 @@ Entity {
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 positionning
property Camera camera: null
@ -41,6 +46,10 @@ Entity {
"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,
@ -146,144 +155,234 @@ Entity {
id: instantiator
model: m.mediaModel
delegate: MediaLoader {
id: mediaLoader
delegate: Entity {
id: instantiatedEntity
property alias fullyInstantiated: mediaLoader.fullyInstantiated
readonly property alias modelSource: mediaLoader.modelSource
// whether MediaLoader has been fully instantiated by the NodeInstantiator
property bool fullyInstantiated: false
// Get the node
property var currentNode: model.attribute ? model.attribute.node : null
property string nodeType: currentNode ? currentNode.nodeType: null
// explicitely 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
readonly 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 && attribute.isOutput)
return attribute.node.globalStatus === "SUCCESS";
if(attribute && attribute.isLink)
return attribute.linkParam.node.globalStatus === "SUCCESS";
return true;
// 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
}
// source based on raw source + dependency status
readonly property string currentSource: dependencyReady ? rawSource : ""
// source based on currentSource + "requested" property
readonly property string finalSource: model.requested ? currentSource : ""
onHasBoundingBoxChanged: model.hasBoundingBox = hasBoundingBox
property bool displayBoundingBox: model.displayBoundingBox
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 [" + attribute.toString() + "]" }
onObjectRemoved: remove(idx)
// 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
// '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 explicitely unloaded media
if(model.requested && object && dependencyReady) {
// cache current object
if(cache.add(Filepath.urlToString(mediaLoader.source), object));
object = null;
// Create the media
MediaLoader {
id: mediaLoader
// whether MediaLoader has been fully instantiated by the NodeInstantiator
property bool fullyInstantiated: false
// explicitely 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)
// if the node is removed, the attribute will be invalid
return false
const rootAttribute = attribute.isLink ? attribute.rootLinkParam : attribute
if(rootAttribute.isOutput)
return rootAttribute.node.globalStatus === "SUCCESS"
return true // is an input param so no dependency
}
updateModel(forceRequest);
}
// source based on raw source + dependency status
property string currentSource: dependencyReady ? rawSource : ""
// source based on currentSource + "requested" property
property string finalSource: model.requested ? currentSource : ""
function updateModel(forceRequest) {
// update model's source path if input is an attribute
if(attribute) {
model.source = rawSource;
// 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 overwritting 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)
}
// 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] = mediaLoader;
// 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)
}
camera: root.camera
renderMode: root.renderMode
enabled: visible
onCurrentSourceChanged: {
updateCacheAndModel(false)
}
// 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)
}
onFinalSourceChanged: {
// update media visibility
// (useful if media was explicitly unloaded or hidden but loaded back from cache)
model.visible = model.requested;
property bool alive: attribute ? attribute.node.alive : false
onAliveChanged: {
if(!alive && index >= 0)
remove(index)
}
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;
// '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"];
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)
}
}
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"];
boundProperties.forEach( function(prop){
model[prop] = Qt.binding(function() { return object ? object[prop] : 0; });
})
onFullyInstantiatedChanged: {
// delayed reparenting of object coming from the cache
if(object)
object.parent = mediaLoader;
}
else if(finalSource) {
// source was valid but no loader was created, remove element
remove(index);
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
}
}
}
onFullyInstantiatedChanged: {
// delayed reparenting of object coming from the cache
if(object)
object.parent = mediaLoader;
}
// 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
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)
MeshingBoundingBox {
sceneCameraController: root.sceneCameraController
frontLayerComponent: root.frontLayerComponent
window: root.window
currentMeshingNode: instantiatedEntity.currentNode
enabled: mediaLoader.visible && instantiatedEntity.displayBoundingBox
}
]
}
}
onObjectAdded: {
@ -292,7 +391,8 @@ Entity {
}
onObjectRemoved: {
delete m.sourceToEntity[object.modelSource];
if(m.sourceToEntity[object.modelSource])
delete m.sourceToEntity[object.modelSource]
}
}
}

View file

@ -31,7 +31,7 @@ import Utils 1.0
return;
}
// clear previously created objet if any
// clear previously created object if any
if(object) {
object.destroy();
object = null;

View file

@ -0,0 +1,91 @@
import Qt3D.Core 2.0
import Qt3D.Render 2.9
import Qt3D.Input 2.0
import Qt3D.Extras 2.10
import QtQuick 2.9
/**
* BoundingBox entity for Meshing node. Used to define the area to reconstruct.
* Simple box controlled by a gizmo to give easy and visual representation.
*/
Entity {
id: root
property DefaultCameraController sceneCameraController
property Layer frontLayerComponent
property var window
property var currentMeshingNode: null
enabled: true
EntityWithGizmo {
id: boundingBoxEntity
sceneCameraController: root.sceneCameraController
frontLayerComponent: root.frontLayerComponent
window: root.window
// Update node meshing slider values when the gizmo has changed: translation, rotation, scale, type
transformGizmo.onGizmoChanged: {
switch(type) {
case TransformGizmo.Type.TRANSLATION: {
_reconstruction.setAttribute(
root.currentMeshingNode.attribute("boundingBox.bboxTranslation"),
JSON.stringify([translation.x, translation.y, translation.z])
)
break
}
case TransformGizmo.Type.ROTATION: {
_reconstruction.setAttribute(
root.currentMeshingNode.attribute("boundingBox.bboxRotation"),
JSON.stringify([rotation.x, rotation.y, rotation.z])
)
break
}
case TransformGizmo.Type.SCALE: {
_reconstruction.setAttribute(
root.currentMeshingNode.attribute("boundingBox.bboxScale"),
JSON.stringify([scale.x, scale.y, scale.z])
)
break
}
case TransformGizmo.Type.ALL: {
_reconstruction.setAttribute(
root.currentMeshingNode.attribute("boundingBox"),
JSON.stringify([
[translation.x, translation.y, translation.z],
[rotation.x, rotation.y, rotation.z],
[scale.x, scale.y, scale.z]
])
)
break
}
}
}
// Translation values from node (vector3d because this is the type of QTransform.translation)
property var nodeTranslation : Qt.vector3d(
root.currentMeshingNode ? root.currentMeshingNode.attribute("boundingBox.bboxTranslation.x").value : 0,
root.currentMeshingNode ? root.currentMeshingNode.attribute("boundingBox.bboxTranslation.y").value : 0,
root.currentMeshingNode ? root.currentMeshingNode.attribute("boundingBox.bboxTranslation.z").value : 0
)
// Rotation values from node (3 separated values because QTransform stores Euler angles like this)
property var nodeRotationX: root.currentMeshingNode ? root.currentMeshingNode.attribute("boundingBox.bboxRotation.x").value : 0
property var nodeRotationY: root.currentMeshingNode ? root.currentMeshingNode.attribute("boundingBox.bboxRotation.y").value : 0
property var nodeRotationZ: root.currentMeshingNode ? root.currentMeshingNode.attribute("boundingBox.bboxRotation.z").value : 0
// Scale values from node (vector3d because this is the type of QTransform.scale3D)
property var nodeScale: Qt.vector3d(
root.currentMeshingNode ? root.currentMeshingNode.attribute("boundingBox.bboxScale.x").value : 1,
root.currentMeshingNode ? root.currentMeshingNode.attribute("boundingBox.bboxScale.y").value : 1,
root.currentMeshingNode ? root.currentMeshingNode.attribute("boundingBox.bboxScale.z").value : 1
)
// Automatically evaluate the Transform: value is taken from the node OR from the actual modification if the gizmo is moved by mouse.
// When the gizmo has changed (with mouse), the new values are set to the node, the priority is given back to the node and the Transform is re-evaluated once with those values.
transformGizmo.gizmoDisplayTransform.translation: transformGizmo.focusGizmoPriority ? transformGizmo.gizmoDisplayTransform.translation : nodeTranslation
transformGizmo.gizmoDisplayTransform.rotationX: transformGizmo.focusGizmoPriority ? transformGizmo.gizmoDisplayTransform.rotationX : nodeRotationX
transformGizmo.gizmoDisplayTransform.rotationY: transformGizmo.focusGizmoPriority ? transformGizmo.gizmoDisplayTransform.rotationY : nodeRotationY
transformGizmo.gizmoDisplayTransform.rotationZ: transformGizmo.focusGizmoPriority ? transformGizmo.gizmoDisplayTransform.rotationZ : nodeRotationZ
transformGizmo.objectTransform.scale3D: transformGizmo.focusGizmoPriority ? transformGizmo.objectTransform.scale3D : nodeScale
// The entity
BoundingBox { transform: boundingBoxEntity.objectTransform }
}
}

View file

@ -0,0 +1,88 @@
import Qt3D.Core 2.0
import Qt3D.Render 2.9
import Qt3D.Input 2.0
import Qt3D.Extras 2.10
import QtQuick 2.9
/**
* Gizmo for SfMTransform node.
* Uses EntityWithGizmo wrapper because we should not instantiate TransformGizmo alone.
*/
Entity {
id: root
property DefaultCameraController sceneCameraController
property Layer frontLayerComponent
property var window
property var currentSfMTransformNode: null
enabled: true
readonly property alias objectTransform: sfmTranformGizmoEntity.objectTransform // The Transform the object should use
EntityWithGizmo {
id: sfmTranformGizmoEntity
sceneCameraController: root.sceneCameraController
frontLayerComponent: root.frontLayerComponent
window: root.window
uniformScale: true // We want to make uniform scale transformations
// Update node SfMTransform slider values when the gizmo has changed: translation, rotation, scale, type
transformGizmo.onGizmoChanged: {
switch(type) {
case TransformGizmo.Type.TRANSLATION: {
_reconstruction.setAttribute(
root.currentSfMTransformNode.attribute("manualTransform.manualTranslation"),
JSON.stringify([translation.x, translation.y, translation.z])
)
break
}
case TransformGizmo.Type.ROTATION: {
_reconstruction.setAttribute(
root.currentSfMTransformNode.attribute("manualTransform.manualRotation"),
JSON.stringify([rotation.x, rotation.y, rotation.z])
)
break
}
case TransformGizmo.Type.SCALE: {
// Only one scale is needed since the scale is uniform
_reconstruction.setAttribute(
root.currentSfMTransformNode.attribute("manualTransform.manualScale"),
scale.x
)
break
}
case TransformGizmo.Type.ALL: {
_reconstruction.setAttribute(
root.currentSfMTransformNode.attribute("manualTransform"),
JSON.stringify([
[translation.x, translation.y, translation.z],
[rotation.x, rotation.y, rotation.z],
scale.x
])
)
break
}
}
}
// Translation values from node (vector3d because this is the type of QTransform.translation)
property var nodeTranslation : Qt.vector3d(
root.currentSfMTransformNode ? root.currentSfMTransformNode.attribute("manualTransform.manualTranslation.x").value : 0,
root.currentSfMTransformNode ? root.currentSfMTransformNode.attribute("manualTransform.manualTranslation.y").value : 0,
root.currentSfMTransformNode ? root.currentSfMTransformNode.attribute("manualTransform.manualTranslation.z").value : 0
)
// Rotation values from node (3 separated values because QTransform stores Euler angles like this)
property var nodeRotationX: root.currentSfMTransformNode ? root.currentSfMTransformNode.attribute("manualTransform.manualRotation.x").value : 0
property var nodeRotationY: root.currentSfMTransformNode ? root.currentSfMTransformNode.attribute("manualTransform.manualRotation.y").value : 0
property var nodeRotationZ: root.currentSfMTransformNode ? root.currentSfMTransformNode.attribute("manualTransform.manualRotation.z").value : 0
// Scale value from node (simple number because we use uniform scale)
property var nodeScale: root.currentSfMTransformNode ? root.currentSfMTransformNode.attribute("manualTransform.manualScale").value : 1
// Automatically evaluate the Transform: value is taken from the node OR from the actual modification if the gizmo is moved by mouse.
// When the gizmo has changed (with mouse), the new values are set to the node, the priority is given back to the node and the Transform is re-evaluated once with those values.
transformGizmo.gizmoDisplayTransform.translation: transformGizmo.focusGizmoPriority ? transformGizmo.gizmoDisplayTransform.translation : nodeTranslation
transformGizmo.gizmoDisplayTransform.rotationX: transformGizmo.focusGizmoPriority ? transformGizmo.gizmoDisplayTransform.rotationX : nodeRotationX
transformGizmo.gizmoDisplayTransform.rotationY: transformGizmo.focusGizmoPriority ? transformGizmo.gizmoDisplayTransform.rotationY : nodeRotationY
transformGizmo.gizmoDisplayTransform.rotationZ: transformGizmo.focusGizmoPriority ? transformGizmo.gizmoDisplayTransform.rotationZ : nodeRotationZ
transformGizmo.objectTransform.scale3D: transformGizmo.focusGizmoPriority ? transformGizmo.objectTransform.scale3D : Qt.vector3d(nodeScale, nodeScale, nodeScale)
}
}

View file

@ -0,0 +1,594 @@
import Qt3D.Core 2.0
import Qt3D.Render 2.9
import Qt3D.Input 2.0
import Qt3D.Extras 2.10
import QtQuick 2.9
import Qt3D.Logic 2.0
import QtQuick.Controls 2.3
import Utils 1.0
/**
* Simple transformation gizmo entirely made with Qt3D entities.
* Uses Python Transformations3DHelper to compute matrices.
* This TransformGizmo entity should only be instantiated in EntityWithGizmo entity which is its wrapper.
* It means, to use it for a specified application, make sure to instantiate EntityWithGizmo.
*/
Entity {
id: root
property Camera camera
property var windowSize
property Layer frontLayerComponent // Used to draw gizmo on top of everything
property var window
readonly property alias gizmoScale: gizmoScaleLookSlider.value
property bool uniformScale: false // By default, the scale is not uniform
property bool focusGizmoPriority: false // If true, it is used to give the priority to the current transformation (and not to a upper-level binding)
property Transform gizmoDisplayTransform: Transform {
id: gizmoDisplayTransform
scale: root.gizmoScale * (camera.position.minus(gizmoDisplayTransform.translation)).length() // The gizmo needs a constant apparent size
}
// Component the object controlled by the gizmo must use
property Transform objectTransform : Transform {
translation: gizmoDisplayTransform.translation
rotation: gizmoDisplayTransform.rotation
scale3D: Qt.vector3d(1,1,1)
}
signal pickedChanged(bool pressed)
signal gizmoChanged(var translation, var rotation, var scale, int type)
function emitGizmoChanged(type) {
const translation = gizmoDisplayTransform.translation // Position in space
const rotation = Qt.vector3d(gizmoDisplayTransform.rotationX, gizmoDisplayTransform.rotationY, gizmoDisplayTransform.rotationZ) // Euler angles
const scale = objectTransform.scale3D // Scale of the object
gizmoChanged(translation, rotation, scale, type)
root.focusGizmoPriority = false
}
components: [gizmoDisplayTransform, mouseHandler, frontLayerComponent]
/***** ENUMS *****/
enum Axis {
X,
Y,
Z
}
enum Type {
TRANSLATION,
ROTATION,
SCALE,
ALL
}
function convertAxisEnum(axis) {
switch(axis) {
case TransformGizmo.Axis.X: return Qt.vector3d(1,0,0)
case TransformGizmo.Axis.Y: return Qt.vector3d(0,1,0)
case TransformGizmo.Axis.Z: return Qt.vector3d(0,0,1)
}
}
function convertTypeEnum(type) {
switch(type) {
case TransformGizmo.Type.TRANSLATION: return "TRANSLATION"
case TransformGizmo.Type.ROTATION: return "ROTATION"
case TransformGizmo.Type.SCALE: return "SCALE"
case TransformGizmo.Type.ALL: return "ALL"
}
}
/***** TRANSFORMATIONS (using local vars) *****/
/**
* @brief Translate locally the gizmo and the object.
*
* @remarks
* To make local translation, we need to recompute a new matrix.
* Update gizmoDisplayTransform's matrix and all its properties while avoiding the override of translation property.
* Update objectTransform in the same time thanks to binding on translation property.
*
* @param initialModelMatrix object containing position, rotation and scale matrices + rotation quaternion
* @param translateVec vector3d used to make the local translation
*/
function doRelativeTranslation(initialModelMatrix, translateVec) {
Transformations3DHelper.relativeLocalTranslate(
gizmoDisplayTransform,
initialModelMatrix.position,
initialModelMatrix.rotation,
initialModelMatrix.scale,
translateVec
)
}
/**
* @brief Rotate the gizmo and the object around a specific axis.
*
* @remarks
* To make local rotation around an axis, we need to recompute a new matrix from a quaternion.
* Update gizmoDisplayTransform's matrix and all its properties while avoiding the override of rotation, rotationX, rotationY and rotationZ properties.
* Update objectTransform in the same time thanks to binding on rotation property.
*
* @param initialModelMatrix object containing position, rotation and scale matrices + rotation quaternion
* @param axis vector3d describing the axis to rotate around
* @param degree angle of rotation in degrees
*/
function doRelativeRotation(initialModelMatrix, axis, degree) {
Transformations3DHelper.relativeLocalRotate(
gizmoDisplayTransform,
initialModelMatrix.position,
initialModelMatrix.quaternion,
initialModelMatrix.scale,
axis,
degree
)
}
/**
* @brief Scale the object relatively to its current scale.
*
* @remarks
* To change scale of the object, we need to recompute a new matrix to avoid overriding bindings.
* Update objectTransform properties only (gizmoDisplayTransform is not affected).
*
* @param initialModelMatrix object containing position, rotation and scale matrices + rotation quaternion
* @param scaleVec vector3d used to make the relative scale
*/
function doRelativeScale(initialModelMatrix, scaleVec) {
Transformations3DHelper.relativeLocalScale(
objectTransform,
initialModelMatrix.position,
initialModelMatrix.rotation,
initialModelMatrix.scale,
scaleVec
)
}
/**
* @brief Reset the translation of the gizmo and the object.
*
* @remarks
* Update gizmoDisplayTransform's matrix and all its properties while avoiding the override of translation property.
* Update objectTransform in the same time thanks to binding on translation property.
*/
function resetTranslation() {
const mat = gizmoDisplayTransform.matrix
const newMat = Qt.matrix4x4(
mat.m11, mat.m12, mat.m13, 0,
mat.m21, mat.m22, mat.m23, 0,
mat.m31, mat.m32, mat.m33, 0,
mat.m41, mat.m42, mat.m43, 1
)
gizmoDisplayTransform.setMatrix(newMat)
}
/**
* @brief Reset the rotation of the gizmo and the object.
*
* @remarks
* Update gizmoDisplayTransform's quaternion while avoiding the override of rotationX, rotationY and rotationZ properties.
* Update objectTransform in the same time thanks to binding on rotation property.
* Here, we can change the rotation property (but not rotationX, rotationY and rotationZ because they can be used in upper-level bindings).
*
* @note
* We could implement a way of changing the matrix instead of overriding rotation (quaternion) property.
*/
function resetRotation() {
gizmoDisplayTransform.rotation = Qt.quaternion(1,0,0,0)
}
/**
* @brief Reset the scale of the object.
*
* @remarks
* To reset the scale, we make the difference of the current one to 1 and recompute the matrix.
* Like this, we kind of apply an inverse scale transformation.
* It prevents overriding scale3D property (because it can be used in upper-level binding).
*/
function resetScale() {
const modelMat = Transformations3DHelper.modelMatrixToMatrices(objectTransform.matrix)
const scaleDiff = Qt.vector3d(
-(objectTransform.scale3D.x - 1),
-(objectTransform.scale3D.y - 1),
-(objectTransform.scale3D.z - 1)
)
doRelativeScale(modelMat, scaleDiff)
}
/***** DEVICES *****/
MouseDevice { id: mouseSourceDevice }
MouseHandler {
id: mouseHandler
sourceDevice: enabled ? mouseSourceDevice : null
property var objectPicker: null
property bool enabled: false
onPositionChanged: {
if (objectPicker && objectPicker.button === Qt.LeftButton) {
root.focusGizmoPriority = true
// Get the selected axis
const pickedAxis = convertAxisEnum(objectPicker.gizmoAxis)
// TRANSLATION or SCALE transformation
if(objectPicker.gizmoType === TransformGizmo.Type.TRANSLATION || objectPicker.gizmoType === TransformGizmo.Type.SCALE) {
// Compute the vector PickedPosition -> CurrentMousePoint
const pickedPosition = objectPicker.screenPoint
const mouseVector = Qt.vector2d(mouse.x - pickedPosition.x, -(mouse.y - pickedPosition.y))
// Transform the positive picked axis vector from World Coord to Screen Coord
const gizmoLocalPointOnAxis = gizmoDisplayTransform.matrix.times(Qt.vector4d(pickedAxis.x, pickedAxis.y, pickedAxis.z, 1))
const gizmoCenterPoint = gizmoDisplayTransform.matrix.times(Qt.vector4d(0, 0, 0, 1))
const screenPoint2D = Transformations3DHelper.pointFromWorldToScreen(gizmoLocalPointOnAxis, camera, windowSize)
const screenCenter2D = Transformations3DHelper.pointFromWorldToScreen(gizmoCenterPoint, camera, windowSize)
const screenAxisVector = Qt.vector2d(screenPoint2D.x - screenCenter2D.x, -(screenPoint2D.y - screenCenter2D.y))
// Get the cosinus of the angle from the screenAxisVector to the mouseVector
// It will be used as a intensity factor
const cosAngle = screenAxisVector.dotProduct(mouseVector) / (screenAxisVector.length() * mouseVector.length())
const offset = cosAngle * mouseVector.length() / objectPicker.scaleUnit
// Do the transformation
if(objectPicker.gizmoType === TransformGizmo.Type.TRANSLATION && offset !== 0) {
doRelativeTranslation(objectPicker.modelMatrix, pickedAxis.times(offset)) // Do a translation from the initial Object Model Matrix when we picked the gizmo
}
else if(objectPicker.gizmoType === TransformGizmo.Type.SCALE && offset !== 0) {
if(root.uniformScale)
doRelativeScale(objectPicker.modelMatrix, Qt.vector3d(1,1,1).times(offset)) // Do a uniform scale from the initial Object Model Matrix when we picked the gizmo
else
doRelativeScale(objectPicker.modelMatrix, pickedAxis.times(offset)) // Do a scale on one axis from the initial Object Model Matrix when we picked the gizmo
}
return
}
// ROTATION transformation
else if(objectPicker.gizmoType === TransformGizmo.Type.ROTATION) {
// Get Screen Coordinates of the gizmo center
const gizmoCenterPoint = gizmoDisplayTransform.matrix.times(Qt.vector4d(0, 0, 0, 1))
const screenCenter2D = Transformations3DHelper.pointFromWorldToScreen(gizmoCenterPoint, camera, root.windowSize)
// Get the vector screenCenter2D -> PickedPosition
const originalVector = Qt.vector2d(objectPicker.screenPoint.x - screenCenter2D.x, -(objectPicker.screenPoint.y - screenCenter2D.y))
// Compute the vector screenCenter2D -> CurrentMousePoint
const mouseVector = Qt.vector2d(mouse.x - screenCenter2D.x, -(mouse.y - screenCenter2D.y))
// Get the angle from the originalVector to the mouseVector
const angle = Math.atan2(-originalVector.y*mouseVector.x + originalVector.x*mouseVector.y, originalVector.x*mouseVector.x + originalVector.y*mouseVector.y) * 180 / Math.PI
// Get the orientation of the gizmo in function of the camera
const gizmoLocalAxisVector = gizmoDisplayTransform.matrix.times(Qt.vector4d(pickedAxis.x, pickedAxis.y, pickedAxis.z, 0))
const gizmoToCameraVector = camera.position.toVector4d().minus(gizmoCenterPoint)
const orientation = gizmoLocalAxisVector.dotProduct(gizmoToCameraVector) > 0 ? 1 : -1
if (angle !== 0) doRelativeRotation(objectPicker.modelMatrix, pickedAxis, angle*orientation) // Do a rotation from the initial Object Model Matrix when we picked the gizmo
return
}
}
if(objectPicker && objectPicker.button === Qt.RightButton) {
resetMenu.popup(window)
}
}
onReleased: {
if(objectPicker && mouse.button === Qt.LeftButton) {
const type = objectPicker.gizmoType
objectPicker = null // To prevent going again in the onPositionChanged
emitGizmoChanged(type)
}
}
}
Menu {
id: resetMenu
MenuItem {
text: "Reset Translation"
onTriggered: {
resetTranslation()
emitGizmoChanged(TransformGizmo.Type.TRANSLATION)
}
}
MenuItem {
text: "Reset Rotation"
onTriggered: {
resetRotation()
emitGizmoChanged(TransformGizmo.Type.ROTATION)
}
}
MenuItem {
text: "Reset Scale"
onTriggered: {
resetScale()
emitGizmoChanged(TransformGizmo.Type.SCALE)
}
}
MenuItem {
text: "Reset All"
onTriggered: {
resetTranslation()
resetRotation()
resetScale()
emitGizmoChanged(TransformGizmo.Type.ALL)
}
}
MenuItem {
text: "Gizmo Scale Look"
Slider {
id: gizmoScaleLookSlider
anchors.right: parent.right
anchors.rightMargin: 10
height: parent.height
width: parent.width * 0.40
from: 0.06
to: 0.30
stepSize: 0.01
value: 0.15
}
}
}
/***** GIZMO'S BASIC COMPONENTS *****/
Entity {
id: centerSphereEntity
components: [centerSphereMesh, centerSphereMaterial, frontLayerComponent]
SphereMesh {
id: centerSphereMesh
radius: 0.04
rings: 8
slices: 8
}
PhongMaterial {
id: centerSphereMaterial
property color base: "white"
ambient: base
shininess: 0.2
}
}
// AXIS GIZMO INSTANTIATOR => X, Y and Z
NodeInstantiator {
model: 3
Entity {
id: axisContainer
property int axis : {
switch(index) {
case 0: return TransformGizmo.Axis.X
case 1: return TransformGizmo.Axis.Y
case 2: return TransformGizmo.Axis.Z
}
}
property color baseColor: {
switch(axis) {
case TransformGizmo.Axis.X: return "#e63b55" // Red
case TransformGizmo.Axis.Y: return "#83c414" // Green
case TransformGizmo.Axis.Z: return "#3387e2" // Blue
}
}
property real lineRadius: 0.011
// SCALE ENTITY
Entity {
id: scaleEntity
Entity {
id: axisCylinder
components: [cylinderMesh, cylinderTransform, scaleMaterial, frontLayerComponent]
CylinderMesh {
id: cylinderMesh
length: 0.5
radius: axisContainer.lineRadius
rings: 2
slices: 16
}
Transform {
id: cylinderTransform
matrix: {
const offset = cylinderMesh.length/2 + centerSphereMesh.radius
const m = Qt.matrix4x4()
switch(axis) {
case TransformGizmo.Axis.X: {
m.translate(Qt.vector3d(offset, 0, 0))
m.rotate(90, Qt.vector3d(0,0,1))
break
}
case TransformGizmo.Axis.Y: {
m.translate(Qt.vector3d(0, offset, 0))
break
}
case TransformGizmo.Axis.Z: {
m.translate(Qt.vector3d(0, 0, offset))
m.rotate(90, Qt.vector3d(1,0,0))
break
}
}
return m
}
}
}
Entity {
id: axisScaleBox
components: [cubeScaleMesh, cubeScaleTransform, scaleMaterial, scalePicker, frontLayerComponent]
CuboidMesh {
id: cubeScaleMesh
property real edge: 0.06
xExtent: edge
yExtent: edge
zExtent: edge
}
Transform {
id: cubeScaleTransform
matrix: {
const offset = cylinderMesh.length + centerSphereMesh.radius
const m = Qt.matrix4x4()
switch(axis) {
case TransformGizmo.Axis.X: {
m.translate(Qt.vector3d(offset, 0, 0))
m.rotate(90, Qt.vector3d(0,0,1))
break
}
case TransformGizmo.Axis.Y: {
m.translate(Qt.vector3d(0, offset, 0))
break
}
case TransformGizmo.Axis.Z: {
m.translate(Qt.vector3d(0, 0, offset))
m.rotate(90, Qt.vector3d(1,0,0))
break
}
}
return m
}
}
}
PhongMaterial {
id: scaleMaterial
ambient: baseColor
}
TransformGizmoPicker {
id: scalePicker
mouseController: mouseHandler
gizmoMaterial: scaleMaterial
gizmoBaseColor: baseColor
gizmoAxis: axis
gizmoType: TransformGizmo.Type.SCALE
onPickedChanged: {
// Save the current transformations of the OBJECT
this.modelMatrix = Transformations3DHelper.modelMatrixToMatrices(objectTransform.matrix)
// Compute a scale unit at picking time
this.scaleUnit = Transformations3DHelper.computeScaleUnitFromModelMatrix(convertAxisEnum(gizmoAxis), gizmoDisplayTransform.matrix, camera, root.windowSize)
// Prevent camera transformations
root.pickedChanged(picker.isPressed)
}
}
}
// TRANSLATION ENTITY
Entity {
id: positionEntity
components: [coneMesh, coneTransform, positionMaterial, positionPicker, frontLayerComponent]
ConeMesh {
id: coneMesh
bottomRadius : 0.035
topRadius : 0.001
hasBottomEndcap : true
hasTopEndcap : true
length : 0.13
rings : 2
slices : 8
}
Transform {
id: coneTransform
matrix: {
const offset = cylinderMesh.length + centerSphereMesh.radius + 0.4
const m = Qt.matrix4x4()
switch(axis) {
case TransformGizmo.Axis.X: {
m.translate(Qt.vector3d(offset, 0, 0))
m.rotate(-90, Qt.vector3d(0,0,1))
break
}
case TransformGizmo.Axis.Y: {
m.translate(Qt.vector3d(0, offset, 0))
break
}
case TransformGizmo.Axis.Z: {
m.translate(Qt.vector3d(0, 0, offset))
m.rotate(90, Qt.vector3d(1,0,0))
break
}
}
return m
}
}
PhongMaterial {
id: positionMaterial
ambient: baseColor
}
TransformGizmoPicker {
id: positionPicker
mouseController: mouseHandler
gizmoMaterial: positionMaterial
gizmoBaseColor: baseColor
gizmoAxis: axis
gizmoType: TransformGizmo.Type.TRANSLATION
onPickedChanged: {
// Save the current transformations of the OBJECT
this.modelMatrix = Transformations3DHelper.modelMatrixToMatrices(objectTransform.matrix)
// Compute a scale unit at picking time
this.scaleUnit = Transformations3DHelper.computeScaleUnitFromModelMatrix(convertAxisEnum(gizmoAxis), gizmoDisplayTransform.matrix, camera, root.windowSize)
// Prevent camera transformations
root.pickedChanged(picker.isPressed)
}
}
}
// ROTATION ENTITY
Entity {
id: rotationEntity
components: [torusMesh, torusTransform, rotationMaterial, rotationPicker, frontLayerComponent]
TorusMesh {
id: torusMesh
radius: cylinderMesh.length + 0.25
minorRadius: axisContainer.lineRadius
slices: 8
rings: 32
}
Transform {
id: torusTransform
matrix: {
const scaleDiff = 2*torusMesh.minorRadius + 0.01 // Just to make sure there is no face overlapping
const m = Qt.matrix4x4()
switch(axis) {
case TransformGizmo.Axis.X: m.rotate(90, Qt.vector3d(0,1,0)); break
case TransformGizmo.Axis.Y: m.rotate(90, Qt.vector3d(1,0,0)); m.scale(Qt.vector3d(1-scaleDiff, 1-scaleDiff, 1-scaleDiff)); break
case TransformGizmo.Axis.Z: m.scale(Qt.vector3d(1-2*scaleDiff, 1-2*scaleDiff, 1-2*scaleDiff)); break
}
return m
}
}
PhongMaterial {
id: rotationMaterial
ambient: baseColor
}
TransformGizmoPicker {
id: rotationPicker
mouseController: mouseHandler
gizmoMaterial: rotationMaterial
gizmoBaseColor: baseColor
gizmoAxis: axis
gizmoType: TransformGizmo.Type.ROTATION
onPickedChanged: {
// Save the current transformations of the OBJECT
this.modelMatrix = Transformations3DHelper.modelMatrixToMatrices(objectTransform.matrix)
// No need to compute a scale unit for rotation
// Prevent camera transformations
root.pickedChanged(picker.isPressed)
}
}
}
}
}
}

View file

@ -0,0 +1,46 @@
import Qt3D.Core 2.0
import Qt3D.Render 2.9
import Qt3D.Input 2.0
import Qt3D.Extras 2.10
import QtQuick 2.9
import Qt3D.Logic 2.0
ObjectPicker {
id: root
property bool isPressed : false
property MouseHandler mouseController
property var gizmoMaterial
property color gizmoBaseColor
property int gizmoAxis
property int gizmoType
property point screenPoint
property var modelMatrix
property real scaleUnit
property int button
signal pickedChanged(var picker)
hoverEnabled: true
onPressed: {
mouseController.enabled = true
mouseController.objectPicker = this
root.isPressed = true
screenPoint = pick.position
button = pick.button
pickedChanged(this)
}
onEntered: {
gizmoMaterial.ambient = "white"
}
onExited: {
if(!isPressed) gizmoMaterial.ambient = gizmoBaseColor
}
onReleased: {
gizmoMaterial.ambient = gizmoBaseColor
root.isPressed = false
mouseController.objectPicker = null
mouseController.enabled = false
pickedChanged(this)
}
}

View file

@ -207,6 +207,17 @@ FocusScope {
]
}
}
LayerFilter {
filterMode: LayerFilter.DiscardAnyMatchingLayers
layers: Layer {id: drawOnFront}
}
LayerFilter {
filterMode: LayerFilter.AcceptAnyMatchingLayers
layers: [drawOnFront]
RenderStateSet {
renderStates: DepthTest { depthFunction: DepthTest.GreaterOrEqual }
}
}
}
}
}
@ -223,6 +234,11 @@ FocusScope {
pickingEnabled: cameraController.pickingActive || doubleClickTimer.running
camera: cameraSelector.camera
// Used for TransformGizmo in BoundingBox
sceneCameraController: cameraController
frontLayerComponent: drawOnFront
window: root
components: [
Transform {
id: transform

View file

@ -453,6 +453,10 @@ class Reconstruction(UIGraph):
self.setDefaultPipeline(defaultPipeline)
def clear(self):
self.clearActiveNodes()
super(Reconstruction, self).clear()
def setDefaultPipeline(self, defaultPipeline):
self._defaultPipeline = defaultPipeline
@ -463,6 +467,10 @@ class Reconstruction(UIGraph):
for nodeType, _ in meshroom.core.nodesDesc.items():
self._activeNodes.add(ActiveNode(nodeType, self))
def clearActiveNodes(self):
for key in self._activeNodes.keys():
self._activeNodes.get(key).node = None
def onCameraInitChanged(self):
# Update active nodes when CameraInit changes
nodes = self._graph.nodesFromNode(self._cameraInit)[0]
@ -559,7 +567,7 @@ class Reconstruction(UIGraph):
def getViewpoints(self):
""" Return the Viewpoints model. """
# TODO: handle multiple Viewpoints models
return self._cameraInit.viewpoints.value if self._cameraInit else None
return self._cameraInit.viewpoints.value if self._cameraInit else QObjectListModel(parent=self)
def updateCameraInits(self):
cameraInits = self._graph.nodesByType("CameraInit", sortedByIndex=True)