diff --git a/api/robert_engine.py b/api/robert_engine.py new file mode 100644 index 00000000..3995ee06 --- /dev/null +++ b/api/robert_engine.py @@ -0,0 +1,150 @@ +""" +Robert Physics Engine V10 — TryOnYou fabric simulation. + +TRYONYOU — Robert Physics Engine (Unified Python Version) +© 2025-2026 Rubén Espinar Rodríguez — All Rights Reserved +Patent: PCT/EP2025/067317 — 22 Claims Protected +""" + +from __future__ import annotations + +import math +import time + + +class RobertEngineV10: + """ + TRYONYOU — Robert Physics Engine (Unified Python Version) + © 2025-2026 Rubén Espinar Rodríguez — All Rights Reserved + Patent: PCT/EP2025/067317 — 22 Claims Protected + """ + + def __init__(self) -> None: + # ─── CONFIGURACIÓN DE LOS 5 LOOKS DEL PILOTO (LAFAYETTE HAUSSMANN) ─── + self.PILOT_COLLECTION: dict[str, dict] = { + "eg0": {"name": "Silk Haussmann", "drape": 0.85, "gsm": 60, "elasticity": 12, "recovery": 95, "friction": 0.22}, + "eg1": {"name": "Business Elite", "drape": 0.35, "gsm": 280, "elasticity": 4, "recovery": 80, "friction": 0.65}, + "eg2": {"name": "Velvet Night", "drape": 0.55, "gsm": 320, "elasticity": 8, "recovery": 88, "friction": 0.45}, + "eg3": {"name": "Tech Shell", "drape": 0.15, "gsm": 180, "elasticity": 2, "recovery": 98, "friction": 0.75}, + "eg4": {"name": "Cashmere Cloud", "drape": 0.70, "gsm": 220, "elasticity": 15, "recovery": 92, "friction": 0.30}, + } + + # ─── MÓDULO 1: FÍSICA DE TEJIDOS (Puntos 1, 3 y 4) ─── + def _calculate_physics( + self, + fabric: dict, + shoulder_w: float, + torso_h: float, + now_ms: int, + ) -> tuple[float, float]: + # Lafayette Factor (Caída) + drape_pull = fabric["drape"] * 0.4 + lafayette_f = 2.2 + (0.5 - drape_pull) + garment_w = shoulder_w * lafayette_f + + # Gravity Stretch (15% Max Elongation) + weight_norm = min(1.0, max(0.0, (fabric["gsm"] - 50) / 350)) + gravity_h = torso_h * (1.0 + (weight_norm * 0.15)) + + # Elasticity Breathing (Oscilación sutil) + amplitude = fabric["elasticity"] * 0.0005 + breathing = 1.0 + (math.sin(now_ms * 0.0015) * amplitude) + + return garment_w * breathing, gravity_h + + # ─── MÓDULO 2: RENDERIZADO AVANZADO (Sombras y Brillos) ─── + def _get_visual_effects( + self, + fabric: dict, + garment_w: float, + gravity_h: float, + now_ms: int, + fit_score: float, + ) -> dict: + effects: dict = {} + + # Silk Highlight (Brillo dinámico si friction < 0.35) + if fabric["friction"] < 0.35: + highlight_x = math.sin(now_ms * 0.001) * garment_w * 0.25 + shine_int = (0.35 - fabric["friction"]) * 0.2 + effects["highlight"] = {"x": highlight_x, "intensity": shine_int} + + # Pliegues y Sombras (Shadow Gradient) + num_folds = 3 if fabric["drape"] < 0.5 else 5 + effects["folds"] = [] + for i in range(num_folds): + opacity = 0.05 + (fabric["drape"] * 0.1) + effects["folds"].append({"offset": i, "alpha": opacity}) + + # Línea de Escaneo Dorada (Ciclo 2000ms) + if 0 < fit_score < 95: + progress = (now_ms % 2000) / 2000 + effects["scan_line_y"] = progress * gravity_h + + return effects + + # ─── MÓDULO 3: ACCESORIOS Y PROBADOR (Punto 2) ─── + def get_accessory_render( + self, + has_body: bool, + shoulder_w: float, + hip_y: float, + canvas_w: float, + img_ar: float, + ) -> dict: + # Opacidad fija 0.88 y escalado al 18% si no hay cuerpo + bag_w = shoulder_w * 0.8 if has_body else canvas_w * 0.18 + bag_h = bag_w * img_ar + return { + "width": bag_w, + "height": bag_h, + "alpha": 0.88, + "mode": "BodyAnchor" if has_body else "FloatingExhibition", + } + + # ─── MÓDULO 4: SOBERANÍA Y MÉTRICAS (Punto 6) ─── + def process_frame( + self, + look_id: str, + shoulder_w: float, + hip_y: float, + fit_score: float, + canvas_dim: dict, + ) -> dict: + fabric = self.PILOT_COLLECTION.get(look_id, self.PILOT_COLLECTION["eg0"]) + now_ms = int(time.time() * 1000) + torso_h = hip_y - 100 # Simplificación de hombroY + + # Ejecución del Motor + w, h = self._calculate_physics(fabric, shoulder_w, torso_h, now_ms) + fx = self._get_visual_effects(fabric, w, h, now_ms, fit_score) + + # Retorno de buffer firmado por la patente + return { + "render": {"width": w, "height": h, "effects": fx}, + "metadata": { + "patent": "PCT/EP2025/067317", + "claim": "22_PROTECTED", + "recovery_status": "STABLE" if fabric["recovery"] > 85 else "DEGRADED", + }, + } + + +# --- EJECUCIÓN MAESTRA --- +if __name__ == "__main__": + engine = RobertEngineV10() + + # Simulación de un frame en Galeries Lafayette + resultado = engine.process_frame( + look_id="eg0", # Silk Haussmann + shoulder_w=450, # Detección biométrica + hip_y=900, # Detección biométrica + fit_score=88, # Escaneo en curso (activará línea dorada) + canvas_dim={"w": 1080, "h": 1920}, + ) + + print("--- ROBERT ENGINE V10 OUTPUT ---") + print(f"Prenda: {engine.PILOT_COLLECTION['eg0']['name']}") + print(f"Ancho Físico: {resultado['render']['width']:.2f}px") + print(f"Efectos Activos: {list(resultado['render']['effects'].keys())}") + print(f"Firma Legal: {resultado['metadata']['patent']}") diff --git a/tests/test_robert_engine.py b/tests/test_robert_engine.py new file mode 100644 index 00000000..26029865 --- /dev/null +++ b/tests/test_robert_engine.py @@ -0,0 +1,230 @@ +"""Tests for RobertEngineV10 — Robert Physics Engine.""" + +from __future__ import annotations + +import math +import os +import sys +import unittest + +_ROOT = os.path.normpath(os.path.join(os.path.dirname(__file__), "..")) +_API = os.path.join(_ROOT, "api") +for _p in (_ROOT, _API): + if _p not in sys.path: + sys.path.insert(0, _p) + +from robert_engine import RobertEngineV10 + + +class TestPilotCollection(unittest.TestCase): + """Module 0: verify the five Pilot Collection looks are correctly configured.""" + + def setUp(self) -> None: + self.engine = RobertEngineV10() + + def test_five_looks_present(self) -> None: + self.assertEqual(len(self.engine.PILOT_COLLECTION), 5) + + def test_all_look_ids_present(self) -> None: + for key in ("eg0", "eg1", "eg2", "eg3", "eg4"): + self.assertIn(key, self.engine.PILOT_COLLECTION) + + def test_silk_haussmann_name(self) -> None: + self.assertEqual(self.engine.PILOT_COLLECTION["eg0"]["name"], "Silk Haussmann") + + def test_business_elite_name(self) -> None: + self.assertEqual(self.engine.PILOT_COLLECTION["eg1"]["name"], "Business Elite") + + def test_velvet_night_name(self) -> None: + self.assertEqual(self.engine.PILOT_COLLECTION["eg2"]["name"], "Velvet Night") + + def test_tech_shell_name(self) -> None: + self.assertEqual(self.engine.PILOT_COLLECTION["eg3"]["name"], "Tech Shell") + + def test_cashmere_cloud_name(self) -> None: + self.assertEqual(self.engine.PILOT_COLLECTION["eg4"]["name"], "Cashmere Cloud") + + def test_required_fabric_keys(self) -> None: + required = {"name", "drape", "gsm", "elasticity", "recovery", "friction"} + for look_id, fabric in self.engine.PILOT_COLLECTION.items(): + with self.subTest(look_id=look_id): + self.assertTrue(required.issubset(fabric.keys())) + + +class TestCalculatePhysics(unittest.TestCase): + """Module 1: fabric physics — Lafayette factor, gravity stretch, breathing.""" + + def setUp(self) -> None: + self.engine = RobertEngineV10() + + def test_garment_width_greater_than_shoulder(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg0"] + w, _ = self.engine._calculate_physics(fabric, 450, 800, 0) + self.assertGreater(w, 450) + + def test_gravity_height_at_least_torso(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg0"] + _, h = self.engine._calculate_physics(fabric, 450, 800, 0) + self.assertGreaterEqual(h, 800) + + def test_heavy_fabric_stretches_more_than_light(self) -> None: + light = self.engine.PILOT_COLLECTION["eg0"] # gsm=60 + heavy = self.engine.PILOT_COLLECTION["eg2"] # gsm=320 + _, h_light = self.engine._calculate_physics(light, 450, 800, 0) + _, h_heavy = self.engine._calculate_physics(heavy, 450, 800, 0) + self.assertGreater(h_heavy, h_light) + + def test_breathing_oscillates_with_time(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg4"] # elasticity=15 + w0, _ = self.engine._calculate_physics(fabric, 450, 800, 0) + # At now_ms = π/0.0015 ≈ 2094 the sin peaks; results must differ + w1, _ = self.engine._calculate_physics(fabric, 450, 800, 2094) + self.assertNotAlmostEqual(w0, w1, places=3) + + def test_weight_norm_clamped_to_one_for_very_heavy(self) -> None: + # GSM well above 400 should clamp weight_norm at 1.0 (15% max elongation) + fabric = {"drape": 0.5, "gsm": 1000, "elasticity": 0, "recovery": 90, "friction": 0.5} + _, h = self.engine._calculate_physics(fabric, 100, 500, 0) + self.assertAlmostEqual(h, 500 * 1.15, places=5) + + def test_weight_norm_clamped_to_zero_for_very_light(self) -> None: + # GSM at or below 50 should clamp weight_norm at 0.0 (no elongation) + fabric = {"drape": 0.5, "gsm": 50, "elasticity": 0, "recovery": 90, "friction": 0.5} + _, h = self.engine._calculate_physics(fabric, 100, 500, 0) + self.assertAlmostEqual(h, 500.0, places=5) + + +class TestGetVisualEffects(unittest.TestCase): + """Module 2: visual effects — highlight, folds, scan line.""" + + def setUp(self) -> None: + self.engine = RobertEngineV10() + + def test_silk_has_highlight(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg0"] # friction=0.22 + fx = self.engine._get_visual_effects(fabric, 900, 800, 0, 88) + self.assertIn("highlight", fx) + + def test_high_friction_no_highlight(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg1"] # friction=0.65 + fx = self.engine._get_visual_effects(fabric, 900, 800, 0, 88) + self.assertNotIn("highlight", fx) + + def test_low_drape_three_folds(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg3"] # drape=0.15 + fx = self.engine._get_visual_effects(fabric, 900, 800, 0, 88) + self.assertEqual(len(fx["folds"]), 3) + + def test_high_drape_five_folds(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg0"] # drape=0.85 + fx = self.engine._get_visual_effects(fabric, 900, 800, 0, 88) + self.assertEqual(len(fx["folds"]), 5) + + def test_scan_line_present_when_fit_score_in_range(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg0"] + fx = self.engine._get_visual_effects(fabric, 900, 800, 0, 50) + self.assertIn("scan_line_y", fx) + + def test_scan_line_absent_when_fit_score_zero(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg0"] + fx = self.engine._get_visual_effects(fabric, 900, 800, 0, 0) + self.assertNotIn("scan_line_y", fx) + + def test_scan_line_absent_when_fit_score_95(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg0"] + fx = self.engine._get_visual_effects(fabric, 900, 800, 0, 95) + self.assertNotIn("scan_line_y", fx) + + def test_scan_line_absent_when_fit_score_100(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg0"] + fx = self.engine._get_visual_effects(fabric, 900, 800, 0, 100) + self.assertNotIn("scan_line_y", fx) + + def test_fold_alpha_formula(self) -> None: + fabric = self.engine.PILOT_COLLECTION["eg3"] # drape=0.15 + fx = self.engine._get_visual_effects(fabric, 900, 800, 0, 50) + expected_alpha = 0.05 + (0.15 * 0.1) + for fold in fx["folds"]: + self.assertAlmostEqual(fold["alpha"], expected_alpha, places=6) + + +class TestGetAccessoryRender(unittest.TestCase): + """Module 3: accessory rendering — body-anchored vs floating.""" + + def setUp(self) -> None: + self.engine = RobertEngineV10() + + def test_body_anchor_mode(self) -> None: + result = self.engine.get_accessory_render(True, 450, 900, 1080, 1.5) + self.assertEqual(result["mode"], "BodyAnchor") + + def test_floating_mode(self) -> None: + result = self.engine.get_accessory_render(False, 450, 900, 1080, 1.5) + self.assertEqual(result["mode"], "FloatingExhibition") + + def test_alpha_always_088(self) -> None: + for has_body in (True, False): + with self.subTest(has_body=has_body): + result = self.engine.get_accessory_render(has_body, 450, 900, 1080, 1.5) + self.assertAlmostEqual(result["alpha"], 0.88) + + def test_body_anchor_width(self) -> None: + result = self.engine.get_accessory_render(True, 450, 900, 1080, 1.5) + self.assertAlmostEqual(result["width"], 450 * 0.8) + + def test_floating_width_18_percent_canvas(self) -> None: + result = self.engine.get_accessory_render(False, 450, 900, 1080, 1.5) + self.assertAlmostEqual(result["width"], 1080 * 0.18) + + def test_height_respects_aspect_ratio(self) -> None: + result = self.engine.get_accessory_render(True, 450, 900, 1080, 2.0) + self.assertAlmostEqual(result["height"], result["width"] * 2.0) + + +class TestProcessFrame(unittest.TestCase): + """Module 4: process_frame — full pipeline + metadata.""" + + def setUp(self) -> None: + self.engine = RobertEngineV10() + + def test_returns_render_and_metadata(self) -> None: + result = self.engine.process_frame("eg0", 450, 900, 88, {"w": 1080, "h": 1920}) + self.assertIn("render", result) + self.assertIn("metadata", result) + + def test_render_has_width_height_effects(self) -> None: + result = self.engine.process_frame("eg0", 450, 900, 88, {"w": 1080, "h": 1920}) + render = result["render"] + self.assertIn("width", render) + self.assertIn("height", render) + self.assertIn("effects", render) + + def test_patent_in_metadata(self) -> None: + result = self.engine.process_frame("eg0", 450, 900, 88, {"w": 1080, "h": 1920}) + self.assertEqual(result["metadata"]["patent"], "PCT/EP2025/067317") + + def test_claim_in_metadata(self) -> None: + result = self.engine.process_frame("eg0", 450, 900, 88, {"w": 1080, "h": 1920}) + self.assertEqual(result["metadata"]["claim"], "22_PROTECTED") + + def test_recovery_stable_for_eg0(self) -> None: + result = self.engine.process_frame("eg0", 450, 900, 88, {"w": 1080, "h": 1920}) + self.assertEqual(result["metadata"]["recovery_status"], "STABLE") + + def test_recovery_degraded_for_eg1(self) -> None: + # eg1 recovery=80, which is <= 85 → DEGRADED + result = self.engine.process_frame("eg1", 450, 900, 88, {"w": 1080, "h": 1920}) + self.assertEqual(result["metadata"]["recovery_status"], "DEGRADED") + + def test_unknown_look_falls_back_to_eg0(self) -> None: + result = self.engine.process_frame("unknown", 450, 900, 88, {"w": 1080, "h": 1920}) + # eg0 recovery=95 → STABLE + self.assertEqual(result["metadata"]["recovery_status"], "STABLE") + + def test_scan_line_active_when_fit_score_88(self) -> None: + result = self.engine.process_frame("eg0", 450, 900, 88, {"w": 1080, "h": 1920}) + self.assertIn("scan_line_y", result["render"]["effects"]) + + +if __name__ == "__main__": + unittest.main()