diff options
104 files changed, 3312 insertions, 0 deletions
diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..adfa9bf --- /dev/null +++ b/.gitignore @@ -0,0 +1,19 @@ +*.iml +.kotlin +.gradle +**/build/ +xcuserdata +!src/**/build/ +local.properties +.idea +.DS_Store +captures +.externalNativeBuild +.cxx +*.xcodeproj/* +!*.xcodeproj/project.pbxproj +!*.xcodeproj/xcshareddata/ +!*.xcodeproj/project.xcworkspace/ +!*.xcworkspace/contents.xcworkspacedata +**/xcshareddata/WorkspaceSettings.xcsettings +node_modules/ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..c0da46a --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,88 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +This is a Kotlin Multiplatform project using Compose Multiplatform, targeting Android, iOS, and Desktop (JVM). The project is called "The Abstraction Engine" with the package namespace `coffee.liz.abstractionengine`. + +## Build Commands + +### Building +- **Android Debug Build**: `./gradlew :composeApp:assembleDebug` +- **Android Release Build**: `./gradlew :composeApp:assembleRelease` +- **Desktop Build**: `./gradlew :composeApp:packageDistributionForCurrentOS` + +### Running +- **Desktop App**: `./gradlew :composeApp:run` +- **iOS App**: Open `iosApp/` directory in Xcode and run from there +- **Android App**: Use Android Studio run configurations or `./gradlew :composeApp:installDebug` + +### Testing +- **Run Common Tests**: `./gradlew :composeApp:testDebugUnitTest` +- **Run All Tests**: `./gradlew test` + +### Clean +- **Clean Build**: `./gradlew clean` + +## Architecture + +### Source Structure + +The project follows Kotlin Multiplatform's platform-specific source set structure: + +- **composeApp/src/commonMain/kotlin**: Shared code for all platforms + - Contains the main `App.kt` composable and shared business logic + - Uses Compose Multiplatform for cross-platform UI + - Platform-agnostic interface definitions (e.g., `Platform.kt`) + +- **composeApp/src/androidMain/kotlin**: Android-specific implementations + - `MainActivity.kt`: Android app entry point + - `Platform.android.kt`: Android platform implementations + +- **composeApp/src/iosMain/kotlin**: iOS-specific implementations + - `MainViewController.kt`: iOS app entry point + - `Platform.ios.kt`: iOS platform implementations + +- **composeApp/src/jvmMain/kotlin**: Desktop (JVM) specific implementations + - `main.kt`: Desktop app entry point with window configuration + - `Platform.jvm.kt`: JVM platform implementations + +- **iosApp/**: Native iOS app wrapper that hosts the Compose Multiplatform framework + +### Platform Abstraction Pattern + +The codebase uses the expect/actual pattern for platform-specific implementations: +1. Define an `expect` declaration in `commonMain` (e.g., `Platform.kt`) +2. Provide `actual` implementations in each platform source set (androidMain, iosMain, jvmMain) + +### Key Dependencies + +- **Compose Multiplatform 1.9.0**: UI framework +- **Kotlin 2.2.20**: Language version +- **AndroidX Lifecycle 2.9.4**: ViewModel and lifecycle support +- **Compose Hot Reload**: Development feature for faster iteration + +### Build Configuration + +- Uses Gradle version catalogs (`gradle/libs.versions.toml`) for dependency management +- Android targets API 36, minimum API 24 +- JVM target is Java 11 +- iOS targets arm64 and simulator arm64 +- Desktop distribution formats: DMG (macOS), MSI (Windows), DEB (Linux) + +## Development Notes + +### Entry Points +- **Desktop**: `coffee.liz.abstractionengine.MainKt` (composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt:6) +- **Android**: `MainActivity` (composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt) +- **iOS**: `MainViewController` (composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt) + +### Adding Platform-Specific Code +When adding functionality that requires platform APIs: +1. Add the interface/expect declaration in `commonMain/kotlin/coffee/liz/abstractionengine` +2. Implement actual platform-specific versions in the corresponding platform source sets +3. Platform implementations can use native APIs (Android SDK, iOS UIKit, JVM Swing) + +### Resource Management +Compose resources are stored in `composeApp/src/commonMain/composeResources/` and are accessible via the generated `Res` object. diff --git a/README.md b/README.md new file mode 100644 index 0000000..3cc70ff --- /dev/null +++ b/README.md @@ -0,0 +1,48 @@ +This is a Kotlin Multiplatform project targeting Android, iOS, Desktop (JVM). + +* [/composeApp](./composeApp/src) is for code that will be shared across your Compose Multiplatform applications. + It contains several subfolders: + - [commonMain](./composeApp/src/commonMain/kotlin) is for code that’s common for all targets. + - Other folders are for Kotlin code that will be compiled for only the platform indicated in the folder name. + For example, if you want to use Apple’s CoreCrypto for the iOS part of your Kotlin app, + the [iosMain](./composeApp/src/iosMain/kotlin) folder would be the right place for such calls. + Similarly, if you want to edit the Desktop (JVM) specific part, the [jvmMain](./composeApp/src/jvmMain/kotlin) + folder is the appropriate location. + +* [/iosApp](./iosApp/iosApp) contains iOS applications. Even if you’re sharing your UI with Compose Multiplatform, + you need this entry point for your iOS app. This is also where you should add SwiftUI code for your project. + +### Build and Run Android Application + +To build and run the development version of the Android app, use the run configuration from the run widget +in your IDE’s toolbar or build it directly from the terminal: +- on macOS/Linux + ```shell + ./gradlew :composeApp:assembleDebug + ``` +- on Windows + ```shell + .\gradlew.bat :composeApp:assembleDebug + ``` + +### Build and Run Desktop (JVM) Application + +To build and run the development version of the desktop app, use the run configuration from the run widget +in your IDE’s toolbar or run it directly from the terminal: +- on macOS/Linux + ```shell + ./gradlew :composeApp:run + ``` +- on Windows + ```shell + .\gradlew.bat :composeApp:run + ``` + +### Build and Run iOS Application + +To build and run the development version of the iOS app, use the run configuration from the run widget +in your IDE’s toolbar or open the [/iosApp](./iosApp) directory in Xcode and run it from there. + +--- + +Learn more about [Kotlin Multiplatform](https://www.jetbrains.com/help/kotlin-multiplatform-dev/get-started.html)…
\ No newline at end of file diff --git a/build.gradle.kts b/build.gradle.kts new file mode 100644 index 0000000..98ddb8c --- /dev/null +++ b/build.gradle.kts @@ -0,0 +1,10 @@ +plugins { + // this is necessary to avoid the plugins to be loaded multiple times + // in each subproject's classloader + alias(libs.plugins.androidApplication) apply false + alias(libs.plugins.androidLibrary) apply false + alias(libs.plugins.composeHotReload) apply false + alias(libs.plugins.composeMultiplatform) apply false + alias(libs.plugins.composeCompiler) apply false + alias(libs.plugins.kotlinMultiplatform) apply false +}
\ No newline at end of file diff --git a/composeApp/build.gradle.kts b/composeApp/build.gradle.kts new file mode 100644 index 0000000..9e17fc5 --- /dev/null +++ b/composeApp/build.gradle.kts @@ -0,0 +1,101 @@ +import org.jetbrains.compose.desktop.application.dsl.TargetFormat +import org.jetbrains.kotlin.gradle.dsl.JvmTarget + +plugins { + alias(libs.plugins.kotlinMultiplatform) + alias(libs.plugins.androidApplication) + alias(libs.plugins.composeMultiplatform) + alias(libs.plugins.composeCompiler) + alias(libs.plugins.composeHotReload) +} + +kotlin { + compilerOptions { + freeCompilerArgs.add("-opt-in=kotlin.time.ExperimentalTime") + } + + androidTarget { + compilerOptions { + jvmTarget.set(JvmTarget.JVM_11) + } + } + + listOf( + iosArm64(), + iosSimulatorArm64() + ).forEach { iosTarget -> + iosTarget.binaries.framework { + baseName = "ComposeApp" + isStatic = true + } + } + + jvm() + + sourceSets { + androidMain.dependencies { + implementation(compose.preview) + implementation(libs.androidx.activity.compose) + } + commonMain.dependencies { + implementation(compose.runtime) + implementation(compose.foundation) + implementation(compose.material3) + implementation(compose.ui) + implementation(compose.components.resources) + implementation(compose.components.uiToolingPreview) + implementation(libs.androidx.lifecycle.viewmodelCompose) + implementation(libs.androidx.lifecycle.runtimeCompose) + } + commonTest.dependencies { + implementation(libs.kotlin.test) + } + jvmMain.dependencies { + implementation(compose.desktop.currentOs) + implementation(libs.kotlinx.coroutinesSwing) + } + } +} + +android { + namespace = "coffee.liz.abstractionengine" + compileSdk = libs.versions.android.compileSdk.get().toInt() + + defaultConfig { + applicationId = "coffee.liz.abstractionengine" + minSdk = libs.versions.android.minSdk.get().toInt() + targetSdk = libs.versions.android.targetSdk.get().toInt() + versionCode = 1 + versionName = "1.0" + } + packaging { + resources { + excludes += "/META-INF/{AL2.0,LGPL2.1}" + } + } + buildTypes { + getByName("release") { + isMinifyEnabled = false + } + } + compileOptions { + sourceCompatibility = JavaVersion.VERSION_11 + targetCompatibility = JavaVersion.VERSION_11 + } +} + +dependencies { + debugImplementation(compose.uiTooling) +} + +compose.desktop { + application { + mainClass = "coffee.liz.abstractionengine.MainKt" + + nativeDistributions { + targetFormats(TargetFormat.Dmg, TargetFormat.Msi, TargetFormat.Deb) + packageName = "coffee.liz.abstractionengine" + packageVersion = "1.0.0" + } + } +} diff --git a/composeApp/src/androidMain/AndroidManifest.xml b/composeApp/src/androidMain/AndroidManifest.xml new file mode 100644 index 0000000..26403a7 --- /dev/null +++ b/composeApp/src/androidMain/AndroidManifest.xml @@ -0,0 +1,22 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android"> + + <application + android:allowBackup="true" + android:icon="@mipmap/ic_launcher" + android:label="@string/app_name" + android:roundIcon="@mipmap/ic_launcher_round" + android:supportsRtl="true" + android:theme="@android:style/Theme.Material.Light.NoActionBar"> + <activity + android:exported="true" + android:name=".MainActivity"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + </activity> + </application> + +</manifest>
\ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt new file mode 100644 index 0000000..49d0dfa --- /dev/null +++ b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/MainActivity.kt @@ -0,0 +1,25 @@ +package coffee.liz.abstractionengine + +import android.os.Bundle +import androidx.activity.ComponentActivity +import androidx.activity.compose.setContent +import androidx.activity.enableEdgeToEdge +import androidx.compose.runtime.Composable +import androidx.compose.ui.tooling.preview.Preview + +class MainActivity : ComponentActivity() { + override fun onCreate(savedInstanceState: Bundle?) { + enableEdgeToEdge() + super.onCreate(savedInstanceState) + + setContent { + App() + } + } +} + +@Preview +@Composable +fun AppAndroidPreview() { + App() +}
\ No newline at end of file diff --git a/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt new file mode 100644 index 0000000..3ab6a6a --- /dev/null +++ b/composeApp/src/androidMain/kotlin/coffee/liz/abstractionengine/Platform.android.kt @@ -0,0 +1,9 @@ +package coffee.liz.abstractionengine + +import android.os.Build + +class AndroidPlatform : Platform { + override val name: String = "Android ${Build.VERSION.SDK_INT}" +} + +actual fun getPlatform(): Platform = AndroidPlatform()
\ No newline at end of file diff --git a/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml new file mode 100644 index 0000000..2b068d1 --- /dev/null +++ b/composeApp/src/androidMain/res/drawable-v24/ic_launcher_foreground.xml @@ -0,0 +1,30 @@ +<vector xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path android:pathData="M31,63.928c0,0 6.4,-11 12.1,-13.1c7.2,-2.6 26,-1.4 26,-1.4l38.1,38.1L107,108.928l-32,-1L31,63.928z"> + <aapt:attr name="android:fillColor"> + <gradient + android:endX="85.84757" + android:endY="92.4963" + android:startX="42.9492" + android:startY="49.59793" + android:type="linear"> + <item + android:color="#44000000" + android:offset="0.0" /> + <item + android:color="#00000000" + android:offset="1.0" /> + </gradient> + </aapt:attr> + </path> + <path + android:fillColor="#FFFFFF" + android:fillType="nonZero" + android:pathData="M65.3,45.828l3.8,-6.6c0.2,-0.4 0.1,-0.9 -0.3,-1.1c-0.4,-0.2 -0.9,-0.1 -1.1,0.3l-3.9,6.7c-6.3,-2.8 -13.4,-2.8 -19.7,0l-3.9,-6.7c-0.2,-0.4 -0.7,-0.5 -1.1,-0.3C38.8,38.328 38.7,38.828 38.9,39.228l3.8,6.6C36.2,49.428 31.7,56.028 31,63.928h46C76.3,56.028 71.8,49.428 65.3,45.828zM43.4,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2c-0.3,-0.7 -0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C45.3,56.528 44.5,57.328 43.4,57.328L43.4,57.328zM64.6,57.328c-0.8,0 -1.5,-0.5 -1.8,-1.2s-0.1,-1.5 0.4,-2.1c0.5,-0.5 1.4,-0.7 2.1,-0.4c0.7,0.3 1.2,1 1.2,1.8C66.5,56.528 65.6,57.328 64.6,57.328L64.6,57.328z" + android:strokeWidth="1" + android:strokeColor="#00000000" /> +</vector>
\ No newline at end of file diff --git a/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml new file mode 100644 index 0000000..e93e11a --- /dev/null +++ b/composeApp/src/androidMain/res/drawable/ic_launcher_background.xml @@ -0,0 +1,170 @@ +<?xml version="1.0" encoding="utf-8"?> +<vector xmlns:android="http://schemas.android.com/apk/res/android" + android:width="108dp" + android:height="108dp" + android:viewportWidth="108" + android:viewportHeight="108"> + <path + android:fillColor="#3DDC84" + android:pathData="M0,0h108v108h-108z" /> + <path + android:fillColor="#00000000" + android:pathData="M9,0L9,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,0L19,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M29,0L29,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M39,0L39,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M49,0L49,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M59,0L59,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M69,0L69,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M79,0L79,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M89,0L89,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M99,0L99,108" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,9L108,9" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,19L108,19" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,29L108,29" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,39L108,39" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,49L108,49" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,59L108,59" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,69L108,69" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,79L108,79" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,89L108,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M0,99L108,99" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,29L89,29" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,39L89,39" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,49L89,49" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,59L89,59" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,69L89,69" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M19,79L89,79" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M29,19L29,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M39,19L39,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M49,19L49,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M59,19L59,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M69,19L69,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> + <path + android:fillColor="#00000000" + android:pathData="M79,19L79,89" + android:strokeWidth="0.8" + android:strokeColor="#33FFFFFF" /> +</vector>
\ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml new file mode 100644 index 0000000..345888d --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher.xml @@ -0,0 +1,6 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@mipmap/ic_launcher_background"/> + <foreground android:drawable="@mipmap/ic_launcher_foreground"/> + <monochrome android:drawable="@mipmap/ic_launcher_monochrome"/> +</adaptive-icon>
\ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml new file mode 100644 index 0000000..eca70cf --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-anydpi-v26/ic_launcher_round.xml @@ -0,0 +1,5 @@ +<?xml version="1.0" encoding="utf-8"?> +<adaptive-icon xmlns:android="http://schemas.android.com/apk/res/android"> + <background android:drawable="@drawable/ic_launcher_background" /> + <foreground android:drawable="@drawable/ic_launcher_foreground" /> +</adaptive-icon>
\ No newline at end of file diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..579c9e1 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher.png diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.png Binary files differnew file mode 100644 index 0000000..d96f4cd --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_background.png diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000..8a44498 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_foreground.png diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.png Binary files differnew file mode 100644 index 0000000..1f56bb1 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_monochrome.png diff --git a/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..61da551 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-hdpi/ic_launcher_round.png diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..85fae95 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher.png diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.png Binary files differnew file mode 100644 index 0000000..d22fcbb --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_background.png diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000..344dfe0 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_foreground.png diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.png Binary files differnew file mode 100644 index 0000000..f942995 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_monochrome.png diff --git a/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..db5080a --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-mdpi/ic_launcher_round.png diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..e0d00ef --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher.png diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.png Binary files differnew file mode 100644 index 0000000..7b4d63b --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_background.png diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000..3f51c64 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_foreground.png diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.png Binary files differnew file mode 100644 index 0000000..baddf51 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_monochrome.png diff --git a/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..da31a87 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xhdpi/ic_launcher_round.png diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..a8aae0a --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher.png diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.png Binary files differnew file mode 100644 index 0000000..762d687 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_background.png diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000..dde1f38 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_foreground.png diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.png Binary files differnew file mode 100644 index 0000000..75429fe --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_monochrome.png diff --git a/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..b216f2d --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxhdpi/ic_launcher_round.png diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png Binary files differnew file mode 100644 index 0000000..ed1272b --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher.png diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.png Binary files differnew file mode 100644 index 0000000..b3af25d --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_background.png diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png Binary files differnew file mode 100644 index 0000000..e92bedb --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_foreground.png diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.png Binary files differnew file mode 100644 index 0000000..ac0eee1 --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_monochrome.png diff --git a/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png Binary files differnew file mode 100644 index 0000000..e96783c --- /dev/null +++ b/composeApp/src/androidMain/res/mipmap-xxxhdpi/ic_launcher_round.png diff --git a/composeApp/src/androidMain/res/play_store_512.png b/composeApp/src/androidMain/res/play_store_512.png Binary files differnew file mode 100644 index 0000000..e44c917 --- /dev/null +++ b/composeApp/src/androidMain/res/play_store_512.png diff --git a/composeApp/src/androidMain/res/values/strings.xml b/composeApp/src/androidMain/res/values/strings.xml new file mode 100644 index 0000000..ffcea6e --- /dev/null +++ b/composeApp/src/androidMain/res/values/strings.xml @@ -0,0 +1,3 @@ +<resources> + <string name="app_name">The Abstraction Engine</string> +</resources>
\ No newline at end of file diff --git a/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml new file mode 100644 index 0000000..1ffc948 --- /dev/null +++ b/composeApp/src/commonMain/composeResources/drawable/compose-multiplatform.xml @@ -0,0 +1,44 @@ +<vector + xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:aapt="http://schemas.android.com/aapt" + android:width="450dp" + android:height="450dp" + android:viewportWidth="64" + android:viewportHeight="64"> + <path + android:pathData="M56.25,18V46L32,60 7.75,46V18L32,4Z" + android:fillColor="#6075f2"/> + <path + android:pathData="m41.5,26.5v11L32,43V60L56.25,46V18Z" + android:fillColor="#6b57ff"/> + <path + android:pathData="m32,43 l-9.5,-5.5v-11L7.75,18V46L32,60Z"> + <aapt:attr name="android:fillColor"> + <gradient + android:centerX="23.131" + android:centerY="18.441" + android:gradientRadius="42.132" + android:type="radial"> + <item android:offset="0" android:color="#FF5383EC"/> + <item android:offset="0.867" android:color="#FF7F52FF"/> + </gradient> + </aapt:attr> + </path> + <path + android:pathData="M22.5,26.5 L32,21 41.5,26.5 56.25,18 32,4 7.75,18Z"> + <aapt:attr name="android:fillColor"> + <gradient + android:startX="44.172" + android:startY="4.377" + android:endX="17.973" + android:endY="34.035" + android:type="linear"> + <item android:offset="0" android:color="#FF33C3FF"/> + <item android:offset="0.878" android:color="#FF5383EC"/> + </gradient> + </aapt:attr> + </path> + <path + android:pathData="m32,21 l9.526,5.5v11L32,43 22.474,37.5v-11z" + android:fillColor="#000000"/> +</vector>
\ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/AbstractionEngine.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/AbstractionEngine.kt new file mode 100644 index 0000000..d557fd8 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/AbstractionEngine.kt @@ -0,0 +1,5 @@ +package coffee.liz.abstractionengine + +class AbstractionEngine { + +}
\ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/App.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/App.kt new file mode 100644 index 0000000..a69e711 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/App.kt @@ -0,0 +1,28 @@ +package coffee.liz.abstractionengine + +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import coffee.liz.abstractionengine.ui.ArcadeControls +import coffee.liz.abstractionengine.ui.GameBoyTheme +import org.jetbrains.compose.ui.tooling.preview.Preview + +@Composable +@Preview +fun App() { + MaterialTheme(colorScheme = GameBoyTheme) { + ArcadeControls( + onDirectionPressed = { direction -> + println("Direction pressed: $direction") + }, + onActionA = { + println("Action A pressed!") + }, + onActionB = { + println("Action B pressed!") + }, + modifier = Modifier.fillMaxSize() + ) + } +}
\ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Greeting.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Greeting.kt new file mode 100644 index 0000000..6f23320 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Greeting.kt @@ -0,0 +1,9 @@ +package coffee.liz.abstractionengine + +class Greeting { + private val platform = getPlatform() + + fun greet(): String { + return "Hello, ${platform.name}!" + } +}
\ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Platform.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Platform.kt new file mode 100644 index 0000000..91faf62 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/Platform.kt @@ -0,0 +1,7 @@ +package coffee.liz.abstractionengine + +interface Platform { + val name: String +} + +expect fun getPlatform(): Platform
\ No newline at end of file diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt new file mode 100644 index 0000000..9490f83 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/Game.kt @@ -0,0 +1,77 @@ +package coffee.liz.abstractionengine.game + +import androidx.compose.foundation.Canvas +import androidx.compose.runtime.* +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import coffee.liz.ecs.World +import kotlinx.coroutines.isActive + +/** + * Manages the game loop for an ECS world. + * Syncs with Compose's frame rate (like requestAnimationFrame) and provides + * the world to child content for custom rendering/UI. + * + * @param world The ECS world containing entities and systems + * @param content Composable content that can use the world for rendering + */ +@Composable +fun Game( + world: World, + content: @Composable () -> Unit +) { + // Trigger recomposition when we need to redraw + var frameCount by remember { mutableStateOf(0) } + + // Game loop - syncs with Compose's frame rate like requestAnimationFrame + LaunchedEffect(world) { + var lastFrameTimeNanos = 0L + + while (isActive) { + // Wait for next frame (like requestAnimationFrame) + withFrameNanos { frameTimeNanos -> + val deltaTime = if (lastFrameTimeNanos != 0L) { + (frameTimeNanos - lastFrameTimeNanos) / 1_000_000_000f + } else { + 0f + } + lastFrameTimeNanos = frameTimeNanos + + // Update ECS world (runs all systems) + if (deltaTime > 0f) { + world.update(deltaTime) + } + + // Trigger recomposition to redraw + frameCount++ + } + } + } + + // Render content + content() +} + +/** + * A Canvas that renders sprites from an ECS world using a RenderSystem. + * + * @param world The ECS world containing entities to render + * @param renderSystem The system responsible for rendering sprites + * @param backgroundColor Background color for the canvas + * @param modifier Modifier for the Canvas + */ +@Composable +fun GameCanvas( + world: World, + renderSystem: RenderSystem, + backgroundColor: Color = Color.Transparent, + modifier: Modifier = Modifier +) { + Canvas(modifier = modifier) { + // Clear background + drawRect(backgroundColor) + + // Render all sprites + renderSystem.render(world, this) + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt new file mode 100644 index 0000000..0722535 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/ImageCache.kt @@ -0,0 +1,23 @@ +package coffee.liz.abstractionengine.game + +import androidx.compose.ui.graphics.ImageBitmap + +/** + * Simple cache for loaded images. + * In a real game, you'd want more sophisticated resource management. + */ +class ImageCache { + private val images = mutableMapOf<String, ImageBitmap>() + + fun loadImage(path: String, image: ImageBitmap) { + images[path] = image + } + + fun getImage(path: String): ImageBitmap? { + return images[path] + } + + fun clear() { + images.clear() + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt new file mode 100644 index 0000000..1d22ae6 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/game/RenderSystem.kt @@ -0,0 +1,70 @@ +package coffee.liz.abstractionengine.game + +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.geometry.Size +import androidx.compose.ui.graphics.drawscope.DrawScope +import androidx.compose.ui.unit.IntOffset +import androidx.compose.ui.unit.IntSize +import coffee.liz.ecs.System +import coffee.liz.ecs.World +import coffee.liz.ecs.animation.AnimationSystem +import coffee.liz.ecs.animation.Animator +import coffee.liz.ecs.animation.Position +import coffee.liz.ecs.animation.SpriteSheet +import kotlin.reflect.KClass + +/** + * System that renders sprites to a DrawScope. + * + * This system queries all entities with Position, SpriteSheet, and Animator components, + * then draws their current animation frame to the provided DrawScope. + * + * Depends on AnimationSystem to ensure animations are updated before rendering. + */ +class RenderSystem( + private val imageCache: ImageCache +) : System { + + override val dependencies: Set<KClass<out System>> = setOf(AnimationSystem::class) + + /** + * The update method is required by the System interface, but rendering + * happens synchronously during Compose's draw phase via the render() method. + */ + override fun update(world: World, deltaTime: Float) { + // Rendering happens in render() method, not here + } + + /** + * Renders all entities with sprites to the given DrawScope. + * This should be called from a Compose Canvas during the draw phase. + */ + fun render(world: World, drawScope: DrawScope) { + // Query all entities that can be rendered + world.query(Position::class, SpriteSheet::class, Animator::class).forEach { entity -> + val position = entity.get(Position::class) ?: return@forEach + val spriteSheet = entity.get(SpriteSheet::class) ?: return@forEach + val animator = entity.get(Animator::class) ?: return@forEach + + // Get the current frame name from the animator + val frameName = animator.getCurrentFrameName() ?: return@forEach + + // Look up the frame rectangle in the sprite sheet + val frameRect = spriteSheet.frames[frameName] ?: return@forEach + + // Get the image from cache + val image = imageCache.getImage(spriteSheet.imagePath) ?: return@forEach + + // Draw the sprite + with(drawScope) { + drawImage( + image = image, + srcOffset = IntOffset(frameRect.x, frameRect.y), + srcSize = IntSize(frameRect.width, frameRect.height), + dstOffset = IntOffset(position.x.toInt(), position.y.toInt()), + dstSize = IntSize(frameRect.width, frameRect.height) + ) + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt new file mode 100644 index 0000000..9d50192 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeButton.kt @@ -0,0 +1,52 @@ +package coffee.liz.abstractionengine.ui + +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.PaddingValues +import androidx.compose.foundation.layout.size +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.material3.Button +import androidx.compose.material3.ButtonDefaults +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +enum class ArcadeButtonColor { + RED, YELLOW +} + +@Composable +fun ArcadeButton( + label: String, + color: ArcadeButtonColor = ArcadeButtonColor.RED, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + val buttonColor = when (color) { + ArcadeButtonColor.RED -> GameBoyColors.ButtonRed + ArcadeButtonColor.YELLOW -> GameBoyColors.ButtonYellow + } + + Button( + onClick = onClick, + modifier = modifier + .size(65.dp) + .border(1.dp, MaterialTheme.colorScheme.outline, CircleShape), + shape = CircleShape, + colors = ButtonDefaults.buttonColors( + containerColor = buttonColor, + contentColor = MaterialTheme.colorScheme.onPrimary + ), + contentPadding = PaddingValues(0.dp) + ) { + Text( + text = label, + fontSize = 20.sp, + fontWeight = FontWeight.Bold, + color = MaterialTheme.colorScheme.onPrimary + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeControls.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeControls.kt new file mode 100644 index 0000000..f5b4839 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/ArcadeControls.kt @@ -0,0 +1,130 @@ +package coffee.liz.abstractionengine.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.MaterialTheme +import androidx.compose.material3.Text +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.text.font.FontFamily +import androidx.compose.ui.text.font.FontWeight +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +@Composable +fun ArcadeControls( + onDirectionPressed: (Direction) -> Unit, + onActionA: () -> Unit, + onActionB: () -> Unit, + modifier: Modifier = Modifier, + gameContent: @Composable BoxScope.() -> Unit = { + // Default placeholder content + Column( + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + Text( + text = "GAME AREA", + fontSize = 18.sp, + fontWeight = FontWeight.Bold, + fontFamily = FontFamily.Monospace, + color = MaterialTheme.colorScheme.onPrimary + ) + } + } +) { + var lastDirection by remember { mutableStateOf<Direction?>(null) } + var lastAction by remember { mutableStateOf<String?>(null) } + + Box(modifier = modifier.fillMaxSize()) { + // PCB background layer + PCBBackground() + + // Transparent casing overlay + Box( + modifier = Modifier + .fillMaxSize() + .background(Color(0x33000000)) + ) + + // Content + Column( + modifier = Modifier + .fillMaxSize() + .padding(16.dp), + horizontalAlignment = Alignment.CenterHorizontally, + verticalArrangement = Arrangement.spacedBy(16.dp) + ) { + // Game area (square) + Box( + modifier = Modifier + .fillMaxWidth(0.95f) + .aspectRatio(1f) + .background( + color = GameBoyColors.ScreenGreen.copy(alpha = 0.85f), + shape = RoundedCornerShape(8.dp) + ) + .border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(8.dp)) + .padding(8.dp), + contentAlignment = Alignment.Center, + content = gameContent + ) + + // Control panel + Box( + modifier = Modifier + .fillMaxWidth(0.95f) + .background( + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.7f), + shape = RoundedCornerShape(16.dp) + ) + .border(1.dp, MaterialTheme.colorScheme.outline, RoundedCornerShape(16.dp)) + .padding(horizontal = 16.dp, vertical = 16.dp), + contentAlignment = Alignment.Center + ) { + Row( + modifier = Modifier.fillMaxWidth(), + horizontalArrangement = Arrangement.SpaceEvenly, + verticalAlignment = Alignment.CenterVertically + ) { + // D-Pad on the left + DPad( + onDirectionPressed = { direction -> + lastDirection = direction + lastAction = null + onDirectionPressed(direction) + } + ) + + // Action buttons on the right + Row( + horizontalArrangement = Arrangement.spacedBy(12.dp) + ) { + ArcadeButton( + label = "B", + color = ArcadeButtonColor.YELLOW, + onClick = { + lastAction = "B" + lastDirection = null + onActionB() + } + ) + ArcadeButton( + label = "A", + color = ArcadeButtonColor.RED, + onClick = { + lastAction = "A" + lastDirection = null + onActionA() + } + ) + } + } + } + } + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/DPad.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/DPad.kt new file mode 100644 index 0000000..52f5866 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/DPad.kt @@ -0,0 +1,105 @@ +package coffee.liz.abstractionengine.ui + +import androidx.compose.foundation.background +import androidx.compose.foundation.border +import androidx.compose.foundation.layout.* +import androidx.compose.foundation.shape.CircleShape +import androidx.compose.foundation.shape.RoundedCornerShape +import androidx.compose.material3.* +import androidx.compose.runtime.* +import androidx.compose.ui.Alignment +import androidx.compose.ui.Modifier +import androidx.compose.ui.unit.dp +import androidx.compose.ui.unit.sp + +enum class Direction { + UP, DOWN, LEFT, RIGHT +} + +@Composable +fun DPad( + onDirectionPressed: (Direction) -> Unit, + modifier: Modifier = Modifier +) { + Box( + modifier = modifier + .size(140.dp) + .background( + color = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), + shape = CircleShape + ) + .border(1.dp, MaterialTheme.colorScheme.outline, CircleShape) + ) { + // Up button + DirectionButton( + text = "▲", + onClick = { onDirectionPressed(Direction.UP) }, + modifier = Modifier + .align(Alignment.TopCenter) + .offset(y = 20.dp) + ) + + // Down button + DirectionButton( + text = "▼", + onClick = { onDirectionPressed(Direction.DOWN) }, + modifier = Modifier + .align(Alignment.BottomCenter) + .offset(y = (-20).dp) + ) + + // Left button + DirectionButton( + text = "◀", + onClick = { onDirectionPressed(Direction.LEFT) }, + modifier = Modifier + .align(Alignment.CenterStart) + .offset(x = 20.dp) + ) + + // Right button + DirectionButton( + text = "▶", + onClick = { onDirectionPressed(Direction.RIGHT) }, + modifier = Modifier + .align(Alignment.CenterEnd) + .offset(x = (-20).dp) + ) + + // Center circle + Box( + modifier = Modifier + .align(Alignment.Center) + .size(38.dp) + .background( + color = MaterialTheme.colorScheme.surface.copy(alpha = 0.5f), + shape = CircleShape + ) + .border(1.dp, MaterialTheme.colorScheme.outline, CircleShape) + ) + } +} + +@Composable +private fun DirectionButton( + text: String, + onClick: () -> Unit, + modifier: Modifier = Modifier +) { + Button( + onClick = onClick, + modifier = modifier.size(38.dp), + shape = RoundedCornerShape(4.dp), + colors = ButtonDefaults.buttonColors( + containerColor = MaterialTheme.colorScheme.surfaceVariant.copy(alpha = 0.6f), + contentColor = MaterialTheme.colorScheme.onSurface.copy(alpha = 0.3f) + ), + contentPadding = PaddingValues(0.dp) + ) { + Text( + text = text, + fontSize = 14.sp, + style = MaterialTheme.typography.headlineMedium + ) + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/PCBBackground.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/PCBBackground.kt new file mode 100644 index 0000000..be35a1a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/PCBBackground.kt @@ -0,0 +1,85 @@ +package coffee.liz.abstractionengine.ui + +import androidx.compose.foundation.Canvas +import androidx.compose.foundation.background +import androidx.compose.foundation.layout.fillMaxSize +import androidx.compose.material3.MaterialTheme +import androidx.compose.runtime.Composable +import androidx.compose.ui.Modifier +import androidx.compose.ui.geometry.Offset +import androidx.compose.ui.graphics.Color +import androidx.compose.ui.graphics.PathEffect + +@Composable +fun PCBBackground(modifier: Modifier = Modifier) { + val pcbGreen = Color(0xFF0D5F0D) + val traceColor = Color(0xFF1A7F1A) + val padColor = Color(0xFFD4AF37) + + Canvas(modifier = modifier.fillMaxSize().background(pcbGreen)) { + val width = size.width + val height = size.height + + // Draw circuit traces (horizontal and vertical lines) + val pathEffect = PathEffect.dashPathEffect(floatArrayOf(10f, 5f), 0f) + + // Horizontal traces + for (i in 0..15) { + val y = (i * height / 15) + drawLine( + color = traceColor, + start = Offset(0f, y), + end = Offset(width, y), + strokeWidth = 2f, + pathEffect = pathEffect + ) + } + + // Vertical traces + for (i in 0..10) { + val x = (i * width / 10) + drawLine( + color = traceColor, + start = Offset(x, 0f), + end = Offset(x, height), + strokeWidth = 2f, + pathEffect = pathEffect + ) + } + + // Draw pads/components (small circles at intersections) + for (i in 0..10 step 2) { + for (j in 0..15 step 3) { + val x = i * width / 10 + val y = j * height / 15 + drawCircle( + color = padColor, + radius = 4f, + center = Offset(x, y) + ) + } + } + + // Draw some resistor-like rectangles + for (i in 1..9 step 4) { + val x = i * width / 10 + val y = height / 2 + drawRect( + color = Color(0xFF2A4A2A), + topLeft = Offset(x - 15f, y - 8f), + size = androidx.compose.ui.geometry.Size(30f, 16f) + ) + // Resistor contacts + drawCircle( + color = padColor, + radius = 3f, + center = Offset(x - 15f, y) + ) + drawCircle( + color = padColor, + radius = 3f, + center = Offset(x + 15f, y) + ) + } + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt new file mode 100644 index 0000000..661cb09 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/abstractionengine/ui/Theme.kt @@ -0,0 +1,32 @@ +package coffee.liz.abstractionengine.ui + +import androidx.compose.material3.darkColorScheme +import androidx.compose.ui.graphics.Color + +// GameBoy-inspired color palette +object GameBoyColors { + val DarkestGreen = Color(0xFF0F380F) + val DarkGreen = Color(0xFF306230) + val MediumGreen = Color(0xFF8BAC0F) + val LightGreen = Color(0xFF9BBC0F) + val ScreenGreen = Color(0xFF8BAC0F) + + // Accent colors for buttons (still retro but with more variety) + val ButtonRed = Color(0xFFE76F51) + val ButtonYellow = Color(0xFFF4A261) + val DPadGray = Color(0xFF4A5759) + val DPadLight = Color(0xFF6B7F82) +} + +val GameBoyTheme = darkColorScheme( + primary = GameBoyColors.MediumGreen, + onPrimary = GameBoyColors.DarkestGreen, + secondary = GameBoyColors.LightGreen, + onSecondary = GameBoyColors.DarkestGreen, + background = GameBoyColors.DarkestGreen, + onBackground = GameBoyColors.LightGreen, + surface = GameBoyColors.DarkGreen, + onSurface = GameBoyColors.LightGreen, + surfaceVariant = GameBoyColors.DPadGray, + outline = GameBoyColors.DarkestGreen, +) diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt new file mode 100644 index 0000000..c64a863 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Component.kt @@ -0,0 +1,7 @@ +package coffee.liz.ecs + +/** + * Marker interface for all components. + * Components are pure data containers with no behavior. + */ +interface Component diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt new file mode 100644 index 0000000..d314065 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/DAGWorld.kt @@ -0,0 +1,127 @@ +package coffee.liz.ecs + +import kotlin.reflect.KClass + +/** + * World implementation that executes systems in dependency order using a DAG. + */ +class DAGWorld(systems: List<System>) : World { + private val entities = mutableSetOf<Entity>() + private val componentCache = mutableMapOf<KClass<out Component>, MutableSet<Entity>>() + private val systemExecutionOrder: List<System> + private var nextEntityId = 0 + + init { + systemExecutionOrder = buildExecutionOrder(systems) + } + + override fun createEntity(): Entity { + val entity = Entity(nextEntityId++) + entities.add(entity) + return entity + } + + override fun destroyEntity(entity: Entity) { + // Remove from all component caches + entity.componentTypes().forEach { componentType -> + componentCache[componentType]?.remove(entity) + } + entities.remove(entity) + } + + override fun query(vararg componentTypes: KClass<out Component>): Set<Entity> { + if (componentTypes.isEmpty()) return entities.toSet() + + // Start with entities that have the first component type + val firstType = componentTypes[0] + val candidates = componentCache[firstType] ?: return emptySet() + + // Filter to entities that have all component types + return candidates.filter { entity -> + entity.hasAll(*componentTypes) + }.toSet() + } + + override fun update(deltaTime: Float) { + // Update component cache based on current entity state + updateComponentCache() + + // Execute systems in dependency order + systemExecutionOrder.forEach { system -> + system.update(this, deltaTime) + } + } + + /** + * Rebuild the component cache based on current entity components. + * Call this after components have been added/removed from entities. + */ + private fun updateComponentCache() { + componentCache.clear() + entities.forEach { entity -> + entity.componentTypes().forEach { componentType -> + componentCache.getOrPut(componentType) { mutableSetOf() }.add(entity) + } + } + } + + /** + * Build a topologically sorted execution order from system dependencies. + * Uses Kahn's algorithm for topological sorting. + */ + private fun buildExecutionOrder(systems: List<System>): List<System> { + if (systems.isEmpty()) return emptyList() + + val systemMap = systems.associateBy { it::class } + val inDegree = mutableMapOf<KClass<out System>, Int>() + val adjacencyList = mutableMapOf<KClass<out System>, MutableSet<KClass<out System>>>() + + // Initialize graph + systems.forEach { system -> + val systemClass = system::class + inDegree[systemClass] = 0 + adjacencyList[systemClass] = mutableSetOf() + } + + // Build dependency graph (reversed - dependencies point to dependents) + systems.forEach { system -> + system.dependencies.forEach { dependency -> + if (systemMap.containsKey(dependency)) { + adjacencyList[dependency]!!.add(system::class) + inDegree[system::class] = inDegree[system::class]!! + 1 + } + } + } + + // Kahn's algorithm + val queue = ArrayDeque<KClass<out System>>() + val result = mutableListOf<System>() + + // Add all systems with no dependencies to queue + inDegree.forEach { (systemClass, degree) -> + if (degree == 0) { + queue.add(systemClass) + } + } + + while (queue.isNotEmpty()) { + val currentClass = queue.removeFirst() + result.add(systemMap[currentClass]!!) + + // Process dependents + adjacencyList[currentClass]?.forEach { dependent -> + inDegree[dependent] = inDegree[dependent]!! - 1 + if (inDegree[dependent] == 0) { + queue.add(dependent) + } + } + } + + // Check for cycles + if (result.size != systems.size) { + throw IllegalArgumentException("Circular dependency detected in systems") + } + + return result + } +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt new file mode 100644 index 0000000..2ceac47 --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/Entity.kt @@ -0,0 +1,70 @@ +package coffee.liz.ecs + +import kotlin.reflect.KClass + +/** + * An entity is a unique identifier with a collection of components. + */ +class Entity(val id: Int) { + private val components = mutableMapOf<KClass<out Component>, Component>() + + /** + * Add a component to this entity. + */ + fun <T : Component> add(component: T): Entity { + components[component::class] = component + return this + } + + /** + * Remove a component from this entity. + */ + fun <T : Component> remove(type: KClass<T>): Entity { + components.remove(type) + return this + } + + /** + * Get a component by type. + */ + @Suppress("UNCHECKED_CAST") + fun <T : Component> get(type: KClass<T>): T? { + return components[type] as? T + } + + /** + * Check if entity has a component. + */ + fun <T : Component> has(type: KClass<T>): Boolean { + return components.containsKey(type) + } + + /** + * Check if entity has all specified components. + */ + fun hasAll(vararg types: KClass<out Component>): Boolean { + return types.all { components.containsKey(it) } + } + + /** + * Get all component types on this entity. + */ + fun componentTypes(): Set<KClass<out Component>> { + return components.keys.toSet() + } + + override fun equals(other: Any?): Boolean { + if (this === other) return true + if (other !is Entity) return false + return id == other.id + } + + override fun hashCode(): Int = id + + override fun toString(): String = "Entity($id)" +} + +// Convenience extensions for cleaner syntax +inline fun <reified T : Component> Entity.get(): T? = get(T::class) +inline fun <reified T : Component> Entity.has(): Boolean = has(T::class) +inline fun <reified T : Component> Entity.remove(): Entity = remove(T::class) diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt new file mode 100644 index 0000000..0f8ef2a --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/System.kt @@ -0,0 +1,22 @@ +package coffee.liz.ecs + +import kotlin.reflect.KClass + +/** + * Systems contain the logic that operates on entities with specific components. + */ +interface System { + /** + * Systems that must run before this system in the update loop. + * Used to construct a dependency DAG. + */ + val dependencies: Set<KClass<out System>> + get() = emptySet() + + /** + * Update this system. Called once per frame. + * @param world The world containing entities and systems + * @param deltaTime Time elapsed since last update in seconds + */ + fun update(world: World, deltaTime: Float) +} diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt new file mode 100644 index 0000000..fd3b6df --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/World.kt @@ -0,0 +1,42 @@ +package coffee.liz.ecs + +import kotlin.jvm.JvmName +import kotlin.reflect.KClass + +/** + * The World manages entities and systems. + */ +interface World { + /** + * Create a new entity with a unique ID. + */ + fun createEntity(): Entity + + /** + * Destroy an entity and remove it from all caches. + */ + fun destroyEntity(entity: Entity) + + /** + * Get all entities that have all the specified component types. + */ + fun query(vararg componentTypes: KClass<out Component>): Set<Entity> + + /** + * Update all systems in dependency order. + * @param deltaTime Time elapsed since last update in seconds + */ + fun update(deltaTime: Float) +} + +// Convenience extension for queries +@JvmName("query1") +inline fun <reified T : Component> World.query(): Set<Entity> = query(T::class) + +@JvmName("query2") +inline fun <reified T1 : Component, reified T2 : Component> World.query(): Set<Entity> = + query(T1::class, T2::class) + +@JvmName("query3") +inline fun <reified T1 : Component, reified T2 : Component, reified T3 : Component> World.query(): Set<Entity> = + query(T1::class, T2::class, T3::class) diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt new file mode 100644 index 0000000..4289eec --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationComponents.kt @@ -0,0 +1,114 @@ +package coffee.liz.ecs.animation + +import coffee.liz.ecs.Component +import kotlin.jvm.JvmInline + +/** + * Loop modes for animation playback. + */ +enum class LoopMode { + /** Play once and stop on last frame */ + ONCE, + /** Repeat from beginning */ + LOOP, + /** Play forward, then backward, repeat */ + PING_PONG +} + +/** + * Type-safe wrapper for animation names. + */ +@JvmInline +value class AnimationName(val value: String) + +/** + * Type-safe wrapper for frame names. + */ +@JvmInline +value class FrameName(val value: String) + +/** + * Represents a rectangle region in a sprite sheet. + */ +data class Rect( + val x: Int, + val y: Int, + val width: Int, + val height: Int +) + +/** + * Defines a single animation clip with frames and playback settings. + */ +data class AnimationClip( + val frameNames: List<FrameName>, + val frameDuration: Float, // seconds per frame + val loopMode: LoopMode = LoopMode.LOOP +) + +/** + * Component containing sprite sheet data - the image and frame definitions. + * This is immutable data that can be shared across entities. + */ +data class SpriteSheet( + val imagePath: String, + val frames: Map<FrameName, Rect> +) : Component + +/** + * Component for animation playback state. + * Contains animation clips and current playback state. + */ +data class Animator( + val clips: Map<AnimationName, AnimationClip>, + var currentClip: AnimationName, + var frameIndex: Int = 0, + var elapsed: Float = 0f, + var playing: Boolean = true, + var direction: Int = 1 // 1 for forward, -1 for backward (used in PING_PONG) +) : Component { + + /** + * Play a specific animation clip by name. + * Resets playback state when switching clips. + */ + fun play(clipName: AnimationName, restart: Boolean = true) { + if (currentClip != clipName || restart) { + currentClip = clipName + frameIndex = 0 + elapsed = 0f + direction = 1 + playing = true + } + } + + /** + * Pause the current animation. + */ + fun pause() { + playing = false + } + + /** + * Resume the current animation. + */ + fun resume() { + playing = true + } + + /** + * Get the current frame name being displayed. + */ + fun getCurrentFrameName(): FrameName? { + val clip = clips[currentClip] ?: return null + return clip.frameNames.getOrNull(frameIndex) + } +} + +/** + * Component for entity position in 2D space. + */ +data class Position( + var x: Float, + var y: Float +) : Component diff --git a/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt new file mode 100644 index 0000000..7ae405e --- /dev/null +++ b/composeApp/src/commonMain/kotlin/coffee/liz/ecs/animation/AnimationSystem.kt @@ -0,0 +1,77 @@ +package coffee.liz.ecs.animation + +import coffee.liz.ecs.System +import coffee.liz.ecs.World + +/** + * System that updates animation playback state for all entities with an Animator component. + * + * This system: + * - Updates elapsed time for playing animations + * - Advances frame indices based on frame duration + * - Handles loop modes (ONCE, LOOP, PING_PONG) + * - Stops animations when they complete (for ONCE mode) + */ +class AnimationSystem : System { + override fun update(world: World, deltaTime: Float) { + // Query all entities that have both Animator and SpriteSheet + world.query(Animator::class, SpriteSheet::class).forEach { entity -> + val animator = entity.get(Animator::class) ?: return@forEach + + // Skip if animation is not playing + if (!animator.playing) return@forEach + + // Get the current animation clip + val clip = animator.clips[animator.currentClip] ?: return@forEach + + // Accumulate elapsed time + animator.elapsed += deltaTime + + // Advance frames while we have enough elapsed time + while (animator.elapsed >= clip.frameDuration) { + animator.elapsed -= clip.frameDuration + advanceFrame(animator, clip) + } + } + } + + /** + * Advances the animation to the next frame based on loop mode. + */ + private fun advanceFrame(animator: Animator, clip: AnimationClip) { + animator.frameIndex += animator.direction + + when (clip.loopMode) { + LoopMode.ONCE -> { + // Play once and stop on last frame + if (animator.frameIndex >= clip.frameNames.size) { + animator.frameIndex = clip.frameNames.size - 1 + animator.playing = false + } + } + + LoopMode.LOOP -> { + // Loop back to beginning + if (animator.frameIndex >= clip.frameNames.size) { + animator.frameIndex = 0 + } else if (animator.frameIndex < 0) { + // Handle negative wrap (shouldn't happen in LOOP but be safe) + animator.frameIndex = clip.frameNames.size - 1 + } + } + + LoopMode.PING_PONG -> { + // Bounce back and forth + if (animator.frameIndex >= clip.frameNames.size) { + // Hit the end, reverse direction + animator.frameIndex = (clip.frameNames.size - 2).coerceAtLeast(0) + animator.direction = -1 + } else if (animator.frameIndex < 0) { + // Hit the beginning, reverse direction + animator.frameIndex = 1.coerceAtMost(clip.frameNames.size - 1) + animator.direction = 1 + } + } + } + } +} diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt new file mode 100644 index 0000000..78e642f --- /dev/null +++ b/composeApp/src/commonTest/kotlin/coffee/liz/abstractionengine/ComposeAppCommonTest.kt @@ -0,0 +1,12 @@ +package coffee.liz.abstractionengine + +import kotlin.test.Test +import kotlin.test.assertEquals + +class ComposeAppCommonTest { + + @Test + fun example() { + assertEquals(3, 1 + 2) + } +}
\ No newline at end of file diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt new file mode 100644 index 0000000..737e386 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/DAGWorldTest.kt @@ -0,0 +1,242 @@ +package coffee.liz.ecs + +import kotlin.reflect.KClass +import kotlin.test.* + +// Test systems for circular dependency test +class CircularSystemA : System { + override val dependencies: Set<KClass<out System>> + get() = setOf(CircularSystemB::class) + override fun update(world: World, deltaTime: Float) {} +} + +class CircularSystemB : System { + override val dependencies: Set<KClass<out System>> + get() = setOf(CircularSystemA::class) + override fun update(world: World, deltaTime: Float) {} +} + +class DAGWorldTest { + + @Test + fun `can create entities with unique ids`() { + val world = DAGWorld(emptyList()) + + val entity1 = world.createEntity() + val entity2 = world.createEntity() + + assertNotEquals(entity1.id, entity2.id) + } + + @Test + fun `can destroy entity`() { + val world = DAGWorld(emptyList()) + val entity = world.createEntity() + entity.add(Position(10f, 20f)) + + world.destroyEntity(entity) + + val results = world.query<Position>() + assertFalse(results.contains(entity)) + } + + @Test + fun `query returns entities with matching components`() { + val world = DAGWorld(emptyList()) + val entity1 = world.createEntity().add(Position(10f, 20f)) + val entity2 = world.createEntity().add(Position(30f, 40f)) + val entity3 = world.createEntity().add(Velocity(1f, 2f)) + + world.update(0f) // Trigger cache update + + val results = world.query<Position>() + + assertEquals(2, results.size) + assertTrue(results.contains(entity1)) + assertTrue(results.contains(entity2)) + assertFalse(results.contains(entity3)) + } + + @Test + fun `query with multiple components returns intersection`() { + val world = DAGWorld(emptyList()) + val entity1 = world.createEntity() + .add(Position(10f, 20f)) + .add(Velocity(1f, 2f)) + val entity2 = world.createEntity() + .add(Position(30f, 40f)) + val entity3 = world.createEntity() + .add(Velocity(3f, 4f)) + + world.update(0f) // Trigger cache update + + val results = world.query<Position, Velocity>() + + assertEquals(1, results.size) + assertTrue(results.contains(entity1)) + } + + @Test + fun `systems execute in dependency order`() { + val executionOrder = mutableListOf<String>() + + val systemA = object : System { + override fun update(world: World, deltaTime: Float) { + executionOrder.add("A") + } + } + + val systemB = object : System { + override val dependencies = setOf(systemA::class) + override fun update(world: World, deltaTime: Float) { + executionOrder.add("B") + } + } + + val systemC = object : System { + override val dependencies = setOf(systemB::class) + override fun update(world: World, deltaTime: Float) { + executionOrder.add("C") + } + } + + val world = DAGWorld(listOf(systemC, systemA, systemB)) + world.update(0f) + + assertEquals(listOf("A", "B", "C"), executionOrder) + } + + @Test + fun `systems with no dependencies can execute in any order`() { + val executionOrder = mutableListOf<String>() + + val systemA = object : System { + override fun update(world: World, deltaTime: Float) { + executionOrder.add("A") + } + } + + val systemB = object : System { + override fun update(world: World, deltaTime: Float) { + executionOrder.add("B") + } + } + + val world = DAGWorld(listOf(systemA, systemB)) + world.update(0f) + + assertEquals(2, executionOrder.size) + assertTrue(executionOrder.contains("A")) + assertTrue(executionOrder.contains("B")) + } + + @Test + fun `circular dependency throws exception`() { + val systemA = CircularSystemA() + val systemB = CircularSystemB() + + assertFailsWith<IllegalArgumentException> { + DAGWorld(listOf(systemA, systemB)) + } + } + + @Test + fun `system receives correct deltaTime`() { + var receivedDelta = 0f + + val system = object : System { + override fun update(world: World, deltaTime: Float) { + receivedDelta = deltaTime + } + } + + val world = DAGWorld(listOf(system)) + world.update(0.016f) + + assertEquals(0.016f, receivedDelta) + } + + @Test + fun `systems can query entities during update`() { + var queriedEntities: Set<Entity>? = null + + val system = object : System { + override fun update(world: World, deltaTime: Float) { + queriedEntities = world.query<Position>() + } + } + + val world = DAGWorld(listOf(system)) + val entity = world.createEntity().add(Position(10f, 20f)) + + world.update(0f) + + assertNotNull(queriedEntities) + assertEquals(1, queriedEntities!!.size) + assertTrue(queriedEntities!!.contains(entity)) + } + + @Test + fun `component cache updates after entity changes`() { + val world = DAGWorld(emptyList()) + val entity = world.createEntity() + + world.update(0f) + assertEquals(0, world.query<Position>().size) + + entity.add(Position(10f, 20f)) + world.update(0f) + assertEquals(1, world.query<Position>().size) + + entity.remove<Position>() + world.update(0f) + assertEquals(0, world.query<Position>().size) + } + + @Test + fun `diamond dependency resolves correctly`() { + val executionOrder = mutableListOf<String>() + + // A + // / \ + // B C + // \ / + // D + + val systemA = object : System { + override fun update(world: World, deltaTime: Float) { + executionOrder.add("A") + } + } + + val systemB = object : System { + override val dependencies = setOf(systemA::class) + override fun update(world: World, deltaTime: Float) { + executionOrder.add("B") + } + } + + val systemC = object : System { + override val dependencies = setOf(systemA::class) + override fun update(world: World, deltaTime: Float) { + executionOrder.add("C") + } + } + + val systemD = object : System { + override val dependencies = setOf(systemB::class, systemC::class) + override fun update(world: World, deltaTime: Float) { + executionOrder.add("D") + } + } + + val world = DAGWorld(listOf(systemD, systemC, systemB, systemA)) + world.update(0f) + + // A must come first, D must come last, B and C in between + assertEquals("A", executionOrder.first()) + assertEquals("D", executionOrder.last()) + assertTrue(executionOrder.indexOf("B") < executionOrder.indexOf("D")) + assertTrue(executionOrder.indexOf("C") < executionOrder.indexOf("D")) + } +} diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt new file mode 100644 index 0000000..e807bd2 --- /dev/null +++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/EntityTest.kt @@ -0,0 +1,116 @@ +package coffee.liz.ecs + +import kotlin.test.* + +// Test components +data class Position(val x: Float, val y: Float) : Component +data class Velocity(val dx: Float, val dy: Float) : Component +data class Health(val value: Int) : Component + +class EntityTest { + + @Test + fun `entity has unique id`() { + val entity1 = Entity(1) + val entity2 = Entity(2) + + assertNotEquals(entity1.id, entity2.id) + } + + @Test + fun `can add component to entity`() { + val entity = Entity(0) + val position = Position(10f, 20f) + + entity.add(position) + + assertTrue(entity.has<Position>()) + assertEquals(position, entity.get<Position>()) + } + + @Test + fun `can remove component from entity`() { + val entity = Entity(0) + entity.add(Position(10f, 20f)) + + entity.remove<Position>() + + assertFalse(entity.has<Position>()) + assertNull(entity.get<Position>()) + } + + @Test + fun `add returns entity for chaining`() { + val entity = Entity(0) + + val result = entity + .add(Position(10f, 20f)) + .add(Velocity(1f, 2f)) + + assertSame(entity, result) + assertTrue(entity.has<Position>()) + assertTrue(entity.has<Velocity>()) + } + + @Test + fun `can replace component by adding same type`() { + val entity = Entity(0) + entity.add(Position(10f, 20f)) + entity.add(Position(30f, 40f)) + + val position = entity.get<Position>() + + assertNotNull(position) + assertEquals(30f, position.x) + assertEquals(40f, position.y) + } + + @Test + fun `hasAll returns true when entity has all components`() { + val entity = Entity(0) + entity.add(Position(10f, 20f)) + entity.add(Velocity(1f, 2f)) + + assertTrue(entity.hasAll(Position::class, Velocity::class)) + } + + @Test + fun `hasAll returns false when entity missing component`() { + val entity = Entity(0) + entity.add(Position(10f, 20f)) + + assertFalse(entity.hasAll(Position::class, Velocity::class)) + } + + @Test + fun `componentTypes returns all component types`() { + val entity = Entity(0) + entity.add(Position(10f, 20f)) + entity.add(Velocity(1f, 2f)) + entity.add(Health(100)) + + val types = entity.componentTypes() + + assertEquals(3, types.size) + assertTrue(types.contains(Position::class)) + assertTrue(types.contains(Velocity::class)) + assertTrue(types.contains(Health::class)) + } + + @Test + fun `entities with same id are equal`() { + val entity1 = Entity(1) + val entity2 = Entity(1) + + assertEquals(entity1, entity2) + assertEquals(entity1.hashCode(), entity2.hashCode()) + } + + @Test + fun `entities with different id are not equal`() { + val entity1 = Entity(1) + val entity2 = Entity(2) + + assertNotEquals(entity1, entity2) + } +} diff --git a/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt new file mode 100644 index 0000000..bb7298e --- /dev/null +++ b/composeApp/src/commonTest/kotlin/coffee/liz/ecs/animation/AnimationSystemTest.kt @@ -0,0 +1,238 @@ +package coffee.liz.ecs.animation + +import coffee.liz.ecs.DAGWorld +import kotlin.test.* + +class AnimationSystemTest { + + private fun createTestSpriteSheet(): SpriteSheet { + return SpriteSheet( + imagePath = "test.png", + frames = mapOf( + FrameName("idle_0") to Rect(0, 0, 32, 32), + FrameName("idle_1") to Rect(32, 0, 32, 32), + FrameName("walk_0") to Rect(0, 32, 32, 32), + FrameName("walk_1") to Rect(32, 32, 32, 32), + FrameName("walk_2") to Rect(64, 32, 32, 32) + ) + ) + } + + private fun createTestAnimator(): Animator { + return Animator( + clips = mapOf( + AnimationName("idle") to AnimationClip( + frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")), + frameDuration = 0.1f, + loopMode = LoopMode.LOOP + ), + AnimationName("walk") to AnimationClip( + frameNames = listOf( + FrameName("walk_0"), + FrameName("walk_1"), + FrameName("walk_2") + ), + frameDuration = 0.15f, + loopMode = LoopMode.LOOP + ) + ), + currentClip = AnimationName("idle") + ) + } + + @Test + fun `animation advances frame after frame duration`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(createTestSpriteSheet()) + entity.add(animator) + + assertEquals(0, animator.frameIndex) + assertEquals(0f, animator.elapsed) + + // Update with less than frame duration - should not advance + world.update(0.05f) + assertEquals(0, animator.frameIndex) + assertEquals(0.05f, animator.elapsed) + + // Update to exceed frame duration - should advance to frame 1 + world.update(0.06f) + assertEquals(1, animator.frameIndex) + assertTrue(animator.elapsed < 0.1f) + } + + @Test + fun `LOOP mode wraps to beginning`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(createTestSpriteSheet()) + entity.add(animator) + + // Idle animation has 2 frames at 0.1s each + world.update(0.1f) // Frame 1 + assertEquals(1, animator.frameIndex) + + world.update(0.1f) // Should wrap to Frame 0 + assertEquals(0, animator.frameIndex) + assertTrue(animator.playing) // Still playing + } + + @Test + fun `ONCE mode stops at last frame`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = Animator( + clips = mapOf( + AnimationName("once") to AnimationClip( + frameNames = listOf(FrameName("idle_0"), FrameName("idle_1")), + frameDuration = 0.1f, + loopMode = LoopMode.ONCE + ) + ), + currentClip = AnimationName("once") + ) + entity.add(createTestSpriteSheet()) + entity.add(animator) + + world.update(0.1f) // Frame 1 + assertEquals(1, animator.frameIndex) + assertTrue(animator.playing) + + world.update(0.1f) // Should stop at frame 1 + assertEquals(1, animator.frameIndex) + assertFalse(animator.playing) + + // Further updates should not change anything + world.update(0.1f) + assertEquals(1, animator.frameIndex) + assertFalse(animator.playing) + } + + @Test + fun `PING_PONG mode bounces back and forth`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = Animator( + clips = mapOf( + AnimationName("pingpong") to AnimationClip( + frameNames = listOf( + FrameName("walk_0"), + FrameName("walk_1"), + FrameName("walk_2") + ), + frameDuration = 0.1f, + loopMode = LoopMode.PING_PONG + ) + ), + currentClip = AnimationName("pingpong") + ) + entity.add(createTestSpriteSheet()) + entity.add(animator) + + // Forward: 0 -> 1 -> 2 + world.update(0.1f) + assertEquals(1, animator.frameIndex) + assertEquals(1, animator.direction) + + world.update(0.1f) + assertEquals(2, animator.frameIndex) + assertEquals(1, animator.direction) + + // Hit end, should reverse: 2 -> 1 + world.update(0.1f) + assertEquals(1, animator.frameIndex) + assertEquals(-1, animator.direction) + + // Continue backward: 1 -> 0 + world.update(0.1f) + assertEquals(0, animator.frameIndex) + assertEquals(-1, animator.direction) + + // Hit beginning, should reverse: 0 -> 1 + world.update(0.1f) + assertEquals(1, animator.frameIndex) + assertEquals(1, animator.direction) + } + + @Test + fun `play() switches animation and resets state`() { + val animator = createTestAnimator() + assertEquals(AnimationName("idle"), animator.currentClip) + assertEquals(0, animator.frameIndex) + + // Manually advance + animator.frameIndex = 1 + animator.elapsed = 0.05f + + // Switch to walk animation + animator.play(AnimationName("walk")) + assertEquals(AnimationName("walk"), animator.currentClip) + assertEquals(0, animator.frameIndex) + assertEquals(0f, animator.elapsed) + assertEquals(1, animator.direction) + assertTrue(animator.playing) + } + + @Test + fun `pause() and resume() control playback`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(createTestSpriteSheet()) + entity.add(animator) + + animator.pause() + assertFalse(animator.playing) + + // Update should not advance animation + world.update(0.1f) + assertEquals(0, animator.frameIndex) + + animator.resume() + assertTrue(animator.playing) + + // Now it should advance + world.update(0.1f) + assertEquals(1, animator.frameIndex) + } + + @Test + fun `getCurrentFrameName returns correct frame`() { + val animator = createTestAnimator() + + assertEquals(FrameName("idle_0"), animator.getCurrentFrameName()) + + animator.frameIndex = 1 + assertEquals(FrameName("idle_1"), animator.getCurrentFrameName()) + + animator.play(AnimationName("walk")) + assertEquals(FrameName("walk_0"), animator.getCurrentFrameName()) + } + + @Test + fun `multiple updates in single frame work correctly`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(createTestSpriteSheet()) + entity.add(animator) + + // Large delta time should advance multiple frames + world.update(0.25f) // Should advance 2 frames and have 0.05s remainder + assertEquals(0, animator.frameIndex) // Wrapped back to 0 (LOOP mode) + assertTrue(animator.elapsed >= 0.04f && animator.elapsed <= 0.06f) + } + + @Test + fun `entities without SpriteSheet are skipped`() { + val world = DAGWorld(listOf(AnimationSystem())) + val entity = world.createEntity() + val animator = createTestAnimator() + entity.add(animator) // No SpriteSheet + + // Should not crash + world.update(0.1f) + } +} diff --git a/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt new file mode 100644 index 0000000..4f98625 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/MainViewController.kt @@ -0,0 +1,5 @@ +package coffee.liz.abstractionengine + +import androidx.compose.ui.window.ComposeUIViewController + +fun MainViewController() = ComposeUIViewController { App() }
\ No newline at end of file diff --git a/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt new file mode 100644 index 0000000..a6362e3 --- /dev/null +++ b/composeApp/src/iosMain/kotlin/coffee/liz/abstractionengine/Platform.ios.kt @@ -0,0 +1,9 @@ +package coffee.liz.abstractionengine + +import platform.UIKit.UIDevice + +class IOSPlatform: Platform { + override val name: String = UIDevice.currentDevice.systemName() + " " + UIDevice.currentDevice.systemVersion +} + +actual fun getPlatform(): Platform = IOSPlatform()
\ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt new file mode 100644 index 0000000..e1487e0 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/Platform.jvm.kt @@ -0,0 +1,7 @@ +package coffee.liz.abstractionengine + +class JVMPlatform: Platform { + override val name: String = "Java ${System.getProperty("java.version")}" +} + +actual fun getPlatform(): Platform = JVMPlatform()
\ No newline at end of file diff --git a/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt new file mode 100644 index 0000000..a426fb4 --- /dev/null +++ b/composeApp/src/jvmMain/kotlin/coffee/liz/abstractionengine/main.kt @@ -0,0 +1,13 @@ +package coffee.liz.abstractionengine + +import androidx.compose.ui.window.Window +import androidx.compose.ui.window.application + +fun main() = application { + Window( + onCloseRequest = ::exitApplication, + title = "The Abstraction Engine", + ) { + App() + } +}
\ No newline at end of file diff --git a/gradle.properties b/gradle.properties new file mode 100644 index 0000000..6f8e6ea --- /dev/null +++ b/gradle.properties @@ -0,0 +1,12 @@ +#Kotlin +kotlin.code.style=official +kotlin.daemon.jvmargs=-Xmx3072M + +#Gradle +org.gradle.jvmargs=-Xmx4096M -Dfile.encoding=UTF-8 +org.gradle.configuration-cache=true +org.gradle.caching=true + +#Android +android.nonTransitiveRClass=true +android.useAndroidX=true
\ No newline at end of file diff --git a/gradle/libs.versions.toml b/gradle/libs.versions.toml new file mode 100644 index 0000000..595245f --- /dev/null +++ b/gradle/libs.versions.toml @@ -0,0 +1,37 @@ +[versions] +agp = "8.11.2" +android-compileSdk = "36" +android-minSdk = "24" +android-targetSdk = "36" +androidx-activity = "1.11.0" +androidx-appcompat = "1.7.1" +androidx-core = "1.17.0" +androidx-espresso = "3.7.0" +androidx-lifecycle = "2.9.4" +androidx-testExt = "1.3.0" +composeHotReload = "1.0.0-beta07" +composeMultiplatform = "1.9.0" +junit = "4.13.2" +kotlin = "2.2.20" +kotlinx-coroutines = "1.10.2" + +[libraries] +kotlin-test = { module = "org.jetbrains.kotlin:kotlin-test", version.ref = "kotlin" } +kotlin-testJunit = { module = "org.jetbrains.kotlin:kotlin-test-junit", version.ref = "kotlin" } +junit = { module = "junit:junit", version.ref = "junit" } +androidx-core-ktx = { module = "androidx.core:core-ktx", version.ref = "androidx-core" } +androidx-testExt-junit = { module = "androidx.test.ext:junit", version.ref = "androidx-testExt" } +androidx-espresso-core = { module = "androidx.test.espresso:espresso-core", version.ref = "androidx-espresso" } +androidx-appcompat = { module = "androidx.appcompat:appcompat", version.ref = "androidx-appcompat" } +androidx-activity-compose = { module = "androidx.activity:activity-compose", version.ref = "androidx-activity" } +androidx-lifecycle-viewmodelCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-viewmodel-compose", version.ref = "androidx-lifecycle" } +androidx-lifecycle-runtimeCompose = { module = "org.jetbrains.androidx.lifecycle:lifecycle-runtime-compose", version.ref = "androidx-lifecycle" } +kotlinx-coroutinesSwing = { module = "org.jetbrains.kotlinx:kotlinx-coroutines-swing", version.ref = "kotlinx-coroutines" } + +[plugins] +androidApplication = { id = "com.android.application", version.ref = "agp" } +androidLibrary = { id = "com.android.library", version.ref = "agp" } +composeHotReload = { id = "org.jetbrains.compose.hot-reload", version.ref = "composeHotReload" } +composeMultiplatform = { id = "org.jetbrains.compose", version.ref = "composeMultiplatform" } +composeCompiler = { id = "org.jetbrains.kotlin.plugin.compose", version.ref = "kotlin" } +kotlinMultiplatform = { id = "org.jetbrains.kotlin.multiplatform", version.ref = "kotlin" }
\ No newline at end of file diff --git a/gradle/wrapper/gradle-wrapper.jar b/gradle/wrapper/gradle-wrapper.jar Binary files differnew file mode 100644 index 0000000..1b33c55 --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.jar diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties new file mode 100644 index 0000000..d4081da --- /dev/null +++ b/gradle/wrapper/gradle-wrapper.properties @@ -0,0 +1,7 @@ +distributionBase=GRADLE_USER_HOME +distributionPath=wrapper/dists +distributionUrl=https\://services.gradle.org/distributions/gradle-8.14.3-bin.zip +networkTimeout=10000 +validateDistributionUrl=true +zipStoreBase=GRADLE_USER_HOME +zipStorePath=wrapper/dists @@ -0,0 +1,251 @@ +#!/bin/sh + +# +# Copyright © 2015-2021 the original authors. +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# https://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. +# +# SPDX-License-Identifier: Apache-2.0 +# + +############################################################################## +# +# Gradle start up script for POSIX generated by Gradle. +# +# Important for running: +# +# (1) You need a POSIX-compliant shell to run this script. If your /bin/sh is +# noncompliant, but you have some other compliant shell such as ksh or +# bash, then to run this script, type that shell name before the whole +# command line, like: +# +# ksh Gradle +# +# Busybox and similar reduced shells will NOT work, because this script +# requires all of these POSIX shell features: +# * functions; +# * expansions «$var», «${var}», «${var:-default}», «${var+SET}», +# «${var#prefix}», «${var%suffix}», and «$( cmd )»; +# * compound commands having a testable exit status, especially «case»; +# * various built-in commands including «command», «set», and «ulimit». +# +# Important for patching: +# +# (2) This script targets any POSIX shell, so it avoids extensions provided +# by Bash, Ksh, etc; in particular arrays are avoided. +# +# The "traditional" practice of packing multiple parameters into a +# space-separated string is a well documented source of bugs and security +# problems, so this is (mostly) avoided, by progressively accumulating +# options in "$@", and eventually passing that to Java. +# +# Where the inherited environment variables (DEFAULT_JVM_OPTS, JAVA_OPTS, +# and GRADLE_OPTS) rely on word-splitting, this is performed explicitly; +# see the in-line comments for details. +# +# There are tweaks for specific operating systems such as AIX, CygWin, +# Darwin, MinGW, and NonStop. +# +# (3) This script is generated from the Groovy template +# https://github.com/gradle/gradle/blob/HEAD/platforms/jvm/plugins-application/src/main/resources/org/gradle/api/internal/plugins/unixStartScript.txt +# within the Gradle project. +# +# You can find Gradle at https://github.com/gradle/gradle/. +# +############################################################################## + +# Attempt to set APP_HOME + +# Resolve links: $0 may be a link +app_path=$0 + +# Need this for daisy-chained symlinks. +while + APP_HOME=${app_path%"${app_path##*/}"} # leaves a trailing /; empty if no leading path + [ -h "$app_path" ] +do + ls=$( ls -ld "$app_path" ) + link=${ls#*' -> '} + case $link in #( + /*) app_path=$link ;; #( + *) app_path=$APP_HOME$link ;; + esac +done + +# This is normally unused +# shellcheck disable=SC2034 +APP_BASE_NAME=${0##*/} +# Discard cd standard output in case $CDPATH is set (https://github.com/gradle/gradle/issues/25036) +APP_HOME=$( cd -P "${APP_HOME:-./}" > /dev/null && printf '%s\n' "$PWD" ) || exit + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD=maximum + +warn () { + echo "$*" +} >&2 + +die () { + echo + echo "$*" + echo + exit 1 +} >&2 + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "$( uname )" in #( + CYGWIN* ) cygwin=true ;; #( + Darwin* ) darwin=true ;; #( + MSYS* | MINGW* ) msys=true ;; #( + NONSTOP* ) nonstop=true ;; +esac + +CLASSPATH="\\\"\\\"" + + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD=$JAVA_HOME/jre/sh/java + else + JAVACMD=$JAVA_HOME/bin/java + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD=java + if ! command -v java >/dev/null 2>&1 + then + die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +fi + +# Increase the maximum file descriptors if we can. +if ! "$cygwin" && ! "$darwin" && ! "$nonstop" ; then + case $MAX_FD in #( + max*) + # In POSIX sh, ulimit -H is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + MAX_FD=$( ulimit -H -n ) || + warn "Could not query maximum file descriptor limit" + esac + case $MAX_FD in #( + '' | soft) :;; #( + *) + # In POSIX sh, ulimit -n is undefined. That's why the result is checked to see if it worked. + # shellcheck disable=SC2039,SC3045 + ulimit -n "$MAX_FD" || + warn "Could not set maximum file descriptor limit to $MAX_FD" + esac +fi + +# Collect all arguments for the java command, stacking in reverse order: +# * args from the command line +# * the main class name +# * -classpath +# * -D...appname settings +# * --module-path (only if needed) +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and GRADLE_OPTS environment variables. + +# For Cygwin or MSYS, switch paths to Windows format before running java +if "$cygwin" || "$msys" ; then + APP_HOME=$( cygpath --path --mixed "$APP_HOME" ) + CLASSPATH=$( cygpath --path --mixed "$CLASSPATH" ) + + JAVACMD=$( cygpath --unix "$JAVACMD" ) + + # Now convert the arguments - kludge to limit ourselves to /bin/sh + for arg do + if + case $arg in #( + -*) false ;; # don't mess with options #( + /?*) t=${arg#/} t=/${t%%/*} # looks like a POSIX filepath + [ -e "$t" ] ;; #( + *) false ;; + esac + then + arg=$( cygpath --path --ignore --mixed "$arg" ) + fi + # Roll the args list around exactly as many times as the number of + # args, so each arg winds up back in the position where it started, but + # possibly modified. + # + # NB: a `for` loop captures its iteration list before it begins, so + # changing the positional parameters here affects neither the number of + # iterations, nor the values presented in `arg`. + shift # remove old arg + set -- "$@" "$arg" # push replacement arg + done +fi + + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS='"-Xmx64m" "-Xms64m"' + +# Collect all arguments for the java command: +# * DEFAULT_JVM_OPTS, JAVA_OPTS, and optsEnvironmentVar are not allowed to contain shell fragments, +# and any embedded shellness will be escaped. +# * For example: A user cannot expect ${Hostname} to be expanded, as it is an environment variable and will be +# treated as '${Hostname}' itself on the command line. + +set -- \ + "-Dorg.gradle.appname=$APP_BASE_NAME" \ + -classpath "$CLASSPATH" \ + -jar "$APP_HOME/gradle/wrapper/gradle-wrapper.jar" \ + "$@" + +# Stop when "xargs" is not available. +if ! command -v xargs >/dev/null 2>&1 +then + die "xargs is not available" +fi + +# Use "xargs" to parse quoted args. +# +# With -n1 it outputs one arg per line, with the quotes and backslashes removed. +# +# In Bash we could simply go: +# +# readarray ARGS < <( xargs -n1 <<<"$var" ) && +# set -- "${ARGS[@]}" "$@" +# +# but POSIX shell has neither arrays nor command substitution, so instead we +# post-process each arg (as a line of input to sed) to backslash-escape any +# character that might be a shell metacharacter, then use eval to reverse +# that process (while maintaining the separation between arguments), and wrap +# the whole thing up as a single "set" statement. +# +# This will of course break if any of these variables contains a newline or +# an unmatched quote. +# + +eval "set -- $( + printf '%s\n' "$DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS" | + xargs -n1 | + sed ' s~[^-[:alnum:]+,./:=@_]~\\&~g; ' | + tr '\n' ' ' + )" '"$@"' + +exec "$JAVACMD" "$@" diff --git a/gradlew.bat b/gradlew.bat new file mode 100644 index 0000000..5eed7ee --- /dev/null +++ b/gradlew.bat @@ -0,0 +1,94 @@ +@rem
+@rem Copyright 2015 the original author or authors.
+@rem
+@rem Licensed under the Apache License, Version 2.0 (the "License");
+@rem you may not use this file except in compliance with the License.
+@rem You may obtain a copy of the License at
+@rem
+@rem https://www.apache.org/licenses/LICENSE-2.0
+@rem
+@rem Unless required by applicable law or agreed to in writing, software
+@rem distributed under the License is distributed on an "AS IS" BASIS,
+@rem WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+@rem See the License for the specific language governing permissions and
+@rem limitations under the License.
+@rem
+@rem SPDX-License-Identifier: Apache-2.0
+@rem
+
+@if "%DEBUG%"=="" @echo off
+@rem ##########################################################################
+@rem
+@rem Gradle startup script for Windows
+@rem
+@rem ##########################################################################
+
+@rem Set local scope for the variables with windows NT shell
+if "%OS%"=="Windows_NT" setlocal
+
+set DIRNAME=%~dp0
+if "%DIRNAME%"=="" set DIRNAME=.
+@rem This is normally unused
+set APP_BASE_NAME=%~n0
+set APP_HOME=%DIRNAME%
+
+@rem Resolve any "." and ".." in APP_HOME to make it shorter.
+for %%i in ("%APP_HOME%") do set APP_HOME=%%~fi
+
+@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script.
+set DEFAULT_JVM_OPTS="-Xmx64m" "-Xms64m"
+
+@rem Find java.exe
+if defined JAVA_HOME goto findJavaFromJavaHome
+
+set JAVA_EXE=java.exe
+%JAVA_EXE% -version >NUL 2>&1
+if %ERRORLEVEL% equ 0 goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:findJavaFromJavaHome
+set JAVA_HOME=%JAVA_HOME:"=%
+set JAVA_EXE=%JAVA_HOME%/bin/java.exe
+
+if exist "%JAVA_EXE%" goto execute
+
+echo. 1>&2
+echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 1>&2
+echo. 1>&2
+echo Please set the JAVA_HOME variable in your environment to match the 1>&2
+echo location of your Java installation. 1>&2
+
+goto fail
+
+:execute
+@rem Setup the command line
+
+set CLASSPATH=
+
+
+@rem Execute Gradle
+"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" -jar "%APP_HOME%\gradle\wrapper\gradle-wrapper.jar" %*
+
+:end
+@rem End local scope for the variables with windows NT shell
+if %ERRORLEVEL% equ 0 goto mainEnd
+
+:fail
+rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of
+rem the _cmd.exe /c_ return code!
+set EXIT_CODE=%ERRORLEVEL%
+if %EXIT_CODE% equ 0 set EXIT_CODE=1
+if not ""=="%GRADLE_EXIT_CONSOLE%" exit %EXIT_CODE%
+exit /b %EXIT_CODE%
+
+:mainEnd
+if "%OS%"=="Windows_NT" endlocal
+
+:omega
diff --git a/iosApp/Configuration/Config.xcconfig b/iosApp/Configuration/Config.xcconfig new file mode 100644 index 0000000..e9ee188 --- /dev/null +++ b/iosApp/Configuration/Config.xcconfig @@ -0,0 +1,7 @@ +TEAM_ID= + +PRODUCT_NAME=The Abstraction Engine +PRODUCT_BUNDLE_IDENTIFIER=coffee.liz.abstractionengine.TheAbstractionEngine$(TEAM_ID) + +CURRENT_PROJECT_VERSION=1 +MARKETING_VERSION=1.0
\ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.pbxproj b/iosApp/iosApp.xcodeproj/project.pbxproj new file mode 100644 index 0000000..1b11e19 --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.pbxproj @@ -0,0 +1,373 @@ +// !$*UTF8*$! +{ + archiveVersion = 1; + classes = { + }; + objectVersion = 77; + objects = { + +/* Begin PBXFileReference section */ + F5FF5B23FF9DADAFFC41EC5B /* The Abstraction Engine.app */ = {isa = PBXFileReference; explicitFileType = wrapper.application; includeInIndex = 0; path = "The Abstraction Engine.app"; sourceTree = BUILT_PRODUCTS_DIR; }; +/* End PBXFileReference section */ + +/* Begin PBXFileSystemSynchronizedBuildFileExceptionSet section */ + B68B0CBE2FD019FBB9ADC062 /* Exceptions for "iosApp" folder in "iosApp" target */ = { + isa = PBXFileSystemSynchronizedBuildFileExceptionSet; + membershipExceptions = ( + Info.plist, + ); + target = 84468865F64DFD79AC9E2C2F /* iosApp */; + }; +/* End PBXFileSystemSynchronizedBuildFileExceptionSet section */ + +/* Begin PBXFileSystemSynchronizedRootGroup section */ + 61BC5DB5739A5B0768C43C39 /* iosApp */ = { + isa = PBXFileSystemSynchronizedRootGroup; + exceptions = ( + B68B0CBE2FD019FBB9ADC062 /* Exceptions for "iosApp" folder in "iosApp" target */, + ); + path = iosApp; + sourceTree = "<group>"; + }; + 831D5F81A482AA3D6A68517F /* Configuration */ = { + isa = PBXFileSystemSynchronizedRootGroup; + path = Configuration; + sourceTree = "<group>"; + }; +/* End PBXFileSystemSynchronizedRootGroup section */ + +/* Begin PBXFrameworksBuildPhase section */ + AC6111C758B22DDF1138CFB9 /* Frameworks */ = { + isa = PBXFrameworksBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXFrameworksBuildPhase section */ + +/* Begin PBXGroup section */ + F5D8D61979EE6CAF6A1A0602 = { + isa = PBXGroup; + children = ( + 831D5F81A482AA3D6A68517F /* Configuration */, + 61BC5DB5739A5B0768C43C39 /* iosApp */, + 495300AED579013BD62BC6B8 /* Products */, + ); + sourceTree = "<group>"; + }; + 495300AED579013BD62BC6B8 /* Products */ = { + isa = PBXGroup; + children = ( + F5FF5B23FF9DADAFFC41EC5B /* The Abstraction Engine.app */, + ); + name = Products; + sourceTree = "<group>"; + }; +/* End PBXGroup section */ + +/* Begin PBXNativeTarget section */ + 84468865F64DFD79AC9E2C2F /* iosApp */ = { + isa = PBXNativeTarget; + buildConfigurationList = E2F0DFE8FC41CBCBBDA48BEE /* Build configuration list for PBXNativeTarget "iosApp" */; + buildPhases = ( + 5A110A979046F24C7C9E7A26 /* Compile Kotlin Framework */, + 0AE70916E8C6CBBEDFAFD3AD /* Sources */, + AC6111C758B22DDF1138CFB9 /* Frameworks */, + B7ABC5FA67E48B4046A73E06 /* Resources */, + ); + buildRules = ( + ); + dependencies = ( + ); + fileSystemSynchronizedGroups = ( + 61BC5DB5739A5B0768C43C39 /* iosApp */, + ); + name = iosApp; + packageProductDependencies = ( + ); + productName = iosApp; + productReference = F5FF5B23FF9DADAFFC41EC5B /* The Abstraction Engine.app */; + productType = "com.apple.product-type.application"; + }; +/* End PBXNativeTarget section */ + +/* Begin PBXProject section */ + 482A5064A76EC02255085D66 /* Project object */ = { + isa = PBXProject; + attributes = { + BuildIndependentTargetsInParallel = 1; + LastSwiftUpdateCheck = 1620; + LastUpgradeCheck = 1620; + TargetAttributes = { + 84468865F64DFD79AC9E2C2F = { + CreatedOnToolsVersion = 16.2; + }; + }; + }; + buildConfigurationList = BC12BB3CA5431F80864A9FB6 /* Build configuration list for PBXProject "iosApp" */; + developmentRegion = en; + hasScannedForEncodings = 0; + knownRegions = ( + en, + Base, + ); + mainGroup = F5D8D61979EE6CAF6A1A0602; + minimizedProjectReferenceProxies = 1; + preferredProjectObjectVersion = 77; + productRefGroup = 495300AED579013BD62BC6B8 /* Products */; + projectDirPath = ""; + projectRoot = ""; + targets = ( + 84468865F64DFD79AC9E2C2F /* iosApp */, + ); + }; +/* End PBXProject section */ + +/* Begin PBXResourcesBuildPhase section */ + B7ABC5FA67E48B4046A73E06 /* Resources */ = { + isa = PBXResourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXResourcesBuildPhase section */ + +/* Begin PBXShellScriptBuildPhase section */ + 5A110A979046F24C7C9E7A26 /* Compile Kotlin Framework */ = { + isa = PBXShellScriptBuildPhase; + alwaysOutOfDate = 1; + buildActionMask = 2147483647; + files = ( + ); + inputFileListPaths = ( + ); + inputPaths = ( + ); + name = "Compile Kotlin Framework"; + outputFileListPaths = ( + ); + outputPaths = ( + ); + runOnlyForDeploymentPostprocessing = 0; + shellPath = /bin/sh; + shellScript = "if [ \"YES\" = \"$OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED\" ]; then\n echo \"Skipping Gradle build task invocation due to OVERRIDE_KOTLIN_BUILD_IDE_SUPPORTED environment variable set to \\\"YES\\\"\"\n exit 0\nfi\ncd \"$SRCROOT/..\"\n./gradlew :composeApp:embedAndSignAppleFrameworkForXcode\n"; + }; +/* End PBXShellScriptBuildPhase section */ + +/* Begin PBXSourcesBuildPhase section */ + 0AE70916E8C6CBBEDFAFD3AD /* Sources */ = { + isa = PBXSourcesBuildPhase; + buildActionMask = 2147483647; + files = ( + ); + runOnlyForDeploymentPostprocessing = 0; + }; +/* End PBXSourcesBuildPhase section */ + +/* Begin XCBuildConfiguration section */ + D94EDDF3060BE1195839DB3B /* Debug */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 831D5F81A482AA3D6A68517F /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = dwarf; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_TESTABILITY = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_DYNAMIC_NO_PIC = NO; + GCC_NO_COMMON_BLOCKS = YES; + GCC_OPTIMIZATION_LEVEL = 0; + GCC_PREPROCESSOR_DEFINITIONS = ( + "DEBUG=1", + "$(inherited)", + ); + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = INCLUDE_SOURCE; + MTL_FAST_MATH = YES; + ONLY_ACTIVE_ARCH = YES; + SDKROOT = iphoneos; + SWIFT_ACTIVE_COMPILATION_CONDITIONS = "DEBUG $(inherited)"; + SWIFT_OPTIMIZATION_LEVEL = "-Onone"; + }; + name = Debug; + }; + 82793F83896C4AA8A17CB990 /* Release */ = { + isa = XCBuildConfiguration; + baseConfigurationReferenceAnchor = 831D5F81A482AA3D6A68517F /* Configuration */; + baseConfigurationReferenceRelativePath = Config.xcconfig; + buildSettings = { + ALWAYS_SEARCH_USER_PATHS = NO; + ASSETCATALOG_COMPILER_GENERATE_SWIFT_ASSET_SYMBOL_EXTENSIONS = YES; + CLANG_ANALYZER_NONNULL = YES; + CLANG_ANALYZER_NUMBER_OBJECT_CONVERSION = YES_AGGRESSIVE; + CLANG_CXX_LANGUAGE_STANDARD = "gnu++20"; + CLANG_ENABLE_MODULES = YES; + CLANG_ENABLE_OBJC_ARC = YES; + CLANG_ENABLE_OBJC_WEAK = YES; + CLANG_WARN_BLOCK_CAPTURE_AUTORELEASING = YES; + CLANG_WARN_BOOL_CONVERSION = YES; + CLANG_WARN_COMMA = YES; + CLANG_WARN_CONSTANT_CONVERSION = YES; + CLANG_WARN_DEPRECATED_OBJC_IMPLEMENTATIONS = YES; + CLANG_WARN_DIRECT_OBJC_ISA_USAGE = YES_ERROR; + CLANG_WARN_DOCUMENTATION_COMMENTS = YES; + CLANG_WARN_EMPTY_BODY = YES; + CLANG_WARN_ENUM_CONVERSION = YES; + CLANG_WARN_INFINITE_RECURSION = YES; + CLANG_WARN_INT_CONVERSION = YES; + CLANG_WARN_NON_LITERAL_NULL_CONVERSION = YES; + CLANG_WARN_OBJC_IMPLICIT_RETAIN_SELF = YES; + CLANG_WARN_OBJC_LITERAL_CONVERSION = YES; + CLANG_WARN_OBJC_ROOT_CLASS = YES_ERROR; + CLANG_WARN_QUOTED_INCLUDE_IN_FRAMEWORK_HEADER = YES; + CLANG_WARN_RANGE_LOOP_ANALYSIS = YES; + CLANG_WARN_STRICT_PROTOTYPES = YES; + CLANG_WARN_SUSPICIOUS_MOVE = YES; + CLANG_WARN_UNGUARDED_AVAILABILITY = YES_AGGRESSIVE; + CLANG_WARN_UNREACHABLE_CODE = YES; + CLANG_WARN__DUPLICATE_METHOD_MATCH = YES; + COPY_PHASE_STRIP = NO; + DEBUG_INFORMATION_FORMAT = "dwarf-with-dsym"; + ENABLE_NS_ASSERTIONS = NO; + ENABLE_STRICT_OBJC_MSGSEND = YES; + ENABLE_USER_SCRIPT_SANDBOXING = NO; + GCC_C_LANGUAGE_STANDARD = gnu17; + GCC_NO_COMMON_BLOCKS = YES; + GCC_WARN_64_TO_32_BIT_CONVERSION = YES; + GCC_WARN_ABOUT_RETURN_TYPE = YES_ERROR; + GCC_WARN_UNDECLARED_SELECTOR = YES; + GCC_WARN_UNINITIALIZED_AUTOS = YES_AGGRESSIVE; + GCC_WARN_UNUSED_FUNCTION = YES; + GCC_WARN_UNUSED_VARIABLE = YES; + IPHONEOS_DEPLOYMENT_TARGET = 18.2; + LOCALIZATION_PREFERS_STRING_CATALOGS = YES; + MTL_ENABLE_DEBUG_INFO = NO; + MTL_FAST_MATH = YES; + SDKROOT = iphoneos; + SWIFT_COMPILATION_MODE = wholemodule; + VALIDATE_PRODUCT = YES; + }; + name = Release; + }; + 5C40F620F8F710C457E29100 /* Debug */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Debug; + }; + 6F527A73C2B3BB7B74541389 /* Release */ = { + isa = XCBuildConfiguration; + buildSettings = { + ARCHS = arm64; + ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon; + ASSETCATALOG_COMPILER_GLOBAL_ACCENT_COLOR_NAME = AccentColor; + CODE_SIGN_IDENTITY = "Apple Development"; + CODE_SIGN_STYLE = Automatic; + DEVELOPMENT_ASSET_PATHS = "\"iosApp/Preview Content\""; + DEVELOPMENT_TEAM = "${TEAM_ID}"; + ENABLE_PREVIEWS = YES; + GENERATE_INFOPLIST_FILE = YES; + INFOPLIST_FILE = iosApp/Info.plist; + INFOPLIST_KEY_UIApplicationSceneManifest_Generation = YES; + INFOPLIST_KEY_UIApplicationSupportsIndirectInputEvents = YES; + INFOPLIST_KEY_UILaunchScreen_Generation = YES; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPad = "UIInterfaceOrientationPortrait UIInterfaceOrientationPortraitUpsideDown UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + INFOPLIST_KEY_UISupportedInterfaceOrientations_iPhone = "UIInterfaceOrientationPortrait UIInterfaceOrientationLandscapeLeft UIInterfaceOrientationLandscapeRight"; + LD_RUNPATH_SEARCH_PATHS = ( + "$(inherited)", + "@executable_path/Frameworks", + ); + SWIFT_EMIT_LOC_STRINGS = YES; + SWIFT_VERSION = 5.0; + TARGETED_DEVICE_FAMILY = "1,2"; + }; + name = Release; + }; +/* End XCBuildConfiguration section */ + +/* Begin XCConfigurationList section */ + BC12BB3CA5431F80864A9FB6 /* Build configuration list for PBXProject "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + D94EDDF3060BE1195839DB3B /* Debug */, + 82793F83896C4AA8A17CB990 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; + E2F0DFE8FC41CBCBBDA48BEE /* Build configuration list for PBXNativeTarget "iosApp" */ = { + isa = XCConfigurationList; + buildConfigurations = ( + 5C40F620F8F710C457E29100 /* Debug */, + 6F527A73C2B3BB7B74541389 /* Release */, + ); + defaultConfigurationIsVisible = 0; + defaultConfigurationName = Release; + }; +/* End XCConfigurationList section */ + }; + rootObject = 482A5064A76EC02255085D66 /* Project object */; +}
\ No newline at end of file diff --git a/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata new file mode 100644 index 0000000..919434a --- /dev/null +++ b/iosApp/iosApp.xcodeproj/project.xcworkspace/contents.xcworkspacedata @@ -0,0 +1,7 @@ +<?xml version="1.0" encoding="UTF-8"?> +<Workspace + version = "1.0"> + <FileRef + location = "self:"> + </FileRef> +</Workspace> diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png Binary files differnew file mode 100644 index 0000000..1ad3abc --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png Binary files differnew file mode 100644 index 0000000..1ad3abc --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@2x~ipad.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png Binary files differnew file mode 100644 index 0000000..d8d4a9d --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20@3x.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png Binary files differnew file mode 100644 index 0000000..a678b1d --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-20~ipad.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png Binary files differnew file mode 100644 index 0000000..0b7c640 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png Binary files differnew file mode 100644 index 0000000..ba13984 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png Binary files differnew file mode 100644 index 0000000..ba13984 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@2x~ipad.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png Binary files differnew file mode 100644 index 0000000..4b730e5 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29@3x.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png Binary files differnew file mode 100644 index 0000000..0b7c640 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-29~ipad.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png Binary files differnew file mode 100644 index 0000000..b9d09fa --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png Binary files differnew file mode 100644 index 0000000..b9d09fa --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@2x~ipad.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png Binary files differnew file mode 100644 index 0000000..e645dd9 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40@3x.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png Binary files differnew file mode 100644 index 0000000..1ad3abc --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-40~ipad.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png Binary files differnew file mode 100644 index 0000000..e645dd9 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@2x~car.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png Binary files differnew file mode 100644 index 0000000..2440719 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-60@3x~car.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png Binary files differnew file mode 100644 index 0000000..3067194 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon-83.5@2x~ipad.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png Binary files differnew file mode 100644 index 0000000..e645dd9 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png Binary files differnew file mode 100644 index 0000000..e7778b4 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@2x~ipad.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png Binary files differnew file mode 100644 index 0000000..2440719 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon@3x.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png Binary files differnew file mode 100644 index 0000000..26c3552 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ios-marketing.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png Binary files differnew file mode 100644 index 0000000..e0428a2 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/AppIcon~ipad.png diff --git a/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json new file mode 100644 index 0000000..bd04914 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/AppIcon.appiconset/Contents.json @@ -0,0 +1,134 @@ +{ + "images": [ + { + "filename": "AppIcon@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "60x60" + }, + { + "filename": "AppIcon@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "60x60" + }, + { + "filename": "AppIcon~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "76x76" + }, + { + "filename": "AppIcon@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "76x76" + }, + { + "filename": "AppIcon-83.5@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "83.5x83.5" + }, + { + "filename": "AppIcon-40@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "40x40" + }, + { + "filename": "AppIcon-40@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "40x40" + }, + { + "filename": "AppIcon-40~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "40x40" + }, + { + "filename": "AppIcon-40@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "40x40" + }, + { + "filename": "AppIcon-20@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "20x20" + }, + { + "filename": "AppIcon-20@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "20x20" + }, + { + "filename": "AppIcon-20~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "20x20" + }, + { + "filename": "AppIcon-20@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "20x20" + }, + { + "filename": "AppIcon-29.png", + "idiom": "iphone", + "scale": "1x", + "size": "29x29" + }, + { + "filename": "AppIcon-29@2x.png", + "idiom": "iphone", + "scale": "2x", + "size": "29x29" + }, + { + "filename": "AppIcon-29@3x.png", + "idiom": "iphone", + "scale": "3x", + "size": "29x29" + }, + { + "filename": "AppIcon-29~ipad.png", + "idiom": "ipad", + "scale": "1x", + "size": "29x29" + }, + { + "filename": "AppIcon-29@2x~ipad.png", + "idiom": "ipad", + "scale": "2x", + "size": "29x29" + }, + { + "filename": "AppIcon-60@2x~car.png", + "idiom": "car", + "scale": "2x", + "size": "60x60" + }, + { + "filename": "AppIcon-60@3x~car.png", + "idiom": "car", + "scale": "3x", + "size": "60x60" + }, + { + "filename": "AppIcon~ios-marketing.png", + "idiom": "ios-marketing", + "scale": "1x", + "size": "1024x1024" + } + ], + "info": { + "author": "iconkitchen", + "version": 1 + } +}
\ No newline at end of file diff --git a/iosApp/iosApp/Assets.xcassets/Contents.json b/iosApp/iosApp/Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iosApp/iosApp/Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/ContentView.swift b/iosApp/iosApp/ContentView.swift new file mode 100644 index 0000000..c765ff2 --- /dev/null +++ b/iosApp/iosApp/ContentView.swift @@ -0,0 +1,21 @@ +import UIKit +import SwiftUI +import ComposeApp + +struct ComposeView: UIViewControllerRepresentable { + func makeUIViewController(context: Context) -> UIViewController { + MainViewControllerKt.MainViewController() + } + + func updateUIViewController(_ uiViewController: UIViewController, context: Context) {} +} + +struct ContentView: View { + var body: some View { + ComposeView() + .ignoresSafeArea() + } +} + + + diff --git a/iosApp/iosApp/Info.plist b/iosApp/iosApp/Info.plist new file mode 100644 index 0000000..11845e1 --- /dev/null +++ b/iosApp/iosApp/Info.plist @@ -0,0 +1,8 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>CADisableMinimumFrameDurationOnPhone</key> + <true/> +</dict> +</plist> diff --git a/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json new file mode 100644 index 0000000..73c0059 --- /dev/null +++ b/iosApp/iosApp/Preview Content/Preview Assets.xcassets/Contents.json @@ -0,0 +1,6 @@ +{ + "info" : { + "author" : "xcode", + "version" : 1 + } +} diff --git a/iosApp/iosApp/iOSApp.swift b/iosApp/iosApp/iOSApp.swift new file mode 100644 index 0000000..d83dca6 --- /dev/null +++ b/iosApp/iosApp/iOSApp.swift @@ -0,0 +1,10 @@ +import SwiftUI + +@main +struct iOSApp: App { + var body: some Scene { + WindowGroup { + ContentView() + } + } +}
\ No newline at end of file diff --git a/settings.gradle.kts b/settings.gradle.kts new file mode 100644 index 0000000..0c54e50 --- /dev/null +++ b/settings.gradle.kts @@ -0,0 +1,35 @@ +rootProject.name = "TheAbstractionEngine" +enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS") + +pluginManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + gradlePluginPortal() + } +} + +dependencyResolutionManagement { + repositories { + google { + mavenContent { + includeGroupAndSubgroups("androidx") + includeGroupAndSubgroups("com.android") + includeGroupAndSubgroups("com.google") + } + } + mavenCentral() + } +} + +plugins { + id("org.gradle.toolchains.foojay-resolver-convention") version "1.0.0" +} + +include(":composeApp")
\ No newline at end of file |
