diff --git a/.github/workflows/swiftformat-lint.yml b/.github/workflows/swiftformat-lint.yml
index e93a257..63c431d 100644
--- a/.github/workflows/swiftformat-lint.yml
+++ b/.github/workflows/swiftformat-lint.yml
@@ -13,13 +13,28 @@ jobs:
- name: Install SwiftFormat
run: |
- brew update
- brew install swiftformat
+ SWIFTFORMAT_VERSION="$(awk -F':=' '/^SWIFTFORMAT_VERSION/ { gsub(/[[:space:]]/, "", $2); print $2 }' Makefile)"
+ if [ -z "$SWIFTFORMAT_VERSION" ]; then
+ echo "Unable to determine SWIFTFORMAT_VERSION from Makefile"
+ exit 1
+ fi
+ curl -fsSL -o /tmp/swiftformat.zip \
+ "https://github.com/nicklockwood/SwiftFormat/releases/download/${SWIFTFORMAT_VERSION}/swiftformat.zip"
+ unzip -o /tmp/swiftformat.zip -d /tmp/swiftformat
+ CURRENT_SWIFTFORMAT="$(command -v swiftformat || true)"
+ if [ -n "$CURRENT_SWIFTFORMAT" ]; then
+ INSTALL_PATH="$CURRENT_SWIFTFORMAT"
+ else
+ INSTALL_PATH="/usr/local/bin/swiftformat"
+ fi
+ sudo install -m 755 /tmp/swiftformat/swiftformat "$INSTALL_PATH"
+ echo "$(dirname "$INSTALL_PATH")" >> "$GITHUB_PATH"
- name: Show versions
run: |
+ which swiftformat
swiftformat --version
swift --version
- name: Run SwiftFormat (lint mode)
- run: swiftformat --lint . --swiftversion 5.8 --reporter github-actions-log
+ run: make lint
diff --git a/CHANGELOG.md b/CHANGELOG.md
index c6133f5..23890bd 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,4 +1,17 @@
# Changelog
+## v0.18.0 - 2026-08-30
+### π Fixes
+- [Patch]Restore scene-authored loading in Environment panel (bac0530β¦)
+- [Patch] Fixed directional light rotation - gizmo (81b1a0dβ¦)
+- [Patch] Fixed parent-child selection in viewport (41df956β¦)
+- [Patch] Fixed the active directional light (1c17fddβ¦)
+- [Patch] Re-added Import textures features (ea5ed67β¦)
+- [Patch] Allow exporting blender files (bd0308eβ¦)
+- [Patch] Show material texture details on inspector hover (37c4b13β¦)
+- [Patch] Coalesce gizmo drag updates during viewport transforms (6ea624cβ¦)
+- [Patch] Removed TaskBar View test (75afd7eβ¦)
+### π Features
+- [Feature] Add Height/POM material channel to Inspector (d460c63β¦)
## v0.16.0 - 2026-08-12
### π Fixes
- [Patch] fix area light mesh culling (9122f78β¦)
diff --git a/LICENSE b/LICENSE
index 0a04128..d7e38a8 100644
--- a/LICENSE
+++ b/LICENSE
@@ -1,165 +1,373 @@
- GNU LESSER GENERAL PUBLIC LICENSE
- Version 3, 29 June 2007
+Mozilla Public License Version 2.0
+==================================
- Copyright (C) 2007 Free Software Foundation, Inc.
- Everyone is permitted to copy and distribute verbatim copies
- of this license document, but changing it is not allowed.
+1. Definitions
+--------------
+1.1. "Contributor"
+ means each individual or legal entity that creates, contributes to
+ the creation of, or owns Covered Software.
- This version of the GNU Lesser General Public License incorporates
-the terms and conditions of version 3 of the GNU General Public
-License, supplemented by the additional permissions listed below.
+1.2. "Contributor Version"
+ means the combination of the Contributions of others (if any) used
+ by a Contributor and that particular Contributor's Contribution.
- 0. Additional Definitions.
+1.3. "Contribution"
+ means Covered Software of a particular Contributor.
- As used herein, "this License" refers to version 3 of the GNU Lesser
-General Public License, and the "GNU GPL" refers to version 3 of the GNU
-General Public License.
+1.4. "Covered Software"
+ means Source Code Form to which the initial Contributor has attached
+ the notice in Exhibit A, the Executable Form of such Source Code
+ Form, and Modifications of such Source Code Form, in each case
+ including portions thereof.
- "The Library" refers to a covered work governed by this License,
-other than an Application or a Combined Work as defined below.
-
- An "Application" is any work that makes use of an interface provided
-by the Library, but which is not otherwise based on the Library.
-Defining a subclass of a class defined by the Library is deemed a mode
-of using an interface provided by the Library.
-
- A "Combined Work" is a work produced by combining or linking an
-Application with the Library. The particular version of the Library
-with which the Combined Work was made is also called the "Linked
-Version".
-
- The "Minimal Corresponding Source" for a Combined Work means the
-Corresponding Source for the Combined Work, excluding any source code
-for portions of the Combined Work that, considered in isolation, are
-based on the Application, and not on the Linked Version.
-
- The "Corresponding Application Code" for a Combined Work means the
-object code and/or source code for the Application, including any data
-and utility programs needed for reproducing the Combined Work from the
-Application, but excluding the System Libraries of the Combined Work.
-
- 1. Exception to Section 3 of the GNU GPL.
-
- You may convey a covered work under sections 3 and 4 of this License
-without being bound by section 3 of the GNU GPL.
-
- 2. Conveying Modified Versions.
-
- If you modify a copy of the Library, and, in your modifications, a
-facility refers to a function or data to be supplied by an Application
-that uses the facility (other than as an argument passed when the
-facility is invoked), then you may convey a copy of the modified
-version:
-
- a) under this License, provided that you make a good faith effort to
- ensure that, in the event an Application does not supply the
- function or data, the facility still operates, and performs
- whatever part of its purpose remains meaningful, or
-
- b) under the GNU GPL, with none of the additional permissions of
- this License applicable to that copy.
-
- 3. Object Code Incorporating Material from Library Header Files.
-
- The object code form of an Application may incorporate material from
-a header file that is part of the Library. You may convey such object
-code under terms of your choice, provided that, if the incorporated
-material is not limited to numerical parameters, data structure
-layouts and accessors, or small macros, inline functions and templates
-(ten or fewer lines in length), you do both of the following:
-
- a) Give prominent notice with each copy of the object code that the
- Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the object code with a copy of the GNU GPL and this license
- document.
-
- 4. Combined Works.
-
- You may convey a Combined Work under terms of your choice that,
-taken together, effectively do not restrict modification of the
-portions of the Library contained in the Combined Work and reverse
-engineering for debugging such modifications, if you also do each of
-the following:
-
- a) Give prominent notice with each copy of the Combined Work that
- the Library is used in it and that the Library and its use are
- covered by this License.
-
- b) Accompany the Combined Work with a copy of the GNU GPL and this license
- document.
-
- c) For a Combined Work that displays copyright notices during
- execution, include the copyright notice for the Library among
- these notices, as well as a reference directing the user to the
- copies of the GNU GPL and this license document.
-
- d) Do one of the following:
-
- 0) Convey the Minimal Corresponding Source under the terms of this
- License, and the Corresponding Application Code in a form
- suitable for, and under terms that permit, the user to
- recombine or relink the Application with a modified version of
- the Linked Version to produce a modified Combined Work, in the
- manner specified by section 6 of the GNU GPL for conveying
- Corresponding Source.
-
- 1) Use a suitable shared library mechanism for linking with the
- Library. A suitable mechanism is one that (a) uses at run time
- a copy of the Library already present on the user's computer
- system, and (b) will operate properly with a modified version
- of the Library that is interface-compatible with the Linked
- Version.
-
- e) Provide Installation Information, but only if you would otherwise
- be required to provide such information under section 6 of the
- GNU GPL, and only to the extent that such information is
- necessary to install and execute a modified version of the
- Combined Work produced by recombining or relinking the
- Application with a modified version of the Linked Version. (If
- you use option 4d0, the Installation Information must accompany
- the Minimal Corresponding Source and Corresponding Application
- Code. If you use option 4d1, you must provide the Installation
- Information in the manner specified by section 6 of the GNU GPL
- for conveying Corresponding Source.)
-
- 5. Combined Libraries.
-
- You may place library facilities that are a work based on the
-Library side by side in a single library together with other library
-facilities that are not Applications and are not covered by this
-License, and convey such a combined library under terms of your
-choice, if you do both of the following:
-
- a) Accompany the combined library with a copy of the same work based
- on the Library, uncombined with any other library facilities,
- conveyed under the terms of this License.
-
- b) Give prominent notice with the combined library that part of it
- is a work based on the Library, and explaining where to find the
- accompanying uncombined form of the same work.
-
- 6. Revised Versions of the GNU Lesser General Public License.
-
- The Free Software Foundation may publish revised and/or new versions
-of the GNU Lesser General Public License from time to time. Such new
-versions will be similar in spirit to the present version, but may
-differ in detail to address new problems or concerns.
-
- Each version is given a distinguishing version number. If the
-Library as you received it specifies that a certain numbered version
-of the GNU Lesser General Public License "or any later version"
-applies to it, you have the option of following the terms and
-conditions either of that published version or of any later version
-published by the Free Software Foundation. If the Library as you
-received it does not specify a version number of the GNU Lesser
-General Public License, you may choose any version of the GNU Lesser
-General Public License ever published by the Free Software Foundation.
-
- If the Library as you received it specifies that a proxy can decide
-whether future versions of the GNU Lesser General Public License shall
-apply, that proxy's public statement of acceptance of any version is
-permanent authorization for you to choose that version for the
-Library.
+1.5. "Incompatible With Secondary Licenses"
+ means
+
+ (a) that the initial Contributor has attached the notice described
+ in Exhibit B to the Covered Software; or
+
+ (b) that the Covered Software was made available under the terms of
+ version 1.1 or earlier of the License, but not also under the
+ terms of a Secondary License.
+
+1.6. "Executable Form"
+ means any form of the work other than Source Code Form.
+
+1.7. "Larger Work"
+ means a work that combines Covered Software with other material, in
+ a separate file or files, that is not Covered Software.
+
+1.8. "License"
+ means this document.
+
+1.9. "Licensable"
+ means having the right to grant, to the maximum extent possible,
+ whether at the time of the initial grant or subsequently, any and
+ all of the rights conveyed by this License.
+
+1.10. "Modifications"
+ means any of the following:
+
+ (a) any file in Source Code Form that results from an addition to,
+ deletion from, or modification of the contents of Covered
+ Software; or
+
+ (b) any new file in Source Code Form that contains any Covered
+ Software.
+
+1.11. "Patent Claims" of a Contributor
+ means any patent claim(s), including without limitation, method,
+ process, and apparatus claims, in any patent Licensable by such
+ Contributor that would be infringed, but for the grant of the
+ License, by the making, using, selling, offering for sale, having
+ made, import, or transfer of either its Contributions or its
+ Contributor Version.
+
+1.12. "Secondary License"
+ means either the GNU General Public License, Version 2.0, the GNU
+ Lesser General Public License, Version 2.1, the GNU Affero General
+ Public License, Version 3.0, or any later versions of those
+ licenses.
+
+1.13. "Source Code Form"
+ means the form of the work preferred for making modifications.
+
+1.14. "You" (or "Your")
+ means an individual or a legal entity exercising rights under this
+ License. For legal entities, "You" includes any entity that
+ controls, is controlled by, or is under common control with You.
+ For purposes of this definition, "control" means (a) the power,
+ direct or indirect, to cause the direction or management of such
+ entity, whether by contract or otherwise, or (b) ownership of more
+ than fifty percent (50%) of the outstanding shares or membership
+ interests of such entity.
+
+2. License Grants and Conditions
+---------------------------------
+
+2.1. Grants
+
+Each Contributor hereby grants You a world-wide, royalty-free,
+non-exclusive license:
+
+(a) under intellectual property rights (other than patent or trademark)
+ Licensable by such Contributor to use, reproduce, make available,
+ modify, display, perform, distribute, and otherwise exploit its
+ Contributions, either on an unmodified basis, with Modifications, or
+ as part of a Larger Work; and
+
+(b) under Patent Claims of such Contributor to make, use, sell, offer
+ for sale, have made, import, and otherwise transfer either its
+ Contributions or its Contributor Version.
+
+2.2. Effective Date
+
+The licenses granted in Section 2.1 with respect to any Contribution
+become effective for each Contribution on the date the Contributor first
+distributes such Contribution.
+
+2.3. Limitations on Grant Scope
+
+The licenses granted in this Section 2 are the only rights granted under
+this License. No additional rights or licenses will be implied from the
+distribution or licensing of Covered Software under this License.
+Notwithstanding Section 2.1(b) above, no patent license is granted by a
+Contributor:
+
+(a) for any code that a Contributor has removed from Covered Software;
+ or
+
+(b) for infringements caused by: (i) Your and any other third party's
+ modifications of Covered Software, or (ii) the combination of its
+ Contributions with other software (except as part of its Contributor
+ Version); or
+
+(c) under Patent Claims infringed by Covered Software in the absence of
+ its Contributions.
+
+This License does not grant any rights in the trademarks, service marks,
+or logos of any Contributor (except as may be necessary to comply with
+the notice requirements in Section 3.4).
+
+2.4. Subsequent Licenses
+
+No Contributor makes additional grants as a result of Your choice to
+distribute the Covered Software under a subsequent version of this
+License (see Section 10.2) or under the terms of a Secondary License (if
+permitted under the terms of Section 3.3).
+
+2.5. Representation
+
+Each Contributor represents that the Contributor believes its
+Contributions are its original creation(s) or it has sufficient rights
+to grant the rights to its Contributions conveyed by this License.
+
+2.6. Fair Use
+
+This License is not intended to limit any rights You have under
+applicable copyright doctrines of fair use, fair dealing, or other
+equivalents.
+
+2.7. Conditions
+
+Sections 3.1, 3.2, 3.3, and 3.4 are conditions of the licenses granted
+in Section 2.1.
+
+3. Responsibilities
+-------------------
+
+3.1. Distribution of Source Form
+
+All distribution of Covered Software in Source Code Form, including any
+Modifications that You create or to which You contribute, must be under
+the terms of this License. You must inform recipients that the Source
+Code Form of the Covered Software is governed by the terms of this
+License, and how they can obtain a copy of this License. You may not
+attempt to alter or restrict the recipients' rights in the Source Code
+Form.
+
+3.2. Distribution of Executable Form
+
+If You distribute Covered Software in Executable Form then:
+
+(a) such Covered Software must also be made available in Source Code
+ Form, as described in Section 3.1, and You must inform recipients of
+ the Executable Form how they can obtain a copy of such Source Code
+ Form by reasonable means in a timely manner, at a charge no more
+ than the cost of distribution to the recipient; and
+
+(b) You may distribute such Executable Form under the terms of this
+ License, or sublicense it under different terms, provided that the
+ license for the Executable Form does not attempt to limit or alter
+ the recipients' rights in the Source Code Form under this License.
+
+3.3. Distribution of a Larger Work
+
+You may create and distribute a Larger Work under terms of Your choice,
+provided that You also comply with the requirements of this License for
+the Covered Software. If the Larger Work is a combination of Covered
+Software with a work governed by one or more Secondary Licenses, and the
+Covered Software is not Incompatible With Secondary Licenses, this
+License permits You to additionally distribute such Covered Software
+under the terms of such Secondary License(s), so that the recipient of
+the Larger Work may, at their option, further distribute the Covered
+Software under the terms of either this License or such Secondary
+License(s).
+
+3.4. Notices
+
+You may not remove or alter the substance of any license notices
+(including copyright notices, patent notices, disclaimers of warranty,
+or limitations of liability) contained within the Source Code Form of
+the Covered Software, except that You may alter any license notices to
+the extent required to remedy known factual inaccuracies.
+
+3.5. Application of Additional Terms
+
+You may choose to offer, and to charge a fee for, warranty, support,
+indemnity or liability obligations to one or more recipients of Covered
+Software. However, You may do so only on Your own behalf, and not on
+behalf of any Contributor. You must make it absolutely clear that any
+such warranty, support, indemnity, or liability obligation is offered by
+You alone, and You hereby agree to indemnify every Contributor for any
+liability incurred by such Contributor as a result of warranty, support,
+indemnity or liability terms You offer. You may include additional
+disclaimers of warranty and limitations of liability specific to any
+jurisdiction.
+
+4. Inability to Comply Due to Statute or Regulation
+---------------------------------------------------
+
+If it is impossible for You to comply with any of the terms of this
+License with respect to some or all of the Covered Software due to
+statute, judicial order, or regulation then You must: (a) comply with
+the terms of this License to the maximum extent possible; and (b)
+describe the limitations and the code they affect. Such description must
+be placed in a text file included with all distributions of the Covered
+Software under this License. Except to the extent prohibited by statute
+or regulation, such description must be sufficiently detailed for a
+recipient of ordinary skill to be able to understand it.
+
+5. Termination
+--------------
+
+5.1. The rights granted under this License will terminate automatically
+if You fail to comply with any of its terms. However, if You become
+compliant, then the rights granted under this License from a particular
+Contributor are reinstated (a) provisionally, unless and until such
+Contributor explicitly and finally terminates Your grants, and (b) on an
+ongoing basis, if such Contributor fails to notify You of the
+non-compliance by some reasonable means prior to 60 days after You have
+come back into compliance. Moreover, Your grants from a particular
+Contributor are reinstated on an ongoing basis if such Contributor
+notifies You of the non-compliance by some reasonable means, this is the
+first time You have received notice of non-compliance with this License
+from such Contributor, and You become compliant prior to 30 days after
+Your receipt of the notice.
+
+5.2. If You initiate litigation against any entity by asserting a patent
+infringement claim (excluding declaratory judgment actions,
+counter-claims, and cross-claims) alleging that a Contributor Version
+directly or indirectly infringes any patent, then the rights granted to
+You by any and all Contributors for the Covered Software under Section
+2.1 of this License shall terminate.
+
+5.3. In the event of termination under Sections 5.1 or 5.2 above, all
+end user license agreements (excluding distributors and resellers) which
+have been validly granted by You or Your distributors under this License
+prior to termination shall survive termination.
+
+************************************************************************
+* *
+* 6. Disclaimer of Warranty *
+* ------------------------- *
+* *
+* Covered Software is provided under this License on an "as is" *
+* basis, without warranty of any kind, either expressed, implied, or *
+* statutory, including, without limitation, warranties that the *
+* Covered Software is free of defects, merchantable, fit for a *
+* particular purpose or non-infringing. The entire risk as to the *
+* quality and performance of the Covered Software is with You. *
+* Should any Covered Software prove defective in any respect, You *
+* (not any Contributor) assume the cost of any necessary servicing, *
+* repair, or correction. This disclaimer of warranty constitutes an *
+* essential part of this License. No use of any Covered Software is *
+* authorized under this License except under this disclaimer. *
+* *
+************************************************************************
+
+************************************************************************
+* *
+* 7. Limitation of Liability *
+* -------------------------- *
+* *
+* Under no circumstances and under no legal theory, whether tort *
+* (including negligence), contract, or otherwise, shall any *
+* Contributor, or anyone who distributes Covered Software as *
+* permitted above, be liable to You for any direct, indirect, *
+* special, incidental, or consequential damages of any character *
+* including, without limitation, damages for lost profits, loss of *
+* goodwill, work stoppage, computer failure or malfunction, or any *
+* and all other commercial damages or losses, even if such party *
+* shall have been advised of the possibility of such damages. This *
+* limitation of liability shall not apply to liability for death or *
+* personal injury resulting from such party's negligence to the *
+* extent applicable law prohibits such limitation. Some *
+* jurisdictions do not allow the exclusion or limitation of *
+* incidental or consequential damages, so this exclusion and *
+* limitation may not apply to You. *
+* *
+************************************************************************
+
+8. Litigation
+-------------
+
+Any litigation relating to this License may be brought only in the
+courts of a jurisdiction where the defendant maintains its principal
+place of business and such litigation shall be governed by laws of that
+jurisdiction, without reference to its conflict-of-law provisions.
+Nothing in this Section shall prevent a party's ability to bring
+cross-claims or counter-claims.
+
+9. Miscellaneous
+----------------
+
+This License represents the complete agreement concerning the subject
+matter hereof. If any provision of this License is held to be
+unenforceable, such provision shall be reformed only to the extent
+necessary to make it enforceable. Any law or regulation which provides
+that the language of a contract shall be construed against the drafter
+shall not be used to construe this License against a Contributor.
+
+10. Versions of the License
+---------------------------
+
+10.1. New Versions
+
+Mozilla Foundation is the license steward. Except as provided in Section
+10.3, no one other than the license steward has the right to modify or
+publish new versions of this License. Each version will be given a
+distinguishing version number.
+
+10.2. Effect of New Versions
+
+You may distribute the Covered Software under the terms of the version
+of the License under which You originally received the Covered Software,
+or under the terms of any subsequent version published by the license
+steward.
+
+10.3. Modified Versions
+
+If you create software not governed by this License, and you want to
+create a new license for such software, you may create and use a
+modified version of this License if you rename the license and remove
+any references to the name of the license steward (except to note that
+such modified license differs from this License).
+
+10.4. Distributing Source Code Form that is Incompatible With Secondary
+Licenses
+
+If You choose to distribute Source Code Form that is Incompatible With
+Secondary Licenses under the terms of this version of the License, the
+notice described in Exhibit B of this License must be attached.
+
+Exhibit A - Source Code Form License Notice
+-------------------------------------------
+
+ This Source Code Form is subject to the terms of the Mozilla Public
+ License, v. 2.0. If a copy of the MPL was not distributed with this
+ file, You can obtain one at http://mozilla.org/MPL/2.0/.
+
+If it is not possible or desirable to put the notice in a particular
+file, then You may include the notice in a location (such as a LICENSE
+file in a relevant directory) where a recipient would be likely to look
+for such a notice.
+
+You may add additional accurate notices of copyright ownership.
+
+Exhibit B - "Incompatible With Secondary Licenses" Notice
+---------------------------------------------------------
+
+ This Source Code Form is "Incompatible With Secondary Licenses", as
+ defined by the Mozilla Public License, v. 2.0.
diff --git a/Makefile b/Makefile
index 5c39494..d69732c 100644
--- a/Makefile
+++ b/Makefile
@@ -14,10 +14,26 @@ clean:
test:
swift test
+# Test against the local UntoldEngine package using a stable build path
+localtest:
+ swift test --build-path /tmp/untoldeditor-local-build
+
+# Required SwiftFormat version
+SWIFTFORMAT_VERSION := 0.60.1
+
+# Verify installed SwiftFormat matches the required version
+check-swiftformat-version:
+ @INSTALLED=$$(swiftformat --version 2>&1 | awk '{print $$NF}'); \
+ if [ "$$INSTALLED" != "$(SWIFTFORMAT_VERSION)" ]; then \
+ echo "Error: swiftformat $(SWIFTFORMAT_VERSION) required, but found $$INSTALLED"; \
+ echo "Install from: https://github.com/nicklockwood/SwiftFormat/releases/tag/$(SWIFTFORMAT_VERSION)"; \
+ exit 1; \
+ fi
+
# Lint Swift files using SwiftFormat
-lint:
+lint: check-swiftformat-version
swiftformat --lint . --swiftversion 5.8 --reporter github-actions-log
# Auto-format Swift files (for convenience)
-format:
+format: check-swiftformat-version
swiftformat . --swiftversion 5.8 --quiet
diff --git a/Package.swift b/Package.swift
index e2987a9..e0613c1 100644
--- a/Package.swift
+++ b/Package.swift
@@ -13,7 +13,7 @@ let package = Package(
// Use a branch during active development:
// .package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop"),
// Or pin to a release:
- .package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.16.0"),
+ .package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.18.0"),
],
targets: [
.executableTarget(
diff --git a/README.md b/README.md
index 87f3e25..3270c32 100644
--- a/README.md
+++ b/README.md
@@ -179,5 +179,5 @@ See **CONTRIBUTING.md** and **CODE_OF_CONDUCT.md** (coming soon).
## π License
-Licensed under **GNU LGPL v3.0**.
+Licensed under **Mozilla Public License 2.0**.
See [LICENSE](LICENSE) for details.
diff --git a/Sources/UntoldEditor/Build/CreateProjectView.swift b/Sources/UntoldEditor/Build/CreateProjectView.swift
index 4ac978e..8922aa4 100644
--- a/Sources/UntoldEditor/Build/CreateProjectView.swift
+++ b/Sources/UntoldEditor/Build/CreateProjectView.swift
@@ -2,9 +2,11 @@
// CreateProjectView.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import SwiftUI
diff --git a/Sources/UntoldEditor/Config/EditorFeatureFlags.swift b/Sources/UntoldEditor/Config/EditorFeatureFlags.swift
index fb795b1..7b366dd 100644
--- a/Sources/UntoldEditor/Config/EditorFeatureFlags.swift
+++ b/Sources/UntoldEditor/Config/EditorFeatureFlags.swift
@@ -2,9 +2,11 @@
// EditorFeatureFlags.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
diff --git a/Sources/UntoldEditor/Editor/AssetBrowserView.swift b/Sources/UntoldEditor/Editor/AssetBrowserView.swift
index 87b5239..e8f1745 100644
--- a/Sources/UntoldEditor/Editor/AssetBrowserView.swift
+++ b/Sources/UntoldEditor/Editor/AssetBrowserView.swift
@@ -2,9 +2,11 @@
// AssetBrowserView.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import SwiftUI
import UniformTypeIdentifiers
@@ -12,8 +14,9 @@ import UntoldEngine
private let runtimeAssetExtension = "untold"
private let runtimeTextureFolderNames = ["Textures", "textures"]
-private let sourceAssetExtensions: Set = ["usd", "usda", "usdc", "usdz"]
+private let sourceAssetExtensions: Set = ["usd", "usda", "usdc", "usdz", "blend"]
private let streamModelResourceFolderNames = ["tile_exports", "tile_export", "Textures", "textures"]
+private let materialTextureExtensions: Set = ["png", "jpg", "jpeg", "tif", "tiff"]
struct RuntimeExportRequest: Identifiable, Equatable {
let id = UUID()
@@ -318,6 +321,66 @@ enum AssetCategory: String, CaseIterable {
}
}
+func editorTextureType(from filename: String) -> TextureType? {
+ let lowercasedName = filename.lowercased()
+
+ if lowercasedName.contains("basecolor")
+ || lowercasedName.contains("base_color")
+ || lowercasedName.contains("albedo")
+ || lowercasedName.contains("diffuse")
+ || lowercasedName.contains("color")
+ {
+ return .baseColor
+ } else if lowercasedName.contains("roughness") || lowercasedName.contains("rough") {
+ return .roughness
+ } else if lowercasedName.contains("metallic") || lowercasedName.contains("metalness") || lowercasedName.contains("metal") {
+ return .metallic
+ } else if lowercasedName.contains("normalgx")
+ || lowercasedName.contains("normaldx")
+ || lowercasedName.contains("normalgl")
+ || lowercasedName.contains("normal")
+ || lowercasedName.contains("nrm")
+ {
+ return .normal
+ } else if lowercasedName.contains("height")
+ || lowercasedName.contains("displacement")
+ || lowercasedName.contains("displace")
+ || lowercasedName.contains("bump")
+ {
+ return .height
+ }
+
+ return nil
+}
+
+func editorMaterialTextureAssignments(in folder: URL, fileManager: FileManager = .default) -> [TextureType: URL] {
+ guard let enumerator = fileManager.enumerator(
+ at: folder,
+ includingPropertiesForKeys: [.isRegularFileKey],
+ options: [.skipsHiddenFiles]
+ ) else {
+ return [:]
+ }
+
+ let textureURLs = enumerator.compactMap { item -> URL? in
+ guard let url = item as? URL else { return nil }
+ guard materialTextureExtensions.contains(url.pathExtension.lowercased()) else { return nil }
+ return url
+ }
+
+ var assignments: [TextureType: URL] = [:]
+ for url in textureURLs.sorted(by: { $0.lastPathComponent.localizedCaseInsensitiveCompare($1.lastPathComponent) == .orderedAscending }) {
+ guard let textureType = editorTextureType(from: url.deletingPathExtension().lastPathComponent),
+ assignments[textureType] == nil
+ else {
+ continue
+ }
+ assignments[textureType] = url
+ }
+
+ return assignments
+}
+
private struct RemoteStreamImportSheet: View {
@Binding var urlString: String
var onImport: () -> Void
@@ -523,10 +586,10 @@ struct AssetBrowserView: View {
importAssetForCategory(category, into: currentDirectoryURL)
}
- // Returns AnyView (not `some View`) so the recursive child call is allowed.
- // `category == nil` marks a generic (non-category) folder such as the root
- // or a custom directory created at root level. `url` may be nil for a
- // category root when no project folder is set yet.
+ /// Returns AnyView (not `some View`) so the recursive child call is allowed.
+ /// `category == nil` marks a generic (non-category) folder such as the root
+ /// or a custom directory created at root level. `url` may be nil for a
+ /// category root when no project folder is set yet.
private func directoryNode(url: URL?, name: String, category: String?, depth: Int) -> AnyView {
let isGeneric = (category == nil)
let subfolders: [URL] = {
@@ -608,8 +671,8 @@ struct AssetBrowserView: View {
)
}
- // Root node of the directory tree (the project's asset folder). Right-click
- // to create a directory at root level.
+ /// Root node of the directory tree (the project's asset folder). Right-click
+ /// to create a directory at root level.
private var rootDirectoryRow: some View {
let root = assetBasePath
let isSelected = root.map { r in selectedDirURL?.standardizedFileURL == r.standardizedFileURL } ?? false
@@ -1025,7 +1088,7 @@ struct AssetBrowserView: View {
.font(.title2)
.bold()
- Text("This USD file needs to be converted to Untold Engine's .untold runtime format before it can be added to your project.")
+ Text("This USD or .blend file needs to be converted to Untold Engine's .untold runtime format before it can be added to your project.")
.fixedSize(horizontal: false, vertical: true)
VStack(alignment: .leading, spacing: 6) {
@@ -1275,7 +1338,7 @@ struct AssetBrowserView: View {
.font(.title2)
.bold()
- Text("This USD file will be partitioned into tile payloads and a manifest JSON using export-untold-tiles.")
+ Text("This USD or .blend file will be partitioned into tile payloads and a manifest JSON using export-untold-tiles.")
.fixedSize(horizontal: false, vertical: true)
VStack(alignment: .leading, spacing: 6) {
@@ -1695,8 +1758,8 @@ struct AssetBrowserView: View {
.cornerRadius(6)
}
- // Shared row list used by both the flat-category listing and
- // folderContentsView. Only the folder-tap navigation differs per call site.
+ /// Shared row list used by both the flat-category listing and
+ /// folderContentsView. Only the folder-tap navigation differs per call site.
private func assetRowList(_ assets: [Asset], spacing: CGFloat, onFolderTap: @escaping (Asset) -> Void) -> some View {
VStack(alignment: .leading, spacing: spacing) {
ForEach(assets) { asset in
@@ -1834,6 +1897,54 @@ struct AssetBrowserView: View {
// MARK: - Add Model with Double Click
+ private func selectedMaterialTarget() -> (entityId: EntityID, meshIndex: Int)? {
+ if let inspectedMesh = selectionManager.inspectedMesh,
+ inspectedMesh.entityId != .invalid,
+ hasComponent(entityId: inspectedMesh.entityId, componentType: RenderComponent.self)
+ {
+ return (inspectedMesh.entityId, inspectedMesh.meshIndex)
+ }
+
+ guard let entityId = selectionManager.selectedEntity,
+ entityId != .invalid,
+ hasComponent(entityId: entityId, componentType: RenderComponent.self)
+ else {
+ return nil
+ }
+
+ return (entityId, 0)
+ }
+
+ private func assignMaterialFolder(_ asset: Asset) {
+ guard asset.category == AssetCategory.materials.rawValue, asset.isFolder else {
+ return
+ }
+
+ guard let target = selectedMaterialTarget() else {
+ showStatus("Select a mesh before assigning materials", isError: true)
+ return
+ }
+
+ let assignments = editorMaterialTextureAssignments(in: asset.path)
+ guard assignments.isEmpty == false else {
+ showStatus("No recognized material textures in \(asset.name)", isError: true)
+ return
+ }
+
+ for textureType in TextureType.allCases {
+ guard let textureURL = assignments[textureType] else { continue }
+ updateMaterialTexture(
+ entityId: target.entityId,
+ textureType: textureType,
+ path: textureURL,
+ meshIndex: target.meshIndex
+ )
+ }
+
+ selectionManager.objectWillChange.send()
+ showStatus("Assigned \(assignments.count) material textures from \(asset.name)")
+ }
+
private func resolvedRuntimeAsset(for asset: Asset) -> Asset? {
guard asset.isFolder else { return asset }
guard asset.category == AssetCategory.models.rawValue || asset.category == AssetCategory.animations.rawValue else {
@@ -2006,6 +2117,11 @@ struct AssetBrowserView: View {
}
private func handle_add_model_double_click(asset: Asset) {
+ if asset.category == AssetCategory.materials.rawValue, asset.isFolder {
+ assignMaterialFolder(asset)
+ return
+ }
+
if asset.category == AssetCategory.streamModels.rawValue {
if asset.path.pathExtension.lowercased() == "remotestream" {
loadRemoteStreamModel(from: asset)
diff --git a/Sources/UntoldEditor/Editor/AssetNodeEditingPolicy.swift b/Sources/UntoldEditor/Editor/AssetNodeEditingPolicy.swift
index 635e6c6..6609318 100644
--- a/Sources/UntoldEditor/Editor/AssetNodeEditingPolicy.swift
+++ b/Sources/UntoldEditor/Editor/AssetNodeEditingPolicy.swift
@@ -2,9 +2,11 @@
// AssetNodeEditingPolicy.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import UntoldEngine
diff --git a/Sources/UntoldEditor/Editor/AssetWindowDelegate.swift b/Sources/UntoldEditor/Editor/AssetWindowDelegate.swift
index ff81547..499cc33 100644
--- a/Sources/UntoldEditor/Editor/AssetWindowDelegate.swift
+++ b/Sources/UntoldEditor/Editor/AssetWindowDelegate.swift
@@ -1,3 +1,9 @@
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
import AppKit
final class AssetWindowDelegate: NSObject, NSWindowDelegate {
diff --git a/Sources/UntoldEditor/Editor/ComponentEditorForm.swift b/Sources/UntoldEditor/Editor/ComponentEditorForm.swift
index c19e231..50ce69d 100644
--- a/Sources/UntoldEditor/Editor/ComponentEditorForm.swift
+++ b/Sources/UntoldEditor/Editor/ComponentEditorForm.swift
@@ -2,9 +2,11 @@
// ComponentEditorForm.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
import SwiftUI
diff --git a/Sources/UntoldEditor/Editor/DemoGalleryView.swift b/Sources/UntoldEditor/Editor/DemoGalleryView.swift
index c711f26..b32fc70 100644
--- a/Sources/UntoldEditor/Editor/DemoGalleryView.swift
+++ b/Sources/UntoldEditor/Editor/DemoGalleryView.swift
@@ -2,9 +2,11 @@
// DemoGalleryView.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import SwiftUI
diff --git a/Sources/UntoldEditor/Editor/DemoSceneCatalog.swift b/Sources/UntoldEditor/Editor/DemoSceneCatalog.swift
index 6bdab82..ca6aa61 100644
--- a/Sources/UntoldEditor/Editor/DemoSceneCatalog.swift
+++ b/Sources/UntoldEditor/Editor/DemoSceneCatalog.swift
@@ -2,9 +2,11 @@
// DemoSceneCatalog.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
diff --git a/Sources/UntoldEditor/Editor/EditorController.swift b/Sources/UntoldEditor/Editor/EditorController.swift
index b2ba6c5..bf797f4 100644
--- a/Sources/UntoldEditor/Editor/EditorController.swift
+++ b/Sources/UntoldEditor/Editor/EditorController.swift
@@ -2,9 +2,11 @@
// EditorController.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import AppKit
import Foundation
@@ -77,6 +79,15 @@ class EditorController: SelectionDelegate, ObservableObject {
@Published var activeAxis: TransformAxis = .none
@Published var currentSceneURL: URL?
+ /// refreshInspector() can be called once per rendered frame while a gizmo is being
+ /// dragged (see EditorUntoldRenderer.handleSceneInput). Each call forces SwiftUI to
+ /// re-diff the whole Inspector, which is far too expensive to do 60 times a second,
+ /// so calls are throttled to minInspectorRefreshInterval with a trailing edge to
+ /// guarantee the final value is always reflected once dragging settles.
+ private var lastInspectorRefreshTime: TimeInterval = 0
+ private var pendingInspectorRefresh: DispatchWorkItem?
+ private let minInspectorRefreshInterval: TimeInterval = 1.0 / 15.0
+
init(selectionManager: SelectionManager) {
self.selectionManager = selectionManager
selectionDelegate = self
@@ -106,6 +117,24 @@ class EditorController: SelectionDelegate, ObservableObject {
}
func refreshInspector() {
+ pendingInspectorRefresh?.cancel()
+
+ let now = Date().timeIntervalSinceReferenceDate
+ let elapsed = now - lastInspectorRefreshTime
+ guard elapsed >= minInspectorRefreshInterval else {
+ // Too soon since the last refresh β schedule a trailing-edge refresh so the
+ // Inspector still catches up to the latest value once the burst quiets down.
+ let workItem = DispatchWorkItem { [weak self] in
+ guard let self else { return }
+ lastInspectorRefreshTime = Date().timeIntervalSinceReferenceDate
+ selectionManager.objectWillChange.send()
+ }
+ pendingInspectorRefresh = workItem
+ DispatchQueue.main.asyncAfter(deadline: .now() + (minInspectorRefreshInterval - elapsed), execute: workItem)
+ return
+ }
+
+ lastInspectorRefreshTime = now
DispatchQueue.main.async {
self.selectionManager.objectWillChange.send()
}
diff --git a/Sources/UntoldEditor/Editor/EditorMenuCommands.swift b/Sources/UntoldEditor/Editor/EditorMenuCommands.swift
index 50050ae..a2bc49a 100644
--- a/Sources/UntoldEditor/Editor/EditorMenuCommands.swift
+++ b/Sources/UntoldEditor/Editor/EditorMenuCommands.swift
@@ -46,7 +46,7 @@ final class EditorPanelVisibility: ObservableObject {
@Published var showBottomPanel: Bool = true
@Published var showRightPanel: Bool = true
- // Saved layout used by the "focus viewport" toggle (βF).
+ /// Saved layout used by the "focus viewport" toggle (βF).
private var savedLayout: (left: Bool, bottom: Bool, right: Bool)?
private init() {}
diff --git a/Sources/UntoldEditor/Editor/EditorSceneView.swift b/Sources/UntoldEditor/Editor/EditorSceneView.swift
index 9ce3f49..6af9a40 100644
--- a/Sources/UntoldEditor/Editor/EditorSceneView.swift
+++ b/Sources/UntoldEditor/Editor/EditorSceneView.swift
@@ -2,9 +2,11 @@
// EditorSceneView.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import MetalKit
import SwiftUI
diff --git a/Sources/UntoldEditor/Editor/EditorScheme.swift b/Sources/UntoldEditor/Editor/EditorScheme.swift
index 6fc1c8b..dd5729c 100644
--- a/Sources/UntoldEditor/Editor/EditorScheme.swift
+++ b/Sources/UntoldEditor/Editor/EditorScheme.swift
@@ -2,47 +2,56 @@
// EditorScheme.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import SwiftUI
// MARK: - Editor color scheme (Dracula-inspired)
-//
-// Single source of truth for the editor UI palette. All views should reference
-// these semantic tokens instead of hardcoding `Color.white`, `.secondary`,
-// `.red`, opacities, etc. Grouped by role so a re-theme only touches this file.
+
+///
+/// Single source of truth for the editor UI palette. All views should reference
+/// these semantic tokens instead of hardcoding `Color.white`, `.secondary`,
+/// `.red`, opacities, etc. Grouped by role so a re-theme only touches this file.
extension Color {
// MARK: Base surfaces
+
static let editorBackground = Color(red: 0.15, green: 0.16, blue: 0.21) // dracula background
static let editorPanelBackground = Color(red: 0.19, green: 0.20, blue: 0.26) // dracula current line
static let editorSurface = Color(red: 0.24, green: 0.25, blue: 0.32) // dracula selection
// MARK: Accents
+
static let editorAccent = Color(red: 0.91, green: 0.64, blue: 0.35) // muted dracula orange
static let editorAccentSoft = Color(red: 0.91, green: 0.64, blue: 0.35, opacity: 0.16)
static let editorSecondaryAccent = Color(red: 0.74, green: 0.58, blue: 0.98) // dracula purple
// MARK: Text hierarchy
+
static let editorTextPrimary = Color.white // titles, primary labels, text on accent buttons
static let editorTextSecondary = Color.white.opacity(0.70) // supporting / secondary labels
static let editorTextTertiary = Color.white.opacity(0.45) // muted / disabled-looking labels
static let editorTextInverse = Color.black.opacity(0.90) // dark text on light/accent fills
// MARK: Semantic status
+
static let editorError = Color(red: 0.94, green: 0.38, blue: 0.42) // dracula red
static let editorSuccess = Color(red: 0.31, green: 0.82, blue: 0.55) // dracula green
static let editorWarning = Color(red: 0.95, green: 0.78, blue: 0.42) // dracula yellow/orange
static let editorInfo = Color(red: 0.55, green: 0.73, blue: 0.98) // dracula blue/cyan
// MARK: Fills & separators
+
static let editorFillSubtle = Color.white.opacity(0.05) // faint row / zebra backgrounds
static let editorFill = Color.white.opacity(0.10) // subtle panel / hover fills
static let editorDivider = Color.white.opacity(0.10) // borders, strokes, separators
static let editorDisabled = Color.white.opacity(0.15) // disabled control backgrounds
// MARK: Overlays & shadows
+
static let editorShadow = Color.black.opacity(0.20) // default drop shadows
static let editorShadowStrong = Color.black.opacity(0.34) // elevated cards / popovers
static let editorScrim = Color.black.opacity(0.40) // floating stat cards over the scene
diff --git a/Sources/UntoldEditor/Editor/EditorUndoManager.swift b/Sources/UntoldEditor/Editor/EditorUndoManager.swift
index d49e219..aba8654 100644
--- a/Sources/UntoldEditor/Editor/EditorUndoManager.swift
+++ b/Sources/UntoldEditor/Editor/EditorUndoManager.swift
@@ -2,9 +2,11 @@
// EditorUndoManager.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
diff --git a/Sources/UntoldEditor/Editor/EditorView.swift b/Sources/UntoldEditor/Editor/EditorView.swift
index 656888e..cd67d0a 100644
--- a/Sources/UntoldEditor/Editor/EditorView.swift
+++ b/Sources/UntoldEditor/Editor/EditorView.swift
@@ -1,3 +1,9 @@
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
import Combine
import MetalKit
import SwiftUI
@@ -446,8 +452,8 @@ public struct EditorView: View {
case effects
}
- // Right panel is contextual: the project shows Environment/Effects (with a
- // themed segmented switch); a selected object shows the Inspector.
+ /// Right panel is contextual: the project shows Environment/Effects (with a
+ /// themed segmented switch); a selected object shows the Inspector.
private var editorRightPanel: some View {
Group {
if selectionManager.projectSelected {
@@ -492,7 +498,7 @@ public struct EditorView: View {
}
}
- // Inspector shown when the active scene is selected in the Scene Graph.
+ /// Inspector shown when the active scene is selected in the Scene Graph.
private var sceneInspector: some View {
let sceneName = editorController?.currentSceneURL?.deletingPathExtension().lastPathComponent ?? "Untitled Scene"
return VStack(alignment: .leading, spacing: 10) {
@@ -567,8 +573,8 @@ public struct EditorView: View {
.focusable(false)
}
- // Themed segmented selector matching the editor style (accent-filled active
- // segment inside a rounded surface container).
+ /// Themed segmented selector matching the editor style (accent-filled active
+ /// segment inside a rounded surface container).
private var editorPanelTabs: some View {
HStack(spacing: 2) {
panelTabButton(.assets, title: "Assets", icon: "shippingbox")
@@ -603,13 +609,13 @@ public struct EditorView: View {
.focusable(false)
}
- // Bottom dock: a segmented Assets/Console selector (replacing the old
- // native TabView tab strip) plus the selected panel below it.
- // Pause the Metal render loop for the duration of a panel show/hide
- // animation so the viewport doesn't compete with the layout change (which
- // caused stutter). Called from onChange, so it covers every trigger: edge
- // tabs, the View menu (β1/2/3) and Focus Viewport (βF). The viewport freezes
- // on its last frame, then resumes.
+ /// Bottom dock: a segmented Assets/Console selector (replacing the old
+ /// native TabView tab strip) plus the selected panel below it.
+ /// Pause the Metal render loop for the duration of a panel show/hide
+ /// animation so the viewport doesn't compete with the layout change (which
+ /// caused stutter). Called from onChange, so it covers every trigger: edge
+ /// tabs, the View menu (β1/2/3) and Focus Viewport (βF). The viewport freezes
+ /// on its last frame, then resumes.
private func pauseRenderForPanelAnimation() {
renderer?.metalView.isPaused = true
renderPauseGeneration += 1
@@ -621,8 +627,8 @@ public struct EditorView: View {
}
}
- // Small always-visible tab that protrudes from a panel's inner edge (placed
- // as an overlay above the viewport) to collapse/expand the panel.
+ /// Small always-visible tab that protrudes from a panel's inner edge (placed
+ /// as an overlay above the viewport) to collapse/expand the panel.
private func panelEdgeTabVertical(
isOpen: Bool,
openIcon: String,
@@ -1085,8 +1091,8 @@ public struct EditorView: View {
}
}
- // Load a scene file from the project into the (single) ECS world, replacing
- // whatever is currently loaded. Used by the Scene Graph panel.
+ /// Load a scene file from the project into the (single) ECS world, replacing
+ /// whatever is currently loaded. Used by the Scene Graph panel.
private func editor_loadScene(from url: URL) {
guard let sceneData = loadGameScene(from: url) else {
print("β Failed to load scene from \(url.lastPathComponent)")
@@ -1114,7 +1120,7 @@ public struct EditorView: View {
print("β
Scene loaded: \(url.lastPathComponent)")
}
- // Ask before switching scenes: loading discards the current world.
+ /// Ask before switching scenes: loading discards the current world.
private func editor_requestLoadScene(_ url: URL) {
if url == editorController?.currentSceneURL { return }
pendingSceneToLoad = url
@@ -1127,13 +1133,13 @@ public struct EditorView: View {
editor_loadScene(from: url)
}
- // Save the project. Projects persist as folders (scenes + imported assets on
- // disk), so for now this writes the active scene's work into the project.
+ /// Save the project. Projects persist as folders (scenes + imported assets on
+ /// disk), so for now this writes the active scene's work into the project.
private func editor_saveProject() {
editor_handleSave()
}
- // Start a fresh, unsaved scene (File β Add New Scene).
+ /// Start a fresh, unsaved scene (File β Add New Scene).
private func editor_newScene() {
editor_clearScene()
editorController?.currentSceneURL = nil
@@ -1150,6 +1156,13 @@ public struct EditorView: View {
let light = createEntity()
setEntityName(entityId: light, name: "Directional Light")
createDirLight(entityId: light)
+ // destroyAllEntities() above only marks the previous scene's entities for deferred
+ // destruction; the actual cleanup (which nulls activeDirectionalLight if it pointed
+ // at the old light) doesn't run until finalizePendingDestroys() later this frame.
+ // createDirLight()'s "activate only if nil" check can therefore see a stale non-nil
+ // pointer here and skip activating this new light. Force it active explicitly so a
+ // freshly cleared scene never ends up with an inactive (shadow-less) default sun.
+ setDirectionalLight(.active(light))
let sceneCamera = findSceneCamera()
@@ -1239,7 +1252,7 @@ public struct EditorView: View {
sceneGraphModel.refreshHierarchy()
}
- // Delete a specific entity (used by the Scene Graph right-click menu).
+ /// Delete a specific entity (used by the Scene Graph right-click menu).
private func editor_removeEntity(_ entityId: EntityID) {
guard isDerivedAssetNode(entityId) == false else {
print("β οΈ Asset nodes cannot be removed directly")
@@ -1891,7 +1904,7 @@ public struct EditorView: View {
let fileName = fileURL.deletingPathExtension().lastPathComponent
pendingQuickPreviewLoadsInExplore = fromExploreMode
- if isUSDSourceAsset(fileURL) {
+ if isConvertibleSourceAsset(fileURL) {
queueQuickPreviewRuntimeExport(sourceURL: fileURL)
return
}
@@ -2057,8 +2070,8 @@ public struct EditorView: View {
}
}
- private func isUSDSourceAsset(_ url: URL) -> Bool {
- ["usd", "usda", "usdc", "usdz"].contains(url.pathExtension.lowercased())
+ private func isConvertibleSourceAsset(_ url: URL) -> Bool {
+ ["usd", "usda", "usdc", "usdz", "blend"].contains(url.pathExtension.lowercased())
}
private func queueQuickPreviewRuntimeExport(sourceURL: URL) {
@@ -2079,7 +2092,7 @@ public struct EditorView: View {
.font(.title2)
.bold()
- Text("This USD file needs to be converted to Untold Engine's .untold runtime format before it can be previewed.")
+ Text("This USD or .blend file needs to be converted to Untold Engine's .untold runtime format before it can be previewed.")
.fixedSize(horizontal: false, vertical: true)
VStack(alignment: .leading, spacing: 6) {
diff --git a/Sources/UntoldEditor/Editor/EngineStatsView.swift b/Sources/UntoldEditor/Editor/EngineStatsView.swift
index 4adca03..53bbb97 100644
--- a/Sources/UntoldEditor/Editor/EngineStatsView.swift
+++ b/Sources/UntoldEditor/Editor/EngineStatsView.swift
@@ -2,9 +2,11 @@
// EngineStatsView.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Combine
diff --git a/Sources/UntoldEditor/Editor/EnvironmentView.swift b/Sources/UntoldEditor/Editor/EnvironmentView.swift
index f7df0a1..24c90b1 100644
--- a/Sources/UntoldEditor/Editor/EnvironmentView.swift
+++ b/Sources/UntoldEditor/Editor/EnvironmentView.swift
@@ -2,9 +2,11 @@
// EnvironmentView.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import CShaderTypes
import simd
diff --git a/Sources/UntoldEditor/Editor/ExplicitClickTextField.swift b/Sources/UntoldEditor/Editor/ExplicitClickTextField.swift
index ce07ebc..547c9a8 100644
--- a/Sources/UntoldEditor/Editor/ExplicitClickTextField.swift
+++ b/Sources/UntoldEditor/Editor/ExplicitClickTextField.swift
@@ -2,9 +2,11 @@
// ExplicitClickTextField.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
#if canImport(AppKit)
import AppKit
diff --git a/Sources/UntoldEditor/Editor/InspectorView.swift b/Sources/UntoldEditor/Editor/InspectorView.swift
index 02a732f..102ce1f 100644
--- a/Sources/UntoldEditor/Editor/InspectorView.swift
+++ b/Sources/UntoldEditor/Editor/InspectorView.swift
@@ -2,12 +2,15 @@
// InspectorView.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import simd
import SwiftUI
+import UniformTypeIdentifiers
import UntoldEngine
public struct ComponentOption_Editor: Identifiable {
@@ -35,6 +38,65 @@ func openFilePicker() -> URL? {
return panel.runModal() == .OK ? panel.urls.first : nil
}
+private func pickTextureImageFile() -> URL? {
+ let panel = NSOpenPanel()
+ panel.allowsMultipleSelection = false
+ panel.canChooseDirectories = false
+ panel.canChooseFiles = true
+ panel.allowedContentTypes = [.png, .jpeg, .tiff]
+
+ return panel.runModal() == .OK ? panel.urls.first : nil
+}
+
+/// Copies an externally-picked texture image into the project's Materials asset folder,
+/// mirroring the single-file fallback of the Asset Browser's own Materials import (a
+/// subfolder named after the file, matching what updateMaterialTexture(path:) expects to
+/// resolve via LoadingSystem's resource search paths).
+private func importTextureAsset(from sourceURL: URL) -> URL? {
+ guard let basePath = assetBasePath else {
+ Logger.logWarning(message: "[InspectorView] assetBasePath is not set; cannot import texture \(sourceURL.lastPathComponent).")
+ return nil
+ }
+
+ let fm = FileManager.default
+ let materialsRoot = basePath.appendingPathComponent("Materials", isDirectory: true)
+ let baseName = sourceURL.deletingPathExtension().lastPathComponent
+ let materialFolder = materialsRoot.appendingPathComponent(baseName, isDirectory: true)
+ let destFile = materialFolder.appendingPathComponent(sourceURL.lastPathComponent)
+
+ do {
+ try fm.createDirectory(at: materialFolder, withIntermediateDirectories: true)
+ if fm.fileExists(atPath: destFile.path) {
+ try fm.removeItem(at: destFile)
+ }
+ try fm.copyItem(at: sourceURL, to: destFile)
+ return destFile
+ } catch {
+ Logger.logWarning(message: "[InspectorView] Failed to import texture \(sourceURL.lastPathComponent): \(error.localizedDescription)")
+ return nil
+ }
+}
+
+func editorMaterialSlotHoverText(textureType: TextureType, textureURL: URL?) -> String {
+ guard let textureURL else {
+ return "No \(textureType.displayName) texture assigned"
+ }
+
+ let materialName: String
+ if let host = textureURL.host, textureURL.scheme == "usdz-embedded", host.isEmpty == false {
+ materialName = host
+ } else {
+ let folderName = textureURL.deletingLastPathComponent().lastPathComponent
+ materialName = folderName.isEmpty ? "Unknown" : folderName
+ }
+
+ return """
+ \(textureType.displayName)
+ Material: \(materialName)
+ Texture: \(textureURL.lastPathComponent)
+ """
+}
+
private func onAddMesh_Editor(entityId: EntityID, url: URL) {
let filename = url.deletingPathExtension().lastPathComponent
let withExtension = url.pathExtension
@@ -316,12 +378,16 @@ struct InspectorView: View {
if let inspectedMesh = selectionManager.inspectedMesh,
inspectedMesh.entityId == entityId
{
+ // inspectMesh() (see SelectionManager) only ever sets inspectedMesh
+ // alongside selectedEntity on the same entity, so reaching this
+ // block means entityId genuinely is the active selection, not just
+ // a passive peek β safe (and expected) to be fully editable here.
RenderingEditorView(
entityId: entityId,
asset: selectedAsset,
refreshView: refreshView,
meshIndex: inspectedMesh.meshIndex,
- inspectionOnly: true
+ inspectionOnly: false
)
Divider()
}
@@ -332,7 +398,16 @@ struct InspectorView: View {
editor_availableComponents: availableComponents_Editor
)
- let sortedComponents = sortEntityComponents(componentOption_Editor: mergedComponents)
+ // The inspectedMesh banner above already renders this entity's
+ // Render Component (with the correct submesh index) when it targets
+ // this same entity. Skip the generic entry here so Material
+ // Properties / Wrap Mode don't render twice.
+ let isInspectedMeshEntity = selectionManager.inspectedMesh?.entityId == entityId
+ let visibleComponents = isInspectedMeshEntity
+ ? mergedComponents.filter { $0.key != ObjectIdentifier(RenderComponent.self) }
+ : mergedComponents
+
+ let sortedComponents = sortEntityComponents(componentOption_Editor: visibleComponents)
// Static Batching Section - Show for any entity with renderable hierarchy
if EditorAuthoringMode.sceneCompositionOnly == false, isDerivedAssetNode(entityId) == false {
@@ -683,6 +758,8 @@ struct RenderingEditorView: View {
ScrollView(.horizontal, showsIndicators: false) {
HStack(alignment: .top, spacing: 24) {
ForEach(TextureType.allCases) { type in
+ let textureURL = getMaterialTextureURL(entityId: entityId, type: type, meshIndex: meshIndex)
+ let hoverText = editorMaterialSlotHoverText(textureType: type, textureURL: textureURL)
let image: NSImage? = {
if let img = getMaterialTextureImage(entityId: entityId, type: type, meshIndex: meshIndex) {
return img
@@ -696,6 +773,11 @@ struct RenderingEditorView: View {
if asset?.category == "Materials", let path = asset?.path {
updateMaterialTexture(entityId: entityId, textureType: type, path: path, meshIndex: meshIndex)
refreshView()
+ } else if let pickedURL = pickTextureImageFile(),
+ let importedURL = importTextureAsset(from: pickedURL)
+ {
+ updateMaterialTexture(entityId: entityId, textureType: type, path: importedURL, meshIndex: meshIndex)
+ refreshView()
}
}) {
if let image {
@@ -711,6 +793,7 @@ struct RenderingEditorView: View {
}
}
.buttonStyle(PlainButtonStyle())
+ .help(hoverText)
HStack(spacing: 8) {
Button(action: {
@@ -848,6 +931,132 @@ struct RenderingEditorView: View {
}
}
+ HStack {
+ // Height Scale Input β total Parallax Occlusion Mapping ray-march depth,
+ // in UV-normalized units. Only has a visible effect once a Height texture
+ // is assigned above.
+ VStack {
+ TextInputNumberView(
+ label: "",
+ value: Binding(
+ get: { getMaterialHeightScale(entityId: entityId, meshIndex: meshIndex) },
+ set: { newValue in
+ updateMaterialHeightScale(entityId: entityId, heightScale: newValue, meshIndex: meshIndex)
+ if inspectionOnly, isUntoldBackedMesh {
+ hasPendingUntoldWrite = true
+ untoldUpdateStatus = nil
+ }
+ refreshView()
+ }
+ )
+ )
+ .frame(width: 60)
+
+ Text("Height Scale")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ // Midlevel Input β matches Blender's Displacement node "Midlevel"
+ // (default 0.5 = no shift).
+ VStack {
+ TextInputNumberView(
+ label: "",
+ value: Binding(
+ get: { getMaterialHeightMidlevel(entityId: entityId, meshIndex: meshIndex) },
+ set: { newValue in
+ updateMaterialHeightMidlevel(entityId: entityId, heightMidlevel: newValue, meshIndex: meshIndex)
+ if inspectionOnly, isUntoldBackedMesh {
+ hasPendingUntoldWrite = true
+ untoldUpdateStatus = nil
+ }
+ refreshView()
+ }
+ )
+ )
+ .frame(width: 60)
+
+ Text("Midlevel")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ // POM Enabled Toggle β lets testers A/B compare normal-mapping-only vs.
+ // normal+POM without discarding the height texture assignment.
+ VStack {
+ Toggle(isOn: Binding(
+ get: { getMaterialHeightEnabled(entityId: entityId, meshIndex: meshIndex) },
+ set: { newValue in
+ updateMaterialHeightEnabled(entityId: entityId, heightEnabled: newValue, meshIndex: meshIndex)
+ if inspectionOnly, isUntoldBackedMesh {
+ hasPendingUntoldWrite = true
+ untoldUpdateStatus = nil
+ }
+ refreshView()
+ }
+ )) {
+ EmptyView()
+ }
+ .toggleStyle(.checkbox)
+ .frame(width: 60)
+
+ Text("POM Enabled")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+
+ HStack {
+ // Height Remap Min/Max β contrast-stretch the raw height sample before
+ // Midlevel is applied. Many real-world displacement maps (Substance/
+ // Poliigon exports especially) only use a narrow slice of the full [0,1]
+ // range, leaving POM almost no local contrast to work with even at a
+ // reasonable Height Scale. Identity is (0.0, 1.0).
+ VStack {
+ TextInputNumberView(
+ label: "",
+ value: Binding(
+ get: { getMaterialHeightRemapMin(entityId: entityId, meshIndex: meshIndex) },
+ set: { newValue in
+ updateMaterialHeightRemapMin(entityId: entityId, heightRemapMin: newValue, meshIndex: meshIndex)
+ if inspectionOnly, isUntoldBackedMesh {
+ hasPendingUntoldWrite = true
+ untoldUpdateStatus = nil
+ }
+ refreshView()
+ }
+ )
+ )
+ .frame(width: 60)
+
+ Text("Height Remap Min")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+
+ VStack {
+ TextInputNumberView(
+ label: "",
+ value: Binding(
+ get: { getMaterialHeightRemapMax(entityId: entityId, meshIndex: meshIndex) },
+ set: { newValue in
+ updateMaterialHeightRemapMax(entityId: entityId, heightRemapMax: newValue, meshIndex: meshIndex)
+ if inspectionOnly, isUntoldBackedMesh {
+ hasPendingUntoldWrite = true
+ untoldUpdateStatus = nil
+ }
+ refreshView()
+ }
+ )
+ )
+ .frame(width: 60)
+
+ Text("Height Remap Max")
+ .font(.caption)
+ .foregroundColor(.secondary)
+ }
+ }
+
HStack {
VStack {
TextInputNumberView(
@@ -1200,6 +1409,7 @@ struct TransformationEditorView: View {
let before = EditorTransformSnapshot(entityId: entityId)
handleTransformChange()
applyAxisRotations(entityId: entityId, axis: newOrientation)
+ syncLightDirectionHandleToActiveLight(entityId: entityId)
EditorUndoManager.shared.registerTransformChange(
entityId: entityId,
before: before,
diff --git a/Sources/UntoldEditor/Editor/LODComponentEditorView.swift b/Sources/UntoldEditor/Editor/LODComponentEditorView.swift
index 8951806..d612b20 100644
--- a/Sources/UntoldEditor/Editor/LODComponentEditorView.swift
+++ b/Sources/UntoldEditor/Editor/LODComponentEditorView.swift
@@ -2,9 +2,11 @@
// LODComponentEditorView.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import SwiftUI
import UntoldEngine
diff --git a/Sources/UntoldEditor/Editor/LoadingIndicatorView.swift b/Sources/UntoldEditor/Editor/LoadingIndicatorView.swift
index 05fc829..274ffe9 100644
--- a/Sources/UntoldEditor/Editor/LoadingIndicatorView.swift
+++ b/Sources/UntoldEditor/Editor/LoadingIndicatorView.swift
@@ -2,9 +2,11 @@
// LoadingIndicatorView.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import SwiftUI
diff --git a/Sources/UntoldEditor/Editor/LogConsoleView.swift b/Sources/UntoldEditor/Editor/LogConsoleView.swift
index 5d69245..1bd40d0 100644
--- a/Sources/UntoldEditor/Editor/LogConsoleView.swift
+++ b/Sources/UntoldEditor/Editor/LogConsoleView.swift
@@ -2,9 +2,11 @@
// LogConsoleView.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Combine
import Foundation
diff --git a/Sources/UntoldEditor/Editor/NumericInputView.swift b/Sources/UntoldEditor/Editor/NumericInputView.swift
index 5c05f1f..b2e163f 100644
--- a/Sources/UntoldEditor/Editor/NumericInputView.swift
+++ b/Sources/UntoldEditor/Editor/NumericInputView.swift
@@ -2,9 +2,11 @@
// NumericInputView.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
#if canImport(AppKit)
import AppKit
diff --git a/Sources/UntoldEditor/Editor/ProjectSceneCatalog.swift b/Sources/UntoldEditor/Editor/ProjectSceneCatalog.swift
index 11fdbb2..70c998d 100644
--- a/Sources/UntoldEditor/Editor/ProjectSceneCatalog.swift
+++ b/Sources/UntoldEditor/Editor/ProjectSceneCatalog.swift
@@ -19,7 +19,9 @@ struct ProjectSceneFile: Identifiable, Hashable {
let url: URL
let name: String
- var id: URL { url }
+ var id: URL {
+ url
+ }
}
final class ProjectSceneCatalog: ObservableObject {
diff --git a/Sources/UntoldEditor/Editor/QuickPreviewComponent.swift b/Sources/UntoldEditor/Editor/QuickPreviewComponent.swift
index b06f27e..ec24341 100644
--- a/Sources/UntoldEditor/Editor/QuickPreviewComponent.swift
+++ b/Sources/UntoldEditor/Editor/QuickPreviewComponent.swift
@@ -2,9 +2,11 @@
// QuickPreviewComponent.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
@@ -19,7 +21,7 @@ enum QuickPreviewImportMode: String, CaseIterable {
var menuTitle: String {
switch self {
case .untoldAsset:
- return "Load Untold Asset (.untold, USD)"
+ return "Load Untold Asset (.untold, USD, .blend)"
case .tiledScene:
return "Load Tiled Stream (.json)"
case .gaussian:
@@ -41,7 +43,7 @@ enum QuickPreviewImportMode: String, CaseIterable {
var filePickerTitle: String {
switch self {
case .untoldAsset:
- return "Load Preview - Select Untold or USD Asset"
+ return "Load Preview - Select Untold, USD, or Blend Asset"
case .tiledScene:
return "Load Preview - Select Tiled Stream Manifest"
case .gaussian:
@@ -52,7 +54,7 @@ enum QuickPreviewImportMode: String, CaseIterable {
var filePickerMessage: String {
switch self {
case .untoldAsset:
- return "Select an Untold runtime asset or USD source asset to preview without creating a project"
+ return "Select an Untold runtime asset, USD source asset, or .blend file to preview without creating a project"
case .tiledScene:
return "Select a tiled stream manifest to preview without creating a project"
case .gaussian:
@@ -63,7 +65,7 @@ enum QuickPreviewImportMode: String, CaseIterable {
var allowedContentTypes: [UTType] {
switch self {
case .untoldAsset:
- return ["untold", "usd", "usda", "usdc", "usdz"].compactMap { UTType(filenameExtension: $0) }
+ return ["untold", "usd", "usda", "usdc", "usdz", "blend"].compactMap { UTType(filenameExtension: $0) }
case .tiledScene:
return [.json]
case .gaussian:
diff --git a/Sources/UntoldEditor/Editor/SceneHierarchyView.swift b/Sources/UntoldEditor/Editor/SceneHierarchyView.swift
index 5a20c9e..c0953c1 100644
--- a/Sources/UntoldEditor/Editor/SceneHierarchyView.swift
+++ b/Sources/UntoldEditor/Editor/SceneHierarchyView.swift
@@ -2,9 +2,11 @@
// SceneHierarchyView.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import SwiftUI
import UntoldEngine
@@ -96,13 +98,15 @@ struct SceneHierarchyView: View {
)
}
- // A scene node in the tree. `url == nil` represents the current, not-yet-saved
- // ("Untitled") scene, which is always the active one.
+ /// A scene node in the tree. `url == nil` represents the current, not-yet-saved
+ /// ("Untitled") scene, which is always the active one.
private struct SceneItem: Identifiable {
let url: URL?
let name: String
let isActive: Bool
- var id: String { url?.absoluteString ?? "__untitled__" }
+ var id: String {
+ url?.absoluteString ?? "__untitled__"
+ }
}
private var sceneItems: [SceneItem] {
@@ -351,7 +355,7 @@ struct HierarchyNode: View {
var onParentEntity: (EntityID, EntityID) -> Void = { _, _ in }
var onUnparentEntity: (EntityID) -> Void = { _ in }
var onDeleteEntity: (EntityID) -> Void = { _ in }
- var addActions: AddEntityActions = AddEntityActions()
+ var addActions: AddEntityActions = .init()
@State private var isDragOver = false
var body: some View {
diff --git a/Sources/UntoldEditor/Editor/ScriptComponentInspector.swift b/Sources/UntoldEditor/Editor/ScriptComponentInspector.swift
index 49ea031..e2dde8b 100644
--- a/Sources/UntoldEditor/Editor/ScriptComponentInspector.swift
+++ b/Sources/UntoldEditor/Editor/ScriptComponentInspector.swift
@@ -2,9 +2,11 @@
// ScriptComponentInspector.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// This file were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Sources/UntoldEditor/Editor/SelectionManager.swift b/Sources/UntoldEditor/Editor/SelectionManager.swift
index aacceb9..1953714 100644
--- a/Sources/UntoldEditor/Editor/SelectionManager.swift
+++ b/Sources/UntoldEditor/Editor/SelectionManager.swift
@@ -2,9 +2,11 @@
// SelectionManager.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
import simd
@@ -53,8 +55,8 @@ struct MeshInspectionSelection: Equatable {
class SceneGraphModel: ObservableObject {
@Published var childrenMap: [EntityID: [EntityID]] = [:]
- // Every node is collapsed by default; we track only the ones the user
- // expanded. Opening a project therefore shows a tidy, collapsed tree.
+ /// Every node is collapsed by default; we track only the ones the user
+ /// expanded. Opening a project therefore shows a tidy, collapsed tree.
@Published private(set) var expandedEntityIds: Set = []
func refreshHierarchy() {
diff --git a/Sources/UntoldEditor/Editor/StarterStreamModels.swift b/Sources/UntoldEditor/Editor/StarterStreamModels.swift
index d0a0628..3560e41 100644
--- a/Sources/UntoldEditor/Editor/StarterStreamModels.swift
+++ b/Sources/UntoldEditor/Editor/StarterStreamModels.swift
@@ -2,9 +2,11 @@
// StarterStreamModels.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
import simd
diff --git a/Sources/UntoldEditor/Editor/StaticBatchingView.swift b/Sources/UntoldEditor/Editor/StaticBatchingView.swift
index ffb8e6f..8927d8c 100644
--- a/Sources/UntoldEditor/Editor/StaticBatchingView.swift
+++ b/Sources/UntoldEditor/Editor/StaticBatchingView.swift
@@ -2,9 +2,11 @@
// StaticBatchingView.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import SwiftUI
import UntoldEngine
diff --git a/Sources/UntoldEditor/Editor/TransformManipulationView.swift b/Sources/UntoldEditor/Editor/TransformManipulationView.swift
index 2f9a0a1..0b3a454 100644
--- a/Sources/UntoldEditor/Editor/TransformManipulationView.swift
+++ b/Sources/UntoldEditor/Editor/TransformManipulationView.swift
@@ -2,9 +2,11 @@
// TransformManipulationView.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
import SwiftUI
diff --git a/Sources/UntoldEditor/Renderer/EditorRenderPasses.swift b/Sources/UntoldEditor/Renderer/EditorRenderPasses.swift
index beb1906..a5184f1 100644
--- a/Sources/UntoldEditor/Renderer/EditorRenderPasses.swift
+++ b/Sources/UntoldEditor/Renderer/EditorRenderPasses.swift
@@ -2,9 +2,11 @@
// EditorRenderPasses.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import CShaderTypes
diff --git a/Sources/UntoldEditor/Renderer/EditorRenderPipeLines.swift b/Sources/UntoldEditor/Renderer/EditorRenderPipeLines.swift
index 28475fd..4057561 100644
--- a/Sources/UntoldEditor/Renderer/EditorRenderPipeLines.swift
+++ b/Sources/UntoldEditor/Renderer/EditorRenderPipeLines.swift
@@ -2,9 +2,11 @@
// EditorRenderPipeLines.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import UntoldEngine
diff --git a/Sources/UntoldEditor/Renderer/EditorRendererInitializer.swift b/Sources/UntoldEditor/Renderer/EditorRendererInitializer.swift
index 9318c53..b248c80 100644
--- a/Sources/UntoldEditor/Renderer/EditorRendererInitializer.swift
+++ b/Sources/UntoldEditor/Renderer/EditorRendererInitializer.swift
@@ -2,9 +2,11 @@
// EditorRendererInitializer.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import CShaderTypes
diff --git a/Sources/UntoldEditor/Renderer/EditorUntoldRenderer.swift b/Sources/UntoldEditor/Renderer/EditorUntoldRenderer.swift
index 2d62d9a..1616980 100644
--- a/Sources/UntoldEditor/Renderer/EditorUntoldRenderer.swift
+++ b/Sources/UntoldEditor/Renderer/EditorUntoldRenderer.swift
@@ -2,9 +2,11 @@
// EditorUntoldRenderer.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import simd
@@ -60,6 +62,13 @@ extension UntoldRenderer {
editorController.refreshInspector()
}
+ @inline(__always)
+ func consumeMouseDragDelta() {
+ InputSystem.shared.mouseDeltaX = 0.0
+ InputSystem.shared.mouseDeltaY = 0.0
+ InputSystem.shared.mouseActive = false
+ }
+
/// Remove static batching when entity is transformed via gizmo
@inline(__always)
func handleStaticBatchOnTransform(entityId: EntityID) {
@@ -72,8 +81,10 @@ extension UntoldRenderer {
}
if hasActiveAxisGizmoDrag() {
- handleStaticBatchOnTransform(entityId: activeEntity)
- refreshInspector()
+ if applyPendingGizmoDragUpdate() {
+ handleStaticBatchOnTransform(entityId: activeEntity)
+ refreshInspector()
+ }
return
}
@@ -95,6 +106,7 @@ extension UntoldRenderer {
translateBy(entityId: activeEntity, position: t)
translateBy(entityId: parentEntityIdGizmo, position: t)
refreshInspector()
+ consumeMouseDragDelta()
case (.translate, .y) where InputSystem.shared.mouseActive:
handleStaticBatchOnTransform(entityId: activeEntity)
@@ -109,6 +121,7 @@ extension UntoldRenderer {
translateBy(entityId: activeEntity, position: t)
translateBy(entityId: parentEntityIdGizmo, position: t)
refreshInspector()
+ consumeMouseDragDelta()
case (.translate, .z) where InputSystem.shared.mouseActive:
handleStaticBatchOnTransform(entityId: activeEntity)
@@ -123,6 +136,7 @@ extension UntoldRenderer {
translateBy(entityId: activeEntity, position: t)
translateBy(entityId: parentEntityIdGizmo, position: t)
refreshInspector()
+ consumeMouseDragDelta()
// MARK: - Rotate
@@ -140,7 +154,9 @@ extension UntoldRenderer {
sensitivity: 100.0
)
applyGizmoRotationDelta(entityId: activeEntity, axis: axis, degrees: -angle * 10)
+ syncLightDirectionHandleToActiveLight(entityId: activeEntity)
refreshInspector()
+ consumeMouseDragDelta()
case (.rotate, .y) where InputSystem.shared.mouseActive:
handleStaticBatchOnTransform(entityId: activeEntity)
@@ -156,7 +172,9 @@ extension UntoldRenderer {
sensitivity: 100.0
)
applyGizmoRotationDelta(entityId: activeEntity, axis: axis, degrees: angle * 10)
+ syncLightDirectionHandleToActiveLight(entityId: activeEntity)
refreshInspector()
+ consumeMouseDragDelta()
case (.rotate, .z) where InputSystem.shared.mouseActive:
handleStaticBatchOnTransform(entityId: activeEntity)
@@ -172,7 +190,9 @@ extension UntoldRenderer {
sensitivity: 100.0
)
applyGizmoRotationDelta(entityId: activeEntity, axis: axis, degrees: angle * 10)
+ syncLightDirectionHandleToActiveLight(entityId: activeEntity)
refreshInspector()
+ consumeMouseDragDelta()
// MARK: - Scale
@@ -191,6 +211,7 @@ extension UntoldRenderer {
applyWorldSpaceScaleDelta(entityId: activeEntity, worldAxis: axis, projectedAmount: amt)
}
refreshInspector()
+ consumeMouseDragDelta()
case (.scale, .y) where InputSystem.shared.mouseActive:
handleStaticBatchOnTransform(entityId: activeEntity)
@@ -207,6 +228,7 @@ extension UntoldRenderer {
applyWorldSpaceScaleDelta(entityId: activeEntity, worldAxis: axis, projectedAmount: amt)
}
refreshInspector()
+ consumeMouseDragDelta()
case (.scale, .z) where InputSystem.shared.mouseActive:
handleStaticBatchOnTransform(entityId: activeEntity)
@@ -223,6 +245,7 @@ extension UntoldRenderer {
applyWorldSpaceScaleDelta(entityId: activeEntity, worldAxis: axis, projectedAmount: amt)
}
refreshInspector()
+ consumeMouseDragDelta()
// MARK: - Light direction (view-plane drag)
@@ -289,6 +312,8 @@ extension UntoldRenderer {
localTransformComponent.rotationY = euler.yaw
localTransformComponent.rotationZ = euler.roll
translateTo(entityId: activeEntity, position: localTransformComponent.position)
+ refreshInspector()
+ consumeMouseDragDelta()
default:
break
diff --git a/Sources/UntoldEditor/Renderer/EditorUntoldRendererConfig.swift b/Sources/UntoldEditor/Renderer/EditorUntoldRendererConfig.swift
index 573cf00..8338f6f 100644
--- a/Sources/UntoldEditor/Renderer/EditorUntoldRendererConfig.swift
+++ b/Sources/UntoldEditor/Renderer/EditorUntoldRendererConfig.swift
@@ -2,9 +2,11 @@
// EditorUntoldRendererConfig.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import UntoldEngine
diff --git a/Sources/UntoldEditor/Systems/EditorCameraSystem.swift b/Sources/UntoldEditor/Systems/EditorCameraSystem.swift
index b93ec77..2c59bc7 100644
--- a/Sources/UntoldEditor/Systems/EditorCameraSystem.swift
+++ b/Sources/UntoldEditor/Systems/EditorCameraSystem.swift
@@ -2,9 +2,11 @@
// EditorCameraSystem.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import UntoldEngine
diff --git a/Sources/UntoldEditor/Systems/EditorInputSystemAppKit.swift b/Sources/UntoldEditor/Systems/EditorInputSystemAppKit.swift
index ac4d20d..e71fd45 100644
--- a/Sources/UntoldEditor/Systems/EditorInputSystemAppKit.swift
+++ b/Sources/UntoldEditor/Systems/EditorInputSystemAppKit.swift
@@ -1,9 +1,11 @@
//
// EditorInputSystemAppKit.swift
// Untold Engine
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright Β© 2024 Untold Engine Studios. All rights reserved.
//
@@ -256,19 +258,24 @@
if hasComponent(entityId: entityId, componentType: GizmoComponent.self) {
activeEntity = selectableTransformEntity(for: entityId)
selectionDelegate?.didSelectEntity(entityId)
- } else if keyState.shiftPressed,
- let rayContext,
+ } else if keyState.commandPressed {
+ // Cmd+click selects the top-level asset root, for moving/rotating
+ // the whole imported group at once.
+ let transformEntityId = editableAssetRootEntity(for: entityId)
+ activeEntity = selectableTransformEntity(for: transformEntityId)
+ selectionDelegate?.didSelectEntity(entityId)
+ } else if let rayContext,
let meshIndex = pickMeshIndexForEntity(
entityId: entityId,
rayOrigin: rayContext.rayOrigin,
rayDirection: rayContext.rayDirection
)
{
+ // Default: click selects the specific child mesh under the cursor.
activeEntity = selectableTransformEntity(for: entityId)
selectionDelegate?.didInspectMesh(entityId, meshIndex: meshIndex)
} else {
- let transformEntityId = editableAssetRootEntity(for: entityId)
- activeEntity = selectableTransformEntity(for: transformEntityId)
+ activeEntity = selectableTransformEntity(for: entityId)
selectionDelegate?.didSelectEntity(entityId)
}
selectionDelegate?.resetActiveAxis()
@@ -343,7 +350,7 @@
if activeHitGizmoEntity != .invalid,
let rayContext = raycastContext(currentLocation: currentLocation, view: view)
{
- updateGizmoDrag(
+ queueGizmoDragUpdate(
ray: GizmoDragRay(
origin: rayContext.rayOrigin,
direction: rayContext.rayDirection
@@ -549,6 +556,13 @@
} else {
keyState.ctrlPressed = false
}
+
+ // Command key
+ if event.modifierFlags.contains(.command) {
+ keyState.commandPressed = true
+ } else {
+ keyState.commandPressed = false
+ }
}
private func raycastContext(currentLocation: NSPoint, view: NSView) -> (rayOrigin: simd_float3, rayDirection: simd_float3)? {
diff --git a/Sources/UntoldEditor/Systems/EditorInputSystemUIKit.swift b/Sources/UntoldEditor/Systems/EditorInputSystemUIKit.swift
index c347682..1433165 100644
--- a/Sources/UntoldEditor/Systems/EditorInputSystemUIKit.swift
+++ b/Sources/UntoldEditor/Systems/EditorInputSystemUIKit.swift
@@ -1,9 +1,11 @@
//
// EditorInputSystemUIKit.swift
// Untold Engine
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright Β© 2024 Untold Engine Studios. All rights reserved.
//
diff --git a/Sources/UntoldEditor/Systems/EditorLightingSystem.swift b/Sources/UntoldEditor/Systems/EditorLightingSystem.swift
index c8462a4..f045f0f 100644
--- a/Sources/UntoldEditor/Systems/EditorLightingSystem.swift
+++ b/Sources/UntoldEditor/Systems/EditorLightingSystem.swift
@@ -2,9 +2,11 @@
// EditorLightingSystem.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import UntoldEngine
diff --git a/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift b/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift
index 5b40619..09b6801 100644
--- a/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift
+++ b/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift
@@ -2,9 +2,11 @@
// EditorRenderingSystem.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import UntoldEngine
diff --git a/Sources/UntoldEditor/Systems/GizmoSystem.swift b/Sources/UntoldEditor/Systems/GizmoSystem.swift
index 148eae3..e9eccfa 100644
--- a/Sources/UntoldEditor/Systems/GizmoSystem.swift
+++ b/Sources/UntoldEditor/Systems/GizmoSystem.swift
@@ -2,9 +2,11 @@
// GizmoSystem.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
@@ -80,6 +82,7 @@ private struct GizmoDragState {
}
private var gizmoDragState: GizmoDragState?
+private var pendingGizmoDragRay: GizmoDragRay?
func gizmoRootWorldPosition() -> simd_float3 {
guard parentEntityIdGizmo != .invalid else {
@@ -175,8 +178,24 @@ func updateGizmoDrag(ray: GizmoDragRay) {
gizmoDragState = state
}
+func queueGizmoDragUpdate(ray: GizmoDragRay) {
+ pendingGizmoDragRay = ray
+}
+
+@discardableResult
+func applyPendingGizmoDragUpdate() -> Bool {
+ guard let ray = pendingGizmoDragRay else {
+ return false
+ }
+
+ pendingGizmoDragRay = nil
+ updateGizmoDrag(ray: ray)
+ return true
+}
+
func endGizmoDrag() {
gizmoDragState = nil
+ pendingGizmoDragRay = nil
}
func hasActiveAxisGizmoDrag() -> Bool {
@@ -405,6 +424,32 @@ private func initialLightDirectionHandleOffset() -> simd_float3 {
return handleDirection * abs(GizmoDimensions.directionHandleOffsetY)
}
+/// Repositions the light direction handle (and its hit proxy) to match the given light
+/// entity's current orientation. Call this after changing a light's rotation through any
+/// path other than dragging the handle itself (e.g. editing the Orientation fields in the
+/// Inspector), so the gizmo doesn't go stale relative to the light it represents.
+func syncLightDirectionHandleToActiveLight(entityId: EntityID) {
+ guard entityId != .invalid,
+ entityId == activeEntity,
+ gizmoActive,
+ parentEntityIdGizmo != .invalid,
+ hasComponent(entityId: entityId, componentType: LightComponent.self)
+ else {
+ return
+ }
+
+ let localPosition = initialLightDirectionHandleOffset()
+
+ for handle in getEntityChildren(parentId: parentEntityIdGizmo) {
+ guard let handleComponent = scene.get(component: GizmoHandleComponent.self, for: handle),
+ handleComponent.mode == .lightRotate, handleComponent.axis == .none
+ else {
+ continue
+ }
+ translateTo(entityId: handle, position: localPosition)
+ }
+}
+
private func rotationFromYAxis(to direction: simd_float3) -> (angle: Float, axis: simd_float3)? {
let up = simd_float3(0.0, 1.0, 0.0)
let dirLength = simd_length(direction)
diff --git a/Sources/UntoldEditor/Systems/HotReloadingSystem.swift b/Sources/UntoldEditor/Systems/HotReloadingSystem.swift
index 469c66c..58eeff4 100644
--- a/Sources/UntoldEditor/Systems/HotReloadingSystem.swift
+++ b/Sources/UntoldEditor/Systems/HotReloadingSystem.swift
@@ -1,9 +1,11 @@
// HotReloadingSystem.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// Copyright Β© 2024 Untold Engine Studios. All rights reserved.
//
diff --git a/Sources/UntoldEditor/Systems/ScriptProjectManager.swift b/Sources/UntoldEditor/Systems/ScriptProjectManager.swift
index aeead25..c03a3a9 100644
--- a/Sources/UntoldEditor/Systems/ScriptProjectManager.swift
+++ b/Sources/UntoldEditor/Systems/ScriptProjectManager.swift
@@ -2,9 +2,11 @@
// ScriptProjectManager.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
@@ -259,8 +261,11 @@ class ScriptProjectManager {
// GenerateScripts.swift
// USC Script Generator
//
- // Copyright (C) Untold Engine Studios
- // Licensed under the GNU LGPL v3.0 or later.
+ // Copyright (C) Untold Engine Studios
+ //
+ // This Source Code Form is subject to the terms of the Mozilla Public
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
@@ -300,8 +305,11 @@ class ScriptProjectManager {
// \(name).swift
// USC Script
//
- // Copyright (C) Untold Engine Studios
- // Licensed under the GNU LGPL v3.0 or later.
+ // Copyright (C) Untold Engine Studios
+ //
+ // This Source Code Form is subject to the terms of the Mozilla Public
+ // License, v. 2.0. If a copy of the MPL was not distributed with this
+ // file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
diff --git a/Sources/UntoldEditor/Systems/VisualDebugger.swift b/Sources/UntoldEditor/Systems/VisualDebugger.swift
index 0af95cf..bbe2c2c 100644
--- a/Sources/UntoldEditor/Systems/VisualDebugger.swift
+++ b/Sources/UntoldEditor/Systems/VisualDebugger.swift
@@ -2,9 +2,11 @@
// VisualDebugger.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
diff --git a/Sources/UntoldEditor/Utils/EditorFuncUtils.swift b/Sources/UntoldEditor/Utils/EditorFuncUtils.swift
index 15ef820..e9c02fc 100644
--- a/Sources/UntoldEditor/Utils/EditorFuncUtils.swift
+++ b/Sources/UntoldEditor/Utils/EditorFuncUtils.swift
@@ -2,9 +2,11 @@
// EditorFuncUtils.swift
//
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
@@ -38,6 +40,8 @@ func bindingForWrapMode(
return material.roughness.wrapMode
case .metallic:
return material.metallic.wrapMode
+ case .height:
+ return material.height.wrapMode
}
},
set: { newValue in
diff --git a/Sources/UntoldEditor/Utils/EditorGlobals.swift b/Sources/UntoldEditor/Utils/EditorGlobals.swift
index 476ef00..93a8ca6 100644
--- a/Sources/UntoldEditor/Utils/EditorGlobals.swift
+++ b/Sources/UntoldEditor/Utils/EditorGlobals.swift
@@ -2,9 +2,11 @@
// Globals.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import Foundation
import UntoldEngine
diff --git a/Sources/UntoldEditor/Utils/UntoldMaterialPersistence.swift b/Sources/UntoldEditor/Utils/UntoldMaterialPersistence.swift
index 618a2b9..cd520f5 100644
--- a/Sources/UntoldEditor/Utils/UntoldMaterialPersistence.swift
+++ b/Sources/UntoldEditor/Utils/UntoldMaterialPersistence.swift
@@ -1,3 +1,9 @@
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
+
import CryptoKit
import Foundation
import UntoldEngine
diff --git a/Sources/UntoldEditor/main.swift b/Sources/UntoldEditor/main.swift
index bdedf4e..b9f8746 100644
--- a/Sources/UntoldEditor/main.swift
+++ b/Sources/UntoldEditor/main.swift
@@ -2,9 +2,11 @@
// main.swift
// UntoldEngine
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import AppKit
@@ -27,7 +29,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
private var rightPanelItem: NSMenuItem?
func applicationDidFinishLaunching(_: Notification) {
- Logger.log(message: "Launching Untold Engine Editor v0.16.0")
+ Logger.log(message: "Launching Untold Engine Editor v0.18.0")
setupMainMenu()
@@ -39,7 +41,7 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
defer: false
)
- window.title = "Untold Engine Editor v0.16.0"
+ window.title = "Untold Engine Editor v0.18.0"
// Force dark appearance so AppKit-drawn chrome (title bar, native tab
// strips, segmented controls) matches the dark editor theme.
window.appearance = NSAppearance(named: .darkAqua)
@@ -137,8 +139,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
return item
}
- // Keep the View-menu checkmarks in sync with the current overlay / camera state.
- func menuNeedsUpdate(_ menu: NSMenu) {
+ /// Keep the View-menu checkmarks in sync with the current overlay / camera state.
+ func menuNeedsUpdate(_: NSMenu) {
let store = EditorEngineStatsStore.shared
showFPSItem?.state = store.overlayMode != .off ? .on : .off
showFPSAdvancedItem?.state = store.overlayMode == .advanced ? .on : .off
@@ -153,13 +155,33 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
// MARK: - File actions (bridged to SwiftUI via notifications)
- @objc private func menuNew() { NotificationCenter.default.post(name: .editorMenuNew, object: nil) }
- @objc private func menuOpen() { NotificationCenter.default.post(name: .editorMenuOpen, object: nil) }
- @objc private func menuNewScene() { NotificationCenter.default.post(name: .editorMenuNewScene, object: nil) }
- @objc private func menuSaveProject() { NotificationCenter.default.post(name: .editorMenuSaveProject, object: nil) }
- @objc private func menuSave() { NotificationCenter.default.post(name: .editorMenuSave, object: nil) }
- @objc private func menuSaveAs() { NotificationCenter.default.post(name: .editorMenuSaveAs, object: nil) }
- @objc private func menuReset() { NotificationCenter.default.post(name: .editorMenuReset, object: nil) }
+ @objc private func menuNew() {
+ NotificationCenter.default.post(name: .editorMenuNew, object: nil)
+ }
+
+ @objc private func menuOpen() {
+ NotificationCenter.default.post(name: .editorMenuOpen, object: nil)
+ }
+
+ @objc private func menuNewScene() {
+ NotificationCenter.default.post(name: .editorMenuNewScene, object: nil)
+ }
+
+ @objc private func menuSaveProject() {
+ NotificationCenter.default.post(name: .editorMenuSaveProject, object: nil)
+ }
+
+ @objc private func menuSave() {
+ NotificationCenter.default.post(name: .editorMenuSave, object: nil)
+ }
+
+ @objc private func menuSaveAs() {
+ NotificationCenter.default.post(name: .editorMenuSaveAs, object: nil)
+ }
+
+ @objc private func menuReset() {
+ NotificationCenter.default.post(name: .editorMenuReset, object: nil)
+ }
// MARK: - View actions (mutate shared stores directly)
@@ -186,8 +208,8 @@ class AppDelegate: NSObject, NSApplicationDelegate, NSMenuDelegate {
EditorPlaybackSettings.shared.useSceneCameraDuringPlay.toggle()
}
- // Animation + render-pause are driven by EditorView (which observes these
- // values), so the menu just flips the state.
+ /// Animation + render-pause are driven by EditorView (which observes these
+ /// values), so the menu just flips the state.
@objc private func menuToggleLeftPanel() {
EditorPanelVisibility.shared.showLeftPanel.toggle()
}
diff --git a/Tests/UntoldEditorTests/AssetBrowserViewTests.swift b/Tests/UntoldEditorTests/AssetBrowserViewTests.swift
index d0127df..c778a3a 100644
--- a/Tests/UntoldEditorTests/AssetBrowserViewTests.swift
+++ b/Tests/UntoldEditorTests/AssetBrowserViewTests.swift
@@ -2,9 +2,11 @@
// AssetBrowserViewTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
@@ -30,6 +32,7 @@ final class AssetBrowserViewTests: XCTestCase {
selectedAsset: Binding,
selectionManager: SelectionManager = SelectionManager(),
sceneGraphModel: SceneGraphModel = SceneGraphModel(),
+ searchQuery: Binding = .constant(""),
editor_addEntityWithAsset: @escaping () -> Void = {},
editor_loadSceneAuthoredFromAsset: @escaping (Asset) -> Void = { _ in }) -> AssetBrowserView
{
@@ -38,6 +41,7 @@ final class AssetBrowserViewTests: XCTestCase {
selectedAsset: selectedAsset,
selectionManager: selectionManager,
sceneGraphModel: sceneGraphModel,
+ searchQuery: searchQuery,
editor_addEntityWithAsset: editor_addEntityWithAsset,
editor_loadSceneAuthoredFromAsset: editor_loadSceneAuthoredFromAsset
)
diff --git a/Tests/UntoldEditorTests/AssetNodeEditingPolicyTests.swift b/Tests/UntoldEditorTests/AssetNodeEditingPolicyTests.swift
index 5e52268..15aa84a 100644
--- a/Tests/UntoldEditorTests/AssetNodeEditingPolicyTests.swift
+++ b/Tests/UntoldEditorTests/AssetNodeEditingPolicyTests.swift
@@ -2,9 +2,11 @@
// AssetNodeEditingPolicyTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import simd
diff --git a/Tests/UntoldEditorTests/ComponentEditorFormTests.swift b/Tests/UntoldEditorTests/ComponentEditorFormTests.swift
index e421902..f0fc7c6 100644
--- a/Tests/UntoldEditorTests/ComponentEditorFormTests.swift
+++ b/Tests/UntoldEditorTests/ComponentEditorFormTests.swift
@@ -2,9 +2,11 @@
// ComponentEditorFormTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/CreateProjectViewTests.swift b/Tests/UntoldEditorTests/CreateProjectViewTests.swift
index 4b811c8..28f39cf 100644
--- a/Tests/UntoldEditorTests/CreateProjectViewTests.swift
+++ b/Tests/UntoldEditorTests/CreateProjectViewTests.swift
@@ -2,9 +2,11 @@
// CreateProjectViewTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/DemoGalleryViewTests.swift b/Tests/UntoldEditorTests/DemoGalleryViewTests.swift
index f98beb8..eebf89d 100644
--- a/Tests/UntoldEditorTests/DemoGalleryViewTests.swift
+++ b/Tests/UntoldEditorTests/DemoGalleryViewTests.swift
@@ -2,9 +2,11 @@
// DemoGalleryViewTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import SwiftUI
diff --git a/Tests/UntoldEditorTests/EditorCameraSystemTests.swift b/Tests/UntoldEditorTests/EditorCameraSystemTests.swift
index 9a625cc..df44afb 100644
--- a/Tests/UntoldEditorTests/EditorCameraSystemTests.swift
+++ b/Tests/UntoldEditorTests/EditorCameraSystemTests.swift
@@ -2,9 +2,11 @@
// EditorCameraSystemTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/EditorControllerTests.swift b/Tests/UntoldEditorTests/EditorControllerTests.swift
index 96dc83a..983e3ac 100644
--- a/Tests/UntoldEditorTests/EditorControllerTests.swift
+++ b/Tests/UntoldEditorTests/EditorControllerTests.swift
@@ -2,9 +2,11 @@
// EditorControllerTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/EditorFuncUtilsTests.swift b/Tests/UntoldEditorTests/EditorFuncUtilsTests.swift
index 26fb99e..a4b7ba8 100644
--- a/Tests/UntoldEditorTests/EditorFuncUtilsTests.swift
+++ b/Tests/UntoldEditorTests/EditorFuncUtilsTests.swift
@@ -2,9 +2,11 @@
// EditorFuncUtilsTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/EditorRenderPassesTests.swift b/Tests/UntoldEditorTests/EditorRenderPassesTests.swift
index 7231ffe..e62d60c 100644
--- a/Tests/UntoldEditorTests/EditorRenderPassesTests.swift
+++ b/Tests/UntoldEditorTests/EditorRenderPassesTests.swift
@@ -2,9 +2,11 @@
// EditorRenderPassesTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/EditorRenderPipeLinesTests.swift b/Tests/UntoldEditorTests/EditorRenderPipeLinesTests.swift
index 9d005b2..dff1343 100644
--- a/Tests/UntoldEditorTests/EditorRenderPipeLinesTests.swift
+++ b/Tests/UntoldEditorTests/EditorRenderPipeLinesTests.swift
@@ -2,9 +2,11 @@
// EditorRenderPipeLinesTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/EditorRendererInitializerTests.swift b/Tests/UntoldEditorTests/EditorRendererInitializerTests.swift
index bc3217e..5ae8143 100644
--- a/Tests/UntoldEditorTests/EditorRendererInitializerTests.swift
+++ b/Tests/UntoldEditorTests/EditorRendererInitializerTests.swift
@@ -2,9 +2,11 @@
// EditorRendererInitializerTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift b/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift
index 6efda4f..c2f44f1 100644
--- a/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift
+++ b/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift
@@ -2,9 +2,11 @@
// EditorRenderingSystemTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
@testable import UntoldEditor
diff --git a/Tests/UntoldEditorTests/EditorUndoManagerTests.swift b/Tests/UntoldEditorTests/EditorUndoManagerTests.swift
index a657401..c57da62 100644
--- a/Tests/UntoldEditorTests/EditorUndoManagerTests.swift
+++ b/Tests/UntoldEditorTests/EditorUndoManagerTests.swift
@@ -2,9 +2,11 @@
// EditorUndoManagerTests.swift
// UntoldEditorTests
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
import simd
diff --git a/Tests/UntoldEditorTests/EditorUntoldRendererTests.swift b/Tests/UntoldEditorTests/EditorUntoldRendererTests.swift
index b2bfb91..50b5cad 100644
--- a/Tests/UntoldEditorTests/EditorUntoldRendererTests.swift
+++ b/Tests/UntoldEditorTests/EditorUntoldRendererTests.swift
@@ -2,9 +2,11 @@
// EditorUntoldRendererTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
@@ -464,8 +466,8 @@ final class EditorUntoldRendererTests: XCTestCase {
// Act
renderer.handleSceneInput()
- // Assert - Should process transformation (may fail due to other missing setup)
- XCTAssertTrue(InputSystem.shared.mouseActive, "Mouse should be active")
+ // Assert - Should process and consume the mouse drag
+ XCTAssertFalse(InputSystem.shared.mouseActive, "Mouse drag should be consumed after transformation")
removeGizmo()
}
diff --git a/Tests/UntoldEditorTests/EnvironmentViewTests.swift b/Tests/UntoldEditorTests/EnvironmentViewTests.swift
index 342d5f5..c0de821 100644
--- a/Tests/UntoldEditorTests/EnvironmentViewTests.swift
+++ b/Tests/UntoldEditorTests/EnvironmentViewTests.swift
@@ -2,9 +2,11 @@
// EnvironmentViewTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/FindSceneCameraTest.swift b/Tests/UntoldEditorTests/FindSceneCameraTest.swift
index 4e9b8bb..d7af686 100644
--- a/Tests/UntoldEditorTests/FindSceneCameraTest.swift
+++ b/Tests/UntoldEditorTests/FindSceneCameraTest.swift
@@ -2,9 +2,11 @@
// FindSceneCameraTest.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/GizmoSystemTest.swift b/Tests/UntoldEditorTests/GizmoSystemTest.swift
index 8bc71cf..b2e0b4e 100644
--- a/Tests/UntoldEditorTests/GizmoSystemTest.swift
+++ b/Tests/UntoldEditorTests/GizmoSystemTest.swift
@@ -2,9 +2,11 @@
// GizmoSystemTest.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/InputSystemCoreTests.swift b/Tests/UntoldEditorTests/InputSystemCoreTests.swift
index 93db31d..52a1484 100644
--- a/Tests/UntoldEditorTests/InputSystemCoreTests.swift
+++ b/Tests/UntoldEditorTests/InputSystemCoreTests.swift
@@ -2,9 +2,11 @@
// InputSystemCoreTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/InspectorViewTests.swift b/Tests/UntoldEditorTests/InspectorViewTests.swift
index a185e69..368e674 100644
--- a/Tests/UntoldEditorTests/InspectorViewTests.swift
+++ b/Tests/UntoldEditorTests/InspectorViewTests.swift
@@ -2,9 +2,11 @@
// InspectorViewTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
@@ -82,6 +84,30 @@ final class InspectorViewTests: XCTestCase {
// MARK: - Tests
+ func test_editorMaterialSlotHoverText_showsEmptySlotFallback() {
+ let text = editorMaterialSlotHoverText(textureType: .roughness, textureURL: nil)
+
+ XCTAssertEqual(text, "No Roughness texture assigned")
+ }
+
+ func test_editorMaterialSlotHoverText_showsMaterialFolderAndTextureName() {
+ let url = URL(fileURLWithPath: "/Project/Materials/RustedIron/RustedIron_Roughness.png")
+ let text = editorMaterialSlotHoverText(textureType: .roughness, textureURL: url)
+
+ XCTAssertTrue(text.contains("Roughness"))
+ XCTAssertTrue(text.contains("Material: RustedIron"))
+ XCTAssertTrue(text.contains("Texture: RustedIron_Roughness.png"))
+ }
+
+ func test_editorMaterialSlotHoverText_showsEmbeddedTextureHostAsMaterialName() throws {
+ let url = try XCTUnwrap(URL(string: "usdz-embedded://Robot/normal"))
+ let text = editorMaterialSlotHoverText(textureType: .normal, textureURL: url)
+
+ XCTAssertTrue(text.contains("Normal"))
+ XCTAssertTrue(text.contains("Material: Robot"))
+ XCTAssertTrue(text.contains("Texture: normal"))
+ }
+
func test_nameEditing_updatesEntityName_andRefreshesHierarchy() {
// Arrange
let e = createEntityWithName("Old Name")
diff --git a/Tests/UntoldEditorTests/LightDebugMeshTest.swift b/Tests/UntoldEditorTests/LightDebugMeshTest.swift
index 32a200d..fa29e28 100644
--- a/Tests/UntoldEditorTests/LightDebugMeshTest.swift
+++ b/Tests/UntoldEditorTests/LightDebugMeshTest.swift
@@ -2,9 +2,11 @@
// LightDebugMeshTest.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/LogConsoleViewTests.swift b/Tests/UntoldEditorTests/LogConsoleViewTests.swift
index d6aeff9..a8e737a 100644
--- a/Tests/UntoldEditorTests/LogConsoleViewTests.swift
+++ b/Tests/UntoldEditorTests/LogConsoleViewTests.swift
@@ -2,9 +2,11 @@
// LogConsoleViewTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
@@ -15,7 +17,7 @@ import XCTest
/// A minimal harness exposing the private helpers of LogConsoleView for testing.
private struct LogConsoleViewHarness {
- let view = LogConsoleView()
+ let view = LogConsoleView(searchQuery: .constant(""), autoScroll: .constant(true))
func tag(for level: LogLevel) -> String {
// Mirror the production logic
diff --git a/Tests/UntoldEditorTests/NumericInputTest.swift b/Tests/UntoldEditorTests/NumericInputTest.swift
index 428c747..3f89f52 100644
--- a/Tests/UntoldEditorTests/NumericInputTest.swift
+++ b/Tests/UntoldEditorTests/NumericInputTest.swift
@@ -2,9 +2,11 @@
// NumericInputTest.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/ParentingSerializationTests.swift b/Tests/UntoldEditorTests/ParentingSerializationTests.swift
index 366d3fa..9429fca 100644
--- a/Tests/UntoldEditorTests/ParentingSerializationTests.swift
+++ b/Tests/UntoldEditorTests/ParentingSerializationTests.swift
@@ -2,9 +2,11 @@
// ParentingSerializationTests.swift
// UntoldEditor Tests
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/SceneHierarchyViewTests.swift b/Tests/UntoldEditorTests/SceneHierarchyViewTests.swift
index 9a8ae73..ec31beb 100644
--- a/Tests/UntoldEditorTests/SceneHierarchyViewTests.swift
+++ b/Tests/UntoldEditorTests/SceneHierarchyViewTests.swift
@@ -2,9 +2,11 @@
// SceneHierarchyViewTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
@@ -246,7 +248,7 @@ final class SceneHierarchyViewTests: XCTestCase {
XCTAssertFalse(sceneGraphModel.hasChildren(entityId: child), "Leaf child should not report children")
}
- func test_sceneGraphModel_nodesAreExpandedByDefault() {
+ func test_sceneGraphModel_nodesAreCollapsedByDefault() {
// Arrange
let entity = createEntity()
@@ -254,35 +256,35 @@ final class SceneHierarchyViewTests: XCTestCase {
sceneGraphModel.refreshHierarchy()
// Assert
- XCTAssertTrue(sceneGraphModel.isExpanded(entityId: entity), "Nodes should be expanded by default")
+ XCTAssertFalse(sceneGraphModel.isExpanded(entityId: entity), "Nodes should be collapsed by default")
}
- func test_sceneGraphModel_toggleExpandedCollapsesAndExpandsNode() {
+ func test_sceneGraphModel_toggleExpandedExpandsAndCollapsesNode() {
// Arrange
let entity = createEntity()
sceneGraphModel.refreshHierarchy()
// Act / Assert
sceneGraphModel.toggleExpanded(entityId: entity)
- XCTAssertFalse(sceneGraphModel.isExpanded(entityId: entity), "Toggle should collapse expanded nodes")
+ XCTAssertTrue(sceneGraphModel.isExpanded(entityId: entity), "Toggle should expand collapsed nodes")
sceneGraphModel.toggleExpanded(entityId: entity)
- XCTAssertTrue(sceneGraphModel.isExpanded(entityId: entity), "Toggle should expand collapsed nodes")
+ XCTAssertFalse(sceneGraphModel.isExpanded(entityId: entity), "Toggle should collapse expanded nodes")
}
- func test_sceneGraphModel_refreshPrunesCollapsedDestroyedEntities() {
+ func test_sceneGraphModel_refreshPrunesExpandedDestroyedEntities() {
// Arrange
let entity = createEntity()
sceneGraphModel.refreshHierarchy()
sceneGraphModel.toggleExpanded(entityId: entity)
- XCTAssertFalse(sceneGraphModel.isExpanded(entityId: entity))
+ XCTAssertTrue(sceneGraphModel.isExpanded(entityId: entity))
// Act
destroyEntity(entityId: entity)
sceneGraphModel.refreshHierarchy()
// Assert
- XCTAssertTrue(sceneGraphModel.isExpanded(entityId: entity), "Destroyed entities should be pruned from collapsed state")
+ XCTAssertFalse(sceneGraphModel.isExpanded(entityId: entity), "Destroyed entities should be pruned from expanded state")
}
// MARK: - SelectionManager Integration Tests
@@ -331,6 +333,11 @@ final class SceneHierarchyViewTests: XCTestCase {
let view = SceneHierarchyView(
selectionManager: selectionManager,
sceneGraphModel: sceneGraphModel,
+ sceneCatalog: ProjectSceneCatalog(),
+ projectName: "TestProject",
+ onSelectScene: { _ in },
+ isPlaying: false,
+ onTogglePlay: {},
entityList: [],
onAddEntity_Editor: { addEntityCalled = true },
onRemoveEntity_Editor: { removeEntityCalled = true },
@@ -403,6 +410,11 @@ final class SceneHierarchyViewTests: XCTestCase {
let view = SceneHierarchyView(
selectionManager: selectionManager,
sceneGraphModel: sceneGraphModel,
+ sceneCatalog: ProjectSceneCatalog(),
+ projectName: "TestProject",
+ onSelectScene: { _ in },
+ isPlaying: false,
+ onTogglePlay: {},
entityList: entityList,
onAddEntity_Editor: {},
onRemoveEntity_Editor: {},
@@ -428,6 +440,11 @@ final class SceneHierarchyViewTests: XCTestCase {
let view = SceneHierarchyView(
selectionManager: selectionManager,
sceneGraphModel: sceneGraphModel,
+ sceneCatalog: ProjectSceneCatalog(),
+ projectName: "TestProject",
+ onSelectScene: { _ in },
+ isPlaying: false,
+ onTogglePlay: {},
entityList: [],
onAddEntity_Editor: {},
onRemoveEntity_Editor: {},
diff --git a/Tests/UntoldEditorTests/ScriptProjectManagerTests.swift b/Tests/UntoldEditorTests/ScriptProjectManagerTests.swift
index 87076e1..6de3735 100644
--- a/Tests/UntoldEditorTests/ScriptProjectManagerTests.swift
+++ b/Tests/UntoldEditorTests/ScriptProjectManagerTests.swift
@@ -2,9 +2,11 @@
// ScriptProjectManagerTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/SelectionManagerTests.swift b/Tests/UntoldEditorTests/SelectionManagerTests.swift
index c96bd41..3a2f441 100644
--- a/Tests/UntoldEditorTests/SelectionManagerTests.swift
+++ b/Tests/UntoldEditorTests/SelectionManagerTests.swift
@@ -2,9 +2,11 @@
// SelectionManagerTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
//
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/Tests/UntoldEditorTests/ToolbarViewTests.swift b/Tests/UntoldEditorTests/ToolbarViewTests.swift
deleted file mode 100644
index d309a75..0000000
--- a/Tests/UntoldEditorTests/ToolbarViewTests.swift
+++ /dev/null
@@ -1,261 +0,0 @@
-//
-// ToolbarViewTests.swift
-// UntoldEditor
-//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
-
-// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
-
-import SwiftUI
-@testable import UntoldEditor
-@testable import UntoldEngine
-import XCTest
-
-#if canImport(AppKit)
- final class ToolbarViewTests: XCTestCase {
- /// Build a ToolbarView with closures that flip flags so we can assert wiring.
- private func makeSUT(
- selectionManager: SelectionManager = SelectionManager(),
- onSaveCalled: UnsafeMutablePointer,
- onSaveAsCalled: UnsafeMutablePointer,
- onClearCalled: UnsafeMutablePointer,
- onCameraSaveCalled _: UnsafeMutablePointer,
- onPlayToggledValues: UnsafeMutablePointer<[Bool]>,
- onDirLightCalled: UnsafeMutablePointer,
- onPointLightCalled: UnsafeMutablePointer,
- onSpotLightCalled: UnsafeMutablePointer,
- onAreaLightCalled: UnsafeMutablePointer,
- onCreateCubeCalled: UnsafeMutablePointer,
- onCreateSphereCalled: UnsafeMutablePointer,
- onCreatePlaneCalled: UnsafeMutablePointer,
- onCreateCylinderCalled: UnsafeMutablePointer,
- onCreateConeCalled: UnsafeMutablePointer
- ) -> ToolbarView {
- ToolbarView(
- selectionManager: selectionManager,
- onSave: { onSaveCalled.pointee = true },
- onSaveAs: { onSaveAsCalled.pointee = true },
- onClear: { onClearCalled.pointee = true },
- onPlayToggled: { value in onPlayToggledValues.pointee.append(value) },
- useSceneCameraDuringPlay: .constant(false),
- dirLightCreate: { onDirLightCalled.pointee = true },
- pointLightCreate: { onPointLightCalled.pointee = true },
- spotLightCreate: { onSpotLightCalled.pointee = true },
- areaLightCreate: { onAreaLightCalled.pointee = true },
- onCreateCube: { onCreateCubeCalled.pointee = true },
- onCreateSphere: { onCreateSphereCalled.pointee = true },
- onCreatePlane: { onCreatePlaneCalled.pointee = true },
- onCreateCylinder: { onCreateCylinderCalled.pointee = true },
- onCreateCone: { onCreateConeCalled.pointee = true }
- )
- }
-
- func test_actionsAreWired_up() {
- var onSave = false
- var onSaveAs = false
- var onClear = false
- var onCameraSave = false
- var playValues: [Bool] = []
- var onDir = false
- var onPoint = false
- var onSpot = false
- var onArea = false
- var onCube = false
- var onSphere = false
- var onPlane = false
- var onCylinder = false
- var onCone = false
-
- let sut = makeSUT(
- onSaveCalled: &onSave,
- onSaveAsCalled: &onSaveAs,
- onClearCalled: &onClear,
- onCameraSaveCalled: &onCameraSave,
- onPlayToggledValues: &playValues,
- onDirLightCalled: &onDir,
- onPointLightCalled: &onPoint,
- onSpotLightCalled: &onSpot,
- onAreaLightCalled: &onArea,
- onCreateCubeCalled: &onCube,
- onCreateSphereCalled: &onSphere,
- onCreatePlaneCalled: &onPlane,
- onCreateCylinderCalled: &onCylinder,
- onCreateConeCalled: &onCone
- )
-
- // We cannot programmatically tap SwiftUI Buttons without a host and introspection.
- // Instead, assert that injected closures can be called and flip their flags.
- sut.onSave()
- sut.onSaveAs()
- sut.onClear()
- sut.dirLightCreate()
- sut.pointLightCreate()
- sut.spotLightCreate()
- sut.areaLightCreate()
- sut.onCreateCube()
- sut.onCreateSphere()
- sut.onCreatePlane()
- sut.onCreateCylinder()
- sut.onCreateCone()
-
- XCTAssertTrue(onSave, "onSave should be wired.")
- XCTAssertTrue(onSaveAs, "onSaveAs should be wired.")
- XCTAssertTrue(onClear, "onClear should be wired.")
- XCTAssertTrue(onDir, "dirLightCreate should be wired.")
- XCTAssertTrue(onPoint, "pointLightCreate should be wired.")
- XCTAssertTrue(onSpot, "spotLightCreate should be wired.")
- XCTAssertTrue(onArea, "areaLightCreate should be wired.")
- XCTAssertTrue(onCube, "onCreateCube should be wired.")
- XCTAssertTrue(onSphere, "onCreateSphere should be wired.")
- XCTAssertTrue(onPlane, "onCreatePlane should be wired.")
- XCTAssertTrue(onCylinder, "onCreateCylinder should be wired.")
- XCTAssertTrue(onCone, "onCreateCone should be wired.")
-
- // For play toggle, verify the closure records values we pass.
- // Since @State is internal, we mimic the button behavior by calling the closure directly.
- sut.onPlayToggled(true)
- sut.onPlayToggled(false)
- XCTAssertEqual(playValues, [true, false], "onPlayToggled should receive toggled values in order.")
- }
-
- func test_viewComposesWithoutCrash() {
- // Smoke test: ensure the view can be created and rendered to a hosting controller without crashing.
- let selection = SelectionManager()
- var playValues: [Bool] = []
-
- let sut = ToolbarView(
- selectionManager: selection,
- onSave: {},
- onSaveAs: {},
- onClear: {},
- onPlayToggled: { playValues.append($0) },
- useSceneCameraDuringPlay: .constant(false),
- dirLightCreate: {},
- pointLightCreate: {},
- spotLightCreate: {},
- areaLightCreate: {},
- onCreateCube: {},
- onCreateSphere: {},
- onCreatePlane: {},
- onCreateCylinder: {},
- onCreateCone: {}
- )
-
- // Wrap in a hosting controller to ensure SwiftUI can build the body.
- let host = NSHostingController(rootView: sut)
- XCTAssertNotNil(host.view, "Hosting controller should create a view.")
- _ = host // keep alive
- _ = playValues // keep referenced to avoid 'never read' warning
- }
-
- // MARK: - Primitive Creation Tests
-
- func test_primitiveButtons_callCorrectClosures() {
- // Given: A ToolbarView with tracked primitive creation callbacks
- var cubeCreated = false
- var sphereCreated = false
- var planeCreated = false
- var cylinderCreated = false
- var coneCreated = false
-
- let sut = ToolbarView(
- selectionManager: SelectionManager(),
- onSave: {},
- onSaveAs: {},
- onClear: {},
- onPlayToggled: { _ in },
- useSceneCameraDuringPlay: .constant(false),
- dirLightCreate: {},
- pointLightCreate: {},
- spotLightCreate: {},
- areaLightCreate: {},
- onCreateCube: { cubeCreated = true },
- onCreateSphere: { sphereCreated = true },
- onCreatePlane: { planeCreated = true },
- onCreateCylinder: { cylinderCreated = true },
- onCreateCone: { coneCreated = true }
- )
-
- // When: Invoking the primitive creation closures
- sut.onCreateCube()
- sut.onCreateSphere()
- sut.onCreatePlane()
- sut.onCreateCylinder()
- sut.onCreateCone()
-
- // Then: Each closure should be called
- XCTAssertTrue(cubeCreated, "Cube creation callback should be invoked")
- XCTAssertTrue(sphereCreated, "Sphere creation callback should be invoked")
- XCTAssertTrue(planeCreated, "Plane creation callback should be invoked")
- XCTAssertTrue(cylinderCreated, "Cylinder creation callback should be invoked")
- XCTAssertTrue(coneCreated, "Cone creation callback should be invoked")
- }
-
- func test_primitivesSection_existsInView() {
- // Given: A ToolbarView instance
- var callCount = 0
- let sut = ToolbarView(
- selectionManager: SelectionManager(),
- onSave: {},
- onSaveAs: {},
- onClear: {},
- onPlayToggled: { _ in },
- useSceneCameraDuringPlay: .constant(false),
- dirLightCreate: {},
- pointLightCreate: {},
- spotLightCreate: {},
- areaLightCreate: {},
- onCreateCube: { callCount += 1 },
- onCreateSphere: { callCount += 1 },
- onCreatePlane: { callCount += 1 },
- onCreateCylinder: {},
- onCreateCone: {}
- )
-
- // When: Calling the primitive closures
- sut.onCreateCube()
- sut.onCreateSphere()
- sut.onCreatePlane()
-
- // Then: All three active primitives should have been called
- XCTAssertEqual(callCount, 3, "All three active primitive buttons (cube, sphere, plane) should be functional")
- }
-
- func test_primitiveCallbacks_areIndependent() {
- // Given: Separate tracking for each primitive
- var cubeCount = 0
- var sphereCount = 0
- var planeCount = 0
-
- let sut = ToolbarView(
- selectionManager: SelectionManager(),
- onSave: {},
- onSaveAs: {},
- onClear: {},
- onPlayToggled: { _ in },
- useSceneCameraDuringPlay: .constant(false),
- dirLightCreate: {},
- pointLightCreate: {},
- spotLightCreate: {},
- areaLightCreate: {},
- onCreateCube: { cubeCount += 1 },
- onCreateSphere: { sphereCount += 1 },
- onCreatePlane: { planeCount += 1 },
- onCreateCylinder: {},
- onCreateCone: {}
- )
-
- // When: Calling specific primitive closures multiple times
- sut.onCreateCube()
- sut.onCreateCube()
- sut.onCreateSphere()
-
- // Then: Each closure should track independently
- XCTAssertEqual(cubeCount, 2, "Cube should be created twice")
- XCTAssertEqual(sphereCount, 1, "Sphere should be created once")
- XCTAssertEqual(planeCount, 0, "Plane should not be created")
- }
- }
-#endif
diff --git a/Tests/UntoldEditorTests/TransformManipulationViewTests.swift b/Tests/UntoldEditorTests/TransformManipulationViewTests.swift
index 302983b..8dc02eb 100644
--- a/Tests/UntoldEditorTests/TransformManipulationViewTests.swift
+++ b/Tests/UntoldEditorTests/TransformManipulationViewTests.swift
@@ -2,9 +2,11 @@
// TransformManipulationViewTests.swift
// UntoldEditor
//
-// Copyright (C) Untold Engine Studios
-// Licensed under the GNU LGPL v3.0 or later.
-// See the LICENSE file or for details.
+// Copyright (C) Untold Engine Studios
+//
+// This Source Code Form is subject to the terms of the Mozilla Public
+// License, v. 2.0. If a copy of the MPL was not distributed with this
+// file, You can obtain one at https://mozilla.org/MPL/2.0/.
// These unit tests were jump-started with AI assistance β then refined by humans. If you spot an issue, please submit an issue.
diff --git a/scripts/changecopyright.sh b/scripts/changecopyright.sh
index 7b293c8..4df15a0 100755
--- a/scripts/changecopyright.sh
+++ b/scripts/changecopyright.sh
@@ -11,7 +11,7 @@
# replaces any line containing βCreated byβ with the standard
# copyright and license notice. This helps ensure all source
# files remain consistent with the Untold Engineβs licensing
-# policy (GNU LGPL v3.0 or later).
+# policy (Mozilla Public License 2.0).
#
# How it works:
# - Uses `find` to locate all `.swift` files.
@@ -34,5 +34,4 @@
YEAR=$(date +"%Y")
-find . -name "*.swift" -type f -exec sed -i '' "1,/Created by/s#//.*Created by.*#// Copyright (C) Untold Engine Studios\n// Licensed under the GNU LGPL v3.0 or later.\n// See the LICENSE file or for details.#" {} +
-
+find . -name "*.swift" -type f -exec sed -i '' "1,/Created by/s#//.*Created by.*#// Copyright (C) Untold Engine Studios\n//\n// This Source Code Form is subject to the terms of the Mozilla Public\n// License, v. 2.0. If a copy of the MPL was not distributed with this\n// file, You can obtain one at https://mozilla.org/MPL/2.0/.#" {} +
diff --git a/scripts/next-version.sh b/scripts/next-version.sh
index 042bd1a..79da3d8 100755
--- a/scripts/next-version.sh
+++ b/scripts/next-version.sh
@@ -13,7 +13,7 @@
# Optional Flags:
# --with-v : Print version with 'v' prefix (e.g. v0.12.7)
# --cliff : Run git-cliff to prepend a changelog section,
-# then update version strings in main.swift and ToolbarView.swift
+# then update version strings in main.swift
# --docs : Run Docusaurus docs:version command to
# snapshot documentation for the new release
#
@@ -79,10 +79,6 @@ if [[ "${DO_CLIFF}" == "true" ]]; then
Sources/UntoldEditor/main.swift
echo "Updated version to ${NEXT} in UntoldEditor/main.swift."
- # Update editorVersionLabel in ToolbarView.swift
- sed -i '' 's/private let editorVersionLabel = "v[^"]*"/private let editorVersionLabel = "v'"${NEXT}"'"/' \
- Sources/UntoldEditor/Editor/ToolbarView.swift
- echo "Updated editorVersionLabel to v${NEXT} in ToolbarView.swift."
fi
# Optionally run Docusaurus docs:version