Skip to content

feat(image-meter): native photo annotation with scaled measurements#85

Open
tstapler wants to merge 5 commits into
mainfrom
stelekit-image-meter
Open

feat(image-meter): native photo annotation with scaled measurements#85
tstapler wants to merge 5 commits into
mainfrom
stelekit-image-meter

Conversation

@tstapler
Copy link
Copy Markdown
Owner

Summary

  • Replaces ImageMeter end-to-end: capture photos, annotate with scaled measurements, and store everything as first-class image_annotation blocks in the Logseq graph — no app-switching required
  • Annotation canvas (Compose Multiplatform, all platforms): line, polygon, angle, text-label, and grid-ref tools over a pannable/zoomable image; coordinates stored in normalized [0,1] space; Path objects retained for 30fps
  • Calibration chain (ordered by accuracy): BLE laser ±1mm → manual reference → ARCore depth ±8–10cm (with explicit warning) → EXIF focal-length ±15% → Depth Anything V2 monocular ML
  • BLE laser rangefinders: Leica DISTO and Bosch GLM protocol implementations in commonMain; Kable device registry; Android ForegroundService for API 31+; USB serial and HID keyboard-emulation fallbacks
  • Google integration: Drive import/export via Ktor REST v3; Photos import via new Picker API (post-March-2025 — old photoslibrary scopes revoked); OAuth with EncryptedSharedPreferences + Android Keystore
  • SteleKit integration: gallery view with tag/sort filtering; journal auto-insert on capture; {{measure: image.label}} block math references; back-links between gallery and pages
  • Platform sensors: GPS tagging, compass bearing auto-annotation, accelerometer tilt warning, iOS LiDAR stub
  • SQLDelight migration 4: image_annotations + measurement_annotations tables; JSON sidecar at .stelekit/images/<uuid>.measure.json written before DB row for atomicity

Key architectural decisions

Decision Rationale
image_annotation blockType extending existing Block Reuses all block infrastructure (search, backlinks, export) without a parallel entity hierarchy
Sidecar-first write order If sidecar fails, DB row is never committed — prevents measurement data loss on disk-full
Google Photos Picker API (new) photoslibrary.readonly and related scopes were revoked March 31 2025; programmatic library browsing is impossible
ARCore as tertiary (not primary) calibration 8–10cm absolute error is incompatible with construction measurement; BLE laser is primary
Compose Canvas + ZoomImage, no third-party annotation lib KMP-native; ZoomImage handles large-image subsampling; full rendering control

Notable things NOT yet wired (require hardware/native SDK work)

  • Full CameraX capture flow (AndroidCameraProvider stub — needs CameraX preview surface integration)
  • Kable BLE scanning (stub until com.juul.kable dependency added; protocol parsing is complete and tested)
  • ARCore depth frames (stub; session setup needs ARCore AAR)
  • Depth Anything V2 ONNX inference (stub; model file and ONNX Runtime dependency not bundled)
  • iOS CMMotionManager / CLLocationManager (stub; needs Swift interop wiring)

Test plan

  • 1,582 jvmTest passing, 0 failures
  • Android unit tests: BUILD SUCCESSFUL
  • Detekt: 0 violations
  • SQLDelight migration compiles and passes schema validation
  • Sidecar atomicity: FileSystem.writeFileBytes throw → DB row not committed (TC-020)
  • Calibration recalculation on change: all committed measurements re-derived (TC-041)
  • BLE GATT 133 simulation: exponential backoff + gatt.close() verified (TC-073/074)
  • Leica DISTO + Bosch GLM protocol parsing: byte-sequence unit tests (commonTest)
  • Google Drive export: mock Ktor engine verifies JPEG + JSON sidecar both uploaded
  • Samsung EXIF ORIENTATION_TRANSVERSE regression test

🤖 Generated with Claude Code

Copilot AI review requested due to automatic review settings May 18, 2026 13:59
@github-actions
Copy link
Copy Markdown
Contributor

JVM Load Benchmark (Desktop)

