Skip to content

Commit d65b491

Browse files
committed
Merge branch 'develop' into testflight
2 parents cce412d + a4f76e0 commit d65b491

3 files changed

Lines changed: 51 additions & 5 deletions

File tree

‎Keychy/Keychy/Presentation/KeyringMaker/Templates/Lenticular/ViewModels/LenticularVM+ImageLoad.swift‎

Lines changed: 34 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,9 @@ extension LenticularVM {
6767
}
6868

6969
/// 핀치 제스처 종료 시 스케일 적용 + 오프셋 재클램핑
70+
/// - cardSize는 composeAtlas의 좌표계 변환을 위해 VM에도 저장됨
7071
func applyScale(_ gestureScale: CGFloat, target: ImageTarget, cardSize: CGSize) {
72+
sourceCardSize = cardSize
7173
switch target {
7274
case .a:
7375
photoScaleA = min(max(photoScaleA * gestureScale, 1.0), 3.0)
@@ -81,7 +83,9 @@ extension LenticularVM {
8183
}
8284

8385
/// 드래그 제스처 종료 시 오프셋 적용 (클램핑 포함)
86+
/// - cardSize는 composeAtlas의 좌표계 변환을 위해 VM에도 저장됨
8487
func applyOffset(_ translation: CGSize, target: ImageTarget, cardSize: CGSize) {
88+
sourceCardSize = cardSize
8589
switch target {
8690
case .a:
8791
let maxOff = maxOffset(for: .a, scale: photoScaleA, cardSize: cardSize)
@@ -103,13 +107,22 @@ extension LenticularVM {
103107
// MARK: - 아틀라스 합성
104108

105109
/// 사용자 크롭(스케일/오프셋) 적용 후 A+B 아틀라스 합성 → bodyImage에 저장
110+
///
111+
/// offset이 `sourceCardSize` 좌표계 기준이므로 `targetSize` 좌표계로 변환이 필요하다.
112+
/// `sourceCardSize`는 `applyScale`/`applyOffset` 호출 시 VM이 자동으로 기록한다.
113+
/// - Precondition: 호출 전에 사용자가 최소 한 번 이상 카드를 편집했어야 한다 (sourceCardSize 기록).
106114
func composeAtlas() {
107115
guard let a = imageA, let b = imageB else { return }
108-
let targetSize = KeyringScale.maxSize(for: "Lenticular") // 300×390
116+
assert(sourceCardSize != .zero, "composeAtlas 호출 전 sourceCardSize가 기록되지 않음 — applyScale/applyOffset이 먼저 호출되어야 함")
117+
let targetSize = KeyringScale.maxSize(for: templateId)
109118

110119
let croppedA = cropImage(a, scale: photoScaleA, offset: photoOffsetA, targetSize: targetSize)
111120
let croppedB = cropImage(b, scale: photoScaleB, offset: photoOffsetB, targetSize: targetSize)
112121

122+
// 썸네일 등 편집 결과를 보여주는 뷰에서 재사용
123+
croppedImageA = croppedA
124+
croppedImageB = croppedB
125+
113126
bodyImage = TextureComposer.compose(
114127
imageA: croppedA,
115128
imageB: croppedB,
@@ -118,6 +131,10 @@ extension LenticularVM {
118131
}
119132

120133
/// 이미지에 사용자 크롭(스케일/오프셋)을 적용하여 targetSize로 렌더링
134+
///
135+
/// 핵심: 사용자의 offset은 `sourceCardSize` 좌표계에서 캡처된 값이므로,
136+
/// `targetSize` 좌표계로 그릴 때는 두 fillScale의 비율만큼 변환해야 한다.
137+
/// 변환 공식: `convertedOffset = offset × (fillScale_target / fillScale_source)`
121138
private func cropImage(
122139
_ image: UIImage,
123140
scale: CGFloat,
@@ -126,15 +143,29 @@ extension LenticularVM {
126143
) -> UIImage {
127144
let renderer = UIGraphicsImageRenderer(size: targetSize)
128145
return renderer.image { _ in
146+
// target 좌표계의 fillScale (실제 그리기에 사용)
129147
let fillScale = max(targetSize.width / image.size.width,
130148
targetSize.height / image.size.height)
131149
let drawSize = CGSize(
132150
width: image.size.width * fillScale * scale,
133151
height: image.size.height * fillScale * scale
134152
)
153+
154+
// source 좌표계의 fillScale (사용자 offset이 기반으로 하는 좌표계)
155+
let sourceFillScale = max(sourceCardSize.width / image.size.width,
156+
sourceCardSize.height / image.size.height)
157+
// 두 좌표계 비율 — source → target 변환 계수
158+
// sourceCardSize가 아직 기록되지 않았으면 변환 없이 원점에 그림 (이 경우 offset은 .zero일 것)
159+
let ratio = sourceFillScale > 0 ? (fillScale / sourceFillScale) : 1.0
160+
161+
let convertedOffset = CGSize(
162+
width: offset.width * ratio,
163+
height: offset.height * ratio
164+
)
165+
135166
let drawOrigin = CGPoint(
136-
x: (targetSize.width - drawSize.width) / 2 + offset.width,
137-
y: (targetSize.height - drawSize.height) / 2 + offset.height
167+
x: (targetSize.width - drawSize.width) / 2 + convertedOffset.width,
168+
y: (targetSize.height - drawSize.height) / 2 + convertedOffset.height
138169
)
139170
image.draw(in: CGRect(origin: drawOrigin, size: drawSize))
140171
}

‎Keychy/Keychy/Presentation/KeyringMaker/Templates/Lenticular/ViewModels/LenticularVM.swift‎

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,11 +55,22 @@ class LenticularVM: KeyringViewModelProtocol {
5555
var photoScaleB: CGFloat = 1.0
5656
var photoOffsetB: CGSize = .zero
5757

58+
/// 편집 뷰에서 제스처가 기반으로 하는 카드 크기
59+
/// `applyScale`/`applyOffset`이 호출될 때 자동 갱신되며,
60+
/// `composeAtlas`가 targetSize 좌표계로 변환할 때 사용한다.
61+
var sourceCardSize: CGSize = .zero
62+
5863
// MARK: - Body Image
5964
/// A+B 가로 합성 아틀라스 (셰이더가 UV로 좌/우 분리 샘플링)
6065
var bodyImage: UIImage? = nil
6166
var hookOffsetY: CGFloat = 0.0
6267

68+
// MARK: - Cropped Images (사용자 편집 반영본)
69+
/// 사용자가 선택/편집한 A/B를 아틀라스용으로 크롭한 결과
70+
/// FusionView 썸네일 등 편집 결과를 보여줘야 하는 곳에서 사용
71+
var croppedImageA: UIImage?
72+
var croppedImageB: UIImage?
73+
6374
// MARK: - Info Data
6475
var nameText: String = ""
6576
var maxTextCount: Int = 10
@@ -156,7 +167,10 @@ class LenticularVM: KeyringViewModelProtocol {
156167
photoOffsetA = .zero
157168
photoScaleB = 1.0
158169
photoOffsetB = .zero
170+
sourceCardSize = .zero
159171
bodyImage = nil
172+
croppedImageA = nil
173+
croppedImageB = nil
160174
selectedShimmerColor = .preset(.silver)
161175
selectedBorderColor = .preset(.silver)
162176
}

‎Keychy/Keychy/Presentation/KeyringMaker/Templates/Lenticular/Views/LenticularFusionView+Effects.swift‎

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,8 @@ extension LenticularFusionView {
9191
let thumbWidth = cardWidth * FusionLayout.thumbScale
9292
let thumbHeight = cardHeight * FusionLayout.thumbScale
9393

94-
if let imageA = viewModel.imageA {
94+
// 사용자 편집이 반영된 크롭 이미지만 사용 (원본 폴백 금지 — 기존 버그 재현 방지)
95+
if let imageA = viewModel.croppedImageA {
9596
Image(uiImage: imageA)
9697
.resizable()
9798
.scaledToFill()
@@ -105,7 +106,7 @@ extension LenticularFusionView {
105106
.opacity(phase == .merge ? 0.0 : (aScale > 0 ? 1.0 : 0.0))
106107
}
107108

108-
if let imageB = viewModel.imageB {
109+
if let imageB = viewModel.croppedImageB {
109110
Image(uiImage: imageB)
110111
.resizable()
111112
.scaledToFill()

0 commit comments

Comments
 (0)