// Modelos 3D do kit: o produto é montado com PEÇAS (formas com borda arredondada) que o Produto3D // desenha com luz de estúdio. Qualquer produto simples cabe aqui (fone, estojo, relógio, garrafa, // controle, caixa de som...): a IA escreve as peças, ou usa um dos modelos prontos abaixo. // Produto com forma complexa demais? Use ProdutoFoto com o recorte (PNG) ou a sequência de um giro 3D. export type V3 = [number, number, number]; export type Forma = 'caixa' | 'capsula' | 'elipsoide' | 'cilindro' | 'anel'; export type Material = 'cetim' | 'metal' | 'grafite' | 'brilho'; export type Peca = { forma: Forma; /** caixa/elipsoide: meias medidas x,y,z · capsula: [raio de baixo, altura, raio de cima] · * cilindro: [raio, meia altura, -] · anel: [raio maior, raio do tubo, -] */ tam: V3; /** arredondamento da borda (caixa e cilindro) */ raio?: number; pos?: V3; /** graus, ordem Y·X·Z */ rot?: V3; material?: Material; cor?: string; /** >0 funde com as peças anteriores (suavidade); <0 ENTALHA (cava um sulco nas anteriores) */ uniao?: number; /** guarda só um lado do plano y = altura (no espaço da peça): [+1 → acima | −1 → abaixo, altura] */ corte?: [number, number]; /** nome do grupo que o `animar` do Produto3D move junto */ grupo?: string; }; /** um modelo pronto, posicionado: {nome, pos, rot, escala, grupo, cor} */ export type UsoModelo = {nome: NomeModelo; pos?: V3; rot?: V3; escala?: number; grupo?: string; cor?: string; opcoes?: Record}; // ---------- matemática mínima (matriz 3×3 em linhas) ---------- export type M3 = number[]; // 9 números, linha a linha const rad = (g: number) => (g * Math.PI) / 180; export const mul = (a: M3, b: M3): M3 => { const r = new Array(9).fill(0); for (let i = 0; i < 3; i++) for (let j = 0; j < 3; j++) for (let k = 0; k < 3; k++) r[i * 3 + j] += a[i * 3 + k] * b[k * 3 + j]; return r; }; export const aplicar = (m: M3, v: V3): V3 => [ m[0] * v[0] + m[1] * v[1] + m[2] * v[2], m[3] * v[0] + m[4] * v[1] + m[5] * v[2], m[6] * v[0] + m[7] * v[1] + m[8] * v[2]]; export const ID: M3 = [1, 0, 0, 0, 1, 0, 0, 0, 1]; /** rotação em graus, aplicada na ordem Z, depois X, depois Y (R = Ry·Rx·Rz) */ export const rotacao = (r: V3 = [0, 0, 0]): M3 => { const [x, y, z] = r.map(rad); const Rx = [1, 0, 0, 0, Math.cos(x), -Math.sin(x), 0, Math.sin(x), Math.cos(x)]; const Ry = [Math.cos(y), 0, Math.sin(y), 0, 1, 0, -Math.sin(y), 0, Math.cos(y)]; const Rz = [Math.cos(z), -Math.sin(z), 0, Math.sin(z), Math.cos(z), 0, 0, 0, 1]; return mul(Ry, mul(Rx, Rz)); }; export const somar = (a: V3, b: V3): V3 => [a[0] + b[0], a[1] + b[1], a[2] + b[2]]; export const vezes = (a: V3, k: number): V3 => [a[0] * k, a[1] * k, a[2] * k]; // ---------- modelos prontos (a demo é um fone fictício; nenhum é cópia de produto real) ---------- const cetim = (o: Peca): Peca => ({material: 'cetim', ...o}); /** um fone intra-auricular: cabeça, bico, ponteira de silicone, tela de som, haste com sulco tocável. * Origem no meio do fone; a ponteira aponta para +x; a frente (sulco) para +z. ~1,6 de altura. * opcoes: {semPonteira: 1} tira a ponteira (pra animá-la separada com o modelo `ponteira`). */ const fone = (o: Record = {}): Peca[] => [ cetim({forma: 'elipsoide', tam: [0.33, 0.3, 0.29], pos: [0, 0.55, 0], rot: [0, 0, -8]}), cetim({forma: 'capsula', tam: [0.1, 1.12, 0.13], pos: [-0.07, -0.12, 0], rot: [0, 0, 5], uniao: 0.2}), cetim({forma: 'cilindro', tam: [0.105, 0.09, 0], raio: 0.04, pos: [0.3, 0.6, 0.03], rot: [0, 0, -80], uniao: 0.06}), // tela de som (entalhe escuro em cima, atrás) {forma: 'elipsoide', tam: [0.1, 0.045, 0.07], pos: [-0.12, 0.85, -0.02], rot: [0, 0, 22], material: 'grafite', uniao: -0.01}, // o sulco tocável na frente da haste: acende com o brilho do acento {forma: 'caixa', tam: [0.032, 0.17, 0.05], raio: 0.03, pos: [-0.075, -0.2, 0.125], rot: [0, 0, 5], material: 'brilho', uniao: -0.006}, // ponta da haste (microfone): anel de metal {forma: 'cilindro', tam: [0.096, 0.018, 0], raio: 0.008, pos: [-0.02, -0.66, 0], rot: [0, 0, 5], material: 'metal'}, ...(o.semPonteira === 1 ? [] : [ponteiraPeca([0.44, 0.61, 0.03], 0.85)]), ]; const ponteiraPeca = (pos: V3, k: number): Peca => ({forma: 'capsula', tam: [0.17 * k, 0.13 * k, 0.125 * k], pos, rot: [0, 0, -90], material: 'cetim', cor: '#E2DED6', uniao: 0.025}); /** ponteira de silicone avulsa, deitada no eixo x (tamanho k: 0,8 · 0,9 · 1 · 1,1) */ const ponteira = (o: Record = {}): Peca[] => [{...ponteiraPeca([0, 0, 0], o.k ?? 1), uniao: 0}]; /** estojo de carregar: base + tampa (grupo "tampa"). Dobradiça (pivô da tampa) em [0, 0,85, −0,4]. * Abrir: animar {grupo:'tampa', rot:[−100,0,0], pivo:[0,0.85,-0.4]} (× escala + pos). Origem no centro do fundo. ~1,9 × 1,36 × 0,8. */ const estojo = (): Peca[] => [ cetim({forma: 'caixa', tam: [0.95, 0.68, 0.4], raio: 0.34, pos: [0, 0.68, 0], corte: [-1, 0.165]}), cetim({forma: 'caixa', tam: [0.95, 0.68, 0.4], raio: 0.34, pos: [0, 0.68, 0], corte: [1, 0.18], grupo: 'tampa'}), {forma: 'elipsoide', tam: [0.028, 0.028, 0.012], pos: [0, 0.52, 0.4], material: 'grafite'}, ]; export const MODELOS = {fone, ponteira, estojo} as const; export type NomeModelo = keyof typeof MODELOS; /** expande os modelos prontos em peças, já com posição/rotação/escala aplicadas */ export const expandir = (usos: UsoModelo[] = []): Peca[] => usos.flatMap((u) => { const R = rotacao(u.rot), k = u.escala ?? 1, p0 = u.pos ?? [0, 0, 0]; return MODELOS[u.nome](u.opcoes).map((pc) => { // a rotação da peça vira matriz e é guardada como "rotM" (o Produto3D usa direto) const Rp = mul(R, rotacao(pc.rot)); return { ...pc, tam: vezes(pc.tam, k), raio: pc.raio !== undefined ? pc.raio * k : undefined, uniao: pc.uniao !== undefined ? pc.uniao * k : undefined, corte: pc.corte ? [pc.corte[0], pc.corte[1] * k] as [number, number] : undefined, pos: somar(p0, aplicar(R, vezes(pc.pos ?? [0, 0, 0], k))), rotM: Rp, cor: u.cor && pc.material === 'cetim' && !pc.cor ? u.cor : pc.cor, grupo: u.grupo && pc.grupo ? `${u.grupo}.${pc.grupo}` : (u.grupo ?? pc.grupo), } as Peca & {rotM: M3}; }); });