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 BaseObject = None
Variant = None Variant = None
VariantList = None VariantList = None
JSValue = None
if meshroom.backend == meshroom.Backend.PYSIDE: if meshroom.backend == meshroom.Backend.PYSIDE:
# PySide types # 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: elif meshroom.backend == meshroom.Backend.STANDALONE:
# Core types # 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: class _BaseModel:

View file

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

View file

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

View file

@ -269,6 +269,7 @@ class Attribute(BaseObject):
hasOutputConnections = Property(bool, hasOutputConnections.fget, notify=hasOutputConnectionsChanged) hasOutputConnections = Property(bool, hasOutputConnections.fget, notify=hasOutputConnectionsChanged)
isDefault = Property(bool, _isDefault, notify=valueChanged) isDefault = Property(bool, _isDefault, notify=valueChanged)
linkParam = Property(BaseObject, getLinkParam, notify=isLinkChanged) linkParam = Property(BaseObject, getLinkParam, notify=isLinkChanged)
rootLinkParam = Property(BaseObject, lambda self: self.getLinkParam(recursive=True), notify=isLinkChanged)
node = Property(BaseObject, node.fget, constant=True) node = Property(BaseObject, node.fget, constant=True)
enabledChanged = Signal() enabledChanged = Signal()
enabled = Property(bool, getEnabled, setEnabled, notify=enabledChanged) enabled = Property(bool, getEnabled, setEnabled, notify=enabledChanged)
@ -312,8 +313,8 @@ class ListAttribute(Attribute):
self._value = value self._value = value
# New value # New value
else: else:
self.desc.validateValue(value) newValue = self.desc.validateValue(value)
self.extend(value) self.extend(newValue)
self.requestGraphUpdate() self.requestGraphUpdate()
@raiseIfLink @raiseIfLink
@ -422,10 +423,16 @@ class GroupAttribute(Attribute):
raise AttributeError(key) raise AttributeError(key)
def _set_value(self, exportedValue): def _set_value(self, exportedValue):
self.desc.validateValue(exportedValue) value = self.desc.validateValue(exportedValue)
if isinstance(value, dict):
# set individual child attribute values # set individual child attribute values
for key, value in exportedValue.items(): for key, v in value.items():
self._value.get(key).value = value 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) @Slot(str, result=Attribute)
def childAttribute(self, key): def childAttribute(self, key):
@ -446,7 +453,7 @@ class GroupAttribute(Attribute):
def uid(self, uidIndex): def uid(self, uidIndex):
uids = [] uids = []
for k, v in self._value.items(): 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)) uids.append(v.uid(uidIndex))
return hashValue(uids) 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 meshroom.core import pyCompatibility
from enum import Enum # available by default in python3. For python2: "pip install enum34" from enum import Enum # available by default in python3. For python2: "pip install enum34"
import math import math
import os import os
import psutil import psutil
import ast
class Attribute(BaseObject): class Attribute(BaseObject):
""" """
@ -32,12 +32,12 @@ class Attribute(BaseObject):
type = Property(str, lambda self: self.__class__.__name__, constant=True) type = Property(str, lambda self: self.__class__.__name__, constant=True)
def validateValue(self, value): def validateValue(self, value):
""" Return validated/conformed 'value'. """ Return validated/conformed 'value'. Need to be implemented in derived classes.
Raises: Raises:
ValueError: if value does not have the proper type 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): def matchDescription(self, value, conform=False):
""" Returns whether the value perfectly match attribute's description. """ 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) joinChar = Property(str, lambda self: self._joinChar, constant=True)
def validateValue(self, value): 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)): if not isinstance(value, (list, tuple)):
raise ValueError('ListAttribute only supports list/tuple input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value))) raise ValueError('ListAttribute only supports list/tuple input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value)))
return value return value
@ -95,12 +103,25 @@ class GroupAttribute(Attribute):
groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True) groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True)
def validateValue(self, value): def validateValue(self, value):
""" Ensure value is a dictionary with keys compatible with the group description. """ """ Ensure value is compatible with the group description and convert value if needed. """
if not isinstance(value, dict): if JSValue is not None and isinstance(value, JSValue):
raise ValueError('GroupAttribute only supports dict input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value))) # 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]) invalidKeys = set(value.keys()).difference([attr.name for attr in self._groupDesc])
if invalidKeys: if invalidKeys:
raise ValueError('Value contains key that does not match group description : {}'.format(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 return value
def matchDescription(self, value, conform=False): def matchDescription(self, value, conform=False):

View file

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

View file

@ -470,6 +470,7 @@ class BaseNode(BaseObject):
self._position = position or Position() self._position = position or Position()
self._attributes = DictModel(keyAttrName='name', parent=self) self._attributes = DictModel(keyAttrName='name', parent=self)
self.attributesPerUid = defaultdict(set) 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._locked = False
self._duplicates = ListModel(parent=self) # list of nodes with the same uid self._duplicates = ListModel(parent=self) # list of nodes with the same uid
@ -566,6 +567,17 @@ class BaseNode(BaseObject):
self._position = value self._position = value
self.positionChanged.emit() 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 @property
def depth(self): def depth(self):
return self.graph.getDepth(self) return self.graph.getDepth(self)
@ -927,6 +939,8 @@ class BaseNode(BaseObject):
globalExecModeChanged = Signal() globalExecModeChanged = Signal()
globalExecMode = Property(str, globalExecMode.fget, notify=globalExecModeChanged) globalExecMode = Property(str, globalExecMode.fget, notify=globalExecModeChanged)
isComputed = Property(bool, _isComputed, notify=globalStatusChanged) isComputed = Property(bool, _isComputed, notify=globalStatusChanged)
aliveChanged = Signal()
alive = Property(bool, alive.fget, alive.fset, notify=aliveChanged)
lockedChanged = Signal() lockedChanged = Signal()
locked = Property(bool, getLocked, setLocked, notify=lockedChanged) locked = Property(bool, getLocked, setLocked, notify=lockedChanged)
duplicatesChanged = Signal() duplicatesChanged = Signal()

View file

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

View file

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

View file

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

View file

@ -1,4 +1,4 @@
__version__ = "5.0" __version__ = "6.0"
from meshroom.core import desc 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='', value='',
uid=[0], 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( desc.BoolParam(
name='estimateSpaceFromSfM', name='estimateSpaceFromSfM',
label='Estimate Space From SfM', label='Estimate Space From SfM',

View file

@ -1,4 +1,4 @@
__version__ = "2.0" __version__ = "3.0"
from meshroom.core import desc from meshroom.core import desc
@ -34,12 +34,13 @@ The transformation can be based on:
label='Transformation Method', label='Transformation Method',
description="Transformation method:\n" description="Transformation method:\n"
" * transformation: Apply a given transformation\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_cameras: Use cameras\n"
" * auto_from_landmarks: Use landmarks\n" " * auto_from_landmarks: Use landmarks\n"
" * from_single_camera: Use a specific camera as the origin of the coordinate system\n" " * from_single_camera: Use a specific camera as the origin of the coordinate system\n"
" * from_markers: Align specific markers to custom coordinates", " * from_markers: Align specific markers to custom coordinates",
value='auto_from_landmarks', 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, exclusive=True,
uid=[0], uid=[0],
), ),
@ -51,6 +52,76 @@ The transformation can be based on:
" * from_single_camera: Camera UID or image filename", " * from_single_camera: Camera UID or image filename",
value='', value='',
uid=[0], 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( desc.ChoiceParam(
name='landmarksDescriberTypes', name='landmarksDescriberTypes',
@ -88,21 +159,24 @@ The transformation can be based on:
label='Scale', label='Scale',
description='Apply scale transformation.', description='Apply scale transformation.',
value=True, value=True,
uid=[0] uid=[0],
enabled=lambda node: node.method.value != "manual",
), ),
desc.BoolParam( desc.BoolParam(
name='applyRotation', name='applyRotation',
label='Rotation', label='Rotation',
description='Apply rotation transformation.', description='Apply rotation transformation.',
value=True, value=True,
uid=[0] uid=[0],
enabled=lambda node: node.method.value != "manual",
), ),
desc.BoolParam( desc.BoolParam(
name='applyTranslation', name='applyTranslation',
label='Translation', label='Translation',
description='Apply translation transformation.', description='Apply translation transformation.',
value=True, value=True,
uid=[0] uid=[0],
enabled=lambda node: node.method.value != "manual",
), ),
desc.ChoiceParam( desc.ChoiceParam(
name='verboseLevel', 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', label='Filter Track Forks',
description='Enable/Disable the track forks removal. A track contains a fork when incoherent matches \n' 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', 'lead to multiple features in the same image for a single track. \n',
value=True, value=False,
uid=[0], uid=[0],
), ),
desc.File( desc.File(

View file

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

View file

@ -4,10 +4,13 @@ def registerTypes():
from meshroom.ui.components.clipboard import ClipboardHelper from meshroom.ui.components.clipboard import ClipboardHelper
from meshroom.ui.components.edge import EdgeMouseArea from meshroom.ui.components.edge import EdgeMouseArea
from meshroom.ui.components.filepath import FilepathHelper 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(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea")
qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable
qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable
qmlRegisterType(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # 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(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.QtCore import QObject, Slot, QSize, Signal, QPointF
from PySide2.Qt3DCore import Qt3DCore from PySide2.Qt3DCore import Qt3DCore
from PySide2.Qt3DRender import Qt3DRender 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 from meshroom.ui.utils import makeProperty
@ -103,3 +103,206 @@ class TrackballController(QObject):
trackballSize = makeProperty(float, '_trackballSize', trackballSizeChanged) trackballSize = makeProperty(float, '_trackballSize', trackballSizeChanged)
rotationSpeedChanged = Signal() rotationSpeedChanged = Signal()
rotationSpeed = makeProperty(float, '_rotationSpeed', rotationSpeedChanged) 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: if self._graph:
self.stopExecution() self.stopExecution()
self.clear() self.clear()
oldGraph = self._graph
self._graph = g self._graph = g
if oldGraph:
oldGraph.deleteLater()
self._graph.updated.connect(self.onGraphUpdated) self._graph.updated.connect(self.onGraphUpdated)
self._graph.update() self._graph.update()
self._taskManager.update(self._graph) self._taskManager.update(self._graph)
@ -328,8 +332,7 @@ class UIGraph(QObject):
self.clearNodeHover() self.clearNodeHover()
self.clearNodeSelection() self.clearNodeSelection()
self._taskManager.clear() self._taskManager.clear()
self._graph.deleteLater() self._graph.clear()
self._graph = None
self._sortedDFSChunks.clear() self._sortedDFSChunks.clear()
self._undoStack.clear() self._undoStack.clear()

View file

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

View file

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

View file

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

View file

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

View file

@ -76,7 +76,7 @@ Panel {
SensorDBDialog { SensorDBDialog {
id: sensorDBDialog id: sensorDBDialog
sensorDatabase: Filepath.stringToUrl(cameraInit.attribute("sensorDatabase").value) sensorDatabase: cameraInit ? Filepath.stringToUrl(cameraInit.attribute("sensorDatabase").value) : ""
readOnly: _reconstruction.computing readOnly: _reconstruction.computing
onUpdateIntrinsicsRequest: _reconstruction.rebuildIntrinsics(cameraInit) 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 Clipboard 1.0 Clipboard.qml
# singleton Filepath 1.0 Filepath.qml # singleton Filepath 1.0 Filepath.qml
# singleton Scene3DHelper 1.0 Scene3DHelper.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 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 { FloatingPane {
id: bottomToolbar id: bottomToolbar
@ -628,6 +640,25 @@ FocusScope {
visible: activeNode 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 { Label {
id: resolutionLabel id: resolutionLabel
Layout.fillWidth: true 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 windowSize: trackball.windowSize
property alias trackballSize: trackball.trackballSize 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 readonly property alias pressed: mouseHandler._pressed
signal mousePressed(var mouse) signal mousePressed(var mouse)
signal mouseReleased(var mouse, var moved) signal mouseReleased(var mouse, var moved)
@ -44,7 +46,7 @@ Entity {
property point lastPosition property point lastPosition
property point currentPosition property point currentPosition
property bool hasMoved property bool hasMoved
sourceDevice: mouseSourceDevice sourceDevice: loseMouseFocus ? null : mouseSourceDevice
onPressed: { onPressed: {
_pressed = true; _pressed = true;
currentPosition.x = lastPosition.x = mouse.x; currentPosition.x = lastPosition.x = mouse.x;
@ -60,6 +62,30 @@ Entity {
onPositionChanged: { onPositionChanged: {
currentPosition.x = mouse.x; currentPosition.x = mouse.x;
currentPosition.y = mouse.y; 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) onDoubleClicked: mouseDoubleClicked(mouse)
onWheel: { 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 // Media label and info
Item { Item {
implicitHeight: childrenRect.height implicitHeight: childrenRect.height

View file

@ -16,6 +16,11 @@ Entity {
property bool pickingEnabled: false property bool pickingEnabled: false
readonly property alias count: instantiator.count // number of instantiated media delegates 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 /// Camera to consider for positionning
property Camera camera: null property Camera camera: null
@ -41,6 +46,10 @@ Entity {
"valid": true, "valid": true,
"label": "", "label": "",
"visible": true, "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": "", "section": "",
"attribute": null, "attribute": null,
"entity": null, "entity": null,
@ -146,7 +155,36 @@ Entity {
id: instantiator id: instantiator
model: m.mediaModel model: m.mediaModel
delegate: MediaLoader { 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 media
MediaLoader {
id: mediaLoader id: mediaLoader
// whether MediaLoader has been fully instantiated by the NodeInstantiator // whether MediaLoader has been fully instantiated by the NodeInstantiator
@ -162,19 +200,30 @@ Entity {
// updated when needed, whether raw source is valid or not // updated when needed, whether raw source is valid or not
// raw source path // raw source path
readonly property string rawSource: attribute ? attribute.value : model.source property string rawSource: attribute ? attribute.value : model.source
// whether dependencies are statified (applies for output/connected input attributes only) // whether dependencies are statified (applies for output/connected input attributes only)
readonly property bool dependencyReady: { readonly property bool dependencyReady: {
if(attribute && attribute.isOutput) if(!attribute)
return attribute.node.globalStatus === "SUCCESS"; // if the node is removed, the attribute will be invalid
if(attribute && attribute.isLink) return false
return attribute.linkParam.node.globalStatus === "SUCCESS";
return true; const rootAttribute = attribute.isLink ? attribute.rootLinkParam : attribute
if(rootAttribute.isOutput)
return rootAttribute.node.globalStatus === "SUCCESS"
return true // is an input param so no dependency
} }
// source based on raw source + dependency status // source based on raw source + dependency status
readonly property string currentSource: dependencyReady ? rawSource : "" property string currentSource: dependencyReady ? rawSource : ""
// source based on currentSource + "requested" property // source based on currentSource + "requested" property
readonly property string finalSource: model.requested ? currentSource : "" 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 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)
}
camera: root.camera camera: root.camera
renderMode: root.renderMode renderMode: root.renderMode
@ -184,8 +233,14 @@ Entity {
// Use the object as NodeInstantiator model to be notified of its deletion // Use the object as NodeInstantiator model to be notified of its deletion
NodeInstantiator { NodeInstantiator {
model: attribute model: attribute
delegate: Entity { objectName: "DestructionWatcher [" + attribute.toString() + "]" } delegate: Entity { objectName: "DestructionWatcher [" + model.toString() + "]" }
onObjectRemoved: remove(idx) onObjectRemoved: remove(index)
}
property bool alive: attribute ? attribute.node.alive : false
onAliveChanged: {
if(!alive && index >= 0)
remove(index)
} }
// 'visible' property drives media loading request // 'visible' property drives media loading request
@ -200,7 +255,7 @@ Entity {
} }
function updateCacheAndModel(forceRequest) { function updateCacheAndModel(forceRequest) {
// don't cache explicitely unloaded media // don't cache explicitly unloaded media
if(model.requested && object && dependencyReady) { if(model.requested && object && dependencyReady) {
// cache current object // cache current object
if(cache.add(Filepath.urlToString(mediaLoader.source), object)); if(cache.add(Filepath.urlToString(mediaLoader.source), object));
@ -221,7 +276,7 @@ Entity {
Component.onCompleted: { Component.onCompleted: {
// keep 'source' -> 'entity' reference // keep 'source' -> 'entity' reference
m.sourceToEntity[modelSource] = mediaLoader; m.sourceToEntity[modelSource] = instantiatedEntity;
// always request media loading when delegate has been created // always request media loading when delegate has been created
updateModel(true); updateModel(true);
// if external media failed to open, remove element from model // if external media failed to open, remove element from model
@ -231,6 +286,10 @@ Entity {
onCurrentSourceChanged: { onCurrentSourceChanged: {
updateCacheAndModel(false) 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: { onFinalSourceChanged: {
@ -258,9 +317,10 @@ Entity {
model[prop] = Qt.binding(function() { return object ? object[prop] : 0; }); model[prop] = Qt.binding(function() { return object ? object[prop] : 0; });
}) })
} }
else if(finalSource) { else if(finalSource && status === Component.Ready) {
// source was valid but no loader was created, remove element // source was valid but no loader was created, remove element
remove(index); // check if component is ready to avoid removing element from the model before adding instance to the node
remove(index)
} }
} }
@ -286,13 +346,53 @@ Entity {
] ]
} }
// 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: { onObjectAdded: {
// notify object that it is now fully instantiated // notify object that it is now fully instantiated
object.fullyInstantiated = true; object.fullyInstantiated = true;
} }
onObjectRemoved: { 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; return;
} }
// clear previously created objet if any // clear previously created object if any
if(object) { if(object) {
object.destroy(); object.destroy();
object = null; 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 pickingEnabled: cameraController.pickingActive || doubleClickTimer.running
camera: cameraSelector.camera camera: cameraSelector.camera
// Used for TransformGizmo in BoundingBox
sceneCameraController: cameraController
frontLayerComponent: drawOnFront
window: root
components: [ components: [
Transform { Transform {
id: transform id: transform

View file

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