Synthetic in-memory benchmark measuring load performance for the desktop (JVM) app.
Comparing 0feedd3 (this PR) vs ae70476 (baseline)
Graph config: xlarge — 230 pages

Metric This PR Baseline Delta
Phase 1 TTI ↓ 11ms 10ms +1ms (+10%) ⚠️
Phase 2 background ↓ 4ms 3ms +1ms (+33%) ⚠️
Phase 3 index ↓ 18ms 14ms +4ms (+29%) ⚠️
Total ↓ 32ms 26ms +6ms (+23%) ⚠️
Write p95 (baseline) ↓ 35ms 30ms +5ms (+17%) ⚠️
Write p95 (under load) ↓ n/a n/a
Jank factor ↓ n/a n/a
↓ lower is better
Flamegraphs (this PR) **Allocation** — object allocation pressure (JDBC/SQLite churn)

Alloc flamegraph not available

CPU — method-level hotspots by on-CPU time

CPU flamegraph not available

Top SQL queries by total time (this PR) | table:operation | calls | p50 | p99 | max | total | |-----------------|-------|-----|-----|-----|-------| | `pages:select` | 2 | 1ms | 1ms | 1ms | 1ms |
Top allocation hotspots (this PR) `68.3%` byte[]_[k] `4.3%` java.lang.String_[k] `3.7%` java.lang.Object[]_[k] `3%` java.lang.StringBuilder_[k] `1.8%` long[]_[k]
Top CPU hotspots (this PR) `99.3%` /usr/lib/x86_64-linux-gnu/libc.so.6 `0.1%` pread `0%` jdk/internal/loader/Resource.cachedInputStream_[i] `0%` ConstMethod::localvariable_table_start `0%` kotlinx/coroutines/internal/DispatchedContinuationKt.safeDispatch_[0]

@github-actions
Copy link
Copy Markdown
Contributor

github-actions Bot commented May 18, 2026

Android Load Benchmark

Instrumented benchmark on an API 30 x86_64 emulator — 500-page synthetic graph.

Comparing 7a9f8db (this PR) vs 20adc0f (baseline)
Device: API 30 x86_64 emulator — 530 pages loaded

Graph Load

Metric This PR Baseline Delta
Phase 1 TTI ↓ 49ms 33ms +16ms (+48%) ⚠️
Phase 3 index ↓ 2289ms 1868ms +421ms (+23%) ⚠️

Interactive Write Latency (during Phase 3)

Metric This PR Baseline Delta
Write p95 (baseline) ↓ 3ms 2ms +1ms (+50%) ⚠️
Write p95 (during phase 3) ↓ 192ms 125ms +67ms (+54%) ⚠️
Jank factor ↓ 64x 62.5x +1.5x (+2%) ⚠️
Concurrent writes ↑ 11 10 +1ms (+10%) ✅

SAF I/O Overhead (ContentProvider vs direct File read)

Measures Binder IPC cost added by ContentResolver per readFile() call.
Real SAF via ExternalStorageProvider will be higher on device; this is a lower bound.

Metric This PR Baseline Delta
Direct read / file ↓ 0.0ms 0.0ms 0 (0%)
Provider read / file ↓ 0.2ms 0.2ms 0 (0%)
IPC overhead ratio ↓ 6x 6x 0 (0%)
↓ lower is better · ↑ higher is better

Copy link
Copy Markdown

Copilot AI left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR introduces the “image-meter” feature scaffolding across SteleKit’s KMP stack: first-class image_annotation blocks with sidecar-backed persistence, measurement annotation storage, gallery/editor navigation, and platform sensor/device integration stubs.

Changes:

  • Adds image/measurement annotation persistence (SQLDelight schema + migration v4) and sidecar serialization/indexing utilities.
  • Introduces UI plumbing for an image gallery + annotation editor entry points (navigation, block rendering hooks, settings UI).
  • Adds cross-platform sensor + external measurement device abstractions (camera/motion/depth/ML, BLE/keyboard/USB stubs) and Google OAuth token storage interfaces/implementations.

Reviewed changes

Copilot reviewed 130 out of 130 changed files in this pull request and generated 11 comments.

