110 lines
3.8 KiB
TypeScript
110 lines
3.8 KiB
TypeScript
import * as THREE from 'three';
|
|
import { DepthProcessor } from './DepthProcessor';
|
|
import type { DepthProvider } from '../types/depth';
|
|
|
|
type WorkerReply =
|
|
| { type: 'status'; id: number; message: string }
|
|
| { type: 'result'; id: number; width: number; height: number; values: ArrayBuffer }
|
|
| { type: 'error'; id: number; message: string };
|
|
|
|
type PendingRequest = {
|
|
id: number;
|
|
resolve: (texture: THREE.Texture) => void;
|
|
reject: (error: Error) => void;
|
|
};
|
|
|
|
export class AiDepthProvider implements DepthProvider {
|
|
private worker: Worker | null = null;
|
|
private pending: PendingRequest | null = null;
|
|
private requestId = 0;
|
|
|
|
constructor(private readonly onStatus: (message: string) => void) {}
|
|
|
|
async getDepthMap(image: HTMLImageElement): Promise<THREE.Texture> {
|
|
if (this.pending) throw new Error('AI depth 추론이 이미 진행 중입니다.');
|
|
const id = ++this.requestId;
|
|
return new Promise<THREE.Texture>((resolve, reject) => {
|
|
this.pending = { id, resolve, reject };
|
|
void this.captureAndSend(image, id).catch((error: unknown) => {
|
|
this.fail(id, error instanceof Error ? error : new Error('AI depth 생성에 실패했습니다.'));
|
|
});
|
|
});
|
|
}
|
|
|
|
cancel(): void {
|
|
const pending = this.pending;
|
|
this.pending = null;
|
|
this.requestId += 1;
|
|
this.worker?.terminate();
|
|
this.worker = null;
|
|
pending?.reject(new Error('AI depth 요청이 취소되었습니다.'));
|
|
}
|
|
|
|
private async captureAndSend(image: HTMLImageElement, id: number): Promise<void> {
|
|
const width = image.naturalWidth || image.width;
|
|
const height = image.naturalHeight || image.height;
|
|
if (!width || !height) throw new Error('사진 크기를 확인할 수 없습니다.');
|
|
const scale = Math.min(1, 1024 / Math.max(width, height));
|
|
const bitmap = await createImageBitmap(image, {
|
|
resizeWidth: Math.max(1, Math.round(width * scale)),
|
|
resizeHeight: Math.max(1, Math.round(height * scale)),
|
|
resizeQuality: 'high',
|
|
});
|
|
if (this.pending?.id !== id) {
|
|
bitmap.close();
|
|
return;
|
|
}
|
|
const worker = this.ensureWorker();
|
|
try {
|
|
worker.postMessage({ type: 'infer', id, image: bitmap }, [bitmap]);
|
|
} catch (error) {
|
|
bitmap.close();
|
|
throw error;
|
|
}
|
|
}
|
|
|
|
private ensureWorker(): Worker {
|
|
if (this.worker) return this.worker;
|
|
const worker = new Worker(new URL('./aiDepth.worker.ts', import.meta.url), { type: 'module' });
|
|
worker.onmessage = (event: MessageEvent<WorkerReply>) => {
|
|
const message = event.data;
|
|
if (this.pending?.id !== message.id) return;
|
|
if (message.type === 'status') {
|
|
this.onStatus(message.message);
|
|
} else if (message.type === 'result') {
|
|
const pending = this.pending;
|
|
this.pending = null;
|
|
try {
|
|
pending.resolve(DepthProcessor.fromNormalizedDepth(new Uint8Array(message.values), message.width, message.height));
|
|
} catch (error) {
|
|
pending.reject(error instanceof Error ? error : new Error('AI depth 결과를 읽지 못했습니다.'));
|
|
}
|
|
} else {
|
|
this.fail(message.id, new Error(message.message));
|
|
}
|
|
};
|
|
worker.onerror = (event) => {
|
|
this.resetWorker(worker);
|
|
this.fail(this.pending?.id ?? -1, new Error(event.message || 'AI depth worker 오류가 발생했습니다.'));
|
|
};
|
|
worker.onmessageerror = () => {
|
|
this.resetWorker(worker);
|
|
this.fail(this.pending?.id ?? -1, new Error('AI depth worker 결과를 읽지 못했습니다.'));
|
|
};
|
|
this.worker = worker;
|
|
return worker;
|
|
}
|
|
|
|
private fail(id: number, error: Error): void {
|
|
if (this.pending?.id !== id) return;
|
|
const pending = this.pending;
|
|
this.pending = null;
|
|
pending.reject(error);
|
|
}
|
|
|
|
private resetWorker(worker: Worker): void {
|
|
worker.terminate();
|
|
if (this.worker === worker) this.worker = null;
|
|
}
|
|
}
|