// Modelos 3D do kit: o produto é montado com PEÇAS (formas com borda arredondada) que o Produto3D // desenha com luz de estúdio escuro. Cada peça pode ter `abre`: para onde ela vai na VISTA EXPLODIDA. // A demo é uma lente fictícia (KAIRO 35); qualquer objeto de revolução ou de caixas cabe aqui // (relógio, fone, caixa de som, garrafa, controle, câmera): a IA escreve as peças. export type V3 = [number, number, number]; export type Forma = 'caixa' | 'capsula' | 'elipsoide' | 'cilindro' | 'anel' | 'estriado'; export type Material = 'anodizado' | 'metal' | 'borracha' | 'acento' | 'vidro'; export type Peca = { forma: Forma; /** caixa/elipsoide: meias medidas x,y,z · capsula: [raio de baixo, altura, raio de cima] · * cilindro: [raio, meia altura, -] · estriado: [raio, meia altura, nº de sulcos] · anel: [raio maior, raio do tubo, -] */ tam: V3; /** arredondamento da borda (caixa, cilindro, estriado) */ raio?: number; pos?: V3; /** graus, ordem Y·X·Z */ rot?: V3; material?: Material; cor?: string; /** >0 funde com as peças anteriores; <0 ENTALHA (cava as peças anteriores; o entalhe pinta a parede com a cor dele) */ 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; /** deslocamento da peça com a vista explodida em 100% (espaço do modelo) */ abre?: V3; }; 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[]; 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]]; /** 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 ---------- /** lente fotográfica: eixo óptico em +y (a frente), baioneta embaixo. Ø 1,47 × 1,7 de altura. * Grupos: baioneta · traseira · abertura · acento · foco · frente · vidro1 (frontal) · vidro2 · vidro3. * Os vidros 2 e 3 ficam escondidos dentro do corpo: só aparecem na vista explodida. */ const lente = (): Peca[] => [ {forma: 'cilindro', tam: [0.6, 0.05, 0], raio: 0.02, pos: [0, -1.0, 0], material: 'metal', grupo: 'baioneta', abre: [0, -0.95, 0]}, {forma: 'cilindro', tam: [0.66, 0.16, 0], raio: 0.03, pos: [0, -0.8, 0], material: 'anodizado', grupo: 'traseira', abre: [0, -0.62, 0]}, {forma: 'estriado', tam: [0.69, 0.1, 48], raio: 0.015, pos: [0, -0.53, 0], material: 'anodizado', grupo: 'abertura', abre: [0, -0.27, 0]}, {forma: 'cilindro', tam: [0.7, 0.02, 0], raio: 0.008, pos: [0, -0.405, 0], material: 'acento', grupo: 'acento', abre: [0, -0.09, 0]}, {forma: 'estriado', tam: [0.735, 0.26, 96], raio: 0.03, pos: [0, -0.12, 0], material: 'borracha', grupo: 'foco', abre: [0, 0, 0]}, {forma: 'cilindro', tam: [0.72, 0.24, 0], raio: 0.04, pos: [0, 0.4, 0], material: 'anodizado', grupo: 'frente', abre: [0, 0.62, 0]}, // o poço da frente: cava o corpo e pinta a parede de preto {forma: 'cilindro', tam: [0.6, 0.2, 0], pos: [0, 0.62, 0], material: 'borracha', cor: '#060607', uniao: -0.012, grupo: 'frente', abre: [0, 0.62, 0]}, {forma: 'anel', tam: [0.6, 0.026, 0], pos: [0, 0.64, 0], material: 'metal', grupo: 'frente', abre: [0, 0.62, 0]}, {forma: 'elipsoide', tam: [0.44, 0.09, 0.44], pos: [0, -0.55, 0], material: 'vidro', grupo: 'vidro3', abre: [0, -0.5, 0]}, {forma: 'elipsoide', tam: [0.5, 0.1, 0.5], pos: [0, 0.1, 0], material: 'vidro', grupo: 'vidro2', abre: [0, 0.42, 0]}, {forma: 'elipsoide', tam: [0.575, 0.13, 0.575], pos: [0, 0.46, 0], material: 'vidro', grupo: 'vidro1', abre: [0, 1.02, 0]}, ]; export const MODELOS = {lente} 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 & {rotM: M3})[] => usos.flatMap((u) => { const R = rotacao(u.rot), k = u.escala ?? 1, p0 = u.pos ?? [0, 0, 0]; return MODELOS[u.nome]().map((pc) => ({ ...pc, tam: (pc.forma === 'estriado' ? [pc.tam[0] * k, pc.tam[1] * k, pc.tam[2]] : vezes(pc.tam, k)) as V3, 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))), abre: pc.abre ? aplicar(R, vezes(pc.abre, k)) : undefined, rotM: mul(R, rotacao(pc.rot)), cor: u.cor && pc.material === 'anodizado' && !pc.cor ? u.cor : pc.cor, grupo: u.grupo && pc.grupo ? `${u.grupo}.${pc.grupo}` : (u.grupo ?? pc.grupo), })); });