<!DOCTYPE html>
<html>
<head>
  <title>3 Panel Display Board Maker</title>
  <style>
    :root {
      --primary-color: #2c3e50;
      --secondary-color: #3498db;
    }
    * {
      box-sizing: border-box;
      margin: 0;
      padding: 0;
    }
    /* The sidebar scrolls; its width is fixed */
    .sidebar {
      width: 320px;
      background: #f5f6fa;
      padding: 20px;
      border-right: 2px solid #ddd;
      overflow-y: auto;
      height: 100vh;
    }
    /* The main area (editor & 3D view) fills the remaining space and does not scroll */
    .main {
      flex: 1;
      position: relative;
      overflow: hidden;
      background: #fff;
    }
    /* The 2D editor container – its size is computed via script, then scaled to fit */
    #editor-container {
      position: absolute;
      top: 0;
      left: 0;
      transform-origin: top left;
    }
    /* Fabric.js canvas container fills the editor container */
    #canvas-container {
      position: relative;
      width: 100%;
      height: 100%;
    }
    #c {
      position: absolute;
      top: 0;
      left: 0;
    }
    /* The 3D container fills the main area (hidden by default) */
    #threejs-container {
      position: absolute;
      top: 0;
      left: 0;
      width: 100%;
      height: 100%;
      display: none;
      background: #cccccc;
    }
    .controls-group {
      margin: 15px 0;
      padding: 10px;
      border: 1px solid #ddd;
    }
    .text-toolbar {
      display: flex;
      gap: 5px;
      margin: 10px 0;
      flex-wrap: wrap;
    }
    button, select, input {
      padding: 5px 10px;
      border: 1px solid #ddd;
      background: white;
      cursor: pointer;
    }
    #exportBtn {
      position: fixed;
      bottom: 20px;
      right: 20px;
      padding: 10px 20px;
      background: var(--secondary-color);
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
    }
    #exit3DBtn {
      position: fixed;
      top: 20px;
      right: 20px;
      padding: 10px 20px;
      background: var(--secondary-color);
      color: white;
      border: none;
      border-radius: 4px;
      cursor: pointer;
      z-index: 10;
      display: none;
    }
    body {
      display: flex;
      height: 100vh;
    }
  </style>
