First Commit.
This commit is contained in:
584
index.html
Normal file
584
index.html
Normal file
@@ -0,0 +1,584 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<title>Three.js Texture Editor</title>
|
||||
<style>
|
||||
body { margin: 0; overflow: hidden; }
|
||||
#texturePanel {
|
||||
position: absolute;
|
||||
top: 10px;
|
||||
right: 10px;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
z-index: 10;
|
||||
font-family: sans-serif;
|
||||
display: none;
|
||||
border-radius: 8px;
|
||||
}
|
||||
#texturePanel input {
|
||||
margin: 4px 0;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div id="materialPanel" style="position: absolute; top: 10px; right: 10px; background: #fff; padding: 10px; border-radius: 8px; width: 240px; z-index: 10;">
|
||||
<h3>Materials</h3>
|
||||
<input id="matName" placeholder="Material Name" style="width: 100%;" />
|
||||
<input id="matColor" type="color" style="width: 100%; margin-top: 5px;" />
|
||||
<button id="addMat" style="width: 100%; margin-top: 5px;">Add / Update</button>
|
||||
<div id="materialList" style="margin-top: 10px; max-height: 150px; overflow-y: auto;"></div>
|
||||
|
||||
<hr>
|
||||
<strong>Selected Material Properties</strong><br>
|
||||
<label>Base Color: <input type="color" id="colorPicker"></label><br>
|
||||
<label>Roughness: <input type="range" id="roughnessSlider" min="0" max="1" step="0.01"></label><br>
|
||||
<label>Metalness: <input type="range" id="metalnessSlider" min="0" max="1" step="0.01"></label><br>
|
||||
<label>Displacement: <input type="range" id="displacementSlider" min="0" max="0.1" step="0.001"></label><br>
|
||||
<label>Opacity: <input type="range" id="opacitySlider" min="0" max="1" step="0.01"></label><br>
|
||||
|
||||
<strong>Upload Textures</strong><br>
|
||||
<label>Color Map: <input type="file" id="mapInput"></label><br>
|
||||
<label>Normal Map: <input type="file" id="normalInput"></label><br>
|
||||
<label>Roughness Map: <input type="file" id="roughnessInput"></label><br>
|
||||
<label>Metalness Map: <input type="file" id="metalnessInput"></label><br>
|
||||
<label>Alpha Map: <input type="file" id="alphaInput"></label><br>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<button id="saveScene" style="position:fixed;bottom:80px;left:10px;">Save Scene</button>
|
||||
|
||||
|
||||
<div id="addShapePanel" style="
|
||||
position: fixed;
|
||||
bottom: 10px;
|
||||
left: 10px;
|
||||
background: #222;
|
||||
color: #fff;
|
||||
padding: 10px;
|
||||
border-radius: 8px;
|
||||
font-family: sans-serif;
|
||||
z-index: 10;
|
||||
">
|
||||
<label >Add Shape: </label>
|
||||
<select id="shapeSelect">
|
||||
<option value="">--Select--</option>
|
||||
<option value="box">Box</option>
|
||||
<option value="sphere">Sphere</option>
|
||||
<option value="cylinder">Cylinder</option>
|
||||
<option value="plane">Plane</option>
|
||||
</select>
|
||||
<button id="addShapeBtn">Add</button>
|
||||
</div>
|
||||
<script type="module">
|
||||
import * as THREE from 'https://esm.sh/three@0.160.0';
|
||||
import { OrbitControls } from 'https://esm.sh/three@0.160.0/examples/jsm/controls/OrbitControls.js';
|
||||
import { TransformControls } from 'https://esm.sh/three@0.160.0/examples/jsm/controls/TransformControls.js';
|
||||
import { EffectComposer } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/EffectComposer.js';
|
||||
import { RenderPass } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/RenderPass.js';
|
||||
import { SSAOPass } from 'https://esm.sh/three@0.160.0/examples/jsm/postprocessing/SSAOPass.js';
|
||||
|
||||
let scene, camera, renderer, composer, ssaoPass, controls, transformControls;
|
||||
let pointLight, raycaster, selectedObject = null;
|
||||
const mouse = new THREE.Vector2();
|
||||
const selectable = [];
|
||||
|
||||
const sceneObjects = []; // Store shape, position, rotation
|
||||
|
||||
const draggableMeshes = [];
|
||||
|
||||
init();
|
||||
animate();
|
||||
|
||||
function init() {
|
||||
scene = new THREE.Scene();
|
||||
camera = new THREE.PerspectiveCamera(70, window.innerWidth / window.innerHeight, 0.1, 100);
|
||||
camera.position.set(0, 1.5, 6);
|
||||
camera.lookAt(0, 0, 0);
|
||||
|
||||
renderer = new THREE.WebGLRenderer({ antialias: true });
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
renderer.shadowMap.enabled = true;
|
||||
renderer.shadowMap.type = THREE.PCFSoftShadowMap;
|
||||
renderer.toneMapping = THREE.ACESFilmicToneMapping;
|
||||
renderer.toneMappingExposure = 1.3;
|
||||
renderer.setClearColor(0x151515);
|
||||
document.body.appendChild(renderer.domElement);
|
||||
|
||||
controls = new OrbitControls(camera, renderer.domElement);
|
||||
|
||||
pointLight = new THREE.PointLight(0xffddaa, 18, 30, 1.5);
|
||||
pointLight.position.set(0, 1.5, 3.5);
|
||||
pointLight.castShadow = true;
|
||||
pointLight.shadow.mapSize.set(2048, 2048);
|
||||
pointLight.shadow.radius = 2;
|
||||
scene.add(pointLight);
|
||||
scene.add(new THREE.AmbientLight(0xffffff, 0.6));
|
||||
const bulb = new THREE.Mesh(new THREE.SphereGeometry(0.1), new THREE.MeshBasicMaterial({ color: 0xffddaa }));
|
||||
pointLight.add(bulb);
|
||||
scene.add(new THREE.HemisphereLight(0xfff0e0, 0x222244, 0.3));
|
||||
|
||||
const textureLoader = new THREE.TextureLoader();
|
||||
|
||||
const roomMaterial = new THREE.MeshPhysicalMaterial({
|
||||
map: textureLoader.load('tiles/Tiles133A_1K-JPG_Color.jpg'),
|
||||
normalMap: textureLoader.load('tiles/Tiles133A_1K-JPG_NormalGL.jpg'),
|
||||
roughnessMap: textureLoader.load('tiles/Tiles133A_1K-JPG_Roughness.jpg'),
|
||||
displacementMap: textureLoader.load('tiles/Tiles133A_1K-JPG_Displacement.jpg'),
|
||||
side: THREE.BackSide,
|
||||
metalness: 0.2,
|
||||
roughness: 0.6,
|
||||
displacementScale: 0.01
|
||||
});
|
||||
|
||||
const room = new THREE.Mesh(new THREE.BoxGeometry(6, 4, 6), roomMaterial);
|
||||
room.receiveShadow = true;
|
||||
scene.add(room);
|
||||
|
||||
const box = new THREE.Mesh(
|
||||
new THREE.BoxGeometry(2, 2, 2),
|
||||
new THREE.MeshStandardMaterial({
|
||||
color: 'rgb(34,139,34)',
|
||||
metalness: 0.2,
|
||||
roughness: 0.7
|
||||
})
|
||||
);
|
||||
box.position.set(1.5, 0, 0);
|
||||
box.castShadow = true;
|
||||
box.receiveShadow = true;
|
||||
selectable.push(box);
|
||||
scene.add(box);
|
||||
|
||||
loadSceneFromStorage();
|
||||
|
||||
composer = new EffectComposer(renderer);
|
||||
composer.addPass(new RenderPass(scene, camera));
|
||||
ssaoPass = new SSAOPass(scene, camera, window.innerWidth, window.innerHeight);
|
||||
ssaoPass.kernelRadius = 6;
|
||||
ssaoPass.minDistance = 0.02;
|
||||
ssaoPass.maxDistance = 0.15;
|
||||
composer.addPass(ssaoPass);
|
||||
|
||||
raycaster = new THREE.Raycaster();
|
||||
window.addEventListener('click', (event) => {
|
||||
mouse.x = (event.clientX / window.innerWidth) * 2 - 1;
|
||||
mouse.y = -(event.clientY / window.innerHeight) * 2 + 1;
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
|
||||
const intersects = raycaster.intersectObjects(selectable, true); // use `selectable` array
|
||||
if (intersects.length > 0) {
|
||||
const obj = intersects[0].object;
|
||||
|
||||
|
||||
|
||||
if (obj.material && obj.material.isMaterial) {
|
||||
selectedObject = obj;
|
||||
transformControls.attach(obj);
|
||||
document.getElementById('materialPanel').style.display = 'block';
|
||||
attachMaterialControls(obj.material);
|
||||
|
||||
// Optional: show material name if it's in your materials array
|
||||
const matIndex = materials.findIndex(m => m.material === obj.material);
|
||||
if (matIndex >= 0) {
|
||||
matNameInput.value = materials[matIndex].name;
|
||||
matColorInput.value = `#${obj.material.color.getHexString()}`;
|
||||
selectedMaterialIndex = matIndex;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
});
|
||||
|
||||
transformControls = new TransformControls(camera, renderer.domElement);
|
||||
transformControls.addEventListener('dragging-changed', e => controls.enabled = !e.value);
|
||||
scene.add(transformControls);
|
||||
|
||||
const tools = ['translate', 'rotate', 'scale', 'none'];
|
||||
tools.forEach(tool => {
|
||||
const btn = document.createElement('button');
|
||||
btn.textContent = tool.charAt(0).toUpperCase() + tool.slice(1);
|
||||
btn.style.cssText = 'position:fixed;top:10px;margin:2px;padding:6px;';
|
||||
btn.style.left = `${10 + tools.indexOf(tool) * 80}px`;
|
||||
btn.onclick = () => {
|
||||
if (tool === 'none') {
|
||||
transformControls.detach();
|
||||
} else {
|
||||
transformControls.setMode(tool);
|
||||
}
|
||||
};
|
||||
document.body.appendChild(btn);
|
||||
});
|
||||
|
||||
window.addEventListener('resize', () => {
|
||||
camera.aspect = window.innerWidth / window.innerHeight;
|
||||
camera.updateProjectionMatrix();
|
||||
renderer.setSize(window.innerWidth, window.innerHeight);
|
||||
composer.setSize(window.innerWidth, window.innerHeight);
|
||||
});
|
||||
|
||||
window.addEventListener('mousemove', (e) => {
|
||||
const x = (e.clientX / window.innerWidth) * 2 - 1;
|
||||
const y = -(e.clientY / window.innerHeight) * 2 + 1;
|
||||
pointLight.position.x = x * 3;
|
||||
pointLight.position.y = y * 1 + 1.5;
|
||||
});
|
||||
|
||||
// Texture Upload UI Setup
|
||||
const texturePanel = document.getElementById('texturePanel');
|
||||
const inputs = {
|
||||
map: document.getElementById('mapInput'),
|
||||
normalMap: document.getElementById('normalInput'),
|
||||
roughnessMap: document.getElementById('roughnessInput'),
|
||||
metalnessMap: document.getElementById('metalnessInput'),
|
||||
alphaMap: document.getElementById('alphaInput')
|
||||
};
|
||||
|
||||
const materials = [];
|
||||
|
||||
const matNameInput = document.getElementById("matName");
|
||||
const matColorInput = document.getElementById("matColor");
|
||||
const addMatBtn = document.getElementById("addMat");
|
||||
const matList = document.getElementById("materialList");
|
||||
|
||||
function refreshMaterialList() {
|
||||
matList.innerHTML = '';
|
||||
materials.forEach((entry, index) => {
|
||||
const div = document.createElement('div');
|
||||
div.textContent = entry.name;
|
||||
div.style.background = entry.material.color.getStyle();
|
||||
div.style.color = '#fff';
|
||||
div.style.padding = '5px';
|
||||
div.style.marginBottom = '5px';
|
||||
div.style.cursor = 'grab';
|
||||
div.draggable = true;
|
||||
|
||||
// Drag & Drop
|
||||
div.addEventListener('dragstart', (e) => {
|
||||
e.dataTransfer.setData('materialIndex', index);
|
||||
});
|
||||
|
||||
|
||||
|
||||
// Click to edit
|
||||
div.addEventListener('click', () => {
|
||||
matNameInput.value = entry.name;
|
||||
matColorInput.value = '#' + entry.material.color.getHexString();
|
||||
selectedMaterialIndex = index;
|
||||
|
||||
if (selectedObject) {
|
||||
selectedObject.material = entry.material.clone();
|
||||
attachMaterialControls(entry.material);
|
||||
}
|
||||
});
|
||||
|
||||
matList.appendChild(div);
|
||||
});
|
||||
}
|
||||
|
||||
addMatBtn.addEventListener('click', () => {
|
||||
const name = matNameInput.value.trim();
|
||||
const color = matColorInput.value;
|
||||
if (!name) return;
|
||||
|
||||
const hexColor = new THREE.Color(color);
|
||||
let mat;
|
||||
|
||||
if (selectedMaterialIndex >= 0) {
|
||||
// Update existing
|
||||
mat = materials[selectedMaterialIndex].material;
|
||||
mat.color = hexColor;
|
||||
materials[selectedMaterialIndex].name = name;
|
||||
} else {
|
||||
// Add new
|
||||
mat = new THREE.MeshStandardMaterial({ color: hexColor });
|
||||
materials.push({ name, material: mat });
|
||||
}
|
||||
|
||||
// Re-assign to selected object (optional)
|
||||
if (selectedObject) {
|
||||
selectedObject.material = mat;
|
||||
attachMaterialControls(mat);
|
||||
}
|
||||
|
||||
refreshMaterialList();
|
||||
matNameInput.value = '';
|
||||
selectedMaterialIndex = -1;
|
||||
});
|
||||
|
||||
let selectedMaterialIndex = -1;
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
renderer.domElement.addEventListener('dragover', (e) => {
|
||||
e.preventDefault();
|
||||
});
|
||||
|
||||
renderer.domElement.addEventListener('drop', (e) => {
|
||||
e.preventDefault();
|
||||
|
||||
const matIndex = e.dataTransfer.getData('materialIndex');
|
||||
if (matIndex === '') return;
|
||||
|
||||
// Convert screen coords to NDC
|
||||
const rect = renderer.domElement.getBoundingClientRect();
|
||||
const mouse = new THREE.Vector2(
|
||||
((e.clientX - rect.left) / rect.width) * 2 - 1,
|
||||
-((e.clientY - rect.top) / rect.height) * 2 + 1
|
||||
);
|
||||
|
||||
// Raycast
|
||||
raycaster.setFromCamera(mouse, camera);
|
||||
const intersects = raycaster.intersectObjects(draggableMeshes, true);
|
||||
console.log("intersects", intersects);
|
||||
if (intersects.length > 0) {
|
||||
const intersectedMesh = intersects[0].object;
|
||||
intersectedMesh.material = materials[matIndex].material.clone();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
|
||||
const loader = new THREE.TextureLoader();
|
||||
|
||||
function applyTexture(mapType, file) {
|
||||
|
||||
console.log(selectedObject);
|
||||
if (!selectedObject || !selectedObject.material) return;
|
||||
|
||||
const url = URL.createObjectURL(file);
|
||||
|
||||
const tex = new THREE.TextureLoader().load(url, () => {
|
||||
tex.wrapS = tex.wrapT = THREE.RepeatWrapping;
|
||||
tex.repeat.set(1, 1);
|
||||
selectedObject.material[mapType] = tex;
|
||||
selectedObject.material.needsUpdate = true;
|
||||
|
||||
if (mapType === 'alphaMap') {
|
||||
selectedObject.material.transparent = true;
|
||||
selectedObject.material.alphaTest = 0.5;
|
||||
}
|
||||
});
|
||||
}
|
||||
for (const mapType in inputs) {
|
||||
inputs[mapType].addEventListener('change', (e) => {
|
||||
if (e.target.files.length > 0) {
|
||||
applyTexture(mapType, e.target.files[0]);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
const colorInput = document.getElementById('colorPicker');
|
||||
const roughnessInput = document.getElementById('roughnessSlider');
|
||||
const metalnessInput = document.getElementById('metalnessSlider');
|
||||
const displacementInput = document.getElementById('displacementSlider');
|
||||
const opacityInput = document.getElementById('opacitySlider');
|
||||
|
||||
function updateUIFromMaterial(mat) {
|
||||
if (!mat) return;
|
||||
if (mat.color) colorInput.value = `#${mat.color.getHexString()}`;
|
||||
roughnessInput.value = mat.roughness ?? 0.5;
|
||||
metalnessInput.value = mat.metalness ?? 0.5;
|
||||
displacementInput.value = mat.displacementScale ?? 0;
|
||||
opacityInput.value = mat.opacity ?? 1;
|
||||
}
|
||||
|
||||
function attachMaterialControls(mat) {
|
||||
updateUIFromMaterial(mat);
|
||||
|
||||
colorInput.oninput = () => {
|
||||
mat.color.set(colorInput.value);
|
||||
mat.needsUpdate = true;
|
||||
};
|
||||
roughnessInput.oninput = () => {
|
||||
mat.roughness = parseFloat(roughnessInput.value);
|
||||
mat.needsUpdate = true;
|
||||
};
|
||||
metalnessInput.oninput = () => {
|
||||
mat.metalness = parseFloat(metalnessInput.value);
|
||||
mat.needsUpdate = true;
|
||||
};
|
||||
displacementInput.oninput = () => {
|
||||
mat.displacementScale = parseFloat(displacementInput.value);
|
||||
mat.needsUpdate = true;
|
||||
};
|
||||
opacityInput.oninput = () => {
|
||||
mat.opacity = parseFloat(opacityInput.value);
|
||||
mat.transparent = mat.opacity < 1;
|
||||
mat.needsUpdate = true;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
function saveSceneToStorage() {
|
||||
const data = sceneObjects.map(obj => ({
|
||||
type: obj.type,
|
||||
position: {
|
||||
x: obj.mesh.position.x,
|
||||
y: obj.mesh.position.y,
|
||||
z: obj.mesh.position.z
|
||||
},
|
||||
rotation: {
|
||||
x: obj.mesh.rotation.x,
|
||||
y: obj.mesh.rotation.y,
|
||||
z: obj.mesh.rotation.z
|
||||
}
|
||||
}));
|
||||
|
||||
console.log("save", data);
|
||||
localStorage.setItem('savedScene', JSON.stringify(data));
|
||||
}
|
||||
|
||||
|
||||
// Tool to add primitive shapes
|
||||
const shapeSelect = document.getElementById('shapeSelect');
|
||||
const addShapeBtn = document.getElementById('addShapeBtn');
|
||||
console.log("shapeSelect", shapeSelect);
|
||||
addShapeBtn.addEventListener('click', () => {
|
||||
const type = shapeSelect.value;
|
||||
if (!type) return;
|
||||
|
||||
let geometry;
|
||||
switch (type) {
|
||||
case 'box': geometry = new THREE.BoxGeometry(1, 1, 1); break;
|
||||
case 'sphere': geometry = new THREE.SphereGeometry(0.75, 32, 32); break;
|
||||
case 'cylinder': geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32); break;
|
||||
case 'plane': geometry = new THREE.PlaneGeometry(2, 2); break;
|
||||
}
|
||||
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.3, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.set(0, 0, 0);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
|
||||
draggableMeshes.push(mesh);
|
||||
|
||||
scene.add(mesh);
|
||||
selectable.push(mesh);
|
||||
transformControls.attach(mesh);
|
||||
selectedObject = mesh;
|
||||
|
||||
if (texturePanel) texturePanel.style.display = 'block';
|
||||
attachMaterialControls(mesh.material);
|
||||
|
||||
// ✅ Save object state
|
||||
sceneObjects.push({
|
||||
mesh,
|
||||
type,
|
||||
update() {
|
||||
this.position = mesh.position.clone();
|
||||
this.rotation = mesh.rotation.clone();
|
||||
},
|
||||
position: mesh.position.clone(),
|
||||
rotation: mesh.rotation.clone()
|
||||
});
|
||||
|
||||
saveSceneToStorage();
|
||||
});
|
||||
|
||||
function loadSceneFromStorage() {
|
||||
const saved = localStorage.getItem('savedScene');
|
||||
if (!saved) return;
|
||||
|
||||
const objects = JSON.parse(saved);
|
||||
for (const obj of objects) {
|
||||
let geometry;
|
||||
switch (obj.type) {
|
||||
case 'box': geometry = new THREE.BoxGeometry(1, 1, 1); break;
|
||||
case 'sphere': geometry = new THREE.SphereGeometry(0.75, 32, 32); break;
|
||||
case 'cylinder': geometry = new THREE.CylinderGeometry(0.5, 0.5, 1, 32); break;
|
||||
case 'plane': geometry = new THREE.PlaneGeometry(2, 2); break;
|
||||
}
|
||||
|
||||
const material = new THREE.MeshStandardMaterial({ color: 0x888888, metalness: 0.3, roughness: 0.7 });
|
||||
const mesh = new THREE.Mesh(geometry, material);
|
||||
mesh.position.set(obj.position.x, obj.position.y, obj.position.z);
|
||||
mesh.rotation.set(obj.rotation.x, obj.rotation.y, obj.rotation.z);
|
||||
mesh.castShadow = true;
|
||||
mesh.receiveShadow = true;
|
||||
|
||||
draggableMeshes.push(mesh);
|
||||
|
||||
scene.add(mesh);
|
||||
selectable.push(mesh);
|
||||
sceneObjects.push({
|
||||
type: obj.type,
|
||||
mesh,
|
||||
position: mesh.position.clone(),
|
||||
rotation: mesh.rotation.clone()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
transformControls.addEventListener('objectChange', () => {
|
||||
const moved = transformControls.object;
|
||||
sceneObjects.forEach(obj => {
|
||||
if (obj.mesh === moved) {
|
||||
obj.position = moved.position.clone();
|
||||
obj.rotation = moved.rotation.clone();
|
||||
}
|
||||
});
|
||||
saveSceneToStorage();
|
||||
});
|
||||
|
||||
document.getElementById('saveScene').addEventListener('click', () => {
|
||||
const data = sceneObjects.map(obj => ({
|
||||
type: obj.type,
|
||||
position: obj.position,
|
||||
rotation: {
|
||||
x: obj.rotation.x,
|
||||
y: obj.rotation.y,
|
||||
z: obj.rotation.z
|
||||
}
|
||||
}));
|
||||
|
||||
|
||||
|
||||
console.log([JSON.stringify(data)]);
|
||||
|
||||
const blob = new Blob([JSON.stringify(data, null, 2)], { type: 'application/json' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
a.href = url;
|
||||
a.download = 'scene.json';
|
||||
a.click();
|
||||
URL.revokeObjectURL(url);
|
||||
});
|
||||
|
||||
|
||||
function animate() {
|
||||
requestAnimationFrame(animate);
|
||||
controls.update();
|
||||
composer.render();
|
||||
}
|
||||
</script>
|
||||
|
||||
<script>
|
||||
window.addEventListener('DOMContentLoaded', () => {
|
||||
const shapeSelect = document.getElementById('shapeSelect');
|
||||
const addShapeBtn = document.getElementById('addShapeBtn');
|
||||
|
||||
console.log("shapeSelect:", shapeSelect);
|
||||
console.log("addShapeBtn:", addShapeBtn);
|
||||
|
||||
if (!addShapeBtn) {
|
||||
console.error("❌ Button not found in DOM!");
|
||||
return;
|
||||
}
|
||||
|
||||
addShapeBtn.addEventListener('click', (e) => {
|
||||
e.preventDefault();
|
||||
console.log("✅ Button click detected");
|
||||
});
|
||||
});
|
||||
</script>
|
||||
Reference in New Issue
Block a user