<?xml version="1.0"?>
<feed xmlns="http://www.w3.org/2005/Atom" xml:lang="fr">
	<id>https://wiki.alfinternational.eu/index.php?action=history&amp;feed=atom&amp;title=Application_Palais_Prouvost</id>
	<title>Application Palais Prouvost - Historique des versions</title>
	<link rel="self" type="application/atom+xml" href="https://wiki.alfinternational.eu/index.php?action=history&amp;feed=atom&amp;title=Application_Palais_Prouvost"/>
	<link rel="alternate" type="text/html" href="https://wiki.alfinternational.eu/index.php?title=Application_Palais_Prouvost&amp;action=history"/>
	<updated>2026-04-13T05:12:59Z</updated>
	<subtitle>Historique des versions pour cette page sur le wiki</subtitle>
	<generator>MediaWiki 1.43.1</generator>
	<entry>
		<id>https://wiki.alfinternational.eu/index.php?title=Application_Palais_Prouvost&amp;diff=1958&amp;oldid=prev</id>
		<title>Admin : Page créée avec « import React, { useEffect, useMemo, useRef, useState } from &quot;react&quot;;  // ============================================= // Prouvost Palace Builder – Single-file React App // ============================================= // • Créez des salons/pièces (hôtel particulier, palais) // • Définissez une image de fond par pièce (plan, photo, texture, couleur) // • Importez des photos d&#039;objets/tableaux/meubles et placez-les dans la pièce // • Déplacez / red... »</title>
		<link rel="alternate" type="text/html" href="https://wiki.alfinternational.eu/index.php?title=Application_Palais_Prouvost&amp;diff=1958&amp;oldid=prev"/>
		<updated>2025-10-18T16:24:25Z</updated>

		<summary type="html">&lt;p&gt;Page créée avec « import React, { useEffect, useMemo, useRef, useState } from &amp;quot;react&amp;quot;;  // ============================================= // Prouvost Palace Builder – Single-file React App // ============================================= // • Créez des salons/pièces (hôtel particulier, palais) // • Définissez une image de fond par pièce (plan, photo, texture, couleur) // • Importez des photos d&amp;#039;objets/tableaux/meubles et placez-les dans la pièce // • Déplacez / red... »&lt;/p&gt;
