primocode 10.16.0 → 10.17.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (237) hide show
  1. package/LICENSES/edit-ia-pro.txt +32 -0
  2. package/LICENSES/fontes-OFL.txt +476 -0
  3. package/LICENSES/hyperframes-Apache-2.0.txt +205 -0
  4. package/LICENSES/permanent-marker-Apache-2.0.txt +204 -0
  5. package/LICENSES/selfie-multiclass-Apache-2.0.txt +227 -0
  6. package/LICENSES/three-MIT.txt +23 -0
  7. package/LICENSES/video-use-MIT.txt +24 -0
  8. package/LICENSES/woven-sfx-CC0.txt +16 -0
  9. package/LICENSES/yunet-MIT.txt +21 -0
  10. package/README.md +2 -2
  11. package/bin/primocode.js +28 -8
  12. package/editor/LICENSE.md +11 -0
  13. package/editor/README.md +321 -0
  14. package/editor/SKILL.md +307 -0
  15. package/editor/THIRD_PARTY_NOTICES.md +228 -0
  16. package/editor/assets/baixar.json +591 -0
  17. package/editor/assets/fonts/anton/OFL.txt +93 -0
  18. package/editor/assets/fonts/caveat/OFL.txt +93 -0
  19. package/editor/assets/fonts/figtree/OFL.txt +93 -0
  20. package/editor/assets/fonts/fonts.json +10 -0
  21. package/editor/assets/fonts/montserrat/OFL.txt +93 -0
  22. package/editor/assets/fonts/permanentmarker/LICENSE.txt +202 -0
  23. package/editor/assets/models/SELFIE_MULTICLASS_LICENSE.txt +227 -0
  24. package/editor/assets/models/YUNET_LICENSE.txt +21 -0
  25. package/editor/assets/sfx/licencas/bass_drop.txt +32 -0
  26. package/editor/assets/sfx/licencas/cash.txt +76 -0
  27. package/editor/assets/sfx/licencas/charge.txt +31 -0
  28. package/editor/assets/sfx/licencas/click.txt +46 -0
  29. package/editor/assets/sfx/licencas/coin.txt +50 -0
  30. package/editor/assets/sfx/licencas/count.txt +47 -0
  31. package/editor/assets/sfx/licencas/data.txt +31 -0
  32. package/editor/assets/sfx/licencas/error.txt +32 -0
  33. package/editor/assets/sfx/licencas/flash.txt +31 -0
  34. package/editor/assets/sfx/licencas/gears.txt +82 -0
  35. package/editor/assets/sfx/licencas/keyboard.txt +48 -0
  36. package/editor/assets/sfx/licencas/metal_hit.txt +62 -0
  37. package/editor/assets/sfx/licencas/metal_riser.txt +61 -0
  38. package/editor/assets/sfx/licencas/mouse_click.txt +50 -0
  39. package/editor/assets/sfx/licencas/notification.txt +54 -0
  40. package/editor/assets/sfx/licencas/pop.txt +38 -0
  41. package/editor/assets/sfx/licencas/rewind.txt +32 -0
  42. package/editor/assets/sfx/licencas/rise.txt +46 -0
  43. package/editor/assets/sfx/licencas/sinteticos.txt +21 -0
  44. package/editor/assets/sfx/licencas/snap.txt +50 -0
  45. package/editor/assets/sfx/licencas/success.txt +40 -0
  46. package/editor/assets/sfx/licencas/synth_hit.txt +32 -0
  47. package/editor/assets/sfx/licencas/tick.txt +14 -0
  48. package/editor/assets/sfx/licencas/whoosh.txt +47 -0
  49. package/editor/assets/sfx/licencas/wind.txt +31 -0
  50. package/editor/assets/sfx/sfx.json +832 -0
  51. package/editor/assets/sfx/sfx_sinteticos.json +317 -0
  52. package/editor/assets/sfx/sfx_woven.json +387 -0
  53. package/editor/assets/sfx/woven/LICENCA.md +16 -0
  54. package/editor/fichas/briefing-do-video.md +45 -0
  55. package/editor/fichas/conferencia-da-amostra.md +56 -0
  56. package/editor/fichas/matriz-de-ganchos.csv +3 -0
  57. package/editor/fichas/padrao.json +16 -0
  58. package/editor/fichas/planilha-de-cenas.csv +4 -0
  59. package/editor/fichas/planilha-de-versoes.csv +4 -0
  60. package/editor/fichas/plano-em-blocos.md +32 -0
  61. package/editor/fichas/registro-de-materiais.json +27 -0
  62. package/editor/helpers/assets_ocr/ocr_vision.swift +41 -0
  63. package/editor/helpers/bordas_precisas.py +272 -0
  64. package/editor/helpers/camera_viva.py +426 -0
  65. package/editor/helpers/captions.py +601 -0
  66. package/editor/helpers/common.py +308 -0
  67. package/editor/helpers/compose_final.py +3555 -0
  68. package/editor/helpers/conferir_corte.py +234 -0
  69. package/editor/helpers/conferir_dados.py +405 -0
  70. package/editor/helpers/contact_sheet.py +282 -0
  71. package/editor/helpers/corte_por_onda.py +433 -0
  72. package/editor/helpers/corte_seco.py +595 -0
  73. package/editor/helpers/denoise.py +971 -0
  74. package/editor/helpers/efeitos.py +408 -0
  75. package/editor/helpers/efeitos_gerar.py +159 -0
  76. package/editor/helpers/elevenlabs_api.py +442 -0
  77. package/editor/helpers/face_layout.py +570 -0
  78. package/editor/helpers/fetch_images.py +423 -0
  79. package/editor/helpers/ganho_por_trecho.py +214 -0
  80. package/editor/helpers/gen_image.py +312 -0
  81. package/editor/helpers/gen_music.py +316 -0
  82. package/editor/helpers/gen_sfx.py +148 -0
  83. package/editor/helpers/grade.py +307 -0
  84. package/editor/helpers/icloud.py +119 -0
  85. package/editor/helpers/make_sfx.py +940 -0
  86. package/editor/helpers/matte.py +1342 -0
  87. package/editor/helpers/medir_referencia.py +2232 -0
  88. package/editor/helpers/mixar_trilha.py +354 -0
  89. package/editor/helpers/montar_corte.py +295 -0
  90. package/editor/helpers/motion/__init__.py +13 -0
  91. package/editor/helpers/motion/engine.py +1915 -0
  92. package/editor/helpers/motion/registry.py +510 -0
  93. package/editor/helpers/motion/render.py +665 -0
  94. package/editor/helpers/motion/templates/barra_progresso.py +370 -0
  95. package/editor/helpers/motion/templates/carimbo.py +132 -0
  96. package/editor/helpers/motion/templates/checklist.py +271 -0
  97. package/editor/helpers/motion/templates/comentario_cta.py +380 -0
  98. package/editor/helpers/motion/templates/contador.py +222 -0
  99. package/editor/helpers/motion/templates/destaque.py +263 -0
  100. package/editor/helpers/motion/templates/etiqueta_preco.py +461 -0
  101. package/editor/helpers/motion/templates/grafico_barras.py +510 -0
  102. package/editor/helpers/motion/templates/grafico_linha.py +462 -0
  103. package/editor/helpers/motion/templates/notificacao.py +266 -0
  104. package/editor/helpers/motion/templates/palavra_impacto.py +410 -0
  105. package/editor/helpers/motion/templates/passos.py +321 -0
  106. package/editor/helpers/motion/templates/riscado.py +298 -0
  107. package/editor/helpers/motion/templates/selo_garantia.py +150 -0
  108. package/editor/helpers/motion_render.py +241 -0
  109. package/editor/helpers/motion_sugerir.py +961 -0
  110. package/editor/helpers/noise_check.py +134 -0
  111. package/editor/helpers/pack_transcripts.py +223 -0
  112. package/editor/helpers/palavras_na_onda.py +176 -0
  113. package/editor/helpers/plano_sfx.py +839 -0
  114. package/editor/helpers/portao.py +427 -0
  115. package/editor/helpers/preview_server.py +1177 -0
  116. package/editor/helpers/rastrear_rosto.py +298 -0
  117. package/editor/helpers/render_cut.py +1256 -0
  118. package/editor/helpers/respiros.py +89 -0
  119. package/editor/helpers/sincronizar_cameras.py +571 -0
  120. package/editor/helpers/speech_regions.py +423 -0
  121. package/editor/helpers/style_catalog.json +231 -0
  122. package/editor/helpers/style_samples.py +246 -0
  123. package/editor/helpers/tela_verde.py +2113 -0
  124. package/editor/helpers/text_render.py +2059 -0
  125. package/editor/helpers/texto_na_tela.py +366 -0
  126. package/editor/helpers/timeline_view.py +292 -0
  127. package/editor/helpers/tira_de_quadros.py +113 -0
  128. package/editor/helpers/transcribe.py +734 -0
  129. package/editor/helpers/transcribe_batch.py +129 -0
  130. package/editor/helpers/trilhas.py +1146 -0
  131. package/editor/helpers/trilhas_humores.json +66 -0
  132. package/editor/helpers/valores.py +187 -0
  133. package/editor/helpers/verify_cut.py +820 -0
  134. package/editor/helpers/voice_levels.py +233 -0
  135. package/editor/helpers/voz_check.py +386 -0
  136. package/editor/helpers/voz_limpa.py +81 -0
  137. package/editor/helpers/watch_preview.py +372 -0
  138. package/editor/modelo-projeto/LEIA.md +402 -0
  139. package/editor/modelo-projeto/assets/fontes/OFL-Caveat.txt +93 -0
  140. package/editor/modelo-projeto/assets/fontes/OFL-JetBrainsMono.txt +93 -0
  141. package/editor/modelo-projeto/assets/fontes/OFL-Montserrat.txt +93 -0
  142. package/editor/modelo-projeto/assets/fontes/larguras.json +1 -0
  143. package/editor/modelo-projeto/assets/three/BufferGeometryUtils.js +1438 -0
  144. package/editor/modelo-projeto/assets/three/GLTFLoader.js +4895 -0
  145. package/editor/modelo-projeto/assets/three/LICENSE +21 -0
  146. package/editor/modelo-projeto/assets/three/ORIGEM.txt +17 -0
  147. package/editor/modelo-projeto/assets/three/RoomEnvironment.js +185 -0
  148. package/editor/modelo-projeto/assets/three/three.core.min.js +6 -0
  149. package/editor/modelo-projeto/assets/three/three.module.min.js +6 -0
  150. package/editor/modelo-projeto/build.py +4828 -0
  151. package/editor/modelo-projeto/compositions/cenas/2d/k2d_antes_depois.html +199 -0
  152. package/editor/modelo-projeto/compositions/cenas/2d/k2d_arquivo.html +232 -0
  153. package/editor/modelo-projeto/compositions/cenas/2d/k2d_busca.html +315 -0
  154. package/editor/modelo-projeto/compositions/cenas/2d/k2d_comando.html +304 -0
  155. package/editor/modelo-projeto/compositions/cenas/2d/k2d_conversa.html +217 -0
  156. package/editor/modelo-projeto/compositions/cenas/2d/k2d_esquema.html +289 -0
  157. package/editor/modelo-projeto/compositions/cenas/2d/k2d_medidor.html +249 -0
  158. package/editor/modelo-projeto/compositions/cenas/2d/k2d_notas.html +233 -0
  159. package/editor/modelo-projeto/compositions/cenas/3d/k3d_caminho.html +197 -0
  160. package/editor/modelo-projeto/compositions/cenas/3d/k3d_explodida.html +152 -0
  161. package/editor/modelo-projeto/compositions/cenas/3d/k3d_tela.html +167 -0
  162. package/editor/modelo-projeto/compositions/cenas/3d/k3d_volume.html +124 -0
  163. package/editor/modelo-projeto/compositions/cenas/3d/motor-three.js +728 -0
  164. package/editor/modelo-projeto/compositions/cenas/LEIA.md +177 -0
  165. package/editor/modelo-projeto/ferramentas/medir_fontes.py +65 -0
  166. package/editor/modelo-projeto/ferramentas/pessoa_recortada.py +176 -0
  167. package/editor/modelo-projeto/ferramentas/rastrear_maos.py +189 -0
  168. package/editor/modelo-projeto/hyperframes.json +12 -0
  169. package/editor/modelo-projeto/package.json +11 -0
  170. package/editor/modelo-projeto/projeto.json +16 -0
  171. package/editor/preview/app.css +1009 -0
  172. package/editor/preview/app.js +2122 -0
  173. package/editor/preview/index.html +307 -0
  174. package/editor/pyproject.toml +23 -0
  175. package/editor/references/3d.md +236 -0
  176. package/editor/references/animacao-catalogo.md +672 -0
  177. package/editor/references/animacao.md +498 -0
  178. package/editor/references/comandos.md +92 -0
  179. package/editor/references/corte-seco-como-funciona.md +50 -0
  180. package/editor/references/corte-seco.md +88 -0
  181. package/editor/references/corte.md +290 -0
  182. package/editor/references/direcao-por-blocos.md +218 -0
  183. package/editor/references/enquadramento.md +654 -0
  184. package/editor/references/estilos-prontos.md +355 -0
  185. package/editor/references/estudio.md +100 -0
  186. package/editor/references/exemplo-eventos.json +67 -0
  187. package/editor/references/lettering.md +252 -0
  188. package/editor/references/modelos.md +494 -0
  189. package/editor/references/phase2.md +1007 -0
  190. package/editor/references/referencia-aplicar.md +203 -0
  191. package/editor/references/referencia-medir.md +375 -0
  192. package/editor/references/revisao.md +335 -0
  193. package/editor/references/som-e-trilha.md +146 -0
  194. package/editor/references/tabela-de-sons.md +130 -0
  195. package/editor/references/variacoes.md +234 -0
  196. package/editor/uv.lock +1141 -0
  197. package/lib/act.js +36 -8
  198. package/lib/catalogo.js +4 -0
  199. package/lib/chamada-torta.js +81 -0
  200. package/lib/editor-pro.js +786 -0
  201. package/lib/entrega.js +58 -0
  202. package/lib/estudio/conferencia-visual.js +76 -0
  203. package/lib/estudio/diretor.js +6 -1
  204. package/lib/estudio/exportar.js +3 -1
  205. package/lib/estudio/gosto.js +45 -4
  206. package/lib/estudio/index.js +8 -5
  207. package/lib/estudio/kit-ae/base.jsx +119 -13
  208. package/lib/estudio/kit-ae/cenas.jsx +47 -15
  209. package/lib/estudio/kit-ae/guarda.jsx +274 -0
  210. package/lib/estudio/kit-ae/primitivas.jsx +180 -24
  211. package/lib/estudio/maquina-diretor.js +61 -7
  212. package/lib/estudio/maquina-foto.js +177 -0
  213. package/lib/estudio/maquina-guarda.js +207 -0
  214. package/lib/estudio/maquina-layout.js +22 -1
  215. package/lib/estudio/maquina.js +60 -11
  216. package/lib/estudio/referencias.js +1 -1
  217. package/lib/estudio/visao/visao.swift +97 -0
  218. package/lib/estudio/visao.js +60 -0
  219. package/lib/local/cerebro.json +192 -6
  220. package/lib/local/motor.js +5 -2
  221. package/lib/mcp.js +105 -7
  222. package/lib/nuvem.js +11 -1
  223. package/lib/outra-pasta.js +1 -1
  224. package/lib/passagem.js +50 -0
  225. package/lib/perigo.js +29 -12
  226. package/lib/protecao.js +134 -0
  227. package/lib/sandbox.js +11 -4
  228. package/lib/titulo-conversa.js +46 -0
  229. package/lib/tools.js +3 -0
  230. package/lib/ui.js +1 -1
  231. package/package.json +3 -2
  232. package/skills/editar-meu-video/SKILL.md +6 -2
  233. package/skills/editar-video/SKILL.md +113 -0
  234. package/studio/pessoa.py +52 -0
  235. package/voz/primo_voz/app.py +43 -4
  236. package/voz/primo_voz/chat.html +17 -6
  237. package/voz/primo_voz/painel_dados.py +134 -0
