junkerstock
 vrz120 

<!DOCTYPE html>
<html>
<head>
<title>VRテスト環境 Ver12.0 半透明 順 反 追加</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);
}
}
});
},
// オブジェクトに力を加える関数



// acceleratorコンポーネントの中のapplyEffect関数

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: // 'left' と 'right'
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);

// ★★★ ここが最重要の修正点です ★★★
// 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 #hud-main-content-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; }
#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ボタン (変更なし) --- */
#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; }

</style>

</head>
<body>

<div id="hud-pc">
<div id="hud-main-content-pc">
<!-- Row 1: 操作ボタン -->
<div class="hud-buttons">
<div id="replay-button-pc" onclick="handleStartReplayClick()">スタート</div>
<div id="change-background-pc">背景</div>
<div id="tidy-up-pc">整理</div>
</div>

<!-- Row 2: カラーパレット -->
<div id="color-palette-pc" class="color-palette-pc"></div>

<!-- Row 3: オブジェクト生成ボタン -->
<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>

<!-- Row 4: 加速ブロック生成ボタン -->
<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 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;">

<!-- ここからVRのパネルUI -->

<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="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="change-background-vr" class="clickable" position="-0.05 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.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.17 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.17 0.016"></a-entity>

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

<a-box id="toggle-ground-vr" class="clickable" visible="false"
width="0.07" height="0.03" depth="0.01" position="-0.05 0.17 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.05 0.17 0.016" visible="false"></a-entity>


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


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


<a-box id="create-accel-up-vr" class="clickable" width="0.07" height="0.03" depth="0.01" position="-0.16 -0.15 0.01" material="color: #e74c3c; shader: flat"></a-box>
<a-entity troika-text="value: 上; color: black; fontSize: 0.012;" position="-0.16 -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.08 -0.15 0.01" material="color: #f1c40f; shader: flat"></a-box>
<a-entity troika-text="value: 右; color: black; fontSize: 0.012;" position="-0.08 -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 -0.15 0.01" material="color: #e67e22; shader: flat"></a-box>
<a-entity troika-text="value: 左; color: black; fontSize: 0.012;" position="0 -0.15 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.15 0.01" material="color: #3498db; shader: flat"></a-box>
<a-entity troika-text="value: 順; color: black; fontSize: 0.012;" position="0.08 -0.15 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.15 0.01" material="color: #2ecc71; shader: flat"></a-box>
<a-entity troika-text="value: 反; color: black; fontSize: 0.012;" position="0.16 -0.15 0.016"></a-entity>


</a-entity>

<!-- ここからVRのパネルUI ここまで -->

</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 -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 = 'リセット';

// startGame()関数の中に入れる、ボタンを変化させる処理

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

// 1. ボタンを小さくして左に移動させ、色を赤系のリセットカラーに変える
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 });
}

// 2. テキストを「リセット」に変更し、サイズと位置をボタンに合わせる
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) {
// 左手の3Dモデルの全パーツを検索して非表示にする
leftHand.object3D.traverse(function (node) {
if (node.isSkinnedMesh) {
node.visible = false;
}
});
}

// ★★★ ここからが追加・修正箇所です ★★★
const rightHand = document.getElementById('rightHand');
if (rightHand) {
// 1. 右手の3Dモデルを探して「非表示」にする
rightHand.object3D.traverse(function (node) {
if (node.isSkinnedMesh) {
node.visible = false;
}
});
}
// 2. 短い棒(シリンダー)を「表示」する
const handPointer = document.getElementById('hand-pointer-visual');
if (handPointer) {
handPointer.setAttribute('visible', true);
}
// ★★★ ここまで ★★★

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


// 滑りやすいかどうかの値 angularDamping:0.75で、しばらくしたら止まる。 0.7でちょっと動くがとまらない
if (isGravity) {
sphere.setAttribute('ammo-body', 'type: dynamic; mass: 2; angularDamping: 0.3;');
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 = 5; break; // 赤
case 'right': color = '#f1c40f'; force = 50; break; // 黄
case 'left': color = '#e67e22'; force = 50; break; // オレンジ
case 'straight': color = '#3498db'; force = 50; break; // 青 ()
case 'reverse': color = '#2ecc71'; force = 50; 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 = 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;





// メインの<script>タグのトップレベル(関数外)で変数を準備
let handModelVisual = null;

function toggleGrabMode() {
isLaserGrabMode = !isLaserGrabMode;
console.log("Grab mode toggled. Laser enabled:", 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');
console.log('手のモデルを探しました:', handModelVisual);
}
// ★★★ ここまで ★★★

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;

// ★★★ ここからが修正箇所です ★★★
// VR用ボタンのmaterial属性を、shaderとcolorをセットで更新します
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 changeBackground() {
findNextAvailableImage(backgroundIndex + 1);
}


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




// 11.7追加
function stopObjectMotion(el) {
if (el.body) {
const zeroVector = new Ammo.btVector3(0, 0, 0);
el.body.setLinearVelocity(zeroVector);
el.body.setAngularVelocity(zeroVector);
}
}




/**
* シーンを整理する新しいメイン関数 (優先順位付き)
*/
function tidyUpScene() {
const objectContainer = document.getElementById('object-container');
const children = Array.from(objectContainer.children);
const objectBlueprints = [];

// --- フェーズ1: 記録 (変更なし) ---
console.log('整理開始: オブジェクト情報を記録中...');
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);
});

