Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
38 commits
Select commit Hold shift + click to select a range
843d28c
WIP
muukii Jan 30, 2026
68c82fd
Merge main into muukii/state-graph
muukii Jan 30, 2026
d9148de
WIP
muukii Jan 31, 2026
6ef35c3
Update
muukii Feb 2, 2026
9fcb1b0
Fix debounced crop layout update
muukii May 17, 2026
0230584
Migrate editor surfaces to SwiftUI
muukii May 22, 2026
147075b
Add SwiftUI demo validation flows
muukii May 22, 2026
f71c893
Update Pick image demo
muukii May 22, 2026
e90bb6d
Add PhotosCrop rotation slider
muukii May 22, 2026
85d0705
Improve Metal image preview background handling
muukii May 22, 2026
649c3a2
Disable fill when button is not enabled
muukii May 23, 2026
22c9afb
Relax renderer input color space assertions
muukii May 23, 2026
896a5f5
Add agents file
muukii May 23, 2026
a32ab38
Add render crop canonicalization
muukii May 23, 2026
5f56720
Consolidate demo crop flows around PhotosCrop
muukii May 23, 2026
2384737
Cancel debounce timer source
muukii May 23, 2026
fa45359
Remove ClassicImage editor
muukii May 23, 2026
c2da242
Move crop loading boundary into SwiftUICropView
muukii May 23, 2026
296d2ac
Fix PhotosCrop pinch interaction on device
muukii May 23, 2026
aba026d
Internalize UIKit editor components
muukii May 23, 2026
38c5511
[codex] Add cube LUT support (#285)
muukii May 23, 2026
b0e3d57
Disable scroll edge effect
muukii May 24, 2026
6ad4b43
Slider
muukii May 24, 2026
dfd2e91
Update UI
muukii May 24, 2026
9eea790
Slider
muukii May 24, 2026
6b3a4ba
ignore
muukii May 24, 2026
9b80235
Add PhotosCrop perspective correction
muukii May 24, 2026
b65d868
Fix PhotosCrop pinch interaction on device
muukii May 23, 2026
8d23cae
Internalize UIKit editor components
muukii May 23, 2026
108a57c
Disable scroll edge effect
muukii May 24, 2026
f9b73f5
Slider
muukii May 24, 2026
cdcbea8
Update UI
muukii May 24, 2026
2d71bd1
Slider
muukii May 24, 2026
c331f65
ignore
muukii May 24, 2026
efb5df5
Merge remote-tracking branch 'origin/muukii/state-graph' into codex/p…
muukii May 24, 2026
7e890b8
Cleanup project configuration
muukii May 24, 2026
ad6a7d6
Merge remote-tracking branch 'origin/muukii/state-graph' into codex/p…
muukii May 24, 2026
3d66f9f
Merge remote-tracking branch 'origin/main' into codex/photoscrop-pers…
muukii May 25, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 18 additions & 3 deletions .maestro/photos-crop.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -15,9 +15,20 @@ tags:
- assertVisible: "Cancel"
- assertVisible: "Done"
- assertVisible: "Rotate"
- assertVisible: "Flip Horizontal"
- assertVisible: "Flip Vertical"
- assertVisible: "Aspect Ratio"
- assertVisible: "Straighten"
- assertVisible: "Vertical"
- assertVisible: "Horizontal"
- assertVisible: "Rotation"
- assertNotVisible: "Reset"
- tapOn: "Flip Horizontal"
- assertVisible: "Reset"
- tapOn: "Flip Horizontal"
- tapOn: "Vertical"
- assertVisible: "Vertical Perspective"
- tapOn: "Straighten"
- tapOn: "Aspect Ratio"
- tapOn: "SQUARE"
- assertVisible: "Reset"
Expand All @@ -26,11 +37,12 @@ tags:
- assertVisible:
id: "photos.crop"
- assertVisible: "Reset"
- tapOn: "Reset"
- tapOn:
point: "50%,10%"
- assertNotVisible: "Reset"
- swipe:
start: 50%, 45%
end: 45%, 45%
end: 50%, 42%
duration: 300
- assertNotVisible: "Reset"
- tapOn: "Aspect Ratio"
Expand Down Expand Up @@ -65,7 +77,10 @@ tags:
- assertVisible: "Reset"
- tapOn:
text: "Rotate"
- tapOn: "Reset"
- tapOn:
point: "50%,10%"
- tapOn:
point: "50%,10%"
- assertNotVisible: "Reset"
- tapOn: "9:16"
- tapOn: "Done"
Expand Down
149 changes: 149 additions & 0 deletions Dev/Tests/BrightroomEngineTests/RendererTests.swift
Original file line number Diff line number Diff line change
Expand Up @@ -353,12 +353,16 @@ final class RenderCropTests: XCTestCase {
cropRect: .init(x: 0.2, y: 0.2, width: 99.6, height: 99.6)
)
editingCrop.rotation = .angle_90
editingCrop.flip = [.horizontal]
editingCrop.adjustmentAngle = .degrees(0.25)
editingCrop.perspectiveCorrection = .init(horizontal: 0.2, vertical: -0.35)

let crop = RenderCrop(editingCrop)

XCTAssertEqual(crop.rotation, .angle_90)
XCTAssertEqual(crop.flip, [.horizontal])
XCTAssertEqual(crop.adjustmentAngle, .degrees(0.25))
XCTAssertEqual(crop.perspectiveCorrection, .init(horizontal: 0.2, vertical: -0.35))
}

