npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2026 – Pkg Stats / Ryan Hefner

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.

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-mundo

Peers: 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 por style y children.
  • CapaDeClicks — la capa inset: 0 que 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, …]), con cerrado para el tramo de cierre de un circuito y activo para 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_ANILLO para el anillo de espera.
  • Ventanas: useArrastrable(inicial, activo), posPanelSuelto(), estiloAsa. VentanaSuelta en editor-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 watch

Desarrollarlo 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.