fix(v2): preview rotation + crop aspect for 13.3" hardware
CI / test (push) Has been cancelled

Two related bugs that surfaced on the first 13.3" device's first photo:

1) Web-UI portrait preview was 90° sideways. DeviceApiController::
   renderBinToPng rotated whenever the device was Portrait — correct
   for V1 (landscape-native, Portrait => renderer rotated, so preview
   un-rotates) but wrong for V2 (portrait-native — the renderer
   doesn't rotate, so the preview shouldn't either). Now mirrors the
   render-pipeline check: rotate only when `orientation !==
   model->nativeOrientation()`. Two new functional tests pin the V2
   portrait and V2 landscape PNG dimensions to guard against
   regressions.

2) Cropped photo letterboxed on the 13.3" panel. CropEditor /
   StickerCanvas / FrameCard had V1 dimensions hardcoded (1600×960
   = 5:3 aspect). V2 is 4:3 (1200×1600 portrait / 1600×1200
   landscape), so a "full crop" came out the wrong shape and the
   server's white-canvas composite added bars. New `panelDims(model,
   orientation)` helper in @/types is the single source of truth on
   the frontend; matches DeviceModel::width/height on the server.
   Threaded `model` through Device serializer → Device type →
   UploadView → CropEditor / StickerCanvas, and HomeView → FrameCard.
   FrameCard tests updated to cover all four model × orientation
   placeholders.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-05-14 12:02:39 -04:00
parent b286a1f241
commit 081ca83613
11 changed files with 198 additions and 28 deletions
+2
View File
@@ -30,6 +30,7 @@
size="large"
:status="deviceStatus(devicesStore.devices[0])"
:orientation="devicesStore.devices[0].orientation"
:model="devicesStore.devices[0].model"
:thumbnailUrl="previewUrl(devicesStore.devices[0])"
:lastSync="lastSyncLabel(devicesStore.devices[0])"
:nextSync="nextSyncLabel(devicesStore.devices[0])"
@@ -59,6 +60,7 @@
size="large"
:status="deviceStatus(device)"
:orientation="device.orientation"
:model="device.model"
:thumbnailUrl="previewUrl(device)"
:lastSync="lastSyncLabel(device)"
:nextSync="nextSyncLabel(device)"
+6 -1
View File
@@ -30,6 +30,7 @@
v-if="step === 'crop' && uploadStore.originalUrl"
:src="uploadStore.originalUrl"
:orientation="contextOrientation"
:model="contextModel"
:device-name="contextDeviceName"
:initial-params="uploadStore.cropParams"
:initial-orientation="uploadStore.cropOrientation"
@@ -42,6 +43,7 @@
v-else-if="step === 'stickers' && uploadStore.croppedUrl"
:cropped-url="uploadStore.croppedUrl"
:orientation="effectiveOrientation"
:model="contextModel"
:stickers="uploadStore.stickers"
class="upload-view__stage"
@add-sticker="uploadStore.addSticker"
@@ -78,7 +80,7 @@ import { useUploadStore } from '@/stores/upload'
import { useDevicesStore } from '@/stores/devices'
import { useImagesStore } from '@/stores/images'
import { useToastStore } from '@/stores/toast'
import type { CropParams } from '@/types'
import type { CropParams, DeviceModel } from '@/types'
import CropEditor from '@/components/CropEditor.vue'
import StickerCanvas from '@/components/StickerCanvas.vue'
import DevicePicker from '@/components/DevicePicker.vue'
@@ -120,6 +122,9 @@ const contextDevice = computed(() =>
: devicesStore.devices[0]
)
const contextModel = computed<DeviceModel>(() =>
contextDevice.value?.model ?? 'v1'
)
const contextOrientation = computed<'landscape' | 'portrait'>(() =>
contextDevice.value?.orientation ?? 'landscape'
)