見出し画像

自作レバーレスのあれこれ [レイアウト編]

はじめに

「レバーレスは、自由なのです!ノれるのです!カッコいいのです!!ギュイーンとソウルがシャウトするのです!!」

※注意

レバーレスアケコンは自作するよりも買ったほうがお手軽かつコスト面でも優れています
自作はオススメしません
自作自己満足自己責任です


レイアウトについて

自作の最大の利点は自分の好きなようにボタンをレイアウトできること
奇妙奇天烈なスタイルも思いのまま

使用ツール

レイアウトに使用するツールは以下の3つ
誰でもどこでも無料

  • Google スライド

    • ボタン配置のデザインに使用する

    • そのままA4用紙に印刷して穴あけガイドにする

  • Google スプレッドシート

    • スライドの各ボタンの座標等を設定する

    • GP2040-CE編にて説明するConfigファイルのボタンレイアウトを設定する

  • Google Apps Script

    • スプレッドシートにて設定した値をもとにスライドにボタンオブジェクトを自動配置する

    • Configファイルのボタンレイアウトをlogとして出力する

完成イメージ

スライド、スプレッドシート、GASを使用すれば自分好みのボタン配置が数秒で完成します

ノーマルスタイル
スプリットスタイル

手順


1.Google スライド

1.新しいプレゼンテーションを作成から空白のプレゼンテーションを選択する

2.ファイル > ページ設定から A4用紙サイズ(29.7cm x 21.0 cm)に設定する

3.レイアウトを空白に設定する

4.挿入 > 図形 > ドーナツを選択する

5.ドーナツを挿入し、内径を極力小さくしたものをボタン数だけコピペする
(サンプルの場合はディスプレイを含め19個)

6.ドーナツ同様に長方形を挿入する

スライドでの作業はここで一旦終了


2.Google スプレッドシート

1.新しいスプレッドシートを作成から空白のスプレッドシートを選択する

2.項目を以下のように設定する

  • No.

    • 各ボタンの通し番号

  • BTN_NAME

    • ボタン名

    • ラベルとして使用

  • XINPUT_NAME

    • XInputで使用するボタン名

  • GPIO_PIN

    • ボタンが接続されているGPIOピンの番号

  • X_POS

    • スライドのボタンX座標

    • REF_SIZE * X_OFF

  • Y_POS

    • スライドのボタンY座標

    • REF_SIZE * Y_OFF

  • REF_SIZE

    • ボタンの基準サイズ

  • X_OFF

    • REF_SIZE * X_OFF でスライドのX座標

  • Y_OFF

    • REF_SIZE * Y_OFF でスライドのY座標

  • COLOR

    • スライドのボタンの色

  • CFG_ATTR

    • ボタンの設定属性

    • DIR = 方向キー, BTN = ボタン, DSP = ディスプレイ

  • CFG_SIZE

    • Configファイルのボタンレイアウトサイズ

  • CFG_AB

    • Configファイルのボタン全体レイアウト

    • A = 左, B = 右

  • CFG_REF_SIZE

    • Configファイルのボタン基準サイズ

  • CFG_X

    • ConfigファイルのボタンX座標

    • X_OFF * 2 * CFG_REF_SIZE + CFG_REF_SIZE

  • CFG_Y

    • ConfigファイルのボタンY座標

    • Y_OFF * 2 * CFG_REF_SIZE + CFG_REF_SIZE

3.アドレスバーのID部分をメモっておく

スプレッドシートでの作業はここで一旦終了


3.Google Apps Script

1.さきほど作成したスライドに戻る

2.拡張機能 > Apps Script を選択する

3.無題のプロジェクトが開いたら以下のソースをコピペする

※ 以下の【fileId】に先ほどメモったスプレッドシートのIDを入れる ※

// スプレッドシートからデータを取得する
function getSpreadsheetData_() {
  // ※※※※※※ 作成するボタン配置によってIDを変える ※※※※※
  const fileId = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
  const sheet = SpreadsheetApp.openById(fileId).getSheets()[0];
  return sheet.getDataRange().getValues();
}

ソース本体

function ButtonSetup() {
  initializeButtonSetup_();
}

// スライド上のShapeの座標をログに出力する
function logShapeCoordinates() {
  const presentation = SlidesApp.getActivePresentation();
  const slide = presentation.getSlides()[0];
  const data = getSpreadsheetData_();
  const config = initializeConfig_();

  slide.getShapes().forEach(shape => {
    const shapeName = shape.getDescription();
    const elementType = getCellValue_(data, shapeName, "CFG_ATTR");

    if (["DIR", "BTN"].includes(elementType)) {
      const tmpConfig = generateShapeConfig_(shape, shapeName, data);
      if (getCellValue_(data, shapeName, "CFG_AB") === "A") {
        config.tmpA += tmpConfig;
      } else {
        config.tmpB += tmpConfig;
      }
    }
  });

  finalizeConfigStrings_(config);
  logConfig_(config);
}

