mirror of
https://github.com/alicevision/Meshroom.git
synced 2025-06-05 04:12:15 +02:00
Merge remote-tracking branch 'origin/develop' into dev/nodesAndTaskManager
This commit is contained in:
commit
79e1c69d5d
39 changed files with 2060 additions and 209 deletions
|
@ -8,13 +8,14 @@ Property = None
|
|||
BaseObject = None
|
||||
Variant = None
|
||||
VariantList = None
|
||||
JSValue = None
|
||||
|
||||
if meshroom.backend == meshroom.Backend.PYSIDE:
|
||||
# PySide types
|
||||
from .qt import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList
|
||||
from .qt import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList, JSValue
|
||||
elif meshroom.backend == meshroom.Backend.STANDALONE:
|
||||
# Core types
|
||||
from .core import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList
|
||||
from .core import DictModel, ListModel, Slot, Signal, Property, BaseObject, Variant, VariantList, JSValue
|
||||
|
||||
|
||||
class _BaseModel:
|
||||
|
|
|
@ -146,3 +146,4 @@ Property = CoreProperty
|
|||
BaseObject = CoreObject
|
||||
Variant = object
|
||||
VariantList = object
|
||||
JSValue = None
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
from PySide2 import QtCore
|
||||
from PySide2 import QtCore, QtQml
|
||||
import shiboken2
|
||||
|
||||
class QObjectListModel(QtCore.QAbstractListModel):
|
||||
|
@ -375,3 +375,4 @@ Property = QtCore.Property
|
|||
BaseObject = QtCore.QObject
|
||||
Variant = "QVariant"
|
||||
VariantList = "QVariantList"
|
||||
JSValue = QtQml.QJSValue
|
||||
|
|
|
@ -269,6 +269,7 @@ class Attribute(BaseObject):
|
|||
hasOutputConnections = Property(bool, hasOutputConnections.fget, notify=hasOutputConnectionsChanged)
|
||||
isDefault = Property(bool, _isDefault, notify=valueChanged)
|
||||
linkParam = Property(BaseObject, getLinkParam, notify=isLinkChanged)
|
||||
rootLinkParam = Property(BaseObject, lambda self: self.getLinkParam(recursive=True), notify=isLinkChanged)
|
||||
node = Property(BaseObject, node.fget, constant=True)
|
||||
enabledChanged = Signal()
|
||||
enabled = Property(bool, getEnabled, setEnabled, notify=enabledChanged)
|
||||
|
@ -312,8 +313,8 @@ class ListAttribute(Attribute):
|
|||
self._value = value
|
||||
# New value
|
||||
else:
|
||||
self.desc.validateValue(value)
|
||||
self.extend(value)
|
||||
newValue = self.desc.validateValue(value)
|
||||
self.extend(newValue)
|
||||
self.requestGraphUpdate()
|
||||
|
||||
@raiseIfLink
|
||||
|
@ -422,10 +423,16 @@ class GroupAttribute(Attribute):
|
|||
raise AttributeError(key)
|
||||
|
||||
def _set_value(self, exportedValue):
|
||||
self.desc.validateValue(exportedValue)
|
||||
# set individual child attribute values
|
||||
for key, value in exportedValue.items():
|
||||
self._value.get(key).value = value
|
||||
value = self.desc.validateValue(exportedValue)
|
||||
if isinstance(value, dict):
|
||||
# set individual child attribute values
|
||||
for key, v in value.items():
|
||||
self._value.get(key).value = v
|
||||
elif isinstance(value, (list, tuple)):
|
||||
for attrDesc, v in zip(self.desc._groupDesc, value):
|
||||
self._value.get(attrDesc.name).value = v
|
||||
else:
|
||||
raise AttributeError("Failed to set on GroupAttribute: {}".format(str(value)))
|
||||
|
||||
@Slot(str, result=Attribute)
|
||||
def childAttribute(self, key):
|
||||
|
@ -446,7 +453,7 @@ class GroupAttribute(Attribute):
|
|||
def uid(self, uidIndex):
|
||||
uids = []
|
||||
for k, v in self._value.items():
|
||||
if uidIndex in v.desc.uid:
|
||||
if v.enabled and uidIndex in v.desc.uid:
|
||||
uids.append(v.uid(uidIndex))
|
||||
return hashValue(uids)
|
||||
|
||||
|
|
|
@ -1,10 +1,10 @@
|
|||
from meshroom.common import BaseObject, Property, Variant, VariantList
|
||||
from meshroom.common import BaseObject, Property, Variant, VariantList, JSValue
|
||||
from meshroom.core import pyCompatibility
|
||||
from enum import Enum # available by default in python3. For python2: "pip install enum34"
|
||||
import math
|
||||
import os
|
||||
import psutil
|
||||
|
||||
import ast
|
||||
|
||||
class Attribute(BaseObject):
|
||||
"""
|
||||
|
@ -32,12 +32,12 @@ class Attribute(BaseObject):
|
|||
type = Property(str, lambda self: self.__class__.__name__, constant=True)
|
||||
|
||||
def validateValue(self, value):
|
||||
""" Return validated/conformed 'value'.
|
||||
""" Return validated/conformed 'value'. Need to be implemented in derived classes.
|
||||
|
||||
Raises:
|
||||
ValueError: if value does not have the proper type
|
||||
"""
|
||||
return value
|
||||
raise NotImplementedError("Attribute.validateValue is an abstract function that should be implemented in the derived class.")
|
||||
|
||||
def matchDescription(self, value, conform=False):
|
||||
""" Returns whether the value perfectly match attribute's description.
|
||||
|
@ -68,6 +68,14 @@ class ListAttribute(Attribute):
|
|||
joinChar = Property(str, lambda self: self._joinChar, constant=True)
|
||||
|
||||
def validateValue(self, value):
|
||||
if JSValue is not None and isinstance(value, JSValue):
|
||||
# Note: we could use isArray(), property("length").toInt() to retrieve all values
|
||||
raise ValueError("ListAttribute.validateValue: cannot recognize QJSValue. Please, use JSON.stringify(value) in QML.")
|
||||
if isinstance(value, pyCompatibility.basestring):
|
||||
# Alternative solution to set values from QML is to convert values to JSON string
|
||||
# In this case, it works with all data types
|
||||
value = ast.literal_eval(value)
|
||||
|
||||
if not isinstance(value, (list, tuple)):
|
||||
raise ValueError('ListAttribute only supports list/tuple input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value)))
|
||||
return value
|
||||
|
@ -95,12 +103,25 @@ class GroupAttribute(Attribute):
|
|||
groupDesc = Property(Variant, lambda self: self._groupDesc, constant=True)
|
||||
|
||||
def validateValue(self, value):
|
||||
""" Ensure value is a dictionary with keys compatible with the group description. """
|
||||
if not isinstance(value, dict):
|
||||
raise ValueError('GroupAttribute only supports dict input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value)))
|
||||
invalidKeys = set(value.keys()).difference([attr.name for attr in self._groupDesc])
|
||||
if invalidKeys:
|
||||
raise ValueError('Value contains key that does not match group description : {}'.format(invalidKeys))
|
||||
""" Ensure value is compatible with the group description and convert value if needed. """
|
||||
if JSValue is not None and isinstance(value, JSValue):
|
||||
# Note: we could use isArray(), property("length").toInt() to retrieve all values
|
||||
raise ValueError("GroupAttribute.validateValue: cannot recognize QJSValue. Please, use JSON.stringify(value) in QML.")
|
||||
if isinstance(value, pyCompatibility.basestring):
|
||||
# Alternative solution to set values from QML is to convert values to JSON string
|
||||
# In this case, it works with all data types
|
||||
value = ast.literal_eval(value)
|
||||
|
||||
if isinstance(value, dict):
|
||||
invalidKeys = set(value.keys()).difference([attr.name for attr in self._groupDesc])
|
||||
if invalidKeys:
|
||||
raise ValueError('Value contains key that does not match group description : {}'.format(invalidKeys))
|
||||
elif isinstance(value, (list, tuple)):
|
||||
if len(value) != len(self._groupDesc):
|
||||
raise ValueError('Value contains incoherent number of values: desc size: {}, value size: {}'.format(len(self._groupDesc), len(value)))
|
||||
else:
|
||||
raise ValueError('GroupAttribute only supports dict/list/tuple input values (param:{}, value:{}, type:{})'.format(self.name, value, type(value)))
|
||||
|
||||
return value
|
||||
|
||||
def matchDescription(self, value, conform=False):
|
||||
|
@ -169,7 +190,7 @@ class BoolParam(Param):
|
|||
|
||||
def validateValue(self, value):
|
||||
try:
|
||||
return bool(int(value)) # int cast is useful to handle string values ('0', '1')
|
||||
return bool(int(value)) # int cast is useful to handle string values ('0', '1')
|
||||
except:
|
||||
raise ValueError('BoolParam only supports bool value (param:{}, value:{}, type:{})'.format(self.name, value, type(value)))
|
||||
|
||||
|
|
|
@ -223,8 +223,11 @@ class Graph(BaseObject):
|
|||
def clear(self):
|
||||
self.header.clear()
|
||||
self._compatibilityNodes.clear()
|
||||
self._nodes.clear()
|
||||
self._edges.clear()
|
||||
# Tell QML nodes are going to be deleted
|
||||
for node in self._nodes:
|
||||
node.alive = False
|
||||
self._nodes.clear()
|
||||
|
||||
@property
|
||||
def fileFeatures(self):
|
||||
|
@ -437,6 +440,7 @@ class Graph(BaseObject):
|
|||
self.removeEdge(edge.dst)
|
||||
inEdges[edge.dst.getFullName()] = edge.src.getFullName()
|
||||
|
||||
node.alive = False
|
||||
self._nodes.remove(node)
|
||||
self.update()
|
||||
|
||||
|
|
|
@ -470,6 +470,7 @@ class BaseNode(BaseObject):
|
|||
self._position = position or Position()
|
||||
self._attributes = DictModel(keyAttrName='name', parent=self)
|
||||
self.attributesPerUid = defaultdict(set)
|
||||
self._alive = True # for QML side to know if the node can be used or is going to be deleted
|
||||
self._locked = False
|
||||
self._duplicates = ListModel(parent=self) # list of nodes with the same uid
|
||||
|
||||
|
@ -566,6 +567,17 @@ class BaseNode(BaseObject):
|
|||
self._position = value
|
||||
self.positionChanged.emit()
|
||||
|
||||
@property
|
||||
def alive(self):
|
||||
return self._alive
|
||||
|
||||
@alive.setter
|
||||
def alive(self, value):
|
||||
if self._alive == value:
|
||||
return
|
||||
self._alive = value
|
||||
self.aliveChanged.emit()
|
||||
|
||||
@property
|
||||
def depth(self):
|
||||
return self.graph.getDepth(self)
|
||||
|
@ -927,6 +939,8 @@ class BaseNode(BaseObject):
|
|||
globalExecModeChanged = Signal()
|
||||
globalExecMode = Property(str, globalExecMode.fget, notify=globalExecModeChanged)
|
||||
isComputed = Property(bool, _isComputed, notify=globalStatusChanged)
|
||||
aliveChanged = Signal()
|
||||
alive = Property(bool, alive.fget, alive.fset, notify=aliveChanged)
|
||||
lockedChanged = Signal()
|
||||
locked = Property(bool, getLocked, setLocked, notify=lockedChanged)
|
||||
duplicatesChanged = Signal()
|
||||
|
|
|
@ -150,7 +150,12 @@ class LdrToHdrCalibration(desc.CommandLineNode):
|
|||
if not cameraInitOutput.node.hasAttribute('viewpoints'):
|
||||
if cameraInitOutput.node.hasAttribute('input'):
|
||||
cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True)
|
||||
viewpoints = cameraInitOutput.node.viewpoints.value
|
||||
if cameraInitOutput and cameraInitOutput.node and cameraInitOutput.node.hasAttribute('viewpoints'):
|
||||
viewpoints = cameraInitOutput.node.viewpoints.value
|
||||
else:
|
||||
# No connected CameraInit
|
||||
node.nbBrackets.value = 0
|
||||
return
|
||||
|
||||
# logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints)))
|
||||
inputs = []
|
||||
|
@ -184,7 +189,12 @@ class LdrToHdrCalibration(desc.CommandLineNode):
|
|||
exposures = None
|
||||
bracketSizes = set()
|
||||
if len(exposureGroups) == 1:
|
||||
node.nbBrackets.value = 1
|
||||
if len(set(exposureGroups[0])) == 1:
|
||||
# Single exposure and multiple views
|
||||
node.nbBrackets.value = 1
|
||||
else:
|
||||
# Single view and multiple exposures
|
||||
node.nbBrackets.value = len(exposureGroups[0])
|
||||
else:
|
||||
for expGroup in exposureGroups:
|
||||
bracketSizes.add(len(expGroup))
|
||||
|
|
|
@ -180,7 +180,12 @@ class LdrToHdrMerge(desc.CommandLineNode):
|
|||
if not cameraInitOutput.node.hasAttribute('viewpoints'):
|
||||
if cameraInitOutput.node.hasAttribute('input'):
|
||||
cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True)
|
||||
viewpoints = cameraInitOutput.node.viewpoints.value
|
||||
if cameraInitOutput and cameraInitOutput.node and cameraInitOutput.node.hasAttribute('viewpoints'):
|
||||
viewpoints = cameraInitOutput.node.viewpoints.value
|
||||
else:
|
||||
# No connected CameraInit
|
||||
node.nbBrackets.value = 0
|
||||
return
|
||||
|
||||
# logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints)))
|
||||
inputs = []
|
||||
|
@ -214,7 +219,12 @@ class LdrToHdrMerge(desc.CommandLineNode):
|
|||
exposures = None
|
||||
bracketSizes = set()
|
||||
if len(exposureGroups) == 1:
|
||||
node.nbBrackets.value = 1
|
||||
if len(set(exposureGroups[0])) == 1:
|
||||
# Single exposure and multiple views
|
||||
node.nbBrackets.value = 1
|
||||
else:
|
||||
# Single view and multiple exposures
|
||||
node.nbBrackets.value = len(exposureGroups[0])
|
||||
else:
|
||||
for expGroup in exposureGroups:
|
||||
bracketSizes.add(len(expGroup))
|
||||
|
|
|
@ -176,7 +176,12 @@ class LdrToHdrSampling(desc.CommandLineNode):
|
|||
if not cameraInitOutput.node.hasAttribute('viewpoints'):
|
||||
if cameraInitOutput.node.hasAttribute('input'):
|
||||
cameraInitOutput = cameraInitOutput.node.input.getLinkParam(recursive=True)
|
||||
viewpoints = cameraInitOutput.node.viewpoints.value
|
||||
if cameraInitOutput and cameraInitOutput.node and cameraInitOutput.node.hasAttribute('viewpoints'):
|
||||
viewpoints = cameraInitOutput.node.viewpoints.value
|
||||
else:
|
||||
# No connected CameraInit
|
||||
node.nbBrackets.value = 0
|
||||
return
|
||||
|
||||
# logging.info("[LDRToHDR] Update start: nb viewpoints:" + str(len(viewpoints)))
|
||||
inputs = []
|
||||
|
@ -210,7 +215,12 @@ class LdrToHdrSampling(desc.CommandLineNode):
|
|||
exposures = None
|
||||
bracketSizes = set()
|
||||
if len(exposureGroups) == 1:
|
||||
node.nbBrackets.value = 1
|
||||
if len(set(exposureGroups[0])) == 1:
|
||||
# Single exposure and multiple views
|
||||
node.nbBrackets.value = 1
|
||||
else:
|
||||
# Single view and multiple exposures
|
||||
node.nbBrackets.value = len(exposureGroups[0])
|
||||
else:
|
||||
for expGroup in exposureGroups:
|
||||
bracketSizes.add(len(expGroup))
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
__version__ = "5.0"
|
||||
__version__ = "6.0"
|
||||
|
||||
from meshroom.core import desc
|
||||
|
||||
|
@ -35,6 +35,101 @@ A Graph Cut Max-Flow is applied to optimally cut the volume. This cut represents
|
|||
value='',
|
||||
uid=[0],
|
||||
),
|
||||
desc.BoolParam(
|
||||
name='useBoundingBox',
|
||||
label='Custom Bounding Box',
|
||||
description='Edit the meshing bounding box. If enabled, it takes priority over the Estimate From SfM option. Parameters can be adjusted in advanced settings.',
|
||||
value=False,
|
||||
uid=[0],
|
||||
group=''
|
||||
),
|
||||
desc.GroupAttribute(
|
||||
name="boundingBox",
|
||||
label="Bounding Box Settings",
|
||||
description="Translation, rotation and scale of the bounding box.",
|
||||
groupDesc=[
|
||||
desc.GroupAttribute(
|
||||
name="bboxTranslation",
|
||||
label="Translation",
|
||||
description="Position in space.",
|
||||
groupDesc=[
|
||||
desc.FloatParam(
|
||||
name="x", label="x", description="X Offset",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-20.0, 20.0, 0.01)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="y", label="y", description="Y Offset",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-20.0, 20.0, 0.01)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="z", label="z", description="Z Offset",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-20.0, 20.0, 0.01)
|
||||
)
|
||||
],
|
||||
joinChar=","
|
||||
),
|
||||
desc.GroupAttribute(
|
||||
name="bboxRotation",
|
||||
label="Euler Rotation",
|
||||
description="Rotation in Euler degrees.",
|
||||
groupDesc=[
|
||||
desc.FloatParam(
|
||||
name="x", label="x", description="Euler X Rotation",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-90.0, 90.0, 1)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="y", label="y", description="Euler Y Rotation",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-180.0, 180.0, 1)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="z", label="z", description="Euler Z Rotation",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-180.0, 180.0, 1)
|
||||
)
|
||||
],
|
||||
joinChar=","
|
||||
),
|
||||
desc.GroupAttribute(
|
||||
name="bboxScale",
|
||||
label="Scale",
|
||||
description="Scale of the bounding box.",
|
||||
groupDesc=[
|
||||
desc.FloatParam(
|
||||
name="x", label="x", description="X Scale",
|
||||
value=1.0,
|
||||
uid=[0],
|
||||
range=(0.0, 20.0, 0.01)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="y", label="y", description="Y Scale",
|
||||
value=1.0,
|
||||
uid=[0],
|
||||
range=(0.0, 20.0, 0.01)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="z", label="z", description="Z Scale",
|
||||
value=1.0,
|
||||
uid=[0],
|
||||
range=(0.0, 20.0, 0.01)
|
||||
)
|
||||
],
|
||||
joinChar=","
|
||||
)
|
||||
],
|
||||
joinChar=",",
|
||||
enabled=lambda node: node.useBoundingBox.value,
|
||||
),
|
||||
desc.BoolParam(
|
||||
name='estimateSpaceFromSfM',
|
||||
label='Estimate Space From SfM',
|
||||
|
|
|
@ -1,4 +1,4 @@
|
|||
__version__ = "2.0"
|
||||
__version__ = "3.0"
|
||||
|
||||
from meshroom.core import desc
|
||||
|
||||
|
@ -34,12 +34,13 @@ The transformation can be based on:
|
|||
label='Transformation Method',
|
||||
description="Transformation method:\n"
|
||||
" * transformation: Apply a given transformation\n"
|
||||
" * manual: Apply the gizmo transformation (show the transformed input)\n"
|
||||
" * auto_from_cameras: Use cameras\n"
|
||||
" * auto_from_landmarks: Use landmarks\n"
|
||||
" * from_single_camera: Use a specific camera as the origin of the coordinate system\n"
|
||||
" * from_markers: Align specific markers to custom coordinates",
|
||||
value='auto_from_landmarks',
|
||||
values=['transformation', 'auto_from_cameras', 'auto_from_landmarks', 'from_single_camera', 'from_markers'],
|
||||
values=['transformation', 'manual', 'auto_from_cameras', 'auto_from_landmarks', 'from_single_camera', 'from_markers'],
|
||||
exclusive=True,
|
||||
uid=[0],
|
||||
),
|
||||
|
@ -51,6 +52,76 @@ The transformation can be based on:
|
|||
" * from_single_camera: Camera UID or image filename",
|
||||
value='',
|
||||
uid=[0],
|
||||
enabled=lambda node: node.method.value == "transformation" or node.method.value == "from_single_camera",
|
||||
),
|
||||
desc.GroupAttribute(
|
||||
name="manualTransform",
|
||||
label="Manual Transform (Gizmo)",
|
||||
description="Translation, rotation (Euler ZXY) and uniform scale.",
|
||||
groupDesc=[
|
||||
desc.GroupAttribute(
|
||||
name="manualTranslation",
|
||||
label="Translation",
|
||||
description="Translation in space.",
|
||||
groupDesc=[
|
||||
desc.FloatParam(
|
||||
name="x", label="x", description="X Offset",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-20.0, 20.0, 0.01)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="y", label="y", description="Y Offset",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-20.0, 20.0, 0.01)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="z", label="z", description="Z Offset",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-20.0, 20.0, 0.01)
|
||||
)
|
||||
],
|
||||
joinChar=","
|
||||
),
|
||||
desc.GroupAttribute(
|
||||
name="manualRotation",
|
||||
label="Euler Rotation",
|
||||
description="Rotation in Euler degrees.",
|
||||
groupDesc=[
|
||||
desc.FloatParam(
|
||||
name="x", label="x", description="Euler X Rotation",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-90.0, 90.0, 1)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="y", label="y", description="Euler Y Rotation",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-180.0, 180.0, 1)
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="z", label="z", description="Euler Z Rotation",
|
||||
value=0.0,
|
||||
uid=[0],
|
||||
range=(-180.0, 180.0, 1)
|
||||
)
|
||||
],
|
||||
joinChar=","
|
||||
),
|
||||
desc.FloatParam(
|
||||
name="manualScale",
|
||||
label="Scale",
|
||||
description="Uniform Scale.",
|
||||
value=1.0,
|
||||
uid=[0],
|
||||
range=(0.0, 20.0, 0.01)
|
||||
)
|
||||
],
|
||||
joinChar=",",
|
||||
enabled=lambda node: node.method.value == "manual",
|
||||
),
|
||||
desc.ChoiceParam(
|
||||
name='landmarksDescriberTypes',
|
||||
|
@ -88,21 +159,24 @@ The transformation can be based on:
|
|||
label='Scale',
|
||||
description='Apply scale transformation.',
|
||||
value=True,
|
||||
uid=[0]
|
||||
uid=[0],
|
||||
enabled=lambda node: node.method.value != "manual",
|
||||
),
|
||||
desc.BoolParam(
|
||||
name='applyRotation',
|
||||
label='Rotation',
|
||||
description='Apply rotation transformation.',
|
||||
value=True,
|
||||
uid=[0]
|
||||
uid=[0],
|
||||
enabled=lambda node: node.method.value != "manual",
|
||||
),
|
||||
desc.BoolParam(
|
||||
name='applyTranslation',
|
||||
label='Translation',
|
||||
description='Apply translation transformation.',
|
||||
value=True,
|
||||
uid=[0]
|
||||
uid=[0],
|
||||
enabled=lambda node: node.method.value != "manual",
|
||||
),
|
||||
desc.ChoiceParam(
|
||||
name='verboseLevel',
|
||||
|
|
|
@ -283,7 +283,7 @@ It iterates like that, adding cameras and triangulating new 2D features into 3D
|
|||
label='Filter Track Forks',
|
||||
description='Enable/Disable the track forks removal. A track contains a fork when incoherent matches \n'
|
||||
'lead to multiple features in the same image for a single track. \n',
|
||||
value=True,
|
||||
value=False,
|
||||
uid=[0],
|
||||
),
|
||||
desc.File(
|
||||
|
|
|
@ -13,7 +13,7 @@ from meshroom.core import pyCompatibility
|
|||
from meshroom.ui import components
|
||||
from meshroom.ui.components.clipboard import ClipboardHelper
|
||||
from meshroom.ui.components.filepath import FilepathHelper
|
||||
from meshroom.ui.components.scene3D import Scene3DHelper
|
||||
from meshroom.ui.components.scene3D import Scene3DHelper, Transformations3DHelper
|
||||
from meshroom.ui.palette import PaletteManager
|
||||
from meshroom.ui.reconstruction import Reconstruction
|
||||
from meshroom.ui.utils import QmlInstantEngine
|
||||
|
@ -127,6 +127,7 @@ class MeshroomApp(QApplication):
|
|||
# => expose them as context properties instead
|
||||
self.engine.rootContext().setContextProperty("Filepath", FilepathHelper(parent=self))
|
||||
self.engine.rootContext().setContextProperty("Scene3DHelper", Scene3DHelper(parent=self))
|
||||
self.engine.rootContext().setContextProperty("Transformations3DHelper", Transformations3DHelper(parent=self))
|
||||
self.engine.rootContext().setContextProperty("Clipboard", ClipboardHelper(parent=self))
|
||||
|
||||
# additional context properties
|
||||
|
|
|
@ -4,10 +4,13 @@ def registerTypes():
|
|||
from meshroom.ui.components.clipboard import ClipboardHelper
|
||||
from meshroom.ui.components.edge import EdgeMouseArea
|
||||
from meshroom.ui.components.filepath import FilepathHelper
|
||||
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController
|
||||
from meshroom.ui.components.scene3D import Scene3DHelper, TrackballController, Transformations3DHelper
|
||||
from meshroom.ui.components.csvData import CsvData
|
||||
|
||||
qmlRegisterType(EdgeMouseArea, "GraphEditor", 1, 0, "EdgeMouseArea")
|
||||
qmlRegisterType(ClipboardHelper, "Meshroom.Helpers", 1, 0, "ClipboardHelper") # TODO: uncreatable
|
||||
qmlRegisterType(FilepathHelper, "Meshroom.Helpers", 1, 0, "FilepathHelper") # TODO: uncreatable
|
||||
qmlRegisterType(Scene3DHelper, "Meshroom.Helpers", 1, 0, "Scene3DHelper") # TODO: uncreatable
|
||||
qmlRegisterType(Transformations3DHelper, "Meshroom.Helpers", 1, 0, "Transformations3DHelper") # TODO: uncreatable
|
||||
qmlRegisterType(TrackballController, "Meshroom.Helpers", 1, 0, "TrackballController")
|
||||
qmlRegisterType(CsvData, "DataObjects", 1, 0, "CsvData")
|
||||
|
|
117
meshroom/ui/components/csvData.py
Normal file
117
meshroom/ui/components/csvData.py
Normal 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)
|
|
@ -3,7 +3,7 @@ from math import acos, pi, sqrt
|
|||
from PySide2.QtCore import QObject, Slot, QSize, Signal, QPointF
|
||||
from PySide2.Qt3DCore import Qt3DCore
|
||||
from PySide2.Qt3DRender import Qt3DRender
|
||||
from PySide2.QtGui import QVector3D, QQuaternion, QVector2D
|
||||
from PySide2.QtGui import QVector3D, QQuaternion, QVector2D, QVector4D, QMatrix4x4
|
||||
|
||||
from meshroom.ui.utils import makeProperty
|
||||
|
||||
|
@ -103,3 +103,206 @@ class TrackballController(QObject):
|
|||
trackballSize = makeProperty(float, '_trackballSize', trackballSizeChanged)
|
||||
rotationSpeedChanged = Signal()
|
||||
rotationSpeed = makeProperty(float, '_rotationSpeed', rotationSpeedChanged)
|
||||
|
||||
|
||||
class Transformations3DHelper(QObject):
|
||||
|
||||
# ---------- Exposed to QML ---------- #
|
||||
|
||||
@Slot(QVector4D, Qt3DRender.QCamera, QSize, result=QVector2D)
|
||||
def pointFromWorldToScreen(self, point, camera, windowSize):
|
||||
""" Compute the Screen point corresponding to a World Point.
|
||||
Args:
|
||||
point (QVector4D): point in world coordinates
|
||||
camera (QCamera): camera viewing the scene
|
||||
windowSize (QSize): size of the Scene3D window
|
||||
Returns:
|
||||
QVector2D: point in screen coordinates
|
||||
"""
|
||||
# Transform the point from World Coord to Normalized Device Coord
|
||||
viewMatrix = camera.transform().matrix().inverted()
|
||||
projectedPoint = (camera.projectionMatrix() * viewMatrix[0]).map(point)
|
||||
projectedPoint2D = QVector2D(
|
||||
projectedPoint.x()/projectedPoint.w(),
|
||||
projectedPoint.y()/projectedPoint.w()
|
||||
)
|
||||
|
||||
# Transform the point from Normalized Device Coord to Screen Coord
|
||||
screenPoint2D = QVector2D(
|
||||
int((projectedPoint2D.x() + 1) * windowSize.width() / 2),
|
||||
int((projectedPoint2D.y() - 1) * windowSize.height() / -2)
|
||||
)
|
||||
|
||||
return screenPoint2D
|
||||
|
||||
@Slot(Qt3DCore.QTransform, QMatrix4x4, QMatrix4x4, QMatrix4x4, QVector3D)
|
||||
def relativeLocalTranslate(self, transformQtInstance, initialPosMat, initialRotMat, initialScaleMat, translateVec):
|
||||
""" Translate the QTransform in its local space relatively to an initial state.
|
||||
Args:
|
||||
transformQtInstance (QTransform): reference to the Transform to modify
|
||||
initialPosMat (QMatrix4x4): initial position matrix
|
||||
initialRotMat (QMatrix4x4): initial rotation matrix
|
||||
initialScaleMat (QMatrix4x4): initial scale matrix
|
||||
translateVec (QVector3D): vector used for the local translation
|
||||
"""
|
||||
# Compute the translation transformation matrix
|
||||
translationMat = QMatrix4x4()
|
||||
translationMat.translate(translateVec)
|
||||
|
||||
# Compute the new model matrix (POSITION * ROTATION * TRANSLATE * SCALE) and set it to the Transform
|
||||
mat = initialPosMat * initialRotMat * translationMat * initialScaleMat
|
||||
transformQtInstance.setMatrix(mat)
|
||||
|
||||
@Slot(Qt3DCore.QTransform, QMatrix4x4, QQuaternion, QMatrix4x4, QVector3D, int)
|
||||
def relativeLocalRotate(self, transformQtInstance, initialPosMat, initialRotQuat, initialScaleMat, axis, degree):
|
||||
""" Rotate the QTransform in its local space relatively to an initial state.
|
||||
Args:
|
||||
transformQtInstance (QTransform): reference to the Transform to modify
|
||||
initialPosMat (QMatrix4x4): initial position matrix
|
||||
initialRotQuat (QQuaternion): initial rotation quaternion
|
||||
initialScaleMat (QMatrix4x4): initial scale matrix
|
||||
axis (QVector3D): axis to rotate around
|
||||
degree (int): angle of rotation in degree
|
||||
"""
|
||||
# Compute the transformation quaternion from axis and angle in degrees
|
||||
transformQuat = QQuaternion.fromAxisAndAngle(axis, degree)
|
||||
|
||||
# Compute the new rotation quaternion and then calculate the matrix
|
||||
newRotQuat = initialRotQuat * transformQuat # Order is important
|
||||
newRotationMat = self.quaternionToRotationMatrix(newRotQuat)
|
||||
|
||||
# Compute the new model matrix (POSITION * NEW_COMPUTED_ROTATION * SCALE) and set it to the Transform
|
||||
mat = initialPosMat * newRotationMat * initialScaleMat
|
||||
transformQtInstance.setMatrix(mat)
|
||||
|
||||
@Slot(Qt3DCore.QTransform, QMatrix4x4, QMatrix4x4, QMatrix4x4, QVector3D)
|
||||
def relativeLocalScale(self, transformQtInstance, initialPosMat, initialRotMat, initialScaleMat, scaleVec):
|
||||
""" Scale the QTransform in its local space relatively to an initial state.
|
||||
Args:
|
||||
transformQtInstance (QTransform): reference to the Transform to modify
|
||||
initialPosMat (QMatrix4x4): initial position matrix
|
||||
initialRotMat (QMatrix4x4): initial rotation matrix
|
||||
initialScaleMat (QMatrix4x4): initial scale matrix
|
||||
scaleVec (QVector3D): vector used for the relative scale
|
||||
"""
|
||||
# Make a copy of the scale matrix (otherwise, it is a reference and it does not work as expected)
|
||||
scaleMat = self.copyMatrix4x4(initialScaleMat)
|
||||
|
||||
# Update the scale matrix copy (X then Y then Z) with the scaleVec values
|
||||
scaleVecTuple = scaleVec.toTuple()
|
||||
for i in range(3):
|
||||
currentRow = list(scaleMat.row(i).toTuple()) # QVector3D does not implement [] operator or easy way to access value by index so this little hack is required
|
||||
value = currentRow[i] + scaleVecTuple[i]
|
||||
value = value if value >= 0 else -value # Make sure to have only positive scale (because negative scale can make issues with matrix decomposition)
|
||||
currentRow[i] = value
|
||||
|
||||
scaleMat.setRow(i, QVector3D(currentRow[0], currentRow[1], currentRow[2])) # Apply the new row to the scale matrix
|
||||
|
||||
# Compute the new model matrix (POSITION * ROTATION * SCALE) and set it to the Transform
|
||||
mat = initialPosMat * initialRotMat * scaleMat
|
||||
transformQtInstance.setMatrix(mat)
|
||||
|
||||
@Slot(QMatrix4x4, result="QVariant")
|
||||
def modelMatrixToMatrices(self, modelMat):
|
||||
""" Decompose a model matrix into individual matrices.
|
||||
Args:
|
||||
modelMat (QMatrix4x4): model matrix to decompose
|
||||
Returns:
|
||||
QVariant: object containing position, rotation and scale matrices + rotation quaternion
|
||||
"""
|
||||
decomposition = self.decomposeModelMatrix(modelMat)
|
||||
|
||||
posMat = QMatrix4x4()
|
||||
posMat.translate(decomposition.get("translation"))
|
||||
|
||||
rotMat = self.quaternionToRotationMatrix(decomposition.get("quaternion"))
|
||||
|
||||
scaleMat = QMatrix4x4()
|
||||
scaleMat.scale(decomposition.get("scale"))
|
||||
|
||||
return {"position": posMat, "rotation": rotMat, "scale": scaleMat, "quaternion": decomposition.get("quaternion")}
|
||||
|
||||
@Slot(QVector3D, QVector3D, QVector3D, result=QMatrix4x4)
|
||||
def computeModelMatrixWithEuler(self, translation, rotation, scale):
|
||||
""" Compute a model matrix from three Vector3D.
|
||||
Args:
|
||||
translation (QVector3D): position in space (x, y, z)
|
||||
rotation (QVector3D): Euler angles in degrees (x, y, z)
|
||||
scale (QVector3D): scale of the object (x, y, z)
|
||||
Returns:
|
||||
QMatrix4x4: corresponding model matrix
|
||||
"""
|
||||
posMat = QMatrix4x4()
|
||||
posMat.translate(translation)
|
||||
|
||||
quaternion = QQuaternion.fromEulerAngles(rotation)
|
||||
rotMat = self.quaternionToRotationMatrix(quaternion)
|
||||
|
||||
scaleMat = QMatrix4x4()
|
||||
scaleMat.scale(scale)
|
||||
|
||||
modelMat = posMat * rotMat * scaleMat
|
||||
|
||||
return modelMat
|
||||
|
||||
@Slot(QVector3D, QMatrix4x4, Qt3DRender.QCamera, QSize, result=float)
|
||||
def computeScaleUnitFromModelMatrix(self, axis, modelMat, camera, windowSize):
|
||||
""" Compute the length of the screen projected vector axis unit transformed by the model matrix.
|
||||
Args:
|
||||
axis (QVector3D): chosen axis ((1,0,0) or (0,1,0) or (0,0,1))
|
||||
modelMat (QMatrix4x4): model matrix used for the transformation
|
||||
camera (QCamera): camera viewing the scene
|
||||
windowSize (QSize): size of the window in pixels
|
||||
Returns:
|
||||
float: length (in pixels)
|
||||
"""
|
||||
decomposition = self.decomposeModelMatrix(modelMat)
|
||||
|
||||
posMat = QMatrix4x4()
|
||||
posMat.translate(decomposition.get("translation"))
|
||||
|
||||
rotMat = self.quaternionToRotationMatrix(decomposition.get("quaternion"))
|
||||
|
||||
unitScaleModelMat = posMat * rotMat * QMatrix4x4()
|
||||
|
||||
worldCenterPoint = unitScaleModelMat.map(QVector4D(0,0,0,1))
|
||||
worldAxisUnitPoint = unitScaleModelMat.map(QVector4D(axis.x(),axis.y(),axis.z(),1))
|
||||
screenCenter2D = self.pointFromWorldToScreen(worldCenterPoint, camera, windowSize)
|
||||
screenAxisUnitPoint2D = self.pointFromWorldToScreen(worldAxisUnitPoint, camera, windowSize)
|
||||
|
||||
screenVector = QVector2D(screenAxisUnitPoint2D.x() - screenCenter2D.x(), -(screenAxisUnitPoint2D.y() - screenCenter2D.y()))
|
||||
|
||||
value = screenVector.length()
|
||||
return value if (value and value > 10) else 10 # Threshold to avoid problems in extreme case
|
||||
|
||||
# ---------- "Private" Methods ---------- #
|
||||
|
||||
def copyMatrix4x4(self, mat):
|
||||
""" Make a deep copy of a QMatrix4x4. """
|
||||
newMat = QMatrix4x4()
|
||||
for i in range(4):
|
||||
newMat.setRow(i, mat.row(i))
|
||||
return newMat
|
||||
|
||||
def decomposeModelMatrix(self, modelMat):
|
||||
""" Decompose a model matrix into individual component.
|
||||
Args:
|
||||
modelMat (QMatrix4x4): model matrix to decompose
|
||||
Returns:
|
||||
QVariant: object containing translation and scale vectors + rotation quaternion
|
||||
"""
|
||||
translation = modelMat.column(3).toVector3D()
|
||||
quaternion = QQuaternion.fromDirection(modelMat.column(2).toVector3D(), modelMat.column(1).toVector3D())
|
||||
scale = QVector3D(modelMat.column(0).length(), modelMat.column(1).length(), modelMat.column(2).length())
|
||||
|
||||
return {"translation": translation, "quaternion": quaternion, "scale": scale}
|
||||
|
||||
def quaternionToRotationMatrix(self, q):
|
||||
""" Return a rotation matrix from a quaternion. """
|
||||
rotMat3x3 = q.toRotationMatrix()
|
||||
return QMatrix4x4(
|
||||
rotMat3x3(0, 0), rotMat3x3(0, 1), rotMat3x3(0, 2), 0,
|
||||
rotMat3x3(1, 0), rotMat3x3(1, 1), rotMat3x3(1, 2), 0,
|
||||
rotMat3x3(2, 0), rotMat3x3(2, 1), rotMat3x3(2, 2), 0,
|
||||
0, 0, 0, 1
|
||||
)
|
||||
|
|
|
@ -281,7 +281,11 @@ class UIGraph(QObject):
|
|||
if self._graph:
|
||||
self.stopExecution()
|
||||
self.clear()
|
||||
oldGraph = self._graph
|
||||
self._graph = g
|
||||
if oldGraph:
|
||||
oldGraph.deleteLater()
|
||||
|
||||
self._graph.updated.connect(self.onGraphUpdated)
|
||||
self._graph.update()
|
||||
self._taskManager.update(self._graph)
|
||||
|
@ -328,8 +332,7 @@ class UIGraph(QObject):
|
|||
self.clearNodeHover()
|
||||
self.clearNodeSelection()
|
||||
self._taskManager.clear()
|
||||
self._graph.deleteLater()
|
||||
self._graph = None
|
||||
self._graph.clear()
|
||||
self._sortedDFSChunks.clear()
|
||||
self._undoStack.clear()
|
||||
|
||||
|
|
|
@ -23,15 +23,14 @@ RowLayout {
|
|||
readonly property point outputAnchorPos: Qt.point(outputAnchor.x + outputAnchor.width/2,
|
||||
outputAnchor.y + outputAnchor.height/2)
|
||||
|
||||
readonly property bool isList: attribute.type == "ListAttribute"
|
||||
readonly property bool isList: attribute && attribute.type === "ListAttribute"
|
||||
|
||||
signal childPinCreated(var childAttribute, var pin)
|
||||
signal childPinDeleted(var childAttribute, var pin)
|
||||
|
||||
signal pressed(var mouse)
|
||||
|
||||
|
||||
objectName: attribute.name + "."
|
||||
objectName: attribute ? attribute.name + "." : ""
|
||||
layoutDirection: Qt.LeftToRight
|
||||
spacing: 3
|
||||
|
||||
|
@ -58,7 +57,6 @@ RowLayout {
|
|||
border.color: Colors.sysPalette.mid
|
||||
color: Colors.sysPalette.base
|
||||
|
||||
|
||||
Rectangle {
|
||||
visible: inputConnectMA.containsMouse || childrenRepeater.count > 0 || attribute.isLink
|
||||
radius: isList ? 0 : 2
|
||||
|
@ -114,7 +112,7 @@ RowLayout {
|
|||
readonly property string connectorType: "input"
|
||||
readonly property alias attribute: root.attribute
|
||||
readonly property alias nodeItem: root.nodeItem
|
||||
readonly property bool isOutput: attribute.isOutput
|
||||
readonly property bool isOutput: attribute && attribute.isOutput
|
||||
readonly property alias isList: root.isList
|
||||
property bool dragAccepted: false
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
@ -175,8 +173,8 @@ RowLayout {
|
|||
elide: hovered ? Text.ElideNone : Text.ElideMiddle
|
||||
width: hovered ? contentWidth : parent.width
|
||||
font.pointSize: 7
|
||||
horizontalAlignment: attribute.isOutput ? Text.AlignRight : Text.AlignLeft
|
||||
anchors.right: attribute.isOutput ? parent.right : undefined
|
||||
horizontalAlignment: attribute && attribute.isOutput ? Text.AlignRight : Text.AlignLeft
|
||||
anchors.right: attribute && attribute.isOutput ? parent.right : undefined
|
||||
rightPadding: 0
|
||||
color: hovered ? palette.highlight : palette.text
|
||||
}
|
||||
|
|
|
@ -91,15 +91,15 @@ MessageDialog {
|
|||
|
||||
Label {
|
||||
Layout.preferredWidth: 130
|
||||
text: compatibilityNodeDelegate.node.nodeType
|
||||
text: compatibilityNodeDelegate.node ? compatibilityNodeDelegate.node.nodeType : ""
|
||||
}
|
||||
Label {
|
||||
Layout.fillWidth: true
|
||||
text: compatibilityNodeDelegate.node.issueDetails
|
||||
text: compatibilityNodeDelegate.node ? compatibilityNodeDelegate.node.issueDetails : ""
|
||||
}
|
||||
Label {
|
||||
text: compatibilityNodeDelegate.node.canUpgrade ? MaterialIcons.check : MaterialIcons.clear
|
||||
color: compatibilityNodeDelegate.node.canUpgrade ? "#4CAF50" : "#F44336"
|
||||
text: compatibilityNodeDelegate.node && compatibilityNodeDelegate.node.canUpgrade ? MaterialIcons.check : MaterialIcons.clear
|
||||
color: compatibilityNodeDelegate.node && compatibilityNodeDelegate.node.canUpgrade ? "#4CAF50" : "#F44336"
|
||||
font.family: MaterialIcons.fontFamily
|
||||
font.pointSize: 14
|
||||
font.bold: true
|
||||
|
|
|
@ -232,11 +232,11 @@ Item {
|
|||
id: edgesRepeater
|
||||
|
||||
// delay edges loading after nodes (edges needs attribute pins to be created)
|
||||
model: nodeRepeater.loaded ? root.graph.edges : undefined
|
||||
model: nodeRepeater.loaded && root.graph ? root.graph.edges : undefined
|
||||
|
||||
delegate: Edge {
|
||||
property var src: root._attributeToDelegate[edge.src]
|
||||
property var dst: root._attributeToDelegate[edge.dst]
|
||||
property var src: edge ? root._attributeToDelegate[edge.src] : undefined
|
||||
property var dst: edge ? root._attributeToDelegate[edge.dst] : undefined
|
||||
property var srcAnchor: src.nodeItem.mapFromItem(src, src.outputAnchorPos.x, src.outputAnchorPos.y)
|
||||
property var dstAnchor: dst.nodeItem.mapFromItem(dst, dst.inputAnchorPos.x, dst.inputAnchorPos.y)
|
||||
|
||||
|
@ -406,8 +406,8 @@ Item {
|
|||
Repeater {
|
||||
id: nodeRepeater
|
||||
|
||||
model: root.graph.nodes
|
||||
property bool loaded: count === model.count
|
||||
model: root.graph ? root.graph.nodes : undefined
|
||||
property bool loaded: model ? count === model.count : false
|
||||
|
||||
delegate: Node {
|
||||
id: nodeDelegate
|
||||
|
|
|
@ -18,7 +18,7 @@ Item {
|
|||
/// Whether the node can be modified
|
||||
property bool readOnly: node.locked
|
||||
/// Whether the node is in compatibility mode
|
||||
readonly property bool isCompatibilityNode: node.hasOwnProperty("compatibilityIssue")
|
||||
readonly property bool isCompatibilityNode: node ? node.hasOwnProperty("compatibilityIssue") : false
|
||||
/// Mouse related states
|
||||
property bool selected: false
|
||||
property bool hovered: false
|
||||
|
@ -40,11 +40,11 @@ Item {
|
|||
signal attributePinDeleted(var attribute, var pin)
|
||||
|
||||
// use node name as object name to simplify debugging
|
||||
objectName: node.name
|
||||
objectName: node ? node.name : ""
|
||||
|
||||
// initialize position with node coordinates
|
||||
x: root.node.x
|
||||
y: root.node.y
|
||||
x: root.node ? root.node.x : undefined
|
||||
y: root.node ? root.node.y : undefined
|
||||
|
||||
implicitHeight: childrenRect.height
|
||||
|
||||
|
|
|
@ -76,7 +76,7 @@ Panel {
|
|||
|
||||
SensorDBDialog {
|
||||
id: sensorDBDialog
|
||||
sensorDatabase: Filepath.stringToUrl(cameraInit.attribute("sensorDatabase").value)
|
||||
sensorDatabase: cameraInit ? Filepath.stringToUrl(cameraInit.attribute("sensorDatabase").value) : ""
|
||||
readOnly: _reconstruction.computing
|
||||
onUpdateIntrinsicsRequest: _reconstruction.rebuildIntrinsics(cameraInit)
|
||||
}
|
||||
|
|
6
meshroom/ui/qml/Utils/Transformations3DHelper.qml
Normal file
6
meshroom/ui/qml/Utils/Transformations3DHelper.qml
Normal file
|
@ -0,0 +1,6 @@
|
|||
pragma Singleton
|
||||
import Meshroom.Helpers 1.0
|
||||
|
||||
Transformations3DHelper {
|
||||
|
||||
}
|
|
@ -8,3 +8,4 @@ Format 1.0 format.js
|
|||
# singleton Clipboard 1.0 Clipboard.qml
|
||||
# singleton Filepath 1.0 Filepath.qml
|
||||
# singleton Scene3DHelper 1.0 Scene3DHelper.qml
|
||||
# singleton Transformations3DHelper 1.0 Transformations3DHelper.qml
|
||||
|
|
139
meshroom/ui/qml/Viewer/CameraResponseGraph.qml
Normal file
139
meshroom/ui/qml/Viewer/CameraResponseGraph.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -552,6 +552,18 @@ FocusScope {
|
|||
featuresViewer: featuresViewerLoader.item
|
||||
}
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: ldrHdrCalibrationGraph
|
||||
anchors.fill: parent
|
||||
|
||||
property var activeNode: _reconstruction.activeNodes.get('LdrToHdrCalibration').node
|
||||
active: activeNode && activeNode.isComputed && displayLdrHdrCalibrationGraph.checked
|
||||
|
||||
sourceComponent: CameraResponseGraph {
|
||||
ldrHdrCalibrationNode: activeNode
|
||||
}
|
||||
}
|
||||
}
|
||||
FloatingPane {
|
||||
id: bottomToolbar
|
||||
|
@ -628,6 +640,25 @@ FocusScope {
|
|||
visible: activeNode
|
||||
}
|
||||
|
||||
MaterialToolButton {
|
||||
id: displayLdrHdrCalibrationGraph
|
||||
property var activeNode: _reconstruction.activeNodes.get("LdrToHdrCalibration").node
|
||||
property bool isComputed: activeNode && activeNode.isComputed
|
||||
ToolTip.text: "Display Camera Response Function: " + (activeNode ? activeNode.label : "No Node")
|
||||
text: MaterialIcons.timeline
|
||||
font.pointSize: 11
|
||||
Layout.minimumWidth: 0
|
||||
checkable: true
|
||||
checked: false
|
||||
enabled: activeNode && activeNode.isComputed
|
||||
visible: activeNode
|
||||
|
||||
onIsComputedChanged: {
|
||||
if(!isComputed)
|
||||
checked = false
|
||||
}
|
||||
}
|
||||
|
||||
Label {
|
||||
id: resolutionLabel
|
||||
Layout.fillWidth: true
|
||||
|
|
92
meshroom/ui/qml/Viewer3D/BoundingBox.qml
Normal file
92
meshroom/ui/qml/Viewer3D/BoundingBox.qml
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
|
@ -23,6 +23,8 @@ Entity {
|
|||
property alias windowSize: trackball.windowSize
|
||||
property alias trackballSize: trackball.trackballSize
|
||||
|
||||
property bool loseMouseFocus: false // Must be changed by other entities when they want to take mouse focus
|
||||
|
||||
readonly property alias pressed: mouseHandler._pressed
|
||||
signal mousePressed(var mouse)
|
||||
signal mouseReleased(var mouse, var moved)
|
||||
|
@ -44,7 +46,7 @@ Entity {
|
|||
property point lastPosition
|
||||
property point currentPosition
|
||||
property bool hasMoved
|
||||
sourceDevice: mouseSourceDevice
|
||||
sourceDevice: loseMouseFocus ? null : mouseSourceDevice
|
||||
onPressed: {
|
||||
_pressed = true;
|
||||
currentPosition.x = lastPosition.x = mouse.x;
|
||||
|
@ -60,6 +62,30 @@ Entity {
|
|||
onPositionChanged: {
|
||||
currentPosition.x = mouse.x;
|
||||
currentPosition.y = mouse.y;
|
||||
|
||||
const dt = 0.02
|
||||
|
||||
if(panning) { // translate
|
||||
var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.03;
|
||||
var tx = axisMX.value * root.translateSpeed * d;
|
||||
var ty = axisMY.value * root.translateSpeed * d;
|
||||
mouseHandler.hasMoved = true;
|
||||
root.camera.translate(Qt.vector3d(-tx, -ty, 0).times(dt));
|
||||
return;
|
||||
}
|
||||
if(moving){ // trackball rotation
|
||||
trackball.rotate(mouseHandler.lastPosition, mouseHandler.currentPosition, dt);
|
||||
mouseHandler.lastPosition = mouseHandler.currentPosition;
|
||||
mouseHandler.hasMoved = true;
|
||||
return;
|
||||
}
|
||||
if(zooming) { // zoom with alt + RMD
|
||||
var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.1;
|
||||
var tz = axisMX.value * root.translateSpeed * d;
|
||||
mouseHandler.hasMoved = true;
|
||||
root.camera.translate(Qt.vector3d(0, 0, tz).times(dt), Camera.DontTranslateViewCenter)
|
||||
return;
|
||||
}
|
||||
}
|
||||
onDoubleClicked: mouseDoubleClicked(mouse)
|
||||
onWheel: {
|
||||
|
@ -162,32 +188,4 @@ Entity {
|
|||
}
|
||||
]
|
||||
}
|
||||
|
||||
components: [
|
||||
FrameAction {
|
||||
onTriggered: {
|
||||
if(panning) { // translate
|
||||
var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.03;
|
||||
var tx = axisMX.value * root.translateSpeed * d;
|
||||
var ty = axisMY.value * root.translateSpeed * d;
|
||||
mouseHandler.hasMoved = true;
|
||||
root.camera.translate(Qt.vector3d(-tx, -ty, 0).times(dt));
|
||||
return;
|
||||
}
|
||||
if(moving){ // trackball rotation
|
||||
trackball.rotate(mouseHandler.lastPosition, mouseHandler.currentPosition, dt);
|
||||
mouseHandler.lastPosition = mouseHandler.currentPosition;
|
||||
mouseHandler.hasMoved = true;
|
||||
return;
|
||||
}
|
||||
if(zooming) { // zoom with alt + RMD
|
||||
var d = (root.camera.viewCenter.minus(root.camera.position)).length() * 0.1;
|
||||
var tz = axisMX.value * root.translateSpeed * d;
|
||||
mouseHandler.hasMoved = true;
|
||||
root.camera.translate(Qt.vector3d(0, 0, tz).times(dt), Camera.DontTranslateViewCenter)
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
36
meshroom/ui/qml/Viewer3D/EntityWithGizmo.qml
Normal file
36
meshroom/ui/qml/Viewer3D/EntityWithGizmo.qml
Normal 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
|
||||
}
|
|
@ -284,6 +284,38 @@ FloatingPane {
|
|||
}
|
||||
}
|
||||
|
||||
// BoundingBox visibility (if meshing node)
|
||||
MaterialToolButton {
|
||||
visible: model.hasBoundingBox
|
||||
enabled: model.visible
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillHeight: true
|
||||
text: MaterialIcons.transform
|
||||
font.pointSize: 10
|
||||
ToolTip.text: model.displayBoundingBox ? "Hide BBox" : "Show BBox"
|
||||
flat: true
|
||||
opacity: model.visible ? (model.displayBoundingBox ? 1.0 : 0.6) : 0.6
|
||||
onClicked: {
|
||||
model.displayBoundingBox = !model.displayBoundingBox
|
||||
}
|
||||
}
|
||||
|
||||
// Transform visibility (if SfMTransform node)
|
||||
MaterialToolButton {
|
||||
visible: model.hasTransform
|
||||
enabled: model.visible
|
||||
Layout.alignment: Qt.AlignTop
|
||||
Layout.fillHeight: true
|
||||
text: MaterialIcons._3d_rotation
|
||||
font.pointSize: 10
|
||||
ToolTip.text: model.displayTransform ? "Hide Gizmo" : "Show Gizmo"
|
||||
flat: true
|
||||
opacity: model.visible ? (model.displayTransform ? 1.0 : 0.6) : 0.6
|
||||
onClicked: {
|
||||
model.displayTransform = !model.displayTransform
|
||||
}
|
||||
}
|
||||
|
||||
// Media label and info
|
||||
Item {
|
||||
implicitHeight: childrenRect.height
|
||||
|
|
|
@ -16,6 +16,11 @@ Entity {
|
|||
property bool pickingEnabled: false
|
||||
readonly property alias count: instantiator.count // number of instantiated media delegates
|
||||
|
||||
// For TransformGizmo in BoundingBox
|
||||
property DefaultCameraController sceneCameraController
|
||||
property Layer frontLayerComponent
|
||||
property var window
|
||||
|
||||
/// Camera to consider for positionning
|
||||
property Camera camera: null
|
||||
|
||||
|
@ -41,6 +46,10 @@ Entity {
|
|||
"valid": true,
|
||||
"label": "",
|
||||
"visible": true,
|
||||
"hasBoundingBox": false, // for Meshing node only
|
||||
"displayBoundingBox": true, // for Meshing node only
|
||||
"hasTransform": false, // for SfMTransform node only
|
||||
"displayTransform": true, // for SfMTransform node only
|
||||
"section": "",
|
||||
"attribute": null,
|
||||
"entity": null,
|
||||
|
@ -146,144 +155,234 @@ Entity {
|
|||
id: instantiator
|
||||
model: m.mediaModel
|
||||
|
||||
delegate: MediaLoader {
|
||||
id: mediaLoader
|
||||
delegate: Entity {
|
||||
id: instantiatedEntity
|
||||
property alias fullyInstantiated: mediaLoader.fullyInstantiated
|
||||
readonly property alias modelSource: mediaLoader.modelSource
|
||||
|
||||
// whether MediaLoader has been fully instantiated by the NodeInstantiator
|
||||
property bool fullyInstantiated: false
|
||||
// Get the node
|
||||
property var currentNode: model.attribute ? model.attribute.node : null
|
||||
property string nodeType: currentNode ? currentNode.nodeType: null
|
||||
|
||||
// explicitely store some attached model properties for outside use and ease binding
|
||||
readonly property var attribute: model.attribute
|
||||
readonly property int idx: index
|
||||
readonly property var modelSource: attribute || model.source
|
||||
readonly property bool visible: model.visible
|
||||
|
||||
// multi-step binding to ensure MediaLoader source is properly
|
||||
// updated when needed, whether raw source is valid or not
|
||||
|
||||
// raw source path
|
||||
readonly property string rawSource: attribute ? attribute.value : model.source
|
||||
// whether dependencies are statified (applies for output/connected input attributes only)
|
||||
readonly property bool dependencyReady: {
|
||||
if(attribute && attribute.isOutput)
|
||||
return attribute.node.globalStatus === "SUCCESS";
|
||||
if(attribute && attribute.isLink)
|
||||
return attribute.linkParam.node.globalStatus === "SUCCESS";
|
||||
return true;
|
||||
// Specific properties to the MESHING node (declared and initialized for every Entity anyway)
|
||||
property bool hasBoundingBox: {
|
||||
if(nodeType === "Meshing" && currentNode.attribute("useBoundingBox")) // Can have a BoundingBox
|
||||
return currentNode.attribute("useBoundingBox").value
|
||||
return false
|
||||
}
|
||||
// source based on raw source + dependency status
|
||||
readonly property string currentSource: dependencyReady ? rawSource : ""
|
||||
// source based on currentSource + "requested" property
|
||||
readonly property string finalSource: model.requested ? currentSource : ""
|
||||
onHasBoundingBoxChanged: model.hasBoundingBox = hasBoundingBox
|
||||
property bool displayBoundingBox: model.displayBoundingBox
|
||||
|
||||
camera: root.camera
|
||||
renderMode: root.renderMode
|
||||
enabled: visible
|
||||
|
||||
// QObject.destroyed signal is not accessible
|
||||
// Use the object as NodeInstantiator model to be notified of its deletion
|
||||
NodeInstantiator {
|
||||
model: attribute
|
||||
delegate: Entity { objectName: "DestructionWatcher [" + attribute.toString() + "]" }
|
||||
onObjectRemoved: remove(idx)
|
||||
// Specific properties to the SFMTRANSFORM node (declared and initialized for every Entity anyway)
|
||||
property bool hasTransform: {
|
||||
if(nodeType === "SfMTransform" && currentNode.attribute("method")) // Can have a Transform
|
||||
return currentNode.attribute("method").value === "manual"
|
||||
return false
|
||||
}
|
||||
onHasTransformChanged: model.hasTransform = hasTransform
|
||||
property bool displayTransform: model.displayTransform
|
||||
|
||||
// 'visible' property drives media loading request
|
||||
onVisibleChanged: {
|
||||
// always request media loading if visible
|
||||
if(model.visible)
|
||||
model.requested = true;
|
||||
// only cancel loading request if media is not valid
|
||||
// (a media won't be unloaded if already loaded, only hidden)
|
||||
else if(!model.valid)
|
||||
model.requested = false;
|
||||
}
|
||||
|
||||
function updateCacheAndModel(forceRequest) {
|
||||
// don't cache explicitely unloaded media
|
||||
if(model.requested && object && dependencyReady) {
|
||||
// cache current object
|
||||
if(cache.add(Filepath.urlToString(mediaLoader.source), object));
|
||||
object = null;
|
||||
// Create the media
|
||||
MediaLoader {
|
||||
id: mediaLoader
|
||||
|
||||
// whether MediaLoader has been fully instantiated by the NodeInstantiator
|
||||
property bool fullyInstantiated: false
|
||||
|
||||
// explicitely store some attached model properties for outside use and ease binding
|
||||
readonly property var attribute: model.attribute
|
||||
readonly property int idx: index
|
||||
readonly property var modelSource: attribute || model.source
|
||||
readonly property bool visible: model.visible
|
||||
|
||||
// multi-step binding to ensure MediaLoader source is properly
|
||||
// updated when needed, whether raw source is valid or not
|
||||
|
||||
// raw source path
|
||||
property string rawSource: attribute ? attribute.value : model.source
|
||||
// whether dependencies are statified (applies for output/connected input attributes only)
|
||||
readonly property bool dependencyReady: {
|
||||
if(!attribute)
|
||||
// if the node is removed, the attribute will be invalid
|
||||
return false
|
||||
|
||||
const rootAttribute = attribute.isLink ? attribute.rootLinkParam : attribute
|
||||
if(rootAttribute.isOutput)
|
||||
return rootAttribute.node.globalStatus === "SUCCESS"
|
||||
return true // is an input param so no dependency
|
||||
}
|
||||
updateModel(forceRequest);
|
||||
}
|
||||
// source based on raw source + dependency status
|
||||
property string currentSource: dependencyReady ? rawSource : ""
|
||||
// source based on currentSource + "requested" property
|
||||
property string finalSource: model.requested ? currentSource : ""
|
||||
|
||||
function updateModel(forceRequest) {
|
||||
// update model's source path if input is an attribute
|
||||
if(attribute) {
|
||||
model.source = rawSource;
|
||||
// To use only if we want to draw the input source and not the current node output (Warning: to use with caution)
|
||||
// There is maybe a better way to do this to avoid overwritting bindings which should be readonly properties
|
||||
function drawInputSource() {
|
||||
rawSource = Qt.binding(() => instantiatedEntity.currentNode ? instantiatedEntity.currentNode.attribute("input").value: "")
|
||||
currentSource = Qt.binding(() => rawSource)
|
||||
finalSource = Qt.binding(() => rawSource)
|
||||
}
|
||||
// auto-restore entity if raw source is in cache
|
||||
model.requested = forceRequest || (!model.valid && model.requested) || cache.contains(rawSource);
|
||||
model.valid = Filepath.exists(rawSource) && dependencyReady;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// keep 'source' -> 'entity' reference
|
||||
m.sourceToEntity[modelSource] = mediaLoader;
|
||||
// always request media loading when delegate has been created
|
||||
updateModel(true);
|
||||
// if external media failed to open, remove element from model
|
||||
if(!attribute && !object)
|
||||
remove(index)
|
||||
}
|
||||
camera: root.camera
|
||||
renderMode: root.renderMode
|
||||
enabled: visible
|
||||
|
||||
onCurrentSourceChanged: {
|
||||
updateCacheAndModel(false)
|
||||
}
|
||||
// QObject.destroyed signal is not accessible
|
||||
// Use the object as NodeInstantiator model to be notified of its deletion
|
||||
NodeInstantiator {
|
||||
model: attribute
|
||||
delegate: Entity { objectName: "DestructionWatcher [" + model.toString() + "]" }
|
||||
onObjectRemoved: remove(index)
|
||||
}
|
||||
|
||||
onFinalSourceChanged: {
|
||||
// update media visibility
|
||||
// (useful if media was explicitly unloaded or hidden but loaded back from cache)
|
||||
model.visible = model.requested;
|
||||
property bool alive: attribute ? attribute.node.alive : false
|
||||
onAliveChanged: {
|
||||
if(!alive && index >= 0)
|
||||
remove(index)
|
||||
}
|
||||
|
||||
var cachedObject = cache.pop(rawSource);
|
||||
cached = cachedObject !== undefined;
|
||||
if(cached) {
|
||||
object = cachedObject;
|
||||
// only change cached object parent if mediaLoader has been fully instantiated
|
||||
// by the NodeInstantiator; otherwise re-parenting will fail silently and the object will disappear...
|
||||
// see "onFullyInstantiatedChanged" and parent NodeInstantiator's "onObjectAdded"
|
||||
if(fullyInstantiated) {
|
||||
object.parent = mediaLoader;
|
||||
// 'visible' property drives media loading request
|
||||
onVisibleChanged: {
|
||||
// always request media loading if visible
|
||||
if(model.visible)
|
||||
model.requested = true;
|
||||
// only cancel loading request if media is not valid
|
||||
// (a media won't be unloaded if already loaded, only hidden)
|
||||
else if(!model.valid)
|
||||
model.requested = false;
|
||||
}
|
||||
|
||||
function updateCacheAndModel(forceRequest) {
|
||||
// don't cache explicitly unloaded media
|
||||
if(model.requested && object && dependencyReady) {
|
||||
// cache current object
|
||||
if(cache.add(Filepath.urlToString(mediaLoader.source), object));
|
||||
object = null;
|
||||
}
|
||||
updateModel(forceRequest);
|
||||
}
|
||||
|
||||
function updateModel(forceRequest) {
|
||||
// update model's source path if input is an attribute
|
||||
if(attribute) {
|
||||
model.source = rawSource;
|
||||
}
|
||||
// auto-restore entity if raw source is in cache
|
||||
model.requested = forceRequest || (!model.valid && model.requested) || cache.contains(rawSource);
|
||||
model.valid = Filepath.exists(rawSource) && dependencyReady;
|
||||
}
|
||||
|
||||
Component.onCompleted: {
|
||||
// keep 'source' -> 'entity' reference
|
||||
m.sourceToEntity[modelSource] = instantiatedEntity;
|
||||
// always request media loading when delegate has been created
|
||||
updateModel(true);
|
||||
// if external media failed to open, remove element from model
|
||||
if(!attribute && !object)
|
||||
remove(index)
|
||||
}
|
||||
|
||||
onCurrentSourceChanged: {
|
||||
updateCacheAndModel(false)
|
||||
|
||||
// Avoid the bounding box to disappear when we move it after a mesh already computed
|
||||
if(instantiatedEntity.hasBoundingBox && !currentSource)
|
||||
model.visible = true
|
||||
}
|
||||
|
||||
onFinalSourceChanged: {
|
||||
// update media visibility
|
||||
// (useful if media was explicitly unloaded or hidden but loaded back from cache)
|
||||
model.visible = model.requested;
|
||||
|
||||
var cachedObject = cache.pop(rawSource);
|
||||
cached = cachedObject !== undefined;
|
||||
if(cached) {
|
||||
object = cachedObject;
|
||||
// only change cached object parent if mediaLoader has been fully instantiated
|
||||
// by the NodeInstantiator; otherwise re-parenting will fail silently and the object will disappear...
|
||||
// see "onFullyInstantiatedChanged" and parent NodeInstantiator's "onObjectAdded"
|
||||
if(fullyInstantiated) {
|
||||
object.parent = mediaLoader;
|
||||
}
|
||||
}
|
||||
mediaLoader.source = Filepath.stringToUrl(finalSource);
|
||||
if(object) {
|
||||
// bind media info to corresponding model roles
|
||||
// (test for object validity to avoid error messages right after object has been deleted)
|
||||
var boundProperties = ["vertexCount", "faceCount", "cameraCount", "textureCount"];
|
||||
boundProperties.forEach( function(prop){
|
||||
model[prop] = Qt.binding(function() { return object ? object[prop] : 0; });
|
||||
})
|
||||
}
|
||||
else if(finalSource && status === Component.Ready) {
|
||||
// source was valid but no loader was created, remove element
|
||||
// check if component is ready to avoid removing element from the model before adding instance to the node
|
||||
remove(index)
|
||||
}
|
||||
}
|
||||
mediaLoader.source = Filepath.stringToUrl(finalSource);
|
||||
if(object) {
|
||||
// bind media info to corresponding model roles
|
||||
// (test for object validity to avoid error messages right after object has been deleted)
|
||||
var boundProperties = ["vertexCount", "faceCount", "cameraCount", "textureCount"];
|
||||
boundProperties.forEach( function(prop){
|
||||
model[prop] = Qt.binding(function() { return object ? object[prop] : 0; });
|
||||
})
|
||||
|
||||
onFullyInstantiatedChanged: {
|
||||
// delayed reparenting of object coming from the cache
|
||||
if(object)
|
||||
object.parent = mediaLoader;
|
||||
}
|
||||
else if(finalSource) {
|
||||
// source was valid but no loader was created, remove element
|
||||
remove(index);
|
||||
|
||||
onStatusChanged: {
|
||||
model.status = status
|
||||
// remove model entry for external media that failed to load
|
||||
if(status === SceneLoader.Error && !model.attribute)
|
||||
remove(index);
|
||||
}
|
||||
|
||||
components: [
|
||||
ObjectPicker {
|
||||
enabled: mediaLoader.enabled && pickingEnabled
|
||||
hoverEnabled: false
|
||||
onPressed: root.pressed(pick)
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
// Transform: display a TransformGizmo for SfMTransform node only
|
||||
// note: use a NodeInstantiator to evaluate if the current node is a SfMTransform node and if the transform mode is set to Manual
|
||||
NodeInstantiator {
|
||||
id: sfmTransformGizmoInstantiator
|
||||
active: instantiatedEntity.hasTransform
|
||||
model: 1
|
||||
|
||||
SfMTransformGizmo {
|
||||
id: sfmTransformGizmoEntity
|
||||
sceneCameraController: root.sceneCameraController
|
||||
frontLayerComponent: root.frontLayerComponent
|
||||
window: root.window
|
||||
currentSfMTransformNode: instantiatedEntity.currentNode
|
||||
enabled: mediaLoader.visible && instantiatedEntity.displayTransform
|
||||
|
||||
Component.onCompleted: {
|
||||
mediaLoader.drawInputSource() // Because we are sure we want to show the input in MANUAL mode only
|
||||
Scene3DHelper.addComponent(mediaLoader, sfmTransformGizmoEntity.objectTransform) // Add the transform to the media to see real-time transformations
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
onFullyInstantiatedChanged: {
|
||||
// delayed reparenting of object coming from the cache
|
||||
if(object)
|
||||
object.parent = mediaLoader;
|
||||
}
|
||||
// BoundingBox: display bounding box for MESHING computation
|
||||
// note: use a NodeInstantiator to evaluate if the current node is a MESHING node and if the checkbox is active
|
||||
NodeInstantiator {
|
||||
id: boundingBoxInstantiator
|
||||
active: instantiatedEntity.hasBoundingBox
|
||||
model: 1
|
||||
|
||||
onStatusChanged: {
|
||||
model.status = status
|
||||
// remove model entry for external media that failed to load
|
||||
if(status === SceneLoader.Error && !model.attribute)
|
||||
remove(index);
|
||||
}
|
||||
|
||||
components: [
|
||||
ObjectPicker {
|
||||
enabled: mediaLoader.enabled && pickingEnabled
|
||||
hoverEnabled: false
|
||||
onPressed: root.pressed(pick)
|
||||
MeshingBoundingBox {
|
||||
sceneCameraController: root.sceneCameraController
|
||||
frontLayerComponent: root.frontLayerComponent
|
||||
window: root.window
|
||||
currentMeshingNode: instantiatedEntity.currentNode
|
||||
enabled: mediaLoader.visible && instantiatedEntity.displayBoundingBox
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
onObjectAdded: {
|
||||
|
@ -292,7 +391,8 @@ Entity {
|
|||
}
|
||||
|
||||
onObjectRemoved: {
|
||||
delete m.sourceToEntity[object.modelSource];
|
||||
if(m.sourceToEntity[object.modelSource])
|
||||
delete m.sourceToEntity[object.modelSource]
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -31,7 +31,7 @@ import Utils 1.0
|
|||
return;
|
||||
}
|
||||
|
||||
// clear previously created objet if any
|
||||
// clear previously created object if any
|
||||
if(object) {
|
||||
object.destroy();
|
||||
object = null;
|
||||
|
|
91
meshroom/ui/qml/Viewer3D/MeshingBoundingBox.qml
Normal file
91
meshroom/ui/qml/Viewer3D/MeshingBoundingBox.qml
Normal 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 }
|
||||
}
|
||||
}
|
88
meshroom/ui/qml/Viewer3D/SfMTransformGizmo.qml
Normal file
88
meshroom/ui/qml/Viewer3D/SfMTransformGizmo.qml
Normal 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)
|
||||
}
|
||||
}
|
594
meshroom/ui/qml/Viewer3D/TransformGizmo.qml
Normal file
594
meshroom/ui/qml/Viewer3D/TransformGizmo.qml
Normal 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)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
46
meshroom/ui/qml/Viewer3D/TransformGizmoPicker.qml
Normal file
46
meshroom/ui/qml/Viewer3D/TransformGizmoPicker.qml
Normal 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)
|
||||
}
|
||||
}
|
|
@ -207,6 +207,17 @@ FocusScope {
|
|||
]
|
||||
}
|
||||
}
|
||||
LayerFilter {
|
||||
filterMode: LayerFilter.DiscardAnyMatchingLayers
|
||||
layers: Layer {id: drawOnFront}
|
||||
}
|
||||
LayerFilter {
|
||||
filterMode: LayerFilter.AcceptAnyMatchingLayers
|
||||
layers: [drawOnFront]
|
||||
RenderStateSet {
|
||||
renderStates: DepthTest { depthFunction: DepthTest.GreaterOrEqual }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -223,6 +234,11 @@ FocusScope {
|
|||
pickingEnabled: cameraController.pickingActive || doubleClickTimer.running
|
||||
camera: cameraSelector.camera
|
||||
|
||||
// Used for TransformGizmo in BoundingBox
|
||||
sceneCameraController: cameraController
|
||||
frontLayerComponent: drawOnFront
|
||||
window: root
|
||||
|
||||
components: [
|
||||
Transform {
|
||||
id: transform
|
||||
|
|
|
@ -453,6 +453,10 @@ class Reconstruction(UIGraph):
|
|||
|
||||
self.setDefaultPipeline(defaultPipeline)
|
||||
|
||||
def clear(self):
|
||||
self.clearActiveNodes()
|
||||
super(Reconstruction, self).clear()
|
||||
|
||||
def setDefaultPipeline(self, defaultPipeline):
|
||||
self._defaultPipeline = defaultPipeline
|
||||
|
||||
|
@ -463,6 +467,10 @@ class Reconstruction(UIGraph):
|
|||
for nodeType, _ in meshroom.core.nodesDesc.items():
|
||||
self._activeNodes.add(ActiveNode(nodeType, self))
|
||||
|
||||
def clearActiveNodes(self):
|
||||
for key in self._activeNodes.keys():
|
||||
self._activeNodes.get(key).node = None
|
||||
|
||||
def onCameraInitChanged(self):
|
||||
# Update active nodes when CameraInit changes
|
||||
nodes = self._graph.nodesFromNode(self._cameraInit)[0]
|
||||
|
@ -559,7 +567,7 @@ class Reconstruction(UIGraph):
|
|||
def getViewpoints(self):
|
||||
""" Return the Viewpoints model. """
|
||||
# TODO: handle multiple Viewpoints models
|
||||
return self._cameraInit.viewpoints.value if self._cameraInit else None
|
||||
return self._cameraInit.viewpoints.value if self._cameraInit else QObjectListModel(parent=self)
|
||||
|
||||
def updateCameraInits(self):
|
||||
cameraInits = self._graph.nodesByType("CameraInit", sortedByIndex=True)
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue