diff --git a/Package.swift b/Package.swift index aeda5a8..fb76408 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.8.2"), + .package(url: "https://github.com/untoldengine/UntoldEngine.git", exact: "0.9.0"), ], targets: [ .executableTarget( diff --git a/Sources/UntoldEditor/Editor/EditorView.swift b/Sources/UntoldEditor/Editor/EditorView.swift index a92c8e8..c23c1a9 100644 --- a/Sources/UntoldEditor/Editor/EditorView.swift +++ b/Sources/UntoldEditor/Editor/EditorView.swift @@ -124,6 +124,11 @@ public struct EditorView: View { Label("Effects", systemImage: "cube") } + StaticBatchingView() + .tabItem { + Label("Batching", systemImage: "square.3.layers.3d") + } + InspectorView( selectionManager: selectionManager, sceneGraphModel: sceneGraphModel, diff --git a/Sources/UntoldEditor/Editor/InspectorView.swift b/Sources/UntoldEditor/Editor/InspectorView.swift index 2c20b14..29fb8d5 100644 --- a/Sources/UntoldEditor/Editor/InspectorView.swift +++ b/Sources/UntoldEditor/Editor/InspectorView.swift @@ -159,6 +159,24 @@ var availableComponents_Editor: [ComponentOption_Editor] = [ } ) }), + + ComponentOption_Editor( + id: getComponentId(for: LODComponent.self), + name: "LOD Component", + type: LODComponent.self, + view: { selectedId, asset, refreshView in + AnyView( + Group { + if let entityId = selectedId { + LODComponentEditorView(entityId: entityId, asset: asset, refreshView: refreshView) + } + } + ) + }, + onAdd: { entityId in + setEntityLodComponent(entityId: entityId) + } + ), ] // Script Component - controlled by feature flag @@ -277,6 +295,9 @@ struct InspectorView: View { let sortedComponents = sortEntityComponents(componentOption_Editor: mergedComponents) + // Static Batching Section - Show for any entity with renderable hierarchy + StaticBatchingEditorView(entityId: entityId, refreshView: refreshView) + ForEach(sortedComponents, id: \.id) { editor_component in VStack(alignment: .leading, spacing: 4) { HStack { @@ -407,6 +428,8 @@ struct InspectorView: View { scene.remove(component: GaussianComponent.self, from: entityId) } else if key == ObjectIdentifier(ScriptComponent.self) { scene.remove(component: ScriptComponent.self, from: entityId) + } else if key == ObjectIdentifier(LODComponent.self) { + scene.remove(component: LODComponent.self, from: entityId) } refreshView() @@ -438,6 +461,102 @@ struct InspectorView: View { } */ +// Standalone Static Batching Section +struct StaticBatchingEditorView: View { + let entityId: EntityID + let refreshView: () -> Void + + @State private var staticBatchCheckboxState: Bool = false + + // Check if entity or any of its children have RenderComponent + private func hasRenderableHierarchy(entityId: EntityID) -> Bool { + // Check self + if hasComponent(entityId: entityId, componentType: RenderComponent.self) { + return true + } + + // Check children recursively + let children = getEntityChildren(parentId: entityId) + for child in children { + if hasRenderableHierarchy(entityId: child) { + return true + } + } + + return false + } + + // Check if entity or any of its children have StaticBatchComponent + private func isMarkedAsStatic(entityId: EntityID) -> Bool { + // Check self + if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) { + return true + } + + // Check children recursively + let children = getEntityChildren(parentId: entityId) + for child in children { + if isMarkedAsStatic(entityId: child) { + return true + } + } + + return false + } + + var body: some View { + // Only show if entity or children have RenderComponent (but not lights) + if hasRenderableHierarchy(entityId: entityId), hasComponent(entityId: entityId, componentType: LightComponent.self) == false { + VStack(alignment: .leading, spacing: 4) { + Text("Static Batching") + .font(.headline) + .frame(maxWidth: .infinity, alignment: .leading) + + let hasOwnRenderComponent = hasComponent(entityId: entityId, componentType: RenderComponent.self) + let labelText = hasOwnRenderComponent ? "Mark as Static" : "Mark Children as Static" + let helpText = hasOwnRenderComponent + ? "Enable static batching for this entity (combines geometry to reduce draw calls)" + : "Enable static batching for all children of this entity (combines geometry to reduce draw calls)" + + Toggle(isOn: Binding( + get: { staticBatchCheckboxState }, + set: { isStatic in + if isStatic { + setEntityStaticBatchComponent(entityId: entityId) + } else { + removeEntityStaticBatchComponent(entityId: entityId) + } + staticBatchCheckboxState = isStatic + refreshView() + } + )) { + HStack { + Image(systemName: "square.3.layers.3d") + .foregroundColor(.blue) + Text(labelText) + .font(.callout) + } + } + .padding(.vertical, 6) + .padding(.horizontal, 8) + .background(Color.secondary.opacity(0.05)) + .cornerRadius(8) + .help(helpText) + .onAppear { + // Update checkbox state when view appears + staticBatchCheckboxState = isMarkedAsStatic(entityId: entityId) + } + .onChange(of: entityId) { newEntityId in + // Update checkbox state when entity selection changes + staticBatchCheckboxState = isMarkedAsStatic(entityId: newEntityId) + } + } + + Divider() + } + } +} + struct RenderingEditorView: View { let entityId: EntityID let asset: Asset? @@ -665,8 +784,25 @@ struct TransformationEditorView: View { let entityId: EntityID let refreshView: () -> Void + @State private var showStaticBatchWarning = false + var body: some View { Text("Transform Properties") + + // Warning banner if entity is marked as static + if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) { + HStack { + Image(systemName: "exclamationmark.triangle.fill") + .foregroundColor(.orange) + Text("This entity is marked for static batching. Transforming it will disable batching.") + .font(.caption) + .foregroundColor(.orange) + } + .padding(6) + .background(Color.orange.opacity(0.1)) + .cornerRadius(6) + } + let localTransformComponent = scene.get(component: LocalTransformComponent.self, for: entityId) let position = getLocalPosition(entityId: entityId) let orientation = simd_float3(localTransformComponent!.rotationX, localTransformComponent!.rotationY, localTransformComponent!.rotationZ) @@ -674,6 +810,7 @@ struct TransformationEditorView: View { TextInputVectorView(label: "Position", value: Binding( get: { position }, set: { newPosition in + handleTransformChange() translateTo(entityId: entityId, position: newPosition) refreshView() } @@ -682,6 +819,7 @@ struct TransformationEditorView: View { TextInputVectorView(label: "Orientation", value: Binding( get: { orientation }, set: { newOrientation in + handleTransformChange() applyAxisRotations(entityId: entityId, axis: newOrientation) refreshView() } @@ -690,11 +828,22 @@ struct TransformationEditorView: View { TextInputVectorView(label: "Scale", value: Binding( get: { scale }, set: { newScale in + handleTransformChange() scaleTo(entityId: entityId, scale: newScale) refreshView() } )) } + + private func handleTransformChange() { + if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) { + removeEntityStaticBatchComponent(entityId: entityId) + // Optionally regenerate batches without this entity + if isBatchingEnabled() { + generateBatches() + } + } + } } struct AnimationEditorView: View { diff --git a/Sources/UntoldEditor/Editor/LODComponentEditorView.swift b/Sources/UntoldEditor/Editor/LODComponentEditorView.swift new file mode 100644 index 0000000..06c6876 --- /dev/null +++ b/Sources/UntoldEditor/Editor/LODComponentEditorView.swift @@ -0,0 +1,192 @@ +// +// LODComponentEditorView.swift +// +// +// Copyright (C) Untold Engine Studios +// Licensed under the GNU LGPL v3.0 or later. +// See the LICENSE file or for details. +// +import SwiftUI +import UntoldEngine + +struct LODComponentEditorView: View { + let entityId: EntityID + let asset: Asset? + let refreshView: () -> Void + + var body: some View { + VStack(alignment: .leading, spacing: 12) { + HStack { + Image(systemName: "square.3.layers.3d") + .foregroundColor(.blue) + Text("LOD Levels") + .font(.headline) + } + + if let lodComponent = scene.get(component: LODComponent.self, for: entityId) { + // Show existing LOD levels + ForEach(Array(lodComponent.lodLevels.enumerated()), id: \.offset) { index, lodLevel in + LODLevelRow( + entityId: entityId, + lodIndex: index, + lodLevel: lodLevel, + refreshView: refreshView + ) + } + + // Add LOD Level button + Button(action: { + addLODLevelFromFilePicker() + }) { + HStack(spacing: 6) { + Image(systemName: "plus.circle.fill") + .foregroundColor(.white) + Text("Add LOD Level") + .fontWeight(.regular) + } + .padding(.vertical, 6) + .padding(.horizontal, 10) + .background(Color.green) + .foregroundColor(.white) + .cornerRadius(6) + } + .buttonStyle(PlainButtonStyle()) + .padding(.top, 8) + } + } + .padding(12) + .background(Color.blue.opacity(0.05)) + .cornerRadius(8) + } + + private func addLODLevelFromFilePicker() { + // Check if user has selected a model asset + guard let asset, + asset.category == "Models", + !asset.isFolder + else { + Logger.log(message: "⚠️ Please select a USDZ file from the Models section first") + return + } + + let url = asset.path + let filename = url.deletingPathExtension().lastPathComponent + let ext = url.pathExtension + + // Get the next LOD index + if let lodComponent = scene.get(component: LODComponent.self, for: entityId) { + let lodIndex = lodComponent.lodLevels.count + + // Default distance based on index: 50, 100, 150, 200, etc. + let maxDistance = Float((lodIndex + 1) * 50) + + // Add LOD level + addLODLevel( + entityId: entityId, + lodIndex: lodIndex, + fileName: filename, + withExtension: ext, + maxDistance: maxDistance + ) { success in + if success { + Logger.log(message: "✅ Added LOD level \(lodIndex): \(filename)") + refreshView() + } else { + Logger.log(message: "⚠️ Failed to add LOD level \(lodIndex)") + } + } + } + } +} + +struct LODLevelRow: View { + let entityId: EntityID + let lodIndex: Int + let lodLevel: LODLevel + let refreshView: () -> Void + + @State private var editingDistance: Bool = false + @State private var distanceValue: String = "" + + var body: some View { + VStack(alignment: .leading, spacing: 6) { + // Top row: LOD badge and filename + HStack(spacing: 8) { + Text("LOD\(lodIndex)") + .font(.system(size: 10, weight: .bold)) + .foregroundColor(.white) + .padding(.horizontal, 6) + .padding(.vertical, 3) + .background(Color.blue) + .cornerRadius(4) + + Text(lodLevel.url?.deletingPathExtension().lastPathComponent ?? "Unknown") + .font(.system(size: 11)) + .foregroundColor(.primary) + .lineLimit(1) + + Spacer() + } + + // Bottom row: Distance and remove button + HStack(spacing: 8) { + Text("Distance:") + .font(.system(size: 10)) + .foregroundColor(.secondary) + + // Distance editor + if editingDistance { + TextField("Distance", text: $distanceValue) + .textFieldStyle(RoundedBorderTextFieldStyle()) + .frame(width: 60) + .onSubmit { + if let newDistance = Float(distanceValue) { + updateLODDistance(newDistance: newDistance) + } + editingDistance = false + } + } else { + Button(action: { + distanceValue = String(format: "%.0f", lodLevel.maxDistance) + editingDistance = true + }) { + Text(String(format: "%.0f", lodLevel.maxDistance)) + .font(.system(size: 10)) + .foregroundColor(.secondary) + .padding(.horizontal, 8) + .padding(.vertical, 4) + .background(Color.secondary.opacity(0.1)) + .cornerRadius(4) + } + .buttonStyle(PlainButtonStyle()) + } + + Spacer() + + // Remove button + Button(action: { + removeLODLevel(entityId: entityId, lodIndex: lodIndex) + refreshView() + }) { + Image(systemName: "trash") + .foregroundColor(.red) + .font(.system(size: 12)) + } + .buttonStyle(BorderlessButtonStyle()) + } + } + .padding(.vertical, 8) + .padding(.horizontal, 8) + .background(Color.secondary.opacity(0.05)) + .cornerRadius(6) + } + + private func updateLODDistance(newDistance: Float) { + if let lodComponent = scene.get(component: LODComponent.self, for: entityId), + lodIndex < lodComponent.lodLevels.count + { + lodComponent.lodLevels[lodIndex].maxDistance = newDistance + refreshView() + } + } +} diff --git a/Sources/UntoldEditor/Editor/StaticBatchingView.swift b/Sources/UntoldEditor/Editor/StaticBatchingView.swift new file mode 100644 index 0000000..12c8400 --- /dev/null +++ b/Sources/UntoldEditor/Editor/StaticBatchingView.swift @@ -0,0 +1,192 @@ +// +// StaticBatchingView.swift +// +// +// Copyright (C) Untold Engine Studios +// Licensed under the GNU LGPL v3.0 or later. +// See the LICENSE file or for details. +// +import SwiftUI +import UntoldEngine + +@available(macOS 12.0, *) +struct StaticBatchingView: View { + @State private var isBatchingEnabled: Bool = false + @State private var batchCount: Int = 0 + @State private var showGenerateSuccess: Bool = false + + var body: some View { + VStack(alignment: .leading, spacing: 8) { + // MARK: - Header + + HStack(spacing: 6) { + Image(systemName: "square.3.layers.3d") + .foregroundColor(.accentColor) + .font(.system(size: 14)) + Text("Static Batching") + .font(.headline) + .foregroundColor(.primary) + } + .padding(.bottom, 6) + + Divider() + + // MARK: - Enable Batching Toggle + + VStack(alignment: .leading, spacing: 6) { + Toggle(isOn: $isBatchingEnabled) { + Label("Enable Batching", systemImage: isBatchingEnabled ? "checkmark.circle.fill" : "circle") + .font(.system(size: 12)) + } + .toggleStyle(SwitchToggleStyle()) + .scaleEffect(0.85) + .onChange(of: isBatchingEnabled) { _, newValue in + enableBatching(newValue) + } + + Text("Enable the batching system globally") + .font(.system(size: 10)) + .foregroundColor(.secondary) + } + + Divider() + + // MARK: - Action Buttons + + VStack(spacing: 8) { + // Generate Batches Button + Button(action: { + generateBatches() + updateBatchCount() + showGenerateSuccess = true + + // Hide success message after 2 seconds + DispatchQueue.main.asyncAfter(deadline: .now() + 2) { + showGenerateSuccess = false + } + }) { + HStack(spacing: 6) { + Image(systemName: "square.stack.3d.up.fill") + .foregroundColor(.white) + .font(.system(size: 12)) + Text("Generate Batches") + .font(.system(size: 12)) + .fontWeight(.semibold) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 6) + .padding(.horizontal, 8) + .background(Color.blue) + .foregroundColor(.white) + .cornerRadius(6) + } + .buttonStyle(PlainButtonStyle()) + .disabled(!isBatchingEnabled) + + // Clear Batches Button + Button(action: { + clearSceneBatches() + updateBatchCount() + }) { + HStack(spacing: 6) { + Image(systemName: "trash.fill") + .foregroundColor(.white) + .font(.system(size: 12)) + Text("Clear Batches") + .font(.system(size: 12)) + .fontWeight(.semibold) + } + .frame(maxWidth: .infinity) + .padding(.vertical, 6) + .padding(.horizontal, 8) + .background(Color.red.opacity(0.8)) + .foregroundColor(.white) + .cornerRadius(6) + } + .buttonStyle(PlainButtonStyle()) + } + + // Success message + if showGenerateSuccess { + HStack { + Image(systemName: "checkmark.circle.fill") + .foregroundColor(.green) + Text("Batches generated successfully!") + .font(.system(size: 11)) + .foregroundColor(.green) + } + .padding(6) + .background(Color.green.opacity(0.1)) + .cornerRadius(6) + .transition(.opacity) + } + + Divider() + + // MARK: - Info Section + + VStack(alignment: .leading, spacing: 6) { + Text("Batch Statistics") + .font(.system(size: 12)) + .fontWeight(.semibold) + .foregroundColor(.primary) + + HStack { + Text("Active Batches:") + .font(.system(size: 11)) + .foregroundColor(.secondary) + Spacer() + Text("\(batchCount)") + .font(.system(size: 11)) + .fontWeight(.medium) + .foregroundColor(.primary) + } + } + .padding(.vertical, 4) + + Divider() + + // MARK: - Help Section + + VStack(alignment: .leading, spacing: 4) { + Text("How to use:") + .font(.system(size: 11)) + .fontWeight(.semibold) + .foregroundColor(.primary) + + Text("1. Mark entities as static in Inspector") + .font(.system(size: 10)) + .foregroundColor(.secondary) + + Text("2. Enable batching toggle above") + .font(.system(size: 10)) + .foregroundColor(.secondary) + + Text("3. Click 'Generate Batches'") + .font(.system(size: 10)) + .foregroundColor(.secondary) + + Text("Note: Moving a static entity will automatically disable its batching.") + .font(.system(size: 9)) + .foregroundColor(.orange) + .padding(.top, 4) + } + .padding(.vertical, 4) + + Spacer() + } + .padding(8) + .background(Color.secondary.opacity(0.1)) + .cornerRadius(8) + .shadow(color: Color.black.opacity(0.1), radius: 3, x: 0, y: 1) + .onAppear { + isBatchingEnabled = UntoldEngine.isBatchingEnabled() + updateBatchCount() + } + } + + private func updateBatchCount() { + // Get batch count from BatchingSystem + batchCount = BatchingSystem.shared.batchGroups.count + } +} diff --git a/Sources/UntoldEditor/Editor/ToolbarView.swift b/Sources/UntoldEditor/Editor/ToolbarView.swift index 84b5ddc..417d6f4 100644 --- a/Sources/UntoldEditor/Editor/ToolbarView.swift +++ b/Sources/UntoldEditor/Editor/ToolbarView.swift @@ -14,7 +14,7 @@ struct ToolbarView: View { @ObservedObject var selectionManager: SelectionManager @ObservedObject var editorBasePath = EditorAssetBasePath.shared - private let editorVersionLabel = "v0.8.1" + private let editorVersionLabel = "v0.9.0" var onSave: () -> Void var onSaveAs: () -> Void diff --git a/Sources/UntoldEditor/Renderer/EditorUntoldRenderer.swift b/Sources/UntoldEditor/Renderer/EditorUntoldRenderer.swift index 0048cde..39ae9c4 100644 --- a/Sources/UntoldEditor/Renderer/EditorUntoldRenderer.swift +++ b/Sources/UntoldEditor/Renderer/EditorUntoldRenderer.swift @@ -54,10 +54,22 @@ extension UntoldRenderer { @inline(__always) func refreshInspector() { editorController?.refreshInspector() } + // Remove static batching when entity is transformed via gizmo + @inline(__always) + func handleStaticBatchOnTransform(entityId: EntityID) { + if hasComponent(entityId: entityId, componentType: StaticBatchComponent.self) { + removeEntityStaticBatchComponent(entityId: entityId) + if isBatchingEnabled() { + generateBatches() + } + } + } + switch (editorController!.activeMode, editorController!.activeAxis) { // MARK: - Translate case (.translate, .x) where InputSystem.shared.mouseActive: + handleStaticBatchOnTransform(entityId: activeEntity) let axis = simd_float3(1, 0, 0) let amt = computeAxisTranslationGizmo( axisWorldDir: axis, @@ -73,6 +85,7 @@ extension UntoldRenderer { refreshInspector() case (.translate, .y) where InputSystem.shared.mouseActive: + handleStaticBatchOnTransform(entityId: activeEntity) let axis = simd_float3(0, 1, 0) let amt = computeAxisTranslationGizmo(axisWorldDir: axis, gizmoWorldPosition: getLocalPosition(entityId: activeEntity), @@ -86,6 +99,7 @@ extension UntoldRenderer { refreshInspector() case (.translate, .z) where InputSystem.shared.mouseActive: + handleStaticBatchOnTransform(entityId: activeEntity) let axis = simd_float3(0, 0, 1) let amt = computeAxisTranslationGizmo(axisWorldDir: axis, gizmoWorldPosition: getLocalPosition(entityId: activeEntity), @@ -101,6 +115,7 @@ extension UntoldRenderer { // MARK: - Rotate case (.rotate, .x) where InputSystem.shared.mouseActive: + handleStaticBatchOnTransform(entityId: activeEntity) let axis = simd_float3(1, 0, 0) let angle = computeRotationAngleFromGizmo( axis: axis, @@ -118,6 +133,7 @@ extension UntoldRenderer { refreshInspector() case (.rotate, .y) where InputSystem.shared.mouseActive: + handleStaticBatchOnTransform(entityId: activeEntity) let axis = simd_float3(0, 1, 0) let angle = computeRotationAngleFromGizmo( axis: axis, @@ -135,6 +151,7 @@ extension UntoldRenderer { refreshInspector() case (.rotate, .z) where InputSystem.shared.mouseActive: + handleStaticBatchOnTransform(entityId: activeEntity) let axis = simd_float3(0, 0, 1) let angle = computeRotationAngleFromGizmo( axis: axis, @@ -154,6 +171,7 @@ extension UntoldRenderer { // MARK: - Scale case (.scale, .x) where InputSystem.shared.mouseActive: + handleStaticBatchOnTransform(entityId: activeEntity) let axis = simd_float3(1, 0, 0) let amt = computeAxisTranslationGizmo(axisWorldDir: axis, gizmoWorldPosition: getLocalPosition(entityId: activeEntity), @@ -169,6 +187,7 @@ extension UntoldRenderer { refreshInspector() case (.scale, .y) where InputSystem.shared.mouseActive: + handleStaticBatchOnTransform(entityId: activeEntity) let axis = simd_float3(0, 1, 0) let amt = computeAxisTranslationGizmo(axisWorldDir: axis, gizmoWorldPosition: getLocalPosition(entityId: activeEntity), @@ -184,6 +203,7 @@ extension UntoldRenderer { refreshInspector() case (.scale, .z) where InputSystem.shared.mouseActive: + handleStaticBatchOnTransform(entityId: activeEntity) let axis = simd_float3(0, 0, 1) let amt = computeAxisTranslationGizmo(axisWorldDir: axis, gizmoWorldPosition: getLocalPosition(entityId: activeEntity), diff --git a/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift b/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift index 937b240..603b4a3 100644 --- a/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift +++ b/Sources/UntoldEditor/Systems/EditorRenderingSystem.swift @@ -82,16 +82,28 @@ func buildEditModeGraph() -> RenderGraphResult { ) graph[shadowPass.id] = shadowPass + // Add batched shadow pass (runs after regular shadow pass) + let batchedShadowPass = RenderPass( + id: "batchedShadow", dependencies: [shadowPass.id], execute: RenderPasses.batchedShadowExecution + ) + graph[batchedShadowPass.id] = batchedShadowPass + let modelPass = RenderPass( - id: "model", dependencies: [shadowPass.id], execute: RenderPasses.modelExecution + id: "model", dependencies: [batchedShadowPass.id], execute: RenderPasses.modelExecution ) graph[modelPass.id] = modelPass - let lightPass = RenderPass(id: "lightPass", dependencies: [modelPass.id, shadowPass.id], execute: RenderPasses.lightExecution) + // Add batched model pass (runs after regular model pass) + let batchedModelPass = RenderPass( + id: "batchedModel", dependencies: [modelPass.id], execute: RenderPasses.batchedModelExecution + ) + graph[batchedModelPass.id] = batchedModelPass + + let lightPass = RenderPass(id: "lightPass", dependencies: [batchedModelPass.id, modelPass.id, shadowPass.id], execute: RenderPasses.lightExecution) graph[lightPass.id] = lightPass let highlightPass = RenderPass( - id: "outline", dependencies: [modelPass.id], execute: RenderPasses.highlightExecution + id: "outline", dependencies: [batchedModelPass.id], execute: RenderPasses.highlightExecution ) graph[highlightPass.id] = highlightPass diff --git a/Sources/UntoldEditor/main.swift b/Sources/UntoldEditor/main.swift index 5f466d2..00005e6 100644 --- a/Sources/UntoldEditor/main.swift +++ b/Sources/UntoldEditor/main.swift @@ -17,7 +17,7 @@ class AppDelegate: NSObject, NSApplicationDelegate { var window: NSWindow! func applicationDidFinishLaunching(_: Notification) { - Logger.log(message: "Launching Untold Engine Editor v0.8.2") + Logger.log(message: "Launching Untold Engine Editor v0.9.0") // Step 1. Create and configure the window window = NSWindow( diff --git a/Tests/UntoldEditorTests/BuildEditModeGraphTests.swift b/Tests/UntoldEditorTests/BuildEditModeGraphTests.swift index 4803213..2239da6 100644 --- a/Tests/UntoldEditorTests/BuildEditModeGraphTests.swift +++ b/Tests/UntoldEditorTests/BuildEditModeGraphTests.swift @@ -60,16 +60,18 @@ final class BuildEditModeGraphTests: XCTestCase { XCTAssertEqual(finalID, "precomp") let expectedIDs: Set = [ - "environment", "shadow", "model", "lightPass", + "environment", "shadow", "batchedShadow", "model", "batchedModel", "lightPass", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", ] XCTAssertEqual(Set(graph.keys), expectedIDs) assertDeps(graph, "environment", []) assertDeps(graph, "shadow", ["environment"]) - assertDeps(graph, "model", ["shadow"]) - assertDeps(graph, "lightPass", ["model", "shadow"]) - assertDeps(graph, "outline", ["model"]) + assertDeps(graph, "batchedShadow", ["shadow"]) + assertDeps(graph, "model", ["batchedShadow"]) + assertDeps(graph, "batchedModel", ["model"]) + assertDeps(graph, "lightPass", ["batchedModel", "model", "shadow"]) + assertDeps(graph, "outline", ["batchedModel"]) assertDeps(graph, "lightVisualPass", ["outline"]) assertDeps(graph, "gizmo", ["lightVisualPass"]) assertDeps(graph, "gaussian", ["model"]) @@ -89,16 +91,18 @@ final class BuildEditModeGraphTests: XCTestCase { XCTAssertEqual(finalID, "precomp") let expectedIDs: Set = [ - "grid", "shadow", "model", "lightPass", + "grid", "shadow", "batchedShadow", "model", "batchedModel", "lightPass", "outline", "lightVisualPass", "gizmo", "precomp", "gaussian", ] XCTAssertEqual(Set(graph.keys), expectedIDs) assertDeps(graph, "grid", []) assertDeps(graph, "shadow", ["grid"]) - assertDeps(graph, "model", ["shadow"]) - assertDeps(graph, "lightPass", ["model", "shadow"]) - assertDeps(graph, "outline", ["model"]) + assertDeps(graph, "batchedShadow", ["shadow"]) + assertDeps(graph, "model", ["batchedShadow"]) + assertDeps(graph, "batchedModel", ["model"]) + assertDeps(graph, "lightPass", ["batchedModel", "model", "shadow"]) + assertDeps(graph, "outline", ["batchedModel"]) assertDeps(graph, "lightVisualPass", ["outline"]) assertDeps(graph, "gizmo", ["lightVisualPass"]) assertDeps(graph, "gaussian", ["model"]) diff --git a/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift b/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift index ad817e5..aeeb135 100644 --- a/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift +++ b/Tests/UntoldEditorTests/EditorRenderingSystemTests.swift @@ -67,7 +67,9 @@ final class EditorRenderingSystemTests: XCTestCase { // Verify essential passes exist XCTAssertNotNil(graph["shadow"], "Shadow pass should exist") + XCTAssertNotNil(graph["batchedShadow"], "Batched shadow pass should exist") XCTAssertNotNil(graph["model"], "Model pass should exist") + XCTAssertNotNil(graph["batchedModel"], "Batched model pass should exist") XCTAssertNotNil(graph["lightPass"], "Light pass should exist") XCTAssertNotNil(graph["outline"], "Highlight/outline pass should exist") XCTAssertNotNil(graph["lightVisualPass"], "Light visual pass should exist") @@ -91,7 +93,9 @@ final class EditorRenderingSystemTests: XCTestCase { // Verify essential passes exist XCTAssertNotNil(graph["shadow"], "Shadow pass should exist") + XCTAssertNotNil(graph["batchedShadow"], "Batched shadow pass should exist") XCTAssertNotNil(graph["model"], "Model pass should exist") + XCTAssertNotNil(graph["batchedModel"], "Batched model pass should exist") XCTAssertNotNil(graph["lightPass"], "Light pass should exist") XCTAssertNotNil(graph["outline"], "Highlight/outline pass should exist") XCTAssertNotNil(graph["lightVisualPass"], "Light visual pass should exist") @@ -112,10 +116,13 @@ final class EditorRenderingSystemTests: XCTestCase { // Assert - Verify dependency chain XCTAssertEqual(graph["grid"]?.dependencies.count, 0, "Grid pass should have no dependencies") XCTAssertEqual(graph["shadow"]?.dependencies, ["grid"], "Shadow should depend on grid") - XCTAssertEqual(graph["model"]?.dependencies, ["shadow"], "Model should depend on shadow") + XCTAssertEqual(graph["batchedShadow"]?.dependencies, ["shadow"], "Batched shadow should depend on shadow") + XCTAssertEqual(graph["model"]?.dependencies, ["batchedShadow"], "Model should depend on batchedShadow") + XCTAssertEqual(graph["batchedModel"]?.dependencies, ["model"], "Batched model should depend on model") XCTAssertTrue(graph["lightPass"]?.dependencies.contains("model") ?? false, "Light pass should depend on model") XCTAssertTrue(graph["lightPass"]?.dependencies.contains("shadow") ?? false, "Light pass should depend on shadow") - XCTAssertEqual(graph["outline"]?.dependencies, ["model"], "Outline should depend on model") + XCTAssertTrue(graph["lightPass"]?.dependencies.contains("batchedModel") ?? false, "Light pass should depend on batchedModel") + XCTAssertEqual(graph["outline"]?.dependencies, ["batchedModel"], "Outline should depend on batchedModel") XCTAssertEqual(graph["lightVisualPass"]?.dependencies, ["outline"], "Light visual pass should depend on outline") XCTAssertEqual(graph["gizmo"]?.dependencies, ["lightVisualPass"], "Gizmo should depend on light visual pass") @@ -135,7 +142,9 @@ final class EditorRenderingSystemTests: XCTestCase { // Assert - Verify dependency chain XCTAssertEqual(graph["environment"]?.dependencies.count, 0, "Environment pass should have no dependencies") XCTAssertEqual(graph["shadow"]?.dependencies, ["environment"], "Shadow should depend on environment") - XCTAssertEqual(graph["model"]?.dependencies, ["shadow"], "Model should depend on shadow") + XCTAssertEqual(graph["batchedShadow"]?.dependencies, ["shadow"], "Batched shadow should depend on shadow") + XCTAssertEqual(graph["model"]?.dependencies, ["batchedShadow"], "Model should depend on batchedShadow") + XCTAssertEqual(graph["batchedModel"]?.dependencies, ["model"], "Batched model should depend on model") } func test_buildEditModeGraph_canBeTopologicallySorted() { @@ -285,9 +294,10 @@ final class EditorRenderingSystemTests: XCTestCase { return } - XCTAssertEqual(lightPass.dependencies.count, 2, "Light pass should have exactly 2 dependencies") + XCTAssertEqual(lightPass.dependencies.count, 3, "Light pass should have exactly 3 dependencies") XCTAssertTrue(lightPass.dependencies.contains("model"), "Light pass should depend on model") XCTAssertTrue(lightPass.dependencies.contains("shadow"), "Light pass should depend on shadow") + XCTAssertTrue(lightPass.dependencies.contains("batchedModel"), "Light pass should depend on batchedModel") } // MARK: - Helper Methods