// ShapeのX座標とY座標を使って、座標を含むtmpConfig文字列を生成する
function generateShapeConfig_(shape, shapeName, data) {
  const currentX = pointsToCm_(shape.getLeft());
  const currentY = pointsToCm_(shape.getTop());
  const refSize = getCellValue_(data, shapeName, "REF_SIZE");
  const cfgRefSize = getCellValue_(data, shapeName, "CFG_REF_SIZE");

  const cfgX = (currentX / refSize) * 2 * cfgRefSize + cfgRefSize;
  const cfgY = (currentY / refSize) * 2 * cfgRefSize + cfgRefSize;

  return generateTempConfigWithCoordinates_(shapeName, data, Math.round(cfgX), Math.round(Math.round(cfgY)));
}

// pointsをcmに変換する
function pointsToCm_(points) {
  return points / (72 / 2.54);
}

// 座標を含むtmpConfig文字列を生成する
function generateTempConfigWithCoordinates_(shapeName, data, confX, confY) {
  const elementType = getCellValue_(data, shapeName, "CFG_ATTR");
  const size = getCellValue_(data, shapeName, "CFG_SIZE");
  const mask = `GAMEPAD_MASK_${shapeName}`;

  return `      {GP_ELEMENT_${elementType}_BUTTON, {${confX}, ${confY}, ${size}, ${size}, 1, 1, ${mask}, GP_SHAPE_ELLIPSE}},  \\ \n`;
}

// スライドのボタン位置と設定を初期化する
function initializeButtonSetup_() {
  const presentation = SlidesApp.getActivePresentation();
  const slide = presentation.getSlides()[0];
  const data = getSpreadsheetData_();
  const buttonNames = extractButtonNames_(data);
  const config = initializeConfig_();

  ungroupSlideObjects_(slide);
  assignButtonNamesToShapes_(slide, buttonNames);
  processSlideShapes_(slide, buttonNames, data, config);
  finalizeConfigStrings_(config);
  groupAllSlideObjects_(slide);

  logConfig_(config);
}

// スプレッドシートからデータを取得する
function getSpreadsheetData_() {
  // ※※※※※※ 作成するボタン配置によってIDを変える ※※※※※
  const fileId = 'XXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX';
  const sheet = SpreadsheetApp.openById(fileId).getSheets()[0];
  return sheet.getDataRange().getValues();
}

// Configを初期化する
function initializeConfig_() {
  return {
    tmpA: "#define DEFAULT_BOARD_LAYOUT_A  \\\n  {  \\\n",
    tmpB: "#define DEFAULT_BOARD_LAYOUT_B  \\\n  {  \\\n"
  };
}

// Config文字列を最終化する
function finalizeConfigStrings_(config) {
  config.tmpA += "  }  \n";
  config.tmpB += "  }  \n";
}

// データからボタン名を抽出する
function extractButtonNames_(data) {
  const headers = data[0];
  return data.slice(1).map(row => row[headers.indexOf("BTN_NAME")]);
}

// スライド上のボタンに名前を割り当てる
function assignButtonNamesToShapes_(slide, buttonNames) {
  let nameIndex = 0;

  slide.getShapes().forEach(shape => {
    const shapeType = shape.getShapeType();
    if (shapeType === SlidesApp.ShapeType.RECTANGLE) {
      shape.setDescription("Frame");
    } else if (shapeType === SlidesApp.ShapeType.DONUT && nameIndex < buttonNames.length) {
      shape.setDescription(buttonNames[nameIndex]);
      nameIndex++;
    }
  });
}

// スライド上のオブジェクトのグループ化を解除する
function ungroupSlideObjects_(slide) {
  slide.getPageElements().forEach(pageElement => {
    if (pageElement.getPageElementType() === SlidesApp.PageElementType.GROUP) {
      pageElement.asGroup().ungroup();
    }
  });
}

// スライド上のShapeを処理する
function processSlideShapes_(slide, buttonNames, data, config) {
  slide.getShapes().forEach(shape => {
    processShape_(shape, buttonNames, data, config);
  });
}

// Shapeを処理する
function processShape_(shape, buttonNames, data, config) {
  const shapeName = shape.getDescription();
  if (buttonNames.includes(shapeName)) {
    configureButtonShape_(shape, shapeName, data, config);
  } else if (shapeName === "Frame") {
    adjustFrameShape_(shape, data);
  }
  highlightShapeBorder_(shape);
}

