30.1 Del modelo físico al modelo de datos
Una simulación necesita traducir cuerpos, vectores y contactos a valores organizados. La representación elegida debe conservar el significado físico y facilitar las operaciones que realizaremos con frecuencia.
30.2 Representar un vector
En dos dimensiones podemos usar un objeto con propiedades numéricas x e y:
const velocidad = { x: 3, y: -2 };
const impulso = { x: -4, y: 1.5 };
console.log("Velocidad:", velocidad, "m/s");
console.log("Impulso:", impulso, "N·s");El mismo formato sirve para posición, velocidad, momento, fuerza e impulso. El nombre de la variable y la documentación indican qué magnitud y unidad representa.
30.3 Estructura mínima de un cuerpo
const cuerpo = {
id: "A",
masa: 2,
radio: 0.5,
posicion: { x: 1, y: 3 },
velocidad: { x: 4, y: -1 }
};
console.log("Cuerpo creado:");
console.log(JSON.stringify(cuerpo, null, 2));id permite relacionar registros y mensajes con el cuerpo.masa y radio describen parámetros del modelo.posicion y velocidad cambian durante la simulación.30.4 Nombres, unidades y convenciones
Antes de programar conviene documentar una convención única. Por ejemplo: kilogramos, metros, segundos, ángulos en radianes y ejes cartesianos con +y hacia arriba.
| Propiedad | Tipo | Unidad o regla |
|---|---|---|
masa | number | kg, estrictamente positiva. |
radio | number | m, positivo. |
posicion | vector | m, coordenadas globales. |
velocidad | vector | m/s, coordenadas globales. |
30.5 Crear y validar cuerpos
Una función constructora centraliza valores predeterminados y rechaza estados inválidos:
function validarVector(vector, nombre = "vector") {
if (!vector || !Number.isFinite(vector.x) || !Number.isFinite(vector.y)) {
throw new Error(`${nombre} debe tener componentes x e y finitas`);
}
}
function crearCuerpo({ id, masa, radio, posicion, velocidad }) {
if (!id) throw new Error("Falta el identificador");
if (!Number.isFinite(masa) || masa <= 0) {
throw new Error("La masa debe ser positiva y finita");
}
if (!Number.isFinite(radio) || radio <= 0) {
throw new Error("El radio debe ser positivo y finito");
}
validarVector(posicion, "posición");
validarVector(velocidad, "velocidad");
return {
id,
masa,
radio,
posicion: { ...posicion },
velocidad: { ...velocidad }
};
}
const cuerpo = crearCuerpo({
id: "A", masa: 2, radio: 0.5,
posicion: { x: 1, y: 3 }, velocidad: { x: 4, y: -1 }
});
console.log("Cuerpo validado:", cuerpo);30.6 Validar vectores
function validarVector(vector, nombre = "vector") {
if (!vector || !Number.isFinite(vector.x) || !Number.isFinite(vector.y)) {
throw new Error(`${nombre} debe tener componentes x e y finitas`);
}
return vector;
}
console.log("Vector válido:", validarVector({ x: 3, y: -2 }, "velocidad"));
try {
validarVector({ x: NaN, y: 1 }, "impulso");
} catch (error) {
console.log("Vector rechazado:", error.message);
}typeof NaN es "number"; por eso Number.isFinite es más útil que comprobar solamente el tipo.
30.7 Operaciones vectoriales puras
Funciones pequeñas permiten escribir las ecuaciones de forma cercana a su notación matemática:
const sumar = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
const restar = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
const escalar = (k, v) => ({ x: k * v.x, y: k * v.y });
const dot = (a, b) => a.x * b.x + a.y * b.y;
const modulo = v => Math.hypot(v.x, v.y);
const a = { x: 3, y: 4 };
const b = { x: -1, y: 2 };
console.log("a + b =", sumar(a, b));
console.log("a - b =", restar(a, b));
console.log("2a =", escalar(2, a));
console.log("a · b =", dot(a, b));
console.log("|a| =", modulo(a));Estas funciones devuelven objetos nuevos y no modifican sus argumentos, lo que facilita probarlas y combinarlas.
30.8 Magnitudes derivadas
El momento y la energía cinética se calculan desde el estado actual:
const escalar = (k, v) => ({ x: k * v.x, y: k * v.y });
const dot = (a, b) => a.x * b.x + a.y * b.y;
function momento(cuerpo) {
return escalar(cuerpo.masa, cuerpo.velocidad);
}
function energiaCinetica(cuerpo) {
return 0.5 * cuerpo.masa * dot(cuerpo.velocidad, cuerpo.velocidad);
}
const cuerpo = { masa: 2, velocidad: { x: 3, y: 4 } };
console.log("Momento lineal:", momento(cuerpo), "kg·m/s");
console.log("Energía cinética:", energiaCinetica(cuerpo), "J");No es necesario guardar estas magnitudes dentro del cuerpo. Si se derivan al solicitarlas, no pueden quedar desactualizadas respecto de la velocidad.
30.9 Representar y aplicar un impulso
Un impulso es un vector. Para masa constante:
function validarVector(vector, nombre = "vector") {
if (!vector || !Number.isFinite(vector.x) || !Number.isFinite(vector.y)) {
throw new Error(`${nombre} debe tener componentes finitas`);
}
}
function aplicarImpulso(cuerpo, impulso) {
validarVector(impulso, "impulso");
cuerpo.velocidad.x += impulso.x / cuerpo.masa;
cuerpo.velocidad.y += impulso.y / cuerpo.masa;
}
const cuerpo = { masa: 2, velocidad: { x: 3, y: -1 } };
console.log("Velocidad inicial:", { ...cuerpo.velocidad });
aplicarImpulso(cuerpo, { x: 4, y: 2 });
console.log("Velocidad final:", cuerpo.velocidad);La función modifica intencionalmente el estado del cuerpo. Su nombre debe hacer visible ese efecto.
30.10 Mutación o actualización inmutable
| Enfoque | Ventaja | Precaución |
|---|---|---|
| Mutable | Menos asignaciones y acceso directo al estado actual. | Debe controlarse exactamente cuándo se escribe. |
| Inmutable | Conserva estados anteriores y facilita depuración. | Requiere crear copias, incluidas las propiedades anidadas. |
const sumar = (a, b) => ({ x: a.x + b.x, y: a.y + b.y });
const escalar = (k, v) => ({ x: k * v.x, y: k * v.y });
function conImpulso(cuerpo, impulso) {
return {
...cuerpo,
velocidad: sumar(cuerpo.velocidad, escalar(1 / cuerpo.masa, impulso))
};
}
const original = { id: "A", masa: 2, velocidad: { x: 3, y: -1 } };
const actualizado = conImpulso(original, { x: 4, y: 2 });
console.log("Original sin cambios:", original);
console.log("Nuevo estado:", actualizado);
console.log("¿Son objetos distintos?", original !== actualizado);30.11 Representar un contacto
El contacto describe una relación temporal entre dos cuerpos, no una propiedad permanente de uno solo:
const contacto = {
cuerpoA: "A",
cuerpoB: "B",
normal: { x: 0.8, y: 0.6 },
punto: { x: 4.2, y: 1.1 },
penetracion: 0.003,
restitucion: 0.7
};
console.log("Contacto detectado:");
console.log(JSON.stringify(contacto, null, 2));La normal debe tener una orientación documentada, por ejemplo desde A hacia B.
30.12 Separar detección y respuesta
La detección decide si existe contacto y calcula su geometría. La respuesta usa esa información para obtener impulsos y actualizar velocidades.
Esta separación permite cambiar la forma de detectar sin reescribir las ecuaciones físicas.
30.13 Registrar una colisión resuelta
Un registro facilita depuración, reproducción y pruebas:
const evento = {
tipo: "colision",
tiempo: 1.275,
participantes: ["A", "B"],
normal: { x: 1, y: 0 },
impulsoEscalar: 8.4,
velocidadesAntes: {
A: { x: 5, y: 0 },
B: { x: 0, y: 0 }
},
velocidadesDespues: {
A: { x: 0.8, y: 0 },
B: { x: 2.8, y: 0 }
}
};
console.log("Evento registrado:");
console.log(JSON.stringify(evento, null, 2));30.14 Identificadores y referencias
Guardar identificadores en los eventos evita duplicar cuerpos completos y luego modificar una copia equivocada. Una colección puede indexarse con Map:
const cuerpos = [
{ id: "A", masa: 2 },
{ id: "B", masa: 3 }
];
const contacto = { cuerpoA: "A", cuerpoB: "B" };
const porId = new Map(cuerpos.map(cuerpo => [cuerpo.id, cuerpo]));
const cuerpoA = porId.get(contacto.cuerpoA);
const cuerpoB = porId.get(contacto.cuerpoB);
console.log("Cuerpo referido como A:", cuerpoA);
console.log("Cuerpo referido como B:", cuerpoB);
console.log("Identificadores indexados:", [...porId.keys()]);Los identificadores deben ser únicos y estables durante la vida del cuerpo.
30.15 Colecciones y estado de la simulación
const simulacion = {
tiempo: 0,
cuerpos: [],
eventos: [],
configuracion: {
gravedad: { x: 0, y: 0 },
tolerancia: 1e-9,
maximoEventosPorPaso: 100
}
};
console.log("Estado inicial de la simulación:");
console.log(JSON.stringify(simulacion, null, 2));Separar configuración, estado dinámico y registro evita usar valores mágicos dispersos en el programa.
30.16 Copias superficiales y alias compartidos
El operador de propagación solo copia el primer nivel:
const cuerpo = {
id: "A",
posicion: { x: 1, y: 3 },
velocidad: { x: 4, y: -1 }
};
const copiaIncompleta = { ...cuerpo };
copiaIncompleta.velocidad.x = 99;
// También cambia cuerpo.velocidad.x: ambos comparten el mismo vector.
console.log("Tras modificar la copia superficial:", cuerpo.velocidad);
const copiaCorrecta = {
...cuerpo,
posicion: { ...cuerpo.posicion },
velocidad: { ...cuerpo.velocidad }
};
copiaCorrecta.velocidad.x = -5;
console.log("Copia independiente:", copiaCorrecta.velocidad);
console.log("Original sin el segundo cambio:", cuerpo.velocidad);Este detalle es crítico al guardar estados “antes” y “después”.
30.17 Serializar con JSON
JSON.stringify permite guardar o transmitir estructuras compuestas por datos simples:
const simulacion = {
tiempo: 1.5,
cuerpos: [{ id: "A", masa: 2, velocidad: { x: 3, y: 1 } }],
eventos: [{ tipo: "impulso", tiempo: 1.5 }]
};
const texto = JSON.stringify(simulacion, null, 2);
const restaurada = JSON.parse(texto);
console.log("JSON serializado:");
console.log(texto);
console.log("Estado restaurado:", restaurada);JSON no conserva funciones, instancias especiales, Map, Infinity ni NaN de manera útil. Conviene serializar solo el estado de datos y reconstruir las herramientas del programa.
30.18 Pruebas e invariantes
Las estructuras permiten escribir comprobaciones pequeñas:
- masa y radio son finitos y positivos;
- cada vector posee componentes finitas;
- los identificadores no se repiten;
- una normal declarada unitaria tiene módulo próximo a uno;
- aplicar J⃗ cambia el momento exactamente en J⃗ dentro de la tolerancia;
- un evento conserva copias independientes de los estados anterior y posterior.
30.19 Actividad interactiva: inspector de estado
Configurá un cuerpo y un impulso. Podés aplicarlo varias veces para observar la actualización acumulativa del objeto y sus magnitudes derivadas.
Inspector de cuerpo e impulso
Azul: velocidad actual. Naranja: impulso configurado. Verde: momento actual.
El objeto está validado y listo para recibir el impulso.
30.20 Un módulo físico reutilizable
Podemos reunir creación, actualización y auditoría sin mezclar código de interfaz:
function crearCuerpo(datos) {
if (!datos.id || !Number.isFinite(datos.masa) || Math.sign(datos.masa) !== 1) {
throw new Error("Datos del cuerpo no válidos");
}
return {
...datos,
posicion: { ...datos.posicion },
velocidad: { ...datos.velocidad }
};
}
const escalar = (k, v) => ({ x: k * v.x, y: k * v.y });
const restar = (a, b) => ({ x: a.x - b.x, y: a.y - b.y });
const momento = cuerpo => escalar(cuerpo.masa, cuerpo.velocidad);
function aplicarImpulso(cuerpo, impulso) {
cuerpo.velocidad.x += impulso.x / cuerpo.masa;
cuerpo.velocidad.y += impulso.y / cuerpo.masa;
}
function crearModeloDeCuerpo(datos) {
const cuerpo = crearCuerpo(datos);
return {
obtenerEstado() {
return {
...cuerpo,
posicion: { ...cuerpo.posicion },
velocidad: { ...cuerpo.velocidad }
};
},
aplicar(impulso) {
const momentoAntes = momento(cuerpo);
aplicarImpulso(cuerpo, impulso);
const momentoDespues = momento(cuerpo);
return {
momentoAntes,
momentoDespues,
cambio: restar(momentoDespues, momentoAntes)
};
}
};
}
const modelo = crearModeloDeCuerpo({
id: "A", masa: 2, radio: 0.5,
posicion: { x: 1, y: 3 }, velocidad: { x: 4, y: -1 }
});
console.log("Estado inicial:", modelo.obtenerEstado());
console.log("Auditoría del impulso:", modelo.aplicar({ x: -4, y: 6 }));
console.log("Estado final:", modelo.obtenerEstado());La interfaz gráfica consume este modelo, pero las ecuaciones pueden probarse sin navegador.
30.21 Errores frecuentes y ejercicios
Errores frecuentes
- Representar un vector como texto y luego intentar calcular con él.
- Guardar momento y energía sin actualizarlos al cambiar la velocidad.
- Compartir accidentalmente objetos de posición o velocidad entre cuerpos.
- Mezclar unidades o convenciones de ejes dentro de la misma colección.
- Aceptar masas nulas, componentes
NaNo identificadores duplicados. - Mezclar detección geométrica, respuesta física y dibujo en una sola función.
- Guardar referencias mutables como si fueran fotografías del estado anterior.
Ejercicios propuestos
- Creá un objeto para un cuerpo de 3 kg en (2, −1) m con velocidad (4, 5) m/s.
- Implementá una función
normalizar(v)que rechace vectores casi nulos. - Calculá con funciones el momento y la energía del cuerpo del ejercicio 1.
- Aplicá J⃗ = (−6, 3) N·s y obtené la nueva velocidad.
- Escribí una versión inmutable de
avanzar(cuerpo, dt). - Diseñá un registro para una colisión entre A y B a t = 2,4 s.
- Explicá por qué
{ ...cuerpo }no crea una copia profunda. - Agregá al laboratorio un historial de los impulsos aplicados.
Ver soluciones y orientaciones
{ id: "A", masa: 3, posicion: {x: 2, y: -1}, velocidad: {x: 4, y: 5} }; falta elegir un radio positivo.- Calculá
longitud = Math.hypot(v.x, v.y), comprobá una tolerancia y devolvé{x: v.x/longitud, y: v.y/longitud}. - p⃗ = (12, 15) kg·m/s y K = ½·3·(4² + 5²) = 61,5 J.
- Δv⃗ = J⃗/m = (−2, 1) m/s; v⃗f = (2, 6) m/s.
- Debe devolver un objeto nuevo, copiar
posiciony sumarvelocidad·dtsin modificar el original. - Incluí tipo, tiempo, identificadores, normal, restitución y copias de velocidades antes y después.
- Las propiedades anidadas continúan apuntando a los mismos objetos.
- Guardá una copia del impulso y del estado resultante después de cada aplicación.
30.22 Ideas para recordar
- Los vectores pueden representarse uniformemente como objetos
{x, y}. - Parámetros, estado dinámico y eventos cumplen funciones diferentes.
- Las magnitudes derivadas se calculan desde el estado actual.
- Las operaciones vectoriales puras reducen errores y facilitan pruebas.
- Una función mutable debe hacer explícito que modifica el cuerpo.
- Las copias de objetos anidados deben realizarse de manera intencional.
- Detección, respuesta física y presentación conviene mantenerlas separadas.
- La validación impide que un dato inválido contamine toda la simulación.
En el próximo tema usaremos estas estructuras para calcular velocidades posteriores y comprobar los resultados.