중복 탐지 - 이미지 근접쌍 밴드 색인 + 스트리밍 클러스터, 후보 검증(타일 다수결·크롭 해시) - 색 지문으로 색만 다른 오탐 제거, 회전·반전본 탐지(8변형 질의, 선택 옵션) - 오디오 지문(chromaprint)을 같은 디코드 패스에서 뽑아 레터박스 사본을 잡는다. 배경음만 같은 남남은 시각 확인 패스(트림 축 프레임 거리)로 걸러낸다 — 실측 9 vs 29. - 비교 재생(CompareView): 여러 사본을 offset 정렬해 나란히 재생 - 지문 생성 속도 조절(느림·보통·빠름) — 재시작 없이 반영 간단 편집 (원본을 직접 고침, Ctrl+Z로 되돌림) - 회전·반전: JPEG은 EXIF 방향 태그 2바이트만, MP4는 회전 행렬만 — 화질 손실 0. 방향값 전이표는 군의 성질(90도x4=제자리)로 검산하고, 실제 화소를 transpose=1과 비교해 "시계 방향"이 정말 시계인지 확인한다. - 자르기: 화면 좌표 기준(회전 메타가 붙어 있어도 어긋나지 않는다) - 구간 자르기: 스트림 복사라 무손실. 필름스트립 타임라인 + 손잡이 끌기 + 구간 미리보기. - 되돌릴 수 없는 편집(자르기·반전)은 원본을 .archive_trash/edits로 옮겨 보관 - 편집이 수정시각을 바꾸지 않는다 — 기본 정렬이 촬영일이라 항목이 튀면 다시 찾아야 한다 동시 재생 - 고른 영상을 최대 9개 격자에 놓고 함께 재생. 소리는 한 칸만. - 실시간 변환이 필요한 코덱은 2개까지만 — 그 이상은 ffmpeg가 기계를 멈춘다 그 외 - 별점(스냅샷 flags 남는 비트에 얹어 항목당 바이트 증가 0) - 태그: 검색 없이 클릭으로 붙이기, 세트+단축키, 이름 수정 시 붙은 항목에 전파 - 파일 로깅(tracing-appender) — 릴리스는 콘솔이 없어 stdout만으로는 원인을 못 찾았다 - ARM64 크로스 컴파일 스크립트(관리자 권한 없이 VS 카탈로그에서 조립) - DB 마이그레이션 v7~v10 Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
82 lines
2.4 KiB
TypeScript
82 lines
2.4 KiB
TypeScript
// Justified 레이아웃 — 그리디 행 채우기. 메인 스레드 동기 계산.
|
|
// (프로덕션 Tauri 빌드에서 module worker가 첫 메시지 후 응답하지 않는 이슈를 피하기 위해
|
|
// 워커 대신 동기 계산을 쓴다. 재계산은 리사이즈/줌/데이터 변경 시에만 발생한다.)
|
|
//
|
|
// 워커로 옮길 이유가 없다 — V8 실측: 1만 0.8ms / 10만 3.1ms / **100만 18.4ms**
|
|
// (aspectRatios 언팩 포함 ~20ms). 스크롤 중에는 아예 돌지 않고, 워커로 보내면
|
|
// boxes 15MB를 매번 되돌려 받아야 해서 오히려 느려진다.
|
|
|
|
export interface LayoutResult {
|
|
/** n*4: [x, y, w, h] */
|
|
boxes: Float32Array;
|
|
/** 행별 상단 y (마지막 요소 = 전체 높이) */
|
|
rowTops: Float32Array;
|
|
/** 행별 첫 항목 인덱스 */
|
|
rowFirst: Uint32Array;
|
|
totalHeight: number;
|
|
}
|
|
|
|
export function computeLayout(
|
|
ratios: Float32Array,
|
|
containerWidth: number,
|
|
targetRowHeight: number,
|
|
gap: number,
|
|
): LayoutResult {
|
|
const n = ratios.length;
|
|
const boxes = new Float32Array(n * 4);
|
|
const rowTopsArr: number[] = [];
|
|
const rowFirstArr: number[] = [];
|
|
|
|
const clamp = (r: number) => Math.min(4, Math.max(0.25, r || 1));
|
|
|
|
let y = 0;
|
|
let rowStart = 0;
|
|
let ratioSum = 0;
|
|
|
|
const flushRow = (end: number, justify: boolean) => {
|
|
const count = end - rowStart;
|
|
if (count <= 0) return;
|
|
const gaps = gap * (count - 1);
|
|
let h = targetRowHeight;
|
|
if (justify) {
|
|
h = (containerWidth - gaps) / ratioSum;
|
|
} else {
|
|
const natural = ratioSum * targetRowHeight + gaps;
|
|
if (natural > containerWidth) h = (containerWidth - gaps) / ratioSum;
|
|
}
|
|
let x = 0;
|
|
for (let i = rowStart; i < end; i++) {
|
|
const w = clamp(ratios[i]) * h;
|
|
boxes[i * 4] = x;
|
|
boxes[i * 4 + 1] = y;
|
|
boxes[i * 4 + 2] = w;
|
|
boxes[i * 4 + 3] = h;
|
|
x += w + gap;
|
|
}
|
|
rowTopsArr.push(y);
|
|
rowFirstArr.push(rowStart);
|
|
y += h + gap;
|
|
rowStart = end;
|
|
ratioSum = 0;
|
|
};
|
|
|
|
for (let i = 0; i < n; i++) {
|
|
ratioSum += clamp(ratios[i]);
|
|
const width = ratioSum * targetRowHeight + gap * (i - rowStart);
|
|
if (width >= containerWidth) {
|
|
flushRow(i + 1, true);
|
|
}
|
|
}
|
|
flushRow(n, false);
|
|
|
|
const totalHeight = y > 0 ? y - gap : 0;
|
|
rowTopsArr.push(totalHeight); // 마지막 = 전체 높이 (이진탐색 경계)
|
|
|
|
return {
|
|
boxes,
|
|
rowTops: new Float32Array(rowTopsArr),
|
|
rowFirst: new Uint32Array(rowFirstArr),
|
|
totalHeight,
|
|
};
|
|
}
|