junkerstock
 vrz100k 

<!DOCTYPE html>
<html>
<head>
<title>VRテスト環境 Ver10.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 (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'].includes(event.code)) { this.keys[event.code] = true; } },
onKeyUp: function (event) { if (this.keys[event.code] !== undefined) { delete this.keys[event.code]; } }
});

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) {
// ご提案のロジックを実装: 属性を保管し、元の物体を一時的に物理実体でなくしてからコピーする
// 1. コピー操作の情報を準備
window.copyDragData = {
originalEl: closestEl,
originalPosition: closestEl.object3D.position.clone(),
physicsType: closestEl.dataset.physicsType,
isVr: true // VRからの操作であることを記録
};

// 2. 元のオブジェクトから物理属性を一時的に削除
closestEl.removeAttribute('ammo-body');

// 3. 物理属性がない状態でコピーを作成(これで衝突しない)
const dragClone = closestEl.cloneNode(true);

// 4. 掴む対象を、新しく作ったコピー(の抜け殻)に設定
this.grabbedEl = dragClone;
this.el.object3D.attach(this.grabbedEl.object3D);

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

// ★★★ ここからコピー完了処理を修正 ★★★
if (window.copyDragData && window.copyDragData.isVr) {
// コピー操作中の場合
const data = window.copyDragData;

// 1. 手から離してワールドに配置
this.objectContainer.object3D.attach(this.grabbedEl.object3D);

// 2. 最終的な位置を取得
const finalPosition = this.grabbedEl.object3D.position.clone();

// 3. 元の位置と比較して、コピーを確定するかキャンセルするか決める
if (finalPosition.distanceTo(data.originalPosition) < 0.1) {
// 位置がほとんど変わっていない場合、コピーをキャンセル
this.grabbedEl.parentNode.removeChild(this.grabbedEl);
} else {
// 位置が変わっている場合、コピーを確定
// コピー(掴んでいたオブジェクト)に物理属性を復元
this.grabbedEl.setAttribute('ammo-body', `type: ${data.physicsType};`);
this.grabbedEl.dataset.physicsType = data.physicsType;
// 必要であればIDを再設定
const originalType = this.grabbedEl.tagName.toLowerCase() === 'a-box' ? 'box' : 'sphere';
this.grabbedEl.setAttribute('id', `${originalType}-${window.createdObjectCounter++}`);
// 静的ならグリッドスナップ
if(data.physicsType === 'static') {
window.alignSingleObject(this.grabbedEl);
}
}

// 4. 元のオブジェクトの物理属性を復元
data.originalEl.setAttribute('ammo-body', `type: ${data.physicsType};`);

// 5. 状態をリセット
window.copyDragData = null;

} else {
// 通常の掴んで離す操作
this.objectContainer.object3D.attach(this.grabbedEl.object3D);
if (this.grabbedEl.hasAttribute('ammo-body')) {
const physicsType = this.grabbedEl.dataset.physicsType || 'dynamic';
if (physicsType === 'static') {
window.alignSingleObject(this.grabbedEl);
}
this.grabbedEl.setAttribute('ammo-body', 'type', physicsType);
}
}
// ★★★ ここまでコピー完了処理を修正 ★★★
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;
}

#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>
</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></a-assets>

<a-entity id="rig" position="0 0 5"
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 1.6 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.22" color="#FAFAFA" opacity="0.8" side="double"></a-plane>

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

<a-box id="replay-button-vr" class="clickable" position="-0.15 -0.07 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.07 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.07 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.07 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.07 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.07 0.01" material="color: #27AE60; shader: flat"></a-sphere>

<a-box id="toggle-grab-mode-vr" class="clickable" position="0.15 -0.07 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.07 0.016"></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-plane id="ground" position="0 0 0" rotation="-90 0 0" width="500" height="500" color="#7BC8A4"
ammo-body="type: static;"
ammo-shape="type: box; halfExtents: 250 0.05 250;"></a-plane>

<a-entity id="object-container"></a-entity>
</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;

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 0 5');
rigEl.setAttribute('rotation', '0 0 0');
}

function createBox(position, isGravity) {
const box = document.createElement('a-box');
box.setAttribute('id', `box-${window.createdObjectCounter++}`);
box.classList.add('grabbable');
box.setAttribute('position', position);
box.setAttribute('width', 0.1);
box.setAttribute('height', 0.1);
box.setAttribute('depth', 0.1);
box.setAttribute('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) {
const sphere = document.createElement('a-sphere');
sphere.setAttribute('id', `sphere-${window.createdObjectCounter++}`);
sphere.classList.add('grabbable');
sphere.setAttribute('position', position);
sphere.setAttribute('radius', 0.05);
sphere.setAttribute('color', selectedColor);

if (isGravity) {
sphere.setAttribute('ammo-body', 'type: dynamic; mass: 2;');
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 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 = obj.getAttribute('radius') * 2;
}
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 onMouseMove(event) {
if (!draggedObject) return;
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
mouse.y = - (event.clientY / window.innerHeight) * 2 + 1;
const camera = sceneEl.camera;
raycaster.setFromCamera(mouse, camera);
raycaster.ray.at(dragDistance, worldPosition);
draggedObject.object3D.parent.worldToLocal(worldPosition);
draggedObject.object3D.position.copy(worldPosition);
}

// ★★★ ここからマウスを離した際のコピー完了処理を全面的に修正 ★★★
function onMouseUp() {
if (!draggedObject) return;

if (window.copyDragData) {
// コピー操作中の場合
const data = window.copyDragData;

// 1. 最終的な位置をグリッドに合わせる
alignSingleObject(draggedObject);
const finalPosition = draggedObject.object3D.position.clone();

// 2. 元の位置と比較して、コピーを確定するかキャンセルするか決める
if (finalPosition.distanceTo(data.originalPosition) < 0.01) {
// 位置がほとんど変わっていない場合、コピーをキャンセル
draggedObject.parentNode.removeChild(draggedObject);
} else {
// 位置が変わっている場合、コピーを確定
// コピー(掴んでいたオブジェクト)に物理属性を復元
draggedObject.setAttribute('ammo-body', `type: ${data.physicsType};`);
draggedObject.dataset.physicsType = data.physicsType;
const originalType = draggedObject.tagName.toLowerCase() === 'a-box' ? 'box' : 'sphere';
draggedObject.setAttribute('id', `${originalType}-${window.createdObjectCounter++}`);
}

// 3. 元のオブジェクトの物理属性を復元
data.originalEl.setAttribute('ammo-body', `type: ${data.physicsType};`);

// 4. 状態をリセット
window.copyDragData = null;

} else {
// 通常の掴んで離す操作
const physicsType = draggedObject.dataset.physicsType || 'dynamic';
if (physicsType === 'static') {
alignSingleObject(draggedObject);
}
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) {
// ご提案のロジックを実装: 属性を保管し、元の物体を一時的に物理実体でなくしてからコピーする
// 1. コピー操作の情報を準備
window.copyDragData = {
originalEl: intersectedEl,
originalPosition: intersectedEl.object3D.position.clone(),
physicsType: intersectedEl.dataset.physicsType,
isVr: false // PCからの操作であることを記録
};

// 2. 元のオブジェクトから物理属性を一時的に削除
intersectedEl.removeAttribute('ammo-body');

// 3. 物理属性がない状態でコピーを作成(これで衝突しない)
const dragClone = intersectedEl.cloneNode(true);
objectContainer.appendChild(dragClone);

// 4. 掴む対象を、新しく作ったコピー(の抜け殻)に設定
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; });

// ★★★ VRのトリガー操作はhand-grabコンポーネントに移植したため、ここでは削除 ★★★
// (hand-grabコンポーネント内でPC版と同様のロジックを実装済み)
}

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

if (arButton) {
arButton.addEventListener('click', async () => {
pcHud.style.display = 'none';
xrButtons.style.display = 'none';
sceneEl.setAttribute('background', 'transparent', true);
if(groundEl) groundEl.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 (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';
sceneEl.setAttribute('background', 'color', '#87CEEB');
if(groundEl) groundEl.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; }); // ★★★ VRのトリガー操作はhand-grabコンポーネントに移植したため、ここでは削除 ★★★ // -------( Function )
) { isBButtonPressed = true; }); rightHand.addEventListener -------( Function )
) { this.isAButtonPressed = true; }, onAButtonUp: function -------( Function )
) { this.isBButtonPressed = true; }, onBButtonUp: function -------( Function )
alignSingleObject -------( Function )
alignSingleObject
arButton
ateGravitySpherePC
background
backgroundColor
body
box
camera
cameraDirection
cameraEl
cameraObject
cameraRight
class
closestDistSq
closestEl
color
controls
copyDragData
createBox -------( Function )
createGravityBoxPC
createGridBoxPC
createGridSpherePC
createObjectPC
createSphere -------( Function )
currentVelocity
cursor
data
deltaPosition
depth
desiredVelocity
direction
display
distSq
dragClone
dragDistance
draggedObject
dt
eatedObjectCounter
eftThumbstickInput
el
eraWorldQuaternion
event) { if -------( Function )
evt) { if -------( Function )
ffectiveLerpFactor
finalPosition
geometry
ghtThumbstickInput
grabbableEls
grabbedEl
grabbedObject
groundEl
handleStartReplayClick -------( Function )
handPos
height
hue
i
id
innerHTML
intersectedEl
intersection
isAButtonPressed
isBButtonPressed
isCtrlKeyPressed
isGameStarted
isInputting
isLaserGrabMode
isReady
isShiftKeyPressed
key
keys
leftHand
lerpFactor
leStartReplayClick
light
lineEl
material
modeTextEl
mouse
moveDirection
newPos
newY
objectContainer
objPos
onAButtonDown
onAButtonUp
onBButtonDown
onBButtonUp
onclick
onKeyDown
onKeyUp
onMouseMove -------( Function )
onMouseUp -------( Function )
onTriggerDown
onTriggerUp
opacity
originalType
paletteContainerPC
paletteContainerVR
PALETTE_COLS
PALETTE_ROWS
pcHud
physics
physicsType
pos
position
radius
raycaster
renderer
replayGame -------( Function )
rigEl
rightHand
rotation
sceneEl
selectColor -------( Function )
selectedColor
setupGame -------( Function )
shape
side
snapSize
sphere
src
startGame -------( Function )
swatchPC
swatchVR
text
textContent
toggleGrabMode -------( Function )
ui
verticalMovement
vrButton
webxr
width
worldPos
worldPosition
x
xrButtons
y
ZERO_VECTOR