adiciona modelo real
This commit is contained in:
1 parent
71d13753c9
commit
3a659a52c7
18 files changed
+612
-176
No files matched your search
@@ -5,7 +5,10 @@ import { fileURLToPath } from "node:url";
|
||||
import { buildGeometry } from "./geometry.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const manifestPath = path.resolve(__dirname, "../seed/manifest.json");
|
||||
const manifestPath = path.resolve(
|
||||
__dirname,
|
||||
process.argv[2] ?? "../seed/manifest-tegumentar.json",
|
||||
);
|
||||
const outDir = path.resolve(__dirname, "../seed/models");
|
||||
mkdirSync(outDir, { recursive: true });
|
||||
|
||||
@@ -80,9 +83,9 @@ for (const [systemSlug, items] of bySystem) {
|
||||
.setIndices(idxAccessor)
|
||||
.setMaterial(material);
|
||||
|
||||
const mesh = doc.createMesh(item.slug).addPrimitive(prim);
|
||||
const mesh = doc.createMesh(item.meshName).addPrimitive(prim);
|
||||
const node = doc
|
||||
.createNode(item.slug)
|
||||
.createNode(item.meshName)
|
||||
.setMesh(mesh)
|
||||
.setTranslation(item.position)
|
||||
.setRotation(eulerToQuat(item.rotation ?? [0, 0, 0]));
|
||||
|
||||
@@ -0,0 +1,24 @@
|
||||
import { execFileSync } from "node:child_process";
|
||||
import { existsSync, mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { SOURCES } from "./sources.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const RAW_DIR = path.resolve(__dirname, "../raw");
|
||||
const CONVERTED_DIR = path.resolve(__dirname, "../converted");
|
||||
mkdirSync(CONVERTED_DIR, { recursive: true });
|
||||
|
||||
for (const { fbx, key } of SOURCES) {
|
||||
const inPath = path.join(RAW_DIR, fbx);
|
||||
const outPath = path.join(CONVERTED_DIR, `${key}.glb`);
|
||||
if (existsSync(outPath)) {
|
||||
console.log(`✓ já convertido: ${fbx}`);
|
||||
continue;
|
||||
}
|
||||
console.log(`⚙ convertendo ${fbx} → ${key}.glb ...`);
|
||||
execFileSync("assimp", ["export", inPath, outPath, "-f", "glb2"], { stdio: "inherit" });
|
||||
console.log(`✓ ${key}.glb gerado`);
|
||||
}
|
||||
|
||||
console.log("Conversão FBX → glTF concluída.");
|
||||
@@ -0,0 +1,136 @@
|
||||
// Lê os .glb convertidos do FBX real (Z-Anatomy), recolore por sistema,
|
||||
// garante nomes de nó únicos por arquivo (Z-Anatomy tem estruturas
|
||||
// repetidas — costelas, vértebras, falanges — que podem compartilhar o
|
||||
// mesmo nome bruto) e gera:
|
||||
// - data-pipeline/seed/models/<arquivo>.glb (final, servido pela API)
|
||||
// - data-pipeline/seed/manifest-real.json (entradas para o seed do banco)
|
||||
import { NodeIO } from "@gltf-transform/core";
|
||||
import { ALL_EXTENSIONS } from "@gltf-transform/extensions";
|
||||
import { prune, dedup } from "@gltf-transform/functions";
|
||||
import { writeFileSync, mkdirSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { SOURCES, SOURCE_TARGETS, RESPIRATORY_KEYWORDS } from "./sources.mjs";
|
||||
import { translateToPt } from "./translate-pt.mjs";
|
||||
import { SYSTEMS } from "../seed/systems.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const CONVERTED_DIR = path.resolve(__dirname, "../converted");
|
||||
const MODELS_OUT_DIR = path.resolve(__dirname, "../seed/models");
|
||||
mkdirSync(MODELS_OUT_DIR, { recursive: true });
|
||||
|
||||
const systemBySlug = new Map(SYSTEMS.map((s) => [s.slug, s]));
|
||||
|
||||
function hexToRgb(hex) {
|
||||
const n = parseInt(hex.replace("#", ""), 16);
|
||||
return [((n >> 16) & 255) / 255, ((n >> 8) & 255) / 255, (n & 255) / 255, 1];
|
||||
}
|
||||
|
||||
const COMBINING_MARKS = new RegExp("[\\u0300-\\u036f]", "g");
|
||||
|
||||
function slugify(s) {
|
||||
return s
|
||||
.normalize("NFD")
|
||||
.replace(COMBINING_MARKS, "") // remove diacríticos (á -> a, ç -> c, ...)
|
||||
.toLowerCase()
|
||||
.replace(/[^a-z0-9]+/g, "-")
|
||||
.replace(/^-+|-+$/g, "");
|
||||
}
|
||||
|
||||
function classifySystem(key, rawName) {
|
||||
const target = SOURCE_TARGETS[key];
|
||||
if (target.system) return target.system;
|
||||
// Só o "visceral" tem system === null: decide por palavra-chave.
|
||||
const lower = rawName.toLowerCase();
|
||||
return RESPIRATORY_KEYWORDS.some((kw) => lower.includes(kw)) ? "respiratorio" : "visceral";
|
||||
}
|
||||
|
||||
const io = new NodeIO().registerExtensions(ALL_EXTENSIONS);
|
||||
const manifest = [];
|
||||
let totalStructures = 0;
|
||||
|
||||
for (const { fbx, key } of SOURCES) {
|
||||
const inPath = path.join(CONVERTED_DIR, `${key}.glb`);
|
||||
const target = SOURCE_TARGETS[key];
|
||||
console.log(`Processando ${fbx} (${key})...`);
|
||||
|
||||
const doc = io.read(inPath);
|
||||
const root = doc.getRoot();
|
||||
const scene = root.getDefaultScene() ?? root.listScenes()[0];
|
||||
if (!scene) {
|
||||
console.warn(` ⚠ nenhuma cena encontrada em ${inPath}, pulando.`);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Um material por sistema alvo deste arquivo (recoloração consistente,
|
||||
// independente de como a FBX/Unity original colorizava as malhas).
|
||||
const materialCache = new Map();
|
||||
function materialFor(systemSlug) {
|
||||
if (materialCache.has(systemSlug)) return materialCache.get(systemSlug);
|
||||
const sys = systemBySlug.get(systemSlug);
|
||||
const mat = doc
|
||||
.createMaterial(systemSlug)
|
||||
.setBaseColorFactor(hexToRgb(sys?.colorHex ?? "#cccccc"))
|
||||
.setRoughnessFactor(0.55)
|
||||
.setMetallicFactor(0.05);
|
||||
materialCache.set(systemSlug, mat);
|
||||
return mat;
|
||||
}
|
||||
|
||||
const nameCounts = new Map(); // dedupe de nomes brutos duplicados no MESMO arquivo
|
||||
let countThisFile = 0;
|
||||
|
||||
function visit(node) {
|
||||
const mesh = node.getMesh();
|
||||
if (mesh) {
|
||||
const rawName = (node.getName() || mesh.getName() || "Estrutura sem nome").trim();
|
||||
const n = (nameCounts.get(rawName) ?? 0) + 1;
|
||||
nameCounts.set(rawName, n);
|
||||
const uniqueMeshName = n === 1 ? rawName : `${rawName} (${n})`;
|
||||
node.setName(uniqueMeshName);
|
||||
|
||||
const systemSlug = classifySystem(key, rawName);
|
||||
const material = materialFor(systemSlug);
|
||||
for (const prim of mesh.listPrimitives()) prim.setMaterial(material);
|
||||
|
||||
const sys = systemBySlug.get(systemSlug);
|
||||
const slugPart = slugify(uniqueMeshName) || `sem-nome-${totalStructures}`;
|
||||
const slug = `${systemSlug}-${slugPart}`;
|
||||
|
||||
manifest.push({
|
||||
system: systemSlug,
|
||||
slug,
|
||||
namePt: translateToPt(rawName) + (n > 1 ? ` (${n})` : ""),
|
||||
nameEn: rawName + (n > 1 ? ` (${n})` : ""),
|
||||
synonyms: [],
|
||||
description: "",
|
||||
regionPath: [sys?.namePt ?? systemSlug],
|
||||
modelFile: target.modelFile,
|
||||
meshName: uniqueMeshName,
|
||||
});
|
||||
countThisFile += 1;
|
||||
totalStructures += 1;
|
||||
}
|
||||
for (const child of node.listChildren()) visit(child);
|
||||
}
|
||||
|
||||
for (const topNode of scene.listChildren()) visit(topNode);
|
||||
|
||||
console.log(` ${countThisFile} estruturas nomeadas encontradas.`);
|
||||
if (countThisFile === 0) {
|
||||
console.warn(` ⚠ nenhuma malha nomeada encontrada em ${fbx} — confira o log do assimp/conversão.`);
|
||||
}
|
||||
|
||||
await doc.transform(prune(), dedup());
|
||||
|
||||
const outPath = path.join(MODELS_OUT_DIR, target.modelFile);
|
||||
await io.write(outPath, doc);
|
||||
console.log(` ✓ ${target.modelFile} salvo em seed/models/`);
|
||||
}
|
||||
|
||||
writeFileSync(
|
||||
path.resolve(__dirname, "../seed/manifest-real.json"),
|
||||
JSON.stringify({ structures: manifest }, null, 2),
|
||||
);
|
||||
|
||||
console.log(`\nTotal: ${totalStructures} estruturas reais extraídas do Z-Anatomy.`);
|
||||
@@ -0,0 +1,32 @@
|
||||
import { createWriteStream, existsSync, mkdirSync, statSync } from "node:fs";
|
||||
import { pipeline } from "node:stream/promises";
|
||||
import { Readable } from "node:stream";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { RAW_BASE, SOURCES } from "./sources.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const RAW_DIR = path.resolve(__dirname, "../raw");
|
||||
mkdirSync(RAW_DIR, { recursive: true });
|
||||
|
||||
async function download(fbx) {
|
||||
const dest = path.join(RAW_DIR, fbx);
|
||||
if (existsSync(dest) && statSync(dest).size > 0) {
|
||||
console.log(`✓ já baixado: ${fbx}`);
|
||||
return;
|
||||
}
|
||||
const url = RAW_BASE + fbx;
|
||||
console.log(`↓ baixando ${fbx}...`);
|
||||
const res = await fetch(url);
|
||||
if (!res.ok || !res.body) {
|
||||
throw new Error(`Falha ao baixar ${fbx}: HTTP ${res.status} (${url})`);
|
||||
}
|
||||
await pipeline(Readable.fromWeb(res.body), createWriteStream(dest));
|
||||
console.log(`✓ ${fbx} baixado (${(statSync(dest).size / 1024 / 1024).toFixed(1)} MB)`);
|
||||
}
|
||||
|
||||
for (const { fbx } of SOURCES) {
|
||||
await download(fbx);
|
||||
}
|
||||
|
||||
console.log("Todos os modelos-fonte baixados.");
|
||||
@@ -0,0 +1,18 @@
|
||||
import { readFileSync, writeFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const SEED_DIR = path.resolve(__dirname, "../seed");
|
||||
|
||||
const real = JSON.parse(readFileSync(path.join(SEED_DIR, "manifest-real.json"), "utf-8"));
|
||||
const tegumentar = JSON.parse(readFileSync(path.join(SEED_DIR, "manifest-tegumentar.json"), "utf-8"));
|
||||
|
||||
const structures = [...real.structures, ...tegumentar.structures];
|
||||
|
||||
writeFileSync(
|
||||
path.join(SEED_DIR, "manifest.json"),
|
||||
JSON.stringify({ structures }, null, 2),
|
||||
);
|
||||
|
||||
console.log(`manifest.json final: ${structures.length} estruturas (${real.structures.length} reais + ${tegumentar.structures.length} tegumentar).`);
|
||||
@@ -6,10 +6,11 @@ import { readFileSync } from "node:fs";
|
||||
import path from "node:path";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import pg from "pg";
|
||||
import { SYSTEMS as systems } from "../seed/systems.mjs";
|
||||
|
||||
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
||||
const manifestPath = path.resolve(__dirname, "../seed/manifest.json");
|
||||
const { systems, structures } = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
||||
const { structures } = JSON.parse(readFileSync(manifestPath, "utf-8"));
|
||||
|
||||
const connectionString = process.env.DATABASE_URL;
|
||||
if (!connectionString) {
|
||||
@@ -80,8 +81,8 @@ try {
|
||||
st.synonyms ?? [],
|
||||
st.description ?? null,
|
||||
st.regionPath ?? [],
|
||||
`${st.system}.glb`,
|
||||
st.slug,
|
||||
st.modelFile,
|
||||
st.meshName,
|
||||
systemId,
|
||||
],
|
||||
);
|
||||
|
||||
@@ -0,0 +1,47 @@
|
||||
// Fonte real dos modelos: Z-Anatomy-Sample (github.com/LluisV/Z-Anatomy-Sample),
|
||||
// CC-BY-SA 4.0 — reempacota o dataset BodyParts3D/FMA em malhas organizadas
|
||||
// por sistema, com nomes de objeto reais em inglês. Servidos como blobs FBX
|
||||
// binários comuns (não Git LFS), então dá para baixar via raw.githubusercontent.com.
|
||||
//
|
||||
// Deixamos de fora "Regions of human body100.fbx" e "Reference lines,
|
||||
// planes and movements 1.fbx" nesta primeira importação (nomes com espaço,
|
||||
// conteúdo mais voltado a navegação/didática do que estruturas discretas) —
|
||||
// dá para adicionar depois seguindo o mesmo padrão.
|
||||
export const RAW_BASE =
|
||||
"https://raw.githubusercontent.com/LluisV/Z-Anatomy-Sample/main/Assets/Models/1.0%20Models/";
|
||||
|
||||
export const SOURCES = [
|
||||
{ fbx: "SkeletalSystem100.fbx", key: "skeletal" },
|
||||
{ fbx: "MuscularSystem100.fbx", key: "muscular" },
|
||||
{ fbx: "NervousSystem100.fbx", key: "nervous" },
|
||||
{ fbx: "CardioVascular41.fbx", key: "cardio" },
|
||||
{ fbx: "VisceralSystem100.fbx", key: "visceral" },
|
||||
{ fbx: "Joints100.fbx", key: "joints" },
|
||||
{ fbx: "LymphoidOrgans100.fbx", key: "lymphoid" },
|
||||
];
|
||||
|
||||
// Palavras-chave (substring, case-insensitive) que classificam um nó do
|
||||
// VisceralSystem100.fbx como respiratório em vez de "visceral genérico".
|
||||
export const RESPIRATORY_KEYWORDS = [
|
||||
"lung",
|
||||
"trachea",
|
||||
"bronch",
|
||||
"larynx",
|
||||
"pharynx",
|
||||
"diaphragm",
|
||||
"pleura",
|
||||
"alveol",
|
||||
];
|
||||
|
||||
// key (fonte) -> { modelFile, system } — "visceral" é especial: o system
|
||||
// real é decidido por nó (respiratorio vs visceral) via RESPIRATORY_KEYWORDS,
|
||||
// mas os dois compartilham o mesmo arquivo .glb final.
|
||||
export const SOURCE_TARGETS = {
|
||||
skeletal: { modelFile: "esqueletico.glb", system: "esqueletico" },
|
||||
muscular: { modelFile: "muscular.glb", system: "muscular" },
|
||||
nervous: { modelFile: "nervoso.glb", system: "nervoso" },
|
||||
cardio: { modelFile: "cardiovascular.glb", system: "cardiovascular" },
|
||||
visceral: { modelFile: "visceral-organs.glb", system: null },
|
||||
joints: { modelFile: "articular.glb", system: "articular" },
|
||||
lymphoid: { modelFile: "linfatico.glb", system: "linfatico" },
|
||||
};
|
||||
@@ -0,0 +1,174 @@
|
||||
// Tradução EN→PT-BR por correspondência EXATA (não palavra-por-palavra).
|
||||
//
|
||||
// Decisão deliberada: este é um app de estudo para prova de anatomia — uma
|
||||
// tradução errada de um termo médico é pior do que nenhuma tradução. Por
|
||||
// isso só traduzimos quando o nome bate 100% (após normalizar espaços e
|
||||
// remover prefixo/sufixo "Left"/"Right") com uma entrada conhecida deste
|
||||
// dicionário. Fora daqui, o nome original em inglês/latim é mantido —
|
||||
// nomenclatura anatômica internacional também é usada normalmente no
|
||||
// ensino de anatomia no Brasil, então isso não compromete o estudo.
|
||||
const EXACT_PT = {
|
||||
// Ossos
|
||||
"skull": "Crânio",
|
||||
"mandible": "Mandíbula",
|
||||
"maxilla": "Maxila",
|
||||
"frontal bone": "Osso Frontal",
|
||||
"parietal bone": "Osso Parietal",
|
||||
"occipital bone": "Osso Occipital",
|
||||
"temporal bone": "Osso Temporal",
|
||||
"sphenoid bone": "Osso Esfenoide",
|
||||
"ethmoid bone": "Osso Etmoide",
|
||||
"zygomatic bone": "Osso Zigomático",
|
||||
"nasal bone": "Osso Nasal",
|
||||
"hyoid bone": "Osso Hioide",
|
||||
"clavicle": "Clavícula",
|
||||
"scapula": "Escápula",
|
||||
"sternum": "Esterno",
|
||||
"rib": "Costela",
|
||||
"humerus": "Úmero",
|
||||
"radius": "Rádio",
|
||||
"ulna": "Ulna",
|
||||
"femur": "Fêmur",
|
||||
"patella": "Patela",
|
||||
"tibia": "Tíbia",
|
||||
"fibula": "Fíbula",
|
||||
"pelvis": "Pelve",
|
||||
"sacrum": "Sacro",
|
||||
"coccyx": "Cóccix",
|
||||
"ilium": "Ílio",
|
||||
"ischium": "Ísquio",
|
||||
"pubis": "Púbis",
|
||||
"vertebra": "Vértebra",
|
||||
"atlas": "Atlas (C1)",
|
||||
"axis": "Áxis (C2)",
|
||||
|
||||
// Músculos
|
||||
"deltoid": "Deltoide",
|
||||
"trapezius": "Trapézio",
|
||||
"biceps brachii": "Bíceps Braquial",
|
||||
"triceps brachii": "Tríceps Braquial",
|
||||
"pectoralis major": "Peitoral Maior",
|
||||
"pectoralis minor": "Peitoral Menor",
|
||||
"rectus abdominis": "Reto Abdominal",
|
||||
"external oblique": "Oblíquo Externo do Abdome",
|
||||
"internal oblique": "Oblíquo Interno do Abdome",
|
||||
"latissimus dorsi": "Latíssimo do Dorso",
|
||||
"gluteus maximus": "Glúteo Máximo",
|
||||
"gluteus medius": "Glúteo Médio",
|
||||
"gluteus minimus": "Glúteo Mínimo",
|
||||
"sartorius": "Sartório",
|
||||
"gracilis": "Grácil",
|
||||
"biceps femoris": "Bíceps Femoral",
|
||||
"semitendinosus": "Semitendíneo",
|
||||
"semimembranosus": "Semimembranáceo",
|
||||
"gastrocnemius": "Gastrocnêmio",
|
||||
"soleus": "Sóleo",
|
||||
"tibialis anterior": "Tibial Anterior",
|
||||
"sternocleidomastoid": "Esternocleidomastóideo",
|
||||
"masseter": "Masseter",
|
||||
"temporalis": "Temporal (músculo)",
|
||||
"frontalis": "Frontal (músculo)",
|
||||
"diaphragm": "Diafragma",
|
||||
|
||||
// Sistema nervoso
|
||||
"brain": "Encéfalo",
|
||||
"cerebrum": "Cérebro",
|
||||
"cerebellum": "Cerebelo",
|
||||
"brainstem": "Tronco Encefálico",
|
||||
"spinal cord": "Medula Espinhal",
|
||||
"sciatic nerve": "Nervo Isquiático (Ciático)",
|
||||
"femoral nerve": "Nervo Femoral",
|
||||
"median nerve": "Nervo Mediano",
|
||||
"ulnar nerve": "Nervo Ulnar",
|
||||
"radial nerve": "Nervo Radial",
|
||||
"phrenic nerve": "Nervo Frênico",
|
||||
"vagus nerve": "Nervo Vago",
|
||||
"facial nerve": "Nervo Facial",
|
||||
"trigeminal nerve": "Nervo Trigêmeo",
|
||||
"optic nerve": "Nervo Óptico",
|
||||
"olfactory nerve": "Nervo Olfatório",
|
||||
"brachial plexus": "Plexo Braquial",
|
||||
|
||||
// Cardiovascular
|
||||
"heart": "Coração",
|
||||
"aorta": "Aorta",
|
||||
"superior vena cava": "Veia Cava Superior",
|
||||
"inferior vena cava": "Veia Cava Inferior",
|
||||
"carotid artery": "Artéria Carótida",
|
||||
"jugular vein": "Veia Jugular",
|
||||
"subclavian artery": "Artéria Subclávia",
|
||||
"brachial artery": "Artéria Braquial",
|
||||
"femoral artery": "Artéria Femoral",
|
||||
"pulmonary artery": "Artéria Pulmonar",
|
||||
"pulmonary vein": "Veia Pulmonar",
|
||||
"coronary artery": "Artéria Coronária",
|
||||
|
||||
// Respiratório
|
||||
"trachea": "Traqueia",
|
||||
"larynx": "Laringe",
|
||||
"pharynx": "Faringe",
|
||||
"lung": "Pulmão",
|
||||
"bronchus": "Brônquio",
|
||||
|
||||
// Vísceras
|
||||
"liver": "Fígado",
|
||||
"stomach": "Estômago",
|
||||
"kidney": "Rim",
|
||||
"bladder": "Bexiga",
|
||||
"pancreas": "Pâncreas",
|
||||
"spleen": "Baço",
|
||||
"esophagus": "Esôfago",
|
||||
"colon": "Cólon",
|
||||
"small intestine": "Intestino Delgado",
|
||||
"large intestine": "Intestino Grosso",
|
||||
"thyroid": "Tireoide",
|
||||
"adrenal gland": "Glândula Suprarrenal",
|
||||
"ovary": "Ovário",
|
||||
"testis": "Testículo",
|
||||
"uterus": "Útero",
|
||||
"prostate": "Próstata",
|
||||
|
||||
// Articulações / linfático
|
||||
"knee joint": "Articulação do Joelho",
|
||||
"elbow joint": "Articulação do Cotovelo",
|
||||
"shoulder joint": "Articulação do Ombro",
|
||||
"hip joint": "Articulação do Quadril",
|
||||
"ankle joint": "Articulação do Tornozelo",
|
||||
"wrist joint": "Articulação do Punho",
|
||||
"lymph node": "Linfonodo",
|
||||
"thymus": "Timo",
|
||||
"tonsil": "Tonsila",
|
||||
};
|
||||
|
||||
function normalize(s) {
|
||||
return s.trim().toLowerCase().replace(/\s+/g, " ");
|
||||
}
|
||||
|
||||
/**
|
||||
* Traduz um nome anatômico em inglês para PT-BR quando há correspondência
|
||||
* exata (ignorando prefixo/sufixo Left/Right), senão retorna o original.
|
||||
*/
|
||||
export function translateToPt(rawName) {
|
||||
const trimmed = rawName.trim().replace(/\s+/g, " ");
|
||||
const norm = normalize(trimmed);
|
||||
|
||||
let side = null;
|
||||
let base = norm;
|
||||
if (/^left\b/.test(norm)) {
|
||||
side = "Esquerdo";
|
||||
base = norm.replace(/^left\s*/, "");
|
||||
} else if (/^right\b/.test(norm)) {
|
||||
side = "Direito";
|
||||
base = norm.replace(/^right\s*/, "");
|
||||
} else if (/\bleft$/.test(norm)) {
|
||||
side = "Esquerdo";
|
||||
base = norm.replace(/\s*left$/, "");
|
||||
} else if (/\bright$/.test(norm)) {
|
||||
side = "Direito";
|
||||
base = norm.replace(/\s*right$/, "");
|
||||
}
|
||||
|
||||
const translated = EXACT_PT[base];
|
||||
if (!translated) return trimmed;
|
||||
return side ? `${translated} ${side}` : translated;
|
||||
}
|
||||
Reference in new issue
Block a user