diff --git a/app/script/Dragger.js b/app/script/Dragger.js new file mode 100644 index 0000000..30dc47e --- /dev/null +++ b/app/script/Dragger.js @@ -0,0 +1,82 @@ +import * as THREE from 'three'; +import * as EFSim from "./Init.js"; + + +// 点電荷をドラッグして移動させるクラス +export class Dragger { + constructor(positions, objects, camera, dom, controls, scene) { + + this.positions = positions; + this.objects = objects; + this.camera = camera; + this.dom = dom; + this.controls = controls; + this.scene = scene; + + // ドラッグでオブジェクトを移動するためのコントロール + this.trans_controls = EFSim.CreateTransformControls(camera, dom, controls, scene); + + this.ray = new THREE.Raycaster(); + this.pointer = new THREE.Vector2(); + + this.listeners = []; + + this.selected = null; + + this.on_down_position = new THREE.Vector2(); + this.on_up_position = new THREE.Vector2(); + + this.setEvent(); + } + + setEvent = () => { + const onClick = (event) => { + this.pointer.x = (event.clientX / this.dom.offsetWidth) * 2 - 1; + this.pointer.y = -(event.clientY / this.dom.offsetHeight) * 2 + 1; + + // 現在のカメラの位置からクリックした位置に向かう光線を作成 + this.ray.setFromCamera(this.pointer, this.camera); + + // 光線との交差判定 + const intersects = this.ray.intersectObjects(this.objects, false); + + if (intersects.length > 0) { + const object = intersects[0].object; + + if (object !== this.trans_controls.object) { + this.trans_controls.attach(object); + this.selected = object; + } + } + }; + this.dom.addEventListener('click', onClick); + + // コントロール外をクリックすることで選択を解除する + // (オブジェクトを移動させなかった場合に、選択を解除する) + // https://github.com/mrdoob/three.js/blob/master/examples/webgl_geometry_spline_editor.html + document.addEventListener('pointerdown', (event) => { + this.on_down_position.x = event.clientX; + this.on_down_position.y = event.clientY; + }); + document.addEventListener('pointerup', (event) => { + this.on_up_position.x = event.clientX; + this.on_up_position.y = event.clientY; + if (this.on_down_position.distanceTo(this.on_up_position) === 0) { + this.trans_controls.detach(); + } + }); + + // 座標を更新 + this.trans_controls.addEventListener('change', () => { + this.positions.forEach((position, index) => { + position.copy(this.objects[index].position); + }); + }); + } + + // オブジェクトがドラッグされたときのイベント + addEventListener = (type, listener) => { + this.trans_controls.addEventListener(type, listener); + } + +} \ No newline at end of file diff --git a/app/script/Field3D.js b/app/script/Field3D.js index b9e6fed..fa2d9f4 100644 --- a/app/script/Field3D.js +++ b/app/script/Field3D.js @@ -1,30 +1,28 @@ import * as THREE from 'three'; // THREE.Vector3 が等しいかどうかを判定 -const EqualsVector3 = (v1, v2, eps = Number.EPSILON) => { +const EqualsVector = (v1, v2, eps = Number.EPSILON) => { return ( Math.abs(v1.x - v2.x) < eps && Math.abs(v1.y - v2.y) < eps && Math.abs(v1.z - v2.z) < eps); }; -// 1つの点電荷からの電界ベクトルを算出 +// 1つの点電荷から受ける電界ベクトルを算出 // @param pos 観測点の座標 // @param point_charge 点電荷 -const EFVector = (pos, point_charge) => { - // 観測点が点電荷と重なっている場合 - if (EqualsVector3(pos, point_charge.pos)) - { - return new THREE.Vector3(); +const EFVectorFromSingleCharge = (pos, point_charge) => { + + if (EqualsVector(pos, point_charge.pos)) { + return new THREE.Vector3(); // 観測点が点電荷と重なっている場合 } const diff = new THREE.Vector3(); diff.subVectors(pos, point_charge.pos); // 点電荷と観測点との差分 const k = 8.987552 * 10 ** 9; // クーロン定数 - const r_sq = - diff.lengthSq(); // 点電荷と観測点との距離^2 (平方根処理をなくすため) + const r_sq4 = diff.lengthSq() ** 2; // 点電荷と観測点との距離^4 - diff.multiplyScalar((k * point_charge.charge) / r_sq ** 2); // クーロン力算出 + diff.multiplyScalar((k * point_charge.charge) / r_sq4); // クーロン力算出 return diff; // ベクトルにかける係数 }; @@ -32,125 +30,122 @@ const EFVector = (pos, point_charge) => { // 指定座標における電場ベクトルを計算 // @param pos 観測点の座標 // @param point_charges 点電荷の配列 -const EFieldVectors = (pos, point_charges) => { +const EFVector = (pos, point_charges) => { let electric_field_vector = new THREE.Vector3(); - for (const point_charge of point_charges) - { - electric_field_vector.add(EFVector(pos, point_charge)); + for (const point_charge of point_charges) { + electric_field_vector.add(EFVectorFromSingleCharge(pos, point_charge)); } return electric_field_vector; }; /// 電気力線 -class ElectricLines3D extends THREE.Object3D -{ - constructor(point_charges) - { +class ElectricLines3D extends THREE.Object3D { + constructor(point_charges) { super(); this.point_charges = point_charges; + this.line_material = new THREE.LineBasicMaterial({ color: 0xaaaaff }); + this.createField(point_charges); } /// @param origin 始点 (PointCharge) - createElectricLineGeometry = - (origin_charge, vector, point_charges, length) => { - const points = [ origin_charge.pos.clone() ]; - const _origin = origin_charge.pos.clone(); - const _vector = vector.clone(); - _vector.normalize(); - _origin.add(_vector); - for (let i = 0; i < length; i++) - { - const d_vector = EFieldVectors(_origin, point_charges); - d_vector.normalize(); - if (origin_charge.charge < 0) - d_vector.multiplyScalar(-1); - // 点電荷との衝突判定 - for (const point_charge of point_charges) - { - const diff = new THREE.Vector3(); - diff.subVectors(_origin, point_charge.pos); - if (diff.length() < 1) - { - return new THREE.BufferGeometry().setFromPoints(points); - } + createElectricLineGeometry = (origin_charge, vector, point_charges, length) => { + const points = [origin_charge.pos.clone()]; + const _origin = origin_charge.pos.clone(); + const _vector = vector.clone(); + _vector.normalize(); + _origin.add(_vector); + for (let i = 0; i < length; i++) { + const d_vector = EFVector(_origin, point_charges); + d_vector.normalize(); + if (origin_charge.charge < 0) + d_vector.multiplyScalar(-1); + // 点電荷との衝突判定 + for (const point_charge of point_charges) { + const diff = new THREE.Vector3(); + diff.subVectors(_origin, point_charge.pos); + if (diff.length() < 1) { + return new THREE.BufferGeometry().setFromPoints(points); } - _origin.add(d_vector); - points.push(_origin.clone()); } - return new THREE.BufferGeometry().setFromPoints(points); - }; - - createField(point_charges) - { - const line_material = new THREE.LineBasicMaterial({ color : 0xaaaaff }); - for (const point_charge of point_charges) - { - for (let n_theta = 0; n_theta < 10; n_theta++) - { - for (let n_phi = 0; n_phi < 10; n_phi++) - { + _origin.add(d_vector); + points.push(_origin.clone()); + } + return new THREE.BufferGeometry().setFromPoints(points); + }; + + createField(point_charges) { + for (const point_charge of point_charges) { + for (let n_theta = 0; n_theta < 10; n_theta++) { + for (let n_phi = 0; n_phi < 10; n_phi++) { const theta = ((Math.PI * 2) / 10) * n_theta; - const phi = ((Math.PI * 2) / 10) * n_phi; - const x = 10 * Math.sin(theta) * Math.cos(phi); - const y = 10 * Math.sin(theta) * Math.sin(phi); - const z = 10 * Math.cos(theta); + const phi = ((Math.PI * 2) / 10) * n_phi; + const x = 10 * Math.sin(theta) * Math.cos(phi); + const y = 10 * Math.sin(theta) * Math.sin(phi); + const z = 10 * Math.cos(theta); const line = new THREE.Line( - this.createElectricLineGeometry( - point_charge, new THREE.Vector3(x, y, z), point_charges, - 1000), - line_material); + this.createElectricLineGeometry(point_charge, new THREE.Vector3(x, y, z), point_charges, 1000), + this.line_material); this.add(line); } } } } + + update() { + this.children = []; + this.createField(this.point_charges); + } } // 点電荷 -class PointCharges3D extends THREE.Object3D -{ - constructor(point_charges) - { +class PointCharges3D extends THREE.Object3D { + constructor(point_charges) { super(); - const geometry = new THREE.SphereGeometry(1, 32, 32); - const material_minus = new THREE.MeshBasicMaterial({ color : 0x0000ff }); - const material_plus = new THREE.MeshBasicMaterial({ color : 0xff0000 }); - for (const point_charge of point_charges) - { - const material = point_charge.charge > 0 ? material_plus : material_minus; - const sphere = new THREE.Mesh(geometry, material); + this.point_charges = point_charges; + this.geometry = new THREE.SphereGeometry(1, 32, 32); + this.material_minus = new THREE.MeshBasicMaterial({ color: 0x0000ff }); + this.material_plus = new THREE.MeshBasicMaterial({ color: 0xff0000 }); + this.createPointChargesGeometry(); + } + + createPointChargesGeometry = () => { + for (const point_charge of this.point_charges) { + const material = point_charge.charge > 0 ? this.material_plus : this.material_minus; + const sphere = new THREE.Mesh(this.geometry, material); sphere.position.copy(point_charge.pos); this.add(sphere); } } + + update() { + this.children = []; + this.createPointChargesGeometry(); + } } // 電界ベクトル -class ElectricFieldVectors3D extends THREE.Object3D -{ - constructor(point_charges) - { +class ElectricFieldVectors3D extends THREE.Object3D { + constructor(point_charges) { super(); this.point_charges = point_charges; + this.createEFVectorGeometry(); + } - for (let point_charge of point_charges) - { - for (let x = -4; x <= 4; x++) - { - for (let y = -4; y <= 4; y++) - { - for (let z = -4; z <= 4; z++) - { + createEFVectorGeometry = () => { + for (let point_charge of this.point_charges) { + for (let x = -4; x <= 4; x++) { + for (let y = -4; y <= 4; y++) { + for (let z = -4; z <= 4; z++) { if (x ** 2 + y ** 2 + z ** 2 > 5 ** 2) continue; // if (x === 0 && y === 0 && z === 0) continue; const origin = new THREE.Vector3(x * 10, y * 10, z * 10); origin.add(point_charge.pos); - const electric_field_vector = EFieldVectors(origin, point_charges); + const electric_field_vector = EFVector(origin, this.point_charges); const len = electric_field_vector.length(); this.add(new THREE.ArrowHelper(electric_field_vector.normalize(), origin, 1, 0xffffff, 2, 0.5)); @@ -159,94 +154,74 @@ class ElectricFieldVectors3D extends THREE.Object3D } } } + + update() { + this.children = []; + this.createEFVectorGeometry(); + } } /// 電界 -export class Field3D extends THREE.Object3D -{ - constructor(dom, camera, trans_controls, point_charges) - { +export class Field3D extends THREE.Object3D { + constructor(dom, camera, trans_controls, point_charges) { super(); - this.dom = dom; - this.camera = camera; + this.dom = dom; + this.camera = camera; this.trans_controls = trans_controls; - this.point_charges = point_charges; - this.enablePointCharges(true); - this.initEvents(); - - this.raycaster = new THREE.Raycaster(); - this.pointer = new THREE.Vector2(); - } - - initEvents = () => { - const onPointerMove = (event) => { - this.pointer.x = (event.clientX / this.dom.offsetWidth) * 2 - 1; - this.pointer.y = -(event.clientY / this.dom.offsetHeight) * 2 + 1; - - this.raycaster.setFromCamera(this.pointer, this.camera); - - const intersects = this.raycaster.intersectObjects( - this.point_charges_3d.children, false); + this.point_charges = point_charges; - if (intersects.length > 0) - { - const object = intersects[0].object; + this.point_charges_3d = null; + this.electric_lines_3d = null; + this.electric_field_vectors_3d = null; - if (object !== this.trans_controls.object) - { - this.trans_controls.attach(object); - } - } + this.enablePointCharges(true); + } - console.log(this.point_charges); - }; + /// フィールドの更新 + update = () => { + if (this.children.find((child) => child === this.point_charges_3d) != null) + this.point_charges_3d.update(); - this.dom.addEventListener('mousemove', onPointerMove); - }; + if (this.children.find((child) => child === this.electric_lines_3d) != null) + this.electric_lines_3d.update(); - point_charges_3d = null; - electric_lines_3d = null; - electric_field_vectors_3d = null; + if (this.children.find((child) => child === this.electric_field_vectors_3d) != null) + this.electric_field_vectors_3d.update(); + } /// 点電荷の表示切替 enablePointCharges = (enable) => { - if (enable) - { + if (enable) { if (this.point_charges_3d == null) this.point_charges_3d = new PointCharges3D(this.point_charges); this.add(this.point_charges_3d); } - else - { + else { this.remove(this.point_charges_3d); } }; /// 電気力線の表示切替 enableElectricLines = (enable) => { - if (enable) - { + if (enable) { if (this.electric_lines_3d == null) this.electric_lines_3d = new ElectricLines3D(this.point_charges); this.add(this.electric_lines_3d); } - else - { + else { this.remove(this.electric_lines_3d); } }; /// 電界ベクトルの表示切替 enableElectricFieldVectors = (enable) => { - if (enable) - { + if (enable) { if (this.electric_field_vectors_3d == null) this.electric_field_vectors_3d = new ElectricFieldVectors3D(this.point_charges); this.add(this.electric_field_vectors_3d); } - else - { + else { this.remove(this.electric_field_vectors_3d); } }; diff --git a/app/script/Init.js b/app/script/Init.js index fea2ab8..761939f 100644 --- a/app/script/Init.js +++ b/app/script/Init.js @@ -14,7 +14,7 @@ export const CreateScene = () => { // レンダラーを作成 export const CreateRenderer = (dom) => { const renderer = new THREE.WebGLRenderer(dom, { - antialias: true, + antialias: false, }); renderer.setSize(dom.offsetWidth, dom.offsetHeight); diff --git a/app/script/main.js b/app/script/main.js index a15df97..3ca1368 100644 --- a/app/script/main.js +++ b/app/script/main.js @@ -1,108 +1,151 @@ import * as THREE from "three"; import * as EFSim from "./Init.js"; +import { Dragger } from "./Dragger.js"; import PointCharge from "./PointCharge.js"; import { Field3D } from "./Field3D.js"; +import debounce from 'debounce'; +import { throttle } from 'throttle-debounce'; + const init = () => { - const dom = document.getElementById("canvas"); - const scene = EFSim.CreateScene(); - const renderer = EFSim.CreateRenderer(dom); - const camera = EFSim.CreateCamera(dom); - EFSim.ResisterResizeObserver(dom, renderer, camera); - - // マウスコントロール - const controls = EFSim.CreateControls(camera, dom); - - // ドラッグでオブジェクトを移動するためのコントロール - const transControls = EFSim.CreateTransformControls( - camera, - dom, - controls, - scene - ); - - // 点電荷たち - const point_charges = [ - new PointCharge(0.00001, new THREE.Vector3(60, 0, 60)), - new PointCharge(-0.00001, new THREE.Vector3(20, 200, 0)), - new PointCharge(+0.00001, new THREE.Vector3(-50, 0, 20)), - new PointCharge(0.00005, new THREE.Vector3(20, 0, 0)), - ]; - - const field_3d = new Field3D(dom, camera, transControls, point_charges); - // transControls.attach(field_3d); - - // 自動回転切り替え - { - const checkbox = document.getElementById("checkbox_auto_rotate"); - controls.autoRotate = checkbox.checked; // 初期値 - checkbox.addEventListener("change", (e) => { - controls.autoRotate = e.target.checked; - }); - } - - // 2D/3D切り替え - { - const sw = document.getElementById("dimension_toggle_switch"); - const ChangeDimension = (is_3d) => { - if (is_3d) { - scene.add(field_3d); - // transControls.attach(field_3d); - } else { - scene.remove(field_3d); - // transControls.detach(field_3d); - } - }; - ChangeDimension(sw.checked); // 初期値 - sw.addEventListener("change", (e) => { - ChangeDimension(e.target.checked); - }); - } - - // 電気力線 表示/非表示 - { - const checkbox = document.getElementById("checkbox_electric_lines"); - field_3d.enableElectricLines(checkbox.checked); // 初期値 - checkbox.addEventListener("change", (e) => { - field_3d.enableElectricLines(e.target.checked); - }); - } - - // 電界ベクトル 表示/非表示 - { - const checkbox = document.getElementById("checkbox_electric_field_vectors"); - field_3d.enableElectricFieldVectors(checkbox.checked); // 初期値 - checkbox.addEventListener("change", (e) => { - field_3d.enableElectricFieldVectors(e.target.checked); - }); - } + const dom = document.getElementById("canvas"); + const scene = EFSim.CreateScene(); + const renderer = EFSim.CreateRenderer(dom); + const camera = EFSim.CreateCamera(dom); + const controls = EFSim.CreateControls(camera, dom); + EFSim.ResisterResizeObserver(dom, renderer, camera); + + // ドラッグでオブジェクトを移動するためのコントロール + const transControls = EFSim.CreateTransformControls( + camera, + dom, + controls, + scene + ); + + // 点電荷たち + const point_charges = [ + new PointCharge(0.00001, new THREE.Vector3(60, 0, 60)), + new PointCharge(-0.00001, new THREE.Vector3(20, 200, 0)), + new PointCharge(+0.00001, new THREE.Vector3(-50, 0, 20)), + new PointCharge(0.00005, new THREE.Vector3(20, 0, 0)), + ]; + + const field_3d = new Field3D(dom, camera, transControls, point_charges); + + { + const positions = point_charges.map((charge) => charge.pos); + const geometry = new THREE.SphereGeometry(10, 32, 32); + const material = new THREE.MeshBasicMaterial({ color: 0x0000ff, wireframe: true, visible: false }); + const hit_ball = positions.map((position) => { + const sphere = new THREE.Mesh(geometry, material); + sphere.position.copy(position); + scene.add(sphere); + return sphere; + }); + const dragger = new Dragger(positions, hit_ball, camera, dom, controls, scene); + + dragger.addEventListener('objectChange', debounce(() => { + field_3d.update(); + }, 10)); - const helper = new THREE.GridHelper(2000, 100); - helper.material.opacity = 0.5; - // helper.material.transparent = true; - scene.add(helper); + // function throttle(func, limit) { + // let lastFunc; + // let lastRan; + // return function () { + // const context = this; + // const args = arguments; + // if (!lastRan) { + // func.apply(context, args); + // lastRan = Date.now(); + // } else { + // clearTimeout(lastFunc); + // lastFunc = setTimeout(function () { + // if ((Date.now() - lastRan) >= limit) { + // func.apply(context, args); + // lastRan = Date.now(); + // } + // }, limit - (Date.now() - lastRan)); + // } + // } + // } - main(scene, renderer, camera, controls); + // dragger.addEventListener('objectChange', throttle( + // 100, () => { + // field_3d.update(); + // })); + } + + + // 自動回転切り替え + { + const checkbox = document.getElementById("checkbox_auto_rotate"); + controls.autoRotate = checkbox.checked; // 初期値 + checkbox.addEventListener("change", (e) => { + controls.autoRotate = e.target.checked; + }); + } + + // 2D/3D切り替え + { + const sw = document.getElementById("dimension_toggle_switch"); + const ChangeDimension = (is_3d) => { + if (is_3d) { + scene.add(field_3d); + } else { + scene.remove(field_3d); + } + }; + ChangeDimension(sw.checked); // 初期値 + sw.addEventListener("change", (e) => { + ChangeDimension(e.target.checked); + }); + } + + // 電気力線 表示/非表示 + { + const checkbox = document.getElementById("checkbox_electric_lines"); + field_3d.enableElectricLines(checkbox.checked); // 初期値 + checkbox.addEventListener("change", (e) => { + field_3d.enableElectricLines(e.target.checked); + }); + } + + // 電界ベクトル 表示/非表示 + { + const checkbox = document.getElementById("checkbox_electric_field_vectors"); + field_3d.enableElectricFieldVectors(checkbox.checked); // 初期値 + checkbox.addEventListener("change", (e) => { + field_3d.enableElectricFieldVectors(e.target.checked); + }); + } + + const helper = new THREE.GridHelper(2000, 100); + helper.material.opacity = 0.5; + // helper.material.transparent = true; + scene.add(helper); + + main(scene, renderer, camera, controls); }; // 関数の実行時間を計測する関数 // 実行にかかった時間をミリ秒で出力 function measure(name, func) { - const start = performance.now(); - func(); - const end = performance.now(); + const start = performance.now(); + func(); + const end = performance.now(); - console.log(`${name}: ${Math.floor(end - start)}ms`); + console.log(`${name}: ${Math.floor(end - start)}ms`); } window.addEventListener("load", () => { - measure("init", init); + measure("init", init); }); // エントリーポイント const main = (scene, renderer, camera, controls) => { - requestAnimationFrame(() => { - main(scene, renderer, camera, controls); - }); + requestAnimationFrame(() => { + main(scene, renderer, camera, controls); + }); - renderer.render(scene, camera); - controls.update(); + renderer.render(scene, camera); + controls.update(); }; diff --git a/package-lock.json b/package-lock.json index 9b5b769..bf51878 100644 --- a/package-lock.json +++ b/package-lock.json @@ -8,7 +8,11 @@ "name": "efsim", "version": "1.0.0", "license": "MIT", + "dependencies": { + "throttle-debounce": "^5.0.0" + }, "devDependencies": { + "debounce": "^2.0.0", "three": "^0.159.0", "vite": "^5.0.10" } @@ -550,6 +554,18 @@ "win32" ] }, + "node_modules/debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz", + "integrity": "sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==", + "dev": true, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/esbuild": { "version": "0.19.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.10.tgz", @@ -698,6 +714,14 @@ "integrity": "sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==", "dev": true }, + "node_modules/throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==", + "engines": { + "node": ">=12.22" + } + }, "node_modules/vite": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", @@ -1007,6 +1031,12 @@ "dev": true, "optional": true }, + "debounce": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/debounce/-/debounce-2.0.0.tgz", + "integrity": "sha512-xRetU6gL1VJbs85Mc4FoEGSjQxzpdxRyFhe3lmWFyy2EzydIcD4xzUvRJMD+NPDfMwKNhxa3PvsIOU32luIWeA==", + "dev": true + }, "esbuild": { "version": "0.19.10", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.19.10.tgz", @@ -1102,6 +1132,11 @@ "integrity": "sha512-eCmhlLGbBgucuo4VEA9IO3Qpc7dh8Bd4VKzr7WfW4+8hMcIfoAVi1ev0pJYN9PTTsCslbcKgBwr2wNZ1EvLInA==", "dev": true }, + "throttle-debounce": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/throttle-debounce/-/throttle-debounce-5.0.0.tgz", + "integrity": "sha512-2iQTSgkkc1Zyk0MeVrt/3BvuOXYPl/R8Z0U2xxo9rjwNciaHDG3R+Lm6dh4EeUci49DanvBnuqI6jshoQQRGEg==" + }, "vite": { "version": "5.0.10", "resolved": "https://registry.npmjs.org/vite/-/vite-5.0.10.tgz", diff --git a/package.json b/package.json index d1d17d7..2482181 100644 --- a/package.json +++ b/package.json @@ -19,8 +19,12 @@ }, "homepage": "https://github.com/CaseyNelson314/EFSim#readme", "devDependencies": { + "debounce": "^2.0.0", "three": "^0.159.0", "vite": "^5.0.10" }, - "type": "module" + "type": "module", + "dependencies": { + "throttle-debounce": "^5.0.0" + } }