Initial commit
This commit is contained in:
@@ -0,0 +1,70 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
import QtQuick
|
||||
import QtQuick3D
|
||||
|
||||
// The rotation math is based on the paper
|
||||
// ARCBALL:
|
||||
// A User Interface for Specifying Three-Dimensional Orientation Using a Mouse
|
||||
// by Ken Shoemake, 1992
|
||||
Item {
|
||||
id: root
|
||||
visible: false
|
||||
|
||||
required property Node controlledObject
|
||||
property vector3d lastPos: Qt.vector3d(0, 0, 0)
|
||||
property bool moving: false
|
||||
|
||||
// From Shoemake 1992:
|
||||
// pt.x <- (screen.x - center.x)/radius;
|
||||
// pt.y <- (screen.y - center.y)/radius;
|
||||
// r <- pt.x*pt.x + pt.y*pt.y;
|
||||
// IF r > 1.0
|
||||
// THEN s <- 1.0/Sqrt[r];
|
||||
// pt.x <- s*pt.x;
|
||||
// pt.y <- s*pt.y;
|
||||
// pt.z <- 0.0;
|
||||
// ELSE pt.z <- Sqrt[1.0 - r] ;
|
||||
function pos2DToPos3D(posNDC) {
|
||||
var pt = Qt.vector3d(posNDC.x, posNDC.y, 0)
|
||||
let r = posNDC.x * posNDC.x + posNDC.y * posNDC.y
|
||||
if (r > 1.0) {
|
||||
let s = 1.0 / Math.sqrt(r)
|
||||
pt.x = s * pt.x
|
||||
pt.y = s * pt.y
|
||||
pt.z = 0.0
|
||||
} else {
|
||||
pt.z = Math.sqrt(1.0 - r)
|
||||
}
|
||||
|
||||
return pt
|
||||
}
|
||||
|
||||
function mousePressed(posNDC) {
|
||||
lastPos = pos2DToPos3D(posNDC)
|
||||
moving = true
|
||||
}
|
||||
|
||||
function mouseReleased(posNDC) {
|
||||
moving = false
|
||||
}
|
||||
|
||||
function mouseMoved(posNDC) {
|
||||
if (!moving)
|
||||
return
|
||||
|
||||
let currentPos = pos2DToPos3D(posNDC)
|
||||
|
||||
// From Shoemake 1992:
|
||||
// [q.x, q.y, q.z] <- V3_Cross[pO, p1];
|
||||
// q.w <- V3_Dot[pO, p1];
|
||||
// qnow <- QuatMul[q, qstart];
|
||||
let p0 = lastPos
|
||||
let p1 = currentPos
|
||||
let p0p1 = p0.crossProduct(p1)
|
||||
let q = Qt.quaternion(p0.dotProduct(p1), p0p1.x, p0p1.y, p0p1.z)
|
||||
let qnow = q.times(controlledObject.rotation)
|
||||
controlledObject.rotation = qnow
|
||||
lastPos = currentPos
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,135 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
import QtQuick
|
||||
|
||||
import LightmapFile 1.0
|
||||
|
||||
Rectangle {
|
||||
id: scrollView
|
||||
clip: true
|
||||
color: "black"
|
||||
|
||||
property real lastMouseX: 0
|
||||
property real lastMouseY: 0
|
||||
|
||||
function clamp() {
|
||||
// If the image is smaller than the scroll view, center it
|
||||
if (image.width <= scrollView.width) {
|
||||
imageCenterX = 0
|
||||
} else {
|
||||
const maxOffsetX = (image.width - scrollView.width) / 2
|
||||
imageCenterX = Math.max(-maxOffsetX,
|
||||
Math.min(imageCenterX,
|
||||
maxOffsetX))
|
||||
}
|
||||
|
||||
if (image.height <= scrollView.height) {
|
||||
imageCenterY = 0
|
||||
} else {
|
||||
const maxOffsetY = (image.height - scrollView.height) / 2
|
||||
imageCenterY = Math.max(-maxOffsetY,
|
||||
Math.min(imageCenterY,
|
||||
maxOffsetY))
|
||||
}
|
||||
}
|
||||
|
||||
onWidthChanged: clamp()
|
||||
onHeightChanged: clamp()
|
||||
|
||||
Connections {
|
||||
target: window
|
||||
function onSelectedEntryChanged() {
|
||||
if (imageLoader.item === scrollView) {
|
||||
imageZoom = 1
|
||||
imageCenterX = 0
|
||||
imageCenterY = 0
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
id: mouseArea
|
||||
property bool dragging: false
|
||||
anchors.fill: parent
|
||||
onPressed: mouse => {
|
||||
scrollView.lastMouseX = mouse.x
|
||||
scrollView.lastMouseY = mouse.y
|
||||
dragging = true
|
||||
}
|
||||
onReleased: mouse => {
|
||||
dragging = false
|
||||
}
|
||||
|
||||
onPositionChanged: mouse => {
|
||||
var dx = mouse.x - scrollView.lastMouseX
|
||||
var dy = mouse.y - scrollView.lastMouseY
|
||||
|
||||
scrollView.lastMouseX = mouse.x
|
||||
scrollView.lastMouseY = mouse.y
|
||||
|
||||
imageCenterX += dx
|
||||
imageCenterY += dy
|
||||
|
||||
clamp()
|
||||
}
|
||||
cursorShape: mouseArea.dragging ? Qt.ClosedHandCursor : Qt.ArrowCursor
|
||||
|
||||
onWheel: event => {
|
||||
const oldZoom = imageZoom
|
||||
const zoomDelta = event.angleDelta.y / 256
|
||||
const newZoom = Math.max(
|
||||
1, Math.min(32, oldZoom + zoomDelta))
|
||||
|
||||
if (newZoom === oldZoom)
|
||||
return
|
||||
|
||||
// Adjust center offset so the same point remains at the center
|
||||
const scaleFactor = newZoom / oldZoom
|
||||
imageCenterX *= scaleFactor
|
||||
imageCenterY *= scaleFactor
|
||||
|
||||
imageZoom = newZoom
|
||||
clamp()
|
||||
|
||||
event.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Image {
|
||||
id: baseGrid
|
||||
anchors.fill: scrollView
|
||||
source: "grid.png"
|
||||
fillMode: Image.Tile
|
||||
opacity: 0.75
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: image.width + (border.width * 2)
|
||||
height: image.height + (border.width * 2)
|
||||
x: image.x - border.width
|
||||
y: image.y - border.width
|
||||
color: "white" // This is the border color
|
||||
|
||||
border.width: 0
|
||||
border.color: "white"
|
||||
opacity: 0.25
|
||||
visible: window.isImage(window.selectedEntry)
|
||||
}
|
||||
|
||||
Image {
|
||||
id: image
|
||||
x: Math.round(parent.width / 2 - width / 2) + imageCenterX
|
||||
y: Math.round(parent.height / 2 - height / 2) + imageCenterY
|
||||
source: window.isImage(
|
||||
window.selectedEntry) ? `image://lightmaps/key=${selectedEntry.key}&tag=${selectedEntry.tag}&file=${LightmapFile.source}&alpha=${alphaSwitch.checked}` : ""
|
||||
onWidthChanged: clamp()
|
||||
onHeightChanged: clamp()
|
||||
fillMode: Image.PreserveAspectFit
|
||||
smooth: false
|
||||
antialiasing: false
|
||||
|
||||
// Let the image scale visibly
|
||||
width: sourceSize.width * imageZoom
|
||||
height: sourceSize.height * imageZoom
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,325 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick3D
|
||||
import QtQuick3D.Helpers
|
||||
|
||||
import QtQuick3D.lightmapviewer
|
||||
import LightmapFile 1.0
|
||||
|
||||
ApplicationWindow {
|
||||
width: 1024
|
||||
height: 768
|
||||
visible: true
|
||||
title: qsTr("Lightmap Viewer")
|
||||
|
||||
id: window
|
||||
|
||||
property var selectedEntry: listView.model.length ? listView.model[0] : null
|
||||
property real imageZoom: 1
|
||||
property real imageCenterX: 0
|
||||
property real imageCenterY: 0
|
||||
|
||||
function isImage(entry) {
|
||||
return entry && entry.kind === "image"
|
||||
}
|
||||
function isMesh(entry) {
|
||||
return entry && entry.kind === "mesh"
|
||||
}
|
||||
|
||||
Dialog {
|
||||
id: sceneMetadataDialog
|
||||
modal: true
|
||||
standardButtons: Dialog.NoButton
|
||||
x: Math.round((window.width - width) / 2)
|
||||
y: Math.round((window.height - height) / 2)
|
||||
visible: false
|
||||
width: 220
|
||||
height: 360
|
||||
|
||||
contentItem: SceneMetadataView {}
|
||||
}
|
||||
|
||||
header: ToolBar {
|
||||
RowLayout {
|
||||
Button {
|
||||
text: qsTr("Open Lightmap...")
|
||||
onClicked: fileDialog.open()
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
color: "darkgray"
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Button {
|
||||
text: qsTr("Scene Metadata...")
|
||||
onClicked: sceneMetadataDialog.open()
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Zoom: " + window.imageZoom.toFixed(1)
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
color: "darkgray"
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Switch {
|
||||
id: alphaSwitch
|
||||
padding: 0
|
||||
checked: true
|
||||
text: "Alpha"
|
||||
}
|
||||
|
||||
Rectangle {
|
||||
width: 1
|
||||
color: "darkgray"
|
||||
Layout.fillHeight: true
|
||||
Layout.alignment: Qt.AlignVCenter
|
||||
}
|
||||
|
||||
Text {
|
||||
text: "Path: " + LightmapFile.source
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
FileDialog {
|
||||
id: fileDialog
|
||||
onAccepted: {
|
||||
LightmapFile.source = selectedFile
|
||||
LightmapFile.loadData()
|
||||
}
|
||||
}
|
||||
|
||||
Shortcut {
|
||||
sequences: [StandardKey.Open]
|
||||
onActivated: {
|
||||
fileDialog.open()
|
||||
}
|
||||
}
|
||||
|
||||
SplitView {
|
||||
anchors.fill: parent
|
||||
orientation: Qt.Horizontal
|
||||
|
||||
focus: true
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Up) {
|
||||
listView.currentIndex = Math.max(
|
||||
0, listView.currentIndex - 1)
|
||||
selectedEntry = listView.model[listView.currentIndex]
|
||||
} else if (event.key === Qt.Key_Down) {
|
||||
listView.currentIndex = Math.min(
|
||||
listView.model.length - 1,
|
||||
listView.currentIndex + 1)
|
||||
selectedEntry = listView.model[listView.currentIndex]
|
||||
}
|
||||
}
|
||||
|
||||
SplitView {
|
||||
id: leftSplit
|
||||
SplitView.preferredWidth: 220
|
||||
SplitView.minimumWidth: 120
|
||||
orientation: Qt.Vertical
|
||||
|
||||
Item {
|
||||
id: metaArea
|
||||
SplitView.preferredHeight: 120
|
||||
anchors.left: parent.left
|
||||
anchors.right: parent.right
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 4
|
||||
|
||||
Pane {
|
||||
id: metaPane
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
clip: true
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
|
||||
ColumnLayout {
|
||||
id: metadataColumn
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: LightmapFile.metadataFor(selectedEntry)
|
||||
delegate: RowLayout {
|
||||
width: metadataColumn.width
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: (modelData.key ?? "—") + ":"
|
||||
font.bold: true
|
||||
}
|
||||
Label {
|
||||
text: modelData.value
|
||||
!== undefined ? String(
|
||||
modelData.value) : "—"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ListView {
|
||||
id: listView
|
||||
clip: true
|
||||
spacing: 2
|
||||
highlightMoveVelocity: -1
|
||||
highlightMoveDuration: 1
|
||||
model: LightmapFile.dataList
|
||||
property var sectionExpanded: ({})
|
||||
|
||||
section.property: "owner"
|
||||
section.criteria: ViewSection.FullString
|
||||
section.delegate: Rectangle {
|
||||
width: listView.width
|
||||
height: 26
|
||||
color: Qt.rgba(0, 0, 0, 0.05)
|
||||
radius: 4
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 8
|
||||
anchors.rightMargin: 8
|
||||
spacing: 6
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
|
||||
Text {
|
||||
text: (listView.sectionExpanded[section] === false) ? "▸" : "▾"
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
Text {
|
||||
text: section
|
||||
font.bold: true
|
||||
verticalAlignment: Text.AlignVCenter
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
onClicked: {
|
||||
listView.sectionExpanded[section]
|
||||
= !(listView.sectionExpanded[section] !== false)
|
||||
listView.sectionExpanded = Object.assign(
|
||||
{}, listView.sectionExpanded)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
delegate: Item {
|
||||
width: listView.width
|
||||
|
||||
property bool isExpanded: listView.sectionExpanded[modelData.owner] !== false
|
||||
|
||||
height: isExpanded ? Math.max(
|
||||
24, rowText.implicitHeight + 6) : 0
|
||||
opacity: isExpanded ? 1 : 0
|
||||
|
||||
Behavior on height {
|
||||
NumberAnimation {
|
||||
duration: 120
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
Behavior on opacity {
|
||||
NumberAnimation {
|
||||
duration: 120
|
||||
easing.type: Easing.OutCubic
|
||||
}
|
||||
}
|
||||
|
||||
Row {
|
||||
anchors.fill: parent
|
||||
anchors.leftMargin: 16
|
||||
anchors.rightMargin: 8
|
||||
anchors.verticalCenter: parent.verticalCenter
|
||||
Text {
|
||||
id: rowText
|
||||
text: modelData.display
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
|
||||
MouseArea {
|
||||
anchors.fill: parent
|
||||
hoverEnabled: true
|
||||
enabled: isExpanded
|
||||
onClicked: {
|
||||
listView.currentIndex = index
|
||||
selectedEntry = modelData
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
highlight: Rectangle {
|
||||
color: Qt.rgba(76 / 255, 134 / 255, 191 / 255, 0.25)
|
||||
radius: 6
|
||||
anchors.margins: 2
|
||||
}
|
||||
|
||||
ScrollBar.vertical: ScrollBar {}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
id: rightSplit
|
||||
SplitView.fillWidth: true
|
||||
SplitView.fillHeight: true
|
||||
|
||||
// These are toggled based on what is currently selected
|
||||
Loader {
|
||||
id: imageLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: ImageViewer {}
|
||||
active: true
|
||||
visible: isImage(selectedEntry)
|
||||
enabled: visible
|
||||
}
|
||||
|
||||
Loader {
|
||||
id: meshLoader
|
||||
anchors.fill: parent
|
||||
sourceComponent: MeshViewer {}
|
||||
active: true
|
||||
visible: isMesh(selectedEntry)
|
||||
enabled: visible
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
DropArea {
|
||||
id: dropArea
|
||||
anchors.fill: parent
|
||||
onEntered: drag => {
|
||||
drag.accept(Qt.LinkAction)
|
||||
}
|
||||
// Just take first url if several
|
||||
onDropped: drop => {
|
||||
if (drop.hasUrls) {
|
||||
LightmapFile.source = drop.urls[0]
|
||||
LightmapFile.loadData()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,314 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
import QtQuick.Window
|
||||
import QtQuick.Dialogs
|
||||
import QtQuick3D
|
||||
import QtQuick3D.Helpers
|
||||
|
||||
import QtQuick3D.lightmapviewer
|
||||
import LightmapFile 1.0
|
||||
|
||||
ColumnLayout {
|
||||
id: root
|
||||
anchors.fill: parent
|
||||
|
||||
property string selectedModelCandidate: ""
|
||||
|
||||
onSelectedModelCandidateChanged: {
|
||||
if (!selectedModelCandidate || selectedModelCandidate === "") {
|
||||
view.lmTextureCandidates = []
|
||||
lview.mSelectedTextureCandidate = -1
|
||||
return
|
||||
}
|
||||
view.lmTextureCandidates = LightmapFile.texturesAvailableFor(
|
||||
selectedModelCandidate)
|
||||
view.lmSelectedTextureCandidate
|
||||
= view.lmTextureCandidates.length ? view.lmTextureCandidates[0].value : -1
|
||||
}
|
||||
|
||||
Pane {
|
||||
Layout.fillWidth: true
|
||||
padding: 8
|
||||
|
||||
ColumnLayout {
|
||||
width: parent.width
|
||||
spacing: 6
|
||||
|
||||
ColumnLayout {
|
||||
RowLayout {
|
||||
Label {
|
||||
text: "Key:"
|
||||
}
|
||||
ComboBox {
|
||||
id: comboLmModelCandidate
|
||||
Layout.preferredWidth: 220
|
||||
model: view.lmModelCandidates
|
||||
onActivated: root.selectedModelCandidate = currentText
|
||||
currentIndex: {
|
||||
const i = view.lmModelCandidates.indexOf(
|
||||
root.selectedModelCandidate)
|
||||
return (i >= 0 ? i : -1)
|
||||
}
|
||||
enabled: !!selectedEntry
|
||||
&& selectedEntry.kind === "mesh"
|
||||
}
|
||||
Label {
|
||||
text: "Texture:"
|
||||
}
|
||||
ComboBox {
|
||||
id: comboLmTextureCandidate
|
||||
Layout.preferredWidth: 220
|
||||
model: view.lmTextureCandidates
|
||||
textRole: "name"
|
||||
valueRole: "value"
|
||||
|
||||
function indexForValue(val) {
|
||||
for (var i = 0; i < view.lmTextureCandidates.length; ++i)
|
||||
if (view.lmTextureCandidates[i].value === val)
|
||||
return i
|
||||
return -1
|
||||
}
|
||||
|
||||
onActivated: view.lmSelectedTextureCandidate = Number(
|
||||
currentValue)
|
||||
currentIndex: indexForValue(
|
||||
view.lmSelectedTextureCandidate)
|
||||
enabled: !!selectedEntry
|
||||
&& selectedEntry.kind === "mesh"
|
||||
}
|
||||
}
|
||||
|
||||
RowLayout {
|
||||
CheckBox {
|
||||
id: checkboxBfCull
|
||||
text: "Backface Culling"
|
||||
checked: true
|
||||
}
|
||||
CheckBox {
|
||||
id: checkboxDebugUV
|
||||
text: "Debug UV"
|
||||
checked: false
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
View3D {
|
||||
id: view
|
||||
Layout.fillWidth: true
|
||||
Layout.fillHeight: true
|
||||
enabled: meshLoader.visible
|
||||
visible: meshLoader.visible
|
||||
|
||||
property var lmModelCandidates: []
|
||||
property var lmTextureCandidates: []
|
||||
property int lmSelectedTextureCandidate: -1
|
||||
|
||||
property real boundsDiameter: 0
|
||||
property vector3d boundsCenter
|
||||
property vector3d boundsSize
|
||||
|
||||
function updateBounds(bounds) {
|
||||
boundsSize = Qt.vector3d(bounds.maximum.x - bounds.minimum.x,
|
||||
bounds.maximum.y - bounds.minimum.y,
|
||||
bounds.maximum.z - bounds.minimum.z)
|
||||
boundsDiameter = Math.max(boundsSize.x, boundsSize.y, boundsSize.z)
|
||||
boundsCenter = Qt.vector3d(
|
||||
(bounds.maximum.x + bounds.minimum.x) / 2,
|
||||
(bounds.maximum.y + bounds.minimum.y) / 2,
|
||||
(bounds.maximum.z + bounds.minimum.z) / 2)
|
||||
model.position = Qt.vector3d(-boundsCenter.x, -boundsCenter.y,
|
||||
-boundsCenter.z)
|
||||
cameraNode.clipNear = boundsDiameter / 100
|
||||
cameraNode.clipFar = boundsDiameter * 10
|
||||
resetCamera()
|
||||
}
|
||||
|
||||
function resetCamera() {
|
||||
cameraNode.position = boundsCenter
|
||||
cameraNode.position = Qt.vector3d(0, 0, 2 * boundsDiameter)
|
||||
cameraNode.eulerRotation = Qt.vector3d(0, 0, 0)
|
||||
}
|
||||
|
||||
function refreshLightmapSelection() {
|
||||
if (!selectedEntry) {
|
||||
lmModelCandidates = []
|
||||
root.selectedModelCandidate = ""
|
||||
lmTextureCandidates = []
|
||||
lmSelectedTextureCandidate = -1
|
||||
return
|
||||
}
|
||||
|
||||
if (selectedEntry.kind === "image") {
|
||||
lmModelCandidates = [selectedEntry.key]
|
||||
root.selectedModelCandidate = selectedEntry.key
|
||||
} else if (selectedEntry.kind === "mesh") {
|
||||
lmModelCandidates = LightmapFile.keysReferencingMesh(
|
||||
selectedEntry.key)
|
||||
root.selectedModelCandidate = lmModelCandidates.length ? lmModelCandidates[0] : ""
|
||||
} else {
|
||||
lmModelCandidates = []
|
||||
root.selectedModelCandidate = ""
|
||||
lmTextureCandidates = []
|
||||
lmSelectedTextureCandidate = -1
|
||||
}
|
||||
}
|
||||
|
||||
Component.onCompleted: refreshLightmapSelection()
|
||||
Connections {
|
||||
target: window
|
||||
function onSelectedEntryChanged() {
|
||||
view.refreshLightmapSelection()
|
||||
}
|
||||
}
|
||||
|
||||
environment: SceneEnvironment {
|
||||
backgroundMode: SceneEnvironment.Color
|
||||
clearColor: "black"
|
||||
}
|
||||
|
||||
PerspectiveCamera {
|
||||
id: cameraNode
|
||||
z: 300
|
||||
}
|
||||
|
||||
Node {
|
||||
id: modelNode
|
||||
|
||||
Model {
|
||||
id: model
|
||||
geometry: LightmapMesh {
|
||||
source: LightmapFile.source
|
||||
key: selectedEntry.key
|
||||
onBoundsChanged: view.updateBounds(bounds)
|
||||
}
|
||||
materials: CustomMaterial {
|
||||
shadingMode: CustomMaterial.Unshaded
|
||||
cullMode: checkboxBfCull.checked ? Material.BackFaceCulling : Material.NoCulling
|
||||
|
||||
property TextureInput baseMap: TextureInput {
|
||||
texture: Texture {
|
||||
id: lmTexture
|
||||
minFilter: Texture.Linear
|
||||
magFilter: Texture.Linear
|
||||
mipFilter: Texture.None
|
||||
tilingModeHorizontal: Texture.ClampToEdge
|
||||
tilingModeVertical: Texture.ClampToEdge
|
||||
|
||||
textureData: LightmapFile.textureDataFor(
|
||||
root.selectedModelCandidate,
|
||||
view.lmSelectedTextureCandidate)
|
||||
}
|
||||
}
|
||||
property bool debugUV: checkboxDebugUV.checked
|
||||
|
||||
vertexShader: "mesh.vert"
|
||||
fragmentShader: "mesh.frag"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ArcballController {
|
||||
id: arcballController
|
||||
controlledObject: modelNode
|
||||
|
||||
function jumpToAxis(axis) {
|
||||
cameraRotation.from = arcballController.controlledObject.rotation
|
||||
cameraRotation.to = originGizmo.quaternionForAxis(
|
||||
axis, arcballController.controlledObject.rotation)
|
||||
cameraRotation.duration = 200
|
||||
cameraRotation.start()
|
||||
}
|
||||
|
||||
function jumpToRotation(qRotation) {
|
||||
cameraRotation.from = arcballController.controlledObject.rotation
|
||||
cameraRotation.to = qRotation
|
||||
cameraRotation.duration = 100
|
||||
cameraRotation.start()
|
||||
}
|
||||
|
||||
QuaternionAnimation {
|
||||
id: cameraRotation
|
||||
target: arcballController.controlledObject
|
||||
property: "rotation"
|
||||
type: QuaternionAnimation.Slerp
|
||||
running: false
|
||||
loops: 1
|
||||
}
|
||||
}
|
||||
|
||||
OriginGizmo {
|
||||
id: originGizmo
|
||||
anchors.top: parent.top
|
||||
anchors.right: parent.right
|
||||
anchors.margins: 10
|
||||
width: 120
|
||||
height: 120
|
||||
targetNode: modelNode
|
||||
|
||||
onAxisClicked: axis => {
|
||||
arcballController.jumpToAxis(axis)
|
||||
}
|
||||
}
|
||||
|
||||
DragHandler {
|
||||
id: dragHandler
|
||||
target: null
|
||||
acceptedModifiers: Qt.NoModifier
|
||||
onCentroidChanged: {
|
||||
arcballController.mouseMoved(toNDC(centroid.position.x,
|
||||
centroid.position.y))
|
||||
}
|
||||
|
||||
onActiveChanged: {
|
||||
if (active) {
|
||||
view.forceActiveFocus()
|
||||
arcballController.mousePressed(toNDC(centroid.position.x,
|
||||
centroid.position.y))
|
||||
} else
|
||||
arcballController.mouseReleased(toNDC(centroid.position.x,
|
||||
centroid.position.y))
|
||||
}
|
||||
|
||||
function toNDC(x, y) {
|
||||
return Qt.vector2d((2.0 * x / width) - 1.0,
|
||||
1.0 - (2.0 * y / height))
|
||||
}
|
||||
}
|
||||
|
||||
WheelHandler {
|
||||
id: wheelHandler
|
||||
orientation: Qt.Vertical
|
||||
target: null
|
||||
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
|
||||
onWheel: event => {
|
||||
let delta = -event.angleDelta.y * 0.01
|
||||
cameraNode.z += cameraNode.z * 0.1 * delta
|
||||
}
|
||||
}
|
||||
|
||||
Keys.onPressed: event => {
|
||||
if (event.key === Qt.Key_Space) {
|
||||
let rotation = originGizmo.quaternionAlign(
|
||||
arcballController.controlledObject.rotation)
|
||||
arcballController.jumpToRotation(rotation)
|
||||
} else if (event.key === Qt.Key_S) {
|
||||
settingsPane.toggleHide()
|
||||
} else if (event.key === Qt.Key_Left
|
||||
|| event.key === Qt.Key_A) {
|
||||
let rotation = originGizmo.quaternionRotateLeft(
|
||||
arcballController.controlledObject.rotation)
|
||||
arcballController.jumpToRotation(rotation)
|
||||
} else if (event.key === Qt.Key_Right
|
||||
|| event.key === Qt.Key_D) {
|
||||
let rotation = originGizmo.quaternionRotateRight(
|
||||
arcballController.controlledObject.rotation)
|
||||
arcballController.jumpToRotation(rotation)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,401 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
import QtQuick
|
||||
import QtQuick3D
|
||||
|
||||
Item {
|
||||
id: root
|
||||
required property Node targetNode
|
||||
|
||||
enum Axis {
|
||||
PositiveZ = 0,
|
||||
NegativeZ = 1,
|
||||
PositiveY = 2,
|
||||
NegativeY = 3,
|
||||
PositiveX = 4,
|
||||
NegativeX = 5
|
||||
}
|
||||
|
||||
// These are the 24 different rotations a rotation aligned on axes can have.
|
||||
// They are ordered in groups of 4 where the +Z,-Z,+Y,-Y,+X,-X axis is pointing
|
||||
// towards the screen (+Z). Inside this group the rotations are ordered to
|
||||
// rotate counter-clockwise.
|
||||
readonly property list<quaternion> rotations: [
|
||||
// +Z
|
||||
Qt.quaternion(1, 0, 0, 0),
|
||||
Qt.quaternion(Math.SQRT1_2, 0, 0, -Math.SQRT1_2),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(Math.SQRT1_2, 0, 0, Math.SQRT1_2),
|
||||
// -Z
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, -Math.SQRT1_2, -Math.SQRT1_2, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, Math.SQRT1_2, -Math.SQRT1_2, 0),
|
||||
// +Y
|
||||
Qt.quaternion(0.5, 0.5, 0.5, 0.5),
|
||||
Qt.quaternion(Math.SQRT1_2, Math.SQRT1_2, 0, 0),
|
||||
Qt.quaternion(-0.5, -0.5, 0.5, 0.5),
|
||||
Qt.quaternion(0, 0, -Math.SQRT1_2, -Math.SQRT1_2),
|
||||
// -Y
|
||||
Qt.quaternion(0.5, -0.5, 0.5, -0.5),
|
||||
Qt.quaternion(0, 0, Math.SQRT1_2, -Math.SQRT1_2),
|
||||
Qt.quaternion(-0.5, 0.5, 0.5, -0.5),
|
||||
Qt.quaternion(-Math.SQRT1_2, Math.SQRT1_2, 0, 0),
|
||||
// +X
|
||||
Qt.quaternion(-0.5, -0.5, 0.5, -0.5),
|
||||
Qt.quaternion(-Math.SQRT1_2, 0, Math.SQRT1_2, 0),
|
||||
Qt.quaternion(-0.5, 0.5, 0.5, 0.5),
|
||||
Qt.quaternion(0, Math.SQRT1_2, 0, Math.SQRT1_2),
|
||||
// -X
|
||||
Qt.quaternion(0, Math.SQRT1_2, 0, -Math.SQRT1_2),
|
||||
Qt.quaternion(0.5, -0.5, 0.5, 0.5),
|
||||
Qt.quaternion(Math.SQRT1_2, 0, Math.SQRT1_2, 0),
|
||||
Qt.quaternion(0.5, 0.5, 0.5, -0.5),
|
||||
]
|
||||
|
||||
readonly property list<quaternion> xRotationGoals : [
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(-0, -1, -0, -0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(-0, -1, -0, -0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(-0, 1, -0, -0),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
]
|
||||
|
||||
readonly property list<quaternion> yRotationGoals : [
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
]
|
||||
|
||||
readonly property list<quaternion> zRotationGoals : [
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, 1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 0, 0, -1),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, -1, 0, 0),
|
||||
Qt.quaternion(0, 0, 1, 0),
|
||||
Qt.quaternion(0, 1, 0, 0),
|
||||
Qt.quaternion(0, 0, -1, 0),
|
||||
]
|
||||
|
||||
// This function works by using a rotation to rotate x,y,z normal vectors
|
||||
// and see what axis-aligned rotation gives the closest distance to the
|
||||
// rotated normal vectors.
|
||||
function findClosestRotation(rotation, startI, stopI) {
|
||||
let rotationConjugated = rotation.conjugated();
|
||||
let xRotated = rotation.times(Qt.quaternion(0, 1, 0, 0)).times(rotationConjugated);
|
||||
let yRotated = rotation.times(Qt.quaternion(0, 0, 1, 0)).times(rotationConjugated);
|
||||
let zRotated = rotation.times(Qt.quaternion(0, 0, 0, 1)).times(rotationConjugated);
|
||||
|
||||
var closestIndex = 0;
|
||||
var closestDistance = 123456789; // big number
|
||||
|
||||
for (var i = startI; i < stopI ; i++) {
|
||||
let distance = xRotated.minus(xRotationGoals[i]).length() +
|
||||
yRotated.minus(yRotationGoals[i]).length() +
|
||||
zRotated.minus(zRotationGoals[i]).length();
|
||||
if (distance <= closestDistance) {
|
||||
closestDistance = distance;
|
||||
closestIndex = i;
|
||||
}
|
||||
}
|
||||
|
||||
return closestIndex;
|
||||
}
|
||||
|
||||
function quaternionAlign(rotation) {
|
||||
let closestIndex = findClosestRotation(rotation, 0, 24);
|
||||
return rotations[closestIndex];
|
||||
}
|
||||
|
||||
function quaternionForAxis(axis, rotation) {
|
||||
let closestIndex = findClosestRotation(rotation, axis*4, (axis + 1)*4);
|
||||
return rotations[closestIndex];
|
||||
}
|
||||
|
||||
function quaternionRotateLeft(rotation) {
|
||||
let closestIndex = findClosestRotation(rotation, 0, 24);
|
||||
let offset = (4 + closestIndex - 1) % 4;
|
||||
let group = Math.floor(closestIndex / 4);
|
||||
return rotations[offset + group * 4];
|
||||
}
|
||||
|
||||
function quaternionRotateRight(rotation) {
|
||||
let closestIndex = findClosestRotation(rotation, 0, 24);
|
||||
let offset = (closestIndex + 1) % 4;
|
||||
let group = Math.floor(closestIndex / 4);
|
||||
return rotations[offset + group * 4];
|
||||
}
|
||||
|
||||
signal axisClicked(int axis)
|
||||
signal ballMoved(vector2d velocity)
|
||||
|
||||
QtObject {
|
||||
id: stylePalette
|
||||
property color white: "#fdf6e3"
|
||||
property color black: "#002b36"
|
||||
property color red: "#dc322f"
|
||||
property color green: "#859900"
|
||||
property color blue: "#268bd2"
|
||||
property color background: "#99002b36"
|
||||
}
|
||||
|
||||
component LineRectangle : Rectangle {
|
||||
property vector2d startPoint: Qt.vector2d(0, 0)
|
||||
property vector2d endPoint: Qt.vector2d(0, 0)
|
||||
property real lineWidth: 5
|
||||
transformOrigin: Item.Left
|
||||
height: lineWidth
|
||||
|
||||
readonly property vector2d offset: startPoint.plus(endPoint).times(0.5);
|
||||
|
||||
width: startPoint.minus(endPoint).length()
|
||||
rotation: Math.atan2(endPoint.y - startPoint.y, endPoint.x - startPoint.x) * 180 / Math.PI
|
||||
}
|
||||
|
||||
|
||||
Rectangle {
|
||||
id: ballBackground
|
||||
anchors.centerIn: parent
|
||||
width: parent.width > parent.height ? parent.height : parent.width
|
||||
height: width
|
||||
radius: width / 2
|
||||
color: ballBackgroundHoverHandler.hovered ? stylePalette.background : "transparent"
|
||||
|
||||
readonly property real subBallWidth: width / 5
|
||||
readonly property real subBallHalfWidth: subBallWidth * 0.5
|
||||
readonly property real subBallOffset: radius - subBallWidth / 2
|
||||
|
||||
Item {
|
||||
anchors.centerIn: parent
|
||||
|
||||
component SubBall : Rectangle {
|
||||
id: subBallRoot
|
||||
required property Node targetNode
|
||||
required property real offset
|
||||
|
||||
property alias labelText: label.text
|
||||
property alias labelColor: label.color
|
||||
property alias labelVisible: label.visible
|
||||
property alias hovered: subBallHoverHandler.hovered
|
||||
property var initialPosition: Qt.vector3d(0, 0, 0)
|
||||
readonly property vector3d position: quaternionVectorMultiply(targetNode.rotation, initialPosition)
|
||||
|
||||
signal tapped()
|
||||
|
||||
function quaternionVectorMultiply(q, v) {
|
||||
var qv = Qt.vector3d(q.x, q.y, q.z)
|
||||
var uv = qv.crossProduct(v)
|
||||
var uuv = qv.crossProduct(uv)
|
||||
uv = uv.times(2.0 * q.scalar)
|
||||
uuv = uuv.times(2.0)
|
||||
return v.plus(uv).plus(uuv)
|
||||
}
|
||||
|
||||
height: width
|
||||
radius: width / 2
|
||||
x: offset * position.x - width / 2
|
||||
y: offset * -position.y - height / 2
|
||||
z: position.z
|
||||
|
||||
HoverHandler {
|
||||
id: subBallHoverHandler
|
||||
}
|
||||
|
||||
TapHandler {
|
||||
acceptedButtons: Qt.LeftButton
|
||||
onTapped: (eventPoint, button)=>{
|
||||
subBallRoot.tapped()
|
||||
//eventPoint.accepted = true
|
||||
}
|
||||
}
|
||||
|
||||
Text {
|
||||
id: label
|
||||
anchors.centerIn: parent
|
||||
}
|
||||
}
|
||||
|
||||
SubBall {
|
||||
id: positiveX
|
||||
targetNode: root.targetNode
|
||||
width: ballBackground.subBallWidth
|
||||
offset: ballBackground.subBallOffset
|
||||
labelText: "X"
|
||||
labelColor: hovered ? stylePalette.white : stylePalette.black
|
||||
color: stylePalette.red
|
||||
initialPosition: Qt.vector3d(1, 0, 0)
|
||||
onTapped: {
|
||||
root.axisClicked(OriginGizmo.Axis.PositiveX)
|
||||
}
|
||||
}
|
||||
|
||||
LineRectangle {
|
||||
endPoint: Qt.vector2d(positiveX.x + ballBackground.subBallHalfWidth, positiveX.y + ballBackground.subBallHalfWidth)
|
||||
color: stylePalette.red
|
||||
z: positiveX.z - 0.001
|
||||
}
|
||||
|
||||
SubBall {
|
||||
id: positiveY
|
||||
targetNode: root.targetNode
|
||||
width: ballBackground.subBallWidth
|
||||
offset: ballBackground.subBallOffset
|
||||
labelText: "Y"
|
||||
labelColor: hovered ? stylePalette.white : stylePalette.black
|
||||
color: stylePalette.green
|
||||
initialPosition: Qt.vector3d(0, 1, 0)
|
||||
onTapped: {
|
||||
root.axisClicked(OriginGizmo.Axis.PositiveY)
|
||||
}
|
||||
}
|
||||
|
||||
LineRectangle {
|
||||
endPoint: Qt.vector2d(positiveY.x + ballBackground.subBallHalfWidth, positiveY.y + ballBackground.subBallHalfWidth)
|
||||
color: stylePalette.green
|
||||
z: positiveY.z - 0.001
|
||||
}
|
||||
|
||||
SubBall {
|
||||
id: positiveZ
|
||||
targetNode: root.targetNode
|
||||
width: ballBackground.subBallWidth
|
||||
offset: ballBackground.subBallOffset
|
||||
labelText: "Z"
|
||||
labelColor: hovered ? stylePalette.white : stylePalette.black
|
||||
color: stylePalette.blue
|
||||
initialPosition: Qt.vector3d(0, 0, 1)
|
||||
onTapped: {
|
||||
root.axisClicked(OriginGizmo.Axis.PositiveZ)
|
||||
}
|
||||
}
|
||||
|
||||
LineRectangle {
|
||||
endPoint: Qt.vector2d(positiveZ.x + ballBackground.subBallHalfWidth, positiveZ.y + ballBackground.subBallHalfWidth)
|
||||
color: stylePalette.blue
|
||||
z: positiveZ.z - 0.001
|
||||
}
|
||||
|
||||
SubBall {
|
||||
targetNode: root.targetNode
|
||||
width: ballBackground.subBallWidth
|
||||
offset: ballBackground.subBallOffset
|
||||
labelText: "-X"
|
||||
labelColor: stylePalette.white
|
||||
labelVisible: hovered
|
||||
color: Qt.rgba(stylePalette.red.r, stylePalette.red.g, stylePalette.red.b, z + 1 * 0.5)
|
||||
border.color: stylePalette.red
|
||||
border.width: 2
|
||||
initialPosition: Qt.vector3d(-1, 0, 0)
|
||||
onTapped: {
|
||||
root.axisClicked(OriginGizmo.Axis.NegativeX)
|
||||
}
|
||||
}
|
||||
|
||||
SubBall {
|
||||
targetNode: root.targetNode
|
||||
width: ballBackground.subBallWidth
|
||||
offset: ballBackground.subBallOffset
|
||||
labelText: "-Y"
|
||||
labelColor: stylePalette.white
|
||||
labelVisible: hovered
|
||||
color: Qt.rgba(stylePalette.green.r, stylePalette.green.g, stylePalette.green.b, z + 1 * 0.5)
|
||||
border.color: stylePalette.green
|
||||
border.width: 2
|
||||
initialPosition: Qt.vector3d(0, -1, 0)
|
||||
onTapped: {
|
||||
root.axisClicked(OriginGizmo.Axis.NegativeY)
|
||||
}
|
||||
}
|
||||
|
||||
SubBall {
|
||||
targetNode: root.targetNode
|
||||
width: ballBackground.subBallWidth
|
||||
offset: ballBackground.subBallOffset
|
||||
labelText: "-Z"
|
||||
labelColor: stylePalette.white
|
||||
labelVisible: hovered
|
||||
color: Qt.rgba(stylePalette.blue.r, stylePalette.blue.g, stylePalette.blue.b, z + 1 * 0.5)
|
||||
border.color: stylePalette.blue
|
||||
border.width: 2
|
||||
initialPosition: Qt.vector3d(0, 0, -1)
|
||||
onTapped: {
|
||||
root.axisClicked(OriginGizmo.Axis.NegativeZ)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
HoverHandler {
|
||||
id: ballBackgroundHoverHandler
|
||||
acceptedDevices: PointerDevice.Mouse
|
||||
cursorShape: Qt.PointingHandCursor
|
||||
}
|
||||
|
||||
DragHandler {
|
||||
id: dragHandler
|
||||
target: null
|
||||
enabled: ballBackground.visible
|
||||
onCentroidChanged: {
|
||||
if (centroid.velocity.x > 0 && centroid.velocity.y > 0) {
|
||||
root.ballMoved(centroid.velocity)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,86 @@
|
||||
// Copyright (C) 2025 The Qt Company Ltd.
|
||||
// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0
|
||||
import QtQuick
|
||||
import QtQuick.Controls
|
||||
import QtQuick.Layouts
|
||||
|
||||
import LightmapFile 1.0
|
||||
|
||||
Item {
|
||||
id: root
|
||||
implicitWidth: 360
|
||||
implicitHeight: 420
|
||||
|
||||
ScrollView {
|
||||
anchors.fill: parent
|
||||
contentWidth: availableWidth
|
||||
|
||||
ColumnLayout {
|
||||
anchors.fill: parent
|
||||
spacing: 6
|
||||
|
||||
Label {
|
||||
text: `Baked with Qt version: ${LightmapFile.qtVersion || "—"}`
|
||||
font.bold: true
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
Label {
|
||||
text: LightmapFile.bakeStart
|
||||
visible: text.length > 0
|
||||
Layout.fillWidth: true
|
||||
wrapMode: Text.Wrap
|
||||
Component.onCompleted: if (text.length)
|
||||
text = "Bake initiated at:\n" + text
|
||||
}
|
||||
|
||||
Label {
|
||||
text: LightmapFile.bakeDuration
|
||||
visible: text.length > 0
|
||||
wrapMode: Text.Wrap
|
||||
Layout.fillWidth: true
|
||||
Component.onCompleted: if (text.length)
|
||||
text = "Bake took:\n" + text
|
||||
}
|
||||
|
||||
Label {
|
||||
text: "Options used:"
|
||||
Layout.fillWidth: true
|
||||
}
|
||||
|
||||
ColumnLayout {
|
||||
id: optionsColumn
|
||||
Layout.fillWidth: true
|
||||
spacing: 4
|
||||
|
||||
Repeater {
|
||||
model: LightmapFile.options
|
||||
delegate: RowLayout {
|
||||
width: optionsColumn.width
|
||||
spacing: 8
|
||||
|
||||
Label {
|
||||
text: (modelData.key ?? "—") + ":"
|
||||
font.bold: true
|
||||
Layout.preferredWidth: 150
|
||||
Layout.alignment: Qt.AlignRight | Qt.AlignTop
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
Label {
|
||||
text: modelData.value !== undefined ? String(
|
||||
modelData.value) : "—"
|
||||
Layout.fillWidth: true
|
||||
Layout.alignment: Qt.AlignLeft | Qt.AlignTop
|
||||
elide: Text.ElideRight
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Item {
|
||||
width: 1
|
||||
height: 6
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
import QtQuick.tooling 1.2
|
||||
|
||||
// This file describes the plugin-supplied types contained in the library.
|
||||
// It is used for QML tooling purposes only.
|
||||
//
|
||||
// This file was auto-generated by qmltyperegistrar.
|
||||
|
||||
Module {
|
||||
Component {
|
||||
file: "lightmapmesh.h"
|
||||
lineNumber: 11
|
||||
name: "LightmapMesh"
|
||||
accessSemantics: "reference"
|
||||
prototype: "QQuick3DGeometry"
|
||||
exports: ["QtQuick3D.lightmapviewer/LightmapMesh 1.0"]
|
||||
exportMetaObjectRevisions: [256]
|
||||
Property {
|
||||
name: "source"
|
||||
type: "QUrl"
|
||||
read: "source"
|
||||
write: "setSource"
|
||||
notify: "sourceChanged"
|
||||
index: 0
|
||||
lineNumber: 14
|
||||
isFinal: true
|
||||
}
|
||||
Property {
|
||||
name: "key"
|
||||
type: "QString"
|
||||
read: "key"
|
||||
write: "setKey"
|
||||
notify: "keyChanged"
|
||||
index: 1
|
||||
lineNumber: 15
|
||||
isFinal: true
|
||||
}
|
||||
Property {
|
||||
name: "bounds"
|
||||
type: "QQuick3DBounds3"
|
||||
read: "bounds"
|
||||
notify: "boundsChanged"
|
||||
index: 2
|
||||
lineNumber: 16
|
||||
isReadonly: true
|
||||
}
|
||||
Signal { name: "sourceChanged"; lineNumber: 30 }
|
||||
Signal { name: "keyChanged"; lineNumber: 31 }
|
||||
Signal { name: "boundsChanged"; lineNumber: 32 }
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
module QtQuick3D.lightmapviewer
|
||||
typeinfo lightmapviewer.qmltypes
|
||||
import QtQuick3D
|
||||
prefer :/qt-project.org/imports/QtQuick3D/lightmapviewer/
|
||||
LightmapViewer 1.0 LightmapViewer.qml
|
||||
SceneMetadataView 1.0 SceneMetadataView.qml
|
||||
MeshViewer 1.0 MeshViewer.qml
|
||||
ImageViewer 1.0 ImageViewer.qml
|
||||
ArcballController 1.0 ArcballController.qml
|
||||
OriginGizmo 1.0 OriginGizmo.qml
|
||||
depends QtQuick
|
||||
|
||||
Reference in New Issue
Block a user