// ボタンを設定する
function configureButtonShape_(shape, shapeName, data, config) {
  const elementType = getCellValue_(data, shapeName, "CFG_ATTR");
  configureShapeDimensions_(shape, data, shapeName);
  configureShapeText_(shape, data, shapeName);

  if (["DIR", "BTN"].includes(elementType)) {
    const tmpConfig = generateTempConfig_(shapeName, data);
    if (getCellValue_(data, shapeName, "CFG_AB") === "A") {
      config.tmpA += tmpConfig;
    } else {
      config.tmpB += tmpConfig;
    }
  }
}

// Shapeのサイズと座標を設定する
function configureShapeDimensions_(shape, data, shapeName) {
  shape
    .setLeft(cmToPoints_(getCellValue_(data, shapeName, "X_POS")))
    .setTop(cmToPoints_(getCellValue_(data, shapeName, "Y_POS")))
    .setWidth(cmToPoints_(getCellValue_(data, shapeName, "REF_SIZE")))
    .setHeight(cmToPoints_(getCellValue_(data, shapeName, "REF_SIZE")))
    .getFill().setSolidFill(getCellValue_(data, shapeName, "COLOR"));
}

// Shapeのテキストを設定する
function configureShapeText_(shape, data, shapeName) {
  const textContent = `${getCellValue_(data, shapeName, "BTN_NAME")}\n${getCellValue_(data, shapeName, "GPIO_PIN")}`;
  shape.getText()
    .setText(textContent)
    .getTextStyle()
    .setFontSize(10)
    .setForegroundColor("#000000");
}

// Frameのサイズと位置を調整する
function adjustFrameShape_(shape, data) {
  shape
    .setLeft(0)
    .setTop(0)
    .setWidth(cmToPoints_(getCellValue_(data, "MAX", "X_POS")) + cmToPoints_(getCellValue_(data, "MAX", "REF_SIZE")))
    .setHeight(cmToPoints_(getCellValue_(data, "MAX", "Y_POS")) + cmToPoints_(getCellValue_(data, "MAX", "REF_SIZE")))
    .getFill().setTransparent();
}

// Shapeの枠線を赤にする
function highlightShapeBorder_(shape) {
  shape.getBorder().getLineFill().setSolidFill("#FF0000");
}

// Config用の文字列を生成する
function generateTempConfig_(shapeName, data) {
  const elementType = getCellValue_(data, shapeName, "CFG_ATTR");
  const confX = getCellValue_(data, shapeName, "CFG_X");
  const confY = getCellValue_(data, shapeName, "CFG_Y");
  const size = getCellValue_(data, shapeName, "CFG_SIZE");
  const mask = `GAMEPAD_MASK_${shapeName}`;

  return `      {GP_ELEMENT_${elementType}_BUTTON, {${confX}, ${confY}, ${size}, ${size}, 1, 1, ${mask}, GP_SHAPE_ELLIPSE}},  \\\n`;
}

// スライド上のすべてのオブジェクトをグループ化する
function groupAllSlideObjects_(slide) {
  const pageElements = slide.getPageElements();
  if (pageElements.length > 1) {
    const group = slide.group(pageElements);
    centerObjectOnSlide_(group);
  }
}

// オブジェクトをスライドの中央に配置する
function centerObjectOnSlide_(object) {
  const presentation = SlidesApp.getActivePresentation();
  const slideWidth = presentation.getPageWidth();
  const slideHeight = presentation.getPageHeight();
  const newX = (slideWidth - object.getWidth()) / 2;
  const newY = (slideHeight - object.getHeight()) / 2;

  object.setLeft(newX);
  object.setTop(newY);
}

// Configをログ出力する
function logConfig_(config) {
  Logger.log(config.tmpA);
  Logger.log(config.tmpB);
}

// セルの値を取得する
function getCellValue_(data, button, header) {
  const headers = data[0];
  const rows = data.slice(1);
  const columnIndex = headers.indexOf(header);

  if (columnIndex === -1) return 'Header not found.';

  return button === "MAX" ? getMaxValue_(rows, columnIndex) : getValueByButton_(rows, columnIndex, button);
}

// ヘッダー列の最大値を取得する
function getMaxValue_(rows, columnIndex) {
  const numericValues = rows.map(row => parseFloat(row[columnIndex])).filter(val => !isNaN(val));
  return numericValues.length > 0 ? Math.max(...numericValues) : 'No numeric values found in column.';
}

// 指定された値を含む行からセルの値を取得する
function getValueByButton_(rows, columnIndex, button) {
  const row = rows.find(row => row.includes(button));
  return row ? row[columnIndex] : 'Value not found.';
}

// cmをpointに変換する
function cmToPoints_(cm) {
  return cm * (72 / 2.54);
}

4.プルダウンをButtonSetupにし、▶実行をクリック

5.スライドに戻ると・・・あら不思議!!


enjoy!!


この記事が気に入ったらサポートをしてみませんか?