junkerstock
 vrz115p 

<!DOCTYPE html>
<html>
<head>
<title>VRテスト環境 Ver11.5p コピー調整、125の機能を取り込み。全部、部分ずつ質問でのとりこみ。全文はとりあえず中止</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>
// --- Component: Player Controls ---
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.ZERO_VECTOR = new THREE.Vector3();
this.cameraDirection = new THREE.Vector3();
this.cameraRight = new THREE.Vector3();
this.moveDirection = new THREE.Vector3();
this.desiredVelocity = 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) {
this.rigEl.object3D.rotation.y += -this.rightThumbstickInput.x * data.rotationSpeed * dt;
}
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]; } }
});

// ★★★ ここから修正 ★★★
// acceleratorコンポーネントを、物理イベントに頼らない、手動での当たり判定方式に完全に書き換える
AFRAME.registerComponent('accelerator', {
schema: {
type: { type: 'string', default: 'up' },
force: { type: 'number', default: 18 }
},
init: function () {
// 現在接触しているオブジェクトを記録し、エフェクトの多重発動を防ぐためのSet
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);

// 2つのボックスが重なっているか(接触しているか)を判定
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();

if (this.data.type === 'up') {
otherEl.body.setLinearVelocity(new Ammo.btVector3(velocity.x(), 0, velocity.z()));
const impulse = new THREE.Vector3(0, 1, 0).multiplyScalar(this.data.force);
const ammoImpulse = new Ammo.btVector3(impulse.x, impulse.y, impulse.z);
otherEl.body.applyCentralImpulse(ammoImpulse);
} else {
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);
const ammoForce = new Ammo.btVector3(forceVec.x, forceVec.y, forceVec.z);
otherEl.body.applyCentralForce(ammoForce);
}
}
}
});
// ★★★ ここまで修正 ★★★

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);

// ★★★ ここが最重要の修正点です ★★★
// cloneNodeがマテリアルの値を正しく引き継げない問題への対策として、
// 元のオブジェクトからmaterial属性を直接取得し、コピーに再設定します。
const originalMaterial = closestEl.getAttribute('material');
if (originalMaterial) {
copyEl.setAttribute('material', originalMaterial);
}
// ★★★ ここまでが追加の修正です ★★★

// ★★★ ここが今回の修正点です ★★★
// 対策2: accelerator属性も手動でコピーする
const originalAccelerator = closestEl.getAttribute('accelerator');
if (originalAccelerator) {
copyEl.setAttribute('accelerator', originalAccelerator);
}
// ★★★ ここまで ★★★

// ★★★ ここからが修正箇所です ★★★
// コピー元が加速ブロックかどうかを判断します
if (closestEl.hasAttribute('accelerator')) {
// 【A】加速ブロックの場合
// 加速ブロックは物理ボディを持たないので、確実に削除します
copyEl.removeAttribute('ammo-body');
// (onTriggerUpでの整列処理のため、physicsTypeはstaticにしておきます)
copyEl.dataset.physicsType = 'static';
} else {
// 【B】通常のブロックの場合 (ご提示いただいたコードのロジックをそのまま使用します)
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');
const rotationDegrees = {
x: THREE.MathUtils.radToDeg(euler.x),
y: THREE.MathUtils.radToDeg(euler.y),
z: THREE.MathUtils.radToDeg(euler.z)
};
copyEl.setAttribute('position', worldPos);
copyEl.setAttribute('rotation', rotationDegrees);
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>
#hud-pc {
position: fixed; bottom: 20px; left: 20px; color: white;
background-color: rgba(0, 0, 0, 0.4); padding: 20px; border-radius: 10px;
font-family: sans-serif; font-size: 26px; font-weight: bold; z-index: 10;
}
#hud-pc > div:not(:last-child) { margin-bottom: 12px; }
.hud-buttons { display: flex; gap: 10px; }
.hud-buttons > div {
flex-grow: 1; cursor: pointer; padding: 10px; text-align: center; border-radius: 5px;
}
#replay-button-pc { background-color: #C0392B; }
#replay-button-pc:hover { background-color: #A93226; }
#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; }

.color-palette-pc {
display: grid;
grid-template-columns: repeat(5, 1fr);
gap: 5px;
margin-bottom: 10px;
}
.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; }

#change-background-pc { background-color: #7f8c8d; margin-top: 10px; }
#change-background-pc:hover { background-color: #95a5a6; }