&lt;p&gt;&lt;b&gt;Nouvelle page&lt;/b&gt;&lt;/p&gt;&lt;div&gt;import React, { useEffect, useMemo, useRef, useState } from &amp;quot;react&amp;quot;;&lt;br /&gt;
&lt;br /&gt;
// =============================================&lt;br /&gt;
// Prouvost Palace Builder – Single-file React App&lt;br /&gt;
// =============================================&lt;br /&gt;
// • Créez des salons/pièces (hôtel particulier, palais)&lt;br /&gt;
// • Définissez une image de fond par pièce (plan, photo, texture, couleur)&lt;br /&gt;
// • Importez des photos d&amp;#039;objets/tableaux/meubles et placez-les dans la pièce&lt;br /&gt;
// • Déplacez / redimensionnez / superposez les objets&lt;br /&gt;
// • Sauvegarde auto dans localStorage + Export/Import JSON&lt;br /&gt;
// • Mode Présentation pour naviguer en plein écran entre les pièces&lt;br /&gt;
// =============================================&lt;br /&gt;
&lt;br /&gt;
function useLocalStorage(key, initialValue) {&lt;br /&gt;
  const [value, setValue] = useState(() =&amp;gt; {&lt;br /&gt;
    try {&lt;br /&gt;
      const item = window.localStorage.getItem(key);&lt;br /&gt;
      return item ? JSON.parse(item) : initialValue;&lt;br /&gt;
    } catch (e) {&lt;br /&gt;
      return initialValue;&lt;br /&gt;
    }&lt;br /&gt;
  });&lt;br /&gt;
  useEffect(() =&amp;gt; {&lt;br /&gt;
    try {&lt;br /&gt;
      window.localStorage.setItem(key, JSON.stringify(value));&lt;br /&gt;
    } catch {}&lt;br /&gt;
  }, [key, value]);&lt;br /&gt;
  return [value, setValue];&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
const defaultProject = {&lt;br /&gt;
  title: &amp;quot;Hôtel particulier Prouveau&amp;quot;,&lt;br /&gt;
  rooms: [&lt;br /&gt;
    {&lt;br /&gt;
      id: cryptoRandomId(),&lt;br /&gt;
      name: &amp;quot;Grand Salon&amp;quot;,&lt;br /&gt;
      bgType: &amp;quot;color&amp;quot;, // &amp;#039;image&amp;#039; | &amp;#039;color&amp;#039;&lt;br /&gt;
      bgColor: &amp;quot;#f5f1ea&amp;quot;,&lt;br /&gt;
      bgImage: null,&lt;br /&gt;
      width: 1400,&lt;br /&gt;
      height: 800,&lt;br /&gt;
      items: [],&lt;br /&gt;
    },&lt;br /&gt;
  ],&lt;br /&gt;
  activeRoomId: null,&lt;br /&gt;
};&lt;br /&gt;
&lt;br /&gt;
function cryptoRandomId() {&lt;br /&gt;
  const arr = new Uint8Array(8);&lt;br /&gt;
  if (window.crypto &amp;amp;&amp;amp; window.crypto.getRandomValues) {&lt;br /&gt;
    window.crypto.getRandomValues(arr);&lt;br /&gt;
  } else {&lt;br /&gt;
    for (let i = 0; i &amp;lt; arr.length; i++) arr[i] = Math.floor(Math.random() * 256);&lt;br /&gt;
  }&lt;br /&gt;
  return Array.from(arr, (b) =&amp;gt; b.toString(16).padStart(2, &amp;quot;0&amp;quot;)).join(&amp;quot;&amp;quot;);&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
export default function ProuveauPalaceBuilder() {&lt;br /&gt;
  const [project, setProject] = useLocalStorage(&amp;quot;prv_palace_project_v1&amp;quot;, defaultProject);&lt;br /&gt;
  const activeRoom = useMemo(() =&amp;gt; {&lt;br /&gt;
    const id = project.activeRoomId ?? project.rooms[0]?.id;&lt;br /&gt;
    return project.rooms.find((r) =&amp;gt; r.id === id) || project.rooms[0];&lt;br /&gt;
  }, [project]);&lt;br /&gt;
&lt;br /&gt;
  // Ensure activeRoomId initialized&lt;br /&gt;
  useEffect(() =&amp;gt; {&lt;br /&gt;
    if (!project.activeRoomId &amp;amp;&amp;amp; project.rooms[0]) {&lt;br /&gt;
      setProject((p) =&amp;gt; ({ ...p, activeRoomId: p.rooms[0].id }));&lt;br /&gt;
    }&lt;br /&gt;
  }, []);&lt;br /&gt;
&lt;br /&gt;
  const setActiveRoom = (id) =&amp;gt; setProject((p) =&amp;gt; ({ ...p, activeRoomId: id }));&lt;br /&gt;
&lt;br /&gt;
  const addRoom = () =&amp;gt; {&lt;br /&gt;
    const r = {&lt;br /&gt;
      id: cryptoRandomId(),&lt;br /&gt;
      name: `Pièce ${project.rooms.length + 1}`,&lt;br /&gt;
      bgType: &amp;quot;color&amp;quot;,&lt;br /&gt;
      bgColor: &amp;quot;#ffffff&amp;quot;,&lt;br /&gt;
      bgImage: null,&lt;br /&gt;
      width: 1200,&lt;br /&gt;
      height: 700,&lt;br /&gt;
      items: [],&lt;br /&gt;
    };&lt;br /&gt;
    setProject((p) =&amp;gt; ({ ...p, rooms: [...p.rooms, r], activeRoomId: r.id }));&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  const deleteRoom = (id) =&amp;gt; {&lt;br /&gt;
    setProject((p) =&amp;gt; {&lt;br /&gt;
      const nextRooms = p.rooms.filter((r) =&amp;gt; r.id !== id);&lt;br /&gt;
      const nextActive = nextRooms[0]?.id || null;&lt;br /&gt;
      return { ...p, rooms: nextRooms, activeRoomId: nextActive };&lt;br /&gt;
    });&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  const updateRoom = (patch) =&amp;gt; {&lt;br /&gt;
    setProject((p) =&amp;gt; ({&lt;br /&gt;
      ...p,&lt;br /&gt;
      rooms: p.rooms.map((r) =&amp;gt; (r.id === activeRoom.id ? { ...r, ...patch } : r)),&lt;br /&gt;
    }));&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  const exportJSON = () =&amp;gt; {&lt;br /&gt;
    const blob = new Blob([JSON.stringify(project, null, 2)], { type: &amp;quot;application/json&amp;quot; });&lt;br /&gt;
    const url = URL.createObjectURL(blob);&lt;br /&gt;
    const a = document.createElement(&amp;quot;a&amp;quot;);&lt;br /&gt;
    a.href = url;&lt;br /&gt;
    a.download = `${(project.title || &amp;quot;projet&amp;quot;).replace(/\s+/g, &amp;quot;_&amp;quot;)}.json`;&lt;br /&gt;
    a.click();&lt;br /&gt;
    URL.revokeObjectURL(url);&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  const importJSON = (file) =&amp;gt; {&lt;br /&gt;
    const reader = new FileReader();&lt;br /&gt;
    reader.onload = () =&amp;gt; {&lt;br /&gt;
      try {&lt;br /&gt;
        const data = JSON.parse(reader.result);&lt;br /&gt;
        if (data &amp;amp;&amp;amp; data.rooms) setProject(data);&lt;br /&gt;
      } catch (e) {&lt;br /&gt;
        alert(&amp;quot;Fichier invalide&amp;quot;);&lt;br /&gt;
      }&lt;br /&gt;
    };&lt;br /&gt;
    reader.readAsText(file);&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  const [present, setPresent] = useState(false);&lt;br /&gt;
&lt;br /&gt;
  return (&lt;br /&gt;
    &amp;lt;div className=&amp;quot;min-h-screen w-full bg-neutral-50 text-neutral-900&amp;quot;&amp;gt;&lt;br /&gt;
      {/* Header */}&lt;br /&gt;
      &amp;lt;header className=&amp;quot;sticky top-0 z-30 bg-white border-b border-neutral-200&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div className=&amp;quot;mx-auto max-w-7xl px-4 py-3 flex items-center gap-3&amp;quot;&amp;gt;&lt;br /&gt;
          &amp;lt;div className=&amp;quot;w-9 h-9 rounded-xl bg-gradient-to-br from-yellow-500 to-red-600 flex items-center justify-center text-white font-bold shadow-sm&amp;quot;&amp;gt;PP&amp;lt;/div&amp;gt;&lt;br /&gt;
          &amp;lt;input&lt;br /&gt;
            className=&amp;quot;text-xl md:text-2xl font-semibold bg-transparent outline-none flex-1&amp;quot;&lt;br /&gt;
            value={project.title}&lt;br /&gt;
            onChange={(e) =&amp;gt; setProject((p) =&amp;gt; ({ ...p, title: e.target.value }))}&lt;br /&gt;
          /&amp;gt;&lt;br /&gt;
          &amp;lt;div className=&amp;quot;flex items-center gap-2&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;button className=&amp;quot;px-3 py-1.5 rounded-lg bg-neutral-900 text-white hover:bg-neutral-800&amp;quot; onClick={addRoom}&amp;gt;&lt;br /&gt;
              + Ajouter une pièce&lt;br /&gt;
            &amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;label className=&amp;quot;px-3 py-1.5 rounded-lg bg-neutral-200 hover:bg-neutral-300 cursor-pointer&amp;quot;&amp;gt;&lt;br /&gt;
              Importer&lt;br /&gt;
              &amp;lt;input type=&amp;quot;file&amp;quot; className=&amp;quot;hidden&amp;quot; accept=&amp;quot;application/json&amp;quot; onChange={(e) =&amp;gt; e.target.files?.[0] &amp;amp;&amp;amp; importJSON(e.target.files[0])} /&amp;gt;&lt;br /&gt;
            &amp;lt;/label&amp;gt;&lt;br /&gt;
            &amp;lt;button className=&amp;quot;px-3 py-1.5 rounded-lg bg-neutral-200 hover:bg-neutral-300&amp;quot; onClick={exportJSON}&amp;gt;&lt;br /&gt;
              Exporter&lt;br /&gt;
            &amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;button className=&amp;quot;px-3 py-1.5 rounded-lg bg-emerald-600 text-white hover:bg-emerald-700&amp;quot; onClick={() =&amp;gt; setPresent((s) =&amp;gt; !s)}&amp;gt;&lt;br /&gt;
              {present ? &amp;quot;Quitter Présentation&amp;quot; : &amp;quot;Mode Présentation&amp;quot;}&lt;br /&gt;
            &amp;lt;/button&amp;gt;&lt;br /&gt;
          &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;/header&amp;gt;&lt;br /&gt;
&lt;br /&gt;
      &amp;lt;main className=&amp;quot;mx-auto max-w-7xl grid grid-cols-1 md:grid-cols-12 gap-4 px-4 py-4&amp;quot;&amp;gt;&lt;br /&gt;
        {/* Sidebar rooms */}&lt;br /&gt;
        &amp;lt;aside className=&amp;quot;md:col-span-3 lg:col-span-3 bg-white border border-neutral-200 rounded-2xl p-3 shadow-sm&amp;quot;&amp;gt;&lt;br /&gt;
          &amp;lt;h2 className=&amp;quot;text-sm font-semibold uppercase tracking-wider text-neutral-500 mb-2&amp;quot;&amp;gt;Pièces&amp;lt;/h2&amp;gt;&lt;br /&gt;
          &amp;lt;div className=&amp;quot;space-y-2 max-h-[50vh] overflow-auto pr-1&amp;quot;&amp;gt;&lt;br /&gt;
            {project.rooms.map((r) =&amp;gt; (&lt;br /&gt;
              &amp;lt;button&lt;br /&gt;
                key={r.id}&lt;br /&gt;
                onClick={() =&amp;gt; setActiveRoom(r.id)}&lt;br /&gt;
                className={`w-full text-left px-3 py-2 rounded-xl border ${&lt;br /&gt;
                  r.id === activeRoom?.id ? &amp;quot;border-neutral-900 bg-neutral-900 text-white&amp;quot; : &amp;quot;border-neutral-200 hover:border-neutral-300&amp;quot;&lt;br /&gt;
                }`}&lt;br /&gt;
              &amp;gt;&lt;br /&gt;
                &amp;lt;div className=&amp;quot;font-medium&amp;quot;&amp;gt;{r.name}&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div className=&amp;quot;text-xs opacity-70&amp;quot;&amp;gt;{r.width} × {r.height}px&amp;lt;/div&amp;gt;&lt;br /&gt;
              &amp;lt;/button&amp;gt;&lt;br /&gt;
            ))}&lt;br /&gt;
          &amp;lt;/div&amp;gt;&lt;br /&gt;
          {activeRoom &amp;amp;&amp;amp; (&lt;br /&gt;
            &amp;lt;div className=&amp;quot;mt-4 space-y-3&amp;quot;&amp;gt;&lt;br /&gt;
              &amp;lt;div className=&amp;quot;flex items-center gap-2&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;input&lt;br /&gt;
                  className=&amp;quot;border rounded-lg px-2 py-1 flex-1&amp;quot;&lt;br /&gt;
                  value={activeRoom.name}&lt;br /&gt;
                  onChange={(e) =&amp;gt; updateRoom({ name: e.target.value })}&lt;br /&gt;
                /&amp;gt;&lt;br /&gt;
                &amp;lt;button className=&amp;quot;px-3 py-1.5 rounded-lg border border-red-200 text-red-700 hover:bg-red-50&amp;quot; onClick={() =&amp;gt; deleteRoom(activeRoom.id)}&amp;gt;&lt;br /&gt;
                  Supprimer&lt;br /&gt;
                &amp;lt;/button&amp;gt;&lt;br /&gt;
              &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
              &amp;lt;div className=&amp;quot;grid grid-cols-2 gap-2&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;label className=&amp;quot;col-span-2 text-sm font-semibold&amp;quot;&amp;gt;Fond de la pièce&amp;lt;/label&amp;gt;&lt;br /&gt;
                &amp;lt;button&lt;br /&gt;
                  className={`px-2 py-1 rounded-lg border ${activeRoom.bgType === &amp;quot;color&amp;quot; ? &amp;quot;border-neutral-900&amp;quot; : &amp;quot;border-neutral-200&amp;quot;}`}&lt;br /&gt;
                  onClick={() =&amp;gt; updateRoom({ bgType: &amp;quot;color&amp;quot; })}&lt;br /&gt;
                &amp;gt;&lt;br /&gt;
                  Couleur&lt;br /&gt;
                &amp;lt;/button&amp;gt;&lt;br /&gt;
                &amp;lt;button&lt;br /&gt;
                  className={`px-2 py-1 rounded-lg border ${activeRoom.bgType === &amp;quot;image&amp;quot; ? &amp;quot;border-neutral-900&amp;quot; : &amp;quot;border-neutral-200&amp;quot;}`}&lt;br /&gt;
                  onClick={() =&amp;gt; updateRoom({ bgType: &amp;quot;image&amp;quot; })}&lt;br /&gt;
                &amp;gt;&lt;br /&gt;
                  Image&lt;br /&gt;
                &amp;lt;/button&amp;gt;&lt;br /&gt;
                {activeRoom.bgType === &amp;quot;color&amp;quot; ? (&lt;br /&gt;
                  &amp;lt;input&lt;br /&gt;
                    type=&amp;quot;color&amp;quot;&lt;br /&gt;
                    value={activeRoom.bgColor}&lt;br /&gt;
                    onChange={(e) =&amp;gt; updateRoom({ bgColor: e.target.value })}&lt;br /&gt;
                    className=&amp;quot;col-span-2 w-full h-10 rounded-lg border&amp;quot;&lt;br /&gt;
                  /&amp;gt;&lt;br /&gt;
                ) : (&lt;br /&gt;
                  &amp;lt;label className=&amp;quot;col-span-2 w-full px-3 py-2 rounded-lg border cursor-pointer hover:bg-neutral-50&amp;quot;&amp;gt;&lt;br /&gt;
                    Importer une image de fond&lt;br /&gt;
                    &amp;lt;input&lt;br /&gt;
                      type=&amp;quot;file&amp;quot;&lt;br /&gt;
                      accept=&amp;quot;image/*&amp;quot;&lt;br /&gt;
                      className=&amp;quot;hidden&amp;quot;&lt;br /&gt;
                      onChange={async (e) =&amp;gt; {&lt;br /&gt;
                        const f = e.target.files?.[0];&lt;br /&gt;
                        if (!f) return;&lt;br /&gt;
                        const dataUrl = await fileToDataURL(f);&lt;br /&gt;
                        updateRoom({ bgImage: dataUrl });&lt;br /&gt;
                      }}&lt;br /&gt;
                    /&amp;gt;&lt;br /&gt;
                  &amp;lt;/label&amp;gt;&lt;br /&gt;
                )}&lt;br /&gt;
              &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
              &amp;lt;div className=&amp;quot;grid grid-cols-2 gap-2&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;label className=&amp;quot;col-span-2 text-sm font-semibold&amp;quot;&amp;gt;Taille de la scène&amp;lt;/label&amp;gt;&lt;br /&gt;
                &amp;lt;input&lt;br /&gt;
                  type=&amp;quot;number&amp;quot;&lt;br /&gt;
                  className=&amp;quot;border rounded-lg px-2 py-1&amp;quot;&lt;br /&gt;
                  value={activeRoom.width}&lt;br /&gt;
                  onChange={(e) =&amp;gt; updateRoom({ width: clampInt(e.target.value, 400, 4000) })}&lt;br /&gt;
                /&amp;gt;&lt;br /&gt;
                &amp;lt;input&lt;br /&gt;
                  type=&amp;quot;number&amp;quot;&lt;br /&gt;
                  className=&amp;quot;border rounded-lg px-2 py-1&amp;quot;&lt;br /&gt;
                  value={activeRoom.height}&lt;br /&gt;
                  onChange={(e) =&amp;gt; updateRoom({ height: clampInt(e.target.value, 300, 3000) })}&lt;br /&gt;
                /&amp;gt;&lt;br /&gt;
              &amp;lt;/div&amp;gt;&lt;br /&gt;
&lt;br /&gt;
              &amp;lt;AddItemPanel onAdd={async (item) =&amp;gt; {&lt;br /&gt;
                setProject((p) =&amp;gt; ({&lt;br /&gt;
                  ...p,&lt;br /&gt;
                  rooms: p.rooms.map((r) =&amp;gt; r.id === activeRoom.id ? { ...r, items: [...r.items, item] } : r),&lt;br /&gt;
                }));&lt;br /&gt;
              }} /&amp;gt;&lt;br /&gt;
&lt;br /&gt;
              &amp;lt;LayersPanel room={activeRoom} onChange={(items) =&amp;gt; updateRoom({ items })} /&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
          )}&lt;br /&gt;
        &amp;lt;/aside&amp;gt;&lt;br /&gt;
&lt;br /&gt;
        {/* Canvas */}&lt;br /&gt;
        &amp;lt;section className=&amp;quot;md:col-span-9 lg:col-span-9&amp;quot;&amp;gt;&lt;br /&gt;
          {activeRoom ? (&lt;br /&gt;
            &amp;lt;RoomCanvas key={activeRoom.id} room={activeRoom} onChange={(patch) =&amp;gt; updateRoom(patch)} present={present} /&amp;gt;&lt;br /&gt;
          ) : (&lt;br /&gt;
            &amp;lt;div className=&amp;quot;h-96 bg-white border border-dashed rounded-2xl grid place-items-center text-neutral-400&amp;quot;&amp;gt;Aucune pièce&amp;lt;/div&amp;gt;&lt;br /&gt;
          )}&lt;br /&gt;
        &amp;lt;/section&amp;gt;&lt;br /&gt;
      &amp;lt;/main&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
  );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function clampInt(val, min, max) {&lt;br /&gt;
  const n = parseInt(val, 10);&lt;br /&gt;
  if (isNaN(n)) return min;&lt;br /&gt;
  return Math.max(min, Math.min(max, n));&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
async function fileToDataURL(file) {&lt;br /&gt;
  return new Promise((resolve, reject) =&amp;gt; {&lt;br /&gt;
    const reader = new FileReader();&lt;br /&gt;
    reader.onload = () =&amp;gt; resolve(reader.result);&lt;br /&gt;
    reader.onerror = reject;&lt;br /&gt;
    reader.readAsDataURL(file);&lt;br /&gt;
  });&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function AddItemPanel({ onAdd }) {&lt;br /&gt;
  const [label, setLabel] = useState(&amp;quot;&amp;quot;);&lt;br /&gt;
  const [scale, setScale] = useState(1);&lt;br /&gt;
  const [file, setFile] = useState(null);&lt;br /&gt;
&lt;br /&gt;
  const handleAdd = async () =&amp;gt; {&lt;br /&gt;
    let src = null;&lt;br /&gt;
    if (file) src = await fileToDataURL(file);&lt;br /&gt;
    const item = {&lt;br /&gt;
      id: cryptoRandomId(),&lt;br /&gt;
      label: label || &amp;quot;Objet&amp;quot;,&lt;br /&gt;
      x: 50 + Math.random() * 200,&lt;br /&gt;
      y: 50 + Math.random() * 120,&lt;br /&gt;
      w: 180,&lt;br /&gt;
      h: 180,&lt;br /&gt;
      rotation: 0,&lt;br /&gt;
      src,&lt;br /&gt;
      scale: scale || 1,&lt;br /&gt;
      z: Date.now(),&lt;br /&gt;
      note: &amp;quot;&amp;quot;,&lt;br /&gt;
    };&lt;br /&gt;
    onAdd(item);&lt;br /&gt;
    setLabel(&amp;quot;&amp;quot;);&lt;br /&gt;
    setFile(null);&lt;br /&gt;
    setScale(1);&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  return (&lt;br /&gt;
    &amp;lt;div className=&amp;quot;mt-2 p-3 border rounded-2xl&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;div className=&amp;quot;text-sm font-semibold mb-2&amp;quot;&amp;gt;Ajouter un objet / tableau / meuble&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div className=&amp;quot;flex flex-col gap-2&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;input className=&amp;quot;border rounded-lg px-2 py-1&amp;quot; placeholder=&amp;quot;Nom (ex. Portrait de 1860)&amp;quot; value={label} onChange={(e) =&amp;gt; setLabel(e.target.value)} /&amp;gt;&lt;br /&gt;
        &amp;lt;label className=&amp;quot;px-3 py-2 rounded-lg border cursor-pointer hover:bg-neutral-50&amp;quot;&amp;gt;&lt;br /&gt;
          Importer une image (PNG/JPG)&lt;br /&gt;
          &amp;lt;input type=&amp;quot;file&amp;quot; accept=&amp;quot;image/*&amp;quot; className=&amp;quot;hidden&amp;quot; onChange={(e) =&amp;gt; setFile(e.target.files?.[0] || null)} /&amp;gt;&lt;br /&gt;
        &amp;lt;/label&amp;gt;&lt;br /&gt;
        &amp;lt;div className=&amp;quot;flex items-center gap-2&amp;quot;&amp;gt;&lt;br /&gt;
          &amp;lt;span className=&amp;quot;text-sm text-neutral-600&amp;quot;&amp;gt;Échelle&amp;lt;/span&amp;gt;&lt;br /&gt;
          &amp;lt;input type=&amp;quot;range&amp;quot; min={0.2} max={3} step={0.1} value={scale} onChange={(e) =&amp;gt; setScale(parseFloat(e.target.value))} className=&amp;quot;flex-1&amp;quot; /&amp;gt;&lt;br /&gt;
          &amp;lt;span className=&amp;quot;text-sm w-10 text-right&amp;quot;&amp;gt;{scale.toFixed(1)}×&amp;lt;/span&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;button className=&amp;quot;px-3 py-2 rounded-lg bg-neutral-900 text-white hover:bg-neutral-800&amp;quot; onClick={handleAdd}&amp;gt;+ Ajouter à la pièce&amp;lt;/button&amp;gt;&lt;br /&gt;
        &amp;lt;p className=&amp;quot;text-xs text-neutral-500&amp;quot;&amp;gt;Astuce : vous pouvez ajouter des objets sans image (cartels textuels) pour matérialiser des idées à imaginer plus tard.&amp;lt;/p&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
  );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function LayersPanel({ room, onChange }) {&lt;br /&gt;
  const [selectedId, setSelectedId] = useState(null);&lt;br /&gt;
  const items = room.items || [];&lt;br /&gt;
&lt;br /&gt;
  const remove = (id) =&amp;gt; onChange(items.filter((i) =&amp;gt; i.id !== id));&lt;br /&gt;
  const bringToFront = (id) =&amp;gt; onChange(items.map((i) =&amp;gt; (i.id === id ? { ...i, z: Date.now() } : i)));&lt;br /&gt;
&lt;br /&gt;
  return (&lt;br /&gt;
    &amp;lt;div className=&amp;quot;mt-2 p-3 border rounded-2xl&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;div className=&amp;quot;flex items-center justify-between mb-2&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div className=&amp;quot;text-sm font-semibold&amp;quot;&amp;gt;Calques / objets ({items.length})&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div className=&amp;quot;max-h-56 overflow-auto divide-y&amp;quot;&amp;gt;&lt;br /&gt;
        {items.length === 0 &amp;amp;&amp;amp; &amp;lt;div className=&amp;quot;text-sm text-neutral-500 py-4&amp;quot;&amp;gt;Aucun objet encore&amp;lt;/div&amp;gt;}&lt;br /&gt;
        {items&lt;br /&gt;
          .slice()&lt;br /&gt;
          .sort((a, b) =&amp;gt; a.z - b.z)&lt;br /&gt;
          .map((i) =&amp;gt; (&lt;br /&gt;
            &amp;lt;div key={i.id} className={`flex items-center gap-2 py-2 ${selectedId === i.id ? &amp;quot;bg-neutral-50&amp;quot; : &amp;quot;&amp;quot;}`}&amp;gt;&lt;br /&gt;
              &amp;lt;button&lt;br /&gt;
                className=&amp;quot;px-2 py-1 rounded border&amp;quot;&lt;br /&gt;
                onClick={() =&amp;gt; bringToFront(i.id)}&lt;br /&gt;
                title=&amp;quot;Amener au premier plan&amp;quot;&lt;br /&gt;
              &amp;gt;⬆️&amp;lt;/button&amp;gt;&lt;br /&gt;
              &amp;lt;div className=&amp;quot;flex-1&amp;quot;&amp;gt;&lt;br /&gt;
                &amp;lt;div className=&amp;quot;text-sm font-medium&amp;quot;&amp;gt;{i.label}&amp;lt;/div&amp;gt;&lt;br /&gt;
                &amp;lt;div className=&amp;quot;text-[11px] text-neutral-500&amp;quot;&amp;gt;x:{i.x.toFixed(0)} y:{i.y.toFixed(0)} • rot:{i.rotation}°&amp;lt;/div&amp;gt;&lt;br /&gt;
              &amp;lt;/div&amp;gt;&lt;br /&gt;
              &amp;lt;button className=&amp;quot;px-2 py-1 rounded border border-red-200 text-red-700&amp;quot; onClick={() =&amp;gt; remove(i.id)}&amp;gt;Suppr&amp;lt;/button&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
          ))}&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
  );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function RoomCanvas({ room, onChange, present }) {&lt;br /&gt;
  const containerRef = useRef(null);&lt;br /&gt;
  const [drag, setDrag] = useState(null); // {id, dx, dy}&lt;br /&gt;
  const [resize, setResize] = useState(null); // {id, startW, startH}&lt;br /&gt;
  const [rot, setRot] = useState(null); // {id, startRot}&lt;br /&gt;
&lt;br /&gt;
  const setItems = (updater) =&amp;gt; onChange({ items: updater(room.items || []) });&lt;br /&gt;
&lt;br /&gt;
  const styleBg = room.bgType === &amp;quot;image&amp;quot;&lt;br /&gt;
    ? { backgroundImage: `url(${room.bgImage})`, backgroundSize: &amp;quot;cover&amp;quot;, backgroundPosition: &amp;quot;center&amp;quot; }&lt;br /&gt;
    : { backgroundColor: room.bgColor };&lt;br /&gt;
&lt;br /&gt;
  const scaleFit = present ? Math.min((window.innerWidth - 48) / room.width, (window.innerHeight - 160) / room.height, 1) : 1;&lt;br /&gt;
&lt;br /&gt;
  useEffect(() =&amp;gt; {&lt;br /&gt;
    const onUp = () =&amp;gt; {&lt;br /&gt;
      setDrag(null);&lt;br /&gt;
      setResize(null);&lt;br /&gt;
      setRot(null);&lt;br /&gt;
    };&lt;br /&gt;
    const onMove = (e) =&amp;gt; {&lt;br /&gt;
      if (drag) {&lt;br /&gt;
        const { id, startX, startY, origX, origY } = drag;&lt;br /&gt;
        const dx = (e.clientX - startX) / scaleFit;&lt;br /&gt;
        const dy = (e.clientY - startY) / scaleFit;&lt;br /&gt;
        setItems((items) =&amp;gt; items.map((it) =&amp;gt; (it.id === id ? { ...it, x: origX + dx, y: origY + dy } : it)));&lt;br /&gt;
      }&lt;br /&gt;
      if (resize) {&lt;br /&gt;
        const { id, startX, startY, startW, startH } = resize;&lt;br /&gt;
        const dx = (e.clientX - startX) / scaleFit;&lt;br /&gt;
        const dy = (e.clientY - startY) / scaleFit;&lt;br /&gt;
        setItems((items) =&amp;gt; items.map((it) =&amp;gt; (it.id === id ? { ...it, w: Math.max(40, startW + dx), h: Math.max(40, startH + dy) } : it)));&lt;br /&gt;
      }&lt;br /&gt;
      if (rot) {&lt;br /&gt;
        const { id, centerX, centerY, startAngle } = rot;&lt;br /&gt;
        const ang = Math.atan2(e.clientY - centerY, e.clientX - centerX);&lt;br /&gt;
        const deg = Math.round((ang * 180) / Math.PI);&lt;br /&gt;
        setItems((items) =&amp;gt; items.map((it) =&amp;gt; (it.id === id ? { ...it, rotation: deg - startAngle } : it)));&lt;br /&gt;
      }&lt;br /&gt;
    };&lt;br /&gt;
    window.addEventListener(&amp;quot;mousemove&amp;quot;, onMove);&lt;br /&gt;
    window.addEventListener(&amp;quot;mouseup&amp;quot;, onUp);&lt;br /&gt;
    return () =&amp;gt; {&lt;br /&gt;
      window.removeEventListener(&amp;quot;mousemove&amp;quot;, onMove);&lt;br /&gt;
      window.removeEventListener(&amp;quot;mouseup&amp;quot;, onUp);&lt;br /&gt;
    };&lt;br /&gt;
  }, [drag, resize, rot, scaleFit]);&lt;br /&gt;
&lt;br /&gt;
  const startDrag = (e, it) =&amp;gt; {&lt;br /&gt;
    e.stopPropagation();&lt;br /&gt;
    setDrag({ id: it.id, startX: e.clientX, startY: e.clientY, origX: it.x, origY: it.y });&lt;br /&gt;
    bringToFront(it.id);&lt;br /&gt;
  };&lt;br /&gt;
  const startResize = (e, it) =&amp;gt; {&lt;br /&gt;
    e.stopPropagation();&lt;br /&gt;
    setResize({ id: it.id, startX: e.clientX, startY: e.clientY, startW: it.w, startH: it.h });&lt;br /&gt;
    bringToFront(it.id);&lt;br /&gt;
  };&lt;br /&gt;
  const startRotate = (e, it, box) =&amp;gt; {&lt;br /&gt;
    e.stopPropagation();&lt;br /&gt;
    const rect = box.getBoundingClientRect();&lt;br /&gt;
    const centerX = rect.left + rect.width / 2;&lt;br /&gt;
    const centerY = rect.top + rect.height / 2;&lt;br /&gt;
    const ang = Math.atan2(e.clientY - centerY, e.clientX - centerX);&lt;br /&gt;
    const startAngle = Math.round((ang * 180) / Math.PI) - it.rotation;&lt;br /&gt;
    setRot({ id: it.id, centerX, centerY, startAngle });&lt;br /&gt;
    bringToFront(it.id);&lt;br /&gt;
  };&lt;br /&gt;
  const bringToFront = (id) =&amp;gt; setItems((items) =&amp;gt; items.map((i) =&amp;gt; (i.id === id ? { ...i, z: Date.now() } : i)));&lt;br /&gt;
&lt;br /&gt;
  return (&lt;br /&gt;
    &amp;lt;div className=&amp;quot;bg-white border border-neutral-200 rounded-2xl p-3 shadow-sm&amp;quot;&amp;gt;&lt;br /&gt;
      &amp;lt;div className=&amp;quot;flex items-center justify-between mb-2&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div&amp;gt;&lt;br /&gt;
          &amp;lt;div className=&amp;quot;text-lg font-semibold&amp;quot;&amp;gt;{room.name}&amp;lt;/div&amp;gt;&lt;br /&gt;
          &amp;lt;div className=&amp;quot;text-xs text-neutral-500&amp;quot;&amp;gt;Scène {room.width} × {room.height}px&amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
        &amp;lt;div className=&amp;quot;text-xs text-neutral-500&amp;quot;&amp;gt;Glissez, redimensionnez, faites pivoter. Double‑clic pour éditer le libellé.&amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;div className=&amp;quot;w-full overflow-auto&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;div&lt;br /&gt;
          ref={containerRef}&lt;br /&gt;
          className=&amp;quot;relative rounded-xl border border-neutral-200 mx-auto&amp;quot;&lt;br /&gt;
          style={{ width: room.width * scaleFit, height: room.height * scaleFit, transform: `scale(${scaleFit})`, transformOrigin: &amp;quot;top left&amp;quot;, ...styleBg }}&lt;br /&gt;
        &amp;gt;&lt;br /&gt;
          {(room.items || [])&lt;br /&gt;
            .slice()&lt;br /&gt;
            .sort((a, b) =&amp;gt; a.z - b.z)&lt;br /&gt;
            .map((it) =&amp;gt; (&lt;br /&gt;
              &amp;lt;DraggableItem&lt;br /&gt;
                key={it.id}&lt;br /&gt;
                item={it}&lt;br /&gt;
                onMouseDown={startDrag}&lt;br /&gt;
                onResize={startResize}&lt;br /&gt;
                onRotate={startRotate}&lt;br /&gt;
                onChange={(patch) =&amp;gt; setItems((items) =&amp;gt; items.map((i) =&amp;gt; (i.id === it.id ? { ...i, ...patch } : i)))}&lt;br /&gt;
              /&amp;gt;&lt;br /&gt;
            ))}&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
  );&lt;br /&gt;
}&lt;br /&gt;
&lt;br /&gt;
function DraggableItem({ item, onMouseDown, onResize, onRotate, onChange }) {&lt;br /&gt;
  const boxRef = useRef(null);&lt;br /&gt;
  const [editing, setEditing] = useState(false);&lt;br /&gt;
&lt;br /&gt;
  const style = {&lt;br /&gt;
    position: &amp;quot;absolute&amp;quot;,&lt;br /&gt;
    left: item.x,&lt;br /&gt;
    top: item.y,&lt;br /&gt;
    width: item.w,&lt;br /&gt;
    height: item.h,&lt;br /&gt;
    transform: `rotate(${item.rotation || 0}deg) scale(${item.scale || 1})`,&lt;br /&gt;
    transformOrigin: &amp;quot;center center&amp;quot;,&lt;br /&gt;
  };&lt;br /&gt;
&lt;br /&gt;
  return (&lt;br /&gt;
    &amp;lt;div ref={boxRef} style={style} className=&amp;quot;group select-none&amp;quot; onDoubleClick={() =&amp;gt; setEditing(true)}&amp;gt;&lt;br /&gt;
      &amp;lt;div&lt;br /&gt;
        className=&amp;quot;w-full h-full rounded-xl overflow-hidden border border-neutral-300 bg-white/70 backdrop-blur cursor-move shadow-sm&amp;quot;&lt;br /&gt;
        onMouseDown={(e) =&amp;gt; onMouseDown(e, item)}&lt;br /&gt;
      &amp;gt;&lt;br /&gt;
        {item.src ? (&lt;br /&gt;
          &amp;lt;img src={item.src} alt={item.label} className=&amp;quot;w-full h-full object-contain&amp;quot; draggable={false} /&amp;gt;&lt;br /&gt;
        ) : (&lt;br /&gt;
          &amp;lt;div className=&amp;quot;w-full h-full grid place-items-center text-center p-3 text-neutral-600&amp;quot;&amp;gt;&lt;br /&gt;
            &amp;lt;div&amp;gt;&lt;br /&gt;
              &amp;lt;div className=&amp;quot;font-semibold&amp;quot;&amp;gt;{item.label}&amp;lt;/div&amp;gt;&lt;br /&gt;
              &amp;lt;div className=&amp;quot;text-xs&amp;quot;&amp;gt;(Sans image – idée/repère)&amp;lt;/div&amp;gt;&lt;br /&gt;
            &amp;lt;/div&amp;gt;&lt;br /&gt;
          &amp;lt;/div&amp;gt;&lt;br /&gt;
        )}&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
      {/* Controls */}&lt;br /&gt;
      &amp;lt;div className=&amp;quot;absolute -top-8 left-1/2 -translate-x-1/2 scale-0 group-hover:scale-100 transition&amp;quot;&amp;gt;&lt;br /&gt;
        &amp;lt;button&lt;br /&gt;
          className=&amp;quot;px-2 py-1 text-xs rounded-lg bg-neutral-900 text-white&amp;quot;&lt;br /&gt;
          onClick={() =&amp;gt; onChange({ note: prompt(&amp;quot;Ajouter/éditer une note :&amp;quot;, item.note || &amp;quot;&amp;quot;) || item.note })}&lt;br /&gt;
        &amp;gt;&lt;br /&gt;
          ✎ Note&lt;br /&gt;
        &amp;lt;/button&amp;gt;&lt;br /&gt;
      &amp;lt;/div&amp;gt;&lt;br /&gt;
      &amp;lt;button&lt;br /&gt;
        className=&amp;quot;absolute -bottom-3 -right-3 w-6 h-6 rounded-full bg-neutral-900 text-white grid place-items-center cursor-se-resize&amp;quot;&lt;br /&gt;
        onMouseDown={(e) =&amp;gt; onResize(e, item)}&lt;br /&gt;
        title=&amp;quot;Redimensionner&amp;quot;&lt;br /&gt;
      &amp;gt;&lt;br /&gt;
        ↘&lt;br /&gt;
      &amp;lt;/button&amp;gt;&lt;br /&gt;
      &amp;lt;button&lt;br /&gt;
        className=&amp;quot;absolute -top-3 -right-3 w-6 h-6 rounded-full bg-neutral-900 text-white grid place-items-center cursor-alias&amp;quot;&lt;br /&gt;
        onMouseDown={(e) =&amp;gt; onRotate(e, item, boxRef.current)}&lt;br /&gt;
        title=&amp;quot;Pivoter&amp;quot;&lt;br /&gt;
      &amp;gt;&lt;br /&gt;
        ⟳&lt;br /&gt;
      &amp;lt;/button&amp;gt;&lt;br /&gt;
      {editing &amp;amp;&amp;amp; (&lt;br /&gt;
        &amp;lt;div className=&amp;quot;absolute inset-0 grid place-items-center bg-white/80 p-2&amp;quot; onDoubleClick={(e) =&amp;gt; e.stopPropagation()}&amp;gt;&lt;br /&gt;
          &amp;lt;input&lt;br /&gt;
            autoFocus&lt;br /&gt;
            className=&amp;quot;w-11/12 border rounded-lg px-2 py-1&amp;quot;&lt;br /&gt;
            value={item.label}&lt;br /&gt;
            onChange={(e) =&amp;gt; onChange({ label: e.target.value })}&lt;br /&gt;
            onBlur={() =&amp;gt; setEditing(false)}&lt;br /&gt;
            onKeyDown={(e) =&amp;gt; e.key === &amp;quot;Enter&amp;quot; &amp;amp;&amp;amp; setEditing(false)}&lt;br /&gt;
          /&amp;gt;&lt;br /&gt;
        &amp;lt;/div&amp;gt;&lt;br /&gt;
      )}&lt;br /&gt;
    &amp;lt;/div&amp;gt;&lt;br /&gt;
  );&lt;br /&gt;
}&lt;/div&gt;</summary>
		<author><name>Admin</name></author>
	</entry>
</feed>