diff --git a/app.js b/app.js index 5928fc0..89dc273 100644 --- a/app.js +++ b/app.js @@ -21419,6 +21419,8 @@ function ricordaRecenteCatalogo(k, nome, over){ kw:"scaletta setlist set list brani canzoni pezzi ordine durata durate pausa bis intro cambio palco"}); entries.push({nome:"Nuova variante", dim:"copia della scena attiva, da cambiare", noQuick:true, iconHtml:VAR_ICON, action:function(){ if(typeof createVariant==="function") createVariant(); }, kw:"variante varianti scena scene versione alternativa seconda formazione"}); + entries.push({nome:"Importa organico", dim:"da una riga o da Excel, con anteprima", noQuick:true, iconHtml:LISTA_ICON, action:function(){ orgImportApri(); }, + kw:"importa importare incolla organico orchestra formazione foglio convocazione csv excel archi formula sezioni"}); /* G1 fase 2 (13/11): fuori dal menu File, che resta essenziale */ entries.push({nome:"Frequenze radio (RF)", dim:"si scrivono su ogni radiomic e in-ear", noQuick:true, iconHtml:RF_ICON, action:openRfFreq, kw:"frequenza frequenze radiofrequenza radiofrequenze radio radiomicrofoni banda mhz lista rf"}); ["metro"].forEach(function(k){ @@ -24562,6 +24564,7 @@ var ORCH_DEF = [ ]; var pendingOut = null; +var pendingDopo = null; /* callback di chi chiama placeOut (import organico, 13/11): gira a posti creati, anche dopo «Continua» dell'avviso */ var pendingReviewModal = "orchModal"; var ORCH_PRESETS = [ {id:"quartetto", nome:"Quartetto archi — 2.0.1.1", hint:"Quartetto classico: 2 violini, viola e violoncello. Ideale per piccoli palchi o cerimonie.", counts:{v1:2,v2:0,vle:1,vc:1}}, @@ -24818,6 +24821,7 @@ function applyOrchestraPreset(presetId){ /* Piazza una lista di postazioni (out, coord. relative al riferimento) sul palco. Riusato da organico orchestra e da tutte le formazioni tipo. */ function placeOut(out, fitStage, clearFirst, force){ + var _dopo=pendingDopo; pendingDopo=null; var m=measureOrchestraOut(out), fp=frontPad(m); if(fitStage){ var nw=Math.max(800, Math.ceil((2*m.maxX+160)/50)*50); @@ -24829,13 +24833,13 @@ function placeOut(out, fitStage, clearFirst, force){ var C={x:state.stage.w/2, y:state.stage.d-fp}; var assessment=assessOut(out, state.stage.w, state.stage.d, fp); if(!force && (assessment.tooSmall || assessment.relevantOverlaps>0)){ - pendingOut={out:out, fitStage:fitStage, clearFirst:clearFirst}; + pendingOut={out:out, fitStage:fitStage, clearFirst:clearFirst, dopo:_dopo}; var msg=[]; if(assessment.tooSmall) msg.push("Il palco è troppo piccolo: alcune postazioni escono dall'area utile."); if(assessment.relevantOverlaps>0) msg.push("La disposizione automatica crea "+assessment.overlaps+" sovrapposizion"+(assessment.overlaps===1?"e":"i")+" tra postazioni."); msg.push("Puoi continuare e poi sistemare a mano, oppure rivedere l'organico."); document.getElementById("orchWarnText").textContent=msg.join(" "); - document.getElementById(pendingReviewModal||"orchModal").hidden=true; + var _rev=document.getElementById(pendingReviewModal||"orchModal"); if(_rev) _rev.hidden=true; /* il modale da cui si arriva può non esistere (import chiamato senza aprire la finestra) */ document.getElementById("orchWarnModal").hidden=false; return false; } @@ -24854,6 +24858,7 @@ function placeOut(out, fitStage, clearFirst, force){ if(!clearFirst && state.items.length) avoidGroupOverlap(news); /* non sovrapporsi a ciò che è già sul palco */ news.forEach(function(it){ state.items.push(it); }); clearSelection(); save(); + if(_dopo) _dopo(); return true; } /* sposta il nuovo gruppo DIETRO gli elementi esistenti che gli stanno davanti; allarga il palco sul fondo se serve */ @@ -24885,6 +24890,190 @@ function genOrchestra(counts, fitStage, clearFirst, force){ return placeOut(buildOrchestraOut(counts), fitStage, clearFirst, force); } +/* ═══ IMPORT DELL'ORGANICO (13/11/2026, G1 fase 2) ═══ + L'organico dei fogli di convocazione si incolla, non si ricostruisce a mano: una riga «cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2», la formula degli archi + «8-6-4-4-2» (violini I · II · viole · celli · bassi; con meno numeri si fermano prima), oppure righe «sezione, strumento, n.» (CSV, o copiate da Excel). + Il testo diventa un conto per sezione (le stesse di ORCH_DEF), si guarda in anteprima e SOLO dopo si creano i posti con lo stesso generatore + dell'organico orchestra; «Annulla» (toast o ⌘Z) rimette il palco com'era. Con le colonne «Completo · Medio · Ridotto» si sceglie quale disegnare. + DATI PERSONALI: non si leggono. Di una riga contano solo sezione, strumento e quantità; celle con @, IBAN, telefoni, «contratto», «cachet», «CV», + «sostituto», «nominativo» si saltano e si contano; il contenuto di righe non riconosciute non si mostra. Niente nasce nel documento: nessun campo nuovo. */ +var ORG_IMP_ALIAS=(function(){ + var A={}, add=function(id,l){ l.split("|").forEach(function(k){ A[k]=id; }); }; + add("v1","v1|vl1|vln1|vn1|vni|violini 1|violino 1|violini primi|primi violini|violino primo"); + add("v2","v2|vl2|vln2|vn2|vnii|violini 2|violino 2|violini secondi|secondi violini"); + add("vle","vle|vla|viola|viole|vle"); add("vc","vc|vcl|vcello|celli|cello|violoncello|violoncelli"); + add("cb","cb|cbs|contrabbasso|contrabbassi"); + add("fl","fl|flauto|flauti"); add("ob","o|ob|oboe|oboi"); add("cl","cl|clar|clarinetto|clarinetti"); add("fg","fg|fag|fagotto|fagotti"); + add("cor","cr|cor|corno|corni|hn"); add("tr","tr|tromba|trombe|tp"); add("tbn","trb|tbn|trbn|trombone|tromboni"); add("tba","tu|tba|tuba|tube"); + add("arp","arp|arpa|arpe|hp"); add("pf","pf|pianoforte|pianoforti|piano"); add("cel","cel|celesta"); + add("tim","timp|tim|timpani|timpano"); add("gc","gc|gran cassa|grancassa"); add("pia","pia|piatti"); add("rul","rul|rullante"); + add("cmp","cmp|campane|campane tubolari"); add("tam","tam|tamtam|tam tam"); + return A; +})(); +var ORG_IMP_MAX=40; +var ORG_IMP_GRUPPO={v1:"v1",v2:"v2",vle:"vle",vc:"vc",cb:"cb",fl:"fl",ob:"ob",cl:"cl",fg:"fg",cor:"cor",tr:"tr",tbn:"tbn",tba:"tba",arp:"arp",pf:"pf",cel:"cel",tim:"tim"}; /* sezioni che il foglio Organico sa riconoscere nel disegno */ +function orgImpNorm(s){ + s=String(s==null?"":s).toLowerCase(); + try{ s=s.normalize("NFD").replace(/[̀-ͯ]/g,""); }catch(e){} + s=s.replace(/[.'’_]/g,"").replace(/[\-–]/g," ").replace(/\s+/g," ").trim(); + return s.replace(/ iii$/," 3").replace(/ ii$/," 2").replace(/ i$/," 1"); +} +function orgImpRisolvi(nome){ + var s=orgImpNorm(String(nome||"").split("/")[0].replace(/\([^)]*\)/g,"")); + return Object.prototype.hasOwnProperty.call(ORG_IMP_ALIAS,s) ? ORG_IMP_ALIAS[s] : null; +} +var ORG_IMP_PRIV=/@|\biban\b|\b[a-z]{2}\d{2}[a-z0-9]{11,}\b|(?:\+|\b00)\d[\d ]{8,}\d|\b3\d{2}[ .\-]?\d{6,7}\b|\b0\d{1,3}[ \/.\-]?\d{5,8}\b|\b(?:cv|curriculum|contratt\w*|compens\w*|cachet|fattur\w*|sostitut\w*|nominativ\w*|codice fiscale)\b/i; +function orgImportParse(testo){ + var R={varianti:[], ignorati:[], righeNo:[], privati:0, tagliati:[]}, vm={}; + var NOMI={completo:"Completo", medio:"Medio", ridotto:"Ridotto", "":"Organico"}; + function va(k){ if(!vm[k]){ vm[k]={id:k, nome:NOMI[k], counts:{}, tot:0}; R.varianti.push(vm[k]); } return vm[k]; } + function agg(k,id,n){ var v=va(k); v.counts[id]=(v.counts[id]||0)+n; } + function isInt(s){ return /^\d{1,2}$/.test(String(s).trim()); } + var cur="", hdr=null; + String(testo||"").split(/\r?\n/).forEach(function(riga,ri){ + var line=riga.trim(); if(!line) return; + var m=/^(completo|medio|ridotto)\s*[:=]\s*(.*)$/i.exec(line); + if(m){ cur=m[1].toLowerCase(); line=m[2].trim(); if(!line) return; } + var cells=line.split(/\t|;|,|\|/).map(function(c){ return c.trim(); }); + if(!/\d/.test(line) && cells.length>=2 && /\b(sezione|strumento|strumenti|gruppo)\b/i.test(line)){ /* intestazione: quale colonna è «n.», quali le varianti */ + hdr={nCol:-1, vcols:[]}; + cells.forEach(function(c,i){ var n=orgImpNorm(c); + if(/^(n|n°|numero|quantita|qta|posti)$/.test(n)) hdr.nCol=i; + else if(n==="completo"||n==="medio"||n==="ridotto") hdr.vcols.push({idx:i, k:n}); }); + return; + } + cells=cells.map(function(c){ if(c && ORG_IMP_PRIV.test(c)){ R.privati++; return ""; } return c; }); /* cella per cella: la riga resta, il dato personale no */ + if(cells.length>=2 && cells.some(function(c){ return /^\d+$/.test(c); })){ /* RIGA DI TABELLA: sezione, strumento, n. (un numero solo di cifre: una quantità sensata ne ha 1-2) */ + var id=null; + for(var i=cells.length-1;i>=0 && !id;i--){ if(cells[i] && !/^\d+$/.test(cells[i])) id=orgImpRisolvi(cells[i]); } + if(!id){ R.righeNo.push(ri+1); return; } + var letto=false; + if(hdr && hdr.vcols.length) hdr.vcols.forEach(function(vc){ if(isInt(cells[vc.idx]||"")){ agg(vc.k,id,+cells[vc.idx]); letto=true; } }); + if(letto) return; + var ni=(hdr && hdr.nCol>=0 && isInt(cells[hdr.nCol]||"")) ? hdr.nCol : cells.findIndex(isInt); + if(ni<0){ R.righeNo.push(ri+1); return; } + agg(cur,id,+cells[ni]); + return; + } + /* RIGA IN LINEA: formule degli archi + «sigla+numero» */ + var s=cells.join(" ; ").replace(/(^|[^\w])(\d{1,2}(?:\s*[-–.]\s*\d{1,2}){3,4})(?![\w])/g, function(_t,pre,f){ + f.split(/\s*[-–.]\s*/).forEach(function(n,i){ agg(cur,["v1","v2","vle","vc","cb"][i],+n); }); + return pre+" ; "; + }); + s.split(/[;,|]+/).forEach(function(chunk){ + chunk=chunk.trim(); if(!chunk) return; + var words=[], pn=null; + function emetti(nome,n){ var id=orgImpRisolvi(nome); if(id) agg(cur,id,n); else R.ignorati.push(String(nome).slice(0,16)); } + function chiudi(){ if(words.length){ emetti(words.join(" "), pn!==null ? pn : 1); words=[]; pn=null; } } + chunk.split(/[\s\-–]+/).filter(Boolean).forEach(function(t){ + var ld=/^([a-zà-ÿ]+?)\.?(\d{1,2})$/i.exec(t); + if(isInt(t)){ + if(words.length){ if(pn!==null){ emetti(words.join(" "),pn); words=[]; pn=+t; } else { emetti(words.join(" "),+t); words=[]; } } + else if(pn!==null){ R.ignorati.push(String(pn)); pn=+t; } else pn=+t; + } else if(ld){ chiudi(); emetti(ld[1], +ld[2]); } + else words.push(t); + }); + if(words.length) chiudi(); else if(pn!==null) R.ignorati.push(String(pn)); + }); + }); + R.varianti.forEach(function(v){ + Object.keys(v.counts).forEach(function(k){ if(v.counts[k]>ORG_IMP_MAX){ R.tagliati.push(k); v.counts[k]=ORG_IMP_MAX; } if(!v.counts[k]) delete v.counts[k]; }); + v.tot=orchTotal(v.counts); + }); + R.varianti=R.varianti.filter(function(v){ return v.tot>0; }); + R.ignorati=R.ignorati.filter(function(x,i,a){ return a.indexOf(x)===i; }); + R.tagliati=R.tagliati.filter(function(x,i,a){ return a.indexOf(x)===i; }); + return R; +} +/* quanti posti di ogni sezione ci sono ORA nel disegno (solo le sezioni che il foglio Organico riconosce) */ +function orgImportDisegnato(){ + var D=organicoDati(), r={}; + Object.keys(D.righe).forEach(function(k){ var x=D.righe[k]; r[x.id]=(r[x.id]||0)+x.n; }); + var out={}; Object.keys(ORG_IMP_GRUPPO).forEach(function(id){ out[id]=r[ORG_IMP_GRUPPO[id]]||0; }); + return out; +} +/* «organico dichiarato ≠ disegnato»: sezioni dove il conto importato e quello del disegno non coincidono */ +function orgImportDifferenze(counts){ + var d=orgImportDisegnato(), diff=[]; + ORCH_DEF.forEach(function(s){ if(!ORG_IMP_GRUPPO[s.id]) return; var a=counts[s.id]||0, b=d[s.id]||0; if(a!==b) diff.push(s.nome+": "+a+" dichiarati, "+b+" disegnati"); }); + return diff; +} +function orgImportApplica(counts, opz){ + opz=opz||{}; + if(!orchTotal(counts)) return false; + pendingReviewModal="orgImpModal"; /* se il palco è troppo piccolo o la disposizione si sovrappone, «Rivedi» torna a questa finestra */ + pendingDopo=function(){ + try{ fit(); }catch(e){} + var diff=orgImportDifferenze(counts); + showToastAnnulla("Creati "+orchTotal(counts)+" posti dall'organico importato"+(diff.length ? " · dichiarato ≠ disegnato: "+diff.slice(0,3).join("; ")+(diff.length>3?"…":"") : "")); + }; + return placeOut(buildOrchestraOut(counts), !!opz.fit, !!opz.clear, false); +} +function showToastAnnulla(msg){ + try{ if(window.__toast) window.__toast(msg, false, {label:"Annulla", run:function(){ undo(); }}); }catch(e){} +} +function orgImportApri(){ + var ov=document.getElementById("orgImpModal"); + if(!ov){ + ov=document.createElement("div"); ov.className="modal"; ov.id="orgImpModal"; ov.hidden=true; + ov.setAttribute("role","dialog"); ov.setAttribute("aria-modal","true"); ov.setAttribute("aria-labelledby","orgImpTitolo"); + ov.innerHTML='

Importa l\'organico

' + + '

Incolla una riga come cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2, la formula degli archi 8-6-4-4-2 (violini I · II · viole · celli · bassi) oppure righe «sezione, strumento, n.» copiate da Excel. Con le colonne Completo · Medio · Ridotto scegli quale disegnare. Contano solo sezione, strumento e quantità: nomi di persone e dati personali non vengono letti.

' + + '' + + '' + + '
' + + '' + + '' + + '
'; + document.body.appendChild(ov); + var ta=ov.querySelector("#orgImpTesto"), sel=ov.querySelector("#orgImpVarSel"), go=ov.querySelector("#orgImpGo"); + var P=null; + function scelta(){ if(!P) return null; for(var i=0;i'+esc(v.nome)+' ('+v.tot+')'; }).join(""); + if(prima && P.varianti.some(function(v){ return v.id===prima; })) sel.value=prima; + else { var c=P.varianti.filter(function(v){ return v.id==="completo"; })[0]; if(c) sel.value="completo"; } + ov.querySelector("#orgImpVar").hidden=P.varianti.length<2; + var v=scelta(), now=orgImportDisegnato(), clear=ov.querySelector("#orgImpClear").checked; + if(v){ + var righe=ORCH_DEF.filter(function(s){ return v.counts[s.id]; }), gia=0; + h+='' + + righe.map(function(s){ var g=ORG_IMP_GRUPPO[s.id] ? now[s.id] : null; if(g) gia+=g; + return ''; }).join("") + + '
SezioneDa importareOra sul palco
'+esc(s.nome)+''+v.counts[s.id]+''+(g===null?"—":g)+'
Totale'+v.tot+'
'; + var a=[v.counts.v1||0,v.counts.v2||0,v.counts.vle||0,v.counts.vc||0,v.counts.cb||0]; + if(a.some(Boolean)){ while(a.length && !a[a.length-1]) a.pop(); h+='
Archi '+a.join("-")+'
'; } + if(gia && !clear) h+='
Sul palco ci sono già dei posti di queste sezioni: restano, e il disegno avrà più posti di quelli dichiarati. Per sostituirli spunta «Svuota il palco».
'; + } else h+='
'+(ta.value.trim() ? "Non ho trovato nessuna sezione da importare." : "Incolla il testo qui sopra: vedrai subito cosa verrebbe creato.")+'
'; + var note=[]; + if(P.ignorati.length) note.push("Non riconosciuti: "+P.ignorati.slice(0,8).map(esc).join(", ")+(P.ignorati.length>8?"…":"")); + if(P.righeNo.length) note.push("Righe non riconosciute (contenuto non mostrato): "+P.righeNo.slice(0,10).join(", ")); + if(P.privati) note.push(P.privati+" dat"+(P.privati===1?"o personale saltato":"i personali saltati")+", non letti"); + if(P.tagliati.length) note.push("Oltre "+ORG_IMP_MAX+" per sezione: ridotto a "+ORG_IMP_MAX); + if(note.length) h+='
'+note.join("
")+'
'; + ov.querySelector("#orgImpAnte").innerHTML=h; + go.disabled=!v; + } + ta.addEventListener("input", aggiorna); sel.addEventListener("change", aggiorna); + ov.querySelector("#orgImpClear").addEventListener("change", aggiorna); + ov.__agg=aggiorna; + function chiudi(){ ov.hidden=true; } + ov.querySelector("#orgImpNo").addEventListener("click", chiudi); + ov.addEventListener("click", function(e){ if(e.target===ov) chiudi(); }); + ov.addEventListener("keydown", function(e){ if(e.key==="Escape") chiudi(); }); + go.addEventListener("click", function(){ + var v=scelta(); if(!v) return; + var r=orgImportApplica(v.counts, {clear:ov.querySelector("#orgImpClear").checked, fit:ov.querySelector("#orgImpFit").checked}); + if(r!==false) chiudi(); + }); + } + ov.querySelector("#orgImpTesto").value=""; ov.querySelector("#orgImpClear").checked=false; ov.querySelector("#orgImpFit").checked=false; + ov.__agg(); ov.hidden=false; + try{ ov.querySelector("#orgImpTesto").focus(); }catch(e){} +} + /* ============ FORMAZIONI TIPO (band, coro, combinazioni) ============ */ /* convenzione coord.: origine = centro fronte palco; -y = verso il fondo, +y = verso il pubblico; -x = lato DX palco, +x = lato SX palco (come la scena). */ @@ -25304,17 +25493,20 @@ function buildOrchCoroOut(){ }); document.getElementById("orchWarnReview").addEventListener("click", function(){ document.getElementById("orchWarnModal").hidden=true; - document.getElementById(pendingReviewModal||"orchModal").hidden=false; - }); - document.getElementById("orchWarnContinue").addEventListener("click", function(){ - if(!pendingOut) return; - placeOut(pendingOut.out, pendingOut.fitStage, pendingOut.clearFirst, true); - pendingOut=null; - document.getElementById("orchWarnModal").hidden=true; - fit(); + var _rev=document.getElementById(pendingReviewModal||"orchModal"); if(_rev) _rev.hidden=false; }); + document.getElementById("orchWarnContinue").addEventListener("click", orchWarnContinua); })(); +function orchWarnContinua(){ + if(!pendingOut) return; + var po=pendingOut; pendingOut=null; + pendingDopo=po.dopo||null; + placeOut(po.out, po.fitStage, po.clearFirst, true); + document.getElementById("orchWarnModal").hidden=true; + fit(); +} + function readStringCounts(){ return { v1:Math.max(0,parseInt(document.getElementById("str_v1").value,10)||0), diff --git a/index.template.html b/index.template.html index e5388a1..00f3b48 100644 --- a/index.template.html +++ b/index.template.html @@ -23674,6 +23674,8 @@

Esporta PDF in scala

kw:"scaletta setlist set list brani canzoni pezzi ordine durata durate pausa bis intro cambio palco"}); entries.push({nome:"Nuova variante", dim:"copia della scena attiva, da cambiare", noQuick:true, iconHtml:VAR_ICON, action:function(){ if(typeof createVariant==="function") createVariant(); }, kw:"variante varianti scena scene versione alternativa seconda formazione"}); + entries.push({nome:"Importa organico", dim:"da una riga o da Excel, con anteprima", noQuick:true, iconHtml:LISTA_ICON, action:function(){ orgImportApri(); }, + kw:"importa importare incolla organico orchestra formazione foglio convocazione csv excel archi formula sezioni"}); /* G1 fase 2 (13/11): fuori dal menu File, che resta essenziale */ entries.push({nome:"Frequenze radio (RF)", dim:"si scrivono su ogni radiomic e in-ear", noQuick:true, iconHtml:RF_ICON, action:openRfFreq, kw:"frequenza frequenze radiofrequenza radiofrequenze radio radiomicrofoni banda mhz lista rf"}); ["metro"].forEach(function(k){ @@ -26817,6 +26819,7 @@

Esporta PDF in scala

]; var pendingOut = null; +var pendingDopo = null; /* callback di chi chiama placeOut (import organico, 13/11): gira a posti creati, anche dopo «Continua» dell'avviso */ var pendingReviewModal = "orchModal"; var ORCH_PRESETS = [ {id:"quartetto", nome:"Quartetto archi — 2.0.1.1", hint:"Quartetto classico: 2 violini, viola e violoncello. Ideale per piccoli palchi o cerimonie.", counts:{v1:2,v2:0,vle:1,vc:1}}, @@ -27073,6 +27076,7 @@

Esporta PDF in scala

/* Piazza una lista di postazioni (out, coord. relative al riferimento) sul palco. Riusato da organico orchestra e da tutte le formazioni tipo. */ function placeOut(out, fitStage, clearFirst, force){ + var _dopo=pendingDopo; pendingDopo=null; var m=measureOrchestraOut(out), fp=frontPad(m); if(fitStage){ var nw=Math.max(800, Math.ceil((2*m.maxX+160)/50)*50); @@ -27084,13 +27088,13 @@

Esporta PDF in scala

var C={x:state.stage.w/2, y:state.stage.d-fp}; var assessment=assessOut(out, state.stage.w, state.stage.d, fp); if(!force && (assessment.tooSmall || assessment.relevantOverlaps>0)){ - pendingOut={out:out, fitStage:fitStage, clearFirst:clearFirst}; + pendingOut={out:out, fitStage:fitStage, clearFirst:clearFirst, dopo:_dopo}; var msg=[]; if(assessment.tooSmall) msg.push("Il palco è troppo piccolo: alcune postazioni escono dall'area utile."); if(assessment.relevantOverlaps>0) msg.push("La disposizione automatica crea "+assessment.overlaps+" sovrapposizion"+(assessment.overlaps===1?"e":"i")+" tra postazioni."); msg.push("Puoi continuare e poi sistemare a mano, oppure rivedere l'organico."); document.getElementById("orchWarnText").textContent=msg.join(" "); - document.getElementById(pendingReviewModal||"orchModal").hidden=true; + var _rev=document.getElementById(pendingReviewModal||"orchModal"); if(_rev) _rev.hidden=true; /* il modale da cui si arriva può non esistere (import chiamato senza aprire la finestra) */ document.getElementById("orchWarnModal").hidden=false; return false; } @@ -27109,6 +27113,7 @@

Esporta PDF in scala

if(!clearFirst && state.items.length) avoidGroupOverlap(news); /* non sovrapporsi a ciò che è già sul palco */ news.forEach(function(it){ state.items.push(it); }); clearSelection(); save(); + if(_dopo) _dopo(); return true; } /* sposta il nuovo gruppo DIETRO gli elementi esistenti che gli stanno davanti; allarga il palco sul fondo se serve */ @@ -27140,6 +27145,190 @@

Esporta PDF in scala

return placeOut(buildOrchestraOut(counts), fitStage, clearFirst, force); } +/* ═══ IMPORT DELL'ORGANICO (13/11/2026, G1 fase 2) ═══ + L'organico dei fogli di convocazione si incolla, non si ricostruisce a mano: una riga «cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2», la formula degli archi + «8-6-4-4-2» (violini I · II · viole · celli · bassi; con meno numeri si fermano prima), oppure righe «sezione, strumento, n.» (CSV, o copiate da Excel). + Il testo diventa un conto per sezione (le stesse di ORCH_DEF), si guarda in anteprima e SOLO dopo si creano i posti con lo stesso generatore + dell'organico orchestra; «Annulla» (toast o ⌘Z) rimette il palco com'era. Con le colonne «Completo · Medio · Ridotto» si sceglie quale disegnare. + DATI PERSONALI: non si leggono. Di una riga contano solo sezione, strumento e quantità; celle con @, IBAN, telefoni, «contratto», «cachet», «CV», + «sostituto», «nominativo» si saltano e si contano; il contenuto di righe non riconosciute non si mostra. Niente nasce nel documento: nessun campo nuovo. */ +var ORG_IMP_ALIAS=(function(){ + var A={}, add=function(id,l){ l.split("|").forEach(function(k){ A[k]=id; }); }; + add("v1","v1|vl1|vln1|vn1|vni|violini 1|violino 1|violini primi|primi violini|violino primo"); + add("v2","v2|vl2|vln2|vn2|vnii|violini 2|violino 2|violini secondi|secondi violini"); + add("vle","vle|vla|viola|viole|vle"); add("vc","vc|vcl|vcello|celli|cello|violoncello|violoncelli"); + add("cb","cb|cbs|contrabbasso|contrabbassi"); + add("fl","fl|flauto|flauti"); add("ob","o|ob|oboe|oboi"); add("cl","cl|clar|clarinetto|clarinetti"); add("fg","fg|fag|fagotto|fagotti"); + add("cor","cr|cor|corno|corni|hn"); add("tr","tr|tromba|trombe|tp"); add("tbn","trb|tbn|trbn|trombone|tromboni"); add("tba","tu|tba|tuba|tube"); + add("arp","arp|arpa|arpe|hp"); add("pf","pf|pianoforte|pianoforti|piano"); add("cel","cel|celesta"); + add("tim","timp|tim|timpani|timpano"); add("gc","gc|gran cassa|grancassa"); add("pia","pia|piatti"); add("rul","rul|rullante"); + add("cmp","cmp|campane|campane tubolari"); add("tam","tam|tamtam|tam tam"); + return A; +})(); +var ORG_IMP_MAX=40; +var ORG_IMP_GRUPPO={v1:"v1",v2:"v2",vle:"vle",vc:"vc",cb:"cb",fl:"fl",ob:"ob",cl:"cl",fg:"fg",cor:"cor",tr:"tr",tbn:"tbn",tba:"tba",arp:"arp",pf:"pf",cel:"cel",tim:"tim"}; /* sezioni che il foglio Organico sa riconoscere nel disegno */ +function orgImpNorm(s){ + s=String(s==null?"":s).toLowerCase(); + try{ s=s.normalize("NFD").replace(/[̀-ͯ]/g,""); }catch(e){} + s=s.replace(/[.'’_]/g,"").replace(/[\-–]/g," ").replace(/\s+/g," ").trim(); + return s.replace(/ iii$/," 3").replace(/ ii$/," 2").replace(/ i$/," 1"); +} +function orgImpRisolvi(nome){ + var s=orgImpNorm(String(nome||"").split("/")[0].replace(/\([^)]*\)/g,"")); + return Object.prototype.hasOwnProperty.call(ORG_IMP_ALIAS,s) ? ORG_IMP_ALIAS[s] : null; +} +var ORG_IMP_PRIV=/@|\biban\b|\b[a-z]{2}\d{2}[a-z0-9]{11,}\b|(?:\+|\b00)\d[\d ]{8,}\d|\b3\d{2}[ .\-]?\d{6,7}\b|\b0\d{1,3}[ \/.\-]?\d{5,8}\b|\b(?:cv|curriculum|contratt\w*|compens\w*|cachet|fattur\w*|sostitut\w*|nominativ\w*|codice fiscale)\b/i; +function orgImportParse(testo){ + var R={varianti:[], ignorati:[], righeNo:[], privati:0, tagliati:[]}, vm={}; + var NOMI={completo:"Completo", medio:"Medio", ridotto:"Ridotto", "":"Organico"}; + function va(k){ if(!vm[k]){ vm[k]={id:k, nome:NOMI[k], counts:{}, tot:0}; R.varianti.push(vm[k]); } return vm[k]; } + function agg(k,id,n){ var v=va(k); v.counts[id]=(v.counts[id]||0)+n; } + function isInt(s){ return /^\d{1,2}$/.test(String(s).trim()); } + var cur="", hdr=null; + String(testo||"").split(/\r?\n/).forEach(function(riga,ri){ + var line=riga.trim(); if(!line) return; + var m=/^(completo|medio|ridotto)\s*[:=]\s*(.*)$/i.exec(line); + if(m){ cur=m[1].toLowerCase(); line=m[2].trim(); if(!line) return; } + var cells=line.split(/\t|;|,|\|/).map(function(c){ return c.trim(); }); + if(!/\d/.test(line) && cells.length>=2 && /\b(sezione|strumento|strumenti|gruppo)\b/i.test(line)){ /* intestazione: quale colonna è «n.», quali le varianti */ + hdr={nCol:-1, vcols:[]}; + cells.forEach(function(c,i){ var n=orgImpNorm(c); + if(/^(n|n°|numero|quantita|qta|posti)$/.test(n)) hdr.nCol=i; + else if(n==="completo"||n==="medio"||n==="ridotto") hdr.vcols.push({idx:i, k:n}); }); + return; + } + cells=cells.map(function(c){ if(c && ORG_IMP_PRIV.test(c)){ R.privati++; return ""; } return c; }); /* cella per cella: la riga resta, il dato personale no */ + if(cells.length>=2 && cells.some(function(c){ return /^\d+$/.test(c); })){ /* RIGA DI TABELLA: sezione, strumento, n. (un numero solo di cifre: una quantità sensata ne ha 1-2) */ + var id=null; + for(var i=cells.length-1;i>=0 && !id;i--){ if(cells[i] && !/^\d+$/.test(cells[i])) id=orgImpRisolvi(cells[i]); } + if(!id){ R.righeNo.push(ri+1); return; } + var letto=false; + if(hdr && hdr.vcols.length) hdr.vcols.forEach(function(vc){ if(isInt(cells[vc.idx]||"")){ agg(vc.k,id,+cells[vc.idx]); letto=true; } }); + if(letto) return; + var ni=(hdr && hdr.nCol>=0 && isInt(cells[hdr.nCol]||"")) ? hdr.nCol : cells.findIndex(isInt); + if(ni<0){ R.righeNo.push(ri+1); return; } + agg(cur,id,+cells[ni]); + return; + } + /* RIGA IN LINEA: formule degli archi + «sigla+numero» */ + var s=cells.join(" ; ").replace(/(^|[^\w])(\d{1,2}(?:\s*[-–.]\s*\d{1,2}){3,4})(?![\w])/g, function(_t,pre,f){ + f.split(/\s*[-–.]\s*/).forEach(function(n,i){ agg(cur,["v1","v2","vle","vc","cb"][i],+n); }); + return pre+" ; "; + }); + s.split(/[;,|]+/).forEach(function(chunk){ + chunk=chunk.trim(); if(!chunk) return; + var words=[], pn=null; + function emetti(nome,n){ var id=orgImpRisolvi(nome); if(id) agg(cur,id,n); else R.ignorati.push(String(nome).slice(0,16)); } + function chiudi(){ if(words.length){ emetti(words.join(" "), pn!==null ? pn : 1); words=[]; pn=null; } } + chunk.split(/[\s\-–]+/).filter(Boolean).forEach(function(t){ + var ld=/^([a-zà-ÿ]+?)\.?(\d{1,2})$/i.exec(t); + if(isInt(t)){ + if(words.length){ if(pn!==null){ emetti(words.join(" "),pn); words=[]; pn=+t; } else { emetti(words.join(" "),+t); words=[]; } } + else if(pn!==null){ R.ignorati.push(String(pn)); pn=+t; } else pn=+t; + } else if(ld){ chiudi(); emetti(ld[1], +ld[2]); } + else words.push(t); + }); + if(words.length) chiudi(); else if(pn!==null) R.ignorati.push(String(pn)); + }); + }); + R.varianti.forEach(function(v){ + Object.keys(v.counts).forEach(function(k){ if(v.counts[k]>ORG_IMP_MAX){ R.tagliati.push(k); v.counts[k]=ORG_IMP_MAX; } if(!v.counts[k]) delete v.counts[k]; }); + v.tot=orchTotal(v.counts); + }); + R.varianti=R.varianti.filter(function(v){ return v.tot>0; }); + R.ignorati=R.ignorati.filter(function(x,i,a){ return a.indexOf(x)===i; }); + R.tagliati=R.tagliati.filter(function(x,i,a){ return a.indexOf(x)===i; }); + return R; +} +/* quanti posti di ogni sezione ci sono ORA nel disegno (solo le sezioni che il foglio Organico riconosce) */ +function orgImportDisegnato(){ + var D=organicoDati(), r={}; + Object.keys(D.righe).forEach(function(k){ var x=D.righe[k]; r[x.id]=(r[x.id]||0)+x.n; }); + var out={}; Object.keys(ORG_IMP_GRUPPO).forEach(function(id){ out[id]=r[ORG_IMP_GRUPPO[id]]||0; }); + return out; +} +/* «organico dichiarato ≠ disegnato»: sezioni dove il conto importato e quello del disegno non coincidono */ +function orgImportDifferenze(counts){ + var d=orgImportDisegnato(), diff=[]; + ORCH_DEF.forEach(function(s){ if(!ORG_IMP_GRUPPO[s.id]) return; var a=counts[s.id]||0, b=d[s.id]||0; if(a!==b) diff.push(s.nome+": "+a+" dichiarati, "+b+" disegnati"); }); + return diff; +} +function orgImportApplica(counts, opz){ + opz=opz||{}; + if(!orchTotal(counts)) return false; + pendingReviewModal="orgImpModal"; /* se il palco è troppo piccolo o la disposizione si sovrappone, «Rivedi» torna a questa finestra */ + pendingDopo=function(){ + try{ fit(); }catch(e){} + var diff=orgImportDifferenze(counts); + showToastAnnulla("Creati "+orchTotal(counts)+" posti dall'organico importato"+(diff.length ? " · dichiarato ≠ disegnato: "+diff.slice(0,3).join("; ")+(diff.length>3?"…":"") : "")); + }; + return placeOut(buildOrchestraOut(counts), !!opz.fit, !!opz.clear, false); +} +function showToastAnnulla(msg){ + try{ if(window.__toast) window.__toast(msg, false, {label:"Annulla", run:function(){ undo(); }}); }catch(e){} +} +function orgImportApri(){ + var ov=document.getElementById("orgImpModal"); + if(!ov){ + ov=document.createElement("div"); ov.className="modal"; ov.id="orgImpModal"; ov.hidden=true; + ov.setAttribute("role","dialog"); ov.setAttribute("aria-modal","true"); ov.setAttribute("aria-labelledby","orgImpTitolo"); + ov.innerHTML='

Importa l\'organico

' + + '

Incolla una riga come cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2, la formula degli archi 8-6-4-4-2 (violini I · II · viole · celli · bassi) oppure righe «sezione, strumento, n.» copiate da Excel. Con le colonne Completo · Medio · Ridotto scegli quale disegnare. Contano solo sezione, strumento e quantità: nomi di persone e dati personali non vengono letti.

' + + '' + + '' + + '
' + + '' + + '' + + '
'; + document.body.appendChild(ov); + var ta=ov.querySelector("#orgImpTesto"), sel=ov.querySelector("#orgImpVarSel"), go=ov.querySelector("#orgImpGo"); + var P=null; + function scelta(){ if(!P) return null; for(var i=0;i'+esc(v.nome)+' ('+v.tot+')'; }).join(""); + if(prima && P.varianti.some(function(v){ return v.id===prima; })) sel.value=prima; + else { var c=P.varianti.filter(function(v){ return v.id==="completo"; })[0]; if(c) sel.value="completo"; } + ov.querySelector("#orgImpVar").hidden=P.varianti.length<2; + var v=scelta(), now=orgImportDisegnato(), clear=ov.querySelector("#orgImpClear").checked; + if(v){ + var righe=ORCH_DEF.filter(function(s){ return v.counts[s.id]; }), gia=0; + h+='' + + righe.map(function(s){ var g=ORG_IMP_GRUPPO[s.id] ? now[s.id] : null; if(g) gia+=g; + return ''; }).join("") + + '
SezioneDa importareOra sul palco
'+esc(s.nome)+''+v.counts[s.id]+''+(g===null?"—":g)+'
Totale'+v.tot+'
'; + var a=[v.counts.v1||0,v.counts.v2||0,v.counts.vle||0,v.counts.vc||0,v.counts.cb||0]; + if(a.some(Boolean)){ while(a.length && !a[a.length-1]) a.pop(); h+='
Archi '+a.join("-")+'
'; } + if(gia && !clear) h+='
Sul palco ci sono già dei posti di queste sezioni: restano, e il disegno avrà più posti di quelli dichiarati. Per sostituirli spunta «Svuota il palco».
'; + } else h+='
'+(ta.value.trim() ? "Non ho trovato nessuna sezione da importare." : "Incolla il testo qui sopra: vedrai subito cosa verrebbe creato.")+'
'; + var note=[]; + if(P.ignorati.length) note.push("Non riconosciuti: "+P.ignorati.slice(0,8).map(esc).join(", ")+(P.ignorati.length>8?"…":"")); + if(P.righeNo.length) note.push("Righe non riconosciute (contenuto non mostrato): "+P.righeNo.slice(0,10).join(", ")); + if(P.privati) note.push(P.privati+" dat"+(P.privati===1?"o personale saltato":"i personali saltati")+", non letti"); + if(P.tagliati.length) note.push("Oltre "+ORG_IMP_MAX+" per sezione: ridotto a "+ORG_IMP_MAX); + if(note.length) h+='
'+note.join("
")+'
'; + ov.querySelector("#orgImpAnte").innerHTML=h; + go.disabled=!v; + } + ta.addEventListener("input", aggiorna); sel.addEventListener("change", aggiorna); + ov.querySelector("#orgImpClear").addEventListener("change", aggiorna); + ov.__agg=aggiorna; + function chiudi(){ ov.hidden=true; } + ov.querySelector("#orgImpNo").addEventListener("click", chiudi); + ov.addEventListener("click", function(e){ if(e.target===ov) chiudi(); }); + ov.addEventListener("keydown", function(e){ if(e.key==="Escape") chiudi(); }); + go.addEventListener("click", function(){ + var v=scelta(); if(!v) return; + var r=orgImportApplica(v.counts, {clear:ov.querySelector("#orgImpClear").checked, fit:ov.querySelector("#orgImpFit").checked}); + if(r!==false) chiudi(); + }); + } + ov.querySelector("#orgImpTesto").value=""; ov.querySelector("#orgImpClear").checked=false; ov.querySelector("#orgImpFit").checked=false; + ov.__agg(); ov.hidden=false; + try{ ov.querySelector("#orgImpTesto").focus(); }catch(e){} +} + /* ============ FORMAZIONI TIPO (band, coro, combinazioni) ============ */ /* convenzione coord.: origine = centro fronte palco; -y = verso il fondo, +y = verso il pubblico; -x = lato DX palco, +x = lato SX palco (come la scena). */ @@ -27559,17 +27748,20 @@

Esporta PDF in scala

}); document.getElementById("orchWarnReview").addEventListener("click", function(){ document.getElementById("orchWarnModal").hidden=true; - document.getElementById(pendingReviewModal||"orchModal").hidden=false; - }); - document.getElementById("orchWarnContinue").addEventListener("click", function(){ - if(!pendingOut) return; - placeOut(pendingOut.out, pendingOut.fitStage, pendingOut.clearFirst, true); - pendingOut=null; - document.getElementById("orchWarnModal").hidden=true; - fit(); + var _rev=document.getElementById(pendingReviewModal||"orchModal"); if(_rev) _rev.hidden=false; }); + document.getElementById("orchWarnContinue").addEventListener("click", orchWarnContinua); })(); +function orchWarnContinua(){ + if(!pendingOut) return; + var po=pendingOut; pendingOut=null; + pendingDopo=po.dopo||null; + placeOut(po.out, po.fitStage, po.clearFirst, true); + document.getElementById("orchWarnModal").hidden=true; + fit(); +} + function readStringCounts(){ return { v1:Math.max(0,parseInt(document.getElementById("str_v1").value,10)||0), diff --git a/test/engines.test.mjs b/test/engines.test.mjs index ee271ca..3e1b850 100644 --- a/test/engines.test.mjs +++ b/test/engines.test.mjs @@ -72,7 +72,7 @@ const A = loadApp(); /* ---- mini test runner ---- */ let pass = 0, fail = 0; -function t(name, fn) { try { fn(); pass++; console.log(" ✓ " + name); } catch (e) { fail++; console.log(" ✗ " + name + "\n " + e.message); } } +function t(name, fn) { if (process.env.SOLO && !name.includes(process.env.SOLO)) return; try { fn(); pass++; console.log(" ✓ " + name); } catch (e) { fail++; console.log(" ✗ " + name + "\n " + e.message); } } function eq(got, exp, msg) { const g = JSON.stringify(got), e = JSON.stringify(exp); if (g !== e) throw new Error((msg || "") + " atteso " + e + ", ottenuto " + g); } function ok(cond, msg) { if (!cond) throw new Error(msg || "condizione falsa"); } function throws(fn, code) { @@ -32215,6 +32215,100 @@ t("editor: in alto a sinistra il logo stageplot.it, vettoriale, che porta alla h ok(/@media \(min-width:881px\) and \(max-width:1210px\)\{[^}]*header \.brand \.brand-full\{display:none\}/.test(stylesCss), "fra 881 e 1210 px solo il simbolo"); }); +/* ═══ IMPORT DELL'ORGANICO (13/11/2026, G1 fase 2): una riga o una tabella diventa il conto per sezione, anteprima, poi i posti; dati personali mai letti ═══ */ +const impC = (t, k) => { const P = A.orgImportParse(t); return (P.varianti.find((v) => v.id === (k || "")) || { counts: {} }).counts; }; +t("Import organico: la riga d'esempio del piano crea esattamente quei posti", () => { + eq(impC("cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2"), { cor: 4, tr: 2, tbn: 3, tba: 1, fl: 2, ob: 2, cl: 2, fg: 2 }, "sigle col numero attaccato, separate da trattini e punto e virgola"); + eq(impC("12-12-6-6-4"), { v1: 12, v2: 12, vle: 6, vc: 6, cb: 4 }, "formula degli archi coi trattini"); + eq(impC("8-6-4-4"), { v1: 8, v2: 6, vle: 4, vc: 4 }, "con quattro numeri il contrabbasso non c'è"); + eq(impC("10.8.6.6.3"), { v1: 10, v2: 8, vle: 6, vc: 6, cb: 3 }, "anche col punto (12.10.8.6.4, come nei preset)"); + eq(impC("2 flauti, 2 oboi, 4 corni, gran cassa 1, tam-tam"), { fl: 2, ob: 2, cor: 4, gc: 1, tam: 1 }, "numero davanti, nomi estesi, nome di due parole, strumento senza numero = 1"); + eq(impC("cr4-tr2; 12-10-8-6-4; arpa; timpani 1"), { v1: 12, v2: 10, vle: 8, vc: 6, cb: 4, cor: 4, tr: 2, arp: 1, tim: 1 }, "righe miste (la formula degli archi si legge per prima)"); + eq(impC("1-2-3"), {}, "tre numeri non sono una formula degli archi"); +}); +t("Import organico: tabella «sezione, strumento, n.» (CSV, TSV, ;) e colonne Completo · Medio · Ridotto", () => { + eq(impC("Sezione,Strumento,N.\nARCHI,Violini I,12\nARCHI,Violini II,10\nFIATI,Corni,4\nLEGNI,Flauto/Ottavino,2"), { v1: 12, v2: 10, cor: 4, fl: 2 }, "CSV con intestazione; «Flauto/Ottavino» è un flauto"); + eq(impC("Sezione\tStrumento\tN.\nARCHI\tViole\t6\nARCHI\tViole\t2"), { vle: 8 }, "copiato da Excel (tabulazioni); le righe dello stesso strumento si sommano"); + eq(impC("ARCHI;Violini 1;7\nARCHI;Violoncelli;4"), { v1: 7, vc: 4 }, "senza intestazione, con il punto e virgola"); + const P = A.orgImportParse("Sezione;Strumento;Completo;Medio;Ridotto\nARCHI;Violini I;12;8;4\nFIATI;Corni;4;2;2"); + eq(P.varianti.map((v) => v.id), ["completo", "medio", "ridotto"], "tre varianti"); + eq(P.varianti.map((v) => v.counts), [{ v1: 12, cor: 4 }, { v1: 8, cor: 2 }, { v1: 4, cor: 2 }], "ognuna con i suoi numeri"); + const Q = A.orgImportParse("Ridotto: 6-5-4-3-2; fl1\nCompleto: 12-10-8-6-4; fl2"); + eq(Q.varianti.map((v) => [v.id, v.counts.v1, v.counts.fl]), [["ridotto", 6, 1], ["completo", 12, 2]], "varianti anche in linea («Completo: …»)"); +}); +t("Import organico: dati personali mai letti né mostrati; righe non riconosciute solo per numero; tetto per sezione", () => { + const T = "Sezione,Strumento,N.\nARCHI,Viole,6,mario.rossi@example.org,IBAN IT60X0542811101000000123456\nARCHI,Violini,3\nFIATI,Corni,2,contratto firmato\nFIATI,Tube,1,+39 333 1234567\nRossi Mario,Cantante,1\nARCHI,Celli,350"; + const P = A.orgImportParse(T); + eq(P.varianti[0].counts, { vle: 6, cor: 2, tba: 1 }, "la riga resta, la cella personale no (celli 350: più di 2 cifre non è una quantità, la riga si scarta)"); + ok(P.privati >= 4, "le celle personali sono contate (" + P.privati + ")"); + const J = JSON.stringify(P); + ok(!/mario|rossi|example|IT60|contratto|1234567/i.test(J), "nulla di personale nel risultato: " + J); + eq(P.righeNo.length >= 2, true, "le righe non riconosciute (violini senza sezione, cantante…) sono elencate"); + ok(P.righeNo.every((n) => typeof n === "number"), "per numero di riga, mai col contenuto"); + eq(P.righeNo, [3, 6, 7], "righe 3 (violini senza sezione), 6 (cantante) e 7 (celli con quantità di 3 cifre)"); + const iban = A.orgImportParse("FIATI,Corni,2,coordinate IBAN da inviare\nFIATI,Tube,1,+39 06 1234 5678\nFIATI,Trombe,2,CV allegato"); + eq([iban.varianti[0].counts, iban.privati], [{ cor: 2, tba: 1, tr: 2 }, 3], "la parola IBAN, un fisso con prefisso, «CV»: celle saltate, righe tenute"); + const sost = A.orgImportParse("FIATI,Corno (sostituto Rossi),2"); + eq([sost.varianti, sost.righeNo, sost.privati], [[], [1], 1], "uno strumento con il nome di un sostituto nella stessa cella: la riga si scarta (e si dice quale), non si importa a metà"); + const lin = A.orgImportParse("cr4; IBAN IT60X0542811101000000123456; fl2"); + eq([lin.varianti[0].counts, lin.privati, lin.ignorati], [{ cor: 4, fl: 2 }, 1, []], "anche in linea: il pezzo personale si salta, non finisce fra i «non riconosciuti»"); + const pipe = A.orgImportParse("cr4 | IBAN IT60X0542811101000000123456 | fl2"); + eq([pipe.varianti[0].counts, pipe.privati, pipe.ignorati], [{ cor: 4, fl: 2 }, 1, []], "anche con la barra verticale come separatore"); + const due = A.orgImportParse("4 2 flauti"); + eq([due.varianti[0].counts, due.ignorati], [{ fl: 2 }, ["4"]], "due numeri di fila: il primo resta senza nome e si dice"); + eq(impC("Sezione,Strumento,Leggii,N.\nARCHI,Violini I,6,12"), { v1: 12 }, "la colonna «N.» dell'intestazione vince su un altro numero della riga"); + eq(impC("2 flauti 2 oboi 4 corni"), { fl: 2, ob: 2, cor: 4 }, "numero davanti, senza virgole"); + eq(impC("cor 4 tr 2 fl 2"), { cor: 4, tr: 2, fl: 2 }, "sigla e numero, senza virgole"); + eq(A.orgImportParse("fl0 cr0").varianti, [], "tutto a zero: niente da importare"); + eq(impC("fl99"), { fl: 40 }, "oltre 40 per sezione si ferma a 40"); + eq(A.orgImportParse("fl99").tagliati, ["fl"], "e lo dice"); + eq(A.orgImportParse("sax2 fl2 pippo").ignorati, ["sax", "pippo"], "gli strumenti che il generatore non ha si dichiarano (sax) invece di sparire"); + eq(A.orgImportParse("").varianti, [], "testo vuoto: nessuna variante"); +}); +t("Import organico: i posti creati ridanno gli stessi numeri nella pagina «Organico»; dichiarato ≠ disegnato se il palco ha già altro", () => { + const vuoto = () => A.loadDoc({ titolo: "Prova", stage: { w: 1600, d: 1000, blocks: [{ x: 0, y: 0, w: 1600, d: 1000 }] }, inputs: [], outputs: [], items: [] }); + vuoto(); const prima = Object.keys(A.state).sort(); + const c = impC("12-12-6-6-4; cr4-tr2-trb3-tu1; fl2-o2-cl2-fg2; arpa; timpani"); + const toast = []; A.window.__toast = (m, e, az) => toast.push([m, az && az.label]); + A.orgImportApplica(c, { clear: true, fit: true }); + if (A.pendingOut) A.orchWarnContinua(); + const L = A.organicoList(), righe = Object.fromEntries(L.rows.filter((r) => r.tipo === "riga").map((r) => [r.nome, r.n])); + eq(L.formula, "12-12-6-6-4", "formula archi"); + eq([righe["Violini I"], righe["Violini II"], righe["Viole"], righe["Violoncelli"], righe["Contrabbassi"]], [12, 12, 6, 6, 4], "archi"); + eq([righe["Corni"], righe["Trombe"], righe["Tromboni"], righe["Tuba"], righe["Flauti"], righe["Oboi"], righe["Clarinetti"], righe["Fagotti"], righe["Arpa"], righe["Timpani"]], [4, 2, 3, 1, 2, 2, 2, 2, 1, 1], "fiati, legni, arpa, timpani"); + eq(A.orgImportDifferenze(c), [], "dichiarato = disegnato"); + eq(toast.length, 1, "un solo messaggio"); eq(toast[0][1], "Annulla", "con il pulsante «Annulla»"); + ok(/^Creati 60 posti/.test(toast[0][0]) && !/dichiarato ≠/.test(toast[0][0]), "senza avviso: " + toast[0][0]); + eq(Object.keys(A.state).sort(), prima, "nessun campo nuovo nel documento"); + vuoto(); ok(A.orgImportDifferenze(c).length > 0, "palco vuoto: dichiarato più del disegnato, e si vede"); + eq(A.orgImportApplica({}, {}), false, "nessun posto dichiarato: non si crea niente"); eq(!!A.pendingOut, false, "e non si apre nessun avviso"); + toast.length = 0; A.orgImportApplica(c, { clear: true, fit: true }); + eq([!!A.pendingOut, A.state.items.length, toast.length], [true, 0, 0], "con sovrapposizioni l'avviso viene PRIMA: nessun posto creato e nessun messaggio finché non si preme «Continua»"); + A.placeOut([{ type: "flauto", x: 0, y: 0 }], false, false, true); eq(toast.length, 0, "un altro posizionamento, nel frattempo, non eredita il messaggio dell'import"); + A.orchWarnContinua(); eq([!!A.pendingOut, toast.length], [false, 1], "dopo «Continua» i posti ci sono e arriva il messaggio con «Annulla»"); + vuoto(); toast.length = 0; + /* palco con altro già dentro e senza svuotare: i numeri non coincidono, e lo dice */ + s5Fix(orgFix()); toast.length = 0; + A.orgImportApplica(impC("12-12-6-6-4"), { clear: false, fit: false }); if (A.pendingOut) A.orchWarnContinua(); + ok(A.orgImportDifferenze(impC("12-12-6-6-4")).length > 0, "palco non svuotato: dichiarato ≠ disegnato"); + ok(/dichiarato ≠ disegnato/.test(toast[0][0]), "il messaggio lo dice: " + toast[0][0]); + /* e la stessa importazione con «svuota» torna pari */ + A.orgImportApplica(impC("12-12-6-6-4"), { clear: true, fit: true }); if (A.pendingOut) A.orchWarnContinua(); + eq(A.orgImportDifferenze(impC("12-12-6-6-4")), [], "svuotando il palco il disegno coincide"); +}); +t("Import organico: si trova dalla ricerca («importa organico»), il menu File resta essenziale, nessun documento cambia e niente va nel database", () => { + const trovati = (q) => A.__spSearch(q).map((e) => e.nome); + ok(trovati("importa organico").includes("Importa organico"), "«importa organico» lo trova: " + trovati("importa organico")); + ok(trovati("incolla").includes("Importa organico") && trovati("excel").includes("Importa organico"), "anche «incolla» e «excel»"); + eq(trovati("violino").includes("Importa organico"), false, "ma non ruba i risultati di uno strumento"); + const html = readFileSync(join(root, "app/index.html"), "utf8"); + ok(!/data-file="orgimp"/.test(html), "niente voce nel menu File (tetto di sette, decisione di Simone)"); + const js = readFileSync(join(root, "app.js"), "utf8"), src = js.slice(js.indexOf("function orgImportParse"), js.indexOf("function orgImportApri")); + ok(src.length > 500, "premessa: il blocco si trova"); + ok(!/state\.[a-zA-Z]+\s*=[^=]/.test(src.replace(/state\.items/g, "")), "il parser e l'applicazione non scrivono campi nuovi in state"); + ok(!/fetch\(|supabase|localStorage|sessionStorage/.test(src), "niente rete, niente archivio: il testo incollato resta sulla pagina"); +}); + console.log("\n" + (fail === 0 ? "✓ TUTTI VERDI" : "✗ " + fail + " FALLITI") + " — " + pass + " passati, " + fail + " falliti."); process.exit(fail === 0 ? 0 : 1);