mirror of
https://github.com/immich-app/immich.git
synced 2026-06-04 05:05:22 -04:00
Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9cd5d9e218 |
@@ -286,7 +286,11 @@
|
|||||||
{/snippet}
|
{/snippet}
|
||||||
</AdaptiveImage>
|
</AdaptiveImage>
|
||||||
|
|
||||||
{#if assetViewerManager.isFaceEditMode && assetViewerManager.imgRef}
|
{#if assetViewerManager.isFaceEditMode && assetViewerManager.imgRef && asset.width && asset.height}
|
||||||
<FaceEditor htmlElement={assetViewerManager.imgRef} {containerWidth} {containerHeight} assetId={asset.id} />
|
<FaceEditor
|
||||||
|
imageSize={{ width: asset.width, height: asset.height }}
|
||||||
|
containerSize={{ width: containerWidth, height: containerHeight }}
|
||||||
|
assetId={asset.id}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -308,9 +308,31 @@
|
|||||||
let containerHeight = $state(0);
|
let containerHeight = $state(0);
|
||||||
|
|
||||||
$effect(() => {
|
$effect(() => {
|
||||||
if (assetViewerManager.isFaceEditMode) {
|
if (!assetViewerManager.isFaceEditMode || !videoPlayer) {
|
||||||
videoPlayer?.pause();
|
return;
|
||||||
}
|
}
|
||||||
|
videoPlayer.pause();
|
||||||
|
|
||||||
|
const { videoWidth, videoHeight } = videoPlayer;
|
||||||
|
if (videoWidth === 0 || videoHeight === 0) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const canvas = document.createElement('canvas');
|
||||||
|
canvas.width = videoWidth;
|
||||||
|
canvas.height = videoHeight;
|
||||||
|
canvas.getContext('2d')?.drawImage(videoPlayer, 0, 0);
|
||||||
|
|
||||||
|
const img = new Image();
|
||||||
|
const onImageLoad = () => (assetViewerManager.imgRef = img);
|
||||||
|
img.addEventListener('load', onImageLoad);
|
||||||
|
img.src = canvas.toDataURL('image/png');
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
img.removeEventListener('load', onImageLoad);
|
||||||
|
img.src = '';
|
||||||
|
assetViewerManager.imgRef = undefined;
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
// The time is only refreshed on HLS fragment decode by default,
|
// The time is only refreshed on HLS fragment decode by default,
|
||||||
@@ -454,8 +476,12 @@
|
|||||||
</div>
|
</div>
|
||||||
{/if}
|
{/if}
|
||||||
|
|
||||||
{#if assetViewerManager.isFaceEditMode && videoPlayer}
|
{#if assetViewerManager.isFaceEditMode}
|
||||||
<FaceEditor htmlElement={videoPlayer} {containerWidth} {containerHeight} {assetId} />
|
<FaceEditor
|
||||||
|
imageSize={{ width: asset.width ?? 0, height: asset.height ?? 0 }}
|
||||||
|
containerSize={{ width: containerWidth, height: containerHeight }}
|
||||||
|
{assetId}
|
||||||
|
/>
|
||||||
{/if}
|
{/if}
|
||||||
{/if}
|
{/if}
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -4,7 +4,7 @@
|
|||||||
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
import { assetViewerManager } from '$lib/managers/asset-viewer-manager.svelte';
|
||||||
import FaceCreateTagModal from '$lib/modals/CreateFaceModal.svelte';
|
import FaceCreateTagModal from '$lib/modals/CreateFaceModal.svelte';
|
||||||
import { getPeopleThumbnailUrl } from '$lib/utils';
|
import { getPeopleThumbnailUrl } from '$lib/utils';
|
||||||
import { getNaturalSize, scaleToFit } from '$lib/utils/container-utils';
|
import { computeContentMetrics, mapContentRectToNatural, type Size } from '$lib/utils/container-utils';
|
||||||
import { handleError } from '$lib/utils/handle-error';
|
import { handleError } from '$lib/utils/handle-error';
|
||||||
import { createFace, getAllPeople, type PersonResponseDto } from '@immich/sdk';
|
import { createFace, getAllPeople, type PersonResponseDto } from '@immich/sdk';
|
||||||
import { Button, Input, modalManager, toastManager } from '@immich/ui';
|
import { Button, Input, modalManager, toastManager } from '@immich/ui';
|
||||||
@@ -14,13 +14,12 @@
|
|||||||
import { t } from 'svelte-i18n';
|
import { t } from 'svelte-i18n';
|
||||||
|
|
||||||
type Props = {
|
type Props = {
|
||||||
htmlElement: HTMLImageElement | HTMLVideoElement;
|
imageSize: Size;
|
||||||
containerWidth: number;
|
containerSize: Size;
|
||||||
containerHeight: number;
|
|
||||||
assetId: string;
|
assetId: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
let { htmlElement, containerWidth, containerHeight, assetId }: Props = $props();
|
let { imageSize, containerSize, assetId }: Props = $props();
|
||||||
|
|
||||||
let canvasEl: HTMLCanvasElement | undefined = $state();
|
let canvasEl: HTMLCanvasElement | undefined = $state();
|
||||||
let canvas: Canvas | undefined = $state();
|
let canvas: Canvas | undefined = $state();
|
||||||
@@ -54,7 +53,7 @@
|
|||||||
};
|
};
|
||||||
|
|
||||||
const setupCanvas = () => {
|
const setupCanvas = () => {
|
||||||
if (!canvasEl || !htmlElement) {
|
if (!canvasEl) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -86,17 +85,7 @@
|
|||||||
searchInputEl?.focus();
|
searchInputEl?.focus();
|
||||||
});
|
});
|
||||||
|
|
||||||
const imageContentMetrics = $derived.by(() => {
|
const imageContentMetrics = $derived(computeContentMetrics(imageSize, containerSize));
|
||||||
const natural = getNaturalSize(htmlElement);
|
|
||||||
const container = { width: containerWidth, height: containerHeight };
|
|
||||||
const { width: contentWidth, height: contentHeight } = scaleToFit(natural, container);
|
|
||||||
return {
|
|
||||||
contentWidth,
|
|
||||||
contentHeight,
|
|
||||||
offsetX: (containerWidth - contentWidth) / 2,
|
|
||||||
offsetY: (containerHeight - contentHeight) / 2,
|
|
||||||
};
|
|
||||||
});
|
|
||||||
|
|
||||||
const setDefaultFaceRectanglePosition = (faceRect: Rect) => {
|
const setDefaultFaceRectanglePosition = (faceRect: Rect) => {
|
||||||
const { offsetX, offsetY } = imageContentMetrics;
|
const { offsetX, offsetY } = imageContentMetrics;
|
||||||
@@ -116,8 +105,8 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
canvas.setDimensions({
|
canvas.setDimensions({
|
||||||
width: containerWidth,
|
width: containerSize.width,
|
||||||
height: containerHeight,
|
height: containerSize.height,
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!faceRect) {
|
if (!faceRect) {
|
||||||
@@ -175,11 +164,11 @@
|
|||||||
};
|
};
|
||||||
const selectorWidth = faceSelectorEl.offsetWidth;
|
const selectorWidth = faceSelectorEl.offsetWidth;
|
||||||
const chromeHeight = faceSelectorEl.offsetHeight - scrollableListEl.offsetHeight;
|
const chromeHeight = faceSelectorEl.offsetHeight - scrollableListEl.offsetHeight;
|
||||||
const listHeight = Math.min(MAX_LIST_HEIGHT, containerHeight - gap * 2 - chromeHeight);
|
const listHeight = Math.min(MAX_LIST_HEIGHT, containerSize.height - gap * 2 - chromeHeight);
|
||||||
const selectorHeight = listHeight + chromeHeight;
|
const selectorHeight = listHeight + chromeHeight;
|
||||||
|
|
||||||
const clampTop = (top: number) => clamp(top, gap, containerHeight - selectorHeight - gap);
|
const clampTop = (top: number) => clamp(top, gap, containerSize.height - selectorHeight - gap);
|
||||||
const clampLeft = (left: number) => clamp(left, gap, containerWidth - selectorWidth - gap);
|
const clampLeft = (left: number) => clamp(left, gap, containerSize.width - selectorWidth - gap);
|
||||||
|
|
||||||
const overlapArea = (position: { top: number; left: number }) => {
|
const overlapArea = (position: { top: number; left: number }) => {
|
||||||
const selectorRight = position.left + selectorWidth;
|
const selectorRight = position.left + selectorWidth;
|
||||||
@@ -238,61 +227,42 @@
|
|||||||
});
|
});
|
||||||
|
|
||||||
const getFaceCroppedCoordinates = () => {
|
const getFaceCroppedCoordinates = () => {
|
||||||
if (!faceRect || !htmlElement) {
|
if (!faceRect || imageContentMetrics.contentWidth === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const { left, top, width, height } = faceRect.getBoundingRect();
|
const imageRect = mapContentRectToNatural(faceRect.getBoundingRect(), imageContentMetrics, imageSize);
|
||||||
const { offsetX, offsetY, contentWidth, contentHeight } = imageContentMetrics;
|
|
||||||
const natural = getNaturalSize(htmlElement);
|
|
||||||
|
|
||||||
const scaleX = natural.width / contentWidth;
|
|
||||||
const scaleY = natural.height / contentHeight;
|
|
||||||
const imageX = (left - offsetX) * scaleX;
|
|
||||||
const imageY = (top - offsetY) * scaleY;
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
imageWidth: natural.width,
|
imageWidth: imageSize.width,
|
||||||
imageHeight: natural.height,
|
imageHeight: imageSize.height,
|
||||||
x: Math.floor(imageX),
|
x: Math.floor(imageRect.left),
|
||||||
y: Math.floor(imageY),
|
y: Math.floor(imageRect.top),
|
||||||
width: Math.floor(width * scaleX),
|
width: Math.floor(imageRect.width),
|
||||||
height: Math.floor(height * scaleY),
|
height: Math.floor(imageRect.height),
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
type FaceCoordinates = NonNullable<ReturnType<typeof getFaceCroppedCoordinates>>;
|
type FaceCoordinates = NonNullable<ReturnType<typeof getFaceCroppedCoordinates>>;
|
||||||
|
|
||||||
const getFacePreviewUrl = (data: FaceCoordinates) => {
|
const getFacePreviewUrl = (data: FaceCoordinates) => {
|
||||||
if (!htmlElement) {
|
const imgRef = assetViewerManager.imgRef;
|
||||||
|
if (!imgRef || imageContentMetrics.contentWidth === 0) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
const natural = getNaturalSize(htmlElement);
|
const scaleX = imgRef.naturalWidth / imageSize.width;
|
||||||
if (natural.width <= 0 || natural.height <= 0) {
|
const scaleY = imgRef.naturalHeight / imageSize.height;
|
||||||
return;
|
const x = clamp(Math.floor(data.x * scaleX), 0, imgRef.naturalWidth - 1);
|
||||||
}
|
const y = clamp(Math.floor(data.y * scaleY), 0, imgRef.naturalHeight - 1);
|
||||||
|
const width = clamp(Math.floor(data.width * scaleX), 1, imgRef.naturalWidth - x);
|
||||||
const x = clamp(data.x, 0, natural.width - 1);
|
const height = clamp(Math.floor(data.height * scaleY), 1, imgRef.naturalHeight - y);
|
||||||
const y = clamp(data.y, 0, natural.height - 1);
|
|
||||||
const width = clamp(data.width, 1, natural.width - x);
|
|
||||||
const height = clamp(data.height, 1, natural.height - y);
|
|
||||||
|
|
||||||
if (width <= 0 || height <= 0) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
const canvas = document.createElement('canvas');
|
const canvas = document.createElement('canvas');
|
||||||
canvas.width = width;
|
canvas.width = width;
|
||||||
canvas.height = height;
|
canvas.height = height;
|
||||||
|
|
||||||
const context = canvas.getContext('2d');
|
|
||||||
if (!context) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
try {
|
try {
|
||||||
context.drawImage(htmlElement, x, y, width, height, 0, 0, width, height);
|
canvas.getContext('2d')?.drawImage(imgRef, x, y, width, height, 0, 0, width, height);
|
||||||
return canvas.toDataURL('image/png');
|
return canvas.toDataURL('image/png');
|
||||||
} catch {
|
} catch {
|
||||||
return;
|
return;
|
||||||
|
|||||||
@@ -1,18 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
getContentMetrics,
|
computeContentMetrics,
|
||||||
getNaturalSize,
|
getNaturalSize,
|
||||||
|
mapContentRectToNatural,
|
||||||
mapNormalizedRectToContent,
|
mapNormalizedRectToContent,
|
||||||
mapNormalizedToContent,
|
mapNormalizedToContent,
|
||||||
scaleToCover,
|
scaleToCover,
|
||||||
scaleToFit,
|
scaleToFit,
|
||||||
} from '$lib/utils/container-utils';
|
} from '$lib/utils/container-utils';
|
||||||
|
|
||||||
const mockImage = (props: {
|
const mockImage = (props: { naturalWidth: number; naturalHeight: number }): HTMLImageElement =>
|
||||||
naturalWidth: number;
|
props as unknown as HTMLImageElement;
|
||||||
naturalHeight: number;
|
|
||||||
width: number;
|
|
||||||
height: number;
|
|
||||||
}): HTMLImageElement => props as unknown as HTMLImageElement;
|
|
||||||
|
|
||||||
const mockVideo = (props: {
|
const mockVideo = (props: {
|
||||||
videoWidth: number;
|
videoWidth: number;
|
||||||
@@ -49,48 +46,85 @@ describe('scaleToFit', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getContentMetrics', () => {
|
describe('computeContentMetrics', () => {
|
||||||
it('should compute zero offsets when aspect ratios match', () => {
|
it('should return zero metrics for zero-width content', () => {
|
||||||
const img = mockImage({ naturalWidth: 1600, naturalHeight: 900, width: 800, height: 450 });
|
expect(computeContentMetrics({ width: 0, height: 1080 }, { width: 800, height: 600 })).toEqual({
|
||||||
expect(getContentMetrics(img)).toEqual({
|
contentWidth: 0,
|
||||||
|
contentHeight: 0,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return zero metrics for zero-height content', () => {
|
||||||
|
expect(computeContentMetrics({ width: 1920, height: 0 }, { width: 800, height: 600 })).toEqual({
|
||||||
|
contentWidth: 0,
|
||||||
|
contentHeight: 0,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should center wide content vertically', () => {
|
||||||
|
expect(computeContentMetrics({ width: 2000, height: 1000 }, { width: 800, height: 600 })).toEqual({
|
||||||
|
contentWidth: 800,
|
||||||
|
contentHeight: 400,
|
||||||
|
offsetX: 0,
|
||||||
|
offsetY: 100,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should center tall content horizontally', () => {
|
||||||
|
expect(computeContentMetrics({ width: 1000, height: 2000 }, { width: 800, height: 600 })).toEqual({
|
||||||
|
contentWidth: 300,
|
||||||
|
contentHeight: 600,
|
||||||
|
offsetX: 250,
|
||||||
|
offsetY: 0,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should produce zero offsets when aspect ratios match', () => {
|
||||||
|
expect(computeContentMetrics({ width: 1600, height: 900 }, { width: 800, height: 450 })).toEqual({
|
||||||
contentWidth: 800,
|
contentWidth: 800,
|
||||||
contentHeight: 450,
|
contentHeight: 450,
|
||||||
offsetX: 0,
|
offsetX: 0,
|
||||||
offsetY: 0,
|
offsetY: 0,
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
it('should compute horizontal letterbox offsets for tall image', () => {
|
describe('mapContentRectToNatural', () => {
|
||||||
const img = mockImage({ naturalWidth: 1000, naturalHeight: 2000, width: 800, height: 600 });
|
it('should map a full-content rect back to natural size', () => {
|
||||||
const metrics = getContentMetrics(img);
|
const metrics = { contentWidth: 800, contentHeight: 400, offsetX: 0, offsetY: 100 };
|
||||||
expect(metrics.contentWidth).toBe(300);
|
const rect = mapContentRectToNatural({ left: 0, top: 100, width: 800, height: 400 }, metrics, {
|
||||||
expect(metrics.contentHeight).toBe(600);
|
width: 2000,
|
||||||
expect(metrics.offsetX).toBe(250);
|
height: 1000,
|
||||||
expect(metrics.offsetY).toBe(0);
|
});
|
||||||
|
expect(rect).toEqual({ left: 0, top: 0, width: 2000, height: 1000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should compute vertical letterbox offsets for wide image', () => {
|
it('should map a centered sub-rect to natural coordinates', () => {
|
||||||
const img = mockImage({ naturalWidth: 2000, naturalHeight: 1000, width: 800, height: 600 });
|
const metrics = { contentWidth: 800, contentHeight: 400, offsetX: 0, offsetY: 100 };
|
||||||
const metrics = getContentMetrics(img);
|
const rect = mapContentRectToNatural({ left: 200, top: 200, width: 400, height: 200 }, metrics, {
|
||||||
expect(metrics.contentWidth).toBe(800);
|
width: 2000,
|
||||||
expect(metrics.contentHeight).toBe(400);
|
height: 1000,
|
||||||
expect(metrics.offsetX).toBe(0);
|
});
|
||||||
expect(metrics.offsetY).toBe(100);
|
expect(rect).toEqual({ left: 500, top: 250, width: 1000, height: 500 });
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should use clientWidth/clientHeight for video elements', () => {
|
it('should handle letterboxed content with horizontal offset', () => {
|
||||||
const video = mockVideo({ videoWidth: 1920, videoHeight: 1080, clientWidth: 800, clientHeight: 600 });
|
const metrics = { contentWidth: 300, contentHeight: 600, offsetX: 250, offsetY: 0 };
|
||||||
const metrics = getContentMetrics(video);
|
const rect = mapContentRectToNatural({ left: 250, top: 0, width: 300, height: 600 }, metrics, {
|
||||||
expect(metrics.contentWidth).toBe(800);
|
width: 1000,
|
||||||
expect(metrics.contentHeight).toBe(450);
|
height: 2000,
|
||||||
expect(metrics.offsetX).toBe(0);
|
});
|
||||||
expect(metrics.offsetY).toBe(75);
|
expect(rect).toEqual({ left: 0, top: 0, width: 1000, height: 2000 });
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('getNaturalSize', () => {
|
describe('getNaturalSize', () => {
|
||||||
it('should return naturalWidth/naturalHeight for images', () => {
|
it('should return naturalWidth/naturalHeight for images', () => {
|
||||||
const img = mockImage({ naturalWidth: 4000, naturalHeight: 3000, width: 800, height: 600 });
|
const img = mockImage({ naturalWidth: 4000, naturalHeight: 3000 });
|
||||||
expect(getNaturalSize(img)).toEqual({ width: 4000, height: 3000 });
|
expect(getNaturalSize(img)).toEqual({ width: 4000, height: 3000 });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|||||||
@@ -49,13 +49,6 @@ export const scaleToFit = (dimensions: Size, container: Size): Size => {
|
|||||||
};
|
};
|
||||||
};
|
};
|
||||||
|
|
||||||
const getElementSize = (element: HTMLImageElement | HTMLVideoElement): Size => {
|
|
||||||
if (element instanceof HTMLVideoElement) {
|
|
||||||
return { width: element.clientWidth, height: element.clientHeight };
|
|
||||||
}
|
|
||||||
return { width: element.width, height: element.height };
|
|
||||||
};
|
|
||||||
|
|
||||||
export const getNaturalSize = (element: HTMLImageElement | HTMLVideoElement): Size => {
|
export const getNaturalSize = (element: HTMLImageElement | HTMLVideoElement): Size => {
|
||||||
if (element instanceof HTMLVideoElement) {
|
if (element instanceof HTMLVideoElement) {
|
||||||
return { width: element.videoWidth, height: element.videoHeight };
|
return { width: element.videoWidth, height: element.videoHeight };
|
||||||
@@ -63,17 +56,18 @@ export const getNaturalSize = (element: HTMLImageElement | HTMLVideoElement): Si
|
|||||||
return { width: element.naturalWidth, height: element.naturalHeight };
|
return { width: element.naturalWidth, height: element.naturalHeight };
|
||||||
};
|
};
|
||||||
|
|
||||||
export const getContentMetrics = (element: HTMLImageElement | HTMLVideoElement): ContentMetrics => {
|
export function computeContentMetrics(imageSize: Size, containerSize: Size): ContentMetrics {
|
||||||
const natural = getNaturalSize(element);
|
if (imageSize.width === 0 || imageSize.height === 0) {
|
||||||
const client = getElementSize(element);
|
return { contentWidth: 0, contentHeight: 0, offsetX: 0, offsetY: 0 };
|
||||||
const { width: contentWidth, height: contentHeight } = scaleToFit(natural, client);
|
}
|
||||||
|
const { width: contentWidth, height: contentHeight } = scaleToFit(imageSize, containerSize);
|
||||||
return {
|
return {
|
||||||
contentWidth,
|
contentWidth,
|
||||||
contentHeight,
|
contentHeight,
|
||||||
offsetX: (client.width - contentWidth) / 2,
|
offsetX: (containerSize.width - contentWidth) / 2,
|
||||||
offsetY: (client.height - contentHeight) / 2,
|
offsetY: (containerSize.height - contentHeight) / 2,
|
||||||
};
|
};
|
||||||
};
|
}
|
||||||
|
|
||||||
export function mapNormalizedToContent(point: Point, sizeOrMetrics: Size | ContentMetrics): Point {
|
export function mapNormalizedToContent(point: Point, sizeOrMetrics: Size | ContentMetrics): Point {
|
||||||
if ('contentWidth' in sizeOrMetrics) {
|
if ('contentWidth' in sizeOrMetrics) {
|
||||||
@@ -109,3 +103,25 @@ export function mapNormalizedRectToContent(
|
|||||||
height: br.y - tl.y,
|
height: br.y - tl.y,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function mapContentToNatural(point: Point, metrics: ContentMetrics, naturalSize: Size): Point {
|
||||||
|
return {
|
||||||
|
x: ((point.x - metrics.offsetX) / metrics.contentWidth) * naturalSize.width,
|
||||||
|
y: ((point.y - metrics.offsetY) / metrics.contentHeight) * naturalSize.height,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export function mapContentRectToNatural(rect: Rect, metrics: ContentMetrics, naturalSize: Size): Rect {
|
||||||
|
const topLeft = mapContentToNatural({ x: rect.left, y: rect.top }, metrics, naturalSize);
|
||||||
|
const bottomRight = mapContentToNatural(
|
||||||
|
{ x: rect.left + rect.width, y: rect.top + rect.height },
|
||||||
|
metrics,
|
||||||
|
naturalSize,
|
||||||
|
);
|
||||||
|
return {
|
||||||
|
top: topLeft.y,
|
||||||
|
left: topLeft.x,
|
||||||
|
width: bottomRight.x - topLeft.x,
|
||||||
|
height: bottomRight.y - topLeft.y,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user