@@ -0,0 +1,728 @@
1
+ // Motor das cenas 3D do kit (compositions/cenas/3d/). Código da skill em cima do three.js r181 (MIT), que fica
2
+ // em assets/three/ com a licença ao lado. Nada é baixado na hora do render.
3
+ //
4
+ // Regra de ouro: tudo é função do tempo da cena. A cena escreve uma função atualizar(t) que põe peças, câmera,
5
+ // luz e rótulos no lugar para aquele t, sem guardar nada do quadro anterior. O mesmo t desenha sempre o mesmo
6
+ // quadro, em qualquer ordem, em qualquer processo do render (é o que deixa o render repetível).
7
+ //
8
+ // Quem chama: o <script> de cada cena do kit, com import() deste arquivo (veja k3d_volume.html). Guia completo:
9
+ // references/3d.md da skill.
10
+ import * as THREE from '../../../assets/three/three.module.min.js';
11
+ import { RoomEnvironment } from '../../../assets/three/RoomEnvironment.js';
12
+
13
+ export { THREE };
14
+ export const REVISAO = THREE.REVISION;
15
+
16
+ // ------------------------------------------------------------------------------------------ tempo
17
+ export const lim = (v, a = 0, b = 1) => Math.min(b, Math.max(a, v));
18
+ export const mistura = (a, b, p) => a + (b - a) * p;
19
+ // começa e termina devagar (o movimento "abre parado" e "termina parado")
20
+ export const suave = (p) => { p = lim(p); return p < 0.5 ? 4 * p * p * p : 1 - Math.pow(-2 * p + 2, 3) / 2; };
21
+ // sai rápido e freia no fim
22
+ export const freia = (p) => 1 - Math.pow(1 - lim(p), 3);
23
+ // quanto (0 a 1) de um movimento que começa em `de` e dura `dur` já andou no tempo t
24
+ export const trecho = (t, de, dur, curva = suave) => (dur <= 0 ? (t >= de ? 1 : 0) : curva(lim((t - de) / dur)));
25
+
26
+ const semAcento = (s) => String(s).normalize('NFD').replace(/[̀-ͯ]/g, '').toLowerCase().replace(/[^a-z0-9]/g, '');
27
+
28
+ // tempo (s da cena) em que a palavra começa; `depois` escolhe a repetição; `padrao` vale quando a fala não tem a
29
+ // palavra (cena aberta sozinha no estúdio, ou palavra trocada)
30
+ export function palavra(C, texto, depois = 0, padrao = depois) {
31
+ const alvo = semAcento(texto);
32
+ const p = (C.palavras || []).find((w) => w.t >= depois - 1e-3 && semAcento(w.texto).startsWith(alvo));
33
+ return p ? p.t : padrao;
34
+ }
35
+
36
+ // um parâmetro de tempo da cena: número (s da cena), "palavra" ou {palavra, depois, padrao}
37
+ export function momento(C, m, padrao = 0) {
38
+ if (typeof m === 'number') return m;
39
+ if (typeof m === 'string') return palavra(C, m, 0, padrao);
40
+ if (m && typeof m === 'object') {
41
+ const depois = m.depois == null ? 0 : momento(C, m.depois, 0);
42
+ return palavra(C, m.palavra || '', depois, m.padrao == null ? padrao : m.padrao);
43
+ }
44
+ return padrao;
45
+ }
46
+
47
+ // ------------------------------------------------------------------------------------------ câmera
48
+ // pose = {alvo: [x, y, z], dist, azim (graus, 0 = de frente, positivo gira para a direita), elev (graus)}
49
+ export function orbita(camera, pose) {
50
+ const a = (pose.azim * Math.PI) / 180;
51
+ const e = (pose.elev * Math.PI) / 180;
52
+ const [x, y, z] = pose.alvo || [0, 0, 0];
53
+ camera.position.set(x + pose.dist * Math.cos(e) * Math.sin(a), y + pose.dist * Math.sin(e), z + pose.dist * Math.cos(e) * Math.cos(a));
54
+ camera.lookAt(x, y, z);
55
+ camera.updateMatrixWorld();
56
+ }
57
+
58
+ export function misturaPose(p0, p1, k) {
59
+ const a0 = p0.alvo || [0, 0, 0];
60
+ const a1 = p1.alvo || a0;
61
+ return {
62
+ alvo: [mistura(a0[0], a1[0], k), mistura(a0[1], a1[1], k), mistura(a0[2], a1[2], k)],
63
+ dist: mistura(p0.dist, p1.dist == null ? p0.dist : p1.dist, k),
64
+ azim: mistura(p0.azim, p1.azim == null ? p0.azim : p1.azim, k),
65
+ elev: mistura(p0.elev, p1.elev == null ? p0.elev : p1.elev, k),
66
+ };
67
+ }
68
+
69
+ // põe o centro da imagem (para onde a câmera olha) no meio da área [x0, y0, x1, y1] da tela, em pixels
70
+ export function enquadrar(camera, W, H, area) {
71
+ if (!area) { camera.clearViewOffset(); return; }
72
+ const cx = (area[0] + area[2]) / 2;
73
+ const cy = (area[1] + area[3]) / 2;
74
+ camera.setViewOffset(W, H, W / 2 - cx, H / 2 - cy, W, H);
75
+ }
76
+
77
+ // distância para uma esfera de raio r caber na área (com folga), com a câmera enquadrada nela
78
+ export function distanciaParaCaber(camera, raio, H, area, folga = 1.12) {
79
+ const t = Math.tan((camera.fov * Math.PI) / 360);
80
+ const ah = area[3] - area[1];
81
+ const aw = area[2] - area[0];
82
+ const k = t * Math.min(ah, aw) / H;
83
+ return (raio * folga) / k;
84
+ }
85
+
86
+ // distância para um volume de largura x altura (na tela) caber na área, com folga
87
+ export function distanciaParaCaixa(camera, largura, altura, H, area, folga = 1.1) {
88
+ const t = Math.tan((camera.fov * Math.PI) / 360);
89
+ const ah = area[3] - area[1];
90
+ const aw = area[2] - area[0];
91
+ return Math.max((altura / 2) * folga / (t * ah / H), (largura / 2) * folga / (t * aw / H));
92
+ }
93
+
94
+ // a menor distância em que a caixa (Box3) inteira aparece dentro da área, vista daquela pose (azim, elev, alvo).
95
+ // Mede de verdade: projeta os 8 cantos na câmera enquadrada e procura a distância (busca binária).
96
+ export function distanciaQueCabe(camera, W, H, area, caixa, pose, folga = 1.08) {
97
+ const cantos = [];
98
+ for (const x of [caixa.min.x, caixa.max.x]) for (const y of [caixa.min.y, caixa.max.y]) for (const z of [caixa.min.z, caixa.max.z]) cantos.push(new THREE.Vector3(x, y, z));
99
+ const cx = (area[0] + area[2]) / 2, cy = (area[1] + area[3]) / 2;
100
+ const hw = (area[2] - area[0]) / 2 / folga, hh = (area[3] - area[1]) / 2 / folga;
101
+ const v = new THREE.Vector3();
102
+ const cabe = (d) => {
103
+ orbita(camera, { ...pose, dist: d });
104
+ for (const c of cantos) {
105
+ v.copy(c).project(camera);
106
+ if (v.z > 1 || v.z < -1) return false;
107
+ if (Math.abs((v.x * 0.5 + 0.5) * W - cx) > hw || Math.abs((-v.y * 0.5 + 0.5) * H - cy) > hh) return false;
108
+ }
109
+ return true;
110
+ };
111
+ let lo = camera.near, hi = 1;
112
+ while (!cabe(hi) && hi < 1e4) hi *= 2;
113
+ for (let i = 0; i < 40; i++) { const m = (lo + hi) / 2; if (cabe(m)) hi = m; else lo = m; }
114
+ return hi;
115
+ }
116
+
117
+ // ------------------------------------------------------------------------------------------ texturas desenhadas
118
+ function texturaDeCanvas(cv) {
119
+ const tex = new THREE.CanvasTexture(cv);
120
+ tex.colorSpace = THREE.SRGBColorSpace;
121
+ return tex;
122
+ }
123
+
124
+ function hexA(cor, a) {
125
+ const c = new THREE.Color(cor);
126
+ return `rgba(${Math.round(c.r * 255)},${Math.round(c.g * 255)},${Math.round(c.b * 255)},${a})`;
127
+ }
128
+
129
+ // grade do chão com as bordas sumindo (dá profundidade: as linhas convergem para o fundo)
130
+ function texturaDeGrade(corLinha, passos = 24, px = 1024) {
131
+ const cv = document.createElement('canvas');
132
+ cv.width = cv.height = px;
133
+ const g = cv.getContext('2d');
134
+ g.strokeStyle = corLinha;
135
+ g.globalAlpha = 0.75;
136
+ g.lineWidth = Math.max(2, px / 420);
137
+ for (let i = 0; i <= passos; i++) {
138
+ const p = Math.round((i / passos) * (px - 2)) + 1;
139
+ g.beginPath(); g.moveTo(p, 0); g.lineTo(p, px); g.stroke();
140
+ g.beginPath(); g.moveTo(0, p); g.lineTo(px, p); g.stroke();
141
+ }
142
+ // some para as bordas
143
+ g.globalCompositeOperation = 'destination-in';
144
+ const r = g.createRadialGradient(px / 2, px / 2, px * 0.12, px / 2, px / 2, px / 2);
145
+ r.addColorStop(0, 'rgba(0,0,0,1)');
146
+ r.addColorStop(1, 'rgba(0,0,0,0)');
147
+ g.fillStyle = r;
148
+ g.fillRect(0, 0, px, px);
149
+ return texturaDeCanvas(cv);
150
+ }
151
+
152
+ let _manchaTex = null;
153
+ function texturaDeMancha() {
154
+ if (_manchaTex) return _manchaTex;
155
+ const px = 256;
156
+ const cv = document.createElement('canvas');
157
+ cv.width = cv.height = px;
158
+ const g = cv.getContext('2d');
159
+ const r = g.createRadialGradient(px / 2, px / 2, 0, px / 2, px / 2, px / 2);
160
+ r.addColorStop(0, 'rgba(0,0,0,1)');
161
+ r.addColorStop(0.45, 'rgba(0,0,0,0.55)');
162
+ r.addColorStop(1, 'rgba(0,0,0,0)');
163
+ g.fillStyle = r;
164
+ g.fillRect(0, 0, px, px);
165
+ _manchaTex = new THREE.CanvasTexture(cv);
166
+ return _manchaTex;
167
+ }
168
+
169
+ // ------------------------------------------------------------------------------------------ sólidos
170
+ export function material(cor, { rugosidade = 0.5, metal = 0, verniz = 0.25 } = {}) {
171
+ return new THREE.MeshPhysicalMaterial({ color: new THREE.Color(cor), roughness: rugosidade, metalness: metal, clearcoat: verniz, clearcoatRoughness: 0.35 });
172
+ }
173
+
174
+ function sombreia(obj) {
175
+ obj.traverse((o) => { if (o.isMesh) { o.castShadow = true; o.receiveShadow = true; } });
176
+ return obj;
177
+ }
178
+
179
+ function retanguloArredondado(w, d, r) {
180
+ r = Math.max(0.001, Math.min(r, w / 2 - 0.001, d / 2 - 0.001));
181
+ const s = new THREE.Shape();
182
+ const x = -w / 2, y = -d / 2;
183
+ s.moveTo(x + r, y);
184
+ s.lineTo(x + w - r, y);
185
+ s.quadraticCurveTo(x + w, y, x + w, y + r);
186
+ s.lineTo(x + w, y + d - r);
187
+ s.quadraticCurveTo(x + w, y + d, x + w - r, y + d);
188
+ s.lineTo(x + r, y + d);
189
+ s.quadraticCurveTo(x, y + d, x, y + d - r);
190
+ s.lineTo(x, y + r);
191
+ s.quadraticCurveTo(x, y, x + r, y);
192
+ return s;
193
+ }
194
+
195
+ // peça de cantos arredondados e borda chanfrada, com a base em y = 0 (largura em x, altura em y, profundidade em z).
196
+ // `lados` pinta as laterais de outra cor: é o que mostra a espessura quando a câmera gira.
197
+ export function placa({ largura = 2, altura = 0.2, profundidade = 1.4, raio = 0.12, chanfro = 0.03, cor = '#c7b7ef', lados = null,
198
+ rugosidade = 0.5, metal = 0, verniz = 0.25 } = {}) {
199
+ const ch = Math.max(0.001, Math.min(chanfro, altura / 2 - 0.001, largura / 4, profundidade / 4));
200
+ const forma = retanguloArredondado(largura - 2 * ch, profundidade - 2 * ch, Math.max(0.002, raio - ch));
201
+ const geo = new THREE.ExtrudeGeometry(forma, { depth: Math.max(0.001, altura - 2 * ch), bevelEnabled: true, bevelThickness: ch,
202
+ bevelSize: ch, bevelSegments: 3, curveSegments: 10 });
203
+ geo.rotateX(-Math.PI / 2);
204
+ geo.translate(0, ch, 0);
205
+ geo.computeVertexNormals();
206
+ const tampa = material(cor, { rugosidade, metal, verniz });
207
+ const lateral = lados ? material(lados, { rugosidade, metal, verniz }) : tampa;
208
+ return sombreia(new THREE.Mesh(geo, [tampa, lateral]));
209
+ }
210
+
211
+ export function caixa(op = {}) {
212
+ return placa({ largura: 1, altura: 1, profundidade: 1, raio: 0.1, chanfro: 0.05, ...op });
213
+ }
214
+
215
+ export function cilindro({ raio = 0.5, altura = 1, cor = '#c7b7ef', segmentos = 48, rugosidade = 0.5, metal = 0, verniz = 0.25 } = {}) {
216
+ const geo = new THREE.CylinderGeometry(raio, raio, altura, segmentos, 1);
217
+ geo.translate(0, altura / 2, 0);
218
+ return sombreia(new THREE.Mesh(geo, material(cor, { rugosidade, metal, verniz })));
219
+ }
220
+
221
+ // ------------------------------------------------------------------------------------------ texto em volume
222
+ // As letras são desenhadas na fonte do projeto (Montserrat), o contorno de cada uma é medido no desenho
223
+ // (quadrados marchando, com o meio-termo entre pixels) e vira geometria extrudada de verdade, com chanfro.
224
+ // Qualquer letra acentuada serve. Não usa arquivo de fonte 3D nem biblioteca extra.
225
+ function contornosDoDesenho(linhas, fonteCss, tamanhoPx, entrelinha) {
226
+ const cv = document.createElement('canvas');
227
+ const g = cv.getContext('2d', { willReadFrequently: true });
228
+ g.font = fonteCss;
229
+ const larg = Math.max(...linhas.map((l) => g.measureText(l).width));
230
+ const pad = Math.ceil(tamanhoPx * 0.3);
231
+ const lh = Math.ceil(tamanhoPx * entrelinha);
232
+ const W = Math.ceil(larg) + pad * 2;
233
+ const H = lh * linhas.length + pad * 2;
234
+ cv.width = W;
235
+ cv.height = H;
236
+ g.font = fonteCss;
237
+ g.fillStyle = '#ffffff';
238
+ g.textAlign = 'center';
239
+ g.textBaseline = 'middle';
240
+ linhas.forEach((l, i) => g.fillText(l, W / 2, pad + lh * (i + 0.5)));
241
+ const px = g.getImageData(0, 0, W, H).data;
242
+ const v = new Float32Array(W * H);
243
+ for (let i = 0; i < W * H; i++) v[i] = px[i * 4 + 3] / 255;
244
+ const iso = 0.5;
245
+ const prox = new Int32Array(2 * W * H).fill(-1);
246
+ const pontoX = new Float32Array(2 * W * H);
247
+ const pontoY = new Float32Array(2 * W * H);
248
+ const idH = (x, y) => 2 * (y * W + x);
249
+ const idV = (x, y) => 2 * (y * W + x) + 1;
250
+ const ponto = (id) => {
251
+ if (pontoX[id] !== 0 || pontoY[id] !== 0) return;
252
+ const k = id >> 1;
253
+ const x = k % W;
254
+ const y = (k - x) / W;
255
+ if ((id & 1) === 0) { const a = v[y * W + x], b = v[y * W + x + 1]; pontoX[id] = x + (iso - a) / (b - a); pontoY[id] = y; } else { const a = v[y * W + x], b = v[(y + 1) * W + x]; pontoX[id] = x; pontoY[id] = y + (iso - a) / (b - a); }
256
+ };
257
+ const liga = (de, ate) => { prox[de] = ate; ponto(de); ponto(ate); };
258
+ for (let y = 0; y < H - 1; y++) {
259
+ for (let x = 0; x < W - 1; x++) {
260
+ const a = v[y * W + x] > iso, b = v[y * W + x + 1] > iso, c = v[(y + 1) * W + x + 1] > iso, d = v[(y + 1) * W + x] > iso;
261
+ const caso = (a ? 8 : 0) | (b ? 4 : 0) | (c ? 2 : 0) | (d ? 1 : 0);
262
+ if (caso === 0 || caso === 15) continue;
263
+ const T = idH(x, y), B = idH(x, y + 1), L = idV(x, y), R = idV(x + 1, y);
264
+ const meio = (v[y * W + x] + v[y * W + x + 1] + v[(y + 1) * W + x + 1] + v[(y + 1) * W + x]) / 4 > iso;
265
+ switch (caso) {
266
+ case 1: liga(L, B); break;
267
+ case 2: liga(B, R); break;
268
+ case 3: liga(L, R); break;
269
+ case 4: liga(R, T); break;
270
+ case 5: if (meio) { liga(L, T); liga(R, B); } else { liga(L, B); liga(R, T); } break;
271
+ case 6: liga(B, T); break;
272
+ case 7: liga(L, T); break;
273
+ case 8: liga(T, L); break;
274
+ case 9: liga(T, B); break;
275
+ case 10: if (meio) { liga(T, R); liga(B, L); } else { liga(T, L); liga(B, R); } break;
276
+ case 11: liga(T, R); break;
277
+ case 12: liga(R, L); break;
278
+ case 13: liga(R, B); break;
279
+ case 14: liga(B, L); break;
280
+ default: break;
281
+ }
282
+ }
283
+ }
284
+ const voltas = [];
285
+ for (let id = 0; id < prox.length; id++) {
286
+ if (prox[id] < 0) continue;
287
+ const pts = [];
288
+ let k = id;
289
+ let guarda = 0;
290
+ while (k >= 0 && guarda++ < 4000000) {
291
+ pts.push([pontoX[k], pontoY[k]]);
292
+ const n = prox[k];
293
+ prox[k] = -1;
294
+ k = n;
295
+ if (k === id) break;
296
+ }
297
+ if (pts.length >= 3) voltas.push(pts);
298
+ }
299
+ return { voltas, W, H };
300
+ }
301
+
302
+ function simplifica(pts, eps) {
303
+ // Ramer-Douglas-Peucker numa volta fechada: parte no ponto mais longe do primeiro
304
+ if (pts.length < 8) return pts;
305
+ let far = 0, dmax = -1;
306
+ for (let i = 1; i < pts.length; i++) {
307
+ const d = (pts[i][0] - pts[0][0]) ** 2 + (pts[i][1] - pts[0][1]) ** 2;
308
+ if (d > dmax) { dmax = d; far = i; }
309
+ }
310
+ const rdp = (arr) => {
311
+ const out = [arr[0]];
312
+ const pilha = [[0, arr.length - 1]];
313
+ const manter = new Uint8Array(arr.length);
314
+ manter[0] = manter[arr.length - 1] = 1;
315
+ while (pilha.length) {
316
+ const [i0, i1] = pilha.pop();
317
+ const [x0, y0] = arr[i0], [x1, y1] = arr[i1];
318
+ const dx = x1 - x0, dy = y1 - y0, n = Math.hypot(dx, dy) || 1e-9;
319
+ let im = -1, dm = eps;
320
+ for (let i = i0 + 1; i < i1; i++) {
321
+ const d = Math.abs(dy * arr[i][0] - dx * arr[i][1] + x1 * y0 - y1 * x0) / n;
322
+ if (d > dm) { dm = d; im = i; }
323
+ }
324
+ if (im >= 0) { manter[im] = 1; pilha.push([i0, im], [im, i1]); }
325
+ }
326
+ for (let i = 1; i < arr.length; i++) if (manter[i]) out.push(arr[i]);
327
+ return out;
328
+ };
329
+ const a = rdp(pts.slice(0, far + 1));
330
+ const b = rdp(pts.slice(far).concat([pts[0]]));
331
+ return a.concat(b.slice(1, -1));
332
+ }
333
+
334
+ const areaDe = (p) => { let s = 0; for (let i = 0, j = p.length - 1; i < p.length; j = i++) s += (p[j][0] + p[i][0]) * (p[j][1] - p[i][1]); return s / 2; };
335
+ const dentro = (pt, p) => {
336
+ let c = false;
337
+ for (let i = 0, j = p.length - 1; i < p.length; j = i++) {
338
+ if ((p[i][1] > pt[1]) !== (p[j][1] > pt[1]) && pt[0] < ((p[j][0] - p[i][0]) * (pt[1] - p[i][1])) / (p[j][1] - p[i][1]) + p[i][0]) c = !c;
339
+ }
340
+ return c;
341
+ };
342
+
343
+ export async function textoEmVolume(texto, { fonte = 'Montserrat', peso = 900, tamanhoPx = 260, altura = 1, espessura = 0.3, chanfro = 0.02,
344
+ entrelinha = 1.05, cor = '#665080', lados = null, rugosidade = 0.4, metal = 0, verniz = 0.5 } = {}) {
345
+ const css = `${peso} ${tamanhoPx}px ${fonte}`;
346
+ try { await document.fonts.load(css, texto); } catch (e) { /* segue com a fonte que tiver */ }
347
+ if (document.fonts && !document.fonts.check(css, texto)) console.warn(`[cena 3D] a fonte ${fonte} não carregou; o texto sai em outra fonte`);
348
+ const linhas = String(texto).split('\n');
349
+ const { voltas } = contornosDoDesenho(linhas, css, tamanhoPx, entrelinha);
350
+ const esc = altura / tamanhoPx; // 1 px do desenho em unidades da cena
351
+ const polis = voltas.map((p) => simplifica(p, 0.35)).filter((p) => p.length >= 3 && Math.abs(areaDe(p)) > 4);
352
+ if (!polis.length) throw new Error(`não consegui desenhar o texto "${texto}"`);
353
+ let maior = polis[0];
354
+ for (const p of polis) if (Math.abs(areaDe(p)) > Math.abs(areaDe(maior))) maior = p;
355
+ const sinalFora = Math.sign(areaDe(maior));
356
+ const fora = polis.filter((p) => Math.sign(areaDe(p)) === sinalFora);
357
+ const furos = polis.filter((p) => Math.sign(areaDe(p)) !== sinalFora);
358
+ const vira = (p) => p.map(([x, y]) => new THREE.Vector2(x * esc, -y * esc));
359
+ const formas = fora.map((p) => ({ p, s: new THREE.Shape(vira(p)), a: Math.abs(areaDe(p)) }));
360
+ for (const f of furos) {
361
+ const dono = formas.filter((o) => dentro(f[0], o.p)).sort((a, b) => a.a - b.a)[0];
362
+ if (dono) dono.s.holes.push(new THREE.Path(vira(f)));
363
+ }
364
+ const ch = Math.max(0.001, chanfro * altura);
365
+ const geo = new THREE.ExtrudeGeometry(formas.map((o) => o.s), { depth: Math.max(0.001, espessura * altura - 2 * ch), bevelEnabled: true,
366
+ bevelThickness: ch, bevelSize: ch, bevelSegments: 2, curveSegments: 1 });
367
+ geo.computeBoundingBox();
368
+ const bb = geo.boundingBox;
369
+ geo.translate(-(bb.min.x + bb.max.x) / 2, -bb.min.y, -(bb.min.z + bb.max.z) / 2); // base no chão, centro em x e z
370
+ geo.computeVertexNormals();
371
+ const frente = material(cor, { rugosidade, metal, verniz });
372
+ const lateral = lados ? material(lados, { rugosidade, metal, verniz }) : frente;
373
+ return sombreia(new THREE.Mesh(geo, [frente, lateral]));
374
+ }
375
+
376
+ // ------------------------------------------------------------------------------------------ o palco
377
+ function criarRotulos(palco, camada, itens) {
378
+ const { W, H, escala: e, cores } = palco;
379
+ const ns = 'http://www.w3.org/2000/svg';
380
+ const svg = document.createElementNS(ns, 'svg');
381
+ svg.setAttribute('width', W);
382
+ svg.setAttribute('height', H);
383
+ svg.setAttribute('viewBox', `0 0 ${W} ${H}`);
384
+ svg.style.cssText = 'position:absolute;left:0;top:0;overflow:visible;pointer-events:none';
385
+ camada.appendChild(svg);
386
+ const lista = itens.map((it) => {
387
+ const el = document.createElement('div');
388
+ el.textContent = it.texto;
389
+ el.style.cssText = `position:absolute;left:0;top:0;opacity:0;white-space:nowrap;font-family:Montserrat,sans-serif;font-weight:800;`
390
+ + `font-size:${Math.round((it.tamanho || 36) * e)}px;line-height:1.05;padding:${Math.round(14 * e)}px ${Math.round(22 * e)}px;`
391
+ + `border-radius:${Math.round(18 * e)}px;color:${it.cor || cores.tinta};background:${it.fundo || cores.papel};`
392
+ // sombra dura (sem desfoque): sombra desfocada do CSS varia um tico de um render para outro
393
+ + `box-shadow:0 0 0 ${Math.max(2, Math.round(3 * e))}px ${cores.base},0 ${Math.round(8 * e)}px 0 ${Math.round(-1 * e)}px ${cores.base}`;
394
+ camada.appendChild(el);
395
+ const linha = document.createElementNS(ns, 'line');
396
+ linha.setAttribute('stroke', it.corLinha || cores.destaque);
397
+ linha.setAttribute('stroke-width', String(Math.max(2, 4 * e)));
398
+ linha.setAttribute('stroke-linecap', 'round');
399
+ linha.setAttribute('opacity', '0');
400
+ const bola = document.createElementNS(ns, 'circle');
401
+ bola.setAttribute('r', String(Math.max(4, 10 * e)));
402
+ bola.setAttribute('fill', it.corLinha || cores.destaque);
403
+ bola.setAttribute('stroke', cores.papel);
404
+ bola.setAttribute('stroke-width', String(Math.max(2, 4 * e)));
405
+ bola.setAttribute('opacity', '0');
406
+ svg.appendChild(linha);
407
+ svg.appendChild(bola);
408
+ return { el, linha, bola, lado: it.lado || 'dir', w: el.offsetWidth, h: el.offsetHeight };
409
+ });
410
+ const v = new THREE.Vector3();
411
+ return {
412
+ medir() { for (const r of lista) { r.w = r.el.offsetWidth; r.h = r.el.offsetHeight; } },
413
+ // estados[i] = {ponto: [x, y, z] ou Vector3 (o que o rótulo descreve), opacidade (0 a 1)}
414
+ // op.coluna = true: os rótulos do mesmo lado ficam alinhados numa coluna, logo depois do ponto mais de fora
415
+ atualizar(estados, op = {}) {
416
+ const [sx0, sy0, sx1, sy1] = palco.limites;
417
+ const folga = (op.folga == null ? 70 : op.folga) * e;
418
+ const vivos = [];
419
+ lista.forEach((r, i) => {
420
+ const s = estados[i] || {};
421
+ const o = lim(s.opacidade == null ? 1 : s.opacidade);
422
+ if (!s.ponto || o <= 0.001) { r.el.style.opacity = '0'; r.linha.setAttribute('opacity', '0'); r.bola.setAttribute('opacity', '0'); return; }
423
+ if (!r.w) { r.w = r.el.offsetWidth; r.h = r.el.offsetHeight; }
424
+ if (s.ponto.isVector3) v.copy(s.ponto); else v.set(s.ponto[0], s.ponto[1], s.ponto[2]);
425
+ v.project(palco.camera);
426
+ if (v.z > 1 || v.z < -1) { r.el.style.opacity = '0'; r.linha.setAttribute('opacity', '0'); r.bola.setAttribute('opacity', '0'); return; }
427
+ const ax = (v.x * 0.5 + 0.5) * W, ay = (-v.y * 0.5 + 0.5) * H;
428
+ // o que o rótulo descreve saiu da área (ou da janela): o rótulo some junto, em vez de apontar para o nada
429
+ const fora = Math.max(sx0 - ax, ax - sx1, sy0 - ay, ay - sy1, 0);
430
+ const oo = o * (1 - lim(fora / (40 * e)));
431
+ if (oo <= 0.001) { r.el.style.opacity = '0'; r.linha.setAttribute('opacity', '0'); r.bola.setAttribute('opacity', '0'); return; }
432
+ vivos.push({ r, o: oo, ax, ay, x: 0, y: 0 });
433
+ });
434
+ const dir = vivos.filter((q) => q.r.lado === 'dir'), esq = vivos.filter((q) => q.r.lado === 'esq');
435
+ const colDir = op.coluna && dir.length ? Math.max(...dir.map((q) => q.ax)) + folga : null;
436
+ const colEsq = op.coluna && esq.length ? Math.min(...esq.map((q) => q.ax)) - folga : null;
437
+ for (const q of vivos) {
438
+ const { r } = q;
439
+ if (r.lado === 'cima') { q.x = q.ax - r.w / 2; q.y = q.ay - folga - r.h; } else if (r.lado === 'baixo') { q.x = q.ax - r.w / 2; q.y = q.ay + folga; } else if (r.lado === 'esq') { q.x = (colEsq != null ? colEsq : q.ax - folga) - r.w; q.y = q.ay - r.h / 2; } else { q.x = colDir != null ? colDir : q.ax + folga; q.y = q.ay - r.h / 2; }
440
+ q.x = lim(q.x, sx0, sx1 - r.w);
441
+ q.y = lim(q.y, sy0, sy1 - r.h);
442
+ }
443
+ // rótulos que se encostam descem (e, se passar do limite, o grupo sobe)
444
+ vivos.sort((a, b) => a.y - b.y);
445
+ const gap = 16 * e;
446
+ for (let i = 1; i < vivos.length; i++) {
447
+ for (let j = 0; j < i; j++) {
448
+ const A = vivos[j], B = vivos[i];
449
+ const cruzaX = B.x < A.x + A.r.w && A.x < B.x + B.r.w;
450
+ if (cruzaX && B.y < A.y + A.r.h + gap) B.y = A.y + A.r.h + gap;
451
+ }
452
+ }
453
+ const excesso = vivos.length ? Math.max(0, Math.max(...vivos.map((q) => q.y + q.r.h)) - sy1) : 0;
454
+ for (const q of vivos) {
455
+ q.y = Math.max(sy0, q.y - excesso);
456
+ const { r } = q;
457
+ r.el.style.transform = `translate(${q.x.toFixed(1)}px, ${q.y.toFixed(1)}px)`;
458
+ r.el.style.opacity = q.o.toFixed(3);
459
+ let lx = q.x, ly = q.y + r.h / 2;
460
+ if (r.lado === 'cima') { lx = q.x + r.w / 2; ly = q.y + r.h; } else if (r.lado === 'baixo') { lx = q.x + r.w / 2; ly = q.y; } else if (q.x + r.w / 2 < q.ax) { lx = q.x + r.w; }
461
+ r.linha.setAttribute('x1', q.ax.toFixed(1)); r.linha.setAttribute('y1', q.ay.toFixed(1));
462
+ r.linha.setAttribute('x2', lx.toFixed(1)); r.linha.setAttribute('y2', ly.toFixed(1));
463
+ r.linha.setAttribute('opacity', q.o.toFixed(3));
464
+ r.bola.setAttribute('cx', q.ax.toFixed(1)); r.bola.setAttribute('cy', q.ay.toFixed(1));
465
+ r.bola.setAttribute('opacity', q.o.toFixed(3));
466
+ }
467
+ },
468
+ };
469
+ }
470
+
471
+ // vídeo ou imagem como textura. É um plano chapado: girar a câmera mostra o plano de lado, não dá volume ao que
472
+ // está na imagem. No render, o HyperFrames troca o <video> por um <img class="__render_frame__"> ao lado com o
473
+ // quadro certo e manda desenhar de novo: a textura lê dali quando existe.
474
+ // encaixe "cobrir": preenche e corta (foco = que altura da imagem fica, 0 topo, 0.5 meio); "conter": a imagem
475
+ // inteira por cima de uma cópia ampliada e escurecida dela (fundo borrado feito por redução e ampliação: o
476
+ // filtro blur() do canvas muda um pouco de um render para outro e quebraria o render repetível).
477
+ export function texturaDeMidia(el, { largura = 1280, altura = 720, foco = 0.4, encaixe = 'cobrir' } = {}) {
478
+ const cv = document.createElement('canvas');
479
+ cv.width = largura;
480
+ cv.height = altura;
481
+ // canvas na CPU (willReadFrequently): o desenho sai igual em todo render
482
+ const g = cv.getContext('2d', { willReadFrequently: true });
483
+ g.fillStyle = '#000';
484
+ g.fillRect(0, 0, largura, altura);
485
+ const mini = document.createElement('canvas');
486
+ const gm = mini.getContext('2d', { willReadFrequently: true });
487
+ const tex = texturaDeCanvas(cv);
488
+ const fonte = () => {
489
+ const irmao = el.nextElementSibling;
490
+ if (irmao && irmao.classList && irmao.classList.contains('__render_frame__') && irmao.complete && irmao.naturalWidth > 0
491
+ && irmao.style.visibility !== 'hidden') return irmao;
492
+ if (el.tagName === 'VIDEO') return el.readyState >= 2 ? el : null;
493
+ return el.complete && el.naturalWidth > 0 ? el : null;
494
+ };
495
+ return {
496
+ textura: tex,
497
+ atualizar() {
498
+ const f = fonte();
499
+ if (!f) return false;
500
+ const sw = f.videoWidth || f.naturalWidth, sh = f.videoHeight || f.naturalHeight;
501
+ const kc = Math.max(largura / sw, altura / sh);
502
+ if (encaixe === 'conter') {
503
+ const km = 40 / Math.max(sw, sh);
504
+ mini.width = Math.max(2, Math.round(sw * km));
505
+ mini.height = Math.max(2, Math.round(sh * km));
506
+ gm.drawImage(f, 0, 0, mini.width, mini.height);
507
+ g.imageSmoothingEnabled = true;
508
+ g.drawImage(mini, (largura - sw * kc) / 2, (altura - sh * kc) / 2, sw * kc, sh * kc);
509
+ g.fillStyle = 'rgba(0,0,0,0.45)';
510
+ g.fillRect(0, 0, largura, altura);
511
+ const ki = Math.min(largura / sw, altura / sh);
512
+ g.drawImage(f, (largura - sw * ki) / 2, (altura - sh * ki) / 2, sw * ki, sh * ki);
513
+ } else {
514
+ g.drawImage(f, (largura - sw * kc) / 2, (altura - sh * kc) * foco, sw * kc, sh * kc);
515
+ }
516
+ tex.needsUpdate = true;
517
+ return true;
518
+ },
519
+ };
520
+ }
521
+
522
+ // modelo .glb/.gltf da pessoa (liberado para uso), carregado da pasta do projeto antes do render começar
523
+ export async function carregarModelo(url, { altura = null, largura = null } = {}) {
524
+ const { GLTFLoader } = await import('../../../assets/three/GLTFLoader.js');
525
+ const gltf = await new GLTFLoader().loadAsync(url);
526
+ const obj = gltf.scene;
527
+ sombreia(obj);
528
+ const box = new THREE.Box3().setFromObject(obj);
529
+ const tam = box.getSize(new THREE.Vector3());
530
+ let k = 1;
531
+ if (altura) k = altura / (tam.y || 1);
532
+ else if (largura) k = largura / (Math.max(tam.x, tam.z) || 1);
533
+ obj.scale.setScalar(k);
534
+ const c = box.getCenter(new THREE.Vector3());
535
+ const grupo = new THREE.Group();
536
+ obj.position.set(-c.x * k, -box.min.y * k, -c.z * k); // base no chão, centro em x e z
537
+ grupo.add(obj);
538
+ const mix = gltf.animations.length ? new THREE.AnimationMixer(obj) : null;
539
+ if (mix) gltf.animations.forEach((a) => mix.clipAction(a).play());
540
+ // animação do próprio modelo: posta no tempo (mix.setTime), nunca tocada pelo relógio
541
+ return { objeto: grupo, animacoes: gltf.animations, noTempo: (t) => { if (mix) mix.setTime(t); } };
542
+ }
543
+
544
+ function falhaVisivel(camada, texto) {
545
+ if (!camada) return;
546
+ const el = document.createElement('div');
547
+ el.textContent = texto;
548
+ el.style.cssText = 'position:absolute;left:60px;right:60px;top:40%;padding:28px;border-radius:20px;background:#fff;color:#b00020;'
549
+ + 'font:700 34px/1.25 Montserrat,sans-serif;box-shadow:0 0 0 6px #b00020;opacity:1';
550
+ camada.appendChild(el);
551
+ }
552
+
553
+ // Monta a cena e devolve desenhar(t). opcoes:
554
+ // id, C (window.__cenas[id] com os padrões), raiz (URL da pasta do projeto), canvas, camada (div dos rótulos),
555
+ // fundo ('#cor' ou null = transparente, para mostrar a pessoa numa moldura por baixo), area ([x0, y0, x1, y1]
556
+ // onde fica o 3D; padrão: a zona segura), limites (onde os rótulos podem ficar; padrão: a area), recorte
557
+ // ([x0, y0, x1, y1]: o 3D só aparece dentro desse retângulo de cantos redondos, a "janela" da cena quando a
558
+ // pessoa fica numa moldura ao lado; null = tela toda), fov, sombras,
559
+ // ambiente (luz de estúdio refletida, true),
560
+ // construir: async (palco) => (t) => {...} a cena: monta os objetos e devolve atualizar(t)
561
+ export async function iniciar(opcoes) {
562
+ const { id, C, canvas, camada } = opcoes;
563
+ const t0 = performance.now(); // só para medir a montagem (diagnóstico); nunca decide o que aparece
564
+ const estado = (window.__cena3d = window.__cena3d || {})[id] = { pronto: false, avisos: [], revisao: REVISAO };
565
+ const falha = (msg) => {
566
+ estado.erro = msg;
567
+ console.error(`[cena 3D ${id}] ${msg}`);
568
+ falhaVisivel(camada, `Cena 3D "${id}" não montou: ${msg}`);
569
+ return () => {};
570
+ };
571
+ const W = C.largura || 1080, H = C.altura || 1920;
572
+ let renderer;
573
+ try {
574
+ renderer = new THREE.WebGLRenderer({ canvas, antialias: true, alpha: opcoes.fundo == null || !!opcoes.recorte, preserveDrawingBuffer: true,
575
+ powerPreference: 'high-performance' });
576
+ } catch (e) {
577
+ return falha(`o WebGL não abriu neste navegador (${e && e.message ? e.message : e}). Veja references/3d.md, "Se o teste falhar".`);
578
+ }
579
+ renderer.setPixelRatio(1);
580
+ renderer.setSize(W, H, false);
581
+ renderer.outputColorSpace = THREE.SRGBColorSpace;
582
+ renderer.toneMapping = THREE.NeutralToneMapping;
583
+ renderer.shadowMap.enabled = opcoes.sombras !== false;
584
+ renderer.shadowMap.type = THREE.PCFSoftShadowMap;
585
+ const gl = renderer.getContext();
586
+ const info = gl.getExtension('WEBGL_debug_renderer_info');
587
+ estado.placa = info ? gl.getParameter(info.UNMASKED_RENDERER_WEBGL) : gl.getParameter(gl.RENDERER);
588
+ const cena = new THREE.Scene();
589
+ if (opcoes.fundo != null) cena.background = new THREE.Color(opcoes.fundo);
590
+ else renderer.setClearColor(0x000000, 0);
591
+ const rec = opcoes.recorte;
592
+ if (rec) {
593
+ // fora da janela o canvas fica vazio (transparente) e os cantos são arredondados
594
+ const r = Math.round(36 * Math.min(W, H) / 1080);
595
+ canvas.style.clipPath = `inset(${rec[1]}px ${W - rec[2]}px ${H - rec[3]}px ${rec[0]}px round ${r}px)`;
596
+ }
597
+ if (opcoes.ambiente !== false) {
598
+ const pm = new THREE.PMREMGenerator(renderer);
599
+ cena.environment = pm.fromScene(new RoomEnvironment(), 0.04).texture;
600
+ cena.environmentIntensity = opcoes.ambiente === true || opcoes.ambiente == null ? 0.55 : Number(opcoes.ambiente);
601
+ pm.dispose();
602
+ }
603
+ const camera = new THREE.PerspectiveCamera(opcoes.fov || 30, W / H, 0.05, 400);
604
+ const area = opcoes.area || C.seguro || [0, 0, W, H];
605
+ enquadrar(camera, W, H, area);
606
+ const raiz = opcoes.raiz || document.baseURI;
607
+ const solidos = [];
608
+ const texturas = [];
609
+ const palco = {
610
+ THREE, renderer, cena, camera, W, H, C, area, limites: opcoes.limites || area, cores: C.cores, escala: Math.min(W, H) / 1080,
611
+ transparente: opcoes.fundo == null,
612
+ url: (caminho) => new URL(caminho, raiz).href,
613
+ // registra um objeto para a conferência "a câmera nunca entra num objeto"
614
+ solido(obj, nome) { solidos.push({ obj, nome: nome || obj.name || 'objeto' }); return obj; },
615
+ luzes({ chave = [4, 9, 6], forca = 2.6, alvo = [0, 0.5, 0], area: tam = 6, contra = 0.9, ceu = 0.7 } = {}) {
616
+ const hemi = new THREE.HemisphereLight('#ffffff', C.cores.base, ceu);
617
+ cena.add(hemi);
618
+ const k = new THREE.DirectionalLight('#ffffff', forca);
619
+ k.position.set(...chave);
620
+ k.target.position.set(...alvo);
621
+ k.castShadow = palco.renderer.shadowMap.enabled;
622
+ k.shadow.mapSize.set(2048, 2048);
623
+ Object.assign(k.shadow.camera, { left: -tam, right: tam, top: tam, bottom: -tam, near: 0.5, far: 60 });
624
+ k.shadow.bias = -0.0004;
625
+ k.shadow.normalBias = 0.02;
626
+ k.shadow.radius = 4;
627
+ cena.add(k, k.target);
628
+ const r = new THREE.DirectionalLight('#ffffff', contra);
629
+ r.position.set(-chave[0] * 0.8, chave[1] * 0.6, -Math.abs(chave[2]) - 4);
630
+ cena.add(r);
631
+ return { hemi, chave: k, contra: r };
632
+ },
633
+ // chão: grade que some nas bordas + um plano que só mostra sombra. Sem fundo (transparente), só a sombra:
634
+ // a grade riscaria o vídeo que aparece por baixo.
635
+ chao({ tamanho = 24, passos = 24, grade = opcoes.fundo != null, sombra = 0.22, y = 0 } = {}) {
636
+ const g = new THREE.Group();
637
+ if (grade) {
638
+ const mapa = texturaDeGrade(C.cores.linha, passos);
639
+ mapa.anisotropy = renderer.capabilities.getMaxAnisotropy(); // linhas firmes mesmo vistas de lado
640
+ const m = new THREE.MeshBasicMaterial({ map: mapa, transparent: true, depthWrite: false });
641
+ const p = new THREE.Mesh(new THREE.PlaneGeometry(tamanho, tamanho), m);
642
+ p.rotation.x = -Math.PI / 2;
643
+ p.position.y = y + 0.0005;
644
+ p.renderOrder = -2;
645
+ g.add(p);
646
+ }
647
+ const s = new THREE.Mesh(new THREE.PlaneGeometry(tamanho, tamanho), new THREE.ShadowMaterial({ opacity: sombra }));
648
+ s.rotation.x = -Math.PI / 2;
649
+ s.position.y = y + 0.001;
650
+ s.receiveShadow = true;
651
+ s.renderOrder = -1;
652
+ g.add(s);
653
+ cena.add(g);
654
+ return g;
655
+ },
656
+ // sombra de contato: mancha escura e macia colada no chão, embaixo da peça; clareia quando a peça sobe
657
+ sombraDeContato({ largura = 1, profundidade = 1, forca = 0.45, y = 0 } = {}) {
658
+ const m = new THREE.MeshBasicMaterial({ map: texturaDeMancha(), transparent: true, opacity: forca, depthWrite: false, color: '#000000' });
659
+ const p = new THREE.Mesh(new THREE.PlaneGeometry(1, 1), m);
660
+ p.rotation.x = -Math.PI / 2;
661
+ p.position.y = y + 0.002;
662
+ p.renderOrder = -1;
663
+ cena.add(p);
664
+ return {
665
+ malha: p,
666
+ colocar(x, z, alturaAcima = 0, escalaExtra = 1, anguloY = 0) {
667
+ const k = 1 + alturaAcima * 0.6;
668
+ p.position.set(x, y + 0.002, z);
669
+ p.rotation.set(-Math.PI / 2, 0, anguloY);
670
+ p.scale.set(largura * 1.25 * k * escalaExtra, profundidade * 1.25 * k * escalaExtra, 1);
671
+ m.opacity = forca / (1 + alturaAcima * 2.2);
672
+ },
673
+ };
674
+ },
675
+ placa, caixa, cilindro, material, textoEmVolume,
676
+ carregarModelo: (caminho, op) => carregarModelo(palco.url(caminho), op),
677
+ texturaDeMidia(el, op) { const t = texturaDeMidia(el, op); texturas.push(t); return t; },
678
+ rotulos: (itens) => criarRotulos(palco, camada, itens),
679
+ orbita: (pose) => orbita(camera, pose),
680
+ distanciaParaCaber: (raio, folga) => distanciaParaCaber(camera, raio, H, area, folga),
681
+ distanciaParaCaixa: (largura, altura, folga) => distanciaParaCaixa(camera, largura, altura, H, area, folga),
682
+ // a distância em que a caixa (THREE.Box3) cabe inteira na área, vista da pose {alvo, azim, elev}
683
+ distanciaQueCabe: (caixa, pose, folga) => distanciaQueCabe(camera, W, H, area, caixa, pose, folga),
684
+ };
685
+ try { await document.fonts.load(`800 ${Math.round(36 * palco.escala)}px Montserrat`); } catch (e) { /* segue */ }
686
+ let atualizar;
687
+ try {
688
+ atualizar = await opcoes.construir(palco);
689
+ } catch (e) {
690
+ return falha(`erro ao montar os objetos: ${e && e.message ? e.message : e}`);
691
+ }
692
+ // conferência: em cada quadro da cena, a câmera fica fora de todos os sólidos registrados
693
+ const DUR = C.duracao || 5;
694
+ const caixaDe = new THREE.Box3();
695
+ const folga = camera.near + 0.03;
696
+ const vistos = new Set();
697
+ for (let f = 0; f <= Math.ceil(DUR * 30); f++) {
698
+ const t = Math.min(DUR, f / 30);
699
+ atualizar(t);
700
+ for (const s of solidos) {
701
+ caixaDe.setFromObject(s.obj).expandByScalar(folga);
702
+ if (caixaDe.containsPoint(camera.position) && !vistos.has(s.nome)) {
703
+ vistos.add(s.nome);
704
+ const msg = `a câmera entra em "${s.nome}" em ${t.toFixed(2)} s: afaste a câmera ou mude o caminho`;
705
+ estado.avisos.push(msg);
706
+ console.error(`[cena 3D ${id}] ${msg}`);
707
+ }
708
+ }
709
+ }
710
+ const desenhar = (t) => {
711
+ const tt = lim(Number(t) || 0, 0, DUR);
712
+ estado.t = tt; // o último tempo desenhado (diagnóstico: confira no console do estúdio)
713
+ atualizar(tt);
714
+ for (const tx of texturas) tx.atualizar();
715
+ if (rec) {
716
+ renderer.setScissorTest(false);
717
+ renderer.setClearColor(0x000000, 0);
718
+ renderer.clear();
719
+ renderer.setScissorTest(true);
720
+ renderer.setScissor(rec[0], H - rec[3], rec[2] - rec[0], rec[3] - rec[1]);
721
+ }
722
+ renderer.render(cena, camera);
723
+ };
724
+ estado.pronto = true;
725
+ estado.montagemMs = Math.round(performance.now() - t0);
726
+ estado.desenhar = desenhar;
727
+ return desenhar;
728
+ }