Show a summary per file
File Description
kmp/src/wasmJsMain/kotlin/dev/stapler/stelekit/platform/sensor/WebCameraProvider.kt WASM camera provider stub returning HardwareUnavailable.
kmp/src/jvmTest/kotlin/dev/stapler/stelekit/db/ImageSidecarManagerTest.kt JVM tests for sidecar manager/indexer and filesystem failure paths.
kmp/src/jvmTest/kotlin/dev/stapler/stelekit/annotate/AnnotationExporterTest.kt JVM integration tests for baking/encoding annotation overlays.
kmp/src/jvmMain/kotlin/dev/stapler/stelekit/ui/annotate/ImageEncoder.jvm.kt Desktop JPEG encoding via ImageIO.
kmp/src/jvmMain/kotlin/dev/stapler/stelekit/platform/sensor/WebcamCameraProvider.kt Desktop camera provider stub delegating to file picker.
kmp/src/jvmMain/kotlin/dev/stapler/stelekit/platform/sensor/DesktopFilePicker.kt Swing-based image file picker for desktop imports.
kmp/src/jvmMain/kotlin/dev/stapler/stelekit/platform/google/JvmGoogleTokenStore.kt Desktop Google token persistence via Preferences (not secure).
kmp/src/iosMain/kotlin/dev/stapler/stelekit/ui/annotate/ImageEncoder.ios.kt iOS JPEG encoder stub.
kmp/src/iosMain/kotlin/dev/stapler/stelekit/platform/sensor/IOSMotionSensorProvider.kt iOS motion sensor provider stub.
kmp/src/iosMain/kotlin/dev/stapler/stelekit/platform/sensor/IOSLidarDepthProvider.kt iOS LiDAR depth provider stub + hardware check placeholder.
kmp/src/iosMain/kotlin/dev/stapler/stelekit/platform/sensor/IOSCameraProvider.kt iOS camera provider stub.
kmp/src/iosMain/kotlin/dev/stapler/stelekit/platform/google/IosGoogleTokenStore.kt iOS Google token store stub (in-memory).
kmp/src/commonTest/kotlin/dev/stapler/stelekit/repository/InMemoryMeasurementAnnotationRepositoryTest.kt commonTest coverage for in-memory measurement repository.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/repository/InMemoryImageAnnotationRepositoryTest.kt commonTest coverage for in-memory image annotation repository.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/sensor/SensorDataPropagationTest.kt Tests for propagating sensor snapshots into imported annotations.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/sensor/NoOpCameraProviderTest.kt Tests for NoOp camera provider behavior.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/sensor/MotionSensorProviderTest.kt Tests for motion sensor provider contract and defaults.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/sensor/GpsTaggingTest.kt Tests for GPS presence + tilt warning threshold logic.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/sensor/BearingAnnotationTest.kt Tests for compass-bearing label formatting rules.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/measurement/NoOpBleScannerTest.kt Tests for no-op BLE scanner and registry registration.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/measurement/MeasurementDeviceRegistryTest.kt Tests for measurement device registry aggregation and reset.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/measurement/LeicaDistoProtocolTest.kt Tests for Leica DISTO protocol parsing.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/measurement/KeyboardMeasurementParserTest.kt Tests for keyboard measurement parsing (units, separators, edge cases).
kmp/src/commonTest/kotlin/dev/stapler/stelekit/platform/measurement/BoschGlmProtocolTest.kt Tests for Bosch GLM protocol parsing.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/model/UnitConversionTest.kt Tests for meter↔display unit conversions.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/model/ImageAnnotationModelTest.kt Tests for ImageAnnotation defaults + validation.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/google/GoogleTokenStoreTest.kt Tests for token store contract + expiry logic.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/error/DomainErrorTest.kt Extends DomainError message coverage for SensorError variants.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/db/sidecar/SidecarSerializationTest.kt Sidecar JSON round-trip tests and malformed JSON handling.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/db/sidecar/FakeFileSystem.kt In-memory FileSystem for sidecar/image tests.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/db/ImageStoragePathResolverTest.kt Tests for image asset path conventions.
kmp/src/commonTest/kotlin/dev/stapler/stelekit/db/ImageImportServicePathTest.kt Tests for reservePath directory creation + UUID prefixing.
kmp/src/commonMain/sqldelight/dev/stapler/stelekit/db/SteleDatabase.sq Adds image_annotations + measurement_annotations tables and queries.
kmp/src/commonMain/sqldelight/dev/stapler/stelekit/db/migrations/4.sqm SQLDelight migration v4 for image/measurement tables + indexes.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/StelekitViewModel.kt Adds navigation helpers for Gallery and AnnotationEditor screens.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/screens/PageView.kt Wires callback to open annotation editor from page blocks.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/screens/JournalsView.kt Adds annotation editor callback plumbing through journal block list.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/gallery/GalleryViewModel.kt New gallery state + loading/filtering/sorting logic.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/drive/DriveFileBrowserViewModel.kt New Drive file browser state and view model flow.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/Sidebar.kt Adds Gallery navigation item.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/SettingsDialog.kt Adds Google Account settings category and wiring parameters.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/settings/GoogleAccountSettings.kt New settings UI section for Google account connect/disconnect.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/SearchDialog.kt Adjusts SearchResultRow signature (modifier) + wrapper overload.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/BlockRenderer.kt Adds callback for opening annotation editor from rendered blocks.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/BlockList.kt Passes onOpenAnnotationEditor through block list rendering.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/components/BlockItem.kt Adds IMAGE_ANNOTATION rendering branch and editor open callback.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/AppState.kt Adds Screen.Gallery and Screen.AnnotationEditor routes.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/App.kt Routes Gallery/AnnotationEditor screens; wires view models.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/annotate/TagEditorPanel.kt Tag editing UI for the annotation editor.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/annotate/MeasurementLabelOverlay.kt Canvas overlay to render measurement labels with leader lines.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/annotate/LabelInputOverlay.kt Floating overlay for label text entry tool.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/ui/annotate/ImageEncoder.kt expect/actual JPEG encoder abstraction.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/repository/SqlDelightMeasurementAnnotationRepository.kt SQLDelight-backed measurement annotation repository.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/repository/MeasurementAnnotationRepository.kt Repository interface for measurement annotations.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/repository/InMemoryMeasurementAnnotationRepository.kt In-memory measurement annotation repository.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/repository/InMemoryImageAnnotationRepository.kt In-memory image annotation repository.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/repository/ImageAnnotationRepository.kt Repository interface for image annotations.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/repository/GraphRepository.kt Adds image/measurement repositories to RepositorySet defaults.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/sensor/SensorModule.kt Global wiring point for platform sensor providers/estimators.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/sensor/PlatformImageFile.kt Represents captured/imported image file + EXIF/sensor snapshot.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/sensor/NoOpCameraProvider.kt No-op camera provider returning HardwareUnavailable.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/sensor/MotionSensorProvider.kt Motion sensor provider interface + NoOp implementation.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/sensor/DepthSensorProvider.kt Depth sensor provider interface + NoOp implementation.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/sensor/CameraProvider.kt Camera provider abstraction for capture pipeline.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/ml/MonocularDepthEstimator.kt ML depth estimator abstraction + NoOp implementation.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/measurement/MeasurementDeviceRegistry.kt Registry for aggregating measurement device factories.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/measurement/keyboard/KeyboardMeasurementParser.kt Parser for typed measurement strings into meters.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/measurement/keyboard/KeyboardEmulationDeviceFactory.kt Factory emitting a singleton “Keyboard” measurement device.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/measurement/keyboard/KeyboardEmulationDevice.kt Keyboard-emulation measurement device emitting readings from text.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/measurement/ExternalMeasurementDevice.kt Device interfaces/models for external measurement sources.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/measurement/ble/NoOpBleScanner.kt No-op BLE scanner factory.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/measurement/ble/LeicaDistoProtocol.kt Leica DISTO pure-Kotlin protocol parser.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/measurement/ble/BoschGlmProtocol.kt Bosch GLM pure-Kotlin protocol parser.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/google/GoogleTokenStore.kt Token store interface + expiry helper.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/google/GoogleTokenRefresher.kt Ktor-based refresh-token exchange helper.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/platform/google/GoogleAuthManager.kt Platform interface for initiating OAuth flow.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/model/Models.kt Registers image_annotation as a valid block type.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/model/ImageAnnotation.kt Adds image annotation domain models (calibration/sensor/source).
kmp/src/commonMain/kotlin/dev/stapler/stelekit/model/BlockTypes.kt Adds BlockTypes.IMAGE_ANNOTATION constant.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/error/DomainError.kt Adds SensorError variants + UI mapping.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/domain/MeasurementPropertySyncer.kt Generates block properties + resolves {{measure: ...}} templates.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/sidecar/ImageSidecarSchema.kt Defines sidecar JSON schema v1 for images/measurements.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/sidecar/ImageSidecarIndexer.kt Rebuilds DB state from sidecar files.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/RestrictedDatabaseQueries.kt Adds restricted write helpers for new image/measurement queries.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/db/ImageStoragePathResolver.kt Resolves <graph>/assets/images/<date>-<uuidPrefix>.jpg storage path.
kmp/src/commonMain/kotlin/dev/stapler/stelekit/calibration/CalibrationFallbackChain.kt Implements calibration method fallback chain with logging.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/ui/annotate/ImageEncoder.android.kt Android JPEG encoder using Bitmap.compress.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/sensor/ARCoreDepthProvider.kt ARCore depth provider stub.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/sensor/AndroidPhotoPickerLauncher.kt Photo Picker wrapper that copies selected content into temp file.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/sensor/AndroidCameraProvider.kt CameraX-based camera provider stub.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/ml/OnnxMonocularDepthEstimator.kt ONNX monocular depth estimator stub.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/measurement/usb/AndroidUsbSerialFactory.kt USB serial factory stub (delegates to no-op).
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/measurement/ble/KableBleScanner.kt Kable BLE scanner stub with permission guard.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/measurement/ble/AndroidMeasurementForegroundService.kt Foreground service stub for BLE connections.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/google/AndroidGoogleTokenStore.kt Secure Android token store using EncryptedSharedPreferences.
kmp/src/androidMain/kotlin/dev/stapler/stelekit/platform/google/AndroidGoogleAuthManager.kt Android OAuth flow stub launching browser with consent URL.
kmp/src/androidMain/AndroidManifest.xml Adds permissions/features for GPS/BLE/USB and registers BLE service.
kmp/build.gradle.kts Adds Android ExifInterface dependency for future capture pipeline.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +64 to +75
private fun loadImages() {
scope.launch {
val tag = _state.value.selectedTag
val flow = if (tag != null) {
imageAnnotationRepository.getImageAnnotationsByTag(tag)
} else {
imageAnnotationRepository.getAllImageAnnotations()
}
flow.collect { result ->
result.fold(
ifLeft = { err ->
logger.error("Failed to load gallery images: ${err.message}")
Comment on lines +1111 to +1125
is Screen.Gallery -> {
NavigationTracingEffect("Gallery")
val galleryViewModel = remember {
GalleryViewModel(repos.imageAnnotationRepository)
}
GalleryScreen(
viewModel = galleryViewModel,
onOpenAnnotationEditor = { uuid ->
viewModel.navigateToAnnotationEditor(uuid)
},
onNavigateToPage = { pageUuid ->
viewModel.navigateToPageByUuid(pageUuid)
},
)
}
Comment on lines +41 to +78
return withContext(Dispatchers.IO) {
try {
UIManager.setLookAndFeel(UIManager.getSystemLookAndFeelClassName())
} catch (e: CancellationException) {
throw e
} catch (_: Exception) {
// Non-fatal: system L&F is cosmetic only
}

var chosenFile: File? = null
val done = java.util.concurrent.CountDownLatch(1)

SwingUtilities.invokeLater {
try {
val chooser = JFileChooser().apply {
dialogTitle = "Select Image"
fileSelectionMode = JFileChooser.FILES_ONLY
isMultiSelectionEnabled = false
fileFilter = FileNameExtensionFilter(
"Image files (JPEG, PNG)",
"jpg", "jpeg", "png"
)
}
val result = chooser.showOpenDialog(null)
if (result == JFileChooser.APPROVE_OPTION) {
chosenFile = chooser.selectedFile
}
} catch (e: CancellationException) {
throw e
} catch (_: Exception) {
// Swallow; chosenFile stays null → user-cancel path
} finally {
done.countDown()
}
}

done.await()

Comment on lines +35 to +40
override fun getLastModifiedTime(path: String): Long? = null
override fun readFileBytes(path: String): ByteArray? = files[path]?.encodeToByteArray()
override fun writeFileBytes(path: String, data: ByteArray): Boolean {
files[path] = data.decodeToString()
return true
}
Comment on lines +7 to +17
* Secure storage interface for Google OAuth 2.0 tokens.
*
* Platform implementations:
* - androidMain: [AndroidGoogleTokenStore] via EncryptedSharedPreferences + Android Keystore
* - jvmMain: [JvmGoogleTokenStore] via java.util.prefs.Preferences (not production-grade)
* - iosMain: [IosGoogleTokenStore] via in-memory storage (stub; full Keychain impl deferred)
*
* SECURITY: Tokens must NEVER be stored in plaintext. Each platform implementation
* must use the strongest available secure storage. If secure storage is unavailable,
* propagate the error to the caller rather than falling back silently.
*/
Comment on lines +10 to +23
/**
* JVM (Desktop) implementation of [GoogleTokenStore] using [java.util.prefs.Preferences].
*
* NOTE: This is NOT production-grade secure storage. Java Preferences stores data in
* the OS user profile directory (e.g., ~/.java/.userPrefs or the Windows Registry) without
* encryption. Tokens stored here can be read by any process running as the same OS user.
*
* TODO: Replace with OS keyring integration:
* - Linux: libsecret / Secret Service DBus API
* - macOS: Keychain Services via JNA or the macOS Keychain Java bridge
* - Windows: Windows Credential Manager via JNA
*
* This implementation is sufficient for development and local testing only.
*/
Comment on lines +28 to +37
val writers = ImageIO.getImageWritersByFormatName("jpeg")
if (!writers.hasNext()) return ByteArray(0)
val writer = writers.next()
val params: ImageWriteParam = writer.defaultWriteParam.apply {
compressionMode = ImageWriteParam.MODE_EXPLICIT
compressionQuality = quality / 100f
}
val out = ByteArrayOutputStream()
writer.output = ImageIO.createImageOutputStream(out)
writer.write(null, IIOImage(buffered, null, null), params)
Comment on lines +47 to +58
@DirectRepositoryWrite
override suspend fun saveImageAnnotation(annotation: ImageAnnotation): Either<DomainError, Unit> {
val current = annotations.value.toMutableMap()
if (annotation.uuid in current) {
return DomainError.ValidationError.ConstraintViolation(
"ImageAnnotation with uuid ${annotation.uuid} already exists"
).left()
}
current[annotation.uuid] = annotation
annotations.value = current
return Unit.right()
}
Comment on lines +65 to +73
// Upsert: delete first (no-op if absent), then insert
imageAnnotationRepository.deleteImageAnnotation(annotation.uuid)
imageAnnotationRepository.saveImageAnnotation(annotation).onLeft { err ->
logger.warn("ImageSidecarIndexer: failed to upsert annotation ${annotation.uuid}: ${err.message}")
return@onLeft
}
measurementAnnotationRepository.deleteMeasurementsForImage(annotation.uuid)
measurementAnnotationRepository.saveMeasurements(annotation.uuid, measurements)
upserted++
Comment on lines +25 to +29
@Test
fun `isAuthenticated returns true after saveTokens`() = runTest {
store.saveTokens("access", "refresh", System.currentTimeMillis() + 3_600_000)
assertTrue(store.isAuthenticated())
}
tstapler and others added 3 commits May 18, 2026 07:40
Replaces ImageMeter app workflow end-to-end inside SteleKit:

- Annotated images are first-class `image_annotation` blocks in the
  Logseq graph; measurement data stored as block properties and
  queryable via the existing block system
- Compose Multiplatform annotation canvas (DrawScope + ZoomImage) with
  line, polygon, angle, text-label, and grid-ref tools; all coordinates
  stored in normalized [0,1] space; Path objects retained to hit 30fps
- Calibration fallback chain: BLE laser (±1mm) → manual reference →
  ARCore depth (±8–10cm, explicit warning) → EXIF focal-length (±15%) →
  Depth Anything V2 monocular ML (last resort)
- BLE laser rangefinder support: Leica DISTO and Bosch GLM protocol
  implementations; Kable/USB-serial/HID-keyboard-emulation device
  registry; Android ForegroundService for API 31+ BLE lifecycle
- Camera capture (Android/iOS CameraK), Android Photo Picker import
  (post-March-2025 API — photoslibrary scopes revoked), Google Drive
  import/export via Ktor REST v3, OAuth via Credential Manager
- Gallery view with tag filtering and sort; journal auto-insert on
  camera capture; `{{measure: image.label}}` block math references
- Platform sensors: GPS tagging, compass bearing auto-annotation,
  accelerometer tilt warning, iOS LiDAR depth stub
- SQLDelight migration 4: `image_annotations` + `measurement_annotations`
  tables; JSON sidecar at `.stelekit/images/<uuid>.measure.json` as
  portable ground truth (sidecar written before DB row for atomicity)
- 1582 tests passing, 0 detekt violations

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…RCore, ONNX, iOS sensors

- CameraX full capture: ProcessCameraProvider + ImageCapture bound to
  ProcessLifecycleOwner, suspendCancellableCoroutine bridge, ExifOrientationFixer
  applied post-capture, EXIF metadata extracted into PlatformImageFile.sensorData
- Kable 0.32.0 BLE scanning: LeicaDistoKableDevice and BoschGlmKableDevice with
  real GATT connect, MTU negotiation (100 bytes), characteristic observe loop,
  GATT-133 exponential backoff (5 retries, 2–60s), mandatory peripheral.disconnect()
  in finally; iOS equivalents via IOSKableBleScanner
- ARCore 1.46.0 depth: full ArSession lifecycle, DepthMode.AUTOMATIC, uint16→float
  depth map + confidence map extraction; optional AR feature in manifest
- ONNX Runtime 1.20.0: DepthModelDownloader via Android DownloadManager (survives
  process death), OnnxMonocularDepthEstimator with NNAPI/CPU fallback, 518×518
  ImageNet normalization, DepthEstimationPanel UI (Absent/Downloading/Ready/Failed)
- iOS CMMotionManager: device motion at 10Hz (pitch/roll), CLLocationManager for
  GPS + compass heading, callbackFlow emission
- iOS ARKit LiDAR: ARSession with sceneDepth semantics, CVPixelBuffer float32
  extraction, confidence map from ARConfidenceLevel
- macrobenchmark module: fixed pre-existing desugaring + resource-merge build gaps
- CancellationException rethrow guards added across all new catch blocks

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…lan, ADRs)

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
@tstapler tstapler force-pushed the stelekit-image-meter branch from c0cfb5a to 4e8b23c Compare May 18, 2026 14:40
tstapler and others added 2 commits May 18, 2026 08:58
…nMain

- Replace System.currentTimeMillis() with Clock.System.now().toEpochMilliseconds() in Google API classes
- Replace .toByteArray(Charsets.UTF_8) with .encodeToByteArray() in DriveExportService
- Remove @volatile annotations from SensorModule (Wasm/JS single-threaded)
- Replace String.format() with custom formatDecimals() helper in AnnotationEditorScreen
- Add missing wasmJsMain actual for ImageEncoder expect declaration
- Add BleError and AttachmentError branches to DomainErrorTest exhaustive when

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
…eKitApplication)

Conflict: both branches added imports to SteleKitApplication.kt —
main added WriteBehindQueue, branch added SensorModule/ARCore/BLE wiring.
Kept all imports from both sides.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants