junkerstock
 vrz152 

<!DOCTYPE html>
<html>
<head>
<title>VRテスト環境 Ver14.8 背景機能強化版</title>
<script src="https://aframe.io/releases/1.7.0/aframe.min.js"></script>
<script src="https://cdn.jsdelivr.net/gh/MozillaReality/ammo.js@8bbc0ea/builds/ammo.wasm.js"></script>
<script src="./js2/aframe-physics-system.js"></script>
<script src="https://unpkg.com/aframe-look-at-component@0.8.0/dist/aframe-look-at-component.min.js"></script>
<script src="https://unpkg.com/aframe-troika-text/dist/aframe-troika-text.min.js"></script>
<script>

// ▼▼▼ グローバル変数 ▼▼▼
window.selectedGridButtonId = null;
window.availableTags = []; // haikei-tag.txtから読み込んだタグ
window.currentBackgroundIndex = 400; // 初期値
window.maxBackgroundCount = 1500; // 背景画像の最大数(gemini_count_no.txtから更新される)

// ▼▼▼ 背景の上限数を読み込む関数 (新規追加) ▼▼▼
window.fetchMaxCount = function() {
fetch('./gemini_count_no.txt')
.then(response => {
if (!response.ok) throw new Error("Count file not found");
return response.text();
})
.then(text => {
const val = parseInt(text.trim(), 10);
if (!isNaN(val)) {
window.maxBackgroundCount = val;
console.log("背景最大数を設定しました:", window.maxBackgroundCount);
}
})
.catch(err => {
console.log("背景最大数の読み込みに失敗、デフォルト値(1500)を使用します。", err);
});
};

// ▼▼▼ グリッドのデータを保存する関数 (LocalStorage) ▼▼▼
window.saveGridData = function() {
const container = document.getElementById('bg-grid-container');
if (!container) return;

const gridData = {};
const buttons = container.querySelectorAll('a-box');

buttons.forEach(btn => {
if (btn.dataset.bgIndex && btn.dataset.bgIndex !== "") {
// IDがグリッド由来のものだけ保存(タグボタンなどを除外)
if(btn.id.startsWith('bg-grid-')) {
gridData[btn.id] = btn.dataset.bgIndex;
}
}
});

localStorage.setItem('bgGridData', JSON.stringify(gridData));
console.log("グリッドデータを保存しました");
};

// ▼▼▼ 現在の背景番号を保存する関数 (LocalStorage) ▼▼▼
window.saveCurrentBgIndex = function(index) {
localStorage.setItem('lastBgIndex', index.toString());
};

// ▼▼▼ タグデータを保存する関数 (LocalStorage) ▼▼▼
// 形式: { "19": ["城", "谷"], "25": ["美しい"] }
window.saveTagData = function(index, tags) {
const raw = localStorage.getItem('bgTagData');
const data = raw ? JSON.parse(raw) : {};

if (tags.length === 0) {
delete data[index];
} else {
data[index] = tags;
}

localStorage.setItem('bgTagData', JSON.stringify(data));
};

// ▼▼▼ タグデータを読み込む関数 ▼▼▼
window.loadTagData = function(index) {
const raw = localStorage.getItem('bgTagData');
if (!raw) return [];
const data = JSON.parse(raw);
return data[index] || [];
};

// ▼▼▼ haikei-tag.txt を読み込む関数 ▼▼▼
window.fetchTags = function() {
fetch('./haikei-tag.txt')
.then(response => {
if (!response.ok) throw new Error("Tag file not found");
return response.text();
})
.then(text => {
// カンマ区切りで配列化し、空白を除去
window.availableTags = text.split(',')
.map(t => t.trim())
.filter(t => t !== "");
console.log("読み込んだタグ:", window.availableTags);
// タグボタンを生成
window.createTagButtons();
})
.catch(err => {
console.log("タグファイルの読み込みに失敗、またはファイルがありません。", err);
window.availableTags = [];
});
};

// ▼▼▼ タグボタンを生成する関数 (修正版: 20個で折り返し対応) ▼▼▼
window.createTagButtons = function() {
const container = document.getElementById('bg-mode-group');
if (!container) return;

// 既に作成済みのタグボタンがあれば削除(リロード時など)
const oldButtons = container.querySelectorAll('.tag-button-group');
oldButtons.forEach(el => el.parentNode.removeChild(el));

// ボタン配置の設定
const startX = 0.23; // 初期X (変更)
const startY = 0.17; // 初期Y (変更)
const gapY = 0.018; // 縦の間隔
const gapX = 0.08; // 横の折り返し間隔 (変更)
const maxPerColumn = 20; // 1列あたりの最大数 (変更)

window.availableTags.forEach((tag, i) => {
// 列番号と行番号を計算
const columnIndex = Math.floor(i / maxPerColumn); // 0, 1, 2...
const rowIndex = i % maxPerColumn; // 0~19

// 座標を決定 (列数分だけXをずらし、行数分だけYを下げる)
const x = startX + (columnIndex * gapX);
const y = startY - (rowIndex * gapY);

// ボタン本体
const btn = document.createElement('a-box');
const btnId = `bg-tag-btn-${i}`;
btn.setAttribute('id', btnId);
btn.classList.add('clickable', 'tag-button-group');
btn.setAttribute('width', 0.06);
btn.setAttribute('height', 0.015);
btn.setAttribute('depth', 0.01);
btn.setAttribute('position', `${x} ${y} 0`);
btn.setAttribute('material', 'color: #b0c4de; shader: flat'); // デフォルトはグレー
btn.dataset.tagName = tag; // タグ名をデータ属性に保持

// テキスト
const textEl = document.createElement('a-entity');
textEl.setAttribute('troika-text', {
value: tag,
color: 'white',
fontSize: 0.01,
align: 'center',
anchor: 'center'
});
textEl.setAttribute('position', '0 0 0.006');
btn.appendChild(textEl);

container.appendChild(btn);
});
};

// ▼▼▼ タグボタンの表示状態を更新する関数(整合性チェック含む) ▼▼▼
window.updateTagButtonsState = function(bgIndex) {
const container = document.getElementById('bg-mode-group');
if (!container) return;

// LocalStorageからこの背景のタグを取得
let savedTags = window.loadTagData(bgIndex);

// 【重要】txtに無いタグがLSに残っていたら削除する処理
const validTags = savedTags.filter(t => window.availableTags.includes(t));

// フィルタリング前後で数が減っていたら、無効なタグがあったということなのでLSを更新
if (validTags.length !== savedTags.length) {
console.log(`背景 ${bgIndex} の無効なタグを削除しました`);
window.saveTagData(bgIndex, validTags);
savedTags = validTags;
}

// ボタンの色を反映
const buttons = container.querySelectorAll('.tag-button-group');
buttons.forEach(btn => {
const tagName = btn.dataset.tagName;
if (savedTags.includes(tagName)) {
// 選択状態:黄色
btn.setAttribute('material', 'color', '#f1c40f');
} else {
// 非選択:グレー
btn.setAttribute('material', 'color', '#b0c4de');
}
});
};

// ▼▼▼ タグボタンクリック時の処理 ▼▼▼
window.handleTagClick = function(btnElement) {
const tagName = btnElement.dataset.tagName;
if (!tagName) return;

const bgIndex = window.currentBackgroundIndex;
let savedTags = window.loadTagData(bgIndex);

if (savedTags.includes(tagName)) {
// 既に選択済みなら削除(OFFにする)
savedTags = savedTags.filter(t => t !== tagName);
btnElement.setAttribute('material', 'color', '#b0c4de');
} else {
// 未選択なら追加(ONにする)
savedTags.push(tagName);
btnElement.setAttribute('material', 'color', '#f1c40f');
}

// 保存
window.saveTagData(bgIndex, savedTags);
console.log(`背景 ${bgIndex} のタグ更新:`, savedTags);
};


// ▼▼▼ 地面表示切り替え関数 ▼▼▼
window.toggleGroundWithText = function() {
const groundEl = document.getElementById('ground');
const textEl = document.getElementById('toggle-ground-text-vr');
if (!textEl) return;

const currentAttr = textEl.getAttribute('troika-text');
const currentText = currentAttr ? currentAttr.value : "";
let turnOn = (currentText === '地面OFF');

if (groundEl) { groundEl.setAttribute('visible', turnOn); }

textEl.setAttribute('troika-text', { ...currentAttr, value: turnOn ? '地面ON' : '地面OFF' });
};

// ★★★ 背景選択モードへの切り替え関数(表示更新追加) ★★★
window.enterBackgroundMode = function() {
const menuGroup = document.getElementById('main-menu-group');
const bgModeGroup = document.getElementById('bg-mode-group');

if (menuGroup) {
menuGroup.setAttribute('position', "0 0 -1000");
menuGroup.setAttribute('visible', false);
}

if (bgModeGroup) {
bgModeGroup.setAttribute('position', "0 0 0.01");
bgModeGroup.setAttribute('visible', true);
}

// モード移行時に、現在の背景番号を更新し、タグの状態も更新
window.updateBackgroundDisplay(window.currentBackgroundIndex);
window.updateTagButtonsState(window.currentBackgroundIndex);

console.log("背景選択モードへ移行しました");
};

// ★★★ 通常モードへの復帰関数 ★★★
window.exitBackgroundMode = function() {
const menuGroup = document.getElementById('main-menu-group');
const bgModeGroup = document.getElementById('bg-mode-group');

if (menuGroup) {
menuGroup.setAttribute('position', "0 0 0");
menuGroup.setAttribute('visible', true);
}

if (bgModeGroup) {
bgModeGroup.setAttribute('position', "0 0 -2000");
bgModeGroup.setAttribute('visible', false);
}
console.log("通常モードへ復帰しました");
};

// ★★★ 9x10のグリッドボタンを生成する関数 ★★★
window.createBackgroundGrid = function() {
const container = document.getElementById('bg-grid-container');
if (!container) return;

const savedDataString = localStorage.getItem('bgGridData');
const savedData = savedDataString ? JSON.parse(savedDataString) : {};

const rows = 10;
const cols = 9;
const size = 0.025;
const gap = 0.005;

// ★グリッド位置微調整
const startX = -0.16;
const startY = 0.12;

for (let r = 0; r < rows; r++) {
for (let c = 0; c < cols; c++) {
const btn = document.createElement('a-box');
const id = `bg-grid-${r}-${c}`;
btn.setAttribute('id', id);
btn.classList.add('clickable');
btn.setAttribute('width', size);
btn.setAttribute('height', size);
btn.setAttribute('depth', 0.01);
btn.setAttribute('material', 'color: #7f8c8d; shader: flat');

let savedValue = "";
if (savedData[id]) {
savedValue = savedData[id];
}
btn.dataset.bgIndex = savedValue;

const x = startX + (c * (size + gap));
const y = startY - (r * (size + gap));
btn.setAttribute('position', `${x} ${y} 0`);

const textEl = document.createElement('a-entity');
textEl.setAttribute('id', `${id}-text`);
textEl.setAttribute('troika-text', {
value: savedValue,
color: 'white',
fontSize: 0.01,
align: 'center',
anchor: 'center'
});
textEl.setAttribute('position', '0 0 0.011');
btn.appendChild(textEl);

container.appendChild(btn);
}
}
};

