From 05a38fee4a19cbee0e52d020b26be9d6cdc32a54 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:44:23 -0600 Subject: [PATCH 01/11] feat(skill): Enhance Android Maps 3D SDK skill with L1/L2 structure and Kotlin Views template --- .gemini/skills/android-maps3d-sdk/SKILL.md | 250 +++--------------- .../references/kotlin_views_template.md | 205 ++++++++++++++ skill-dev.md | 215 +++++++++++++++ 3 files changed, 456 insertions(+), 214 deletions(-) create mode 100644 .gemini/skills/android-maps3d-sdk/references/kotlin_views_template.md create mode 100644 skill-dev.md diff --git a/.gemini/skills/android-maps3d-sdk/SKILL.md b/.gemini/skills/android-maps3d-sdk/SKILL.md index 0692098..d2446d1 100644 --- a/.gemini/skills/android-maps3d-sdk/SKILL.md +++ b/.gemini/skills/android-maps3d-sdk/SKILL.md @@ -1,27 +1,41 @@ --- name: android-maps3d-sdk -description: Guide for integrating the Google Maps 3D SDK into an Android Jetpack Compose application. Use when users ask to add Maps 3D, 3D maps, or Map3DView to their Android app in Compose. +description: Use this skill when the user wants to integrate the Google Maps 3D SDK into an Android application. This skill provides procedural guidance for setup, lifecycle, and 3D object manipulation. Do NOT use for standard 2D maps. --- # Android Maps 3D SDK Integration -You are an expert Android developer specializing in Jetpack Compose and modern Android architecture. Follow these instructions carefully to integrate the `play-services-maps3d` library into the user's Android application. +This skill guides you through integrating the Google Maps 3D SDK into an Android project. It follows the principles of progressive disclosure and relies on environment-specific templates. -We should start with a few questions about how the developer want to use `Maps3DView`. +## Prerequisites & Skill Alignment -Are they using or planning on using Jetpack Compose? +> [!NOTE] +> This skill should be used in conjunction with: +> - **Android Architecture Skill**: For proper MVVM/MVI layering. +> - **Android Security Skill**: For API key protection and permissions. -Are they using or planning on using dependency injection (such as Hilt or Koin)? +## Procedural Workflow -## 1. Setup Dependencies +### Step 1: Determine the Environment +You MUST ask the user which development environment they are targeting before providing code: +1. **Kotlin (Views)** (Prioritized) +2. **Kotlin + Compose** +3. **Java** -First, add the necessary versions and libraries to your `libs.versions.toml` file: +Additionally, ask if they want to use the **Object Tracking Delegate** pattern (useful for clean cleanup in complex apps). + +Based on their response, you will load the appropriate template from the `references/` directory. + +### Step 2: Base Setup +Regardless of the environment, the following setup is required. + +#### 1. Dependencies +Add the necessary versions and libraries to your `libs.versions.toml` file: ```toml [versions] -# NOTE: Verify this is the latest version of the Maps 3D SDK, as it is subject to change. +# NOTE: Verify this is the latest version of the Maps 3D SDK playServicesMaps3d = "0.2.0" -# NOTE: Verify this is the latest version of lifecycle-runtime-ktx. lifecycleRuntimeKtx = "2.8.5" [libraries] @@ -29,20 +43,18 @@ play-services-maps3d = { group = "com.google.android.gms", name = "play-services androidx-lifecycle-runtime-ktx = { group = "androidx.lifecycle", name = "lifecycle-runtime-ktx", version.ref = "lifecycleRuntimeKtx" } ``` -Then, add the dependencies to the app-level `build.gradle.kts` file. +Then, add the dependencies to the app-level `build.gradle.kts` file: ```kotlin dependencies { // Google Maps 3D SDK implementation(libs.play.services.maps3d) - // Lifecycle Runtime KTX for Coroutine interop implementation(libs.androidx.lifecycle.runtime.ktx) } ``` -## 2. Setup the Secrets Gradle Plugin - +#### 2. API Key & Manifest Use the Secrets Gradle Plugin for Android to inject the API key securely. In app-level `build.gradle.kts`: ```kotlin @@ -64,7 +76,6 @@ In `AndroidManifest.xml`, add the required permissions and reference the injecte - ``` -Add the API Key to `secrets.properties`: - -```properties -MAPS3D_API_KEY=YOUR_API_KEY -``` - -## 3. Implement the Map3D Container Composable - -If the user is working in a Jetpack Compose app or is creating a Compose app, We can use an -`AndroidView` to bridge between the View-based `Map3DView` and Jetpack Compose. - -```kotlin -import androidx.compose.foundation.layout.Box -import androidx.compose.foundation.layout.fillMaxSize -import androidx.compose.runtime.* -import androidx.compose.ui.Modifier -import androidx.compose.ui.viewinterop.AndroidView -import com.google.android.gms.maps.model.LatLng -import com.google.android.gms.maps.model.Map3DMode -import com.google.android.gms.maps.model.Map3DOptions -import com.google.android.gms.maps.Map3DView -import com.google.android.gms.maps.GoogleMap3D -import com.google.android.gms.maps.OnMap3DViewReadyCallback - -@Composable -fun Map3DContainer( - modifier: Modifier = Modifier, - options: Map3DOptions -) { - // 1. Hoist State: Remember the map object - var googleMap by remember { mutableStateOf(null) } - - Box(modifier = modifier.fillMaxSize()) { - AndroidView( - modifier = Modifier.fillMaxSize(), - factory = { context -> - Map3DView(context, options).apply { - // Manually call onCreate. - onCreate(null) - } - }, - update = { view -> - view.getMap3DViewAsync( - object : OnMap3DViewReadyCallback { - override fun onMap3DViewReady(map3D: GoogleMap3D) { - googleMap = map3D // Capture the controller - } - override fun onError(e: Exception) { - googleMap = null - throw e - } - } - ) - }, - onRelease = { view -> - googleMap = null - view.onDestroy() - } - ) - } -} -``` - -## 4. Best Practices & Guidelines -* **Double-Wait Pattern:** Triggering animations from Compose buttons requires the **Double-Wait** pattern (`awaitCameraAnimation` + `awaitSteady`) to ensure peak visual quality. -* **Coroutine Bridging:** Animations in the 3D SDK are fire-and-forget. Use an `awaitCameraAnimation(map: GoogleMap3D)` suspend wrapper function using `suspendCancellableCoroutine` for structured concurrency: - -```kotlin -suspend fun awaitCameraAnimation(map: GoogleMap3D) = suspendCancellableCoroutine { continuation -> - map.setCameraAnimationEndListener { - map.setCameraAnimationEndListener(null) // Cleanup listener to avoid leaks - if (continuation.isActive) { - continuation.resume(Unit) - } - } - continuation.invokeOnCancellation { - map.setCameraAnimationEndListener(null) - } -} -``` - -* **Lifecycle:** You must pass lifecycle events down to `Map3DView`. In Compose, `factory` block takes care of instantiation and `onRelease` handles cleanup (`onDestroy()`). Ensure `onCreate` is called in the factory block. - * *Critical Note:* The underlying `GoogleMap3D` engine instance is effectively created once per application lifecycle. If your `AndroidView` Composable leaves the composition and later returns (creating a new `Map3DView`), the underlying 3D engine may still retain previously added objects (like Polygons) from the destroyed view. You must manually clear or track your objects to avoid duplicates across recompositions or Navigation transitions. -* **Initialization & Adding Objects:** Do **not** attempt to set the camera or add 3D objects (like Polygons) immediately after the `GoogleMap3D` reference is ready. The renderer needs time to warm up. - * **Initial Camera:** Always set the initial camera position declaratively via `Map3DOptions` (passed into your container view) rather than imperatively moving the camera after the map loads. This avoids dizzying "flight" animations from coordinate `(0,0)` on startup. - * **Adding Objects:** Only inject geometries into the scene after the map has signaled it is fully ready and stable. Typically, this means waiting for an `onMapSteady` callback. -* **Updating Map Objects:** When updating an existing Map Object (e.g., `Polygon`, `Polyline`), do **not** use `remove()` and re-add a new one, as this causes flickering. Instead, use `getId()` from the existing object and pass it to a new `PolygonOptions` (or equivalent) builder, then call `addPolygon()` with those new options on the same `GoogleMap3D` instance. The SDK uses the matching ID to update the existing object gracefully without flickering. -* **Nullable Camera Properties:** The 3D SDK's `Camera` object has 6 degrees of freedom. Properties like `heading`, `tilt`, `roll`, and `range` are returned as `Double?` (nullable) since the renderer does not always guarantee a value for every property. Handle these nulls defensively when extracting camera telemetry, especially when persisting position data. -* **Parameter Validation:** The Maps 3D library will throw exceptions and crash if passed out-of-bounds telemetry for camera movements or locations. Standardize a validation/coercion layer (e.g., returning a `toValidCamera()` extension object) covering: - * `latitude`: clamped to `[-90.0, 90.0]` - * `longitude`: clamped to `[-180.0, 180.0]` - * `tilt`: clamped to `[0.0, 90.0]` - * `range`: clamped to `[0.0, 63170000.0]` - * `heading`: wrapped to `[0.0, 360.0]` - * `roll`: wrapped to `[-360.0, 360.0]` - * `altitude`: clamped to `[0.0, MAX_ALTITUDE_METERS]` - - **Example Extension:** - ```kotlin - /** Helper to wrap cyclic values like heading and roll */ - fun Double.wrapIn(lower: Double, upper: Double): Double { - val range = upper - lower - if (range <= 0) return this - val offset = this - lower - return lower + (offset - Math.floor(offset / range) * range) - } - - /** Extension to sanitize camera telemetry before passing to engine */ - fun Camera?.toValidCamera(): Camera { - val source = this ?: return Camera.DEFAULT_CAMERA - return camera { - center = latLngAltitude { - latitude = source.center.latitude.coerceIn(-90.0..90.0) - longitude = source.center.longitude.coerceIn(-180.0..180.0) - altitude = source.center.altitude.coerceIn(0.0..LatLngAltitude.MAX_ALTITUDE_METERS) - } - heading = source.heading?.toDouble()?.wrapIn(0.0, 360.0) ?: 0.0 - tilt = source.tilt?.toDouble()?.coerceIn(0.0..90.0) ?: 60.0 - roll = source.roll?.toDouble()?.wrapIn(-360.0, 360.0) ?: 0.0 - range = source.range?.toDouble()?.coerceIn(0.0..63170000.0) ?: 1500.0 - } - } - ``` - -* **Immutable Updates (`copy` Extensions):** The 3D SDK builders (like `camera {}` or `latLngAltitude {}`) do not natively provide a `copy()` method like Kotlin data classes. To gracefully update a single property (like altitude) while retaining the rest of the object's complex state, implement custom `.copy()` extensions: - - ```kotlin - /** Extension to clone and modify a Camera */ - fun Camera.copy( - center: LatLngAltitude? = null, - heading: Double? = null, - tilt: Double? = null, - range: Double? = null, - roll: Double? = null, - ): Camera { - val objectToCopy = this - return camera { - this.center = center ?: objectToCopy.center - this.heading = heading ?: objectToCopy.heading - this.tilt = tilt ?: objectToCopy.tilt - this.range = range ?: objectToCopy.range - this.roll = roll ?: objectToCopy.roll - } - } - - /** Extension to clone and modify a LatLngAltitude */ - fun LatLngAltitude.copy( - latitude: Double? = null, - longitude: Double? = null, - altitude: Double? = null, - ): LatLngAltitude { - val objectToCopy = this - return latLngAltitude { - this.latitude = latitude ?: objectToCopy.latitude - this.longitude = longitude ?: objectToCopy.longitude - this.altitude = altitude ?: objectToCopy.altitude - } - } - ``` - -## 5. A Note on Initialization - -Immediate Setup (onMap3DViewReady): Fails on cold starts because the viewport layout and binding matrix are not yet stable. Camera updates are completely ignored, and overlays may render offset. -OnMapReady & OnMapSteady Listeners: These callbacks are strictly edge-triggered. While they may fire on a cold start, they will skip execution entirely on a warm restore (e.g., returning to the Activity) because the view is already considered ready/steady. This leaves the user with a frozen camera state and missing overlays. -The Solution: Timer-Based Delay Workaround -Until the SDK introduces native Coroutine support (like an .awaitMap() extension) or synchronous state getters (like isMapReady), the most reliable workaround for both cold and warm starts is a timer-based delay. By intentionally deferring the initialization logic slightly, we bypass the brittle edge-triggered listeners entirely. - -Kotlin Implementation (Preferred) -Use a coroutine with delay() inside your initialization flow: - - ```kotlin - // Ensure you are launching on the Main thread to interact with the Map3DView safely - lifecycleScope.launch { - // Wait for the viewport to fully inflate and bindings to stabilize. - // 500ms is a safe brute-force threshold to avoid edge-trigger races. - delay(500) - - // Position camera and add overlays safely - setupMapElements() - } - ``` - -Java Implementation -Use a standard Handler mapped to the Main Looper: - - ```java - new Handler(Looper.getMainLooper()).postDelayed(() -> { - // Wait for the viewport to fully inflate, then safely apply updates - setupMapElements(); - }, 500); - ``` +### Step 3: Load Environment Template +After the user selects the environment, load the corresponding file from: +- Kotlin (Views): `references/kotlin_views_template.md` +- Kotlin + Compose: (To be created) +- Java: (To be created) -[IMPORTANT] Even with the timer delay successfully ensuring your camera updates fire, you must still implement an isInitialized boolean latch -(or dynamically check if your layers exist) within setupMapElements(). Otherwise, you will endlessly stack duplicate markers, model nodes, -and polyline overlays on top of each other during every warm Activity re-entry. +### Step 4: Apply Best Practices +Follow these strict procedural rules to avoid common pitfalls: -## 6. Execution Steps -1. Add the 3D Maps SDK dependencies. -2. Setup the Secrets Gradle plugin if not already set. -3. Update `AndroidManifest.xml` with the specific `com.google.android.geo.maps3d.API_KEY` tag. -4. Create the `Map3DContainer` composable wrapped in `AndroidView`. -5. Inform the user how to add `MAPS3D_API_KEY` securely. +1. **Initialization Delay**: Fails on cold starts due to viewport layout races. Always use a 500ms delay before initializing map elements (camera updates, adding objects). +2. **Double-Wait Pattern**: For animations, wait for camera animation end AND map steady state. +3. **Object Updates**: Use matching IDs to update Polygons/Polylines instead of removing and re-adding (prevents flickering). +4. **Null Safety**: Camera properties like heading and tilt can be null. Handle them defensively. diff --git a/.gemini/skills/android-maps3d-sdk/references/kotlin_views_template.md b/.gemini/skills/android-maps3d-sdk/references/kotlin_views_template.md new file mode 100644 index 0000000..00b588e --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/kotlin_views_template.md @@ -0,0 +1,205 @@ +# Kotlin (Views) Template for Android Maps 3D SDK + +Use this template when building an Android application using Kotlin and standard XML Views (not Compose). + +## 1. Layout XML + +Create a layout file (e.g., `activity_main.xml`) and add the `Map3DView`. + +```xml + + + + + + +``` + +## 2. Activity Code + +Implement the activity to handle the map lifecycle and initialization. + +```kotlin +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.maps3d.GoogleMap3D +import com.google.android.gms.maps3d.Map3DView +import com.google.android.gms.maps3d.OnMap3DViewReadyCallback +import com.google.android.gms.maps3d.model.camera +import com.google.android.gms.maps3d.model.latLngAltitude +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class MainActivity : AppCompatActivity() { + + private lateinit var mapView: Map3DView + private var googleMap: GoogleMap3D? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + mapView = findViewById(R.id.map_view) + mapView.onCreate(savedInstanceState) + + // Use DefaultLifecycleObserver to handle most lifecycle events automatically + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { mapView.onStart() } + override fun onResume(owner: LifecycleOwner) { mapView.onResume() } + override fun onPause(owner: LifecycleOwner) { mapView.onPause() } + override fun onStop(owner: LifecycleOwner) { mapView.onStop() } + override fun onDestroy(owner: LifecycleOwner) { mapView.onDestroy() } + }) + + mapView.getMap3DViewAsync(object : OnMap3DViewReadyCallback { + override fun onMap3DViewReady(map: GoogleMap3D) { + googleMap = map + + // Fails on cold starts because the viewport layout and binding matrix are not yet stable. + // Use a timer-based delay workaround. + lifecycleScope.launch { + // Wait for the viewport to fully inflate and bindings to stabilize. + delay(500) + setupMapElements() + } + } + + override fun onError(e: Exception) { + Log.e("MainActivity", "Error loading map", e) + } + }) + } + + private fun setupMapElements() { + val map = googleMap ?: return + + // Example: Set initial camera position + val initialCamera = camera { + center = latLngAltitude { + latitude = 40.0150 + longitude = -105.2705 + altitude = 5000.0 + } + heading = 0.0 + tilt = 45.0 + roll = 0.0 + range = 10000.0 + } + map.setCamera(initialCamera) + + // Add more initialization logic here (markers, polylines, etc.) + } + + // onLowMemory and onSaveInstanceState still need to be forwarded manually + override fun onLowMemory() { + super.onLowMemory() + mapView.onLowMemory() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + mapView.onSaveInstanceState(outState) + } +} +``` + +## 3. Best Practices + +- **Lifecycle**: Always forward lifecycle methods to `Map3DView`. +- **Initialization Delay**: Use the 500ms delay pattern shown above to avoid edge-trigger races on startup. +- **State Latch**: Implement an `isInitialized` boolean latch in `setupMapElements()` if you plan to call it multiple times or across lifecycle events to avoid stacking duplicate objects. + +## 4. Optional: Object Tracking Delegate + +For advanced use cases where you need to add and remove many objects (Markers, Polylines, etc.) dynamically, it is helpful to use a delegate wrapper to track these objects for easy cleanup. + +Here is a simplified version of the `TrackedMap3D` pattern used in the project samples: + +```kotlin +import com.google.android.gms.maps3d.GoogleMap3D +import com.google.android.gms.maps3d.model.Marker +import com.google.android.gms.maps3d.model.MarkerOptions +import com.google.android.gms.maps3d.model.Polygon +import com.google.android.gms.maps3d.model.PolygonOptions +import com.google.android.gms.maps3d.model.Polyline +import com.google.android.gms.maps3d.model.PolylineOptions +import com.google.android.gms.maps3d.model.Camera + +class TrackedMap3D( + val delegate: GoogleMap3D, + private val items: MutableList = mutableListOf() +) { + fun addMarker(options: MarkerOptions): Marker? { + val marker = delegate.addMarker(options) + if (marker != null) items.add(marker) + return marker + } + + fun addPolyline(options: PolylineOptions): Polyline? { + val polyline = delegate.addPolyline(options) + if (polyline != null) items.add(polyline) + return polyline + } + + fun addPolygon(options: PolygonOptions): Polygon? { + val polygon = delegate.addPolygon(options) + if (polygon != null) items.add(polygon) + return polygon + } + + /** + * Clears all tracked objects from the map. + */ + fun clearAll() { + items.forEach { item -> + when (item) { + is Marker -> item.remove() + is Polyline -> item.remove() + is Polygon -> item.remove() + } + } + items.clear() + } + + // Forward other necessary methods to the delegate as needed + fun setCamera(camera: Camera) = delegate.setCamera(camera) +} +``` + +To use it, wrap the `GoogleMap3D` instance and ensure you clear it in `onDestroy` to avoid retaining objects across Activity instances (since the 3D engine persists across Activity recreation): + +```kotlin +private var trackedMap: TrackedMap3D? = null + +override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + // ... + + lifecycle.addObserver(object : DefaultLifecycleObserver { + // ... + override fun onDestroy(owner: LifecycleOwner) { + // Automatically clean up objects to avoid cruft + trackedMap?.clearAll() + mapView.onDestroy() + } + }) + + mapView.getMap3DViewAsync(object : OnMap3DViewReadyCallback { + override fun onMap3DViewReady(map: GoogleMap3D) { + trackedMap = TrackedMap3D(map) + // ... + } + // ... + }) +} +``` + diff --git a/skill-dev.md b/skill-dev.md new file mode 100644 index 0000000..7805c15 --- /dev/null +++ b/skill-dev.md @@ -0,0 +1,215 @@ +# **Architectural Frameworks for Advanced Agentic Skill Development: A Technical Synthesis** + +The rapid maturation of Large Language Model (LLM) ecosystems has necessitated a transition from static Software Development Kits (SDKs) to dynamic, agent-centric capabilities known as Agentic Skills. This architectural shift represents a fundamental move from code libraries designed for human invocation to portable, reusable packages of instructions, scripts, and resources specifically optimized for autonomous systems1. For organizations possessing an extensive body of sample code but struggling with lackluster agent performance, the transformation involves more than simple prompt engineering; it requires the systematic encapsulation of domain expertise into the AgentSkills.io open standard, utilizing progressive disclosure to manage the inherent constraints of the LLM context window3. + +## **The Taxonomy of Agentic Skills versus Traditional SDKs** + +The primary limitation of traditional SDK-based approaches for agents is threefold: token inefficiency, exponential growth in decision complexity, and a lack of modular reusability3. When a developer provides an agent with thousands of lines of raw API samples, the agent often suffers from "context dilution," where the core task instructions compete for attention with low-level implementation details4. Agentic skills resolve this by functioning as "onboarding guides" or "static cheat-sheets" that transform a general-purpose model into a specialized agent equipped with procedural knowledge and explicit applicability conditions7. + +The formal definition of a skill extends beyond the simple prompt. It is a four-tuple formalization consisting of a textual descriptor (![][image1]), an intra-skill policy (![][image2]), a resource set (![][image3]), and a set of applicability conditions (![][image4])7. This formalization allows the skill to persist across sessions and carry executable policies that are significantly more robust than one-time, session-scoped plans10. + +| Concept | Unit of Reuse | Internal Logic | Context Impact | +| :---- | :---- | :---- | :---- | +| **SDK Tool** | Atomic Function | Fixed Code | High (if all loaded) | +| **Prompt Template** | Static Text | No decision logic | High (monolithic) | +| **Agent Skill** | Procedural Module | ReAct/Heuristics | Low (progressive) | +| **Workflow** | Deterministic Graph | Rigid Sequence | Moderate | + +1 + +## **The Three-Tier Progressive Disclosure Architecture** + +The defining characteristic of an "advanced" skill is its adherence to the principle of progressive disclosure. This architectural pattern originates from user interface design but, when applied to agents, ensures that the system loads only the necessary information at each phase of the task lifecycle5. Advanced SDKs, such as the Microsoft Agent Framework and Google ADK, implement this through three distinct levels of information loading1. + +### **Level 1: Metadata and Discovery (The L1 Layer)** + +At the start of any agentic session, only the metadata—specifically the name and description—is injected into the system prompt. This "menu" allows the agent to identify available capabilities without incurring a massive token penalty5. A skill with 100 installed capabilities might only consume 1,000 baseline tokens if structured correctly, representing a 90% reduction in context overhead compared to monolithic prompts12. + +### **Level 2: Instructions and Workflow (The L2 Layer)** + +The full SKILL.md body is loaded only when the agent explicitly activates a skill based on user intent. This file acts as the "brain," containing the high-level procedural instructions that guide the agent through multi-step tasks like deployment, code review, or architecture planning5. The specification recommends keeping this layer under 500 lines and 5,000 tokens to ensure the agent remains focused on the primary workflow4. + +### **Level 3: Resources and Deterministic Scripts (The L3 Layer)** + +Detailed reference materials, such as API specifications, schemas, and templates, are stored in a references/ or assets/ directory. These are loaded on demand only when the L2 instructions require them1. Furthermore, executable scripts (Python, Bash, or PowerShell) reside in a scripts/ directory to handle fragile, repetitive, or algorithmic tasks that the LLM would otherwise struggle to perform reliably15. + +## **Engineering Semantic Assets from Existing Code Samples** + +A common pitfall in skill development is providing the agent with raw code samples and expecting it to infer the underlying patterns. To create a "productive" skill, these samples must be transformed into structured assets and parameterized templates20. + +### **Few-Shot Template Parameterization** + +Few-shot prompting is a well-established best practice that significantly improves output consistency6. To utilize an existing body of sample code effectively, developers should extract 3 to 5 high-quality examples that cover different slices of the API usage space24. These should not be raw files but rather "semantic templates" where variable sections are marked for the agent to populate25. + +Mathematically, the probability of a correct agent response ![][image5] is positively correlated with the relevance and quality of the provided few-shot examples ![][image6]: + +![][image7] +where ![][image8] is the user query and ![][image9] is the weight of the example based on its structural similarity to the task7. + +### **Asset vs. Reference Directory Organization** + +The distinction between assets/ and references/ is critical for accurate execution. Files that the agent is meant to read and build upon (e.g., boilerplate code, schemas) should be placed in references/17. Conversely, static files used "as-is" in the final output (e.g., images, company logos, fonts) belong in the assets/ directory17. + +| Directory | Purpose | Loaded into Context? | Interaction Type | +| :---- | :---- | :---- | :---- | +| references/ | API specs, Schemas | Yes, on-demand | Read and Interpret | +| assets/ | Templates, Logos | No | Direct Output Injection | +| scripts/ | Automation, CLI tools | No (Output only) | Execution | +| examples/ | Test data, Use cases | Yes, if requested | Pattern Matching | + +15 + +## **Intent Mapping and Description Optimization** + +The description field in the YAML frontmatter of the SKILL.md file carries the entire burden of triggering the skill during implicit invocation29. If the description fails to accurately map user intent to the skill's capabilities, the agent will never reach for the instructions, regardless of their quality29. + +### **The Imperative Phrase Principle** + +Advanced skill descriptions should use imperative phrasing—"Use this skill when..."—rather than descriptive phrasing—"This skill does..."29. This framing treats the description as an instruction for the agent to act29. Developers should also include "negative triggers" to prevent false positives, clearly stating scenarios where the skill should *not* be used15. + +### **The Trigger Evaluation Loop** + +To improve accuracy, developers should implement an optimization loop for their descriptions. This involves splitting a set of user queries into a "train set" (60%) and a "validation set" (40%)29. The train set queries guide the refinement of the description, while the validation set determines if the changes generalize across diverse phrasing and casual language29. + +| Strategy | Description Attribute | Impact on Accuracy | +| :---- | :---- | :---- | +| **Intent-Based** | Focuses on user's end goal | High | +| **Domain-Specific** | Mentions specific file types or APIs | Moderate | +| **Exclusionary** | Lists "do not use" cases | High (reduces false triggers) | +| **Technical** | Describes internal mechanics | Low | + +15 + +## **Procedural Logic and Deterministic Execution Patterns** + +A "lackluster" skill often provides vague guidance that leads to hallucinations. A productive, advanced skill employs strict procedural patterns such as ReAct (Reasoning \+ Acting) and Plan-Validate-Execute4. + +### **The ReAct Pattern in Skills** + +The ReAct pattern addresses the issue of models hallucinating tool actions. By forcing the agent to interleave thoughts (analyzing the state), actions (calling a tool or script), and observations (evaluating the result), the skill ensures the agent remains grounded in real-world data8. + +### **The Plan-Validate-Execute Cycle** + +For complex or destructive operations—such as database migrations or large-scale code refactors—the SKILL.md should enforce a "Plan-Validate-Execute" pattern4. The agent first creates an implementation plan (often as a JSON or Markdown artifact), validates it against a "source of truth" using a bundled validation script, and only executes the changes after the plan is verified4. This pattern is machine-verifiable and allows for reversible planning, where the agent can iterate on the plan without modifying the original files16. + +## **Advanced State Management and Persistence** + +Maintaining the state of an agentic workflow across multiple turns is a primary differentiator for advanced systems. Standard LLM calls are stateless, but advanced agent frameworks introduce "shared memory" through state schemas and checkpointers34. + +### **Stateful Orchestration with LangGraph** + +LangGraph represents a shift toward stateful, event-driven orchestration by modeling workflows as a StateGraph34. In this architecture, the state is a first-class citizen, typically defined as a Pydantic model that accumulates conversation history and task-specific memory34. Reducer functions are used to manage how nodes update the state, enabling "append-only" logs that prevent overwriting critical information34. + +### **Checkpointing and "Time Travel"** + +Production-grade skills implement state persistence using backends like SQLite or Postgres37. This allows for: + +* **Thread Recovery**: Resuming a session after an interruption33. +* **Time Travel**: Reverting to a previous agent state to debug or retry from a different branch31. +* **Approval Gates**: Pausing execution until a human provides validation, particularly for high-stakes financial or legal tasks32. + +## **Evaluation and Lifecycle Management of Agentic Skills** + +A skill's productivity is measured by its success rate, latency, and reliability. Tools like SkillGrade provide a framework for automated evaluation, allowing developers to catch regressions before deployment15. + +### **The SkillGrade Rubric** + +SkillGrade automates the testing of agent discovery and execution. It generates tasks based on the skill's eval.yaml and uses "grader" models to assess performance based on a rubric15. Effective grading should focus on outcomes (e.g., "Is the security vulnerability patched?") rather than procedural steps40. + +| Metric | Measurement Method | Target Baseline | +| :---- | :---- | :---- | +| **Discovery Rate** | Trigger success on validation set | \> 0.85 | +| **Success Rate** | Task completion (Outcome-based) | \> 0.80 | +| **Token Efficiency** | Token usage per successful task | Lower is better | +| **Latency** | End-to-end processing time | Dependent on model | + +30 + +### **Logic Validation and "Ruthless" QA** + +Before releasing a skill, developers should subject it to a "ruthless" QA audit. This involves feeding the full SKILL.md to an LLM and asking it to hunt for vulnerabilities, unsupported configurations, or implicit assumptions about the environment15. This "Logic Validation" phase uncovers "Execution Blockers" where the agent might be forced to guess due to ambiguous instructions15. + +## **Security and Governance in Skill Deployment** + +As agent skills become more autonomous, security teams must implement strict dependency governance and sandboxing1. Advanced skills should follow the principle of "least privilege," requesting only the minimum toolset required for their specific domain43. + +### **Sandboxing and Input Filtering** + +Executable scripts bundled with skills must run in isolated environments (e.g., containers, seccomp, or firejail) to prevent malicious filesystem or network access1. Furthermore, "Output Filtering" patterns should be used to scrub Personally Identifiable Information (PII) or harmful content before it is shared with the user39. + +### **The Role of Meta-Skills and Human Oversight** + +While the generation of skills can be automated using "Meta-Skills" (skills designed to create other skills), human oversight remains paramount1. Organizations are encouraged to treat SKILL.md files like code dependencies, requiring peer reviews and version control for every change12. + +## **Synthesis: The Roadmap to a Productive Agent Skill** + +The transition from a "lackluster" attempt to a "productive" skill is a multi-stage process of refinement. It begins with the extraction of real expertise from hands-on tasks and existing project artifacts4. By organizing this expertise into the three-level progressive disclosure architecture, developers ensure that deep domain knowledge is available on-demand without degrading the agent's performance5. + +The integration of semantic assets—such as parameterized templates derived from the user's extensive code samples—provides the agent with the "analogical reasoning" capabilities needed to excel at complex SDK implementations21. When combined with deterministic scripts for algorithmic verification and robust state management for multi-turn persistence, the resulting agent skill becomes a reliable, enterprise-ready capability that moves beyond basic assistance toward true autonomous agency8. The ultimate utility of these systems is found not just in the intelligence of the underlying model, but in the robustness of the structural and procedural guardrails provided by the well-designed skill package18. + +#### **Works cited** + +1. Agent Skills | Microsoft Learn, [https://learn.microsoft.com/en-us/agent-framework/agents/skills](https://learn.microsoft.com/en-us/agent-framework/agents/skills) +2. Give Your Agents Domain Expertise with Agent Skills in Microsoft Agent Framework, [https://devblogs.microsoft.com/agent-framework/give-your-agents-domain-expertise-with-agent-skills-in-microsoft-agent-framework/](https://devblogs.microsoft.com/agent-framework/give-your-agents-domain-expertise-with-agent-skills-in-microsoft-agent-framework/) +3. aws-samples/sample-strands-agents-agentskills: Agent Skills implementation for Strands Agents SDK \- GitHub, [https://github.com/aws-samples/sample-strands-agents-agentskills](https://github.com/aws-samples/sample-strands-agents-agentskills) +4. [https://agentskills.io/skill-creation/best-practices](https://agentskills.io/skill-creation/best-practices) +5. The SKILL.md Pattern: How to Write AI Agent Skills That Actually Work | by Bibek Poudel, [https://bibek-poudel.medium.com/the-skill-md-pattern-how-to-write-ai-agent-skills-that-actually-work-72a3169dd7ee](https://bibek-poudel.medium.com/the-skill-md-pattern-how-to-write-ai-agent-skills-that-actually-work-72a3169dd7ee) +6. Effective context engineering for AI agents \- Anthropic, [https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents) +7. Agent Skill Framework: Perspectives on the Potential of Small Language Models in Industrial Environments \- arXiv, [https://arxiv.org/html/2602.16653v1](https://arxiv.org/html/2602.16653v1) +8. Agent Skills: The Missing Layer That Makes AI Agents Enterprise Ready \- DEV Community, [https://dev.to/sreeni5018/agent-skills-the-missing-layer-that-makes-ai-agents-enterprise-ready-3gc](https://dev.to/sreeni5018/agent-skills-the-missing-layer-that-makes-ai-agents-enterprise-ready-3gc) +9. deepagentsjs/examples/skills/skill-creator/SKILL.md at main \- GitHub, [https://github.com/langchain-ai/deepagentsjs/blob/main/examples/skills/skill-creator/SKILL.md](https://github.com/langchain-ai/deepagentsjs/blob/main/examples/skills/skill-creator/SKILL.md) +10. SoK: Agentic Skills — Beyond Tool Use in LLM Agents \- arXiv, [https://arxiv.org/html/2602.20867v1](https://arxiv.org/html/2602.20867v1) +11. Choose a design pattern for your agentic AI system | Cloud Architecture Center, [https://docs.cloud.google.com/architecture/choose-design-pattern-agentic-ai-system](https://docs.cloud.google.com/architecture/choose-design-pattern-agentic-ai-system) +12. Developer's Guide to Building ADK Agents with Skills, [https://developers.googleblog.com/en/developers-guide-to-building-adk-agents-with-skills/](https://developers.googleblog.com/en/developers-guide-to-building-adk-agents-with-skills/) +13. Progressive Disclosure: the technique that helps control context (and tokens) in AI agents, [https://medium.com/@martia\_es/progressive-disclosure-the-technique-that-helps-control-context-and-tokens-in-ai-agents-8d6108b09289](https://medium.com/@martia_es/progressive-disclosure-the-technique-that-helps-control-context-and-tokens-in-ai-agents-8d6108b09289) +14. Introducing Agent Plugins for AWS | AWS Developer Tools Blog, [https://aws.amazon.com/blogs/developer/introducing-agent-plugins-for-aws/](https://aws.amazon.com/blogs/developer/introducing-agent-plugins-for-aws/) +15. GitHub \- mgechev/skills-best-practices: Write professional-grade skills for agents, validate them using LLMs, and maintain a lean context window., [https://github.com/mgechev/skills-best-practices](https://github.com/mgechev/skills-best-practices) +16. Skill authoring best practices \- Claude API Docs, [https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) +17. skill-creator \- anthropics/financial-services-plugins \- GitHub, [https://github.com/anthropics/financial-services-plugins/blob/main/financial-analysis/skills/skill-creator/SKILL.md](https://github.com/anthropics/financial-services-plugins/blob/main/financial-analysis/skills/skill-creator/SKILL.md) +18. GitHub \- gohypergiant/agent-skills: A collection of skills for AI coding agents. Skills are packaged instructions and scripts that extend agent capabilities., [https://github.com/gohypergiant/agent-skills](https://github.com/gohypergiant/agent-skills) +19. agent-skills.instructions.md \- github/awesome-copilot, [https://github.com/github/awesome-copilot/blob/main/instructions/agent-skills.instructions.md](https://github.com/github/awesome-copilot/blob/main/instructions/agent-skills.instructions.md) +20. Include few-shot examples | Generative AI on Vertex AI \- Google Cloud Documentation, [https://docs.cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/few-shot-examples](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/few-shot-examples) +21. Achieving 5x Agentic Coding Performance with Few-Shot Prompting, [https://towardsdatascience.com/5x-agentic-coding-performance-with-few-shot-prompting/](https://towardsdatascience.com/5x-agentic-coding-performance-with-few-shot-prompting/) +22. Daily Papers \- Hugging Face, [https://huggingface.co/papers?q=experience%20memory](https://huggingface.co/papers?q=experience+memory) +23. Few-Shot Learning for LLMs: Examples and Implementation Guide \- Tetrate, [https://tetrate.io/learn/ai/few-shot-learning-llms](https://tetrate.io/learn/ai/few-shot-learning-llms) +24. Few-Shot Prompting for Agentic Systems: Teaching by Example \- Comet, [https://www.comet.com/site/blog/few-shot-prompting/](https://www.comet.com/site/blog/few-shot-prompting/) +25. Few-Shot Prompting \- Portkey Docs, [https://docs.portkey.ai/docs/guides/use-cases/few-shot-prompting](https://docs.portkey.ai/docs/guides/use-cases/few-shot-prompting) +26. LangChain Prompt Templates: Complete Guide with Examples \- Latenode Blog, [https://latenode.com/blog/ai-frameworks-technical-infrastructure/langchain-setup-tools-agents-memory/langchain-prompt-templates-complete-guide-with-examples](https://latenode.com/blog/ai-frameworks-technical-infrastructure/langchain-setup-tools-agents-memory/langchain-prompt-templates-complete-guide-with-examples) +27. FloorplanQA: A Benchmark for Spatial Reasoning in LLMs using Structured Representations, [https://arxiv.org/html/2507.07644v2](https://arxiv.org/html/2507.07644v2) +28. Provide examples (few-shot prompting) \- Amazon Nova \- AWS Documentation, [https://docs.aws.amazon.com/nova/latest/userguide/prompting-examples.html](https://docs.aws.amazon.com/nova/latest/userguide/prompting-examples.html) +29. Optimizing skill descriptions \- Agent Skills, [https://agentskills.io/skill-creation/optimizing-descriptions](https://agentskills.io/skill-creation/optimizing-descriptions) +30. Building AI with Chatbot Intent Recognition: Guide \- IrisAgent, [https://irisagent.com/blog/building-chatbots-with-intent-detection-guide/](https://irisagent.com/blog/building-chatbots-with-intent-detection-guide/) +31. Design Patterns for Agentic AI and Multi-Agent Systems \- AppsTek Corp, [https://appstekcorp.com/blog/design-patterns-for-agentic-ai-and-multi-agent-systems/](https://appstekcorp.com/blog/design-patterns-for-agentic-ai-and-multi-agent-systems/) +32. 7 Must-Know Agentic AI Design Patterns \- MachineLearningMastery.com, [https://machinelearningmastery.com/7-must-know-agentic-ai-design-patterns/](https://machinelearningmastery.com/7-must-know-agentic-ai-design-patterns/) +33. Single-responsibility agents and multi-agent workflows in AI-powered development tools, [https://www.epam.com/insights/ai/blogs/single-responsibility-agents-and-multi-agent-workflows](https://www.epam.com/insights/ai/blogs/single-responsibility-agents-and-multi-agent-workflows) +34. The Architecture of Agents: Planning, Action, and State Management in Large Language Models | by Tejaswi kashyap \- GoPenAI, [https://blog.gopenai.com/the-architecture-of-agents-planning-action-and-state-management-in-large-language-models-e00b340fcf09](https://blog.gopenai.com/the-architecture-of-agents-planning-action-and-state-management-in-large-language-models-e00b340fcf09) +35. LangGraph State Management Patterns | Claude Code Skill \- MCP Market, [https://mcpmarket.com/tools/skills/langgraph-state-management](https://mcpmarket.com/tools/skills/langgraph-state-management) +36. The Best Open Source Frameworks For Building AI Agents in 2026 \- Firecrawl, [https://www.firecrawl.dev/blog/best-open-source-agent-frameworks](https://www.firecrawl.dev/blog/best-open-source-agent-frameworks) +37. LangGraph Checkpoint Patterns \- Claude Code Skill \- MCP Market, [https://mcpmarket.com/tools/skills/langgraph-state-persistence](https://mcpmarket.com/tools/skills/langgraph-state-persistence) +38. Claude Code Guide: Features and Best Practices, [https://www.elliotjreed.com/ai/claude-code-guide-and-tips](https://www.elliotjreed.com/ai/claude-code-guide-and-tips) +39. claude-skills/engineering/agent-designer/SKILL.md at main \- GitHub, [https://github.com/alirezarezvani/claude-skills/blob/main/engineering/agent-designer/SKILL.md](https://github.com/alirezarezvani/claude-skills/blob/main/engineering/agent-designer/SKILL.md) +40. GitHub \- mgechev/skillgrade: "Unit tests" for your agent skills, [https://github.com/mgechev/skillgrade](https://github.com/mgechev/skillgrade) +41. Skill Design for LLM Agents by Minko Gechev \- GitNation, [https://gitnation.com/contents/skill-design-for-llm-agents](https://gitnation.com/contents/skill-design-for-llm-agents) +42. What is agentic coding? How it works and use cases | Google Cloud, [https://cloud.google.com/discover/what-is-agentic-coding](https://cloud.google.com/discover/what-is-agentic-coding) +43. Top 10 Claude Code Skills Every Builder Should Know in 2026 \- Composio, [https://composio.dev/content/top-claude-skills](https://composio.dev/content/top-claude-skills) +44. CodeBuddy Code Best Practices, [https://www.codebuddy.ai/docs/cli/best-practices](https://www.codebuddy.ai/docs/cli/best-practices) +45. 3 Principles for Designing Agent Skills \- Block Engineering Blog, [https://engineering.block.xyz/blog/3-principles-for-designing-agent-skills](https://engineering.block.xyz/blog/3-principles-for-designing-agent-skills) +46. The Few Shot Prompting Guide \- PromptHub, [https://www.prompthub.us/blog/the-few-shot-prompting-guide](https://www.prompthub.us/blog/the-few-shot-prompting-guide) + +[image1]: + +[image2]: + +[image3]: + +[image4]: + +[image5]: + +[image6]: + +[image7]: + +[image8]: + +[image9]: \ No newline at end of file From 075872078ca50bd2b0105d43e978d3e0223ecd80 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 14:58:55 -0600 Subject: [PATCH 02/11] feat(skill): Restructure skill into assets and references --- .gemini/skills/android-maps3d-sdk/SKILL.md | 41 ++-- .../samples/views_kotlin/MapActivity.kt.txt | 85 ++++++++ .../samples/views_kotlin/activity_main.xml | 11 + .../views_kotlin/snippets/TrackedMap3D.kt.txt | 41 ++++ .../snippets/object_manager_usage.kt.txt | 18 ++ .../references/best_practices.md | 30 +++ .../references/kotlin_views_template.md | 205 ------------------ 7 files changed, 209 insertions(+), 222 deletions(-) create mode 100644 .gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/MapActivity.kt.txt create mode 100644 .gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml create mode 100644 .gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/snippets/TrackedMap3D.kt.txt create mode 100644 .gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/snippets/object_manager_usage.kt.txt create mode 100644 .gemini/skills/android-maps3d-sdk/references/best_practices.md delete mode 100644 .gemini/skills/android-maps3d-sdk/references/kotlin_views_template.md diff --git a/.gemini/skills/android-maps3d-sdk/SKILL.md b/.gemini/skills/android-maps3d-sdk/SKILL.md index d2446d1..b8ebc53 100644 --- a/.gemini/skills/android-maps3d-sdk/SKILL.md +++ b/.gemini/skills/android-maps3d-sdk/SKILL.md @@ -16,15 +16,20 @@ This skill guides you through integrating the Google Maps 3D SDK into an Android ## Procedural Workflow -### Step 1: Determine the Environment -You MUST ask the user which development environment they are targeting before providing code: -1. **Kotlin (Views)** (Prioritized) -2. **Kotlin + Compose** -3. **Java** +### Step 1: Determine the Environment and Features +You MUST ask the user to clarify their stack and needs: +1. **Language**: Kotlin or Java? +2. **UI Framework**: Jetpack Compose or standard XML Views? +3. **Features**: Do they need automatic object management (cleanup)? -Additionally, ask if they want to use the **Object Tracking Delegate** pattern (useful for clean cleanup in complex apps). +Based on their response, follow the **Selection Logic** to retrieve boilerplate from `assets/samples/` and consult rules in `references/`. -Based on their response, you will load the appropriate template from the `references/` directory. +### Implementation Guidance (Selection Logic) +1. Identify the user's stack: (Language: Kotlin/Java, UI: Compose/Views). +2. Check if Lifecycle Management is required (always for 3D maps). +3. **Retrieve** the corresponding boilerplate from `assets/samples/` (e.g., `assets/samples/views_kotlin/MapActivity.kt.txt`). +4. If object tracking is needed, **retrieve** the snippet from `assets/samples/views_kotlin/snippets/`. +5. **Reference** the memory management best practices in `references/best_practices.md` to ensure the generated code follows SDK safety guidelines. ### Step 2: Base Setup Regardless of the environment, the following setup is required. @@ -86,15 +91,17 @@ In `AndroidManifest.xml`, add the required permissions and reference the injecte ``` ### Step 3: Load Environment Template -After the user selects the environment, load the corresponding file from: -- Kotlin (Views): `references/kotlin_views_template.md` -- Kotlin + Compose: (To be created) -- Java: (To be created) +After determining the stack, load the corresponding files from `assets/samples/`: +- Kotlin (Views): + - Layout: `assets/samples/views_kotlin/activity_main.xml` + - Activity: `assets/samples/views_kotlin/MapActivity.kt.txt` + - Snippet (Object Manager): `assets/samples/views_kotlin/snippets/object_manager_usage.kt.txt` +- Kotlin + Compose: (To be created in `assets/samples/compose/`) +- Java: (To be created in `assets/samples/views_java/`) ### Step 4: Apply Best Practices -Follow these strict procedural rules to avoid common pitfalls: - -1. **Initialization Delay**: Fails on cold starts due to viewport layout races. Always use a 500ms delay before initializing map elements (camera updates, adding objects). -2. **Double-Wait Pattern**: For animations, wait for camera animation end AND map steady state. -3. **Object Updates**: Use matching IDs to update Polygons/Polylines instead of removing and re-adding (prevents flickering). -4. **Null Safety**: Camera properties like heading and tilt can be null. Handle them defensively. +Consult `references/best_practices.md` for detailed explanation of rules. Key rules to enforce: +1. **Initialization Delay**: Always use a 500ms delay before initializing map elements. +2. **Object Management**: Use the `TrackedMap3D` delegate to clean up objects on destroy to avoid cruft. +3. **Double-Wait Pattern**: For animations, wait for camera animation end AND map steady state. +4. **Object Updates**: Use matching IDs to update Polygons/Polylines instead of removing and re-adding. diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/MapActivity.kt.txt b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/MapActivity.kt.txt new file mode 100644 index 0000000..f8c2b36 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/MapActivity.kt.txt @@ -0,0 +1,85 @@ +import android.os.Bundle +import android.util.Log +import androidx.appcompat.app.AppCompatActivity +import androidx.lifecycle.DefaultLifecycleObserver +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.lifecycleScope +import com.google.android.gms.maps3d.GoogleMap3D +import com.google.android.gms.maps3d.Map3DView +import com.google.android.gms.maps3d.OnMap3DViewReadyCallback +import com.google.android.gms.maps3d.model.camera +import com.google.android.gms.maps3d.model.latLngAltitude +import kotlinx.coroutines.delay +import kotlinx.coroutines.launch + +class MainActivity : AppCompatActivity() { + + private lateinit var mapView: Map3DView + private var googleMap: GoogleMap3D? = null + + override fun onCreate(savedInstanceState: Bundle?) { + super.onCreate(savedInstanceState) + setContentView(R.layout.activity_main) + + mapView = findViewById(R.id.map_view) + mapView.onCreate(savedInstanceState) + + // Use DefaultLifecycleObserver to handle most lifecycle events automatically + lifecycle.addObserver(object : DefaultLifecycleObserver { + override fun onStart(owner: LifecycleOwner) { mapView.onStart() } + override fun onResume(owner: LifecycleOwner) { mapView.onResume() } + override fun onPause(owner: LifecycleOwner) { mapView.onPause() } + override fun onStop(owner: LifecycleOwner) { mapView.onStop() } + override fun onDestroy(owner: LifecycleOwner) { mapView.onDestroy() } + }) + + mapView.getMap3DViewAsync(object : OnMap3DViewReadyCallback { + override fun onMap3DViewReady(map: GoogleMap3D) { + googleMap = map + + // Fails on cold starts because the viewport layout and binding matrix are not yet stable. + // Use a timer-based delay workaround. + lifecycleScope.launch { + // Wait for the viewport to fully inflate and bindings to stabilize. + delay(500) + setupMapElements() + } + } + + override fun onError(e: Exception) { + Log.e("MainActivity", "Error loading map", e) + } + }) + } + + private fun setupMapElements() { + val map = googleMap ?: return + + // Example: Set initial camera position + val initialCamera = camera { + center = latLngAltitude { + latitude = 40.0150 + longitude = -105.2705 + altitude = 5000.0 + } + heading = 0.0 + tilt = 45.0 + roll = 0.0 + range = 10000.0 + } + map.setCamera(initialCamera) + + // Add more initialization logic here (markers, polylines, etc.) + } + + // onLowMemory and onSaveInstanceState still need to be forwarded manually + override fun onLowMemory() { + super.onLowMemory() + mapView.onLowMemory() + } + + override fun onSaveInstanceState(outState: Bundle) { + super.onSaveInstanceState(outState) + mapView.onSaveInstanceState(outState) + } +} diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml new file mode 100644 index 0000000..2fa964b --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml @@ -0,0 +1,11 @@ + + + + + + diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/snippets/TrackedMap3D.kt.txt b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/snippets/TrackedMap3D.kt.txt new file mode 100644 index 0000000..570e3e9 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/snippets/TrackedMap3D.kt.txt @@ -0,0 +1,41 @@ +import com.google.android.gms.maps3d.GoogleMap3D +import com.google.android.gms.maps3d.model.Marker +import com.google.android.gms.maps3d.model.MarkerOptions +import com.google.android.gms.maps3d.model.Polygon +import com.google.android.gms.maps3d.model.PolygonOptions +import com.google.android.gms.maps3d.model.Polyline +import com.google.android.gms.maps3d.model.PolylineOptions + +class TrackedMap3D( + val delegate: GoogleMap3D, + private val items: MutableList = mutableListOf() +) { + fun addMarker(options: MarkerOptions): Marker? { + val marker = delegate.addMarker(options) + if (marker != null) items.add(marker) + return marker + } + + fun addPolyline(options: PolylineOptions): Polyline? { + val polyline = delegate.addPolyline(options) + if (polyline != null) items.add(polyline) + return polyline + } + + fun addPolygon(options: PolygonOptions): Polygon? { + val polygon = delegate.addPolygon(options) + if (polygon != null) items.add(polygon) + return polygon + } + + fun clearAll() { + items.forEach { item -> + when (item) { + is Marker -> item.remove() + is Polyline -> item.remove() + is Polygon -> item.remove() + } + } + items.clear() + } +} diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/snippets/object_manager_usage.kt.txt b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/snippets/object_manager_usage.kt.txt new file mode 100644 index 0000000..1be702c --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/snippets/object_manager_usage.kt.txt @@ -0,0 +1,18 @@ +// 1. Declare the tracked map instance +private var trackedMap: TrackedMap3D? = null + +// 2. In onCreate, update the lifecycle observer to clean up on destroy +lifecycle.addObserver(object : DefaultLifecycleObserver { + // ... other methods + override fun onDestroy(owner: LifecycleOwner) { + // Automatically clean up objects to avoid cruft + trackedMap?.clearAll() + mapView.onDestroy() + } +}) + +// 3. In onMap3DViewReady, wrap the map +override fun onMap3DViewReady(map: GoogleMap3D) { + trackedMap = TrackedMap3D(map) + // ... +} diff --git a/.gemini/skills/android-maps3d-sdk/references/best_practices.md b/.gemini/skills/android-maps3d-sdk/references/best_practices.md new file mode 100644 index 0000000..463e17d --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/best_practices.md @@ -0,0 +1,30 @@ +# Best Practices for Android Maps 3D SDK + +This document explains the architectural decisions and constraints when working with the Google Maps 3D SDK for Android. + +## 1. Lifecycle Management + +The `Map3DView` is a heavy component that requires explicit lifecycle management. Failing to forward lifecycle events can lead to memory leaks, crashes, or black screens. + +### The Observer Pattern +Instead of overriding lifecycle methods in the Activity (like `onStart`, `onResume`, etc.), it is best practice to use a `DefaultLifecycleObserver`. This keeps the Activity code clean and ensures that lifecycle events are automatically forwarded. + +*Constraint*: `onCreate`, `onLowMemory`, and `onSaveInstanceState` still need manual handling in the Activity as they are not fully covered by the standard lifecycle observer or require specific arguments (like `Bundle`). + +## 2. Initialization Delay (Cold Starts) + +When the 3D map is first loaded, the viewport layout and binding matrix may not be fully stable immediately after `onMap3DViewReady` is called. + +### The Delay Pattern +If you attempt to add objects or set the camera immediately in `onMap3DViewReady`, it might fail or render incorrectly on cold starts. +*Rule*: Always introduce a small delay (e.g., 500ms using coroutines `delay(500)`) before initializing map elements to ensure the renderer is fully ready. + +## 3. Object Management and Cleanup + +The underlying `GoogleMap3D` engine instance is effectively created once per application lifecycle (singleton-like behavior). It persists even across Activity recreation. + +### The Cruft Pitfall +If you add markers, polylines, or polygons to the map and do not remove them when the Activity is destroyed, they will remain on the map. When a new Activity instance is created, the user will see the old objects ("cruft"). + +### The Delegate Solution +Use a wrapper like `TrackedMap3D` to keep track of all objects added during a session. Hook into the `onDestroy` lifecycle event to call `clearAll()` on this delegate, ensuring a clean state for the next usage. diff --git a/.gemini/skills/android-maps3d-sdk/references/kotlin_views_template.md b/.gemini/skills/android-maps3d-sdk/references/kotlin_views_template.md deleted file mode 100644 index 00b588e..0000000 --- a/.gemini/skills/android-maps3d-sdk/references/kotlin_views_template.md +++ /dev/null @@ -1,205 +0,0 @@ -# Kotlin (Views) Template for Android Maps 3D SDK - -Use this template when building an Android application using Kotlin and standard XML Views (not Compose). - -## 1. Layout XML - -Create a layout file (e.g., `activity_main.xml`) and add the `Map3DView`. - -```xml - - - - - - -``` - -## 2. Activity Code - -Implement the activity to handle the map lifecycle and initialization. - -```kotlin -import android.os.Bundle -import android.util.Log -import androidx.appcompat.app.AppCompatActivity -import androidx.lifecycle.DefaultLifecycleObserver -import androidx.lifecycle.LifecycleOwner -import androidx.lifecycle.lifecycleScope -import com.google.android.gms.maps3d.GoogleMap3D -import com.google.android.gms.maps3d.Map3DView -import com.google.android.gms.maps3d.OnMap3DViewReadyCallback -import com.google.android.gms.maps3d.model.camera -import com.google.android.gms.maps3d.model.latLngAltitude -import kotlinx.coroutines.delay -import kotlinx.coroutines.launch - -class MainActivity : AppCompatActivity() { - - private lateinit var mapView: Map3DView - private var googleMap: GoogleMap3D? = null - - override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - setContentView(R.layout.activity_main) - - mapView = findViewById(R.id.map_view) - mapView.onCreate(savedInstanceState) - - // Use DefaultLifecycleObserver to handle most lifecycle events automatically - lifecycle.addObserver(object : DefaultLifecycleObserver { - override fun onStart(owner: LifecycleOwner) { mapView.onStart() } - override fun onResume(owner: LifecycleOwner) { mapView.onResume() } - override fun onPause(owner: LifecycleOwner) { mapView.onPause() } - override fun onStop(owner: LifecycleOwner) { mapView.onStop() } - override fun onDestroy(owner: LifecycleOwner) { mapView.onDestroy() } - }) - - mapView.getMap3DViewAsync(object : OnMap3DViewReadyCallback { - override fun onMap3DViewReady(map: GoogleMap3D) { - googleMap = map - - // Fails on cold starts because the viewport layout and binding matrix are not yet stable. - // Use a timer-based delay workaround. - lifecycleScope.launch { - // Wait for the viewport to fully inflate and bindings to stabilize. - delay(500) - setupMapElements() - } - } - - override fun onError(e: Exception) { - Log.e("MainActivity", "Error loading map", e) - } - }) - } - - private fun setupMapElements() { - val map = googleMap ?: return - - // Example: Set initial camera position - val initialCamera = camera { - center = latLngAltitude { - latitude = 40.0150 - longitude = -105.2705 - altitude = 5000.0 - } - heading = 0.0 - tilt = 45.0 - roll = 0.0 - range = 10000.0 - } - map.setCamera(initialCamera) - - // Add more initialization logic here (markers, polylines, etc.) - } - - // onLowMemory and onSaveInstanceState still need to be forwarded manually - override fun onLowMemory() { - super.onLowMemory() - mapView.onLowMemory() - } - - override fun onSaveInstanceState(outState: Bundle) { - super.onSaveInstanceState(outState) - mapView.onSaveInstanceState(outState) - } -} -``` - -## 3. Best Practices - -- **Lifecycle**: Always forward lifecycle methods to `Map3DView`. -- **Initialization Delay**: Use the 500ms delay pattern shown above to avoid edge-trigger races on startup. -- **State Latch**: Implement an `isInitialized` boolean latch in `setupMapElements()` if you plan to call it multiple times or across lifecycle events to avoid stacking duplicate objects. - -## 4. Optional: Object Tracking Delegate - -For advanced use cases where you need to add and remove many objects (Markers, Polylines, etc.) dynamically, it is helpful to use a delegate wrapper to track these objects for easy cleanup. - -Here is a simplified version of the `TrackedMap3D` pattern used in the project samples: - -```kotlin -import com.google.android.gms.maps3d.GoogleMap3D -import com.google.android.gms.maps3d.model.Marker -import com.google.android.gms.maps3d.model.MarkerOptions -import com.google.android.gms.maps3d.model.Polygon -import com.google.android.gms.maps3d.model.PolygonOptions -import com.google.android.gms.maps3d.model.Polyline -import com.google.android.gms.maps3d.model.PolylineOptions -import com.google.android.gms.maps3d.model.Camera - -class TrackedMap3D( - val delegate: GoogleMap3D, - private val items: MutableList = mutableListOf() -) { - fun addMarker(options: MarkerOptions): Marker? { - val marker = delegate.addMarker(options) - if (marker != null) items.add(marker) - return marker - } - - fun addPolyline(options: PolylineOptions): Polyline? { - val polyline = delegate.addPolyline(options) - if (polyline != null) items.add(polyline) - return polyline - } - - fun addPolygon(options: PolygonOptions): Polygon? { - val polygon = delegate.addPolygon(options) - if (polygon != null) items.add(polygon) - return polygon - } - - /** - * Clears all tracked objects from the map. - */ - fun clearAll() { - items.forEach { item -> - when (item) { - is Marker -> item.remove() - is Polyline -> item.remove() - is Polygon -> item.remove() - } - } - items.clear() - } - - // Forward other necessary methods to the delegate as needed - fun setCamera(camera: Camera) = delegate.setCamera(camera) -} -``` - -To use it, wrap the `GoogleMap3D` instance and ensure you clear it in `onDestroy` to avoid retaining objects across Activity instances (since the 3D engine persists across Activity recreation): - -```kotlin -private var trackedMap: TrackedMap3D? = null - -override fun onCreate(savedInstanceState: Bundle?) { - super.onCreate(savedInstanceState) - // ... - - lifecycle.addObserver(object : DefaultLifecycleObserver { - // ... - override fun onDestroy(owner: LifecycleOwner) { - // Automatically clean up objects to avoid cruft - trackedMap?.clearAll() - mapView.onDestroy() - } - }) - - mapView.getMap3DViewAsync(object : OnMap3DViewReadyCallback { - override fun onMap3DViewReady(map: GoogleMap3D) { - trackedMap = TrackedMap3D(map) - // ... - } - // ... - }) -} -``` - From 3e8567c073ef3f897d0af4bbc8ef6b1bbb9459c7 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 15:40:29 -0600 Subject: [PATCH 03/11] feat(skill): Add Java samples, utilities, and secrets enforcement to Maps 3D skill --- .gemini/skills/android-maps3d-sdk/SKILL.md | 20 +- .../samples/views_java/MapActivity.java.txt | 86 +++++++ .../samples/views_java/activity_main.xml | 29 +++ .../views_java/snippets/TrackedMap3D.java.txt | 53 ++++ .../snippets/object_manager_usage.java.txt | 23 ++ .../samples/views_kotlin/MapActivity.kt.txt | 5 +- .../samples/views_kotlin/activity_main.xml | 20 +- .../references/best_practices.md | 5 +- .../references/secrets_enforcement.md | 94 +++++++ .../references/units_java.md | 121 +++++++++ .../references/units_kotlin.md | 100 ++++++++ .../references/utilities_java.md | 226 +++++++++++++++++ .../references/utilities_kotlin.md | 238 ++++++++++++++++++ 13 files changed, 1013 insertions(+), 7 deletions(-) create mode 100644 .gemini/skills/android-maps3d-sdk/assets/samples/views_java/MapActivity.java.txt create mode 100644 .gemini/skills/android-maps3d-sdk/assets/samples/views_java/activity_main.xml create mode 100644 .gemini/skills/android-maps3d-sdk/assets/samples/views_java/snippets/TrackedMap3D.java.txt create mode 100644 .gemini/skills/android-maps3d-sdk/assets/samples/views_java/snippets/object_manager_usage.java.txt create mode 100644 .gemini/skills/android-maps3d-sdk/references/secrets_enforcement.md create mode 100644 .gemini/skills/android-maps3d-sdk/references/units_java.md create mode 100644 .gemini/skills/android-maps3d-sdk/references/units_kotlin.md create mode 100644 .gemini/skills/android-maps3d-sdk/references/utilities_java.md create mode 100644 .gemini/skills/android-maps3d-sdk/references/utilities_kotlin.md diff --git a/.gemini/skills/android-maps3d-sdk/SKILL.md b/.gemini/skills/android-maps3d-sdk/SKILL.md index b8ebc53..76be23b 100644 --- a/.gemini/skills/android-maps3d-sdk/SKILL.md +++ b/.gemini/skills/android-maps3d-sdk/SKILL.md @@ -97,11 +97,23 @@ After determining the stack, load the corresponding files from `assets/samples/` - Activity: `assets/samples/views_kotlin/MapActivity.kt.txt` - Snippet (Object Manager): `assets/samples/views_kotlin/snippets/object_manager_usage.kt.txt` - Kotlin + Compose: (To be created in `assets/samples/compose/`) -- Java: (To be created in `assets/samples/views_java/`) +- Java: + - Layout: `assets/samples/views_java/activity_main.xml` + - Activity: `assets/samples/views_java/MapActivity.java.txt` + - Snippet (Object Manager): `assets/samples/views_java/snippets/object_manager_usage.java.txt` + ### Step 4: Apply Best Practices Consult `references/best_practices.md` for detailed explanation of rules. Key rules to enforce: -1. **Initialization Delay**: Always use a 500ms delay before initializing map elements. +1. **Initialization Delay**: Always use a 1-second delay before initializing map elements. 2. **Object Management**: Use the `TrackedMap3D` delegate to clean up objects on destroy to avoid cruft. -3. **Double-Wait Pattern**: For animations, wait for camera animation end AND map steady state. -4. **Object Updates**: Use matching IDs to update Polygons/Polylines instead of removing and re-adding. +3. **Utilities**: Use validation utilities to prevent crashes, and path utilities for smoothing/simplification (see `references/utilities_kotlin.md` or `references/utilities_java.md`). + +4. **Double-Wait Pattern**: For animations, wait for camera animation end AND map steady state. +5. **Object Updates**: Use matching IDs to update Polygons/Polylines instead of removing and re-adding. +6. **Unit Conversions**: For type-safe measurements and conversions, see `references/units_kotlin.md` or `references/units_java.md` (Optional). +7. **Secrets Security**: NEVER add `secrets.properties` to version control. NEVER add real API keys to source code or `local.defaults.properties` (see `references/secrets_enforcement.md` for Gradle enforcement snippet). + + + + diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/MapActivity.java.txt b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/MapActivity.java.txt new file mode 100644 index 0000000..db0e930 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/MapActivity.java.txt @@ -0,0 +1,86 @@ +import android.os.Bundle; +import android.os.Handler; +import android.os.Looper; +import android.util.Log; +import androidx.activity.EdgeToEdge; +import androidx.annotation.NonNull; +import androidx.appcompat.app.AppCompatActivity; +import androidx.lifecycle.DefaultLifecycleObserver; +import androidx.lifecycle.LifecycleOwner; +import com.google.android.gms.maps3d.GoogleMap3D; +import com.google.android.gms.maps3d.Map3DView; +import com.google.android.gms.maps3d.OnMap3DViewReadyCallback; + + +public class MainActivity extends AppCompatActivity { + + private Map3DView mapView; + private GoogleMap3D googleMap; + + @Override + protected void onCreate(Bundle savedInstanceState) { + super.onCreate(savedInstanceState); + EdgeToEdge.enable(this); + setContentView(R.layout.activity_main); + + mapView = findViewById(R.id.map_view); + mapView.onCreate(savedInstanceState); + + // Use DefaultLifecycleObserver to handle most lifecycle events automatically + getLifecycle().addObserver(new DefaultLifecycleObserver() { + @Override + public void onStart(@NonNull LifecycleOwner owner) { mapView.onStart(); } + @Override + public void onResume(@NonNull LifecycleOwner owner) { mapView.onResume(); } + @Override + public void onPause(@NonNull LifecycleOwner owner) { mapView.onPause(); } + @Override + public void onStop(@NonNull LifecycleOwner owner) { mapView.onStop(); } + @Override + public void onDestroy(@NonNull LifecycleOwner owner) { mapView.onDestroy(); } + }); + + mapView.getMap3DViewAsync(new OnMap3DViewReadyCallback() { + @Override + public void onMap3DViewReady(@NonNull GoogleMap3D map) { + googleMap = map; + + // Fails on cold starts because the viewport layout and binding matrix are not yet stable. + // The SDK requires a delay to bypass these readiness bugs before adding objects or setting camera. + // 1 second is usually sufficient. + new Handler(Looper.getMainLooper()).postDelayed(new Runnable() { + @Override + public void run() { + setupMapElements(); + } + }, 1000); + + + } + + @Override + public void onError(@NonNull Exception e) { + Log.e("MainActivity", "Error loading map", e); + } + }); + } + + private void setupMapElements() { + if (googleMap == null) return; + + Log.d("MainActivity", "Setting up map elements after delay"); + // Add your map initialization logic here (markers, polylines, etc.) + } + + @Override + public void onLowMemory() { + super.onLowMemory(); + mapView.onLowMemory(); + } + + @Override + protected void onSaveInstanceState(@NonNull Bundle outState) { + super.onSaveInstanceState(outState); + mapView.onSaveInstanceState(outState); + } +} diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/activity_main.xml b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/activity_main.xml new file mode 100644 index 0000000..53a6692 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/activity_main.xml @@ -0,0 +1,29 @@ + + + + + + + diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/snippets/TrackedMap3D.java.txt b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/snippets/TrackedMap3D.java.txt new file mode 100644 index 0000000..329191e --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/snippets/TrackedMap3D.java.txt @@ -0,0 +1,53 @@ +import com.google.android.gms.maps3d.GoogleMap3D; +import com.google.android.gms.maps3d.model.Marker; +import com.google.android.gms.maps3d.model.MarkerOptions; +import com.google.android.gms.maps3d.model.Polygon; +import com.google.android.gms.maps3d.model.PolygonOptions; +import com.google.android.gms.maps3d.model.Polyline; +import com.google.android.gms.maps3d.model.PolylineOptions; +import java.util.List; + +/** Decorator wrapper around GoogleMap3D to track elements for automated cleanup. */ +public class TrackedMap3D { + + private final GoogleMap3D delegate; + private final List items; + + public TrackedMap3D(GoogleMap3D delegate, List items) { + this.delegate = delegate; + this.items = items; + } + + public Marker addMarker(MarkerOptions options) { + Marker marker = delegate.addMarker(options); + if (marker != null) items.add(marker); + return marker; + } + + public Polyline addPolyline(PolylineOptions options) { + Polyline polyline = delegate.addPolyline(options); + if (polyline != null) items.add(polyline); + return polyline; + } + + public Polygon addPolygon(PolygonOptions options) { + Polygon polygon = delegate.addPolygon(options); + if (polygon != null) items.add(polygon); + return polygon; + } + + public void clearAll() { + for (Object item : items) { + if (item instanceof Marker) { + ((Marker) item).remove(); + } else if (item instanceof Polyline) { + ((Polyline) item).remove(); + } else if (item instanceof Polygon) { + ((Polygon) item).remove(); + } + } + items.clear(); + } + + // Forward other necessary methods to the delegate as needed +} diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/snippets/object_manager_usage.java.txt b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/snippets/object_manager_usage.java.txt new file mode 100644 index 0000000..3195df9 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_java/snippets/object_manager_usage.java.txt @@ -0,0 +1,23 @@ +// 1. Declare the tracked map instance and items list +private TrackedMap3D trackedMap; +private final List mapItems = new ArrayList<>(); + +// 2. In onCreate, update the lifecycle observer to clean up on destroy +getLifecycle().addObserver(new DefaultLifecycleObserver() { + // ... other methods + @Override + public void onDestroy(@NonNull LifecycleOwner owner) { + // Automatically clean up objects to avoid cruft + if (trackedMap != null) { + trackedMap.clearAll(); + } + mapView.onDestroy(); + } +}); + +// 3. In onMap3DViewReady, wrap the map +@Override +public void onMap3DViewReady(@NonNull GoogleMap3D map) { + trackedMap = new TrackedMap3D(map, mapItems); + // ... +} diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/MapActivity.kt.txt b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/MapActivity.kt.txt index f8c2b36..d1c5228 100644 --- a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/MapActivity.kt.txt +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/MapActivity.kt.txt @@ -1,6 +1,7 @@ import android.os.Bundle import android.util.Log import androidx.appcompat.app.AppCompatActivity +import androidx.core.view.WindowCompat import androidx.lifecycle.DefaultLifecycleObserver import androidx.lifecycle.LifecycleOwner import androidx.lifecycle.lifecycleScope @@ -19,6 +20,7 @@ class MainActivity : AppCompatActivity() { override fun onCreate(savedInstanceState: Bundle?) { super.onCreate(savedInstanceState) + WindowCompat.setDecorFitsSystemWindows(window, false) setContentView(R.layout.activity_main) mapView = findViewById(R.id.map_view) @@ -41,7 +43,8 @@ class MainActivity : AppCompatActivity() { // Use a timer-based delay workaround. lifecycleScope.launch { // Wait for the viewport to fully inflate and bindings to stabilize. - delay(500) + // Samples use a delay to bypass readiness bugs (1 second recommended). + delay(1000) setupMapElements() } } diff --git a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml index 2fa964b..53a6692 100644 --- a/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml +++ b/.gemini/skills/android-maps3d-sdk/assets/samples/views_kotlin/activity_main.xml @@ -1,11 +1,29 @@ + + android:layout_height="match_parent" + map3d:mapId="your_map_id" + map3d:mode="hybrid" + map3d:centerLat="40.748392" + map3d:centerLng="-73.986060" + map3d:centerAlt="175" + map3d:heading="0" + map3d:tilt="45" + map3d:range="1000" + map3d:roll="0" + map3d:minAltitude="0" + map3d:maxAltitude="1000000" + map3d:minHeading="0" + map3d:maxHeading="360" + map3d:minTilt="0" + map3d:maxTilt="90" /> diff --git a/.gemini/skills/android-maps3d-sdk/references/best_practices.md b/.gemini/skills/android-maps3d-sdk/references/best_practices.md index 463e17d..ef65e54 100644 --- a/.gemini/skills/android-maps3d-sdk/references/best_practices.md +++ b/.gemini/skills/android-maps3d-sdk/references/best_practices.md @@ -17,7 +17,10 @@ When the 3D map is first loaded, the viewport layout and binding matrix may not ### The Delay Pattern If you attempt to add objects or set the camera immediately in `onMap3DViewReady`, it might fail or render incorrectly on cold starts. -*Rule*: Always introduce a small delay (e.g., 500ms using coroutines `delay(500)`) before initializing map elements to ensure the renderer is fully ready. +*Rule*: Always introduce a delay before initializing map elements to ensure the renderer is fully ready. A **1-second delay** (e.g., `delay(1000)` or `Handler.postDelayed` with 1000ms) is recommended to bypass these readiness bugs. + + + ## 3. Object Management and Cleanup diff --git a/.gemini/skills/android-maps3d-sdk/references/secrets_enforcement.md b/.gemini/skills/android-maps3d-sdk/references/secrets_enforcement.md new file mode 100644 index 0000000..5e2d077 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/secrets_enforcement.md @@ -0,0 +1,94 @@ +# Secrets Enforcement (Gradle) + +To prevent accidental exposure of API keys, add this Gradle task to your project's root `build.gradle.kts` or module-level `build.gradle.kts`. It checks if `secrets.properties` is tracked by Git and fails the build if it is. + +## Kotlin DSL (`build.gradle.kts`) + +```kotlin +tasks.register("checkSecretsExposure") { + doLast { + // 1. Check if secrets.properties is tracked by Git + val secretsFile = file("secrets.properties") + if (secretsFile.exists()) { + val process = ProcessBuilder("git", "ls-files", "--error-unmatch", "secrets.properties") + .directory(project.rootDir) + .start() + val exitCode = process.waitFor() + + // If git ls-files finds the file, it returns 0 + if (exitCode == 0) { + throw GradleException( + "SECURITY ALERT: 'secrets.properties' is tracked by Git! " + + "Remove it from version control immediately using 'git rm --cached secrets.properties'." + ) + } + } + + // 2. Check if local.defaults.properties contains a real-looking key + val defaultsFile = file("local.defaults.properties") + if (defaultsFile.exists()) { + val props = java.util.Properties() + defaultsFile.inputStream().use { props.load(it) } + + props.forEach { key, value -> + val valueStr = value.toString() + // Simple heuristic: if it's long and doesn't look like a placeholder + if (valueStr.length > 20 && !valueStr.contains("YOUR_API_KEY") && !valueStr.contains("PLACEHOLDER")) { + throw GradleException( + "SECURITY ALERT: Potential real API key found in 'local.defaults.properties' for key '$key'. " + + "Use placeholders in this file and put real keys in 'secrets.properties'." + ) + } + } + } + } +} + +// Run this check before building +tasks.named("preBuild") { + dependsOn("checkSecretsExposure") +} +``` + +## Groovy DSL (`build.gradle`) + +```groovy +task checkSecretsExposure { + doLast { + // 1. Check if secrets.properties is tracked by Git + def secretsFile = file("secrets.properties") + if (secretsFile.exists()) { + def process = new ProcessBuilder("git", "ls-files", "--error-unmatch", "secrets.properties") + .directory(project.rootDir) + .start() + def exitCode = process.waitFor() + + if (exitCode == 0) { + throw new GradleException( + "SECURITY ALERT: 'secrets.properties' is tracked by Git! " + + "Remove it from version control immediately using 'git rm --cached secrets.properties'." + ) + } + } + + // 2. Check if local.defaults.properties contains a real-looking key + def defaultsFile = file("local.defaults.properties") + if (defaultsFile.exists()) { + def props = new Properties() + defaultsFile.withInputStream { props.load(it) } + + props.each { key, value -> + def valueStr = value.toString() + if (valueStr.length() > 20 && !valueStr.contains("YOUR_API_KEY") && !valueStr.contains("PLACEHOLDER")) { + throw new GradleException( + "SECURITY ALERT: Potential real API key found in 'local.defaults.properties' for key '$key'. " + + "Use placeholders in this file and put real keys in 'secrets.properties'." + ) + } + } + } + } +} + +preBuild.dependsOn checkSecretsExposure +``` diff --git a/.gemini/skills/android-maps3d-sdk/references/units_java.md b/.gemini/skills/android-maps3d-sdk/references/units_java.md new file mode 100644 index 0000000..1a1c841 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/units_java.md @@ -0,0 +1,121 @@ +# Unit Conversions (Java) + +These utilities provide type-safe measurements in meters and conversions to other units (feet, miles, kilometers), which are useful for displaying distances in 3D map applications. + +## Meters Class + +A class to wrap a value representing a measurement in meters. + +```java +public class Meters implements Comparable { + public static final double METERS_PER_FOOT = 3.28084; + public static final double METERS_PER_KILOMETER = 1000; + public static final double FEET_PER_METER = 1 / METERS_PER_FOOT; + public static final double FEET_PER_MILE = 5280; + public static final double MILES_PER_METER = 0.000621371; + + private final double value; + + public Meters(double value) { + this.value = value; + } + + public double getValue() { + return value; + } + + @Override + public int compareTo(Meters other) { + return Double.compare(this.value, other.value); + } + + public Meters minus(Meters other) { + return new Meters(this.value - other.value); + } + + public Meters plus(Meters other) { + return new Meters(this.value + other.value); + } + + // Factory methods + public static Meters fromMeters(double value) { + return new Meters(value); + } + + public static Meters fromKilometers(double km) { + return new Meters(km * METERS_PER_KILOMETER); + } + + public static Meters fromFeet(double feet) { + return new Meters(feet * FEET_PER_METER); + } + + public static Meters fromMiles(double miles) { + return new Meters(miles / MILES_PER_METER); + } + + // Conversions + public double toFeet() { + return value * METERS_PER_FOOT; + } + + public double toKilometers() { + return value / METERS_PER_KILOMETER; + } + + public double toMiles() { + return value * MILES_PER_METER; + } +} +``` + +## Units Converter (Optional) + +```java +public class ValueWithUnits { + public final double value; + public final String unitLabel; + + public ValueWithUnits(double value, String unitLabel) { + this.value = value; + this.unitLabel = unitLabel; + } +} + +public interface UnitsConverter { + ValueWithUnits toDistanceUnits(Meters meters); + ValueWithUnits toElevationUnits(Meters meters); +} + +class ImperialUnitsConverter implements UnitsConverter { + @Override + public ValueWithUnits toDistanceUnits(Meters meters) { + if (meters.getValue() < Meters.fromMiles(0.25).getValue()) { + return new ValueWithUnits(meters.toFeet(), "ft"); + } else { + return new ValueWithUnits(meters.toMiles(), "mi"); + } + } + + @Override + public ValueWithUnits toElevationUnits(Meters meters) { + return new ValueWithUnits(meters.toFeet(), "ft"); + } +} + +class MetricUnitsConverter implements UnitsConverter { + @Override + public ValueWithUnits toDistanceUnits(Meters meters) { + if (meters.getValue() < 1000) { + return new ValueWithUnits(meters.getValue(), "m"); + } else { + return new ValueWithUnits(meters.toKilometers(), "km"); + } + } + + @Override + public ValueWithUnits toElevationUnits(Meters meters) { + return new ValueWithUnits(meters.getValue(), "m"); + } +} +``` diff --git a/.gemini/skills/android-maps3d-sdk/references/units_kotlin.md b/.gemini/skills/android-maps3d-sdk/references/units_kotlin.md new file mode 100644 index 0000000..12581b0 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/units_kotlin.md @@ -0,0 +1,100 @@ +# Unit Conversions (Kotlin) + +These utilities provide type-safe measurements in meters and conversions to other units (feet, miles, kilometers), which are useful for displaying distances in 3D map applications. + +## Meters Value Class + +A value class to wrap a value representing a measurement in meters, preventing accidental mixing of units. + +```kotlin +import androidx.annotation.StringRes +import androidx.compose.runtime.Composable +import androidx.compose.runtime.Immutable +import androidx.compose.runtime.Stable +import androidx.compose.runtime.compositionLocalOf +import androidx.compose.ui.res.stringResource + +const val METERS_PER_FOOT = 3.28084 +const val METERS_PER_KILOMETER = 1000 +const val FEET_PER_METER = 1 / METERS_PER_FOOT +const val FEET_PER_MILE = 5280 +const val MILES_PER_METER = 0.000621371 + +@Immutable +@JvmInline +value class Meters(val value: Double) : Comparable { + override fun compareTo(other: Meters) = value.compareTo(other.value) + operator fun minus(other: Meters) = Meters(value = this.value - other.value) +} + +@Stable +inline val Number.meters: Meters get() = Meters(value = this.toDouble()) + +@Stable +inline val Number.m: Meters get() = Meters(value = this.toDouble()) + +@Stable +inline val Number.km: Meters get() = Meters(value = this.toDouble() * METERS_PER_KILOMETER) + +@Stable +inline val Number.feet: Meters get() = Meters(value = this.toDouble() * FEET_PER_METER) + +@Stable +inline val Number.miles: Meters get() = Meters(value = this.toDouble() / MILES_PER_METER) + +@Stable +inline val Meters.toFeet: Double get() = value * METERS_PER_FOOT + +@Stable +inline val Meters.toMeters: Double get() = value + +@Stable +inline val Meters.toKilometers: Double get() = value / METERS_PER_KILOMETER + +@Stable +inline val Meters.toMiles: Double get() = (value * MILES_PER_METER) + +@Stable +fun Meters.plus(other: Meters) = Meters(value = this.value + other.value) +``` + +## Units Converter (Optional) + +If you need to display localized strings, you can use a pattern like this (requires defining corresponding string resources in your project): + +```kotlin +data class ValueWithUnitsTemplate(val value: Double, val unitLabel: String) + +abstract class UnitsConverter { + abstract fun toDistanceUnits(meters: Meters): ValueWithUnitsTemplate + abstract fun toElevationUnits(meters: Meters): ValueWithUnitsTemplate +} + +object ImperialUnitsConverter : UnitsConverter() { + override fun toDistanceUnits(meters: Meters): ValueWithUnitsTemplate { + return if (meters < 0.25.miles) { + ValueWithUnitsTemplate(meters.toFeet, "ft") + } else { + ValueWithUnitsTemplate(meters.toMiles, "mi") + } + } + + override fun toElevationUnits(meters: Meters): ValueWithUnitsTemplate { + return ValueWithUnitsTemplate(meters.toFeet, "ft") + } +} + +object MetricUnitsConverter : UnitsConverter() { + override fun toDistanceUnits(meters: Meters): ValueWithUnitsTemplate { + return if (meters < 1000.meters) { + ValueWithUnitsTemplate(meters.toMeters, "m") + } else { + ValueWithUnitsTemplate(meters.toKilometers, "km") + } + } + + override fun toElevationUnits(meters: Meters): ValueWithUnitsTemplate { + return ValueWithUnitsTemplate(meters.toMeters, "m") + } +} +``` diff --git a/.gemini/skills/android-maps3d-sdk/references/utilities_java.md b/.gemini/skills/android-maps3d-sdk/references/utilities_java.md new file mode 100644 index 0000000..abe1ef4 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/utilities_java.md @@ -0,0 +1,226 @@ +# Camera Utilities (Java) + +These utility functions help ensure that camera parameters are within acceptable ranges for the Maps 3D SDK, preventing crashes due to invalid values. + +## Camera Validation + +Use these static methods to sanitize camera parameters before applying them to the map. + +```java +import com.google.android.gms.maps3d.model.Camera; +import com.google.android.gms.maps3d.model.LatLngAltitude; + +public class CameraUtils { + + public static final double DEFAULT_HEADING = 0.0; + public static final double DEFAULT_TILT = 60.0; + public static final double DEFAULT_RANGE = 1500.0; + public static final double DEFAULT_ROLL = 0.0; + + /** + * Validates a LatLngAltitude object, clamping values to valid ranges. + */ + public static LatLngAltitude toValidLocation(LatLngAltitude location) { + if (location == null) { + return new LatLngAltitude(0, 0, 0); + } + + double lat = Math.max(-90.0, Math.min(90.0, location.getLatitude())); + double lng = Math.max(-180.0, Math.min(180.0, location.getLongitude())); + double alt = Math.max(0.0, Math.min(LatLngAltitude.MAX_ALTITUDE_METERS, location.getAltitude())); + + return new LatLngAltitude(lat, lng, alt); + } + + /** + * Validates heading, wrapping values to [0, 360). + */ + public static double toHeading(Double heading) { + if (heading == null) return DEFAULT_HEADING; + return wrapIn(heading, 0.0, 360.0); + } + + /** + * Validates tilt, clamping values to [0, 90]. + */ + public static double toTilt(Double tilt) { + if (tilt == null) return DEFAULT_TILT; + return Math.max(0.0, Math.min(90.0, tilt)); + } + + /** + * Validates roll, wrapping values to [-360, 360]. + */ + public static double toRoll(Double roll) { + if (roll == null) return DEFAULT_ROLL; + return wrapIn(roll, -360.0, 360.0); + } + + /** + * Validates range, clamping values to [0, 63170000]. + */ + public static double toRange(Double range) { + if (range == null) return DEFAULT_RANGE; + return Math.max(0.0, Math.min(63170000.0, range)); + } + + /** + * Helper to wrap values within a range [lower, upper). + */ + public static double wrapIn(double value, double lower, double upper) { + double range = upper - lower; + if (range <= 0) { + throw new IllegalArgumentException("Upper bound must be greater than lower bound"); + } + double offset = value - lower; + return lower + (offset - Math.floor(offset / range) * range); + } +} +``` + +### Usage Example + +When updating the camera, use these methods to ensure values are valid: + +```java +LatLngAltitude validCenter = CameraUtils.toValidLocation(currentCenter); +double validHeading = CameraUtils.toHeading(currentHeading); +double validTilt = CameraUtils.toTilt(currentTilt); + +// Rebuild your camera object using these valid values... +``` + +## Path and Animation Utilities + +These utilities help with path smoothing, simplification, heading calculation, and distance calculations. + +```java +import com.google.android.gms.maps.model.LatLng; +import java.util.ArrayList; +import java.util.List; + +public class PathUtils { + + /** + * Smooths a path of LatLng points using Chaikin's algorithm. + */ + public static List smoothPath(List path, int iterations) { + if (path.size() < 3 || iterations <= 0) return path; + + List currentPath = path; + for (int iter = 0; iter < iterations; iter++) { + List nextPath = new ArrayList<>(); + nextPath.add(currentPath.get(0)); + + for (int i = 0; i < currentPath.size() - 1; i++) { + LatLng p0 = currentPath.get(i); + LatLng p1 = currentPath.get(i + 1); + + LatLng q = new LatLng( + p0.latitude * 0.75 + p1.latitude * 0.25, + p0.longitude * 0.75 + p1.longitude * 0.25 + ); + + LatLng r = new LatLng( + p0.latitude * 0.25 + p1.latitude * 0.75, + p0.longitude * 0.25 + p1.longitude * 0.75 + ); + + nextPath.add(q); + nextPath.add(r); + } + + nextPath.add(currentPath.get(currentPath.size() - 1)); + currentPath = nextPath; + } + + return currentPath; + } + + /** + * Calculates the heading (bearing) from one LatLng to another. + */ + public static double calculateHeading(LatLng from, LatLng to) { + double lat1 = Math.toRadians(from.latitude); + double lon1 = Math.toRadians(from.longitude); + double lat2 = Math.toRadians(to.latitude); + double lon2 = Math.toRadians(to.longitude); + + double dLon = lon2 - lon1; + double y = Math.sin(dLon) * Math.cos(lat2); + double x = Math.cos(lat1) * Math.sin(lat2) - + Math.sin(lat1) * Math.cos(lat2) * Math.cos(dLon); + + double bearing = Math.toDegrees(Math.atan2(y, x)); + return (bearing + 360.0) % 360.0; + } + + /** + * Simplifies a path of LatLng points using the Ramer-Douglas-Peucker algorithm. + */ + public static List simplifyPath(List path, double epsilon) { + if (path.size() < 3) return path; + + double maxDistance = 0.0; + int index = 0; + LatLng first = path.get(0); + LatLng last = path.get(path.size() - 1); + + for (int i = 1; i < path.size() - 1; i++) { + double distance = perpendicularDistance(path.get(i), first, last); + if (distance > maxDistance) { + index = i; + maxDistance = distance; + } + } + + if (maxDistance > epsilon) { + List left = simplifyPath(path.subList(0, index + 1), epsilon); + List right = simplifyPath(path.subList(index, path.size()), epsilon); + + List result = new ArrayList<>(left.subList(0, left.size() - 1)); + result.addAll(right); + return result; + } else { + List result = new ArrayList<>(); + result.add(first); + result.add(last); + return result; + } + } + + private static double perpendicularDistance(LatLng point, LatLng start, LatLng end) { + double x = point.longitude; + double y = point.latitude; + double x1 = start.longitude; + double y1 = start.latitude; + double x2 = end.longitude; + double y2 = end.latitude; + + double area = Math.abs((y2 - y1) * x - (x2 - x1) * y + x2 * y1 - y2 * x1); + double bottom = Math.sqrt(Math.pow(y2 - y1, 2.0) + Math.pow(x2 - x1, 2.0)); + return area / bottom; + } + + /** + * Calculates the distance in meters between two [LatLng] points using the Haversine formula. + */ + public static double haversineDistance(LatLng p1, LatLng p2) { + double r = 6371000.0; // Earth radius in meters + double lat1 = Math.toRadians(p1.latitude); + double lon1 = Math.toRadians(p1.longitude); + double lat2 = Math.toRadians(p2.latitude); + double lon2 = Math.toRadians(p2.longitude); + + double dLat = lat2 - lat1; + double dLon = lon2 - lon1; + + double a = Math.pow(Math.sin(dLat / 2), 2.0) + + Math.cos(lat1) * Math.cos(lat2) * Math.pow(Math.sin(dLon / 2), 2.0); + double c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + + return r * c; + } +} +``` + diff --git a/.gemini/skills/android-maps3d-sdk/references/utilities_kotlin.md b/.gemini/skills/android-maps3d-sdk/references/utilities_kotlin.md new file mode 100644 index 0000000..ff29d8e --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/utilities_kotlin.md @@ -0,0 +1,238 @@ +# Camera Utilities (Kotlin) + +These utility functions help ensure that camera parameters are within acceptable ranges for the Maps 3D SDK, preventing crashes due to invalid values. + +## Camera Validation + +Use `toValidCamera()` to sanitize a `Camera` object before applying it to the map. + +```kotlin +import com.google.android.gms.maps3d.model.Camera +import com.google.android.gms.maps3d.model.LatLngAltitude +import com.google.android.gms.maps3d.model.camera +import com.google.android.gms.maps3d.model.latLngAltitude +import kotlin.math.floor + +val headingRange = 0.0..360.0 +val tiltRange = 0.0..90.0 +val rangeRange = 0.0..63170000.0 +val rollRange = -360.0..360.0 + +val latitudeRange = -90.0..90.0 +val longitudeRange = -180.0..180.0 +val altitudeRange = 0.0..LatLngAltitude.MAX_ALTITUDE_METERS + +val ORIGIN = latLngAltitude { + latitude = 0.0 + longitude = 0.0 + altitude = 0.0 +} + +val DEFAULT_CAMERA: Camera = camera { + center = ORIGIN + heading = 0.0 + tilt = 0.0 + roll = 0.0 + range = 1000.0 +} + +/** + * Converts a nullable Camera object into a valid, non-null Camera object. + */ +fun Camera?.toValidCamera(): Camera { + val source = this ?: return DEFAULT_CAMERA + + return camera { + center = source.center.toValidLocation() + heading = source.heading.toHeading() + tilt = source.tilt.toTilt() + roll = source.roll.toRoll() + range = source.range.toRange() + } +} + +fun LatLngAltitude.toValidLocation(): LatLngAltitude { + val objectToCopy = this + return latLngAltitude { + latitude = objectToCopy.latitude.coerceIn(latitudeRange) + longitude = objectToCopy.longitude.coerceIn(longitudeRange) + altitude = objectToCopy.altitude.coerceIn(altitudeRange) + } +} + +fun Number?.toHeading(): Double = + this?.toDouble()?.wrapIn(headingRange.start, headingRange.endInclusive) ?: 0.0 + +fun Number?.toTilt(): Double = this?.toDouble()?.coerceIn(tiltRange) ?: 0.0 + +fun Number?.toRoll(): Double = this?.toDouble()?.wrapIn(rollRange) ?: 0.0 + +fun Number?.toRange(): Double = this?.toDouble()?.coerceIn(rangeRange) ?: 0.0 + +fun Double.wrapIn(lower: Double, upper: Double): Double { + val range = upper - lower + if (range <= 0) { + throw IllegalArgumentException("Upper bound must be greater than lower bound") + } + val offset = this - lower + return lower + (offset - floor(offset / range) * range) +} +``` + +## Path and Animation Utilities + +These utilities help with path smoothing, simplification, heading calculation, and distance calculations. + +```kotlin +import com.google.android.gms.maps.model.LatLng +import com.google.android.gms.maps3d.model.FlyAroundOptions +import com.google.android.gms.maps3d.model.FlyToOptions +import com.google.android.gms.maps3d.model.flyAroundOptions +import com.google.android.gms.maps3d.model.flyToOptions +import kotlin.math.abs +import kotlin.math.atan2 +import kotlin.math.cos +import kotlin.math.pow +import kotlin.math.sin +import kotlin.math.sqrt + +fun FlyAroundOptions.copy( + center: Camera? = null, + durationInMillis: Long? = null, + rounds: Double? = null, +) : FlyAroundOptions { + val objectToCopy = this + return flyAroundOptions { + this.center = (center ?: objectToCopy.center) + this.durationInMillis = durationInMillis ?: objectToCopy.durationInMillis + this.rounds = rounds ?: objectToCopy.rounds + } +} + +fun FlyToOptions.copy( + endCamera: Camera? = null, + durationInMillis: Long? = null, +) : FlyToOptions { + val objectToCopy = this + return flyToOptions { + this.endCamera = (endCamera ?: objectToCopy.endCamera) + this.durationInMillis = durationInMillis ?: objectToCopy.durationInMillis + } +} + +/** + * Smooths a path of LatLng points using Chaikin's algorithm. + */ +fun List.smoothPath(iterations: Int = 1): List { + if (size < 3 || iterations <= 0) return this + + var currentPath = this + repeat(iterations) { + val nextPath = mutableListOf() + nextPath.add(currentPath.first()) + + for (i in 0 until currentPath.size - 1) { + val p0 = currentPath[i] + val p1 = currentPath[i + 1] + + val q = LatLng( + p0.latitude * 0.75 + p1.latitude * 0.25, + p0.longitude * 0.75 + p1.longitude * 0.25 + ) + + val r = LatLng( + p0.latitude * 0.25 + p1.latitude * 0.75, + p0.longitude * 0.25 + p1.longitude * 0.75 + ) + + nextPath.add(q) + nextPath.add(r) + } + + nextPath.add(currentPath.last()) + currentPath = nextPath + } + + return currentPath +} + +/** + * Calculates the heading (bearing) from one LatLng to another. + */ +fun calculateHeading(from: LatLng, to: LatLng): Double { + val lat1 = Math.toRadians(from.latitude) + val lon1 = Math.toRadians(from.longitude) + val lat2 = Math.toRadians(to.latitude) + val lon2 = Math.toRadians(to.longitude) + + val dLon = lon2 - lon1 + val y = sin(dLon) * cos(lat2) + val x = cos(lat1) * sin(lat2) - + sin(lat1) * cos(lat2) * cos(dLon) + + val bearing = Math.toDegrees(atan2(y, x)) + return (bearing + 360.0) % 360.0 +} + +/** + * Simplifies a path of LatLng points using the Ramer-Douglas-Peucker algorithm. + */ +fun List.simplifyPath(epsilon: Double = 0.001): List { + if (size < 3) return this + + var maxDistance = 0.0 + var index = 0 + val first = first() + val last = last() + + for (i in 1 until size - 1) { + val distance = perpendicularDistance(this[i], first, last) + if (distance > maxDistance) { + index = i + maxDistance = distance + } + } + + return if (maxDistance > epsilon) { + val left = subList(0, index + 1).simplifyPath(epsilon) + val right = subList(index, size).simplifyPath(epsilon) + left.dropLast(1) + right + } else { + listOf(first, last) + } +} + +private fun perpendicularDistance(point: LatLng, start: LatLng, end: LatLng): Double { + val x = point.longitude + val y = point.latitude + val x1 = start.longitude + val y1 = start.latitude + val x2 = end.longitude + val y2 = end.latitude + + val area = abs((y2 - y1) * x - (x2 - x1) * y + x2 * y1 - y2 * x1) + val bottom = sqrt((y2 - y1).pow(2.0) + (x2 - x1).pow(2.0)) + return area / bottom +} + +/** + * Calculates the distance in meters between two [LatLng] points using the Haversine formula. + */ +fun haversineDistance(p1: LatLng, p2: LatLng): Double { + val r = 6371000.0 // Earth radius in meters + val lat1 = Math.toRadians(p1.latitude) + val lon1 = Math.toRadians(p1.longitude) + val lat2 = Math.toRadians(p2.latitude) + val lon2 = Math.toRadians(p2.longitude) + + val dLat = lat2 - lat1 + val dLon = lon2 - lon1 + + val a = sin(dLat / 2).pow(2.0) + + cos(lat1) * cos(lat2) * sin(dLon / 2).pow(2.0) + val c = 2 * atan2(sqrt(a), sqrt(1 - a)) + + return r * c +} +``` + From 2ef391effdcef91556b96f9a07250289c08ba421 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:07:27 -0600 Subject: [PATCH 04/11] docs: add documentation guide and common operations catalogs to skill --- .gemini/skills/android-maps3d-sdk/SKILL.md | 15 ++ .../references/catalog_compose.md | 192 ++++++++++++++++++ .../references/catalog_java.md | 134 ++++++++++++ .../references/catalog_kotlin_views.md | 178 ++++++++++++++++ .../references/documentation.md | 35 ++++ 5 files changed, 554 insertions(+) create mode 100644 .gemini/skills/android-maps3d-sdk/references/catalog_compose.md create mode 100644 .gemini/skills/android-maps3d-sdk/references/catalog_java.md create mode 100644 .gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md create mode 100644 .gemini/skills/android-maps3d-sdk/references/documentation.md diff --git a/.gemini/skills/android-maps3d-sdk/SKILL.md b/.gemini/skills/android-maps3d-sdk/SKILL.md index 76be23b..dc8fb04 100644 --- a/.gemini/skills/android-maps3d-sdk/SKILL.md +++ b/.gemini/skills/android-maps3d-sdk/SKILL.md @@ -7,6 +7,9 @@ description: Use this skill when the user wants to integrate the Google Maps 3D This skill guides you through integrating the Google Maps 3D SDK into an Android project. It follows the principles of progressive disclosure and relies on environment-specific templates. +> [!IMPORTANT] +> **Limited Training Data Alert**: The Google Maps 3D SDK is a new technology. Your internal parametric knowledge is likely outdated or incomplete. You **MUST NOT** guess API signatures. You **MUST** consult the local samples in `assets/samples/` and the external documentation listed in `references/documentation.md` before generating any code. + ## Prerequisites & Skill Alignment > [!NOTE] @@ -16,8 +19,15 @@ This skill guides you through integrating the Google Maps 3D SDK into an Android ## Procedural Workflow +### Step 0: Research Existing Patterns +Before asking the user for clarification or writing code, search the local workspace or repository for existing implementations of the requested feature. +1. Use your available search tools to look for keywords (e.g., "Polygon", "FlyTo") in the `snippets/` and `Maps3DSamples/` directories. +2. Consult `references/documentation.md` for a map of where to look. + + ### Step 1: Determine the Environment and Features You MUST ask the user to clarify their stack and needs: + 1. **Language**: Kotlin or Java? 2. **UI Framework**: Jetpack Compose or standard XML Views? 3. **Features**: Do they need automatic object management (cleanup)? @@ -113,6 +123,11 @@ Consult `references/best_practices.md` for detailed explanation of rules. Key ru 5. **Object Updates**: Use matching IDs to update Polygons/Polylines instead of removing and re-adding. 6. **Unit Conversions**: For type-safe measurements and conversions, see `references/units_kotlin.md` or `references/units_java.md` (Optional). 7. **Secrets Security**: NEVER add `secrets.properties` to version control. NEVER add real API keys to source code or `local.defaults.properties` (see `references/secrets_enforcement.md` for Gradle enforcement snippet). +8. **Common Operations**: Consult the language-specific catalog for short reference snippets (Marker, Polyline, Animation, etc.): + * Java: `references/catalog_java.md` + * Kotlin + Views: `references/catalog_kotlin_views.md` + * Jetpack Compose: `references/catalog_compose.md` + diff --git a/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md b/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md new file mode 100644 index 0000000..0d87e92 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md @@ -0,0 +1,192 @@ +# Common Operations Catalog (Jetpack Compose) + +This catalog provides reference snippets for common operations when using the Google Maps 3D SDK with Jetpack Compose (via `AndroidView` interoperability). + +## 1. Adding a Marker +Description: Markers point out points of interest. + +```kotlin +var marker by remember { mutableStateOf(null) } + +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + marker = map.addMarker(MarkerOptions() + .position(LatLngAltitude(37.7749, -122.4194, 0.0)) + .label("San Francisco")) + } + } + }, + update = { view -> + // Update marker properties if needed + } +) +``` + +## 2. Adding a Polyline +Description: Polylines are lines on the map. + +```kotlin +var polyline by remember { mutableStateOf(null) } + +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + polyline = map.addPolyline(PolylineOptions() + .add(LatLngAltitude(37.77, -122.41, 0.0)) + .add(LatLngAltitude(37.78, -122.42, 0.0)) + .color(Color.RED)) + } + } + }, + update = { view -> + // To update points: + polyline?.points = newPointsList + } +) +``` + +## 3. Camera Animation +Description: Animating the camera using the map instance. + +```kotlin +var map3D by remember { mutableStateOf(null) } + +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + map3D = map + } + } + } +) + +// Trigger animation in a side effect +LaunchedEffect(trigger) { + map3D?.flyTo(FlyToOptions() + .endCamera(targetCamera) + .durationInMillis(3000)) +} +``` + +## 4. Handling Map Click Events +Description: Listening for clicks on the map. + +```kotlin +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + map.addOnMapClickListener { location -> + onMapClick(location) + } + } + } + } +) +``` + +## 5. Object Animation (Moving Objects) +Description: Animating the position of an object using Compose animation states. + +```kotlin +var marker by remember { mutableStateOf(null) } +val moveTrigger by remember { mutableStateOf(false) } + +// Animate a fraction from 0 to 1 +val fraction by animateFloatAsState( + targetValue = if (moveTrigger) 1f else 0f, + animationSpec = tween(durationMillis = 1000) +) + +// Calculate intermediate position +val currentLat = startLat + (endLat - startLat) * fraction +val currentLng = startLng + (endLng - startLng) * fraction + +// Update marker position in the update block or side effect +AndroidView( + factory = { /* ... */ }, + update = { view -> + marker?.position = LatLngAltitude(currentLat, currentLng, 0.0) + } +) +``` + +## 6. Object Click Listeners +Description: Setting click listeners on objects within the `AndroidView` factory or update block. + +```kotlin +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + val marker = map.addMarker(...) + marker.setClickListener { + // Handle marker click + // NOTE: This is not on the UI thread! + } + } + } + } +) +``` + +> [!WARNING] +> The `setClickListener` callback does **NOT** execute on the UI thread. If you need to update Compose state or perform UI operations, you must switch to the main thread (e.g., using `withContext(Dispatchers.Main)` or updating a thread-safe state). + + +## 7. Stopping & Waiting for Camera Animations +Description: Controlling camera animations in Compose side effects. + +### Stopping Animations +```kotlin +LaunchedEffect(stopTrigger) { + if (stopTrigger) { + map3D?.stopCameraAnimation() + } +} +``` + +### Waiting for Completion +```kotlin +LaunchedEffect(trigger) { + map3D?.let { map -> + // Using the custom awaitFlyTo extension mentioned in Kotlin catalog + map.awaitFlyTo(FlyToOptions() + .endCamera(targetCamera) + .durationInMillis(3000)) + + // This runs AFTER animation completes + onAnimationComplete() + } +} +``` + +## 8. Waiting for Scene to Settle (Steady State) +Description: Hoisting the map steady state to Compose state. + +```kotlin +var isMapSteady by remember { mutableStateOf(false) } + +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + map.setOnMapSteadyListener { steady -> + isMapSteady = steady + } + } + } + } +) + +// React to steady state +if (isMapSteady) { + // Scene is fully rendered +} +``` + + diff --git a/.gemini/skills/android-maps3d-sdk/references/catalog_java.md b/.gemini/skills/android-maps3d-sdk/references/catalog_java.md new file mode 100644 index 0000000..9481e29 --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/catalog_java.md @@ -0,0 +1,134 @@ +# Common Operations Catalog (Java) + +This catalog provides short, reference snippets for common operations when using the Google Maps 3D SDK with Java and XML Views. + +## 1. Adding a Marker +Description: Markers point out points of interest. You can set position, altitude mode, and labels. + +```java +Marker marker = map.addMarker(new MarkerOptions() + .position(new LatLngAltitude(37.7749, -122.4194, 0.0)) + .altitudeMode(AltitudeMode.RELATIVE_TO_GROUND) + .label("San Francisco")); +``` + +## 2. Adding a Polyline +Description: Polylines are lines on the map. Use matching IDs to update them instead of re-adding. + +```java +Polyline polyline = map.addPolyline(new PolylineOptions() + .add(new LatLngAltitude(37.77, -122.41, 0.0)) + .add(new LatLngAltitude(37.78, -122.42, 0.0)) + .color(Color.RED) + .width(5f)); + +// To update: +polyline.setPoints(newPointsList); +``` + +## 3. Adding a Polygon +Description: Polygons represent areas. + +```java +Polygon polygon = map.addPolygon(new PolygonOptions() + .add(new LatLngAltitude(37.77, -122.41, 0.0)) + .add(new LatLngAltitude(37.78, -122.41, 0.0)) + .add(new LatLngAltitude(37.78, -122.42, 0.0)) + .fillColor(Color.argb(128, 255, 0, 0))); +``` + +## 4. Camera Animation +Description: Animating the camera to a new position or flying around a center. + +```java +// Fly To +map.flyTo(new FlyToOptions() + .endCamera(targetCamera) + .durationInMillis(3000)); + +// Fly Around +map.flyAround(new FlyAroundOptions() + .center(currentCamera) + .durationInMillis(5000) + .rounds(1.0)); +``` + +## 5. Handling Map Click Events +Description: Listening for clicks on the map. + +```java +map.addOnMapClickListener(latLngAltitude -> { + // Handle click at location + double lat = latLngAltitude.getLatitude(); + double lng = latLngAltitude.getLongitude(); +}); +``` + +## 6. Object Animation (Moving Objects) +Description: Animating the position of an object (like a Marker or Polygon) using Android `ValueAnimator`. + +```java +ValueAnimator animator = ValueAnimator.ofFloat(0f, 1f); +animator.setDuration(1000); +animator.addUpdateListener(animation -> { + float fraction = (float) animation.getAnimatedValue(); + double newLat = startLat + (endLat - startLat) * fraction; + double newLng = startLng + (endLng - startLng) * fraction; + marker.setPosition(new LatLngAltitude(newLat, newLng, 0.0)); +}); +animator.start(); +``` + +## 7. Object Click Listeners +Description: Unlike the 2D SDK, click listeners are set directly on the object instances (Marker, Polyline, Polygon, Model) rather than on the map. + +```java +// Marker +marker.setClickListener(() -> { + // Handle marker click + // NOTE: This is not on the UI thread! +}); + +// Polyline +polyline.setClickListener(() -> { + // Handle polyline click + // NOTE: This is not on the UI thread! +}); +``` + +> [!WARNING] +> The `setClickListener` callback does **NOT** execute on the UI thread. If you need to update views or perform UI operations, you must switch to the main thread (e.g., using `Handler(Looper.getMainLooper()).post(...)` or `runOnUiThread(...)` if in an Activity). + + +## 8. Stopping & Waiting for Camera Animations +Description: Controlling camera animations and waiting for their completion. + +### Stopping Animations +```java +// Halts any in-progress camera movement +map.stopCameraAnimation(); + +// Clear the listener as well if needed +map.setCameraAnimationEndListener(null); +``` + +### Waiting for Completion (Callback) +```java +map.setCameraAnimationEndListener(() -> { + // Trigger next action after animation ends +}); +map.flyTo(...); +``` + +## 9. Waiting for Scene to Settle (Steady State) +Description: The 3D SDK can notify you when the scene has fully rendered (terrain and mesh data loaded). This is crucial for high-fidelity snapshots or visual synchronization. + +```java +map.setOnMapSteadyListener(isSteady -> { + if (isSteady) { + // Scene is settled and fully rendered + map.setOnMapSteadyListener(null); // Clear if one-shot + } +}); +``` + diff --git a/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md b/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md new file mode 100644 index 0000000..d602bdd --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md @@ -0,0 +1,178 @@ +# Common Operations Catalog (Kotlin + Views) + +This catalog provides short, reference snippets for common operations when using the Google Maps 3D SDK with Kotlin and XML Views. + +## 1. Adding a Marker +Description: Markers point out points of interest. You can set position, altitude mode, and labels. + +```kotlin +val marker = map.addMarker(MarkerOptions() + .position(LatLngAltitude(37.7749, -122.4194, 0.0)) + .altitudeMode(AltitudeMode.RELATIVE_TO_GROUND) + .label("San Francisco")) +``` + +## 2. Adding a Polyline +Description: Polylines are lines on the map. Use matching IDs to update them instead of re-adding. + +```kotlin +val polyline = map.addPolyline(PolylineOptions() + .add(LatLngAltitude(37.77, -122.41, 0.0)) + .add(LatLngAltitude(37.78, -122.42, 0.0)) + .color(Color.RED) + .width(5f)) + +// To update: +polyline.points = newPointsList +``` + +## 3. Adding a Polygon +Description: Polygons represent areas. + +```kotlin +val polygon = map.addPolygon(PolygonOptions() + .add(LatLngAltitude(37.77, -122.41, 0.0)) + .add(LatLngAltitude(37.78, -122.41, 0.0)) + .add(LatLngAltitude(37.78, -122.42, 0.0)) + .fillColor(Color.argb(128, 255, 0, 0))) +``` + +## 4. Camera Animation +Description: Animating the camera to a new position or flying around a center. + +```kotlin +// Fly To +map.flyTo(FlyToOptions() + .endCamera(targetCamera) + .durationInMillis(3000)) + +// Fly Around +map.flyAround(FlyAroundOptions() + .center(currentCamera) + .durationInMillis(5000) + .rounds(1.0)) +``` + +## 5. Handling Map Click Events +Description: Listening for clicks on the map. + +```kotlin +map.addOnMapClickListener { latLngAltitude -> + // Handle click at location + val lat = latLngAltitude.latitude + val lng = latLngAltitude.longitude +} +``` + +## 6. Object Animation (Moving Objects) +Description: Animating the position of an object (like a Marker or Polygon) using Android `ValueAnimator` or Coroutines. + +### Using ValueAnimator +```kotlin +val animator = ValueAnimator.ofFloat(0f, 1f) +animator.duration = 1000 +animator.addUpdateListener { animation -> + val fraction = animation.animatedValue as Float + val newLat = startLat + (endLat - startLat) * fraction + val newLng = startLng + (endLng - startLng) * fraction + marker.position = LatLngAltitude(newLat, newLng, 0.0) +} +animator.start() +``` + +### Using Coroutines (Custom) +```kotlin +suspend fun animateMarker(marker: Marker, start: LatLng, end: LatLng, duration: Long = 1000) { + val steps = duration / 20 + val stepLat = (end.latitude - start.latitude) / steps + val stepLng = (end.longitude - start.longitude) / steps + var currentLat = start.latitude + var currentLng = start.longitude + + for (i in 0 until steps) { + currentLat += stepLat + currentLng += stepLng + marker.position = LatLngAltitude(currentLat, currentLng, 0.0) + delay(20) + } + marker.position = LatLngAltitude(end.latitude, end.longitude, 0.0) +} +## 9. Waiting for Scene to Settle (Steady State) +Description: The 3D SDK can notify you when the scene has fully rendered (terrain and mesh data loaded). This is crucial for high-fidelity snapshots or visual synchronization. + +### Callback Pattern +```kotlin +map.setOnMapSteadyListener { isSteady -> + if (isSteady) { + // Scene is settled and fully rendered + map.setOnMapSteadyListener(null) // Clear if one-shot + } +} +``` + +### Coroutine Pattern +```kotlin +suspend fun GoogleMap3D.awaitMapSteady(timeout: Duration): Boolean = suspendCancellableCoroutine { cont -> + setOnMapSteadyListener { isSteady -> + if (isSteady) { + setOnMapSteadyListener(null) + cont.resume(true) + } + } + // Add timeout logic if needed (see full samples) +} +``` + +## 7. Object Click Listeners +Description: Unlike the 2D SDK, click listeners are set directly on the object instances (Marker, Polyline, Polygon, Model) rather than on the map. + +```kotlin +// Marker +marker.setClickListener { + // Handle marker click + // NOTE: This is not on the UI thread! +} + +// Polyline +polyline.setClickListener { + // Handle polyline click + // NOTE: This is not on the UI thread! +} +``` + +> [!WARNING] +> The `setClickListener` callback does **NOT** execute on the UI thread. If you need to update views or perform UI operations, you must switch to the main thread (e.g., using `withContext(Dispatchers.Main)` or `Handler(Looper.getMainLooper()).post(...)`). + + +## 8. Stopping & Waiting for Camera Animations +Description: Controlling camera animations and waiting for their completion. + +### Stopping Animations +```kotlin +// Halts any in-progress camera movement +map.stopCameraAnimation() + +// Clear the listener as well if needed +map.setCameraAnimationEndListener(null) +``` + +### Waiting for Completion (Callback) +```kotlin +map.setCameraAnimationEndListener { + // Trigger next action after animation ends +} +map.flyTo(...) +``` + +### Waiting for Completion (Coroutine) +```kotlin +// Using a custom suspend function wrapper (common pattern) +suspend fun GoogleMap3D.awaitFlyTo(options: FlyToOptions) = suspendCancellableCoroutine { cont -> + setCameraAnimationEndListener { + setCameraAnimationEndListener(null) + cont.resume(Unit) + } + flyTo(options) +} +``` + diff --git a/.gemini/skills/android-maps3d-sdk/references/documentation.md b/.gemini/skills/android-maps3d-sdk/references/documentation.md new file mode 100644 index 0000000..b63739d --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/documentation.md @@ -0,0 +1,35 @@ +# Google Maps 3D SDK Documentation & Resources + +This file guides you to external and internal resources for the Google Maps 3D SDK. Because this SDK is new, these resources are your primary source of truth. + +## External Documentation +* **Official Reference**: [Google Maps Platform 3D SDK for Android](https://developers.google.com/maps/documentation/android-3d-sdk) +* **API Reference**: Refer to the Javadoc/KDoc links provided on the developer site for specific class signatures. + +## Resources & Samples + +Depending on whether you are working inside the `android-maps3d-samples` repository or in a standalone project, use the appropriate source below. + +### 1. Local Workspace (If working inside the sample repo) +If you are working directly in the `android-maps3d-samples` repository, use these local directories: +* **Snippets**: `./snippets/` +* **Sample Apps**: `./Maps3DSamples/` (Contains advanced usage and Compose integration examples). + + +### 2. GitHub Repository (If working in a separate project) +If you are working in a new or separate project, you can read the reference implementations directly from GitHub using your tools (e.g., `read_url_content`): +* **Main Repository**: `https://github.com/googlemaps-samples/android-maps3d-samples` +* **Kotlin Snippets**: `https://github.com/googlemaps-samples/android-maps3d-samples/tree/main/snippets/kotlin-app` +* **Java Snippets**: `https://github.com/googlemaps-samples/android-maps3d-samples/tree/main/snippets/java-app` +* **Advanced Samples**: `https://github.com/googlemaps-samples/android-maps3d-samples/tree/main/Maps3DSamples` (Excellent reference for Jetpack Compose integration). + + + +## Search Strategies for Agents +When asked to implement a feature, do not assume the API. Use your search tools to find examples in the local workspace or repository first. + +### Example Searches: +* To find how to add a Marker: Search for `"Marker"` in `snippets/` or `Maps3DSamples/`. +* To find camera animation examples: Search for `"animateCamera"` or `"FlyTo"`. +* To find polyline usage: Search for `"Polyline"`. + From f36a86651d7d36bb24abc8efeb7260d31924eda7 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:13:17 -0600 Subject: [PATCH 05/11] docs: add models, popovers, and extrusions to catalogs --- .../references/catalog_compose.md | 56 +++++++++++++++++++ .../references/catalog_java.md | 44 +++++++++++++++ .../references/catalog_kotlin_views.md | 50 +++++++++++++++++ 3 files changed, 150 insertions(+) diff --git a/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md b/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md index 0d87e92..444d1a5 100644 --- a/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md +++ b/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md @@ -189,4 +189,60 @@ if (isMapSteady) { } ``` +## 9. Adding a 3D Model +Description: Adding a model within `AndroidView`. + +```kotlin +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + map.addModel(ModelOptions() + .position(LatLngAltitude(37.7749, -122.4194, 0.0)) + .url("https://.../model.glb")) + } + } + } +) +``` + +## 10. Adding a Popover (Info Window) +Description: Adding a popover within `AndroidView`. + +```kotlin +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + val textView = TextView(context).apply { text = "Hello" } + map.addPopover(PopoverOptions() + .positionAnchor(LatLngAltitude(37.7749, -122.4194, 10.0)) + .content(textView)) + } + } + } +) +``` + +## 11. Extruded Polygons (3D Volumes) +Description: Adding extruded polygons within `AndroidView`. + +```kotlin +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + val faces = extrude(basePoints, 35.0) + faces.forEach { face -> + map.addPolygon(PolygonOptions() + .addAll(face) + .fillColor(Color.argb(128, 255, 215, 0))) + } + } + } + } +) +``` + + diff --git a/.gemini/skills/android-maps3d-sdk/references/catalog_java.md b/.gemini/skills/android-maps3d-sdk/references/catalog_java.md index 9481e29..2b0e61f 100644 --- a/.gemini/skills/android-maps3d-sdk/references/catalog_java.md +++ b/.gemini/skills/android-maps3d-sdk/references/catalog_java.md @@ -132,3 +132,47 @@ map.setOnMapSteadyListener(isSteady -> { }); ``` +## 10. Adding a 3D Model +Description: Loading and placing glTF assets on the map. + +```java +Model model = map.addModel(new ModelOptions() + .position(new LatLngAltitude(37.7749, -122.4194, 0.0)) + .url("https://storage.googleapis.com/gmp-maps-demos/p3d-map/assets/Airplane.glb") + .scale(new Vector3D(1.0, 1.0, 1.0)) + .altitudeMode(AltitudeMode.RELATIVE_TO_GROUND)); +``` + +## 11. Adding a Popover (Info Window) +Description: 2D views that stick to a 3D location and always face the camera. + +```java +TextView textView = new TextView(context); +textView.setText("Hello World"); +textView.setBackgroundColor(Color.WHITE); + +Popover popover = map.addPopover(new PopoverOptions() + .positionAnchor(new LatLngAltitude(37.7749, -122.4194, 10.0)) + .content(textView) + .altitudeMode(AltitudeMode.RELATIVE_TO_MESH) + .autoCloseEnabled(true)); + +popover.show(); +``` + +## 12. Extruded Polygons (3D Volumes) +Description: Turning flat footprints into 3D volumes by duplicating vertices at height and stitching sides. + +```java +// Helper to extrude (see full Codelab for complete implementation) +List> faces = extrude(basePoints, 35.0); + +for (List face : faces) { + map.addPolygon(new PolygonOptions() + .addAll(face) + .fillColor(Color.argb(128, 255, 215, 0)) + .altitudeMode(AltitudeMode.ABSOLUTE)); +} +``` + + diff --git a/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md b/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md index d602bdd..d7e930f 100644 --- a/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md +++ b/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md @@ -176,3 +176,53 @@ suspend fun GoogleMap3D.awaitFlyTo(options: FlyToOptions) = suspendCancellableCo } ``` +## 10. Adding a 3D Model +Description: Loading and placing glTF assets on the map. + +```kotlin +val model = map.addModel(ModelOptions() + .position(LatLngAltitude(37.7749, -122.4194, 0.0)) + .url("https://storage.googleapis.com/gmp-maps-demos/p3d-map/assets/Airplane.glb") + .scale(Vector3D(1.0, 1.0, 1.0)) + .altitudeMode(AltitudeMode.RELATIVE_TO_GROUND)) +``` + +## 11. Adding a Popover (Info Window) +Description: 2D views that stick to a 3D location and always face the camera. + +```kotlin +val textView = TextView(context).apply { + text = "Hello World" + setBackgroundColor(Color.WHITE) +} + +val popover = map.addPopover(PopoverOptions() + .positionAnchor(LatLngAltitude(37.7749, -122.4194, 10.0)) + .content(textView) + .altitudeMode(AltitudeMode.RELATIVE_TO_MESH) + .autoCloseEnabled(true)) + +popover.show() +``` + +## 12. Extruded Polygons (3D Volumes) +Description: Turning flat footprints into 3D volumes by duplicating vertices at height and stitching sides. + +```kotlin +// Helper to extrude (see full Codelab for complete implementation) +fun extrude(basePoints: List, height: Double): List> { + // Implementation creates top points and side walls... + return faces +} + +// Adding extruded faces to map +val faces = extrude(basePoints, 35.0) +faces.forEach { face -> + map.addPolygon(PolygonOptions() + .addAll(face) + .fillColor(Color.argb(128, 255, 215, 0)) + .altitudeMode(AltitudeMode.ABSOLUTE)) +} +``` + + From d8901ef51de7eaec7f441981e26e022589a1f1e1 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:14:00 -0600 Subject: [PATCH 06/11] docs: link android security skill in SKILL.md --- .gemini/skills/android-maps3d-sdk/SKILL.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.gemini/skills/android-maps3d-sdk/SKILL.md b/.gemini/skills/android-maps3d-sdk/SKILL.md index dc8fb04..ed668de 100644 --- a/.gemini/skills/android-maps3d-sdk/SKILL.md +++ b/.gemini/skills/android-maps3d-sdk/SKILL.md @@ -15,7 +15,8 @@ This skill guides you through integrating the Google Maps 3D SDK into an Android > [!NOTE] > This skill should be used in conjunction with: > - **Android Architecture Skill**: For proper MVVM/MVI layering. -> - **Android Security Skill**: For API key protection and permissions. +> - [Android Security Skill](https://github.com/kikoso/android-skills/blob/main/android-security-skill/SKILL.md): For API key protection and permissions. + ## Procedural Workflow From 01cdf6cf5edf68e7d6fbe2e6399e4c4b4e2fa14e Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:31:39 -0600 Subject: [PATCH 07/11] docs: level up skill with scenarios, testing, and stack detective --- .gemini/skills/android-maps3d-sdk/SKILL.md | 34 ++- .../references/catalog_compose.md | 117 +++++++++ .../references/catalog_java.md | 19 ++ .../references/catalog_kotlin_views.md | 35 +++ .../references/scenarios.md | 242 ++++++++++++++++++ 5 files changed, 437 insertions(+), 10 deletions(-) create mode 100644 .gemini/skills/android-maps3d-sdk/references/scenarios.md diff --git a/.gemini/skills/android-maps3d-sdk/SKILL.md b/.gemini/skills/android-maps3d-sdk/SKILL.md index ed668de..f0d3761 100644 --- a/.gemini/skills/android-maps3d-sdk/SKILL.md +++ b/.gemini/skills/android-maps3d-sdk/SKILL.md @@ -1,8 +1,14 @@ --- name: android-maps3d-sdk -description: Use this skill when the user wants to integrate the Google Maps 3D SDK into an Android application. This skill provides procedural guidance for setup, lifecycle, and 3D object manipulation. Do NOT use for standard 2D maps. +description: Integrates the Google Maps 3D SDK into an Android application. Provides procedural guidance for setup, lifecycle management, and 3D object manipulation (Markers, Polylines, Models, Popovers). Use when the user wants to build immersive 3D map experiences or migrate from 2D maps. +license: Apache-2.0 +compatibility: Requires Android project, internet access, and at least version 0.2.0 of play-services-maps3d. +metadata: + version: "1.1" + author: Google Maps Samples --- + # Android Maps 3D SDK Integration This skill guides you through integrating the Google Maps 3D SDK into an Android project. It follows the principles of progressive disclosure and relies on environment-specific templates. @@ -26,14 +32,16 @@ Before asking the user for clarification or writing code, search the local works 2. Consult `references/documentation.md` for a map of where to look. -### Step 1: Determine the Environment and Features -You MUST ask the user to clarify their stack and needs: +### Step 1: Determine the Environment and Features (Stack Detective) +You MUST proactively discover the environment by inspecting the codebase before writing any code. Do NOT ask the user unless the environment is highly ambiguous. -1. **Language**: Kotlin or Java? -2. **UI Framework**: Jetpack Compose or standard XML Views? -3. **Features**: Do they need automatic object management (cleanup)? +1. **Run Search/Grep**: + * Search for `androidx.compose` or `compose-compiler` in `build.gradle` or `libs.versions.toml` to detect **Jetpack Compose**. + * Search for `com.android.application` or `com.android.library` to understand the module type. + * Look for `.kt` vs `.java` files to determine the dominant **Language**. +2. **Identify Features**: Determine if the user needs automatic object management (cleanup) or specific 3D features based on their request. -Based on their response, follow the **Selection Logic** to retrieve boilerplate from `assets/samples/` and consult rules in `references/`. +Based on your discovery, follow the **Selection Logic** to retrieve boilerplate from `assets/samples/` and consult rules in `references/`. ### Implementation Guidance (Selection Logic) 1. Identify the user's stack: (Language: Kotlin/Java, UI: Compose/Views). @@ -45,12 +53,16 @@ Based on their response, follow the **Selection Logic** to retrieve boilerplate ### Step 2: Base Setup Regardless of the environment, the following setup is required. -#### 1. Dependencies +#### 1. Dependencies (Dynamic Version Resolution) +Before adding the dependency, you MUST identify the latest version. +1. Run `./gradlew :app:dependencies | grep maps3d` to check if a version is already resolved. +2. Or search the Google Maven repository or use available tools to find the latest version (must be **at least** `0.2.0`). + Add the necessary versions and libraries to your `libs.versions.toml` file: ```toml [versions] -# NOTE: Verify this is the latest version of the Maps 3D SDK +# NOTE: Verify this is the latest version of the Maps 3D SDK (must be at least "0.2.0") playServicesMaps3d = "0.2.0" lifecycleRuntimeKtx = "2.8.5" @@ -107,7 +119,7 @@ After determining the stack, load the corresponding files from `assets/samples/` - Layout: `assets/samples/views_kotlin/activity_main.xml` - Activity: `assets/samples/views_kotlin/MapActivity.kt.txt` - Snippet (Object Manager): `assets/samples/views_kotlin/snippets/object_manager_usage.kt.txt` -- Kotlin + Compose: (To be created in `assets/samples/compose/`) +- Kotlin + Compose: Refer to `references/catalog_compose.md` for the `Map3DContainer` wrapper and Compose patterns. - Java: - Layout: `assets/samples/views_java/activity_main.xml` - Activity: `assets/samples/views_java/MapActivity.java.txt` @@ -128,6 +140,8 @@ Consult `references/best_practices.md` for detailed explanation of rules. Key ru * Java: `references/catalog_java.md` * Kotlin + Views: `references/catalog_kotlin_views.md` * Jetpack Compose: `references/catalog_compose.md` +9. **Scenario Storyboards**: For complex, multi-step workflows (e.g., Immersive Arrival), consult `references/scenarios.md`. + diff --git a/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md b/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md index 444d1a5..0b09c00 100644 --- a/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md +++ b/.gemini/skills/android-maps3d-sdk/references/catalog_compose.md @@ -2,7 +2,42 @@ This catalog provides reference snippets for common operations when using the Google Maps 3D SDK with Jetpack Compose (via `AndroidView` interoperability). +## 0. The Reusable `Map3DContainer` Pattern +Description: The recommended way to use `Map3DView` in Compose is to create a reusable wrapper Composable that handles lifecycle and map state. + +```kotlin +@Composable +fun Map3DContainer( + modifier: Modifier = Modifier, + options: Map3DOptions, + onMapReady: (GoogleMap3D) -> Unit +) { + AndroidView( + modifier = modifier, + factory = { context -> + Map3DView(context, options).apply { + onCreate(null) // Handle lifecycle manually if needed + } + }, + update = { view -> + view.getMap3DViewAsync(object : OnMap3DViewReadyCallback { + override fun onMap3DViewReady(map: GoogleMap3D) { + onMapReady(map) + } + override fun onError(e: Exception) { + // Handle error + } + }) + }, + onRelease = { view -> + view.onDestroy() + } + ) +} +``` + ## 1. Adding a Marker + Description: Markers point out points of interest. ```kotlin @@ -244,5 +279,87 @@ AndroidView( ) ``` +## 12. ViewModel Integration +Description: The advanced sample demonstrates hoisting the `GoogleMap3D` instance to a `ViewModel` to manage state and handle actions outside the UI tree. + +```kotlin +class MapViewModel : ViewModel() { + private val _googleMap3D = MutableStateFlow(null) + val googleMap3D: StateFlow = _googleMap3D.asStateFlow() + + private val _isMapSteady = MutableStateFlow(false) + val isMapSteady: StateFlow = _isMapSteady.asStateFlow() + + fun setGoogleMap3D(map: GoogleMap3D) { + _googleMap3D.value = map + } + + fun onMapSteadyChange(isSteady: Boolean) { + _isMapSteady.value = isSteady + } + + fun releaseGoogleMap3D() { + _googleMap3D.value = null + } +} +``` + +In your Composable: +```kotlin +val viewModel: MapViewModel = viewModel() +val map3D by viewModel.googleMap3D.collectAsState() + +ThreeDMap( + options = mapOptions, + onMapReady = { map -> + viewModel.setGoogleMap3D(map) + map.setOnMapSteadyListener { isSteady -> + viewModel.onMapSteadyChange(isSteady) + } + } +) +``` + +## 13. Camera State Logger (Debug Helper) +Description: A utility to log the current camera state to Logcat on every movement, helping you design 3D views. + +```kotlin +AndroidView( + factory = { context -> + Map3DView(context).apply { + getMapAsync { map -> + map.addOnCameraMoveListener { + val camera = map.camera + Log.d("CameraLogger", "Lat: ${camera.center.latitude}, Lng: ${camera.center.longitude}, Alt: ${camera.center.altitude}, Heading: ${camera.heading}, Tilt: ${camera.tilt}, Range: ${camera.range}") + } + } + } + } +) +``` + +## 14. 3D View Validation (Testing) +Description: Verifying that the `Map3DView` is visible using `ComposeTestRule`. + +```kotlin +@Test +fun testMapVisible() { + composeTestRule.setContent { + Map3DContainer( + modifier = Modifier.testTag("map3d_container"), + options = Map3DOptions(), + onMapReady = {} + ) + } + + // Verify AndroidView hosting Map3DView is displayed + composeTestRule.onNodeWithTag("map3d_container") + .assertIsDisplayed() +} +``` + + + + diff --git a/.gemini/skills/android-maps3d-sdk/references/catalog_java.md b/.gemini/skills/android-maps3d-sdk/references/catalog_java.md index 2b0e61f..b0f8efa 100644 --- a/.gemini/skills/android-maps3d-sdk/references/catalog_java.md +++ b/.gemini/skills/android-maps3d-sdk/references/catalog_java.md @@ -175,4 +175,23 @@ for (List face : faces) { } ``` +## 13. Camera State Logger (Debug Helper) +Description: A utility to log the current camera state to Logcat on every movement, helping you design 3D views. + +```java +map.addOnCameraMoveListener(() -> { + Camera camera = map.getCamera(); + Log.d("CameraLogger", String.format( + "Camera State:\nLat: %f\nLng: %f\nAlt: %f\nHeading: %f\nTilt: %f\nRange: %f", + camera.getCenter().getLatitude(), + camera.getCenter().getLongitude(), + camera.getCenter().getAltitude(), + camera.getHeading(), + camera.getTilt(), + camera.getRange() + )); +}); +``` + + diff --git a/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md b/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md index d7e930f..db80eb1 100644 --- a/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md +++ b/.gemini/skills/android-maps3d-sdk/references/catalog_kotlin_views.md @@ -225,4 +225,39 @@ faces.forEach { face -> } ``` +## 13. Camera State Logger (Debug Helper) +Description: A utility to log the current camera state to Logcat on every movement, helping you design 3D views. + +```kotlin +map.addOnCameraMoveListener { + val camera = map.camera + Log.d("CameraLogger", """ + Camera State: + Lat: ${camera.center.latitude} + Lng: ${camera.center.longitude} + Alt: ${camera.center.altitude} + Heading: ${camera.heading} + Tilt: ${camera.tilt} + Range: ${camera.range} + """.trimIndent()) +} +``` + +## 14. 3D View Validation (Testing) +Description: Verifying that the `Map3DView` is visible and loads correctly using Espresso. + +```kotlin +@Test +fun testMapVisible() { + // Launch Activity + ActivityScenario.launch(MapActivity::class.java) + + // Verify Map3DView is displayed + onView(withId(R.id.map3d_view)) + .check(matches(isDisplayed())) +} +``` + + + diff --git a/.gemini/skills/android-maps3d-sdk/references/scenarios.md b/.gemini/skills/android-maps3d-sdk/references/scenarios.md new file mode 100644 index 0000000..6401dae --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/references/scenarios.md @@ -0,0 +1,242 @@ +# Scenario Storyboard Catalog + +This catalog provides "orchestration recipes" for complex, multi-step workflows in the 3D SDK. Instead of atomic snippets, these scenarios demonstrate how to synchronize asynchronous events (camera motion, asset loading, rendering steady state) to create high-fidelity user experiences. + +--- + +## Scenario 1: The Immersive Arrival +Description: Transitioning from a high-altitude "Global View" to a specific building with high-detail 3D assets. This pattern handles the race condition between camera movement and mesh loading. + +### The Strategy (Orchestration Logic) +1. **Travel**: Execute a cinematic `flyTo` from the current camera to the target. +2. **Synchronization**: Use the **Double-Wait Pattern**. Wait for the camera animation to end and for the map steady-state (ensures the building mesh is loaded before placing assets). +3. **Enhancement**: Add a high-detail 3D model (glTF) at the location. +4. **Context**: Show a Popover with metadata. +5. **Engagement**: Start a slow `flyAround` to provide a 360-degree context. + +### The Storyboard Code (Kotlin + Coroutines) + +```kotlin +/** + * SCENARIO: The Immersive Arrival + * Orchestrates a high-to-low altitude transition with asset loading. + * + * Note: Assumes extensions like `awaitFlyTo` and `awaitMapSteady` are available + * (see catalogs for implementation). + */ +suspend fun GoogleMap3D.executeImmersiveArrival( + targetLocation: LatLngAltitude, + modelUrl: String, + title: String, + context: Context +) { + // 1. Travel: High-speed cinematic flight + val targetCamera = Camera.builder() + .center(targetLocation) + .range(500.0) // Close zoom + .tilt(45.0) + .heading(0.0) + .build() + + // Custom awaitFlyTo extension ensures we don't proceed until we arrive + awaitFlyTo(FlyToOptions.builder() + .endCamera(targetCamera) + .durationInMillis(4000) + .build()) + + // 2. Synchronization: Wait for the 3D mesh (buildings/terrain) to settle + // This prevents the "popping" of 3D models into empty space + awaitMapSteady(timeout = 5000) + + // 3. Enhancement: Place the hero asset + val model = addModel(ModelOptions.builder() + .position(targetLocation) + .url(modelUrl) + .altitudeMode(AltitudeMode.RELATIVE_TO_MESH) + .build()) + + // 4. Context: Show the info UI + val textView = TextView(context).apply { text = title } + val popover = addPopover(PopoverOptions.builder() + .positionAnchor(LatLngAltitude(targetLocation.latitude, targetLocation.longitude, targetLocation.altitude + 20.0)) + .content(textView) + .build()) + popover.show() + + // 5. Engagement: Subtle rotation to show off the 3D space + flyAround(FlyAroundOptions.builder() + .center(camera) // Rotate around current view + .durationInMillis(20000) + .rounds(0.5) + .build()) +} +``` + +--- + +## Scenario 2: The Flyover Tour +Description: A guided tour that visits multiple points of interest sequentially, waiting for the scene to settle at each stop before proceeding. + +### The Strategy +1. **Fly to Stop**: Move to the first POI. +2. **Wait**: Wait for camera and mesh to settle. +3. **Pause/Inspect**: Hold the view for a few seconds or trigger an action (e.g., show a marker). +4. **Repeat**: Move to the next POI. + +### The Storyboard Code (Kotlin + Coroutines) + +```kotlin +suspend fun GoogleMap3D.executeFlyoverTour( + stops: List, + onStopVisited: (Int) -> Unit +) { + stops.forEachIndexed { index, stop -> + // Fly to stop + val camera = Camera.builder().center(stop).range(1000.0).tilt(30.0).build() + awaitFlyTo(FlyToOptions.builder().endCamera(camera).durationInMillis(3000).build()) + + // Wait for mesh + awaitMapSteady(timeout = 5000) + + // Trigger callback (e.g., show UI or highlight asset) + onStopVisited(index) + + // Hold for inspection + delay(2000) + } +} +``` + +--- + +## Scenario 3: The Fragment Interop Overlay (Places UI Kit) +Description: Hosting a traditional Android Fragment (like the Places UI Kit `PlaceDetailsCompactFragment`) inside a Compose UI anchored to map events. This solves the problem of rapid recomposition causing fragment recreation. + +### The Strategy +1. **Single Instantiation**: Create the `FragmentContainerView` and transact the Fragment exactly ONCE in the `factory` block of `AndroidView`. +2. **Decoupled Updates**: Use `LaunchedEffect` keyed on the state (e.g., `placeId`) to update the existing fragment, avoiding full recreation. +3. **Activity Support**: Use the Activity's `supportFragmentManager` directly to avoid Hilt context casting issues (cast `LocalContext.current` to `FragmentActivity`). + +### The Storyboard Code (Kotlin + Compose) + +```kotlin +@Composable +fun PlaceDetailsOverlay( + placeId: String, + onDismiss: () -> Unit, + modifier: Modifier = Modifier +) { + val containerId = remember { View.generateViewId() } + val context = LocalContext.current + val supportFragmentManager = remember(context) { + (context as FragmentActivity).supportFragmentManager + } + + // Decoupled state observer: Updates existing fragment when placeId changes + LaunchedEffect(placeId) { + val fragment = supportFragmentManager.findFragmentById(containerId) as? PlaceDetailsCompactFragment + if (fragment != null) { + fragment.loadWithPlaceId(placeId) + } + } + + Box(modifier = modifier) { + AndroidView( + factory = { ctx -> + FragmentContainerView(ctx).apply { + id = containerId + + val newFragment = PlaceDetailsCompactFragment.newInstance( + PlaceDetailsCompactFragment.ALL_CONTENT, + Orientation.VERTICAL, + R.style.CustomizedPlaceDetailsTheme + ) + + supportFragmentManager.commit { + replace(containerId, newFragment) + } + + post { newFragment.loadWithPlaceId(placeId) } + } + }, + modifier = Modifier.fillMaxWidth() + ) + } + + // Clean up fragment when leaving composition + DisposableEffect(containerId) { + onDispose { + supportFragmentManager.findFragmentById(containerId)?.let { + supportFragmentManager.commit { remove(it) } + } + } + } +} +``` + +--- + +## Scenario 4: Continuous Route Tracking (Drone View) +Description: Simulating a smooth, frame-driven flight along a complex polyline route, updating camera and markers on each frame. This avoids the jerky movement of waypoint jumping. + +### The Strategy +1. **Headless Engine**: Use a `LaunchedEffect` with `withFrameMillis` to run a continuous physics loop. +2. **Interpolation**: Calculate the precise position along the route based on elapsed distance. +3. **Smooth Camera**: Use linear interpolation (lerp) and spherical linear interpolation (slerp) for camera position and heading to prevent jitter. +4. **Object Mutation**: Directly mutate existing object properties (e.g., `m.orientation = ...`) rather than recreating objects to maintain 60FPS. + +### The Storyboard Code (Kotlin + Compose) + +```kotlin +@Composable +fun RouteFlightEngine( + map3D: GoogleMap3D?, + path: List, + isPlaying: Boolean, + speedMps: Float +) { + val safeMap = map3D ?: return + val cumulativeDistances = remember(path) { calculateCumulativeDistances(path) } + val totalDistance = cumulativeDistances.last() + + var elapsedDistance by remember { mutableFloatStateOf(0f) } + var lastFrameTime by remember { mutableLongStateOf(0L) } + + LaunchedEffect(isPlaying, path) { + if (!isPlaying) return@LaunchedEffect + + while (isPlaying) { + withFrameMillis { frameTime -> + if (lastFrameTime == 0L) { + lastFrameTime = frameTime + return@withFrameMillis + } + val dtMs = frameTime - lastFrameTime + lastFrameTime = frameTime + + // Advance distance + elapsedDistance += (speedMps * (dtMs / 1000.0)).toFloat() + if (elapsedDistance >= totalDistance) elapsedDistance = totalDistance.toFloat() + + // Calculate interpolated position + val targetPos = getInterpolatedPoint(elapsedDistance.toDouble(), path, cumulativeDistances) + + // Update camera smoothly + val currentCamera = safeMap.camera + val newCamera = Camera.builder() + .center(targetPos) + .heading(currentCamera.heading) // Or calculate lookahead heading + .tilt(currentCamera.tilt) + .range(currentCamera.range) + .build() + + safeMap.setCamera(newCamera) + } + } + } +} +``` +> [!NOTE] +> This is a simplified extraction. Refer to the full sample in `Maps3DSamples/advanced/app/src/main/java/com/example/advancedmaps3dsamples/route/RouteSampleActivity.kt` for advanced details on `slerpHeading`, lookahead vectors, and the "Teleport to Null Island" pattern for hiding inactive models. + + From af36851f068d6f5b8304e49d86b3280386f147f4 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:40:28 -0600 Subject: [PATCH 08/11] docs: add double-wait utilities and clarify compatibility --- .gemini/skills/android-maps3d-sdk/SKILL.md | 2 +- .../references/utilities_java.md | 17 ++++++++++++ .../references/utilities_kotlin.md | 26 +++++++++++++++++++ 3 files changed, 44 insertions(+), 1 deletion(-) diff --git a/.gemini/skills/android-maps3d-sdk/SKILL.md b/.gemini/skills/android-maps3d-sdk/SKILL.md index f0d3761..4a6728a 100644 --- a/.gemini/skills/android-maps3d-sdk/SKILL.md +++ b/.gemini/skills/android-maps3d-sdk/SKILL.md @@ -2,7 +2,7 @@ name: android-maps3d-sdk description: Integrates the Google Maps 3D SDK into an Android application. Provides procedural guidance for setup, lifecycle management, and 3D object manipulation (Markers, Polylines, Models, Popovers). Use when the user wants to build immersive 3D map experiences or migrate from 2D maps. license: Apache-2.0 -compatibility: Requires Android project, internet access, and at least version 0.2.0 of play-services-maps3d. +compatibility: Requires an Android project and internet access. Uses play-services-maps3d version 0.2.0 or higher. metadata: version: "1.1" author: Google Maps Samples diff --git a/.gemini/skills/android-maps3d-sdk/references/utilities_java.md b/.gemini/skills/android-maps3d-sdk/references/utilities_java.md index abe1ef4..a37be4c 100644 --- a/.gemini/skills/android-maps3d-sdk/references/utilities_java.md +++ b/.gemini/skills/android-maps3d-sdk/references/utilities_java.md @@ -221,6 +221,23 @@ public class PathUtils { return r * c; } + + /** + * Standardized "Double-Wait" utility for Java. + * Returns a CompletableFuture that completes when the map becomes steady. + * Use this after starting a camera animation to ensure the scene is fully loaded. + */ + public static java.util.concurrent.CompletableFuture awaitArrivedAndSteady(com.google.android.gms.maps3d.GoogleMap3D map) { + java.util.concurrent.CompletableFuture future = new java.util.concurrent.CompletableFuture<>(); + map.setOnMapSteadyListener(isSteady -> { + if (isSteady) { + map.setOnMapSteadyListener(null); + future.complete(true); + } + }); + return future; + } } + ``` diff --git a/.gemini/skills/android-maps3d-sdk/references/utilities_kotlin.md b/.gemini/skills/android-maps3d-sdk/references/utilities_kotlin.md index ff29d8e..ebd3cb0 100644 --- a/.gemini/skills/android-maps3d-sdk/references/utilities_kotlin.md +++ b/.gemini/skills/android-maps3d-sdk/references/utilities_kotlin.md @@ -236,3 +236,29 @@ fun haversineDistance(p1: LatLng, p2: LatLng): Double { } ``` +## Synchronization Utilities + +### Double-Wait Utility + +Use `awaitArrivedAndSteady()` to ensure the camera has arrived and the 3D scene has fully rendered before proceeding. + +```kotlin +import com.google.android.gms.maps3d.GoogleMap3D +import kotlinx.coroutines.suspendCancellableCoroutine +import kotlin.coroutines.resume + +/** + * Standardized "Double-Wait" utility. + * Waits for the map to become steady (rendering complete and camera idle). + * Use this after starting a camera animation to ensure the scene is fully loaded. + */ +suspend fun GoogleMap3D.awaitArrivedAndSteady(timeoutMs: Long = 5000): Boolean = suspendCancellableCoroutine { cont -> + setOnMapSteadyListener { isSteady -> + if (isSteady) { + setOnMapSteadyListener(null) + cont.resume(true) + } + } +} +``` + From c6597127cd8f7cc1c298e2a641521a9e5ae87f80 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:46:02 -0600 Subject: [PATCH 09/11] docs: stop tracking skill-dev.md and add future work --- .../skills/android-maps3d-sdk/FUTURE_WORK.md | 24 ++ .gitignore | 4 + skill-dev.md | 215 ------------------ 3 files changed, 28 insertions(+), 215 deletions(-) create mode 100644 .gemini/skills/android-maps3d-sdk/FUTURE_WORK.md delete mode 100644 skill-dev.md diff --git a/.gemini/skills/android-maps3d-sdk/FUTURE_WORK.md b/.gemini/skills/android-maps3d-sdk/FUTURE_WORK.md new file mode 100644 index 0000000..c426bcc --- /dev/null +++ b/.gemini/skills/android-maps3d-sdk/FUTURE_WORK.md @@ -0,0 +1,24 @@ +# Future Work for Android Maps 3D SDK Skill + +This document tracks planned enhancements and evaluation strategies to further level up the skill. + +## 1. Adaptive Fidelity Patterns +To make the skill respect hardware constraints (battery, thermal), we plan to add an "Adaptive Fidelity" pattern. +* **Proposal**: Provide a `DefaultLifecycleObserver` that listens for `ACTION_POWER_SAVE_MODE_CHANGED` and throttles the camera `range` or disables heavy animations when battery saver is active. +* **Status**: Conceptualized. + +## 2. Behavioral & Visual Evaluation Strategies +To accurately evaluate an agent using this skill (moving beyond static text assertions), we plan to implement the following eval strategies: + +### A. Logcat State-Machine Verification +* **Concept**: Assert on the sequence of events emitted by the agent's code (e.g., using `CameraLogger`). +* **Example**: Verify that `CameraAnimationEnd` happens before `MapSteady` (if applicable) and before assets are loaded. + +### B. Automated Visual Testing +* **Concept**: Use emulator screenshots or video recordings to verify that 3D elements are rendered correctly and centered. + +### C. Synthesized Test Assertion +* **Concept**: Require the agent to generate a passing instrumentation test (using the Espresso or `ComposeTestRule` snippets in the catalogs) as part of the evaluation success criteria. + +### D. Walkthrough Artifact Grading +* **Concept**: Grade the agent on its ability to produce a `walkthrough.md` with an embedded video demonstrating the working feature. diff --git a/.gitignore b/.gitignore index 62c6c00..fd77aea 100644 --- a/.gitignore +++ b/.gitignore @@ -39,3 +39,7 @@ google-services.json # VS Code .vscode/ snippets/docs/ + +# Skill development scratchpad +skill-dev.md + diff --git a/skill-dev.md b/skill-dev.md deleted file mode 100644 index 7805c15..0000000 --- a/skill-dev.md +++ /dev/null @@ -1,215 +0,0 @@ -# **Architectural Frameworks for Advanced Agentic Skill Development: A Technical Synthesis** - -The rapid maturation of Large Language Model (LLM) ecosystems has necessitated a transition from static Software Development Kits (SDKs) to dynamic, agent-centric capabilities known as Agentic Skills. This architectural shift represents a fundamental move from code libraries designed for human invocation to portable, reusable packages of instructions, scripts, and resources specifically optimized for autonomous systems1. For organizations possessing an extensive body of sample code but struggling with lackluster agent performance, the transformation involves more than simple prompt engineering; it requires the systematic encapsulation of domain expertise into the AgentSkills.io open standard, utilizing progressive disclosure to manage the inherent constraints of the LLM context window3. - -## **The Taxonomy of Agentic Skills versus Traditional SDKs** - -The primary limitation of traditional SDK-based approaches for agents is threefold: token inefficiency, exponential growth in decision complexity, and a lack of modular reusability3. When a developer provides an agent with thousands of lines of raw API samples, the agent often suffers from "context dilution," where the core task instructions compete for attention with low-level implementation details4. Agentic skills resolve this by functioning as "onboarding guides" or "static cheat-sheets" that transform a general-purpose model into a specialized agent equipped with procedural knowledge and explicit applicability conditions7. - -The formal definition of a skill extends beyond the simple prompt. It is a four-tuple formalization consisting of a textual descriptor (![][image1]), an intra-skill policy (![][image2]), a resource set (![][image3]), and a set of applicability conditions (![][image4])7. This formalization allows the skill to persist across sessions and carry executable policies that are significantly more robust than one-time, session-scoped plans10. - -| Concept | Unit of Reuse | Internal Logic | Context Impact | -| :---- | :---- | :---- | :---- | -| **SDK Tool** | Atomic Function | Fixed Code | High (if all loaded) | -| **Prompt Template** | Static Text | No decision logic | High (monolithic) | -| **Agent Skill** | Procedural Module | ReAct/Heuristics | Low (progressive) | -| **Workflow** | Deterministic Graph | Rigid Sequence | Moderate | - -1 - -## **The Three-Tier Progressive Disclosure Architecture** - -The defining characteristic of an "advanced" skill is its adherence to the principle of progressive disclosure. This architectural pattern originates from user interface design but, when applied to agents, ensures that the system loads only the necessary information at each phase of the task lifecycle5. Advanced SDKs, such as the Microsoft Agent Framework and Google ADK, implement this through three distinct levels of information loading1. - -### **Level 1: Metadata and Discovery (The L1 Layer)** - -At the start of any agentic session, only the metadata—specifically the name and description—is injected into the system prompt. This "menu" allows the agent to identify available capabilities without incurring a massive token penalty5. A skill with 100 installed capabilities might only consume 1,000 baseline tokens if structured correctly, representing a 90% reduction in context overhead compared to monolithic prompts12. - -### **Level 2: Instructions and Workflow (The L2 Layer)** - -The full SKILL.md body is loaded only when the agent explicitly activates a skill based on user intent. This file acts as the "brain," containing the high-level procedural instructions that guide the agent through multi-step tasks like deployment, code review, or architecture planning5. The specification recommends keeping this layer under 500 lines and 5,000 tokens to ensure the agent remains focused on the primary workflow4. - -### **Level 3: Resources and Deterministic Scripts (The L3 Layer)** - -Detailed reference materials, such as API specifications, schemas, and templates, are stored in a references/ or assets/ directory. These are loaded on demand only when the L2 instructions require them1. Furthermore, executable scripts (Python, Bash, or PowerShell) reside in a scripts/ directory to handle fragile, repetitive, or algorithmic tasks that the LLM would otherwise struggle to perform reliably15. - -## **Engineering Semantic Assets from Existing Code Samples** - -A common pitfall in skill development is providing the agent with raw code samples and expecting it to infer the underlying patterns. To create a "productive" skill, these samples must be transformed into structured assets and parameterized templates20. - -### **Few-Shot Template Parameterization** - -Few-shot prompting is a well-established best practice that significantly improves output consistency6. To utilize an existing body of sample code effectively, developers should extract 3 to 5 high-quality examples that cover different slices of the API usage space24. These should not be raw files but rather "semantic templates" where variable sections are marked for the agent to populate25. - -Mathematically, the probability of a correct agent response ![][image5] is positively correlated with the relevance and quality of the provided few-shot examples ![][image6]: - -![][image7] -where ![][image8] is the user query and ![][image9] is the weight of the example based on its structural similarity to the task7. - -### **Asset vs. Reference Directory Organization** - -The distinction between assets/ and references/ is critical for accurate execution. Files that the agent is meant to read and build upon (e.g., boilerplate code, schemas) should be placed in references/17. Conversely, static files used "as-is" in the final output (e.g., images, company logos, fonts) belong in the assets/ directory17. - -| Directory | Purpose | Loaded into Context? | Interaction Type | -| :---- | :---- | :---- | :---- | -| references/ | API specs, Schemas | Yes, on-demand | Read and Interpret | -| assets/ | Templates, Logos | No | Direct Output Injection | -| scripts/ | Automation, CLI tools | No (Output only) | Execution | -| examples/ | Test data, Use cases | Yes, if requested | Pattern Matching | - -15 - -## **Intent Mapping and Description Optimization** - -The description field in the YAML frontmatter of the SKILL.md file carries the entire burden of triggering the skill during implicit invocation29. If the description fails to accurately map user intent to the skill's capabilities, the agent will never reach for the instructions, regardless of their quality29. - -### **The Imperative Phrase Principle** - -Advanced skill descriptions should use imperative phrasing—"Use this skill when..."—rather than descriptive phrasing—"This skill does..."29. This framing treats the description as an instruction for the agent to act29. Developers should also include "negative triggers" to prevent false positives, clearly stating scenarios where the skill should *not* be used15. - -### **The Trigger Evaluation Loop** - -To improve accuracy, developers should implement an optimization loop for their descriptions. This involves splitting a set of user queries into a "train set" (60%) and a "validation set" (40%)29. The train set queries guide the refinement of the description, while the validation set determines if the changes generalize across diverse phrasing and casual language29. - -| Strategy | Description Attribute | Impact on Accuracy | -| :---- | :---- | :---- | -| **Intent-Based** | Focuses on user's end goal | High | -| **Domain-Specific** | Mentions specific file types or APIs | Moderate | -| **Exclusionary** | Lists "do not use" cases | High (reduces false triggers) | -| **Technical** | Describes internal mechanics | Low | - -15 - -## **Procedural Logic and Deterministic Execution Patterns** - -A "lackluster" skill often provides vague guidance that leads to hallucinations. A productive, advanced skill employs strict procedural patterns such as ReAct (Reasoning \+ Acting) and Plan-Validate-Execute4. - -### **The ReAct Pattern in Skills** - -The ReAct pattern addresses the issue of models hallucinating tool actions. By forcing the agent to interleave thoughts (analyzing the state), actions (calling a tool or script), and observations (evaluating the result), the skill ensures the agent remains grounded in real-world data8. - -### **The Plan-Validate-Execute Cycle** - -For complex or destructive operations—such as database migrations or large-scale code refactors—the SKILL.md should enforce a "Plan-Validate-Execute" pattern4. The agent first creates an implementation plan (often as a JSON or Markdown artifact), validates it against a "source of truth" using a bundled validation script, and only executes the changes after the plan is verified4. This pattern is machine-verifiable and allows for reversible planning, where the agent can iterate on the plan without modifying the original files16. - -## **Advanced State Management and Persistence** - -Maintaining the state of an agentic workflow across multiple turns is a primary differentiator for advanced systems. Standard LLM calls are stateless, but advanced agent frameworks introduce "shared memory" through state schemas and checkpointers34. - -### **Stateful Orchestration with LangGraph** - -LangGraph represents a shift toward stateful, event-driven orchestration by modeling workflows as a StateGraph34. In this architecture, the state is a first-class citizen, typically defined as a Pydantic model that accumulates conversation history and task-specific memory34. Reducer functions are used to manage how nodes update the state, enabling "append-only" logs that prevent overwriting critical information34. - -### **Checkpointing and "Time Travel"** - -Production-grade skills implement state persistence using backends like SQLite or Postgres37. This allows for: - -* **Thread Recovery**: Resuming a session after an interruption33. -* **Time Travel**: Reverting to a previous agent state to debug or retry from a different branch31. -* **Approval Gates**: Pausing execution until a human provides validation, particularly for high-stakes financial or legal tasks32. - -## **Evaluation and Lifecycle Management of Agentic Skills** - -A skill's productivity is measured by its success rate, latency, and reliability. Tools like SkillGrade provide a framework for automated evaluation, allowing developers to catch regressions before deployment15. - -### **The SkillGrade Rubric** - -SkillGrade automates the testing of agent discovery and execution. It generates tasks based on the skill's eval.yaml and uses "grader" models to assess performance based on a rubric15. Effective grading should focus on outcomes (e.g., "Is the security vulnerability patched?") rather than procedural steps40. - -| Metric | Measurement Method | Target Baseline | -| :---- | :---- | :---- | -| **Discovery Rate** | Trigger success on validation set | \> 0.85 | -| **Success Rate** | Task completion (Outcome-based) | \> 0.80 | -| **Token Efficiency** | Token usage per successful task | Lower is better | -| **Latency** | End-to-end processing time | Dependent on model | - -30 - -### **Logic Validation and "Ruthless" QA** - -Before releasing a skill, developers should subject it to a "ruthless" QA audit. This involves feeding the full SKILL.md to an LLM and asking it to hunt for vulnerabilities, unsupported configurations, or implicit assumptions about the environment15. This "Logic Validation" phase uncovers "Execution Blockers" where the agent might be forced to guess due to ambiguous instructions15. - -## **Security and Governance in Skill Deployment** - -As agent skills become more autonomous, security teams must implement strict dependency governance and sandboxing1. Advanced skills should follow the principle of "least privilege," requesting only the minimum toolset required for their specific domain43. - -### **Sandboxing and Input Filtering** - -Executable scripts bundled with skills must run in isolated environments (e.g., containers, seccomp, or firejail) to prevent malicious filesystem or network access1. Furthermore, "Output Filtering" patterns should be used to scrub Personally Identifiable Information (PII) or harmful content before it is shared with the user39. - -### **The Role of Meta-Skills and Human Oversight** - -While the generation of skills can be automated using "Meta-Skills" (skills designed to create other skills), human oversight remains paramount1. Organizations are encouraged to treat SKILL.md files like code dependencies, requiring peer reviews and version control for every change12. - -## **Synthesis: The Roadmap to a Productive Agent Skill** - -The transition from a "lackluster" attempt to a "productive" skill is a multi-stage process of refinement. It begins with the extraction of real expertise from hands-on tasks and existing project artifacts4. By organizing this expertise into the three-level progressive disclosure architecture, developers ensure that deep domain knowledge is available on-demand without degrading the agent's performance5. - -The integration of semantic assets—such as parameterized templates derived from the user's extensive code samples—provides the agent with the "analogical reasoning" capabilities needed to excel at complex SDK implementations21. When combined with deterministic scripts for algorithmic verification and robust state management for multi-turn persistence, the resulting agent skill becomes a reliable, enterprise-ready capability that moves beyond basic assistance toward true autonomous agency8. The ultimate utility of these systems is found not just in the intelligence of the underlying model, but in the robustness of the structural and procedural guardrails provided by the well-designed skill package18. - -#### **Works cited** - -1. Agent Skills | Microsoft Learn, [https://learn.microsoft.com/en-us/agent-framework/agents/skills](https://learn.microsoft.com/en-us/agent-framework/agents/skills) -2. Give Your Agents Domain Expertise with Agent Skills in Microsoft Agent Framework, [https://devblogs.microsoft.com/agent-framework/give-your-agents-domain-expertise-with-agent-skills-in-microsoft-agent-framework/](https://devblogs.microsoft.com/agent-framework/give-your-agents-domain-expertise-with-agent-skills-in-microsoft-agent-framework/) -3. aws-samples/sample-strands-agents-agentskills: Agent Skills implementation for Strands Agents SDK \- GitHub, [https://github.com/aws-samples/sample-strands-agents-agentskills](https://github.com/aws-samples/sample-strands-agents-agentskills) -4. [https://agentskills.io/skill-creation/best-practices](https://agentskills.io/skill-creation/best-practices) -5. The SKILL.md Pattern: How to Write AI Agent Skills That Actually Work | by Bibek Poudel, [https://bibek-poudel.medium.com/the-skill-md-pattern-how-to-write-ai-agent-skills-that-actually-work-72a3169dd7ee](https://bibek-poudel.medium.com/the-skill-md-pattern-how-to-write-ai-agent-skills-that-actually-work-72a3169dd7ee) -6. Effective context engineering for AI agents \- Anthropic, [https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents](https://www.anthropic.com/engineering/effective-context-engineering-for-ai-agents) -7. Agent Skill Framework: Perspectives on the Potential of Small Language Models in Industrial Environments \- arXiv, [https://arxiv.org/html/2602.16653v1](https://arxiv.org/html/2602.16653v1) -8. Agent Skills: The Missing Layer That Makes AI Agents Enterprise Ready \- DEV Community, [https://dev.to/sreeni5018/agent-skills-the-missing-layer-that-makes-ai-agents-enterprise-ready-3gc](https://dev.to/sreeni5018/agent-skills-the-missing-layer-that-makes-ai-agents-enterprise-ready-3gc) -9. deepagentsjs/examples/skills/skill-creator/SKILL.md at main \- GitHub, [https://github.com/langchain-ai/deepagentsjs/blob/main/examples/skills/skill-creator/SKILL.md](https://github.com/langchain-ai/deepagentsjs/blob/main/examples/skills/skill-creator/SKILL.md) -10. SoK: Agentic Skills — Beyond Tool Use in LLM Agents \- arXiv, [https://arxiv.org/html/2602.20867v1](https://arxiv.org/html/2602.20867v1) -11. Choose a design pattern for your agentic AI system | Cloud Architecture Center, [https://docs.cloud.google.com/architecture/choose-design-pattern-agentic-ai-system](https://docs.cloud.google.com/architecture/choose-design-pattern-agentic-ai-system) -12. Developer's Guide to Building ADK Agents with Skills, [https://developers.googleblog.com/en/developers-guide-to-building-adk-agents-with-skills/](https://developers.googleblog.com/en/developers-guide-to-building-adk-agents-with-skills/) -13. Progressive Disclosure: the technique that helps control context (and tokens) in AI agents, [https://medium.com/@martia\_es/progressive-disclosure-the-technique-that-helps-control-context-and-tokens-in-ai-agents-8d6108b09289](https://medium.com/@martia_es/progressive-disclosure-the-technique-that-helps-control-context-and-tokens-in-ai-agents-8d6108b09289) -14. Introducing Agent Plugins for AWS | AWS Developer Tools Blog, [https://aws.amazon.com/blogs/developer/introducing-agent-plugins-for-aws/](https://aws.amazon.com/blogs/developer/introducing-agent-plugins-for-aws/) -15. GitHub \- mgechev/skills-best-practices: Write professional-grade skills for agents, validate them using LLMs, and maintain a lean context window., [https://github.com/mgechev/skills-best-practices](https://github.com/mgechev/skills-best-practices) -16. Skill authoring best practices \- Claude API Docs, [https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices](https://platform.claude.com/docs/en/agents-and-tools/agent-skills/best-practices) -17. skill-creator \- anthropics/financial-services-plugins \- GitHub, [https://github.com/anthropics/financial-services-plugins/blob/main/financial-analysis/skills/skill-creator/SKILL.md](https://github.com/anthropics/financial-services-plugins/blob/main/financial-analysis/skills/skill-creator/SKILL.md) -18. GitHub \- gohypergiant/agent-skills: A collection of skills for AI coding agents. Skills are packaged instructions and scripts that extend agent capabilities., [https://github.com/gohypergiant/agent-skills](https://github.com/gohypergiant/agent-skills) -19. agent-skills.instructions.md \- github/awesome-copilot, [https://github.com/github/awesome-copilot/blob/main/instructions/agent-skills.instructions.md](https://github.com/github/awesome-copilot/blob/main/instructions/agent-skills.instructions.md) -20. Include few-shot examples | Generative AI on Vertex AI \- Google Cloud Documentation, [https://docs.cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/few-shot-examples](https://docs.cloud.google.com/vertex-ai/generative-ai/docs/learn/prompts/few-shot-examples) -21. Achieving 5x Agentic Coding Performance with Few-Shot Prompting, [https://towardsdatascience.com/5x-agentic-coding-performance-with-few-shot-prompting/](https://towardsdatascience.com/5x-agentic-coding-performance-with-few-shot-prompting/) -22. Daily Papers \- Hugging Face, [https://huggingface.co/papers?q=experience%20memory](https://huggingface.co/papers?q=experience+memory) -23. Few-Shot Learning for LLMs: Examples and Implementation Guide \- Tetrate, [https://tetrate.io/learn/ai/few-shot-learning-llms](https://tetrate.io/learn/ai/few-shot-learning-llms) -24. Few-Shot Prompting for Agentic Systems: Teaching by Example \- Comet, [https://www.comet.com/site/blog/few-shot-prompting/](https://www.comet.com/site/blog/few-shot-prompting/) -25. Few-Shot Prompting \- Portkey Docs, [https://docs.portkey.ai/docs/guides/use-cases/few-shot-prompting](https://docs.portkey.ai/docs/guides/use-cases/few-shot-prompting) -26. LangChain Prompt Templates: Complete Guide with Examples \- Latenode Blog, [https://latenode.com/blog/ai-frameworks-technical-infrastructure/langchain-setup-tools-agents-memory/langchain-prompt-templates-complete-guide-with-examples](https://latenode.com/blog/ai-frameworks-technical-infrastructure/langchain-setup-tools-agents-memory/langchain-prompt-templates-complete-guide-with-examples) -27. FloorplanQA: A Benchmark for Spatial Reasoning in LLMs using Structured Representations, [https://arxiv.org/html/2507.07644v2](https://arxiv.org/html/2507.07644v2) -28. Provide examples (few-shot prompting) \- Amazon Nova \- AWS Documentation, [https://docs.aws.amazon.com/nova/latest/userguide/prompting-examples.html](https://docs.aws.amazon.com/nova/latest/userguide/prompting-examples.html) -29. Optimizing skill descriptions \- Agent Skills, [https://agentskills.io/skill-creation/optimizing-descriptions](https://agentskills.io/skill-creation/optimizing-descriptions) -30. Building AI with Chatbot Intent Recognition: Guide \- IrisAgent, [https://irisagent.com/blog/building-chatbots-with-intent-detection-guide/](https://irisagent.com/blog/building-chatbots-with-intent-detection-guide/) -31. Design Patterns for Agentic AI and Multi-Agent Systems \- AppsTek Corp, [https://appstekcorp.com/blog/design-patterns-for-agentic-ai-and-multi-agent-systems/](https://appstekcorp.com/blog/design-patterns-for-agentic-ai-and-multi-agent-systems/) -32. 7 Must-Know Agentic AI Design Patterns \- MachineLearningMastery.com, [https://machinelearningmastery.com/7-must-know-agentic-ai-design-patterns/](https://machinelearningmastery.com/7-must-know-agentic-ai-design-patterns/) -33. Single-responsibility agents and multi-agent workflows in AI-powered development tools, [https://www.epam.com/insights/ai/blogs/single-responsibility-agents-and-multi-agent-workflows](https://www.epam.com/insights/ai/blogs/single-responsibility-agents-and-multi-agent-workflows) -34. The Architecture of Agents: Planning, Action, and State Management in Large Language Models | by Tejaswi kashyap \- GoPenAI, [https://blog.gopenai.com/the-architecture-of-agents-planning-action-and-state-management-in-large-language-models-e00b340fcf09](https://blog.gopenai.com/the-architecture-of-agents-planning-action-and-state-management-in-large-language-models-e00b340fcf09) -35. LangGraph State Management Patterns | Claude Code Skill \- MCP Market, [https://mcpmarket.com/tools/skills/langgraph-state-management](https://mcpmarket.com/tools/skills/langgraph-state-management) -36. The Best Open Source Frameworks For Building AI Agents in 2026 \- Firecrawl, [https://www.firecrawl.dev/blog/best-open-source-agent-frameworks](https://www.firecrawl.dev/blog/best-open-source-agent-frameworks) -37. LangGraph Checkpoint Patterns \- Claude Code Skill \- MCP Market, [https://mcpmarket.com/tools/skills/langgraph-state-persistence](https://mcpmarket.com/tools/skills/langgraph-state-persistence) -38. Claude Code Guide: Features and Best Practices, [https://www.elliotjreed.com/ai/claude-code-guide-and-tips](https://www.elliotjreed.com/ai/claude-code-guide-and-tips) -39. claude-skills/engineering/agent-designer/SKILL.md at main \- GitHub, [https://github.com/alirezarezvani/claude-skills/blob/main/engineering/agent-designer/SKILL.md](https://github.com/alirezarezvani/claude-skills/blob/main/engineering/agent-designer/SKILL.md) -40. GitHub \- mgechev/skillgrade: "Unit tests" for your agent skills, [https://github.com/mgechev/skillgrade](https://github.com/mgechev/skillgrade) -41. Skill Design for LLM Agents by Minko Gechev \- GitNation, [https://gitnation.com/contents/skill-design-for-llm-agents](https://gitnation.com/contents/skill-design-for-llm-agents) -42. What is agentic coding? How it works and use cases | Google Cloud, [https://cloud.google.com/discover/what-is-agentic-coding](https://cloud.google.com/discover/what-is-agentic-coding) -43. Top 10 Claude Code Skills Every Builder Should Know in 2026 \- Composio, [https://composio.dev/content/top-claude-skills](https://composio.dev/content/top-claude-skills) -44. CodeBuddy Code Best Practices, [https://www.codebuddy.ai/docs/cli/best-practices](https://www.codebuddy.ai/docs/cli/best-practices) -45. 3 Principles for Designing Agent Skills \- Block Engineering Blog, [https://engineering.block.xyz/blog/3-principles-for-designing-agent-skills](https://engineering.block.xyz/blog/3-principles-for-designing-agent-skills) -46. The Few Shot Prompting Guide \- PromptHub, [https://www.prompthub.us/blog/the-few-shot-prompting-guide](https://www.prompthub.us/blog/the-few-shot-prompting-guide) - -[image1]: - -[image2]: - -[image3]: - -[image4]: - -[image5]: - -[image6]: - -[image7]: - -[image8]: - -[image9]: \ No newline at end of file From 030da6df1b7763069b9f26b0e98cd604d7bddc40 Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:46:36 -0600 Subject: [PATCH 10/11] docs: stop tracking FUTURE_WORK.md --- .../skills/android-maps3d-sdk/FUTURE_WORK.md | 24 ------------------- .gitignore | 4 ++++ 2 files changed, 4 insertions(+), 24 deletions(-) delete mode 100644 .gemini/skills/android-maps3d-sdk/FUTURE_WORK.md diff --git a/.gemini/skills/android-maps3d-sdk/FUTURE_WORK.md b/.gemini/skills/android-maps3d-sdk/FUTURE_WORK.md deleted file mode 100644 index c426bcc..0000000 --- a/.gemini/skills/android-maps3d-sdk/FUTURE_WORK.md +++ /dev/null @@ -1,24 +0,0 @@ -# Future Work for Android Maps 3D SDK Skill - -This document tracks planned enhancements and evaluation strategies to further level up the skill. - -## 1. Adaptive Fidelity Patterns -To make the skill respect hardware constraints (battery, thermal), we plan to add an "Adaptive Fidelity" pattern. -* **Proposal**: Provide a `DefaultLifecycleObserver` that listens for `ACTION_POWER_SAVE_MODE_CHANGED` and throttles the camera `range` or disables heavy animations when battery saver is active. -* **Status**: Conceptualized. - -## 2. Behavioral & Visual Evaluation Strategies -To accurately evaluate an agent using this skill (moving beyond static text assertions), we plan to implement the following eval strategies: - -### A. Logcat State-Machine Verification -* **Concept**: Assert on the sequence of events emitted by the agent's code (e.g., using `CameraLogger`). -* **Example**: Verify that `CameraAnimationEnd` happens before `MapSteady` (if applicable) and before assets are loaded. - -### B. Automated Visual Testing -* **Concept**: Use emulator screenshots or video recordings to verify that 3D elements are rendered correctly and centered. - -### C. Synthesized Test Assertion -* **Concept**: Require the agent to generate a passing instrumentation test (using the Espresso or `ComposeTestRule` snippets in the catalogs) as part of the evaluation success criteria. - -### D. Walkthrough Artifact Grading -* **Concept**: Grade the agent on its ability to produce a `walkthrough.md` with an embedded video demonstrating the working feature. diff --git a/.gitignore b/.gitignore index fd77aea..31fe268 100644 --- a/.gitignore +++ b/.gitignore @@ -43,3 +43,7 @@ snippets/docs/ # Skill development scratchpad skill-dev.md +# Skill future work +.gemini/skills/android-maps3d-sdk/FUTURE_WORK.md + + From 5adc851450e880a823cc4ceb03e8219639c2652d Mon Sep 17 00:00:00 2001 From: dkhawk <107309+dkhawk@users.noreply.github.com> Date: Fri, 10 Apr 2026 16:48:24 -0600 Subject: [PATCH 11/11] docs: remove skill-dev.md and FUTURE_WORK.md from .gitignore --- .gitignore | 5 ----- 1 file changed, 5 deletions(-) diff --git a/.gitignore b/.gitignore index 31fe268..70ff3fc 100644 --- a/.gitignore +++ b/.gitignore @@ -40,10 +40,5 @@ google-services.json .vscode/ snippets/docs/ -# Skill development scratchpad -skill-dev.md - -# Skill future work -.gemini/skills/android-maps3d-sdk/FUTURE_WORK.md