From 761dc471226eb7b74fda85a115a7e7db40a15ec6 Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Sat, 25 Apr 2026 23:28:41 -0700 Subject: [PATCH 1/6] [Patch] Updated script to follow engine dependency tag version --- scripts/next-version.sh | 111 +++++++++++++--------------------------- 1 file changed, 36 insertions(+), 75 deletions(-) diff --git a/scripts/next-version.sh b/scripts/next-version.sh index 3c94301..ad00844 100755 --- a/scripts/next-version.sh +++ b/scripts/next-version.sh @@ -4,38 +4,29 @@ # next-version.sh # ------------------------------------------------------------- # Description: -# Determines the next semantic version of the Untold Engine -# by comparing the current base ref (release/x.y.z branch or vX.Y.Z tag) -# against the latest commits in develop. +# Determines the current version of the Untold Editor by +# reading the UntoldEngine dependency pin in Package.swift. # -# The script scans commit messages between the release branch -# and develop for keywords that indicate the bump level: -# [API Change] β†’ major -# [Feature] β†’ minor -# [Patch]/[Bugfix] β†’ patch -# -# It then prints the next version number (e.g. 0.3.1), -# optionally prefixed with "v" if the --with-v flag is used. +# The editor version always mirrors the engine version, so +# no commit-log scanning or bump calculation is needed. # # Optional Flags: -# --with-v : Print version with 'v' prefix (e.g. v0.3.1) -# --cliff : Run git-cliff to prepend a changelog section -# for the new version based on recent commits +# --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 # --docs : Run Docusaurus docs:version command to # snapshot documentation for the new release # # Usage Examples: -# ./scripts/next-version.sh # auto-picks latest vX.Y.Z tag from develop +# ./scripts/next-version.sh # prints current engine-pinned version # ./scripts/next-version.sh --with-v -# ./scripts/next-version.sh release/0.3.0 --cliff -# ./scripts/next-version.sh v0.3.0 --cliff --docs +# ./scripts/next-version.sh --cliff +# ./scripts/next-version.sh --cliff --docs # # Notes: # - Must be executed from the repository root. -# - Base ref can be a release/x.y.z branch or vX.Y.Z tag. -# - Designed to simplify the Untold Engine release flow by -# automating version calculation, changelog generation, and -# docs versioning in a single step. +# - Requires Package.swift to pin UntoldEngine with exact: "x.y.z". +# - To change the editor version, update the engine pin in Package.swift first. # # ------------------------------------------------------------- @@ -44,79 +35,49 @@ set -euo pipefail WITH_V="false" DO_CLIFF="false" DO_DOCS="false" -BASE_REF="" for arg in "$@"; do case "$arg" in --with-v) WITH_V="true" ;; --cliff) DO_CLIFF="true" ;; --docs) DO_DOCS="true" ;; - release/*) BASE_REF="$arg" ;; - v[0-9]*|[0-9]*.[0-9]*.[0-9]*) BASE_REF="$arg" ;; # allow tags like v0.3.0 or 0.3.0 *) echo "Unknown argument: $arg" >&2; exit 2 ;; esac done -# Auto-pick latest reachable tag (vX.Y.Z) from develop if none provided -if [[ -z "${BASE_REF}" ]]; then - BASE_REF="$(git describe --tags --match 'v[0-9]*' --abbrev=0 develop 2>/dev/null || true)" -fi -[[ -n "${BASE_REF}" ]] || { echo "No base ref found. Pass a release branch (release/x.y.z) or tag (vX.Y.Z)." >&2; exit 1; } - -# Parse base version from branch or tag name -if [[ "${BASE_REF}" =~ ^release/([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - BASE_MAJOR="${BASH_REMATCH[1]}" - BASE_MINOR="${BASH_REMATCH[2]}" - BASE_PATCH="${BASH_REMATCH[3]}" -elif [[ "${BASE_REF}" =~ ^v?([0-9]+)\.([0-9]+)\.([0-9]+)$ ]]; then - BASE_MAJOR="${BASH_REMATCH[1]}" - BASE_MINOR="${BASH_REMATCH[2]}" - BASE_PATCH="${BASH_REMATCH[3]}" -else - echo "Base ref must look like release/x.y.z or vX.Y.Z (got: ${BASE_REF})" >&2 +# Read version from UntoldEngine dependency pin in Package.swift +NEXT="$(grep -oE 'exact: "[0-9]+\.[0-9]+\.[0-9]+"' Package.swift | grep -oE '[0-9]+\.[0-9]+\.[0-9]+' | head -1)" +[[ -n "${NEXT}" ]] || { + echo "Could not read engine version from Package.swift." >&2 + echo "Make sure UntoldEngine is pinned with: exact: \"x.y.z\"" >&2 exit 1 -fi - -# Ensure develop exists locally -git rev-parse --verify develop >/dev/null 2>&1 || { echo "Local branch 'develop' not found." >&2; exit 1; } - -# Collect commit messages BASE..develop (local) -LOG="$(git log --pretty=%B "${BASE_REF}..develop" || true)" +} -# Highest bump wins -if echo "$LOG" | grep -q "\[API Change\]"; then - LEVEL="major" -elif echo "$LOG" | grep -q "\[Feature\]"; then - LEVEL="minor" -elif echo "$LOG" | grep -q "\[Patch\]"; then - LEVEL="patch" -elif echo "$LOG" | grep -q "\[Bugfix\]"; then - LEVEL="patch" -else - LEVEL="patch" -fi - -# Compute NEXT from BASE + bump -case "$LEVEL" in - major) NEXT="$((BASE_MAJOR+1)).0.0" ;; - minor) NEXT="${BASE_MAJOR}.$((BASE_MINOR+1)).0" ;; - patch) NEXT="${BASE_MAJOR}.${BASE_MINOR}.$((BASE_PATCH+1))" ;; - *) echo "Unknown bump level: $LEVEL" >&2; exit 1 ;; -esac - -# Print next version (default behavior) +# Print version (default behavior) if [[ "${WITH_V}" == "true" ]]; then echo "v${NEXT}" else echo "${NEXT}" fi -# Optionally run git-cliff to prepend changelog for BASE..HEAD +# Optionally run git-cliff to prepend changelog for ..HEAD if [[ "${DO_CLIFF}" == "true" ]]; then command -v git-cliff >/dev/null 2>&1 || { echo "git-cliff not found. Install it first." >&2; exit 1; } TAG="v${NEXT}" - RANGE="${BASE_REF}..HEAD" + BASE_REF="$(git describe --tags --match 'v[0-9]*' --abbrev=0 HEAD 2>/dev/null || true)" + if [[ -n "${BASE_REF}" ]]; then + RANGE="${BASE_REF}..HEAD" + else + RANGE="HEAD" + fi git cliff "${RANGE}" --tag "${TAG}" --prepend CHANGELOG.md + + # Update version strings in main.swift + sed -i '' 's/Logger\.log(message: "Launching Untold Engine Editor v[^"]*")/Logger.log(message: "Launching Untold Engine Editor v'"${NEXT}"'")/' \ + Sources/UntoldEditor/main.swift + sed -i '' 's/window\.title = "Untold Engine Editor v[^"]*"/window.title = "Untold Engine Editor v'"${NEXT}"'"/' \ + Sources/UntoldEditor/main.swift + echo "Updated version to ${NEXT} in UntoldEditor/main.swift." fi # Optionally run Docusaurus docs:version @@ -125,15 +86,15 @@ if [[ "${DO_DOCS}" == "true" ]]; then DOCS_DIR="website" if [[ -d "${DOCS_DIR}" && -f "${DOCS_DIR}/package.json" ]]; then - echo "🧭 Running Docusaurus versioning for ${NEXT} (in ${DOCS_DIR})..." + echo "Running Docusaurus versioning for ${NEXT} (in ${DOCS_DIR})..." ( cd "${DOCS_DIR}" npm run docusaurus docs:version "${NEXT}" ) else - echo "🧭 Running Docusaurus versioning for ${NEXT} (current directory)..." + echo "Running Docusaurus versioning for ${NEXT} (current directory)..." npm run docusaurus docs:version "${NEXT}" fi - echo "βœ… Docusaurus version ${NEXT} created." + echo "Docusaurus version ${NEXT} created." fi From 0df020727062fcbedfd5aae2fe756d0263834841 Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Mon, 27 Apr 2026 07:19:50 -0700 Subject: [PATCH 2/6] [Patch] make app bundle script copy 'usdz-untold' script --- create_app_bundle.sh | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/create_app_bundle.sh b/create_app_bundle.sh index 593383a..d4d7740 100755 --- a/create_app_bundle.sh +++ b/create_app_bundle.sh @@ -62,6 +62,19 @@ if [ -f "Resources/AppIcon.icns" ]; then cp "Resources/AppIcon.icns" "$APP_BUNDLE/Contents/Resources/" fi +# Copy exporter scripts +SCRIPTS_SRC=".build/checkouts/UntoldEngine/scripts" +if [ -d "$SCRIPTS_SRC" ]; then + echo "πŸ“œ Copying exporter scripts..." + mkdir -p "$APP_BUNDLE/Contents/Resources/scripts" + cp "$SCRIPTS_SRC/export-untold" "$APP_BUNDLE/Contents/Resources/scripts/" + cp "$SCRIPTS_SRC/export-untold-tiles" "$APP_BUNDLE/Contents/Resources/scripts/" + chmod +x "$APP_BUNDLE/Contents/Resources/scripts/export-untold" + chmod +x "$APP_BUNDLE/Contents/Resources/scripts/export-untold-tiles" +else + echo "⚠️ Warning: Exporter scripts not found at $SCRIPTS_SRC β€” run 'swift build' first to populate checkouts" +fi + # Create Info.plist echo "πŸ“ Creating Info.plist..." From 943c3e80067e04c60725a1758fa95e10aadfe720 Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Mon, 27 Apr 2026 07:20:14 -0700 Subject: [PATCH 3/6] [Patch] added support to export tile scenes --- .../Editor/AssetBrowserView.swift | 274 +++++++++++++++++- 1 file changed, 263 insertions(+), 11 deletions(-) diff --git a/Sources/UntoldEditor/Editor/AssetBrowserView.swift b/Sources/UntoldEditor/Editor/AssetBrowserView.swift index 96f284a..3bd96dc 100644 --- a/Sources/UntoldEditor/Editor/AssetBrowserView.swift +++ b/Sources/UntoldEditor/Editor/AssetBrowserView.swift @@ -23,6 +23,13 @@ struct RuntimeExportRequest: Identifiable, Equatable { let outputURL: URL } +struct TilesExportRequest: Identifiable, Equatable { + let id = UUID() + let sourceURL: URL + let destinationFolder: URL + let outputDirURL: URL +} + func copyRuntimeAssetSidecars(for sourceURL: URL, to destinationFolder: URL, fileManager fm: FileManager = .default) throws { let sourceFolder = sourceURL.deletingLastPathComponent() @@ -170,16 +177,57 @@ func importStreamModelManifest(sourceURL: URL, destinationFolder: URL, fileManag } } -func findExportUntoldScript(fileManager fm: FileManager = .default) -> URL? { +func findUntoldEngineScript(named name: String, fileManager fm: FileManager = .default) -> URL? { let cwd = URL(fileURLWithPath: fm.currentDirectoryPath, isDirectory: true) - let candidates = [ - cwd.appendingPathComponent("../UntoldEngine/scripts/export-untold").standardizedFileURL, - cwd.appendingPathComponent("scripts/export-untold").standardizedFileURL, - URL(fileURLWithPath: NSHomeDirectory(), isDirectory: true) - .appendingPathComponent("Desktop/UntoldEngineStudio/UntoldEngine/scripts/export-untold"), + + var scriptsDirCandidates: [URL] = [] + + // DMG / installed app: scripts are bundled in Contents/Resources/scripts/ + if let resourceURL = Bundle.main.resourceURL { + scriptsDirCandidates.append(resourceURL.appendingPathComponent("scripts").standardizedFileURL) + } + + scriptsDirCandidates += [ + // `swift run` from the package root: cwd is the package root + cwd.appendingPathComponent(".build/checkouts/UntoldEngine/scripts").standardizedFileURL, + // Local package override (Package.swift uses path: "../UntoldEngine") + cwd.appendingPathComponent("../UntoldEngine/scripts").standardizedFileURL, ] - return candidates.first { fm.isExecutableFile(atPath: $0.path) || fm.fileExists(atPath: $0.path) } + if let execURL = Bundle.main.executableURL { + // SPM CLI build: executable is at .build/// + // Three levels up lands at .build/ + let spmBuildDir = execURL + .deletingLastPathComponent() // / + .deletingLastPathComponent() // / + .deletingLastPathComponent() // .build/ + scriptsDirCandidates.append( + spmBuildDir.appendingPathComponent("checkouts/UntoldEngine/scripts").standardizedFileURL + ) + + // Xcode build: executable is at DerivedData//Build/Products// + // Four levels up lands at DerivedData// + let derivedDataDir = execURL + .deletingLastPathComponent() // / + .deletingLastPathComponent() // Products/ + .deletingLastPathComponent() // Build/ + .deletingLastPathComponent() // DerivedData// + scriptsDirCandidates.append( + derivedDataDir.appendingPathComponent("SourcePackages/checkouts/UntoldEngine/scripts").standardizedFileURL + ) + } + + return scriptsDirCandidates + .map { $0.appendingPathComponent(name) } + .first { fm.isExecutableFile(atPath: $0.path) || fm.fileExists(atPath: $0.path) } +} + +func findExportUntoldScript(fileManager fm: FileManager = .default) -> URL? { + findUntoldEngineScript(named: "export-untold", fileManager: fm) +} + +func findExportUntoldTilesScript(fileManager fm: FileManager = .default) -> URL? { + findUntoldEngineScript(named: "export-untold-tiles", fileManager: fm) } enum AssetCategory: String, CaseIterable { @@ -247,6 +295,12 @@ struct AssetBrowserView: View { @State private var isExportingRuntimeAsset = false @State private var exportConvertOrientation = true @State private var exportSourceOrientation = "blender-native" + @State private var pendingTilesExport: TilesExportRequest? + @State private var tilesExportQueue: [TilesExportRequest] = [] + @State private var isExportingTilesAsset = false + @State private var exportTileSizeX: String = "25" + @State private var exportTileSizeY: String = "10000" + @State private var exportTileSizeZ: String = "25" var editor_addEntityWithAsset: () -> Void private var currentFolderPath: URL? { folderPathStack.last @@ -506,6 +560,9 @@ struct AssetBrowserView: View { .sheet(item: $pendingRuntimeExport) { request in runtimeExportSheet(for: request) } + .sheet(item: $pendingTilesExport) { request in + tilesExportSheet(for: request) + } .overlay(alignment: .bottom) { if let statusMessage { Text(statusMessage) @@ -536,7 +593,7 @@ struct AssetBrowserView: View { UTType(filenameExtension: $0) } case .streamModels: - openPanel.allowedContentTypes = [UTType(filenameExtension: "json")!] + openPanel.allowedContentTypes = ([UTType(filenameExtension: "json")!] + sourceAssetExtensions.sorted().compactMap { UTType(filenameExtension: $0) }) case .scripts: openPanel.allowedContentTypes = [UTType(filenameExtension: "uscript")!] case .scenes: @@ -610,7 +667,12 @@ struct AssetBrowserView: View { } case "StreamModels": - if sourceURL.hasDirectoryPath { + let sourceExtension = sourceURL.pathExtension.lowercased() + if sourceAssetExtensions.contains(sourceExtension) { + let baseName = sourceURL.deletingPathExtension().lastPathComponent + let destFolder = categoryRoot.appendingPathComponent(baseName, isDirectory: true) + queueTilesExport(sourceURL: sourceURL, destinationFolder: destFolder) + } else if sourceURL.hasDirectoryPath { guard primaryTiledSceneManifest(in: sourceURL, fileManager: fm) != nil else { showStatus("No tiled scene manifest found in selected folder", isError: true) continue @@ -649,7 +711,9 @@ struct AssetBrowserView: View { } loadAssets() - if runtimeExportQueue.isEmpty, pendingRuntimeExport == nil { + if runtimeExportQueue.isEmpty, pendingRuntimeExport == nil, + tilesExportQueue.isEmpty, pendingTilesExport == nil + { showStatus("Queued import of \(openPanel.urls.count) item(s) (see Console)") } } @@ -756,7 +820,7 @@ struct AssetBrowserView: View { guard !isExportingRuntimeAsset else { return } guard let exporterScript = findExportUntoldScript() else { showStatus("export-untold script not found", isError: true) - Logger.log(message: "❌ export-untold script not found. Expected ../UntoldEngine/scripts/export-untold") + Logger.log(message: "❌ export-untold script not found. Expected at .build/checkouts/UntoldEngine/scripts/export-untold") pendingRuntimeExport = nil presentNextRuntimeExportIfNeeded() return @@ -843,6 +907,194 @@ struct AssetBrowserView: View { } } + private func queueTilesExport(sourceURL: URL, destinationFolder: URL) { + let outputDirURL = destinationFolder.appendingPathComponent("tile_exports", isDirectory: true) + let request = TilesExportRequest( + sourceURL: sourceURL, + destinationFolder: destinationFolder, + outputDirURL: outputDirURL + ) + tilesExportQueue.append(request) + presentNextTilesExportIfNeeded() + } + + private func presentNextTilesExportIfNeeded() { + guard pendingTilesExport == nil, !tilesExportQueue.isEmpty else { return } + pendingTilesExport = tilesExportQueue.removeFirst() + } + + private func tilesExportSheet(for request: TilesExportRequest) -> some View { + VStack(alignment: .leading, spacing: 16) { + Text("Convert to Tiled Stream Model") + .font(.title2) + .bold() + + Text("This USD 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) { + Text("Source") + .font(.caption) + .foregroundColor(.secondary) + Text(request.sourceURL.path) + .font(.system(size: 12, design: .monospaced)) + .lineLimit(2) + + Text("Output directory") + .font(.caption) + .foregroundColor(.secondary) + .padding(.top, 6) + Text(request.outputDirURL.path) + .font(.system(size: 12, design: .monospaced)) + .lineLimit(2) + } + + VStack(alignment: .leading, spacing: 10) { + Text("Tile size (world units)") + .font(.caption) + .foregroundColor(.secondary) + + HStack(spacing: 12) { + VStack(alignment: .leading, spacing: 4) { + Text("X").font(.caption) + TextField("25", text: $exportTileSizeX) + .frame(width: 70) + .textFieldStyle(.roundedBorder) + } + VStack(alignment: .leading, spacing: 4) { + Text("Y").font(.caption) + TextField("10000", text: $exportTileSizeY) + .frame(width: 70) + .textFieldStyle(.roundedBorder) + } + VStack(alignment: .leading, spacing: 4) { + Text("Z").font(.caption) + TextField("25", text: $exportTileSizeZ) + .frame(width: 70) + .textFieldStyle(.roundedBorder) + } + } + } + + if isExportingTilesAsset { + HStack(spacing: 10) { + ProgressView() + .controlSize(.small) + Text("Exporting tiles...") + .foregroundColor(.secondary) + } + } + + HStack { + Spacer() + Button("Cancel") { + pendingTilesExport = nil + presentNextTilesExportIfNeeded() + } + .disabled(isExportingTilesAsset) + + Button("Export") { + exportTilesAsset(request) + } + .keyboardShortcut(.defaultAction) + .disabled(isExportingTilesAsset) + } + } + .padding(20) + .frame(width: 560) + } + + private func exportTilesAsset(_ request: TilesExportRequest) { + guard !isExportingTilesAsset else { return } + guard let exporterScript = findExportUntoldTilesScript() else { + showStatus("export-untold-tiles script not found", isError: true) + Logger.log(message: "❌ export-untold-tiles script not found. Expected at .build/checkouts/UntoldEngine/scripts/export-untold-tiles") + pendingTilesExport = nil + presentNextTilesExportIfNeeded() + return + } + + isExportingTilesAsset = true + showStatus("Exporting tiles for \(request.sourceURL.lastPathComponent)...") + let tileSizeX = exportTileSizeX + let tileSizeY = exportTileSizeY + let tileSizeZ = exportTileSizeZ + + DispatchQueue.global(qos: .userInitiated).async { + let process = Process() + let tempDirectory = FileManager.default.temporaryDirectory + let outputLogURL = tempDirectory.appendingPathComponent("untold-tiles-export-\(UUID().uuidString).out") + let errorLogURL = tempDirectory.appendingPathComponent("untold-tiles-export-\(UUID().uuidString).err") + + do { + try FileManager.default.createDirectory(at: request.destinationFolder, withIntermediateDirectories: true) + FileManager.default.createFile(atPath: outputLogURL.path, contents: nil) + FileManager.default.createFile(atPath: errorLogURL.path, contents: nil) + let outputHandle = try FileHandle(forWritingTo: outputLogURL) + let errorHandle = try FileHandle(forWritingTo: errorLogURL) + defer { + try? outputHandle.close() + try? errorHandle.close() + try? FileManager.default.removeItem(at: outputLogURL) + try? FileManager.default.removeItem(at: errorLogURL) + } + + process.executableURL = exporterScript + var arguments = [ + "--input", request.sourceURL.path, + "--output-dir", request.outputDirURL.path, + ] + if let x = Double(tileSizeX), x > 0 { + arguments.append(contentsOf: ["--tile-size-x", tileSizeX]) + } + if let y = Double(tileSizeY), y > 0 { + arguments.append(contentsOf: ["--tile-size-y", tileSizeY]) + } + if let z = Double(tileSizeZ), z > 0 { + arguments.append(contentsOf: ["--tile-size-z", tileSizeZ]) + } + process.arguments = arguments + process.standardOutput = outputHandle + process.standardError = errorHandle + + try process.run() + process.waitUntilExit() + + let stdout = (try? String(contentsOf: outputLogURL, encoding: .utf8)) ?? "" + let stderr = (try? String(contentsOf: errorLogURL, encoding: .utf8)) ?? "" + + DispatchQueue.main.async { + if !stdout.isEmpty { + Logger.log(message: stdout.trimmingCharacters(in: .whitespacesAndNewlines)) + } + if !stderr.isEmpty { + Logger.log(message: stderr.trimmingCharacters(in: .whitespacesAndNewlines)) + } + + isExportingTilesAsset = false + pendingTilesExport = nil + + if process.terminationStatus == 0 { + loadAssets() + showStatus("Exported tiles for \(request.sourceURL.deletingPathExtension().lastPathComponent)") + } else { + showStatus("Tiles export failed for \(request.sourceURL.lastPathComponent)", isError: true) + } + + presentNextTilesExportIfNeeded() + } + } catch { + DispatchQueue.main.async { + isExportingTilesAsset = false + pendingTilesExport = nil + Logger.log(message: "❌ Tiles export failed: \(error)") + showStatus("Tiles export failed for \(request.sourceURL.lastPathComponent)", isError: true) + presentNextTilesExportIfNeeded() + } + } + } + } + private func promptDeleteAsset() { guard let asset = selectedAsset else { return } pendingDeleteAsset = asset From 8a403a3a6c9f9857c8732794a459288ae6500b49 Mon Sep 17 00:00:00 2001 From: Untold Engine Date: Mon, 27 Apr 2026 17:00:29 -0700 Subject: [PATCH 4/6] [Patch] Added support for astc and lz4 optimization. --- .../Editor/AssetBrowserView.swift | 270 ++++++++++++++++-- create_app_bundle.sh | 1 + 2 files changed, 255 insertions(+), 16 deletions(-) diff --git a/Sources/UntoldEditor/Editor/AssetBrowserView.swift b/Sources/UntoldEditor/Editor/AssetBrowserView.swift index 3bd96dc..d2239c7 100644 --- a/Sources/UntoldEditor/Editor/AssetBrowserView.swift +++ b/Sources/UntoldEditor/Editor/AssetBrowserView.swift @@ -230,6 +230,52 @@ func findExportUntoldTilesScript(fileManager fm: FileManager = .default) -> URL? findUntoldEngineScript(named: "export-untold-tiles", fileManager: fm) } +func findTexbakeScript(fileManager fm: FileManager = .default) -> URL? { + findUntoldEngineScript(named: "texbake.py", fileManager: fm) +} + +/// Runs `python3