// --- フェーズ2: 全削除 (変更なし) ---
console.log('全オブジェクトを削除します...');
objectContainer.innerHTML = '';

// ★★★ ここからが修正箇所です ★★★
// --- フェーズ3: 優先順位に従って、ループを3回に分けて再配置 ---
console.log('オブジェクトを優先順位に従って再配置します...');
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);
}
};

// ループ1: 半透明ブロック (最優先)
objectBlueprints.filter(bp => bp.type === 'accelerator').forEach(placeObject);

// ループ2: 重力のないブロック
objectBlueprints.filter(bp => bp.type !== 'accelerator' && !bp.isGravity).forEach(placeObject);

// ループ3: 重力のあるブロック (最後)
objectBlueprints.filter(bp => bp.type !== 'accelerator' && bp.isGravity).forEach(placeObject);

console.log('整理完了。');
// ★★★ ここまで ★★★
}





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

// ▼ このif文を追加 ▼
if (hitEl.id === 'tidy-up-vr') {
tidyUpScene();
return;
}

// 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'),
'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;
}



// 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 createAccelStraightPC = document.getElementById('create-accel-straight-pc');
createAccelStraightPC.addEventListener('click', () => createObjectPC(createAccelerator, 'straight'));
const createAccelReversePC = document.getElementById('create-accel-reverse-pc');
createAccelReversePC.addEventListener('click', () => createObjectPC(createAccelerator, 'reverse'));


// ★★★ ここに追加します ★★★
const tidyUpPC = document.getElementById('tidy-up-pc');
if (tidyUpPC) {
tidyUpPC.addEventListener('click', tidyUpScene);
}
// ★★★ ここまで ★★★

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);toggleGroundVR.classList.add('clickable');
if (toggleGroundTextVR) toggleGroundTextVR.setAttribute('visible', true);
if (changeBackgroundVR) changeBackgroundVR.setAttribute('visible', false);changeBackgroundVR.classList.remove('clickable');
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);toggleGroundVR.classList.remove('clickable');
if (toggleGroundTextVR) toggleGroundTextVR.setAttribute('visible', false);
if (changeBackgroundVR) changeBackgroundVR.setAttribute('visible', true);changeBackgroundVR.classList.add('clickable');
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 )
accelType
alignSingleObject -------( Function )
alignSingleObject
angle
arButton
ateAccelStraightPC
ateGravitySpherePC
background
backgroundColor
backgroundImages
backgroundIndex
bbox
block
blueprint
body
box
bp
camera
cameraDirection
cameraEl
cameraObject
cameraRight
ccupiedCoordinates
changeBackground -------( Function )
changeBackgroundPC
changeBackgroundVR
children
class
closestDistSq
closestEl
collidingEls
color
controls
coordKey
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
eateAccelReversePC
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 )
handModelVisual
handPointer
handPos
height
hitEl
hue
i
id
imageUrl
img
impulse
innerHTML
intersectedEl
intersectedEls
intersection
isAButtonPressed
isBButtonPressed
isColliding
isCopyCancelled
isCtrlKeyPressed
isGameStarted
isGravity
isGravityEnabled
isInputting
isLaserGrabMode
isReady
isShiftKeyPressed
key
keys
laserLine
leftHand
length
lerpFactor
leStartReplayClick
light
material
materialConfig
modeTextEl
mouse
moveDirection
newPos
newY
objectBlueprints
objectContainer
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
placeObject
pos
position
radius
raycaster
raycasterComponent
renderer
replayButtonVR
replayGame -------( Function )
replayTextVR
reverseDir
reverseForce
rigEl
rightHand
rightHandEl
riginalAccelerator
rotation
rotationDegrees
sceneEl
selectColor -------( Function )
selectedColor
setupGame -------( Function )
shape
side
size
skyEl
snappedPosition
snapSize
sphere
src
startGame -------( Function )
stopObjectMotion -------( Function )
straightDir
straightForce
swatchPC
swatchVR
targetPosition
text
textContent
tidyUpPC
tidyUpScene -------( Function )
toggleGrabMode -------( Function )
toggleGroundTextVR
toggleGroundVisibility -------( Function )
toggleGroundVR
tolerance
travelDir
type
ui
velocity
verticalMovement
visible
vrButton
vrCopyGrabData
webxr
width
worldPos
worldPosition
worldQuat
x
xrButtons
y
zeroVector
ZERO_VECTOR