// ★★★ グリッドボタン選択時の処理 ★★★
window.handleGridSelection = function(clickedId) {
if (window.selectedGridButtonId) {
const prevBtn = document.getElementById(window.selectedGridButtonId);
if (prevBtn) {
prevBtn.setAttribute('material', 'color', '#7f8c8d');
}
}
const clickedBtn = document.getElementById(clickedId);
if (clickedBtn) {
clickedBtn.setAttribute('material', 'color', '#e74c3c');
window.selectedGridButtonId = clickedId;
}
};

// ★★★ 記録ボタンの処理 ★★★
window.recordBackgroundNumber = function(currentBgIndex) {
if (!window.selectedGridButtonId) {
console.log("ボタンが選択されていません");
return;
}

const btn = document.getElementById(window.selectedGridButtonId);
if (btn) {
btn.dataset.bgIndex = currentBgIndex;
}

const textId = `${window.selectedGridButtonId}-text`;
const textEl = document.getElementById(textId);
if (textEl) {
textEl.setAttribute('troika-text', 'value', currentBgIndex.toString());
}
console.log(`ボタン ${window.selectedGridButtonId} に背景番号 ${currentBgIndex} を記録しました`);

window.saveGridData();
};

// ★★★ 移動ボタンの処理 ★★★
window.jumpToRecordedBackground = function() {
if (!window.selectedGridButtonId) {
console.log("ボタンが選択されていません");
return;
}

const btn = document.getElementById(window.selectedGridButtonId);
if (btn && btn.dataset.bgIndex && btn.dataset.bgIndex !== "") {
const targetIndex = parseInt(btn.dataset.bgIndex, 10);
console.log(`記録された背景番号 ${targetIndex} へ移動します`);

// 刷新されたサーチロジックで移動
window.searchAndApplyImage(targetIndex, 1);
} else {
console.log("このボタンには数値が記録されていません");
}
};

// ★★★ 消去ボタンの処理 ★★★
window.clearRecordedNumber = function() {
if (!window.selectedGridButtonId) {
console.log("ボタンが選択されていません");
return;
}

const btn = document.getElementById(window.selectedGridButtonId);
if (btn) {
btn.dataset.bgIndex = "";
}

const textId = `${window.selectedGridButtonId}-text`;
const textEl = document.getElementById(textId);
if (textEl) {
textEl.setAttribute('troika-text', 'value', "");
}
console.log(`ボタン ${window.selectedGridButtonId} の記録を消去しました`);

window.saveGridData();
};


// ★★★ サーバーへテキスト保存する関数 ★★★
window.saveGridToTextFile = function() {
const container = document.getElementById('bg-grid-container');
if (!container) return;

const rows = 10;
const cols = 9;
let outputText = "";

// グリッドデータを走査してテキスト化
for (let r = 0; r < rows; r++) {
let rowValues = [];
for (let c = 0; c < cols; c++) {
const id = `bg-grid-${r}-${c}`;
const btn = document.getElementById(id);
let val = "0"; // デフォルトは0

if (btn && btn.dataset.bgIndex && btn.dataset.bgIndex !== "") {
val = btn.dataset.bgIndex;
}
rowValues.push(val);
}
// スペース区切りで結合し、改行を追加
outputText += rowValues.join(" ") + "\n";
}

console.log("送信データ:\n" + outputText);

// テキスト表示を一時的に「送信中」にする
const textEl = document.getElementById('bg-text-save-text');
if (textEl) textEl.setAttribute('troika-text', 'value', '送信中...');

// CGIへPOST送信
const formData = new FormData();
formData.append('gridData', outputText);

fetch('./haikei-save.cgi', {
method: 'POST',
body: formData
})
.then(response => response.json())
.then(data => {
console.log('Server response:', data);
if (textEl) {
if (data.status === 'success') {
textEl.setAttribute('troika-text', 'value', '完了!');
} else {
textEl.setAttribute('troika-text', 'value', 'エラー');
}
// 2秒後に元の表示に戻す
setTimeout(() => {
textEl.setAttribute('troika-text', 'value', 'TXT保存');
}, 2000);
}
})
.catch(error => {
console.error('Error:', error);
if (textEl) textEl.setAttribute('troika-text', 'value', '通信失敗');
});
};


// ▼▼▼ 全消去ボタンの処理関数 ▼▼▼
window.clearAllRecordedNumbers = function() {
const container = document.getElementById('bg-grid-container');
if (!container) return;

// グリッド内のすべてのボタンを走査してクリア
const buttons = container.querySelectorAll('a-box');
buttons.forEach(btn => {
// グリッドボタンのみ対象(タグボタンなどはIDで判別)
if (btn.id.startsWith('bg-grid-')) {
btn.dataset.bgIndex = "";
const textId = `${btn.id}-text`;
const textEl = document.getElementById(textId);
if (textEl) {
textEl.setAttribute('troika-text', 'value', "");
}
}
});

console.log("全ての記録を消去しました");
window.saveGridData();
};