#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;
}
</style>
</head>
<body>
<div id="hud-pc">
<div id="hud-main-content-pc">
<div id="color-palette-pc" class="color-palette-pc"></div>
<div class="hud-buttons">
<div id="replay-button-pc" onclick="handleStartReplayClick()">スタート</div>
<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" style="margin-top: 10px;">
<div id="create-accel-up-pc" style="background-color: #e74c3c;">上</div>
<div id="create-accel-right-pc" style="background-color: #f1c40f;">右</div>
<div id="create-accel-left-pc" style="background-color: #e67e22;">左</div>
</div>
<div id="change-background-pc" class="hud-buttons" style="margin-top: 10px;"><div>背景</div></div>
</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/h126.png">
<img id="gridTexture" src="https://cdn.jsdelivr.net/gh/mrdoob/three.js/examples/textures/grid.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.4" color="#FAFAFA" opacity="0.8" side="double"></a-plane>



<!-- ▼▼▼ 背景ボタン (位置は変更なし) ▼▼▼ -->
<a-box id="change-background-vr" class="clickable" position="0 0.16 0.01"
width="0.15" height="0.03" depth="0.01" material="color: #bdc3c7; shader: flat"></a-box>
<!-- ★★★ テキストにIDを追加 ★★★ -->
<a-entity id="change-background-text-vr" troika-text="value: 背景; color: black; fontSize: 0.012; align: center; anchor: center;"
position="0 0.16 0.016"></a-entity>



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

<a-box id="replay-button-vr" class="clickable" position="-0.15 -0.03 0.01"
width="0.07" height="0.03" depth="0.01" material="color: red; shader: flat"></a-box>
<a-entity id="replay-text-vr" troika-text="value: スタート; color: black; fontSize: 0.012; align: center; anchor: center;"
position="-0.15 -0.03 0.016"></a-entity>

<a-box id="create-gravity-box-vr" class="clickable" width="0.03" height="0.03" depth="0.03" position="-0.09 -0.03 0.01" material="color: #8E44AD; shader: flat"></a-box>
<a-sphere id="create-gravity-sphere-vr" class="clickable" radius="0.015" position="-0.03 -0.03 0.01" material="color: #D35400; shader: flat"></a-sphere>
<a-box id="create-grid-box-vr" class="clickable" width="0.03" height="0.03" depth="0.03" position="0.03 -0.03 0.01" material="color: #2980B9; shader: flat"></a-box>
<a-sphere id="create-grid-sphere-vr" class="clickable" radius="0.015" position="0.09 -0.03 0.01" material="color: #27AE60; shader: flat"></a-sphere>

<a-box id="toggle-grab-mode-vr" class="clickable" position="0.15 -0.03 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.15 -0.03 0.016"></a-entity>

<a-box id="create-accel-up-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="-0.1 -0.15 0.01" material="color: #e74c3c; shader: flat"></a-box>
<a-entity troika-text="value: 上; color: black; fontSize: 0.012; align: center; anchor: center;" position="-0.1 -0.15 0.016"></a-entity>
<a-box id="create-accel-right-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0 -0.15 0.01" material="color: #f1c40f; shader: flat"></a-box>
<a-entity troika-text="value: 右; color: black; fontSize: 0.012; align: center; anchor: center;" position="0 -0.15 0.016"></a-entity>
<a-box id="create-accel-left-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="0.1 -0.15 0.01" material="color: #e67e22; shader: flat"></a-box>
<a-entity troika-text="value: 左; color: black; fontSize: 0.012; align: center; anchor: center;" position="0.1 -0.15 0.016"></a-entity>

<!-- ▼▼▼ 地面ON/OFFボタン (位置を背景ボタンに合わせる) ▼▼▼ -->
<a-box id="toggle-ground-vr" class="clickable" visible="false"
width="0.15" height="0.03" depth="0.01" position="0 0.16 0.01" material="color: #95a5a6; shader: flat"></a-box>
<a-entity id="toggle-ground-text-vr" troika-text="value: 地面ON/OFF; color: black; fontSize: 0.012; align: center; anchor: center;"
position="0 0.16 0.016" visible="false"></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-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 -1 0"
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>
// --- Main Scene Logic ---
document.addEventListener('DOMContentLoaded', function () {
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 backgroundImages = [];
for (let i = 1; i <= 200; i++) {
backgroundImages.push(`./pic/h${i}.png`);
}
let backgroundIndex = 125;
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;
isGameStarted = true;
console.log("Game started!");
document.getElementById('replay-button-pc').textContent = 'リセット';
document.getElementById('replay-text-vr').setAttribute('troika-text', 'value', 'リセット');
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;');
box.dataset.physicsType = 'dynamic';
} else {
box.setAttribute('ammo-body', 'type: static;');
box.dataset.physicsType = 'static';
}
box.setAttribute('ammo-shape', 'type: box;');
objectContainer.appendChild(box);
}

function createSphere(position, isGravity, color) {
const radius = 0.045;
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.7;');
sphere.dataset.physicsType = 'dynamic';
} else {
sphere.setAttribute('ammo-body', 'type: static;');
sphere.dataset.physicsType = 'static';
}
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 = 10; block.dataset.objectType = 'accelerator-up'; break;
case 'right': color = '#f1c40f'; force = 150; block.dataset.objectType = 'accelerator-right'; break;
case 'left': color = '#e67e22'; force = 150; block.dataset.objectType = 'accelerator-left'; 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(snapSize, Math.round(position.y / snapSize) * snapSize),
z: Math.round(position.z / snapSize) * snapSize
};
block.setAttribute('position', snappedPosition);

// ammo-bodyを削除し、代わりにacceleratorコンポーネントのみを設定
// これにより物理的な壁ではなくなり、スカスカになる
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 = 1;
if (obj.tagName.toLowerCase() === 'a-box') {
snapSize = obj.getAttribute('width');
} else if (obj.tagName.toLowerCase() === 'a-sphere') {
snapSize = 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;

function toggleGrabMode() {
isLaserGrabMode = !isLaserGrabMode;
console.log("Grab mode toggled. Laser enabled:", isLaserGrabMode);
const lineEl = rightHand.querySelector('[line]');
const modeTextEl = document.getElementById('grab-mode-text-vr');
if (isLaserGrabMode) {
rightHand.setAttribute('laser-controls', 'enabled', true);
rightHand.setAttribute('hand-grab', 'enabled', false);
if (lineEl) lineEl.setAttribute('visible', true);
if (modeTextEl) modeTextEl.setAttribute('troika-text', 'value', 'ライン');
} else {
rightHand.setAttribute('laser-controls', 'enabled', false);
rightHand.setAttribute('hand-grab', 'enabled', true);
if (lineEl) lineEl.setAttribute('visible', false);
if (modeTextEl) modeTextEl.setAttribute('troika-text', 'value', 'ハンド');
}
}

function selectColor(newColor) {
selectedColor = newColor;
createGravityBoxPC.style.backgroundColor = newColor;
createGravitySpherePC.style.backgroundColor = newColor;
createGridBoxPC.style.backgroundColor = newColor;
createGridSpherePC.style.backgroundColor = newColor;
document.getElementById('create-gravity-box-vr').setAttribute('material', 'color', newColor);
document.getElementById('create-gravity-sphere-vr').setAttribute('material', 'color', newColor);
document.getElementById('create-grid-box-vr').setAttribute('material', 'color', newColor);
document.getElementById('create-grid-sphere-vr').setAttribute('material', 'color', newColor);
}

function changeBackground() {
findNextAvailableImage(backgroundIndex + 1);
}


// setupGame()関数の外、またはすぐ中に追加
function toggleGroundVisibility() {
const groundEl = document.getElementById('ground');
if (groundEl) {
const currentVisibility = groundEl.getAttribute('visible');
groundEl.setAttribute('visible', !currentVisibility);
}
}


function findNextAvailableImage(index) {
if (index >= 200) {
findNextAvailableImage(0);
return;
}
const img = new Image();
const imageUrl = `./pic/h${index + 1}.png`;
img.onload = function() {
backgroundIndex = index;
const skyEl = document.getElementById('sky');
if (skyEl) {
skyEl.setAttribute('src', imageUrl);
}
};
img.onerror = function() {
findNextAvailableImage(index + 1);
};
img.src = imageUrl;
}


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);
}



/**
* 指定された位置にあるオブジェクトをシーンから削除する関数
* @param {THREE.Vector3} targetPosition - 削除したいオブジェクトの中心座標
*/
function deleteObjectAtPosition(targetPosition) {
const objectContainer = document.getElementById('object-container');
const children = Array.from(objectContainer.children); // コンテナ内の全オブジェクトを取得
const tolerance = 0.01; // 座標の許容誤差

for (let i = 0; i < children.length; i++) {
const el = children[i];

// オブジェクトの現在位置と、指定された位置との距離を計算
const distance = el.object3D.position.distanceTo(targetPosition);

// 距離が非常に近ければ(誤差の範囲内であれば)、同じ位置にあると判断
if (distance < tolerance) {
// オブジェクトをシーンから削除
el.parentNode.removeChild(el);
console.log('オブジェクトを位置情報から削除しました:', targetPosition);
return; // 1つ見つけたら処理を終了
}
}
console.warn('指定された位置に削除対象のオブジェクトが見つかりませんでした:', targetPosition);
}


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

// --- Shiftキーでのコピー操作の後処理 ---
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);


// ▼▼▼ ここが最重要の修正ポイントです ▼▼▼
// mousedownで記録した data.isAccelerator を見て、処理を完全に分岐させます。
if (data.isAccelerator) {
// 【A】加速ブロックの場合の処理
// 元の位置に再生成
createAccelerator(originalPosition, data.accelType);
// キャンセルされていなければ、新しい位置にコピーを生成
if (!isCopyCancelled) {
createAccelerator(finalPosition, data.accelType);
}
} else {
// 【B】通常のブロックの場合の処理 (従来通り)
const creatorFn = data.shape === 'box' ? createBox : createSphere;
// 元の位置に再生成
creatorFn(originalPosition, data.isGravity, data.color);
// キャンセルされていなければ、新しい位置にコピーを生成
if (!isCopyCancelled) {
creatorFn(finalPosition, data.isGravity, data.color);
}
}
// ▲▲▲ これで、AかBのどちらか一方の処理しか実行されなくなります ▲▲▲

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 setupGame() {
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.05;
swatchVR.setAttribute('position', `${x} ${y} 0`);
swatchVR.dataset.color = color;
paletteContainerVR.appendChild(swatchVR);
}
selectColor(selectedColor);

sceneEl.addEventListener('mousedown', function(evt) {
if (sceneEl.is('vr-mode') || !isGameStarted) return;
const intersectedEl = evt.detail.intersectedEl;
if (intersectedEl && intersectedEl.classList.contains('color-swatch-pc')) {
selectColor(intersectedEl.style.backgroundColor);
return;
}
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, // 'up', 'left', 'right'のタイプも記録
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);
}
});

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 === 'toggle-ground-vr') {
toggleGroundVisibility();
return;
}

if (isBButtonPressed && hitEl.classList.contains('grabbable')) {
hitEl.parentNode.removeChild(hitEl);
return;
}
if (hitEl.id === 'change-background-vr') { changeBackground(); 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; }

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')
};
if (isGameStarted && createActions[hitEl.id]) {
createActions[hitEl.id]();
return;
}



// quest3のラインモードのA押しながらトリガーでつかんだのところ ここから

if (isLaserGrabMode && isGameStarted) {
if (hitEl.classList.contains('grabbable') && !grabbedObject) {
// ★★★ ここから修正 ★★★
if (isAButtonPressed) {
// 【コピー準備モード】
// 1. 元オブジェクトの情報を記録



// コピー対象が加速ブロックかどうかを判断
if (hitEl.hasAttribute('accelerator')) {
// 【A-1】加速ブロックの場合の情報を保存
copyOperationData = {
isAccelerator: true,
originalPosition: hitEl.object3D.position.clone(),
accelType: hitEl.getAttribute('accelerator').type
};
// 【A-2】元の位置に「仮の加速ブロック」をプレースホルダーとして生成
createAccelerator(copyOperationData.originalPosition, copyOperationData.accelType);

} else {
// 【B-1】通常のブロックの場合の情報を保存 (従来通り)
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
};
// 【B-2】元の位置に「仮の通常ブロック」をプレースホルダーとして生成
const creatorFn = copyOperationData.shape === 'box' ? createBox : createSphere;
creatorFn(copyOperationData.originalPosition, false, copyOperationData.color); // isGravityはfalseで固定
}

// 作ったばかりのプレースホルダーを記録しておく (共通)
copyOperationData.placeholderEl = objectContainer.lastChild;
}



// 4. 通常モードと全く同じく、掴むのは元のオブジェクト
grabbedObject = hitEl;
if (grabbedObject.hasAttribute('ammo-body')) {
// ドラッグ中は物理法則を無視する'kinematic'タイプに変更
grabbedObject.setAttribute('ammo-body', 'type', 'kinematic');
}
rightHand.object3D.attach(grabbedObject.object3D);
// ★★★ ここまで修正 ★★★
}
}

