Skip to content

Commit 49de816

Browse files
committed
fix: align chrome screen geometry
1 parent 2f9ccf0 commit 49de816

6 files changed

Lines changed: 139 additions & 104 deletions

File tree

client/src/app/AppShell.tsx

Lines changed: 60 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -211,6 +211,59 @@ function buildAuthenticatedAssetUrl(
211211
return url.toString();
212212
}
213213

214+
function chromeStampNumber(value: number | undefined): string {
215+
return Number.isFinite(value) ? String(Math.round((value ?? 0) * 1000)) : "0";
216+
}
217+
218+
function chromeStampText(value: string | undefined | null): string {
219+
return (value ?? "").replace(/[^a-zA-Z0-9_.-]+/g, "_");
220+
}
221+
222+
function buildChromeProfileAssetStamp(profile: ChromeProfile | null): string {
223+
if (!profile) {
224+
return "";
225+
}
226+
227+
const geometryStamp = [
228+
profile.totalWidth,
229+
profile.totalHeight,
230+
profile.screenX,
231+
profile.screenY,
232+
profile.screenWidth,
233+
profile.screenHeight,
234+
profile.cornerRadius,
235+
]
236+
.map(chromeStampNumber)
237+
.join("x");
238+
const maskStamp = profile.hasScreenMask ? "mask" : "nomask";
239+
const buttonStamp = [...(profile.buttons ?? [])]
240+
.sort((left, right) => left.name.localeCompare(right.name))
241+
.map((button) =>
242+
[
243+
chromeStampText(button.name),
244+
chromeStampText(button.type),
245+
chromeStampText(button.imageName),
246+
chromeStampText(button.imageDownName),
247+
chromeStampText(button.anchor),
248+
chromeStampText(button.align),
249+
button.onTop ? "top" : "under",
250+
chromeStampNumber(button.x),
251+
chromeStampNumber(button.y),
252+
chromeStampNumber(button.width),
253+
chromeStampNumber(button.height),
254+
chromeStampNumber(button.normalOffset?.x),
255+
chromeStampNumber(button.normalOffset?.y),
256+
chromeStampNumber(button.rolloverOffset?.x),
257+
chromeStampNumber(button.rolloverOffset?.y),
258+
String(button.usagePage ?? ""),
259+
String(button.usage ?? ""),
260+
].join(","),
261+
)
262+
.join(";");
263+
264+
return [geometryStamp, maskStamp, buttonStamp].filter(Boolean).join(":");
265+
}
266+
214267
function shouldUseRemoteStreamDefault(apiRoot: string): boolean {
215268
if (apiRoot) {
216269
return true;
@@ -918,18 +971,9 @@ export function AppShell({
918971
button.name.toLowerCase() === "digital-crown",
919972
),
920973
);
921-
const chromeGeometryStamp = viewportChromeProfile
922-
? [
923-
viewportChromeProfile.totalWidth,
924-
viewportChromeProfile.totalHeight,
925-
viewportChromeProfile.screenX,
926-
viewportChromeProfile.screenY,
927-
viewportChromeProfile.screenWidth,
928-
viewportChromeProfile.screenHeight,
929-
]
930-
.map((value) => Math.round(value))
931-
.join("x")
932-
: "";
974+
const chromeGeometryStamp = buildChromeProfileAssetStamp(
975+
viewportChromeProfile,
976+
);
933977
const chromeAssetStamp = [
934978
selectedSimulator?.deviceTypeIdentifier,
935979
selectedSimulator?.deviceTypeName,
@@ -1718,18 +1762,10 @@ export function AppShell({
17181762
const chromeScreenStyle =
17191763
viewportChromeProfile && chromeScreenRect
17201764
? ({
1721-
left: viewportChromeProfile.hasScreenMask
1722-
? `calc(${(chromeScreenRect.x / viewportChromeProfile.totalWidth) * 100}% - 1px)`
1723-
: `${(chromeScreenRect.x / viewportChromeProfile.totalWidth) * 100}%`,
1724-
top: viewportChromeProfile.hasScreenMask
1725-
? `calc(${(chromeScreenRect.y / viewportChromeProfile.totalHeight) * 100}% - 1px)`
1726-
: `${(chromeScreenRect.y / viewportChromeProfile.totalHeight) * 100}%`,
1727-
width: viewportChromeProfile.hasScreenMask
1728-
? `calc(${(chromeScreenRect.width / viewportChromeProfile.totalWidth) * 100}% + 2px)`
1729-
: `${(chromeScreenRect.width / viewportChromeProfile.totalWidth) * 100}%`,
1730-
height: viewportChromeProfile.hasScreenMask
1731-
? `calc(${(chromeScreenRect.height / viewportChromeProfile.totalHeight) * 100}% + 2px)`
1732-
: `${(chromeScreenRect.height / viewportChromeProfile.totalHeight) * 100}%`,
1765+
left: `${(chromeScreenRect.x / viewportChromeProfile.totalWidth) * 100}%`,
1766+
top: `${(chromeScreenRect.y / viewportChromeProfile.totalHeight) * 100}%`,
1767+
width: `${(chromeScreenRect.width / viewportChromeProfile.totalWidth) * 100}%`,
1768+
height: `${(chromeScreenRect.height / viewportChromeProfile.totalHeight) * 100}%`,
17331769
borderRadius: viewportChromeProfile.hasScreenMask
17341770
? "0"
17351771
: (chromeScreenBorderRadius ?? "0"),

client/src/features/viewport/viewportMath.test.ts

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ describe("viewportMath", () => {
4343
expect(clamped).toEqual({ x: 0, y: -60 });
4444
});
4545

46-
it("fits device aspect inside chrome screen rect", () => {
46+
it("uses the exact chrome screen rect even when stream aspect differs", () => {
4747
const rect = computeChromeScreenRect(
4848
{
4949
cornerRadius: 40,
@@ -58,8 +58,12 @@ describe("viewportMath", () => {
5858
);
5959

6060
expect(rect).not.toBeNull();
61-
expect(rect?.x).toBeGreaterThanOrEqual(50);
62-
expect(rect?.y).toBeGreaterThanOrEqual(25);
61+
expect(rect).toEqual({
62+
height: 600,
63+
width: 300,
64+
x: 50,
65+
y: 25,
66+
});
6367
});
6468

6569
it("uses the full chrome screen when stream and profile aspect nearly match", () => {

client/src/features/viewport/viewportMath.ts

Lines changed: 15 additions & 30 deletions
Original file line numberDiff line numberDiff line change
@@ -88,44 +88,29 @@ export function mapDisplayedPointToNaturalOrientation(
8888

8989
export function computeChromeScreenRect(
9090
chromeProfile: ChromeProfile | null,
91-
deviceNaturalSize: Size | null,
91+
_deviceNaturalSize: Size | null,
9292
): ScreenRect | null {
9393
if (!chromeProfile) {
9494
return null;
9595
}
9696

97-
const profileAspect = chromeProfile.screenWidth / chromeProfile.screenHeight;
98-
const deviceAspect = deviceNaturalSize
99-
? deviceNaturalSize.width / deviceNaturalSize.height
100-
: profileAspect;
101-
if (!deviceAspect || !Number.isFinite(deviceAspect)) {
97+
if (
98+
!Number.isFinite(chromeProfile.screenX) ||
99+
!Number.isFinite(chromeProfile.screenY) ||
100+
!Number.isFinite(chromeProfile.screenWidth) ||
101+
!Number.isFinite(chromeProfile.screenHeight) ||
102+
chromeProfile.screenWidth <= 0 ||
103+
chromeProfile.screenHeight <= 0
104+
) {
102105
return null;
103106
}
104107

105-
const aspectDelta = Math.abs(deviceAspect - profileAspect) / profileAspect;
106-
if (aspectDelta <= 0.01) {
107-
return {
108-
height: chromeProfile.screenHeight,
109-
width: chromeProfile.screenWidth,
110-
x: chromeProfile.screenX,
111-
y: chromeProfile.screenY,
112-
};
113-
}
114-
115-
let width = chromeProfile.screenWidth;
116-
let height = width / deviceAspect;
117-
let x = chromeProfile.screenX;
118-
let y = chromeProfile.screenY;
119-
120-
if (height > chromeProfile.screenHeight) {
121-
height = chromeProfile.screenHeight;
122-
width = height * deviceAspect;
123-
x += (chromeProfile.screenWidth - width) / 2;
124-
} else {
125-
y += (chromeProfile.screenHeight - height) / 2;
126-
}
127-
128-
return { x, y, width, height };
108+
return {
109+
height: chromeProfile.screenHeight,
110+
width: chromeProfile.screenWidth,
111+
x: chromeProfile.screenX,
112+
y: chromeProfile.screenY,
113+
};
129114
}
130115

131116
export function computeChromeScreenBorderRadius(

ios/SimDeckStudio.xcodeproj/project.pbxproj

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -363,7 +363,7 @@
363363
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
364364
CODE_SIGN_ENTITLEMENTS = SimDeckStudio/SimDeckStudio.entitlements;
365365
CODE_SIGN_STYLE = Automatic;
366-
CURRENT_PROJECT_VERSION = 202605180047;
366+
CURRENT_PROJECT_VERSION = 202605180103;
367367
DEVELOPMENT_ASSET_PATHS = "";
368368
DEVELOPMENT_TEAM = CS838V553Y;
369369
ENABLE_PREVIEWS = YES;
@@ -392,7 +392,7 @@
392392
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
393393
CODE_SIGN_ENTITLEMENTS = SimDeckStudio/SimDeckStudio.entitlements;
394394
CODE_SIGN_STYLE = Automatic;
395-
CURRENT_PROJECT_VERSION = 202605180047;
395+
CURRENT_PROJECT_VERSION = 202605180103;
396396
DEVELOPMENT_ASSET_PATHS = "";
397397
DEVELOPMENT_TEAM = CS838V553Y;
398398
ENABLE_PREVIEWS = YES;

ios/SimDeckStudio/App/Models.swift

Lines changed: 46 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -252,7 +252,7 @@ struct ChromeProfile: Hashable, Decodable, Sendable {
252252
let buttons: [ChromeButtonProfile]?
253253

254254
var assetStamp: String {
255-
[
255+
var parts = [
256256
totalWidth,
257257
totalHeight,
258258
screenX,
@@ -262,23 +262,66 @@ struct ChromeProfile: Hashable, Decodable, Sendable {
262262
cornerRadius
263263
]
264264
.map { value in
265-
value.isFinite ? String(Int((value * 1000).rounded())) : "0"
265+
Self.stampValue(value)
266266
}
267-
.joined(separator: "x")
267+
parts.append(hasScreenMask == true ? "mask" : "nomask")
268+
parts.append(contentsOf: (buttons ?? [])
269+
.sorted { $0.name < $1.name }
270+
.map(\.assetStamp))
271+
return parts.joined(separator: "x")
272+
}
273+
274+
private static func stampValue(_ value: Double) -> String {
275+
value.isFinite ? String(Int((value * 1000).rounded())) : "0"
268276
}
269277
}
270278

271279
struct ChromeButtonProfile: Hashable, Decodable, Sendable {
272280
let name: String
273281
let label: String?
274282
let type: String?
283+
let imageName: String?
284+
let imageDownName: String?
275285
let x: Double
276286
let y: Double
277287
let width: Double
278288
let height: Double
289+
let anchor: String?
290+
let align: String?
279291
let usagePage: Int?
280292
let usage: Int?
281293
let onTop: Bool?
294+
295+
var assetStamp: String {
296+
[
297+
sanitized(name),
298+
sanitized(type),
299+
sanitized(imageName),
300+
sanitized(imageDownName),
301+
sanitized(anchor),
302+
sanitized(align),
303+
onTop == true ? "top" : "under",
304+
stampValue(x),
305+
stampValue(y),
306+
stampValue(width),
307+
stampValue(height),
308+
usagePage.map(String.init) ?? "",
309+
usage.map(String.init) ?? ""
310+
].joined(separator: ".")
311+
}
312+
313+
private func stampValue(_ value: Double) -> String {
314+
value.isFinite ? String(Int((value * 1000).rounded())) : "0"
315+
}
316+
317+
private func sanitized(_ value: String?) -> String {
318+
(value ?? "").map { character in
319+
character.isLetter || character.isNumber || character == "_" || character == "-" || character == "."
320+
? character
321+
: "_"
322+
}
323+
.reduce(into: "") { $0.append($1) }
324+
}
282325
}
283326

284327
struct SimulatorsResponse: Decodable, Sendable {

ios/SimDeckStudio/Views/SimulatorStreamView.swift

Lines changed: 9 additions & 42 deletions
Original file line numberDiff line numberDiff line change
@@ -1185,7 +1185,7 @@ private struct DeviceViewportLayout {
11851185
let profileSize = CGSize(width: CGFloat(chromeProfile.totalWidth), height: CGFloat(chromeProfile.totalHeight))
11861186
let shell = profileSize.aspectFit(in: viewport)
11871187
let scale = shell.width / profileSize.width
1188-
let screenRect = Self.chromeScreenRect(profile: chromeProfile, videoSize: videoSize)
1188+
let screenRect = Self.chromeScreenRect(profile: chromeProfile)
11891189
shellFrame = shell
11901190
chromeCoordinateScale = scale
11911191
screenFrame = CGRect(
@@ -1195,7 +1195,7 @@ private struct DeviceViewportLayout {
11951195
height: screenRect.height * scale
11961196
)
11971197
screenBackingFrame = screenFrame.insetBy(dx: -2, dy: -2)
1198-
videoFrame = screenFrame.insetBy(dx: -1, dy: -1)
1198+
videoFrame = screenFrame
11991199
screenCornerRadius = Self.screenCornerRadius(
12001200
profile: chromeProfile,
12011201
profileScreenRect: screenRect,
@@ -1228,46 +1228,13 @@ private struct DeviceViewportLayout {
12281228
)
12291229
}
12301230

1231-
private static func chromeScreenRect(profile: ChromeProfile, videoSize: CGSize) -> CGRect {
1232-
let profileScreenWidth = profile.screenWidth
1233-
let profileScreenHeight = profile.screenHeight
1234-
let profileScreenX = profile.screenX
1235-
let profileScreenY = profile.screenY
1236-
let profileAspect = profileScreenWidth / profileScreenHeight
1237-
let videoAspect = videoSize.width > 0 && videoSize.height > 0
1238-
? Double(videoSize.width / videoSize.height)
1239-
: profileAspect
1240-
guard profileAspect.isFinite, profileAspect > 0, videoAspect.isFinite, videoAspect > 0 else {
1241-
return CGRect(
1242-
x: CGFloat(profileScreenX),
1243-
y: CGFloat(profileScreenY),
1244-
width: CGFloat(profileScreenWidth),
1245-
height: CGFloat(profileScreenHeight)
1246-
)
1247-
}
1248-
1249-
let aspectDelta = abs(videoAspect - profileAspect) / profileAspect
1250-
if aspectDelta <= 0.01 {
1251-
return CGRect(
1252-
x: CGFloat(profileScreenX),
1253-
y: CGFloat(profileScreenY),
1254-
width: CGFloat(profileScreenWidth),
1255-
height: CGFloat(profileScreenHeight)
1256-
)
1257-
}
1258-
1259-
var width = profileScreenWidth
1260-
var height = width / videoAspect
1261-
var x = profileScreenX
1262-
var y = profileScreenY
1263-
if height > profileScreenHeight {
1264-
height = profileScreenHeight
1265-
width = height * videoAspect
1266-
x += (profileScreenWidth - width) / 2
1267-
} else {
1268-
y += (profileScreenHeight - height) / 2
1269-
}
1270-
return CGRect(x: CGFloat(x), y: CGFloat(y), width: CGFloat(width), height: CGFloat(height))
1231+
private static func chromeScreenRect(profile: ChromeProfile) -> CGRect {
1232+
CGRect(
1233+
x: CGFloat(profile.screenX),
1234+
y: CGFloat(profile.screenY),
1235+
width: CGFloat(profile.screenWidth),
1236+
height: CGFloat(profile.screenHeight)
1237+
)
12711238
}
12721239

12731240
private static func screenCornerRadius(profile: ChromeProfile, profileScreenRect: CGRect, scale: CGFloat) -> CGFloat {

0 commit comments

Comments
 (0)