Files
haptika/mobile/scripts/compile-scenes.cjs
T
2026-09-08 09:07:27 +02:00

110 lines
9.3 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
const fs = require('fs');
const path = require('path');
const root = path.resolve(__dirname, '..');
const scenes = JSON.parse(fs.readFileSync(root + '/design/scenes.json'));
const assetFiles = require('../design/asset-files.json');
const infoFiles = new Set(scenes.flatMap(s=>s.layers.filter(l=>l.asset&&/info/i.test(l.name)).map(l=>assetFiles[l.asset])));
const normalize = value => value.normalize('NFD').replace(/[\u0300-\u036f]/g, '').replace(/[’']/g, '').replace(/\s+/g, ' ').trim().toLowerCase();
const onboarding = ['2:2', '2:30', '677:64', '2:268', '2:334', '2:213', '2:431'];
function compile(scene) {
const watch = scene.section.endsWith('+ WATCH');
const section = watch ? 'ABONNEMENT IPHONE + WATCH' : 'ABONNEMENT IPHONE';
const available = scenes.filter(s => s.section === section && s.w === 402);
const find = name => available.find(s => normalize(s.name) === normalize(name))?.id;
const go = (name, label) => {const target = find(name);return target ? {type:'navigate', target, label} : undefined;};
const panels = scenes.filter(s => s.section === scene.section && s.w === 362 && Math.abs((s.canvasX + 181) - (scene.canvasX + 201)) < 35);
const panel = panels.sort((a,b)=>Math.abs(a.canvasY-scene.canvasY)-Math.abs(b.canvasY-scene.canvasY))[0];
const family = normalize(scene.name).split(' - ')[0];
const result = () => {
if (/mesure|hrv/.test(family)) return find('Résultat Mesure VFC');
if (family === 'ma coherence') return find('Ma Cohérence - Restitution');
if (family === 'coherence cardiaque') return find('Cohérence Cardiaque - Fin');
if (family.includes('4·4·4·4')) return find('4·4·4·4 - Respiration carré - Restitution (simple)');
if (family.includes('calme express')) return find('Calme express - Restitution (simple)');
if (/4.?7.?8|sommeil/.test(family)) return find('Respirer - Restitution (simple)');
if (/micro|apaiser|booster|endormissement/.test(family)) return find('Ancrer - Restitution');
if (/focus|concentrer|miroir/.test(family)) return find('Recentrer - Restitution');
if (/decouverte|presence|sensation|atterrissage|libre|meditation/.test(family)) return find('Méditation haptique - Restitution');
if (/sos/.test(family)) return find('SOS Stress - Fin');
};
const start = () => {
if (/mesure/.test(family)) return find('Mesure HRV en cours');
if (family === 'ma coherence') return find('Ma Cohérence - En cours');
if (family.includes('4·7·8')) return find('4-7-8 - Séance');
if (family.includes('4·4·4·4')) return find('4·4·4·4 - Respiration carré - Séance');
return available.find(s => normalize(s.name).startsWith(family + ' - ') && /séance/i.test(s.name))?.id;
};
const categories = {
'respirer':'Vos outils - respirez', 'respirez':'Vos outils - respirez', 'ancrer':'Vos outils - Ancrer', 'recentrer':'Vos outils - Recentrer',
'meditation haptique':'Vos outils - Méditation haptique', 'reveil intelligent':'Vos outils - Réveil intelligent',
'ma coherence':'Ma Cohérence - Préparation', 'coherence cardiaque':'Cohérence Cardiaque - Réglages',
'calme express':'Calme express - Préparation', '4·4·4·4 - equilibre':'4·4·4·4 - Équilibre - Préparation',
'4·7·8 - sommeil':'4·7·8 - Sommeil - Préparation', 'micro-ancrage':'Micro-Ancrage - Préparation',
'apaiser':'Apaiser - Préparation', 'booster':'Booster - Préparation', 'endormissement':'Endormissement - Préparation',
'focus':'Focus - Préparation', 'concentrer':'Concentrer - Préparation', 'miroir cardiaque':'Miroir Cardiaque - Préparation',
'decouverte':'Découverte - Préparation', 'presence':'Présence - Préparation', 'sensation':'Sensation - Préparation',
'atterrissage':'Atterrissage - Préparation', 'libre':'Libre - Préparation', 'creer mon protocole':'Créer mon protocole',
'references scientifiques':'Références scientifiques', 'diagnostic matinal':'Accueil - Diagnostic matinal',
};
function action(label, layer) {
const l = normalize(label); const name = normalize(layer.name);
if (panel && layer.asset && (/info/i.test(layer.name)||infoFiles.has(assetFiles[layer.asset]))) return {type:'overlay',target:panel.id,label:'En savoir plus'};
if (!l && /button/i.test(layer.name) && layer.y > 65 && layer.w <= 40 && panel) return {type:'overlay',target:panel.id,label:'En savoir plus'};
if (/informations personnelles/.test(l)) return {type:'navigate',target:'2:268',label};
if (/^equipement/.test(l)) return {type:'navigate',target:'2:334',label};
if (/guide des exercices/.test(l)) return go('Vos Outils',label);
if (/^voir tout$|mes sessions/.test(l)) return go('Évolution - V1',label);
if (!l && layer.asset && layer.y < 65) return layer.x > 300 ? go('Profil', 'Profil') : layer.x < 45 ? go('Vos Outils', 'Outils') : undefined;
if (scene.section === 'ONBOARDING') {
if (/^continuer$|^plus tard$|^connecter apple sante$/.test(l)) return {type:'navigate', target:onboarding[onboarding.indexOf(scene.id)+1], label};
if (l==='commencer') return {type:'finish-onboarding',label};
if (l==='iphone' || l==='iphone + apple watch') return {type:'variant',value:l==='iphone'?'phone':'watch',label};
if (/entrez votre prenom/.test(l)||name.includes('input')) return {type:'input',key:'firstName',label};
if (/selectionnez une annee/.test(l)) return {type:'input',key:'birthYear',label};
if (l==='homme'||l==='femme') return {type:'select',key:'gender',value:label,label};
}
const tabs={'mon etat':'Accueil - Mon état actuel',outils:'Vos Outils','sos stress':'SOS Stress - Entrée',evolution:'Évolution - V1',profil:'Profil'};
if(tabs[l]) return go(tabs[l],label);
if(/^annuler$|^retour$|^fermer$/.test(l))return {type:'back',label};
if (/mesurer mon etat|nouvelle mesure|mesurer a nouveau|refaire.*mesure/.test(l)) return go('Préparation Mesure mon etat',label);
if (/mesurer.*vfc/.test(l)) return go('Préparation Mesure VFC',label);
if (l==='commencer la session'&&/respirez/.test(scene.name)) return go('Ma Cohérence - Préparation',label);
if (/^(commencer|demarrer|lancer|tester maintenant)/.test(l)) {const target=start();if(target)return {type:'navigate',target,label}; if(/sos/.test(l))return go('SOS Stress - Séance',label);}
if (/^(arreter|terminer|fin de seance)/.test(l)){const target=result();return target?{type:'navigate',target,label}:go('Vos Outils',label);}
if (/^pause$|^reprendre$/.test(l)) return {type:'pause',label};
if (/^(retour a|revenir a)/.test(l)) return go(/outil/.test(l)?'Vos Outils':'Accueil - Mon état actuel',label);
if (/enregistrer/.test(l))return {type:'back',label};
if (/tester maintenant/.test(l)&&family==='creer mon protocole')return go('Cohérence Cardiaque - Séance',label);
if (/mode ecran noir|escalade sonore|activer le reveil|vibration/.test(l) && l.length<50) return {type:'toggle',key:l,label};
if (/^\d+\s?min$|^autre$|^focus$|^guide$|^libre$|^semaine$|^mois$|^jour$|^7j$|^30j$|^90j$/.test(l) && (layer.t==='TEXT'||name.includes('button'))) {
if(categories[l]&&/vos outils/.test(normalize(scene.name)))return go(categories[l],label);
return {type:'select',key:/min|autre/.test(l)?'duration':scene.id+':selection',value:label,label};
}
if (/^\d+s$|^\d+%$|^07$|^00$/.test(l) && /protocole|reveil/.test(family))return {type:'input',key:scene.id+':'+layer.id,label};
if (categories[l]) return go(categories[l],label);
// Protocol cards have both a title and a description. Match the title prefix.
if(name.includes('button')||name.includes('card')) for(const [title,target] of Object.entries(categories)) if(l.startsWith(title+' ')||l===title)return go(target,label);
}
for (const layer of scene.layers) {
const children = scene.layers.filter(l => l.text!==undefined&&l.path.includes(layer.id));
const label = layer.text ?? children.map(l=>l.text).join(' ');
if (layer.t==='TEXT'||/button|card|input|toggle|switch/i.test(layer.name)||layer.asset&&(layer.y<65||panel)) layer.action = action(label,layer);
}
// Resolve icon-only controls and broad card taps from the text they contain.
for(const layer of scene.layers) if(/button/i.test(layer.name)&&!layer.action){const child=scene.layers.find(l=>l.path.includes(layer.id)&&l.action);if(child)layer.action=child.action;}
for(const layer of scene.layers) if(layer.name==='Input'&&layer.h<=12&&layer.w>100){
const value=scene.layers.filter(l=>l.text&&/^\d+(s|%| min)$/.test(l.text.trim())&&l.y<layer.y&&layer.y-l.y<55).sort((a,b)=>b.y-a.y)[0];
const label=scene.layers.filter(l=>l.text&&l.x<layer.x+100&&l.y<layer.y&&layer.y-l.y<55).sort((a,b)=>b.y-a.y)[0];
if(value){const suffix=value.text.trim().replace(/^\d+/,'');layer.action={type:'slider',key:scene.id+':'+layer.id,value:parseInt(value.text,10).toString(),valueNode:value.id,maximum:suffix==='%'?100:suffix===' min'?60:20,suffix,label:label?.text||value.text};value.action=undefined;}
}
for(const layer of scene.layers) if(layer.r>=100&&layer.w>=30&&layer.w<=60&&layer.h>=18&&layer.h<=32){
const label=scene.layers.find(l=>l.action?.type==='toggle'&&l.text&&Math.abs(l.y-layer.y)<15&&l.x<layer.x);
if(label)layer.action={...label.action};
}
return {...scene, resultTarget:result(), startTarget:start()};
}
const output=scenes.map(compile);
fs.mkdirSync(root+'/src/design',{recursive:true});fs.writeFileSync(root+'/src/design/scenes.json',JSON.stringify(output));
console.log({scenes:output.length,actions:output.reduce((n,s)=>n+s.layers.filter(l=>l.action).length,0)});
module.exports={compile,normalize,onboarding};