// quest3のラインモードのA押しながらトリガーでつかんだのところ ここまで

});







// quest3のラインモードのA押しながらトリガー離したところ ここから

rightHand.addEventListener('triggerup', function () {
if (isLaserGrabMode && grabbedObject) {
// ★★★ ここから修正 ★★★

// 【1. 通常モードと全く同じ解放処理】
// 掴んでいたオブジェクト(=元のオブジェクト)をドロップした位置に配置し、物理演算を元に戻す
objectContainer.object3D.attach(grabbedObject.object3D);



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

// 【2. コピーモードだった場合の追加処理】
if (copyOperationData) {
// (2-1) 元の位置に残したオブジェクトBを見つける
const placeholder = copyOperationData.placeholderEl;


if (placeholder) {
// (2) オブジェクトBを一度シーンから完全に削除する
placeholder.parentNode.removeChild(placeholder);
}

// (2-2) 保存した情報をもとに、正しいオブジェクトを元の位置に再生成
if (copyOperationData.isAccelerator) {
// 加速ブロックを再生成
createAccelerator(copyOperationData.originalPosition, copyOperationData.accelType);
} else {
// 通常のブロックを再生成
const creatorFn = copyOperationData.shape === 'box' ? createBox : createSphere;
creatorFn(copyOperationData.originalPosition, copyOperationData.isGravity, copyOperationData.color);
}


// (2-3) コピー情報をクリア
copyOperationData = null;
}

// 【3. 掴んでいる状態をリセット】
grabbedObject = null;
// ★★★ ここまで修正 ★★★
}
});


// quest3のラインモードのA押しながらトリガー離したところ ここまで





}

const createObjectPC = (creatorFn, isGravity) => {
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, isGravity);
}
};

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

const createAccelUpPC = document.getElementById('create-accel-up-pc');
createAccelUpPC.addEventListener('click', () => createObjectPC(createAccelerator, 'up'));
const createAccelRightPC = document.getElementById('create-accel-right-pc');
createAccelRightPC.addEventListener('click', () => createObjectPC(createAccelerator, 'right'));
const createAccelLeftPC = document.getElementById('create-accel-left-pc');
createAccelLeftPC.addEventListener('click', () => createObjectPC(createAccelerator, 'left'));

const changeBackgroundPC = document.getElementById('change-background-pc');
if (changeBackgroundPC) {
changeBackgroundPC.addEventListener('click', changeBackground);
}

const pcHud = document.getElementById('hud-pc');
const groundEl = document.getElementById('ground');
const arButton = document.getElementById('ar-button');
const vrButton = document.getElementById('vr-button');
const xrButtons = document.getElementById('xr-buttons');


// ★★★ 以下の2行を追加してください ★★★
const toggleGroundVR = document.getElementById('toggle-ground-vr');
const toggleGroundTextVR = document.getElementById('toggle-ground-text-vr');
// ★★★ ここまで ★★★

// ★★★ この2行を追加 ★★★
const changeBackgroundVR = document.getElementById('change-background-vr');
const changeBackgroundTextVR = document.getElementById('change-background-text-vr');


if (arButton) {
arButton.addEventListener('click', async () => {
pcHud.style.display = 'none';
xrButtons.style.display = 'none';

// ARモードに入るとき、VR用の地面ボタンだけを表示
if (toggleGroundVR) toggleGroundVR.setAttribute('visible', true);
if (toggleGroundTextVR) toggleGroundTextVR.setAttribute('visible', true);
if (changeBackgroundVR) changeBackgroundVR.setAttribute('visible', false);
if (changeBackgroundTextVR) changeBackgroundTextVR.setAttribute('visible', false);


const skyEl = document.getElementById('sky');
if (skyEl) { skyEl.setAttribute('visible', false); }
try {
await sceneEl.enterAR();
} catch (e) {
console.error("ARモードへの移行に失敗しました", e);
pcHud.style.display = 'block';
xrButtons.style.display = 'flex';
sceneEl.setAttribute('background', 'color', '#87CEEB');
if(groundEl) groundEl.setAttribute('visible', true);
if (toggleGroundVR) toggleGroundVR.setAttribute('visible', false);
if (toggleGroundTextVR) toggleGroundTextVR.setAttribute('visible', false);

}
});
}