func testEditingCropRenderingEquivalenceUsesPixelCropContract() {
Expand Down Expand Up @@ -472,6 +476,102 @@ final class RenderCropRendererTests: XCTestCase {
try Self.assertEdgesAreDark(renderedImage)
}

func testHorizontalFlipMirrorsRenderCrop() throws {
let sourceImage = try Self.makeHorizontalColorImage()
let imageSource = ImageSource(cgImage: sourceImage)
let renderer = BrightRoomImageRenderer(source: imageSource, orientation: .up)

var crop = EditingCrop(imageSize: sourceImage.size)
crop.flip = [.horizontal]

renderer.edit = .init(
croppingRect: crop,
modifiers: [],
drawer: []
)

let renderedImage = try renderer.render().cgImage
let leftPixel = try Self.rgbaPixel(at: .init(x: 0, y: 0), in: renderedImage)
let rightPixel = try Self.rgbaPixel(at: .init(x: 1, y: 0), in: renderedImage)

XCTAssertGreaterThan(leftPixel.blue, leftPixel.red)
XCTAssertGreaterThan(rightPixel.red, rightPixel.blue)
}

func testPerspectiveCorrectionPreservesRenderCropSize() throws {
let sourceImage = try Self.makeImageWithBrightBorder(size: 16)
let imageSource = ImageSource(cgImage: sourceImage)
let renderer = BrightRoomImageRenderer(source: imageSource, orientation: .up)

var crop = Self.fractionalCrop(for: sourceImage)
crop.perspectiveCorrection = .init(horizontal: 0.3, vertical: -0.2)

renderer.edit = .init(
croppingRect: crop,
modifiers: [],
drawer: []
)

let renderedImage = try renderer.render().cgImage

XCTAssertEqual(renderedImage.width, 14)
XCTAssertEqual(renderedImage.height, 14)
}

func testPerspectiveTransformCreatesTrapezoidCanvas() throws {
let sourceImage = try Self.makeSolidColorImage(
size: .init(width: 20, height: 20),
red: 1,
green: 0,
blue: 0
)

let transformedImage = try sourceImage.perspectiveTransformed(.init(vertical: 1))

XCTAssertEqual(transformedImage.width, 20)
XCTAssertEqual(transformedImage.height, 20)

let centerPixel = try Self.rgbaPixel(at: .init(x: 10, y: 10), in: transformedImage)
let corners = try [
CGPoint(x: 0, y: 0),
CGPoint(x: 19, y: 0),
CGPoint(x: 0, y: 19),
CGPoint(x: 19, y: 19),
].map { try Self.rgbaPixel(at: $0, in: transformedImage) }

XCTAssertGreaterThan(centerPixel.red, 200)
XCTAssertTrue(corners.contains { $0.alpha < 16 })
}

func testPerspectiveCoverageRectUsesOnlyAlwaysCoveredArea() {
let rect = CGRect(x: 0, y: 0, width: 100, height: 80)

Self.assertRectEqual(
EditingCrop.PerspectiveCorrection(vertical: 1).axisAlignedCoverageRect(in: rect),
CGRect(x: 36, y: 0, width: 28, height: 80)
)
Self.assertRectEqual(
EditingCrop.PerspectiveCorrection(horizontal: 1).axisAlignedCoverageRect(in: rect),
CGRect(x: 0, y: 28.8, width: 100, height: 22.4)
)
Self.assertRectEqual(
EditingCrop.PerspectiveCorrection(horizontal: 1, vertical: 1).axisAlignedCoverageRect(in: rect),
CGRect(x: 36, y: 28.8, width: 28, height: 22.4)
)
}

private static func assertRectEqual(
_ actual: CGRect,
_ expected: CGRect,
file: StaticString = #filePath,
line: UInt = #line
) {
XCTAssertEqual(actual.origin.x, expected.origin.x, accuracy: 1e-6, file: file, line: line)
XCTAssertEqual(actual.origin.y, expected.origin.y, accuracy: 1e-6, file: file, line: line)
XCTAssertEqual(actual.size.width, expected.size.width, accuracy: 1e-6, file: file, line: line)
XCTAssertEqual(actual.size.height, expected.size.height, accuracy: 1e-6, file: file, line: line)
}

private static func fractionalCrop(for image: CGImage) -> EditingCrop {
EditingCrop(
imageSize: image.size,
Expand Down Expand Up @@ -512,6 +612,55 @@ final class RenderCropRendererTests: XCTestCase {
return try XCTUnwrap(context.makeImage())
}

private static func makeHorizontalColorImage() throws -> CGImage {
let bitmapInfo = CGBitmapInfo.byteOrder32Big.rawValue
| CGImageAlphaInfo.premultipliedLast.rawValue
let context = try XCTUnwrap(
CGContext(
data: nil,
width: 2,
height: 1,
bitsPerComponent: 8,
bytesPerRow: 2 * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: bitmapInfo
)
)

context.setFillColor(red: 1, green: 0, blue: 0, alpha: 1)
context.fill(.init(x: 0, y: 0, width: 1, height: 1))
context.setFillColor(red: 0, green: 0, blue: 1, alpha: 1)
context.fill(.init(x: 1, y: 0, width: 1, height: 1))

return try XCTUnwrap(context.makeImage())
}

private static func makeSolidColorImage(
size: PixelDimensions,
red: CGFloat,
green: CGFloat,
blue: CGFloat
) throws -> CGImage {
let bitmapInfo = CGBitmapInfo.byteOrder32Big.rawValue
| CGImageAlphaInfo.premultipliedLast.rawValue
let context = try XCTUnwrap(
CGContext(
data: nil,
width: size.width,
height: size.height,
bitsPerComponent: 8,
bytesPerRow: size.width * 4,
space: CGColorSpaceCreateDeviceRGB(),
bitmapInfo: bitmapInfo
)
)

context.setFillColor(red: red, green: green, blue: blue, alpha: 1)
context.fill(.init(origin: .zero, size: size.cgSize))

return try XCTUnwrap(context.makeImage())
}

private static func assertEdgesAreDark(
_ image: CGImage,
file: StaticString = #filePath,
Expand Down
Loading
Loading