editor-mundo
v0.2.0
Published
Editor de nivel in-game para mundos React Three Fiber: mapa 2D exacto + edición sobre el terreno 3D, con guardado al repo desde el dev server de Vite.
Maintainers
Readme
editor-mundo
Editor de nivel in-game para mundos hechos con React Three Fiber: se
acomoda la planta sobre un mapa cenital exacto (DOM) y se afina sobre el
terreno real (3D, con física), y el resultado se guarda en el repo del
proyecto desde el dev server de Vite. El editor existe solo en desarrollo;
lo único que un juego puede querer en producción es MapaMarco (el mapa
grande con zoom y paneo), que por eso no arrastra nada del resto.
Nació como el modo edición de Virtual Vitae — la Rambla
(docs/editor-como-motor.md en ese repo es el análisis completo de lo que hace
y por qué). Este paquete es su extracción, tanda por tanda.
Estado
| tanda | qué | estado |
|---|---|---|
| 1 | núcleo puro: matemática 3D, puentes, ciclo de vida, ventanas, zoom del mapa | ✅ editor-mundo |
| 1 | plugin de Vite para guardar al repo + cliente | ✅ editor-mundo/vite |
| 2 | el mapa: MapaMarco, Bandera, CapaDeClicks, Trazos/Trazo, mundoDelEvento, VentanaSuelta | ✅ editor-mundo/mapa |
| 3 | EdicionEnMundo (cámara, congelado, grilla, arrastre, gestos) sobre una Superficie inyectada | ⏳ |
| 4 | registros vivos + formato de instancia + comoCodigo | ⏳ |
| 5 | adaptadores por tipo (sueltos / cadena / partes), el panel común y el pincel | ⏳ |
| 6 | sondas offline (probe-terreno, generar-mapa) como bin | ⏳ |
Instalar
npm install editor-mundoPeers: react y react-dom ≥ 18. vite ≥ 4 solo si se usa el plugin. Cuando
entre la tanda 3 se suman three, @react-three/fiber y (opcional)
@react-three/rapier.
Configurar
import { configurarEditor } from 'editor-mundo';
import { worldToMap, mapToWorld } from './mapMath';
// Una vez, antes de montar nada del editor (en el módulo que lo monta).
configurarEditor({
// mundo (x, z) ↔ imagen del mapa (u, v en 0..1). Tiene que ser EXACTA: la
// imagen se genera desde el mundo y estas son las constantes con las que se
// dibujó, no una calibración a ojo.
proyeccion: { worldToMap, mapToWorld },
// opcional: ¿el foco está en un input? (los atajos son letras). Tiene default.
escribiendoTexto: () => document.activeElement?.tagName === 'INPUT',
});El mapa (editor-mundo/mapa)
MapaMarco es el mapa grande como pantalla: telón, marco, lienzo con zoom por
rueda y paneo, controles de zoom, y los dos modos de apartarse (mini: chico y
arrastrable pero clickeable; apartado: invisible sin desmontar). El zoom es
el ANCHO del lienzo y no un transform: scale(), así que lo que se dibuja
encima conserva su tamaño en pantalla y no necesita saber del zoom.
<MapaMarco
imagen="/static/mapa.png" aspecto={979 / 805}
onCerrar={cerrar} mini={enElMundo} apartado={camaraEnElMovil} editando={hayEditor}
imagenRef={imgRef} onImagenClick={teleport} onMarco={setMarco}
controles={<button …>🛠</button>} pie="rueda = zoom · M cierra"
>
{/* lo que se dibuja sobre el mapa, en % del lienzo: marcadores, el editor */}
</MapaMarco>Sobre el lienzo, un editor usa:
Bandera— un punto arrastrable en%del lienzo, con el anillo del gesto de sostener. Forma, color y contenido porstyleychildren.CapaDeClicks— la capainset: 0que recibe el click de "agregar acá" o el pincel, y se come el click de la imagen.activa={false}la apaga sin desmontarla (el modo del mundo).Trazos+Trazo— un<svg>y polilíneas en coordenadas del mundo ([x, y, z, …]), concerradopara el tramo de cierre de un circuito yactivopara el trazado que se está editando frente a los de referencia.mundoDelEvento(evento, imagen)— de un evento de puntero a{ x, z }del mundo, midiendo contra la imagen (por eso el zoom no les cambia nada).VentanaSuelta— el panel del editor: clavado en el marco (host) o suelto y arrastrable, sin remontar lo de adentro al cambiar.
Una sola copia. Los puentes entre el DOM y el mundo 3D son singletons de
módulo (el contexto de React no cruza el <Canvas>), así que dos copias del
paquete en node_modules son dos editores que no se hablan.
Guardar al repo: el plugin de Vite
// vite.config.js
import { editorMundo } from 'editor-mundo/vite';
export default defineConfig({
plugins: [
react(),
editorMundo({ carpetas: ['src/features'] }), // dónde puede escribir
],
});Levanta tres endpoints solo en vite dev (apply: 'serve'), acotados a
carpetas (relativas a la raíz, default ['src']) y a extensiones de datos
(default json/js/jsx/ts/tsx). Nada absoluto, nada con .. que salga, nada fuera
de esas carpetas: la política es rutaPermitida y tiene tests.
| endpoint | para |
|---|---|
| GET /__editor-mundo/estado | saber si el plugin está (el panel muestra 💾 o solo ⧉) |
| GET /__editor-mundo/leer?archivo= | el texto de un archivo permitido |
| POST /__editor-mundo/guardar { archivo, contenido } | escribirlo (temporal + rename) |
Del lado del editor:
import { guardarArchivo, guardarEntreMarcadores, guardarOCopiar } from 'editor-mundo';
// un .json por lista: se escribe entero
await guardarArchivo('src/features/props/palmeras.json', JSON.stringify(lista, null, 2));
// un array dentro de un .js: se reemplaza lo que hay entre los marcadores
// // editor-mundo:inicio palmeras
// ...
// // editor-mundo:fin palmeras
await guardarEntreMarcadores('src/features/props/palmeras.js', 'palmeras', comoCodigo(lista));
// lo que llama el botón: guarda si hay plugin, si no copia al portapapeles
const resultado = await guardarOCopiar(
() => guardarEntreMarcadores(archivo, id, codigo),
codigo
); // 'guardado' | 'copiado' | 'nada'Después del guardado, el HMR de Vite recarga el módulo solo.
Núcleo (editor-mundo)
configurarEditor({ escribiendoTexto })— lo que el proyecto inyecta. Hoy solo la guarda de "¿el foco está en un input?" (tiene default). Se llama una vez, antes de montar el editor.- Matemática (pura, testeada):
alPaso,mismaCelda,conTope,esPared,baseDeSuperficie,aPlano/aMundo,verticesDeGrilla,verticesEnPlano,segmentosDeGrilla,segmentosEnPlano,paresDeGrilla,propBajoElRayo,ejeDe,sobreElEje,rotarSobreEje,anguloEnPantalla,orbita, y los topes (DISTANCIA_MIN/MAX,PITCH_MIN/MAX,UMBRAL_PISO). - Puentes:
edicion3dRef(el contrato de sesión DOM → 3D, documentado en el archivo),mundo3dRef(3D → teclado:trabarEje,agarrar,rotar,escalar,cancelarGesto),pedirCentrado()/cuantosCentrados(),propEnEdicionRef. - Ciclo de vida:
useModoMundo({ puente, onMini, atajos, alEntrar, avisar, aviso })— el gesto de sostener (MS_PARA_3D), los atajos comunes (R, S, X/Y/Z, F, ESC en capas) y el cleanup.estiloAnillo+CSS_ANILLOpara el anillo de espera. - Ventanas:
useArrastrable(inicial, activo),posPanelSuelto(),estiloAsa.VentanaSueltaeneditor-mundo/mapa(portal que no remonta). - Zoom del mapa:
encajarVista,acercarEn,ZOOM_MAX,VISTA_INICIAL.
Desarrollo
npm install
npm test # vitest
npm run build # esbuild → dist/ (tres entradas, peers externos)
npm run dev # build en watchDesarrollarlo a la par de un proyecto que lo usa, sin republicar ni
enlazar: en el Vite del proyecto, un alias condicional que apunte editor-mundo
y editor-mundo/mapa al src/ de esta carpeta (más server.fs.allow para que
el dev server la sirva y resolve.dedupe: ['react', 'react-dom', 'three'] para
que los peers salgan del proyecto). Vite transforma el JSX del paquete como si
fuera código propio y el HMR llega hasta acá. Virtual-vitae/vite.config.ts lo
tiene resuelto con EDITOR_MUNDO_LOCAL=1, y es el modelo a copiar.
No uses npm link: un paquete enlazado resuelve sus peers desde SU carpeta, y
con un node_modules intermedio que tenga otro react el síntoma es "Invalid
hook call".
Publicar: npm version patch|minor (0.x: cada tanda es un minor) y
npm publish. prepublishOnly corre los tests y el build; una versión
publicada no se pisa.
Cómo se escribe acá
Todo en español. Los comentarios explican por qué, no qué. Lo que se puede equivocar en silencio va en una función pura con test; lo visual se mira.
