diff --git a/CHANGELOG.md b/CHANGELOG.md index d3d2a9612..5bf3ffcf0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,36 @@ # Changelog +## v0.11.3 - 2026-03-24 +### 🐞 Fixes +- [Patch] Request world sensing authorization before starting ARKit session (31eac96…) +- [Patch] fix camera orbit function (0099534…) +- [Patch] Fixed issues with texture streaming and batch system (ff3856b…) +- [Patch] Added stream texture debugger (1a0638e…) +- [Patch] Implemented progressive asset loader (123f5ac…) +- [Patch] Modified the min and max for texture streaming (a36344e…) +- [Patch] Fixed issue with progressive loading in XR (45ba06c…) +- [Patch] First implementation of out-of-core (2e5e01e…) +- [Patch] Improve out-of-core loading and eviction logic (0f0ebd3…) +- [Patch] Improved performance of OOC (3496d13…) +- [Patch]Stabilize asset streaming (bcb4c73…) +- [Patch]Release MDLAsset CPU residency, add disk-backed cold re-streaming (07455b7…) +- [Patch] Refine admission gate: add soft zone and fallback mesh on reject (9517e64…) +- [Patch] Fix wrong initial textures by correcting TextureLoader cache key (db87e46…) +- [Patch] Fix wrong batch textures by using object-identity. (65abf1d…) +- [Patch] Complete three-tier texture streaming and add scene profiles (b8d7b66…) +- [Patch] fixed lod system to work with OOC (180b650…) +- [Patch] Formatted files (43a7f59…) +- [Patch] Improved OOC performance (41c96df…) +- [Patch] Added profiling documentation (58961cf…) +- [Patch] Fixed Out-of-Core texture crash (0c3e5cf…) +- [Patch] Added g-buffer debugger (12b60d2…) +- [Patch] Fixed camera position for OOC system for XR (828cac7…) +- [Patch] Fixed memory budget manager for XR (fdf9d61…) +- [Patch] Fixed closest point used for OOC (659c795…) +### 📚 Docs +- [Docs] Added system architecture docs (e52dc7a…) +- [Docs] Update progressive asset loading docs (85428b7…) +- [Docs] Updated documentation (5ef25bb…) +- [Docs] Updated Out of core docs (e225123…) ## v0.11.2 - 2026-03-18 ### 🐞 Fixes - [Patch] fixed texture streaming (f676f75…) diff --git a/README.md b/README.md index c9b01786a..be7894942 100644 --- a/README.md +++ b/README.md @@ -29,10 +29,24 @@ Untold Engine is an **open-source 3D engine written in Swift and powered by Metal**, designed for Apple platforms including **macOS, iOS, and visionOS**. -The project focuses on building a clean and approachable architecture with modern rendering, an ECS-based gameplay model, and an extensible asset pipeline. +The project focuses on building a **clean, system-driven architecture** with modern rendering, an ECS-based gameplay model, and an extensible asset pipeline. The engine is under active development and continues to evolve as new systems and workflows are added. +--- + +## 🎯 Who is this for? + +Untold Engine is designed for developers who: + +- Want **full control over rendering and systems** +- Prefer working directly with **Swift + Metal** +- Are building **XR, 3D, or visualization applications** +- Need to handle **large scenes, streaming data, or custom pipelines** + +This is not a drag-and-drop editor-first engine — it is a **code-driven engine for developers who want to understand and shape the system**. + + Creator & Lead Developer: http://www.haroldserrano.com @@ -42,7 +56,7 @@ http://www.haroldserrano.com The fastest way to experience Untold Engine is to run the demo project. -Clone the repository and run the demo: +Clone the repository, run the engine and load a USDZ file: ```bash git clone https://github.com/untoldengine/UntoldEngine.git @@ -60,11 +74,29 @@ No additional setup is required. --- -# Demo +## 🧱 Core Direction + +Untold Engine is being developed with the following goals: + +- **Large Scene Rendering** + Striving to support LOD, geometry streaming, batching, and memory-aware systems for large datasets + +- **XR / visionOS Support** + Expanding support for spatial input, AR workflows, and Vision Pro experiences + +- **Metal-First Architecture** + Keeping the rendering layer close to Metal to maintain performance and control + +--- + +## 🖼 Example Use Cases -Click the image below to watch the engine in action. +Untold Engine aims to support applications such as: -[![Watch the video](docs/images/enginethumbnail.jpg)](https://vimeo.com/1116239409?share=copy#t=0) +- XR applications (Vision Pro, ARKit-based apps) +- Large-scale scene visualization (cities, archviz, datasets) +- Custom rendering pipelines and experiments +- Simulation tools and interactive 3D systems --- @@ -86,72 +118,46 @@ Click the image below to watch the engine in action. --- -# Project Ecosystem - -The Untold Engine project includes several related components. - -## Untold Engine Studio - -Standalone application that includes: - -- Visual editor -- Full engine integration -- Asset management tools - -Download: - -https://github.com/untoldengine/UntoldEditor/releases - -Ideal for developers who want to **create games using the editor**. - ---- - -## Untold Engine (this repository) - -Contains the **core engine systems**: - -- Rendering -- ECS architecture -- Physics and animation -- Scene graph -- Input systems - -This repository is primarily for: - -- Engine contributors -- Developers studying the engine architecture -- Users running demo projects - ---- - -## Untold Editor - -The visual editor used for managing assets, scenes, and workflows. - -Repository: - -https://github.com/untoldengine/UntoldEditor - ---- - -# Documentation - -For guides and API documentation visit: - -https://untoldengine.github.io/UntoldEngine - -You will find: - -- Tutorials for using the engine -- Architecture documentation -- Setup instructions -- Guides for common tasks +# Engine Architecture: + +- [Rendering System](docs/Architecture/renderingSystem.md) +- [XR Rendering System](docs/Architecture/xrRenderingSystem.md) +- [Static Batching System](docs/Architecture/batchingSystem.md) +- [Geometry Streaming System](docs/Architecture/geometryStreamingSystem.md) +- [LOD System](docs/Architecture/lodSystem.md) +- [Progressive Asset Loader](docs/Architecture/progressiveAssetLoader.md) +- [Streaming Cache Lifecycle](docs/Architecture/streamingCacheLifecycle.md) +- [Texture Streaming System](docs/Architecture/textureStreamingSystem.md) +- [Out of Core](docs/Architecture/outOfCore.md) + +# Engine API + +- [Registration System](docs/API/UsingRegistrationSystem.md) +- [Scenegraph](docs/API/UsingScenegraph.md) +- [Transform System](docs/API/UsingTransformSystem.md) +- [Camera System](docs/API/UsingCameraSystem.md) +- [Rendering System](docs/API/UsingRenderingSystem.md) +- [Lighting System](docs/API/UsingLightingSystem.md) +- [Materials](docs/API/UsingMaterials.md) +- [Input System](docs/API/UsingInputSystem.md) +- [Physics System](docs/API/UsingPhysicsSystem.md) +- [Steering System](docs/API/UsingSteeringSystem.md) +- [Animation System](docs/API/UsingAnimationSystem.md) +- [Async Loading](docs/API/UsingAsyncLoading.md) +- [LOD System](docs/API/UsingLODSystem.md) +- [Static Batching System](docs/API/UsingStaticBatchingSystem.md) +- [Geometry Streaming System](docs/API/UsingGeometryStreamingSystem.md) +- [LOD-Batching-Streaming](docs/API/UsingLOD-Batching-Streaming.md) +- [Spatial Input](docs/API/UsingSpatialInput.md) +- [Gaussian System](docs/API/UsingGaussianSystem.md) +- [Spatical Debugger](docs/API/SpatialDebugger.md) +- [Profiler](/docs/API/UsingProfiler.md) --- -# Command Line Tool (Optional) +# Set Up an Xcode Project with Untold Engine -If you prefer working from the terminal, you can use the project creation CLI. +You can easily create an xcode project using the Untold Engine as dependency Install it from the repository: @@ -169,6 +175,22 @@ cd MyGame untoldengine-create create MyGame ``` +The CLI supports multiple platforms: + +```bash +# macOS (default) +untoldengine-create create MyGame --platform macos + +# iOS +untoldengine-create create MyGame --platform ios + +# iOS with ARKit +untoldengine-create create MyGame --platform iosar + +# visionOS (Apple Vision Pro) +untoldengine-create create MyGame --platform visionos +``` + Features: - Create macOS, iOS, and visionOS projects @@ -212,6 +234,9 @@ Before submitting a pull request please read: - CONTRIBUTING.md - CONTRIBUTOR_LICENSE_AGREEMENT.md +- [Contributing Guidelines](docs/Contributor/ContributionGuidelines.md) +- [Formatting](docs/Contributor/Formatting.md) +- [Versioning](docs/Contributor/versioning.md) All contributions are licensed under **MPL-2.0**. diff --git a/Sources/CShaderTypes/ShaderTypes.h b/Sources/CShaderTypes/ShaderTypes.h index 99d5e2b8a..68f2c97df 100644 --- a/Sources/CShaderTypes/ShaderTypes.h +++ b/Sources/CShaderTypes/ShaderTypes.h @@ -119,6 +119,11 @@ typedef enum{ prePassPassthroughBufferIndex }PrePassBufferIndices; +typedef enum{ + debugPassModeIndex, + debugPassFrustumPlanesIndex +}DebugPassBufferIndices; + typedef enum{ prePassFinalTextureIndex, prePassEnvTextureIndex, diff --git a/Sources/DemoGame/AppDelegate.swift b/Sources/DemoGame/AppDelegate.swift new file mode 100644 index 000000000..fa8a74cc0 --- /dev/null +++ b/Sources/DemoGame/AppDelegate.swift @@ -0,0 +1,101 @@ +// +// AppDelegate.swift +// + +#if os(macOS) + import AppKit + import SwiftUI + import UntoldEngine + + @MainActor + final class AppDelegate: NSObject, NSApplicationDelegate { + private enum Constants { + static let appVersion = "0.11.2" + static let windowSize = NSSize(width: 1920, height: 1080) + } + + var window: NSWindow! + var renderer: UntoldRenderer! + var gameScene: GameScene! + var demoState = DemoState() + + func applicationDidFinishLaunching(_: Notification) { + print("Launching Untold Engine v\(Constants.appVersion)") + + setupWindow() + setupRendererAndScene() + wireDemoStateCallbacks() + presentHUD() + } + + func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { + true + } + + private func setupWindow() { + window = NSWindow( + contentRect: NSRect(origin: .zero, size: Constants.windowSize), + styleMask: [.titled, .closable, .resizable], + backing: .buffered, + defer: false + ) + window.title = "Untold Engine v\(Constants.appVersion)" + window.center() + } + + private func setupRendererAndScene() { + guard let renderer = UntoldRenderer.create() else { + print("Failed to initialize the renderer.") + return + } + self.renderer = renderer + + gameScene = GameScene() + renderer.setupCallbacks( + gameUpdate: { [weak self] deltaTime in self?.gameScene.update(deltaTime: deltaTime) }, + handleInput: { [weak self] in self?.gameScene.handleInput() } + ) + } + + private func wireDemoStateCallbacks() { + demoState.onLoadFile = { [weak self] path, completion in + self?.gameScene.loadFile(path: path, completion: completion) + } + demoState.onBatchingChanged = { [weak self] enabled in + self?.gameScene.setBatching(enabled) + } + demoState.onStreamingChanged = { [weak self] enabled, radius, unloadRadius in + self?.gameScene.setStreaming( + enabled, + streamingRadius: Float(radius), + unloadRadius: Float(unloadRadius) + ) + } + demoState.onLodDebugChanged = { [weak self] enabled in + self?.gameScene.setLodDebug(enabled) + } + demoState.onTextureStreamingTierDebugChanged = { [weak self] enabled in + self?.gameScene.setStreamingTierDebug(enabled) + } + demoState.onRenderDebugViewChanged = { [weak self] mode in + self?.gameScene.setRenderDebugView(mode) + } + demoState.onSpatialDebugChanged = { [weak self] enabled, occupiedOnly, colorMode in + self?.gameScene.setSpatialDebug( + enabled: enabled, + occupiedOnly: occupiedOnly, + colorMode: colorMode + ) + } + } + + private func presentHUD() { + guard let renderer else { return } + let hostingView = NSHostingView(rootView: DemoHUD(renderer: renderer, state: demoState)) + window.contentView = hostingView + window.makeKeyAndOrderFront(nil) + NSApp.setActivationPolicy(.regular) + NSApp.activate(ignoringOtherApps: true) + } + } +#endif diff --git a/Sources/DemoGame/BallSystem.swift b/Sources/DemoGame/BallSystem.swift deleted file mode 100644 index ec6b2ee61..000000000 --- a/Sources/DemoGame/BallSystem.swift +++ /dev/null @@ -1,126 +0,0 @@ -// -// CustomSystem.swift -// -// -// 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 os(macOS) - import simd - import SwiftUI - import UntoldEngine - - /// BallState represents the different states a ball can be in during gameplay. - /// This makes it easier to manage transitions like idle → kick → moving → decelerating. - enum BallState: Codable { - case idle - case kick - case moving - case decelerating - } - - /// BallComponent is a custom ECS component that stores the ball's state and motion data. - /// Every entity with this component will behave like a ball in the game. - public class BallComponent: Component, Codable { - var motionAccumulator: simd_float3 = .zero // Used to accumulate velocity for smoother force application - var state: BallState = .idle // Current state of the ball (idle, moving, etc.) - var velocity: simd_float3 = .zero // Current velocity of the ball - public required init() {} - } - - /// ballSystemUpdate runs once per frame and updates all entities that have a BallComponent. - /// This is where we apply physics, update states, and define how the ball behaves. - public func ballSystemUpdate(deltaTime: Float) { - // Get the ID of the BallComponent so we can query entities that use it - let customId = getComponentId(for: BallComponent.self) - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - for entity in entities { - guard let ballComponent = scene.get(component: BallComponent.self, for: entity) else { continue } - - // Apply drag to simulate resistance as the ball moves - setLinearDragCoefficient(entityId: entity, coefficients: simd_float2(0.7, 0.0)) - setAngularDragCoefficient(entityId: entity, coefficients: simd_float2(0.07, 0.0)) - - // Update the ball based on its current state - switch ballComponent.state { - case .idle: - // Do nothing, the ball is at rest - break - case .kick: - // Transition to moving when the ball is kicked - ballComponent.state = .moving - applyVelocity(finalVelocity: ballComponent.velocity * 5.0, deltaTime: deltaTime, ball: entity) - case .moving: - // If the velocity drops below a threshold, start decelerating - if simd_length(getVelocity(entityId: entity)) <= 0.1 { - ballComponent.state = .decelerating - } - case .decelerating: - // Gradually slow down the ball - decelerate(deltaTime: deltaTime, ball: entity) - if simd_length(getVelocity(entityId: entity)) < 0.1 { - // You could transition back to .idle here if desired - } - } - } - } - - /// Apply a force to the ball to simulate a kick or strong push. - /// Uses an accumulator to smooth motion and applies both linear and angular forces. - func applyVelocity(finalVelocity: simd_float3, deltaTime: Float, ball: EntityID) { - guard let customComponent = scene.get(component: BallComponent.self, for: ball) else { return } - - let mass: Float = getMass(entityId: ball) - let ballDim = getDimension(entityId: ball) - - // Blend previous motion with new input for smoother physics - let bias: Float = 0.4 - let vComp: simd_float3 = finalVelocity * (1.0 - bias) - customComponent.motionAccumulator = customComponent.motionAccumulator * bias + vComp - - // Apply linear force based on mass and deltaTime - var force: simd_float3 = (customComponent.motionAccumulator * mass) / deltaTime - applyForce(entityId: ball, force: force) - - // Apply angular force so the ball spins as it moves - let upAxis = simd_float3(0.0, ballDim.depth / 2.0, 0.0) - force *= 0.25 - applyMoment(entityId: ball, force: force, at: upAxis) - - // Reset velocity so physics is only applied through forces - clearVelocity(entityId: ball) - clearAngularVelocity(entityId: ball) - } - - /// Gradually slow down the ball by applying counter-forces. - /// Works similarly to applyVelocity, but reduces motion instead of adding it. - func decelerate(deltaTime: Float, ball: EntityID) { - guard let customComponent = scene.get(component: BallComponent.self, for: ball) else { return } - - let ballDim = getDimension(entityId: ball) - let velocity: simd_float3 = getVelocity(entityId: ball) - - // Blend down velocity for smoother deceleration - let bias: Float = 0.5 - let vComp: simd_float3 = velocity * (1.0 - bias) - customComponent.motionAccumulator = customComponent.motionAccumulator * bias + vComp - - // Apply counter-force to slow down - var force: simd_float3 = (customComponent.motionAccumulator * getMass(entityId: ball)) / deltaTime - force *= 0.15 - applyForce(entityId: ball, force: force) - - // Apply spin reduction - let upAxis = simd_float3(0.0, ballDim.depth / 2.0, 0.0) - force *= 0.25 - applyMoment(entityId: ball, force: force, at: upAxis) - - // Clear velocity so deceleration is handled through applied forces - clearVelocity(entityId: ball) - clearAngularVelocity(entityId: ball) - } - -#endif diff --git a/Sources/DemoGame/CameraFollow.swift b/Sources/DemoGame/CameraFollow.swift deleted file mode 100644 index 2c0134a79..000000000 --- a/Sources/DemoGame/CameraFollow.swift +++ /dev/null @@ -1,113 +0,0 @@ -// -// CameraFollow.swift -// -// -// 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 os(macOS) - import Foundation - import simd - import SwiftUI - import UntoldEngine - - /// CameraFollowComponent defines how a camera should follow a target entity. - /// It stores the name of the target and the offset from that target. - /// This makes it possible to create a third-person or top-down camera easily. - public class CameraFollowComponent: Component, Codable { - var targetName: String = "ball" // The name of the entity the camera should follow - var offset: simd_float3 = .init(0.0, 1.0, 5.0) // The offset position relative to the target - public required init() {} - } - - // ----------------------------------------------------------------------------- - // Camera Follow System - // Updates all entities with a CameraFollowComponent once per frame. - // Calls the cameraFollow function to smoothly track the target entity. - // ----------------------------------------------------------------------------- - - public func cameraFollowUpdate(deltaTime _: Float) { - let customId = getComponentId(for: CameraFollowComponent.self) - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - for entity in entities { - guard let cameraFollowComponent = scene.get(component: CameraFollowComponent.self, for: entity) else { continue } - - cameraFollow( - entityId: entity, - targetName: cameraFollowComponent.targetName, - offset: cameraFollowComponent.offset, - smoothSpeed: 0.5, - alignWithOrientation: false - ) - } - } - - // ----------------------------------------------------------------------------- - // Core camera follow logic - // Moves the camera smoothly to follow the target and optionally aligns its - // orientation with the target’s forward direction. - // ----------------------------------------------------------------------------- - - func cameraFollow( - entityId: EntityID, - targetName: String, - offset: simd_float3, - smoothSpeed: Float, - lockXAxis: Bool = false, - lockYAxis: Bool = false, - lockZAxis: Bool = false, - alignWithOrientation: Bool = true - ) { - // Ensure the entity has a CameraComponent - guard let cameraComponent = scene.get(component: CameraComponent.self, for: entityId) else { - return - } - - // Find the target entity by name - guard let targetId: EntityID = findEntity(name: targetName) else { - return - } - - // Get the target's position and orientation - var targetPosition: simd_float3 = UntoldEngine.getPosition(entityId: targetId) - let targetOrientation: simd_float3x3 = UntoldEngine.getOrientation(entityId: targetId) // Rotation matrix - var cameraPosition = cameraComponent.localPosition - - // Apply axis locking (keeps the camera fixed on chosen axes) - if lockXAxis { targetPosition.x = cameraPosition.x } - if lockYAxis { targetPosition.y = cameraPosition.y } - if lockZAxis { targetPosition.z = cameraPosition.z } - - // Adjust offset based on orientation if requested - var adjustedOffset = offset - if alignWithOrientation { - // Rotate the offset by the target's orientation matrix - adjustedOffset = targetOrientation * offset - } - - // Calculate where the camera *wants* to be - let desiredPosition: simd_float3 = targetPosition + adjustedOffset - - // Smoothly interpolate from the current camera position to the desired position - cameraPosition = lerp(start: cameraPosition, end: desiredPosition, t: smoothSpeed) - - // Move the camera to its new position - moveCameraTo(entityId: entityId, cameraPosition.x, cameraPosition.y, cameraPosition.z) - - // Orient the camera towards the target if enabled - if alignWithOrientation { - // Forward direction of the target - let _: simd_float3 = targetOrientation * simd_float3(0, 0, 1) - - // Up vector (adjust depending on coordinate system) - let up: simd_float3 = targetOrientation * simd_float3(0, 1, 0) - - // Look at the target from the camera’s position - cameraLookAt(entityId: entityId, eye: cameraPosition, target: targetPosition, up: up) - } - } - -#endif diff --git a/Sources/DemoGame/DemoHUD.swift b/Sources/DemoGame/DemoHUD.swift new file mode 100644 index 000000000..a570fa819 --- /dev/null +++ b/Sources/DemoGame/DemoHUD.swift @@ -0,0 +1,234 @@ +// +// DemoHUD.swift +// + +#if os(macOS) + import SwiftUI + import UniformTypeIdentifiers + import UntoldEngine + + struct StatsPanel: View { + let stats: EngineStatsSnapshot + + private var fps: Double { + stats.timing.smoothedFrameMs > 0 ? 1000.0 / stats.timing.smoothedFrameMs : 0 + } + + var body: some View { + VStack(alignment: .leading, spacing: 4) { + Text("Engine Stats").font(.headline) + Divider() + + row("FPS", String(format: "%.1f", fps)) + row("CPU Frame", String(format: "%.2f ms", stats.timing.frameTotalMs)) + row("GPU", String(format: "%.2f ms", stats.timing.gpuExecutionMs)) + Divider() + + row("Draw Calls", "\(stats.render.drawCallsTotal)") + row(" Opaque", "\(stats.render.drawCallsOpaque)") + row(" Batched", "\(stats.render.drawCallsBatched)") + row("Triangles", fmt(stats.render.trianglesTotal)) + row("Visible", "\(stats.render.visibleInstances)") + Divider() + + row("Frustum", "\(stats.culling.frustumPassed) / \(stats.culling.frustumTested)") + row("Occlusion", "\(stats.culling.occlusionPassed) / \(stats.culling.occlusionTested)") + Divider() + + row("Batch Groups", "\(stats.batching.batchGroupCount)") + row("Batched Meshes", "\(stats.batching.batchedMeshCount)") + } + .font(.system(.caption, design: .monospaced)) + .padding(12) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) + } + + private func row(_ label: String, _ value: String) -> some View { + HStack { + Text(label).foregroundStyle(.secondary) + Spacer() + Text(value) + } + } + + private func fmt(_ n: Int) -> String { + if n >= 1_000_000 { return String(format: "%.1fM", Double(n) / 1_000_000) } + if n >= 1000 { return String(format: "%.1fK", Double(n) / 1000) } + return "\(n)" + } + } + + struct DemoHUD: View { + private enum Constants { + static let statsRefreshInterval: TimeInterval = 0.1 + static let usdzExtension = "usdz" + } + + var renderer: UntoldRenderer + @Bindable var state: DemoState + @State private var showFilePicker = false + + var body: some View { + ZStack(alignment: .topLeading) { + SceneView(renderer: renderer) + + VStack(alignment: .leading, spacing: 8) { + HStack(spacing: 8) { + Button("Load USDZ...") { showFilePicker = true } + .buttonStyle(.bordered) + .disabled(state.isLoading) + if state.isLoading { + ProgressView() + .scaleEffect(0.6) + .frame(width: 16, height: 16) + } + } + + Divider() + + sectionLabel("CONTROLS") + controlHint("WASD / QE", "Translate") + controlHint("Right-click drag", "Rotate") + + Divider() + + sectionLabel("FEATURES") + + Toggle("Static Batching", isOn: $state.batchingEnabled) + .toggleStyle(.checkbox) + .disabled(!state.hasLoadedEntity || state.isLoading) + + Toggle("Geometry Streaming", isOn: $state.streamingEnabled) + .toggleStyle(.checkbox) + .disabled(!state.hasLoadedEntity || state.isLoading) + + HStack { + Text("Stream Radius").foregroundStyle(.secondary) + Spacer() + TextField("", value: $state.streamingRadius, format: .number) + .textFieldStyle(.roundedBorder) + .frame(width: 70) + } + .padding(.leading, 12) + .opacity(state.streamingEnabled ? 1.0 : 0.35) + .disabled(!state.streamingEnabled) + + HStack { + Text("Unload Radius").foregroundStyle(.secondary) + Spacer() + TextField("", value: $state.unloadRadius, format: .number) + .textFieldStyle(.roundedBorder) + .frame(width: 70) + } + .padding(.leading, 12) + .opacity(state.streamingEnabled ? 1.0 : 0.35) + .disabled(!state.streamingEnabled) + + Divider() + + sectionLabel("DEBUG") + + Toggle("LOD Debug", isOn: $state.lodDebugEnabled) + .toggleStyle(.checkbox) + + Toggle("Texture Streaming Debug", isOn: $state.textureStreamingTierDebugEnabled) + .toggleStyle(.checkbox) + + Picker("G-Buffer View", selection: $state.renderDebugView) { + Text("Lit").tag(RenderDebugViewMode.lit) + Text("Albedo").tag(RenderDebugViewMode.albedo) + Text("Normal").tag(RenderDebugViewMode.normal) + Text("Depth").tag(RenderDebugViewMode.depth) + Text("SSAO (Blurred)").tag(RenderDebugViewMode.ssaoBlurred) + } + .pickerStyle(.menu) + + Toggle("Spatial Debug", isOn: $state.spatialDebugEnabled) + .toggleStyle(.checkbox) + if state.spatialDebugEnabled { + Toggle("Occupied Only", isOn: $state.spatialOccupiedOnly) + .toggleStyle(.checkbox) + .padding(.leading, 12) + Picker("Mode", selection: $state.spatialColorMode) { + Text("Plain").tag(SpatialDebugLeafColorMode.plain) + Text("Residency").tag(SpatialDebugLeafColorMode.residency) + Text("Culling").tag(SpatialDebugLeafColorMode.culling) + } + .pickerStyle(.segmented) + .frame(minWidth: 180) + } + + Divider() + + Toggle("Engine Stats", isOn: $state.showStats) + .toggleStyle(.checkbox) + } + .padding(12) + .fixedSize(horizontal: true, vertical: false) + .background(.regularMaterial, in: RoundedRectangle(cornerRadius: 8)) + .padding() + + if state.showStats { + HStack { + Spacer() + StatsPanel(stats: state.stats) + .frame(width: 260) + .padding() + } + .frame(maxWidth: .infinity, alignment: .topTrailing) + } + } + .onReceive(Timer.publish(every: Constants.statsRefreshInterval, on: .main, in: .common).autoconnect()) { _ in + if state.showStats { + state.stats = getEngineStatsSnapshot() + } + } + .fileImporter( + isPresented: $showFilePicker, + allowedContentTypes: [UTType(filenameExtension: Constants.usdzExtension) ?? .data] + ) { result in + guard case let .success(url) = result else { return } + let accessing = url.startAccessingSecurityScopedResource() + let path = url.deletingPathExtension().path + + state.batchingEnabled = false + state.streamingEnabled = false + state.isLoading = true + + guard let onLoadFile = state.onLoadFile else { + state.isLoading = false + if accessing { url.stopAccessingSecurityScopedResource() } + return + } + + onLoadFile(path) { isOutOfCore in + state.isLoading = false + state.hasLoadedEntity = true + if isOutOfCore { + state.streamingEnabled = true + } + if accessing { url.stopAccessingSecurityScopedResource() } + } + } + } + + private func sectionLabel(_ text: String) -> some View { + Text(text) + .font(.system(.caption, design: .default).weight(.semibold)) + .foregroundStyle(.secondary) + } + + private func controlHint(_ key: String, _ action: String) -> some View { + HStack { + Text(key) + .font(.system(.caption, design: .monospaced)) + .foregroundStyle(.primary) + Spacer() + Text(action) + .font(.system(.caption, design: .default)) + .foregroundStyle(.secondary) + } + .padding(.leading, 4) + } + } +#endif diff --git a/Sources/DemoGame/DemoState.swift b/Sources/DemoGame/DemoState.swift new file mode 100644 index 000000000..6cd141d29 --- /dev/null +++ b/Sources/DemoGame/DemoState.swift @@ -0,0 +1,81 @@ +// +// DemoState.swift +// + +#if os(macOS) + import Observation + import UntoldEngine + + /// Pure UI state. No engine calls here. + /// AppDelegate wires callbacks so this state can trigger GameScene methods. + @Observable final class DemoState { + private enum Defaults { + static let streamingRadius: Double = 200.0 + static let unloadRadius: Double = 350.0 + } + + // MARK: - File Loading + + var hasLoadedEntity: Bool = false + var isLoading: Bool = false + + // MARK: - Features + + var batchingEnabled: Bool = false { + didSet { onBatchingChanged?(batchingEnabled) } + } + + var streamingEnabled: Bool = false { + didSet { onStreamingChanged?(streamingEnabled, streamingRadius, unloadRadius) } + } + + var streamingRadius: Double = Defaults.streamingRadius { + didSet { if streamingEnabled { onStreamingChanged?(true, streamingRadius, unloadRadius) } } + } + + var unloadRadius: Double = Defaults.unloadRadius { + didSet { if streamingEnabled { onStreamingChanged?(true, streamingRadius, unloadRadius) } } + } + + // MARK: - Debug + + var lodDebugEnabled: Bool = false { + didSet { onLodDebugChanged?(lodDebugEnabled) } + } + + var textureStreamingTierDebugEnabled: Bool = false { + didSet { onTextureStreamingTierDebugChanged?(textureStreamingTierDebugEnabled) } + } + + var renderDebugView: RenderDebugViewMode = .lit { + didSet { onRenderDebugViewChanged?(renderDebugView) } + } + + var spatialDebugEnabled: Bool = false { + didSet { onSpatialDebugChanged?(spatialDebugEnabled, spatialOccupiedOnly, spatialColorMode) } + } + + var spatialColorMode: SpatialDebugLeafColorMode = .plain { + didSet { if spatialDebugEnabled { onSpatialDebugChanged?(true, spatialOccupiedOnly, spatialColorMode) } } + } + + var spatialOccupiedOnly: Bool = true { + didSet { if spatialDebugEnabled { onSpatialDebugChanged?(true, spatialOccupiedOnly, spatialColorMode) } } + } + + // MARK: - Stats + + var showStats: Bool = true + var stats: EngineStatsSnapshot = .init() + + // MARK: - Callbacks (wired by AppDelegate) + + var onLoadFile: ((String, @escaping (Bool) -> Void) -> Void)? + var onBatchingChanged: ((Bool) -> Void)? + var onStreamingChanged: ((Bool, Double, Double) -> Void)? + var onLodDebugChanged: ((Bool) -> Void)? + var onTextureStreamingTierDebugChanged: ((Bool) -> Void)? + var onRenderDebugViewChanged: ((RenderDebugViewMode) -> Void)? + var onSpatialDebugChanged: ((Bool, Bool, SpatialDebugLeafColorMode) -> Void)? + } +#endif diff --git a/Sources/DemoGame/DribblingSystem.swift b/Sources/DemoGame/DribblingSystem.swift deleted file mode 100644 index 3f1b37e2a..000000000 --- a/Sources/DemoGame/DribblingSystem.swift +++ /dev/null @@ -1,90 +0,0 @@ -// -// DribblingSystem.swift -// -// -// 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 os(macOS) - import Foundation - import simd - import SwiftUI - import UntoldEngine - - /// DribblinComponent stores data related to the player’s dribbling behavior. - /// It defines how fast the player can move, how hard they can kick the ball, - /// and the current input direction. This component will be attached to entities - /// (like a player) that can dribble the ball. - public class DribblinComponent: Component, Codable { - public required init() {} - var maxSpeed: Float = 5.0 // Maximum movement speed for dribbling - var kickSpeed: Float = 15.0 // Speed applied when kicking the ball - var direction: simd_float3 = .zero // Current movement direction (from input) - } - - // ----------------------------------------------------------------------------- - // Dribbling System - // This system is called every frame and updates entities with a DribblinComponent. - // It handles input (WASD keys), animations, and ball interactions. - // ----------------------------------------------------------------------------- - - public func dribblingSystemUpdate(deltaTime: Float) { - let customId = getComponentId(for: DribblinComponent.self) - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - // Look up the ball in the scene by name - guard let ball = findEntity(name: "ball") else { - return - } - guard let ballComponent = scene.get(component: BallComponent.self, for: ball) else { - return - } - - for entity in entities { - guard let dribblingComponent = scene.get(component: DribblinComponent.self, for: entity) else { - continue - } - - // If WASD keys are pressed → player is moving - if isWASDPressed() { - changeAnimation(entityId: entity, name: "running") - pausePhysicsComponent(entityId: entity, isPaused: false) - } else { - // No input → idle animation, freeze physics - changeAnimation(entityId: entity, name: "idle") - pausePhysicsComponent(entityId: entity, isPaused: true) - return - } - - // Compute direction from key inputs - var newPosition: simd_float3 = .zero - if InputSystem.shared.keyState.wPressed { newPosition.z += 1.0 } - if InputSystem.shared.keyState.sPressed { newPosition.z -= 1.0 } - if InputSystem.shared.keyState.aPressed { newPosition.x -= 1.0 } - if InputSystem.shared.keyState.dPressed { newPosition.x += 1.0 } - - // Get the ball’s position (ignore height for ground movement) - var ballPosition: simd_float3 = getPosition(entityId: ball) - ballPosition.y = 0.0 - - // Check distance: if close to the ball, apply a kick - if simd_length(getPosition(entityId: entity) - getPosition(entityId: ball)) < 1.0 { - ballComponent.state = .kick - ballComponent.velocity = simd_normalize(newPosition) - } - - // Steer the player toward the ball position (dribbling behavior) - steerSeek( - entityId: entity, - targetPosition: ballPosition, - maxSpeed: dribblingComponent.maxSpeed, - deltaTime: deltaTime, - turnSpeed: 50.0 - ) - } - } - -#endif diff --git a/Sources/DemoGame/GameScene.swift b/Sources/DemoGame/GameScene.swift new file mode 100644 index 000000000..babd848bb --- /dev/null +++ b/Sources/DemoGame/GameScene.swift @@ -0,0 +1,196 @@ +// +// GameScene.swift +// + +#if os(macOS) + import simd + import UntoldEngine + + // MARK: - GameScene + + /// Demo-facing bridge over the core engine API. + /// + /// Core Engine API map used by this demo: + /// - Entity lifecycle: `createEntity`, `setEntityName`, `destroyAllEntities` + /// - Camera/input: `createGameCamera`, `findGameCamera`, `moveCameraWithInput`, `orbitCameraAround` + /// - Asset loading: `setEntityMeshAsync` + /// - Performance features: `setEntityStaticBatchComponent`, `enableBatching`, `generateBatches`, `enableStreaming` + /// - Debug overlays: `setLODLevelDebug`, `setTextureStreamingTierDebug`, `setOctreeLeafBoundsDebug` + final class GameScene { + private enum Constants { + static let orbitTargetOffset: Float = 5.0 + static let cameraMoveSpeed: Float = 1.0 + static let cameraInputDeltaTime: Float = 0.1 + static let streamingPriority: Int = 10 + static let usdzExtension = "usdz" + } + + private(set) var loadedEntity: EntityID? + private var wasRightMousePressed: Bool = false + + init() { + InputSystem.shared.registerKeyboardEvents() + InputSystem.shared.registerMouseEvents() + bypassPostProcessing = true + setupDefaultSceneObjects() + } + } + + // MARK: - Scene Setup + + fileprivate extension GameScene { + func setupDefaultSceneObjects() { + let gameCamera = createEntity() + setEntityName(entityId: gameCamera, name: "Main Camera") + createGameCamera(entityId: gameCamera) + + let light = createEntity() + setEntityName(entityId: light, name: "Directional Light") + createDirLight(entityId: light) + + CameraSystem.shared.activeCamera = gameCamera + } + } + + // MARK: - Asset Loading + + extension GameScene { + /// Loads a USDZ file into the scene, replacing any previously loaded model. + /// + /// Asset load lifecycle contract: + /// 1. `destroyAllEntities` completion means teardown is finished; only then rebuild scene entities. + /// 2. `setEntityMeshAsync` completion means mesh loading/streaming setup is complete; only then update UI. + func loadFile(path: String, completion: @escaping (Bool) -> Void) { + clearSceneBatches() + loadedEntity = nil + + destroyAllEntities { [weak self] in + guard let self else { return } + setupDefaultSceneObjects() + + let camera = findGameCamera() + setOrbitOffset(entityId: camera, uTargetOffset: Constants.orbitTargetOffset) + + let entity = createEntity() + loadedEntity = entity + + setEntityMeshAsync(entityId: entity, filename: path, withExtension: Constants.usdzExtension) { isOutOfCore in + completion(isOutOfCore) + } + } + } + } + + // MARK: - Performance Features + + extension GameScene { + /// Marks the loaded entity as a static batch and generates batches, + /// or disables the batching system when turned off. + func setBatching(_ enabled: Bool) { + guard let entity = loadedEntity else { return } + if enabled { + setEntityStaticBatchComponent(entityId: entity) + enableBatching(true) + generateBatches() + } else { + enableBatching(false) + } + } + + /// Attaches a streaming component to the loaded entity and enables the + /// geometry streaming system, or shuts it down when turned off. + func setStreaming(_ enabled: Bool, streamingRadius: Float, unloadRadius: Float) { + guard let entity = loadedEntity else { return } + if enabled { + enableStreaming( + entityId: entity, + streamingRadius: streamingRadius, + unloadRadius: unloadRadius, + priority: Constants.streamingPriority + ) + GeometryStreamingSystem.shared.enabled = true + } else { + GeometryStreamingSystem.shared.enabled = false + } + } + } + + // MARK: - Debug Views + + extension GameScene { + /// Toggles the per-entity LOD level colour overlay. + func setLodDebug(_ enabled: Bool) { + setLODLevelDebug(enabled: enabled) + } + + /// Toggles the texture streaming tier colour overlay. + func setStreamingTierDebug(_ enabled: Bool) { + setTextureStreamingTierDebug(enabled: enabled) + } + + /// Selects the renderer debug output. + func setRenderDebugView(_ mode: RenderDebugViewMode) { + if mode == .ssaoBlurred, SSAO.isEnabled() == false { + SSAO.setEnabled(true) + } + renderDebugViewMode = mode + } + + /// Draws (or hides) the octree leaf-node bounds debug overlay. + func setSpatialDebug( + enabled: Bool, + occupiedOnly: Bool, + colorMode: SpatialDebugLeafColorMode + ) { + if enabled { + setOctreeLeafBoundsDebug( + enabled: true, + maxLeafNodeCount: 0, + occupiedOnly: occupiedOnly, + colorMode: colorMode + ) + } else { + disableSpatialDebugVisualization() + } + } + } + + // MARK: - Frame Loop + + extension GameScene { + func update(deltaTime _: Float) { + if gameMode == false { return } + } + + func handleInput() { + if gameMode == false { return } + let input = InputSystem.shared + let camera = findGameCamera() + + moveCameraWithInput( + entityId: camera, + input: ( + w: input.keyState.wPressed, + a: input.keyState.aPressed, + s: input.keyState.sPressed, + d: input.keyState.dPressed, + q: input.keyState.qPressed, + e: input.keyState.ePressed + ), + speed: Constants.cameraMoveSpeed, + deltaTime: Constants.cameraInputDeltaTime + ) + + if input.keyState.rightMousePressed { + if !wasRightMousePressed { + setOrbitOffset(entityId: camera, uTargetOffset: Constants.orbitTargetOffset) + } + var dx = input.mouseDeltaX + var dy = input.mouseDeltaY + if abs(dx) < abs(dy) { dx = 0 } else { dy = 0 } + orbitCameraAround(entityId: camera, uDelta: simd_float2(dx, dy)) + } + wasRightMousePressed = input.keyState.rightMousePressed + } + } +#endif diff --git a/Sources/DemoGame/main.swift b/Sources/DemoGame/main.swift index 67afeb7bc..d1fca54f7 100644 --- a/Sources/DemoGame/main.swift +++ b/Sources/DemoGame/main.swift @@ -1,271 +1,12 @@ // // main.swift // -// -// 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 os(macOS) - import MetalKit - import SwiftUI - import UntoldEngine - - /// GameScene is where you initialize your game and write game-specific logic. - class GameScene { - /// Toggle between Editor-loaded scene (true) and Code-built scene (false). - var useEditorScene: Bool = false - - // Demo assets location + scene file name (adjust as needed). - private let demoAssetsRelativePath = "DemoGameAssets/Assets" - private let sceneFilename = "soccergamedemo.json" - - /// Resolve ~/Desktop/DemoGameAssets/Assets - private func demoAssetsBaseURL() -> URL? { - FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first? - .appendingPathComponent(demoAssetsRelativePath) - } - - /// Resolve the full scene URL if it exists. - private func demoSceneURL() -> URL? { - guard let base = demoAssetsBaseURL() else { return nil } - let url = base.appendingPathComponent(sceneFilename) - return FileManager.default.fileExists(atPath: url.path) ? url : nil - } - - init() { - // - // ----------------------------------------------------- - // Demo Game Tutorial – How it Works - // ----------------------------------------------------- - // - // The Untold Engine is built on an ECS (Entity–Component–System) architecture: - // - // - Entities: These are just IDs (no logic or data by themselves). - // - Components: Small data containers that hold attributes. For example, - // a `BallComponent` might store ball-related properties (speed, position, etc.). - // - Systems: Functions that run every frame and update entities based on the - // components they have. For example, a `ballSystemUpdate` might handle ball physics. - // - // By combining these three, you extend what entities can *do*. - // Think of it as: - // - Entity = the "thing" - // - Component = the "data" - // - System = the "behavior" - // - // ----------------------------------------------------- - // IMPORTANT: Download Game Scene and assets - // ----------------------------------------------------- - // To save you time, we have included preloaded assets you can use right away: - // - // Models: Soccer stadium, player, ball, and more. - // Animations: Prebuilt running, idle, and other character motions. - // Game Scene: soccergamedemo.json - // - // You can download them Demo Game Assets v1.0 (https://github.com/untoldengine/UntoldEngine-Assets/releases/tag/v1). - // - // - // ----------------------------------------------------- - // Two Ways to Initialize a Scene - // ----------------------------------------------------- - // - // **Option 1 – Load Scene from the Editor:** - // - // 1. Build your scene visually in the Untold Engine Editor and save it (e.g., - // `soccergamedemo.json`). This was done already done for you. - // 2. Point the engine to your asset folder (in this demo: "DemoGameAssets/Assets"). - // 3. Call `playSceneAt(url:)` to load and deserialize the scene. - // 4. Look up specific entities by name and attach extra *custom components* - // (like `BallComponent` or `DribblinComponent`). - // 5. Register your custom systems so they update every frame. - // 6. Hook up input events (WASD movement). - // - // This approach is great when working with designers or when you want to quickly - // assemble a level visually, then extend it with gameplay logic. - // - // **Option 2 – Create by Code:** - // - // If you prefer to build the scene entirely in code, you can do so by setting - // a simple flag (e.g., `useEditorScene = false`). When this flag is false, - // you skip loading the `.json` file and instead: - // - // - Call `createEntity()` for each object in your scene. - // - Assign models with `setEntityMesh`. - // - Apply transforms with `translateBy` / `rotateTo`. - // - Add animations with `setEntityAnimations`. - // - Give entities names with `setEntityName` so they can be looked up later. - // - Enable physics with `setEntityKinetics`. - // - Move the camera with `moveCameraTo`. - // - Adjust global settings like `ambientIntensity`. - // - // This approach is useful when you want *full control in code* or prefer to - // generate scenes procedurally. - // - // ----------------------------------------------------- - // Summary - // ----------------------------------------------------- - // - // Whether you load from the Editor or create everything in code, the workflow - // is the same once the scene exists: - // - // - Entities are just IDs. - // - Components attach data/attributes to those IDs. - // - Systems operate on those components every frame. - // - // Use the Editor for fast prototyping, or code for precise control. - // You can even mix both: load a base scene from the Editor and then add more - // entities or components through code. - // - - // Point the engine to your asset folder (used by both options) - assetBasePath = demoAssetsBaseURL() - - if useEditorScene { - // --- Option 1: Load the scene created with the Editor --- - if let url = demoSceneURL() { - playSceneAt(url: url) - } else { - print("⚠️ Could not find scene file \(sceneFilename). " + - "Expected at Desktop/\(demoAssetsRelativePath). " + - "Falling back to code-built scene.") - // Fallback to code path if the JSON is missing. - buildSceneInCode() - } - } else { - // --- Option 2: Build the exact same scene in code --- - buildSceneInCode() - } - - // Input (WASD) for the demo - InputSystem.shared.registerKeyboardEvents() - - // bypass Post FX effects - bypassPostProcessing = true - - // Disable SSAO - SSAO.setEnabled(false) - SSAO.setQuality(.high) - } - - /// Build the same demo scene procedurally. - private func buildSceneInCode() { - // Stadium (static mesh) - let stadium = createEntity() - setEntityMesh(entityId: stadium, filename: "stadium", withExtension: "usdz") - translateBy(entityId: stadium, position: simd_float3(0.0, 0.0, 0.0)) - - // Player (animated, named for lookup) - let player = createEntity() - setEntityMesh(entityId: player, filename: "redplayer", withExtension: "usdz", flip: false) - setEntityName(entityId: player, name: "player") - rotateTo(entityId: player, angle: 0, axis: simd_float3(0.0, 1.0, 0.0)) - setEntityAnimations(entityId: player, filename: "running", withExtension: "usdz", name: "running") - setEntityAnimations(entityId: player, filename: "idle", withExtension: "usdz", name: "idle") - setEntityKinetics(entityId: player) - - // Ball (named for lookup) - let ball = createEntity() - setEntityMesh(entityId: ball, filename: "ball", withExtension: "usdz") - setEntityName(entityId: ball, name: "ball") - translateBy(entityId: ball, position: simd_float3(0.0, 0.5, 3.0)) - setEntityKinetics(entityId: ball) - - // Camera + lighting - moveCameraTo(entityId: findGameCamera(), 0.0, 3.0, 10.0) - ambientIntensity = 0.4 - - // ----------------------------------------------------- - // Extend behavior by registering custom components - // (attach data to specific entities) - // ----------------------------------------------------- - if let ball = findEntity(name: "ball") { - registerComponent(entityId: ball, componentType: BallComponent.self) - } - - if let player = findEntity(name: "player") { - registerComponent(entityId: player, componentType: DribblinComponent.self) - } - - registerComponent(entityId: findGameCamera(), componentType: CameraFollowComponent.self) - - // ----------------------------------------------------- - // Register systems (run every frame) - // ----------------------------------------------------- - registerCustomSystem(ballSystemUpdate) - registerCustomSystem(dribblingSystemUpdate) - registerCustomSystem(cameraFollowUpdate) - } - - func update(deltaTime _: Float) { - // Skip logic if not in game mode - if gameMode == false { return } - } - - func handleInput() { - // Skip logic if not in game mode - if gameMode == false { return } - - // Handle input here - } - } - - // AppDelegate: Boiler plate code -- Handles everything – Renderer, Metal setup, and GameScene initialization - class AppDelegate: NSObject, NSApplicationDelegate { - var window: NSWindow! - var renderer: UntoldRenderer! - var gameScene: GameScene! - - func applicationDidFinishLaunching(_: Notification) { - print("Launching Untold Engine") - - // Step 1. Create and configure the window - window = NSWindow( - contentRect: NSRect(x: 0, y: 0, width: 1920, height: 1080), - styleMask: [.titled, .closable, .resizable], - backing: .buffered, - defer: false - ) - - window.title = "Untold Engine" - window.center() - - // Step 2. Initialize the renderer and connect metal content - guard let renderer = UntoldRenderer.create() else { - print("Failed to initialize the renderer.") - return - } - - window.contentView = renderer.metalView - - self.renderer = renderer - - // Step 3. Create the game scene and connect callbacks - gameScene = GameScene() - renderer.setupCallbacks( - gameUpdate: { [weak self] deltaTime in self?.gameScene.update(deltaTime: deltaTime) }, - handleInput: { [weak self] in self?.gameScene.handleInput() } - ) - - let hostingView = NSHostingView(rootView: SceneView(renderer: renderer)) - window.contentView = hostingView - - window.makeKeyAndOrderFront(nil) - NSApp.setActivationPolicy(.regular) - NSApp.activate(ignoringOtherApps: true) - } - - func applicationShouldTerminateAfterLastWindowClosed(_: NSApplication) -> Bool { - true - } - } - - // Entry point + import AppKit let app = NSApplication.shared let delegate = AppDelegate() app.delegate = delegate - app.run() #endif diff --git a/Sources/UntoldEngine/ECS/Components.swift b/Sources/UntoldEngine/ECS/Components.swift index 2c5a5a7bf..c9ec06764 100644 --- a/Sources/UntoldEngine/ECS/Components.swift +++ b/Sources/UntoldEngine/ECS/Components.swift @@ -346,7 +346,8 @@ public class LODComponent: Component { /// Check if the desired LOD level has a resident mesh public func isLODResident(_ lodIndex: Int) -> Bool { guard lodIndex >= 0, lodIndex < lodLevels.count else { return false } - return !lodLevels[lodIndex].mesh.isEmpty + let level = lodLevels[lodIndex] + return level.residencyState == .resident && !level.mesh.isEmpty } /// Find the best available fallback LOD (coarser than desired) diff --git a/Sources/UntoldEngine/Mesh/Mesh.swift b/Sources/UntoldEngine/Mesh/Mesh.swift index 3e3c87cee..ebac8f044 100644 --- a/Sources/UntoldEngine/Mesh/Mesh.swift +++ b/Sources/UntoldEngine/Mesh/Mesh.swift @@ -79,7 +79,7 @@ private func orientationTransformForAsset(_ asset: MDLAsset, conversion: Coordin } } -private func composedWorldTransform(for object: MDLObject) -> simd_float4x4 { +func composedWorldTransform(for object: MDLObject) -> simd_float4x4 { var result = simd_float4x4.identity var current: MDLObject? = object @@ -545,6 +545,263 @@ public struct Mesh { return meshGroups } + // MARK: - Progressive Loading Support + + /// Holds MDLAsset and its top-level objects after a CPU-only parse. + /// No Metal buffers are allocated yet — MTKMesh creation happens per-batch in ProgressiveAssetLoader. + struct ProgressiveAssetData: @unchecked Sendable { + let asset: MDLAsset // Retained so MDLMesh objects stay valid during batch processing + let topLevelObjects: [MDLObject] + let textureLoader: TextureLoader + let totalObjectCount: Int + } + + /// Parse a USD/USDZ asset without allocating GPU buffers. + /// + /// Passes `nil` as the `bufferAllocator` so ModelIO stores vertex/index data in CPU + /// heap memory instead of Metal shared buffers. This eliminates the all-at-once GPU + /// memory spike that occurs when loading very large scenes. + /// + /// Call `Mesh.makeMeshes(object:...)` on the returned objects in small batches to + /// create Metal resources gradually over multiple frames. + static func parseAssetAsync( + url: URL, + vertexDescriptor: MDLVertexDescriptor, + device: MTLDevice, + coordinateConversion: CoordinateSystemConversion = .autoDetect + ) async -> ProgressiveAssetData? { + await Task.detached { () -> ProgressiveAssetData? in + guard FileManager.default.fileExists(atPath: url.path) else { + Logger.logError( + message: "[ProgressiveLoader] Asset file not found: \(url.path)", + category: LogCategory.assetLoader.rawValue + ) + return nil + } + + if let attrs = try? FileManager.default.attributesOfItem(atPath: url.path), + let size = attrs[.size] as? UInt64 + { + let mb = Double(size) / (1024 * 1024) + Logger.log( + message: "[ProgressiveLoader] Parsing '\(url.lastPathComponent)' (\(String(format: "%.1f", mb)) MB) — CPU allocator, no GPU pre-allocation", + category: LogCategory.assetLoader.rawValue + ) + } + + // MDLMeshBufferDataAllocator stores all vertex/index data in CPU heap memory + // (NSData-backed MDLMeshBufferData objects). No Metal buffers are allocated yet. + // When MTKMesh(mesh:device:) is called per-batch it copies each mesh's Data + // into a new MTLBuffer — this is what enables staged GPU upload. + // + // nil would not work here: passing nil causes ModelIO to skip buffer + // materialisation entirely, leaving empty buffers that MTKMesh cannot copy + // (MTKModelErrorNoMTLBuffer / error 0). + let cpuAllocator = MDLMeshBufferDataAllocator() + let asset = MDLAsset(url: url, vertexDescriptor: vertexDescriptor, bufferAllocator: cpuAllocator) + + let transform = orientationTransformForAsset(asset, conversion: coordinateConversion) + if transform != matrix_identity_float4x4 { + for object in asset.childObjects(of: MDLObject.self) { + object.transform = MDLTransform( + matrix: simd_mul(transform, object.transform?.matrix ?? .identity) + ) + } + } + + // loadTextures() is intentionally NOT called here. + // + // Texture loading is deferred to first-upload time and driven by the + // asset's `TextureResidencyPolicy` (set by AssetProfiler.classifyPolicy): + // + // - `.eager` policy: `GeometryStreamingSystem.uploadFromCPUEntry` calls + // `ProgressiveAssetLoader.ensureTexturesLoaded` before makeMeshesFromCPUBuffers. + // This calls asset.loadTextures() exactly once, decompressing all embedded + // textures into CPU RAM just before the first mesh upload — avoiding a RAM spike + // at parse time when no GPU work has started yet. + // + // - `.streaming` policy: ensureTexturesLoaded is skipped. TextureLoader's lazy + // hydration path (texelDataWithTopLeftOrigin(atMipLevel:0, create:true)) loads + // each texture individually on demand during Material.init, so the full-asset + // decompression spike never occurs. Source references (baseColorMDLTexture / + // baseColorURL) are populated in the Material so TextureStreamingSystem can + // upgrade resolution tiers by camera distance later. + + let textureLoader = TextureLoader(device: device) + // childObjects(of: MDLMesh.self) returns only the actual geometry leaves at + // every level of the hierarchy — no intermediate transform groups. + // Using MDLObject.self instead would return ALL nodes (groups + meshes), + // causing tick() to process the root group first (which recursively uploads + // the entire asset in one shot) and then fail on the individual mesh items + // whose CPU buffers were already cleared. + let objects = Array(asset.childObjects(of: MDLMesh.self)) + + Logger.log( + message: "[ProgressiveLoader] '\(url.lastPathComponent)': \(objects.count) mesh leaves queued for progressive registration", + category: LogCategory.assetLoader.rawValue + ) + + return ProgressiveAssetData( + asset: asset, + topLevelObjects: objects, + textureLoader: textureLoader, + totalObjectCount: objects.count + ) + }.value + } + + /// Copy all vertex/index buffers from a CPU-backed MDLMesh (MDLMeshBufferDataAllocator) + /// to fresh Metal-backed buffers using a per-mesh MTKMeshBufferAllocator. + /// + /// `MTKMesh(mesh:device:)` requires MTKMeshBuffer-backed buffers and cannot copy from + /// MDLMeshBufferData (CPU-heap) buffers — this is what causes MTKModelErrorNoMTLBuffer. + /// This function performs the explicit CPU→GPU copy so each batch item only allocates + /// Metal memory for its own mesh data, keeping GPU allocation incremental. + private static func copyBuffersToMetal( + _ mdlMesh: MDLMesh, + device: MTLDevice + ) -> MDLMesh? { + let mtkAllocator = MTKMeshBufferAllocator(device: device) + + // Copy vertex buffers + let newVertexBuffers: [MDLMeshBuffer] = mdlMesh.vertexBuffers.map { srcBuf in + let dst = mtkAllocator.newBuffer(srcBuf.length, type: .vertex) + let srcMap = srcBuf.map() + let dstMap = dst.map() + memcpy(dstMap.bytes, srcMap.bytes, srcBuf.length) + return dst + } + + // Copy index buffers and rebuild submeshes + var newSubmeshes: [MDLSubmesh] = [] + for case let sub as MDLSubmesh in mdlMesh.submeshes ?? NSMutableArray() { + let srcIdx = sub.indexBuffer + let dstIdx = mtkAllocator.newBuffer(srcIdx.length, type: .index) + let srcMap = srcIdx.map() + let dstMap = dstIdx.map() + memcpy(dstMap.bytes, srcMap.bytes, srcIdx.length) + let newSub = MDLSubmesh( + name: sub.name, + indexBuffer: dstIdx, + indexCount: sub.indexCount, + indexType: sub.indexType, + geometryType: sub.geometryType, + material: sub.material + ) + newSubmeshes.append(newSub) + } + + let mtkMesh = MDLMesh( + vertexBuffers: newVertexBuffers, + vertexCount: mdlMesh.vertexCount, + descriptor: mdlMesh.vertexDescriptor, + submeshes: newSubmeshes + ) + // Copy name and local transform from original — no world-transform baking. + // localSpace and worldSpace are assigned directly after Mesh.init to avoid + // the MDLTransform(matrix:) decompose/recompose round-trip that can corrupt scale. + mtkMesh.name = mdlMesh.parent?.name ?? mdlMesh.name + mtkMesh.transform = mdlMesh.transform + return mtkMesh + } + + /// Progressive-loading variant of makeMeshes for CPU-parsed assets. + /// + /// For each MDLMesh in the object hierarchy: + /// 1. Computes tangent basis on CPU (addOrthTanBasis) — safe with MDLMeshBufferDataAllocator. + /// 2. Copies vertex/index buffers from CPU heap to new Metal-backed (MTK) buffers. + /// 3. Creates Mesh via normal Mesh.init — MTKMesh creation succeeds on MTK-backed buffers. + /// + /// Called from ProgressiveAssetLoader.tick() instead of makeMeshes() because + /// MTKMesh(mesh:device:) requires MTKMeshBuffer-backed buffers and produces + /// MTKModelErrorNoMTLBuffer (error 0) when given MDLMeshBufferData CPU buffers. + static func makeMeshesFromCPUBuffers( + object: MDLObject, + vertexDescriptor: MDLVertexDescriptor, + textureLoader: TextureLoader, + device: MTLDevice, + flip: Bool + ) -> [Mesh] { + var meshes = [Mesh]() + + if let mdlMesh = object as? MDLMesh { + // Step 1: compute tangent basis in CPU memory + if hasTextureCoordinates(mesh: mdlMesh) { + mdlMesh.addOrthTanBasis( + forTextureCoordinateAttributeNamed: MDLVertexAttributeTextureCoordinate, + normalAttributeNamed: MDLVertexAttributeNormal, + tangentAttributeNamed: MDLVertexAttributeTangent + ) + } + + // Step 2: capture transforms directly from original (has full parent chain). + // We read these now and assign them directly after Mesh.init to avoid the + // MDLTransform(matrix:) decompose/recompose round-trip that can corrupt scale. + let localTransform = mdlMesh.transform?.matrix ?? .identity + let worldTransform = composedWorldTransform(for: mdlMesh) + let assetName = mdlMesh.parent?.name ?? mdlMesh.name + + // Step 3: copy CPU buffers to Metal-backed buffers + if let mtkBacked = copyBuffersToMetal(mdlMesh, device: device) { + // Step 4: Mesh.init — addOrthTanBasis is a no-op (done above), + // vertexDescriptor= works on MTK-backed buffers, MTKMesh succeeds. + if var mesh = Mesh(modelIOMesh: mtkBacked, vertexDescriptor: vertexDescriptor, textureLoader: textureLoader, device: device, flip: flip) { + // Directly assign the correct transforms from the original MDLMesh. + // This matches exactly what Mesh.init would produce with the original + // (MTKMeshBufferAllocator-backed) mesh that has a full parent chain. + mesh.localSpace = localTransform + mesh.worldSpace = worldTransform + meshes.append(mesh) + } else { + Logger.logError( + message: "[ProgressiveLoader] MTKMesh creation failed for '\(assetName)' even after CPU to Metal buffer copy.", + category: LogCategory.assetLoader.rawValue + ) + } + } + } + + if object.conforms(to: MDLObjectContainerComponent.self) { + for child in object.children.objects { + meshes.append(contentsOf: makeMeshesFromCPUBuffers( + object: child, + vertexDescriptor: vertexDescriptor, + textureLoader: textureLoader, + device: device, + flip: flip + )) + } + } + + return meshes + } + + /// Free CPU-heap vertex buffers for every MDLMesh in the hierarchy. + /// + /// Called immediately after `makeMeshesFromCPUBuffers` so that `MDLMeshBufferData` + /// objects (CPU heap, potentially hundreds of MB for large USDZ files) are released + /// as soon as their data has been copied to Metal. Without this, the full file remains + /// in CPU RAM for the entire duration of progressive loading, doubling peak memory and + /// causing OOM kills on Vision Pro. + /// + /// Index buffers (`MDLSubmesh.indexBuffer`) are read-only in ModelIO and cannot be + /// cleared here, but vertex data is the dominant cost so this still reduces peak + /// CPU memory by ~80–90%. + /// + /// Note: after this call the object's vertex buffers are empty. Do not call + /// `makeMeshesFromCPUBuffers` on the same object again — use the Metal-backed + /// `Mesh` structs that were returned by the previous call instead. + static func clearCPUBuffers(object: MDLObject) { + if let mesh = object as? MDLMesh { + mesh.vertexBuffers = [] + } + if object.conforms(to: MDLObjectContainerComponent.self) { + for child in object.children.objects { + clearCPUBuffers(object: child) + } + } + } + static func loadMeshWithName(name: String, url: URL, vertexDescriptor: MDLVertexDescriptor, device: MTLDevice, coordinateConversion: CoordinateSystemConversion = .autoDetect) -> [Mesh] { let bufferAllocator = MTKMeshBufferAllocator(device: device) let asset = MDLAsset(url: url, vertexDescriptor: vertexDescriptor, bufferAllocator: bufferAllocator) @@ -964,10 +1221,22 @@ private func textureLikelyHasAlphaChannel(_ texture: MTLTexture?) -> Bool { return textureFormatHasAlpha(texture.pixelFormat) } +/// Enable per-operation cache logging in `TextureLoader`. +/// +/// When `true` every GPU texture load — hit or miss — emits a `[TextureCache]` log line +/// with the cache key, key source, MDLTexture object identity, texture name, and map type. +/// Set this flag before loading assets to diagnose cache-key collisions. +/// +/// **This flag is temporary and intended for diagnostic use only.** +/// Turn it off in production builds — logging one line per submesh texture per upload +/// can be very noisy for large scenes. +public nonisolated(unsafe) var textureCacheLoggingEnabled: Bool = false + /// Tracks whether a texture slot is at full or capped resolution -public enum TextureStreamingLevel { - case full // Original resolution - case capped // Downsampled to fit TextureLoader.maxTextureDimension +public enum TextureStreamingLevel: Equatable { + case full // Native source resolution (nil cap — no downsampling) + case capped // Downsampled to medium tier (e.g. maxTextureDimension = 1024 px) + case minimum // Downsampled to minimum tier (e.g. minimumTextureDimension = 256 px) } public struct Material { @@ -1184,12 +1453,18 @@ public struct Material { final class TextureLoader { /// Initial texture cap applied during material import. - /// Runtime texture streaming can upgrade/downgrade from this bootstrap level. + /// + /// This value is intentionally aligned with `TextureStreamingSystem.platformDefaultMinimumTextureDimension` + /// so that all entities start at the streaming system's minimum tier. The streaming system then + /// only ever upgrades textures (toward medium or full) as the camera approaches — it never + /// issues an immediate downgrade on freshly-loaded entities. Keeping bootstrap = minimum tier + /// avoids the visual artifact where a far entity loads at a higher resolution and is then + /// immediately degraded by the streaming system before the user moves the camera. static let defaultMaxTextureDimension: Int = { #if os(visionOS) - 384 + 192 // matches TextureStreamingSystem.platformDefaultMinimumTextureDimension on visionOS #else - 512 + 256 // matches TextureStreamingSystem.platformDefaultMinimumTextureDimension on macOS/iOS #endif }() @@ -1198,6 +1473,10 @@ final class TextureLoader { private var textureCache: [TextureCacheKey: MTLTexture] = [:] private var sourceDimensionsCache: [TextureCacheKey: simd_int2] = [:] + /// Serializes all cache reads and writes when the same TextureLoader instance + /// is shared across concurrent entity uploads (OOC path). + private let stateLock = NSLock() + /// Tracks unique textures loaded (not cache hits) for summary logging private var loadedTextureCount: Int = 0 private var loadedTextureBytes: Int = 0 @@ -1316,28 +1595,84 @@ final class TextureLoader { return tex.makeTextureView(pixelFormat: target) ?? tex } + /// Parse a ModelIO USDZ bracket-notation string into a USDZ file URL and the + /// inner texture path within the package. + /// + /// ModelIO returns embedded texture paths in the form: + /// `"file:///path/to/scene.usdz[0/texture_base_color.png]"` + /// + /// This method extracts the host-file URL (`file:///path/to/scene.usdz`) and the + /// inner path (`0/texture_base_color.png`). Returns `nil` if the string is not in + /// bracket-notation format, has an empty inner path, or the host URL is not a file URL. + /// + /// Exposed as `internal` so it can be unit-tested from `UntoldEngineTests`. + static func parseUSDZBracketPath(from str: String) -> (usdzURL: URL, innerPath: String)? { + guard let openBracket = str.lastIndex(of: "["), + let closeBracket = str.lastIndex(of: "]"), + openBracket < closeBracket + else { return nil } + + let usdzPathStr = String(str[str.startIndex ..< openBracket]) + let innerPath = String(str[str.index(after: openBracket) ..< closeBracket]) + .trimmingCharacters(in: .whitespacesAndNewlines) + .replacingOccurrences(of: "\\", with: "/") + + guard !innerPath.isEmpty, + let usdzURL = URL(string: usdzPathStr), + usdzURL.isFileURL + else { return nil } + + return (usdzURL, innerPath) + } + /// Build a stable identity URL for USDZ-embedded textures. - /// Uses the package-relative path when available (from ModelIO stringValue), - /// so identical embedded textures shared across meshes get the same identity. - private func embeddedTextureURL(from property: MDLMaterialProperty, fallbackTextureName: String) -> URL? { + /// + /// **Priority 1 — bracket-notation path:** When `property.stringValue` contains a parseable + /// bracket-notation path (e.g. `"file:///scene.usdz[0/texture.png]"`), returns + /// `usdz-embedded://`. This is the most specific identifier available. + /// + /// **Priority 2 — texture name + asset scope:** When no bracket path is found and + /// `textureName` is non-empty, returns `usdz-embedded:///`. + /// `assetScope` is the USDZ filename (from `assetBasePath`) or "embedded" if unknown. + /// This is stable across reload cycles and consistent for all entities that load the same + /// USDZ file, which is required for BatchingSystem.getMaterialHash to group them together. + /// + /// Using just `textureName` (without `assetScope`) was the original bug: it produced the + /// same URL for any two USDZ files that happened to share a texture name, causing cache + /// poisoning across assets. Scoping to the USDZ filename eliminates cross-asset collisions + /// while preserving cross-entity consistency within the same file. + private func embeddedTextureURL(from property: MDLMaterialProperty, textureName: String = "") -> URL? { + // Priority 1: bracket-notation path → most specific if let propertyString = property.stringValue, - let openBracket = propertyString.lastIndex(of: "["), - let closeBracket = propertyString.lastIndex(of: "]"), - openBracket < closeBracket + let parsed = TextureLoader.parseUSDZBracketPath(from: propertyString) { - let start = propertyString.index(after: openBracket) - let rawInnerPath = String(propertyString[start ..< closeBracket]) - .trimmingCharacters(in: .whitespacesAndNewlines) - .replacingOccurrences(of: "\\", with: "/") - - if !rawInnerPath.isEmpty { - let encodedPath = rawInnerPath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? rawInnerPath - return URL(string: "usdz-embedded://\(encodedPath)") - } + let encodedPath = parsed.innerPath.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? parsed.innerPath + return URL(string: "usdz-embedded://\(encodedPath)") } - let encodedFallback = fallbackTextureName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? fallbackTextureName - return URL(string: "usdz-embedded://\(encodedFallback)") + // Priority 2: stable name-based URL scoped to the USDZ file. + // Consistent across all entities from the same USDZ, required for batching. + guard !textureName.isEmpty else { return nil } + let scope = (assetBasePath?.lastPathComponent) + .flatMap { $0.isEmpty ? nil : $0 } ?? "embedded" + let encodedScope = scope.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? scope + let encodedName = textureName.addingPercentEncoding(withAllowedCharacters: .urlPathAllowed) ?? textureName + return URL(string: "usdz-embedded://\(encodedScope)/\(encodedName)") + } + + /// Build a unique URL for an MDLTexture keyed by its object pointer. + /// + /// Used as the GPU texture cache key when bracket-notation path is absent. + /// The MDLAsset is kept alive in `ProgressiveAssetLoader.rootAssetRefs` for the + /// entire lifetime of the out-of-core asset, so the pointer is stable across + /// GPU-resource eviction/reload cycles. + /// + /// Two MDLTexture objects with the SAME pointer are physically the same texture + /// object and should share a GPU texture — this key correctly deduplicates them. + /// Two MDLTexture objects with DIFFERENT pointers are different textures — this key + /// correctly gives them independent cache entries even if they carry the same name. + private static func objectIdentityURL(for mdlTex: MDLTexture) -> URL { + URL(string: "mdl-obj-\(UInt(bitPattern: ObjectIdentifier(mdlTex)))")! } func loadTexture(from property: MDLMaterialProperty?, @@ -1348,6 +1683,8 @@ final class TextureLoader { mapType: String) -> MTLTexture? { guard let property else { return nil } + stateLock.lock() + defer { stateLock.unlock() } let options: [MTKTextureLoader.Option: Any] = [ .textureUsage: MTLTextureUsage([.shaderRead, .pixelFormatView]).rawValue, @@ -1362,11 +1699,80 @@ final class TextureLoader { let mdlTex = sampler.texture { let textureName = mdlTex.name.isEmpty ? "embedded_\(mapType.replacingOccurrences(of: " ", with: "_"))" : mdlTex.name - let stableEmbeddedURL = embeddedTextureURL(from: property, fallbackTextureName: textureName) - let stableIdentity = stableEmbeddedURL?.absoluteString ?? "usdz-embedded://\(textureName)" - let cacheKey = TextureCacheKey(id: stableIdentity, isSRGB: isSRGB) + + // Determine whether a bracket-notation path is available for this texture. + // Bracket paths (e.g. "usdz-embedded://0/floor_albedo.png") are unique per + // physical embedded file and are safe to use as both a cache key and a stable URL. + // + // When bracket notation is absent, `embeddedTextureURL` falls back to a + // name-scoped URL (Priority 2: "usdz-embedded://GameData/embedded_Basecolor_map"). + // That URL is appropriate for `outputURL` / BatchingSystem.getMaterialHash (cross- + // entity consistency), but is UNSAFE as a GPU texture cache key: two genuinely + // different materials whose MDLTextures both lack names (or share the same name) + // produce the same string and collide in the cache, causing the second entity to + // receive the first entity's GPU texture. + // + // Fix: split cache key from stable URL. + // - cacheKeyURL uses object identity (mdl-obj-) when no bracket path is found. + // Two MDLTexture objects with different pointers never collide, regardless of name. + // Two references to the SAME MDLTexture object (genuinely shared texture) share + // the same pointer → correctly share the cached GPU texture. + // - uniqueURL (stored in outputURL) keeps the stable name-based or bracket URL so + // BatchingSystem.getMaterialHash continues to group shared materials correctly. + let hasBracketPath: Bool = { + guard let str = property.stringValue else { return false } + return TextureLoader.parseUSDZBracketPath(from: str) != nil + }() + + // stableURL: used for outputURL / material hashing (stable, cross-entity consistent). + let stableURL = embeddedTextureURL(from: property, textureName: textureName) + + // cacheKeyURL: used exclusively as the GPU texture cache key. + // Bracket path when available (unique per file); object identity otherwise. + let cacheKeyURL: URL = hasBracketPath + ? (stableURL ?? TextureLoader.objectIdentityURL(for: mdlTex)) + : TextureLoader.objectIdentityURL(for: mdlTex) + + // uniqueURL stored in outputURL / baseColorURL etc. + // + // When a bracket path is available it is unique per embedded file and safe for + // both caching and batch-material hashing — use it directly. + // + // When bracket notation is absent, the name-based stableURL collapses every + // unnamed texture from the same USDZ to the same string + // (e.g. "usdz-embedded://scene.usdz/embedded_Basecolor_map"). + // BatchingSystem.normalizeTextureURL then strips the asset-scope host, leaving + // "usdz-embedded://embedded_Basecolor_map" for ALL entities, so they all hash + // to the same BatchBuildKey and are grouped into one batch whose representative + // material is only the first entity's GPU texture — showing the wrong texture + // on every other entity. + // + // Fix: mirror cacheKeyURL — use object identity when no bracket path is found. + // Same MDLTexture pointer ↔ same physical texture ↔ share GPU cache + batch. + // Different pointers ↔ different textures ↔ separate batch groups. + let uniqueURL: URL = hasBracketPath + ? (stableURL ?? TextureLoader.objectIdentityURL(for: mdlTex)) + : TextureLoader.objectIdentityURL(for: mdlTex) + + let cacheKey = TextureCacheKey(id: cacheKeyURL.absoluteString, isSRGB: isSRGB) + + // Diagnostic logging [temporary] — controlled by textureCacheLoggingEnabled. + // Set `textureCacheLoggingEnabled = true` before loading to trace cache hits/misses. + if textureCacheLoggingEnabled { + let keySource: String + if hasBracketPath { + keySource = "bracket" + } else if mdlTex.name.isEmpty { + keySource = "obj-identity(unnamed)" + } else { + keySource = "obj-identity(named-no-bracket)" + } + let isHit = textureCache[cacheKey] != nil + Logger.log(message: "[TextureCache] \(isHit ? "HIT " : "MISS") key=\(cacheKeyURL.absoluteString) source=\(keySource) mdlTex=0x\(String(UInt(bitPattern: ObjectIdentifier(mdlTex)), radix: 16)) name='\(textureName)' map=\(mapType) isSRGB=\(isSRGB)") + } + if let cached = textureCache[cacheKey] { - outputURL = stableEmbeddedURL ?? URL(string: cacheKey.id) + outputURL = uniqueURL outputMDLTexture = mdlTex outputSourceDimensions = sourceDimensionsCache[cacheKey] return cached @@ -1376,9 +1782,7 @@ final class TextureLoader { let fullTex = try mtkLoader.newTexture(texture: mdlTex, options: options) let tex = downsampleIfNeeded(fullTex) - outputURL = stableEmbeddedURL - - // Store the MDLTexture reference for later use (e.g., texture extraction) + outputURL = uniqueURL outputMDLTexture = mdlTex return cacheAndRecordTexture( @@ -1389,7 +1793,28 @@ final class TextureLoader { nameForLog: textureName, outputSourceDimensions: &outputSourceDimensions ) - } catch { + } catch let initialError { + // mtkLoader.newTexture(texture:) failed — this happens when loadTextures() was + // skipped for large assets and the MDLTexture has no pixel data yet. + // Ask the MDLTexture to lazily fetch its own data from the USDZ package and retry. + // This loads only this one texture, not the entire asset. + Logger.log(message: "[TextureLoad] MDL path failed for '\(textureName)' — retrying with lazy hydration (\(initialError.localizedDescription))") + if mdlTex.texelDataWithTopLeftOrigin(atMipLevel: 0, create: true) != nil, + let retryTex = try? mtkLoader.newTexture(texture: mdlTex, options: options) + { + let tex = downsampleIfNeeded(retryTex) + outputURL = uniqueURL + outputMDLTexture = mdlTex + return cacheAndRecordTexture( + loadedTexture: tex, + sourceTexture: retryTex, + cacheKey: cacheKey, + isSRGB: isSRGB, + nameForLog: textureName, + outputSourceDimensions: &outputSourceDimensions + ) + } + Logger.log(message: "[TextureLoad] Lazy hydration also failed for '\(textureName)' — falling through to URL paths") handleError(.textureFailedLoading) } } @@ -1418,6 +1843,32 @@ final class TextureLoader { // String (relative) -> try to resolve against the model's base path if you keep it if let str = property.stringValue, !str.isEmpty { + // 0) USDZ embedded texture: "file:///path/to.usdz[0/texture.png]" + // sampler.texture was nil (loadTextures() was skipped) so path 1 never fired. + // Build a package URL (slash-separated) and try MTKTextureLoader directly. + if let parsed = TextureLoader.parseUSDZBracketPath(from: str) { + let packageURL = parsed.usdzURL.appendingPathComponent(parsed.innerPath) + let cacheKey = TextureCacheKey(id: packageURL.absoluteString, isSRGB: isSRGB) + if let cached = textureCache[cacheKey] { + outputURL = packageURL + outputSourceDimensions = sourceDimensionsCache[cacheKey] + return cached + } + if let fullTex = try? mtkLoader.newTexture(URL: packageURL, options: options) { + let tex = downsampleIfNeeded(fullTex) + outputURL = packageURL + return cacheAndRecordTexture( + loadedTexture: tex, + sourceTexture: fullTex, + cacheKey: cacheKey, + isSRGB: isSRGB, + nameForLog: parsed.innerPath, + outputSourceDimensions: &outputSourceDimensions + ) + } + Logger.log(message: "[TextureLoad] USDZ package URL failed for '\(parsed.innerPath)' — falling through to remaining paths") + } + // 1) Try as-is (absolute or already-resolved) if let url = URL(string: str), url.isFileURL { let cacheKey = TextureCacheKey(id: url.standardizedFileURL.path, isSRGB: isSRGB) diff --git a/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift b/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift index 950234dd8..cb39d9993 100644 --- a/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift +++ b/Sources/UntoldEngine/Profiling/EngineStatsMonitor.swift @@ -233,9 +233,15 @@ public final class EngineStatsMonitor: @unchecked Sendable { switch profileToLog { case .compact: - Logger.log(message: "[EngineStats] " + formatEngineStats(snapshotToLog, style: .compact)) + Logger.log( + message: "[EngineStats] " + formatEngineStats(snapshotToLog, style: .compact), + category: LogCategory.engineStats.rawValue + ) case .verbose: - Logger.log(message: "[EngineStats]\n" + formatEngineStats(snapshotToLog, style: .expanded)) + Logger.log( + message: "[EngineStats]\n" + formatEngineStats(snapshotToLog, style: .expanded), + category: LogCategory.engineStats.rawValue + ) } #endif } diff --git a/Sources/UntoldEngine/Profiling/README.md b/Sources/UntoldEngine/Profiling/README.md deleted file mode 100644 index f5d161222..000000000 --- a/Sources/UntoldEngine/Profiling/README.md +++ /dev/null @@ -1,94 +0,0 @@ -# UntoldEngine Profiling Module - -A lightweight, cross-platform (macOS, iOS, visionOS) benchmarking and metrics system for UntoldEngine. - -## Features - -- **CPU Frame Time Measurement**: Per-frame CPU duration tracking with rolling statistics (mean, p95, p99) -- **GPU Duration Measurement**: Command buffer GPU time tracking using `MTLCommandBuffer.gpuStartTime/gpuEndTime` -- **Signpost Integration**: Named signpost scopes for Instruments profiling -- **Low Overhead**: Disabled by default, minimal cost when enabled -- **Thread-Safe**: GPU metrics collection safe across completion handler threads -- **No GPU Stalls**: Uses completion handlers, never calls `waitUntilCompleted()` - -## Usage - -### Enabling Metrics - -Metrics are disabled by default. Enable them in one of two ways: - -#### 1. Runtime Flag (in code) -```swift -enableEngineMetrics = true -``` - -#### 2. Environment Variable -```bash -export UNTOLD_METRICS=1 -./YourApp -``` - -### Retrieving Metrics - -```swift -let snapshot = EngineProfiler.shared.snapshot() - -print("CPU Frame Mean: \(snapshot.cpuFrame.meanMs) ms") -print("CPU Frame P95: \(snapshot.cpuFrame.p95Ms) ms") -print("CPU Frame P99: \(snapshot.cpuFrame.p99Ms) ms") - -print("GPU Mean: \(snapshot.gpuCommandBuffer.meanMs) ms") -print("GPU P95: \(snapshot.gpuCommandBuffer.p95Ms) ms") -print("GPU P99: \(snapshot.gpuCommandBuffer.p99Ms) ms") -``` - -### Debug Logging (DEBUG builds only) - -```swift -#if DEBUG -let metricsLogger = MetricsDebugLogger() - -// Call this once per frame (e.g., in your render loop) -metricsLogger.logIfNeeded() // Prints stats every ~1 second -#endif -``` - -### Instruments Integration - -When metrics are enabled, the following signpost scopes are emitted: - -- **Frame**: Entire frame boundary (begin to end) -- **Update**: Game/physics update phase -- **RenderPrep**: Culling, gaussian, and bitonic sort -- **Encode**: Render graph execution -- **Submit**: Command buffer commit - -To view in Instruments: -1. Open Instruments -2. Select "Points of Interest" template -3. Filter by subsystem: `com.untoldengine.profiling` -4. Run your app with metrics enabled - -## Architecture - -- **FrameMetricsCollector**: Ring buffer (2000 samples) for CPU frame times -- **CommandBufferMetricsCollector**: Thread-safe ring buffer for GPU durations -- **EngineSignposts**: os.signpost integration for Instruments -- **EngineProfiler**: Single front-door API -- **MetricsSnapshot**: Data structures for retrieving statistics - -## Integration Points - -The profiling system is automatically integrated into: - -- `UntoldEngine.swift` (`runFrame`): Frame and Update scopes -- `RenderingSystem.swift` (`UpdateRenderingSystem`): RenderPrep, Encode scopes, GPU metrics -- `UntoldEngineXR.swift` (`executeXRSystemPass`): XR-specific scoping and GPU metrics -- `UntoldEngineAR.swift` (`draw`): AR-specific scoping and GPU metrics - -## Notes - -- Percentiles (p95, p99) are computed only when `snapshot()` is called, not per-frame -- GPU times can be zero or invalid on some systems; these samples are automatically dropped -- The ring buffer holds up to 2000 samples (approximately 33 seconds at 60 FPS) -- When disabled, all profiler calls return immediately with near-zero overhead diff --git a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift index 1fc2506f5..dacab6239 100644 --- a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift +++ b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipeLines.swift @@ -591,6 +591,20 @@ public func InitOutputTransformPipeline() -> RenderPipeline? { ) } +public func InitDebugPipeline() -> RenderPipeline? { + CreatePipeline( + vertexShader: "vertexDebugShader", + fragmentShader: "fragmentDebugShader", + vertexDescriptor: createPostProcessVertexDescriptor(), + colorFormats: [wf.lookOutput], + // Look/debug passes use postProcessRenderPassDescriptor, which carries depthMap. + // Match that attachment format even though we do not depth-test/write here. + depthFormat: renderInfo.depthPixelFormat, + depthEnabled: false, + name: "Debug Pipeline" + ) +} + public func InitTransparencyPipeline() -> RenderPipeline? { CreatePipeline( vertexShader: "vertexModelShader", @@ -648,6 +662,7 @@ public func DefaultPipeLines() -> [(RenderPipelineType, RenderPipelineInitBlock) (.spatialDebug, InitSpatialDebugPipeline), (.look, InitLookPipeline), (.outputTransform, InitOutputTransformPipeline), + (.debug, InitDebugPipeline), (.transparency, InitTransparencyPipeline), ] } diff --git a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipelineType.swift b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipelineType.swift index 48d545774..80f4f4b5f 100644 --- a/Sources/UntoldEngine/Renderer/Pipelines/RenderPipelineType.swift +++ b/Sources/UntoldEngine/Renderer/Pipelines/RenderPipelineType.swift @@ -50,4 +50,5 @@ public extension RenderPipelineType { static let look: RenderPipelineType = "look" static let outputTransform: RenderPipelineType = "outputTransform" static let transparency: RenderPipelineType = "transparency" + static let debug: RenderPipelineType = "debug" } diff --git a/Sources/UntoldEngine/Renderer/RenderPasses.swift b/Sources/UntoldEngine/Renderer/RenderPasses.swift index 2c3a8a8b4..1450bd67b 100644 --- a/Sources/UntoldEngine/Renderer/RenderPasses.swift +++ b/Sources/UntoldEngine/Renderer/RenderPasses.swift @@ -27,6 +27,15 @@ public enum RenderPasses { simd_float3(0.0, 1.0, 1.0), // LOD5+ = cyan (palette wraps) ] + /// Streaming tier debug colors. + /// Blue = full resolution, Orange = medium (capped), Red = minimum, Yellow = in-flight. + private static let streamingTierDebugColors: [simd_float3] = [ + simd_float3(0.1, 0.4, 1.0), // [0] full = blue + simd_float3(1.0, 0.55, 0.0), // [1] capped = orange + simd_float3(1.0, 0.2, 0.1), // [2] minimum = red + simd_float3(1.0, 0.85, 0.0), // [3] in-flight = yellow + ] + private struct SpatialDebugColorKey: Hashable { let r: UInt32 let g: UInt32 @@ -215,7 +224,7 @@ public enum RenderPasses { @inline(__always) private static func applyLODDebugColorOverride( entityId: EntityID? = nil, - batchKey: String? = nil, + batchGroup: BatchGroup? = nil, materialParameters: inout MaterialParametersUniform ) { guard SpatialDebugVisualization.shared.colorRenderablesByLOD else { return } @@ -224,9 +233,12 @@ public enum RenderPasses { if let entityId, let lod = scene.get(component: LODComponent.self, for: entityId) { + // Non-batched path: read the live currentLOD directly. lodIndex = lod.currentLOD - } else if let batchKey { - lodIndex = extractLODIndex(from: batchKey) + } else if let batchGroup, batchGroup.isLODBatch { + // Batched path: only color batches that actually contain LOD entities. + // Non-LOD batches also have _LOD0 in their key but should not be tinted. + lodIndex = extractLODIndex(from: batchGroup.batchKey) } else { lodIndex = nil } @@ -238,6 +250,45 @@ public enum RenderPasses { materialParameters.hasTexture.x = 0 } + @inline(__always) + private static func applyStreamingTierDebugColorOverride( + entityId: EntityID? = nil, + batchMaterial: Material? = nil, + materialParameters: inout MaterialParametersUniform + ) { + guard SpatialDebugVisualization.shared.colorRenderablesByStreamingTier else { return } + + let streamingLevel: TextureStreamingLevel + let inFlight: Bool + + if let entityId { + guard let render = scene.get(component: RenderComponent.self, for: entityId), + let material = render.mesh.first?.submeshes.first?.material + else { return } + streamingLevel = material.baseColorStreamingLevel + inFlight = TextureStreamingSystem.shared.isStreaming(entityId: entityId) + } else if let batchMaterial { + streamingLevel = batchMaterial.baseColorStreamingLevel + inFlight = false + } else { + return + } + + let color: simd_float3 + if inFlight { + color = streamingTierDebugColors[3] + } else { + switch streamingLevel { + case .full: color = streamingTierDebugColors[0] + case .capped: color = streamingTierDebugColors[1] + case .minimum: color = streamingTierDebugColors[2] + } + } + + materialParameters.baseColor = simd_float4(color.x, color.y, color.z, materialParameters.baseColor.w) + materialParameters.hasTexture.x = 0 + } + public static let gridExecution: RenderPassExecution = { commandBuffer in guard let gridPipeline = PipelineManager.shared.renderPipelinesByType[.grid] else { handleError(.pipelineStateNulled, "gridPipeline is nil") @@ -1001,6 +1052,7 @@ public enum RenderPasses { 0 ) applyLODDebugColorOverride(entityId: entityId, materialParameters: &materialParameters) + applyStreamingTierDebugColorOverride(entityId: entityId, materialParameters: &materialParameters) renderEncoder.setFragmentBytes( &materialParameters, length: MemoryLayout.stride, @@ -1215,9 +1267,10 @@ public enum RenderPasses { 0 ) applyLODDebugColorOverride( - batchKey: batchGroup.batchKey, + batchGroup: batchGroup, materialParameters: &materialParameters ) + applyStreamingTierDebugColorOverride(batchMaterial: material, materialParameters: &materialParameters) renderEncoder.setFragmentBytes( &materialParameters, @@ -2456,6 +2509,7 @@ public enum RenderPasses { 0 ) applyLODDebugColorOverride(entityId: entityId, materialParameters: &materialParameters) + applyStreamingTierDebugColorOverride(entityId: entityId, materialParameters: &materialParameters) renderEncoder.setFragmentBytes( &materialParameters, diff --git a/Sources/UntoldEngine/Renderer/UntoldEngine.swift b/Sources/UntoldEngine/Renderer/UntoldEngine.swift index 08f669f31..0720fa83e 100644 --- a/Sources/UntoldEngine/Renderer/UntoldEngine.swift +++ b/Sources/UntoldEngine/Renderer/UntoldEngine.swift @@ -419,6 +419,10 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { SystemEventBus.shared.flushEvents() // 5. Batching incremental update (consumes LOD change events) + // Note: ProgressiveAssetLoader.shared.tick() is called before runFrame() in draw() + // (non-XR) or via DispatchQueue.main.async in UntoldEngineXR.renderNewFrame() (XR). + // It is NOT called here because runFrame() is invoked from the visionOS compositor + // render thread in XR, which violates tick()'s main-thread precondition. #if ENGINE_STATS_ENABLED let batchingTickStart = CACurrentMediaTime() #endif @@ -485,6 +489,11 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { initSizeableResources() pendingResize = false } + // Tick the progressive loader here (main thread, before runFrame) so newly + // registered entities are picked up by BatchingSystem in the same frame. + // In XR, UntoldEngineXR.renderNewFrame() dispatches this to the main thread + // separately, since its runLoop() runs on the compositor render thread. + ProgressiveAssetLoader.shared.tick() _ = runFrame( beforeRender: { [weak self] in self?.delegate?.willDraw(in: view) }, render: { [weak self] in @@ -569,6 +578,30 @@ public class UntoldRenderer: NSObject, MTKViewDelegate { runFrame(render: render, statsLifecycle: useExternalStatsLifecycle ? .externalManaged : .internalManaged) } + /// Syncs the physical headset world position into the ECS camera components used by + /// streaming and LOD distance calculations. Must be called before updateXR() each frame. + /// + /// Only the three position fields are written: + /// - LocalTransformComponent.position — so traverseSceneGraph() derives a consistent WorldTransformComponent + /// - CameraComponent.localPosition — read directly by TextureStreamingSystem and LODSystem + /// - WorldTransformComponent.space.columns.3 — read directly by GeometryStreamingSystem and StreamingRegionManager + /// before traverseSceneGraph() runs + /// + /// viewSpace and rotation are intentionally left untouched; renderXR() sets viewSpace + /// per-eye from the device anchor view matrix each frame. + public func setXRCameraWorldPosition(_ worldPosition: simd_float3) { + guard let camera = CameraSystem.shared.activeCamera, + let localTransform = scene.get(component: LocalTransformComponent.self, for: camera), + let cameraComp = scene.get(component: CameraComponent.self, for: camera), + let worldTransform = scene.get(component: WorldTransformComponent.self, for: camera) + else { return } + + localTransform.position = worldPosition + cameraComp.localPosition = worldPosition + // Only the translation column is touched; rotation and scale columns are unchanged. + worldTransform.space.columns.3 = simd_float4(worldPosition, 1.0) + } + /// XR path finalization hook. Call this once after XR submission for the frame. public func finalizeXRStatsAndMonitors(frameStartTime: Double) { #if ENGINE_STATS_ENABLED diff --git a/Sources/UntoldEngine/Shaders/debugShader.metal b/Sources/UntoldEngine/Shaders/debugShader.metal index 9e743d36d..8d7e3a11c 100644 --- a/Sources/UntoldEngine/Shaders/debugShader.metal +++ b/Sources/UntoldEngine/Shaders/debugShader.metal @@ -25,19 +25,30 @@ vertex VertexDebugOutput vertexDebugShader(VertexCompositeIn in [[stage_in]]){ fragment float4 fragmentDebugShader(VertexDebugOutput vertexOut [[stage_in]], texture2d finalTexture[[texture(0)]], depth2d depthTexture [[texture(1)]], - constant bool &isDepthTexture [[buffer(0)]], - constant simd_float2 &frustumPlanes [[buffer(1)]]){ + constant int &debugMode [[buffer(debugPassModeIndex)]], + constant simd_float2 &frustumPlanes [[buffer(debugPassFrustumPlanesIndex)]]) { constexpr sampler s(min_filter::linear,mag_filter::linear); - if(isDepthTexture){ + // Keep these values aligned with RenderDebugViewMode. + const int depthMode = 3; + const int normalMode = 2; + const int ssaoMode = 4; + + if (debugMode == depthMode) { float near = frustumPlanes.x; float far = frustumPlanes.y; float rawDepth = depthTexture.sample(s, vertexOut.uvCoords); float normalized = linearizeDepthForViewing(rawDepth, near, far); return float4(normalized, normalized, normalized, 1.0); } - - return finalTexture.sample(s, vertexOut.uvCoords); - + + float4 sampled = finalTexture.sample(s, vertexOut.uvCoords); + if (debugMode == normalMode) { + sampled = float4(sampled.xyz * 0.5 + 0.5, 1.0); + } else if (debugMode == ssaoMode) { + sampled = float4(sampled.r, sampled.r, sampled.r, 1.0); + } + + return sampled; } diff --git a/Sources/UntoldEngine/Shaders/modelShader.metal b/Sources/UntoldEngine/Shaders/modelShader.metal index 5477a46c7..e270f6985 100644 --- a/Sources/UntoldEngine/Shaders/modelShader.metal +++ b/Sources/UntoldEngine/Shaders/modelShader.metal @@ -97,7 +97,7 @@ fragment GBufferOut fragmentModelShader(VertexOutModel in [[stage_in]], // Base color - float4 sampledColor = baseColor.sample(baseColorSampler, st); + float4 sampledColor = baseColor.sample(baseColorSampler, st, bias(0.25f)); // Detect if basecolor is all zeros bool isBaseColorZero = all(materialParameter.baseColor.rgb < 0.001); @@ -126,7 +126,7 @@ fragment GBufferOut fragmentModelShader(VertexOutModel in [[stage_in]], gBufferOut.color = inBaseColor; //normal map is in Tangent space - float3 normalMap=normalize(normalTexture.sample(normalSampler, st).rgb); + float3 normalMap=normalize(normalTexture.sample(normalSampler, st, bias(0.25f)).rgb); //[0,1] to [-1,1] normalMap=normalMap*2.0-1.0; @@ -142,11 +142,11 @@ fragment GBufferOut fragmentModelShader(VertexOutModel in [[stage_in]], normalMap=(hasNormal==false)?normalize(normalVectorInWorldSpace):normalize(TBN*normalMap); float roughness=(materialParameter.hasTexture.y==1) - ? roughnessTexture.sample(materialSampler,st).r * materialParameter.roughness + ? roughnessTexture.sample(materialSampler, st, bias(0.25f)).r * materialParameter.roughness : materialParameter.roughness; float metallic=(materialParameter.hasTexture.z==1) - ? metallicTexture.sample(materialSampler,st).r * materialParameter.metallic + ? metallicTexture.sample(materialSampler, st, bias(0.25f)).r * materialParameter.metallic : materialParameter.metallic; float4 color=inBaseColor; diff --git a/Sources/UntoldEngine/Systems/AssetLoadingPolicy.swift b/Sources/UntoldEngine/Systems/AssetLoadingPolicy.swift new file mode 100644 index 000000000..e820b3cea --- /dev/null +++ b/Sources/UntoldEngine/Systems/AssetLoadingPolicy.swift @@ -0,0 +1,105 @@ +// +// AssetLoadingPolicy.swift +// UntoldEngine +// +// 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 + +// MARK: - Geometry Residency Policy + +/// Controls how an asset's geometry (vertex and index buffers) is managed in GPU memory. +public enum GeometryResidencyPolicy: String, Sendable { + /// Upload all geometry immediately during registration. + /// The mesh is permanently resident and is never evicted by the streaming system. + /// Prefer for assets where total geometry is small relative to the platform memory budget. + case eager + + /// Register leaf meshes as zero-GPU stub entities. + /// `GeometryStreamingSystem` uploads each stub from CPU RAM when the camera enters + /// `streamingRadius` and evicts it when the camera moves beyond `unloadRadius`. + /// No GPU allocation happens at registration time. + case streaming +} + +// MARK: - Texture Residency Policy + +/// Controls how an asset's textures are managed in GPU memory. +public enum TextureResidencyPolicy: String, Sendable { + /// Load and cap textures at import time using `TextureLoader.maxTextureDimension`. + /// `TextureStreamingSystem` may still adjust resolution by camera distance at runtime. + case eager + + /// `TextureStreamingSystem` manages resolution tiers by camera distance. + /// Textures are upgraded toward full resolution when the camera is close and + /// downgraded to lower tiers when it moves away, subject to the memory budget. + case streaming +} + +// MARK: - Asset Loading Policy + +/// The combined per-asset loading policy that controls both geometry and texture residency. +/// +/// When `source == .auto` this policy was computed by `AssetProfiler` from signals such as +/// estimated geometry bytes, estimated texture bytes, mesh count, and the platform memory +/// budget. When `source == .userForced` it was constructed directly from a caller-supplied +/// `MeshStreamingPolicy`. +/// +/// ## Typical combinations +/// | Geometry | Texture | When to use | +/// |---|---|---| +/// | `.eager` | `.eager` | Small asset; fits comfortably in the budget | +/// | `.streaming` | `.eager` | Geometry-dominated; many meshes or large vertex data | +/// | `.eager` | `.streaming` | Texture-dominated; few meshes but large textures | +/// | `.streaming` | `.streaming` | Mixed or very large asset; both domains are significant | +public struct AssetLoadingPolicy: Sendable { + /// Controls geometry (vertex/index buffer) residency. + public var geometryPolicy: GeometryResidencyPolicy + + /// Controls texture residency. + public var texturePolicy: TextureResidencyPolicy + + /// Whether this policy was computed automatically or forced by the caller. + public var source: PolicySource + + public enum PolicySource: String, Sendable { + case auto + case userForced + } + + public init( + geometry: GeometryResidencyPolicy, + texture: TextureResidencyPolicy, + source: PolicySource = .userForced + ) { + geometryPolicy = geometry + texturePolicy = texture + self.source = source + } + + // MARK: - Presets + + /// Full eager load: all geometry and textures uploaded immediately at registration time. + public static var fullLoad: AssetLoadingPolicy { + AssetLoadingPolicy(geometry: .eager, texture: .eager) + } + + /// Geometry streaming only: stubs registered; textures loaded eagerly at first GPU upload. + public static var geometryStreaming: AssetLoadingPolicy { + AssetLoadingPolicy(geometry: .streaming, texture: .eager) + } + + /// Texture streaming only: geometry uploaded immediately; textures streamed by distance. + public static var textureStreaming: AssetLoadingPolicy { + AssetLoadingPolicy(geometry: .eager, texture: .streaming) + } + + /// Combined streaming: geometry stubs registered and textures streamed by distance. + public static var combinedStreaming: AssetLoadingPolicy { + AssetLoadingPolicy(geometry: .streaming, texture: .streaming) + } +} diff --git a/Sources/UntoldEngine/Systems/AssetProfiler.swift b/Sources/UntoldEngine/Systems/AssetProfiler.swift new file mode 100644 index 000000000..0c7575a62 --- /dev/null +++ b/Sources/UntoldEngine/Systems/AssetProfiler.swift @@ -0,0 +1,329 @@ +// +// AssetProfiler.swift +// UntoldEngine +// +// 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 MetalKit +import ModelIO + +// MARK: - Asset Profile + +/// A lightweight snapshot of an asset's composition used to select the optimal loading policy. +/// +/// All byte estimates are rough order-of-magnitude approximations computed at registration +/// time — no texture decompression or full GPU allocation is performed. `AssetProfiler` is +/// designed to run in < a few milliseconds inside the async registration task, well within +/// the cost of `Mesh.parseAssetAsync()` itself. +public struct AssetProfile: Sendable { + // MARK: Byte Estimates + + /// On-disk file size in bytes. + public let totalFileBytes: Int + + /// Estimated GPU geometry footprint: vertex + index buffers summed across all meshes. + /// Uses the same vertex-stride formula as `CPUMeshEntry.estimatedGPUBytes`. + public let estimatedGeometryBytes: Int + + /// Estimated GPU texture footprint. Derived from texture URL file sizes with a + /// decompression expansion factor, or a heuristic when textures are embedded in USDZ. + /// May be 0 if no material texture references could be resolved. + public let estimatedTextureBytes: Int + + // MARK: Structural Signals + + /// Number of top-level `MDLMesh` objects in the parsed asset. + public let meshCount: Int + + /// Total number of `MDLSubmesh` / material slots encountered across all meshes. + public let materialCount: Int + + /// GPU byte estimate for the single largest individual mesh in the asset. + public let largestSingleMeshBytes: Int + + /// `true` when the asset has ≤ 2 meshes. + /// + /// Monolithic assets cannot be incrementally streamed — the single mesh occupies its + /// full GPU allocation in one step. Geometry streaming still prevents OOM at + /// registration time, but load appearance will not be incremental. + public let isEffectivelyMonolithic: Bool + + // MARK: Asset Character + + /// High-level classification of what dominates this asset's memory footprint. + public let assetCharacter: AssetCharacter + + public enum AssetCharacter: String, Sendable { + /// Textures account for > 75% of the combined geometry + texture estimate. + /// Few or small meshes with large textures (e.g. a single hero prop with 4K maps). + case textureDominated + + /// Geometry accounts for > 75% of the combined estimate. + /// Many or large meshes with few or small textures (e.g. a dense city LOD mesh). + case geometryDominated + + /// Neither domain dominates (25–75% split). + case mixed + + /// Asset has ≤ 2 meshes. Streaming prevents OOM but won't provide incremental load-in. + case monolithic + } +} + +// MARK: - Asset Profiler + +/// Analyzes a parsed `ProgressiveAssetData` to produce an `AssetProfile` and recommend +/// the most appropriate `AssetLoadingPolicy` for the current platform memory budget. +/// +/// ## Usage +/// ```swift +/// let profile = AssetProfiler.profile(url: url, assetData: assetData, fileSizeBytes: fileSize) +/// let policy = AssetProfiler.classifyPolicy(profile: profile, budget: MemoryBudgetManager.shared.meshBudget) +/// ``` +/// +/// `AssetProfiler` runs after `Mesh.parseAssetAsync()` returns a `ProgressiveAssetData` and +/// before any ECS entity or GPU resource is created, so it has access to all `MDLMesh` +/// objects but no Metal allocations have occurred yet. +public enum AssetProfiler { + // MARK: - Profile + + /// Build an `AssetProfile` from a parsed asset. + /// + /// - Parameters: + /// - url: Source URL of the asset (used only to stat on-disk size when `fileSizeBytes == 0`). + /// - assetData: Result of `Mesh.parseAssetAsync()`. + /// - fileSizeBytes: Pre-computed on-disk file size in bytes. Pass 0 to skip (estimates only). + /// - Returns: A populated `AssetProfile`. + static func profile( + url _: URL, + assetData: Mesh.ProgressiveAssetData, + fileSizeBytes: Int + ) -> AssetProfile { + var totalGeometryBytes = 0 + var largestMeshBytes = 0 + var materialCount = 0 + var uniqueTextureRefs = Set() + var meshCount = 0 + + for obj in assetData.topLevelObjects { + guard let mesh = obj as? MDLMesh else { continue } + meshCount += 1 + + // Geometry estimate: vertex bytes + index bytes. + // Identical formula to CPUMeshEntry.estimatedGPUBytes so the profiler and the + // budget manager agree on per-mesh costs. + let stride = Int( + (mesh.vertexDescriptor.layouts.firstObject as? MDLVertexBufferLayout)?.stride ?? 48 + ) + let vertexBytes = mesh.vertexCount * stride + let indexBytes = mesh.vertexCount * 3 * 4 // ~3 indices/vertex, 4 bytes each + let meshBytes = vertexBytes + indexBytes + totalGeometryBytes += meshBytes + largestMeshBytes = max(largestMeshBytes, meshBytes) + + // Scan submesh materials for texture URL references. + if let submeshes = mesh.submeshes { + for case let submesh as MDLSubmesh in submeshes { + guard let material = submesh.material else { continue } + materialCount += 1 + scanMaterialForTextureRefs(material, into: &uniqueTextureRefs) + } + } + } + + // Estimate total texture GPU footprint from the collected URL references. + let textureBytes = estimateTextureBytes( + textureRefs: uniqueTextureRefs, + fileSizeBytes: fileSizeBytes, + geometryBytes: totalGeometryBytes + ) + + // Classify the asset's dominant memory domain. + let isMonolithic = meshCount <= 2 + let assetCharacter: AssetProfile.AssetCharacter + + if isMonolithic { + assetCharacter = .monolithic + } else { + let total = totalGeometryBytes + textureBytes + if total == 0 { + assetCharacter = .mixed + } else { + let textureFraction = Float(textureBytes) / Float(total) + let geoFraction = Float(totalGeometryBytes) / Float(total) + switch (textureFraction, geoFraction) { + case let (t, _) where t > 0.75: + assetCharacter = .textureDominated + case let (_, g) where g > 0.75: + assetCharacter = .geometryDominated + default: + assetCharacter = .mixed + } + } + } + + return AssetProfile( + totalFileBytes: fileSizeBytes, + estimatedGeometryBytes: totalGeometryBytes, + estimatedTextureBytes: textureBytes, + meshCount: meshCount, + materialCount: materialCount, + largestSingleMeshBytes: largestMeshBytes, + isEffectivelyMonolithic: isMonolithic, + assetCharacter: assetCharacter + ) + } + + // MARK: - Classify Policy + + /// Recommend an `AssetLoadingPolicy` for the given profile and platform memory budget. + /// + /// ## Geometry policy logic + /// Geometry streaming is selected when any of the following is true: + /// - The asset has ≥ 50 meshes (many small meshes still spike GPU allocation without streaming). + /// - Estimated geometry bytes exceed 30% of the platform budget. + /// - The asset is monolithic AND geometry exceeds 30% of the budget (prevents OOM at registration). + /// + /// ## Texture policy logic + /// Texture streaming is selected when estimated texture bytes exceed 10% of the budget + /// or exceed 32 MB in absolute terms. `TextureStreamingSystem` already runs on all + /// entities with a `RenderComponent`; this policy makes the intent explicit and will + /// gate per-entity texture streaming in future phases. + /// + /// All thresholds are expressed as fractions of `budget` so the policy scales correctly + /// across macOS (1 GB), high-end iOS (512 MB), low-end iOS (256 MB), and visionOS (512 MB). + /// + /// - Parameters: + /// - profile: The `AssetProfile` produced by `profile(url:assetData:fileSizeBytes:)`. + /// - budget: Current platform GPU memory budget in bytes (`MemoryBudgetManager.meshBudget`). + /// - Returns: The recommended `AssetLoadingPolicy` with `source == .auto`. + public static func classifyPolicy(profile: AssetProfile, budget: Int) -> AssetLoadingPolicy { + let geometryPolicy = classifyGeometryPolicy(profile: profile, budget: budget) + let texturePolicy = classifyTexturePolicy(profile: profile, budget: budget) + return AssetLoadingPolicy(geometry: geometryPolicy, texture: texturePolicy, source: .auto) + } + + // MARK: - Private: Policy Classification + + private static func classifyGeometryPolicy( + profile: AssetProfile, + budget: Int + ) -> GeometryResidencyPolicy { + let budgetFraction: Float = budget > 0 + ? Float(profile.estimatedGeometryBytes) / Float(budget) + : 1.0 + + // Monolithic assets: stream only if geometry would consume a significant budget share. + // Streaming still prevents OOM at registration, even though the mesh loads in one step. + if profile.isEffectivelyMonolithic { + return budgetFraction > 0.30 ? .streaming : .eager + } + + // Many meshes always benefit from streaming (incremental, distance-ordered upload). + if profile.meshCount >= 50 { + return .streaming + } + + // Geometry footprint is significant relative to the platform budget. + if budgetFraction > 0.30 { + return .streaming + } + + return .eager + } + + private static func classifyTexturePolicy( + profile: AssetProfile, + budget: Int + ) -> TextureResidencyPolicy { + let budgetFraction: Float = budget > 0 + ? Float(profile.estimatedTextureBytes) / Float(budget) + : 0.0 + + // Stream textures when they are either significant relative to the budget + // or exceed a fixed 32 MB floor (small budget devices may have small absolute limits). + if budgetFraction > 0.10 || profile.estimatedTextureBytes > 32 * 1024 * 1024 { + return .streaming + } + + return .eager + } + + // MARK: - Private: Texture Estimation Helpers + + /// Collect texture URL strings from an MDLMaterial's known semantic slots. + /// + /// Checks `urlValue` and `stringValue` for each semantic. Both regular file URLs and + /// USDZ bracket-notation strings (e.g. `"file:///asset.usdz[0/texture.png]"`) are captured. + private static func scanMaterialForTextureRefs( + _ material: MDLMaterial, + into refs: inout Set + ) { + let textureSemantics: [MDLMaterialSemantic] = [ + .baseColor, + .roughness, + .metallic, + .bump, + .emission, + .opacity, + .ambientOcclusion, + ] + for semantic in textureSemantics { + guard let prop = material.property(with: semantic) else { continue } + if let url = prop.urlValue { + refs.insert(url.absoluteString) + } else if let str = prop.stringValue, !str.isEmpty { + refs.insert(str) + } + } + } + + /// Estimate the GPU texture footprint from a set of texture URL strings. + /// + /// **Regular file URLs**: stats the file and multiplies by 3× (conservative PNG/JPEG + /// decode expansion; ASTC expands ~6–8× but is less common for external files). + /// + /// **USDZ-embedded textures** (bracket-notation URLs): cannot stat individual zip entries + /// without decompressing. Uses a single heuristic: `(fileSize − geometryBytes) × 3`. + /// This overestimates for geometry-heavy USDZs but is safe to err high (only triggers + /// more streaming, never less). + /// + /// **No URLs found**: returns 0 (profiler leaves texture policy to the default). + private static func estimateTextureBytes( + textureRefs: Set, + fileSizeBytes: Int, + geometryBytes: Int + ) -> Int { + guard !textureRefs.isEmpty else { return 0 } + + var totalBytes = 0 + var hasEmbeddedTextures = false + + for ref in textureRefs { + if ref.contains("[") { + // USDZ bracket-notation — embedded textures; use a file-level heuristic below. + hasEmbeddedTextures = true + } else if let textureURL = URL(string: ref), textureURL.isFileURL { + let fileSize = (try? FileManager.default.attributesOfItem(atPath: textureURL.path))?[.size] as? Int ?? 0 + // PNG/JPEG typically decode to 3–4× their compressed size on the GPU. + totalBytes += fileSize * 3 + } + } + + // For embedded USDZ textures where we cannot stat individual entries: + // estimate total packed texture bytes as (fileSize − estimated_packed_geometry). + // Packed geometry is roughly 1/10 of its uncompressed GPU size. + if hasEmbeddedTextures, totalBytes == 0 { + let estimatedPackedGeometry = geometryBytes / 10 + let estimatedPackedTextureBytes = max(0, fileSizeBytes - estimatedPackedGeometry) + totalBytes = estimatedPackedTextureBytes * 3 + } + + return totalBytes + } +} diff --git a/Sources/UntoldEngine/Systems/BatchingSystem.swift b/Sources/UntoldEngine/Systems/BatchingSystem.swift index d7b0a68cf..9f3886a5f 100644 --- a/Sources/UntoldEngine/Systems/BatchingSystem.swift +++ b/Sources/UntoldEngine/Systems/BatchingSystem.swift @@ -423,6 +423,15 @@ public struct BatchGroup { var meshIndices: [(entityId: EntityID, meshIndex: Int)] // Track source meshes var boundingBox: (min: simd_float3, max: simd_float3) + /// Incremented each time `updateBatchMaterialInPlace` patches this group's textures. + /// Used to detect whether a batch artifact is stale relative to the live streaming state. + var textureGeneration: Int = 0 + + /// True when at least one entity in this batch has a LODComponent. + /// Used by the LOD debug visualizer to distinguish LOD batches from non-LOD batches + /// so that non-LOD entities are not incorrectly colored with an LOD0 tint. + var isLODBatch: Bool = false + /// Backward-compatible alias; prefer batchKey for clarity. var materialHash: String { batchKey @@ -1091,11 +1100,18 @@ public class BatchingSystem: @unchecked Sendable { return } + let insertedStart = batchGroups.count batchGroups.append(contentsOf: artifact.builtGroups) batchedCells.insert(cellId) for (entityId, info) in artifact.entityBatchMap { entityToBatch[entityId] = info } + + // The artifact was built from a material snapshot that may predate one or more + // texture streaming patches. Re-apply the live streaming state from each group's + // representative entity so the freshly-installed batch reflects current resolution. + reconcileStreamingTexturesAfterArtifact(insertedStart: insertedStart) + batchIndexNeedsRebuild = true setCellState(cellId, .renderableBatched) @@ -1115,6 +1131,66 @@ public class BatchingSystem: @unchecked Sendable { SystemIntegrationMonitor.shared.recordBatchRebuild() } + /// After a fresh artifact is appended to `batchGroups`, copy the live texture state + /// from each group's representative entity into the group's material. + /// + /// This prevents a background build (snapshotted before a streaming patch) from + /// silently reverting an entity to a lower-resolution tier. + private func reconcileStreamingTexturesAfterArtifact(insertedStart: Int) { + guard insertedStart < batchGroups.count else { return } + + for i in insertedStart ..< batchGroups.count { + // All entities in a group share the same material, so the first live entity + // is a valid representative for the current streaming state. + var liveTexture: ( + baseTex: MTLTexture?, baseLevel: TextureStreamingLevel, + roughTex: MTLTexture?, roughLevel: TextureStreamingLevel, + metalTex: MTLTexture?, metalLevel: TextureStreamingLevel, + normalTex: MTLTexture?, normalLevel: TextureStreamingLevel + )? + + for entityId in batchGroups[i].entityIds { + guard let render = scene.get(component: RenderComponent.self, for: entityId), + let mesh = render.mesh.first, + let submesh = mesh.submeshes.first, + let material = submesh.material + else { continue } + + liveTexture = ( + baseTex: material.baseColor.texture, + baseLevel: material.baseColorStreamingLevel, + roughTex: material.roughness.texture, + roughLevel: material.roughnessStreamingLevel, + metalTex: material.metallic.texture, + metalLevel: material.metallicStreamingLevel, + normalTex: material.normal.texture, + normalLevel: material.normalStreamingLevel + ) + break + } + + guard let live = liveTexture else { continue } + + // Only reconcile if the entity's streaming level differs from the artifact's + // snapshot — i.e., a streaming patch actually occurred after the snapshot. + let artifactLevel = batchGroups[i].material.baseColorStreamingLevel + guard live.baseLevel != artifactLevel + || live.roughLevel != batchGroups[i].material.roughnessStreamingLevel + || live.metalLevel != batchGroups[i].material.metallicStreamingLevel + || live.normalLevel != batchGroups[i].material.normalStreamingLevel + else { continue } + + batchGroups[i].material.baseColor.texture = live.baseTex + batchGroups[i].material.baseColorStreamingLevel = live.baseLevel + batchGroups[i].material.roughness.texture = live.roughTex + batchGroups[i].material.roughnessStreamingLevel = live.roughLevel + batchGroups[i].material.metallic.texture = live.metalTex + batchGroups[i].material.metallicStreamingLevel = live.metalLevel + batchGroups[i].material.normal.texture = live.normalTex + batchGroups[i].material.normalStreamingLevel = live.normalLevel + } + } + private func estimateCellWork(for cellId: BatchCellID) -> CellWorkEstimate { guard let members = cellToEntities[cellId], !members.isEmpty else { return .zero } var estimate = CellWorkEstimate.zero @@ -1718,6 +1794,8 @@ public class BatchingSystem: @unchecked Sendable { tangentBuffer.label = "Batch Tangent Buffer" indexBuffer.label = "Batch Index Buffer" + let isLODBatch = meshGroup.contains { scene.get(component: LODComponent.self, for: $0.entityId) != nil } + return BatchGroup( id: UUID(), cellId: cellId, @@ -1733,7 +1811,8 @@ public class BatchingSystem: @unchecked Sendable { vertexCount: allPositions.count, entityIds: entityIds, meshIndices: meshIndices, - boundingBox: (min: minBounds, max: maxBounds) + boundingBox: (min: minBounds, max: maxBounds), + isLODBatch: isLODBatch ) } @@ -1793,6 +1872,7 @@ public class BatchingSystem: @unchecked Sendable { batchGroups.indices.contains(index) else { return false } update(&batchGroups[index].material) + batchGroups[index].textureGeneration += 1 return true } diff --git a/Sources/UntoldEngine/Systems/CameraSystem.swift b/Sources/UntoldEngine/Systems/CameraSystem.swift index 925b79765..8e0ec2e50 100644 --- a/Sources/UntoldEngine/Systems/CameraSystem.swift +++ b/Sources/UntoldEngine/Systems/CameraSystem.swift @@ -171,12 +171,13 @@ public func orbitAround(entityId: EntityID, uPosition: simd_float2) { let length: Float = simd_length(target) var direction: simd_float3 = simd_normalize(target) - // rot about yaw first + // rot about yaw first — always anchor to world Y so the up axis + // never drifts across frames and causes roll accumulation. let rotationX: simd_quatf = getRotationQuaternion( axis: simd_float3(0.0, 1.0, 0.0), angle: uPosition.x ) direction = rotateVectorUsingQuaternion(q: rotationX, v: direction) - var newUpAxis = rotateVectorUsingQuaternion(q: rotationX, v: cameraComponent.yAxis) + var newUpAxis = simd_float3(0.0, 1.0, 0.0) direction = simd_normalize(direction) newUpAxis = simd_normalize(newUpAxis) diff --git a/Sources/UntoldEngine/Systems/GeometryStreamingSystem.swift b/Sources/UntoldEngine/Systems/GeometryStreamingSystem.swift index 947973feb..060c6ebe8 100644 --- a/Sources/UntoldEngine/Systems/GeometryStreamingSystem.swift +++ b/Sources/UntoldEngine/Systems/GeometryStreamingSystem.swift @@ -9,6 +9,7 @@ // file, You can obtain one at https://mozilla.org/MPL/2.0/. import Foundation +import ModelIO import simd public class GeometryStreamingSystem: @unchecked Sendable { @@ -24,15 +25,55 @@ public class GeometryStreamingSystem: @unchecked Sendable { /// Lower values reduce frame spikes when many entities leave range at once. public var maxUnloadsPerUpdate: Int = 12 - /// How often to check for load/unload (seconds) + /// How often to check for load/unload (seconds) during steady-state streaming. public var updateInterval: Float = 0.1 + /// Tick interval used during initial hydration bursts (near-band backlog > 0). + /// A fast tick drains the queue quickly rather than waiting the full updateInterval + /// between each batch dispatch. Default: ~60 fps equivalent. + public var burstTickInterval: Float = 0.016 + /// Maximum radius to query from octree (should cover largest unload radius) public var maxQueryRadius: Float = 500.0 + // MARK: - Near-Band Concurrency + + /// Fraction of an entity's streamingRadius that defines the "near band". + /// Entities closer than (streamingRadius × nearBandFraction) are serialized so + /// the closest mesh always appears before farther ones. Default: first third of range. + public var nearBandFraction: Float = 0.33 + + /// Maximum concurrent loads allowed within the near band. + /// Setting this to 1 serializes near-band uploads, guaranteeing distance-ordered appearance. + public var nearBandMaxConcurrentLoads: Int = 1 + + // MARK: - Value-Based Eviction Weights + + /// Weight given to camera distance when scoring eviction candidates (0–1). + /// Higher = farther entities are evicted first. + public var evictionDistanceWeight: Float = 0.6 + + /// Weight given to GPU memory size when scoring eviction candidates (0–1). + /// Higher = larger meshes are evicted first when at equal distance. + public var evictionSizeWeight: Float = 0.4 + + /// Distance (metres) within which a currently-visible entity is protected from eviction. + /// + /// Entities that are both visible AND closer than this radius are never evicted — removing + /// them would cause an obvious foreground pop. Entities beyond this radius CAN be evicted + /// under memory pressure even while visible, because the visual cost of a distant pop is + /// far lower than blocking a nearby mesh from loading entirely. + /// + /// Default: 30 m. Increase if you see unwanted pops on meshes that are far but prominent. + /// Decrease if zoom-out → zoom-in residency deadlocks persist (far meshes blocking near ones). + public var visibleEvictionProtectionRadius: Float = 30.0 + private let stateLock = NSLock() private var timeSinceLastUpdate: Float = 0 private var activeLoads: Set = [] + /// Subset of activeLoads that belong to the near band. Tracked separately so the + /// near-band concurrency limit can be enforced independently of the global limit. + private var activeNearBandLoads: Set = [] private var loadedStreamingEntities: Set = [] // Track loaded entities for efficient unload checks private var currentFrame: Int = 0 private var lastLoadCandidateCount: Int = 0 @@ -41,6 +82,12 @@ public class GeometryStreamingSystem: @unchecked Sendable { private var cumulativeAsyncLoadMs: Double = 0.0 private var completedAsyncLoads: Int = 0 + /// First-detection timestamps (CFAbsoluteTime) keyed by entity ID. + /// Records when each entity first appeared as a load candidate so we can measure + /// scheduler latency: time from entering range to actual dispatch. + /// Accessed only from update() and its synchronous callees — no lock needed. + private var firstRangeTimestamps: [EntityID: Double] = [:] + private init() {} @inline(__always) @@ -70,6 +117,18 @@ public class GeometryStreamingSystem: @unchecked Sendable { withStateLock { activeLoads.count } } + private func reserveNearBandLoad(entityId: EntityID) { + withStateLock { activeNearBandLoads.insert(entityId) } + } + + private func releaseNearBandLoad(entityId: EntityID) { + withStateLock { _ = activeNearBandLoads.remove(entityId) } + } + + private func activeNearBandLoadCount() -> Int { + withStateLock { activeNearBandLoads.count } + } + private func loadedStreamingEntitiesSnapshot() -> [EntityID] { withStateLock { Array(loadedStreamingEntities) } } @@ -102,9 +161,12 @@ public class GeometryStreamingSystem: @unchecked Sendable { let activeLoadsAtStart = activeLoadCountSnapshot() - // Throttle updates + // Throttle updates. Switch to a fast tick when there is a pending near-band + // backlog so initial hydration bursts drain quickly. Reverts to the normal + // updateInterval once the backlog clears. + let effectiveInterval = lastPendingLoadBacklog > 0 ? burstTickInterval : updateInterval timeSinceLastUpdate += deltaTime - guard timeSinceLastUpdate >= updateInterval else { + guard timeSinceLastUpdate >= effectiveInterval else { withStateLock { diagnostics.updateFrame = currentFrame diagnostics.updateTriggered = false @@ -142,6 +204,10 @@ public class GeometryStreamingSystem: @unchecked Sendable { case .unloaded: // Small epsilon to handle floating-point boundary cases (e.g., 200.0001 vs 200.0) if distance <= streaming.streamingRadius + 1.0 { + // Record first-detection time once; used to measure tick-to-dispatch latency. + if firstRangeTimestamps[entityId] == nil { + firstRangeTimestamps[entityId] = CFAbsoluteTimeGetCurrent() + } loadCandidates.append((entityId, distance, streaming.priority)) } @@ -202,23 +268,155 @@ public class GeometryStreamingSystem: @unchecked Sendable { return lhs.1 < rhs.1 // distance } - // Load within concurrent limit + // Check memory budget BEFORE starting new loads. + // Without this guard, all in-range stubs can upload simultaneously, pushing + // GPU memory past the OS kill threshold on Vision Pro. + var evictionTriggered = false + var evictedByLRU = 0 + + // Texture-first relief: if combined GPU memory (mesh + texture) is high but + // geometry alone is not, downgrade textures on distant entities before + // considering geometry eviction. A texture resolution drop on a far wall is + // far less noticeable than a missing mesh. + if MemoryBudgetManager.shared.shouldEvict(), !MemoryBudgetManager.shared.shouldEvictGeometry() { + TextureStreamingSystem.shared.shedTextureMemory(cameraPosition: effectiveCameraPosition) + } + + if MemoryBudgetManager.shared.shouldEvictGeometry() { + // Shed texture quality first; geometry eviction is the last resort. + TextureStreamingSystem.shared.shedTextureMemory( + cameraPosition: effectiveCameraPosition, maxEntities: 8 + ) + evictionTriggered = true + evictedByLRU = evictLRU(cameraPosition: effectiveCameraPosition) + } + + // Partition candidates into near band and rest band. + // Near band (distance ≤ streamingRadius × nearBandFraction) is serialized so the + // closest meshes always appear in distance order. Rest band uses remaining slots freely. + var nearBandCandidates: [(EntityID, Float, Int)] = [] + var restBandCandidates: [(EntityID, Float, Int)] = [] + for candidate in loadCandidates { + let (entityId, distance, priority) = candidate + let radius = scene.get(component: StreamingComponent.self, for: entityId)?.streamingRadius ?? Float.greatestFiniteMagnitude + if radius < Float.greatestFiniteMagnitude, distance <= radius * nearBandFraction { + nearBandCandidates.append((entityId, distance, priority)) + } else { + restBandCandidates.append((entityId, distance, priority)) + } + } + let availableSlots = maxConcurrentLoads - activeLoadCountSnapshot() lastLoadCandidateCount = loadCandidates.count lastPendingLoadBacklog = max(0, loadCandidates.count - max(0, availableSlots)) - let loadsToStart = max(0, availableSlots) var startedLoads = 0 - for (entityId, _, _) in loadCandidates.prefix(loadsToStart) { - loadMesh(entityId: entityId) - startedLoads += 1 + + // [Instrumentation] Log queue depth every tick that has candidates. + // Helps confirm whether near-band serialization is building a backlog. + if !loadCandidates.isEmpty { + Logger.log( + message: "[OOC-Timing] Queue: near=\(nearBandCandidates.count) rest=\(restBandCandidates.count) activeNear=\(activeNearBandLoadCount()) activeTotal=\(activeLoadCountSnapshot()) slots=\(availableSlots) backlog=\(lastPendingLoadBacklog)", + category: LogCategory.oocTiming.rawValue + ) } - // Memory pressure check - var evictionTriggered = false - var evictedByLRU = 0 - if MemoryBudgetManager.shared.shouldEvict() { - evictionTriggered = true - evictedByLRU = evictLRU() + // Determine effective near-band concurrency for this tick. + // + // Default (nearBandMaxConcurrentLoads = 1): serializes near-band loads so the + // closest mesh always appears before farther ones — prevents random pop-in order + // across different objects. + // + // Burst exception: when every near-band candidate shares the same root asset + // (i.e., all are sub-meshes of one USDZ), the distance-ordering goal is already + // satisfied at the asset level and per-mesh serialization only wastes slots. + // In that case, allow the full global concurrency — the per-asset texture lock + // is the actual safety gate against MDLAsset races. + let nearBandEffectiveMax: Int = { + guard nearBandCandidates.count > 1 else { return nearBandMaxConcurrentLoads } + var commonRoot: EntityID? = nil + for (entityId, _, _) in nearBandCandidates { + guard let r = scene.get(component: DerivedAssetNodeComponent.self, for: entityId)?.assetRootEntityId else { + return nearBandMaxConcurrentLoads // non-OOC entity → keep default ordering + } + if commonRoot == nil { commonRoot = r } + else if commonRoot != r { return nearBandMaxConcurrentLoads } // multiple roots → keep ordering + } + return commonRoot != nil ? maxConcurrentLoads : nearBandMaxConcurrentLoads + }() + + // Geometry-only gate: texture memory does not block mesh loads. + // Texture pressure is managed independently by TextureStreamingSystem. + if !MemoryBudgetManager.shared.shouldEvictGeometry() { + // Near band: serialized by default; expanded to maxConcurrentLoads for single-root bursts. + let nearSlots = max(0, min( + nearBandEffectiveMax - activeNearBandLoadCount(), + availableSlots - startedLoads + )) + var nearDispatched = 0 + for (entityId, _, _) in nearBandCandidates { + guard nearDispatched < nearSlots else { break } + // Skip OOC child entities whose CPU data isn't registered yet. + // Dispatching them wastes a slot on a disk-path fallback that will fail — + // CPU entries are populated by the registration system shortly after this tick. + // Cold roots are exempt: they rehydrate intentionally from disk. + if let rootId = scene.get(component: DerivedAssetNodeComponent.self, for: entityId)?.assetRootEntityId { + // Skip entities whose CPU data isn't registered yet (pre-streaming slot jam). + if !ProgressiveAssetLoader.shared.isColdRoot(rootId), + ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId) == nil, + !ProgressiveAssetLoader.shared.hasCPULODData(for: entityId) + { + continue + } + // Defer dispatch until background prewarm releases the per-asset texture lock. + // Dispatching while prewarm holds the lock blocks the first batch for the full + // remaining prewarm duration (~1-2 s). Wait until lockWait ≈ 0. + if ProgressiveAssetLoader.shared.isPrewarmActive(for: rootId) { + continue + } + } + // Per-candidate geometry budget check: evict if this mesh won't fit. + if let cpuEntry = ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId), + !MemoryBudgetManager.shared.canAcceptMesh(sizeBytes: cpuEntry.estimatedGPUBytes) + { + evictedByLRU += evictLRU(cameraPosition: effectiveCameraPosition) + evictionTriggered = true + guard MemoryBudgetManager.shared.canAcceptMesh(sizeBytes: cpuEntry.estimatedGPUBytes) else { continue } + } + loadMesh(entityId: entityId, isNearBand: true) + startedLoads += 1 + nearDispatched += 1 + } + + // Rest band: remaining global slots + let restSlots = max(0, availableSlots - startedLoads) + var restDispatched = 0 + for (entityId, _, _) in restBandCandidates { + guard restDispatched < restSlots else { break } + // Same guard: skip OOC child entities whose CPU data isn't ready yet. + if let rootId = scene.get(component: DerivedAssetNodeComponent.self, for: entityId)?.assetRootEntityId { + if !ProgressiveAssetLoader.shared.isColdRoot(rootId), + ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId) == nil, + !ProgressiveAssetLoader.shared.hasCPULODData(for: entityId) + { + continue + } + // Defer until background prewarm releases the texture lock. + if ProgressiveAssetLoader.shared.isPrewarmActive(for: rootId) { + continue + } + } + // Per-candidate geometry budget check for out-of-core rest-band entities. + if let cpuEntry = ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId), + !MemoryBudgetManager.shared.canAcceptMesh(sizeBytes: cpuEntry.estimatedGPUBytes) + { + evictedByLRU += evictLRU(cameraPosition: effectiveCameraPosition) + evictionTriggered = true + guard MemoryBudgetManager.shared.canAcceptMesh(sizeBytes: cpuEntry.estimatedGPUBytes) else { continue } + } + loadMesh(entityId: entityId, isNearBand: false) + startedLoads += 1 + restDispatched += 1 + } } let updateWorkMs = (CFAbsoluteTimeGetCurrent() - updateStart) * 1000.0 @@ -232,7 +430,7 @@ public class GeometryStreamingSystem: @unchecked Sendable { diagnostics.processedUnloads = processedUnloads diagnostics.loadCandidates = loadCandidates.count diagnostics.startedLoads = startedLoads - diagnostics.availableLoadSlots = loadsToStart + diagnostics.availableLoadSlots = availableSlots diagnostics.activeLoadsAtUpdateStart = activeLoadsAtStart diagnostics.activeLoadsAtUpdateEnd = activeLoadsAtEnd diagnostics.evictionTriggered = evictionTriggered @@ -240,17 +438,28 @@ public class GeometryStreamingSystem: @unchecked Sendable { } } - private func loadMesh(entityId: EntityID) { + private func loadMesh(entityId: EntityID, isNearBand: Bool = false) { guard let streaming = scene.get(component: StreamingComponent.self, for: entityId), streaming.state == .unloaded else { return } guard reserveActiveLoad(entityId: entityId) else { return } + if isNearBand { reserveNearBandLoad(entityId: entityId) } streaming.state = .loading BatchingSystem.shared.notifyEntityStreamingStarted(entityId: entityId) - // Check if entity has LOD component + // [Instrumentation] Measure scheduler latency: time from first range-detection to dispatch. + if let firstDetected = firstRangeTimestamps.removeValue(forKey: entityId) { + let tickToDispatchMs = (CFAbsoluteTimeGetCurrent() - firstDetected) * 1000.0 + Logger.log( + message: "[OOC-Timing] Entity \(entityId): tick-to-dispatch=\(String(format: "%.1f", tickToDispatchMs))ms band=\(isNearBand ? "near" : "rest")", + category: LogCategory.oocTiming.rawValue + ) + } + + // Check if entity has LOD component and CPU LOD data (LOD+OOC path) let hasLOD = scene.get(component: LODComponent.self, for: entityId) != nil + let hasCPULODData = hasLOD && ProgressiveAssetLoader.shared.hasCPULODData(for: entityId) let filename = streaming.assetFilename let ext = streaming.assetExtension @@ -258,11 +467,14 @@ public class GeometryStreamingSystem: @unchecked Sendable { let task = Task { let asyncLoadStart = CFAbsoluteTimeGetCurrent() - let success = if hasLOD { - // LOD entity: reload all LOD levels and set correct one for current distance + let success = if hasCPULODData { + // LOD+OOC entity: upload all LOD levels from CPU registry (no disk I/O) + await uploadActiveLODFromCPU(entityId: entityId) + } else if hasLOD { + // LOD entity (disk-based): reload all LOD levels and set correct one for current distance await reloadLODEntity(entityId: entityId) } else { - // Regular entity: load single mesh + // Regular entity: load single mesh from disk / cache await loadMeshAsync( entityId: entityId, filename: filename, @@ -301,6 +513,7 @@ public class GeometryStreamingSystem: @unchecked Sendable { Logger.logError(message: "Failed to stream mesh for entity \(entityId)") } releaseActiveLoad(entityId: entityId) + if isNearBand { releaseNearBandLoad(entityId: entityId) } applyMs = (CFAbsoluteTimeGetCurrent() - applyStart) * 1000.0 } recordLoadCompletion(success: success, asyncLoadMs: asyncLoadMs, applyMs: applyMs, wasLODReload: hasLOD) @@ -414,6 +627,344 @@ public class GeometryStreamingSystem: @unchecked Sendable { return true } + /// Upload one out-of-core stub entity from CPU-resident MDLMesh data to Metal. + /// + /// Called instead of the disk-based `MeshResourceManager` path when the entity was + /// registered by the out-of-core stub system. CPU→Metal copy happens here; no USDZ + /// re-read is required. The CPU data is NOT cleared after upload so that future + /// eviction+reload cycles can re-upload from the same in-memory source. + private func uploadFromCPUEntry( + entityId: EntityID, + cpuEntry: ProgressiveAssetLoader.CPUMeshEntry + ) async -> Bool { + // Serialize texture loading per asset and ensure loadTextures() has been called. + // MDLAsset is not thread-safe. The lock prevents two concurrent uploads from the + // same asset racing on MDLTexture internal state. + // ensureTexturesLoaded() is a no-op after the first call per asset — it calls + // asset.loadTextures() exactly once, deferred from parse time to first-upload time + // so the full texture decompression spike doesn't happen before any mesh is rendered. + let rootEntityId = scene.get(component: DerivedAssetNodeComponent.self, for: entityId)?.assetRootEntityId + var lockWaitMs: Double = 0 + var textureMs: Double = 0 + if let rootId = rootEntityId { + // [Instrumentation] Measure time blocked waiting for the per-asset texture lock. + let lockStart = CFAbsoluteTimeGetCurrent() + ProgressiveAssetLoader.shared.acquireAssetTextureLock(for: rootId) + lockWaitMs = (CFAbsoluteTimeGetCurrent() - lockStart) * 1000.0 + + // [Instrumentation] Measure ensureTexturesLoaded duration. + // Non-zero only on the FIRST upload from this asset; subsequent calls are no-ops. + let textureStart = CFAbsoluteTimeGetCurrent() + // Always call ensureTexturesLoaded before makeMeshesFromCPUBuffers. This calls + // asset.loadTextures() exactly once per asset — USDZ-embedded textures require it + // before MTKTextureLoader can decode them. The lock scope ends here: the MDLAsset + // is in a stable read-only state after loadTextures() and concurrent GPU uploads + // from the same asset are safe without the lock. + ProgressiveAssetLoader.shared.ensureTexturesLoaded(for: rootId) + textureMs = (CFAbsoluteTimeGetCurrent() - textureStart) * 1000.0 + ProgressiveAssetLoader.shared.releaseAssetTextureLock(for: rootId) + } + // [Instrumentation] Measure CPU→Metal buffer copy time. + let copyStart = CFAbsoluteTimeGetCurrent() + let meshes = Mesh.makeMeshesFromCPUBuffers( + object: cpuEntry.object, + vertexDescriptor: cpuEntry.vertexDescriptor, + textureLoader: cpuEntry.textureLoader, + device: cpuEntry.device, + flip: true + ) + let copyMs = (CFAbsoluteTimeGetCurrent() - copyStart) * 1000.0 + Logger.log( + message: "[OOC-Timing] Entity \(entityId) '\(cpuEntry.uniqueAssetName)': lockWait=\(String(format: "%.1f", lockWaitMs))ms textures=\(String(format: "%.1f", textureMs))ms cpuToMetal=\(String(format: "%.1f", copyMs))ms", + category: LogCategory.oocTiming.rawValue + ) + + guard !meshes.isEmpty else { + Logger.logError( + message: "[OutOfCore] CPU→Metal upload failed for entity \(entityId) ('\(cpuEntry.uniqueAssetName)')", + category: LogCategory.oocStatus.rawValue + ) + return false + } + + // Stamp the unique asset name so the RenderComponent matches the StreamingComponent. + let namedMeshes = meshes.map { m -> Mesh in + var copy = m + copy.assetName = cpuEntry.uniqueAssetName + return copy + } + + withWorldMutationGate { + registerRenderComponent( + entityId: entityId, + meshes: namedMeshes, + url: cpuEntry.url, + assetName: cpuEntry.uniqueAssetName + ) + } + + // Register Metal allocation with the budget manager so shouldEvict() sees these + // GPU bytes. Without this the budget gate in update() is blind to out-of-core uploads + // and will never throttle them — defeating the memory-pressure guard entirely. + let meshSize = calculateMeshArrayMemory(namedMeshes) + MemoryBudgetManager.shared.registerMesh( + entityId: entityId, + meshSizeBytes: meshSize, + textureSizeBytes: 0 + ) + + // CPU data is intentionally kept alive in ProgressiveAssetLoader.cpuMeshRegistry + // so eviction + re-approach triggers another uploadFromCPUEntry, not a disk read. + return true + } + + /// Upload all LOD levels for an LOD+OOC entity from the CPU registry (no disk I/O). + /// + /// Mirrors `reloadLODEntity` but reads MDLObject data from `ProgressiveAssetLoader.cpuLODRegistry` + /// instead of re-reading from disk. After all levels are uploaded, the render component is set to + /// the LOD level appropriate for the current camera distance — identical selection logic to `reloadLODEntity`. + private func uploadActiveLODFromCPU(entityId: EntityID) async -> Bool { + // Determine root entity for texture lock serialization. + let rootEntityId = scene.get(component: DerivedAssetNodeComponent.self, for: entityId)? + .assetRootEntityId ?? entityId + + // If the root asset has gone cold, re-parse from disk to restore CPU entries. + if ProgressiveAssetLoader.shared.isColdRoot(rootEntityId) { + guard let context = ProgressiveAssetLoader.shared.rehydrationContext(for: rootEntityId) else { + Logger.logError( + message: "[OutOfCore] LOD+OOC entity \(entityId): root \(rootEntityId) is cold with no rehydration context", + category: LogCategory.oocStatus.rawValue + ) + return false + } + let ok = await rehydrateColdAsset(rootEntityId: rootEntityId, context: context) + guard ok else { return false } + } + + guard let allLODEntries = ProgressiveAssetLoader.shared.retrieveAllCPULODMeshes(for: entityId), + !allLODEntries.isEmpty + else { + Logger.logError( + message: "[OutOfCore] LOD+OOC entity \(entityId): no CPU LOD entries found", + category: LogCategory.oocStatus.rawValue + ) + return false + } + + // Ensure loadTextures() has been called before any MTKTextureLoader decoding. + // The lock scope covers only ensureTexturesLoaded — the MDLAsset is read-only after + // that point and concurrent GPU uploads across LOD levels are safe without it. + ProgressiveAssetLoader.shared.acquireAssetTextureLock(for: rootEntityId) + ProgressiveAssetLoader.shared.ensureTexturesLoaded(for: rootEntityId) + ProgressiveAssetLoader.shared.releaseAssetTextureLock(for: rootEntityId) + + // Upload every LOD level from CPU to Metal. + var uploadedMeshes: [Int: [Mesh]] = [:] + for (lodIndex, cpuEntry) in allLODEntries { + let meshes = Mesh.makeMeshesFromCPUBuffers( + object: cpuEntry.object, + vertexDescriptor: cpuEntry.vertexDescriptor, + textureLoader: cpuEntry.textureLoader, + device: cpuEntry.device, + flip: true + ) + guard !meshes.isEmpty else { + Logger.logWarning( + message: "[OutOfCore] LOD+OOC entity \(entityId): CPU→Metal failed for LOD\(lodIndex), skipping level", + category: LogCategory.oocStatus.rawValue + ) + continue + } + let levelSkin = Skin() + var namedMeshes = meshes.map { m -> Mesh in var copy = m; copy.assetName = cpuEntry.uniqueAssetName; return copy } + for i in namedMeshes.indices where namedMeshes[i].skin == nil { + namedMeshes[i].skin = levelSkin + } + uploadedMeshes[lodIndex] = namedMeshes + } + + guard !uploadedMeshes.isEmpty else { + Logger.logError( + message: "[OutOfCore] LOD+OOC entity \(entityId): all LOD level uploads failed", + category: LogCategory.oocStatus.rawValue + ) + return false + } + + withWorldMutationGate { + guard let lodComponent = scene.get(component: LODComponent.self, for: entityId) else { return } + + // Store uploaded meshes in LOD levels and mark resident. + for (lodIndex, meshes) in uploadedMeshes { + guard lodIndex < lodComponent.lodLevels.count else { continue } + lodComponent.lodLevels[lodIndex].mesh = meshes + lodComponent.lodLevels[lodIndex].residencyState = .resident + } + + // Select correct LOD for current camera distance (same logic as reloadLODEntity). + var selectedLOD = lodComponent.lodLevels.count - 1 + if let camera = CameraSystem.shared.activeCamera, + let cameraComponent = scene.get(component: CameraComponent.self, for: camera), + let transform = scene.get(component: WorldTransformComponent.self, for: entityId), + let local = scene.get(component: LocalTransformComponent.self, for: entityId) + { + let cameraPos = cameraComponent.localPosition + let center = (local.boundingBox.min + local.boundingBox.max) * 0.5 + let worldCenter = transform.space * simd_float4(center, 1.0) + let distance = simd_distance(cameraPos, simd_float3(worldCenter.x, worldCenter.y, worldCenter.z)) + for (index, level) in lodComponent.lodLevels.enumerated() { + if distance <= level.maxDistance, lodComponent.isLODResident(index) { + selectedLOD = index + break + } + } + } + + if selectedLOD < lodComponent.lodLevels.count, lodComponent.isLODResident(selectedLOD) { + let lodLevel = lodComponent.lodLevels[selectedLOD] + if let cpuEntry = ProgressiveAssetLoader.shared.retrieveCPULODMesh(for: entityId, lodIndex: selectedLOD) { + registerRenderComponent(entityId: entityId, meshes: lodLevel.mesh, url: cpuEntry.url, assetName: cpuEntry.uniqueAssetName) + } + lodComponent.currentLOD = selectedLOD + lodComponent.desiredLOD = selectedLOD + lodComponent.isUsingFallback = false + } + } + + // Register total GPU allocation (all levels) with the budget manager. + let totalMeshSize = uploadedMeshes.values.reduce(0) { $0 + calculateMeshArrayMemory($1) } + MemoryBudgetManager.shared.registerMesh(entityId: entityId, meshSizeBytes: totalMeshSize, textureSizeBytes: 0) + + Logger.log( + message: "[OutOfCore] LOD+OOC entity \(entityId): uploaded \(uploadedMeshes.count) LOD level(s) from CPU", + category: LogCategory.oocStatus.rawValue + ) + return true + } + + /// Re-parse a cold root asset from disk and restore all child CPU entries. + /// + /// At most one re-parse Task runs per root at a time: `getOrCreateRehydrationTask` ensures + /// concurrent child entity requests all await the same `Task` rather than + /// each launching a duplicate re-parse. Once complete, the root transitions back to warm + /// via `markAsWarm` and all child `CPUMeshEntry` objects are restored in `cpuMeshRegistry`. + private func rehydrateColdAsset( + rootEntityId: EntityID, + context: ProgressiveAssetLoader.RootRehydrationContext + ) async -> Bool { + let task = ProgressiveAssetLoader.shared.getOrCreateRehydrationTask(for: rootEntityId) { + Task { + Logger.log( + message: "[OutOfCore] Cold re-stream: re-parsing '\(context.url.lastPathComponent)' for root \(rootEntityId)", + category: LogCategory.oocStatus.rawValue + ) + guard let assetData = await Mesh.parseAssetAsync( + url: context.url, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device + ) else { + Logger.logError( + message: "[OutOfCore] Cold re-stream: parseAssetAsync failed for root \(rootEntityId)", + category: LogCategory.oocStatus.rawValue + ) + ProgressiveAssetLoader.shared.clearRehydrationTask(for: rootEntityId) + return false + } + + let children = ProgressiveAssetLoader.shared.getChildren(for: rootEntityId) + let filename = context.url.deletingPathExtension().lastPathComponent + let ext = context.url.pathExtension + + // Detect whether this is a LOD+OOC asset by checking if the re-parsed + // top-level objects form LOD groups (same detection as registration time). + let topLevelNames = assetData.topLevelObjects.map { + ($0 as? MDLMesh)?.parent?.name ?? $0.name + } + let lodDetection = detectImportedLODGroups(fromSourceNames: topLevelNames) + + if !lodDetection.groups.isEmpty, !children.isEmpty { + // LOD+OOC: rebuild cpuLODRegistry from detected groups. + // Groups are sorted by baseName (same order as at registration time), + // so children[groupIdx] corresponds to lodDetection.groups[groupIdx]. + var nameToObject: [String: MDLObject] = [:] + for obj in assetData.topLevelObjects { + let name = (obj as? MDLMesh)?.parent?.name ?? obj.name + nameToObject[name] = obj + } + var restoredEntries = 0 + for (groupIdx, group) in lodDetection.groups.enumerated() { + guard groupIdx < children.count else { break } + let groupEntityId = children[groupIdx] + for level in group.levels { + guard let obj = nameToObject[level.sourceName] else { continue } + let estimatedGPUBytes: Int = { + guard let mdlMesh = obj as? MDLMesh else { return 0 } + let stride = Int((mdlMesh.vertexDescriptor.layouts.firstObject as? MDLVertexBufferLayout)?.stride ?? 48) + return mdlMesh.vertexCount * stride + mdlMesh.vertexCount * 3 * 4 + }() + let entry = ProgressiveAssetLoader.CPUMeshEntry( + object: obj, + vertexDescriptor: vertexDescriptor.model, + textureLoader: assetData.textureLoader, + device: renderInfo.device, + url: context.url, + filename: filename, + withExtension: ext, + uniqueAssetName: level.sourceName, + estimatedGPUBytes: estimatedGPUBytes, + residencyPolicy: context.loadingPolicy + ) + ProgressiveAssetLoader.shared.storeCPULODMesh(entry, for: groupEntityId, lodIndex: level.lodIndex) + restoredEntries += 1 + } + } + ProgressiveAssetLoader.shared.storeAsset(assetData.asset, for: rootEntityId) + ProgressiveAssetLoader.shared.markAsWarm(rootEntityId: rootEntityId) + Logger.log( + message: "[OutOfCore] Cold re-stream complete (LOD+OOC): root \(rootEntityId) is warm (\(restoredEntries) LOD entries restored across \(lodDetection.groups.count) group(s))", + category: LogCategory.oocStatus.rawValue + ) + } else { + // Regular OOC: rebuild cpuMeshRegistry, one entry per child stub entity. + for (i, obj) in assetData.topLevelObjects.enumerated() { + guard i < children.count else { break } + let childId = children[i] + let baseName = (obj as? MDLMesh)?.parent?.name ?? obj.name + let uniqueName = "\(baseName)#\(i)" + let estimatedGPUBytes: Int = { + guard let mdlMesh = obj as? MDLMesh else { return 0 } + let stride = Int((mdlMesh.vertexDescriptor.layouts.firstObject as? MDLVertexBufferLayout)?.stride ?? 48) + let vertexBytes = mdlMesh.vertexCount * stride + let indexBytes = mdlMesh.vertexCount * 3 * 4 + return vertexBytes + indexBytes + }() + let entry = ProgressiveAssetLoader.CPUMeshEntry( + object: obj, + vertexDescriptor: vertexDescriptor.model, + textureLoader: assetData.textureLoader, + device: renderInfo.device, + url: context.url, + filename: filename, + withExtension: ext, + uniqueAssetName: uniqueName, + estimatedGPUBytes: estimatedGPUBytes, + residencyPolicy: context.loadingPolicy + ) + ProgressiveAssetLoader.shared.storeCPUMesh(entry, for: childId) + } + ProgressiveAssetLoader.shared.storeAsset(assetData.asset, for: rootEntityId) + ProgressiveAssetLoader.shared.markAsWarm(rootEntityId: rootEntityId) + Logger.log( + message: "[OutOfCore] Cold re-stream complete: root \(rootEntityId) is warm (\(min(assetData.topLevelObjects.count, children.count)) entries restored)", + category: LogCategory.oocStatus.rawValue + ) + } + return true + } + } + return await task.value + } + /// Load mesh asynchronously - returns true on success, false on failure private func loadMeshAsync( entityId: EntityID, @@ -421,6 +972,32 @@ public class GeometryStreamingSystem: @unchecked Sendable { withExtension ext: String, assetName: String? ) async -> Bool { + // Out-of-core fast path: entity has CPU-resident MDLMesh data from stub registration. + // Upload from RAM — no disk I/O, no MeshResourceManager parse. + if let cpuEntry = ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId) { + return await uploadFromCPUEntry(entityId: entityId, cpuEntry: cpuEntry) + } + + // Out-of-core cold re-stream path: CPU data was released via releaseWarmAsset() but + // the entity has a rehydration context (URL + policy). Re-parse from disk, restore + // all child CPU entries, then upload this entity from the freshly-parsed data. + if let rootId = scene.get(component: DerivedAssetNodeComponent.self, for: entityId)?.assetRootEntityId, + ProgressiveAssetLoader.shared.isColdRoot(rootId), + let context = ProgressiveAssetLoader.shared.rehydrationContext(for: rootId) + { + let rehydrated = await rehydrateColdAsset(rootEntityId: rootId, context: context) + if rehydrated, + let cpuEntry = ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId) + { + return await uploadFromCPUEntry(entityId: entityId, cpuEntry: cpuEntry) + } + Logger.logError( + message: "[OutOfCore] Cold re-stream failed for entity \(entityId)", + category: LogCategory.oocStatus.rawValue + ) + return false + } + // Build URL guard let url = LoadingSystem.shared.resourceURL( forResource: filename, @@ -486,6 +1063,9 @@ public class GeometryStreamingSystem: @unchecked Sendable { streaming.state == .loaded else { return } + // Clear first-detection timestamp so a future re-approach records a fresh baseline. + firstRangeTimestamps.removeValue(forKey: entityId) + let unloadStart = CFAbsoluteTimeGetCurrent() withWorldMutationGate { streaming.state = .unloading @@ -541,46 +1121,73 @@ public class GeometryStreamingSystem: @unchecked Sendable { updateLastUnloadDuration(unloadMs) } - private func evictLRU() -> Int { + /// Evict loaded entities under memory pressure, prioritising by value score. + /// + /// Score = `evictionDistanceWeight × distanceFactor + evictionSizeWeight × sizeFactor`. + /// Entities with high distance and large GPU footprint are evicted first, protecting + /// nearby small meshes that are most valuable for scene coverage near the camera. + /// LRU frame is retained as a tiebreaker for equal-score candidates. + /// + /// Visibility guard is distance-aware: entities within `visibleEvictionProtectionRadius` + /// are never evicted while visible (prevents foreground popping). Entities beyond that + /// radius may be evicted even while visible — a distant pop is cheaper than a nearby + /// mesh failing to load under memory pressure. + private func evictLRU(cameraPosition: simd_float3) -> Int { // First, evict any unused cached files MeshResourceManager.shared.evictUnused() - // Use tracked loaded entities instead of querying all entities - var candidates: [(EntityID, Int)] = [] // (entity, lastVisibleFrame) + var candidates: [(entityId: EntityID, score: Float, lastFrame: Int, distance: Float)] = [] var staleEntityIds: [EntityID] = [] let trackedLoadedSnapshot = loadedStreamingEntitiesSnapshot() + let budget = Float(max(1, MemoryBudgetManager.shared.meshBudget)) + for entityId in trackedLoadedSnapshot { - // Check if entity still exists guard scene.exists(entityId) else { staleEntityIds.append(entityId) continue } - guard let streaming = scene.get(component: StreamingComponent.self, for: entityId), streaming.state == .loaded else { continue } - candidates.append((entityId, streaming.lastVisibleFrame)) + let distance = calculateDistance(entityId: entityId, cameraPosition: cameraPosition) + let distanceFactor = min(1.0, distance / maxQueryRadius) + + let meshBytes = Float(MemoryBudgetManager.shared.getMemorySize(for: entityId) ?? 0) + let sizeFactor = min(1.0, meshBytes / budget) + + let score = evictionDistanceWeight * distanceFactor + evictionSizeWeight * sizeFactor + candidates.append((entityId, score, streaming.lastVisibleFrame, distance)) } - // Clean up stale entity IDs for staleId in staleEntityIds { unmarkLoadedStreamingEntity(staleId) } - // Sort by oldest first - candidates.sort { $0.1 < $1.1 } + // Sort: highest eviction score first; LRU frame as tiebreaker. + candidates.sort { + if abs($0.score - $1.score) > 0.001 { return $0.score > $1.score } + return $0.lastFrame < $1.lastFrame + } - // Evict until memory pressure relieved + let visibleSet = Set(visibleEntityIds) var evictedCount = 0 - for (entityId, _) in candidates { - guard MemoryBudgetManager.shared.shouldEvict() else { break } - - // Don't evict currently visible entities - if visibleEntityIds.contains(entityId) { continue } + for candidate in candidates { + // Stop when geometry-only pressure clears — texture memory is managed + // independently by TextureStreamingSystem and should not force extra + // geometry evictions. + guard MemoryBudgetManager.shared.shouldEvictGeometry() else { break } + + // Distance-aware visibility guard. + // Close visible meshes (< visibleEvictionProtectionRadius) are protected — evicting + // them would cause an obvious foreground pop. Far visible meshes are evictable under + // memory pressure; a distant pop is less harmful than a nearby mesh failing to load. + if visibleSet.contains(candidate.entityId), candidate.distance < visibleEvictionProtectionRadius { + continue + } - unloadMesh(entityId: entityId) + unloadMesh(entityId: candidate.entityId) evictedCount += 1 } return evictedCount @@ -619,8 +1226,32 @@ public class GeometryStreamingSystem: @unchecked Sendable { else { return Float.infinity } let center = (local.boundingBox.min + local.boundingBox.max) * 0.5 - let worldCenter = transform.space * simd_float4(center, 1.0) - return simd_distance(cameraPosition, simd_float3(worldCenter.x, worldCenter.y, worldCenter.z)) + + // Transform the camera into entity-local space so that streamingRadius and + // unloadRadius are scale-invariant. Without this, applying scaleTo(0.1) on a + // parent compresses all child world-space positions by 10×, collapsing every + // stub into streaming range simultaneously and exhausting the memory budget. + // With this approach, streamingRadius is effectively in model-local units: + // scale 1.0 → same result as world-space comparison (backward compatible) + // scale 0.1 → camera must be within streamingRadius *local* units, which + // is streamingRadius×0.1 world-space metres — matching the + // proportionally smaller scene footprint. + // If worldMatrix is degenerate (zero scale), inverse produces infinities and + // simd_distance returns infinity, so the entity is safely skipped. + let inv = transform.space.inverse + let localCamera4 = inv * simd_float4(cameraPosition, 1.0) + let localCamera = simd_float3(localCamera4.x, localCamera4.y, localCamera4.z) + + // Use closest point on AABB rather than bounding box center. + // This ensures large flat meshes (exterior walls, floors) load as soon as + // the camera reaches their *surface*, not their center. Without this, a wall + // spanning 1600 units has its center hundreds of units away even when the + // camera is right against it, so small interior objects closer to their own + // centers would incorrectly load first. + // When the camera is inside the AABB the closest point equals the camera + // position, giving distance = 0, which correctly loads the enclosing mesh. + let closestPoint = simd_clamp(localCamera, local.boundingBox.min, local.boundingBox.max) + return simd_distance(localCamera, closestPoint) } /// Force load an entity's mesh immediately @@ -659,6 +1290,12 @@ public class GeometryStreamingSystem: @unchecked Sendable { markLoadedStreamingEntity(entityId) } + /// Returns the current frame counter so callers can seed `lastVisibleFrame` on + /// newly registered entities without holding the state lock themselves. + public func currentFrameSnapshot() -> Int { + withStateLock { currentFrame } + } + /// Remove an entity from streaming tracking sets. public func unregisterEntity(_ entityId: EntityID) { withWorldMutationGate { diff --git a/Sources/UntoldEngine/Systems/LODSystem.swift b/Sources/UntoldEngine/Systems/LODSystem.swift index 2dfba8ad5..98e8c2625 100644 --- a/Sources/UntoldEngine/Systems/LODSystem.swift +++ b/Sources/UntoldEngine/Systems/LODSystem.swift @@ -110,15 +110,26 @@ public class LODSystem: @unchecked Sendable { // Apply LOD bias let adjustedDistance = distance * LODConfig.shared.lodBias + let globalDistances = LODConfig.shared.lodDistances - // Find appropriate LOD level + // Find appropriate LOD level. + // Per-level maxDistance takes priority; fall back to LODConfig.lodDistances[index] + // when maxDistance is 0 (unset). This lets users configure distances either + // per-level at registration time OR globally via LODConfig. for (index, lodLevel) in lodComponent.lodLevels.enumerated() { - var threshold = lodLevel.maxDistance + let baseThreshold: Float + if lodLevel.maxDistance > 0 { + baseThreshold = lodLevel.maxDistance + } else if index < globalDistances.count { + baseThreshold = globalDistances[index] + } else { + continue // No threshold available for this level — skip + } // Apply hysteresis when switching to higher detail (prevents flickering) - if index < currentLOD { - threshold -= LODConfig.shared.hysteresis - } + let threshold = index < currentLOD + ? baseThreshold - LODConfig.shared.hysteresis + : baseThreshold if adjustedDistance <= threshold { return index diff --git a/Sources/UntoldEngine/Systems/MemoryBudgetManager.swift b/Sources/UntoldEngine/Systems/MemoryBudgetManager.swift index 5e11b7ccd..671531028 100644 --- a/Sources/UntoldEngine/Systems/MemoryBudgetManager.swift +++ b/Sources/UntoldEngine/Systems/MemoryBudgetManager.swift @@ -114,6 +114,16 @@ public class MemoryBudgetManager: @unchecked Sendable { /// Total texture memory currently tracked private var totalTextureMemory: Int = 0 + /// In-flight texture upgrade reservations. + /// + /// When `TextureStreamingSystem` accepts a budget check for an upgrade batch, it + /// atomically reserves the estimated bytes here before spawning the async Task. + /// Subsequent concurrent `canAcceptTexture` / `reserveTexture` checks include this + /// value so they see the already-committed headroom and cannot collectively overshoot + /// the budget. The reservation is released (and replaced by actual bytes via + /// `updateTextureSizeBytes`) once the async work completes or fails. + private var inFlightTextureReservation: Int = 0 + /// Thread safety lock private let lock = NSLock() @@ -131,6 +141,10 @@ public class MemoryBudgetManager: @unchecked Sendable { #if os(macOS) // macOS typically has more GPU memory meshBudget = 1024 * 1024 * 1024 // 1 GB + #elseif os(visionOS) + // Vision Pro (M2/M4) has 16 GB unified memory; geometry + textures + // share this budget, so give ample room for both. + meshBudget = 1536 * 1024 * 1024 // 1.5 GB #elseif os(iOS) // iOS devices vary widely, use conservative default if ProcessInfo.processInfo.physicalMemory > 4 * 1024 * 1024 * 1024 { @@ -255,7 +269,7 @@ public class MemoryBudgetManager: @unchecked Sendable { ) } - /// Check if we should start evicting entities + /// Check if we should start evicting entities (combined geometry + texture budget). public func shouldEvict() -> Bool { guard enabled else { return false } @@ -266,14 +280,112 @@ public class MemoryBudgetManager: @unchecked Sendable { return utilization >= highWaterMark } - /// Check if we can accept a new mesh of the given size + /// Check if geometry memory alone has hit the high-water mark. + /// + /// Used by `GeometryStreamingSystem` so that texture upgrades on already-loaded + /// entities cannot block new mesh loads. Texture memory is intentionally excluded + /// from this check — texture pressure is managed independently by `TextureStreamingSystem`. + public func shouldEvictGeometry() -> Bool { + guard enabled else { return false } + + lock.lock() + defer { lock.unlock() } + + let utilization = Float(totalMeshMemory) / Float(meshBudget) + return utilization >= highWaterMark + } + + /// Check if we can accept a new mesh of the given size. + /// + /// Accounts for both geometry and texture memory already tracked so the + /// combined GPU footprint stays within the budget after the new allocation. public func canAccept(sizeBytes: Int) -> Bool { lock.lock() defer { lock.unlock() } + return (totalMeshMemory + totalTextureMemory + sizeBytes) <= meshBudget + } + + /// Check if a new mesh of the given size fits within the geometry portion of the budget. + /// + /// Only counts geometry (vertex/index) memory — texture memory is excluded so that + /// texture upgrades cannot prevent mesh loads. Used by `GeometryStreamingSystem` + /// for its per-candidate pre-emptive budget reservation. + public func canAcceptMesh(sizeBytes: Int) -> Bool { + lock.lock() + defer { lock.unlock() } + return (totalMeshMemory + sizeBytes) <= meshBudget } + /// Check if we can accept a new texture allocation of the given size. + /// + /// Includes in-flight reservations (`inFlightTextureReservation`) so callers see + /// headroom already committed to in-progress upgrade Tasks. Use `reserveTexture` + /// instead of this method when you intend to actually start an upgrade — `reserveTexture` + /// atomically checks and reserves in one lock acquisition, eliminating the TOCTOU race + /// between checking and acting. + public func canAcceptTexture(sizeBytes: Int) -> Bool { + lock.lock() + defer { lock.unlock() } + + return (totalMeshMemory + totalTextureMemory + inFlightTextureReservation + sizeBytes) <= meshBudget + } + + /// Atomically check whether a texture upgrade fits within the budget and, if so, + /// reserve the estimated bytes. + /// + /// Unlike `canAcceptTexture` + a separate action, this method eliminates the + /// check-then-act race: two concurrent callers cannot both see "budget available" + /// and both proceed, because the first reservation reduces the apparent headroom + /// for the second caller before it acquires the lock. + /// + /// - Parameter sizeBytes: Estimated GPU bytes for the upgrade batch (from + /// `TextureStreamingSystem.estimatedUpgradeBytes`). + /// - Returns: `true` if the reservation was accepted; `false` if the budget would + /// be exceeded. If `false`, the caller should proceed with downgrades only and + /// must NOT call `releaseTextureReservation`. + public func reserveTexture(sizeBytes: Int) -> Bool { + lock.lock() + defer { lock.unlock() } + + guard (totalMeshMemory + totalTextureMemory + inFlightTextureReservation + sizeBytes) <= meshBudget else { + return false + } + inFlightTextureReservation += sizeBytes + return true + } + + /// Release a previously accepted texture upgrade reservation. + /// + /// Call this after the async upgrade Task completes (success, failure, or cancellation), + /// before or concurrently with `updateTextureSizeBytes`. Releasing the reservation + /// restores the apparent headroom for subsequent `canAcceptTexture` / `reserveTexture` + /// calls. Only call this if `reserveTexture` returned `true`. + public func releaseTextureReservation(sizeBytes: Int) { + lock.lock() + inFlightTextureReservation = max(0, inFlightTextureReservation - sizeBytes) + lock.unlock() + } + + /// Update texture size tracking for an entity after texture streaming completes. + /// + /// Called by `TextureStreamingSystem` whenever a texture upgrade or downgrade + /// finishes so `totalTextureMemory` reflects the actual current GPU allocation. + /// No-op if the entity is not currently tracked (e.g. entity was evicted while + /// the streaming Task was in-flight). + public func updateTextureSizeBytes(entityId: EntityID, newSizeBytes: Int) { + lock.lock() + defer { lock.unlock() } + + guard var entry = memoryEntries[entityId] else { return } + totalTextureMemory -= entry.textureSizeBytes + entry.textureSizeBytes = newSizeBytes + entry.lastUsedFrame = currentFrame + totalTextureMemory += newSizeBytes + memoryEntries[entityId] = entry + } + /// Get memory size for an entity (if tracked) public func getMemorySize(for entityId: EntityID) -> Int? { lock.lock() @@ -331,14 +443,17 @@ public class MemoryBudgetManager: @unchecked Sendable { return Array(sorted) } - /// Get candidates to evict to reach the low water mark - /// - Returns: Array of entity IDs that should be evicted + /// Get candidates to evict to reach the low water mark. + /// + /// Uses combined geometry + texture memory to compute how much needs to be + /// freed, and counts each candidate's total size (mesh + texture) toward the + /// target. This ensures eviction correctly accounts for texture-heavy entities. public func getEvictionCandidatesToTarget() -> [EntityID] { lock.lock() defer { lock.unlock() } let targetMemory = Int(Float(meshBudget) * lowWaterMark) - var memoryToFree = totalMeshMemory - targetMemory + var memoryToFree = (totalMeshMemory + totalTextureMemory) - targetMemory guard memoryToFree > 0 else { return [] } @@ -349,7 +464,7 @@ public class MemoryBudgetManager: @unchecked Sendable { for entry in sorted { if memoryToFree <= 0 { break } candidates.append(entry.entityId) - memoryToFree -= entry.meshSizeBytes + memoryToFree -= entry.totalSize } return candidates @@ -379,6 +494,7 @@ public class MemoryBudgetManager: @unchecked Sendable { memoryEntries.removeAll() totalMeshMemory = 0 totalTextureMemory = 0 + inFlightTextureReservation = 0 } /// Get number of tracked entities diff --git a/Sources/UntoldEngine/Systems/ProgressiveAssetLoader.swift b/Sources/UntoldEngine/Systems/ProgressiveAssetLoader.swift new file mode 100644 index 000000000..9dcf5b3d5 --- /dev/null +++ b/Sources/UntoldEngine/Systems/ProgressiveAssetLoader.swift @@ -0,0 +1,493 @@ +// +// ProgressiveAssetLoader.swift +// UntoldEngine +// +// 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 MetalKit +import ModelIO + +// MARK: - Loader + +/// Manages the out-of-core streaming CPU registry for large USD/USDZ assets. +/// +/// Large assets are registered as zero-GPU stub entities immediately. CPU-side MDLMesh data is +/// stored in `cpuMeshRegistry` keyed by child entity ID so `GeometryStreamingSystem` can upload +/// each mesh on demand — no disk re-read required on normal eviction/reload cycles. +/// +/// ## Warm / Cold Residency Lifecycle +/// +/// Each root asset starts **CPU-warm**: its `MDLAsset` and all child `MDLObject` buffers are +/// alive in `rootAssetRefs` / `cpuMeshRegistry`. Uploads read directly from RAM. +/// +/// Call `releaseWarmAsset(rootEntityId:)` to transition an asset to **CPU-cold**: the +/// `MDLAsset` and all child CPU buffers are released, freeing heap memory. The +/// `RootRehydrationContext` (URL + loading policy) stored at registration time is retained so +/// `GeometryStreamingSystem` can re-parse from disk transparently when a cold entity +/// re-enters streaming range. +/// +/// Cold re-hydration is serialized per root entity via `getOrCreateRehydrationTask`: exactly one +/// re-parse `Task` runs per root regardless of how many child entities concurrently detect the +/// cold state. Once re-parsing completes, `markAsWarm` restores the warm state. +/// +/// ## Integration +/// - `setEntityMeshAsync()` registers stubs, stores CPU entries, and calls +/// `storeRootRehydrationContext` for large assets. +/// - `GeometryStreamingSystem.loadMeshAsync()` retrieves CPU entries via `retrieveCPUMesh(for:)`; +/// if the asset is cold it calls `rehydrateColdAsset` to re-parse from disk. +/// - `UntoldEngine.swift` calls `tick()` each frame (no-op; retained for API compatibility). +/// - Call `removeOutOfCoreAsset(rootEntityId:)` when destroying a root entity. +/// - Call `releaseWarmAsset(rootEntityId:)` to free CPU-heap memory while keeping the +/// entity registered so it can be re-hydrated on demand. +public final class ProgressiveAssetLoader: @unchecked Sendable { + public static let shared = ProgressiveAssetLoader() + + // MARK: Configuration + + /// File-size threshold (bytes) that routes assets to the out-of-core stub path. + /// + /// Assets whose on-disk size exceeds this value are registered as stub entities with + /// no GPU allocation. GeometryStreamingSystem uploads each stub from CPU RAM on demand. + /// Assets at or below use the immediate fast path (all meshes registered in one pass). + /// + /// Default: 50 MB. Adjust based on the target device's GPU memory budget. + /// + /// - Note: Deprecated. Asset classification is now handled by the two-stage admission gate + /// in `RegistrationSystem.setEntityMeshAsync` (Stage 1: 20× file-size expansion vs 50% + /// physical RAM; Stage 2: `AssetProfiler.profile` geometry bytes vs 75% physical RAM) + /// and by `AssetProfiler.classifyPolicy` for the `.auto` streaming policy. This property + /// is retained only for call-site compatibility and has no effect on admission decisions. + @available(*, deprecated, message: "No longer used for admission decisions. The two-stage gate in setEntityMeshAsync and AssetProfiler.classifyPolicy replace this threshold. Safe to remove from call sites.") + public var fileSizeThresholdBytes: Int = 50 * 1024 * 1024 // 50 MB + + /// Minimum leaf-mesh count that triggers the out-of-core stub path regardless of file size. + /// + /// A small USDZ (e.g. 18 MB) with 200+ objects would bypass the file-size threshold + /// and never get out-of-core treatment. This count-based trigger catches those cases. + /// Default: 50 meshes. Set to `Int.max` to disable count-based triggering. + /// + /// - Note: Deprecated. Asset classification is now handled by the two-stage admission gate + /// in `RegistrationSystem.setEntityMeshAsync` and by `AssetProfiler.classifyPolicy`. + /// This property is retained only for call-site compatibility and has no effect on + /// classification decisions. + @available(*, deprecated, message: "No longer used for classification. AssetProfiler.classifyPolicy and the two-stage admission gate replace this threshold. Safe to remove from call sites.") + public var outOfCoreObjectCountThreshold: Int = 50 + + /// Set to `false` to skip all texture loading during mesh upload. + /// Useful for testing geometry-only throughput without the texture decompression overhead. + public var textureLoadingEnabled: Bool = true + + // MARK: State + + private let lock = NSLock() + + // MARK: Out-of-Core CPU Registry + + /// All context needed to upload one MDLMesh leaf to Metal without re-reading the USDZ. + /// + /// Stored in `cpuMeshRegistry` keyed by child entity ID. The registry entry persists + /// across eviction/reload cycles so GeometryStreamingSystem can re-upload the mesh from + /// CPU RAM whenever the entity re-enters streaming range — no disk I/O required. + struct CPUMeshEntry: @unchecked Sendable { + let object: MDLObject + let vertexDescriptor: MDLVertexDescriptor + let textureLoader: TextureLoader + let device: MTLDevice + let url: URL + let filename: String + let withExtension: String + /// Pre-computed unique name for this entity: "\(parentName)#\(index)". + let uniqueAssetName: String + /// Estimated GPU memory (bytes) for pre-emptive budget reservation. + /// Computed from MDLMesh vertex/index counts at stub registration time — no disk I/O. + let estimatedGPUBytes: Int + /// The loading policy selected at classification time (computed by AssetProfiler + /// for .auto, or derived from the caller's MeshStreamingPolicy for .outOfCore / + /// .immediate). This is immutable classification-time intent, not mutable runtime + /// state. uploadFromCPUEntry reads this to decide geometry and texture upload behaviour. + let residencyPolicy: AssetLoadingPolicy + } + + /// Immutable context needed to re-parse a cold root asset from disk. + /// + /// Stored at stub-registration time in `rootRehydrationContexts`. Survives `releaseWarmAsset` + /// so `GeometryStreamingSystem` can re-parse the USDZ and rebuild CPU entries without + /// any information from the caller. + struct RootRehydrationContext: @unchecked Sendable { + let url: URL + let loadingPolicy: AssetLoadingPolicy + } + + /// CPU-resident mesh data keyed by child entity ID. + private var cpuMeshRegistry: [EntityID: CPUMeshEntry] = [:] + + /// CPU-resident LOD mesh data keyed by LOD group entity ID → LOD index. + /// + /// Used exclusively by the LOD+OOC path where a single entity holds a `LODComponent` + /// whose levels are each backed by a separate `CPUMeshEntry`. Unlike `cpuMeshRegistry` + /// (one entry per stub entity), this stores N entries per entity — one per LOD level. + private var cpuLODRegistry: [EntityID: [Int: CPUMeshEntry]] = [:] + + /// MDLAsset references kept alive per root entity so the MDLMeshBufferDataAllocator + /// that backs all child MDLMesh CPU buffers is not prematurely released. + private var rootAssetRefs: [EntityID: MDLAsset] = [:] + + /// Child entity IDs grouped by root entity ID — used to bulk-remove CPU entries + /// when the root entity is destroyed. + private var rootEntityChildren: [EntityID: [EntityID]] = [:] + + /// Per-asset NSLocks that serialize texture hydration across concurrent streaming uploads. + /// + /// MDLAsset is not thread-safe. Two Tasks uploading different meshes from the same asset + /// simultaneously can race during `loadTextures()` or `texelDataWithTopLeftOrigin`. One lock + /// per root entity ensures only one mesh from a given asset loads textures at a time. + private var assetTextureLocks: [EntityID: NSLock] = [:] + + /// Tracks which root assets have already had `loadTextures()` called on their MDLAsset. + /// After the first upload from an asset triggers deferred texture loading, subsequent + /// uploads from the same asset skip the call. + private var assetTexturesLoaded: Set = [] + + /// Root entity IDs whose background prewarm task is currently executing `loadTextures()`. + /// Entities belonging to a root in this set are deferred by the scheduler: dispatching + /// them while the prewarm holds the per-asset texture lock would block the entire first + /// batch for the full remaining prewarm duration (~1-2 s). Slots stay free until the + /// prewarm releases the lock, then the burst fires with lockWait ≈ 0. + private var activePrewarmRoots: Set = [] + + // MARK: Warm / Cold Residency State + + /// Root entity IDs whose CPU data (MDLAsset + cpuMeshRegistry entries) has been released. + /// Warm roots are absent from this set. Cold roots are re-hydratable from `rootRehydrationContexts`. + private var coldRoots: Set = [] + + /// Rehydration context (URL + policy) keyed by root entity ID. + /// Populated at stub-registration time; survives `releaseWarmAsset`. + private var rootRehydrationContexts: [EntityID: RootRehydrationContext] = [:] + + /// In-flight re-parse tasks keyed by root entity ID. + /// `getOrCreateRehydrationTask` ensures at most one task runs per root concurrently. + private var coldRehydrationTasks: [EntityID: Task] = [:] + + func storeCPUMesh(_ entry: CPUMeshEntry, for entityId: EntityID) { + lock.lock() + cpuMeshRegistry[entityId] = entry + lock.unlock() + } + + func retrieveCPUMesh(for entityId: EntityID) -> CPUMeshEntry? { + lock.lock() + defer { lock.unlock() } + return cpuMeshRegistry[entityId] + } + + func removeCPUMesh(for entityId: EntityID) { + lock.lock() + cpuMeshRegistry.removeValue(forKey: entityId) + lock.unlock() + } + + // MARK: LOD CPU Registry + + /// Store the CPU mesh entry for one LOD level of a LOD group entity. + func storeCPULODMesh(_ entry: CPUMeshEntry, for entityId: EntityID, lodIndex: Int) { + lock.lock() + if cpuLODRegistry[entityId] == nil { + cpuLODRegistry[entityId] = [:] + } + cpuLODRegistry[entityId]![lodIndex] = entry + lock.unlock() + } + + /// Retrieve the CPU mesh entry for one LOD level of a LOD group entity. + func retrieveCPULODMesh(for entityId: EntityID, lodIndex: Int) -> CPUMeshEntry? { + lock.lock() + defer { lock.unlock() } + return cpuLODRegistry[entityId]?[lodIndex] + } + + /// Retrieve all LOD-level CPU entries for a LOD group entity (keyed by LOD index). + func retrieveAllCPULODMeshes(for entityId: EntityID) -> [Int: CPUMeshEntry]? { + lock.lock() + defer { lock.unlock() } + return cpuLODRegistry[entityId] + } + + /// Returns `true` if the entity has at least one CPU LOD entry (i.e. was registered via the LOD+OOC path). + func hasCPULODData(for entityId: EntityID) -> Bool { + lock.lock() + defer { lock.unlock() } + return !(cpuLODRegistry[entityId]?.isEmpty ?? true) + } + + /// Remove all CPU LOD entries for a LOD group entity (called on entity destruction or cold-release). + func removeCPULODEntry(for entityId: EntityID) { + lock.lock() + cpuLODRegistry.removeValue(forKey: entityId) + lock.unlock() + } + + func storeAsset(_ asset: MDLAsset, for rootEntityId: EntityID) { + lock.lock() + rootAssetRefs[rootEntityId] = asset + assetTextureLocks[rootEntityId] = NSLock() + lock.unlock() + // Kick off a background pre-warm so loadTextures() completes before any mesh + // enters streaming range. This moves the first-texture penalty off the critical + // upload path — the per-asset lock ensures at-most-once execution. + prewarmTexturesAsync(for: rootEntityId) + } + + /// Returns `true` while the background prewarm task for `rootEntityId` is running. + /// + /// The scheduler uses this to defer dispatching entities for this root until the prewarm + /// releases the per-asset texture lock. Once it returns `false`, the next burst tick + /// dispatches the full first batch with lockWait ≈ 0. + func isPrewarmActive(for rootEntityId: EntityID) -> Bool { + lock.lock() + defer { lock.unlock() } + return activePrewarmRoots.contains(rootEntityId) + } + + /// Fire a background task to call `loadTextures()` on this root asset's MDLAsset. + /// + /// Runs at user-initiated priority so it completes quickly before meshes enter range. + /// `ensureTexturesLoaded` is idempotent — if the upload path races and calls it first, + /// the pre-warm becomes a no-op, and vice versa. The per-asset texture lock prevents + /// both from running `loadTextures()` simultaneously. + /// Marks `activePrewarmRoots` while running so the scheduler can defer dispatch. + private func clearPrewarmActive(for rootEntityId: EntityID) { + lock.lock() + activePrewarmRoots.remove(rootEntityId) + lock.unlock() + } + + private func prewarmTexturesAsync(for rootEntityId: EntityID) { + guard textureLoadingEnabled else { return } + lock.lock() + activePrewarmRoots.insert(rootEntityId) + lock.unlock() + Task.detached(priority: .userInitiated) { + ProgressiveAssetLoader.shared.acquireAssetTextureLock(for: rootEntityId) + ProgressiveAssetLoader.shared.ensureTexturesLoaded(for: rootEntityId) + ProgressiveAssetLoader.shared.releaseAssetTextureLock(for: rootEntityId) + ProgressiveAssetLoader.shared.clearPrewarmActive(for: rootEntityId) + } + } + + /// Acquire the per-asset texture-load lock before calling makeMeshesFromCPUBuffers. + /// Returns immediately if no asset is registered for this root (e.g. non-out-of-core entity). + func acquireAssetTextureLock(for rootEntityId: EntityID) { + lock.lock() + let assetLock = assetTextureLocks[rootEntityId] + lock.unlock() + assetLock?.lock() + } + + /// Release the per-asset texture-load lock after makeMeshesFromCPUBuffers returns. + func releaseAssetTextureLock(for rootEntityId: EntityID) { + lock.lock() + let assetLock = assetTextureLocks[rootEntityId] + lock.unlock() + assetLock?.unlock() + } + + /// Call `loadTextures()` on the MDLAsset for `rootEntityId` if it has not been called yet. + /// + /// **Must be called while the per-asset texture lock is held** (i.e. between + /// `acquireAssetTextureLock` and `releaseAssetTextureLock`) to prevent two concurrent + /// uploads from both seeing `texturesLoaded == false` and both calling `loadTextures()`. + /// + /// This defers the texture decompression from parse time (where it could OOM-kill the + /// process) to first-upload time, where the app is already interactive and the RAM budget + /// is more predictable. Subsequent uploads from the same asset skip the call entirely. + func ensureTexturesLoaded(for rootEntityId: EntityID) { + lock.lock() + let alreadyLoaded = assetTexturesLoaded.contains(rootEntityId) + let asset = rootAssetRefs[rootEntityId] + lock.unlock() + + guard !alreadyLoaded, let asset else { return } + guard textureLoadingEnabled else { return } + + Logger.log( + message: "[OutOfCore] Deferred loadTextures() for root entity \(rootEntityId) — loading textures at first upload", + category: LogCategory.oocStatus.rawValue + ) + // [Instrumentation] Time the first-texture penalty: loadTextures() is only called + // once per asset, but it decodes all embedded textures synchronously. If this is + // large it confirms the first-texture setup cost as a dominant bottleneck. + let loadTexturesStart = CFAbsoluteTimeGetCurrent() + asset.loadTextures() + let loadTexturesMs = (CFAbsoluteTimeGetCurrent() - loadTexturesStart) * 1000.0 + Logger.log( + message: "[OOC-Timing] Root \(rootEntityId): loadTextures() first-texture penalty=\(String(format: "%.1f", loadTexturesMs))ms", + category: LogCategory.oocTiming.rawValue + ) + + lock.lock() + assetTexturesLoaded.insert(rootEntityId) + lock.unlock() + } + + func registerChildren(_ childIds: [EntityID], for rootEntityId: EntityID) { + lock.lock() + rootEntityChildren[rootEntityId] = childIds + lock.unlock() + } + + /// Store the rehydration context for a root entity. + /// Call this once at stub-registration time (after `storeAsset` and `registerChildren`). + func storeRootRehydrationContext(url: URL, policy: AssetLoadingPolicy, for rootEntityId: EntityID) { + lock.lock() + rootRehydrationContexts[rootEntityId] = RootRehydrationContext(url: url, loadingPolicy: policy) + lock.unlock() + } + + // MARK: Warm / Cold Lifecycle + + /// Transition a root entity from CPU-warm to CPU-cold. + /// + /// Releases the `MDLAsset` and all child `CPUMeshEntry` objects, freeing CPU-heap memory. + /// The `RootRehydrationContext` is retained so `GeometryStreamingSystem` can re-parse + /// the asset from disk when the entity next enters streaming range. + /// + /// - Note: This does NOT destroy ECS entities or GPU resources. It only frees the CPU-side + /// MDLAsset and CPU buffers. GPU-resident meshes (if any) remain until eviction. + public func releaseWarmAsset(rootEntityId: EntityID) { + lock.lock() + let children = rootEntityChildren[rootEntityId] ?? [] + rootAssetRefs.removeValue(forKey: rootEntityId) + assetTextureLocks.removeValue(forKey: rootEntityId) + assetTexturesLoaded.remove(rootEntityId) + for childId in children { + cpuMeshRegistry.removeValue(forKey: childId) + cpuLODRegistry.removeValue(forKey: childId) + } + coldRoots.insert(rootEntityId) + lock.unlock() + Logger.log( + message: "[OutOfCore] Released warm CPU data for root \(rootEntityId) (\(children.count) children) — asset is now cold", + category: LogCategory.oocStatus.rawValue + ) + } + + /// Returns `true` if the root entity is CPU-cold (warm assets were released via `releaseWarmAsset`). + func isColdRoot(_ rootEntityId: EntityID) -> Bool { + lock.lock() + defer { lock.unlock() } + return coldRoots.contains(rootEntityId) + } + + /// Returns the rehydration context for a root entity, or `nil` if none is registered. + func rehydrationContext(for rootEntityId: EntityID) -> RootRehydrationContext? { + lock.lock() + defer { lock.unlock() } + return rootRehydrationContexts[rootEntityId] + } + + /// Returns the child entity IDs for a root entity (in registration order). + func getChildren(for rootEntityId: EntityID) -> [EntityID] { + lock.lock() + defer { lock.unlock() } + return rootEntityChildren[rootEntityId] ?? [] + } + + /// Return the existing in-flight rehydration task for `rootEntityId`, or create and + /// store a new one using `factory`. Exactly one task runs per root at a time. + func getOrCreateRehydrationTask( + for rootEntityId: EntityID, + factory: () -> Task + ) -> Task { + lock.lock() + defer { lock.unlock() } + if let existing = coldRehydrationTasks[rootEntityId] { + return existing + } + let task = factory() + coldRehydrationTasks[rootEntityId] = task + return task + } + + /// Restore a root entity to warm state after successful re-hydration. + /// Removes it from `coldRoots` and clears the completed rehydration task. + func markAsWarm(rootEntityId: EntityID) { + lock.lock() + coldRoots.remove(rootEntityId) + coldRehydrationTasks.removeValue(forKey: rootEntityId) + lock.unlock() + } + + /// Remove the in-flight rehydration task for `rootEntityId` (e.g. after failure). + func clearRehydrationTask(for rootEntityId: EntityID) { + lock.lock() + coldRehydrationTasks.removeValue(forKey: rootEntityId) + lock.unlock() + } + + /// Release all CPU mesh entries and the MDLAsset for a root entity. + /// Call this when the root entity is destroyed to free CPU-heap geometry data. + public func removeOutOfCoreAsset(rootEntityId: EntityID) { + lock.lock() + let children = rootEntityChildren.removeValue(forKey: rootEntityId) ?? [] + rootAssetRefs.removeValue(forKey: rootEntityId) + assetTextureLocks.removeValue(forKey: rootEntityId) + assetTexturesLoaded.remove(rootEntityId) + for childId in children { + cpuMeshRegistry.removeValue(forKey: childId) + cpuLODRegistry.removeValue(forKey: childId) + } + // Clear warm/cold lifecycle state and prewarm tracking. + coldRoots.remove(rootEntityId) + rootRehydrationContexts.removeValue(forKey: rootEntityId) + activePrewarmRoots.remove(rootEntityId) + let task = coldRehydrationTasks.removeValue(forKey: rootEntityId) + lock.unlock() + task?.cancel() + if !children.isEmpty { + Logger.log( + message: "[OutOfCore] Released CPU mesh data for \(children.count) entities (root \(rootEntityId))", + category: LogCategory.oocStatus.rawValue + ) + } + } + + private init() {} + + // MARK: Per-Frame Tick + + /// No-op stub retained for call-site compatibility (UntoldEngine.swift, UntoldEngineXR.swift). + /// The out-of-core path registers all stubs synchronously in setEntityMeshAsync and drives + /// GPU uploads via GeometryStreamingSystem — no per-frame job processing is needed here. + public func tick() {} + + // MARK: Cancellation + + /// Release all out-of-core CPU mesh entries and the MDLAsset for every root entity. + /// Call this during scene resets or test teardown. + public func cancelAll() { + lock.lock() + cpuMeshRegistry.removeAll() + cpuLODRegistry.removeAll() + rootAssetRefs.removeAll() + rootEntityChildren.removeAll() + assetTextureLocks.removeAll() + assetTexturesLoaded.removeAll() + coldRoots.removeAll() + rootRehydrationContexts.removeAll() + activePrewarmRoots.removeAll() + let tasks = Array(coldRehydrationTasks.values) + coldRehydrationTasks.removeAll() + lock.unlock() + tasks.forEach { $0.cancel() } + Logger.log( + message: "[OutOfCore] Released all CPU mesh data (cancelAll)", + category: LogCategory.oocStatus.rawValue + ) + } +} diff --git a/Sources/UntoldEngine/Systems/RegistrationSystem.swift b/Sources/UntoldEngine/Systems/RegistrationSystem.swift index b4b3fd719..6fa6dc100 100644 --- a/Sources/UntoldEngine/Systems/RegistrationSystem.swift +++ b/Sources/UntoldEngine/Systems/RegistrationSystem.swift @@ -441,7 +441,7 @@ private func configureLODComponent(entityId: EntityID, lodLevels: [LODLevel], ac } } -private func applyWorldTransform(_ transform: simd_float4x4, to entityId: EntityID) { +func applyWorldTransform(_ transform: simd_float4x4, to entityId: EntityID) { let translation = simd_float3( transform.columns.3.x, transform.columns.3.y, @@ -714,12 +714,101 @@ private func setEntityMeshCommon( } /// Generate a stable node path for a derived mesh node -private func generateStableNodePath(assetName: String, index: Int) -> String { +func generateStableNodePath(assetName: String, index: Int) -> String { // Use a deterministic format: "Root/#" // This ensures the same USDZ file produces the same nodePath each time "Root/\(assetName)#\(index)" } +/// Register one MDLMesh leaf as an out-of-core stub entity. +/// +/// Creates the full ECS presence (transform, scenegraph, streaming component) with NO GPU +/// allocation. The `StreamingComponent` starts in `.unloaded` state with placeholder +/// radii (`Float.greatestFiniteMagnitude`) so the streaming system ignores the entity +/// until `enableStreaming()` is called and real radii are set. +/// +/// **Must be called from within an existing `withWorldMutationGate` block.** +/// The caller (setEntityMeshAsync) wraps the entire stub-registration loop in a single gate +/// acquisition rather than one gate per stub, avoiding N × acquire/release overhead for +/// assets with hundreds of mesh leaves. +/// +/// The caller is responsible for storing the MDLMesh in `ProgressiveAssetLoader.cpuMeshRegistry` +/// so `GeometryStreamingSystem.loadMeshAsync` can upload it from CPU when the entity enters range. +/// +/// - Returns: The newly created child `EntityID`. +@discardableResult +func registerProgressiveStubEntity( + mdlObject: MDLObject, + index: Int, + uniqueAssetName: String, + rootEntityId: EntityID, + url _: URL, + filename: String, + withExtension ext: String +) -> EntityID { + let childEntityId = createEntity() + + if hasComponent(entityId: childEntityId, componentType: LocalTransformComponent.self) == false { + registerTransformComponent(entityId: childEntityId) + } + + if hasComponent(entityId: childEntityId, componentType: ScenegraphComponent.self) == false { + registerSceneGraphComponent(entityId: childEntityId) + } + + // Set world position from the MDLObject's composed transform. + // This is what the octree and distance calculations will use. + let worldTransform = composedWorldTransform(for: mdlObject) + applyWorldTransform(worldTransform, to: childEntityId) + + // Seed the bounding box from the MDLMesh so OctreeSystem and calculateDistance + // compute meaningful spatial extents even before the RenderComponent exists. + if let mdlMesh = mdlObject as? MDLMesh, + let local = scene.get(component: LocalTransformComponent.self, for: childEntityId) + { + local.boundingBox = (min: mdlMesh.boundingBox.minBounds, max: mdlMesh.boundingBox.maxBounds) + } + + setEntityName(entityId: childEntityId, name: uniqueAssetName) + setParent(childId: childEntityId, parentId: rootEntityId) + + // Stable identity for serialisation / scene graph lookup. + let nodePath = generateStableNodePath(assetName: uniqueAssetName, index: index) + registerComponent(entityId: childEntityId, componentType: DerivedAssetNodeComponent.self) + if let derived = scene.get(component: DerivedAssetNodeComponent.self, for: childEntityId) { + derived.assetRootEntityId = rootEntityId + derived.nodePath = nodePath + } + + // StreamingComponent in .unloaded state — GPU resources will be created by + // GeometryStreamingSystem when the entity enters streamingRadius. + registerComponent(entityId: childEntityId, componentType: StreamingComponent.self) + if let sc = scene.get(component: StreamingComponent.self, for: childEntityId) { + sc.assetFilename = filename + sc.assetExtension = ext + sc.assetName = uniqueAssetName + sc.state = .unloaded + // Large placeholder radii: enableStreaming() sets the real values. + // This prevents the streaming system from immediately queueing a disk-based + // reload before the out-of-core CPU registry entry is in place. + sc.streamingRadius = Float.greatestFiniteMagnitude + sc.unloadRadius = Float.greatestFiniteMagnitude + } + + // Register with the octree so update() spatial queries can find this stub. + OctreeSystem.shared.registerEntity(childEntityId) + + return childEntityId +} + +/// Synchronously load and set an entity mesh on the calling thread. +/// +/// This API always uses the **immediate** path: all Metal resources are created in a single +/// pass before the function returns. It does not support out-of-core stub registration or +/// distance-based streaming — the mesh is permanently GPU-resident after this call. +/// +/// For large assets or any asset that should benefit from distance-based streaming and +/// eviction, use `setEntityMeshAsync(streamingPolicy:)` instead. public func setEntityMesh(entityId: EntityID, filename: String, withExtension: String, assetName: String? = nil, flip: Bool = true, coordinateConversion: CoordinateSystemConversion = .autoDetect) { setEntityMeshCommon( entityId: entityId, @@ -734,6 +823,28 @@ public func setEntityMesh(entityId: EntityID, filename: String, withExtension: S ) } +/// Controls how `setEntityMeshAsync` manages GPU residency for a loaded asset. +public enum MeshStreamingPolicy: Sendable { + /// Automatic: uses `ProgressiveAssetLoader.fileSizeThresholdBytes` and + /// `outOfCoreObjectCountThreshold` to decide. Large or many-object assets + /// go out-of-core; small assets upload directly. Default. + case auto + + /// Always register leaf meshes as `.unloaded` stub entities. The streaming + /// system uploads each mesh to the GPU when the camera enters `streamingRadius` + /// and evicts it when the camera moves beyond `unloadRadius`. + /// + /// The completion callback fires immediately after stub registration — no GPU + /// work happens at load time. **You must call `enableStreaming(entityId:streamingRadius:unloadRadius:)` + /// inside the completion block** so the streaming system knows the real radii. + case outOfCore + + /// Always upload directly to the GPU in a single pass. The mesh is permanently + /// resident and is never evicted by the streaming system. Use for small assets + /// that must be visible without any streaming delay (e.g. character, weapon, HUD). + case immediate +} + /// Asynchronously load and set entity mesh without blocking the main thread public func setEntityMeshAsync( entityId: EntityID, @@ -742,6 +853,7 @@ public func setEntityMeshAsync( assetName: String? = nil, flip _: Bool = true, coordinateConversion: CoordinateSystemConversion = .autoDetect, + streamingPolicy: MeshStreamingPolicy = .auto, completion: ((Bool) -> Void)? = nil ) { let completionBox = completion.map { BoolCompletionBox(callback: $0) } @@ -776,7 +888,575 @@ public func setEntityMeshAsync( return } - // Load meshes asynchronously + // MARK: Out-of-core / small-file routing + + // All assets parse with a CPU-only allocator to avoid the GPU memory spike caused + // by MTKMeshBufferAllocator pre-allocating Metal buffers for the entire scene. + // + // Two-stage admission gate (V1): + // Stage 1 (pre-parse): coarse file-size × expansion multiplier check — rejects + // obviously unsafe assets before Model I/O touches the file. + // Stage 2 (post-parse): accurate profiler-based check after parse completes — + // the final authority. Note: Stage 2 cannot prevent the + // parse-time RAM spike; it prevents all downstream work + // (stub registration, MDLAsset retention, CPU registry storage). + // + // If both gates pass, large assets register every leaf mesh immediately as a stub + // entity (zero-GPU). CPU-side MDLMesh data is stored in ProgressiveAssetLoader so + // GeometryStreamingSystem can upload each stub on demand without a disk re-read. + // Small assets create all Metal resources right here in a single pass. + if assetName == nil { + // ── Stage 1: Pre-parse admission gate ───────────────────────────────────── + // Compute file size before parseAssetAsync so the gate fires before Model I/O + // allocates CPU heap for all mesh buffers. + // + // Expansion factor: 20× — conservative upper bound for USDZ geometry + // decompression. Real-world worst case is ~55× (a 159 MB city USDZ + // expanding to ~8858 MB of geometry). 20× catches obvious outliers without + // rejecting normal-sized files. + // + // Three-zone model: + // Safe zone projectedCPU ≤ 50% RAM — allow, no log + // Soft zone projectedCPU > 50% AND < 75% RAM + // → log warning, allow parse, delegate to Stage 2 + // → expected for texture-heavy USDZs: compressed texture bytes + // in a USDZ do not expand at parse time (MDLMeshBufferData- + // Allocator only decompresses geometry; textures are decoded + // lazily at first-upload time via ensureTexturesLoaded). + // Stage 2 is the accurate authority for these borderline cases. + // Hard reject projectedCPU ≥ 75% RAM — reject before parse, load fallback + // → geometry expansion of this magnitude would risk an OOM kill + // before Stage 2 can even run. + // + // Known gap: the assetName != nil path (Mesh.loadSceneMeshesAsync) is not + // guarded. That path is only used for named-mesh lookups and is not expected + // to be called with large assets in normal production use. + // + // Future refinement: a lightweight USDZ ZIP central-directory scan could + // separate texture-entry bytes from scene-entry bytes before parsing and apply + // the 20× multiplier only to the scene portion, eliminating soft-zone false + // positives for texture-heavy assets entirely. Validate the soft-zone model + // on real assets before adding that complexity. + let fileSizeBytes = (try? FileManager.default.attributesOfItem(atPath: url.path))?[.size] as? Int ?? 0 + if fileSizeBytes > 0 { + let physicalMemory = Int(ProcessInfo.processInfo.physicalMemory) + let softZoneThreshold = Int(Double(physicalMemory) * 0.50) // soft zone starts here + let hardRejectThreshold = Int(Double(physicalMemory) * 0.75) // hard reject at or above + let projectedCPUBytes = fileSizeBytes * 20 + + let fileMB = String(format: "%.1f", Double(fileSizeBytes) / 1_048_576) + let projGB = String(format: "%.1f", Double(projectedCPUBytes) / 1_073_741_824) + let ramGB = String(format: "%.1f", Double(physicalMemory) / 1_073_741_824) + + if projectedCPUBytes >= hardRejectThreshold { + let thrGB = String(format: "%.1f", Double(hardRejectThreshold) / 1_073_741_824) + Logger.logError(message: "[AdmissionGate] Stage 1 HARD REJECT '\(filename)' — File: \(fileMB) MB | Expansion: 20× | Projected CPU: ~\(projGB) GB | Hard-reject threshold: \(thrGB) GB (75% of \(ramGB) GB RAM). Asset too large to parse safely on this device. Use a lower-polygon asset or split into smaller files.") + loadFallbackMesh(entityId: entityId, filename: filename) + await AssetLoadingState.shared.finishLoading(entityId: entityId) + completionBox?.call(false) + return + } else if projectedCPUBytes > softZoneThreshold { + let softGB = String(format: "%.1f", Double(softZoneThreshold) / 1_073_741_824) + Logger.logWarning(message: "[AdmissionGate] Stage 1 SOFT ZONE '\(filename)' — File: \(fileMB) MB | Expansion: 20× | Projected CPU: ~\(projGB) GB | Soft threshold: \(softGB) GB (50% of \(ramGB) GB RAM). Parse will proceed; Stage 2 is the authoritative gate. Typical for texture-heavy assets whose compressed texture bytes do not expand at parse time.") + // Fall through — parse proceeds. Stage 2 is the accurate authority. + } + // else: safe zone (projectedCPU ≤ softZoneThreshold) — allow, no log. + } + + guard let assetData = await Mesh.parseAssetAsync( + url: url, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + coordinateConversion: coordinateConversion + ) else { + handleError(.assetDataMissing, filename) + loadFallbackMesh(entityId: entityId, filename: filename) + await AssetLoadingState.shared.finishLoading(entityId: entityId) + completionBox?.call(false) + return + } + + // ── Stage 2: Post-parse accurate admission gate ─────────────────────────── + // AssetProfiler measures actual geometry + texture byte estimates from the + // parsed MDLMesh objects. This is the accurate gate; Stage 1 (pre-parse) is + // only a coarse early filter. + // + // IMPORTANT: by the time this check runs, parseAssetAsync() has already + // allocated CPU heap for all MDLMesh buffers. This gate cannot prevent the + // parse-time RAM spike. What it prevents is all downstream work: + // - stub registration (no ECS entities created), + // - MDLAsset retention in rootAssetRefs (no CPU RAM kept permanently), + // - CPU registry storage in ProgressiveAssetLoader. + // When the gate fires, assetData goes out of scope and ARC releases the + // parsed MDLMesh buffers, recovering the RAM that the parse consumed. + // + // The profile is computed regardless of streamingPolicy so all three policy + // modes (.auto, .outOfCore, .immediate) are subject to the same gate. + let assetProfile = AssetProfiler.profile(url: url, assetData: assetData, fileSizeBytes: fileSizeBytes) + let postParsePhysicalMemory = Int(ProcessInfo.processInfo.physicalMemory) + let postParseSafetyThreshold = Int(Double(postParsePhysicalMemory) * 0.75) + if assetProfile.estimatedGeometryBytes > postParseSafetyThreshold { + let geoGB = String(format: "%.1f", Double(assetProfile.estimatedGeometryBytes) / 1_073_741_824) + let thrGB = String(format: "%.1f", Double(postParseSafetyThreshold) / 1_073_741_824) + let ramGB = String(format: "%.1f", Double(postParsePhysicalMemory) / 1_073_741_824) + let fileMBStr = String(format: "%.1f", Double(fileSizeBytes) / 1_048_576) + Logger.logError(message: "[AdmissionGate] Stage 2 HARD REJECT '\(filename)' — File: \(fileMBStr) MB | Profiled geometry: ~\(geoGB) GB | Threshold: \(thrGB) GB (75% of \(ramGB) GB RAM). Stub registration and CPU registry storage are skipped; the parsed MDLAsset will be released by ARC. Fallback mesh assigned.") + // Load fallback so the entity is visually stable — the scene shows a + // placeholder cube rather than an invisible, mesh-less entity. + loadFallbackMesh(entityId: entityId, filename: filename) + await AssetLoadingState.shared.finishLoading(entityId: entityId) + completionBox?.call(false) + return + } + + // Resolve the effective loading policy from the caller's streamingPolicy. + // + // For .auto, AssetProfiler classifies the already-computed assetProfile + // against the live platform memory budget to select independent geometry + // and texture residency policies. + // + // For .outOfCore / .immediate, the caller's intent is mapped directly to the + // policy types for a clean internal representation. + let loadingPolicy: AssetLoadingPolicy + let outOfCoreReason: String? + switch streamingPolicy { + case .outOfCore: + loadingPolicy = .geometryStreaming + outOfCoreReason = "explicit .outOfCore policy" + case .immediate: + loadingPolicy = .fullLoad + outOfCoreReason = nil + case .auto: + let budget = MemoryBudgetManager.shared.meshBudget + loadingPolicy = AssetProfiler.classifyPolicy(profile: assetProfile, budget: budget) + + let fileMB = String(format: "%.1f", Double(fileSizeBytes) / 1_048_576) + let geoMB = String(format: "%.1f", Double(assetProfile.estimatedGeometryBytes) / 1_048_576) + let texMB = String(format: "%.1f", Double(assetProfile.estimatedTextureBytes) / 1_048_576) + let budgetMB = String(format: "%.0f", Double(budget) / 1_048_576) + Logger.log(message: "[AssetProfiler] '\(filename)' (\(fileMB) MB) → \(assetProfile.assetCharacter.rawValue) | geo ~\(geoMB) MB, tex ~\(texMB) MB | budget: \(budgetMB) MB | meshes: \(assetProfile.meshCount)") + Logger.log(message: "[AssetProfiler] Policy → geometry: \(loadingPolicy.geometryPolicy.rawValue), texture: \(loadingPolicy.texturePolicy.rawValue) (source: \(loadingPolicy.source.rawValue))") + + if loadingPolicy.geometryPolicy == .streaming { + outOfCoreReason = "\(assetProfile.assetCharacter.rawValue) asset, geo ~\(geoMB) MB on \(budgetMB) MB budget" + } else { + outOfCoreReason = nil + } + } + + // Detect LOD groups before choosing the loading path. + let topLevelNames = assetData.topLevelObjects.map { + ($0 as? MDLMesh)?.parent?.name ?? $0.name + } + let lodNameDetection = detectImportedLODGroups(fromSourceNames: topLevelNames) + let hasLODGroups = !lodNameDetection.groups.isEmpty + let useOutOfCore = loadingPolicy.geometryPolicy == .streaming + + if useOutOfCore, hasLODGroups { + // LOD + OUT-OF-CORE PATH ──────────────────────────────────────────────── + // Each LOD group becomes ONE entity with a LODComponent whose levels are + // stub LODLevels (empty mesh, .notResident). CPU-side MDLObject data for + // each level is stored in ProgressiveAssetLoader.cpuLODRegistry so + // GeometryStreamingSystem can upload only the active LOD level from RAM + // when the entity enters streaming range — no disk re-read required. + Logger.log( + message: "[OutOfCore] '\(filename)': LOD asset with \(lodNameDetection.groups.count) group(s) — LOD+OOC stub registration (\(assetData.totalObjectCount) objects)", + category: LogCategory.oocStatus.rawValue + ) + + // Build name→MDLObject map using the same naming formula as topLevelNames. + var nameToObject: [String: MDLObject] = [:] + for obj in assetData.topLevelObjects { + let name = (obj as? MDLMesh)?.parent?.name ?? obj.name + nameToObject[name] = obj + } + + let isMultiGroup = lodNameDetection.groups.count > 1 + + // Register AssetInstanceComponent on root for multi-group assets. + if isMultiGroup { + withWorldMutationGate { + registerComponent(entityId: entityId, componentType: AssetInstanceComponent.self) + if let inst = scene.get(component: AssetInstanceComponent.self, for: entityId) { + inst.assetURL = url + inst.assetName = filename + inst.importMode = "preserveHierarchy" + } + } + } + + var lodGroupEntityIds: [EntityID] = [] + lodGroupEntityIds.reserveCapacity(lodNameDetection.groups.count) + var cpuLODEntries: [(groupEntityId: EntityID, lodIndex: Int, entry: ProgressiveAssetLoader.CPUMeshEntry)] = [] + cpuLODEntries.reserveCapacity(assetData.totalObjectCount) + + let configuredDistances = LODConfig.shared.lodDistances + + withWorldMutationGate { + for (groupIdx, group) in lodNameDetection.groups.enumerated() { + // Single group: the root entity IS the LOD entity. + // Multi-group: create a child entity per group. + let groupEntityId: EntityID + if isMultiGroup { + groupEntityId = createEntity() + if hasComponent(entityId: groupEntityId, componentType: LocalTransformComponent.self) == false { + registerTransformComponent(entityId: groupEntityId) + } + if hasComponent(entityId: groupEntityId, componentType: ScenegraphComponent.self) == false { + registerSceneGraphComponent(entityId: groupEntityId) + } + setEntityName(entityId: groupEntityId, name: group.baseName) + setParent(childId: groupEntityId, parentId: entityId) + let nodePath = generateStableNodePath(assetName: group.baseName, index: groupIdx) + registerComponent(entityId: groupEntityId, componentType: DerivedAssetNodeComponent.self) + if let derived = scene.get(component: DerivedAssetNodeComponent.self, for: groupEntityId) { + derived.assetRootEntityId = entityId + derived.nodePath = nodePath + } + } else { + groupEntityId = entityId + if hasComponent(entityId: entityId, componentType: LocalTransformComponent.self) == false { + registerTransformComponent(entityId: entityId) + } + if hasComponent(entityId: entityId, componentType: ScenegraphComponent.self) == false { + registerSceneGraphComponent(entityId: entityId) + } + } + + // Seed transform and bounding box from the LOD0 MDLObject. + if let lod0Level = group.levels.first(where: { $0.lodIndex == 0 }), + let lod0Object = nameToObject[lod0Level.sourceName] + { + let worldTransform = composedWorldTransform(for: lod0Object) + applyWorldTransform(worldTransform, to: groupEntityId) + if let mdlMesh = lod0Object as? MDLMesh, + let local = scene.get(component: LocalTransformComponent.self, for: groupEntityId) + { + local.boundingBox = (min: mdlMesh.boundingBox.minBounds, max: mdlMesh.boundingBox.maxBounds) + } + } + + // Build stub LODLevels: empty mesh + .notResident for every level. + let maxLODIndex = group.levels.map(\.lodIndex).max() ?? 0 + var stubLODLevels: [LODLevel] = (0 ... maxLODIndex).map { lodIdx in + LODLevel( + mesh: [], + maxDistance: defaultLODMaxDistance(for: lodIdx, configuredDistances: configuredDistances), + url: url, + assetName: nil + ) + } + for level in group.levels { + stubLODLevels[level.lodIndex] = LODLevel( + mesh: [], + maxDistance: defaultLODMaxDistance(for: level.lodIndex, configuredDistances: configuredDistances), + url: url, + assetName: level.sourceName + ) + } + + // Configure LODComponent with stubs (nothing resident yet). + configureLODComponent(entityId: groupEntityId, lodLevels: stubLODLevels, activeLODIndex: 0) + + // StreamingComponent (.unloaded) so GeometryStreamingSystem picks this up. + registerComponent(entityId: groupEntityId, componentType: StreamingComponent.self) + if let sc = scene.get(component: StreamingComponent.self, for: groupEntityId) { + sc.assetFilename = filename + sc.assetExtension = withExtension + sc.assetName = group.baseName + sc.state = .unloaded + // Placeholder radii — enableStreaming() sets the real values. + sc.streamingRadius = Float.greatestFiniteMagnitude + sc.unloadRadius = Float.greatestFiniteMagnitude + } + + OctreeSystem.shared.registerEntity(groupEntityId) + lodGroupEntityIds.append(groupEntityId) + + // Collect CPU entries (stored outside the gate below). + for level in group.levels { + guard let obj = nameToObject[level.sourceName] else { continue } + let estimatedGPUBytes: Int = { + guard let mdlMesh = obj as? MDLMesh else { return 0 } + let stride = Int((mdlMesh.vertexDescriptor.layouts.firstObject as? MDLVertexBufferLayout)?.stride ?? 48) + return mdlMesh.vertexCount * stride + mdlMesh.vertexCount * 3 * 4 + }() + let entry = ProgressiveAssetLoader.CPUMeshEntry( + object: obj, + vertexDescriptor: vertexDescriptor.model, + textureLoader: assetData.textureLoader, + device: renderInfo.device, + url: url, + filename: filename, + withExtension: withExtension, + uniqueAssetName: level.sourceName, + estimatedGPUBytes: estimatedGPUBytes, + residencyPolicy: loadingPolicy + ) + cpuLODEntries.append((groupEntityId, level.lodIndex, entry)) + } + } + } + + // Store CPU LOD entries outside the gate (lock-based, no ECS mutation). + for (groupEntityId, lodIdx, entry) in cpuLODEntries { + ProgressiveAssetLoader.shared.storeCPULODMesh(entry, for: groupEntityId, lodIndex: lodIdx) + } + + // Keep MDLAsset alive so MDLMeshBufferDataAllocator is not prematurely released. + ProgressiveAssetLoader.shared.storeAsset(assetData.asset, for: entityId) + ProgressiveAssetLoader.shared.registerChildren(lodGroupEntityIds, for: entityId) + ProgressiveAssetLoader.shared.storeRootRehydrationContext(url: url, policy: loadingPolicy, for: entityId) + + Logger.log( + message: "[OutOfCore] '\(filename)': \(lodGroupEntityIds.count) LOD group entities registered — GeometryStreamingSystem will upload active LOD on demand", + category: LogCategory.oocStatus.rawValue + ) + + await AssetLoadingState.shared.finishLoading(entityId: entityId) + completionBox?.call(true) + return + } + + if useOutOfCore { + // OUT-OF-CORE PATH ────────────────────────────────────────────────────── + // Register ALL leaf meshes immediately as .unloaded stub entities (ECS-only, + // no GPU allocation). Each stub's MDLMesh data is stored in the CPU registry + // so GeometryStreamingSystem can upload it from RAM when the entity enters + // streaming range — no disk re-read required. + // + // This replaces the old ProgressiveLoadJob / tick() approach: + // Old: upload nearest N → skip rest → skipped entities permanently absent + // New: all entities present from the start, streaming drives GPU residency + Logger.log( + message: "[OutOfCore] '\(filename)': \(outOfCoreReason ?? "policy") → out-of-core stub registration (\(assetData.totalObjectCount) stubs)", + category: LogCategory.oocStatus.rawValue + ) + + // Register AssetInstanceComponent on the root entity so scene-graph + // serialisation can identify this as a multi-mesh asset instance. + withWorldMutationGate { + let assetInstanceComp = AssetInstanceComponent( + assetURL: url, + assetName: filename, + importMode: "preserveHierarchy", + rootPrimPath: nil + ) + registerComponent(entityId: entityId, componentType: AssetInstanceComponent.self) + if let instanceComp = scene.get(component: AssetInstanceComponent.self, for: entityId) { + instanceComp.assetURL = assetInstanceComp.assetURL + instanceComp.assetName = assetInstanceComp.assetName + instanceComp.importMode = assetInstanceComp.importMode + instanceComp.rootPrimPath = assetInstanceComp.rootPrimPath + } + } + + // Register ALL stub entities inside a single withWorldMutationGate. + // Batching N stubs into one gate acquisition avoids N × acquire/release + // overhead on assets with hundreds of mesh leaves (e.g. 500 buildings). + var childEntityIds: [EntityID] = [] + childEntityIds.reserveCapacity(assetData.totalObjectCount) + var cpuEntries: [(EntityID, ProgressiveAssetLoader.CPUMeshEntry)] = [] + cpuEntries.reserveCapacity(assetData.totalObjectCount) + + withWorldMutationGate { + for (i, obj) in assetData.topLevelObjects.enumerated() { + let baseName = (obj as? MDLMesh)?.parent?.name ?? obj.name + let uniqueAssetName = "\(baseName)#\(i)" + + let childId = registerProgressiveStubEntity( + mdlObject: obj, + index: i, + uniqueAssetName: uniqueAssetName, + rootEntityId: entityId, + url: url, + filename: filename, + withExtension: withExtension + ) + + // Estimate GPU bytes from MDLMesh vertex/index counts. + // Used by GeometryStreamingSystem for pre-emptive budget reservation + // before starting a CPU→Metal upload, so the budget gate fires before + // a load rather than reacting after allocation. + let estimatedGPUBytes: Int = { + guard let mdlMesh = obj as? MDLMesh else { return 0 } + let stride = Int((mdlMesh.vertexDescriptor.layouts.firstObject as? MDLVertexBufferLayout)?.stride ?? 48) + let vertexBytes = mdlMesh.vertexCount * stride + // Approximate: ~3 indices per vertex (conservative, no sharing assumed) + let indexBytes = mdlMesh.vertexCount * 3 * 4 + return vertexBytes + indexBytes + }() + + let entry = ProgressiveAssetLoader.CPUMeshEntry( + object: obj, + vertexDescriptor: vertexDescriptor.model, + textureLoader: assetData.textureLoader, + device: renderInfo.device, + url: url, + filename: filename, + withExtension: withExtension, + uniqueAssetName: uniqueAssetName, + estimatedGPUBytes: estimatedGPUBytes, + residencyPolicy: loadingPolicy + ) + cpuEntries.append((childId, entry)) + childEntityIds.append(childId) + } + } + + // Store CPU entries outside the gate (lock-based, no ECS mutation). + for (childId, entry) in cpuEntries { + ProgressiveAssetLoader.shared.storeCPUMesh(entry, for: childId) + } + + // Keep MDLAsset alive so the MDLMeshBufferDataAllocator backing all + // child CPU buffers is not released prematurely. + ProgressiveAssetLoader.shared.storeAsset(assetData.asset, for: entityId) + ProgressiveAssetLoader.shared.registerChildren(childEntityIds, for: entityId) + + // Store URL + policy so GeometryStreamingSystem can re-parse from disk if + // releaseWarmAsset() transitions this asset to CPU-cold in a future frame. + ProgressiveAssetLoader.shared.storeRootRehydrationContext( + url: url, + policy: loadingPolicy, + for: entityId + ) + + Logger.log( + message: "[OutOfCore] '\(filename)': \(assetData.totalObjectCount) stubs registered — GeometryStreamingSystem will upload on demand", + category: LogCategory.oocStatus.rawValue + ) + + // Release the loading gate immediately — no GPU work happens here. + await AssetLoadingState.shared.finishLoading(entityId: entityId) + completionBox?.call(true) + return + } + + // SMALL-FILE FAST PATH (CPU-parsed) ──────────────────────────────────────── + // File is below the size threshold: create all mesh groups from the + // CPU-parsed data right now, then continue with the normal registration code below. + // Must use makeMeshesFromCPUBuffers (not makeMeshes) because parseAssetAsync + // uses MDLMeshBufferDataAllocator — CPU-heap buffers that MTKMesh(mesh:device:) + // cannot accept directly (MTKModelErrorNoMTLBuffer). makeMeshesFromCPUBuffers + // copies each buffer to a fresh MTKMeshBufferAllocator-backed buffer first. + // + // parseAssetAsync intentionally skips loadTextures() to defer the decompression + // cost. The OOC path calls ensureTexturesLoaded() in uploadFromCPUEntry before + // makeMeshesFromCPUBuffers. This fast path bypasses that route, so we must call + // loadTextures() here to ensure USDZ-embedded textures are decoded — otherwise + // MTKTextureLoader cannot read the pixel data and all textures silently fail. + assetData.asset.loadTextures() + let smallAssetMeshes: [[Mesh]] = assetData.topLevelObjects.map { obj in + Mesh.makeMeshesFromCPUBuffers( + object: obj, + vertexDescriptor: vertexDescriptor.model, + textureLoader: assetData.textureLoader, + device: renderInfo.device, + flip: true + ) + } + MeshResourceManager.shared.cacheLoadedMeshes(url: url, meshArrays: smallAssetMeshes) + + // Continue to the validation + registration block below using these meshes. + // ─── SMALL-ASSET CONTINUATION ────────────────────────────────────────────── + let meshes = smallAssetMeshes + + if meshes.isEmpty { + handleError(.assetDataMissing, filename) + loadFallbackMesh(entityId: entityId, filename: filename) + await AssetLoadingState.shared.finishLoading(entityId: entityId) + completionBox?.call(false) + return + } + + let nonEmptyMeshes = meshes.filter { !$0.isEmpty } + + // assetName is nil here (progressive path requires nil assetName). + + await AssetLoadingState.shared.updateProgress(entityId: entityId, currentMesh: 0, totalMeshes: nonEmptyMeshes.count, phase: .registering) + + var loadingEntityIds: [EntityID] = [entityId] + + let handledImportedLOD = tryRegisterImportedLODGroup( + entityId: entityId, + url: url, + filename: filename, + withExtension: withExtension, + nonEmptyMeshes: nonEmptyMeshes + ) + + if handledImportedLOD { + await AssetLoadingState.shared.updateProgress(entityId: entityId, currentMesh: nonEmptyMeshes.count, totalMeshes: nonEmptyMeshes.count) + } else if nonEmptyMeshes.count == 1 { + let mesh = nonEmptyMeshes[0] + associateMeshesToEntity(entityId: entityId, meshes: mesh) + registerRenderComponent(entityId: entityId, meshes: mesh, url: url, assetName: mesh.first!.assetName) + setEntitySkeleton(entityId: entityId, filename: filename, withExtension: withExtension) + if let renderComp = scene.get(component: RenderComponent.self, for: entityId) { + renderComp.isVisible = false + } + await AssetLoadingState.shared.updateProgress(entityId: entityId, currentMesh: 1, totalMeshes: 1) + } else if nonEmptyMeshes.count > 1 { + let assetInstanceComp = AssetInstanceComponent( + assetURL: url, + assetName: filename, + importMode: "preserveHierarchy", + rootPrimPath: nil + ) + registerComponent(entityId: entityId, componentType: AssetInstanceComponent.self) + if let instanceComp = scene.get(component: AssetInstanceComponent.self, for: entityId) { + instanceComp.assetURL = assetInstanceComp.assetURL + instanceComp.assetName = assetInstanceComp.assetName + instanceComp.importMode = assetInstanceComp.importMode + instanceComp.rootPrimPath = assetInstanceComp.rootPrimPath + } + for (index, mesh) in nonEmptyMeshes.enumerated() { + let childEntityId = createEntity() + if hasComponent(entityId: childEntityId, componentType: LocalTransformComponent.self) == false { + registerTransformComponent(entityId: childEntityId) + } + if hasComponent(entityId: childEntityId, componentType: ScenegraphComponent.self) == false { + registerSceneGraphComponent(entityId: childEntityId) + } + if let firstMesh = mesh.first { + applyWorldTransform(firstMesh.worldSpace, to: childEntityId) + } + associateMeshesToEntity(entityId: childEntityId, meshes: mesh) + registerRenderComponent(entityId: childEntityId, meshes: mesh, url: url, assetName: mesh.first!.assetName) + let meshAssetName = mesh.first!.assetName + setEntityName(entityId: childEntityId, name: meshAssetName) + setParent(childId: childEntityId, parentId: entityId) + let nodePath = generateStableNodePath(assetName: meshAssetName, index: index) + let derivedComp = DerivedAssetNodeComponent(assetRootEntityId: entityId, nodePath: nodePath) + registerComponent(entityId: childEntityId, componentType: DerivedAssetNodeComponent.self) + if let derived = scene.get(component: DerivedAssetNodeComponent.self, for: childEntityId) { + derived.assetRootEntityId = derivedComp.assetRootEntityId + derived.nodePath = derivedComp.nodePath + } + setEntitySkeleton(entityId: childEntityId, filename: filename, withExtension: withExtension) + if let renderComp = scene.get(component: RenderComponent.self, for: childEntityId) { + renderComp.isVisible = false + } + loadingEntityIds.append(childEntityId) + await AssetLoadingState.shared.updateProgress(entityId: entityId, currentMesh: index + 1, totalMeshes: nonEmptyMeshes.count) + } + } + + for id in loadingEntityIds { + if let renderComp = scene.get(component: RenderComponent.self, for: id) { + renderComp.isVisible = true + } + } + + await AssetLoadingState.shared.finishLoading(entityId: entityId) + completionBox?.call(true) + return + } + + // ORIGINAL PATH (assetName specified, or progressive loading disabled) ────────── + // Uses MTKMeshBufferAllocator: all Metal buffers allocated at parse time. + // Kept for named-mesh lookups and fallback when progressive loading is off. let meshes = await Mesh.loadSceneMeshesAsync( url: url, vertexDescriptor: vertexDescriptor.model, @@ -2248,13 +2928,17 @@ public func enableStreaming( return } - // No direct RenderComponent - check children (multi-mesh asset) + // No direct RenderComponent — check children. + // Handles both loaded multi-mesh assets (children have RenderComponent) and + // out-of-core stub assets (children have StreamingComponent but no RenderComponent yet). if let sceneGraph = scene.get(component: ScenegraphComponent.self, for: entityId), !sceneGraph.children.isEmpty { var enabledCount = 0 for childId in sceneGraph.children { - if scene.get(component: RenderComponent.self, for: childId) != nil { + let hasRender = scene.get(component: RenderComponent.self, for: childId) != nil + let hasStreaming = scene.get(component: StreamingComponent.self, for: childId) != nil + if hasRender || hasStreaming { enableStreamingForSingleEntity( entityId: childId, streamingRadius: streamingRadius, @@ -2267,7 +2951,7 @@ public func enableStreaming( if enabledCount > 0 { Logger.log(message: "✅ Enabled streaming for \(enabledCount) child entities") } else { - Logger.logWarning(message: "Cannot enable streaming: entity \(entityId) has no children with RenderComponents") + Logger.logWarning(message: "Cannot enable streaming: entity \(entityId) has no children with RenderComponent or StreamingComponent") } return } @@ -2276,19 +2960,45 @@ public func enableStreaming( } } -/// Internal helper to enable streaming on a single entity with a RenderComponent +/// Internal helper to enable streaming on a single entity. +/// Handles two cases: +/// - Loaded entity (has RenderComponent): state stays `.loaded`, registered as loaded. +/// - Out-of-core stub (StreamingComponent only, no RenderComponent): state stays `.unloaded`, +/// only the radii are updated so GeometryStreamingSystem can start distance checks. private func enableStreamingForSingleEntity( entityId: EntityID, streamingRadius: Float, unloadRadius: Float, priority: Int ) { + // Out-of-core stub path: entity has a StreamingComponent from stub registration + // but no RenderComponent yet. Just update the radii — the streaming system will + // upload the mesh from the CPU registry when the entity enters streamingRadius. + if let streaming = scene.get(component: StreamingComponent.self, for: entityId), + scene.get(component: RenderComponent.self, for: entityId) == nil + { + streaming.streamingRadius = streamingRadius + streaming.unloadRadius = unloadRadius + streaming.priority = priority + // State remains .unloaded — GeometryStreamingSystem drives the first upload. + return + } + guard let render = scene.get(component: RenderComponent.self, for: entityId) else { return } - // Register streaming component - registerComponent(entityId: entityId, componentType: StreamingComponent.self) + // Register streaming component only if not already present. + // scene.assign() always calls typedPointer.initialize(to: T()), which resets an + // existing StreamingComponent to its default .unloaded state. If a progressive-load + // child entity already has a .loaded StreamingComponent (added by + // registerProgressiveChildEntity), calling registerComponent again would briefly + // set it to .unloaded — creating a race with GeometryStreamingSystem.update() on + // the compositor render thread, which would see the .unloaded state and immediately + // queue a full reload of the USDZ file. + if scene.get(component: StreamingComponent.self, for: entityId) == nil { + registerComponent(entityId: entityId, componentType: StreamingComponent.self) + } guard let streaming = scene.get(component: StreamingComponent.self, for: entityId) else { return @@ -2298,14 +3008,14 @@ private func enableStreamingForSingleEntity( let url = render.assetURL streaming.assetFilename = url.deletingPathExtension().lastPathComponent streaming.assetExtension = url.pathExtension - streaming.assetName = render.assetName // The specific mesh name within the USDZ + streaming.assetName = render.assetName streaming.streamingRadius = streamingRadius streaming.unloadRadius = unloadRadius streaming.priority = priority streaming.state = .loaded // Already has mesh - // Register with streaming system for tracking + // Register with streaming system for eviction tracking GeometryStreamingSystem.shared.registerLoadedEntity(entityId) } diff --git a/Sources/UntoldEngine/Systems/RenderingSystem.swift b/Sources/UntoldEngine/Systems/RenderingSystem.swift index f7efa0ebf..f123c681e 100644 --- a/Sources/UntoldEngine/Systems/RenderingSystem.swift +++ b/Sources/UntoldEngine/Systems/RenderingSystem.swift @@ -777,29 +777,89 @@ func outputTransformCustomization(encoder: MTLRenderCommandEncoder) { ) } -public let lookRenderPass: RenderPasses.RenderPassExecution = { commandBuffer in - guard let sourceTexture = textureResources.sceneCompositeTexture else { - handleError(.renderPassCreationFailed, "Look Pass: source texture is nil") - return +private func debugSourceTexture(for mode: RenderDebugViewMode) -> MTLTexture? { + switch mode { + case .lit: + return textureResources.sceneCompositeTexture + case .albedo: + return textureResources.colorMap + case .normal: + return textureResources.normalMap + case .depth: + return textureResources.colorMap ?? textureResources.sceneCompositeTexture + case .ssaoBlurred: + return textureResources.ssaoBlurTexture } +} + +public let lookRenderPass: RenderPasses.RenderPassExecution = { commandBuffer in + let viewMode = renderDebugViewMode guard let destinationTexture = textureResources.lookTexture else { handleError(.renderPassCreationFailed, "Look Pass: destination texture is nil") return } - guard let pipeline = PipelineManager.shared.renderPipelinesByType[.look] else { - handleError(.pipelineStateNulled, "Look Pipeline is nil") + + if viewMode == .lit { + guard let sourceTexture = textureResources.sceneCompositeTexture else { + handleError(.renderPassCreationFailed, "Look Pass: source texture is nil") + return + } + guard let pipeline = PipelineManager.shared.renderPipelinesByType[.look] else { + handleError(.pipelineStateNulled, "Look Pipeline is nil") + return + } + if !pipeline.success { + handleError(.pipelineStateNulled, pipeline.name ?? "Look Pipeline") + return + } + + RenderPasses.executePostProcess( + pipeline, + source: sourceTexture, + destination: destinationTexture, + customization: colorGradingCustomization + )(commandBuffer) return } - if !pipeline.success { - handleError(.pipelineStateNulled, pipeline.name ?? "Look Pipeline") + + guard let debugSource = debugSourceTexture(for: viewMode) else { + handleError(.renderPassCreationFailed, "Debug View Pass: source texture is nil") + return + } + guard let debugDepth = renderInfo.offscreenRenderPassDescriptor?.depthAttachment.texture ?? textureResources.depthMap else { + handleError(.renderPassCreationFailed, "Debug View Pass: depth texture is nil") + return + } + guard let debugPipeline = PipelineManager.shared.renderPipelinesByType[.debug] else { + handleError(.pipelineStateNulled, "Debug Pipeline is nil") + return + } + if !debugPipeline.success { + handleError(.pipelineStateNulled, debugPipeline.name ?? "Debug Pipeline") return } RenderPasses.executePostProcess( - pipeline, - source: sourceTexture, + debugPipeline, + source: debugSource, destination: destinationTexture, - customization: colorGradingCustomization + customization: { encoder in + var mode = Int32(viewMode.rawValue) + encoder.setFragmentBytes( + &mode, + length: MemoryLayout.stride, + index: Int(debugPassModeIndex.rawValue) + ) + + var frustumPlanes = simd_float2(near, far) + encoder.setFragmentBytes( + &frustumPlanes, + length: MemoryLayout.stride, + index: Int(debugPassFrustumPlanesIndex.rawValue) + ) + + encoder.setFragmentTexture(debugDepth, index: 1) + } )(commandBuffer) } diff --git a/Sources/UntoldEngine/Systems/SystemEvents.swift b/Sources/UntoldEngine/Systems/SystemEvents.swift index 94556632e..1781bfd4d 100644 --- a/Sources/UntoldEngine/Systems/SystemEvents.swift +++ b/Sources/UntoldEngine/Systems/SystemEvents.swift @@ -250,7 +250,10 @@ public final class SystemIntegrationMonitor: @unchecked Sendable { lock.unlock() if shouldLog { - Logger.log(message: "[Integration] Loads: \(snapshot.streamingLoadsThisSecond), Unloads: \(snapshot.streamingUnloadsThisSecond), LOD switches: \(snapshot.lodSwitchesThisSecond), Fallbacks: \(snapshot.lodFallbacksThisSecond), Batch rebuilds: \(snapshot.batchRebuildsThisSecond)") + Logger.log( + message: "[Integration] Loads: \(snapshot.streamingLoadsThisSecond), Unloads: \(snapshot.streamingUnloadsThisSecond), LOD switches: \(snapshot.lodSwitchesThisSecond), Fallbacks: \(snapshot.lodFallbacksThisSecond), Batch rebuilds: \(snapshot.batchRebuildsThisSecond)", + category: LogCategory.integration.rawValue + ) } } diff --git a/Sources/UntoldEngine/Systems/TextureStreamingSystem.swift b/Sources/UntoldEngine/Systems/TextureStreamingSystem.swift index 2e5bf9b30..adbee5d62 100644 --- a/Sources/UntoldEngine/Systems/TextureStreamingSystem.swift +++ b/Sources/UntoldEngine/Systems/TextureStreamingSystem.swift @@ -24,6 +24,9 @@ import simd /// /// Heavy work (I/O + GPU resampling) runs asynchronously. /// ECS mutations are applied inside `withWorldMutationGate`. +/// +/// Threading: `update()` and all configuration mutations must be called +/// from the same thread (typically the game loop / main thread). public class TextureStreamingSystem: @unchecked Sendable { public static let shared = TextureStreamingSystem() @@ -45,15 +48,90 @@ public class TextureStreamingSystem: @unchecked Sendable { // MARK: - Configuration + // MARK: - Profiles + + /// Built-in tuning presets for common scene types. + /// + /// Apply at setup time via `TextureStreamingSystem.shared.apply(.archviz)`. + /// Individual properties can be overridden after applying a profile. + public enum Profile { + /// Indoor archviz scenes (living rooms, kitchens, offices). + /// + /// Prioritises quality over memory: full res within arm's reach, + /// high minimum (512 px) because "distant" objects in a room are + /// still large on screen, wide hysteresis to avoid mid-walkthrough + /// transitions, and more concurrent ops since ops are GPU-bound. + /// + /// Typical room depth: 4–7 m. + case archviz + + /// Large outdoor / open-world scenes (cities, landscapes, terrain). + /// + /// Spreads three tiers across a much wider distance range so that + /// GPU memory stays bounded while the camera traverses hundreds of + /// metres. Minimum stays at 256 px — distant objects are tiny. + case openWorld + + /// Balanced default for mid-size interior or mixed scenes. + /// + /// Suitable as a starting point when the scene type is unknown. + case balanced + } + + /// Apply a built-in tuning profile, overwriting all streaming parameters. + /// + /// ```swift + /// // At scene init: + /// TextureStreamingSystem.shared.apply(.archviz) + /// + /// // Fine-tune a single value afterwards if needed: + /// TextureStreamingSystem.shared.upgradeRadius = 3.0 + /// ``` + public func apply(_ profile: Profile) { + switch profile { + case .archviz: + // Full res within inspection distance; almost nothing in a living + // room is beyond 6 m, so the minimum tier fires rarely. + upgradeRadius = 2.5 + downgradeRadius = 6.0 + maxTextureDimension = 1024 + minimumTextureDimension = 512 // 256 px looks muddy on a wall at 5 m + hysteresisFraction = 0.20 // wider dead band — no transitions mid-walkthrough + maxConcurrentOps = 6 // GPU-bound; no disk I/O on warm path + updateInterval = 0.1 // more responsive at walking speed + + case .openWorld: + // Spread tiers across a city-block / landscape scale. + upgradeRadius = 15.0 + downgradeRadius = 60.0 + maxTextureDimension = 1024 + minimumTextureDimension = 256 // distant objects are tiny on screen + hysteresisFraction = 0.15 + maxConcurrentOps = 3 + updateInterval = 0.2 + + case .balanced: + upgradeRadius = 12.0 + downgradeRadius = 20.0 + maxTextureDimension = TextureStreamingSystem.platformDefaultMaxTextureDimension + minimumTextureDimension = TextureStreamingSystem.platformDefaultMinimumTextureDimension + hysteresisFraction = 0.15 + maxConcurrentOps = 3 + updateInterval = 0.2 + } + } + + // MARK: - Configuration + /// Enable/disable texture streaming public var enabled: Bool = true /// Textures closer than this distance stream to full resolution. - public var upgradeRadius: Float = 30.0 + public var upgradeRadius: Float = 12.0 /// Textures between `upgradeRadius` and `downgradeRadius` stream to `maxTextureDimension`. /// Textures beyond `downgradeRadius` stream to `minimumTextureDimension`. - public var downgradeRadius: Float = 60.0 + public var downgradeRadius: Float = 20.0 /// Mid-distance max dimension. public var maxTextureDimension: Int = TextureStreamingSystem.platformDefaultMaxTextureDimension @@ -64,12 +142,24 @@ public class TextureStreamingSystem: @unchecked Sendable { /// How often to evaluate texture streaming (seconds) public var updateInterval: Float = 0.2 - /// Print to console when texture resolution changes. - public var verboseLogging: Bool = true + /// Print resolution-change events to the console. + public var verboseLogging: Bool = false /// Maximum concurrent texture streaming operations. public var maxConcurrentOps: Int = 3 + /// Hysteresis fraction applied to each tier boundary to prevent oscillation. + /// + /// An entity must move `hysteresisFraction × radius` *past* a tier boundary before a + /// direction change is issued. This creates an asymmetric dead band: + /// - Upgrade: requires distance < boundary × (1 - hysteresisFraction) + /// - Downgrade: requires distance > boundary × (1 + hysteresisFraction) + /// + /// At the default of 0.15, an entity at the `downgradeRadius = 20 m` boundary must + /// reach 17 m before upgrading and 23 m before downgrading. This eliminates per-tick + /// oscillation for entities hovering near a tier boundary. + public var hysteresisFraction: Float = 0.15 + // MARK: - State private var timeSinceLastUpdate: Float = 0 @@ -81,14 +171,33 @@ public class TextureStreamingSystem: @unchecked Sendable { private let lock = NSLock() /// Reusable command queue for GPU resampling. + /// Initialized once in `scheduleResolutionChange` before any Task is spawned. private var commandQueue: MTLCommandQueue? + /// Reusable texture loader. + /// Initialized once in `scheduleResolutionChange` before any Task is spawned. + private var textureLoader: MTKTextureLoader? + // MARK: - Stats private var totalUpgrades: Int = 0 private var totalDowngrades: Int = 0 - private init() {} + private init() { + // When an entity's LOD switches, its render.mesh is swapped to a different + // LOD level's mesh array. Any previously streamed textures live in the old + // mesh and are now unreachable. Remove the entity from upgradedEntities so + // the next update() tick re-evaluates and re-streams the new LOD's meshes. + SystemEventBus.shared.subscribeToLODChanges { [weak self] event in + self?.handleLODChange(event) + } + } + + private func handleLODChange(_ event: EntityLODChangedEvent) { + lock.lock() + upgradedEntities.remove(event.entityId) + lock.unlock() + } // MARK: - Resolution Model @@ -196,14 +305,19 @@ public class TextureStreamingSystem: @unchecked Sendable { // Keep non-visible downgrade tracking current for entities we visit. setTrackedAboveMinimum(entityId, isAboveMinimum: entityHasTexturesAboveMinimumTier(entityId: entityId)) - let targetMaxDimension = desiredMaxDimension(distance: distance, isVisible: true) - guard entityNeedsResolutionChange(entityId: entityId, targetMaxDimension: targetMaxDimension) else { continue } + let targetMaxDimension = lodAwareMaxDimension(entityId: entityId, distanceBased: desiredMaxDimension(distance: distance)) + let workItems = buildWorkItems(entityId: entityId, targetMaxDimension: targetMaxDimension, distance: distance) + guard !workItems.isEmpty else { continue } - scheduleResolutionChange(entityId: entityId, distance: distance, targetMaxDimension: targetMaxDimension, isVisible: true) + let capturedLOD = scene.get(component: LODComponent.self, for: entityId)?.currentLOD + scheduleResolutionChange(entityId: entityId, distance: distance, workItems: workItems, targetMaxDimension: targetMaxDimension, isVisible: true, capturedLOD: capturedLOD) opsScheduled += 1 } - // Priority 2: entities no longer visible should settle to minimum mip tier. + // Priority 2: entities no longer visible should settle to the appropriate tier + // based on their actual distance. Do NOT assume minimum — a nearby entity that + // left the frustum (e.g. camera rotation) should keep its high-res texture so + // there is no quality drop when the camera rotates back. lock.lock() let upgradedSnapshot = Array(upgradedEntities) lock.unlock() @@ -220,15 +334,18 @@ public class TextureStreamingSystem: @unchecked Sendable { continue } - let targetMaxDimension = desiredMaxDimension(distance: .infinity, isVisible: false) - guard entityNeedsResolutionChange(entityId: entityId, targetMaxDimension: targetMaxDimension) else { + let distance = calculateDistance(entityId: entityId, cameraPosition: effectiveCameraPosition) + let targetMaxDimension = lodAwareMaxDimension(entityId: entityId, distanceBased: desiredMaxDimension(distance: distance)) + let workItems = buildWorkItems(entityId: entityId, targetMaxDimension: targetMaxDimension, distance: distance) + guard !workItems.isEmpty else { lock.lock() upgradedEntities.remove(entityId) lock.unlock() continue } - scheduleResolutionChange(entityId: entityId, distance: -1, targetMaxDimension: targetMaxDimension, isVisible: false) + let capturedLOD = scene.get(component: LODComponent.self, for: entityId)?.currentLOD + scheduleResolutionChange(entityId: entityId, distance: distance, workItems: workItems, targetMaxDimension: targetMaxDimension, isVisible: false, capturedLOD: capturedLOD) opsScheduled += 1 } } @@ -245,16 +362,45 @@ public class TextureStreamingSystem: @unchecked Sendable { /// Returns desired max dimension for the entity at this distance. /// `nil` means full source resolution. - private func desiredMaxDimension(distance: Float, isVisible: Bool) -> Int? { - if isVisible, distance <= upgradeRadius { + /// + /// Tier is determined by distance alone. Visibility is not a factor here — + /// an entity behind the camera may still be very close, and downgrading it + /// to minimum just because it left the frustum causes a visible quality drop + /// when the camera rotates back. + private func desiredMaxDimension(distance: Float) -> Int? { + if distance <= upgradeRadius { return nil } - if isVisible, distance <= downgradeRadius { + if distance <= downgradeRadius { return normalizedMediumDimension() } return normalizedMinimumDimension() } + /// Returns the LOD-aware desired max dimension for an entity. + /// + /// For LOD-enabled entities, caps texture quality based on the active LOD index + /// so the system never streams full-res textures onto low-poly geometry: + /// - LOD 0 (highest detail): quality driven by distance + /// - LOD 1 … n-2 (intermediate): cap at `maxTextureDimension` + /// - LOD n-1 (lowest detail): cap at `minimumTextureDimension` + private func lodAwareMaxDimension(entityId: EntityID, distanceBased: Int?) -> Int? { + guard let lodComponent = scene.get(component: LODComponent.self, for: entityId), + lodComponent.lodLevels.count > 1 + else { return distanceBased } + + let activeLOD = lodComponent.currentLOD + guard activeLOD > 0 else { return distanceBased } + + let isLowestDetail = activeLOD == lodComponent.lodLevels.count - 1 + let lodCap = isLowestDetail ? normalizedMinimumDimension() : normalizedMediumDimension() + + if let distDim = distanceBased { + return min(distDim, lodCap) + } + return lodCap + } + private func calculateDistance(entityId: EntityID, cameraPosition: simd_float3) -> Float { guard let transform = scene.get(component: WorldTransformComponent.self, for: entityId), let local = scene.get(component: LocalTransformComponent.self, for: entityId) @@ -333,13 +479,16 @@ public class TextureStreamingSystem: @unchecked Sendable { return slots } - private func buildWorkItems(entityId: EntityID, targetMaxDimension: Int?) -> [StreamWorkItem] { + private func buildWorkItems(entityId: EntityID, targetMaxDimension: Int?, distance: Float) -> [StreamWorkItem] { let slots = streamableSlots(entityId: entityId) guard !slots.isEmpty else { return [] } var workItems: [StreamWorkItem] = [] workItems.reserveCapacity(slots.count) + let h = hysteresisFraction + let medium = normalizedMediumDimension() + for slot in slots { let currentMax = max(slot.currentTexture.width, slot.currentTexture.height) let desiredMax = min(targetMaxDimension ?? slot.sourceMaxDimension, slot.sourceMaxDimension) @@ -353,6 +502,39 @@ public class TextureStreamingSystem: @unchecked Sendable { continue } + // Hysteresis dead band: suppress tier crossings when the entity is within + // `h × radius` of a tier boundary. This prevents per-tick oscillation for + // entities hovering near upgradeRadius or downgradeRadius. + // + // Upgrade is suppressed if the entity hasn't moved sufficiently inside the + // higher-resolution zone yet (distance still too large relative to boundary). + // Downgrade is suppressed if the entity hasn't moved sufficiently outside the + // higher-resolution zone yet (distance still too small relative to boundary). + // + // Memory-pressure callers pass Float.greatestFiniteMagnitude so that the + // downgrade suppress conditions (`distance < threshold`) are never true, + // effectively bypassing hysteresis for forced evictions. + let suppress: Bool + switch direction { + case .upgrade: + if desiredMax >= slot.sourceMaxDimension { + // Upgrading toward full resolution: boundary is at upgradeRadius. + suppress = distance > upgradeRadius * (1.0 - h) + } else { + // Upgrading toward medium resolution: boundary is at downgradeRadius. + suppress = distance > downgradeRadius * (1.0 - h) + } + case .downgrade: + if currentMax > medium { + // Downgrading from full resolution: boundary is at upgradeRadius. + suppress = distance < upgradeRadius * (1.0 + h) + } else { + // Downgrading from medium resolution: boundary is at downgradeRadius. + suppress = distance < downgradeRadius * (1.0 + h) + } + } + if suppress { continue } + let target: Int? = desiredMax >= slot.sourceMaxDimension ? nil : desiredMax workItems.append(StreamWorkItem(slot: slot, direction: direction, targetMaxDimension: target)) } @@ -360,10 +542,6 @@ public class TextureStreamingSystem: @unchecked Sendable { return workItems } - private func entityNeedsResolutionChange(entityId: EntityID, targetMaxDimension: Int?) -> Bool { - !buildWorkItems(entityId: entityId, targetMaxDimension: targetMaxDimension).isEmpty - } - private func entityHasTexturesAboveMinimumTier(entityId: EntityID) -> Bool { let minDim = normalizedMinimumDimension() let slots = streamableSlots(entityId: entityId) @@ -376,37 +554,136 @@ public class TextureStreamingSystem: @unchecked Sendable { return false } + // MARK: - Budget Helpers + + /// Estimate the net GPU byte increase for a set of upgrade work items. + /// + /// Uses `dim × dim × 4 bytes/pixel × 4/3` (RGBA8 + mip overhead) as a + /// conservative upper bound. The estimate intentionally over-counts slightly + /// so the budget check errs on the side of caution. + /// + /// Only upgrade items contribute to the estimate — downgrades free memory + /// and are always allowed regardless of budget state. + private func estimatedUpgradeBytes(_ workItems: [StreamWorkItem]) -> Int { + workItems.reduce(0) { total, item in + guard item.direction == .upgrade else { return total } + let newDim = item.targetMaxDimension ?? item.slot.sourceMaxDimension + let oldDim = max(item.slot.currentTexture.width, item.slot.currentTexture.height) + // RGBA8 (4 bytes/pixel) × 4/3 mip factor, net increase only. + let newBytes = newDim * newDim * 4 * 4 / 3 + let oldBytes = oldDim * oldDim * 4 * 4 / 3 + return total + max(0, newBytes - oldBytes) + } + } + + /// Sum the actual allocated GPU bytes for every texture on an entity's render component. + /// + /// Uses `MTLTexture.allocatedSize` — the exact bytes the driver reserved — so + /// the value passed to `updateTextureSizeBytes` is accurate rather than estimated. + private func currentTextureBytes(entityId: EntityID) -> Int { + guard let render = scene.get(component: RenderComponent.self, for: entityId) else { return 0 } + var total = 0 + for mesh in render.mesh { + for submesh in mesh.submeshes { + guard let mat = submesh.material else { continue } + total += mat.baseColor.texture?.allocatedSize ?? 0 + total += mat.roughness.texture?.allocatedSize ?? 0 + total += mat.metallic.texture?.allocatedSize ?? 0 + total += mat.normal.texture?.allocatedSize ?? 0 + } + } + return total + } + // MARK: - Scheduling - private func scheduleResolutionChange(entityId: EntityID, distance: Float, targetMaxDimension: Int?, isVisible: Bool) { + private func scheduleResolutionChange( + entityId: EntityID, + distance: Float, + workItems: [StreamWorkItem], + targetMaxDimension: Int?, + isVisible: Bool, + capturedLOD: Int? = nil + ) { guard reserveOp(entityId) else { return } - let workItems = buildWorkItems(entityId: entityId, targetMaxDimension: targetMaxDimension) - guard !workItems.isEmpty else { + // Budget gate: upgrades consume GPU memory; downgrades free it. + // + // For upgrades, `reserveTexture` atomically checks the budget AND reserves the + // estimated bytes in one lock acquisition. This eliminates the TOCTOU race where + // multiple concurrent callers each see "budget available" and all proceed, + // collectively overshooting the budget before any accounting catches up. + // + // If the reservation succeeds: budgetedWorkItems = full workItems (upgrades + downgrades) + // If the reservation fails: budgetedWorkItems = downgrades only (upgrades stripped) + // If upgradeBytes == 0: no reservation needed; all items are downgrades + // + // The reservation MUST be released after the async Task completes (success, failure, + // or cancellation). It is released via defer inside the Task's MainActor.run block + // so all exit paths (entity destroyed, no changes, normal completion) are covered. + let upgradeBytes = estimatedUpgradeBytes(workItems) + let reservedUpgradeBytes: Int + let budgetedWorkItems: [StreamWorkItem] + if upgradeBytes > 0 { + if MemoryBudgetManager.shared.reserveTexture(sizeBytes: upgradeBytes) { + reservedUpgradeBytes = upgradeBytes + budgetedWorkItems = workItems + } else { + reservedUpgradeBytes = 0 + budgetedWorkItems = workItems.filter { $0.direction == .downgrade } + Logger.log(message: "[TextureStreaming] entity=\(entityId) skipped \(workItems.filter { $0.direction == .upgrade }.count) upgrade(s) — budget full (need \(upgradeBytes.formattedAsMemory))") + } + } else { + reservedUpgradeBytes = 0 + budgetedWorkItems = workItems + } + + guard !budgetedWorkItems.isEmpty else { releaseOp(entityId) + // reservedUpgradeBytes == 0 here: if reservation was made, budgetedWorkItems + // contains the full workItems and cannot be empty at this point. return } guard let device = renderInfo.device else { releaseOp(entityId) + if reservedUpgradeBytes > 0 { + MemoryBudgetManager.shared.releaseTextureReservation(sizeBytes: reservedUpgradeBytes) + } + return + } + + // Initialize reusable resources once on the calling thread before spawning the Task, + // then capture them as local constants so the Task never touches instance state. + if commandQueue == nil { commandQueue = device.makeCommandQueue() } + if textureLoader == nil { textureLoader = MTKTextureLoader(device: device) } + + guard let queue = commandQueue, let loader = textureLoader else { + releaseOp(entityId) + if reservedUpgradeBytes > 0 { + MemoryBudgetManager.shared.releaseTextureReservation(sizeBytes: reservedUpgradeBytes) + } return } + // Capture minimum dimension before spawning the Task so the apply-side + // level assignment uses the same threshold that was in effect at schedule time. + let capturedMinimumDim = normalizedMinimumDimension() + Task { var loaded: [LoadedTexture] = [] - loaded.reserveCapacity(workItems.count) + loaded.reserveCapacity(budgetedWorkItems.count) - let loader = MTKTextureLoader(device: device) - for item in workItems { + for item in budgetedWorkItems { let texture: MTLTexture? switch item.direction { case .upgrade: guard let sourceTexture = self.loadSourceTexture(item.slot.source, isSRGB: item.slot.isSRGB, loader: loader) else { continue } - texture = self.resampleTextureIfNeeded(sourceTexture, targetMaxDimension: item.targetMaxDimension) + texture = await self.resampleTextureIfNeeded(sourceTexture, targetMaxDimension: item.targetMaxDimension, commandQueue: queue) case .downgrade: - texture = self.resampleTextureIfNeeded(item.slot.currentTexture, targetMaxDimension: item.targetMaxDimension) + texture = await self.resampleTextureIfNeeded(item.slot.currentTexture, targetMaxDimension: item.targetMaxDimension, commandQueue: queue) } guard let texture else { continue } @@ -423,29 +700,54 @@ public class TextureStreamingSystem: @unchecked Sendable { await MainActor.run { withWorldMutationGate { - defer { self.releaseOp(entityId) } + defer { + self.releaseOp(entityId) + if reservedUpgradeBytes > 0 { + MemoryBudgetManager.shared.releaseTextureReservation(sizeBytes: reservedUpgradeBytes) + } + } guard scene.exists(entityId) else { return } + // If the entity switched LOD levels while this op was in-flight, + // the mesh indices in `loaded` now refer to a different LOD's mesh + // layout. Discard the apply — the next update() tick will reschedule + // against the new LOD's meshes with the correct indices. + if let scheduledLOD = capturedLOD, + let lodComp = scene.get(component: LODComponent.self, for: entityId), + lodComp.currentLOD != scheduledLOD + { + return + } + var didAnyChange = false var didUpgrade = false var didDowngrade = false for item in loaded { + // Resolve the three-tier streaming level from the item's target + // dimension. Both .capped (medium) and .minimum use the same + // `.capped` value in the old two-case enum, which made + // reconcileStreamingTexturesAfterArtifact blind to medium→minimum + // downgrades that happened while a batch artifact was building. + let streamLevel: TextureStreamingLevel = { + guard let dim = item.targetMaxDimension else { return .full } + return dim <= capturedMinimumDim ? .minimum : .capped + }() + let applied = updateMaterial(entityId: entityId, meshIndex: item.meshIndex, submeshIndex: item.submeshIndex) { material in - let isFull = item.targetMaxDimension == nil switch item.textureType { case .baseColor: material.baseColor.texture = item.texture - material.baseColorStreamingLevel = isFull ? .full : .capped + material.baseColorStreamingLevel = streamLevel case .roughness: material.roughness.texture = item.texture - material.roughnessStreamingLevel = isFull ? .full : .capped + material.roughnessStreamingLevel = streamLevel case .metallic: material.metallic.texture = item.texture - material.metallicStreamingLevel = isFull ? .full : .capped + material.metallicStreamingLevel = streamLevel case .normal: material.normal.texture = item.texture - material.normalStreamingLevel = isFull ? .full : .capped + material.normalStreamingLevel = streamLevel } } @@ -464,24 +766,39 @@ public class TextureStreamingSystem: @unchecked Sendable { // new texture is visible on the next frame with zero batch churn. BatchingSystem.shared.updateBatchMaterialInPlace(for: entityId) { batchMaterial in for item in loaded { - let isFull = item.targetMaxDimension == nil + let streamLevel: TextureStreamingLevel = { + guard let dim = item.targetMaxDimension else { return .full } + return dim <= capturedMinimumDim ? .minimum : .capped + }() switch item.textureType { case .baseColor: batchMaterial.baseColor.texture = item.texture - batchMaterial.baseColorStreamingLevel = isFull ? .full : .capped + batchMaterial.baseColorStreamingLevel = streamLevel case .roughness: batchMaterial.roughness.texture = item.texture - batchMaterial.roughnessStreamingLevel = isFull ? .full : .capped + batchMaterial.roughnessStreamingLevel = streamLevel case .metallic: batchMaterial.metallic.texture = item.texture - batchMaterial.metallicStreamingLevel = isFull ? .full : .capped + batchMaterial.metallicStreamingLevel = streamLevel case .normal: batchMaterial.normal.texture = item.texture - batchMaterial.normalStreamingLevel = isFull ? .full : .capped + batchMaterial.normalStreamingLevel = streamLevel } } } + // Update texture memory tracking so MemoryBudgetManager reflects + // the actual GPU allocation after this upgrade or downgrade. + // Only update if the entity has a budget entry (i.e. it was + // uploaded via GeometryStreamingSystem or the immediate path). + if MemoryBudgetManager.shared.isTracked(entityId: entityId) { + let newTextureBytes = self.currentTextureBytes(entityId: entityId) + MemoryBudgetManager.shared.updateTextureSizeBytes( + entityId: entityId, + newSizeBytes: newTextureBytes + ) + } + let hasAboveMinimum = self.entityHasTexturesAboveMinimumTier(entityId: entityId) self.lock.lock() if hasAboveMinimum { @@ -493,9 +810,12 @@ public class TextureStreamingSystem: @unchecked Sendable { if didDowngrade { self.totalDowngrades += 1 } self.lock.unlock() - _ = self.verboseLogging - _ = distance - _ = isVisible + if self.verboseLogging { + let dir = didUpgrade ? "↑" : "↓" + let dim = targetMaxDimension.map { "\($0)px" } ?? "full" + let distStr = distance >= 0 ? String(format: "%.1f", distance) : "offscreen" + print("[TextureStreaming] entity=\(entityId) \(dir) → \(dim) dist=\(distStr) visible=\(isVisible)") + } } } } @@ -520,13 +840,13 @@ public class TextureStreamingSystem: @unchecked Sendable { } } - private func resampleTextureIfNeeded(_ texture: MTLTexture, targetMaxDimension: Int?) -> MTLTexture? { + private func resampleTextureIfNeeded(_ texture: MTLTexture, targetMaxDimension: Int?, commandQueue: MTLCommandQueue) async -> MTLTexture? { guard let targetMaxDimension else { return texture } - return downsampleTexture(texture, maxDimension: targetMaxDimension) + return await downsampleTexture(texture, maxDimension: targetMaxDimension, commandQueue: commandQueue) } /// Downsample a texture to fit within maxDimension, preserving aspect ratio. - private func downsampleTexture(_ texture: MTLTexture, maxDimension: Int) -> MTLTexture? { + private func downsampleTexture(_ texture: MTLTexture, maxDimension: Int, commandQueue: MTLCommandQueue) async -> MTLTexture? { guard maxDimension > 0 else { return texture } guard texture.width > maxDimension || texture.height > maxDimension else { return texture } @@ -552,13 +872,19 @@ public class TextureStreamingSystem: @unchecked Sendable { desc.usage = [.shaderRead, .shaderWrite, .pixelFormatView] desc.storageMode = .private - if commandQueue == nil { - commandQueue = renderInfo.device.makeCommandQueue() + guard let target = renderInfo.device.makeTexture(descriptor: desc) else { + if verboseLogging { + print("[TextureStreaming] GPU resample failed: makeTexture returned nil (size: \(targetWidth)x\(targetHeight))") + } + return nil } - guard let target = renderInfo.device.makeTexture(descriptor: desc), - let commandBuffer = commandQueue?.makeCommandBuffer() - else { return nil } + guard let commandBuffer = commandQueue.makeCommandBuffer() else { + if verboseLogging { + print("[TextureStreaming] GPU resample failed: makeCommandBuffer returned nil") + } + return nil + } let scale = MPSImageBilinearScale(device: renderInfo.device) scale.encode(commandBuffer: commandBuffer, sourceTexture: texture, destinationTexture: target) @@ -568,8 +894,10 @@ public class TextureStreamingSystem: @unchecked Sendable { blit.endEncoding() } - commandBuffer.commit() - commandBuffer.waitUntilCompleted() + await withCheckedContinuation { (cont: CheckedContinuation) in + commandBuffer.addCompletedHandler { _ in cont.resume() } + commandBuffer.commit() + } return target } @@ -589,16 +917,76 @@ public class TextureStreamingSystem: @unchecked Sendable { return tex.makeTextureView(pixelFormat: target) ?? tex } + // MARK: - Memory Relief + + /// Force-downgrade textures on the farthest loaded entities to relieve combined GPU memory pressure. + /// + /// Called by `GeometryStreamingSystem` when combined mesh+texture memory is high but + /// geometry-only memory is not. Texture downgrades are far less visually disruptive than + /// geometry eviction — a distant wall losing resolution is much less noticeable than a + /// missing mesh. + /// + /// - Parameters: + /// - cameraPosition: Current camera world position for distance sorting. + /// - maxEntities: Maximum number of entities to downgrade in this call. Defaults to 4. + /// - Returns: Number of entities scheduled for downgrade. + @discardableResult + public func shedTextureMemory(cameraPosition: simd_float3, maxEntities: Int = 4) -> Int { + let effectiveCameraPosition = SceneRootTransform.shared.effectiveCameraPosition(cameraPosition) + + lock.lock() + let snapshot = Array(upgradedEntities) + lock.unlock() + + guard !snapshot.isEmpty else { return 0 } + + // Sort farthest-first: distant entities are least valuable at their current resolution. + let sorted = snapshot + .compactMap { id -> (EntityID, Float)? in + let d = calculateDistance(entityId: id, cameraPosition: effectiveCameraPosition) + return d.isFinite ? (id, d) : nil + } + .sorted { $0.1 > $1.1 } + + let minDimension = normalizedMinimumDimension() + var scheduled = 0 + + for (entityId, distance) in sorted.prefix(maxEntities) { + guard !isActiveOp(entityId) else { continue } + // Pass Float.greatestFiniteMagnitude as distance so hysteresis dead-band checks + // (`distance < threshold`) are never true — memory-pressure downgrades are unconditional. + let workItems = buildWorkItems(entityId: entityId, targetMaxDimension: minDimension, distance: Float.greatestFiniteMagnitude) + guard !workItems.isEmpty else { continue } + scheduleResolutionChange( + entityId: entityId, + distance: distance, + workItems: workItems, + targetMaxDimension: minDimension, + isVisible: false + ) + scheduled += 1 + } + + return scheduled + } + // MARK: - Stats / Debug + /// Returns `true` while a streaming operation is in-flight for this entity. + public func isStreaming(entityId: EntityID) -> Bool { + isActiveOp(entityId) + } + public func getStats() -> TextureStreamingStats { lock.lock() let upgradedCount = upgradedEntities.count let activeCount = activeOps.count + let upgrades = totalUpgrades + let downgrades = totalDowngrades lock.unlock() return TextureStreamingStats( - totalUpgrades: totalUpgrades, - totalDowngrades: totalDowngrades, + totalUpgrades: upgrades, + totalDowngrades: downgrades, upgradedEntityCount: upgradedCount, activeOps: activeCount ) @@ -608,10 +996,10 @@ public class TextureStreamingSystem: @unchecked Sendable { lock.lock() upgradedEntities.removeAll() activeOps.removeAll() - lock.unlock() - timeSinceLastUpdate = 0 totalUpgrades = 0 totalDowngrades = 0 + lock.unlock() + timeSinceLastUpdate = 0 } } diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air index 4f895b74a..2c1158e78 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib index 02e8ad7b4..fcc144531 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-ios.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air index abd5b57b6..5814b7974 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib index cdab19f45..b3582c351 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvos.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air index 3ac917f7b..cac2d075f 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib index 5710bb0a6..ade81abdc 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-tvossim.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air index 1147bc0a4..2d1157930 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib index bb49db2a1..6338ce629 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xros.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air index c2171d638..8cfed6d84 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.air differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib index 1c67e1dab..2b285c41f 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels-xrossim.metallib differ diff --git a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib index 057730f0c..90be4a405 100644 Binary files a/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib and b/Sources/UntoldEngine/UntoldEngineKernels/UntoldEngineKernels.metallib differ diff --git a/Sources/UntoldEngine/Utils/Globals.swift b/Sources/UntoldEngine/Utils/Globals.swift index c43cc55ea..8778dfa58 100644 --- a/Sources/UntoldEngine/Utils/Globals.swift +++ b/Sources/UntoldEngine/Utils/Globals.swift @@ -588,6 +588,7 @@ private final class RuntimeGlobalsStore: @unchecked Sendable { private var activeEntityValue: EntityID = .invalid private var enableEngineMetricsValue: Bool = false private var bypassPostProcessingValue: Bool = false + private var renderDebugViewModeValue: RenderDebugViewMode = .lit private var entityMeshMapValue: [EntityID: [Mesh]] = [:] private var entityNameMapValue: [EntityID: String] = [:] private var reverseEntityNameMapValue: [String: [EntityID]] = [:] @@ -1028,6 +1029,20 @@ private final class RuntimeGlobalsStore: @unchecked Sendable { } } + var renderDebugViewMode: RenderDebugViewMode { + get { + lock.lock() + let value = renderDebugViewModeValue + lock.unlock() + return value + } + set { + lock.lock() + renderDebugViewModeValue = newValue + lock.unlock() + } + } + var entityMeshMap: [EntityID: [Mesh]] { get { lock.lock() @@ -1137,6 +1152,14 @@ public enum TextureType: String, CaseIterable, Identifiable { } } +public enum RenderDebugViewMode: Int, CaseIterable, Sendable { + case lit = 0 + case albedo = 1 + case normal = 2 + case depth = 3 + case ssaoBlurred = 4 +} + // TODO: try to remove this var, because only make sense on the editor side public var gameMode: Bool { get { RuntimeGlobalsStore.shared.gameMode } @@ -1350,6 +1373,11 @@ public var bypassPostProcessing: Bool { set { RuntimeGlobalsStore.shared.bypassPostProcessing = newValue } } +public var renderDebugViewMode: RenderDebugViewMode { + get { RuntimeGlobalsStore.shared.renderDebugViewMode } + set { RuntimeGlobalsStore.shared.renderDebugViewMode = newValue } +} + public final class ToneMappingParams: ObservableObject, @unchecked Sendable { static let shared = ToneMappingParams() diff --git a/Sources/UntoldEngine/Utils/Logger.swift b/Sources/UntoldEngine/Utils/Logger.swift index 3ee9f9fc8..5d52a5ff5 100644 --- a/Sources/UntoldEngine/Utils/Logger.swift +++ b/Sources/UntoldEngine/Utils/Logger.swift @@ -20,6 +20,16 @@ public enum LogLevel: Int, Sendable { case test } +public enum LogCategory: String, CaseIterable, Sendable { + case general = "General" + case ecs = "ECS" + case oocTiming = "OOCTiming" + case oocStatus = "OOCStatus" + case assetLoader = "AssetLoader" + case engineStats = "EngineStats" + case integration = "Integration" +} + public struct LogEvent: Identifiable, Sendable { public let id = UUID() public let timestamp = Date() @@ -42,6 +52,26 @@ public enum Logger { set { state.logLevel = newValue } } + public static func enable(category: LogCategory) { + state.setCategoryEnabled(category.rawValue, enabled: true) + } + + public static func disable(category: LogCategory) { + state.setCategoryEnabled(category.rawValue, enabled: false) + } + + public static func set(category: LogCategory, enabled: Bool) { + state.setCategoryEnabled(category.rawValue, enabled: enabled) + } + + public static func isEnabled(category: LogCategory) -> Bool { + state.isCategoryEnabled(category.rawValue) + } + + public static func resetCategoryToggles() { + state.resetCategoryToggles() + } + #if canImport(AppKit) private static let sinkQueue = DispatchQueue(label: "engine.logger.sinks", qos: .utility) private static let backlogLimit = 2000 @@ -63,7 +93,7 @@ public enum Logger { private static func emit(level: LogLevel, message: String, - category: String = "General", + category: String = LogCategory.general.rawValue, file: String = #fileID, function: String = #function, line: Int = #line) @@ -79,61 +109,68 @@ public enum Logger { #endif } - public static func log(message: String, - category: String = "General", + public static func log(message: @autoclosure () -> String, + category: String = LogCategory.general.rawValue, file: String = #fileID, function: String = #function, line: Int = #line) { guard logLevel.rawValue >= LogLevel.info.rawValue else { return } - print("Log: \(message)") - emit(level: .info, message: message, category: category, file: file, function: function, line: line) + guard state.isCategoryEnabled(category) else { return } + let renderedMessage = message() + print("Log: \(renderedMessage)") + emit(level: .info, message: renderedMessage, category: category, file: file, function: function, line: line) } - public static func logError(message: String, - category: String = "General", + public static func logError(message: @autoclosure () -> String, + category: String = LogCategory.general.rawValue, file: String = #fileID, function: String = #function, line: Int = #line) { guard logLevel.rawValue >= LogLevel.error.rawValue else { return } - print("Error: \(message)") - emit(level: .error, message: message, category: category, file: file, function: function, line: line) + let renderedMessage = message() + print("Error: \(renderedMessage)") + emit(level: .error, message: renderedMessage, category: category, file: file, function: function, line: line) } - public static func logWarning(message: String, - category: String = "General", + public static func logWarning(message: @autoclosure () -> String, + category: String = LogCategory.general.rawValue, file: String = #fileID, function: String = #function, line: Int = #line) { guard logLevel.rawValue >= LogLevel.warning.rawValue else { return } - print("Warning: \(message)") - emit(level: .warning, message: message, category: category, file: file, function: function, line: line) + let renderedMessage = message() + print("Warning: \(renderedMessage)") + emit(level: .warning, message: renderedMessage, category: category, file: file, function: function, line: line) } public static func log(vector: simd_float3, - category: String = "General", + category: String = LogCategory.general.rawValue, file: String = #fileID, function: String = #function, line: Int = #line) { guard logLevel.rawValue >= LogLevel.debug.rawValue else { return } + guard state.isCategoryEnabled(category) else { return } let s = String(format: "simd_float3(%f, %f, %f)", vector.x, vector.y, vector.z) print(s) emit(level: .debug, message: s, category: category, file: file, function: function, line: line) } - public static func log(message: String, vector: simd_float3, - category: String = "General", + public static func log(message: @autoclosure () -> String, vector: simd_float3, + category: String = LogCategory.general.rawValue, file: String = #fileID, function: String = #function, line: Int = #line) { guard logLevel.rawValue >= LogLevel.debug.rawValue else { return } + guard state.isCategoryEnabled(category) else { return } + let renderedMessage = message() let s = String(format: "simd_float3(%f, %f, %f)", vector.x, vector.y, vector.z) - print(message); print(s) - emit(level: .debug, message: "\(message) \(s)", category: category, file: file, function: function, line: line) + print(renderedMessage); print(s) + emit(level: .debug, message: "\(renderedMessage) \(s)", category: category, file: file, function: function, line: line) } // …repeat same idea for simd_uint3, float4, 3x3, 4x4 (compose a string, print, emit) @@ -145,6 +182,12 @@ public enum Logger { private final class LoggerState: @unchecked Sendable { private let lock = NSLock() private var _logLevel: LogLevel = .debug + private let defaultDisabledCategories: Set = [ + LogCategory.oocTiming.rawValue, + LogCategory.oocStatus.rawValue, + LogCategory.assetLoader.rawValue, + ] + private var categoryOverrides: [String: Bool] = [:] var logLevel: LogLevel { get { @@ -159,6 +202,27 @@ public enum Logger { } } + func setCategoryEnabled(_ category: String, enabled: Bool) { + lock.lock() + categoryOverrides[category] = enabled + lock.unlock() + } + + func isCategoryEnabled(_ category: String) -> Bool { + lock.lock() + defer { lock.unlock() } + if let override = categoryOverrides[category] { + return override + } + return !defaultDisabledCategories.contains(category) + } + + func resetCategoryToggles() { + lock.lock() + categoryOverrides.removeAll(keepingCapacity: true) + lock.unlock() + } + #if canImport(AppKit) private var sinks: [WeakBox] = [] private var backlog: [LogEvent] = [] diff --git a/Sources/UntoldEngine/Utils/SpatialDebugVisualization.swift b/Sources/UntoldEngine/Utils/SpatialDebugVisualization.swift index a4b4c370d..a9c97ae8a 100644 --- a/Sources/UntoldEngine/Utils/SpatialDebugVisualization.swift +++ b/Sources/UntoldEngine/Utils/SpatialDebugVisualization.swift @@ -125,6 +125,23 @@ public final class SpatialDebugVisualization: @unchecked Sendable { } } + /// Color shaded renderables by current texture streaming tier. + /// Blue = full, Orange = medium (capped), Red = minimum, Yellow = in-flight. + private var _colorRenderablesByStreamingTier: Bool = false + public var colorRenderablesByStreamingTier: Bool { + get { + lock.lock() + let value = _colorRenderablesByStreamingTier + lock.unlock() + return value + } + set { + lock.lock() + _colorRenderablesByStreamingTier = newValue + lock.unlock() + } + } + /// Draw static batch cell bounds. private var _showStaticBatchCellBounds: Bool = false public var showStaticBatchCellBounds: Bool { @@ -182,7 +199,7 @@ public final class SpatialDebugVisualization: @unchecked Sendable { colorMode: SpatialDebugLeafColorMode = .plain ) { lock.lock() - _enabled = enabled || _showStaticBatchCellBounds || _colorRenderablesByLOD + _enabled = enabled || _showStaticBatchCellBounds || _colorRenderablesByLOD || _colorRenderablesByStreamingTier _showOctreeLeafBounds = enabled _maxLeafNodeCount = max(0, maxLeafNodeCount) _octreeLeafOccupiedOnly = occupiedOnly @@ -196,7 +213,7 @@ public final class SpatialDebugVisualization: @unchecked Sendable { colorMode: SpatialDebugBatchCellColorMode = .plain ) { lock.lock() - _enabled = _showOctreeLeafBounds || enabled || _colorRenderablesByLOD + _enabled = _showOctreeLeafBounds || enabled || _colorRenderablesByLOD || _colorRenderablesByStreamingTier _showStaticBatchCellBounds = enabled _maxStaticBatchCellCount = max(0, maxCellCount) _staticBatchCellColorMode = colorMode @@ -206,7 +223,14 @@ public final class SpatialDebugVisualization: @unchecked Sendable { public func configureLODLevelColoring(enabled: Bool) { lock.lock() _colorRenderablesByLOD = enabled - _enabled = _showOctreeLeafBounds || _showStaticBatchCellBounds || _colorRenderablesByLOD + _enabled = _showOctreeLeafBounds || _showStaticBatchCellBounds || _colorRenderablesByLOD || _colorRenderablesByStreamingTier + lock.unlock() + } + + public func configureTextureStreamingTierColoring(enabled: Bool) { + lock.lock() + _colorRenderablesByStreamingTier = enabled + _enabled = _showOctreeLeafBounds || _showStaticBatchCellBounds || _colorRenderablesByLOD || enabled lock.unlock() } @@ -216,6 +240,7 @@ public final class SpatialDebugVisualization: @unchecked Sendable { _showOctreeLeafBounds = false _showStaticBatchCellBounds = false _colorRenderablesByLOD = false + _colorRenderablesByStreamingTier = false lock.unlock() } } @@ -268,6 +293,12 @@ public func disableSpatialDebugVisualization() { SpatialDebugVisualization.shared.disableAll() } +/// Enable/disable texture streaming tier debug coloring for renderables. +/// Blue = full, Orange = medium (capped), Red = minimum, Yellow = in-flight. +public func setTextureStreamingTierDebug(enabled: Bool) { + SpatialDebugVisualization.shared.configureTextureStreamingTierColoring(enabled: enabled) +} + /// Enable/disable LOD level debug coloring for renderables. public func setLODLevelDebug(enabled: Bool) { SpatialDebugVisualization.shared.configureLODLevelColoring(enabled: enabled) diff --git a/Sources/UntoldEngineXR/UntoldEngineXR.swift b/Sources/UntoldEngineXR/UntoldEngineXR.swift index 0c1afd2ff..fbad5e5d3 100644 --- a/Sources/UntoldEngineXR/UntoldEngineXR.swift +++ b/Sources/UntoldEngineXR/UntoldEngineXR.swift @@ -78,6 +78,16 @@ Task { do { guard worldTracking.state != .running else { return } + + // Check world sensing authorization before attempting to run. + let authStatus = await arSession.queryAuthorization(for: [.worldSensing]) + if authStatus[.worldSensing] == .denied { + print("⚠️ World sensing authorization denied — plane detection disabled. Grant permission in Settings > Privacy > World Sensing.") + // Still run with world tracking only so device tracking works. + try await arSession.run([worldTracking]) + return + } + var providers: [any DataProvider] = [worldTracking] if PlaneDetectionProvider.isSupported { providers.append(planeDetection) @@ -310,6 +320,15 @@ let sceneReady = isSceneReady() let allowSpatialInputProcessing = !loading && sceneReady + // Drive the progressive asset loader from the main thread. + // runLoop() runs on the visionOS compositor render thread, so tick() (which + // requires the main thread via dispatchPrecondition) must be dispatched here. + // This mirrors what UntoldEngine.swift does inside draw() on the MTKViewDelegate + // main-thread path. Without this, progressive jobs never advance in XR. + DispatchQueue.main.async { + ProgressiveAssetLoader.shared.tick() + } + // 4. Update spatial input state from queued events if allowSpatialInputProcessing { updateSpatialInputState() @@ -320,6 +339,10 @@ // 5. Perform any rendering-related work that doesn't rely on the device anchor info guard let renderer else { return } if !loading { + // Sync the physical headset position into ECS camera components before + // streaming/LOD systems run inside updateXR(). Without this, all four OOC + // systems see a frozen default eye position regardless of where the user walks. + syncStreamingCameraPosition() renderer.updateXR(useExternalStatsLifecycle: true) } @@ -618,6 +641,25 @@ #endif } + /// Extracts the headset world position from the device anchor and pushes it into the + /// ECS camera components read by all four OOC systems (StreamingRegionManager, + /// GeometryStreamingSystem, TextureStreamingSystem, LODSystem). + /// + /// Uses the headset center position (originFromAnchorTransform.columns.3) rather than + /// a per-eye position — the ~3 cm per-eye IPD offset is irrelevant for streaming radii. + /// + /// Queries at CACurrentMediaTime() for the freshest position; falls back to + /// lastValidDeviceAnchor on a tracking gap (one-frame lag is harmless for streaming). + private func syncStreamingCameraPosition() { + #if canImport(ARKit) + let anchor = queryDeviceAnchorIfTrackingRunning(atTimestamp: CACurrentMediaTime()) + ?? lastValidDeviceAnchor + guard let anchor else { return } + let t = anchor.originFromAnchorTransform + renderer?.setXRCameraWorldPosition(simd_float3(t.columns.3.x, t.columns.3.y, t.columns.3.z)) + #endif + } + private func shouldLogAnchorDiagnostics() -> Bool { let now = CACurrentMediaTime() guard now - lastAnchorDiagnosticsLogTime >= anchorDiagnosticsLogIntervalSeconds else { diff --git a/Tests/UntoldEngineRenderTests/AsyncMeshLoadingTest.swift b/Tests/UntoldEngineRenderTests/AsyncMeshLoadingTest.swift index ac4be044c..0ad1e1062 100644 --- a/Tests/UntoldEngineRenderTests/AsyncMeshLoadingTest.swift +++ b/Tests/UntoldEngineRenderTests/AsyncMeshLoadingTest.swift @@ -23,9 +23,14 @@ final class AsyncMeshLoadingTest: BaseRenderSetup { override func setUp() async throws { try await super.setUp() LoadingSystem.shared.resourceURLFn = getResourceURL + // Cancel any leftover out-of-core CPU entries from a previous test run. + // ProgressiveAssetLoader no longer has a per-frame scheduler to disable; + // the out-of-core path only activates for large/many-object assets. + ProgressiveAssetLoader.shared.cancelAll() } override func tearDown() async throws { + ProgressiveAssetLoader.shared.cancelAll() LoadingSystem.shared.resourceURLFn = getResourceURL destroyAllEntities() try await super.tearDown() diff --git a/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift b/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift index 9ae4d22a9..6b5b6dac8 100644 --- a/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift +++ b/Tests/UntoldEngineRenderTests/BaseRenderSetup.swift @@ -24,6 +24,10 @@ class BaseRenderSetup: XCTestCase { let windowHeight = 1080 private func waitForOutstandingAssetLoadsToFinish(context: String, timeout: TimeInterval = 15.0) async { + // Cancel any in-flight progressive loads so they don't register entities + // into a stale scene after destroyAllEntities() or between tests. + ProgressiveAssetLoader.shared.cancelAll() + let drained = expectation(description: "Drain asset loading state (\(context))") var finalLoadingCount = Int.max var finalGateActive = true @@ -55,6 +59,7 @@ class BaseRenderSetup: XCTestCase { } private func resetGlobalEngineState() { + ProgressiveAssetLoader.shared.cancelAll() scene = Scene() CameraSystem.shared.activeCamera = nil visibleEntityIds.removeAll(keepingCapacity: true) diff --git a/Tests/UntoldEngineRenderTests/GeometryStreamingEvictionTests.swift b/Tests/UntoldEngineRenderTests/GeometryStreamingEvictionTests.swift new file mode 100644 index 000000000..f92395487 --- /dev/null +++ b/Tests/UntoldEngineRenderTests/GeometryStreamingEvictionTests.swift @@ -0,0 +1,321 @@ +// +// GeometryStreamingEvictionTests.swift +// UntoldEngine +// +// 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 ModelIO +import simd +@testable import UntoldEngine +import XCTest + +/// Tests for the eviction and concurrency fixes introduced alongside the +/// distance-aware visibility guard (`visibleEvictionProtectionRadius`). +/// +/// These tests do NOT require actual GPU uploads. They verify the control-flow +/// decisions made by `GeometryStreamingSystem.evictLRU` and the near-band +/// concurrency limiter — both of which are purely CPU-side. +@MainActor +final class GeometryStreamingEvictionTests: BaseRenderSetup { + // MARK: - Setup / Teardown + + override func setUp() async throws { + try await super.setUp() + + // Reset both systems to a known, isolated state before each test. + GeometryStreamingSystem.shared.reset() + GeometryStreamingSystem.shared.enabled = true + GeometryStreamingSystem.shared.updateInterval = 0.0 // no throttle + GeometryStreamingSystem.shared.maxConcurrentLoads = 3 + GeometryStreamingSystem.shared.nearBandFraction = 0.33 + GeometryStreamingSystem.shared.nearBandMaxConcurrentLoads = 1 + GeometryStreamingSystem.shared.visibleEvictionProtectionRadius = 30.0 + GeometryStreamingSystem.shared.evictionDistanceWeight = 0.6 + GeometryStreamingSystem.shared.evictionSizeWeight = 0.4 + GeometryStreamingSystem.shared.maxQueryRadius = 500.0 + + // Clear budget so base-setup entity registrations don't interfere. + MemoryBudgetManager.shared.clear() + MemoryBudgetManager.shared.enabled = true + MemoryBudgetManager.shared.meshBudget = 100 * 1024 * 1024 // 100 MB + MemoryBudgetManager.shared.highWaterMark = 0.85 + MemoryBudgetManager.shared.lowWaterMark = 0.70 + + // Clear the global visible-entity list so tests control it explicitly. + visibleEntityIds = [] + } + + override func tearDown() async throws { + GeometryStreamingSystem.shared.reset() + GeometryStreamingSystem.shared.enabled = false + GeometryStreamingSystem.shared.updateInterval = 0.1 + MemoryBudgetManager.shared.clear() + visibleEntityIds = [] + try await super.tearDown() + } + + // MARK: - Helpers + + /// Create an entity that is already loaded (state = .loaded) and tracked by + /// the streaming system, placed at `(distance, 0, 0)` from the origin. + /// + /// `evictLRU` discovers it via `loadedStreamingEntitiesSnapshot()`. + @discardableResult + private func makeLoadedEntity( + distance: Float, + memoryBytes: Int, + streamingRadius: Float = 100.0, + unloadRadius: Float = 200.0 + ) -> EntityID { + let entityId = createEntity() + + // Place the bounding-box center at (distance, 0, 0) with an identity + // WorldTransform. calculateDistance() uses (bb.min + bb.max) / 2 + // multiplied by WorldTransform.space. + if let local = scene.assign(to: entityId, component: LocalTransformComponent.self) { + local.boundingBox = ( + min: simd_float3(distance - 0.5, -0.5, -0.5), + max: simd_float3(distance + 0.5, 0.5, 0.5) + ) + } + if let world = scene.assign(to: entityId, component: WorldTransformComponent.self) { + world.space = simd_float4x4(1.0) + } + if let streaming = scene.assign(to: entityId, component: StreamingComponent.self) { + streaming.state = .loaded + streaming.streamingRadius = streamingRadius + streaming.unloadRadius = unloadRadius + streaming.assetFilename = "dummy" + streaming.assetExtension = "usdz" + streaming.lastVisibleFrame = 1 + } + + // Register in streaming system tracking set (same path as a real load). + GeometryStreamingSystem.shared.registerLoadedEntity(entityId) + + // Register memory so shouldEvict() / canAccept() see it. + MemoryBudgetManager.shared.registerMesh( + entityId: entityId, + meshSizeBytes: memoryBytes, + textureSizeBytes: 0 + ) + + return entityId + } + + /// Create an entity that is unloaded (state = .unloaded) and registered + /// in the octree so `update()` discovers it as a load candidate. + @discardableResult + private func makeUnloadedEntity( + distance: Float, + streamingRadius: Float = 100.0, + unloadRadius: Float = 200.0 + ) -> EntityID { + let entityId = createEntity() + + if let local = scene.assign(to: entityId, component: LocalTransformComponent.self) { + local.boundingBox = ( + min: simd_float3(distance - 0.5, -0.5, -0.5), + max: simd_float3(distance + 0.5, 0.5, 0.5) + ) + } + if let world = scene.assign(to: entityId, component: WorldTransformComponent.self) { + world.space = simd_float4x4(1.0) + } + if let streaming = scene.assign(to: entityId, component: StreamingComponent.self) { + streaming.state = .unloaded + streaming.streamingRadius = streamingRadius + streaming.unloadRadius = unloadRadius + // Intentionally nonexistent asset so loadMeshAsync returns false quickly. + streaming.assetFilename = "nonexistent_test_asset" + streaming.assetExtension = "usdz" + } + + // Must be in octree so the spatial query in update() finds it as a candidate. + OctreeSystem.shared.registerEntity(entityId) + + return entityId + } + + // MARK: - Test 1: CPUMeshEntry estimatedGPUBytes round-trip + + /// `estimatedGPUBytes` is computed at stub-registration time and stored in + /// `CPUMeshEntry`. Verify the value survives `storeCPUMesh` / `retrieveCPUMesh` + /// and is cleared by `removeCPUMesh`. + func testCPUMeshEntryEstimatedGPUBytesStoredAndRetrieved() { + let entityId: EntityID = 99001 + let expectedBytes = 1_234_567 + + let entry = ProgressiveAssetLoader.CPUMeshEntry( + object: MDLObject(), + vertexDescriptor: MDLVertexDescriptor(), + textureLoader: TextureLoader(device: renderInfo.device), + device: renderInfo.device, + url: URL(fileURLWithPath: "/dev/null"), + filename: "test", + withExtension: "usdz", + uniqueAssetName: "TestMesh#0", + estimatedGPUBytes: expectedBytes, + residencyPolicy: .fullLoad + ) + + ProgressiveAssetLoader.shared.storeCPUMesh(entry, for: entityId) + + let retrieved = ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId) + XCTAssertEqual( + retrieved?.estimatedGPUBytes, expectedBytes, + "estimatedGPUBytes should survive storeCPUMesh / retrieveCPUMesh round-trip" + ) + + ProgressiveAssetLoader.shared.removeCPUMesh(for: entityId) + XCTAssertNil( + ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId), + "Entry should be absent after removeCPUMesh" + ) + } + + // MARK: - Test 2: Far visible entity is evicted under budget pressure + + /// Regression test for the zoom-out → zoom-in eviction deadlock. + /// + /// Before the fix, `evictLRU` would skip every entity that appeared in + /// `visibleEntityIds` regardless of distance, making eviction a complete + /// no-op whenever all loaded meshes were in the frustum. Far visible meshes + /// would permanently occupy the budget, blocking nearby meshes from loading. + /// + /// After the fix, entities beyond `visibleEvictionProtectionRadius` (30 m) + /// may be evicted even while visible. + func testFarVisibleEntityIsEvictedUnderBudgetPressure() { + // Close entity (10 m) — inside protection radius, must NOT be evicted. + let closeEntity = makeLoadedEntity(distance: 10.0, memoryBytes: 50 * 1024 * 1024) + // Far entity (200 m) — outside protection radius, MUST be evicted. + let farEntity = makeLoadedEntity(distance: 200.0, memoryBytes: 50 * 1024 * 1024) + + // Both are visible (in-frustum). Total = 100 MB → exactly at budget. + // Adding 1 byte pushes shouldEvict() over 85 % high-water mark. + // Instead of a third entity, tip the budget by adjusting the far entity's size. + // Re-register far entity with 51 MB to push total to 101 MB (> 85 % of 100 MB). + MemoryBudgetManager.shared.unregisterMesh(entityId: farEntity) + MemoryBudgetManager.shared.registerMesh( + entityId: farEntity, + meshSizeBytes: 51 * 1024 * 1024, + textureSizeBytes: 0 + ) + + // Both entities are visible. + visibleEntityIds = [closeEntity, farEntity] + + XCTAssertTrue(MemoryBudgetManager.shared.shouldEvict(), "Pre-condition: budget should be over threshold") + + // Camera at origin (distance to closeEntity ≈ 10 m, farEntity ≈ 200 m). + GeometryStreamingSystem.shared.update(cameraPosition: .zero, deltaTime: 1.0) + + let closeState = scene.get(component: StreamingComponent.self, for: closeEntity)?.state + let farState = scene.get(component: StreamingComponent.self, for: farEntity)?.state + + XCTAssertEqual(closeState, .loaded, + "Close visible entity (10 m < 30 m protection radius) must NOT be evicted") + XCTAssertEqual(farState, .unloaded, + "Far visible entity (200 m > 30 m protection radius) MUST be evicted to free budget") + } + + // MARK: - Test 3: Close visible entity is protected from eviction + + /// Entities within `visibleEvictionProtectionRadius` must never be evicted + /// while visible, even under severe budget pressure. + func testCloseVisibleEntityIsProtectedFromEviction() { + // One entity very close to camera (5 m), well inside 30 m protection radius. + let closeEntity = makeLoadedEntity(distance: 5.0, memoryBytes: 90 * 1024 * 1024) + + // Visible and budget over threshold. + visibleEntityIds = [closeEntity] + + XCTAssertTrue(MemoryBudgetManager.shared.shouldEvict(), "Pre-condition: budget should be over threshold") + + GeometryStreamingSystem.shared.update(cameraPosition: .zero, deltaTime: 1.0) + + let state = scene.get(component: StreamingComponent.self, for: closeEntity)?.state + XCTAssertEqual(state, .loaded, + "Visible entity within visibleEvictionProtectionRadius (5 m < 30 m) must remain loaded") + } + + // MARK: - Test 4: Value-score sorts eviction candidates correctly + + /// Far + large entities must be evicted before far + small, which must be + /// evicted before near + large. + /// + /// Scores (evictionDistanceWeight=0.6, evictionSizeWeight=0.4, budget=100 MB, + /// maxQueryRadius=500 m): + /// A 200 m, 50 MB → 0.6·(200/500) + 0.4·(50/100) = 0.44 (highest) + /// B 200 m, 5 MB → 0.6·(200/500) + 0.4·( 5/100) = 0.26 + /// C 10 m, 50 MB → 0.6·( 10/500) + 0.4·(50/100) = 0.21 (lowest) + /// + /// Eviction stops once shouldEvict() returns false. With a 100 MB budget at + /// 85 % high-water mark, evicting A (50 MB) drops utilisation to ~55 MB and + /// shouldEvict() turns false — B and C remain loaded. + func testValueScoreEvictsHighScoreEntityFirst() { + let entityA = makeLoadedEntity(distance: 200.0, memoryBytes: 50 * 1024 * 1024) // far + large + let entityB = makeLoadedEntity(distance: 200.0, memoryBytes: 5 * 1024 * 1024) // far + small + let entityC = makeLoadedEntity(distance: 10.0, memoryBytes: 50 * 1024 * 1024) // near + large + // Total: 105 MB → shouldEvict() fires. + + // None are visible — visibility guard should not interfere. + visibleEntityIds = [] + + XCTAssertTrue(MemoryBudgetManager.shared.shouldEvict(), "Pre-condition: budget should be over threshold") + + GeometryStreamingSystem.shared.update(cameraPosition: .zero, deltaTime: 1.0) + + let stateA = scene.get(component: StreamingComponent.self, for: entityA)?.state + let stateB = scene.get(component: StreamingComponent.self, for: entityB)?.state + let stateC = scene.get(component: StreamingComponent.self, for: entityC)?.state + + // A has the highest score and must be evicted first. Evicting A brings + // utilisation below the threshold, so B and C are left intact. + XCTAssertEqual(stateA, .unloaded, "Entity A (far + large, highest score) must be evicted first") + XCTAssertEqual(stateB, .loaded, "Entity B (far + small) must remain loaded after one eviction") + XCTAssertEqual(stateC, .loaded, "Entity C (near + large) must remain loaded after one eviction") + } + + // MARK: - Test 5: Near-band concurrency limit is enforced + + /// When `nearBandMaxConcurrentLoads = 1`, at most one near-band entity + /// should transition to `.loading` per `update()` call, regardless of how + /// many near-band candidates are available. + /// + /// The async load Tasks are not awaited here — the test checks state + /// immediately after `update()` returns, before any Task completes. + func testNearBandConcurrencyLimitIsEnforced() { + // 5 unloaded entities all within nearBand distance. + // streamingRadius=100, nearBandFraction=0.33 → near band ≤ 33 m. + let nearBandEntities = (0 ..< 5).map { i in + makeUnloadedEntity( + distance: Float(i + 1) * 5.0, // 5, 10, 15, 20, 25 m + streamingRadius: 100.0, + unloadRadius: 200.0 + ) + } + + // Budget has plenty of headroom — only the concurrency cap should limit starts. + XCTAssertFalse(MemoryBudgetManager.shared.shouldEvict(), "Pre-condition: budget must be under threshold") + + GeometryStreamingSystem.shared.update(cameraPosition: .zero, deltaTime: 1.0) + + // Count entities that entered .loading state synchronously. + let loadingCount = nearBandEntities.filter { + scene.get(component: StreamingComponent.self, for: $0)?.state == .loading + }.count + + XCTAssertLessThanOrEqual( + loadingCount, + GeometryStreamingSystem.shared.nearBandMaxConcurrentLoads, + "At most nearBandMaxConcurrentLoads (\(GeometryStreamingSystem.shared.nearBandMaxConcurrentLoads)) near-band loads should start per update()" + ) + XCTAssertGreaterThan(loadingCount, 0, + "At least one near-band load should have been started") + } +} diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorTargetReference.png index 0a8f5c192..b78fd5c3c 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/ColorTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png index 5e5d693f0..c00f27a0c 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/CompositeColorTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png index 140f4cbbb..1a9ed5c3b 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/DepthTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png index 03c8173ed..d381da6b7 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/LightPassColorReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png index 853a7fc5e..f9f254ab1 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/NormalTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/PositionTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/PositionTargetReference.png index b5d576d51..d10885d87 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/PositionTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/PositionTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png index 03c8173ed..d381da6b7 100644 Binary files a/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png and b/Tests/UntoldEngineRenderTests/Resources/ReferenceImages/TransparencyTargetReference.png differ diff --git a/Tests/UntoldEngineRenderTests/StaticBatchingTest.swift b/Tests/UntoldEngineRenderTests/StaticBatchingTest.swift index 8f3144b27..f2d812537 100644 --- a/Tests/UntoldEngineRenderTests/StaticBatchingTest.swift +++ b/Tests/UntoldEngineRenderTests/StaticBatchingTest.swift @@ -173,19 +173,19 @@ final class StaticBatchingTest: BaseRenderSetup { return } + // Load meshes once so all entities share the same texture object identity + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + // Create multiple entities with same mesh (same material) var entities: [EntityID] = [] for i in 0 ..< 5 { let entity = createEntity() - // Load mesh - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) - // Add RenderComponent if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes @@ -228,16 +228,16 @@ final class StaticBatchingTest: BaseRenderSetup { return } + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + for _ in 0 ..< 3 { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) - if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes } @@ -276,9 +276,10 @@ final class StaticBatchingTest: BaseRenderSetup { return } + let meshes = Mesh.loadMeshes(url: ballURL, vertexDescriptor: vertexDescriptor.model, device: renderInfo.device, flip: true) + for _ in 0 ..< 3 { let entity = createEntity() - let meshes = Mesh.loadMeshes(url: ballURL, vertexDescriptor: vertexDescriptor.model, device: renderInfo.device, flip: true) if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes @@ -471,16 +472,16 @@ final class StaticBatchingTest: BaseRenderSetup { return } + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + for i in 0 ..< 3 { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) - if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes } @@ -624,17 +625,17 @@ final class StaticBatchingTest: BaseRenderSetup { return } + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + let entityCount = 10 for _ in 0 ..< entityCount { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) - if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes } @@ -765,17 +766,17 @@ final class StaticBatchingTest: BaseRenderSetup { return } + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + var entities: [EntityID] = [] for _ in 0 ..< 4 { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) - if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes renderComponent.assetURL = ballURL @@ -835,15 +836,17 @@ final class StaticBatchingTest: BaseRenderSetup { return } + // Load meshes once so all entities share the same texture object identity + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + // First, create some batched entities so there's a batch to join for _ in 0 ..< 3 { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes renderComponent.assetURL = ballURL @@ -871,13 +874,7 @@ final class StaticBatchingTest: BaseRenderSetup { // Verify target entity is NOT batched (no mesh) XCTAssertFalse(BatchingSystem.shared.isBatched(entityId: targetEntity), "❌ Entity should not be batched without mesh") - // When: Load mesh for the entity - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) + // When: Assign the shared meshes to simulate mesh becoming resident if let renderComponent = scene.get(component: RenderComponent.self, for: targetEntity) { renderComponent.mesh = meshes } @@ -916,14 +913,15 @@ final class StaticBatchingTest: BaseRenderSetup { BatchingSystem.shared.setQuiescenceFramesBeforeBatchBuild(2) + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + for _ in 0 ..< 3 { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes renderComponent.assetURL = ballURL @@ -947,12 +945,6 @@ final class StaticBatchingTest: BaseRenderSetup { generateBatches() XCTAssertFalse(BatchingSystem.shared.isBatched(entityId: targetEntity), "❌ Entity starts unbatched") - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) if let renderComponent = scene.get(component: RenderComponent.self, for: targetEntity) { renderComponent.mesh = meshes } @@ -1114,14 +1106,15 @@ final class StaticBatchingTest: BaseRenderSetup { BatchingSystem.shared.setBatchCellSize(10.0) enableBatching(true) + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + func makeEntity(position: simd_float3) -> EntityID { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes renderComponent.assetURL = ballURL @@ -1187,14 +1180,15 @@ final class StaticBatchingTest: BaseRenderSetup { BatchingSystem.shared.setMaxDirtyCellsPerTick(1) enableBatching(true) + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + func makeEntity(position: simd_float3) -> EntityID { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes renderComponent.assetURL = ballURL @@ -1260,14 +1254,15 @@ final class StaticBatchingTest: BaseRenderSetup { BatchingSystem.shared.setMaxRebuildBufferBytesPerTick(1) enableBatching(true) + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + func makeEntity(position: simd_float3) -> EntityID { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes renderComponent.assetURL = ballURL @@ -1331,14 +1326,15 @@ final class StaticBatchingTest: BaseRenderSetup { BatchingSystem.shared.setQuiescenceFramesBeforeBatchBuild(0) enableBatching(true) + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + func makeEntity(position: simd_float3) -> EntityID { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes renderComponent.assetURL = ballURL @@ -1413,14 +1409,15 @@ final class StaticBatchingTest: BaseRenderSetup { BatchingSystem.shared.setQuiescenceFramesBeforeBatchBuild(0) enableBatching(true) + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + func makeEntity(position: simd_float3) -> EntityID { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes renderComponent.assetURL = ballURL @@ -1482,14 +1479,15 @@ final class StaticBatchingTest: BaseRenderSetup { BatchingSystem.shared.setQuiescenceFramesBeforeBatchBuild(0) enableBatching(true) + let meshes = Mesh.loadMeshes( + url: ballURL, + vertexDescriptor: vertexDescriptor.model, + device: renderInfo.device, + flip: true + ) + func makeEntity(position: simd_float3) -> EntityID { let entity = createEntity() - let meshes = Mesh.loadMeshes( - url: ballURL, - vertexDescriptor: vertexDescriptor.model, - device: renderInfo.device, - flip: true - ) if let renderComponent = scene.assign(to: entity, component: RenderComponent.self) { renderComponent.mesh = meshes renderComponent.assetURL = ballURL diff --git a/Tests/UntoldEngineRenderTests/StreamLodBatchTests.swift b/Tests/UntoldEngineRenderTests/StreamLodBatchTests.swift index 604461e86..8cc4d336c 100644 --- a/Tests/UntoldEngineRenderTests/StreamLodBatchTests.swift +++ b/Tests/UntoldEngineRenderTests/StreamLodBatchTests.swift @@ -11,6 +11,8 @@ // 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 Metal +import ModelIO import simd @testable import UntoldEngine import XCTest @@ -577,6 +579,111 @@ final class StreamLodBatchLODAwareStreamingTests: BaseRenderSetup { } } +// MARK: - LOD+OOC Integration Tests + +/// Tests that verify the LOD+OOC integration path in ProgressiveAssetLoader. +/// These test the CPU registry behaviour for LOD group entities without GPU or disk I/O. +@MainActor +final class StreamLodBatchOOCIntegrationTests: XCTestCase { + var loader: ProgressiveAssetLoader! + + override func setUp() async throws { + loader = ProgressiveAssetLoader.shared + loader.cancelAll() + guard let device = MTLCreateSystemDefaultDevice() else { + XCTFail("No Metal device") + return + } + renderInfo.device = device + } + + override func tearDown() async throws { + loader.cancelAll() + } + + private func makeEntry(name: String) -> ProgressiveAssetLoader.CPUMeshEntry { + guard let device = MTLCreateSystemDefaultDevice() else { + fatalError("No Metal device") + } + return ProgressiveAssetLoader.CPUMeshEntry( + object: MDLObject(), + vertexDescriptor: MDLVertexDescriptor(), + textureLoader: TextureLoader(device: device), + device: device, + url: URL(fileURLWithPath: "/dev/null"), + filename: "scene", + withExtension: "usdz", + uniqueAssetName: name, + estimatedGPUBytes: 0, + residencyPolicy: .fullLoad + ) + } + + func testHasCPULODData_detectsLODOOCEntity() { + let groupEntityId: EntityID = 5000 + loader.storeCPULODMesh(makeEntry(name: "Tree_LOD0"), for: groupEntityId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(name: "Tree_LOD1"), for: groupEntityId, lodIndex: 1) + + XCTAssertTrue(loader.hasCPULODData(for: groupEntityId), + "hasCPULODData must return true for a LOD+OOC entity") + } + + func testHasCPULODData_distinguishesFromRegularOOCEntity() { + let lodEntityId: EntityID = 5010 + let regularEntityId: EntityID = 5011 + + loader.storeCPULODMesh(makeEntry(name: "Tree_LOD0"), for: lodEntityId, lodIndex: 0) + loader.storeCPUMesh(makeEntry(name: "Building#0"), for: regularEntityId) + + XCTAssertTrue(loader.hasCPULODData(for: lodEntityId), + "LOD+OOC entity must report hasCPULODData = true") + XCTAssertFalse(loader.hasCPULODData(for: regularEntityId), + "Regular OOC entity must report hasCPULODData = false") + } + + func testLODOOCRegistration_multiGroupChildrenAreMappedToRoot() { + let rootId: EntityID = 5020 + let treeGroupId: EntityID = 5021 + let rockGroupId: EntityID = 5022 + + // Register LOD entries for two groups + loader.storeCPULODMesh(makeEntry(name: "Tree_LOD0"), for: treeGroupId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(name: "Tree_LOD1"), for: treeGroupId, lodIndex: 1) + loader.storeCPULODMesh(makeEntry(name: "Rock_LOD0"), for: rockGroupId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(name: "Rock_LOD1"), for: rockGroupId, lodIndex: 1) + loader.registerChildren([treeGroupId, rockGroupId], for: rootId) + + // Both groups should have LOD data + XCTAssertTrue(loader.hasCPULODData(for: treeGroupId)) + XCTAssertTrue(loader.hasCPULODData(for: rockGroupId)) + + // Children lookup should return both group entities + let children = loader.getChildren(for: rootId) + XCTAssertEqual(Set(children), Set([treeGroupId, rockGroupId])) + } + + func testLODOOCRegistration_allLevelsRestoredAfterCancelAll() { + let entityId: EntityID = 5030 + loader.storeCPULODMesh(makeEntry(name: "Prop_LOD0"), for: entityId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(name: "Prop_LOD1"), for: entityId, lodIndex: 1) + loader.storeCPULODMesh(makeEntry(name: "Prop_LOD2"), for: entityId, lodIndex: 2) + + loader.cancelAll() + + XCTAssertNil(loader.retrieveAllCPULODMeshes(for: entityId), + "cancelAll must clear all LOD registry entries") + XCTAssertFalse(loader.hasCPULODData(for: entityId)) + } + + func testLODStubLevelStartsNotResident() { + // Verify that a LODLevel created with empty mesh starts .notResident — + // the automatic residency state that the LOD+OOC path relies on. + let stubLevel = LODLevel(mesh: [], maxDistance: 50, url: URL(fileURLWithPath: "/dev/null"), assetName: "Tree_LOD0") + XCTAssertEqual(stubLevel.residencyState, .notResident, + "Stub LODLevel (empty mesh) must start as .notResident") + } +} + // MARK: - Region Streaming Event Tests @MainActor diff --git a/Tests/UntoldEngineRenderTests/USDZTextureTest.swift b/Tests/UntoldEngineRenderTests/USDZTextureTest.swift index 0814ef95f..2a251c66f 100644 --- a/Tests/UntoldEngineRenderTests/USDZTextureTest.swift +++ b/Tests/UntoldEngineRenderTests/USDZTextureTest.swift @@ -63,13 +63,25 @@ final class USDZTextureTest: BaseRenderSetup { // When: Get material texture URL if let baseColorURL = getMaterialTextureURL(entityId: entity, type: .baseColor) { - // Then: Check if it's an embedded URL - XCTAssertEqual(baseColorURL.scheme, "usdz-embedded", "Embedded texture should have usdz-embedded:// scheme") - XCTAssertFalse(baseColorURL.absoluteString.contains("Mesh_SoccerPlayer1"), "Embedded URL identity should not be mesh-scoped") + // Then: URL should be either a usdz-embedded:// pseudo-URL (bracket-path available) + // or an mdl-obj- object-identity URL (no bracket path — new architecture). + let isEmbeddedScheme = baseColorURL.scheme == "usdz-embedded" + let isObjectIdentity = baseColorURL.absoluteString.hasPrefix("mdl-obj-") XCTAssertTrue( - baseColorURL.absoluteString.contains("textures/") || baseColorURL.absoluteString.contains("embedded_"), - "Embedded URL should use package-relative texture path when available, otherwise fallback token" + isEmbeddedScheme || isObjectIdentity, + "Texture URL should use usdz-embedded:// scheme or mdl-obj- object identity, got: \(baseColorURL)" ) + XCTAssertFalse(baseColorURL.absoluteString.contains("Mesh_SoccerPlayer1"), "Embedded URL identity should not be mesh-scoped") + + // When bracket notation is available the URL carries a package-relative + // texture path or an embedded_ fallback token. Object-identity URLs are + // opaque by design and don't carry human-readable paths. + if isEmbeddedScheme { + XCTAssertTrue( + baseColorURL.absoluteString.contains("textures/") || baseColorURL.absoluteString.contains("embedded_"), + "Embedded URL should use package-relative texture path when available, otherwise fallback token" + ) + } } } diff --git a/Tests/UntoldEngineTests/MemoryBudgetManagerTests.swift b/Tests/UntoldEngineTests/MemoryBudgetManagerTests.swift index db77f9211..0ed4aa72a 100644 --- a/Tests/UntoldEngineTests/MemoryBudgetManagerTests.swift +++ b/Tests/UntoldEngineTests/MemoryBudgetManagerTests.swift @@ -302,4 +302,141 @@ final class MemoryBudgetManagerTests: XCTestCase { XCTAssertEqual(stats.textureMemoryUsed, 0) XCTAssertEqual(stats.totalTrackedMemory, 0) } + + // MARK: - Geometry-only gate (shouldEvictGeometry / canAcceptMesh) + + func testShouldEvictGeometry_notTriggeredByTextureMemoryAlone() { + // 90 MB of texture memory on a 100 MB budget — combined gate fires, geo gate must not. + manager.registerMesh(entityId: 1, meshSizeBytes: 1 * 1024 * 1024, textureSizeBytes: 89 * 1024 * 1024) + + XCTAssertTrue(manager.shouldEvict(), "Combined gate should fire when total ≥ 85 %") + XCTAssertFalse(manager.shouldEvictGeometry(), "Geo-only gate must not fire when mesh memory is low") + } + + func testShouldEvictGeometry_triggeredWhenMeshMemoryHigh() { + // 86 MB of mesh memory on a 100 MB budget — geo gate must fire. + manager.registerMesh(entityId: 1, meshSizeBytes: 86 * 1024 * 1024) + + XCTAssertTrue(manager.shouldEvictGeometry(), "Geo-only gate should fire when mesh memory ≥ 85 %") + } + + func testShouldEvictGeometry_notTriggeredWhenBelowThreshold() { + manager.registerMesh(entityId: 1, meshSizeBytes: 50 * 1024 * 1024) + + XCTAssertFalse(manager.shouldEvictGeometry(), "Geo-only gate should not fire below high-water mark") + } + + func testCanAcceptMesh_trueWhenBelowBudget() { + manager.registerMesh(entityId: 1, meshSizeBytes: 10 * 1024 * 1024) + let candidate = 5 * 1024 * 1024 // 5 MB + + XCTAssertTrue(manager.canAcceptMesh(sizeBytes: candidate), "Should accept mesh that fits within budget") + } + + func testCanAcceptMesh_falseWhenExceedsBudget() { + manager.registerMesh(entityId: 1, meshSizeBytes: 98 * 1024 * 1024) + let candidate = 5 * 1024 * 1024 // would push to 103 MB > 100 MB budget + + XCTAssertFalse(manager.canAcceptMesh(sizeBytes: candidate), "Should reject mesh that exceeds budget") + } + + func testCanAcceptMesh_ignoresTextureMemory() { + // 80 MB texture + 5 MB mesh = 85 MB combined, but mesh alone is only 5 MB. + // canAcceptMesh must look only at mesh memory. + manager.registerMesh(entityId: 1, meshSizeBytes: 5 * 1024 * 1024, textureSizeBytes: 80 * 1024 * 1024) + let candidate = 10 * 1024 * 1024 // 5 + 10 = 15 MB mesh, well within 100 MB budget + + XCTAssertTrue(manager.canAcceptMesh(sizeBytes: candidate), + "canAcceptMesh should ignore texture memory and pass when mesh-only budget is fine") + } + + // MARK: - Texture Reservation Tests (V1.4 — TOCTOU fix) + + // These tests verify the atomic reserveTexture / releaseTextureReservation pair + // that eliminates the check-then-act race in TextureStreamingSystem. + + func testReserveTexture_succeedsWhenBudgetAvailable() { + // 10 MB used, 90 MB free — a 20 MB reservation should succeed. + manager.registerMesh(entityId: 1, meshSizeBytes: 10 * 1024 * 1024) + + XCTAssertTrue(manager.reserveTexture(sizeBytes: 20 * 1024 * 1024), + "reserveTexture should succeed when budget has headroom") + manager.releaseTextureReservation(sizeBytes: 20 * 1024 * 1024) // cleanup + } + + func testReserveTexture_failsWhenBudgetFull() { + // 95 MB used, only 5 MB free — a 10 MB reservation must be rejected. + manager.registerMesh(entityId: 1, meshSizeBytes: 95 * 1024 * 1024) + + XCTAssertFalse(manager.reserveTexture(sizeBytes: 10 * 1024 * 1024), + "reserveTexture should fail when the upgrade would exceed the budget") + } + + func testReserveTexture_secondCallSeesReducedHeadroom() { + // Budget: 100 MB. Used: 0 MB. Reserve 60 MB → success. + // Then try to reserve another 60 MB → must fail because only 40 MB remains. + XCTAssertTrue(manager.reserveTexture(sizeBytes: 60 * 1024 * 1024), + "First 60 MB reservation should succeed on an empty budget") + XCTAssertFalse(manager.reserveTexture(sizeBytes: 60 * 1024 * 1024), + "Second 60 MB reservation must fail — first reservation reduces apparent headroom") + + manager.releaseTextureReservation(sizeBytes: 60 * 1024 * 1024) // cleanup + } + + func testReleaseTextureReservation_restoresHeadroom() { + // Reserve 60 MB, verify second 60 MB fails, then release and verify it now succeeds. + XCTAssertTrue(manager.reserveTexture(sizeBytes: 60 * 1024 * 1024)) + XCTAssertFalse(manager.reserveTexture(sizeBytes: 60 * 1024 * 1024), + "Second reservation should fail before release") + + manager.releaseTextureReservation(sizeBytes: 60 * 1024 * 1024) + + XCTAssertTrue(manager.reserveTexture(sizeBytes: 60 * 1024 * 1024), + "After releasing the first reservation the second attempt should succeed") + manager.releaseTextureReservation(sizeBytes: 60 * 1024 * 1024) // cleanup + } + + func testReleaseTextureReservation_doesNotGoBelowZero() { + // Releasing more than was reserved must clamp at 0, not underflow. + manager.releaseTextureReservation(sizeBytes: 100 * 1024 * 1024) + // If inFlightTextureReservation were negative, canAcceptTexture would over-report headroom. + // Verify the budget check still behaves correctly (no crash, no phantom headroom). + manager.registerMesh(entityId: 1, meshSizeBytes: 99 * 1024 * 1024) + XCTAssertFalse(manager.reserveTexture(sizeBytes: 5 * 1024 * 1024), + "Over-releasing must not create phantom headroom — budget should still be nearly full") + } + + func testCanAcceptTexture_includesInFlightReservation() { + // 50 MB used. Reserve 40 MB in-flight (total committed = 90 MB). + // canAcceptTexture for 15 MB would push to 105 MB > 100 MB budget → must return false. + manager.registerMesh(entityId: 1, meshSizeBytes: 50 * 1024 * 1024) + _ = manager.reserveTexture(sizeBytes: 40 * 1024 * 1024) + + XCTAssertFalse(manager.canAcceptTexture(sizeBytes: 15 * 1024 * 1024), + "canAcceptTexture must account for in-flight reservations") + + manager.releaseTextureReservation(sizeBytes: 40 * 1024 * 1024) // cleanup + } + + func testCanAcceptTexture_trueWhenReservationFitsWithinBudget() { + // 50 MB used, 20 MB reserved in-flight (70 MB committed). 20 MB query fits in 30 MB remaining. + manager.registerMesh(entityId: 1, meshSizeBytes: 50 * 1024 * 1024) + _ = manager.reserveTexture(sizeBytes: 20 * 1024 * 1024) + + XCTAssertTrue(manager.canAcceptTexture(sizeBytes: 20 * 1024 * 1024), + "canAcceptTexture should return true when reserved + query still fits in budget") + + manager.releaseTextureReservation(sizeBytes: 20 * 1024 * 1024) // cleanup + } + + func testClearResetsInFlightReservation() { + // Reserve some bytes, then clear. After clear a previously-oversized reservation must succeed. + _ = manager.reserveTexture(sizeBytes: 90 * 1024 * 1024) + manager.clear() // resets everything including inFlightTextureReservation + + // With a fresh state, a 50 MB reservation should succeed even though it was blocked before. + XCTAssertTrue(manager.reserveTexture(sizeBytes: 50 * 1024 * 1024), + "clear() must reset inFlightTextureReservation to zero") + manager.releaseTextureReservation(sizeBytes: 50 * 1024 * 1024) // cleanup + } } diff --git a/Tests/UntoldEngineTests/ProgressiveAssetLoaderTests.swift b/Tests/UntoldEngineTests/ProgressiveAssetLoaderTests.swift new file mode 100644 index 000000000..fb50a25f9 --- /dev/null +++ b/Tests/UntoldEngineTests/ProgressiveAssetLoaderTests.swift @@ -0,0 +1,782 @@ +// +// ProgressiveAssetLoaderTests.swift +// UntoldEngineTests +// +// 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 Metal +import MetalKit +import ModelIO +@testable import UntoldEngine +import XCTest + +// MARK: - ProgressiveAssetLoader CPU Registry Tests + +/// Tests for ProgressiveAssetLoader's CPU mesh registry. +/// +/// ProgressiveAssetLoader is now a pure CPU registry: it stores MDLMesh data +/// keyed by entity ID so GeometryStreamingSystem can upload each mesh on demand +/// without re-reading disk. The old per-frame job scheduler (ProgressiveLoadJob, +/// PendingObjectItem, tick() budget, round-robin) was removed; tick() is a +/// retained no-op for call-site compatibility. +@MainActor +final class ProgressiveAssetLoaderRegistryTests: XCTestCase { + var loader: ProgressiveAssetLoader! + var device: MTLDevice! + var textureLoader: TextureLoader! + + override func setUp() async throws { + loader = ProgressiveAssetLoader.shared + loader.cancelAll() + + guard let mtlDevice = MTLCreateSystemDefaultDevice() else { + XCTFail("No Metal device available — skipping ProgressiveAssetLoader tests") + return + } + device = mtlDevice + renderInfo.device = mtlDevice + textureLoader = TextureLoader(device: mtlDevice) + } + + override func tearDown() async throws { + loader.cancelAll() + device = nil + textureLoader = nil + } + + // MARK: - Helpers + + /// Builds a CPUMeshEntry with a plain MDLObject (no mesh data) and a known + /// estimatedGPUBytes value so registry round-trips can be verified. + private func makeEntry(estimatedGPUBytes: Int = 0) -> ProgressiveAssetLoader.CPUMeshEntry { + ProgressiveAssetLoader.CPUMeshEntry( + object: MDLObject(), + vertexDescriptor: MDLVertexDescriptor(), + textureLoader: textureLoader, + device: device, + url: URL(fileURLWithPath: "/dev/null"), + filename: "test", + withExtension: "usdz", + uniqueAssetName: "TestMesh#0", + estimatedGPUBytes: estimatedGPUBytes, + residencyPolicy: .fullLoad + ) + } + + // MARK: - tick() no-op + + func testTickIsNoOp() { + // tick() is retained only for call-site compatibility; it must not crash + // and must leave the registry untouched. + let entityId: EntityID = 1 + loader.storeCPUMesh(makeEntry(), for: entityId) + + loader.tick() + loader.tick() + loader.tick() + + XCTAssertNotNil(loader.retrieveCPUMesh(for: entityId), + "tick() must not clear CPU registry entries") + } + + // MARK: - storeCPUMesh / retrieveCPUMesh + + func testStoreThenRetrieve_returnsStoredEntry() { + let entityId: EntityID = 10 + let entry = makeEntry(estimatedGPUBytes: 512_000) + loader.storeCPUMesh(entry, for: entityId) + + let retrieved = loader.retrieveCPUMesh(for: entityId) + XCTAssertNotNil(retrieved, "Entry should be present after store") + } + + func testRetrieve_returnsNilForUnknownEntity() { + XCTAssertNil(loader.retrieveCPUMesh(for: 99999), + "Retrieving an unknown entity ID should return nil") + } + + func testStore_overwritesPreviousEntry() { + let entityId: EntityID = 20 + loader.storeCPUMesh(makeEntry(estimatedGPUBytes: 100), for: entityId) + loader.storeCPUMesh(makeEntry(estimatedGPUBytes: 200), for: entityId) + + let retrieved = loader.retrieveCPUMesh(for: entityId) + XCTAssertEqual(retrieved?.estimatedGPUBytes, 200, + "Second store should overwrite the first") + } + + // MARK: - estimatedGPUBytes round-trip + + func testEstimatedGPUBytes_survivesStoreRetrieveRoundTrip() { + let entityId: EntityID = 30 + let expectedBytes = 1_234_567 + loader.storeCPUMesh(makeEntry(estimatedGPUBytes: expectedBytes), for: entityId) + + let retrieved = loader.retrieveCPUMesh(for: entityId) + XCTAssertEqual(retrieved?.estimatedGPUBytes, expectedBytes, + "estimatedGPUBytes must survive the store / retrieve round-trip") + } + + func testEstimatedGPUBytes_zeroIsValid() { + let entityId: EntityID = 31 + loader.storeCPUMesh(makeEntry(estimatedGPUBytes: 0), for: entityId) + XCTAssertEqual(loader.retrieveCPUMesh(for: entityId)?.estimatedGPUBytes, 0) + } + + // MARK: - removeCPUMesh + + func testRemoveCPUMesh_entryIsAbsentAfterRemoval() { + let entityId: EntityID = 40 + loader.storeCPUMesh(makeEntry(), for: entityId) + XCTAssertNotNil(loader.retrieveCPUMesh(for: entityId)) + + loader.removeCPUMesh(for: entityId) + XCTAssertNil(loader.retrieveCPUMesh(for: entityId), + "Entry should be absent after removeCPUMesh") + } + + func testRemoveCPUMesh_unknownEntityIsNoOp() { + // Must not crash when removing an entity that was never stored. + loader.removeCPUMesh(for: 88888) + } + + func testRemoveCPUMesh_doesNotAffectOtherEntries() { + let keepId: EntityID = 50 + let removeId: EntityID = 51 + loader.storeCPUMesh(makeEntry(), for: keepId) + loader.storeCPUMesh(makeEntry(), for: removeId) + + loader.removeCPUMesh(for: removeId) + + XCTAssertNotNil(loader.retrieveCPUMesh(for: keepId), + "Removing one entity must not affect other registry entries") + XCTAssertNil(loader.retrieveCPUMesh(for: removeId)) + } + + // MARK: - cancelAll + + func testCancelAll_clearsCPURegistry() { + loader.storeCPUMesh(makeEntry(), for: 60) + loader.storeCPUMesh(makeEntry(), for: 61) + loader.storeCPUMesh(makeEntry(), for: 62) + + loader.cancelAll() + + XCTAssertNil(loader.retrieveCPUMesh(for: 60)) + XCTAssertNil(loader.retrieveCPUMesh(for: 61)) + XCTAssertNil(loader.retrieveCPUMesh(for: 62)) + } + + func testCancelAll_onEmptyRegistryIsNoOp() { + // Must not crash when called on an already-empty registry. + loader.cancelAll() + loader.cancelAll() + } + + // MARK: - registerChildren / removeOutOfCoreAsset + + func testRemoveOutOfCoreAsset_removesRegisteredChildren() { + let rootId: EntityID = 70 + let childIds: [EntityID] = [71, 72, 73] + + for id in childIds { + loader.storeCPUMesh(makeEntry(), for: id) + } + loader.registerChildren(childIds, for: rootId) + + loader.removeOutOfCoreAsset(rootEntityId: rootId) + + for id in childIds { + XCTAssertNil(loader.retrieveCPUMesh(for: id), + "Child \(id) should be cleared after removeOutOfCoreAsset") + } + } + + func testRemoveOutOfCoreAsset_doesNotAffectUnrelatedEntries() { + let rootId: EntityID = 80 + let childId: EntityID = 81 + let otherId: EntityID = 82 + + loader.storeCPUMesh(makeEntry(), for: childId) + loader.storeCPUMesh(makeEntry(), for: otherId) + loader.registerChildren([childId], for: rootId) + + loader.removeOutOfCoreAsset(rootEntityId: rootId) + + XCTAssertNil(loader.retrieveCPUMesh(for: childId), + "Registered child should be removed") + XCTAssertNotNil(loader.retrieveCPUMesh(for: otherId), + "Unrelated entry must not be removed") + } + + func testRemoveOutOfCoreAsset_unknownRootIsNoOp() { + // Must not crash when called for a root that was never registered. + loader.removeOutOfCoreAsset(rootEntityId: 99000) + } + + func testRemoveOutOfCoreAsset_calledTwiceIsIdempotent() { + let rootId: EntityID = 90 + let childId: EntityID = 91 + loader.storeCPUMesh(makeEntry(), for: childId) + loader.registerChildren([childId], for: rootId) + + loader.removeOutOfCoreAsset(rootEntityId: rootId) + loader.removeOutOfCoreAsset(rootEntityId: rootId) // second call must not crash + } + + // MARK: - Configuration + + func testFileSizeThresholdBytesDefaultIs50MB() { + XCTAssertEqual(loader.fileSizeThresholdBytes, 50 * 1024 * 1024) + } + + func testOutOfCoreObjectCountThresholdDefaultIs50() { + XCTAssertEqual(loader.outOfCoreObjectCountThreshold, 50) + } + + func testConfigurationPropertiesAreWritable() { + let originalSize = loader.fileSizeThresholdBytes + let originalCount = loader.outOfCoreObjectCountThreshold + + loader.fileSizeThresholdBytes = 100 * 1024 * 1024 + loader.outOfCoreObjectCountThreshold = 200 + + XCTAssertEqual(loader.fileSizeThresholdBytes, 100 * 1024 * 1024) + XCTAssertEqual(loader.outOfCoreObjectCountThreshold, 200) + + // Restore defaults so other tests are not affected. + loader.fileSizeThresholdBytes = originalSize + loader.outOfCoreObjectCountThreshold = originalCount + } + + // MARK: - textureLoadingEnabled flag + + func testTextureLoadingEnabledDefaultIsTrue() { + XCTAssertTrue(loader.textureLoadingEnabled, "Texture loading should be enabled by default") + } + + func testTextureLoadingEnabledIsWritable() { + loader.textureLoadingEnabled = false + XCTAssertFalse(loader.textureLoadingEnabled) + loader.textureLoadingEnabled = true + XCTAssertTrue(loader.textureLoadingEnabled) + } + + func testEnsureTexturesLoaded_skipsLoadWhenDisabled() { + // Register a fake asset reference so ensureTexturesLoaded would normally call loadTextures() + // (with textureLoadingEnabled = false it must not crash, and assetTexturesLoaded + // must remain empty so a future re-enable actually calls loadTextures()). + let rootId: EntityID = 9999 + + loader.textureLoadingEnabled = false + // Should not crash; no textures loaded. + loader.acquireAssetTextureLock(for: rootId) + loader.ensureTexturesLoaded(for: rootId) + loader.releaseAssetTextureLock(for: rootId) + + // After re-enabling, a second call should be allowed (entity not in assetTexturesLoaded). + loader.textureLoadingEnabled = true + // Just validate no crash and flag is restored. + XCTAssertTrue(loader.textureLoadingEnabled) + + loader.textureLoadingEnabled = true // restore + } +} + +// MARK: - V2 Warm/Cold Residency Lifecycle Tests + +/// Tests for the warm/cold residency lifecycle introduced in V2. +/// +/// CPU-warm: MDLAsset + CPUMeshEntry objects are alive in the registry. +/// CPU-cold: MDLAsset released; rehydration context (URL + policy) retained for re-parse. +/// +/// These tests are purely CPU-side — no GPU, no disk I/O. +@MainActor +final class ProgressiveAssetLoaderWarmColdTests: XCTestCase { + var loader: ProgressiveAssetLoader! + var device: MTLDevice! + var textureLoader: TextureLoader! + + override func setUp() async throws { + loader = ProgressiveAssetLoader.shared + loader.cancelAll() + + guard let mtlDevice = MTLCreateSystemDefaultDevice() else { + XCTFail("No Metal device available") + return + } + device = mtlDevice + renderInfo.device = mtlDevice + textureLoader = TextureLoader(device: mtlDevice) + } + + override func tearDown() async throws { + loader.cancelAll() + device = nil + textureLoader = nil + } + + // MARK: - Helpers + + private func makeEntry(estimatedGPUBytes: Int = 0) -> ProgressiveAssetLoader.CPUMeshEntry { + ProgressiveAssetLoader.CPUMeshEntry( + object: MDLObject(), + vertexDescriptor: MDLVertexDescriptor(), + textureLoader: textureLoader, + device: device, + url: URL(fileURLWithPath: "/dev/null"), + filename: "test", + withExtension: "usdz", + uniqueAssetName: "TestMesh#0", + estimatedGPUBytes: estimatedGPUBytes, + residencyPolicy: .fullLoad + ) + } + + // MARK: - Test 1: isColdRoot returns false before releaseWarmAsset + + func testIsColdRoot_falseBeforeRelease() { + let rootId: EntityID = 200 + loader.registerChildren([201, 202], for: rootId) + XCTAssertFalse(loader.isColdRoot(rootId), + "Asset should start warm (isColdRoot must be false before releaseWarmAsset)") + } + + // MARK: - Test 2: releaseWarmAsset transitions to cold + + func testReleaseWarmAsset_transitionsToCold() { + let rootId: EntityID = 210 + let childIds: [EntityID] = [211, 212] + + for id in childIds { + loader.storeCPUMesh(makeEntry(), for: id) + } + loader.registerChildren(childIds, for: rootId) + + loader.releaseWarmAsset(rootEntityId: rootId) + + XCTAssertTrue(loader.isColdRoot(rootId), + "isColdRoot must be true after releaseWarmAsset") + } + + // MARK: - Test 3: releaseWarmAsset clears child CPU entries + + func testReleaseWarmAsset_clearsCPUEntriesForChildren() { + let rootId: EntityID = 220 + let childIds: [EntityID] = [221, 222, 223] + + for id in childIds { + loader.storeCPUMesh(makeEntry(estimatedGPUBytes: 1024), for: id) + } + loader.registerChildren(childIds, for: rootId) + + loader.releaseWarmAsset(rootEntityId: rootId) + + for id in childIds { + XCTAssertNil(loader.retrieveCPUMesh(for: id), + "Child \(id) CPUMeshEntry must be cleared after releaseWarmAsset") + } + } + + // MARK: - Test 4: rehydration context survives releaseWarmAsset + + func testRehydrationContext_survivesReleaseWarmAsset() { + let rootId: EntityID = 230 + let testURL = URL(fileURLWithPath: "/tmp/test.usdz") + let policy = AssetLoadingPolicy.fullLoad + + loader.registerChildren([231], for: rootId) + loader.storeRootRehydrationContext(url: testURL, policy: policy, for: rootId) + loader.releaseWarmAsset(rootEntityId: rootId) + + let context = loader.rehydrationContext(for: rootId) + XCTAssertNotNil(context, "Rehydration context must survive releaseWarmAsset") + XCTAssertEqual(context?.url, testURL, "Rehydration context URL must be preserved") + } + + // MARK: - Test 5: markAsWarm restores warm state + + func testMarkAsWarm_restoringWarmStateAfterCold() { + let rootId: EntityID = 240 + loader.registerChildren([241], for: rootId) + loader.releaseWarmAsset(rootEntityId: rootId) + + XCTAssertTrue(loader.isColdRoot(rootId), "Pre-condition: must be cold") + + loader.markAsWarm(rootEntityId: rootId) + + XCTAssertFalse(loader.isColdRoot(rootId), + "isColdRoot must be false after markAsWarm") + } + + // MARK: - Test 6: getOrCreateRehydrationTask returns same task for concurrent calls + + func testGetOrCreateRehydrationTask_factoryCalledOnlyOnceForDuplicateCalls() { + let rootId: EntityID = 250 + var factoryCallCount = 0 + + let task1 = loader.getOrCreateRehydrationTask(for: rootId) { + factoryCallCount += 1 + return Task { true } + } + _ = loader.getOrCreateRehydrationTask(for: rootId) { + factoryCallCount += 1 + return Task { true } + } + + XCTAssertEqual(factoryCallCount, 1, + "Factory must be called exactly once even when called twice for the same root") + + loader.clearRehydrationTask(for: rootId) + task1.cancel() + } + + // MARK: - Test 7: clearRehydrationTask causes next call to create a new task + + func testClearRehydrationTask_allowsNewTaskOnNextCall() { + let rootId: EntityID = 260 + var factoryCallCount = 0 + + let task1 = loader.getOrCreateRehydrationTask(for: rootId) { + factoryCallCount += 1 + return Task { false } + } + task1.cancel() + loader.clearRehydrationTask(for: rootId) + + let task2 = loader.getOrCreateRehydrationTask(for: rootId) { + factoryCallCount += 1 + return Task { true } + } + XCTAssertEqual(factoryCallCount, 2, + "After clearRehydrationTask, factory must be called again for the same root") + + loader.clearRehydrationTask(for: rootId) + task2.cancel() + } + + // MARK: - Test 8: removeOutOfCoreAsset clears cold state + + func testRemoveOutOfCoreAsset_clearsColdState() { + let rootId: EntityID = 270 + let testURL = URL(fileURLWithPath: "/tmp/remove_test.usdz") + + loader.registerChildren([271], for: rootId) + loader.storeRootRehydrationContext(url: testURL, policy: .fullLoad, for: rootId) + loader.releaseWarmAsset(rootEntityId: rootId) + + XCTAssertTrue(loader.isColdRoot(rootId), "Pre-condition: must be cold") + + loader.removeOutOfCoreAsset(rootEntityId: rootId) + + XCTAssertFalse(loader.isColdRoot(rootId), + "removeOutOfCoreAsset must clear the cold state") + XCTAssertNil(loader.rehydrationContext(for: rootId), + "removeOutOfCoreAsset must remove the rehydration context") + } + + // MARK: - Test 9: cancelAll clears all cold state + + func testCancelAll_clearsAllColdState() { + let rootIds: [EntityID] = [280, 281, 282] + for rootId in rootIds { + loader.registerChildren([rootId + 100], for: rootId) + loader.storeRootRehydrationContext( + url: URL(fileURLWithPath: "/tmp/asset_\(rootId).usdz"), + policy: .fullLoad, + for: rootId + ) + loader.releaseWarmAsset(rootEntityId: rootId) + } + + loader.cancelAll() + + for rootId in rootIds { + XCTAssertFalse(loader.isColdRoot(rootId), + "cancelAll must clear cold state for root \(rootId)") + XCTAssertNil(loader.rehydrationContext(for: rootId), + "cancelAll must remove rehydration context for root \(rootId)") + } + } + + // MARK: - Test 10: getChildren returns children in registration order + + func testGetChildren_returnsChildrenInRegistrationOrder() { + let rootId: EntityID = 290 + let childIds: [EntityID] = [291, 292, 293, 294, 295] + loader.registerChildren(childIds, for: rootId) + + let retrieved = loader.getChildren(for: rootId) + XCTAssertEqual(retrieved, childIds, + "getChildren must return children in the same order they were registered") + } + + func testGetChildren_returnsEmptyForUnknownRoot() { + XCTAssertTrue(loader.getChildren(for: 99999).isEmpty, + "getChildren must return empty array for an unregistered root") + } + + // MARK: - Test 11: releaseWarmAsset on already-cold root is a no-op + + func testReleaseWarmAsset_calledTwiceIsNoOp() { + let rootId: EntityID = 300 + loader.registerChildren([301, 302], for: rootId) + loader.releaseWarmAsset(rootEntityId: rootId) + loader.releaseWarmAsset(rootEntityId: rootId) // Must not crash or corrupt state + + XCTAssertTrue(loader.isColdRoot(rootId), + "Root should remain cold after a redundant releaseWarmAsset call") + } +} + +// MARK: - LOD CPU Registry Tests + +/// Tests for ProgressiveAssetLoader's LOD CPU registry (cpuLODRegistry). +/// +/// The LOD+OOC path stores one CPUMeshEntry per LOD level per group entity in +/// cpuLODRegistry, keyed by (EntityID, lodIndex). These tests verify all CRUD +/// operations and lifecycle interactions (releaseWarmAsset, removeOutOfCoreAsset, cancelAll). +@MainActor +final class ProgressiveAssetLoaderLODRegistryTests: XCTestCase { + var loader: ProgressiveAssetLoader! + var device: MTLDevice! + var textureLoader: TextureLoader! + + override func setUp() async throws { + loader = ProgressiveAssetLoader.shared + loader.cancelAll() + guard let mtlDevice = MTLCreateSystemDefaultDevice() else { + XCTFail("No Metal device available") + return + } + device = mtlDevice + renderInfo.device = mtlDevice + textureLoader = TextureLoader(device: mtlDevice) + } + + override func tearDown() async throws { + loader.cancelAll() + device = nil + textureLoader = nil + } + + private func makeEntry(uniqueAssetName: String = "TestMesh_LOD0", estimatedGPUBytes: Int = 0) -> ProgressiveAssetLoader.CPUMeshEntry { + ProgressiveAssetLoader.CPUMeshEntry( + object: MDLObject(), + vertexDescriptor: MDLVertexDescriptor(), + textureLoader: textureLoader, + device: device, + url: URL(fileURLWithPath: "/dev/null"), + filename: "test", + withExtension: "usdz", + uniqueAssetName: uniqueAssetName, + estimatedGPUBytes: estimatedGPUBytes, + residencyPolicy: .fullLoad + ) + } + + // MARK: - storeCPULODMesh / retrieveCPULODMesh + + func testStoreThenRetrieve_returnsStoredLODEntry() { + let entityId: EntityID = 1000 + let entry = makeEntry(uniqueAssetName: "Tree_LOD0", estimatedGPUBytes: 256_000) + loader.storeCPULODMesh(entry, for: entityId, lodIndex: 0) + + let retrieved = loader.retrieveCPULODMesh(for: entityId, lodIndex: 0) + XCTAssertNotNil(retrieved, "LOD entry should be present after store") + XCTAssertEqual(retrieved?.uniqueAssetName, "Tree_LOD0") + XCTAssertEqual(retrieved?.estimatedGPUBytes, 256_000) + } + + func testRetrieve_returnsNilForUnknownEntityOrLOD() { + XCTAssertNil(loader.retrieveCPULODMesh(for: 99999, lodIndex: 0), + "Unknown entity should return nil") + + let entityId: EntityID = 1001 + loader.storeCPULODMesh(makeEntry(), for: entityId, lodIndex: 0) + XCTAssertNil(loader.retrieveCPULODMesh(for: entityId, lodIndex: 5), + "Unknown LOD index should return nil") + } + + func testStore_multipleLODLevelsForSameEntity() { + let entityId: EntityID = 1002 + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Tree_LOD0"), for: entityId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Tree_LOD1"), for: entityId, lodIndex: 1) + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Tree_LOD2"), for: entityId, lodIndex: 2) + + XCTAssertEqual(loader.retrieveCPULODMesh(for: entityId, lodIndex: 0)?.uniqueAssetName, "Tree_LOD0") + XCTAssertEqual(loader.retrieveCPULODMesh(for: entityId, lodIndex: 1)?.uniqueAssetName, "Tree_LOD1") + XCTAssertEqual(loader.retrieveCPULODMesh(for: entityId, lodIndex: 2)?.uniqueAssetName, "Tree_LOD2") + } + + func testStore_overwritesPreviousEntryForSameLODIndex() { + let entityId: EntityID = 1003 + loader.storeCPULODMesh(makeEntry(estimatedGPUBytes: 100), for: entityId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(estimatedGPUBytes: 200), for: entityId, lodIndex: 0) + + XCTAssertEqual(loader.retrieveCPULODMesh(for: entityId, lodIndex: 0)?.estimatedGPUBytes, 200, + "Second store should overwrite the first for the same LOD index") + } + + // MARK: - retrieveAllCPULODMeshes + + func testRetrieveAll_returnsAllStoredLevels() { + let entityId: EntityID = 1010 + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Rock_LOD0"), for: entityId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Rock_LOD1"), for: entityId, lodIndex: 1) + + let all = loader.retrieveAllCPULODMeshes(for: entityId) + XCTAssertNotNil(all, "Should return a dictionary when entries exist") + XCTAssertEqual(all?.count, 2, "Should have 2 LOD entries") + XCTAssertEqual(all?[0]?.uniqueAssetName, "Rock_LOD0") + XCTAssertEqual(all?[1]?.uniqueAssetName, "Rock_LOD1") + } + + func testRetrieveAll_returnsNilForUnknownEntity() { + XCTAssertNil(loader.retrieveAllCPULODMeshes(for: 99998), + "Unknown entity should return nil") + } + + // MARK: - hasCPULODData + + func testHasCPULODData_trueAfterStoring() { + let entityId: EntityID = 1020 + XCTAssertFalse(loader.hasCPULODData(for: entityId), "Should be false before any store") + loader.storeCPULODMesh(makeEntry(), for: entityId, lodIndex: 0) + XCTAssertTrue(loader.hasCPULODData(for: entityId), "Should be true after storing a LOD entry") + } + + func testHasCPULODData_falseForRegularCPUMeshEntry() { + let entityId: EntityID = 1021 + // Storing in cpuMeshRegistry (not cpuLODRegistry) must not affect hasCPULODData + loader.storeCPUMesh(makeEntry(), for: entityId) + XCTAssertFalse(loader.hasCPULODData(for: entityId), + "hasCPULODData should remain false for regular OOC entries") + } + + func testHasCPULODData_falseAfterRemoveCPULODEntry() { + let entityId: EntityID = 1022 + loader.storeCPULODMesh(makeEntry(), for: entityId, lodIndex: 0) + XCTAssertTrue(loader.hasCPULODData(for: entityId)) + + loader.removeCPULODEntry(for: entityId) + XCTAssertFalse(loader.hasCPULODData(for: entityId), + "hasCPULODData should be false after removeCPULODEntry") + } + + // MARK: - removeCPULODEntry + + func testRemoveCPULODEntry_removesAllLevels() { + let entityId: EntityID = 1030 + loader.storeCPULODMesh(makeEntry(), for: entityId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(), for: entityId, lodIndex: 1) + loader.storeCPULODMesh(makeEntry(), for: entityId, lodIndex: 2) + + loader.removeCPULODEntry(for: entityId) + + XCTAssertNil(loader.retrieveCPULODMesh(for: entityId, lodIndex: 0)) + XCTAssertNil(loader.retrieveCPULODMesh(for: entityId, lodIndex: 1)) + XCTAssertNil(loader.retrieveCPULODMesh(for: entityId, lodIndex: 2)) + } + + func testRemoveCPULODEntry_unknownEntityIsNoOp() { + loader.removeCPULODEntry(for: 99997) // Must not crash + } + + func testRemoveCPULODEntry_doesNotAffectOtherEntities() { + let keepId: EntityID = 1040 + let removeId: EntityID = 1041 + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Tree_LOD0"), for: keepId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Rock_LOD0"), for: removeId, lodIndex: 0) + + loader.removeCPULODEntry(for: removeId) + + XCTAssertNotNil(loader.retrieveCPULODMesh(for: keepId, lodIndex: 0), + "Removing one entity's LOD data must not affect other entities") + } + + // MARK: - releaseWarmAsset clears LOD entries for children + + func testReleaseWarmAsset_clearsCPULODEntriesForChildren() { + let rootId: EntityID = 1050 + let groupId: EntityID = 1051 + + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Tree_LOD0"), for: groupId, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Tree_LOD1"), for: groupId, lodIndex: 1) + loader.registerChildren([groupId], for: rootId) + + loader.releaseWarmAsset(rootEntityId: rootId) + + XCTAssertNil(loader.retrieveCPULODMesh(for: groupId, lodIndex: 0), + "LOD entry for LOD0 should be cleared after releaseWarmAsset") + XCTAssertNil(loader.retrieveCPULODMesh(for: groupId, lodIndex: 1), + "LOD entry for LOD1 should be cleared after releaseWarmAsset") + XCTAssertFalse(loader.hasCPULODData(for: groupId), + "hasCPULODData should be false after releaseWarmAsset") + } + + // MARK: - removeOutOfCoreAsset clears LOD entries + + func testRemoveOutOfCoreAsset_clearsCPULODEntriesForChildren() { + let rootId: EntityID = 1060 + let groupId1: EntityID = 1061 + let groupId2: EntityID = 1062 + + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Tree_LOD0"), for: groupId1, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "Rock_LOD0"), for: groupId2, lodIndex: 0) + loader.registerChildren([groupId1, groupId2], for: rootId) + + loader.removeOutOfCoreAsset(rootEntityId: rootId) + + XCTAssertFalse(loader.hasCPULODData(for: groupId1), + "Group 1 LOD data should be cleared after removeOutOfCoreAsset") + XCTAssertFalse(loader.hasCPULODData(for: groupId2), + "Group 2 LOD data should be cleared after removeOutOfCoreAsset") + } + + // MARK: - cancelAll clears LOD registry + + func testCancelAll_clearsCPULODRegistry() { + let entityId1: EntityID = 1070 + let entityId2: EntityID = 1071 + loader.storeCPULODMesh(makeEntry(), for: entityId1, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(), for: entityId2, lodIndex: 0) + loader.storeCPULODMesh(makeEntry(), for: entityId2, lodIndex: 1) + + loader.cancelAll() + + XCTAssertFalse(loader.hasCPULODData(for: entityId1), + "cancelAll must clear all LOD registry entries") + XCTAssertFalse(loader.hasCPULODData(for: entityId2), + "cancelAll must clear all LOD registry entries") + } + + // MARK: - LOD and regular OOC registries are independent + + func testLODAndRegularRegistriesAreIndependent() { + let entityId: EntityID = 1080 + loader.storeCPUMesh(makeEntry(uniqueAssetName: "Regular#0"), for: entityId) + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "LOD_LOD0"), for: entityId, lodIndex: 0) + + // Both should be independently accessible + XCTAssertNotNil(loader.retrieveCPUMesh(for: entityId), + "Regular OOC entry should be accessible independently") + XCTAssertNotNil(loader.retrieveCPULODMesh(for: entityId, lodIndex: 0), + "LOD OOC entry should be accessible independently") + + // Removing from LOD registry must not affect regular registry + loader.removeCPULODEntry(for: entityId) + XCTAssertNotNil(loader.retrieveCPUMesh(for: entityId), + "Regular OOC entry must survive removeCPULODEntry") + + // Removing from regular registry must not affect LOD registry data + loader.storeCPULODMesh(makeEntry(uniqueAssetName: "LOD_LOD0"), for: entityId, lodIndex: 0) + loader.removeCPUMesh(for: entityId) + XCTAssertTrue(loader.hasCPULODData(for: entityId), + "LOD OOC data must survive removeCPUMesh") + } +} diff --git a/Tests/UntoldEngineTests/TextureLoaderBracketParserTests.swift b/Tests/UntoldEngineTests/TextureLoaderBracketParserTests.swift new file mode 100644 index 000000000..1ac8f6c46 --- /dev/null +++ b/Tests/UntoldEngineTests/TextureLoaderBracketParserTests.swift @@ -0,0 +1,111 @@ +// +// TextureLoaderBracketParserTests.swift +// UntoldEngineTests +// +// 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 UntoldEngine +import XCTest + +/// Unit tests for `TextureLoader.parseUSDZBracketPath`. +/// +/// This static function is the only piece of Option-B URL-based texture loading that +/// contains non-trivial logic (string parsing + URL construction). All other code paths +/// delegate to MTKTextureLoader, which can only be exercised on-device. +final class TextureLoaderBracketParserTests: XCTestCase { + // MARK: - Valid inputs + + func testTypicalUSDZEmbeddedPath_returnsURLAndInnerPath() { + let str = "file:///var/containers/scene.usdz[0/texture_base_color.png]" + let result = TextureLoader.parseUSDZBracketPath(from: str) + + XCTAssertNotNil(result) + XCTAssertEqual(result?.usdzURL, URL(string: "file:///var/containers/scene.usdz")) + XCTAssertEqual(result?.innerPath, "0/texture_base_color.png") + } + + func testInnerPathWithSubdirectory_preservesSlashes() { + let str = "file:///path/to/my%20scene.usdz[textures/diffuse/albedo.png]" + let result = TextureLoader.parseUSDZBracketPath(from: str) + + XCTAssertNotNil(result) + XCTAssertEqual(result?.innerPath, "textures/diffuse/albedo.png") + } + + func testBackslashesInInnerPath_areNormalized() { + let str = "file:///models/city.usdz[0\\building_wall.jpg]" + let result = TextureLoader.parseUSDZBracketPath(from: str) + + XCTAssertNotNil(result) + XCTAssertEqual(result?.innerPath, "0/building_wall.jpg") + } + + func testLeadingAndTrailingWhitespaceInInnerPath_isTrimmed() { + let str = "file:///models/props.usdz[ 0/prop_diffuse.png ]" + let result = TextureLoader.parseUSDZBracketPath(from: str) + + XCTAssertNotNil(result) + XCTAssertEqual(result?.innerPath, "0/prop_diffuse.png") + } + + func testLastBracketPairIsUsed_whenMultipleBracketsPresent() { + // lastIndex(of:) is used intentionally — the USDZ path itself never contains brackets. + let str = "file:///path/to/[weird]name.usdz[0/texture.png]" + let result = TextureLoader.parseUSDZBracketPath(from: str) + + XCTAssertNotNil(result) + XCTAssertEqual(result?.innerPath, "0/texture.png") + } + + // MARK: - Invalid / edge-case inputs + + func testEmptyString_returnsNil() { + XCTAssertNil(TextureLoader.parseUSDZBracketPath(from: "")) + } + + func testStringWithNoBrackets_returnsNil() { + XCTAssertNil(TextureLoader.parseUSDZBracketPath(from: "file:///path/to/scene.usdz")) + } + + func testEmptyInnerPath_returnsNil() { + XCTAssertNil(TextureLoader.parseUSDZBracketPath(from: "file:///scene.usdz[]")) + } + + func testWhitespaceOnlyInnerPath_returnsNil() { + XCTAssertNil(TextureLoader.parseUSDZBracketPath(from: "file:///scene.usdz[ ]")) + } + + func testNonFileURL_returnsNil() { + // Only file:// URLs should be treated as USDZ package paths. + XCTAssertNil(TextureLoader.parseUSDZBracketPath(from: "https://example.com/scene.usdz[0/tex.png]")) + } + + func testOpenBracketAfterCloseBracket_returnsNil() { + // Malformed: "]" comes before "[" when searching from the end. + XCTAssertNil(TextureLoader.parseUSDZBracketPath(from: "file:///scene.usdz]0/tex.png[")) + } + + func testMissingCloseBracket_returnsNil() { + XCTAssertNil(TextureLoader.parseUSDZBracketPath(from: "file:///scene.usdz[0/tex.png")) + } + + func testMissingOpenBracket_returnsNil() { + XCTAssertNil(TextureLoader.parseUSDZBracketPath(from: "file:///scene.usdz 0/tex.png]")) + } + + // MARK: - Package URL construction + + func testPackageURLIsCorrectlyAssembled() { + let str = "file:///Users/harold/models/city.usdz[0/building_albedo.png]" + guard let parsed = TextureLoader.parseUSDZBracketPath(from: str) else { + XCTFail("Expected successful parse") + return + } + let packageURL = parsed.usdzURL.appendingPathComponent(parsed.innerPath) + XCTAssertEqual(packageURL.absoluteString, "file:///Users/harold/models/city.usdz/0/building_albedo.png") + } +} diff --git a/Tests/UntoldEngineTests/TextureStreamingSystemTests.swift b/Tests/UntoldEngineTests/TextureStreamingSystemTests.swift index 4242a73af..fde9a6348 100644 --- a/Tests/UntoldEngineTests/TextureStreamingSystemTests.swift +++ b/Tests/UntoldEngineTests/TextureStreamingSystemTests.swift @@ -173,4 +173,50 @@ final class TextureStreamingSystemTests: XCTestCase { system.update(cameraPosition: .zero, deltaTime: 1.0) XCTAssertLessThanOrEqual(system.getStats().activeOps, system.maxConcurrentOps) } + + // MARK: - Three-Tier TextureStreamingLevel + + func testTextureStreamingLevelHasThreeCases() { + // Ensure all three enum cases are distinct and can be compared. + XCTAssertNotEqual(TextureStreamingLevel.full, TextureStreamingLevel.capped) + XCTAssertNotEqual(TextureStreamingLevel.full, TextureStreamingLevel.minimum) + XCTAssertNotEqual(TextureStreamingLevel.capped, TextureStreamingLevel.minimum) + } + + func testTextureStreamingLevelFullIsDefaultValue() { + // .full is the sentinel "not yet streamed" state — verify its raw identity. + // (Material cannot be constructed without Metal state; this test confirms + // the enum's default value without needing a live GPU device.) + let defaultLevel = TextureStreamingLevel.full + XCTAssertNotEqual(defaultLevel, .capped) + XCTAssertNotEqual(defaultLevel, .minimum) + } + + func testStreamLevelThresholdCappedVsMinimum() { + // Simulate the three-tier streamLevel assignment used in scheduleResolutionChange. + // capturedMinimumDim = 256; anything <= 256 → .minimum, anything > 256 → .capped, nil → .full. + let minimumDim = 256 + + let resolveLevel: (Int?) -> TextureStreamingLevel = { targetMaxDimension in + guard let dim = targetMaxDimension else { return .full } + return dim <= minimumDim ? .minimum : .capped + } + + XCTAssertEqual(resolveLevel(nil), .full) + XCTAssertEqual(resolveLevel(256), .minimum) // exactly at minimum threshold + XCTAssertEqual(resolveLevel(192), .minimum) // below minimum threshold (visionOS) + XCTAssertEqual(resolveLevel(257), .capped) // just above minimum threshold + XCTAssertEqual(resolveLevel(1024), .capped) // medium dimension + } + + func testNormalizedMinimumDimensionIsPositive() { + system.minimumTextureDimension = 256 + XCTAssertGreaterThan(system.minimumTextureDimension, 0) + } + + func testMinimumDimensionBelowOrEqualToMaxDimension() { + system.maxTextureDimension = 1024 + system.minimumTextureDimension = 256 + XCTAssertLessThanOrEqual(system.minimumTextureDimension, system.maxTextureDimension) + } } diff --git a/docs/01-Intro.md b/docs/01-Intro.md index 81b5d6707..c1eff3f3a 100644 --- a/docs/01-Intro.md +++ b/docs/01-Intro.md @@ -2,117 +2,75 @@ slug: /intro --- -# Untold Engine Documentation +# Untold Engine -Welcome to the **Untold Engine documentation**. +Untold Engine is an **open-source 3D engine written in Swift and powered by Metal**, designed for Apple platforms including **macOS, iOS, and visionOS**. -These docs are the primary reference for working with the Untold Engine ecosystem — whether you are building a game, extending the engine, or contributing to the editor. +The project focuses on building a **clean, system-driven architecture** with modern rendering, an ECS-based gameplay model, and an extensible asset pipeline. ---- - -## What Is Untold Engine? - -![untoldengine](images/Editor/EditorMainShot.png) - -The Untold Engine strives to be a stable, performant, and developer-friendly 3D engine that empowers creativity, removes friction, and makes game development feel effortless for Apple developers - -The Untold Engine is an open-source 3D game engine under active development, designed for macOS, iOS, xrOS platforms. Written in Swift and powered by Metal, its goal is to simplify game creation with a clean, intuitive API. - -While the engine already supports many core systems like rendering, physics, and animation, there’s still much to build and improve. +The engine is under active development and continues to evolve as new systems and workflows are added. --- -## The Untold Engine Ecosystem - -Untold Engine is delivered through three closely related products: +## 🎯 Who is this for? -### Untold Engine Studio -A downloadable application that includes: -- The Untold Engine runtime -- The Untold Editor -- Built-in tools for scripting, assets, and scene editing +Untold Engine is designed for developers who: -This is the recommended starting point for most users. - ---- +- Want **full control over rendering and systems** +- Prefer working directly with **Swift + Metal** +- Are building **XR, 3D, or visualization applications** +- Need to handle **large scenes, streaming data, or custom pipelines** -### Untold Engine -The core engine runtime. +This is not a drag-and-drop editor-first engine — it is a **code-driven engine for developers who want to understand and shape the system**. -This is intended for: -- Engine developers -- Contributors -- Advanced users who want to modify or extend engine systems -Installation is performed via the command line. +Creator & Lead Developer: +http://www.haroldserrano.com --- -### Untold Editor -The editor application built on top of the engine runtime. +# 🚀 Try the Engine Right Now -This is intended for: -- Contributors working on editor features -- Developers extending tools and workflows +The fastest way to experience Untold Engine is to run the demo project. -The editor uses the same runtime as games, ensuring consistent behavior. +Clone the repository, run the engine and load a USDZ file: ---- - -## Choose Your Path - -These docs are organized around **how you intend to use the engine**. +```bash +git clone https://github.com/untoldengine/UntoldEngine.git +cd UntoldEngine +swift run DemoGame +``` -### Game Development -For developers building games using Untold Engine. +This will: -You will learn: -- How to create scenes visually -- How to write game logic (Swift or USC scripts) -- How to work with assets and entities -- How to build and run your game +- Build the engine using **Swift Package Manager** +- Compile the demo project +- Launch the demo so you can see the engine running immediately -Untold Engine supports two approaches for writing gameplay code: -- **Swift in Xcode** (recommended) - Full engine API access -- **USC Scripts** (experimental) - Component-based scripting - -Start here if your goal is to build a game. +No additional setup is required. --- -### Engine Development -For developers who want to understand or extend the engine itself. +## 🧱 Core Direction -You will learn: -- The engine architecture -- ECS and system execution -- Rendering and simulation internals -- How to contribute new engine features +Untold Engine is being developed with the following goals: -Start here if you want to work on the engine runtime. +- **Large Scene Rendering** + Striving to support LOD, geometry streaming, batching, and memory-aware systems for large datasets ---- +- **XR / visionOS Support** + Expanding support for spatial input, AR workflows, and Vision Pro experiences -### Editor Development -For contributors working on the Untold Editor. - -You will learn: -- Editor architecture -- Views, tools, and interaction models -- How the editor coordinates with the engine -- How to extend or add editor functionality - -Start here if you want to improve the editor. +- **Metal-First Architecture** + Keeping the rendering layer close to Metal to maintain performance and control --- -## Getting Started - -If you are unsure where to begin: - -- New users: **Game Development → Overview** -- Scripting users: **USC → Introduction** -- Contributors: **Engine Development → Architecture** +## 🖼 Example Use Cases -Each section is designed to stand on its own. +Untold Engine aims to support applications such as: +- XR applications (Vision Pro, ARKit-based apps) +- Large-scale scene visualization (cities, archviz, datasets) +- Custom rendering pipelines and experiments +- Simulation tools and interactive 3D systems diff --git a/docs/02-Getting Started/02-Installation.md b/docs/02-Getting Started/02-Installation.md deleted file mode 100644 index 89e99a7ee..000000000 --- a/docs/02-Getting Started/02-Installation.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -id: intro -title: Installation -sidebar_position: 1 ---- - -# Installation - -This page explains how to install **Untold Engine Studio**, the recommended way to get started with Untold Engine. - -Untold Engine Studio is a downloadable app that includes: -- The **Untold Engine** runtime -- The **Untold Editor** for building and editing games - -![editorbottomshot](../images/Editor/EditorBottomShot.png) - -If your goal is to **make games**, this is the only installation you need. - ---- - -## Recommended Installation (Untold Engine Studio) - -### 1. Download - -Download the latest version of **Untold Engine Studio** from the official website: - -[Download Releases](https://github.com/untoldengine/UntoldEditor/releases) - -The download is provided as a `.dmg` file for macOS. - ---- - -### 2. Install - -1. Open the downloaded `.dmg` file -2. Drag **Untold Engine Studio** into your `Applications` folder -3. Launch the app from `Applications` - -No additional setup is required. - ---- - -### 3. First Launch - -On first launch, Untold Engine Studio will: -- Initialize the engine runtime -- Set up the editor environment -- Prompt you to create or open a project - -From here, you can immediately: -- Create scenes visually -- Import 3D models and assets -- Write game logic (Swift in Xcode or USC scripts) -- Build and test your game - ---- - -## System Requirements - -- macOS (Apple Silicon recommended) -- Metal-capable GPU -- Keyboard and mouse - ---- - -## What You Get - -By installing Untold Engine Studio, you get: - -- A complete **game development environment** -- Visual editor for scenes, assets, and scripts -- Full **Untold Engine Swift API** for game logic in Xcode -- **USC scripting system** (experimental component-based scripting) -- Build and run support for macOS, iOS, and visionOS - -You do **not** need to install the engine or editor separately. - -### Two Ways to Write Game Logic - -Untold Engine Studio supports two approaches for writing gameplay code: - -**1. Swift in Xcode (Recommended)** -- Write game logic in `GameScene.swift` using the full Untold Engine API -- Complete control over game systems and performance -- Best for complex games and experienced developers -- Works seamlessly with Xcode debugging and profiling - -**2. USC Scripts (Experimental)** -- Component-based scripting attached to entities -- Write gameplay behaviors in the integrated script editor -- Good for prototyping and simple game mechanics -- API is experimental and subject to change - -You can **use both approaches** in the same project. - ---- - -## Alternative Installation: CLI Workflow - -For **advanced users** or those who prefer a **command-line workflow** without the visual editor, you can install the CLI tools. - -### When to Use CLI - -- You prefer working entirely in Xcode without a visual editor -- You want to script project creation and automation -- You're building tools or integrations on top of UntoldEngine - -### CLI Installation - -**1. Clone the repository:** - -```bash -git clone https://github.com/untoldengine/UntoldEngine.git -cd UntoldEngine -``` - -**2. Install the CLI globally:** - -```bash -./scripts/install-create.sh -``` - -**3. Verify installation:** - -```bash -untoldengine-create --version -untoldengine-create --help -``` - -### CLI Quick Start - -After installing the CLI, create a project from anywhere: - -```bash -# 1. Create project directory -cd ~/anywhere -mkdir MyGame && cd MyGame - -# 2. Create the project -untoldengine-create create MyGame - -# 3. Open in Xcode -open MyGame/MyGame.xcodeproj -``` - -For complete CLI documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - ---- - -### What You Get - -The CLI creates a complete, ready-to-run project: - -- **Xcode project** - Configured and ready to build -- **GameScene.swift** - Your game logic goes here -- **GameViewController.swift** - Renderer and view setup -- **GameData/** directory - All game assets location -- **Platform-specific** files (AppDelegate, Info.plist, etc.) - -### Project Structure - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Open this in Xcode - ├── project.yml # XcodeGen configuration - └── Sources/ - └── MyGame/ - ├── GameData/ # ← Put your assets here - │ ├── Models/ # 3D models - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # USC scripts - │ ├── Textures/ # Images - │ └── ... - ├── GameScene.swift # Your game logic - ├── GameViewController.swift # View controller - └── AppDelegate.swift # App entry point -``` - -### Platform Support - -The CLI supports multiple platforms: - -```bash -# macOS (default) -untoldengine-create create MyGame --platform macos - -# iOS -untoldengine-create create MyGame --platform ios - -# iOS with ARKit -untoldengine-create create MyGame --platform iosar - -# visionOS (Apple Vision Pro) -untoldengine-create create MyGame --platform visionos -``` - -### Development Workflow - -1. **Write code** in GameScene.swift (game logic) -2. **Add assets** to the GameData/ directory -3. **Build & run** in Xcode (Cmd+R) -4. **Iterate** - make changes and rebuild - -For complete CLI documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - ---- - -## Preloaded Assets - -To kickstart development, download prebuilt demo assets: - -- **Models**: Soccer stadium, player, ball, and more -- **Animations**: Running, idle, and other character motions -- **Textures**: Sample materials - -[Download Demo Assets v1.0](https://haroldserrano.gumroad.com/l/iqjlac) - -Extract and copy into your project's `GameData/` directory. - ---- diff --git a/docs/02-Getting Started/03-ChoosingYourPath.md b/docs/02-Getting Started/03-ChoosingYourPath.md deleted file mode 100644 index 4d003ace7..000000000 --- a/docs/02-Getting Started/03-ChoosingYourPath.md +++ /dev/null @@ -1,79 +0,0 @@ -# Choosing Your Path - -Untold Engine supports different types of developers. - -This page helps you choose the path that best matches what you want to do. - ---- - -## I Want to Make a Game - -Choose this path if your goal is to: -- Build gameplay -- Create scenes -- Write scripts -- Ship a game - -### What You’ll Use - -- **Untold Engine Studio** -- **USC scripting API** - -### Where to Start - -> **Game Development → Overview** - -You do not need to understand engine internals to make a game. - ---- - -## I Want to Improve the Engine - -Choose this path if you want to: -- Work on core engine systems -- Improve rendering, physics, or ECS -- Extend platform support - -### What You’ll Use - -- **Untold Engine (core)** -- Command-line tools -- Source builds - -### Where to Start - -> **Engine Development → Overview** - -This path assumes familiarity with engine concepts and systems programming. - ---- - -## I Want to Improve the Editor - -Choose this path if you want to: -- Improve the editor UI -- Add new tools or views -- Improve workflows and usability - -### What You’ll Use - -- **Untold Editor** -- Editor-specific APIs -- Engine integration points - -### Where to Start - -> **Editor Development → Overview** - -Editor development focuses on tooling rather than gameplay. - ---- - -## Not Sure Yet? - -If you’re not sure where to begin, start here: - -> **Game Development → Overview** - -You can always explore the other paths later. - diff --git a/docs/02-Getting Started/_category.json b/docs/02-Getting Started/_category.json deleted file mode 100644 index ac4e7d573..000000000 --- a/docs/02-Getting Started/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "01-Getting Starter", "position": 1, "collapsed": false } - diff --git a/docs/03-Game Development/01-Overview.md b/docs/03-Game Development/01-Overview.md deleted file mode 100644 index a41e30ce9..000000000 --- a/docs/03-Game Development/01-Overview.md +++ /dev/null @@ -1,116 +0,0 @@ -# Overview - -This section covers **game development** with the Untold Engine. - -You'll learn how to create games using **Untold Engine Studio**, with two approaches for writing game logic: - -1. **Swift in Xcode** - Full engine API access (recommended) -2. **USC Scripts** - Component-based scripting (experimental) - -![editorsideshotalt](../images/Editor/EditorSideShotWide-alt.png) - ---- - -## Two Approaches to Game Logic - -Untold Engine gives you flexibility in how you write gameplay code: - -### Option 1: Swift in Xcode (Recommended) - -Write game logic in `GameScene.swift` using the full Untold Engine Swift API: -- Complete control over game systems and performance -- Access to all engine features and APIs -- Seamless integration with Xcode debugging and profiling -- Best for complex games and experienced developers - -### Option 2: USC Scripts (Experimental) - -Write component-based scripts that attach to entities: -- Simpler, component-oriented approach -- Good for prototyping and simple game mechanics -- Edit scripts in the integrated editor or Xcode -- API is experimental and subject to change - -**You can use both approaches in the same project.** - ---- - -## The Development Workflow - -A typical development workflow with Untold Engine Studio: - -1. **Create a project** using the "New" button in Untold Engine Studio -2. **Compose scenes visually** using the editor -3. **Write game logic** (Swift in `GameScene.swift` or USC scripts) -4. **Add assets** to the GameData/ directory -5. **Build & run** in Xcode (Cmd+R) -6. **Iterate** quickly with visual feedback - ---- - -## Entry Point 1: GameScene.swift (Swift) - -When you create a project, you get a clean `GameScene.swift` file for writing game logic in Swift: - -```swift -class GameScene { - - init() { - // Configure asset paths - setupAssetPaths() - - // Load game content - loadBundledScripts() - loadAndPlayFirstScene() - - // Start game systems - startGameSystems() - } - - func update(deltaTime: Float) { - // Your game logic goes here - } - - func handleInput() { - // Handle user input here - } -} -``` - -**This is where your game comes to life.** Write Swift code, access the full engine API, and build your game. - ---- - -## Entry Point 2: USC Scripts (Experimental) - -Alternatively, write game logic as USC scripts that attach to entities: - -Create USC scripts from the **Script** menu in Untold Engine Studio, then attach them to entities in the Inspector. - ---- - - -## Project Structure - -Your generated project has everything you need: - -``` -MyGame/ -└── MyGame/ - ├── MyGame.xcodeproj # Open in Xcode - └── Sources/ - └── MyGame/ - ├── GameData/ # Assets location - │ ├── Models/ - │ ├── Scenes/ - │ ├── Scripts/ - │ └── Textures/ - ├── GameScene.swift # Your game logic ⭐ - ├── GameViewController.swift # Renderer setup - └── AppDelegate.swift # App entry -``` - -**Focus on GameScene.swift** - that's where your game lives. - ---- - diff --git a/docs/03-Game Development/02-Tutorials/000_HelloEditor.md b/docs/03-Game Development/02-Tutorials/000_HelloEditor.md deleted file mode 100644 index 02c658707..000000000 --- a/docs/03-Game Development/02-Tutorials/000_HelloEditor.md +++ /dev/null @@ -1,170 +0,0 @@ -# Getting Started with Untold Engine Studio - -This guide will walk you through the basics of getting up and running with **Untold Engine Studio**, from installation to loading your first model. - ---- - -## Downloading Untold Engine Studio - -To start developing with the Untold Engine, download the latest version of **Untold Engine Studio** from the official GitHub releases page: - -👉 https://github.com/untoldengine/UntoldEditor/releases - -Once downloaded: - -1. Drag **Untold Engine Studio** into your **Applications** folder -2. Double-click the app to launch it - ---- - -## Creating a New Project - -When the editor launches, you will be presented with the main startup screen. - -![editor_empty_view](../../images/Editor/Editor_scene_empty.png) - -At this point, create a new project: - -1. Click **New** -2. A new window will appear asking for: - - **Project name** - - **Target platform** - - **Project location** -3. Fill in the details and click **Create** - -Once completed, the engine will generate a fully configured **Xcode game project** for you. - -You will then be prompted to open the project in Xcode. I recommend doing so. - -![editor_scene_xcode](../../images/Editor/Editor_scene_xcode.png) - ---- - -## Exploring the Generated Project - -After opening the project in Xcode, take a moment to explore the file structure. This will help you navigate the project later. - -### Project Structure - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Open this in Xcode - ├── project.yml # XcodeGen configuration - └── Sources/ - └── MyGame/ - ├── GameData/ # ← Put your assets here - │ ├── Models/ # 3D models - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # USC scripts - │ ├── Textures/ # Images - │ └── ... - ├── GameScene.swift # Your game logic - ├── GameViewController.swift # View controller - └── AppDelegate.swift # App entry point -``` - -The most important file to look for is: - -```GameScene.swift``` - -This is where you will do most of your coding. -It contains the core lifecycle functions, including: - -- `init()` – scene setup -- `update()` – per-frame logic - -You’ll be spending most of your time here when writing game logic. - ---- - -## Back to the Editor - -Return to **Untold Engine Studio**. - -If you look at the editor toolbar, you’ll notice the name of your newly created project displayed on the right side of the window. - -At this point, you can already start adding content to your scene. - ---- - -## Adding Entities to the Scene - -You can quickly add a primitive by clicking the **“+”** button in the **Scenegraph View** and selecting a cube or other built-in shapes. - -However, it’s more fun to load real models. - ---- - -## Downloading Sample Models - -I’ve provided a small set of [sample models](https://haroldserrano.gumroad.com/l/iqjlac) you can use to get started: - -- A soccer player -- A soccer ball -- A stadium -- Idle and running animations - -All assets are provided as **`.usdz`** files. - -> **Note** -> Untold Engine currently accepts **USDZ** files only. - -Download the sample assets from the provided link. - ---- - -## Importing Models into Your Project - -Once the models are downloaded, it’s time to import them into your project. - -1. Open the **Asset Browser** -2. Select the **Model** category -3. Click **Import** -4. Navigate to the folder containing your downloaded `.usdz` file -5. Select a model and confirm - -After importing, the engine will display a feedback message confirming the import. -You will also see the `.usdz` file listed under the **Model** category. - ---- - -## Loading a Model into the Scene - -Now for the fun part. - -- **Double-click** the imported `.usdz` file in the Asset Browser - -The model will immediately appear in the editor viewport. - -![Editor_scene_model_viewport](../../images/Editor/Editor_scene_model_viewport.png) - -Behind the scenes, two things just happened: - -1. An **entity** was created -2. The `.usdz` asset was linked to that entity - ---- - -## Importing and Linking Animations - -The same workflow applies to animations. - -1. Select the **Animation** category in the Asset Browser -2. Click **Import** -3. Locate and import an animation `.usdz` file -4. Double-click the animation asset - -The animation will automatically be linked to the currently selected entity. - -If you open the **Inspector** tab, you’ll see the animation listed as part of the entity’s components. - ---- - -## What’s Next? - -Now that you have a basic understanding of the editor, asset workflow, and project structure, you’re ready to start coding with the Untold Engine. - -👉 Continue with the **Hello World** tutorial to write your first gameplay logic. - - diff --git a/docs/03-Game Development/02-Tutorials/00_HelloWorld.md b/docs/03-Game Development/02-Tutorials/00_HelloWorld.md deleted file mode 100644 index 6c5007b74..000000000 --- a/docs/03-Game Development/02-Tutorials/00_HelloWorld.md +++ /dev/null @@ -1,152 +0,0 @@ -# Hello World - -Your first UntoldEngine program - logging a message every frame. - ---- - -## Overview - -This tutorial shows you how to add custom code to your game's update loop and log output to the console. - ---- - -## Prerequisites - -This tutorial assumes you have: -- Created a project using the 'Untold Engine Studio` or `untoldengine-create` -- Opened the project in Xcode -- Located `GameScene.swift` in your project - ---- - -## Step 1: Open GameScene.swift - -In Xcode, navigate to: - -``` -Sources/YourProjectName/GameScene.swift -``` - ---- - -## Step 2: Add Your First Game Logic - -Find the `update(deltaTime:)` method in `GameScene.swift`: - -```swift path=null start=null -func update(deltaTime: Float) { - // Skip logic if not in game mode - if gameMode == false { return } - - // Add your custom update logic here -} -``` - -Replace the comment with: - -```swift path=null start=null -func update(deltaTime: Float) { - // Skip logic if not in game mode - if gameMode == false { return } - - // Your first game code! 🎉 - Logger.log(message: "Hello World! Delta: \(deltaTime)") -} -``` - ---- - -## Step 3: Build and Run - -Press **Cmd+R** in Xcode. - -Open the **Debug Console** (Cmd+Shift+Y) to see: - -``` -Hello World! Delta: 0.016 -Hello World! Delta: 0.017 -Hello World! Delta: 0.016 -... -``` - -The message appears every frame! 🚀 - ---- - -## What Just Happened? - -### The Update Loop - -`update(deltaTime:)` is called every frame by the engine: - -- **60 FPS** = called 60 times per second -- **deltaTime** = time since last frame (in seconds) - -All game logic goes here: movement, input handling, collision detection, etc. - -### Logging - -```swift path=null start=null -Logger.log(message: "Hello World!") -``` - -Use `Logger.log()` to print debug messages. It's better than `print()` because: -- Engine-aware logging -- Can be filtered/disabled in production -- Consistent formatting - -### Other Logging Methods - -```swift path=null start=null -Logger.logWarning(message: "Something might be wrong") -Logger.logError(message: "Something went wrong!") -``` - ---- - -## Limiting Output (Recommended) - -Logging every frame creates spam. Let's log once per second instead: - -```swift path=null start=null -class GameScene { - var elapsedTime: Float = 0.0 // Add this property - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Accumulate time - elapsedTime += deltaTime - - // Log once per second - if elapsedTime >= 1.0 { - Logger.log(message: "Hello World! One second passed.") - elapsedTime = 0.0 // Reset - } - } -} -``` - -Now you'll see: - -``` -Hello World! One second passed. -Hello World! One second passed. -... -``` - -Much cleaner! - ---- - -## Summary - -You've learned: - -✅ The `update(deltaTime:)` method runs every frame -✅ `deltaTime` is the time between frames -✅ `Logger.log()` prints messages to the console -✅ How to accumulate time for periodic actions - -This is the foundation of game development: **write code that runs every frame**. - diff --git a/docs/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md b/docs/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md deleted file mode 100644 index 94cf91c38..000000000 --- a/docs/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md +++ /dev/null @@ -1,214 +0,0 @@ -# Move an Entity - -Learn how to move entities using the Transform System. - ---- - -## Overview - -This tutorial shows you how to: -- Find an entity from a loaded scene -- Move an entity to an absolute position -- Move an entity relative to its current position - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity (created in Untold Engine Studio or loaded via `loadScene()`) -- The entity has a name set in the editor (e.g., "Player") - -For complete API documentation: - -➡️ **[Transform System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingTransformSystem.md)** - ---- - -## Step 1: Find the Entity from Your Scene - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code (setupAssetPaths, loadScene, etc.) ... - startGameSystems() - - // Find the entity by name (set in the editor) - player = findEntity(name: "Player") - - if player == nil { - Logger.logWarning(message: "Player entity not found in scene") - } - } -} -``` - -**Important**: "Player" must match the entity name you set in Untold Engine Studio. - ---- - -## Step 2: Move to an Absolute Position - -Use `translateTo()` to set an entity to a specific world position: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - - player = findEntity(name: "Player") - - // Move player to position (5, 0, -10) - translateTo(entityId: player, position: SIMD3(5.0, 0.0, -10.0)) - } -} -``` - -**Result**: The entity immediately moves to position (5, 0, -10) in world space. - ---- - -## Step 3: Move Relative to Current Position - -Use `translateBy()` to move an entity by an offset: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - - player = findEntity(name: "Player") - - // Move player 3 units to the right (X-axis) - translateBy(entityId: player, delta: SIMD3(3.0, 0.0, 0.0)) - } -} -``` - -**Result**: The entity moves 3 units along the X-axis from its current position. - ---- - -## Step 4: Continuous Movement in Update Loop - -For smooth movement every frame, use `translateBy()` in `update(deltaTime:)`: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let moveSpeed: Float = 5.0 // Units per second - - init() { - // ... setup code ... - player = findEntity(name: "Player") - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Move forward continuously - let movement = SIMD3(0, 0, -moveSpeed * deltaTime) - translateBy(entityId: player, delta: movement) - } -} -``` - -**Result**: The player moves forward smoothly at 5 units per second. - ---- - -## Understanding Delta Time - -**Why multiply by `deltaTime`?** - -`deltaTime` is the time (in seconds) since the last frame: -- At 60 FPS: `deltaTime ≈ 0.016` seconds -- At 30 FPS: `deltaTime ≈ 0.033` seconds - -By multiplying speed by `deltaTime`, movement becomes **frame-rate independent**: - -```swift path=null start=null -// Without deltaTime (BAD) -translateBy(entityId: player, delta: SIMD3(0, 0, -0.1)) -// Result: Speed varies with frame rate ❌ - -// With deltaTime (GOOD) -let movement = SIMD3(0, 0, -moveSpeed * deltaTime) -translateBy(entityId: player, delta: movement) -// Result: Consistent speed regardless of frame rate ✅ -``` - ---- - -## Movement Examples - -### Move Forward - -```swift path=null start=null -let movement = SIMD3(0, 0, -moveSpeed * deltaTime) -translateBy(entityId: player, delta: movement) -``` - -### Move Right - -```swift path=null start=null -let movement = SIMD3(moveSpeed * deltaTime, 0, 0) -translateBy(entityId: player, delta: movement) -``` - -### Move Up - -```swift path=null start=null -let movement = SIMD3(0, moveSpeed * deltaTime, 0) -translateBy(entityId: player, delta: movement) -``` - -### Move Along Entity's Forward Direction - -```swift path=null start=null -let forward = getForwardAxisVector(entityId: player) -let movement = forward * moveSpeed * deltaTime -translateBy(entityId: player, delta: movement) -``` - ---- - -## Checking Entity Position - -To read an entity's current position: - -```swift path=null start=null -// World position (absolute) -let worldPos = getPosition(entityId: player) -Logger.log(message: "Player world position: \(worldPos)") - -// Local position (relative to parent, if parented) -let localPos = getLocalPosition(entityId: player) -Logger.log(message: "Player local position: \(localPos)") -``` - ---- - -## Summary - -You've learned: - -✅ `findEntity(name:)` - Find entities from loaded scenes -✅ `translateTo()` - Set absolute world position -✅ `translateBy()` - Move relative to current position -✅ `deltaTime` - Make movement frame-rate independent -✅ `getPosition()` - Read current position - ---- - - - diff --git a/docs/03-Game Development/02-Tutorials/01_Transform/02_RotateAnEntity.md b/docs/03-Game Development/02-Tutorials/01_Transform/02_RotateAnEntity.md deleted file mode 100644 index 1fc40aaaf..000000000 --- a/docs/03-Game Development/02-Tutorials/01_Transform/02_RotateAnEntity.md +++ /dev/null @@ -1,246 +0,0 @@ -# Rotate an Entity - -Learn how to rotate entities using the Transform System. - ---- - -## Overview - -This tutorial shows you how to: -- Rotate an entity to an absolute angle -- Rotate an entity incrementally -- Create smooth rotation using `deltaTime` - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity -- The entity has a name set in the editor (e.g., "Propeller") - -For complete API documentation: - -➡️ **[Transform System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingTransformSystem.md)** - ---- - -## Step 1: Find the Entity from Your Scene - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - - init() { - // ... setup code (setupAssetPaths, loadScene, etc.) ... - startGameSystems() - - // Find the entity by name (set in the editor) - propeller = findEntity(name: "Propeller") - - if propeller == nil { - Logger.logWarning(message: "Propeller entity not found in scene") - } - } -} -``` - ---- - -## Step 2: Rotate to an Absolute Angle - -Use `rotateTo()` to set an entity to a specific rotation: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - - init() { - // ... setup code ... - - propeller = findEntity(name: "Propeller") - - // Rotate 45 degrees around the Y-axis (up) - rotateTo(entityId: propeller, angle: 45.0, axis: SIMD3(0, 1, 0)) - } -} -``` - -**Result**: The entity immediately rotates to 45 degrees around the Y-axis. - ---- - -## Step 3: Rotate Incrementally - -Use `rotateBy()` to add rotation to the current orientation: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - - init() { - // ... setup code ... - - propeller = findEntity(name: "Propeller") - - // Rotate 15 degrees from current rotation - rotateBy(entityId: propeller, angle: 15.0, axis: SIMD3(0, 1, 0)) - } -} -``` - -**Result**: The entity rotates an additional 15 degrees around the Y-axis. - ---- - -## Step 4: Continuous Rotation in Update Loop - -For smooth spinning, use `rotateBy()` in `update(deltaTime:)`: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - let rotationSpeed: Float = 90.0 // Degrees per second - - init() { - // ... setup code ... - propeller = findEntity(name: "Propeller") - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Rotate continuously around Y-axis - let angleThisFrame = rotationSpeed * deltaTime - rotateBy(entityId: propeller, angle: angleThisFrame, axis: SIMD3(0, 1, 0)) - } -} -``` - -**Result**: The propeller spins smoothly at 90 degrees per second. - ---- - -## Understanding Rotation Axes - -### Common Rotation Axes - -```swift path=null start=null -// Rotate around Y-axis (up) - typical yaw rotation -rotateBy(entityId: entity, angle: 45.0, axis: SIMD3(0, 1, 0)) - -// Rotate around X-axis (right) - pitch rotation -rotateBy(entityId: entity, angle: 45.0, axis: SIMD3(1, 0, 0)) - -// Rotate around Z-axis (forward) - roll rotation -rotateBy(entityId: entity, angle: 45.0, axis: SIMD3(0, 0, 1)) -``` - -### Using Entity's Local Axes - -You can also rotate around an entity's own forward/right/up vectors: - -```swift path=null start=null -// Rotate around entity's own up direction -let up = getUpAxisVector(entityId: entity) -rotateBy(entityId: entity, angle: 45.0, axis: up) - -// Rotate around entity's own right direction -let right = getRightAxisVector(entityId: entity) -rotateBy(entityId: entity, angle: 45.0, axis: right) -``` - ---- - -## Rotation Examples - -### Spin Clockwise (Y-axis) - -```swift path=null start=null -let angle = rotationSpeed * deltaTime -rotateBy(entityId: entity, angle: angle, axis: SIMD3(0, 1, 0)) -``` - -### Spin Counter-Clockwise (Y-axis) - -```swift path=null start=null -let angle = -rotationSpeed * deltaTime // Negative for opposite direction -rotateBy(entityId: entity, angle: angle, axis: SIMD3(0, 1, 0)) -``` - -### Tumble (X-axis) - -```swift path=null start=null -let angle = rotationSpeed * deltaTime -rotateBy(entityId: entity, angle: angle, axis: SIMD3(1, 0, 0)) -``` - -### Face a Direction - -To face a target, you typically calculate the direction and convert to rotation. This is more advanced, but here's a simple Y-axis example: - -```swift path=null start=null -let targetPos = SIMD3(10, 0, 5) -let currentPos = getPosition(entityId: entity) -let direction = normalize(targetPos - currentPos) - -// Calculate angle to target (simplified for Y-axis only) -let angle = atan2(direction.x, direction.z) * (180.0 / .pi) -rotateTo(entityId: entity, angle: angle, axis: SIMD3(0, 1, 0)) -``` - ---- - -## Checking Current Rotation - -To read an entity's current orientation: - -```swift path=null start=null -// World orientation matrix -let worldOrientation = getOrientation(entityId: entity) -Logger.log(message: "World orientation: \(worldOrientation)") - -// Local orientation matrix (relative to parent) -let localOrientation = getLocalOrientation(entityId: entity) -Logger.log(message: "Local orientation: \(localOrientation)") -``` - ---- - -## Combining Translation and Rotation - -You can move and rotate in the same frame: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - // Move forward - let forward = getForwardAxisVector(entityId: player) - let movement = forward * moveSpeed * deltaTime - translateBy(entityId: player, delta: movement) - - // Rotate based on input - let turnAngle = turnSpeed * deltaTime - rotateBy(entityId: player, angle: turnAngle, axis: SIMD3(0, 1, 0)) -} -``` - ---- - -## Summary - -You've learned: - -✅ `rotateTo()` - Set absolute rotation angle -✅ `rotateBy()` - Rotate incrementally from current orientation -✅ `deltaTime` - Make rotation frame-rate independent -✅ Rotation axes - Control rotation direction -✅ `getOrientation()` - Read current rotation - ---- - - diff --git a/docs/03-Game Development/02-Tutorials/02_Input/01_KeyboardMovement.md b/docs/03-Game Development/02-Tutorials/02_Input/01_KeyboardMovement.md deleted file mode 100644 index 988a49707..000000000 --- a/docs/03-Game Development/02-Tutorials/02_Input/01_KeyboardMovement.md +++ /dev/null @@ -1,261 +0,0 @@ -# Keyboard Movement - -Learn how to move entities using keyboard input. - ---- - -## Overview - -This tutorial shows you how to: -- Detect keyboard input using the Input System -- Move an entity based on WASD keys -- Combine input with Transform System - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity -- The entity has a name set in the editor (e.g., "Player") - -For complete API documentation: - -➡️ **[Input System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingInputSystem.md)** - ---- - -## Step 1: Enable Input System - -Before detecting input, you need to register keyboard events. Add this to your `startGameSystems()` helper or in `init()`: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - startGameSystems() - - // Register input keyboard events - InputSystem.shared.registerKeyboardEvents() - - // Find the entity by name (set in the editor) - player = findEntity(name: "Player") - } -} -``` - -**Important**: Call `InputSystem.shared.registerKeyboardEvents()` once during initialization to enable keyboard input detection. - ---- - -## Step 2: Detect Keyboard Input - -The Input System provides a `keyState` object to check if keys are pressed: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - // Check if W key is pressed - if inputSystem.keyState.wPressed == true { - Logger.log(message: "W key pressed!") - } -} -``` - -**Available Keys**: -- `inputSystem.keyState.wPressed` - W key -- `inputSystem.keyState.aPressed` - A key -- `inputSystem.keyState.sPressed` - S key -- `inputSystem.keyState.dPressed` - D key - ---- - -## Step 3: Move Entity with WASD - -Combine input detection with movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let moveSpeed: Float = 5.0 - - init() { - // ... setup code ... - player = findEntity(name: "Player") - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - var movement = SIMD3(0, 0, 0) - - // Forward (W) - if inputSystem.keyState.wPressed == true { - movement.z += moveSpeed * deltaTime - } - - // Backward (S) - if inputSystem.keyState.sPressed == true { - movement.z -= moveSpeed * deltaTime - } - - // Left (A) - if inputSystem.keyState.aPressed == true { - movement.x -= moveSpeed * deltaTime - } - - // Right (D) - if inputSystem.keyState.dPressed == true { - movement.x += moveSpeed * deltaTime - } - - // Apply movement - if movement != SIMD3(0, 0, 0) { - translateBy(entityId: player, delta: movement) - } - } -} -``` - -**Result**: The player moves based on WASD input at 5 units per second. - ---- - -## Understanding the Code - -### Input Detection - -```swift path=null start=null -if inputSystem.keyState.wPressed == true { - // Key is currently pressed -} -``` - -The Input System automatically tracks key states. You don't need to register listeners or handle events. - -### Accumulating Movement - -```swift path=null start=null -var movement = SIMD3(0, 0, 0) - -if inputSystem.keyState.wPressed == true { - movement.z += moveSpeed * deltaTime -} - -if inputSystem.keyState.dPressed == true { - movement.x += moveSpeed * deltaTime -} -``` - -By accumulating input into a `movement` vector, diagonal movement (W+D) works correctly. - -### Delta Time - -Multiplying by `deltaTime` ensures consistent speed regardless of frame rate: - -```swift path=null start=null -movement.z += moveSpeed * deltaTime // ✅ Frame-rate independent -movement.z += 0.1 // ❌ Varies with frame rate -``` - ---- - -## Advanced: Normalized Diagonal Movement - -Diagonal movement (W+D) is currently faster than single-direction movement. To fix this, normalize the movement vector: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - var movement = SIMD3(0, 0, 0) - - // Accumulate input - if inputSystem.keyState.wPressed == true { - movement.z += 1.0 - } - if inputSystem.keyState.sPressed == true { - movement.z -= 1.0 - } - if inputSystem.keyState.aPressed == true { - movement.x -= 1.0 - } - if inputSystem.keyState.dPressed == true { - movement.x += 1.0 - } - - // Normalize and apply speed - if movement != SIMD3(0, 0, 0) { - movement = normalize(movement) * moveSpeed * deltaTime - translateBy(entityId: player, delta: movement) - } -} -``` - -**Result**: Diagonal movement is now the same speed as cardinal movement. - ---- - -## Example: Tank Controls (Forward + Rotation) - -For tank-style controls where forward moves along the entity's facing direction: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let moveSpeed: Float = 5.0 - let turnSpeed: Float = 90.0 // Degrees per second - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Forward/backward movement - var forwardMovement: Float = 0.0 - if inputSystem.keyState.wPressed == true { - forwardMovement = moveSpeed * deltaTime - } - if inputSystem.keyState.sPressed == true { - forwardMovement = -moveSpeed * deltaTime - } - - // Apply forward movement along entity's forward direction - if forwardMovement != 0.0 { - let forward = getForwardAxisVector(entityId: player) - translateBy(entityId: player, delta: forward * forwardMovement) - } - - // Left/right rotation - var turnAngle: Float = 0.0 - if inputSystem.keyState.aPressed == true { - turnAngle = -turnSpeed * deltaTime - } - if inputSystem.keyState.dPressed == true { - turnAngle = turnSpeed * deltaTime - } - - // Apply rotation - if turnAngle != 0.0 { - rotateBy(entityId: player, angle: turnAngle, axis: SIMD3(0, 1, 0)) - } - } -} -``` - ---- - -## Summary - -You've learned: - -✅ `inputSystem.keyState` - Check keyboard input -✅ Detect W, A, S, D keys -✅ Combine input with `translateBy()` -✅ Normalize diagonal movement -✅ Tank-style controls with rotation - ---- - diff --git a/docs/03-Game Development/02-Tutorials/03_Animation/01_PlayAnimation.md b/docs/03-Game Development/02-Tutorials/03_Animation/01_PlayAnimation.md deleted file mode 100644 index 8c903a355..000000000 --- a/docs/03-Game Development/02-Tutorials/03_Animation/01_PlayAnimation.md +++ /dev/null @@ -1,252 +0,0 @@ -# Play Animation - -Learn how to load and play animations on entities. - ---- - -## Overview - -This tutorial shows you how to: -- Load animation data from a `.usdc` file -- Play an animation on an entity -- Pause animations - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity that has an animation skeleton -- An animation file (e.g., `running.usdc`) in `GameData/Models/` or `GameData/Animations/` -- The entity has a name set in the editor (e.g., "Player") -- The entity has animations linked in the editor - -For complete API documentation: - -➡️ **[Animation System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingAnimationSystem.md)** - ---- - -## Step 1: Find the Entity - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - startGameSystems() - - // Find the entity by name (set in the editor) - player = findEntity(name: "Player") - } -} -``` - ---- - -## Step 2: Load the Animation - ->>> Skip this section if you linked an animation to the entity through the editor. - -Use `setEntityAnimations()` to load animation data: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - player = findEntity(name: "Player") - - // Load the running animation - setEntityAnimations( - entityId: player, - filename: "running", - withExtension: "usdc", - name: "running" - ) - } -} -``` - -**Parameters**: -- `filename`: The animation file name (without extension) -- `withExtension`: File extension (usually `"usdc"`) -- `name`: A label to reference this animation later - ---- - -## Step 3: Play the Animation - -Use `changeAnimation()` to start playing the animation: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - player = findEntity(name: "Player") - - // Load animation -- Ignore if you linked an animation through the editor - setEntityAnimations( - entityId: player, - filename: "running", - withExtension: "usdc", - name: "running" - ) - - // Play the animation - we assume that the animation linked is called running - changeAnimation(entityId: player, name: "running") - } -} -``` - -**Result**: The entity plays the "running" animation in a loop. - ---- - -## Step 4: Pause the Animation (Optional) - -To pause or resume an animation: - -```swift path=null start=null -// Pause the animation -pauseAnimationComponent(entityId: player, isPaused: true) - -// Resume the animation -pauseAnimationComponent(entityId: player, isPaused: false) -``` - ---- - -## Loading Multiple Animations - -You can load multiple animations for the same entity: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - player = findEntity(name: "Player") - - // Load multiple animations -- Ignore if you linked all three animations through the editor - setEntityAnimations( - entityId: player, - filename: "idle", - withExtension: "usdc", - name: "idle" - ) - - setEntityAnimations( - entityId: player, - filename: "running", - withExtension: "usdc", - name: "running" - ) - - setEntityAnimations( - entityId: player, - filename: "jumping", - withExtension: "usdc", - name: "jumping" - ) - - // Start with idle animation - changeAnimation(entityId: player, name: "idle") - } -} -``` - ---- - -## Switching Animations at Runtime - -Switch between animations based on game state: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var isRunning: Bool = false - - init() { - // ... setup code ... - startGameSystems() - - // Register input keyboard events - InputSystem.shared.registerKeyboardEvents() - - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Check if player is moving - let wasRunning = isRunning - isRunning = inputSystem.keyState.wPressed || - inputSystem.keyState.aPressed || - inputSystem.keyState.sPressed || - inputSystem.keyState.dPressed - - // Switch animation when state changes - if isRunning && !wasRunning { - changeAnimation(entityId: player, name: "running") - } else if !isRunning && wasRunning { - changeAnimation(entityId: player, name: "idle") - } - } -} -``` - ---- - -## File Organization - -Animation files should be placed in your project's `GameData` directory: - - -``` -Sources/YourProject/GameData/ -├── Models/ -│ └── player.usdz -├── Animations/ -│ ├── idle.usdc -│ ├── running.usdc -│ └── jumping.usdc -``` - ---- - -## Troubleshooting - -### Animation doesn't play - -1. **Check the entity has a skeleton**: Not all models support animation. The model must have a skeletal rig. -2. **Verify file path**: Make sure the animation file exists in `GameData/Models/` or `GameData/Animations/`. -3. **Check animation name**: The `name` parameter must match when loading and playing. - -### Animation plays too fast/slow - -Animation playback speed is controlled by the animation file itself. To adjust speed, you'll need to modify the animation in your 3D software (e.g., Blender, Maya) or use animation blending (advanced topic). - ---- - -## Summary - -You've learned: - -✅ `setEntityAnimations()` - Load animation data -✅ `changeAnimation()` - Play a specific animation -✅ `pauseAnimationComponent()` - Pause/resume animations -✅ Load multiple animations per entity -✅ Switch animations based on game state - ---- - - diff --git a/docs/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md b/docs/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md deleted file mode 100644 index 3bbd8abc7..000000000 --- a/docs/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md +++ /dev/null @@ -1,319 +0,0 @@ -# Animation State Switching - -Learn how to switch between different animations based on player input and game state. - ---- - -## Overview - -This tutorial shows you how to: -- Create a state-based animation system -- Switch between idle, running, and jumping animations -- Trigger animations based on input - ---- - -## Prerequisites - -This tutorial assumes you have: -- Completed the [Play Animation tutorial](./01_PlayAnimation.md) -- A project with multiple animations loaded (idle, running, jumping) -- An entity with a skeleton that supports animation - -For complete API documentation: - -➡️ **[Animation System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingAnimationSystem.md)** - ---- - -## Step 1: Set Up Animation States - -Define an enum to represent your animation states: - -```swift path=null start=null -enum PlayerState { - case idle - case running - case jumping -} - -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - - init() { - // ... setup code ... - startGameSystems() - - // Register input - InputSystem.shared.registerKeyboardEvents() - - player = findEntity(name: "Player") - - // Load all animations -- ignore if you linked all three animations through the editor. - loadPlayerAnimations() - - // Start with idle - changeAnimation(entityId: player, name: "idle") - } - - func loadPlayerAnimations() { - setEntityAnimations( - entityId: player, - filename: "idle", - withExtension: "usdc", - name: "idle" - ) - - setEntityAnimations( - entityId: player, - filename: "running", - withExtension: "usdc", - name: "running" - ) - - setEntityAnimations( - entityId: player, - filename: "jumping", - withExtension: "usdc", - name: "jumping" - ) - } -} -``` - ---- - -## Step 2: Implement State Switching Logic - -Create a function to handle state transitions: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true // Track if player is on ground - - func update(deltaTime: Float) { - if gameMode == false { return } - - updatePlayerState() - } - - func updatePlayerState() { - let oldState = playerState - - // Determine new state based on input and game conditions - if !isGrounded { - playerState = .jumping - } else if isMovementKeyPressed() { - playerState = .running - } else { - playerState = .idle - } - - // Only change animation if state actually changed - if playerState != oldState { - switchToAnimation(for: playerState) - } - } - - func isMovementKeyPressed() -> Bool { - return inputSystem.keyState.wPressed || - inputSystem.keyState.aPressed || - inputSystem.keyState.sPressed || - inputSystem.keyState.dPressed - } - - func switchToAnimation(for state: PlayerState) { - switch state { - case .idle: - changeAnimation(entityId: player, name: "idle") - Logger.log(message: "Switched to idle animation") - - case .running: - changeAnimation(entityId: player, name: "running") - Logger.log(message: "Switched to running animation") - - case .jumping: - changeAnimation(entityId: player, name: "jumping") - Logger.log(message: "Switched to jumping animation") - } - } -} -``` - ---- - -## Step 3: Add Jump Trigger - -Add space bar input to trigger jumping: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true - var jumpTimer: Float = 0.0 - let jumpDuration: Float = 0.5 // Jump animation duration in seconds - - func update(deltaTime: Float) { - if gameMode == false { return } - - handleJumpInput() - updateJumpTimer(deltaTime: deltaTime) - updatePlayerState() - } - - func handleJumpInput() { - // Trigger jump on space press (only if grounded) - if inputSystem.keyState.spacePressed && isGrounded { - isGrounded = false - jumpTimer = jumpDuration - } - } - - func updateJumpTimer(deltaTime: Float) { - // Count down jump timer - if !isGrounded { - jumpTimer -= deltaTime - - // Land when timer expires - if jumpTimer <= 0.0 { - isGrounded = true - jumpTimer = 0.0 - } - } - } - - func updatePlayerState() { - let oldState = playerState - - // Priority: jumping > running > idle - if !isGrounded { - playerState = .jumping - } else if isMovementKeyPressed() { - playerState = .running - } else { - playerState = .idle - } - - if playerState != oldState { - switchToAnimation(for: playerState) - } - } -} -``` - ---- - -## Step 4: Combine Animation with Movement - -Integrate animation state switching with actual movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true - var jumpTimer: Float = 0.0 - let moveSpeed: Float = 5.0 - let jumpDuration: Float = 0.5 - - func update(deltaTime: Float) { - if gameMode == false { return } - - handleJumpInput() - updateJumpTimer(deltaTime: deltaTime) - updatePlayerState() - updatePlayerMovement(deltaTime: deltaTime) - } - - func updatePlayerMovement(deltaTime: Float) { - var movement = SIMD3(0, 0, 0) - - // Only move if grounded - if isGrounded { - if inputSystem.keyState.wPressed { - movement.z += moveSpeed * deltaTime - } - if inputSystem.keyState.sPressed { - movement.z -= moveSpeed * deltaTime - } - if inputSystem.keyState.aPressed { - movement.x -= moveSpeed * deltaTime - } - if inputSystem.keyState.dPressed { - movement.x += moveSpeed * deltaTime - } - - if movement != SIMD3(0, 0, 0) { - translateBy(entityId: player, delta: movement) - } - } - } -} -``` - ---- - -## Advanced: State Machine Pattern - -For more complex state management, consider using a proper state machine: - -```swift path=null start=null -class AnimationStateMachine { - var currentState: PlayerState = .idle - var entityId: EntityID - - init(entityId: EntityID) { - self.entityId = entityId - } - - func canTransition(to newState: PlayerState) -> Bool { - switch (currentState, newState) { - case (.jumping, .idle), (.jumping, .running): - // Can only exit jumping state when landing - return false - default: - return true - } - } - - func transition(to newState: PlayerState) { - guard canTransition(to: newState) else { return } - - if currentState != newState { - currentState = newState - playAnimation(for: newState) - } - } - - func playAnimation(for state: PlayerState) { - let animationName: String - switch state { - case .idle: animationName = "idle" - case .running: animationName = "running" - case .jumping: animationName = "jumping" - } - - changeAnimation(entityId: entityId, name: animationName) - } -} -``` - ---- - -## Summary - -You've learned: - -✅ Create animation states using enums -✅ Switch animations based on game state -✅ Trigger animations from input -✅ Prevent animation flickering with state checks -✅ Combine animations with movement logic - ---- - - diff --git a/docs/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md b/docs/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md deleted file mode 100644 index ef6850642..000000000 --- a/docs/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md +++ /dev/null @@ -1,331 +0,0 @@ -# Apply Force - -Learn how to use the Physics System to apply forces and create realistic movement. - ---- - -## Overview - -This tutorial shows you how to: -- Enable physics on an entity -- Configure mass and gravity -- Apply forces for dynamic movement -- Use the Steering System for advanced behaviors - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity -- The entity has a name set in the editor (e.g., "Ball") -- The entity has a kinetic component applied through the editor - -For complete API documentation: - -➡️ **[Physics System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingPhysicsSystem.md)** -➡️ **[Steering System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingSteeringSystem.md)** - ---- - -## Step 1: Find the Entity - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - startGameSystems() - - ball = findEntity(name: "Ball") - } -} -``` - ---- - -## Step 2: Enable Physics (Kinetics) - -Before applying forces, enable physics on the entity: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - // Enable physics components - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - } -} -``` - -**What this does**: `setEntityKinetics()` adds `PhysicsComponents` and `KineticComponent` to the entity, enabling it to respond to forces. - ---- - -## Step 3: Configure Mass and Gravity - -Set the entity's mass and gravity scale: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - // Enable physics - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - - // Configure physics properties - setMass(entityId: ball, mass: 0.5) // Lighter = easier to move - setGravityScale(entityId: ball, gravityScale: 1.0) // Normal gravity - } -} -``` - -**Parameters**: -- `mass`: How heavy the entity is. Lower = easier to push. Default is `1.0`. -- `gravityScale`: How much gravity affects it. `0.0` = no gravity, `1.0` = normal gravity. - ---- - -## Step 4: Apply a Force - -Use `applyForce()` to push the entity: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - setMass(entityId: ball, mass: 0.5) - setGravityScale(entityId: ball, gravityScale: 1.0) - - InputSystem.shared.registerKeyboardEvents() - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Apply forward force when W is pressed - if inputSystem.keyState.wPressed { - applyForce(entityId: ball, force: SIMD3(0.0, 0.0, 5.0)) - } - } -} -``` - -**Result**: When you press W, the ball accelerates forward. - -**Important**: Forces are applied every frame while the key is pressed. The physics system automatically integrates forces into velocity and position. - ---- - -## Understanding Forces vs. Direct Movement - -### Direct Movement (Transform System) - -```swift path=null start=null -// Immediate, precise movement -translateBy(entityId: entity, delta: SIMD3(0, 0, speed * deltaTime)) -``` - -✅ Precise control -✅ No physics overhead -❌ No inertia or momentum -❌ Doesn't interact with physics - -### Force-Based Movement (Physics System) - -```swift path=null start=null -// Gradual acceleration with momentum -applyForce(entityId: entity, force: SIMD3(0, 0, 5.0)) -``` - -✅ Realistic inertia -✅ Physics interactions -✅ Momentum and deceleration -❌ Less precise -❌ Requires tuning - ---- - -## Step 5: Use the Steering System - -For easier physics-based movement, use the Steering System: - -### Steer to a Target Position - -```swift path=null start=null -class GameScene { - var player: EntityID! - let maxSpeed: Float = 5.0 - - init() { - // ... setup code ... - player = findEntity(name: "Player") - - setEntityKinetics(entityId: player) // Ignore this if you linked kinetic component through the editor - setMass(entityId: player, mass: 1.0) - setGravityScale(entityId: player, gravityScale: 0.0) // No gravity for top-down - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - let targetPosition = SIMD3(10.0, 0.0, 5.0) - steerSeek(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) - } -} -``` - -**Result**: The entity smoothly accelerates toward the target position. - ---- - -## Steering System Examples - -### Steer with WASD Keys - -The easiest way to add physics-based movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let maxSpeed: Float = 5.0 - - init() { - // ... setup code ... - InputSystem.shared.registerKeyboardEvents() - - player = findEntity(name: "Player") - setEntityKinetics(entityId: player) // Ignore this if you linked kinetic component through the editor - setMass(entityId: player, mass: 1.0) - setGravityScale(entityId: player, gravityScale: 0.0) - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Automatic WASD steering - steerWithWASD(entityId: player, maxSpeed: maxSpeed, deltaTime: deltaTime) - } -} -``` - -**Result**: WASD keys apply forces in the corresponding directions with smooth acceleration/deceleration. - -### Flee from a Threat - -```swift path=null start=null -let threatPosition = SIMD3(0.0, 0.0, 0.0) -steerFlee(entityId: player, threatPosition: threatPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Follow a Path - -```swift path=null start=null -let waypoints = [ - SIMD3(0, 0, 0), - SIMD3(5, 0, 0), - SIMD3(5, 0, 5), - SIMD3(0, 0, 5) -] -steerFollowPath(entityId: player, path: waypoints, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Pursue a Moving Target - -```swift path=null start=null -let enemy = findEntity(name: "Enemy") -steerPursuit(entityId: player, targetEntity: enemy!, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Avoid Obstacles - -```swift path=null start=null -let obstacles = [ - findEntity(name: "Rock1")!, - findEntity(name: "Rock2")!, - findEntity(name: "Tree1")! -] -steerAvoidObstacles(entityId: player, obstacles: obstacles, avoidanceRadius: 2.0, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Arrive at Target (Slowing Down) - -```swift path=null start=null -let targetPosition = SIMD3(10.0, 0.0, 5.0) -steerArrive(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -**Difference from `steerSeek()`**: `steerArrive()` slows down as it approaches the target, preventing overshoot. - ---- - -## Combining Steering Behaviors - -You can use multiple steering behaviors together: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - // Steer toward target while avoiding obstacles - let targetPosition = SIMD3(10.0, 0.0, 5.0) - - steerSeek(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) - - let obstacles = [findEntity(name: "Rock1")!, findEntity(name: "Rock2")!] - steerAvoidObstacles(entityId: player, obstacles: obstacles, avoidanceRadius: 2.0, maxSpeed: maxSpeed, deltaTime: deltaTime) -} -``` - ---- - -## When to Use What? - -### Use Transform System (`translateBy`, `translateTo`) -- Precise, scripted movement -- UI elements or camera -- Platformer-style movement -- When you don't need physics interactions - -### Use Physics System (`applyForce`) -- Projectiles (bullets, grenades) -- Vehicles with custom physics -- When you need low-level control - -### Use Steering System (`steerSeek`, `steerWithWASD`, etc.) -- Character movement in top-down or 3D games -- AI pathfinding and behaviors -- When you want smooth, realistic movement with minimal code - ---- - -## Summary - -You've learned: - -✅ `setEntityKinetics()` - Enable physics on entities -✅ `setMass()` and `setGravityScale()` - Configure physics properties -✅ `applyForce()` - Apply custom forces -✅ `steerWithWASD()` - Easy WASD physics movement -✅ Steering behaviors - Seek, flee, pursue, avoid, arrive -✅ When to use Transform vs. Physics vs. Steering - ---- - diff --git a/docs/03-Game Development/03-EditorOverview.md b/docs/03-Game Development/03-EditorOverview.md deleted file mode 100644 index 191fd6b5f..000000000 --- a/docs/03-Game Development/03-EditorOverview.md +++ /dev/null @@ -1,92 +0,0 @@ -# Editor Overview - -The editor is the primary environment for building games with the Untold Engine. - -It is designed to be **explicit, predictable, and tightly integrated** with the engine runtime. -What you see in the editor reflects what runs in the game. - -This page provides a **user-facing overview** of the editor for game developers. - -![editorbottomshot](../images/Editor/EditorBottomShot.png) - ---- - -## Purpose of the Editor - -The editor allows you to: - -- Create and manage scenes -- Inspect and modify entities -- Write and iterate on gameplay scripts -- Manage assets -- Preview runtime behavior in real time - -The editor is not a separate simulation layer — it runs directly on top of the engine runtime. - ---- - -## Core Editor Views - -The editor is composed of several focused views, each with a clear responsibility. - ---- - -### Scene View - -The Scene View is where you visually interact with the world. - -You can: -- Navigate the scene camera -- Select entities -- Translate, rotate, and scale entities -- Preview lighting and scene composition - -This view reflects the engine’s real transform and rendering state. - -![editormainshot](../images/Editor/EditorMainShot.png) - ---- - -### Scene Hierarchy View - -The Scene Hierarchy shows all entities in the current scene. - -It allows you to: -- View parent–child relationships -- Select entities -- Organize scene structure - -The hierarchy mirrors the engine’s internal scene graph. - -![editorscenegraphview](../images/Editor/EditorScenegraphView.png) - ---- - -### Inspector View - -The Inspector displays detailed information about the selected entity. - -From the Inspector, you can: -- View and modify components -- Adjust transforms and properties -- Attach scripts and assets - -Changes made in the Inspector apply directly to the runtime state. - -![editorinspectorview](../images/Editor/EditorInspectorView.png) - ---- - -### Asset Browser View - -The Asset Browser provides access to project assets such as: - -- Models -- Textures -- Scripts -- Scenes - -Assets imported through the editor are organized and made available to the engine automatically. - -![editorassetbrowserview](../images/Editor/EditorAssetBrowserView-alt.png) - diff --git a/docs/03-Game Development/04-Scripting-Experimental/01-Overview.md b/docs/03-Game Development/04-Scripting-Experimental/01-Overview.md deleted file mode 100644 index e2ab16284..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/01-Overview.md +++ /dev/null @@ -1,100 +0,0 @@ -# Overview - -⚠️ **EXPERIMENTAL FEATURE** - -This section documents the **UntoldEngine Scripting (USC)** language - an experimental scripting system that is **currently in development**. - ---- - -## Current Status - -USC scripting is in **early experimental phases** and is **not recommended for production use**. - -For game development, we strongly recommend using **Swift** and **Xcode** (see parent section: [Game Development Overview](../01-Overview.md)). - ---- - -## What is USC? - -USC (Untold Scripting Language) is a custom scripting language designed for the UntoldEngine. It aims to provide: - -- Simple C-like syntax -- Entity-component scripting -- Scene-based game logic - -However, the language is still evolving, and **its future direction is under evaluation**. - ---- - -## Why USC? - -Originally, USC was designed to provide: - -1. **Lower barrier to entry** - Simpler syntax than Swift -2. **Scene-embedded scripts** - Scripts attached to entities in the editor -3. **Hot-reload** - Faster iteration for non-compiled changes - -However, **Swift + Xcode** has proven to be more practical for most use cases: - -- Mature tooling (debugging, profiling, autocomplete) -- Type safety and compile-time errors -- Familiar to Apple platform developers -- Better performance - ---- - -## Should You Use USC? - -**No, not yet.** Here's why: - -❌ **Incomplete** - Many features are missing or unstable -❌ **Underdocumented** - Documentation is sparse and may be outdated -❌ **Limited tooling** - No debugger, no IDE support -❌ **Breaking changes** - Syntax and behavior may change without notice -❌ **Unoptimized** - Performance is not production-ready - -Use **Swift** for all game development until USC reaches a stable release. - ---- - -## When Will USC Be Ready? - -USC is in **exploratory development**. We are evaluating: - -- Whether USC provides enough value over Swift -- What the ideal syntax and feature set should be -- How to integrate it seamlessly with Xcode workflows - -There is **no timeline** for USC becoming production-ready. - ---- - -## Exploring USC Anyway? - -If you want to experiment with USC despite the warnings: - -1. Read the **[USC Language Reference](./02-USC/01_Introduction.md)** -2. Try the **[USC Tutorials](./03-Tutorials/00_HelloWorld.md)** -3. Understand that **nothing is guaranteed to work** - -Do NOT use USC for: -- Production games -- Serious projects -- Teaching materials -- Anything you want to maintain long-term - ---- - -## Feedback - -If you experiment with USC, we welcome feedback: - -- What do you like about it? -- What frustrates you? -- Does it solve problems Swift doesn't? -- Would you use it if it were stable? - -Your feedback helps us decide USC's future direction. - ---- - diff --git a/docs/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md b/docs/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md deleted file mode 100644 index 06d6abd30..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -id: usc-scripting-introducction -title: Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -# Introduction to USC - -USC (Untold Script Core) is the scripting system used to define **gameplay behavior** in Untold Engine. - -USC is designed to be: -- Explicit -- Predictable -- Easy to read and reason about - -It focuses on *what your game does*, not how the engine is implemented. - ---- - -## What USC Is - -USC is a **script-based API** that allows you to attach behavior to entities. - -You use USC to: -- Respond to input -- Move entities -- Control cameras -- Apply forces -- Drive simple gameplay logic - -USC scripts describe *behavior over time*. - ---- - -## What USC Is Not - -USC is **not**: -- A general-purpose programming language -- A replacement for engine code -- A system for complex algorithms or heavy math - -If you need low-level control or complex systems, those belong in the engine itself. - -USC intentionally keeps the surface area small. - ---- - -## Why USC Exists - -Traditional game engines often expose: -- Large, complex APIs -- Implicit behavior -- Hidden update order - -USC takes a different approach. - -It is designed so that: -- Scripts read top-to-bottom -- Behavior is explicit -- There is no hidden execution magic - -This makes gameplay logic easier to understand, debug, and maintain. - ---- - -## How USC Fits into Untold Engine - -USC sits **above the engine** and **below the editor**. - -- The engine provides systems and data -- USC expresses gameplay intent -- The editor helps you attach and manage scripts - -USC does not replace the engine — it builds on top of it. - ---- - -## The Script Lifecycle - -A USC script runs as part of the engine update loop. - -At a high level: -1. The engine updates input -2. USC scripts are evaluated -3. The engine applies the results - -Scripts are evaluated every frame while the game is running. - -You do not manually manage update loops. - ---- - -## Working with Entities - -USC scripts operate on **entities**. - -A script is attached to an entity and can: -- Read entity state -- Modify transforms -- Interact with components -- Respond to input - -Scripts do not create entities or systems — they control behavior. - ---- - -## Example Use Cases - -USC is ideal for: -- Player movement -- Camera follow logic -- Simple physics interaction -- Trigger-based events -- Prototyping gameplay ideas - -If something feels *game-specific*, it likely belongs in USC. - ---- - -## Design Philosophy - -USC follows a few guiding principles: - -- **Explicit over implicit** - No hidden behavior. - -- **Readable over clever** - Scripts should be easy to understand at a glance. - -- **Stable over flexible** - Fewer features, fewer surprises. - -These principles are intentional. - - diff --git a/docs/03-Game Development/04-Scripting-Experimental/02-USC/02_QuickStart.md b/docs/03-Game Development/04-Scripting-Experimental/02-USC/02_QuickStart.md deleted file mode 100644 index bb021ce36..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/02-USC/02_QuickStart.md +++ /dev/null @@ -1,197 +0,0 @@ -# USC Scripting – Quick Start - -This tutorial will walk you through creating your first script in the Untold Engine, from setup to testing in Play mode. - -**What you'll build:** A cube that moves upward by changing its position every frame. - -**Time:** ~10 minutes - ---- - -## Step 1: Create Your First Script - -### 1.1 Configure Asset Path - -1. Open Untold Engine. -2. Go to **Asset Library → Set Asset Folder** -3. Create/Select your projects's asset directory. - -This is where your assets will be saved. Including your Scripts. - -![assetlibraryloupe](../../../images/Editor/EditorAssetLibraryLoupe.png) - -### 1.2 Create Your First Script - -1. In the editor toolbar, click **Scripts: Open in Xcode** (blue button). -2. Click the `+` button to create a new script. -3. Enter the script name: `MovingCube` -4. Xcode opens the Scripts package so you can edit the new file. - -When the script is created: -- The source file is added to your project -- Xcode shows a template like this: - -```swift -import Foundation -import UntoldEngine - -extension GenerateScripts { - static func generateMovingCube(to dir: URL) { - // Write your script here - } -} -``` - -You will edit everything in Xcode. - ---- - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateMovingCube(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateMovingCube) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - -## Step 3: Write USC Logic - -Replace the function with this complete script: - -```swift -extension GenerateScripts { - static func generateMovingCube(to dir: URL) { - let script = buildScript(name: "MovingCube") { s in - // Run every frame - s.onUpdate() - .getProperty(.position, as: "pos") - .setVariable("offset", to: simd_float3(x: 0.0, y: 0.1, z: 0.0)) - .addVec3("pos", "offset", as: "newPos") - .setProperty(.position, toVariable: "newPos") - } - - let outputPath = dir.appendingPathComponent("MovingCube.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MovingCube.uscript") - } -} -``` - ---- - -## Step 4: Build Scripts - -After editing scripts, build and run in Xcode so the engine can use the generated output. - -- In Xcode, press `Cmd+R` to run the GenerateScripts target and produce the `.uscript` files. (Optional: `Cmd+B` to check the build first.) -- Build and run output appears inside Xcode. - -> A successful build makes the script available in the Asset Library to attach to entities. - -**First build?** It may take 30-60 seconds to download dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Add an Entity to the scene - -1. In the editor, click on "+" in the scenegraph -2. Click on "Cube" -3. A cube will show up in the scene. Make sure to select it. - -## Step 6: Link script to entity - -4. Open the **Asset Library** -5. Under the Script category, look for your script `MovingCube` and double click on the `.uscript`. This will automatically link the script to the entity. -6. To verify, the Inspector View will show the newly added script linked to the entity. - -![editorassetbrowserscript](../../../images/Editor/EditorAssetBrowserScripts.png) - -The script is now active and will run according to the engine update loop. - ---- - -## Step 7: Test in Play Mode - -### 7.1 Run the Scene - -1. Click **Play** in the editor -2. Watch your entity move upward continuously! - -### 7.2 Stop Play Mode - -Click **Stop** when done testing. - ---- - -## Step 8: Test Hot Reload - Move Sideways - -Let's modify the script to move the cube sideways instead of upward, and test the hot reload feature. - -### 8.1 Modify the Offset - -Modify your script to change the offset direction: - -```swift -extension GenerateScripts { - static func generateMovingCube(to dir: URL) { - let script = buildScript(name: "MovingCube") { s in - s.onUpdate() - .getProperty(.position, as: "pos") - .setVariable("offset", to: simd_float3(x: 0.1, y: 0.0, z: 0.0)) // Changed to move sideways - .addVec3("pos", "offset", as: "newPos") - .setProperty(.position, toVariable: "newPos") - } - - let outputPath = dir.appendingPathComponent("MovingCube.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MovingCube.uscript") - } -} -``` - -**What changed:** The offset is now `(0.1, 0.0, 0.0)` instead of `(0.0, 0.1, 0.0)`, so the cube will move along the X-axis (sideways) instead of the Y-axis (upward). - -### 8.2 Rebuild - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target. -2. Wait for "✅ MovingCube.uscript" in the Xcode run output - -### 8.3 Hot Reload - -Back in the editor: -1. With your entity selected -2. Click **"Reload"** (orange button) in the Script Component Inspector -3. Click **Play** to test the changes -4. The cube should now move sideways instead of upward! - -![scriptreload](../../../images/Editor/ScriptReload.png) - diff --git a/docs/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md b/docs/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md deleted file mode 100644 index 87ff77797..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md +++ /dev/null @@ -1,108 +0,0 @@ -# USC Scripting Workflow - -This document describes the **end-to-end workflow** for developing USC scripts in the Untold Engine. - -USC scripts are authored in Xcode. The Untold Editor coordinates assets and playback but does not include a built-in script editor. - ---- - -## Overview - -The USC workflow follows a clear sequence: - -1. Author scripts -2. Register scripts for generation -3. Build scripts -4. Attach scripts to entities -5. Iterate using hot reload - -The editor is the central coordination point for this process, while Xcode is where you author scripts. - ---- - -## Primary Editing Surface - -USC scripts are written in Xcode. Use **Scripts: Open in Xcode** to open the Scripts package; the Untold Editor does not provide an in-editor script editor. - -Xcode provides: -- Direct access to script source files -- Editing of generation entry points -- Build/run feedback and diagnostics - ---- - -## Script Authoring - -Scripts are written as Swift source files using the USC DSL. - -Responsibilities: -- Express gameplay behavior -- React to input and engine state -- Remain independent of engine internals - -Scripts do not: -- Manage entities directly -- Access low-level engine systems -- Perform rendering or physics logic - ---- - -## Script Registration - -All USC scripts are registered through `GenerateScripts.swift`. - -This file: -- Defines which scripts are generated -- Acts as a single source of truth for scripting output -- Is edited directly in Xcode - -Unregistered scripts are ignored by the engine. - ---- - -## Script Generation and Build - -Building and running the GenerateScripts target: -- Runs the USC generation pipeline -- Produces engine-consumable output -- Reports errors and warnings in Xcode - -Builds are explicit and controlled. - ---- - -## Runtime Attachment - -After a successful build: -- Scripts appear as selectable components -- Scripts can be attached to entities via the Inspector -- Multiple entities may share the same script - -The engine controls execution timing. - ---- - -## Hot Reload and Iteration - -USC supports rapid iteration: - -- Edit script -- Build scripts -- Observe changes immediately - -This workflow encourages experimentation and fast feedback. - ---- - -## Design Intent - -The USC workflow is intentionally: - -- Xcode-first for authoring -- Editor-coordinated for playback -- Explicit -- Predictable -- Easy to debug - -It avoids hidden build steps and implicit execution. - diff --git a/docs/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md b/docs/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md deleted file mode 100644 index e1744538d..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md +++ /dev/null @@ -1,167 +0,0 @@ -# USC Scripts - -This document explains **what a USC script is**, how it fits into the Untold Engine, and the rules that govern its behavior. - -It is intended to give you a **clear mental model** before diving into APIs, examples, or workflows. - -This page does **not** describe how to write scripts step by step. -It explains what scripts *represent* and *why they are designed the way they are*. - ---- - -## What Is a USC Script? - -A **USC script** is a declarative description of gameplay behavior. - -You write scripts in Swift using a constrained, fluent DSL, and the engine executes those scripts at runtime as part of its update loop. - -A script expresses **intent**, not implementation details. - -Examples of intent: -- Move an entity when input is received -- Apply forces in response to events -- Adjust camera behavior over time - -The engine owns *how* those intentions are executed. - ---- - -## Scripts and Entities - -USC scripts are **attached to entities**. - -- A script always operates in the context of the entity it is attached to -- Scripts do not own entities -- Scripts do not create or destroy entities - -Multiple entities may share the same script definition, each with its own execution context and script-local state. - ---- - -## Execution Model - -Scripts participate in the engine’s execution model. - -Key characteristics: - -- Scripts run when their declared events are triggered -- The engine controls execution order and timing -- Scripts do not manage threads or scheduling -- Execution is deterministic and engine-driven - -A script never decides *when* it runs — it only declares *what should happen* when it does. - ---- - -## Script Lifecycle - -At a high level, a USC script goes through the following lifecycle: - -1. Script is authored -2. Script is registered and generated -3. Script is attached to an entity -4. Script becomes active -5. Script executes in response to events -6. Script stops executing when detached or when the entity is destroyed - -Lifecycle transitions are managed by the engine. - ---- - -## Events and Entry Points - -Scripts are organized around **events**, also called entry points. - -Examples include: -- Startup events -- Per-frame updates -- Custom or engine-driven events - -Each event defines **when a block of instructions executes**. - -Scripts may contain multiple event handlers, each expressing a different behavior. - ---- - -## Script Context - -Every script executes with a **context** provided by the engine. - -The context gives controlled access to: -- The attached entity’s properties -- Script-defined variables -- Engine-provided values (such as delta time or input state) - -Scripts never access engine state directly — all access flows through this context. - ---- - -## State and Variables - -Scripts may store local state using variables. - -- Variables are scoped to the script instance -- Each entity gets its own variable set -- Variables persist across executions unless reset - -Script variables are designed for **lightweight gameplay state**, not long-term data storage. - ---- - -## What Scripts Can Do - -USC scripts are designed to express common gameplay behaviors, such as: - -- Reading input -- Modifying transforms -- Applying forces or impulses -- Steering entities -- Controlling cameras -- Triggering animations -- Reacting to events - -Scripts focus on *what should happen*, not *how systems work internally*. - ---- - -## What Scripts Cannot Do - -USC scripts intentionally **cannot**: - -- Create or destroy entities -- Access rendering pipelines or GPU resources -- Manage memory -- Spawn threads -- Call arbitrary engine internals -- Control execution order or threading - -These constraints are deliberate and central to USC’s design. - ---- - -## Design Intent - -USC exists to provide: - -- Predictable gameplay behavior -- Clear ownership of state -- Safe boundaries between game logic and engine internals -- Fast iteration and easy debugging - -By limiting what scripts can do, the engine remains stable and behavior remains easy to reason about. - ---- - -## Relationship to the API Reference - -This document explains **what a script is**. - -For detailed information about: -- Available events -- Instructions and commands -- Math operations -- Input handling -- Physics and camera helpers - -See **USC → API Reference**. - diff --git a/docs/03-Game Development/04-Scripting-Experimental/02-USC/05_APIReference.md b/docs/03-Game Development/04-Scripting-Experimental/02-USC/05_APIReference.md deleted file mode 100644 index ac00431a7..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/02-USC/05_APIReference.md +++ /dev/null @@ -1,624 +0,0 @@ ---- -id: usc-scripting-api -title: USC Scripting API Reference -sidebar_label: API Reference -sidebar_position: 10 ---- - -# Untold Engine – USC Scripting API Reference - -USC (Untold Script Core) is the scripting system inside the Untold Engine. -You write scripts in Swift using a fluent DSL, and the engine executes them at runtime. - -This reference provides the complete API surface for building gameplay scripts. - ---- - -## 1. Script Lifecycle - -### Building and Exporting Scripts - -USC provides two ways to create scripts: - -**buildScript()** - Creates a script in memory: -```swift -let script = buildScript(name: "MyScript") { s in - s.onUpdate() - .log("Running every frame") -} -``` - -**saveUSCScript()** - Saves a script to a `.uscript` file: -```swift -let outputPath = dir.appendingPathComponent("MyScript.uscript") -try? saveUSCScript(script, to: outputPath) -``` - -**Typical Pattern** - Build then save: -```swift -extension GenerateScripts { - static func generateMyScript(to dir: URL) { - let script = buildScript(name: "MyScript") { s in - s.onUpdate() - .log("Running every frame") - } - - let outputPath = dir.appendingPathComponent("MyScript.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MyScript.uscript") - } -} -``` - -### TriggerType (Optional) - -**Default:** `.perFrame` (runs every frame) - -You only need to specify `triggerType` if you want something other than the default: - -**When to override:** - -- **`.event`** - For event-driven scripts (collision handlers, triggers) - ```swift - let script = buildScript(name: "DoorTrigger", triggerType: .event) { s in - s.onCollision(tag: "Player") // Coming soon - collision system not yet implemented - .log("Door opened!") - } - ``` - -- **`.manual`** - For manually controlled scripts (cutscenes, special sequences) - ```swift - let script = buildScript(name: "Cutscene", triggerType: .manual) { s in - s.onEvent("StartCutscene") - .log("Cutscene playing...") - } - ``` - -**Most scripts don't need to specify this** - the default `.perFrame` works for continuous behaviors like movement and AI. - -### ExecutionMode (Optional) - -**Default:** `.auto` (engine manages execution) - -You rarely need to override this. Only specify `executionMode` for advanced scenarios: - -- **`.interpreted`** - Force interpreter-based execution (debugging, special cases) - ```swift - let script = buildScript(name: "DebugScript", executionMode: .interpreted) { s in - s.onUpdate() - .log("Debug mode") - } - ``` - -**Most scripts should use the default** `.auto` mode. - ---- - -## 2. Events (Entry Points) - -Events define when code blocks execute. Chain commands after each event: - -**onStart()** - Runs once when the entity starts (like Awake/Start in Unity): -```swift -s.onStart() - .setVariable("health", to: 100.0) - .setVariable("speed", to: 5.0) - .log("Entity initialized") -``` - -**onUpdate()** - Runs every frame (like Update in Unity): -```swift -s.onUpdate() - .getProperty(.position, as: "pos") - .log("Current position") -``` - -**onCollision(tag:)** - Runs when colliding with tagged entities: -> ⚠️ **Coming Soon** - The collision system is not yet implemented. This API is planned for a future release. - -```swift -s.onCollision(tag: "Enemy") - .log("Hit an enemy!") - .setVariable("health", to: 0.0) -``` - -**onEvent(_:)** - Runs when a custom event is fired: -```swift -s.onEvent("PowerUpCollected") - .setVariable("speed", to: 10.0) - .log("Speed boost activated") -``` - -### Multiple Event Handlers - -You can define multiple event handlers in one script: -```swift -let script = buildScript(name: "Player") { s in - s.onStart() - .setVariable("score", to: 0.0) - - s.onUpdate() - .getProperty(.position, as: "pos") - - // Coming soon - collision system not yet implemented - s.onCollision(tag: "Coin") - .addFloat("score", 1.0, as: "score") -} - -let outputPath = dir.appendingPathComponent("Player.uscript") -try? saveUSCScript(script, to: outputPath) -``` - -### Interpreter Execution (Advanced) - -For `.interpreted` execution mode: -```swift -interpreter.execute(script: script, context: context, forEvent: "OnStart") -interpreter.execute(script: script, context: context, forEvent: nil) // onUpdate -``` - ---- - -## 3. Script Context - -Every script runs with a **context** that provides access to: -- **Entity properties** (position, scale, velocity, acceleration, lights) -- **Script variables** (custom data you store) -- **Engine state** (delta time, input, etc.) - -You access the entity's properties using `.getProperty()` and `.setProperty()`. - -**Available at Runtime:** -- Current entity's transform (position, scale) -- Physics properties (velocity, acceleration, mass) -- Rendering properties (color, intensity for lights) -- All script variables you've defined - -**Example:** -```swift -s.onUpdate() - .getProperty(.position, as: "currentPos") // Read from entity - .getProperty(.velocity, as: "currentVel") // Read physics - .setVariable("myCustomData", to: 42.0) // Store in script - .setProperty(.position, toVariable: "newPos") // Write to entity -``` - ---- - -## 4. Flow Control - -**Conditionals** - Execute code based on comparisons: -```swift -s.ifCondition( - lhs: .variableRef("speed"), - .greater, - rhs: .float(10.0) -) { nested in - nested.log("Too fast!") - nested.setVariable("speed", to: 10.0) -} -``` - -**Available operators:** -- `.greater`, `.less` -- `.equal`, `.notEqual` -- `.lessOrEqual`, `.greaterOrEqual` - -**Convenience conditionals:** -```swift -s.ifGreater("speed", than: 10.0) { nested in - nested.log("Too fast!") -} - -s.ifLess("health", than: 20.0) { nested in - nested.log("Low health!") -} - -s.ifEqual("state", to: 1.0) { nested in - nested.log("State is 1") -} -``` - -**Organizing math-heavy code with `.math { ... }`:** -```swift -s.onUpdate() - .math { m in - m.getProperty(.velocity, as: "vel") - m.lengthVec3("vel", as: "speed") - m.ifGreater("speed", than: 10) { n in - n.normalizeVec3("vel", as: "dir") - n.scaleVec3("dir", literal: 10, as: "clampedVel") - n.setProperty(.velocity, toVariable: "clampedVel") - } - } - .log("Velocity clamped if above 10") -``` - ---- - -## 5. Values & Variables - -**Value Types** - USC supports these data types: -```swift -enum Value { - case float(Float) // Single number - case vec3(x: Float, y: Float, z: Float) // 3D vector - case string(String) // Text - case bool(Bool) // True/false - case variableRef(String) // Reference to a variable -} -``` - -**Setting Variables:** -```swift -s.setVariable("speed", to: 5.0) -s.setVariable("direction", to: simd_float3(x: 1, y: 0, z: 0)) -s.setVariable("isActive", to: true) -s.setVariable("playerName", to: "Hero") -``` - -**Using Variable References:** -```swift -s.setVariable("maxSpeed", to: 10.0) -s.setVariable("currentSpeed", to: .variableRef("maxSpeed")) // Copy value -``` - ---- - -## 6. Engine Properties - -**Available Properties** - Read/write entity properties: -```swift -enum ScriptProperty: String { - // Transform - case position, scale - - // Physics - case velocity, acceleration, mass, angularVelocity - - // Rendering (lights) - case intensity, color - - // Engine time - case deltaTime -} -``` - -**Reading Properties:** -```swift -s.getProperty(.position, as: "pos") // Store position in "pos" variable -s.getProperty(.velocity, as: "vel") // Store velocity in "vel" variable -s.getProperty(.deltaTime, as: "dt") // Store frame delta time -``` - -**Writing Properties:** -```swift -s.setProperty(.position, toVariable: "newPos") // Set from variable -s.setProperty(.velocity, to: simd_float3(x: 0, y: 5, z: 0)) // Set from literal -s.setProperty(.angularVelocity, to: simd_float3(x: 0, y: 1, z: 0)) // Set spin (write-only today) -``` - -> Note: Rotation is controlled through `rotateTo` / `rotateBy` instructions. Reading rotation via `getProperty(.rotation, ...)` is not yet supported. - -**Complete Example:** -```swift -s.onUpdate() - .getProperty(.position, as: "currentPos") - .setVariable("offset", to: simd_float3(x: 0, y: 0.1, z: 0)) - .addVec3("currentPos", "offset", as: "newPos") - .setProperty(.position, toVariable: "newPos") // Move entity up -``` - ---- - -## 7. Math Operations - -**Float Math:** -```swift -s.addFloat("a", "b", as: "sum") // sum = a + b (two variables) -s.addFloat("a", literal: 5.0, as: "sum") // sum = a + 5 (variable + literal) -s.mulFloat("a", "b", as: "product") // product = a * b (two variables) -s.mulFloat("a", literal: 2.0, as: "product") // product = a * 2 (variable * literal) -``` - -**Vector Math:** -```swift -s.addVec3("v1", "v2", as: "sum") // sum = v1 + v2 -s.scaleVec3("dir", literal: 2.0, as: "scaled") // scaled = dir * 2.0 -s.scaleVec3("dir", by: "scale", as: "scaled") // scaled = dir * scale -s.lengthVec3("vec", as: "length") // length = magnitude of vec -s.normalizeVec3("vec", as: "unitVec") // normalized vec (zero-safe) -s.dotVec3("a", "b", as: "dot") // dot product -> float -s.crossVec3("a", "b", as: "cross") // cross product -> vec3 -s.lerpVec3(from: "a", to: "b", t: "t", as: "lerped") // linear interpolation -s.lerpFloat(from: "a", to: "b", t: "t", as: "out") // scalar lerp -s.reflectVec3("v", normal: "n", as: "reflected") // reflect v about normal -s.projectVec3("v", onto: "axis", as: "proj") // project v onto axis -s.angleBetweenVec3("a", "b", as: "angleDeg") // angle in degrees -s.clampFloat("speed", min: "minSpeed", max: "maxSpeed", as: "clampedSpeed") // bounds via vars -s.clampVec3("velocity", min: "minVel", max: "maxVel", as: "clampedVel") // component-wise -``` - -**Example - Calculate velocity:** -```swift -s.onUpdate() - .setVariable("direction", to: simd_float3(x: 1, y: 0, z: 0)) - .setVariable("speed", to: 5.0) - .scaleVec3("direction", by: "speed", as: "velocity") - .setProperty(.velocity, toVariable: "velocity") -``` - ---- - -## 8. Built-in Behaviors (Steering, Camera, Physics) - -All behaviors are instruction helpers—no `callAction` or `ScriptArgKey`. - -**Steering** -```swift -// Seek toward a target and store the steering force -s.seek(targetPosition: .vec3(x: 10, y: 0, z: 0), - maxSpeed: .float(5.0), - result: "seekForce") - -s.steerSeek(targetPosition: .variableRef("targetPos"), - maxSpeed: .variableRef("maxSpeed"), - deltaTime: .variableRef("dt"), - turnSpeed: .variableRef("turnSpeed")) - -// Arrive with slowing radius -s.steerArrive(targetPosition: .variableRef("targetPos"), - maxSpeed: .variableRef("maxSpeed"), - slowingRadius: .variableRef("slowingRadius"), - deltaTime: .variableRef("dt"), - turnSpeed: .variableRef("turnSpeed")) - -// Evade a threat: compute force into result or apply directly -s.steerEvade(threatEntity: .string("Enemy"), - maxSpeed: .float(6.0), - result: "evadeForce") // omit result to apply immediately - -// Pursuit -s.steerPursuit(targetEntity: .variableRef("targetName"), - maxSpeed: .variableRef("maxSpeed"), - deltaTime: .variableRef("dt"), - turnSpeed: .variableRef("turnSpeed")) - -// Align orientation to current velocity (smooth) -s.alignOrientation(deltaTime: .float(0.016), - turnSpeed: .float(1.0)) -``` - -**Camera** -```swift -// Snap camera to a position -s.cameraMoveTo(.vec3(x: 0, y: 3, z: -10)) - -// Look at a target -s.cameraLookAt(eye: .vec3(x: 0, y: 3, z: -8), - target: .variableRef("lookTarget"), - up: .vec3(x: 0, y: 1, z: 0)) - -// Follow a target with smoothing -s.cameraFollow(target: .string("Player"), - offset: .vec3(x: 0, y: 3, z: -6), - smoothFactor: .float(5.0), - deltaTime: .float(0.016)) - -// WASDQE fly camera -s.cameraMoveWithInput(speedVar: "moveSpeed", - deltaTimeVar: "dt", - wVar: "wPressed", - aVar: "aPressed", - sVar: "sPressed", - dVar: "dPressed", - qVar: "qPressed", - eVar: "ePressed") - -// Orbit a target entity (auto look-at) -s.cameraOrbitTarget(target: .string("Boss"), - radius: .float(12.0), - speed: .float(1.5), - deltaTime: .float(0.016), - offsetY: .float(1.5)) -``` - -**Physics** -```swift -// Impulse -s.applyLinearImpulse(direction: .vec3(x: 1, y: 0, z: 0), - magnitude: .float(5.0)) - -// Continuous world force -s.applyWorldForce(direction: .vec3(x: 0, y: 1, z: 0), - magnitude: .float(3.0)) - -// Velocity control -s.setLinearVelocity(.vec3(x: 0, y: 0, z: 5)) -s.addLinearVelocity(.variableRef("deltaVel")) -s.clampLinearSpeed(min: .float(2.0), max: .float(8.0)) - -// Angular control -s.applyAngularImpulse(axis: .vec3(x: 0, y: 1, z: 0), magnitude: .float(2.0)) -s.clampAngularSpeed(max: .float(5.0)) -s.applyAngularDamping(damping: .float(0.6), deltaTime: .float(0.016)) -``` - ---- - -## 9. Transform & Physics Helpers - -**Transform:** -```swift -s.translateTo(x: 1, y: 2, z: 3) // Set absolute position -s.translateTo(simd_float3(x: 1, y: 2, z: 3)) // Alternative syntax -s.translateBy(x: 0.1, y: 0, z: 0) // Move relative -s.translateBy(simd_float3(x: 0.1, y: 0, z: 0)) // Alternative syntax -s.rotateTo(degrees: 45, axis: simd_float3(x: 0, y: 1, z: 0)) // Set absolute rotation -s.rotateBy(degrees: 45, axis: simd_float3(x: 0, y: 1, z: 0)) // Rotate relative -s.lookAt("targetEntityName") // Face another entity -``` - -**Physics - Force & Torque:** -```swift -s.applyForce(force: simd_float3(x: 0, y: 10, z: 0)) // Apply linear force -s.applyMoment(force: simd_float3(x: 5, y: 0, z: 0), at: simd_float3(x: 1, y: 0, z: 0)) // Apply torque at point -``` - -**Physics - Velocity Control:** -```swift -s.clearVelocity() // Stop linear movement instantly -s.clearAngularVelocity() // Stop rotation instantly -s.clearForces() // Clear accumulated forces -``` - -**Physics - Gravity & Pause:** -```swift -s.setGravityScale(0.5) // Half gravity (0 = no gravity, 1 = normal, 2 = double) -s.pausePhysicsComponent(isPaused: true) // Pause/unpause physics simulation -``` - -**Example - Jump mechanic:** -```swift -s.onEvent("Jump") - .getProperty(.velocity, as: "currentVel") - .setVariable("jumpForce", to: simd_float3(x: 0, y: 15, z: 0)) - .addVec3("currentVel", "jumpForce", as: "newVel") - .setProperty(.velocity, toVariable: "newVel") -``` - -**Example - Reset physics:** -```swift -s.onEvent("Respawn") - .clearVelocity() // Stop all movement - .clearAngularVelocity() // Stop all rotation - .clearForces() // Clear force accumulation - .translateTo(simd_float3(x: 0, y: 5, z: 0)) // Move to spawn point -``` - -**Example - Apply torque to spin:** -```swift -s.onUpdate() - .ifKeyPressed("R") { n in - // Apply torque at the right edge to spin left - n.applyMoment(force: simd_float3(x: 0, y: 10, z: 0), at: simd_float3(x: 1, y: 0, z: 0)) - } -``` - -**Animation:** -```swift -s.playAnimation("Walk", loop: true) // Play looping animation -s.playAnimation("Jump", loop: false) // Play once -s.stopAnimation() // Stop current animation -``` - ---- - -## 10. Input Conditions - -**Keyboard Input:** -```swift -s.ifKeyPressed("W") { nested in - nested.log("Forward") - nested.applyForce(force: simd_float3(x: 0, y: 0, z: -1)) -} - -s.ifKeyPressed("Space") { nested in - nested.log("Jump!") - nested.applyForce(force: simd_float3(x: 0, y: 10, z: 0)) -} -``` - -**Example - WASD movement:** -```swift -s.onUpdate() - .setVariable("moveSpeed", to: 5.0) - .ifKeyPressed("W") { n in - n.applyForce(force: simd_float3(x: 0, y: 0, z: -5)) - } - .ifKeyPressed("S") { n in - n.applyForce(force: simd_float3(x: 0, y: 0, z: 5)) - } - .ifKeyPressed("A") { n in - n.applyForce(force: simd_float3(x: -5, y: 0, z: 0)) - } - .ifKeyPressed("D") { n in - n.applyForce(force: simd_float3(x: 5, y: 0, z: 0)) - } -``` - ---- - -## 11. Logging & Debugging - -**Log Messages:** -```swift -s.log("Debug message") // Simple message -s.log("Player health: 100") // Can include values -s.logValue("velocity", value: .variableRef("vel")) // Log a labeled variable -s.logValue("spawnPoint", value: .vec3(x: 0, y: 1, z: 2)) // Log a literal with a label -``` - -**Debug Variables:** -```swift -s.onUpdate() - .getProperty(.position, as: "pos") - .log("Position updated") // Track when events occur -``` - ---- - -## 12. Best Practices - -### Use enums instead of raw strings -✅ **Good:** -```swift -s.getProperty(.position, as: "pos") -s.setProperty(.velocity, toVariable: "vel") -``` - -❌ **Avoid:** -```swift -s.getProperty("position", as: "pos") // String-based, no autocomplete -``` - -### Variable Naming -- Use descriptive names: `"playerHealth"` not `"h"` -- Consistent naming: `"currentPos"`, `"targetPos"`, `"newPos"` -- Avoid conflicts with property names - -### Performance -- Use `.perFrame` for continuous behaviors (movement, AI) -- Use `.event` for one-time triggers (collision, pickups) -- Minimize operations in `onUpdate()` when possible - -### Script Organization -```swift -let script = buildScript(name: "Enemy") { s in - // Initialization - s.onStart() - .setVariable("health", to: 100.0) - .setVariable("speed", to: 3.0) - - // Main loop - s.onUpdate() - .setVariable("maxSpeed", to: 5.0) - .seek(targetPosition: .string("Player"), - maxSpeed: .variableRef("maxSpeed"), - result: "steer") - .applyForce(force: .variableRef("steer")) - - // Event handlers (collision system coming soon) - s.onCollision(tag: "Bullet") - .subtractFloat("health", 10.0, as: "health") -} - -let outputPath = dir.appendingPathComponent("Enemy.uscript") -try? saveUSCScript(script, to: outputPath) -``` - -### Debugging Tips -- Add `.log()` statements to trace execution -- Use meaningful variable names for debugging -- Test scripts incrementally -- Check console output in Play mode diff --git a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/00_HelloWorld.md b/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/00_HelloWorld.md deleted file mode 100644 index d2300510a..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/00_HelloWorld.md +++ /dev/null @@ -1,228 +0,0 @@ -# "Hello World" - Your First Script - -**What you'll learn:** -- Creating a script in Untold Engine Studio -- Using `onStart()` and `onUpdate()` lifecycle events -- Logging messages for debugging -- Attaching scripts to entities - -**Time:** ~5 minutes - ---- - -## What We're Building - -A simple script that: -1. Logs "Hello, Untold Engine!" when the entity starts -2. Logs "Script running..." every frame during Play mode - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `HelloWorld` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateHelloWorld(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateHelloWorld(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateHelloWorld) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateHelloWorld(to dir: URL) { - let script = buildScript(name: "HelloWorld") { s in - // Runs once when entity starts - s.onStart() - .log("Hello, Untold Engine!") - .log("Script initialized successfully") - - // Runs every frame - s.onUpdate() - .log("Script running...") - } - - let outputPath = dir.appendingPathComponent("HelloWorld.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ HelloWorld.uscript") - } -} -``` - -### Understanding the Code - -**`buildScript(name:)`** - Creates a new script -- The name identifies the script in the editor - -**`onStart()`** - Lifecycle event that runs once -- Perfect for initialization -- Logs appear in the console when Play mode starts - -**`onUpdate()`** - Lifecycle event that runs every frame -- Use for continuous behaviors -- Be mindful of performance (runs 60+ times per second!) - -**`.log()`** - Outputs debug messages -- Messages appear in the editor's Console view -- Great for debugging and tracking execution - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ HelloWorld.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select any entity in your scene (create a cube if needed) -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `HelloWorld.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. Open the **Console** view (bottom panel) -3. You should see: - ``` - Hello, Untold Engine! - Script initialized successfully - Script running... - Script running... - Script running... - ... - ``` - -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- **"Hello, Untold Engine!"** appears once (from `onStart()`) -- **"Script running..."** appears continuously (from `onUpdate()`) - -⚠️ **Performance Note:** `onUpdate()` runs every frame! In a real game, avoid heavy logging in `onUpdate()`. This example is just for demonstration. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Change the Messages -```swift -s.onStart() - .log("Game initialized") - .log("Player ready!") -``` - -### Remove the Update Log -```swift -s.onUpdate() - // Remove .log() to avoid console spam -``` - -### Add Initialization Variables -```swift -s.onStart() - .setVariable("playerName", to: "Hero") - .setVariable("health", to: 100.0) - .log("Player initialized with 100 health") -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ How to create a script in Untold Engine Studio -✅ Using `onStart()` for initialization -✅ Using `onUpdate()` for per-frame logic -✅ Logging debug messages -✅ Building and attaching scripts to entities -✅ Testing scripts in Play mode - ---- diff --git a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md b/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md deleted file mode 100644 index 57cf2d07e..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md +++ /dev/null @@ -1,235 +0,0 @@ -# Move an Entity - Basic Translation - -**What you'll learn:** -- Moving an entity with `setProperty(.position)` -- Using `simd_float3` direction vectors -- Controlling speed with script variables -- Updating position every frame with `onUpdate()` - -**Time:** ~7 minutes - -**Prerequisites:** -- Untold Engine Studio installed and a scene with any entity (a cube works great) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple movement script that: -1. Moves an entity steadily along a chosen axis -2. Uses variables to control speed and direction -3. Logs a message when movement begins - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `MoveAnEntity` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateMoveAnEntity(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateMoveAnEntity(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateMoveAnEntity) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateMoveAnEntity(to dir: URL) { - let script = buildScript(name: "MoveAnEntity") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("moveSpeed", to: 0.05) // units per frame - .setVariable("direction", to: simd_float3(x: 0, y: 0, z: 1)) - .log("MoveAnEntity ready") - - // Runs every frame - s.onUpdate() - .getProperty(.position, as: "currentPos") - .scaleVec3("direction", by: "moveSpeed", as: "step") - .addVec3("currentPos", "step", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") - } - - let outputPath = dir.appendingPathComponent("MoveAnEntity.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MoveAnEntity.uscript") - } -} -``` - -### Understanding the Code - -**`moveSpeed` + `direction`** - Control how fast and where to move -- Speed is a scalar; direction is a vector (x, y, z) -- Change either without touching the rest of the script - -**`getProperty(.position, as:)`** - Reads the current position -- Positions are `simd_float3` values - -**`scaleVec3()` + `addVec3()`** - Build the new position -- Scales the direction by speed -- Adds it to the current position for smooth motion - -**`setProperty(.position, toVariable:)`** - Applies the updated position -- Runs every frame inside `onUpdate()` -- Keeps the movement continuous while Play mode runs - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ MoveAnEntity.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 4: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select any entity in your scene (a cube or platform) -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `MoveAnEntity.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 5: Test It! - -1. Click **Play** in the toolbar -2. Watch the entity move steadily along the +Z axis -3. Check the **Console** view to confirm: - ``` - MoveAnEntity ready - ``` -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- The entity moves every frame toward +Z -- Speed comes from `moveSpeed`; direction comes from `direction` -- To stop movement, disable the Script Component or set speed to 0 - -⚠️ **Placement Note:** If the entity is already near a boundary, lower the speed or adjust the start position to avoid moving out of view. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Move Upward Instead -```swift -.setVariable("direction", to: simd_float3(x: 0, y: 1, z: 0)) -``` - -### Slow or Fast Motion -```swift -.setVariable("moveSpeed", to: 0.01) // slower -// or -.setVariable("moveSpeed", to: 0.15) // faster -``` - -### Pause After a Distance -```swift -s.onUpdate() - .getProperty(.position, as: "currentPos") - .ifGreater("currentPos.z", than: 10.0) { n in - n.setVariable("moveSpeed", to: 0.0) // stop after z > 10 - } - .scaleVec3("direction", by: "moveSpeed", as: "step") - .addVec3("currentPos", "step", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Creating a movement script in Untold Engine Studio -✅ Using variables for speed and direction -✅ Updating position every frame with `onUpdate()` -✅ Building and attaching scripts to entities -✅ Testing motion in Play mode - ---- - diff --git a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/02_RotateAnEntity.md b/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/02_RotateAnEntity.md deleted file mode 100644 index 7e92ccbb5..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/02_RotateAnEntity.md +++ /dev/null @@ -1,239 +0,0 @@ -# Rotate an Entity - Continuous Spin - -**What you'll learn:** -- Rotating entities with `rotateTo()` inside `onUpdate()` -- Tracking angles with script variables -- Choosing rotation axes with `simd_float3` -- Building USC scripts entirely in Xcode - -**Time:** ~7 minutes - -**Prerequisites:** -- Untold Engine Studio open with any entity selected (a cube makes rotation easy to see) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A rotation script that: -1. Spins an entity around a chosen axis -2. Uses a variable to track the current angle -3. Lets you adjust spin speed without rewriting the script - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `RotateAnEntity` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateRotateAnEntity(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateRotateAnEntity(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateRotateAnEntity) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateRotateAnEntity(to dir: URL) { - let script = buildScript(name: "RotateAnEntity") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("rotationSpeed", to: 2.0) // degrees per frame - .setVariable("currentAngle", to: 0.0) - .setVariable("axis", to: simd_float3(x: 0, y: 1, z: 0)) - .log("RotateAnEntity ready") - - // Runs every frame - s.onUpdate() - .addFloat("currentAngle", "rotationSpeed", as: "nextAngle") - .setVariable("currentAngle", to: .variableRef("nextAngle")) - .rotateTo( - degrees: .variableRef("currentAngle"), - axis: simd_float3(x:0,y:1,z:0) - ) - } - - let outputPath = dir.appendingPathComponent("RotateAnEntity.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ RotateAnEntity.uscript") - } -} -``` - -### Understanding the Code - -**`rotationSpeed`** - Controls how much to rotate each frame -- Higher values spin faster -- Change this without touching the rest of the script - -**`currentAngle`** - Tracks total rotation -- Prevents drift and keeps rotation smooth - -**`axis`** - A `simd_float3` defining which way to spin -- `(0, 1, 0)` spins around Y (like a turntable) -- `(1, 0, 0)` spins forward/backward -- `(0, 0, 1)` spins left/right - -**`rotateTo()`** - Sets an absolute rotation -- Uses the stored angle each frame -- Cleanly resets if you set `currentAngle` back to `0` - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ RotateAnEntity.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select the entity you want to rotate -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `RotateAnEntity.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. Watch the entity spin around the Y-axis -3. Open the **Console** view to confirm: - ``` - RotateAnEntity ready - ``` -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- The entity rotates smoothly using `rotationSpeed` degrees per frame -- Changing `axis` changes the spin direction without new code -- Resetting `currentAngle` to `0` realigns the entity instantly - -⚠️ **Consistency Note:** Large `rotationSpeed` values can cause visible jumps. Increase gradually for smoother motion. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Rotate with `rotateBy()` -```swift -s.onUpdate() - .rotateBy( - degrees: 1.0, - axis: simd_float3(x: 0, y: 1, z: 0) - ) -``` - -### Spin on Multiple Axes -```swift -.setVariable("axis", to: simd_float3(x: 1, y: 1, z: 0)) // diagonal spin -``` - -### Start Facing a Specific Direction -```swift -s.onStart() - .rotateTo( - degrees: 45.0, - axis: simd_float3(x: 0, y: 1, z: 0) - ) - .setVariable("currentAngle", to: 45.0) -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Creating a rotation script in Untold Engine Studio -✅ Tracking angles with script variables -✅ Rotating entities around any axis -✅ Building and attaching scripts to entities -✅ Testing rotations in Play mode - ---- - diff --git a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/02_Input/01_KeyboardMovement.md b/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/02_Input/01_KeyboardMovement.md deleted file mode 100644 index 20a9f5800..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/02_Input/01_KeyboardMovement.md +++ /dev/null @@ -1,268 +0,0 @@ -# Keyboard Movement - WASD Motion - -**What you'll learn:** -- Reading keyboard input with `getKeyState()` -- Building movement vectors from multiple keys -- Combining input with `setProperty(.position)` -- Iterating quickly with Xcode builds - -**Time:** ~10 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity you want to move -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple controller that: -1. Moves with **WASD** -2. Supports diagonal movement by combining keys -3. Uses a configurable speed variable - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `KeyboardMovement` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateKeyboardMovement(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateKeyboardMovement(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateKeyboardMovement) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateKeyboardMovement(to dir: URL) { - let script = buildScript(name: "KeyboardMovement") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("moveSpeed", to: 0.2) // units per frame - .setVariable("forward", to: simd_float3(x: 0, y: 0, z: 1)) - .setVariable("right", to: simd_float3(x: 1, y: 0, z: 0)) - .log("KeyboardMovement ready") - - // Runs every frame - s.onUpdate() - .getProperty(.position, as: "currentPos") - .setVariable("offset", to: simd_float3(x: 0, y: 0, z: 0)) - - // Read keys - .getKeyState("w", as: "wPressed") - .getKeyState("s", as: "sPressed") - .getKeyState("a", as: "aPressed") - .getKeyState("d", as: "dPressed") - - // Forward (W) - .ifEqual("wPressed", to: true) { n in - n.scaleVec3("forward", by: "moveSpeed", as: "step") - n.addVec3("offset", "step", as: "offset") - } - // Backward (S) - .ifEqual("sPressed", to: true) { n in - n.scaleVec3("forward", by: "moveSpeed", as: "stepForward") - n.scaleVec3("stepForward", literal: -1.0, as: "stepBack") - n.addVec3("offset", "stepBack", as: "offset") - } - // Left (A) - .ifEqual("aPressed", to: true) { n in - n.scaleVec3("right", by: "moveSpeed", as: "stepRight") - n.scaleVec3("stepRight", literal: -1.0, as: "stepLeft") - n.addVec3("offset", "stepLeft", as: "offset") - } - // Right (D) - .ifEqual("dPressed", to: true) { n in - n.scaleVec3("right", by: "moveSpeed", as: "stepRight") - n.addVec3("offset", "stepRight", as: "offset") - } - - // Apply movement - .addVec3("currentPos", "offset", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") - } - - let outputPath = dir.appendingPathComponent("KeyboardMovement.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ KeyboardMovement.uscript") - } -} -``` - -### Understanding the Code - -**`getKeyState()`** - Reads whether a key is pressed this frame -- Lowercase strings like `"w"` match keyboard keys - -**Offset pattern** - Build movement from multiple keys -- Start with `offset = (0,0,0)` -- Add forward/back and left/right contributions -- Apply once per frame - -**`moveSpeed` variable** - Single place to tune speed -- Scale any direction by this value (or its negative) - -**Position-based control** - Directly updates `.position` -- No physics required for this example -- Works consistently every frame - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ KeyboardMovement.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select the entity you want to move -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `KeyboardMovement.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 5: Test It! - -1. Click **Play** in the toolbar -2. Press `W`, `A`, `S`, `D` to move the entity -3. Hold multiple keys (e.g., `W` + `D`) to move diagonally -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- Movement updates every frame based on current key states -- Diagonal movement combines offset vectors naturally -- Changing `moveSpeed` updates all directions at once - -⚠️ **Scene Scale Note:** If motion is too fast for your scene scale, drop `moveSpeed` to `0.05` and retest. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Adjustable Sprint -```swift -.getKeyState("lshift", as: "shiftPressed") -.setVariable("currentSpeed", to: 0.15) -.ifEqual("shiftPressed", to: true) { n in - n.setVariable("currentSpeed", to: 0.4) -} -// Use currentSpeed instead of moveSpeed when scaling direction -``` - -### Vertical Movement -```swift -.getKeyState("space", as: "upPressed") -.getKeyState("c", as: "downPressed") -.ifEqual("upPressed", to: true) { n in - n.addVec3("offset", simd_float3(x: 0, y: 0.2, z: 0), as: "offset") -} -.ifEqual("downPressed", to: true) { n in - n.addVec3("offset", simd_float3(x: 0, y: -0.2, z: 0), as: "offset") -} -``` - -### Normalize Diagonals -```swift -.normalizeVec3("offset", as: "offset") -.scaleVec3("offset", by: "moveSpeed", as: "offset") -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Reading keyboard input with `getKeyState()` -✅ Building movement vectors from multiple keys -✅ Updating position every frame with `onUpdate()` -✅ Building and attaching scripts to entities -✅ Testing interactive input in Play mode - ---- - diff --git a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md b/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md deleted file mode 100644 index 6542e635f..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md +++ /dev/null @@ -1,241 +0,0 @@ -# Play an Animation - Single Clip - -**What you'll learn:** -- Playing an animation clip with `playAnimation()` -- Looping vs. one-shot playback -- Using variables to track the current clip -- Building and testing animations from Xcode - -**Time:** ~8 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity that has an **Animation Component** and at least one loaded clip (e.g., `idle`) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple animation script that: -1. Plays a single clip on start -2. Loops it continuously -3. Shows how to trigger a one-shot clip on demand - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `PlayAnimation` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generatePlayAnimation(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generatePlayAnimation(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generatePlayAnimation) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generatePlayAnimation(to dir: URL) { - let script = buildScript(name: "PlayAnimation") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("idleClip", to: "idle") // Must match the clip name in the Animation Component - .setVariable("currentClip", to: "idle") - .playAnimation("idle", loop: true) - .log("Playing idle animation (looping)") - - // Optional: trigger a one-shot animation with Space - s.onUpdate() - .getKeyState("space", as: "spacePressed") - .ifEqual("spacePressed", to: true) { n in - n.playAnimation("jump", loop: false) // Replace with your one-shot clip name - n.setVariable("currentClip", to: "jump") - n.log("Playing jump animation (one-shot)") - } - } - - let outputPath = dir.appendingPathComponent("PlayAnimation.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ PlayAnimation.uscript") - } -} -``` - -### Understanding the Code - -**`playAnimation(name, loop:)`** - Starts an animation by name -- `loop: true` repeats continuously (great for idle or walk) -- `loop: false` plays once (great for jump or attacks) - -**Clip names** - Must match exactly what you see in the Animation Component -- Case-sensitive -- If you rename a clip in the editor, update the script too - -**`currentClip` variable** - Tracks what is playing -- Useful when adding more conditions later - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ PlayAnimation.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select an entity that has an **Animation Component** -3. In the Asset Library, double click on the animations you want to add to the entity, i.e. `idle` and `jump` -4. In the Inspector panel, click **Add Component** → **Script Component** -5. In the Asset Browser, find `PlayAnimation.uscript` under Scripts/Generated -6. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. The entity should immediately play the `idle` clip on loop -3. Tap **Space** to trigger the one-shot clip (e.g., `jump`) -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- Looping clip runs continuously until another clip interrupts it -- One-shot clip plays once; if you need to return to idle automatically, add a timer or state check -- If nothing plays, verify the clip names match the Animation Component exactly - -⚠️ **Asset Note:** The script only calls animations that already exist on the entity’s Animation Component. Ensure clips are loaded and named correctly. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Start with a Different Clip -```swift -.setVariable("idleClip", to: "breathing_idle") -.playAnimation("breathing_idle", loop: true) -``` - -### Return to Idle After One-Shot -```swift -s.onUpdate() - .getKeyState("space", as: "spacePressed") - .ifEqual("spacePressed", to: true) { n in - n.playAnimation("jump", loop: false) - n.setVariable("currentClip", to: "jump") - } - // Simple timer to return to idle after ~1 second - .setVariable("returnTimer", to: 60.0) // frames at ~60 FPS - .ifGreater("returnTimer", than: 0.0) { n in - n.addFloat("returnTimer", literal: -1.0, as: "returnTimer") - } - .ifEqual("returnTimer", to: 0.0) { n in - n.playAnimation("idle", loop: true) - n.setVariable("currentClip", to: "idle") - } -``` - -### Add a Stop Button -```swift -.getKeyState("p", as: "pPressed") -.ifEqual("pPressed", to: true) { n in - n.stopAnimation() - n.log("Animation stopped") -} -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Playing animation clips with `playAnimation()` -✅ Looping vs. one-shot playback -✅ Tracking the current clip with script variables -✅ Building, attaching, and testing animation scripts - ---- - diff --git a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/02_AnimationStateSwitch.md b/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/02_AnimationStateSwitch.md deleted file mode 100644 index a43e42f11..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/02_AnimationStateSwitch.md +++ /dev/null @@ -1,265 +0,0 @@ -# Animation State Switch - Idle to Walk - -**What you'll learn:** -- Switching animations based on input -- Tracking current state to avoid restart flicker -- Using `playAnimation()` for looped clips -- Building and testing animations from Xcode - -**Time:** ~10 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity that has an **Animation Component** and two clips loaded (e.g., `idle`, `walk`) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -An animation controller that: -1. Plays `idle` when no movement keys are pressed -2. Switches to `walk` when any WASD key is pressed -3. Only changes clips when the state actually changes - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `AnimationStateSwitch` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateAnimationStateSwitch(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateAnimationStateSwitch(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateAnimationStateSwitch) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateAnimationStateSwitch(to dir: URL) { - let script = buildScript(name: "AnimationStateSwitch") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("currentAnim", to: "idle") - .playAnimation("idle", loop: true) - .log("AnimationStateSwitch ready - idle playing") - - // Runs every frame - s.onUpdate() - // Read movement keys - .getKeyState("w", as: "wPressed") - .getKeyState("a", as: "aPressed") - .getKeyState("s", as: "sPressed") - .getKeyState("d", as: "dPressed") - - // Determine if moving - .setVariable("isMoving", to: false) - .ifEqual("wPressed", to: true) { n in n.setVariable("isMoving", to: true) } - .ifEqual("aPressed", to: true) { n in n.setVariable("isMoving", to: true) } - .ifEqual("sPressed", to: true) { n in n.setVariable("isMoving", to: true) } - .ifEqual("dPressed", to: true) { n in n.setVariable("isMoving", to: true) } - - // Switch to walk when moving - .ifEqual("isMoving", to: true) { n in - n.ifCondition(lhs: .variableRef("currentAnim"), .notEqual, rhs: .string("walk")) { change in - change.playAnimation("walk", loop: true) - change.setVariable("currentAnim", to: "walk") - change.log("Switched to walk") - } - } - - // Switch back to idle when not moving - .ifEqual("isMoving", to: false) { n in - n.ifCondition(lhs: .variableRef("currentAnim"), .notEqual, rhs: .string("idle")) { change in - change.playAnimation("idle", loop: true) - change.setVariable("currentAnim", to: "idle") - change.log("Switched to idle") - } - } - } - - let outputPath = dir.appendingPathComponent("AnimationStateSwitch.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ AnimationStateSwitch.uscript") - } -} -``` - -### Understanding the Code - -**State tracking** - `currentAnim` stores what is playing -- Prevents restarting the same clip every frame -- Eliminates flicker when staying in the same state - -**Input gating** - `isMoving` becomes true if any WASD key is pressed -- One boolean drives all animation switching - -**`playAnimation()` only on change** - Uses a condition to switch -- Keeps transitions smooth -- Avoids repeating the same clip call - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ AnimationStateSwitch.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Add an entity to the scene. -3. In the Asset Library, double click on the animations you want to add to the entity, such as `idle` and `walk` -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `AnimationStateSwitch.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 5: Test It! - -1. Click **Play** in the toolbar -2. With no input, the entity plays `idle` -3. Press any of `W`, `A`, `S`, `D` to switch to `walk` -4. Release the keys to return to `idle` -5. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- Idle is the default and fallback state -- Walk plays only while movement keys are down -- No flicker because the script checks before switching - -⚠️ **Name Matching:** Clip names are case-sensitive. If your clips are named differently (e.g., `Idle`, `Walking`), update the script strings. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Add a Run State with Shift -```swift -.getKeyState("lshift", as: "shiftPressed") -.ifEqual("shiftPressed", to: true) { n in - n.ifEqual("isMoving", to: true) { run in - run.ifCondition(lhs: .variableRef("currentAnim"), .notEqual, rhs: .string("run")) { change in - change.playAnimation("run", loop: true) - change.setVariable("currentAnim", to: "run") - } - } -} -``` - -### Add a Jump One-Shot -```swift -.getKeyState("space", as: "jumpPressed") -.ifEqual("jumpPressed", to: true) { n in - n.playAnimation("jump", loop: false) - n.setVariable("currentAnim", to: "jump") -} -``` - -### Reset to Idle After a Delay -```swift -.setVariable("idleTimer", to: 30.0) // half-second at ~60 FPS -.ifEqual("isMoving", to: false) { n in - n.addFloat("idleTimer", literal: -1.0, as: "idleTimer") - n.ifEqual("idleTimer", to: 0.0) { back in - back.playAnimation("idle", loop: true) - back.setVariable("currentAnim", to: "idle") - } -} -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Switching animations based on player input -✅ Tracking the current clip to avoid restart flicker -✅ Using looped clips for idle/walk states -✅ Building, attaching, and testing animation controllers - ---- - diff --git a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/04_Physics/01_ApplyForce.md b/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/04_Physics/01_ApplyForce.md deleted file mode 100644 index ca5ba9cc7..000000000 --- a/docs/03-Game Development/04-Scripting-Experimental/03-Tutorials/04_Physics/01_ApplyForce.md +++ /dev/null @@ -1,232 +0,0 @@ -# Apply Force - Physics Push - -**What you'll learn:** -- Applying world-space forces with `applyWorldForce()` -- Controlling force direction and magnitude with variables -- Testing physics-driven motion from Xcode builds -- Building scripts in Xcode before playtesting - -**Time:** ~8 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity that has a Physics/Kinetic component -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A physics push that: -1. Applies a continuous upward force -2. Uses variables for direction and magnitude -3. Shows how to tweak force strength between builds - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `ApplyForce` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateApplyForce(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateApplyForce(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateApplyForce) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateApplyForce(to dir: URL) { - let script = buildScript(name: "ApplyForce") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("forceDir", to: simd_float3(x: 0, y: 1, z: 0)) - .setVariable("forceMagnitude", to: 0.2) - .log("ApplyForce ready") - - // Runs every frame - s.onUpdate() - .applyWorldForce( - direction: .variableRef("forceDir"), - magnitude: .variableRef("forceMagnitude") - ) - } - - let outputPath = dir.appendingPathComponent("ApplyForce.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ ApplyForce.uscript") - } -} -``` - -### Understanding the Code - -**`forceDir` + `forceMagnitude`** - Set the push direction and strength -- `(0, 1, 0)` pushes upward -- Reduce magnitude for gentle movement; increase for stronger lifts - -**`applyWorldForce()`** - Applies a force in world space -- Requires a Physics/Kinetic component on the entity -- Accumulates over time, so the object accelerates while the force runs - -**`onUpdate()`** - Applies force every frame -- Stop the push by setting magnitude to 0 or disabling the Script Component - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ ApplyForce.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Add an entity to the scene. -3. In the Inspector panel, click **Add Component** → **Kinetic Component** -4. In the Inspector panel, click **Add Component** → **Script Component** -5. In the Asset Browser, find `ApplyForce.uscript` under Scripts/Generated -6. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. Watch the entity accelerate upward as the force is applied each frame -3. Open the **Console** view to confirm: - ``` - ApplyForce ready - ``` -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- The entity accelerates while the force is active -- Direction is world-based; change `forceDir` to push sideways -- Lower gravity or heavier masses will change how fast it moves - -⚠️ **Physics Note:** Forces accumulate; if the object moves too fast, lower `forceMagnitude` or apply the force only on certain frames. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Short Burst Instead of Continuous Force -```swift -s.onStart() - .setVariable("framesLeft", to: 60) // apply for 1 second at ~60 FPS - -s.onUpdate() - .ifGreater("framesLeft", than: 0) { n in - n.applyWorldForce(direction: .variableRef("forceDir"), - magnitude: .variableRef("forceMagnitude")) - n.addFloat("framesLeft", literal: -1, as: "framesLeft") - } -``` - -### Sideways Push -```swift -.setVariable("forceDir", to: simd_float3(x: 1, y: 0, z: 0)) // push along +X -``` - -### Switch to Impulse -```swift -s.onUpdate() - .applyLinearImpulse(direction: .variableRef("forceDir"), - magnitude: .variableRef("forceMagnitude")) -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Applying forces to physics-enabled entities -✅ Controlling direction and magnitude with variables -✅ Building and attaching physics scripts in the editor -✅ Testing physics responses in Play mode - ---- - diff --git a/docs/04-Engine Development/01-Overview.md b/docs/04-Engine Development/01-Overview.md deleted file mode 100644 index 670971d70..000000000 --- a/docs/04-Engine Development/01-Overview.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -id: engine-overview -title: Overview -sidebar_position: 1 ---- - -# Overview - -This section is for **engine developers and contributors** who want to work on the core of Untold Engine. - -It assumes familiarity with engine concepts and systems programming. - ---- - -## What You’ll Work On - -In this section, you’ll learn about: -- The engine architecture -- The ECS design -- Rendering systems -- Simulation and update flow -- Platform-specific layers - -This is where the **core behavior** of Untold Engine lives. - ---- - -## Installing the Engine for Development - -Engine development requires installing Untold Engine via the command line and working from source. - -Detailed installation steps are provided here: - - -1. Clone the Repository - -```bash -git clone https://github.com/untoldengine/UntoldEngine -cd UntoldEngine -open Package.swift -``` - -**How to Run** -1. Select the **DemoGame** scheme. -2. Download the [Demo Game Assets v1.0](https://github.com/untoldengine/UntoldEngine-Assets/releases/tag/v1) and place them in your Desktop folder. -3. Set the Scheme to **Demo Game**. Then set **My Mac** as the target device and hit **Run**. -4. Use **WASD** keys to move the player around. - - -![DemoGame](../images/demogame-noeditor.png) - - -You do not need Untold Engine Studio to work on the engine itself. - ---- - -## Engine Architecture - -Untold Engine is built around: -- A clear ECS model -- Explicit system update order -- Minimal hidden state -- Platform-specific rendering backends - -The architecture favors **clarity over abstraction**. - -A high-level breakdown is provided in: - -> **Engine Development → Architecture** - ---- - -## Rendering and Systems - -Rendering, physics, and other systems are implemented as: -- Independent modules -- Explicit update stages -- Predictable data flow - -Rendering is designed to expose low-level control while remaining approachable. - ---- - -## Who This Section Is Not For - -This section is **not** intended for: -- Game developers writing gameplay scripts -- Users who only want to use the editor -- Beginners to engine development - -If you want to make a game, see: - -> **Game Development → Overview** - diff --git a/docs/04-Engine Development/02-Architecture/Internals.md b/docs/04-Engine Development/02-Architecture/Internals.md deleted file mode 100644 index 0d5c089d4..000000000 --- a/docs/04-Engine Development/02-Architecture/Internals.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: engine-architecture -title: Engine Architecture Internals -sidebar_position: 2 ---- - -# Untold Engine Architecture Internals - -You’re looking under the hood of Untold Engine. This is how the pieces fit together, why we picked them, and where contributors can plug in. - -## The Big Idea: ECS at the Core -Untold Engine is deliberately **ECS-first**: -- **Entities** are just 64-bit IDs (index + version) managed in `Scenes.swift` with pooling, masks, and tombstones. -- **Components** are plain data (no logic) in `ECS/Components.swift`: transforms, render payloads, physics state, animation sets, lights, scripts, etc. Components live in type-specific pools keyed by component ID. -- **Systems** are the behavior layer in `Sources/UntoldEngine/Systems/`. Each system queries entities by component mask and runs every frame (or in fixed steps for physics). Examples: Transform, Scenegraph, Rendering, Physics, Animation, Input, Culling, Loading, Lighting, Shadow, Steering, USC scripting. - -Why this matters: contributors can add data (components) and behavior (systems) without rewriting the core. Keep components dumb, keep systems focused, and everything stays modular and testable. - -## Frame Flow -`UntoldRenderer.runFrame` orchestrates each tick: -1) **Simulation prep**: delta time, scene graph traversal, input handling. -2) **Gameplay & scripting**: AnimationSystem, USCSystem, custom game update callbacks. -3) **Physics**: fixed-timestep accumulator, gravity/drag/forces, Runge–Kutta integration (collision/contact still open for contribution). -4) **Culling & rendering**: frustum cull, Gaussian depth/sort, build render graph, execute passes, present. - -## Rendering Stack -- **Entry point**: `UntoldRenderer` (MTKView delegate) sets up device/queue, loads metallib, initializes buffers, and drives the frame loop. Platform variants exist for macOS/iOS, visionOS (XR), and AR. -- **Render graph**: `RenderingSystem.swift` builds a dependency graph (environment/grid/AR base → shadow → GBuffer/model → gaussian → post → precomp) and executes via `RenderPasses` + `PipelineManager`. -- **Pipelines**: Render and compute pipelines live in `Renderer/Pipelines/`. Gaussian splats run dedicated compute passes (depth + bitonic sort) before their render pass. - -## Physics & Motion -- **Data**: `PhysicsComponents` and `KineticComponent` store mass, velocity/angular velocity, drag, inertia tensors, forces, moments, and pause flags. -- **Runtime**: `updatePhysicsSystem` accumulates forces/moments, applies gravity/drag, and integrates with Runge–Kutta. Collision/contact resolution is intentionally minimal today—prime territory for contributions. -- **Steering & Animation**: SteeringSystem and AnimationSystem run alongside physics to drive motion and skeletal playback. - -## Scripting (USC) -- **Data**: `ScriptComponent` holds one or more scripts plus file paths (backward compatible with single-script scenes). -- **Runtime**: `USCSystem` ticks the `USCInterpreter` each frame. Actions are registered in `USCScripting.swift` (math helpers, etc.). Extend the DSL by adding actions to `USCActionRegistry`. - -## Scenes, Assets, Resources -- **Scene authoring**: Swift DSL in `Scenes/Builder/` (`Node`/`SceneBuilder`) plus `SceneSerializer` for persistence. -- **Meshes/animations**: abstractions in `Mesh/` and `Skeleton`; resources created through MTK allocators/loaders. -- **Bundled bits**: Prebuilt metallibs per platform and demo resources under `Resources/`. - -## Platform Layers -- **XR (visionOS)**: `UntoldEngineXR` ties CompositorServices/ARKit to the core renderer entry. -- **AR (iOS)**: `UntoldEngineAR` wraps MTKView + ARKit for AR mode. -- **Sample**: `Sources/DemoGame` shows system registration and simple gameplay loops. - -## Where Contributors Can Make Impact -- **Collision/contact & determinism**: Build the missing collision stack, material/friction models, and deterministic stepping for netcode/replays. -- **Render passes/pipelines**: Add or refine passes in the render graph (effects, optimizations, new pipelines). -- **USC actions & API surface**: Expose new engine capabilities to scripts; add higher-level gameplay helpers. -- **Debug/observability**: Better logging sinks, on-screen overlays, profiling, and error surfacing. -- **New systems/components**: AI utilities, networking hooks, gameplay-specific data—keep components data-only and register them for serialization where relevant. - -When proposing big changes, consider ECS storage impact, system ordering, and cross-platform Metal constraints. Align early with maintainers for collision/determinism/netcode-sized work so we keep the architecture cohesive. diff --git a/docs/04-Engine Development/02-Architecture/Overview.md b/docs/04-Engine Development/02-Architecture/Overview.md deleted file mode 100644 index ae33b9cd6..000000000 --- a/docs/04-Engine Development/02-Architecture/Overview.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -id: engine-architecture overview -title: Architecture Overview -sidebar_position: 1 ---- - -# Engine Architecture - -This document describes the **high-level architecture** of Untold Engine. - -It is intended for developers who want to understand how the engine is structured *before* working on individual subsystems such as rendering, physics, or scripting. - -This page focuses on **concepts and responsibilities**, not file layouts or implementation details. - ---- - -## Architectural Philosophy - -Untold Engine is designed with a small set of guiding principles: - -- **Clarity over abstraction** - Systems should be understandable without deep indirection. - -- **Explicit execution order** - Engine behavior is deterministic and visible. - -- **Data-oriented design** - Data and behavior are separated cleanly. - -- **Composable systems** - New systems can be added without destabilizing existing ones. - -The architecture favors *learnability and debuggability* over convenience shortcuts. - ---- - -## High-Level Structure - -At a conceptual level, Untold Engine is organized into the following layers: - -Gameplay (USC Scripts) -↓ -Engine Systems -↓ -Core Runtime -↓ -Platform & Rendering Backends - -Each layer has a single responsibility and communicates downward through well-defined interfaces. - ---- - -## Core Runtime - -The **core runtime** is responsible for: - -- Entity and component storage -- System registration and ordering -- Scene lifecycle management -- Global engine state -- Frame orchestration - -The runtime does **not** contain gameplay logic, editor logic, or platform-specific behavior. - -Its role is to provide a **stable execution environment** for all systems. - ---- - -## Entity–Component–System (ECS) - -Untold Engine uses an **Entity–Component–System (ECS)** architecture. - -### Entities -- Lightweight identifiers -- Contain no data or behavior -- Used to associate components - -### Components -- Plain data containers -- Represent state (transform, physics, rendering, scripting, etc.) -- Do not contain logic - -### Systems -- Contain all behavior -- Operate on entities matching specific component sets -- Are executed in an explicit order - -This separation keeps systems focused and minimizes hidden dependencies. - ---- - -## System Execution Model - -Systems are executed as part of a well-defined update flow. - -A simplified frame looks like this: - -1. Input collection -2. Simulation and gameplay systems -3. USC script evaluation -4. Physics integration -5. Culling and render preparation -6. Rendering and presentation - -There is no implicit or callback-driven execution. - -Each system declares: -- When it runs -- What data it reads -- What data it writes - ---- - -## Rendering Architecture - -Rendering in Untold Engine is **explicit and staged**. - -Key characteristics: -- Clear separation between simulation and rendering -- Explicit render passes -- Minimal global render state -- Platform-specific backends hidden behind a thin abstraction - -Rendering is treated as a system, not a special case. - ---- - -## Platform Abstraction - -Untold Engine supports multiple platforms through **thin platform layers**. - -These layers handle: -- Window and surface management -- Input sources -- Graphics API integration -- Platform lifecycle events - -The core engine remains platform-agnostic. - ---- - -## USC Integration - -USC (Untold Script Core) is layered on top of the engine. - -- The engine owns the update loop -- USC expresses gameplay intent -- Scripts interact with engine state through a constrained API - -USC does not bypass engine systems. - -This ensures predictable behavior and keeps ownership of state clear. - ---- - -## Editor Relationship - -The Untold Editor is built **on top of the same engine runtime** used by games. - -- Editor features are implemented as engine clients -- Editor-only systems are isolated -- Play mode uses the same execution path as runtime builds - -This minimizes editor-only behavior and keeps debugging consistent. - ---- - -## Design Tradeoffs - -Untold Engine intentionally avoids: - -- Hidden execution order -- Large global managers -- Overly generic abstractions -- Implicit engine magic - -These tradeoffs prioritize: -- Predictability -- Debuggability -- Long-term maintainability - ---- - -## Architecture in Practice - -This document provides the conceptual overview. - -For implementation-level details, see: - -Engine Development → Architecture → Engine internals - diff --git a/docs/04-Engine Development/02-Architecture/_category.json b/docs/04-Engine Development/02-Architecture/_category.json deleted file mode 100644 index b32af10ff..000000000 --- a/docs/04-Engine Development/02-Architecture/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Architecture", "position": 2, "collapsed": false } diff --git a/docs/04-Engine Development/03-Engine Systems/UsingGeometryStreamingSystem.md b/docs/04-Engine Development/03-Engine Systems/UsingGeometryStreamingSystem.md deleted file mode 100644 index a3304a48e..000000000 --- a/docs/04-Engine Development/03-Engine Systems/UsingGeometryStreamingSystem.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -id: geometrystreaminggsystem -title: Geometry Streaming System -sidebar_position: 13 ---- - -# Geometry Streaming System - -The Geometry Streaming System dynamically loads and unloads geometry based on the camera's proximity to objects. This system is essential for large-scale scenes where loading all geometry at once would exceed available memory or cause performance issues. - -## How It Works - -The streaming system monitors the distance between the camera and entities that have streaming enabled. Based on configurable radius values, the system automatically: - -1. **Loads geometry** when the camera moves within the streaming radius -2. **Keeps geometry loaded** while the camera remains between the streaming and unload radii -3. **Unloads geometry** when the camera moves beyond the unload radius - -This creates a "bubble" of loaded geometry around the camera that moves with it through the scene. - -## When to Use Geometry Streaming - -**Ideal for:** -- Large open-world environments with distant objects -- Scenes where not all objects are visible simultaneously -- Memory-constrained scenarios -- Games with large view distances (forests, cities, landscapes) - -**Not recommended for:** -- Small scenes where all objects fit comfortably in memory -- Objects that are always visible -- Dynamic objects that move frequently -- Critical gameplay objects that must always be loaded - -## Basic Usage - -Here's a simple example of enabling streaming for a single entity: - -```swift -private func setupStreaming(){ - let stadium = createEntity() - setEntityMeshAsync(entityId: stadium, filename: "stadium", withExtension: "usdz") { success in - if success { - - print("Scene loaded successfully") - - enableStreaming( - entityId: stadium, - streamingRadius: 250.0, // Load when within 250 units - unloadRadius: 350.0, // Unload when beyond 350 units - priority: 10 - ) - } - } -} -``` - -### Important Notes - -1. **Load mesh first**: Always call `setEntityMeshAsync()` before enabling streaming -2. **Use completion callback**: Enable streaming inside the completion callback to ensure the mesh is loaded -3. **Async loading**: The `setEntityMeshAsync()` function loads the mesh asynchronously, preventing frame drops - -## Parameters Explained - -### `streamingRadius` -The distance from the camera at which geometry will be loaded. -- Objects closer than this distance will have their geometry loaded -- Should be set based on your camera's view distance and scene requirements -- **Typical values**: 100-500 units depending on object size and importance - -### `unloadRadius` -The distance from the camera at which geometry will be unloaded. -- Must be **larger** than `streamingRadius` to create a buffer zone -- Prevents "thrashing" (rapid loading/unloading as camera moves near the boundary) -- **Recommended**: At least 50-100 units larger than `streamingRadius` - -### `priority` -Determines the loading order when multiple objects need to be streamed. -- Higher values = loaded first -- Lower values = loaded last -- **Range**: Typically 1-10, but can be any positive integer -- **Usage**: - - High priority (8-10): Important landmarks, gameplay-critical objects - - Medium priority (4-7): Standard environment objects - - Low priority (1-3): Background details, distant decorations - -## Radius Configuration Guidelines - -Choosing the right radius values is crucial for optimal performance: - -``` -Camera Position - | - |<-- streamingRadius (250) -->|<-- buffer zone -->|<-- unloadRadius (350) -->| - | - | Geometry LOADS here | Stays loaded | Geometry UNLOADS here -``` - -### Example Configurations - -**Small objects (trees, props):** -- `streamingRadius`: 150-250 units -- `unloadRadius`: 250-350 units -- Buffer: 100 units - -**Medium objects (buildings, vehicles):** -- `streamingRadius`: 250-400 units -- `unloadRadius`: 400-550 units -- Buffer: 150 units - -**Large objects (stadiums, mountains):** -- `streamingRadius`: 500-1000 units -- `unloadRadius`: 700-1300 units -- Buffer: 200-300 units - -## Combining with Other Systems - -Geometry streaming works seamlessly with LOD and Batching systems: - -- **LOD + Streaming**: Use LOD for quality management and streaming for memory management -- **Batching + Streaming**: Batches are automatically updated as geometry loads/unloads -- **All three together**: Optimal for large open-world scenes - -See the [Combining LOD, Batching, and Streaming](./UsingLOD-Batching-Streaming.md) guide for detailed examples. - -## Best Practices - -1. **Test radius values**: Start conservative and adjust based on performance metrics -2. **Monitor memory**: Use profiling tools to ensure streaming is reducing memory usage -3. **Priority assignment**: Reserve high priorities for gameplay-critical objects -4. **Buffer zones**: Always maintain adequate buffer between streaming and unload radii -5. **Camera speed**: Faster-moving cameras may need larger streaming radii to prevent pop-in -6. **Position before streaming**: Set entity transforms before enabling streaming - -## Common Issues - -### Objects Not Loading -- Ensure `streamingRadius` is large enough for your camera's viewing distance -- Check that the mesh was loaded successfully in the completion callback -- Verify the entity has been positioned in the scene - -### Geometry "Popping" In and Out -- Increase the buffer between `streamingRadius` and `unloadRadius` -- Consider using LOD to smooth transitions -- Adjust camera movement speed or increase radii - -### Performance Issues -- Too many objects loading simultaneously: Adjust priorities to stagger loading -- Streaming radius too large: Reduce radius to load fewer objects -- Use LOD to reduce complexity of loaded geometry diff --git a/docs/04-Engine Development/03-Engine Systems/_category.json b/docs/04-Engine Development/03-Engine Systems/_category.json deleted file mode 100644 index a1c32169e..000000000 --- a/docs/04-Engine Development/03-Engine Systems/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Engine Systems", "position": 4, "collapsed": false } diff --git a/docs/04-Engine Development/04-Custom Components/_category.json b/docs/04-Engine Development/04-Custom Components/_category.json deleted file mode 100644 index 71b201664..000000000 --- a/docs/04-Engine Development/04-Custom Components/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Custom Components", "position": 5, "collapsed": false } diff --git a/docs/04-Engine Development/04-Custom Components/customComponent.md b/docs/04-Engine Development/04-Custom Components/customComponent.md deleted file mode 100644 index d327a3577..000000000 --- a/docs/04-Engine Development/04-Custom Components/customComponent.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -id: ecs-create-custom-component -title: Create a Custom Component -sidebar_position: 1 ---- - -# Create a Custom Component - -In your game, you may want to **extend functionality to an entity**. -You can do this by creating a **custom component**. - -Components in the Untold Engine are **data-only objects** that you attach to entities. -They should hold **state, not behavior**. All game logic is handled in systems. - -Every custom component must conform to the `Component` protocol. - -By following this design, your game stays modular: -- **Components** define what an entity *is capable of*. -- **Systems** define *how that capability behaves*. - ---- - -## Minimal Template - -Here’s an example of a simple custom component for a soccer player’s dribbling behavior: - -```swift -public class DribblinComponent: Component { - public required init() {} - var maxSpeed: Float = 5.0 - var kickSpeed: Float = 15.0 - var direction: simd_float3 = .zero -} -``` - -> ⚠️ Note: Components should not include functions or game logic. Keep them as pure data containers. - -## Attaching a Component to an Entity - -Once you’ve defined a component, you attach it to an entity in your scene: - -```swift -let player = createEntity(name: "player") - -// Attach DribblinComponent to the entity -registerComponent(entityId: player, componentType: DribblinComponent.self) - -// Access and modify component data -if let c = scene.get(component: DribblinComponent.self, for: player) { - c.maxSpeed = 6.5 - c.kickSpeed = 18.0 -} - -``` - -This example creates a new entity called player, attaches a DribblinComponent, and updates its values. - - -On its own, the component just stores numbers — it doesn’t do anything yet. -To make the player actually dribble, you’ll need to implement a system that processes this component each frame. diff --git a/docs/04-Engine Development/04-Custom Components/customSystem.md b/docs/04-Engine Development/04-Custom Components/customSystem.md deleted file mode 100644 index 0b6ed75bc..000000000 --- a/docs/04-Engine Development/04-Custom Components/customSystem.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: ecs-create-custom-system -title: Create a Custom System -sidebar_position: 2 ---- - -# Create a Custom System - -If you’ve created a **custom component**, you’ll usually also want to create a **custom system** to make it do something. -Components store the data, but systems are where the behavior lives. - -The engine automatically calls systems every frame. A system typically: - -1. Resolves the component IDs it cares about -2. Queries entities that have those components -3. Reads and updates their state (transforms, physics, animation, etc.) - -This separation ensures components remain pure data containers, while systems drive the simulation. - ---- - -## Minimal Template - -Here’s a simple system that works with the `DribblinComponent` we defined earlier: - -```swift -public func dribblingSystemUpdate(deltaTime: Float) { - // 1. Get the ID of the DribblinComponent - let customId = getComponentId(for: DribblinComponent.self) - - // 2. Query all entities that have this component - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - // 3. Loop through each entity and update its data - for entity in entities { - guard let dribblingComponent = scene.get(component: DribblinComponent.self, for: entity) else { - continue - } - - // Example logic: move player in the dribbling direction - dribblingComponent.direction = simd_normalize(dribblingComponent.direction) - let displacement = dribblingComponent.direction * dribblingComponent.maxSpeed * deltaTime - - if let transform = scene.get(component: LocalTransformComponent.self, for: entity) { - transform.position += displacement - } - } -} -``` - -## Registering the System -All custom systems must be registered during initialization so the engine knows to run them every frame: - -```swift -registerCustomSystem(dribblingSystemUpdate) -``` - diff --git a/docs/04-Engine Development/_category.json b/docs/04-Engine Development/_category.json deleted file mode 100644 index e71fa89a8..000000000 --- a/docs/04-Engine Development/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "Engine Development", "position": 3, "collapsed": false } - diff --git a/docs/05-Editor Development/01-Overview.md b/docs/05-Editor Development/01-Overview.md deleted file mode 100644 index 9abb30205..000000000 --- a/docs/05-Editor Development/01-Overview.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -id: editoroverview -title: Overview -sidebar_position: 1 ---- - -# Editor Development Overview - -This section is for developers who want to **improve or extend the Untold Editor**. - -The editor is a separate application built on top of Untold Engine. - ---- - -## What You’ll Work On - -In this section, you’ll learn how to: -- Understand the editor architecture -- Add or modify editor views -- Improve workflows and tooling -- Integrate editor features with the engine - -Editor development focuses on **usability and tooling**, not gameplay. - ---- - -## Installing the Editor for Development - -Working on the editor requires installing Untold Editor via the command line and running it from source. - -For editor development, clone this repository: - -```bash -git clone https://github.com/untoldengine/UntoldEditor.git -cd UntoldEditor -``` -The Editor is a Swift Package with an executable target named UntoldEditor. -It declares a dependency on the Untold Engine package; Xcode/SwiftPM will resolve it automatically. - -### Open in Xcode - -1. Open Xcode → File ▸ Open → select the Package.swift in this repo -2. Xcode will create a workspace view for the package -3. Choose the UntoldEditor scheme → Run - -### Build & run via CLI -If you prefer working with the terminal, you can build and run the Editor as follows: - -```bash -swift build -swift run UntoldEditor -``` - -### Pinning the Engine Dependency - -By default, this repo pins Untold Engine to a released version. -If you want the latest engine changes: - -#### Option A — Xcode UI -- In the project navigator: Package Dependencies → UntoldEngine -- Set Dependency Rule to Branch and type develop - -#### Option B — Edit Package.swift - -```swift -.dependencies = [ - .package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop") -] -``` - -Then reload packages: - -```bash -xcodebuild -resolvePackageDependencies -# or in Xcode: File ▸ Packages ▸ Resolve Package Versions -``` - - -The editor can be developed independently of game projects. - ---- - -## 🕹 Using the Editor - -1. **Create / Open a Project** – Use the start screen or File menu -2. **Set Asset Folder** – Choose an **external** directory for your project’s assets -3. **Import Assets** – Drag files in or use the Asset Browser “+” -4. **Build Your Scene** – Create entities, add components, use gizmos to position -5. **Play Mode** – Toggle to simulate and validate behavior -6. **Save / Load** – Save project and scene files; reopen later to continue - -> 💡 Why an *external* asset folder? -> It enables **runtime importing** and iteration without copying everything into the app bundle. - - -## Editor Architecture - -The Untold Editor is structured around: -- Independent views -- Shared engine state -- Explicit data flow between UI and engine -- Minimal magic - -Understanding the editor architecture is key before adding features. - -You can start with: - -> **Editor Development → Architecture** - ---- - -## Editor Views and Interaction - -The editor is composed of views such as: -- Scene View -- Inspector -- Asset Browser -- Scene Hierarchy -- Code Editor (note: USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - -Each view is designed to be modular and replaceable. - ---- - -## Who This Section Is Not For - -This section is **not** intended for: -- Game developers building gameplay -- Engine developers working on core systems -- Users looking to learn the editor UI - -If you want to *use* the editor, see: - -> **Game Development → Using the Editor** - diff --git a/docs/05-Editor Development/02-Architecture/EditorArchitecture.md b/docs/05-Editor Development/02-Architecture/EditorArchitecture.md deleted file mode 100644 index c2d198577..000000000 --- a/docs/05-Editor Development/02-Architecture/EditorArchitecture.md +++ /dev/null @@ -1,187 +0,0 @@ -# Editor Architecture - -This document provides a high-level overview of the **Untold Editor architecture**. - -It is intended for contributors who want to understand how the editor is structured before working on specific views, tools, or workflows. - -This page focuses on **concepts, responsibilities, and data flow**, not implementation details. - ---- - -## Architectural Goals - -The Untold Editor is designed with the following goals: - -- **Editor as a client of the engine** - The editor uses the same runtime as the game. - -- **Explicit data flow** - Editor actions translate directly into engine state changes. - -- **Minimal editor-only behavior** - Play mode mirrors runtime behavior as closely as possible. - -- **Composable tools and views** - Editor features should be modular and replaceable. - -The editor is a tool — not a separate simulation environment. - ---- - -## High-Level Structure - -At a conceptual level, the editor is organized into these layers: - -Editor UI (Views & Tools) -↓ -Editor Coordination Layer -↓ -Engine Runtime -↓ -Platform & Rendering Backends - -The editor does not own the engine — it **drives** it. - ---- - -## Editor as an Engine Client - -The Untold Editor runs on top of the same engine runtime used by games. - -Key implications: -- Scene data is real engine data -- Systems execute through the same update loop -- Rendering paths are shared -- Bugs reproduced in the editor are runtime-relevant - -There is no “fake” editor simulation. - ---- - -## Editor Coordination Layer - -Between the UI and the engine sits a thin coordination layer. - -This layer is responsible for: -- Translating UI actions into engine operations -- Managing selection state -- Coordinating editor-only modes (Edit vs Play) -- Routing commands between views - -It does **not** contain simulation logic. - ---- - -## Views - -The editor is composed of **independent views**, each with a focused responsibility. - -Typical views include: -- Scene View -- Inspector -- Scene Hierarchy -- Asset Browser -- Code Editor (USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - -Views: -- Observe engine state -- Emit commands -- Do not own core data - -This keeps views simple and interchangeable. - ---- - -## Tools and Interaction - -Editor tools (selection, transform, manipulation, etc.) are built as: -- Stateless or minimally stateful controllers -- Operating on selected engine entities -- Emitting explicit transform or component changes - -Input handling is centralized and routed to active tools. - ---- - -## Scene Editing Model - -Scene editing operates directly on engine data: - -- Entities and components are real -- Transforms update immediately -- Changes are visible to all views - -Editor-only metadata (selection, highlighting, gizmos) is stored separately. - ---- - -## Edit Mode vs Play Mode - -The editor supports two primary modes: - -### Edit Mode -- Systems that mutate simulation state are paused -- Editor tools manipulate entity state directly -- Scene changes are persistent - -### Play Mode -- Full engine update loop runs -- USC scripts execute -- Physics and animation systems are active -- Scene state may be restored on exit - -The transition between modes is explicit and controlled. - ---- - -## Asset and Resource Handling - -The editor manages assets as references to engine resources. - -Responsibilities include: -- Importing external files -- Tracking asset paths -- Updating resource bindings -- Refreshing views when assets change - -The engine remains the owner of resource lifetimes. - ---- - -## Relationship to USC - -USC scripts are authored in Xcode and managed through the editor but executed by the engine. The editor itself does not host a built-in USC script editor. - -The editor: -- Creates script source files -- Triggers script builds -- Attaches generated scripts to entities - -The editor does not interpret or execute USC logic. - ---- - -## Design Tradeoffs - -The Untold Editor intentionally avoids: - -- Duplicating engine logic -- Editor-only simulation paths -- Heavy UI-driven state mutation -- Hidden side effects from tools - -These tradeoffs prioritize: -- Consistency with runtime behavior -- Debuggability -- Contributor approachability - ---- - -## Architecture in Practice - -This document describes the conceptual structure of the editor. - -For implementation details, see: - -Editor Development → Architecture → Internals - diff --git a/docs/05-Editor Development/03-Views/AssetBrowserView.md b/docs/05-Editor Development/03-Views/AssetBrowserView.md deleted file mode 100644 index 5f4a73fa3..000000000 --- a/docs/05-Editor Development/03-Views/AssetBrowserView.md +++ /dev/null @@ -1,128 +0,0 @@ -# Asset Browser View - -The **Asset Browser View** is the discovery and selection surface for project assets within the Untold Editor. - -It presents filtered asset metadata, supports browsing and search, and emits asset selection or assignment intents through the coordination layer. - -This document describes the **architectural role** of the Asset Browser View, not how to use it as an end user. - ---- - -## Purpose - -The Asset Browser View exists to: - -- Surface the project’s asset library for discovery -- Enable filtering, searching, and grouping of assets -- Provide a control point for selecting assets -- Initiate asset assignment intents (e.g., “assign mesh,” “assign script”) without owning import or load logic - -It is the bridge between asset metadata and editor interactions that consume assets. - ---- - -## Responsibilities - -The Asset Browser View is responsible for: - -- Displaying asset entries with relevant metadata and previews -- Providing search, filter, and organization affordances -- Reflecting shared selection state for assets -- Emitting asset selection changes -- Emitting asset assignment intents to other systems (e.g., drag-and-drop targets) via commands -- Respecting editor mode (Edit vs Play) where assignment is allowed - -The Asset Browser View does **not** own asset data or selection state. - ---- - -## What This View Does NOT Do - -The Asset Browser View intentionally does **not**: - -- Own or manage asset import, processing, or versioning -- Load GPU resources or instantiate runtime objects -- Apply asset assignments directly; it emits intents through the coordination layer -- Manage project file I/O beyond reading metadata exposed by asset services -- Execute scripts or run simulation logic -- Decide selection globally; it only requests changes through shared state - -If asset browser behavior appears to require import, loading, or state ownership, that logic belongs elsewhere. - ---- - -## Data Flow - -The Asset Browser View participates in the editor data flow as follows: - -### Reads -- Asset catalog metadata (names, types, tags, thumbnails, availability) -- Asset selection state -- Editor mode state -- Search/filter parameters provided by shared state or local UI - -### Emits -- Asset selection change requests -- Asset assignment intents (e.g., assign to component field, instantiate into scene) -- Folder or collection navigation commands (if supported by asset services) -- Mode-aware warnings when an assignment is not allowed - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Asset Browser View interacts indirectly with other views via shared editor state: - -- **Scene View / Scene Hierarchy** - Receives asset drops or assignment intents to create or replace scene content - -- **Inspector** - Accepts asset assignments into component fields - -- **Code Editor** - May reference scripts as assets; USC authoring itself happens in Xcode, not inside the editor - -The Asset Browser View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full asset browsing, selection, and assignment intents are available -- Assignments update authoring data through commands - -### Play Mode -- Asset browsing remains available; assignments that mutate authoring data may be blocked or limited -- Runtime-safe assignments (if supported) are routed through the same command pathways - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Asset Browser View by: - -- Adding advanced filters (tags, dependencies, usage frequency) -- Introducing custom previews or inspectors for specific asset types -- Extending drag-and-drop behaviors for assignment targets -- Adding collection/saved search features backed by shared state - -Extensions should integrate through existing asset metadata sources and command pathways. - ---- - -## Design Constraints - -The Asset Browser View is intentionally constrained to: - -- Visualization of asset metadata and selection -- Command emission for selection and assignment intents -- Stateless or minimal local UI state driven by shared data -- No direct import, loading, or mutation of asset storage - -Keeping these boundaries strict ensures predictable asset workflows and debuggability. - diff --git a/docs/05-Editor Development/03-Views/CodeEditorView.md b/docs/05-Editor Development/03-Views/CodeEditorView.md deleted file mode 100644 index 89753d95c..000000000 --- a/docs/05-Editor Development/03-Views/CodeEditorView.md +++ /dev/null @@ -1,129 +0,0 @@ -# Code Editor View - -The **Code Editor View** is the text editing surface for scripts and other source assets in the Untold Editor. USC scripts themselves are authored in Xcode; the editor does not provide a built-in USC script editor. - -It presents source files, supports editing workflows, and emits save/build/run intents through the coordination layer without owning compilation or runtime. - -This document describes the **architectural role** of the Code Editor View, not how to use it as an end user. - ---- - -## Purpose - -The Code Editor View exists to: - -- Surface project scripts and source files for editing -- Provide an authoring surface for text assets; USC authoring lives in Xcode and is only surfaced here for viewing/coordination -- Bridge editing to build and run workflows via explicit commands -- Reflect scripting workflow state (open project, build output) in the editor context - -It is the link between source assets and scripting workflows, not the runtime itself. - ---- - -## Responsibilities - -The Code Editor View is responsible for: - -- Displaying and editing text buffers for selected source assets -- Reflecting file metadata (path, dirty state, read-only status) -- Emitting save commands for edited buffers -- Emitting build/run/test commands tied to scripting workflows -- Surfacing diagnostics and build output provided by external services -- Respecting editor mode (Edit vs Play) when enabling edits or execution commands - -The Code Editor View does **not** own source storage, compilation, or runtime execution. - ---- - -## What This View Does NOT Do - -The Code Editor View intentionally does **not**: - -- Interpret or execute USC or other scripts -- Own compilation, packaging, or deployment workflows; it triggers them -- Manage source control operations beyond reflecting status indicators -- Own project discovery or dependency resolution -- Apply engine changes directly; all actions flow through commands -- Decide selection globally; it observes selection/open-file state from shared editor context - -If code editor behavior appears to require compilation or runtime ownership, that logic belongs elsewhere. - ---- - -## Data Flow - -The Code Editor View participates in the editor data flow as follows: - -### Reads -- Source file contents and metadata (path, permissions, timestamps) -- Open-file/session state from the editor -- Build/run status and diagnostics from scripting services -- Editor mode state - -### Emits -- Save commands for current buffers -- Build/run/test commands for the active scripting project -- File open/close requests within the editor session -- Selection change requests for symbols or files if shared selection is used - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Code Editor View interacts indirectly with other views via shared editor state: - -- **Asset Browser** - Opens scripts selected in the asset list - -- **Scene View / Scene Hierarchy** - May reflect selection-driven context (e.g., open script referenced by a component) but does not call views directly - -- **Inspector** - Consumes scripts as assignable assets; script edits propagate via save and build commands - -The Code Editor View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full text editing, save, and build/run commands are enabled -- Diagnostics and build output are surfaced for authoring - -### Play Mode -- Editing may be limited or read-only depending on project policy -- Build/run commands that mutate authoring state may be blocked or deferred -- Runtime logs may be surfaced without enabling authoring changes - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Code Editor View by: - -- Adding language services (syntax highlighting, completion, diagnostics) via existing plugin hooks -- Integrating advanced navigation (symbol search, references) through shared project metadata -- Surfacing build/test pipelines with richer progress and error reporting -- Enhancing run configurations to emit explicit launch commands - -Extensions should integrate through existing command pathways and shared scripting state. - ---- - -## Design Constraints - -The Code Editor View is intentionally constrained to: - -- Text editing and presentation of scripting-related feedback -- Command emission for save/build/run -- Stateless or minimal local UI state driven by shared data -- No direct compilation, execution, or mutation of engine state - -Keeping these boundaries strict ensures predictable scripting workflows and debuggability. - diff --git a/docs/05-Editor Development/03-Views/InspectorView.md b/docs/05-Editor Development/03-Views/InspectorView.md deleted file mode 100644 index 6745678a4..000000000 --- a/docs/05-Editor Development/03-Views/InspectorView.md +++ /dev/null @@ -1,135 +0,0 @@ -# Inspector View - -The **Inspector View** is the focused property panel for examining and editing the selected entities and components in the Untold Editor. - -It presents authoritative component data for the current selection and emits structured changes back through the editor coordination layer. - -This document describes the **architectural role** of the Inspector View, not how to use it as an end user. - ---- - -## Purpose - -The Inspector View exists to: - -- Surface component data for the current selection -- Provide structured editors for component properties -- Validate edits against component rules and modes -- Emit explicit change requests to the coordination layer - -It is the bridge between selection state and component-level editing. - ---- - -## Responsibilities - -The Inspector View is responsible for: - -- Displaying the current selection (single or multi-entity) -- Rendering component sections and property editors -- Showing validation and state (read-only, overridden, default) -- Emitting add/remove component requests -- Emitting property change commands with full context (target entity, component, field, value) -- Reflecting editor mode (Edit vs Play) in allowed operations - -The Inspector View does **not** own component data or selection state. - ---- - -## What This View Does NOT Do - -The Inspector View intentionally does **not**: - -- Manage or decide selection; it only observes it -- Apply engine changes directly; all edits flow through commands -- Run simulation, scripting, or component lifecycle logic -- Own component storage or serialization; it only issues edits -- Own schemas or define component types (it consumes published definitions) -- Manage undo/redo stacks (it emits commands compatible with them) -- Provide asset authoring workflows beyond property-level references - -If Inspector behavior appears to require simulation or data ownership, that logic belongs elsewhere. - ---- - -## Data Flow - -The Inspector View participates in the editor data flow as follows: - -### Reads -- Selection state -- Component definitions and metadata (including editability rules) -- Component instances for selected entities -- Editor mode state -- Validation results and command feedback - -### Emits -- Property change commands -- Component add/remove requests -- Component reorder or enable/disable requests (if supported by schema) -- Mode-aware warnings (e.g., attempted edit in Play mode) - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Inspector View interacts indirectly with other views via shared editor state: - -- **Scene Hierarchy** - Mirrors selection changes driven by hierarchy navigation - -- **Scene View** - Reflects selection changes initiated in world space - -- **Asset Browser** - Accepts drag-and-drop asset references into component fields - -- **Code Editor** - Consumes component schemas defined in code; USC authoring occurs in Xcode, not inside the editor; does not depend on editor code directly - -The Inspector View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full component editing is enabled -- Changes persist to the authoring state -- Validation prevents invalid component configurations - -### Play Mode -- Inspector prioritizes observation; many edits are read-only or limited -- Runtime values may be displayed distinctly from authoring values -- Commands that would mutate persistent data may be blocked or deferred - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Inspector View by: - -- Adding custom property drawers for specific field types -- Providing component-specific inspectors with bespoke UI/validation -- Introducing contextual warnings or hints based on selection -- Enhancing multi-edit behavior across heterogeneous selections - -Extensions should integrate through existing data binding and command pathways. - ---- - -## Design Constraints - -The Inspector View is intentionally constrained to: - -- Observation of selection and component state -- Stateless or minimal local UI state -- Deterministic rendering based on shared data -- Command-only mutations; no direct state writes - -Keeping these boundaries strict ensures predictable editing and debuggability. - diff --git a/docs/05-Editor Development/03-Views/Overview.md b/docs/05-Editor Development/03-Views/Overview.md deleted file mode 100644 index 109c70294..000000000 --- a/docs/05-Editor Development/03-Views/Overview.md +++ /dev/null @@ -1,172 +0,0 @@ -# Editor Views Overview - -This section documents the **views that make up the Untold Editor**. - -It is intended for contributors who want to understand how editor views are structured, how they interact with the engine, and how they coordinate with each other. - -This is **not** user documentation. -It does not explain how to *use* the editor — only how it is built. - ---- - -## What Is a View? - -A **View** is a UI surface that: -- Observes engine and editor state -- Presents that state visually -- Emits explicit commands in response to user input - -Views do **not** own core data. - -They are clients of the editor and engine systems. - ---- - -## Responsibilities of a View - -Every editor view follows the same core responsibilities: - -- Render information derived from engine or editor state -- React to user input (mouse, keyboard, gestures) -- Emit commands to the editor coordination layer -- Stay stateless or minimally stateful - -A view should be easy to reason about in isolation. - ---- - -## What Views Do NOT Do - -Views intentionally do **not**: - -- Own or mutate engine state directly -- Contain simulation logic -- Manage entity lifetimes -- Execute USC scripts -- Perform rendering or physics work themselves - -If a view feels like it needs to “do logic,” that logic likely belongs elsewhere. - ---- - -## Data Flow Model - -All views participate in a shared data flow model: - -Engine Runtime -↓ -Editor Coordination Layer -↓ -Editor Views -↓ -User Input -↓ -Editor Commands -↓ -Engine Runtime - -Key characteristics: -- Data flows downward -- Commands flow upward -- Views never bypass the coordination layer - -This keeps behavior predictable and debuggable. - ---- - -## Selection as Shared State - -Selection is a cross-view concern. - -- Views may observe the current selection -- Views may request selection changes -- No single view owns selection state - -This allows multiple views to remain decoupled while staying synchronized. - ---- - -## View Independence - -Views are designed to be independent and replaceable. - -This means: -- Views do not directly call each other -- Communication happens through shared editor state -- Views can be added, removed, or replaced without breaking others - -This is a deliberate architectural decision. - ---- - -## Editor Modes and Views - -Views must respect the editor’s active mode. - -### Edit Mode -- Views manipulate persistent scene data -- Simulation systems are paused -- Tools operate directly on entity state - -### Play Mode -- Views observe runtime state -- Simulation systems are active -- Editing is limited or disabled - -Views should not decide mode behavior — they respond to it. - ---- - -## Common View Patterns - -While views are independent, many follow similar patterns: - -- Scene-oriented views -- Inspection views -- Asset-oriented views -- Code-oriented views - -Each category emphasizes different interactions but follows the same architectural rules. - ---- - -## Extending or Adding a View - -When adding a new view: - -1. Define the view’s purpose -2. Identify the data it observes -3. Identify the commands it emits -4. Ensure it does not own core state -5. Integrate it through the coordination layer - -If a new view requires engine changes, document those explicitly. - ---- - -## View Documentation Structure - -Each individual view document follows this structure: - -- Purpose -- Responsibilities -- What This View Does NOT Do -- Data Flow -- Interaction With Other Views -- Extension Points - -This keeps documentation consistent and easy to scan. - ---- - -## Available Views - -The following views are documented in this section: - -- Scene View -- Inspector -- Scene Hierarchy -- Asset Browser -- Code Editor (USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - ---- diff --git a/docs/05-Editor Development/03-Views/SceneHierarchyView.md b/docs/05-Editor Development/03-Views/SceneHierarchyView.md deleted file mode 100644 index aad738a1a..000000000 --- a/docs/05-Editor Development/03-Views/SceneHierarchyView.md +++ /dev/null @@ -1,127 +0,0 @@ -# Scene Hierarchy View - -The **Scene Hierarchy View** presents the scene graph as an ordered list/tree of entities and is the primary control surface for selection and hierarchy manipulation. - -It exposes parent/child relationships conceptually and emits structured commands for selection, reparenting, ordering, and basic entity lifecycle actions. - -This document describes the **architectural role** of the Scene Hierarchy View, not how to use it as an end user. - ---- - -## Purpose - -The Scene Hierarchy View exists to: - -- Visualize the scene graph and entity ordering -- Provide a clear control point for selecting entities -- Enable conceptual hierarchy edits (parenting, unparenting, reordering) -- Initiate entity lifecycle actions (create, duplicate, delete) through commands - -It is the bridge between the scene graph model and selection-centric editing. - ---- - -## Responsibilities - -The Scene Hierarchy View is responsible for: - -- Displaying the scene graph structure and entity ordering -- Reflecting shared selection state and allowing selection changes -- Emitting hierarchy edit commands (reparent, reorder, toggle visibility/activation) -- Emitting entity lifecycle requests (create, duplicate, delete) via commands -- Respecting editor mode (Edit vs Play) when enabling hierarchy edits - -The Scene Hierarchy View does **not** own scene data or selection state. - ---- - -## What This View Does NOT Do - -The Scene Hierarchy View intentionally does **not**: - -- Compute transforms or resolve world/local matrices -- Maintain or persist the scene graph; it only visualizes and issues commands -- Perform simulation, physics, or animation updates -- Decide selection globally; it requests changes through shared state -- Apply engine changes directly; all edits flow through the coordination layer -- Execute scripting or component logic - -If hierarchy behavior appears to require graph ownership or simulation, that logic belongs elsewhere. - ---- - -## Data Flow - -The Scene Hierarchy View participates in the editor data flow as follows: - -### Reads -- Scene graph structure (entities, parent/child links, ordering) -- Entity metadata needed for display (names, icons, state flags) -- Selection state -- Editor mode state - -### Emits -- Selection change requests -- Reparent and reorder commands -- Visibility/activation toggle commands (if exposed) -- Entity lifecycle requests (create, duplicate, delete) - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Scene Hierarchy View interacts indirectly with other views via shared editor state: - -- **Scene View** - Synchronizes selection changes between hierarchy and world-space interaction - -- **Inspector** - Displays and edits data for entities selected in the hierarchy - -- **Asset Browser** - May support drag-and-drop of assets to instantiate entities or assign references - -The Scene Hierarchy View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full hierarchy editing (reparent, reorder, lifecycle actions) is available -- Selection changes drive authoring-state inspection - -### Play Mode -- Hierarchy is primarily observational; destructive edits may be blocked or limited -- Selection may reflect runtime entities without persisting authoring changes - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Scene Hierarchy View by: - -- Adding filters, search, or grouping affordances -- Introducing custom badges or state indicators derived from metadata -- Extending drag-and-drop behaviors for entity or asset interactions -- Providing bulk operations that emit explicit hierarchy commands - -Extensions should integrate through existing command pathways and shared state. - ---- - -## Design Constraints - -The Scene Hierarchy View is intentionally constrained to: - -- Visualization of the scene graph and selection state -- Command emission for hierarchy and lifecycle edits -- Stateless or minimal local UI state driven by shared data -- No direct mutation of engine or editor core data - -Keeping these boundaries strict ensures predictable editing and debuggability. - diff --git a/docs/05-Editor Development/03-Views/SceneView.md b/docs/05-Editor Development/03-Views/SceneView.md deleted file mode 100644 index 984b7fd7b..000000000 --- a/docs/05-Editor Development/03-Views/SceneView.md +++ /dev/null @@ -1,158 +0,0 @@ -# Scene View - -The **Scene View** is the primary spatial view in the Untold Editor. - -It provides a visual representation of the current scene and allows contributors and tools to interact with entities in world space. - -This document describes the **architectural role** of the Scene View, not how to use it as an end user. - ---- - -## Purpose - -The Scene View exists to: - -- Visualize the current scene state -- Display entities and their transforms -- Provide spatial context for selection and manipulation -- Act as the main interaction surface for world-space tools - -It is the bridge between engine data and spatial editor interaction. - ---- - -## Responsibilities - -The Scene View is responsible for: - -- Rendering a visual representation of the scene -- Displaying editor overlays (selection outlines, gizmos, helpers) -- Receiving world-space input (mouse, keyboard, gestures) -- Emitting commands related to selection and transformation -- Respecting the current editor mode (Edit vs Play) - -The Scene View does **not** own scene data. - ---- - -## What This View Does NOT Do - -The Scene View intentionally does **not**: - -- Own or modify engine state directly -- Perform simulation, physics, or animation updates -- Decide how entities are selected globally -- Execute USC scripts -- Implement rendering pipelines or render passes - -If the Scene View appears to require simulation logic, that logic belongs elsewhere. - ---- - -## Data Flow - -The Scene View participates in the editor data flow as follows: - -### Reads -- Scene graph data -- Entity transforms -- Camera state -- Selection state -- Editor mode state - -### Emits -- Selection change requests -- Transform manipulation commands -- Camera navigation commands -- Tool activation signals - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Scene View interacts indirectly with other views via shared editor state: - -- **Scene Hierarchy** - Synchronizes selection changes - -- **Inspector** - Displays and edits data for selected entities - -- **Asset Browser** - May initiate drag-and-drop actions into the scene - -The Scene View does not directly communicate with other views. - ---- - -## Tools and Manipulation - -World-space tools (translate, rotate, scale, etc.) are driven through the Scene View. - -Key characteristics: -- Tools operate on selected entities -- Input is routed to the active tool -- Tools emit explicit transform commands -- Visual gizmos are editor-only overlays - -The Scene View hosts tool interaction but does not implement tool logic. - ---- - -## Camera Control - -The Scene View owns the **editor camera**, which is distinct from game cameras. - -Responsibilities include: -- Camera navigation (orbit, pan, zoom) -- Framing selected entities -- Switching camera perspectives - -Editor camera state is isolated from runtime camera components. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Scene View allows full manipulation -- Entity transforms are persistent -- Editor overlays and gizmos are visible - -### Play Mode -- Scene View observes runtime state -- Manipulation may be limited or disabled -- Runtime cameras may be previewed - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Scene View by: - -- Adding new world-space tools -- Introducing new editor overlays -- Customizing camera behavior -- Adding debug visualization layers - -Extensions should integrate through existing tool and command pathways. - ---- - -## Design Constraints - -The Scene View is intentionally constrained to: - -- Visualization -- Input capture -- Command emission - -Keeping these boundaries strict ensures: -- Predictable behavior -- Easier debugging -- Cleaner separation of concerns - diff --git a/docs/05-Editor Development/EditorOverview.md b/docs/05-Editor Development/EditorOverview.md deleted file mode 100644 index 27704bf80..000000000 --- a/docs/05-Editor Development/EditorOverview.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -id: editorfeatures -title: Editor Features -sidebar_position: 2 ---- - -# Untold Engine Editor Features - -The Untold Engine Editor makes it easier than ever to set up your scenes and entities without touching code. With the Editor, you can visually create, configure, and organize your game world, while keeping Swift code focused on game logic and behaviors. - -Here’s a quick tour of the Editor’s main features: - ---- - -## Scene Graph - -![Scene Graph](../images/engine-scenegraph.png) - -The **Scene Graph** shows all the entities in your scene in a hierarchical view. You can add, rename, and organize entities, as well as set up parent–child relationships. This is where you’ll find your cameras, lights, and models at a glance. - ---- - -## Inspector - -![Inspector](../images/engine-inspector.png) - -The **Inspector** lets you configure properties of the selected entity. Adjust position, rotation, and scale, or fine-tune camera settings and add components. Think of it as the control panel for whatever you’re working on. - ---- - -## Gizmo Manipulation - -![Gizmo](../images/engine-gizmo.png) - -Use the **3D gizmo** to interactively move, rotate, and scale entities directly in the scene. This makes it quick to place objects exactly where you want them. - ---- - -## Materials - -![Materials](../images/engine-materials.png) - -Assign meshes and materials through the **Inspector**. You can drop in textures for base color, roughness, metallic, and emissive maps, then tweak material properties to get the right look. - ---- - -## Lighting & Environment - -![Lighting](../images/engine-lights.png) - -The **Lighting panel** give you control over your scene’s mood. Add directional, point, spotlight and area lights, adjust intensities. - -## Environment - -The **Environment panel** enable **Image-Based Lighting (IBL)** for realistic reflections and ambient light. - -![HDR](../images/engine-hdr.png) - ---- - -## Post-Processing Effects - -![Post Processing](../images/engine-post-processing.png) - -The **Effects tab** lets you add and tweak post-processing features such as: -- Depth of Field -- Chromatic Aberration -- Bloom -- Color Grading -- SSAO, Vignette, White Balance, and more - -These effects bring your scenes closer to a polished, production-ready look. - ---- - -## Asset Browser - -![Asset Browser](../images/engine-assetbrowser.png) - -The **Asset Browser** keeps your models, textures, and materials organized. Import new assets, set paths for your project, and quickly assign resources to entities in your scene. - ---- - -## Console Log - -![Console](../images/engine-consolelog.png) - -The **Console Log** provides real-time feedback from the engine. Use it to debug entity creation, monitor system messages, and track issues while working on your scene. - ---- - -## Putting It All Together - -With the Editor, you now have a clear separation of responsibilities: -- **Use the Editor** for entity initialization, scene setup, materials, and visual configuration. -- **Use Swift code** for gameplay logic, physics tweaks, and systems that bring your game to life. - -This workflow makes iteration faster and keeps your codebase focused on what matters most: gameplay. - diff --git a/docs/06-Reference/01-USCAPI.md b/docs/06-Reference/01-USCAPI.md deleted file mode 100644 index 0c961d1cb..000000000 --- a/docs/06-Reference/01-USCAPI.md +++ /dev/null @@ -1,133 +0,0 @@ -# Untold Script Core API - -Use this page as a compact checklist of the most-used USC (Untold Script Core) DSL calls. All snippets assume you’re inside a `buildScript` closure on the current entity (`self`). - ---- - -## Lifecycle -- `onStart()` – one-time init per play. -- `onUpdate()` – every frame. -- `onEvent("Name")` – custom triggers. -- `onCollision(tag:)` – **planned** (not yet available). - -## Properties (get/set) -Supported keys: `.position`, `.scale`, `.velocity`, `.acceleration`, `.mass`, `.angularVelocity` (write-only today), `.intensity`, `.color`, `.deltaTime`. -```swift -.getProperty(.position, as: "pos") -.setProperty(.position, toVariable: "nextPos") -.setProperty(.velocity, to: simd_float3(0, 2, 0)) -``` - -## Input -```swift -.ifKeyPressed("W") { n in n.log("forward") } -.ifKeyReleased("Space") { n.log("jump released") } -.getKeyState("w", as: "wPressed") -``` - -## Transform -```swift -.translateTo(simd_float3(0, 1, 0)) -.translateBy(simd_float3(0, 0, 1)) -.rotateTo(degrees: 45, axis: simd_float3(0, 1, 0)) -.rotateBy(degrees: .float(5), axis: simd_float3(0, 1, 0)) -.lookAt("TargetEntity") -``` - -## Animation -```swift -.playAnimation("Walk", loop: true) -.stopAnimation() -``` - -## Physics (forces/velocity) -```swift -.applyForce(force: simd_float3(0, 10, 0)) -.applyLinearImpulse(direction: .vec3(x: 0, y: 1, z: 0), magnitude: .float(5)) -.setLinearVelocity(.vec3(x: 0, y: 0, z: 5)) -.addLinearVelocity(.vec3(x: 0, y: 0, z: -1)) -.clampLinearSpeed(min: .float(0), max: .float(8)) -.clearVelocity() -.clearAngularVelocity() -.clearForces() -.setGravityScale(0.5) -.pausePhysicsComponent(isPaused: true) -``` - -## Steering (vectors or side effects) -```swift -.seek(targetPosition: .vec3(x: 10, y: 0, z: 0), maxSpeed: .float(5), result: "steer") -.flee(threatPosition: .vec3(x: 0, y: 0, z: 0), maxSpeed: .float(6), result: "steer") -.arrive(targetPosition: .vec3(x: 0, y: 0, z: 0), maxSpeed: .float(6), slowingRadius: .float(2), result: "steer") -.pursuit(targetEntity: .string("Player"), maxSpeed: .float(6), result: "steer") -.evade(threatEntity: .string("Enemy"), maxSpeed: .float(6), result: "steer") -.steerSeek(targetPosition: .variableRef("targetPos"), maxSpeed: .variableRef("maxSpeed"), deltaTime: .variableRef("dt")) -.steerArrive(targetPosition: .variableRef("targetPos"), maxSpeed: .variableRef("maxSpeed"), slowingRadius: .variableRef("slow"), deltaTime: .variableRef("dt")) -.steerFlee(threatPosition: .variableRef("threatPos"), maxSpeed: .variableRef("maxSpeed"), deltaTime: .variableRef("dt")) -.steerPursuit(targetEntity: .string("Target"), maxSpeed: .float(6), deltaTime: .float(0.016)) -.orbit(centerPosition: .vec3(x: 0, y: 0, z: 0), radius: .float(5), maxSpeed: .float(4), deltaTime: .float(0.016)) -.alignOrientation(deltaTime: .float(0.016), turnSpeed: .float(1.0)) -``` - -## Camera -```swift -.cameraMoveTo(.vec3(x: 0, y: 3, z: -10)) -.cameraMoveBy(.vec3(x: 1, y: 0, z: 0)) -.cameraRotate(pitch: .float(0.02), yaw: .float(-0.08)) -.cameraFollow(target: .string("Player"), - offset: .vec3(x: 0, y: 3, z: -6), - smoothFactor: .float(5), - deltaTime: .float(0.016)) -.cameraFollowLocal(target: .string("Player"), - localOffset: .vec3(x: 0, y: 2, z: -4), - smoothFactor: .float(5), - deltaTime: .float(0.016)) -.cameraOrbitTarget(target: .string("Boss"), - radius: .float(12), - speed: .float(1.5), - deltaTime: .float(0.016), - offsetY: .float(1.5)) -.cameraMoveWithInput(speedVar: "moveSpeed", - deltaTimeVar: "dt", - wVar: "wPressed", aVar: "aPressed", - sVar: "sPressed", dVar: "dPressed", - qVar: "qPressed", eVar: "ePressed") -``` - -## Math (variables only) -```swift -.addFloat("a", "b", as: "sum") -.addFloat("a", literal: 1, as: "sum") -.subFloat("a", "b", as: "diff") -.mulFloat("a", "b", as: "prod") -.divFloat("a", "b", as: "quot") -.addVec3("v1", "v2", as: "sum") -.scaleVec3("v", by: "s", as: "out") -.scaleVec3("v", literal: 2, as: "out") -.lengthVec3("v", as: "len") -.normalizeVec3("v", as: "unit") -.dotVec3("a", "b", as: "dot") -.crossVec3("a", "b", as: "cross") -.lerpVec3(from: "a", to: "b", t: "t", as: "out") -.lerpFloat(from: "a", to: "b", t: "t", as: "out") -.reflectVec3("v", normal: "n", as: "reflected") -.projectVec3("v", onto: "axis", as: "proj") -.angleBetweenVec3("a", "b", as: "angleDeg") -.clampFloat("speed", min: "minSpeed", max: "maxSpeed", as: "clamped") -.clampVec3("vel", min: "minVel", max: "maxVel", as: "clampedVel") -``` - -## Flow / Variables / Debug -```swift -.ifCondition(lhs: .variableRef("speed"), .greater, rhs: .float(10)) { n in n.log("Too fast") } -.ifGreater("health", than: 0) { n in n.log("Alive") }.else { n in n.log("Dead") } -.ifEqual("flag", to: true) { n in n.log("Flag set") } -.setVariable("speed", to: 5.0) -.setVariable("dir", to: simd_float3(0, 0, 1)) -.setVariable("copy", fromVariable: "speed") -.log("Hello") -.logValue("velocity", value: .variableRef("vel")) -``` - -> Tip: If you need an entity other than `self`, use names in your instructions (e.g., `.lookAt("TargetName")`) or stash them in variables, then call `findEntity`-driven instructions like `pursuit`/`evade`. - diff --git a/docs/06-Reference/02-EngineAPI.md b/docs/06-Reference/02-EngineAPI.md deleted file mode 100644 index 0d649d551..000000000 --- a/docs/06-Reference/02-EngineAPI.md +++ /dev/null @@ -1,408 +0,0 @@ -# Untold Engine Core API - -## Quick Reference - - -| Category | Common APIs | -|--------------|-------------| -| **Entities** | [createEntity](#create-an-entity), [destroyEntity](#destroy-an-entity), [setParent](#parent-child-relationships), [findEntity](#find-entity) | -| **Transforms** | [getLocalPosition](#get-local-position), [getPosition](#get-world-position), [getLocalOrientation](#get-local-orientation), [getOrientation](#get-world-orientation), [getForwardAxisVector](#get-axis-vectors), [translateTo](#translate-the-entity), [translateBy](#translate-the-entity), [rotateTo](#rotate-the-entity), [rotateBy](#rotate-the-entity) | -| **Assets** | [assetBasePath](#base-path-to-assets) | -| **Rendering** | [setEntityMesh](#link-a-mesh-to-the-entity), [setEntityGaussian](#link-a-gaussian-splat-to-the-entity), [createDirLight](#directional-light), [createPointLight](#point-light), [createSpotLight](#spot-light), [createAreaLight](#area-light) | -| **Animation** | [setEntityAnimations](#load-an-animation), [changeAnimation](#set-the-animation-to-play), [pauseAnimationComponent](#pause-the-animation-optional) | -| **Physics** | [setEntityKinetics](#enable-physics-on-the-entity), [setMass](#configure-physics-properties), [setGravityScale](#configure-physics-properties), [applyForce](#apply-forces-optional), [steerTo](#use-the-steering-system), [steerAway](#additional-steering-functions), [steerPursuit](#additional-steering-functions), [followPath](#additional-steering-functions) | -| **Components** | [registerComponent](#register-components), [create-custom-component](#create-custom-component), [attach-component](#attaching-a-component-to-an-entity) | -| **Systems** | [create-custom-system](#create-custom-system), [registerCustomSystem](#registering-the-custom-system) | - - -# Entities - -### Create an Entity - -Entities represent objects in the scene. Use the createEntity() function to create a new entity. - -```swift -let entity = createEntity() -``` - -### Destroy an Entity - -To remove an entity and its components from the scene, use destroyEntity. - -```swift -destroyEntity(entityId: entity) -``` - -This ensures the entity is properly removed from all systems. - ---- - -### Parent-Child Relationships - -To assign a parent to an entity, use the setParent function. This function establishes a hierarchical relationship between the specified entities. - -```swift -// Create child and parent entities -let childEntity = createEntity() -let parentEntity = createEntity() - -// Set parent-child relationship -setParent(childId: childEntity, parentId: parentEntity) -``` - -### Find Entity - -You can find an entity by name using the following function: - -```swift -let ball = findEntity(name: "ball") -``` ---- - -# Transforms - -### Get Local Position - -Retrieves the entity’s position relative to its parent. - -```swift -let localPosition = getLocalPosition(entityId: entity) -``` - -### Get World Position - -Retrieves the entity’s absolute position in the scene. - -```swift -let worldPosition = getPosition(entityId: entity) -``` - -### Get Local Orientation - -Retrieves the entity’s orientation matrix relative to its parent. - -```swift -let localOrientation = getLocalOrientation(entityId: entity) -``` - -### Get World Orientation - -Retrieves the entity’s absolute orientation matrix. - -```swift -let worldOrientation = getOrientation(entityId: entity) -``` - -### Get Axis Vectors - -Retrieve the entity’s forward, right, or up axis: - -```swift -let forward = getForwardAxisVector(entityId: entity) -let right = getRightAxisVector(entityId: entity) -let up = getUpAxisVector(entityId: entity) -``` - ---- - -### Translate the Entity - -Move the entity to a new position: - -```swift -translateTo(entityId: entity, position: simd_float3(5.0, 0.0, 3.0)) -``` - -Move the entity by an offset relative to its current position: - -```swift -translateBy(entityId: entity, position: simd_float3(1.0, 0.0, 0.0)) -``` - -### Rotate the Entity - -Rotate the entity to a specific angle around an axis: - -```swift -rotateTo(entityId: entity, angle: 45.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Apply an incremental rotation to the entity: - -```swift -rotateBy(entityId: entity, angle: 15.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Directly set the entity’s rotation matrix: - -```swift -rotateTo(entityId: entity, rotation: simd_float4x4( /* matrix values */ )) -``` - - ---- - -### Base path to assets - -Define the asset directory the engine will use to load content (Assuming you are using an external folder during development). Please see Import-Export section for more details. - -```swift -// Here we point it to a folder named "DemoGameAssets/Assets" on the Desktop. -// You can change this to any folder where you keep your own assets. -if let desktopURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first { - assetBasePath = desktopURL.appendingPathComponent("DemoGameAssets/Assets") -} -``` - -# Rendering - -### Link a Mesh to the Entity - -To display a model, load its .usdc file and link it to the entity using setEntityMesh. - -```swift -setEntityMesh(entityId: entity, filename: "entity", withExtension: "usdc") -``` - -Parameters: - -- entityId: The ID of the entity created earlier. -- filename: The name of the .usdc file (without the extension). -- withExtension: The file extension, typically "usdc". - -> Note: If PBR textures (e.g., albedo, normal, roughness, metallic maps) are included, the rendering system will automatically use the appropriate PBR shader to render the model with realistic lighting and material properties. - -### Link a Gaussian Splat to the Entity - -To display a Gaussian Splat model, load its .ply file and link it to the entity using setEntityGaussian. - -```swift -setEntityGaussian(entityId: entity, filename: "splat", withExtension: "ply") -``` - -Parameters: - -- entityId: The ID of the entity created earlier. -- filename: The name of the .ply file (without the extension). -- withExtension: The file extension, typically "ply". - -> Note: The Gaussian System renders point cloud data stored in the .ply format. Ensure your Gaussian Splat file is properly formatted and contains the necessary attributes (position, color, opacity, scale, rotation). - -### Directional Light - -Use for sunlight or distant key lights. Orientation (rotation) defines its direction. - -```swift -let sun = createEntity() -createDirLight(entityId: sun) -``` -### Point Light - -Omni light that radiates equally in all directions from a position. - -```swift -let bulb = createEntity() -createPointLight(entityId: bulb) -``` - -### Spot Light - -Cone-shaped light with a position and direction. - -```swift -let spot = createEntity() -createSpotLight(entityId: spot) -``` - -### Area Light - -Rect/area emitter used to mimic panels/windows; position and orientation matter. - -```swift -let panel = createEntity() -createAreaLight(entityId: panel) -``` - -### Enable or Disable PostFX - -Toggle post-processing effects globally through the PostFX facade: - -```swift -PostFX.setEnabled(.colorGrading, false) - -PostFX.enableVignette(false) -``` - -### SSAO Controls - -SSAO is managed through a dedicated API because it affects render quality and pipelines: - -```swift -SSAO.setEnabled(true) -SSAO.setQuality(.high) -SSAO.setRadius(0.8) -SSAO.setBias(0.02) -SSAO.setIntensity(1.2) -``` - ---- - -# Animation - -### Load an Animation -Load the animation data for your model by providing the animation .usdc file and a name to reference the animation later. - -```swift -setEntityAnimations(entityId: redPlayer, filename: "running", withExtension: "usdc", name: "running") -``` - -### Set the Animation to play - -Trigger the animation by referencing its name. This will set the animation to play on the entity. - -```swift -changeAnimation(entityId: redPlayer, name: "running") -``` - -### Pause the animation (Optional) - -To pause the current animation, simply call the following function. The animation component will be paused for the current entity. - -```swift -pauseAnimationComponent(entityId: redPlayer, isPaused: true) -``` - ---- - -# Physics - -### Enable Physics on the Entity - -Activate the physics simulation for your entity using the setEntityKinetics function. This function prepares the entity for movement and dynamic interaction. - -```swift -setEntityKinetics(entityId: redPlayer) -``` ---- - -### Configure Physics Properties -You can customize the entity’s physics behavior by defining its mass and gravity scale: - -- Mass: Determines the force needed to move the object. Heavier objects require more force. -- Gravity Scale: Controls how strongly gravity affects the entity (default is 0.0). - -```swift -setMass(entityId: redPlayer, mass: 0.5) -setGravityScale(entityId: redPlayer, gravityScale: 1.0) -``` - -### Apply Forces (Optional) -You can apply a custom force to the entity for dynamic movement. This is useful for simulating actions like jumps or pushes. - -```swift -applyForce(entityId: redPlayer, force: simd_float3(0.0, 0.0, 5.0)) -``` - -> Note: Forces are applied per frame. To avoid unintended behavior, only apply forces when necessary. - -### Use the Steering System -For advanced movement behaviors, leverage the Steering System to steer entities toward or away from targets. This system automatically calculates the required forces. - -Example: Steering Toward a Position - -```swift -steerTo(entityId: redPlayer, targetPosition: simd_float3(0.0, 0.0, 5.0), maxSpeed: 2.0, deltaTime: deltaTime) -``` - -### Additional Steering Functions - -The Steering System includes other useful behaviors, such as: - -- steerAway() -- steerPursuit() -- followPath() - -These functions simplify complex movement patterns, making them easy to implement. - ---- - -# Components - -### Register Components - -Components define the behavior or attributes of an entity. Use registerComponent to add a component to an entity. - -```swift -let entity = createEntity() - -registerComponent(entityId: entity, componentType: RenderComponent.self) -``` - -### Create Custom Component - -Here’s an example of a simple custom component for a soccer player’s dribbling behavior: - -```swift -public class DribblinComponent: Component { - public required init() {} - var maxSpeed: Float = 5.0 - var kickSpeed: Float = 15.0 - var direction: simd_float3 = .zero -} -``` - -> ⚠️ Note: Components should not include functions or game logic. Keep them as pure data containers. - -### Attaching a Component to an Entity - -Once you’ve defined a component, you attach it to an entity in your scene: - -```swift -let player = createEntity(name: "player") - -// Attach DribblinComponent to the entity -registerComponent(entityId: player, componentType: DribblinComponent.self) -``` - -# Systems - -### Create Custom System - -If you’ve created a **custom component**, you’ll usually also want to create a **custom system** to make it do something. -Components store the data, but systems are where the behavior lives. - -The engine automatically calls systems every frame. - -Here’s a simple system that works with the `DribblinComponent` we defined earlier: - -```swift -public func dribblingSystemUpdate(deltaTime: Float) { - // 1. Get the ID of the DribblinComponent - let customId = getComponentId(for: DribblinComponent.self) - - // 2. Query all entities that have this component - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - // 3. Loop through each entity and update its data - for entity in entities { - guard let dribblingComponent = scene.get(component: DribblinComponent.self, for: entity) else { - continue - } - - // Example logic: move player in the dribbling direction - dribblingComponent.direction = simd_normalize(dribblingComponent.direction) - let displacement = dribblingComponent.direction * dribblingComponent.maxSpeed * deltaTime - - if let transform = scene.get(component: LocalTransformComponent.self, for: entity) { - transform.position += displacement - } - } -} -``` - -### Registering the Custom System -All custom systems must be registered during initialization so the engine knows to run them every frame: - -```swift -registerCustomSystem(dribblingSystemUpdate) -``` diff --git a/docs/07-Contributor/_category.json b/docs/07-Contributor/_category.json deleted file mode 100644 index 4308c167f..000000000 --- a/docs/07-Contributor/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "10-Contributor", "position": 99, "collapsed": false } - diff --git a/docs/08-CLI/CLI.md b/docs/08-CLI/CLI.md deleted file mode 100644 index 0ff5498a8..000000000 --- a/docs/08-CLI/CLI.md +++ /dev/null @@ -1,505 +0,0 @@ -# UntoldEngine CLI Tool - -> **Note:** The CLI tool source code is located in the `Tools/UntoldEngineCLI/` directory of the repository. -> -> For the most up-to-date documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - -The `untoldengine-create` command-line tool allows you to create and manage UntoldEngine game projects without launching the UntoldEditor. - -## Quick Start - -```bash -# Clone the repository -git clone https://github.com/untoldengine/UntoldEngine.git -cd UntoldEngine - -# Install the CLI globally -./scripts/install-create.sh - -# Create a project from anywhere -cd ~/anywhere -mkdir MyGame && cd MyGame -untoldengine-create create MyGame -``` - -## Complete Documentation - -For complete CLI documentation including: - -- Installation instructions -- Command reference -- Platform options -- Usage examples -- Troubleshooting - -Please refer to `Tools/UntoldEngineCLI/README.md` in the repository. - -## Quick Start - -Create a new game project in three simple steps: - -```bash -# 1. Create and enter your project directory -mkdir MyGame && cd MyGame - -# 2. Create the project (default: macOS) -untoldengine-create create MyGame - -# 3. Open in Xcode -open MyGame/MyGame.xcodeproj -``` - -All project files and assets will be created in the current directory. - -## Commands - -### create - -Creates a new UntoldEngine game project with the specified platform configuration. - -**Usage:** -```bash -untoldengine-create create [--platform ] -``` - -**Arguments:** -- ``: Name of your game project (required) - -**Options:** -- `--platform `: Target platform (default: `macos`) - - `macos` - macOS desktop application - - `ios` - iOS application - - `iosar` - iOS with ARKit support - - `visionos` - visionOS (Apple Vision Pro) - -**Examples:** - -Create a macOS game: -```bash -mkdir SpaceShooter && cd SpaceShooter -untoldengine-create create SpaceShooter -``` - -Create an iOS game: -```bash -mkdir MobileRPG && cd MobileRPG -untoldengine-create create MobileRPG --platform ios -``` - -Create an AR game for iOS: -```bash -mkdir ARAdventure && cd ARAdventure -untoldengine-create create ARAdventure --platform iosar -``` - -Create a visionOS game: -```bash -mkdir VisionGame && cd VisionGame -untoldengine-create create VisionGame --platform visionos -``` - -**What it does:** - -1. Creates the complete project structure in the current directory -2. Generates an Xcode project using XcodeGen -3. Sets up the game asset directories at `/Sources//GameData/` -4. Configures the UntoldEngine to load assets from the GameData directory -5. Creates platform-specific configuration files - -### update - -Updates the game data directory structure for an existing project. - -**Usage:** -```bash -untoldengine-create update -``` - -**Arguments:** -- ``: Name of the existing project (required) - -**What it does:** - -Re-creates the GameData folder structure at `/Sources//GameData/` with all standard asset directories: -- Models/ -- Scenes/ -- Scripts/ -- Animations/ -- Gaussians/ -- Shaders/ -- Textures/ - -**Example:** - -```bash -cd MyGame -untoldengine-create update MyGame -``` - -**When to use:** -- After accidentally deleting asset folders -- When you need to reset the folder structure -- To ensure all standard directories exist - -## Platform Options - -### macOS -**Command:** `--platform macos` (default) - -**Features:** -- Full desktop application -- Window management -- Keyboard and mouse input -- High performance rendering - -**Use for:** -- Desktop games -- Development and testing -- Maximum performance applications - -### iOS -**Command:** `--platform ios` - -**Features:** -- iPhone and iPad support -- Touch input -- Accelerometer support -- App Store distribution - -**Use for:** -- Mobile games -- Touch-based experiences -- Wide distribution via App Store - -### iOS AR (ARKit) -**Command:** `--platform iosar` - -**Features:** -- All iOS features -- ARKit integration -- World tracking -- Plane detection -- Face tracking - -**Use for:** -- Augmented reality games -- AR experiences -- Real-world interactive applications - -### visionOS -**Command:** `--platform visionos` - -**Features:** -- Apple Vision Pro support -- Spatial computing -- 3D UI elements -- Hand tracking -- Eye tracking - -**Use for:** -- Immersive 3D experiences -- Spatial applications -- Next-generation mixed reality - -## Project Structure - -After running `untoldengine-create create MyGame`, your project structure will look like: - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Xcode project (generated) - ├── project.yml # XcodeGen configuration - ├── README.md # Project documentation - └── Sources/ - └── MyGame/ - ├── GameData/ # Game assets location - │ ├── Models/ # 3D models (.usdz, etc.) - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # Game logic scripts - │ ├── Animations/ # Animation data - │ ├── Gaussians/ # Gaussian splatting data - │ ├── Shaders/ # Custom shaders - │ └── Textures/ # Image files - ├── AppDelegate.swift # Application entry point - ├── GameViewController.swift # Main game controller - └── Info.plist # App configuration -``` - -### Important Directories - -**GameData/**: The single location for all game assets -- Path: `MyGame/Sources/MyGame/GameData/` -- The UntoldEngine is configured to load all assets from this location -- Copy your game assets (models, textures, etc.) here -- Subdirectories are organized by asset type - -**Sources/**: Swift source code -- Contains your game's Swift files -- Platform-specific delegates and controllers -- Can add additional Swift files here - -### Adding Assets - -To add assets to your game, copy them into the appropriate GameData subdirectory: - -```bash -# Add a 3D model -cp ~/Downloads/spaceship.usdz MyGame/Sources/MyGame/GameData/Models/ - -# Add textures -cp ~/Downloads/texture_*.png MyGame/Sources/MyGame/GameData/Textures/ - -# Add a scene -cp ~/Downloads/level1.json MyGame/Sources/MyGame/GameData/Scenes/ -``` - -The engine will automatically find assets in these locations. - -## Workflow Examples - -### Typical Development Workflow - -```bash -# 1. Create project directory -mkdir MyAwesomeGame -cd MyAwesomeGame - -# 2. Create the game project -untoldengine-create create MyAwesomeGame --platform ios - -# 3. Add your game assets -cp -r ~/GameAssets/Models/* MyAwesomeGame/Sources/MyAwesomeGame/GameData/Models/ -cp -r ~/GameAssets/Textures/* MyAwesomeGame/Sources/MyAwesomeGame/GameData/Textures/ - -# 4. Open in Xcode and start developing -open MyAwesomeGame/MyAwesomeGame.xcodeproj -``` - -### Starting from Scratch - -```bash -# Create a new game from scratch -mkdir PuzzleGame && cd PuzzleGame -untoldengine-create create PuzzleGame --platform macos - -# The GameData directory is ready for your assets -ls -la PuzzleGame/Sources/PuzzleGame/GameData/ -# Models/ Scenes/ Scripts/ Animations/ Gaussians/ Shaders/ Textures/ -``` - -### Multi-Platform Development - -You might want to test the same game on different platforms: - -```bash -# Create macOS version for development -mkdir MyGame-macOS && cd MyGame-macOS -untoldengine-create create MyGame --platform macos - -# Create iOS version in a separate directory -cd .. -mkdir MyGame-iOS && cd MyGame-iOS -untoldengine-create create MyGame --platform ios - -# Copy assets between projects -cp -r ../MyGame-macOS/MyGame/Sources/MyGame/GameData/* \ - MyGame/Sources/MyGame/GameData/ -``` - -### Resetting Asset Structure - -If you accidentally delete asset folders or need to reset: - -```bash -cd MyGame -untoldengine-create update MyGame -# GameData structure is recreated -``` - -## Troubleshooting - -### Command Not Found - -**Problem:** `untoldengine-create: command not found` - -**Solution:** -1. Ensure you ran the installation script: - ```bash - cd /path/to/UntoldEngine - ./Scripts/install-create.sh - ``` - -2. Check if the tool exists: - ```bash - ls -l /usr/local/bin/untoldengine-create - ``` - -3. If it exists but still not found, add `/usr/local/bin` to your PATH: - ```bash - echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.zshrc - source ~/.zshrc - ``` - -### XcodeGen Failed - -**Problem:** Error message about XcodeGen failing - -**Solution:** -1. Ensure XcodeGen is installed: - ```bash - brew install xcodegen - ``` - -2. Verify XcodeGen works: - ```bash - xcodegen --version - ``` - -3. Check the project.yml file for syntax errors: - ```bash - cat path/to/project/project.yml - ``` - -### Build System Errors - -**Problem:** Error message about BuildSystem failure - -**Solution:** -1. Verify the UntoldEngine framework is properly built -2. Check that you're running the command from the correct directory -3. Ensure sufficient disk space for the project - -### Permission Denied - -**Problem:** Permission errors during installation - -**Solution:** -Use sudo for installation: -```bash -sudo ./Scripts/install-create.sh -``` - -Or install to a user directory (advanced): -```bash -# Edit install-create.sh to use a different install location -# Change INSTALL_PATH to ~/bin/untoldengine-create -``` - -### Xcode Project Won't Open - -**Problem:** Generated .xcodeproj file won't open - -**Solution:** -1. Regenerate the project: - ```bash - cd path/to/project - xcodegen generate - ``` - -2. Check Xcode version compatibility: - ```bash - xcodebuild -version - ``` - (Requires Xcode 15.0+) - -3. Verify project.yml is valid: - ```bash - cat project.yml - ``` - -### Assets Not Loading - -**Problem:** Game can't find assets at runtime - -**Solution:** -1. Verify assets are in the correct location: - ```bash - ls -la MyGame/Sources/MyGame/GameData/ - ``` - -2. Ensure assets are in the right subdirectories: - - Models go in `GameData/Models/` - - Textures go in `GameData/Textures/` - - etc. - -3. Check asset file names and extensions are correct - -4. Verify the GameData directory structure by running update: - ```bash - untoldengine-create update MyGame - ``` - -## Advanced Usage - -### Scripting and Automation - -The CLI tool is designed to work well in scripts: - -```bash -#!/bin/bash -# create-all-platforms.sh - -PROJECT_NAME="MyGame" - -for PLATFORM in macos ios iosar visionos; do - DIR="${PROJECT_NAME}-${PLATFORM}" - mkdir -p "$DIR" && cd "$DIR" - untoldengine-create create "$PROJECT_NAME" --platform "$PLATFORM" - cd .. -done -``` - -### CI/CD Integration - -Example GitHub Actions workflow: - -```yaml -name: Create Game Projects -on: [push] - -jobs: - create-projects: - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - - name: Install CLI tool - run: | - cd UntoldEngine - ./Scripts/install-create.sh - - - name: Create projects - run: | - mkdir -p builds/MyGame-iOS && cd builds/MyGame-iOS - untoldengine-create create MyGame --platform ios -``` - -### Environment Variables - -While the CLI doesn't currently use environment variables, you can use them in your workflow: - -```bash -export GAME_NAME="MyGame" -export PLATFORM="ios" - -mkdir "$GAME_NAME" && cd "$GAME_NAME" -untoldengine-create create "$GAME_NAME" --platform "$PLATFORM" -``` - -## Getting Help - -For additional help: - -```bash -# General help -untoldengine-create --help - -# Command-specific help -untoldengine-create create --help -untoldengine-create update --help -``` - -## Feedback and Issues - -Found a bug or have a feature request? Please report it on the [UntoldEngine GitHub repository](https://github.com/untoldengine/UntoldEngine/issues). diff --git a/docs/06-Reference/03-SpatialDebugger.md b/docs/API/SpatialDebugger.md similarity index 100% rename from docs/06-Reference/03-SpatialDebugger.md rename to docs/API/SpatialDebugger.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingAnimationSystem.md b/docs/API/UsingAnimationSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingAnimationSystem.md rename to docs/API/UsingAnimationSystem.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingAsyncLoading.md b/docs/API/UsingAsyncLoading.md similarity index 75% rename from docs/04-Engine Development/03-Engine Systems/UsingAsyncLoading.md rename to docs/API/UsingAsyncLoading.md index a2a308f5c..27bbdf0bf 100644 --- a/docs/04-Engine Development/03-Engine Systems/UsingAsyncLoading.md +++ b/docs/API/UsingAsyncLoading.md @@ -48,13 +48,14 @@ You mainly need `setSceneReady(...)` for custom workflows outside those built-in setSceneReady(false) let root = createEntity() -setEntityMeshAsync(entityId: root, filename: "large_model", withExtension: "usdz") { success in - if success { - // custom post-load work: hierarchy edits, batching, metadata pass, etc. - setSceneReady(true) - } else { - setSceneReady(false) +setEntityMeshAsync(entityId: root, filename: "large_model", withExtension: "usdz") { isOutOfCore in + if isOutOfCore { + // Out-of-core: enable streaming so stubs start uploading + GeometryStreamingSystem.shared.enabled = true + enableStreaming(entityId: root, streamingRadius: 200, unloadRadius: 350, priority: 10) } + // custom post-load work: hierarchy edits, batching, metadata pass, etc. + setSceneReady(true) } ``` @@ -89,6 +90,11 @@ setEntityMeshAsync( ### With Completion Callback +The completion Bool indicates how the asset was routed: + +- `true` — asset was registered as out-of-core stubs; `GeometryStreamingSystem` must be enabled for anything to render. +- `false` — asset used the immediate path (all meshes GPU-resident) **or** loading failed (entity has a fallback cube). + ```swift let entityId = createEntity() @@ -96,15 +102,60 @@ setEntityMeshAsync( entityId: entityId, filename: "large_model", withExtension: "usdz" -) { success in - if success { - print("✅ Model loaded successfully!") +) { isOutOfCore in + if isOutOfCore { + // Asset was registered as streaming stubs — enable the system to start uploads. + GeometryStreamingSystem.shared.enabled = true + enableStreaming(entityId: entityId, streamingRadius: 200, unloadRadius: 350, priority: 10) } else { - print("❌ Failed to load - fallback cube used") + print("Model loaded immediately (small asset or error fallback)") } } ``` +### Controlling the Loading Path (`streamingPolicy`) + +By default the engine automatically selects the loading path using `AssetProfiler`. You can override this with the `streamingPolicy` parameter: + +```swift +// Always use the stub path — good for assets you know are large +setEntityMeshAsync( + entityId: entityId, + filename: "huge_city", + withExtension: "usdz", + streamingPolicy: .outOfCore +) + +// Always upload immediately — good for small, always-visible assets +setEntityMeshAsync( + entityId: entityId, + filename: "small_prop", + withExtension: "usdz", + streamingPolicy: .immediate +) + +// Let the engine decide (default — recommended for most cases) +setEntityMeshAsync( + entityId: entityId, + filename: "model", + withExtension: "usdz", + streamingPolicy: .auto +) +``` + +| Policy | Behavior | +|--------|----------| +| `.auto` (default) | `AssetProfiler` estimates geometry and texture bytes vs. the platform budget and independently selects `.streaming` or `.eager` for each domain | +| `.outOfCore` | Always stubs + geometry streaming; completion returns `true` | +| `.immediate` | Always direct GPU upload; completion returns `false` | + +For `.auto`, the engine logs the classification so you can see exactly why each decision was made: + +``` +[AssetProfiler] 'dungeon3' (2.1 MB) → mixed | geo ~2.9 MB, tex ~6.2 MB | budget: 1024 MB | meshes: 410 +[AssetProfiler] Policy → geometry: streaming, texture: eager (source: auto) +``` + ### Load Entire Scene Asynchronously ```swift diff --git a/docs/04-Engine Development/03-Engine Systems/UsingCameraSystem.md b/docs/API/UsingCameraSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingCameraSystem.md rename to docs/API/UsingCameraSystem.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingGaussianSystem.md b/docs/API/UsingGaussianSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingGaussianSystem.md rename to docs/API/UsingGaussianSystem.md diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingGeometryStreamingSystem.md b/docs/API/UsingGeometryStreamingSystem.md similarity index 64% rename from website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingGeometryStreamingSystem.md rename to docs/API/UsingGeometryStreamingSystem.md index a3304a48e..6b73ab2b8 100644 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingGeometryStreamingSystem.md +++ b/docs/API/UsingGeometryStreamingSystem.md @@ -34,20 +34,48 @@ This creates a "bubble" of loaded geometry around the camera that moves with it ## Basic Usage -Here's a simple example of enabling streaming for a single entity: +### Immediate-path assets (small files, `streamingPolicy: .immediate`) + +When the engine uses the immediate path, all meshes are GPU-resident by the time the completion fires. Call `enableStreaming` inside the callback to attach streaming radii for distance-based load/unload management: ```swift -private func setupStreaming(){ +private func setupStreaming() { let stadium = createEntity() - setEntityMeshAsync(entityId: stadium, filename: "stadium", withExtension: "usdz") { success in - if success { - - print("Scene loaded successfully") + setEntityMeshAsync(entityId: stadium, filename: "stadium", withExtension: "usdz") { isOutOfCore in + guard !isOutOfCore else { return } // handled below + + print("Scene loaded — enabling streaming") + enableStreaming( + entityId: stadium, + streamingRadius: 250.0, + unloadRadius: 350.0, + priority: 10 + ) + GeometryStreamingSystem.shared.enabled = true + } +} +``` +### Out-of-core assets (large files, many objects, or `streamingPolicy: .outOfCore`) + +Large assets are registered as stub entities with no GPU allocation. The completion callback fires with `isOutOfCore: true` as soon as all stubs are registered — before any GPU work occurs. You **must** enable `GeometryStreamingSystem` for anything to render: + +```swift +private func setupLargeAssetStreaming() { + let city = createEntity() + setEntityMeshAsync( + entityId: city, + filename: "city_block", + withExtension: "usdz" + ) { isOutOfCore in + if isOutOfCore { + // Enable the streaming system — stubs start uploading as camera approaches. + GeometryStreamingSystem.shared.enabled = true + // Set real streaming radii (replaces the internal Float.greatestFiniteMagnitude placeholders). enableStreaming( - entityId: stadium, - streamingRadius: 250.0, // Load when within 250 units - unloadRadius: 350.0, // Unload when beyond 350 units + entityId: city, + streamingRadius: 200.0, + unloadRadius: 350.0, priority: 10 ) } @@ -55,11 +83,14 @@ private func setupStreaming(){ } ``` +The engine automatically routes assets to the out-of-core path when they exceed the size threshold (default 50 MB) or object-count threshold (default 50 objects). Use `streamingPolicy: .outOfCore` to force the path regardless of file size. + ### Important Notes 1. **Load mesh first**: Always call `setEntityMeshAsync()` before enabling streaming -2. **Use completion callback**: Enable streaming inside the completion callback to ensure the mesh is loaded -3. **Async loading**: The `setEntityMeshAsync()` function loads the mesh asynchronously, preventing frame drops +2. **Use the completion Bool**: `true` = out-of-core stubs registered; `false` = immediate path (already GPU-resident) +3. **Enable the system and call `enableStreaming`**: Both are required for out-of-core assets. `GeometryStreamingSystem.shared.enabled = true` starts the upload loop; `enableStreaming(entityId: root, ...)` propagates real streaming radii to all child stubs so distance-based load/unload works correctly +4. **Async loading**: The `setEntityMeshAsync()` function loads the mesh asynchronously, preventing frame drops ## Parameters Explained @@ -136,9 +167,11 @@ See the [Combining LOD, Batching, and Streaming](./UsingLOD-Batching-Streaming.m ## Common Issues ### Objects Not Loading +- **Out-of-core assets**: Confirm `GeometryStreamingSystem.shared.enabled = true` is set after load — stubs never upload if the system is disabled - Ensure `streamingRadius` is large enough for your camera's viewing distance -- Check that the mesh was loaded successfully in the completion callback +- Check that the completion callback received `isOutOfCore: true` (out-of-core) or `false` (immediate) and handled each case - Verify the entity has been positioned in the scene +- After loading a second asset, re-enable the streaming system if you disabled it before loading (setting `enabled = false` does not re-enable automatically) ### Geometry "Popping" In and Out - Increase the buffer between `streamingRadius` and `unloadRadius` diff --git a/docs/04-Engine Development/03-Engine Systems/UsingInputSystem.md b/docs/API/UsingInputSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingInputSystem.md rename to docs/API/UsingInputSystem.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingLOD-Batching-Streaming.md b/docs/API/UsingLOD-Batching-Streaming.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingLOD-Batching-Streaming.md rename to docs/API/UsingLOD-Batching-Streaming.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingLODSystem.md b/docs/API/UsingLODSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingLODSystem.md rename to docs/API/UsingLODSystem.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingLightingSystem.md b/docs/API/UsingLightingSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingLightingSystem.md rename to docs/API/UsingLightingSystem.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingMaterials.md b/docs/API/UsingMaterials.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingMaterials.md rename to docs/API/UsingMaterials.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingPhysicsSystem.md b/docs/API/UsingPhysicsSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingPhysicsSystem.md rename to docs/API/UsingPhysicsSystem.md diff --git a/docs/API/UsingProfiler.md b/docs/API/UsingProfiler.md new file mode 100644 index 000000000..7a410fa35 --- /dev/null +++ b/docs/API/UsingProfiler.md @@ -0,0 +1,130 @@ +--- +id: profiler +title: Profiler +sidebar_position: 20 +--- + +# Profiler + +UntoldEngine profiling has two layers that are meant to be used together: + +1. **Structured metrics** (stable numbers for trends and regressions) +2. **Category logs** (on-demand narrative traces for deep debugging) + +Use structured metrics as the source of truth, then enable category logs only when you need extra context. + +## Quick Start + +Enable the profiler at runtime: + +```swift +enableEngineMetrics = true +``` + +Or via environment variable: + +```bash +export UNTOLD_METRICS=1 +./YourApp +``` + +Enable periodic frame stats logging: + +```swift +setEngineStatsLogging( + enabled: true, + profile: .compact, // or .verbose + intervalSeconds: 1.0 +) +``` + +Read profiler snapshots programmatically: + +```swift +let metrics = EngineProfiler.shared.snapshot() +print("CPU mean: \(metrics.cpuFrame.meanMs) ms") +print("GPU mean: \(metrics.gpuCommandBuffer.meanMs) ms") + +let frameStats = getEngineStatsSnapshot() +print("Frame: \(frameStats.frameIndex)") +print("Update: \(frameStats.timing.updateMs) ms") +print("Render: \(frameStats.timing.renderTotalMs) ms") +``` + +## OOC And Asset Triage Mode + +High-volume instrumentation categories are disabled by default: + +- `OOCTiming` +- `OOCStatus` +- `AssetLoader` + +Enable them when diagnosing OOC/loader behavior: + +```swift +// Keep structured profiler metrics on +enableEngineMetrics = true +setEngineStatsLogging(enabled: true, profile: .compact, intervalSeconds: 1.0) + +// Add focused trace logs +Logger.enable(category: .oocStatus) // OutOfCore lifecycle/status +Logger.enable(category: .oocTiming) // OOC timing detail +Logger.enable(category: .assetLoader) // progressive loader parse/upload +``` + +Disable after capture: + +```swift +Logger.disable(category: .oocTiming) +Logger.disable(category: .oocStatus) +Logger.disable(category: .assetLoader) +``` + +## Instruments Workflow + +When metrics are enabled, the engine emits signpost scopes: + +- `Frame` +- `Update` +- `RenderPrep` +- `Encode` +- `Submit` + +To inspect timeline data: + +1. Open Instruments +2. Choose **Points of Interest** +3. Filter subsystem to `com.untoldengine.profiling` +4. Run the app with `enableEngineMetrics = true` (or `UNTOLD_METRICS=1`) + +## Build Configuration Notes + +- `EngineProfiler` is available in all configs, but disabled by default until enabled at runtime. +- `EngineStats` collection is compiled in debug by default (`ENGINE_STATS_ENABLED`). +- For release profiling with `EngineStats`, build with `-DENGINE_STATS_ENABLED`. + +If `ENGINE_STATS_ENABLED` is not compiled in, `getEngineStatsSnapshot()` returns default values and `setEngineStatsLogging(...)` is effectively a no-op. + +## Category Toggle Notes + +- Category filtering applies to `Logger.log(...)` debug/info trace paths. +- Warnings and errors still emit regardless of category state. +- Logger messages are lazily evaluated, so disabled categories avoid message-building cost. + +## Debug Helper (DEBUG Only) + +```swift +#if DEBUG +let metricsLogger = MetricsDebugLogger() +metricsLogger.logIfNeeded() // throttle-prints approximately once per second +#endif +``` + +## Integrated Systems + +Profiler hooks are already integrated into: + +- `UntoldEngine.swift` (`runFrame`) +- `RenderingSystem.swift` (`UpdateRenderingSystem`) +- `UntoldEngineXR.swift` (`executeXRSystemPass`) +- `UntoldEngineAR.swift` (`draw`) diff --git a/docs/04-Engine Development/03-Engine Systems/UsingRegistrationSystem.md b/docs/API/UsingRegistrationSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingRegistrationSystem.md rename to docs/API/UsingRegistrationSystem.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingRenderingSystem.md b/docs/API/UsingRenderingSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingRenderingSystem.md rename to docs/API/UsingRenderingSystem.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingScenegraph.md b/docs/API/UsingScenegraph.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingScenegraph.md rename to docs/API/UsingScenegraph.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingSpatialInput.md b/docs/API/UsingSpatialInput.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingSpatialInput.md rename to docs/API/UsingSpatialInput.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingStaticBatchingSystem.md b/docs/API/UsingStaticBatchingSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingStaticBatchingSystem.md rename to docs/API/UsingStaticBatchingSystem.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingSteeringSystem.md b/docs/API/UsingSteeringSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingSteeringSystem.md rename to docs/API/UsingSteeringSystem.md diff --git a/docs/04-Engine Development/03-Engine Systems/UsingTransformSystem.md b/docs/API/UsingTransformSystem.md similarity index 100% rename from docs/04-Engine Development/03-Engine Systems/UsingTransformSystem.md rename to docs/API/UsingTransformSystem.md diff --git a/docs/Architecture/assetProfiler.md b/docs/Architecture/assetProfiler.md new file mode 100644 index 000000000..a1476105d --- /dev/null +++ b/docs/Architecture/assetProfiler.md @@ -0,0 +1,209 @@ +# Asset Profiler + +## Purpose + +`AssetProfiler` is a lightweight classification layer that runs between `Mesh.parseAssetAsync()` and ECS entity creation. It analyzes a parsed asset's composition — geometry bytes, texture bytes, mesh count — and recommends an `AssetLoadingPolicy` that selects the most appropriate residency strategy for each memory domain independently. + +Its job is to answer: **given this asset and this platform's memory budget, should geometry stream or load eagerly, and should textures stream or load eagerly?** + +--- + +## Where It Fits + +``` +setEntityMeshAsync(.auto) + │ + ├─ Mesh.parseAssetAsync() ← CPU-only parse, no GPU allocation + │ └─ ProgressiveAssetData ← MDLMesh objects in CPU RAM + │ + ├─ AssetProfiler.profile() ← analyze ProgressiveAssetData + │ └─ AssetProfile ← geo bytes, tex bytes, mesh count, character + │ + ├─ AssetProfiler.classifyPolicy() ← compare profile against platform budget + │ └─ AssetLoadingPolicy ← geometryPolicy + texturePolicy + │ + └─ Routing decision + geometryPolicy == .streaming → out-of-core stubs path + geometryPolicy == .eager → immediate GPU upload path +``` + +The profiler runs entirely on the CPU in the async registration task. No GPU resources are allocated during classification. + +--- + +## AssetProfile + +```swift +struct AssetProfile { + let totalFileBytes: Int + let estimatedGeometryBytes: Int // vertex + index bytes, summed across all MDLMesh objects + let estimatedTextureBytes: Int // estimated GPU footprint after decompression + let meshCount: Int + let materialCount: Int + let largestSingleMeshBytes: Int + let isEffectivelyMonolithic: Bool // meshCount <= 2 + let assetCharacter: AssetCharacter +} +``` + +### AssetCharacter + +| Value | Meaning | +|---|---| +| `.textureDominated` | Textures > 75% of combined estimate. Few or small meshes with large maps. | +| `.geometryDominated` | Geometry > 75% of combined estimate. Many or large meshes with minimal textures. | +| `.mixed` | Neither domain exceeds 75%. Both contribute meaningfully. | +| `.monolithic` | ≤ 2 meshes. Streaming still prevents OOM at registration, but the mesh loads in one step rather than incrementally. | + +--- + +## Geometry Byte Estimation + +Geometry bytes are estimated using the same formula as `CPUMeshEntry.estimatedGPUBytes` — this keeps the profiler and the budget manager consistent: + +``` +vertexBytes = vertexCount × vertexDescriptor.stride (stride default: 48 bytes) +indexBytes = vertexCount × 3 × 4 (~3 indices/vertex, 4 bytes each) +meshBytes = vertexBytes + indexBytes +``` + +Summed across all `MDLMesh` leaves in `ProgressiveAssetData.topLevelObjects`. + +--- + +## Texture Byte Estimation + +Texture bytes are estimated **without decompressing any texture data**. The profiler scans `MDLMaterial` semantic slots for texture URL references and uses file sizes as a proxy: + +**For regular file URLs** (external textures): +``` +textureBytes += fileSize × 3 // PNG/JPEG decode expansion; conservative cross-format estimate +``` + +**For USDZ-embedded textures** (bracket-notation paths like `file:///scene.usdz[0/tex.png]`): +Individual zip entries cannot be statted without decompressing. A file-level heuristic is used instead: +``` +packedTextureBytes = max(0, fileSizeBytes − (geometryBytes / 10)) +textureBytes = packedTextureBytes × 3 +``` +The geometry division by 10 approximates the compression ratio of vertex/index data in the USDZ package. This overestimates for geometry-heavy assets, which is intentional — it is always safer to over-estimate texture cost (triggers streaming) than to under-estimate it (causes OOM). + +**If no texture URLs are found**, `estimatedTextureBytes` is 0 and `texturePolicy` defaults to `.eager`. + +### Scanned Material Semantics + +```swift +.baseColor, .roughness, .metallic, .bump, .emission, .opacity, .ambientOcclusion +``` + +--- + +## AssetLoadingPolicy + +```swift +struct AssetLoadingPolicy { + var geometryPolicy: GeometryResidencyPolicy // .eager or .streaming + var texturePolicy: TextureResidencyPolicy // .eager or .streaming + var source: PolicySource // .auto or .userForced +} +``` + +The two policies are independent. A texture-dominated asset with 3 meshes and 150 MB of maps gets `geometry: .eager, texture: .streaming`. A geometry-dominated city with 400 small meshes gets `geometry: .streaming, texture: .eager`. + +### Built-in Presets + +| Preset | Geometry | Texture | +|---|---|---| +| `.fullLoad` | `.eager` | `.eager` | +| `.geometryStreaming` | `.streaming` | `.eager` | +| `.textureStreaming` | `.eager` | `.streaming` | +| `.combinedStreaming` | `.streaming` | `.streaming` | + +--- + +## Classification Logic + +### Geometry Policy + +``` +if isMonolithic: + streaming if geometryBytes / budget > 0.30 + else eager + +if meshCount >= 50: + streaming ← many meshes spike GPU allocation regardless of total size + +if geometryBytes / budget > 0.30: + streaming + +else: + eager +``` + +### Texture Policy + +``` +if textureBytes / budget > 0.10 OR textureBytes > 32 MB: + streaming + +else: + eager +``` + +### Why fractions of budget, not fixed thresholds + +The same 200 MB asset routes differently depending on the device: + +| Device | Budget | Geo fraction | Geometry policy | +|---|---|---|---| +| macOS | 1 GB | 20% | `.eager` — fits comfortably | +| iOS high-end | 512 MB | 39% | `.streaming` — too large | +| iOS low-end | 256 MB | 78% | `.streaming` — far too large | +| visionOS | 512 MB | 39% | `.streaming` — too large | + +Fixed thresholds (the old `fileSizeThresholdBytes = 50 MB`) applied the same cutoff on all platforms. A 200 MB asset would always trigger streaming, even on a macOS workstation with 1 GB of headroom. + +--- + +## Log Output + +For every `.auto` classification the profiler emits two log lines: + +``` +[AssetProfiler] 'dungeon3' (2.1 MB) → mixed | geo ~2.9 MB, tex ~6.2 MB | budget: 1024 MB | meshes: 410 +[AssetProfiler] Policy → geometry: streaming, texture: eager (source: auto) +``` + +**Line 1** — profile snapshot: filename, file size, asset character, estimated geometry bytes, estimated texture bytes, platform budget, mesh count. + +**Line 2** — the chosen policy for each domain and whether it was auto-selected or user-forced. + +If geometry streaming is selected, the out-of-core log also captures the reason: + +``` +[OutOfCore] 'dungeon3': mixed asset, geo ~2.9 MB on 1024 MB budget → out-of-core stub registration (410 stubs) +``` + +--- + +## Limitations and Known Heuristics + +**Texture estimation for USDZ-embedded textures is approximate.** The `(fileSizeBytes − geometryBytes/10) × 3` heuristic overestimates for geometry-heavy assets and underestimates for assets with highly compressed textures (e.g. ASTC at 8:1). It is biased toward overestimation intentionally. + +**Monolithic assets stream but do not incrementally load.** An asset with 1 mesh and 400 MB of geometry enters the streaming path (to prevent OOM at registration) but loads its full geometry in a single upload step. There is no incremental benefit beyond registration-time safety. + +**External texture files must be accessible at classification time.** If textures are on a remote URL or behind a slow filesystem, the `stat()` calls in `estimateTextureBytes` will block the registration task. This is only a concern for non-local assets. + +**Material semantics scanned are limited to the seven standard PBR slots.** Custom material properties outside those semantics are not counted. If an asset uses non-standard semantic names, its texture estimate will be lower than the true cost. + +--- + +## Relationship to Other Systems + +| System | Relationship | +|---|---| +| `ProgressiveAssetLoader` | Provides `ProgressiveAssetData` that `AssetProfiler.profile()` analyzes | +| `MemoryBudgetManager` | Provides `meshBudget`; all thresholds are fractions of this value | +| `GeometryStreamingSystem` | Activated when `geometryPolicy == .streaming`; manages GPU residency per entity | +| `TextureStreamingSystem` | Runs on all entities with `RenderComponent` regardless of texture policy; the policy makes the intent explicit for future per-entity gating | +| `RegistrationSystem` | Calls `AssetProfiler` in the `.auto` branch of `setEntityMeshAsync`; maps the result to `useOutOfCore` | diff --git a/docs/Architecture/batchingSystem.md b/docs/Architecture/batchingSystem.md new file mode 100644 index 000000000..9804f9576 --- /dev/null +++ b/docs/Architecture/batchingSystem.md @@ -0,0 +1,145 @@ +# BatchingSystem — How It Works + +The goal is simple: instead of issuing 100 separate draw calls (one per entity), merge entities that share the same material into a single combined GPU buffer and issue **one draw call per material group**. This is called **static batching**. + +--- + +## Step 0: The World is Divided into Cells + +The 3D world is partitioned into a 3D grid of **cells**, each 32 units wide (`batchCellSize = 32.0`). Every entity is assigned to a cell based on the world-space center of its bounding box: + +``` +cellId(x, y, z) = floor(worldCenter / 32.0) +``` + +**Why cells?** Batching 100 entities scattered across a huge world into one mesh is wasteful — you'd rebuild everything when anything changes. Cells localize the damage. + +--- + +## Step 1: Entity Registration + +When your 100 entities load, each one that has a `StaticBatchComponent` gets registered: + +- **Eligibility check** (`resolveBatchCandidate`): the entity must have a `RenderComponent`, `WorldTransformComponent`, no skeleton/animation, no transparency, no gizmo/light component, and its mesh must already be resident in memory. +- If eligible → it gets assigned to a cell and added to `cellToEntities[cellId]`. +- The cell is marked **dirty** and its state becomes `renderableUnbatched`. + +--- + +## Step 2: Per-Frame Tick — The Pipeline + +Every frame, `tick()` runs through this pipeline: + +### 2a. Process Removals & Additions +Any entities that changed (LOD switch, mesh evicted/streamed in) are removed from their old cell and re-registered in their current cell. This marks the affected cells dirty. + +### 2b. Update Visibility History +The system checks which cells currently contain visible entities and records `cellLastVisibleFrame[cellId]`. This drives **visibility gating** — the system won't waste CPU rebuilding cells you can't see. + +### 2c. Promote Dirty Cells → `batchPending` +For each dirty cell in state `renderableUnbatched` or `streaming`: +- Is it visible (or recently visible within 120 frames)? +- Has it been stable for at least `quiescenceFramesBeforeBatchBuild` frames (default: 1)? + +If yes → state becomes **`batchPending`**. + +### 2d. Rebuild Dirty Cells (`rebuildDirtyCells`) + +This is the core build loop: + +1. **Apply completed background artifacts first** — results from previous frames' async builds are swapped in (up to `maxArtifactAppliesPerTick = 4` per frame). + +2. **Gather `batchPending` cells** and build rebuild candidates. For each: + - Estimate the work: count total vertices + indices + bytes across all entities in the cell. + - If a cell **exceeds the per-cell complexity guard** (>160K verts, >300K indices, >8MB), it's flagged `runtimeIneligibleCells` and stays unbatched. + - Otherwise it becomes a `CellRebuildCandidate`. + +3. **Sort candidates** by priority: + - Currently visible > recently visible > long ago visible + - Smaller estimated bytes first (lighter work first) + - Oldest dirty-since-frame first + +4. **Apply per-tick budgets**: up to 8 cells, 120K verts, 220K indices, 6MB total per tick. Once budgets are exhausted, remaining cells defer to next frame. + +5. **Snapshot build inputs** under a world mutation gate: for each selected cell, group its entities' meshes by `BatchBuildKey = (cellId, materialHash, lodIndex)`. This produces `CellBuildInput`. + +6. **Dispatch background builds** on `artifactBuildQueue` (a `.utility` DispatchQueue). The heavy work — actually merging vertex data — happens off the main thread. + +--- + +## Step 3: Building the Batch (`buildPreparedArtifact`) + +For each `CellBuildInput`, on the background thread: + +- Iterate material groups. **Skip any group with < 2 meshes** (no point batching a single mesh). +- For each group that qualifies, call `createBatchGroup`: + - Loop through all meshes in the group. + - For each mesh, extract positions, normals, UVs, tangents from the Metal buffers. + - Transform each vertex by the entity's world transform (`worldTransform.space * mesh.localSpace`). + - Re-index indices with an offset (since vertices are now concatenated into one flat array). + - Allocate new `MTLBuffer`s for the merged position/normal/UV/tangent/index data. +- The result is a `PreparedCellArtifact` containing `[BatchGroup]`. + +So 100 entities all sharing the same wood-plank material → **1 BatchGroup** with 1 merged MTLBuffer. + +> **What is an artifact?** +> An artifact is the **output package produced by a build job**: + the **input** is `CellBuildInput` (a snapshot of which entities are in a cell and how they're grouped by material), and the **artifact** (`PreparedCellArtifact`) is the finished result — the merged MTLBuffers, entity-to-batch mappings, vertex/index counts, and build time. Everything needed to install the batch into the live scene. +> + +--- + +## Step 4: Applying the Artifact + +Back on the main thread (next frame or same frame if sync mode): + +- Validate the artifact is still current (epoch + generation check — discards stale builds if the scene changed while it was building). +- Remove any existing batches for the cell (queue old GPU buffers for retirement with a 3-frame safety delay so the GPU isn't still using them). +- Append the new `BatchGroup`s to `batchGroups`. +- Update `entityToBatch[entityId]` so the renderer knows each entity is now represented by a batch. +- Reconcile streaming textures: if a texture streamed to a higher mip while the build was in flight, patch the batch's material in-place so it doesn't revert. +- Mark cell state → **`renderableBatched`**. + +--- + +## Step 5: Rendering + +The renderer checks `entityToBatch` — if an entity is in a batch, it **skips the per-entity draw call** and instead the batch groups are rendered directly. Each `BatchGroup` is one draw call with its merged buffer. 100 entities sharing one material = **1 draw call**. + +--- + +## Step 6: Retirement (Safe GPU Buffer Release) + +Old GPU buffers aren't freed immediately. They go into `retiringBatchArtifacts` with a `retireAfterFrame = currentFrame + 3`. After 3 frames, the system drops the Swift reference, allowing ARC to release the MTLBuffers — guaranteeing the GPU has finished with them. + +--- + +## Cell Lifecycle State Machine + +``` +unloaded + ↓ (entity becomes resident) +streaming + ↓ (quiescence + visibility check pass) +renderableUnbatched + ↓ (promoted, budget available) +batchPending + ↓ (build dispatched + applied) +renderableBatched + ↓ (entity removed or LOD change) +retiring → unloaded +``` + +--- + +## With 100 Entities — Concrete Example + +Suppose your 100 entities break down as: +- 60 entities: wood material, LOD 0, all in cell (0,0,0) +- 30 entities: stone material, LOD 0, cell (0,0,0) +- 10 entities: glass material (transparent) → **excluded from batching** + +Result: +- **2 BatchGroups** for cell (0,0,0): one wood, one stone +- **2 draw calls** instead of 90 (the 10 transparent ones draw individually) +- On a LOD change (say 20 wood entities switch to LOD 1), cell (0,0,0) is marked dirty → rebuild fires next eligible tick → now 3 BatchGroups (wood LOD0, wood LOD1, stone LOD0) diff --git a/docs/Architecture/geometryStreamingSystem.md b/docs/Architecture/geometryStreamingSystem.md new file mode 100644 index 000000000..89bd53259 --- /dev/null +++ b/docs/Architecture/geometryStreamingSystem.md @@ -0,0 +1,181 @@ +# GeometryStreamingSystem + +## The Setup: City Block as Streaming Entities + +Imagine your city block USDZ is broken into many individually registered entities — `building_A`, `building_B`, `streetlamp_01`, `car_01`, etc. Each has a `StreamingComponent` that stores: + +- `assetFilename` / `assetExtension` — where the mesh lives on disk +- `streamingRadius` — how close the camera must be to **load** it +- `unloadRadius` — how far before it gets **unloaded** +- `priority` — which buildings load first when slots are contested +- `state` — `.unloaded`, `.loading`, `.loaded`, or `.unloading` + +--- + +## Every Frame: `update(cameraPosition:deltaTime:)` + +The engine calls this every frame. Here's what happens: + +### 1. Throttle Check +The system normally does real work every **0.1 seconds** (`updateInterval`). Between ticks, it's a no-op. This prevents wasting CPU every single frame. + +When `lastPendingLoadBacklog > 0` (candidates are queued but all slots are busy), the effective interval drops to `burstTickInterval` (default 16 ms). This prevents a 100 ms stall between slot pickups during active loading. The tick rate returns to 100 ms once the backlog drains. + +### 2. Spatial Query via Octree (line 123) +Instead of checking all 500 city entities, it asks the `OctreeSystem`: +> "Give me every entity within 500m of the camera." + +This is the key performance trick — only nearby entities are evaluated. + +### 3. Classify Each Nearby Entity (lines 129–157) + +For each entity the octree returns, the system calculates the **distance from camera to the entity's bounding box center**, then: + +| State | Condition | Action | +|---|---|---| +| `.unloaded` | distance ≤ `streamingRadius` | → add to **load candidates** | +| `.loaded` | distance > `unloadRadius` | → add to **unload candidates** | +| `.loaded` | still in range | → stamp `lastVisibleFrame` (keep alive) | +| `.loading` / `.unloading` | — | skip, already in progress | + +### 4. Out-of-Range Loaded Entities (lines 164–183) +The octree query only covers nearby space. But what if `building_Z` was loaded and the player sprinted far away — it might not be in the octree result anymore. So the system also checks its `loadedStreamingEntities` tracking set for any loaded entity **not** in the octree result, and adds those to unload candidates if they're too far. + +--- + +## Unloading First: Free Memory Before Loading New Things (lines 191–197) + +Unload candidates are **sorted farthest-first** (most wasteful memory first). Up to `maxUnloadsPerUpdate = 12` are processed per tick to avoid frame spikes. + +`unloadMesh()` does: +1. Sets state → `.unloading` +2. Notifies `BatchingSystem` the entity is retiring +3. Cancels any in-flight load task +4. Calls `MeshResourceManager.shared.release(entityId:)` — decrements reference count on the cached mesh +5. Clears `render.mesh = []` — the GPU buffers are **not destroyed** (cache still owns them) +6. Clears LOD level meshes if applicable +7. Unregisters from `MemoryBudgetManager` +8. Sets state → `.unloaded` +9. Fires an `AssetResidencyChangedEvent(isResident: false)` + +--- + +## Loading: Async, Concurrency-Limited + +Load candidates are **sorted by priority then distance** (high priority + closest first). Only `maxConcurrentLoads = 3` can be active simultaneously. + +Before dispatching, the scheduler applies three guards in order: + +1. **CPU-entry readiness** — OOC entities whose `CPUMeshEntry` is not yet stored in `ProgressiveAssetLoader` are skipped. This prevents pre-streaming stubs from holding slots while registration is still running. +2. **Prewarm-active deferral** — entities for roots whose background texture prewarm is still running are skipped. Dispatching while the prewarm holds the per-asset texture lock would block all concurrent slots for the remaining prewarm duration. Slots stay free until `isPrewarmActive` returns `false`. +3. **Per-candidate geometry budget check** — if the candidate's estimated GPU footprint would exceed the geometry budget, `evictLRU` is called first. + +When all near-band candidates share one `assetRootEntityId`, the near-band concurrency limit expands from `nearBandMaxConcurrentLoads` to `maxConcurrentLoads`. All sub-meshes of one USDZ are treated as a single burst rather than being serialized one-at-a-time. + +`loadMesh()` does: +1. Reserves a slot in `activeLoads` (thread-safe via `NSLock`) +2. Sets state → `.loading` +3. Notifies `BatchingSystem` streaming started +4. Spawns a Swift `Task` (runs off the main thread) + +Inside the async task: +- If the entity has a `LODComponent` → calls `reloadLODEntity()` which loads all LOD levels +- Otherwise → calls `loadMeshAsync()` which goes to `MeshResourceManager` (cache-first, file fallback) +- After loading, back on the main thread via `withWorldMutationGate`: + - Assigns `render.mesh` with fresh copies of uniform buffers (critical — prevents entities sharing GPU state from overwriting each other) + - Sets state → `.loaded` + - Fires `AssetResidencyChangedEvent(isResident: true)` + - Records load in `MemoryBudgetManager` + +--- + +## LOD Path: `reloadLODEntity()` (lines 313–415) + +For LOD entities (e.g., a skyscraper with 3 detail levels), it: +1. Loads **all LOD levels** concurrently from cache/disk +2. Calculates current camera-to-entity distance +3. Picks the appropriate LOD level (highest detail that fits distance) +4. Sets `renderComponent.mesh` to that LOD's mesh data +5. Marks `lodComponent.currentLOD` + +--- + +## Memory Pressure: Texture Relief First, Geometry Eviction Last + +The engine uses two independent memory pressure signals and responds to them in priority order: + +| Pressure signal | Method | Meaning | +|---|---|---| +| Combined (mesh + texture) | `shouldEvict()` | Total GPU allocation ≥ 85% of `meshBudget` | +| Geometry only | `shouldEvictGeometry()` | Mesh allocations alone ≥ 85% of `meshBudget` | + +**Why two signals?** `TextureStreamingSystem` upgrades visible textures to higher resolutions after meshes load. Those upgrades increase `totalTextureMemory` in `MemoryBudgetManager`. If the load gate used the combined signal, texture upgrades on already-loaded meshes would silently prevent new mesh loads — even when the geometry-only footprint is well within budget. The split ensures texture pressure cannot block geometry loading. + +### Step 1 — Texture downgrade relief + +Before considering geometry eviction, the system sheds texture quality on the farthest loaded entities: + +``` +if combined pressure is high AND geometry pressure is NOT high: + TextureStreamingSystem.shedTextureMemory(maxEntities: 4) + → no geometry eviction; texture relief only +``` + +`shedTextureMemory` forces the farthest entities in the `upgradedEntities` set to `minimumTextureDimension` immediately, bypassing the normal distance-band schedule. A distant wall dropping from 1024 px to 256 px is far less noticeable than a missing mesh. + +### Step 2 — Geometry eviction (last resort) + +Only triggered when geometry memory itself hits the high-water mark: + +``` +if geometry pressure is high: + TextureStreamingSystem.shedTextureMemory(maxEntities: 8) ← try texture relief first + evictLRU(cameraPosition:) ← then fall back to geometry eviction +``` + +`evictLRU`: +1. First evicts unused cached meshes (`MeshResourceManager.evictUnused()`) +2. Collects all loaded streaming entities +3. Sorts by value score (far + large = first to go; see value-score eviction in the out-of-core walkthrough) +4. Unloads them one by one until **geometry-only** pressure clears (loop breaks on `shouldEvictGeometry()`, not the combined signal) +5. Skips entities that are both visible and within `visibleEvictionProtectionRadius` (30 m default) + +--- + +## City Block Scenario: Summary Flow + +``` +Player spawns at corner of city block +│ +├─ Frame 1 tick: Octree finds 8 nearby buildings +│ ├─ 5 are unloaded + within streamingRadius → load candidates +│ └─ 3 are loading already → skip +│ +├─ Up to 3 async loads fire simultaneously +│ ├─ building_A: cache miss → read from USDZ file +│ ├─ building_B: cache hit → instant +│ └─ building_C: cache miss → read from USDZ file +│ +├─ Player walks forward → building_K enters range +│ └─ Queued in load candidates (backlog until a slot frees) +│ +├─ Player runs past old buildings → building_A now > unloadRadius +│ └─ render.mesh cleared, reference released, memory freed +│ +└─ Memory pressure → LRU eviction kicks in + └─ building_E (not visible, oldest lastVisibleFrame) → evicted +``` + +The key design decisions here are: +- **Octree spatial query** prevents O(n) entity iteration every tick +- **Concurrency cap (3)** prevents GPU/IO saturation during fast movement +- **Adaptive tick rate** — 16 ms during backlog, 100 ms steady-state — prevents stalls between slot pickups without wasting CPU when idle +- **Single-root burst detection** — when all near-band candidates are sub-meshes of one asset, concurrency expands to the global cap so the asset loads in parallel rather than one mesh at a time +- **Background texture prewarm** — `loadTextures()` runs at registration time so the first-upload path is a no-op and lock wait ≈ 0 +- **Prewarm-active deferral** — dispatch is held until the prewarm releases the texture lock, keeping all slots free for the burst +- **Narrowed texture lock scope** — the per-asset lock covers only `ensureTexturesLoaded`; `makeMeshesFromCPUBuffers` runs outside the lock so all slots upload in parallel +- **CPU-entry readiness guard** — stubs registered before their CPU data is ready are skipped rather than wasting a slot +- **Unload-before-load** ordering ensures you free memory before consuming more +- **Cache ownership** means unloading just clears references, actual GPU memory is reused if the same mesh comes back into range +- **Geometry-only load gate** prevents texture upgrades from blocking mesh loads — each domain is budgeted independently +- **Texture relief before geometry eviction** means a drop in distant texture resolution is always preferred over a missing mesh diff --git a/docs/Architecture/lodSystem.md b/docs/Architecture/lodSystem.md new file mode 100644 index 000000000..de7eaa7ef --- /dev/null +++ b/docs/Architecture/lodSystem.md @@ -0,0 +1,144 @@ +## LODSystem: City Block with 500 Buildings, 3 LODs Each + +### Setup (before the system runs) + +Each building entity has a `LODComponent` with an array of `LODLevel` entries sorted by detail: + +``` +Building Entity + LODComponent.lodLevels[0] → LOD0: highDetailMesh, maxDistance: 50.0 + LODComponent.lodLevels[1] → LOD1: medDetailMesh, maxDistance: 100.0 + LODComponent.lodLevels[2] → LOD2: lowDetailMesh, maxDistance: 200.0 +``` + +Each `LODLevel` tracks its own `residencyState` (`.resident`, `.loading`, `.notResident`, `.unknown`) and a `url` so the streaming system knows where to reload it from. + +--- + +### Every Frame: `LODSystem.update()` + +The system runs once per frame. Here's what happens for all 500 buildings: + +**Step 1 — Get camera position** +``` +CameraSystem → activeCamera → CameraComponent.localPosition +``` + +**Step 2 — Query all LOD entities** +```swift +queryEntitiesWithComponentIds([LODComponent, WorldTransformComponent]) +``` +Returns all 500 building entities in one shot. + +**Step 3 — For each building: `updateEntityLOD()`** + +This is the core loop. Three sub-steps per building: + +--- + +#### 3a. `calculateDistance()` + +Takes the building's local AABB bounding box, finds its center, transforms it to world space via `WorldTransformComponent.space`, then computes the straight-line distance to the camera. + +``` +distance = |cameraPosition - worldCenter| +``` + +--- + +#### 3b. `selectLODLevel()` + +Applies `lodBias` to the distance (default `1.0`, so no change), then walks through `lodLevels` in order, comparing against each level's `maxDistance`: + +``` +adjustedDistance = distance * lodBias + +If adjustedDistance ≤ 50 → desiredLOD = 0 (high detail) +If adjustedDistance ≤ 100 → desiredLOD = 1 (medium) +If adjustedDistance ≤ 200 → desiredLOD = 2 (low) +Beyond all thresholds → desiredLOD = 2 (lowest available) +``` + +**Hysteresis:** When switching to a *finer* LOD (e.g. camera approaching, LOD2→LOD1), the threshold is tightened by `5.0` units. This prevents flickering when the camera hovers right at a boundary. Switching to *coarser* LODs has no penalty — it happens immediately. + +--- + +#### 3c. `resolveActualLOD()` + +The desired LOD may not be resident yet (e.g. it's still streaming in). So: + +``` +Is desiredLOD mesh resident? + YES → use it (isUsingFallback = false) + NO → findFallbackLOD(): + Try coarser LODs first (LOD2, LOD3...) + Then try finer LODs (LOD0...) + If nothing → stay at currentLOD +``` + +This means a building 30m away that wants LOD0 but hasn't finished loading will temporarily show LOD1 or LOD2 — always something visible, never a pop-in hole. + +--- + +#### 3d. `applyLOD()` + +This is the write step. Runs inside `withWorldMutationGate` to be thread-safe. + +``` +If newLOD == currentLOD and no transition in progress → skip (no-op) + +Otherwise: + - Update lodComponent.currentLOD = newLOD + - Copy lodLevels[newLOD].mesh → renderComponent.mesh + - Generate meshAssetID: "_LOD" + - Store in lodComponent.activeMeshAssetID (used by batching system) + - If LOD actually changed → emit EntityLODChangedEvent to SystemEventBus +``` + +The `renderComponent.mesh` swap is the handoff to the renderer — whatever mesh array sits there is what gets drawn next frame. + +--- + +### What the 500-Building Frame Looks Like + +Given a camera standing near one end of the block: + +| Distance | Buildings | Desired LOD | Typical Outcome | +|---|---|---|---| +| 0–50m | ~20 | LOD0 | High detail meshes | +| 50–100m | ~80 | LOD1 | Medium meshes | +| 100–200m | ~150 | LOD2 | Low meshes | +| 200m+ | ~250 | LOD2 (last) | Lowest available | + +The system processes all 500 in sequence, but buildings where LOD hasn't changed are early-exited with no mutation (the `newLOD == previousLODIndex` guard). In a stable scene, the vast majority of buildings hit that fast path. + +--- + +### Key Design Observations + +- **Fallback-first streaming:** The system never waits for a mesh to load — it always degrades gracefully to whatever is resident. +- **`activeMeshAssetID`** is the bridge to the batching system. When a LOD switches, the new asset ID tells the batcher to move that entity into a different batch group. +- **`EntityLODChangedEvent`** on `SystemEventBus` is how downstream systems (geometry streaming, batching) learn that a switch happened — they react to the event rather than polling. +- **Fade transitions** exist in the code (`transitionProgress`, `previousLOD`) but `enableFadeTransitions` defaults to `false`, so currently all switches are instant. + +--- + +### LOD + Out-of-Core Integration + +Prior to this integration, LOD and OOC were mutually exclusive: assets that qualified for out-of-core streaming would bypass LOD group detection, causing each `LOD0`/`LOD1`/`LOD2` object to become an independent stub entity. + +**How it works now:** + +`setEntityMeshAsync` runs LOD group detection *before* the OOC branching decision. When the asset both qualifies for OOC streaming *and* contains LOD groups, the **LOD+OOC path** runs: + +1. **Registration** — one entity per LOD group (not one per MDLObject). Each entity gets a `LODComponent` with stub `LODLevel`s (empty mesh, `.notResident`) and a `StreamingComponent(.unloaded)`. + +2. **CPU registry** — `ProgressiveAssetLoader.cpuLODRegistry[groupEntityId][lodIndex]` stores a `CPUMeshEntry` for every LOD level. The MDLAsset is retained so CPU buffers remain valid. + +3. **GPU upload** — when `GeometryStreamingSystem` picks up the entity (`.unloaded` → in streaming range), it calls `uploadActiveLODFromCPU`. This uploads **all** LOD levels in one pass from the CPU registry, marks each `LODLevel.residencyState = .resident`, and sets `renderComponent.mesh` to the level appropriate for the current camera distance. + +4. **LOD switching after load** — `LODSystem.applyLOD` continues to work as normal: it reads from `lodComponent.lodLevels[n].mesh` (now populated) and swaps `renderComponent.mesh`. No additional streaming requests are needed — all levels are already GPU-resident. + +5. **Cold re-hydration** — if `releaseWarmAsset` was called on the root and a group entity re-enters streaming range, `rehydrateColdAsset` re-parses the USDZ, re-runs LOD detection, and rebuilds `cpuLODRegistry` entries before `uploadActiveLODFromCPU` runs. + +**Result:** the caller sets any `MeshStreamingPolicy` and LOD assets always get proper `LODComponent` wiring. The mutual exclusivity that required users to choose between OOC and LOD is eliminated. diff --git a/docs/Architecture/meshResourceManager.md b/docs/Architecture/meshResourceManager.md new file mode 100644 index 000000000..fd1ec7c66 --- /dev/null +++ b/docs/Architecture/meshResourceManager.md @@ -0,0 +1,170 @@ +# MeshResourceManager + +`MeshResourceManager` is a singleton that acts as the shared GPU memory layer for mesh assets. It caches entire USDZ files, tracks which entities use which meshes via reference counting, and evicts unused geometry under memory pressure. + +--- + +## The Core Data Model + +`MeshResourceManager` is a **singleton** (`shared`) that manages two key dictionaries: + +``` +resources: URL -> MeshResource (the cache — one entry per USDZ file) +entityToMesh: EntityID -> (URL, name) (which entity uses which mesh) +``` + +A `MeshResource` holds **all meshes** from a single USDZ file, keyed by asset name: + +``` +city_block_A.usdz -> MeshResource { + meshesByName: { "building_01": [...], "window_01": [...], "door_01": [...] } + refCountByName: { "building_01": 12, "window_01": 48, "door_01": 12 } + totalMemorySize: 42_000_000 // bytes on GPU + lastAccessFrame: 1042 +} +``` + +--- + +## Phase 1 — Loading (First Request) + +Say entity `e001` needs `"building_01"` from `city_block_A.usdz`: + +``` +loadMesh(url: city_block_A.usdz, meshName: "building_01") +``` + +**Step 1 — Cache check** (`getCachedMesh`): nothing cached yet → miss. + +**Step 2 — Single-flight gate** (`waitForExistingLoadOrBecomeLoader`): +- First caller wins: it is designated the **loader** and gets `true`. +- If 10 other entities request the same file simultaneously, they all queue up as **waiters** inside `inFlightLoadWaiters[url]`. They suspend via `CheckedContinuation` — no busy-waiting. + +**Step 3 — Parse & upload** (`Mesh.loadSceneMeshesAsync`): +- The entire USDZ is parsed once. Every mesh in the file is uploaded to GPU buffers. +- Result is a `[[Mesh]]` — one inner array per named asset. + +**Step 4 — Build the dictionary and cache it**: +```swift +meshesByName["building_01"] = [mesh1, mesh2, ...] // LODs or submeshes +meshesByName["window_01"] = [mesh3, ...] +meshesByName["door_01"] = [mesh4, ...] +totalMemorySize = 42 MB +``` +This is stored in `resources[city_block_A.usdz]`. + +**Step 5 — Wake waiters** (`finishInFlightLoad`): +- All 10 suspended callers are resumed with `false` (they don't load — file is already cached). +- Each then calls `getCachedMesh` and gets their mesh instantly. + +--- + +## Phase 2 — Reference Counting (Entity Lifecycle) + +When the streaming system decides entity `e001` will **render** `"building_01"`: + +```swift +retain(url: city_block_A.usdz, meshName: "building_01", for: e001) +``` + +This: +1. Releases any previous mesh the entity held (safety cleanup). +2. Records `entityToMesh[e001] = (city_block_A.usdz, "building_01")`. +3. Increments `refCountByName["building_01"]` from 0 → 1. + +After 500 entities are assigned across the city block: + +``` +refCountByName: { "building_01": 45, "window_01": 180, "streetlight": 60, ... } +totalRefCount = 500 +isInUse = true ← cannot be evicted +``` + +When an entity scrolls **out of view** and is culled: + +```swift +release(entityId: e099) +``` + +This removes `entityToMesh[e099]` and decrements `refCountByName["building_01"]` from 45 → 44. + +--- + +## Phase 3 — Eviction (Memory Pressure) + +Three eviction strategies exist: + +| Method | When to Use | +|---|---| +| `evict(url:)` | Force-remove one specific file (only if `refCount == 0`) | +| `evictUnused()` | Sweep all files with zero references | +| `evictToFreeMemory(targetBytes:)` | **LRU** — evict oldest-accessed files first until `targetBytes` freed | + +For a city block, `evictToFreeMemory` is most useful. Say GPU budget is exceeded by 80 MB: + +``` +candidates = resources where totalRefCount == 0 + sorted by lastAccessFrame ascending (oldest first) + +Evict city_block_C.usdz → frees 38 MB (last seen frame 200) +Evict city_block_D.usdz → frees 44 MB (last seen frame 310) +Total freed: 82 MB ✓ +``` + +For each evicted mesh, `mesh.cleanUp()` is called to free the Metal GPU buffers. + +`lastAccessFrame` is updated every time a mesh is accessed (cache hit) or retained, so actively-used files naturally survive LRU pressure. + +--- + +## Thread Safety + +All state is protected by a **concurrent `DispatchQueue`** with the readers-writers pattern: +- Reads use `accessQueue.sync` — concurrent reads are fine. +- Writes use `accessQueue.sync(flags: .barrier)` — exclusive access, blocks concurrent reads. + +This makes the manager safe to call from multiple streaming tasks loading different USDZ files in parallel. + +--- + +## Summary Flow for 500-Mesh City Block + +``` +Frame 0: Scene starts loading + → 5 USDZ files queued + → Each file: one task becomes loader, others wait + → All 5 files parsed, all meshes cached in GPU memory + +Frame 1–N: Entities stream in/out of view + → retain() as entities enter view frustum + → release() as entities leave + → refCounts track exactly how many entities use each mesh + +Memory pressure detected: + → evictToFreeMemory() walks LRU list + → Only files with refCount==0 are eligible + → GPU buffers freed via cleanUp() + → Files still in view are untouched +``` + +The key insight: **one disk parse per USDZ file**, no matter how many entities share its meshes. Reference counting prevents premature eviction, and LRU ensures the least-recently-seen geometry is freed first under memory pressure. + +--- + +## Callers + +`MeshResourceManager` is used by three systems: + +### GeometryStreamingSystem — Primary driver +Owns the full lifecycle: +- Updates `currentFrame` each tick to keep LRU timestamps fresh. +- Calls `loadMesh` + `retain` when an entity streams into view. +- Calls `release` when an entity streams out of view. +- Calls `evictUnused()` to free GPU memory. +- Reads `getStats()` for diagnostics and memory budgeting. + +### RegistrationSystem — Cache pre-warmer +Calls `cacheLoadedMeshes(url:meshArrays:)` when entities are registered into the scene, so meshes are already in the cache before the streaming system requests them. Also calls `release` when entities are unregistered. + +### UntoldEngine (Renderer) — Read-only monitoring +Reads `getStats()` only, likely for a debug overlay or performance HUD. diff --git a/docs/Architecture/outOfCore.md b/docs/Architecture/outOfCore.md new file mode 100644 index 000000000..5042be364 --- /dev/null +++ b/docs/Architecture/outOfCore.md @@ -0,0 +1,686 @@ +# Out-of-Core System Walkthrough: 500-Building City Block + +--- + +## The Cast + +Three independent systems, each with a separate job: + +| System | Runs | Job | +|--------|------|-----| +| `ProgressiveAssetLoader` | On demand | CPU registry — stores MDLMesh data, manages asset lifetime | +| `GeometryStreamingSystem` | Every 0.1 s (background) | Load/unload entities by distance; uploads from CPU registry | +| `MeshResourceManager` | On demand (fallback) | Disk cache for non-stub entities | + +`ProgressiveAssetLoader` no longer processes per-frame jobs. Its sole responsibility is the CPU registry: storing `CPUMeshEntry` records at registration time and serving them to `GeometryStreamingSystem` on demand. `tick()` is a retained no-op for call-site compatibility. + +--- + +## Controlling OOC: `streamingPolicy` and `GeometryStreamingSystem.enabled` + +There is no global OOC on/off switch. Whether OOC applies is decided **per-asset** at `setEntityMeshAsync` call time via the `streamingPolicy` parameter. + +### What `streamingPolicy` controls + +The policy selects the **registration path** — what happens inside `setEntityMeshAsync` before the completion callback fires: + +| Policy | What happens at registration | `StreamingComponent` added? | `GeometryStreamingSystem` involved? | +|---|---|---|---| +| `.immediate` | All meshes uploaded to GPU immediately; `RenderComponent` registered | No | No | +| `.outOfCore` | Zero-GPU stub entities registered; CPU entries stored in `ProgressiveAssetLoader` | Yes (`.unloaded`) | Yes — must be running for anything to render | +| `.auto` | `AssetProfiler` decides based on memory budget | Depends | Depends | + +`.immediate` means OOC is not used at all for this asset. The full mesh is GPU-resident before the completion callback fires. `GeometryStreamingSystem` never sees this entity because it only operates on entities with a `StreamingComponent` in the `.unloaded` state. + +`.outOfCore` means OOC is the only path — nothing renders until `GeometryStreamingSystem` is enabled and uploads the stubs as the camera enters streaming range. + +### Relationship with `GeometryStreamingSystem.enabled` + +`GeometryStreamingSystem.enabled` is a separate runtime flag that gates whether the streaming loop runs at all: + +``` +streamingPolicy = .immediate → entity has no StreamingComponent + GeometryStreamingSystem.enabled = true/false → no effect on this entity + +streamingPolicy = .outOfCore → entity has StreamingComponent(.unloaded) + GeometryStreamingSystem.enabled = false → nothing ever renders + GeometryStreamingSystem.enabled = true → uploads on demand, evicts when far +``` + +These are genuinely different registration paths, not just different timing for the same outcome: + +- `.immediate` — load now, streaming system plays no role for this entity +- `.outOfCore` — register stubs now, `GeometryStreamingSystem` drives GPU residency for the entity's lifetime + +--- + +## Phase 0 — Parse (happens once, async) + +``` +setEntityMeshAsync(entityId: root, filename: "city_block", withExtension: "usdz") +``` + +Before any parse begins, the asset passes through a **two-stage admission gate**. Both stages must pass before any ECS entity is created or any GPU memory is allocated. + +### Two-Stage Admission Gate + +#### Stage 1 — Pre-Parse Gate (three-zone model) + +Stage 1 fires before `parseAssetAsync` is called. It uses only the on-disk file size and a conservative expansion multiplier to estimate the worst-case CPU heap spike during parsing: + +``` +projectedCPUBytes = fileSizeBytes × 20 +``` + +The 20× multiplier is a conservative upper bound for USDZ **geometry** decompression — real-world worst case is ~55× for a dense city geometry USDZ. It is intentionally blunt because no content information is available before parsing. + +Results are classified into three zones: + +| Zone | Condition | Outcome | +|------|-----------|---------| +| **Safe zone** | `projectedCPU ≤ 50% RAM` | Allow parse. No log entry. | +| **Soft zone** | `projectedCPU > 50% AND < 75% RAM` | Log `[AdmissionGate] Stage 1 SOFT ZONE` warning. Allow parse. Stage 2 is the authoritative gate. | +| **Hard reject** | `projectedCPU ≥ 75% RAM` | Log `[AdmissionGate] Stage 1 HARD REJECT` error. Assign fallback mesh. Return. | + +**Why a soft zone?** The 20× multiplier is calibrated for geometry-heavy USDZs. For texture-heavy assets, most of the on-disk bytes are compressed textures — which are **not** decoded during `parseAssetAsync`. The `MDLMeshBufferDataAllocator` only decompresses geometry; textures remain as compressed references until `ensureTexturesLoaded()` is called at first-upload time. A 555 MB texture-heavy USDZ may produce only ~2 GB of parse-time CPU allocation despite a projected 11 GB figure. The soft zone lets these assets pass to Stage 2, which measures the actual geometry footprint. + +**Hard reject still calls `loadFallbackMesh`** — the entity gets a visible placeholder cube so the scene remains stable and the rejection is immediately apparent. + +**Future refinement:** a lightweight ZIP central-directory scan before parsing could separate texture-entry bytes from geometry-entry bytes and apply the 20× multiplier only to the geometry portion, eliminating soft-zone false positives for texture-heavy assets entirely. This is deferred until the soft-zone model is validated on real assets. + +#### Stage 2 — Post-Parse Accurate Gate + +Stage 2 runs unconditionally after any successful parse — including assets that passed through the soft zone. It uses `AssetProfiler` to measure actual geometry byte estimates from the parsed `MDLMesh` objects: + +``` +if assetProfile.estimatedGeometryBytes > 75% of physicalMemory → HARD REJECT +``` + +Stage 2 is the **accurate authority**. It has full visibility into the parsed asset content and rejects based on actual geometry bytes, not file-size heuristics. When Stage 2 fires it also assigns the fallback mesh so the entity remains visible. + +The key limitation: by the time Stage 2 runs, `parseAssetAsync` has already allocated CPU heap for all MDLMesh buffers. Stage 2 cannot prevent the parse-time spike — it prevents all *downstream* work (stub registration, MDLAsset retention, CPU registry storage). When the gate fires, `assetData` goes out of scope and ARC releases the parsed buffers. + +#### Fallback Behavior on Rejection + +When either gate issues a hard reject, `loadFallbackMesh` is called before returning: + +```swift +// Both Stage 1 hard reject and Stage 2 hard reject now call: +loadFallbackMesh(entityId: entityId, filename: filename) +``` + +This assigns a default cube mesh to the entity so the scene is visually stable and the user receives immediate feedback that something was loaded (but replaced). Without the fallback the entity would be invisible and mesh-less. + +--- + +`parseAssetAsync` opens the USDZ using `MDLMeshBufferDataAllocator`. This allocator stores all vertex/index data on the **CPU heap** — no Metal buffers are allocated, so the entire file loads without touching the GPU. + +`childObjects(of: MDLMesh.self)` walks the hierarchy and returns **only leaf geometry nodes** — 500 MDLMesh objects, one per building. Each carries its full parent-chain transform and lives entirely in CPU RAM. + +The routing decision is controlled by the caller's `MeshStreamingPolicy` parameter: + +```swift +setEntityMeshAsync(entityId: root, filename: "city_block", withExtension: "usdz", + streamingPolicy: .auto) // default +``` + +| Policy | Routing | +|--------|---------| +| `.auto` (default) | `AssetProfiler`-based — budget-relative, domain-aware classification | +| `.outOfCore` | Always stubs + streaming, regardless of size or object count | +| `.immediate` | Always direct GPU upload; permanently GPU-resident, no streaming | + +For `.auto`, the engine runs `AssetProfiler` on the parsed `ProgressiveAssetData` to produce an `AssetProfile` and select an `AssetLoadingPolicy`: + +```swift +let profile = AssetProfiler.profile(url: url, assetData: assetData, fileSizeBytes: fileSizeBytes) +let policy = AssetProfiler.classifyPolicy(profile: profile, budget: MemoryBudgetManager.shared.meshBudget) +``` + +The `AssetLoadingPolicy` has two independent axes — geometry residency and texture residency. The geometry policy drives the out-of-core routing decision: + +| `geometryPolicy` | Routing | +|---|---| +| `.streaming` | Out-of-core stubs path — all leaf meshes registered as `.unloaded` stubs | +| `.eager` | Immediate path — all geometry uploaded to GPU in a single pass | + +Geometry streaming is selected when any of the following is true: + +- **Mesh count ≥ 50** — many meshes spike GPU allocation simultaneously even if total size is small +- **Geometry bytes exceed 30% of the platform budget** — e.g. 300 MB asset on a 1 GB machine +- **Monolithic asset (≤ 2 meshes) AND geometry exceeds 30% of budget** — streaming prevents OOM at registration, though the mesh still loads in one step + +All thresholds are expressed as **fractions of the live platform budget** (`MemoryBudgetManager.meshBudget`), so they scale correctly across devices: + +| Device class | `meshBudget` | 30% geometry threshold | +|---|---|---| +| macOS | 1 GB | ~300 MB | +| iOS high-end | 512 MB | ~154 MB | +| iOS low-end | 256 MB | ~77 MB | +| visionOS | 512 MB | ~154 MB | + +This means the same 200 MB asset routes to `.eager` on macOS (fits comfortably) but to `.streaming` on low-end iOS (too large to load all at once). The old fixed thresholds (`fileSizeThresholdBytes = 50 MB`, `outOfCoreObjectCountThreshold = 50 objects`) applied the same cutoff regardless of the target device. + +The profiler also classifies the asset's dominant memory domain and logs a full breakdown: + +``` +[AssetProfiler] 'dungeon3' (2.1 MB) → mixed | geo ~2.9 MB, tex ~6.2 MB | budget: 1024 MB | meshes: 410 +[AssetProfiler] Policy → geometry: streaming, texture: eager (source: auto) +``` + +See [AssetProfiler architecture](assetProfiler.md) for the full classification logic and how geometry and texture policies are derived independently. + +--- + +## Phase 1 — Stub Registration (happens once, synchronous within async context) + +### LOD Asset Detection + +Before choosing the registration path, `setEntityMeshAsync` inspects the top-level object names for LOD suffixes (`_LOD0`, `_LOD1`, …): + +```swift +let lodNameDetection = detectImportedLODGroups(fromSourceNames: topLevelNames) +let hasLODGroups = !lodNameDetection.groups.isEmpty +let useOutOfCore = loadingPolicy.geometryPolicy == .streaming +``` + +| Condition | Path | +|---|---| +| `useOutOfCore && hasLODGroups` | **LOD+OOC path** — one entity per LOD group, `cpuLODRegistry` | +| `useOutOfCore && !hasLODGroups` | **Regular OOC path** — one stub entity per `MDLObject`, `cpuMeshRegistry` | +| `!useOutOfCore` | Immediate path — all geometry uploaded to GPU in one pass | + +### LOD+OOC Stub Registration + +When LOD groups are detected in an OOC asset, each group becomes **one entity** (not one entity per MDLObject): + +``` +withWorldMutationGate { + Tree group → createEntity() → LODComponent(stubs) + StreamingComponent(.unloaded) + Rock group → createEntity() → LODComponent(stubs) + StreamingComponent(.unloaded) + ... +} +``` + +**Per LOD group entity:** +1. `createEntity()` — one entity for all LOD levels of this group +2. `applyWorldTransform(composedWorldTransform(for: lod0MDLObject))` — position from LOD0 object +3. `LocalTransformComponent.boundingBox` — seeded from LOD0 `MDLMesh.boundingBox` +4. `LODComponent` — stub `LODLevel`s for every level: `mesh: []`, `residencyState: .notResident`, `url` and `assetName` set for future disk reload reference +5. `StreamingComponent` — state `.unloaded`, placeholder radii (replaced by `enableStreaming`) +6. `OctreeSystem.shared.registerEntity` — appears in spatial queries immediately + +After the gate, CPU entries are stored per (group entity, LOD index): + +```swift +cpuLODRegistry[treeEntityId] = [ + 0: CPUMeshEntry(object: tree_LOD0_MDLObject, uniqueAssetName: "Tree_LOD0", ...), + 1: CPUMeshEntry(object: tree_LOD1_MDLObject, uniqueAssetName: "Tree_LOD1", ...), + 2: CPUMeshEntry(object: tree_LOD2_MDLObject, uniqueAssetName: "Tree_LOD2", ...), +] +``` + +### Regular OOC Stub Registration + +Instead of uploading to the GPU, all 500 buildings are registered immediately as **stub entities** — full ECS presence, zero GPU allocation. + +All stubs are registered inside a **single `withWorldMutationGate` acquisition**. This avoids N × acquire/release overhead — for 500 buildings that would be 500 separate gate round-trips on the XR compositor thread. One gate wraps the entire loop: + +``` +withWorldMutationGate { + Building #1 → createEntity() → LocalTransform + Scenegraph + StreamingComponent(.unloaded) + Building #2 → createEntity() → LocalTransform + Scenegraph + StreamingComponent(.unloaded) + ... + Building #500 → createEntity() → LocalTransform + Scenegraph + StreamingComponent(.unloaded) +} +``` + +**Per stub (`registerProgressiveStubEntity`):** +1. `createEntity()` — new ECS entity +2. `applyWorldTransform(composedWorldTransform(for: mdlMesh))` — world position set from the full MDL parent chain, used by octree and distance calculations +3. `LocalTransformComponent.boundingBox` — seeded from `MDLMesh.boundingBox` so spatial queries return correct extents +4. `StreamingComponent` — state `.unloaded`, `streamingRadius = Float.greatestFiniteMagnitude` (placeholder until `enableStreaming` is called) +5. `OctreeSystem.shared.registerEntity` — stub appears in spatial queries immediately +6. No `RenderComponent`, no Metal buffers + +After the gate closes, CPU entries are stored in `ProgressiveAssetLoader.cpuMeshRegistry` (lock-based, no ECS mutation needed): + +```swift +cpuMeshRegistry[childEntityId] = CPUMeshEntry( + object: mdlMesh, // MDLMesh with CPU-heap vertex data + vertexDescriptor: ..., + textureLoader: ..., + device: ..., + url: ..., + uniqueAssetName: "Hull_A#42", + estimatedGPUBytes: 524288 // vertex + index bytes, computed from MDLMesh at stub time +) +``` + +`estimatedGPUBytes` is computed at stub registration from `MDLMesh.vertexCount` and the vertex descriptor stride — no disk I/O required. It is used by the pre-emptive budget reservation in Phase 3 so the system can check `canAccept()` before starting each upload. + +The `MDLAsset` container is retained in `rootAssetRefs[rootEntityId]` so the `MDLMeshBufferDataAllocator` backing all child CPU buffers stays alive. + +**Completion callback fires immediately** — no GPU work was done, no frame budget was consumed. The app is unblocked. + +--- + +## Phase 2 — Completion Callback and Enabling Streaming + +`setEntityMeshAsync`'s completion closure receives `isOutOfCore: Bool`: + +- `true` — the asset was registered as stubs; `GeometryStreamingSystem` must be enabled for anything to render. +- `false` — the asset used the immediate path; all meshes are already GPU-resident. + +```swift +setEntityMeshAsync(entityId: root, filename: "city_block", withExtension: "usdz") { isOutOfCore in + if isOutOfCore { + // Enable the system, then set real streaming radii on the root. + // enableStreaming propagates the radii down to all child stubs, + // replacing their Float.greatestFiniteMagnitude placeholders. + GeometryStreamingSystem.shared.enabled = true + enableStreaming(entityId: root, streamingRadius: 80, unloadRadius: 120) + } +} +``` + +`enableStreaming` iterates all children. For out-of-core stubs it finds them via `StreamingComponent` (not `RenderComponent`, which doesn't exist yet): + +```swift +for childId in sceneGraph.children { + if hasRenderComponent || hasStreamingComponent { + enableStreamingForSingleEntity(childId, streamingRadius: 80, unloadRadius: 120) + } +} +``` + +For each stub, `enableStreamingForSingleEntity` detects the no-RenderComponent case and only updates the radii — state stays `.unloaded`: + +``` +Building #1 StreamingComponent: streamingRadius=80, unloadRadius=120, state=.unloaded +Building #2 StreamingComponent: streamingRadius=80, unloadRadius=120, state=.unloaded +... +Building #500 StreamingComponent: streamingRadius=80, unloadRadius=120, state=.unloaded +``` + +The streaming system can now load buildings within 80 m and unload them beyond 120 m as the camera moves. + +--- + +## Phase 3 — Distance-Based Streaming (every 0.1 s, ongoing) + +`GeometryStreamingSystem.update()` runs every 0.1 s. + +### Camera position + +Distance calculations use `CameraComponent.localPosition` (transformed via `SceneRootTransform.effectiveCameraPosition`), not the `WorldTransformComponent`-derived position. On Vision Pro, `CameraComponent.localPosition` is updated every ARKit frame directly — it is always current. The `WorldTransformComponent` goes through the scene-graph propagation pass and can lag by a frame, causing incorrect distance ordering. + +### Memory budget gate (runs before any load) + +The system maintains two independent memory pressure signals: + +| Signal | Method | Meaning | +|---|---|---| +| Combined | `shouldEvict()` | (mesh + texture) ≥ 85% of `meshBudget` | +| Geometry only | `shouldEvictGeometry()` | mesh bytes alone ≥ 85% of `meshBudget` | + +The load gate uses **geometry-only pressure** so that texture upgrades on already-loaded entities cannot block new mesh loads. The two signals drive a three-step response before any load starts: + +``` +1. if combined high AND geometry NOT high: + TextureStreamingSystem.shedTextureMemory(maxEntities: 4) + → texture relief only; no eviction, no load blocking + +2. if geometry high: + TextureStreamingSystem.shedTextureMemory(maxEntities: 8) ← texture first + evictLRU() ← geometry fallback + +3. Snapshot shouldEvictGeometry() once after eviction + → only start new loads if geometry budget allows +``` + +This prevents in-range stubs from uploading simultaneously and pushing GPU memory past the OS kill threshold. The geometry-only gate also prevents the budget-exhaustion/eviction deadlock that occurs on scenes where every entity fits within the streaming radius: texture upgrades no longer consume geometry headroom, so all stubs can load regardless of how much texture memory is in use. + +### Adaptive tick rate + +By default `update()` runs every `updateInterval` (0.1 s). When `lastPendingLoadBacklog > 0` — meaning candidates were queued but not dispatched due to the concurrency cap — the tick interval drops to `burstTickInterval` (default 16 ms). This prevents a 100 ms cadence stall while work is actively waiting for a slot. + +``` +backlog > 0 → tick at 16 ms (burst mode) +backlog = 0 → tick at 100 ms (steady state) +``` + +The tick rate returns to 100 ms automatically once the backlog drains. + +### Distance-banded concurrency + +Load candidates are split into two bands before any load starts: + +``` +Near band: distance ≤ streamingRadius × nearBandFraction (default 0.33) + → normally serialized: nearBandMaxConcurrentLoads (default 1) in-flight at a time + → guarantees distance-ordered appearance for the closest meshes + +Rest band: distance > streamingRadius × nearBandFraction + → uses remaining global slots (maxConcurrentLoads − near-band in-flight) +``` + +Near-band loads are tracked in a separate `activeNearBandLoads` set so the concurrency limit is enforced independently of the global slot count. + +#### Single-root burst detection + +When all near-band candidates share the same `assetRootEntityId` (e.g. all 75 meshes are sub-objects of one USDZ), distance-ordering within the asset is already guaranteed at the asset level. Per-mesh serialization in this case only wastes slots. The system detects this and expands `nearBandEffectiveMax` to `maxConcurrentLoads` for that tick: + +``` +all near-band candidates share one root → nearBandEffectiveMax = maxConcurrentLoads (3) +mixed roots or non-OOC entities → nearBandEffectiveMax = nearBandMaxConcurrentLoads (1) +``` + +The per-asset texture lock remains the actual safety gate against MDLAsset races. + +### Prewarm-active dispatch deferral + +When `storeAsset` is called, a background `Task` immediately starts running `loadTextures()` on the asset (the prewarm). If `GeometryStreamingSystem` dispatches an upload while the prewarm task holds the per-asset texture lock, the upload blocks for the full remaining prewarm duration — typically 1–2 s — wasting all concurrent slots. + +The scheduler avoids this by checking `ProgressiveAssetLoader.shared.isPrewarmActive(for: rootId)` before dispatching any entity. Entities for roots with an active prewarm are skipped; their candidates remain in the backlog (and burst-tick mode keeps checking at 16 ms). Once the prewarm completes and releases the lock, `isPrewarmActive` returns `false` and the next tick dispatches the full batch with lock wait ≈ 0 ms. + +### CPU-entry readiness guard + +Stub entities with a `StreamingComponent` may appear in the near-band candidate list before their `CPUMeshEntry` has been stored in `ProgressiveAssetLoader` (registration happens async in parallel with the streaming system running). Dispatching such an entity wastes a slot on a fallback that will fail. + +The dispatch loop skips any OOC entity whose CPU entry is not yet available, unless the root is CPU-cold (cold roots rehydrate intentionally from disk): + +```swift +if !isColdRoot(rootId) + && retrieveCPUMesh(entityId) == nil + && !hasCPULODData(entityId) { + continue // CPU data not ready yet — skip, will dispatch next tick +} +``` + +### Pre-emptive budget reservation + +Before each load starts (both bands), the system checks whether the mesh will fit using the **geometry-only** budget check: + +```swift +if let cpuEntry = ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId), + !MemoryBudgetManager.shared.canAcceptMesh(sizeBytes: cpuEntry.estimatedGPUBytes) { + evictLRU(cameraPosition:) // targeted geometry eviction to make room + guard canAcceptMesh(...) else { continue } // skip if still no room +} +``` + +`canAcceptMesh` checks only `totalMeshMemory + sizeBytes ≤ meshBudget` — texture memory is excluded. This ensures that a large batch of texture upgrades on visible entities cannot prevent a nearby stub from loading its geometry. + +`estimatedGPUBytes` (stored in `CPUMeshEntry` at stub-registration time) lets this check run without any GPU work or disk I/O. + +### Load / unload loop + +``` +For each nearby entity: + distance = length(entity.worldCenter - effectiveCameraPosition) + + if state == .unloaded && distance <= streamingRadius (80m): + canAccept(estimatedGPUBytes)? → evict if not, skip if still no room + loadMesh(entityId, isNearBand) → checks cpuMeshRegistry → uploadFromCPUEntry() + → makeMeshesFromCPUBuffers → registerRenderComponent + → MemoryBudgetManager.registerMesh() ← GPU bytes tracked + → state = .loaded + + if state == .loaded && distance > unloadRadius (120m): + unloadMesh(entityId) → render.mesh = [] + → MemoryBudgetManager.unregisterMesh() + → state = .unloaded + → cpuMeshRegistry entry kept intact +``` + +### Value-score eviction + +`evictLRU` no longer evicts purely by least-recently-used frame. Candidates are ranked by a value score: + +``` +distanceFactor = min(1.0, distance / maxQueryRadius) +sizeFactor = min(1.0, meshBytes / meshBudget) +score = evictionDistanceWeight × distanceFactor + evictionSizeWeight × sizeFactor +``` + +Highest score is evicted first — far, large meshes go before near, small ones. `lastVisibleFrame` is the tiebreaker for equal scores. This protects nearby small meshes (high camera-coverage value) while freeing the largest far meshes first. + +#### Distance-aware visibility guard + +The eviction loop also applies a distance-aware guard to visible entities: + +``` +if visible AND distance < visibleEvictionProtectionRadius (default 30 m) → skip (protect close foreground) +if visible AND distance ≥ visibleEvictionProtectionRadius → allow eviction +``` + +This replaces the previous hard `visibleEntityIds.contains` block that prevented evicting any visible entity regardless of distance. The old guard caused a residency deadlock on zoom-out → zoom-in cycles: after zooming back in, all loaded far meshes were in-frustum, making every candidate unevictable — budget was permanently stuck and nearby meshes could not load. + +With the distance-aware guard, far visible meshes (beyond 30 m) are evictable under memory pressure. Meshes within 30 m of the camera remain protected from eviction to prevent obvious foreground popping. + +**Tuning:** `visibleEvictionProtectionRadius` should be set to ~15% of your `streamingRadius`. For `streamingRadius = 200 m`, the default 30 m is appropriate. + +### The CPU upload path + +`loadMesh` selects the upload function based on the entity's registration type: + +```swift +if hasCPULODData { + // LOD+OOC entity: upload all LOD levels from cpuLODRegistry + await uploadActiveLODFromCPU(entityId: entityId) +} else if hasLOD { + // Disk-based LOD entity (no CPU registry): reload from MeshResourceManager + await reloadLODEntity(entityId: entityId) +} else { + // Regular entity (OOC or immediate): upload from cpuMeshRegistry or disk + await loadMeshAsync(...) +} +``` + +#### `uploadActiveLODFromCPU` (LOD+OOC entities) + +1. Checks if the root asset is cold — if so, calls `rehydrateColdAsset` to re-parse and rebuild `cpuLODRegistry` +2. `retrieveAllCPULODMeshes(for: entityId)` — fetches all LOD-level CPU entries +3. Texture lock acquired; `ensureTexturesLoaded` called once for the root asset +4. `makeMeshesFromCPUBuffers` — uploads every LOD level from CPU heap to Metal +5. `lodComponent.lodLevels[i].residencyState = .resident` for each uploaded level +6. `registerRenderComponent` — entity becomes visible at the distance-appropriate LOD +7. `MemoryBudgetManager.registerMesh` — total GPU bytes for all LOD levels registered +8. CPU data retained — re-approach after eviction re-uploads all levels from RAM + +#### `uploadFromCPUEntry` (regular OOC entities) + +When `loadMeshAsync` is called for a regular out-of-core stub, it checks the CPU registry **before** going to disk: + +```swift +if let cpuEntry = ProgressiveAssetLoader.shared.retrieveCPUMesh(for: entityId) { + return await uploadFromCPUEntry(entityId: entityId, cpuEntry: cpuEntry) +} +// fallback: MeshResourceManager (disk / cache) for non-stub entities +``` + +1. `makeMeshesFromCPUBuffers` — copies MDLMesh vertex/index data from CPU heap to Metal-backed buffers +2. `registerRenderComponent` — entity gets a `RenderComponent`, becomes visible +3. `MemoryBudgetManager.registerMesh` — registers the Metal allocation so `shouldEvict()` sees it +4. CPU data is **not** cleared — the `cpuMeshRegistry` entry stays so the next eviction+reload cycle re-uploads from RAM, not disk + +### Memory model at steady state + +``` +CPU RAM: ~100-200 MB (all 500 buildings' MDLMesh data, always resident) +GPU RAM: ~10-30 MB (only the ~15-20 buildings within 80m of camera) +Disk: read once at startup +``` + +--- + +## What "Walking Around the City" Actually Does + +``` +Camera starts at south entrance (0, 0, 0) +→ buildings within 80m: #1-#18 → uploadFromCPUEntry → .loaded → visible +→ buildings 81-500m away: .unloaded → invisible, CPU data resident + +Camera walks north 200m to (0, 0, -200) +→ buildings #1-#18 now beyond 120m → unloadMesh → .unloaded → Metal buffers freed +→ buildings #220-#238 now within 80m → uploadFromCPUEntry → .loaded → visible +→ re-approach #1-#18 later → uploadFromCPUEntry again (from CPU RAM, not disk) +``` + +Every building is always present as an ECS entity. The GPU footprint at any moment reflects only what the camera can actually see. No entity is ever permanently absent — all 500 are available for upload at any time. + +--- + +## Texture Loading + +### Background Texture Prewarm + +`storeAsset` immediately fires a background `Task` at `.userInitiated` priority to call `loadTextures()` as soon as the asset is registered — before streaming is enabled and before any mesh enters range. By the time the camera gets close enough to trigger uploads, `loadTextures()` has typically already completed. + +`ProgressiveAssetLoader.activePrewarmRoots` tracks roots with an in-flight prewarm. The dispatch loop calls `isPrewarmActive(for: rootId)` and defers all entities for that root until the prewarm task finishes and releases the texture lock. Once it does, the first batch of uploads proceeds with `lockWait ≈ 0 ms`. + +### Why `loadTextures()` Is Deferred from Parse Time + +`MDLAsset` decompresses texture data lazily. Calling `asset.loadTextures()` at parse time for a 500 MB USDZ can OOM-kill the process before the app is interactive. The out-of-core path skips `loadTextures()` at parse time — deferred initially to first-upload time, and now moved earlier via the background prewarm. + +### `ensureTexturesLoaded` — Called Once Per Asset + +Both `prewarmTexturesAsync` and the upload path call `ensureTexturesLoaded(for: rootId)`. The method is idempotent: + +1. Checks `assetTexturesLoaded` — returns immediately if already done. +2. Calls `asset.loadTextures()` — decompresses textures into CPU RAM once. +3. Marks the asset in `assetTexturesLoaded` — all subsequent calls are no-ops. + +In normal operation the prewarm wins the race. The upload path call becomes a no-op. + +### Per-Asset NSLock — Scope Covers Only `loadTextures()` + +`MDLAsset` is not thread-safe during `loadTextures()`. Each asset has a dedicated `NSLock` in `ProgressiveAssetLoader.assetTextureLocks`. The lock scope covers **only** `ensureTexturesLoaded` — it is released before `makeMeshesFromCPUBuffers`: + +``` +Task A (Building #1) Task B (Building #2) +acquireAssetTextureLock(rootId) acquireAssetTextureLock(rootId) ← BLOCKS +ensureTexturesLoaded(rootId) ...waiting for lock... +releaseAssetTextureLock(rootId) ← unblocks +makeMeshesFromCPUBuffers(#1) ensureTexturesLoaded(rootId) ← no-op (already done) + (runs without lock) releaseAssetTextureLock(rootId) + makeMeshesFromCPUBuffers(#2) + (runs without lock) +``` + +After `loadTextures()` completes the `MDLAsset` is in a stable read-only state. Concurrent `makeMeshesFromCPUBuffers` calls from the same asset are safe, so all three upload slots can proceed in parallel. Uploads from *different* assets run concurrently without any contention. + +### Texture Cache Key Uniqueness (`objectIdentityURL`) + +`TextureLoader` caches textures by URL. USDZ files with bracket-notation paths (e.g., `file:///scene.usdz[0/texture.png]`) produce stable, unique URLs via `parseUSDZBracketPath`. But when an MDLTexture has no parseable bracket path, a fallback URL is generated from the pointer identity of the `MDLTexture` object: + +```swift +URL(string: "mdl-obj-\(UInt(bitPattern: ObjectIdentifier(mdlTex)))")! +``` + +This ensures every unnamed or identically-named texture gets a unique cache key, preventing one texture from being substituted for another on meshes that happen to share a texture name. + +The same identity URL is used as `material.baseColorURL`, so `BatchingSystem.getMaterialHash` also distinguishes these textures correctly — which prevents wrong textures from appearing after static batching. + +--- + +## Warm / Cold CPU Residency + +By default every registered root asset is **CPU-warm**: its `MDLAsset` and all child `CPUMeshEntry` objects live in RAM indefinitely. That is the right default for scenes that fit in RAM, but for extremely large world-scale scenes (hundreds of open-world chunks, each with its own USDZ) it is desirable to evict CPU-heap geometry data for assets that the camera is far from. + +### `releaseWarmAsset` — Free CPU Heap Without Destroying the Entity + +```swift +ProgressiveAssetLoader.shared.releaseWarmAsset(rootEntityId: rootId) +``` + +Releases the `MDLAsset` and all child `CPUMeshEntry` objects for `rootId`, freeing CPU heap memory. The root is now **CPU-cold**. + +The rehydration context (URL + loading policy) stored at stub-registration time is **retained** — the asset can be re-parsed from disk transparently when the camera re-approaches. + +`releaseWarmAsset` does **not** destroy ECS entities, streaming components, or GPU-resident meshes. GPU eviction remains the responsibility of `GeometryStreamingSystem.evictLRU`. + +### Transparent Cold Re-Stream + +When `loadMeshAsync` is called for a child entity whose root is cold (i.e. `retrieveCPUMesh` returns `nil` AND `isColdRoot` is true), `GeometryStreamingSystem` automatically calls `rehydrateColdAsset`: + +``` +loadMeshAsync(entityId: building_42) + → retrieveCPUMesh(building_42) = nil + → isColdRoot(rootId) = true + → rehydrateColdAsset(rootId, context) ← re-parse from disk + getOrCreateRehydrationTask(rootId) ← exactly one Task per root + Mesh.parseAssetAsync(context.url) ← USDZ re-read + storeCPUMesh for all children ← rebuild CPU registry + storeAsset + markAsWarm ← root is warm again + → retrieveCPUMesh(building_42) = CPUMeshEntry + → uploadFromCPUEntry ← normal CPU→Metal upload +``` + +Concurrent child uploads for the same cold root all await the **same** `Task` — `getOrCreateRehydrationTask` ensures only one re-parse runs per root regardless of how many children simultaneously detect the cold state. + +### Warm/Cold State Machine + +``` + storeAsset + registerChildren + storeRootRehydrationContext + │ + ▼ + [CPU-warm] + (default state) + │ + releaseWarmAsset() │ + ▼ + [CPU-cold] + MDLAsset released + CPUMeshEntry[] cleared + rehydrationContext alive + │ + rehydrateColdAsset() + markAsWarm() + │ + ▼ + [CPU-warm] + MDLAsset restored + CPUMeshEntry[] rebuilt +``` + +`removeOutOfCoreAsset` exits the state machine entirely — ECS entities remain but all registry entries (warm or cold) are cleared and the rehydration context is removed. + +--- + +## Lifetime and Cleanup + +When the root entity is destroyed, call: + +```swift +ProgressiveAssetLoader.shared.removeOutOfCoreAsset(rootEntityId: rootId) +``` + +This releases all `CPUMeshEntry` references, the `MDLAsset`, warm/cold state, and the rehydration context — freeing all CPU-heap geometry data for all 500 buildings. + +To free CPU heap without destroying the entity (e.g., for a far chunk that may return): + +```swift +ProgressiveAssetLoader.shared.releaseWarmAsset(rootEntityId: rootId) +// Entity remains registered; re-approach triggers transparent cold re-stream from disk. +``` + +--- + +## Tuning Reference + +| Property | Default | Effect | +|----------|---------|--------| +| `MemoryBudgetManager.meshBudget` | device-set | GPU memory ceiling; all `AssetProfiler` thresholds are expressed as fractions of this value | +| `GeometryStreamingSystem.maxConcurrentLoads` | 3 | Total concurrent CPU→Metal uploads across both bands | +| `GeometryStreamingSystem.nearBandFraction` | 0.33 | Fraction of `streamingRadius` defining the near band; near-band loads are serialized | +| `GeometryStreamingSystem.nearBandMaxConcurrentLoads` | 1 | Max in-flight loads in the near band; 1 guarantees distance-ordered appearance | +| `GeometryStreamingSystem.updateInterval` | 0.1 s | Steady-state tick interval | +| `GeometryStreamingSystem.burstTickInterval` | 0.016 s | Tick interval when a load backlog exists; drops to 16 ms for faster slot pickup | +| `GeometryStreamingSystem.maxQueryRadius` | 500 m | Octree query radius; must be ≥ `unloadRadius` | +| `GeometryStreamingSystem.evictionDistanceWeight` | 0.6 | How much distance contributes to eviction score; higher = farther entities evicted first | +| `GeometryStreamingSystem.evictionSizeWeight` | 0.4 | How much GPU size contributes to eviction score; higher = larger meshes evicted first | +| `GeometryStreamingSystem.visibleEvictionProtectionRadius` | 30 m | Visible entities within this distance are never evicted; set to ~15% of `streamingRadius` | +| `streamingRadius` | caller-set | Distance at which `.unloaded` entities get uploaded | +| `unloadRadius` | caller-set | Distance beyond which `.loaded` entities are evicted; must be > `streamingRadius` | diff --git a/docs/Architecture/progressiveAssetLoader.md b/docs/Architecture/progressiveAssetLoader.md new file mode 100644 index 000000000..2f438a1e6 --- /dev/null +++ b/docs/Architecture/progressiveAssetLoader.md @@ -0,0 +1,231 @@ +# Progressive Asset Loader + +## TL;DR + +`ProgressiveAssetLoader` is a **CPU registry** — its sole responsibility is storing `CPUMeshEntry` records for out-of-core stub entities and serving them to `GeometryStreamingSystem` on demand. + +> **Note:** This document describes the current architecture. The earlier tick-based progressive loader (per-frame job queue, `PendingObjectItem`, `enqueue(job)`, `tick()` processing N meshes per frame) was replaced by the out-of-core stub system. `tick()` is retained as a no-op for call-site compatibility only. + +--- + +## What It Stores + +When `setEntityMeshAsync` routes an asset through the out-of-core path it stores CPU-side geometry in one of two registries depending on whether the asset contains LOD groups: + +- **`cpuMeshRegistry`** (`[EntityID: CPUMeshEntry]`) — one entry per stub entity (regular OOC assets) +- **`cpuLODRegistry`** (`[EntityID: [Int: CPUMeshEntry]]`) — one entry per LOD level per LOD group entity (LOD+OOC assets) + +Both registries store `CPUMeshEntry` records: + +```swift +struct CPUMeshEntry { + let object: MDLObject // MDLMesh with CPU-heap vertex/index data + let vertexDescriptor: MDLVertexDescriptor + let textureLoader: TextureLoader + let device: MTLDevice + let url: URL + let filename: String + let withExtension: String + let uniqueAssetName: String // "Hull_A#42" — stable across load cycles + let estimatedGPUBytes: Int // vertex + index bytes; used for pre-emptive budget reservation +} +``` + +Entries are keyed by child entity ID. `GeometryStreamingSystem` retrieves them via `retrieveCPUMesh(for:)` when an entity enters streaming range, copies the MDL buffers into Metal-backed buffers, and registers a `RenderComponent`. The CPU entry is **never removed on unload** — re-approaching an evicted entity re-uploads from RAM with no disk I/O. + +### LOD CPU Registry (LOD+OOC Path) + +When a USDZ asset contains LOD groups (top-level objects named `Tree_LOD0`, `Tree_LOD1`, etc.) and qualifies for OOC streaming, `setEntityMeshAsync` takes the **LOD+OOC path** instead of the per-stub path: + +- One entity is created per LOD group (instead of one entity per MDLObject) +- Each entity gets a `LODComponent` with stub `LODLevel`s — empty mesh arrays, `residencyState: .notResident` +- One `CPUMeshEntry` is stored in `cpuLODRegistry[groupEntityId][lodIndex]` for each LOD level + +```swift +// LOD+OOC: per-level CPU entries, keyed by (group entity ID, LOD index) +cpuLODRegistry[treeEntityId] = [ + 0: CPUMeshEntry(object: tree_LOD0_MDLObject, uniqueAssetName: "Tree_LOD0", ...), + 1: CPUMeshEntry(object: tree_LOD1_MDLObject, uniqueAssetName: "Tree_LOD1", ...), + 2: CPUMeshEntry(object: tree_LOD2_MDLObject, uniqueAssetName: "Tree_LOD2", ...), +] +``` + +`GeometryStreamingSystem` detects the LOD+OOC path via `hasCPULODData(for:)` and calls `uploadActiveLODFromCPU` instead of `uploadFromCPUEntry`. This uploads **all** LOD levels from the CPU registry in one pass, then sets the render component to the level appropriate for the current camera distance. Subsequent LOD switches are handled by `LODSystem` which swaps `renderComponent.mesh` from the already-resident `lodComponent.lodLevels` array — no additional streaming needed until the entity is evicted and re-enters range. + +--- + +## The MDLAsset Lifetime Problem + +`MDLMeshBufferDataAllocator` (used by `parseAssetAsync`) backs all CPU buffers via the `MDLAsset` container. If the asset is released, all child MDLMesh CPU pointers become dangling. + +`ProgressiveAssetLoader` solves this with `rootAssetRefs`: + +```swift +private var rootAssetRefs: [EntityID: MDLAsset] = [:] +``` + +`storeAsset(_:for:)` pins the `MDLAsset` to the root entity ID. It stays alive until `removeOutOfCoreAsset(rootEntityId:)` is called at entity destruction time. + +--- + +## Background Texture Prewarm + +`storeAsset(_:for:)` immediately fires a background `Task` at `.userInitiated` priority to call `loadTextures()` before any mesh enters streaming range: + +```swift +func storeAsset(_ asset: MDLAsset, for rootEntityId: EntityID) { + // Pin the asset and create the per-asset texture lock. + lock.lock() + rootAssetRefs[rootEntityId] = asset + assetTextureLocks[rootEntityId] = NSLock() + lock.unlock() + // Kick off background prewarm immediately. + prewarmTexturesAsync(for: rootEntityId) +} +``` + +The prewarm task acquires the per-asset texture lock, calls `ensureTexturesLoaded`, and releases the lock — all off the critical path. By the time the first mesh enters streaming range, `loadTextures()` has typically already completed, so the first-upload path sees a no-op `ensureTexturesLoaded` call and zero lock wait. + +`activePrewarmRoots` tracks which roots have an in-flight prewarm task. `GeometryStreamingSystem` queries `isPrewarmActive(for:)` in the dispatch loop and defers uploading entities for that root until the prewarm completes. This prevents the first batch of uploads from blocking on the texture lock for the full remaining prewarm duration. + +--- + +## Per-Asset Texture Serialization + +`MDLAsset` is not thread-safe. Two `GeometryStreamingSystem` tasks uploading different meshes from the same asset concurrently can race during `loadTextures()`. `ProgressiveAssetLoader` prevents this with a per-asset `NSLock`: + +```swift +private var assetTextureLocks: [EntityID: NSLock] = [:] +``` + +`storeAsset` creates the lock alongside the asset reference. Every upload task brackets only `ensureTexturesLoaded` with the lock — the lock is released **before** `makeMeshesFromCPUBuffers`: + +```swift +ProgressiveAssetLoader.shared.acquireAssetTextureLock(for: rootId) +ProgressiveAssetLoader.shared.ensureTexturesLoaded(for: rootId) +ProgressiveAssetLoader.shared.releaseAssetTextureLock(for: rootId) +// makeMeshesFromCPUBuffers runs without the lock — MDLAsset is read-only after loadTextures() +``` + +After `loadTextures()` completes the `MDLAsset` is in a stable read-only state. Concurrent `makeMeshesFromCPUBuffers` calls from the same asset are safe without the lock, so all three upload slots can proceed in parallel once the prewarm is done. + +Only the `ensureTexturesLoaded` call is serialized per asset. Meshes from *different* assets upload concurrently without any contention. + +--- + +## Deferred `loadTextures()` + +Large assets skip `asset.loadTextures()` at parse time to avoid the OOM risk of decompressing all textures before the app is interactive. The call is deferred via `ensureTexturesLoaded`: + +```swift +func ensureTexturesLoaded(for rootEntityId: EntityID) { + // Must be called while per-asset texture lock is held. + // Calls asset.loadTextures() exactly once per asset lifetime. +} +``` + +`assetTexturesLoaded: Set` ensures the call happens exactly once. In normal operation the prewarm task wins the race and marks the asset loaded before any upload task reaches `ensureTexturesLoaded`, making the upload-path call a no-op. + +--- + +## API Surface + +| Method | Purpose | +|--------|---------| +| `storeCPUMesh(_:for:)` | Store a `CPUMeshEntry` keyed by child entity ID (regular OOC) | +| `retrieveCPUMesh(for:)` | Fetch the entry for `GeometryStreamingSystem` upload (regular OOC) | +| `removeCPUMesh(for:)` | Remove a single entry (rarely needed; prefer `removeOutOfCoreAsset`) | +| `storeCPULODMesh(_:for:lodIndex:)` | Store a `CPUMeshEntry` for one LOD level of a LOD group entity | +| `retrieveCPULODMesh(for:lodIndex:)` | Fetch the entry for a specific LOD level | +| `retrieveAllCPULODMeshes(for:)` | Fetch all LOD-level entries for a group entity | +| `hasCPULODData(for:)` | Returns `true` if the entity was registered via the LOD+OOC path | +| `removeCPULODEntry(for:)` | Remove all LOD entries for a group entity | +| `storeAsset(_:for:)` | Pin an `MDLAsset`, create its per-asset texture lock, and kick off background prewarm | +| `isPrewarmActive(for:)` | Returns `true` while the background prewarm task holds the texture lock for this root | +| `registerChildren(_:for:)` | Associate child entity IDs with a root for bulk cleanup | +| `acquireAssetTextureLock(for:)` | Lock before calling `ensureTexturesLoaded` | +| `releaseAssetTextureLock(for:)` | Unlock immediately after `ensureTexturesLoaded` — before GPU upload work | +| `ensureTexturesLoaded(for:)` | Call `loadTextures()` exactly once per asset (must hold texture lock) | +| `removeOutOfCoreAsset(rootEntityId:)` | Release all CPU entries (both registries) + MDLAsset for a destroyed root entity | +| `cancelAll()` | Release everything — use on scene reset or test teardown | +| `tick()` | No-op stub; retained for call-site compatibility | + +--- + +## Data Flow + +``` +setEntityMeshAsync (out-of-core path — regular OOC) + │ + ├─ parseAssetAsync() → MDLAsset in CPU RAM (no GPU spike) + ├─ registerProgressiveStubEntity() → N ECS stubs, StreamingComponent(.unloaded) + ├─ storeCPUMesh(entry, for: childId) × N → cpuMeshRegistry + ├─ storeAsset(asset, for: rootId) → rootAssetRefs, assetTextureLocks + │ └─ prewarmTexturesAsync() → background Task: acquireLock / loadTextures() / releaseLock + ├─ registerChildren(childIds, for: rootId) + └─ completion(true) → caller enables GeometryStreamingSystem + +setEntityMeshAsync (out-of-core path — LOD+OOC) + │ + ├─ parseAssetAsync() → MDLAsset in CPU RAM + ├─ detectImportedLODGroups() → N LOD groups detected + ├─ (per group) createEntity + LODComponent(stubs) + StreamingComponent(.unloaded) + ├─ storeCPULODMesh(entry, for: groupId, lodIndex:) × (N groups × L levels) → cpuLODRegistry + ├─ storeAsset(asset, for: rootId) + │ └─ prewarmTexturesAsync() → background Task: acquireLock / loadTextures() / releaseLock + ├─ registerChildren(groupEntityIds, for: rootId) + └─ completion(true) + +GeometryStreamingSystem (adaptive tick: 16 ms during backlog, 100 ms steady-state) + │ + ├─ isPrewarmActive(rootId)? → YES → defer all entities for this root (slots stay free) + │ + ├─ entity within streamingRadius && state == .unloaded + │ ├─ hasCPULODData? → YES → uploadActiveLODFromCPU() + │ │ ├─ retrieveAllCPULODMeshes(for: entityId) + │ │ ├─ acquireAssetTextureLock / ensureTexturesLoaded / releaseAssetTextureLock + │ │ ├─ makeMeshesFromCPUBuffers() × L levels ← lock released; parallel uploads safe + │ │ ├─ LODComponent.lodLevels[i].residencyState = .resident for each uploaded level + │ │ └─ registerRenderComponent() at distance-appropriate LOD + │ │ + │ └─ hasCPULODData? → NO → retrieveCPUMesh / uploadFromCPUEntry (regular OOC) + │ ├─ acquireAssetTextureLock / ensureTexturesLoaded / releaseAssetTextureLock + │ ├─ makeMeshesFromCPUBuffers() ← lock released; parallel uploads safe + │ └─ registerRenderComponent() + │ + └─ entity beyond unloadRadius && state == .loaded + └─ render.mesh = [] (cpu entries kept — re-upload from RAM on re-approach) + +destroyAllEntities / scene reset + └─ removeOutOfCoreAsset(rootEntityId:) → frees both CPU registries + MDLAsset +``` + +--- + +## Memory Model at Steady State + +``` +CPU RAM: all leaf meshes' MDLMesh vertex/index data — always resident +GPU RAM: only entities within streamingRadius — uploaded on demand +Disk: read exactly once at parse time +``` + +This trades a modest CPU-RAM footprint for predictable GPU memory usage and zero-latency re-uploads after eviction. + +--- + +## Cleanup + +Call `removeOutOfCoreAsset(rootEntityId:)` when destroying a root entity to free its CPU-heap geometry and texture-lock state: + +```swift +ProgressiveAssetLoader.shared.removeOutOfCoreAsset(rootEntityId: rootId) +``` + +`destroyAllEntities` does not call this automatically — you must call it explicitly if you are managing entity lifetimes outside the engine's destruction path. + +For full teardown (scene resets, tests): + +```swift +ProgressiveAssetLoader.shared.cancelAll() +``` diff --git a/docs/Architecture/renderingSystem.md b/docs/Architecture/renderingSystem.md new file mode 100644 index 000000000..2df70920b --- /dev/null +++ b/docs/Architecture/renderingSystem.md @@ -0,0 +1,312 @@ +# RenderingSystem — How It Works + +The rendering system's job is to take the current set of visible entities and turn them into pixels on screen every frame. It does this in two distinct phases: **pre-render compute** (GPU culling and sorting) followed by **a render graph** — a dependency-ordered DAG of passes that each write into shared textures until the final image lands on the drawable. + +The entry point is `UpdateRenderingSystem(in view: MTKView)`, called once per frame from the MTKView draw loop. + +--- + +## Step 0: The Visible Entity List + +Before any rendering begins, the system needs to know **which entities are visible**. This is managed through a triple-buffer called `tripleVisibleEntities`: + +```swift +visibleEntityIds = tripleVisibleEntities.snapshotForRead(frame: cullFrameIndex) +``` + +The key insight here is that `visibleEntityIds` is **not rebuilt from scratch each frame**. It is the result of the *previous frame's* GPU frustum cull — a compute pass that ran last frame and wrote its output into the triple-buffer. The current frame reads that result and uses it immediately. + +**Why triple-buffered?** The GPU may still be consuming last frame's cull output while the CPU is already preparing the next frame. Three slots prevent read/write races across overlapping frames. + +**While loading:** When `AssetLoadingGate.shared.isLoadingAny` is true, the snapshot step is skipped entirely. The last-known-good `visibleEntityIds` is reused. This prevents reading from ECS storage while asset loading is mutating it on a background thread. + +--- + +## Step 1: Command Buffer Slot Acquisition + +```swift +commandBufferSemaphore.wait() +renderInfo.currentInFlightFrameSlot = acquireUniformFrameSlot() +``` + +The engine allows at most **3 command buffers in flight** at once (matching the triple-buffer count). The semaphore blocks the CPU if the GPU is still consuming all three slots. + +`acquireUniformFrameSlot()` returns the index into the per-frame uniform buffer ring. Because the CPU writes entity transforms and camera matrices into these buffers while the GPU reads them, each in-flight frame needs its own slot to avoid corruption. + +--- + +## Step 2: Root Transform Propagation + +```swift +SceneRootTransform.shared.updateIfNeeded() +``` + +Before any uniforms are uploaded, dirty transforms are propagated down the scene graph. An entity whose parent moved needs its `WorldTransformComponent` updated before the model matrix is sent to the GPU. This runs lazily — only if something was marked dirty since the last frame. + +--- + +## Step 3: Pre-Render Compute Passes + +These three compute dispatches run **before any render encoder is opened**. They prepare data that the render passes will consume. + +### 3a. Frustum Culling → `performFrustumCulling(commandBuffer:)` + +A compute shader tests every entity's axis-aligned bounding box (`EntityAABB`) against the camera's 6 frustum planes. Entities outside the frustum are excluded. + +The result is written into `tripleVisibleEntities` — **for the next frame**. So culling is always one frame behind rendering. This is an intentional latency trade-off: GPU-driven culling is far faster than CPU culling, and one frame of lag is imperceptible. + +For XR, a reduce-scan variant runs the test against both eyes simultaneously. + +### 3b. Gaussian Depth → `executeGaussianDepth(commandBuffer)` + +For entities carrying a `GaussianComponent` (3D Gaussian splat data), a compute pass calculates the camera-space depth of each splat. This depth value is used as the sort key in the next step. + +### 3c. Bitonic Sort → `executeBitonicSort(commandBuffer)` + +A GPU bitonic sort reorders the Gaussian splats **back-to-front** by depth. Gaussian splats must be composited in this order for correct alpha blending. The sort runs entirely on the GPU and its output feeds directly into the Gaussian render pass later in the graph. + +--- + +## Step 4: Building the Render Graph → `buildGameModeGraph()` + +Rather than hard-coding a linear sequence of passes, the engine constructs a **directed acyclic graph (DAG)** of `RenderPass` nodes each frame: + +```swift +struct RenderPass { + let id: String + var dependencies: [String] + var execute: (MTLCommandBuffer) -> Void +} +``` + +Each pass declares which other passes must complete before it can run. `buildGameModeGraph()` assembles these nodes into a dictionary and returns it. Nothing executes yet — this is purely declarative. + +The full graph for a typical frame looks like this: + +``` +environment/grid + └── shadow + └── batchedShadow + └── model ──────────────────────────── gaussian + └── batchedModel │ + ├── ssao │ + └── lightPass │ + └── transparency │ + └── spatialDebug + └── [post-processing chain] + └── precomp ◄── (gaussian joins here) + └── look + └── outputTransform +``` + +### Base Pass (environment or grid) + +The graph always starts with a background pass whose type depends on the platform and rendering mode: + +| Context | Pass | Purpose | +|---|---|---| +| macOS/iOS with HDR sky | `environment` | Renders the IBL skybox cubemap | +| macOS/iOS without HDR | `grid` | Renders the editor debug grid | +| XR passthrough (mixed) | *(none)* | Camera feed is the background | +| XR full immersion | `environment` | Skybox inside the headset | + +This pass has **no dependencies** — it is always the root of the graph. + +### Shadow Passes + +``` +shadow → batchedShadow +``` + +Both passes render scene geometry from the **directional light's point of view** into a shadow map depth texture. No color is written — only depth. The renderer checks `entityToBatch` and routes each entity to the appropriate pass: +- Regular entities → `shadowExecution` +- Batched entities → `batchedShadowExecution` + +The shadow map produced here is consumed later by `lightPass`. + +### G-Buffer Passes (deferred rendering) + +``` +model → batchedModel → ssao → lightPass +``` + +This is the core of the deferred rendering pipeline. Entities do not produce a shaded color here — they write raw surface data into multiple render targets (the G-Buffer): + +- **Albedo** — base color +- **Normal** — world-space surface normal +- **World position** — reconstructed from depth +- **Material** — roughness, metalness, emissive flags + +`modelExecution` iterates `visibleEntityIds`. For each entity that is not batched: +- Binds vertex/index buffers +- Uploads the model matrix, normal matrix, and camera uniforms into the current in-flight frame slot +- Issues a draw call per mesh submesh + +`batchedModelExecution` follows the same logic for entities managed by the `BatchingSystem`, using merged buffers instead of per-entity ones. + +`ssaoOptimizedExecution` reads the G-Buffer normals and depth and produces a screen-space ambient occlusion texture. Blurring is handled internally — no separate blur nodes appear in the graph. + +`lightExecution` is where the entity **first appears fully lit**. It reads all four G-Buffer textures plus the shadow map and SSAO texture and combines them into a single HDR scene color texture using the deferred lighting algorithm. This is a full-screen quad pass — geometry is never touched again after the G-Buffer step. + +> **Why deferred?** Deferred rendering means the lighting cost scales with the number of lit pixels, not the number of geometry draw calls × number of lights. Complex scenes with many overlapping objects benefit greatly because each pixel is only shaded once, regardless of how many triangles projected onto it. + +### Transparency Pass + +```swift +RenderPass(id: "transparency", dependencies: ["lightPass"]) +``` + +Transparent materials cannot go through the G-Buffer — they require alpha blending which deferred rendering cannot express per-fragment. These entities are rendered **forward** in a separate pass on top of the deferred lit scene color. They depend on `lightPass` being complete so they composite correctly against the opaque scene. + +### Spatial Debug Pass + +```swift +RenderPass(id: "spatialDebug", dependencies: ["transparency"]) +``` + +Draws wireframe AABB overlays for debug purposes. Runs last in the geometry chain so it draws on top of everything. + +### Gaussian Pass + +```swift +RenderPass(id: "gaussian", dependencies: ["model"]) +``` + +Renders the back-to-front-sorted Gaussian splats using the indices produced by the bitonic sort. This pass **depends on "model"** because it needs the depth buffer that was populated during the G-Buffer model pass — splats use that depth to correctly composite against solid geometry. + +Note that Gaussian **does not** depend on `lightPass`, `transparency`, or the post-processing chain. It runs in parallel with those in the dependency graph and merges back at `precomp`. + +### Post-Processing Chain + +``` +spatialDebug → depthOfField → chromatic → bloomThreshold + → blur_hor_1 → blur_ver_1 → blur_hor_2 → blur_ver_2 + → bloomComposite → vignette +``` + +`postProcessingEffects()` builds this chain dynamically inside `buildGameModeGraph()`. Each effect reads from the previous pass's output texture and writes to its own. + +**Fast path:** If every effect (`BloomThresholdParams`, `VignetteParams`, `ChromaticAberrationParams`, `DepthOfFieldParams`) is disabled, the entire chain is replaced by a single bypass pass that points the post-process descriptor at the deferred output texture directly. This avoids allocating ~142 MB of intermediate render targets that would be unused. + +The number of blur iterations is driven by `BloomThresholdParams.shared.enabled` — when bloom is on, two horizontal/vertical pairs are dispatched; when off, zero. The loop that generates blur nodes in the graph is: + +```swift +for i in 0 ..< blurPassCount { + // horizontal blur pass + // vertical blur pass +} +``` + +So the graph topology literally changes based on whether bloom is enabled. + +### Pre-Composite Pass + +```swift +RenderPass(id: "precomp", dependencies: [postProcessID, gaussianPass.id]) +``` + +This is the **convergence point** of the two parallel tracks. The post-processed scene color and the Gaussian splat render both arrive here and are composited into a single texture. Neither track can be finalized without the other. + +### Look Pass (Color Grading) + +```swift +RenderPass(id: "look", dependencies: ["precomp"]) +``` + +Applies lift/gamma/gain color correction and optional LUT-based grading to the composited image. + +### Output Transform Pass + +```swift +RenderPass(id: "outputTransform", dependencies: ["look"]) +``` + +Tone maps the HDR scene color into the display's color space (SDR or EDR depending on the target). This is the **terminal node** of the graph — its output texture is what gets presented to the drawable. + +--- + +## Step 5: Graph Execution + +With the graph assembled, the engine sorts and executes it: + +```swift +let sortedPasses = try! topologicalSortGraph(graph: graph) +executeGraph(graph, sortedPasses, commandBuffer) +``` + +`topologicalSortGraph` performs a depth-first search over the dependency edges and returns a `[String]` of pass IDs in a valid execution order — every pass appears after all its dependencies. + +`executeGraph` iterates that list and calls each pass's `execute` closure, encoding Metal render or compute commands into the shared `commandBuffer`. All passes share one command buffer, so Metal can pipeline them efficiently on the GPU. + +--- + +## Step 6: HZB Depth Pyramid + +```swift +buildHZBDepthPyramid(commandBuffer) +``` + +After the render graph finishes, the depth texture produced during the G-Buffer model pass is downsampled into a **hierarchical Z-buffer** mip pyramid. This feeds **next frame's** occlusion culling — a coarse depth mip level can quickly reject large occluded objects before the fine cull. + +This is intentionally scheduled here, after the render graph and before `commit()`, so the HZB is built from the freshest depth available and ready for the next frame's culling compute dispatch. + +--- + +## Step 7: Present and Commit + +```swift +commandBuffer.present(drawable) +commandBuffer.commit() +``` + +The completion handler fires on the GPU thread when the command buffer finishes executing: + +- `commandBufferSemaphore.signal()` — frees one slot, allowing the CPU to encode the next frame +- `needsFinalizeDestroys = true` — deferred ECS entity removal can proceed safely now that the GPU is done with this frame's data +- `MemoryBudgetManager.shared.markUsed(entityIds:)` — records which entities were rendered so the memory budget manager knows what to keep resident and what to evict + +--- + +## The Full Frame in One Picture + +``` +[CPU] snapshotVisibleEntities (from last frame's cull) +[CPU] wait on semaphore / acquire uniform slot +[CPU] propagate dirty transforms + │ + ▼ +[GPU compute] frustumCulling → writes next frame's visibleEntityIds +[GPU compute] gaussianDepth → depth per splat +[GPU compute] bitonicSort → sort splats back-to-front + │ + ▼ +[CPU] buildGameModeGraph() → construct render pass DAG +[CPU] topologicalSortGraph() → linearize pass order + │ + ▼ +[GPU render] environment/grid +[GPU render] shadow + batchedShadow (depth from light POV) +[GPU render] model + batchedModel (entity → G-Buffer) +[GPU render] ssao (occlusion from G-Buffer) +[GPU render] lightPass (entity appears fully lit) +[GPU render] transparency (forward-rendered alphas) +[GPU render] spatialDebug (debug overlays) +[GPU render] gaussian (sorted splats) +[GPU render] post-processing chain (DOF, bloom, vignette) +[GPU render] precomp (merge scene + splats) +[GPU render] look (color grading) +[GPU render] outputTransform (tone map → drawable) +[GPU compute] buildHZB (depth pyramid for next frame) + │ + ▼ +[CPU] present drawable + commit +[GPU→CPU callback] signal semaphore, mark memory used +``` + +--- + +## Why a Render Graph Instead of a Fixed Pass Order? + +A fixed sequence of `if` statements works fine until the graph needs to change — when post-processing is disabled, when XR changes the base pass, or when the number of bloom blur iterations varies based on settings. A render graph makes these variations **declarative**: each pass states what it needs, and the topology sorts itself. Adding a new pass means adding one `RenderPass` node with its dependency list — the rest of the system adapts automatically. + +It also makes the dependency structure explicit and auditable. If a pass reads a texture produced by another pass, that relationship is encoded as a graph edge rather than buried in execution order assumptions. diff --git a/docs/Architecture/streamingCacheLifecycle.md b/docs/Architecture/streamingCacheLifecycle.md new file mode 100644 index 000000000..1dc5b5d59 --- /dev/null +++ b/docs/Architecture/streamingCacheLifecycle.md @@ -0,0 +1,170 @@ +# Streaming Cache Lifecycle + +This document describes how `GeometryStreamingSystem` and `MeshResourceManager` interact across the full load/unload lifecycle of a city block scene with 500 buildings. + +The clean division of responsibility: **`GeometryStreamingSystem` decides when**, **`MeshResourceManager` decides what's in memory**. The streaming system never touches GPU memory directly — it only calls `retain`, `release`, and `loadMesh` on the cache. + +--- + +## The Setup + +Each of the 500 buildings has a `StreamingComponent` attached. That component holds: +- `assetFilename` / `assetExtension` — points to the USDZ file +- `assetName` — the specific mesh name inside the file (e.g. `"building_42"`) +- `streamingRadius` — how close the camera must be to trigger a load +- `unloadRadius` — how far the camera must be to trigger an unload +- `state` — `.unloaded`, `.loading`, `.loaded`, or `.unloading` + +`MeshResourceManager` knows nothing about distance or cameras. It is purely a **cache + reference counter**. `GeometryStreamingSystem` is the one that decides when to load and unload. + +--- + +## Every Frame: `update(cameraPosition:deltaTime:)` + +This is called once per frame from the engine loop. It does two things before any load/unload work: + +```swift +currentFrame += 1 +MeshResourceManager.shared.currentFrame = currentFrame +``` + +The frame number is pushed into `MeshResourceManager` so its LRU timestamps stay current. Without this, the cache would have no sense of time and couldn't decide which files are stale. + +Updates are **throttled** — by default only run every 0.1 seconds to avoid doing spatial queries every frame. + +--- + +## Step 1 — Spatial Query (Who needs loading?) + +The system queries the **Octree** for all entities within `maxQueryRadius` (500m by default): + +```swift +let nearbyEntities = OctreeSystem.shared.queryNear(point: effectiveCameraPosition, radius: maxQueryRadius) +``` + +For each nearby entity, it calculates the camera distance and bins it: + +- **`.unloaded` + within `streamingRadius`** → goes into `loadCandidates` +- **`.loaded` + beyond `unloadRadius`** → goes into `unloadCandidates` + +Entities already `.loading` or `.unloading` are skipped — they're in progress. + +It also checks `loadedStreamingEntities` (a tracked set of currently-loaded entity IDs) for any loaded buildings that drifted **outside** the octree query radius, catching far-away stragglers the spatial query might miss. + +--- + +## Step 2 — Unloads First + +```swift +unloadCandidates.sort { lhs.1 > rhs.1 } // farthest first +``` + +Unloads are processed before loads to free memory before consuming more. Up to `maxUnloadsPerUpdate` (12 by default) are processed per tick to avoid frame spikes. + +Inside `unloadMesh(entityId:)`: + +```swift +MeshResourceManager.shared.release(entityId: entityId) +render.mesh = [] // clear reference, GPU data stays in cache +``` + +The key detail: `render.mesh` is cleared but **`cleanUp()` is NOT called on the meshes**. The GPU buffers stay alive in `MeshResourceManager`'s cache. Only `release()` is called, which decrements the ref count for that mesh. So if 10 buildings all used `"building_type_A"` and 3 get unloaded, the ref count drops from 10 → 7 and the GPU data stays put. + +The entity's state is set back to `.unloaded` and it's removed from `loadedStreamingEntities`. + +--- + +## Step 3 — Loads (Up to `maxConcurrentLoads`) + +```swift +loadCandidates.sort { /* high priority first, then closest */ } +let availableSlots = maxConcurrentLoads - activeLoadCountSnapshot() // default: 3 concurrent +``` + +For each candidate within the slot budget, `loadMesh(entityId:)` is called. This: + +1. Sets state to `.loading` +2. Notifies `BatchingSystem` that streaming started +3. Fires off an async `Task` + +Inside that async task, `loadMeshAsync` runs: + +```swift +guard let meshes = await MeshResourceManager.shared.loadMesh(url: url, meshName: meshName) else { ... } + +MeshResourceManager.shared.retain(url: url, meshName: meshName, for: entityId) +``` + +**`loadMesh` is a cache-first call.** If `city_block.usdz` is already cached (because another building from the same file loaded first), this returns instantly — no disk I/O. If it's not cached, the single-flight gate ensures only one task parses the USDZ even if 50 buildings request it simultaneously. + +After `retain`, the mesh data is **copied** for this entity: + +```swift +var entityMeshes = meshes.map { $0.copyWithNewUniformBuffers() } +``` + +This is critical: the cached mesh is shared, but each entity needs its **own uniform buffers** (transform matrices, material data). Without this, 500 buildings would overwrite each other's render data every frame. + +The mesh is assigned to the entity's `RenderComponent`, and the entity is registered with `MemoryBudgetManager`. + +Back on the main thread (inside `withWorldMutationGate`), state is set to `.loaded` and the entity is added to `loadedStreamingEntities`. + +--- + +## Step 4 — Memory Pressure Eviction + +```swift +if MemoryBudgetManager.shared.shouldEvict() { + evictedByLRU = evictLRU() +} +``` + +`evictLRU()` has two stages: + +**Stage 1** — sweep the cache for zero-ref files: +```swift +MeshResourceManager.shared.evictUnused() +``` +Any USDZ file with `totalRefCount == 0` has its GPU buffers freed immediately. + +**Stage 2** — if memory pressure remains, walk loaded entities sorted by `lastVisibleFrame` (oldest first): +```swift +candidates.sort { $0.1 < $1.1 } // oldest lastVisibleFrame first +``` + +For each candidate (skipping currently visible entities), `unloadMesh` is called, which decrements ref counts. Once a file's ref count hits zero it becomes eligible for `evictUnused` on the next pass. + +--- + +## The Full Interaction Picture + +``` +GeometryStreamingSystem MeshResourceManager +───────────────────────────────────────────────────────────────── +update() every 0.1s + │ + ├─ currentFrame++ ──────────────────────► currentFrame = N (LRU clock) + │ + ├─ Octree query → loadCandidates + │ + ├─ For each unload candidate: + │ unloadMesh() + │ └─ release(entityId) ────────────► refCount["building_42"]-- + │ render.mesh = [] (GPU data stays in cache) + │ + ├─ For each load candidate (≤3 concurrent): + │ loadMesh() → async Task + │ └─ loadMeshAsync() + │ ├─ loadMesh(url, meshName) ──► cache hit? return instantly + │ │ cache miss? parse USDZ once, + │ │ wake all waiters + │ ├─ retain(url, name, id) ────► refCount["building_42"]++ + │ │ entityToMesh[e042] = (url, name) + │ └─ mesh.copyWithNewUniformBuffers() + │ → entity gets its own render buffers + │ + └─ If memory pressure: + evictLRU() + ├─ evictUnused() ───────────────► free GPU buffers for refCount==0 files + └─ unloadMesh() on oldest entities (same as above) +``` diff --git a/docs/Architecture/textureStreamingSystem.md b/docs/Architecture/textureStreamingSystem.md new file mode 100644 index 000000000..bcfa78d60 --- /dev/null +++ b/docs/Architecture/textureStreamingSystem.md @@ -0,0 +1,401 @@ +# Texture Streaming System + +`TextureStreamingSystem.swift` dynamically adjusts the resolution of textures on entities based on their distance from the camera. Instead of keeping every texture at full resolution all the time, it streams textures up or down as the player moves through the scene — saving GPU memory while keeping nearby geometry crisp. + +--- + +## Scenario: A City Block with 500 Buildings + +Imagine a USDZ scene with a city block containing 500 buildings. Each building has a `RenderComponent` with meshes and submeshes, and each submesh has a `Material` containing up to four PBR textures: + +- **Base Color** (sRGB) +- **Roughness** (linear) +- **Metallic** (linear) +- **Normal** (linear) + +At full resolution, each building's textures might be 2048×2048 or larger. Loading all 500 buildings at full resolution at once would immediately exhaust GPU memory, causing frame drops or crashes. + +The `TextureStreamingSystem` solves this by managing three quality tiers and promoting/demoting each building's textures as the camera moves. + +--- + +## Quality Tiers + +The system operates with three tiers, controlled by two distance thresholds: + +| Tier | Condition | Max Dimension | +|------|-----------|---------------| +| **Full** | `distance <= upgradeRadius` (default 4m) | Native source resolution (nil cap) | +| **Medium** | `upgradeRadius < distance <= downgradeRadius` (default 12m) | `maxTextureDimension` (1024px on macOS, 768px on visionOS) | +| **Minimum** | `distance > downgradeRadius` | `minimumTextureDimension` (256px on macOS, 192px on visionOS) | + +On first import, the `ProgressiveAssetLoader` caps all textures at the medium dimension. The streaming system then upgrades/downgrades from there. + +--- + +## The Update Loop + +Every frame, the game loop calls: + +```swift +TextureStreamingSystem.shared.update(cameraPosition: ..., deltaTime: ...) +``` + +**Throttle:** The system only does real work every `updateInterval` seconds (default 0.2s). This prevents spending every frame scanning all entities. + +**Concurrency cap:** At most `maxConcurrentOps` (default 3) async streaming operations run simultaneously. If all slots are busy, the tick exits early. + +``` +Frame N arrives + └─ timeSinceLastUpdate += deltaTime + └─ if < 0.2s → return (skip this frame) + └─ availableSlots = 3 − activeOps.count + └─ if 0 slots → return +``` + +--- + +## Priority Pass 1: Visible Entities + +The system gets the current list of visible entity IDs (from the scene's frustum culling or visibility tracking) and iterates them first: + +``` +For each visible entity: + 1. Calculate distance from camera to entity's world-space bounding box center + 2. Determine desired tier via desiredMaxDimension(distance:) + 3. Build work items — which textures actually need to change + 4. If work items exist → scheduleResolutionChange(...) + 5. Decrement available slots +``` + +**City block example:** The camera is standing on the sidewalk in front of Building #42. Buildings #42 and #43 are within 4m (full tier). Buildings #44–#60 are within 12m (medium tier). The remaining 440 buildings are beyond 12m (minimum tier). + +On this tick, the three available slots might be assigned to: +- Building #42: upgrade base color from 1024px → full resolution (2048px) +- Building #43: upgrade roughness from 1024px → full resolution +- Building #57: already at 1024px medium — no change needed, slot freed + +--- + +## Priority Pass 2: Upgraded-but-Not-Visible Entities + +After handling visible entities, the system checks its `upgradedEntities` set — entities whose textures are currently above the minimum tier. Even if they're off-screen now (the camera rotated away), they may still be nearby and deserve high-res textures so there's no quality drop when the camera rotates back. + +``` +For each entity in upgradedEntities that is NOT in the visible set: + 1. Calculate actual distance (do not assume minimum) + 2. Determine desired tier + 3. Build work items + 4. Schedule if needed +``` + +**City block example:** The camera rotated 90°, so Building #42 left the frustum. It is still 3m away. The system sees it in `upgradedEntities`, computes distance = 3m, desired tier = full — no downgrade is needed. It stays tracked. + +If the camera then walks 20m away, Building #42's distance becomes 20m > 12m, so the system schedules a downgrade from full → 256px minimum. + +--- + +## Building Work Items + +`buildWorkItems(entityId:targetMaxDimension:)` inspects every texture slot on the entity's meshes and filters to only the ones that actually need to change: + +``` +For each mesh → submesh → material: + For each texture type (baseColor, roughness, metallic, normal): + currentMax = max(currentTexture.width, currentTexture.height) + desiredMax = min(targetMaxDimension, sourceMaxDimension) + + if currentMax == desiredMax → skip (already correct) + if upgrading but source is no bigger than current → skip + + else → emit StreamWorkItem(slot, direction, targetMaxDimension) +``` + +Each `StreamWorkItem` carries: +- The mesh/submesh index to know where to write back +- The direction (`.upgrade` or `.downgrade`) +- The target max dimension (`nil` means full source resolution) +- The texture source: either an `MDLTexture` object or a `URL` on disk + +--- + +## Scheduling: The Async Task + +`scheduleResolutionChange(...)` is where the real work happens — but critically it happens **off the main thread**: + +``` +1. reserveOp(entityId) — mark entity as busy, return false if already active +2. Initialize MTLCommandQueue and MTKTextureLoader once (reused across ticks) +3. Spawn a Swift Task (async, off main thread) +``` + +Inside the `Task`, for each work item: + +### Upgrade Path + +``` +loadSourceTexture(source, isSRGB:, loader:) + └─ MTKTextureLoader loads the original MDLTexture or URL from disk + └─ options: shaderRead | pixelFormatView, generateMipmaps: true, SRGB flag + └─ Returns a full-resolution MTLTexture + +resampleTextureIfNeeded(sourceTexture, targetMaxDimension:, commandQueue:) + └─ if targetMaxDimension == nil → return texture as-is (full res) + └─ else → GPU downsample to targetMaxDimension +``` + +### Downgrade Path + +``` +resampleTextureIfNeeded(currentTexture, targetMaxDimension:, commandQueue:) + └─ GPU downsample the already-loaded texture to targetMaxDimension + └─ No disk I/O needed — current texture is the source +``` + +### GPU Resampling (`downsampleTexture`) + +``` +1. Compute target dimensions preserving aspect ratio +2. Allocate new MTLTexture (private storage, mipmapped) +3. Encode MPSImageBilinearScale → bilinear downsample +4. Encode BlitCommandEncoder.generateMipmaps(for:) +5. commit() and await completion via CheckedContinuation +``` + +Using `MPSImageBilinearScale` means the downsampled texture is high quality (bilinear filtering by the GPU shader), and the full mip chain is generated immediately so the renderer can use the appropriate mip level right away. + +--- + +## Applying Results Back to ECS + +After all textures in the task are loaded/resampled, execution returns to the main thread via `await MainActor.run { withWorldMutationGate { ... } }`: + +``` +For each LoadedTexture: + 1. textureViewMatchingSRGB(texture, wantSRGB:) + └─ creates a MTLTextureView with sRGB or linear pixel format + without copying pixel data (zero cost) + + 2. updateMaterial(entityId:meshIndex:submeshIndex:) { material in + // Three-tier level: .full (nil cap), .capped (medium), .minimum + let streamLevel: TextureStreamingLevel = item.targetMaxDimension == nil + ? .full + : (item.targetMaxDimension! <= capturedMinimumDim ? .minimum : .capped) + material.baseColor.texture = item.texture + material.baseColorStreamingLevel = streamLevel + // (same for roughness, metallic, normal) + } + + 3. BatchingSystem.shared.updateBatchMaterialInPlace(for: entityId) { batchMaterial in + // Mirror the same three-tier level into the batch group's representative + // material so the new texture is visible on the next frame with zero batch churn + } +``` + +The `withWorldMutationGate` wrapper ensures the ECS is not mutated mid-render. The `BatchingSystem` update ensures batched draw calls reflect the new texture without rebuilding the batch. + +After applying, the entity's membership in `upgradedEntities` is updated: if any texture is still above the minimum tier, the entity stays tracked. + +--- + +## The sRGB View + +`textureViewMatchingSRGB` handles a subtle correctness issue: after GPU resampling, the output texture may have a linear pixel format even if the original was sRGB (e.g., `rgba8Unorm` instead of `rgba8Unorm_srgb`). Rather than re-encoding with the correct format, the system creates a `MTLTextureView` — a zero-copy reinterpretation of the same underlying memory with the correct format. This costs nothing in GPU memory. + +--- + +## Full Walk-Through: Building #42 Goes from Far to Near + +| Event | Action | +|-------|--------| +| Scene loads | All 500 buildings loaded at 1024px (medium tier) by ProgressiveAssetLoader | +| Camera 30m away from Building #42 | distance > 12m → desired = 256px; downgrade scheduled | +| Downgrade task runs | GPU resamples current 1024px → 256px; applied to ECS + batch | +| Building #42 removed from `upgradedEntities` | (256px = minimum, not tracked) | +| Camera walks to 10m away | distance 10m → desired = 1024px medium; upgrade scheduled | +| Upgrade task runs | Loads MDLTexture from source → 1024px; applied to ECS + batch | +| Building #42 added to `upgradedEntities` | (1024px > minimum) | +| Camera walks to 3m away | distance 3m ≤ 4m → desired = nil (full); upgrade scheduled | +| Upgrade task runs | Loads MDLTexture → full 2048px, no GPU resample needed; applied | +| Camera walks away to 15m | distance 15m > 12m → desired = 256px; downgrade scheduled | +| Downgrade task runs | GPU resamples 2048px → 256px; applied; entity removed from tracking | + +At no point are more than 3 buildings being streamed simultaneously, keeping GPU command submission predictable. + +--- + +## Threading Model + +| Thread | What happens there | +|--------|-------------------| +| Main / game loop | `update()` called; distance math; `buildWorkItems`; `reserveOp`; resource init | +| Swift Task (async) | Disk I/O (`MTKTextureLoader`); GPU encode + await (`MPSImageBilinearScale`) | +| MainActor | ECS mutation (`updateMaterial`); batch update; `upgradedEntities` bookkeeping | + +`activeOps` and `upgradedEntities` are protected by `NSLock`. The command queue and texture loader are initialized once on the main thread before any `Task` is spawned, then captured as local constants — no concurrent access to instance state from async tasks. + +--- + +## Memory Relief: `shedTextureMemory` + +`TextureStreamingSystem` exposes a public method for on-demand texture downgrade under memory pressure: + +```swift +@discardableResult +public func shedTextureMemory(cameraPosition: simd_float3, maxEntities: Int = 4) -> Int +``` + +This is called by `GeometryStreamingSystem` — not on a timer, but reactively whenever combined GPU memory (mesh + texture) hits the 85% high-water mark. It bypasses the normal distance-band schedule and forces immediate action. + +**What it does:** +1. Snapshots `upgradedEntities` — the set of entities currently holding textures above `minimumTextureDimension` +2. Calculates the camera distance for each +3. Sorts **farthest-first** — the least visually valuable textures at their current resolution get downgraded first +4. Schedules up to `maxEntities` force-downgrades to `minimumTextureDimension`, skipping any entity already in an active op +5. Returns the number of entities scheduled + +**Why farthest-first?** A distant entity's 1024 px texture dropping to 256 px is nearly invisible. A nearby entity's texture downgrading would be immediately obvious. This ordering gives the maximum memory relief for the minimum perceptible quality loss. + +**Relationship to the update loop:** Normal `update()` ticks also schedule downgrades for out-of-range entities, but only as slots become available and on the 0.2 s timer. `shedTextureMemory` is a burst — it fills up to `maxEntities` slots immediately, regardless of the timer, to respond to pressure before the next geometry load attempt. + +### When it is called + +| Caller | `maxEntities` | Condition | +|---|---|---| +| `GeometryStreamingSystem.update()` | 4 | Combined pressure high, geometry pressure low — texture relief only, no geometry eviction | +| `GeometryStreamingSystem.update()` | 8 | Geometry pressure also high — shed texture first, then evict geometry | + +The larger batch size (8) when geometry is also under pressure reflects that more aggressive texture shedding is needed before the costlier geometry eviction path runs. + +--- + +## Tuning Profiles + +Apply a built-in profile at scene init instead of setting every property individually: + +```swift +TextureStreamingSystem.shared.apply(.archviz) // indoor archviz +TextureStreamingSystem.shared.apply(.openWorld) // large outdoor scenes +TextureStreamingSystem.shared.apply(.balanced) // general-purpose default +``` + +Individual properties can be overridden after applying a profile: + +```swift +TextureStreamingSystem.shared.apply(.archviz) +TextureStreamingSystem.shared.upgradeRadius = 3.0 // widen full-res zone +``` + +| Profile | `upgradeRadius` | `downgradeRadius` | `minDim` | `maxConcurrentOps` | Best for | +|---|---|---|---|---|---| +| `.archviz` | 2.5 m | 6.0 m | 512 px | 6 | Living rooms, kitchens, offices | +| `.openWorld` | 15.0 m | 60.0 m | 256 px | 3 | Cities, landscapes, terrain | +| `.balanced` | 12.0 m | 20.0 m | platform default | 3 | Mixed / unknown scene type | + +**Archviz rationale:** rooms are 4–7 m deep, so "distant" objects are still large on screen. The minimum tier is raised to 512 px (from the engine default of 256 px) because 256 px looks visibly compressed on a wall or floor texture at 5 m. `maxConcurrentOps = 6` is safe here because archviz streaming ops are GPU-bound (no cold disk I/O on the warm path). + +**Open-world rationale:** tiers are spread across a city-block scale. The minimum tier stays at 256 px because objects beyond 60 m occupy very few pixels. Keeping `maxConcurrentOps = 3` avoids GPU memory spikes when hundreds of entities enter range simultaneously. + +--- + +## Key Configuration + +```swift +TextureStreamingSystem.shared.upgradeRadius = 4.0 // meters: go full-res inside this +TextureStreamingSystem.shared.downgradeRadius = 12.0 // meters: go minimum beyond this +TextureStreamingSystem.shared.maxTextureDimension = 1024 +TextureStreamingSystem.shared.minimumTextureDimension = 256 +TextureStreamingSystem.shared.updateInterval = 0.2 // seconds between evaluations +TextureStreamingSystem.shared.maxConcurrentOps = 3 // parallel streaming tasks +TextureStreamingSystem.shared.hysteresisFraction = 0.15 // dead-band fraction at tier boundaries +TextureStreamingSystem.shared.verboseLogging = true // log each up/downgrade +``` + +--- + +## Hysteresis Dead Band + +Without hysteresis, an entity hovering exactly at a tier boundary (e.g. `downgradeRadius = 20 m`) oscillates between tiers on alternate streaming ticks, causing mip-map flicker on distant meshes. + +`hysteresisFraction` (default `0.15`) applies an asymmetric dead band at each tier boundary: + +| Transition | Triggers at | +|---|---| +| Upgrade to full (boundary: `upgradeRadius`) | `distance < upgradeRadius × (1 − h)` | +| Downgrade from full | `distance > upgradeRadius × (1 + h)` | +| Upgrade to medium (boundary: `downgradeRadius`) | `distance < downgradeRadius × (1 − h)` | +| Downgrade to minimum | `distance > downgradeRadius × (1 + h)` | + +At defaults (`upgradeRadius = 12 m`, `downgradeRadius = 20 m`, `h = 0.15`): +- Full ↔ medium transition: upgrade at < 10.2 m, downgrade at > 13.8 m +- Medium ↔ minimum transition: upgrade at < 17 m, downgrade at > 23 m + +`shedTextureMemory` always bypasses hysteresis (passes `Float.greatestFiniteMagnitude` as distance) so memory-pressure downgrades are never suppressed. + +--- + +## Bootstrap Tier Alignment + +`TextureLoader.defaultMaxTextureDimension` (set in `Mesh.swift`) is aligned to `TextureStreamingSystem.platformDefaultMinimumTextureDimension`: + +- **visionOS:** 192 px +- **macOS / iOS:** 256 px + +This ensures every freshly loaded entity starts at the streaming system's minimum tier. The streaming system then only **upgrades** as the camera approaches — it never issues an immediate downgrade on a newly-loaded entity (which would have been visible as a resolution pop on the first frame the entity appeared). + +--- + +## TextureLoader Cache Key Design + +`TextureLoader` (the private helper class in `Mesh.swift`) maintains a per-instance GPU texture cache keyed by `TextureCacheKey(id: String, isSRGB: Bool)`. Two possible key strategies are used depending on what information ModelIO provides: + +### Priority 1 — Bracket-notation path (safe for deduplication) + +When `property.stringValue` contains a parseable USDZ bracket path +(e.g. `"file:///scene.usdz[0/floor_albedo.png]"`), the key is: +`usdz-embedded://0/floor_albedo.png` + +This is unique per physical embedded file. Two materials that reference the **same** embedded texture file correctly share one GPU texture via this key. + +### Priority 2 — Object identity (safe from collision) + +When bracket notation is absent, the key is the MDLTexture object's memory address: +`mdl-obj-` + +This is used because the name-based fallback (`usdz-embedded://GameData/embedded_Basecolor_map`) is **not** safe as a cache key: multiple genuinely different materials can map to the same synthetic name when their MDLTexture objects have an empty `.name` property. Using a shared cache entry for different physical textures causes some meshes to display the wrong texture on first load. + +Sharing still works correctly: two code paths that hold a reference to the **same** MDLTexture object (same pointer) get the same cache key and share one GPU texture, which is the intended deduplication. + +### outputURL vs. cacheKeyURL + +Both values use the same strategy: bracket URL when available, object-identity URL otherwise. + +| Field | Value | Used by | +|---|---|---| +| `cacheKeyURL` | Bracket URL or object-identity URL | GPU `textureCache` lookup only | +| `outputURL` → `material.baseColorURL` | Bracket URL or object-identity URL | `BatchingSystem.getMaterialHash`, `TextureStreamingSystem` source reference | + +**Why both use object identity when bracket notation is absent:** + +The name-based fallback (`usdz-embedded://scene.usdz/embedded_Basecolor_map`) is the same string for every unnamed texture from the same USDZ, regardless of its actual pixel content. `BatchingSystem.normalizeTextureURL` then strips the asset-scope host, collapsing all unnamed textures to the same token (`usdz-embedded://embedded_Basecolor_map`). This causes `getMaterialHash` to produce the same hash for entities with genuinely different textures, grouping them into one batch and rendering all of them with the first entity's GPU texture — the wrong texture on every other entity. + +Using object identity for `outputURL` as well means: +- Same MDLTexture pointer → same physical texture → same `material.baseColorURL` → same batch hash → share a batch group ✓ +- Different MDLTexture pointers → different physical textures → different `material.baseColorURL` → different batch hash → separate batch groups ✓ + +The MDLAsset is kept alive in `ProgressiveAssetLoader.rootAssetRefs` for the entity's lifetime, so MDLTexture pointers are stable across warm eviction/re-upload cycles. + +### Diagnostic Logging + +Set `textureCacheLoggingEnabled = true` before loading to trace every cache hit/miss: + +```swift +// Enable before calling setEntityMeshAsync +textureCacheLoggingEnabled = true +``` + +Each log line contains: `HIT/MISS`, cache key, key source (`bracket` / `obj-identity(unnamed)` / `obj-identity(named-no-bracket)`), MDLTexture pointer, texture name, map type, and isSRGB flag. + +**Before the fix:** you would see `HIT` entries where the same key is reused across different MDLTexture object identities for base-color textures — the collision. + +**After the fix:** each unnamed/no-bracket texture gets its own `obj-identity` key; `HIT` entries are only seen when the same MDLTexture object is referenced by multiple materials (correct sharing). diff --git a/docs/Architecture/xrRenderingSystem.md b/docs/Architecture/xrRenderingSystem.md new file mode 100644 index 000000000..419564547 --- /dev/null +++ b/docs/Architecture/xrRenderingSystem.md @@ -0,0 +1,257 @@ +# XR Rendering System — How It Works + +The XR rendering system is the visionOS counterpart to `UpdateRenderingSystem`. It drives the same deferred render graph used on macOS, but within a completely different frame lifecycle imposed by **CompositorServices** — Apple's low-latency compositor for spatial computing. + +The entry point is `UntoldEngineXR`, a class that owns the render loop, the ARKit session, and the spatial input bridge. Everything in this file is compiled only on visionOS (`#if os(visionOS)`). + +--- + +## Why XR Rendering Is Different + +On macOS, the MTKView delegate calls `draw(in:)` on the main thread at display refresh rate. The engine owns the timing. + +On visionOS, **the compositor owns the timing**. It tells you when to render, provides per-eye textures, and requires you to attach a device anchor (head pose) to every frame before presenting. If you miss the deadline or present without an anchor, the compositor either drops your frame or logs a warning. The `UntoldEngineXR` run loop is specifically structured to satisfy these compositor requirements frame by frame. + +--- + +## Step 0: Initialization + +```swift +UntoldEngineXR(layerRenderer: LayerRenderer, device: MTLDevice) +``` + +At init time, three things happen in parallel: + +**ARKit session startup** (async Task): Queries world sensing authorization, then launches `WorldTrackingProvider` and `PlaneDetectionProvider`. World tracking is what gives you the device anchor — the head pose needed to render correctly in the user's space. If world sensing is denied (e.g., the user blocked it in Settings), the engine still runs with world tracking only so rendering doesn't break, it just has no plane data. + +**Plane monitor** (background Task): A long-running Swift structured concurrency task that consumes the `planeDetection.anchorUpdates` async stream. Every time the system detects, updates, or removes a real-world surface (floor, wall, table, etc.), it maps the ARKit classification to the engine's `RealSurfaceKind` enum and forwards it to `RealSurfacePlaneStore`. Game code queries this store to snap objects to real surfaces. + +**Renderer creation**: `UntoldRenderer.createXR(...)` initializes the Metal device, command queue, G-Buffer textures, pipeline states, and all other GPU resources at the fixed visionOS viewport size (2048 × 1984 per eye). + +--- + +## Step 1: The Run Loop + +`runLoop()` is called from a dedicated background thread (the compositor render thread) and runs for the lifetime of the XR session: + +```swift +while true { + switch layerRenderer.state { + case .paused: layerRenderer.waitUntilRunning() + case .running: renderNewFrame() + case .invalidated: break // exit + } +} +``` + +The `.paused` state blocks the thread cheaply until the compositor is ready — this happens when the user puts the app in the background or when the system needs to reclaim resources. The `.invalidated` state is the clean shutdown signal. + +**Why a background thread?** The compositor render thread must never be blocked by Swift's main actor or UIKit layout passes. Running here ensures that Metal encoding proceeds at compositor frame rate (90 FPS on Vision Pro) without contention. + +--- + +## Step 2: The Per-Frame Lifecycle — `renderNewFrame()` + +CompositorServices frames follow a strict protocol. Every call to `renderNewFrame()` must progress through these phases in order: + +### 2a. Query + Predict + +```swift +let frame = layerRenderer.queryNextFrame() +let timing = frame.predictTiming() +``` + +`queryNextFrame()` dequeues the next compositor frame. `predictTiming()` returns `optimalInputTime` — the deadline by which you must finish reading input and preparing CPU-side data for the frame. These are not suggestions; missing them causes judder. + +### 2b. Update Phase + +```swift +frame.startUpdate() +// ... do CPU work ... +frame.endUpdate() +``` + +Everything between `startUpdate` and `endUpdate` is CPU-side frame preparation: + +- **Progressive loading tick**: `ProgressiveAssetLoader.shared.tick()` is dispatched to the main thread via `DispatchQueue.main.async`. The run loop lives on the compositor thread, but `tick()` requires `@MainActor`. This mirrors what `UntoldEngine.swift`'s `draw()` does on macOS. + +- **Spatial input processing**: `updateSpatialInputState()` drains the queued `XRSpatialInputSnapshot` events and updates the `InputSystem`. If assets are loading or the scene isn't ready, input is cleared instead to avoid acting on stale state. + +- **Game update**: `renderer.updateXR()` calls the user's `gameUpdate` and `handleInput` callbacks. This is where game logic runs — entity movement, animation state machines, physics steps. It is skipped entirely while `AssetLoadingGate.shared.isLoadingAny` is true. + +### 2c. Wait for Optimal Input Time + +```swift +LayerRenderer.Clock().wait(until: timing.optimalInputTime, tolerance: .zero) +``` + +The thread sleeps until the compositor says it's the best moment to submit GPU work. Submitting too early wastes GPU time on a stale pose; submitting too late misses the scanline. This one call is what makes visionOS rendering feel low-latency. + +### 2d. Submission Phase + +```swift +frame.startSubmission() +defer { frame.endSubmission() } +``` + +The `defer` is important: `endSubmission()` **must** be called even if rendering fails partway through. If it isn't called, the compositor stalls. The `defer` guarantees this regardless of how the function exits. + +### 2e. Device Anchor Acquisition + +```swift +let deviceAnchor = worldTracking.queryDeviceAnchor(atTimestamp: presentationTimeCA) +drawable.deviceAnchor = anchor +``` + +The device anchor is the head pose — a 4×4 transform from world space to the device. The compositor requires it to be attached to the drawable before presenting; without it, the system can't reproject the frame correctly for the user's eyes. + +The engine queries the anchor at **presentation time** (the future moment when the frame will appear on screen), not at "now". This is predictive — it compensates for the latency between encoding and display by predicting where the head will be. + +**Resilience strategy**: ARKit can occasionally return `nil` for the anchor (e.g., tracking hiccup, recovery). A three-level fallback prevents dropped frames: +1. Query at predicted presentation time → use it if valid +2. Query at "now" as a retry → use it if valid +3. Fall back to `lastValidDeviceAnchor` — the last anchor that was valid + +The engine never skips presenting a drawable once it has been dequeued. Even with no anchor at all, the drawable is presented (the compositor handles it gracefully); skipping would cause a more disruptive compositor error. + +--- + +## Step 3: GPU Encoding — `executeXRSystemPass()` + +This is where the actual Metal work happens, split into three parts. + +### 3a. Pre-Render Compute (runs once for both eyes) + +```swift +performFrustumCulling(commandBuffer: commandBuffer) +executeGaussianDepth(commandBuffer) +executeBitonicSort(commandBuffer) +``` + +These are the **same three compute passes** as the macOS path: frustum cull, Gaussian depth, bitonic sort. The key difference is they run **once per frame**, not once per eye. The culled visibility list and sorted splat indices produced here are reused by both the left and right eye render passes. + +> **Why only once?** Running culling and sorting twice — once per eye — at 90 FPS would double the compute budget for work that produces nearly identical results (the two eyes are only ~65mm apart). One cull pass with a slightly conservative frustum covers both views. + +### 3b. Per-Eye Render Loop + +```swift +for (viewIndex, view) in drawable.views.enumerated() { + // compute view and projection matrices + // configure pass descriptor + // call renderer.renderXR(...) +} +``` + +The drawable provides two views (left eye, right eye). For each: + +**View matrix construction**: +```swift +let cameraMatrix = simd_inverse(originFromDevice * deviceFromView) +``` +`originFromDevice` is the world-to-device transform from the anchor. `deviceFromView` is the eye offset relative to the device center (the IPD offset). Multiplying them gives world-to-eye, then inverting gives the view matrix the shaders expect. + +**Projection matrix**: +```swift +let projection = drawable.computeProjection(convention: .rightUpBack, viewIndex: viewIndex) +``` +The compositor provides the exact asymmetric projection for each eye. This accounts for the different FOV angles per eye and the physical lens geometry of the headset — it cannot be constructed manually. + +**Pass descriptor**: Pre-allocated (`passDescriptorLeft`, `passDescriptorRight`) and reused every frame to avoid 180 allocations/second (2 eyes × 90 FPS). The color and depth textures are swapped in from `drawable.colorTextures[viewIndex]` and `drawable.depthTextures[viewIndex]`. + +**`renderer.renderXR(...)`** calls `buildGameModeGraph()` + `topologicalSortGraph()` + `executeGraph()` — the exact same render graph pipeline as macOS. The only thing that changes is: +- `renderInfo.currentEye = viewIndex` — tells uniform uploads which eye's matrices to use +- The base pass mode: `.mixed` immersion omits the base pass (camera passthrough is the background), `.full` immersion renders the skybox + +### 3c. HZB Pyramid (built once after both eyes) + +```swift +buildHZBDepthPyramid(commandBuffer) +``` + +After both eyes are encoded into the same command buffer, the HZB depth pyramid is built from the depth texture of the **last eye rendered**. In stereo, this mono pyramid is used for both per-eye occlusion tests in the next frame's frustum cull. + +**Why after both eyes, not per eye?** Building HZB per eye would double the cost and produce two pyramids that next frame's single-dispatch cull can't easily consume. The right eye's depth is a reasonable approximation for the combined scene. + +### 3d. Present and Commit + +```swift +drawable.encodePresent(commandBuffer: commandBuffer) +commandBuffer.commit() +``` + +Note `encodePresent` vs. macOS's `commandBuffer.present(drawable)`. On visionOS, the present must be **encoded into the command buffer** as a GPU command so the compositor can precisely time when the drawable lands on screen relative to GPU work completion. It is not a CPU-side call. + +The completion handler signals `commandBufferSemaphore` when the GPU finishes, freeing a slot for the next frame. + +--- + +## Spatial Input Bridge + +`configureSpatialEventBridge()` registers a closure on `layerRenderer.onSpatialEvent`. Every time the compositor fires a pinch gesture or spatial tap, the closure: + +1. Extracts the **selection ray** — origin and direction in world space — from the event's `selectionRay` field +2. Extracts the **input device pose** (hand position and orientation) if available +3. Maps the CompositorServices phase (`.active`, `.ended`, `.cancelled`) to the engine's `XRSpatialInteractionPhase` +4. Packs everything into an `XRSpatialInputSnapshot` and enqueues it in `InputSystem` + +The snapshot is processed on the next frame's update phase by `spatialGestureRecognizer.updateSpatialInputState()`, which converts raw ray/phase sequences into higher-level gesture events (tap, hold, drag) that game code can query. + +> The bridge is gated on `isSceneReady()` and `!AssetLoadingGate.shared.isLoadingAny`. Input events while loading are discarded to prevent game code from acting on uninitialized entities. + +--- + +## The Full XR Frame in One Picture + +``` +[Compositor thread] runLoop() → renderNewFrame() + │ + ├─ queryNextFrame() + predictTiming() + ├─ frame.startUpdate() + │ ├─ [Main thread async] ProgressiveAssetLoader.tick() + │ ├─ updateSpatialInputState() (drain gesture queue) + │ └─ renderer.updateXR() (gameUpdate + handleInput) + ├─ frame.endUpdate() + ├─ wait(until: optimalInputTime) (sleep until compositor deadline) + ├─ frame.startSubmission() + ├─ queryDrawable() (get per-eye textures) + ├─ queryDeviceAnchor() → fallback chain → drawable.deviceAnchor = anchor + │ + └─ executeXRSystemPass() + │ + ├─ [GPU compute] frustumCulling (once, covers both eyes) + ├─ [GPU compute] gaussianDepth + ├─ [GPU compute] bitonicSort + │ + ├─ for eye in [left, right]: + │ ├─ compute view matrix (originFromDevice × deviceFromView)⁻¹ + │ ├─ compute projection (drawable.computeProjection) + │ ├─ configure passDescriptor (color + depth textures) + │ └─ renderer.renderXR() → buildGameModeGraph() + │ topologicalSortGraph() + │ executeGraph() + │ [same DAG as macOS] + │ + ├─ [GPU compute] buildHZBDepthPyramid (once, after both eyes) + ├─ drawable.encodePresent(commandBuffer) + └─ commandBuffer.commit() + │ + └─ [GPU→thread callback] semaphore.signal() + │ + └─ frame.endSubmission() +``` + +--- + +## Key Differences From the macOS Path + +| | macOS (`UpdateRenderingSystem`) | visionOS (`executeXRSystemPass`) | +|---|---|---| +| Frame timing | MTKView drives at display rate | CompositorServices dictates via `optimalInputTime` | +| Eyes | 1 | 2 (per-eye loop over `drawable.views`) | +| Compute passes | Once per frame | Once per frame (shared across both eyes) | +| View matrix | Camera entity transform | `(originFromDevice × deviceFromView)⁻¹` from ARKit anchor | +| Projection | Camera component FOV | `drawable.computeProjection()` — asymmetric per-eye | +| Present call | `commandBuffer.present(drawable)` | `drawable.encodePresent(commandBuffer:)` — encoded as GPU command | +| HZB build | After the single render graph | After both eyes, once | +| Base pass | Environment or grid | Environment (full immersion) or none (mixed/passthrough) | +| Game update thread | Main thread (MTKView delegate) | Compositor thread, with main-thread dispatch for restricted APIs | diff --git a/docs/07-Contributor/ContributionGuidelines.md b/docs/Contributor/ContributionGuidelines.md similarity index 100% rename from docs/07-Contributor/ContributionGuidelines.md rename to docs/Contributor/ContributionGuidelines.md diff --git a/docs/07-Contributor/Formatting.md b/docs/Contributor/Formatting.md similarity index 100% rename from docs/07-Contributor/Formatting.md rename to docs/Contributor/Formatting.md diff --git a/docs/07-Contributor/versioning.md b/docs/Contributor/versioning.md similarity index 100% rename from docs/07-Contributor/versioning.md rename to docs/Contributor/versioning.md diff --git a/docs/images/Editor/EditorAssetBrowserScripts.png b/docs/images/Editor/EditorAssetBrowserScripts.png deleted file mode 100644 index 0b523ad75..000000000 Binary files a/docs/images/Editor/EditorAssetBrowserScripts.png and /dev/null differ diff --git a/docs/images/Editor/EditorAssetBrowserView-alt.png b/docs/images/Editor/EditorAssetBrowserView-alt.png deleted file mode 100644 index b925157b9..000000000 Binary files a/docs/images/Editor/EditorAssetBrowserView-alt.png and /dev/null differ diff --git a/docs/images/Editor/EditorAssetBrowserView.png b/docs/images/Editor/EditorAssetBrowserView.png deleted file mode 100644 index c5cbe12fe..000000000 Binary files a/docs/images/Editor/EditorAssetBrowserView.png and /dev/null differ diff --git a/docs/images/Editor/EditorAssetLibraryLoupe.png b/docs/images/Editor/EditorAssetLibraryLoupe.png deleted file mode 100644 index 9a4a0c8e3..000000000 Binary files a/docs/images/Editor/EditorAssetLibraryLoupe.png and /dev/null differ diff --git a/docs/images/Editor/EditorBottomShot.png b/docs/images/Editor/EditorBottomShot.png deleted file mode 100644 index ed3f3c731..000000000 Binary files a/docs/images/Editor/EditorBottomShot.png and /dev/null differ diff --git a/docs/images/Editor/EditorCodeScriptView-alt.png b/docs/images/Editor/EditorCodeScriptView-alt.png deleted file mode 100644 index 84d7461bc..000000000 Binary files a/docs/images/Editor/EditorCodeScriptView-alt.png and /dev/null differ diff --git a/docs/images/Editor/EditorCodeScriptView.png b/docs/images/Editor/EditorCodeScriptView.png deleted file mode 100644 index 654da1eb3..000000000 Binary files a/docs/images/Editor/EditorCodeScriptView.png and /dev/null differ diff --git a/docs/images/Editor/EditorEffects.png b/docs/images/Editor/EditorEffects.png deleted file mode 100644 index d69f37874..000000000 Binary files a/docs/images/Editor/EditorEffects.png and /dev/null differ diff --git a/docs/images/Editor/EditorEnvironment.png b/docs/images/Editor/EditorEnvironment.png deleted file mode 100644 index c6f55a224..000000000 Binary files a/docs/images/Editor/EditorEnvironment.png and /dev/null differ diff --git a/docs/images/Editor/EditorInspectorView.png b/docs/images/Editor/EditorInspectorView.png deleted file mode 100644 index 04f65dce7..000000000 Binary files a/docs/images/Editor/EditorInspectorView.png and /dev/null differ diff --git a/docs/images/Editor/EditorMainShot-alt.png b/docs/images/Editor/EditorMainShot-alt.png deleted file mode 100644 index 2ba7138d1..000000000 Binary files a/docs/images/Editor/EditorMainShot-alt.png and /dev/null differ diff --git a/docs/images/Editor/EditorMainShot.png b/docs/images/Editor/EditorMainShot.png deleted file mode 100644 index 981a26e45..000000000 Binary files a/docs/images/Editor/EditorMainShot.png and /dev/null differ diff --git a/docs/images/Editor/EditorScenegraphView.png b/docs/images/Editor/EditorScenegraphView.png deleted file mode 100644 index 87b1cc9c0..000000000 Binary files a/docs/images/Editor/EditorScenegraphView.png and /dev/null differ diff --git a/docs/images/Editor/EditorSideShotWide-alt.png b/docs/images/Editor/EditorSideShotWide-alt.png deleted file mode 100644 index 42a9c804b..000000000 Binary files a/docs/images/Editor/EditorSideShotWide-alt.png and /dev/null differ diff --git a/docs/images/Editor/EditorSideShotWide.png b/docs/images/Editor/EditorSideShotWide.png deleted file mode 100644 index 14ea9d471..000000000 Binary files a/docs/images/Editor/EditorSideShotWide.png and /dev/null differ diff --git a/docs/images/Editor/Editor_scene_empty.png b/docs/images/Editor/Editor_scene_empty.png deleted file mode 100644 index 8dbc28800..000000000 Binary files a/docs/images/Editor/Editor_scene_empty.png and /dev/null differ diff --git a/docs/images/Editor/Editor_scene_model_import.png b/docs/images/Editor/Editor_scene_model_import.png deleted file mode 100644 index 310df2be5..000000000 Binary files a/docs/images/Editor/Editor_scene_model_import.png and /dev/null differ diff --git a/docs/images/Editor/Editor_scene_model_viewport.png b/docs/images/Editor/Editor_scene_model_viewport.png deleted file mode 100644 index 321d25529..000000000 Binary files a/docs/images/Editor/Editor_scene_model_viewport.png and /dev/null differ diff --git a/docs/images/Editor/Editor_scene_name.png b/docs/images/Editor/Editor_scene_name.png deleted file mode 100644 index 09e711f6b..000000000 Binary files a/docs/images/Editor/Editor_scene_name.png and /dev/null differ diff --git a/docs/images/Editor/Editor_scene_new.png b/docs/images/Editor/Editor_scene_new.png deleted file mode 100644 index 99bc768bd..000000000 Binary files a/docs/images/Editor/Editor_scene_new.png and /dev/null differ diff --git a/docs/images/Editor/Editor_scene_xcode.png b/docs/images/Editor/Editor_scene_xcode.png deleted file mode 100644 index 65b5c908d..000000000 Binary files a/docs/images/Editor/Editor_scene_xcode.png and /dev/null differ diff --git a/docs/images/Editor/ScriptEditorAdd.png b/docs/images/Editor/ScriptEditorAdd.png deleted file mode 100644 index f425394bd..000000000 Binary files a/docs/images/Editor/ScriptEditorAdd.png and /dev/null differ diff --git a/docs/images/Editor/ScriptReload.png b/docs/images/Editor/ScriptReload.png deleted file mode 100644 index 79f698548..000000000 Binary files a/docs/images/Editor/ScriptReload.png and /dev/null differ diff --git a/docs/images/UntoldEngineGrid.png b/docs/images/UntoldEngineGrid.png deleted file mode 100644 index d6790ef10..000000000 Binary files a/docs/images/UntoldEngineGrid.png and /dev/null differ diff --git a/docs/images/add-animation-component.png b/docs/images/add-animation-component.png deleted file mode 100644 index 4fb63092e..000000000 Binary files a/docs/images/add-animation-component.png and /dev/null differ diff --git a/docs/images/add-button-scenegraph.png b/docs/images/add-button-scenegraph.png deleted file mode 100644 index 17c6f06a0..000000000 Binary files a/docs/images/add-button-scenegraph.png and /dev/null differ diff --git a/docs/images/addcomponent.png b/docs/images/addcomponent.png deleted file mode 100644 index cc2995fe7..000000000 Binary files a/docs/images/addcomponent.png and /dev/null differ diff --git a/docs/images/animation-assign.png b/docs/images/animation-assign.png deleted file mode 100644 index 9a36e09ba..000000000 Binary files a/docs/images/animation-assign.png and /dev/null differ diff --git a/docs/images/animation-running.png b/docs/images/animation-running.png deleted file mode 100644 index 73c84ad72..000000000 Binary files a/docs/images/animation-running.png and /dev/null differ diff --git a/docs/images/animationexportblender.png b/docs/images/animationexportblender.png deleted file mode 100644 index da6553855..000000000 Binary files a/docs/images/animationexportblender.png and /dev/null differ diff --git a/docs/images/animationexportblenderStandalone.png b/docs/images/animationexportblenderStandalone.png deleted file mode 100644 index 77666c05e..000000000 Binary files a/docs/images/animationexportblenderStandalone.png and /dev/null differ diff --git a/docs/images/asset-browser-folder.png b/docs/images/asset-browser-folder.png deleted file mode 100644 index 6f0406ced..000000000 Binary files a/docs/images/asset-browser-folder.png and /dev/null differ diff --git a/docs/images/asset-browser-model.png b/docs/images/asset-browser-model.png deleted file mode 100644 index 7b293ec8c..000000000 Binary files a/docs/images/asset-browser-model.png and /dev/null differ diff --git a/docs/images/asset-browser-usdc-file.png b/docs/images/asset-browser-usdc-file.png deleted file mode 100644 index e6d2f0cb8..000000000 Binary files a/docs/images/asset-browser-usdc-file.png and /dev/null differ diff --git a/docs/images/camera.png b/docs/images/camera.png deleted file mode 100644 index 4fdc106c2..000000000 Binary files a/docs/images/camera.png and /dev/null differ diff --git a/docs/images/demogame-noeditor.png b/docs/images/demogame-noeditor.png deleted file mode 100644 index d0a39e9e0..000000000 Binary files a/docs/images/demogame-noeditor.png and /dev/null differ diff --git a/docs/images/editor-animation.png b/docs/images/editor-animation.png deleted file mode 100644 index d54a8c8e9..000000000 Binary files a/docs/images/editor-animation.png and /dev/null differ diff --git a/docs/images/editorscreenshot.png b/docs/images/editorscreenshot.png deleted file mode 100644 index 7f78a96d1..000000000 Binary files a/docs/images/editorscreenshot.png and /dev/null differ diff --git a/docs/images/engine-assetbrowser.png b/docs/images/engine-assetbrowser.png deleted file mode 100644 index c58c536c2..000000000 Binary files a/docs/images/engine-assetbrowser.png and /dev/null differ diff --git a/docs/images/engine-consolelog.png b/docs/images/engine-consolelog.png deleted file mode 100644 index 767e792c4..000000000 Binary files a/docs/images/engine-consolelog.png and /dev/null differ diff --git a/docs/images/engine-editor-startup.png b/docs/images/engine-editor-startup.png deleted file mode 100644 index 52519a1ed..000000000 Binary files a/docs/images/engine-editor-startup.png and /dev/null differ diff --git a/docs/images/engine-gizmo.png b/docs/images/engine-gizmo.png deleted file mode 100644 index 0faa36698..000000000 Binary files a/docs/images/engine-gizmo.png and /dev/null differ diff --git a/docs/images/engine-hdr.png b/docs/images/engine-hdr.png deleted file mode 100644 index 273b38545..000000000 Binary files a/docs/images/engine-hdr.png and /dev/null differ diff --git a/docs/images/engine-inspector.png b/docs/images/engine-inspector.png deleted file mode 100644 index bd778f251..000000000 Binary files a/docs/images/engine-inspector.png and /dev/null differ diff --git a/docs/images/engine-lights.png b/docs/images/engine-lights.png deleted file mode 100644 index a18833041..000000000 Binary files a/docs/images/engine-lights.png and /dev/null differ diff --git a/docs/images/engine-materials.png b/docs/images/engine-materials.png deleted file mode 100644 index 1f813dfc8..000000000 Binary files a/docs/images/engine-materials.png and /dev/null differ diff --git a/docs/images/engine-post-processing.png b/docs/images/engine-post-processing.png deleted file mode 100644 index 3e3f1f32e..000000000 Binary files a/docs/images/engine-post-processing.png and /dev/null differ diff --git a/docs/images/engine-scenegraph.png b/docs/images/engine-scenegraph.png deleted file mode 100644 index 51e6e51a0..000000000 Binary files a/docs/images/engine-scenegraph.png and /dev/null differ diff --git a/docs/images/enginethumbnail.jpg b/docs/images/enginethumbnail.jpg deleted file mode 100644 index 882b7a07f..000000000 Binary files a/docs/images/enginethumbnail.jpg and /dev/null differ diff --git a/docs/images/gamedemoscreenshot.png b/docs/images/gamedemoscreenshot.png deleted file mode 100644 index 7f78a96d1..000000000 Binary files a/docs/images/gamedemoscreenshot.png and /dev/null differ diff --git a/docs/images/gamescene1.png b/docs/images/gamescene1.png deleted file mode 100644 index 512513157..000000000 Binary files a/docs/images/gamescene1.png and /dev/null differ diff --git a/docs/images/howtoexport.png b/docs/images/howtoexport.png deleted file mode 100644 index ccd4079c6..000000000 Binary files a/docs/images/howtoexport.png and /dev/null differ diff --git a/docs/images/howtoexportStandalone.png b/docs/images/howtoexportStandalone.png deleted file mode 100644 index 0858b82a5..000000000 Binary files a/docs/images/howtoexportStandalone.png and /dev/null differ diff --git a/docs/images/importassetbutton.png b/docs/images/importassetbutton.png deleted file mode 100644 index a2ea71d9d..000000000 Binary files a/docs/images/importassetbutton.png and /dev/null differ diff --git a/docs/images/importheader.gif b/docs/images/importheader.gif deleted file mode 100644 index 9a1b60a2a..000000000 Binary files a/docs/images/importheader.gif and /dev/null differ diff --git a/docs/images/inspector.png b/docs/images/inspector.png deleted file mode 100644 index 0504eb302..000000000 Binary files a/docs/images/inspector.png and /dev/null differ diff --git a/docs/images/launchgame.gif b/docs/images/launchgame.gif deleted file mode 100644 index f13e49088..000000000 Binary files a/docs/images/launchgame.gif and /dev/null differ diff --git a/docs/images/linkerissue.png b/docs/images/linkerissue.png deleted file mode 100644 index 192c9e5ea..000000000 Binary files a/docs/images/linkerissue.png and /dev/null differ diff --git a/docs/images/modelexportblender.png b/docs/images/modelexportblender.png deleted file mode 100644 index bbfaa6c49..000000000 Binary files a/docs/images/modelexportblender.png and /dev/null differ diff --git a/docs/images/modelineditor.png b/docs/images/modelineditor.png deleted file mode 100644 index 951e24a5b..000000000 Binary files a/docs/images/modelineditor.png and /dev/null differ diff --git a/docs/images/modelsriggedexportblender.png b/docs/images/modelsriggedexportblender.png deleted file mode 100644 index 1eb6740e8..000000000 Binary files a/docs/images/modelsriggedexportblender.png and /dev/null differ diff --git a/docs/images/render-component.png b/docs/images/render-component.png deleted file mode 100644 index d15b36bf2..000000000 Binary files a/docs/images/render-component.png and /dev/null differ diff --git a/docs/images/script_component_selection.png b/docs/images/script_component_selection.png deleted file mode 100644 index a3a65bcb4..000000000 Binary files a/docs/images/script_component_selection.png and /dev/null differ diff --git a/docs/images/script_open_in_xcode.png b/docs/images/script_open_in_xcode.png deleted file mode 100644 index 529766292..000000000 Binary files a/docs/images/script_open_in_xcode.png and /dev/null differ diff --git a/docs/images/script_properties.png b/docs/images/script_properties.png deleted file mode 100644 index 1a926da20..000000000 Binary files a/docs/images/script_properties.png and /dev/null differ diff --git a/docs/images/scripts_in_asset_browser.png b/docs/images/scripts_in_asset_browser.png deleted file mode 100644 index 002e8072b..000000000 Binary files a/docs/images/scripts_in_asset_browser.png and /dev/null differ diff --git a/docs/images/setpathbutton.png b/docs/images/setpathbutton.png deleted file mode 100644 index a2ea71d9d..000000000 Binary files a/docs/images/setpathbutton.png and /dev/null differ diff --git a/website/_bak/index._bak.mdx b/website/_bak/index._bak.mdx deleted file mode 100644 index 37c6034d1..000000000 --- a/website/_bak/index._bak.mdx +++ /dev/null @@ -1,52 +0,0 @@ ---- -title: Untold Engine -description: Fast, clear docs ---- - -> "A Swift-based 3D game engine designed for simplicity and creativity on macOS and iOS." - -The Untold Engine is an open-source 3D game engine under active development, designed for macOS and iOS platforms. Written in Swift and powered by Metal, its goal is to simplify game creation with a clean, intuitive API. While the engine already supports many core systems like rendering, physics, and animation, there’s still much to build and improve. - - -## Vision - -The Untold Engine strives to be a **stable, performant, and developer-friendly** 3D engine that empowers creativity, removes friction, and makes game development feel effortless for Apple developers - -## Guiding Principles - -To achieve this vision, we follow these principles: - -- The engine strives to remain stable and crash-free. -- The codebase is backed by unit tests. -- We profile continuously to prevent regressions (visual and performance). -- The API must remain clear and user-friendly. -- We always think about the developer first—removing friction so they can focus on their games. - - -### Current Features: - -- Simple API: Focused on ease of use, even for those new to game development. -- Core Systems: Includes foundational systems for entity registration, rendering, physics, and more. -- Metal Integration: Leverages Apple’s graphics API for efficient rendering. - -### The Journey Ahead: - -The Untold Engine is a work in progress, with ambitious goals to: - -- Expand physics capabilities with collision detection. -- Enhance PBR rendering for more realistic visuals. -- Add new features to make game development a breeze. - -### Why Try the Untold Engine? - -- For Learners: A great way to explore game development with an engine that prioritizes simplicity. -- For Game Developers: An opportunity to contribute to an open-source project and shape its future. -- For Apple Developers: A Swift and Metal-based engine that feels at home on macOS and iOS. - -The engine is far from complete, but with every iteration, it gets closer to being an amazing tool for developers. By trying it out, contributing, or sharing your feedback, you can help make the Untold Engine better for everyone. - -Author: [Harold Serrano](http://www.haroldserrano.com) - ---- - -## Getting Started diff --git a/website/_bak/index.module.css b/website/_bak/index.module.css deleted file mode 100644 index 9f71a5da7..000000000 --- a/website/_bak/index.module.css +++ /dev/null @@ -1,23 +0,0 @@ -/** - * CSS files with the .module.css suffix will be treated as CSS modules - * and scoped locally. - */ - -.heroBanner { - padding: 4rem 0; - text-align: center; - position: relative; - overflow: hidden; -} - -@media screen and (max-width: 996px) { - .heroBanner { - padding: 2rem; - } -} - -.buttons { - display: flex; - align-items: center; - justify-content: center; -} diff --git a/website/docs/tutorial-basics/_category_.json b/website/docs/tutorial-basics/_category_.json deleted file mode 100644 index 2e6db55b1..000000000 --- a/website/docs/tutorial-basics/_category_.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "label": "Tutorial - Basics", - "position": 2, - "link": { - "type": "generated-index", - "description": "5 minutes to learn the most important Docusaurus concepts." - } -} diff --git a/website/docs/tutorial-basics/congratulations.md b/website/docs/tutorial-basics/congratulations.md deleted file mode 100644 index 04771a00b..000000000 --- a/website/docs/tutorial-basics/congratulations.md +++ /dev/null @@ -1,23 +0,0 @@ ---- -sidebar_position: 6 ---- - -# Congratulations! - -You have just learned the **basics of Docusaurus** and made some changes to the **initial template**. - -Docusaurus has **much more to offer**! - -Have **5 more minutes**? Take a look at **[versioning](../tutorial-extras/manage-docs-versions.md)** and **[i18n](../tutorial-extras/translate-your-site.md)**. - -Anything **unclear** or **buggy** in this tutorial? [Please report it!](https://github.com/facebook/docusaurus/discussions/4610) - -## What's next? - -- Read the [official documentation](https://docusaurus.io/) -- Modify your site configuration with [`docusaurus.config.js`](https://docusaurus.io/docs/api/docusaurus-config) -- Add navbar and footer items with [`themeConfig`](https://docusaurus.io/docs/api/themes/configuration) -- Add a custom [Design and Layout](https://docusaurus.io/docs/styling-layout) -- Add a [search bar](https://docusaurus.io/docs/search) -- Find inspirations in the [Docusaurus showcase](https://docusaurus.io/showcase) -- Get involved in the [Docusaurus Community](https://docusaurus.io/community/support) diff --git a/website/docs/tutorial-basics/create-a-blog-post.md b/website/docs/tutorial-basics/create-a-blog-post.md deleted file mode 100644 index 550ae17ee..000000000 --- a/website/docs/tutorial-basics/create-a-blog-post.md +++ /dev/null @@ -1,34 +0,0 @@ ---- -sidebar_position: 3 ---- - -# Create a Blog Post - -Docusaurus creates a **page for each blog post**, but also a **blog index page**, a **tag system**, an **RSS** feed... - -## Create your first Post - -Create a file at `blog/2021-02-28-greetings.md`: - -```md title="blog/2021-02-28-greetings.md" ---- -slug: greetings -title: Greetings! -authors: - - name: Joel Marcey - title: Co-creator of Docusaurus 1 - url: https://github.com/JoelMarcey - image_url: https://github.com/JoelMarcey.png - - name: Sébastien Lorber - title: Docusaurus maintainer - url: https://sebastienlorber.com - image_url: https://github.com/slorber.png -tags: [greetings] ---- - -Congratulations, you have made your first post! - -Feel free to play around and edit this post as much as you like. -``` - -A new blog post is now available at [http://localhost:3000/blog/greetings](http://localhost:3000/blog/greetings). diff --git a/website/docs/tutorial-basics/create-a-document.md b/website/docs/tutorial-basics/create-a-document.md deleted file mode 100644 index c22fe2944..000000000 --- a/website/docs/tutorial-basics/create-a-document.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Create a Document - -Documents are **groups of pages** connected through: - -- a **sidebar** -- **previous/next navigation** -- **versioning** - -## Create your first Doc - -Create a Markdown file at `docs/hello.md`: - -```md title="docs/hello.md" -# Hello - -This is my **first Docusaurus document**! -``` - -A new document is now available at [http://localhost:3000/docs/hello](http://localhost:3000/docs/hello). - -## Configure the Sidebar - -Docusaurus automatically **creates a sidebar** from the `docs` folder. - -Add metadata to customize the sidebar label and position: - -```md title="docs/hello.md" {1-4} ---- -sidebar_label: 'Hi!' -sidebar_position: 3 ---- - -# Hello - -This is my **first Docusaurus document**! -``` - -It is also possible to create your sidebar explicitly in `sidebars.js`: - -```js title="sidebars.js" -export default { - tutorialSidebar: [ - 'intro', - // highlight-next-line - 'hello', - { - type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], - }, - ], -}; -``` diff --git a/website/docs/tutorial-basics/create-a-page.md b/website/docs/tutorial-basics/create-a-page.md deleted file mode 100644 index 20e2ac300..000000000 --- a/website/docs/tutorial-basics/create-a-page.md +++ /dev/null @@ -1,43 +0,0 @@ ---- -sidebar_position: 1 ---- - -# Create a Page - -Add **Markdown or React** files to `src/pages` to create a **standalone page**: - -- `src/pages/index.js` → `localhost:3000/` -- `src/pages/foo.md` → `localhost:3000/foo` -- `src/pages/foo/bar.js` → `localhost:3000/foo/bar` - -## Create your first React Page - -Create a file at `src/pages/my-react-page.js`: - -```jsx title="src/pages/my-react-page.js" -import React from 'react'; -import Layout from '@theme/Layout'; - -export default function MyReactPage() { - return ( - -

My React page

-

This is a React page

-
- ); -} -``` - -A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). - -## Create your first Markdown Page - -Create a file at `src/pages/my-markdown-page.md`: - -```mdx title="src/pages/my-markdown-page.md" -# My Markdown page - -This is a Markdown page -``` - -A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). diff --git a/website/docs/tutorial-basics/deploy-your-site.md b/website/docs/tutorial-basics/deploy-your-site.md deleted file mode 100644 index 1c50ee063..000000000 --- a/website/docs/tutorial-basics/deploy-your-site.md +++ /dev/null @@ -1,31 +0,0 @@ ---- -sidebar_position: 5 ---- - -# Deploy your site - -Docusaurus is a **static-site-generator** (also called **[Jamstack](https://jamstack.org/)**). - -It builds your site as simple **static HTML, JavaScript and CSS files**. - -## Build your site - -Build your site **for production**: - -```bash -npm run build -``` - -The static files are generated in the `build` folder. - -## Deploy your site - -Test your production build locally: - -```bash -npm run serve -``` - -The `build` folder is now served at [http://localhost:3000/](http://localhost:3000/). - -You can now deploy the `build` folder **almost anywhere** easily, **for free** or very small cost (read the **[Deployment Guide](https://docusaurus.io/docs/deployment)**). diff --git a/website/docs/tutorial-basics/markdown-features.mdx b/website/docs/tutorial-basics/markdown-features.mdx deleted file mode 100644 index 35e00825e..000000000 --- a/website/docs/tutorial-basics/markdown-features.mdx +++ /dev/null @@ -1,152 +0,0 @@ ---- -sidebar_position: 4 ---- - -# Markdown Features - -Docusaurus supports **[Markdown](https://daringfireball.net/projects/markdown/syntax)** and a few **additional features**. - -## Front Matter - -Markdown documents have metadata at the top called [Front Matter](https://jekyllrb.com/docs/front-matter/): - -```text title="my-doc.md" -// highlight-start ---- -id: my-doc-id -title: My document title -description: My document description -slug: /my-custom-url ---- -// highlight-end - -## Markdown heading - -Markdown text with [links](./hello.md) -``` - -## Links - -Regular Markdown links are supported, using url paths or relative file paths. - -```md -Let's see how to [Create a page](/create-a-page). -``` - -```md -Let's see how to [Create a page](./create-a-page.md). -``` - -**Result:** Let's see how to [Create a page](./create-a-page.md). - -## Images - -Regular Markdown images are supported. - -You can use absolute paths to reference images in the static directory (`static/img/docusaurus.png`): - -```md -![Docusaurus logo](/img/docusaurus.png) -``` - -![Docusaurus logo](/img/docusaurus.png) - -You can reference images relative to the current file as well. This is particularly useful to colocate images close to the Markdown files using them: - -```md -![Docusaurus logo](./img/docusaurus.png) -``` - -## Code Blocks - -Markdown code blocks are supported with Syntax highlighting. - -````md -```jsx title="src/components/HelloDocusaurus.js" -function HelloDocusaurus() { - return

Hello, Docusaurus!

; -} -``` -```` - -```jsx title="src/components/HelloDocusaurus.js" -function HelloDocusaurus() { - return

Hello, Docusaurus!

; -} -``` - -## Admonitions - -Docusaurus has a special syntax to create admonitions and callouts: - -```md -:::tip My tip - -Use this awesome feature option - -::: - -:::danger Take care - -This action is dangerous - -::: -``` - -:::tip My tip - -Use this awesome feature option - -::: - -:::danger Take care - -This action is dangerous - -::: - -## MDX and React Components - -[MDX](https://mdxjs.com/) can make your documentation more **interactive** and allows using any **React components inside Markdown**: - -```jsx -export const Highlight = ({children, color}) => ( - { - alert(`You clicked the color ${color} with label ${children}`) - }}> - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! -``` - -export const Highlight = ({children, color}) => ( - { - alert(`You clicked the color ${color} with label ${children}`); - }}> - {children} - -); - -This is Docusaurus green ! - -This is Facebook blue ! diff --git a/website/docs/tutorial-extras/_category_.json b/website/docs/tutorial-extras/_category_.json deleted file mode 100644 index a8ffcc193..000000000 --- a/website/docs/tutorial-extras/_category_.json +++ /dev/null @@ -1,7 +0,0 @@ -{ - "label": "Tutorial - Extras", - "position": 3, - "link": { - "type": "generated-index" - } -} diff --git a/website/docs/tutorial-extras/img/docsVersionDropdown.png b/website/docs/tutorial-extras/img/docsVersionDropdown.png deleted file mode 100644 index 97e416461..000000000 Binary files a/website/docs/tutorial-extras/img/docsVersionDropdown.png and /dev/null differ diff --git a/website/docs/tutorial-extras/translate-your-site.md b/website/docs/tutorial-extras/translate-your-site.md deleted file mode 100644 index b5a644abd..000000000 --- a/website/docs/tutorial-extras/translate-your-site.md +++ /dev/null @@ -1,88 +0,0 @@ ---- -sidebar_position: 2 ---- - -# Translate your site - -Let's translate `docs/intro.md` to French. - -## Configure i18n - -Modify `docusaurus.config.js` to add support for the `fr` locale: - -```js title="docusaurus.config.js" -export default { - i18n: { - defaultLocale: 'en', - locales: ['en', 'fr'], - }, -}; -``` - -## Translate a doc - -Copy the `docs/intro.md` file to the `i18n/fr` folder: - -```bash -mkdir -p i18n/fr/docusaurus-plugin-content-docs/current/ - -cp docs/intro.md i18n/fr/docusaurus-plugin-content-docs/current/intro.md -``` - -Translate `i18n/fr/docusaurus-plugin-content-docs/current/intro.md` in French. - -## Start your localized site - -Start your site on the French locale: - -```bash -npm run start -- --locale fr -``` - -Your localized site is accessible at [http://localhost:3000/fr/](http://localhost:3000/fr/) and the `Getting Started` page is translated. - -:::caution - -In development, you can only use one locale at a time. - -::: - -## Add a Locale Dropdown - -To navigate seamlessly across languages, add a locale dropdown. - -Modify the `docusaurus.config.js` file: - -```js title="docusaurus.config.js" -export default { - themeConfig: { - navbar: { - items: [ - // highlight-start - { - type: 'localeDropdown', - }, - // highlight-end - ], - }, - }, -}; -``` - -The locale dropdown now appears in your navbar: - -![Locale Dropdown](./img/localeDropdown.png) - -## Build your localized site - -Build your site for a specific locale: - -```bash -npm run build -- --locale fr -``` - -Or build your site to include all the locales at once: - -```bash -npm run build -``` diff --git a/website/package-lock.json b/website/package-lock.json deleted file mode 100644 index 78fedc887..000000000 --- a/website/package-lock.json +++ /dev/null @@ -1,17597 +0,0 @@ -{ - "name": "website", - "version": "0.0.0", - "lockfileVersion": 3, - "requires": true, - "packages": { - "": { - "name": "website", - "version": "0.0.0", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/plugin-client-redirects": "^3.8.1", - "@docusaurus/preset-classic": "3.8.1", - "@mdx-js/react": "^3.0.0", - "clsx": "^2.0.0", - "prism-react-renderer": "^2.3.0", - "react": "^19.0.0", - "react-dom": "^19.0.0" - }, - "devDependencies": { - "@docusaurus/module-type-aliases": "3.8.1", - "@docusaurus/types": "3.8.1" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@algolia/abtesting": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@algolia/abtesting/-/abtesting-1.3.0.tgz", - "integrity": "sha512-KqPVLdVNfoJzX5BKNGM9bsW8saHeyax8kmPFXul5gejrSPN3qss7PgsFH5mMem7oR8tvjvNkia97ljEYPYCN8Q==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/autocomplete-core": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-core/-/autocomplete-core-1.17.9.tgz", - "integrity": "sha512-O7BxrpLDPJWWHv/DLA9DRFWs+iY1uOJZkqUwjS5HSZAGcl0hIVCQ97LTLewiZmZ402JYUrun+8NqFP+hCknlbQ==", - "license": "MIT", - "dependencies": { - "@algolia/autocomplete-plugin-algolia-insights": "1.17.9", - "@algolia/autocomplete-shared": "1.17.9" - } - }, - "node_modules/@algolia/autocomplete-plugin-algolia-insights": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-plugin-algolia-insights/-/autocomplete-plugin-algolia-insights-1.17.9.tgz", - "integrity": "sha512-u1fEHkCbWF92DBeB/KHeMacsjsoI0wFhjZtlCq2ddZbAehshbZST6Hs0Avkc0s+4UyBGbMDnSuXHLuvRWK5iDQ==", - "license": "MIT", - "dependencies": { - "@algolia/autocomplete-shared": "1.17.9" - }, - "peerDependencies": { - "search-insights": ">= 1 < 3" - } - }, - "node_modules/@algolia/autocomplete-preset-algolia": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-preset-algolia/-/autocomplete-preset-algolia-1.17.9.tgz", - "integrity": "sha512-Na1OuceSJeg8j7ZWn5ssMu/Ax3amtOwk76u4h5J4eK2Nx2KB5qt0Z4cOapCsxot9VcEN11ADV5aUSlQF4RhGjQ==", - "license": "MIT", - "dependencies": { - "@algolia/autocomplete-shared": "1.17.9" - }, - "peerDependencies": { - "@algolia/client-search": ">= 4.9.1 < 6", - "algoliasearch": ">= 4.9.1 < 6" - } - }, - "node_modules/@algolia/autocomplete-shared": { - "version": "1.17.9", - "resolved": "https://registry.npmjs.org/@algolia/autocomplete-shared/-/autocomplete-shared-1.17.9.tgz", - "integrity": "sha512-iDf05JDQ7I0b7JEA/9IektxN/80a2MZ1ToohfmNS3rfeuQnIKI3IJlIafD0xu4StbtQTghx9T3Maa97ytkXenQ==", - "license": "MIT", - "peerDependencies": { - "@algolia/client-search": ">= 4.9.1 < 6", - "algoliasearch": ">= 4.9.1 < 6" - } - }, - "node_modules/@algolia/client-abtesting": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/client-abtesting/-/client-abtesting-5.37.0.tgz", - "integrity": "sha512-Dp2Zq+x9qQFnuiQhVe91EeaaPxWBhzwQ6QnznZQnH9C1/ei3dvtmAFfFeaTxM6FzfJXDLvVnaQagTYFTQz3R5g==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-analytics": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/client-analytics/-/client-analytics-5.37.0.tgz", - "integrity": "sha512-wyXODDOluKogTuZxRII6mtqhAq4+qUR3zIUJEKTiHLe8HMZFxfUEI4NO2qSu04noXZHbv/sRVdQQqzKh12SZuQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-common": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/client-common/-/client-common-5.37.0.tgz", - "integrity": "sha512-GylIFlPvLy9OMgFG8JkonIagv3zF+Dx3H401Uo2KpmfMVBBJiGfAb9oYfXtplpRMZnZPxF5FnkWaI/NpVJMC+g==", - "license": "MIT", - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-insights": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/client-insights/-/client-insights-5.37.0.tgz", - "integrity": "sha512-T63afO2O69XHKw2+F7mfRoIbmXWGzgpZxgOFAdP3fR4laid7pWBt20P4eJ+Zn23wXS5kC9P2K7Bo3+rVjqnYiw==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-personalization": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/client-personalization/-/client-personalization-5.37.0.tgz", - "integrity": "sha512-1zOIXM98O9zD8bYDCJiUJRC/qNUydGHK/zRK+WbLXrW1SqLFRXECsKZa5KoG166+o5q5upk96qguOtE8FTXDWQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-query-suggestions": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/client-query-suggestions/-/client-query-suggestions-5.37.0.tgz", - "integrity": "sha512-31Nr2xOLBCYVal+OMZn1rp1H4lPs1914Tfr3a34wU/nsWJ+TB3vWjfkUUuuYhWoWBEArwuRzt3YNLn0F/KRVkg==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/client-search": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/client-search/-/client-search-5.37.0.tgz", - "integrity": "sha512-DAFVUvEg+u7jUs6BZiVz9zdaUebYULPiQ4LM2R4n8Nujzyj7BZzGr2DCd85ip4p/cx7nAZWKM8pLcGtkTRTdsg==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/events": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@algolia/events/-/events-4.0.1.tgz", - "integrity": "sha512-FQzvOCgoFXAbf5Y6mYozw2aj5KCJoA3m4heImceldzPSMbdyS4atVjJzXKMsfX3wnZTFYwkkt8/z8UesLHlSBQ==", - "license": "MIT" - }, - "node_modules/@algolia/ingestion": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/@algolia/ingestion/-/ingestion-1.37.0.tgz", - "integrity": "sha512-pkCepBRRdcdd7dTLbFddnu886NyyxmhgqiRcHHaDunvX03Ij4WzvouWrQq7B7iYBjkMQrLS8wQqSP0REfA4W8g==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/monitoring": { - "version": "1.37.0", - "resolved": "https://registry.npmjs.org/@algolia/monitoring/-/monitoring-1.37.0.tgz", - "integrity": "sha512-fNw7pVdyZAAQQCJf1cc/ih4fwrRdQSgKwgor4gchsI/Q/ss9inmC6bl/69jvoRSzgZS9BX4elwHKdo0EfTli3w==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/recommend": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/recommend/-/recommend-5.37.0.tgz", - "integrity": "sha512-U+FL5gzN2ldx3TYfQO5OAta2TBuIdabEdFwD5UVfWPsZE5nvOKkc/6BBqP54Z/adW/34c5ZrvvZhlhNTZujJXQ==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-browser-xhr": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-browser-xhr/-/requester-browser-xhr-5.37.0.tgz", - "integrity": "sha512-Ao8GZo8WgWFABrU7iq+JAftXV0t+UcOtCDL4mzHHZ+rQeTTf1TZssr4d0vIuoqkVNnKt9iyZ7T4lQff4ydcTrw==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-fetch": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-fetch/-/requester-fetch-5.37.0.tgz", - "integrity": "sha512-H7OJOXrFg5dLcGJ22uxx8eiFId0aB9b0UBhoOi4SMSuDBe6vjJJ/LeZyY25zPaSvkXNBN3vAM+ad6M0h6ha3AA==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@algolia/requester-node-http": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/@algolia/requester-node-http/-/requester-node-http-5.37.0.tgz", - "integrity": "sha512-npZ9aeag4SGTx677eqPL3rkSPlQrnzx/8wNrl1P7GpWq9w/eTmRbOq+wKrJ2r78idlY0MMgmY/mld2tq6dc44g==", - "license": "MIT", - "dependencies": { - "@algolia/client-common": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/@babel/code-frame": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.27.1.tgz", - "integrity": "sha512-cjQ7ZlQ0Mv3b47hABuTevyTuYN4i+loJKGeV9flcCgIK37cCXRh+L1bd3iBHlynerhQ7BhCkn2BPbQUL+rGqFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-validator-identifier": "^7.27.1", - "js-tokens": "^4.0.0", - "picocolors": "^1.1.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/compat-data": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.28.4.tgz", - "integrity": "sha512-YsmSKC29MJwf0gF8Rjjrg5LQCmyh+j/nD8/eP7f+BeoQTKYqs9RoWbjGOdy0+1Ekr68RJZMUOPVQaQisnIo4Rw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/core": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.28.4.tgz", - "integrity": "sha512-2BCOP7TN8M+gVDj7/ht3hsaO/B/n5oDbiAyyvnRlNOs+u1o+JWNYTQrmpuNp1/Wq2gcFrI01JAW+paEKDMx/CA==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-module-transforms": "^7.28.3", - "@babel/helpers": "^7.28.4", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.4", - "@babel/types": "^7.28.4", - "@jridgewell/remapping": "^2.3.5", - "convert-source-map": "^2.0.0", - "debug": "^4.1.0", - "gensync": "^1.0.0-beta.2", - "json5": "^2.2.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/babel" - } - }, - "node_modules/@babel/core/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/generator": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.28.3.tgz", - "integrity": "sha512-3lSpxGgvnmZznmBkCRnVREPUFJv2wrv9iAoFDvADJc0ypmdOxdUtcLeBgBJ6zE0PMeTKnxeQzyk0xTBq4Ep7zw==", - "license": "MIT", - "dependencies": { - "@babel/parser": "^7.28.3", - "@babel/types": "^7.28.2", - "@jridgewell/gen-mapping": "^0.3.12", - "@jridgewell/trace-mapping": "^0.3.28", - "jsesc": "^3.0.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-annotate-as-pure": { - "version": "7.27.3", - "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.27.3.tgz", - "integrity": "sha512-fXSwMQqitTGeHLBC08Eq5yXz2m37E4pJX1qAU1+2cNedz/ifv/bVXft90VeSav5nFO61EcNgwr0aJxbyPaWBPg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.3" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.27.2.tgz", - "integrity": "sha512-2+1thGUUWWjLTYTHZWK1n8Yga0ijBz1XAhUXcKy81rd5g6yh7hGqMp45v7cadSbEHc9G3OTv45SyneRN3ps4DQ==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.2", - "@babel/helper-validator-option": "^7.27.1", - "browserslist": "^4.24.0", - "lru-cache": "^5.1.1", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-compilation-targets/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-class-features-plugin": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.28.3.tgz", - "integrity": "sha512-V9f6ZFIYSLNEbuGA/92uOvYsGCJNsuA8ESZ4ldc09bWk/j8H8TKiPw8Mk1eG6olpnO0ALHJmYfZvF4MEE4gajg==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/traverse": "^7.28.3", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.27.1.tgz", - "integrity": "sha512-uVDC72XVf8UbrH5qQTc18Agb8emwjTiZrQE11Nv3CuBEZmVvTwwE9CBUEvHku06gQCAyYf8Nv6ja1IN+6LMbxQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "regexpu-core": "^6.2.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/helper-define-polyfill-provider": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.5.tgz", - "integrity": "sha512-uJnGFcPsWQK8fvjgGP5LZUZZsYGIoPeRjSF5PGwrelYgq7Q15/Ft9NGFp1zglwgIv//W0uG4BevRuSJRyylZPg==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "debug": "^4.4.1", - "lodash.debounce": "^4.0.8", - "resolve": "^1.22.10" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/helper-globals": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", - "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-member-expression-to-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.27.1.tgz", - "integrity": "sha512-E5chM8eWjTp/aNoVpcbfM7mLxu9XGLWYise2eBKGQomAk/Mb4XoxyqXTZbuTohbsl8EKqdlMhnDI2CCLfcs9wA==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-imports": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.27.1.tgz", - "integrity": "sha512-0gSFWUPNXNopqtIPQvlD5WgXYI5GY2kP2cCvoT8kczjbfcfuIljTbcWrulD1CIPIX2gt1wghbDy08yE1p+/r3w==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-module-transforms": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.3.tgz", - "integrity": "sha512-gytXUbs8k2sXS9PnQptz5o0QnpLL51SwASIORY6XaBKF88nsOT0Zw9szLqlSGQDP/4TljBAD5y98p2U1fqkdsw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-optimise-call-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.27.1.tgz", - "integrity": "sha512-URMGH08NzYFhubNSGJrpUEphGKQwMQYBySzat5cAByY1/YgIRkULnIy3tAMeszlL/so2HbeilYloUmSpd7GdVw==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-plugin-utils": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.27.1.tgz", - "integrity": "sha512-1gn1Up5YXka3YYAHGKpbideQ5Yjf1tDa9qYcgysz+cNCXukyLl6DjPXhD3VRwSb8c0J9tA4b2+rHEZtc6R0tlw==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-remap-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.27.1.tgz", - "integrity": "sha512-7fiA521aVw8lSPeI4ZOD3vRFkoqkJcS+z4hFo82bFSH/2tNd6eJ5qCVMS5OzDmZh/kaHQeBaeyxK6wljcPtveA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-wrap-function": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-replace-supers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.27.1.tgz", - "integrity": "sha512-7EHz6qDZc8RYS5ElPoShMheWvEgERonFCs7IAonWLLUTXW59DP14bCZt89/GKyreYn8g3S83m21FelHKbeDCKA==", - "license": "MIT", - "dependencies": { - "@babel/helper-member-expression-to-functions": "^7.27.1", - "@babel/helper-optimise-call-expression": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/helper-skip-transparent-expression-wrappers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.27.1.tgz", - "integrity": "sha512-Tub4ZKEXqbPjXgWLl2+3JpQAYBJ8+ikpQ2Ocj/q/r0LwE3UhENh7EUabyHjz2kCEsrRY83ew2DQdHluuiDQFzg==", - "license": "MIT", - "dependencies": { - "@babel/traverse": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.27.1.tgz", - "integrity": "sha512-D2hP9eA+Sqx1kBZgzxZh0y1trbuU+JoDkiEwqhQ36nodYqJwyEIhPSdMNd7lOm/4io72luTPWH20Yda0xOuUow==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-option": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", - "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-wrap-function": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.28.3.tgz", - "integrity": "sha512-zdf983tNfLZFletc0RRXYrHrucBEg95NIFMkn6K9dbeMYnsgHaSBGcQqdsCSStG2PYwRre0Qc2NNSCXbG+xc6g==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/traverse": "^7.28.3", - "@babel/types": "^7.28.2" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helpers": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.4.tgz", - "integrity": "sha512-HFN59MmQXGHVyYadKLVumYsA9dBFun/ldYxipEjzA4196jpLZd8UjEEBLkbEkvfYreDqJhZxYAWFPtrfhNpj4w==", - "license": "MIT", - "dependencies": { - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.28.4.tgz", - "integrity": "sha512-yZbBqeM6TkpP9du/I2pUZnJsRMGGvOuIrhjzC1AwHwW+6he4mni6Bp/m8ijn0iOuZuPI2BfkCoSRunpyjnrQKg==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.28.4" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.27.1.tgz", - "integrity": "sha512-QPG3C9cCVRQLxAVwmefEmwdTanECuUBMQZ/ym5kiw3XKCGA7qkuQLcjWWHcrD/GKbn/WmJwaezfuuAOcyKlRPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-class-field-initializer-scope": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-class-field-initializer-scope/-/plugin-bugfix-safari-class-field-initializer-scope-7.27.1.tgz", - "integrity": "sha512-qNeq3bCKnGgLkEXUuFry6dPlGfCdQNZbn7yUAPCInwAJHMU7THJfrBSozkcWq5sNM6RcF3S8XyQL2A52KNR9IA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.27.1.tgz", - "integrity": "sha512-g4L7OYun04N1WyqMNjldFwlfPCLVkgB54A/YCXICZYBsvJJE3kByKv9c9+R/nAfmIfjl2rKYLNyMHboYbZaWaA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.27.1.tgz", - "integrity": "sha512-oO02gcONcD5O1iTLi/6frMJBIwWEHceWGSGqrpCmEL8nogiS6J9PBlE48CaK20/Jx1LuRml9aDftLgdjXT8+Cw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.13.0" - } - }, - "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.28.3.tgz", - "integrity": "sha512-b6YTX108evsvE4YgWyQ921ZAFFQm3Bn+CA3+ZXlNVnPhx+UfsVURoPjfGAPCjBgrqo30yX/C2nZGX96DxvR9Iw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.3" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-proposal-private-property-in-object": { - "version": "7.21.0-placeholder-for-preset-env.2", - "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", - "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-dynamic-import": { - "version": "7.8.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", - "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.8.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-assertions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.27.1.tgz", - "integrity": "sha512-UT/Jrhw57xg4ILHLFnzFpPDlMbcdEicaAtjPQpbj9wa8T4r5KVWCimHcL/460g8Ht0DMxDyjsLgiWSkVjnwPFg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-import-attributes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.27.1.tgz", - "integrity": "sha512-oFT0FrKHgF53f4vOsZGi2Hh3I35PfSmVs4IBFLFj4dnafP+hIWDLg3VyKmUHfLoLHlyxY4C7DGtmHuJgn+IGww==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.27.1.tgz", - "integrity": "sha512-y8YTNIeKoyhGd9O0Jiyzyyqk8gdjnumGTQPsz0xOZOQ2RmkVJeZ1vmmfIvFEKqucBG6axJGBZDE/7iI5suUI/w==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.27.1.tgz", - "integrity": "sha512-xfYCBMxveHrRMnAWl1ZlPXOZjzkN82THFvLhQhFXFt81Z5HnN+EtUkZhv/zcKpmT3fzmWZB0ywiBrbC3vogbwQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-syntax-unicode-sets-regex": { - "version": "7.18.6", - "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", - "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.18.6", - "@babel/helper-plugin-utils": "^7.18.6" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-arrow-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.27.1.tgz", - "integrity": "sha512-8Z4TGic6xW70FKThA5HYEKKyBpOOsucTOD1DjU3fZxDg+K3zBJcXMFnt/4yQiZnf5+MiOMSXQ9PaEK/Ilh1DeA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-generator-functions": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.28.0.tgz", - "integrity": "sha512-BEOdvX4+M765icNPZeidyADIvQ1m1gmunXufXxvRESy/jNNyfovIqUyE7MVgGBjWktCoJlzvFA1To2O4ymIO3Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-async-to-generator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.27.1.tgz", - "integrity": "sha512-NREkZsZVJS4xmTr8qzE5y8AfIPqsdQfRuUiLRTEzb7Qii8iFWCyDKaUV2c0rCuh4ljDZ98ALHP/PetiBV2nddA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-remap-async-to-generator": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoped-functions": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.27.1.tgz", - "integrity": "sha512-cnqkuOtZLapWYZUYM5rVIdv1nXYuFVIltZ6ZJ7nIj585QsjKM5dhL2Fu/lICXZ1OyIAFc7Qy+bvDAtTXqGrlhg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-block-scoping": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.28.4.tgz", - "integrity": "sha512-1yxmvN0MJHOhPVmAsmoW5liWwoILobu/d/ShymZmj867bAdxGbehIrew1DuLpw2Ukv+qDSSPQdYW1dLNE7t11A==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.27.1.tgz", - "integrity": "sha512-D0VcalChDMtuRvJIu3U/fwWjf8ZMykz5iZsg77Nuj821vCKI3zCyRLwRdWbsuJ/uRwZhZ002QtCqIkwC/ZkvbA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-class-static-block": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.28.3.tgz", - "integrity": "sha512-LtPXlBbRoc4Njl/oh1CeD/3jC+atytbnf/UqLoqTDcEYGUPj022+rvfkbDYieUrSj3CaV4yHDByPE+T2HwfsJg==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.28.3", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0" - } - }, - "node_modules/@babel/plugin-transform-classes": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.28.4.tgz", - "integrity": "sha512-cFOlhIYPBv/iBoc+KS3M6et2XPtbT2HiCRfBXWtfpc9OAyostldxIf9YAYB6ypURBBbx+Qv6nyrLzASfJe+hBA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-globals": "^7.28.0", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1", - "@babel/traverse": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-computed-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.27.1.tgz", - "integrity": "sha512-lj9PGWvMTVksbWiDT2tW68zGS/cyo4AkZ/QTp0sQT0mjPopCmrSkzxeXkznjqBxzDI6TclZhOJbBmbBLjuOZUw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/template": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-destructuring": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.28.0.tgz", - "integrity": "sha512-v1nrSMBiKcodhsyJ4Gf+Z0U/yawmJDBOTpEB3mcQY52r9RIyPneGyAS/yM6seP/8I+mWI3elOMtT5dB8GJVs+A==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-dotall-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.27.1.tgz", - "integrity": "sha512-gEbkDVGRvjj7+T1ivxrfgygpT7GUd4vmODtYpbs0gZATdkX8/iSnOtZSxiZnsgm1YjTgjI6VKBGSJJevkrclzw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-keys": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.27.1.tgz", - "integrity": "sha512-MTyJk98sHvSs+cvZ4nOauwTTG1JeonDjSGvGGUNHreGQns+Mpt6WX/dVzWBHgg+dYZhkC4X+zTDfkTU+Vy9y7Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-duplicate-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-named-capturing-groups-regex/-/plugin-transform-duplicate-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-hkGcueTEzuhB30B3eJCbCYeCaaEQOmQR0AdvzpD4LoN0GXMWzzGSuRrxR2xTnCrvNbVwK9N6/jQ92GSLfiZWoQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-dynamic-import": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.27.1.tgz", - "integrity": "sha512-MHzkWQcEmjzzVW9j2q8LGjwGWpG2mjwaaB0BNQwst3FIjqsg8Ct/mIZlvSPJvfi9y2AC8mi/ktxbFVL9pZ1I4A==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-explicit-resource-management": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-explicit-resource-management/-/plugin-transform-explicit-resource-management-7.28.0.tgz", - "integrity": "sha512-K8nhUcn3f6iB+P3gwCv/no7OdzOZQcKchW6N389V6PD8NUWKZHzndOd9sPDVbMoBsbmjMqlB4L9fm+fEFNVlwQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-exponentiation-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.27.1.tgz", - "integrity": "sha512-uspvXnhHvGKf2r4VVtBpeFnuDWsJLQ6MF6lGJLC89jBR1uoVeqM416AZtTuhTezOfgHicpJQmoD5YUakO/YmXQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-export-namespace-from": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.27.1.tgz", - "integrity": "sha512-tQvHWSZ3/jH2xuq/vZDy0jNn+ZdXJeM8gHvX4lnJmsc3+50yPlWdZXIc5ay+umX+2/tJIqHqiEqcJvxlmIvRvQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-for-of": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.27.1.tgz", - "integrity": "sha512-BfbWFFEJFQzLCQ5N8VocnCtA8J1CLkNTe2Ms2wocj75dd6VpiqS5Z5quTYcUoo4Yq+DN0rtikODccuv7RU81sw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-function-name": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.27.1.tgz", - "integrity": "sha512-1bQeydJF9Nr1eBCMMbC+hdwmRlsv5XYOMu03YSWFwNs0HsAmtSxxF1fyuYPqemVldVyFmlCU7w8UE14LupUSZQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-json-strings": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.27.1.tgz", - "integrity": "sha512-6WVLVJiTjqcQauBhn1LkICsR2H+zm62I3h9faTDKt1qP4jn2o72tSvqMwtGFKGTpojce0gJs+76eZ2uCHRZh0Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.27.1.tgz", - "integrity": "sha512-0HCFSepIpLTkLcsi86GG3mTUzxV5jpmbv97hTETW3yzrAij8aqlD36toB1D0daVFJM8NK6GvKO0gslVQmm+zZA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-logical-assignment-operators": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.27.1.tgz", - "integrity": "sha512-SJvDs5dXxiae4FbSL1aBJlG4wvl594N6YEVVn9e3JGulwioy6z3oPjx/sQBO3Y4NwUu5HNix6KJ3wBZoewcdbw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-member-expression-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.27.1.tgz", - "integrity": "sha512-hqoBX4dcZ1I33jCSWcXrP+1Ku7kdqXf1oeah7ooKOIiAdKQ+uqftgCFNOSzA5AMS2XIHEYeGFg4cKRCdpxzVOQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-amd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.27.1.tgz", - "integrity": "sha512-iCsytMg/N9/oFq6n+gFTvUYDZQOMK5kEdeYxmxt91fcJGycfxVP9CnrxoliM0oumFERba2i8ZtwRUCMhvP1LnA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-commonjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.27.1.tgz", - "integrity": "sha512-OJguuwlTYlN0gBZFRPqwOGNWssZjfIUdS7HMYtN8c1KmwpwHFBwTeFZrg9XZa+DFTitWOW5iTAG7tyCUPsCCyw==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-systemjs": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.27.1.tgz", - "integrity": "sha512-w5N1XzsRbc0PQStASMksmUeqECuzKuTJer7kFagK8AXgpCMkeDMO5S+aaFb7A51ZYDF7XI34qsTX+fkHiIm5yA==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1", - "@babel/traverse": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-modules-umd": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.27.1.tgz", - "integrity": "sha512-iQBE/xC5BV1OxJbp6WG7jq9IWiD+xxlZhLrdwpPkTX3ydmXdvoCpyfJN7acaIBZaOqTfr76pgzqBJflNbeRK+w==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-transforms": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.27.1.tgz", - "integrity": "sha512-SstR5JYy8ddZvD6MhV0tM/j16Qds4mIpJTOd1Yu9J9pJjH93bxHECF7pgtc28XvkzTD6Pxcm/0Z73Hvk7kb3Ng==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-new-target": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.27.1.tgz", - "integrity": "sha512-f6PiYeqXQ05lYq3TIfIDu/MtliKUbNwkGApPUvyo6+tc7uaR4cPjPe7DFPr15Uyycg2lZU6btZ575CuQoYh7MQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.27.1.tgz", - "integrity": "sha512-aGZh6xMo6q9vq1JGcw58lZ1Z0+i0xB2x0XaauNIUXd6O1xXc3RwoWEBlsTQrY4KQ9Jf0s5rgD6SiNkaUdJegTA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-numeric-separator": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.27.1.tgz", - "integrity": "sha512-fdPKAcujuvEChxDBJ5c+0BTaS6revLV7CJL08e4m3de8qJfNIuCc2nc7XJYOjBoTMJeqSmwXJ0ypE14RCjLwaw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-rest-spread": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.28.4.tgz", - "integrity": "sha512-373KA2HQzKhQCYiRVIRr+3MjpCObqzDlyrM6u4I201wL8Mp2wHf7uB8GhDwis03k2ti8Zr65Zyyqs1xOxUF/Ew==", - "license": "MIT", - "dependencies": { - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/traverse": "^7.28.4" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-object-super": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.27.1.tgz", - "integrity": "sha512-SFy8S9plRPbIcxlJ8A6mT/CxFdJx/c04JEctz4jf8YZaVS2px34j7NXRrlGlHkN/M2gnpL37ZpGRGVFLd3l8Ng==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-replace-supers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-catch-binding": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.27.1.tgz", - "integrity": "sha512-txEAEKzYrHEX4xSZN4kJ+OfKXFVSWKB2ZxM9dpcE3wT7smwkNmXo5ORRlVzMVdJbD+Q8ILTgSD7959uj+3Dm3Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-optional-chaining": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.27.1.tgz", - "integrity": "sha512-BQmKPPIuc8EkZgNKsv0X4bPmOoayeu4F1YCwx2/CfmDSXDbp7GnzlUH+/ul5VGfRg1AoFPsrIThlEBj2xb4CAg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-parameters": { - "version": "7.27.7", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.27.7.tgz", - "integrity": "sha512-qBkYTYCb76RRxUM6CcZA5KRu8K4SM8ajzVeUgVdMVO9NN9uI/GaVmBg/WKJJGnNokV9SY8FxNOVWGXzqzUidBg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-methods": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.27.1.tgz", - "integrity": "sha512-10FVt+X55AjRAYI9BrdISN9/AQWHqldOeZDUoLyif1Kn05a56xVBXb8ZouL8pZ9jem8QpXaOt8TS7RHUIS+GPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-private-property-in-object": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.27.1.tgz", - "integrity": "sha512-5J+IhqTi1XPa0DXF83jYOaARrX+41gOewWbkPyjMNRDqgOCqdffGh8L3f/Ek5utaEBZExjSAzcyjmV9SSAWObQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-property-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.27.1.tgz", - "integrity": "sha512-oThy3BCuCha8kDZ8ZkgOg2exvPYUlprMukKQXI1r1pJ47NCvxfkEy8vK+r/hT9nF0Aa4H1WUPZZjHTFtAhGfmQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-constant-elements": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.27.1.tgz", - "integrity": "sha512-edoidOjl/ZxvYo4lSBOQGDSyToYVkTAwyVoa2tkuYTSmjrB1+uAedoL5iROVLXkxH+vRgA7uP4tMg2pUJpZ3Ug==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-display-name": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.28.0.tgz", - "integrity": "sha512-D6Eujc2zMxKjfa4Zxl4GHMsmhKKZ9VpcqIchJLvwTxad9zWIYulwYItBovpDOoNLISpcZSXoDJ5gaGbQUDqViA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.27.1.tgz", - "integrity": "sha512-2KH4LWGSrJIkVf5tSiBFYuXDAoWRq2MMwgivCf+93dd0GQi8RXLjKA/0EvRnVV5G0hrHczsquXuD01L8s6dmBw==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-jsx-development": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.27.1.tgz", - "integrity": "sha512-ykDdF5yI4f1WrAolLqeF3hmYU12j9ntLQl/AOG1HAS21jxyg1Q0/J/tpREuYLfatGdGmXp/3yS0ZA76kOlVq9Q==", - "license": "MIT", - "dependencies": { - "@babel/plugin-transform-react-jsx": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-react-pure-annotations": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.27.1.tgz", - "integrity": "sha512-JfuinvDOsD9FVMTHpzA/pBLisxpv1aSf+OIV8lgH3MuWrks19R27e6a6DipIg4aX1Zm9Wpb04p8wljfKrVSnPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regenerator": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.28.4.tgz", - "integrity": "sha512-+ZEdQlBoRg9m2NnzvEeLgtvBMO4tkFBw5SQIUgLICgTrumLoU7lr+Oghi6km2PFj+dbUt2u1oby2w3BDO9YQnA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-regexp-modifiers": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regexp-modifiers/-/plugin-transform-regexp-modifiers-7.27.1.tgz", - "integrity": "sha512-TtEciroaiODtXvLZv4rmfMhkCv8jx3wgKpL68PuiPh2M4fvz5jhsA7697N1gMvkvr/JTF13DrFYyEbY9U7cVPA==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/plugin-transform-reserved-words": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.27.1.tgz", - "integrity": "sha512-V2ABPHIJX4kC7HegLkYoDpfg9PVmuWy/i6vUM5eGK22bx4YVFD3M5F0QQnWQoDs6AGsUWTVOopBiMFQgHaSkVw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-runtime/-/plugin-transform-runtime-7.28.3.tgz", - "integrity": "sha512-Y6ab1kGqZ0u42Zv/4a7l0l72n9DKP/MKoKWaUSBylrhNZO2prYuqFOLbn5aW5SIFXwSH93yfjbgllL8lxuGKLg==", - "license": "MIT", - "dependencies": { - "@babel/helper-module-imports": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-runtime/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/plugin-transform-shorthand-properties": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.27.1.tgz", - "integrity": "sha512-N/wH1vcn4oYawbJ13Y/FxcQrWk63jhfNa7jef0ih7PHSIHX2LB7GWE1rkPrOnka9kwMxb6hMl19p7lidA+EHmQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-spread": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.27.1.tgz", - "integrity": "sha512-kpb3HUqaILBJcRFVhFUs6Trdd4mkrzcGXss+6/mxUd273PfbWqSDHRzMT2234gIg2QYfAjvXLSquP1xECSg09Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-sticky-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.27.1.tgz", - "integrity": "sha512-lhInBO5bi/Kowe2/aLdBAawijx+q1pQzicSgnkB6dUPc1+RC8QmJHKf2OjvU+NZWitguJHEaEmbV6VWEouT58g==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-template-literals": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.27.1.tgz", - "integrity": "sha512-fBJKiV7F2DxZUkg5EtHKXQdbsbURW3DZKQUWphDum0uRP6eHGGa/He9mc0mypL680pb+e/lDIthRohlv8NCHkg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typeof-symbol": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.27.1.tgz", - "integrity": "sha512-RiSILC+nRJM7FY5srIyc4/fGIwUhyDuuBSdWn4y6yT6gm652DpCHZjIipgn6B7MQ1ITOUnAKWixEUjQRIBIcLw==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-typescript": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.28.0.tgz", - "integrity": "sha512-4AEiDEBPIZvLQaWlc9liCavE0xRM0dNca41WtBeM3jgFptfUOSG9z0uteLhq6+3rq+WB6jIvUwKDTpXEHPJ2Vg==", - "license": "MIT", - "dependencies": { - "@babel/helper-annotate-as-pure": "^7.27.3", - "@babel/helper-create-class-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-skip-transparent-expression-wrappers": "^7.27.1", - "@babel/plugin-syntax-typescript": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-escapes": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.27.1.tgz", - "integrity": "sha512-Ysg4v6AmF26k9vpfFuTZg8HRfVWzsh1kVfowA23y9j/Gu6dOuahdUVhkLqpObp3JIv27MLSii6noRnuKN8H0Mg==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-property-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.27.1.tgz", - "integrity": "sha512-uW20S39PnaTImxp39O5qFlHLS9LJEmANjMG7SxIhap8rCHqu0Ik+tLEPX5DKmHn6CsWQ7j3lix2tFOa5YtL12Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.27.1.tgz", - "integrity": "sha512-xvINq24TRojDuyt6JGtHmkVkrfVV3FPT16uytxImLeBZqW3/H52yN+kM1MGuyPkIQxrzKwPHs5U/MP3qKyzkGw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/plugin-transform-unicode-sets-regex": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.27.1.tgz", - "integrity": "sha512-EtkOujbc4cgvb0mlpQefi4NTPBzhSIevblFevACNLUspmrALgmEBdL/XfnyyITfd8fKBZrZys92zOWcik7j9Tw==", - "license": "MIT", - "dependencies": { - "@babel/helper-create-regexp-features-plugin": "^7.27.1", - "@babel/helper-plugin-utils": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0" - } - }, - "node_modules/@babel/preset-env": { - "version": "7.28.3", - "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.28.3.tgz", - "integrity": "sha512-ROiDcM+GbYVPYBOeCR6uBXKkQpBExLl8k9HO1ygXEyds39j+vCCsjmj7S8GOniZQlEs81QlkdJZe76IpLSiqpg==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.28.0", - "@babel/helper-compilation-targets": "^7.27.2", - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.27.1", - "@babel/plugin-bugfix-safari-class-field-initializer-scope": "^7.27.1", - "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.27.1", - "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.27.1", - "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.28.3", - "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", - "@babel/plugin-syntax-import-assertions": "^7.27.1", - "@babel/plugin-syntax-import-attributes": "^7.27.1", - "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", - "@babel/plugin-transform-arrow-functions": "^7.27.1", - "@babel/plugin-transform-async-generator-functions": "^7.28.0", - "@babel/plugin-transform-async-to-generator": "^7.27.1", - "@babel/plugin-transform-block-scoped-functions": "^7.27.1", - "@babel/plugin-transform-block-scoping": "^7.28.0", - "@babel/plugin-transform-class-properties": "^7.27.1", - "@babel/plugin-transform-class-static-block": "^7.28.3", - "@babel/plugin-transform-classes": "^7.28.3", - "@babel/plugin-transform-computed-properties": "^7.27.1", - "@babel/plugin-transform-destructuring": "^7.28.0", - "@babel/plugin-transform-dotall-regex": "^7.27.1", - "@babel/plugin-transform-duplicate-keys": "^7.27.1", - "@babel/plugin-transform-duplicate-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-dynamic-import": "^7.27.1", - "@babel/plugin-transform-explicit-resource-management": "^7.28.0", - "@babel/plugin-transform-exponentiation-operator": "^7.27.1", - "@babel/plugin-transform-export-namespace-from": "^7.27.1", - "@babel/plugin-transform-for-of": "^7.27.1", - "@babel/plugin-transform-function-name": "^7.27.1", - "@babel/plugin-transform-json-strings": "^7.27.1", - "@babel/plugin-transform-literals": "^7.27.1", - "@babel/plugin-transform-logical-assignment-operators": "^7.27.1", - "@babel/plugin-transform-member-expression-literals": "^7.27.1", - "@babel/plugin-transform-modules-amd": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-modules-systemjs": "^7.27.1", - "@babel/plugin-transform-modules-umd": "^7.27.1", - "@babel/plugin-transform-named-capturing-groups-regex": "^7.27.1", - "@babel/plugin-transform-new-target": "^7.27.1", - "@babel/plugin-transform-nullish-coalescing-operator": "^7.27.1", - "@babel/plugin-transform-numeric-separator": "^7.27.1", - "@babel/plugin-transform-object-rest-spread": "^7.28.0", - "@babel/plugin-transform-object-super": "^7.27.1", - "@babel/plugin-transform-optional-catch-binding": "^7.27.1", - "@babel/plugin-transform-optional-chaining": "^7.27.1", - "@babel/plugin-transform-parameters": "^7.27.7", - "@babel/plugin-transform-private-methods": "^7.27.1", - "@babel/plugin-transform-private-property-in-object": "^7.27.1", - "@babel/plugin-transform-property-literals": "^7.27.1", - "@babel/plugin-transform-regenerator": "^7.28.3", - "@babel/plugin-transform-regexp-modifiers": "^7.27.1", - "@babel/plugin-transform-reserved-words": "^7.27.1", - "@babel/plugin-transform-shorthand-properties": "^7.27.1", - "@babel/plugin-transform-spread": "^7.27.1", - "@babel/plugin-transform-sticky-regex": "^7.27.1", - "@babel/plugin-transform-template-literals": "^7.27.1", - "@babel/plugin-transform-typeof-symbol": "^7.27.1", - "@babel/plugin-transform-unicode-escapes": "^7.27.1", - "@babel/plugin-transform-unicode-property-regex": "^7.27.1", - "@babel/plugin-transform-unicode-regex": "^7.27.1", - "@babel/plugin-transform-unicode-sets-regex": "^7.27.1", - "@babel/preset-modules": "0.1.6-no-external-plugins", - "babel-plugin-polyfill-corejs2": "^0.4.14", - "babel-plugin-polyfill-corejs3": "^0.13.0", - "babel-plugin-polyfill-regenerator": "^0.6.5", - "core-js-compat": "^3.43.0", - "semver": "^6.3.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-env/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/@babel/preset-modules": { - "version": "0.1.6-no-external-plugins", - "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", - "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.0.0", - "@babel/types": "^7.4.4", - "esutils": "^2.0.2" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/@babel/preset-react": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.27.1.tgz", - "integrity": "sha512-oJHWh2gLhU9dW9HHr42q0cI0/iHHXTLGe39qvpAZZzagHy0MzYLCnCVV0symeRvzmjHyVU7mw2K06E6u/JwbhA==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-transform-react-display-name": "^7.27.1", - "@babel/plugin-transform-react-jsx": "^7.27.1", - "@babel/plugin-transform-react-jsx-development": "^7.27.1", - "@babel/plugin-transform-react-pure-annotations": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/preset-typescript": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.27.1.tgz", - "integrity": "sha512-l7WfQfX0WK4M0v2RudjuQK4u99BS6yLHYEmdtVPP7lKV013zr9DygFuWNlnbvQ9LR+LS0Egz/XAvGx5U9MX0fQ==", - "license": "MIT", - "dependencies": { - "@babel/helper-plugin-utils": "^7.27.1", - "@babel/helper-validator-option": "^7.27.1", - "@babel/plugin-syntax-jsx": "^7.27.1", - "@babel/plugin-transform-modules-commonjs": "^7.27.1", - "@babel/plugin-transform-typescript": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.28.4.tgz", - "integrity": "sha512-Q/N6JNWvIvPnLDvjlE1OUBLPQHH6l3CltCEsHIujp45zQUSSh8K+gHnaEX45yAT1nyngnINhvWtzN+Nb9D8RAQ==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/runtime-corejs3": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/runtime-corejs3/-/runtime-corejs3-7.28.4.tgz", - "integrity": "sha512-h7iEYiW4HebClDEhtvFObtPmIvrd1SSfpI9EhOeKk4CtIK/ngBWFpuhCzhdmRKtg71ylcue+9I6dv54XYO1epQ==", - "license": "MIT", - "dependencies": { - "core-js-pure": "^3.43.0" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/template": { - "version": "7.27.2", - "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.27.2.tgz", - "integrity": "sha512-LPDZ85aEJyYSd18/DkjNh4/y1ntkE5KwUHWTiqgRxruuZL2F1yuHligVHLvcHY2vMHXttKFpJn6LwfI7cw7ODw==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/parser": "^7.27.2", - "@babel/types": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/traverse": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.28.4.tgz", - "integrity": "sha512-YEzuboP2qvQavAcjgQNVgsvHIDv6ZpwXvcvjmyySP2DIMuByS/6ioU5G9pYrWHM6T2YDfc7xga9iNzYOs12CFQ==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.27.1", - "@babel/generator": "^7.28.3", - "@babel/helper-globals": "^7.28.0", - "@babel/parser": "^7.28.4", - "@babel/template": "^7.27.2", - "@babel/types": "^7.28.4", - "debug": "^4.3.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.28.4", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.28.4.tgz", - "integrity": "sha512-bkFqkLhh3pMBUQQkpVgWDWq/lqzc2678eUyDlTBhRqhCHFguYYGM0Efga7tYk4TogG/3x0EEl66/OQ+WGbWB/Q==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.27.1" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@colors/colors": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", - "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", - "license": "MIT", - "optional": true, - "engines": { - "node": ">=0.1.90" - } - }, - "node_modules/@csstools/cascade-layer-name-parser": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@csstools/cascade-layer-name-parser/-/cascade-layer-name-parser-2.0.5.tgz", - "integrity": "sha512-p1ko5eHgV+MgXFVa4STPKpvPxr6ReS8oS2jzTukjR74i5zJNyWO1ZM1m8YKBXnzDKWfBN1ztLYlHxbVemDD88A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/color-helpers": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@csstools/color-helpers/-/color-helpers-5.1.0.tgz", - "integrity": "sha512-S11EXWJyy0Mz5SYvRmY8nJYTFFd1LCNV+7cXyAgQtOOuzb4EsgfqDufL+9esx72/eLhsRdGZwaldu/h+E4t4BA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/css-calc": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/@csstools/css-calc/-/css-calc-2.1.4.tgz", - "integrity": "sha512-3N8oaj+0juUw/1H3YwmDDJXCgTB1gKU6Hc/bB502u9zR0q2vd786XJH9QfrKIEgFlZmhZiq6epXl4rHqhzsIgQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-color-parser": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/css-color-parser/-/css-color-parser-3.1.0.tgz", - "integrity": "sha512-nbtKwh3a6xNVIp/VRuXV64yTKnb1IjTAEEh3irzS+HkKjAOYLTGNb9pmVNntZ8iVBHcWDA2Dof0QtPgFI1BaTA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "@csstools/css-calc": "^2.1.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-parser-algorithms": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/css-parser-algorithms/-/css-parser-algorithms-3.0.5.tgz", - "integrity": "sha512-DaDeUkXZKjdGhgYaHNJTV9pV7Y9B3b644jCLs9Upc3VeNGg6LWARAT6O+Q+/COo+2gg/bM5rhpMAtf70WqfBdQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/css-tokenizer": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/css-tokenizer/-/css-tokenizer-3.0.4.tgz", - "integrity": "sha512-Vd/9EVDiu6PPJt9yAh6roZP6El1xHrdvIVGjyBsHR0RYwNHgL7FJPyIIW4fANJNG6FtyZfvlRPpFI4ZM/lubvw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - } - }, - "node_modules/@csstools/media-query-list-parser": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@csstools/media-query-list-parser/-/media-query-list-parser-4.0.3.tgz", - "integrity": "sha512-HAYH7d3TLRHDOUQK4mZKf9k9Ph/m8Akstg66ywKR4SFAigjs3yBiUeZtFxywiTm5moZMAp/5W/ZuFnNXXYLuuQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - } - }, - "node_modules/@csstools/postcss-alpha-function": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-alpha-function/-/postcss-alpha-function-1.0.1.tgz", - "integrity": "sha512-isfLLwksH3yHkFXfCI2Gcaqg7wGGHZZwunoJzEZk0yKYIokgre6hYVFibKL3SYAoR1kBXova8LB+JoO5vZzi9w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-cascade-layers": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-cascade-layers/-/postcss-cascade-layers-5.0.2.tgz", - "integrity": "sha512-nWBE08nhO8uWl6kSAeCx4im7QfVko3zLrtgWZY4/bP87zrSPpSyN/3W3TDqz1jJuH+kbKOHXg5rJnK+ZVYcFFg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-specificity": "^5.0.0", - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-cascade-layers/node_modules/@csstools/selector-specificity": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", - "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/@csstools/postcss-cascade-layers/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@csstools/postcss-color-function": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function/-/postcss-color-function-4.0.12.tgz", - "integrity": "sha512-yx3cljQKRaSBc2hfh8rMZFZzChaFgwmO2JfFgFr1vMcF3C/uyy5I4RFIBOIWGq1D+XbKCG789CGkG6zzkLpagA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-color-function-display-p3-linear": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-function-display-p3-linear/-/postcss-color-function-display-p3-linear-1.0.1.tgz", - "integrity": "sha512-E5qusdzhlmO1TztYzDIi8XPdPoYOjoTY6HBYBCYSj+Gn4gQRBlvjgPQXzfzuPQqt8EhkC/SzPKObg4Mbn8/xMg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-color-mix-function": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-function/-/postcss-color-mix-function-3.0.12.tgz", - "integrity": "sha512-4STERZfCP5Jcs13P1U5pTvI9SkgLgfMUMhdXW8IlJWkzOOOqhZIjcNhWtNJZes2nkBDsIKJ0CJtFtuaZ00moag==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-color-mix-variadic-function-arguments": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@csstools/postcss-color-mix-variadic-function-arguments/-/postcss-color-mix-variadic-function-arguments-1.0.2.tgz", - "integrity": "sha512-rM67Gp9lRAkTo+X31DUqMEq+iK+EFqsidfecmhrteErxJZb6tUoJBVQca1Vn1GpDql1s1rD1pKcuYzMsg7Z1KQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-content-alt-text": { - "version": "2.0.8", - "resolved": "https://registry.npmjs.org/@csstools/postcss-content-alt-text/-/postcss-content-alt-text-2.0.8.tgz", - "integrity": "sha512-9SfEW9QCxEpTlNMnpSqFaHyzsiRpZ5J5+KqCu1u5/eEJAWsMhzT40qf0FIbeeglEvrGRMdDzAxMIz3wqoGSb+Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-contrast-color-function": { - "version": "2.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-contrast-color-function/-/postcss-contrast-color-function-2.0.12.tgz", - "integrity": "sha512-YbwWckjK3qwKjeYz/CijgcS7WDUCtKTd8ShLztm3/i5dhh4NaqzsbYnhm4bjrpFpnLZ31jVcbK8YL77z3GBPzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-exponential-functions": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-exponential-functions/-/postcss-exponential-functions-2.0.9.tgz", - "integrity": "sha512-abg2W/PI3HXwS/CZshSa79kNWNZHdJPMBXeZNyPQFbbj8sKO3jXxOt/wF7juJVjyDTc6JrvaUZYFcSBZBhaxjw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-font-format-keywords": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-font-format-keywords/-/postcss-font-format-keywords-4.0.0.tgz", - "integrity": "sha512-usBzw9aCRDvchpok6C+4TXC57btc4bJtmKQWOHQxOVKen1ZfVqBUuCZ/wuqdX5GHsD0NRSr9XTP+5ID1ZZQBXw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-gamut-mapping": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gamut-mapping/-/postcss-gamut-mapping-2.0.11.tgz", - "integrity": "sha512-fCpCUgZNE2piVJKC76zFsgVW1apF6dpYsqGyH8SIeCcM4pTEsRTWTLCaJIMKFEundsCKwY1rwfhtrio04RJ4Dw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-gradients-interpolation-method": { - "version": "5.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-gradients-interpolation-method/-/postcss-gradients-interpolation-method-5.0.12.tgz", - "integrity": "sha512-jugzjwkUY0wtNrZlFeyXzimUL3hN4xMvoPnIXxoZqxDvjZRiSh+itgHcVUWzJ2VwD/VAMEgCLvtaJHX+4Vj3Ow==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-hwb-function": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-hwb-function/-/postcss-hwb-function-4.0.12.tgz", - "integrity": "sha512-mL/+88Z53KrE4JdePYFJAQWFrcADEqsLprExCM04GDNgHIztwFzj0Mbhd/yxMBngq0NIlz58VVxjt5abNs1VhA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-ic-unit": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-ic-unit/-/postcss-ic-unit-4.0.4.tgz", - "integrity": "sha512-yQ4VmossuOAql65sCPppVO1yfb7hDscf4GseF0VCA/DTDaBc0Wtf8MTqVPfjGYlT5+2buokG0Gp7y0atYZpwjg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-initial": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-initial/-/postcss-initial-2.0.1.tgz", - "integrity": "sha512-L1wLVMSAZ4wovznquK0xmC7QSctzO4D0Is590bxpGqhqjboLXYA16dWZpfwImkdOgACdQ9PqXsuRroW6qPlEsg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-is-pseudo-class/-/postcss-is-pseudo-class-5.0.3.tgz", - "integrity": "sha512-jS/TY4SpG4gszAtIg7Qnf3AS2pjcUM5SzxpApOrlndMeGhIbaTzWBzzP/IApXoNWEW7OhcjkRT48jnAUIFXhAQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-specificity": "^5.0.0", - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class/node_modules/@csstools/selector-specificity": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", - "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/@csstools/postcss-is-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@csstools/postcss-light-dark-function": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@csstools/postcss-light-dark-function/-/postcss-light-dark-function-2.0.11.tgz", - "integrity": "sha512-fNJcKXJdPM3Lyrbmgw2OBbaioU7yuKZtiXClf4sGdQttitijYlZMD5K7HrC/eF83VRWRrYq6OZ0Lx92leV2LFA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-float-and-clear": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-float-and-clear/-/postcss-logical-float-and-clear-3.0.0.tgz", - "integrity": "sha512-SEmaHMszwakI2rqKRJgE+8rpotFfne1ZS6bZqBoQIicFyV+xT1UF42eORPxJkVJVrH9C0ctUgwMSn3BLOIZldQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-overflow": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overflow/-/postcss-logical-overflow-2.0.0.tgz", - "integrity": "sha512-spzR1MInxPuXKEX2csMamshR4LRaSZ3UXVaRGjeQxl70ySxOhMpP2252RAFsg8QyyBXBzuVOOdx1+bVO5bPIzA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-overscroll-behavior": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-overscroll-behavior/-/postcss-logical-overscroll-behavior-2.0.0.tgz", - "integrity": "sha512-e/webMjoGOSYfqLunyzByZj5KKe5oyVg/YSbie99VEaSDE2kimFm0q1f6t/6Jo+VVCQ/jbe2Xy+uX+C4xzWs4w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-resize": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-resize/-/postcss-logical-resize-3.0.0.tgz", - "integrity": "sha512-DFbHQOFW/+I+MY4Ycd/QN6Dg4Hcbb50elIJCfnwkRTCX05G11SwViI5BbBlg9iHRl4ytB7pmY5ieAFk3ws7yyg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-logical-viewport-units": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-logical-viewport-units/-/postcss-logical-viewport-units-3.0.4.tgz", - "integrity": "sha512-q+eHV1haXA4w9xBwZLKjVKAWn3W2CMqmpNpZUk5kRprvSiBEGMgrNH3/sJZ8UA3JgyHaOt3jwT9uFa4wLX4EqQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-media-minmax": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-minmax/-/postcss-media-minmax-2.0.9.tgz", - "integrity": "sha512-af9Qw3uS3JhYLnCbqtZ9crTvvkR+0Se+bBqSr7ykAnl9yKhk6895z9rf+2F4dClIDJWxgn0iZZ1PSdkhrbs2ig==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/media-query-list-parser": "^4.0.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-media-queries-aspect-ratio-number-values": { - "version": "3.0.5", - "resolved": "https://registry.npmjs.org/@csstools/postcss-media-queries-aspect-ratio-number-values/-/postcss-media-queries-aspect-ratio-number-values-3.0.5.tgz", - "integrity": "sha512-zhAe31xaaXOY2Px8IYfoVTB3wglbJUVigGphFLj6exb7cjZRH9A6adyE22XfFK3P2PzwRk0VDeTJmaxpluyrDg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/media-query-list-parser": "^4.0.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-nested-calc": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-nested-calc/-/postcss-nested-calc-4.0.0.tgz", - "integrity": "sha512-jMYDdqrQQxE7k9+KjstC3NbsmC063n1FTPLCgCRS2/qHUbHM0mNy9pIn4QIiQGs9I/Bg98vMqw7mJXBxa0N88A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-normalize-display-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-normalize-display-values/-/postcss-normalize-display-values-4.0.0.tgz", - "integrity": "sha512-HlEoG0IDRoHXzXnkV4in47dzsxdsjdz6+j7MLjaACABX2NfvjFS6XVAnpaDyGesz9gK2SC7MbNwdCHusObKJ9Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-oklab-function": { - "version": "4.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-oklab-function/-/postcss-oklab-function-4.0.12.tgz", - "integrity": "sha512-HhlSmnE1NKBhXsTnNGjxvhryKtO7tJd1w42DKOGFD6jSHtYOrsJTQDKPMwvOfrzUAk8t7GcpIfRyM7ssqHpFjg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-progressive-custom-properties": { - "version": "4.2.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-progressive-custom-properties/-/postcss-progressive-custom-properties-4.2.1.tgz", - "integrity": "sha512-uPiiXf7IEKtUQXsxu6uWtOlRMXd2QWWy5fhxHDnPdXKCQckPP3E34ZgDoZ62r2iT+UOgWsSbM4NvHE5m3mAEdw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-random-function": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-random-function/-/postcss-random-function-2.0.1.tgz", - "integrity": "sha512-q+FQaNiRBhnoSNo+GzqGOIBKoHQ43lYz0ICrV+UudfWnEF6ksS6DsBIJSISKQT2Bvu3g4k6r7t0zYrk5pDlo8w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-relative-color-syntax": { - "version": "3.0.12", - "resolved": "https://registry.npmjs.org/@csstools/postcss-relative-color-syntax/-/postcss-relative-color-syntax-3.0.12.tgz", - "integrity": "sha512-0RLIeONxu/mtxRtf3o41Lq2ghLimw0w9ByLWnnEVuy89exmEEq8bynveBxNW3nyHqLAFEeNtVEmC1QK9MZ8Huw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-scope-pseudo-class": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@csstools/postcss-scope-pseudo-class/-/postcss-scope-pseudo-class-4.0.1.tgz", - "integrity": "sha512-IMi9FwtH6LMNuLea1bjVMQAsUhFxJnyLSgOp/cpv5hrzWmrUYU5fm0EguNDIIOHUqzXode8F/1qkC/tEo/qN8Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-scope-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/@csstools/postcss-sign-functions": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/@csstools/postcss-sign-functions/-/postcss-sign-functions-1.1.4.tgz", - "integrity": "sha512-P97h1XqRPcfcJndFdG95Gv/6ZzxUBBISem0IDqPZ7WMvc/wlO+yU0c5D/OCpZ5TJoTt63Ok3knGk64N+o6L2Pg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-stepped-value-functions": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-stepped-value-functions/-/postcss-stepped-value-functions-4.0.9.tgz", - "integrity": "sha512-h9btycWrsex4dNLeQfyU3y3w40LMQooJWFMm/SK9lrKguHDcFl4VMkncKKoXi2z5rM9YGWbUQABI8BT2UydIcA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-text-decoration-shorthand": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/@csstools/postcss-text-decoration-shorthand/-/postcss-text-decoration-shorthand-4.0.3.tgz", - "integrity": "sha512-KSkGgZfx0kQjRIYnpsD7X2Om9BUXX/Kii77VBifQW9Ih929hK0KNjVngHDH0bFB9GmfWcR9vJYJJRvw/NQjkrA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/color-helpers": "^5.1.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-trigonometric-functions": { - "version": "4.0.9", - "resolved": "https://registry.npmjs.org/@csstools/postcss-trigonometric-functions/-/postcss-trigonometric-functions-4.0.9.tgz", - "integrity": "sha512-Hnh5zJUdpNrJqK9v1/E3BbrQhaDTj5YiX7P61TOvUhoDHnUmsNNxcDAgkQ32RrcWx9GVUvfUNPcUkn8R3vIX6A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-calc": "^2.1.4", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/postcss-unset-value": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@csstools/postcss-unset-value/-/postcss-unset-value-4.0.0.tgz", - "integrity": "sha512-cBz3tOCI5Fw6NIFEwU3RiwK6mn3nKegjpJuzCndoGq3BZPkUjnsq7uQmIeMNeMbMk7YD2MfKcgCpZwX5jyXqCA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@csstools/utilities": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@csstools/utilities/-/utilities-2.0.0.tgz", - "integrity": "sha512-5VdOr0Z71u+Yp3ozOx8T11N703wIFGVRgOWbOZMKgglPJsWA54MRIoMNVMa7shUToIhx5J8vX4sOZgD2XiihiQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/@discoveryjs/json-ext": { - "version": "0.5.7", - "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", - "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docsearch/css": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@docsearch/css/-/css-3.9.0.tgz", - "integrity": "sha512-cQbnVbq0rrBwNAKegIac/t6a8nWoUAn8frnkLFW6YARaRmAQr5/Eoe6Ln2fqkUCZ40KpdrKbpSAmgrkviOxuWA==", - "license": "MIT" - }, - "node_modules/@docsearch/react": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/@docsearch/react/-/react-3.9.0.tgz", - "integrity": "sha512-mb5FOZYZIkRQ6s/NWnM98k879vu5pscWqTLubLFBO87igYYT4VzVazh4h5o/zCvTIZgEt3PvsCOMOswOUo9yHQ==", - "license": "MIT", - "dependencies": { - "@algolia/autocomplete-core": "1.17.9", - "@algolia/autocomplete-preset-algolia": "1.17.9", - "@docsearch/css": "3.9.0", - "algoliasearch": "^5.14.2" - }, - "peerDependencies": { - "@types/react": ">= 16.8.0 < 20.0.0", - "react": ">= 16.8.0 < 20.0.0", - "react-dom": ">= 16.8.0 < 20.0.0", - "search-insights": ">= 1 < 3" - }, - "peerDependenciesMeta": { - "@types/react": { - "optional": true - }, - "react": { - "optional": true - }, - "react-dom": { - "optional": true - }, - "search-insights": { - "optional": true - } - } - }, - "node_modules/@docusaurus/babel": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/babel/-/babel-3.8.1.tgz", - "integrity": "sha512-3brkJrml8vUbn9aeoZUlJfsI/GqyFcDgQJwQkmBtclJgWDEQBKKeagZfOgx0WfUQhagL1sQLNW0iBdxnI863Uw==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.9", - "@babel/generator": "^7.25.9", - "@babel/plugin-syntax-dynamic-import": "^7.8.3", - "@babel/plugin-transform-runtime": "^7.25.9", - "@babel/preset-env": "^7.25.9", - "@babel/preset-react": "^7.25.9", - "@babel/preset-typescript": "^7.25.9", - "@babel/runtime": "^7.25.9", - "@babel/runtime-corejs3": "^7.25.9", - "@babel/traverse": "^7.25.9", - "@docusaurus/logger": "3.8.1", - "@docusaurus/utils": "3.8.1", - "babel-plugin-dynamic-import-node": "^2.3.3", - "fs-extra": "^11.1.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/bundler": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/bundler/-/bundler-3.8.1.tgz", - "integrity": "sha512-/z4V0FRoQ0GuSLToNjOSGsk6m2lQUG4FRn8goOVoZSRsTrU8YR2aJacX5K3RG18EaX9b+52pN4m1sL3MQZVsQA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.25.9", - "@docusaurus/babel": "3.8.1", - "@docusaurus/cssnano-preset": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "babel-loader": "^9.2.1", - "clean-css": "^5.3.3", - "copy-webpack-plugin": "^11.0.0", - "css-loader": "^6.11.0", - "css-minimizer-webpack-plugin": "^5.0.1", - "cssnano": "^6.1.2", - "file-loader": "^6.2.0", - "html-minifier-terser": "^7.2.0", - "mini-css-extract-plugin": "^2.9.2", - "null-loader": "^4.0.1", - "postcss": "^8.5.4", - "postcss-loader": "^7.3.4", - "postcss-preset-env": "^10.2.1", - "terser-webpack-plugin": "^5.3.9", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "webpack": "^5.95.0", - "webpackbar": "^6.0.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/faster": "*" - }, - "peerDependenciesMeta": { - "@docusaurus/faster": { - "optional": true - } - } - }, - "node_modules/@docusaurus/core": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/core/-/core-3.8.1.tgz", - "integrity": "sha512-ENB01IyQSqI2FLtOzqSI3qxG2B/jP4gQPahl2C3XReiLebcVh5B5cB9KYFvdoOqOWPyr5gXK4sjgTKv7peXCrA==", - "license": "MIT", - "dependencies": { - "@docusaurus/babel": "3.8.1", - "@docusaurus/bundler": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "boxen": "^6.2.1", - "chalk": "^4.1.2", - "chokidar": "^3.5.3", - "cli-table3": "^0.6.3", - "combine-promises": "^1.1.0", - "commander": "^5.1.0", - "core-js": "^3.31.1", - "detect-port": "^1.5.1", - "escape-html": "^1.0.3", - "eta": "^2.2.0", - "eval": "^0.1.8", - "execa": "5.1.1", - "fs-extra": "^11.1.1", - "html-tags": "^3.3.1", - "html-webpack-plugin": "^5.6.0", - "leven": "^3.1.0", - "lodash": "^4.17.21", - "open": "^8.4.0", - "p-map": "^4.0.0", - "prompts": "^2.4.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0", - "react-loadable-ssr-addon-v5-slorber": "^1.0.1", - "react-router": "^5.3.4", - "react-router-config": "^5.1.1", - "react-router-dom": "^5.3.4", - "semver": "^7.5.4", - "serve-handler": "^6.1.6", - "tinypool": "^1.0.2", - "tslib": "^2.6.0", - "update-notifier": "^6.0.2", - "webpack": "^5.95.0", - "webpack-bundle-analyzer": "^4.10.2", - "webpack-dev-server": "^4.15.2", - "webpack-merge": "^6.0.1" - }, - "bin": { - "docusaurus": "bin/docusaurus.mjs" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@mdx-js/react": "^3.0.0", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/cssnano-preset": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/cssnano-preset/-/cssnano-preset-3.8.1.tgz", - "integrity": "sha512-G7WyR2N6SpyUotqhGznERBK+x84uyhfMQM2MmDLs88bw4Flom6TY46HzkRkSEzaP9j80MbTN8naiL1fR17WQug==", - "license": "MIT", - "dependencies": { - "cssnano-preset-advanced": "^6.1.2", - "postcss": "^8.5.4", - "postcss-sort-media-queries": "^5.2.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/logger": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/logger/-/logger-3.8.1.tgz", - "integrity": "sha512-2wjeGDhKcExEmjX8k1N/MRDiPKXGF2Pg+df/bDDPnnJWHXnVEZxXj80d6jcxp1Gpnksl0hF8t/ZQw9elqj2+ww==", - "license": "MIT", - "dependencies": { - "chalk": "^4.1.2", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/mdx-loader": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/mdx-loader/-/mdx-loader-3.8.1.tgz", - "integrity": "sha512-DZRhagSFRcEq1cUtBMo4TKxSNo/W6/s44yhr8X+eoXqCLycFQUylebOMPseHi5tc4fkGJqwqpWJLz6JStU9L4w==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "@mdx-js/mdx": "^3.0.0", - "@slorber/remark-comment": "^1.0.0", - "escape-html": "^1.0.3", - "estree-util-value-to-estree": "^3.0.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "image-size": "^2.0.2", - "mdast-util-mdx": "^3.0.0", - "mdast-util-to-string": "^4.0.0", - "rehype-raw": "^7.0.0", - "remark-directive": "^3.0.0", - "remark-emoji": "^4.0.0", - "remark-frontmatter": "^5.0.0", - "remark-gfm": "^4.0.0", - "stringify-object": "^3.3.0", - "tslib": "^2.6.0", - "unified": "^11.0.3", - "unist-util-visit": "^5.0.0", - "url-loader": "^4.1.1", - "vfile": "^6.0.1", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/module-type-aliases": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/module-type-aliases/-/module-type-aliases-3.8.1.tgz", - "integrity": "sha512-6xhvAJiXzsaq3JdosS7wbRt/PwEPWHr9eM4YNYqVlbgG1hSK3uQDXTVvQktasp3VO6BmfYWPozueLWuj4gB+vg==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.8.1", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "@types/react-router-dom": "*", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "react-loadable": "npm:@docusaurus/react-loadable@6.0.0" - }, - "peerDependencies": { - "react": "*", - "react-dom": "*" - } - }, - "node_modules/@docusaurus/plugin-client-redirects": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-client-redirects/-/plugin-client-redirects-3.8.1.tgz", - "integrity": "sha512-F+86R7PBn6VNgy/Ux8w3ZRypJGJEzksbejQKlbTC8u6uhBUhfdXWkDp6qdOisIoW0buY5nLqucvZt1zNJzhJhA==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "eta": "^2.2.0", - "fs-extra": "^11.1.1", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-blog": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-blog/-/plugin-content-blog-3.8.1.tgz", - "integrity": "sha512-vNTpMmlvNP9n3hGEcgPaXyvTljanAKIUkuG9URQ1DeuDup0OR7Ltvoc8yrmH+iMZJbcQGhUJF+WjHLwuk8HSdw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "cheerio": "1.0.0-rc.12", - "feed": "^4.2.2", - "fs-extra": "^11.1.1", - "lodash": "^4.17.21", - "schema-dts": "^1.1.2", - "srcset": "^4.0.0", - "tslib": "^2.6.0", - "unist-util-visit": "^5.0.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/plugin-content-docs": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-docs": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-docs/-/plugin-content-docs-3.8.1.tgz", - "integrity": "sha512-oByRkSZzeGNQByCMaX+kif5Nl2vmtj2IHQI2fWjCfCootsdKZDPFLonhIp5s3IGJO7PLUfe0POyw0Xh/RrGXJA==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/module-type-aliases": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "@types/react-router-config": "^5.0.7", - "combine-promises": "^1.1.0", - "fs-extra": "^11.1.1", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "schema-dts": "^1.1.2", - "tslib": "^2.6.0", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-content-pages": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-content-pages/-/plugin-content-pages-3.8.1.tgz", - "integrity": "sha512-a+V6MS2cIu37E/m7nDJn3dcxpvXb6TvgdNI22vJX8iUTp8eoMoPa0VArEbWvCxMY/xdC26WzNv4wZ6y0iIni/w==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "fs-extra": "^11.1.1", - "tslib": "^2.6.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-css-cascade-layers": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-css-cascade-layers/-/plugin-css-cascade-layers-3.8.1.tgz", - "integrity": "sha512-VQ47xRxfNKjHS5ItzaVXpxeTm7/wJLFMOPo1BkmoMG4Cuz4nuI+Hs62+RMk1OqVog68Swz66xVPK8g9XTrBKRw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/plugin-debug": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-debug/-/plugin-debug-3.8.1.tgz", - "integrity": "sha512-nT3lN7TV5bi5hKMB7FK8gCffFTBSsBsAfV84/v293qAmnHOyg1nr9okEw8AiwcO3bl9vije5nsUvP0aRl2lpaw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "fs-extra": "^11.1.1", - "react-json-view-lite": "^2.3.0", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-analytics": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-analytics/-/plugin-google-analytics-3.8.1.tgz", - "integrity": "sha512-Hrb/PurOJsmwHAsfMDH6oVpahkEGsx7F8CWMjyP/dw1qjqmdS9rcV1nYCGlM8nOtD3Wk/eaThzUB5TSZsGz+7Q==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-gtag": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-gtag/-/plugin-google-gtag-3.8.1.tgz", - "integrity": "sha512-tKE8j1cEZCh8KZa4aa80zpSTxsC2/ZYqjx6AAfd8uA8VHZVw79+7OTEP2PoWi0uL5/1Is0LF5Vwxd+1fz5HlKg==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "@types/gtag.js": "^0.0.12", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-google-tag-manager": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-google-tag-manager/-/plugin-google-tag-manager-3.8.1.tgz", - "integrity": "sha512-iqe3XKITBquZq+6UAXdb1vI0fPY5iIOitVjPQ581R1ZKpHr0qe+V6gVOrrcOHixPDD/BUKdYwkxFjpNiEN+vBw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-sitemap": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-sitemap/-/plugin-sitemap-3.8.1.tgz", - "integrity": "sha512-+9YV/7VLbGTq8qNkjiugIelmfUEVkTyLe6X8bWq7K5qPvGXAjno27QAfFq63mYfFFbJc7z+pudL63acprbqGzw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "fs-extra": "^11.1.1", - "sitemap": "^7.1.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/plugin-svgr": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/plugin-svgr/-/plugin-svgr-3.8.1.tgz", - "integrity": "sha512-rW0LWMDsdlsgowVwqiMb/7tANDodpy1wWPwCcamvhY7OECReN3feoFwLjd/U4tKjNY3encj0AJSTxJA+Fpe+Gw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "@svgr/core": "8.1.0", - "@svgr/webpack": "^8.1.0", - "tslib": "^2.6.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/preset-classic": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/preset-classic/-/preset-classic-3.8.1.tgz", - "integrity": "sha512-yJSjYNHXD8POMGc2mKQuj3ApPrN+eG0rO1UPgSx7jySpYU+n4WjBikbrA2ue5ad9A7aouEtMWUoiSRXTH/g7KQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/plugin-content-blog": "3.8.1", - "@docusaurus/plugin-content-docs": "3.8.1", - "@docusaurus/plugin-content-pages": "3.8.1", - "@docusaurus/plugin-css-cascade-layers": "3.8.1", - "@docusaurus/plugin-debug": "3.8.1", - "@docusaurus/plugin-google-analytics": "3.8.1", - "@docusaurus/plugin-google-gtag": "3.8.1", - "@docusaurus/plugin-google-tag-manager": "3.8.1", - "@docusaurus/plugin-sitemap": "3.8.1", - "@docusaurus/plugin-svgr": "3.8.1", - "@docusaurus/theme-classic": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/theme-search-algolia": "3.8.1", - "@docusaurus/types": "3.8.1" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-classic": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-classic/-/theme-classic-3.8.1.tgz", - "integrity": "sha512-bqDUCNqXeYypMCsE1VcTXSI1QuO4KXfx8Cvl6rYfY0bhhqN6d2WZlRkyLg/p6pm+DzvanqHOyYlqdPyP0iz+iw==", - "license": "MIT", - "dependencies": { - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/module-type-aliases": "3.8.1", - "@docusaurus/plugin-content-blog": "3.8.1", - "@docusaurus/plugin-content-docs": "3.8.1", - "@docusaurus/plugin-content-pages": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/theme-translations": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "@mdx-js/react": "^3.0.0", - "clsx": "^2.0.0", - "copy-text-to-clipboard": "^3.2.0", - "infima": "0.2.0-alpha.45", - "lodash": "^4.17.21", - "nprogress": "^0.2.0", - "postcss": "^8.5.4", - "prism-react-renderer": "^2.3.0", - "prismjs": "^1.29.0", - "react-router-dom": "^5.3.4", - "rtlcss": "^4.1.0", - "tslib": "^2.6.0", - "utility-types": "^3.10.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-common": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-common/-/theme-common-3.8.1.tgz", - "integrity": "sha512-UswMOyTnPEVRvN5Qzbo+l8k4xrd5fTFu2VPPfD6FcW/6qUtVLmJTQCktbAL3KJ0BVXGm5aJXz/ZrzqFuZERGPw==", - "license": "MIT", - "dependencies": { - "@docusaurus/mdx-loader": "3.8.1", - "@docusaurus/module-type-aliases": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router-config": "*", - "clsx": "^2.0.0", - "parse-numeric-range": "^1.3.0", - "prism-react-renderer": "^2.3.0", - "tslib": "^2.6.0", - "utility-types": "^3.10.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "@docusaurus/plugin-content-docs": "*", - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-search-algolia": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-search-algolia/-/theme-search-algolia-3.8.1.tgz", - "integrity": "sha512-NBFH5rZVQRAQM087aYSRKQ9yGEK9eHd+xOxQjqNpxMiV85OhJDD4ZGz6YJIod26Fbooy54UWVdzNU0TFeUUUzQ==", - "license": "MIT", - "dependencies": { - "@docsearch/react": "^3.9.0", - "@docusaurus/core": "3.8.1", - "@docusaurus/logger": "3.8.1", - "@docusaurus/plugin-content-docs": "3.8.1", - "@docusaurus/theme-common": "3.8.1", - "@docusaurus/theme-translations": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-validation": "3.8.1", - "algoliasearch": "^5.17.1", - "algoliasearch-helper": "^3.22.6", - "clsx": "^2.0.0", - "eta": "^2.2.0", - "fs-extra": "^11.1.1", - "lodash": "^4.17.21", - "tslib": "^2.6.0", - "utility-types": "^3.10.0" - }, - "engines": { - "node": ">=18.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/theme-translations": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/theme-translations/-/theme-translations-3.8.1.tgz", - "integrity": "sha512-OTp6eebuMcf2rJt4bqnvuwmm3NVXfzfYejL+u/Y1qwKhZPrjPoKWfk1CbOP5xH5ZOPkiAsx4dHdQBRJszK3z2g==", - "license": "MIT", - "dependencies": { - "fs-extra": "^11.1.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/types": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/types/-/types-3.8.1.tgz", - "integrity": "sha512-ZPdW5AB+pBjiVrcLuw3dOS6BFlrG0XkS2lDGsj8TizcnREQg3J8cjsgfDviszOk4CweNfwo1AEELJkYaMUuOPg==", - "license": "MIT", - "dependencies": { - "@mdx-js/mdx": "^3.0.0", - "@types/history": "^4.7.11", - "@types/react": "*", - "commander": "^5.1.0", - "joi": "^17.9.2", - "react-helmet-async": "npm:@slorber/react-helmet-async@1.3.0", - "utility-types": "^3.10.0", - "webpack": "^5.95.0", - "webpack-merge": "^5.9.0" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0", - "react-dom": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/@docusaurus/types/node_modules/webpack-merge": { - "version": "5.10.0", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", - "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.0" - }, - "engines": { - "node": ">=10.0.0" - } - }, - "node_modules/@docusaurus/utils": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/utils/-/utils-3.8.1.tgz", - "integrity": "sha512-P1ml0nvOmEFdmu0smSXOqTS1sxU5tqvnc0dA4MTKV39kye+bhQnjkIKEE18fNOvxjyB86k8esoCIFM3x4RykOQ==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.8.1", - "@docusaurus/types": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "escape-string-regexp": "^4.0.0", - "execa": "5.1.1", - "file-loader": "^6.2.0", - "fs-extra": "^11.1.1", - "github-slugger": "^1.5.0", - "globby": "^11.1.0", - "gray-matter": "^4.0.3", - "jiti": "^1.20.0", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "micromatch": "^4.0.5", - "p-queue": "^6.6.2", - "prompts": "^2.4.2", - "resolve-pathname": "^3.0.0", - "tslib": "^2.6.0", - "url-loader": "^4.1.1", - "utility-types": "^3.10.0", - "webpack": "^5.88.1" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/utils-common": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-common/-/utils-common-3.8.1.tgz", - "integrity": "sha512-zTZiDlvpvoJIrQEEd71c154DkcriBecm4z94OzEE9kz7ikS3J+iSlABhFXM45mZ0eN5pVqqr7cs60+ZlYLewtg==", - "license": "MIT", - "dependencies": { - "@docusaurus/types": "3.8.1", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@docusaurus/utils-validation": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/@docusaurus/utils-validation/-/utils-validation-3.8.1.tgz", - "integrity": "sha512-gs5bXIccxzEbyVecvxg6upTwaUbfa0KMmTj7HhHzc016AGyxH2o73k1/aOD0IFrdCsfJNt37MqNI47s2MgRZMA==", - "license": "MIT", - "dependencies": { - "@docusaurus/logger": "3.8.1", - "@docusaurus/utils": "3.8.1", - "@docusaurus/utils-common": "3.8.1", - "fs-extra": "^11.2.0", - "joi": "^17.9.2", - "js-yaml": "^4.1.0", - "lodash": "^4.17.21", - "tslib": "^2.6.0" - }, - "engines": { - "node": ">=18.0" - } - }, - "node_modules/@hapi/hoek": { - "version": "9.3.0", - "resolved": "https://registry.npmjs.org/@hapi/hoek/-/hoek-9.3.0.tgz", - "integrity": "sha512-/c6rf4UJlmHlC9b5BaNvzAcFv7HZ2QHaV0D4/HNlBdvFnvQq8RI4kYdhyPCl7Xj+oWvTWQ8ujhqS53LIgAe6KQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@hapi/topo": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/@hapi/topo/-/topo-5.1.0.tgz", - "integrity": "sha512-foQZKJig7Ob0BMAYBfcJk8d77QtOe7Wo4ox7ff1lQYoNNAb6jwcY1ncdoy2e9wQZzvNy7ODZCYJkK8kzmcAnAg==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@jest/schemas": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/schemas/-/schemas-29.6.3.tgz", - "integrity": "sha512-mo5j5X+jIZmJQveBKeS/clAueipV7KgiX1vMgCxam1RNYiqE1w62n0/tJJnHtjW8ZHcQco5gY85jA3mi0L+nSA==", - "license": "MIT", - "dependencies": { - "@sinclair/typebox": "^0.27.8" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jest/types": { - "version": "29.6.3", - "resolved": "https://registry.npmjs.org/@jest/types/-/types-29.6.3.tgz", - "integrity": "sha512-u3UPsIilWKOM3F9CXtrG8LEJmNxwoCQC/XVj4IKYXvvpx7QIi/Kg1LI5uDmDpKlac62NUtX7eLjRh+jVZcLOzw==", - "license": "MIT", - "dependencies": { - "@jest/schemas": "^29.6.3", - "@types/istanbul-lib-coverage": "^2.0.0", - "@types/istanbul-reports": "^3.0.0", - "@types/node": "*", - "@types/yargs": "^17.0.8", - "chalk": "^4.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/@jridgewell/gen-mapping": { - "version": "0.3.13", - "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", - "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", - "license": "MIT", - "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.0", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/remapping": { - "version": "2.3.5", - "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", - "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.24" - } - }, - "node_modules/@jridgewell/resolve-uri": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", - "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@jridgewell/source-map": { - "version": "0.3.11", - "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", - "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", - "license": "MIT", - "dependencies": { - "@jridgewell/gen-mapping": "^0.3.5", - "@jridgewell/trace-mapping": "^0.3.25" - } - }, - "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", - "license": "MIT" - }, - "node_modules/@jridgewell/trace-mapping": { - "version": "0.3.31", - "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", - "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", - "license": "MIT", - "dependencies": { - "@jridgewell/resolve-uri": "^3.1.0", - "@jridgewell/sourcemap-codec": "^1.4.14" - } - }, - "node_modules/@leichtgewicht/ip-codec": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@leichtgewicht/ip-codec/-/ip-codec-2.0.5.tgz", - "integrity": "sha512-Vo+PSpZG2/fmgmiNzYK9qWRh8h/CHrwD0mo1h1DzL4yzHNSfWYujGTYsWGreD000gcgmZ7K4Ys6Tx9TxtsKdDw==", - "license": "MIT" - }, - "node_modules/@mdx-js/mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", - "integrity": "sha512-f6ZO2ifpwAQIpzGWaBQT2TXxPv6z3RBzQKpVftEWN78Vl/YweF1uwussDx8ECAXVtr3Rs89fKyG9YlzUs9DyGQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdx": "^2.0.0", - "acorn": "^8.0.0", - "collapse-white-space": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-util-scope": "^1.0.0", - "estree-walker": "^3.0.0", - "hast-util-to-jsx-runtime": "^2.0.0", - "markdown-extensions": "^2.0.0", - "recma-build-jsx": "^1.0.0", - "recma-jsx": "^1.0.0", - "recma-stringify": "^1.0.0", - "rehype-recma": "^1.0.0", - "remark-mdx": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.0.0", - "source-map": "^0.7.0", - "unified": "^11.0.0", - "unist-util-position-from-estree": "^2.0.0", - "unist-util-stringify-position": "^4.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/@mdx-js/react": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/@mdx-js/react/-/react-3.1.1.tgz", - "integrity": "sha512-f++rKLQgUVYDAtECQ6fn/is15GkEH9+nZPM3MS0RcxVqoTfawHvDlSCH7JbMhAM6uJ32v3eXLvLmLvjGu7PTQw==", - "license": "MIT", - "dependencies": { - "@types/mdx": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "@types/react": ">=16", - "react": ">=16" - } - }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/@pnpm/config.env-replace": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@pnpm/config.env-replace/-/config.env-replace-1.1.0.tgz", - "integrity": "sha512-htyl8TWnKL7K/ESFa1oW2UB5lVDxuF5DpM7tBi6Hu2LNL3mWkIzNLG6N4zoCUP1lCKNxWy/3iu8mS8MvToGd6w==", - "license": "MIT", - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/@pnpm/network.ca-file/-/network.ca-file-1.0.2.tgz", - "integrity": "sha512-YcPQ8a0jwYU9bTdJDpXjMi7Brhkr1mXsXrUJvjqM2mQDgkRiz8jFaQGOdaLxgjtUfQgZhKy/O3cG/YwmgKaxLA==", - "license": "MIT", - "dependencies": { - "graceful-fs": "4.2.10" - }, - "engines": { - "node": ">=12.22.0" - } - }, - "node_modules/@pnpm/network.ca-file/node_modules/graceful-fs": { - "version": "4.2.10", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.10.tgz", - "integrity": "sha512-9ByhssR2fPVsNZj478qUUbKfmL0+t5BDVyjShtyZZLiK7ZDAArFFfopyOTj0M05wE2tJPisA4iTnnXl2YoPvOA==", - "license": "ISC" - }, - "node_modules/@pnpm/npm-conf": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/@pnpm/npm-conf/-/npm-conf-2.3.1.tgz", - "integrity": "sha512-c83qWb22rNRuB0UaVCI0uRPNRr8Z0FWnEIvT47jiHAmOIUHbBOg5XvV7pM5x+rKn9HRpjxquDbXYSXr3fAKFcw==", - "license": "MIT", - "dependencies": { - "@pnpm/config.env-replace": "^1.1.0", - "@pnpm/network.ca-file": "^1.0.1", - "config-chain": "^1.1.11" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/@polka/url": { - "version": "1.0.0-next.29", - "resolved": "https://registry.npmjs.org/@polka/url/-/url-1.0.0-next.29.tgz", - "integrity": "sha512-wwQAWhWSuHaag8c4q/KN/vCoeOJYshAIvMQwD4GpSb3OiZklFfvAgmj0VCBBImRpuF/aFgIRzllXlVX93Jevww==", - "license": "MIT" - }, - "node_modules/@sideway/address": { - "version": "4.1.5", - "resolved": "https://registry.npmjs.org/@sideway/address/-/address-4.1.5.tgz", - "integrity": "sha512-IqO/DUQHUkPeixNQ8n0JA6102hT9CmaljNTPmQ1u8MEhBo/R4Q8eKLN/vGZxuebwOroDB4cbpjheD4+/sKFK4Q==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.0.0" - } - }, - "node_modules/@sideway/formula": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@sideway/formula/-/formula-3.0.1.tgz", - "integrity": "sha512-/poHZJJVjx3L+zVD6g9KgHfYnb443oi7wLu/XKojDviHy6HOEOA6z1Trk5aR1dGcmPenJEgb2sK2I80LeS3MIg==", - "license": "BSD-3-Clause" - }, - "node_modules/@sideway/pinpoint": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/@sideway/pinpoint/-/pinpoint-2.0.0.tgz", - "integrity": "sha512-RNiOoTPkptFtSVzQevY/yWtZwf/RxyVnPy/OcA9HBM3MlGDnBEYL5B41H0MTn0Uec8Hi+2qUtTfG2WWZBmMejQ==", - "license": "BSD-3-Clause" - }, - "node_modules/@sinclair/typebox": { - "version": "0.27.8", - "resolved": "https://registry.npmjs.org/@sinclair/typebox/-/typebox-0.27.8.tgz", - "integrity": "sha512-+Fj43pSMwJs4KRrH/938Uf+uAELIgVBmQzg/q1YG10djyfA3TnrU8N8XzqCh/okZdszqBQTZf96idMfE5lnwTA==", - "license": "MIT" - }, - "node_modules/@sindresorhus/is": { - "version": "4.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-4.6.0.tgz", - "integrity": "sha512-t09vSN3MdfsyCHoFcTRCH/iUtG7OJ0CsjzB8cjAmKc/va/kIgeDI/TxsigdncE/4be734m0cvIYwNaV4i2XqAw==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/@slorber/remark-comment": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/@slorber/remark-comment/-/remark-comment-1.0.0.tgz", - "integrity": "sha512-RCE24n7jsOj1M0UPvIQCHTe7fI0sFL4S2nwKVWwHyVr/wI/H8GosgsJGyhnsZoGFnD/P2hLf1mSbrrgSLN93NA==", - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^1.0.0", - "micromark-util-character": "^1.1.0", - "micromark-util-symbol": "^1.0.1" - } - }, - "node_modules/@svgr/babel-plugin-add-jsx-attribute": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", - "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", - "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", - "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-dynamic-title": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", - "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-svg-em-dimensions": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", - "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-react-native-svg": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", - "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-plugin-transform-svg-component": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", - "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/babel-preset": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", - "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", - "license": "MIT", - "dependencies": { - "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", - "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", - "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", - "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", - "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", - "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", - "@svgr/babel-plugin-transform-svg-component": "8.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@babel/core": "^7.0.0-0" - } - }, - "node_modules/@svgr/core": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", - "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "camelcase": "^6.2.0", - "cosmiconfig": "^8.1.3", - "snake-case": "^3.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/hast-util-to-babel-ast": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", - "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.21.3", - "entities": "^4.4.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@svgr/plugin-jsx": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", - "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@svgr/babel-preset": "8.1.0", - "@svgr/hast-util-to-babel-ast": "8.0.0", - "svg-parser": "^2.0.4" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" - } - }, - "node_modules/@svgr/plugin-svgo": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", - "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", - "license": "MIT", - "dependencies": { - "cosmiconfig": "^8.1.3", - "deepmerge": "^4.3.1", - "svgo": "^3.0.2" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - }, - "peerDependencies": { - "@svgr/core": "*" - } - }, - "node_modules/@svgr/webpack": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", - "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", - "license": "MIT", - "dependencies": { - "@babel/core": "^7.21.3", - "@babel/plugin-transform-react-constant-elements": "^7.21.3", - "@babel/preset-env": "^7.20.2", - "@babel/preset-react": "^7.18.6", - "@babel/preset-typescript": "^7.21.0", - "@svgr/core": "8.1.0", - "@svgr/plugin-jsx": "8.1.0", - "@svgr/plugin-svgo": "8.1.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/gregberge" - } - }, - "node_modules/@szmarczak/http-timer": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-5.0.1.tgz", - "integrity": "sha512-+PmQX0PiAYPMeVYe237LJAYvOMYW1j2rH5YROyS3b4CTVJum34HfRvKvAzozHAQG0TnHNdUfY9nCeUyRAs//cw==", - "license": "MIT", - "dependencies": { - "defer-to-connect": "^2.0.1" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/@trysound/sax": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", - "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", - "license": "ISC", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/@types/body-parser": { - "version": "1.19.6", - "resolved": "https://registry.npmjs.org/@types/body-parser/-/body-parser-1.19.6.tgz", - "integrity": "sha512-HLFeCYgz89uk22N5Qg3dvGvsv46B8GLvKKo1zKG4NybA8U2DiEO3w9lqGg29t/tfLRJpJ6iQxnVw4OnB7MoM9g==", - "license": "MIT", - "dependencies": { - "@types/connect": "*", - "@types/node": "*" - } - }, - "node_modules/@types/bonjour": { - "version": "3.5.13", - "resolved": "https://registry.npmjs.org/@types/bonjour/-/bonjour-3.5.13.tgz", - "integrity": "sha512-z9fJ5Im06zvUL548KvYNecEVlA7cVDkGUi6kZusb04mpyEFKCIZJvloCcmpmLaIahDpOQGHaHmG6imtPMmPXGQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect": { - "version": "3.4.38", - "resolved": "https://registry.npmjs.org/@types/connect/-/connect-3.4.38.tgz", - "integrity": "sha512-K6uROf1LD88uDQqJCktA4yzL1YYAK6NgfsI0v/mTgyPKWsX1CnJ0XPSDhViejru1GcRkLWb8RlzFYJRqGUbaug==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/connect-history-api-fallback": { - "version": "1.5.4", - "resolved": "https://registry.npmjs.org/@types/connect-history-api-fallback/-/connect-history-api-fallback-1.5.4.tgz", - "integrity": "sha512-n6Cr2xS1h4uAulPRdlw6Jl6s1oG8KrVilPN2yUITEs+K48EzMJJ3W1xy8K5eWuFvjp3R74AOIGSmp2UfBJ8HFw==", - "license": "MIT", - "dependencies": { - "@types/express-serve-static-core": "*", - "@types/node": "*" - } - }, - "node_modules/@types/debug": { - "version": "4.1.12", - "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.12.tgz", - "integrity": "sha512-vIChWdVG3LG1SMxEvI/AK+FWJthlrqlTu7fbrlywTkkaONwk/UAGaULXRlf8vkzFBLVm0zkMdCquhL5aOjhXPQ==", - "license": "MIT", - "dependencies": { - "@types/ms": "*" - } - }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "license": "MIT", - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, - "node_modules/@types/eslint-scope": { - "version": "3.7.7", - "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", - "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", - "license": "MIT", - "dependencies": { - "@types/eslint": "*", - "@types/estree": "*" - } - }, - "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", - "license": "MIT" - }, - "node_modules/@types/estree-jsx": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/@types/estree-jsx/-/estree-jsx-1.0.5.tgz", - "integrity": "sha512-52CcUVNFyfb1A2ALocQw/Dd1BQFNmSdkuC3BkZ6iqhdMfQz7JWOFRuJFloOzjk+6WijU56m9oKXFAXc7o3Towg==", - "license": "MIT", - "dependencies": { - "@types/estree": "*" - } - }, - "node_modules/@types/express": { - "version": "4.17.23", - "resolved": "https://registry.npmjs.org/@types/express/-/express-4.17.23.tgz", - "integrity": "sha512-Crp6WY9aTYP3qPi2wGDo9iUe/rceX01UMhnF1jmwDcKCFM6cx7YhGP/Mpr3y9AASpfHixIG0E6azCcL5OcDHsQ==", - "license": "MIT", - "dependencies": { - "@types/body-parser": "*", - "@types/express-serve-static-core": "^4.17.33", - "@types/qs": "*", - "@types/serve-static": "*" - } - }, - "node_modules/@types/express-serve-static-core": { - "version": "5.0.7", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-5.0.7.tgz", - "integrity": "sha512-R+33OsgWw7rOhD1emjU7dzCDHucJrgJXMA5PYCzJxVil0dsyx5iBEPHqpPfiKNJQb7lZ1vxwoLR4Z87bBUpeGQ==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/express/node_modules/@types/express-serve-static-core": { - "version": "4.19.6", - "resolved": "https://registry.npmjs.org/@types/express-serve-static-core/-/express-serve-static-core-4.19.6.tgz", - "integrity": "sha512-N4LZ2xG7DatVqhCZzOGb1Yi5lMbXSZcmdLDe9EzSndPV2HpWYWzRbaerl2n27irrm94EPpprqa8KpskPT085+A==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "@types/qs": "*", - "@types/range-parser": "*", - "@types/send": "*" - } - }, - "node_modules/@types/gtag.js": { - "version": "0.0.12", - "resolved": "https://registry.npmjs.org/@types/gtag.js/-/gtag.js-0.0.12.tgz", - "integrity": "sha512-YQV9bUsemkzG81Ea295/nF/5GijnD2Af7QhEofh7xu+kvCN6RdodgNwwGWXB5GMI3NoyvQo0odNctoH/qLMIpg==", - "license": "MIT" - }, - "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/history": { - "version": "4.7.11", - "resolved": "https://registry.npmjs.org/@types/history/-/history-4.7.11.tgz", - "integrity": "sha512-qjDJRrmvBMiTx+jyLxvLfJU7UznFuokDv4f3WRuriHKERccVpFU+8XMQUAbDzoiJCsmexxRExQeMwwCdamSKDA==", - "license": "MIT" - }, - "node_modules/@types/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/@types/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-oh/6byDPnL1zeNXFrDXFLyZjkr1MsBG667IM792caf1L2UPOOMf65NFzjUH/ltyfwjAGfs1rsX1eftK0jC/KIg==", - "license": "MIT" - }, - "node_modules/@types/http-cache-semantics": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/http-cache-semantics/-/http-cache-semantics-4.0.4.tgz", - "integrity": "sha512-1m0bIFVc7eJWyve9S0RnuRgcQqF/Xd5QsUZAZeQFr1Q3/p9JWoQQEqmVy+DPTNpGXwhgIetAoYF8JSc33q29QA==", - "license": "MIT" - }, - "node_modules/@types/http-errors": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@types/http-errors/-/http-errors-2.0.5.tgz", - "integrity": "sha512-r8Tayk8HJnX0FztbZN7oVqGccWgw98T/0neJphO91KkmOzug1KkofZURD4UaD5uH8AqcFLfdPErnBod0u71/qg==", - "license": "MIT" - }, - "node_modules/@types/http-proxy": { - "version": "1.17.16", - "resolved": "https://registry.npmjs.org/@types/http-proxy/-/http-proxy-1.17.16.tgz", - "integrity": "sha512-sdWoUajOB1cd0A8cRRQ1cfyWNbmFKLAqBB89Y8x5iYyG/mkJHc0YUH8pdWBy2omi9qtCpiIgGjuwO0dQST2l5w==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/istanbul-lib-coverage": { - "version": "2.0.6", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-coverage/-/istanbul-lib-coverage-2.0.6.tgz", - "integrity": "sha512-2QF/t/auWm0lsy8XtKVPG19v3sSOQlJe/YHZgfjb/KBBHOGSV+J2q/S671rcq9uTBrLAXmZpqJiaQbMT+zNU1w==", - "license": "MIT" - }, - "node_modules/@types/istanbul-lib-report": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/istanbul-lib-report/-/istanbul-lib-report-3.0.3.tgz", - "integrity": "sha512-NQn7AHQnk/RSLOxrBbGyJM/aVQ+pjj5HCgasFxc0K/KhoATfQ/47AyUl15I2yBUpihjmas+a+VJBOqecrFH+uA==", - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-coverage": "*" - } - }, - "node_modules/@types/istanbul-reports": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/istanbul-reports/-/istanbul-reports-3.0.4.tgz", - "integrity": "sha512-pk2B1NWalF9toCRu6gjBzR69syFjP4Od8WRAX+0mmf9lAjCRicLOWc+ZrxZHx/0XRjotgkF9t6iaMJ+aXcOdZQ==", - "license": "MIT", - "dependencies": { - "@types/istanbul-lib-report": "*" - } - }, - "node_modules/@types/json-schema": { - "version": "7.0.15", - "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", - "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", - "license": "MIT" - }, - "node_modules/@types/mdast": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/@types/mdast/-/mdast-4.0.4.tgz", - "integrity": "sha512-kGaNbPh1k7AFzgpud/gMdvIm5xuECykRR+JnWKQno9TAXVa6WIVCGTPvYGekIDL4uwCZQSYbUxNBSb1aUo79oA==", - "license": "MIT", - "dependencies": { - "@types/unist": "*" - } - }, - "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", - "license": "MIT" - }, - "node_modules/@types/mime": { - "version": "1.3.5", - "resolved": "https://registry.npmjs.org/@types/mime/-/mime-1.3.5.tgz", - "integrity": "sha512-/pyBZWSLD2n0dcHE3hq8s8ZvcETHtEuF+3E7XVt0Ig2nvsVQXdghHVcEkIWjy9A0wKfTn97a/PSDYohKIlnP/w==", - "license": "MIT" - }, - "node_modules/@types/ms": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/@types/ms/-/ms-2.1.0.tgz", - "integrity": "sha512-GsCCIZDE/p3i96vtEqx+7dBUGXrc7zeSK3wwPHIaRThS+9OhWIXRqzs4d6k1SVU8g91DrNRWxWUGhp5KXQb2VA==", - "license": "MIT" - }, - "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", - "license": "MIT", - "dependencies": { - "undici-types": "~7.12.0" - } - }, - "node_modules/@types/node-forge": { - "version": "1.3.14", - "resolved": "https://registry.npmjs.org/@types/node-forge/-/node-forge-1.3.14.tgz", - "integrity": "sha512-mhVF2BnD4BO+jtOp7z1CdzaK4mbuK0LLQYAvdOLqHTavxFNq4zA1EmYkpnFjP8HOUzedfQkRnp0E2ulSAYSzAw==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/prismjs": { - "version": "1.26.5", - "resolved": "https://registry.npmjs.org/@types/prismjs/-/prismjs-1.26.5.tgz", - "integrity": "sha512-AUZTa7hQ2KY5L7AmtSiqxlhWxb4ina0yd8hNbl4TWuqnv/pFP0nDMb3YrfSBf4hJVGLh2YEIBfKaBW/9UEl6IQ==", - "license": "MIT" - }, - "node_modules/@types/qs": { - "version": "6.14.0", - "resolved": "https://registry.npmjs.org/@types/qs/-/qs-6.14.0.tgz", - "integrity": "sha512-eOunJqu0K1923aExK6y8p6fsihYEn/BYuQ4g0CxAAgFc4b/ZLN4CrsRZ55srTdqoiLzU2B2evC+apEIxprEzkQ==", - "license": "MIT" - }, - "node_modules/@types/range-parser": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/range-parser/-/range-parser-1.2.7.tgz", - "integrity": "sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==", - "license": "MIT" - }, - "node_modules/@types/react": { - "version": "19.1.13", - "resolved": "https://registry.npmjs.org/@types/react/-/react-19.1.13.tgz", - "integrity": "sha512-hHkbU/eoO3EG5/MZkuFSKmYqPbSVk5byPFa3e7y/8TybHiLMACgI8seVYlicwk7H5K/rI2px9xrQp/C+AUDTiQ==", - "license": "MIT", - "dependencies": { - "csstype": "^3.0.2" - } - }, - "node_modules/@types/react-router": { - "version": "5.1.20", - "resolved": "https://registry.npmjs.org/@types/react-router/-/react-router-5.1.20.tgz", - "integrity": "sha512-jGjmu/ZqS7FjSH6owMcD5qpq19+1RS9DeVRqfl1FeBMxTDQAGwlMWOcs52NDoXaNKyG3d1cYQFMs9rCrb88o9Q==", - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*" - } - }, - "node_modules/@types/react-router-config": { - "version": "5.0.11", - "resolved": "https://registry.npmjs.org/@types/react-router-config/-/react-router-config-5.0.11.tgz", - "integrity": "sha512-WmSAg7WgqW7m4x8Mt4N6ZyKz0BubSj/2tVUMsAHp+Yd2AMwcSbeFq9WympT19p5heCFmF97R9eD5uUR/t4HEqw==", - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "^5.1.0" - } - }, - "node_modules/@types/react-router-dom": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/@types/react-router-dom/-/react-router-dom-5.3.3.tgz", - "integrity": "sha512-kpqnYK4wcdm5UaWI3fLcELopqLrHgLqNsdpHauzlQktfkHL3npOSwtj1Uz9oKBAzs7lFtVkV8j83voAz2D8fhw==", - "license": "MIT", - "dependencies": { - "@types/history": "^4.7.11", - "@types/react": "*", - "@types/react-router": "*" - } - }, - "node_modules/@types/retry": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/@types/retry/-/retry-0.12.0.tgz", - "integrity": "sha512-wWKOClTTiizcZhXnPY4wikVAwmdYHp8q6DmC+EJUzAMsycb7HB32Kh9RN4+0gExjmPmZSAQjgURXIGATPegAvA==", - "license": "MIT" - }, - "node_modules/@types/sax": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/@types/sax/-/sax-1.2.7.tgz", - "integrity": "sha512-rO73L89PJxeYM3s3pPPjiPgVVcymqU490g0YO5n5By0k2Erzj6tay/4lr1CHAAU4JyOWd1rpQ8bCf6cZfHU96A==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/send": { - "version": "0.17.5", - "resolved": "https://registry.npmjs.org/@types/send/-/send-0.17.5.tgz", - "integrity": "sha512-z6F2D3cOStZvuk2SaP6YrwkNO65iTZcwA2ZkSABegdkAh/lf+Aa/YQndZVfmEXT5vgAp6zv06VQ3ejSVjAny4w==", - "license": "MIT", - "dependencies": { - "@types/mime": "^1", - "@types/node": "*" - } - }, - "node_modules/@types/serve-index": { - "version": "1.9.4", - "resolved": "https://registry.npmjs.org/@types/serve-index/-/serve-index-1.9.4.tgz", - "integrity": "sha512-qLpGZ/c2fhSs5gnYsQxtDEq3Oy8SXPClIXkW5ghvAvsNuVSA8k+gCONcUCS/UjLEYvYps+e8uBtfgXgvhwfNug==", - "license": "MIT", - "dependencies": { - "@types/express": "*" - } - }, - "node_modules/@types/serve-static": { - "version": "1.15.8", - "resolved": "https://registry.npmjs.org/@types/serve-static/-/serve-static-1.15.8.tgz", - "integrity": "sha512-roei0UY3LhpOJvjbIP6ZZFngyLKl5dskOtDhxY5THRSpO+ZI+nzJ+m5yUMzGrp89YRa7lvknKkMYjqQFGwA7Sg==", - "license": "MIT", - "dependencies": { - "@types/http-errors": "*", - "@types/node": "*", - "@types/send": "*" - } - }, - "node_modules/@types/sockjs": { - "version": "0.3.36", - "resolved": "https://registry.npmjs.org/@types/sockjs/-/sockjs-0.3.36.tgz", - "integrity": "sha512-MK9V6NzAS1+Ud7JV9lJLFqW85VbC9dq3LmwZCuBe4wBDgKC0Kj/jd8Xl+nSviU+Qc3+m7umHHyHg//2KSa0a0Q==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/unist": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-3.0.3.tgz", - "integrity": "sha512-ko/gIFJRv177XgZsZcBwnqJN5x/Gien8qNOn0D5bQU/zAzVf9Zt3BlcUiLqhV9y4ARk0GbT3tnUiPNgnTXzc/Q==", - "license": "MIT" - }, - "node_modules/@types/ws": { - "version": "8.18.1", - "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", - "integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==", - "license": "MIT", - "dependencies": { - "@types/node": "*" - } - }, - "node_modules/@types/yargs": { - "version": "17.0.33", - "resolved": "https://registry.npmjs.org/@types/yargs/-/yargs-17.0.33.tgz", - "integrity": "sha512-WpxBCKWPLr4xSsHgz511rFJAM+wS28w2zEO1QDNY5zM/S8ok70NNfztH0xwhqKyaK0OHCbN98LDAZuy1ctxDkA==", - "license": "MIT", - "dependencies": { - "@types/yargs-parser": "*" - } - }, - "node_modules/@types/yargs-parser": { - "version": "21.0.3", - "resolved": "https://registry.npmjs.org/@types/yargs-parser/-/yargs-parser-21.0.3.tgz", - "integrity": "sha512-I4q9QU9MQv4oEOz4tAHJtNz1cwuLxn2F3xcc2iV5WdqLPpUnj30aUuxt1mAxYTG+oe8CZMV/+6rU4S4gRDzqtQ==", - "license": "MIT" - }, - "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", - "license": "ISC" - }, - "node_modules/@webassemblyjs/ast": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", - "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/helper-numbers": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2" - } - }, - "node_modules/@webassemblyjs/floating-point-hex-parser": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", - "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-api-error": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", - "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-buffer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", - "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-numbers": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", - "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/floating-point-hex-parser": "1.13.2", - "@webassemblyjs/helper-api-error": "1.13.2", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/helper-wasm-bytecode": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", - "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/helper-wasm-section": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", - "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/wasm-gen": "1.14.1" - } - }, - "node_modules/@webassemblyjs/ieee754": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", - "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", - "license": "MIT", - "dependencies": { - "@xtuc/ieee754": "^1.2.0" - } - }, - "node_modules/@webassemblyjs/leb128": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", - "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", - "license": "Apache-2.0", - "dependencies": { - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@webassemblyjs/utf8": { - "version": "1.13.2", - "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", - "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", - "license": "MIT" - }, - "node_modules/@webassemblyjs/wasm-edit": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", - "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/helper-wasm-section": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-opt": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1", - "@webassemblyjs/wast-printer": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-gen": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", - "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wasm-opt": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", - "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-buffer": "1.14.1", - "@webassemblyjs/wasm-gen": "1.14.1", - "@webassemblyjs/wasm-parser": "1.14.1" - } - }, - "node_modules/@webassemblyjs/wasm-parser": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", - "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@webassemblyjs/helper-api-error": "1.13.2", - "@webassemblyjs/helper-wasm-bytecode": "1.13.2", - "@webassemblyjs/ieee754": "1.13.2", - "@webassemblyjs/leb128": "1.13.2", - "@webassemblyjs/utf8": "1.13.2" - } - }, - "node_modules/@webassemblyjs/wast-printer": { - "version": "1.14.1", - "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", - "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", - "license": "MIT", - "dependencies": { - "@webassemblyjs/ast": "1.14.1", - "@xtuc/long": "4.2.2" - } - }, - "node_modules/@xtuc/ieee754": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", - "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", - "license": "BSD-3-Clause" - }, - "node_modules/@xtuc/long": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", - "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", - "license": "Apache-2.0" - }, - "node_modules/accepts": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", - "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "license": "MIT", - "dependencies": { - "mime-types": "~2.1.34", - "negotiator": "0.6.3" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/accepts/node_modules/negotiator": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", - "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/acorn": { - "version": "8.15.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.15.0.tgz", - "integrity": "sha512-NZyJarBfL7nWwIq+FDL6Zp/yHEhePMNnnJ0y3qfieCrmNvYct8uvtiV41UvlSe6apAfk0fY1FbWx+NwfmpvtTg==", - "license": "MIT", - "bin": { - "acorn": "bin/acorn" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/acorn-import-phases": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", - "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "acorn": "^8.14.0" - } - }, - "node_modules/acorn-jsx": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/acorn-jsx/-/acorn-jsx-5.3.2.tgz", - "integrity": "sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==", - "license": "MIT", - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/acorn-walk": { - "version": "8.3.4", - "resolved": "https://registry.npmjs.org/acorn-walk/-/acorn-walk-8.3.4.tgz", - "integrity": "sha512-ueEepnujpqee2o5aIYnvHU6C0A42MNdsIDeqy5BydrkuC5R1ZuUFnm27EeFJGoEHJQgn3uleRvmTXaJgfXbt4g==", - "license": "MIT", - "dependencies": { - "acorn": "^8.11.0" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/address": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/address/-/address-1.2.2.tgz", - "integrity": "sha512-4B/qKCfeE/ODUaAUpSwfzazo5x29WD4r3vXiWsB7I2mSDAihwEqKO+g8GELZUQSSAo5e1XTYh3ZVfLyxBc12nA==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/aggregate-error": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/aggregate-error/-/aggregate-error-3.1.0.tgz", - "integrity": "sha512-4I7Td01quW/RpocfNayFdFVk1qSuoh0E7JrbRJ16nH01HhKFQ88INq9Sd+nd72zqRySlr9BmDA8xlEJ6vJMrYA==", - "license": "MIT", - "dependencies": { - "clean-stack": "^2.0.0", - "indent-string": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ajv": { - "version": "8.17.1", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.17.1.tgz", - "integrity": "sha512-B/gBuNg5SiMTrPkC+A2+cW0RszwxYmn6VYxB/inlBStS5nx6xHIt/ehKRhIMhqusl7a8LjQoZnjCs5vhwxOQ1g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "fast-uri": "^3.0.1", - "json-schema-traverse": "^1.0.0", - "require-from-string": "^2.0.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/ajv-formats": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", - "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", - "license": "MIT", - "dependencies": { - "ajv": "^8.0.0" - }, - "peerDependencies": { - "ajv": "^8.0.0" - }, - "peerDependenciesMeta": { - "ajv": { - "optional": true - } - } - }, - "node_modules/ajv-keywords": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", - "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3" - }, - "peerDependencies": { - "ajv": "^8.8.2" - } - }, - "node_modules/algoliasearch": { - "version": "5.37.0", - "resolved": "https://registry.npmjs.org/algoliasearch/-/algoliasearch-5.37.0.tgz", - "integrity": "sha512-y7gau/ZOQDqoInTQp0IwTOjkrHc4Aq4R8JgpmCleFwiLl+PbN2DMWoDUWZnrK8AhNJwT++dn28Bt4NZYNLAmuA==", - "license": "MIT", - "dependencies": { - "@algolia/abtesting": "1.3.0", - "@algolia/client-abtesting": "5.37.0", - "@algolia/client-analytics": "5.37.0", - "@algolia/client-common": "5.37.0", - "@algolia/client-insights": "5.37.0", - "@algolia/client-personalization": "5.37.0", - "@algolia/client-query-suggestions": "5.37.0", - "@algolia/client-search": "5.37.0", - "@algolia/ingestion": "1.37.0", - "@algolia/monitoring": "1.37.0", - "@algolia/recommend": "5.37.0", - "@algolia/requester-browser-xhr": "5.37.0", - "@algolia/requester-fetch": "5.37.0", - "@algolia/requester-node-http": "5.37.0" - }, - "engines": { - "node": ">= 14.0.0" - } - }, - "node_modules/algoliasearch-helper": { - "version": "3.26.0", - "resolved": "https://registry.npmjs.org/algoliasearch-helper/-/algoliasearch-helper-3.26.0.tgz", - "integrity": "sha512-Rv2x3GXleQ3ygwhkhJubhhYGsICmShLAiqtUuJTUkr9uOCOXyF2E71LVT4XDnVffbknv8XgScP4U0Oxtgm+hIw==", - "license": "MIT", - "dependencies": { - "@algolia/events": "^4.0.1" - }, - "peerDependencies": { - "algoliasearch": ">= 3.1 < 6" - } - }, - "node_modules/ansi-align": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/ansi-align/-/ansi-align-3.0.1.tgz", - "integrity": "sha512-IOfwwBF5iczOjp/WeY4YxyjqAFMQoZufdQWDd19SEExbVLNXqvpzSJ/M7Za4/sCPmQ0+GRquoA7bGcINcxew6w==", - "license": "ISC", - "dependencies": { - "string-width": "^4.1.0" - } - }, - "node_modules/ansi-align/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/ansi-align/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-escapes": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-4.3.2.tgz", - "integrity": "sha512-gKXj5ALrKWQLsYG9jlTRmR/xKluxHV+Z9QEwNIgCfM1/uwPMCuzVVnh5mwTd+OuBZcwSIMbqssNWRm1lE51QaQ==", - "license": "MIT", - "dependencies": { - "type-fest": "^0.21.3" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-escapes/node_modules/type-fest": { - "version": "0.21.3", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-0.21.3.tgz", - "integrity": "sha512-t0rzBq87m3fVcduHDUFhKmyyX+9eo6WQjZvf51Ea/M0Q7+T374Jp1aUiyUl0GKxp8M/OETVHSDvmkyPgvX+X2w==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/ansi-html-community": { - "version": "0.0.8", - "resolved": "https://registry.npmjs.org/ansi-html-community/-/ansi-html-community-0.0.8.tgz", - "integrity": "sha512-1APHAyr3+PCamwNw3bXCPp4HFLONZt/yIH0sZp0/469KWNTEy+qN5jQ3GVX6DMZ1UXAi34yVwtTeaG/HpBuuzw==", - "engines": [ - "node >= 0.8.0" - ], - "license": "Apache-2.0", - "bin": { - "ansi-html": "bin/ansi-html" - } - }, - "node_modules/ansi-regex": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", - "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/ansi-styles": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", - "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "license": "MIT", - "dependencies": { - "color-convert": "^2.0.1" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/anymatch": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", - "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", - "license": "ISC", - "dependencies": { - "normalize-path": "^3.0.0", - "picomatch": "^2.0.4" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/arg": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/arg/-/arg-5.0.2.tgz", - "integrity": "sha512-PYjyFOLKQ9y57JvQ6QLo8dAgNqswh8M1RMJYdQduT6xbWSgK36P/Z/v+p888pM69jMMfS8Xd8F6I1kQ/I9HUGg==", - "license": "MIT" - }, - "node_modules/argparse": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", - "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", - "license": "Python-2.0" - }, - "node_modules/array-flatten": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", - "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "license": "MIT" - }, - "node_modules/array-union": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/array-union/-/array-union-2.1.0.tgz", - "integrity": "sha512-HGyxoOTYUyCM6stUe6EJgnd4EoewAI7zMdfqO+kGjnlZmBDz/cR5pf8r/cR4Wq60sL/p0IkcjUEEPwS3GFrIyw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/astring": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/astring/-/astring-1.9.0.tgz", - "integrity": "sha512-LElXdjswlqjWrPpJFg1Fx4wpkOCxj1TDHlSV4PlaRxHGWko024xICaa97ZkMfs6DRKlCguiAI+rbXv5GWwXIkg==", - "license": "MIT", - "bin": { - "astring": "bin/astring" - } - }, - "node_modules/autoprefixer": { - "version": "10.4.21", - "resolved": "https://registry.npmjs.org/autoprefixer/-/autoprefixer-10.4.21.tgz", - "integrity": "sha512-O+A6LWV5LDHSJD3LjHYoNi4VLsj/Whi7k6zG12xTYaU4cQ8oxQGckXNX8cRHK5yOZ/ppVHe0ZBXGzSV9jXdVbQ==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/autoprefixer" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "browserslist": "^4.24.4", - "caniuse-lite": "^1.0.30001702", - "fraction.js": "^4.3.7", - "normalize-range": "^0.1.2", - "picocolors": "^1.1.1", - "postcss-value-parser": "^4.2.0" - }, - "bin": { - "autoprefixer": "bin/autoprefixer" - }, - "engines": { - "node": "^10 || ^12 || >=14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/babel-loader": { - "version": "9.2.1", - "resolved": "https://registry.npmjs.org/babel-loader/-/babel-loader-9.2.1.tgz", - "integrity": "sha512-fqe8naHt46e0yIdkjUZYqddSXfej3AHajX+CSO5X7oy0EmPc6o5Xh+RClNoHjnieWz9AW4kZxW9yyFMhVB1QLA==", - "license": "MIT", - "dependencies": { - "find-cache-dir": "^4.0.0", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "peerDependencies": { - "@babel/core": "^7.12.0", - "webpack": ">=5" - } - }, - "node_modules/babel-plugin-dynamic-import-node": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/babel-plugin-dynamic-import-node/-/babel-plugin-dynamic-import-node-2.3.3.tgz", - "integrity": "sha512-jZVI+s9Zg3IqA/kdi0i6UDCybUI3aSBLnglhYbSSjKlV7yF1F/5LWv8MakQmvYpnbJDS6fcBL2KzHSxNCMtWSQ==", - "license": "MIT", - "dependencies": { - "object.assign": "^4.1.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2": { - "version": "0.4.14", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.14.tgz", - "integrity": "sha512-Co2Y9wX854ts6U8gAAPXfn0GmAyctHuK8n0Yhfjd6t30g7yvKjspvvOo9yG+z52PZRgFErt7Ka2pYnXCjLKEpg==", - "license": "MIT", - "dependencies": { - "@babel/compat-data": "^7.27.7", - "@babel/helper-define-polyfill-provider": "^0.6.5", - "semver": "^6.3.1" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", - "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - } - }, - "node_modules/babel-plugin-polyfill-corejs3": { - "version": "0.13.0", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.13.0.tgz", - "integrity": "sha512-U+GNwMdSFgzVmfhNm8GJUX88AadB3uo9KpJqS3FaqNIPKgySuvMb+bHPsOmmuWyIcuqZj/pzt1RUIUZns4y2+A==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5", - "core-js-compat": "^3.43.0" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/babel-plugin-polyfill-regenerator": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.5.tgz", - "integrity": "sha512-ISqQ2frbiNU9vIJkzg7dlPpznPZ4jOiUQ1uSmB0fEHeowtN3COYRsXr/xexn64NpU13P06jc/L5TgiJXOgrbEg==", - "license": "MIT", - "dependencies": { - "@babel/helper-define-polyfill-provider": "^0.6.5" - }, - "peerDependencies": { - "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" - } - }, - "node_modules/bail": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/bail/-/bail-2.0.2.tgz", - "integrity": "sha512-0xO6mYd7JB2YesxDKplafRpsiOzPt9V02ddPCLbY1xYGPOX24NTyN50qnUxgCPcSoYMhKpAuBTjQoRZCAkUDRw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "license": "MIT" - }, - "node_modules/baseline-browser-mapping": { - "version": "2.8.6", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.8.6.tgz", - "integrity": "sha512-wrH5NNqren/QMtKUEEJf7z86YjfqW/2uw3IL3/xpqZUC95SSVIFXYQeeGjL6FT/X68IROu6RMehZQS5foy2BXw==", - "license": "Apache-2.0", - "bin": { - "baseline-browser-mapping": "dist/cli.js" - } - }, - "node_modules/batch": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/batch/-/batch-0.6.1.tgz", - "integrity": "sha512-x+VAiMRL6UPkx+kudNvxTl6hB2XNNCG2r+7wixVfIYwu/2HKRXimwQyaumLjMveWvT2Hkd/cAJw+QBMfJ/EKVw==", - "license": "MIT" - }, - "node_modules/big.js": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/big.js/-/big.js-5.2.2.tgz", - "integrity": "sha512-vyL2OymJxmarO8gxMr0mhChsO9QGwhynfuu4+MHTAW6czfq9humCB7rKpUjDd9YUiDPU4mzpyupFSvOClAwbmQ==", - "license": "MIT", - "engines": { - "node": "*" - } - }, - "node_modules/binary-extensions": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", - "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/body-parser": { - "version": "1.20.3", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", - "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "content-type": "~1.0.5", - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "on-finished": "2.4.1", - "qs": "6.13.0", - "raw-body": "2.5.2", - "type-is": "~1.6.18", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/body-parser/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/body-parser/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/body-parser/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/bonjour-service": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/bonjour-service/-/bonjour-service-1.3.0.tgz", - "integrity": "sha512-3YuAUiSkWykd+2Azjgyxei8OWf8thdn8AITIog2M4UICzoqfjlqr64WIjEXZllf/W6vK1goqleSR6brGomxQqA==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.3", - "multicast-dns": "^7.2.5" - } - }, - "node_modules/boolbase": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", - "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", - "license": "ISC" - }, - "node_modules/boxen": { - "version": "6.2.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-6.2.1.tgz", - "integrity": "sha512-H4PEsJXfFI/Pt8sjDWbHlQPx4zL/bvSQjcilJmaulGt5mLDorHOHpmdXAJcBcmru7PhYSp/cDMWRko4ZUMFkSw==", - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^6.2.0", - "chalk": "^4.1.2", - "cli-boxes": "^3.0.0", - "string-width": "^5.0.1", - "type-fest": "^2.5.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.0.1" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/brace-expansion": { - "version": "1.1.12", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.12.tgz", - "integrity": "sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==", - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/braces": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", - "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", - "license": "MIT", - "dependencies": { - "fill-range": "^7.1.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/browserslist": { - "version": "4.26.2", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.26.2.tgz", - "integrity": "sha512-ECFzp6uFOSB+dcZ5BK/IBaGWssbSYBHvuMeMt3MMFyhI0Z8SqGgEkBLARgpRH3hutIgPVsALcMwbDrJqPxQ65A==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "baseline-browser-mapping": "^2.8.3", - "caniuse-lite": "^1.0.30001741", - "electron-to-chromium": "^1.5.218", - "node-releases": "^2.0.21", - "update-browserslist-db": "^1.1.3" - }, - "bin": { - "browserslist": "cli.js" - }, - "engines": { - "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" - } - }, - "node_modules/buffer-from": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", - "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", - "license": "MIT" - }, - "node_modules/bytes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.0.0.tgz", - "integrity": "sha512-pMhOfFDPiv9t5jjIXkHosWmkSyQbvsgEVNkz0ERHbuLh2T/7j4Mqqpz523Fe8MVY89KC6Sh/QfS2sM+SjgFDcw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/cacheable-lookup": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/cacheable-lookup/-/cacheable-lookup-7.0.0.tgz", - "integrity": "sha512-+qJyx4xiKra8mZrcwhjMRMUhD5NR1R8esPkzIYxX96JiecFoxAXFuz/GpR3+ev4PE1WamHip78wV0vcmPQtp8w==", - "license": "MIT", - "engines": { - "node": ">=14.16" - } - }, - "node_modules/cacheable-request": { - "version": "10.2.14", - "resolved": "https://registry.npmjs.org/cacheable-request/-/cacheable-request-10.2.14.tgz", - "integrity": "sha512-zkDT5WAF4hSSoUgyfg5tFIxz8XQK+25W/TLVojJTMKBaxevLBBtLxgqguAuVQB8PVW79FVjHcU+GJ9tVbDZ9mQ==", - "license": "MIT", - "dependencies": { - "@types/http-cache-semantics": "^4.0.2", - "get-stream": "^6.0.1", - "http-cache-semantics": "^4.1.1", - "keyv": "^4.5.3", - "mimic-response": "^4.0.0", - "normalize-url": "^8.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - } - }, - "node_modules/call-bind": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", - "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.0", - "es-define-property": "^1.0.0", - "get-intrinsic": "^1.2.4", - "set-function-length": "^1.2.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/call-bind-apply-helpers": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", - "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/call-bound": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", - "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "get-intrinsic": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/callsites": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/callsites/-/callsites-3.1.0.tgz", - "integrity": "sha512-P8BjAsXvZS+VIDUI11hHCQEv74YT67YUi5JJFNWIqL235sBmjX4+qx9Muvls5ivyNENctx46xQLQ3aTuE7ssaQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/camel-case": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/camel-case/-/camel-case-4.1.2.tgz", - "integrity": "sha512-gxGWBrTT1JuMx6R+o5PTXMmUnhnVzLQ9SNutD4YqKtI6ap897t3tKECYla6gCWEkplXnlNybEkZg9GEGxKFCgw==", - "license": "MIT", - "dependencies": { - "pascal-case": "^3.1.2", - "tslib": "^2.0.3" - } - }, - "node_modules/camelcase": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", - "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/caniuse-api": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/caniuse-api/-/caniuse-api-3.0.0.tgz", - "integrity": "sha512-bsTwuIg/BZZK/vreVTYYbSWoe2F+71P7K5QGEX+pT250DZbfU1MQ5prOKpPR+LL6uWKK3KMwMCAS74QB3Um1uw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.0.0", - "caniuse-lite": "^1.0.0", - "lodash.memoize": "^4.1.2", - "lodash.uniq": "^4.5.0" - } - }, - "node_modules/caniuse-lite": { - "version": "1.0.30001743", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001743.tgz", - "integrity": "sha512-e6Ojr7RV14Un7dz6ASD0aZDmQPT/A+eZU+nuTNfjqmRrmkmQlnTNWH0SKmqagx9PeW87UVqapSurtAXifmtdmw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/caniuse-lite" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "CC-BY-4.0" - }, - "node_modules/ccount": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/ccount/-/ccount-2.0.1.tgz", - "integrity": "sha512-eyrF0jiFpY+3drT6383f1qhkbGsLSifNAjA61IUjZjmLCWjItY6LB9ft9YhoDgwfmclB2zhu51Lc7+95b8NRAg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/chalk": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", - "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.1.0", - "supports-color": "^7.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/char-regex": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/char-regex/-/char-regex-1.0.2.tgz", - "integrity": "sha512-kWWXztvZ5SBQV+eRgKFeh8q5sLuZY2+8WUIzlxWVTg+oGwY14qylx1KbKzHd8P6ZYkAg0xyIDU9JMHhyJMZ1jw==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/character-entities": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/character-entities/-/character-entities-2.0.2.tgz", - "integrity": "sha512-shx7oQ0Awen/BRIdkjkvz54PnEEI/EjwXDSIZp86/KKdbafHh1Df/RYGBhn4hbe2+uKC9FnT5UCEdyPz3ai9hQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-html4": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/character-entities-html4/-/character-entities-html4-2.1.0.tgz", - "integrity": "sha512-1v7fgQRj6hnSwFpq1Eu0ynr/CDEw0rXo2B61qXrLNdHZmPKgb7fqS1a2JwF0rISo9q77jDI8VMEHoApn8qDoZA==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-entities-legacy": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/character-entities-legacy/-/character-entities-legacy-3.0.0.tgz", - "integrity": "sha512-RpPp0asT/6ufRm//AJVwpViZbGM/MkjQFxJccQRHmISF/22NBtsHqAWmL+/pmkPWoIUJdWyeVleTl1wydHATVQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/character-reference-invalid": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/character-reference-invalid/-/character-reference-invalid-2.0.1.tgz", - "integrity": "sha512-iBZ4F4wRbyORVsu0jPV7gXkOsGYjGHPmAyv+HiHG8gi5PtC9KI2j1+v8/tlibRvjoWX027ypmG/n0HtO5t7unw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/cheerio": { - "version": "1.0.0-rc.12", - "resolved": "https://registry.npmjs.org/cheerio/-/cheerio-1.0.0-rc.12.tgz", - "integrity": "sha512-VqR8m68vM46BNnuZ5NtnGBKIE/DfN0cRIzg9n40EIq9NOv90ayxLBXA8fXC5gquFRGJSTRqBq25Jt2ECLR431Q==", - "license": "MIT", - "dependencies": { - "cheerio-select": "^2.1.0", - "dom-serializer": "^2.0.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "htmlparser2": "^8.0.1", - "parse5": "^7.0.0", - "parse5-htmlparser2-tree-adapter": "^7.0.0" - }, - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/cheeriojs/cheerio?sponsor=1" - } - }, - "node_modules/cheerio-select": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/cheerio-select/-/cheerio-select-2.1.0.tgz", - "integrity": "sha512-9v9kG0LvzrlcungtnJtpGNxY+fzECQKhK4EGJX2vByejiMX84MFNQw4UxPJl3bFbTMw+Dfs37XaIkCwTZfLh4g==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-select": "^5.1.0", - "css-what": "^6.1.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/chokidar": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", - "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", - "license": "MIT", - "dependencies": { - "anymatch": "~3.1.2", - "braces": "~3.0.2", - "glob-parent": "~5.1.2", - "is-binary-path": "~2.1.0", - "is-glob": "~4.0.1", - "normalize-path": "~3.0.0", - "readdirp": "~3.6.0" - }, - "engines": { - "node": ">= 8.10.0" - }, - "funding": { - "url": "https://paulmillr.com/funding/" - }, - "optionalDependencies": { - "fsevents": "~2.3.2" - } - }, - "node_modules/chrome-trace-event": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", - "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", - "license": "MIT", - "engines": { - "node": ">=6.0" - } - }, - "node_modules/ci-info": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/ci-info/-/ci-info-3.9.0.tgz", - "integrity": "sha512-NIxF55hv4nSqQswkAeiOi1r83xy8JldOFDTWiug55KBu9Jnblncd2U6ViHmYgHf01TPZS77NJBhBMKdWj9HQMQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/sibiraj-s" - } - ], - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/clean-css": { - "version": "5.3.3", - "resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.3.tgz", - "integrity": "sha512-D5J+kHaVb/wKSFcyyV75uCn8fiY4sV38XJoe4CUyGQ+mOU/fMVYUdH1hJC+CJQ5uY3EnW27SbJYS4X8BiLrAFg==", - "license": "MIT", - "dependencies": { - "source-map": "~0.6.0" - }, - "engines": { - "node": ">= 10.0" - } - }, - "node_modules/clean-css/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/clean-stack": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/clean-stack/-/clean-stack-2.2.0.tgz", - "integrity": "sha512-4diC9HaTE+KRAMWhDhrGOECgWZxoevMc5TlkObMqNSsVU62PYzXZ/SMTjzyGAFF1YusgxGcSWTEXBhp0CPwQ1A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/cli-boxes": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cli-boxes/-/cli-boxes-3.0.0.tgz", - "integrity": "sha512-/lzGpEWL/8PfI0BmBOPRwp0c/wFNX1RdUML3jK/RcSBA9T8mZDdQpqYBKtCFTOfQbwPqWEOpjqW+Fnayc0969g==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/cli-table3": { - "version": "0.6.5", - "resolved": "https://registry.npmjs.org/cli-table3/-/cli-table3-0.6.5.tgz", - "integrity": "sha512-+W/5efTR7y5HRD7gACw9yQjqMVvEMLBHmboM/kPWam+H+Hmyrgjh6YncVKK122YZkXrLudzTuAukUw9FnMf7IQ==", - "license": "MIT", - "dependencies": { - "string-width": "^4.2.0" - }, - "engines": { - "node": "10.* || >= 12.*" - }, - "optionalDependencies": { - "@colors/colors": "1.5.0" - } - }, - "node_modules/cli-table3/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/cli-table3/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/clone-deep": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", - "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", - "license": "MIT", - "dependencies": { - "is-plain-object": "^2.0.4", - "kind-of": "^6.0.2", - "shallow-clone": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/clsx": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", - "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/collapse-white-space": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/collapse-white-space/-/collapse-white-space-2.1.0.tgz", - "integrity": "sha512-loKTxY1zCOuG4j9f6EPnuyyYkf58RnhhWTvRoZEokgB+WbdXehfjFviyOVYkqzEWz1Q5kRiZdBYS5SwxbQYwzw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/color-convert": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", - "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "license": "MIT", - "dependencies": { - "color-name": "~1.1.4" - }, - "engines": { - "node": ">=7.0.0" - } - }, - "node_modules/color-name": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", - "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "license": "MIT" - }, - "node_modules/colord": { - "version": "2.9.3", - "resolved": "https://registry.npmjs.org/colord/-/colord-2.9.3.tgz", - "integrity": "sha512-jeC1axXpnb0/2nn/Y1LPuLdgXBLH7aDcHu4KEKfqw3CUhX7ZpfBSlPKyqXE6btIgEzfWtrX3/tyBCaCvXvMkOw==", - "license": "MIT" - }, - "node_modules/colorette": { - "version": "2.0.20", - "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", - "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", - "license": "MIT" - }, - "node_modules/combine-promises": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/combine-promises/-/combine-promises-1.2.0.tgz", - "integrity": "sha512-VcQB1ziGD0NXrhKxiwyNbCDmRzs/OShMs2GqW2DlU2A/Sd0nQxE1oWDAE5O0ygSx5mgQOn9eIFh7yKPgFRVkPQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/comma-separated-tokens": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/comma-separated-tokens/-/comma-separated-tokens-2.0.3.tgz", - "integrity": "sha512-Fu4hJdvzeylCfQPp9SGWidpzrMs7tTrlu6Vb8XGaRGck8QSNZJJp538Wrb60Lax4fPwR64ViY468OIUTbRlGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/commander": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-5.1.0.tgz", - "integrity": "sha512-P0CysNDQ7rtVw4QIQtm+MRxV66vKFSvlsQvGYXZWR3qFU0jlMKHZZZgw8e+8DSah4UDKMqnknRDQz+xuQXQ/Zg==", - "license": "MIT", - "engines": { - "node": ">= 6" - } - }, - "node_modules/common-path-prefix": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/common-path-prefix/-/common-path-prefix-3.0.0.tgz", - "integrity": "sha512-QE33hToZseCH3jS0qN96O/bSh3kaw/h+Tq7ngyY9eWDUnTlTNUyqfqvCXioLe5Na5jFsL78ra/wuBU4iuEgd4w==", - "license": "ISC" - }, - "node_modules/compressible": { - "version": "2.0.18", - "resolved": "https://registry.npmjs.org/compressible/-/compressible-2.0.18.tgz", - "integrity": "sha512-AF3r7P5dWxL8MxyITRMlORQNaOA2IkAFaTr4k7BUumjPtRpGDTZpl0Pb1XCO6JeDCBdp126Cgs9sMxqSjgYyRg==", - "license": "MIT", - "dependencies": { - "mime-db": ">= 1.43.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compressible/node_modules/mime-db": { - "version": "1.54.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.54.0.tgz", - "integrity": "sha512-aU5EJuIN2WDemCcAp2vFBfp/m4EAhWJnUNSSw0ixs7/kXbd6Pg64EmwJkNdFhB8aWt1sH2CTXrLxo/iAGV3oPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/compression": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/compression/-/compression-1.8.1.tgz", - "integrity": "sha512-9mAqGPHLakhCLeNyxPkK4xVo746zQ/czLH1Ky+vkitMnWfWZps8r0qXuwhwizagCRttsL4lfG4pIOvaWLpAP0w==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "compressible": "~2.0.18", - "debug": "2.6.9", - "negotiator": "~0.6.4", - "on-headers": "~1.1.0", - "safe-buffer": "5.2.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/compression/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/compression/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/compression/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/concat-map": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", - "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "license": "MIT" - }, - "node_modules/config-chain": { - "version": "1.1.13", - "resolved": "https://registry.npmjs.org/config-chain/-/config-chain-1.1.13.tgz", - "integrity": "sha512-qj+f8APARXHrM0hraqXYb2/bOVSV4PvJQlNZ/DVj0QrmNM2q2euizkeuVckQ57J+W0mRH6Hvi+k50M4Jul2VRQ==", - "license": "MIT", - "dependencies": { - "ini": "^1.3.4", - "proto-list": "~1.2.1" - } - }, - "node_modules/config-chain/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/configstore": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/configstore/-/configstore-6.0.0.tgz", - "integrity": "sha512-cD31W1v3GqUlQvbBCGcXmd2Nj9SvLDOP1oQ0YFuLETufzSPaKp11rYBsSOm7rCsW3OnIRAFM3OxRhceaXNYHkA==", - "license": "BSD-2-Clause", - "dependencies": { - "dot-prop": "^6.0.1", - "graceful-fs": "^4.2.6", - "unique-string": "^3.0.0", - "write-file-atomic": "^3.0.3", - "xdg-basedir": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/yeoman/configstore?sponsor=1" - } - }, - "node_modules/connect-history-api-fallback": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/connect-history-api-fallback/-/connect-history-api-fallback-2.0.0.tgz", - "integrity": "sha512-U73+6lQFmfiNPrYbXqr6kZ1i1wiRqXnp2nhMsINseWXO8lDau0LGEffJ8kQi4EjLZympVgRdvqjAgiZ1tgzDDA==", - "license": "MIT", - "engines": { - "node": ">=0.8" - } - }, - "node_modules/consola": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/consola/-/consola-3.4.2.tgz", - "integrity": "sha512-5IKcdX0nnYavi6G7TtOhwkYzyjfJlatbjMjuLSfE2kYT5pMDOilZ4OvMhi637CcDICTmz3wARPoyhqyX1Y+XvA==", - "license": "MIT", - "engines": { - "node": "^14.18.0 || >=16.10.0" - } - }, - "node_modules/content-disposition": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.2.tgz", - "integrity": "sha512-kRGRZw3bLlFISDBgwTSA1TMBFN6J6GWDeubmDE3AF+3+yXL8hTWv8r5rkLbqYXY4RjPk/EzHnClI3zQf1cFmHA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/content-type": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", - "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/convert-source-map": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", - "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", - "license": "MIT" - }, - "node_modules/cookie": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", - "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/cookie-signature": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", - "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "license": "MIT" - }, - "node_modules/copy-text-to-clipboard": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/copy-text-to-clipboard/-/copy-text-to-clipboard-3.2.1.tgz", - "integrity": "sha512-3am6cw+WOicd0+HyzhC4kYS02wHJUiVQXmAADxfUARKsHBkWl1Vl3QQEiILlSs8YcPS/C0+y/urCNEYQk+byWA==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/copy-webpack-plugin/-/copy-webpack-plugin-11.0.0.tgz", - "integrity": "sha512-fX2MWpamkW0hZxMEg0+mYnA40LTosOSa5TqZ9GYIBzyJa9C3QUaMPSE2xAi/buNr8u89SfD9wHSQVBzrRa/SOQ==", - "license": "MIT", - "dependencies": { - "fast-glob": "^3.2.11", - "glob-parent": "^6.0.1", - "globby": "^13.1.1", - "normalize-path": "^3.0.0", - "schema-utils": "^4.0.0", - "serialize-javascript": "^6.0.0" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/glob-parent": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz", - "integrity": "sha512-XxwI8EOhVQgWp6iDL+3b0r86f4d6AX6zSU55HfB4ydCEuXLXc5FcYeOu+nnGftS4TEju/11rt4KJPTMgbfmv4A==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.3" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/copy-webpack-plugin/node_modules/globby": { - "version": "13.2.2", - "resolved": "https://registry.npmjs.org/globby/-/globby-13.2.2.tgz", - "integrity": "sha512-Y1zNGV+pzQdh7H39l9zgB4PJqjRNqydvdYCDG4HFXM4XuvSaQQlEc91IU1yALL8gUTDomgBAfz3XJdmUS+oo0w==", - "license": "MIT", - "dependencies": { - "dir-glob": "^3.0.1", - "fast-glob": "^3.3.0", - "ignore": "^5.2.4", - "merge2": "^1.4.1", - "slash": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/copy-webpack-plugin/node_modules/slash": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-4.0.0.tgz", - "integrity": "sha512-3dOsAHXXUkQTpOYcoAxLIorMTp4gIQr5IW3iVb7A7lFIp0VHhnynm9izx6TssdrIcVIESAlVjtnO2K8bg+Coew==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/core-js": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.45.1.tgz", - "integrity": "sha512-L4NPsJlCfZsPeXukyzHFlg/i7IIVwHSItR0wg0FLNqYClJ4MQYTYLbC7EkjKYRLZF2iof2MUgN0EGy7MdQFChg==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-compat": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.45.1.tgz", - "integrity": "sha512-tqTt5T4PzsMIZ430XGviK4vzYSoeNJ6CXODi6c/voxOT6IZqBht5/EKaSNnYiEjjRYxjVz7DQIsOsY0XNi8PIA==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.25.3" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-js-pure": { - "version": "3.45.1", - "resolved": "https://registry.npmjs.org/core-js-pure/-/core-js-pure-3.45.1.tgz", - "integrity": "sha512-OHnWFKgTUshEU8MK+lOs1H8kC8GkTi9Z1tvNkxrCcw9wl3MJIO7q2ld77wjWn4/xuGrVu2X+nME1iIIPBSdyEQ==", - "hasInstallScript": true, - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/core-js" - } - }, - "node_modules/core-util-is": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", - "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==", - "license": "MIT" - }, - "node_modules/cosmiconfig": { - "version": "8.3.6", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", - "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", - "license": "MIT", - "dependencies": { - "import-fresh": "^3.3.0", - "js-yaml": "^4.1.0", - "parse-json": "^5.2.0", - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/d-fischer" - }, - "peerDependencies": { - "typescript": ">=4.9.5" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, - "node_modules/cross-spawn": { - "version": "7.0.6", - "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", - "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", - "license": "MIT", - "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/crypto-random-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/crypto-random-string/-/crypto-random-string-4.0.0.tgz", - "integrity": "sha512-x8dy3RnvYdlUcPOjkEHqozhiwzKNSq7GcPuXFbnyMOCHxX8V3OgIg/pYuabl2sbUPfIJaeAQB7PMOK8DFIdoRA==", - "license": "MIT", - "dependencies": { - "type-fest": "^1.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/crypto-random-string/node_modules/type-fest": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-1.4.0.tgz", - "integrity": "sha512-yGSza74xk0UG8k+pLh5oeoYirvIiWo5t0/o3zHHAO2tRDiZcxWP7fywNlXhqb6/r6sWvwi+RsyQMWhVLe4BVuA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/css-blank-pseudo": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/css-blank-pseudo/-/css-blank-pseudo-7.0.1.tgz", - "integrity": "sha512-jf+twWGDf6LDoXDUode+nc7ZlrqfaNphrBIBrcmeP3D8yw1uPaix1gCC8LUQUGQ6CycuK2opkbFFWFuq/a94ag==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-blank-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/css-declaration-sorter": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/css-declaration-sorter/-/css-declaration-sorter-7.3.0.tgz", - "integrity": "sha512-LQF6N/3vkAMYF4xoHLJfG718HRJh34Z8BnNhd6bosOMIVjMlhuZK5++oZa3uYAgrI5+7x2o27gUqTR2U/KjUOQ==", - "license": "ISC", - "engines": { - "node": "^14 || ^16 || >=18" - }, - "peerDependencies": { - "postcss": "^8.0.9" - } - }, - "node_modules/css-has-pseudo": { - "version": "7.0.3", - "resolved": "https://registry.npmjs.org/css-has-pseudo/-/css-has-pseudo-7.0.3.tgz", - "integrity": "sha512-oG+vKuGyqe/xvEMoxAQrhi7uY16deJR3i7wwhBerVrGQKSqUC5GiOVxTpM9F9B9hw0J+eKeOWLH7E9gZ1Dr5rA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-specificity": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-has-pseudo/node_modules/@csstools/selector-specificity": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", - "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/css-has-pseudo/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/css-loader": { - "version": "6.11.0", - "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", - "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", - "license": "MIT", - "dependencies": { - "icss-utils": "^5.1.0", - "postcss": "^8.4.33", - "postcss-modules-extract-imports": "^3.1.0", - "postcss-modules-local-by-default": "^4.0.5", - "postcss-modules-scope": "^3.2.0", - "postcss-modules-values": "^4.0.0", - "postcss-value-parser": "^4.2.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/css-minimizer-webpack-plugin": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/css-minimizer-webpack-plugin/-/css-minimizer-webpack-plugin-5.0.1.tgz", - "integrity": "sha512-3caImjKFQkS+ws1TGcFn0V1HyDJFq1Euy589JlD6/3rV2kj+w7r5G9WDMgSHvpvXHNZ2calVypZWuEDQd9wfLg==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.18", - "cssnano": "^6.0.1", - "jest-worker": "^29.4.3", - "postcss": "^8.4.24", - "schema-utils": "^4.0.1", - "serialize-javascript": "^6.0.1" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - }, - "peerDependenciesMeta": { - "@parcel/css": { - "optional": true - }, - "@swc/css": { - "optional": true - }, - "clean-css": { - "optional": true - }, - "csso": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "lightningcss": { - "optional": true - } - } - }, - "node_modules/css-prefers-color-scheme": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/css-prefers-color-scheme/-/css-prefers-color-scheme-10.0.0.tgz", - "integrity": "sha512-VCtXZAWivRglTZditUfB4StnsWr6YVZ2PRtuxQLKTNRdtAf8tpzaVPE9zXIF3VaSc7O70iK/j1+NXxyQCqdPjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/css-tree": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", - "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.30", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" - } - }, - "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">= 6" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/cssdb": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/cssdb/-/cssdb-8.4.2.tgz", - "integrity": "sha512-PzjkRkRUS+IHDJohtxkIczlxPPZqRo0nXplsYXOMBRPjcVRjj1W4DfvRgshUYTVuUigU7ptVYkFJQ7abUB0nyg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - }, - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - } - ], - "license": "MIT-0" - }, - "node_modules/cssesc": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", - "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", - "license": "MIT", - "bin": { - "cssesc": "bin/cssesc" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/cssnano": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano/-/cssnano-6.1.2.tgz", - "integrity": "sha512-rYk5UeX7VAM/u0lNqewCdasdtPK81CgX8wJFLEIXHbV2oldWRgJAsZrdhRXkV1NJzA2g850KiFm9mMU2HxNxMA==", - "license": "MIT", - "dependencies": { - "cssnano-preset-default": "^6.1.2", - "lilconfig": "^3.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/cssnano" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-preset-advanced": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-advanced/-/cssnano-preset-advanced-6.1.2.tgz", - "integrity": "sha512-Nhao7eD8ph2DoHolEzQs5CfRpiEP0xa1HBdnFZ82kvqdmbwVBUr2r1QuQ4t1pi+D1ZpqpcO4T+wy/7RxzJ/WPQ==", - "license": "MIT", - "dependencies": { - "autoprefixer": "^10.4.19", - "browserslist": "^4.23.0", - "cssnano-preset-default": "^6.1.2", - "postcss-discard-unused": "^6.0.5", - "postcss-merge-idents": "^6.0.3", - "postcss-reduce-idents": "^6.0.3", - "postcss-zindex": "^6.0.2" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-preset-default": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/cssnano-preset-default/-/cssnano-preset-default-6.1.2.tgz", - "integrity": "sha512-1C0C+eNaeN8OcHQa193aRgYexyJtU8XwbdieEjClw+J9d94E41LwT6ivKH0WT+fYwYWB0Zp3I3IZ7tI/BbUbrg==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "css-declaration-sorter": "^7.2.0", - "cssnano-utils": "^4.0.2", - "postcss-calc": "^9.0.1", - "postcss-colormin": "^6.1.0", - "postcss-convert-values": "^6.1.0", - "postcss-discard-comments": "^6.0.2", - "postcss-discard-duplicates": "^6.0.3", - "postcss-discard-empty": "^6.0.3", - "postcss-discard-overridden": "^6.0.2", - "postcss-merge-longhand": "^6.0.5", - "postcss-merge-rules": "^6.1.1", - "postcss-minify-font-values": "^6.1.0", - "postcss-minify-gradients": "^6.0.3", - "postcss-minify-params": "^6.1.0", - "postcss-minify-selectors": "^6.0.4", - "postcss-normalize-charset": "^6.0.2", - "postcss-normalize-display-values": "^6.0.2", - "postcss-normalize-positions": "^6.0.2", - "postcss-normalize-repeat-style": "^6.0.2", - "postcss-normalize-string": "^6.0.2", - "postcss-normalize-timing-functions": "^6.0.2", - "postcss-normalize-unicode": "^6.1.0", - "postcss-normalize-url": "^6.0.2", - "postcss-normalize-whitespace": "^6.0.2", - "postcss-ordered-values": "^6.0.2", - "postcss-reduce-initial": "^6.1.0", - "postcss-reduce-transforms": "^6.0.2", - "postcss-svgo": "^6.0.3", - "postcss-unique-selectors": "^6.0.4" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/cssnano-utils": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/cssnano-utils/-/cssnano-utils-4.0.2.tgz", - "integrity": "sha512-ZR1jHg+wZ8o4c3zqf1SIUSTIvm/9mU343FMR6Obe/unskbvpGhZOo1J6d/r8D1pzkRQYuwbcH3hToOuoA2G7oQ==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/csso": { - "version": "5.0.5", - "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", - "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", - "license": "MIT", - "dependencies": { - "css-tree": "~2.2.0" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/css-tree": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", - "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", - "license": "MIT", - "dependencies": { - "mdn-data": "2.0.28", - "source-map-js": "^1.0.1" - }, - "engines": { - "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", - "npm": ">=7.0.0" - } - }, - "node_modules/csso/node_modules/mdn-data": { - "version": "2.0.28", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", - "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", - "license": "CC0-1.0" - }, - "node_modules/csstype": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", - "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==", - "license": "MIT" - }, - "node_modules/debounce": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/debounce/-/debounce-1.2.1.tgz", - "integrity": "sha512-XRRe6Glud4rd/ZGQfiV1ruXSfbvfJedlV9Y6zOlP+2K04vBYiJEte6stfFkCP03aMnY5tsipamumUjL14fofug==", - "license": "MIT" - }, - "node_modules/debug": { - "version": "4.4.3", - "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", - "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", - "license": "MIT", - "dependencies": { - "ms": "^2.1.3" - }, - "engines": { - "node": ">=6.0" - }, - "peerDependenciesMeta": { - "supports-color": { - "optional": true - } - } - }, - "node_modules/decode-named-character-reference": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/decode-named-character-reference/-/decode-named-character-reference-1.2.0.tgz", - "integrity": "sha512-c6fcElNV6ShtZXmsgNgFFV5tVX2PaV4g+MOAkb8eXHvn6sryJBrZa9r0zV6+dtTyoCKxtDy5tyQ5ZwQuidtd+Q==", - "license": "MIT", - "dependencies": { - "character-entities": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/decompress-response": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", - "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", - "license": "MIT", - "dependencies": { - "mimic-response": "^3.1.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/decompress-response/node_modules/mimic-response": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", - "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/deep-extend": { - "version": "0.6.0", - "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", - "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", - "license": "MIT", - "engines": { - "node": ">=4.0.0" - } - }, - "node_modules/deepmerge": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", - "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/default-gateway": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/default-gateway/-/default-gateway-6.0.3.tgz", - "integrity": "sha512-fwSOJsbbNzZ/CUFpqFBqYfYNLj1NbMPm8MMCIzHjC83iSJRBEGmDUxU+WP661BaBQImeC2yHwXtz+P/O9o+XEg==", - "license": "BSD-2-Clause", - "dependencies": { - "execa": "^5.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/defer-to-connect": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/defer-to-connect/-/defer-to-connect-2.0.1.tgz", - "integrity": "sha512-4tvttepXG1VaYGrRibk5EwJd1t4udunSOVMdLSAL6mId1ix438oPwPZMALY41FCijukO1L0twNcGsdzS7dHgDg==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/define-data-property": { - "version": "1.1.4", - "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", - "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0", - "es-errors": "^1.3.0", - "gopd": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/define-lazy-prop": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/define-lazy-prop/-/define-lazy-prop-2.0.0.tgz", - "integrity": "sha512-Ds09qNh8yw3khSjiJjiUInaGX9xlqZDY7JVryGxdxV7NPeuqQfplOpQ66yJFZut3jLa5zOwkXw1g9EI2uKh4Og==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/define-properties": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", - "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.0.1", - "has-property-descriptors": "^1.0.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/depd": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", - "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/dequal": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/dequal/-/dequal-2.0.3.tgz", - "integrity": "sha512-0je+qPKHEMohvfRTCEo3CrPG6cAzAYgmzKyxRiYSSDkS6eGJdyVJm7WaYA5ECaAD9wLB2T4EEeymA5aFVcYXCA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/destroy": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", - "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "license": "MIT", - "engines": { - "node": ">= 0.8", - "npm": "1.2.8000 || >= 1.4.16" - } - }, - "node_modules/detect-node": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/detect-node/-/detect-node-2.1.0.tgz", - "integrity": "sha512-T0NIuQpnTvFDATNuHN5roPwSBG83rFsuO+MXXH9/3N1eFbn4wcPjttvjMLEPWJ0RGUYgQE7cGgS3tNxbqCGM7g==", - "license": "MIT" - }, - "node_modules/detect-port": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/detect-port/-/detect-port-1.6.1.tgz", - "integrity": "sha512-CmnVc+Hek2egPx1PeTFVta2W78xy2K/9Rkf6cC4T59S50tVnzKj+tnx5mmx5lwvCkujZ4uRrpRSuV+IVs3f90Q==", - "license": "MIT", - "dependencies": { - "address": "^1.0.1", - "debug": "4" - }, - "bin": { - "detect": "bin/detect-port.js", - "detect-port": "bin/detect-port.js" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/devlop": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/devlop/-/devlop-1.1.0.tgz", - "integrity": "sha512-RWmIqhcFf1lRYBvNmr7qTNuyCt/7/ns2jbpp1+PalgE/rDQcBT0fioSMUpJ93irlUhC5hrg4cYqe6U+0ImW0rA==", - "license": "MIT", - "dependencies": { - "dequal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/dir-glob": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/dir-glob/-/dir-glob-3.0.1.tgz", - "integrity": "sha512-WkrWp9GR4KXfKGYzOLmTuGVi1UWFfws377n9cc55/tb6DuqyF6pcQ5AbiHEshaDpY9v6oaSr2XCDidGmMwdzIA==", - "license": "MIT", - "dependencies": { - "path-type": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/dns-packet": { - "version": "5.6.1", - "resolved": "https://registry.npmjs.org/dns-packet/-/dns-packet-5.6.1.tgz", - "integrity": "sha512-l4gcSouhcgIKRvyy99RNVOgxXiicE+2jZoNmaNmZ6JXiGajBOJAesk1OBlJuM5k2c+eudGdLxDqXuPCKIj6kpw==", - "license": "MIT", - "dependencies": { - "@leichtgewicht/ip-codec": "^2.0.1" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/dom-converter": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/dom-converter/-/dom-converter-0.2.0.tgz", - "integrity": "sha512-gd3ypIPfOMr9h5jIKq8E3sHOTCjeirnl0WK5ZdS1AW0Odt0b1PaWaHdJ4Qk4klv+YB9aJBS7mESXjFoDQPu6DA==", - "license": "MIT", - "dependencies": { - "utila": "~0.4" - } - }, - "node_modules/dom-serializer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", - "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.2", - "entities": "^4.2.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/domelementtype": { - "version": "2.3.0", - "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", - "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "BSD-2-Clause" - }, - "node_modules/domhandler": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", - "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.3.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/domutils": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.2.2.tgz", - "integrity": "sha512-6kZKyUajlDuqlHKVX1w7gyslj9MPIXzIFiz/rGu35uC1wMi+kMhQwGhl4lt9unC9Vb9INnY9Z3/ZA3+FhASLaw==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^2.0.0", - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/dot-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", - "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/dot-prop": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/dot-prop/-/dot-prop-6.0.1.tgz", - "integrity": "sha512-tE7ztYzXHIeyvc7N+hR3oi7FIbf/NIjVP9hmAt3yMXzrQ072/fpjGLx2GxNxGxUl5V73MEqYzioOMoVhGMJ5cA==", - "license": "MIT", - "dependencies": { - "is-obj": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/dot-prop/node_modules/is-obj": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-2.0.0.tgz", - "integrity": "sha512-drqDG3cbczxxEJRoOXcOjtdp1J/lyp1mNn0xaznRs8+muBhgQcrnbspox5X5fOw0HnMnbfDzvnEMEtqDEJEo8w==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/dunder-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", - "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.1", - "es-errors": "^1.3.0", - "gopd": "^1.2.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/duplexer": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/duplexer/-/duplexer-0.1.2.tgz", - "integrity": "sha512-jtD6YG370ZCIi/9GTaJKQxWTZD045+4R4hTk/x1UyoqadyJ9x9CgSi1RlVDQF8U2sxLLSnFkCaMihqljHIWgMg==", - "license": "MIT" - }, - "node_modules/eastasianwidth": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", - "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==", - "license": "MIT" - }, - "node_modules/ee-first": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", - "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "license": "MIT" - }, - "node_modules/electron-to-chromium": { - "version": "1.5.222", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.222.tgz", - "integrity": "sha512-gA7psSwSwQRE60CEoLz6JBCQPIxNeuzB2nL8vE03GK/OHxlvykbLyeiumQy1iH5C2f3YbRAZpGCMT12a/9ih9w==", - "license": "ISC" - }, - "node_modules/emoji-regex": { - "version": "9.2.2", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", - "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==", - "license": "MIT" - }, - "node_modules/emojilib": { - "version": "2.4.0", - "resolved": "https://registry.npmjs.org/emojilib/-/emojilib-2.4.0.tgz", - "integrity": "sha512-5U0rVMU5Y2n2+ykNLQqMoqklN9ICBT/KsvC1Gz6vqHbz2AXXGkG+Pm5rMWk/8Vjrr/mY9985Hi8DYzn1F09Nyw==", - "license": "MIT" - }, - "node_modules/emojis-list": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/emojis-list/-/emojis-list-3.0.0.tgz", - "integrity": "sha512-/kyM18EfinwXZbno9FyUGeFh87KC8HRQBQGildHZbEuRyWFOmv1U10o9BBp8XVZDVNNuQKyIGIu5ZYAAXJ0V2Q==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/emoticon": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/emoticon/-/emoticon-4.1.0.tgz", - "integrity": "sha512-VWZfnxqwNcc51hIy/sbOdEem6D+cVtpPzEEtVAFdaas30+1dgkyaOQ4sQ6Bp0tOMqWO1v+HQfYaoodOkdhK6SQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/encodeurl": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", - "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/enhanced-resolve": { - "version": "5.18.3", - "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.18.3.tgz", - "integrity": "sha512-d4lC8xfavMeBjzGr2vECC3fsGXziXZQyJxD868h2M/mBI3PwAuODxAkLkq5HYuvrPYcUtiLzsTo8U3PgX3Ocww==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.4", - "tapable": "^2.2.0" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/entities": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", - "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/error-ex": { - "version": "1.3.4", - "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.4.tgz", - "integrity": "sha512-sqQamAnR14VgCr1A618A3sGrygcpK+HEbenA/HiEAkkUwcZIIB/tgWqHFxWgOyDh4nB4JCRimh79dR5Ywc9MDQ==", - "license": "MIT", - "dependencies": { - "is-arrayish": "^0.2.1" - } - }, - "node_modules/es-define-property": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", - "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-errors": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", - "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/es-module-lexer": { - "version": "1.7.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-1.7.0.tgz", - "integrity": "sha512-jEQoCwk8hyb2AZziIOLhDqpm5+2ww5uIE6lkO/6jcOCusfk6LhMHpXXfBLXTZ7Ydyt0j4VoUQv6uGNYbdW+kBA==", - "license": "MIT" - }, - "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/esast-util-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/esast-util-from-estree/-/esast-util-from-estree-2.0.0.tgz", - "integrity": "sha512-4CyanoAudUSBAn5K13H4JhsMH6L9ZP7XbLVe/dKybkxMO7eDyLsT8UHl9TRNrU2Gr9nz+FovfSIjuXWJ81uVwQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/esast-util-from-js": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/esast-util-from-js/-/esast-util-from-js-2.0.1.tgz", - "integrity": "sha512-8Ja+rNJ0Lt56Pcf3TAmpBZjmx8ZcK5Ts4cAzIOjsjevg9oSXJnl6SUQ2EevU8tv3h6ZLWmoKL5H4fgWvdvfETw==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "acorn": "^8.0.0", - "esast-util-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/escalade": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", - "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/escape-goat": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-goat/-/escape-goat-4.0.0.tgz", - "integrity": "sha512-2Sd4ShcWxbx6OY1IHyla/CVNwvg7XwZVoXZHcSu9w9SReNP1EzzD5T8NWKIR38fIqEns9kDWKUQTXXAmlDrdPg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/escape-html": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", - "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "license": "MIT" - }, - "node_modules/escape-string-regexp": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", - "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/eslint-scope": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", - "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", - "license": "BSD-2-Clause", - "dependencies": { - "esrecurse": "^4.3.0", - "estraverse": "^4.1.1" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/esprima": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/esprima/-/esprima-4.0.1.tgz", - "integrity": "sha512-eGuFFw7Upda+g4p+QHvnW0RyTX/SVeJBDM/gCtMARO0cLuT2HcEKnTPvhjV6aGeqrCB/sbNop0Kszm0jsaWU4A==", - "license": "BSD-2-Clause", - "bin": { - "esparse": "bin/esparse.js", - "esvalidate": "bin/esvalidate.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/esrecurse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", - "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", - "license": "BSD-2-Clause", - "dependencies": { - "estraverse": "^5.2.0" - }, - "engines": { - "node": ">=4.0" - } - }, - "node_modules/esrecurse/node_modules/estraverse": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", - "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estraverse": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", - "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=4.0" - } - }, - "node_modules/estree-util-attach-comments": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-attach-comments/-/estree-util-attach-comments-3.0.0.tgz", - "integrity": "sha512-cKUwm/HUcTDsYh/9FgnuFqpfquUbwIqwKM26BVCGDPVgvaCl/nDCCjUfiLlx6lsEZ3Z4RFxNbOQ60pkaEwFxGw==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-build-jsx": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/estree-util-build-jsx/-/estree-util-build-jsx-3.0.1.tgz", - "integrity": "sha512-8U5eiL6BTrPxp/CHbs2yMgP8ftMhR5ww1eIKoWRMlqvltHF8fZn5LRDvTKuxD3DUn+shRbLGqXemcP51oFCsGQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "estree-walker": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-is-identifier-name": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/estree-util-is-identifier-name/-/estree-util-is-identifier-name-3.0.0.tgz", - "integrity": "sha512-hFtqIDZTIUZ9BXLb8y4pYGyk6+wekIivNVTcmvk8NoOh+VeRn5y6cEHzbURrWbfp1fIqdVipilzj+lfaadNZmg==", - "license": "MIT", - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-scope": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", - "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-to-js": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-to-js/-/estree-util-to-js-2.0.0.tgz", - "integrity": "sha512-WDF+xj5rRWmD5tj6bIqRi6CkLIXbbNQUcxQHzGysQzvHmdYG2G7p/Tf0J0gpxGgkeMZNTIjT/AoSvC9Xehcgdg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "astring": "^1.8.0", - "source-map": "^0.7.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-util-value-to-estree": { - "version": "3.4.0", - "resolved": "https://registry.npmjs.org/estree-util-value-to-estree/-/estree-util-value-to-estree-3.4.0.tgz", - "integrity": "sha512-Zlp+gxis+gCfK12d3Srl2PdX2ybsEA8ZYy6vQGVQTNNYLEGRQQ56XB64bjemN8kxIKXP1nC9ip4Z+ILy9LGzvQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/remcohaszing" - } - }, - "node_modules/estree-util-visit": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/estree-util-visit/-/estree-util-visit-2.0.0.tgz", - "integrity": "sha512-m5KgiH85xAhhW8Wta0vShLcUvOsh3LLPI2YVwcbio1l7E09NTLL1EyMZFM1OyWowoH0skScNbhOPl4kcBgzTww==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/estree-walker": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-3.0.3.tgz", - "integrity": "sha512-7RUKfXgSMMkzt6ZuXmqapOurLGPPfgj6l9uRZ7lRGolvk0y2yocc35LdcxKC5PQZdn2DMqioAQ2NoWcrTKmm6g==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0" - } - }, - "node_modules/esutils": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/esutils/-/esutils-2.0.3.tgz", - "integrity": "sha512-kVscqXk4OCp68SZ0dkgEKVi6/8ij300KBWTJq32P/dYeWTSwK41WyTxalN1eRmA5Z9UU/LX9D7FWSmV9SAYx6g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/eta": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/eta/-/eta-2.2.0.tgz", - "integrity": "sha512-UVQ72Rqjy/ZKQalzV5dCCJP80GrmPrMxh6NlNf+erV6ObL0ZFkhCstWRawS85z3smdr3d2wXPsZEY7rDPfGd2g==", - "license": "MIT", - "engines": { - "node": ">=6.0.0" - }, - "funding": { - "url": "https://github.com/eta-dev/eta?sponsor=1" - } - }, - "node_modules/etag": { - "version": "1.8.1", - "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", - "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/eval": { - "version": "0.1.8", - "resolved": "https://registry.npmjs.org/eval/-/eval-0.1.8.tgz", - "integrity": "sha512-EzV94NYKoO09GLXGjXj9JIlXijVck4ONSr5wiCWDvhsvj5jxSrzTmRU/9C1DyB6uToszLs8aifA6NQ7lEQdvFw==", - "dependencies": { - "@types/node": "*", - "require-like": ">= 0.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/eventemitter3": { - "version": "4.0.7", - "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", - "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==", - "license": "MIT" - }, - "node_modules/events": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", - "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", - "license": "MIT", - "engines": { - "node": ">=0.8.x" - } - }, - "node_modules/execa": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/execa/-/execa-5.1.1.tgz", - "integrity": "sha512-8uSpZZocAZRBAPIEINJj3Lo9HyGitllczc27Eh5YYojjMFMn8yHMDMaUHE2Jqfq05D/wucwI4JGURyXt1vchyg==", - "license": "MIT", - "dependencies": { - "cross-spawn": "^7.0.3", - "get-stream": "^6.0.0", - "human-signals": "^2.1.0", - "is-stream": "^2.0.0", - "merge-stream": "^2.0.0", - "npm-run-path": "^4.0.1", - "onetime": "^5.1.2", - "signal-exit": "^3.0.3", - "strip-final-newline": "^2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sindresorhus/execa?sponsor=1" - } - }, - "node_modules/express": { - "version": "4.21.2", - "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", - "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.8", - "array-flatten": "1.1.1", - "body-parser": "1.20.3", - "content-disposition": "0.5.4", - "content-type": "~1.0.4", - "cookie": "0.7.1", - "cookie-signature": "1.0.6", - "debug": "2.6.9", - "depd": "2.0.0", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "finalhandler": "1.3.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "merge-descriptors": "1.0.3", - "methods": "~1.1.2", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "path-to-regexp": "0.1.12", - "proxy-addr": "~2.0.7", - "qs": "6.13.0", - "range-parser": "~1.2.1", - "safe-buffer": "5.2.1", - "send": "0.19.0", - "serve-static": "1.16.2", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "type-is": "~1.6.18", - "utils-merge": "1.0.1", - "vary": "~1.1.2" - }, - "engines": { - "node": ">= 0.10.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/express" - } - }, - "node_modules/express/node_modules/content-disposition": { - "version": "0.5.4", - "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", - "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "5.2.1" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/express/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/express/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/express/node_modules/path-to-regexp": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", - "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "license": "MIT" - }, - "node_modules/express/node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/extend": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", - "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", - "license": "MIT" - }, - "node_modules/extend-shallow": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/extend-shallow/-/extend-shallow-2.0.1.tgz", - "integrity": "sha512-zCnTtlxNoAiDc3gqY2aYAWFx7XWWiasuF2K8Me5WbN8otHKTUKBwjPtNpRs/rbUZm7KxWAaNj7P1a/p52GbVug==", - "license": "MIT", - "dependencies": { - "is-extendable": "^0.1.0" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/fast-deep-equal": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", - "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", - "license": "MIT" - }, - "node_modules/fast-glob": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/fast-glob/-/fast-glob-3.3.3.tgz", - "integrity": "sha512-7MptL8U0cqcFdzIzwOTHoilX9x5BrNqye7Z/LuC7kCMRio1EMSyqRK3BEAUD7sXRq4iT4AzTVuZdhgQ2TCvYLg==", - "license": "MIT", - "dependencies": { - "@nodelib/fs.stat": "^2.0.2", - "@nodelib/fs.walk": "^1.2.3", - "glob-parent": "^5.1.2", - "merge2": "^1.3.0", - "micromatch": "^4.0.8" - }, - "engines": { - "node": ">=8.6.0" - } - }, - "node_modules/fast-json-stable-stringify": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/fast-json-stable-stringify/-/fast-json-stable-stringify-2.1.0.tgz", - "integrity": "sha512-lhd/wF+Lk98HZoTCtlVraHtfh5XYijIjalXck7saUtuanSDyLMxnHhSXEDJqHxD7msR8D0uCmqlkwjCV8xvwHw==", - "license": "MIT" - }, - "node_modules/fast-uri": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", - "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/fastify" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/fastify" - } - ], - "license": "BSD-3-Clause" - }, - "node_modules/fastq": { - "version": "1.19.1", - "resolved": "https://registry.npmjs.org/fastq/-/fastq-1.19.1.tgz", - "integrity": "sha512-GwLTyxkCXjXbxqIhTsMI2Nui8huMPtnxg7krajPJAjnEG/iiOS7i+zCtWGZR9G0NBKbXKh6X9m9UIsYX/N6vvQ==", - "license": "ISC", - "dependencies": { - "reusify": "^1.0.4" - } - }, - "node_modules/fault": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/fault/-/fault-2.0.1.tgz", - "integrity": "sha512-WtySTkS4OKev5JtpHXnib4Gxiurzh5NCGvWrFaZ34m6JehfTUhKZvn9njTfw48t6JumVQOmrKqpmGcdwxnhqBQ==", - "license": "MIT", - "dependencies": { - "format": "^0.2.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/faye-websocket": { - "version": "0.11.4", - "resolved": "https://registry.npmjs.org/faye-websocket/-/faye-websocket-0.11.4.tgz", - "integrity": "sha512-CzbClwlXAuiRQAlUyfqPgvPoNKTckTPGfwZV4ZdAhVcP2lh9KUxJg2b5GkE7XbjKQ3YJnQ9z6D9ntLAlB+tP8g==", - "license": "Apache-2.0", - "dependencies": { - "websocket-driver": ">=0.5.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/feed": { - "version": "4.2.2", - "resolved": "https://registry.npmjs.org/feed/-/feed-4.2.2.tgz", - "integrity": "sha512-u5/sxGfiMfZNtJ3OvQpXcvotFpYkL0n9u9mM2vkui2nGo8b4wvDkJ8gAkYqbA8QpGyFCv3RK0Z+Iv+9veCS9bQ==", - "license": "MIT", - "dependencies": { - "xml-js": "^1.6.11" - }, - "engines": { - "node": ">=0.4.0" - } - }, - "node_modules/figures": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/figures/-/figures-3.2.0.tgz", - "integrity": "sha512-yaduQFRKLXYOGgEn6AZau90j3ggSOyiqXU0F9JZfeXYhNa+Jk4X+s45A2zg5jns87GAFa34BBm2kXw4XpNcbdg==", - "license": "MIT", - "dependencies": { - "escape-string-regexp": "^1.0.5" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/figures/node_modules/escape-string-regexp": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", - "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", - "license": "MIT", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/file-loader": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/file-loader/-/file-loader-6.2.0.tgz", - "integrity": "sha512-qo3glqyTa61Ytg4u73GultjHGjdRyig3tG6lPtyX/jOEJvHif9uB0/OCI2Kif6ctF3caQTW2G5gym21oAsI4pw==", - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/file-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/file-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/file-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/file-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/fill-range": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", - "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", - "license": "MIT", - "dependencies": { - "to-regex-range": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/finalhandler": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", - "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "on-finished": "2.4.1", - "parseurl": "~1.3.3", - "statuses": "2.0.1", - "unpipe": "~1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/finalhandler/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/finalhandler/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/find-cache-dir": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/find-cache-dir/-/find-cache-dir-4.0.0.tgz", - "integrity": "sha512-9ZonPT4ZAK4a+1pUPVPZJapbi7O5qbbJPdYw/NOQWZZbVLdDTYM3A4R9z/DpAM08IDaFGsvPgiGZ82WEwUDWjg==", - "license": "MIT", - "dependencies": { - "common-path-prefix": "^3.0.0", - "pkg-dir": "^7.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/find-up": { - "version": "6.3.0", - "resolved": "https://registry.npmjs.org/find-up/-/find-up-6.3.0.tgz", - "integrity": "sha512-v2ZsoEuVHYy8ZIlYqwPe/39Cy+cFDzp4dXPaxNvkEuouymu+2Jbz0PxpKarJHYJTmv2HWT3O382qY8l4jMWthw==", - "license": "MIT", - "dependencies": { - "locate-path": "^7.1.0", - "path-exists": "^5.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/flat": { - "version": "5.0.2", - "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", - "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", - "license": "BSD-3-Clause", - "bin": { - "flat": "cli.js" - } - }, - "node_modules/follow-redirects": { - "version": "1.15.11", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.11.tgz", - "integrity": "sha512-deG2P0JfjrTxl50XGCDyfI97ZGVCxIpfKYmfyrQ54n5FO/0gfIES8C/Psl6kWVDolizcaaxZJnTS0QSMxvnsBQ==", - "funding": [ - { - "type": "individual", - "url": "https://github.com/sponsors/RubenVerborgh" - } - ], - "license": "MIT", - "engines": { - "node": ">=4.0" - }, - "peerDependenciesMeta": { - "debug": { - "optional": true - } - } - }, - "node_modules/form-data-encoder": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/form-data-encoder/-/form-data-encoder-2.1.4.tgz", - "integrity": "sha512-yDYSgNMraqvnxiEXO4hi88+YZxaHC6QKzb5N84iRCTDeRO7ZALpir/lVmf/uXUhnwUr2O4HU8s/n6x+yNjQkHw==", - "license": "MIT", - "engines": { - "node": ">= 14.17" - } - }, - "node_modules/format": { - "version": "0.2.2", - "resolved": "https://registry.npmjs.org/format/-/format-0.2.2.tgz", - "integrity": "sha512-wzsgA6WOq+09wrU1tsJ09udeR/YZRaeArL9e1wPbFg3GG2yDnC2ldKpxs4xunpFF9DgqCqOIra3bc1HWrJ37Ww==", - "engines": { - "node": ">=0.4.x" - } - }, - "node_modules/forwarded": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", - "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fraction.js": { - "version": "4.3.7", - "resolved": "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz", - "integrity": "sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==", - "license": "MIT", - "engines": { - "node": "*" - }, - "funding": { - "type": "patreon", - "url": "https://github.com/sponsors/rawify" - } - }, - "node_modules/fresh": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", - "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/fs-extra": { - "version": "11.3.2", - "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-11.3.2.tgz", - "integrity": "sha512-Xr9F6z6up6Ws+NjzMCZc6WXg2YFRlrLP9NQDO3VQrWrfiojdhS56TzueT88ze0uBdCTwEIhQ3ptnmKeWGFAe0A==", - "license": "MIT", - "dependencies": { - "graceful-fs": "^4.2.0", - "jsonfile": "^6.0.1", - "universalify": "^2.0.0" - }, - "engines": { - "node": ">=14.14" - } - }, - "node_modules/fs-monkey": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/fs-monkey/-/fs-monkey-1.1.0.tgz", - "integrity": "sha512-QMUezzXWII9EV5aTFXW1UBVUO77wYPpjqIF8/AviUCThNeSYZykpoTixUeaNNBwmCev0AMDWMAni+f8Hxb1IFw==", - "license": "Unlicense" - }, - "node_modules/fs.realpath": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", - "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "license": "ISC" - }, - "node_modules/fsevents": { - "version": "2.3.3", - "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", - "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", - "hasInstallScript": true, - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ], - "engines": { - "node": "^8.16.0 || ^10.6.0 || >=11.0.0" - } - }, - "node_modules/function-bind": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", - "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/gensync": { - "version": "1.0.0-beta.2", - "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", - "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/get-intrinsic": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", - "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "license": "MIT", - "dependencies": { - "call-bind-apply-helpers": "^1.0.2", - "es-define-property": "^1.0.1", - "es-errors": "^1.3.0", - "es-object-atoms": "^1.1.1", - "function-bind": "^1.1.2", - "get-proto": "^1.0.1", - "gopd": "^1.2.0", - "has-symbols": "^1.1.0", - "hasown": "^2.0.2", - "math-intrinsics": "^1.1.0" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/get-own-enumerable-property-symbols": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/get-own-enumerable-property-symbols/-/get-own-enumerable-property-symbols-3.0.2.tgz", - "integrity": "sha512-I0UBV/XOz1XkIJHEUDMZAbzCThU/H8DxmSfmdGcKPnVhu2VfFqr34jr9777IyaTYvxjedWhqVIilEDsCdP5G6g==", - "license": "ISC" - }, - "node_modules/get-proto": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", - "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "license": "MIT", - "dependencies": { - "dunder-proto": "^1.0.1", - "es-object-atoms": "^1.0.0" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/get-stream": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-6.0.1.tgz", - "integrity": "sha512-ts6Wi+2j3jQjqi70w5AlN8DFnkSwC+MqmxEzdEALB2qXZYV3X/b1CTfgPLGJNMeAWxdPfU8FO1ms3NUfaHCPYg==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/github-slugger": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-1.5.0.tgz", - "integrity": "sha512-wIh+gKBI9Nshz2o46B0B3f5k/W+WI9ZAv6y5Dn5WJ5SK1t0TnDimB4WE5rmTD05ZAIn8HALCZVmCsvj0w0v0lw==", - "license": "ISC" - }, - "node_modules/glob": { - "version": "7.2.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", - "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", - "deprecated": "Glob versions prior to v9 are no longer supported", - "license": "ISC", - "dependencies": { - "fs.realpath": "^1.0.0", - "inflight": "^1.0.4", - "inherits": "2", - "minimatch": "^3.1.1", - "once": "^1.3.0", - "path-is-absolute": "^1.0.0" - }, - "engines": { - "node": "*" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/glob-parent": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", - "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", - "license": "ISC", - "dependencies": { - "is-glob": "^4.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/glob-to-regexp": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", - "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", - "license": "BSD-2-Clause" - }, - "node_modules/global-dirs": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/global-dirs/-/global-dirs-3.0.1.tgz", - "integrity": "sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==", - "license": "MIT", - "dependencies": { - "ini": "2.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/globby": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/globby/-/globby-11.1.0.tgz", - "integrity": "sha512-jhIXaOzy1sb8IyocaruWSn1TjmnBVs8Ayhcy83rmxNJ8q2uWKCAj3CnJY+KpGSXCueAPc0i05kVvVKtP1t9S3g==", - "license": "MIT", - "dependencies": { - "array-union": "^2.1.0", - "dir-glob": "^3.0.1", - "fast-glob": "^3.2.9", - "ignore": "^5.2.0", - "merge2": "^1.4.1", - "slash": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/gopd": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", - "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/got": { - "version": "12.6.1", - "resolved": "https://registry.npmjs.org/got/-/got-12.6.1.tgz", - "integrity": "sha512-mThBblvlAF1d4O5oqyvN+ZxLAYwIJK7bpMxgYqPD9okW0C3qm5FFn7k811QrcuEBwaogR3ngOFoCfs6mRv7teQ==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^5.2.0", - "@szmarczak/http-timer": "^5.0.1", - "cacheable-lookup": "^7.0.0", - "cacheable-request": "^10.2.8", - "decompress-response": "^6.0.0", - "form-data-encoder": "^2.1.2", - "get-stream": "^6.0.1", - "http2-wrapper": "^2.1.10", - "lowercase-keys": "^3.0.0", - "p-cancelable": "^3.0.0", - "responselike": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/got?sponsor=1" - } - }, - "node_modules/got/node_modules/@sindresorhus/is": { - "version": "5.6.0", - "resolved": "https://registry.npmjs.org/@sindresorhus/is/-/is-5.6.0.tgz", - "integrity": "sha512-TV7t8GKYaJWsn00tFDqBw8+Uqmr8A0fRU1tvTQhyZzGv0sJCGRQL3JGMI3ucuKo3XIZdUP+Lx7/gh2t3lewy7g==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sindresorhus/is?sponsor=1" - } - }, - "node_modules/graceful-fs": { - "version": "4.2.11", - "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", - "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", - "license": "ISC" - }, - "node_modules/gray-matter": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/gray-matter/-/gray-matter-4.0.3.tgz", - "integrity": "sha512-5v6yZd4JK3eMI3FqqCouswVqwugaA9r4dNZB1wwcmrD02QkV5H0y7XBQW8QwQqEaZY1pM9aqORSORhJRdNK44Q==", - "license": "MIT", - "dependencies": { - "js-yaml": "^3.13.1", - "kind-of": "^6.0.2", - "section-matter": "^1.0.0", - "strip-bom-string": "^1.0.0" - }, - "engines": { - "node": ">=6.0" - } - }, - "node_modules/gray-matter/node_modules/argparse": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/argparse/-/argparse-1.0.10.tgz", - "integrity": "sha512-o5Roy6tNG4SL/FOkCAN6RzjiakZS25RLYFrcMttJqbdd8BWrnA+fGz57iN5Pb06pvBGvl5gQ0B48dJlslXvoTg==", - "license": "MIT", - "dependencies": { - "sprintf-js": "~1.0.2" - } - }, - "node_modules/gray-matter/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", - "license": "MIT", - "dependencies": { - "argparse": "^1.0.7", - "esprima": "^4.0.0" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/gzip-size": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/gzip-size/-/gzip-size-6.0.0.tgz", - "integrity": "sha512-ax7ZYomf6jqPTQ4+XCpUGyXKHk5WweS+e05MBO4/y3WJ5RkmPXNKvX+bx1behVILVwr6JSQvZAku021CHPXG3Q==", - "license": "MIT", - "dependencies": { - "duplexer": "^0.1.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/handle-thing": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/handle-thing/-/handle-thing-2.0.1.tgz", - "integrity": "sha512-9Qn4yBxelxoh2Ow62nP+Ka/kMnOXRi8BXnRaUwezLNhqelnN49xKz4F/dPP8OYLxLxq6JDtZb2i9XznUQbNPTg==", - "license": "MIT" - }, - "node_modules/has-flag": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", - "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/has-property-descriptors": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", - "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", - "license": "MIT", - "dependencies": { - "es-define-property": "^1.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-symbols": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", - "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/has-yarn": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/has-yarn/-/has-yarn-3.0.0.tgz", - "integrity": "sha512-IrsVwUHhEULx3R8f/aA8AHuEzAorplsab/v8HBzEiIukwq5i/EC+xmOW+HfP1OaDP+2JkgT1yILHN2O3UFIbcA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "license": "MIT", - "dependencies": { - "function-bind": "^1.1.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/hast-util-from-parse5": { - "version": "8.0.3", - "resolved": "https://registry.npmjs.org/hast-util-from-parse5/-/hast-util-from-parse5-8.0.3.tgz", - "integrity": "sha512-3kxEVkEKt0zvcZ3hCRYI8rqrgwtlIOFMWkbclACvjlDw8Li9S2hk/d51OI0nr/gIpdMHNepwgOKqZ/sy0Clpyg==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "hastscript": "^9.0.0", - "property-information": "^7.0.0", - "vfile": "^6.0.0", - "vfile-location": "^5.0.0", - "web-namespaces": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-parse-selector": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/hast-util-parse-selector/-/hast-util-parse-selector-4.0.0.tgz", - "integrity": "sha512-wkQCkSYoOGCRKERFWcxMVMOcYE2K1AaNLU8DXS9arxnLOUEWbOXKXiJUNzEpqZ3JOKpnha3jkFrumEjVliDe7A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-raw": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/hast-util-raw/-/hast-util-raw-9.1.0.tgz", - "integrity": "sha512-Y8/SBAHkZGoNkpzqqfCldijcuUKh7/su31kEBp67cFY09Wy0mTRgtsLYsiIxMJxlu0f6AA5SUTbDR8K0rxnbUw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "@ungap/structured-clone": "^1.0.0", - "hast-util-from-parse5": "^8.0.0", - "hast-util-to-parse5": "^8.0.0", - "html-void-elements": "^3.0.0", - "mdast-util-to-hast": "^13.0.0", - "parse5": "^7.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-estree": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/hast-util-to-estree/-/hast-util-to-estree-3.1.3.tgz", - "integrity": "sha512-48+B/rJWAp0jamNbAAf9M7Uf//UVqAoMmgXhBdxTDJLGKY+LRnZ99qcG+Qjl5HfMpYNzS5v4EAwVEF34LeAj7w==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-attach-comments": "^3.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-jsx-runtime": { - "version": "2.3.6", - "resolved": "https://registry.npmjs.org/hast-util-to-jsx-runtime/-/hast-util-to-jsx-runtime-2.3.6.tgz", - "integrity": "sha512-zl6s8LwNyo1P9uw+XJGvZtdFF1GdAkOg8ujOw+4Pyb76874fLps4ueHXDhXWdk6YHQ6OgUtinliG7RsYvCbbBg==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/unist": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "hast-util-whitespace": "^3.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0", - "style-to-js": "^1.0.0", - "unist-util-position": "^5.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/hast-util-to-parse5/-/hast-util-to-parse5-8.0.0.tgz", - "integrity": "sha512-3KKrV5ZVI8if87DVSi1vDeByYrkGzg4mEfeu4alwgmmIeARiBLKCZS2uw5Gb6nU9x9Yufyj3iudm6i7nl52PFw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "devlop": "^1.0.0", - "property-information": "^6.0.0", - "space-separated-tokens": "^2.0.0", - "web-namespaces": "^2.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hast-util-to-parse5/node_modules/property-information": { - "version": "6.5.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-6.5.0.tgz", - "integrity": "sha512-PgTgs/BlvHxOu8QuEN7wi5A0OmXaBcHpmCSTehcs6Uuu9IkDIEo13Hy7n898RHfrQ49vKCoGeWZSaAK01nwVig==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/hast-util-whitespace": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/hast-util-whitespace/-/hast-util-whitespace-3.0.0.tgz", - "integrity": "sha512-88JUN06ipLwsnv+dVn+OIYOvAuvBMy/Qoi6O7mQHxdPXpjy+Cd6xRkWwux7DKO+4sYILtLBRIKgsdpS2gQc7qw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/hastscript": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/hastscript/-/hastscript-9.0.1.tgz", - "integrity": "sha512-g7df9rMFX/SPi34tyGCyUBREQoKkapwdY/T04Qn9TDWfHhAYt4/I0gMVirzK5wEzeUqIjEB+LXC/ypb7Aqno5w==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "comma-separated-tokens": "^2.0.0", - "hast-util-parse-selector": "^4.0.0", - "property-information": "^7.0.0", - "space-separated-tokens": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/he": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", - "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", - "license": "MIT", - "bin": { - "he": "bin/he" - } - }, - "node_modules/history": { - "version": "4.10.1", - "resolved": "https://registry.npmjs.org/history/-/history-4.10.1.tgz", - "integrity": "sha512-36nwAD620w12kuzPAsyINPWJqlNbij+hpK1k9XRloDtym8mxzGYl2c17LnV6IAGB2Dmg4tEa7G7DlawS0+qjew==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.1.2", - "loose-envify": "^1.2.0", - "resolve-pathname": "^3.0.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0", - "value-equal": "^1.0.1" - } - }, - "node_modules/hoist-non-react-statics": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/hoist-non-react-statics/-/hoist-non-react-statics-3.3.2.tgz", - "integrity": "sha512-/gGivxi8JPKWNm/W0jSmzcMPpfpPLc3dY/6GxhX2hQ9iGj3aDfklV4ET7NjKpSinLpJ5vafa9iiGIEZg10SfBw==", - "license": "BSD-3-Clause", - "dependencies": { - "react-is": "^16.7.0" - } - }, - "node_modules/hpack.js": { - "version": "2.1.6", - "resolved": "https://registry.npmjs.org/hpack.js/-/hpack.js-2.1.6.tgz", - "integrity": "sha512-zJxVehUdMGIKsRaNt7apO2Gqp0BdqW5yaiGHXXmbpvxgBYVZnAql+BJb4RO5ad2MgpbZKn5G6nMnegrH1FcNYQ==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.1", - "obuf": "^1.0.0", - "readable-stream": "^2.0.1", - "wbuf": "^1.1.0" - } - }, - "node_modules/hpack.js/node_modules/isarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", - "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==", - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/readable-stream": { - "version": "2.3.8", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", - "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", - "license": "MIT", - "dependencies": { - "core-util-is": "~1.0.0", - "inherits": "~2.0.3", - "isarray": "~1.0.0", - "process-nextick-args": "~2.0.0", - "safe-buffer": "~5.1.1", - "string_decoder": "~1.1.1", - "util-deprecate": "~1.0.1" - } - }, - "node_modules/hpack.js/node_modules/safe-buffer": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", - "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==", - "license": "MIT" - }, - "node_modules/hpack.js/node_modules/string_decoder": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", - "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.1.0" - } - }, - "node_modules/html-entities": { - "version": "2.6.0", - "resolved": "https://registry.npmjs.org/html-entities/-/html-entities-2.6.0.tgz", - "integrity": "sha512-kig+rMn/QOVRvr7c86gQ8lWXq+Hkv6CbAH1hLu+RG338StTpE8Z0b44SDVaqVu7HGKf27frdmUYEs9hTUX/cLQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/mdevils" - }, - { - "type": "patreon", - "url": "https://patreon.com/mdevils" - } - ], - "license": "MIT" - }, - "node_modules/html-escaper": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/html-escaper/-/html-escaper-2.0.2.tgz", - "integrity": "sha512-H2iMtd0I4Mt5eYiapRdIDjp+XzelXQ0tFE4JS7YFwFevXXMmOp9myNrUvCg0D6ws8iqkRPBfKHgbwig1SmlLfg==", - "license": "MIT" - }, - "node_modules/html-minifier-terser": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-7.2.0.tgz", - "integrity": "sha512-tXgn3QfqPIpGl9o+K5tpcj3/MN4SfLtsx2GWwBC3SSd0tXQGyF3gsSqad8loJgKZGM3ZxbYDd5yhiBIdWpmvLA==", - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "~5.3.2", - "commander": "^10.0.0", - "entities": "^4.4.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.15.1" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": "^14.13.1 || >=16.0.0" - } - }, - "node_modules/html-minifier-terser/node_modules/commander": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", - "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/html-tags": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/html-tags/-/html-tags-3.3.1.tgz", - "integrity": "sha512-ztqyC3kLto0e9WbNp0aeP+M3kTt+nbaIveGmUxAtZa+8iFgKLUOD4YKM5j+f3QD89bra7UeumolZHKuOXnTmeQ==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/html-void-elements": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/html-void-elements/-/html-void-elements-3.0.0.tgz", - "integrity": "sha512-bEqo66MRXsUGxWHV5IP0PUiAWwoEjba4VCzg0LjFJBpchPaTfyfCKTG6bc5F8ucKec3q5y6qOdGyYTSBEvhCrg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/html-webpack-plugin": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/html-webpack-plugin/-/html-webpack-plugin-5.6.4.tgz", - "integrity": "sha512-V/PZeWsqhfpE27nKeX9EO2sbR+D17A+tLf6qU+ht66jdUsN0QLKJN27Z+1+gHrVMKgndBahes0PU6rRihDgHTw==", - "license": "MIT", - "dependencies": { - "@types/html-minifier-terser": "^6.0.0", - "html-minifier-terser": "^6.0.2", - "lodash": "^4.17.21", - "pretty-error": "^4.0.0", - "tapable": "^2.0.0" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/html-webpack-plugin" - }, - "peerDependencies": { - "@rspack/core": "0.x || 1.x", - "webpack": "^5.20.0" - }, - "peerDependenciesMeta": { - "@rspack/core": { - "optional": true - }, - "webpack": { - "optional": true - } - } - }, - "node_modules/html-webpack-plugin/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, - "node_modules/html-webpack-plugin/node_modules/html-minifier-terser": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/html-minifier-terser/-/html-minifier-terser-6.1.0.tgz", - "integrity": "sha512-YXxSlJBZTP7RS3tWnQw74ooKa6L9b9i9QYXY21eUEvhZ3u9XLfv6OnFsQq6RxkhHygsaUMvYsZRV5rU/OVNZxw==", - "license": "MIT", - "dependencies": { - "camel-case": "^4.1.2", - "clean-css": "^5.2.2", - "commander": "^8.3.0", - "he": "^1.2.0", - "param-case": "^3.0.4", - "relateurl": "^0.2.7", - "terser": "^5.10.0" - }, - "bin": { - "html-minifier-terser": "cli.js" - }, - "engines": { - "node": ">=12" - } - }, - "node_modules/htmlparser2": { - "version": "8.0.2", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-8.0.2.tgz", - "integrity": "sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.3.0", - "domhandler": "^5.0.3", - "domutils": "^3.0.1", - "entities": "^4.4.0" - } - }, - "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", - "license": "BSD-2-Clause" - }, - "node_modules/http-deceiver": { - "version": "1.2.7", - "resolved": "https://registry.npmjs.org/http-deceiver/-/http-deceiver-1.2.7.tgz", - "integrity": "sha512-LmpOGxTfbpgtGVxJrj5k7asXHCgNZp5nLfp+hWc8QQRqtb7fUy6kRY3BO1h9ddF6yIPYUARgxGOwB42DnxIaNw==", - "license": "MIT" - }, - "node_modules/http-errors": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", - "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "license": "MIT", - "dependencies": { - "depd": "2.0.0", - "inherits": "2.0.4", - "setprototypeof": "1.2.0", - "statuses": "2.0.1", - "toidentifier": "1.0.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/http-parser-js": { - "version": "0.5.10", - "resolved": "https://registry.npmjs.org/http-parser-js/-/http-parser-js-0.5.10.tgz", - "integrity": "sha512-Pysuw9XpUq5dVc/2SMHpuTY01RFl8fttgcyunjL7eEMhGM3cI4eOmiCycJDVCo/7O7ClfQD3SaI6ftDzqOXYMA==", - "license": "MIT" - }, - "node_modules/http-proxy": { - "version": "1.18.1", - "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", - "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.0", - "follow-redirects": "^1.0.0", - "requires-port": "^1.0.0" - }, - "engines": { - "node": ">=8.0.0" - } - }, - "node_modules/http-proxy-middleware": { - "version": "2.0.9", - "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz", - "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==", - "license": "MIT", - "dependencies": { - "@types/http-proxy": "^1.17.8", - "http-proxy": "^1.18.1", - "is-glob": "^4.0.1", - "is-plain-obj": "^3.0.0", - "micromatch": "^4.0.2" - }, - "engines": { - "node": ">=12.0.0" - }, - "peerDependencies": { - "@types/express": "^4.17.13" - }, - "peerDependenciesMeta": { - "@types/express": { - "optional": true - } - } - }, - "node_modules/http-proxy-middleware/node_modules/is-plain-obj": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-3.0.0.tgz", - "integrity": "sha512-gwsOE28k+23GP1B6vFl1oVh/WOzmawBrKwo5Ev6wMKzPkaXaCDIQKzLnvsA42DRlbVTWorkgTKIviAKCWkfUwA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/http2-wrapper": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/http2-wrapper/-/http2-wrapper-2.2.1.tgz", - "integrity": "sha512-V5nVw1PAOgfI3Lmeaj2Exmeg7fenjhRUgz1lPSezy1CuhPYbgQtbQj4jZfEAEMlaL+vupsvhjqCyjzob0yxsmQ==", - "license": "MIT", - "dependencies": { - "quick-lru": "^5.1.1", - "resolve-alpn": "^1.2.0" - }, - "engines": { - "node": ">=10.19.0" - } - }, - "node_modules/human-signals": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/human-signals/-/human-signals-2.1.0.tgz", - "integrity": "sha512-B4FFZ6q/T2jhhksgkbEW3HBvWIfDW85snkQgawt07S7J5QXTk6BkNV+0yAeZrM5QpMAdYlocGoljn0sJ/WQkFw==", - "license": "Apache-2.0", - "engines": { - "node": ">=10.17.0" - } - }, - "node_modules/iconv-lite": { - "version": "0.4.24", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", - "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/icss-utils": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", - "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/ignore": { - "version": "5.3.2", - "resolved": "https://registry.npmjs.org/ignore/-/ignore-5.3.2.tgz", - "integrity": "sha512-hsBTNUqQTDwkWtcdYI2i06Y/nUBEsNEDJKjWdigLvegy8kDuJAS8uRlpkkcQpyEXL0Z/pjDy5HBmMjRCJ2gq+g==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/image-size": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/image-size/-/image-size-2.0.2.tgz", - "integrity": "sha512-IRqXKlaXwgSMAMtpNzZa1ZAe8m+Sa1770Dhk8VkSsP9LS+iHD62Zd8FQKs8fbPiagBE7BzoFX23cxFnwshpV6w==", - "license": "MIT", - "bin": { - "image-size": "bin/image-size.js" - }, - "engines": { - "node": ">=16.x" - } - }, - "node_modules/import-fresh": { - "version": "3.3.1", - "resolved": "https://registry.npmjs.org/import-fresh/-/import-fresh-3.3.1.tgz", - "integrity": "sha512-TR3KfrTZTYLPB6jUjfx6MF9WcWrHL9su5TObK4ZkYgBdWKPOFoSoQIdEuTuR82pmtxH2spWG9h6etwfr1pLBqQ==", - "license": "MIT", - "dependencies": { - "parent-module": "^1.0.0", - "resolve-from": "^4.0.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/import-lazy": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/import-lazy/-/import-lazy-4.0.0.tgz", - "integrity": "sha512-rKtvo6a868b5Hu3heneU+L4yEQ4jYKLtjpnPeUdK7h0yzXGmyBTypknlkCvHFBqfX9YlorEiMM6Dnq/5atfHkw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/imurmurhash": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/imurmurhash/-/imurmurhash-0.1.4.tgz", - "integrity": "sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==", - "license": "MIT", - "engines": { - "node": ">=0.8.19" - } - }, - "node_modules/indent-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/indent-string/-/indent-string-4.0.0.tgz", - "integrity": "sha512-EdDDZu4A2OyIK7Lr/2zG+w5jmbuk1DVBnEwREQvBzspBJkCEbRa8GxU1lghYcaGJCnRWibjDXlq779X1/y5xwg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/infima": { - "version": "0.2.0-alpha.45", - "resolved": "https://registry.npmjs.org/infima/-/infima-0.2.0-alpha.45.tgz", - "integrity": "sha512-uyH0zfr1erU1OohLk0fT4Rrb94AOhguWNOcD9uGrSpRvNB+6gZXUoJX5J0NtvzBO10YZ9PgvA4NFgt+fYg8ojw==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/inflight": { - "version": "1.0.6", - "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", - "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", - "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "license": "ISC", - "dependencies": { - "once": "^1.3.0", - "wrappy": "1" - } - }, - "node_modules/inherits": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", - "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "license": "ISC" - }, - "node_modules/ini": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ini/-/ini-2.0.0.tgz", - "integrity": "sha512-7PnF4oN3CvZF23ADhA5wRaYEQpJ8qygSkbtTXWBeXWXmEVRXK+1ITciHWwHhsjv1TmW0MgacIv6hEi5pX5NQdA==", - "license": "ISC", - "engines": { - "node": ">=10" - } - }, - "node_modules/inline-style-parser": { - "version": "0.2.4", - "resolved": "https://registry.npmjs.org/inline-style-parser/-/inline-style-parser-0.2.4.tgz", - "integrity": "sha512-0aO8FkhNZlj/ZIbNi7Lxxr12obT7cL1moPfE4tg1LkX7LlLfC6DeX4l2ZEud1ukP9jNQyNnfzQVqwbwmAATY4Q==", - "license": "MIT" - }, - "node_modules/invariant": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/invariant/-/invariant-2.2.4.tgz", - "integrity": "sha512-phJfQVBuaJM5raOpJjSfkiD6BpbCE4Ns//LaXl6wGYtUBY83nWS6Rf9tXm2e8VaK60JEjYldbPif/A2B1C2gNA==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.0.0" - } - }, - "node_modules/ipaddr.js": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-2.2.0.tgz", - "integrity": "sha512-Ag3wB2o37wslZS19hZqorUnrnzSkpOVy+IiiDEiTqNubEYpYuHWIf6K4psgN2ZWKExS4xhVCrRVfb/wfW8fWJA==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/is-alphabetical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", - "integrity": "sha512-FWyyY60MeTNyeSRpkM2Iry0G9hpr7/9kD40mD/cGQEuilcZYS4okz8SN2Q6rLCJ8gbCt6fN+rC+6tMGS99LaxQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-alphanumerical": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-alphanumerical/-/is-alphanumerical-2.0.1.tgz", - "integrity": "sha512-hmbYhX/9MUMF5uh7tOXyK/n0ZvWpad5caBA17GsC6vyuCqaWliRG5K1qS9inmUhEMaOBIW7/whAnSwveW/LtZw==", - "license": "MIT", - "dependencies": { - "is-alphabetical": "^2.0.0", - "is-decimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-arrayish": { - "version": "0.2.1", - "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", - "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", - "license": "MIT" - }, - "node_modules/is-binary-path": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", - "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", - "license": "MIT", - "dependencies": { - "binary-extensions": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-ci": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/is-ci/-/is-ci-3.0.1.tgz", - "integrity": "sha512-ZYvCgrefwqoQ6yTyYUbQu64HsITZ3NfKX1lzaEYdkTDcfKzzCI/wthRRYKkdjHKFVgNiXKAKm65Zo1pk2as/QQ==", - "license": "MIT", - "dependencies": { - "ci-info": "^3.2.0" - }, - "bin": { - "is-ci": "bin.js" - } - }, - "node_modules/is-core-module": { - "version": "2.16.1", - "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", - "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", - "license": "MIT", - "dependencies": { - "hasown": "^2.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/is-decimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-decimal/-/is-decimal-2.0.1.tgz", - "integrity": "sha512-AAB9hiomQs5DXWcRB1rqsxGUstbRroFOPPVAomNk/3XHR5JyEZChOyTWe2oayKnsSsr/kcGqF+z6yuH6HHpN0A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-docker": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-2.2.1.tgz", - "integrity": "sha512-F+i2BKsFrH66iaUFc0woD8sLy8getkwTwtOBjvs56Cx4CgJDeKQeqfz8wAYiSb8JOprWhHH5p77PbmYCvvUuXQ==", - "license": "MIT", - "bin": { - "is-docker": "cli.js" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-extendable": { - "version": "0.1.1", - "resolved": "https://registry.npmjs.org/is-extendable/-/is-extendable-0.1.1.tgz", - "integrity": "sha512-5BMULNob1vgFX6EjQw5izWDxrecWK9AM72rugNr0TFldMOi0fj6Jk+zeKIt0xGj4cEfQIJth4w3OKWOJ4f+AFw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-extglob": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", - "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-fullwidth-code-point": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", - "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-glob": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", - "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", - "license": "MIT", - "dependencies": { - "is-extglob": "^2.1.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-hexadecimal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-hexadecimal/-/is-hexadecimal-2.0.1.tgz", - "integrity": "sha512-DgZQp241c8oO6cA1SbTEWiXeoxV42vlcJxgH+B3hi1AiqqKruZR3ZGF8In3fj4+/y/7rHvlOZLZtgJ/4ttYGZg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/is-installed-globally": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/is-installed-globally/-/is-installed-globally-0.4.0.tgz", - "integrity": "sha512-iwGqO3J21aaSkC7jWnHP/difazwS7SFeIqxv6wEtLU8Y5KlzFTjyqcSIT0d8s4+dDhKytsk9PJZ2BkS5eZwQRQ==", - "license": "MIT", - "dependencies": { - "global-dirs": "^3.0.0", - "is-path-inside": "^3.0.2" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-npm": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/is-npm/-/is-npm-6.1.0.tgz", - "integrity": "sha512-O2z4/kNgyjhQwVR1Wpkbfc19JIhggF97NZNCpWTnjH7kVcZMUrnut9XSN7txI7VdyIYk5ZatOq3zvSuWpU8hoA==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-number": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", - "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", - "license": "MIT", - "engines": { - "node": ">=0.12.0" - } - }, - "node_modules/is-obj": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/is-obj/-/is-obj-1.0.1.tgz", - "integrity": "sha512-l4RyHgRqGN4Y3+9JHVrNqO+tN0rV5My76uW5/nuO4K1b6vw5G8d/cmFjP9tRfEsdhZNt0IFdZuK/c2Vr4Nb+Qg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-path-inside": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/is-path-inside/-/is-path-inside-3.0.3.tgz", - "integrity": "sha512-Fd4gABb+ycGAmKou8eMftCupSir5lRxqf4aD/vd0cD2qc4HL07OjCeuHMr8Ro4CoMaeCKDB0/ECBOVWjTwUvPQ==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/is-plain-obj": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", - "integrity": "sha512-+Pgi+vMuUNkJyExiMBt5IlFoMyKnr5zhJ4Uspz58WOhBF5QoIZkFyNHIbBAtHwzVAgk5RtndVNsDRN61/mmDqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-plain-object": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", - "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", - "license": "MIT", - "dependencies": { - "isobject": "^3.0.1" - }, - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-regexp": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-regexp/-/is-regexp-1.0.0.tgz", - "integrity": "sha512-7zjFAPO4/gwyQAAgRRmqeEeyIICSdmCqa3tsVHMdBzaXXRiqopZL4Cyghg/XulGWrtABTpbnYYzzIRffLkP4oA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/is-stream": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz", - "integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/is-typedarray": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz", - "integrity": "sha512-cyA56iCMHAh5CdzjJIa4aohJyeO1YbwLi3Jc35MmRU6poroFjIGZzUzupGiRPOjgHg9TLu43xbpwXk523fMxKA==", - "license": "MIT" - }, - "node_modules/is-wsl": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-2.2.0.tgz", - "integrity": "sha512-fKzAra0rGJUUBwGBgNkHZuToZcn+TtXHpeCgmkMJMMYx1sQDYaCSyjJBSCa2nH1DGm7s3n1oBnohoVTBaN7Lww==", - "license": "MIT", - "dependencies": { - "is-docker": "^2.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/is-yarn-global": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/is-yarn-global/-/is-yarn-global-0.4.1.tgz", - "integrity": "sha512-/kppl+R+LO5VmhYSEWARUFjodS25D68gvj8W7z0I7OWhUla5xWu8KL6CtB2V0R6yqhnRgbcaREMr4EEM6htLPQ==", - "license": "MIT", - "engines": { - "node": ">=12" - } - }, - "node_modules/isarray": { - "version": "0.0.1", - "resolved": "https://registry.npmjs.org/isarray/-/isarray-0.0.1.tgz", - "integrity": "sha512-D2S+3GLxWH+uhrNEcoh/fnmYeP8E8/zHl644d/jdA0g2uyXvy3sb0qxotE+ne0LtccHknQzWwZEzhak7oJ0COQ==", - "license": "MIT" - }, - "node_modules/isexe": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", - "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", - "license": "ISC" - }, - "node_modules/isobject": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", - "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/jest-util": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-util/-/jest-util-29.7.0.tgz", - "integrity": "sha512-z6EbKajIpqGKU56y5KBUgy1dt1ihhQJgWzUlZHArA/+X2ad7Cb5iF+AK1EWVL/Bo7Rz9uurpqw6SiBCefUbCGA==", - "license": "MIT", - "dependencies": { - "@jest/types": "^29.6.3", - "@types/node": "*", - "chalk": "^4.0.0", - "ci-info": "^3.2.0", - "graceful-fs": "^4.2.9", - "picomatch": "^2.2.3" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker": { - "version": "29.7.0", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-29.7.0.tgz", - "integrity": "sha512-eIz2msL/EzL9UFTFFx7jBTkeZfku0yUAyZZZmJ93H2TYEiroIx2PQjEXcwYtYl8zXCxb+PAmA2hLIt/6ZEkPHw==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "jest-util": "^29.7.0", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": "^14.15.0 || ^16.10.0 || >=18.0.0" - } - }, - "node_modules/jest-worker/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/jiti": { - "version": "1.21.7", - "resolved": "https://registry.npmjs.org/jiti/-/jiti-1.21.7.tgz", - "integrity": "sha512-/imKNG4EbWNrVjoNC/1H5/9GFy+tqjGBHCaSsN+P2RnPqjsLmv6UD3Ej+Kj8nBWaRAwyk7kK5ZUc+OEatnTR3A==", - "license": "MIT", - "bin": { - "jiti": "bin/jiti.js" - } - }, - "node_modules/joi": { - "version": "17.13.3", - "resolved": "https://registry.npmjs.org/joi/-/joi-17.13.3.tgz", - "integrity": "sha512-otDA4ldcIx+ZXsKHWmp0YizCweVRZG96J10b0FevjfuncLO1oX59THoAmHkNubYJ+9gWsYsp5k8v4ib6oDv1fA==", - "license": "BSD-3-Clause", - "dependencies": { - "@hapi/hoek": "^9.3.0", - "@hapi/topo": "^5.1.0", - "@sideway/address": "^4.1.5", - "@sideway/formula": "^3.0.1", - "@sideway/pinpoint": "^2.0.0" - } - }, - "node_modules/js-tokens": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", - "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", - "license": "MIT" - }, - "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", - "license": "MIT", - "dependencies": { - "argparse": "^2.0.1" - }, - "bin": { - "js-yaml": "bin/js-yaml.js" - } - }, - "node_modules/jsesc": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", - "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "license": "MIT" - }, - "node_modules/json-parse-even-better-errors": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", - "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", - "license": "MIT" - }, - "node_modules/json-schema-traverse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", - "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", - "license": "MIT" - }, - "node_modules/json5": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", - "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", - "license": "MIT", - "bin": { - "json5": "lib/cli.js" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/jsonfile": { - "version": "6.2.0", - "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-6.2.0.tgz", - "integrity": "sha512-FGuPw30AdOIUTRMC2OMRtQV+jkVj2cfPqSeWXv1NEAJ1qZ5zb1X6z1mFhbfOB/iy3ssJCD+3KuZ8r8C3uVFlAg==", - "license": "MIT", - "dependencies": { - "universalify": "^2.0.0" - }, - "optionalDependencies": { - "graceful-fs": "^4.1.6" - } - }, - "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", - "license": "MIT", - "dependencies": { - "json-buffer": "3.0.1" - } - }, - "node_modules/kind-of": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", - "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/kleur": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/kleur/-/kleur-3.0.3.tgz", - "integrity": "sha512-eTIzlVOSUR+JxdDFepEYcBMtZ9Qqdef+rnzWdRZuMbOywu5tO2w2N7rqjoANZ5k9vywhL6Br1VRjUIgTQx4E8w==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/latest-version": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/latest-version/-/latest-version-7.0.0.tgz", - "integrity": "sha512-KvNT4XqAMzdcL6ka6Tl3i2lYeFDgXNCuIX+xNx6ZMVR1dFq+idXd9FLKNMOIx0t9mJ9/HudyX4oZWXZQ0UJHeg==", - "license": "MIT", - "dependencies": { - "package-json": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/launch-editor": { - "version": "2.11.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.11.1.tgz", - "integrity": "sha512-SEET7oNfgSaB6Ym0jufAdCeo3meJVeCaaDyzRygy0xsp2BFKCprcfHljTq4QkzTLUxEKkFK6OK4811YM2oSrRg==", - "license": "MIT", - "dependencies": { - "picocolors": "^1.1.1", - "shell-quote": "^1.8.3" - } - }, - "node_modules/leven": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/leven/-/leven-3.1.0.tgz", - "integrity": "sha512-qsda+H8jTaUaN/x5vzW2rzc+8Rw4TAQ/4KjB46IwK5VH+IlVeeeje/EoZRpiXvIqjFgK84QffqPztGI3VBLG1A==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/lilconfig": { - "version": "3.1.3", - "resolved": "https://registry.npmjs.org/lilconfig/-/lilconfig-3.1.3.tgz", - "integrity": "sha512-/vlFKAoH5Cgt3Ie+JLhRbwOsCQePABiU3tJ1egGvyQ+33R/vcwM2Zl2QR/LzjsBeItPt3oSVXapn+m4nQDvpzw==", - "license": "MIT", - "engines": { - "node": ">=14" - }, - "funding": { - "url": "https://github.com/sponsors/antonk52" - } - }, - "node_modules/lines-and-columns": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/lines-and-columns/-/lines-and-columns-1.2.4.tgz", - "integrity": "sha512-7ylylesZQ/PV29jhEDl3Ufjo6ZX7gCqJr5F7PKrqc93v7fzSymt1BpwEU8nAUXs8qzzvqhbjhK5QZg6Mt/HkBg==", - "license": "MIT" - }, - "node_modules/loader-runner": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.0.tgz", - "integrity": "sha512-3R/1M+yS3j5ou80Me59j7F9IMs4PXs3VqRrm0TU3AbKPxlmpoY1TNscJV/oGJXo8qCatFGTfDbY6W6ipGOYXfg==", - "license": "MIT", - "engines": { - "node": ">=6.11.5" - } - }, - "node_modules/loader-utils": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/loader-utils/-/loader-utils-2.0.4.tgz", - "integrity": "sha512-xXqpXoINfFhgua9xiqD8fPFHgkoq1mmmpE92WlDbm9rNRd/EbRb+Gqf908T2DMfuHjjJlksiK2RbHVOdD/MqSw==", - "license": "MIT", - "dependencies": { - "big.js": "^5.2.2", - "emojis-list": "^3.0.0", - "json5": "^2.1.2" - }, - "engines": { - "node": ">=8.9.0" - } - }, - "node_modules/locate-path": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-7.2.0.tgz", - "integrity": "sha512-gvVijfZvn7R+2qyPX8mAuKcFGDf6Nc61GdvGafQsHL0sBIxfKzA+usWn4GFC/bk+QdwPUD4kWFJLhElipq+0VA==", - "license": "MIT", - "dependencies": { - "p-locate": "^6.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lodash": { - "version": "4.17.21", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", - "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==", - "license": "MIT" - }, - "node_modules/lodash.debounce": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", - "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", - "license": "MIT" - }, - "node_modules/lodash.memoize": { - "version": "4.1.2", - "resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz", - "integrity": "sha512-t7j+NzmgnQzTAYXcsHYLgimltOV1MXHtlOWf6GjL9Kj8GK5FInw5JotxvbOs+IvV1/Dzo04/fCGfLVs7aXb4Ag==", - "license": "MIT" - }, - "node_modules/lodash.uniq": { - "version": "4.5.0", - "resolved": "https://registry.npmjs.org/lodash.uniq/-/lodash.uniq-4.5.0.tgz", - "integrity": "sha512-xfBaXQd9ryd9dlSDvnvI0lvxfLJlYAZzXomUYzLKtUeOQvOP5piqAWuGtrhWeqaXK9hhoM/iyJc5AV+XfsX3HQ==", - "license": "MIT" - }, - "node_modules/longest-streak": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", - "integrity": "sha512-9Ri+o0JYgehTaVBBDoMqIl8GXtbWg711O3srftcHhZ0dqnETqLaoIK0x17fUw9rFSlK/0NlsKe0Ahhyl5pXE2g==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/loose-envify": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", - "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", - "license": "MIT", - "dependencies": { - "js-tokens": "^3.0.0 || ^4.0.0" - }, - "bin": { - "loose-envify": "cli.js" - } - }, - "node_modules/lower-case": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", - "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", - "license": "MIT", - "dependencies": { - "tslib": "^2.0.3" - } - }, - "node_modules/lowercase-keys": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/lowercase-keys/-/lowercase-keys-3.0.0.tgz", - "integrity": "sha512-ozCC6gdQ+glXOQsveKD0YsDy8DSQFjDTz4zyzEHNV5+JP5D62LmfDZ6o1cycFx9ouG940M5dE8C8CTewdj2YWQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/lru-cache": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", - "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", - "license": "ISC", - "dependencies": { - "yallist": "^3.0.2" - } - }, - "node_modules/markdown-extensions": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-extensions/-/markdown-extensions-2.0.0.tgz", - "integrity": "sha512-o5vL7aDWatOTX8LzaS1WMoaoxIiLRQJuIKKe2wAw6IeULDHaqbiqiggmx+pKvZDb1Sj+pE46Sn1T7lCqfFtg1Q==", - "license": "MIT", - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/markdown-table": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-3.0.4.tgz", - "integrity": "sha512-wiYz4+JrLyb/DqW2hkFJxP7Vd7JuTDm77fvbM8VfEQdmSMqcImWeeRbHwZjBjIFki/VaMK2BhFi7oUUZeM5bqw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/math-intrinsics": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", - "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/mdast-util-directive": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", - "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "escape-string-regexp": "^5.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-find-and-replace/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-from-markdown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.2.tgz", - "integrity": "sha512-uZhTV/8NBuw0WHkPTrCqDOl0zVe1BIng5ZtHoDk49ME1qqcjYmmLmOf0gELgcRMxN4w2iuIeVso5/6QymSrgmA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark": "^4.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-from-markdown/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-frontmatter": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-frontmatter/-/mdast-util-frontmatter-2.0.1.tgz", - "integrity": "sha512-LRqI9+wdgC25P0URIJY9vwocIzCcksduHQ9OF2joxQoyTNVduwLAFUzjoopuRJbJAReaKrNQKAZKL3uCMugWJA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "escape-string-regexp": "^5.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-extension-frontmatter": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-frontmatter/node_modules/escape-string-regexp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", - "integrity": "sha512-/veY75JbMK4j1yjvuUxuVsiS/hr/4iHs9FTT6cgTexxdE0Ly/glccBAkloH/DofkjRbZU3bnoj38mOmhkZ0lHw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mdast-util-gfm": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm/-/mdast-util-gfm-3.1.0.tgz", - "integrity": "sha512-0ulfdQOM3ysHhCJ1p06l0b0VKlhU0wuQs3thxZQagjcjPrlFRqY215uZGHHJan9GEAXd9MbfPjFJz+qMkVR6zQ==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-gfm-autolink-literal": "^2.0.0", - "mdast-util-gfm-footnote": "^2.0.0", - "mdast-util-gfm-strikethrough": "^2.0.0", - "mdast-util-gfm-table": "^2.0.0", - "mdast-util-gfm-task-list-item": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-autolink-literal/-/mdast-util-gfm-autolink-literal-2.0.1.tgz", - "integrity": "sha512-5HVP2MKaP6L+G6YaxPNjuL0BPrq9orG3TsrZ9YXbA3vDw/ACI4MEsnoDpn6ZNm7GnZgtAcONJyPhOP8tNJQavQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "ccount": "^2.0.0", - "devlop": "^1.0.0", - "mdast-util-find-and-replace": "^3.0.0", - "micromark-util-character": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/mdast-util-gfm-autolink-literal/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/mdast-util-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-footnote/-/mdast-util-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-sqpDWlsHn7Ac9GNZQMeUzPQSMzR6Wv0WKRNvQRg0KqHh02fpTz69Qc1QSseNX29bhz1ROIyNyxExfawVKTm1GQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-table/-/mdast-util-gfm-table-2.0.0.tgz", - "integrity": "sha512-78UEvebzz/rJIxLvE7ZtDd/vIQ0RHv+3Mh5DR96p7cS7HsBhYIICDBCu8csTNWNO6tBWfqXPWekRuj2FNOGOZg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "markdown-table": "^3.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-gfm-task-list-item": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-task-list-item/-/mdast-util-gfm-task-list-item-2.0.0.tgz", - "integrity": "sha512-IrtvNvjxC1o06taBAVJznEnkiHxLFTzgonUdy8hzFVeDun0uTjxxrRGVaNFqkU1wJR3RBPEfsxmU6jDWPofrTQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx/-/mdast-util-mdx-3.0.0.tgz", - "integrity": "sha512-JfbYLAW7XnYTTbUsmpu0kdBUVe+yKVJZBItEjwyYJiDJuZ9w4eeaqks4HQO+R7objWgS2ymV60GYpI14Ug554w==", - "license": "MIT", - "dependencies": { - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-mdx-expression": "^2.0.0", - "mdast-util-mdx-jsx": "^3.0.0", - "mdast-util-mdxjs-esm": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-expression": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-expression/-/mdast-util-mdx-expression-2.0.1.tgz", - "integrity": "sha512-J6f+9hUp+ldTZqKRSg7Vw5V6MqjATc+3E4gf3CFNcuZNWD8XdyI6zQ8GqH7f8169MM6P7hMBRDVGnn7oHB9kXQ==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdx-jsx": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-mdx-jsx/-/mdast-util-mdx-jsx-3.2.0.tgz", - "integrity": "sha512-lj/z8v0r6ZtsN/cGNNtemmmfoLAFZnjMbNyLzBafjzikOM+glrjNHPlf6lQDOTccj9n5b0PPihEBbhneMyGs1Q==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "ccount": "^2.0.0", - "devlop": "^1.1.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0", - "parse-entities": "^4.0.0", - "stringify-entities": "^4.0.0", - "unist-util-stringify-position": "^4.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-mdxjs-esm": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mdast-util-mdxjs-esm/-/mdast-util-mdxjs-esm-2.0.1.tgz", - "integrity": "sha512-EcmOpxsZ96CvlP03NghtH1EsLtr0n9Tm4lPUJUBccV9RwUOneqSycg19n5HGzCf+10LozMRSObtVr3ee1WoHtg==", - "license": "MIT", - "dependencies": { - "@types/estree-jsx": "^1.0.0", - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "devlop": "^1.0.0", - "mdast-util-from-markdown": "^2.0.0", - "mdast-util-to-markdown": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-phrasing": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-phrasing/-/mdast-util-phrasing-4.1.0.tgz", - "integrity": "sha512-TqICwyvJJpBwvGAMZjj4J2n0X8QWp21b9l0o7eXyVJ25YNWYbJDVIyD1bZXE6WtV6RmKJVYmQAKWa0zWOABz2w==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-hast": { - "version": "13.2.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-hast/-/mdast-util-to-hast-13.2.0.tgz", - "integrity": "sha512-QGYKEuUsYT9ykKBCMOEDLsU5JRObWQusAolFMeko/tYPufNkRffBAQjIE+99jbA87xv6FgmjLtwjh9wBWajwAA==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "@ungap/structured-clone": "^1.0.0", - "devlop": "^1.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "trim-lines": "^3.0.0", - "unist-util-position": "^5.0.0", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "@types/unist": "^3.0.0", - "longest-streak": "^3.0.0", - "mdast-util-phrasing": "^4.0.0", - "mdast-util-to-string": "^4.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-decode-string": "^2.0.0", - "unist-util-visit": "^5.0.0", - "zwitch": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdast-util-to-string": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-to-string/-/mdast-util-to-string-4.0.0.tgz", - "integrity": "sha512-0H44vDimn51F0YwvxSJSm0eCDOJTRlmN0R1yBh4HLj9wiV1Dn0QoXGbvFAWj2hSItVTlCmBF1hqKlIyUBVFLPg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/mdn-data": { - "version": "2.0.30", - "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", - "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", - "license": "CC0-1.0" - }, - "node_modules/media-typer": { - "version": "0.3.0", - "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", - "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/memfs": { - "version": "3.5.3", - "resolved": "https://registry.npmjs.org/memfs/-/memfs-3.5.3.tgz", - "integrity": "sha512-UERzLsxzllchadvbPs5aolHh65ISpKpM+ccLbOJ8/vvpBKmAWf+la7dXFy7Mr0ySHbdHrFv5kGFCUHHe6GFEmw==", - "license": "Unlicense", - "dependencies": { - "fs-monkey": "^1.0.4" - }, - "engines": { - "node": ">= 4.0.0" - } - }, - "node_modules/merge-descriptors": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", - "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/merge-stream": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", - "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", - "license": "MIT" - }, - "node_modules/merge2": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", - "integrity": "sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==", - "license": "MIT", - "engines": { - "node": ">= 8" - } - }, - "node_modules/methods": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", - "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/debug": "^4.0.0", - "debug": "^4.0.0", - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-destination": "^2.0.0", - "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-title": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-html-tag-name": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-subtokenize": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-core-commonmark/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-directive": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", - "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-factory-whitespace": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "parse-entities": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-directive/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-frontmatter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-frontmatter/-/micromark-extension-frontmatter-2.0.0.tgz", - "integrity": "sha512-C4AkuM3dA58cgZha7zVnuVxBhDsbttIMiytjgsM2XbHAB2faRVaHRle40558FBN+DJcrLNCoqG5mlrpdU4cRtg==", - "license": "MIT", - "dependencies": { - "fault": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-frontmatter/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm/-/micromark-extension-gfm-3.0.0.tgz", - "integrity": "sha512-vsKArQsicm7t0z2GugkCKtZehqUm31oeGBV/KVSorWSy8ZlNAv7ytjFhvaryUiCUJYqs+NoE6AFhpQvBTM6Q4w==", - "license": "MIT", - "dependencies": { - "micromark-extension-gfm-autolink-literal": "^2.0.0", - "micromark-extension-gfm-footnote": "^2.0.0", - "micromark-extension-gfm-strikethrough": "^2.0.0", - "micromark-extension-gfm-table": "^2.0.0", - "micromark-extension-gfm-tagfilter": "^2.0.0", - "micromark-extension-gfm-task-list-item": "^2.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-autolink-literal/-/micromark-extension-gfm-autolink-literal-2.1.0.tgz", - "integrity": "sha512-oOg7knzhicgQ3t4QCjCWgTmfNhvQbDDnJeVu9v81r7NltNCVmhPy1fJRX27pISafdjL+SVc4d3l48Gb6pbRypw==", - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-autolink-literal/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-footnote": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-footnote/-/micromark-extension-gfm-footnote-2.1.0.tgz", - "integrity": "sha512-/yPhxI1ntnDNsiHtzLKYnE3vf9JZ6cAisqVDauhp4CEHxlb4uoOTxOCJ+9s51bIB8U1N1FJ1RXOKTIlD5B/gqw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-normalize-identifier": "^2.0.0", - "micromark-util-sanitize-uri": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-footnote/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-strikethrough": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-strikethrough/-/micromark-extension-gfm-strikethrough-2.1.0.tgz", - "integrity": "sha512-ADVjpOOkjz1hhkZLlBiYA9cR2Anf8F4HqZUO6e5eDcPQd0Txw5fxLzzxnEkSkfnD0wziSGiv7sYhk/ktvbf1uw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-classify-character": "^2.0.0", - "micromark-util-resolve-all": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-strikethrough/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-table/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-table/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-gfm-tagfilter": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-tagfilter/-/micromark-extension-gfm-tagfilter-2.0.0.tgz", - "integrity": "sha512-xHlTOmuCSotIA8TW1mDIM6X2O1SiX5P9IuDtqGonFhEK0qgRI4yeC6vMxEV2dgyr2TiD+2PQ10o+cOhdVAcwfg==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-task-list-item/-/micromark-extension-gfm-task-list-item-2.1.0.tgz", - "integrity": "sha512-qIBZhqxqI6fjLDYFTBIa4eivDMnP+OZqsNwmQ3xNLE4Cxwc+zfQEfbs6tzAo2Hjq+bh6q5F+Z8/cksrLFYWQQw==", - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-gfm-task-list-item/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-mdx-expression": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", - "integrity": "sha512-dD/ADLJ1AeMvSAKBwO22zG22N4ybhe7kFIZ3LsDI0GlsNr2A3KYxb0LdC1u5rj4Nw+CHKY0RVdnHX8vj8ejm4Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-expression/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-expression/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-mdx-jsx": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-jsx/-/micromark-extension-mdx-jsx-3.0.2.tgz", - "integrity": "sha512-e5+q1DjMh62LZAJOnDraSSbDMvGJ8x3cbjygy2qFEi7HCeUT4BDKCvMozPozcD6WmOt6sVvYDNBKhFSz3kjOVQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "estree-util-is-identifier-name": "^3.0.0", - "micromark-factory-mdx-expression": "^2.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdx-jsx/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-extension-mdx-md": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdx-md/-/micromark-extension-mdx-md-2.0.0.tgz", - "integrity": "sha512-EpAiszsB3blw4Rpba7xTOUptcFeBFi+6PY8VnJ2hhimH+vCQDirWgsMpz7w1XcZE7LVrSAUGb9VJpG9ghlYvYQ==", - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs/-/micromark-extension-mdxjs-3.0.0.tgz", - "integrity": "sha512-A873fJfhnJ2siZyUrJ31l34Uqwy4xIFmvPY1oj+Ean5PHcPBYzEsvqvWGaWcfEIr11O5Dlw3p2y0tZWpKHDejQ==", - "license": "MIT", - "dependencies": { - "acorn": "^8.0.0", - "acorn-jsx": "^5.0.0", - "micromark-extension-mdx-expression": "^3.0.0", - "micromark-extension-mdx-jsx": "^3.0.0", - "micromark-extension-mdx-md": "^2.0.0", - "micromark-extension-mdxjs-esm": "^3.0.0", - "micromark-util-combine-extensions": "^2.0.0", - "micromark-util-types": "^2.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/micromark-extension-mdxjs-esm/-/micromark-extension-mdxjs-esm-3.0.0.tgz", - "integrity": "sha512-DJFl4ZqkErRpq/dAPyeWp15tGrcrrJho1hKK5uBS70BCtfrIFg81sqcTVu3Ta+KD1Tk5vAtBNElWxtAa+m8K9A==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-core-commonmark": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-extension-mdxjs-esm/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-destination": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-destination/-/micromark-factory-destination-2.0.1.tgz", - "integrity": "sha512-Xe6rDdJlkmbFRExpTOmRj9N3MaWmbAgdpSrBQvCFqhezUn4AHqJHbaEnfbVYYiexVSs//tqOdY/DxhjdCiJnIA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-destination/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-label": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-label/-/micromark-factory-label-2.0.1.tgz", - "integrity": "sha512-VFMekyQExqIW7xIChcXn4ok29YE3rnuyveW3wZQWWqF4Nv9Wk5rgJ99KzPvHjkmPXF93FXIbBp6YdW3t71/7Vg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-label/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-mdx-expression": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-factory-mdx-expression/-/micromark-factory-mdx-expression-2.0.3.tgz", - "integrity": "sha512-kQnEtA3vzucU2BkrIa8/VaSAsP+EJ3CKOvhMuJgOEGg9KDC6OAY6nSnNDVRiVNRqj7Y4SlSzcStaH/5jge8JdQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "devlop": "^1.0.0", - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-events-to-acorn": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unist-util-position-from-estree": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-mdx-expression/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-space": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-1.1.0.tgz", - "integrity": "sha512-cRzEj7c0OL4Mw2v6nwzttyOZe8XY/Z8G0rzmWQZTBi/jjwyw/U4uqKtUORXQrR5bAZZnbTI/feRV/R7hc4jQYQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-factory-space/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-title": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-title/-/micromark-factory-title-2.0.1.tgz", - "integrity": "sha512-5bZ+3CjhAd9eChYTHsjy6TGxpOFSKgKKJPJxr293jTbfry2KDoWkhBb6TcPVB4NmzaPhMs1Frm9AZH7OD4Cjzw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-title/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-factory-whitespace": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-whitespace/-/micromark-factory-whitespace-2.0.1.tgz", - "integrity": "sha512-Ob0nuZ3PKt/n0hORHyvoD9uZhr+Za8sFoP+OnMcnWK5lngSzALgQYKMr9RJVOWLqQYuyn6ulqGWSXdwf6F80lQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-factory-space": "^2.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-factory-whitespace/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-character": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-1.2.0.tgz", - "integrity": "sha512-lXraTwcX3yH/vMDaFWCQJP1uIszLVebzUa3ZHdrgxr7KEU/9mL4mVgCpGbyhvNLNlauROiNUq7WN5u7ndbY6xg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^1.0.0", - "micromark-util-types": "^1.0.0" - } - }, - "node_modules/micromark-util-character/node_modules/micromark-util-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-1.1.0.tgz", - "integrity": "sha512-ukRBgie8TIAcacscVHSiddHjO4k/q3pnedmzMQ4iwDcK0FtFCohKOlFbaOL/mPgfnPsL3C1ZyxJa4sbWrBl3jg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-chunked": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-chunked/-/micromark-util-chunked-2.0.1.tgz", - "integrity": "sha512-QUNFEOPELfmvv+4xiNg2sRYeS/P84pTW0TCgP5zc9FpXetHY0ab7SxKyAQCNCc1eK0459uoLI1y5oO5Vc1dbhA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-chunked/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-classify-character": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-classify-character/-/micromark-util-classify-character-2.0.1.tgz", - "integrity": "sha512-K0kHzM6afW/MbeWYWLjoHQv1sgg2Q9EccHEDzSkxiP/EaagNzCm7T/WMKZ3rjMbvIpvBiZgwR3dKMygtA4mG1Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-classify-character/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-combine-extensions": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-combine-extensions/-/micromark-util-combine-extensions-2.0.1.tgz", - "integrity": "sha512-OnAnH8Ujmy59JcyZw8JSbK9cGpdVY44NKgSM7E9Eh7DiLS2E9RNQf0dONaGDzEG9yjEl5hcqeIsj4hfRkLH/Bg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-chunked": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-decode-numeric-character-reference/-/micromark-util-decode-numeric-character-reference-2.0.2.tgz", - "integrity": "sha512-ccUbYk6CwVdkmCQMyr64dXz42EfHGkPQlBj5p7YVGzq8I7CtjXZJrubAYezf7Rp+bjPseiROqe7G6foFd+lEuw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-numeric-character-reference/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-decode-string": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-decode-string/-/micromark-util-decode-string-2.0.1.tgz", - "integrity": "sha512-nDV/77Fj6eH1ynwscYTOsbK7rR//Uj0bZXBwJZRfaLEJ1iGBR6kIfNmlNqaqJf649EP0F3NWNdeJi03elllNUQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "decode-named-character-reference": "^1.0.0", - "micromark-util-character": "^2.0.0", - "micromark-util-decode-numeric-character-reference": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-decode-string/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-encode": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", - "integrity": "sha512-c3cVx2y4KqUnwopcO9b/SCdo2O67LwJJ/UyqGfbigahfegL9myoEFoDYZgkT7f36T0bLrM9hZTAaAyH+PCAXjw==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-events-to-acorn": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-util-events-to-acorn/-/micromark-util-events-to-acorn-2.0.3.tgz", - "integrity": "sha512-jmsiEIiZ1n7X1Rr5k8wVExBQCg5jy4UXVADItHmNk1zkwEVhBuIUKRu3fqv+hs4nxLISi2DQGlqIOGiFxgbfHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/unist": "^3.0.0", - "devlop": "^1.0.0", - "estree-util-visit": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0", - "vfile-message": "^4.0.0" - } - }, - "node_modules/micromark-util-events-to-acorn/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-html-tag-name": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-html-tag-name/-/micromark-util-html-tag-name-2.0.1.tgz", - "integrity": "sha512-2cNEiYDhCWKI+Gs9T0Tiysk136SnR13hhO8yW6BGNyhOC4qYFnwF1nKfD3HFAIXA5c45RrIG1ub11GiXeYd1xA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-normalize-identifier": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-normalize-identifier/-/micromark-util-normalize-identifier-2.0.1.tgz", - "integrity": "sha512-sxPqmo70LyARJs0w2UclACPUUEqltCkJ6PhKdMIDuJ3gSf/Q+/GIe3WKl0Ijb/GyH9lOpUkRAO2wp0GVkLvS9Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-normalize-identifier/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-resolve-all": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-resolve-all/-/micromark-util-resolve-all-2.0.1.tgz", - "integrity": "sha512-VdQyxFWFT2/FGJgwQnJYbe1jjQoNTS4RjglmSjTUlpUMa95Htx9NHeYW4rGDJzbjvCsl9eLjMQwGeElsqmzcHg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-sanitize-uri/-/micromark-util-sanitize-uri-2.0.1.tgz", - "integrity": "sha512-9N9IomZ/YuGGZZmQec1MbgxtlgougxTodVwDzzEouPKo3qFWvymFHWcnDi2vzV1ff6kas9ucW+o3yzJK9YB1AQ==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-encode": "^2.0.0", - "micromark-util-symbol": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-sanitize-uri/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-subtokenize": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-subtokenize/-/micromark-util-subtokenize-2.1.0.tgz", - "integrity": "sha512-XQLu552iSctvnEcgXw6+Sx75GflAPNED1qx7eBJ+wydBb2KCbRZe+NwvIEEMM83uml1+2WSXpBAcp9IUCgCYWA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "devlop": "^1.0.0", - "micromark-util-chunked": "^2.0.0", - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark-util-subtokenize/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-symbol": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-1.1.0.tgz", - "integrity": "sha512-uEjpEYY6KMs1g7QfJ2eX1SQEV+ZT4rUD3UcF6l57acZvLNK7PBZL+ty82Z1qhK1/yXIY4bdx04FKMgR0g4IAag==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromark/node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-character": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark/node_modules/micromark-util-character": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-util-character/-/micromark-util-character-2.1.1.tgz", - "integrity": "sha512-wv8tdUTJ3thSFFFJKtpYKOYiGP2+v96Hvk4Tu8KpCAsTMs6yi+nVmGh1syvSCsaxz45J6Jbw+9DD6g97+NV67Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT", - "dependencies": { - "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" - } - }, - "node_modules/micromark/node_modules/micromark-util-symbol": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-util-symbol/-/micromark-util-symbol-2.0.1.tgz", - "integrity": "sha512-vs5t8Apaud9N28kgCrRUdEed4UJ+wWNvicHLPxCa9ENlYuAY31M0ETy5y1vA33YoNPDFTghEbnh6efaE8h4x0Q==", - "funding": [ - { - "type": "GitHub Sponsors", - "url": "https://github.com/sponsors/unifiedjs" - }, - { - "type": "OpenCollective", - "url": "https://opencollective.com/unified" - } - ], - "license": "MIT" - }, - "node_modules/micromatch": { - "version": "4.0.8", - "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", - "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", - "license": "MIT", - "dependencies": { - "braces": "^3.0.3", - "picomatch": "^2.3.1" - }, - "engines": { - "node": ">=8.6" - } - }, - "node_modules/mime": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", - "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "license": "MIT", - "bin": { - "mime": "cli.js" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/mime-db": { - "version": "1.33.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.33.0.tgz", - "integrity": "sha512-BHJ/EKruNIqJf/QahvxwQZXKygOQ256myeN/Ew+THcAa5q+PjyTTMMeNQC4DZw5AwfvelsUrA6B67NKMqXDbzQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mime-types": { - "version": "2.1.18", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.18.tgz", - "integrity": "sha512-lc/aahn+t4/SWV/qcmumYjymLsWfN3ELhpmVuUFjgsORruuZPVSwAQryq+HHGvO/SI2KVX26bx+En+zhM8g8hQ==", - "license": "MIT", - "dependencies": { - "mime-db": "~1.33.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/mimic-fn": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", - "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/mimic-response": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-4.0.0.tgz", - "integrity": "sha512-e5ISH9xMYU0DzrT+jl8q2ze9D6eWBto+I8CNpe+VI+K2J/F/k3PdkdTdz4wvGVH4NTpo+NRYTVIuMQEMMcsLqg==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/mini-css-extract-plugin": { - "version": "2.9.4", - "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.4.tgz", - "integrity": "sha512-ZWYT7ln73Hptxqxk2DxPU9MmapXRhxkJD6tkSR04dnQxm8BGu2hzgKLugK5yySD97u/8yy7Ma7E76k9ZdvtjkQ==", - "license": "MIT", - "dependencies": { - "schema-utils": "^4.0.0", - "tapable": "^2.2.1" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.0.0" - } - }, - "node_modules/minimalistic-assert": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", - "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==", - "license": "ISC" - }, - "node_modules/minimatch": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", - "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, - "node_modules/minimist": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", - "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", - "license": "MIT", - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/mrmime": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/mrmime/-/mrmime-2.0.1.tgz", - "integrity": "sha512-Y3wQdFg2Va6etvQ5I82yUhGdsKrcYox6p7FfL1LbK2J4V01F9TGlepTIhnK24t7koZibmg82KGglhA1XK5IsLQ==", - "license": "MIT", - "engines": { - "node": ">=10" - } - }, - "node_modules/ms": { - "version": "2.1.3", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", - "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "license": "MIT" - }, - "node_modules/multicast-dns": { - "version": "7.2.5", - "resolved": "https://registry.npmjs.org/multicast-dns/-/multicast-dns-7.2.5.tgz", - "integrity": "sha512-2eznPJP8z2BFLX50tf0LuODrpINqP1RVIm/CObbTcBRITQgmC/TjcREF1NeTBzIcR5XO/ukWo+YHOjBbFwIupg==", - "license": "MIT", - "dependencies": { - "dns-packet": "^5.2.2", - "thunky": "^1.0.2" - }, - "bin": { - "multicast-dns": "cli.js" - } - }, - "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "bin": { - "nanoid": "bin/nanoid.cjs" - }, - "engines": { - "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" - } - }, - "node_modules/negotiator": { - "version": "0.6.4", - "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.4.tgz", - "integrity": "sha512-myRT3DiWPHqho5PrJaIRyaMv2kgYf0mUVgBNOYMuCH5Ki1yEiQaf/ZJuQ62nvpc44wL5WDbTX7yGJi1Neevw8w==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/neo-async": { - "version": "2.6.2", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", - "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", - "license": "MIT" - }, - "node_modules/no-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", - "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", - "license": "MIT", - "dependencies": { - "lower-case": "^2.0.2", - "tslib": "^2.0.3" - } - }, - "node_modules/node-emoji": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/node-emoji/-/node-emoji-2.2.0.tgz", - "integrity": "sha512-Z3lTE9pLaJF47NyMhd4ww1yFTAP8YhYI8SleJiHzM46Fgpm5cnNzSl9XfzFNqbaz+VlJrIj3fXQ4DeN1Rjm6cw==", - "license": "MIT", - "dependencies": { - "@sindresorhus/is": "^4.6.0", - "char-regex": "^1.0.2", - "emojilib": "^2.4.0", - "skin-tone": "^2.0.0" - }, - "engines": { - "node": ">=18" - } - }, - "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", - "license": "(BSD-3-Clause OR GPL-2.0)", - "engines": { - "node": ">= 6.13.0" - } - }, - "node_modules/node-releases": { - "version": "2.0.21", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.21.tgz", - "integrity": "sha512-5b0pgg78U3hwXkCM8Z9b2FJdPZlr9Psr9V2gQPESdGHqbntyFJKFW4r5TeWGFzafGY3hzs1JC62VEQMbl1JFkw==", - "license": "MIT" - }, - "node_modules/normalize-path": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", - "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-range": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/normalize-range/-/normalize-range-0.1.2.tgz", - "integrity": "sha512-bdok/XvKII3nUpklnV6P2hxtMNrCboOjAcyBuQnWEhO665FwrSNRxU+AqpsyvO6LgGYPspN+lu5CLtw4jPRKNA==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/normalize-url": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/normalize-url/-/normalize-url-8.1.0.tgz", - "integrity": "sha512-X06Mfd/5aKsRHc0O0J5CUedwnPmnDtLF2+nq+KN9KSDlJHkPuh0JUviWjEWMe0SW/9TDdSLVPuk7L5gGTIA1/w==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/npm-run-path": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-4.0.1.tgz", - "integrity": "sha512-S48WzZW777zhNIrn7gxOlISNAqi9ZC/uQFnRdbeIHhZhCA6UqpkOT8T1G7BvfdgP4Er8gF4sUbaS0i7QvIfCWw==", - "license": "MIT", - "dependencies": { - "path-key": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/nprogress": { - "version": "0.2.0", - "resolved": "https://registry.npmjs.org/nprogress/-/nprogress-0.2.0.tgz", - "integrity": "sha512-I19aIingLgR1fmhftnbWWO3dXc0hSxqHQHQb3H8m+K3TnEn/iSeTZZOyvKXWqQESMwuUVnatlCnZdLBZZt2VSA==", - "license": "MIT" - }, - "node_modules/nth-check": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", - "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0" - }, - "funding": { - "url": "https://github.com/fb55/nth-check?sponsor=1" - } - }, - "node_modules/null-loader": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/null-loader/-/null-loader-4.0.1.tgz", - "integrity": "sha512-pxqVbi4U6N26lq+LmgIbB5XATP0VdZKOG25DhHi8btMmJJefGArFyDg1yc4U3hWCJbMqSrw0qyrz1UQX+qYXqg==", - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/null-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/null-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/null-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/null-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/object-assign": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", - "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/object-inspect": { - "version": "1.13.4", - "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", - "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/object-keys": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", - "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/object.assign": { - "version": "4.1.7", - "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", - "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", - "license": "MIT", - "dependencies": { - "call-bind": "^1.0.8", - "call-bound": "^1.0.3", - "define-properties": "^1.2.1", - "es-object-atoms": "^1.0.0", - "has-symbols": "^1.1.0", - "object-keys": "^1.1.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/obuf": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/obuf/-/obuf-1.1.2.tgz", - "integrity": "sha512-PX1wu0AmAdPqOL1mWhqmlOd8kOIZQwGZw6rh7uby9fTc5lhaOWFLX3I6R1hrF9k3zUY40e6igsLGkDXK92LJNg==", - "license": "MIT" - }, - "node_modules/on-finished": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", - "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "license": "MIT", - "dependencies": { - "ee-first": "1.1.1" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/on-headers": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.1.0.tgz", - "integrity": "sha512-737ZY3yNnXy37FHkQxPzt4UZ2UWPWiCZWLvFZ4fu5cueciegX0zGPnrlY6bwRg4FdQOe9YU8MkmJwGhoMybl8A==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/once": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", - "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "license": "ISC", - "dependencies": { - "wrappy": "1" - } - }, - "node_modules/onetime": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.2.tgz", - "integrity": "sha512-kbpaSSGJTWdAY5KPVeMOKXSrPtr8C8C7wodJbcsd51jRnmD+GZu8Y0VoU6Dm5Z4vWr0Ig/1NKuWRKf7j5aaYSg==", - "license": "MIT", - "dependencies": { - "mimic-fn": "^2.1.0" - }, - "engines": { - "node": ">=6" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/open": { - "version": "8.4.2", - "resolved": "https://registry.npmjs.org/open/-/open-8.4.2.tgz", - "integrity": "sha512-7x81NCL719oNbsq/3mh+hVrAWmFuEYUqrq/Iw3kUzH8ReypT9QQ0BLoJS7/G9k6N81XjW4qHWtjWwe/9eLy1EQ==", - "license": "MIT", - "dependencies": { - "define-lazy-prop": "^2.0.0", - "is-docker": "^2.1.1", - "is-wsl": "^2.2.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/opener": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/opener/-/opener-1.5.2.tgz", - "integrity": "sha512-ur5UIdyw5Y7yEj9wLzhqXiy6GZ3Mwx0yGI+5sMn2r0N0v3cKJvUmFH5yPP+WXh9e0xfyzyJX95D8l088DNFj7A==", - "license": "(WTFPL OR MIT)", - "bin": { - "opener": "bin/opener-bin.js" - } - }, - "node_modules/p-cancelable": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/p-cancelable/-/p-cancelable-3.0.0.tgz", - "integrity": "sha512-mlVgR3PGuzlo0MmTdk4cXqXWlwQDLnONTAg6sm62XkMJEiRxN3GL3SffkYvqwonbkJBcrI7Uvv5Zh9yjvn2iUw==", - "license": "MIT", - "engines": { - "node": ">=12.20" - } - }, - "node_modules/p-finally": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-1.0.0.tgz", - "integrity": "sha512-LICb2p9CB7FS+0eR1oqWnHhp0FljGLZCWBE9aix0Uye9W8LTQPwMTYVGWQWIw9RdQiDg4+epXQODwIYJtSJaow==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/p-limit": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-4.0.0.tgz", - "integrity": "sha512-5b0R4txpzjPWVw/cXXUResoD4hb6U/x9BH08L7nw+GN1sezDzPdxeRvpc9c433fZhBan/wusjbCsqwqm4EIBIQ==", - "license": "MIT", - "dependencies": { - "yocto-queue": "^1.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-locate": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-6.0.0.tgz", - "integrity": "sha512-wPrq66Llhl7/4AGC6I+cqxT07LhXvWL08LNXz1fENOw0Ap4sRZZ/gZpTTJ5jpurzzzfS2W/Ge9BY3LgLjCShcw==", - "license": "MIT", - "dependencies": { - "p-limit": "^4.0.0" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-map": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/p-map/-/p-map-4.0.0.tgz", - "integrity": "sha512-/bjOqmgETBYB5BoEeGVea8dmvHb2m9GLy1E9W43yeyfP6QQCZGFNa+XRceJEuDB6zqr+gKpIAmlLebMpykw/MQ==", - "license": "MIT", - "dependencies": { - "aggregate-error": "^3.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-queue": { - "version": "6.6.2", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-6.6.2.tgz", - "integrity": "sha512-RwFpb72c/BhQLEXIZ5K2e+AhgNVmIejGlTgiB9MzZ0e93GRvqZ7uSi0dvRF7/XIXDeNkra2fNHBxTyPDGySpjQ==", - "license": "MIT", - "dependencies": { - "eventemitter3": "^4.0.4", - "p-timeout": "^3.2.0" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/p-retry": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/p-retry/-/p-retry-4.6.2.tgz", - "integrity": "sha512-312Id396EbJdvRONlngUx0NydfrIQ5lsYu0znKVUzVvArzEIt08V1qhtyESbGVd1FGX7UKtiFp5uwKZdM8wIuQ==", - "license": "MIT", - "dependencies": { - "@types/retry": "0.12.0", - "retry": "^0.13.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/p-timeout": { - "version": "3.2.0", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-3.2.0.tgz", - "integrity": "sha512-rhIwUycgwwKcP9yTOOFK/AKsAopjjCakVqLHePO3CC6Mir1Z99xT+R63jZxAT5lFZLa2inS5h+ZS2GvR99/FBg==", - "license": "MIT", - "dependencies": { - "p-finally": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/package-json": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/package-json/-/package-json-8.1.1.tgz", - "integrity": "sha512-cbH9IAIJHNj9uXi196JVsRlt7cHKak6u/e6AkL/bkRelZ7rlL3X1YKxsZwa36xipOEKAsdtmaG6aAJoM1fx2zA==", - "license": "MIT", - "dependencies": { - "got": "^12.1.0", - "registry-auth-token": "^5.0.1", - "registry-url": "^6.0.0", - "semver": "^7.3.7" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/param-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/param-case/-/param-case-3.0.4.tgz", - "integrity": "sha512-RXlj7zCYokReqWpOPH9oYivUzLYZ5vAPIfEmCTNViosC78F8F0H9y7T7gG2M39ymgutxF5gcFEsyZQSph9Bp3A==", - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/parent-module": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/parent-module/-/parent-module-1.0.1.tgz", - "integrity": "sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==", - "license": "MIT", - "dependencies": { - "callsites": "^3.0.0" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/parse-entities": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/parse-entities/-/parse-entities-4.0.2.tgz", - "integrity": "sha512-GG2AQYWoLgL877gQIKeRPGO1xF9+eG1ujIb5soS5gPvLQ1y2o8FL90w2QWNdf9I361Mpp7726c+lj3U0qK1uGw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^2.0.0", - "character-entities-legacy": "^3.0.0", - "character-reference-invalid": "^2.0.0", - "decode-named-character-reference": "^1.0.0", - "is-alphanumerical": "^2.0.0", - "is-decimal": "^2.0.0", - "is-hexadecimal": "^2.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/parse-entities/node_modules/@types/unist": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/@types/unist/-/unist-2.0.11.tgz", - "integrity": "sha512-CmBKiL6NNo/OqgmMn95Fk9Whlp2mtvIv+KNpQKN2F4SjvrEesubTRWGYSg+BnWZOnlCaSTU1sMpsBOzgbYhnsA==", - "license": "MIT" - }, - "node_modules/parse-json": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", - "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", - "license": "MIT", - "dependencies": { - "@babel/code-frame": "^7.0.0", - "error-ex": "^1.3.1", - "json-parse-even-better-errors": "^2.3.0", - "lines-and-columns": "^1.1.6" - }, - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/parse-numeric-range": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/parse-numeric-range/-/parse-numeric-range-1.3.0.tgz", - "integrity": "sha512-twN+njEipszzlMJd4ONUYgSfZPDxgHhT9Ahed5uTigpQn90FggW4SA/AIPq/6a149fTbE9qBEcSwE3FAEp6wQQ==", - "license": "ISC" - }, - "node_modules/parse5": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/parse5/-/parse5-7.3.0.tgz", - "integrity": "sha512-IInvU7fabl34qmi9gY8XOVxhYyMyuH2xUNpb2q8/Y+7552KlejkRvqvD19nMoUW/uQGGbqNpA6Tufu5FL5BZgw==", - "license": "MIT", - "dependencies": { - "entities": "^6.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5-htmlparser2-tree-adapter": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/parse5-htmlparser2-tree-adapter/-/parse5-htmlparser2-tree-adapter-7.1.0.tgz", - "integrity": "sha512-ruw5xyKs6lrpo9x9rCZqZZnIUntICjQAd0Wsmp396Ul9lN/h+ifgVV1x1gZHi8euej6wTfpqX8j+BFQxF0NS/g==", - "license": "MIT", - "dependencies": { - "domhandler": "^5.0.3", - "parse5": "^7.0.0" - }, - "funding": { - "url": "https://github.com/inikulin/parse5?sponsor=1" - } - }, - "node_modules/parse5/node_modules/entities": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/entities/-/entities-6.0.1.tgz", - "integrity": "sha512-aN97NXWF6AWBTahfVOIrB/NShkzi5H7F9r1s9mD3cDj4Ko5f2qhhVoYMibXF7GlLveb/D2ioWay8lxI97Ven3g==", - "license": "BSD-2-Clause", - "engines": { - "node": ">=0.12" - }, - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/parseurl": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", - "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/pascal-case": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/pascal-case/-/pascal-case-3.1.2.tgz", - "integrity": "sha512-uWlGT3YSnK9x3BQJaOdcZwrnV6hPpd8jFH1/ucpiLRPh/2zCVJKS19E4GvYHvaCcACn3foXZ0cLB9Wrx1KGe5g==", - "license": "MIT", - "dependencies": { - "no-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/path-exists": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-5.0.0.tgz", - "integrity": "sha512-RjhtfwJOxzcFmNOi6ltcbcu4Iu+FL3zEj83dk4kAS+fVpTxXLO1b38RvJgT/0QwvV/L3aY9TAnyv0EOqW4GoMQ==", - "license": "MIT", - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/path-is-absolute": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", - "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/path-is-inside": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/path-is-inside/-/path-is-inside-1.0.2.tgz", - "integrity": "sha512-DUWJr3+ULp4zXmol/SZkFf3JGsS9/SIv+Y3Rt93/UjPpDpklB5f1er4O3POIbUuUJ3FXgqte2Q7SrU6zAqwk8w==", - "license": "(WTFPL OR MIT)" - }, - "node_modules/path-key": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", - "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/path-parse": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", - "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", - "license": "MIT" - }, - "node_modules/path-to-regexp": { - "version": "1.9.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-1.9.0.tgz", - "integrity": "sha512-xIp7/apCFJuUHdDLWe8O1HIkb0kQrOMb/0u6FXQjemHn/ii5LrIzU6bdECnsiTF/GjZkMEKg1xdiZwNqDYlZ6g==", - "license": "MIT", - "dependencies": { - "isarray": "0.0.1" - } - }, - "node_modules/path-type": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/path-type/-/path-type-4.0.0.tgz", - "integrity": "sha512-gDKb8aZMDeD/tZWs9P6+q0J9Mwkdl6xMV8TjnGP3qJVJ06bdMgkbBlLU8IdfOsIsFz2BW1rNVT3XuNEl8zPAvw==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/picocolors": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", - "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", - "license": "ISC" - }, - "node_modules/picomatch": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", - "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", - "license": "MIT", - "engines": { - "node": ">=8.6" - }, - "funding": { - "url": "https://github.com/sponsors/jonschlinkert" - } - }, - "node_modules/pkg-dir": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-7.0.0.tgz", - "integrity": "sha512-Ie9z/WINcxxLp27BKOCHGde4ITq9UklYKDzVo1nhk5sqGEXU3FpkwP5GM2voTGJkGd9B3Otl+Q4uwSOeSUtOBA==", - "license": "MIT", - "dependencies": { - "find-up": "^6.3.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/postcss": { - "version": "8.5.6", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", - "integrity": "sha512-3Ybi1tAuwAP9s0r1UQ2J4n5Y0G05bJkpUIO0/bI9MhwmD70S5aTWbXGBwxHrelT+XM1k6dM0pk+SwNkpTRN7Pg==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/postcss/" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/postcss" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "nanoid": "^3.3.11", - "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" - }, - "engines": { - "node": "^10 || ^12 || >=14" - } - }, - "node_modules/postcss-attribute-case-insensitive": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/postcss-attribute-case-insensitive/-/postcss-attribute-case-insensitive-7.0.1.tgz", - "integrity": "sha512-Uai+SupNSqzlschRyNx3kbCTWgY/2hcwtHEI/ej2LJWc9JJ77qKgGptd8DHwY1mXtZ7Aoh4z4yxfwMBue9eNgw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-attribute-case-insensitive/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-calc": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-calc/-/postcss-calc-9.0.1.tgz", - "integrity": "sha512-TipgjGyzP5QzEhsOZUaIkeO5mKeMFpebWzRogWG/ysonUlnHcq5aJe0jOjpfzUU8PeSaBQnrE8ehR0QA5vs8PQ==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.11", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.2.2" - } - }, - "node_modules/postcss-clamp": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/postcss-clamp/-/postcss-clamp-4.1.0.tgz", - "integrity": "sha512-ry4b1Llo/9zz+PKC+030KUnPITTJAHeOwjfAyyB60eT0AorGLdzp52s31OsPRHRf8NchkgFoG2y6fCfn1IV1Ow==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=7.6.0" - }, - "peerDependencies": { - "postcss": "^8.4.6" - } - }, - "node_modules/postcss-color-functional-notation": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/postcss-color-functional-notation/-/postcss-color-functional-notation-7.0.12.tgz", - "integrity": "sha512-TLCW9fN5kvO/u38/uesdpbx3e8AkTYhMvDZYa9JpmImWuTE99bDQ7GU7hdOADIZsiI9/zuxfAJxny/khknp1Zw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-hex-alpha": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-color-hex-alpha/-/postcss-color-hex-alpha-10.0.0.tgz", - "integrity": "sha512-1kervM2cnlgPs2a8Vt/Qbe5cQ++N7rkYo/2rz2BkqJZIHQwaVuJgQH38REHrAi4uM0b1fqxMkWYmese94iMp3w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-color-rebeccapurple": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-color-rebeccapurple/-/postcss-color-rebeccapurple-10.0.0.tgz", - "integrity": "sha512-JFta737jSP+hdAIEhk1Vs0q0YF5P8fFcj+09pweS8ktuGuZ8pPlykHsk6mPxZ8awDl4TrcxUqJo9l1IhVr/OjQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-colormin": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-colormin/-/postcss-colormin-6.1.0.tgz", - "integrity": "sha512-x9yX7DOxeMAR+BgGVnNSAxmAj98NX/YxEMNFP+SDCEeNLb2r3i6Hh1ksMsnW8Ub5SLCpbescQqn9YEbE9554Sw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "colord": "^2.9.3", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-convert-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-convert-values/-/postcss-convert-values-6.1.0.tgz", - "integrity": "sha512-zx8IwP/ts9WvUM6NkVSkiU902QZL1bwPhaVaLynPtCsOTqp+ZKbNi+s6XJg3rfqpKGA/oc7Oxk5t8pOQJcwl/w==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-custom-media": { - "version": "11.0.6", - "resolved": "https://registry.npmjs.org/postcss-custom-media/-/postcss-custom-media-11.0.6.tgz", - "integrity": "sha512-C4lD4b7mUIw+RZhtY7qUbf4eADmb7Ey8BFA2px9jUbwg7pjTZDl4KY4bvlUV+/vXQvzQRfiGEVJyAbtOsCMInw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.5", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/media-query-list-parser": "^4.0.3" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-properties": { - "version": "14.0.6", - "resolved": "https://registry.npmjs.org/postcss-custom-properties/-/postcss-custom-properties-14.0.6.tgz", - "integrity": "sha512-fTYSp3xuk4BUeVhxCSJdIPhDLpJfNakZKoiTDx7yRGCdlZrSJR7mWKVOBS4sBF+5poPQFMj2YdXx1VHItBGihQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.5", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-selectors": { - "version": "8.0.5", - "resolved": "https://registry.npmjs.org/postcss-custom-selectors/-/postcss-custom-selectors-8.0.5.tgz", - "integrity": "sha512-9PGmckHQswiB2usSO6XMSswO2yFWVoCAuih1yl9FVcwkscLjRKjwsjM3t+NIWpSU2Jx3eOiK2+t4vVTQaoCHHg==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "@csstools/cascade-layer-name-parser": "^2.0.5", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-custom-selectors/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-dir-pseudo-class": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-dir-pseudo-class/-/postcss-dir-pseudo-class-9.0.1.tgz", - "integrity": "sha512-tRBEK0MHYvcMUrAuYMEOa0zg9APqirBcgzi6P21OhxtJyJADo/SWBwY1CAwEohQ/6HDaa9jCjLRG7K3PVQYHEA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-dir-pseudo-class/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-discard-comments": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-comments/-/postcss-discard-comments-6.0.2.tgz", - "integrity": "sha512-65w/uIqhSBBfQmYnG92FO1mWZjJ4GL5b8atm5Yw2UgrwD7HiNiSSNwJor1eCFGzUgYnN/iIknhNRVqjrrpuglw==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-duplicates": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-duplicates/-/postcss-discard-duplicates-6.0.3.tgz", - "integrity": "sha512-+JA0DCvc5XvFAxwx6f/e68gQu/7Z9ud584VLmcgto28eB8FqSFZwtrLwB5Kcp70eIoWP/HXqz4wpo8rD8gpsTw==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-empty": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-discard-empty/-/postcss-discard-empty-6.0.3.tgz", - "integrity": "sha512-znyno9cHKQsK6PtxL5D19Fj9uwSzC2mB74cpT66fhgOadEUPyXFkbgwm5tvc3bt3NAy8ltE5MrghxovZRVnOjQ==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-overridden": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-discard-overridden/-/postcss-discard-overridden-6.0.2.tgz", - "integrity": "sha512-j87xzI4LUggC5zND7KdjsI25APtyMuynXZSujByMaav2roV6OZX+8AaCUcZSWqckZpjAjRyFDdpqybgjFO0HJQ==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-discard-unused": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-discard-unused/-/postcss-discard-unused-6.0.5.tgz", - "integrity": "sha512-wHalBlRHkaNnNwfC8z+ppX57VhvS+HWgjW508esjdaEYr3Mx7Gnn2xA4R/CKf5+Z9S5qsqC+Uzh4ueENWwCVUA==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-double-position-gradients": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-double-position-gradients/-/postcss-double-position-gradients-6.0.4.tgz", - "integrity": "sha512-m6IKmxo7FxSP5nF2l63QbCC3r+bWpFUWmZXZf096WxG0m7Vl1Q1+ruFOhpdDRmKrRS+S3Jtk+TVk/7z0+BVK6g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-visible": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-focus-visible/-/postcss-focus-visible-10.0.1.tgz", - "integrity": "sha512-U58wyjS/I1GZgjRok33aE8juW9qQgQUNwTSdxQGuShHzwuYdcklnvK/+qOWX1Q9kr7ysbraQ6ht6r+udansalA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-visible/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-focus-within": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/postcss-focus-within/-/postcss-focus-within-9.0.1.tgz", - "integrity": "sha512-fzNUyS1yOYa7mOjpci/bR+u+ESvdar6hk8XNK/TRR0fiGTp2QT5N+ducP0n3rfH/m9I7H/EQU6lsa2BrgxkEjw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-focus-within/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-font-variant": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/postcss-font-variant/-/postcss-font-variant-5.0.0.tgz", - "integrity": "sha512-1fmkBaCALD72CK2a9i468mA/+tr9/1cBxRRMXOUaZqO43oWPR5imcyPjXwuv7PXbCid4ndlP5zWhidQVVa3hmA==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-gap-properties": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-gap-properties/-/postcss-gap-properties-6.0.0.tgz", - "integrity": "sha512-Om0WPjEwiM9Ru+VhfEDPZJAKWUd0mV1HmNXqp2C29z80aQ2uP9UVhLc7e3aYMIor/S5cVhoPgYQ7RtfeZpYTRw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-image-set-function": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/postcss-image-set-function/-/postcss-image-set-function-7.0.0.tgz", - "integrity": "sha512-QL7W7QNlZuzOwBTeXEmbVckNt1FSmhQtbMRvGGqqU4Nf4xk6KUEQhAoWuMzwbSv5jxiRiSZ5Tv7eiDB9U87znA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/utilities": "^2.0.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-lab-function": { - "version": "7.0.12", - "resolved": "https://registry.npmjs.org/postcss-lab-function/-/postcss-lab-function-7.0.12.tgz", - "integrity": "sha512-tUcyRk1ZTPec3OuKFsqtRzW2Go5lehW29XA21lZ65XmzQkz43VY2tyWEC202F7W3mILOjw0voOiuxRGTsN+J9w==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/css-color-parser": "^3.1.0", - "@csstools/css-parser-algorithms": "^3.0.5", - "@csstools/css-tokenizer": "^3.0.4", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/utilities": "^2.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-loader": { - "version": "7.3.4", - "resolved": "https://registry.npmjs.org/postcss-loader/-/postcss-loader-7.3.4.tgz", - "integrity": "sha512-iW5WTTBSC5BfsBJ9daFMPVrLT36MrNiC6fqOZTTaHjBNX6Pfd5p+hSBqe/fEeNd7pc13QiAyGt7VdGMw4eRC4A==", - "license": "MIT", - "dependencies": { - "cosmiconfig": "^8.3.5", - "jiti": "^1.20.0", - "semver": "^7.5.4" - }, - "engines": { - "node": ">= 14.15.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "postcss": "^7.0.0 || ^8.0.1", - "webpack": "^5.0.0" - } - }, - "node_modules/postcss-logical": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/postcss-logical/-/postcss-logical-8.1.0.tgz", - "integrity": "sha512-pL1hXFQ2fEXNKiNiAgtfA005T9FBxky5zkX6s4GZM2D8RkVgRqz3f4g1JUoq925zXv495qk8UNldDwh8uGEDoA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-merge-idents": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-merge-idents/-/postcss-merge-idents-6.0.3.tgz", - "integrity": "sha512-1oIoAsODUs6IHQZkLQGO15uGEbK3EAl5wi9SS8hs45VgsxQfMnxvt+L+zIr7ifZFIH14cfAeVe2uCTa+SPRa3g==", - "license": "MIT", - "dependencies": { - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-longhand": { - "version": "6.0.5", - "resolved": "https://registry.npmjs.org/postcss-merge-longhand/-/postcss-merge-longhand-6.0.5.tgz", - "integrity": "sha512-5LOiordeTfi64QhICp07nzzuTDjNSO8g5Ksdibt44d+uvIIAE1oZdRn8y/W5ZtYgRH/lnLDlvi9F8btZcVzu3w==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "stylehacks": "^6.1.1" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-merge-rules": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/postcss-merge-rules/-/postcss-merge-rules-6.1.1.tgz", - "integrity": "sha512-KOdWF0gju31AQPZiD+2Ar9Qjowz1LTChSjFFbS+e2sFgc4uHOp3ZvVX4sNeTlk0w2O31ecFGgrFzhO0RSWbWwQ==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0", - "cssnano-utils": "^4.0.2", - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-font-values": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-font-values/-/postcss-minify-font-values-6.1.0.tgz", - "integrity": "sha512-gklfI/n+9rTh8nYaSJXlCo3nOKqMNkxuGpTn/Qm0gstL3ywTr9/WRKznE+oy6fvfolH6dF+QM4nCo8yPLdvGJg==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-gradients": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-minify-gradients/-/postcss-minify-gradients-6.0.3.tgz", - "integrity": "sha512-4KXAHrYlzF0Rr7uc4VrfwDJ2ajrtNEpNEuLxFgwkhFZ56/7gaE4Nr49nLsQDZyUe+ds+kEhf+YAUolJiYXF8+Q==", - "license": "MIT", - "dependencies": { - "colord": "^2.9.3", - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-params": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-minify-params/-/postcss-minify-params-6.1.0.tgz", - "integrity": "sha512-bmSKnDtyyE8ujHQK0RQJDIKhQ20Jq1LYiez54WiaOoBtcSuflfK3Nm596LvbtlFcpipMjgClQGyGr7GAs+H1uA==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-minify-selectors": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-minify-selectors/-/postcss-minify-selectors-6.0.4.tgz", - "integrity": "sha512-L8dZSwNLgK7pjTto9PzWRoMbnLq5vsZSTu8+j1P/2GB8qdtGQfn+K1uSvFgYvgh83cbyxT5m43ZZhUMTJDSClQ==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-modules-extract-imports": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", - "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", - "license": "ISC", - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", - "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", - "license": "MIT", - "dependencies": { - "icss-utils": "^5.0.0", - "postcss-selector-parser": "^7.0.0", - "postcss-value-parser": "^4.1.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-local-by-default/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-scope": { - "version": "3.2.1", - "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", - "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", - "license": "ISC", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-modules-scope/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-modules-values": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", - "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", - "license": "ISC", - "dependencies": { - "icss-utils": "^5.0.0" - }, - "engines": { - "node": "^10 || ^12 || >= 14" - }, - "peerDependencies": { - "postcss": "^8.1.0" - } - }, - "node_modules/postcss-nesting": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/postcss-nesting/-/postcss-nesting-13.0.2.tgz", - "integrity": "sha512-1YCI290TX+VP0U/K/aFxzHzQWHWURL+CtHMSbex1lCdpXD1SoR2sYuxDu5aNI9lPoXpKTCggFZiDJbwylU0LEQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/selector-resolve-nested": "^3.1.0", - "@csstools/selector-specificity": "^5.0.0", - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-nesting/node_modules/@csstools/selector-resolve-nested": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-resolve-nested/-/selector-resolve-nested-3.1.0.tgz", - "integrity": "sha512-mf1LEW0tJLKfWyvn5KdDrhpxHyuxpbNwTIwOYLIvsTffeyOf85j5oIzfG0yosxDgx/sswlqBnESYUcQH0vgZ0g==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/postcss-nesting/node_modules/@csstools/selector-specificity": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/@csstools/selector-specificity/-/selector-specificity-5.0.0.tgz", - "integrity": "sha512-PCqQV3c4CoVm3kdPhyeZ07VmBRdH2EpMFA/pd9OASpOEC3aXNGoqPDAZ80D0cLpMBxnmk0+yNhGsEx31hq7Gtw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss-selector-parser": "^7.0.0" - } - }, - "node_modules/postcss-nesting/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-normalize-charset": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-charset/-/postcss-normalize-charset-6.0.2.tgz", - "integrity": "sha512-a8N9czmdnrjPHa3DeFlwqst5eaL5W8jYu3EBbTTkI5FHkfMhFZh1EGbku6jhHhIzTA6tquI2P42NtZ59M/H/kQ==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-display-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-display-values/-/postcss-normalize-display-values-6.0.2.tgz", - "integrity": "sha512-8H04Mxsb82ON/aAkPeq8kcBbAtI5Q2a64X/mnRRfPXBq7XeogoQvReqxEfc0B4WPq1KimjezNC8flUtC3Qz6jg==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-positions": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-positions/-/postcss-normalize-positions-6.0.2.tgz", - "integrity": "sha512-/JFzI441OAB9O7VnLA+RtSNZvQ0NCFZDOtp6QPFo1iIyawyXg0YI3CYM9HBy1WvwCRHnPep/BvI1+dGPKoXx/Q==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-repeat-style": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-repeat-style/-/postcss-normalize-repeat-style-6.0.2.tgz", - "integrity": "sha512-YdCgsfHkJ2jEXwR4RR3Tm/iOxSfdRt7jplS6XRh9Js9PyCR/aka/FCb6TuHT2U8gQubbm/mPmF6L7FY9d79VwQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-string": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-string/-/postcss-normalize-string-6.0.2.tgz", - "integrity": "sha512-vQZIivlxlfqqMp4L9PZsFE4YUkWniziKjQWUtsxUiVsSSPelQydwS8Wwcuw0+83ZjPWNTl02oxlIvXsmmG+CiQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-timing-functions": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-timing-functions/-/postcss-normalize-timing-functions-6.0.2.tgz", - "integrity": "sha512-a+YrtMox4TBtId/AEwbA03VcJgtyW4dGBizPl7e88cTFULYsprgHWTbfyjSLyHeBcK/Q9JhXkt2ZXiwaVHoMzA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-unicode": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-normalize-unicode/-/postcss-normalize-unicode-6.1.0.tgz", - "integrity": "sha512-QVC5TQHsVj33otj8/JD869Ndr5Xcc/+fwRh4HAsFsAeygQQXm+0PySrKbr/8tkDKzW+EVT3QkqZMfFrGiossDg==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-url": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-url/-/postcss-normalize-url-6.0.2.tgz", - "integrity": "sha512-kVNcWhCeKAzZ8B4pv/DnrU1wNh458zBNp8dh4y5hhxih5RZQ12QWMuQrDgPRw3LRl8mN9vOVfHl7uhvHYMoXsQ==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-normalize-whitespace": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-normalize-whitespace/-/postcss-normalize-whitespace-6.0.2.tgz", - "integrity": "sha512-sXZ2Nj1icbJOKmdjXVT9pnyHQKiSAyuNQHSgRCUgThn2388Y9cGVDR+E9J9iAYbSbLHI+UUwLVl1Wzco/zgv0Q==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-opacity-percentage": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/postcss-opacity-percentage/-/postcss-opacity-percentage-3.0.0.tgz", - "integrity": "sha512-K6HGVzyxUxd/VgZdX04DCtdwWJ4NGLG212US4/LA1TLAbHgmAsTWVR86o+gGIbFtnTkfOpb9sCRBx8K7HO66qQ==", - "funding": [ - { - "type": "kofi", - "url": "https://ko-fi.com/mrcgrtz" - }, - { - "type": "liberapay", - "url": "https://liberapay.com/mrcgrtz" - } - ], - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-ordered-values": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-ordered-values/-/postcss-ordered-values-6.0.2.tgz", - "integrity": "sha512-VRZSOB+JU32RsEAQrO94QPkClGPKJEL/Z9PCBImXMhIeK5KAYo6slP/hBYlLgrCjFxyqvn5VC81tycFEDBLG1Q==", - "license": "MIT", - "dependencies": { - "cssnano-utils": "^4.0.2", - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-overflow-shorthand": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/postcss-overflow-shorthand/-/postcss-overflow-shorthand-6.0.0.tgz", - "integrity": "sha512-BdDl/AbVkDjoTofzDQnwDdm/Ym6oS9KgmO7Gr+LHYjNWJ6ExORe4+3pcLQsLA9gIROMkiGVjjwZNoL/mpXHd5Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-page-break": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/postcss-page-break/-/postcss-page-break-3.0.4.tgz", - "integrity": "sha512-1JGu8oCjVXLa9q9rFTo4MbeeA5FMe00/9C7lN4va606Rdb+HkxXtXsmEDrIraQ11fGz/WvKWa8gMuCKkrXpTsQ==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8" - } - }, - "node_modules/postcss-place": { - "version": "10.0.0", - "resolved": "https://registry.npmjs.org/postcss-place/-/postcss-place-10.0.0.tgz", - "integrity": "sha512-5EBrMzat2pPAxQNWYavwAfoKfYcTADJ8AXGVPcUZ2UkNloUTWzJQExgrzrDkh3EKzmAx1evfTAzF9I8NGcc+qw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-preset-env": { - "version": "10.4.0", - "resolved": "https://registry.npmjs.org/postcss-preset-env/-/postcss-preset-env-10.4.0.tgz", - "integrity": "sha512-2kqpOthQ6JhxqQq1FSAAZGe9COQv75Aw8WbsOvQVNJ2nSevc9Yx/IKZGuZ7XJ+iOTtVon7LfO7ELRzg8AZ+sdw==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "@csstools/postcss-alpha-function": "^1.0.1", - "@csstools/postcss-cascade-layers": "^5.0.2", - "@csstools/postcss-color-function": "^4.0.12", - "@csstools/postcss-color-function-display-p3-linear": "^1.0.1", - "@csstools/postcss-color-mix-function": "^3.0.12", - "@csstools/postcss-color-mix-variadic-function-arguments": "^1.0.2", - "@csstools/postcss-content-alt-text": "^2.0.8", - "@csstools/postcss-contrast-color-function": "^2.0.12", - "@csstools/postcss-exponential-functions": "^2.0.9", - "@csstools/postcss-font-format-keywords": "^4.0.0", - "@csstools/postcss-gamut-mapping": "^2.0.11", - "@csstools/postcss-gradients-interpolation-method": "^5.0.12", - "@csstools/postcss-hwb-function": "^4.0.12", - "@csstools/postcss-ic-unit": "^4.0.4", - "@csstools/postcss-initial": "^2.0.1", - "@csstools/postcss-is-pseudo-class": "^5.0.3", - "@csstools/postcss-light-dark-function": "^2.0.11", - "@csstools/postcss-logical-float-and-clear": "^3.0.0", - "@csstools/postcss-logical-overflow": "^2.0.0", - "@csstools/postcss-logical-overscroll-behavior": "^2.0.0", - "@csstools/postcss-logical-resize": "^3.0.0", - "@csstools/postcss-logical-viewport-units": "^3.0.4", - "@csstools/postcss-media-minmax": "^2.0.9", - "@csstools/postcss-media-queries-aspect-ratio-number-values": "^3.0.5", - "@csstools/postcss-nested-calc": "^4.0.0", - "@csstools/postcss-normalize-display-values": "^4.0.0", - "@csstools/postcss-oklab-function": "^4.0.12", - "@csstools/postcss-progressive-custom-properties": "^4.2.1", - "@csstools/postcss-random-function": "^2.0.1", - "@csstools/postcss-relative-color-syntax": "^3.0.12", - "@csstools/postcss-scope-pseudo-class": "^4.0.1", - "@csstools/postcss-sign-functions": "^1.1.4", - "@csstools/postcss-stepped-value-functions": "^4.0.9", - "@csstools/postcss-text-decoration-shorthand": "^4.0.3", - "@csstools/postcss-trigonometric-functions": "^4.0.9", - "@csstools/postcss-unset-value": "^4.0.0", - "autoprefixer": "^10.4.21", - "browserslist": "^4.26.0", - "css-blank-pseudo": "^7.0.1", - "css-has-pseudo": "^7.0.3", - "css-prefers-color-scheme": "^10.0.0", - "cssdb": "^8.4.2", - "postcss-attribute-case-insensitive": "^7.0.1", - "postcss-clamp": "^4.1.0", - "postcss-color-functional-notation": "^7.0.12", - "postcss-color-hex-alpha": "^10.0.0", - "postcss-color-rebeccapurple": "^10.0.0", - "postcss-custom-media": "^11.0.6", - "postcss-custom-properties": "^14.0.6", - "postcss-custom-selectors": "^8.0.5", - "postcss-dir-pseudo-class": "^9.0.1", - "postcss-double-position-gradients": "^6.0.4", - "postcss-focus-visible": "^10.0.1", - "postcss-focus-within": "^9.0.1", - "postcss-font-variant": "^5.0.0", - "postcss-gap-properties": "^6.0.0", - "postcss-image-set-function": "^7.0.0", - "postcss-lab-function": "^7.0.12", - "postcss-logical": "^8.1.0", - "postcss-nesting": "^13.0.2", - "postcss-opacity-percentage": "^3.0.0", - "postcss-overflow-shorthand": "^6.0.0", - "postcss-page-break": "^3.0.4", - "postcss-place": "^10.0.0", - "postcss-pseudo-class-any-link": "^10.0.1", - "postcss-replace-overflow-wrap": "^4.0.0", - "postcss-selector-not": "^8.0.1" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-pseudo-class-any-link": { - "version": "10.0.1", - "resolved": "https://registry.npmjs.org/postcss-pseudo-class-any-link/-/postcss-pseudo-class-any-link-10.0.1.tgz", - "integrity": "sha512-3el9rXlBOqTFaMFkWDOkHUTQekFIYnaQY55Rsp8As8QQkpiSgIYEcF/6Ond93oHiDsGb4kad8zjt+NPlOC1H0Q==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT-0", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-pseudo-class-any-link/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-reduce-idents": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-reduce-idents/-/postcss-reduce-idents-6.0.3.tgz", - "integrity": "sha512-G3yCqZDpsNPoQgbDUy3T0E6hqOQ5xigUtBQyrmq3tn2GxlyiL0yyl7H+T8ulQR6kOcHJ9t7/9H4/R2tv8tJbMA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-initial": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/postcss-reduce-initial/-/postcss-reduce-initial-6.1.0.tgz", - "integrity": "sha512-RarLgBK/CrL1qZags04oKbVbrrVK2wcxhvta3GCxrZO4zveibqbRPmm2VI8sSgCXwoUHEliRSbOfpR0b/VIoiw==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "caniuse-api": "^3.0.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-reduce-transforms": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-reduce-transforms/-/postcss-reduce-transforms-6.0.2.tgz", - "integrity": "sha512-sB+Ya++3Xj1WaT9+5LOOdirAxP7dJZms3GRcYheSPi1PiTMigsxHAdkrbItHxwYHr4kt1zL7mmcHstgMYT+aiA==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-replace-overflow-wrap": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/postcss-replace-overflow-wrap/-/postcss-replace-overflow-wrap-4.0.0.tgz", - "integrity": "sha512-KmF7SBPphT4gPPcKZc7aDkweHiKEEO8cla/GjcBK+ckKxiZslIu3C4GCRW3DNfL0o7yW7kMQu9xlZ1kXRXLXtw==", - "license": "MIT", - "peerDependencies": { - "postcss": "^8.0.3" - } - }, - "node_modules/postcss-selector-not": { - "version": "8.0.1", - "resolved": "https://registry.npmjs.org/postcss-selector-not/-/postcss-selector-not-8.0.1.tgz", - "integrity": "sha512-kmVy/5PYVb2UOhy0+LqUYAhKj7DUGDpSWa5LZqlkWJaaAV+dxxsOG3+St0yNLu6vsKD7Dmqx+nWQt0iil89+WA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/csstools" - }, - { - "type": "opencollective", - "url": "https://opencollective.com/csstools" - } - ], - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^7.0.0" - }, - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "postcss": "^8.4" - } - }, - "node_modules/postcss-selector-not/node_modules/postcss-selector-parser": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.0.tgz", - "integrity": "sha512-8sLjZwK0R+JlxlYcTuVnyT2v+htpdrjDOKuMcOVdYjt52Lh8hWRYpxBPoKx/Zg+bcjc3wx6fmQevMmUztS/ccA==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", - "license": "MIT", - "dependencies": { - "cssesc": "^3.0.0", - "util-deprecate": "^1.0.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/postcss-sort-media-queries": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/postcss-sort-media-queries/-/postcss-sort-media-queries-5.2.0.tgz", - "integrity": "sha512-AZ5fDMLD8SldlAYlvi8NIqo0+Z8xnXU2ia0jxmuhxAU+Lqt9K+AlmLNJ/zWEnE9x+Zx3qL3+1K20ATgNOr3fAA==", - "license": "MIT", - "dependencies": { - "sort-css-media-queries": "2.2.0" - }, - "engines": { - "node": ">=14.0.0" - }, - "peerDependencies": { - "postcss": "^8.4.23" - } - }, - "node_modules/postcss-svgo": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/postcss-svgo/-/postcss-svgo-6.0.3.tgz", - "integrity": "sha512-dlrahRmxP22bX6iKEjOM+c8/1p+81asjKT+V5lrgOH944ryx/OHpclnIbGsKVd3uWOXFLYJwCVf0eEkJGvO96g==", - "license": "MIT", - "dependencies": { - "postcss-value-parser": "^4.2.0", - "svgo": "^3.2.0" - }, - "engines": { - "node": "^14 || ^16 || >= 18" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-unique-selectors": { - "version": "6.0.4", - "resolved": "https://registry.npmjs.org/postcss-unique-selectors/-/postcss-unique-selectors-6.0.4.tgz", - "integrity": "sha512-K38OCaIrO8+PzpArzkLKB42dSARtC2tmG6PvD4b1o1Q2E9Os8jzfWFfSy/rixsHwohtsDdFtAWGjFVFUdwYaMg==", - "license": "MIT", - "dependencies": { - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/postcss-value-parser": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", - "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", - "license": "MIT" - }, - "node_modules/postcss-zindex": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/postcss-zindex/-/postcss-zindex-6.0.2.tgz", - "integrity": "sha512-5BxW9l1evPB/4ZIc+2GobEBoKC+h8gPGCMi+jxsYvd2x0mjq7wazk6DrP71pStqxE9Foxh5TVnonbWpFZzXaYg==", - "license": "MIT", - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/pretty-error": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/pretty-error/-/pretty-error-4.0.0.tgz", - "integrity": "sha512-AoJ5YMAcXKYxKhuJGdcvse+Voc6v1RgnsR3nWcYU7q4t6z0Q6T86sv5Zq8VIRbOWWFpvdGE83LtdSMNd+6Y0xw==", - "license": "MIT", - "dependencies": { - "lodash": "^4.17.20", - "renderkid": "^3.0.0" - } - }, - "node_modules/pretty-time": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/pretty-time/-/pretty-time-1.1.0.tgz", - "integrity": "sha512-28iF6xPQrP8Oa6uxE6a1biz+lWeTOAPKggvjB8HAs6nVMKZwf5bG++632Dx614hIWgUPkgivRfG+a8uAXGTIbA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/prism-react-renderer": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/prism-react-renderer/-/prism-react-renderer-2.4.1.tgz", - "integrity": "sha512-ey8Ls/+Di31eqzUxC46h8MksNuGx/n0AAC8uKpwFau4RPDYLuE3EXTp8N8G2vX2N7UC/+IXeNUnlWBGGcAG+Ig==", - "license": "MIT", - "dependencies": { - "@types/prismjs": "^1.26.0", - "clsx": "^2.0.0" - }, - "peerDependencies": { - "react": ">=16.0.0" - } - }, - "node_modules/prismjs": { - "version": "1.30.0", - "resolved": "https://registry.npmjs.org/prismjs/-/prismjs-1.30.0.tgz", - "integrity": "sha512-DEvV2ZF2r2/63V+tK8hQvrR2ZGn10srHbXviTlcv7Kpzw8jWiNTqbVgjO3IY8RxrrOUF8VPMQQFysYYYv0YZxw==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/process-nextick-args": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", - "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==", - "license": "MIT" - }, - "node_modules/prompts": { - "version": "2.4.2", - "resolved": "https://registry.npmjs.org/prompts/-/prompts-2.4.2.tgz", - "integrity": "sha512-NxNv/kLguCA7p3jE8oL2aEBsrJWgAakBpgmgK6lpPWV+WuOmY6r2/zbAVnP+T8bQlA0nzHXSJSJW0Hq7ylaD2Q==", - "license": "MIT", - "dependencies": { - "kleur": "^3.0.3", - "sisteransi": "^1.0.5" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/prop-types": { - "version": "15.8.1", - "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", - "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", - "license": "MIT", - "dependencies": { - "loose-envify": "^1.4.0", - "object-assign": "^4.1.1", - "react-is": "^16.13.1" - } - }, - "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/proto-list": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/proto-list/-/proto-list-1.2.4.tgz", - "integrity": "sha512-vtK/94akxsTMhe0/cbfpR+syPuszcuwhqVjJq26CuNDgFGj682oRBXOP5MJpv2r7JtE8MsiepGIqvvOTBwn2vA==", - "license": "ISC" - }, - "node_modules/proxy-addr": { - "version": "2.0.7", - "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", - "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "license": "MIT", - "dependencies": { - "forwarded": "0.2.0", - "ipaddr.js": "1.9.1" - }, - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/proxy-addr/node_modules/ipaddr.js": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", - "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/punycode": { - "version": "2.3.1", - "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", - "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/pupa": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/pupa/-/pupa-3.3.0.tgz", - "integrity": "sha512-LjgDO2zPtoXP2wJpDjZrGdojii1uqO0cnwKoIoUzkfS98HDmbeiGmYiXo3lXeFlq2xvne1QFQhwYXSUCLKtEuA==", - "license": "MIT", - "dependencies": { - "escape-goat": "^4.0.0" - }, - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/qs": { - "version": "6.13.0", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", - "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "license": "BSD-3-Clause", - "dependencies": { - "side-channel": "^1.0.6" - }, - "engines": { - "node": ">=0.6" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/queue-microtask": { - "version": "1.2.3", - "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", - "integrity": "sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/quick-lru": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/quick-lru/-/quick-lru-5.1.1.tgz", - "integrity": "sha512-WuyALRjWPDGtt/wzJiadO5AXY+8hZ80hVpe6MyivgraREW751X3SbhRvG3eLKOYN+8VEvqLcf3wdnt44Z4S4SA==", - "license": "MIT", - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/randombytes": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", - "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", - "license": "MIT", - "dependencies": { - "safe-buffer": "^5.1.0" - } - }, - "node_modules/range-parser": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.0.tgz", - "integrity": "sha512-kA5WQoNVo4t9lNx2kQNFCxKeBl5IbbSNBl1M/tLkw9WCn+hxNBAW5Qh8gdhs63CJnhjJ2zQWFoqPJP2sK1AV5A==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/raw-body": { - "version": "2.5.2", - "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", - "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "license": "MIT", - "dependencies": { - "bytes": "3.1.2", - "http-errors": "2.0.0", - "iconv-lite": "0.4.24", - "unpipe": "1.0.0" - }, - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/raw-body/node_modules/bytes": { - "version": "3.1.2", - "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", - "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/rc": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", - "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", - "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", - "dependencies": { - "deep-extend": "^0.6.0", - "ini": "~1.3.0", - "minimist": "^1.2.0", - "strip-json-comments": "~2.0.1" - }, - "bin": { - "rc": "cli.js" - } - }, - "node_modules/rc/node_modules/ini": { - "version": "1.3.8", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", - "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", - "license": "ISC" - }, - "node_modules/rc/node_modules/strip-json-comments": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", - "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react/-/react-19.1.1.tgz", - "integrity": "sha512-w8nqGImo45dmMIfljjMwOGtbmC/mk4CMYhWIicdSflH91J9TyCyczcPFXJzrZ/ZXcgGRFeP6BU0BEJTw6tZdfQ==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/react-dom": { - "version": "19.1.1", - "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-19.1.1.tgz", - "integrity": "sha512-Dlq/5LAZgF0Gaz6yiqZCf6VCcZs1ghAJyrsu84Q/GT0gV+mCxbfmKNoGRKBYMJ8IEdGPqu49YWXD02GCknEDkw==", - "license": "MIT", - "dependencies": { - "scheduler": "^0.26.0" - }, - "peerDependencies": { - "react": "^19.1.1" - } - }, - "node_modules/react-fast-compare": { - "version": "3.2.2", - "resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz", - "integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ==", - "license": "MIT" - }, - "node_modules/react-helmet-async": { - "name": "@slorber/react-helmet-async", - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@slorber/react-helmet-async/-/react-helmet-async-1.3.0.tgz", - "integrity": "sha512-e9/OK8VhwUSc67diWI8Rb3I0YgI9/SBQtnhe9aEuK6MhZm7ntZZimXgwXnd8W96YTmSOb9M4d8LwhRZyhWr/1A==", - "license": "Apache-2.0", - "dependencies": { - "@babel/runtime": "^7.12.5", - "invariant": "^2.2.4", - "prop-types": "^15.7.2", - "react-fast-compare": "^3.2.0", - "shallowequal": "^1.1.0" - }, - "peerDependencies": { - "react": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", - "react-dom": "^16.6.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-is": { - "version": "16.13.1", - "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", - "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==", - "license": "MIT" - }, - "node_modules/react-json-view-lite": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/react-json-view-lite/-/react-json-view-lite-2.5.0.tgz", - "integrity": "sha512-tk7o7QG9oYyELWHL8xiMQ8x4WzjCzbWNyig3uexmkLb54r8jO0yH3WCWx8UZS0c49eSA4QUmG5caiRJ8fAn58g==", - "license": "MIT", - "engines": { - "node": ">=18" - }, - "peerDependencies": { - "react": "^18.0.0 || ^19.0.0" - } - }, - "node_modules/react-loadable": { - "name": "@docusaurus/react-loadable", - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/@docusaurus/react-loadable/-/react-loadable-6.0.0.tgz", - "integrity": "sha512-YMMxTUQV/QFSnbgrP3tjDzLHRg7vsbMn8e9HAa8o/1iXoiomo48b7sk/kkmWEuWNDPJVlKSJRB6Y2fHqdJk+SQ==", - "license": "MIT", - "dependencies": { - "@types/react": "*" - }, - "peerDependencies": { - "react": "*" - } - }, - "node_modules/react-loadable-ssr-addon-v5-slorber": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/react-loadable-ssr-addon-v5-slorber/-/react-loadable-ssr-addon-v5-slorber-1.0.1.tgz", - "integrity": "sha512-lq3Lyw1lGku8zUEJPDxsNm1AfYHBrO9Y1+olAYwpUJ2IGFBskM0DMKok97A6LWUpHm+o7IvQBOWu9MLenp9Z+A==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.10.3" - }, - "engines": { - "node": ">=10.13.0" - }, - "peerDependencies": { - "react-loadable": "*", - "webpack": ">=4.41.1 || 5.x" - } - }, - "node_modules/react-router": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router/-/react-router-5.3.4.tgz", - "integrity": "sha512-Ys9K+ppnJah3QuaRiLxk+jDWOR1MekYQrlytiXxC1RyfbdsZkS5pvKAzCCr031xHixZwpnsYNT5xysdFHQaYsA==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "hoist-non-react-statics": "^3.1.0", - "loose-envify": "^1.3.1", - "path-to-regexp": "^1.7.0", - "prop-types": "^15.6.2", - "react-is": "^16.6.0", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/react-router-config": { - "version": "5.1.1", - "resolved": "https://registry.npmjs.org/react-router-config/-/react-router-config-5.1.1.tgz", - "integrity": "sha512-DuanZjaD8mQp1ppHjgnnUnyOlqYXZVjnov/JzFhjLEwd3Z4dYjMSnqrEzzGThH47vpCOqPPwJM2FtthLeJ8Pbg==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.1.2" - }, - "peerDependencies": { - "react": ">=15", - "react-router": ">=5" - } - }, - "node_modules/react-router-dom": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/react-router-dom/-/react-router-dom-5.3.4.tgz", - "integrity": "sha512-m4EqFMHv/Ih4kpcBCONHbkT68KoAeHN4p3lAGoNryfHi0dMy0kCzEZakiKRsvg5wHZ/JLrLW8o8KomWiz/qbYQ==", - "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.12.13", - "history": "^4.9.0", - "loose-envify": "^1.3.1", - "prop-types": "^15.6.2", - "react-router": "5.3.4", - "tiny-invariant": "^1.0.2", - "tiny-warning": "^1.0.0" - }, - "peerDependencies": { - "react": ">=15" - } - }, - "node_modules/readable-stream": { - "version": "3.6.2", - "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", - "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", - "license": "MIT", - "dependencies": { - "inherits": "^2.0.3", - "string_decoder": "^1.1.1", - "util-deprecate": "^1.0.1" - }, - "engines": { - "node": ">= 6" - } - }, - "node_modules/readdirp": { - "version": "3.6.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", - "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", - "license": "MIT", - "dependencies": { - "picomatch": "^2.2.1" - }, - "engines": { - "node": ">=8.10.0" - } - }, - "node_modules/recma-build-jsx": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-build-jsx/-/recma-build-jsx-1.0.0.tgz", - "integrity": "sha512-8GtdyqaBcDfva+GUKDr3nev3VpKAhup1+RvkMvUxURHpW7QyIvk9F5wz7Vzo06CEMSilw6uArgRqhpiUcWp8ew==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-build-jsx": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-jsx": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/recma-jsx/-/recma-jsx-1.0.1.tgz", - "integrity": "sha512-huSIy7VU2Z5OLv6oFLosQGGDqPqdO1iq6bWNAdhzMxSJP7RAso4fCZ1cKu8j9YHCZf3TPrq4dw3okhrylgcd7w==", - "license": "MIT", - "dependencies": { - "acorn-jsx": "^5.0.0", - "estree-util-to-js": "^2.0.0", - "recma-parse": "^1.0.0", - "recma-stringify": "^1.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - }, - "peerDependencies": { - "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" - } - }, - "node_modules/recma-parse": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-parse/-/recma-parse-1.0.0.tgz", - "integrity": "sha512-OYLsIGBB5Y5wjnSnQW6t3Xg7q3fQ7FWbw/vcXtORTnyaSFscOtABg+7Pnz6YZ6c27fG1/aN8CjfwoUEUIdwqWQ==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "esast-util-from-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/recma-stringify": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/recma-stringify/-/recma-stringify-1.0.0.tgz", - "integrity": "sha512-cjwII1MdIIVloKvC9ErQ+OgAtwHBmcZ0Bg4ciz78FtbT8In39aAYbaA7zvxQ61xVMSPE8WxhLwLbhif4Js2C+g==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "estree-util-to-js": "^2.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/regenerate": { - "version": "1.4.2", - "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", - "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", - "license": "MIT" - }, - "node_modules/regenerate-unicode-properties": { - "version": "10.2.2", - "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.2.2.tgz", - "integrity": "sha512-m03P+zhBeQd1RGnYxrGyDAPpWX/epKirLrp8e3qevZdVkKtnCrjjWczIbYc8+xd6vcTStVlqfycTx1KR4LOr0g==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/regexpu-core": { - "version": "6.3.1", - "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-6.3.1.tgz", - "integrity": "sha512-DzcswPr252wEr7Qz8AyAVbfyBDKLoYp6eRA1We2Fa9qirRFSdtkP5sHr3yglDKy2BbA0fd2T+j/CUSKes3FeVQ==", - "license": "MIT", - "dependencies": { - "regenerate": "^1.4.2", - "regenerate-unicode-properties": "^10.2.2", - "regjsgen": "^0.8.0", - "regjsparser": "^0.12.0", - "unicode-match-property-ecmascript": "^2.0.0", - "unicode-match-property-value-ecmascript": "^2.2.1" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/registry-auth-token": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/registry-auth-token/-/registry-auth-token-5.1.0.tgz", - "integrity": "sha512-GdekYuwLXLxMuFTwAPg5UKGLW/UXzQrZvH/Zj791BQif5T05T0RsaLfHc9q3ZOKi7n+BoprPD9mJ0O0k4xzUlw==", - "license": "MIT", - "dependencies": { - "@pnpm/npm-conf": "^2.1.0" - }, - "engines": { - "node": ">=14" - } - }, - "node_modules/registry-url": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/registry-url/-/registry-url-6.0.1.tgz", - "integrity": "sha512-+crtS5QjFRqFCoQmvGduwYWEBng99ZvmFvF+cUJkGYF1L1BfU8C6Zp9T7f5vPAwyLkUExpvK+ANVZmGU49qi4Q==", - "license": "MIT", - "dependencies": { - "rc": "1.2.8" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/regjsgen": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/regjsgen/-/regjsgen-0.8.0.tgz", - "integrity": "sha512-RvwtGe3d7LvWiDQXeQw8p5asZUmfU1G/l6WbUXeHta7Y2PEIvBTwH6E2EfmYUK8pxcxEdEmaomqyp0vZZ7C+3Q==", - "license": "MIT" - }, - "node_modules/regjsparser": { - "version": "0.12.0", - "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.12.0.tgz", - "integrity": "sha512-cnE+y8bz4NhMjISKbgeVJtqNbtf5QpjZP+Bslo+UqkIt9QPnX9q095eiRRASJG1/tz6dlNr6Z5NsBiWYokp6EQ==", - "license": "BSD-2-Clause", - "dependencies": { - "jsesc": "~3.0.2" - }, - "bin": { - "regjsparser": "bin/parser" - } - }, - "node_modules/regjsparser/node_modules/jsesc": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.0.2.tgz", - "integrity": "sha512-xKqzzWXDttJuOcawBt4KnKHHIf5oQ/Cxax+0PWFG+DFDgHNAdi+TXECADI+RYiFUMmx8792xsMbbgXj4CwnP4g==", - "license": "MIT", - "bin": { - "jsesc": "bin/jsesc" - }, - "engines": { - "node": ">=6" - } - }, - "node_modules/rehype-raw": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/rehype-raw/-/rehype-raw-7.0.0.tgz", - "integrity": "sha512-/aE8hCfKlQeA8LmyeyQvQF3eBiLRGNlfBJEvWH7ivp9sBqs7TNqBL5X3v157rM4IFETqDnIOO+z5M/biZbo9Ww==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-raw": "^9.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/rehype-recma": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/rehype-recma/-/rehype-recma-1.0.0.tgz", - "integrity": "sha512-lqA4rGUf1JmacCNWWZx0Wv1dHqMwxzsDWYMTowuplHF3xH0N/MmrZ/G3BDZnzAkRmxDadujCjaKM2hqYdCBOGw==", - "license": "MIT", - "dependencies": { - "@types/estree": "^1.0.0", - "@types/hast": "^3.0.0", - "hast-util-to-estree": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/relateurl": { - "version": "0.2.7", - "resolved": "https://registry.npmjs.org/relateurl/-/relateurl-0.2.7.tgz", - "integrity": "sha512-G08Dxvm4iDN3MLM0EsP62EDV9IuhXPR6blNz6Utcp7zyV3tr4HVNINt6MpaRWbxoOHT3Q7YN2P+jaHX8vUbgog==", - "license": "MIT", - "engines": { - "node": ">= 0.10" - } - }, - "node_modules/remark-directive": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", - "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-directive": "^3.0.0", - "micromark-extension-directive": "^3.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-emoji": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-emoji/-/remark-emoji-4.0.1.tgz", - "integrity": "sha512-fHdvsTR1dHkWKev9eNyhTo4EFwbUvJ8ka9SgeWkMPYFX4WoI7ViVBms3PjlQYgw5TLvNQso3GUB/b/8t3yo+dg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.2", - "emoticon": "^4.0.1", - "mdast-util-find-and-replace": "^3.0.1", - "node-emoji": "^2.1.0", - "unified": "^11.0.4" - }, - "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" - } - }, - "node_modules/remark-frontmatter": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/remark-frontmatter/-/remark-frontmatter-5.0.0.tgz", - "integrity": "sha512-XTFYvNASMe5iPN0719nPrdItC9aU0ssC4v14mH1BCi1u0n1gAocqcujWUrByftZTbLhRtiKRyjYTSIOcr69UVQ==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-frontmatter": "^2.0.0", - "micromark-extension-frontmatter": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-gfm": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/remark-gfm/-/remark-gfm-4.0.1.tgz", - "integrity": "sha512-1quofZ2RQ9EWdeN34S79+KExV1764+wCUGop5CPL1WGdD0ocPpu91lzPGbwWMECpEpd42kJGQwzRfyov9j4yNg==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-gfm": "^3.0.0", - "micromark-extension-gfm": "^3.0.0", - "remark-parse": "^11.0.0", - "remark-stringify": "^11.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-mdx": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/remark-mdx/-/remark-mdx-3.1.1.tgz", - "integrity": "sha512-Pjj2IYlUY3+D8x00UJsIOg5BEvfMyeI+2uLPn9VO9Wg4MEtN/VTIq2NEJQfde9PnX15KgtHyl9S0BcTnWrIuWg==", - "license": "MIT", - "dependencies": { - "mdast-util-mdx": "^3.0.0", - "micromark-extension-mdxjs": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-parse": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-parse/-/remark-parse-11.0.0.tgz", - "integrity": "sha512-FCxlKLNGknS5ba/1lmpYijMUzX2esxW5xQqjWxw2eHFfS2MSdaHVINFmhjo+qN1WhZhNimq0dZATN9pH0IDrpA==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-from-markdown": "^2.0.0", - "micromark-util-types": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-rehype": { - "version": "11.1.2", - "resolved": "https://registry.npmjs.org/remark-rehype/-/remark-rehype-11.1.2.tgz", - "integrity": "sha512-Dh7l57ianaEoIpzbp0PC9UKAdCSVklD8E5Rpw7ETfbTl3FqcOOgq5q2LVDhgGCkaBv7p24JXikPdvhhmHvKMsw==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "@types/mdast": "^4.0.0", - "mdast-util-to-hast": "^13.0.0", - "unified": "^11.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/remark-stringify": { - "version": "11.0.0", - "resolved": "https://registry.npmjs.org/remark-stringify/-/remark-stringify-11.0.0.tgz", - "integrity": "sha512-1OSmLd3awB/t8qdoEOMazZkNsfVTeY4fTsgzcQFdXNq8ToTN4ZGwrMnlda4K6smTFKD+GRV6O48i6Z4iKgPPpw==", - "license": "MIT", - "dependencies": { - "@types/mdast": "^4.0.0", - "mdast-util-to-markdown": "^2.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/renderkid": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/renderkid/-/renderkid-3.0.0.tgz", - "integrity": "sha512-q/7VIQA8lmM1hF+jn+sFSPWGlMkSAeNYcPLmDQx2zzuiDfaLrOmumR8iaUKlenFgh0XRPIUeSPlH3A+AW3Z5pg==", - "license": "MIT", - "dependencies": { - "css-select": "^4.1.3", - "dom-converter": "^0.2.0", - "htmlparser2": "^6.1.0", - "lodash": "^4.17.21", - "strip-ansi": "^6.0.1" - } - }, - "node_modules/renderkid/node_modules/css-select": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-4.3.0.tgz", - "integrity": "sha512-wPpOYtnsVontu2mODhA19JrqWxNsfdatRKd64kmpRbQgh1KtItko5sTnEpPdpSaJszTOhEMlF/RPz28qj4HqhQ==", - "license": "BSD-2-Clause", - "dependencies": { - "boolbase": "^1.0.0", - "css-what": "^6.0.1", - "domhandler": "^4.3.1", - "domutils": "^2.8.0", - "nth-check": "^2.0.1" - }, - "funding": { - "url": "https://github.com/sponsors/fb55" - } - }, - "node_modules/renderkid/node_modules/dom-serializer": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-1.4.1.tgz", - "integrity": "sha512-VHwB3KfrcOOkelEG2ZOfxqLZdfkil8PtJi4P8N2MMXucZq2yLp75ClViUlOVwyoHEDjYU433Aq+5zWP61+RGag==", - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.2.0", - "entities": "^2.0.0" - }, - "funding": { - "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/domhandler": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-4.3.1.tgz", - "integrity": "sha512-GrwoxYN+uWlzO8uhUXRl0P+kHE4GtVPfYzVLcUxPL7KNdHKj66vvlhiweIHqYYXWlw+T8iLMp42Lm67ghw4WMQ==", - "license": "BSD-2-Clause", - "dependencies": { - "domelementtype": "^2.2.0" - }, - "engines": { - "node": ">= 4" - }, - "funding": { - "url": "https://github.com/fb55/domhandler?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/domutils": { - "version": "2.8.0", - "resolved": "https://registry.npmjs.org/domutils/-/domutils-2.8.0.tgz", - "integrity": "sha512-w96Cjofp72M5IIhpjgobBimYEfoPjx1Vx0BSX9P30WBdZW2WIKU0T1Bd0kz2eNZ9ikjKgHbEyKx8BB6H1L3h3A==", - "license": "BSD-2-Clause", - "dependencies": { - "dom-serializer": "^1.0.1", - "domelementtype": "^2.2.0", - "domhandler": "^4.2.0" - }, - "funding": { - "url": "https://github.com/fb55/domutils?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/entities": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/entities/-/entities-2.2.0.tgz", - "integrity": "sha512-p92if5Nz619I0w+akJrLZH0MX0Pb5DX39XOwQTtXSdQQOaYH03S1uIQp4mhOZtAXrxq4ViO67YTiLBo2638o9A==", - "license": "BSD-2-Clause", - "funding": { - "url": "https://github.com/fb55/entities?sponsor=1" - } - }, - "node_modules/renderkid/node_modules/htmlparser2": { - "version": "6.1.0", - "resolved": "https://registry.npmjs.org/htmlparser2/-/htmlparser2-6.1.0.tgz", - "integrity": "sha512-gyyPk6rgonLFEDGoeRgQNaEUvdJ4ktTmmUh/h2t7s+M8oPpIPxgNACWa+6ESR57kXstwqPiCut0V8NRpcwgU7A==", - "funding": [ - "https://github.com/fb55/htmlparser2?sponsor=1", - { - "type": "github", - "url": "https://github.com/sponsors/fb55" - } - ], - "license": "MIT", - "dependencies": { - "domelementtype": "^2.0.1", - "domhandler": "^4.0.0", - "domutils": "^2.5.2", - "entities": "^2.0.0" - } - }, - "node_modules/repeat-string": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/repeat-string/-/repeat-string-1.6.1.tgz", - "integrity": "sha512-PV0dzCYDNfRi1jCDbJzpW7jNNDRuCOG/jI5ctQcGKt/clZD+YcPS3yIlWuTJMmESC8aevCFmWJy5wjAFgNqN6w==", - "license": "MIT", - "engines": { - "node": ">=0.10" - } - }, - "node_modules/require-from-string": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", - "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/require-like": { - "version": "0.1.2", - "resolved": "https://registry.npmjs.org/require-like/-/require-like-0.1.2.tgz", - "integrity": "sha512-oyrU88skkMtDdauHDuKVrgR+zuItqr6/c//FXzvmxRGMexSDc6hNvJInGW3LL46n+8b50RykrvwSUIIQH2LQ5A==", - "engines": { - "node": "*" - } - }, - "node_modules/requires-port": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", - "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", - "license": "MIT" - }, - "node_modules/resolve": { - "version": "1.22.10", - "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.10.tgz", - "integrity": "sha512-NPRy+/ncIMeDlTAsuqwKIiferiawhefFJtkNSW0qZJEqMEb+qBt/77B/jGeeek+F0uOeN05CDa6HXbbIgtVX4w==", - "license": "MIT", - "dependencies": { - "is-core-module": "^2.16.0", - "path-parse": "^1.0.7", - "supports-preserve-symlinks-flag": "^1.0.0" - }, - "bin": { - "resolve": "bin/resolve" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/resolve-alpn": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/resolve-alpn/-/resolve-alpn-1.2.1.tgz", - "integrity": "sha512-0a1F4l73/ZFZOakJnQ3FvkJ2+gSTQWz/r2KE5OdDY0TxPm5h4GkqkWWfM47T7HsbnOtcJVEF4epCVy6u7Q3K+g==", - "license": "MIT" - }, - "node_modules/resolve-from": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-4.0.0.tgz", - "integrity": "sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/resolve-pathname": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/resolve-pathname/-/resolve-pathname-3.0.0.tgz", - "integrity": "sha512-C7rARubxI8bXFNB/hqcp/4iUeIXJhJZvFPFPiSPRnhU5UPxzMFIl+2E6yY6c4k9giDJAhtV+enfA+G89N6Csng==", - "license": "MIT" - }, - "node_modules/responselike": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/responselike/-/responselike-3.0.0.tgz", - "integrity": "sha512-40yHxbNcl2+rzXvZuVkrYohathsSJlMTXKryG5y8uciHv1+xDLHQpgjG64JUO9nrEq2jGLH6IZ8BcZyw3wrweg==", - "license": "MIT", - "dependencies": { - "lowercase-keys": "^3.0.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/retry": { - "version": "0.13.1", - "resolved": "https://registry.npmjs.org/retry/-/retry-0.13.1.tgz", - "integrity": "sha512-XQBQ3I8W1Cge0Seh+6gjj03LbmRFWuoszgK9ooCpwYIrhhoO80pfq4cUkU5DkknwfOfFteRwlZ56PYOGYyFWdg==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/reusify": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.1.0.tgz", - "integrity": "sha512-g6QUff04oZpHs0eG5p83rFLhHeV00ug/Yf9nZM6fLeUrPguBTkTQOdpAWWspMh55TZfVQDPaN3NQJfbVRAxdIw==", - "license": "MIT", - "engines": { - "iojs": ">=1.0.0", - "node": ">=0.10.0" - } - }, - "node_modules/rimraf": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", - "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", - "deprecated": "Rimraf versions prior to v4 are no longer supported", - "license": "ISC", - "dependencies": { - "glob": "^7.1.3" - }, - "bin": { - "rimraf": "bin.js" - }, - "funding": { - "url": "https://github.com/sponsors/isaacs" - } - }, - "node_modules/rtlcss": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/rtlcss/-/rtlcss-4.3.0.tgz", - "integrity": "sha512-FI+pHEn7Wc4NqKXMXFM+VAYKEj/mRIcW4h24YVwVtyjI+EqGrLc2Hx/Ny0lrZ21cBWU2goLy36eqMcNj3AQJig==", - "license": "MIT", - "dependencies": { - "escalade": "^3.1.1", - "picocolors": "^1.0.0", - "postcss": "^8.4.21", - "strip-json-comments": "^3.1.1" - }, - "bin": { - "rtlcss": "bin/rtlcss.js" - }, - "engines": { - "node": ">=12.0.0" - } - }, - "node_modules/run-parallel": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/run-parallel/-/run-parallel-1.2.0.tgz", - "integrity": "sha512-5l4VyZR86LZ/lDxZTR6jqL8AFE2S0IFLMP26AbjsLVADxHdhB/c0GUsH+y39UfCi3dzz8OlQuPmnaJOMoDHQBA==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT", - "dependencies": { - "queue-microtask": "^1.2.2" - } - }, - "node_modules/safe-buffer": { - "version": "5.2.1", - "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", - "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "funding": [ - { - "type": "github", - "url": "https://github.com/sponsors/feross" - }, - { - "type": "patreon", - "url": "https://www.patreon.com/feross" - }, - { - "type": "consulting", - "url": "https://feross.org/support" - } - ], - "license": "MIT" - }, - "node_modules/safer-buffer": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", - "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "license": "MIT" - }, - "node_modules/sax": { - "version": "1.4.1", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.4.1.tgz", - "integrity": "sha512-+aWOz7yVScEGoKNd4PA10LZ8sk0A/z5+nXQG5giUO5rprX9jgYsTdov9qCchZiPIZezbZH+jRut8nPodFAX4Jg==", - "license": "ISC" - }, - "node_modules/scheduler": { - "version": "0.26.0", - "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.26.0.tgz", - "integrity": "sha512-NlHwttCI/l5gCPR3D1nNXtWABUmBwvZpEQiD4IXSbIDq8BzLIK/7Ir5gTFSGZDUu37K5cMNp0hFtzO38sC7gWA==", - "license": "MIT" - }, - "node_modules/schema-dts": { - "version": "1.1.5", - "resolved": "https://registry.npmjs.org/schema-dts/-/schema-dts-1.1.5.tgz", - "integrity": "sha512-RJr9EaCmsLzBX2NDiO5Z3ux2BVosNZN5jo0gWgsyKvxKIUL5R3swNvoorulAeL9kLB0iTSX7V6aokhla2m7xbg==", - "license": "Apache-2.0" - }, - "node_modules/schema-utils": { - "version": "4.3.2", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz", - "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.9", - "ajv": "^8.9.0", - "ajv-formats": "^2.1.1", - "ajv-keywords": "^5.1.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/search-insights": { - "version": "2.17.3", - "resolved": "https://registry.npmjs.org/search-insights/-/search-insights-2.17.3.tgz", - "integrity": "sha512-RQPdCYTa8A68uM2jwxoY842xDhvx3E5LFL1LxvxCNMev4o5mLuokczhzjAgGwUZBAmOKZknArSxLKmXtIi2AxQ==", - "license": "MIT", - "peer": true - }, - "node_modules/section-matter": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/section-matter/-/section-matter-1.0.0.tgz", - "integrity": "sha512-vfD3pmTzGpufjScBh50YHKzEu2lxBWhVEHsNGoEXmCmn2hKGfeNLYMzCJpe8cD7gqX7TJluOVpBkAequ6dgMmA==", - "license": "MIT", - "dependencies": { - "extend-shallow": "^2.0.1", - "kind-of": "^6.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/select-hose": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/select-hose/-/select-hose-2.0.0.tgz", - "integrity": "sha512-mEugaLK+YfkijB4fx0e6kImuJdCIt2LxCRcbEYPqRGCs4F2ogyfZU5IAZRdjCP8JPq2AtdNoC/Dux63d9Kiryg==", - "license": "MIT" - }, - "node_modules/selfsigned": { - "version": "2.4.1", - "resolved": "https://registry.npmjs.org/selfsigned/-/selfsigned-2.4.1.tgz", - "integrity": "sha512-th5B4L2U+eGLq1TVh7zNRGBapioSORUeymIydxgFpwww9d2qyKvtuPU2jJuHvYAwwqi2Y596QBL3eEqcPEYL8Q==", - "license": "MIT", - "dependencies": { - "@types/node-forge": "^1.3.0", - "node-forge": "^1" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver": { - "version": "7.7.2", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", - "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", - "license": "ISC", - "bin": { - "semver": "bin/semver.js" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/semver-diff": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/semver-diff/-/semver-diff-4.0.0.tgz", - "integrity": "sha512-0Ju4+6A8iOnpL/Thra7dZsSlOHYAHIeMxfhWQRI1/VLcT3WDBZKKtQt/QkBOsiIN9ZpuvHE6cGZ0x4glCMmfiA==", - "license": "MIT", - "dependencies": { - "semver": "^7.3.5" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/send": { - "version": "0.19.0", - "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", - "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "license": "MIT", - "dependencies": { - "debug": "2.6.9", - "depd": "2.0.0", - "destroy": "1.2.0", - "encodeurl": "~1.0.2", - "escape-html": "~1.0.3", - "etag": "~1.8.1", - "fresh": "0.5.2", - "http-errors": "2.0.0", - "mime": "1.6.0", - "ms": "2.1.3", - "on-finished": "2.4.1", - "range-parser": "~1.2.1", - "statuses": "2.0.1" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/send/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/send/node_modules/debug/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/send/node_modules/encodeurl": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", - "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/send/node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serialize-javascript": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.2.tgz", - "integrity": "sha512-Saa1xPByTTq2gdeFZYLLo+RFE35NHZkAbqZeWNd3BpzppeVisAqpDjcp8dyf6uIvEqJRd46jemmyA4iFIeVk8g==", - "license": "BSD-3-Clause", - "dependencies": { - "randombytes": "^2.1.0" - } - }, - "node_modules/serve-handler": { - "version": "6.1.6", - "resolved": "https://registry.npmjs.org/serve-handler/-/serve-handler-6.1.6.tgz", - "integrity": "sha512-x5RL9Y2p5+Sh3D38Fh9i/iQ5ZK+e4xuXRd/pGbM4D13tgo/MGwbttUk8emytcr1YYzBYs+apnUngBDFYfpjPuQ==", - "license": "MIT", - "dependencies": { - "bytes": "3.0.0", - "content-disposition": "0.5.2", - "mime-types": "2.1.18", - "minimatch": "3.1.2", - "path-is-inside": "1.0.2", - "path-to-regexp": "3.3.0", - "range-parser": "1.2.0" - } - }, - "node_modules/serve-handler/node_modules/path-to-regexp": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-3.3.0.tgz", - "integrity": "sha512-qyCH421YQPS2WFDxDjftfc1ZR5WKQzVzqsp4n9M2kQhVOo/ByahFoUNJfl58kOcEGfQ//7weFTDhm+ss8Ecxgw==", - "license": "MIT" - }, - "node_modules/serve-index": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz", - "integrity": "sha512-pXHfKNP4qujrtteMrSBb0rc8HJ9Ms/GrXwcUtUtD5s4ewDJI8bT3Cz2zTVRMKtri49pLx2e0Ya8ziP5Ya2pZZw==", - "license": "MIT", - "dependencies": { - "accepts": "~1.3.4", - "batch": "0.6.1", - "debug": "2.6.9", - "escape-html": "~1.0.3", - "http-errors": "~1.6.2", - "mime-types": "~2.1.17", - "parseurl": "~1.3.2" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/serve-index/node_modules/debug": { - "version": "2.6.9", - "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", - "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "license": "MIT", - "dependencies": { - "ms": "2.0.0" - } - }, - "node_modules/serve-index/node_modules/depd": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/depd/-/depd-1.1.2.tgz", - "integrity": "sha512-7emPTl6Dpo6JRXOXjLRxck+FlLRX5847cLKEn00PLAgc3g2hTZZgr+e4c2v6QpSmLeFP3n5yUo7ft6avBK/5jQ==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/http-errors": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-1.6.3.tgz", - "integrity": "sha512-lks+lVC8dgGyh97jxvxeYTWQFvh4uw4yC12gVl63Cg30sjPX4wuGcdkICVXDAESr6OJGjqGA8Iz5mkeN6zlD7A==", - "license": "MIT", - "dependencies": { - "depd": "~1.1.2", - "inherits": "2.0.3", - "setprototypeof": "1.1.0", - "statuses": ">= 1.4.0 < 2" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-index/node_modules/inherits": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", - "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==", - "license": "ISC" - }, - "node_modules/serve-index/node_modules/ms": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", - "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "license": "MIT" - }, - "node_modules/serve-index/node_modules/setprototypeof": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.1.0.tgz", - "integrity": "sha512-BvE/TwpZX4FXExxOxZyRGQQv651MSwmWKZGqvmPcRIjDqWub67kTKuIMx43cZZrS/cBBzwBcNDWoFxt2XEFIpQ==", - "license": "ISC" - }, - "node_modules/serve-index/node_modules/statuses": { - "version": "1.5.0", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", - "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/serve-static": { - "version": "1.16.2", - "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", - "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "license": "MIT", - "dependencies": { - "encodeurl": "~2.0.0", - "escape-html": "~1.0.3", - "parseurl": "~1.3.3", - "send": "0.19.0" - }, - "engines": { - "node": ">= 0.8.0" - } - }, - "node_modules/set-function-length": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", - "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", - "license": "MIT", - "dependencies": { - "define-data-property": "^1.1.4", - "es-errors": "^1.3.0", - "function-bind": "^1.1.2", - "get-intrinsic": "^1.2.4", - "gopd": "^1.0.1", - "has-property-descriptors": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - } - }, - "node_modules/setprototypeof": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", - "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "license": "ISC" - }, - "node_modules/shallow-clone": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", - "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", - "license": "MIT", - "dependencies": { - "kind-of": "^6.0.2" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shallowequal": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/shallowequal/-/shallowequal-1.1.0.tgz", - "integrity": "sha512-y0m1JoUZSlPAjXVtPPW70aZWfIL/dSP7AFkRnniLCrK/8MDKog3TySTBmckD+RObVxH0v4Tox67+F14PdED2oQ==", - "license": "MIT" - }, - "node_modules/shebang-command": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", - "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", - "license": "MIT", - "dependencies": { - "shebang-regex": "^3.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/shebang-regex": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", - "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/shell-quote": { - "version": "1.8.3", - "resolved": "https://registry.npmjs.org/shell-quote/-/shell-quote-1.8.3.tgz", - "integrity": "sha512-ObmnIF4hXNg1BqhnHmgbDETF8dLPCggZWBjkQfhZpbszZnYur5DUljTcCHii5LC3J5E0yeO/1LIMyH+UvHQgyw==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", - "side-channel-map": "^1.0.1", - "side-channel-weakmap": "^1.0.2" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-list": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", - "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "license": "MIT", - "dependencies": { - "es-errors": "^1.3.0", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-map": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", - "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/side-channel-weakmap": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", - "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "license": "MIT", - "dependencies": { - "call-bound": "^1.0.2", - "es-errors": "^1.3.0", - "get-intrinsic": "^1.2.5", - "object-inspect": "^1.13.3", - "side-channel-map": "^1.0.1" - }, - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/signal-exit": { - "version": "3.0.7", - "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", - "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", - "license": "ISC" - }, - "node_modules/sirv": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/sirv/-/sirv-2.0.4.tgz", - "integrity": "sha512-94Bdh3cC2PKrbgSOUqTiGPWVZeSiXfKOVZNJniWoqrWrRkB1CJzBU3NEbiTsPcYy1lDsANA/THzS+9WBiy5nfQ==", - "license": "MIT", - "dependencies": { - "@polka/url": "^1.0.0-next.24", - "mrmime": "^2.0.0", - "totalist": "^3.0.0" - }, - "engines": { - "node": ">= 10" - } - }, - "node_modules/sisteransi": { - "version": "1.0.5", - "resolved": "https://registry.npmjs.org/sisteransi/-/sisteransi-1.0.5.tgz", - "integrity": "sha512-bLGGlR1QxBcynn2d5YmDX4MGjlZvy2MRBDRNHLJ8VI6l6+9FUiyTFNJ0IveOSP0bcXgVDPRcfGqA0pjaqUpfVg==", - "license": "MIT" - }, - "node_modules/sitemap": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/sitemap/-/sitemap-7.1.2.tgz", - "integrity": "sha512-ARCqzHJ0p4gWt+j7NlU5eDlIO9+Rkr/JhPFZKKQ1l5GCus7rJH4UdrlVAh0xC/gDS/Qir2UMxqYNHtsKr2rpCw==", - "license": "MIT", - "dependencies": { - "@types/node": "^17.0.5", - "@types/sax": "^1.2.1", - "arg": "^5.0.0", - "sax": "^1.2.4" - }, - "bin": { - "sitemap": "dist/cli.js" - }, - "engines": { - "node": ">=12.0.0", - "npm": ">=5.6.0" - } - }, - "node_modules/sitemap/node_modules/@types/node": { - "version": "17.0.45", - "resolved": "https://registry.npmjs.org/@types/node/-/node-17.0.45.tgz", - "integrity": "sha512-w+tIMs3rq2afQdsPJlODhoUEKzFP1ayaoyl1CcnwtIlsVe7K7bA1NGm4s3PraqTLlXnbIN84zuBlxBWo1u9BLw==", - "license": "MIT" - }, - "node_modules/skin-tone": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/skin-tone/-/skin-tone-2.0.0.tgz", - "integrity": "sha512-kUMbT1oBJCpgrnKoSr0o6wPtvRWT9W9UKvGLwfJYO2WuahZRHOpEyL1ckyMGgMWh0UdpmaoFqKKD29WTomNEGA==", - "license": "MIT", - "dependencies": { - "unicode-emoji-modifier-base": "^1.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/slash": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/slash/-/slash-3.0.0.tgz", - "integrity": "sha512-g9Q1haeby36OSStwb4ntCGGGaKsaVSjQ68fBxoQcutl5fS1vuY18H3wSt3jFyFtrkx+Kz0V1G85A4MyAdDMi2Q==", - "license": "MIT", - "engines": { - "node": ">=8" - } - }, - "node_modules/snake-case": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", - "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", - "license": "MIT", - "dependencies": { - "dot-case": "^3.0.4", - "tslib": "^2.0.3" - } - }, - "node_modules/sockjs": { - "version": "0.3.24", - "resolved": "https://registry.npmjs.org/sockjs/-/sockjs-0.3.24.tgz", - "integrity": "sha512-GJgLTZ7vYb/JtPSSZ10hsOYIvEYsjbNU+zPdIHcUaWVNUEPivzxku31865sSSud0Da0W4lEeOPlmw93zLQchuQ==", - "license": "MIT", - "dependencies": { - "faye-websocket": "^0.11.3", - "uuid": "^8.3.2", - "websocket-driver": "^0.7.4" - } - }, - "node_modules/sort-css-media-queries": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/sort-css-media-queries/-/sort-css-media-queries-2.2.0.tgz", - "integrity": "sha512-0xtkGhWCC9MGt/EzgnvbbbKhqWjl1+/rncmhTh5qCpbYguXh6S/qwePfv/JQ8jePXXmqingylxoC49pCkSPIbA==", - "license": "MIT", - "engines": { - "node": ">= 6.3.0" - } - }, - "node_modules/source-map": { - "version": "0.7.6", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", - "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", - "license": "BSD-3-Clause", - "engines": { - "node": ">= 12" - } - }, - "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/source-map-support": { - "version": "0.5.21", - "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", - "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", - "license": "MIT", - "dependencies": { - "buffer-from": "^1.0.0", - "source-map": "^0.6.0" - } - }, - "node_modules/source-map-support/node_modules/source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "license": "BSD-3-Clause", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/space-separated-tokens": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/space-separated-tokens/-/space-separated-tokens-2.0.2.tgz", - "integrity": "sha512-PEGlAwrG8yXGXRjW32fGbg66JAlOAwbObuqVoJpv/mRgoWDQfgH1wDPvtzWyUSNAXBGSk8h755YDbbcEy3SH2Q==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/spdy": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/spdy/-/spdy-4.0.2.tgz", - "integrity": "sha512-r46gZQZQV+Kl9oItvl1JZZqJKGr+oEkB08A6BzkiR7593/7IbtuncXHd2YoYeTsG4157ZssMu9KYvUHLcjcDoA==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "handle-thing": "^2.0.0", - "http-deceiver": "^1.2.7", - "select-hose": "^2.0.0", - "spdy-transport": "^3.0.0" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/spdy-transport": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/spdy-transport/-/spdy-transport-3.0.0.tgz", - "integrity": "sha512-hsLVFE5SjA6TCisWeJXFKniGGOpBgMLmerfO2aCyCU5s7nJ/rpAepqmFifv/GCbSbueEeAJJnmSQ2rKC/g8Fcw==", - "license": "MIT", - "dependencies": { - "debug": "^4.1.0", - "detect-node": "^2.0.4", - "hpack.js": "^2.1.6", - "obuf": "^1.1.2", - "readable-stream": "^3.0.6", - "wbuf": "^1.7.3" - } - }, - "node_modules/sprintf-js": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/sprintf-js/-/sprintf-js-1.0.3.tgz", - "integrity": "sha512-D9cPgkvLlV3t3IzL0D0YLvGA9Ahk4PcvVwUbN0dSGr1aP0Nrt4AEnTUbuGvquEC0mA64Gqt1fzirlRs5ibXx8g==", - "license": "BSD-3-Clause" - }, - "node_modules/srcset": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/srcset/-/srcset-4.0.0.tgz", - "integrity": "sha512-wvLeHgcVHKO8Sc/H/5lkGreJQVeYMm9rlmt8PuR1xE31rIuXhuzznUUqAt8MqLhB3MqJdFzlNAfpcWnxiFUcPw==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/statuses": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", - "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/std-env": { - "version": "3.9.0", - "resolved": "https://registry.npmjs.org/std-env/-/std-env-3.9.0.tgz", - "integrity": "sha512-UGvjygr6F6tpH7o2qyqR6QYpwraIjKSdtzyBdyytFOHmPZY917kwdwLG0RbOjWOnKmnm3PeHjaoLLMie7kPLQw==", - "license": "MIT" - }, - "node_modules/string_decoder": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", - "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", - "license": "MIT", - "dependencies": { - "safe-buffer": "~5.2.0" - } - }, - "node_modules/string-width": { - "version": "5.1.2", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-5.1.2.tgz", - "integrity": "sha512-HnLOCR3vjcY8beoNLtcjZ5/nxn2afmME6lhrDrebokqMap+XbeW8n9TXpPDOqdGK5qcI3oT0GKTW6wC7EMiVqA==", - "license": "MIT", - "dependencies": { - "eastasianwidth": "^0.2.0", - "emoji-regex": "^9.2.2", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/string-width/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/string-width/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/stringify-entities": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/stringify-entities/-/stringify-entities-4.0.4.tgz", - "integrity": "sha512-IwfBptatlO+QCJUo19AqvrPNqlVMpW9YEL2LIVY+Rpv2qsjCGxaDLNRgeGsQWJhfItebuJhsGSLjaBbNSQ+ieg==", - "license": "MIT", - "dependencies": { - "character-entities-html4": "^2.0.0", - "character-entities-legacy": "^3.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/stringify-object": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/stringify-object/-/stringify-object-3.3.0.tgz", - "integrity": "sha512-rHqiFh1elqCQ9WPLIC8I0Q/g/wj5J1eMkyoiD6eoQApWHP0FtlK7rqnhmabL5VUY9JQCcqwwvlOaSuutekgyrw==", - "license": "BSD-2-Clause", - "dependencies": { - "get-own-enumerable-property-symbols": "^3.0.0", - "is-obj": "^1.0.1", - "is-regexp": "^1.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/strip-ansi": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", - "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^5.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/strip-bom-string": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/strip-bom-string/-/strip-bom-string-1.0.0.tgz", - "integrity": "sha512-uCC2VHvQRYu+lMh4My/sFNmF2klFymLX1wHJeXnbEJERpV/ZsVuonzerjfrGpIGF7LBVa1O7i9kjiWvJiFck8g==", - "license": "MIT", - "engines": { - "node": ">=0.10.0" - } - }, - "node_modules/strip-final-newline": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", - "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/strip-json-comments": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", - "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", - "license": "MIT", - "engines": { - "node": ">=8" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/style-to-js": { - "version": "1.1.17", - "resolved": "https://registry.npmjs.org/style-to-js/-/style-to-js-1.1.17.tgz", - "integrity": "sha512-xQcBGDxJb6jjFCTzvQtfiPn6YvvP2O8U1MDIPNfJQlWMYfktPy+iGsHE7cssjs7y84d9fQaK4UF3RIJaAHSoYA==", - "license": "MIT", - "dependencies": { - "style-to-object": "1.0.9" - } - }, - "node_modules/style-to-object": { - "version": "1.0.9", - "resolved": "https://registry.npmjs.org/style-to-object/-/style-to-object-1.0.9.tgz", - "integrity": "sha512-G4qppLgKu/k6FwRpHiGiKPaPTFcG3g4wNVX/Qsfu+RqQM30E7Tyu/TEgxcL9PNLF5pdRLwQdE3YKKf+KF2Dzlw==", - "license": "MIT", - "dependencies": { - "inline-style-parser": "0.2.4" - } - }, - "node_modules/stylehacks": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/stylehacks/-/stylehacks-6.1.1.tgz", - "integrity": "sha512-gSTTEQ670cJNoaeIp9KX6lZmm8LJ3jPB5yJmX8Zq/wQxOsAFXV3qjWzHas3YYk1qesuVIyYWWUpZ0vSE/dTSGg==", - "license": "MIT", - "dependencies": { - "browserslist": "^4.23.0", - "postcss-selector-parser": "^6.0.16" - }, - "engines": { - "node": "^14 || ^16 || >=18.0" - }, - "peerDependencies": { - "postcss": "^8.4.31" - } - }, - "node_modules/supports-color": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", - "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/supports-preserve-symlinks-flag": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", - "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", - "license": "MIT", - "engines": { - "node": ">= 0.4" - }, - "funding": { - "url": "https://github.com/sponsors/ljharb" - } - }, - "node_modules/svg-parser": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", - "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", - "license": "MIT" - }, - "node_modules/svgo": { - "version": "3.3.2", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", - "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", - "license": "MIT", - "dependencies": { - "@trysound/sax": "0.2.0", - "commander": "^7.2.0", - "css-select": "^5.1.0", - "css-tree": "^2.3.1", - "css-what": "^6.1.0", - "csso": "^5.0.5", - "picocolors": "^1.0.0" - }, - "bin": { - "svgo": "bin/svgo" - }, - "engines": { - "node": ">=14.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/svgo" - } - }, - "node_modules/svgo/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/tapable": { - "version": "2.2.3", - "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.2.3.tgz", - "integrity": "sha512-ZL6DDuAlRlLGghwcfmSn9sK3Hr6ArtyudlSAiCqQ6IfE+b+HHbydbYDIG15IfS5do+7XQQBdBiubF/cV2dnDzg==", - "license": "MIT", - "engines": { - "node": ">=6" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/terser": { - "version": "5.44.0", - "resolved": "https://registry.npmjs.org/terser/-/terser-5.44.0.tgz", - "integrity": "sha512-nIVck8DK+GM/0Frwd+nIhZ84pR/BX7rmXMfYwyg+Sri5oGVE99/E3KvXqpC2xHFxyqXyGHTKBSioxxplrO4I4w==", - "license": "BSD-2-Clause", - "dependencies": { - "@jridgewell/source-map": "^0.3.3", - "acorn": "^8.15.0", - "commander": "^2.20.0", - "source-map-support": "~0.5.20" - }, - "bin": { - "terser": "bin/terser" - }, - "engines": { - "node": ">=10" - } - }, - "node_modules/terser-webpack-plugin": { - "version": "5.3.14", - "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.14.tgz", - "integrity": "sha512-vkZjpUjb6OMS7dhV+tILUW6BhpDR7P2L/aQSAv+Uwk+m8KATX9EccViHTJR2qDtACKPIYndLGCyl3FMo+r2LMw==", - "license": "MIT", - "dependencies": { - "@jridgewell/trace-mapping": "^0.3.25", - "jest-worker": "^27.4.5", - "schema-utils": "^4.3.0", - "serialize-javascript": "^6.0.2", - "terser": "^5.31.1" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^5.1.0" - }, - "peerDependenciesMeta": { - "@swc/core": { - "optional": true - }, - "esbuild": { - "optional": true - }, - "uglify-js": { - "optional": true - } - } - }, - "node_modules/terser-webpack-plugin/node_modules/jest-worker": { - "version": "27.5.1", - "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", - "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", - "license": "MIT", - "dependencies": { - "@types/node": "*", - "merge-stream": "^2.0.0", - "supports-color": "^8.0.0" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/terser-webpack-plugin/node_modules/supports-color": { - "version": "8.1.1", - "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", - "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", - "license": "MIT", - "dependencies": { - "has-flag": "^4.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/supports-color?sponsor=1" - } - }, - "node_modules/terser/node_modules/commander": { - "version": "2.20.3", - "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", - "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", - "license": "MIT" - }, - "node_modules/thunky": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/thunky/-/thunky-1.1.0.tgz", - "integrity": "sha512-eHY7nBftgThBqOyHGVN+l8gF0BucP09fMo0oO/Lb0w1OF80dJv+lDVpXG60WMQvkcxAkNybKsrEIE3ZtKGmPrA==", - "license": "MIT" - }, - "node_modules/tiny-invariant": { - "version": "1.3.3", - "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", - "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==", - "license": "MIT" - }, - "node_modules/tiny-warning": { - "version": "1.0.3", - "resolved": "https://registry.npmjs.org/tiny-warning/-/tiny-warning-1.0.3.tgz", - "integrity": "sha512-lBN9zLN/oAf68o3zNXYrdCt1kP8WsiGW8Oo2ka41b2IM5JL/S1CTyX1rW0mb/zSuJun0ZUrDxx4sqvYS2FWzPA==", - "license": "MIT" - }, - "node_modules/tinypool": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/tinypool/-/tinypool-1.1.1.tgz", - "integrity": "sha512-Zba82s87IFq9A9XmjiX5uZA/ARWDrB03OHlq+Vw1fSdt0I+4/Kutwy8BP4Y/y/aORMo61FQ0vIb5j44vSo5Pkg==", - "license": "MIT", - "engines": { - "node": "^18.0.0 || >=20.0.0" - } - }, - "node_modules/to-regex-range": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", - "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", - "license": "MIT", - "dependencies": { - "is-number": "^7.0.0" - }, - "engines": { - "node": ">=8.0" - } - }, - "node_modules/toidentifier": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", - "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "license": "MIT", - "engines": { - "node": ">=0.6" - } - }, - "node_modules/totalist": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/totalist/-/totalist-3.0.1.tgz", - "integrity": "sha512-sf4i37nQ2LBx4m3wB74y+ubopq6W/dIzXg0FDGjsYnZHVa1Da8FH853wlL2gtUhg+xJXjfk3kUZS3BRoQeoQBQ==", - "license": "MIT", - "engines": { - "node": ">=6" - } - }, - "node_modules/trim-lines": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/trim-lines/-/trim-lines-3.0.1.tgz", - "integrity": "sha512-kRj8B+YHZCc9kQYdWfJB2/oUl9rA99qbowYYBtr4ui4mZyAQ2JpvVBd/6U2YloATfqBhBTSMhTpgBHtU0Mf3Rg==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/trough": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/trough/-/trough-2.2.0.tgz", - "integrity": "sha512-tmMpK00BjZiUyVyvrBK7knerNgmgvcV/KLVyuma/SC+TQN167GrMRciANTz09+k3zW8L8t60jWO1GpfkZdjTaw==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/tslib": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", - "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==", - "license": "0BSD" - }, - "node_modules/type-fest": { - "version": "2.19.0", - "resolved": "https://registry.npmjs.org/type-fest/-/type-fest-2.19.0.tgz", - "integrity": "sha512-RAH822pAdBgcNMAfWnCBU3CFZcfZ/i1eZjwFU/dsLKumyuuP3niueg2UAukXYF0E2AAoc82ZSSf9J0WQBinzHA==", - "license": "(MIT OR CC0-1.0)", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/type-is": { - "version": "1.6.18", - "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", - "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "license": "MIT", - "dependencies": { - "media-typer": "0.3.0", - "mime-types": "~2.1.24" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/type-is/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/typedarray-to-buffer": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/typedarray-to-buffer/-/typedarray-to-buffer-3.1.5.tgz", - "integrity": "sha512-zdu8XMNEDepKKR+XYOXAVPtWui0ly0NtohUscw+UmaHiAWT8hrV1rr//H6V+0DvJ3OQ19S979M0laLfX8rm82Q==", - "license": "MIT", - "dependencies": { - "is-typedarray": "^1.0.0" - } - }, - "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", - "license": "MIT" - }, - "node_modules/unicode-canonical-property-names-ecmascript": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.1.tgz", - "integrity": "sha512-dA8WbNeb2a6oQzAQ55YlT5vQAWGV9WXOsi3SskE3bcCdM0P4SDd+24zS/OCacdRq5BkdsRj9q3Pg6YyQoxIGqg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-emoji-modifier-base": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unicode-emoji-modifier-base/-/unicode-emoji-modifier-base-1.0.0.tgz", - "integrity": "sha512-yLSH4py7oFH3oG/9K+XWrz1pSi3dfUrWEnInbxMfArOfc1+33BlGPQtLsOYwvdMy11AwUBetYuaRxSPqgkq+8g==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-ecmascript": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", - "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", - "license": "MIT", - "dependencies": { - "unicode-canonical-property-names-ecmascript": "^2.0.0", - "unicode-property-aliases-ecmascript": "^2.0.0" - }, - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-match-property-value-ecmascript": { - "version": "2.2.1", - "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.2.1.tgz", - "integrity": "sha512-JQ84qTuMg4nVkx8ga4A16a1epI9H6uTXAknqxkGF/aFfRLw1xC/Bp24HNLaZhHSkWd3+84t8iXnp1J0kYcZHhg==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unicode-property-aliases-ecmascript": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.2.0.tgz", - "integrity": "sha512-hpbDzxUY9BFwX+UeBnxv3Sh1q7HFxj48DTmXchNgRa46lO8uj3/1iEn3MiNUYTg1g9ctIqXCCERn8gYZhHC5lQ==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, - "node_modules/unified": { - "version": "11.0.5", - "resolved": "https://registry.npmjs.org/unified/-/unified-11.0.5.tgz", - "integrity": "sha512-xKvGhPWw3k84Qjh8bI3ZeJjqnyadK+GEFtazSfZv/rKeTkTjOJho6mFqh2SM96iIcZokxiOpg78GazTSg8+KHA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "bail": "^2.0.0", - "devlop": "^1.0.0", - "extend": "^3.0.0", - "is-plain-obj": "^4.0.0", - "trough": "^2.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unique-string": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/unique-string/-/unique-string-3.0.0.tgz", - "integrity": "sha512-VGXBUVwxKMBUznyffQweQABPRRW1vHZAbadFZud4pLFAqRGvv/96vafgjWFqzourzr8YonlQiPgH0YCJfawoGQ==", - "license": "MIT", - "dependencies": { - "crypto-random-string": "^4.0.0" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/unist-util-is": { - "version": "6.0.0", - "resolved": "https://registry.npmjs.org/unist-util-is/-/unist-util-is-6.0.0.tgz", - "integrity": "sha512-2qCTHimwdxLfz+YzdGfkqNlH0tLi9xjTnHddPmJwtIG9MGsdbutfTc4P+haPD7l7Cjxf/WZj+we5qfVPvvxfYw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position/-/unist-util-position-5.0.0.tgz", - "integrity": "sha512-fucsC7HjXvkB5R3kTCO7kUjRdrS0BJt3M/FPxmHMBOm8JQi2BsHAHFsy27E0EolP8rp0NzXsJ+jNPyDWvOJZPA==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-position-from-estree": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/unist-util-position-from-estree/-/unist-util-position-from-estree-2.0.0.tgz", - "integrity": "sha512-KaFVRjoqLyF6YXCbVLNad/eS4+OfPQQn2yOd7zF/h5T/CSL2v8NpN6a5TPvtbXthAGw5nG+PuTtq+DdIZr+cRQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-stringify-position": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/unist-util-stringify-position/-/unist-util-stringify-position-4.0.0.tgz", - "integrity": "sha512-0ASV06AAoKCDkS2+xw5RXJywruurpbC4JZSm7nr7MOt1ojAzvyyaO+UxZf18j8FCF6kmzCZKcAgN/yu2gm2XgQ==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/unist-util-visit/-/unist-util-visit-5.0.0.tgz", - "integrity": "sha512-MR04uvD+07cwl/yhVuVWAtw+3GOR/knlL55Nd/wAdblk27GCVt3lqpTivy/tkJcZoNPzTwS1Y+KMojlLDhoTzg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0", - "unist-util-visit-parents": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/unist-util-visit-parents": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/unist-util-visit-parents/-/unist-util-visit-parents-6.0.1.tgz", - "integrity": "sha512-L/PqWzfTP9lzzEa6CKs0k2nARxTdZduw3zyh8d2NVBnsyvHjSX4TWse388YrrQKbvI8w20fGjGlhgT96WwKykw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-is": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/universalify": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/universalify/-/universalify-2.0.1.tgz", - "integrity": "sha512-gptHNQghINnc/vTGIk0SOFGFNXw7JVrlRUtConJRlvaw6DuX0wO5Jeko9sWrMBhh+PsYAZ7oXAiOnf/UKogyiw==", - "license": "MIT", - "engines": { - "node": ">= 10.0.0" - } - }, - "node_modules/unpipe": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", - "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/update-browserslist-db": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.1.3.tgz", - "integrity": "sha512-UxhIZQ+QInVdunkDAaiazvvT/+fXL5Osr0JZlJulepYu6Jd7qJtDZjlur0emRlT71EN3ScPoE7gvsuIKKNavKw==", - "funding": [ - { - "type": "opencollective", - "url": "https://opencollective.com/browserslist" - }, - { - "type": "tidelift", - "url": "https://tidelift.com/funding/github/npm/browserslist" - }, - { - "type": "github", - "url": "https://github.com/sponsors/ai" - } - ], - "license": "MIT", - "dependencies": { - "escalade": "^3.2.0", - "picocolors": "^1.1.1" - }, - "bin": { - "update-browserslist-db": "cli.js" - }, - "peerDependencies": { - "browserslist": ">= 4.21.0" - } - }, - "node_modules/update-notifier": { - "version": "6.0.2", - "resolved": "https://registry.npmjs.org/update-notifier/-/update-notifier-6.0.2.tgz", - "integrity": "sha512-EDxhTEVPZZRLWYcJ4ZXjGFN0oP7qYvbXWzEgRm/Yql4dHX5wDbvh89YHP6PK1lzZJYrMtXUuZZz8XGK+U6U1og==", - "license": "BSD-2-Clause", - "dependencies": { - "boxen": "^7.0.0", - "chalk": "^5.0.1", - "configstore": "^6.0.0", - "has-yarn": "^3.0.0", - "import-lazy": "^4.0.0", - "is-ci": "^3.0.1", - "is-installed-globally": "^0.4.0", - "is-npm": "^6.0.0", - "is-yarn-global": "^0.4.0", - "latest-version": "^7.0.0", - "pupa": "^3.1.0", - "semver": "^7.3.7", - "semver-diff": "^4.0.0", - "xdg-basedir": "^5.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/yeoman/update-notifier?sponsor=1" - } - }, - "node_modules/update-notifier/node_modules/boxen": { - "version": "7.1.1", - "resolved": "https://registry.npmjs.org/boxen/-/boxen-7.1.1.tgz", - "integrity": "sha512-2hCgjEmP8YLWQ130n2FerGv7rYpfBmnmp9Uy2Le1vge6X3gZIfSmEzP5QTDElFxcvVcXlEn8Aq6MU/PZygIOog==", - "license": "MIT", - "dependencies": { - "ansi-align": "^3.0.1", - "camelcase": "^7.0.1", - "chalk": "^5.2.0", - "cli-boxes": "^3.0.0", - "string-width": "^5.1.2", - "type-fest": "^2.13.0", - "widest-line": "^4.0.1", - "wrap-ansi": "^8.1.0" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/update-notifier/node_modules/camelcase": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-7.0.1.tgz", - "integrity": "sha512-xlx1yCK2Oc1APsPXDL2LdlNP6+uu8OCDdhOBSVT279M/S+y75O30C2VuD8T2ogdePBBl7PfPF4504tnLgX3zfw==", - "license": "MIT", - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/update-notifier/node_modules/chalk": { - "version": "5.6.2", - "resolved": "https://registry.npmjs.org/chalk/-/chalk-5.6.2.tgz", - "integrity": "sha512-7NzBL0rN6fMUW+f7A6Io4h40qQlG+xGmtMxfbnH/K7TAtt8JQWVQK+6g0UXKMeVJoyV5EkkNsErQ8pVD3bLHbA==", - "license": "MIT", - "engines": { - "node": "^12.17.0 || ^14.13 || >=16.0.0" - }, - "funding": { - "url": "https://github.com/chalk/chalk?sponsor=1" - } - }, - "node_modules/uri-js": { - "version": "4.4.1", - "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", - "integrity": "sha512-7rKUyy33Q1yc98pQ1DAmLtwX109F7TIfWlW1Ydo8Wl1ii1SeHieeh0HHfPeL2fMXK6z0s8ecKs9frCuLJvndBg==", - "license": "BSD-2-Clause", - "dependencies": { - "punycode": "^2.1.0" - } - }, - "node_modules/url-loader": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/url-loader/-/url-loader-4.1.1.tgz", - "integrity": "sha512-3BTV812+AVHHOJQO8O5MkWgZ5aosP7GnROJwvzLS9hWDj00lZ6Z0wNak423Lp9PBZN05N+Jk/N5Si8jRAlGyWA==", - "license": "MIT", - "dependencies": { - "loader-utils": "^2.0.0", - "mime-types": "^2.1.27", - "schema-utils": "^3.0.0" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "file-loader": "*", - "webpack": "^4.0.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "file-loader": { - "optional": true - } - } - }, - "node_modules/url-loader/node_modules/ajv": { - "version": "6.12.6", - "resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz", - "integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==", - "license": "MIT", - "dependencies": { - "fast-deep-equal": "^3.1.1", - "fast-json-stable-stringify": "^2.0.0", - "json-schema-traverse": "^0.4.1", - "uri-js": "^4.2.2" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/epoberezkin" - } - }, - "node_modules/url-loader/node_modules/ajv-keywords": { - "version": "3.5.2", - "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz", - "integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==", - "license": "MIT", - "peerDependencies": { - "ajv": "^6.9.1" - } - }, - "node_modules/url-loader/node_modules/json-schema-traverse": { - "version": "0.4.1", - "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", - "integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==", - "license": "MIT" - }, - "node_modules/url-loader/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/url-loader/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/url-loader/node_modules/schema-utils": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz", - "integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==", - "license": "MIT", - "dependencies": { - "@types/json-schema": "^7.0.8", - "ajv": "^6.12.5", - "ajv-keywords": "^3.5.2" - }, - "engines": { - "node": ">= 10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - } - }, - "node_modules/util-deprecate": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", - "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", - "license": "MIT" - }, - "node_modules/utila": { - "version": "0.4.0", - "resolved": "https://registry.npmjs.org/utila/-/utila-0.4.0.tgz", - "integrity": "sha512-Z0DbgELS9/L/75wZbro8xAnT50pBVFQZ+hUEueGDU5FN51YSCYM+jdxsfCiHjwNP/4LCDD0i/graKpeBnOXKRA==", - "license": "MIT" - }, - "node_modules/utility-types": { - "version": "3.11.0", - "resolved": "https://registry.npmjs.org/utility-types/-/utility-types-3.11.0.tgz", - "integrity": "sha512-6Z7Ma2aVEWisaL6TvBCy7P8rm2LQoPv6dJ7ecIaIixHcwfbJ0x7mWdbcwlIM5IGQxPZSFYeqRCqlOOeKoJYMkw==", - "license": "MIT", - "engines": { - "node": ">= 4" - } - }, - "node_modules/utils-merge": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", - "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "license": "MIT", - "engines": { - "node": ">= 0.4.0" - } - }, - "node_modules/uuid": { - "version": "8.3.2", - "resolved": "https://registry.npmjs.org/uuid/-/uuid-8.3.2.tgz", - "integrity": "sha512-+NYs2QeMWy+GWFOEm9xnn6HCDp0l7QBD7ml8zLUmJ+93Q5NF0NocErnwkTkXVFNiX3/fpC6afS8Dhb/gz7R7eg==", - "license": "MIT", - "bin": { - "uuid": "dist/bin/uuid" - } - }, - "node_modules/value-equal": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/value-equal/-/value-equal-1.0.1.tgz", - "integrity": "sha512-NOJ6JZCAWr0zlxZt+xqCHNTEKOsrks2HQd4MqhP1qy4z1SkbEP467eNx6TgDKXMvUOb+OENfJCZwM+16n7fRfw==", - "license": "MIT" - }, - "node_modules/vary": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", - "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "license": "MIT", - "engines": { - "node": ">= 0.8" - } - }, - "node_modules/vfile": { - "version": "6.0.3", - "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", - "integrity": "sha512-KzIbH/9tXat2u30jf+smMwFCsno4wHVdNmzFyL+T/L3UGqqk6JKfVqOFOZEpZSHADH1k40ab6NUIXZq422ov3Q==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile-message": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-location": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/vfile-location/-/vfile-location-5.0.3.tgz", - "integrity": "sha512-5yXvWDEgqeiYiBe1lbxYF7UMAIm/IcopxMHrMQDq3nvKcjPKIhZklUKL+AE7J7uApI4kwe2snsK+eI6UTj9EHg==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "vfile": "^6.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/vfile-message": { - "version": "4.0.3", - "resolved": "https://registry.npmjs.org/vfile-message/-/vfile-message-4.0.3.tgz", - "integrity": "sha512-QTHzsGd1EhbZs4AsQ20JX1rC3cOlt/IWJruk893DfLRr57lcnOeMaWG4K0JrRta4mIJZKth2Au3mM3u03/JWKw==", - "license": "MIT", - "dependencies": { - "@types/unist": "^3.0.0", - "unist-util-stringify-position": "^4.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, - "node_modules/watchpack": { - "version": "2.4.4", - "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.4.tgz", - "integrity": "sha512-c5EGNOiyxxV5qmTtAB7rbiXxi1ooX1pQKMLX/MIabJjRA0SJBQOjKF+KSVfHkr9U1cADPon0mRiVe/riyaiDUA==", - "license": "MIT", - "dependencies": { - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.1.2" - }, - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/wbuf": { - "version": "1.7.3", - "resolved": "https://registry.npmjs.org/wbuf/-/wbuf-1.7.3.tgz", - "integrity": "sha512-O84QOnr0icsbFGLS0O3bI5FswxzRr8/gHwWkDlQFskhSPryQXvrTMxjxGP4+iWYoauLoBvfDpkrOauZ+0iZpDA==", - "license": "MIT", - "dependencies": { - "minimalistic-assert": "^1.0.0" - } - }, - "node_modules/web-namespaces": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/web-namespaces/-/web-namespaces-2.0.1.tgz", - "integrity": "sha512-bKr1DkiNa2krS7qxNtdrtHAmzuYGFQLiQ13TsorsdT6ULTkPLKuu5+GsFpDlg6JFjUTwX2DyhMPG2be8uPrqsQ==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/webpack": { - "version": "5.101.3", - "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.101.3.tgz", - "integrity": "sha512-7b0dTKR3Ed//AD/6kkx/o7duS8H3f1a4w3BYpIriX4BzIhjkn4teo05cptsxvLesHFKK5KObnadmCHBwGc+51A==", - "license": "MIT", - "dependencies": { - "@types/eslint-scope": "^3.7.7", - "@types/estree": "^1.0.8", - "@types/json-schema": "^7.0.15", - "@webassemblyjs/ast": "^1.14.1", - "@webassemblyjs/wasm-edit": "^1.14.1", - "@webassemblyjs/wasm-parser": "^1.14.1", - "acorn": "^8.15.0", - "acorn-import-phases": "^1.0.3", - "browserslist": "^4.24.0", - "chrome-trace-event": "^1.0.2", - "enhanced-resolve": "^5.17.3", - "es-module-lexer": "^1.2.1", - "eslint-scope": "5.1.1", - "events": "^3.2.0", - "glob-to-regexp": "^0.4.1", - "graceful-fs": "^4.2.11", - "json-parse-even-better-errors": "^2.3.1", - "loader-runner": "^4.2.0", - "mime-types": "^2.1.27", - "neo-async": "^2.6.2", - "schema-utils": "^4.3.2", - "tapable": "^2.1.1", - "terser-webpack-plugin": "^5.3.11", - "watchpack": "^2.4.1", - "webpack-sources": "^3.3.3" - }, - "bin": { - "webpack": "bin/webpack.js" - }, - "engines": { - "node": ">=10.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependenciesMeta": { - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-bundle-analyzer": { - "version": "4.10.2", - "resolved": "https://registry.npmjs.org/webpack-bundle-analyzer/-/webpack-bundle-analyzer-4.10.2.tgz", - "integrity": "sha512-vJptkMm9pk5si4Bv922ZbKLV8UTT4zib4FPgXMhgzUny0bfDDkLXAVQs3ly3fS4/TN9ROFtb0NFrm04UXFE/Vw==", - "license": "MIT", - "dependencies": { - "@discoveryjs/json-ext": "0.5.7", - "acorn": "^8.0.4", - "acorn-walk": "^8.0.0", - "commander": "^7.2.0", - "debounce": "^1.2.1", - "escape-string-regexp": "^4.0.0", - "gzip-size": "^6.0.0", - "html-escaper": "^2.0.2", - "opener": "^1.5.2", - "picocolors": "^1.0.0", - "sirv": "^2.0.3", - "ws": "^7.3.1" - }, - "bin": { - "webpack-bundle-analyzer": "lib/bin/analyzer.js" - }, - "engines": { - "node": ">= 10.13.0" - } - }, - "node_modules/webpack-bundle-analyzer/node_modules/commander": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", - "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", - "license": "MIT", - "engines": { - "node": ">= 10" - } - }, - "node_modules/webpack-dev-middleware": { - "version": "5.3.4", - "resolved": "https://registry.npmjs.org/webpack-dev-middleware/-/webpack-dev-middleware-5.3.4.tgz", - "integrity": "sha512-BVdTqhhs+0IfoeAf7EoH5WE+exCmqGerHfDM0IL096Px60Tq2Mn9MAbnaGUe6HiMa41KMCYF19gyzZmBcq/o4Q==", - "license": "MIT", - "dependencies": { - "colorette": "^2.0.10", - "memfs": "^3.4.3", - "mime-types": "^2.1.31", - "range-parser": "^1.2.1", - "schema-utils": "^4.0.0" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.0.0 || ^5.0.0" - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-middleware/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-middleware/node_modules/range-parser": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", - "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack-dev-server": { - "version": "4.15.2", - "resolved": "https://registry.npmjs.org/webpack-dev-server/-/webpack-dev-server-4.15.2.tgz", - "integrity": "sha512-0XavAZbNJ5sDrCbkpWL8mia0o5WPOd2YGtxrEiZkBK9FjLppIUK2TgxK6qGD2P3hUXTJNNPVibrerKcx5WkR1g==", - "license": "MIT", - "dependencies": { - "@types/bonjour": "^3.5.9", - "@types/connect-history-api-fallback": "^1.3.5", - "@types/express": "^4.17.13", - "@types/serve-index": "^1.9.1", - "@types/serve-static": "^1.13.10", - "@types/sockjs": "^0.3.33", - "@types/ws": "^8.5.5", - "ansi-html-community": "^0.0.8", - "bonjour-service": "^1.0.11", - "chokidar": "^3.5.3", - "colorette": "^2.0.10", - "compression": "^1.7.4", - "connect-history-api-fallback": "^2.0.0", - "default-gateway": "^6.0.3", - "express": "^4.17.3", - "graceful-fs": "^4.2.6", - "html-entities": "^2.3.2", - "http-proxy-middleware": "^2.0.3", - "ipaddr.js": "^2.0.1", - "launch-editor": "^2.6.0", - "open": "^8.0.9", - "p-retry": "^4.5.0", - "rimraf": "^3.0.2", - "schema-utils": "^4.0.0", - "selfsigned": "^2.1.1", - "serve-index": "^1.9.1", - "sockjs": "^0.3.24", - "spdy": "^4.0.2", - "webpack-dev-middleware": "^5.3.4", - "ws": "^8.13.0" - }, - "bin": { - "webpack-dev-server": "bin/webpack-dev-server.js" - }, - "engines": { - "node": ">= 12.13.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/webpack" - }, - "peerDependencies": { - "webpack": "^4.37.0 || ^5.0.0" - }, - "peerDependenciesMeta": { - "webpack": { - "optional": true - }, - "webpack-cli": { - "optional": true - } - } - }, - "node_modules/webpack-dev-server/node_modules/ws": { - "version": "8.18.3", - "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.3.tgz", - "integrity": "sha512-PEIGCY5tSlUt50cqyMXfCzX+oOPqN0vuGqWzbcJ2xvnkzkq46oOpz7dQaTDBdfICb4N14+GARUDw2XV2N4tvzg==", - "license": "MIT", - "engines": { - "node": ">=10.0.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": ">=5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/webpack-merge": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-6.0.1.tgz", - "integrity": "sha512-hXXvrjtx2PLYx4qruKl+kyRSLc52V+cCvMxRjmKwoA+CBbbF5GfIBtR6kCvl0fYGqTUPKB+1ktVmTHqMOzgCBg==", - "license": "MIT", - "dependencies": { - "clone-deep": "^4.0.1", - "flat": "^5.0.2", - "wildcard": "^2.0.1" - }, - "engines": { - "node": ">=18.0.0" - } - }, - "node_modules/webpack-sources": { - "version": "3.3.3", - "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.3.tgz", - "integrity": "sha512-yd1RBzSGanHkitROoPFd6qsrxt+oFhg/129YzheDGqeustzX0vTZJZsSsQjVQC4yzBQ56K55XU8gaNCtIzOnTg==", - "license": "MIT", - "engines": { - "node": ">=10.13.0" - } - }, - "node_modules/webpack/node_modules/mime-db": { - "version": "1.52.0", - "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", - "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "license": "MIT", - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpack/node_modules/mime-types": { - "version": "2.1.35", - "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", - "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "license": "MIT", - "dependencies": { - "mime-db": "1.52.0" - }, - "engines": { - "node": ">= 0.6" - } - }, - "node_modules/webpackbar": { - "version": "6.0.1", - "resolved": "https://registry.npmjs.org/webpackbar/-/webpackbar-6.0.1.tgz", - "integrity": "sha512-TnErZpmuKdwWBdMoexjio3KKX6ZtoKHRVvLIU0A47R0VVBDtx3ZyOJDktgYixhoJokZTYTt1Z37OkO9pnGJa9Q==", - "license": "MIT", - "dependencies": { - "ansi-escapes": "^4.3.2", - "chalk": "^4.1.2", - "consola": "^3.2.3", - "figures": "^3.2.0", - "markdown-table": "^2.0.0", - "pretty-time": "^1.1.0", - "std-env": "^3.7.0", - "wrap-ansi": "^7.0.0" - }, - "engines": { - "node": ">=14.21.3" - }, - "peerDependencies": { - "webpack": "3 || 4 || 5" - } - }, - "node_modules/webpackbar/node_modules/emoji-regex": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", - "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "license": "MIT" - }, - "node_modules/webpackbar/node_modules/markdown-table": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/markdown-table/-/markdown-table-2.0.0.tgz", - "integrity": "sha512-Ezda85ToJUBhM6WGaG6veasyym+Tbs3cMAw/ZhOPqXiYsr0jgocBV3j3nx+4lk47plLlIqjwuTm/ywVI+zjJ/A==", - "license": "MIT", - "dependencies": { - "repeat-string": "^1.0.0" - }, - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - }, - "node_modules/webpackbar/node_modules/string-width": { - "version": "4.2.3", - "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", - "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "license": "MIT", - "dependencies": { - "emoji-regex": "^8.0.0", - "is-fullwidth-code-point": "^3.0.0", - "strip-ansi": "^6.0.1" - }, - "engines": { - "node": ">=8" - } - }, - "node_modules/webpackbar/node_modules/wrap-ansi": { - "version": "7.0.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", - "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^4.0.0", - "string-width": "^4.1.0", - "strip-ansi": "^6.0.0" - }, - "engines": { - "node": ">=10" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/websocket-driver": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/websocket-driver/-/websocket-driver-0.7.4.tgz", - "integrity": "sha512-b17KeDIQVjvb0ssuSDF2cYXSg2iztliJ4B9WdsuB6J952qCPKmnVq4DyW5motImXHDC1cBT/1UezrJVsKw5zjg==", - "license": "Apache-2.0", - "dependencies": { - "http-parser-js": ">=0.5.1", - "safe-buffer": ">=5.1.0", - "websocket-extensions": ">=0.1.1" - }, - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/websocket-extensions": { - "version": "0.1.4", - "resolved": "https://registry.npmjs.org/websocket-extensions/-/websocket-extensions-0.1.4.tgz", - "integrity": "sha512-OqedPIGOfsDlo31UNwYbCFMSaO9m9G/0faIHj5/dZFDMFqPTcx6UwqyOy3COEaEOg/9VsGIpdqn62W5KhoKSpg==", - "license": "Apache-2.0", - "engines": { - "node": ">=0.8.0" - } - }, - "node_modules/which": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", - "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", - "license": "ISC", - "dependencies": { - "isexe": "^2.0.0" - }, - "bin": { - "node-which": "bin/node-which" - }, - "engines": { - "node": ">= 8" - } - }, - "node_modules/widest-line": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/widest-line/-/widest-line-4.0.1.tgz", - "integrity": "sha512-o0cyEG0e8GPzT4iGHphIOh0cJOV8fivsXxddQasHPHfoZf1ZexrfeA21w2NaEN1RHE+fXlfISmOE8R9N3u3Qig==", - "license": "MIT", - "dependencies": { - "string-width": "^5.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/wildcard": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", - "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", - "license": "MIT" - }, - "node_modules/wrap-ansi": { - "version": "8.1.0", - "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-8.1.0.tgz", - "integrity": "sha512-si7QWI6zUMq56bESFvagtmzMdGOtoxfR+Sez11Mobfc7tm+VkUckk9bW2UeffTGVUbOksxmSw0AA2gs8g71NCQ==", - "license": "MIT", - "dependencies": { - "ansi-styles": "^6.1.0", - "string-width": "^5.0.1", - "strip-ansi": "^7.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/wrap-ansi?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-regex": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-6.2.2.tgz", - "integrity": "sha512-Bq3SmSpyFHaWjPk8If9yc6svM8c56dB5BAtW4Qbw5jHTwwXXcTLoRMkpDJp6VL0XzlWaCHTXrkFURMYmD0sLqg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-regex?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/ansi-styles": { - "version": "6.2.3", - "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-6.2.3.tgz", - "integrity": "sha512-4Dj6M28JB+oAH8kFkTLUo+a2jwOFkuqb3yucU0CANcRRUbxS0cP0nZYCGjcc3BNXwRIsUVmDGgzawme7zvJHvg==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/ansi-styles?sponsor=1" - } - }, - "node_modules/wrap-ansi/node_modules/strip-ansi": { - "version": "7.1.2", - "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-7.1.2.tgz", - "integrity": "sha512-gmBGslpoQJtgnMAvOVqGZpEz9dyoKTCzy2nfz/n8aIFhN/jCE/rCmcxabB6jOOHV+0WNnylOxaxBQPSvcWklhA==", - "license": "MIT", - "dependencies": { - "ansi-regex": "^6.0.1" - }, - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/chalk/strip-ansi?sponsor=1" - } - }, - "node_modules/wrappy": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", - "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" - }, - "node_modules/write-file-atomic": { - "version": "3.0.3", - "resolved": "https://registry.npmjs.org/write-file-atomic/-/write-file-atomic-3.0.3.tgz", - "integrity": "sha512-AvHcyZ5JnSfq3ioSyjrBkH9yW4m7Ayk8/9My/DD9onKeu/94fwrMocemO2QAJFAlnnDN+ZDS+ZjAR5ua1/PV/Q==", - "license": "ISC", - "dependencies": { - "imurmurhash": "^0.1.4", - "is-typedarray": "^1.0.0", - "signal-exit": "^3.0.2", - "typedarray-to-buffer": "^3.1.5" - } - }, - "node_modules/ws": { - "version": "7.5.10", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", - "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", - "license": "MIT", - "engines": { - "node": ">=8.3.0" - }, - "peerDependencies": { - "bufferutil": "^4.0.1", - "utf-8-validate": "^5.0.2" - }, - "peerDependenciesMeta": { - "bufferutil": { - "optional": true - }, - "utf-8-validate": { - "optional": true - } - } - }, - "node_modules/xdg-basedir": { - "version": "5.1.0", - "resolved": "https://registry.npmjs.org/xdg-basedir/-/xdg-basedir-5.1.0.tgz", - "integrity": "sha512-GCPAHLvrIH13+c0SuacwvRYj2SxJXQ4kaVTT5xgL3kPrz56XxkF21IGhjSE1+W0aw7gpBWRGXLCPnPby6lSpmQ==", - "license": "MIT", - "engines": { - "node": ">=12" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/xml-js": { - "version": "1.6.11", - "resolved": "https://registry.npmjs.org/xml-js/-/xml-js-1.6.11.tgz", - "integrity": "sha512-7rVi2KMfwfWFl+GpPg6m80IVMWXLRjO+PxTq7V2CDhoGak0wzYzFgUY2m4XJ47OGdXd8eLE8EmwfAmdjw7lC1g==", - "license": "MIT", - "dependencies": { - "sax": "^1.2.4" - }, - "bin": { - "xml-js": "bin/cli.js" - } - }, - "node_modules/yallist": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", - "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", - "license": "ISC" - }, - "node_modules/yocto-queue": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-1.2.1.tgz", - "integrity": "sha512-AyeEbWOu/TAXdxlV9wmGcR0+yh2j3vYPGOECcIj2S7MkrLyC7ne+oye2BKTItt0ii2PHk4cDy+95+LshzbXnGg==", - "license": "MIT", - "engines": { - "node": ">=12.20" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, - "node_modules/zwitch": { - "version": "2.0.4", - "resolved": "https://registry.npmjs.org/zwitch/-/zwitch-2.0.4.tgz", - "integrity": "sha512-bXE4cR/kVZhKZX/RjPEflHaKVhUVl85noU3v6b8apfQEc1x4A+zBxjZ4lN8LqGd6WZ3dl98pY4o717VFmoPp+A==", - "license": "MIT", - "funding": { - "type": "github", - "url": "https://github.com/sponsors/wooorm" - } - } - } -} diff --git a/website/sidebars.js b/website/sidebars.js deleted file mode 100644 index f77355c3e..000000000 --- a/website/sidebars.js +++ /dev/null @@ -1,35 +0,0 @@ -// @ts-check - -// This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) - -/** - * Creating a sidebar enables you to: - - create an ordered group of docs - - render a sidebar for each doc of that group - - provide next/previous navigation - - The sidebars can be generated from the filesystem, or explicitly defined here. - - Create as many sidebars as you want. - - @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} - */ -const sidebars = { - // By default, Docusaurus generates a sidebar from the docs folder structure - tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], - - // But you can create a sidebar manually - /* - tutorialSidebar: [ - 'intro', - 'hello', - { - type: 'category', - label: 'Tutorial', - items: ['tutorial-basics/create-a-document'], - }, - ], - */ -}; - -export default sidebars; diff --git a/website/src/components/HomepageFeatures/index.js b/website/src/components/HomepageFeatures/index.js deleted file mode 100644 index acc762199..000000000 --- a/website/src/components/HomepageFeatures/index.js +++ /dev/null @@ -1,64 +0,0 @@ -import clsx from 'clsx'; -import Heading from '@theme/Heading'; -import styles from './styles.module.css'; - -const FeatureList = [ - { - title: 'Easy to Use', - Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, - description: ( - <> - Docusaurus was designed from the ground up to be easily installed and - used to get your website up and running quickly. - - ), - }, - { - title: 'Focus on What Matters', - Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, - description: ( - <> - Docusaurus lets you focus on your docs, and we'll do the chores. Go - ahead and move your docs into the docs directory. - - ), - }, - { - title: 'Powered by React', - Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, - description: ( - <> - Extend or customize your website layout by reusing React. Docusaurus can - be extended while reusing the same header and footer. - - ), - }, -]; - -function Feature({Svg, title, description}) { - return ( -
-
- -
-
- {title} -

{description}

-
-
- ); -} - -export default function HomepageFeatures() { - return ( -
-
-
- {FeatureList.map((props, idx) => ( - - ))} -
-
-
- ); -} diff --git a/website/static/img/docusaurus.png b/website/static/img/docusaurus.png deleted file mode 100644 index f458149e3..000000000 Binary files a/website/static/img/docusaurus.png and /dev/null differ diff --git a/website/static/img/favicon.ico b/website/static/img/favicon.ico deleted file mode 100644 index c01d54bcd..000000000 Binary files a/website/static/img/favicon.ico and /dev/null differ diff --git a/website/static/img/logo.svg b/website/static/img/logo.svg deleted file mode 100644 index 9db6d0d06..000000000 --- a/website/static/img/logo.svg +++ /dev/null @@ -1 +0,0 @@ - \ No newline at end of file diff --git a/website/static/img/undraw_docusaurus_mountain.svg b/website/static/img/undraw_docusaurus_mountain.svg deleted file mode 100644 index af961c49a..000000000 --- a/website/static/img/undraw_docusaurus_mountain.svg +++ /dev/null @@ -1,171 +0,0 @@ - - Easy to Use - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/undraw_docusaurus_react.svg b/website/static/img/undraw_docusaurus_react.svg deleted file mode 100644 index 94b5cf08f..000000000 --- a/website/static/img/undraw_docusaurus_react.svg +++ /dev/null @@ -1,170 +0,0 @@ - - Powered by React - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/static/img/undraw_docusaurus_tree.svg b/website/static/img/undraw_docusaurus_tree.svg deleted file mode 100644 index d9161d339..000000000 --- a/website/static/img/undraw_docusaurus_tree.svg +++ /dev/null @@ -1,40 +0,0 @@ - - Focus on What Matters - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - diff --git a/website/versioned_docs/version-0.10.10/01-Intro.md b/website/versioned_docs/version-0.10.10/01-Intro.md deleted file mode 100644 index 81b5d6707..000000000 --- a/website/versioned_docs/version-0.10.10/01-Intro.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -slug: /intro ---- - -# Untold Engine Documentation - -Welcome to the **Untold Engine documentation**. - -These docs are the primary reference for working with the Untold Engine ecosystem — whether you are building a game, extending the engine, or contributing to the editor. - ---- - -## What Is Untold Engine? - -![untoldengine](images/Editor/EditorMainShot.png) - -The Untold Engine strives to be a stable, performant, and developer-friendly 3D engine that empowers creativity, removes friction, and makes game development feel effortless for Apple developers - -The Untold Engine is an open-source 3D game engine under active development, designed for macOS, iOS, xrOS platforms. Written in Swift and powered by Metal, its goal is to simplify game creation with a clean, intuitive API. - -While the engine already supports many core systems like rendering, physics, and animation, there’s still much to build and improve. - ---- - -## The Untold Engine Ecosystem - -Untold Engine is delivered through three closely related products: - -### Untold Engine Studio -A downloadable application that includes: -- The Untold Engine runtime -- The Untold Editor -- Built-in tools for scripting, assets, and scene editing - -This is the recommended starting point for most users. - ---- - -### Untold Engine -The core engine runtime. - -This is intended for: -- Engine developers -- Contributors -- Advanced users who want to modify or extend engine systems - -Installation is performed via the command line. - ---- - -### Untold Editor -The editor application built on top of the engine runtime. - -This is intended for: -- Contributors working on editor features -- Developers extending tools and workflows - -The editor uses the same runtime as games, ensuring consistent behavior. - ---- - -## Choose Your Path - -These docs are organized around **how you intend to use the engine**. - -### Game Development -For developers building games using Untold Engine. - -You will learn: -- How to create scenes visually -- How to write game logic (Swift or USC scripts) -- How to work with assets and entities -- How to build and run your game - -Untold Engine supports two approaches for writing gameplay code: -- **Swift in Xcode** (recommended) - Full engine API access -- **USC Scripts** (experimental) - Component-based scripting - -Start here if your goal is to build a game. - ---- - -### Engine Development -For developers who want to understand or extend the engine itself. - -You will learn: -- The engine architecture -- ECS and system execution -- Rendering and simulation internals -- How to contribute new engine features - -Start here if you want to work on the engine runtime. - ---- - -### Editor Development -For contributors working on the Untold Editor. - -You will learn: -- Editor architecture -- Views, tools, and interaction models -- How the editor coordinates with the engine -- How to extend or add editor functionality - -Start here if you want to improve the editor. - ---- - -## Getting Started - -If you are unsure where to begin: - -- New users: **Game Development → Overview** -- Scripting users: **USC → Introduction** -- Contributors: **Engine Development → Architecture** - -Each section is designed to stand on its own. - diff --git a/website/versioned_docs/version-0.10.10/02-Getting Started/02-Installation.md b/website/versioned_docs/version-0.10.10/02-Getting Started/02-Installation.md deleted file mode 100644 index 89e99a7ee..000000000 --- a/website/versioned_docs/version-0.10.10/02-Getting Started/02-Installation.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -id: intro -title: Installation -sidebar_position: 1 ---- - -# Installation - -This page explains how to install **Untold Engine Studio**, the recommended way to get started with Untold Engine. - -Untold Engine Studio is a downloadable app that includes: -- The **Untold Engine** runtime -- The **Untold Editor** for building and editing games - -![editorbottomshot](../images/Editor/EditorBottomShot.png) - -If your goal is to **make games**, this is the only installation you need. - ---- - -## Recommended Installation (Untold Engine Studio) - -### 1. Download - -Download the latest version of **Untold Engine Studio** from the official website: - -[Download Releases](https://github.com/untoldengine/UntoldEditor/releases) - -The download is provided as a `.dmg` file for macOS. - ---- - -### 2. Install - -1. Open the downloaded `.dmg` file -2. Drag **Untold Engine Studio** into your `Applications` folder -3. Launch the app from `Applications` - -No additional setup is required. - ---- - -### 3. First Launch - -On first launch, Untold Engine Studio will: -- Initialize the engine runtime -- Set up the editor environment -- Prompt you to create or open a project - -From here, you can immediately: -- Create scenes visually -- Import 3D models and assets -- Write game logic (Swift in Xcode or USC scripts) -- Build and test your game - ---- - -## System Requirements - -- macOS (Apple Silicon recommended) -- Metal-capable GPU -- Keyboard and mouse - ---- - -## What You Get - -By installing Untold Engine Studio, you get: - -- A complete **game development environment** -- Visual editor for scenes, assets, and scripts -- Full **Untold Engine Swift API** for game logic in Xcode -- **USC scripting system** (experimental component-based scripting) -- Build and run support for macOS, iOS, and visionOS - -You do **not** need to install the engine or editor separately. - -### Two Ways to Write Game Logic - -Untold Engine Studio supports two approaches for writing gameplay code: - -**1. Swift in Xcode (Recommended)** -- Write game logic in `GameScene.swift` using the full Untold Engine API -- Complete control over game systems and performance -- Best for complex games and experienced developers -- Works seamlessly with Xcode debugging and profiling - -**2. USC Scripts (Experimental)** -- Component-based scripting attached to entities -- Write gameplay behaviors in the integrated script editor -- Good for prototyping and simple game mechanics -- API is experimental and subject to change - -You can **use both approaches** in the same project. - ---- - -## Alternative Installation: CLI Workflow - -For **advanced users** or those who prefer a **command-line workflow** without the visual editor, you can install the CLI tools. - -### When to Use CLI - -- You prefer working entirely in Xcode without a visual editor -- You want to script project creation and automation -- You're building tools or integrations on top of UntoldEngine - -### CLI Installation - -**1. Clone the repository:** - -```bash -git clone https://github.com/untoldengine/UntoldEngine.git -cd UntoldEngine -``` - -**2. Install the CLI globally:** - -```bash -./scripts/install-create.sh -``` - -**3. Verify installation:** - -```bash -untoldengine-create --version -untoldengine-create --help -``` - -### CLI Quick Start - -After installing the CLI, create a project from anywhere: - -```bash -# 1. Create project directory -cd ~/anywhere -mkdir MyGame && cd MyGame - -# 2. Create the project -untoldengine-create create MyGame - -# 3. Open in Xcode -open MyGame/MyGame.xcodeproj -``` - -For complete CLI documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - ---- - -### What You Get - -The CLI creates a complete, ready-to-run project: - -- **Xcode project** - Configured and ready to build -- **GameScene.swift** - Your game logic goes here -- **GameViewController.swift** - Renderer and view setup -- **GameData/** directory - All game assets location -- **Platform-specific** files (AppDelegate, Info.plist, etc.) - -### Project Structure - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Open this in Xcode - ├── project.yml # XcodeGen configuration - └── Sources/ - └── MyGame/ - ├── GameData/ # ← Put your assets here - │ ├── Models/ # 3D models - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # USC scripts - │ ├── Textures/ # Images - │ └── ... - ├── GameScene.swift # Your game logic - ├── GameViewController.swift # View controller - └── AppDelegate.swift # App entry point -``` - -### Platform Support - -The CLI supports multiple platforms: - -```bash -# macOS (default) -untoldengine-create create MyGame --platform macos - -# iOS -untoldengine-create create MyGame --platform ios - -# iOS with ARKit -untoldengine-create create MyGame --platform iosar - -# visionOS (Apple Vision Pro) -untoldengine-create create MyGame --platform visionos -``` - -### Development Workflow - -1. **Write code** in GameScene.swift (game logic) -2. **Add assets** to the GameData/ directory -3. **Build & run** in Xcode (Cmd+R) -4. **Iterate** - make changes and rebuild - -For complete CLI documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - ---- - -## Preloaded Assets - -To kickstart development, download prebuilt demo assets: - -- **Models**: Soccer stadium, player, ball, and more -- **Animations**: Running, idle, and other character motions -- **Textures**: Sample materials - -[Download Demo Assets v1.0](https://haroldserrano.gumroad.com/l/iqjlac) - -Extract and copy into your project's `GameData/` directory. - ---- diff --git a/website/versioned_docs/version-0.10.10/02-Getting Started/03-ChoosingYourPath.md b/website/versioned_docs/version-0.10.10/02-Getting Started/03-ChoosingYourPath.md deleted file mode 100644 index 4d003ace7..000000000 --- a/website/versioned_docs/version-0.10.10/02-Getting Started/03-ChoosingYourPath.md +++ /dev/null @@ -1,79 +0,0 @@ -# Choosing Your Path - -Untold Engine supports different types of developers. - -This page helps you choose the path that best matches what you want to do. - ---- - -## I Want to Make a Game - -Choose this path if your goal is to: -- Build gameplay -- Create scenes -- Write scripts -- Ship a game - -### What You’ll Use - -- **Untold Engine Studio** -- **USC scripting API** - -### Where to Start - -> **Game Development → Overview** - -You do not need to understand engine internals to make a game. - ---- - -## I Want to Improve the Engine - -Choose this path if you want to: -- Work on core engine systems -- Improve rendering, physics, or ECS -- Extend platform support - -### What You’ll Use - -- **Untold Engine (core)** -- Command-line tools -- Source builds - -### Where to Start - -> **Engine Development → Overview** - -This path assumes familiarity with engine concepts and systems programming. - ---- - -## I Want to Improve the Editor - -Choose this path if you want to: -- Improve the editor UI -- Add new tools or views -- Improve workflows and usability - -### What You’ll Use - -- **Untold Editor** -- Editor-specific APIs -- Engine integration points - -### Where to Start - -> **Editor Development → Overview** - -Editor development focuses on tooling rather than gameplay. - ---- - -## Not Sure Yet? - -If you’re not sure where to begin, start here: - -> **Game Development → Overview** - -You can always explore the other paths later. - diff --git a/website/versioned_docs/version-0.10.10/02-Getting Started/_category.json b/website/versioned_docs/version-0.10.10/02-Getting Started/_category.json deleted file mode 100644 index ac4e7d573..000000000 --- a/website/versioned_docs/version-0.10.10/02-Getting Started/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "01-Getting Starter", "position": 1, "collapsed": false } - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/01-Overview.md b/website/versioned_docs/version-0.10.10/03-Game Development/01-Overview.md deleted file mode 100644 index a41e30ce9..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/01-Overview.md +++ /dev/null @@ -1,116 +0,0 @@ -# Overview - -This section covers **game development** with the Untold Engine. - -You'll learn how to create games using **Untold Engine Studio**, with two approaches for writing game logic: - -1. **Swift in Xcode** - Full engine API access (recommended) -2. **USC Scripts** - Component-based scripting (experimental) - -![editorsideshotalt](../images/Editor/EditorSideShotWide-alt.png) - ---- - -## Two Approaches to Game Logic - -Untold Engine gives you flexibility in how you write gameplay code: - -### Option 1: Swift in Xcode (Recommended) - -Write game logic in `GameScene.swift` using the full Untold Engine Swift API: -- Complete control over game systems and performance -- Access to all engine features and APIs -- Seamless integration with Xcode debugging and profiling -- Best for complex games and experienced developers - -### Option 2: USC Scripts (Experimental) - -Write component-based scripts that attach to entities: -- Simpler, component-oriented approach -- Good for prototyping and simple game mechanics -- Edit scripts in the integrated editor or Xcode -- API is experimental and subject to change - -**You can use both approaches in the same project.** - ---- - -## The Development Workflow - -A typical development workflow with Untold Engine Studio: - -1. **Create a project** using the "New" button in Untold Engine Studio -2. **Compose scenes visually** using the editor -3. **Write game logic** (Swift in `GameScene.swift` or USC scripts) -4. **Add assets** to the GameData/ directory -5. **Build & run** in Xcode (Cmd+R) -6. **Iterate** quickly with visual feedback - ---- - -## Entry Point 1: GameScene.swift (Swift) - -When you create a project, you get a clean `GameScene.swift` file for writing game logic in Swift: - -```swift -class GameScene { - - init() { - // Configure asset paths - setupAssetPaths() - - // Load game content - loadBundledScripts() - loadAndPlayFirstScene() - - // Start game systems - startGameSystems() - } - - func update(deltaTime: Float) { - // Your game logic goes here - } - - func handleInput() { - // Handle user input here - } -} -``` - -**This is where your game comes to life.** Write Swift code, access the full engine API, and build your game. - ---- - -## Entry Point 2: USC Scripts (Experimental) - -Alternatively, write game logic as USC scripts that attach to entities: - -Create USC scripts from the **Script** menu in Untold Engine Studio, then attach them to entities in the Inspector. - ---- - - -## Project Structure - -Your generated project has everything you need: - -``` -MyGame/ -└── MyGame/ - ├── MyGame.xcodeproj # Open in Xcode - └── Sources/ - └── MyGame/ - ├── GameData/ # Assets location - │ ├── Models/ - │ ├── Scenes/ - │ ├── Scripts/ - │ └── Textures/ - ├── GameScene.swift # Your game logic ⭐ - ├── GameViewController.swift # Renderer setup - └── AppDelegate.swift # App entry -``` - -**Focus on GameScene.swift** - that's where your game lives. - ---- - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/00_HelloWorld.md b/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/00_HelloWorld.md deleted file mode 100644 index 6c5007b74..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/00_HelloWorld.md +++ /dev/null @@ -1,152 +0,0 @@ -# Hello World - -Your first UntoldEngine program - logging a message every frame. - ---- - -## Overview - -This tutorial shows you how to add custom code to your game's update loop and log output to the console. - ---- - -## Prerequisites - -This tutorial assumes you have: -- Created a project using the 'Untold Engine Studio` or `untoldengine-create` -- Opened the project in Xcode -- Located `GameScene.swift` in your project - ---- - -## Step 1: Open GameScene.swift - -In Xcode, navigate to: - -``` -Sources/YourProjectName/GameScene.swift -``` - ---- - -## Step 2: Add Your First Game Logic - -Find the `update(deltaTime:)` method in `GameScene.swift`: - -```swift path=null start=null -func update(deltaTime: Float) { - // Skip logic if not in game mode - if gameMode == false { return } - - // Add your custom update logic here -} -``` - -Replace the comment with: - -```swift path=null start=null -func update(deltaTime: Float) { - // Skip logic if not in game mode - if gameMode == false { return } - - // Your first game code! 🎉 - Logger.log(message: "Hello World! Delta: \(deltaTime)") -} -``` - ---- - -## Step 3: Build and Run - -Press **Cmd+R** in Xcode. - -Open the **Debug Console** (Cmd+Shift+Y) to see: - -``` -Hello World! Delta: 0.016 -Hello World! Delta: 0.017 -Hello World! Delta: 0.016 -... -``` - -The message appears every frame! 🚀 - ---- - -## What Just Happened? - -### The Update Loop - -`update(deltaTime:)` is called every frame by the engine: - -- **60 FPS** = called 60 times per second -- **deltaTime** = time since last frame (in seconds) - -All game logic goes here: movement, input handling, collision detection, etc. - -### Logging - -```swift path=null start=null -Logger.log(message: "Hello World!") -``` - -Use `Logger.log()` to print debug messages. It's better than `print()` because: -- Engine-aware logging -- Can be filtered/disabled in production -- Consistent formatting - -### Other Logging Methods - -```swift path=null start=null -Logger.logWarning(message: "Something might be wrong") -Logger.logError(message: "Something went wrong!") -``` - ---- - -## Limiting Output (Recommended) - -Logging every frame creates spam. Let's log once per second instead: - -```swift path=null start=null -class GameScene { - var elapsedTime: Float = 0.0 // Add this property - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Accumulate time - elapsedTime += deltaTime - - // Log once per second - if elapsedTime >= 1.0 { - Logger.log(message: "Hello World! One second passed.") - elapsedTime = 0.0 // Reset - } - } -} -``` - -Now you'll see: - -``` -Hello World! One second passed. -Hello World! One second passed. -... -``` - -Much cleaner! - ---- - -## Summary - -You've learned: - -✅ The `update(deltaTime:)` method runs every frame -✅ `deltaTime` is the time between frames -✅ `Logger.log()` prints messages to the console -✅ How to accumulate time for periodic actions - -This is the foundation of game development: **write code that runs every frame**. - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md b/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md deleted file mode 100644 index 94cf91c38..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md +++ /dev/null @@ -1,214 +0,0 @@ -# Move an Entity - -Learn how to move entities using the Transform System. - ---- - -## Overview - -This tutorial shows you how to: -- Find an entity from a loaded scene -- Move an entity to an absolute position -- Move an entity relative to its current position - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity (created in Untold Engine Studio or loaded via `loadScene()`) -- The entity has a name set in the editor (e.g., "Player") - -For complete API documentation: - -➡️ **[Transform System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingTransformSystem.md)** - ---- - -## Step 1: Find the Entity from Your Scene - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code (setupAssetPaths, loadScene, etc.) ... - startGameSystems() - - // Find the entity by name (set in the editor) - player = findEntity(name: "Player") - - if player == nil { - Logger.logWarning(message: "Player entity not found in scene") - } - } -} -``` - -**Important**: "Player" must match the entity name you set in Untold Engine Studio. - ---- - -## Step 2: Move to an Absolute Position - -Use `translateTo()` to set an entity to a specific world position: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - - player = findEntity(name: "Player") - - // Move player to position (5, 0, -10) - translateTo(entityId: player, position: SIMD3(5.0, 0.0, -10.0)) - } -} -``` - -**Result**: The entity immediately moves to position (5, 0, -10) in world space. - ---- - -## Step 3: Move Relative to Current Position - -Use `translateBy()` to move an entity by an offset: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - - player = findEntity(name: "Player") - - // Move player 3 units to the right (X-axis) - translateBy(entityId: player, delta: SIMD3(3.0, 0.0, 0.0)) - } -} -``` - -**Result**: The entity moves 3 units along the X-axis from its current position. - ---- - -## Step 4: Continuous Movement in Update Loop - -For smooth movement every frame, use `translateBy()` in `update(deltaTime:)`: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let moveSpeed: Float = 5.0 // Units per second - - init() { - // ... setup code ... - player = findEntity(name: "Player") - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Move forward continuously - let movement = SIMD3(0, 0, -moveSpeed * deltaTime) - translateBy(entityId: player, delta: movement) - } -} -``` - -**Result**: The player moves forward smoothly at 5 units per second. - ---- - -## Understanding Delta Time - -**Why multiply by `deltaTime`?** - -`deltaTime` is the time (in seconds) since the last frame: -- At 60 FPS: `deltaTime ≈ 0.016` seconds -- At 30 FPS: `deltaTime ≈ 0.033` seconds - -By multiplying speed by `deltaTime`, movement becomes **frame-rate independent**: - -```swift path=null start=null -// Without deltaTime (BAD) -translateBy(entityId: player, delta: SIMD3(0, 0, -0.1)) -// Result: Speed varies with frame rate ❌ - -// With deltaTime (GOOD) -let movement = SIMD3(0, 0, -moveSpeed * deltaTime) -translateBy(entityId: player, delta: movement) -// Result: Consistent speed regardless of frame rate ✅ -``` - ---- - -## Movement Examples - -### Move Forward - -```swift path=null start=null -let movement = SIMD3(0, 0, -moveSpeed * deltaTime) -translateBy(entityId: player, delta: movement) -``` - -### Move Right - -```swift path=null start=null -let movement = SIMD3(moveSpeed * deltaTime, 0, 0) -translateBy(entityId: player, delta: movement) -``` - -### Move Up - -```swift path=null start=null -let movement = SIMD3(0, moveSpeed * deltaTime, 0) -translateBy(entityId: player, delta: movement) -``` - -### Move Along Entity's Forward Direction - -```swift path=null start=null -let forward = getForwardAxisVector(entityId: player) -let movement = forward * moveSpeed * deltaTime -translateBy(entityId: player, delta: movement) -``` - ---- - -## Checking Entity Position - -To read an entity's current position: - -```swift path=null start=null -// World position (absolute) -let worldPos = getPosition(entityId: player) -Logger.log(message: "Player world position: \(worldPos)") - -// Local position (relative to parent, if parented) -let localPos = getLocalPosition(entityId: player) -Logger.log(message: "Player local position: \(localPos)") -``` - ---- - -## Summary - -You've learned: - -✅ `findEntity(name:)` - Find entities from loaded scenes -✅ `translateTo()` - Set absolute world position -✅ `translateBy()` - Move relative to current position -✅ `deltaTime` - Make movement frame-rate independent -✅ `getPosition()` - Read current position - ---- - - - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/02_Input/01_KeyboardMovement.md b/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/02_Input/01_KeyboardMovement.md deleted file mode 100644 index 988a49707..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/02_Input/01_KeyboardMovement.md +++ /dev/null @@ -1,261 +0,0 @@ -# Keyboard Movement - -Learn how to move entities using keyboard input. - ---- - -## Overview - -This tutorial shows you how to: -- Detect keyboard input using the Input System -- Move an entity based on WASD keys -- Combine input with Transform System - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity -- The entity has a name set in the editor (e.g., "Player") - -For complete API documentation: - -➡️ **[Input System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingInputSystem.md)** - ---- - -## Step 1: Enable Input System - -Before detecting input, you need to register keyboard events. Add this to your `startGameSystems()` helper or in `init()`: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - startGameSystems() - - // Register input keyboard events - InputSystem.shared.registerKeyboardEvents() - - // Find the entity by name (set in the editor) - player = findEntity(name: "Player") - } -} -``` - -**Important**: Call `InputSystem.shared.registerKeyboardEvents()` once during initialization to enable keyboard input detection. - ---- - -## Step 2: Detect Keyboard Input - -The Input System provides a `keyState` object to check if keys are pressed: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - // Check if W key is pressed - if inputSystem.keyState.wPressed == true { - Logger.log(message: "W key pressed!") - } -} -``` - -**Available Keys**: -- `inputSystem.keyState.wPressed` - W key -- `inputSystem.keyState.aPressed` - A key -- `inputSystem.keyState.sPressed` - S key -- `inputSystem.keyState.dPressed` - D key - ---- - -## Step 3: Move Entity with WASD - -Combine input detection with movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let moveSpeed: Float = 5.0 - - init() { - // ... setup code ... - player = findEntity(name: "Player") - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - var movement = SIMD3(0, 0, 0) - - // Forward (W) - if inputSystem.keyState.wPressed == true { - movement.z += moveSpeed * deltaTime - } - - // Backward (S) - if inputSystem.keyState.sPressed == true { - movement.z -= moveSpeed * deltaTime - } - - // Left (A) - if inputSystem.keyState.aPressed == true { - movement.x -= moveSpeed * deltaTime - } - - // Right (D) - if inputSystem.keyState.dPressed == true { - movement.x += moveSpeed * deltaTime - } - - // Apply movement - if movement != SIMD3(0, 0, 0) { - translateBy(entityId: player, delta: movement) - } - } -} -``` - -**Result**: The player moves based on WASD input at 5 units per second. - ---- - -## Understanding the Code - -### Input Detection - -```swift path=null start=null -if inputSystem.keyState.wPressed == true { - // Key is currently pressed -} -``` - -The Input System automatically tracks key states. You don't need to register listeners or handle events. - -### Accumulating Movement - -```swift path=null start=null -var movement = SIMD3(0, 0, 0) - -if inputSystem.keyState.wPressed == true { - movement.z += moveSpeed * deltaTime -} - -if inputSystem.keyState.dPressed == true { - movement.x += moveSpeed * deltaTime -} -``` - -By accumulating input into a `movement` vector, diagonal movement (W+D) works correctly. - -### Delta Time - -Multiplying by `deltaTime` ensures consistent speed regardless of frame rate: - -```swift path=null start=null -movement.z += moveSpeed * deltaTime // ✅ Frame-rate independent -movement.z += 0.1 // ❌ Varies with frame rate -``` - ---- - -## Advanced: Normalized Diagonal Movement - -Diagonal movement (W+D) is currently faster than single-direction movement. To fix this, normalize the movement vector: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - var movement = SIMD3(0, 0, 0) - - // Accumulate input - if inputSystem.keyState.wPressed == true { - movement.z += 1.0 - } - if inputSystem.keyState.sPressed == true { - movement.z -= 1.0 - } - if inputSystem.keyState.aPressed == true { - movement.x -= 1.0 - } - if inputSystem.keyState.dPressed == true { - movement.x += 1.0 - } - - // Normalize and apply speed - if movement != SIMD3(0, 0, 0) { - movement = normalize(movement) * moveSpeed * deltaTime - translateBy(entityId: player, delta: movement) - } -} -``` - -**Result**: Diagonal movement is now the same speed as cardinal movement. - ---- - -## Example: Tank Controls (Forward + Rotation) - -For tank-style controls where forward moves along the entity's facing direction: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let moveSpeed: Float = 5.0 - let turnSpeed: Float = 90.0 // Degrees per second - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Forward/backward movement - var forwardMovement: Float = 0.0 - if inputSystem.keyState.wPressed == true { - forwardMovement = moveSpeed * deltaTime - } - if inputSystem.keyState.sPressed == true { - forwardMovement = -moveSpeed * deltaTime - } - - // Apply forward movement along entity's forward direction - if forwardMovement != 0.0 { - let forward = getForwardAxisVector(entityId: player) - translateBy(entityId: player, delta: forward * forwardMovement) - } - - // Left/right rotation - var turnAngle: Float = 0.0 - if inputSystem.keyState.aPressed == true { - turnAngle = -turnSpeed * deltaTime - } - if inputSystem.keyState.dPressed == true { - turnAngle = turnSpeed * deltaTime - } - - // Apply rotation - if turnAngle != 0.0 { - rotateBy(entityId: player, angle: turnAngle, axis: SIMD3(0, 1, 0)) - } - } -} -``` - ---- - -## Summary - -You've learned: - -✅ `inputSystem.keyState` - Check keyboard input -✅ Detect W, A, S, D keys -✅ Combine input with `translateBy()` -✅ Normalize diagonal movement -✅ Tank-style controls with rotation - ---- - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md b/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md deleted file mode 100644 index 3bbd8abc7..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md +++ /dev/null @@ -1,319 +0,0 @@ -# Animation State Switching - -Learn how to switch between different animations based on player input and game state. - ---- - -## Overview - -This tutorial shows you how to: -- Create a state-based animation system -- Switch between idle, running, and jumping animations -- Trigger animations based on input - ---- - -## Prerequisites - -This tutorial assumes you have: -- Completed the [Play Animation tutorial](./01_PlayAnimation.md) -- A project with multiple animations loaded (idle, running, jumping) -- An entity with a skeleton that supports animation - -For complete API documentation: - -➡️ **[Animation System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingAnimationSystem.md)** - ---- - -## Step 1: Set Up Animation States - -Define an enum to represent your animation states: - -```swift path=null start=null -enum PlayerState { - case idle - case running - case jumping -} - -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - - init() { - // ... setup code ... - startGameSystems() - - // Register input - InputSystem.shared.registerKeyboardEvents() - - player = findEntity(name: "Player") - - // Load all animations -- ignore if you linked all three animations through the editor. - loadPlayerAnimations() - - // Start with idle - changeAnimation(entityId: player, name: "idle") - } - - func loadPlayerAnimations() { - setEntityAnimations( - entityId: player, - filename: "idle", - withExtension: "usdc", - name: "idle" - ) - - setEntityAnimations( - entityId: player, - filename: "running", - withExtension: "usdc", - name: "running" - ) - - setEntityAnimations( - entityId: player, - filename: "jumping", - withExtension: "usdc", - name: "jumping" - ) - } -} -``` - ---- - -## Step 2: Implement State Switching Logic - -Create a function to handle state transitions: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true // Track if player is on ground - - func update(deltaTime: Float) { - if gameMode == false { return } - - updatePlayerState() - } - - func updatePlayerState() { - let oldState = playerState - - // Determine new state based on input and game conditions - if !isGrounded { - playerState = .jumping - } else if isMovementKeyPressed() { - playerState = .running - } else { - playerState = .idle - } - - // Only change animation if state actually changed - if playerState != oldState { - switchToAnimation(for: playerState) - } - } - - func isMovementKeyPressed() -> Bool { - return inputSystem.keyState.wPressed || - inputSystem.keyState.aPressed || - inputSystem.keyState.sPressed || - inputSystem.keyState.dPressed - } - - func switchToAnimation(for state: PlayerState) { - switch state { - case .idle: - changeAnimation(entityId: player, name: "idle") - Logger.log(message: "Switched to idle animation") - - case .running: - changeAnimation(entityId: player, name: "running") - Logger.log(message: "Switched to running animation") - - case .jumping: - changeAnimation(entityId: player, name: "jumping") - Logger.log(message: "Switched to jumping animation") - } - } -} -``` - ---- - -## Step 3: Add Jump Trigger - -Add space bar input to trigger jumping: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true - var jumpTimer: Float = 0.0 - let jumpDuration: Float = 0.5 // Jump animation duration in seconds - - func update(deltaTime: Float) { - if gameMode == false { return } - - handleJumpInput() - updateJumpTimer(deltaTime: deltaTime) - updatePlayerState() - } - - func handleJumpInput() { - // Trigger jump on space press (only if grounded) - if inputSystem.keyState.spacePressed && isGrounded { - isGrounded = false - jumpTimer = jumpDuration - } - } - - func updateJumpTimer(deltaTime: Float) { - // Count down jump timer - if !isGrounded { - jumpTimer -= deltaTime - - // Land when timer expires - if jumpTimer <= 0.0 { - isGrounded = true - jumpTimer = 0.0 - } - } - } - - func updatePlayerState() { - let oldState = playerState - - // Priority: jumping > running > idle - if !isGrounded { - playerState = .jumping - } else if isMovementKeyPressed() { - playerState = .running - } else { - playerState = .idle - } - - if playerState != oldState { - switchToAnimation(for: playerState) - } - } -} -``` - ---- - -## Step 4: Combine Animation with Movement - -Integrate animation state switching with actual movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true - var jumpTimer: Float = 0.0 - let moveSpeed: Float = 5.0 - let jumpDuration: Float = 0.5 - - func update(deltaTime: Float) { - if gameMode == false { return } - - handleJumpInput() - updateJumpTimer(deltaTime: deltaTime) - updatePlayerState() - updatePlayerMovement(deltaTime: deltaTime) - } - - func updatePlayerMovement(deltaTime: Float) { - var movement = SIMD3(0, 0, 0) - - // Only move if grounded - if isGrounded { - if inputSystem.keyState.wPressed { - movement.z += moveSpeed * deltaTime - } - if inputSystem.keyState.sPressed { - movement.z -= moveSpeed * deltaTime - } - if inputSystem.keyState.aPressed { - movement.x -= moveSpeed * deltaTime - } - if inputSystem.keyState.dPressed { - movement.x += moveSpeed * deltaTime - } - - if movement != SIMD3(0, 0, 0) { - translateBy(entityId: player, delta: movement) - } - } - } -} -``` - ---- - -## Advanced: State Machine Pattern - -For more complex state management, consider using a proper state machine: - -```swift path=null start=null -class AnimationStateMachine { - var currentState: PlayerState = .idle - var entityId: EntityID - - init(entityId: EntityID) { - self.entityId = entityId - } - - func canTransition(to newState: PlayerState) -> Bool { - switch (currentState, newState) { - case (.jumping, .idle), (.jumping, .running): - // Can only exit jumping state when landing - return false - default: - return true - } - } - - func transition(to newState: PlayerState) { - guard canTransition(to: newState) else { return } - - if currentState != newState { - currentState = newState - playAnimation(for: newState) - } - } - - func playAnimation(for state: PlayerState) { - let animationName: String - switch state { - case .idle: animationName = "idle" - case .running: animationName = "running" - case .jumping: animationName = "jumping" - } - - changeAnimation(entityId: entityId, name: animationName) - } -} -``` - ---- - -## Summary - -You've learned: - -✅ Create animation states using enums -✅ Switch animations based on game state -✅ Trigger animations from input -✅ Prevent animation flickering with state checks -✅ Combine animations with movement logic - ---- - - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md b/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md deleted file mode 100644 index ef6850642..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md +++ /dev/null @@ -1,331 +0,0 @@ -# Apply Force - -Learn how to use the Physics System to apply forces and create realistic movement. - ---- - -## Overview - -This tutorial shows you how to: -- Enable physics on an entity -- Configure mass and gravity -- Apply forces for dynamic movement -- Use the Steering System for advanced behaviors - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity -- The entity has a name set in the editor (e.g., "Ball") -- The entity has a kinetic component applied through the editor - -For complete API documentation: - -➡️ **[Physics System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingPhysicsSystem.md)** -➡️ **[Steering System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingSteeringSystem.md)** - ---- - -## Step 1: Find the Entity - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - startGameSystems() - - ball = findEntity(name: "Ball") - } -} -``` - ---- - -## Step 2: Enable Physics (Kinetics) - -Before applying forces, enable physics on the entity: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - // Enable physics components - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - } -} -``` - -**What this does**: `setEntityKinetics()` adds `PhysicsComponents` and `KineticComponent` to the entity, enabling it to respond to forces. - ---- - -## Step 3: Configure Mass and Gravity - -Set the entity's mass and gravity scale: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - // Enable physics - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - - // Configure physics properties - setMass(entityId: ball, mass: 0.5) // Lighter = easier to move - setGravityScale(entityId: ball, gravityScale: 1.0) // Normal gravity - } -} -``` - -**Parameters**: -- `mass`: How heavy the entity is. Lower = easier to push. Default is `1.0`. -- `gravityScale`: How much gravity affects it. `0.0` = no gravity, `1.0` = normal gravity. - ---- - -## Step 4: Apply a Force - -Use `applyForce()` to push the entity: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - setMass(entityId: ball, mass: 0.5) - setGravityScale(entityId: ball, gravityScale: 1.0) - - InputSystem.shared.registerKeyboardEvents() - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Apply forward force when W is pressed - if inputSystem.keyState.wPressed { - applyForce(entityId: ball, force: SIMD3(0.0, 0.0, 5.0)) - } - } -} -``` - -**Result**: When you press W, the ball accelerates forward. - -**Important**: Forces are applied every frame while the key is pressed. The physics system automatically integrates forces into velocity and position. - ---- - -## Understanding Forces vs. Direct Movement - -### Direct Movement (Transform System) - -```swift path=null start=null -// Immediate, precise movement -translateBy(entityId: entity, delta: SIMD3(0, 0, speed * deltaTime)) -``` - -✅ Precise control -✅ No physics overhead -❌ No inertia or momentum -❌ Doesn't interact with physics - -### Force-Based Movement (Physics System) - -```swift path=null start=null -// Gradual acceleration with momentum -applyForce(entityId: entity, force: SIMD3(0, 0, 5.0)) -``` - -✅ Realistic inertia -✅ Physics interactions -✅ Momentum and deceleration -❌ Less precise -❌ Requires tuning - ---- - -## Step 5: Use the Steering System - -For easier physics-based movement, use the Steering System: - -### Steer to a Target Position - -```swift path=null start=null -class GameScene { - var player: EntityID! - let maxSpeed: Float = 5.0 - - init() { - // ... setup code ... - player = findEntity(name: "Player") - - setEntityKinetics(entityId: player) // Ignore this if you linked kinetic component through the editor - setMass(entityId: player, mass: 1.0) - setGravityScale(entityId: player, gravityScale: 0.0) // No gravity for top-down - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - let targetPosition = SIMD3(10.0, 0.0, 5.0) - steerSeek(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) - } -} -``` - -**Result**: The entity smoothly accelerates toward the target position. - ---- - -## Steering System Examples - -### Steer with WASD Keys - -The easiest way to add physics-based movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let maxSpeed: Float = 5.0 - - init() { - // ... setup code ... - InputSystem.shared.registerKeyboardEvents() - - player = findEntity(name: "Player") - setEntityKinetics(entityId: player) // Ignore this if you linked kinetic component through the editor - setMass(entityId: player, mass: 1.0) - setGravityScale(entityId: player, gravityScale: 0.0) - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Automatic WASD steering - steerWithWASD(entityId: player, maxSpeed: maxSpeed, deltaTime: deltaTime) - } -} -``` - -**Result**: WASD keys apply forces in the corresponding directions with smooth acceleration/deceleration. - -### Flee from a Threat - -```swift path=null start=null -let threatPosition = SIMD3(0.0, 0.0, 0.0) -steerFlee(entityId: player, threatPosition: threatPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Follow a Path - -```swift path=null start=null -let waypoints = [ - SIMD3(0, 0, 0), - SIMD3(5, 0, 0), - SIMD3(5, 0, 5), - SIMD3(0, 0, 5) -] -steerFollowPath(entityId: player, path: waypoints, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Pursue a Moving Target - -```swift path=null start=null -let enemy = findEntity(name: "Enemy") -steerPursuit(entityId: player, targetEntity: enemy!, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Avoid Obstacles - -```swift path=null start=null -let obstacles = [ - findEntity(name: "Rock1")!, - findEntity(name: "Rock2")!, - findEntity(name: "Tree1")! -] -steerAvoidObstacles(entityId: player, obstacles: obstacles, avoidanceRadius: 2.0, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Arrive at Target (Slowing Down) - -```swift path=null start=null -let targetPosition = SIMD3(10.0, 0.0, 5.0) -steerArrive(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -**Difference from `steerSeek()`**: `steerArrive()` slows down as it approaches the target, preventing overshoot. - ---- - -## Combining Steering Behaviors - -You can use multiple steering behaviors together: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - // Steer toward target while avoiding obstacles - let targetPosition = SIMD3(10.0, 0.0, 5.0) - - steerSeek(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) - - let obstacles = [findEntity(name: "Rock1")!, findEntity(name: "Rock2")!] - steerAvoidObstacles(entityId: player, obstacles: obstacles, avoidanceRadius: 2.0, maxSpeed: maxSpeed, deltaTime: deltaTime) -} -``` - ---- - -## When to Use What? - -### Use Transform System (`translateBy`, `translateTo`) -- Precise, scripted movement -- UI elements or camera -- Platformer-style movement -- When you don't need physics interactions - -### Use Physics System (`applyForce`) -- Projectiles (bullets, grenades) -- Vehicles with custom physics -- When you need low-level control - -### Use Steering System (`steerSeek`, `steerWithWASD`, etc.) -- Character movement in top-down or 3D games -- AI pathfinding and behaviors -- When you want smooth, realistic movement with minimal code - ---- - -## Summary - -You've learned: - -✅ `setEntityKinetics()` - Enable physics on entities -✅ `setMass()` and `setGravityScale()` - Configure physics properties -✅ `applyForce()` - Apply custom forces -✅ `steerWithWASD()` - Easy WASD physics movement -✅ Steering behaviors - Seek, flee, pursue, avoid, arrive -✅ When to use Transform vs. Physics vs. Steering - ---- - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/03-EditorOverview.md b/website/versioned_docs/version-0.10.10/03-Game Development/03-EditorOverview.md deleted file mode 100644 index 191fd6b5f..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/03-EditorOverview.md +++ /dev/null @@ -1,92 +0,0 @@ -# Editor Overview - -The editor is the primary environment for building games with the Untold Engine. - -It is designed to be **explicit, predictable, and tightly integrated** with the engine runtime. -What you see in the editor reflects what runs in the game. - -This page provides a **user-facing overview** of the editor for game developers. - -![editorbottomshot](../images/Editor/EditorBottomShot.png) - ---- - -## Purpose of the Editor - -The editor allows you to: - -- Create and manage scenes -- Inspect and modify entities -- Write and iterate on gameplay scripts -- Manage assets -- Preview runtime behavior in real time - -The editor is not a separate simulation layer — it runs directly on top of the engine runtime. - ---- - -## Core Editor Views - -The editor is composed of several focused views, each with a clear responsibility. - ---- - -### Scene View - -The Scene View is where you visually interact with the world. - -You can: -- Navigate the scene camera -- Select entities -- Translate, rotate, and scale entities -- Preview lighting and scene composition - -This view reflects the engine’s real transform and rendering state. - -![editormainshot](../images/Editor/EditorMainShot.png) - ---- - -### Scene Hierarchy View - -The Scene Hierarchy shows all entities in the current scene. - -It allows you to: -- View parent–child relationships -- Select entities -- Organize scene structure - -The hierarchy mirrors the engine’s internal scene graph. - -![editorscenegraphview](../images/Editor/EditorScenegraphView.png) - ---- - -### Inspector View - -The Inspector displays detailed information about the selected entity. - -From the Inspector, you can: -- View and modify components -- Adjust transforms and properties -- Attach scripts and assets - -Changes made in the Inspector apply directly to the runtime state. - -![editorinspectorview](../images/Editor/EditorInspectorView.png) - ---- - -### Asset Browser View - -The Asset Browser provides access to project assets such as: - -- Models -- Textures -- Scripts -- Scenes - -Assets imported through the editor are organized and made available to the engine automatically. - -![editorassetbrowserview](../images/Editor/EditorAssetBrowserView-alt.png) - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/01-Overview.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/01-Overview.md deleted file mode 100644 index e2ab16284..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/01-Overview.md +++ /dev/null @@ -1,100 +0,0 @@ -# Overview - -⚠️ **EXPERIMENTAL FEATURE** - -This section documents the **UntoldEngine Scripting (USC)** language - an experimental scripting system that is **currently in development**. - ---- - -## Current Status - -USC scripting is in **early experimental phases** and is **not recommended for production use**. - -For game development, we strongly recommend using **Swift** and **Xcode** (see parent section: [Game Development Overview](../01-Overview.md)). - ---- - -## What is USC? - -USC (Untold Scripting Language) is a custom scripting language designed for the UntoldEngine. It aims to provide: - -- Simple C-like syntax -- Entity-component scripting -- Scene-based game logic - -However, the language is still evolving, and **its future direction is under evaluation**. - ---- - -## Why USC? - -Originally, USC was designed to provide: - -1. **Lower barrier to entry** - Simpler syntax than Swift -2. **Scene-embedded scripts** - Scripts attached to entities in the editor -3. **Hot-reload** - Faster iteration for non-compiled changes - -However, **Swift + Xcode** has proven to be more practical for most use cases: - -- Mature tooling (debugging, profiling, autocomplete) -- Type safety and compile-time errors -- Familiar to Apple platform developers -- Better performance - ---- - -## Should You Use USC? - -**No, not yet.** Here's why: - -❌ **Incomplete** - Many features are missing or unstable -❌ **Underdocumented** - Documentation is sparse and may be outdated -❌ **Limited tooling** - No debugger, no IDE support -❌ **Breaking changes** - Syntax and behavior may change without notice -❌ **Unoptimized** - Performance is not production-ready - -Use **Swift** for all game development until USC reaches a stable release. - ---- - -## When Will USC Be Ready? - -USC is in **exploratory development**. We are evaluating: - -- Whether USC provides enough value over Swift -- What the ideal syntax and feature set should be -- How to integrate it seamlessly with Xcode workflows - -There is **no timeline** for USC becoming production-ready. - ---- - -## Exploring USC Anyway? - -If you want to experiment with USC despite the warnings: - -1. Read the **[USC Language Reference](./02-USC/01_Introduction.md)** -2. Try the **[USC Tutorials](./03-Tutorials/00_HelloWorld.md)** -3. Understand that **nothing is guaranteed to work** - -Do NOT use USC for: -- Production games -- Serious projects -- Teaching materials -- Anything you want to maintain long-term - ---- - -## Feedback - -If you experiment with USC, we welcome feedback: - -- What do you like about it? -- What frustrates you? -- Does it solve problems Swift doesn't? -- Would you use it if it were stable? - -Your feedback helps us decide USC's future direction. - ---- - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md deleted file mode 100644 index 06d6abd30..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -id: usc-scripting-introducction -title: Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -# Introduction to USC - -USC (Untold Script Core) is the scripting system used to define **gameplay behavior** in Untold Engine. - -USC is designed to be: -- Explicit -- Predictable -- Easy to read and reason about - -It focuses on *what your game does*, not how the engine is implemented. - ---- - -## What USC Is - -USC is a **script-based API** that allows you to attach behavior to entities. - -You use USC to: -- Respond to input -- Move entities -- Control cameras -- Apply forces -- Drive simple gameplay logic - -USC scripts describe *behavior over time*. - ---- - -## What USC Is Not - -USC is **not**: -- A general-purpose programming language -- A replacement for engine code -- A system for complex algorithms or heavy math - -If you need low-level control or complex systems, those belong in the engine itself. - -USC intentionally keeps the surface area small. - ---- - -## Why USC Exists - -Traditional game engines often expose: -- Large, complex APIs -- Implicit behavior -- Hidden update order - -USC takes a different approach. - -It is designed so that: -- Scripts read top-to-bottom -- Behavior is explicit -- There is no hidden execution magic - -This makes gameplay logic easier to understand, debug, and maintain. - ---- - -## How USC Fits into Untold Engine - -USC sits **above the engine** and **below the editor**. - -- The engine provides systems and data -- USC expresses gameplay intent -- The editor helps you attach and manage scripts - -USC does not replace the engine — it builds on top of it. - ---- - -## The Script Lifecycle - -A USC script runs as part of the engine update loop. - -At a high level: -1. The engine updates input -2. USC scripts are evaluated -3. The engine applies the results - -Scripts are evaluated every frame while the game is running. - -You do not manually manage update loops. - ---- - -## Working with Entities - -USC scripts operate on **entities**. - -A script is attached to an entity and can: -- Read entity state -- Modify transforms -- Interact with components -- Respond to input - -Scripts do not create entities or systems — they control behavior. - ---- - -## Example Use Cases - -USC is ideal for: -- Player movement -- Camera follow logic -- Simple physics interaction -- Trigger-based events -- Prototyping gameplay ideas - -If something feels *game-specific*, it likely belongs in USC. - ---- - -## Design Philosophy - -USC follows a few guiding principles: - -- **Explicit over implicit** - No hidden behavior. - -- **Readable over clever** - Scripts should be easy to understand at a glance. - -- **Stable over flexible** - Fewer features, fewer surprises. - -These principles are intentional. - - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/02_QuickStart.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/02_QuickStart.md deleted file mode 100644 index bb021ce36..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/02_QuickStart.md +++ /dev/null @@ -1,197 +0,0 @@ -# USC Scripting – Quick Start - -This tutorial will walk you through creating your first script in the Untold Engine, from setup to testing in Play mode. - -**What you'll build:** A cube that moves upward by changing its position every frame. - -**Time:** ~10 minutes - ---- - -## Step 1: Create Your First Script - -### 1.1 Configure Asset Path - -1. Open Untold Engine. -2. Go to **Asset Library → Set Asset Folder** -3. Create/Select your projects's asset directory. - -This is where your assets will be saved. Including your Scripts. - -![assetlibraryloupe](../../../images/Editor/EditorAssetLibraryLoupe.png) - -### 1.2 Create Your First Script - -1. In the editor toolbar, click **Scripts: Open in Xcode** (blue button). -2. Click the `+` button to create a new script. -3. Enter the script name: `MovingCube` -4. Xcode opens the Scripts package so you can edit the new file. - -When the script is created: -- The source file is added to your project -- Xcode shows a template like this: - -```swift -import Foundation -import UntoldEngine - -extension GenerateScripts { - static func generateMovingCube(to dir: URL) { - // Write your script here - } -} -``` - -You will edit everything in Xcode. - ---- - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateMovingCube(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateMovingCube) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - -## Step 3: Write USC Logic - -Replace the function with this complete script: - -```swift -extension GenerateScripts { - static func generateMovingCube(to dir: URL) { - let script = buildScript(name: "MovingCube") { s in - // Run every frame - s.onUpdate() - .getProperty(.position, as: "pos") - .setVariable("offset", to: simd_float3(x: 0.0, y: 0.1, z: 0.0)) - .addVec3("pos", "offset", as: "newPos") - .setProperty(.position, toVariable: "newPos") - } - - let outputPath = dir.appendingPathComponent("MovingCube.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MovingCube.uscript") - } -} -``` - ---- - -## Step 4: Build Scripts - -After editing scripts, build and run in Xcode so the engine can use the generated output. - -- In Xcode, press `Cmd+R` to run the GenerateScripts target and produce the `.uscript` files. (Optional: `Cmd+B` to check the build first.) -- Build and run output appears inside Xcode. - -> A successful build makes the script available in the Asset Library to attach to entities. - -**First build?** It may take 30-60 seconds to download dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Add an Entity to the scene - -1. In the editor, click on "+" in the scenegraph -2. Click on "Cube" -3. A cube will show up in the scene. Make sure to select it. - -## Step 6: Link script to entity - -4. Open the **Asset Library** -5. Under the Script category, look for your script `MovingCube` and double click on the `.uscript`. This will automatically link the script to the entity. -6. To verify, the Inspector View will show the newly added script linked to the entity. - -![editorassetbrowserscript](../../../images/Editor/EditorAssetBrowserScripts.png) - -The script is now active and will run according to the engine update loop. - ---- - -## Step 7: Test in Play Mode - -### 7.1 Run the Scene - -1. Click **Play** in the editor -2. Watch your entity move upward continuously! - -### 7.2 Stop Play Mode - -Click **Stop** when done testing. - ---- - -## Step 8: Test Hot Reload - Move Sideways - -Let's modify the script to move the cube sideways instead of upward, and test the hot reload feature. - -### 8.1 Modify the Offset - -Modify your script to change the offset direction: - -```swift -extension GenerateScripts { - static func generateMovingCube(to dir: URL) { - let script = buildScript(name: "MovingCube") { s in - s.onUpdate() - .getProperty(.position, as: "pos") - .setVariable("offset", to: simd_float3(x: 0.1, y: 0.0, z: 0.0)) // Changed to move sideways - .addVec3("pos", "offset", as: "newPos") - .setProperty(.position, toVariable: "newPos") - } - - let outputPath = dir.appendingPathComponent("MovingCube.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MovingCube.uscript") - } -} -``` - -**What changed:** The offset is now `(0.1, 0.0, 0.0)` instead of `(0.0, 0.1, 0.0)`, so the cube will move along the X-axis (sideways) instead of the Y-axis (upward). - -### 8.2 Rebuild - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target. -2. Wait for "✅ MovingCube.uscript" in the Xcode run output - -### 8.3 Hot Reload - -Back in the editor: -1. With your entity selected -2. Click **"Reload"** (orange button) in the Script Component Inspector -3. Click **Play** to test the changes -4. The cube should now move sideways instead of upward! - -![scriptreload](../../../images/Editor/ScriptReload.png) - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md deleted file mode 100644 index 87ff77797..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md +++ /dev/null @@ -1,108 +0,0 @@ -# USC Scripting Workflow - -This document describes the **end-to-end workflow** for developing USC scripts in the Untold Engine. - -USC scripts are authored in Xcode. The Untold Editor coordinates assets and playback but does not include a built-in script editor. - ---- - -## Overview - -The USC workflow follows a clear sequence: - -1. Author scripts -2. Register scripts for generation -3. Build scripts -4. Attach scripts to entities -5. Iterate using hot reload - -The editor is the central coordination point for this process, while Xcode is where you author scripts. - ---- - -## Primary Editing Surface - -USC scripts are written in Xcode. Use **Scripts: Open in Xcode** to open the Scripts package; the Untold Editor does not provide an in-editor script editor. - -Xcode provides: -- Direct access to script source files -- Editing of generation entry points -- Build/run feedback and diagnostics - ---- - -## Script Authoring - -Scripts are written as Swift source files using the USC DSL. - -Responsibilities: -- Express gameplay behavior -- React to input and engine state -- Remain independent of engine internals - -Scripts do not: -- Manage entities directly -- Access low-level engine systems -- Perform rendering or physics logic - ---- - -## Script Registration - -All USC scripts are registered through `GenerateScripts.swift`. - -This file: -- Defines which scripts are generated -- Acts as a single source of truth for scripting output -- Is edited directly in Xcode - -Unregistered scripts are ignored by the engine. - ---- - -## Script Generation and Build - -Building and running the GenerateScripts target: -- Runs the USC generation pipeline -- Produces engine-consumable output -- Reports errors and warnings in Xcode - -Builds are explicit and controlled. - ---- - -## Runtime Attachment - -After a successful build: -- Scripts appear as selectable components -- Scripts can be attached to entities via the Inspector -- Multiple entities may share the same script - -The engine controls execution timing. - ---- - -## Hot Reload and Iteration - -USC supports rapid iteration: - -- Edit script -- Build scripts -- Observe changes immediately - -This workflow encourages experimentation and fast feedback. - ---- - -## Design Intent - -The USC workflow is intentionally: - -- Xcode-first for authoring -- Editor-coordinated for playback -- Explicit -- Predictable -- Easy to debug - -It avoids hidden build steps and implicit execution. - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md deleted file mode 100644 index e1744538d..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md +++ /dev/null @@ -1,167 +0,0 @@ -# USC Scripts - -This document explains **what a USC script is**, how it fits into the Untold Engine, and the rules that govern its behavior. - -It is intended to give you a **clear mental model** before diving into APIs, examples, or workflows. - -This page does **not** describe how to write scripts step by step. -It explains what scripts *represent* and *why they are designed the way they are*. - ---- - -## What Is a USC Script? - -A **USC script** is a declarative description of gameplay behavior. - -You write scripts in Swift using a constrained, fluent DSL, and the engine executes those scripts at runtime as part of its update loop. - -A script expresses **intent**, not implementation details. - -Examples of intent: -- Move an entity when input is received -- Apply forces in response to events -- Adjust camera behavior over time - -The engine owns *how* those intentions are executed. - ---- - -## Scripts and Entities - -USC scripts are **attached to entities**. - -- A script always operates in the context of the entity it is attached to -- Scripts do not own entities -- Scripts do not create or destroy entities - -Multiple entities may share the same script definition, each with its own execution context and script-local state. - ---- - -## Execution Model - -Scripts participate in the engine’s execution model. - -Key characteristics: - -- Scripts run when their declared events are triggered -- The engine controls execution order and timing -- Scripts do not manage threads or scheduling -- Execution is deterministic and engine-driven - -A script never decides *when* it runs — it only declares *what should happen* when it does. - ---- - -## Script Lifecycle - -At a high level, a USC script goes through the following lifecycle: - -1. Script is authored -2. Script is registered and generated -3. Script is attached to an entity -4. Script becomes active -5. Script executes in response to events -6. Script stops executing when detached or when the entity is destroyed - -Lifecycle transitions are managed by the engine. - ---- - -## Events and Entry Points - -Scripts are organized around **events**, also called entry points. - -Examples include: -- Startup events -- Per-frame updates -- Custom or engine-driven events - -Each event defines **when a block of instructions executes**. - -Scripts may contain multiple event handlers, each expressing a different behavior. - ---- - -## Script Context - -Every script executes with a **context** provided by the engine. - -The context gives controlled access to: -- The attached entity’s properties -- Script-defined variables -- Engine-provided values (such as delta time or input state) - -Scripts never access engine state directly — all access flows through this context. - ---- - -## State and Variables - -Scripts may store local state using variables. - -- Variables are scoped to the script instance -- Each entity gets its own variable set -- Variables persist across executions unless reset - -Script variables are designed for **lightweight gameplay state**, not long-term data storage. - ---- - -## What Scripts Can Do - -USC scripts are designed to express common gameplay behaviors, such as: - -- Reading input -- Modifying transforms -- Applying forces or impulses -- Steering entities -- Controlling cameras -- Triggering animations -- Reacting to events - -Scripts focus on *what should happen*, not *how systems work internally*. - ---- - -## What Scripts Cannot Do - -USC scripts intentionally **cannot**: - -- Create or destroy entities -- Access rendering pipelines or GPU resources -- Manage memory -- Spawn threads -- Call arbitrary engine internals -- Control execution order or threading - -These constraints are deliberate and central to USC’s design. - ---- - -## Design Intent - -USC exists to provide: - -- Predictable gameplay behavior -- Clear ownership of state -- Safe boundaries between game logic and engine internals -- Fast iteration and easy debugging - -By limiting what scripts can do, the engine remains stable and behavior remains easy to reason about. - ---- - -## Relationship to the API Reference - -This document explains **what a script is**. - -For detailed information about: -- Available events -- Instructions and commands -- Math operations -- Input handling -- Physics and camera helpers - -See **USC → API Reference**. - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/05_APIReference.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/05_APIReference.md deleted file mode 100644 index ac00431a7..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/02-USC/05_APIReference.md +++ /dev/null @@ -1,624 +0,0 @@ ---- -id: usc-scripting-api -title: USC Scripting API Reference -sidebar_label: API Reference -sidebar_position: 10 ---- - -# Untold Engine – USC Scripting API Reference - -USC (Untold Script Core) is the scripting system inside the Untold Engine. -You write scripts in Swift using a fluent DSL, and the engine executes them at runtime. - -This reference provides the complete API surface for building gameplay scripts. - ---- - -## 1. Script Lifecycle - -### Building and Exporting Scripts - -USC provides two ways to create scripts: - -**buildScript()** - Creates a script in memory: -```swift -let script = buildScript(name: "MyScript") { s in - s.onUpdate() - .log("Running every frame") -} -``` - -**saveUSCScript()** - Saves a script to a `.uscript` file: -```swift -let outputPath = dir.appendingPathComponent("MyScript.uscript") -try? saveUSCScript(script, to: outputPath) -``` - -**Typical Pattern** - Build then save: -```swift -extension GenerateScripts { - static func generateMyScript(to dir: URL) { - let script = buildScript(name: "MyScript") { s in - s.onUpdate() - .log("Running every frame") - } - - let outputPath = dir.appendingPathComponent("MyScript.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MyScript.uscript") - } -} -``` - -### TriggerType (Optional) - -**Default:** `.perFrame` (runs every frame) - -You only need to specify `triggerType` if you want something other than the default: - -**When to override:** - -- **`.event`** - For event-driven scripts (collision handlers, triggers) - ```swift - let script = buildScript(name: "DoorTrigger", triggerType: .event) { s in - s.onCollision(tag: "Player") // Coming soon - collision system not yet implemented - .log("Door opened!") - } - ``` - -- **`.manual`** - For manually controlled scripts (cutscenes, special sequences) - ```swift - let script = buildScript(name: "Cutscene", triggerType: .manual) { s in - s.onEvent("StartCutscene") - .log("Cutscene playing...") - } - ``` - -**Most scripts don't need to specify this** - the default `.perFrame` works for continuous behaviors like movement and AI. - -### ExecutionMode (Optional) - -**Default:** `.auto` (engine manages execution) - -You rarely need to override this. Only specify `executionMode` for advanced scenarios: - -- **`.interpreted`** - Force interpreter-based execution (debugging, special cases) - ```swift - let script = buildScript(name: "DebugScript", executionMode: .interpreted) { s in - s.onUpdate() - .log("Debug mode") - } - ``` - -**Most scripts should use the default** `.auto` mode. - ---- - -## 2. Events (Entry Points) - -Events define when code blocks execute. Chain commands after each event: - -**onStart()** - Runs once when the entity starts (like Awake/Start in Unity): -```swift -s.onStart() - .setVariable("health", to: 100.0) - .setVariable("speed", to: 5.0) - .log("Entity initialized") -``` - -**onUpdate()** - Runs every frame (like Update in Unity): -```swift -s.onUpdate() - .getProperty(.position, as: "pos") - .log("Current position") -``` - -**onCollision(tag:)** - Runs when colliding with tagged entities: -> ⚠️ **Coming Soon** - The collision system is not yet implemented. This API is planned for a future release. - -```swift -s.onCollision(tag: "Enemy") - .log("Hit an enemy!") - .setVariable("health", to: 0.0) -``` - -**onEvent(_:)** - Runs when a custom event is fired: -```swift -s.onEvent("PowerUpCollected") - .setVariable("speed", to: 10.0) - .log("Speed boost activated") -``` - -### Multiple Event Handlers - -You can define multiple event handlers in one script: -```swift -let script = buildScript(name: "Player") { s in - s.onStart() - .setVariable("score", to: 0.0) - - s.onUpdate() - .getProperty(.position, as: "pos") - - // Coming soon - collision system not yet implemented - s.onCollision(tag: "Coin") - .addFloat("score", 1.0, as: "score") -} - -let outputPath = dir.appendingPathComponent("Player.uscript") -try? saveUSCScript(script, to: outputPath) -``` - -### Interpreter Execution (Advanced) - -For `.interpreted` execution mode: -```swift -interpreter.execute(script: script, context: context, forEvent: "OnStart") -interpreter.execute(script: script, context: context, forEvent: nil) // onUpdate -``` - ---- - -## 3. Script Context - -Every script runs with a **context** that provides access to: -- **Entity properties** (position, scale, velocity, acceleration, lights) -- **Script variables** (custom data you store) -- **Engine state** (delta time, input, etc.) - -You access the entity's properties using `.getProperty()` and `.setProperty()`. - -**Available at Runtime:** -- Current entity's transform (position, scale) -- Physics properties (velocity, acceleration, mass) -- Rendering properties (color, intensity for lights) -- All script variables you've defined - -**Example:** -```swift -s.onUpdate() - .getProperty(.position, as: "currentPos") // Read from entity - .getProperty(.velocity, as: "currentVel") // Read physics - .setVariable("myCustomData", to: 42.0) // Store in script - .setProperty(.position, toVariable: "newPos") // Write to entity -``` - ---- - -## 4. Flow Control - -**Conditionals** - Execute code based on comparisons: -```swift -s.ifCondition( - lhs: .variableRef("speed"), - .greater, - rhs: .float(10.0) -) { nested in - nested.log("Too fast!") - nested.setVariable("speed", to: 10.0) -} -``` - -**Available operators:** -- `.greater`, `.less` -- `.equal`, `.notEqual` -- `.lessOrEqual`, `.greaterOrEqual` - -**Convenience conditionals:** -```swift -s.ifGreater("speed", than: 10.0) { nested in - nested.log("Too fast!") -} - -s.ifLess("health", than: 20.0) { nested in - nested.log("Low health!") -} - -s.ifEqual("state", to: 1.0) { nested in - nested.log("State is 1") -} -``` - -**Organizing math-heavy code with `.math { ... }`:** -```swift -s.onUpdate() - .math { m in - m.getProperty(.velocity, as: "vel") - m.lengthVec3("vel", as: "speed") - m.ifGreater("speed", than: 10) { n in - n.normalizeVec3("vel", as: "dir") - n.scaleVec3("dir", literal: 10, as: "clampedVel") - n.setProperty(.velocity, toVariable: "clampedVel") - } - } - .log("Velocity clamped if above 10") -``` - ---- - -## 5. Values & Variables - -**Value Types** - USC supports these data types: -```swift -enum Value { - case float(Float) // Single number - case vec3(x: Float, y: Float, z: Float) // 3D vector - case string(String) // Text - case bool(Bool) // True/false - case variableRef(String) // Reference to a variable -} -``` - -**Setting Variables:** -```swift -s.setVariable("speed", to: 5.0) -s.setVariable("direction", to: simd_float3(x: 1, y: 0, z: 0)) -s.setVariable("isActive", to: true) -s.setVariable("playerName", to: "Hero") -``` - -**Using Variable References:** -```swift -s.setVariable("maxSpeed", to: 10.0) -s.setVariable("currentSpeed", to: .variableRef("maxSpeed")) // Copy value -``` - ---- - -## 6. Engine Properties - -**Available Properties** - Read/write entity properties: -```swift -enum ScriptProperty: String { - // Transform - case position, scale - - // Physics - case velocity, acceleration, mass, angularVelocity - - // Rendering (lights) - case intensity, color - - // Engine time - case deltaTime -} -``` - -**Reading Properties:** -```swift -s.getProperty(.position, as: "pos") // Store position in "pos" variable -s.getProperty(.velocity, as: "vel") // Store velocity in "vel" variable -s.getProperty(.deltaTime, as: "dt") // Store frame delta time -``` - -**Writing Properties:** -```swift -s.setProperty(.position, toVariable: "newPos") // Set from variable -s.setProperty(.velocity, to: simd_float3(x: 0, y: 5, z: 0)) // Set from literal -s.setProperty(.angularVelocity, to: simd_float3(x: 0, y: 1, z: 0)) // Set spin (write-only today) -``` - -> Note: Rotation is controlled through `rotateTo` / `rotateBy` instructions. Reading rotation via `getProperty(.rotation, ...)` is not yet supported. - -**Complete Example:** -```swift -s.onUpdate() - .getProperty(.position, as: "currentPos") - .setVariable("offset", to: simd_float3(x: 0, y: 0.1, z: 0)) - .addVec3("currentPos", "offset", as: "newPos") - .setProperty(.position, toVariable: "newPos") // Move entity up -``` - ---- - -## 7. Math Operations - -**Float Math:** -```swift -s.addFloat("a", "b", as: "sum") // sum = a + b (two variables) -s.addFloat("a", literal: 5.0, as: "sum") // sum = a + 5 (variable + literal) -s.mulFloat("a", "b", as: "product") // product = a * b (two variables) -s.mulFloat("a", literal: 2.0, as: "product") // product = a * 2 (variable * literal) -``` - -**Vector Math:** -```swift -s.addVec3("v1", "v2", as: "sum") // sum = v1 + v2 -s.scaleVec3("dir", literal: 2.0, as: "scaled") // scaled = dir * 2.0 -s.scaleVec3("dir", by: "scale", as: "scaled") // scaled = dir * scale -s.lengthVec3("vec", as: "length") // length = magnitude of vec -s.normalizeVec3("vec", as: "unitVec") // normalized vec (zero-safe) -s.dotVec3("a", "b", as: "dot") // dot product -> float -s.crossVec3("a", "b", as: "cross") // cross product -> vec3 -s.lerpVec3(from: "a", to: "b", t: "t", as: "lerped") // linear interpolation -s.lerpFloat(from: "a", to: "b", t: "t", as: "out") // scalar lerp -s.reflectVec3("v", normal: "n", as: "reflected") // reflect v about normal -s.projectVec3("v", onto: "axis", as: "proj") // project v onto axis -s.angleBetweenVec3("a", "b", as: "angleDeg") // angle in degrees -s.clampFloat("speed", min: "minSpeed", max: "maxSpeed", as: "clampedSpeed") // bounds via vars -s.clampVec3("velocity", min: "minVel", max: "maxVel", as: "clampedVel") // component-wise -``` - -**Example - Calculate velocity:** -```swift -s.onUpdate() - .setVariable("direction", to: simd_float3(x: 1, y: 0, z: 0)) - .setVariable("speed", to: 5.0) - .scaleVec3("direction", by: "speed", as: "velocity") - .setProperty(.velocity, toVariable: "velocity") -``` - ---- - -## 8. Built-in Behaviors (Steering, Camera, Physics) - -All behaviors are instruction helpers—no `callAction` or `ScriptArgKey`. - -**Steering** -```swift -// Seek toward a target and store the steering force -s.seek(targetPosition: .vec3(x: 10, y: 0, z: 0), - maxSpeed: .float(5.0), - result: "seekForce") - -s.steerSeek(targetPosition: .variableRef("targetPos"), - maxSpeed: .variableRef("maxSpeed"), - deltaTime: .variableRef("dt"), - turnSpeed: .variableRef("turnSpeed")) - -// Arrive with slowing radius -s.steerArrive(targetPosition: .variableRef("targetPos"), - maxSpeed: .variableRef("maxSpeed"), - slowingRadius: .variableRef("slowingRadius"), - deltaTime: .variableRef("dt"), - turnSpeed: .variableRef("turnSpeed")) - -// Evade a threat: compute force into result or apply directly -s.steerEvade(threatEntity: .string("Enemy"), - maxSpeed: .float(6.0), - result: "evadeForce") // omit result to apply immediately - -// Pursuit -s.steerPursuit(targetEntity: .variableRef("targetName"), - maxSpeed: .variableRef("maxSpeed"), - deltaTime: .variableRef("dt"), - turnSpeed: .variableRef("turnSpeed")) - -// Align orientation to current velocity (smooth) -s.alignOrientation(deltaTime: .float(0.016), - turnSpeed: .float(1.0)) -``` - -**Camera** -```swift -// Snap camera to a position -s.cameraMoveTo(.vec3(x: 0, y: 3, z: -10)) - -// Look at a target -s.cameraLookAt(eye: .vec3(x: 0, y: 3, z: -8), - target: .variableRef("lookTarget"), - up: .vec3(x: 0, y: 1, z: 0)) - -// Follow a target with smoothing -s.cameraFollow(target: .string("Player"), - offset: .vec3(x: 0, y: 3, z: -6), - smoothFactor: .float(5.0), - deltaTime: .float(0.016)) - -// WASDQE fly camera -s.cameraMoveWithInput(speedVar: "moveSpeed", - deltaTimeVar: "dt", - wVar: "wPressed", - aVar: "aPressed", - sVar: "sPressed", - dVar: "dPressed", - qVar: "qPressed", - eVar: "ePressed") - -// Orbit a target entity (auto look-at) -s.cameraOrbitTarget(target: .string("Boss"), - radius: .float(12.0), - speed: .float(1.5), - deltaTime: .float(0.016), - offsetY: .float(1.5)) -``` - -**Physics** -```swift -// Impulse -s.applyLinearImpulse(direction: .vec3(x: 1, y: 0, z: 0), - magnitude: .float(5.0)) - -// Continuous world force -s.applyWorldForce(direction: .vec3(x: 0, y: 1, z: 0), - magnitude: .float(3.0)) - -// Velocity control -s.setLinearVelocity(.vec3(x: 0, y: 0, z: 5)) -s.addLinearVelocity(.variableRef("deltaVel")) -s.clampLinearSpeed(min: .float(2.0), max: .float(8.0)) - -// Angular control -s.applyAngularImpulse(axis: .vec3(x: 0, y: 1, z: 0), magnitude: .float(2.0)) -s.clampAngularSpeed(max: .float(5.0)) -s.applyAngularDamping(damping: .float(0.6), deltaTime: .float(0.016)) -``` - ---- - -## 9. Transform & Physics Helpers - -**Transform:** -```swift -s.translateTo(x: 1, y: 2, z: 3) // Set absolute position -s.translateTo(simd_float3(x: 1, y: 2, z: 3)) // Alternative syntax -s.translateBy(x: 0.1, y: 0, z: 0) // Move relative -s.translateBy(simd_float3(x: 0.1, y: 0, z: 0)) // Alternative syntax -s.rotateTo(degrees: 45, axis: simd_float3(x: 0, y: 1, z: 0)) // Set absolute rotation -s.rotateBy(degrees: 45, axis: simd_float3(x: 0, y: 1, z: 0)) // Rotate relative -s.lookAt("targetEntityName") // Face another entity -``` - -**Physics - Force & Torque:** -```swift -s.applyForce(force: simd_float3(x: 0, y: 10, z: 0)) // Apply linear force -s.applyMoment(force: simd_float3(x: 5, y: 0, z: 0), at: simd_float3(x: 1, y: 0, z: 0)) // Apply torque at point -``` - -**Physics - Velocity Control:** -```swift -s.clearVelocity() // Stop linear movement instantly -s.clearAngularVelocity() // Stop rotation instantly -s.clearForces() // Clear accumulated forces -``` - -**Physics - Gravity & Pause:** -```swift -s.setGravityScale(0.5) // Half gravity (0 = no gravity, 1 = normal, 2 = double) -s.pausePhysicsComponent(isPaused: true) // Pause/unpause physics simulation -``` - -**Example - Jump mechanic:** -```swift -s.onEvent("Jump") - .getProperty(.velocity, as: "currentVel") - .setVariable("jumpForce", to: simd_float3(x: 0, y: 15, z: 0)) - .addVec3("currentVel", "jumpForce", as: "newVel") - .setProperty(.velocity, toVariable: "newVel") -``` - -**Example - Reset physics:** -```swift -s.onEvent("Respawn") - .clearVelocity() // Stop all movement - .clearAngularVelocity() // Stop all rotation - .clearForces() // Clear force accumulation - .translateTo(simd_float3(x: 0, y: 5, z: 0)) // Move to spawn point -``` - -**Example - Apply torque to spin:** -```swift -s.onUpdate() - .ifKeyPressed("R") { n in - // Apply torque at the right edge to spin left - n.applyMoment(force: simd_float3(x: 0, y: 10, z: 0), at: simd_float3(x: 1, y: 0, z: 0)) - } -``` - -**Animation:** -```swift -s.playAnimation("Walk", loop: true) // Play looping animation -s.playAnimation("Jump", loop: false) // Play once -s.stopAnimation() // Stop current animation -``` - ---- - -## 10. Input Conditions - -**Keyboard Input:** -```swift -s.ifKeyPressed("W") { nested in - nested.log("Forward") - nested.applyForce(force: simd_float3(x: 0, y: 0, z: -1)) -} - -s.ifKeyPressed("Space") { nested in - nested.log("Jump!") - nested.applyForce(force: simd_float3(x: 0, y: 10, z: 0)) -} -``` - -**Example - WASD movement:** -```swift -s.onUpdate() - .setVariable("moveSpeed", to: 5.0) - .ifKeyPressed("W") { n in - n.applyForce(force: simd_float3(x: 0, y: 0, z: -5)) - } - .ifKeyPressed("S") { n in - n.applyForce(force: simd_float3(x: 0, y: 0, z: 5)) - } - .ifKeyPressed("A") { n in - n.applyForce(force: simd_float3(x: -5, y: 0, z: 0)) - } - .ifKeyPressed("D") { n in - n.applyForce(force: simd_float3(x: 5, y: 0, z: 0)) - } -``` - ---- - -## 11. Logging & Debugging - -**Log Messages:** -```swift -s.log("Debug message") // Simple message -s.log("Player health: 100") // Can include values -s.logValue("velocity", value: .variableRef("vel")) // Log a labeled variable -s.logValue("spawnPoint", value: .vec3(x: 0, y: 1, z: 2)) // Log a literal with a label -``` - -**Debug Variables:** -```swift -s.onUpdate() - .getProperty(.position, as: "pos") - .log("Position updated") // Track when events occur -``` - ---- - -## 12. Best Practices - -### Use enums instead of raw strings -✅ **Good:** -```swift -s.getProperty(.position, as: "pos") -s.setProperty(.velocity, toVariable: "vel") -``` - -❌ **Avoid:** -```swift -s.getProperty("position", as: "pos") // String-based, no autocomplete -``` - -### Variable Naming -- Use descriptive names: `"playerHealth"` not `"h"` -- Consistent naming: `"currentPos"`, `"targetPos"`, `"newPos"` -- Avoid conflicts with property names - -### Performance -- Use `.perFrame` for continuous behaviors (movement, AI) -- Use `.event` for one-time triggers (collision, pickups) -- Minimize operations in `onUpdate()` when possible - -### Script Organization -```swift -let script = buildScript(name: "Enemy") { s in - // Initialization - s.onStart() - .setVariable("health", to: 100.0) - .setVariable("speed", to: 3.0) - - // Main loop - s.onUpdate() - .setVariable("maxSpeed", to: 5.0) - .seek(targetPosition: .string("Player"), - maxSpeed: .variableRef("maxSpeed"), - result: "steer") - .applyForce(force: .variableRef("steer")) - - // Event handlers (collision system coming soon) - s.onCollision(tag: "Bullet") - .subtractFloat("health", 10.0, as: "health") -} - -let outputPath = dir.appendingPathComponent("Enemy.uscript") -try? saveUSCScript(script, to: outputPath) -``` - -### Debugging Tips -- Add `.log()` statements to trace execution -- Use meaningful variable names for debugging -- Test scripts incrementally -- Check console output in Play mode diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/00_HelloWorld.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/00_HelloWorld.md deleted file mode 100644 index d2300510a..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/00_HelloWorld.md +++ /dev/null @@ -1,228 +0,0 @@ -# "Hello World" - Your First Script - -**What you'll learn:** -- Creating a script in Untold Engine Studio -- Using `onStart()` and `onUpdate()` lifecycle events -- Logging messages for debugging -- Attaching scripts to entities - -**Time:** ~5 minutes - ---- - -## What We're Building - -A simple script that: -1. Logs "Hello, Untold Engine!" when the entity starts -2. Logs "Script running..." every frame during Play mode - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `HelloWorld` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateHelloWorld(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateHelloWorld(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateHelloWorld) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateHelloWorld(to dir: URL) { - let script = buildScript(name: "HelloWorld") { s in - // Runs once when entity starts - s.onStart() - .log("Hello, Untold Engine!") - .log("Script initialized successfully") - - // Runs every frame - s.onUpdate() - .log("Script running...") - } - - let outputPath = dir.appendingPathComponent("HelloWorld.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ HelloWorld.uscript") - } -} -``` - -### Understanding the Code - -**`buildScript(name:)`** - Creates a new script -- The name identifies the script in the editor - -**`onStart()`** - Lifecycle event that runs once -- Perfect for initialization -- Logs appear in the console when Play mode starts - -**`onUpdate()`** - Lifecycle event that runs every frame -- Use for continuous behaviors -- Be mindful of performance (runs 60+ times per second!) - -**`.log()`** - Outputs debug messages -- Messages appear in the editor's Console view -- Great for debugging and tracking execution - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ HelloWorld.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select any entity in your scene (create a cube if needed) -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `HelloWorld.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. Open the **Console** view (bottom panel) -3. You should see: - ``` - Hello, Untold Engine! - Script initialized successfully - Script running... - Script running... - Script running... - ... - ``` - -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- **"Hello, Untold Engine!"** appears once (from `onStart()`) -- **"Script running..."** appears continuously (from `onUpdate()`) - -⚠️ **Performance Note:** `onUpdate()` runs every frame! In a real game, avoid heavy logging in `onUpdate()`. This example is just for demonstration. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Change the Messages -```swift -s.onStart() - .log("Game initialized") - .log("Player ready!") -``` - -### Remove the Update Log -```swift -s.onUpdate() - // Remove .log() to avoid console spam -``` - -### Add Initialization Variables -```swift -s.onStart() - .setVariable("playerName", to: "Hero") - .setVariable("health", to: 100.0) - .log("Player initialized with 100 health") -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ How to create a script in Untold Engine Studio -✅ Using `onStart()` for initialization -✅ Using `onUpdate()` for per-frame logic -✅ Logging debug messages -✅ Building and attaching scripts to entities -✅ Testing scripts in Play mode - ---- diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md deleted file mode 100644 index 57cf2d07e..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md +++ /dev/null @@ -1,235 +0,0 @@ -# Move an Entity - Basic Translation - -**What you'll learn:** -- Moving an entity with `setProperty(.position)` -- Using `simd_float3` direction vectors -- Controlling speed with script variables -- Updating position every frame with `onUpdate()` - -**Time:** ~7 minutes - -**Prerequisites:** -- Untold Engine Studio installed and a scene with any entity (a cube works great) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple movement script that: -1. Moves an entity steadily along a chosen axis -2. Uses variables to control speed and direction -3. Logs a message when movement begins - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `MoveAnEntity` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateMoveAnEntity(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateMoveAnEntity(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateMoveAnEntity) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateMoveAnEntity(to dir: URL) { - let script = buildScript(name: "MoveAnEntity") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("moveSpeed", to: 0.05) // units per frame - .setVariable("direction", to: simd_float3(x: 0, y: 0, z: 1)) - .log("MoveAnEntity ready") - - // Runs every frame - s.onUpdate() - .getProperty(.position, as: "currentPos") - .scaleVec3("direction", by: "moveSpeed", as: "step") - .addVec3("currentPos", "step", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") - } - - let outputPath = dir.appendingPathComponent("MoveAnEntity.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MoveAnEntity.uscript") - } -} -``` - -### Understanding the Code - -**`moveSpeed` + `direction`** - Control how fast and where to move -- Speed is a scalar; direction is a vector (x, y, z) -- Change either without touching the rest of the script - -**`getProperty(.position, as:)`** - Reads the current position -- Positions are `simd_float3` values - -**`scaleVec3()` + `addVec3()`** - Build the new position -- Scales the direction by speed -- Adds it to the current position for smooth motion - -**`setProperty(.position, toVariable:)`** - Applies the updated position -- Runs every frame inside `onUpdate()` -- Keeps the movement continuous while Play mode runs - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ MoveAnEntity.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 4: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select any entity in your scene (a cube or platform) -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `MoveAnEntity.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 5: Test It! - -1. Click **Play** in the toolbar -2. Watch the entity move steadily along the +Z axis -3. Check the **Console** view to confirm: - ``` - MoveAnEntity ready - ``` -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- The entity moves every frame toward +Z -- Speed comes from `moveSpeed`; direction comes from `direction` -- To stop movement, disable the Script Component or set speed to 0 - -⚠️ **Placement Note:** If the entity is already near a boundary, lower the speed or adjust the start position to avoid moving out of view. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Move Upward Instead -```swift -.setVariable("direction", to: simd_float3(x: 0, y: 1, z: 0)) -``` - -### Slow or Fast Motion -```swift -.setVariable("moveSpeed", to: 0.01) // slower -// or -.setVariable("moveSpeed", to: 0.15) // faster -``` - -### Pause After a Distance -```swift -s.onUpdate() - .getProperty(.position, as: "currentPos") - .ifGreater("currentPos.z", than: 10.0) { n in - n.setVariable("moveSpeed", to: 0.0) // stop after z > 10 - } - .scaleVec3("direction", by: "moveSpeed", as: "step") - .addVec3("currentPos", "step", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Creating a movement script in Untold Engine Studio -✅ Using variables for speed and direction -✅ Updating position every frame with `onUpdate()` -✅ Building and attaching scripts to entities -✅ Testing motion in Play mode - ---- - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/02_Input/01_KeyboardMovement.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/02_Input/01_KeyboardMovement.md deleted file mode 100644 index 20a9f5800..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/02_Input/01_KeyboardMovement.md +++ /dev/null @@ -1,268 +0,0 @@ -# Keyboard Movement - WASD Motion - -**What you'll learn:** -- Reading keyboard input with `getKeyState()` -- Building movement vectors from multiple keys -- Combining input with `setProperty(.position)` -- Iterating quickly with Xcode builds - -**Time:** ~10 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity you want to move -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple controller that: -1. Moves with **WASD** -2. Supports diagonal movement by combining keys -3. Uses a configurable speed variable - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `KeyboardMovement` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateKeyboardMovement(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateKeyboardMovement(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateKeyboardMovement) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateKeyboardMovement(to dir: URL) { - let script = buildScript(name: "KeyboardMovement") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("moveSpeed", to: 0.2) // units per frame - .setVariable("forward", to: simd_float3(x: 0, y: 0, z: 1)) - .setVariable("right", to: simd_float3(x: 1, y: 0, z: 0)) - .log("KeyboardMovement ready") - - // Runs every frame - s.onUpdate() - .getProperty(.position, as: "currentPos") - .setVariable("offset", to: simd_float3(x: 0, y: 0, z: 0)) - - // Read keys - .getKeyState("w", as: "wPressed") - .getKeyState("s", as: "sPressed") - .getKeyState("a", as: "aPressed") - .getKeyState("d", as: "dPressed") - - // Forward (W) - .ifEqual("wPressed", to: true) { n in - n.scaleVec3("forward", by: "moveSpeed", as: "step") - n.addVec3("offset", "step", as: "offset") - } - // Backward (S) - .ifEqual("sPressed", to: true) { n in - n.scaleVec3("forward", by: "moveSpeed", as: "stepForward") - n.scaleVec3("stepForward", literal: -1.0, as: "stepBack") - n.addVec3("offset", "stepBack", as: "offset") - } - // Left (A) - .ifEqual("aPressed", to: true) { n in - n.scaleVec3("right", by: "moveSpeed", as: "stepRight") - n.scaleVec3("stepRight", literal: -1.0, as: "stepLeft") - n.addVec3("offset", "stepLeft", as: "offset") - } - // Right (D) - .ifEqual("dPressed", to: true) { n in - n.scaleVec3("right", by: "moveSpeed", as: "stepRight") - n.addVec3("offset", "stepRight", as: "offset") - } - - // Apply movement - .addVec3("currentPos", "offset", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") - } - - let outputPath = dir.appendingPathComponent("KeyboardMovement.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ KeyboardMovement.uscript") - } -} -``` - -### Understanding the Code - -**`getKeyState()`** - Reads whether a key is pressed this frame -- Lowercase strings like `"w"` match keyboard keys - -**Offset pattern** - Build movement from multiple keys -- Start with `offset = (0,0,0)` -- Add forward/back and left/right contributions -- Apply once per frame - -**`moveSpeed` variable** - Single place to tune speed -- Scale any direction by this value (or its negative) - -**Position-based control** - Directly updates `.position` -- No physics required for this example -- Works consistently every frame - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ KeyboardMovement.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select the entity you want to move -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `KeyboardMovement.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 5: Test It! - -1. Click **Play** in the toolbar -2. Press `W`, `A`, `S`, `D` to move the entity -3. Hold multiple keys (e.g., `W` + `D`) to move diagonally -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- Movement updates every frame based on current key states -- Diagonal movement combines offset vectors naturally -- Changing `moveSpeed` updates all directions at once - -⚠️ **Scene Scale Note:** If motion is too fast for your scene scale, drop `moveSpeed` to `0.05` and retest. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Adjustable Sprint -```swift -.getKeyState("lshift", as: "shiftPressed") -.setVariable("currentSpeed", to: 0.15) -.ifEqual("shiftPressed", to: true) { n in - n.setVariable("currentSpeed", to: 0.4) -} -// Use currentSpeed instead of moveSpeed when scaling direction -``` - -### Vertical Movement -```swift -.getKeyState("space", as: "upPressed") -.getKeyState("c", as: "downPressed") -.ifEqual("upPressed", to: true) { n in - n.addVec3("offset", simd_float3(x: 0, y: 0.2, z: 0), as: "offset") -} -.ifEqual("downPressed", to: true) { n in - n.addVec3("offset", simd_float3(x: 0, y: -0.2, z: 0), as: "offset") -} -``` - -### Normalize Diagonals -```swift -.normalizeVec3("offset", as: "offset") -.scaleVec3("offset", by: "moveSpeed", as: "offset") -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Reading keyboard input with `getKeyState()` -✅ Building movement vectors from multiple keys -✅ Updating position every frame with `onUpdate()` -✅ Building and attaching scripts to entities -✅ Testing interactive input in Play mode - ---- - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md deleted file mode 100644 index 6542e635f..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md +++ /dev/null @@ -1,241 +0,0 @@ -# Play an Animation - Single Clip - -**What you'll learn:** -- Playing an animation clip with `playAnimation()` -- Looping vs. one-shot playback -- Using variables to track the current clip -- Building and testing animations from Xcode - -**Time:** ~8 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity that has an **Animation Component** and at least one loaded clip (e.g., `idle`) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple animation script that: -1. Plays a single clip on start -2. Loops it continuously -3. Shows how to trigger a one-shot clip on demand - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `PlayAnimation` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generatePlayAnimation(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generatePlayAnimation(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generatePlayAnimation) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generatePlayAnimation(to dir: URL) { - let script = buildScript(name: "PlayAnimation") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("idleClip", to: "idle") // Must match the clip name in the Animation Component - .setVariable("currentClip", to: "idle") - .playAnimation("idle", loop: true) - .log("Playing idle animation (looping)") - - // Optional: trigger a one-shot animation with Space - s.onUpdate() - .getKeyState("space", as: "spacePressed") - .ifEqual("spacePressed", to: true) { n in - n.playAnimation("jump", loop: false) // Replace with your one-shot clip name - n.setVariable("currentClip", to: "jump") - n.log("Playing jump animation (one-shot)") - } - } - - let outputPath = dir.appendingPathComponent("PlayAnimation.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ PlayAnimation.uscript") - } -} -``` - -### Understanding the Code - -**`playAnimation(name, loop:)`** - Starts an animation by name -- `loop: true` repeats continuously (great for idle or walk) -- `loop: false` plays once (great for jump or attacks) - -**Clip names** - Must match exactly what you see in the Animation Component -- Case-sensitive -- If you rename a clip in the editor, update the script too - -**`currentClip` variable** - Tracks what is playing -- Useful when adding more conditions later - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ PlayAnimation.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select an entity that has an **Animation Component** -3. In the Asset Library, double click on the animations you want to add to the entity, i.e. `idle` and `jump` -4. In the Inspector panel, click **Add Component** → **Script Component** -5. In the Asset Browser, find `PlayAnimation.uscript` under Scripts/Generated -6. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. The entity should immediately play the `idle` clip on loop -3. Tap **Space** to trigger the one-shot clip (e.g., `jump`) -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- Looping clip runs continuously until another clip interrupts it -- One-shot clip plays once; if you need to return to idle automatically, add a timer or state check -- If nothing plays, verify the clip names match the Animation Component exactly - -⚠️ **Asset Note:** The script only calls animations that already exist on the entity’s Animation Component. Ensure clips are loaded and named correctly. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Start with a Different Clip -```swift -.setVariable("idleClip", to: "breathing_idle") -.playAnimation("breathing_idle", loop: true) -``` - -### Return to Idle After One-Shot -```swift -s.onUpdate() - .getKeyState("space", as: "spacePressed") - .ifEqual("spacePressed", to: true) { n in - n.playAnimation("jump", loop: false) - n.setVariable("currentClip", to: "jump") - } - // Simple timer to return to idle after ~1 second - .setVariable("returnTimer", to: 60.0) // frames at ~60 FPS - .ifGreater("returnTimer", than: 0.0) { n in - n.addFloat("returnTimer", literal: -1.0, as: "returnTimer") - } - .ifEqual("returnTimer", to: 0.0) { n in - n.playAnimation("idle", loop: true) - n.setVariable("currentClip", to: "idle") - } -``` - -### Add a Stop Button -```swift -.getKeyState("p", as: "pPressed") -.ifEqual("pPressed", to: true) { n in - n.stopAnimation() - n.log("Animation stopped") -} -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Playing animation clips with `playAnimation()` -✅ Looping vs. one-shot playback -✅ Tracking the current clip with script variables -✅ Building, attaching, and testing animation scripts - ---- - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/02_AnimationStateSwitch.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/02_AnimationStateSwitch.md deleted file mode 100644 index a43e42f11..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/02_AnimationStateSwitch.md +++ /dev/null @@ -1,265 +0,0 @@ -# Animation State Switch - Idle to Walk - -**What you'll learn:** -- Switching animations based on input -- Tracking current state to avoid restart flicker -- Using `playAnimation()` for looped clips -- Building and testing animations from Xcode - -**Time:** ~10 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity that has an **Animation Component** and two clips loaded (e.g., `idle`, `walk`) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -An animation controller that: -1. Plays `idle` when no movement keys are pressed -2. Switches to `walk` when any WASD key is pressed -3. Only changes clips when the state actually changes - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `AnimationStateSwitch` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateAnimationStateSwitch(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateAnimationStateSwitch(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateAnimationStateSwitch) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateAnimationStateSwitch(to dir: URL) { - let script = buildScript(name: "AnimationStateSwitch") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("currentAnim", to: "idle") - .playAnimation("idle", loop: true) - .log("AnimationStateSwitch ready - idle playing") - - // Runs every frame - s.onUpdate() - // Read movement keys - .getKeyState("w", as: "wPressed") - .getKeyState("a", as: "aPressed") - .getKeyState("s", as: "sPressed") - .getKeyState("d", as: "dPressed") - - // Determine if moving - .setVariable("isMoving", to: false) - .ifEqual("wPressed", to: true) { n in n.setVariable("isMoving", to: true) } - .ifEqual("aPressed", to: true) { n in n.setVariable("isMoving", to: true) } - .ifEqual("sPressed", to: true) { n in n.setVariable("isMoving", to: true) } - .ifEqual("dPressed", to: true) { n in n.setVariable("isMoving", to: true) } - - // Switch to walk when moving - .ifEqual("isMoving", to: true) { n in - n.ifCondition(lhs: .variableRef("currentAnim"), .notEqual, rhs: .string("walk")) { change in - change.playAnimation("walk", loop: true) - change.setVariable("currentAnim", to: "walk") - change.log("Switched to walk") - } - } - - // Switch back to idle when not moving - .ifEqual("isMoving", to: false) { n in - n.ifCondition(lhs: .variableRef("currentAnim"), .notEqual, rhs: .string("idle")) { change in - change.playAnimation("idle", loop: true) - change.setVariable("currentAnim", to: "idle") - change.log("Switched to idle") - } - } - } - - let outputPath = dir.appendingPathComponent("AnimationStateSwitch.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ AnimationStateSwitch.uscript") - } -} -``` - -### Understanding the Code - -**State tracking** - `currentAnim` stores what is playing -- Prevents restarting the same clip every frame -- Eliminates flicker when staying in the same state - -**Input gating** - `isMoving` becomes true if any WASD key is pressed -- One boolean drives all animation switching - -**`playAnimation()` only on change** - Uses a condition to switch -- Keeps transitions smooth -- Avoids repeating the same clip call - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ AnimationStateSwitch.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Add an entity to the scene. -3. In the Asset Library, double click on the animations you want to add to the entity, such as `idle` and `walk` -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `AnimationStateSwitch.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 5: Test It! - -1. Click **Play** in the toolbar -2. With no input, the entity plays `idle` -3. Press any of `W`, `A`, `S`, `D` to switch to `walk` -4. Release the keys to return to `idle` -5. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- Idle is the default and fallback state -- Walk plays only while movement keys are down -- No flicker because the script checks before switching - -⚠️ **Name Matching:** Clip names are case-sensitive. If your clips are named differently (e.g., `Idle`, `Walking`), update the script strings. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Add a Run State with Shift -```swift -.getKeyState("lshift", as: "shiftPressed") -.ifEqual("shiftPressed", to: true) { n in - n.ifEqual("isMoving", to: true) { run in - run.ifCondition(lhs: .variableRef("currentAnim"), .notEqual, rhs: .string("run")) { change in - change.playAnimation("run", loop: true) - change.setVariable("currentAnim", to: "run") - } - } -} -``` - -### Add a Jump One-Shot -```swift -.getKeyState("space", as: "jumpPressed") -.ifEqual("jumpPressed", to: true) { n in - n.playAnimation("jump", loop: false) - n.setVariable("currentAnim", to: "jump") -} -``` - -### Reset to Idle After a Delay -```swift -.setVariable("idleTimer", to: 30.0) // half-second at ~60 FPS -.ifEqual("isMoving", to: false) { n in - n.addFloat("idleTimer", literal: -1.0, as: "idleTimer") - n.ifEqual("idleTimer", to: 0.0) { back in - back.playAnimation("idle", loop: true) - back.setVariable("currentAnim", to: "idle") - } -} -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Switching animations based on player input -✅ Tracking the current clip to avoid restart flicker -✅ Using looped clips for idle/walk states -✅ Building, attaching, and testing animation controllers - ---- - diff --git a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/04_Physics/01_ApplyForce.md b/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/04_Physics/01_ApplyForce.md deleted file mode 100644 index ca5ba9cc7..000000000 --- a/website/versioned_docs/version-0.10.10/03-Game Development/04-Scripting-Experimental/03-Tutorials/04_Physics/01_ApplyForce.md +++ /dev/null @@ -1,232 +0,0 @@ -# Apply Force - Physics Push - -**What you'll learn:** -- Applying world-space forces with `applyWorldForce()` -- Controlling force direction and magnitude with variables -- Testing physics-driven motion from Xcode builds -- Building scripts in Xcode before playtesting - -**Time:** ~8 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity that has a Physics/Kinetic component -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A physics push that: -1. Applies a continuous upward force -2. Uses variables for direction and magnitude -3. Shows how to tweak force strength between builds - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `ApplyForce` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateApplyForce(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateApplyForce(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateApplyForce) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateApplyForce(to dir: URL) { - let script = buildScript(name: "ApplyForce") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("forceDir", to: simd_float3(x: 0, y: 1, z: 0)) - .setVariable("forceMagnitude", to: 0.2) - .log("ApplyForce ready") - - // Runs every frame - s.onUpdate() - .applyWorldForce( - direction: .variableRef("forceDir"), - magnitude: .variableRef("forceMagnitude") - ) - } - - let outputPath = dir.appendingPathComponent("ApplyForce.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ ApplyForce.uscript") - } -} -``` - -### Understanding the Code - -**`forceDir` + `forceMagnitude`** - Set the push direction and strength -- `(0, 1, 0)` pushes upward -- Reduce magnitude for gentle movement; increase for stronger lifts - -**`applyWorldForce()`** - Applies a force in world space -- Requires a Physics/Kinetic component on the entity -- Accumulates over time, so the object accelerates while the force runs - -**`onUpdate()`** - Applies force every frame -- Stop the push by setting magnitude to 0 or disabling the Script Component - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ ApplyForce.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Add an entity to the scene. -3. In the Inspector panel, click **Add Component** → **Kinetic Component** -4. In the Inspector panel, click **Add Component** → **Script Component** -5. In the Asset Browser, find `ApplyForce.uscript` under Scripts/Generated -6. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. Watch the entity accelerate upward as the force is applied each frame -3. Open the **Console** view to confirm: - ``` - ApplyForce ready - ``` -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- The entity accelerates while the force is active -- Direction is world-based; change `forceDir` to push sideways -- Lower gravity or heavier masses will change how fast it moves - -⚠️ **Physics Note:** Forces accumulate; if the object moves too fast, lower `forceMagnitude` or apply the force only on certain frames. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Short Burst Instead of Continuous Force -```swift -s.onStart() - .setVariable("framesLeft", to: 60) // apply for 1 second at ~60 FPS - -s.onUpdate() - .ifGreater("framesLeft", than: 0) { n in - n.applyWorldForce(direction: .variableRef("forceDir"), - magnitude: .variableRef("forceMagnitude")) - n.addFloat("framesLeft", literal: -1, as: "framesLeft") - } -``` - -### Sideways Push -```swift -.setVariable("forceDir", to: simd_float3(x: 1, y: 0, z: 0)) // push along +X -``` - -### Switch to Impulse -```swift -s.onUpdate() - .applyLinearImpulse(direction: .variableRef("forceDir"), - magnitude: .variableRef("forceMagnitude")) -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Applying forces to physics-enabled entities -✅ Controlling direction and magnitude with variables -✅ Building and attaching physics scripts in the editor -✅ Testing physics responses in Play mode - ---- - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/01-Overview.md b/website/versioned_docs/version-0.10.10/04-Engine Development/01-Overview.md deleted file mode 100644 index 670971d70..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/01-Overview.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -id: engine-overview -title: Overview -sidebar_position: 1 ---- - -# Overview - -This section is for **engine developers and contributors** who want to work on the core of Untold Engine. - -It assumes familiarity with engine concepts and systems programming. - ---- - -## What You’ll Work On - -In this section, you’ll learn about: -- The engine architecture -- The ECS design -- Rendering systems -- Simulation and update flow -- Platform-specific layers - -This is where the **core behavior** of Untold Engine lives. - ---- - -## Installing the Engine for Development - -Engine development requires installing Untold Engine via the command line and working from source. - -Detailed installation steps are provided here: - - -1. Clone the Repository - -```bash -git clone https://github.com/untoldengine/UntoldEngine -cd UntoldEngine -open Package.swift -``` - -**How to Run** -1. Select the **DemoGame** scheme. -2. Download the [Demo Game Assets v1.0](https://github.com/untoldengine/UntoldEngine-Assets/releases/tag/v1) and place them in your Desktop folder. -3. Set the Scheme to **Demo Game**. Then set **My Mac** as the target device and hit **Run**. -4. Use **WASD** keys to move the player around. - - -![DemoGame](../images/demogame-noeditor.png) - - -You do not need Untold Engine Studio to work on the engine itself. - ---- - -## Engine Architecture - -Untold Engine is built around: -- A clear ECS model -- Explicit system update order -- Minimal hidden state -- Platform-specific rendering backends - -The architecture favors **clarity over abstraction**. - -A high-level breakdown is provided in: - -> **Engine Development → Architecture** - ---- - -## Rendering and Systems - -Rendering, physics, and other systems are implemented as: -- Independent modules -- Explicit update stages -- Predictable data flow - -Rendering is designed to expose low-level control while remaining approachable. - ---- - -## Who This Section Is Not For - -This section is **not** intended for: -- Game developers writing gameplay scripts -- Users who only want to use the editor -- Beginners to engine development - -If you want to make a game, see: - -> **Game Development → Overview** - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/02-Architecture/Internals.md b/website/versioned_docs/version-0.10.10/04-Engine Development/02-Architecture/Internals.md deleted file mode 100644 index 0d5c089d4..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/02-Architecture/Internals.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: engine-architecture -title: Engine Architecture Internals -sidebar_position: 2 ---- - -# Untold Engine Architecture Internals - -You’re looking under the hood of Untold Engine. This is how the pieces fit together, why we picked them, and where contributors can plug in. - -## The Big Idea: ECS at the Core -Untold Engine is deliberately **ECS-first**: -- **Entities** are just 64-bit IDs (index + version) managed in `Scenes.swift` with pooling, masks, and tombstones. -- **Components** are plain data (no logic) in `ECS/Components.swift`: transforms, render payloads, physics state, animation sets, lights, scripts, etc. Components live in type-specific pools keyed by component ID. -- **Systems** are the behavior layer in `Sources/UntoldEngine/Systems/`. Each system queries entities by component mask and runs every frame (or in fixed steps for physics). Examples: Transform, Scenegraph, Rendering, Physics, Animation, Input, Culling, Loading, Lighting, Shadow, Steering, USC scripting. - -Why this matters: contributors can add data (components) and behavior (systems) without rewriting the core. Keep components dumb, keep systems focused, and everything stays modular and testable. - -## Frame Flow -`UntoldRenderer.runFrame` orchestrates each tick: -1) **Simulation prep**: delta time, scene graph traversal, input handling. -2) **Gameplay & scripting**: AnimationSystem, USCSystem, custom game update callbacks. -3) **Physics**: fixed-timestep accumulator, gravity/drag/forces, Runge–Kutta integration (collision/contact still open for contribution). -4) **Culling & rendering**: frustum cull, Gaussian depth/sort, build render graph, execute passes, present. - -## Rendering Stack -- **Entry point**: `UntoldRenderer` (MTKView delegate) sets up device/queue, loads metallib, initializes buffers, and drives the frame loop. Platform variants exist for macOS/iOS, visionOS (XR), and AR. -- **Render graph**: `RenderingSystem.swift` builds a dependency graph (environment/grid/AR base → shadow → GBuffer/model → gaussian → post → precomp) and executes via `RenderPasses` + `PipelineManager`. -- **Pipelines**: Render and compute pipelines live in `Renderer/Pipelines/`. Gaussian splats run dedicated compute passes (depth + bitonic sort) before their render pass. - -## Physics & Motion -- **Data**: `PhysicsComponents` and `KineticComponent` store mass, velocity/angular velocity, drag, inertia tensors, forces, moments, and pause flags. -- **Runtime**: `updatePhysicsSystem` accumulates forces/moments, applies gravity/drag, and integrates with Runge–Kutta. Collision/contact resolution is intentionally minimal today—prime territory for contributions. -- **Steering & Animation**: SteeringSystem and AnimationSystem run alongside physics to drive motion and skeletal playback. - -## Scripting (USC) -- **Data**: `ScriptComponent` holds one or more scripts plus file paths (backward compatible with single-script scenes). -- **Runtime**: `USCSystem` ticks the `USCInterpreter` each frame. Actions are registered in `USCScripting.swift` (math helpers, etc.). Extend the DSL by adding actions to `USCActionRegistry`. - -## Scenes, Assets, Resources -- **Scene authoring**: Swift DSL in `Scenes/Builder/` (`Node`/`SceneBuilder`) plus `SceneSerializer` for persistence. -- **Meshes/animations**: abstractions in `Mesh/` and `Skeleton`; resources created through MTK allocators/loaders. -- **Bundled bits**: Prebuilt metallibs per platform and demo resources under `Resources/`. - -## Platform Layers -- **XR (visionOS)**: `UntoldEngineXR` ties CompositorServices/ARKit to the core renderer entry. -- **AR (iOS)**: `UntoldEngineAR` wraps MTKView + ARKit for AR mode. -- **Sample**: `Sources/DemoGame` shows system registration and simple gameplay loops. - -## Where Contributors Can Make Impact -- **Collision/contact & determinism**: Build the missing collision stack, material/friction models, and deterministic stepping for netcode/replays. -- **Render passes/pipelines**: Add or refine passes in the render graph (effects, optimizations, new pipelines). -- **USC actions & API surface**: Expose new engine capabilities to scripts; add higher-level gameplay helpers. -- **Debug/observability**: Better logging sinks, on-screen overlays, profiling, and error surfacing. -- **New systems/components**: AI utilities, networking hooks, gameplay-specific data—keep components data-only and register them for serialization where relevant. - -When proposing big changes, consider ECS storage impact, system ordering, and cross-platform Metal constraints. Align early with maintainers for collision/determinism/netcode-sized work so we keep the architecture cohesive. diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingAnimationSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingAnimationSystem.md deleted file mode 100644 index e0530058e..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingAnimationSystem.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: animationsystem -title: Animation System -sidebar_position: 4 ---- - -# Enabling Animation in Untold Engine - -The Untold Engine simplifies adding animations to your rigged models, allowing for lifelike movement and dynamic interactions. This guide will show you how to set up and play animations for a rigged model. - - -## How to Enable Animation - -### Step 1: Create an Entity - -Start by creating an entity to represent your animated model. - -```swift -let redPlayer = createEntity() -``` - ---- - -### Step 2: Link the Mesh to the Entity - -Load your rigged model’s .usdc file and link it to the entity. This step ensures the entity is visually represented in the scene. - -```swift -setEntityMesh(entityId: redPlayer, filename: "redplayer", withExtension: "usdc", flip: false) -``` ->>> Note: If your model renders with the wrong orientation, set the flip parameter to false. - ---- - -### Step 3: Load the Animation -Load the animation data for your model by providing the animation .usdc file and a name to reference the animation later. - -```swift -setEntityAnimations(entityId: redPlayer, filename: "running", withExtension: "usdc", name: "running") -``` - ---- - -### Step 4: Set the Animation to play - -Trigger the animation by referencing its name. This will set the animation to play on the entity. - -```swift -changeAnimation(entityId: redPlayer, name: "running") -``` - ---- - -### Step 5. Pause the animation (Optional) - -To pause the current animation, simply call the following function. The animation component will be paused for the current entity. - -```swift -pauseAnimationComponent(entityId: redPlayer, isPaused: true) -``` - ---- - -### Running the Animation - -Once the animation is set up: - -1. Run the project: Your model will appear in the game window. -2. Click on "Play" to enter Game Mode: -- The model will play the assigned animation in real time. - ---- - -## Tips and Best Practices - -- Name Animations Clearly: Use descriptive names like "running" or "jumping" to make it easier to manage multiple animations. -- Debug Orientation Issues: If the model’s animation appears misaligned, revisit the flip parameter or check the model’s export settings. -- Combine Animations: For complex behaviors, load multiple animations (e.g., walking, idle, jumping) and switch between them dynamically. diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingAsyncLoading.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingAsyncLoading.md deleted file mode 100644 index c7312f5fb..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingAsyncLoading.md +++ /dev/null @@ -1,325 +0,0 @@ ---- -id: asyncloadingsystem -title: Async Loading System -sidebar_position: 9 ---- - -# Async USDZ Loading System - Usage Guide - -## Overview - -The async loading system allows you to load USDZ files with many models without blocking the main thread. The engine remains responsive during loading, and you can track progress for UI feedback. - -## Features - -✅ **Non-blocking loading** - Engine continues running while assets load -✅ **Progress tracking** - Monitor loading progress per entity or globally -✅ **Fallback meshes** - Automatically loads a cube if loading fails -✅ **Backward compatible** - Old synchronous APIs still work - ---- - -## Basic Usage - -### Load Entity Mesh Asynchronously - -```swift -// Create entity -let entityId = createEntity() - -// Load mesh asynchronously (fire and forget) -setEntityMeshAsync( - entityId: entityId, - filename: "large_model", - withExtension: "usdz" -) - -// Engine continues running - mesh appears when loaded -``` - -### With Completion Callback - -```swift -let entityId = createEntity() - -setEntityMeshAsync( - entityId: entityId, - filename: "large_model", - withExtension: "usdz" -) { success in - if success { - print("✅ Model loaded successfully!") - } else { - print("❌ Failed to load - fallback cube used") - } -} -``` - -### Load Entire Scene Asynchronously - -```swift -loadSceneAsync( - filename: "complex_scene", - withExtension: "usdz" -) { success in - if success { - print("Scene loaded with all entities") - } -} -``` - ---- - -## Progress Tracking - -### Check if Assets are Loading - -```swift -Task { - let isLoading = await AssetLoadingState.shared.isLoadingAny() - if isLoading { - print("Assets are currently loading...") - } -} -``` - -### Get Loading Count - -```swift -Task { - let count = await AssetLoadingState.shared.loadingCount() - print("Loading \(count) entities") -} -``` - -### Get Detailed Progress - -```swift -Task { - let (current, total) = await AssetLoadingState.shared.totalProgress() - let percentage = Float(current) / Float(total) * 100 - print("Progress: \(percentage)% (\(current)/\(total) meshes)") -} -``` - -### Get Progress Summary String - -```swift -Task { - let summary = await AssetLoadingState.shared.loadingSummary() - print(summary) // "Loading large_model.usdz: 5/20 meshes" -} -``` - -### Track Specific Entity - -```swift -Task { - if let progress = await AssetLoadingState.shared.getProgress(for: entityId) { - print("\(progress.filename): \(progress.currentMesh)/\(progress.totalMeshes)") - print("Percentage: \(progress.percentage * 100)%") - } -} -``` - ---- - -## Untold Editor Integration - -### Display Loading UI - -```swift -// In your editor's update loop or UI -Task { - if await AssetLoadingState.shared.isLoadingAny() { - let summary = await AssetLoadingState.shared.loadingSummary() - // Show loading indicator with summary text - showLoadingIndicator(text: summary) - } else { - // Hide loading indicator - hideLoadingIndicator() - } -} -``` - -### Progress Bar Example - -```swift -Task { - let allProgress = await AssetLoadingState.shared.getAllProgress() - - for progress in allProgress { - let percentage = progress.percentage * 100 - print("📦 \(progress.filename): \(Int(percentage))%") - - // Update UI progress bar - updateProgressBar( - id: progress.entityId, - filename: progress.filename, - percentage: percentage - ) - } -} -``` - ---- - -## Error Handling - -### Automatic Fallback - -When async loading fails, a fallback cube mesh is automatically loaded: - -```swift -setEntityMeshAsync( - entityId: entityId, - filename: "missing_file", - withExtension: "usdz" -) { success in - if !success { - // Entity now has a cube mesh as fallback - print("Loaded fallback cube") - } -} -``` - -### Manual Error Handling - -```swift -setEntityMeshAsync( - entityId: entityId, - filename: "model", - withExtension: "usdz" -) { success in - if !success { - // Handle error - entity has fallback cube - showErrorDialog("Failed to load model.usdz") - - // Optionally destroy entity or retry - // destroyEntity(entityId: entityId) - } -} -``` - ---- - -## Backward Compatibility - -The old synchronous APIs still work unchanged: - -```swift -// Old way (blocks main thread) -setEntityMesh( - entityId: entityId, - filename: "model", - withExtension: "usdz" -) - -// New way (non-blocking) -setEntityMeshAsync( - entityId: entityId, - filename: "model", - withExtension: "usdz" -) -``` - ---- - -## Advanced Usage - -### Loading Multiple Models - -```swift -let entity1 = createEntity() -let entity2 = createEntity() -let entity3 = createEntity() - -// All three load in parallel without blocking -setEntityMeshAsync(entityId: entity1, filename: "model1", withExtension: "usdz") -setEntityMeshAsync(entityId: entity2, filename: "model2", withExtension: "usdz") -setEntityMeshAsync(entityId: entity3, filename: "model3", withExtension: "usdz") - -// Track total progress -Task { - while await AssetLoadingState.shared.isLoadingAny() { - let summary = await AssetLoadingState.shared.loadingSummary() - print(summary) - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - } - print("All models loaded!") -} -``` - -### Coordinate System Conversion - -```swift -setEntityMeshAsync( - entityId: entityId, - filename: "blender_model", - withExtension: "usdz", - coordinateConversion: .forceZUpToYUp -) -``` - ---- - -## Implementation Details - -### What Runs on Background Thread - -- Reading USDZ file from disk -- Parsing MDL data structures -- Loading texture data -- Applying coordinate transformations - -### What Runs on Main Thread - -- Creating Metal resources (MTKMesh, MTLTexture) -- Registering ECS components -- Updating entity transforms -- Creating fallback meshes on error - -### Thread Safety - -All `AssetLoadingState` operations are thread-safe via Swift's actor model. You can safely query loading state from any thread. - ---- - -## Performance Tips - -1. **Prefer async for large files** (>20 models or >50MB) -2. **Use sync for small files** (1-5 models, <10MB) - less overhead -3. **Batch loads together** - loading multiple files in parallel is efficient -4. **Monitor progress** - use for UI feedback during long loads - ---- - -## Migration Guide - -### Before (Blocking) - -```swift -func loadGameAssets() { - let entity = createEntity() - setEntityMesh(entityId: entity, filename: "huge_scene", withExtension: "usdz") - // Engine was frozen here ❌ -} -``` - -### After (Non-blocking) - -```swift -func loadGameAssets() { - let entity = createEntity() - setEntityMeshAsync(entityId: entity, filename: "huge_scene", withExtension: "usdz") { - success in - if success { - print("Scene ready!") - } - } - // Engine continues running ✅ -} -``` - ---- - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingCameraSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingCameraSystem.md deleted file mode 100644 index 3f4b480de..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingCameraSystem.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -id: camerasystem -title: Camera System -sidebar_position: 10 ---- - -# Using the Camera System - -This document explains how to move, rotate, and control cameras using the APIs in `CameraSystem.swift`. - -## Get the Game Camera - -For gameplay, always use the game camera (not the editor/scene camera). Call `findGameCamera()` and make it active: - -```swift -let camera = findGameCamera() -CameraSystem.shared.activeCamera = camera -``` - -If no game camera exists, `findGameCamera()` creates one and sets it up with default values. - -## Translate (Move) the Camera - -Use absolute or relative movement: - -```swift -// Absolute position -moveCameraTo(entityId: camera, 0.0, 3.0, 7.0) - -// Relative movement in camera local space -cameraMoveBy(entityId: camera, delta: simd_float3(0.0, 0.0, -1.0), space: .local) - -// Relative movement in world space -cameraMoveBy(entityId: camera, delta: simd_float3(1.0, 0.0, 0.0), space: .world) -``` - -## Rotate the Camera - -Use `rotateCamera` for pitch/yaw rotation, or `cameraLookAt` to aim at a target. - -```swift -// Rotate by pitch/yaw (radians), with optional sensitivity -rotateCamera(entityId: camera, pitch: 0.02, yaw: 0.01, sensitivity: 1.0) - -// Look-at orientation -cameraLookAt( - entityId: camera, - eye: simd_float3(0.0, 3.0, 7.0), - target: simd_float3(0.0, 0.0, 0.0), - up: simd_float3(0.0, 1.0, 0.0) -) -``` - -## Camera Follow - -Follow a target entity with a fixed offset. You can optionally smooth the motion. - -```swift -let target = findEntity(name: "player") ?? createEntity() -let offset = simd_float3(0.0, 2.0, 6.0) - -// Instant follow -cameraFollow(entityId: camera, targetEntity: target, offset: offset) - -// Smoothed follow -cameraFollow(entityId: camera, targetEntity: target, offset: offset, smoothFactor: 6.0, deltaTime: deltaTime) -``` - -### Dead-Zone Follow - -`cameraFollowDeadZone` only moves the camera when the target leaves a box around it. This is useful for platformers and shoulder cameras. - -```swift -let deadZone = simd_float3(1.0, 0.5, 1.0) -cameraFollowDeadZone( - entityId: camera, - targetEntity: target, - offset: offset, - deadZoneExtents: deadZone, - smoothFactor: 6.0, - deltaTime: deltaTime -) -``` - -## Camera Path Following - -The camera path system moves the active camera through a sequence of waypoints with smooth interpolation. - -### Start a Path - -```swift -let waypoints = [ - CameraWaypoint( - position: simd_float3(0, 5, 10), - rotation: simd_quatf(angle: 0, axis: simd_float3(0, 1, 0)), - segmentDuration: 2.0 - ), - CameraWaypoint( - position: simd_float3(10, 5, 10), - rotation: simd_quatf(angle: Float.pi / 4, axis: simd_float3(0, 1, 0)), - segmentDuration: 2.0 - ) -] - -startCameraPath(waypoints: waypoints, mode: .once) -``` - -You can also build waypoints that look at a target: - -```swift -let waypoint = CameraWaypoint( - position: simd_float3(0, 5, 10), - lookAt: simd_float3(0, 0, 0), - up: simd_float3(0, 1, 0), - segmentDuration: 2.0 -) -``` - -### Update Every Frame - -Call `updateCameraPath(deltaTime:)` from your main update loop: - -```swift -func update(deltaTime: Float) { - updateCameraPath(deltaTime: deltaTime) -} -``` - -### Looping and Completion - -```swift -startCameraPath(waypoints: waypoints, mode: .loop) - -let settings = CameraPathSettings(startImmediately: true) { - print("Camera path completed") -} -startCameraPath(waypoints: waypoints, mode: .once, settings: settings) -``` - -## Notes - -- `startCameraPath` and `updateCameraPath` operate on `CameraSystem.shared.activeCamera`. -- `segmentDuration` is the time to move from the current waypoint to the next. -- For gameplay, always acquire the camera with `findGameCamera()` and set it active before path playback or follow logic. diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingGaussianSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingGaussianSystem.md deleted file mode 100644 index 63d87e5a8..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingGaussianSystem.md +++ /dev/null @@ -1,47 +0,0 @@ ---- -id: gaussiansystem -title: Gaussian System -sidebar_position: 3 ---- - -# Enabling Gaussian System in Untold Engine - -The Gaussian System in the Untold Engine is responsible for rendering Gaussian Splatting models. It enables you to visualize high-quality 3D reconstructions created from photogrammetry or neural rendering techniques, providing a modern approach to displaying complex 3D scenes. - -## How to Enable the Gaussian System - -### Step 1: Create an Entity - -Start by creating an entity that represents your Gaussian Splat object. - -```swift -let myEntity = createEntity() -``` ---- - -### Step 2: Link a Gaussian Splat to the Entity - -To display a Gaussian Splat model, load its .ply file and link it to the entity using setEntityGaussian. - -```swift -setEntityGaussian(entityId: myEntity, filename: "splat", withExtension: "ply") -``` - -Parameters: - -- entityId: The ID of the entity created earlier. -- filename: The name of the .ply file (without the extension). -- withExtension: The file extension, typically "ply". - -> Note: The Gaussian System renders point cloud data stored in the .ply format. Ensure your Gaussian Splat file is properly formatted and contains the necessary attributes (position, color, opacity, scale, rotation). - ---- - -### Running the Gaussian System - -Once everything is set up: - -1. Run the project. -2. Your Gaussian Splat model will appear in the game window. -3. If the model is not visible or appears incorrect, revisit the file path and format to ensure everything is loaded correctly. - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingGeometryStreamingSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingGeometryStreamingSystem.md deleted file mode 100644 index a3304a48e..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingGeometryStreamingSystem.md +++ /dev/null @@ -1,151 +0,0 @@ ---- -id: geometrystreaminggsystem -title: Geometry Streaming System -sidebar_position: 13 ---- - -# Geometry Streaming System - -The Geometry Streaming System dynamically loads and unloads geometry based on the camera's proximity to objects. This system is essential for large-scale scenes where loading all geometry at once would exceed available memory or cause performance issues. - -## How It Works - -The streaming system monitors the distance between the camera and entities that have streaming enabled. Based on configurable radius values, the system automatically: - -1. **Loads geometry** when the camera moves within the streaming radius -2. **Keeps geometry loaded** while the camera remains between the streaming and unload radii -3. **Unloads geometry** when the camera moves beyond the unload radius - -This creates a "bubble" of loaded geometry around the camera that moves with it through the scene. - -## When to Use Geometry Streaming - -**Ideal for:** -- Large open-world environments with distant objects -- Scenes where not all objects are visible simultaneously -- Memory-constrained scenarios -- Games with large view distances (forests, cities, landscapes) - -**Not recommended for:** -- Small scenes where all objects fit comfortably in memory -- Objects that are always visible -- Dynamic objects that move frequently -- Critical gameplay objects that must always be loaded - -## Basic Usage - -Here's a simple example of enabling streaming for a single entity: - -```swift -private func setupStreaming(){ - let stadium = createEntity() - setEntityMeshAsync(entityId: stadium, filename: "stadium", withExtension: "usdz") { success in - if success { - - print("Scene loaded successfully") - - enableStreaming( - entityId: stadium, - streamingRadius: 250.0, // Load when within 250 units - unloadRadius: 350.0, // Unload when beyond 350 units - priority: 10 - ) - } - } -} -``` - -### Important Notes - -1. **Load mesh first**: Always call `setEntityMeshAsync()` before enabling streaming -2. **Use completion callback**: Enable streaming inside the completion callback to ensure the mesh is loaded -3. **Async loading**: The `setEntityMeshAsync()` function loads the mesh asynchronously, preventing frame drops - -## Parameters Explained - -### `streamingRadius` -The distance from the camera at which geometry will be loaded. -- Objects closer than this distance will have their geometry loaded -- Should be set based on your camera's view distance and scene requirements -- **Typical values**: 100-500 units depending on object size and importance - -### `unloadRadius` -The distance from the camera at which geometry will be unloaded. -- Must be **larger** than `streamingRadius` to create a buffer zone -- Prevents "thrashing" (rapid loading/unloading as camera moves near the boundary) -- **Recommended**: At least 50-100 units larger than `streamingRadius` - -### `priority` -Determines the loading order when multiple objects need to be streamed. -- Higher values = loaded first -- Lower values = loaded last -- **Range**: Typically 1-10, but can be any positive integer -- **Usage**: - - High priority (8-10): Important landmarks, gameplay-critical objects - - Medium priority (4-7): Standard environment objects - - Low priority (1-3): Background details, distant decorations - -## Radius Configuration Guidelines - -Choosing the right radius values is crucial for optimal performance: - -``` -Camera Position - | - |<-- streamingRadius (250) -->|<-- buffer zone -->|<-- unloadRadius (350) -->| - | - | Geometry LOADS here | Stays loaded | Geometry UNLOADS here -``` - -### Example Configurations - -**Small objects (trees, props):** -- `streamingRadius`: 150-250 units -- `unloadRadius`: 250-350 units -- Buffer: 100 units - -**Medium objects (buildings, vehicles):** -- `streamingRadius`: 250-400 units -- `unloadRadius`: 400-550 units -- Buffer: 150 units - -**Large objects (stadiums, mountains):** -- `streamingRadius`: 500-1000 units -- `unloadRadius`: 700-1300 units -- Buffer: 200-300 units - -## Combining with Other Systems - -Geometry streaming works seamlessly with LOD and Batching systems: - -- **LOD + Streaming**: Use LOD for quality management and streaming for memory management -- **Batching + Streaming**: Batches are automatically updated as geometry loads/unloads -- **All three together**: Optimal for large open-world scenes - -See the [Combining LOD, Batching, and Streaming](./UsingLOD-Batching-Streaming.md) guide for detailed examples. - -## Best Practices - -1. **Test radius values**: Start conservative and adjust based on performance metrics -2. **Monitor memory**: Use profiling tools to ensure streaming is reducing memory usage -3. **Priority assignment**: Reserve high priorities for gameplay-critical objects -4. **Buffer zones**: Always maintain adequate buffer between streaming and unload radii -5. **Camera speed**: Faster-moving cameras may need larger streaming radii to prevent pop-in -6. **Position before streaming**: Set entity transforms before enabling streaming - -## Common Issues - -### Objects Not Loading -- Ensure `streamingRadius` is large enough for your camera's viewing distance -- Check that the mesh was loaded successfully in the completion callback -- Verify the entity has been positioned in the scene - -### Geometry "Popping" In and Out -- Increase the buffer between `streamingRadius` and `unloadRadius` -- Consider using LOD to smooth transitions -- Adjust camera movement speed or increase radii - -### Performance Issues -- Too many objects loading simultaneously: Adjust priorities to stagger loading -- Streaming radius too large: Reduce radius to load fewer objects -- Use LOD to reduce complexity of loaded geometry diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingInputSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingInputSystem.md deleted file mode 100644 index 1c73ff69d..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingInputSystem.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -id: inputsystem -title: Input System -sidebar_position: 6 ---- - -# Using the Input System in Untold Engine - -The Input System in the Untold Engine allows you to detect user inputs, such as keystrokes and mouse movements, to control entities and interact with the game. This guide will explain how to use the Input System effectively. - - -## How to Use the Input System (Keyboard) - -### Step 1: Detect Keystrokes - -To detect if a specific key is pressed, use the keyState object from the Input System. - -Example: Detecting the 'W' Key - -```swift -func init(){ -// Make sure that you have enabled keyevents in your init function: -InputSystem.shared.registerKeyboardEvents() -} - -// Then in the handleInput callback, you can do this: - -func handleInput() { - // Skip logic if not in game mode - if gameMode == false { return } - - let inputSystem = InputSystem.shared - - // Handle input here - if inputSystem.keyState.wPressed{ - Logger.log(message: "w pressed") - } -} -``` -You can use the same logic for other keys like A, S, and D: - -```swift -let inputSystem = InputSystem.shared - -if inputSystem.keyState.aPressed == true { - // Move left -} - -if inputSystem.keyState.sPressed == true { - // Move backward -} - -if inputSystem.keyState.dPressed == true { - // Move right -} -``` - -###Step 2: Using Input to Control Entities - -Here’s an example function that moves a car entity based on keyboard inputs: - -```swift -func moveCar(entityId: EntityID, dt: Float) { - - let inputSystem = InputSystem.shared - - // Ensure we are in game mode - if gameMode == false { - return - } - - var position = simd_float3(0.0, 0.0, 0.0) - - // Move forward - if inputSystem.keyState.wPressed == true { - position.z += 1.0 * dt - } - - // Move backward - if inputSystem.keyState.sPressed == true { - position.z -= 1.0 * dt - } - - // Move left - if inputSystem.keyState.aPressed == true { - position.x -= 1.0 * dt - } - - // Move right - if inputSystem.keyState.dPressed == true { - position.x += 1.0 * dt - } - - // Apply the translation to the entity - translateTo(entityId: entityId, position: position) -} -``` - -## How to Use the Input System with a Game Controller - -To detect if a specific button is pressed, use the gameControllerState object from the Input System. - -Example: Detecting the 'A' button - -```swift -func init(){ -// Make sure that you have enabled game controller events in your init function: - InputSystem.shared.registerGameControllerEvents() -} - -// Then in the handleInput callback, you can do this: - -func handleInput() { - // Skip logic if not in game mode - if gameMode == false { return } - let inputSystem = InputSystem.shared - - // Handle input here - if inputSystem.gameControllerState.aPressed { - Logger.log(message: "Pressed A key") - } -} -``` - ---- - -## Tips and Best Practices -- Debouncing: If you want to execute an action only once per key press, track the key's previous state to avoid repeated triggers. -- Game Mode Check: Always ensure the game is in the appropriate mode (e.g., Game Mode) before processing inputs. -- Smooth Movement: Use dt (delta time) to ensure frame-rate-independent movement. - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingLOD-Batching-Streaming.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingLOD-Batching-Streaming.md deleted file mode 100644 index 44cbcd17e..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingLOD-Batching-Streaming.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -id: lodstaticbatchinstreaminggsystem -title: Lod + Static Batching + Streaming System -sidebar_position: 12 ---- - -# Combining LOD, Batching, and Geometry Streaming - -The Untold Engine provides three complementary systems for optimizing rendering performance: - -- **LOD (Level of Detail)**: Automatically switches between different mesh resolutions based on distance from the camera. Closer objects use high-detail meshes, while distant objects use simplified versions. -- **Static Batching**: Combines multiple static meshes with the same material into a single draw call, reducing CPU overhead. -- **Geometry Streaming**: Dynamically loads and unloads geometry based on proximity to the camera, keeping only nearby objects in memory. - -When used together, these systems provide powerful performance optimization: -- LOD reduces GPU load by rendering appropriate detail levels -- Batching reduces CPU overhead by minimizing draw calls -- Streaming reduces memory usage by only keeping nearby geometry loaded - -Before using the examples below, assume your scene has: -- A valid active camera (used for LOD switching and streaming distance checks) -- Entity transforms in world space before runtime optimization kicks in -- Mesh/material naming consistency across LOD levels (to avoid mismatched assets) -- A clear decision on whether setup happens procedurally at runtime or from a serialized scene file - -In practice, there are two common workflows: -- **Procedural setup**: You create entities and configure LOD/batching/streaming in code during scene initialization. -- **Scene deserialization**: You load a saved scene where components were already authored, then finalize runtime systems in completion callbacks. - -## LOD + Batching - -This combination is ideal for scenes with many static objects that remain loaded throughout the scene lifetime. The LOD system manages visual quality based on distance, while batching reduces draw calls for objects using the same material. - -**Key Points:** -- All entities must have their meshes loaded before calling `generateBatches()` -- Use the completion callback from `addLODLevels()` to ensure meshes are ready -- Enable batching and generate batches only after all entities are configured - -Why this order matters: batching relies on mesh/material data that is only guaranteed after async LOD loading completes. If `generateBatches()` runs too early, some entities may be missing from batches, causing inconsistent draw-call reduction. - -```swift -private func setupLODWithBatching() { - var loadedCount = 0 - let totalTrees = 20 - - for i in 0 ..< totalTrees { - let tree = createEntity() - setEntityName(entityId: tree, name: "Tree_\(i)") - - // Capture position values for the closure - let x = Float(i % 5) * 10.0 - let z = Float(i / 5) * 10.0 - - setEntityLodComponent(entityId: tree) - - addLODLevels(entityId: tree, levels: [ - (0, "tree_LOD0", "usdz", 50.0, 0.0), - (1, "tree_LOD1", "usdz", 100.0, 0.0), - (2, "tree_LOD2", "usdz", 200.0, 0.0), - ]) { success in - if success { - // Apply transform AFTER mesh is loaded - translateTo(entityId: tree, position: simd_float3(x, 0, z)) - setEntityStaticBatchComponent(entityId: tree) - } - - // Track completion - loadedCount += 1 - if loadedCount == totalTrees { - enableBatching(true) - generateBatches() - print("\(totalTrees) trees configured with LOD + Batching") - } - } - } - -} -``` - -For LOD + batching loaded from a scene file, the key is to defer batch generation until playSceneAt reports completion (after async mesh loading finishes): - -```swift -playSceneAt(url:url){ - enableBatching(true) - generateBatches() -} -``` - -## LOD + Batching + Streaming - -This combination is ideal for large open-world scenes where you have many objects spread across a large area. Streaming ensures only nearby geometry is loaded into memory, LOD manages visual quality, and batching reduces draw calls for loaded objects. - -**Key Points:** -- Enable streaming **after** the mesh is loaded and positioned -- Set `streamingRadius` larger than your farthest LOD distance to ensure smooth transitions -- Set `unloadRadius` larger than `streamingRadius` to provide a buffer zone -- Batching works with streamed geometry - batches are automatically updated as objects load/unload -- The order matters: LOD component → Load meshes → Transform → Enable streaming → Mark for batching - -This setup is best treated as a lifecycle: -1. Author LOD levels and streaming radii based on gameplay visibility needs. -2. Wait for asset load completion before enabling dependent systems. -3. Activate batching once entities are fully initialized, then let streaming maintain runtime memory pressure. - -**Radius Guidelines:** -- `streamingRadius`: Should be greater than your farthest LOD distance plus a buffer (e.g., if farthest LOD is 200, use 250) -- `unloadRadius`: Should be significantly larger than `streamingRadius` to avoid thrashing (e.g., 350 when streaming radius is 250) - -```swift -private func setupLODBatchingStreaming() { - var loadedCount = 0 - let totalTrees = 20 - - for i in 0 ..< totalTrees { - let tree = createEntity() - setEntityName(entityId: tree, name: "Tree_\(i)") - - // Capture position values for the closure - let x = Float(i % 5) * 10.0 - let z = Float(i / 5) * 10.0 - - // 1. Set LOD component FIRST - setEntityLodComponent(entityId: tree) - - // 2. Load LOD levels - addLODLevels(entityId: tree, levels: [ - (0, "tree_LOD0", "usdz", 50.0, 0.0), - (1, "tree_LOD1", "usdz", 100.0, 0.0), - (2, "tree_LOD2", "usdz", 200.0, 0.0), - (3, "tree_LOD3", "usdz", 300.0, 0.0), - ]) { success in - if success { - // 3. Apply transform AFTER mesh is loaded - translateTo(entityId: tree, position: simd_float3(x, 0, z)) - - // 4. Enable streaming AFTER mesh exists - // streamingRadius > farthest LOD distance (200) with buffer - // unloadRadius > streamingRadius with buffer - enableStreaming( - entityId: tree, - streamingRadius: 250.0, // Load when within 250 units - unloadRadius: 350.0, // Unload when beyond 350 units - priority: 10 - ) - - // 5. Mark for batching - setEntityStaticBatchComponent(entityId: tree) - } - - // Track completion - loadedCount += 1 - if loadedCount == totalTrees { - enableBatching(true) - generateBatches() - print("\(totalTrees) trees configured with LOD + Batching + Streaming") - } - } - } -} -``` - -When loading LOD + batching + streaming from a scene file, deserialization restores component state first, then your completion handler enables systems that depend on fully loaded geometry: - -```swift -playSceneAt(url: sceneURL) { - // Called after deserializeScene completion (async LOD/mesh loads done) - - enableBatching(true) - generateBatches() - - GeometryStreamingSystem.shared.enabled = true - print("Scene loaded with LOD + Batching + Streaming enabled") -} -``` - -## Best Practices - -### When to Use Each Combination - -**LOD Only:** -- Small scenes with few objects -- When objects are always visible and memory isn't a concern -- Dynamic objects that move frequently - -**LOD + Batching:** -- Medium-sized scenes with many static objects -- Objects share materials and remain loaded -- Memory usage is acceptable -- Example: Interior spaces, small outdoor areas - -**LOD + Batching + Streaming:** -- Large open-world scenes -- Many objects spread across large distances -- Memory optimization is critical -- Example: Forests, cities, large outdoor environments - -### Performance Tips - -1. **LOD Distances**: Set LOD transition distances based on your object size and visual importance. Smaller objects can transition earlier. - -2. **Batching Materials**: Entities must share the same material to be batched together. Group objects by material when possible. - -3. **Streaming Priorities**: Use higher priority values (e.g., 10) for important objects like landmarks, lower values (e.g., 1) for background details. - -4. **Testing**: Monitor frame rate and memory usage to fine-tune your radius values and LOD distances for your specific scene. - -5. **Completion Callbacks**: Always use the completion callback from `addLODLevels()` to ensure meshes are fully loaded before enabling other systems. diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingLODSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingLODSystem.md deleted file mode 100644 index 7af29fed7..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingLODSystem.md +++ /dev/null @@ -1,466 +0,0 @@ ---- -id: lodsystem -title: LOD System -sidebar_position: 10 ---- - -# LOD (Level of Detail) System - Usage Guide - -The Untold Engine provides a flexible LOD system for optimizing rendering performance by displaying different mesh details based on camera distance. - -## Overview - -The LOD system allows you to: -- Add multiple levels of detail to any entity -- Automatically switch between LOD levels based on distance -- Customize distance thresholds for each LOD level -- Configure LOD behavior (bias, hysteresis, fade transitions) - -> **Choose Your Path:** You can set up LOD via the **Editor** (no code required) or **programmatically** in Swift. - ---- - -## Using the Editor - -### Adding LOD Support to an Entity - -1. **Select your entity** in the Scene Hierarchy -2. **Open the Inspector** and click **"Add Components"** -3. **Select "LOD Component"** from the list -4. An LOD Levels panel will appear in the Inspector - -### Adding LOD Levels - -1. **Select a model** from the Asset Browser (Models folder) -2. In the LOD Levels panel, click **"Add LOD Level"** -3. The selected model will be added as the next LOD level with a default distance: - - LOD0: 50 units - - LOD1: 100 units - - LOD2: 150 units, etc. - -### Adjusting LOD Distances - -1. Click the **distance value** next to any LOD level -2. Enter a new distance and press **Enter** -3. Objects will switch to this LOD when the camera is within this distance - -### Removing LOD Levels - -Click the **trash icon** next to any LOD level to remove it. - ---- - -## Using Code - -### Quick Start - -### Basic LOD Setup - -```swift -// Create entity -let tree = createEntity() - -// Add LOD component -setEntityLodComponent(entityId: tree) - -// Add LOD levels (from highest to lowest detail) -addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "usdz", maxDistance: 50.0) -addLODLevel(entityId: tree, lodIndex: 1, fileName: "tree_LOD1", withExtension: "usdz", maxDistance: 100.0) -addLODLevel(entityId: tree, lodIndex: 2, fileName: "tree_LOD2", withExtension: "usdz", maxDistance: 200.0) -addLODLevel(entityId: tree, lodIndex: 3, fileName: "tree_LOD3", withExtension: "usdz", maxDistance: 400.0) -``` - -**How it works:** -- LOD0 (highest detail) renders when camera is < 50 units away -- LOD1 renders between 50-100 units -- LOD2 renders between 100-200 units -- LOD3 (lowest detail) renders beyond 200 units - -### Loading Multiple LOD Levels (Recommended) - -Use `addLODLevels` to load all LOD levels with a single completion handler. This is especially important when combining LOD with static batching: - -```swift -let tree = createEntity() -setEntityLodComponent(entityId: tree) - -// Load all LOD levels and wait for completion -addLODLevels(entityId: tree, levels: [ - (0, "tree_LOD0", "usdz", 50.0, 0.0), - (1, "tree_LOD1", "usdz", 100.0, 0.0), - (2, "tree_LOD2", "usdz", 200.0, 0.0), - (3, "tree_LOD3", "usdz", 400.0, 0.0) -]) { success in - if success { - print("All LOD levels loaded") - - // Apply transforms AFTER mesh is loaded - translateTo(entityId: tree, position: simd_float3(10, 0, 5)) - - // Apply static batching if necessary - } -} -``` - -> **Important:** When using LOD with async loading, apply transforms (`translateTo`, `rotateTo`, `scaleTo`) inside the completion handler. Transforms applied before the mesh loads may not take effect. - -### With Initial Mesh Loading - -You can also load an initial mesh synchronously before adding LOD levels: - -```swift -let tree = createEntity() - -// Load initial mesh synchronously (shows immediately) -setEntityMesh(entityId: tree, filename: "tree_LOD0", withExtension: "usdz") - -// Add LOD component -setEntityLodComponent(entityId: tree) - -// Add LOD levels (will replace initial mesh when ready) -addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "usdz", maxDistance: 50.0) -addLODLevel(entityId: tree, lodIndex: 1, fileName: "tree_LOD1", withExtension: "usdz", maxDistance: 100.0) -addLODLevel(entityId: tree, lodIndex: 2, fileName: "tree_LOD2", withExtension: "usdz", maxDistance: 200.0) -addLODLevel(entityId: tree, lodIndex: 3, fileName: "tree_LOD3", withExtension: "usdz", maxDistance: 400.0) -``` - -### With Completion Handler - -Since `addLODLevel` loads meshes asynchronously, use the completion handler when you need to perform actions after loading completes: - -```swift -let tree = createEntity() -setEntityLodComponent(entityId: tree) - -// Chain completion handlers for sequential loading -addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "usdz", maxDistance: 50.0) { success in - if success { - print("LOD0 loaded") - // Now it's safe to use the mesh data - } -} -``` - -## File Organization - -LOD files should be organized in subdirectories: - -``` -GameData/ -└── Models/ - ├── tree_LOD0/ - │ └── tree_LOD0.usdz - ├── tree_LOD1/ - │ └── tree_LOD1.usdz - ├── tree_LOD2/ - │ └── tree_LOD2.usdz - └── tree_LOD3/ - └── tree_LOD3.usdz -``` - -**Note:** Each LOD file should be in its own folder with the same name as the file (without extension). - -## API Reference - -### Core Functions - -#### `setEntityLodComponent(entityId:)` -Registers an LOD component on an entity. Call this before adding LOD levels. - -```swift -setEntityLodComponent(entityId: tree) -``` - -#### `addLODLevel(entityId:lodIndex:fileName:withExtension:maxDistance:completion:)` -Adds a single LOD level to an entity. - -**Parameters:** -- `entityId`: The entity to add LOD to -- `lodIndex`: LOD level index (0 = highest detail) -- `fileName`: Name of the mesh file (without extension) -- `withExtension`: File extension (e.g., "usdz") -- `maxDistance`: Maximum camera distance for this LOD -- `completion`: Optional callback when loading completes - -```swift -addLODLevel( - entityId: tree, - lodIndex: 0, - fileName: "tree_LOD0", - withExtension: "usdz", - maxDistance: 50.0 -) { success in - if success { - print("LOD0 loaded successfully") - } -} -``` - -#### `addLODLevels(entityId:levels:completion:)` -Adds multiple LOD levels with a single completion handler. Useful when you need to wait for all LOD levels to load. - -**Parameters:** -- `entityId`: The entity to add LOD levels to -- `levels`: Array of tuples: `(lodIndex, fileName, withExtension, maxDistance, screenPercentage)` -- `completion`: Called when ALL levels finish loading (true only if all succeeded) - -```swift -addLODLevels(entityId: tree, levels: [ - (0, "tree_LOD0", "usdz", 50.0, 0.0), - (1, "tree_LOD1", "usdz", 100.0, 0.0), - (2, "tree_LOD2", "usdz", 200.0, 0.0) -]) { success in - if success { - print("All LODs loaded") - } -} -``` - -#### `removeLODLevel(entityId:lodIndex:)` -Removes a specific LOD level from an entity. - -```swift -removeLODLevel(entityId: tree, lodIndex: 2) -``` - -#### `replaceLODLevel(entityId:lodIndex:fileName:withExtension:maxDistance:completion:)` -Replaces an existing LOD level with a new mesh. - -```swift -replaceLODLevel( - entityId: tree, - lodIndex: 1, - fileName: "tree_LOD1_new", - withExtension: "usdz", - maxDistance: 100.0 -) -``` - -#### `getLODLevelCount(entityId:) -> Int` -Returns the number of LOD levels for an entity. - -```swift -let count = getLODLevelCount(entityId: tree) -print("Entity has \(count) LOD levels") -``` - ---- - -## Advanced Usage - -### Custom Distance Thresholds - -Adjust distances based on your scene scale: - -```swift -// Small scene (indoor environment) -addLODLevel(entityId: prop, lodIndex: 0, fileName: "prop_LOD0", withExtension: "usdz", maxDistance: 10.0) -addLODLevel(entityId: prop, lodIndex: 1, fileName: "prop_LOD1", withExtension: "usdz", maxDistance: 20.0) - -// Large scene (outdoor landscape) -addLODLevel(entityId: mountain, lodIndex: 0, fileName: "mountain_LOD0", withExtension: "usdz", maxDistance: 500.0) -addLODLevel(entityId: mountain, lodIndex: 1, fileName: "mountain_LOD1", withExtension: "usdz", maxDistance: 1000.0) -addLODLevel(entityId: mountain, lodIndex: 2, fileName: "mountain_LOD2", withExtension: "usdz", maxDistance: 2000.0) -``` - -### LOD Configuration - -Configure global LOD behavior: - -```swift -// Adjust LOD bias (higher = switch to lower detail sooner) -LODConfig.shared.lodBias = 1.5 // Performance mode -LODConfig.shared.lodBias = 0.75 // Quality mode - -// Adjust hysteresis to prevent flickering -LODConfig.shared.hysteresis = 10.0 - -// Enable fade transitions between LODs - Not yet implemented -LODConfig.shared.enableFadeTransitions = true -LODConfig.shared.fadeTransitionTime = 0.5 // seconds -``` - -### Forced LOD Override - -Force a specific LOD level (useful for debugging): - -```swift -if let lodComponent = scene.get(component: LODComponent.self, for: tree) { - lodComponent.forcedLOD = 2 // Always show LOD2 - // lodComponent.forcedLOD = nil // Resume automatic LOD selection -} -``` - -### Programmatic LOD Management - -```swift -// Create entity with LOD component -let rock = createEntity() -setEntityLodComponent(entityId: rock) - -// Add LODs dynamically based on performance -let lodFiles = ["rock_LOD0", "rock_LOD1", "rock_LOD2"] -let distances: [Float] = [30.0, 60.0, 120.0] - -for (index, fileName) in lodFiles.enumerated() { - addLODLevel( - entityId: rock, - lodIndex: index, - fileName: fileName, - withExtension: "usdz", - maxDistance: distances[index] - ) -} - -// Check LOD count -let lodCount = getLODLevelCount(entityId: rock) -print("Rock has \(lodCount) LOD levels") - -// Remove highest detail LOD on low-end hardware -if isLowEndDevice { - removeLODLevel(entityId: rock, lodIndex: 0) -} -``` - ---- - -## Best Practices - -### Recommended LOD Counts -- **Small props**: 2-3 LODs -- **Characters**: 3-4 LODs -- **Vehicles**: 3-4 LODs -- **Buildings**: 4-5 LODs -- **Terrain**: 5-8 LODs - -### Polygon Reduction Guidelines -- **LOD0** (full detail): 100% polygons -- **LOD1**: ~50% polygon reduction -- **LOD2**: ~75% polygon reduction -- **LOD3**: ~90% polygon reduction or billboard - -### Distance Thresholds -Base distances on object importance and size: -- **Hero objects**: Longer high-detail distance -- **Background objects**: Shorter high-detail distance -- **Large objects**: Visible from farther away, need more LODs - -### Performance Tips -1. Always use async loading (`setEntityMeshAsync`) for better performance -2. Keep LOD0 for objects within 50 units of camera -3. Use billboards or impostors for very distant objects (LOD3+) -4. Test LOD transitions in-game to ensure smooth visual quality -5. Use `forcedLOD` during development to preview each LOD level - -## Troubleshooting - -### LODs Not Switching -- Verify LOD component is registered: `hasComponent(entityId: tree, componentType: LODComponent.self)` -- Check distance thresholds are set correctly -- Ensure camera has `CameraComponent` and is active - -### Visual Popping Between LODs -- Increase `LODConfig.shared.hysteresis` value -- Enable fade transitions: `LODConfig.shared.enableFadeTransitions = true` - not yet implemented -- Adjust LOD bias for smoother transitions - -### File Not Found Errors -- Verify file organization follows the subdirectory structure -- Check file names match exactly (case-sensitive) -- Ensure files are in the correct `GameData/Models/` path - -### Performance Issues -- Reduce number of LOD levels for less important objects -- Increase distance thresholds to switch LODs sooner -- Use LOD bias > 1.0 for performance mode - -## Example: Complete LOD Setup - -```swift -import UntoldEngine - -// Create multiple trees with LODs -var trees: [EntityID] = [] - -for i in 0..<10 { - let tree = createEntity() - setEntityName(entityId: tree, name: "Tree_\(i)") - - // Position trees - translateTo(entityId: tree, position: simd_float3(Float(i * 10), 0, 0)) - - // Add LOD component - setEntityLodComponent(entityId: tree) - - // Add 4 LOD levels - addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "usdz", maxDistance: 50.0) - addLODLevel(entityId: tree, lodIndex: 1, fileName: "tree_LOD1", withExtension: "usdz", maxDistance: 100.0) - addLODLevel(entityId: tree, lodIndex: 2, fileName: "tree_LOD2", withExtension: "usdz", maxDistance: 200.0) - addLODLevel(entityId: tree, lodIndex: 3, fileName: "tree_LOD3", withExtension: "usdz", maxDistance: 400.0) - - trees.append(tree) -} - -// Configure LOD system for this scene -LODConfig.shared.lodBias = 1.2 // Slightly favor performance -LODConfig.shared.hysteresis = 8.0 // Prevent flickering -LODConfig.shared.enableFadeTransitions = true - -print("Created \(trees.count) trees with LOD support") -``` - -## Example: LOD with Static Batching - -When combining LOD with static batching, ensure transforms and batching setup happen **after** meshes are loaded: - -```swift -import UntoldEngine - -private func setupLODWithBatching() { - var loadedCount = 0 - let totalTrees = 20 - - for i in 0.. Note: Forces are applied per frame. To avoid unintended behavior, only apply forces when necessary. - ---- - -#### Step 6: Use the Steering System -For advanced movement behaviors, leverage the Steering System to steer entities toward or away from targets. This system automatically calculates the required forces. - -Example: Steering Toward a Position - -```swift -steerTo(entityId: redPlayer, targetPosition: simd_float3(0.0, 0.0, 5.0), maxSpeed: 2.0, deltaTime: deltaTime) -``` - ---- - -#### Additional Steering Functions - -The Steering System includes other useful behaviors, such as: - -- steerAway() -- steerPursuit() -- followPath() - -These functions simplify complex movement patterns, making them easy to implement. - ---- - -### What Happens Behind the Scenes? - -1. Physics Simulation: -- Entities with physics enabled are updated each frame to account for forces, gravity, and other dynamic factors. -- Transformations are recalculated based on velocity, acceleration, and forces applied. -2. Realistic Motion: -- The system ensures consistent, physics-based movement without manual updates to the transform. - ---- - -### Running the Simulation -Once you've set up physics, run the project to see it in action: - -1. Launch the project: Your model will appear in the game window. -2. Press "P" to enter Game Mode: -- Gravity and forces will affect the entity. -- If forces are applied, you’ll see dynamic motion in real time. diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingRegistrationSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingRegistrationSystem.md deleted file mode 100644 index 0e9925a5b..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingRegistrationSystem.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -id: registrationsystem -title: Registration System -sidebar_position: 2 ---- - -# Using the Registration System in Untold Engine - -The Registration System in the Untold Engine is an integral part of its Entity-Component-System (ECS) architecture. It provides core functionalities to manage entities and components, such as: - -- Creating and destroying entities. -- Registering components to entities. -- Setting up helper functions for other systems by configuring necessary components. - - -## How to Use the Registration System - -### Step 1: Create an Entity - -Entities represent objects in the scene. Use the createEntity() function to create a new entity. - -```swift -let entity = createEntity() -``` - ---- - -### Step 2: Register Components - -Components define the behavior or attributes of an entity. Use registerComponent to add a component to an entity. - -```swift -registerComponent(entityId: entity, componentType: RenderComponent.self) -``` -Example: - -When you load a mesh for rendering, the system automatically registers the required components: - -```swift -setEntityMesh(entityId: entity, filename: "model", withExtension: "usdz") -``` - -This function: - -- Loads the mesh from the specified .usdc file. -- Associates the mesh with the entity. -- Registers default components like RenderComponent and TransformComponent. - ---- - -### Step 3: Destroy an Entity - -To remove an entity and its components from the scene, use destroyEntity. - -```swift -destroyEntity(entityId: entity) -``` - -This ensures the entity is properly removed from all systems. - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingRenderingSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingRenderingSystem.md deleted file mode 100644 index c42d5ae1a..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingRenderingSystem.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -id: renderingsystem -title: Rendering System -sidebar_position: 2 ---- - -# Enabling Rendering System in Untold Engine - -The Rendering System in the Untold Engine is responsible for displaying your models on the screen. It supports advanced features such as Physically Based Rendering (PBR) for realistic visuals and multiple types of lights to illuminate your scenes. - -## How to Enable the Rendering System - -### Step 1: Create an Entity - -Start by creating an entity that represents your 3D object. - -```swift -let entity = createEntity() -``` ---- - -### Step 2: Link a Mesh to the Entity - -To display a model, load its .usdc file and link it to the entity using setEntityMesh. - -```swift -setEntityMesh(entityId: entity, filename: "entity", withExtension: "usdz") -``` - -Parameters: - -- entityId: The ID of the entity created earlier. -- filename: The name of the .usdc file (without the extension). -- withExtension: The file extension, typically "usdz". - -> Note: If PBR textures (e.g., albedo, normal, roughness, metallic maps) are included, the rendering system will automatically use the appropriate PBR shader to render the model with realistic lighting and material properties. - ---- - -### Running the Rendering System - -Once everything is set up: - -1. Run the project. -2. Your model will appear in the game window, illuminated by the configured lights. -3. If the model is not visible or appears flat, revisit the lighting and texture setup to ensure everything is loaded correctly. - ---- - -## Common Issues and Fixes - -#### Issue: My Model Isn’t Visible! - -- Cause: The scene lacks a light source. -- Solution: Add a directional or point light as shown above. Lighting is required to render objects visibly. - -#### Issue: Model Appears Flat or Dull - -- Cause: PBR textures are missing or not linked properly. -- Solution: Ensure the .usdc file includes the correct PBR textures, and verify their paths during the loading process. - -#### Debugging Tip: - -- Log the addition of lights and entities to verify the scene setup. -- Ensure the position of the point light is within the visible range of the camera and the objects it is meant to illuminate. - ---- - -### Tips and Best Practices - -- Combine Light Types: Use directional lights for overall scene lighting and point lights for localized effects. -- Use PBR Materials: Provide high-quality PBR textures for realistic rendering. -- Position Lights Intelligently: Place point lights strategically to highlight key areas without excessive overlap. - ---- - - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingScenegraph.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingScenegraph.md deleted file mode 100644 index 6dbdcb6af..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingScenegraph.md +++ /dev/null @@ -1,40 +0,0 @@ ---- -id: scenegraphsystem -title: Scenegraph System -sidebar_position: 8 ---- - -# Adding Parent-Child Relationships in Untold Engine - -The Untold Engine includes a Scene Graph data structure, designed to manage hierarchical transformations efficiently. This enables parent-child relationships between entities, where a child's transformation (position, rotation, scale) is relative to its parent. For example, a car's wheels (children) move and rotate relative to the car body (parent). - -## Why Use Parent-Child Relationships? - -Parent-child relationships are useful when you want multiple entities to move or transform together. When a parent entity changes its position, rotation, or scale, its child entities inherit those changes automatically. This is ideal for scenarios like: - -- A car (parent) and its wheels (children) -- A robot (parent) with movable arms and legs (children) -- A group of objects that should remain in a fixed configuration relative to each other - -## Assigning Parent-Child Relationships - -To assign a parent to an entity, use the setParent function. This function establishes a hierarchical relationship between the specified entities. - -```swift -// Create child and parent entities -let childEntity = createEntity() -let parentEntity = createEntity() - -// Set parent-child relationship -setParent(childId: childEntity, parentId: parentEntity) -``` - -## What Happens Behind the Scenes? - -1. Transformation Inheritance: -- Once the relationship is established, any transformation applied to the parent entity (e.g., movement, rotation) will automatically affect the child entity. -- The child’s transformation is expressed relative to the parent. - -2. Independent Local Transformations: -- While the child inherits the parent's transformations, it can also have its own independent local transformations, such as offset positions or rotations relative to the parent. - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingStaticBatchingSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingStaticBatchingSystem.md deleted file mode 100644 index 672578390..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingStaticBatchingSystem.md +++ /dev/null @@ -1,342 +0,0 @@ ---- -id: staticbatchingsystem -title: Static Batching System -sidebar_position: 11 ---- - -# Static Batching System - Usage Guide - -The Untold Engine provides a static batching system that dramatically reduces draw calls by combining static (non-moving) geometry into optimized batches. - -> **Choose Your Path:** You can set up Static Batching via the **Editor** (no code required) or **programmatically** in Swift. - ---- - -## Using the Editor - -### Step 1: Mark Entities as Static - -1. **Select an entity** with a Render Component in the Scene Hierarchy -2. In the **Inspector**, find the **"Static Batching"** section -3. Toggle **"Mark as Static"** (or "Mark Children as Static" for parent entities) - -### Step 2: Enable the Batching System - -1. Open the **Static Batching panel** in the editor sidebar -2. Toggle **"Enable Batching"** to ON - -### Step 3: Generate Batches - -1. Click **"Generate Batches"** -2. A success message will appear -3. The **Active Batches** count shows how many batch groups were created - -### Managing Batches - -- **Clear Batches**: Removes all generated batches -- **Regenerate**: Click "Generate Batches" again after marking new entities - -### Important Notes - -- **Moving a static entity** automatically removes it from batching and regenerates batches -- Batches are grouped by **material** — objects with the same material are combined -- You can mark/unmark entities as static at any time, then regenerate - ---- - -## Using Code - -### Quick Start - -### Basic Static Batching Setup - -```swift -// Create entities -let cube1 = createEntity() -setEntityMesh(entityId: cube1, filename: "cube", withExtension: "usdz") -translateTo(entityId: cube1, position: simd_float3(0, 0, 0)) - -let cube2 = createEntity() -setEntityMesh(entityId: cube2, filename: "cube", withExtension: "usdz") -translateTo(entityId: cube2, position: simd_float3(2, 0, 0)) - -let cube3 = createEntity() -setEntityMesh(entityId: cube3, filename: "cube", withExtension: "usdz") -translateTo(entityId: cube3, position: simd_float3(4, 0, 0)) - -// Mark entities as static -setEntityStaticBatchComponent(entityId: cube1) -setEntityStaticBatchComponent(entityId: cube2) -setEntityStaticBatchComponent(entityId: cube3) - -// Enable batching and generate batches -enableBatching(true) -generateBatches() -``` - -**How it works:** -- Static entities are marked for batching -- `generateBatches()` combines entities with the same material into batch groups -- Rendering system uses batched draw calls instead of per-entity calls - -### With Async Mesh Loading (Recommended) - -For better performance, use async loading and enable batching in the completion handler: - -```swift -let stadium = createEntity() -setEntityMeshAsync(entityId: stadium, filename: "stadium", withExtension: "usdz") { success in - if success { - print("Scene loaded successfully") - - // Mark as static AFTER mesh is loaded - setEntityStaticBatchComponent(entityId: stadium) - - // Enable batching system - enableBatching(true) - - // Generate batches - generateBatches() - } -} -``` - -**Important:** Always call `setEntityStaticBatchComponent()` **after** the mesh loads successfully, then enable and generate batches. - -### Multi-Mesh Assets (USDZ with Multiple Objects) - -For USDZ files with multiple meshes (like a building with walls, roof, windows): - -```swift -let building = createEntity() -setEntityMeshAsync(entityId: building, filename: "office_building", withExtension: "usdz") { success in - if success { - // Mark parent entity - automatically marks all children as static - setEntityStaticBatchComponent(entityId: building) - - enableBatching(true) - generateBatches() - } -} -``` - -**How it works:** `setEntityStaticBatchComponent()` recursively marks the parent and all children, so the entire building is batched. - -## API Reference - -### Core Functions - -#### `setEntityStaticBatchComponent(entityId:)` -Marks an entity (and all its children) as static for batching. - -```swift -setEntityStaticBatchComponent(entityId: entity) -``` - -**Note:** Entity must have a `RenderComponent` (i.e., mesh must be loaded). - -#### `removeEntityStaticBatchComponent(entityId:)` -Removes static batching from an entity (and all its children). - -```swift -removeEntityStaticBatchComponent(entityId: entity) -``` - -**Use case:** If you need to move a previously static object. - -#### `enableBatching(_:)` -Globally enables or disables the batching system. - -```swift -enableBatching(true) // Enable batching -enableBatching(false) // Disable batching -``` - -#### `isBatchingEnabled() -> Bool` -Checks if batching is currently enabled. - -```swift -if isBatchingEnabled() { - print("Batching is active") -} -``` - -#### `generateBatches()` -Generates batch groups from all entities marked as static. - -```swift -generateBatches() -``` - -**Important:** Call this after marking entities as static and enabling batching. - -#### `clearSceneBatches()` -Clears all generated batches. - -```swift -clearSceneBatches() -``` - -**Use case:** When loading a new scene or reconfiguring static geometry. - -## Complete Workflow Examples - -### Example 1: Multiple Static Objects - -```swift -import UntoldEngine - -// Create multiple static props -var props: [EntityID] = [] - -for i in 0..<50 { - let rock = createEntity() - setEntityName(entityId: rock, name: "Rock_\(i)") - - // Load mesh - setEntityMesh(entityId: rock, filename: "rock", withExtension: "usdz") - - // Position randomly - let x = Float.random(in: -20...20) - let z = Float.random(in: -20...20) - translateTo(entityId: rock, position: simd_float3(x, 0, z)) - - // Mark as static - setEntityStaticBatchComponent(entityId: rock) - - props.append(rock) -} - -// Enable and generate batches -enableBatching(true) -generateBatches() - -print("Batched \(props.count) rocks") -``` - -### Example 2: Scene Loading with Batching - -```swift -// Load scene from file -if let sceneData = loadGameScene(from: sceneURL) { - deserializeScene(sceneData: sceneData) - - // Scene automatically restores StaticBatchComponent for marked entities - // Enable batching and generate - enableBatching(true) - generateBatches() - - print("Scene loaded with batching enabled") -} -``` - -### Example 3: Dynamic Scene with Mixed Objects - -```swift -// Static environment -let ground = createEntity() -setEntityMesh(entityId: ground, filename: "ground_plane", withExtension: "usdz") -setEntityStaticBatchComponent(entityId: ground) - -let walls = createEntity() -setEntityMesh(entityId: walls, filename: "walls", withExtension: "usdz") -setEntityStaticBatchComponent(entityId: walls) - -// Dynamic objects (NOT marked as static) -let player = createEntity() -setEntityMesh(entityId: player, filename: "character", withExtension: "usdz") -// Do NOT call setEntityStaticBatchComponent for moving objects - -let enemy = createEntity() -setEntityMesh(entityId: enemy, filename: "enemy", withExtension: "usdz") -// Enemies move, so no static batching - -// Enable batching (only affects static entities) -enableBatching(true) -generateBatches() -``` - -### Example 4: Large Async Scene Loading - -```swift -let cityBlock = createEntity() -setEntityMeshAsync(entityId: cityBlock, filename: "city_block", withExtension: "usdz") { success in - if success { - print("City block loaded with all buildings") - - // Mark entire hierarchy as static - setEntityStaticBatchComponent(entityId: cityBlock) - - // Enable batching system - enableBatching(true) - - // Generate batches - generateBatches() - - print("Static batching enabled - draw calls optimized") - } else { - print("Failed to load city block") - } -} -``` ---- - -## Best Practices - -### What to Mark as Static -✅ **Good candidates:** -- Environment geometry (walls, floors, ceilings) -- Props that never move (rocks, trees, furniture) -- Buildings and structures -- Terrain meshes -- Static decorations - -❌ **Bad candidates:** -- Characters and NPCs -- Vehicles -- Projectiles -- Animated objects -- UI elements - -### Batching Requirements -For entities to batch together, they must have: -- ✅ Same material (textures, colors) -- ✅ `StaticBatchComponent` marked -- ✅ Valid `RenderComponent` (mesh loaded) - -Entities with different materials will be in separate batch groups. - -### Performance Tips - -1. **Mark entities AFTER mesh loading:** - ```swift - setEntityMeshAsync(...) { success in - setEntityStaticBatchComponent(entityId: entity) // ✅ Correct timing - } - ``` - -2. **Enable batching once per scene:** - ```swift - // Game initialization or scene load - enableBatching(true) - generateBatches() - ``` - -3. **Group entities by material:** - - Entities with the same material batch better - - Reduce material variations for better batching - -4. **Regenerate batches when needed:** - ```swift - // When adding/removing static entities - clearSceneBatches() - generateBatches() - ``` - -## Limitations - -- **No dynamic batching:** Only works for static geometry -- **Transform baked:** Entity positions are baked into batch geometry -- **Material grouping:** Different materials create separate batches -- **No skeletal meshes:** Animated/skinned meshes cannot be batched - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingTransformSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingTransformSystem.md deleted file mode 100644 index 874cec3c9..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/UsingTransformSystem.md +++ /dev/null @@ -1,141 +0,0 @@ ---- -id: transformsystem -title: Transform System -sidebar_position: 3 ---- - -# Using the Transform System in Untold Engine - -The Transform System is a core part of the Untold Engine, responsible for managing the position, rotation, and scale of entities. It provides both local transformations (relative to a parent entity) and world transformations (absolute in the scene). - -## How to Use the Transform System - -### Step 1: Retrieve Transform Data -You can retrieve an entity’s position, orientation, or axis vectors using the provided functions. - -#### Get Local Position - -Retrieves the entity’s position relative to its parent. - -```swift -let localPosition = getLocalPosition(entityId: entity) -``` - -#### Get World Position - -Retrieves the entity’s absolute position in the scene. - -```swift -let worldPosition = getPosition(entityId: entity) -``` - -#### Get Local Orientation - -Retrieves the entity’s orientation matrix relative to its parent. - -```swift -let localOrientation = getLocalOrientation(entityId: entity) -``` - -#### Get World Orientation - -Retrieves the entity’s absolute orientation matrix. - -```swift -let worldOrientation = getOrientation(entityId: entity) -``` - -#### Get Axis Vectors - -Retrieve the entity’s forward, right, or up axis: - -```swift -let forward = getForwardAxisVector(entityId: entity) -let right = getRightAxisVector(entityId: entity) -let up = getUpAxisVector(entityId: entity) -``` - ---- - -### Step 2: Update Transform Data - -Modify an entity’s transform by translating or rotating it. - -#### Translate the Entity - -Move the entity to a new position: - -```swift -translateTo(entityId: entity, position: simd_float3(5.0, 0.0, 3.0)) -``` - -Move the entity by an offset relative to its current position: - -```swift -translateBy(entityId: entity, position: simd_float3(1.0, 0.0, 0.0)) -``` - -#### Rotate the Entity - -Rotate the entity to a specific angle around an axis: - -```swift -rotateTo(entityId: entity, angle: 45.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Apply an incremental rotation to the entity: - -```swift -rotateBy(entityId: entity, angle: 15.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Directly set the entity’s rotation matrix: - -```swift -rotateTo(entityId: entity, rotation: simd_float4x4( /* matrix values */ )) -``` - ---- - -## What Happens Behind the Scenes? - -1. Local and World Transform Components: -- Each entity has a LocalTransformComponent for transformations relative to its parent. -- The WorldTransformComponent calculates the absolute transform by combining the local transform with the parent’s world transform. -2. Transform Matrices: -- Transformations are stored in 4x4 matrices that include position, rotation, and scale. -- These matrices are updated whenever you translate or rotate an entity. -3. Scene Graph Integration: -- Changes to a parent entity automatically propagate to its children through the scene graph. - ---- - -### Step 3: Translate the Entire Scene - -These functions move the **scene root** without modifying individual entity transforms. Because per-entity transforms stay untouched, static batches remain intact and no rebatching is needed. - -#### Move Scene to an Absolute Position - -```swift -translateSceneTo(position: simd_float3(10.0, 0.0, 5.0)) -``` - -#### Move Scene by a Relative Offset - -```swift -translateSceneBy(delta: simd_float3(1.0, 0.0, 0.0)) -``` - -Use these when you need to pan or reposition the entire world — for example, sliding a map or architectural model during a spatial drag gesture. - ---- - -## Tips and Best Practices -- Use Local Transformations for Hierarchies: - - For example, a car’s wheels (children) should use local transforms relative to the car body (parent). -- Combine Translations and Rotations: - - Use translateTo to set an entity's absolute position or rotation. - - Use translateBy for incremental adjustments. -- Use Scene-Level Translation for Batch-Safe Movement: - - Use `translateSceneTo` / `translateSceneBy` instead of moving every entity individually. - - This avoids breaking static batches and is ideal for spatial drag gestures on the whole scene. diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/_category.json b/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/_category.json deleted file mode 100644 index a1c32169e..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/03-Engine Systems/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Engine Systems", "position": 4, "collapsed": false } diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/04-Custom Components/_category.json b/website/versioned_docs/version-0.10.10/04-Engine Development/04-Custom Components/_category.json deleted file mode 100644 index 71b201664..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/04-Custom Components/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Custom Components", "position": 5, "collapsed": false } diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/04-Custom Components/customComponent.md b/website/versioned_docs/version-0.10.10/04-Engine Development/04-Custom Components/customComponent.md deleted file mode 100644 index d327a3577..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/04-Custom Components/customComponent.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -id: ecs-create-custom-component -title: Create a Custom Component -sidebar_position: 1 ---- - -# Create a Custom Component - -In your game, you may want to **extend functionality to an entity**. -You can do this by creating a **custom component**. - -Components in the Untold Engine are **data-only objects** that you attach to entities. -They should hold **state, not behavior**. All game logic is handled in systems. - -Every custom component must conform to the `Component` protocol. - -By following this design, your game stays modular: -- **Components** define what an entity *is capable of*. -- **Systems** define *how that capability behaves*. - ---- - -## Minimal Template - -Here’s an example of a simple custom component for a soccer player’s dribbling behavior: - -```swift -public class DribblinComponent: Component { - public required init() {} - var maxSpeed: Float = 5.0 - var kickSpeed: Float = 15.0 - var direction: simd_float3 = .zero -} -``` - -> ⚠️ Note: Components should not include functions or game logic. Keep them as pure data containers. - -## Attaching a Component to an Entity - -Once you’ve defined a component, you attach it to an entity in your scene: - -```swift -let player = createEntity(name: "player") - -// Attach DribblinComponent to the entity -registerComponent(entityId: player, componentType: DribblinComponent.self) - -// Access and modify component data -if let c = scene.get(component: DribblinComponent.self, for: player) { - c.maxSpeed = 6.5 - c.kickSpeed = 18.0 -} - -``` - -This example creates a new entity called player, attaches a DribblinComponent, and updates its values. - - -On its own, the component just stores numbers — it doesn’t do anything yet. -To make the player actually dribble, you’ll need to implement a system that processes this component each frame. diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/04-Custom Components/customSystem.md b/website/versioned_docs/version-0.10.10/04-Engine Development/04-Custom Components/customSystem.md deleted file mode 100644 index 0b6ed75bc..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/04-Custom Components/customSystem.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: ecs-create-custom-system -title: Create a Custom System -sidebar_position: 2 ---- - -# Create a Custom System - -If you’ve created a **custom component**, you’ll usually also want to create a **custom system** to make it do something. -Components store the data, but systems are where the behavior lives. - -The engine automatically calls systems every frame. A system typically: - -1. Resolves the component IDs it cares about -2. Queries entities that have those components -3. Reads and updates their state (transforms, physics, animation, etc.) - -This separation ensures components remain pure data containers, while systems drive the simulation. - ---- - -## Minimal Template - -Here’s a simple system that works with the `DribblinComponent` we defined earlier: - -```swift -public func dribblingSystemUpdate(deltaTime: Float) { - // 1. Get the ID of the DribblinComponent - let customId = getComponentId(for: DribblinComponent.self) - - // 2. Query all entities that have this component - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - // 3. Loop through each entity and update its data - for entity in entities { - guard let dribblingComponent = scene.get(component: DribblinComponent.self, for: entity) else { - continue - } - - // Example logic: move player in the dribbling direction - dribblingComponent.direction = simd_normalize(dribblingComponent.direction) - let displacement = dribblingComponent.direction * dribblingComponent.maxSpeed * deltaTime - - if let transform = scene.get(component: LocalTransformComponent.self, for: entity) { - transform.position += displacement - } - } -} -``` - -## Registering the System -All custom systems must be registered during initialization so the engine knows to run them every frame: - -```swift -registerCustomSystem(dribblingSystemUpdate) -``` - diff --git a/website/versioned_docs/version-0.10.10/04-Engine Development/_category.json b/website/versioned_docs/version-0.10.10/04-Engine Development/_category.json deleted file mode 100644 index e71fa89a8..000000000 --- a/website/versioned_docs/version-0.10.10/04-Engine Development/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "Engine Development", "position": 3, "collapsed": false } - diff --git a/website/versioned_docs/version-0.10.10/05-Editor Development/01-Overview.md b/website/versioned_docs/version-0.10.10/05-Editor Development/01-Overview.md deleted file mode 100644 index 9abb30205..000000000 --- a/website/versioned_docs/version-0.10.10/05-Editor Development/01-Overview.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -id: editoroverview -title: Overview -sidebar_position: 1 ---- - -# Editor Development Overview - -This section is for developers who want to **improve or extend the Untold Editor**. - -The editor is a separate application built on top of Untold Engine. - ---- - -## What You’ll Work On - -In this section, you’ll learn how to: -- Understand the editor architecture -- Add or modify editor views -- Improve workflows and tooling -- Integrate editor features with the engine - -Editor development focuses on **usability and tooling**, not gameplay. - ---- - -## Installing the Editor for Development - -Working on the editor requires installing Untold Editor via the command line and running it from source. - -For editor development, clone this repository: - -```bash -git clone https://github.com/untoldengine/UntoldEditor.git -cd UntoldEditor -``` -The Editor is a Swift Package with an executable target named UntoldEditor. -It declares a dependency on the Untold Engine package; Xcode/SwiftPM will resolve it automatically. - -### Open in Xcode - -1. Open Xcode → File ▸ Open → select the Package.swift in this repo -2. Xcode will create a workspace view for the package -3. Choose the UntoldEditor scheme → Run - -### Build & run via CLI -If you prefer working with the terminal, you can build and run the Editor as follows: - -```bash -swift build -swift run UntoldEditor -``` - -### Pinning the Engine Dependency - -By default, this repo pins Untold Engine to a released version. -If you want the latest engine changes: - -#### Option A — Xcode UI -- In the project navigator: Package Dependencies → UntoldEngine -- Set Dependency Rule to Branch and type develop - -#### Option B — Edit Package.swift - -```swift -.dependencies = [ - .package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop") -] -``` - -Then reload packages: - -```bash -xcodebuild -resolvePackageDependencies -# or in Xcode: File ▸ Packages ▸ Resolve Package Versions -``` - - -The editor can be developed independently of game projects. - ---- - -## 🕹 Using the Editor - -1. **Create / Open a Project** – Use the start screen or File menu -2. **Set Asset Folder** – Choose an **external** directory for your project’s assets -3. **Import Assets** – Drag files in or use the Asset Browser “+” -4. **Build Your Scene** – Create entities, add components, use gizmos to position -5. **Play Mode** – Toggle to simulate and validate behavior -6. **Save / Load** – Save project and scene files; reopen later to continue - -> 💡 Why an *external* asset folder? -> It enables **runtime importing** and iteration without copying everything into the app bundle. - - -## Editor Architecture - -The Untold Editor is structured around: -- Independent views -- Shared engine state -- Explicit data flow between UI and engine -- Minimal magic - -Understanding the editor architecture is key before adding features. - -You can start with: - -> **Editor Development → Architecture** - ---- - -## Editor Views and Interaction - -The editor is composed of views such as: -- Scene View -- Inspector -- Asset Browser -- Scene Hierarchy -- Code Editor (note: USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - -Each view is designed to be modular and replaceable. - ---- - -## Who This Section Is Not For - -This section is **not** intended for: -- Game developers building gameplay -- Engine developers working on core systems -- Users looking to learn the editor UI - -If you want to *use* the editor, see: - -> **Game Development → Using the Editor** - diff --git a/website/versioned_docs/version-0.10.10/05-Editor Development/02-Architecture/EditorArchitecture.md b/website/versioned_docs/version-0.10.10/05-Editor Development/02-Architecture/EditorArchitecture.md deleted file mode 100644 index c2d198577..000000000 --- a/website/versioned_docs/version-0.10.10/05-Editor Development/02-Architecture/EditorArchitecture.md +++ /dev/null @@ -1,187 +0,0 @@ -# Editor Architecture - -This document provides a high-level overview of the **Untold Editor architecture**. - -It is intended for contributors who want to understand how the editor is structured before working on specific views, tools, or workflows. - -This page focuses on **concepts, responsibilities, and data flow**, not implementation details. - ---- - -## Architectural Goals - -The Untold Editor is designed with the following goals: - -- **Editor as a client of the engine** - The editor uses the same runtime as the game. - -- **Explicit data flow** - Editor actions translate directly into engine state changes. - -- **Minimal editor-only behavior** - Play mode mirrors runtime behavior as closely as possible. - -- **Composable tools and views** - Editor features should be modular and replaceable. - -The editor is a tool — not a separate simulation environment. - ---- - -## High-Level Structure - -At a conceptual level, the editor is organized into these layers: - -Editor UI (Views & Tools) -↓ -Editor Coordination Layer -↓ -Engine Runtime -↓ -Platform & Rendering Backends - -The editor does not own the engine — it **drives** it. - ---- - -## Editor as an Engine Client - -The Untold Editor runs on top of the same engine runtime used by games. - -Key implications: -- Scene data is real engine data -- Systems execute through the same update loop -- Rendering paths are shared -- Bugs reproduced in the editor are runtime-relevant - -There is no “fake” editor simulation. - ---- - -## Editor Coordination Layer - -Between the UI and the engine sits a thin coordination layer. - -This layer is responsible for: -- Translating UI actions into engine operations -- Managing selection state -- Coordinating editor-only modes (Edit vs Play) -- Routing commands between views - -It does **not** contain simulation logic. - ---- - -## Views - -The editor is composed of **independent views**, each with a focused responsibility. - -Typical views include: -- Scene View -- Inspector -- Scene Hierarchy -- Asset Browser -- Code Editor (USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - -Views: -- Observe engine state -- Emit commands -- Do not own core data - -This keeps views simple and interchangeable. - ---- - -## Tools and Interaction - -Editor tools (selection, transform, manipulation, etc.) are built as: -- Stateless or minimally stateful controllers -- Operating on selected engine entities -- Emitting explicit transform or component changes - -Input handling is centralized and routed to active tools. - ---- - -## Scene Editing Model - -Scene editing operates directly on engine data: - -- Entities and components are real -- Transforms update immediately -- Changes are visible to all views - -Editor-only metadata (selection, highlighting, gizmos) is stored separately. - ---- - -## Edit Mode vs Play Mode - -The editor supports two primary modes: - -### Edit Mode -- Systems that mutate simulation state are paused -- Editor tools manipulate entity state directly -- Scene changes are persistent - -### Play Mode -- Full engine update loop runs -- USC scripts execute -- Physics and animation systems are active -- Scene state may be restored on exit - -The transition between modes is explicit and controlled. - ---- - -## Asset and Resource Handling - -The editor manages assets as references to engine resources. - -Responsibilities include: -- Importing external files -- Tracking asset paths -- Updating resource bindings -- Refreshing views when assets change - -The engine remains the owner of resource lifetimes. - ---- - -## Relationship to USC - -USC scripts are authored in Xcode and managed through the editor but executed by the engine. The editor itself does not host a built-in USC script editor. - -The editor: -- Creates script source files -- Triggers script builds -- Attaches generated scripts to entities - -The editor does not interpret or execute USC logic. - ---- - -## Design Tradeoffs - -The Untold Editor intentionally avoids: - -- Duplicating engine logic -- Editor-only simulation paths -- Heavy UI-driven state mutation -- Hidden side effects from tools - -These tradeoffs prioritize: -- Consistency with runtime behavior -- Debuggability -- Contributor approachability - ---- - -## Architecture in Practice - -This document describes the conceptual structure of the editor. - -For implementation details, see: - -Editor Development → Architecture → Internals - diff --git a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/AssetBrowserView.md b/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/AssetBrowserView.md deleted file mode 100644 index 5f4a73fa3..000000000 --- a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/AssetBrowserView.md +++ /dev/null @@ -1,128 +0,0 @@ -# Asset Browser View - -The **Asset Browser View** is the discovery and selection surface for project assets within the Untold Editor. - -It presents filtered asset metadata, supports browsing and search, and emits asset selection or assignment intents through the coordination layer. - -This document describes the **architectural role** of the Asset Browser View, not how to use it as an end user. - ---- - -## Purpose - -The Asset Browser View exists to: - -- Surface the project’s asset library for discovery -- Enable filtering, searching, and grouping of assets -- Provide a control point for selecting assets -- Initiate asset assignment intents (e.g., “assign mesh,” “assign script”) without owning import or load logic - -It is the bridge between asset metadata and editor interactions that consume assets. - ---- - -## Responsibilities - -The Asset Browser View is responsible for: - -- Displaying asset entries with relevant metadata and previews -- Providing search, filter, and organization affordances -- Reflecting shared selection state for assets -- Emitting asset selection changes -- Emitting asset assignment intents to other systems (e.g., drag-and-drop targets) via commands -- Respecting editor mode (Edit vs Play) where assignment is allowed - -The Asset Browser View does **not** own asset data or selection state. - ---- - -## What This View Does NOT Do - -The Asset Browser View intentionally does **not**: - -- Own or manage asset import, processing, or versioning -- Load GPU resources or instantiate runtime objects -- Apply asset assignments directly; it emits intents through the coordination layer -- Manage project file I/O beyond reading metadata exposed by asset services -- Execute scripts or run simulation logic -- Decide selection globally; it only requests changes through shared state - -If asset browser behavior appears to require import, loading, or state ownership, that logic belongs elsewhere. - ---- - -## Data Flow - -The Asset Browser View participates in the editor data flow as follows: - -### Reads -- Asset catalog metadata (names, types, tags, thumbnails, availability) -- Asset selection state -- Editor mode state -- Search/filter parameters provided by shared state or local UI - -### Emits -- Asset selection change requests -- Asset assignment intents (e.g., assign to component field, instantiate into scene) -- Folder or collection navigation commands (if supported by asset services) -- Mode-aware warnings when an assignment is not allowed - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Asset Browser View interacts indirectly with other views via shared editor state: - -- **Scene View / Scene Hierarchy** - Receives asset drops or assignment intents to create or replace scene content - -- **Inspector** - Accepts asset assignments into component fields - -- **Code Editor** - May reference scripts as assets; USC authoring itself happens in Xcode, not inside the editor - -The Asset Browser View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full asset browsing, selection, and assignment intents are available -- Assignments update authoring data through commands - -### Play Mode -- Asset browsing remains available; assignments that mutate authoring data may be blocked or limited -- Runtime-safe assignments (if supported) are routed through the same command pathways - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Asset Browser View by: - -- Adding advanced filters (tags, dependencies, usage frequency) -- Introducing custom previews or inspectors for specific asset types -- Extending drag-and-drop behaviors for assignment targets -- Adding collection/saved search features backed by shared state - -Extensions should integrate through existing asset metadata sources and command pathways. - ---- - -## Design Constraints - -The Asset Browser View is intentionally constrained to: - -- Visualization of asset metadata and selection -- Command emission for selection and assignment intents -- Stateless or minimal local UI state driven by shared data -- No direct import, loading, or mutation of asset storage - -Keeping these boundaries strict ensures predictable asset workflows and debuggability. - diff --git a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/CodeEditorView.md b/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/CodeEditorView.md deleted file mode 100644 index 89753d95c..000000000 --- a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/CodeEditorView.md +++ /dev/null @@ -1,129 +0,0 @@ -# Code Editor View - -The **Code Editor View** is the text editing surface for scripts and other source assets in the Untold Editor. USC scripts themselves are authored in Xcode; the editor does not provide a built-in USC script editor. - -It presents source files, supports editing workflows, and emits save/build/run intents through the coordination layer without owning compilation or runtime. - -This document describes the **architectural role** of the Code Editor View, not how to use it as an end user. - ---- - -## Purpose - -The Code Editor View exists to: - -- Surface project scripts and source files for editing -- Provide an authoring surface for text assets; USC authoring lives in Xcode and is only surfaced here for viewing/coordination -- Bridge editing to build and run workflows via explicit commands -- Reflect scripting workflow state (open project, build output) in the editor context - -It is the link between source assets and scripting workflows, not the runtime itself. - ---- - -## Responsibilities - -The Code Editor View is responsible for: - -- Displaying and editing text buffers for selected source assets -- Reflecting file metadata (path, dirty state, read-only status) -- Emitting save commands for edited buffers -- Emitting build/run/test commands tied to scripting workflows -- Surfacing diagnostics and build output provided by external services -- Respecting editor mode (Edit vs Play) when enabling edits or execution commands - -The Code Editor View does **not** own source storage, compilation, or runtime execution. - ---- - -## What This View Does NOT Do - -The Code Editor View intentionally does **not**: - -- Interpret or execute USC or other scripts -- Own compilation, packaging, or deployment workflows; it triggers them -- Manage source control operations beyond reflecting status indicators -- Own project discovery or dependency resolution -- Apply engine changes directly; all actions flow through commands -- Decide selection globally; it observes selection/open-file state from shared editor context - -If code editor behavior appears to require compilation or runtime ownership, that logic belongs elsewhere. - ---- - -## Data Flow - -The Code Editor View participates in the editor data flow as follows: - -### Reads -- Source file contents and metadata (path, permissions, timestamps) -- Open-file/session state from the editor -- Build/run status and diagnostics from scripting services -- Editor mode state - -### Emits -- Save commands for current buffers -- Build/run/test commands for the active scripting project -- File open/close requests within the editor session -- Selection change requests for symbols or files if shared selection is used - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Code Editor View interacts indirectly with other views via shared editor state: - -- **Asset Browser** - Opens scripts selected in the asset list - -- **Scene View / Scene Hierarchy** - May reflect selection-driven context (e.g., open script referenced by a component) but does not call views directly - -- **Inspector** - Consumes scripts as assignable assets; script edits propagate via save and build commands - -The Code Editor View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full text editing, save, and build/run commands are enabled -- Diagnostics and build output are surfaced for authoring - -### Play Mode -- Editing may be limited or read-only depending on project policy -- Build/run commands that mutate authoring state may be blocked or deferred -- Runtime logs may be surfaced without enabling authoring changes - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Code Editor View by: - -- Adding language services (syntax highlighting, completion, diagnostics) via existing plugin hooks -- Integrating advanced navigation (symbol search, references) through shared project metadata -- Surfacing build/test pipelines with richer progress and error reporting -- Enhancing run configurations to emit explicit launch commands - -Extensions should integrate through existing command pathways and shared scripting state. - ---- - -## Design Constraints - -The Code Editor View is intentionally constrained to: - -- Text editing and presentation of scripting-related feedback -- Command emission for save/build/run -- Stateless or minimal local UI state driven by shared data -- No direct compilation, execution, or mutation of engine state - -Keeping these boundaries strict ensures predictable scripting workflows and debuggability. - diff --git a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/InspectorView.md b/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/InspectorView.md deleted file mode 100644 index 6745678a4..000000000 --- a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/InspectorView.md +++ /dev/null @@ -1,135 +0,0 @@ -# Inspector View - -The **Inspector View** is the focused property panel for examining and editing the selected entities and components in the Untold Editor. - -It presents authoritative component data for the current selection and emits structured changes back through the editor coordination layer. - -This document describes the **architectural role** of the Inspector View, not how to use it as an end user. - ---- - -## Purpose - -The Inspector View exists to: - -- Surface component data for the current selection -- Provide structured editors for component properties -- Validate edits against component rules and modes -- Emit explicit change requests to the coordination layer - -It is the bridge between selection state and component-level editing. - ---- - -## Responsibilities - -The Inspector View is responsible for: - -- Displaying the current selection (single or multi-entity) -- Rendering component sections and property editors -- Showing validation and state (read-only, overridden, default) -- Emitting add/remove component requests -- Emitting property change commands with full context (target entity, component, field, value) -- Reflecting editor mode (Edit vs Play) in allowed operations - -The Inspector View does **not** own component data or selection state. - ---- - -## What This View Does NOT Do - -The Inspector View intentionally does **not**: - -- Manage or decide selection; it only observes it -- Apply engine changes directly; all edits flow through commands -- Run simulation, scripting, or component lifecycle logic -- Own component storage or serialization; it only issues edits -- Own schemas or define component types (it consumes published definitions) -- Manage undo/redo stacks (it emits commands compatible with them) -- Provide asset authoring workflows beyond property-level references - -If Inspector behavior appears to require simulation or data ownership, that logic belongs elsewhere. - ---- - -## Data Flow - -The Inspector View participates in the editor data flow as follows: - -### Reads -- Selection state -- Component definitions and metadata (including editability rules) -- Component instances for selected entities -- Editor mode state -- Validation results and command feedback - -### Emits -- Property change commands -- Component add/remove requests -- Component reorder or enable/disable requests (if supported by schema) -- Mode-aware warnings (e.g., attempted edit in Play mode) - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Inspector View interacts indirectly with other views via shared editor state: - -- **Scene Hierarchy** - Mirrors selection changes driven by hierarchy navigation - -- **Scene View** - Reflects selection changes initiated in world space - -- **Asset Browser** - Accepts drag-and-drop asset references into component fields - -- **Code Editor** - Consumes component schemas defined in code; USC authoring occurs in Xcode, not inside the editor; does not depend on editor code directly - -The Inspector View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full component editing is enabled -- Changes persist to the authoring state -- Validation prevents invalid component configurations - -### Play Mode -- Inspector prioritizes observation; many edits are read-only or limited -- Runtime values may be displayed distinctly from authoring values -- Commands that would mutate persistent data may be blocked or deferred - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Inspector View by: - -- Adding custom property drawers for specific field types -- Providing component-specific inspectors with bespoke UI/validation -- Introducing contextual warnings or hints based on selection -- Enhancing multi-edit behavior across heterogeneous selections - -Extensions should integrate through existing data binding and command pathways. - ---- - -## Design Constraints - -The Inspector View is intentionally constrained to: - -- Observation of selection and component state -- Stateless or minimal local UI state -- Deterministic rendering based on shared data -- Command-only mutations; no direct state writes - -Keeping these boundaries strict ensures predictable editing and debuggability. - diff --git a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/Overview.md b/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/Overview.md deleted file mode 100644 index 109c70294..000000000 --- a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/Overview.md +++ /dev/null @@ -1,172 +0,0 @@ -# Editor Views Overview - -This section documents the **views that make up the Untold Editor**. - -It is intended for contributors who want to understand how editor views are structured, how they interact with the engine, and how they coordinate with each other. - -This is **not** user documentation. -It does not explain how to *use* the editor — only how it is built. - ---- - -## What Is a View? - -A **View** is a UI surface that: -- Observes engine and editor state -- Presents that state visually -- Emits explicit commands in response to user input - -Views do **not** own core data. - -They are clients of the editor and engine systems. - ---- - -## Responsibilities of a View - -Every editor view follows the same core responsibilities: - -- Render information derived from engine or editor state -- React to user input (mouse, keyboard, gestures) -- Emit commands to the editor coordination layer -- Stay stateless or minimally stateful - -A view should be easy to reason about in isolation. - ---- - -## What Views Do NOT Do - -Views intentionally do **not**: - -- Own or mutate engine state directly -- Contain simulation logic -- Manage entity lifetimes -- Execute USC scripts -- Perform rendering or physics work themselves - -If a view feels like it needs to “do logic,” that logic likely belongs elsewhere. - ---- - -## Data Flow Model - -All views participate in a shared data flow model: - -Engine Runtime -↓ -Editor Coordination Layer -↓ -Editor Views -↓ -User Input -↓ -Editor Commands -↓ -Engine Runtime - -Key characteristics: -- Data flows downward -- Commands flow upward -- Views never bypass the coordination layer - -This keeps behavior predictable and debuggable. - ---- - -## Selection as Shared State - -Selection is a cross-view concern. - -- Views may observe the current selection -- Views may request selection changes -- No single view owns selection state - -This allows multiple views to remain decoupled while staying synchronized. - ---- - -## View Independence - -Views are designed to be independent and replaceable. - -This means: -- Views do not directly call each other -- Communication happens through shared editor state -- Views can be added, removed, or replaced without breaking others - -This is a deliberate architectural decision. - ---- - -## Editor Modes and Views - -Views must respect the editor’s active mode. - -### Edit Mode -- Views manipulate persistent scene data -- Simulation systems are paused -- Tools operate directly on entity state - -### Play Mode -- Views observe runtime state -- Simulation systems are active -- Editing is limited or disabled - -Views should not decide mode behavior — they respond to it. - ---- - -## Common View Patterns - -While views are independent, many follow similar patterns: - -- Scene-oriented views -- Inspection views -- Asset-oriented views -- Code-oriented views - -Each category emphasizes different interactions but follows the same architectural rules. - ---- - -## Extending or Adding a View - -When adding a new view: - -1. Define the view’s purpose -2. Identify the data it observes -3. Identify the commands it emits -4. Ensure it does not own core state -5. Integrate it through the coordination layer - -If a new view requires engine changes, document those explicitly. - ---- - -## View Documentation Structure - -Each individual view document follows this structure: - -- Purpose -- Responsibilities -- What This View Does NOT Do -- Data Flow -- Interaction With Other Views -- Extension Points - -This keeps documentation consistent and easy to scan. - ---- - -## Available Views - -The following views are documented in this section: - -- Scene View -- Inspector -- Scene Hierarchy -- Asset Browser -- Code Editor (USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - ---- diff --git a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/SceneHierarchyView.md b/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/SceneHierarchyView.md deleted file mode 100644 index aad738a1a..000000000 --- a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/SceneHierarchyView.md +++ /dev/null @@ -1,127 +0,0 @@ -# Scene Hierarchy View - -The **Scene Hierarchy View** presents the scene graph as an ordered list/tree of entities and is the primary control surface for selection and hierarchy manipulation. - -It exposes parent/child relationships conceptually and emits structured commands for selection, reparenting, ordering, and basic entity lifecycle actions. - -This document describes the **architectural role** of the Scene Hierarchy View, not how to use it as an end user. - ---- - -## Purpose - -The Scene Hierarchy View exists to: - -- Visualize the scene graph and entity ordering -- Provide a clear control point for selecting entities -- Enable conceptual hierarchy edits (parenting, unparenting, reordering) -- Initiate entity lifecycle actions (create, duplicate, delete) through commands - -It is the bridge between the scene graph model and selection-centric editing. - ---- - -## Responsibilities - -The Scene Hierarchy View is responsible for: - -- Displaying the scene graph structure and entity ordering -- Reflecting shared selection state and allowing selection changes -- Emitting hierarchy edit commands (reparent, reorder, toggle visibility/activation) -- Emitting entity lifecycle requests (create, duplicate, delete) via commands -- Respecting editor mode (Edit vs Play) when enabling hierarchy edits - -The Scene Hierarchy View does **not** own scene data or selection state. - ---- - -## What This View Does NOT Do - -The Scene Hierarchy View intentionally does **not**: - -- Compute transforms or resolve world/local matrices -- Maintain or persist the scene graph; it only visualizes and issues commands -- Perform simulation, physics, or animation updates -- Decide selection globally; it requests changes through shared state -- Apply engine changes directly; all edits flow through the coordination layer -- Execute scripting or component logic - -If hierarchy behavior appears to require graph ownership or simulation, that logic belongs elsewhere. - ---- - -## Data Flow - -The Scene Hierarchy View participates in the editor data flow as follows: - -### Reads -- Scene graph structure (entities, parent/child links, ordering) -- Entity metadata needed for display (names, icons, state flags) -- Selection state -- Editor mode state - -### Emits -- Selection change requests -- Reparent and reorder commands -- Visibility/activation toggle commands (if exposed) -- Entity lifecycle requests (create, duplicate, delete) - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Scene Hierarchy View interacts indirectly with other views via shared editor state: - -- **Scene View** - Synchronizes selection changes between hierarchy and world-space interaction - -- **Inspector** - Displays and edits data for entities selected in the hierarchy - -- **Asset Browser** - May support drag-and-drop of assets to instantiate entities or assign references - -The Scene Hierarchy View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full hierarchy editing (reparent, reorder, lifecycle actions) is available -- Selection changes drive authoring-state inspection - -### Play Mode -- Hierarchy is primarily observational; destructive edits may be blocked or limited -- Selection may reflect runtime entities without persisting authoring changes - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Scene Hierarchy View by: - -- Adding filters, search, or grouping affordances -- Introducing custom badges or state indicators derived from metadata -- Extending drag-and-drop behaviors for entity or asset interactions -- Providing bulk operations that emit explicit hierarchy commands - -Extensions should integrate through existing command pathways and shared state. - ---- - -## Design Constraints - -The Scene Hierarchy View is intentionally constrained to: - -- Visualization of the scene graph and selection state -- Command emission for hierarchy and lifecycle edits -- Stateless or minimal local UI state driven by shared data -- No direct mutation of engine or editor core data - -Keeping these boundaries strict ensures predictable editing and debuggability. - diff --git a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/SceneView.md b/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/SceneView.md deleted file mode 100644 index 984b7fd7b..000000000 --- a/website/versioned_docs/version-0.10.10/05-Editor Development/03-Views/SceneView.md +++ /dev/null @@ -1,158 +0,0 @@ -# Scene View - -The **Scene View** is the primary spatial view in the Untold Editor. - -It provides a visual representation of the current scene and allows contributors and tools to interact with entities in world space. - -This document describes the **architectural role** of the Scene View, not how to use it as an end user. - ---- - -## Purpose - -The Scene View exists to: - -- Visualize the current scene state -- Display entities and their transforms -- Provide spatial context for selection and manipulation -- Act as the main interaction surface for world-space tools - -It is the bridge between engine data and spatial editor interaction. - ---- - -## Responsibilities - -The Scene View is responsible for: - -- Rendering a visual representation of the scene -- Displaying editor overlays (selection outlines, gizmos, helpers) -- Receiving world-space input (mouse, keyboard, gestures) -- Emitting commands related to selection and transformation -- Respecting the current editor mode (Edit vs Play) - -The Scene View does **not** own scene data. - ---- - -## What This View Does NOT Do - -The Scene View intentionally does **not**: - -- Own or modify engine state directly -- Perform simulation, physics, or animation updates -- Decide how entities are selected globally -- Execute USC scripts -- Implement rendering pipelines or render passes - -If the Scene View appears to require simulation logic, that logic belongs elsewhere. - ---- - -## Data Flow - -The Scene View participates in the editor data flow as follows: - -### Reads -- Scene graph data -- Entity transforms -- Camera state -- Selection state -- Editor mode state - -### Emits -- Selection change requests -- Transform manipulation commands -- Camera navigation commands -- Tool activation signals - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Scene View interacts indirectly with other views via shared editor state: - -- **Scene Hierarchy** - Synchronizes selection changes - -- **Inspector** - Displays and edits data for selected entities - -- **Asset Browser** - May initiate drag-and-drop actions into the scene - -The Scene View does not directly communicate with other views. - ---- - -## Tools and Manipulation - -World-space tools (translate, rotate, scale, etc.) are driven through the Scene View. - -Key characteristics: -- Tools operate on selected entities -- Input is routed to the active tool -- Tools emit explicit transform commands -- Visual gizmos are editor-only overlays - -The Scene View hosts tool interaction but does not implement tool logic. - ---- - -## Camera Control - -The Scene View owns the **editor camera**, which is distinct from game cameras. - -Responsibilities include: -- Camera navigation (orbit, pan, zoom) -- Framing selected entities -- Switching camera perspectives - -Editor camera state is isolated from runtime camera components. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Scene View allows full manipulation -- Entity transforms are persistent -- Editor overlays and gizmos are visible - -### Play Mode -- Scene View observes runtime state -- Manipulation may be limited or disabled -- Runtime cameras may be previewed - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Scene View by: - -- Adding new world-space tools -- Introducing new editor overlays -- Customizing camera behavior -- Adding debug visualization layers - -Extensions should integrate through existing tool and command pathways. - ---- - -## Design Constraints - -The Scene View is intentionally constrained to: - -- Visualization -- Input capture -- Command emission - -Keeping these boundaries strict ensures: -- Predictable behavior -- Easier debugging -- Cleaner separation of concerns - diff --git a/website/versioned_docs/version-0.10.10/05-Editor Development/EditorOverview.md b/website/versioned_docs/version-0.10.10/05-Editor Development/EditorOverview.md deleted file mode 100644 index 27704bf80..000000000 --- a/website/versioned_docs/version-0.10.10/05-Editor Development/EditorOverview.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -id: editorfeatures -title: Editor Features -sidebar_position: 2 ---- - -# Untold Engine Editor Features - -The Untold Engine Editor makes it easier than ever to set up your scenes and entities without touching code. With the Editor, you can visually create, configure, and organize your game world, while keeping Swift code focused on game logic and behaviors. - -Here’s a quick tour of the Editor’s main features: - ---- - -## Scene Graph - -![Scene Graph](../images/engine-scenegraph.png) - -The **Scene Graph** shows all the entities in your scene in a hierarchical view. You can add, rename, and organize entities, as well as set up parent–child relationships. This is where you’ll find your cameras, lights, and models at a glance. - ---- - -## Inspector - -![Inspector](../images/engine-inspector.png) - -The **Inspector** lets you configure properties of the selected entity. Adjust position, rotation, and scale, or fine-tune camera settings and add components. Think of it as the control panel for whatever you’re working on. - ---- - -## Gizmo Manipulation - -![Gizmo](../images/engine-gizmo.png) - -Use the **3D gizmo** to interactively move, rotate, and scale entities directly in the scene. This makes it quick to place objects exactly where you want them. - ---- - -## Materials - -![Materials](../images/engine-materials.png) - -Assign meshes and materials through the **Inspector**. You can drop in textures for base color, roughness, metallic, and emissive maps, then tweak material properties to get the right look. - ---- - -## Lighting & Environment - -![Lighting](../images/engine-lights.png) - -The **Lighting panel** give you control over your scene’s mood. Add directional, point, spotlight and area lights, adjust intensities. - -## Environment - -The **Environment panel** enable **Image-Based Lighting (IBL)** for realistic reflections and ambient light. - -![HDR](../images/engine-hdr.png) - ---- - -## Post-Processing Effects - -![Post Processing](../images/engine-post-processing.png) - -The **Effects tab** lets you add and tweak post-processing features such as: -- Depth of Field -- Chromatic Aberration -- Bloom -- Color Grading -- SSAO, Vignette, White Balance, and more - -These effects bring your scenes closer to a polished, production-ready look. - ---- - -## Asset Browser - -![Asset Browser](../images/engine-assetbrowser.png) - -The **Asset Browser** keeps your models, textures, and materials organized. Import new assets, set paths for your project, and quickly assign resources to entities in your scene. - ---- - -## Console Log - -![Console](../images/engine-consolelog.png) - -The **Console Log** provides real-time feedback from the engine. Use it to debug entity creation, monitor system messages, and track issues while working on your scene. - ---- - -## Putting It All Together - -With the Editor, you now have a clear separation of responsibilities: -- **Use the Editor** for entity initialization, scene setup, materials, and visual configuration. -- **Use Swift code** for gameplay logic, physics tweaks, and systems that bring your game to life. - -This workflow makes iteration faster and keeps your codebase focused on what matters most: gameplay. - diff --git a/website/versioned_docs/version-0.10.10/06-Reference/01-USCAPI.md b/website/versioned_docs/version-0.10.10/06-Reference/01-USCAPI.md deleted file mode 100644 index 0c961d1cb..000000000 --- a/website/versioned_docs/version-0.10.10/06-Reference/01-USCAPI.md +++ /dev/null @@ -1,133 +0,0 @@ -# Untold Script Core API - -Use this page as a compact checklist of the most-used USC (Untold Script Core) DSL calls. All snippets assume you’re inside a `buildScript` closure on the current entity (`self`). - ---- - -## Lifecycle -- `onStart()` – one-time init per play. -- `onUpdate()` – every frame. -- `onEvent("Name")` – custom triggers. -- `onCollision(tag:)` – **planned** (not yet available). - -## Properties (get/set) -Supported keys: `.position`, `.scale`, `.velocity`, `.acceleration`, `.mass`, `.angularVelocity` (write-only today), `.intensity`, `.color`, `.deltaTime`. -```swift -.getProperty(.position, as: "pos") -.setProperty(.position, toVariable: "nextPos") -.setProperty(.velocity, to: simd_float3(0, 2, 0)) -``` - -## Input -```swift -.ifKeyPressed("W") { n in n.log("forward") } -.ifKeyReleased("Space") { n.log("jump released") } -.getKeyState("w", as: "wPressed") -``` - -## Transform -```swift -.translateTo(simd_float3(0, 1, 0)) -.translateBy(simd_float3(0, 0, 1)) -.rotateTo(degrees: 45, axis: simd_float3(0, 1, 0)) -.rotateBy(degrees: .float(5), axis: simd_float3(0, 1, 0)) -.lookAt("TargetEntity") -``` - -## Animation -```swift -.playAnimation("Walk", loop: true) -.stopAnimation() -``` - -## Physics (forces/velocity) -```swift -.applyForce(force: simd_float3(0, 10, 0)) -.applyLinearImpulse(direction: .vec3(x: 0, y: 1, z: 0), magnitude: .float(5)) -.setLinearVelocity(.vec3(x: 0, y: 0, z: 5)) -.addLinearVelocity(.vec3(x: 0, y: 0, z: -1)) -.clampLinearSpeed(min: .float(0), max: .float(8)) -.clearVelocity() -.clearAngularVelocity() -.clearForces() -.setGravityScale(0.5) -.pausePhysicsComponent(isPaused: true) -``` - -## Steering (vectors or side effects) -```swift -.seek(targetPosition: .vec3(x: 10, y: 0, z: 0), maxSpeed: .float(5), result: "steer") -.flee(threatPosition: .vec3(x: 0, y: 0, z: 0), maxSpeed: .float(6), result: "steer") -.arrive(targetPosition: .vec3(x: 0, y: 0, z: 0), maxSpeed: .float(6), slowingRadius: .float(2), result: "steer") -.pursuit(targetEntity: .string("Player"), maxSpeed: .float(6), result: "steer") -.evade(threatEntity: .string("Enemy"), maxSpeed: .float(6), result: "steer") -.steerSeek(targetPosition: .variableRef("targetPos"), maxSpeed: .variableRef("maxSpeed"), deltaTime: .variableRef("dt")) -.steerArrive(targetPosition: .variableRef("targetPos"), maxSpeed: .variableRef("maxSpeed"), slowingRadius: .variableRef("slow"), deltaTime: .variableRef("dt")) -.steerFlee(threatPosition: .variableRef("threatPos"), maxSpeed: .variableRef("maxSpeed"), deltaTime: .variableRef("dt")) -.steerPursuit(targetEntity: .string("Target"), maxSpeed: .float(6), deltaTime: .float(0.016)) -.orbit(centerPosition: .vec3(x: 0, y: 0, z: 0), radius: .float(5), maxSpeed: .float(4), deltaTime: .float(0.016)) -.alignOrientation(deltaTime: .float(0.016), turnSpeed: .float(1.0)) -``` - -## Camera -```swift -.cameraMoveTo(.vec3(x: 0, y: 3, z: -10)) -.cameraMoveBy(.vec3(x: 1, y: 0, z: 0)) -.cameraRotate(pitch: .float(0.02), yaw: .float(-0.08)) -.cameraFollow(target: .string("Player"), - offset: .vec3(x: 0, y: 3, z: -6), - smoothFactor: .float(5), - deltaTime: .float(0.016)) -.cameraFollowLocal(target: .string("Player"), - localOffset: .vec3(x: 0, y: 2, z: -4), - smoothFactor: .float(5), - deltaTime: .float(0.016)) -.cameraOrbitTarget(target: .string("Boss"), - radius: .float(12), - speed: .float(1.5), - deltaTime: .float(0.016), - offsetY: .float(1.5)) -.cameraMoveWithInput(speedVar: "moveSpeed", - deltaTimeVar: "dt", - wVar: "wPressed", aVar: "aPressed", - sVar: "sPressed", dVar: "dPressed", - qVar: "qPressed", eVar: "ePressed") -``` - -## Math (variables only) -```swift -.addFloat("a", "b", as: "sum") -.addFloat("a", literal: 1, as: "sum") -.subFloat("a", "b", as: "diff") -.mulFloat("a", "b", as: "prod") -.divFloat("a", "b", as: "quot") -.addVec3("v1", "v2", as: "sum") -.scaleVec3("v", by: "s", as: "out") -.scaleVec3("v", literal: 2, as: "out") -.lengthVec3("v", as: "len") -.normalizeVec3("v", as: "unit") -.dotVec3("a", "b", as: "dot") -.crossVec3("a", "b", as: "cross") -.lerpVec3(from: "a", to: "b", t: "t", as: "out") -.lerpFloat(from: "a", to: "b", t: "t", as: "out") -.reflectVec3("v", normal: "n", as: "reflected") -.projectVec3("v", onto: "axis", as: "proj") -.angleBetweenVec3("a", "b", as: "angleDeg") -.clampFloat("speed", min: "minSpeed", max: "maxSpeed", as: "clamped") -.clampVec3("vel", min: "minVel", max: "maxVel", as: "clampedVel") -``` - -## Flow / Variables / Debug -```swift -.ifCondition(lhs: .variableRef("speed"), .greater, rhs: .float(10)) { n in n.log("Too fast") } -.ifGreater("health", than: 0) { n in n.log("Alive") }.else { n in n.log("Dead") } -.ifEqual("flag", to: true) { n in n.log("Flag set") } -.setVariable("speed", to: 5.0) -.setVariable("dir", to: simd_float3(0, 0, 1)) -.setVariable("copy", fromVariable: "speed") -.log("Hello") -.logValue("velocity", value: .variableRef("vel")) -``` - -> Tip: If you need an entity other than `self`, use names in your instructions (e.g., `.lookAt("TargetName")`) or stash them in variables, then call `findEntity`-driven instructions like `pursuit`/`evade`. - diff --git a/website/versioned_docs/version-0.10.10/06-Reference/02-EngineAPI.md b/website/versioned_docs/version-0.10.10/06-Reference/02-EngineAPI.md deleted file mode 100644 index 0d649d551..000000000 --- a/website/versioned_docs/version-0.10.10/06-Reference/02-EngineAPI.md +++ /dev/null @@ -1,408 +0,0 @@ -# Untold Engine Core API - -## Quick Reference - - -| Category | Common APIs | -|--------------|-------------| -| **Entities** | [createEntity](#create-an-entity), [destroyEntity](#destroy-an-entity), [setParent](#parent-child-relationships), [findEntity](#find-entity) | -| **Transforms** | [getLocalPosition](#get-local-position), [getPosition](#get-world-position), [getLocalOrientation](#get-local-orientation), [getOrientation](#get-world-orientation), [getForwardAxisVector](#get-axis-vectors), [translateTo](#translate-the-entity), [translateBy](#translate-the-entity), [rotateTo](#rotate-the-entity), [rotateBy](#rotate-the-entity) | -| **Assets** | [assetBasePath](#base-path-to-assets) | -| **Rendering** | [setEntityMesh](#link-a-mesh-to-the-entity), [setEntityGaussian](#link-a-gaussian-splat-to-the-entity), [createDirLight](#directional-light), [createPointLight](#point-light), [createSpotLight](#spot-light), [createAreaLight](#area-light) | -| **Animation** | [setEntityAnimations](#load-an-animation), [changeAnimation](#set-the-animation-to-play), [pauseAnimationComponent](#pause-the-animation-optional) | -| **Physics** | [setEntityKinetics](#enable-physics-on-the-entity), [setMass](#configure-physics-properties), [setGravityScale](#configure-physics-properties), [applyForce](#apply-forces-optional), [steerTo](#use-the-steering-system), [steerAway](#additional-steering-functions), [steerPursuit](#additional-steering-functions), [followPath](#additional-steering-functions) | -| **Components** | [registerComponent](#register-components), [create-custom-component](#create-custom-component), [attach-component](#attaching-a-component-to-an-entity) | -| **Systems** | [create-custom-system](#create-custom-system), [registerCustomSystem](#registering-the-custom-system) | - - -# Entities - -### Create an Entity - -Entities represent objects in the scene. Use the createEntity() function to create a new entity. - -```swift -let entity = createEntity() -``` - -### Destroy an Entity - -To remove an entity and its components from the scene, use destroyEntity. - -```swift -destroyEntity(entityId: entity) -``` - -This ensures the entity is properly removed from all systems. - ---- - -### Parent-Child Relationships - -To assign a parent to an entity, use the setParent function. This function establishes a hierarchical relationship between the specified entities. - -```swift -// Create child and parent entities -let childEntity = createEntity() -let parentEntity = createEntity() - -// Set parent-child relationship -setParent(childId: childEntity, parentId: parentEntity) -``` - -### Find Entity - -You can find an entity by name using the following function: - -```swift -let ball = findEntity(name: "ball") -``` ---- - -# Transforms - -### Get Local Position - -Retrieves the entity’s position relative to its parent. - -```swift -let localPosition = getLocalPosition(entityId: entity) -``` - -### Get World Position - -Retrieves the entity’s absolute position in the scene. - -```swift -let worldPosition = getPosition(entityId: entity) -``` - -### Get Local Orientation - -Retrieves the entity’s orientation matrix relative to its parent. - -```swift -let localOrientation = getLocalOrientation(entityId: entity) -``` - -### Get World Orientation - -Retrieves the entity’s absolute orientation matrix. - -```swift -let worldOrientation = getOrientation(entityId: entity) -``` - -### Get Axis Vectors - -Retrieve the entity’s forward, right, or up axis: - -```swift -let forward = getForwardAxisVector(entityId: entity) -let right = getRightAxisVector(entityId: entity) -let up = getUpAxisVector(entityId: entity) -``` - ---- - -### Translate the Entity - -Move the entity to a new position: - -```swift -translateTo(entityId: entity, position: simd_float3(5.0, 0.0, 3.0)) -``` - -Move the entity by an offset relative to its current position: - -```swift -translateBy(entityId: entity, position: simd_float3(1.0, 0.0, 0.0)) -``` - -### Rotate the Entity - -Rotate the entity to a specific angle around an axis: - -```swift -rotateTo(entityId: entity, angle: 45.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Apply an incremental rotation to the entity: - -```swift -rotateBy(entityId: entity, angle: 15.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Directly set the entity’s rotation matrix: - -```swift -rotateTo(entityId: entity, rotation: simd_float4x4( /* matrix values */ )) -``` - - ---- - -### Base path to assets - -Define the asset directory the engine will use to load content (Assuming you are using an external folder during development). Please see Import-Export section for more details. - -```swift -// Here we point it to a folder named "DemoGameAssets/Assets" on the Desktop. -// You can change this to any folder where you keep your own assets. -if let desktopURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first { - assetBasePath = desktopURL.appendingPathComponent("DemoGameAssets/Assets") -} -``` - -# Rendering - -### Link a Mesh to the Entity - -To display a model, load its .usdc file and link it to the entity using setEntityMesh. - -```swift -setEntityMesh(entityId: entity, filename: "entity", withExtension: "usdc") -``` - -Parameters: - -- entityId: The ID of the entity created earlier. -- filename: The name of the .usdc file (without the extension). -- withExtension: The file extension, typically "usdc". - -> Note: If PBR textures (e.g., albedo, normal, roughness, metallic maps) are included, the rendering system will automatically use the appropriate PBR shader to render the model with realistic lighting and material properties. - -### Link a Gaussian Splat to the Entity - -To display a Gaussian Splat model, load its .ply file and link it to the entity using setEntityGaussian. - -```swift -setEntityGaussian(entityId: entity, filename: "splat", withExtension: "ply") -``` - -Parameters: - -- entityId: The ID of the entity created earlier. -- filename: The name of the .ply file (without the extension). -- withExtension: The file extension, typically "ply". - -> Note: The Gaussian System renders point cloud data stored in the .ply format. Ensure your Gaussian Splat file is properly formatted and contains the necessary attributes (position, color, opacity, scale, rotation). - -### Directional Light - -Use for sunlight or distant key lights. Orientation (rotation) defines its direction. - -```swift -let sun = createEntity() -createDirLight(entityId: sun) -``` -### Point Light - -Omni light that radiates equally in all directions from a position. - -```swift -let bulb = createEntity() -createPointLight(entityId: bulb) -``` - -### Spot Light - -Cone-shaped light with a position and direction. - -```swift -let spot = createEntity() -createSpotLight(entityId: spot) -``` - -### Area Light - -Rect/area emitter used to mimic panels/windows; position and orientation matter. - -```swift -let panel = createEntity() -createAreaLight(entityId: panel) -``` - -### Enable or Disable PostFX - -Toggle post-processing effects globally through the PostFX facade: - -```swift -PostFX.setEnabled(.colorGrading, false) - -PostFX.enableVignette(false) -``` - -### SSAO Controls - -SSAO is managed through a dedicated API because it affects render quality and pipelines: - -```swift -SSAO.setEnabled(true) -SSAO.setQuality(.high) -SSAO.setRadius(0.8) -SSAO.setBias(0.02) -SSAO.setIntensity(1.2) -``` - ---- - -# Animation - -### Load an Animation -Load the animation data for your model by providing the animation .usdc file and a name to reference the animation later. - -```swift -setEntityAnimations(entityId: redPlayer, filename: "running", withExtension: "usdc", name: "running") -``` - -### Set the Animation to play - -Trigger the animation by referencing its name. This will set the animation to play on the entity. - -```swift -changeAnimation(entityId: redPlayer, name: "running") -``` - -### Pause the animation (Optional) - -To pause the current animation, simply call the following function. The animation component will be paused for the current entity. - -```swift -pauseAnimationComponent(entityId: redPlayer, isPaused: true) -``` - ---- - -# Physics - -### Enable Physics on the Entity - -Activate the physics simulation for your entity using the setEntityKinetics function. This function prepares the entity for movement and dynamic interaction. - -```swift -setEntityKinetics(entityId: redPlayer) -``` ---- - -### Configure Physics Properties -You can customize the entity’s physics behavior by defining its mass and gravity scale: - -- Mass: Determines the force needed to move the object. Heavier objects require more force. -- Gravity Scale: Controls how strongly gravity affects the entity (default is 0.0). - -```swift -setMass(entityId: redPlayer, mass: 0.5) -setGravityScale(entityId: redPlayer, gravityScale: 1.0) -``` - -### Apply Forces (Optional) -You can apply a custom force to the entity for dynamic movement. This is useful for simulating actions like jumps or pushes. - -```swift -applyForce(entityId: redPlayer, force: simd_float3(0.0, 0.0, 5.0)) -``` - -> Note: Forces are applied per frame. To avoid unintended behavior, only apply forces when necessary. - -### Use the Steering System -For advanced movement behaviors, leverage the Steering System to steer entities toward or away from targets. This system automatically calculates the required forces. - -Example: Steering Toward a Position - -```swift -steerTo(entityId: redPlayer, targetPosition: simd_float3(0.0, 0.0, 5.0), maxSpeed: 2.0, deltaTime: deltaTime) -``` - -### Additional Steering Functions - -The Steering System includes other useful behaviors, such as: - -- steerAway() -- steerPursuit() -- followPath() - -These functions simplify complex movement patterns, making them easy to implement. - ---- - -# Components - -### Register Components - -Components define the behavior or attributes of an entity. Use registerComponent to add a component to an entity. - -```swift -let entity = createEntity() - -registerComponent(entityId: entity, componentType: RenderComponent.self) -``` - -### Create Custom Component - -Here’s an example of a simple custom component for a soccer player’s dribbling behavior: - -```swift -public class DribblinComponent: Component { - public required init() {} - var maxSpeed: Float = 5.0 - var kickSpeed: Float = 15.0 - var direction: simd_float3 = .zero -} -``` - -> ⚠️ Note: Components should not include functions or game logic. Keep them as pure data containers. - -### Attaching a Component to an Entity - -Once you’ve defined a component, you attach it to an entity in your scene: - -```swift -let player = createEntity(name: "player") - -// Attach DribblinComponent to the entity -registerComponent(entityId: player, componentType: DribblinComponent.self) -``` - -# Systems - -### Create Custom System - -If you’ve created a **custom component**, you’ll usually also want to create a **custom system** to make it do something. -Components store the data, but systems are where the behavior lives. - -The engine automatically calls systems every frame. - -Here’s a simple system that works with the `DribblinComponent` we defined earlier: - -```swift -public func dribblingSystemUpdate(deltaTime: Float) { - // 1. Get the ID of the DribblinComponent - let customId = getComponentId(for: DribblinComponent.self) - - // 2. Query all entities that have this component - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - // 3. Loop through each entity and update its data - for entity in entities { - guard let dribblingComponent = scene.get(component: DribblinComponent.self, for: entity) else { - continue - } - - // Example logic: move player in the dribbling direction - dribblingComponent.direction = simd_normalize(dribblingComponent.direction) - let displacement = dribblingComponent.direction * dribblingComponent.maxSpeed * deltaTime - - if let transform = scene.get(component: LocalTransformComponent.self, for: entity) { - transform.position += displacement - } - } -} -``` - -### Registering the Custom System -All custom systems must be registered during initialization so the engine knows to run them every frame: - -```swift -registerCustomSystem(dribblingSystemUpdate) -``` diff --git a/website/versioned_docs/version-0.10.10/07-Contributor/Formatting.md b/website/versioned_docs/version-0.10.10/07-Contributor/Formatting.md deleted file mode 100644 index 6d45d6214..000000000 --- a/website/versioned_docs/version-0.10.10/07-Contributor/Formatting.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -id: formatting -title: Formatting -sidebar_position: 2 ---- - -# Formatting and Linting - -To maintain a consistent code style across the Untold Engine repo, we use [SwiftFormat](https://github.com/nicklockwood/SwiftFormat). SwiftFormat is a code formatter for Swift that helps enforce Swift style conventions and keep the codebase clean. If you don't have SwiftFormat installed, see the **Installing SwiftFormat** section below. - -## Quick Formatting & Linting - -Navigate to the root directory of Untold Engine and then run the commands below: - -### 🔍 Lint files - -To lint (check) all Swift files without making changes: - -```bash -swiftformat --lint . --swiftversion 5.8 --reporter github-actions-log -``` - -Or, using the Makefile: - -```bash -make lint -``` - -This command runs the same lint configuration as our GitHub Actions workflow and pre-commit hook, ensuring consistent results locally and in CI. - -### ✅ Formatting Files - -To format files: - -```bash -swiftformat --swiftversion 5.8 . -``` - -Alternatively, you can use the Makefile shortcut: - -```bash -make format -``` - -💡 Tip -If the pre-commit hook blocks your commit due to formatting issues, simply run: - -```bash -make format -``` - -then re-stage your changes and try committing again. - -You can bypass the hook temporarily (not recommended) with: - -```bash -git commit --no-verify -``` - -## Installing SwiftFormat - -The simplest way to install SwiftFormat is through the command line. - -1. Install SwiftFormat Using Homebrew: Open the terminal and run the following command: - -```bash -brew install swiftformat -``` -2. Verify Installation: After installation, verify that SwiftFormat is installed correctly by running: - -```bash -swiftformat --version -``` -This should print the installed version of SwiftFormat. - -### Using SwiftFormat - -Format a Single File - -To format a specific Swift file: - -1. Open the terminal and navigate to your project directory. - -2. Run the following command: - -```bash -swiftformat path/to/YourFile.swift -``` -This will format YourFile.swift according to the default rules. - -### Format Multiple Files - -To format all Swift files in your project: - -1. Navigate to your project directory in the terminal. - -2. Run the following command: - -```bash -swiftformat . -``` - -This will recursively format all Swift files in the current directory and its subdirectories. - - diff --git a/website/versioned_docs/version-0.10.10/07-Contributor/_category.json b/website/versioned_docs/version-0.10.10/07-Contributor/_category.json deleted file mode 100644 index 4308c167f..000000000 --- a/website/versioned_docs/version-0.10.10/07-Contributor/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "10-Contributor", "position": 99, "collapsed": false } - diff --git a/website/versioned_docs/version-0.10.10/07-Contributor/versioning.md b/website/versioned_docs/version-0.10.10/07-Contributor/versioning.md deleted file mode 100644 index ddb6c7a9d..000000000 --- a/website/versioned_docs/version-0.10.10/07-Contributor/versioning.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -id: versioning -title: Versioning -sidebar_position: 3 ---- - -# Versioning - -To help us identity the purpose of your commits, make sure to use the following tags in your commit messages. The tags will also automatically increment the the current version when pushed to github. - -- [Patch] - Bug fixes (eg. v1.0.0 -> v1.0.1) -- [Feature] - For new feature that don't break compatibility (eg. v1.0.0 -> v1.1.0) -- [API Change] - For major changes that are not API backward compatible (eg. v1.0.0 -> v2.0.0) - - - diff --git a/website/versioned_docs/version-0.10.10/08-CLI/CLI.md b/website/versioned_docs/version-0.10.10/08-CLI/CLI.md deleted file mode 100644 index 0ff5498a8..000000000 --- a/website/versioned_docs/version-0.10.10/08-CLI/CLI.md +++ /dev/null @@ -1,505 +0,0 @@ -# UntoldEngine CLI Tool - -> **Note:** The CLI tool source code is located in the `Tools/UntoldEngineCLI/` directory of the repository. -> -> For the most up-to-date documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - -The `untoldengine-create` command-line tool allows you to create and manage UntoldEngine game projects without launching the UntoldEditor. - -## Quick Start - -```bash -# Clone the repository -git clone https://github.com/untoldengine/UntoldEngine.git -cd UntoldEngine - -# Install the CLI globally -./scripts/install-create.sh - -# Create a project from anywhere -cd ~/anywhere -mkdir MyGame && cd MyGame -untoldengine-create create MyGame -``` - -## Complete Documentation - -For complete CLI documentation including: - -- Installation instructions -- Command reference -- Platform options -- Usage examples -- Troubleshooting - -Please refer to `Tools/UntoldEngineCLI/README.md` in the repository. - -## Quick Start - -Create a new game project in three simple steps: - -```bash -# 1. Create and enter your project directory -mkdir MyGame && cd MyGame - -# 2. Create the project (default: macOS) -untoldengine-create create MyGame - -# 3. Open in Xcode -open MyGame/MyGame.xcodeproj -``` - -All project files and assets will be created in the current directory. - -## Commands - -### create - -Creates a new UntoldEngine game project with the specified platform configuration. - -**Usage:** -```bash -untoldengine-create create [--platform ] -``` - -**Arguments:** -- ``: Name of your game project (required) - -**Options:** -- `--platform `: Target platform (default: `macos`) - - `macos` - macOS desktop application - - `ios` - iOS application - - `iosar` - iOS with ARKit support - - `visionos` - visionOS (Apple Vision Pro) - -**Examples:** - -Create a macOS game: -```bash -mkdir SpaceShooter && cd SpaceShooter -untoldengine-create create SpaceShooter -``` - -Create an iOS game: -```bash -mkdir MobileRPG && cd MobileRPG -untoldengine-create create MobileRPG --platform ios -``` - -Create an AR game for iOS: -```bash -mkdir ARAdventure && cd ARAdventure -untoldengine-create create ARAdventure --platform iosar -``` - -Create a visionOS game: -```bash -mkdir VisionGame && cd VisionGame -untoldengine-create create VisionGame --platform visionos -``` - -**What it does:** - -1. Creates the complete project structure in the current directory -2. Generates an Xcode project using XcodeGen -3. Sets up the game asset directories at `/Sources//GameData/` -4. Configures the UntoldEngine to load assets from the GameData directory -5. Creates platform-specific configuration files - -### update - -Updates the game data directory structure for an existing project. - -**Usage:** -```bash -untoldengine-create update -``` - -**Arguments:** -- ``: Name of the existing project (required) - -**What it does:** - -Re-creates the GameData folder structure at `/Sources//GameData/` with all standard asset directories: -- Models/ -- Scenes/ -- Scripts/ -- Animations/ -- Gaussians/ -- Shaders/ -- Textures/ - -**Example:** - -```bash -cd MyGame -untoldengine-create update MyGame -``` - -**When to use:** -- After accidentally deleting asset folders -- When you need to reset the folder structure -- To ensure all standard directories exist - -## Platform Options - -### macOS -**Command:** `--platform macos` (default) - -**Features:** -- Full desktop application -- Window management -- Keyboard and mouse input -- High performance rendering - -**Use for:** -- Desktop games -- Development and testing -- Maximum performance applications - -### iOS -**Command:** `--platform ios` - -**Features:** -- iPhone and iPad support -- Touch input -- Accelerometer support -- App Store distribution - -**Use for:** -- Mobile games -- Touch-based experiences -- Wide distribution via App Store - -### iOS AR (ARKit) -**Command:** `--platform iosar` - -**Features:** -- All iOS features -- ARKit integration -- World tracking -- Plane detection -- Face tracking - -**Use for:** -- Augmented reality games -- AR experiences -- Real-world interactive applications - -### visionOS -**Command:** `--platform visionos` - -**Features:** -- Apple Vision Pro support -- Spatial computing -- 3D UI elements -- Hand tracking -- Eye tracking - -**Use for:** -- Immersive 3D experiences -- Spatial applications -- Next-generation mixed reality - -## Project Structure - -After running `untoldengine-create create MyGame`, your project structure will look like: - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Xcode project (generated) - ├── project.yml # XcodeGen configuration - ├── README.md # Project documentation - └── Sources/ - └── MyGame/ - ├── GameData/ # Game assets location - │ ├── Models/ # 3D models (.usdz, etc.) - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # Game logic scripts - │ ├── Animations/ # Animation data - │ ├── Gaussians/ # Gaussian splatting data - │ ├── Shaders/ # Custom shaders - │ └── Textures/ # Image files - ├── AppDelegate.swift # Application entry point - ├── GameViewController.swift # Main game controller - └── Info.plist # App configuration -``` - -### Important Directories - -**GameData/**: The single location for all game assets -- Path: `MyGame/Sources/MyGame/GameData/` -- The UntoldEngine is configured to load all assets from this location -- Copy your game assets (models, textures, etc.) here -- Subdirectories are organized by asset type - -**Sources/**: Swift source code -- Contains your game's Swift files -- Platform-specific delegates and controllers -- Can add additional Swift files here - -### Adding Assets - -To add assets to your game, copy them into the appropriate GameData subdirectory: - -```bash -# Add a 3D model -cp ~/Downloads/spaceship.usdz MyGame/Sources/MyGame/GameData/Models/ - -# Add textures -cp ~/Downloads/texture_*.png MyGame/Sources/MyGame/GameData/Textures/ - -# Add a scene -cp ~/Downloads/level1.json MyGame/Sources/MyGame/GameData/Scenes/ -``` - -The engine will automatically find assets in these locations. - -## Workflow Examples - -### Typical Development Workflow - -```bash -# 1. Create project directory -mkdir MyAwesomeGame -cd MyAwesomeGame - -# 2. Create the game project -untoldengine-create create MyAwesomeGame --platform ios - -# 3. Add your game assets -cp -r ~/GameAssets/Models/* MyAwesomeGame/Sources/MyAwesomeGame/GameData/Models/ -cp -r ~/GameAssets/Textures/* MyAwesomeGame/Sources/MyAwesomeGame/GameData/Textures/ - -# 4. Open in Xcode and start developing -open MyAwesomeGame/MyAwesomeGame.xcodeproj -``` - -### Starting from Scratch - -```bash -# Create a new game from scratch -mkdir PuzzleGame && cd PuzzleGame -untoldengine-create create PuzzleGame --platform macos - -# The GameData directory is ready for your assets -ls -la PuzzleGame/Sources/PuzzleGame/GameData/ -# Models/ Scenes/ Scripts/ Animations/ Gaussians/ Shaders/ Textures/ -``` - -### Multi-Platform Development - -You might want to test the same game on different platforms: - -```bash -# Create macOS version for development -mkdir MyGame-macOS && cd MyGame-macOS -untoldengine-create create MyGame --platform macos - -# Create iOS version in a separate directory -cd .. -mkdir MyGame-iOS && cd MyGame-iOS -untoldengine-create create MyGame --platform ios - -# Copy assets between projects -cp -r ../MyGame-macOS/MyGame/Sources/MyGame/GameData/* \ - MyGame/Sources/MyGame/GameData/ -``` - -### Resetting Asset Structure - -If you accidentally delete asset folders or need to reset: - -```bash -cd MyGame -untoldengine-create update MyGame -# GameData structure is recreated -``` - -## Troubleshooting - -### Command Not Found - -**Problem:** `untoldengine-create: command not found` - -**Solution:** -1. Ensure you ran the installation script: - ```bash - cd /path/to/UntoldEngine - ./Scripts/install-create.sh - ``` - -2. Check if the tool exists: - ```bash - ls -l /usr/local/bin/untoldengine-create - ``` - -3. If it exists but still not found, add `/usr/local/bin` to your PATH: - ```bash - echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.zshrc - source ~/.zshrc - ``` - -### XcodeGen Failed - -**Problem:** Error message about XcodeGen failing - -**Solution:** -1. Ensure XcodeGen is installed: - ```bash - brew install xcodegen - ``` - -2. Verify XcodeGen works: - ```bash - xcodegen --version - ``` - -3. Check the project.yml file for syntax errors: - ```bash - cat path/to/project/project.yml - ``` - -### Build System Errors - -**Problem:** Error message about BuildSystem failure - -**Solution:** -1. Verify the UntoldEngine framework is properly built -2. Check that you're running the command from the correct directory -3. Ensure sufficient disk space for the project - -### Permission Denied - -**Problem:** Permission errors during installation - -**Solution:** -Use sudo for installation: -```bash -sudo ./Scripts/install-create.sh -``` - -Or install to a user directory (advanced): -```bash -# Edit install-create.sh to use a different install location -# Change INSTALL_PATH to ~/bin/untoldengine-create -``` - -### Xcode Project Won't Open - -**Problem:** Generated .xcodeproj file won't open - -**Solution:** -1. Regenerate the project: - ```bash - cd path/to/project - xcodegen generate - ``` - -2. Check Xcode version compatibility: - ```bash - xcodebuild -version - ``` - (Requires Xcode 15.0+) - -3. Verify project.yml is valid: - ```bash - cat project.yml - ``` - -### Assets Not Loading - -**Problem:** Game can't find assets at runtime - -**Solution:** -1. Verify assets are in the correct location: - ```bash - ls -la MyGame/Sources/MyGame/GameData/ - ``` - -2. Ensure assets are in the right subdirectories: - - Models go in `GameData/Models/` - - Textures go in `GameData/Textures/` - - etc. - -3. Check asset file names and extensions are correct - -4. Verify the GameData directory structure by running update: - ```bash - untoldengine-create update MyGame - ``` - -## Advanced Usage - -### Scripting and Automation - -The CLI tool is designed to work well in scripts: - -```bash -#!/bin/bash -# create-all-platforms.sh - -PROJECT_NAME="MyGame" - -for PLATFORM in macos ios iosar visionos; do - DIR="${PROJECT_NAME}-${PLATFORM}" - mkdir -p "$DIR" && cd "$DIR" - untoldengine-create create "$PROJECT_NAME" --platform "$PLATFORM" - cd .. -done -``` - -### CI/CD Integration - -Example GitHub Actions workflow: - -```yaml -name: Create Game Projects -on: [push] - -jobs: - create-projects: - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - - name: Install CLI tool - run: | - cd UntoldEngine - ./Scripts/install-create.sh - - - name: Create projects - run: | - mkdir -p builds/MyGame-iOS && cd builds/MyGame-iOS - untoldengine-create create MyGame --platform ios -``` - -### Environment Variables - -While the CLI doesn't currently use environment variables, you can use them in your workflow: - -```bash -export GAME_NAME="MyGame" -export PLATFORM="ios" - -mkdir "$GAME_NAME" && cd "$GAME_NAME" -untoldengine-create create "$GAME_NAME" --platform "$PLATFORM" -``` - -## Getting Help - -For additional help: - -```bash -# General help -untoldengine-create --help - -# Command-specific help -untoldengine-create create --help -untoldengine-create update --help -``` - -## Feedback and Issues - -Found a bug or have a feature request? Please report it on the [UntoldEngine GitHub repository](https://github.com/untoldengine/UntoldEngine/issues). diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetBrowserScripts.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetBrowserScripts.png deleted file mode 100644 index 0b523ad75..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetBrowserScripts.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetBrowserView-alt.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetBrowserView-alt.png deleted file mode 100644 index b925157b9..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetBrowserView-alt.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetBrowserView.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetBrowserView.png deleted file mode 100644 index c5cbe12fe..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetBrowserView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetLibraryLoupe.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetLibraryLoupe.png deleted file mode 100644 index 9a4a0c8e3..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorAssetLibraryLoupe.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorBottomShot.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorBottomShot.png deleted file mode 100644 index ed3f3c731..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorBottomShot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorCodeScriptView-alt.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorCodeScriptView-alt.png deleted file mode 100644 index 84d7461bc..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorCodeScriptView-alt.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorCodeScriptView.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorCodeScriptView.png deleted file mode 100644 index 654da1eb3..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorCodeScriptView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorEnvironment.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorEnvironment.png deleted file mode 100644 index c6f55a224..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorEnvironment.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorInspectorView.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorInspectorView.png deleted file mode 100644 index 04f65dce7..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorInspectorView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorMainShot-alt.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorMainShot-alt.png deleted file mode 100644 index 2ba7138d1..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorMainShot-alt.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorMainShot.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorMainShot.png deleted file mode 100644 index 981a26e45..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorMainShot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/EditorScenegraphView.png b/website/versioned_docs/version-0.10.10/images/Editor/EditorScenegraphView.png deleted file mode 100644 index 87b1cc9c0..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/EditorScenegraphView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/Editor_scene_empty.png b/website/versioned_docs/version-0.10.10/images/Editor/Editor_scene_empty.png deleted file mode 100644 index 8dbc28800..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/Editor_scene_empty.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/Editor_scene_model_viewport.png b/website/versioned_docs/version-0.10.10/images/Editor/Editor_scene_model_viewport.png deleted file mode 100644 index 321d25529..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/Editor_scene_model_viewport.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/Editor/Editor_scene_new.png b/website/versioned_docs/version-0.10.10/images/Editor/Editor_scene_new.png deleted file mode 100644 index 99bc768bd..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/Editor/Editor_scene_new.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/UntoldEngineGrid.png b/website/versioned_docs/version-0.10.10/images/UntoldEngineGrid.png deleted file mode 100644 index d6790ef10..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/UntoldEngineGrid.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/add-button-scenegraph.png b/website/versioned_docs/version-0.10.10/images/add-button-scenegraph.png deleted file mode 100644 index 17c6f06a0..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/add-button-scenegraph.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/animation-running.png b/website/versioned_docs/version-0.10.10/images/animation-running.png deleted file mode 100644 index 73c84ad72..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/animation-running.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/animationexportblender.png b/website/versioned_docs/version-0.10.10/images/animationexportblender.png deleted file mode 100644 index da6553855..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/animationexportblender.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/animationexportblenderStandalone.png b/website/versioned_docs/version-0.10.10/images/animationexportblenderStandalone.png deleted file mode 100644 index 77666c05e..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/animationexportblenderStandalone.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/asset-browser-folder.png b/website/versioned_docs/version-0.10.10/images/asset-browser-folder.png deleted file mode 100644 index 6f0406ced..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/asset-browser-folder.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/asset-browser-usdc-file.png b/website/versioned_docs/version-0.10.10/images/asset-browser-usdc-file.png deleted file mode 100644 index e6d2f0cb8..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/asset-browser-usdc-file.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/demogame-noeditor.png b/website/versioned_docs/version-0.10.10/images/demogame-noeditor.png deleted file mode 100644 index d0a39e9e0..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/demogame-noeditor.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/editor-animation.png b/website/versioned_docs/version-0.10.10/images/editor-animation.png deleted file mode 100644 index d54a8c8e9..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/editor-animation.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/editorscreenshot.png b/website/versioned_docs/version-0.10.10/images/editorscreenshot.png deleted file mode 100644 index 7f78a96d1..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/editorscreenshot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/engine-assetbrowser.png b/website/versioned_docs/version-0.10.10/images/engine-assetbrowser.png deleted file mode 100644 index c58c536c2..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/engine-assetbrowser.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/engine-gizmo.png b/website/versioned_docs/version-0.10.10/images/engine-gizmo.png deleted file mode 100644 index 0faa36698..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/engine-gizmo.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/engine-inspector.png b/website/versioned_docs/version-0.10.10/images/engine-inspector.png deleted file mode 100644 index bd778f251..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/engine-inspector.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/engine-lights.png b/website/versioned_docs/version-0.10.10/images/engine-lights.png deleted file mode 100644 index a18833041..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/engine-lights.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/engine-post-processing.png b/website/versioned_docs/version-0.10.10/images/engine-post-processing.png deleted file mode 100644 index 3e3f1f32e..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/engine-post-processing.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/engine-scenegraph.png b/website/versioned_docs/version-0.10.10/images/engine-scenegraph.png deleted file mode 100644 index 51e6e51a0..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/engine-scenegraph.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/enginethumbnail.jpg b/website/versioned_docs/version-0.10.10/images/enginethumbnail.jpg deleted file mode 100644 index 882b7a07f..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/enginethumbnail.jpg and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/gamedemoscreenshot.png b/website/versioned_docs/version-0.10.10/images/gamedemoscreenshot.png deleted file mode 100644 index 7f78a96d1..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/gamedemoscreenshot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/howtoexport.png b/website/versioned_docs/version-0.10.10/images/howtoexport.png deleted file mode 100644 index ccd4079c6..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/howtoexport.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/howtoexportStandalone.png b/website/versioned_docs/version-0.10.10/images/howtoexportStandalone.png deleted file mode 100644 index 0858b82a5..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/howtoexportStandalone.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/importassetbutton.png b/website/versioned_docs/version-0.10.10/images/importassetbutton.png deleted file mode 100644 index a2ea71d9d..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/importassetbutton.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/importheader.gif b/website/versioned_docs/version-0.10.10/images/importheader.gif deleted file mode 100644 index 9a1b60a2a..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/importheader.gif and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/inspector.png b/website/versioned_docs/version-0.10.10/images/inspector.png deleted file mode 100644 index 0504eb302..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/inspector.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/launchgame.gif b/website/versioned_docs/version-0.10.10/images/launchgame.gif deleted file mode 100644 index f13e49088..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/launchgame.gif and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/linkerissue.png b/website/versioned_docs/version-0.10.10/images/linkerissue.png deleted file mode 100644 index 192c9e5ea..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/linkerissue.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/modelexportblender.png b/website/versioned_docs/version-0.10.10/images/modelexportblender.png deleted file mode 100644 index bbfaa6c49..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/modelexportblender.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/modelineditor.png b/website/versioned_docs/version-0.10.10/images/modelineditor.png deleted file mode 100644 index 951e24a5b..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/modelineditor.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/modelsriggedexportblender.png b/website/versioned_docs/version-0.10.10/images/modelsriggedexportblender.png deleted file mode 100644 index 1eb6740e8..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/modelsriggedexportblender.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/render-component.png b/website/versioned_docs/version-0.10.10/images/render-component.png deleted file mode 100644 index d15b36bf2..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/render-component.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/script_properties.png b/website/versioned_docs/version-0.10.10/images/script_properties.png deleted file mode 100644 index 1a926da20..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/script_properties.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/scripts_in_asset_browser.png b/website/versioned_docs/version-0.10.10/images/scripts_in_asset_browser.png deleted file mode 100644 index 002e8072b..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/scripts_in_asset_browser.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/setpathbutton.png b/website/versioned_docs/version-0.10.10/images/setpathbutton.png deleted file mode 100644 index a2ea71d9d..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/setpathbutton.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.10/images/top_contributors/MioLogo.png b/website/versioned_docs/version-0.10.10/images/top_contributors/MioLogo.png deleted file mode 100644 index dc0da3039..000000000 Binary files a/website/versioned_docs/version-0.10.10/images/top_contributors/MioLogo.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/01-Intro.md b/website/versioned_docs/version-0.10.6/01-Intro.md deleted file mode 100644 index 81b5d6707..000000000 --- a/website/versioned_docs/version-0.10.6/01-Intro.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -slug: /intro ---- - -# Untold Engine Documentation - -Welcome to the **Untold Engine documentation**. - -These docs are the primary reference for working with the Untold Engine ecosystem — whether you are building a game, extending the engine, or contributing to the editor. - ---- - -## What Is Untold Engine? - -![untoldengine](images/Editor/EditorMainShot.png) - -The Untold Engine strives to be a stable, performant, and developer-friendly 3D engine that empowers creativity, removes friction, and makes game development feel effortless for Apple developers - -The Untold Engine is an open-source 3D game engine under active development, designed for macOS, iOS, xrOS platforms. Written in Swift and powered by Metal, its goal is to simplify game creation with a clean, intuitive API. - -While the engine already supports many core systems like rendering, physics, and animation, there’s still much to build and improve. - ---- - -## The Untold Engine Ecosystem - -Untold Engine is delivered through three closely related products: - -### Untold Engine Studio -A downloadable application that includes: -- The Untold Engine runtime -- The Untold Editor -- Built-in tools for scripting, assets, and scene editing - -This is the recommended starting point for most users. - ---- - -### Untold Engine -The core engine runtime. - -This is intended for: -- Engine developers -- Contributors -- Advanced users who want to modify or extend engine systems - -Installation is performed via the command line. - ---- - -### Untold Editor -The editor application built on top of the engine runtime. - -This is intended for: -- Contributors working on editor features -- Developers extending tools and workflows - -The editor uses the same runtime as games, ensuring consistent behavior. - ---- - -## Choose Your Path - -These docs are organized around **how you intend to use the engine**. - -### Game Development -For developers building games using Untold Engine. - -You will learn: -- How to create scenes visually -- How to write game logic (Swift or USC scripts) -- How to work with assets and entities -- How to build and run your game - -Untold Engine supports two approaches for writing gameplay code: -- **Swift in Xcode** (recommended) - Full engine API access -- **USC Scripts** (experimental) - Component-based scripting - -Start here if your goal is to build a game. - ---- - -### Engine Development -For developers who want to understand or extend the engine itself. - -You will learn: -- The engine architecture -- ECS and system execution -- Rendering and simulation internals -- How to contribute new engine features - -Start here if you want to work on the engine runtime. - ---- - -### Editor Development -For contributors working on the Untold Editor. - -You will learn: -- Editor architecture -- Views, tools, and interaction models -- How the editor coordinates with the engine -- How to extend or add editor functionality - -Start here if you want to improve the editor. - ---- - -## Getting Started - -If you are unsure where to begin: - -- New users: **Game Development → Overview** -- Scripting users: **USC → Introduction** -- Contributors: **Engine Development → Architecture** - -Each section is designed to stand on its own. - diff --git a/website/versioned_docs/version-0.10.6/02-Getting Started/02-Installation.md b/website/versioned_docs/version-0.10.6/02-Getting Started/02-Installation.md deleted file mode 100644 index 89e99a7ee..000000000 --- a/website/versioned_docs/version-0.10.6/02-Getting Started/02-Installation.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -id: intro -title: Installation -sidebar_position: 1 ---- - -# Installation - -This page explains how to install **Untold Engine Studio**, the recommended way to get started with Untold Engine. - -Untold Engine Studio is a downloadable app that includes: -- The **Untold Engine** runtime -- The **Untold Editor** for building and editing games - -![editorbottomshot](../images/Editor/EditorBottomShot.png) - -If your goal is to **make games**, this is the only installation you need. - ---- - -## Recommended Installation (Untold Engine Studio) - -### 1. Download - -Download the latest version of **Untold Engine Studio** from the official website: - -[Download Releases](https://github.com/untoldengine/UntoldEditor/releases) - -The download is provided as a `.dmg` file for macOS. - ---- - -### 2. Install - -1. Open the downloaded `.dmg` file -2. Drag **Untold Engine Studio** into your `Applications` folder -3. Launch the app from `Applications` - -No additional setup is required. - ---- - -### 3. First Launch - -On first launch, Untold Engine Studio will: -- Initialize the engine runtime -- Set up the editor environment -- Prompt you to create or open a project - -From here, you can immediately: -- Create scenes visually -- Import 3D models and assets -- Write game logic (Swift in Xcode or USC scripts) -- Build and test your game - ---- - -## System Requirements - -- macOS (Apple Silicon recommended) -- Metal-capable GPU -- Keyboard and mouse - ---- - -## What You Get - -By installing Untold Engine Studio, you get: - -- A complete **game development environment** -- Visual editor for scenes, assets, and scripts -- Full **Untold Engine Swift API** for game logic in Xcode -- **USC scripting system** (experimental component-based scripting) -- Build and run support for macOS, iOS, and visionOS - -You do **not** need to install the engine or editor separately. - -### Two Ways to Write Game Logic - -Untold Engine Studio supports two approaches for writing gameplay code: - -**1. Swift in Xcode (Recommended)** -- Write game logic in `GameScene.swift` using the full Untold Engine API -- Complete control over game systems and performance -- Best for complex games and experienced developers -- Works seamlessly with Xcode debugging and profiling - -**2. USC Scripts (Experimental)** -- Component-based scripting attached to entities -- Write gameplay behaviors in the integrated script editor -- Good for prototyping and simple game mechanics -- API is experimental and subject to change - -You can **use both approaches** in the same project. - ---- - -## Alternative Installation: CLI Workflow - -For **advanced users** or those who prefer a **command-line workflow** without the visual editor, you can install the CLI tools. - -### When to Use CLI - -- You prefer working entirely in Xcode without a visual editor -- You want to script project creation and automation -- You're building tools or integrations on top of UntoldEngine - -### CLI Installation - -**1. Clone the repository:** - -```bash -git clone https://github.com/untoldengine/UntoldEngine.git -cd UntoldEngine -``` - -**2. Install the CLI globally:** - -```bash -./scripts/install-create.sh -``` - -**3. Verify installation:** - -```bash -untoldengine-create --version -untoldengine-create --help -``` - -### CLI Quick Start - -After installing the CLI, create a project from anywhere: - -```bash -# 1. Create project directory -cd ~/anywhere -mkdir MyGame && cd MyGame - -# 2. Create the project -untoldengine-create create MyGame - -# 3. Open in Xcode -open MyGame/MyGame.xcodeproj -``` - -For complete CLI documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - ---- - -### What You Get - -The CLI creates a complete, ready-to-run project: - -- **Xcode project** - Configured and ready to build -- **GameScene.swift** - Your game logic goes here -- **GameViewController.swift** - Renderer and view setup -- **GameData/** directory - All game assets location -- **Platform-specific** files (AppDelegate, Info.plist, etc.) - -### Project Structure - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Open this in Xcode - ├── project.yml # XcodeGen configuration - └── Sources/ - └── MyGame/ - ├── GameData/ # ← Put your assets here - │ ├── Models/ # 3D models - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # USC scripts - │ ├── Textures/ # Images - │ └── ... - ├── GameScene.swift # Your game logic - ├── GameViewController.swift # View controller - └── AppDelegate.swift # App entry point -``` - -### Platform Support - -The CLI supports multiple platforms: - -```bash -# macOS (default) -untoldengine-create create MyGame --platform macos - -# iOS -untoldengine-create create MyGame --platform ios - -# iOS with ARKit -untoldengine-create create MyGame --platform iosar - -# visionOS (Apple Vision Pro) -untoldengine-create create MyGame --platform visionos -``` - -### Development Workflow - -1. **Write code** in GameScene.swift (game logic) -2. **Add assets** to the GameData/ directory -3. **Build & run** in Xcode (Cmd+R) -4. **Iterate** - make changes and rebuild - -For complete CLI documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - ---- - -## Preloaded Assets - -To kickstart development, download prebuilt demo assets: - -- **Models**: Soccer stadium, player, ball, and more -- **Animations**: Running, idle, and other character motions -- **Textures**: Sample materials - -[Download Demo Assets v1.0](https://haroldserrano.gumroad.com/l/iqjlac) - -Extract and copy into your project's `GameData/` directory. - ---- diff --git a/website/versioned_docs/version-0.10.6/02-Getting Started/03-ChoosingYourPath.md b/website/versioned_docs/version-0.10.6/02-Getting Started/03-ChoosingYourPath.md deleted file mode 100644 index 4d003ace7..000000000 --- a/website/versioned_docs/version-0.10.6/02-Getting Started/03-ChoosingYourPath.md +++ /dev/null @@ -1,79 +0,0 @@ -# Choosing Your Path - -Untold Engine supports different types of developers. - -This page helps you choose the path that best matches what you want to do. - ---- - -## I Want to Make a Game - -Choose this path if your goal is to: -- Build gameplay -- Create scenes -- Write scripts -- Ship a game - -### What You’ll Use - -- **Untold Engine Studio** -- **USC scripting API** - -### Where to Start - -> **Game Development → Overview** - -You do not need to understand engine internals to make a game. - ---- - -## I Want to Improve the Engine - -Choose this path if you want to: -- Work on core engine systems -- Improve rendering, physics, or ECS -- Extend platform support - -### What You’ll Use - -- **Untold Engine (core)** -- Command-line tools -- Source builds - -### Where to Start - -> **Engine Development → Overview** - -This path assumes familiarity with engine concepts and systems programming. - ---- - -## I Want to Improve the Editor - -Choose this path if you want to: -- Improve the editor UI -- Add new tools or views -- Improve workflows and usability - -### What You’ll Use - -- **Untold Editor** -- Editor-specific APIs -- Engine integration points - -### Where to Start - -> **Editor Development → Overview** - -Editor development focuses on tooling rather than gameplay. - ---- - -## Not Sure Yet? - -If you’re not sure where to begin, start here: - -> **Game Development → Overview** - -You can always explore the other paths later. - diff --git a/website/versioned_docs/version-0.10.6/02-Getting Started/_category.json b/website/versioned_docs/version-0.10.6/02-Getting Started/_category.json deleted file mode 100644 index ac4e7d573..000000000 --- a/website/versioned_docs/version-0.10.6/02-Getting Started/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "01-Getting Starter", "position": 1, "collapsed": false } - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/01-Overview.md b/website/versioned_docs/version-0.10.6/03-Game Development/01-Overview.md deleted file mode 100644 index a41e30ce9..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/01-Overview.md +++ /dev/null @@ -1,116 +0,0 @@ -# Overview - -This section covers **game development** with the Untold Engine. - -You'll learn how to create games using **Untold Engine Studio**, with two approaches for writing game logic: - -1. **Swift in Xcode** - Full engine API access (recommended) -2. **USC Scripts** - Component-based scripting (experimental) - -![editorsideshotalt](../images/Editor/EditorSideShotWide-alt.png) - ---- - -## Two Approaches to Game Logic - -Untold Engine gives you flexibility in how you write gameplay code: - -### Option 1: Swift in Xcode (Recommended) - -Write game logic in `GameScene.swift` using the full Untold Engine Swift API: -- Complete control over game systems and performance -- Access to all engine features and APIs -- Seamless integration with Xcode debugging and profiling -- Best for complex games and experienced developers - -### Option 2: USC Scripts (Experimental) - -Write component-based scripts that attach to entities: -- Simpler, component-oriented approach -- Good for prototyping and simple game mechanics -- Edit scripts in the integrated editor or Xcode -- API is experimental and subject to change - -**You can use both approaches in the same project.** - ---- - -## The Development Workflow - -A typical development workflow with Untold Engine Studio: - -1. **Create a project** using the "New" button in Untold Engine Studio -2. **Compose scenes visually** using the editor -3. **Write game logic** (Swift in `GameScene.swift` or USC scripts) -4. **Add assets** to the GameData/ directory -5. **Build & run** in Xcode (Cmd+R) -6. **Iterate** quickly with visual feedback - ---- - -## Entry Point 1: GameScene.swift (Swift) - -When you create a project, you get a clean `GameScene.swift` file for writing game logic in Swift: - -```swift -class GameScene { - - init() { - // Configure asset paths - setupAssetPaths() - - // Load game content - loadBundledScripts() - loadAndPlayFirstScene() - - // Start game systems - startGameSystems() - } - - func update(deltaTime: Float) { - // Your game logic goes here - } - - func handleInput() { - // Handle user input here - } -} -``` - -**This is where your game comes to life.** Write Swift code, access the full engine API, and build your game. - ---- - -## Entry Point 2: USC Scripts (Experimental) - -Alternatively, write game logic as USC scripts that attach to entities: - -Create USC scripts from the **Script** menu in Untold Engine Studio, then attach them to entities in the Inspector. - ---- - - -## Project Structure - -Your generated project has everything you need: - -``` -MyGame/ -└── MyGame/ - ├── MyGame.xcodeproj # Open in Xcode - └── Sources/ - └── MyGame/ - ├── GameData/ # Assets location - │ ├── Models/ - │ ├── Scenes/ - │ ├── Scripts/ - │ └── Textures/ - ├── GameScene.swift # Your game logic ⭐ - ├── GameViewController.swift # Renderer setup - └── AppDelegate.swift # App entry -``` - -**Focus on GameScene.swift** - that's where your game lives. - ---- - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/000_HelloEditor.md b/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/000_HelloEditor.md deleted file mode 100644 index 02c658707..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/000_HelloEditor.md +++ /dev/null @@ -1,170 +0,0 @@ -# Getting Started with Untold Engine Studio - -This guide will walk you through the basics of getting up and running with **Untold Engine Studio**, from installation to loading your first model. - ---- - -## Downloading Untold Engine Studio - -To start developing with the Untold Engine, download the latest version of **Untold Engine Studio** from the official GitHub releases page: - -👉 https://github.com/untoldengine/UntoldEditor/releases - -Once downloaded: - -1. Drag **Untold Engine Studio** into your **Applications** folder -2. Double-click the app to launch it - ---- - -## Creating a New Project - -When the editor launches, you will be presented with the main startup screen. - -![editor_empty_view](../../images/Editor/Editor_scene_empty.png) - -At this point, create a new project: - -1. Click **New** -2. A new window will appear asking for: - - **Project name** - - **Target platform** - - **Project location** -3. Fill in the details and click **Create** - -Once completed, the engine will generate a fully configured **Xcode game project** for you. - -You will then be prompted to open the project in Xcode. I recommend doing so. - -![editor_scene_xcode](../../images/Editor/Editor_scene_xcode.png) - ---- - -## Exploring the Generated Project - -After opening the project in Xcode, take a moment to explore the file structure. This will help you navigate the project later. - -### Project Structure - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Open this in Xcode - ├── project.yml # XcodeGen configuration - └── Sources/ - └── MyGame/ - ├── GameData/ # ← Put your assets here - │ ├── Models/ # 3D models - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # USC scripts - │ ├── Textures/ # Images - │ └── ... - ├── GameScene.swift # Your game logic - ├── GameViewController.swift # View controller - └── AppDelegate.swift # App entry point -``` - -The most important file to look for is: - -```GameScene.swift``` - -This is where you will do most of your coding. -It contains the core lifecycle functions, including: - -- `init()` – scene setup -- `update()` – per-frame logic - -You’ll be spending most of your time here when writing game logic. - ---- - -## Back to the Editor - -Return to **Untold Engine Studio**. - -If you look at the editor toolbar, you’ll notice the name of your newly created project displayed on the right side of the window. - -At this point, you can already start adding content to your scene. - ---- - -## Adding Entities to the Scene - -You can quickly add a primitive by clicking the **“+”** button in the **Scenegraph View** and selecting a cube or other built-in shapes. - -However, it’s more fun to load real models. - ---- - -## Downloading Sample Models - -I’ve provided a small set of [sample models](https://haroldserrano.gumroad.com/l/iqjlac) you can use to get started: - -- A soccer player -- A soccer ball -- A stadium -- Idle and running animations - -All assets are provided as **`.usdz`** files. - -> **Note** -> Untold Engine currently accepts **USDZ** files only. - -Download the sample assets from the provided link. - ---- - -## Importing Models into Your Project - -Once the models are downloaded, it’s time to import them into your project. - -1. Open the **Asset Browser** -2. Select the **Model** category -3. Click **Import** -4. Navigate to the folder containing your downloaded `.usdz` file -5. Select a model and confirm - -After importing, the engine will display a feedback message confirming the import. -You will also see the `.usdz` file listed under the **Model** category. - ---- - -## Loading a Model into the Scene - -Now for the fun part. - -- **Double-click** the imported `.usdz` file in the Asset Browser - -The model will immediately appear in the editor viewport. - -![Editor_scene_model_viewport](../../images/Editor/Editor_scene_model_viewport.png) - -Behind the scenes, two things just happened: - -1. An **entity** was created -2. The `.usdz` asset was linked to that entity - ---- - -## Importing and Linking Animations - -The same workflow applies to animations. - -1. Select the **Animation** category in the Asset Browser -2. Click **Import** -3. Locate and import an animation `.usdz` file -4. Double-click the animation asset - -The animation will automatically be linked to the currently selected entity. - -If you open the **Inspector** tab, you’ll see the animation listed as part of the entity’s components. - ---- - -## What’s Next? - -Now that you have a basic understanding of the editor, asset workflow, and project structure, you’re ready to start coding with the Untold Engine. - -👉 Continue with the **Hello World** tutorial to write your first gameplay logic. - - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/00_HelloWorld.md b/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/00_HelloWorld.md deleted file mode 100644 index 6c5007b74..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/00_HelloWorld.md +++ /dev/null @@ -1,152 +0,0 @@ -# Hello World - -Your first UntoldEngine program - logging a message every frame. - ---- - -## Overview - -This tutorial shows you how to add custom code to your game's update loop and log output to the console. - ---- - -## Prerequisites - -This tutorial assumes you have: -- Created a project using the 'Untold Engine Studio` or `untoldengine-create` -- Opened the project in Xcode -- Located `GameScene.swift` in your project - ---- - -## Step 1: Open GameScene.swift - -In Xcode, navigate to: - -``` -Sources/YourProjectName/GameScene.swift -``` - ---- - -## Step 2: Add Your First Game Logic - -Find the `update(deltaTime:)` method in `GameScene.swift`: - -```swift path=null start=null -func update(deltaTime: Float) { - // Skip logic if not in game mode - if gameMode == false { return } - - // Add your custom update logic here -} -``` - -Replace the comment with: - -```swift path=null start=null -func update(deltaTime: Float) { - // Skip logic if not in game mode - if gameMode == false { return } - - // Your first game code! 🎉 - Logger.log(message: "Hello World! Delta: \(deltaTime)") -} -``` - ---- - -## Step 3: Build and Run - -Press **Cmd+R** in Xcode. - -Open the **Debug Console** (Cmd+Shift+Y) to see: - -``` -Hello World! Delta: 0.016 -Hello World! Delta: 0.017 -Hello World! Delta: 0.016 -... -``` - -The message appears every frame! 🚀 - ---- - -## What Just Happened? - -### The Update Loop - -`update(deltaTime:)` is called every frame by the engine: - -- **60 FPS** = called 60 times per second -- **deltaTime** = time since last frame (in seconds) - -All game logic goes here: movement, input handling, collision detection, etc. - -### Logging - -```swift path=null start=null -Logger.log(message: "Hello World!") -``` - -Use `Logger.log()` to print debug messages. It's better than `print()` because: -- Engine-aware logging -- Can be filtered/disabled in production -- Consistent formatting - -### Other Logging Methods - -```swift path=null start=null -Logger.logWarning(message: "Something might be wrong") -Logger.logError(message: "Something went wrong!") -``` - ---- - -## Limiting Output (Recommended) - -Logging every frame creates spam. Let's log once per second instead: - -```swift path=null start=null -class GameScene { - var elapsedTime: Float = 0.0 // Add this property - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Accumulate time - elapsedTime += deltaTime - - // Log once per second - if elapsedTime >= 1.0 { - Logger.log(message: "Hello World! One second passed.") - elapsedTime = 0.0 // Reset - } - } -} -``` - -Now you'll see: - -``` -Hello World! One second passed. -Hello World! One second passed. -... -``` - -Much cleaner! - ---- - -## Summary - -You've learned: - -✅ The `update(deltaTime:)` method runs every frame -✅ `deltaTime` is the time between frames -✅ `Logger.log()` prints messages to the console -✅ How to accumulate time for periodic actions - -This is the foundation of game development: **write code that runs every frame**. - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md b/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md deleted file mode 100644 index 94cf91c38..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md +++ /dev/null @@ -1,214 +0,0 @@ -# Move an Entity - -Learn how to move entities using the Transform System. - ---- - -## Overview - -This tutorial shows you how to: -- Find an entity from a loaded scene -- Move an entity to an absolute position -- Move an entity relative to its current position - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity (created in Untold Engine Studio or loaded via `loadScene()`) -- The entity has a name set in the editor (e.g., "Player") - -For complete API documentation: - -➡️ **[Transform System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingTransformSystem.md)** - ---- - -## Step 1: Find the Entity from Your Scene - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code (setupAssetPaths, loadScene, etc.) ... - startGameSystems() - - // Find the entity by name (set in the editor) - player = findEntity(name: "Player") - - if player == nil { - Logger.logWarning(message: "Player entity not found in scene") - } - } -} -``` - -**Important**: "Player" must match the entity name you set in Untold Engine Studio. - ---- - -## Step 2: Move to an Absolute Position - -Use `translateTo()` to set an entity to a specific world position: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - - player = findEntity(name: "Player") - - // Move player to position (5, 0, -10) - translateTo(entityId: player, position: SIMD3(5.0, 0.0, -10.0)) - } -} -``` - -**Result**: The entity immediately moves to position (5, 0, -10) in world space. - ---- - -## Step 3: Move Relative to Current Position - -Use `translateBy()` to move an entity by an offset: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - - player = findEntity(name: "Player") - - // Move player 3 units to the right (X-axis) - translateBy(entityId: player, delta: SIMD3(3.0, 0.0, 0.0)) - } -} -``` - -**Result**: The entity moves 3 units along the X-axis from its current position. - ---- - -## Step 4: Continuous Movement in Update Loop - -For smooth movement every frame, use `translateBy()` in `update(deltaTime:)`: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let moveSpeed: Float = 5.0 // Units per second - - init() { - // ... setup code ... - player = findEntity(name: "Player") - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Move forward continuously - let movement = SIMD3(0, 0, -moveSpeed * deltaTime) - translateBy(entityId: player, delta: movement) - } -} -``` - -**Result**: The player moves forward smoothly at 5 units per second. - ---- - -## Understanding Delta Time - -**Why multiply by `deltaTime`?** - -`deltaTime` is the time (in seconds) since the last frame: -- At 60 FPS: `deltaTime ≈ 0.016` seconds -- At 30 FPS: `deltaTime ≈ 0.033` seconds - -By multiplying speed by `deltaTime`, movement becomes **frame-rate independent**: - -```swift path=null start=null -// Without deltaTime (BAD) -translateBy(entityId: player, delta: SIMD3(0, 0, -0.1)) -// Result: Speed varies with frame rate ❌ - -// With deltaTime (GOOD) -let movement = SIMD3(0, 0, -moveSpeed * deltaTime) -translateBy(entityId: player, delta: movement) -// Result: Consistent speed regardless of frame rate ✅ -``` - ---- - -## Movement Examples - -### Move Forward - -```swift path=null start=null -let movement = SIMD3(0, 0, -moveSpeed * deltaTime) -translateBy(entityId: player, delta: movement) -``` - -### Move Right - -```swift path=null start=null -let movement = SIMD3(moveSpeed * deltaTime, 0, 0) -translateBy(entityId: player, delta: movement) -``` - -### Move Up - -```swift path=null start=null -let movement = SIMD3(0, moveSpeed * deltaTime, 0) -translateBy(entityId: player, delta: movement) -``` - -### Move Along Entity's Forward Direction - -```swift path=null start=null -let forward = getForwardAxisVector(entityId: player) -let movement = forward * moveSpeed * deltaTime -translateBy(entityId: player, delta: movement) -``` - ---- - -## Checking Entity Position - -To read an entity's current position: - -```swift path=null start=null -// World position (absolute) -let worldPos = getPosition(entityId: player) -Logger.log(message: "Player world position: \(worldPos)") - -// Local position (relative to parent, if parented) -let localPos = getLocalPosition(entityId: player) -Logger.log(message: "Player local position: \(localPos)") -``` - ---- - -## Summary - -You've learned: - -✅ `findEntity(name:)` - Find entities from loaded scenes -✅ `translateTo()` - Set absolute world position -✅ `translateBy()` - Move relative to current position -✅ `deltaTime` - Make movement frame-rate independent -✅ `getPosition()` - Read current position - ---- - - - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/01_Transform/02_RotateAnEntity.md b/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/01_Transform/02_RotateAnEntity.md deleted file mode 100644 index 1fc40aaaf..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/01_Transform/02_RotateAnEntity.md +++ /dev/null @@ -1,246 +0,0 @@ -# Rotate an Entity - -Learn how to rotate entities using the Transform System. - ---- - -## Overview - -This tutorial shows you how to: -- Rotate an entity to an absolute angle -- Rotate an entity incrementally -- Create smooth rotation using `deltaTime` - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity -- The entity has a name set in the editor (e.g., "Propeller") - -For complete API documentation: - -➡️ **[Transform System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingTransformSystem.md)** - ---- - -## Step 1: Find the Entity from Your Scene - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - - init() { - // ... setup code (setupAssetPaths, loadScene, etc.) ... - startGameSystems() - - // Find the entity by name (set in the editor) - propeller = findEntity(name: "Propeller") - - if propeller == nil { - Logger.logWarning(message: "Propeller entity not found in scene") - } - } -} -``` - ---- - -## Step 2: Rotate to an Absolute Angle - -Use `rotateTo()` to set an entity to a specific rotation: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - - init() { - // ... setup code ... - - propeller = findEntity(name: "Propeller") - - // Rotate 45 degrees around the Y-axis (up) - rotateTo(entityId: propeller, angle: 45.0, axis: SIMD3(0, 1, 0)) - } -} -``` - -**Result**: The entity immediately rotates to 45 degrees around the Y-axis. - ---- - -## Step 3: Rotate Incrementally - -Use `rotateBy()` to add rotation to the current orientation: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - - init() { - // ... setup code ... - - propeller = findEntity(name: "Propeller") - - // Rotate 15 degrees from current rotation - rotateBy(entityId: propeller, angle: 15.0, axis: SIMD3(0, 1, 0)) - } -} -``` - -**Result**: The entity rotates an additional 15 degrees around the Y-axis. - ---- - -## Step 4: Continuous Rotation in Update Loop - -For smooth spinning, use `rotateBy()` in `update(deltaTime:)`: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - let rotationSpeed: Float = 90.0 // Degrees per second - - init() { - // ... setup code ... - propeller = findEntity(name: "Propeller") - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Rotate continuously around Y-axis - let angleThisFrame = rotationSpeed * deltaTime - rotateBy(entityId: propeller, angle: angleThisFrame, axis: SIMD3(0, 1, 0)) - } -} -``` - -**Result**: The propeller spins smoothly at 90 degrees per second. - ---- - -## Understanding Rotation Axes - -### Common Rotation Axes - -```swift path=null start=null -// Rotate around Y-axis (up) - typical yaw rotation -rotateBy(entityId: entity, angle: 45.0, axis: SIMD3(0, 1, 0)) - -// Rotate around X-axis (right) - pitch rotation -rotateBy(entityId: entity, angle: 45.0, axis: SIMD3(1, 0, 0)) - -// Rotate around Z-axis (forward) - roll rotation -rotateBy(entityId: entity, angle: 45.0, axis: SIMD3(0, 0, 1)) -``` - -### Using Entity's Local Axes - -You can also rotate around an entity's own forward/right/up vectors: - -```swift path=null start=null -// Rotate around entity's own up direction -let up = getUpAxisVector(entityId: entity) -rotateBy(entityId: entity, angle: 45.0, axis: up) - -// Rotate around entity's own right direction -let right = getRightAxisVector(entityId: entity) -rotateBy(entityId: entity, angle: 45.0, axis: right) -``` - ---- - -## Rotation Examples - -### Spin Clockwise (Y-axis) - -```swift path=null start=null -let angle = rotationSpeed * deltaTime -rotateBy(entityId: entity, angle: angle, axis: SIMD3(0, 1, 0)) -``` - -### Spin Counter-Clockwise (Y-axis) - -```swift path=null start=null -let angle = -rotationSpeed * deltaTime // Negative for opposite direction -rotateBy(entityId: entity, angle: angle, axis: SIMD3(0, 1, 0)) -``` - -### Tumble (X-axis) - -```swift path=null start=null -let angle = rotationSpeed * deltaTime -rotateBy(entityId: entity, angle: angle, axis: SIMD3(1, 0, 0)) -``` - -### Face a Direction - -To face a target, you typically calculate the direction and convert to rotation. This is more advanced, but here's a simple Y-axis example: - -```swift path=null start=null -let targetPos = SIMD3(10, 0, 5) -let currentPos = getPosition(entityId: entity) -let direction = normalize(targetPos - currentPos) - -// Calculate angle to target (simplified for Y-axis only) -let angle = atan2(direction.x, direction.z) * (180.0 / .pi) -rotateTo(entityId: entity, angle: angle, axis: SIMD3(0, 1, 0)) -``` - ---- - -## Checking Current Rotation - -To read an entity's current orientation: - -```swift path=null start=null -// World orientation matrix -let worldOrientation = getOrientation(entityId: entity) -Logger.log(message: "World orientation: \(worldOrientation)") - -// Local orientation matrix (relative to parent) -let localOrientation = getLocalOrientation(entityId: entity) -Logger.log(message: "Local orientation: \(localOrientation)") -``` - ---- - -## Combining Translation and Rotation - -You can move and rotate in the same frame: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - // Move forward - let forward = getForwardAxisVector(entityId: player) - let movement = forward * moveSpeed * deltaTime - translateBy(entityId: player, delta: movement) - - // Rotate based on input - let turnAngle = turnSpeed * deltaTime - rotateBy(entityId: player, angle: turnAngle, axis: SIMD3(0, 1, 0)) -} -``` - ---- - -## Summary - -You've learned: - -✅ `rotateTo()` - Set absolute rotation angle -✅ `rotateBy()` - Rotate incrementally from current orientation -✅ `deltaTime` - Make rotation frame-rate independent -✅ Rotation axes - Control rotation direction -✅ `getOrientation()` - Read current rotation - ---- - - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md b/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md deleted file mode 100644 index 3bbd8abc7..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md +++ /dev/null @@ -1,319 +0,0 @@ -# Animation State Switching - -Learn how to switch between different animations based on player input and game state. - ---- - -## Overview - -This tutorial shows you how to: -- Create a state-based animation system -- Switch between idle, running, and jumping animations -- Trigger animations based on input - ---- - -## Prerequisites - -This tutorial assumes you have: -- Completed the [Play Animation tutorial](./01_PlayAnimation.md) -- A project with multiple animations loaded (idle, running, jumping) -- An entity with a skeleton that supports animation - -For complete API documentation: - -➡️ **[Animation System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingAnimationSystem.md)** - ---- - -## Step 1: Set Up Animation States - -Define an enum to represent your animation states: - -```swift path=null start=null -enum PlayerState { - case idle - case running - case jumping -} - -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - - init() { - // ... setup code ... - startGameSystems() - - // Register input - InputSystem.shared.registerKeyboardEvents() - - player = findEntity(name: "Player") - - // Load all animations -- ignore if you linked all three animations through the editor. - loadPlayerAnimations() - - // Start with idle - changeAnimation(entityId: player, name: "idle") - } - - func loadPlayerAnimations() { - setEntityAnimations( - entityId: player, - filename: "idle", - withExtension: "usdc", - name: "idle" - ) - - setEntityAnimations( - entityId: player, - filename: "running", - withExtension: "usdc", - name: "running" - ) - - setEntityAnimations( - entityId: player, - filename: "jumping", - withExtension: "usdc", - name: "jumping" - ) - } -} -``` - ---- - -## Step 2: Implement State Switching Logic - -Create a function to handle state transitions: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true // Track if player is on ground - - func update(deltaTime: Float) { - if gameMode == false { return } - - updatePlayerState() - } - - func updatePlayerState() { - let oldState = playerState - - // Determine new state based on input and game conditions - if !isGrounded { - playerState = .jumping - } else if isMovementKeyPressed() { - playerState = .running - } else { - playerState = .idle - } - - // Only change animation if state actually changed - if playerState != oldState { - switchToAnimation(for: playerState) - } - } - - func isMovementKeyPressed() -> Bool { - return inputSystem.keyState.wPressed || - inputSystem.keyState.aPressed || - inputSystem.keyState.sPressed || - inputSystem.keyState.dPressed - } - - func switchToAnimation(for state: PlayerState) { - switch state { - case .idle: - changeAnimation(entityId: player, name: "idle") - Logger.log(message: "Switched to idle animation") - - case .running: - changeAnimation(entityId: player, name: "running") - Logger.log(message: "Switched to running animation") - - case .jumping: - changeAnimation(entityId: player, name: "jumping") - Logger.log(message: "Switched to jumping animation") - } - } -} -``` - ---- - -## Step 3: Add Jump Trigger - -Add space bar input to trigger jumping: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true - var jumpTimer: Float = 0.0 - let jumpDuration: Float = 0.5 // Jump animation duration in seconds - - func update(deltaTime: Float) { - if gameMode == false { return } - - handleJumpInput() - updateJumpTimer(deltaTime: deltaTime) - updatePlayerState() - } - - func handleJumpInput() { - // Trigger jump on space press (only if grounded) - if inputSystem.keyState.spacePressed && isGrounded { - isGrounded = false - jumpTimer = jumpDuration - } - } - - func updateJumpTimer(deltaTime: Float) { - // Count down jump timer - if !isGrounded { - jumpTimer -= deltaTime - - // Land when timer expires - if jumpTimer <= 0.0 { - isGrounded = true - jumpTimer = 0.0 - } - } - } - - func updatePlayerState() { - let oldState = playerState - - // Priority: jumping > running > idle - if !isGrounded { - playerState = .jumping - } else if isMovementKeyPressed() { - playerState = .running - } else { - playerState = .idle - } - - if playerState != oldState { - switchToAnimation(for: playerState) - } - } -} -``` - ---- - -## Step 4: Combine Animation with Movement - -Integrate animation state switching with actual movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true - var jumpTimer: Float = 0.0 - let moveSpeed: Float = 5.0 - let jumpDuration: Float = 0.5 - - func update(deltaTime: Float) { - if gameMode == false { return } - - handleJumpInput() - updateJumpTimer(deltaTime: deltaTime) - updatePlayerState() - updatePlayerMovement(deltaTime: deltaTime) - } - - func updatePlayerMovement(deltaTime: Float) { - var movement = SIMD3(0, 0, 0) - - // Only move if grounded - if isGrounded { - if inputSystem.keyState.wPressed { - movement.z += moveSpeed * deltaTime - } - if inputSystem.keyState.sPressed { - movement.z -= moveSpeed * deltaTime - } - if inputSystem.keyState.aPressed { - movement.x -= moveSpeed * deltaTime - } - if inputSystem.keyState.dPressed { - movement.x += moveSpeed * deltaTime - } - - if movement != SIMD3(0, 0, 0) { - translateBy(entityId: player, delta: movement) - } - } - } -} -``` - ---- - -## Advanced: State Machine Pattern - -For more complex state management, consider using a proper state machine: - -```swift path=null start=null -class AnimationStateMachine { - var currentState: PlayerState = .idle - var entityId: EntityID - - init(entityId: EntityID) { - self.entityId = entityId - } - - func canTransition(to newState: PlayerState) -> Bool { - switch (currentState, newState) { - case (.jumping, .idle), (.jumping, .running): - // Can only exit jumping state when landing - return false - default: - return true - } - } - - func transition(to newState: PlayerState) { - guard canTransition(to: newState) else { return } - - if currentState != newState { - currentState = newState - playAnimation(for: newState) - } - } - - func playAnimation(for state: PlayerState) { - let animationName: String - switch state { - case .idle: animationName = "idle" - case .running: animationName = "running" - case .jumping: animationName = "jumping" - } - - changeAnimation(entityId: entityId, name: animationName) - } -} -``` - ---- - -## Summary - -You've learned: - -✅ Create animation states using enums -✅ Switch animations based on game state -✅ Trigger animations from input -✅ Prevent animation flickering with state checks -✅ Combine animations with movement logic - ---- - - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md b/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md deleted file mode 100644 index ef6850642..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md +++ /dev/null @@ -1,331 +0,0 @@ -# Apply Force - -Learn how to use the Physics System to apply forces and create realistic movement. - ---- - -## Overview - -This tutorial shows you how to: -- Enable physics on an entity -- Configure mass and gravity -- Apply forces for dynamic movement -- Use the Steering System for advanced behaviors - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity -- The entity has a name set in the editor (e.g., "Ball") -- The entity has a kinetic component applied through the editor - -For complete API documentation: - -➡️ **[Physics System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingPhysicsSystem.md)** -➡️ **[Steering System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingSteeringSystem.md)** - ---- - -## Step 1: Find the Entity - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - startGameSystems() - - ball = findEntity(name: "Ball") - } -} -``` - ---- - -## Step 2: Enable Physics (Kinetics) - -Before applying forces, enable physics on the entity: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - // Enable physics components - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - } -} -``` - -**What this does**: `setEntityKinetics()` adds `PhysicsComponents` and `KineticComponent` to the entity, enabling it to respond to forces. - ---- - -## Step 3: Configure Mass and Gravity - -Set the entity's mass and gravity scale: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - // Enable physics - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - - // Configure physics properties - setMass(entityId: ball, mass: 0.5) // Lighter = easier to move - setGravityScale(entityId: ball, gravityScale: 1.0) // Normal gravity - } -} -``` - -**Parameters**: -- `mass`: How heavy the entity is. Lower = easier to push. Default is `1.0`. -- `gravityScale`: How much gravity affects it. `0.0` = no gravity, `1.0` = normal gravity. - ---- - -## Step 4: Apply a Force - -Use `applyForce()` to push the entity: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - setMass(entityId: ball, mass: 0.5) - setGravityScale(entityId: ball, gravityScale: 1.0) - - InputSystem.shared.registerKeyboardEvents() - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Apply forward force when W is pressed - if inputSystem.keyState.wPressed { - applyForce(entityId: ball, force: SIMD3(0.0, 0.0, 5.0)) - } - } -} -``` - -**Result**: When you press W, the ball accelerates forward. - -**Important**: Forces are applied every frame while the key is pressed. The physics system automatically integrates forces into velocity and position. - ---- - -## Understanding Forces vs. Direct Movement - -### Direct Movement (Transform System) - -```swift path=null start=null -// Immediate, precise movement -translateBy(entityId: entity, delta: SIMD3(0, 0, speed * deltaTime)) -``` - -✅ Precise control -✅ No physics overhead -❌ No inertia or momentum -❌ Doesn't interact with physics - -### Force-Based Movement (Physics System) - -```swift path=null start=null -// Gradual acceleration with momentum -applyForce(entityId: entity, force: SIMD3(0, 0, 5.0)) -``` - -✅ Realistic inertia -✅ Physics interactions -✅ Momentum and deceleration -❌ Less precise -❌ Requires tuning - ---- - -## Step 5: Use the Steering System - -For easier physics-based movement, use the Steering System: - -### Steer to a Target Position - -```swift path=null start=null -class GameScene { - var player: EntityID! - let maxSpeed: Float = 5.0 - - init() { - // ... setup code ... - player = findEntity(name: "Player") - - setEntityKinetics(entityId: player) // Ignore this if you linked kinetic component through the editor - setMass(entityId: player, mass: 1.0) - setGravityScale(entityId: player, gravityScale: 0.0) // No gravity for top-down - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - let targetPosition = SIMD3(10.0, 0.0, 5.0) - steerSeek(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) - } -} -``` - -**Result**: The entity smoothly accelerates toward the target position. - ---- - -## Steering System Examples - -### Steer with WASD Keys - -The easiest way to add physics-based movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let maxSpeed: Float = 5.0 - - init() { - // ... setup code ... - InputSystem.shared.registerKeyboardEvents() - - player = findEntity(name: "Player") - setEntityKinetics(entityId: player) // Ignore this if you linked kinetic component through the editor - setMass(entityId: player, mass: 1.0) - setGravityScale(entityId: player, gravityScale: 0.0) - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Automatic WASD steering - steerWithWASD(entityId: player, maxSpeed: maxSpeed, deltaTime: deltaTime) - } -} -``` - -**Result**: WASD keys apply forces in the corresponding directions with smooth acceleration/deceleration. - -### Flee from a Threat - -```swift path=null start=null -let threatPosition = SIMD3(0.0, 0.0, 0.0) -steerFlee(entityId: player, threatPosition: threatPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Follow a Path - -```swift path=null start=null -let waypoints = [ - SIMD3(0, 0, 0), - SIMD3(5, 0, 0), - SIMD3(5, 0, 5), - SIMD3(0, 0, 5) -] -steerFollowPath(entityId: player, path: waypoints, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Pursue a Moving Target - -```swift path=null start=null -let enemy = findEntity(name: "Enemy") -steerPursuit(entityId: player, targetEntity: enemy!, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Avoid Obstacles - -```swift path=null start=null -let obstacles = [ - findEntity(name: "Rock1")!, - findEntity(name: "Rock2")!, - findEntity(name: "Tree1")! -] -steerAvoidObstacles(entityId: player, obstacles: obstacles, avoidanceRadius: 2.0, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Arrive at Target (Slowing Down) - -```swift path=null start=null -let targetPosition = SIMD3(10.0, 0.0, 5.0) -steerArrive(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -**Difference from `steerSeek()`**: `steerArrive()` slows down as it approaches the target, preventing overshoot. - ---- - -## Combining Steering Behaviors - -You can use multiple steering behaviors together: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - // Steer toward target while avoiding obstacles - let targetPosition = SIMD3(10.0, 0.0, 5.0) - - steerSeek(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) - - let obstacles = [findEntity(name: "Rock1")!, findEntity(name: "Rock2")!] - steerAvoidObstacles(entityId: player, obstacles: obstacles, avoidanceRadius: 2.0, maxSpeed: maxSpeed, deltaTime: deltaTime) -} -``` - ---- - -## When to Use What? - -### Use Transform System (`translateBy`, `translateTo`) -- Precise, scripted movement -- UI elements or camera -- Platformer-style movement -- When you don't need physics interactions - -### Use Physics System (`applyForce`) -- Projectiles (bullets, grenades) -- Vehicles with custom physics -- When you need low-level control - -### Use Steering System (`steerSeek`, `steerWithWASD`, etc.) -- Character movement in top-down or 3D games -- AI pathfinding and behaviors -- When you want smooth, realistic movement with minimal code - ---- - -## Summary - -You've learned: - -✅ `setEntityKinetics()` - Enable physics on entities -✅ `setMass()` and `setGravityScale()` - Configure physics properties -✅ `applyForce()` - Apply custom forces -✅ `steerWithWASD()` - Easy WASD physics movement -✅ Steering behaviors - Seek, flee, pursue, avoid, arrive -✅ When to use Transform vs. Physics vs. Steering - ---- - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/03-EditorOverview.md b/website/versioned_docs/version-0.10.6/03-Game Development/03-EditorOverview.md deleted file mode 100644 index 191fd6b5f..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/03-EditorOverview.md +++ /dev/null @@ -1,92 +0,0 @@ -# Editor Overview - -The editor is the primary environment for building games with the Untold Engine. - -It is designed to be **explicit, predictable, and tightly integrated** with the engine runtime. -What you see in the editor reflects what runs in the game. - -This page provides a **user-facing overview** of the editor for game developers. - -![editorbottomshot](../images/Editor/EditorBottomShot.png) - ---- - -## Purpose of the Editor - -The editor allows you to: - -- Create and manage scenes -- Inspect and modify entities -- Write and iterate on gameplay scripts -- Manage assets -- Preview runtime behavior in real time - -The editor is not a separate simulation layer — it runs directly on top of the engine runtime. - ---- - -## Core Editor Views - -The editor is composed of several focused views, each with a clear responsibility. - ---- - -### Scene View - -The Scene View is where you visually interact with the world. - -You can: -- Navigate the scene camera -- Select entities -- Translate, rotate, and scale entities -- Preview lighting and scene composition - -This view reflects the engine’s real transform and rendering state. - -![editormainshot](../images/Editor/EditorMainShot.png) - ---- - -### Scene Hierarchy View - -The Scene Hierarchy shows all entities in the current scene. - -It allows you to: -- View parent–child relationships -- Select entities -- Organize scene structure - -The hierarchy mirrors the engine’s internal scene graph. - -![editorscenegraphview](../images/Editor/EditorScenegraphView.png) - ---- - -### Inspector View - -The Inspector displays detailed information about the selected entity. - -From the Inspector, you can: -- View and modify components -- Adjust transforms and properties -- Attach scripts and assets - -Changes made in the Inspector apply directly to the runtime state. - -![editorinspectorview](../images/Editor/EditorInspectorView.png) - ---- - -### Asset Browser View - -The Asset Browser provides access to project assets such as: - -- Models -- Textures -- Scripts -- Scenes - -Assets imported through the editor are organized and made available to the engine automatically. - -![editorassetbrowserview](../images/Editor/EditorAssetBrowserView-alt.png) - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md b/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md deleted file mode 100644 index 06d6abd30..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -id: usc-scripting-introducction -title: Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -# Introduction to USC - -USC (Untold Script Core) is the scripting system used to define **gameplay behavior** in Untold Engine. - -USC is designed to be: -- Explicit -- Predictable -- Easy to read and reason about - -It focuses on *what your game does*, not how the engine is implemented. - ---- - -## What USC Is - -USC is a **script-based API** that allows you to attach behavior to entities. - -You use USC to: -- Respond to input -- Move entities -- Control cameras -- Apply forces -- Drive simple gameplay logic - -USC scripts describe *behavior over time*. - ---- - -## What USC Is Not - -USC is **not**: -- A general-purpose programming language -- A replacement for engine code -- A system for complex algorithms or heavy math - -If you need low-level control or complex systems, those belong in the engine itself. - -USC intentionally keeps the surface area small. - ---- - -## Why USC Exists - -Traditional game engines often expose: -- Large, complex APIs -- Implicit behavior -- Hidden update order - -USC takes a different approach. - -It is designed so that: -- Scripts read top-to-bottom -- Behavior is explicit -- There is no hidden execution magic - -This makes gameplay logic easier to understand, debug, and maintain. - ---- - -## How USC Fits into Untold Engine - -USC sits **above the engine** and **below the editor**. - -- The engine provides systems and data -- USC expresses gameplay intent -- The editor helps you attach and manage scripts - -USC does not replace the engine — it builds on top of it. - ---- - -## The Script Lifecycle - -A USC script runs as part of the engine update loop. - -At a high level: -1. The engine updates input -2. USC scripts are evaluated -3. The engine applies the results - -Scripts are evaluated every frame while the game is running. - -You do not manually manage update loops. - ---- - -## Working with Entities - -USC scripts operate on **entities**. - -A script is attached to an entity and can: -- Read entity state -- Modify transforms -- Interact with components -- Respond to input - -Scripts do not create entities or systems — they control behavior. - ---- - -## Example Use Cases - -USC is ideal for: -- Player movement -- Camera follow logic -- Simple physics interaction -- Trigger-based events -- Prototyping gameplay ideas - -If something feels *game-specific*, it likely belongs in USC. - ---- - -## Design Philosophy - -USC follows a few guiding principles: - -- **Explicit over implicit** - No hidden behavior. - -- **Readable over clever** - Scripts should be easy to understand at a glance. - -- **Stable over flexible** - Fewer features, fewer surprises. - -These principles are intentional. - - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/02_QuickStart.md b/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/02_QuickStart.md deleted file mode 100644 index bb021ce36..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/02_QuickStart.md +++ /dev/null @@ -1,197 +0,0 @@ -# USC Scripting – Quick Start - -This tutorial will walk you through creating your first script in the Untold Engine, from setup to testing in Play mode. - -**What you'll build:** A cube that moves upward by changing its position every frame. - -**Time:** ~10 minutes - ---- - -## Step 1: Create Your First Script - -### 1.1 Configure Asset Path - -1. Open Untold Engine. -2. Go to **Asset Library → Set Asset Folder** -3. Create/Select your projects's asset directory. - -This is where your assets will be saved. Including your Scripts. - -![assetlibraryloupe](../../../images/Editor/EditorAssetLibraryLoupe.png) - -### 1.2 Create Your First Script - -1. In the editor toolbar, click **Scripts: Open in Xcode** (blue button). -2. Click the `+` button to create a new script. -3. Enter the script name: `MovingCube` -4. Xcode opens the Scripts package so you can edit the new file. - -When the script is created: -- The source file is added to your project -- Xcode shows a template like this: - -```swift -import Foundation -import UntoldEngine - -extension GenerateScripts { - static func generateMovingCube(to dir: URL) { - // Write your script here - } -} -``` - -You will edit everything in Xcode. - ---- - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateMovingCube(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateMovingCube) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - -## Step 3: Write USC Logic - -Replace the function with this complete script: - -```swift -extension GenerateScripts { - static func generateMovingCube(to dir: URL) { - let script = buildScript(name: "MovingCube") { s in - // Run every frame - s.onUpdate() - .getProperty(.position, as: "pos") - .setVariable("offset", to: simd_float3(x: 0.0, y: 0.1, z: 0.0)) - .addVec3("pos", "offset", as: "newPos") - .setProperty(.position, toVariable: "newPos") - } - - let outputPath = dir.appendingPathComponent("MovingCube.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MovingCube.uscript") - } -} -``` - ---- - -## Step 4: Build Scripts - -After editing scripts, build and run in Xcode so the engine can use the generated output. - -- In Xcode, press `Cmd+R` to run the GenerateScripts target and produce the `.uscript` files. (Optional: `Cmd+B` to check the build first.) -- Build and run output appears inside Xcode. - -> A successful build makes the script available in the Asset Library to attach to entities. - -**First build?** It may take 30-60 seconds to download dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Add an Entity to the scene - -1. In the editor, click on "+" in the scenegraph -2. Click on "Cube" -3. A cube will show up in the scene. Make sure to select it. - -## Step 6: Link script to entity - -4. Open the **Asset Library** -5. Under the Script category, look for your script `MovingCube` and double click on the `.uscript`. This will automatically link the script to the entity. -6. To verify, the Inspector View will show the newly added script linked to the entity. - -![editorassetbrowserscript](../../../images/Editor/EditorAssetBrowserScripts.png) - -The script is now active and will run according to the engine update loop. - ---- - -## Step 7: Test in Play Mode - -### 7.1 Run the Scene - -1. Click **Play** in the editor -2. Watch your entity move upward continuously! - -### 7.2 Stop Play Mode - -Click **Stop** when done testing. - ---- - -## Step 8: Test Hot Reload - Move Sideways - -Let's modify the script to move the cube sideways instead of upward, and test the hot reload feature. - -### 8.1 Modify the Offset - -Modify your script to change the offset direction: - -```swift -extension GenerateScripts { - static func generateMovingCube(to dir: URL) { - let script = buildScript(name: "MovingCube") { s in - s.onUpdate() - .getProperty(.position, as: "pos") - .setVariable("offset", to: simd_float3(x: 0.1, y: 0.0, z: 0.0)) // Changed to move sideways - .addVec3("pos", "offset", as: "newPos") - .setProperty(.position, toVariable: "newPos") - } - - let outputPath = dir.appendingPathComponent("MovingCube.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MovingCube.uscript") - } -} -``` - -**What changed:** The offset is now `(0.1, 0.0, 0.0)` instead of `(0.0, 0.1, 0.0)`, so the cube will move along the X-axis (sideways) instead of the Y-axis (upward). - -### 8.2 Rebuild - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target. -2. Wait for "✅ MovingCube.uscript" in the Xcode run output - -### 8.3 Hot Reload - -Back in the editor: -1. With your entity selected -2. Click **"Reload"** (orange button) in the Script Component Inspector -3. Click **Play** to test the changes -4. The cube should now move sideways instead of upward! - -![scriptreload](../../../images/Editor/ScriptReload.png) - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md b/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md deleted file mode 100644 index 87ff77797..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md +++ /dev/null @@ -1,108 +0,0 @@ -# USC Scripting Workflow - -This document describes the **end-to-end workflow** for developing USC scripts in the Untold Engine. - -USC scripts are authored in Xcode. The Untold Editor coordinates assets and playback but does not include a built-in script editor. - ---- - -## Overview - -The USC workflow follows a clear sequence: - -1. Author scripts -2. Register scripts for generation -3. Build scripts -4. Attach scripts to entities -5. Iterate using hot reload - -The editor is the central coordination point for this process, while Xcode is where you author scripts. - ---- - -## Primary Editing Surface - -USC scripts are written in Xcode. Use **Scripts: Open in Xcode** to open the Scripts package; the Untold Editor does not provide an in-editor script editor. - -Xcode provides: -- Direct access to script source files -- Editing of generation entry points -- Build/run feedback and diagnostics - ---- - -## Script Authoring - -Scripts are written as Swift source files using the USC DSL. - -Responsibilities: -- Express gameplay behavior -- React to input and engine state -- Remain independent of engine internals - -Scripts do not: -- Manage entities directly -- Access low-level engine systems -- Perform rendering or physics logic - ---- - -## Script Registration - -All USC scripts are registered through `GenerateScripts.swift`. - -This file: -- Defines which scripts are generated -- Acts as a single source of truth for scripting output -- Is edited directly in Xcode - -Unregistered scripts are ignored by the engine. - ---- - -## Script Generation and Build - -Building and running the GenerateScripts target: -- Runs the USC generation pipeline -- Produces engine-consumable output -- Reports errors and warnings in Xcode - -Builds are explicit and controlled. - ---- - -## Runtime Attachment - -After a successful build: -- Scripts appear as selectable components -- Scripts can be attached to entities via the Inspector -- Multiple entities may share the same script - -The engine controls execution timing. - ---- - -## Hot Reload and Iteration - -USC supports rapid iteration: - -- Edit script -- Build scripts -- Observe changes immediately - -This workflow encourages experimentation and fast feedback. - ---- - -## Design Intent - -The USC workflow is intentionally: - -- Xcode-first for authoring -- Editor-coordinated for playback -- Explicit -- Predictable -- Easy to debug - -It avoids hidden build steps and implicit execution. - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md b/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md deleted file mode 100644 index e1744538d..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md +++ /dev/null @@ -1,167 +0,0 @@ -# USC Scripts - -This document explains **what a USC script is**, how it fits into the Untold Engine, and the rules that govern its behavior. - -It is intended to give you a **clear mental model** before diving into APIs, examples, or workflows. - -This page does **not** describe how to write scripts step by step. -It explains what scripts *represent* and *why they are designed the way they are*. - ---- - -## What Is a USC Script? - -A **USC script** is a declarative description of gameplay behavior. - -You write scripts in Swift using a constrained, fluent DSL, and the engine executes those scripts at runtime as part of its update loop. - -A script expresses **intent**, not implementation details. - -Examples of intent: -- Move an entity when input is received -- Apply forces in response to events -- Adjust camera behavior over time - -The engine owns *how* those intentions are executed. - ---- - -## Scripts and Entities - -USC scripts are **attached to entities**. - -- A script always operates in the context of the entity it is attached to -- Scripts do not own entities -- Scripts do not create or destroy entities - -Multiple entities may share the same script definition, each with its own execution context and script-local state. - ---- - -## Execution Model - -Scripts participate in the engine’s execution model. - -Key characteristics: - -- Scripts run when their declared events are triggered -- The engine controls execution order and timing -- Scripts do not manage threads or scheduling -- Execution is deterministic and engine-driven - -A script never decides *when* it runs — it only declares *what should happen* when it does. - ---- - -## Script Lifecycle - -At a high level, a USC script goes through the following lifecycle: - -1. Script is authored -2. Script is registered and generated -3. Script is attached to an entity -4. Script becomes active -5. Script executes in response to events -6. Script stops executing when detached or when the entity is destroyed - -Lifecycle transitions are managed by the engine. - ---- - -## Events and Entry Points - -Scripts are organized around **events**, also called entry points. - -Examples include: -- Startup events -- Per-frame updates -- Custom or engine-driven events - -Each event defines **when a block of instructions executes**. - -Scripts may contain multiple event handlers, each expressing a different behavior. - ---- - -## Script Context - -Every script executes with a **context** provided by the engine. - -The context gives controlled access to: -- The attached entity’s properties -- Script-defined variables -- Engine-provided values (such as delta time or input state) - -Scripts never access engine state directly — all access flows through this context. - ---- - -## State and Variables - -Scripts may store local state using variables. - -- Variables are scoped to the script instance -- Each entity gets its own variable set -- Variables persist across executions unless reset - -Script variables are designed for **lightweight gameplay state**, not long-term data storage. - ---- - -## What Scripts Can Do - -USC scripts are designed to express common gameplay behaviors, such as: - -- Reading input -- Modifying transforms -- Applying forces or impulses -- Steering entities -- Controlling cameras -- Triggering animations -- Reacting to events - -Scripts focus on *what should happen*, not *how systems work internally*. - ---- - -## What Scripts Cannot Do - -USC scripts intentionally **cannot**: - -- Create or destroy entities -- Access rendering pipelines or GPU resources -- Manage memory -- Spawn threads -- Call arbitrary engine internals -- Control execution order or threading - -These constraints are deliberate and central to USC’s design. - ---- - -## Design Intent - -USC exists to provide: - -- Predictable gameplay behavior -- Clear ownership of state -- Safe boundaries between game logic and engine internals -- Fast iteration and easy debugging - -By limiting what scripts can do, the engine remains stable and behavior remains easy to reason about. - ---- - -## Relationship to the API Reference - -This document explains **what a script is**. - -For detailed information about: -- Available events -- Instructions and commands -- Math operations -- Input handling -- Physics and camera helpers - -See **USC → API Reference**. - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/00_HelloWorld.md b/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/00_HelloWorld.md deleted file mode 100644 index d2300510a..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/00_HelloWorld.md +++ /dev/null @@ -1,228 +0,0 @@ -# "Hello World" - Your First Script - -**What you'll learn:** -- Creating a script in Untold Engine Studio -- Using `onStart()` and `onUpdate()` lifecycle events -- Logging messages for debugging -- Attaching scripts to entities - -**Time:** ~5 minutes - ---- - -## What We're Building - -A simple script that: -1. Logs "Hello, Untold Engine!" when the entity starts -2. Logs "Script running..." every frame during Play mode - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `HelloWorld` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateHelloWorld(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateHelloWorld(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateHelloWorld) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateHelloWorld(to dir: URL) { - let script = buildScript(name: "HelloWorld") { s in - // Runs once when entity starts - s.onStart() - .log("Hello, Untold Engine!") - .log("Script initialized successfully") - - // Runs every frame - s.onUpdate() - .log("Script running...") - } - - let outputPath = dir.appendingPathComponent("HelloWorld.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ HelloWorld.uscript") - } -} -``` - -### Understanding the Code - -**`buildScript(name:)`** - Creates a new script -- The name identifies the script in the editor - -**`onStart()`** - Lifecycle event that runs once -- Perfect for initialization -- Logs appear in the console when Play mode starts - -**`onUpdate()`** - Lifecycle event that runs every frame -- Use for continuous behaviors -- Be mindful of performance (runs 60+ times per second!) - -**`.log()`** - Outputs debug messages -- Messages appear in the editor's Console view -- Great for debugging and tracking execution - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ HelloWorld.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select any entity in your scene (create a cube if needed) -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `HelloWorld.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. Open the **Console** view (bottom panel) -3. You should see: - ``` - Hello, Untold Engine! - Script initialized successfully - Script running... - Script running... - Script running... - ... - ``` - -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- **"Hello, Untold Engine!"** appears once (from `onStart()`) -- **"Script running..."** appears continuously (from `onUpdate()`) - -⚠️ **Performance Note:** `onUpdate()` runs every frame! In a real game, avoid heavy logging in `onUpdate()`. This example is just for demonstration. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Change the Messages -```swift -s.onStart() - .log("Game initialized") - .log("Player ready!") -``` - -### Remove the Update Log -```swift -s.onUpdate() - // Remove .log() to avoid console spam -``` - -### Add Initialization Variables -```swift -s.onStart() - .setVariable("playerName", to: "Hero") - .setVariable("health", to: 100.0) - .log("Player initialized with 100 health") -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ How to create a script in Untold Engine Studio -✅ Using `onStart()` for initialization -✅ Using `onUpdate()` for per-frame logic -✅ Logging debug messages -✅ Building and attaching scripts to entities -✅ Testing scripts in Play mode - ---- diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md b/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md deleted file mode 100644 index 57cf2d07e..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md +++ /dev/null @@ -1,235 +0,0 @@ -# Move an Entity - Basic Translation - -**What you'll learn:** -- Moving an entity with `setProperty(.position)` -- Using `simd_float3` direction vectors -- Controlling speed with script variables -- Updating position every frame with `onUpdate()` - -**Time:** ~7 minutes - -**Prerequisites:** -- Untold Engine Studio installed and a scene with any entity (a cube works great) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple movement script that: -1. Moves an entity steadily along a chosen axis -2. Uses variables to control speed and direction -3. Logs a message when movement begins - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `MoveAnEntity` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateMoveAnEntity(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateMoveAnEntity(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateMoveAnEntity) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateMoveAnEntity(to dir: URL) { - let script = buildScript(name: "MoveAnEntity") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("moveSpeed", to: 0.05) // units per frame - .setVariable("direction", to: simd_float3(x: 0, y: 0, z: 1)) - .log("MoveAnEntity ready") - - // Runs every frame - s.onUpdate() - .getProperty(.position, as: "currentPos") - .scaleVec3("direction", by: "moveSpeed", as: "step") - .addVec3("currentPos", "step", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") - } - - let outputPath = dir.appendingPathComponent("MoveAnEntity.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MoveAnEntity.uscript") - } -} -``` - -### Understanding the Code - -**`moveSpeed` + `direction`** - Control how fast and where to move -- Speed is a scalar; direction is a vector (x, y, z) -- Change either without touching the rest of the script - -**`getProperty(.position, as:)`** - Reads the current position -- Positions are `simd_float3` values - -**`scaleVec3()` + `addVec3()`** - Build the new position -- Scales the direction by speed -- Adds it to the current position for smooth motion - -**`setProperty(.position, toVariable:)`** - Applies the updated position -- Runs every frame inside `onUpdate()` -- Keeps the movement continuous while Play mode runs - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ MoveAnEntity.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 4: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select any entity in your scene (a cube or platform) -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `MoveAnEntity.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 5: Test It! - -1. Click **Play** in the toolbar -2. Watch the entity move steadily along the +Z axis -3. Check the **Console** view to confirm: - ``` - MoveAnEntity ready - ``` -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- The entity moves every frame toward +Z -- Speed comes from `moveSpeed`; direction comes from `direction` -- To stop movement, disable the Script Component or set speed to 0 - -⚠️ **Placement Note:** If the entity is already near a boundary, lower the speed or adjust the start position to avoid moving out of view. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Move Upward Instead -```swift -.setVariable("direction", to: simd_float3(x: 0, y: 1, z: 0)) -``` - -### Slow or Fast Motion -```swift -.setVariable("moveSpeed", to: 0.01) // slower -// or -.setVariable("moveSpeed", to: 0.15) // faster -``` - -### Pause After a Distance -```swift -s.onUpdate() - .getProperty(.position, as: "currentPos") - .ifGreater("currentPos.z", than: 10.0) { n in - n.setVariable("moveSpeed", to: 0.0) // stop after z > 10 - } - .scaleVec3("direction", by: "moveSpeed", as: "step") - .addVec3("currentPos", "step", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Creating a movement script in Untold Engine Studio -✅ Using variables for speed and direction -✅ Updating position every frame with `onUpdate()` -✅ Building and attaching scripts to entities -✅ Testing motion in Play mode - ---- - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/02_RotateAnEntity.md b/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/02_RotateAnEntity.md deleted file mode 100644 index 7e92ccbb5..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/02_RotateAnEntity.md +++ /dev/null @@ -1,239 +0,0 @@ -# Rotate an Entity - Continuous Spin - -**What you'll learn:** -- Rotating entities with `rotateTo()` inside `onUpdate()` -- Tracking angles with script variables -- Choosing rotation axes with `simd_float3` -- Building USC scripts entirely in Xcode - -**Time:** ~7 minutes - -**Prerequisites:** -- Untold Engine Studio open with any entity selected (a cube makes rotation easy to see) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A rotation script that: -1. Spins an entity around a chosen axis -2. Uses a variable to track the current angle -3. Lets you adjust spin speed without rewriting the script - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `RotateAnEntity` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateRotateAnEntity(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateRotateAnEntity(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateRotateAnEntity) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateRotateAnEntity(to dir: URL) { - let script = buildScript(name: "RotateAnEntity") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("rotationSpeed", to: 2.0) // degrees per frame - .setVariable("currentAngle", to: 0.0) - .setVariable("axis", to: simd_float3(x: 0, y: 1, z: 0)) - .log("RotateAnEntity ready") - - // Runs every frame - s.onUpdate() - .addFloat("currentAngle", "rotationSpeed", as: "nextAngle") - .setVariable("currentAngle", to: .variableRef("nextAngle")) - .rotateTo( - degrees: .variableRef("currentAngle"), - axis: simd_float3(x:0,y:1,z:0) - ) - } - - let outputPath = dir.appendingPathComponent("RotateAnEntity.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ RotateAnEntity.uscript") - } -} -``` - -### Understanding the Code - -**`rotationSpeed`** - Controls how much to rotate each frame -- Higher values spin faster -- Change this without touching the rest of the script - -**`currentAngle`** - Tracks total rotation -- Prevents drift and keeps rotation smooth - -**`axis`** - A `simd_float3` defining which way to spin -- `(0, 1, 0)` spins around Y (like a turntable) -- `(1, 0, 0)` spins forward/backward -- `(0, 0, 1)` spins left/right - -**`rotateTo()`** - Sets an absolute rotation -- Uses the stored angle each frame -- Cleanly resets if you set `currentAngle` back to `0` - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ RotateAnEntity.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select the entity you want to rotate -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `RotateAnEntity.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. Watch the entity spin around the Y-axis -3. Open the **Console** view to confirm: - ``` - RotateAnEntity ready - ``` -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- The entity rotates smoothly using `rotationSpeed` degrees per frame -- Changing `axis` changes the spin direction without new code -- Resetting `currentAngle` to `0` realigns the entity instantly - -⚠️ **Consistency Note:** Large `rotationSpeed` values can cause visible jumps. Increase gradually for smoother motion. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Rotate with `rotateBy()` -```swift -s.onUpdate() - .rotateBy( - degrees: 1.0, - axis: simd_float3(x: 0, y: 1, z: 0) - ) -``` - -### Spin on Multiple Axes -```swift -.setVariable("axis", to: simd_float3(x: 1, y: 1, z: 0)) // diagonal spin -``` - -### Start Facing a Specific Direction -```swift -s.onStart() - .rotateTo( - degrees: 45.0, - axis: simd_float3(x: 0, y: 1, z: 0) - ) - .setVariable("currentAngle", to: 45.0) -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Creating a rotation script in Untold Engine Studio -✅ Tracking angles with script variables -✅ Rotating entities around any axis -✅ Building and attaching scripts to entities -✅ Testing rotations in Play mode - ---- - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md b/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md deleted file mode 100644 index 6542e635f..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md +++ /dev/null @@ -1,241 +0,0 @@ -# Play an Animation - Single Clip - -**What you'll learn:** -- Playing an animation clip with `playAnimation()` -- Looping vs. one-shot playback -- Using variables to track the current clip -- Building and testing animations from Xcode - -**Time:** ~8 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity that has an **Animation Component** and at least one loaded clip (e.g., `idle`) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple animation script that: -1. Plays a single clip on start -2. Loops it continuously -3. Shows how to trigger a one-shot clip on demand - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `PlayAnimation` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generatePlayAnimation(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generatePlayAnimation(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generatePlayAnimation) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generatePlayAnimation(to dir: URL) { - let script = buildScript(name: "PlayAnimation") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("idleClip", to: "idle") // Must match the clip name in the Animation Component - .setVariable("currentClip", to: "idle") - .playAnimation("idle", loop: true) - .log("Playing idle animation (looping)") - - // Optional: trigger a one-shot animation with Space - s.onUpdate() - .getKeyState("space", as: "spacePressed") - .ifEqual("spacePressed", to: true) { n in - n.playAnimation("jump", loop: false) // Replace with your one-shot clip name - n.setVariable("currentClip", to: "jump") - n.log("Playing jump animation (one-shot)") - } - } - - let outputPath = dir.appendingPathComponent("PlayAnimation.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ PlayAnimation.uscript") - } -} -``` - -### Understanding the Code - -**`playAnimation(name, loop:)`** - Starts an animation by name -- `loop: true` repeats continuously (great for idle or walk) -- `loop: false` plays once (great for jump or attacks) - -**Clip names** - Must match exactly what you see in the Animation Component -- Case-sensitive -- If you rename a clip in the editor, update the script too - -**`currentClip` variable** - Tracks what is playing -- Useful when adding more conditions later - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ PlayAnimation.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select an entity that has an **Animation Component** -3. In the Asset Library, double click on the animations you want to add to the entity, i.e. `idle` and `jump` -4. In the Inspector panel, click **Add Component** → **Script Component** -5. In the Asset Browser, find `PlayAnimation.uscript` under Scripts/Generated -6. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. The entity should immediately play the `idle` clip on loop -3. Tap **Space** to trigger the one-shot clip (e.g., `jump`) -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- Looping clip runs continuously until another clip interrupts it -- One-shot clip plays once; if you need to return to idle automatically, add a timer or state check -- If nothing plays, verify the clip names match the Animation Component exactly - -⚠️ **Asset Note:** The script only calls animations that already exist on the entity’s Animation Component. Ensure clips are loaded and named correctly. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Start with a Different Clip -```swift -.setVariable("idleClip", to: "breathing_idle") -.playAnimation("breathing_idle", loop: true) -``` - -### Return to Idle After One-Shot -```swift -s.onUpdate() - .getKeyState("space", as: "spacePressed") - .ifEqual("spacePressed", to: true) { n in - n.playAnimation("jump", loop: false) - n.setVariable("currentClip", to: "jump") - } - // Simple timer to return to idle after ~1 second - .setVariable("returnTimer", to: 60.0) // frames at ~60 FPS - .ifGreater("returnTimer", than: 0.0) { n in - n.addFloat("returnTimer", literal: -1.0, as: "returnTimer") - } - .ifEqual("returnTimer", to: 0.0) { n in - n.playAnimation("idle", loop: true) - n.setVariable("currentClip", to: "idle") - } -``` - -### Add a Stop Button -```swift -.getKeyState("p", as: "pPressed") -.ifEqual("pPressed", to: true) { n in - n.stopAnimation() - n.log("Animation stopped") -} -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Playing animation clips with `playAnimation()` -✅ Looping vs. one-shot playback -✅ Tracking the current clip with script variables -✅ Building, attaching, and testing animation scripts - ---- - diff --git a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/02_AnimationStateSwitch.md b/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/02_AnimationStateSwitch.md deleted file mode 100644 index a43e42f11..000000000 --- a/website/versioned_docs/version-0.10.6/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/02_AnimationStateSwitch.md +++ /dev/null @@ -1,265 +0,0 @@ -# Animation State Switch - Idle to Walk - -**What you'll learn:** -- Switching animations based on input -- Tracking current state to avoid restart flicker -- Using `playAnimation()` for looped clips -- Building and testing animations from Xcode - -**Time:** ~10 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity that has an **Animation Component** and two clips loaded (e.g., `idle`, `walk`) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -An animation controller that: -1. Plays `idle` when no movement keys are pressed -2. Switches to `walk` when any WASD key is pressed -3. Only changes clips when the state actually changes - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `AnimationStateSwitch` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateAnimationStateSwitch(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateAnimationStateSwitch(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateAnimationStateSwitch) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateAnimationStateSwitch(to dir: URL) { - let script = buildScript(name: "AnimationStateSwitch") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("currentAnim", to: "idle") - .playAnimation("idle", loop: true) - .log("AnimationStateSwitch ready - idle playing") - - // Runs every frame - s.onUpdate() - // Read movement keys - .getKeyState("w", as: "wPressed") - .getKeyState("a", as: "aPressed") - .getKeyState("s", as: "sPressed") - .getKeyState("d", as: "dPressed") - - // Determine if moving - .setVariable("isMoving", to: false) - .ifEqual("wPressed", to: true) { n in n.setVariable("isMoving", to: true) } - .ifEqual("aPressed", to: true) { n in n.setVariable("isMoving", to: true) } - .ifEqual("sPressed", to: true) { n in n.setVariable("isMoving", to: true) } - .ifEqual("dPressed", to: true) { n in n.setVariable("isMoving", to: true) } - - // Switch to walk when moving - .ifEqual("isMoving", to: true) { n in - n.ifCondition(lhs: .variableRef("currentAnim"), .notEqual, rhs: .string("walk")) { change in - change.playAnimation("walk", loop: true) - change.setVariable("currentAnim", to: "walk") - change.log("Switched to walk") - } - } - - // Switch back to idle when not moving - .ifEqual("isMoving", to: false) { n in - n.ifCondition(lhs: .variableRef("currentAnim"), .notEqual, rhs: .string("idle")) { change in - change.playAnimation("idle", loop: true) - change.setVariable("currentAnim", to: "idle") - change.log("Switched to idle") - } - } - } - - let outputPath = dir.appendingPathComponent("AnimationStateSwitch.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ AnimationStateSwitch.uscript") - } -} -``` - -### Understanding the Code - -**State tracking** - `currentAnim` stores what is playing -- Prevents restarting the same clip every frame -- Eliminates flicker when staying in the same state - -**Input gating** - `isMoving` becomes true if any WASD key is pressed -- One boolean drives all animation switching - -**`playAnimation()` only on change** - Uses a condition to switch -- Keeps transitions smooth -- Avoids repeating the same clip call - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ AnimationStateSwitch.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Add an entity to the scene. -3. In the Asset Library, double click on the animations you want to add to the entity, such as `idle` and `walk` -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `AnimationStateSwitch.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 5: Test It! - -1. Click **Play** in the toolbar -2. With no input, the entity plays `idle` -3. Press any of `W`, `A`, `S`, `D` to switch to `walk` -4. Release the keys to return to `idle` -5. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- Idle is the default and fallback state -- Walk plays only while movement keys are down -- No flicker because the script checks before switching - -⚠️ **Name Matching:** Clip names are case-sensitive. If your clips are named differently (e.g., `Idle`, `Walking`), update the script strings. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Add a Run State with Shift -```swift -.getKeyState("lshift", as: "shiftPressed") -.ifEqual("shiftPressed", to: true) { n in - n.ifEqual("isMoving", to: true) { run in - run.ifCondition(lhs: .variableRef("currentAnim"), .notEqual, rhs: .string("run")) { change in - change.playAnimation("run", loop: true) - change.setVariable("currentAnim", to: "run") - } - } -} -``` - -### Add a Jump One-Shot -```swift -.getKeyState("space", as: "jumpPressed") -.ifEqual("jumpPressed", to: true) { n in - n.playAnimation("jump", loop: false) - n.setVariable("currentAnim", to: "jump") -} -``` - -### Reset to Idle After a Delay -```swift -.setVariable("idleTimer", to: 30.0) // half-second at ~60 FPS -.ifEqual("isMoving", to: false) { n in - n.addFloat("idleTimer", literal: -1.0, as: "idleTimer") - n.ifEqual("idleTimer", to: 0.0) { back in - back.playAnimation("idle", loop: true) - back.setVariable("currentAnim", to: "idle") - } -} -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Switching animations based on player input -✅ Tracking the current clip to avoid restart flicker -✅ Using looped clips for idle/walk states -✅ Building, attaching, and testing animation controllers - ---- - diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/01-Overview.md b/website/versioned_docs/version-0.10.6/04-Engine Development/01-Overview.md deleted file mode 100644 index 670971d70..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/01-Overview.md +++ /dev/null @@ -1,94 +0,0 @@ ---- -id: engine-overview -title: Overview -sidebar_position: 1 ---- - -# Overview - -This section is for **engine developers and contributors** who want to work on the core of Untold Engine. - -It assumes familiarity with engine concepts and systems programming. - ---- - -## What You’ll Work On - -In this section, you’ll learn about: -- The engine architecture -- The ECS design -- Rendering systems -- Simulation and update flow -- Platform-specific layers - -This is where the **core behavior** of Untold Engine lives. - ---- - -## Installing the Engine for Development - -Engine development requires installing Untold Engine via the command line and working from source. - -Detailed installation steps are provided here: - - -1. Clone the Repository - -```bash -git clone https://github.com/untoldengine/UntoldEngine -cd UntoldEngine -open Package.swift -``` - -**How to Run** -1. Select the **DemoGame** scheme. -2. Download the [Demo Game Assets v1.0](https://github.com/untoldengine/UntoldEngine-Assets/releases/tag/v1) and place them in your Desktop folder. -3. Set the Scheme to **Demo Game**. Then set **My Mac** as the target device and hit **Run**. -4. Use **WASD** keys to move the player around. - - -![DemoGame](../images/demogame-noeditor.png) - - -You do not need Untold Engine Studio to work on the engine itself. - ---- - -## Engine Architecture - -Untold Engine is built around: -- A clear ECS model -- Explicit system update order -- Minimal hidden state -- Platform-specific rendering backends - -The architecture favors **clarity over abstraction**. - -A high-level breakdown is provided in: - -> **Engine Development → Architecture** - ---- - -## Rendering and Systems - -Rendering, physics, and other systems are implemented as: -- Independent modules -- Explicit update stages -- Predictable data flow - -Rendering is designed to expose low-level control while remaining approachable. - ---- - -## Who This Section Is Not For - -This section is **not** intended for: -- Game developers writing gameplay scripts -- Users who only want to use the editor -- Beginners to engine development - -If you want to make a game, see: - -> **Game Development → Overview** - diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/02-Architecture/Internals.md b/website/versioned_docs/version-0.10.6/04-Engine Development/02-Architecture/Internals.md deleted file mode 100644 index 0d5c089d4..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/02-Architecture/Internals.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: engine-architecture -title: Engine Architecture Internals -sidebar_position: 2 ---- - -# Untold Engine Architecture Internals - -You’re looking under the hood of Untold Engine. This is how the pieces fit together, why we picked them, and where contributors can plug in. - -## The Big Idea: ECS at the Core -Untold Engine is deliberately **ECS-first**: -- **Entities** are just 64-bit IDs (index + version) managed in `Scenes.swift` with pooling, masks, and tombstones. -- **Components** are plain data (no logic) in `ECS/Components.swift`: transforms, render payloads, physics state, animation sets, lights, scripts, etc. Components live in type-specific pools keyed by component ID. -- **Systems** are the behavior layer in `Sources/UntoldEngine/Systems/`. Each system queries entities by component mask and runs every frame (or in fixed steps for physics). Examples: Transform, Scenegraph, Rendering, Physics, Animation, Input, Culling, Loading, Lighting, Shadow, Steering, USC scripting. - -Why this matters: contributors can add data (components) and behavior (systems) without rewriting the core. Keep components dumb, keep systems focused, and everything stays modular and testable. - -## Frame Flow -`UntoldRenderer.runFrame` orchestrates each tick: -1) **Simulation prep**: delta time, scene graph traversal, input handling. -2) **Gameplay & scripting**: AnimationSystem, USCSystem, custom game update callbacks. -3) **Physics**: fixed-timestep accumulator, gravity/drag/forces, Runge–Kutta integration (collision/contact still open for contribution). -4) **Culling & rendering**: frustum cull, Gaussian depth/sort, build render graph, execute passes, present. - -## Rendering Stack -- **Entry point**: `UntoldRenderer` (MTKView delegate) sets up device/queue, loads metallib, initializes buffers, and drives the frame loop. Platform variants exist for macOS/iOS, visionOS (XR), and AR. -- **Render graph**: `RenderingSystem.swift` builds a dependency graph (environment/grid/AR base → shadow → GBuffer/model → gaussian → post → precomp) and executes via `RenderPasses` + `PipelineManager`. -- **Pipelines**: Render and compute pipelines live in `Renderer/Pipelines/`. Gaussian splats run dedicated compute passes (depth + bitonic sort) before their render pass. - -## Physics & Motion -- **Data**: `PhysicsComponents` and `KineticComponent` store mass, velocity/angular velocity, drag, inertia tensors, forces, moments, and pause flags. -- **Runtime**: `updatePhysicsSystem` accumulates forces/moments, applies gravity/drag, and integrates with Runge–Kutta. Collision/contact resolution is intentionally minimal today—prime territory for contributions. -- **Steering & Animation**: SteeringSystem and AnimationSystem run alongside physics to drive motion and skeletal playback. - -## Scripting (USC) -- **Data**: `ScriptComponent` holds one or more scripts plus file paths (backward compatible with single-script scenes). -- **Runtime**: `USCSystem` ticks the `USCInterpreter` each frame. Actions are registered in `USCScripting.swift` (math helpers, etc.). Extend the DSL by adding actions to `USCActionRegistry`. - -## Scenes, Assets, Resources -- **Scene authoring**: Swift DSL in `Scenes/Builder/` (`Node`/`SceneBuilder`) plus `SceneSerializer` for persistence. -- **Meshes/animations**: abstractions in `Mesh/` and `Skeleton`; resources created through MTK allocators/loaders. -- **Bundled bits**: Prebuilt metallibs per platform and demo resources under `Resources/`. - -## Platform Layers -- **XR (visionOS)**: `UntoldEngineXR` ties CompositorServices/ARKit to the core renderer entry. -- **AR (iOS)**: `UntoldEngineAR` wraps MTKView + ARKit for AR mode. -- **Sample**: `Sources/DemoGame` shows system registration and simple gameplay loops. - -## Where Contributors Can Make Impact -- **Collision/contact & determinism**: Build the missing collision stack, material/friction models, and deterministic stepping for netcode/replays. -- **Render passes/pipelines**: Add or refine passes in the render graph (effects, optimizations, new pipelines). -- **USC actions & API surface**: Expose new engine capabilities to scripts; add higher-level gameplay helpers. -- **Debug/observability**: Better logging sinks, on-screen overlays, profiling, and error surfacing. -- **New systems/components**: AI utilities, networking hooks, gameplay-specific data—keep components data-only and register them for serialization where relevant. - -When proposing big changes, consider ECS storage impact, system ordering, and cross-platform Metal constraints. Align early with maintainers for collision/determinism/netcode-sized work so we keep the architecture cohesive. diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/02-Architecture/Overview.md b/website/versioned_docs/version-0.10.6/04-Engine Development/02-Architecture/Overview.md deleted file mode 100644 index ae33b9cd6..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/02-Architecture/Overview.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -id: engine-architecture overview -title: Architecture Overview -sidebar_position: 1 ---- - -# Engine Architecture - -This document describes the **high-level architecture** of Untold Engine. - -It is intended for developers who want to understand how the engine is structured *before* working on individual subsystems such as rendering, physics, or scripting. - -This page focuses on **concepts and responsibilities**, not file layouts or implementation details. - ---- - -## Architectural Philosophy - -Untold Engine is designed with a small set of guiding principles: - -- **Clarity over abstraction** - Systems should be understandable without deep indirection. - -- **Explicit execution order** - Engine behavior is deterministic and visible. - -- **Data-oriented design** - Data and behavior are separated cleanly. - -- **Composable systems** - New systems can be added without destabilizing existing ones. - -The architecture favors *learnability and debuggability* over convenience shortcuts. - ---- - -## High-Level Structure - -At a conceptual level, Untold Engine is organized into the following layers: - -Gameplay (USC Scripts) -↓ -Engine Systems -↓ -Core Runtime -↓ -Platform & Rendering Backends - -Each layer has a single responsibility and communicates downward through well-defined interfaces. - ---- - -## Core Runtime - -The **core runtime** is responsible for: - -- Entity and component storage -- System registration and ordering -- Scene lifecycle management -- Global engine state -- Frame orchestration - -The runtime does **not** contain gameplay logic, editor logic, or platform-specific behavior. - -Its role is to provide a **stable execution environment** for all systems. - ---- - -## Entity–Component–System (ECS) - -Untold Engine uses an **Entity–Component–System (ECS)** architecture. - -### Entities -- Lightweight identifiers -- Contain no data or behavior -- Used to associate components - -### Components -- Plain data containers -- Represent state (transform, physics, rendering, scripting, etc.) -- Do not contain logic - -### Systems -- Contain all behavior -- Operate on entities matching specific component sets -- Are executed in an explicit order - -This separation keeps systems focused and minimizes hidden dependencies. - ---- - -## System Execution Model - -Systems are executed as part of a well-defined update flow. - -A simplified frame looks like this: - -1. Input collection -2. Simulation and gameplay systems -3. USC script evaluation -4. Physics integration -5. Culling and render preparation -6. Rendering and presentation - -There is no implicit or callback-driven execution. - -Each system declares: -- When it runs -- What data it reads -- What data it writes - ---- - -## Rendering Architecture - -Rendering in Untold Engine is **explicit and staged**. - -Key characteristics: -- Clear separation between simulation and rendering -- Explicit render passes -- Minimal global render state -- Platform-specific backends hidden behind a thin abstraction - -Rendering is treated as a system, not a special case. - ---- - -## Platform Abstraction - -Untold Engine supports multiple platforms through **thin platform layers**. - -These layers handle: -- Window and surface management -- Input sources -- Graphics API integration -- Platform lifecycle events - -The core engine remains platform-agnostic. - ---- - -## USC Integration - -USC (Untold Script Core) is layered on top of the engine. - -- The engine owns the update loop -- USC expresses gameplay intent -- Scripts interact with engine state through a constrained API - -USC does not bypass engine systems. - -This ensures predictable behavior and keeps ownership of state clear. - ---- - -## Editor Relationship - -The Untold Editor is built **on top of the same engine runtime** used by games. - -- Editor features are implemented as engine clients -- Editor-only systems are isolated -- Play mode uses the same execution path as runtime builds - -This minimizes editor-only behavior and keeps debugging consistent. - ---- - -## Design Tradeoffs - -Untold Engine intentionally avoids: - -- Hidden execution order -- Large global managers -- Overly generic abstractions -- Implicit engine magic - -These tradeoffs prioritize: -- Predictability -- Debuggability -- Long-term maintainability - ---- - -## Architecture in Practice - -This document provides the conceptual overview. - -For implementation-level details, see: - -Engine Development → Architecture → Engine internals - diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/02-Architecture/_category.json b/website/versioned_docs/version-0.10.6/04-Engine Development/02-Architecture/_category.json deleted file mode 100644 index b32af10ff..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/02-Architecture/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Architecture", "position": 2, "collapsed": false } diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingAsyncLoading.md b/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingAsyncLoading.md deleted file mode 100644 index c7312f5fb..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingAsyncLoading.md +++ /dev/null @@ -1,325 +0,0 @@ ---- -id: asyncloadingsystem -title: Async Loading System -sidebar_position: 9 ---- - -# Async USDZ Loading System - Usage Guide - -## Overview - -The async loading system allows you to load USDZ files with many models without blocking the main thread. The engine remains responsive during loading, and you can track progress for UI feedback. - -## Features - -✅ **Non-blocking loading** - Engine continues running while assets load -✅ **Progress tracking** - Monitor loading progress per entity or globally -✅ **Fallback meshes** - Automatically loads a cube if loading fails -✅ **Backward compatible** - Old synchronous APIs still work - ---- - -## Basic Usage - -### Load Entity Mesh Asynchronously - -```swift -// Create entity -let entityId = createEntity() - -// Load mesh asynchronously (fire and forget) -setEntityMeshAsync( - entityId: entityId, - filename: "large_model", - withExtension: "usdz" -) - -// Engine continues running - mesh appears when loaded -``` - -### With Completion Callback - -```swift -let entityId = createEntity() - -setEntityMeshAsync( - entityId: entityId, - filename: "large_model", - withExtension: "usdz" -) { success in - if success { - print("✅ Model loaded successfully!") - } else { - print("❌ Failed to load - fallback cube used") - } -} -``` - -### Load Entire Scene Asynchronously - -```swift -loadSceneAsync( - filename: "complex_scene", - withExtension: "usdz" -) { success in - if success { - print("Scene loaded with all entities") - } -} -``` - ---- - -## Progress Tracking - -### Check if Assets are Loading - -```swift -Task { - let isLoading = await AssetLoadingState.shared.isLoadingAny() - if isLoading { - print("Assets are currently loading...") - } -} -``` - -### Get Loading Count - -```swift -Task { - let count = await AssetLoadingState.shared.loadingCount() - print("Loading \(count) entities") -} -``` - -### Get Detailed Progress - -```swift -Task { - let (current, total) = await AssetLoadingState.shared.totalProgress() - let percentage = Float(current) / Float(total) * 100 - print("Progress: \(percentage)% (\(current)/\(total) meshes)") -} -``` - -### Get Progress Summary String - -```swift -Task { - let summary = await AssetLoadingState.shared.loadingSummary() - print(summary) // "Loading large_model.usdz: 5/20 meshes" -} -``` - -### Track Specific Entity - -```swift -Task { - if let progress = await AssetLoadingState.shared.getProgress(for: entityId) { - print("\(progress.filename): \(progress.currentMesh)/\(progress.totalMeshes)") - print("Percentage: \(progress.percentage * 100)%") - } -} -``` - ---- - -## Untold Editor Integration - -### Display Loading UI - -```swift -// In your editor's update loop or UI -Task { - if await AssetLoadingState.shared.isLoadingAny() { - let summary = await AssetLoadingState.shared.loadingSummary() - // Show loading indicator with summary text - showLoadingIndicator(text: summary) - } else { - // Hide loading indicator - hideLoadingIndicator() - } -} -``` - -### Progress Bar Example - -```swift -Task { - let allProgress = await AssetLoadingState.shared.getAllProgress() - - for progress in allProgress { - let percentage = progress.percentage * 100 - print("📦 \(progress.filename): \(Int(percentage))%") - - // Update UI progress bar - updateProgressBar( - id: progress.entityId, - filename: progress.filename, - percentage: percentage - ) - } -} -``` - ---- - -## Error Handling - -### Automatic Fallback - -When async loading fails, a fallback cube mesh is automatically loaded: - -```swift -setEntityMeshAsync( - entityId: entityId, - filename: "missing_file", - withExtension: "usdz" -) { success in - if !success { - // Entity now has a cube mesh as fallback - print("Loaded fallback cube") - } -} -``` - -### Manual Error Handling - -```swift -setEntityMeshAsync( - entityId: entityId, - filename: "model", - withExtension: "usdz" -) { success in - if !success { - // Handle error - entity has fallback cube - showErrorDialog("Failed to load model.usdz") - - // Optionally destroy entity or retry - // destroyEntity(entityId: entityId) - } -} -``` - ---- - -## Backward Compatibility - -The old synchronous APIs still work unchanged: - -```swift -// Old way (blocks main thread) -setEntityMesh( - entityId: entityId, - filename: "model", - withExtension: "usdz" -) - -// New way (non-blocking) -setEntityMeshAsync( - entityId: entityId, - filename: "model", - withExtension: "usdz" -) -``` - ---- - -## Advanced Usage - -### Loading Multiple Models - -```swift -let entity1 = createEntity() -let entity2 = createEntity() -let entity3 = createEntity() - -// All three load in parallel without blocking -setEntityMeshAsync(entityId: entity1, filename: "model1", withExtension: "usdz") -setEntityMeshAsync(entityId: entity2, filename: "model2", withExtension: "usdz") -setEntityMeshAsync(entityId: entity3, filename: "model3", withExtension: "usdz") - -// Track total progress -Task { - while await AssetLoadingState.shared.isLoadingAny() { - let summary = await AssetLoadingState.shared.loadingSummary() - print(summary) - try? await Task.sleep(nanoseconds: 100_000_000) // 0.1 seconds - } - print("All models loaded!") -} -``` - -### Coordinate System Conversion - -```swift -setEntityMeshAsync( - entityId: entityId, - filename: "blender_model", - withExtension: "usdz", - coordinateConversion: .forceZUpToYUp -) -``` - ---- - -## Implementation Details - -### What Runs on Background Thread - -- Reading USDZ file from disk -- Parsing MDL data structures -- Loading texture data -- Applying coordinate transformations - -### What Runs on Main Thread - -- Creating Metal resources (MTKMesh, MTLTexture) -- Registering ECS components -- Updating entity transforms -- Creating fallback meshes on error - -### Thread Safety - -All `AssetLoadingState` operations are thread-safe via Swift's actor model. You can safely query loading state from any thread. - ---- - -## Performance Tips - -1. **Prefer async for large files** (>20 models or >50MB) -2. **Use sync for small files** (1-5 models, <10MB) - less overhead -3. **Batch loads together** - loading multiple files in parallel is efficient -4. **Monitor progress** - use for UI feedback during long loads - ---- - -## Migration Guide - -### Before (Blocking) - -```swift -func loadGameAssets() { - let entity = createEntity() - setEntityMesh(entityId: entity, filename: "huge_scene", withExtension: "usdz") - // Engine was frozen here ❌ -} -``` - -### After (Non-blocking) - -```swift -func loadGameAssets() { - let entity = createEntity() - setEntityMeshAsync(entityId: entity, filename: "huge_scene", withExtension: "usdz") { - success in - if success { - print("Scene ready!") - } - } - // Engine continues running ✅ -} -``` - ---- - diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingInputSystem.md b/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingInputSystem.md deleted file mode 100644 index 1c73ff69d..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingInputSystem.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -id: inputsystem -title: Input System -sidebar_position: 6 ---- - -# Using the Input System in Untold Engine - -The Input System in the Untold Engine allows you to detect user inputs, such as keystrokes and mouse movements, to control entities and interact with the game. This guide will explain how to use the Input System effectively. - - -## How to Use the Input System (Keyboard) - -### Step 1: Detect Keystrokes - -To detect if a specific key is pressed, use the keyState object from the Input System. - -Example: Detecting the 'W' Key - -```swift -func init(){ -// Make sure that you have enabled keyevents in your init function: -InputSystem.shared.registerKeyboardEvents() -} - -// Then in the handleInput callback, you can do this: - -func handleInput() { - // Skip logic if not in game mode - if gameMode == false { return } - - let inputSystem = InputSystem.shared - - // Handle input here - if inputSystem.keyState.wPressed{ - Logger.log(message: "w pressed") - } -} -``` -You can use the same logic for other keys like A, S, and D: - -```swift -let inputSystem = InputSystem.shared - -if inputSystem.keyState.aPressed == true { - // Move left -} - -if inputSystem.keyState.sPressed == true { - // Move backward -} - -if inputSystem.keyState.dPressed == true { - // Move right -} -``` - -###Step 2: Using Input to Control Entities - -Here’s an example function that moves a car entity based on keyboard inputs: - -```swift -func moveCar(entityId: EntityID, dt: Float) { - - let inputSystem = InputSystem.shared - - // Ensure we are in game mode - if gameMode == false { - return - } - - var position = simd_float3(0.0, 0.0, 0.0) - - // Move forward - if inputSystem.keyState.wPressed == true { - position.z += 1.0 * dt - } - - // Move backward - if inputSystem.keyState.sPressed == true { - position.z -= 1.0 * dt - } - - // Move left - if inputSystem.keyState.aPressed == true { - position.x -= 1.0 * dt - } - - // Move right - if inputSystem.keyState.dPressed == true { - position.x += 1.0 * dt - } - - // Apply the translation to the entity - translateTo(entityId: entityId, position: position) -} -``` - -## How to Use the Input System with a Game Controller - -To detect if a specific button is pressed, use the gameControllerState object from the Input System. - -Example: Detecting the 'A' button - -```swift -func init(){ -// Make sure that you have enabled game controller events in your init function: - InputSystem.shared.registerGameControllerEvents() -} - -// Then in the handleInput callback, you can do this: - -func handleInput() { - // Skip logic if not in game mode - if gameMode == false { return } - let inputSystem = InputSystem.shared - - // Handle input here - if inputSystem.gameControllerState.aPressed { - Logger.log(message: "Pressed A key") - } -} -``` - ---- - -## Tips and Best Practices -- Debouncing: If you want to execute an action only once per key press, track the key's previous state to avoid repeated triggers. -- Game Mode Check: Always ensure the game is in the appropriate mode (e.g., Game Mode) before processing inputs. -- Smooth Movement: Use dt (delta time) to ensure frame-rate-independent movement. - diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingLOD-Batching-Streaming.md b/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingLOD-Batching-Streaming.md deleted file mode 100644 index 44cbcd17e..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingLOD-Batching-Streaming.md +++ /dev/null @@ -1,207 +0,0 @@ ---- -id: lodstaticbatchinstreaminggsystem -title: Lod + Static Batching + Streaming System -sidebar_position: 12 ---- - -# Combining LOD, Batching, and Geometry Streaming - -The Untold Engine provides three complementary systems for optimizing rendering performance: - -- **LOD (Level of Detail)**: Automatically switches between different mesh resolutions based on distance from the camera. Closer objects use high-detail meshes, while distant objects use simplified versions. -- **Static Batching**: Combines multiple static meshes with the same material into a single draw call, reducing CPU overhead. -- **Geometry Streaming**: Dynamically loads and unloads geometry based on proximity to the camera, keeping only nearby objects in memory. - -When used together, these systems provide powerful performance optimization: -- LOD reduces GPU load by rendering appropriate detail levels -- Batching reduces CPU overhead by minimizing draw calls -- Streaming reduces memory usage by only keeping nearby geometry loaded - -Before using the examples below, assume your scene has: -- A valid active camera (used for LOD switching and streaming distance checks) -- Entity transforms in world space before runtime optimization kicks in -- Mesh/material naming consistency across LOD levels (to avoid mismatched assets) -- A clear decision on whether setup happens procedurally at runtime or from a serialized scene file - -In practice, there are two common workflows: -- **Procedural setup**: You create entities and configure LOD/batching/streaming in code during scene initialization. -- **Scene deserialization**: You load a saved scene where components were already authored, then finalize runtime systems in completion callbacks. - -## LOD + Batching - -This combination is ideal for scenes with many static objects that remain loaded throughout the scene lifetime. The LOD system manages visual quality based on distance, while batching reduces draw calls for objects using the same material. - -**Key Points:** -- All entities must have their meshes loaded before calling `generateBatches()` -- Use the completion callback from `addLODLevels()` to ensure meshes are ready -- Enable batching and generate batches only after all entities are configured - -Why this order matters: batching relies on mesh/material data that is only guaranteed after async LOD loading completes. If `generateBatches()` runs too early, some entities may be missing from batches, causing inconsistent draw-call reduction. - -```swift -private func setupLODWithBatching() { - var loadedCount = 0 - let totalTrees = 20 - - for i in 0 ..< totalTrees { - let tree = createEntity() - setEntityName(entityId: tree, name: "Tree_\(i)") - - // Capture position values for the closure - let x = Float(i % 5) * 10.0 - let z = Float(i / 5) * 10.0 - - setEntityLodComponent(entityId: tree) - - addLODLevels(entityId: tree, levels: [ - (0, "tree_LOD0", "usdz", 50.0, 0.0), - (1, "tree_LOD1", "usdz", 100.0, 0.0), - (2, "tree_LOD2", "usdz", 200.0, 0.0), - ]) { success in - if success { - // Apply transform AFTER mesh is loaded - translateTo(entityId: tree, position: simd_float3(x, 0, z)) - setEntityStaticBatchComponent(entityId: tree) - } - - // Track completion - loadedCount += 1 - if loadedCount == totalTrees { - enableBatching(true) - generateBatches() - print("\(totalTrees) trees configured with LOD + Batching") - } - } - } - -} -``` - -For LOD + batching loaded from a scene file, the key is to defer batch generation until playSceneAt reports completion (after async mesh loading finishes): - -```swift -playSceneAt(url:url){ - enableBatching(true) - generateBatches() -} -``` - -## LOD + Batching + Streaming - -This combination is ideal for large open-world scenes where you have many objects spread across a large area. Streaming ensures only nearby geometry is loaded into memory, LOD manages visual quality, and batching reduces draw calls for loaded objects. - -**Key Points:** -- Enable streaming **after** the mesh is loaded and positioned -- Set `streamingRadius` larger than your farthest LOD distance to ensure smooth transitions -- Set `unloadRadius` larger than `streamingRadius` to provide a buffer zone -- Batching works with streamed geometry - batches are automatically updated as objects load/unload -- The order matters: LOD component → Load meshes → Transform → Enable streaming → Mark for batching - -This setup is best treated as a lifecycle: -1. Author LOD levels and streaming radii based on gameplay visibility needs. -2. Wait for asset load completion before enabling dependent systems. -3. Activate batching once entities are fully initialized, then let streaming maintain runtime memory pressure. - -**Radius Guidelines:** -- `streamingRadius`: Should be greater than your farthest LOD distance plus a buffer (e.g., if farthest LOD is 200, use 250) -- `unloadRadius`: Should be significantly larger than `streamingRadius` to avoid thrashing (e.g., 350 when streaming radius is 250) - -```swift -private func setupLODBatchingStreaming() { - var loadedCount = 0 - let totalTrees = 20 - - for i in 0 ..< totalTrees { - let tree = createEntity() - setEntityName(entityId: tree, name: "Tree_\(i)") - - // Capture position values for the closure - let x = Float(i % 5) * 10.0 - let z = Float(i / 5) * 10.0 - - // 1. Set LOD component FIRST - setEntityLodComponent(entityId: tree) - - // 2. Load LOD levels - addLODLevels(entityId: tree, levels: [ - (0, "tree_LOD0", "usdz", 50.0, 0.0), - (1, "tree_LOD1", "usdz", 100.0, 0.0), - (2, "tree_LOD2", "usdz", 200.0, 0.0), - (3, "tree_LOD3", "usdz", 300.0, 0.0), - ]) { success in - if success { - // 3. Apply transform AFTER mesh is loaded - translateTo(entityId: tree, position: simd_float3(x, 0, z)) - - // 4. Enable streaming AFTER mesh exists - // streamingRadius > farthest LOD distance (200) with buffer - // unloadRadius > streamingRadius with buffer - enableStreaming( - entityId: tree, - streamingRadius: 250.0, // Load when within 250 units - unloadRadius: 350.0, // Unload when beyond 350 units - priority: 10 - ) - - // 5. Mark for batching - setEntityStaticBatchComponent(entityId: tree) - } - - // Track completion - loadedCount += 1 - if loadedCount == totalTrees { - enableBatching(true) - generateBatches() - print("\(totalTrees) trees configured with LOD + Batching + Streaming") - } - } - } -} -``` - -When loading LOD + batching + streaming from a scene file, deserialization restores component state first, then your completion handler enables systems that depend on fully loaded geometry: - -```swift -playSceneAt(url: sceneURL) { - // Called after deserializeScene completion (async LOD/mesh loads done) - - enableBatching(true) - generateBatches() - - GeometryStreamingSystem.shared.enabled = true - print("Scene loaded with LOD + Batching + Streaming enabled") -} -``` - -## Best Practices - -### When to Use Each Combination - -**LOD Only:** -- Small scenes with few objects -- When objects are always visible and memory isn't a concern -- Dynamic objects that move frequently - -**LOD + Batching:** -- Medium-sized scenes with many static objects -- Objects share materials and remain loaded -- Memory usage is acceptable -- Example: Interior spaces, small outdoor areas - -**LOD + Batching + Streaming:** -- Large open-world scenes -- Many objects spread across large distances -- Memory optimization is critical -- Example: Forests, cities, large outdoor environments - -### Performance Tips - -1. **LOD Distances**: Set LOD transition distances based on your object size and visual importance. Smaller objects can transition earlier. - -2. **Batching Materials**: Entities must share the same material to be batched together. Group objects by material when possible. - -3. **Streaming Priorities**: Use higher priority values (e.g., 10) for important objects like landmarks, lower values (e.g., 1) for background details. - -4. **Testing**: Monitor frame rate and memory usage to fine-tune your radius values and LOD distances for your specific scene. - -5. **Completion Callbacks**: Always use the completion callback from `addLODLevels()` to ensure meshes are fully loaded before enabling other systems. diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingLODSystem.md b/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingLODSystem.md deleted file mode 100644 index 7af29fed7..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingLODSystem.md +++ /dev/null @@ -1,466 +0,0 @@ ---- -id: lodsystem -title: LOD System -sidebar_position: 10 ---- - -# LOD (Level of Detail) System - Usage Guide - -The Untold Engine provides a flexible LOD system for optimizing rendering performance by displaying different mesh details based on camera distance. - -## Overview - -The LOD system allows you to: -- Add multiple levels of detail to any entity -- Automatically switch between LOD levels based on distance -- Customize distance thresholds for each LOD level -- Configure LOD behavior (bias, hysteresis, fade transitions) - -> **Choose Your Path:** You can set up LOD via the **Editor** (no code required) or **programmatically** in Swift. - ---- - -## Using the Editor - -### Adding LOD Support to an Entity - -1. **Select your entity** in the Scene Hierarchy -2. **Open the Inspector** and click **"Add Components"** -3. **Select "LOD Component"** from the list -4. An LOD Levels panel will appear in the Inspector - -### Adding LOD Levels - -1. **Select a model** from the Asset Browser (Models folder) -2. In the LOD Levels panel, click **"Add LOD Level"** -3. The selected model will be added as the next LOD level with a default distance: - - LOD0: 50 units - - LOD1: 100 units - - LOD2: 150 units, etc. - -### Adjusting LOD Distances - -1. Click the **distance value** next to any LOD level -2. Enter a new distance and press **Enter** -3. Objects will switch to this LOD when the camera is within this distance - -### Removing LOD Levels - -Click the **trash icon** next to any LOD level to remove it. - ---- - -## Using Code - -### Quick Start - -### Basic LOD Setup - -```swift -// Create entity -let tree = createEntity() - -// Add LOD component -setEntityLodComponent(entityId: tree) - -// Add LOD levels (from highest to lowest detail) -addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "usdz", maxDistance: 50.0) -addLODLevel(entityId: tree, lodIndex: 1, fileName: "tree_LOD1", withExtension: "usdz", maxDistance: 100.0) -addLODLevel(entityId: tree, lodIndex: 2, fileName: "tree_LOD2", withExtension: "usdz", maxDistance: 200.0) -addLODLevel(entityId: tree, lodIndex: 3, fileName: "tree_LOD3", withExtension: "usdz", maxDistance: 400.0) -``` - -**How it works:** -- LOD0 (highest detail) renders when camera is < 50 units away -- LOD1 renders between 50-100 units -- LOD2 renders between 100-200 units -- LOD3 (lowest detail) renders beyond 200 units - -### Loading Multiple LOD Levels (Recommended) - -Use `addLODLevels` to load all LOD levels with a single completion handler. This is especially important when combining LOD with static batching: - -```swift -let tree = createEntity() -setEntityLodComponent(entityId: tree) - -// Load all LOD levels and wait for completion -addLODLevels(entityId: tree, levels: [ - (0, "tree_LOD0", "usdz", 50.0, 0.0), - (1, "tree_LOD1", "usdz", 100.0, 0.0), - (2, "tree_LOD2", "usdz", 200.0, 0.0), - (3, "tree_LOD3", "usdz", 400.0, 0.0) -]) { success in - if success { - print("All LOD levels loaded") - - // Apply transforms AFTER mesh is loaded - translateTo(entityId: tree, position: simd_float3(10, 0, 5)) - - // Apply static batching if necessary - } -} -``` - -> **Important:** When using LOD with async loading, apply transforms (`translateTo`, `rotateTo`, `scaleTo`) inside the completion handler. Transforms applied before the mesh loads may not take effect. - -### With Initial Mesh Loading - -You can also load an initial mesh synchronously before adding LOD levels: - -```swift -let tree = createEntity() - -// Load initial mesh synchronously (shows immediately) -setEntityMesh(entityId: tree, filename: "tree_LOD0", withExtension: "usdz") - -// Add LOD component -setEntityLodComponent(entityId: tree) - -// Add LOD levels (will replace initial mesh when ready) -addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "usdz", maxDistance: 50.0) -addLODLevel(entityId: tree, lodIndex: 1, fileName: "tree_LOD1", withExtension: "usdz", maxDistance: 100.0) -addLODLevel(entityId: tree, lodIndex: 2, fileName: "tree_LOD2", withExtension: "usdz", maxDistance: 200.0) -addLODLevel(entityId: tree, lodIndex: 3, fileName: "tree_LOD3", withExtension: "usdz", maxDistance: 400.0) -``` - -### With Completion Handler - -Since `addLODLevel` loads meshes asynchronously, use the completion handler when you need to perform actions after loading completes: - -```swift -let tree = createEntity() -setEntityLodComponent(entityId: tree) - -// Chain completion handlers for sequential loading -addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "usdz", maxDistance: 50.0) { success in - if success { - print("LOD0 loaded") - // Now it's safe to use the mesh data - } -} -``` - -## File Organization - -LOD files should be organized in subdirectories: - -``` -GameData/ -└── Models/ - ├── tree_LOD0/ - │ └── tree_LOD0.usdz - ├── tree_LOD1/ - │ └── tree_LOD1.usdz - ├── tree_LOD2/ - │ └── tree_LOD2.usdz - └── tree_LOD3/ - └── tree_LOD3.usdz -``` - -**Note:** Each LOD file should be in its own folder with the same name as the file (without extension). - -## API Reference - -### Core Functions - -#### `setEntityLodComponent(entityId:)` -Registers an LOD component on an entity. Call this before adding LOD levels. - -```swift -setEntityLodComponent(entityId: tree) -``` - -#### `addLODLevel(entityId:lodIndex:fileName:withExtension:maxDistance:completion:)` -Adds a single LOD level to an entity. - -**Parameters:** -- `entityId`: The entity to add LOD to -- `lodIndex`: LOD level index (0 = highest detail) -- `fileName`: Name of the mesh file (without extension) -- `withExtension`: File extension (e.g., "usdz") -- `maxDistance`: Maximum camera distance for this LOD -- `completion`: Optional callback when loading completes - -```swift -addLODLevel( - entityId: tree, - lodIndex: 0, - fileName: "tree_LOD0", - withExtension: "usdz", - maxDistance: 50.0 -) { success in - if success { - print("LOD0 loaded successfully") - } -} -``` - -#### `addLODLevels(entityId:levels:completion:)` -Adds multiple LOD levels with a single completion handler. Useful when you need to wait for all LOD levels to load. - -**Parameters:** -- `entityId`: The entity to add LOD levels to -- `levels`: Array of tuples: `(lodIndex, fileName, withExtension, maxDistance, screenPercentage)` -- `completion`: Called when ALL levels finish loading (true only if all succeeded) - -```swift -addLODLevels(entityId: tree, levels: [ - (0, "tree_LOD0", "usdz", 50.0, 0.0), - (1, "tree_LOD1", "usdz", 100.0, 0.0), - (2, "tree_LOD2", "usdz", 200.0, 0.0) -]) { success in - if success { - print("All LODs loaded") - } -} -``` - -#### `removeLODLevel(entityId:lodIndex:)` -Removes a specific LOD level from an entity. - -```swift -removeLODLevel(entityId: tree, lodIndex: 2) -``` - -#### `replaceLODLevel(entityId:lodIndex:fileName:withExtension:maxDistance:completion:)` -Replaces an existing LOD level with a new mesh. - -```swift -replaceLODLevel( - entityId: tree, - lodIndex: 1, - fileName: "tree_LOD1_new", - withExtension: "usdz", - maxDistance: 100.0 -) -``` - -#### `getLODLevelCount(entityId:) -> Int` -Returns the number of LOD levels for an entity. - -```swift -let count = getLODLevelCount(entityId: tree) -print("Entity has \(count) LOD levels") -``` - ---- - -## Advanced Usage - -### Custom Distance Thresholds - -Adjust distances based on your scene scale: - -```swift -// Small scene (indoor environment) -addLODLevel(entityId: prop, lodIndex: 0, fileName: "prop_LOD0", withExtension: "usdz", maxDistance: 10.0) -addLODLevel(entityId: prop, lodIndex: 1, fileName: "prop_LOD1", withExtension: "usdz", maxDistance: 20.0) - -// Large scene (outdoor landscape) -addLODLevel(entityId: mountain, lodIndex: 0, fileName: "mountain_LOD0", withExtension: "usdz", maxDistance: 500.0) -addLODLevel(entityId: mountain, lodIndex: 1, fileName: "mountain_LOD1", withExtension: "usdz", maxDistance: 1000.0) -addLODLevel(entityId: mountain, lodIndex: 2, fileName: "mountain_LOD2", withExtension: "usdz", maxDistance: 2000.0) -``` - -### LOD Configuration - -Configure global LOD behavior: - -```swift -// Adjust LOD bias (higher = switch to lower detail sooner) -LODConfig.shared.lodBias = 1.5 // Performance mode -LODConfig.shared.lodBias = 0.75 // Quality mode - -// Adjust hysteresis to prevent flickering -LODConfig.shared.hysteresis = 10.0 - -// Enable fade transitions between LODs - Not yet implemented -LODConfig.shared.enableFadeTransitions = true -LODConfig.shared.fadeTransitionTime = 0.5 // seconds -``` - -### Forced LOD Override - -Force a specific LOD level (useful for debugging): - -```swift -if let lodComponent = scene.get(component: LODComponent.self, for: tree) { - lodComponent.forcedLOD = 2 // Always show LOD2 - // lodComponent.forcedLOD = nil // Resume automatic LOD selection -} -``` - -### Programmatic LOD Management - -```swift -// Create entity with LOD component -let rock = createEntity() -setEntityLodComponent(entityId: rock) - -// Add LODs dynamically based on performance -let lodFiles = ["rock_LOD0", "rock_LOD1", "rock_LOD2"] -let distances: [Float] = [30.0, 60.0, 120.0] - -for (index, fileName) in lodFiles.enumerated() { - addLODLevel( - entityId: rock, - lodIndex: index, - fileName: fileName, - withExtension: "usdz", - maxDistance: distances[index] - ) -} - -// Check LOD count -let lodCount = getLODLevelCount(entityId: rock) -print("Rock has \(lodCount) LOD levels") - -// Remove highest detail LOD on low-end hardware -if isLowEndDevice { - removeLODLevel(entityId: rock, lodIndex: 0) -} -``` - ---- - -## Best Practices - -### Recommended LOD Counts -- **Small props**: 2-3 LODs -- **Characters**: 3-4 LODs -- **Vehicles**: 3-4 LODs -- **Buildings**: 4-5 LODs -- **Terrain**: 5-8 LODs - -### Polygon Reduction Guidelines -- **LOD0** (full detail): 100% polygons -- **LOD1**: ~50% polygon reduction -- **LOD2**: ~75% polygon reduction -- **LOD3**: ~90% polygon reduction or billboard - -### Distance Thresholds -Base distances on object importance and size: -- **Hero objects**: Longer high-detail distance -- **Background objects**: Shorter high-detail distance -- **Large objects**: Visible from farther away, need more LODs - -### Performance Tips -1. Always use async loading (`setEntityMeshAsync`) for better performance -2. Keep LOD0 for objects within 50 units of camera -3. Use billboards or impostors for very distant objects (LOD3+) -4. Test LOD transitions in-game to ensure smooth visual quality -5. Use `forcedLOD` during development to preview each LOD level - -## Troubleshooting - -### LODs Not Switching -- Verify LOD component is registered: `hasComponent(entityId: tree, componentType: LODComponent.self)` -- Check distance thresholds are set correctly -- Ensure camera has `CameraComponent` and is active - -### Visual Popping Between LODs -- Increase `LODConfig.shared.hysteresis` value -- Enable fade transitions: `LODConfig.shared.enableFadeTransitions = true` - not yet implemented -- Adjust LOD bias for smoother transitions - -### File Not Found Errors -- Verify file organization follows the subdirectory structure -- Check file names match exactly (case-sensitive) -- Ensure files are in the correct `GameData/Models/` path - -### Performance Issues -- Reduce number of LOD levels for less important objects -- Increase distance thresholds to switch LODs sooner -- Use LOD bias > 1.0 for performance mode - -## Example: Complete LOD Setup - -```swift -import UntoldEngine - -// Create multiple trees with LODs -var trees: [EntityID] = [] - -for i in 0..<10 { - let tree = createEntity() - setEntityName(entityId: tree, name: "Tree_\(i)") - - // Position trees - translateTo(entityId: tree, position: simd_float3(Float(i * 10), 0, 0)) - - // Add LOD component - setEntityLodComponent(entityId: tree) - - // Add 4 LOD levels - addLODLevel(entityId: tree, lodIndex: 0, fileName: "tree_LOD0", withExtension: "usdz", maxDistance: 50.0) - addLODLevel(entityId: tree, lodIndex: 1, fileName: "tree_LOD1", withExtension: "usdz", maxDistance: 100.0) - addLODLevel(entityId: tree, lodIndex: 2, fileName: "tree_LOD2", withExtension: "usdz", maxDistance: 200.0) - addLODLevel(entityId: tree, lodIndex: 3, fileName: "tree_LOD3", withExtension: "usdz", maxDistance: 400.0) - - trees.append(tree) -} - -// Configure LOD system for this scene -LODConfig.shared.lodBias = 1.2 // Slightly favor performance -LODConfig.shared.hysteresis = 8.0 // Prevent flickering -LODConfig.shared.enableFadeTransitions = true - -print("Created \(trees.count) trees with LOD support") -``` - -## Example: LOD with Static Batching - -When combining LOD with static batching, ensure transforms and batching setup happen **after** meshes are loaded: - -```swift -import UntoldEngine - -private func setupLODWithBatching() { - var loadedCount = 0 - let totalTrees = 20 - - for i in 0.. **Choose Your Path:** You can set up Static Batching via the **Editor** (no code required) or **programmatically** in Swift. - ---- - -## Using the Editor - -### Step 1: Mark Entities as Static - -1. **Select an entity** with a Render Component in the Scene Hierarchy -2. In the **Inspector**, find the **"Static Batching"** section -3. Toggle **"Mark as Static"** (or "Mark Children as Static" for parent entities) - -### Step 2: Enable the Batching System - -1. Open the **Static Batching panel** in the editor sidebar -2. Toggle **"Enable Batching"** to ON - -### Step 3: Generate Batches - -1. Click **"Generate Batches"** -2. A success message will appear -3. The **Active Batches** count shows how many batch groups were created - -### Managing Batches - -- **Clear Batches**: Removes all generated batches -- **Regenerate**: Click "Generate Batches" again after marking new entities - -### Important Notes - -- **Moving a static entity** automatically removes it from batching and regenerates batches -- Batches are grouped by **material** — objects with the same material are combined -- You can mark/unmark entities as static at any time, then regenerate - ---- - -## Using Code - -### Quick Start - -### Basic Static Batching Setup - -```swift -// Create entities -let cube1 = createEntity() -setEntityMesh(entityId: cube1, filename: "cube", withExtension: "usdz") -translateTo(entityId: cube1, position: simd_float3(0, 0, 0)) - -let cube2 = createEntity() -setEntityMesh(entityId: cube2, filename: "cube", withExtension: "usdz") -translateTo(entityId: cube2, position: simd_float3(2, 0, 0)) - -let cube3 = createEntity() -setEntityMesh(entityId: cube3, filename: "cube", withExtension: "usdz") -translateTo(entityId: cube3, position: simd_float3(4, 0, 0)) - -// Mark entities as static -setEntityStaticBatchComponent(entityId: cube1) -setEntityStaticBatchComponent(entityId: cube2) -setEntityStaticBatchComponent(entityId: cube3) - -// Enable batching and generate batches -enableBatching(true) -generateBatches() -``` - -**How it works:** -- Static entities are marked for batching -- `generateBatches()` combines entities with the same material into batch groups -- Rendering system uses batched draw calls instead of per-entity calls - -### With Async Mesh Loading (Recommended) - -For better performance, use async loading and enable batching in the completion handler: - -```swift -let stadium = createEntity() -setEntityMeshAsync(entityId: stadium, filename: "stadium", withExtension: "usdz") { success in - if success { - print("Scene loaded successfully") - - // Mark as static AFTER mesh is loaded - setEntityStaticBatchComponent(entityId: stadium) - - // Enable batching system - enableBatching(true) - - // Generate batches - generateBatches() - } -} -``` - -**Important:** Always call `setEntityStaticBatchComponent()` **after** the mesh loads successfully, then enable and generate batches. - -### Multi-Mesh Assets (USDZ with Multiple Objects) - -For USDZ files with multiple meshes (like a building with walls, roof, windows): - -```swift -let building = createEntity() -setEntityMeshAsync(entityId: building, filename: "office_building", withExtension: "usdz") { success in - if success { - // Mark parent entity - automatically marks all children as static - setEntityStaticBatchComponent(entityId: building) - - enableBatching(true) - generateBatches() - } -} -``` - -**How it works:** `setEntityStaticBatchComponent()` recursively marks the parent and all children, so the entire building is batched. - -## API Reference - -### Core Functions - -#### `setEntityStaticBatchComponent(entityId:)` -Marks an entity (and all its children) as static for batching. - -```swift -setEntityStaticBatchComponent(entityId: entity) -``` - -**Note:** Entity must have a `RenderComponent` (i.e., mesh must be loaded). - -#### `removeEntityStaticBatchComponent(entityId:)` -Removes static batching from an entity (and all its children). - -```swift -removeEntityStaticBatchComponent(entityId: entity) -``` - -**Use case:** If you need to move a previously static object. - -#### `enableBatching(_:)` -Globally enables or disables the batching system. - -```swift -enableBatching(true) // Enable batching -enableBatching(false) // Disable batching -``` - -#### `isBatchingEnabled() -> Bool` -Checks if batching is currently enabled. - -```swift -if isBatchingEnabled() { - print("Batching is active") -} -``` - -#### `generateBatches()` -Generates batch groups from all entities marked as static. - -```swift -generateBatches() -``` - -**Important:** Call this after marking entities as static and enabling batching. - -#### `clearSceneBatches()` -Clears all generated batches. - -```swift -clearSceneBatches() -``` - -**Use case:** When loading a new scene or reconfiguring static geometry. - -## Complete Workflow Examples - -### Example 1: Multiple Static Objects - -```swift -import UntoldEngine - -// Create multiple static props -var props: [EntityID] = [] - -for i in 0..<50 { - let rock = createEntity() - setEntityName(entityId: rock, name: "Rock_\(i)") - - // Load mesh - setEntityMesh(entityId: rock, filename: "rock", withExtension: "usdz") - - // Position randomly - let x = Float.random(in: -20...20) - let z = Float.random(in: -20...20) - translateTo(entityId: rock, position: simd_float3(x, 0, z)) - - // Mark as static - setEntityStaticBatchComponent(entityId: rock) - - props.append(rock) -} - -// Enable and generate batches -enableBatching(true) -generateBatches() - -print("Batched \(props.count) rocks") -``` - -### Example 2: Scene Loading with Batching - -```swift -// Load scene from file -if let sceneData = loadGameScene(from: sceneURL) { - deserializeScene(sceneData: sceneData) - - // Scene automatically restores StaticBatchComponent for marked entities - // Enable batching and generate - enableBatching(true) - generateBatches() - - print("Scene loaded with batching enabled") -} -``` - -### Example 3: Dynamic Scene with Mixed Objects - -```swift -// Static environment -let ground = createEntity() -setEntityMesh(entityId: ground, filename: "ground_plane", withExtension: "usdz") -setEntityStaticBatchComponent(entityId: ground) - -let walls = createEntity() -setEntityMesh(entityId: walls, filename: "walls", withExtension: "usdz") -setEntityStaticBatchComponent(entityId: walls) - -// Dynamic objects (NOT marked as static) -let player = createEntity() -setEntityMesh(entityId: player, filename: "character", withExtension: "usdz") -// Do NOT call setEntityStaticBatchComponent for moving objects - -let enemy = createEntity() -setEntityMesh(entityId: enemy, filename: "enemy", withExtension: "usdz") -// Enemies move, so no static batching - -// Enable batching (only affects static entities) -enableBatching(true) -generateBatches() -``` - -### Example 4: Large Async Scene Loading - -```swift -let cityBlock = createEntity() -setEntityMeshAsync(entityId: cityBlock, filename: "city_block", withExtension: "usdz") { success in - if success { - print("City block loaded with all buildings") - - // Mark entire hierarchy as static - setEntityStaticBatchComponent(entityId: cityBlock) - - // Enable batching system - enableBatching(true) - - // Generate batches - generateBatches() - - print("Static batching enabled - draw calls optimized") - } else { - print("Failed to load city block") - } -} -``` ---- - -## Best Practices - -### What to Mark as Static -✅ **Good candidates:** -- Environment geometry (walls, floors, ceilings) -- Props that never move (rocks, trees, furniture) -- Buildings and structures -- Terrain meshes -- Static decorations - -❌ **Bad candidates:** -- Characters and NPCs -- Vehicles -- Projectiles -- Animated objects -- UI elements - -### Batching Requirements -For entities to batch together, they must have: -- ✅ Same material (textures, colors) -- ✅ `StaticBatchComponent` marked -- ✅ Valid `RenderComponent` (mesh loaded) - -Entities with different materials will be in separate batch groups. - -### Performance Tips - -1. **Mark entities AFTER mesh loading:** - ```swift - setEntityMeshAsync(...) { success in - setEntityStaticBatchComponent(entityId: entity) // ✅ Correct timing - } - ``` - -2. **Enable batching once per scene:** - ```swift - // Game initialization or scene load - enableBatching(true) - generateBatches() - ``` - -3. **Group entities by material:** - - Entities with the same material batch better - - Reduce material variations for better batching - -4. **Regenerate batches when needed:** - ```swift - // When adding/removing static entities - clearSceneBatches() - generateBatches() - ``` - -## Limitations - -- **No dynamic batching:** Only works for static geometry -- **Transform baked:** Entity positions are baked into batch geometry -- **Material grouping:** Different materials create separate batches -- **No skeletal meshes:** Animated/skinned meshes cannot be batched - diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingSteeringSystem.md b/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingSteeringSystem.md deleted file mode 100644 index 05c8465be..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/UsingSteeringSystem.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -id: steeringsystem -title: Steering System -sidebar_position: 7 ---- - -# Using the Steering System in Untold Engine - -The Steering System in the Untold Engine enables entities to move dynamically and intelligently within the scene. It provides both low-level steering behaviors (e.g., seek, flee, arrive) for granular control and high-level behaviors (e.g., steerTo, steerAway, followPath) that integrate seamlessly with the Physics System. - -## Why Use the Steering System? - -The Steering System is essential for creating dynamic and realistic movement for entities, such as: - -- A character chasing a target. -- An enemy avoiding obstacles. -- A vehicle following a predefined path. - -The high-level behaviors are recommended because they are designed to work closely with the Physics System, simplifying implementation while maintaining smooth motion. - ---- - -## How to Use the Steering System - -Examples: - -1. Steer Toward a Target Position: - -```swift -steerSeek(entityId: entity, targetPosition: targetPosition, maxSpeed: 5.0, deltaTime: 0.016) -``` -2. Steer Away from a Threat: - -```swift -steerFlee(entityId: entity, threatPosition: threatPosition, maxSpeed: 5.0, deltaTime: 0.016) -``` - -3. Follow a Path: Guide an entity along a series of waypoints. - -```swift -steerFollowPath(entityId: entity, path: waypoints, maxSpeed: 5.0, deltaTime: 0.016) -``` -4. Pursue a Moving Target: - -```swift -steerPursuit(entityId: chaserEntity, targetEntity: targetEntity, maxSpeed: 5.0, deltaTime: 0.016) -``` - -5. Avoid Obstacles: - -```swift -steerAvoidObstacles(entityId: entity, obstacles: obstacleEntities, avoidanceRadius: 2.0, maxSpeed: 5.0, deltaTime: 0.016) -``` - -6. Steer Toward a Target Position (with Arrive): - -```swift -steerArrive(entityId: entity, targetPosition: targetPosition, maxSpeed: 5.0, deltaTime: 0.016) -``` - -7. Steer using WASD keys - -```swift -steerWithWASD(entityId: entity, maxSpeed: 5.0, deltaTime: 0.016) -``` - ---- - -## What Happens Behind the Scenes? - -1. Low-Level Behaviors: -- Calculate desired velocity based on the target or threat position. -- Generate steering forces by comparing desired velocity with current velocity. -2. High-Level Behaviors: -- Use low-level behaviors to calculate steering adjustments. -- Apply these forces to the entity’s physics system for smooth, realistic motion. -- Align the entity’s orientation to face its movement direction. -3. Physics Integration: -- Forces are applied through the Physics System, ensuring that movement respects mass, velocity, and acceleration. - ---- - -## Tips and Best Practices -- Prefer High-Level Behaviors: They simplify complex movement patterns and automatically handle integration with the Physics System. -- Use Low-Level Behaviors for Custom Logic: When precise control is required, combine low-level behaviors for unique movement styles. -- Smooth Orientation: Use alignOrientation or integrate orientation alignment directly into high-level functions. -- Tune Parameters: Adjust maxSpeed, turnSpeed, and slowingRadius for different entity types (e.g., fast-moving cars vs. slow-moving enemies). - ---- - -## Common Issues and Fixes - -### Issue: Entity Doesn’t Move - -- Cause: The Physics Component is missing or paused. -- Solution: Ensure the entity has a PhysicsComponents and it’s not paused. - -### Issue: Jittery Movement - -- Cause: Conflicting forces or large delta times. -- Solution: Tune maxSpeed and ensure deltaTime is passed correctly. - -### Issue: Entity Ignores Obstacles - -- Cause: Avoidance radius is too small or obstacles are not registered. -- Solution: Increase the avoidanceRadius and verify obstacle entities. - diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/_category.json b/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/_category.json deleted file mode 100644 index a1c32169e..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/03-Engine Systems/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Engine Systems", "position": 4, "collapsed": false } diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/04-Custom Components/_category.json b/website/versioned_docs/version-0.10.6/04-Engine Development/04-Custom Components/_category.json deleted file mode 100644 index 71b201664..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/04-Custom Components/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Custom Components", "position": 5, "collapsed": false } diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/04-Custom Components/customComponent.md b/website/versioned_docs/version-0.10.6/04-Engine Development/04-Custom Components/customComponent.md deleted file mode 100644 index d327a3577..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/04-Custom Components/customComponent.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -id: ecs-create-custom-component -title: Create a Custom Component -sidebar_position: 1 ---- - -# Create a Custom Component - -In your game, you may want to **extend functionality to an entity**. -You can do this by creating a **custom component**. - -Components in the Untold Engine are **data-only objects** that you attach to entities. -They should hold **state, not behavior**. All game logic is handled in systems. - -Every custom component must conform to the `Component` protocol. - -By following this design, your game stays modular: -- **Components** define what an entity *is capable of*. -- **Systems** define *how that capability behaves*. - ---- - -## Minimal Template - -Here’s an example of a simple custom component for a soccer player’s dribbling behavior: - -```swift -public class DribblinComponent: Component { - public required init() {} - var maxSpeed: Float = 5.0 - var kickSpeed: Float = 15.0 - var direction: simd_float3 = .zero -} -``` - -> ⚠️ Note: Components should not include functions or game logic. Keep them as pure data containers. - -## Attaching a Component to an Entity - -Once you’ve defined a component, you attach it to an entity in your scene: - -```swift -let player = createEntity(name: "player") - -// Attach DribblinComponent to the entity -registerComponent(entityId: player, componentType: DribblinComponent.self) - -// Access and modify component data -if let c = scene.get(component: DribblinComponent.self, for: player) { - c.maxSpeed = 6.5 - c.kickSpeed = 18.0 -} - -``` - -This example creates a new entity called player, attaches a DribblinComponent, and updates its values. - - -On its own, the component just stores numbers — it doesn’t do anything yet. -To make the player actually dribble, you’ll need to implement a system that processes this component each frame. diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/04-Custom Components/customSystem.md b/website/versioned_docs/version-0.10.6/04-Engine Development/04-Custom Components/customSystem.md deleted file mode 100644 index 0b6ed75bc..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/04-Custom Components/customSystem.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: ecs-create-custom-system -title: Create a Custom System -sidebar_position: 2 ---- - -# Create a Custom System - -If you’ve created a **custom component**, you’ll usually also want to create a **custom system** to make it do something. -Components store the data, but systems are where the behavior lives. - -The engine automatically calls systems every frame. A system typically: - -1. Resolves the component IDs it cares about -2. Queries entities that have those components -3. Reads and updates their state (transforms, physics, animation, etc.) - -This separation ensures components remain pure data containers, while systems drive the simulation. - ---- - -## Minimal Template - -Here’s a simple system that works with the `DribblinComponent` we defined earlier: - -```swift -public func dribblingSystemUpdate(deltaTime: Float) { - // 1. Get the ID of the DribblinComponent - let customId = getComponentId(for: DribblinComponent.self) - - // 2. Query all entities that have this component - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - // 3. Loop through each entity and update its data - for entity in entities { - guard let dribblingComponent = scene.get(component: DribblinComponent.self, for: entity) else { - continue - } - - // Example logic: move player in the dribbling direction - dribblingComponent.direction = simd_normalize(dribblingComponent.direction) - let displacement = dribblingComponent.direction * dribblingComponent.maxSpeed * deltaTime - - if let transform = scene.get(component: LocalTransformComponent.self, for: entity) { - transform.position += displacement - } - } -} -``` - -## Registering the System -All custom systems must be registered during initialization so the engine knows to run them every frame: - -```swift -registerCustomSystem(dribblingSystemUpdate) -``` - diff --git a/website/versioned_docs/version-0.10.6/04-Engine Development/_category.json b/website/versioned_docs/version-0.10.6/04-Engine Development/_category.json deleted file mode 100644 index e71fa89a8..000000000 --- a/website/versioned_docs/version-0.10.6/04-Engine Development/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "Engine Development", "position": 3, "collapsed": false } - diff --git a/website/versioned_docs/version-0.10.6/05-Editor Development/02-Architecture/EditorArchitecture.md b/website/versioned_docs/version-0.10.6/05-Editor Development/02-Architecture/EditorArchitecture.md deleted file mode 100644 index c2d198577..000000000 --- a/website/versioned_docs/version-0.10.6/05-Editor Development/02-Architecture/EditorArchitecture.md +++ /dev/null @@ -1,187 +0,0 @@ -# Editor Architecture - -This document provides a high-level overview of the **Untold Editor architecture**. - -It is intended for contributors who want to understand how the editor is structured before working on specific views, tools, or workflows. - -This page focuses on **concepts, responsibilities, and data flow**, not implementation details. - ---- - -## Architectural Goals - -The Untold Editor is designed with the following goals: - -- **Editor as a client of the engine** - The editor uses the same runtime as the game. - -- **Explicit data flow** - Editor actions translate directly into engine state changes. - -- **Minimal editor-only behavior** - Play mode mirrors runtime behavior as closely as possible. - -- **Composable tools and views** - Editor features should be modular and replaceable. - -The editor is a tool — not a separate simulation environment. - ---- - -## High-Level Structure - -At a conceptual level, the editor is organized into these layers: - -Editor UI (Views & Tools) -↓ -Editor Coordination Layer -↓ -Engine Runtime -↓ -Platform & Rendering Backends - -The editor does not own the engine — it **drives** it. - ---- - -## Editor as an Engine Client - -The Untold Editor runs on top of the same engine runtime used by games. - -Key implications: -- Scene data is real engine data -- Systems execute through the same update loop -- Rendering paths are shared -- Bugs reproduced in the editor are runtime-relevant - -There is no “fake” editor simulation. - ---- - -## Editor Coordination Layer - -Between the UI and the engine sits a thin coordination layer. - -This layer is responsible for: -- Translating UI actions into engine operations -- Managing selection state -- Coordinating editor-only modes (Edit vs Play) -- Routing commands between views - -It does **not** contain simulation logic. - ---- - -## Views - -The editor is composed of **independent views**, each with a focused responsibility. - -Typical views include: -- Scene View -- Inspector -- Scene Hierarchy -- Asset Browser -- Code Editor (USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - -Views: -- Observe engine state -- Emit commands -- Do not own core data - -This keeps views simple and interchangeable. - ---- - -## Tools and Interaction - -Editor tools (selection, transform, manipulation, etc.) are built as: -- Stateless or minimally stateful controllers -- Operating on selected engine entities -- Emitting explicit transform or component changes - -Input handling is centralized and routed to active tools. - ---- - -## Scene Editing Model - -Scene editing operates directly on engine data: - -- Entities and components are real -- Transforms update immediately -- Changes are visible to all views - -Editor-only metadata (selection, highlighting, gizmos) is stored separately. - ---- - -## Edit Mode vs Play Mode - -The editor supports two primary modes: - -### Edit Mode -- Systems that mutate simulation state are paused -- Editor tools manipulate entity state directly -- Scene changes are persistent - -### Play Mode -- Full engine update loop runs -- USC scripts execute -- Physics and animation systems are active -- Scene state may be restored on exit - -The transition between modes is explicit and controlled. - ---- - -## Asset and Resource Handling - -The editor manages assets as references to engine resources. - -Responsibilities include: -- Importing external files -- Tracking asset paths -- Updating resource bindings -- Refreshing views when assets change - -The engine remains the owner of resource lifetimes. - ---- - -## Relationship to USC - -USC scripts are authored in Xcode and managed through the editor but executed by the engine. The editor itself does not host a built-in USC script editor. - -The editor: -- Creates script source files -- Triggers script builds -- Attaches generated scripts to entities - -The editor does not interpret or execute USC logic. - ---- - -## Design Tradeoffs - -The Untold Editor intentionally avoids: - -- Duplicating engine logic -- Editor-only simulation paths -- Heavy UI-driven state mutation -- Hidden side effects from tools - -These tradeoffs prioritize: -- Consistency with runtime behavior -- Debuggability -- Contributor approachability - ---- - -## Architecture in Practice - -This document describes the conceptual structure of the editor. - -For implementation details, see: - -Editor Development → Architecture → Internals - diff --git a/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/CodeEditorView.md b/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/CodeEditorView.md deleted file mode 100644 index 89753d95c..000000000 --- a/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/CodeEditorView.md +++ /dev/null @@ -1,129 +0,0 @@ -# Code Editor View - -The **Code Editor View** is the text editing surface for scripts and other source assets in the Untold Editor. USC scripts themselves are authored in Xcode; the editor does not provide a built-in USC script editor. - -It presents source files, supports editing workflows, and emits save/build/run intents through the coordination layer without owning compilation or runtime. - -This document describes the **architectural role** of the Code Editor View, not how to use it as an end user. - ---- - -## Purpose - -The Code Editor View exists to: - -- Surface project scripts and source files for editing -- Provide an authoring surface for text assets; USC authoring lives in Xcode and is only surfaced here for viewing/coordination -- Bridge editing to build and run workflows via explicit commands -- Reflect scripting workflow state (open project, build output) in the editor context - -It is the link between source assets and scripting workflows, not the runtime itself. - ---- - -## Responsibilities - -The Code Editor View is responsible for: - -- Displaying and editing text buffers for selected source assets -- Reflecting file metadata (path, dirty state, read-only status) -- Emitting save commands for edited buffers -- Emitting build/run/test commands tied to scripting workflows -- Surfacing diagnostics and build output provided by external services -- Respecting editor mode (Edit vs Play) when enabling edits or execution commands - -The Code Editor View does **not** own source storage, compilation, or runtime execution. - ---- - -## What This View Does NOT Do - -The Code Editor View intentionally does **not**: - -- Interpret or execute USC or other scripts -- Own compilation, packaging, or deployment workflows; it triggers them -- Manage source control operations beyond reflecting status indicators -- Own project discovery or dependency resolution -- Apply engine changes directly; all actions flow through commands -- Decide selection globally; it observes selection/open-file state from shared editor context - -If code editor behavior appears to require compilation or runtime ownership, that logic belongs elsewhere. - ---- - -## Data Flow - -The Code Editor View participates in the editor data flow as follows: - -### Reads -- Source file contents and metadata (path, permissions, timestamps) -- Open-file/session state from the editor -- Build/run status and diagnostics from scripting services -- Editor mode state - -### Emits -- Save commands for current buffers -- Build/run/test commands for the active scripting project -- File open/close requests within the editor session -- Selection change requests for symbols or files if shared selection is used - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Code Editor View interacts indirectly with other views via shared editor state: - -- **Asset Browser** - Opens scripts selected in the asset list - -- **Scene View / Scene Hierarchy** - May reflect selection-driven context (e.g., open script referenced by a component) but does not call views directly - -- **Inspector** - Consumes scripts as assignable assets; script edits propagate via save and build commands - -The Code Editor View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full text editing, save, and build/run commands are enabled -- Diagnostics and build output are surfaced for authoring - -### Play Mode -- Editing may be limited or read-only depending on project policy -- Build/run commands that mutate authoring state may be blocked or deferred -- Runtime logs may be surfaced without enabling authoring changes - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Code Editor View by: - -- Adding language services (syntax highlighting, completion, diagnostics) via existing plugin hooks -- Integrating advanced navigation (symbol search, references) through shared project metadata -- Surfacing build/test pipelines with richer progress and error reporting -- Enhancing run configurations to emit explicit launch commands - -Extensions should integrate through existing command pathways and shared scripting state. - ---- - -## Design Constraints - -The Code Editor View is intentionally constrained to: - -- Text editing and presentation of scripting-related feedback -- Command emission for save/build/run -- Stateless or minimal local UI state driven by shared data -- No direct compilation, execution, or mutation of engine state - -Keeping these boundaries strict ensures predictable scripting workflows and debuggability. - diff --git a/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/Overview.md b/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/Overview.md deleted file mode 100644 index 109c70294..000000000 --- a/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/Overview.md +++ /dev/null @@ -1,172 +0,0 @@ -# Editor Views Overview - -This section documents the **views that make up the Untold Editor**. - -It is intended for contributors who want to understand how editor views are structured, how they interact with the engine, and how they coordinate with each other. - -This is **not** user documentation. -It does not explain how to *use* the editor — only how it is built. - ---- - -## What Is a View? - -A **View** is a UI surface that: -- Observes engine and editor state -- Presents that state visually -- Emits explicit commands in response to user input - -Views do **not** own core data. - -They are clients of the editor and engine systems. - ---- - -## Responsibilities of a View - -Every editor view follows the same core responsibilities: - -- Render information derived from engine or editor state -- React to user input (mouse, keyboard, gestures) -- Emit commands to the editor coordination layer -- Stay stateless or minimally stateful - -A view should be easy to reason about in isolation. - ---- - -## What Views Do NOT Do - -Views intentionally do **not**: - -- Own or mutate engine state directly -- Contain simulation logic -- Manage entity lifetimes -- Execute USC scripts -- Perform rendering or physics work themselves - -If a view feels like it needs to “do logic,” that logic likely belongs elsewhere. - ---- - -## Data Flow Model - -All views participate in a shared data flow model: - -Engine Runtime -↓ -Editor Coordination Layer -↓ -Editor Views -↓ -User Input -↓ -Editor Commands -↓ -Engine Runtime - -Key characteristics: -- Data flows downward -- Commands flow upward -- Views never bypass the coordination layer - -This keeps behavior predictable and debuggable. - ---- - -## Selection as Shared State - -Selection is a cross-view concern. - -- Views may observe the current selection -- Views may request selection changes -- No single view owns selection state - -This allows multiple views to remain decoupled while staying synchronized. - ---- - -## View Independence - -Views are designed to be independent and replaceable. - -This means: -- Views do not directly call each other -- Communication happens through shared editor state -- Views can be added, removed, or replaced without breaking others - -This is a deliberate architectural decision. - ---- - -## Editor Modes and Views - -Views must respect the editor’s active mode. - -### Edit Mode -- Views manipulate persistent scene data -- Simulation systems are paused -- Tools operate directly on entity state - -### Play Mode -- Views observe runtime state -- Simulation systems are active -- Editing is limited or disabled - -Views should not decide mode behavior — they respond to it. - ---- - -## Common View Patterns - -While views are independent, many follow similar patterns: - -- Scene-oriented views -- Inspection views -- Asset-oriented views -- Code-oriented views - -Each category emphasizes different interactions but follows the same architectural rules. - ---- - -## Extending or Adding a View - -When adding a new view: - -1. Define the view’s purpose -2. Identify the data it observes -3. Identify the commands it emits -4. Ensure it does not own core state -5. Integrate it through the coordination layer - -If a new view requires engine changes, document those explicitly. - ---- - -## View Documentation Structure - -Each individual view document follows this structure: - -- Purpose -- Responsibilities -- What This View Does NOT Do -- Data Flow -- Interaction With Other Views -- Extension Points - -This keeps documentation consistent and easy to scan. - ---- - -## Available Views - -The following views are documented in this section: - -- Scene View -- Inspector -- Scene Hierarchy -- Asset Browser -- Code Editor (USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - ---- diff --git a/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/SceneHierarchyView.md b/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/SceneHierarchyView.md deleted file mode 100644 index aad738a1a..000000000 --- a/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/SceneHierarchyView.md +++ /dev/null @@ -1,127 +0,0 @@ -# Scene Hierarchy View - -The **Scene Hierarchy View** presents the scene graph as an ordered list/tree of entities and is the primary control surface for selection and hierarchy manipulation. - -It exposes parent/child relationships conceptually and emits structured commands for selection, reparenting, ordering, and basic entity lifecycle actions. - -This document describes the **architectural role** of the Scene Hierarchy View, not how to use it as an end user. - ---- - -## Purpose - -The Scene Hierarchy View exists to: - -- Visualize the scene graph and entity ordering -- Provide a clear control point for selecting entities -- Enable conceptual hierarchy edits (parenting, unparenting, reordering) -- Initiate entity lifecycle actions (create, duplicate, delete) through commands - -It is the bridge between the scene graph model and selection-centric editing. - ---- - -## Responsibilities - -The Scene Hierarchy View is responsible for: - -- Displaying the scene graph structure and entity ordering -- Reflecting shared selection state and allowing selection changes -- Emitting hierarchy edit commands (reparent, reorder, toggle visibility/activation) -- Emitting entity lifecycle requests (create, duplicate, delete) via commands -- Respecting editor mode (Edit vs Play) when enabling hierarchy edits - -The Scene Hierarchy View does **not** own scene data or selection state. - ---- - -## What This View Does NOT Do - -The Scene Hierarchy View intentionally does **not**: - -- Compute transforms or resolve world/local matrices -- Maintain or persist the scene graph; it only visualizes and issues commands -- Perform simulation, physics, or animation updates -- Decide selection globally; it requests changes through shared state -- Apply engine changes directly; all edits flow through the coordination layer -- Execute scripting or component logic - -If hierarchy behavior appears to require graph ownership or simulation, that logic belongs elsewhere. - ---- - -## Data Flow - -The Scene Hierarchy View participates in the editor data flow as follows: - -### Reads -- Scene graph structure (entities, parent/child links, ordering) -- Entity metadata needed for display (names, icons, state flags) -- Selection state -- Editor mode state - -### Emits -- Selection change requests -- Reparent and reorder commands -- Visibility/activation toggle commands (if exposed) -- Entity lifecycle requests (create, duplicate, delete) - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Scene Hierarchy View interacts indirectly with other views via shared editor state: - -- **Scene View** - Synchronizes selection changes between hierarchy and world-space interaction - -- **Inspector** - Displays and edits data for entities selected in the hierarchy - -- **Asset Browser** - May support drag-and-drop of assets to instantiate entities or assign references - -The Scene Hierarchy View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full hierarchy editing (reparent, reorder, lifecycle actions) is available -- Selection changes drive authoring-state inspection - -### Play Mode -- Hierarchy is primarily observational; destructive edits may be blocked or limited -- Selection may reflect runtime entities without persisting authoring changes - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Scene Hierarchy View by: - -- Adding filters, search, or grouping affordances -- Introducing custom badges or state indicators derived from metadata -- Extending drag-and-drop behaviors for entity or asset interactions -- Providing bulk operations that emit explicit hierarchy commands - -Extensions should integrate through existing command pathways and shared state. - ---- - -## Design Constraints - -The Scene Hierarchy View is intentionally constrained to: - -- Visualization of the scene graph and selection state -- Command emission for hierarchy and lifecycle edits -- Stateless or minimal local UI state driven by shared data -- No direct mutation of engine or editor core data - -Keeping these boundaries strict ensures predictable editing and debuggability. - diff --git a/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/SceneView.md b/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/SceneView.md deleted file mode 100644 index 984b7fd7b..000000000 --- a/website/versioned_docs/version-0.10.6/05-Editor Development/03-Views/SceneView.md +++ /dev/null @@ -1,158 +0,0 @@ -# Scene View - -The **Scene View** is the primary spatial view in the Untold Editor. - -It provides a visual representation of the current scene and allows contributors and tools to interact with entities in world space. - -This document describes the **architectural role** of the Scene View, not how to use it as an end user. - ---- - -## Purpose - -The Scene View exists to: - -- Visualize the current scene state -- Display entities and their transforms -- Provide spatial context for selection and manipulation -- Act as the main interaction surface for world-space tools - -It is the bridge between engine data and spatial editor interaction. - ---- - -## Responsibilities - -The Scene View is responsible for: - -- Rendering a visual representation of the scene -- Displaying editor overlays (selection outlines, gizmos, helpers) -- Receiving world-space input (mouse, keyboard, gestures) -- Emitting commands related to selection and transformation -- Respecting the current editor mode (Edit vs Play) - -The Scene View does **not** own scene data. - ---- - -## What This View Does NOT Do - -The Scene View intentionally does **not**: - -- Own or modify engine state directly -- Perform simulation, physics, or animation updates -- Decide how entities are selected globally -- Execute USC scripts -- Implement rendering pipelines or render passes - -If the Scene View appears to require simulation logic, that logic belongs elsewhere. - ---- - -## Data Flow - -The Scene View participates in the editor data flow as follows: - -### Reads -- Scene graph data -- Entity transforms -- Camera state -- Selection state -- Editor mode state - -### Emits -- Selection change requests -- Transform manipulation commands -- Camera navigation commands -- Tool activation signals - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Scene View interacts indirectly with other views via shared editor state: - -- **Scene Hierarchy** - Synchronizes selection changes - -- **Inspector** - Displays and edits data for selected entities - -- **Asset Browser** - May initiate drag-and-drop actions into the scene - -The Scene View does not directly communicate with other views. - ---- - -## Tools and Manipulation - -World-space tools (translate, rotate, scale, etc.) are driven through the Scene View. - -Key characteristics: -- Tools operate on selected entities -- Input is routed to the active tool -- Tools emit explicit transform commands -- Visual gizmos are editor-only overlays - -The Scene View hosts tool interaction but does not implement tool logic. - ---- - -## Camera Control - -The Scene View owns the **editor camera**, which is distinct from game cameras. - -Responsibilities include: -- Camera navigation (orbit, pan, zoom) -- Framing selected entities -- Switching camera perspectives - -Editor camera state is isolated from runtime camera components. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Scene View allows full manipulation -- Entity transforms are persistent -- Editor overlays and gizmos are visible - -### Play Mode -- Scene View observes runtime state -- Manipulation may be limited or disabled -- Runtime cameras may be previewed - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Scene View by: - -- Adding new world-space tools -- Introducing new editor overlays -- Customizing camera behavior -- Adding debug visualization layers - -Extensions should integrate through existing tool and command pathways. - ---- - -## Design Constraints - -The Scene View is intentionally constrained to: - -- Visualization -- Input capture -- Command emission - -Keeping these boundaries strict ensures: -- Predictable behavior -- Easier debugging -- Cleaner separation of concerns - diff --git a/website/versioned_docs/version-0.10.6/05-Editor Development/EditorOverview.md b/website/versioned_docs/version-0.10.6/05-Editor Development/EditorOverview.md deleted file mode 100644 index 27704bf80..000000000 --- a/website/versioned_docs/version-0.10.6/05-Editor Development/EditorOverview.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -id: editorfeatures -title: Editor Features -sidebar_position: 2 ---- - -# Untold Engine Editor Features - -The Untold Engine Editor makes it easier than ever to set up your scenes and entities without touching code. With the Editor, you can visually create, configure, and organize your game world, while keeping Swift code focused on game logic and behaviors. - -Here’s a quick tour of the Editor’s main features: - ---- - -## Scene Graph - -![Scene Graph](../images/engine-scenegraph.png) - -The **Scene Graph** shows all the entities in your scene in a hierarchical view. You can add, rename, and organize entities, as well as set up parent–child relationships. This is where you’ll find your cameras, lights, and models at a glance. - ---- - -## Inspector - -![Inspector](../images/engine-inspector.png) - -The **Inspector** lets you configure properties of the selected entity. Adjust position, rotation, and scale, or fine-tune camera settings and add components. Think of it as the control panel for whatever you’re working on. - ---- - -## Gizmo Manipulation - -![Gizmo](../images/engine-gizmo.png) - -Use the **3D gizmo** to interactively move, rotate, and scale entities directly in the scene. This makes it quick to place objects exactly where you want them. - ---- - -## Materials - -![Materials](../images/engine-materials.png) - -Assign meshes and materials through the **Inspector**. You can drop in textures for base color, roughness, metallic, and emissive maps, then tweak material properties to get the right look. - ---- - -## Lighting & Environment - -![Lighting](../images/engine-lights.png) - -The **Lighting panel** give you control over your scene’s mood. Add directional, point, spotlight and area lights, adjust intensities. - -## Environment - -The **Environment panel** enable **Image-Based Lighting (IBL)** for realistic reflections and ambient light. - -![HDR](../images/engine-hdr.png) - ---- - -## Post-Processing Effects - -![Post Processing](../images/engine-post-processing.png) - -The **Effects tab** lets you add and tweak post-processing features such as: -- Depth of Field -- Chromatic Aberration -- Bloom -- Color Grading -- SSAO, Vignette, White Balance, and more - -These effects bring your scenes closer to a polished, production-ready look. - ---- - -## Asset Browser - -![Asset Browser](../images/engine-assetbrowser.png) - -The **Asset Browser** keeps your models, textures, and materials organized. Import new assets, set paths for your project, and quickly assign resources to entities in your scene. - ---- - -## Console Log - -![Console](../images/engine-consolelog.png) - -The **Console Log** provides real-time feedback from the engine. Use it to debug entity creation, monitor system messages, and track issues while working on your scene. - ---- - -## Putting It All Together - -With the Editor, you now have a clear separation of responsibilities: -- **Use the Editor** for entity initialization, scene setup, materials, and visual configuration. -- **Use Swift code** for gameplay logic, physics tweaks, and systems that bring your game to life. - -This workflow makes iteration faster and keeps your codebase focused on what matters most: gameplay. - diff --git a/website/versioned_docs/version-0.10.6/06-Reference/01-USCAPI.md b/website/versioned_docs/version-0.10.6/06-Reference/01-USCAPI.md deleted file mode 100644 index 0c961d1cb..000000000 --- a/website/versioned_docs/version-0.10.6/06-Reference/01-USCAPI.md +++ /dev/null @@ -1,133 +0,0 @@ -# Untold Script Core API - -Use this page as a compact checklist of the most-used USC (Untold Script Core) DSL calls. All snippets assume you’re inside a `buildScript` closure on the current entity (`self`). - ---- - -## Lifecycle -- `onStart()` – one-time init per play. -- `onUpdate()` – every frame. -- `onEvent("Name")` – custom triggers. -- `onCollision(tag:)` – **planned** (not yet available). - -## Properties (get/set) -Supported keys: `.position`, `.scale`, `.velocity`, `.acceleration`, `.mass`, `.angularVelocity` (write-only today), `.intensity`, `.color`, `.deltaTime`. -```swift -.getProperty(.position, as: "pos") -.setProperty(.position, toVariable: "nextPos") -.setProperty(.velocity, to: simd_float3(0, 2, 0)) -``` - -## Input -```swift -.ifKeyPressed("W") { n in n.log("forward") } -.ifKeyReleased("Space") { n.log("jump released") } -.getKeyState("w", as: "wPressed") -``` - -## Transform -```swift -.translateTo(simd_float3(0, 1, 0)) -.translateBy(simd_float3(0, 0, 1)) -.rotateTo(degrees: 45, axis: simd_float3(0, 1, 0)) -.rotateBy(degrees: .float(5), axis: simd_float3(0, 1, 0)) -.lookAt("TargetEntity") -``` - -## Animation -```swift -.playAnimation("Walk", loop: true) -.stopAnimation() -``` - -## Physics (forces/velocity) -```swift -.applyForce(force: simd_float3(0, 10, 0)) -.applyLinearImpulse(direction: .vec3(x: 0, y: 1, z: 0), magnitude: .float(5)) -.setLinearVelocity(.vec3(x: 0, y: 0, z: 5)) -.addLinearVelocity(.vec3(x: 0, y: 0, z: -1)) -.clampLinearSpeed(min: .float(0), max: .float(8)) -.clearVelocity() -.clearAngularVelocity() -.clearForces() -.setGravityScale(0.5) -.pausePhysicsComponent(isPaused: true) -``` - -## Steering (vectors or side effects) -```swift -.seek(targetPosition: .vec3(x: 10, y: 0, z: 0), maxSpeed: .float(5), result: "steer") -.flee(threatPosition: .vec3(x: 0, y: 0, z: 0), maxSpeed: .float(6), result: "steer") -.arrive(targetPosition: .vec3(x: 0, y: 0, z: 0), maxSpeed: .float(6), slowingRadius: .float(2), result: "steer") -.pursuit(targetEntity: .string("Player"), maxSpeed: .float(6), result: "steer") -.evade(threatEntity: .string("Enemy"), maxSpeed: .float(6), result: "steer") -.steerSeek(targetPosition: .variableRef("targetPos"), maxSpeed: .variableRef("maxSpeed"), deltaTime: .variableRef("dt")) -.steerArrive(targetPosition: .variableRef("targetPos"), maxSpeed: .variableRef("maxSpeed"), slowingRadius: .variableRef("slow"), deltaTime: .variableRef("dt")) -.steerFlee(threatPosition: .variableRef("threatPos"), maxSpeed: .variableRef("maxSpeed"), deltaTime: .variableRef("dt")) -.steerPursuit(targetEntity: .string("Target"), maxSpeed: .float(6), deltaTime: .float(0.016)) -.orbit(centerPosition: .vec3(x: 0, y: 0, z: 0), radius: .float(5), maxSpeed: .float(4), deltaTime: .float(0.016)) -.alignOrientation(deltaTime: .float(0.016), turnSpeed: .float(1.0)) -``` - -## Camera -```swift -.cameraMoveTo(.vec3(x: 0, y: 3, z: -10)) -.cameraMoveBy(.vec3(x: 1, y: 0, z: 0)) -.cameraRotate(pitch: .float(0.02), yaw: .float(-0.08)) -.cameraFollow(target: .string("Player"), - offset: .vec3(x: 0, y: 3, z: -6), - smoothFactor: .float(5), - deltaTime: .float(0.016)) -.cameraFollowLocal(target: .string("Player"), - localOffset: .vec3(x: 0, y: 2, z: -4), - smoothFactor: .float(5), - deltaTime: .float(0.016)) -.cameraOrbitTarget(target: .string("Boss"), - radius: .float(12), - speed: .float(1.5), - deltaTime: .float(0.016), - offsetY: .float(1.5)) -.cameraMoveWithInput(speedVar: "moveSpeed", - deltaTimeVar: "dt", - wVar: "wPressed", aVar: "aPressed", - sVar: "sPressed", dVar: "dPressed", - qVar: "qPressed", eVar: "ePressed") -``` - -## Math (variables only) -```swift -.addFloat("a", "b", as: "sum") -.addFloat("a", literal: 1, as: "sum") -.subFloat("a", "b", as: "diff") -.mulFloat("a", "b", as: "prod") -.divFloat("a", "b", as: "quot") -.addVec3("v1", "v2", as: "sum") -.scaleVec3("v", by: "s", as: "out") -.scaleVec3("v", literal: 2, as: "out") -.lengthVec3("v", as: "len") -.normalizeVec3("v", as: "unit") -.dotVec3("a", "b", as: "dot") -.crossVec3("a", "b", as: "cross") -.lerpVec3(from: "a", to: "b", t: "t", as: "out") -.lerpFloat(from: "a", to: "b", t: "t", as: "out") -.reflectVec3("v", normal: "n", as: "reflected") -.projectVec3("v", onto: "axis", as: "proj") -.angleBetweenVec3("a", "b", as: "angleDeg") -.clampFloat("speed", min: "minSpeed", max: "maxSpeed", as: "clamped") -.clampVec3("vel", min: "minVel", max: "maxVel", as: "clampedVel") -``` - -## Flow / Variables / Debug -```swift -.ifCondition(lhs: .variableRef("speed"), .greater, rhs: .float(10)) { n in n.log("Too fast") } -.ifGreater("health", than: 0) { n in n.log("Alive") }.else { n in n.log("Dead") } -.ifEqual("flag", to: true) { n in n.log("Flag set") } -.setVariable("speed", to: 5.0) -.setVariable("dir", to: simd_float3(0, 0, 1)) -.setVariable("copy", fromVariable: "speed") -.log("Hello") -.logValue("velocity", value: .variableRef("vel")) -``` - -> Tip: If you need an entity other than `self`, use names in your instructions (e.g., `.lookAt("TargetName")`) or stash them in variables, then call `findEntity`-driven instructions like `pursuit`/`evade`. - diff --git a/website/versioned_docs/version-0.10.6/06-Reference/02-EngineAPI.md b/website/versioned_docs/version-0.10.6/06-Reference/02-EngineAPI.md deleted file mode 100644 index 0d649d551..000000000 --- a/website/versioned_docs/version-0.10.6/06-Reference/02-EngineAPI.md +++ /dev/null @@ -1,408 +0,0 @@ -# Untold Engine Core API - -## Quick Reference - - -| Category | Common APIs | -|--------------|-------------| -| **Entities** | [createEntity](#create-an-entity), [destroyEntity](#destroy-an-entity), [setParent](#parent-child-relationships), [findEntity](#find-entity) | -| **Transforms** | [getLocalPosition](#get-local-position), [getPosition](#get-world-position), [getLocalOrientation](#get-local-orientation), [getOrientation](#get-world-orientation), [getForwardAxisVector](#get-axis-vectors), [translateTo](#translate-the-entity), [translateBy](#translate-the-entity), [rotateTo](#rotate-the-entity), [rotateBy](#rotate-the-entity) | -| **Assets** | [assetBasePath](#base-path-to-assets) | -| **Rendering** | [setEntityMesh](#link-a-mesh-to-the-entity), [setEntityGaussian](#link-a-gaussian-splat-to-the-entity), [createDirLight](#directional-light), [createPointLight](#point-light), [createSpotLight](#spot-light), [createAreaLight](#area-light) | -| **Animation** | [setEntityAnimations](#load-an-animation), [changeAnimation](#set-the-animation-to-play), [pauseAnimationComponent](#pause-the-animation-optional) | -| **Physics** | [setEntityKinetics](#enable-physics-on-the-entity), [setMass](#configure-physics-properties), [setGravityScale](#configure-physics-properties), [applyForce](#apply-forces-optional), [steerTo](#use-the-steering-system), [steerAway](#additional-steering-functions), [steerPursuit](#additional-steering-functions), [followPath](#additional-steering-functions) | -| **Components** | [registerComponent](#register-components), [create-custom-component](#create-custom-component), [attach-component](#attaching-a-component-to-an-entity) | -| **Systems** | [create-custom-system](#create-custom-system), [registerCustomSystem](#registering-the-custom-system) | - - -# Entities - -### Create an Entity - -Entities represent objects in the scene. Use the createEntity() function to create a new entity. - -```swift -let entity = createEntity() -``` - -### Destroy an Entity - -To remove an entity and its components from the scene, use destroyEntity. - -```swift -destroyEntity(entityId: entity) -``` - -This ensures the entity is properly removed from all systems. - ---- - -### Parent-Child Relationships - -To assign a parent to an entity, use the setParent function. This function establishes a hierarchical relationship between the specified entities. - -```swift -// Create child and parent entities -let childEntity = createEntity() -let parentEntity = createEntity() - -// Set parent-child relationship -setParent(childId: childEntity, parentId: parentEntity) -``` - -### Find Entity - -You can find an entity by name using the following function: - -```swift -let ball = findEntity(name: "ball") -``` ---- - -# Transforms - -### Get Local Position - -Retrieves the entity’s position relative to its parent. - -```swift -let localPosition = getLocalPosition(entityId: entity) -``` - -### Get World Position - -Retrieves the entity’s absolute position in the scene. - -```swift -let worldPosition = getPosition(entityId: entity) -``` - -### Get Local Orientation - -Retrieves the entity’s orientation matrix relative to its parent. - -```swift -let localOrientation = getLocalOrientation(entityId: entity) -``` - -### Get World Orientation - -Retrieves the entity’s absolute orientation matrix. - -```swift -let worldOrientation = getOrientation(entityId: entity) -``` - -### Get Axis Vectors - -Retrieve the entity’s forward, right, or up axis: - -```swift -let forward = getForwardAxisVector(entityId: entity) -let right = getRightAxisVector(entityId: entity) -let up = getUpAxisVector(entityId: entity) -``` - ---- - -### Translate the Entity - -Move the entity to a new position: - -```swift -translateTo(entityId: entity, position: simd_float3(5.0, 0.0, 3.0)) -``` - -Move the entity by an offset relative to its current position: - -```swift -translateBy(entityId: entity, position: simd_float3(1.0, 0.0, 0.0)) -``` - -### Rotate the Entity - -Rotate the entity to a specific angle around an axis: - -```swift -rotateTo(entityId: entity, angle: 45.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Apply an incremental rotation to the entity: - -```swift -rotateBy(entityId: entity, angle: 15.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Directly set the entity’s rotation matrix: - -```swift -rotateTo(entityId: entity, rotation: simd_float4x4( /* matrix values */ )) -``` - - ---- - -### Base path to assets - -Define the asset directory the engine will use to load content (Assuming you are using an external folder during development). Please see Import-Export section for more details. - -```swift -// Here we point it to a folder named "DemoGameAssets/Assets" on the Desktop. -// You can change this to any folder where you keep your own assets. -if let desktopURL = FileManager.default.urls(for: .desktopDirectory, in: .userDomainMask).first { - assetBasePath = desktopURL.appendingPathComponent("DemoGameAssets/Assets") -} -``` - -# Rendering - -### Link a Mesh to the Entity - -To display a model, load its .usdc file and link it to the entity using setEntityMesh. - -```swift -setEntityMesh(entityId: entity, filename: "entity", withExtension: "usdc") -``` - -Parameters: - -- entityId: The ID of the entity created earlier. -- filename: The name of the .usdc file (without the extension). -- withExtension: The file extension, typically "usdc". - -> Note: If PBR textures (e.g., albedo, normal, roughness, metallic maps) are included, the rendering system will automatically use the appropriate PBR shader to render the model with realistic lighting and material properties. - -### Link a Gaussian Splat to the Entity - -To display a Gaussian Splat model, load its .ply file and link it to the entity using setEntityGaussian. - -```swift -setEntityGaussian(entityId: entity, filename: "splat", withExtension: "ply") -``` - -Parameters: - -- entityId: The ID of the entity created earlier. -- filename: The name of the .ply file (without the extension). -- withExtension: The file extension, typically "ply". - -> Note: The Gaussian System renders point cloud data stored in the .ply format. Ensure your Gaussian Splat file is properly formatted and contains the necessary attributes (position, color, opacity, scale, rotation). - -### Directional Light - -Use for sunlight or distant key lights. Orientation (rotation) defines its direction. - -```swift -let sun = createEntity() -createDirLight(entityId: sun) -``` -### Point Light - -Omni light that radiates equally in all directions from a position. - -```swift -let bulb = createEntity() -createPointLight(entityId: bulb) -``` - -### Spot Light - -Cone-shaped light with a position and direction. - -```swift -let spot = createEntity() -createSpotLight(entityId: spot) -``` - -### Area Light - -Rect/area emitter used to mimic panels/windows; position and orientation matter. - -```swift -let panel = createEntity() -createAreaLight(entityId: panel) -``` - -### Enable or Disable PostFX - -Toggle post-processing effects globally through the PostFX facade: - -```swift -PostFX.setEnabled(.colorGrading, false) - -PostFX.enableVignette(false) -``` - -### SSAO Controls - -SSAO is managed through a dedicated API because it affects render quality and pipelines: - -```swift -SSAO.setEnabled(true) -SSAO.setQuality(.high) -SSAO.setRadius(0.8) -SSAO.setBias(0.02) -SSAO.setIntensity(1.2) -``` - ---- - -# Animation - -### Load an Animation -Load the animation data for your model by providing the animation .usdc file and a name to reference the animation later. - -```swift -setEntityAnimations(entityId: redPlayer, filename: "running", withExtension: "usdc", name: "running") -``` - -### Set the Animation to play - -Trigger the animation by referencing its name. This will set the animation to play on the entity. - -```swift -changeAnimation(entityId: redPlayer, name: "running") -``` - -### Pause the animation (Optional) - -To pause the current animation, simply call the following function. The animation component will be paused for the current entity. - -```swift -pauseAnimationComponent(entityId: redPlayer, isPaused: true) -``` - ---- - -# Physics - -### Enable Physics on the Entity - -Activate the physics simulation for your entity using the setEntityKinetics function. This function prepares the entity for movement and dynamic interaction. - -```swift -setEntityKinetics(entityId: redPlayer) -``` ---- - -### Configure Physics Properties -You can customize the entity’s physics behavior by defining its mass and gravity scale: - -- Mass: Determines the force needed to move the object. Heavier objects require more force. -- Gravity Scale: Controls how strongly gravity affects the entity (default is 0.0). - -```swift -setMass(entityId: redPlayer, mass: 0.5) -setGravityScale(entityId: redPlayer, gravityScale: 1.0) -``` - -### Apply Forces (Optional) -You can apply a custom force to the entity for dynamic movement. This is useful for simulating actions like jumps or pushes. - -```swift -applyForce(entityId: redPlayer, force: simd_float3(0.0, 0.0, 5.0)) -``` - -> Note: Forces are applied per frame. To avoid unintended behavior, only apply forces when necessary. - -### Use the Steering System -For advanced movement behaviors, leverage the Steering System to steer entities toward or away from targets. This system automatically calculates the required forces. - -Example: Steering Toward a Position - -```swift -steerTo(entityId: redPlayer, targetPosition: simd_float3(0.0, 0.0, 5.0), maxSpeed: 2.0, deltaTime: deltaTime) -``` - -### Additional Steering Functions - -The Steering System includes other useful behaviors, such as: - -- steerAway() -- steerPursuit() -- followPath() - -These functions simplify complex movement patterns, making them easy to implement. - ---- - -# Components - -### Register Components - -Components define the behavior or attributes of an entity. Use registerComponent to add a component to an entity. - -```swift -let entity = createEntity() - -registerComponent(entityId: entity, componentType: RenderComponent.self) -``` - -### Create Custom Component - -Here’s an example of a simple custom component for a soccer player’s dribbling behavior: - -```swift -public class DribblinComponent: Component { - public required init() {} - var maxSpeed: Float = 5.0 - var kickSpeed: Float = 15.0 - var direction: simd_float3 = .zero -} -``` - -> ⚠️ Note: Components should not include functions or game logic. Keep them as pure data containers. - -### Attaching a Component to an Entity - -Once you’ve defined a component, you attach it to an entity in your scene: - -```swift -let player = createEntity(name: "player") - -// Attach DribblinComponent to the entity -registerComponent(entityId: player, componentType: DribblinComponent.self) -``` - -# Systems - -### Create Custom System - -If you’ve created a **custom component**, you’ll usually also want to create a **custom system** to make it do something. -Components store the data, but systems are where the behavior lives. - -The engine automatically calls systems every frame. - -Here’s a simple system that works with the `DribblinComponent` we defined earlier: - -```swift -public func dribblingSystemUpdate(deltaTime: Float) { - // 1. Get the ID of the DribblinComponent - let customId = getComponentId(for: DribblinComponent.self) - - // 2. Query all entities that have this component - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - // 3. Loop through each entity and update its data - for entity in entities { - guard let dribblingComponent = scene.get(component: DribblinComponent.self, for: entity) else { - continue - } - - // Example logic: move player in the dribbling direction - dribblingComponent.direction = simd_normalize(dribblingComponent.direction) - let displacement = dribblingComponent.direction * dribblingComponent.maxSpeed * deltaTime - - if let transform = scene.get(component: LocalTransformComponent.self, for: entity) { - transform.position += displacement - } - } -} -``` - -### Registering the Custom System -All custom systems must be registered during initialization so the engine knows to run them every frame: - -```swift -registerCustomSystem(dribblingSystemUpdate) -``` diff --git a/website/versioned_docs/version-0.10.6/07-Contributor/ContributionGuidelines.md b/website/versioned_docs/version-0.10.6/07-Contributor/ContributionGuidelines.md deleted file mode 100644 index 5f52ebeb9..000000000 --- a/website/versioned_docs/version-0.10.6/07-Contributor/ContributionGuidelines.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -id: contributorguidelines -title: Contributor Guidelines -sidebar_position: 1 ---- - -# Contributing Guidelines - -Thank you for your interest in contributing! - -The vision for Untold Engine is to continually shape a 3D engine that is **stable, performant and developer-friendly**. -As maintainer, my focus is on **performance, testing, quality control, and API design**. -Contributors are encouraged to expand features, fix bugs, improve documentation, and enhance usability — always keeping the vision in mind. - ---- - -## Maintainer Responsibilities - -The Untold Engine is guided by a clear vision: To be a stable, performant, and developer-friendly 3D engine that empowers creativity, removes friction, and makes game development feel effortless. - -## Guiding Principles - -To achieve this vision, we follow these principles: - -- The engine strives to remain stable and crash-free. -- The codebase is backed by unit tests. -- We profile continuously to prevent regressions (visual and performance). -- The API must remain clear and user-friendly. -- We always think about the developer first—removing friction so they can focus on their games. - -As the maintainer, my primary focus is to ensure the project stays true to this vision. - -### What I Focus On -- **Performance** → keeping the renderer and systems lean, efficient, and optimized for Apple hardware. -- **Testing & Stability** → maintaining a reliable codebase with proper testing practices. -- **Quality Control** → reviewing PRs for clarity, readability, and adherence to coding standards. -- **API Design** → ensuring that the engine’s API remains logical, intuitive, and consistent. - -### What Contributors Are Encouraged to Focus On -- **Features** → adding or improving systems, tools, and workflows. -- **Bug Fixes** → addressing open issues and fixing edge cases. -- **Documentation** → clarifying how things work and providing examples. -- **Editor & Usability** → enhancing the UI, workflows, and overall developer experience. - -### Decision Making -All contributions are welcome, but acceptance will be guided by the project’s vision and the priorities above. -PRs that align with clarity, performance, or creativity — while keeping the engine stable and simple — are more likely to be accepted. - -These guidelines aren’t here to block you, but to make sure every contribution keeps the engine stable, clear, and useful for everyone. - -## Pull Request Guidelines - -- **One feature or bug fix per PR** - Each Pull Request should focus on a single feature, bug fix, or documentation improvement. - This keeps the history clean and makes it easier to track down issues later. - -- **Commit hygiene** - - Keep commits meaningful (avoid "misc changes" or "fix stuff"). - - Squash commits if needed, but do not mix unrelated features in the same commit. - - If your PR touches multiple files, make sure they all relate to the same feature or fix. - -✅ Example: -- Good: *“Add PhysicsSystem with gravity integration”* -- Bad: *“Added PhysicsSystem + fixed rendering bug + updated docs”* - ---- - -## Required Contributions for New System Support - -For **new systems or major features**, your PR must include: - -- **Unit Tests** → Validate functionality and cover edge cases. -- **How-To Guide** → A step-by-step markdown guide explaining how to use the system. - -This ensures new features are stable, documented, and accessible to all users. - -👉 Note: For small fixes or incremental features, a How-To is not required. - ---- - -### How-To Guide Format - -Your guide must follow this structure: - -1. Introduction - -- Briefly explain the feature and its purpose. -- Describe what problem it solves or what value it adds. - -2. Why Use It - -- Provide real-world examples or scenarios where the feature is useful. -- Explain the benefits of using the feature in these contexts. - -3. Step-by-Step Implementation - -- Break down the setup process into clear, actionable steps. -- Include well-commented code snippets for each step. - -4. What Happens Behind the Scenes - -- Provide technical insights into how the system works internally (if relevant). -- Explain any significant impacts on performance or functionality. - -5. Tips and Best Practices - -- Share advice for effective usage. -- Highlight common pitfalls and how to avoid them. - -6. Running the Feature - -- Explain how to test or interact with the feature after setup. - ---- - -### Additional Notes - ---- - -## Questions & Discussions - -To keep communication clear and accessible for everyone: - -- 💡 Use **[GitHub Discussions](https://github.com/untoldengine/UntoldEngine/discussions)** for feature proposals, ideas, or general questions. -- 🐞 Use **[GitHub Issues](https://github.com/untoldengine/UntoldEngine/issues)** for bugs or concrete tasks that need tracking. - -This way, conversations stay organized, visible to the community, and future contributors can benefit from past discussions. - ---- - -Thank you for contributing to the Untold Engine! Following these guidelines will ensure that your work aligns with the project's goals and provides value to users. diff --git a/website/versioned_docs/version-0.10.6/07-Contributor/Formatting.md b/website/versioned_docs/version-0.10.6/07-Contributor/Formatting.md deleted file mode 100644 index 6d45d6214..000000000 --- a/website/versioned_docs/version-0.10.6/07-Contributor/Formatting.md +++ /dev/null @@ -1,105 +0,0 @@ ---- -id: formatting -title: Formatting -sidebar_position: 2 ---- - -# Formatting and Linting - -To maintain a consistent code style across the Untold Engine repo, we use [SwiftFormat](https://github.com/nicklockwood/SwiftFormat). SwiftFormat is a code formatter for Swift that helps enforce Swift style conventions and keep the codebase clean. If you don't have SwiftFormat installed, see the **Installing SwiftFormat** section below. - -## Quick Formatting & Linting - -Navigate to the root directory of Untold Engine and then run the commands below: - -### 🔍 Lint files - -To lint (check) all Swift files without making changes: - -```bash -swiftformat --lint . --swiftversion 5.8 --reporter github-actions-log -``` - -Or, using the Makefile: - -```bash -make lint -``` - -This command runs the same lint configuration as our GitHub Actions workflow and pre-commit hook, ensuring consistent results locally and in CI. - -### ✅ Formatting Files - -To format files: - -```bash -swiftformat --swiftversion 5.8 . -``` - -Alternatively, you can use the Makefile shortcut: - -```bash -make format -``` - -💡 Tip -If the pre-commit hook blocks your commit due to formatting issues, simply run: - -```bash -make format -``` - -then re-stage your changes and try committing again. - -You can bypass the hook temporarily (not recommended) with: - -```bash -git commit --no-verify -``` - -## Installing SwiftFormat - -The simplest way to install SwiftFormat is through the command line. - -1. Install SwiftFormat Using Homebrew: Open the terminal and run the following command: - -```bash -brew install swiftformat -``` -2. Verify Installation: After installation, verify that SwiftFormat is installed correctly by running: - -```bash -swiftformat --version -``` -This should print the installed version of SwiftFormat. - -### Using SwiftFormat - -Format a Single File - -To format a specific Swift file: - -1. Open the terminal and navigate to your project directory. - -2. Run the following command: - -```bash -swiftformat path/to/YourFile.swift -``` -This will format YourFile.swift according to the default rules. - -### Format Multiple Files - -To format all Swift files in your project: - -1. Navigate to your project directory in the terminal. - -2. Run the following command: - -```bash -swiftformat . -``` - -This will recursively format all Swift files in the current directory and its subdirectories. - - diff --git a/website/versioned_docs/version-0.10.6/07-Contributor/_category.json b/website/versioned_docs/version-0.10.6/07-Contributor/_category.json deleted file mode 100644 index 4308c167f..000000000 --- a/website/versioned_docs/version-0.10.6/07-Contributor/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "10-Contributor", "position": 99, "collapsed": false } - diff --git a/website/versioned_docs/version-0.10.6/07-Contributor/versioning.md b/website/versioned_docs/version-0.10.6/07-Contributor/versioning.md deleted file mode 100644 index ddb6c7a9d..000000000 --- a/website/versioned_docs/version-0.10.6/07-Contributor/versioning.md +++ /dev/null @@ -1,16 +0,0 @@ ---- -id: versioning -title: Versioning -sidebar_position: 3 ---- - -# Versioning - -To help us identity the purpose of your commits, make sure to use the following tags in your commit messages. The tags will also automatically increment the the current version when pushed to github. - -- [Patch] - Bug fixes (eg. v1.0.0 -> v1.0.1) -- [Feature] - For new feature that don't break compatibility (eg. v1.0.0 -> v1.1.0) -- [API Change] - For major changes that are not API backward compatible (eg. v1.0.0 -> v2.0.0) - - - diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetBrowserScripts.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetBrowserScripts.png deleted file mode 100644 index 0b523ad75..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetBrowserScripts.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetBrowserView-alt.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetBrowserView-alt.png deleted file mode 100644 index b925157b9..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetBrowserView-alt.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetBrowserView.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetBrowserView.png deleted file mode 100644 index c5cbe12fe..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetBrowserView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetLibraryLoupe.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetLibraryLoupe.png deleted file mode 100644 index 9a4a0c8e3..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorAssetLibraryLoupe.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorBottomShot.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorBottomShot.png deleted file mode 100644 index ed3f3c731..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorBottomShot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorCodeScriptView.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorCodeScriptView.png deleted file mode 100644 index 654da1eb3..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorCodeScriptView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorEffects.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorEffects.png deleted file mode 100644 index d69f37874..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorEffects.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorEnvironment.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorEnvironment.png deleted file mode 100644 index c6f55a224..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorEnvironment.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorInspectorView.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorInspectorView.png deleted file mode 100644 index 04f65dce7..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorInspectorView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorMainShot-alt.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorMainShot-alt.png deleted file mode 100644 index 2ba7138d1..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorMainShot-alt.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorMainShot.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorMainShot.png deleted file mode 100644 index 981a26e45..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorMainShot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorScenegraphView.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorScenegraphView.png deleted file mode 100644 index 87b1cc9c0..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorScenegraphView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorSideShotWide-alt.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorSideShotWide-alt.png deleted file mode 100644 index 42a9c804b..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorSideShotWide-alt.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/EditorSideShotWide.png b/website/versioned_docs/version-0.10.6/images/Editor/EditorSideShotWide.png deleted file mode 100644 index 14ea9d471..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/EditorSideShotWide.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_empty.png b/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_empty.png deleted file mode 100644 index 8dbc28800..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_empty.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_model_import.png b/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_model_import.png deleted file mode 100644 index 310df2be5..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_model_import.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_name.png b/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_name.png deleted file mode 100644 index 09e711f6b..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_name.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_new.png b/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_new.png deleted file mode 100644 index 99bc768bd..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_new.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_xcode.png b/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_xcode.png deleted file mode 100644 index 65b5c908d..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/Editor_scene_xcode.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/ScriptEditorAdd.png b/website/versioned_docs/version-0.10.6/images/Editor/ScriptEditorAdd.png deleted file mode 100644 index f425394bd..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/ScriptEditorAdd.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/Editor/ScriptReload.png b/website/versioned_docs/version-0.10.6/images/Editor/ScriptReload.png deleted file mode 100644 index 79f698548..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/Editor/ScriptReload.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/UntoldEngineGrid.png b/website/versioned_docs/version-0.10.6/images/UntoldEngineGrid.png deleted file mode 100644 index d6790ef10..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/UntoldEngineGrid.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/add-animation-component.png b/website/versioned_docs/version-0.10.6/images/add-animation-component.png deleted file mode 100644 index 4fb63092e..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/add-animation-component.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/add-button-scenegraph.png b/website/versioned_docs/version-0.10.6/images/add-button-scenegraph.png deleted file mode 100644 index 17c6f06a0..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/add-button-scenegraph.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/animation-assign.png b/website/versioned_docs/version-0.10.6/images/animation-assign.png deleted file mode 100644 index 9a36e09ba..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/animation-assign.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/animation-running.png b/website/versioned_docs/version-0.10.6/images/animation-running.png deleted file mode 100644 index 73c84ad72..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/animation-running.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/animationexportblender.png b/website/versioned_docs/version-0.10.6/images/animationexportblender.png deleted file mode 100644 index da6553855..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/animationexportblender.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/asset-browser-folder.png b/website/versioned_docs/version-0.10.6/images/asset-browser-folder.png deleted file mode 100644 index 6f0406ced..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/asset-browser-folder.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/asset-browser-model.png b/website/versioned_docs/version-0.10.6/images/asset-browser-model.png deleted file mode 100644 index 7b293ec8c..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/asset-browser-model.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/asset-browser-usdc-file.png b/website/versioned_docs/version-0.10.6/images/asset-browser-usdc-file.png deleted file mode 100644 index e6d2f0cb8..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/asset-browser-usdc-file.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/camera.png b/website/versioned_docs/version-0.10.6/images/camera.png deleted file mode 100644 index 4fdc106c2..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/camera.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/editor-animation.png b/website/versioned_docs/version-0.10.6/images/editor-animation.png deleted file mode 100644 index d54a8c8e9..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/editor-animation.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/editorscreenshot.png b/website/versioned_docs/version-0.10.6/images/editorscreenshot.png deleted file mode 100644 index 7f78a96d1..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/editorscreenshot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/engine-assetbrowser.png b/website/versioned_docs/version-0.10.6/images/engine-assetbrowser.png deleted file mode 100644 index c58c536c2..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/engine-assetbrowser.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/engine-consolelog.png b/website/versioned_docs/version-0.10.6/images/engine-consolelog.png deleted file mode 100644 index 767e792c4..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/engine-consolelog.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/engine-hdr.png b/website/versioned_docs/version-0.10.6/images/engine-hdr.png deleted file mode 100644 index 273b38545..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/engine-hdr.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/engine-inspector.png b/website/versioned_docs/version-0.10.6/images/engine-inspector.png deleted file mode 100644 index bd778f251..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/engine-inspector.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/engine-lights.png b/website/versioned_docs/version-0.10.6/images/engine-lights.png deleted file mode 100644 index a18833041..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/engine-lights.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/engine-materials.png b/website/versioned_docs/version-0.10.6/images/engine-materials.png deleted file mode 100644 index 1f813dfc8..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/engine-materials.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/engine-post-processing.png b/website/versioned_docs/version-0.10.6/images/engine-post-processing.png deleted file mode 100644 index 3e3f1f32e..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/engine-post-processing.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/engine-scenegraph.png b/website/versioned_docs/version-0.10.6/images/engine-scenegraph.png deleted file mode 100644 index 51e6e51a0..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/engine-scenegraph.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/enginethumbnail.jpg b/website/versioned_docs/version-0.10.6/images/enginethumbnail.jpg deleted file mode 100644 index 882b7a07f..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/enginethumbnail.jpg and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/gamedemoscreenshot.png b/website/versioned_docs/version-0.10.6/images/gamedemoscreenshot.png deleted file mode 100644 index 7f78a96d1..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/gamedemoscreenshot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/howtoexport.png b/website/versioned_docs/version-0.10.6/images/howtoexport.png deleted file mode 100644 index ccd4079c6..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/howtoexport.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/importheader.gif b/website/versioned_docs/version-0.10.6/images/importheader.gif deleted file mode 100644 index 9a1b60a2a..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/importheader.gif and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/inspector.png b/website/versioned_docs/version-0.10.6/images/inspector.png deleted file mode 100644 index 0504eb302..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/inspector.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/linkerissue.png b/website/versioned_docs/version-0.10.6/images/linkerissue.png deleted file mode 100644 index 192c9e5ea..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/linkerissue.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/modelexportblender.png b/website/versioned_docs/version-0.10.6/images/modelexportblender.png deleted file mode 100644 index bbfaa6c49..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/modelexportblender.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/modelsriggedexportblender.png b/website/versioned_docs/version-0.10.6/images/modelsriggedexportblender.png deleted file mode 100644 index 1eb6740e8..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/modelsriggedexportblender.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/render-component.png b/website/versioned_docs/version-0.10.6/images/render-component.png deleted file mode 100644 index d15b36bf2..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/render-component.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/script_component_selection.png b/website/versioned_docs/version-0.10.6/images/script_component_selection.png deleted file mode 100644 index a3a65bcb4..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/script_component_selection.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/script_open_in_xcode.png b/website/versioned_docs/version-0.10.6/images/script_open_in_xcode.png deleted file mode 100644 index 529766292..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/script_open_in_xcode.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/script_properties.png b/website/versioned_docs/version-0.10.6/images/script_properties.png deleted file mode 100644 index 1a926da20..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/script_properties.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/setpathbutton.png b/website/versioned_docs/version-0.10.6/images/setpathbutton.png deleted file mode 100644 index a2ea71d9d..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/setpathbutton.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/top_contributors/MioLogo.png b/website/versioned_docs/version-0.10.6/images/top_contributors/MioLogo.png deleted file mode 100644 index dc0da3039..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/top_contributors/MioLogo.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.6/images/untoldenginewhite.png b/website/versioned_docs/version-0.10.6/images/untoldenginewhite.png deleted file mode 100644 index b7440b8c1..000000000 Binary files a/website/versioned_docs/version-0.10.6/images/untoldenginewhite.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/01-Intro.md b/website/versioned_docs/version-0.10.7/01-Intro.md deleted file mode 100644 index 81b5d6707..000000000 --- a/website/versioned_docs/version-0.10.7/01-Intro.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -slug: /intro ---- - -# Untold Engine Documentation - -Welcome to the **Untold Engine documentation**. - -These docs are the primary reference for working with the Untold Engine ecosystem — whether you are building a game, extending the engine, or contributing to the editor. - ---- - -## What Is Untold Engine? - -![untoldengine](images/Editor/EditorMainShot.png) - -The Untold Engine strives to be a stable, performant, and developer-friendly 3D engine that empowers creativity, removes friction, and makes game development feel effortless for Apple developers - -The Untold Engine is an open-source 3D game engine under active development, designed for macOS, iOS, xrOS platforms. Written in Swift and powered by Metal, its goal is to simplify game creation with a clean, intuitive API. - -While the engine already supports many core systems like rendering, physics, and animation, there’s still much to build and improve. - ---- - -## The Untold Engine Ecosystem - -Untold Engine is delivered through three closely related products: - -### Untold Engine Studio -A downloadable application that includes: -- The Untold Engine runtime -- The Untold Editor -- Built-in tools for scripting, assets, and scene editing - -This is the recommended starting point for most users. - ---- - -### Untold Engine -The core engine runtime. - -This is intended for: -- Engine developers -- Contributors -- Advanced users who want to modify or extend engine systems - -Installation is performed via the command line. - ---- - -### Untold Editor -The editor application built on top of the engine runtime. - -This is intended for: -- Contributors working on editor features -- Developers extending tools and workflows - -The editor uses the same runtime as games, ensuring consistent behavior. - ---- - -## Choose Your Path - -These docs are organized around **how you intend to use the engine**. - -### Game Development -For developers building games using Untold Engine. - -You will learn: -- How to create scenes visually -- How to write game logic (Swift or USC scripts) -- How to work with assets and entities -- How to build and run your game - -Untold Engine supports two approaches for writing gameplay code: -- **Swift in Xcode** (recommended) - Full engine API access -- **USC Scripts** (experimental) - Component-based scripting - -Start here if your goal is to build a game. - ---- - -### Engine Development -For developers who want to understand or extend the engine itself. - -You will learn: -- The engine architecture -- ECS and system execution -- Rendering and simulation internals -- How to contribute new engine features - -Start here if you want to work on the engine runtime. - ---- - -### Editor Development -For contributors working on the Untold Editor. - -You will learn: -- Editor architecture -- Views, tools, and interaction models -- How the editor coordinates with the engine -- How to extend or add editor functionality - -Start here if you want to improve the editor. - ---- - -## Getting Started - -If you are unsure where to begin: - -- New users: **Game Development → Overview** -- Scripting users: **USC → Introduction** -- Contributors: **Engine Development → Architecture** - -Each section is designed to stand on its own. - diff --git a/website/versioned_docs/version-0.10.7/02-Getting Started/02-Installation.md b/website/versioned_docs/version-0.10.7/02-Getting Started/02-Installation.md deleted file mode 100644 index 89e99a7ee..000000000 --- a/website/versioned_docs/version-0.10.7/02-Getting Started/02-Installation.md +++ /dev/null @@ -1,221 +0,0 @@ ---- -id: intro -title: Installation -sidebar_position: 1 ---- - -# Installation - -This page explains how to install **Untold Engine Studio**, the recommended way to get started with Untold Engine. - -Untold Engine Studio is a downloadable app that includes: -- The **Untold Engine** runtime -- The **Untold Editor** for building and editing games - -![editorbottomshot](../images/Editor/EditorBottomShot.png) - -If your goal is to **make games**, this is the only installation you need. - ---- - -## Recommended Installation (Untold Engine Studio) - -### 1. Download - -Download the latest version of **Untold Engine Studio** from the official website: - -[Download Releases](https://github.com/untoldengine/UntoldEditor/releases) - -The download is provided as a `.dmg` file for macOS. - ---- - -### 2. Install - -1. Open the downloaded `.dmg` file -2. Drag **Untold Engine Studio** into your `Applications` folder -3. Launch the app from `Applications` - -No additional setup is required. - ---- - -### 3. First Launch - -On first launch, Untold Engine Studio will: -- Initialize the engine runtime -- Set up the editor environment -- Prompt you to create or open a project - -From here, you can immediately: -- Create scenes visually -- Import 3D models and assets -- Write game logic (Swift in Xcode or USC scripts) -- Build and test your game - ---- - -## System Requirements - -- macOS (Apple Silicon recommended) -- Metal-capable GPU -- Keyboard and mouse - ---- - -## What You Get - -By installing Untold Engine Studio, you get: - -- A complete **game development environment** -- Visual editor for scenes, assets, and scripts -- Full **Untold Engine Swift API** for game logic in Xcode -- **USC scripting system** (experimental component-based scripting) -- Build and run support for macOS, iOS, and visionOS - -You do **not** need to install the engine or editor separately. - -### Two Ways to Write Game Logic - -Untold Engine Studio supports two approaches for writing gameplay code: - -**1. Swift in Xcode (Recommended)** -- Write game logic in `GameScene.swift` using the full Untold Engine API -- Complete control over game systems and performance -- Best for complex games and experienced developers -- Works seamlessly with Xcode debugging and profiling - -**2. USC Scripts (Experimental)** -- Component-based scripting attached to entities -- Write gameplay behaviors in the integrated script editor -- Good for prototyping and simple game mechanics -- API is experimental and subject to change - -You can **use both approaches** in the same project. - ---- - -## Alternative Installation: CLI Workflow - -For **advanced users** or those who prefer a **command-line workflow** without the visual editor, you can install the CLI tools. - -### When to Use CLI - -- You prefer working entirely in Xcode without a visual editor -- You want to script project creation and automation -- You're building tools or integrations on top of UntoldEngine - -### CLI Installation - -**1. Clone the repository:** - -```bash -git clone https://github.com/untoldengine/UntoldEngine.git -cd UntoldEngine -``` - -**2. Install the CLI globally:** - -```bash -./scripts/install-create.sh -``` - -**3. Verify installation:** - -```bash -untoldengine-create --version -untoldengine-create --help -``` - -### CLI Quick Start - -After installing the CLI, create a project from anywhere: - -```bash -# 1. Create project directory -cd ~/anywhere -mkdir MyGame && cd MyGame - -# 2. Create the project -untoldengine-create create MyGame - -# 3. Open in Xcode -open MyGame/MyGame.xcodeproj -``` - -For complete CLI documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - ---- - -### What You Get - -The CLI creates a complete, ready-to-run project: - -- **Xcode project** - Configured and ready to build -- **GameScene.swift** - Your game logic goes here -- **GameViewController.swift** - Renderer and view setup -- **GameData/** directory - All game assets location -- **Platform-specific** files (AppDelegate, Info.plist, etc.) - -### Project Structure - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Open this in Xcode - ├── project.yml # XcodeGen configuration - └── Sources/ - └── MyGame/ - ├── GameData/ # ← Put your assets here - │ ├── Models/ # 3D models - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # USC scripts - │ ├── Textures/ # Images - │ └── ... - ├── GameScene.swift # Your game logic - ├── GameViewController.swift # View controller - └── AppDelegate.swift # App entry point -``` - -### Platform Support - -The CLI supports multiple platforms: - -```bash -# macOS (default) -untoldengine-create create MyGame --platform macos - -# iOS -untoldengine-create create MyGame --platform ios - -# iOS with ARKit -untoldengine-create create MyGame --platform iosar - -# visionOS (Apple Vision Pro) -untoldengine-create create MyGame --platform visionos -``` - -### Development Workflow - -1. **Write code** in GameScene.swift (game logic) -2. **Add assets** to the GameData/ directory -3. **Build & run** in Xcode (Cmd+R) -4. **Iterate** - make changes and rebuild - -For complete CLI documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - ---- - -## Preloaded Assets - -To kickstart development, download prebuilt demo assets: - -- **Models**: Soccer stadium, player, ball, and more -- **Animations**: Running, idle, and other character motions -- **Textures**: Sample materials - -[Download Demo Assets v1.0](https://haroldserrano.gumroad.com/l/iqjlac) - -Extract and copy into your project's `GameData/` directory. - ---- diff --git a/website/versioned_docs/version-0.10.7/02-Getting Started/03-ChoosingYourPath.md b/website/versioned_docs/version-0.10.7/02-Getting Started/03-ChoosingYourPath.md deleted file mode 100644 index 4d003ace7..000000000 --- a/website/versioned_docs/version-0.10.7/02-Getting Started/03-ChoosingYourPath.md +++ /dev/null @@ -1,79 +0,0 @@ -# Choosing Your Path - -Untold Engine supports different types of developers. - -This page helps you choose the path that best matches what you want to do. - ---- - -## I Want to Make a Game - -Choose this path if your goal is to: -- Build gameplay -- Create scenes -- Write scripts -- Ship a game - -### What You’ll Use - -- **Untold Engine Studio** -- **USC scripting API** - -### Where to Start - -> **Game Development → Overview** - -You do not need to understand engine internals to make a game. - ---- - -## I Want to Improve the Engine - -Choose this path if you want to: -- Work on core engine systems -- Improve rendering, physics, or ECS -- Extend platform support - -### What You’ll Use - -- **Untold Engine (core)** -- Command-line tools -- Source builds - -### Where to Start - -> **Engine Development → Overview** - -This path assumes familiarity with engine concepts and systems programming. - ---- - -## I Want to Improve the Editor - -Choose this path if you want to: -- Improve the editor UI -- Add new tools or views -- Improve workflows and usability - -### What You’ll Use - -- **Untold Editor** -- Editor-specific APIs -- Engine integration points - -### Where to Start - -> **Editor Development → Overview** - -Editor development focuses on tooling rather than gameplay. - ---- - -## Not Sure Yet? - -If you’re not sure where to begin, start here: - -> **Game Development → Overview** - -You can always explore the other paths later. - diff --git a/website/versioned_docs/version-0.10.7/02-Getting Started/_category.json b/website/versioned_docs/version-0.10.7/02-Getting Started/_category.json deleted file mode 100644 index ac4e7d573..000000000 --- a/website/versioned_docs/version-0.10.7/02-Getting Started/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "01-Getting Starter", "position": 1, "collapsed": false } - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/000_HelloEditor.md b/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/000_HelloEditor.md deleted file mode 100644 index 02c658707..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/000_HelloEditor.md +++ /dev/null @@ -1,170 +0,0 @@ -# Getting Started with Untold Engine Studio - -This guide will walk you through the basics of getting up and running with **Untold Engine Studio**, from installation to loading your first model. - ---- - -## Downloading Untold Engine Studio - -To start developing with the Untold Engine, download the latest version of **Untold Engine Studio** from the official GitHub releases page: - -👉 https://github.com/untoldengine/UntoldEditor/releases - -Once downloaded: - -1. Drag **Untold Engine Studio** into your **Applications** folder -2. Double-click the app to launch it - ---- - -## Creating a New Project - -When the editor launches, you will be presented with the main startup screen. - -![editor_empty_view](../../images/Editor/Editor_scene_empty.png) - -At this point, create a new project: - -1. Click **New** -2. A new window will appear asking for: - - **Project name** - - **Target platform** - - **Project location** -3. Fill in the details and click **Create** - -Once completed, the engine will generate a fully configured **Xcode game project** for you. - -You will then be prompted to open the project in Xcode. I recommend doing so. - -![editor_scene_xcode](../../images/Editor/Editor_scene_xcode.png) - ---- - -## Exploring the Generated Project - -After opening the project in Xcode, take a moment to explore the file structure. This will help you navigate the project later. - -### Project Structure - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Open this in Xcode - ├── project.yml # XcodeGen configuration - └── Sources/ - └── MyGame/ - ├── GameData/ # ← Put your assets here - │ ├── Models/ # 3D models - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # USC scripts - │ ├── Textures/ # Images - │ └── ... - ├── GameScene.swift # Your game logic - ├── GameViewController.swift # View controller - └── AppDelegate.swift # App entry point -``` - -The most important file to look for is: - -```GameScene.swift``` - -This is where you will do most of your coding. -It contains the core lifecycle functions, including: - -- `init()` – scene setup -- `update()` – per-frame logic - -You’ll be spending most of your time here when writing game logic. - ---- - -## Back to the Editor - -Return to **Untold Engine Studio**. - -If you look at the editor toolbar, you’ll notice the name of your newly created project displayed on the right side of the window. - -At this point, you can already start adding content to your scene. - ---- - -## Adding Entities to the Scene - -You can quickly add a primitive by clicking the **“+”** button in the **Scenegraph View** and selecting a cube or other built-in shapes. - -However, it’s more fun to load real models. - ---- - -## Downloading Sample Models - -I’ve provided a small set of [sample models](https://haroldserrano.gumroad.com/l/iqjlac) you can use to get started: - -- A soccer player -- A soccer ball -- A stadium -- Idle and running animations - -All assets are provided as **`.usdz`** files. - -> **Note** -> Untold Engine currently accepts **USDZ** files only. - -Download the sample assets from the provided link. - ---- - -## Importing Models into Your Project - -Once the models are downloaded, it’s time to import them into your project. - -1. Open the **Asset Browser** -2. Select the **Model** category -3. Click **Import** -4. Navigate to the folder containing your downloaded `.usdz` file -5. Select a model and confirm - -After importing, the engine will display a feedback message confirming the import. -You will also see the `.usdz` file listed under the **Model** category. - ---- - -## Loading a Model into the Scene - -Now for the fun part. - -- **Double-click** the imported `.usdz` file in the Asset Browser - -The model will immediately appear in the editor viewport. - -![Editor_scene_model_viewport](../../images/Editor/Editor_scene_model_viewport.png) - -Behind the scenes, two things just happened: - -1. An **entity** was created -2. The `.usdz` asset was linked to that entity - ---- - -## Importing and Linking Animations - -The same workflow applies to animations. - -1. Select the **Animation** category in the Asset Browser -2. Click **Import** -3. Locate and import an animation `.usdz` file -4. Double-click the animation asset - -The animation will automatically be linked to the currently selected entity. - -If you open the **Inspector** tab, you’ll see the animation listed as part of the entity’s components. - ---- - -## What’s Next? - -Now that you have a basic understanding of the editor, asset workflow, and project structure, you’re ready to start coding with the Untold Engine. - -👉 Continue with the **Hello World** tutorial to write your first gameplay logic. - - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/00_HelloWorld.md b/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/00_HelloWorld.md deleted file mode 100644 index 6c5007b74..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/00_HelloWorld.md +++ /dev/null @@ -1,152 +0,0 @@ -# Hello World - -Your first UntoldEngine program - logging a message every frame. - ---- - -## Overview - -This tutorial shows you how to add custom code to your game's update loop and log output to the console. - ---- - -## Prerequisites - -This tutorial assumes you have: -- Created a project using the 'Untold Engine Studio` or `untoldengine-create` -- Opened the project in Xcode -- Located `GameScene.swift` in your project - ---- - -## Step 1: Open GameScene.swift - -In Xcode, navigate to: - -``` -Sources/YourProjectName/GameScene.swift -``` - ---- - -## Step 2: Add Your First Game Logic - -Find the `update(deltaTime:)` method in `GameScene.swift`: - -```swift path=null start=null -func update(deltaTime: Float) { - // Skip logic if not in game mode - if gameMode == false { return } - - // Add your custom update logic here -} -``` - -Replace the comment with: - -```swift path=null start=null -func update(deltaTime: Float) { - // Skip logic if not in game mode - if gameMode == false { return } - - // Your first game code! 🎉 - Logger.log(message: "Hello World! Delta: \(deltaTime)") -} -``` - ---- - -## Step 3: Build and Run - -Press **Cmd+R** in Xcode. - -Open the **Debug Console** (Cmd+Shift+Y) to see: - -``` -Hello World! Delta: 0.016 -Hello World! Delta: 0.017 -Hello World! Delta: 0.016 -... -``` - -The message appears every frame! 🚀 - ---- - -## What Just Happened? - -### The Update Loop - -`update(deltaTime:)` is called every frame by the engine: - -- **60 FPS** = called 60 times per second -- **deltaTime** = time since last frame (in seconds) - -All game logic goes here: movement, input handling, collision detection, etc. - -### Logging - -```swift path=null start=null -Logger.log(message: "Hello World!") -``` - -Use `Logger.log()` to print debug messages. It's better than `print()` because: -- Engine-aware logging -- Can be filtered/disabled in production -- Consistent formatting - -### Other Logging Methods - -```swift path=null start=null -Logger.logWarning(message: "Something might be wrong") -Logger.logError(message: "Something went wrong!") -``` - ---- - -## Limiting Output (Recommended) - -Logging every frame creates spam. Let's log once per second instead: - -```swift path=null start=null -class GameScene { - var elapsedTime: Float = 0.0 // Add this property - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Accumulate time - elapsedTime += deltaTime - - // Log once per second - if elapsedTime >= 1.0 { - Logger.log(message: "Hello World! One second passed.") - elapsedTime = 0.0 // Reset - } - } -} -``` - -Now you'll see: - -``` -Hello World! One second passed. -Hello World! One second passed. -... -``` - -Much cleaner! - ---- - -## Summary - -You've learned: - -✅ The `update(deltaTime:)` method runs every frame -✅ `deltaTime` is the time between frames -✅ `Logger.log()` prints messages to the console -✅ How to accumulate time for periodic actions - -This is the foundation of game development: **write code that runs every frame**. - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md b/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md deleted file mode 100644 index 94cf91c38..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/01_Transform/01_MoveAnEntityy.md +++ /dev/null @@ -1,214 +0,0 @@ -# Move an Entity - -Learn how to move entities using the Transform System. - ---- - -## Overview - -This tutorial shows you how to: -- Find an entity from a loaded scene -- Move an entity to an absolute position -- Move an entity relative to its current position - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity (created in Untold Engine Studio or loaded via `loadScene()`) -- The entity has a name set in the editor (e.g., "Player") - -For complete API documentation: - -➡️ **[Transform System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingTransformSystem.md)** - ---- - -## Step 1: Find the Entity from Your Scene - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code (setupAssetPaths, loadScene, etc.) ... - startGameSystems() - - // Find the entity by name (set in the editor) - player = findEntity(name: "Player") - - if player == nil { - Logger.logWarning(message: "Player entity not found in scene") - } - } -} -``` - -**Important**: "Player" must match the entity name you set in Untold Engine Studio. - ---- - -## Step 2: Move to an Absolute Position - -Use `translateTo()` to set an entity to a specific world position: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - - player = findEntity(name: "Player") - - // Move player to position (5, 0, -10) - translateTo(entityId: player, position: SIMD3(5.0, 0.0, -10.0)) - } -} -``` - -**Result**: The entity immediately moves to position (5, 0, -10) in world space. - ---- - -## Step 3: Move Relative to Current Position - -Use `translateBy()` to move an entity by an offset: - -```swift path=null start=null -class GameScene { - var player: EntityID! - - init() { - // ... setup code ... - - player = findEntity(name: "Player") - - // Move player 3 units to the right (X-axis) - translateBy(entityId: player, delta: SIMD3(3.0, 0.0, 0.0)) - } -} -``` - -**Result**: The entity moves 3 units along the X-axis from its current position. - ---- - -## Step 4: Continuous Movement in Update Loop - -For smooth movement every frame, use `translateBy()` in `update(deltaTime:)`: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let moveSpeed: Float = 5.0 // Units per second - - init() { - // ... setup code ... - player = findEntity(name: "Player") - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Move forward continuously - let movement = SIMD3(0, 0, -moveSpeed * deltaTime) - translateBy(entityId: player, delta: movement) - } -} -``` - -**Result**: The player moves forward smoothly at 5 units per second. - ---- - -## Understanding Delta Time - -**Why multiply by `deltaTime`?** - -`deltaTime` is the time (in seconds) since the last frame: -- At 60 FPS: `deltaTime ≈ 0.016` seconds -- At 30 FPS: `deltaTime ≈ 0.033` seconds - -By multiplying speed by `deltaTime`, movement becomes **frame-rate independent**: - -```swift path=null start=null -// Without deltaTime (BAD) -translateBy(entityId: player, delta: SIMD3(0, 0, -0.1)) -// Result: Speed varies with frame rate ❌ - -// With deltaTime (GOOD) -let movement = SIMD3(0, 0, -moveSpeed * deltaTime) -translateBy(entityId: player, delta: movement) -// Result: Consistent speed regardless of frame rate ✅ -``` - ---- - -## Movement Examples - -### Move Forward - -```swift path=null start=null -let movement = SIMD3(0, 0, -moveSpeed * deltaTime) -translateBy(entityId: player, delta: movement) -``` - -### Move Right - -```swift path=null start=null -let movement = SIMD3(moveSpeed * deltaTime, 0, 0) -translateBy(entityId: player, delta: movement) -``` - -### Move Up - -```swift path=null start=null -let movement = SIMD3(0, moveSpeed * deltaTime, 0) -translateBy(entityId: player, delta: movement) -``` - -### Move Along Entity's Forward Direction - -```swift path=null start=null -let forward = getForwardAxisVector(entityId: player) -let movement = forward * moveSpeed * deltaTime -translateBy(entityId: player, delta: movement) -``` - ---- - -## Checking Entity Position - -To read an entity's current position: - -```swift path=null start=null -// World position (absolute) -let worldPos = getPosition(entityId: player) -Logger.log(message: "Player world position: \(worldPos)") - -// Local position (relative to parent, if parented) -let localPos = getLocalPosition(entityId: player) -Logger.log(message: "Player local position: \(localPos)") -``` - ---- - -## Summary - -You've learned: - -✅ `findEntity(name:)` - Find entities from loaded scenes -✅ `translateTo()` - Set absolute world position -✅ `translateBy()` - Move relative to current position -✅ `deltaTime` - Make movement frame-rate independent -✅ `getPosition()` - Read current position - ---- - - - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/01_Transform/02_RotateAnEntity.md b/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/01_Transform/02_RotateAnEntity.md deleted file mode 100644 index 1fc40aaaf..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/01_Transform/02_RotateAnEntity.md +++ /dev/null @@ -1,246 +0,0 @@ -# Rotate an Entity - -Learn how to rotate entities using the Transform System. - ---- - -## Overview - -This tutorial shows you how to: -- Rotate an entity to an absolute angle -- Rotate an entity incrementally -- Create smooth rotation using `deltaTime` - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity -- The entity has a name set in the editor (e.g., "Propeller") - -For complete API documentation: - -➡️ **[Transform System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingTransformSystem.md)** - ---- - -## Step 1: Find the Entity from Your Scene - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - - init() { - // ... setup code (setupAssetPaths, loadScene, etc.) ... - startGameSystems() - - // Find the entity by name (set in the editor) - propeller = findEntity(name: "Propeller") - - if propeller == nil { - Logger.logWarning(message: "Propeller entity not found in scene") - } - } -} -``` - ---- - -## Step 2: Rotate to an Absolute Angle - -Use `rotateTo()` to set an entity to a specific rotation: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - - init() { - // ... setup code ... - - propeller = findEntity(name: "Propeller") - - // Rotate 45 degrees around the Y-axis (up) - rotateTo(entityId: propeller, angle: 45.0, axis: SIMD3(0, 1, 0)) - } -} -``` - -**Result**: The entity immediately rotates to 45 degrees around the Y-axis. - ---- - -## Step 3: Rotate Incrementally - -Use `rotateBy()` to add rotation to the current orientation: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - - init() { - // ... setup code ... - - propeller = findEntity(name: "Propeller") - - // Rotate 15 degrees from current rotation - rotateBy(entityId: propeller, angle: 15.0, axis: SIMD3(0, 1, 0)) - } -} -``` - -**Result**: The entity rotates an additional 15 degrees around the Y-axis. - ---- - -## Step 4: Continuous Rotation in Update Loop - -For smooth spinning, use `rotateBy()` in `update(deltaTime:)`: - -```swift path=null start=null -class GameScene { - var propeller: EntityID! - let rotationSpeed: Float = 90.0 // Degrees per second - - init() { - // ... setup code ... - propeller = findEntity(name: "Propeller") - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Rotate continuously around Y-axis - let angleThisFrame = rotationSpeed * deltaTime - rotateBy(entityId: propeller, angle: angleThisFrame, axis: SIMD3(0, 1, 0)) - } -} -``` - -**Result**: The propeller spins smoothly at 90 degrees per second. - ---- - -## Understanding Rotation Axes - -### Common Rotation Axes - -```swift path=null start=null -// Rotate around Y-axis (up) - typical yaw rotation -rotateBy(entityId: entity, angle: 45.0, axis: SIMD3(0, 1, 0)) - -// Rotate around X-axis (right) - pitch rotation -rotateBy(entityId: entity, angle: 45.0, axis: SIMD3(1, 0, 0)) - -// Rotate around Z-axis (forward) - roll rotation -rotateBy(entityId: entity, angle: 45.0, axis: SIMD3(0, 0, 1)) -``` - -### Using Entity's Local Axes - -You can also rotate around an entity's own forward/right/up vectors: - -```swift path=null start=null -// Rotate around entity's own up direction -let up = getUpAxisVector(entityId: entity) -rotateBy(entityId: entity, angle: 45.0, axis: up) - -// Rotate around entity's own right direction -let right = getRightAxisVector(entityId: entity) -rotateBy(entityId: entity, angle: 45.0, axis: right) -``` - ---- - -## Rotation Examples - -### Spin Clockwise (Y-axis) - -```swift path=null start=null -let angle = rotationSpeed * deltaTime -rotateBy(entityId: entity, angle: angle, axis: SIMD3(0, 1, 0)) -``` - -### Spin Counter-Clockwise (Y-axis) - -```swift path=null start=null -let angle = -rotationSpeed * deltaTime // Negative for opposite direction -rotateBy(entityId: entity, angle: angle, axis: SIMD3(0, 1, 0)) -``` - -### Tumble (X-axis) - -```swift path=null start=null -let angle = rotationSpeed * deltaTime -rotateBy(entityId: entity, angle: angle, axis: SIMD3(1, 0, 0)) -``` - -### Face a Direction - -To face a target, you typically calculate the direction and convert to rotation. This is more advanced, but here's a simple Y-axis example: - -```swift path=null start=null -let targetPos = SIMD3(10, 0, 5) -let currentPos = getPosition(entityId: entity) -let direction = normalize(targetPos - currentPos) - -// Calculate angle to target (simplified for Y-axis only) -let angle = atan2(direction.x, direction.z) * (180.0 / .pi) -rotateTo(entityId: entity, angle: angle, axis: SIMD3(0, 1, 0)) -``` - ---- - -## Checking Current Rotation - -To read an entity's current orientation: - -```swift path=null start=null -// World orientation matrix -let worldOrientation = getOrientation(entityId: entity) -Logger.log(message: "World orientation: \(worldOrientation)") - -// Local orientation matrix (relative to parent) -let localOrientation = getLocalOrientation(entityId: entity) -Logger.log(message: "Local orientation: \(localOrientation)") -``` - ---- - -## Combining Translation and Rotation - -You can move and rotate in the same frame: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - // Move forward - let forward = getForwardAxisVector(entityId: player) - let movement = forward * moveSpeed * deltaTime - translateBy(entityId: player, delta: movement) - - // Rotate based on input - let turnAngle = turnSpeed * deltaTime - rotateBy(entityId: player, angle: turnAngle, axis: SIMD3(0, 1, 0)) -} -``` - ---- - -## Summary - -You've learned: - -✅ `rotateTo()` - Set absolute rotation angle -✅ `rotateBy()` - Rotate incrementally from current orientation -✅ `deltaTime` - Make rotation frame-rate independent -✅ Rotation axes - Control rotation direction -✅ `getOrientation()` - Read current rotation - ---- - - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md b/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md deleted file mode 100644 index 3bbd8abc7..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/03_Animation/02_AnimationStateSwitch.md +++ /dev/null @@ -1,319 +0,0 @@ -# Animation State Switching - -Learn how to switch between different animations based on player input and game state. - ---- - -## Overview - -This tutorial shows you how to: -- Create a state-based animation system -- Switch between idle, running, and jumping animations -- Trigger animations based on input - ---- - -## Prerequisites - -This tutorial assumes you have: -- Completed the [Play Animation tutorial](./01_PlayAnimation.md) -- A project with multiple animations loaded (idle, running, jumping) -- An entity with a skeleton that supports animation - -For complete API documentation: - -➡️ **[Animation System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingAnimationSystem.md)** - ---- - -## Step 1: Set Up Animation States - -Define an enum to represent your animation states: - -```swift path=null start=null -enum PlayerState { - case idle - case running - case jumping -} - -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - - init() { - // ... setup code ... - startGameSystems() - - // Register input - InputSystem.shared.registerKeyboardEvents() - - player = findEntity(name: "Player") - - // Load all animations -- ignore if you linked all three animations through the editor. - loadPlayerAnimations() - - // Start with idle - changeAnimation(entityId: player, name: "idle") - } - - func loadPlayerAnimations() { - setEntityAnimations( - entityId: player, - filename: "idle", - withExtension: "usdc", - name: "idle" - ) - - setEntityAnimations( - entityId: player, - filename: "running", - withExtension: "usdc", - name: "running" - ) - - setEntityAnimations( - entityId: player, - filename: "jumping", - withExtension: "usdc", - name: "jumping" - ) - } -} -``` - ---- - -## Step 2: Implement State Switching Logic - -Create a function to handle state transitions: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true // Track if player is on ground - - func update(deltaTime: Float) { - if gameMode == false { return } - - updatePlayerState() - } - - func updatePlayerState() { - let oldState = playerState - - // Determine new state based on input and game conditions - if !isGrounded { - playerState = .jumping - } else if isMovementKeyPressed() { - playerState = .running - } else { - playerState = .idle - } - - // Only change animation if state actually changed - if playerState != oldState { - switchToAnimation(for: playerState) - } - } - - func isMovementKeyPressed() -> Bool { - return inputSystem.keyState.wPressed || - inputSystem.keyState.aPressed || - inputSystem.keyState.sPressed || - inputSystem.keyState.dPressed - } - - func switchToAnimation(for state: PlayerState) { - switch state { - case .idle: - changeAnimation(entityId: player, name: "idle") - Logger.log(message: "Switched to idle animation") - - case .running: - changeAnimation(entityId: player, name: "running") - Logger.log(message: "Switched to running animation") - - case .jumping: - changeAnimation(entityId: player, name: "jumping") - Logger.log(message: "Switched to jumping animation") - } - } -} -``` - ---- - -## Step 3: Add Jump Trigger - -Add space bar input to trigger jumping: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true - var jumpTimer: Float = 0.0 - let jumpDuration: Float = 0.5 // Jump animation duration in seconds - - func update(deltaTime: Float) { - if gameMode == false { return } - - handleJumpInput() - updateJumpTimer(deltaTime: deltaTime) - updatePlayerState() - } - - func handleJumpInput() { - // Trigger jump on space press (only if grounded) - if inputSystem.keyState.spacePressed && isGrounded { - isGrounded = false - jumpTimer = jumpDuration - } - } - - func updateJumpTimer(deltaTime: Float) { - // Count down jump timer - if !isGrounded { - jumpTimer -= deltaTime - - // Land when timer expires - if jumpTimer <= 0.0 { - isGrounded = true - jumpTimer = 0.0 - } - } - } - - func updatePlayerState() { - let oldState = playerState - - // Priority: jumping > running > idle - if !isGrounded { - playerState = .jumping - } else if isMovementKeyPressed() { - playerState = .running - } else { - playerState = .idle - } - - if playerState != oldState { - switchToAnimation(for: playerState) - } - } -} -``` - ---- - -## Step 4: Combine Animation with Movement - -Integrate animation state switching with actual movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - var playerState: PlayerState = .idle - var isGrounded: Bool = true - var jumpTimer: Float = 0.0 - let moveSpeed: Float = 5.0 - let jumpDuration: Float = 0.5 - - func update(deltaTime: Float) { - if gameMode == false { return } - - handleJumpInput() - updateJumpTimer(deltaTime: deltaTime) - updatePlayerState() - updatePlayerMovement(deltaTime: deltaTime) - } - - func updatePlayerMovement(deltaTime: Float) { - var movement = SIMD3(0, 0, 0) - - // Only move if grounded - if isGrounded { - if inputSystem.keyState.wPressed { - movement.z += moveSpeed * deltaTime - } - if inputSystem.keyState.sPressed { - movement.z -= moveSpeed * deltaTime - } - if inputSystem.keyState.aPressed { - movement.x -= moveSpeed * deltaTime - } - if inputSystem.keyState.dPressed { - movement.x += moveSpeed * deltaTime - } - - if movement != SIMD3(0, 0, 0) { - translateBy(entityId: player, delta: movement) - } - } - } -} -``` - ---- - -## Advanced: State Machine Pattern - -For more complex state management, consider using a proper state machine: - -```swift path=null start=null -class AnimationStateMachine { - var currentState: PlayerState = .idle - var entityId: EntityID - - init(entityId: EntityID) { - self.entityId = entityId - } - - func canTransition(to newState: PlayerState) -> Bool { - switch (currentState, newState) { - case (.jumping, .idle), (.jumping, .running): - // Can only exit jumping state when landing - return false - default: - return true - } - } - - func transition(to newState: PlayerState) { - guard canTransition(to: newState) else { return } - - if currentState != newState { - currentState = newState - playAnimation(for: newState) - } - } - - func playAnimation(for state: PlayerState) { - let animationName: String - switch state { - case .idle: animationName = "idle" - case .running: animationName = "running" - case .jumping: animationName = "jumping" - } - - changeAnimation(entityId: entityId, name: animationName) - } -} -``` - ---- - -## Summary - -You've learned: - -✅ Create animation states using enums -✅ Switch animations based on game state -✅ Trigger animations from input -✅ Prevent animation flickering with state checks -✅ Combine animations with movement logic - ---- - - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md b/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md deleted file mode 100644 index ef6850642..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/02-Tutorials/04_Physics/01_ApplyForce.md +++ /dev/null @@ -1,331 +0,0 @@ -# Apply Force - -Learn how to use the Physics System to apply forces and create realistic movement. - ---- - -## Overview - -This tutorial shows you how to: -- Enable physics on an entity -- Configure mass and gravity -- Apply forces for dynamic movement -- Use the Steering System for advanced behaviors - ---- - -## Prerequisites - -This tutorial assumes you have: -- A project with `GameScene.swift` open -- **A scene loaded** with at least one entity -- The entity has a name set in the editor (e.g., "Ball") -- The entity has a kinetic component applied through the editor - -For complete API documentation: - -➡️ **[Physics System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingPhysicsSystem.md)** -➡️ **[Steering System API](../../../04-Engine%20Development/03-Engine%20Systems/UsingSteeringSystem.md)** - ---- - -## Step 1: Find the Entity - -In `GameScene.swift`, add a property to store the entity reference: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - startGameSystems() - - ball = findEntity(name: "Ball") - } -} -``` - ---- - -## Step 2: Enable Physics (Kinetics) - -Before applying forces, enable physics on the entity: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - // Enable physics components - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - } -} -``` - -**What this does**: `setEntityKinetics()` adds `PhysicsComponents` and `KineticComponent` to the entity, enabling it to respond to forces. - ---- - -## Step 3: Configure Mass and Gravity - -Set the entity's mass and gravity scale: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - // Enable physics - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - - // Configure physics properties - setMass(entityId: ball, mass: 0.5) // Lighter = easier to move - setGravityScale(entityId: ball, gravityScale: 1.0) // Normal gravity - } -} -``` - -**Parameters**: -- `mass`: How heavy the entity is. Lower = easier to push. Default is `1.0`. -- `gravityScale`: How much gravity affects it. `0.0` = no gravity, `1.0` = normal gravity. - ---- - -## Step 4: Apply a Force - -Use `applyForce()` to push the entity: - -```swift path=null start=null -class GameScene { - var ball: EntityID! - - init() { - // ... setup code ... - ball = findEntity(name: "Ball") - - setEntityKinetics(entityId: ball) // Ignore this if you linked kinetic component through the editor - setMass(entityId: ball, mass: 0.5) - setGravityScale(entityId: ball, gravityScale: 1.0) - - InputSystem.shared.registerKeyboardEvents() - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Apply forward force when W is pressed - if inputSystem.keyState.wPressed { - applyForce(entityId: ball, force: SIMD3(0.0, 0.0, 5.0)) - } - } -} -``` - -**Result**: When you press W, the ball accelerates forward. - -**Important**: Forces are applied every frame while the key is pressed. The physics system automatically integrates forces into velocity and position. - ---- - -## Understanding Forces vs. Direct Movement - -### Direct Movement (Transform System) - -```swift path=null start=null -// Immediate, precise movement -translateBy(entityId: entity, delta: SIMD3(0, 0, speed * deltaTime)) -``` - -✅ Precise control -✅ No physics overhead -❌ No inertia or momentum -❌ Doesn't interact with physics - -### Force-Based Movement (Physics System) - -```swift path=null start=null -// Gradual acceleration with momentum -applyForce(entityId: entity, force: SIMD3(0, 0, 5.0)) -``` - -✅ Realistic inertia -✅ Physics interactions -✅ Momentum and deceleration -❌ Less precise -❌ Requires tuning - ---- - -## Step 5: Use the Steering System - -For easier physics-based movement, use the Steering System: - -### Steer to a Target Position - -```swift path=null start=null -class GameScene { - var player: EntityID! - let maxSpeed: Float = 5.0 - - init() { - // ... setup code ... - player = findEntity(name: "Player") - - setEntityKinetics(entityId: player) // Ignore this if you linked kinetic component through the editor - setMass(entityId: player, mass: 1.0) - setGravityScale(entityId: player, gravityScale: 0.0) // No gravity for top-down - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - let targetPosition = SIMD3(10.0, 0.0, 5.0) - steerSeek(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) - } -} -``` - -**Result**: The entity smoothly accelerates toward the target position. - ---- - -## Steering System Examples - -### Steer with WASD Keys - -The easiest way to add physics-based movement: - -```swift path=null start=null -class GameScene { - var player: EntityID! - let maxSpeed: Float = 5.0 - - init() { - // ... setup code ... - InputSystem.shared.registerKeyboardEvents() - - player = findEntity(name: "Player") - setEntityKinetics(entityId: player) // Ignore this if you linked kinetic component through the editor - setMass(entityId: player, mass: 1.0) - setGravityScale(entityId: player, gravityScale: 0.0) - } - - func update(deltaTime: Float) { - if gameMode == false { return } - - // Automatic WASD steering - steerWithWASD(entityId: player, maxSpeed: maxSpeed, deltaTime: deltaTime) - } -} -``` - -**Result**: WASD keys apply forces in the corresponding directions with smooth acceleration/deceleration. - -### Flee from a Threat - -```swift path=null start=null -let threatPosition = SIMD3(0.0, 0.0, 0.0) -steerFlee(entityId: player, threatPosition: threatPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Follow a Path - -```swift path=null start=null -let waypoints = [ - SIMD3(0, 0, 0), - SIMD3(5, 0, 0), - SIMD3(5, 0, 5), - SIMD3(0, 0, 5) -] -steerFollowPath(entityId: player, path: waypoints, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Pursue a Moving Target - -```swift path=null start=null -let enemy = findEntity(name: "Enemy") -steerPursuit(entityId: player, targetEntity: enemy!, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Avoid Obstacles - -```swift path=null start=null -let obstacles = [ - findEntity(name: "Rock1")!, - findEntity(name: "Rock2")!, - findEntity(name: "Tree1")! -] -steerAvoidObstacles(entityId: player, obstacles: obstacles, avoidanceRadius: 2.0, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -### Arrive at Target (Slowing Down) - -```swift path=null start=null -let targetPosition = SIMD3(10.0, 0.0, 5.0) -steerArrive(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) -``` - -**Difference from `steerSeek()`**: `steerArrive()` slows down as it approaches the target, preventing overshoot. - ---- - -## Combining Steering Behaviors - -You can use multiple steering behaviors together: - -```swift path=null start=null -func update(deltaTime: Float) { - if gameMode == false { return } - - // Steer toward target while avoiding obstacles - let targetPosition = SIMD3(10.0, 0.0, 5.0) - - steerSeek(entityId: player, targetPosition: targetPosition, maxSpeed: maxSpeed, deltaTime: deltaTime) - - let obstacles = [findEntity(name: "Rock1")!, findEntity(name: "Rock2")!] - steerAvoidObstacles(entityId: player, obstacles: obstacles, avoidanceRadius: 2.0, maxSpeed: maxSpeed, deltaTime: deltaTime) -} -``` - ---- - -## When to Use What? - -### Use Transform System (`translateBy`, `translateTo`) -- Precise, scripted movement -- UI elements or camera -- Platformer-style movement -- When you don't need physics interactions - -### Use Physics System (`applyForce`) -- Projectiles (bullets, grenades) -- Vehicles with custom physics -- When you need low-level control - -### Use Steering System (`steerSeek`, `steerWithWASD`, etc.) -- Character movement in top-down or 3D games -- AI pathfinding and behaviors -- When you want smooth, realistic movement with minimal code - ---- - -## Summary - -You've learned: - -✅ `setEntityKinetics()` - Enable physics on entities -✅ `setMass()` and `setGravityScale()` - Configure physics properties -✅ `applyForce()` - Apply custom forces -✅ `steerWithWASD()` - Easy WASD physics movement -✅ Steering behaviors - Seek, flee, pursue, avoid, arrive -✅ When to use Transform vs. Physics vs. Steering - ---- - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/03-EditorOverview.md b/website/versioned_docs/version-0.10.7/03-Game Development/03-EditorOverview.md deleted file mode 100644 index 191fd6b5f..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/03-EditorOverview.md +++ /dev/null @@ -1,92 +0,0 @@ -# Editor Overview - -The editor is the primary environment for building games with the Untold Engine. - -It is designed to be **explicit, predictable, and tightly integrated** with the engine runtime. -What you see in the editor reflects what runs in the game. - -This page provides a **user-facing overview** of the editor for game developers. - -![editorbottomshot](../images/Editor/EditorBottomShot.png) - ---- - -## Purpose of the Editor - -The editor allows you to: - -- Create and manage scenes -- Inspect and modify entities -- Write and iterate on gameplay scripts -- Manage assets -- Preview runtime behavior in real time - -The editor is not a separate simulation layer — it runs directly on top of the engine runtime. - ---- - -## Core Editor Views - -The editor is composed of several focused views, each with a clear responsibility. - ---- - -### Scene View - -The Scene View is where you visually interact with the world. - -You can: -- Navigate the scene camera -- Select entities -- Translate, rotate, and scale entities -- Preview lighting and scene composition - -This view reflects the engine’s real transform and rendering state. - -![editormainshot](../images/Editor/EditorMainShot.png) - ---- - -### Scene Hierarchy View - -The Scene Hierarchy shows all entities in the current scene. - -It allows you to: -- View parent–child relationships -- Select entities -- Organize scene structure - -The hierarchy mirrors the engine’s internal scene graph. - -![editorscenegraphview](../images/Editor/EditorScenegraphView.png) - ---- - -### Inspector View - -The Inspector displays detailed information about the selected entity. - -From the Inspector, you can: -- View and modify components -- Adjust transforms and properties -- Attach scripts and assets - -Changes made in the Inspector apply directly to the runtime state. - -![editorinspectorview](../images/Editor/EditorInspectorView.png) - ---- - -### Asset Browser View - -The Asset Browser provides access to project assets such as: - -- Models -- Textures -- Scripts -- Scenes - -Assets imported through the editor are organized and made available to the engine automatically. - -![editorassetbrowserview](../images/Editor/EditorAssetBrowserView-alt.png) - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/01-Overview.md b/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/01-Overview.md deleted file mode 100644 index e2ab16284..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/01-Overview.md +++ /dev/null @@ -1,100 +0,0 @@ -# Overview - -⚠️ **EXPERIMENTAL FEATURE** - -This section documents the **UntoldEngine Scripting (USC)** language - an experimental scripting system that is **currently in development**. - ---- - -## Current Status - -USC scripting is in **early experimental phases** and is **not recommended for production use**. - -For game development, we strongly recommend using **Swift** and **Xcode** (see parent section: [Game Development Overview](../01-Overview.md)). - ---- - -## What is USC? - -USC (Untold Scripting Language) is a custom scripting language designed for the UntoldEngine. It aims to provide: - -- Simple C-like syntax -- Entity-component scripting -- Scene-based game logic - -However, the language is still evolving, and **its future direction is under evaluation**. - ---- - -## Why USC? - -Originally, USC was designed to provide: - -1. **Lower barrier to entry** - Simpler syntax than Swift -2. **Scene-embedded scripts** - Scripts attached to entities in the editor -3. **Hot-reload** - Faster iteration for non-compiled changes - -However, **Swift + Xcode** has proven to be more practical for most use cases: - -- Mature tooling (debugging, profiling, autocomplete) -- Type safety and compile-time errors -- Familiar to Apple platform developers -- Better performance - ---- - -## Should You Use USC? - -**No, not yet.** Here's why: - -❌ **Incomplete** - Many features are missing or unstable -❌ **Underdocumented** - Documentation is sparse and may be outdated -❌ **Limited tooling** - No debugger, no IDE support -❌ **Breaking changes** - Syntax and behavior may change without notice -❌ **Unoptimized** - Performance is not production-ready - -Use **Swift** for all game development until USC reaches a stable release. - ---- - -## When Will USC Be Ready? - -USC is in **exploratory development**. We are evaluating: - -- Whether USC provides enough value over Swift -- What the ideal syntax and feature set should be -- How to integrate it seamlessly with Xcode workflows - -There is **no timeline** for USC becoming production-ready. - ---- - -## Exploring USC Anyway? - -If you want to experiment with USC despite the warnings: - -1. Read the **[USC Language Reference](./02-USC/01_Introduction.md)** -2. Try the **[USC Tutorials](./03-Tutorials/00_HelloWorld.md)** -3. Understand that **nothing is guaranteed to work** - -Do NOT use USC for: -- Production games -- Serious projects -- Teaching materials -- Anything you want to maintain long-term - ---- - -## Feedback - -If you experiment with USC, we welcome feedback: - -- What do you like about it? -- What frustrates you? -- Does it solve problems Swift doesn't? -- Would you use it if it were stable? - -Your feedback helps us decide USC's future direction. - ---- - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md b/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md deleted file mode 100644 index 06d6abd30..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/02-USC/01_Introduction.md +++ /dev/null @@ -1,136 +0,0 @@ ---- -id: usc-scripting-introducction -title: Introduction -sidebar_label: Introduction -sidebar_position: 1 ---- - -# Introduction to USC - -USC (Untold Script Core) is the scripting system used to define **gameplay behavior** in Untold Engine. - -USC is designed to be: -- Explicit -- Predictable -- Easy to read and reason about - -It focuses on *what your game does*, not how the engine is implemented. - ---- - -## What USC Is - -USC is a **script-based API** that allows you to attach behavior to entities. - -You use USC to: -- Respond to input -- Move entities -- Control cameras -- Apply forces -- Drive simple gameplay logic - -USC scripts describe *behavior over time*. - ---- - -## What USC Is Not - -USC is **not**: -- A general-purpose programming language -- A replacement for engine code -- A system for complex algorithms or heavy math - -If you need low-level control or complex systems, those belong in the engine itself. - -USC intentionally keeps the surface area small. - ---- - -## Why USC Exists - -Traditional game engines often expose: -- Large, complex APIs -- Implicit behavior -- Hidden update order - -USC takes a different approach. - -It is designed so that: -- Scripts read top-to-bottom -- Behavior is explicit -- There is no hidden execution magic - -This makes gameplay logic easier to understand, debug, and maintain. - ---- - -## How USC Fits into Untold Engine - -USC sits **above the engine** and **below the editor**. - -- The engine provides systems and data -- USC expresses gameplay intent -- The editor helps you attach and manage scripts - -USC does not replace the engine — it builds on top of it. - ---- - -## The Script Lifecycle - -A USC script runs as part of the engine update loop. - -At a high level: -1. The engine updates input -2. USC scripts are evaluated -3. The engine applies the results - -Scripts are evaluated every frame while the game is running. - -You do not manually manage update loops. - ---- - -## Working with Entities - -USC scripts operate on **entities**. - -A script is attached to an entity and can: -- Read entity state -- Modify transforms -- Interact with components -- Respond to input - -Scripts do not create entities or systems — they control behavior. - ---- - -## Example Use Cases - -USC is ideal for: -- Player movement -- Camera follow logic -- Simple physics interaction -- Trigger-based events -- Prototyping gameplay ideas - -If something feels *game-specific*, it likely belongs in USC. - ---- - -## Design Philosophy - -USC follows a few guiding principles: - -- **Explicit over implicit** - No hidden behavior. - -- **Readable over clever** - Scripts should be easy to understand at a glance. - -- **Stable over flexible** - Fewer features, fewer surprises. - -These principles are intentional. - - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md b/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md deleted file mode 100644 index 87ff77797..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/02-USC/03_Workflow.md +++ /dev/null @@ -1,108 +0,0 @@ -# USC Scripting Workflow - -This document describes the **end-to-end workflow** for developing USC scripts in the Untold Engine. - -USC scripts are authored in Xcode. The Untold Editor coordinates assets and playback but does not include a built-in script editor. - ---- - -## Overview - -The USC workflow follows a clear sequence: - -1. Author scripts -2. Register scripts for generation -3. Build scripts -4. Attach scripts to entities -5. Iterate using hot reload - -The editor is the central coordination point for this process, while Xcode is where you author scripts. - ---- - -## Primary Editing Surface - -USC scripts are written in Xcode. Use **Scripts: Open in Xcode** to open the Scripts package; the Untold Editor does not provide an in-editor script editor. - -Xcode provides: -- Direct access to script source files -- Editing of generation entry points -- Build/run feedback and diagnostics - ---- - -## Script Authoring - -Scripts are written as Swift source files using the USC DSL. - -Responsibilities: -- Express gameplay behavior -- React to input and engine state -- Remain independent of engine internals - -Scripts do not: -- Manage entities directly -- Access low-level engine systems -- Perform rendering or physics logic - ---- - -## Script Registration - -All USC scripts are registered through `GenerateScripts.swift`. - -This file: -- Defines which scripts are generated -- Acts as a single source of truth for scripting output -- Is edited directly in Xcode - -Unregistered scripts are ignored by the engine. - ---- - -## Script Generation and Build - -Building and running the GenerateScripts target: -- Runs the USC generation pipeline -- Produces engine-consumable output -- Reports errors and warnings in Xcode - -Builds are explicit and controlled. - ---- - -## Runtime Attachment - -After a successful build: -- Scripts appear as selectable components -- Scripts can be attached to entities via the Inspector -- Multiple entities may share the same script - -The engine controls execution timing. - ---- - -## Hot Reload and Iteration - -USC supports rapid iteration: - -- Edit script -- Build scripts -- Observe changes immediately - -This workflow encourages experimentation and fast feedback. - ---- - -## Design Intent - -The USC workflow is intentionally: - -- Xcode-first for authoring -- Editor-coordinated for playback -- Explicit -- Predictable -- Easy to debug - -It avoids hidden build steps and implicit execution. - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md b/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md deleted file mode 100644 index e1744538d..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/02-USC/04_Scritps.md +++ /dev/null @@ -1,167 +0,0 @@ -# USC Scripts - -This document explains **what a USC script is**, how it fits into the Untold Engine, and the rules that govern its behavior. - -It is intended to give you a **clear mental model** before diving into APIs, examples, or workflows. - -This page does **not** describe how to write scripts step by step. -It explains what scripts *represent* and *why they are designed the way they are*. - ---- - -## What Is a USC Script? - -A **USC script** is a declarative description of gameplay behavior. - -You write scripts in Swift using a constrained, fluent DSL, and the engine executes those scripts at runtime as part of its update loop. - -A script expresses **intent**, not implementation details. - -Examples of intent: -- Move an entity when input is received -- Apply forces in response to events -- Adjust camera behavior over time - -The engine owns *how* those intentions are executed. - ---- - -## Scripts and Entities - -USC scripts are **attached to entities**. - -- A script always operates in the context of the entity it is attached to -- Scripts do not own entities -- Scripts do not create or destroy entities - -Multiple entities may share the same script definition, each with its own execution context and script-local state. - ---- - -## Execution Model - -Scripts participate in the engine’s execution model. - -Key characteristics: - -- Scripts run when their declared events are triggered -- The engine controls execution order and timing -- Scripts do not manage threads or scheduling -- Execution is deterministic and engine-driven - -A script never decides *when* it runs — it only declares *what should happen* when it does. - ---- - -## Script Lifecycle - -At a high level, a USC script goes through the following lifecycle: - -1. Script is authored -2. Script is registered and generated -3. Script is attached to an entity -4. Script becomes active -5. Script executes in response to events -6. Script stops executing when detached or when the entity is destroyed - -Lifecycle transitions are managed by the engine. - ---- - -## Events and Entry Points - -Scripts are organized around **events**, also called entry points. - -Examples include: -- Startup events -- Per-frame updates -- Custom or engine-driven events - -Each event defines **when a block of instructions executes**. - -Scripts may contain multiple event handlers, each expressing a different behavior. - ---- - -## Script Context - -Every script executes with a **context** provided by the engine. - -The context gives controlled access to: -- The attached entity’s properties -- Script-defined variables -- Engine-provided values (such as delta time or input state) - -Scripts never access engine state directly — all access flows through this context. - ---- - -## State and Variables - -Scripts may store local state using variables. - -- Variables are scoped to the script instance -- Each entity gets its own variable set -- Variables persist across executions unless reset - -Script variables are designed for **lightweight gameplay state**, not long-term data storage. - ---- - -## What Scripts Can Do - -USC scripts are designed to express common gameplay behaviors, such as: - -- Reading input -- Modifying transforms -- Applying forces or impulses -- Steering entities -- Controlling cameras -- Triggering animations -- Reacting to events - -Scripts focus on *what should happen*, not *how systems work internally*. - ---- - -## What Scripts Cannot Do - -USC scripts intentionally **cannot**: - -- Create or destroy entities -- Access rendering pipelines or GPU resources -- Manage memory -- Spawn threads -- Call arbitrary engine internals -- Control execution order or threading - -These constraints are deliberate and central to USC’s design. - ---- - -## Design Intent - -USC exists to provide: - -- Predictable gameplay behavior -- Clear ownership of state -- Safe boundaries between game logic and engine internals -- Fast iteration and easy debugging - -By limiting what scripts can do, the engine remains stable and behavior remains easy to reason about. - ---- - -## Relationship to the API Reference - -This document explains **what a script is**. - -For detailed information about: -- Available events -- Instructions and commands -- Math operations -- Input handling -- Physics and camera helpers - -See **USC → API Reference**. - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md b/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md deleted file mode 100644 index 57cf2d07e..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/01_MoveAnEntity.md +++ /dev/null @@ -1,235 +0,0 @@ -# Move an Entity - Basic Translation - -**What you'll learn:** -- Moving an entity with `setProperty(.position)` -- Using `simd_float3` direction vectors -- Controlling speed with script variables -- Updating position every frame with `onUpdate()` - -**Time:** ~7 minutes - -**Prerequisites:** -- Untold Engine Studio installed and a scene with any entity (a cube works great) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple movement script that: -1. Moves an entity steadily along a chosen axis -2. Uses variables to control speed and direction -3. Logs a message when movement begins - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `MoveAnEntity` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateMoveAnEntity(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateMoveAnEntity(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateMoveAnEntity) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateMoveAnEntity(to dir: URL) { - let script = buildScript(name: "MoveAnEntity") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("moveSpeed", to: 0.05) // units per frame - .setVariable("direction", to: simd_float3(x: 0, y: 0, z: 1)) - .log("MoveAnEntity ready") - - // Runs every frame - s.onUpdate() - .getProperty(.position, as: "currentPos") - .scaleVec3("direction", by: "moveSpeed", as: "step") - .addVec3("currentPos", "step", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") - } - - let outputPath = dir.appendingPathComponent("MoveAnEntity.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ MoveAnEntity.uscript") - } -} -``` - -### Understanding the Code - -**`moveSpeed` + `direction`** - Control how fast and where to move -- Speed is a scalar; direction is a vector (x, y, z) -- Change either without touching the rest of the script - -**`getProperty(.position, as:)`** - Reads the current position -- Positions are `simd_float3` values - -**`scaleVec3()` + `addVec3()`** - Build the new position -- Scales the direction by speed -- Adds it to the current position for smooth motion - -**`setProperty(.position, toVariable:)`** - Applies the updated position -- Runs every frame inside `onUpdate()` -- Keeps the movement continuous while Play mode runs - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ MoveAnEntity.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 4: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select any entity in your scene (a cube or platform) -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `MoveAnEntity.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 5: Test It! - -1. Click **Play** in the toolbar -2. Watch the entity move steadily along the +Z axis -3. Check the **Console** view to confirm: - ``` - MoveAnEntity ready - ``` -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- The entity moves every frame toward +Z -- Speed comes from `moveSpeed`; direction comes from `direction` -- To stop movement, disable the Script Component or set speed to 0 - -⚠️ **Placement Note:** If the entity is already near a boundary, lower the speed or adjust the start position to avoid moving out of view. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Move Upward Instead -```swift -.setVariable("direction", to: simd_float3(x: 0, y: 1, z: 0)) -``` - -### Slow or Fast Motion -```swift -.setVariable("moveSpeed", to: 0.01) // slower -// or -.setVariable("moveSpeed", to: 0.15) // faster -``` - -### Pause After a Distance -```swift -s.onUpdate() - .getProperty(.position, as: "currentPos") - .ifGreater("currentPos.z", than: 10.0) { n in - n.setVariable("moveSpeed", to: 0.0) // stop after z > 10 - } - .scaleVec3("direction", by: "moveSpeed", as: "step") - .addVec3("currentPos", "step", as: "nextPos") - .setProperty(.position, toVariable: "nextPos") -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Creating a movement script in Untold Engine Studio -✅ Using variables for speed and direction -✅ Updating position every frame with `onUpdate()` -✅ Building and attaching scripts to entities -✅ Testing motion in Play mode - ---- - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/02_RotateAnEntity.md b/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/02_RotateAnEntity.md deleted file mode 100644 index 7e92ccbb5..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/03-Tutorials/01_Transform/02_RotateAnEntity.md +++ /dev/null @@ -1,239 +0,0 @@ -# Rotate an Entity - Continuous Spin - -**What you'll learn:** -- Rotating entities with `rotateTo()` inside `onUpdate()` -- Tracking angles with script variables -- Choosing rotation axes with `simd_float3` -- Building USC scripts entirely in Xcode - -**Time:** ~7 minutes - -**Prerequisites:** -- Untold Engine Studio open with any entity selected (a cube makes rotation easy to see) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A rotation script that: -1. Spins an entity around a chosen axis -2. Uses a variable to track the current angle -3. Lets you adjust spin speed without rewriting the script - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `RotateAnEntity` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateRotateAnEntity(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generateRotateAnEntity(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generateRotateAnEntity) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generateRotateAnEntity(to dir: URL) { - let script = buildScript(name: "RotateAnEntity") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("rotationSpeed", to: 2.0) // degrees per frame - .setVariable("currentAngle", to: 0.0) - .setVariable("axis", to: simd_float3(x: 0, y: 1, z: 0)) - .log("RotateAnEntity ready") - - // Runs every frame - s.onUpdate() - .addFloat("currentAngle", "rotationSpeed", as: "nextAngle") - .setVariable("currentAngle", to: .variableRef("nextAngle")) - .rotateTo( - degrees: .variableRef("currentAngle"), - axis: simd_float3(x:0,y:1,z:0) - ) - } - - let outputPath = dir.appendingPathComponent("RotateAnEntity.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ RotateAnEntity.uscript") - } -} -``` - -### Understanding the Code - -**`rotationSpeed`** - Controls how much to rotate each frame -- Higher values spin faster -- Change this without touching the rest of the script - -**`currentAngle`** - Tracks total rotation -- Prevents drift and keeps rotation smooth - -**`axis`** - A `simd_float3` defining which way to spin -- `(0, 1, 0)` spins around Y (like a turntable) -- `(1, 0, 0)` spins forward/backward -- `(0, 0, 1)` spins left/right - -**`rotateTo()`** - Sets an absolute rotation -- Uses the stored angle each frame -- Cleanly resets if you set `currentAngle` back to `0` - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ RotateAnEntity.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select the entity you want to rotate -3. In the Inspector panel, click **Add Component** → **Script Component** -4. In the Asset Browser, find `RotateAnEntity.uscript` under Scripts/Generated -5. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. Watch the entity spin around the Y-axis -3. Open the **Console** view to confirm: - ``` - RotateAnEntity ready - ``` -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- The entity rotates smoothly using `rotationSpeed` degrees per frame -- Changing `axis` changes the spin direction without new code -- Resetting `currentAngle` to `0` realigns the entity instantly - -⚠️ **Consistency Note:** Large `rotationSpeed` values can cause visible jumps. Increase gradually for smoother motion. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Rotate with `rotateBy()` -```swift -s.onUpdate() - .rotateBy( - degrees: 1.0, - axis: simd_float3(x: 0, y: 1, z: 0) - ) -``` - -### Spin on Multiple Axes -```swift -.setVariable("axis", to: simd_float3(x: 1, y: 1, z: 0)) // diagonal spin -``` - -### Start Facing a Specific Direction -```swift -s.onStart() - .rotateTo( - degrees: 45.0, - axis: simd_float3(x: 0, y: 1, z: 0) - ) - .setVariable("currentAngle", to: 45.0) -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Creating a rotation script in Untold Engine Studio -✅ Tracking angles with script variables -✅ Rotating entities around any axis -✅ Building and attaching scripts to entities -✅ Testing rotations in Play mode - ---- - diff --git a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md b/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md deleted file mode 100644 index 6542e635f..000000000 --- a/website/versioned_docs/version-0.10.7/03-Game Development/04-Scripting-Experimental/03-Tutorials/03_Animation/01_PlayAnimation.md +++ /dev/null @@ -1,241 +0,0 @@ -# Play an Animation - Single Clip - -**What you'll learn:** -- Playing an animation clip with `playAnimation()` -- Looping vs. one-shot playback -- Using variables to track the current clip -- Building and testing animations from Xcode - -**Time:** ~8 minutes - -**Prerequisites:** -- Untold Engine Studio open with an entity that has an **Animation Component** and at least one loaded clip (e.g., `idle`) -- Access to Xcode via **Scripts: Open in Xcode** (toolbar button) - ---- - -## What We're Building - -A simple animation script that: -1. Plays a single clip on start -2. Loops it continuously -3. Shows how to trigger a one-shot clip on demand - ---- - -## Step 1: Create the Script - -1. Open **Untold Engine Studio** -2. In the toolbar, click **Scripts: Open in Xcode** (blue button) -3. Click the `+` button and enter the script name: `PlayAnimation` -4. Click OK, then Xcode opens the Scripts package - -When the script is created: -- The source file is added to your project -- You edit the script in Xcode - -You'll see a template like this: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generatePlayAnimation(to dir: URL) { - // Write your script here - } -} -``` -> This script is added to GenerateScripts, which is the entry point the engine uses to discover and generate USC scripts. - ---- - - -## Step 2: Wire Up the Script - -⚠️ IMPORTANT MANUAL STEP - -The editor created your script file, but you need to tell the build system to generate it. - -1. Click Scripts: Open in Xcode (blue button in toolbar) -2. In Xcode, open GenerateScripts.swift -3. Add your script to the main() function: - -```swift -@main -struct GenerateScripts { - static func main() { - print("🔨 Generating USC scripts...") - - let outputDir = URL(fileURLWithPath: "Generated/") - try? FileManager.default.createDirectory(at: outputDir, withIntermediateDirectories: true) - - // Add this line: - generatePlayAnimation(to: outputDir) - - print("✅ All scripts generated in Generated/") - } -} -``` - -Now run the GenerateScripts target (`Cmd+R`) to generate the `.uscript` files. - - -MPORTANT NOTES: -The function name (e.g. generatePlayAnimation) MUST match the tutorial’s script -Only adjust that single line per tutorial -The structure, wording, and warning must remain consistent across all documents - ---- - - -## Step 3: Write the Script - -Replace the function with this complete script: - -```swift -import Foundation -import UntoldEngine -import simd - -extension GenerateScripts { - static func generatePlayAnimation(to dir: URL) { - let script = buildScript(name: "PlayAnimation") { s in - // Runs once when the entity starts - s.onStart() - .setVariable("idleClip", to: "idle") // Must match the clip name in the Animation Component - .setVariable("currentClip", to: "idle") - .playAnimation("idle", loop: true) - .log("Playing idle animation (looping)") - - // Optional: trigger a one-shot animation with Space - s.onUpdate() - .getKeyState("space", as: "spacePressed") - .ifEqual("spacePressed", to: true) { n in - n.playAnimation("jump", loop: false) // Replace with your one-shot clip name - n.setVariable("currentClip", to: "jump") - n.log("Playing jump animation (one-shot)") - } - } - - let outputPath = dir.appendingPathComponent("PlayAnimation.uscript") - try? saveUSCScript(script, to: outputPath) - print(" ✅ PlayAnimation.uscript") - } -} -``` - -### Understanding the Code - -**`playAnimation(name, loop:)`** - Starts an animation by name -- `loop: true` repeats continuously (great for idle or walk) -- `loop: false` plays once (great for jump or attacks) - -**Clip names** - Must match exactly what you see in the Animation Component -- Case-sensitive -- If you rename a clip in the editor, update the script too - -**`currentClip` variable** - Tracks what is playing -- Useful when adding more conditions later - ---- - -## Step 4: Build the Script - -1. In Xcode, press `Cmd+R` to run the GenerateScripts target and generate the `.uscript` (optional: `Cmd+B` first to verify the build). -2. Watch for the Xcode run output: - -``` -🔨 Generating USC scripts... - ✅ PlayAnimation.uscript -✅ All scripts generated in Generated/ -``` - -**First build?** May take 30-60 seconds to download engine dependencies. Subsequent builds are much faster. - ---- - -## Step 5: Attach to an Entity - -1. Return to **Untold Engine Studio** -2. Select an entity that has an **Animation Component** -3. In the Asset Library, double click on the animations you want to add to the entity, i.e. `idle` and `jump` -4. In the Inspector panel, click **Add Component** → **Script Component** -5. In the Asset Browser, find `PlayAnimation.uscript` under Scripts/Generated -6. Double click on the `.uscript`. The script will be linked to the entity ---- - -## Step 6: Test It! - -1. Click **Play** in the toolbar -2. The entity should immediately play the `idle` clip on loop -3. Tap **Space** to trigger the one-shot clip (e.g., `jump`) -4. Click **Stop** to exit Play mode - ---- - -## Understanding the Output - -- Looping clip runs continuously until another clip interrupts it -- One-shot clip plays once; if you need to return to idle automatically, add a timer or state check -- If nothing plays, verify the clip names match the Animation Component exactly - -⚠️ **Asset Note:** The script only calls animations that already exist on the entity’s Animation Component. Ensure clips are loaded and named correctly. - ---- - -## Modify and Experiment - -Try these changes to learn more: - -### Start with a Different Clip -```swift -.setVariable("idleClip", to: "breathing_idle") -.playAnimation("breathing_idle", loop: true) -``` - -### Return to Idle After One-Shot -```swift -s.onUpdate() - .getKeyState("space", as: "spacePressed") - .ifEqual("spacePressed", to: true) { n in - n.playAnimation("jump", loop: false) - n.setVariable("currentClip", to: "jump") - } - // Simple timer to return to idle after ~1 second - .setVariable("returnTimer", to: 60.0) // frames at ~60 FPS - .ifGreater("returnTimer", than: 0.0) { n in - n.addFloat("returnTimer", literal: -1.0, as: "returnTimer") - } - .ifEqual("returnTimer", to: 0.0) { n in - n.playAnimation("idle", loop: true) - n.setVariable("currentClip", to: "idle") - } -``` - -### Add a Stop Button -```swift -.getKeyState("p", as: "pPressed") -.ifEqual("pPressed", to: true) { n in - n.stopAnimation() - n.log("Animation stopped") -} -``` - -After making changes: -1. In Xcode, press `Cmd+R` to rerun the GenerateScripts target and regenerate the scripts (`Cmd+B` optional first). -2. Click **Reload** in the Script Component Inspector -3. Test in Play mode - ---- - -## What You Learned - -✅ Playing animation clips with `playAnimation()` -✅ Looping vs. one-shot playback -✅ Tracking the current clip with script variables -✅ Building, attaching, and testing animation scripts - ---- - diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/02-Architecture/Internals.md b/website/versioned_docs/version-0.10.7/04-Engine Development/02-Architecture/Internals.md deleted file mode 100644 index 0d5c089d4..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/02-Architecture/Internals.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: engine-architecture -title: Engine Architecture Internals -sidebar_position: 2 ---- - -# Untold Engine Architecture Internals - -You’re looking under the hood of Untold Engine. This is how the pieces fit together, why we picked them, and where contributors can plug in. - -## The Big Idea: ECS at the Core -Untold Engine is deliberately **ECS-first**: -- **Entities** are just 64-bit IDs (index + version) managed in `Scenes.swift` with pooling, masks, and tombstones. -- **Components** are plain data (no logic) in `ECS/Components.swift`: transforms, render payloads, physics state, animation sets, lights, scripts, etc. Components live in type-specific pools keyed by component ID. -- **Systems** are the behavior layer in `Sources/UntoldEngine/Systems/`. Each system queries entities by component mask and runs every frame (or in fixed steps for physics). Examples: Transform, Scenegraph, Rendering, Physics, Animation, Input, Culling, Loading, Lighting, Shadow, Steering, USC scripting. - -Why this matters: contributors can add data (components) and behavior (systems) without rewriting the core. Keep components dumb, keep systems focused, and everything stays modular and testable. - -## Frame Flow -`UntoldRenderer.runFrame` orchestrates each tick: -1) **Simulation prep**: delta time, scene graph traversal, input handling. -2) **Gameplay & scripting**: AnimationSystem, USCSystem, custom game update callbacks. -3) **Physics**: fixed-timestep accumulator, gravity/drag/forces, Runge–Kutta integration (collision/contact still open for contribution). -4) **Culling & rendering**: frustum cull, Gaussian depth/sort, build render graph, execute passes, present. - -## Rendering Stack -- **Entry point**: `UntoldRenderer` (MTKView delegate) sets up device/queue, loads metallib, initializes buffers, and drives the frame loop. Platform variants exist for macOS/iOS, visionOS (XR), and AR. -- **Render graph**: `RenderingSystem.swift` builds a dependency graph (environment/grid/AR base → shadow → GBuffer/model → gaussian → post → precomp) and executes via `RenderPasses` + `PipelineManager`. -- **Pipelines**: Render and compute pipelines live in `Renderer/Pipelines/`. Gaussian splats run dedicated compute passes (depth + bitonic sort) before their render pass. - -## Physics & Motion -- **Data**: `PhysicsComponents` and `KineticComponent` store mass, velocity/angular velocity, drag, inertia tensors, forces, moments, and pause flags. -- **Runtime**: `updatePhysicsSystem` accumulates forces/moments, applies gravity/drag, and integrates with Runge–Kutta. Collision/contact resolution is intentionally minimal today—prime territory for contributions. -- **Steering & Animation**: SteeringSystem and AnimationSystem run alongside physics to drive motion and skeletal playback. - -## Scripting (USC) -- **Data**: `ScriptComponent` holds one or more scripts plus file paths (backward compatible with single-script scenes). -- **Runtime**: `USCSystem` ticks the `USCInterpreter` each frame. Actions are registered in `USCScripting.swift` (math helpers, etc.). Extend the DSL by adding actions to `USCActionRegistry`. - -## Scenes, Assets, Resources -- **Scene authoring**: Swift DSL in `Scenes/Builder/` (`Node`/`SceneBuilder`) plus `SceneSerializer` for persistence. -- **Meshes/animations**: abstractions in `Mesh/` and `Skeleton`; resources created through MTK allocators/loaders. -- **Bundled bits**: Prebuilt metallibs per platform and demo resources under `Resources/`. - -## Platform Layers -- **XR (visionOS)**: `UntoldEngineXR` ties CompositorServices/ARKit to the core renderer entry. -- **AR (iOS)**: `UntoldEngineAR` wraps MTKView + ARKit for AR mode. -- **Sample**: `Sources/DemoGame` shows system registration and simple gameplay loops. - -## Where Contributors Can Make Impact -- **Collision/contact & determinism**: Build the missing collision stack, material/friction models, and deterministic stepping for netcode/replays. -- **Render passes/pipelines**: Add or refine passes in the render graph (effects, optimizations, new pipelines). -- **USC actions & API surface**: Expose new engine capabilities to scripts; add higher-level gameplay helpers. -- **Debug/observability**: Better logging sinks, on-screen overlays, profiling, and error surfacing. -- **New systems/components**: AI utilities, networking hooks, gameplay-specific data—keep components data-only and register them for serialization where relevant. - -When proposing big changes, consider ECS storage impact, system ordering, and cross-platform Metal constraints. Align early with maintainers for collision/determinism/netcode-sized work so we keep the architecture cohesive. diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/02-Architecture/Overview.md b/website/versioned_docs/version-0.10.7/04-Engine Development/02-Architecture/Overview.md deleted file mode 100644 index ae33b9cd6..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/02-Architecture/Overview.md +++ /dev/null @@ -1,191 +0,0 @@ ---- -id: engine-architecture overview -title: Architecture Overview -sidebar_position: 1 ---- - -# Engine Architecture - -This document describes the **high-level architecture** of Untold Engine. - -It is intended for developers who want to understand how the engine is structured *before* working on individual subsystems such as rendering, physics, or scripting. - -This page focuses on **concepts and responsibilities**, not file layouts or implementation details. - ---- - -## Architectural Philosophy - -Untold Engine is designed with a small set of guiding principles: - -- **Clarity over abstraction** - Systems should be understandable without deep indirection. - -- **Explicit execution order** - Engine behavior is deterministic and visible. - -- **Data-oriented design** - Data and behavior are separated cleanly. - -- **Composable systems** - New systems can be added without destabilizing existing ones. - -The architecture favors *learnability and debuggability* over convenience shortcuts. - ---- - -## High-Level Structure - -At a conceptual level, Untold Engine is organized into the following layers: - -Gameplay (USC Scripts) -↓ -Engine Systems -↓ -Core Runtime -↓ -Platform & Rendering Backends - -Each layer has a single responsibility and communicates downward through well-defined interfaces. - ---- - -## Core Runtime - -The **core runtime** is responsible for: - -- Entity and component storage -- System registration and ordering -- Scene lifecycle management -- Global engine state -- Frame orchestration - -The runtime does **not** contain gameplay logic, editor logic, or platform-specific behavior. - -Its role is to provide a **stable execution environment** for all systems. - ---- - -## Entity–Component–System (ECS) - -Untold Engine uses an **Entity–Component–System (ECS)** architecture. - -### Entities -- Lightweight identifiers -- Contain no data or behavior -- Used to associate components - -### Components -- Plain data containers -- Represent state (transform, physics, rendering, scripting, etc.) -- Do not contain logic - -### Systems -- Contain all behavior -- Operate on entities matching specific component sets -- Are executed in an explicit order - -This separation keeps systems focused and minimizes hidden dependencies. - ---- - -## System Execution Model - -Systems are executed as part of a well-defined update flow. - -A simplified frame looks like this: - -1. Input collection -2. Simulation and gameplay systems -3. USC script evaluation -4. Physics integration -5. Culling and render preparation -6. Rendering and presentation - -There is no implicit or callback-driven execution. - -Each system declares: -- When it runs -- What data it reads -- What data it writes - ---- - -## Rendering Architecture - -Rendering in Untold Engine is **explicit and staged**. - -Key characteristics: -- Clear separation between simulation and rendering -- Explicit render passes -- Minimal global render state -- Platform-specific backends hidden behind a thin abstraction - -Rendering is treated as a system, not a special case. - ---- - -## Platform Abstraction - -Untold Engine supports multiple platforms through **thin platform layers**. - -These layers handle: -- Window and surface management -- Input sources -- Graphics API integration -- Platform lifecycle events - -The core engine remains platform-agnostic. - ---- - -## USC Integration - -USC (Untold Script Core) is layered on top of the engine. - -- The engine owns the update loop -- USC expresses gameplay intent -- Scripts interact with engine state through a constrained API - -USC does not bypass engine systems. - -This ensures predictable behavior and keeps ownership of state clear. - ---- - -## Editor Relationship - -The Untold Editor is built **on top of the same engine runtime** used by games. - -- Editor features are implemented as engine clients -- Editor-only systems are isolated -- Play mode uses the same execution path as runtime builds - -This minimizes editor-only behavior and keeps debugging consistent. - ---- - -## Design Tradeoffs - -Untold Engine intentionally avoids: - -- Hidden execution order -- Large global managers -- Overly generic abstractions -- Implicit engine magic - -These tradeoffs prioritize: -- Predictability -- Debuggability -- Long-term maintainability - ---- - -## Architecture in Practice - -This document provides the conceptual overview. - -For implementation-level details, see: - -Engine Development → Architecture → Engine internals - diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/02-Architecture/_category.json b/website/versioned_docs/version-0.10.7/04-Engine Development/02-Architecture/_category.json deleted file mode 100644 index b32af10ff..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/02-Architecture/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Architecture", "position": 2, "collapsed": false } diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingAnimationSystem.md b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingAnimationSystem.md deleted file mode 100644 index e0530058e..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingAnimationSystem.md +++ /dev/null @@ -1,78 +0,0 @@ ---- -id: animationsystem -title: Animation System -sidebar_position: 4 ---- - -# Enabling Animation in Untold Engine - -The Untold Engine simplifies adding animations to your rigged models, allowing for lifelike movement and dynamic interactions. This guide will show you how to set up and play animations for a rigged model. - - -## How to Enable Animation - -### Step 1: Create an Entity - -Start by creating an entity to represent your animated model. - -```swift -let redPlayer = createEntity() -``` - ---- - -### Step 2: Link the Mesh to the Entity - -Load your rigged model’s .usdc file and link it to the entity. This step ensures the entity is visually represented in the scene. - -```swift -setEntityMesh(entityId: redPlayer, filename: "redplayer", withExtension: "usdc", flip: false) -``` ->>> Note: If your model renders with the wrong orientation, set the flip parameter to false. - ---- - -### Step 3: Load the Animation -Load the animation data for your model by providing the animation .usdc file and a name to reference the animation later. - -```swift -setEntityAnimations(entityId: redPlayer, filename: "running", withExtension: "usdc", name: "running") -``` - ---- - -### Step 4: Set the Animation to play - -Trigger the animation by referencing its name. This will set the animation to play on the entity. - -```swift -changeAnimation(entityId: redPlayer, name: "running") -``` - ---- - -### Step 5. Pause the animation (Optional) - -To pause the current animation, simply call the following function. The animation component will be paused for the current entity. - -```swift -pauseAnimationComponent(entityId: redPlayer, isPaused: true) -``` - ---- - -### Running the Animation - -Once the animation is set up: - -1. Run the project: Your model will appear in the game window. -2. Click on "Play" to enter Game Mode: -- The model will play the assigned animation in real time. - ---- - -## Tips and Best Practices - -- Name Animations Clearly: Use descriptive names like "running" or "jumping" to make it easier to manage multiple animations. -- Debug Orientation Issues: If the model’s animation appears misaligned, revisit the flip parameter or check the model’s export settings. -- Combine Animations: For complex behaviors, load multiple animations (e.g., walking, idle, jumping) and switch between them dynamically. diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingCameraSystem.md b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingCameraSystem.md deleted file mode 100644 index 3f4b480de..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingCameraSystem.md +++ /dev/null @@ -1,144 +0,0 @@ ---- -id: camerasystem -title: Camera System -sidebar_position: 10 ---- - -# Using the Camera System - -This document explains how to move, rotate, and control cameras using the APIs in `CameraSystem.swift`. - -## Get the Game Camera - -For gameplay, always use the game camera (not the editor/scene camera). Call `findGameCamera()` and make it active: - -```swift -let camera = findGameCamera() -CameraSystem.shared.activeCamera = camera -``` - -If no game camera exists, `findGameCamera()` creates one and sets it up with default values. - -## Translate (Move) the Camera - -Use absolute or relative movement: - -```swift -// Absolute position -moveCameraTo(entityId: camera, 0.0, 3.0, 7.0) - -// Relative movement in camera local space -cameraMoveBy(entityId: camera, delta: simd_float3(0.0, 0.0, -1.0), space: .local) - -// Relative movement in world space -cameraMoveBy(entityId: camera, delta: simd_float3(1.0, 0.0, 0.0), space: .world) -``` - -## Rotate the Camera - -Use `rotateCamera` for pitch/yaw rotation, or `cameraLookAt` to aim at a target. - -```swift -// Rotate by pitch/yaw (radians), with optional sensitivity -rotateCamera(entityId: camera, pitch: 0.02, yaw: 0.01, sensitivity: 1.0) - -// Look-at orientation -cameraLookAt( - entityId: camera, - eye: simd_float3(0.0, 3.0, 7.0), - target: simd_float3(0.0, 0.0, 0.0), - up: simd_float3(0.0, 1.0, 0.0) -) -``` - -## Camera Follow - -Follow a target entity with a fixed offset. You can optionally smooth the motion. - -```swift -let target = findEntity(name: "player") ?? createEntity() -let offset = simd_float3(0.0, 2.0, 6.0) - -// Instant follow -cameraFollow(entityId: camera, targetEntity: target, offset: offset) - -// Smoothed follow -cameraFollow(entityId: camera, targetEntity: target, offset: offset, smoothFactor: 6.0, deltaTime: deltaTime) -``` - -### Dead-Zone Follow - -`cameraFollowDeadZone` only moves the camera when the target leaves a box around it. This is useful for platformers and shoulder cameras. - -```swift -let deadZone = simd_float3(1.0, 0.5, 1.0) -cameraFollowDeadZone( - entityId: camera, - targetEntity: target, - offset: offset, - deadZoneExtents: deadZone, - smoothFactor: 6.0, - deltaTime: deltaTime -) -``` - -## Camera Path Following - -The camera path system moves the active camera through a sequence of waypoints with smooth interpolation. - -### Start a Path - -```swift -let waypoints = [ - CameraWaypoint( - position: simd_float3(0, 5, 10), - rotation: simd_quatf(angle: 0, axis: simd_float3(0, 1, 0)), - segmentDuration: 2.0 - ), - CameraWaypoint( - position: simd_float3(10, 5, 10), - rotation: simd_quatf(angle: Float.pi / 4, axis: simd_float3(0, 1, 0)), - segmentDuration: 2.0 - ) -] - -startCameraPath(waypoints: waypoints, mode: .once) -``` - -You can also build waypoints that look at a target: - -```swift -let waypoint = CameraWaypoint( - position: simd_float3(0, 5, 10), - lookAt: simd_float3(0, 0, 0), - up: simd_float3(0, 1, 0), - segmentDuration: 2.0 -) -``` - -### Update Every Frame - -Call `updateCameraPath(deltaTime:)` from your main update loop: - -```swift -func update(deltaTime: Float) { - updateCameraPath(deltaTime: deltaTime) -} -``` - -### Looping and Completion - -```swift -startCameraPath(waypoints: waypoints, mode: .loop) - -let settings = CameraPathSettings(startImmediately: true) { - print("Camera path completed") -} -startCameraPath(waypoints: waypoints, mode: .once, settings: settings) -``` - -## Notes - -- `startCameraPath` and `updateCameraPath` operate on `CameraSystem.shared.activeCamera`. -- `segmentDuration` is the time to move from the current waypoint to the next. -- For gameplay, always acquire the camera with `findGameCamera()` and set it active before path playback or follow logic. diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingInputSystem.md b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingInputSystem.md deleted file mode 100644 index 1c73ff69d..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingInputSystem.md +++ /dev/null @@ -1,131 +0,0 @@ ---- -id: inputsystem -title: Input System -sidebar_position: 6 ---- - -# Using the Input System in Untold Engine - -The Input System in the Untold Engine allows you to detect user inputs, such as keystrokes and mouse movements, to control entities and interact with the game. This guide will explain how to use the Input System effectively. - - -## How to Use the Input System (Keyboard) - -### Step 1: Detect Keystrokes - -To detect if a specific key is pressed, use the keyState object from the Input System. - -Example: Detecting the 'W' Key - -```swift -func init(){ -// Make sure that you have enabled keyevents in your init function: -InputSystem.shared.registerKeyboardEvents() -} - -// Then in the handleInput callback, you can do this: - -func handleInput() { - // Skip logic if not in game mode - if gameMode == false { return } - - let inputSystem = InputSystem.shared - - // Handle input here - if inputSystem.keyState.wPressed{ - Logger.log(message: "w pressed") - } -} -``` -You can use the same logic for other keys like A, S, and D: - -```swift -let inputSystem = InputSystem.shared - -if inputSystem.keyState.aPressed == true { - // Move left -} - -if inputSystem.keyState.sPressed == true { - // Move backward -} - -if inputSystem.keyState.dPressed == true { - // Move right -} -``` - -###Step 2: Using Input to Control Entities - -Here’s an example function that moves a car entity based on keyboard inputs: - -```swift -func moveCar(entityId: EntityID, dt: Float) { - - let inputSystem = InputSystem.shared - - // Ensure we are in game mode - if gameMode == false { - return - } - - var position = simd_float3(0.0, 0.0, 0.0) - - // Move forward - if inputSystem.keyState.wPressed == true { - position.z += 1.0 * dt - } - - // Move backward - if inputSystem.keyState.sPressed == true { - position.z -= 1.0 * dt - } - - // Move left - if inputSystem.keyState.aPressed == true { - position.x -= 1.0 * dt - } - - // Move right - if inputSystem.keyState.dPressed == true { - position.x += 1.0 * dt - } - - // Apply the translation to the entity - translateTo(entityId: entityId, position: position) -} -``` - -## How to Use the Input System with a Game Controller - -To detect if a specific button is pressed, use the gameControllerState object from the Input System. - -Example: Detecting the 'A' button - -```swift -func init(){ -// Make sure that you have enabled game controller events in your init function: - InputSystem.shared.registerGameControllerEvents() -} - -// Then in the handleInput callback, you can do this: - -func handleInput() { - // Skip logic if not in game mode - if gameMode == false { return } - let inputSystem = InputSystem.shared - - // Handle input here - if inputSystem.gameControllerState.aPressed { - Logger.log(message: "Pressed A key") - } -} -``` - ---- - -## Tips and Best Practices -- Debouncing: If you want to execute an action only once per key press, track the key's previous state to avoid repeated triggers. -- Game Mode Check: Always ensure the game is in the appropriate mode (e.g., Game Mode) before processing inputs. -- Smooth Movement: Use dt (delta time) to ensure frame-rate-independent movement. - diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingPhysicsSystem.md b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingPhysicsSystem.md deleted file mode 100644 index fd8e53d91..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingPhysicsSystem.md +++ /dev/null @@ -1,100 +0,0 @@ ---- -id: physicssystem -title: Physics System -sidebar_position: 5 ---- - -# Enabling Physics System in Untold Engine - -The physics system in the Untold Engine enables realistic simulations such as gravity, forces, and dynamic interactions. While collision support is still under development, this guide will walk you through adding physics to your entities. - -## How to Enable Physics - -### Step 1: Create an Entity - -Start by creating an entity that represents the object you want to add physics to. - -```swift -let redPlayer = createEntity() -``` ---- - -### Step 2: Link a Mesh to the Entity -Next, load your model’s mesh file and link it to the entity. This step visually represents your entity in the scene. - -```swift -setEntityMesh(entityId: redPlayer, filename: "redplayer", withExtension: "usdc") -``` ---- - -### Step 3: Enable Physics on the Entity -Activate the physics simulation for your entity using the setEntityKinetics function. This function prepares the entity for movement and dynamic interaction. - -```swift -setEntityKinetics(entityId: redPlayer) -``` ---- - -#### Step 4: Configure Physics Properties -You can customize the entity’s physics behavior by defining its mass and gravity scale: - -- Mass: Determines the force needed to move the object. Heavier objects require more force. -- Gravity Scale: Controls how strongly gravity affects the entity (default is 0.0). - -```swift -setMass(entityId: redPlayer, mass: 0.5) -setGravityScale(entityId: redPlayer, gravityScale: 1.0) -``` ---- - -#### Step 5: Apply Forces (Optional) -You can apply a custom force to the entity for dynamic movement. This is useful for simulating actions like jumps or pushes. - -```swift -applyForce(entityId: redPlayer, force: simd_float3(0.0, 0.0, 5.0)) -``` - -> Note: Forces are applied per frame. To avoid unintended behavior, only apply forces when necessary. - ---- - -#### Step 6: Use the Steering System -For advanced movement behaviors, leverage the Steering System to steer entities toward or away from targets. This system automatically calculates the required forces. - -Example: Steering Toward a Position - -```swift -steerTo(entityId: redPlayer, targetPosition: simd_float3(0.0, 0.0, 5.0), maxSpeed: 2.0, deltaTime: deltaTime) -``` - ---- - -#### Additional Steering Functions - -The Steering System includes other useful behaviors, such as: - -- steerAway() -- steerPursuit() -- followPath() - -These functions simplify complex movement patterns, making them easy to implement. - ---- - -### What Happens Behind the Scenes? - -1. Physics Simulation: -- Entities with physics enabled are updated each frame to account for forces, gravity, and other dynamic factors. -- Transformations are recalculated based on velocity, acceleration, and forces applied. -2. Realistic Motion: -- The system ensures consistent, physics-based movement without manual updates to the transform. - ---- - -### Running the Simulation -Once you've set up physics, run the project to see it in action: - -1. Launch the project: Your model will appear in the game window. -2. Press "P" to enter Game Mode: -- Gravity and forces will affect the entity. -- If forces are applied, you’ll see dynamic motion in real time. diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingRenderingSystem.md b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingRenderingSystem.md deleted file mode 100644 index c42d5ae1a..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingRenderingSystem.md +++ /dev/null @@ -1,77 +0,0 @@ ---- -id: renderingsystem -title: Rendering System -sidebar_position: 2 ---- - -# Enabling Rendering System in Untold Engine - -The Rendering System in the Untold Engine is responsible for displaying your models on the screen. It supports advanced features such as Physically Based Rendering (PBR) for realistic visuals and multiple types of lights to illuminate your scenes. - -## How to Enable the Rendering System - -### Step 1: Create an Entity - -Start by creating an entity that represents your 3D object. - -```swift -let entity = createEntity() -``` ---- - -### Step 2: Link a Mesh to the Entity - -To display a model, load its .usdc file and link it to the entity using setEntityMesh. - -```swift -setEntityMesh(entityId: entity, filename: "entity", withExtension: "usdz") -``` - -Parameters: - -- entityId: The ID of the entity created earlier. -- filename: The name of the .usdc file (without the extension). -- withExtension: The file extension, typically "usdz". - -> Note: If PBR textures (e.g., albedo, normal, roughness, metallic maps) are included, the rendering system will automatically use the appropriate PBR shader to render the model with realistic lighting and material properties. - ---- - -### Running the Rendering System - -Once everything is set up: - -1. Run the project. -2. Your model will appear in the game window, illuminated by the configured lights. -3. If the model is not visible or appears flat, revisit the lighting and texture setup to ensure everything is loaded correctly. - ---- - -## Common Issues and Fixes - -#### Issue: My Model Isn’t Visible! - -- Cause: The scene lacks a light source. -- Solution: Add a directional or point light as shown above. Lighting is required to render objects visibly. - -#### Issue: Model Appears Flat or Dull - -- Cause: PBR textures are missing or not linked properly. -- Solution: Ensure the .usdc file includes the correct PBR textures, and verify their paths during the loading process. - -#### Debugging Tip: - -- Log the addition of lights and entities to verify the scene setup. -- Ensure the position of the point light is within the visible range of the camera and the objects it is meant to illuminate. - ---- - -### Tips and Best Practices - -- Combine Light Types: Use directional lights for overall scene lighting and point lights for localized effects. -- Use PBR Materials: Provide high-quality PBR textures for realistic rendering. -- Position Lights Intelligently: Place point lights strategically to highlight key areas without excessive overlap. - ---- - - diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingSpatialInput.md b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingSpatialInput.md deleted file mode 100644 index 5d7aaa10c..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingSpatialInput.md +++ /dev/null @@ -1,275 +0,0 @@ ---- -id: spatialinput -title: Spatial Input VisionPro -sidebar_position: 13 ---- - -# Spatial Input (vision Pro) - -Spatial input in Untold Engine follows a simple pipeline: - -1. visionOS emits raw spatial events. -2. UntoldEngineXR converts each event into an XRSpatialInputSnapshot. -3. Snapshots are queued in InputSystem. -4. XRSpatialGestureRecognizer processes snapshots each frame. -5. The engine publishes a single XRSpatialInputState your game reads in handleInput(). - -That separation keeps the system flexible: the OS-facing code stays in UntoldEngineXR, while gesture classification stays in -the recognizer. - -## What You Get in Game Code - -From XRSpatialInputState, you can read: - -- spatialTapActive -- spatialDragActive -- spatialPinchActive -- spatialPinchDragDelta -- spatialZoomActive + spatialZoomDelta -- spatialRotateActive + spatialRotateDeltaRadians -- pickedEntityId - -So your game logic can stay focused on behavior (select, move, rotate, scale), not event parsing. - -## Important Setup Step - -You must enable XR event ingestion: - -InputSystem.shared.registerXREvents() - -If you skip this, the callback still receives OS events, but the engine ignores them. - -## Typical Frame Usage - -In your handleInput(): - -- Poll InputSystem.shared.xrSpatialInputState. -- React to edge-triggered gestures like tap. -- Apply continuous updates for drag/zoom/rotate while active. - -For object manipulation, use SpatialManipulationSystem for robust pinch-driven transforms, then layer custom behavior on top -when needed. - -## Quick Example - -This example shows how to drag and rotate a mesh using the engine: - -``` swift -func handleInput() { - if gameMode == false { return } - - let state = InputSystem.shared.xrSpatialInputState - - if state.spatialTapActive, let entityId = state.pickedEntityId { - Logger.log(message: "Tapped entity: \(entityId)") - } - - // Handles drag-based translate + twist rotation on picked entity - SpatialManipulationSystem.shared.processPinchTransformLifecycle(from: state) -} -``` - -### What This Does - -- **Tap** → selects entity (via raycast picking) -- **Pinch + Drag** → translates entity in world space -- **Pinch + Twist** → rotates entity around a computed axis - -`processPinchTransformLifecycle` handles: - -- Begin -- Update -- End -- Cancel - -This lifecycle model prevents stuck manipulation sessions. - ------------------------------------------------------------------------- - -### Manipulate Parent Instead Of Picked Child - -If ray picking hits a child mesh and you want to manipulate the parent -actor: - -``` swift -var state = InputSystem.shared.xrSpatialInputState - -if let picked = state.pickedEntityId, - let parent = getEntityParent(entityId: picked) { - state.pickedEntityId = parent -} - -SpatialManipulationSystem.shared.processPinchTransformLifecycle(from: state) -``` - -This is useful when: - -- A character has multiple meshes -- A building has sub-meshes -- You want to move the root actor instead of individual geometry - pieces - ------------------------------------------------------------------------- - -### Important Note - -Do not early-return only because `pickedEntityId == nil` before calling -lifecycle processing. - -End/cancel phases must still propagate to properly close manipulation -sessions.\ -Failing to do so can leave the engine in an inconsistent transform -state. - ------------------------------------------------------------------------- - -# Raw Gesture Examples - -It is strongly recommended to use the Spatial Helper functions instead -of raw gesture access. - -Raw access is useful when: - -- You want custom manipulation behavior -- You are building a custom editor -- You want non-standard gesture responses - ------------------------------------------------------------------------- - -## Tap (Selection) - -Vision Pro air-tap gesture. - -``` swift -let state = InputSystem.shared.xrSpatialInputState -if state.spatialTapActive, let entityId = state.pickedEntityId { - // selectEntity(entityId) -} -``` - -Use this to: - -- Select objects -- Trigger UI -- Activate gameplay logic - ------------------------------------------------------------------------- - -## Pinch Active - -Single-hand pinch detected. - -``` swift -if InputSystem.shared.hasSpatialPinch() { - // pinch is active -} -``` - -This does **not** imply dragging yet --- only that a pinch is currently -held. - ------------------------------------------------------------------------- - -## Pinch Position - -World-space position of pinch. - -``` swift -if let pinchPosition = InputSystem.shared.getPinchPosition() { - // use pinchPosition -} -``` - -Useful for: - -- Placing objects -- Spawning actors -- Visual debugging - ------------------------------------------------------------------------- - -## Pinch Drag Delta - -Drag delta while pinch is active. - -``` swift -let state = InputSystem.shared.xrSpatialInputState -if state.spatialPinchActive { - let dragDelta = InputSystem.shared.getPinchDragDelta() - // app-defined translation/scaling response -} -``` - -Common use cases: - -- Translate object along plane -- Move UI panels -- Drag actors in world space - ------------------------------------------------------------------------- - -## Two-Hand Zoom Signal (Coming soon) - -Two hands pinching and moving closer/farther. - -``` swift -let state = InputSystem.shared.xrSpatialInputState -if state.leftHandPinching, state.rightHandPinching, state.spatialZoomActive { - let zoomDelta = InputSystem.shared.getSpatialZoomDelta() - // app-defined zoom response -} -``` - -### Typical Behavior Options - -You decide what zoom means: - -- Scale selected object -- Move object closer/farther in world space -- Adjust camera rig distance -- Modify FOV (if using custom projection control) - -Untold Engine does not automatically change camera FOV.\ -You define the semantic meaning of zoom. - ------------------------------------------------------------------------- - -## Two-Hand Rotate Signal (Coming soon) - -Two hands pinching and rotating around each other. - -``` swift -let state = InputSystem.shared.xrSpatialInputState -if state.leftHandPinching, state.rightHandPinching, state.spatialRotateActive { - let deltaRadians = InputSystem.shared.getSpatialRotateDelta() - let axisWorld = InputSystem.shared.getSpatialRotateAxisWorld() - // app-defined rotate response -} -``` - -Typical usage: - -- Rotate object in world space -- Rotate parent actor -- Rotate UI panel in 3D - -`axisWorld` allows you to apply physically intuitive rotations rather -than arbitrary axes. - ------------------------------------------------------------------------- - -# Spatial Helper Functions - -Use these helpers from `SpatialManipulationSystem.shared`: - -- `processPinchTransformLifecycle(from:)`\ - Recommended default. Handles translation + twist rotation lifecycle - safely. - -- `applyPinchDragIfNeeded(from:entityId:sensitivity:)`\ - Lower-level translation helper if you want full control. - -- `applyTwoHandZoomIfNeeded(from:sensitivity:)`\ - Provides zoom delta signal. You must define what zoom means in your - app. - diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingStaticBatchingSystem.md b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingStaticBatchingSystem.md deleted file mode 100644 index 672578390..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingStaticBatchingSystem.md +++ /dev/null @@ -1,342 +0,0 @@ ---- -id: staticbatchingsystem -title: Static Batching System -sidebar_position: 11 ---- - -# Static Batching System - Usage Guide - -The Untold Engine provides a static batching system that dramatically reduces draw calls by combining static (non-moving) geometry into optimized batches. - -> **Choose Your Path:** You can set up Static Batching via the **Editor** (no code required) or **programmatically** in Swift. - ---- - -## Using the Editor - -### Step 1: Mark Entities as Static - -1. **Select an entity** with a Render Component in the Scene Hierarchy -2. In the **Inspector**, find the **"Static Batching"** section -3. Toggle **"Mark as Static"** (or "Mark Children as Static" for parent entities) - -### Step 2: Enable the Batching System - -1. Open the **Static Batching panel** in the editor sidebar -2. Toggle **"Enable Batching"** to ON - -### Step 3: Generate Batches - -1. Click **"Generate Batches"** -2. A success message will appear -3. The **Active Batches** count shows how many batch groups were created - -### Managing Batches - -- **Clear Batches**: Removes all generated batches -- **Regenerate**: Click "Generate Batches" again after marking new entities - -### Important Notes - -- **Moving a static entity** automatically removes it from batching and regenerates batches -- Batches are grouped by **material** — objects with the same material are combined -- You can mark/unmark entities as static at any time, then regenerate - ---- - -## Using Code - -### Quick Start - -### Basic Static Batching Setup - -```swift -// Create entities -let cube1 = createEntity() -setEntityMesh(entityId: cube1, filename: "cube", withExtension: "usdz") -translateTo(entityId: cube1, position: simd_float3(0, 0, 0)) - -let cube2 = createEntity() -setEntityMesh(entityId: cube2, filename: "cube", withExtension: "usdz") -translateTo(entityId: cube2, position: simd_float3(2, 0, 0)) - -let cube3 = createEntity() -setEntityMesh(entityId: cube3, filename: "cube", withExtension: "usdz") -translateTo(entityId: cube3, position: simd_float3(4, 0, 0)) - -// Mark entities as static -setEntityStaticBatchComponent(entityId: cube1) -setEntityStaticBatchComponent(entityId: cube2) -setEntityStaticBatchComponent(entityId: cube3) - -// Enable batching and generate batches -enableBatching(true) -generateBatches() -``` - -**How it works:** -- Static entities are marked for batching -- `generateBatches()` combines entities with the same material into batch groups -- Rendering system uses batched draw calls instead of per-entity calls - -### With Async Mesh Loading (Recommended) - -For better performance, use async loading and enable batching in the completion handler: - -```swift -let stadium = createEntity() -setEntityMeshAsync(entityId: stadium, filename: "stadium", withExtension: "usdz") { success in - if success { - print("Scene loaded successfully") - - // Mark as static AFTER mesh is loaded - setEntityStaticBatchComponent(entityId: stadium) - - // Enable batching system - enableBatching(true) - - // Generate batches - generateBatches() - } -} -``` - -**Important:** Always call `setEntityStaticBatchComponent()` **after** the mesh loads successfully, then enable and generate batches. - -### Multi-Mesh Assets (USDZ with Multiple Objects) - -For USDZ files with multiple meshes (like a building with walls, roof, windows): - -```swift -let building = createEntity() -setEntityMeshAsync(entityId: building, filename: "office_building", withExtension: "usdz") { success in - if success { - // Mark parent entity - automatically marks all children as static - setEntityStaticBatchComponent(entityId: building) - - enableBatching(true) - generateBatches() - } -} -``` - -**How it works:** `setEntityStaticBatchComponent()` recursively marks the parent and all children, so the entire building is batched. - -## API Reference - -### Core Functions - -#### `setEntityStaticBatchComponent(entityId:)` -Marks an entity (and all its children) as static for batching. - -```swift -setEntityStaticBatchComponent(entityId: entity) -``` - -**Note:** Entity must have a `RenderComponent` (i.e., mesh must be loaded). - -#### `removeEntityStaticBatchComponent(entityId:)` -Removes static batching from an entity (and all its children). - -```swift -removeEntityStaticBatchComponent(entityId: entity) -``` - -**Use case:** If you need to move a previously static object. - -#### `enableBatching(_:)` -Globally enables or disables the batching system. - -```swift -enableBatching(true) // Enable batching -enableBatching(false) // Disable batching -``` - -#### `isBatchingEnabled() -> Bool` -Checks if batching is currently enabled. - -```swift -if isBatchingEnabled() { - print("Batching is active") -} -``` - -#### `generateBatches()` -Generates batch groups from all entities marked as static. - -```swift -generateBatches() -``` - -**Important:** Call this after marking entities as static and enabling batching. - -#### `clearSceneBatches()` -Clears all generated batches. - -```swift -clearSceneBatches() -``` - -**Use case:** When loading a new scene or reconfiguring static geometry. - -## Complete Workflow Examples - -### Example 1: Multiple Static Objects - -```swift -import UntoldEngine - -// Create multiple static props -var props: [EntityID] = [] - -for i in 0..<50 { - let rock = createEntity() - setEntityName(entityId: rock, name: "Rock_\(i)") - - // Load mesh - setEntityMesh(entityId: rock, filename: "rock", withExtension: "usdz") - - // Position randomly - let x = Float.random(in: -20...20) - let z = Float.random(in: -20...20) - translateTo(entityId: rock, position: simd_float3(x, 0, z)) - - // Mark as static - setEntityStaticBatchComponent(entityId: rock) - - props.append(rock) -} - -// Enable and generate batches -enableBatching(true) -generateBatches() - -print("Batched \(props.count) rocks") -``` - -### Example 2: Scene Loading with Batching - -```swift -// Load scene from file -if let sceneData = loadGameScene(from: sceneURL) { - deserializeScene(sceneData: sceneData) - - // Scene automatically restores StaticBatchComponent for marked entities - // Enable batching and generate - enableBatching(true) - generateBatches() - - print("Scene loaded with batching enabled") -} -``` - -### Example 3: Dynamic Scene with Mixed Objects - -```swift -// Static environment -let ground = createEntity() -setEntityMesh(entityId: ground, filename: "ground_plane", withExtension: "usdz") -setEntityStaticBatchComponent(entityId: ground) - -let walls = createEntity() -setEntityMesh(entityId: walls, filename: "walls", withExtension: "usdz") -setEntityStaticBatchComponent(entityId: walls) - -// Dynamic objects (NOT marked as static) -let player = createEntity() -setEntityMesh(entityId: player, filename: "character", withExtension: "usdz") -// Do NOT call setEntityStaticBatchComponent for moving objects - -let enemy = createEntity() -setEntityMesh(entityId: enemy, filename: "enemy", withExtension: "usdz") -// Enemies move, so no static batching - -// Enable batching (only affects static entities) -enableBatching(true) -generateBatches() -``` - -### Example 4: Large Async Scene Loading - -```swift -let cityBlock = createEntity() -setEntityMeshAsync(entityId: cityBlock, filename: "city_block", withExtension: "usdz") { success in - if success { - print("City block loaded with all buildings") - - // Mark entire hierarchy as static - setEntityStaticBatchComponent(entityId: cityBlock) - - // Enable batching system - enableBatching(true) - - // Generate batches - generateBatches() - - print("Static batching enabled - draw calls optimized") - } else { - print("Failed to load city block") - } -} -``` ---- - -## Best Practices - -### What to Mark as Static -✅ **Good candidates:** -- Environment geometry (walls, floors, ceilings) -- Props that never move (rocks, trees, furniture) -- Buildings and structures -- Terrain meshes -- Static decorations - -❌ **Bad candidates:** -- Characters and NPCs -- Vehicles -- Projectiles -- Animated objects -- UI elements - -### Batching Requirements -For entities to batch together, they must have: -- ✅ Same material (textures, colors) -- ✅ `StaticBatchComponent` marked -- ✅ Valid `RenderComponent` (mesh loaded) - -Entities with different materials will be in separate batch groups. - -### Performance Tips - -1. **Mark entities AFTER mesh loading:** - ```swift - setEntityMeshAsync(...) { success in - setEntityStaticBatchComponent(entityId: entity) // ✅ Correct timing - } - ``` - -2. **Enable batching once per scene:** - ```swift - // Game initialization or scene load - enableBatching(true) - generateBatches() - ``` - -3. **Group entities by material:** - - Entities with the same material batch better - - Reduce material variations for better batching - -4. **Regenerate batches when needed:** - ```swift - // When adding/removing static entities - clearSceneBatches() - generateBatches() - ``` - -## Limitations - -- **No dynamic batching:** Only works for static geometry -- **Transform baked:** Entity positions are baked into batch geometry -- **Material grouping:** Different materials create separate batches -- **No skeletal meshes:** Animated/skinned meshes cannot be batched - diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingSteeringSystem.md b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingSteeringSystem.md deleted file mode 100644 index 05c8465be..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingSteeringSystem.md +++ /dev/null @@ -1,107 +0,0 @@ ---- -id: steeringsystem -title: Steering System -sidebar_position: 7 ---- - -# Using the Steering System in Untold Engine - -The Steering System in the Untold Engine enables entities to move dynamically and intelligently within the scene. It provides both low-level steering behaviors (e.g., seek, flee, arrive) for granular control and high-level behaviors (e.g., steerTo, steerAway, followPath) that integrate seamlessly with the Physics System. - -## Why Use the Steering System? - -The Steering System is essential for creating dynamic and realistic movement for entities, such as: - -- A character chasing a target. -- An enemy avoiding obstacles. -- A vehicle following a predefined path. - -The high-level behaviors are recommended because they are designed to work closely with the Physics System, simplifying implementation while maintaining smooth motion. - ---- - -## How to Use the Steering System - -Examples: - -1. Steer Toward a Target Position: - -```swift -steerSeek(entityId: entity, targetPosition: targetPosition, maxSpeed: 5.0, deltaTime: 0.016) -``` -2. Steer Away from a Threat: - -```swift -steerFlee(entityId: entity, threatPosition: threatPosition, maxSpeed: 5.0, deltaTime: 0.016) -``` - -3. Follow a Path: Guide an entity along a series of waypoints. - -```swift -steerFollowPath(entityId: entity, path: waypoints, maxSpeed: 5.0, deltaTime: 0.016) -``` -4. Pursue a Moving Target: - -```swift -steerPursuit(entityId: chaserEntity, targetEntity: targetEntity, maxSpeed: 5.0, deltaTime: 0.016) -``` - -5. Avoid Obstacles: - -```swift -steerAvoidObstacles(entityId: entity, obstacles: obstacleEntities, avoidanceRadius: 2.0, maxSpeed: 5.0, deltaTime: 0.016) -``` - -6. Steer Toward a Target Position (with Arrive): - -```swift -steerArrive(entityId: entity, targetPosition: targetPosition, maxSpeed: 5.0, deltaTime: 0.016) -``` - -7. Steer using WASD keys - -```swift -steerWithWASD(entityId: entity, maxSpeed: 5.0, deltaTime: 0.016) -``` - ---- - -## What Happens Behind the Scenes? - -1. Low-Level Behaviors: -- Calculate desired velocity based on the target or threat position. -- Generate steering forces by comparing desired velocity with current velocity. -2. High-Level Behaviors: -- Use low-level behaviors to calculate steering adjustments. -- Apply these forces to the entity’s physics system for smooth, realistic motion. -- Align the entity’s orientation to face its movement direction. -3. Physics Integration: -- Forces are applied through the Physics System, ensuring that movement respects mass, velocity, and acceleration. - ---- - -## Tips and Best Practices -- Prefer High-Level Behaviors: They simplify complex movement patterns and automatically handle integration with the Physics System. -- Use Low-Level Behaviors for Custom Logic: When precise control is required, combine low-level behaviors for unique movement styles. -- Smooth Orientation: Use alignOrientation or integrate orientation alignment directly into high-level functions. -- Tune Parameters: Adjust maxSpeed, turnSpeed, and slowingRadius for different entity types (e.g., fast-moving cars vs. slow-moving enemies). - ---- - -## Common Issues and Fixes - -### Issue: Entity Doesn’t Move - -- Cause: The Physics Component is missing or paused. -- Solution: Ensure the entity has a PhysicsComponents and it’s not paused. - -### Issue: Jittery Movement - -- Cause: Conflicting forces or large delta times. -- Solution: Tune maxSpeed and ensure deltaTime is passed correctly. - -### Issue: Entity Ignores Obstacles - -- Cause: Avoidance radius is too small or obstacles are not registered. -- Solution: Increase the avoidanceRadius and verify obstacle entities. - diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingTransformSystem.md b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingTransformSystem.md deleted file mode 100644 index fb91bfdd3..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/UsingTransformSystem.md +++ /dev/null @@ -1,118 +0,0 @@ ---- -id: transformsystem -title: Transform System -sidebar_position: 3 ---- - -# Using the Transform System in Untold Engine - -The Transform System is a core part of the Untold Engine, responsible for managing the position, rotation, and scale of entities. It provides both local transformations (relative to a parent entity) and world transformations (absolute in the scene). - -## How to Use the Transform System - -### Step 1: Retrieve Transform Data -You can retrieve an entity’s position, orientation, or axis vectors using the provided functions. - -#### Get Local Position - -Retrieves the entity’s position relative to its parent. - -```swift -let localPosition = getLocalPosition(entityId: entity) -``` - -#### Get World Position - -Retrieves the entity’s absolute position in the scene. - -```swift -let worldPosition = getPosition(entityId: entity) -``` - -#### Get Local Orientation - -Retrieves the entity’s orientation matrix relative to its parent. - -```swift -let localOrientation = getLocalOrientation(entityId: entity) -``` - -#### Get World Orientation - -Retrieves the entity’s absolute orientation matrix. - -```swift -let worldOrientation = getOrientation(entityId: entity) -``` - -#### Get Axis Vectors - -Retrieve the entity’s forward, right, or up axis: - -```swift -let forward = getForwardAxisVector(entityId: entity) -let right = getRightAxisVector(entityId: entity) -let up = getUpAxisVector(entityId: entity) -``` - ---- - -### Step 2: Update Transform Data - -Modify an entity’s transform by translating or rotating it. - -#### Translate the Entity - -Move the entity to a new position: - -```swift -translateTo(entityId: entity, position: simd_float3(5.0, 0.0, 3.0)) -``` - -Move the entity by an offset relative to its current position: - -```swift -translateBy(entityId: entity, position: simd_float3(1.0, 0.0, 0.0)) -``` - -#### Rotate the Entity - -Rotate the entity to a specific angle around an axis: - -```swift -rotateTo(entityId: entity, angle: 45.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Apply an incremental rotation to the entity: - -```swift -rotateBy(entityId: entity, angle: 15.0, axis: simd_float3(0.0, 1.0, 0.0)) -``` - -Directly set the entity’s rotation matrix: - -```swift -rotateTo(entityId: entity, rotation: simd_float4x4( /* matrix values */ )) -``` - ---- - -## What Happens Behind the Scenes? - -1. Local and World Transform Components: -- Each entity has a LocalTransformComponent for transformations relative to its parent. -- The WorldTransformComponent calculates the absolute transform by combining the local transform with the parent’s world transform. -2. Transform Matrices: -- Transformations are stored in 4x4 matrices that include position, rotation, and scale. -- These matrices are updated whenever you translate or rotate an entity. -3. Scene Graph Integration: -- Changes to a parent entity automatically propagate to its children through the scene graph. - ---- - -## Tips and Best Practices -- Use Local Transformations for Hierarchies: - - For example, a car’s wheels (children) should use local transforms relative to the car body (parent). -- Combine Translations and Rotations: - - Use translateTo or rotateTo to set an entity’s absolute position or rotation. - - Use translateBy or rotateBy for incremental adjustments. diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/_category.json b/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/_category.json deleted file mode 100644 index a1c32169e..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/03-Engine Systems/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Engine Systems", "position": 4, "collapsed": false } diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/04-Custom Components/_category.json b/website/versioned_docs/version-0.10.7/04-Engine Development/04-Custom Components/_category.json deleted file mode 100644 index 71b201664..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/04-Custom Components/_category.json +++ /dev/null @@ -1 +0,0 @@ -{ "label": "Custom Components", "position": 5, "collapsed": false } diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/04-Custom Components/customComponent.md b/website/versioned_docs/version-0.10.7/04-Engine Development/04-Custom Components/customComponent.md deleted file mode 100644 index d327a3577..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/04-Custom Components/customComponent.md +++ /dev/null @@ -1,60 +0,0 @@ ---- -id: ecs-create-custom-component -title: Create a Custom Component -sidebar_position: 1 ---- - -# Create a Custom Component - -In your game, you may want to **extend functionality to an entity**. -You can do this by creating a **custom component**. - -Components in the Untold Engine are **data-only objects** that you attach to entities. -They should hold **state, not behavior**. All game logic is handled in systems. - -Every custom component must conform to the `Component` protocol. - -By following this design, your game stays modular: -- **Components** define what an entity *is capable of*. -- **Systems** define *how that capability behaves*. - ---- - -## Minimal Template - -Here’s an example of a simple custom component for a soccer player’s dribbling behavior: - -```swift -public class DribblinComponent: Component { - public required init() {} - var maxSpeed: Float = 5.0 - var kickSpeed: Float = 15.0 - var direction: simd_float3 = .zero -} -``` - -> ⚠️ Note: Components should not include functions or game logic. Keep them as pure data containers. - -## Attaching a Component to an Entity - -Once you’ve defined a component, you attach it to an entity in your scene: - -```swift -let player = createEntity(name: "player") - -// Attach DribblinComponent to the entity -registerComponent(entityId: player, componentType: DribblinComponent.self) - -// Access and modify component data -if let c = scene.get(component: DribblinComponent.self, for: player) { - c.maxSpeed = 6.5 - c.kickSpeed = 18.0 -} - -``` - -This example creates a new entity called player, attaches a DribblinComponent, and updates its values. - - -On its own, the component just stores numbers — it doesn’t do anything yet. -To make the player actually dribble, you’ll need to implement a system that processes this component each frame. diff --git a/website/versioned_docs/version-0.10.7/04-Engine Development/04-Custom Components/customSystem.md b/website/versioned_docs/version-0.10.7/04-Engine Development/04-Custom Components/customSystem.md deleted file mode 100644 index 0b6ed75bc..000000000 --- a/website/versioned_docs/version-0.10.7/04-Engine Development/04-Custom Components/customSystem.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -id: ecs-create-custom-system -title: Create a Custom System -sidebar_position: 2 ---- - -# Create a Custom System - -If you’ve created a **custom component**, you’ll usually also want to create a **custom system** to make it do something. -Components store the data, but systems are where the behavior lives. - -The engine automatically calls systems every frame. A system typically: - -1. Resolves the component IDs it cares about -2. Queries entities that have those components -3. Reads and updates their state (transforms, physics, animation, etc.) - -This separation ensures components remain pure data containers, while systems drive the simulation. - ---- - -## Minimal Template - -Here’s a simple system that works with the `DribblinComponent` we defined earlier: - -```swift -public func dribblingSystemUpdate(deltaTime: Float) { - // 1. Get the ID of the DribblinComponent - let customId = getComponentId(for: DribblinComponent.self) - - // 2. Query all entities that have this component - let entities = queryEntitiesWithComponentIds([customId], in: scene) - - // 3. Loop through each entity and update its data - for entity in entities { - guard let dribblingComponent = scene.get(component: DribblinComponent.self, for: entity) else { - continue - } - - // Example logic: move player in the dribbling direction - dribblingComponent.direction = simd_normalize(dribblingComponent.direction) - let displacement = dribblingComponent.direction * dribblingComponent.maxSpeed * deltaTime - - if let transform = scene.get(component: LocalTransformComponent.self, for: entity) { - transform.position += displacement - } - } -} -``` - -## Registering the System -All custom systems must be registered during initialization so the engine knows to run them every frame: - -```swift -registerCustomSystem(dribblingSystemUpdate) -``` - diff --git a/website/versioned_docs/version-0.10.7/05-Editor Development/01-Overview.md b/website/versioned_docs/version-0.10.7/05-Editor Development/01-Overview.md deleted file mode 100644 index 9abb30205..000000000 --- a/website/versioned_docs/version-0.10.7/05-Editor Development/01-Overview.md +++ /dev/null @@ -1,135 +0,0 @@ ---- -id: editoroverview -title: Overview -sidebar_position: 1 ---- - -# Editor Development Overview - -This section is for developers who want to **improve or extend the Untold Editor**. - -The editor is a separate application built on top of Untold Engine. - ---- - -## What You’ll Work On - -In this section, you’ll learn how to: -- Understand the editor architecture -- Add or modify editor views -- Improve workflows and tooling -- Integrate editor features with the engine - -Editor development focuses on **usability and tooling**, not gameplay. - ---- - -## Installing the Editor for Development - -Working on the editor requires installing Untold Editor via the command line and running it from source. - -For editor development, clone this repository: - -```bash -git clone https://github.com/untoldengine/UntoldEditor.git -cd UntoldEditor -``` -The Editor is a Swift Package with an executable target named UntoldEditor. -It declares a dependency on the Untold Engine package; Xcode/SwiftPM will resolve it automatically. - -### Open in Xcode - -1. Open Xcode → File ▸ Open → select the Package.swift in this repo -2. Xcode will create a workspace view for the package -3. Choose the UntoldEditor scheme → Run - -### Build & run via CLI -If you prefer working with the terminal, you can build and run the Editor as follows: - -```bash -swift build -swift run UntoldEditor -``` - -### Pinning the Engine Dependency - -By default, this repo pins Untold Engine to a released version. -If you want the latest engine changes: - -#### Option A — Xcode UI -- In the project navigator: Package Dependencies → UntoldEngine -- Set Dependency Rule to Branch and type develop - -#### Option B — Edit Package.swift - -```swift -.dependencies = [ - .package(url: "https://github.com/untoldengine/UntoldEngine.git", branch: "develop") -] -``` - -Then reload packages: - -```bash -xcodebuild -resolvePackageDependencies -# or in Xcode: File ▸ Packages ▸ Resolve Package Versions -``` - - -The editor can be developed independently of game projects. - ---- - -## 🕹 Using the Editor - -1. **Create / Open a Project** – Use the start screen or File menu -2. **Set Asset Folder** – Choose an **external** directory for your project’s assets -3. **Import Assets** – Drag files in or use the Asset Browser “+” -4. **Build Your Scene** – Create entities, add components, use gizmos to position -5. **Play Mode** – Toggle to simulate and validate behavior -6. **Save / Load** – Save project and scene files; reopen later to continue - -> 💡 Why an *external* asset folder? -> It enables **runtime importing** and iteration without copying everything into the app bundle. - - -## Editor Architecture - -The Untold Editor is structured around: -- Independent views -- Shared engine state -- Explicit data flow between UI and engine -- Minimal magic - -Understanding the editor architecture is key before adding features. - -You can start with: - -> **Editor Development → Architecture** - ---- - -## Editor Views and Interaction - -The editor is composed of views such as: -- Scene View -- Inspector -- Asset Browser -- Scene Hierarchy -- Code Editor (note: USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - -Each view is designed to be modular and replaceable. - ---- - -## Who This Section Is Not For - -This section is **not** intended for: -- Game developers building gameplay -- Engine developers working on core systems -- Users looking to learn the editor UI - -If you want to *use* the editor, see: - -> **Game Development → Using the Editor** - diff --git a/website/versioned_docs/version-0.10.7/05-Editor Development/02-Architecture/EditorArchitecture.md b/website/versioned_docs/version-0.10.7/05-Editor Development/02-Architecture/EditorArchitecture.md deleted file mode 100644 index c2d198577..000000000 --- a/website/versioned_docs/version-0.10.7/05-Editor Development/02-Architecture/EditorArchitecture.md +++ /dev/null @@ -1,187 +0,0 @@ -# Editor Architecture - -This document provides a high-level overview of the **Untold Editor architecture**. - -It is intended for contributors who want to understand how the editor is structured before working on specific views, tools, or workflows. - -This page focuses on **concepts, responsibilities, and data flow**, not implementation details. - ---- - -## Architectural Goals - -The Untold Editor is designed with the following goals: - -- **Editor as a client of the engine** - The editor uses the same runtime as the game. - -- **Explicit data flow** - Editor actions translate directly into engine state changes. - -- **Minimal editor-only behavior** - Play mode mirrors runtime behavior as closely as possible. - -- **Composable tools and views** - Editor features should be modular and replaceable. - -The editor is a tool — not a separate simulation environment. - ---- - -## High-Level Structure - -At a conceptual level, the editor is organized into these layers: - -Editor UI (Views & Tools) -↓ -Editor Coordination Layer -↓ -Engine Runtime -↓ -Platform & Rendering Backends - -The editor does not own the engine — it **drives** it. - ---- - -## Editor as an Engine Client - -The Untold Editor runs on top of the same engine runtime used by games. - -Key implications: -- Scene data is real engine data -- Systems execute through the same update loop -- Rendering paths are shared -- Bugs reproduced in the editor are runtime-relevant - -There is no “fake” editor simulation. - ---- - -## Editor Coordination Layer - -Between the UI and the engine sits a thin coordination layer. - -This layer is responsible for: -- Translating UI actions into engine operations -- Managing selection state -- Coordinating editor-only modes (Edit vs Play) -- Routing commands between views - -It does **not** contain simulation logic. - ---- - -## Views - -The editor is composed of **independent views**, each with a focused responsibility. - -Typical views include: -- Scene View -- Inspector -- Scene Hierarchy -- Asset Browser -- Code Editor (USC scripts are authored in Xcode; the editor does not include a built-in USC script editor) - -Views: -- Observe engine state -- Emit commands -- Do not own core data - -This keeps views simple and interchangeable. - ---- - -## Tools and Interaction - -Editor tools (selection, transform, manipulation, etc.) are built as: -- Stateless or minimally stateful controllers -- Operating on selected engine entities -- Emitting explicit transform or component changes - -Input handling is centralized and routed to active tools. - ---- - -## Scene Editing Model - -Scene editing operates directly on engine data: - -- Entities and components are real -- Transforms update immediately -- Changes are visible to all views - -Editor-only metadata (selection, highlighting, gizmos) is stored separately. - ---- - -## Edit Mode vs Play Mode - -The editor supports two primary modes: - -### Edit Mode -- Systems that mutate simulation state are paused -- Editor tools manipulate entity state directly -- Scene changes are persistent - -### Play Mode -- Full engine update loop runs -- USC scripts execute -- Physics and animation systems are active -- Scene state may be restored on exit - -The transition between modes is explicit and controlled. - ---- - -## Asset and Resource Handling - -The editor manages assets as references to engine resources. - -Responsibilities include: -- Importing external files -- Tracking asset paths -- Updating resource bindings -- Refreshing views when assets change - -The engine remains the owner of resource lifetimes. - ---- - -## Relationship to USC - -USC scripts are authored in Xcode and managed through the editor but executed by the engine. The editor itself does not host a built-in USC script editor. - -The editor: -- Creates script source files -- Triggers script builds -- Attaches generated scripts to entities - -The editor does not interpret or execute USC logic. - ---- - -## Design Tradeoffs - -The Untold Editor intentionally avoids: - -- Duplicating engine logic -- Editor-only simulation paths -- Heavy UI-driven state mutation -- Hidden side effects from tools - -These tradeoffs prioritize: -- Consistency with runtime behavior -- Debuggability -- Contributor approachability - ---- - -## Architecture in Practice - -This document describes the conceptual structure of the editor. - -For implementation details, see: - -Editor Development → Architecture → Internals - diff --git a/website/versioned_docs/version-0.10.7/05-Editor Development/03-Views/AssetBrowserView.md b/website/versioned_docs/version-0.10.7/05-Editor Development/03-Views/AssetBrowserView.md deleted file mode 100644 index 5f4a73fa3..000000000 --- a/website/versioned_docs/version-0.10.7/05-Editor Development/03-Views/AssetBrowserView.md +++ /dev/null @@ -1,128 +0,0 @@ -# Asset Browser View - -The **Asset Browser View** is the discovery and selection surface for project assets within the Untold Editor. - -It presents filtered asset metadata, supports browsing and search, and emits asset selection or assignment intents through the coordination layer. - -This document describes the **architectural role** of the Asset Browser View, not how to use it as an end user. - ---- - -## Purpose - -The Asset Browser View exists to: - -- Surface the project’s asset library for discovery -- Enable filtering, searching, and grouping of assets -- Provide a control point for selecting assets -- Initiate asset assignment intents (e.g., “assign mesh,” “assign script”) without owning import or load logic - -It is the bridge between asset metadata and editor interactions that consume assets. - ---- - -## Responsibilities - -The Asset Browser View is responsible for: - -- Displaying asset entries with relevant metadata and previews -- Providing search, filter, and organization affordances -- Reflecting shared selection state for assets -- Emitting asset selection changes -- Emitting asset assignment intents to other systems (e.g., drag-and-drop targets) via commands -- Respecting editor mode (Edit vs Play) where assignment is allowed - -The Asset Browser View does **not** own asset data or selection state. - ---- - -## What This View Does NOT Do - -The Asset Browser View intentionally does **not**: - -- Own or manage asset import, processing, or versioning -- Load GPU resources or instantiate runtime objects -- Apply asset assignments directly; it emits intents through the coordination layer -- Manage project file I/O beyond reading metadata exposed by asset services -- Execute scripts or run simulation logic -- Decide selection globally; it only requests changes through shared state - -If asset browser behavior appears to require import, loading, or state ownership, that logic belongs elsewhere. - ---- - -## Data Flow - -The Asset Browser View participates in the editor data flow as follows: - -### Reads -- Asset catalog metadata (names, types, tags, thumbnails, availability) -- Asset selection state -- Editor mode state -- Search/filter parameters provided by shared state or local UI - -### Emits -- Asset selection change requests -- Asset assignment intents (e.g., assign to component field, instantiate into scene) -- Folder or collection navigation commands (if supported by asset services) -- Mode-aware warnings when an assignment is not allowed - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Asset Browser View interacts indirectly with other views via shared editor state: - -- **Scene View / Scene Hierarchy** - Receives asset drops or assignment intents to create or replace scene content - -- **Inspector** - Accepts asset assignments into component fields - -- **Code Editor** - May reference scripts as assets; USC authoring itself happens in Xcode, not inside the editor - -The Asset Browser View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full asset browsing, selection, and assignment intents are available -- Assignments update authoring data through commands - -### Play Mode -- Asset browsing remains available; assignments that mutate authoring data may be blocked or limited -- Runtime-safe assignments (if supported) are routed through the same command pathways - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Asset Browser View by: - -- Adding advanced filters (tags, dependencies, usage frequency) -- Introducing custom previews or inspectors for specific asset types -- Extending drag-and-drop behaviors for assignment targets -- Adding collection/saved search features backed by shared state - -Extensions should integrate through existing asset metadata sources and command pathways. - ---- - -## Design Constraints - -The Asset Browser View is intentionally constrained to: - -- Visualization of asset metadata and selection -- Command emission for selection and assignment intents -- Stateless or minimal local UI state driven by shared data -- No direct import, loading, or mutation of asset storage - -Keeping these boundaries strict ensures predictable asset workflows and debuggability. - diff --git a/website/versioned_docs/version-0.10.7/05-Editor Development/03-Views/InspectorView.md b/website/versioned_docs/version-0.10.7/05-Editor Development/03-Views/InspectorView.md deleted file mode 100644 index 6745678a4..000000000 --- a/website/versioned_docs/version-0.10.7/05-Editor Development/03-Views/InspectorView.md +++ /dev/null @@ -1,135 +0,0 @@ -# Inspector View - -The **Inspector View** is the focused property panel for examining and editing the selected entities and components in the Untold Editor. - -It presents authoritative component data for the current selection and emits structured changes back through the editor coordination layer. - -This document describes the **architectural role** of the Inspector View, not how to use it as an end user. - ---- - -## Purpose - -The Inspector View exists to: - -- Surface component data for the current selection -- Provide structured editors for component properties -- Validate edits against component rules and modes -- Emit explicit change requests to the coordination layer - -It is the bridge between selection state and component-level editing. - ---- - -## Responsibilities - -The Inspector View is responsible for: - -- Displaying the current selection (single or multi-entity) -- Rendering component sections and property editors -- Showing validation and state (read-only, overridden, default) -- Emitting add/remove component requests -- Emitting property change commands with full context (target entity, component, field, value) -- Reflecting editor mode (Edit vs Play) in allowed operations - -The Inspector View does **not** own component data or selection state. - ---- - -## What This View Does NOT Do - -The Inspector View intentionally does **not**: - -- Manage or decide selection; it only observes it -- Apply engine changes directly; all edits flow through commands -- Run simulation, scripting, or component lifecycle logic -- Own component storage or serialization; it only issues edits -- Own schemas or define component types (it consumes published definitions) -- Manage undo/redo stacks (it emits commands compatible with them) -- Provide asset authoring workflows beyond property-level references - -If Inspector behavior appears to require simulation or data ownership, that logic belongs elsewhere. - ---- - -## Data Flow - -The Inspector View participates in the editor data flow as follows: - -### Reads -- Selection state -- Component definitions and metadata (including editability rules) -- Component instances for selected entities -- Editor mode state -- Validation results and command feedback - -### Emits -- Property change commands -- Component add/remove requests -- Component reorder or enable/disable requests (if supported by schema) -- Mode-aware warnings (e.g., attempted edit in Play mode) - -All emitted actions flow through the editor coordination layer. - ---- - -## Interaction With Other Views - -The Inspector View interacts indirectly with other views via shared editor state: - -- **Scene Hierarchy** - Mirrors selection changes driven by hierarchy navigation - -- **Scene View** - Reflects selection changes initiated in world space - -- **Asset Browser** - Accepts drag-and-drop asset references into component fields - -- **Code Editor** - Consumes component schemas defined in code; USC authoring occurs in Xcode, not inside the editor; does not depend on editor code directly - -The Inspector View does not directly communicate with other views. - ---- - -## Edit Mode vs Play Mode Behavior - -### Edit Mode -- Full component editing is enabled -- Changes persist to the authoring state -- Validation prevents invalid component configurations - -### Play Mode -- Inspector prioritizes observation; many edits are read-only or limited -- Runtime values may be displayed distinctly from authoring values -- Commands that would mutate persistent data may be blocked or deferred - -Mode transitions are handled externally and reflected in the view. - ---- - -## Extension Points - -Contributors may extend the Inspector View by: - -- Adding custom property drawers for specific field types -- Providing component-specific inspectors with bespoke UI/validation -- Introducing contextual warnings or hints based on selection -- Enhancing multi-edit behavior across heterogeneous selections - -Extensions should integrate through existing data binding and command pathways. - ---- - -## Design Constraints - -The Inspector View is intentionally constrained to: - -- Observation of selection and component state -- Stateless or minimal local UI state -- Deterministic rendering based on shared data -- Command-only mutations; no direct state writes - -Keeping these boundaries strict ensures predictable editing and debuggability. - diff --git a/website/versioned_docs/version-0.10.7/05-Editor Development/EditorOverview.md b/website/versioned_docs/version-0.10.7/05-Editor Development/EditorOverview.md deleted file mode 100644 index 27704bf80..000000000 --- a/website/versioned_docs/version-0.10.7/05-Editor Development/EditorOverview.md +++ /dev/null @@ -1,99 +0,0 @@ ---- -id: editorfeatures -title: Editor Features -sidebar_position: 2 ---- - -# Untold Engine Editor Features - -The Untold Engine Editor makes it easier than ever to set up your scenes and entities without touching code. With the Editor, you can visually create, configure, and organize your game world, while keeping Swift code focused on game logic and behaviors. - -Here’s a quick tour of the Editor’s main features: - ---- - -## Scene Graph - -![Scene Graph](../images/engine-scenegraph.png) - -The **Scene Graph** shows all the entities in your scene in a hierarchical view. You can add, rename, and organize entities, as well as set up parent–child relationships. This is where you’ll find your cameras, lights, and models at a glance. - ---- - -## Inspector - -![Inspector](../images/engine-inspector.png) - -The **Inspector** lets you configure properties of the selected entity. Adjust position, rotation, and scale, or fine-tune camera settings and add components. Think of it as the control panel for whatever you’re working on. - ---- - -## Gizmo Manipulation - -![Gizmo](../images/engine-gizmo.png) - -Use the **3D gizmo** to interactively move, rotate, and scale entities directly in the scene. This makes it quick to place objects exactly where you want them. - ---- - -## Materials - -![Materials](../images/engine-materials.png) - -Assign meshes and materials through the **Inspector**. You can drop in textures for base color, roughness, metallic, and emissive maps, then tweak material properties to get the right look. - ---- - -## Lighting & Environment - -![Lighting](../images/engine-lights.png) - -The **Lighting panel** give you control over your scene’s mood. Add directional, point, spotlight and area lights, adjust intensities. - -## Environment - -The **Environment panel** enable **Image-Based Lighting (IBL)** for realistic reflections and ambient light. - -![HDR](../images/engine-hdr.png) - ---- - -## Post-Processing Effects - -![Post Processing](../images/engine-post-processing.png) - -The **Effects tab** lets you add and tweak post-processing features such as: -- Depth of Field -- Chromatic Aberration -- Bloom -- Color Grading -- SSAO, Vignette, White Balance, and more - -These effects bring your scenes closer to a polished, production-ready look. - ---- - -## Asset Browser - -![Asset Browser](../images/engine-assetbrowser.png) - -The **Asset Browser** keeps your models, textures, and materials organized. Import new assets, set paths for your project, and quickly assign resources to entities in your scene. - ---- - -## Console Log - -![Console](../images/engine-consolelog.png) - -The **Console Log** provides real-time feedback from the engine. Use it to debug entity creation, monitor system messages, and track issues while working on your scene. - ---- - -## Putting It All Together - -With the Editor, you now have a clear separation of responsibilities: -- **Use the Editor** for entity initialization, scene setup, materials, and visual configuration. -- **Use Swift code** for gameplay logic, physics tweaks, and systems that bring your game to life. - -This workflow makes iteration faster and keeps your codebase focused on what matters most: gameplay. - diff --git a/website/versioned_docs/version-0.10.7/06-Reference/01-USCAPI.md b/website/versioned_docs/version-0.10.7/06-Reference/01-USCAPI.md deleted file mode 100644 index 0c961d1cb..000000000 --- a/website/versioned_docs/version-0.10.7/06-Reference/01-USCAPI.md +++ /dev/null @@ -1,133 +0,0 @@ -# Untold Script Core API - -Use this page as a compact checklist of the most-used USC (Untold Script Core) DSL calls. All snippets assume you’re inside a `buildScript` closure on the current entity (`self`). - ---- - -## Lifecycle -- `onStart()` – one-time init per play. -- `onUpdate()` – every frame. -- `onEvent("Name")` – custom triggers. -- `onCollision(tag:)` – **planned** (not yet available). - -## Properties (get/set) -Supported keys: `.position`, `.scale`, `.velocity`, `.acceleration`, `.mass`, `.angularVelocity` (write-only today), `.intensity`, `.color`, `.deltaTime`. -```swift -.getProperty(.position, as: "pos") -.setProperty(.position, toVariable: "nextPos") -.setProperty(.velocity, to: simd_float3(0, 2, 0)) -``` - -## Input -```swift -.ifKeyPressed("W") { n in n.log("forward") } -.ifKeyReleased("Space") { n.log("jump released") } -.getKeyState("w", as: "wPressed") -``` - -## Transform -```swift -.translateTo(simd_float3(0, 1, 0)) -.translateBy(simd_float3(0, 0, 1)) -.rotateTo(degrees: 45, axis: simd_float3(0, 1, 0)) -.rotateBy(degrees: .float(5), axis: simd_float3(0, 1, 0)) -.lookAt("TargetEntity") -``` - -## Animation -```swift -.playAnimation("Walk", loop: true) -.stopAnimation() -``` - -## Physics (forces/velocity) -```swift -.applyForce(force: simd_float3(0, 10, 0)) -.applyLinearImpulse(direction: .vec3(x: 0, y: 1, z: 0), magnitude: .float(5)) -.setLinearVelocity(.vec3(x: 0, y: 0, z: 5)) -.addLinearVelocity(.vec3(x: 0, y: 0, z: -1)) -.clampLinearSpeed(min: .float(0), max: .float(8)) -.clearVelocity() -.clearAngularVelocity() -.clearForces() -.setGravityScale(0.5) -.pausePhysicsComponent(isPaused: true) -``` - -## Steering (vectors or side effects) -```swift -.seek(targetPosition: .vec3(x: 10, y: 0, z: 0), maxSpeed: .float(5), result: "steer") -.flee(threatPosition: .vec3(x: 0, y: 0, z: 0), maxSpeed: .float(6), result: "steer") -.arrive(targetPosition: .vec3(x: 0, y: 0, z: 0), maxSpeed: .float(6), slowingRadius: .float(2), result: "steer") -.pursuit(targetEntity: .string("Player"), maxSpeed: .float(6), result: "steer") -.evade(threatEntity: .string("Enemy"), maxSpeed: .float(6), result: "steer") -.steerSeek(targetPosition: .variableRef("targetPos"), maxSpeed: .variableRef("maxSpeed"), deltaTime: .variableRef("dt")) -.steerArrive(targetPosition: .variableRef("targetPos"), maxSpeed: .variableRef("maxSpeed"), slowingRadius: .variableRef("slow"), deltaTime: .variableRef("dt")) -.steerFlee(threatPosition: .variableRef("threatPos"), maxSpeed: .variableRef("maxSpeed"), deltaTime: .variableRef("dt")) -.steerPursuit(targetEntity: .string("Target"), maxSpeed: .float(6), deltaTime: .float(0.016)) -.orbit(centerPosition: .vec3(x: 0, y: 0, z: 0), radius: .float(5), maxSpeed: .float(4), deltaTime: .float(0.016)) -.alignOrientation(deltaTime: .float(0.016), turnSpeed: .float(1.0)) -``` - -## Camera -```swift -.cameraMoveTo(.vec3(x: 0, y: 3, z: -10)) -.cameraMoveBy(.vec3(x: 1, y: 0, z: 0)) -.cameraRotate(pitch: .float(0.02), yaw: .float(-0.08)) -.cameraFollow(target: .string("Player"), - offset: .vec3(x: 0, y: 3, z: -6), - smoothFactor: .float(5), - deltaTime: .float(0.016)) -.cameraFollowLocal(target: .string("Player"), - localOffset: .vec3(x: 0, y: 2, z: -4), - smoothFactor: .float(5), - deltaTime: .float(0.016)) -.cameraOrbitTarget(target: .string("Boss"), - radius: .float(12), - speed: .float(1.5), - deltaTime: .float(0.016), - offsetY: .float(1.5)) -.cameraMoveWithInput(speedVar: "moveSpeed", - deltaTimeVar: "dt", - wVar: "wPressed", aVar: "aPressed", - sVar: "sPressed", dVar: "dPressed", - qVar: "qPressed", eVar: "ePressed") -``` - -## Math (variables only) -```swift -.addFloat("a", "b", as: "sum") -.addFloat("a", literal: 1, as: "sum") -.subFloat("a", "b", as: "diff") -.mulFloat("a", "b", as: "prod") -.divFloat("a", "b", as: "quot") -.addVec3("v1", "v2", as: "sum") -.scaleVec3("v", by: "s", as: "out") -.scaleVec3("v", literal: 2, as: "out") -.lengthVec3("v", as: "len") -.normalizeVec3("v", as: "unit") -.dotVec3("a", "b", as: "dot") -.crossVec3("a", "b", as: "cross") -.lerpVec3(from: "a", to: "b", t: "t", as: "out") -.lerpFloat(from: "a", to: "b", t: "t", as: "out") -.reflectVec3("v", normal: "n", as: "reflected") -.projectVec3("v", onto: "axis", as: "proj") -.angleBetweenVec3("a", "b", as: "angleDeg") -.clampFloat("speed", min: "minSpeed", max: "maxSpeed", as: "clamped") -.clampVec3("vel", min: "minVel", max: "maxVel", as: "clampedVel") -``` - -## Flow / Variables / Debug -```swift -.ifCondition(lhs: .variableRef("speed"), .greater, rhs: .float(10)) { n in n.log("Too fast") } -.ifGreater("health", than: 0) { n in n.log("Alive") }.else { n in n.log("Dead") } -.ifEqual("flag", to: true) { n in n.log("Flag set") } -.setVariable("speed", to: 5.0) -.setVariable("dir", to: simd_float3(0, 0, 1)) -.setVariable("copy", fromVariable: "speed") -.log("Hello") -.logValue("velocity", value: .variableRef("vel")) -``` - -> Tip: If you need an entity other than `self`, use names in your instructions (e.g., `.lookAt("TargetName")`) or stash them in variables, then call `findEntity`-driven instructions like `pursuit`/`evade`. - diff --git a/website/versioned_docs/version-0.10.7/07-Contributor/_category.json b/website/versioned_docs/version-0.10.7/07-Contributor/_category.json deleted file mode 100644 index 4308c167f..000000000 --- a/website/versioned_docs/version-0.10.7/07-Contributor/_category.json +++ /dev/null @@ -1,2 +0,0 @@ -{ "label": "10-Contributor", "position": 99, "collapsed": false } - diff --git a/website/versioned_docs/version-0.10.7/08-CLI/CLI.md b/website/versioned_docs/version-0.10.7/08-CLI/CLI.md deleted file mode 100644 index 0ff5498a8..000000000 --- a/website/versioned_docs/version-0.10.7/08-CLI/CLI.md +++ /dev/null @@ -1,505 +0,0 @@ -# UntoldEngine CLI Tool - -> **Note:** The CLI tool source code is located in the `Tools/UntoldEngineCLI/` directory of the repository. -> -> For the most up-to-date documentation, see `Tools/UntoldEngineCLI/README.md` in the repository. - -The `untoldengine-create` command-line tool allows you to create and manage UntoldEngine game projects without launching the UntoldEditor. - -## Quick Start - -```bash -# Clone the repository -git clone https://github.com/untoldengine/UntoldEngine.git -cd UntoldEngine - -# Install the CLI globally -./scripts/install-create.sh - -# Create a project from anywhere -cd ~/anywhere -mkdir MyGame && cd MyGame -untoldengine-create create MyGame -``` - -## Complete Documentation - -For complete CLI documentation including: - -- Installation instructions -- Command reference -- Platform options -- Usage examples -- Troubleshooting - -Please refer to `Tools/UntoldEngineCLI/README.md` in the repository. - -## Quick Start - -Create a new game project in three simple steps: - -```bash -# 1. Create and enter your project directory -mkdir MyGame && cd MyGame - -# 2. Create the project (default: macOS) -untoldengine-create create MyGame - -# 3. Open in Xcode -open MyGame/MyGame.xcodeproj -``` - -All project files and assets will be created in the current directory. - -## Commands - -### create - -Creates a new UntoldEngine game project with the specified platform configuration. - -**Usage:** -```bash -untoldengine-create create [--platform ] -``` - -**Arguments:** -- ``: Name of your game project (required) - -**Options:** -- `--platform `: Target platform (default: `macos`) - - `macos` - macOS desktop application - - `ios` - iOS application - - `iosar` - iOS with ARKit support - - `visionos` - visionOS (Apple Vision Pro) - -**Examples:** - -Create a macOS game: -```bash -mkdir SpaceShooter && cd SpaceShooter -untoldengine-create create SpaceShooter -``` - -Create an iOS game: -```bash -mkdir MobileRPG && cd MobileRPG -untoldengine-create create MobileRPG --platform ios -``` - -Create an AR game for iOS: -```bash -mkdir ARAdventure && cd ARAdventure -untoldengine-create create ARAdventure --platform iosar -``` - -Create a visionOS game: -```bash -mkdir VisionGame && cd VisionGame -untoldengine-create create VisionGame --platform visionos -``` - -**What it does:** - -1. Creates the complete project structure in the current directory -2. Generates an Xcode project using XcodeGen -3. Sets up the game asset directories at `/Sources//GameData/` -4. Configures the UntoldEngine to load assets from the GameData directory -5. Creates platform-specific configuration files - -### update - -Updates the game data directory structure for an existing project. - -**Usage:** -```bash -untoldengine-create update -``` - -**Arguments:** -- ``: Name of the existing project (required) - -**What it does:** - -Re-creates the GameData folder structure at `/Sources//GameData/` with all standard asset directories: -- Models/ -- Scenes/ -- Scripts/ -- Animations/ -- Gaussians/ -- Shaders/ -- Textures/ - -**Example:** - -```bash -cd MyGame -untoldengine-create update MyGame -``` - -**When to use:** -- After accidentally deleting asset folders -- When you need to reset the folder structure -- To ensure all standard directories exist - -## Platform Options - -### macOS -**Command:** `--platform macos` (default) - -**Features:** -- Full desktop application -- Window management -- Keyboard and mouse input -- High performance rendering - -**Use for:** -- Desktop games -- Development and testing -- Maximum performance applications - -### iOS -**Command:** `--platform ios` - -**Features:** -- iPhone and iPad support -- Touch input -- Accelerometer support -- App Store distribution - -**Use for:** -- Mobile games -- Touch-based experiences -- Wide distribution via App Store - -### iOS AR (ARKit) -**Command:** `--platform iosar` - -**Features:** -- All iOS features -- ARKit integration -- World tracking -- Plane detection -- Face tracking - -**Use for:** -- Augmented reality games -- AR experiences -- Real-world interactive applications - -### visionOS -**Command:** `--platform visionos` - -**Features:** -- Apple Vision Pro support -- Spatial computing -- 3D UI elements -- Hand tracking -- Eye tracking - -**Use for:** -- Immersive 3D experiences -- Spatial applications -- Next-generation mixed reality - -## Project Structure - -After running `untoldengine-create create MyGame`, your project structure will look like: - -``` -MyGame/ # Your working directory -└── MyGame/ # Generated project - ├── MyGame.xcodeproj # Xcode project (generated) - ├── project.yml # XcodeGen configuration - ├── README.md # Project documentation - └── Sources/ - └── MyGame/ - ├── GameData/ # Game assets location - │ ├── Models/ # 3D models (.usdz, etc.) - │ ├── Scenes/ # Scene files - │ ├── Scripts/ # Game logic scripts - │ ├── Animations/ # Animation data - │ ├── Gaussians/ # Gaussian splatting data - │ ├── Shaders/ # Custom shaders - │ └── Textures/ # Image files - ├── AppDelegate.swift # Application entry point - ├── GameViewController.swift # Main game controller - └── Info.plist # App configuration -``` - -### Important Directories - -**GameData/**: The single location for all game assets -- Path: `MyGame/Sources/MyGame/GameData/` -- The UntoldEngine is configured to load all assets from this location -- Copy your game assets (models, textures, etc.) here -- Subdirectories are organized by asset type - -**Sources/**: Swift source code -- Contains your game's Swift files -- Platform-specific delegates and controllers -- Can add additional Swift files here - -### Adding Assets - -To add assets to your game, copy them into the appropriate GameData subdirectory: - -```bash -# Add a 3D model -cp ~/Downloads/spaceship.usdz MyGame/Sources/MyGame/GameData/Models/ - -# Add textures -cp ~/Downloads/texture_*.png MyGame/Sources/MyGame/GameData/Textures/ - -# Add a scene -cp ~/Downloads/level1.json MyGame/Sources/MyGame/GameData/Scenes/ -``` - -The engine will automatically find assets in these locations. - -## Workflow Examples - -### Typical Development Workflow - -```bash -# 1. Create project directory -mkdir MyAwesomeGame -cd MyAwesomeGame - -# 2. Create the game project -untoldengine-create create MyAwesomeGame --platform ios - -# 3. Add your game assets -cp -r ~/GameAssets/Models/* MyAwesomeGame/Sources/MyAwesomeGame/GameData/Models/ -cp -r ~/GameAssets/Textures/* MyAwesomeGame/Sources/MyAwesomeGame/GameData/Textures/ - -# 4. Open in Xcode and start developing -open MyAwesomeGame/MyAwesomeGame.xcodeproj -``` - -### Starting from Scratch - -```bash -# Create a new game from scratch -mkdir PuzzleGame && cd PuzzleGame -untoldengine-create create PuzzleGame --platform macos - -# The GameData directory is ready for your assets -ls -la PuzzleGame/Sources/PuzzleGame/GameData/ -# Models/ Scenes/ Scripts/ Animations/ Gaussians/ Shaders/ Textures/ -``` - -### Multi-Platform Development - -You might want to test the same game on different platforms: - -```bash -# Create macOS version for development -mkdir MyGame-macOS && cd MyGame-macOS -untoldengine-create create MyGame --platform macos - -# Create iOS version in a separate directory -cd .. -mkdir MyGame-iOS && cd MyGame-iOS -untoldengine-create create MyGame --platform ios - -# Copy assets between projects -cp -r ../MyGame-macOS/MyGame/Sources/MyGame/GameData/* \ - MyGame/Sources/MyGame/GameData/ -``` - -### Resetting Asset Structure - -If you accidentally delete asset folders or need to reset: - -```bash -cd MyGame -untoldengine-create update MyGame -# GameData structure is recreated -``` - -## Troubleshooting - -### Command Not Found - -**Problem:** `untoldengine-create: command not found` - -**Solution:** -1. Ensure you ran the installation script: - ```bash - cd /path/to/UntoldEngine - ./Scripts/install-create.sh - ``` - -2. Check if the tool exists: - ```bash - ls -l /usr/local/bin/untoldengine-create - ``` - -3. If it exists but still not found, add `/usr/local/bin` to your PATH: - ```bash - echo 'export PATH="/usr/local/bin:$PATH"' >> ~/.zshrc - source ~/.zshrc - ``` - -### XcodeGen Failed - -**Problem:** Error message about XcodeGen failing - -**Solution:** -1. Ensure XcodeGen is installed: - ```bash - brew install xcodegen - ``` - -2. Verify XcodeGen works: - ```bash - xcodegen --version - ``` - -3. Check the project.yml file for syntax errors: - ```bash - cat path/to/project/project.yml - ``` - -### Build System Errors - -**Problem:** Error message about BuildSystem failure - -**Solution:** -1. Verify the UntoldEngine framework is properly built -2. Check that you're running the command from the correct directory -3. Ensure sufficient disk space for the project - -### Permission Denied - -**Problem:** Permission errors during installation - -**Solution:** -Use sudo for installation: -```bash -sudo ./Scripts/install-create.sh -``` - -Or install to a user directory (advanced): -```bash -# Edit install-create.sh to use a different install location -# Change INSTALL_PATH to ~/bin/untoldengine-create -``` - -### Xcode Project Won't Open - -**Problem:** Generated .xcodeproj file won't open - -**Solution:** -1. Regenerate the project: - ```bash - cd path/to/project - xcodegen generate - ``` - -2. Check Xcode version compatibility: - ```bash - xcodebuild -version - ``` - (Requires Xcode 15.0+) - -3. Verify project.yml is valid: - ```bash - cat project.yml - ``` - -### Assets Not Loading - -**Problem:** Game can't find assets at runtime - -**Solution:** -1. Verify assets are in the correct location: - ```bash - ls -la MyGame/Sources/MyGame/GameData/ - ``` - -2. Ensure assets are in the right subdirectories: - - Models go in `GameData/Models/` - - Textures go in `GameData/Textures/` - - etc. - -3. Check asset file names and extensions are correct - -4. Verify the GameData directory structure by running update: - ```bash - untoldengine-create update MyGame - ``` - -## Advanced Usage - -### Scripting and Automation - -The CLI tool is designed to work well in scripts: - -```bash -#!/bin/bash -# create-all-platforms.sh - -PROJECT_NAME="MyGame" - -for PLATFORM in macos ios iosar visionos; do - DIR="${PROJECT_NAME}-${PLATFORM}" - mkdir -p "$DIR" && cd "$DIR" - untoldengine-create create "$PROJECT_NAME" --platform "$PLATFORM" - cd .. -done -``` - -### CI/CD Integration - -Example GitHub Actions workflow: - -```yaml -name: Create Game Projects -on: [push] - -jobs: - create-projects: - runs-on: macos-latest - steps: - - uses: actions/checkout@v2 - - - name: Install CLI tool - run: | - cd UntoldEngine - ./Scripts/install-create.sh - - - name: Create projects - run: | - mkdir -p builds/MyGame-iOS && cd builds/MyGame-iOS - untoldengine-create create MyGame --platform ios -``` - -### Environment Variables - -While the CLI doesn't currently use environment variables, you can use them in your workflow: - -```bash -export GAME_NAME="MyGame" -export PLATFORM="ios" - -mkdir "$GAME_NAME" && cd "$GAME_NAME" -untoldengine-create create "$GAME_NAME" --platform "$PLATFORM" -``` - -## Getting Help - -For additional help: - -```bash -# General help -untoldengine-create --help - -# Command-specific help -untoldengine-create create --help -untoldengine-create update --help -``` - -## Feedback and Issues - -Found a bug or have a feature request? Please report it on the [UntoldEngine GitHub repository](https://github.com/untoldengine/UntoldEngine/issues). diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorAssetBrowserView-alt.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorAssetBrowserView-alt.png deleted file mode 100644 index b925157b9..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorAssetBrowserView-alt.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorBottomShot.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorBottomShot.png deleted file mode 100644 index ed3f3c731..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorBottomShot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorCodeScriptView-alt.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorCodeScriptView-alt.png deleted file mode 100644 index 84d7461bc..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorCodeScriptView-alt.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorCodeScriptView.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorCodeScriptView.png deleted file mode 100644 index 654da1eb3..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorCodeScriptView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorEffects.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorEffects.png deleted file mode 100644 index d69f37874..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorEffects.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorEnvironment.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorEnvironment.png deleted file mode 100644 index c6f55a224..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorEnvironment.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorInspectorView.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorInspectorView.png deleted file mode 100644 index 04f65dce7..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorInspectorView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorMainShot.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorMainShot.png deleted file mode 100644 index 981a26e45..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorMainShot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorScenegraphView.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorScenegraphView.png deleted file mode 100644 index 87b1cc9c0..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorScenegraphView.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorSideShotWide-alt.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorSideShotWide-alt.png deleted file mode 100644 index 42a9c804b..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorSideShotWide-alt.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/EditorSideShotWide.png b/website/versioned_docs/version-0.10.7/images/Editor/EditorSideShotWide.png deleted file mode 100644 index 14ea9d471..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/EditorSideShotWide.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/Editor_scene_new.png b/website/versioned_docs/version-0.10.7/images/Editor/Editor_scene_new.png deleted file mode 100644 index 99bc768bd..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/Editor_scene_new.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/Editor_scene_xcode.png b/website/versioned_docs/version-0.10.7/images/Editor/Editor_scene_xcode.png deleted file mode 100644 index 65b5c908d..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/Editor_scene_xcode.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/ScriptEditorAdd.png b/website/versioned_docs/version-0.10.7/images/Editor/ScriptEditorAdd.png deleted file mode 100644 index f425394bd..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/ScriptEditorAdd.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/Editor/ScriptReload.png b/website/versioned_docs/version-0.10.7/images/Editor/ScriptReload.png deleted file mode 100644 index 79f698548..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/Editor/ScriptReload.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/add-animation-component.png b/website/versioned_docs/version-0.10.7/images/add-animation-component.png deleted file mode 100644 index 4fb63092e..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/add-animation-component.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/add-button-scenegraph.png b/website/versioned_docs/version-0.10.7/images/add-button-scenegraph.png deleted file mode 100644 index 17c6f06a0..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/add-button-scenegraph.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/animation-assign.png b/website/versioned_docs/version-0.10.7/images/animation-assign.png deleted file mode 100644 index 9a36e09ba..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/animation-assign.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/animation-running.png b/website/versioned_docs/version-0.10.7/images/animation-running.png deleted file mode 100644 index 73c84ad72..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/animation-running.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/animationexportblender.png b/website/versioned_docs/version-0.10.7/images/animationexportblender.png deleted file mode 100644 index da6553855..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/animationexportblender.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/asset-browser-model.png b/website/versioned_docs/version-0.10.7/images/asset-browser-model.png deleted file mode 100644 index 7b293ec8c..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/asset-browser-model.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/asset-browser-usdc-file.png b/website/versioned_docs/version-0.10.7/images/asset-browser-usdc-file.png deleted file mode 100644 index e6d2f0cb8..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/asset-browser-usdc-file.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/camera.png b/website/versioned_docs/version-0.10.7/images/camera.png deleted file mode 100644 index 4fdc106c2..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/camera.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/demogame-noeditor.png b/website/versioned_docs/version-0.10.7/images/demogame-noeditor.png deleted file mode 100644 index d0a39e9e0..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/demogame-noeditor.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/editor-animation.png b/website/versioned_docs/version-0.10.7/images/editor-animation.png deleted file mode 100644 index d54a8c8e9..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/editor-animation.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/editorscreenshot.png b/website/versioned_docs/version-0.10.7/images/editorscreenshot.png deleted file mode 100644 index 7f78a96d1..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/editorscreenshot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/engine-assetbrowser.png b/website/versioned_docs/version-0.10.7/images/engine-assetbrowser.png deleted file mode 100644 index c58c536c2..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/engine-assetbrowser.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/engine-hdr.png b/website/versioned_docs/version-0.10.7/images/engine-hdr.png deleted file mode 100644 index 273b38545..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/engine-hdr.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/engine-lights.png b/website/versioned_docs/version-0.10.7/images/engine-lights.png deleted file mode 100644 index a18833041..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/engine-lights.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/engine-materials.png b/website/versioned_docs/version-0.10.7/images/engine-materials.png deleted file mode 100644 index 1f813dfc8..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/engine-materials.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/engine-post-processing.png b/website/versioned_docs/version-0.10.7/images/engine-post-processing.png deleted file mode 100644 index 3e3f1f32e..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/engine-post-processing.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/enginethumbnail.jpg b/website/versioned_docs/version-0.10.7/images/enginethumbnail.jpg deleted file mode 100644 index 882b7a07f..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/enginethumbnail.jpg and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/gamedemoscreenshot.png b/website/versioned_docs/version-0.10.7/images/gamedemoscreenshot.png deleted file mode 100644 index 7f78a96d1..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/gamedemoscreenshot.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/gamescene1.png b/website/versioned_docs/version-0.10.7/images/gamescene1.png deleted file mode 100644 index 512513157..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/gamescene1.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/importassetbutton.png b/website/versioned_docs/version-0.10.7/images/importassetbutton.png deleted file mode 100644 index a2ea71d9d..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/importassetbutton.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/importheader.gif b/website/versioned_docs/version-0.10.7/images/importheader.gif deleted file mode 100644 index 9a1b60a2a..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/importheader.gif and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/launchgame.gif b/website/versioned_docs/version-0.10.7/images/launchgame.gif deleted file mode 100644 index f13e49088..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/launchgame.gif and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/linkerissue.png b/website/versioned_docs/version-0.10.7/images/linkerissue.png deleted file mode 100644 index 192c9e5ea..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/linkerissue.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/modelexportblender.png b/website/versioned_docs/version-0.10.7/images/modelexportblender.png deleted file mode 100644 index bbfaa6c49..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/modelexportblender.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/modelineditor.png b/website/versioned_docs/version-0.10.7/images/modelineditor.png deleted file mode 100644 index 951e24a5b..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/modelineditor.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/modelsriggedexportblender.png b/website/versioned_docs/version-0.10.7/images/modelsriggedexportblender.png deleted file mode 100644 index 1eb6740e8..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/modelsriggedexportblender.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/script_component_selection.png b/website/versioned_docs/version-0.10.7/images/script_component_selection.png deleted file mode 100644 index a3a65bcb4..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/script_component_selection.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/script_open_in_xcode.png b/website/versioned_docs/version-0.10.7/images/script_open_in_xcode.png deleted file mode 100644 index 529766292..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/script_open_in_xcode.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/script_properties.png b/website/versioned_docs/version-0.10.7/images/script_properties.png deleted file mode 100644 index 1a926da20..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/script_properties.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/setpathbutton.png b/website/versioned_docs/version-0.10.7/images/setpathbutton.png deleted file mode 100644 index a2ea71d9d..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/setpathbutton.png and /dev/null differ diff --git a/website/versioned_docs/version-0.10.7/images/top_contributors/MioLogo.png b/website/versioned_docs/version-0.10.7/images/top_contributors/MioLogo.png deleted file mode 100644 index dc0da3039..000000000 Binary files a/website/versioned_docs/version-0.10.7/images/top_contributors/MioLogo.png and /dev/null differ diff --git a/website/versioned_sidebars/version-0.10.10-sidebars.json b/website/versioned_sidebars/version-0.10.10-sidebars.json deleted file mode 100644 index caea0c03b..000000000 --- a/website/versioned_sidebars/version-0.10.10-sidebars.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "tutorialSidebar": [ - { - "type": "autogenerated", - "dirName": "." - } - ] -} diff --git a/website/versioned_sidebars/version-0.10.6-sidebars.json b/website/versioned_sidebars/version-0.10.6-sidebars.json deleted file mode 100644 index caea0c03b..000000000 --- a/website/versioned_sidebars/version-0.10.6-sidebars.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "tutorialSidebar": [ - { - "type": "autogenerated", - "dirName": "." - } - ] -} diff --git a/website/versioned_sidebars/version-0.10.7-sidebars.json b/website/versioned_sidebars/version-0.10.7-sidebars.json deleted file mode 100644 index caea0c03b..000000000 --- a/website/versioned_sidebars/version-0.10.7-sidebars.json +++ /dev/null @@ -1,8 +0,0 @@ -{ - "tutorialSidebar": [ - { - "type": "autogenerated", - "dirName": "." - } - ] -}