</head>
<body>
  <!-- Sidebar (scrollable) -->
  <div class="sidebar">
    <div class="controls-group">
      <h3>Board Scale: <span id="scaleValue">100%</span></h3>
      <input type="range" id="boardScale" min="0.1" max="3" step="0.1" value="1">
    </div>
    <div class="controls-group">
      <h3>Text</h3>
      <div class="text-toolbar">
        <button id="boldBtn"><strong>B</strong></button>
        <button id="italicBtn"><em>I</em></button>
        <button id="underlineBtn"><u>U</u></button>
        <select id="textAlign">
          <option value="left">Left</option>
          <option value="center">Center</option>
          <option value="right">Right</option>
        </select>
      </div>
      <label>Font: <select id="fontFamily">
        <option>Arial</option>
        <option>Verdana</option>
        <option>Times New Roman</option>
      </select></label>
      <label>Size: <input type="number" id="fontSize" value="16" min="8" max="72"></label>
      <!-- Changing color applies to every selected object that supports fill -->
      <label>Color: <input type="color" id="textColor"></label>
      <button onclick="addTextElement()">Add Text</button>
    </div>
    <div class="controls-group">
      <h3>Images</h3>
      <input type="file" id="imageUpload" accept="image/*">
      <div id="imageGallery"></div>
    </div>
    <div class="controls-group">
      <h3>Shapes</h3>
      <button onclick="addShape('rect')">Rectangle</button>
      <button onclick="addShape('circle')">Circle</button>
      <!-- Changing shape color applies to every selected object that supports fill -->
      <label>Color: <input type="color" id="shapeColor" value="#ff0000"></label>
    </div>
    <div class="controls-group">
      <h3>Board Layout</h3>
      <label>Middle Board Width (inches): 
        <input type="number" id="middleBoardWidth" value="6" min="1">
      </label>
      <br>
      <label>Side Board Width (inches): 
        <input type="number" id="sideBoardWidth" value="3" min="1">
      </label>
      <br>
      <label>Board Height (inches): 
        <input type="number" id="boardHeight" value="8" min="1">
      </label>
      <br>
      <label>Board Color: 
        <input type="color" id="boardColor" value="#ffffff">
      </label>
      <br>
      <button onclick="updateBoardLayout()">Update Board Layout</button>
    </div>
    <div class="controls-group">
      <h3>Layers</h3>
      <button onclick="bringForward()">Bring Forward</button>
      <button onclick="sendBackward()">Send Backward</button>
      <button onclick="bringToFront()">Bring to Front</button>
      <button onclick="sendToBack()">Send to Back</button>
    </div>
    <div class="controls-group">
      <button onclick="enter3DView()">View in 3D</button>
    </div>
    <div class="controls-group">
      <h3>Project</h3>
      <button onclick="clearCanvas()">Clear Canvas</button>
      <button onclick="saveProject()">Save Project JSON</button>
      <button onclick="loadProject()">Load Project JSON</button>
    </div>
  </div>

  <!-- Main area -->
  <div class="main">
    <!-- 2D Editor Container (will be scaled to fit) -->
    <div id="editor-container">
      <div id="canvas-container">
        <canvas id="c"></canvas>
      </div>
    </div>
    <button id="exportBtn">Export</button>
    <!-- 3D Container -->
    <div id="threejs-container"></div>
    <button id="exit3DBtn" onclick="exit3DView()">Exit 3D</button>
  </div>

  <!-- Three.js and OrbitControls via jsDelivr -->
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/build/three.min.js"></script>
  <script src="https://cdn.jsdelivr.net/npm/three@0.128.0/examples/js/controls/OrbitControls.js"></script>
  <!-- Fabric.js -->
  <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js"></script>
  <script>
    /*******************
     *  EDITOR SECTION *
     *******************/
    let canvas;
    let currentScale = 1;
    let selectedObject = null;
    let panels = [];
    let undoStack = [];
    let redoStack = [];
    const MAX_STACK_SIZE = 100;
    const DPI = 96;
    let defaultMiddleBoardInches = 6;
    let defaultSideBoardInches = 3;
    const margin = 20;
    
    // Compute board dimensions from layout settings.
    function computeBoardDimensions() {
      const boardWidth = ((defaultSideBoardInches * 2) + defaultMiddleBoardInches) * DPI + margin * 2;
      const boardHeightInches = parseFloat(document.getElementById('boardHeight').value) || 8;
      const boardHeight = boardHeightInches * DPI;
      return { boardWidth, boardHeight };
    }
    
    // Fit the 2D board into the available main area.
    function fitBoardToScreen() {
      const dims = computeBoardDimensions();
      const mainWidth = window.innerWidth - 320; // subtract sidebar width
      const mainHeight = window.innerHeight;
      const scaleFactor = Math.min(mainWidth / dims.boardWidth, mainHeight / dims.boardHeight, 1);
      document.getElementById('editor-container').style.transform = `scale(${scaleFactor})`;
    }
    
    function initCanvas() {
      const dims = computeBoardDimensions();
      const editorContainer = document.getElementById('editor-container');
      editorContainer.style.width = dims.boardWidth + "px";
      editorContainer.style.height = dims.boardHeight + "px";
      
      const canvasContainer = document.getElementById('canvas-container');
      canvasContainer.style.width = "100%";
      canvasContainer.style.height = "100%";
      
      canvas = new fabric.Canvas('c', {
        preserveObjectStacking: true,
        selection: true,
        defaultCursor: 'default'
      });
      canvas.setDimensions({ width: dims.boardWidth, height: dims.boardHeight });
      document.getElementById('c').style.width = dims.boardWidth + "px";
      document.getElementById('c').style.height = dims.boardHeight + "px";
      
      setupEventListeners();
      createPanels();
      setupKeyboardShortcuts();
      saveState();
      fitBoardToScreen();
    }
    
    window.addEventListener('resize', fitBoardToScreen);
    
    function createPanels() {
      panels.forEach(panel => canvas.remove(panel));
      panels = [];
      
      const dims = computeBoardDimensions();
      const boardColor = document.getElementById('boardColor').value || '#ffffff';
      const leftPanelWidth = defaultSideBoardInches * DPI;
      const middlePanelWidth = defaultMiddleBoardInches * DPI;
      const rightPanelWidth = defaultSideBoardInches * DPI;
      const totalPanelWidth = leftPanelWidth + middlePanelWidth + rightPanelWidth;
      const startX = (dims.boardWidth - totalPanelWidth) / 2;
      const panelHeight = dims.boardHeight - margin * 2;
      
      const commonProps = {
        top: margin,
        height: panelHeight,
        fill: boardColor,
        stroke: '#333',
        strokeWidth: 2,
        selectable: false,
        evented: false,
        lockMovementX: true,
        lockMovementY: true,
        lockRotation: true,
        lockScalingX: true,
        lockScalingY: true,
        isPanel: true
      };
      
      const leftPanel = new fabric.Rect(Object.assign({}, commonProps, {
        left: startX,
        width: leftPanelWidth
      }));
      const middlePanel = new fabric.Rect(Object.assign({}, commonProps, {
        left: startX + leftPanelWidth,
        width: middlePanelWidth
      }));
      const rightPanel = new fabric.Rect(Object.assign({}, commonProps, {
        left: startX + leftPanelWidth + middlePanelWidth,
        width: rightPanelWidth
      }));
      
      panels.push(leftPanel, middlePanel, rightPanel);
      canvas.add(...panels);
      panels.forEach(panel => canvas.sendToBack(panel));
    }
    
    function updateBoardLayout() {
      defaultMiddleBoardInches = parseFloat(document.getElementById('middleBoardWidth').value);
      defaultSideBoardInches = parseFloat(document.getElementById('sideBoardWidth').value);
      const dims = computeBoardDimensions();
      document.getElementById('editor-container').style.width = dims.boardWidth + "px";
      document.getElementById('editor-container').style.height = dims.boardHeight + "px";
      canvas.setDimensions({ width: dims.boardWidth, height: dims.boardHeight });
      createPanels();
      saveState();
      fitBoardToScreen();
    }
    
    function setupEventListeners() {
      canvas.on({
        'selection:created': e => handleSelection(e.selected),
        'selection:updated': e => handleSelection(e.selected),
        'selection:cleared': clearSelection
      });
      
      document.getElementById('boardScale').addEventListener('input', e => {
        currentScale = parseFloat(e.target.value);
        updateZoom();
      });
      
      document.getElementById('imageUpload').addEventListener('change', handleImageUpload);
      document.getElementById('exportBtn').addEventListener('click', exportCanvas);
      
      document.getElementById('boldBtn').addEventListener('click', () => toggleFormat('bold'));
      document.getElementById('italicBtn').addEventListener('click', () => toggleFormat('italic'));
      document.getElementById('underlineBtn').addEventListener('click', () => toggleFormat('underline'));
      document.getElementById('textAlign').addEventListener('change', handleTextAlign);
      document.getElementById('fontSize').addEventListener('input', handleFontSize);
      document.getElementById('textColor').addEventListener('input', handleTextColor);
      document.getElementById('fontFamily').addEventListener('change', handleFontFamily);
      
      document.getElementById('shapeColor').addEventListener('input', handleShapeColorChange);
    }
    
    // When multiple objects are selected the active object is an array or a group.
    function handleSelection(objs) {
      selectedObject = objs.length === 1 ? objs[0] : objs;
      updateFormatButtons();
    }
    
    function clearSelection() {
      selectedObject = null;
    }
    
    // Update format buttons only when a single textbox is selected.
    function updateFormatButtons() {
      if (!selectedObject || Array.isArray(selectedObject) || selectedObject.type !== 'textbox') return;
      document.getElementById('boldBtn').classList.toggle('active', !!selectedObject.fontWeight);
      document.getElementById('italicBtn').classList.toggle('active', !!selectedObject.fontStyle);
      document.getElementById('underlineBtn').classList.toggle('active', !!selectedObject.underline);
    }
    
    // Helper to update fill color for a single object, group, or active selection.
    function updateFillColor(newColor) {
      const active = canvas.getActiveObject();
      if (active) {
        if (active.type === 'activeSelection' || active.type === 'group') {
          active.getObjects().forEach(obj => {
            if (typeof obj.set === 'function' && obj.fill !== undefined) {
              obj.set('fill', newColor);
            }
          });
        } else {
          if (active.fill !== undefined) {
            active.set('fill', newColor);
          }
        }
        canvas.requestRenderAll();
        saveState();
      }
    }
    
    // Undo/Redo system (panels are not part of the undo stack)
    function saveState() {
      let tempPanels = [];
      canvas.getObjects().forEach(obj => {
        if (obj.isPanel) {
          tempPanels.push(obj);
          canvas.remove(obj);
        }
      });
      const state = JSON.stringify(canvas);
      undoStack.push(state);
      if (undoStack.length > MAX_STACK_SIZE) {
        undoStack.shift();
      }
      redoStack = [];
      canvas.add(...tempPanels);
      tempPanels.forEach(panel => canvas.sendToBack(panel));
    }
    
    function loadState(state) {
      canvas.loadFromJSON(state, () => {
        createPanels();
        canvas.renderAll();
      });
    }
    
    function undo() {
      if (undoStack.length > 1) {
        const current = undoStack.pop();
        redoStack.push(current);
        const prevState = undoStack[undoStack.length - 1];
        loadState(prevState);
      }
    }
    
    function redo() {
      if (redoStack.length > 0) {
        const nextState = redoStack.pop();
        undoStack.push(nextState);
        loadState(nextState);
      }
    }
    
    function setupKeyboardShortcuts() {
      document.addEventListener('keydown', (e) => {
        const tagName = document.activeElement.tagName.toLowerCase();
        if (tagName === 'input' || tagName === 'textarea') return;
        if (e.ctrlKey || e.metaKey) {
          switch(e.key.toLowerCase()){
            case 'z':
              if(e.shiftKey){ redo(); } else { undo(); }
              e.preventDefault();
              break;
            case 'y':
              redo();
              e.preventDefault();
              break;
            case 'c':
              copyObject();
              e.preventDefault();
              break;
            case 'v':
              pasteObject();
              e.preventDefault();
              break;
            case 'd':
              duplicateObject();
              e.preventDefault();
              break;
            // Group: Ctrl+G to group; Ctrl+Shift+G to ungroup.
            case 'g':
              if(!e.shiftKey){ groupSelected(); e.preventDefault(); }
              break;
          }
          if ((e.ctrlKey || e.metaKey) && e.shiftKey && e.key.toLowerCase() === 'g') {
            ungroupSelected();
            e.preventDefault();
          }
          // Zoom shortcuts.
          if(e.key === '+' || e.key === '='){
            zoomIn();
            e.preventDefault();
          } else if(e.key === '-'){
            zoomOut();
            e.preventDefault();
          } else if(e.key === '0'){
            resetZoom();
            e.preventDefault();
          }
          // Bring forward/backward layers using Ctrl + ArrowUp/ArrowDown.
          if(e.key === "ArrowUp"){
            bringForward();
            e.preventDefault();
          } else if(e.key === "ArrowDown"){
            sendBackward();
            e.preventDefault();
          }
        }
        if(e.key === 'Delete' || e.key === 'Backspace'){
          if(selectedObject && selectedObject.type === 'textbox' && selectedObject.isEditing) return;
          deleteSelected();
          e.preventDefault();
        }
      });
    }
    
    function copyObject() {
      if(canvas.getActiveObject()){
        canvas.getActiveObject().clone(cloned => {
          fabric.util.enlivenObjects([cloned], function(objects){
            window.clipboard = objects[0];
          });
        });
      }
    }
    
    function pasteObject() {
      if(window.clipboard){
        window.clipboard.clone(cloned => {
          cloned.set({ left: cloned.left + 10, top: cloned.top + 10 });
          canvas.add(cloned);
          saveState();
        });
      }
    }
    
    function duplicateObject() {
      const activeObject = canvas.getActiveObject();
      if(activeObject){
        activeObject.clone(cloned => {
          cloned.set({ left: cloned.left + 10, top: cloned.top + 10 });
          canvas.add(cloned);
          saveState();
        });
      }
    }
    
    function deleteSelected() {
      const activeObject = canvas.getActiveObject();
      if(activeObject){
        canvas.remove(activeObject);
        saveState();
      }
    }
    
    function zoomIn() {
      currentScale = Math.min(currentScale * 1.1, 3);
      updateZoom();
    }
    
    function zoomOut() {
      currentScale = Math.max(currentScale / 1.1, 0.1);
      updateZoom();
    }
    
    function resetZoom() {
      currentScale = 1;
      updateZoom();
    }
    
    function updateZoom() {
      canvas.setZoom(currentScale);
      document.getElementById('boardScale').value = currentScale;
      document.getElementById('scaleValue').textContent = `${Math.round(currentScale * 100)}%`;
    }
    
    // Add a text element.
    function addTextElement() {
      let text = new fabric.Textbox('Edit text', {
        left: 100,
        top: 100,
        width: 200,
        fontSize: 16,
        fontFamily: 'Arial',
        fill: '#000000',
        hasControls: true,
        splitByGrapheme: true,
        minWidth: 50,
        lockScalingFlip: true
      });
      text.on('scaling', function(){
        let newWidth = this.width * this.scaleX;
        let newHeight = this.height * this.scaleY;
        this.set({ width: newWidth, height: newHeight, scaleX: 1, scaleY: 1 });
        this.initDimensions();
        canvas.requestRenderAll();
      });
      canvas.add(text);
      canvas.setActiveObject(text);
      saveState();
    }
    
    // Toggle text formatting (supports group selections as well).
    function toggleFormat(format) {
      const active = canvas.getActiveObject();
      if(!active) return;
      if(active.type === 'activeSelection' || active.type === 'group'){
        active.getObjects().forEach(obj => {
          if(obj.type === 'textbox'){
            switch(format) {
              case 'bold':
                obj.set('fontWeight', obj.fontWeight ? '' : 'bold');
                break;
              case 'italic':
                obj.set('fontStyle', obj.fontStyle ? '' : 'italic');
                break;
              case 'underline':
                obj.set('underline', !obj.underline);
                break;
            }
          }
        });
      } else if(active.type === 'textbox'){
        switch(format) {
          case 'bold':
            active.set('fontWeight', active.fontWeight ? '' : 'bold');
            break;
          case 'italic':
            active.set('fontStyle', active.fontStyle ? '' : 'italic');
            break;
          case 'underline':
            active.set('underline', !active.underline);
            break;
        }
      }
      canvas.requestRenderAll();
      updateFormatButtons();
    }
    
    // Update text alignment for individual or grouped textboxes.
    function handleTextAlign(e) {
      const active = canvas.getActiveObject();
      if(active){
        if(active.type === 'activeSelection' || active.type === 'group'){
          active.getObjects().forEach(obj => {
            if(obj.type === 'textbox'){
              obj.set('textAlign', e.target.value);
            }
          });
        } else if(active.type === 'textbox'){
          active.set('textAlign', e.target.value);
        }
        canvas.requestRenderAll();
      }
    }
    
    // Update font size for individual or grouped textboxes.
    function handleFontSize(e) {
      const newSize = parseInt(e.target.value);
      const active = canvas.getActiveObject();
      if(active){
        if(active.type === 'activeSelection' || active.type === 'group'){
          active.getObjects().forEach(obj => {
            if(obj.type === 'textbox'){
              obj.set('fontSize', newSize);
              obj.initDimensions();
            }
          });
        } else if(active.type === 'textbox'){
          active.set('fontSize', newSize);
          active.initDimensions();
        }
        canvas.requestRenderAll();
      }
    }
    
    // Update text color for individual or grouped objects.
    function handleTextColor(e) {
      const newColor = e.target.value;
      updateFillColor(newColor);
    }
    
    // Update font family for individual or grouped textboxes.
    function handleFontFamily(e) {
      const active = canvas.getActiveObject();
      if(active){
        if(active.type === 'activeSelection' || active.type === 'group'){
          active.getObjects().forEach(obj => {
            if(obj.type === 'textbox'){
              obj.set('fontFamily', e.target.value);
            }
          });
        } else if(active.type === 'textbox'){
          active.set('fontFamily', e.target.value);
        }
        canvas.requestRenderAll();
      }
    }
    
    // Add a shape element.
    function addShape(type) {
      let color = document.getElementById('shapeColor').value;
      let shape;
      if(type === 'circle'){
        shape = new fabric.Circle({
          radius: 50,
          fill: color,
          left: 100,
          top: 100
        });
      } else {
        shape = new fabric.Rect({
          width: 100,
          height: 100,
          fill: color,
          left: 100,
          top: 100
        });
      }
      canvas.add(shape);
      saveState();
    }
    
    // Update shape color for individual or grouped objects.
    function handleShapeColorChange(e) {
      const newColor = e.target.value;
      updateFillColor(newColor);
    }
    
    function handleImageUpload(e) {
      let file = e.target.files[0];
      let reader = new FileReader();
      reader.onload = function(f){
        fabric.Image.fromURL(f.target.result, function(img){
          img.scaleToWidth(200);
          canvas.add(img);
          saveState();
        });
      };
      reader.readAsDataURL(file);
    }
    
    function exportCanvas() {
      let dataURL = canvas.toDataURL({ format: 'png', multiplier: 2 });
      let link = document.createElement('a');
      link.download = 'board-export.png';
      link.href = dataURL;
      link.click();
    }
    
    function bringForward() {
      const active = canvas.getActiveObject();
      if(active){
        canvas.bringForward(active);
        saveState();
      }
    }
    
    function sendBackward() {
      const active = canvas.getActiveObject();
      if(active){
        canvas.sendBackwards(active);
        panels.forEach(panel => canvas.sendToBack(panel));
        saveState();
      }
    }
    
    function bringToFront() {
      const active = canvas.getActiveObject();
      if(active){
        canvas.bringToFront(active);
        saveState();
      }
    }
    
    function sendToBack() {
      const active = canvas.getActiveObject();
      if(active){
        canvas.sendToBack(active);
        panels.forEach(panel => canvas.sendToBack(panel));
        saveState();
      }
    }
    
    // Grouping/Ungrouping functions.
    function groupSelected() {
      const active = canvas.getActiveObject();
      if (active && active.type === 'activeSelection') {
        active.toGroup();
        canvas.requestRenderAll();
        saveState();
      }
    }
    
    function ungroupSelected() {
      const active = canvas.getActiveObject();
      if (active && active.type === 'group') {
        active.toActiveSelection();
        canvas.requestRenderAll();
        saveState();
      }
    }
    
    // Project tools: Clear canvas, Save JSON, Load JSON.
    function clearCanvas() {
      canvas.getObjects().forEach(obj => {
        if(!obj.isPanel) {
          canvas.remove(obj);
        }
      });
      saveState();
    }
    
    function saveProject() {
      const json = JSON.stringify(canvas.toJSON());
      prompt("Copy your project JSON:", json);
    }
    
    function loadProject() {
      const json = prompt("Paste your project JSON:");
      if(json) {
        canvas.loadFromJSON(json, () => {
          createPanels();
          canvas.renderAll();
          saveState();
        });
      }
    }
    
    document.addEventListener('DOMContentLoaded', initCanvas);
    
    /***********************
     *  3D VIEW SECTION   *
     ***********************/
    let threeScene, threeCamera, threeRenderer, orbitControls;
    
    function cropDataURL(dataURL, x, y, width, height, callback) {
      const image = new Image();
      image.onload = function() {
        const offscreen = document.createElement("canvas");
        offscreen.width = width;
        offscreen.height = height;
        const ctx = offscreen.getContext("2d");
        ctx.drawImage(image, x, y, width, height, 0, 0, width, height);
        callback(offscreen.toDataURL());
      };
      image.src = dataURL;
    }
    
    function enter3DView() {
      document.getElementById('editor-container').style.display = "none";
      document.getElementById('threejs-container').style.display = "block";
      document.getElementById('exit3DBtn').style.display = "block";
      
      const threeContainer = document.getElementById('threejs-container');
      const viewportWidth = threeContainer.clientWidth;
      const viewportHeight = threeContainer.clientHeight;
      const dims = computeBoardDimensions();
      
      const originalStrokes = panels.map(panel => panel.stroke);
      panels.forEach(panel => panel.set('stroke', null));
      canvas.renderAll();
      
      const boardDataURL = canvas.toDataURL();
      
      panels.forEach((panel, i) => panel.set('stroke', originalStrokes[i]));
      canvas.renderAll();
      
      const cropSide = 0;
      const cropVertical = 23;
      const fullWidth = dims.boardWidth;
      const fullHeight = dims.boardHeight;
      const croppedHeight = fullHeight - 2 * cropVertical;
      const leftWidth = defaultSideBoardInches * DPI;
      const middleWidth = defaultMiddleBoardInches * DPI;
      const rightWidth = defaultSideBoardInches * DPI;
      const totalPanelWidth = leftWidth + middleWidth + rightWidth;
      const startX = ((fullWidth - totalPanelWidth) / 2) + cropSide;
      
      function getPanelTextureDataURL(x, width, callback) {
        cropDataURL(boardDataURL, x, cropVertical, width, croppedHeight, callback);
      }
      
      const leftX = startX;
      const centerX = startX + leftWidth;
      const rightX = startX + leftWidth + middleWidth;
      
      getPanelTextureDataURL(leftX, leftWidth - 2 * cropSide, function(leftDataURL) {
        getPanelTextureDataURL(centerX, middleWidth - 2 * cropSide, function(centerDataURL) {
          getPanelTextureDataURL(rightX, rightWidth - 2 * cropSide, function(rightDataURL) {
            const loader = new THREE.TextureLoader();
            const leftTexture = loader.load(leftDataURL);
            const centerTexture = loader.load(centerDataURL);
            const rightTexture = loader.load(rightDataURL);
            
            threeScene = new THREE.Scene();
            threeScene.background = new THREE.Color(0xcccccc);
            
            threeRenderer = new THREE.WebGLRenderer({ antialias: true });
            threeRenderer.setSize(viewportWidth, viewportHeight);
            threeContainer.innerHTML = "";
            threeContainer.appendChild(threeRenderer.domElement);
            
            threeCamera = new THREE.PerspectiveCamera(45, viewportWidth / viewportHeight, 1, 200000);
            const panelHeight3D = dims.boardHeight - 2 * margin;
            const centerPanelY = panelHeight3D / 2;
            const centerTarget = new THREE.Vector3(0, centerPanelY, 0);
            
            threeCamera.position.set(0, centerPanelY, dims.boardHeight * 1.2);
            
            orbitControls = new THREE.OrbitControls(threeCamera, threeRenderer.domElement);
            orbitControls.target.copy(centerTarget);
            orbitControls.update();
            
            const ambientLight = new THREE.AmbientLight(0xffffff, 0.7);
            threeScene.add(ambientLight);
            const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5);
            directionalLight.position.set(0, 1, 1);
            threeScene.add(directionalLight);
            const pointLight = new THREE.PointLight(0xffffff, 0.6);
            pointLight.position.set(0, dims.boardHeight, dims.boardHeight/2);
            threeScene.add(pointLight);
            
            function createMaterialArray(panelTexture) {
              const plainMat = new THREE.MeshPhongMaterial({ color: document.getElementById('boardColor').value || '#ffffff', side: THREE.FrontSide });
              const frontMat = new THREE.MeshPhongMaterial({ map: panelTexture, side: THREE.FrontSide });
              return [plainMat, plainMat, plainMat, plainMat, frontMat, plainMat];
            }
            
            function createPanelMesh(width, panelTexture) {
              const geometry = new THREE.BoxGeometry(width, dims.boardHeight - 2 * margin, 3);
              const materialArray = createMaterialArray(panelTexture);
              return new THREE.Mesh(geometry, materialArray);
            }
            
            const leftMesh = createPanelMesh(leftWidth, leftTexture);
            const centerMesh = createPanelMesh(middleWidth, centerTexture);
            const rightMesh = createPanelMesh(rightWidth, rightTexture);
            
            const totalWidth = leftWidth + middleWidth + rightWidth;
            const startX3D = -totalWidth / 2;
            const panelHeight3DAdjusted = dims.boardHeight - 2 * margin;
            
            centerMesh.position.set(startX3D + leftWidth + middleWidth/2, panelHeight3DAdjusted/2, 0);
            
            leftMesh.geometry.translate(-leftWidth/2, 0, 0);
            leftMesh.position.set(startX3D + leftWidth, panelHeight3DAdjusted/2, 0);
            leftMesh.rotation.y = THREE.Math.degToRad(45);
            
            rightMesh.geometry.translate(rightWidth/2, 0, 0);
            rightMesh.position.set(startX3D + leftWidth + middleWidth, panelHeight3DAdjusted/2, 0);
            rightMesh.rotation.y = THREE.Math.degToRad(-45);
            
            threeScene.add(leftMesh);
            threeScene.add(centerMesh);
            threeScene.add(rightMesh);
            
            function animate() {
              requestAnimationFrame(animate);
              orbitControls.update();
              threeRenderer.render(threeScene, threeCamera);
            }
            animate();
          });
        });
      });
    }
    
    function exit3DView() {
      document.getElementById('threejs-container').style.display = "none";
      document.getElementById('exit3DBtn').style.display = "none";
      document.getElementById('editor-container').style.display = "block";
    }
  </script>
</body>
</html>
