@@ -12,16 +12,6 @@ const DEFAULTS = {
1212 direction : "ltr" ,
1313} ;
1414
15- const RULE_NOTE_LABELS = {
16- 0 : "" ,
17- 1 : "Chaos pseudo al\u00e9atoire" ,
18- 2 : "Triangle de Sierpinski" ,
19- 3 : "Calcul universel" ,
20- 4 : "XOR avec auto-r\u00e9f\u00e9rence" ,
21- 5 : "Mod\u00e8le de trafic" ,
22- 6 : "Fronti\u00e8res seulement" ,
23- } ;
24-
2515const fallbackDomain = {
2616 transition ( ruleNumber , left , center , right ) {
2717 const index = left * 4 + center * 2 + right ;
@@ -36,14 +26,14 @@ const fallbackDomain = {
3626 if ( [ 54 , 106 , 110 , 137 , 193 ] . includes ( ruleNumber ) ) return 4 ;
3727 return 2 ;
3828 } ,
39- noteId ( ruleNumber ) {
40- if ( ruleNumber === 30 ) return 1 ;
41- if ( ruleNumber === 90 ) return 2 ;
42- if ( ruleNumber === 110 ) return 3 ;
43- if ( ruleNumber === 150 ) return 4 ;
44- if ( ruleNumber === 184 ) return 5 ;
45- if ( ruleNumber === 254 ) return 6 ;
46- return 0 ;
29+ noteLabel ( ruleNumber ) {
30+ if ( ruleNumber === 30 ) return "Chaos pseudo al\u00e9atoire" ;
31+ if ( ruleNumber === 90 ) return "Triangle de Sierpinski" ;
32+ if ( ruleNumber === 110 ) return "Calcul universel" ;
33+ if ( ruleNumber === 150 ) return "XOR avec auto-r\u00e9f\u00e9rence" ;
34+ if ( ruleNumber === 184 ) return "Mod\u00e8le de trafic" ;
35+ if ( ruleNumber === 254 ) return "Fronti\u00e8res seulement" ;
36+ return "" ;
4737 } ,
4838 interpolateComponent ( start , end , progressScaled ) {
4939 return Math . round ( start + ( end - start ) * ( progressScaled / 1000 ) ) ;
@@ -53,7 +43,9 @@ const fallbackDomain = {
5343let state = structuredClone ( DEFAULTS ) ;
5444let wasm = null ;
5545let wasmAvailable = false ;
56- let galleryKey = "" ;
46+ const textDecoder = new TextDecoder ( ) ;
47+ let galleryLoaded = false ;
48+ let galleryLoading = null ;
5749
5850function clamp ( value , min , max ) {
5951 return Math . max ( min , Math . min ( max , value ) ) ;
@@ -170,6 +162,7 @@ function validateWasmExports(exports) {
170162 || typeof exports . sortie_motif !== "function"
171163 || typeof exports . classe_wolfram !== "function"
172164 || typeof exports . note_regle !== "function"
165+ || typeof exports . etiquette_note_regle !== "function"
173166 || typeof exports . composante_interpolee !== "function"
174167 ) {
175168 return false ;
@@ -211,6 +204,15 @@ function validateWasmExports(exports) {
211204 }
212205 }
213206
207+ const labelPtr = Number ( exports . etiquette_note_regle ( 90 ) ) ;
208+ const labelLength = Number ( exports . __ml_str_len ( ) ) ;
209+ const labelBytes = new Uint8Array ( exports . memory . buffer , labelPtr , labelLength ) ;
210+ const label = textDecoder . decode ( labelBytes . slice ( ) ) ;
211+ exports . __ml_reset ( ) ;
212+ if ( label !== "Triangle de Sierpinski" ) {
213+ return false ;
214+ }
215+
214216 if ( Number ( exports . sortie_motif ( 90 , 4 ) ) !== 1 ) {
215217 return false ;
216218 }
@@ -258,19 +260,22 @@ function wolframClass(ruleNumber) {
258260 return fallbackDomain . wolframClass ( ruleNumber ) ;
259261}
260262
261- function ruleNoteId ( ruleNumber ) {
262- if ( wasmAvailable && wasm && typeof wasm . note_regle === "function" ) {
263+ function ruleNoteLabel ( ruleNumber ) {
264+ if (
265+ wasmAvailable
266+ && wasm
267+ && typeof wasm . etiquette_note_regle === "function"
268+ && typeof wasm . __ml_str_len === "function"
269+ && typeof wasm . __ml_reset === "function"
270+ && wasm . memory instanceof WebAssembly . Memory
271+ ) {
263272 try {
264- return Number ( wasm . note_regle ( ruleNumber ) ) ;
273+ return callWasmString ( wasm . etiquette_note_regle , ruleNumber ) ;
265274 } catch ( error ) {
266275 disableWasmRuntime ( error ) ;
267276 }
268277 }
269- return fallbackDomain . noteId ( ruleNumber ) ;
270- }
271-
272- function ruleNoteLabel ( ruleNumber ) {
273- return RULE_NOTE_LABELS [ ruleNoteId ( ruleNumber ) ] ?? "" ;
278+ return fallbackDomain . noteLabel ( ruleNumber ) ;
274279}
275280
276281function interpolateComponent ( start , end , progressScaled ) {
@@ -284,6 +289,15 @@ function interpolateComponent(start, end, progressScaled) {
284289 return fallbackDomain . interpolateComponent ( start , end , progressScaled ) ;
285290}
286291
292+ function callWasmString ( fn , ...args ) {
293+ const ptr = Number ( fn ( ...args ) ) ;
294+ const length = Number ( wasm . __ml_str_len ( ) ) ;
295+ const bytes = new Uint8Array ( wasm . memory . buffer , ptr , length ) ;
296+ const value = textDecoder . decode ( bytes . slice ( ) ) ;
297+ wasm . __ml_reset ( ) ;
298+ return value ;
299+ }
300+
287301function disableWasmRuntime ( error ) {
288302 wasm = null ;
289303 wasmAvailable = false ;
@@ -503,37 +517,6 @@ function renderGradientPickers() {
503517 } ) ;
504518}
505519
506- function galleryStateKey ( ) {
507- return JSON . stringify ( state ) ;
508- }
509-
510- function buildGallery ( ) {
511- const key = galleryStateKey ( ) ;
512- if ( key === galleryKey ) return ;
513- galleryKey = key ;
514- const grid = document . getElementById ( "gallery-grid" ) ;
515- grid . innerHTML = "" ;
516- for ( let rule = 0 ; rule < 256 ; rule += 1 ) {
517- const item = document . createElement ( "article" ) ;
518- item . className = "gallery-item" ;
519- const canvas = document . createElement ( "canvas" ) ;
520- canvas . className = "gallery-thumb" ;
521- renderToCanvas ( canvas , rule , 50 , 50 , 2 ) ;
522- const label = document . createElement ( "div" ) ;
523- label . className = "gallery-label" ;
524- label . innerHTML = `<strong>R\u00e8gle ${ rule } </strong><span>${ ruleNoteLabel ( rule ) || `Classe ${ wolframClass ( rule ) } ` } </span>` ;
525- item . append ( canvas , label ) ;
526- item . addEventListener ( "click" , ( ) => {
527- state . rule = rule ;
528- syncRuleControls ( ) ;
529- renderRuleDiagram ( ) ;
530- switchTab ( "explorer" ) ;
531- scheduleRender ( ) ;
532- } ) ;
533- grid . appendChild ( item ) ;
534- }
535- }
536-
537520function switchTab ( tab ) {
538521 const explorer = tab === "explorer" ;
539522 const gallery = tab === "gallery" ;
@@ -544,14 +527,63 @@ function switchTab(tab) {
544527 document . getElementById ( "tab-explorer" ) . classList . toggle ( "active" , explorer ) ;
545528 document . getElementById ( "tab-gallery" ) . classList . toggle ( "active" , gallery ) ;
546529 document . getElementById ( "tab-source" ) . classList . toggle ( "active" , source ) ;
547- if ( gallery ) buildGallery ( ) ;
530+ if ( gallery ) loadGalleryFragment ( ) ;
548531}
549532
550533const scheduleRender = debounce ( ( ) => {
551534 renderMainView ( ) ;
552- if ( ! document . getElementById ( "gallery-panel" ) . hidden ) buildGallery ( ) ;
553535} , 100 ) ;
554536
537+ function selectGalleryRule ( rule ) {
538+ state . rule = clamp ( rule , 0 , 255 ) ;
539+ syncRuleControls ( ) ;
540+ renderRuleDiagram ( ) ;
541+ switchTab ( "explorer" ) ;
542+ scheduleRender ( ) ;
543+ }
544+
545+ function bindGallery ( ) {
546+ const grid = document . getElementById ( "gallery-grid" ) ;
547+ grid . addEventListener ( "click" , ( event ) => {
548+ const item = event . target . closest ( ".gallery-item" ) ;
549+ if ( ! item ) return ;
550+ selectGalleryRule ( Number . parseInt ( item . dataset . rule , 10 ) || 0 ) ;
551+ } ) ;
552+ grid . addEventListener ( "keydown" , ( event ) => {
553+ if ( event . key !== "Enter" && event . key !== " " ) return ;
554+ const item = event . target . closest ( ".gallery-item" ) ;
555+ if ( ! item ) return ;
556+ event . preventDefault ( ) ;
557+ selectGalleryRule ( Number . parseInt ( item . dataset . rule , 10 ) || 0 ) ;
558+ } ) ;
559+ }
560+
561+ async function loadGalleryFragment ( ) {
562+ if ( galleryLoaded ) return ;
563+ if ( galleryLoading ) return galleryLoading ;
564+ const grid = document . getElementById ( "gallery-grid" ) ;
565+ grid . innerHTML = '<p class="muted">Chargement de la galerie...</p>' ;
566+ galleryLoading = fetch ( "gallery-fragment.html" )
567+ . then ( ( response ) => {
568+ if ( ! response . ok ) {
569+ throw new Error ( `Fragment galerie indisponible (${ response . status } ).` ) ;
570+ }
571+ return response . text ( ) ;
572+ } )
573+ . then ( ( markup ) => {
574+ grid . innerHTML = markup ;
575+ galleryLoaded = true ;
576+ } )
577+ . catch ( ( error ) => {
578+ grid . innerHTML = '<p class="muted">Impossible de charger la galerie statique.</p>' ;
579+ console . error ( error ) ;
580+ } )
581+ . finally ( ( ) => {
582+ galleryLoading = null ;
583+ } ) ;
584+ return galleryLoading ;
585+ }
586+
555587function bindControls ( ) {
556588 const presets = document . getElementById ( "presets" ) ;
557589
@@ -715,6 +747,7 @@ function bindControls() {
715747async function init ( ) {
716748 loadFromURL ( ) ;
717749 bindControls ( ) ;
750+ bindGallery ( ) ;
718751 renderGradientPickers ( ) ;
719752 syncRuleControls ( ) ;
720753 renderRuleDiagram ( ) ;
0 commit comments