if (vrButton) {
vrButton.addEventListener('click', async () => {
pcHud.style.display = 'none';
xrButtons.style.display = 'none';
sceneEl.setAttribute('background', 'color', '#87CEEB');
if(groundEl) groundEl.setAttribute('visible', true);
try {
await sceneEl.enterVR();
} catch (e) {
console.error("VRモードへの移行に失敗しました", e);
pcHud.style.display = 'block';
xrButtons.style.display = 'flex';
}
});
}

sceneEl.addEventListener('exit-vr', () => {
pcHud.style.display = 'block';
xrButtons.style.display = 'flex';

// ARモードから抜けるとき、VR用の地面ボタンだけを非表示
if (toggleGroundVR) toggleGroundVR.setAttribute('visible', false);
if (toggleGroundTextVR) toggleGroundTextVR.setAttribute('visible', false);
if (changeBackgroundVR) changeBackgroundVR.setAttribute('visible', true);
if (changeBackgroundTextVR) changeBackgroundTextVR.setAttribute('visible', true);


// 地面は必ず表示状態に戻す
if(groundEl) groundEl.setAttribute('visible', true);

const skyEl = document.getElementById('sky');
if (skyEl) { skyEl.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 )
alignSingleObject -------( Function )
alignSingleObject
ammoForce
ammoImpulse
angle
arButton
ateGravitySpherePC
background
backgroundColor
backgroundImages
backgroundIndex
bbox
block
body
box
camera
cameraDirection
cameraEl
cameraObject
cameraRight
changeBackground -------( Function )
changeBackgroundPC
changeBackgroundVR
children
class
closestDistSq
closestEl
collidingEls
color
controls
copyDragData
copyEl
copyOperationData
createAccelerator -------( Function )
createAccelLeftPC
createAccelRightPC
createAccelUpPC
createActions
createBox -------( Function )
createGravityBoxPC
createGridBoxPC
createGridSpherePC
createObjectPC
createSphere -------( Function )
creatorFn
currentVelocity
currentVisibility
cursor
data
deleteObjectAtPosition -------( Function )
deltaPosition
depth
desiredVelocity
direction
display
distance
distSq
dragClone
dragDistance
draggedObject
dt
dynamicEls
eatedObjectCounter
eftThumbstickInput
el
eraWorldQuaternion
euler
event) { if -------( Function )
evt) { if -------( Function )
ffectiveLerpFactor
finalPosition
findNextAvailableImage -------( Function )
force
forceVec
geBackgroundTextVR
geometry
ghtThumbstickInput
grabbableEls
grabbedEl
grabbedObject
groundEl
handleStartReplayClick -------( Function )
handPos
height
hitEl
hue
i
id
imageUrl
img
impulse
innerHTML
intersectedEl
intersectedEls
intersection
isAButtonPressed
isBButtonPressed
isColliding
isCopyCancelled
isCtrlKeyPressed
isGameStarted
isGravityEnabled
isInputting
isLaserGrabMode
isReady
isShiftKeyPressed
key
keys
leftHand
length
lerpFactor
leStartReplayClick
light
lineEl
material
modeTextEl
mouse
moveDirection
newPos
newY
objectContainer
objectType
objPos
onAButtonDown
onAButtonUp
onBButtonDown
onBButtonUp
onclick
onerror
onKeyDown
onKeyUp
onload
onMouseMove -------( Function )
onMouseUp -------( Function )
onTriggerDown
onTriggerUp
opacity
originalMaterial
originalPosition
originalType
otherBbox
otherEl) { if -------( Function )
otherEl
paletteContainerPC
paletteContainerVR
PALETTE_COLS
PALETTE_ROWS
pcHud
physics
physicsType
placeholder
placeholderEl
pos
position
radius
raycaster
raycasterComponent
renderer
replayGame -------( Function )
rigEl
rightHand
riginalAccelerator
rotation
rotationDegrees
sceneEl
selectColor -------( Function )
selectedColor
setupGame -------( Function )
shape
side
size
skyEl
snappedPosition
snapSize
sphere
src
startGame -------( Function )
style
swatchPC
swatchVR
text
textContent
toggleGrabMode -------( Function )
toggleGroundTextVR
toggleGroundVisibility -------( Function )
toggleGroundVR
tolerance
travelDir
type
ui
velocity
verticalMovement
visible
vrButton
vrCopyGrabData
webxr
width
worldPos
worldPosition
worldQuat
x
xrButtons
y
ZERO_VECTOR