// --- Player Controls & Physics Components (そのまま) ---
AFRAME.registerComponent('camera-relative-controls', {
schema: {
targetSpeed: { type: 'number', default: 2 },
acceleration: { type: 'number', default: 3 },
damping: { type: 'number', default: 8 },
brakingDeceleration: { type: 'number', default: 20 },
enabled: { type: 'boolean', default: true },
rotationSpeed: { type: 'number', default: 1.5 },
pitchLimit: { type: 'number', default: 85 },
verticalSpeed: { type: 'number', default: 2 },
groundY: { type: 'number', default: 0 },
ceilingY: { type: 'number', default: 50 },
gravity: { type: 'number', default: 0 }
},
init: function () {
this.keys = {};
this.leftThumbstickInput = { x: 0, y: 0 };
this.rightThumbstickInput = { x: 0, y: 0 };
this.currentVelocity = new THREE.Vector3();
this.desiredVelocity = new THREE.Vector3();
this.cameraDirection = new THREE.Vector3();
this.cameraRight = new THREE.Vector3();
this.moveDirection = new THREE.Vector3();
this.cameraWorldQuaternion = new THREE.Quaternion();
this.rigEl = this.el;
this.cameraEl = this.el.querySelector('[camera]');
this.isReady = false;

if (!this.cameraEl) { console.error('camera-relative-controls: カメラエンティティが見つかりません。'); }

this.el.sceneEl.addEventListener('loaded', () => {
this.leftHand = document.getElementById('leftHand');
if (this.leftHand) { this.leftHand.addEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); }
this.rightHand = document.getElementById('rightHand');
if (this.rightHand) { this.rightHand.addEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); }
});

this.onKeyDown = this.onKeyDown.bind(this);
this.onKeyUp = this.onKeyUp.bind(this);
window.addEventListener('keydown', this.onKeyDown);
window.addEventListener('keyup', this.onKeyUp);
},
remove: function () {
window.removeEventListener('keydown', this.onKeyDown);
window.removeEventListener('keyup', this.onKeyUp);
if (this.leftHand) { try { this.leftHand.removeEventListener('thumbstickmoved', this.onLeftThumbstickMoved.bind(this)); } catch(e){} }
if (this.rightHand) { try { this.rightHand.removeEventListener('thumbstickmoved', this.onRightThumbstickMoved.bind(this)); } catch(e){} }
},
onLeftThumbstickMoved: function (evt) { this.leftThumbstickInput.x = evt.detail.x; this.leftThumbstickInput.y = evt.detail.y; },
onRightThumbstickMoved: function (evt) { this.rightThumbstickInput.x = evt.detail.x; this.rightThumbstickInput.y = evt.detail.y; },
tick: function (time, timeDelta) {
if (!this.data.enabled) return;
if (!this.isReady) {
if (this.cameraEl && this.cameraEl.object3D && this.cameraEl.object3D.matrixWorld) { this.isReady = true; }
else { return; }
}
if (!this.cameraEl || !this.cameraEl.object3D || !this.rigEl || !this.rigEl.object3D) { return; }

const data = this.data;
const dt = timeDelta / 1000;
const position = this.rigEl.object3D.position;

if (this.rigEl.sceneEl.is('vr-mode') || this.rigEl.sceneEl.is('ar-mode')) {
if (Math.abs(this.rightThumbstickInput.x) > 0.1) {
const preRotPos = new THREE.Vector3();
this.cameraEl.object3D.getWorldPosition(preRotPos);

this.rigEl.object3D.rotation.y += -this.rightThumbstickInput.x * data.rotationSpeed * dt;

this.rigEl.object3D.updateMatrixWorld(true);
this.cameraEl.object3D.updateMatrixWorld(true);

const postRotPos = new THREE.Vector3();
this.cameraEl.object3D.getWorldPosition(postRotPos);

const diff = postRotPos.sub(preRotPos);
diff.y = 0;
this.rigEl.object3D.position.sub(diff);
}

if (Math.abs(this.rightThumbstickInput.y) > 0.1) {
const verticalMovement = this.rightThumbstickInput.y * data.verticalSpeed * dt;
this.rigEl.object3D.position.y -= verticalMovement;
}
}

const cameraObject = this.cameraEl.object3D;
cameraObject.getWorldQuaternion(this.cameraWorldQuaternion);
this.cameraDirection.set(0, 0, -1).applyQuaternion(this.cameraWorldQuaternion); this.cameraDirection.normalize();
this.cameraRight.set(1, 0, 0).applyQuaternion(this.cameraWorldQuaternion); this.cameraRight.y = 0; this.cameraRight.normalize();
this.moveDirection.set(0, 0, 0);

if (this.keys['KeyW'] || this.keys['ArrowUp']) { this.moveDirection.add(this.cameraDirection); }
if (this.keys['KeyS'] || this.keys['ArrowDown']) { this.moveDirection.sub(this.cameraDirection); }
if (this.keys['KeyA'] || this.keys['ArrowLeft']) { this.moveDirection.sub(this.cameraRight); }
if (this.keys['KeyD'] || this.keys['ArrowRight']) { this.moveDirection.add(this.cameraRight); }
if (Math.abs(this.leftThumbstickInput.y) > 0.1) { this.moveDirection.add(this.cameraDirection.clone().multiplyScalar(-this.leftThumbstickInput.y)); }
if (Math.abs(this.leftThumbstickInput.x) > 0.1) { this.moveDirection.add(this.cameraRight.clone().multiplyScalar(this.leftThumbstickInput.x)); }

const isInputting = this.moveDirection.lengthSq() > 0.0001;
let lerpFactor = data.damping;
if (isInputting) {
if (this.currentVelocity.dot(this.moveDirection) < -0.1) { this.desiredVelocity.set(0,0,0); lerpFactor = data.brakingDeceleration; }
else { this.desiredVelocity.copy(this.moveDirection).multiplyScalar(data.targetSpeed); lerpFactor = data.acceleration; }
} else { this.desiredVelocity.set(0,0,0); lerpFactor = data.damping; }

const effectiveLerpFactor = 1.0 - Math.exp(-lerpFactor * dt);
this.currentVelocity.lerp(this.desiredVelocity, effectiveLerpFactor);
if (this.currentVelocity.lengthSq() < 0.0001) { this.currentVelocity.set(0,0,0); }

if (this.currentVelocity.lengthSq() > 0) {
const deltaPosition = this.currentVelocity.clone().multiplyScalar(dt);
position.add(deltaPosition);
}

let newY = position.y;
if (this.keys['KeyE']) { newY += data.verticalSpeed * dt; }
if (this.keys['KeyC']) { newY -= data.verticalSpeed * dt; }

if (position.y > data.groundY && data.gravity > 0) {
newY -= data.gravity * dt;
}
position.y = Math.max(data.groundY, Math.min(newY, data.ceilingY));
},
onKeyDown: function (event) { if (!this.data.enabled) { return; } if (['KeyW', 'ArrowUp', 'KeyS', 'ArrowDown', 'KeyA', 'ArrowLeft', 'KeyD', 'ArrowRight', 'KeyE', 'KeyC'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});

AFRAME.registerComponent('accelerator', {
schema: {
type: { type: 'string', default: 'up' },
force: { type: 'number', default: 18 }
},
init: function () {
this.collidingEls = new Set();
this.objectContainer = document.getElementById('object-container');
this.bbox = new THREE.Box3();
this.el.object3D.updateWorldMatrix(true, false);
this.bbox.setFromObject(this.el.object3D);
},
tick: function () {
const dynamicEls = Array.from(this.objectContainer.children)
.filter(el => el.dataset.physicsType === 'dynamic' && el.id !== window.grabbedElId);

if (dynamicEls.length === 0) return;

this.el.object3D.updateWorldMatrix(true, false);
this.bbox.setFromObject(this.el.object3D);

dynamicEls.forEach(otherEl => {
if (!otherEl.object3D) return;
const otherBbox = new THREE.Box3().setFromObject(otherEl.object3D);
const isColliding = this.bbox.intersectsBox(otherBbox);

if (isColliding) {
if (!this.collidingEls.has(otherEl)) {
this.collidingEls.add(otherEl);
this.applyEffect(otherEl);
}
} else {
if (this.collidingEls.has(otherEl)) {
this.collidingEls.delete(otherEl);
}
}
});
},
applyEffect: function(otherEl) {
if (!otherEl || !otherEl.body) return;
const velocity = otherEl.body.getLinearVelocity();
switch (this.data.type) {
case 'up':
otherEl.body.setLinearVelocity(new Ammo.btVector3(velocity.x(), 0, velocity.z()));
const impulse = new THREE.Vector3(0, 1, 0).multiplyScalar(this.data.force);
otherEl.body.applyCentralImpulse(new Ammo.btVector3(impulse.x, impulse.y, impulse.z));
break;
case 'straight':
const straightDir = new THREE.Vector3(velocity.x(), 0, velocity.z()).normalize();
const straightForce = straightDir.multiplyScalar(this.data.force);
otherEl.body.applyCentralForce(new Ammo.btVector3(straightForce.x, 0, straightForce.z));
break;
case 'reverse':
const reverseDir = new THREE.Vector3(-velocity.x(), 0, -velocity.z()).normalize();
const reverseForce = reverseDir.multiplyScalar(this.data.force);
otherEl.body.applyCentralForce(new Ammo.btVector3(reverseForce.x, 0, reverseForce.z));
break;
default:
const travelDir = new THREE.Vector3(velocity.x(), 0, velocity.z()).normalize();
if (travelDir.lengthSq() > 0.01) {
const angle = this.data.type === 'left' ? Math.PI / 2 : -Math.PI / 2;
travelDir.applyAxisAngle(new THREE.Vector3(0, 1, 0), angle);
const forceVec = travelDir.multiplyScalar(this.data.force);
otherEl.body.applyCentralForce(new Ammo.btVector3(forceVec.x, forceVec.y, forceVec.z));
}
break;
}
}
});

AFRAME.registerComponent('hand-grab', {
schema: {
enabled: { type: 'boolean', default: false },
grabRadius: { type: 'number', default: 0.1 }
},
init: function () {
this.grabbedEl = null;
this.objectContainer = document.getElementById('object-container');
this.handPos = new THREE.Vector3();
this.objPos = new THREE.Vector3();
this.isAButtonPressed = false;
this.isBButtonPressed = false;

this.onAButtonDown = this.onAButtonDown.bind(this);
this.onAButtonUp = this.onAButtonUp.bind(this);
this.onBButtonDown = this.onBButtonDown.bind(this);
this.onBButtonUp = this.onBButtonUp.bind(this);
this.onTriggerDown = this.onTriggerDown.bind(this);
this.onTriggerUp = this.onTriggerUp.bind(this);
this.el.addEventListener('triggerdown', this.onTriggerDown);
this.el.addEventListener('triggerup', this.onTriggerUp);
this.el.addEventListener('abuttondown', this.onAButtonDown);
this.el.addEventListener('abuttonup', this.onAButtonUp);
this.el.addEventListener('bbuttondown', this.onBButtonDown);
this.el.addEventListener('bbuttonup', this.onBButtonUp);
},
remove: function () {
this.el.removeEventListener('triggerdown', this.onTriggerDown);
this.el.removeEventListener('triggerup', this.onTriggerUp);
this.el.removeEventListener('abuttondown', this.onAButtonDown);
this.el.removeEventListener('abuttonup', this.onAButtonUp);
this.el.removeEventListener('bbuttondown', this.onBButtonDown);
this.el.removeEventListener('bbuttonup', this.onBButtonUp);
},
onAButtonDown: function() { this.isAButtonPressed = true; },
onAButtonUp: function() { this.isAButtonPressed = false; },
onBButtonDown: function() { this.isBButtonPressed = true; },
onBButtonUp: function() { this.isBButtonPressed = false; },

onTriggerDown: function () {
if (!this.data.enabled || this.grabbedEl) return;

this.el.object3D.getWorldPosition(this.handPos);
const grabbableEls = this.objectContainer.children;
let closestEl = null;
let closestDistSq = this.data.grabRadius * this.data.grabRadius;

for (let i = 0; i < grabbableEls.length; i++) {
const el = grabbableEls[i];
el.object3D.getWorldPosition(this.objPos);
const distSq = this.handPos.distanceToSquared(this.objPos);
if (distSq < closestDistSq) {
closestDistSq = distSq;
closestEl = el;
}
}

if (closestEl) {
if (this.isBButtonPressed) {
closestEl.parentNode.removeChild(closestEl);
return;
}
if (this.isAButtonPressed) {
const worldPos = new THREE.Vector3();
const worldQuat = new THREE.Quaternion();
closestEl.object3D.getWorldPosition(worldPos);
closestEl.object3D.getWorldQuaternion(worldQuat);

const copyEl = closestEl.cloneNode(true);
const originalType = copyEl.tagName.toLowerCase() === 'a-box' ? 'box' : 'sphere';
copyEl.setAttribute('id', `${originalType}-${window.createdObjectCounter++}`);

this.objectContainer.appendChild(copyEl);

const originalMaterial = closestEl.getAttribute('material');
if (originalMaterial) { copyEl.setAttribute('material', originalMaterial); }
const originalAccelerator = closestEl.getAttribute('accelerator');
if (originalAccelerator) { copyEl.setAttribute('accelerator', originalAccelerator); }

if (closestEl.hasAttribute('accelerator')) {
copyEl.removeAttribute('ammo-body');
copyEl.dataset.physicsType = 'static';
} else {
const isGravityEnabled = closestEl.dataset.physicsType === 'dynamic';
if (copyEl.hasAttribute('ammo-body')) {
if (isGravityEnabled) {
copyEl.setAttribute('ammo-body', 'type: dynamic; mass: 2;');
copyEl.dataset.physicsType = 'dynamic';
} else {
copyEl.setAttribute('ammo-body', 'type: static;');
copyEl.dataset.physicsType = 'static';
}
}
}
const euler = new THREE.Euler().setFromQuaternion(worldQuat, 'YXZ');
copyEl.setAttribute('position', worldPos);
copyEl.setAttribute('rotation', {
x: THREE.MathUtils.radToDeg(euler.x),
y: THREE.MathUtils.radToDeg(euler.y),
z: THREE.MathUtils.radToDeg(euler.z)
});
this.grabbedEl = closestEl;
} else {
this.grabbedEl = closestEl;
}
this.el.object3D.attach(this.grabbedEl.object3D);
if (this.grabbedEl.hasAttribute('ammo-body')) {
this.grabbedEl.setAttribute('ammo-body', 'type', 'kinematic');
}
}
},
onTriggerUp: function () {
if (!this.data.enabled || !this.grabbedEl) return;
this.objectContainer.object3D.attach(this.grabbedEl.object3D);
if (this.grabbedEl.hasAttribute('ammo-body')) {
const physicsType = this.grabbedEl.dataset.physicsType || 'dynamic';
this.grabbedEl.setAttribute('ammo-body', 'type', physicsType);
}
window.alignSingleObject(this.grabbedEl);
this.grabbedEl = null;
}
});
</script>

<style>
/* CSSはそのまま */
#hud-pc { position: fixed; bottom: 20px; left: 20px; color: white; background-color: rgba(0, 0, 0, 0.4); padding: 10px; border-radius: 10px; font-family: sans-serif; font-size: 16px; font-weight: bold; z-index: 10; }
#hud-pc #hud-main-content-pc > div:not(:last-child) { margin-bottom: 12px; }
#post-start-panel > div:not(:last-child) { margin-bottom: 12px; }
.hud-buttons { display: flex; gap: 10px; }
.hud-buttons > div { flex-grow: 1; cursor: pointer; padding: 10px; border-radius: 5px; display: flex; justify-content: center; align-items: center; }
#replay-button-pc { background-color: #C0392B; } #replay-button-pc:hover { background-color: #A93226; }
#help-button-pc { background-color: #3498DB; } #credit-button-pc { background-color: #9B59B6; }
#create-gravity-box-pc { background-color: #8E44AD; } #create-gravity-sphere-pc { background-color: #D35400; }
#create-grid-box-pc { background-color: #2980B9; } #create-grid-sphere-pc { background-color: #27AE60; }
#create-accel-up-pc { background-color: #e74c3c; } #create-accel-right-pc { background-color: #f1c40f; } #create-accel-left-pc { background-color: #e67e22; }
#create-accel-straight-pc { background-color: #3498db; } #create-accel-reverse-pc { background-color: #2ecc71; }
#change-background-pc { background-color: #7f8c8d; } #change-background-pc:hover { background-color: #95a5a6; }
.color-palette-pc { display: grid; grid-template-columns: repeat(5, 1fr); gap: 5px; }
.color-swatch-pc { width: 100%; height: 30px; border-radius: 4px; cursor: pointer; border: 2px solid rgba(255, 255, 255, 0.5); box-sizing: border-box; }
.color-swatch-pc:hover { border-color: white; }
#xr-buttons { position: fixed; bottom: 21px; right: 100px; display: flex; flex-direction: row; gap: 10px; z-index: 1000; }
#xr-buttons button { padding: 6px 12px; font-size: 14px; border: 2px solid white; background-color: rgba(0, 0, 0, 0.5); color: white; cursor: pointer; border-radius: 8px; width: 60px; text-align: center; }
#xr-buttons button:hover { background-color: rgba(255, 50, 100, 0.5); }
.a-dom-overlay:not(.a-no-style) { top: auto !important; left: auto !important; padding: 0em; }
#change-background-pc:hover { background-color: #95a5a6; } #tidy-up-pc { background-color: #16A085; }
#post-start-panel { display: none; }
#replay-button-pc.pre-start { font-size: 2em; padding: 20px; }
#info-overlay-pc { position: fixed; top: 0; left: 0; width: 100%; height: 100%; background-color: rgba(0, 0, 0, 0.7); z-index: 20; display: none; justify-content: center; align-items: center; flex-direction: column; }
#info-content-pc { background-color: white; color: black; padding: 30px; border-radius: 10px; max-width: 80%; max-height: 80%; overflow-y: auto; white-space: pre-wrap; font-family: sans-serif; font-size: 16px; }
#info-close-button-pc { margin-top: 15px; padding: 10px 25px; background-color: #E74C3C; color: white; border-radius: 5px; cursor: pointer; font-weight: bold; }
</style>

</head>
<body>

<div id="hud-pc">
<div id="hud-main-content-pc">
<div class="hud-buttons">
<div id="replay-button-pc" onclick="handleStartReplayClick()">スタート</div>
<div id="help-button-pc">説明</div>
<div id="credit-button-pc">Credit</div>
<div id="change-background-pc" style="font-size: 13px;">背景</div>
</div>

<div id="post-start-panel">
<div class="hud-buttons">
<div id="save-button-pc" style="background-color: #1abc9c;">セーブ</div>
<div id="load-button-pc" style="background-color: #9b59b6;">ロード</div>
<div id="tidy-up-pc">整理</div>
</div>
<div id="color-palette-pc" class="color-palette-pc"></div>
<div class="hud-buttons">
<div id="create-gravity-box-pc">■</div>
<div id="create-gravity-sphere-pc">●</div>
<div id="create-grid-box-pc">□</div>
<div id="create-grid-sphere-pc">〇</div>
</div>
<div class="hud-buttons">
<div id="create-accel-up-pc">上</div>
<div id="create-accel-right-pc">右</div>
<div id="create-accel-left-pc">左</div>
<div id="create-accel-straight-pc">順</div>
<div id="create-accel-reverse-pc">反</div>
</div>
</div>
</div>
</div>

<div id="info-overlay-pc">
<div id="info-content-pc"></div>
<div id="info-close-button-pc">閉じる</div>
</div>

<div id="xr-buttons">
<button id="ar-button">AR</button>
</div>

<a-scene id="myScene"
vr-mode-ui="enabled: false"
webxr="optionalFeatures: dom-overlay; overlayElement: #xr-buttons;"
background="color: #87CEEB"
renderer="colorManagement: true"
cursor="rayOrigin: mouse"
physics="driver: ammo; debug: false; gravity: -9.8;">

<a-assets>
<img id="skyTexture" src="./pic/h400.png">
<img id="gridTexture" src="./pic/grid3.png">
</a-assets>

<a-entity id="rig" position="0 1.6 2"
camera-relative-controls="targetSpeed: 2; acceleration: 3; verticalSpeed: 2;">
<a-entity id="player" position="0 0 0">
<a-entity id="camera" camera="far: 20000;" look-controls="pointerLockEnabled: false; touchEnabled: false" position="0 0.1 0">
<a-entity id="mouseCursor" cursor="rayOrigin: mouse;" raycaster="objects: .clickable, .grabbable;" position="0 0 -1" geometry="primitive: ring; radiusInner: 0.02; radiusOuter: 0.03;" material="color: black; shader: flat; opacity: 0.7;"></a-entity>
</a-entity>

<a-entity id="leftHand" hand-controls="hand: left;">

<a-entity id="vr-hud" position="0.02 0.04 -0.35" rotation="-60 0 0">
<a-plane id="hud-background" width="0.4" height="0.40" color="#FAFAFA" opacity="0.8" side="double"></a-plane>

<a-entity id="main-menu-group">
<a-box id="replay-button-vr" class="clickable" position="0 0 0.011"
width="0.4" height="0.4" depth="0.2" material="color: #2ECC71; shader: flat"></a-box>
<a-entity id="replay-text-vr" troika-text="value: スタート; color: black; fontSize: 0.04; align: center; anchor: center;"
position="0 0 0.12"></a-entity>

<a-box id="help-button-vr" class="clickable" position="-0.05 0.17 0.01"
width="0.07" height="0.03" depth="0.01" material="color: #3498DB; shader: flat"></a-box>
<a-entity troika-text="value: 説明; color: black; fontSize: 0.012; align: center; anchor: center;"
position="-0.05 0.17 0.016"></a-entity>

<a-box id="credit-button-vr" class="clickable" position="0.05 0.17 0.01"
width="0.07" height="0.03" depth="0.01" material="color: #9B59B6; shader: flat"></a-box>
<a-entity troika-text="value: Credit; color: black; fontSize: 0.012; align: center; anchor: center;"
position="0.05 0.17 0.016"></a-entity>

<a-box id="tidy-up-vr" class="clickable" position="-0.05 -0.13 0.01"
width="0.07" height="0.03" depth="0.01" material="color: #16A085; shader: flat"></a-box>
<a-entity id="tidy-up-text-vr" troika-text="value: 整理; color: black; fontSize: 0.012; align: center; anchor: center;"
position="-0.05 -0.13 0.016"></a-entity>

<a-box id="toggle-grab-mode-vr" class="clickable" position="0.05 -0.13 0.01"
width="0.07" height="0.03" depth="0.01" material="color: yellow; shader: flat"></a-box>
<a-entity id="grab-mode-text-vr" troika-text="value: ライン; color: black; fontSize: 0.012; align: center; anchor: center;"
position="0.05 -0.13 0.016"></a-entity>

<a-box id="toggle-ground-vr" class="clickable"
width="0.07" height="0.03" depth="0.01"
position="0.15 0.13 0.01"
material="color: #95a5a6; shader: flat"></a-box>
<a-entity id="toggle-ground-text-vr"
troika-text="value: 地面ON; color: black; fontSize: 0.012; align: center; anchor: center;"
position="0.15 0.13 0.016"></a-entity>

<a-box id="exit-vr-button-vr" class="clickable"
width="0.07" height="0.03" depth="0.01"
position="-0.15 0.13 0.01"
material="color: #7f8c8d; shader: flat"></a-box>
<a-entity troika-text="value: VR終了; color: black; fontSize: 0.012; align: center; anchor: center;"
position="-0.15 0.13 0.016"></a-entity>

<a-entity id="color-palette-vr" position="0 0.03 0.01"></a-entity>

<a-box id="create-gravity-box-vr" class="clickable" width="0.06" height="0.06" depth="0.06" position="-0.125 -0.065 0.01" material="color: #8E44AD; shader: flat"></a-box>
<a-entity troika-text="value: 重; color: black; fontSize: 0.012;" position="-0.125 -0.065 0.042"></a-entity>
<a-sphere id="create-gravity-sphere-vr" class="clickable" radius="0.03" position="-0.045 -0.065 0.01" material="color: #D35400; shader: flat"></a-sphere>
<a-entity troika-text="value: 重; color: black; fontSize: 0.012;" position="-0.045 -0.065 0.042"></a-entity>
<a-box id="create-grid-box-vr" class="clickable" width="0.06" height="0.06" depth="0.06" position="0.045 -0.065 0.01" material="color: #2980B9; shader: flat"></a-box>
<a-entity troika-text="value: 固; color: black; fontSize: 0.012;" position="0.045 -0.065 0.042"></a-entity>
<a-sphere id="create-grid-sphere-vr" class="clickable" radius="0.03" position="0.125 -0.065 0.01" material="color: #27AE60; shader: flat"></a-sphere>
<a-entity troika-text="value: 固; color: black; fontSize: 0.012;" position="0.125 -0.065 0.042"></a-entity>

<a-box id="create-accel-up-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="-0.16 -0.17 0.01" material="color: #e74c3c; shader: flat"></a-box>
<a-entity troika-text="value: 上; color: black; fontSize: 0.012;" position="-0.16 -0.17 0.016"></a-entity>
<a-box id="create-accel-right-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="-0.08 -0.17 0.01" material="color: #f1c40f; shader: flat"></a-box>
<a-entity troika-text="value: 右; color: black; fontSize: 0.012;" position="-0.08 -0.17 0.016"></a-entity>
<a-box id="create-accel-left-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0 -0.17 0.01" material="color: #e67e22; shader: flat"></a-box>
<a-entity troika-text="value: 左; color: black; fontSize: 0.012;" position="0 -0.17 0.016"></a-entity>
<a-box id="create-accel-straight-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0.08 -0.17 0.01" material="color: #3498db; shader: flat"></a-box>
<a-entity troika-text="value: 順; color: black; fontSize: 0.012;" position="0.08 -0.17 0.016"></a-entity>
<a-box id="create-accel-reverse-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0.16 -0.17 0.01" material="color: #2ecc71; shader: flat"></a-box>
<a-entity troika-text="value: 反; color: black; fontSize: 0.012;" position="0.16 -0.17 0.016"></a-entity>

<a-box id="save-button-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="-0.05 0.13 0.01" material="color: #1abc9c; shader: flat"></a-box>
<a-entity troika-text="value: セーブ; color: black; fontSize: 0.012; align: center; anchor: center;" position="-0.05 0.13 0.016"></a-entity>
<a-box id="load-button-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0.05 0.13 0.01" material="color: #9b59b6; shader: flat"></a-box>
<a-entity troika-text="value: ロード; color: black; fontSize: 0.012; align: center; anchor: center;" position="0.05 0.13 0.016"></a-entity>
</a-entity>
<a-box id="change-background-vr" class="clickable" position="0.15 0.17 0.01"
width="0.07" height="0.03" depth="0.01" material="color: #bdc3c7; shader: flat"></a-box>
<a-entity id="change-background-text-vr" troika-text="value: 背景; color: black; fontSize: 0.010; align: center; anchor: center;"
position="0.15 0.17 0.016"></a-entity>


<a-entity id="bg-mode-group" position="0 0 -2000" visible="false">

<a-box id="bg-inc-100" class="clickable" position="0.085 0.17 0"
width="0.03" height="0.03" depth="0.01" material="color: #9B59B6; shader: flat"></a-box>
<a-entity troika-text="value: +100; color: white; fontSize: 0.008; align: center;" position="0.085 0.17 0.006"></a-entity>

<a-box id="bg-inc-10" class="clickable" position="0.04 0.17 0"
width="0.03" height="0.03" depth="0.01" material="color: #8E44AD; shader: flat"></a-box>
<a-entity troika-text="value: +10; color: white; fontSize: 0.009; align: center;" position="0.04 0.17 0.006"></a-entity>

<a-box id="bg-inc-1" class="clickable" position="-0.005 0.17 0"
width="0.03" height="0.03" depth="0.01" material="color: #9B59B6; shader: flat"></a-box>
<a-entity troika-text="value: +1; color: white; fontSize: 0.009; align: center;" position="-0.005 0.17 0.006"></a-entity>

<a-box id="bg-dec-1" class="clickable" position="-0.05 0.17 0"
width="0.03" height="0.03" depth="0.01" material="color: #3498DB; shader: flat"></a-box>
<a-entity troika-text="value: -1; color: white; fontSize: 0.009; align: center;" position="-0.05 0.17 0.006"></a-entity>

<a-box id="bg-dec-10" class="clickable" position="-0.095 0.17 0"
width="0.03" height="0.03" depth="0.01" material="color: #2980B9; shader: flat"></a-box>
<a-entity troika-text="value: -10; color: white; fontSize: 0.009; align: center;" position="-0.095 0.17 0.006"></a-entity>

<a-box id="bg-dec-100" class="clickable" position="-0.14 0.17 0"
width="0.03" height="0.03" depth="0.01" material="color: #3498DB; shader: flat"></a-box>
<a-entity troika-text="value: -100; color: white; fontSize: 0.008; align: center;" position="-0.14 0.17 0.006"></a-entity>
<a-box id="bg-back-button" class="clickable" position="0.14 0.12 0"
width="0.07" height="0.03" depth="0.01" material="color: #e74c3c; shader: flat"></a-box>
<a-entity troika-text="value: 戻る; color: white; fontSize: 0.012; align: center; anchor: center;"
position="0.14 0.12 0.006"></a-entity>

<a-box id="bg-record-button" class="clickable" position="0.14 0.07 0"
width="0.07" height="0.03" depth="0.01" material="color: #9B59B6; shader: flat"></a-box>
<a-entity troika-text="value: 記録; color: white; fontSize: 0.012; align: center; anchor: center;"
position="0.14 0.07 0.006"></a-entity>

<a-box id="bg-jump-button" class="clickable" position="0.14 0.02 0"
width="0.07" height="0.03" depth="0.01" material="color: #27AE60; shader: flat"></a-box>
<a-entity troika-text="value: 移動; color: white; fontSize: 0.012; align: center; anchor: center;"
position="0.14 0.02 0.006"></a-entity>

<a-box id="bg-clear-button" class="clickable" position="0.14 -0.03 0"
width="0.07" height="0.03" depth="0.01" material="color: #e67e22; shader: flat"></a-box>
<a-entity troika-text="value: 消去; color: white; fontSize: 0.012; align: center; anchor: center;"
position="0.14 -0.03 0.006"></a-entity>

<a-box id="bg-text-save-button" class="clickable" position="0.14 -0.08 0"
width="0.07" height="0.03" depth="0.01" material="color: #2c3e50; shader: flat"></a-box>
<a-entity id="bg-text-save-text" troika-text="value: TXT保存; color: white; fontSize: 0.012; align: center; anchor: center;"
position="0.14 -0.08 0.006"></a-entity>

<a-box id="bg-clear-all-button" class="clickable" position="0.14 -0.13 0"
width="0.07" height="0.03" depth="0.01" material="color: #c0392b; shader: flat"></a-box>
<a-entity troika-text="value: 全消去; color: white; fontSize: 0.012; align: center; anchor: center;"
position="0.14 -0.13 0.006"></a-entity>
<a-entity id="bg-grid-container" position="0 0 0"></a-entity>
</a-entity>


<a-entity id="info-panel-vr" position="0 0 0.05" visible="false">
<a-plane width="0.4" height="0.52" color="#333" opacity="0.9"></a-plane>
<a-entity id="info-text-vr" troika-text="value: Text here; color: white; fontSize: 0.015; anchor: center; align: left; maxWidth: 0.38; wrapCount: 40;"
position="0 0.03 0.013"></a-entity>

<a-box id="info-close-button-vr" class="clickable" width="0.1" height="0.03" depth="0.01" position="0 -0.25 0.01"
material="color: #E74C3C; shader: flat"></a-box>

<a-entity troika-text="value: 閉じる; color: white; fontSize: 0.012; anchor: center; align: center;"
position="0 -0.25 0.016"></a-entity>
</a-entity>

</a-entity>
</a-entity>

<a-entity id="rightHand"
hand-controls="hand: right;"
laser-controls="hand: right; model: false;"
raycaster="objects: .clickable, .grabbable; lineColor: white; lineOpacity: 0.75"
hand-grab>
<a-cylinder id="hand-pointer-visual" radius="0.005" height="0.25" rotation="90 0 0"
position="0 0 -0.125" material="color: lightblue; shader: flat" visible="false">
</a-cylinder>
</a-entity>

</a-entity>
</a-entity>

<a-entity light="type: ambient; color: #888"></a-entity>
<a-entity light="type: directional; color: #FFF; intensity: 0.8;" position="-100 200 100"></a-entity>

<a-box
id="ground"
position="0.035 -0.955 0.035"
width="16"
height="2"
depth="16"
material="src: #gridTexture; repeat: 8 8;"
ammo-body="type: static;"
ammo-shape="type: box; halfExtents: 8 1 8;">
</a-box>

<a-entity id="object-container"></a-entity>

<a-sky id="sky" src="#skyTexture"></a-sky>

</a-scene>

<script>
document.addEventListener('DOMContentLoaded', function () {

// 初期画像設定(前回記憶した番号があればそれを使う)
let lastIndex = localStorage.getItem('lastBgIndex');
const DEFAULT_BG_INDEX = lastIndex ? parseInt(lastIndex, 10) : 400;

const sceneEl = document.querySelector('a-scene');
let isGameStarted = false;
window.createdObjectCounter = 0;
let grabbedObject = null;
let draggedObject = null;
let dragDistance = 0;
const raycaster = new THREE.Raycaster();
const mouse = new THREE.Vector2();
const worldPosition = new THREE.Vector3();
let isLaserGrabMode = true;
let selectedColor = '#CCCCCC';
let isAButtonPressed = false;
let isShiftKeyPressed = false;
let isCtrlKeyPressed = false;
let isBButtonPressed = false;

window.copyDragData = null;
let vrCopyGrabData = null;
let copyOperationData = null;

const HELP_TEXT = `操作方法
PC:
- 移動: W, A, S, D / 矢印キー
- 上昇/下降: E, C
- オブジェクト掴む: 左クリックでドラッグ
- オブジェクトコピー: Shift + 左クリックでドラッグ
- オブジェクト削除: Ctrl + 左クリック

VR (Quest):
- 移動: 左スティック
- 視点回転/上下移動: 右スティック
- オブジェクト操作 (ライン): 右トリガーで掴む
- オブジェクト操作 (ハンド): 右トリガーで掴む
- コピー: Aボタン + トリガー
- 削除: Bボタン + トリガー
- モード切替: パネルの「ライン/ハンド」ボタン

★ハンドモードは、右手で物をつかみます。
★ラインモードは、ライン上にある物をつかみます。
`;

const CREDIT_TEXT = `VR Test Environment Ver 14.8

開発: setokem(GeminiPro2.5 and 3.0使用)

使用ライブラリ:
- A-Frame
- aframe-physics-system
- aframe-look-at-component
- aframe-troika-text
- Ammo.js

背景画像:
- Imagen 3.0
- Nano Banana Pro (Gemini 3 Pro Image)


`;

let backgroundIndex = DEFAULT_BG_INDEX;
window.currentBackgroundIndex = backgroundIndex; // 表示用に保持

// ★★★ 起動時にタグと上限数を読み込み ★★★
window.fetchTags();
window.fetchMaxCount();

window.addEventListener('keydown', function(event) {
if (event.key === 'Shift') { isShiftKeyPressed = true; }
if (event.key === 'Control') { isCtrlKeyPressed = true; }
});
window.addEventListener('keyup', function(event) {
if (event.key === 'Shift') { isShiftKeyPressed = false; }
if (event.key === 'Control') { isCtrlKeyPressed = false; }
});

const rightHand = document.getElementById('rightHand');
const rigEl = document.getElementById('rig');
const objectContainer = document.getElementById('object-container');
const createGravityBoxPC = document.getElementById('create-gravity-box-pc');
const createGravitySpherePC = document.getElementById('create-gravity-sphere-pc');
const createGridBoxPC = document.getElementById('create-grid-box-pc');
const createGridSpherePC = document.getElementById('create-grid-sphere-pc');

function handleStartReplayClick() {
if (!isGameStarted) {
startGame();
} else {
replayGame();
}
}
window.handleStartReplayClick = handleStartReplayClick;

function startGame() {
if (isGameStarted) return;

const startButton = document.getElementById('replay-button-pc');
if (startButton) {
startButton.classList.remove('pre-start');
}
const postStartPanel = document.getElementById('post-start-panel');
if (postStartPanel) {
postStartPanel.style.display = 'block';
}

isGameStarted = true;
console.log("Game started!");
document.getElementById('replay-button-pc').textContent = 'リセット';

const replayButtonVR = document.getElementById('replay-button-vr');
const replayTextVR = document.getElementById('replay-text-vr');

if (replayButtonVR) {
replayButtonVR.setAttribute('width', '0.07');
replayButtonVR.setAttribute('height', '0.03');
replayButtonVR.setAttribute('depth', '0.01');
replayButtonVR.setAttribute('material', 'color', '#C0392B');
replayButtonVR.setAttribute('position', { x: -0.15, y: 0.17, z: 0.01 });
}
if (replayTextVR) {
replayTextVR.setAttribute('troika-text', { value: 'リセット', fontSize: 0.012 });
replayTextVR.setAttribute('position', { x: -0.15, y: 0.17, z: 0.016 });
}

const leftHand = document.getElementById('leftHand');
if (leftHand) {
leftHand.object3D.traverse(function (node) {
if (node.isSkinnedMesh) {
node.visible = false;
}
});
}

const rightHand = document.getElementById('rightHand');
if (rightHand) {
rightHand.object3D.traverse(function (node) {
if (node.isSkinnedMesh) {
node.visible = false;
}
});
}
const handPointer = document.getElementById('hand-pointer-visual');
if (handPointer) {
handPointer.setAttribute('visible', true);
}
// ★★★ 起動時にグリッドボタンを生成 ★★★
window.createBackgroundGrid();

replayGame();
}

function replayGame() {
console.log("Replaying game...");
objectContainer.innerHTML = '';
window.createdObjectCounter = 0;
if (rigEl && rigEl.components['camera-relative-controls']) {
rigEl.setAttribute('camera-relative-controls', 'enabled', true);
}
rigEl.setAttribute('position', '0 1.6 2');
rigEl.setAttribute('rotation', '0 0 0');
}

function createBox(position, isGravity, color) {
const snapSize = 0.1;
const snappedPosition = {
x: Math.round(position.x / snapSize) * snapSize,
y: Math.max(snapSize, Math.round(position.y / snapSize) * snapSize),
z: Math.round(position.z / snapSize) * snapSize
};
const box = document.createElement('a-box');
box.setAttribute('id', `box-${window.createdObjectCounter++}`);
box.classList.add('grabbable');
box.setAttribute('position', snappedPosition);
box.setAttribute('width', 0.1);
box.setAttribute('height', 0.1);
box.setAttribute('depth', 0.1);
box.setAttribute('color', color || selectedColor);

if (isGravity) {
box.setAttribute('ammo-body', 'type: dynamic; mass: 2; friction: 0.05; restitution: 0.9;');
box.dataset.physicsType = 'dynamic';
box.dataset.objectType = 'gravity-box';
} else {
box.setAttribute('ammo-body', 'type: static; friction: 0.05; restitution: 0.9;');
box.dataset.physicsType = 'static';
box.dataset.objectType = 'grid-box';
}
box.setAttribute('ammo-shape', 'type: box;');
objectContainer.appendChild(box);
}

function createSphere(position, isGravity, color) {
const snapSize = 0.1;
const snappedPosition = {
x: Math.round(position.x / snapSize) * snapSize,
y: Math.max(snapSize, Math.round(position.y / snapSize) * snapSize),
z: Math.round(position.z / snapSize) * snapSize
};
const sphere = document.createElement('a-sphere');
sphere.setAttribute('id', `sphere-${window.createdObjectCounter++}`);
sphere.classList.add('grabbable');
sphere.setAttribute('position', snappedPosition);
sphere.setAttribute('radius', 0.045);
sphere.setAttribute('color', color || selectedColor);

if (isGravity) {
sphere.setAttribute('ammo-body', 'type: dynamic; mass: 2; angularDamping: 0.3; friction: 0.05; restitution: 0.9;');
sphere.dataset.physicsType = 'dynamic';
sphere.dataset.objectType = 'gravity-sphere';
} else {
sphere.setAttribute('ammo-body', 'type: static; friction: 0.05; restitution: 0.9;');
sphere.dataset.physicsType = 'static';
sphere.dataset.objectType = 'grid-sphere';
}
sphere.setAttribute('ammo-shape', 'type: sphere;');
objectContainer.appendChild(sphere);
}

function createAccelerator(position, accelType) {
const block = document.createElement('a-box');
block.setAttribute('id', `accel-${window.createdObjectCounter++}`);
block.classList.add('grabbable');

const size = 0.1;
block.setAttribute('width', size);
block.setAttribute('height', size);
block.setAttribute('depth', size);

let color, force;
switch (accelType) {
case 'up': color = '#e74c3c'; force = 5;
block.dataset.objectType = 'accelerator-up';
break;
case 'right': color = '#f1c40f'; force = 50;
block.dataset.objectType = 'accelerator-right';
break;
case 'left': color = '#e67e22'; force = 50;
block.dataset.objectType = 'accelerator-left';
break;
case 'straight': color = '#3498db'; force = 50;
block.dataset.objectType = 'accelerator-straight';
break;
case 'reverse': color = '#2ecc71'; force = 50;
block.dataset.objectType = 'accelerator-reverse';
break;
}

block.setAttribute('material', { color: color, opacity: 0.5 });

const snapSize = 0.1;
const snappedPosition = {
x: Math.round(position.x / snapSize) * snapSize,
y: Math.max(size, Math.round(position.y / snapSize) * snapSize),
z: Math.round(position.z / snapSize) * snapSize
};
block.setAttribute('position', snappedPosition);

block.dataset.physicsType = 'static';
block.setAttribute('accelerator', { type: accelType, force: force });

objectContainer.appendChild(block);
}

function alignSingleObject(obj) {
if (!obj) return;
const pos = obj.getAttribute('position');
let snapSize = (obj.tagName.toLowerCase() === 'a-box') ? obj.getAttribute('width') : 0.1;
const newPos = {
x: Math.round(pos.x / snapSize) * snapSize,
y: Math.round(pos.y / snapSize) * snapSize,
z: Math.round(pos.z / snapSize) * snapSize
};
obj.setAttribute('position', newPos);
obj.setAttribute('rotation', '0 0 0');
}
window.alignSingleObject = alignSingleObject;

let handModelVisual = null;
function toggleGrabMode() {
isLaserGrabMode = !isLaserGrabMode;
const rightHandEl = document.getElementById('rightHand');
const laserLine = rightHandEl.querySelector('[line]');
const handPointer = document.getElementById('hand-pointer-visual');
const modeTextEl = document.getElementById('grab-mode-text-vr');

if (!handModelVisual && rightHandEl) {
handModelVisual = rightHandEl.object3D.getObjectByProperty('type', 'SkinnedMesh');
}

if (isLaserGrabMode) {
rightHandEl.setAttribute('laser-controls', 'enabled', true);
rightHandEl.setAttribute('hand-grab', 'enabled', false);
if (modeTextEl) modeTextEl.setAttribute('troika-text', 'value', 'ライン');
if (laserLine) laserLine.setAttribute('visible', true);
if (handPointer) handPointer.setAttribute('visible', true);
if (handModelVisual) handModelVisual.visible = false;
} else {
rightHandEl.setAttribute('laser-controls', 'enabled', false);
rightHandEl.setAttribute('hand-grab', 'enabled', true);
if (modeTextEl) modeTextEl.setAttribute('troika-text', 'value', 'ハンド');
if (laserLine) laserLine.setAttribute('visible', false);
if (handPointer) handPointer.setAttribute('visible', false);
if (handModelVisual) handModelVisual.visible = true;
}
}

function selectColor(newColor) {
selectedColor = newColor;
createGravityBoxPC.style.backgroundColor = newColor;
createGravitySpherePC.style.backgroundColor = newColor;
createGridBoxPC.style.backgroundColor = newColor;
createGridSpherePC.style.backgroundColor = newColor;

const materialConfig = { shader: 'flat', color: newColor };
document.getElementById('create-gravity-box-vr').setAttribute('material', materialConfig);
document.getElementById('create-gravity-sphere-vr').setAttribute('material', materialConfig);
document.getElementById('create-grid-box-vr').setAttribute('material', materialConfig);
document.getElementById('create-grid-sphere-vr').setAttribute('material', materialConfig);
}

function saveScene() {
if (!isGameStarted) { return; }
const objectsToSave = Array.from(objectContainer.children).map(el => ({
position: el.getAttribute('position'),
rotation: el.getAttribute('rotation'),
objectType: el.dataset.objectType,
color: el.getAttribute('material')?.color,
physicsType: el.dataset.physicsType
}));
const sceneDataToSave = { objects: objectsToSave, background: backgroundIndex };
localStorage.setItem('aframeSceneData', JSON.stringify(sceneDataToSave));
console.log('Scene saved!');
}

function loadScene() {
if (!isGameStarted) { return; }
const savedDataString = localStorage.getItem('aframeSceneData');
if (!savedDataString) { return; }

const savedSceneData = JSON.parse(savedDataString);
const objectsToLoad = savedSceneData.objects;

objectContainer.innerHTML = '';
window.createdObjectCounter = 0;

if (objectsToLoad) {
for (const data of objectsToLoad) {
const isGravity = data.physicsType === 'dynamic';
if (data.objectType.includes('accelerator')) {
createAccelerator(data.position, data.objectType.split('-')[1]);
} else if (data.objectType.includes('box')) {
createBox(data.position, isGravity, data.color);
} else if (data.objectType.includes('sphere')) {
createSphere(data.position, isGravity, data.color);
}
const newEl = objectContainer.lastChild;
if (newEl && data.rotation) {
newEl.setAttribute('rotation', data.rotation);
}
}
}

const savedBackgroundIndex = savedSceneData.background;
const skyEl = document.getElementById('sky');
if (typeof savedBackgroundIndex !== 'undefined' && savedBackgroundIndex >= 0) {
const targetIndex = savedBackgroundIndex;
const pngUrl = `./pic/h${targetIndex}.png`;
const jpgUrl = `./pic/h${targetIndex}.jpg`;

const imgCheck = new Image();
imgCheck.onload = function() {
backgroundIndex = targetIndex;
if (skyEl) skyEl.setAttribute('src', pngUrl);
window.updateBackgroundDisplay(backgroundIndex);
};
imgCheck.onerror = function() {
const imgCheckJpg = new Image();
imgCheckJpg.onload = function() {
backgroundIndex = targetIndex;
if (skyEl) skyEl.setAttribute('src', jpgUrl);
window.updateBackgroundDisplay(backgroundIndex);
};
imgCheckJpg.src = jpgUrl;
};
imgCheck.src = pngUrl;

} else {
backgroundIndex = DEFAULT_BG_INDEX;
if (skyEl) skyEl.setAttribute('src', '#skyTexture');
window.updateBackgroundDisplay(backgroundIndex);
}
console.log('Scene loaded!');
}

// ▼▼▼ 変更:背景変更の統合ロジック ▼▼▼

// ボタンから呼ばれる関数
window.changeBackgroundStep = function(step) {
// 現在地 + ステップ を最初のターゲットにする
let target = window.currentBackgroundIndex + step;

// ステップの正負で「サーチ方向」を決める (+1 or -1)
const direction = step > 0 ? 1 : -1;

// サーチ開始
window.searchAndApplyImage(target, direction);
};

// 再帰的に画像を探す関数
window.searchAndApplyImage = function(index, direction) {
// 1. ループ処理 (上限・下限のチェック)
if (index > window.maxBackgroundCount) {
index = 0; // 上限を超えたら0へ
} else if (index < 0) {
index = window.maxBackgroundCount; // 0を下回ったら上限へ
}

const pngUrl = `./pic/h${index}.png`;
const jpgUrl = `./pic/h${index}.jpg`;

// 2. 画像の存在確認
const imgPng = new Image();

imgPng.onload = function() {
// PNGが見つかったら適用
window.applyBackground(index, pngUrl);
};

imgPng.onerror = function() {
// PNGが無いならJPGを試す
const imgJpg = new Image();
imgJpg.onload = function() {
// JPGが見つかったら適用
window.applyBackground(index, jpgUrl);
};
imgJpg.onerror = function() {
// 3. どちらも見つからない場合 -> 次の候補へサーチ
// 指定された方向 (direction) に 1 つ進んで再試行
console.log(`背景 ${index} が見つかりません。次は ${index + direction} を探します。`);
window.searchAndApplyImage(index + direction, direction);
};
imgJpg.src = jpgUrl;
};

imgPng.src = pngUrl;
};

// ★★★ 共通の数値更新用ヘルパー関数 ★★★
window.updateBackgroundDisplay = function(index) {
window.currentBackgroundIndex = index;

// メインメニュー側の表示も同期
const bgTextEl = document.getElementById('change-background-text-vr');
if (bgTextEl) {
const currentAttr = bgTextEl.getAttribute('troika-text');
bgTextEl.setAttribute('troika-text', { ...currentAttr, value: `背景: ${index}` });
}

const bgBtnPC = document.getElementById('change-background-pc');
if (bgBtnPC) { bgBtnPC.textContent = `背景: ${index}`; }

// 背景切り替え時、タグボタンの状態も更新
window.updateTagButtonsState(index);
};

window.applyBackground = function(index, url) {
window.currentBackgroundIndex = index;
const skyEl = document.getElementById('sky');
if (skyEl) { skyEl.setAttribute('src', url); }

// 現在の背景番号を保存
window.saveCurrentBgIndex(index);
window.updateBackgroundDisplay(index);
};

// (互換性のため残す: 1方向サーチ)
window.findNextAvailableImage = function(index) {
window.searchAndApplyImage(index, 1);
};


function tidyUpScene() {
const children = Array.from(objectContainer.children);
const objectBlueprints = [];

children.forEach(el => {
if (el.object3D.position.y < 0) return;
let blueprint = { position: el.object3D.position.clone() };
if (el.hasAttribute('accelerator')) {
blueprint.type = 'accelerator';
blueprint.accelType = el.getAttribute('accelerator').type;
} else {
blueprint.isGravity = el.dataset.physicsType === 'dynamic';
blueprint.color = el.getAttribute('material').color;
blueprint.shape = el.tagName.toLowerCase() === 'a-box' ? 'box' : 'sphere';
}
objectBlueprints.push(blueprint);
});

objectContainer.innerHTML = '';
const occupiedCoordinates = new Set();

const placeObject = (bp) => {
const snapSize = 0.1;
let targetPosition = {
x: Math.round(bp.position.x / snapSize) * snapSize,
y: Math.max(snapSize, Math.round(bp.position.y / snapSize) * snapSize),
z: Math.round(bp.position.z / snapSize) * snapSize
};
while (true) {
const coordKey = `${targetPosition.x.toFixed(2)},${targetPosition.y.toFixed(2)},${targetPosition.z.toFixed(2)}`;
if (occupiedCoordinates.has(coordKey)) {
targetPosition.y += snapSize;
} else {
occupiedCoordinates.add(coordKey);
break;
}
}
if (bp.type === 'accelerator') createAccelerator(targetPosition, bp.accelType);
else if (bp.shape === 'box') createBox(targetPosition, bp.isGravity, bp.color);
else if (bp.shape === 'sphere') createSphere(targetPosition, bp.isGravity, bp.color);
};

objectBlueprints.filter(bp => bp.type === 'accelerator').forEach(placeObject);
objectBlueprints.filter(bp => bp.type !== 'accelerator' && !bp.isGravity).forEach(placeObject);
objectBlueprints.filter(bp => bp.type !== 'accelerator' && bp.isGravity).forEach(placeObject);
console.log('整理完了。');
}

function onMouseMove(event) {
if (!draggedObject) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
const camera = sceneEl.camera;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.at(dragDistance, worldPosition);
draggedObject.object3D.parent.worldToLocal(worldPosition);
draggedObject.object3D.position.copy(worldPosition);
}

function deleteObjectAtPosition(targetPosition) {
const children = Array.from(objectContainer.children);
const tolerance = 0.01;
const elToDelete = children.find(el => el.object3D.position.distanceTo(targetPosition) < tolerance);
if (elToDelete) {
elToDelete.parentNode.removeChild(elToDelete);
}
}

function onMouseUp() {
if (!draggedObject) return;

if (window.copyDragData && !window.copyDragData.isVr) {
const data = window.copyDragData;
alignSingleObject(draggedObject);
const finalPosition = draggedObject.object3D.position.clone();
const originalPosition = data.originalPosition;
const isCopyCancelled = finalPosition.distanceTo(originalPosition) < 0.1;

data.originalEl.parentNode.removeChild(data.originalEl);
draggedObject.parentNode.removeChild(draggedObject);
deleteObjectAtPosition(originalPosition);

if (data.isAccelerator) {
createAccelerator(originalPosition, data.accelType);
if (!isCopyCancelled) {
createAccelerator(finalPosition, data.accelType);
}
} else {
const creatorFn = data.shape === 'box' ? createBox : createSphere;
creatorFn(originalPosition, data.isGravity, data.color);
if (!isCopyCancelled) {
creatorFn(finalPosition, data.isGravity, data.color);
}
}
window.copyDragData = null;
}
else {
const physicsType = draggedObject.dataset.physicsType || 'dynamic';
if (physicsType === 'static' || draggedObject.hasAttribute('accelerator')) {
alignSingleObject(draggedObject);
}
if (draggedObject.hasAttribute('ammo-body')) {
draggedObject.setAttribute('ammo-body', 'type', physicsType);
}
}

draggedObject = null;
dragDistance = 0;
window.removeEventListener('mousemove', onMouseMove);
window.removeEventListener('mouseup', onMouseUp);
}

function showInfoPanelPC(contentType) {
const overlay = document.getElementById('info-overlay-pc');
const content = document.getElementById('info-content-pc');
if (overlay && content) {
content.textContent = (contentType === 'help') ? HELP_TEXT : CREDIT_TEXT;
overlay.style.display = 'flex';
}
}

function hideInfoPanelPC() {
const overlay = document.getElementById('info-overlay-pc');
if (overlay) {
overlay.style.display = 'none';
}
}

function showHelpPanelVR() {
const panel = document.getElementById('info-panel-vr');
const textEl = document.getElementById('info-text-vr');
if (panel && textEl) {
panel.setAttribute('visible', true);
setTimeout(() => {
const currentTextConfig = textEl.getAttribute('troika-text');
textEl.setAttribute('troika-text', {...currentTextConfig,value: HELP_TEXT });
}, 0);
}
}
function showCreditPanelVR() {
const panel = document.getElementById('info-panel-vr');
const textEl = document.getElementById('info-text-vr');
if (panel && textEl) {
panel.setAttribute('visible', true);
setTimeout(() => {
const currentTextConfig = textEl.getAttribute('troika-text');
textEl.setAttribute('troika-text', {...currentTextConfig,value: CREDIT_TEXT });
}, 0);
}
}

function hideInfoPanelVR() {
const panel = document.getElementById('info-panel-vr');
if (panel) {
panel.setAttribute('visible', false);
}
}

function setupGame() {
document.getElementById('replay-button-pc').classList.add('pre-start');

const paletteContainerPC = document.getElementById('color-palette-pc');
const paletteContainerVR = document.getElementById('color-palette-vr');
const PALETTE_COLS = 5;
const PALETTE_ROWS = 3;

for (let i = 0; i < PALETTE_ROWS * PALETTE_COLS; i++) {
const hue = Math.floor((i / (PALETTE_ROWS * PALETTE_COLS)) * 360);
const color = `hsl(${hue}, 90%, 60%)`;

const swatchPC = document.createElement('div');
swatchPC.classList.add('color-swatch-pc');
swatchPC.style.backgroundColor = color;
swatchPC.addEventListener('click', () => selectColor(color));
paletteContainerPC.appendChild(swatchPC);

const swatchVR = document.createElement('a-box');
swatchVR.classList.add('clickable');
swatchVR.setAttribute('color', color);
swatchVR.setAttribute('width', '0.06');
swatchVR.setAttribute('height', '0.03');
swatchVR.setAttribute('depth', '0.01');
const x = (i % PALETTE_COLS - (PALETTE_COLS - 1) / 2) * 0.08;
const y = (Math.floor(i / PALETTE_COLS) - (PALETTE_ROWS - 1) / 2) * -0.04;
swatchVR.setAttribute('position', `${x} ${y} 0`);
swatchVR.dataset.color = color;
paletteContainerVR.appendChild(swatchVR);
}
selectColor(selectedColor);

// ★初期背景を適用(起動時)
window.findNextAvailableImage(backgroundIndex);

sceneEl.addEventListener('mousedown', function(evt) {
if (sceneEl.is('vr-mode') || !isGameStarted) return;
const intersectedEl = evt.detail.intersectedEl;
if (intersectedEl && intersectedEl.classList.contains('grabbable')) {
if (isCtrlKeyPressed) {
intersectedEl.parentNode.removeChild(intersectedEl);
return;
}
if (isShiftKeyPressed) {
if (intersectedEl.hasAttribute('accelerator')) {
window.copyDragData = {
originalEl: intersectedEl,
originalPosition: intersectedEl.object3D.position.clone(),
isAccelerator: true,
accelType: intersectedEl.getAttribute('accelerator').type,
isVr: false
};
} else {
window.copyDragData = {
originalEl: intersectedEl,
originalPosition: intersectedEl.object3D.position.clone(),
isAccelerator: false,
isGravity: intersectedEl.dataset.physicsType === 'dynamic',
shape: intersectedEl.tagName.toLowerCase() === 'a-box' ? 'box' : 'sphere',
color: intersectedEl.getAttribute('material').color,
isVr: false
};
}
intersectedEl.removeAttribute('ammo-body');
intersectedEl.setAttribute('visible', false);

const dragClone = intersectedEl.cloneNode(true);
dragClone.setAttribute('position', intersectedEl.getAttribute('position'));
objectContainer.appendChild(dragClone);
draggedObject = dragClone;
} else {
draggedObject = intersectedEl;
if (draggedObject.hasAttribute('ammo-body')) {
draggedObject.setAttribute('ammo-body', 'type', 'kinematic');
}
}
const intersection = evt.detail.intersection;
if (!intersection) return;
dragDistance = intersection.distance;
window.addEventListener('mousemove', onMouseMove);
window.addEventListener('mouseup', onMouseUp);
}
});

// PC用のボタンイベントリスナー
document.getElementById('save-button-pc').addEventListener('click', saveScene);
document.getElementById('load-button-pc').addEventListener('click', loadScene);
document.getElementById('tidy-up-pc').addEventListener('click', tidyUpScene);
document.getElementById('change-background-pc').addEventListener('click', () => window.changeBackgroundStep(1));
document.getElementById('help-button-pc').addEventListener('click', () => showInfoPanelPC('help'));
document.getElementById('credit-button-pc').addEventListener('click', () => showInfoPanelPC('credit'));
document.getElementById('info-close-button-pc').addEventListener('click', hideInfoPanelPC);

if (rightHand) {
rightHand.addEventListener('abuttondown', function() { isAButtonPressed = true; });
rightHand.addEventListener('abuttonup', function() { isAButtonPressed = false; });
rightHand.addEventListener('bbuttondown', function() { isBButtonPressed = true; });
rightHand.addEventListener('bbuttonup', function() { isBButtonPressed = false; });

rightHand.addEventListener('triggerdown', function () {
const raycasterComponent = rightHand.components.raycaster;
if (!raycasterComponent) return;
const intersectedEls = raycasterComponent.intersectedEls;
if (intersectedEls.length === 0) return;

const hitEl = intersectedEls[0];

// --- VRパネルのボタン処理 ---
if (hitEl.id === 'tidy-up-vr') { tidyUpScene(); return; }

if (hitEl.id === 'toggle-ground-vr') {
window.toggleGroundWithText();
return;
}

if (isBButtonPressed && hitEl.classList.contains('grabbable')) { hitEl.parentNode.removeChild(hitEl); return; }

// ★★★ 背景選択モード切替 ★★★
if (hitEl.id === 'change-background-vr') {
window.enterBackgroundMode();
return;
}

// ★★★ 戻るボタン処理 ★★★
if (hitEl.id === 'bg-back-button') {
window.exitBackgroundMode();
return;
}

// ★★★ 背景番号増減ボタン処理(修正・追加) ★★★
if (hitEl.id === 'bg-inc-1') { window.changeBackgroundStep(1); return; }
if (hitEl.id === 'bg-inc-10') { window.changeBackgroundStep(10); return; }
if (hitEl.id === 'bg-inc-100') { window.changeBackgroundStep(100); return; } // 新規

if (hitEl.id === 'bg-dec-1') { window.changeBackgroundStep(-1); return; }
if (hitEl.id === 'bg-dec-10') { window.changeBackgroundStep(-10); return; }
if (hitEl.id === 'bg-dec-100') { window.changeBackgroundStep(-100); return; } // 新規

// ★★★ 記録ボタン処理 ★★★
if (hitEl.id === 'bg-record-button') {
window.recordBackgroundNumber(backgroundIndex);
return;
}

// ★★★ 移動ボタン処理 ★★★
if (hitEl.id === 'bg-jump-button') {
window.jumpToRecordedBackground();
return;
}

// ★★★ 消去ボタン処理 ★★★
if (hitEl.id === 'bg-clear-button') {
window.clearRecordedNumber();
return;
}

// ★★★ テキスト保存ボタン処理 ★★★
if (hitEl.id === 'bg-text-save-button') {
window.saveGridToTextFile();
return;
}

// ★★★ 全消去ボタン処理 ★★★
if (hitEl.id === 'bg-clear-all-button') {
window.clearAllRecordedNumbers();
return;
}

// ★★★ タグボタン処理 ★★★
if (hitEl.classList.contains('tag-button-group')) {
window.handleTagClick(hitEl);
return;
}

// ★★★ グリッドボタンのクリック処理 ★★★
if (hitEl.id.startsWith('bg-grid-')) {
window.handleGridSelection(hitEl.id);
return;
}

if (hitEl.dataset.color) { selectColor(hitEl.dataset.color); return; }
if (hitEl.id === 'replay-button-vr') { handleStartReplayClick(); return; }
if (hitEl.id === 'toggle-grab-mode-vr') { toggleGrabMode(); return; }
if (hitEl.id === 'save-button-vr') { saveScene(); return; }
if (hitEl.id === 'load-button-vr') { loadScene(); return; }
if (hitEl.id === 'help-button-vr') { showHelpPanelVR(); return; }
if (hitEl.id === 'credit-button-vr') { showCreditPanelVR(); return; }
if (hitEl.id === 'info-close-button-vr') { hideInfoPanelVR(); return; }
if (hitEl.id === 'exit-vr-button-vr') { sceneEl.exitVR();return; }

const createActions = {
'create-gravity-box-vr': () => createBox(rightHand.object3D.getWorldPosition(new THREE.Vector3()), true),
'create-gravity-sphere-vr': () => createSphere(rightHand.object3D.getWorldPosition(new THREE.Vector3()), true),
'create-grid-box-vr': () => createBox(rightHand.object3D.getWorldPosition(new THREE.Vector3()), false),
'create-grid-sphere-vr': () => createSphere(rightHand.object3D.getWorldPosition(new THREE.Vector3()), false),
'create-accel-up-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'up'),
'create-accel-right-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'right'),
'create-accel-left-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'left'),
'create-accel-straight-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'straight'),
'create-accel-reverse-vr': () => createAccelerator(rightHand.object3D.getWorldPosition(new THREE.Vector3()), 'reverse')
};
if (isGameStarted && createActions[hitEl.id]) {
createActions[hitEl.id]();
return;
}

if (isLaserGrabMode && isGameStarted && hitEl.classList.contains('grabbable') && !grabbedObject) {
if (isAButtonPressed) {
if (hitEl.hasAttribute('accelerator')) {
copyOperationData = {
isAccelerator: true,
originalPosition: hitEl.object3D.position.clone(),
accelType: hitEl.getAttribute('accelerator').type
};
createAccelerator(copyOperationData.originalPosition, copyOperationData.accelType);
} else {
copyOperationData = {
isAccelerator: false,
originalPosition: hitEl.object3D.position.clone(),
isGravity: hitEl.dataset.physicsType === 'dynamic',
shape: hitEl.tagName.toLowerCase().includes('box') ? 'box' : 'sphere',
color: hitEl.getAttribute('material').color
};
const creatorFn = copyOperationData.shape === 'box' ? createBox : createSphere;
creatorFn(copyOperationData.originalPosition, false, copyOperationData.color);
}
copyOperationData.placeholderEl = objectContainer.lastChild;
}

grabbedObject = hitEl;
if (grabbedObject.hasAttribute('ammo-body')) {
grabbedObject.setAttribute('ammo-body', 'type', 'kinematic');
}
rightHand.object3D.attach(grabbedObject.object3D);
}
});

rightHand.addEventListener('triggerup', function () {
if (isLaserGrabMode && grabbedObject) {
objectContainer.object3D.attach(grabbedObject.object3D);

if (grabbedObject.hasAttribute('ammo-body')) {
const physicsType = grabbedObject.dataset.physicsType || 'dynamic';
grabbedObject.setAttribute('ammo-body', 'type', physicsType);
}
alignSingleObject(grabbedObject);

if (copyOperationData) {
const placeholder = copyOperationData.placeholderEl;
if (placeholder) {
placeholder.parentNode.removeChild(placeholder);
}
if (copyOperationData.isAccelerator) {
createAccelerator(copyOperationData.originalPosition, copyOperationData.accelType);
} else {
const creatorFn = copyOperationData.shape === 'box' ? createBox : createSphere;
creatorFn(copyOperationData.originalPosition, copyOperationData.isGravity, copyOperationData.color);
}
copyOperationData = null;
}
grabbedObject = null;
}
});
}

const createObjectPC = (creatorFn, isGravityOrType) => {
if (isGameStarted) {
const camera = document.getElementById('camera');
const worldPos = new THREE.Vector3();
const direction = new THREE.Vector3();
camera.object3D.getWorldPosition(worldPos);
camera.object3D.getWorldDirection(direction);
worldPos.add(direction.multiplyScalar(-0.5));
creatorFn(worldPos, isGravityOrType);
}
};

createGravityBoxPC.addEventListener('click', () => createObjectPC(createBox, true));
createGravitySpherePC.addEventListener('click', () => createObjectPC(createSphere, true));
createGridBoxPC.addEventListener('click', () => createObjectPC(createBox, false));
createGridSpherePC.addEventListener('click', () => createObjectPC(createSphere, false));

document.getElementById('create-accel-up-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'up'));
document.getElementById('create-accel-right-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'right'));
document.getElementById('create-accel-left-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'left'));
document.getElementById('create-accel-straight-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'straight'));
document.getElementById('create-accel-reverse-pc').addEventListener('click', () => createObjectPC(createAccelerator, 'reverse'));

const arButton = document.getElementById('ar-button');
if (arButton) {
arButton.addEventListener('click', async () => {
document.getElementById('hud-pc').style.display = 'none';
document.getElementById('xr-buttons').style.display = 'none';

const skyEl = document.getElementById('sky');
if (skyEl) { skyEl.setAttribute('visible', false); }
try {
await sceneEl.enterAR();
} catch (e) {
console.error("ARモードへの移行に失敗しました", e);
}
});
}
sceneEl.addEventListener('exit-vr', () => {
document.getElementById('hud-pc').style.display = 'block';
document.getElementById('xr-buttons').style.display = 'flex';

document.getElementById('ground').setAttribute('visible', true);
document.getElementById('sky').setAttribute('visible', true);
});
}

if (sceneEl.hasLoaded) { setupGame(); } else { sceneEl.addEventListener('loaded', setupGame, {once: true}); }
});


</script>
</body>
</html>


使用変数

-------( Function )
) { isAButtonPressed = false; }); rightHand.addEventListener -------( Function )
) { isAButtonPressed = true; }); rightHand.addEventListener -------( Function )
) { isBButtonPressed = false; }); rightHand.addEventListener -------( Function )
) { isBButtonPressed = true; }); rightHand.addEventListener -------( Function )
) { this.isAButtonPressed = true; }, onAButtonUp: function -------( Function )
) { this.isBButtonPressed = true; }, onBButtonUp: function -------( Function )
accelType
alignSingleObject -------( Function )
alignSingleObject
AllRecordedNumbers
andleGridSelection
angeBackgroundStep
angle
applyBackground
arButton
ateGravitySpherePC
ateTagButtonsState
availableTags
background
backgroundColor
backgroundIndex
bbox
bgBtnPC
bgIndex
bgModeGroup
bgTextEl
block
blueprint
body
box
bp
btn
btnId
buttons
c
camera
cameraDirection
cameraEl
cameraObject
cameraRight
ccupiedCoordinates
children
class
clickedBtn
closestDistSq
closestEl
collidingEls
color
cols
columnIndex
container
content
contentType
controls
coordKey
copyDragData
copyEl
copyOperationData
createAccelerator -------( Function )
createActions
createBox -------( Function )
createGravityBoxPC
createGridBoxPC
createGridSpherePC
createObjectPC
createSphere -------( Function )
createTagButtons
creatorFn
CREDIT_TEXT
currentAttr
currentText
currentTextConfig
currentVelocity
cursor
data
DEFAULT_BG_INDEX
deleteObjectAtPosition -------( Function )
deltaPosition
depth
desiredVelocity
diff
direction
display
distSq
dragClone
dragDistance
draggedObject
dt
dynamicEls
earchAndApplyImage
eateBackgroundGrid
eatedObjectCounter
eBackgroundDisplay
eftThumbstickInput
el
elToDelete
entBackgroundIndex
eraWorldQuaternion
err
error
euler
event) { if -------( Function )
evt) { if -------( Function )
exitBackgroundMode
fetchMaxCount
fetchTags
ffectiveLerpFactor
finalPosition
force
forceVec
formData
gap
gapX
gapY
geometry
ggleGroundWithText
ghtThumbstickInput
grabbableEls
grabbedEl
grabbedObject
gridData
groundEl
handleStartReplayClick -------( Function )
handleTagClick
handModelVisual
handPointer
handPos
height
HELP_TEXT
hideInfoPanelPC -------( Function )
hideInfoPanelVR -------( Function )
hitEl
hue
i
id
imgCheck
imgCheckJpg
imgJpg
imgPng
impulse
index
innerHTML
intersectedEl
intersectedEls
intersection
isAButtonPressed
isBButtonPressed
isColliding
isCopyCancelled
isCtrlKeyPressed
isGameStarted
isGravity
isGravityEnabled
isInputting
isLaserGrabMode
isReady
isShiftKeyPressed
jpgUrl
key
keys
laserLine
lastIndex
learRecordedNumber
lectedGridButtonId
leftHand
length
lerpFactor
leStartReplayClick
light
loadScene -------( Function )
loadTagData
material
materialConfig
maxBackgroundCount
maxPerColumn
menuGroup
modeTextEl
mouse
moveDirection
newEl
newPos
newY
NextAvailableImage
nterBackgroundMode
objectBlueprints
objectContainer
objectsToLoad
objectsToSave
objectType
objPos
oldButtons
onAButtonDown
onAButtonUp
onBButtonDown
onBButtonUp
onclick
onerror
onKeyDown
onKeyUp
onload
onMouseMove -------( Function )
onMouseUp -------( Function )
onTriggerDown
onTriggerUp
opacity
originalMaterial
originalPosition
originalType
otherBbox
otherEl) { if -------( Function )
otherEl
outputText
overlay
paletteContainerPC
paletteContainerVR
PALETTE_COLS
PALETTE_ROWS
panel
physics
physicsType
placeholder
placeholderEl
placeObject
pngUrl
pos
position
postRotPos
postStartPanel
preRotPos
prevBtn
r
radius
raw
raycaster
raycasterComponent
rdBackgroundNumber
RecordedBackground
renderer
replayButtonVR
replayGame -------( Function )
replayTextVR
response
reverseDir
reverseForce
rigEl
rightHand
rightHandEl
riginalAccelerator
rotation
rowIndex
rows
rowValues
saveCurrentBgIndex
savedData
savedDataString
savedSceneData
savedTags
savedValue
saveGridData
saveGridToTextFile
saveScene -------( Function )
saveTagData
sceneDataToSave
sceneEl
selectColor -------( Function )
selectedColor
setupGame -------( Function )
shape
showCreditPanelVR -------( Function )
showHelpPanelVR -------( Function )
showInfoPanelPC -------( Function )
side
size
skyEl
snappedPosition
snapSize
sphere
src
startButton
startGame -------( Function )
startX
startY
status
straightDir
straightForce
style
swatchPC
swatchVR
t
tagName
target
targetIndex
targetPosition
text
textContent
textEl
textId
tidyUpScene -------( Function )
toggleGrabMode -------( Function )
tolerance
travelDir
turnOn
type
ui
val
validTags
vedBackgroundIndex
velocity
verticalMovement
visible
vrCopyGrabData
webxr
width
worldPos
worldPosition
worldQuat
x
y