feat: unified 0 and 90 degree PDF envelope and category descriptors

This commit is contained in:
2026-07-08 19:52:34 +00:00
commit 9fece3f174
170 changed files with 27177 additions and 0 deletions
+121
View File
@@ -0,0 +1,121 @@
/**
* Utilitários de captura de canvas 3D — M9.3
*
* Funções determinísticas (sem dependência de React) para:
* - Extrair data URL de um canvas 2D/WebGL
* - Redimensionar a imagem para uma largura máxima (preservando aspect ratio)
* - Validar formato/qualidade
*
* Funciona com qualquer HTMLCanvasElement (incluindo R3F, Konva, D3).
* Para canvas WebGL, o browser exige que `preserveDrawingBuffer: true`
* seja passado ao `getContext('webgl2')` OU que a captura seja feita
* imediatamente após o frame renderizado. Como R3F usa o loop de
* animação do `useFrame`, a captura dentro do mesmo frame funciona.
*
* Dica: para WebGL, chamar `gl.flush()` ou renderizar um frame extra
* antes de `toDataURL` evita canvas em branco.
*/
export interface CaptureOptions {
/** Formato de saída. Padrão: 'png' */
format?: 'png' | 'jpeg' | 'webp';
/** Qualidade JPEG/WebP (01). Ignorado para PNG. Padrão: 0.92 */
quality?: number;
/** Largura máxima do PNG final (px). 0 = sem redimensionamento */
maxWidth?: number;
/** Altura máxima do PNG final (px). 0 = sem limite */
maxHeight?: number;
}
/**
* Converte um HTMLCanvasElement em data URL.
*
* Para PNG, o segundo argumento é ignorado. Para JPEG/WebP, `quality`
* controla a compressão (1 = sem perda, 0 = máxima compressão).
*/
export function canvasToDataURL(
canvas: HTMLCanvasElement,
format: 'png' | 'jpeg' | 'webp' = 'png',
quality = 0.92,
): string {
if (!canvas) throw new Error('canvas é null');
const mime = `image/${format}`;
return canvas.toDataURL(mime, quality);
}
/**
* Captura e opcionalmente redimensiona a imagem do canvas.
*
* Usa um canvas 2D temporário para escalar, preservando a proporção.
* Retorna a data URL final pronta para嵌入 em `<Image src=...>` ou PDF.
*/
export async function captureCanvasImage(
canvas: HTMLCanvasElement,
options: CaptureOptions = {},
): Promise<string> {
const { format = 'png', quality = 0.92, maxWidth = 0, maxHeight = 0 } = options;
const srcW = canvas.width;
const srcH = canvas.height;
let outW = srcW;
let outH = srcH;
if (maxWidth > 0 && maxHeight > 0) {
const ratio = Math.min(maxWidth / srcW, maxHeight / srcH);
outW = Math.round(srcW * ratio);
outH = Math.round(srcH * ratio);
} else if (maxWidth > 0) {
outW = Math.min(maxWidth, srcW);
outH = Math.round((outW / srcW) * srcH);
} else if (maxHeight > 0) {
outH = Math.min(maxHeight, srcH);
outW = Math.round((outH / srcH) * srcW);
}
if (outW === srcW && outH === srcH) {
return canvasToDataURL(canvas, format, quality);
}
const off = document.createElement('canvas');
off.width = outW;
off.height = outH;
const ctx = off.getContext('2d');
if (!ctx) throw new Error('Não foi possível criar contexto 2D');
ctx.imageSmoothingEnabled = true;
ctx.imageSmoothingQuality = 'high';
ctx.drawImage(canvas, 0, 0, outW, outH);
return off.toDataURL(`image/${format}`, quality);
}
/**
* Faz o download da imagem capturada.
*/
export function downloadImage(dataUrl: string, filename: string): void {
const link = document.createElement('a');
link.setAttribute('href', dataUrl);
link.setAttribute('download', filename);
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
}
/**
* Estima o tamanho da data URL em KB (útil para preview).
*/
export function estimateDataUrlSizeKB(dataUrl: string): number {
const commaIdx = dataUrl.indexOf(',');
if (commaIdx < 0) return 0;
const base64 = dataUrl.slice(commaIdx + 1);
return Math.round((base64.length * 3) / 4 / 1024);
}
/**
* Converte data URL em Blob (útil para upload ou PDF embed).
*/
export async function dataURLtoBlob(dataUrl: string): Promise<Blob> {
const res = await fetch(dataUrl);
return res.blob();
}