From 65592f61cb5f33d3e2d83069a7f1aca7695da8ce Mon Sep 17 00:00:00 2001 From: Steinn Steinsen Date: Mon, 16 May 2022 15:39:47 +0000 Subject: [PATCH] format all code using prettier --- copyAssets.js | 23 +- package.json | 132 +-- src/World.js | 970 ++++++++++---------- src/components/Dice/Dice.js | 171 ++-- src/components/Dice/index.js | 2 +- src/components/Dice/meshFaceIds.js | 270 +++--- src/components/Dice/themes.js | 164 ++-- src/components/DiceBox.js | 219 ++--- src/components/camera.js | 28 +- src/components/canvas.js | 29 +- src/components/engine.js | 10 +- src/components/lights.js | 52 +- src/components/offscreenCanvas.worker.js | 585 ++++++------ src/components/physics.worker.js | 1029 ++++++++++++---------- src/components/pointLights.js | 46 +- src/components/scene.js | 28 +- src/components/world.offscreen.js | 215 ++--- src/components/world.onscreen.js | 711 ++++++++------- src/helpers/babylonFileLoader.js | 316 ++++--- src/helpers/index.js | 81 +- src/index.js | 2 +- vite.config.js | 82 +- 22 files changed, 2714 insertions(+), 2451 deletions(-) diff --git a/copyAssets.js b/copyAssets.js index bb25e79..cf5c95b 100644 --- a/copyAssets.js +++ b/copyAssets.js @@ -1,23 +1,22 @@ -'use strict' +"use strict"; -const copydir = require('copy-dir'); -const path = require('path'); -const fs = require('fs'); +const copydir = require("copy-dir"); +const path = require("path"); +const fs = require("fs"); -const filesToCopy = './dist/assets' +const filesToCopy = "./dist/assets"; // User's local directory -const userPath = path.join(process.env.INIT_CWD,'/public/assets') +const userPath = path.join(process.env.INIT_CWD, "/public/assets"); // Creates directory if it doesn't exist fs.mkdir(userPath, { recursive: true }, (err) => { if (err) throw err; - + // Moving files to user's local directory copydir(filesToCopy, userPath, { - utimes: true, // keep add time and modify time - mode: true, // keep file mode - cover: true // cover file when exists, default is true - }) + utimes: true, // keep add time and modify time + mode: true, // keep file mode + cover: true, // cover file when exists, default is true + }); }); - diff --git a/package.json b/package.json index e993b60..ac27ac7 100644 --- a/package.json +++ b/package.json @@ -1,68 +1,68 @@ { - "name": "@3d-dice/dice-box", - "author": { - "name": "Frank Ali" - }, - "description": "A 3D environment for rolling game dice", - "version": "0.6.1", - "keywords": [ - "3D", - "dice", - "roll", - "roller", - "javascript", - "rpg", - "dnd", - "d&d", - "tabletop" - ], - "license": "MIT", - "homepage": "https://fantasticdice.games/", - "repository": { - "type": "git", - "url": "https://github.com/3d-dice/dice-box" - }, - "bugs": { - "url": "https://github.com/3d-dice/dice-box/issues" - }, - "files": [ - "dist", - "copyAssets.js" - ], - "main": "./dist/dice-box.es.js", - "dev-files": [ - "src" - ], - "dev-main": "./src/index", - "scripts": { - "dev": "vite", - "build": "vite build", - "serve": "vite preview", - "postinstall": "node copyAssets.js" - }, - "dependencies": { - "@babylonjs/core": "^5.0.0-rc.3", - "@babylonjs/loaders": "^5.0.0-rc.3", - "@babylonjs/materials": "^5.0.0-rc.3", - "copy-dir": "^1.3.0" - }, - "devDependencies": { - "rollup-plugin-copy": "^3.4.0", - "rollup-plugin-visualizer": "^5.6.0", - "vite": "^2.8.6" - }, - "browserslist": { - "production": [ - ">0.2%", - "not dead", - "not op_mini all", - "not ie all" - ], - "development": [ - ">0.2%", - "not dead", - "not op_mini all", - "not ie all" - ] - } + "name": "@3d-dice/dice-box", + "author": { + "name": "Frank Ali" + }, + "description": "A 3D environment for rolling game dice", + "version": "0.6.1", + "keywords": [ + "3D", + "dice", + "roll", + "roller", + "javascript", + "rpg", + "dnd", + "d&d", + "tabletop" + ], + "license": "MIT", + "homepage": "https://fantasticdice.games/", + "repository": { + "type": "git", + "url": "https://github.com/3d-dice/dice-box" + }, + "bugs": { + "url": "https://github.com/3d-dice/dice-box/issues" + }, + "files": [ + "dist", + "copyAssets.js" + ], + "main": "./dist/dice-box.es.js", + "dev-files": [ + "src" + ], + "dev-main": "./src/index", + "scripts": { + "dev": "vite", + "build": "vite build", + "serve": "vite preview", + "postinstall": "node copyAssets.js" + }, + "dependencies": { + "@babylonjs/core": "^5.0.0-rc.3", + "@babylonjs/loaders": "^5.0.0-rc.3", + "@babylonjs/materials": "^5.0.0-rc.3", + "copy-dir": "^1.3.0" + }, + "devDependencies": { + "rollup-plugin-copy": "^3.4.0", + "rollup-plugin-visualizer": "^5.6.0", + "vite": "^2.8.6" + }, + "browserslist": { + "production": [ + ">0.2%", + "not dead", + "not op_mini all", + "not ie all" + ], + "development": [ + ">0.2%", + "not dead", + "not op_mini all", + "not ie all" + ] + } } diff --git a/src/World.js b/src/World.js index d119719..e7135ff 100644 --- a/src/World.js +++ b/src/World.js @@ -1,466 +1,490 @@ -import { createCanvas } from './components/canvas' +import { createCanvas } from "./components/canvas"; // import WorldOffscreen from './components/world.offscreen' -import physicsWorker from './components/physics.worker.js?worker&inline' -import { debounce } from './helpers' +import physicsWorker from "./components/physics.worker.js?worker&inline"; +import { debounce } from "./helpers"; const defaultOptions = { - id: `dice-canvas-${Date.now()}`, // set the canvas id + id: `dice-canvas-${Date.now()}`, // set the canvas id enableShadows: true, // do dice cast shadows onto DiceBox mesh? delay: 10, // delay between dice being generated - 0 causes stuttering and physics popping - gravity: 2, // note: high gravity will cause dice piles to jiggle - startingHeight: 8, // height to drop the dice from - will not exceed the DiceBox height set by zoom - spinForce: 4, // passed on to physics as an impulse force - throwForce: 5, // passed on to physics as linear velocity - scale: 4, // scale the dice - theme: 'diceOfRolling', // can be a hex color or a pre-defined theme such as 'purpleRock' - offscreen: true, // use offscreen canvas browser feature for performance improvements - will fallback to false based on feature detection - assetPath: '/assets/dice-box/', // path to 'ammo', 'models', 'themes' folders and web workers - origin: location.origin, -} + gravity: 2, // note: high gravity will cause dice piles to jiggle + startingHeight: 8, // height to drop the dice from - will not exceed the DiceBox height set by zoom + spinForce: 4, // passed on to physics as an impulse force + throwForce: 5, // passed on to physics as linear velocity + scale: 4, // scale the dice + theme: "diceOfRolling", // can be a hex color or a pre-defined theme such as 'purpleRock' + offscreen: true, // use offscreen canvas browser feature for performance improvements - will fallback to false based on feature detection + assetPath: "/assets/dice-box/", // path to 'ammo', 'models', 'themes' folders and web workers + origin: location.origin, +}; class World { - rollCollectionData = {} - rollGroupData = {} - rollDiceData = {} - themeData = [] - #collectionIndex = 0 - #groupIndex = 0 - #rollIndex = 0 - #idIndex = 0 - #DiceWorld - diceWorldInit - #DiceWorker - diceWorkerInit - onDieComplete = () => {} - onRollComplete = () => {} - onRemoveComplete = () => {} - - constructor(container, options = {}){ - // extend defaults with options - this.config = {...defaultOptions, ...options} - // if a canvas selector is provided then that will be used for the dicebox, otherwise a canvas will be created using the config.id + rollCollectionData = {}; + rollGroupData = {}; + rollDiceData = {}; + themeData = []; + #collectionIndex = 0; + #groupIndex = 0; + #rollIndex = 0; + #idIndex = 0; + #DiceWorld; + diceWorldInit; + #DiceWorker; + diceWorkerInit; + onDieComplete = () => {}; + onRollComplete = () => {}; + onRemoveComplete = () => {}; + + constructor(container, options = {}) { + // extend defaults with options + this.config = { ...defaultOptions, ...options }; + // if a canvas selector is provided then that will be used for the dicebox, otherwise a canvas will be created using the config.id this.canvas = createCanvas({ selector: container, - id: this.config.id - }) + id: this.config.id, + }); + } + + async #loadWorld() { + if ( + "OffscreenCanvas" in window && + "transferControlToOffscreen" in this.canvas && + this.config.offscreen + ) { + // Ok to use offscreen canvas - transfer controll offscreen + const WorldOffscreen = await import("./components/world.offscreen").then( + (module) => module.default + ); + // WorldOffscreen is just a container class that passes all method calls to the Offscreen Canvas worker + this.#DiceWorld = new WorldOffscreen({ + canvas: this.canvas, + options: this.config, + }); + } else { + if (this.config.offscreen) { + console.warn( + "This browser does not support OffscreenCanvas. Using standard canvas fallback." + ); + this.config.offscreen = false; + } + // code splitting out WorldOnscreen. It's esentially the same as offscreenCanvas.worker.js but communicates with the main thread differently + const WorldOnscreen = await import("./components/world.onscreen").then( + (module) => module.default + ); + this.#DiceWorld = new WorldOnscreen({ + canvas: this.canvas, + options: this.config, + }); + } + } + #connectWorld() { + // create message channels for the two web workers to communicate through + const channel = new MessageChannel(); + + // set up a promise to be fullfilled when a message comes back from DiceWorld indicating init is complete + this.#DiceWorld.init = new Promise((resolve, reject) => { + this.diceWorldInit = resolve; + }); + + this.#DiceWorld.connect(channel.port1); + + // initialize physics world in which AmmoJS runs + this.#DiceWorker = new physicsWorker(); + // set up a promise to be fullfilled when a message comes back from physics.worker indicating init is complete + this.#DiceWorker.init = new Promise((resolve, reject) => { + this.diceWorkerInit = resolve; + }); + + // Setup the connection: Port 2 is for diceWorker + this.#DiceWorker.postMessage( + { + action: "connect", + }, + [channel.port2] + ); } - async #loadWorld(){ - if ("OffscreenCanvas" in window && "transferControlToOffscreen" in this.canvas && this.config.offscreen) { - // Ok to use offscreen canvas - transfer controll offscreen - const WorldOffscreen = await import('./components/world.offscreen').then(module => module.default) - // WorldOffscreen is just a container class that passes all method calls to the Offscreen Canvas worker - this.#DiceWorld = new WorldOffscreen({ - canvas: this.canvas, - options: this.config - }) - } else { - if(this.config.offscreen){ - console.warn("This browser does not support OffscreenCanvas. Using standard canvas fallback.") - this.config.offscreen = false - } - // code splitting out WorldOnscreen. It's esentially the same as offscreenCanvas.worker.js but communicates with the main thread differently - const WorldOnscreen = await import('./components/world.onscreen').then(module => module.default) - this.#DiceWorld = new WorldOnscreen({ - canvas: this.canvas, - options: this.config - }) - } - } - - #connectWorld(){ - // create message channels for the two web workers to communicate through - const channel = new MessageChannel() - - // set up a promise to be fullfilled when a message comes back from DiceWorld indicating init is complete - this.#DiceWorld.init = new Promise((resolve, reject) => { - this.diceWorldInit = resolve - }) - - this.#DiceWorld.connect(channel.port1) - - // initialize physics world in which AmmoJS runs - this.#DiceWorker = new physicsWorker() - // set up a promise to be fullfilled when a message comes back from physics.worker indicating init is complete - this.#DiceWorker.init = new Promise((resolve, reject) => { - this.diceWorkerInit = resolve - }) - - // Setup the connection: Port 2 is for diceWorker - this.#DiceWorker.postMessage({ - action: "connect" - },[ channel.port2 ]) - } - - resizeWorld(){ - // send resize events to workers - debounced for performance - const resizeWorkers = () => { - this.#DiceWorld.resize({width: this.canvas.clientWidth, height: this.canvas.clientHeight}) - this.#DiceWorker.postMessage({action: "resize", width: this.canvas.clientWidth, height: this.canvas.clientHeight}); - } - const debounceResize = debounce(resizeWorkers) - window.addEventListener("resize", debounceResize) - } + resizeWorld() { + // send resize events to workers - debounced for performance + const resizeWorkers = () => { + this.#DiceWorld.resize({ + width: this.canvas.clientWidth, + height: this.canvas.clientHeight, + }); + this.#DiceWorker.postMessage({ + action: "resize", + width: this.canvas.clientWidth, + height: this.canvas.clientHeight, + }); + }; + const debounceResize = debounce(resizeWorkers); + window.addEventListener("resize", debounceResize); + } async init() { - await this.#loadWorld() - this.#connectWorld() - this.resizeWorld() - - this.#DiceWorld.onInitComplete = () => { - this.diceWorldInit() - } - // now that DiceWorld is ready we can attach our callbacks - this.#DiceWorld.onRollResult = (result) => { - const die = this.rollDiceData[result.rollId] - const group = this.rollGroupData[die.groupId] - const collection = this.rollCollectionData[die.collectionId] - - // map die results back to our rollData - // since all rolls are references to this.rollDiceDate the values will be added to those objects - group.rolls[die.rollId].value = result.value - - // increment the completed roll count for this group - collection.completedRolls++ - // if all rolls are completed then resolve the collection promise - returning dice that were in this collection - if(collection.completedRolls == collection.rolls.length) { - // pull out roll.collectionId and roll.id? They're meant to be internal values - collection.resolve(Object.values(collection.rolls).map(({collectionId, id, ...rest}) => rest)) - } - - // trigger callback passing individual die result - const {collectionId, id, ...returnDie} = die - this.onDieComplete(returnDie) - } - this.#DiceWorld.onRollComplete = () => { - // trigger callback passing the roll results - this.onRollComplete(this.getRollResults()) - } - - this.#DiceWorld.onDieRemoved = (rollId) => { - // get die information from cache - let die = this.rollDiceData[rollId] - const collection = this.rollCollectionData[die.removeCollectionId] - collection.completedRolls++ - - // remove this die from cache - delete this.rollDiceData[die.rollId] - - // remove this die from it's group rolls - const group = this.rollGroupData[die.groupId] - delete group.rolls[die.rollId] - - // parse the group value now that the die has been removed from data - const groupData = this.#parseGroup(die.groupId) - // update the value and quantity values - group.value = groupData.value - group.qty = groupData.rollsArray.length - - // if all rolls are completed then resolve the collection promise - returning dice that were removed - if(collection.completedRolls == collection.rolls.length) { - collection.resolve(Object.values(collection.rolls).map(({id, ...rest}) => rest)) - } - const {collectionId, id, removeCollectionId, ...returnDie} = die - this.onRemoveComplete(returnDie) - } + await this.#loadWorld(); + this.#connectWorld(); + this.resizeWorld(); + + this.#DiceWorld.onInitComplete = () => { + this.diceWorldInit(); + }; + // now that DiceWorld is ready we can attach our callbacks + this.#DiceWorld.onRollResult = (result) => { + const die = this.rollDiceData[result.rollId]; + const group = this.rollGroupData[die.groupId]; + const collection = this.rollCollectionData[die.collectionId]; + + // map die results back to our rollData + // since all rolls are references to this.rollDiceDate the values will be added to those objects + group.rolls[die.rollId].value = result.value; + + // increment the completed roll count for this group + collection.completedRolls++; + // if all rolls are completed then resolve the collection promise - returning dice that were in this collection + if (collection.completedRolls == collection.rolls.length) { + // pull out roll.collectionId and roll.id? They're meant to be internal values + collection.resolve( + Object.values(collection.rolls).map( + ({ collectionId, id, ...rest }) => rest + ) + ); + } + + // trigger callback passing individual die result + const { collectionId, id, ...returnDie } = die; + this.onDieComplete(returnDie); + }; + this.#DiceWorld.onRollComplete = () => { + // trigger callback passing the roll results + this.onRollComplete(this.getRollResults()); + }; + + this.#DiceWorld.onDieRemoved = (rollId) => { + // get die information from cache + let die = this.rollDiceData[rollId]; + const collection = this.rollCollectionData[die.removeCollectionId]; + collection.completedRolls++; + + // remove this die from cache + delete this.rollDiceData[die.rollId]; + + // remove this die from it's group rolls + const group = this.rollGroupData[die.groupId]; + delete group.rolls[die.rollId]; + + // parse the group value now that the die has been removed from data + const groupData = this.#parseGroup(die.groupId); + // update the value and quantity values + group.value = groupData.value; + group.qty = groupData.rollsArray.length; + + // if all rolls are completed then resolve the collection promise - returning dice that were removed + if (collection.completedRolls == collection.rolls.length) { + collection.resolve( + Object.values(collection.rolls).map(({ id, ...rest }) => rest) + ); + } + const { collectionId, id, removeCollectionId, ...returnDie } = die; + this.onRemoveComplete(returnDie); + }; // initialize the AmmoJS physics worker this.#DiceWorker.postMessage({ action: "init", width: this.canvas.clientWidth, height: this.canvas.clientHeight, - options: this.config - }) + options: this.config, + }); this.#DiceWorker.onmessage = (e) => { - switch( e.data.action ) { - case "init-complete": - this.diceWorkerInit() // fulfill promise so other things can run - } - } + switch (e.data.action) { + case "init-complete": + this.diceWorkerInit(); // fulfill promise so other things can run + } + }; // pomise.all to initialize both offscreenWorker and DiceWorker - await Promise.all([this.#DiceWorld.init, this.#DiceWorker.init]) + await Promise.all([this.#DiceWorld.init, this.#DiceWorker.init]); - // make this method chainable - return this + // make this method chainable + return this; + } + + // TODO: use getter and setter + // change config options + updateConfig(options) { + const newConfig = { ...this.config, ...options }; + this.config = newConfig; + // pass updates to DiceWorld + this.#DiceWorld.updateConfig(newConfig); + // pass updates to PhysicsWorld + this.#DiceWorker.postMessage({ + action: "updateConfig", + options: newConfig, + }); + // make this method chainable + return this; } - // TODO: use getter and setter - // change config options - updateConfig(options) { - const newConfig = {...this.config,...options} - this.config = newConfig - // pass updates to DiceWorld - this.#DiceWorld.updateConfig(newConfig) - // pass updates to PhysicsWorld - this.#DiceWorker.postMessage({ - action: 'updateConfig', - options: newConfig - }) - - // make this method chainable - return this - } - - clear() { - // reset indexes - this.#collectionIndex = 0 - this.#groupIndex = 0 - this.#rollIndex = 0 - this.#idIndex = 0 - // reset internal data objects - this.rollCollectionData = {} - this.rollGroupData = {} - this.rollDiceData = {} - // clear all rendered die bodies - this.#DiceWorld.clear() + clear() { + // reset indexes + this.#collectionIndex = 0; + this.#groupIndex = 0; + this.#rollIndex = 0; + this.#idIndex = 0; + // reset internal data objects + this.rollCollectionData = {}; + this.rollGroupData = {}; + this.rollDiceData = {}; + // clear all rendered die bodies + this.#DiceWorld.clear(); // clear all physics die bodies - this.#DiceWorker.postMessage({action: "clearDice"}) + this.#DiceWorker.postMessage({ action: "clearDice" }); + + // make this method chainable + return this; + } + + hide() { + this.canvas.style.display = "none"; + + // make this method chainable + return this; + } + + show() { + this.canvas.style.display = "block"; + + // make this method chainable + return this; + } + + // TODO: pass data with roll - such as roll name. Passed back at the end in the results + roll(notation, { theme = undefined, newStartPoint = true } = {}) { + // note: to add to a roll on screen use .add method + // reset the offscreen worker and physics worker with each new roll + this.clear(); + const collectionId = this.#collectionIndex++; + + this.rollCollectionData[collectionId] = new Collection({ + id: collectionId, + notation, + theme, + anustart: newStartPoint, + }); + + const parsedNotation = this.createNotationArray(notation); + this.#makeRoll(parsedNotation, collectionId); + + // returns a Promise that is resolved in onRollComplete + return this.rollCollectionData[collectionId].promise; + } + + add(notation, { theme = undefined, newStartPoint = true } = {}) { + const collectionId = this.#collectionIndex++; + + this.rollCollectionData[collectionId] = new Collection({ + id: collectionId, + notation, + theme, + anustart: newStartPoint, + }); + + const parsedNotation = this.createNotationArray(notation); + this.#makeRoll(parsedNotation, collectionId); + + // returns a Promise that is resolved in onRollComplete + return this.rollCollectionData[collectionId].promise; + } + + reroll( + notation, + { remove = false, hide = false, newStartPoint = true } = {} + ) { + // TODO: add hide if you want to keep the die result for an external parser + + // ensure notation is an array + const rollArray = Array.isArray(notation) ? notation : [notation]; + + // destructure out 'sides', 'theme', 'groupId', 'rollId' - basically just getting rid of value - could do ({value, ...rest}) => rest + const cleanNotation = rollArray.map(({ value, ...rest }) => rest); - // make this method chainable - return this + if (remove === true) { + this.remove(cleanNotation, { hide }); + } + + // .add will return a promise that will then be returned here + return this.add(cleanNotation, { newStartPoint }); } - hide() { - this.canvas.style.display = 'none' - - // make this method chainable - return this - } - - show() { - this.canvas.style.display = 'block' - - // make this method chainable - return this - } - - // TODO: pass data with roll - such as roll name. Passed back at the end in the results - roll(notation, {theme = undefined,newStartPoint = true} = {}) { - // note: to add to a roll on screen use .add method - // reset the offscreen worker and physics worker with each new roll - this.clear() - const collectionId = this.#collectionIndex++ - - this.rollCollectionData[collectionId] = new Collection({ - id: collectionId, - notation, - theme, - anustart: newStartPoint - }) - - const parsedNotation = this.createNotationArray(notation) - this.#makeRoll(parsedNotation, collectionId) - - // returns a Promise that is resolved in onRollComplete - return this.rollCollectionData[collectionId].promise - } - - add(notation, {theme = undefined,newStartPoint = true} = {}) { - - const collectionId = this.#collectionIndex++ - - this.rollCollectionData[collectionId] = new Collection({ - id: collectionId, - notation, - theme, - anustart: newStartPoint - }) - - const parsedNotation = this.createNotationArray(notation) - this.#makeRoll(parsedNotation, collectionId) - - // returns a Promise that is resolved in onRollComplete - return this.rollCollectionData[collectionId].promise + remove(notation, { hide = false } = {}) { + // ensure notation is an array + const rollArray = Array.isArray(notation) ? notation : [notation]; + + const collectionId = this.#collectionIndex++; + + this.rollCollectionData[collectionId] = new Collection({ + id: collectionId, + notation, + rolls: rollArray, + }); + + // loop through each die to be removed + rollArray.map((die) => { + // add the collectionId to the die so it can be looked up in the callback + this.rollDiceData[die.rollId].removeCollectionId = collectionId; + // assign the id for this die from our cache - required for removal + // die.id = this.rollDiceData[die.rollId].id - note: can appear in async roll result data if attached to die object + let id = this.rollDiceData[die.rollId].id; + // remove the die from the render - don't like having to pass two ids. rollId is passed over just so it can be passed back for callback + this.#DiceWorld.remove({ id, rollId: die.rollId }); + // remove the die from the physics bodies + this.#DiceWorker.postMessage({ action: "removeDie", id }); + }); + + return this.rollCollectionData[collectionId].promise; } - reroll(notation, {remove = false, hide = false, newStartPoint = true} = {}) { - // TODO: add hide if you want to keep the die result for an external parser - - // ensure notation is an array - const rollArray = Array.isArray(notation) ? notation : [notation] - - // destructure out 'sides', 'theme', 'groupId', 'rollId' - basically just getting rid of value - could do ({value, ...rest}) => rest - const cleanNotation = rollArray.map(({value, ...rest}) => rest) - - if(remove === true){ - this.remove(cleanNotation, {hide}) - } - - // .add will return a promise that will then be returned here - return this.add(cleanNotation, {newStartPoint}) - } - - remove(notation, {hide = false} = {}) { - // ensure notation is an array - const rollArray = Array.isArray(notation) ? notation : [notation] - - const collectionId = this.#collectionIndex++ - - this.rollCollectionData[collectionId] = new Collection({ - id: collectionId, - notation, - rolls: rollArray, - }) - - // loop through each die to be removed - rollArray.map(die => { - // add the collectionId to the die so it can be looked up in the callback - this.rollDiceData[die.rollId].removeCollectionId = collectionId - // assign the id for this die from our cache - required for removal - // die.id = this.rollDiceData[die.rollId].id - note: can appear in async roll result data if attached to die object - let id = this.rollDiceData[die.rollId].id - // remove the die from the render - don't like having to pass two ids. rollId is passed over just so it can be passed back for callback - this.#DiceWorld.remove({id,rollId: die.rollId}) - // remove the die from the physics bodies - this.#DiceWorker.postMessage({action: "removeDie", id }) - }) - - return this.rollCollectionData[collectionId].promise - } - - async loadTheme(theme){ - if(this.themeData.includes(theme)){ - return - } else { - await this.#DiceWorld.loadTheme(theme) - this.themeData.push(theme) - } - } - - // used by both .add and .roll - .roll clears the box and .add does not - async #makeRoll(parsedNotation, collectionId){ - - const collection = this.rollCollectionData[collectionId] - let anustart = collection.anustart - - // loop through the number of dice in the group and roll each one - parsedNotation.forEach(async notation => { - const theme = notation.theme || collection.theme || this.config.theme - const rolls = {} - const hasGroupId = notation.groupId !== undefined - let index - - // load the theme prior to adding all the dice => give textures a chance to load so you don't see a flash of naked dice - await this.loadTheme(theme) - - // TODO: should I validate that added dice are only joining groups of the same "sides" value - e.g.: d6's can only be added to groups when sides: 6? Probably. - - for (var i = 0, len = notation.qty; i < len; i++) { - // id's start at zero and zero can be falsy, so we check for undefined - let rollId = notation.rollId !== undefined ? notation.rollId : this.#rollIndex++ - let id = notation.id !== undefined ? notation.id : this.#idIndex++ - index = hasGroupId ? notation.groupId : this.#groupIndex - - const roll = { - sides: notation.sides, - groupId: index, - collectionId: collection.id, - rollId, - id, - theme - } - - rolls[rollId] = roll - this.rollDiceData[rollId] = roll - collection.rolls.push(this.rollDiceData[rollId]) - - this.#DiceWorld.add({...roll,anustart}) - - // turn flag off - anustart = false - - } - - if(hasGroupId) { - Object.assign(this.rollGroupData[index].rolls, rolls) - } else { - // save this roll group for later - notation.rolls = rolls - notation.id = index - this.rollGroupData[index] = notation - ++this.#groupIndex - } - }) - } - - // accepts simple notations eg: 4d6 - // accepts array of notations eg: ['4d6','2d10'] - // accepts object {sides:int, qty:int} - // accepts array of objects eg: [{sides:int, qty:int, mods:[]}] - createNotationArray(input){ - const notation = Array.isArray( input ) ? input : [ input ] - let parsedNotation = [] - - - const verifyObject = ( object ) => { - if(!object.hasOwnProperty('qty')) { - object.qty = 1 - } - if ( object.hasOwnProperty('sides') ) { - return true - } else { - const err = "Roll notation is missing sides" - throw new Error(err); - } - } - - const incrementId = (key) => { - key = key.toString() - let splitKey = key.split(".") - if(splitKey[1]){ - splitKey[1] = parseInt(splitKey[1]) + 1 - } else { - splitKey[1] = 1 - } - return splitKey[0] + "." + splitKey[1] - } - - // verify that the rollId is unique. If not then increment it by .1 - // rollIds become keys in the rollDiceData object, so they must be unique or they will overright another entry - const verifyRollId = ( object ) => { - if(object.hasOwnProperty('rollId')){ - if(this.rollDiceData.hasOwnProperty(object.rollId)){ - object.rollId = incrementId(object.rollId) - } - } - } - - // notation is an array of strings or objects - notation.forEach(roll => { - // console.log('roll', roll) - // if notation is an array of strings - if ( typeof roll === 'string' ) { - parsedNotation.push( this.parse( roll ) ) - } else if ( typeof notation === 'object' ) { - verifyRollId( roll ) - verifyObject( roll ) && parsedNotation.push( roll ) - } - }) - - return parsedNotation - } + async loadTheme(theme) { + if (this.themeData.includes(theme)) { + return; + } else { + await this.#DiceWorld.loadTheme(theme); + this.themeData.push(theme); + } + } + + // used by both .add and .roll - .roll clears the box and .add does not + async #makeRoll(parsedNotation, collectionId) { + const collection = this.rollCollectionData[collectionId]; + let anustart = collection.anustart; + + // loop through the number of dice in the group and roll each one + parsedNotation.forEach(async (notation) => { + const theme = notation.theme || collection.theme || this.config.theme; + const rolls = {}; + const hasGroupId = notation.groupId !== undefined; + let index; + + // load the theme prior to adding all the dice => give textures a chance to load so you don't see a flash of naked dice + await this.loadTheme(theme); + + // TODO: should I validate that added dice are only joining groups of the same "sides" value - e.g.: d6's can only be added to groups when sides: 6? Probably. + + for (var i = 0, len = notation.qty; i < len; i++) { + // id's start at zero and zero can be falsy, so we check for undefined + let rollId = + notation.rollId !== undefined ? notation.rollId : this.#rollIndex++; + let id = notation.id !== undefined ? notation.id : this.#idIndex++; + index = hasGroupId ? notation.groupId : this.#groupIndex; + + const roll = { + sides: notation.sides, + groupId: index, + collectionId: collection.id, + rollId, + id, + theme, + }; + + rolls[rollId] = roll; + this.rollDiceData[rollId] = roll; + collection.rolls.push(this.rollDiceData[rollId]); + + this.#DiceWorld.add({ ...roll, anustart }); + + // turn flag off + anustart = false; + } + + if (hasGroupId) { + Object.assign(this.rollGroupData[index].rolls, rolls); + } else { + // save this roll group for later + notation.rolls = rolls; + notation.id = index; + this.rollGroupData[index] = notation; + ++this.#groupIndex; + } + }); + } + + // accepts simple notations eg: 4d6 + // accepts array of notations eg: ['4d6','2d10'] + // accepts object {sides:int, qty:int} + // accepts array of objects eg: [{sides:int, qty:int, mods:[]}] + createNotationArray(input) { + const notation = Array.isArray(input) ? input : [input]; + let parsedNotation = []; + + const verifyObject = (object) => { + if (!object.hasOwnProperty("qty")) { + object.qty = 1; + } + if (object.hasOwnProperty("sides")) { + return true; + } else { + const err = "Roll notation is missing sides"; + throw new Error(err); + } + }; + + const incrementId = (key) => { + key = key.toString(); + let splitKey = key.split("."); + if (splitKey[1]) { + splitKey[1] = parseInt(splitKey[1]) + 1; + } else { + splitKey[1] = 1; + } + return splitKey[0] + "." + splitKey[1]; + }; + + // verify that the rollId is unique. If not then increment it by .1 + // rollIds become keys in the rollDiceData object, so they must be unique or they will overright another entry + const verifyRollId = (object) => { + if (object.hasOwnProperty("rollId")) { + if (this.rollDiceData.hasOwnProperty(object.rollId)) { + object.rollId = incrementId(object.rollId); + } + } + }; + + // notation is an array of strings or objects + notation.forEach((roll) => { + // console.log('roll', roll) + // if notation is an array of strings + if (typeof roll === "string") { + parsedNotation.push(this.parse(roll)); + } else if (typeof notation === "object") { + verifyRollId(roll); + verifyObject(roll) && parsedNotation.push(roll); + } + }); + + return parsedNotation; + } // parse text die notation such as 2d10+3 => {number:2, type:6, modifier:3} // taken from https://github.com/ChapelR/dice-notation parse(notation) { - const diceNotation = /(\d+)[dD](\d+)(.*)$/i - const modifier = /([+-])(\d+)/ - const cleanNotation = notation.trim().replace(/\s+/g, '') + const diceNotation = /(\d+)[dD](\d+)(.*)$/i; + const modifier = /([+-])(\d+)/; + const cleanNotation = notation.trim().replace(/\s+/g, ""); const validNumber = (n, err) => { - n = Number(n) + n = Number(n); if (Number.isNaN(n) || !Number.isInteger(n) || n < 1) { throw new Error(err); } - return n - } + return n; + }; const roll = cleanNotation.match(diceNotation); - let mod = 0; - const msg = 'Invalid notation: ' + notation + ''; + let mod = 0; + const msg = "Invalid notation: " + notation + ""; if (roll.length < 3) { throw new Error(msg); @@ -468,70 +492,72 @@ class World { if (roll[3] && modifier.test(roll[3])) { const modParts = roll[3].match(modifier); let basicMod = validNumber(modParts[2], msg); - if (modParts[1].trim() === '-') { + if (modParts[1].trim() === "-") { basicMod *= -1; } mod = basicMod; } - + roll[1] = validNumber(roll[1], msg); roll[2] = validNumber(roll[2], msg); return { - qty : roll[1], - sides : roll[2], - modifier : mod, - } + qty: roll[1], + sides: roll[2], + modifier: mod, + }; } - #parseGroup(groupId) { - // console.log('groupId', groupId) - const rollGroup = this.rollGroupData[groupId] - // turn object into an array - const rollsArray = Object.values(rollGroup.rolls).map(({collectionId, id, ...rest}) => rest) - // add up the values - // some dice may still be rolling, should this be a promise? - // if dice are still rolling in the group then the value is undefined - hence the isNaN check - let value = rollsArray.reduce((val,roll) => { - const rollVal = isNaN(roll.value) ? 0 : roll.value - return val + rollVal - },0) - // add the modifier - value += rollGroup.modifier ? rollGroup.modifier : 0 - // return the value and the rollsArray - return {value, rollsArray} - } - - getRollResults(){ - // loop through each roll group - return Object.entries(this.rollGroupData).map(([key,val]) => { - // parse the group data to get the value and the rolls as an array - const groupData = this.#parseGroup(key) - // set the value for this roll group in this.rollGroupData - val.value = groupData.value - // set the qty equal to the number of rolls - this can be changed by rerolls and removals - val.qty = groupData.rollsArray.length - // copy the group that will be put into the return object - const groupCopy = {...val} - // replace the rolls object with a rolls array - groupCopy.rolls = groupData.rollsArray - // return the groupCopy - note: we never return this.rollGroupData - return groupCopy - }) - } + #parseGroup(groupId) { + // console.log('groupId', groupId) + const rollGroup = this.rollGroupData[groupId]; + // turn object into an array + const rollsArray = Object.values(rollGroup.rolls).map( + ({ collectionId, id, ...rest }) => rest + ); + // add up the values + // some dice may still be rolling, should this be a promise? + // if dice are still rolling in the group then the value is undefined - hence the isNaN check + let value = rollsArray.reduce((val, roll) => { + const rollVal = isNaN(roll.value) ? 0 : roll.value; + return val + rollVal; + }, 0); + // add the modifier + value += rollGroup.modifier ? rollGroup.modifier : 0; + // return the value and the rollsArray + return { value, rollsArray }; + } + + getRollResults() { + // loop through each roll group + return Object.entries(this.rollGroupData).map(([key, val]) => { + // parse the group data to get the value and the rolls as an array + const groupData = this.#parseGroup(key); + // set the value for this roll group in this.rollGroupData + val.value = groupData.value; + // set the qty equal to the number of rolls - this can be changed by rerolls and removals + val.qty = groupData.rollsArray.length; + // copy the group that will be put into the return object + const groupCopy = { ...val }; + // replace the rolls object with a rolls array + groupCopy.rolls = groupData.rollsArray; + // return the groupCopy - note: we never return this.rollGroupData + return groupCopy; + }); + } } -class Collection{ - constructor(options){ - Object.assign(this,options) - this.rolls = options.rolls || [] - this.completedRolls = 0 - const that = this - this.promise = new Promise((resolve,reject) => { - that.resolve = resolve - that.reject = reject - }) - } +class Collection { + constructor(options) { + Object.assign(this, options); + this.rolls = options.rolls || []; + this.completedRolls = 0; + const that = this; + this.promise = new Promise((resolve, reject) => { + that.resolve = resolve; + that.reject = reject; + }); + } } -export default World \ No newline at end of file +export default World; diff --git a/src/components/Dice/Dice.js b/src/components/Dice/Dice.js index ab777f0..1dbc1a6 100644 --- a/src/components/Dice/Dice.js +++ b/src/components/Dice/Dice.js @@ -1,133 +1,148 @@ -import { SceneLoader } from '@babylonjs/core/Loading/sceneLoader' -import { Vector3 } from '@babylonjs/core/Maths/math' +import { SceneLoader } from "@babylonjs/core/Loading/sceneLoader"; +import { Vector3 } from "@babylonjs/core/Maths/math"; import { Ray } from "@babylonjs/core/Culling/ray"; // import { RayHelper } from '@babylonjs/core/Debug'; -import '../../helpers/babylonFileLoader' -import '@babylonjs/core/Meshes/instancedMesh' -import { meshFaceIds } from './meshFaceIds'; +import "../../helpers/babylonFileLoader"; +import "@babylonjs/core/Meshes/instancedMesh"; +import { meshFaceIds } from "./meshFaceIds"; const defaultOptions = { - assetPath: '', + assetPath: "", enableShadows: false, groupId: null, id: null, - lights: [], + lights: [], rollId: null, scene: null, sides: 6, - theme: 'purpleRock' -} + theme: "purpleRock", +}; class Dice { - mesh = null - result = 0 - asleep = false + mesh = null; + result = 0; + asleep = false; constructor(options) { - this.config = {...defaultOptions, ...options} - this.id = this.config.id !== undefined ? this.config.id : Date.now() - this.dieType = `d${this.config.sides}` - this.comboKey = `${this.dieType}_${this.config.theme}` + this.config = { ...defaultOptions, ...options }; + this.id = this.config.id !== undefined ? this.config.id : Date.now(); + this.dieType = `d${this.config.sides}`; + this.comboKey = `${this.dieType}_${this.config.theme}`; - this.createInstance() - + this.createInstance(); } createInstance() { - const dieInstance = this.config.scene.getMeshByName(this.comboKey).createInstance(`${this.dieType}-instance-${this.id}`) - - // start the instance under the floor, out of camera view - dieInstance.position.y = -100 - dieInstance.scaling = new Vector3(this.config.scale,this.config.scale,this.config.scale) - - if(this.config.enableShadows){ + const dieInstance = this.config.scene + .getMeshByName(this.comboKey) + .createInstance(`${this.dieType}-instance-${this.id}`); + + // start the instance under the floor, out of camera view + dieInstance.position.y = -100; + dieInstance.scaling = new Vector3( + this.config.scale, + this.config.scale, + this.config.scale + ); + + if (this.config.enableShadows) { for (const key in this.config.lights) { - if(key !== 'hemispheric' ) { - this.config.lights[key].shadowGenerator.addShadowCaster(dieInstance) + if (key !== "hemispheric") { + this.config.lights[key].shadowGenerator.addShadowCaster(dieInstance); } } } // attach the instance to the class object - this.mesh = dieInstance + this.mesh = dieInstance; } // TODO: add themeOptions for colored materials, must ensure theme and themeOptions are unique somehow static async loadDie(options) { - const { sides, theme = 'purpleRock', scene} = options - let dieType = 'd' + sides + const { sides, theme = "purpleRock", scene } = options; + let dieType = "d" + sides; // create a key for this die type and theme combo for caching and instance creation - const comboKey = `${dieType}_${theme}` + const comboKey = `${dieType}_${theme}`; if (!scene.getMeshByName(comboKey)) { - const die = scene.getMeshByName(dieType).clone(comboKey) - die.material = scene.getMaterialByName(theme) + const die = scene.getMeshByName(dieType).clone(comboKey); + die.material = scene.getMaterialByName(theme); // die.material.freeze() } - return options + return options; } // load all the dice models static async loadModels(options) { - const {assetPath, scene, scale} = options - const models = await SceneLoader.ImportMeshAsync(null,`${assetPath}models/`, "dice-revised.json", scene) - - models.meshes.forEach(model => { - if(model.id === "__root__") { - model.dispose() + const { assetPath, scene, scale } = options; + const models = await SceneLoader.ImportMeshAsync( + null, + `${assetPath}models/`, + "dice-revised.json", + scene + ); + + models.meshes.forEach((model) => { + if (model.id === "__root__") { + model.dispose(); } - if( model.id.includes("collider")) { - model.scaling = new Vector3(.7,.7,.7) + if (model.id.includes("collider")) { + model.scaling = new Vector3(0.7, 0.7, 0.7); } - model.setEnabled(false) - model.freezeNormals() + model.setEnabled(false); + model.freezeNormals(); model.isPickable = false; model.doNotSyncBoundingInfo = true; - }) + }); } updateConfig(option) { - this.config = {...this.config, ...option} + this.config = { ...this.config, ...option }; } - static ray = new Ray(Vector3.Zero(), Vector3.Zero(), 1) - static vector3 = new Vector3.Zero() + static ray = new Ray(Vector3.Zero(), Vector3.Zero(), 1); + static vector3 = new Vector3.Zero(); - static setVector3(x,y,z) { - return Dice.vector3.set(x,y,z) + static setVector3(x, y, z) { + return Dice.vector3.set(x, y, z); } - + static getVector3() { - return Dice.vector3 + return Dice.vector3; } static async getRollResult(die) { - const getDieRoll = (d=die) => new Promise((resolve,reject) => { - - const dieHitbox = d.config.scene.getMeshByName(`${d.dieType}_collider`).createInstance(`${d.dieType}-hitbox-${d.id}`) - dieHitbox.isPickable = true - dieHitbox.isVisible = true - dieHitbox.setEnabled(true) - dieHitbox.position = d.mesh.position - dieHitbox.rotationQuaternion = d.mesh.rotationQuaternion - - const vector = d.dieType === 'd4' ? Dice.setVector3(0, -1, 0) : Dice.setVector3(0, 1, 0) - - Dice.ray.direction = vector - Dice.ray.origin = d.mesh.position - - const picked = d.config.scene.pickWithRay(Dice.ray) - - dieHitbox.dispose() - - // let rayHelper = new RayHelper(Dice.ray) - // rayHelper.show(d.config.scene) - d.value = meshFaceIds[d.dieType][picked.faceId] - - return resolve(d.value) - }) - return await getDieRoll() + const getDieRoll = (d = die) => + new Promise((resolve, reject) => { + const dieHitbox = d.config.scene + .getMeshByName(`${d.dieType}_collider`) + .createInstance(`${d.dieType}-hitbox-${d.id}`); + dieHitbox.isPickable = true; + dieHitbox.isVisible = true; + dieHitbox.setEnabled(true); + dieHitbox.position = d.mesh.position; + dieHitbox.rotationQuaternion = d.mesh.rotationQuaternion; + + const vector = + d.dieType === "d4" + ? Dice.setVector3(0, -1, 0) + : Dice.setVector3(0, 1, 0); + + Dice.ray.direction = vector; + Dice.ray.origin = d.mesh.position; + + const picked = d.config.scene.pickWithRay(Dice.ray); + + dieHitbox.dispose(); + + // let rayHelper = new RayHelper(Dice.ray) + // rayHelper.show(d.config.scene) + d.value = meshFaceIds[d.dieType][picked.faceId]; + + return resolve(d.value); + }); + return await getDieRoll(); } } -export default Dice \ No newline at end of file +export default Dice; diff --git a/src/components/Dice/index.js b/src/components/Dice/index.js index c7c4ba7..2ff700b 100644 --- a/src/components/Dice/index.js +++ b/src/components/Dice/index.js @@ -1 +1 @@ -export { default } from './Dice' \ No newline at end of file +export { default } from "./Dice"; diff --git a/src/components/Dice/meshFaceIds.js b/src/components/Dice/meshFaceIds.js index 39c68ed..7301c83 100644 --- a/src/components/Dice/meshFaceIds.js +++ b/src/components/Dice/meshFaceIds.js @@ -1,136 +1,136 @@ export const meshFaceIds = { - d4: { - 0: 1, - 2: 2, - 1: 3, - 3: 4, - }, - d6: { - 2: 1, - 3: 1, - 0: 2, - 1: 2, - 10: 3, - 11: 3, - 8: 4, - 9: 4, - 4: 5, - 5: 5, - 6: 6, - 7: 6, - }, - d8: { - 1: 1, - 6: 2, - 5: 3, - 0: 4, - 2: 5, - 4: 6, - 7: 7, - 3: 8, - }, - d10: { - 18: 1, - 19: 1, - 14: 2, - 15: 2, - 4: 3, - 5: 3, - 16: 4, - 17: 4, - 12: 5, - 13: 5, - 6: 6, - 7: 6, - 2: 7, - 3: 7, - 0: 8, - 1: 8, - 10: 9, - 11: 9, - 8: 0, - 9: 0, - }, - d12: { - 21: 1, - 22: 1, - 23: 1, - 3: 2, - 4: 2, - 5: 2, - 0: 3, - 1: 3, - 2: 3, - 24: 4, - 25: 4, - 26: 4, - 18: 5, - 19: 5, - 20: 5, - 30: 6, - 31: 6, - 32: 6, - 6: 7, - 7: 7, - 8: 7, - 9: 8, - 10: 8, - 11: 8, - 15: 9, - 16: 9, - 17: 9, - 27: 10, - 28: 10, - 29: 10, - 33: 11, - 34: 11, - 35: 11, - 12: 12, - 13: 12, - 14: 12 - }, - d20: { - 10: 1, - 9: 2, - 12: 3, - 6: 4, - 0: 5, - 2: 6, - 14: 7, - 3: 8, - 19: 9, - 17: 10, - 1: 11, - 4: 12, - 15: 13, - 7: 14, - 16: 15, - 18: 16, - 13: 17, - 5: 18, - 11: 19, - 8: 20 - }, - d100: { - 18: 10, - 19: 10, - 14: 20, - 15: 20, - 12: 30, - 13: 30, - 8: 40, - 9: 40, - 10: 50, - 11: 50, - 16: 60, - 17: 60, - 2: 70, - 3: 70, - 0: 80, - 1: 80, - 4: 90, - 5: 90, - 6: 0, - 7: 0, - } -} \ No newline at end of file + d4: { + 0: 1, + 2: 2, + 1: 3, + 3: 4, + }, + d6: { + 2: 1, + 3: 1, + 0: 2, + 1: 2, + 10: 3, + 11: 3, + 8: 4, + 9: 4, + 4: 5, + 5: 5, + 6: 6, + 7: 6, + }, + d8: { + 1: 1, + 6: 2, + 5: 3, + 0: 4, + 2: 5, + 4: 6, + 7: 7, + 3: 8, + }, + d10: { + 18: 1, + 19: 1, + 14: 2, + 15: 2, + 4: 3, + 5: 3, + 16: 4, + 17: 4, + 12: 5, + 13: 5, + 6: 6, + 7: 6, + 2: 7, + 3: 7, + 0: 8, + 1: 8, + 10: 9, + 11: 9, + 8: 0, + 9: 0, + }, + d12: { + 21: 1, + 22: 1, + 23: 1, + 3: 2, + 4: 2, + 5: 2, + 0: 3, + 1: 3, + 2: 3, + 24: 4, + 25: 4, + 26: 4, + 18: 5, + 19: 5, + 20: 5, + 30: 6, + 31: 6, + 32: 6, + 6: 7, + 7: 7, + 8: 7, + 9: 8, + 10: 8, + 11: 8, + 15: 9, + 16: 9, + 17: 9, + 27: 10, + 28: 10, + 29: 10, + 33: 11, + 34: 11, + 35: 11, + 12: 12, + 13: 12, + 14: 12, + }, + d20: { + 10: 1, + 9: 2, + 12: 3, + 6: 4, + 0: 5, + 2: 6, + 14: 7, + 3: 8, + 19: 9, + 17: 10, + 1: 11, + 4: 12, + 15: 13, + 7: 14, + 16: 15, + 18: 16, + 13: 17, + 5: 18, + 11: 19, + 8: 20, + }, + d100: { + 18: 10, + 19: 10, + 14: 20, + 15: 20, + 12: 30, + 13: 30, + 8: 40, + 9: 40, + 10: 50, + 11: 50, + 16: 60, + 17: 60, + 2: 70, + 3: 70, + 0: 80, + 1: 80, + 4: 90, + 5: 90, + 6: 0, + 7: 0, + }, +}; diff --git a/src/components/Dice/themes.js b/src/components/Dice/themes.js index d0f5a47..2f59d51 100644 --- a/src/components/Dice/themes.js +++ b/src/components/Dice/themes.js @@ -1,85 +1,111 @@ -import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial' -import { Texture } from '@babylonjs/core/Materials/Textures/texture' -import { Color3 } from '@babylonjs/core/Maths/math.color' -import { CustomMaterial } from '@babylonjs/materials/custom/customMaterial'; +import { StandardMaterial } from "@babylonjs/core/Materials/standardMaterial"; +import { Texture } from "@babylonjs/core/Materials/Textures/texture"; +import { Color3 } from "@babylonjs/core/Maths/math.color"; +import { CustomMaterial } from "@babylonjs/materials/custom/customMaterial"; function sleeper(ms) { - return new Promise(resolve => setTimeout(() => resolve(), ms)); + return new Promise((resolve) => setTimeout(() => resolve(), ms)); } -async function loadStandardMaterial(theme,assetPath,scene) { - let diceMaterial = new StandardMaterial(theme,scene); - let diceTexture = await importTextureAsync(`${assetPath}themes/${theme}/albedo.jpg`,scene) - let diceBumpTexture = await importTextureAsync(`${assetPath}themes/${theme}/normal-dx.jpg`,scene) +async function loadStandardMaterial(theme, assetPath, scene) { + let diceMaterial = new StandardMaterial(theme, scene); + let diceTexture = await importTextureAsync( + `${assetPath}themes/${theme}/albedo.jpg`, + scene + ); + let diceBumpTexture = await importTextureAsync( + `${assetPath}themes/${theme}/normal-dx.jpg`, + scene + ); // let diceSpecTexture = await importTextureAsync(`${assetPath}themes/${theme}/specularity.jpg`,scene) - diceMaterial.diffuseTexture = diceTexture - diceMaterial.bumpTexture = diceBumpTexture + diceMaterial.diffuseTexture = diceTexture; + diceMaterial.bumpTexture = diceBumpTexture; // diceMaterial.specularTexture = diceSpecTexture // diceMaterial.specularPower = 1 - sharedSettings(diceMaterial) + sharedSettings(diceMaterial); - return diceMaterial + return diceMaterial; } -async function loadSemiTransparentMaterial(theme,assetPath,scene) { - let diceMaterial = new StandardMaterial(theme,scene); - let diceTexture = await importTextureAsync(`${assetPath}themes/${theme}/albedo.jpg`,scene) - let diceBumpTexture = await importTextureAsync(`${assetPath}themes/${theme}/normal-dx.jpg`,scene) - let diceOpacityTexture = await importTextureAsync(`${assetPath}themes/${theme}/mask.png`,scene) - diceMaterial.diffuseTexture = diceTexture - diceMaterial.opacityTexture = diceOpacityTexture - diceMaterial.opacityTexture.getAlphaFromRGB = true - diceMaterial.opacityTexture.vScale = -1 - // diceMaterial.backFaceCulling = false - diceMaterial.bumpTexture = diceBumpTexture - - sharedSettings(diceMaterial) - - return diceMaterial +async function loadSemiTransparentMaterial(theme, assetPath, scene) { + let diceMaterial = new StandardMaterial(theme, scene); + let diceTexture = await importTextureAsync( + `${assetPath}themes/${theme}/albedo.jpg`, + scene + ); + let diceBumpTexture = await importTextureAsync( + `${assetPath}themes/${theme}/normal-dx.jpg`, + scene + ); + let diceOpacityTexture = await importTextureAsync( + `${assetPath}themes/${theme}/mask.png`, + scene + ); + diceMaterial.diffuseTexture = diceTexture; + diceMaterial.opacityTexture = diceOpacityTexture; + diceMaterial.opacityTexture.getAlphaFromRGB = true; + diceMaterial.opacityTexture.vScale = -1; + // diceMaterial.backFaceCulling = false + diceMaterial.bumpTexture = diceBumpTexture; + + sharedSettings(diceMaterial); + + return diceMaterial; } -async function loadColorMaterial(theme,assetPath,scene) { - let color = Color3.FromHexString(theme) - let diceMaterial = new CustomMaterial(theme,scene); - let diceTexture - // console.log("color totals",(color.r*256*0.299 + color.g*256*0.587 + color.b*256*0.114)) - if ((color.r*256*0.299 + color.g*256*0.587 + color.b*256*0.114) > 175){ - diceTexture = await importTextureAsync(`${assetPath}themes/transparent/albedo-dark.png`,scene) - } else { - diceTexture = await importTextureAsync(`${assetPath}themes/transparent/albedo-light.png`,scene) - } - diceMaterial.diffuseTexture = diceTexture - // diceMaterial.diffuseTexture.hasAlpha = true; - // diceMaterial.useAlphaFromDiffuseTexture = true; - diceMaterial.Fragment_Custom_Diffuse(` +async function loadColorMaterial(theme, assetPath, scene) { + let color = Color3.FromHexString(theme); + let diceMaterial = new CustomMaterial(theme, scene); + let diceTexture; + // console.log("color totals",(color.r*256*0.299 + color.g*256*0.587 + color.b*256*0.114)) + if ( + color.r * 256 * 0.299 + color.g * 256 * 0.587 + color.b * 256 * 0.114 > + 175 + ) { + diceTexture = await importTextureAsync( + `${assetPath}themes/transparent/albedo-dark.png`, + scene + ); + } else { + diceTexture = await importTextureAsync( + `${assetPath}themes/transparent/albedo-light.png`, + scene + ); + } + diceMaterial.diffuseTexture = diceTexture; + // diceMaterial.diffuseTexture.hasAlpha = true; + // diceMaterial.useAlphaFromDiffuseTexture = true; + diceMaterial.Fragment_Custom_Diffuse(` baseColor.rgb = mix(vec3(${color.r},${color.g},${color.b}), baseColor.rgb, baseColor.a); - `) + `); - let diceBumpTexture = await importTextureAsync(`${assetPath}themes/transparent/normal-dx.jpg`,scene) - diceMaterial.bumpTexture = diceBumpTexture + let diceBumpTexture = await importTextureAsync( + `${assetPath}themes/transparent/normal-dx.jpg`, + scene + ); + diceMaterial.bumpTexture = diceBumpTexture; - sharedSettings(diceMaterial) + sharedSettings(diceMaterial); - return diceMaterial + return diceMaterial; } const sharedSettings = (material) => { - material.diffuseTexture.level = 1.3 + material.diffuseTexture.level = 1.3; // material.bumpTexture.level = 2 // material.invertNormalMapY = true // material.invertNormalMapX = true - // additional settings for .babylon file settings with Preserve Z-up right handed coordinate - // material.diffuseTexture.vScale = -1 + // additional settings for .babylon file settings with Preserve Z-up right handed coordinate + // material.diffuseTexture.vScale = -1 // material.bumpTexture.vScale = -1 - // material.diffuseTexture.uScale = -1 + // material.diffuseTexture.uScale = -1 // material.bumpTexture.uScale = -1 - material.allowShaderHotSwapping = false - return material -} - + material.allowShaderHotSwapping = false; + return material; +}; async function importTextureAsync(url, scene) { return new Promise((resolve, reject) => { @@ -91,24 +117,22 @@ async function importTextureAsync(url, scene) { undefined, // samplingMode?: number () => resolve(texture), // onLoad?: Nullable<() => void> () => reject("Unable to load texture") // onError?: Nullable<(message?: string - ) - }) + ); + }); } -const loadTheme = async (theme,p,s) => { - let material - if(theme.startsWith("#")){ - material = await loadColorMaterial(theme,p,s) - } - else if(theme.toLowerCase().startsWith("trans")) { - material = await loadSemiTransparentMaterial(theme,p,s) - } - else { +const loadTheme = async (theme, p, s) => { + let material; + if (theme.startsWith("#")) { + material = await loadColorMaterial(theme, p, s); + } else if (theme.toLowerCase().startsWith("trans")) { + material = await loadSemiTransparentMaterial(theme, p, s); + } else { // await sleeper(3000).then(async ()=>{ - material = await loadStandardMaterial(theme,p,s) + material = await loadStandardMaterial(theme, p, s); // }) } - return material -} + return material; +}; -export { loadTheme, importTextureAsync } \ No newline at end of file +export { loadTheme, importTextureAsync }; diff --git a/src/components/DiceBox.js b/src/components/DiceBox.js index 0f903b3..82ed4ef 100644 --- a/src/components/DiceBox.js +++ b/src/components/DiceBox.js @@ -1,115 +1,134 @@ -import { Color3 } from '@babylonjs/core/Maths/math.color' -import { CreateBox } from '@babylonjs/core/Meshes/Builders/boxBuilder' -import { StandardMaterial } from '@babylonjs/core/Materials/standardMaterial' -import { TransformNode } from '@babylonjs/core/Meshes/transformNode' -import { Vector3 } from '@babylonjs/core/Maths/math.vector' -import { ShadowOnlyMaterial } from '@babylonjs/materials/shadowOnly/shadowOnlyMaterial' +import { Color3 } from "@babylonjs/core/Maths/math.color"; +import { CreateBox } from "@babylonjs/core/Meshes/Builders/boxBuilder"; +import { StandardMaterial } from "@babylonjs/core/Materials/standardMaterial"; +import { TransformNode } from "@babylonjs/core/Meshes/transformNode"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import { ShadowOnlyMaterial } from "@babylonjs/materials/shadowOnly/shadowOnlyMaterial"; const defaultOptions = { aspect: 300 / 150, enableDebugging: false, enableShadows: true, -} +}; -class DiceBox{ - size = 9.5 - constructor(options){ - this.config = {...defaultOptions, ...options} - this.create() - } - create(options){ - // remove any previously existing boxes - this.destroy() - // extend config with options on create - Object.assign(this.config,options) - const { aspect, enableDebugging = true, enableShadows } = this.config - const wallHeight = 30 - let boxMaterial +class DiceBox { + size = 9.5; + constructor(options) { + this.config = { ...defaultOptions, ...options }; + this.create(); + } + create(options) { + // remove any previously existing boxes + this.destroy(); + // extend config with options on create + Object.assign(this.config, options); + const { aspect, enableDebugging = true, enableShadows } = this.config; + const wallHeight = 30; + let boxMaterial; - this.box = new TransformNode("diceBox"); + this.box = new TransformNode("diceBox"); - if(enableDebugging) { - boxMaterial = new StandardMaterial("diceBox_material") - boxMaterial.alpha = .7 - boxMaterial.diffuseColor = new Color3(1, 1, 0); - } - else { - if(enableShadows) { - boxMaterial = new ShadowOnlyMaterial('shadowOnly',this.config.scene) - // boxMaterial.alpha = 1 - // boxMaterial.diffuseColor = new Color3(1, 1, 1) - // boxMaterial.activeLight = lights.directional - } - } + if (enableDebugging) { + boxMaterial = new StandardMaterial("diceBox_material"); + boxMaterial.alpha = 0.7; + boxMaterial.diffuseColor = new Color3(1, 1, 0); + } else { + if (enableShadows) { + boxMaterial = new ShadowOnlyMaterial("shadowOnly", this.config.scene); + // boxMaterial.alpha = 1 + // boxMaterial.diffuseColor = new Color3(1, 1, 1) + // boxMaterial.activeLight = lights.directional + } + } - // Bottom of the Box - const ground = CreateBox("ground",{ - width: this.size, - height: 1, - depth: this.size - }, this.config.scene) - ground.scaling = new Vector3(aspect, 1, 1) - ground.material = boxMaterial - ground.receiveShadows = true - ground.setParent(this.box) + // Bottom of the Box + const ground = CreateBox( + "ground", + { + width: this.size, + height: 1, + depth: this.size, + }, + this.config.scene + ); + ground.scaling = new Vector3(aspect, 1, 1); + ground.material = boxMaterial; + ground.receiveShadows = true; + ground.setParent(this.box); - if(enableDebugging) { - // North Wall - const wallTop = CreateBox("wallTop",{ - width: this.size, - height: wallHeight, - depth: 1 - }, this.config.scene) - wallTop.position.y = wallHeight / 2 - wallTop.position.z = this.size / -2 - wallTop.scaling = new Vector3(aspect, 1, 1) - wallTop.material = boxMaterial - // wallTop.receiveShadows = true - wallTop.setParent(this.box) + if (enableDebugging) { + // North Wall + const wallTop = CreateBox( + "wallTop", + { + width: this.size, + height: wallHeight, + depth: 1, + }, + this.config.scene + ); + wallTop.position.y = wallHeight / 2; + wallTop.position.z = this.size / -2; + wallTop.scaling = new Vector3(aspect, 1, 1); + wallTop.material = boxMaterial; + // wallTop.receiveShadows = true + wallTop.setParent(this.box); - // Right Wall - const wallRight = CreateBox("wallRight",{ - width: 1, - height: wallHeight, - depth: this.size - }, this.config.scene ) - wallRight.position.x = this.size * aspect / 2 - wallRight.position.y = wallHeight / 2 - wallRight.material = boxMaterial - // wallRight.receiveShadows = true - wallRight.setParent(this.box) + // Right Wall + const wallRight = CreateBox( + "wallRight", + { + width: 1, + height: wallHeight, + depth: this.size, + }, + this.config.scene + ); + wallRight.position.x = (this.size * aspect) / 2; + wallRight.position.y = wallHeight / 2; + wallRight.material = boxMaterial; + // wallRight.receiveShadows = true + wallRight.setParent(this.box); - // South Wall - const wallBottom = CreateBox("wallBottom",{ - width: this.size, - height: wallHeight, - depth: 1 - }, this.config.scene) - wallBottom.position.y = wallHeight / 2 - wallBottom.position.z = this.size / 2 - wallBottom.scaling = new Vector3(aspect, 1, 1) - wallBottom.material = boxMaterial - // wallBottom.receiveShadows = true - wallBottom.setParent(this.box) + // South Wall + const wallBottom = CreateBox( + "wallBottom", + { + width: this.size, + height: wallHeight, + depth: 1, + }, + this.config.scene + ); + wallBottom.position.y = wallHeight / 2; + wallBottom.position.z = this.size / 2; + wallBottom.scaling = new Vector3(aspect, 1, 1); + wallBottom.material = boxMaterial; + // wallBottom.receiveShadows = true + wallBottom.setParent(this.box); - // Left Wall - const wallLeft = CreateBox("wallLeft",{ - width: 1, - height: wallHeight, - depth: this.size - }, this.config.scene) - wallLeft.position.x = this.size * aspect / -2 - wallLeft.position.y = wallHeight / 2 - wallLeft.material = boxMaterial - // wallLeft.receiveShadows = true - wallLeft.setParent(this.box) - } - } - destroy(){ - if(this.box) { - this.box.dispose() - } - } + // Left Wall + const wallLeft = CreateBox( + "wallLeft", + { + width: 1, + height: wallHeight, + depth: this.size, + }, + this.config.scene + ); + wallLeft.position.x = (this.size * aspect) / -2; + wallLeft.position.y = wallHeight / 2; + wallLeft.material = boxMaterial; + // wallLeft.receiveShadows = true + wallLeft.setParent(this.box); + } + } + destroy() { + if (this.box) { + this.box.dispose(); + } + } } -export default DiceBox \ No newline at end of file +export default DiceBox; diff --git a/src/components/camera.js b/src/components/camera.js index 4fe3d98..8708790 100644 --- a/src/components/camera.js +++ b/src/components/camera.js @@ -1,18 +1,22 @@ -import { Vector3 } from '@babylonjs/core/Maths/math.vector' -import { TargetCamera } from '@babylonjs/core/Cameras/targetCamera' +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import { TargetCamera } from "@babylonjs/core/Cameras/targetCamera"; // this module has dynamically loaded modules so it's been made async function createCamera(options) { - const { scene } = options - let camera - const cameraDistance = 36.5 + const { scene } = options; + let camera; + const cameraDistance = 36.5; - camera = new TargetCamera("TargetCamera1", new Vector3(0, cameraDistance, 0), scene) - camera.fov = .25 - camera.minZ = 5 - camera.maxZ = cameraDistance + 1 - camera.setTarget(Vector3.Zero()) - return camera + camera = new TargetCamera( + "TargetCamera1", + new Vector3(0, cameraDistance, 0), + scene + ); + camera.fov = 0.25; + camera.minZ = 5; + camera.maxZ = cameraDistance + 1; + camera.setTarget(Vector3.Zero()); + return camera; } -export { createCamera } \ No newline at end of file +export { createCamera }; diff --git a/src/components/canvas.js b/src/components/canvas.js index 7af054b..249f69f 100644 --- a/src/components/canvas.js +++ b/src/components/canvas.js @@ -1,22 +1,23 @@ function createCanvas(options) { - const { selector, id } = options + const { selector, id } = options; if (!selector) { - throw(new Error("You must provide a selector in order to render the Dice Box")) + throw new Error( + "You must provide a selector in order to render the Dice Box" + ); } - const container = document.querySelector(selector) - let canvas - - if(container.nodeName.toLowerCase() !== 'canvas') { - canvas = document.createElement('canvas') - canvas.id = id - container.appendChild(canvas) - } - else { - canvas = container + const container = document.querySelector(selector); + let canvas; + + if (container.nodeName.toLowerCase() !== "canvas") { + canvas = document.createElement("canvas"); + canvas.id = id; + container.appendChild(canvas); + } else { + canvas = container; } - return canvas + return canvas; } -export { createCanvas } \ No newline at end of file +export { createCanvas }; diff --git a/src/components/engine.js b/src/components/engine.js index de1dd4b..cbc8e0d 100644 --- a/src/components/engine.js +++ b/src/components/engine.js @@ -1,13 +1,13 @@ -import { Engine } from '@babylonjs/core/Engines/engine' -import "@babylonjs/core/Engines/Extensions/engine.debugging" +import { Engine } from "@babylonjs/core/Engines/engine"; +import "@babylonjs/core/Engines/Extensions/engine.debugging"; function createEngine(canvas) { const engine = new Engine(canvas, true, { preserveDrawingBuffer: true, stencil: true, - }) + }); - return engine + return engine; } -export { createEngine } \ No newline at end of file +export { createEngine }; diff --git a/src/components/lights.js b/src/components/lights.js index 6d18b57..8caa107 100644 --- a/src/components/lights.js +++ b/src/components/lights.js @@ -1,34 +1,42 @@ -import { DirectionalLight } from '@babylonjs/core/Lights/directionalLight' -import { HemisphericLight } from '@babylonjs/core/Lights/hemisphericLight' -import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator' -import { Vector3 } from '@babylonjs/core/Maths/math.vector' -import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent' +import { DirectionalLight } from "@babylonjs/core/Lights/directionalLight"; +import { HemisphericLight } from "@babylonjs/core/Lights/hemisphericLight"; +import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import "@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent"; const defaultOptions = { - enableShadows: true -} + enableShadows: true, +}; function createLights(options = defaultOptions) { - const { enableShadows, scene } = options - const d_light = new DirectionalLight("DirectionalLight", new Vector3(-0.3, -1, 0.4), scene) - d_light.position = new Vector3(-50,65,-50) - d_light.intensity = .65 - - const h_light = new HemisphericLight("HemisphericLight", new Vector3(1, 1, 0), scene) - h_light.intensity = .2 - - if(enableShadows){ - d_light.shadowMinZ = 1 - d_light.shadowMaxZ = 70 - // d_light.autoCalcShadowZBounds = true + const { enableShadows, scene } = options; + const d_light = new DirectionalLight( + "DirectionalLight", + new Vector3(-0.3, -1, 0.4), + scene + ); + d_light.position = new Vector3(-50, 65, -50); + d_light.intensity = 0.65; + + const h_light = new HemisphericLight( + "HemisphericLight", + new Vector3(1, 1, 0), + scene + ); + h_light.intensity = 0.2; + + if (enableShadows) { + d_light.shadowMinZ = 1; + d_light.shadowMaxZ = 70; + // d_light.autoCalcShadowZBounds = true d_light.shadowGenerator = new ShadowGenerator(2048, d_light); d_light.shadowGenerator.useCloseExponentialShadowMap = true; // best - d_light.shadowGenerator.darkness = .8 + d_light.shadowGenerator.darkness = 0.8; // d_light.shadowGenerator.usePoissonSampling = true // d_light.shadowGenerator.bias = .01 } - return {directional: d_light, hemispheric: h_light} + return { directional: d_light, hemispheric: h_light }; } -export { createLights } \ No newline at end of file +export { createLights }; diff --git a/src/components/offscreenCanvas.worker.js b/src/components/offscreenCanvas.worker.js index c685396..d238d68 100644 --- a/src/components/offscreenCanvas.worker.js +++ b/src/components/offscreenCanvas.worker.js @@ -1,371 +1,388 @@ -import { createEngine } from './engine' -import { createScene } from './scene' -import { createCamera } from './camera' -import { createLights } from './lights' -import DiceBox from './DiceBox' -import Dice from './Dice' -import { loadTheme } from './Dice/themes' -import { Vector3 } from '@babylonjs/core/Maths/math' - -let - config, - dieCache = {}, - count = 0, - sleeperCount = 0, - dieRollTimer = [], - canvas, - engine, - scene, - camera, - lights, - diceBox, - physicsWorkerPort, - diceBufferView = new Float32Array(8000) +import { createEngine } from "./engine"; +import { createScene } from "./scene"; +import { createCamera } from "./camera"; +import { createLights } from "./lights"; +import DiceBox from "./DiceBox"; +import Dice from "./Dice"; +import { loadTheme } from "./Dice/themes"; +import { Vector3 } from "@babylonjs/core/Maths/math"; + +let config, + dieCache = {}, + count = 0, + sleeperCount = 0, + dieRollTimer = [], + canvas, + engine, + scene, + camera, + lights, + diceBox, + physicsWorkerPort, + diceBufferView = new Float32Array(8000); // these are messages sent to this worker from World.js self.onmessage = (e) => { - switch( e.data.action ) { + switch (e.data.action) { case "rollDie": // kick it over to the physics worker physicsWorkerPort.postMessage({ - action: "roll" - }) - break + action: "roll", + }); + break; case "addDie": - add({...e.data.options}) - break - case "loadTheme": - loadThemes(e.data.id,e.data.theme) - break + add({ ...e.data.options }); + break; + case "loadTheme": + loadThemes(e.data.id, e.data.theme); + break; case "clearDice": - clear() - break - case "removeDie": - remove(e.data.options) - break; + clear(); + break; + case "removeDie": + remove(e.data.options); + break; case "resize": - resize(e.data.options) - break + resize(e.data.options); + break; case "init": - initScene(e.data) - break - case "updateConfig": - updateConfig(e.data.options) - break + initScene(e.data); + break; + case "updateConfig": + updateConfig(e.data.options); + break; case "connect": // These are messages sent from physics.worker.js - physicsWorkerPort = e.data.port + physicsWorkerPort = e.data.port; physicsWorkerPort.onmessage = (e) => { switch (e.data.action) { case "updates": // dice status/position updates from physics worker - updatesFromPhysics(e.data.diceBuffer) + updatesFromPhysics(e.data.diceBuffer); break; - + default: - console.error("action from physicsWorker not found in offscreen worker") + console.error( + "action from physicsWorker not found in offscreen worker" + ); break; } - } - break + }; + break; default: - console.error("action not found in offscreen worker") + console.error("action not found in offscreen worker"); } -} +}; // initialize the babylon scene const initScene = async (data) => { - canvas = data.canvas + canvas = data.canvas; - // set the config from World - config = data.options - canvas.width = data.width - canvas.height = data.height + // set the config from World + config = data.options; + canvas.width = data.width; + canvas.height = data.height; - // setup babylonJS scene - engine = createEngine(canvas) - scene = createScene({engine}) - camera = createCamera({engine}) - lights = createLights({enableShadows: config.enableShadows}) + // setup babylonJS scene + engine = createEngine(canvas); + scene = createScene({ engine }); + camera = createCamera({ engine }); + lights = createLights({ enableShadows: config.enableShadows }); // create the box that provides surfaces for shadows to render on - diceBox = new DiceBox({ - enableShadows: config.enableShadows, + diceBox = new DiceBox({ + enableShadows: config.enableShadows, aspect: canvas.width / canvas.height, lights, - scene, - enableDebugging: false - }) + scene, + enableDebugging: false, + }); // loading all our dice models // we use to load these models individually as needed, but it's faster to load them all at once and prevents animation jank when rolling await Dice.loadModels({ - assetPath: config.origin + config.assetPath, - scene, - scale: config.scale - }) - - physicsWorkerPort.postMessage({ - action: "initBuffer", - diceBuffer: diceBufferView.buffer - }, [diceBufferView.buffer]) + assetPath: config.origin + config.assetPath, + scene, + scale: config.scale, + }); + + physicsWorkerPort.postMessage( + { + action: "initBuffer", + diceBuffer: diceBufferView.buffer, + }, + [diceBufferView.buffer] + ); // init complete - let the world know - self.postMessage({action:"init-complete"}) -} + self.postMessage({ action: "init-complete" }); +}; const updateConfig = (options) => { - const prevConfig = config - config = options - // check if shadows setting has changed - if(prevConfig.enableShadows !== config.enableShadows) { - Object.values(lights).forEach(light => light.dispose()) - lights = createLights({enableShadows: config.enableShadows}) - } - if(prevConfig.scale !== config.scale) { - Object.values(dieCache).forEach(({mesh}) => { - mesh.scaling = new Vector3(config.scale,config.scale,config.scale) - }) - } -} + const prevConfig = config; + config = options; + // check if shadows setting has changed + if (prevConfig.enableShadows !== config.enableShadows) { + Object.values(lights).forEach((light) => light.dispose()); + lights = createLights({ enableShadows: config.enableShadows }); + } + if (prevConfig.scale !== config.scale) { + Object.values(dieCache).forEach(({ mesh }) => { + mesh.scaling = new Vector3(config.scale, config.scale, config.scale); + }); + } +}; // all this does is start the render engine. const render = (anustart) => { // document.body.addEventListener('click',()=>engine.stopRenderLoop()) - engine.runRenderLoop(renderLoop.bind(self)) - physicsWorkerPort.postMessage({ - action: "resumeSimulation", - anustart - }) -} + engine.runRenderLoop(renderLoop.bind(self)); + physicsWorkerPort.postMessage({ + action: "resumeSimulation", + anustart, + }); +}; const renderLoop = () => { // if no dice awake then stop the render loop and save some CPU power (unless we're in debug mode where we want the arc camera to continue working) - if(sleeperCount && sleeperCount === Object.keys(dieCache).length) { + if (sleeperCount && sleeperCount === Object.keys(dieCache).length) { // console.info(`no dice moving`) - engine.stopRenderLoop() - count = 0 - // stop the physics engine + engine.stopRenderLoop(); + count = 0; + // stop the physics engine physicsWorkerPort.postMessage({ action: "stopSimulation", - }) - // post back to the world - self.postMessage({ - action: "roll-complete" - }) + }); + // post back to the world + self.postMessage({ + action: "roll-complete", + }); } // otherwise keep on rendering else { - scene.render() + scene.render(); } -} +}; -const loadThemes = async (id,theme) => { - await loadTheme(theme, config.origin + config.assetPath, scene) - self.postMessage({action:"theme-loaded",id}) -} +const loadThemes = async (id, theme) => { + await loadTheme(theme, config.origin + config.assetPath, scene); + self.postMessage({ action: "theme-loaded", id }); +}; const clear = () => { - if(!Object.keys(dieCache).length && !sleeperCount) { - return - } - if(diceBufferView.byteLength){ - diceBufferView.fill(0) - } - dieRollTimer.forEach(timer=>clearTimeout(timer)) - // stop anything that's currently rendering - engine.stopRenderLoop() - // remove all dice - // dieCache.forEach(die => die.mesh.dispose()) - Object.values(dieCache).forEach(die => die.mesh.dispose()) - - dieCache = {} - count = 0 - sleeperCount = 0 - - // step the animation forward - scene.render() - -} + if (!Object.keys(dieCache).length && !sleeperCount) { + return; + } + if (diceBufferView.byteLength) { + diceBufferView.fill(0); + } + dieRollTimer.forEach((timer) => clearTimeout(timer)); + // stop anything that's currently rendering + engine.stopRenderLoop(); + // remove all dice + // dieCache.forEach(die => die.mesh.dispose()) + Object.values(dieCache).forEach((die) => die.mesh.dispose()); + + dieCache = {}; + count = 0; + sleeperCount = 0; + + // step the animation forward + scene.render(); +}; const add = (options) => { - // loadDie allows you to specify sides(dieType) and theme and returns the options you passed in - Dice.loadDie({ - ...options, - scene - }).then(resp => { - // space out adding the dice so they don't lump together too much - dieRollTimer.push(setTimeout(() => { - _add(resp) - }, count++ * config.delay)) - }) -} + // loadDie allows you to specify sides(dieType) and theme and returns the options you passed in + Dice.loadDie({ + ...options, + scene, + }).then((resp) => { + // space out adding the dice so they don't lump together too much + dieRollTimer.push( + setTimeout(() => { + _add(resp); + }, count++ * config.delay) + ); + }); +}; // add a die to the scene const _add = async (options) => { - if(engine.activeRenderLoops.length === 0) { - render(options.anustart) - } + if (engine.activeRenderLoops.length === 0) { + render(options.anustart); + } - const diceOptions = { - ...options, - assetPath: config.assetPath, - enableShadows: config.enableShadows, - scale: config.scale, - lights, - } + const diceOptions = { + ...options, + assetPath: config.assetPath, + enableShadows: config.enableShadows, + scale: config.scale, + lights, + }; - const newDie = new Dice(diceOptions) + const newDie = new Dice(diceOptions); // save the die just created to the cache - dieCache[newDie.id] = newDie + dieCache[newDie.id] = newDie; - // tell the physics engine to roll this die type - which is a low poly collider - physicsWorkerPort.postMessage({ - action: "addDie", - sides: options.sides, - scale: config.scale, - id: newDie.id, - anustart: options.anustart - }) + // tell the physics engine to roll this die type - which is a low poly collider + physicsWorkerPort.postMessage({ + action: "addDie", + sides: options.sides, + scale: config.scale, + id: newDie.id, + anustart: options.anustart, + }); // for d100's we need to add an additional d10 and pair it up with the d100 just created - if(options.sides === 100) { + if (options.sides === 100) { // assign the new die to a property on the d100 - spread the options in order to pass a matching theme - newDie.d10Instance = await Dice.loadDie({...diceOptions, sides: 10, id: newDie.id + 10000}).then( response => { - const d10Instance = new Dice(response) + newDie.d10Instance = await Dice.loadDie({ + ...diceOptions, + sides: 10, + id: newDie.id + 10000, + }).then((response) => { + const d10Instance = new Dice(response); // identify the parent of this d10 so we can calculate the roll result later - d10Instance.dieParent = newDie - return d10Instance - }) + d10Instance.dieParent = newDie; + return d10Instance; + }); // add the d10 to the cache and ask the physics worker for a collider - dieCache[`${newDie.d10Instance.id}`] = newDie.d10Instance + dieCache[`${newDie.d10Instance.id}`] = newDie.d10Instance; physicsWorkerPort.postMessage({ action: "addDie", sides: 10, - scale: config.scale, - id: newDie.d10Instance.id - }) + scale: config.scale, + id: newDie.d10Instance.id, + }); } // return the die instance - return newDie - -} + return newDie; +}; const remove = (data) => { - // TODO: test this with exploding dice - // check if this is d100 and remove associated d10 first - const dieData = dieCache[data.id] - if(dieData.hasOwnProperty('d10Instance')){ - dieCache[dieData.d10Instance.id].mesh.dispose() - delete dieCache[dieData.d10Instance.id] - physicsWorkerPort.postMessage({ + // TODO: test this with exploding dice + // check if this is d100 and remove associated d10 first + const dieData = dieCache[data.id]; + if (dieData.hasOwnProperty("d10Instance")) { + dieCache[dieData.d10Instance.id].mesh.dispose(); + delete dieCache[dieData.d10Instance.id]; + physicsWorkerPort.postMessage({ action: "removeDie", - id: dieData.d10Instance.id - }) - sleeperCount-- - } + id: dieData.d10Instance.id, + }); + sleeperCount--; + } - // remove die - dieData.mesh.dispose() - // delete entry - delete dieCache[data.id] - // decrement count - sleeperCount-- + // remove die + dieData.mesh.dispose(); + // delete entry + delete dieCache[data.id]; + // decrement count + sleeperCount--; - // step the animation forward - scene.render() + // step the animation forward + scene.render(); - self.postMessage({action:"die-removed", rollId: data.rollId}) -} + self.postMessage({ action: "die-removed", rollId: data.rollId }); +}; const updatesFromPhysics = (buffer) => { - diceBufferView = new Float32Array(buffer) - let bufferIndex = 1 - - // loop will be based on diceBufferView[0] value which is the bodies length in physics.worker - for (let i = 0, len = diceBufferView[0]; i < len; i++) { - if(!Object.keys(dieCache).length){ - continue - } - const die = dieCache[`${diceBufferView[bufferIndex]}`] - if(!die) { - console.log("Error: die not available in scene to animate") - break - } - // if the first position index is -1 then this die has been flagged as asleep - if(diceBufferView[bufferIndex + 1] === -1) { - handleAsleep(die) - } else { - const px = diceBufferView[bufferIndex + 1] - const py = diceBufferView[bufferIndex + 2] - const pz = diceBufferView[bufferIndex + 3] - const qx = diceBufferView[bufferIndex + 4] - const qy = diceBufferView[bufferIndex + 5] - const qz = diceBufferView[bufferIndex + 6] - const qw = diceBufferView[bufferIndex + 7] - - die.mesh.position.set(px, py, pz) - die.mesh.rotationQuaternion.set(qx, qy, qz, qw) - } - - bufferIndex = bufferIndex + 8 - } - - // transfer the buffer back to physics worker - requestAnimationFrame(()=>{ - physicsWorkerPort.postMessage({ - action: "stepSimulation", - diceBuffer: diceBufferView.buffer - }, [diceBufferView.buffer]) - }) -} + diceBufferView = new Float32Array(buffer); + let bufferIndex = 1; + + // loop will be based on diceBufferView[0] value which is the bodies length in physics.worker + for (let i = 0, len = diceBufferView[0]; i < len; i++) { + if (!Object.keys(dieCache).length) { + continue; + } + const die = dieCache[`${diceBufferView[bufferIndex]}`]; + if (!die) { + console.log("Error: die not available in scene to animate"); + break; + } + // if the first position index is -1 then this die has been flagged as asleep + if (diceBufferView[bufferIndex + 1] === -1) { + handleAsleep(die); + } else { + const px = diceBufferView[bufferIndex + 1]; + const py = diceBufferView[bufferIndex + 2]; + const pz = diceBufferView[bufferIndex + 3]; + const qx = diceBufferView[bufferIndex + 4]; + const qy = diceBufferView[bufferIndex + 5]; + const qz = diceBufferView[bufferIndex + 6]; + const qw = diceBufferView[bufferIndex + 7]; + + die.mesh.position.set(px, py, pz); + die.mesh.rotationQuaternion.set(qx, qy, qz, qw); + } + + bufferIndex = bufferIndex + 8; + } + + // transfer the buffer back to physics worker + requestAnimationFrame(() => { + physicsWorkerPort.postMessage( + { + action: "stepSimulation", + diceBuffer: diceBufferView.buffer, + }, + [diceBufferView.buffer] + ); + }); +}; const handleAsleep = async (die) => { - // mark this die as asleep - die.asleep = true - - // get the roll result for this die - let result = await Dice.getRollResult(die) - // TODO: catch error if no result is found - if(result === undefined) { - console.log("No result. This die needs a reroll.") - } - - if(die.d10Instance || die.dieParent) { - // if one of the pair is asleep and the other isn't then it falls through without getting the roll result - // otherwise both dice in the d100 are asleep and ready to calc their roll result - if(die?.d10Instance?.asleep || die?.dieParent?.asleep) { - const d100 = die.config.sides === 100 ? die : die.dieParent - const d10 = die.config.sides === 10 ? die : die.d10Instance - if (d10.value === 0 && d100.value === 0) { - d100.value = 100; // 00 + 0 is 100 on a d100 - } else { - d100.value = d100.value + d10.value - } - - self.postMessage({action:"roll-result", die: { - rollId: d100.config.rollId, - value : d100.value - }}) - } - } else { - // turn 0's on a d10 into a 10 - if(die.config.sides === 10 && die.value === 0) { - die.value = 10 - } - self.postMessage({action:"roll-result", die: { - rollId: die.config.rollId, - value: die.value - }}) - } - // add to the sleeper count - sleeperCount++ -} + // mark this die as asleep + die.asleep = true; + + // get the roll result for this die + let result = await Dice.getRollResult(die); + // TODO: catch error if no result is found + if (result === undefined) { + console.log("No result. This die needs a reroll."); + } + + if (die.d10Instance || die.dieParent) { + // if one of the pair is asleep and the other isn't then it falls through without getting the roll result + // otherwise both dice in the d100 are asleep and ready to calc their roll result + if (die?.d10Instance?.asleep || die?.dieParent?.asleep) { + const d100 = die.config.sides === 100 ? die : die.dieParent; + const d10 = die.config.sides === 10 ? die : die.d10Instance; + if (d10.value === 0 && d100.value === 0) { + d100.value = 100; // 00 + 0 is 100 on a d100 + } else { + d100.value = d100.value + d10.value; + } + + self.postMessage({ + action: "roll-result", + die: { + rollId: d100.config.rollId, + value: d100.value, + }, + }); + } + } else { + // turn 0's on a d10 into a 10 + if (die.config.sides === 10 && die.value === 0) { + die.value = 10; + } + self.postMessage({ + action: "roll-result", + die: { + rollId: die.config.rollId, + value: die.value, + }, + }); + } + // add to the sleeper count + sleeperCount++; +}; const resize = (data) => { - canvas.width = data.width - canvas.height = data.height - // redraw the dicebox - diceBox.create({aspect: data.width / data.height}) - engine.resize() -} \ No newline at end of file + canvas.width = data.width; + canvas.height = data.height; + // redraw the dicebox + diceBox.create({ aspect: data.width / data.height }); + engine.resize(); +}; diff --git a/src/components/physics.worker.js b/src/components/physics.worker.js index b8beefb..7c3ff55 100644 --- a/src/components/physics.worker.js +++ b/src/components/physics.worker.js @@ -1,545 +1,608 @@ -import { lerp } from '../helpers' -import AmmoJS from "../ammo/ammo.wasm.es.js" +import { lerp } from "../helpers"; +import AmmoJS from "../ammo/ammo.wasm.es.js"; // Firefox limitation: https://github.com/vitejs/vite/issues/4586 // there's probably a better place for these variables -let bodies = [] -let sleepingBodies = [] -let colliders = {} -let physicsWorld -let Ammo -let worldWorkerPort -let tmpBtTrans -let sharedVector3 -let width = 150 -let height = 150 -let aspect = 1 -let stopLoop = false -let spinScale = 60 +let bodies = []; +let sleepingBodies = []; +let colliders = {}; +let physicsWorld; +let Ammo; +let worldWorkerPort; +let tmpBtTrans; +let sharedVector3; +let width = 150; +let height = 150; +let aspect = 1; +let stopLoop = false; +let spinScale = 60; const defaultOptions = { - size: 9.5, - startingHeight: 12, - spinForce: 3, - throwForce: 2, - gravity: 1, - mass: 1, - friction: .8, - restitution: .1, - linearDamping: .4, - angularDamping: .4, - settleTimeout: 5000, - // TODO: toss: "center", "edge", "allEdges" -} - -let config = {...defaultOptions} - -let emptyVector -let diceBufferView + size: 9.5, + startingHeight: 12, + spinForce: 3, + throwForce: 2, + gravity: 1, + mass: 1, + friction: 0.8, + restitution: 0.1, + linearDamping: 0.4, + angularDamping: 0.4, + settleTimeout: 5000, + // TODO: toss: "center", "edge", "allEdges" +}; + +let config = { ...defaultOptions }; + +let emptyVector; +let diceBufferView; self.onmessage = (e) => { switch (e.data.action) { case "rollDie": - rollDie(e.data.sides) + rollDie(e.data.sides); break; case "init": - init(e.data).then(()=>{ + init(e.data).then(() => { self.postMessage({ - action:"init-complete" - }) - }) - break + action: "init-complete", + }); + }); + break; case "clearDice": - clearDice() - break - case "removeDie": - removeDie(e.data.id) - break; - case "resize": - width = e.data.width - height = e.data.height - aspect = width / height - addBoxToWorld(config.size, config.startingHeight + 10) - break - case "updateConfig": - updateConfig(e.data.options) - break + clearDice(); + break; + case "removeDie": + removeDie(e.data.id); + break; + case "resize": + width = e.data.width; + height = e.data.height; + aspect = width / height; + addBoxToWorld(config.size, config.startingHeight + 10); + break; + case "updateConfig": + updateConfig(e.data.options); + break; case "connect": - worldWorkerPort = e.ports[0] + worldWorkerPort = e.ports[0]; worldWorkerPort.onmessage = (e) => { switch (e.data.action) { - case "initBuffer": - diceBufferView = new Float32Array(e.data.diceBuffer) - diceBufferView[0] = -1 - break; + case "initBuffer": + diceBufferView = new Float32Array(e.data.diceBuffer); + diceBufferView[0] = -1; + break; case "addDie": - // toss from all edges - // setStartPosition() - if(e.data.anustart){ - setStartPosition() - } - addDie(e.data.sides, e.data.id) + // toss from all edges + // setStartPosition() + if (e.data.anustart) { + setStartPosition(); + } + addDie(e.data.sides, e.data.id); break; case "rollDie": - // TODO: this won't work, need a die object - rollDie(e.data.id) + // TODO: this won't work, need a die object + rollDie(e.data.id); + break; + case "removeDie": + removeDie(e.data.id); break; - case "removeDie": - removeDie(e.data.id) - break; case "stopSimulation": - stopLoop = true - + stopLoop = true; + break; case "resumeSimulation": - if(e.data.anustart){ - setStartPosition() - } - stopLoop = false - loop() + if (e.data.anustart) { + setStartPosition(); + } + stopLoop = false; + loop(); + break; + case "stepSimulation": + diceBufferView = new Float32Array(e.data.diceBuffer); + loop(); break; - case "stepSimulation": - diceBufferView = new Float32Array(e.data.diceBuffer) - loop() - break; default: - console.error("action not found in physics worker from worldOffscreen worker:", e.data.action) + console.error( + "action not found in physics worker from worldOffscreen worker:", + e.data.action + ); } - } - break + }; + break; default: - console.error("action not found in physics worker:", e.data.action) + console.error("action not found in physics worker:", e.data.action); } -} +}; // runs when the worker loads to set up the Ammo physics world and load our colliders // loaded colliders will be cached and added to the world in a later post message const init = async (data) => { - width = data.width - height = data.height - aspect = width / height - - config = {...config,...data.options} - config.gravity === 0 ? 0 : config.gravity + config.mass / 3 - config.mass = 1 + config.mass / 3 - config.spinForce = config.spinForce/spinScale - config.throwForce = config.throwForce / 2 / config.mass * (1 + config.scale / 6) - // config.spinForce = (config.spinForce/100) * (config.scale * (config.scale < 1 ? .5 : 2)) - // config.throwForce = config.throwForce * (config.scale < 1 ? 2 - (config.scale ** config.scale) : 1 + config.scale/6) - // ensure minimum startingHeight of 1 - config.startingHeight = config.startingHeight < 1 ? 1 : config.startingHeight - - const ammoWASM = { - // locateFile: () => '../../node_modules/ammo.js/builds/ammo.wasm.wasm' - locateFile: () => `${config.origin + config.assetPath}ammo/ammo.wasm.wasm` - } - - Ammo = await new AmmoJS(ammoWASM) - - tmpBtTrans = new Ammo.btTransform() - sharedVector3 = new Ammo.btVector3(0, 0, 0) - emptyVector = setVector3(0,0,0) - - setStartPosition() - - // load our collider data - // perhaps we don't await this, let it run and resolve it later - const modelData = await fetch(`${config.origin + config.assetPath}models/dice-revised.json`).then(resp => { - if(resp.ok) { - const contentType = resp.headers.get("content-type") - - if (contentType && contentType.indexOf("application/json") !== -1) { - return resp.json() - } - else if (resp.type && resp.type === 'basic') { - return resp.json() - } - else { - return resp - } - } else { - throw new Error(`Request rejected with status ${resp.status}: ${resp.statusText}`) - } - }) - .then(data => { - return data.meshes.filter(mesh => { - return mesh.id.includes("collider") - }) - }) - .catch(error => { - console.error(error) - return error - }) - - physicsWorld = setupPhysicsWorld() - - // turn our model data into convex hull items for the physics world - modelData.forEach((model,i) => { - model.convexHull = createConvexHull(model) - // model.physicsBody = createRigidBody(model.convexHull, {mass: model.mass}) - - colliders[model.id] = model - }) - - addBoxToWorld(config.size, config.startingHeight + 10) - -} + width = data.width; + height = data.height; + aspect = width / height; + + config = { ...config, ...data.options }; + config.gravity === 0 ? 0 : config.gravity + config.mass / 3; + config.mass = 1 + config.mass / 3; + config.spinForce = config.spinForce / spinScale; + config.throwForce = + (config.throwForce / 2 / config.mass) * (1 + config.scale / 6); + // config.spinForce = (config.spinForce/100) * (config.scale * (config.scale < 1 ? .5 : 2)) + // config.throwForce = config.throwForce * (config.scale < 1 ? 2 - (config.scale ** config.scale) : 1 + config.scale/6) + // ensure minimum startingHeight of 1 + config.startingHeight = config.startingHeight < 1 ? 1 : config.startingHeight; + + const ammoWASM = { + // locateFile: () => '../../node_modules/ammo.js/builds/ammo.wasm.wasm' + locateFile: () => `${config.origin + config.assetPath}ammo/ammo.wasm.wasm`, + }; + + Ammo = await new AmmoJS(ammoWASM); + + tmpBtTrans = new Ammo.btTransform(); + sharedVector3 = new Ammo.btVector3(0, 0, 0); + emptyVector = setVector3(0, 0, 0); + + setStartPosition(); + + // load our collider data + // perhaps we don't await this, let it run and resolve it later + const modelData = await fetch( + `${config.origin + config.assetPath}models/dice-revised.json` + ) + .then((resp) => { + if (resp.ok) { + const contentType = resp.headers.get("content-type"); + + if (contentType && contentType.indexOf("application/json") !== -1) { + return resp.json(); + } else if (resp.type && resp.type === "basic") { + return resp.json(); + } else { + return resp; + } + } else { + throw new Error( + `Request rejected with status ${resp.status}: ${resp.statusText}` + ); + } + }) + .then((data) => { + return data.meshes.filter((mesh) => { + return mesh.id.includes("collider"); + }); + }) + .catch((error) => { + console.error(error); + return error; + }); + + physicsWorld = setupPhysicsWorld(); + + // turn our model data into convex hull items for the physics world + modelData.forEach((model, i) => { + model.convexHull = createConvexHull(model); + // model.physicsBody = createRigidBody(model.convexHull, {mass: model.mass}) + + colliders[model.id] = model; + }); + + addBoxToWorld(config.size, config.startingHeight + 10); +}; const updateConfig = (options) => { - config = {...config,...options} - config.mass = 1 + config.mass / 3 - config.gravity = config.gravity === 0 ? 0 : config.gravity + config.mass / 3 - config.spinForce = config.spinForce/spinScale - config.throwForce = config.throwForce / 2 / config.mass * (1 + config.scale / 6) - config.startingHeight = config.startingHeight < 1 ? 1 : config.startingHeight - removeBoxFromWorld() - addBoxToWorld(config.size, config.startingHeight + 10) - physicsWorld.setGravity(setVector3(0, -9.81 * config.gravity, 0)) - Object.values(colliders).map((collider) => { - collider.convexHull.setLocalScaling(setVector3(config.scale, config.scale, config.scale)) - }) - -} - -const setVector3 = (x,y,z) => { - sharedVector3.setValue(x,y,z) - return sharedVector3 -} + config = { ...config, ...options }; + config.mass = 1 + config.mass / 3; + config.gravity = config.gravity === 0 ? 0 : config.gravity + config.mass / 3; + config.spinForce = config.spinForce / spinScale; + config.throwForce = + (config.throwForce / 2 / config.mass) * (1 + config.scale / 6); + config.startingHeight = config.startingHeight < 1 ? 1 : config.startingHeight; + removeBoxFromWorld(); + addBoxToWorld(config.size, config.startingHeight + 10); + physicsWorld.setGravity(setVector3(0, -9.81 * config.gravity, 0)); + Object.values(colliders).map((collider) => { + collider.convexHull.setLocalScaling( + setVector3(config.scale, config.scale, config.scale) + ); + }); +}; + +const setVector3 = (x, y, z) => { + sharedVector3.setValue(x, y, z); + return sharedVector3; +}; const setStartPosition = () => { - let size = config.size - // let envelopeSize = size * .6 / 2 - let edgeOffset = .5 - let xMin = size * aspect / 2 - edgeOffset - let xMax = size * aspect / -2 + edgeOffset - let yMin = size / 2 - edgeOffset - let yMax = size / -2 + edgeOffset - // let xEnvelope = lerp(envelopeSize * aspect - edgeOffset * aspect, -envelopeSize * aspect + edgeOffset * aspect, Math.random()) - let xEnvelope = lerp(xMin, xMax, Math.random()) - let yEnvelope = lerp(yMin, yMax, Math.random()) - let tossFromTop = Math.round(Math.random()) - let tossFromLeft = Math.round(Math.random()) - let tossX = Math.round(Math.random()) - // console.log(`throw coming from`, tossX ? tossFromTop ? "top" : "bottom" : tossFromLeft ? "left" : "right") - - // forces = { - // xMinForce: tossX ? -config.throwForce * aspect : tossFromLeft ? config.throwForce * aspect * .3 : -config.throwForce * aspect * .3, - // xMaxForce: tossX ? config.throwForce * aspect : tossFromLeft ? config.throwForce * aspect * 1 : -config.throwForce * aspect * 1, - // zMinForce: tossX ? tossFromTop ? config.throwForce * .3 : -config.throwForce * .3 : -config.throwForce, - // zMaxForce: tossX ? tossFromTop ? config.throwForce * 1 : -config.throwForce * 1 : config.throwForce, - // } - - config.startPosition = [ - // tossing on x axis then z should be locked to top or bottom - // not tossing on x axis then x should be locked to the left or right - tossX ? xEnvelope : tossFromLeft ? xMax : xMin, - config.startingHeight, - tossX ? tossFromTop ? yMax : yMin : yEnvelope - ] - - // console.log(`startPosition`, config.startPosition) -} + let size = config.size; + // let envelopeSize = size * .6 / 2 + let edgeOffset = 0.5; + let xMin = (size * aspect) / 2 - edgeOffset; + let xMax = (size * aspect) / -2 + edgeOffset; + let yMin = size / 2 - edgeOffset; + let yMax = size / -2 + edgeOffset; + // let xEnvelope = lerp(envelopeSize * aspect - edgeOffset * aspect, -envelopeSize * aspect + edgeOffset * aspect, Math.random()) + let xEnvelope = lerp(xMin, xMax, Math.random()); + let yEnvelope = lerp(yMin, yMax, Math.random()); + let tossFromTop = Math.round(Math.random()); + let tossFromLeft = Math.round(Math.random()); + let tossX = Math.round(Math.random()); + // console.log(`throw coming from`, tossX ? tossFromTop ? "top" : "bottom" : tossFromLeft ? "left" : "right") + + // forces = { + // xMinForce: tossX ? -config.throwForce * aspect : tossFromLeft ? config.throwForce * aspect * .3 : -config.throwForce * aspect * .3, + // xMaxForce: tossX ? config.throwForce * aspect : tossFromLeft ? config.throwForce * aspect * 1 : -config.throwForce * aspect * 1, + // zMinForce: tossX ? tossFromTop ? config.throwForce * .3 : -config.throwForce * .3 : -config.throwForce, + // zMaxForce: tossX ? tossFromTop ? config.throwForce * 1 : -config.throwForce * 1 : config.throwForce, + // } + + config.startPosition = [ + // tossing on x axis then z should be locked to top or bottom + // not tossing on x axis then x should be locked to the left or right + tossX ? xEnvelope : tossFromLeft ? xMax : xMin, + config.startingHeight, + tossX ? (tossFromTop ? yMax : yMin) : yEnvelope, + ]; + + // console.log(`startPosition`, config.startPosition) +}; const createConvexHull = (mesh) => { - const convexMesh = new Ammo.btConvexHullShape() + const convexMesh = new Ammo.btConvexHullShape(); - let count = mesh.positions.length + let count = mesh.positions.length; - for (let i = 0; i < count; i+=3) { - let v = setVector3(mesh.positions[i], mesh.positions[i+1], mesh.positions[i+2]) - convexMesh.addPoint(v, true) - } + for (let i = 0; i < count; i += 3) { + let v = setVector3( + mesh.positions[i], + mesh.positions[i + 1], + mesh.positions[i + 2] + ); + convexMesh.addPoint(v, true); + } - convexMesh.setLocalScaling(setVector3(mesh.scaling[0] * config.scale, mesh.scaling[1] * config.scale, mesh.scaling[2] * config.scale)) + convexMesh.setLocalScaling( + setVector3( + mesh.scaling[0] * config.scale, + mesh.scaling[1] * config.scale, + mesh.scaling[2] * config.scale + ) + ); - return convexMesh -} + return convexMesh; +}; const createRigidBody = (collisionShape, params) => { - // apply params - const { - mass = .1, - collisionFlags = 0, - // pos = { x: 0, y: 0, z: 0 }, - // quat = { x: 0, y: 0, z: 0, w: 1 } - pos = [0,0,0], - // quat = [0,0,0,-1], - quat = [ - lerp(-1.5, 1.5, Math.random()), - lerp(-1.5, 1.5, Math.random()), - lerp(-1.5, 1.5, Math.random()), - -1 - ], - scale = [1,1,1], - friction = config.friction, - restitution = config.restitution - } = params - - // apply position and rotation - const transform = new Ammo.btTransform() - // console.log(`collisionShape scaling `, collisionShape.getLocalScaling().x(),collisionShape.getLocalScaling().y(),collisionShape.getLocalScaling().z()) - transform.setIdentity() - transform.setOrigin(setVector3(pos[0], pos[1], pos[2])) - transform.setRotation( - new Ammo.btQuaternion(quat[0], quat[1], quat[2], quat[3]) - ) - // collisionShape.setLocalScaling(new Ammo.btVector3(1.1, -1.1, 1.1)) - // transform.ScalingToRef() - // set the scale of the collider - // collisionShape.setLocalScaling(new Ammo.btVector3(scale[0],scale[1],scale[2])) - - // create the rigid body - const motionState = new Ammo.btDefaultMotionState(transform) - const localInertia = setVector3(0, 0, 0) - if (mass > 0) collisionShape.calculateLocalInertia(mass, localInertia) - const rbInfo = new Ammo.btRigidBodyConstructionInfo( - mass, - motionState, - collisionShape, - localInertia - ) - const rigidBody = new Ammo.btRigidBody(rbInfo) - - // rigid body properties - if (mass > 0) rigidBody.setActivationState(4) // Disable deactivation - rigidBody.setCollisionFlags(collisionFlags) - rigidBody.setFriction(friction) - rigidBody.setRestitution(restitution) - rigidBody.setDamping(config.linearDamping, config.angularDamping) - - // ad rigid body to physics world - // physicsWorld.addRigidBody(rigidBody) - - return rigidBody - -} + // apply params + const { + mass = 0.1, + collisionFlags = 0, + // pos = { x: 0, y: 0, z: 0 }, + // quat = { x: 0, y: 0, z: 0, w: 1 } + pos = [0, 0, 0], + // quat = [0,0,0,-1], + quat = [ + lerp(-1.5, 1.5, Math.random()), + lerp(-1.5, 1.5, Math.random()), + lerp(-1.5, 1.5, Math.random()), + -1, + ], + scale = [1, 1, 1], + friction = config.friction, + restitution = config.restitution, + } = params; + + // apply position and rotation + const transform = new Ammo.btTransform(); + // console.log(`collisionShape scaling `, collisionShape.getLocalScaling().x(),collisionShape.getLocalScaling().y(),collisionShape.getLocalScaling().z()) + transform.setIdentity(); + transform.setOrigin(setVector3(pos[0], pos[1], pos[2])); + transform.setRotation( + new Ammo.btQuaternion(quat[0], quat[1], quat[2], quat[3]) + ); + // collisionShape.setLocalScaling(new Ammo.btVector3(1.1, -1.1, 1.1)) + // transform.ScalingToRef() + // set the scale of the collider + // collisionShape.setLocalScaling(new Ammo.btVector3(scale[0],scale[1],scale[2])) + + // create the rigid body + const motionState = new Ammo.btDefaultMotionState(transform); + const localInertia = setVector3(0, 0, 0); + if (mass > 0) collisionShape.calculateLocalInertia(mass, localInertia); + const rbInfo = new Ammo.btRigidBodyConstructionInfo( + mass, + motionState, + collisionShape, + localInertia + ); + const rigidBody = new Ammo.btRigidBody(rbInfo); + + // rigid body properties + if (mass > 0) rigidBody.setActivationState(4); // Disable deactivation + rigidBody.setCollisionFlags(collisionFlags); + rigidBody.setFriction(friction); + rigidBody.setRestitution(restitution); + rigidBody.setDamping(config.linearDamping, config.angularDamping); + + // ad rigid body to physics world + // physicsWorld.addRigidBody(rigidBody) + + return rigidBody; +}; // cache for box parts so it can be removed after a new one has been made -let boxParts = [] +let boxParts = []; const addBoxToWorld = (size, height) => { - const tempParts = [] - // ground - const localInertia = setVector3(0, 0, 0); - const groundTransform = new Ammo.btTransform() - groundTransform.setIdentity() - groundTransform.setOrigin(setVector3(0, -.5, 0)) - const groundShape = new Ammo.btBoxShape(setVector3(size * aspect, 1, size)) - const groundMotionState = new Ammo.btDefaultMotionState(groundTransform) - const groundInfo = new Ammo.btRigidBodyConstructionInfo(0, groundMotionState, groundShape, localInertia) - const groundBody = new Ammo.btRigidBody(groundInfo) - groundBody.setFriction(config.friction) - groundBody.setRestitution(config.restitution) - physicsWorld.addRigidBody(groundBody) - tempParts.push(groundBody) - - const wallTopTransform = new Ammo.btTransform() - wallTopTransform.setIdentity() - wallTopTransform.setOrigin(setVector3(0, 0, (size/-2) - .5)) - const wallTopShape = new Ammo.btBoxShape(setVector3(size * aspect, height, 1)) - const topMotionState = new Ammo.btDefaultMotionState(wallTopTransform) - const topInfo = new Ammo.btRigidBodyConstructionInfo(0, topMotionState, wallTopShape, localInertia) - const topBody = new Ammo.btRigidBody(topInfo) - topBody.setFriction(config.friction) - topBody.setRestitution(config.restitution) - physicsWorld.addRigidBody(topBody) - tempParts.push(topBody) - - const wallBottomTransform = new Ammo.btTransform() - wallBottomTransform.setIdentity() - wallBottomTransform.setOrigin(setVector3(0, 0, (size/2) + .5)) - const wallBottomShape = new Ammo.btBoxShape(setVector3(size * aspect, height, 1)) - const bottomMotionState = new Ammo.btDefaultMotionState(wallBottomTransform) - const bottomInfo = new Ammo.btRigidBodyConstructionInfo(0, bottomMotionState, wallBottomShape, localInertia) - const bottomBody = new Ammo.btRigidBody(bottomInfo) - bottomBody.setFriction(config.friction) - bottomBody.setRestitution(config.restitution) - physicsWorld.addRigidBody(bottomBody) - tempParts.push(bottomBody) - - const wallRightTransform = new Ammo.btTransform() - wallRightTransform.setIdentity() - wallRightTransform.setOrigin(setVector3((size * aspect / -2) - .5, 0, 0)) - const wallRightShape = new Ammo.btBoxShape(setVector3(1, height, size)) - const rightMotionState = new Ammo.btDefaultMotionState(wallRightTransform) - const rightInfo = new Ammo.btRigidBodyConstructionInfo(0, rightMotionState, wallRightShape, localInertia) - const rightBody = new Ammo.btRigidBody(rightInfo) - rightBody.setFriction(config.friction) - rightBody.setRestitution(config.restitution) - physicsWorld.addRigidBody(rightBody) - tempParts.push(rightBody) - - const wallLeftTransform = new Ammo.btTransform() - wallLeftTransform.setIdentity() - wallLeftTransform.setOrigin(setVector3((size * aspect / 2) + .5, 0, 0)) - const wallLeftShape = new Ammo.btBoxShape(setVector3(1, height, size)) - const leftMotionState = new Ammo.btDefaultMotionState(wallLeftTransform) - const leftInfo = new Ammo.btRigidBodyConstructionInfo(0, leftMotionState, wallLeftShape, localInertia) - const leftBody = new Ammo.btRigidBody(leftInfo) - leftBody.setFriction(config.friction) - leftBody.setRestitution(config.restitution) - physicsWorld.addRigidBody(leftBody) - tempParts.push(leftBody) - - if(boxParts.length){ - removeBoxFromWorld() - } - boxParts = [...tempParts] -} + const tempParts = []; + // ground + const localInertia = setVector3(0, 0, 0); + const groundTransform = new Ammo.btTransform(); + groundTransform.setIdentity(); + groundTransform.setOrigin(setVector3(0, -0.5, 0)); + const groundShape = new Ammo.btBoxShape(setVector3(size * aspect, 1, size)); + const groundMotionState = new Ammo.btDefaultMotionState(groundTransform); + const groundInfo = new Ammo.btRigidBodyConstructionInfo( + 0, + groundMotionState, + groundShape, + localInertia + ); + const groundBody = new Ammo.btRigidBody(groundInfo); + groundBody.setFriction(config.friction); + groundBody.setRestitution(config.restitution); + physicsWorld.addRigidBody(groundBody); + tempParts.push(groundBody); + + const wallTopTransform = new Ammo.btTransform(); + wallTopTransform.setIdentity(); + wallTopTransform.setOrigin(setVector3(0, 0, size / -2 - 0.5)); + const wallTopShape = new Ammo.btBoxShape( + setVector3(size * aspect, height, 1) + ); + const topMotionState = new Ammo.btDefaultMotionState(wallTopTransform); + const topInfo = new Ammo.btRigidBodyConstructionInfo( + 0, + topMotionState, + wallTopShape, + localInertia + ); + const topBody = new Ammo.btRigidBody(topInfo); + topBody.setFriction(config.friction); + topBody.setRestitution(config.restitution); + physicsWorld.addRigidBody(topBody); + tempParts.push(topBody); + + const wallBottomTransform = new Ammo.btTransform(); + wallBottomTransform.setIdentity(); + wallBottomTransform.setOrigin(setVector3(0, 0, size / 2 + 0.5)); + const wallBottomShape = new Ammo.btBoxShape( + setVector3(size * aspect, height, 1) + ); + const bottomMotionState = new Ammo.btDefaultMotionState(wallBottomTransform); + const bottomInfo = new Ammo.btRigidBodyConstructionInfo( + 0, + bottomMotionState, + wallBottomShape, + localInertia + ); + const bottomBody = new Ammo.btRigidBody(bottomInfo); + bottomBody.setFriction(config.friction); + bottomBody.setRestitution(config.restitution); + physicsWorld.addRigidBody(bottomBody); + tempParts.push(bottomBody); + + const wallRightTransform = new Ammo.btTransform(); + wallRightTransform.setIdentity(); + wallRightTransform.setOrigin(setVector3((size * aspect) / -2 - 0.5, 0, 0)); + const wallRightShape = new Ammo.btBoxShape(setVector3(1, height, size)); + const rightMotionState = new Ammo.btDefaultMotionState(wallRightTransform); + const rightInfo = new Ammo.btRigidBodyConstructionInfo( + 0, + rightMotionState, + wallRightShape, + localInertia + ); + const rightBody = new Ammo.btRigidBody(rightInfo); + rightBody.setFriction(config.friction); + rightBody.setRestitution(config.restitution); + physicsWorld.addRigidBody(rightBody); + tempParts.push(rightBody); + + const wallLeftTransform = new Ammo.btTransform(); + wallLeftTransform.setIdentity(); + wallLeftTransform.setOrigin(setVector3((size * aspect) / 2 + 0.5, 0, 0)); + const wallLeftShape = new Ammo.btBoxShape(setVector3(1, height, size)); + const leftMotionState = new Ammo.btDefaultMotionState(wallLeftTransform); + const leftInfo = new Ammo.btRigidBodyConstructionInfo( + 0, + leftMotionState, + wallLeftShape, + localInertia + ); + const leftBody = new Ammo.btRigidBody(leftInfo); + leftBody.setFriction(config.friction); + leftBody.setRestitution(config.restitution); + physicsWorld.addRigidBody(leftBody); + tempParts.push(leftBody); + + if (boxParts.length) { + removeBoxFromWorld(); + } + boxParts = [...tempParts]; +}; const removeBoxFromWorld = () => { - boxParts.forEach(part => physicsWorld.removeRigidBody(part)) -} + boxParts.forEach((part) => physicsWorld.removeRigidBody(part)); +}; const addDie = (sides, id) => { - let cType = `d${sides}_collider` - const mass = colliders[cType].physicsMass * config.mass * config.scale // feature? mass should go up with scale, but it throws off the throwForce and spinForce scaling - // clone the collider - const newDie = createRigidBody(colliders[cType].convexHull, { - mass, - scaling: colliders[cType].scaling, - pos: config.startPosition, - // quat: colliders[cType].rotationQuaternion, - }) - newDie.id = id - newDie.timeout = config.settleTimeout - newDie.mass = mass - physicsWorld.addRigidBody(newDie) - bodies.push(newDie) - // console.log(`added collider for `, type) - rollDie(newDie) -} + let cType = `d${sides}_collider`; + const mass = colliders[cType].physicsMass * config.mass * config.scale; // feature? mass should go up with scale, but it throws off the throwForce and spinForce scaling + // clone the collider + const newDie = createRigidBody(colliders[cType].convexHull, { + mass, + scaling: colliders[cType].scaling, + pos: config.startPosition, + // quat: colliders[cType].rotationQuaternion, + }); + newDie.id = id; + newDie.timeout = config.settleTimeout; + newDie.mass = mass; + physicsWorld.addRigidBody(newDie); + bodies.push(newDie); + // console.log(`added collider for `, type) + rollDie(newDie); +}; const rollDie = (die) => { - die.setLinearVelocity(setVector3( - lerp(-config.startPosition[0] * .5, -config.startPosition[0] * config.throwForce, Math.random()), - // lerp(-config.startPosition[1] * .5, -config.startPosition[1] * config.throwForce, Math.random()), - lerp(-config.startPosition[1], -config.startPosition[1] * 2, Math.random()), - lerp(-config.startPosition[2] * .5, -config.startPosition[2] * config.throwForce, Math.random()), - )) - - const force = new Ammo.btVector3( - lerp(-config.spinForce, config.spinForce, Math.random()), - lerp(-config.spinForce, config.spinForce, Math.random()), - lerp(-config.spinForce, config.spinForce, Math.random()) - ) - - // attempting to create an envelope for the force influence based on scale and mass - // linear scale was no good - this creates a nice power curve - const scale = Math.abs(config.scale - 1) + config.scale * config.scale * (die.mass/config.mass) * .75 - - // console.log('scale', scale) - - die.applyImpulse(force, setVector3(scale, scale, scale)) - -} + die.setLinearVelocity( + setVector3( + lerp( + -config.startPosition[0] * 0.5, + -config.startPosition[0] * config.throwForce, + Math.random() + ), + // lerp(-config.startPosition[1] * .5, -config.startPosition[1] * config.throwForce, Math.random()), + lerp( + -config.startPosition[1], + -config.startPosition[1] * 2, + Math.random() + ), + lerp( + -config.startPosition[2] * 0.5, + -config.startPosition[2] * config.throwForce, + Math.random() + ) + ) + ); + + const force = new Ammo.btVector3( + lerp(-config.spinForce, config.spinForce, Math.random()), + lerp(-config.spinForce, config.spinForce, Math.random()), + lerp(-config.spinForce, config.spinForce, Math.random()) + ); + + // attempting to create an envelope for the force influence based on scale and mass + // linear scale was no good - this creates a nice power curve + const scale = + Math.abs(config.scale - 1) + + config.scale * config.scale * (die.mass / config.mass) * 0.75; + + // console.log('scale', scale) + + die.applyImpulse(force, setVector3(scale, scale, scale)); +}; const removeDie = (id) => { - sleepingBodies = sleepingBodies.filter((die) => { - let match = die.id === id - if(match){ - // remove the mesh from the scene - physicsWorld.removeRigidBody(die) - } - return !match - }) - - // step the animation forward - // requestAnimationFrame(loop) -} + sleepingBodies = sleepingBodies.filter((die) => { + let match = die.id === id; + if (match) { + // remove the mesh from the scene + physicsWorld.removeRigidBody(die); + } + return !match; + }); + + // step the animation forward + // requestAnimationFrame(loop) +}; const clearDice = () => { - if(diceBufferView.byteLength){ - diceBufferView.fill(0) - } - stopLoop = true - // clear all bodies - bodies.forEach(body => physicsWorld.removeRigidBody(body)) - sleepingBodies.forEach(body => physicsWorld.removeRigidBody(body)) - // clear cache arrays - bodies = [] - sleepingBodies = [] -} - + if (diceBufferView.byteLength) { + diceBufferView.fill(0); + } + stopLoop = true; + // clear all bodies + bodies.forEach((body) => physicsWorld.removeRigidBody(body)); + sleepingBodies.forEach((body) => physicsWorld.removeRigidBody(body)); + // clear cache arrays + bodies = []; + sleepingBodies = []; +}; const setupPhysicsWorld = () => { - const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration() - const broadphase = new Ammo.btDbvtBroadphase() - const solver = new Ammo.btSequentialImpulseConstraintSolver() - const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration) - const World = new Ammo.btDiscreteDynamicsWorld( - dispatcher, - broadphase, - solver, - collisionConfiguration - ) - World.setGravity(setVector3(0, -9.81 * config.gravity, 0)) - - return World -} + const collisionConfiguration = new Ammo.btDefaultCollisionConfiguration(); + const broadphase = new Ammo.btDbvtBroadphase(); + const solver = new Ammo.btSequentialImpulseConstraintSolver(); + const dispatcher = new Ammo.btCollisionDispatcher(collisionConfiguration); + const World = new Ammo.btDiscreteDynamicsWorld( + dispatcher, + broadphase, + solver, + collisionConfiguration + ); + World.setGravity(setVector3(0, -9.81 * config.gravity, 0)); + + return World; +}; const update = (delta) => { - // step world - const deltaTime = delta / 1000 - - // console.time("stepSimulation") - physicsWorld.stepSimulation(deltaTime, 2, 1 / 90) // higher number = slow motion - // console.timeEnd("stepSimulation") - - diceBufferView[0] = bodies.length - - // looping backwards since bodies are removed as they are put to sleep - for (let i = bodies.length - 1; i >= 0; i--) { - const rb = bodies[i] - const speed = rb.getLinearVelocity().length() - const tilt = rb.getAngularVelocity().length() - - if(speed < .01 && tilt < .01 || rb.timeout < 0) { - // flag the second param for this body so it can be processed in World, first param will be the roll.id - diceBufferView[(i*8) + 1] = rb.id - diceBufferView[(i*8) + 2] = -1 - rb.asleep = true - rb.setMassProps(0) - rb.forceActivationState(3) - // zero out anything left - rb.setLinearVelocity(emptyVector) - rb.setAngularVelocity(emptyVector) - sleepingBodies.push(bodies.splice(i,1)[0]) - continue - } - // tick down the movement timeout on this die - rb.timeout -= delta - const ms = rb.getMotionState() - if (ms) { - ms.getWorldTransform(tmpBtTrans) - let p = tmpBtTrans.getOrigin() - let q = tmpBtTrans.getRotation() - let j = i*8 + 1 - - diceBufferView[j] = rb.id - diceBufferView[j+1] = p.x() - diceBufferView[j+2] = p.y() - diceBufferView[j+3] = p.z() - diceBufferView[j+4] = q.x() - diceBufferView[j+5] = q.y() - diceBufferView[j+6] = q.z() - diceBufferView[j+7] = q.w() - } - } -} - -let last = new Date().getTime() + // step world + const deltaTime = delta / 1000; + + // console.time("stepSimulation") + physicsWorld.stepSimulation(deltaTime, 2, 1 / 90); // higher number = slow motion + // console.timeEnd("stepSimulation") + + diceBufferView[0] = bodies.length; + + // looping backwards since bodies are removed as they are put to sleep + for (let i = bodies.length - 1; i >= 0; i--) { + const rb = bodies[i]; + const speed = rb.getLinearVelocity().length(); + const tilt = rb.getAngularVelocity().length(); + + if ((speed < 0.01 && tilt < 0.01) || rb.timeout < 0) { + // flag the second param for this body so it can be processed in World, first param will be the roll.id + diceBufferView[i * 8 + 1] = rb.id; + diceBufferView[i * 8 + 2] = -1; + rb.asleep = true; + rb.setMassProps(0); + rb.forceActivationState(3); + // zero out anything left + rb.setLinearVelocity(emptyVector); + rb.setAngularVelocity(emptyVector); + sleepingBodies.push(bodies.splice(i, 1)[0]); + continue; + } + // tick down the movement timeout on this die + rb.timeout -= delta; + const ms = rb.getMotionState(); + if (ms) { + ms.getWorldTransform(tmpBtTrans); + let p = tmpBtTrans.getOrigin(); + let q = tmpBtTrans.getRotation(); + let j = i * 8 + 1; + + diceBufferView[j] = rb.id; + diceBufferView[j + 1] = p.x(); + diceBufferView[j + 2] = p.y(); + diceBufferView[j + 3] = p.z(); + diceBufferView[j + 4] = q.x(); + diceBufferView[j + 5] = q.y(); + diceBufferView[j + 6] = q.z(); + diceBufferView[j + 7] = q.w(); + } + } +}; + +let last = new Date().getTime(); const loop = () => { - let now = new Date().getTime() - const delta = now - last - last = now - - if(!stopLoop && diceBufferView.byteLength) { - // console.time("physics") - update(delta) - // console.timeEnd("physics") - worldWorkerPort.postMessage({ - action: 'updates', - diceBuffer: diceBufferView.buffer - }, [diceBufferView.buffer]) - } -} + let now = new Date().getTime(); + const delta = now - last; + last = now; + + if (!stopLoop && diceBufferView.byteLength) { + // console.time("physics") + update(delta); + // console.timeEnd("physics") + worldWorkerPort.postMessage( + { + action: "updates", + diceBuffer: diceBufferView.buffer, + }, + [diceBufferView.buffer] + ); + } +}; diff --git a/src/components/pointLights.js b/src/components/pointLights.js index f9817b7..b2b071c 100644 --- a/src/components/pointLights.js +++ b/src/components/pointLights.js @@ -1,40 +1,40 @@ -import { PointLight } from '@babylonjs/core/Lights/pointLight' -import { ShadowGenerator } from '@babylonjs/core/Lights/Shadows/shadowGenerator' -import { Vector3 } from '@babylonjs/core/Maths/math.vector' -import '@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent' -import '@babylonjs/core/Engines/Extensions/engine.cubeTexture' +import { PointLight } from "@babylonjs/core/Lights/pointLight"; +import { ShadowGenerator } from "@babylonjs/core/Lights/Shadows/shadowGenerator"; +import { Vector3 } from "@babylonjs/core/Maths/math.vector"; +import "@babylonjs/core/Lights/Shadows/shadowGeneratorSceneComponent"; +import "@babylonjs/core/Engines/Extensions/engine.cubeTexture"; const defaultOptions = { - enableShadows: true -} + enableShadows: true, +}; function createPointLights(options = defaultOptions) { - const { enableShadows } = options + const { enableShadows } = options; + + const p_light1 = new PointLight("PointLight1", new Vector3(-10, 35, 10)); + p_light1.intensity = 0.6; + + const p_light2 = new PointLight("PointLight2", new Vector3(11, 35, 0)); + p_light2.intensity = 0.7; - const p_light1 = new PointLight("PointLight1", new Vector3(-10,35,10)) - p_light1.intensity = .6 - - const p_light2 = new PointLight("PointLight2", new Vector3(11,35,0)) - p_light2.intensity = .7 - - if(enableShadows){ - p_light1.shadowMinZ = 1 - p_light1.shadowMaxZ = 50 + if (enableShadows) { + p_light1.shadowMinZ = 1; + p_light1.shadowMaxZ = 50; p_light1.shadowGenerator = new ShadowGenerator(1024, p_light1); p_light1.shadowGenerator.useCloseExponentialShadowMap = true; - p_light1.shadowGenerator.darkness = .6; + p_light1.shadowGenerator.darkness = 0.6; // p_light1.shadowGenerator.usePoissonSampling = true; // p_light1.shadowGenerator.bias = 0 - p_light2.shadowMinZ = 1 - p_light2.shadowMaxZ = 50 + p_light2.shadowMinZ = 1; + p_light2.shadowMaxZ = 50; p_light2.shadowGenerator = new ShadowGenerator(1024, p_light2); p_light2.shadowGenerator.useCloseExponentialShadowMap = true; - p_light2.shadowGenerator.darkness = .6; + p_light2.shadowGenerator.darkness = 0.6; // p_light2.shadowGenerator.usePoissonSampling = true; // p_light2.shadowGenerator.bias = 0 } - return {point1:p_light1, point2: p_light2} + return { point1: p_light1, point2: p_light2 }; } -export { createPointLights } \ No newline at end of file +export { createPointLights }; diff --git a/src/components/scene.js b/src/components/scene.js index a6bd8d7..5872acc 100644 --- a/src/components/scene.js +++ b/src/components/scene.js @@ -1,26 +1,30 @@ -import { Color4 } from '@babylonjs/core/Maths/math.color' -import { Scene } from '@babylonjs/core/scene' -import { SceneOptimizer, SceneOptimizerOptions } from '@babylonjs/core/Misc/sceneOptimizer' +import { Color4 } from "@babylonjs/core/Maths/math.color"; +import { Scene } from "@babylonjs/core/scene"; +import { + SceneOptimizer, + SceneOptimizerOptions, +} from "@babylonjs/core/Misc/sceneOptimizer"; function createScene(options) { - const { engine } = options - const scene = new Scene(engine) + const { engine } = options; + const scene = new Scene(engine); // scene.useRightHandedSystem = true - scene.clearColor = new Color4(0,0,0,0); + scene.clearColor = new Color4(0, 0, 0, 0); scene.pointerMovePredicate = () => false; scene.pointerDownPredicate = () => false; scene.pointerUpPredicate = () => false; scene.clearCachedVertexData(); - const optimizationSettings = SceneOptimizerOptions.LowDegradationAllowed() - optimizationSettings.optimizations = optimizationSettings.optimizations.splice(1) - optimizationSettings.targetFrameRate = 60 + const optimizationSettings = SceneOptimizerOptions.LowDegradationAllowed(); + optimizationSettings.optimizations = + optimizationSettings.optimizations.splice(1); + optimizationSettings.targetFrameRate = 60; - SceneOptimizer.OptimizeAsync(scene,optimizationSettings) + SceneOptimizer.OptimizeAsync(scene, optimizationSettings); - return scene + return scene; } -export { createScene } \ No newline at end of file +export { createScene }; diff --git a/src/components/world.offscreen.js b/src/components/world.offscreen.js index 8c823ed..ce8281a 100644 --- a/src/components/world.offscreen.js +++ b/src/components/world.offscreen.js @@ -1,109 +1,114 @@ -import worldWorker from './offscreenCanvas.worker?worker&inline' // using vits.js worker import - this will be compiled away -import { createUUID } from '../helpers' +import worldWorker from "./offscreenCanvas.worker?worker&inline"; // using vits.js worker import - this will be compiled away +import { createUUID } from "../helpers"; class WorldOffScreen { - initialized = false - offscreenWorkerInit = false - themeLoadedInit = false - pendingThemePromises = [] - #offscreenCanvas - #OffscreenWorker - onInitComplete = () => {} // init callback - onRollResult = () => {} // individual die callback - onRollComplete = () => {} // roll group callback - - constructor(options){ - - // transfer control offscreen - this.#offscreenCanvas = options.canvas.transferControlToOffscreen() - - // initialize 3D World in which BabylonJS runs - this.#OffscreenWorker = new worldWorker() - // need to initialize the web worker and get confirmation that initialization is complete before other scripts can run - // set a property on the worker to a promise that is resolve when the proper message is returned from the worker - this.#OffscreenWorker.init = new Promise((resolve, reject) => { - this.offscreenWorkerInit = resolve - }) - - this.initialized = this.#initScene(options) - } - - // initialize the babylon scene - async #initScene(config) { - // initalize the offscreen worker - this.#OffscreenWorker.postMessage({ - action: "init", - canvas: this.#offscreenCanvas, - width: config.canvas.clientWidth, - height: config.canvas.clientHeight, - options: config.options, - }, [this.#offscreenCanvas]) - - // handle messages from offscreen BabylonJS World - this.#OffscreenWorker.onmessage = (e) => { - switch( e.data.action ) { - case "init-complete": - this.offscreenWorkerInit() //fulfill promise so other things can run - break; - case "theme-loaded": - this.pendingThemePromises[e.data.id]() - break; - case 'roll-result': - this.onRollResult(e.data.die) - break; - case 'roll-complete': - this.onRollComplete() - break; - case 'die-removed': - this.onDieRemoved(e.data.rollId) - break; - } - } - await Promise.all([this.#OffscreenWorker.init]) - - this.onInitComplete(true) - - return true - } - - connect(port){ - // Setup the connection: Port 1 is for this.#OffscreenWorker - this.#OffscreenWorker.postMessage({ - action : "connect", - port - },[ port ]) - } - - updateConfig(options){ - this.#OffscreenWorker.postMessage({action: "updateConfig", options}); - } - - resize(options){ - this.#OffscreenWorker.postMessage({action: "resize", options}); - } - - async loadTheme(theme) { - const id = createUUID() - - return new Promise((resolve, reject) => { - this.#OffscreenWorker.postMessage({action: "loadTheme", id, theme}) - // this.themeLoadedInit = resolve - this.pendingThemePromises[id] = resolve - }) - } - - clear(){ - this.#OffscreenWorker.postMessage({action: "clearDice"}) - } - - add(options){ - this.#OffscreenWorker.postMessage({action: "addDie", options}) - } - - remove(options){ - // remove the die from the render - this.#OffscreenWorker.postMessage({action: "removeDie", options}) - } + initialized = false; + offscreenWorkerInit = false; + themeLoadedInit = false; + pendingThemePromises = []; + #offscreenCanvas; + #OffscreenWorker; + onInitComplete = () => {}; // init callback + onRollResult = () => {}; // individual die callback + onRollComplete = () => {}; // roll group callback + + constructor(options) { + // transfer control offscreen + this.#offscreenCanvas = options.canvas.transferControlToOffscreen(); + + // initialize 3D World in which BabylonJS runs + this.#OffscreenWorker = new worldWorker(); + // need to initialize the web worker and get confirmation that initialization is complete before other scripts can run + // set a property on the worker to a promise that is resolve when the proper message is returned from the worker + this.#OffscreenWorker.init = new Promise((resolve, reject) => { + this.offscreenWorkerInit = resolve; + }); + + this.initialized = this.#initScene(options); + } + + // initialize the babylon scene + async #initScene(config) { + // initalize the offscreen worker + this.#OffscreenWorker.postMessage( + { + action: "init", + canvas: this.#offscreenCanvas, + width: config.canvas.clientWidth, + height: config.canvas.clientHeight, + options: config.options, + }, + [this.#offscreenCanvas] + ); + + // handle messages from offscreen BabylonJS World + this.#OffscreenWorker.onmessage = (e) => { + switch (e.data.action) { + case "init-complete": + this.offscreenWorkerInit(); //fulfill promise so other things can run + break; + case "theme-loaded": + this.pendingThemePromises[e.data.id](); + break; + case "roll-result": + this.onRollResult(e.data.die); + break; + case "roll-complete": + this.onRollComplete(); + break; + case "die-removed": + this.onDieRemoved(e.data.rollId); + break; + } + }; + await Promise.all([this.#OffscreenWorker.init]); + + this.onInitComplete(true); + + return true; + } + + connect(port) { + // Setup the connection: Port 1 is for this.#OffscreenWorker + this.#OffscreenWorker.postMessage( + { + action: "connect", + port, + }, + [port] + ); + } + + updateConfig(options) { + this.#OffscreenWorker.postMessage({ action: "updateConfig", options }); + } + + resize(options) { + this.#OffscreenWorker.postMessage({ action: "resize", options }); + } + + async loadTheme(theme) { + const id = createUUID(); + + return new Promise((resolve, reject) => { + this.#OffscreenWorker.postMessage({ action: "loadTheme", id, theme }); + // this.themeLoadedInit = resolve + this.pendingThemePromises[id] = resolve; + }); + } + + clear() { + this.#OffscreenWorker.postMessage({ action: "clearDice" }); + } + + add(options) { + this.#OffscreenWorker.postMessage({ action: "addDie", options }); + } + + remove(options) { + // remove the die from the render + this.#OffscreenWorker.postMessage({ action: "removeDie", options }); + } } -export default WorldOffScreen \ No newline at end of file +export default WorldOffScreen; diff --git a/src/components/world.onscreen.js b/src/components/world.onscreen.js index 0882e89..536f672 100644 --- a/src/components/world.onscreen.js +++ b/src/components/world.onscreen.js @@ -1,348 +1,375 @@ -import { Vector3 } from '@babylonjs/core/Maths/math' -import { createEngine } from './engine' -import { createScene } from './scene' -import { createCamera } from './camera' -import { createLights } from './lights' -import DiceBox from './DiceBox' -import Dice from './Dice' -import { loadTheme } from './Dice/themes' +import { Vector3 } from "@babylonjs/core/Maths/math"; +import { createEngine } from "./engine"; +import { createScene } from "./scene"; +import { createCamera } from "./camera"; +import { createLights } from "./lights"; +import DiceBox from "./DiceBox"; +import Dice from "./Dice"; +import { loadTheme } from "./Dice/themes"; class WorldOnscreen { - config - initialized = false - #dieCache = {} - #count = 0 - #sleeperCount = 0 - #dieRollTimer = [] - #canvas - #engine - #scene - #camera - #lights - #diceBox - #physicsWorkerPort - onInitComplete = () => {} - onRollResult = () => {} - onRollComplete = () => {} - diceBufferView = new Float32Array(8000) - - constructor(options){ - this.initialized = this.initScene(options) - } - - // initialize the babylon scene - async initScene(config) { - this.#canvas = config.canvas - - // set the config from World - this.config = config.options - - // setup babylonJS scene - this.#engine = createEngine(this.#canvas ) - this.#scene = createScene({engine:this.#engine }) - this.#camera = createCamera({engine:this.#engine, scene: this.#scene}) - this.#lights = createLights({enableShadows: this.config.enableShadows, scene: this.#scene}) - - // create the box that provides surfaces for shadows to render on - this.#diceBox = new DiceBox({ - enableShadows: this.config.enableShadows, - aspect: this.#canvas.width / this.#canvas.height, - lights: this.#lights, - scene: this.#scene - }) - - // loading all our dice models - // we use to load these models individually as needed, but it's faster to load them all at once and prevents animation jank when rolling - await Dice.loadModels({ - assetPath: this.config.origin + this.config.assetPath, - scene: this.#scene, - scale: this.config.scale - }) - - this.#physicsWorkerPort.postMessage({ - action: "initBuffer", - diceBuffer: this.diceBufferView.buffer - }, [this.diceBufferView.buffer]) - - // init complete - let the world know - this.onInitComplete(true) - - // is this needed? - // return true - } - - connect(port){ - this.#physicsWorkerPort = port - this.#physicsWorkerPort.onmessage = (e) => { - switch (e.data.action) { - case "updates": // dice status/position updates from physics worker - this.updatesFromPhysics(e.data.diceBuffer) - break; - - default: - console.error("action from physicsWorker not found in offscreen worker") - break; + config; + initialized = false; + #dieCache = {}; + #count = 0; + #sleeperCount = 0; + #dieRollTimer = []; + #canvas; + #engine; + #scene; + #camera; + #lights; + #diceBox; + #physicsWorkerPort; + onInitComplete = () => {}; + onRollResult = () => {}; + onRollComplete = () => {}; + diceBufferView = new Float32Array(8000); + + constructor(options) { + this.initialized = this.initScene(options); + } + + // initialize the babylon scene + async initScene(config) { + this.#canvas = config.canvas; + + // set the config from World + this.config = config.options; + + // setup babylonJS scene + this.#engine = createEngine(this.#canvas); + this.#scene = createScene({ engine: this.#engine }); + this.#camera = createCamera({ engine: this.#engine, scene: this.#scene }); + this.#lights = createLights({ + enableShadows: this.config.enableShadows, + scene: this.#scene, + }); + + // create the box that provides surfaces for shadows to render on + this.#diceBox = new DiceBox({ + enableShadows: this.config.enableShadows, + aspect: this.#canvas.width / this.#canvas.height, + lights: this.#lights, + scene: this.#scene, + }); + + // loading all our dice models + // we use to load these models individually as needed, but it's faster to load them all at once and prevents animation jank when rolling + await Dice.loadModels({ + assetPath: this.config.origin + this.config.assetPath, + scene: this.#scene, + scale: this.config.scale, + }); + + this.#physicsWorkerPort.postMessage( + { + action: "initBuffer", + diceBuffer: this.diceBufferView.buffer, + }, + [this.diceBufferView.buffer] + ); + + // init complete - let the world know + this.onInitComplete(true); + + // is this needed? + // return true + } + + connect(port) { + this.#physicsWorkerPort = port; + this.#physicsWorkerPort.onmessage = (e) => { + switch (e.data.action) { + case "updates": // dice status/position updates from physics worker + this.updatesFromPhysics(e.data.diceBuffer); + break; + + default: + console.error( + "action from physicsWorker not found in offscreen worker" + ); + break; + } + }; + } + + updateConfig(options) { + const prevConfig = this.config; + this.config = options; + // check if shadows setting has changed + if (prevConfig.enableShadows !== this.config.enableShadows) { + // regenerate the lights + Object.values(this.#lights).forEach((light) => light.dispose()); + this.#lights = createLights({ enableShadows: this.config.enableShadows }); + } + if (prevConfig.scale !== this.config.scale) { + Object.values(this.#dieCache).forEach(({ mesh }) => { + mesh.scaling = new Vector3( + this.config.scale, + this.config.scale, + this.config.scale + ); + }); + } + } + + // all this does is start the render engine. + render(anustart) { + // document.body.addEventListener('click',()=>engine.stopRenderLoop()) + this.#engine.runRenderLoop(this.renderLoop.bind(this)); + this.#physicsWorkerPort.postMessage({ + action: "resumeSimulation", + anustart, + }); + } + + renderLoop() { + // if no dice are awake then stop the render loop and save some CPU power + if ( + this.#sleeperCount && + this.#sleeperCount === Object.keys(this.#dieCache).length + ) { + // console.log(`no dice moving`) + this.#engine.stopRenderLoop(); + + // stop the physics engine + this.#physicsWorkerPort.postMessage({ + action: "stopSimulation", + }); + + // trigger callback that roll is complete + this.onRollComplete(); + } + // otherwise keep on rendering + else { + this.#scene.render(); // not the same as this.render() + } + } + + async loadTheme(theme) { + await loadTheme( + theme, + this.config.origin + this.config.assetPath, + this.#scene + ); + } + + clear() { + if (!Object.keys(this.#dieCache).length && !this.#sleeperCount) { + return; + } + if (this.diceBufferView.byteLength) { + this.diceBufferView.fill(0); + } + this.#dieRollTimer.forEach((timer) => clearTimeout(timer)); + // stop anything that's currently rendering + this.#engine.stopRenderLoop(); + // remove all dice + Object.values(this.#dieCache).forEach((die) => die.mesh.dispose()); + + // reset storage + this.#dieCache = {}; + this.#count = 0; + this.#sleeperCount = 0; + + // step the animation forward + this.#scene.render(); + } + + add(options) { + // loadDie allows you to specify sides(dieType) and theme and returns the options you passed in + Dice.loadDie({ + ...options, + scene: this.#scene, + }).then((resp) => { + // space out adding the dice so they don't lump together too much + this.#dieRollTimer.push( + setTimeout(() => { + this.#add(resp); + }, this.#count++ * this.config.delay) + ); + }); + } + + // add a die to the scene + async #add(options) { + if (this.#engine.activeRenderLoops.length === 0) { + this.render(options.anustart); + } + const diceOptions = { + ...options, + assetPath: this.config.assetPath, + enableShadows: this.config.enableShadows, + scale: this.config.scale, + lights: this.#lights, + }; + + const newDie = new Dice(diceOptions); + + // save the die just created to the cache + this.#dieCache[newDie.id] = newDie; + + // tell the physics engine to roll this die type - which is a low poly collider + this.#physicsWorkerPort.postMessage({ + action: "addDie", + sides: options.sides, + scale: this.config.scale, + id: newDie.id, + }); + + // for d100's we need to add an additional d10 and pair it up with the d100 just created + if (options.sides === 100) { + // assign the new die to a property on the d100 - spread the options in order to pass a matching theme + newDie.d10Instance = await Dice.loadDie({ + ...diceOptions, + sides: 10, + id: newDie.id + 10000, + }).then((response) => { + const d10Instance = new Dice(response); + // identify the parent of this d10 so we can calculate the roll result later + d10Instance.dieParent = newDie; + return d10Instance; + }); + // add the d10 to the cache and ask the physics worker for a collider + this.#dieCache[`${newDie.d10Instance.id}`] = newDie.d10Instance; + this.#physicsWorkerPort.postMessage({ + action: "addDie", + sides: 10, + scale: this.config.scale, + id: newDie.d10Instance.id, + }); + } + + // return the die instance + return newDie; + } + + remove(data) { + // TODO: test this with exploding dice + const dieData = this.#dieCache[data.id]; + + // check if this is d100 and remove associated d10 first + if (dieData.hasOwnProperty("d10Instance")) { + // remove die + this.#dieCache[dieData.d10Instance.id].mesh.dispose(); + // delete entry + delete this.#dieCache[dieData.d10Instance.id]; + // remove physics body + this.#physicsWorkerPort.postMessage({ + action: "removeDie", + id: dieData.d10Instance.id, + }); + // decrement count + this.#sleeperCount--; + } + + // remove die + this.#dieCache[data.id].mesh.dispose(); + // delete entry + delete this.#dieCache[data.id]; + // decrement count + this.#sleeperCount--; + + // step the animation forward + this.#scene.render(); + + this.onDieRemoved(data.rollId); + } + + updatesFromPhysics(buffer) { + this.diceBufferView = new Float32Array(buffer); + let bufferIndex = 1; + + // loop will be based on diceBufferView[0] value which is the bodies length in physics.worker + for (let i = 0, len = this.diceBufferView[0]; i < len; i++) { + if (!Object.keys(this.#dieCache).length) { + continue; + } + const die = this.#dieCache[`${this.diceBufferView[bufferIndex]}`]; + if (!die) { + console.log("Error: die not available in scene to animate"); + break; + } + // if the first position index is -1 then this die has been flagged as asleep + if (this.diceBufferView[bufferIndex + 1] === -1) { + this.handleAsleep(die); + } else { + const px = this.diceBufferView[bufferIndex + 1]; + const py = this.diceBufferView[bufferIndex + 2]; + const pz = this.diceBufferView[bufferIndex + 3]; + const qx = this.diceBufferView[bufferIndex + 4]; + const qy = this.diceBufferView[bufferIndex + 5]; + const qz = this.diceBufferView[bufferIndex + 6]; + const qw = this.diceBufferView[bufferIndex + 7]; + + die.mesh.position.set(px, py, pz); + die.mesh.rotationQuaternion.set(qx, qy, qz, qw); + } + + bufferIndex = bufferIndex + 8; + } + + // transfer the buffer back to physics worker + requestAnimationFrame(() => { + this.#physicsWorkerPort.postMessage( + { + action: "stepSimulation", + diceBuffer: this.diceBufferView.buffer, + }, + [this.diceBufferView.buffer] + ); + }); + } + + // handle the position updates from the physics worker. It's a simple flat array of numbers for quick and easy transfer + async handleAsleep(die) { + // mark this die as asleep + die.asleep = true; + + // get the roll result for this die + let result = await Dice.getRollResult(die); + // TODO: catch error if no result is found + if (result === undefined) { + console.log("No result. This die needs a reroll."); + } + + if (die.d10Instance || die.dieParent) { + // if one of the pair is asleep and the other isn't then it falls through without getting the roll result + // otherwise both dice in the d100 are asleep and ready to calc their roll result + if (die?.d10Instance?.asleep || die?.dieParent?.asleep) { + const d100 = die.config.sides === 100 ? die : die.dieParent; + const d10 = die.config.sides === 10 ? die : die.d10Instance; + if (d10.value === 0 && d100.value === 0) { + d100.value = 100; // 00 + 0 is 100 on a d100 + } else { + d100.value = d100.value + d10.value; } + + this.onRollResult({ + rollId: d100.config.rollId, + value: d100.value, + }); } - } - - updateConfig(options){ - const prevConfig = this.config - this.config = options - // check if shadows setting has changed - if(prevConfig.enableShadows !== this.config.enableShadows) { - // regenerate the lights - Object.values(this.#lights ).forEach(light => light.dispose()) - this.#lights = createLights({enableShadows: this.config.enableShadows}) - } - if(prevConfig.scale !== this.config.scale) { - Object.values(this.#dieCache).forEach(({mesh}) => { - mesh.scaling = new Vector3(this.config.scale,this.config.scale,this.config.scale) - }) - } - } - - // all this does is start the render engine. - render(anustart) { - // document.body.addEventListener('click',()=>engine.stopRenderLoop()) - this.#engine.runRenderLoop(this.renderLoop.bind(this)) - this.#physicsWorkerPort.postMessage({ - action: "resumeSimulation", - anustart - }) - } - - renderLoop() { - // if no dice are awake then stop the render loop and save some CPU power - if(this.#sleeperCount && this.#sleeperCount === Object.keys(this.#dieCache).length) { - // console.log(`no dice moving`) - this.#engine.stopRenderLoop() - - // stop the physics engine - this.#physicsWorkerPort.postMessage({ - action: "stopSimulation", - }) - - // trigger callback that roll is complete - this.onRollComplete() - } - // otherwise keep on rendering - else { - this.#scene.render() // not the same as this.render() - } - } - - async loadTheme(theme) { - await loadTheme(theme, this.config.origin + this.config.assetPath, this.#scene) - } - - clear() { - if(!Object.keys(this.#dieCache).length && !this.#sleeperCount) { - return - } - if(this.diceBufferView.byteLength){ - this.diceBufferView.fill(0) - } - this.#dieRollTimer.forEach(timer=>clearTimeout(timer)) - // stop anything that's currently rendering - this.#engine.stopRenderLoop() - // remove all dice - Object.values(this.#dieCache).forEach(die => die.mesh.dispose()) - - // reset storage - this.#dieCache = {} - this.#count = 0 - this.#sleeperCount = 0 - - // step the animation forward - this.#scene.render() - } - - add(options) { - // loadDie allows you to specify sides(dieType) and theme and returns the options you passed in - Dice.loadDie({ - ...options, - scene: this.#scene - }).then(resp => { - // space out adding the dice so they don't lump together too much - this.#dieRollTimer.push(setTimeout(() => { - this.#add(resp) - }, this.#count++ * this.config.delay)) - }) - } - - // add a die to the scene - async #add(options) { - if(this.#engine.activeRenderLoops.length === 0) { - this.render(options.anustart) - } - const diceOptions = { - ...options, - assetPath: this.config.assetPath, - enableShadows: this.config.enableShadows, - scale: this.config.scale, - lights: this.#lights, - } - - const newDie = new Dice(diceOptions) - - // save the die just created to the cache - this.#dieCache[newDie.id] = newDie - - // tell the physics engine to roll this die type - which is a low poly collider - this.#physicsWorkerPort.postMessage({ - action: "addDie", - sides: options.sides, - scale: this.config.scale, - id: newDie.id - }) - - // for d100's we need to add an additional d10 and pair it up with the d100 just created - if(options.sides === 100) { - // assign the new die to a property on the d100 - spread the options in order to pass a matching theme - newDie.d10Instance = await Dice.loadDie({...diceOptions, sides: 10, id: newDie.id + 10000}).then( response => { - const d10Instance = new Dice(response) - // identify the parent of this d10 so we can calculate the roll result later - d10Instance.dieParent = newDie - return d10Instance - }) - // add the d10 to the cache and ask the physics worker for a collider - this.#dieCache[`${newDie.d10Instance.id}`] = newDie.d10Instance - this.#physicsWorkerPort.postMessage({ - action: "addDie", - sides: 10, - scale: this.config.scale, - id: newDie.d10Instance.id - }) - } - - // return the die instance - return newDie - - } - - remove(data) { - // TODO: test this with exploding dice - const dieData = this.#dieCache[data.id] - - // check if this is d100 and remove associated d10 first - if(dieData.hasOwnProperty('d10Instance')){ - // remove die - this.#dieCache[dieData.d10Instance.id].mesh.dispose() - // delete entry - delete this.#dieCache[dieData.d10Instance.id] - // remove physics body - this.#physicsWorkerPort.postMessage({ - action: "removeDie", - id: dieData.d10Instance.id - }) - // decrement count - this.#sleeperCount-- - } - - // remove die - this.#dieCache[data.id].mesh.dispose() - // delete entry - delete this.#dieCache[data.id] - // decrement count - this.#sleeperCount-- - - // step the animation forward - this.#scene.render() - - this.onDieRemoved(data.rollId) -} - - updatesFromPhysics(buffer) { - this.diceBufferView = new Float32Array(buffer) - let bufferIndex = 1 - - // loop will be based on diceBufferView[0] value which is the bodies length in physics.worker - for (let i = 0, len = this.diceBufferView[0]; i < len; i++) { - if(!Object.keys(this.#dieCache).length){ - continue - } - const die = this.#dieCache[`${this.diceBufferView[bufferIndex]}`] - if(!die) { - console.log("Error: die not available in scene to animate") - break - } - // if the first position index is -1 then this die has been flagged as asleep - if(this.diceBufferView[bufferIndex + 1] === -1) { - this.handleAsleep(die) - } else { - const px = this.diceBufferView[bufferIndex + 1] - const py = this.diceBufferView[bufferIndex + 2] - const pz = this.diceBufferView[bufferIndex + 3] - const qx = this.diceBufferView[bufferIndex + 4] - const qy = this.diceBufferView[bufferIndex + 5] - const qz = this.diceBufferView[bufferIndex + 6] - const qw = this.diceBufferView[bufferIndex + 7] - - die.mesh.position.set(px, py, pz) - die.mesh.rotationQuaternion.set(qx, qy, qz, qw) - } - - bufferIndex = bufferIndex + 8 - } - - // transfer the buffer back to physics worker - requestAnimationFrame(()=>{ - this.#physicsWorkerPort.postMessage({ - action: "stepSimulation", - diceBuffer: this.diceBufferView.buffer - }, [this.diceBufferView.buffer]) - }) - } - - // handle the position updates from the physics worker. It's a simple flat array of numbers for quick and easy transfer - async handleAsleep(die){ - // mark this die as asleep - die.asleep = true - - // get the roll result for this die - let result = await Dice.getRollResult(die) - // TODO: catch error if no result is found - if(result === undefined) { - console.log("No result. This die needs a reroll.") - } - - if(die.d10Instance || die.dieParent) { - // if one of the pair is asleep and the other isn't then it falls through without getting the roll result - // otherwise both dice in the d100 are asleep and ready to calc their roll result - if(die?.d10Instance?.asleep || die?.dieParent?.asleep) { - const d100 = die.config.sides === 100 ? die : die.dieParent - const d10 = die.config.sides === 10 ? die : die.d10Instance - if (d10.value === 0 && d100.value === 0) { - d100.value = 100; // 00 + 0 is 100 on a d100 - } else { - d100.value = d100.value + d10.value - } - - this.onRollResult({ - rollId: d100.config.rollId, - value : d100.value - }) - } - } else { - // turn 0's on a d10 into a 10 - if(die.config.sides === 10 && die.value === 0) { - die.value = 10 - } - this.onRollResult({ - rollId: die.config.rollId, - value: die.value - }) - } - // add to the sleeper count - this.#sleeperCount++ - } - - resize() { - // redraw the dicebox - this.#diceBox.create({aspect: this.#canvas.width / this.#canvas.height}) - this.#engine.resize() - } + } else { + // turn 0's on a d10 into a 10 + if (die.config.sides === 10 && die.value === 0) { + die.value = 10; + } + this.onRollResult({ + rollId: die.config.rollId, + value: die.value, + }); + } + // add to the sleeper count + this.#sleeperCount++; + } + + resize() { + // redraw the dicebox + this.#diceBox.create({ aspect: this.#canvas.width / this.#canvas.height }); + this.#engine.resize(); + } } -export default WorldOnscreen \ No newline at end of file +export default WorldOnscreen; diff --git a/src/helpers/babylonFileLoader.js b/src/helpers/babylonFileLoader.js index 439fc71..f3d9651 100644 --- a/src/helpers/babylonFileLoader.js +++ b/src/helpers/babylonFileLoader.js @@ -7,156 +7,206 @@ export var _BabylonLoaderRegistered = true; * Helps setting up some configuration for the babylon file loader. */ -/** +/** * NOTE: This is a pared down version of the babylon file loader to just load what I need - * Original babylonFileLoader is @babylonjs/core/Loading/Plugins/ + * Original babylonFileLoader is @babylonjs/core/Loading/Plugins/ */ var BabylonFileLoaderConfiguration = /** @class */ (function () { - function BabylonFileLoaderConfiguration() { - } - /** - * The loader does not allow injecting custom physics engine into the plugins. - * Unfortunately in ES6, we need to manually inject them into the plugin. - * So you could set this variable to your engine import to make it work. - */ - BabylonFileLoaderConfiguration.LoaderInjectedPhysicsEngine = false; - return BabylonFileLoaderConfiguration; -}()); + function BabylonFileLoaderConfiguration() {} + /** + * The loader does not allow injecting custom physics engine into the plugins. + * Unfortunately in ES6, we need to manually inject them into the plugin. + * So you could set this variable to your engine import to make it work. + */ + BabylonFileLoaderConfiguration.LoaderInjectedPhysicsEngine = false; + return BabylonFileLoaderConfiguration; +})(); export { BabylonFileLoaderConfiguration }; var isDescendantOf = function (mesh, names, hierarchyIds) { - for (var i in names) { - if (mesh.name === names[i]) { - hierarchyIds.push(mesh.id); - return true; - } - } - if (mesh.parentId && hierarchyIds.indexOf(mesh.parentId) !== -1) { - hierarchyIds.push(mesh.id); - return true; + for (var i in names) { + if (mesh.name === names[i]) { + hierarchyIds.push(mesh.id); + return true; } - return false; + } + if (mesh.parentId && hierarchyIds.indexOf(mesh.parentId) !== -1) { + hierarchyIds.push(mesh.id); + return true; + } + return false; }; var logOperation = function (operation, producer) { - return operation + " of " + (producer ? producer.file + " from " + producer.name + " version: " + producer.version + ", exporter version: " + producer.exporter_version : "unknown"); + return ( + operation + + " of " + + (producer + ? producer.file + + " from " + + producer.name + + " version: " + + producer.version + + ", exporter version: " + + producer.exporter_version + : "unknown") + ); }; SceneLoader.RegisterPlugin({ - name: "babylon.js", - extensions: ".json", - canDirectLoad: function (data) { - if (data.indexOf("json") !== -1) { - return true; - } - return false; - }, - importMesh: function (meshesNames, scene, data, rootUrl, meshes, particleSystems, skeletons, onError) { - // Entire method running in try block, so ALWAYS logs as far as it got, only actually writes details - // when SceneLoader.debugLogging = true (default), or exception encountered. - // Everything stored in var log instead of writing separate lines to support only writing in exception, - // and avoid problems with multiple concurrent .babylon loads. - var log = "importMesh has failed JSON parse"; - try { - var parsedData = JSON.parse(data); - // Force physics off - parsedData.physicsEnabled = false - parsedData?.meshes.map(mesh => delete mesh.physicsImpostor) + name: "babylon.js", + extensions: ".json", + canDirectLoad: function (data) { + if (data.indexOf("json") !== -1) { + return true; + } + return false; + }, + importMesh: function ( + meshesNames, + scene, + data, + rootUrl, + meshes, + particleSystems, + skeletons, + onError + ) { + // Entire method running in try block, so ALWAYS logs as far as it got, only actually writes details + // when SceneLoader.debugLogging = true (default), or exception encountered. + // Everything stored in var log instead of writing separate lines to support only writing in exception, + // and avoid problems with multiple concurrent .babylon loads. + var log = "importMesh has failed JSON parse"; + try { + var parsedData = JSON.parse(data); + // Force physics off + parsedData.physicsEnabled = false; + parsedData?.meshes.map((mesh) => delete mesh.physicsImpostor); - log = ""; + log = ""; - var fullDetails = SceneLoader.loggingLevel === SceneLoader.DETAILED_LOGGING; - if (!meshesNames) { - meshesNames = null; - } - else if (!Array.isArray(meshesNames)) { - meshesNames = [meshesNames]; - } - var hierarchyIds = new Array(); - if (parsedData.meshes !== undefined && parsedData.meshes !== null) { - var index; - var cache; - for (index = 0, cache = parsedData.meshes.length; index < cache; index++) { - var parsedMesh = parsedData.meshes[index]; - if (meshesNames === null || isDescendantOf(parsedMesh, meshesNames, hierarchyIds)) { - if (meshesNames !== null) { - // Remove found mesh name from list. - delete meshesNames[meshesNames.indexOf(parsedMesh.name)]; - } - var mesh = Mesh.Parse(parsedMesh, scene, rootUrl); - meshes.push(mesh); - log += "\n\tMesh " + mesh.toString(fullDetails); - } - } - // Connecting parents and lods - var currentMesh; - for (index = 0, cache = scene.meshes.length; index < cache; index++) { - currentMesh = scene.meshes[index]; - if (currentMesh._waitingParentId) { - currentMesh.parent = scene.getLastEntryByID(currentMesh._waitingParentId); - currentMesh._waitingParentId = null; - } - currentMesh.computeWorldMatrix(true); - } - } - - return true; - } - catch (err) { - var msg = logOperation("importMesh", parsedData ? parsedData.producer : "Unknown") + log; - if (onError) { - onError(msg, err); - } - else { - Logger.Log(msg); - throw err; + var fullDetails = + SceneLoader.loggingLevel === SceneLoader.DETAILED_LOGGING; + if (!meshesNames) { + meshesNames = null; + } else if (!Array.isArray(meshesNames)) { + meshesNames = [meshesNames]; + } + var hierarchyIds = new Array(); + if (parsedData.meshes !== undefined && parsedData.meshes !== null) { + var index; + var cache; + for ( + index = 0, cache = parsedData.meshes.length; + index < cache; + index++ + ) { + var parsedMesh = parsedData.meshes[index]; + if ( + meshesNames === null || + isDescendantOf(parsedMesh, meshesNames, hierarchyIds) + ) { + if (meshesNames !== null) { + // Remove found mesh name from list. + delete meshesNames[meshesNames.indexOf(parsedMesh.name)]; } + var mesh = Mesh.Parse(parsedMesh, scene, rootUrl); + meshes.push(mesh); + log += "\n\tMesh " + mesh.toString(fullDetails); + } } - finally { - if (log !== null && SceneLoader.loggingLevel !== SceneLoader.NO_LOGGING) { - Logger.Log(logOperation("importMesh", parsedData ? parsedData.producer : "Unknown") + (SceneLoader.loggingLevel !== SceneLoader.MINIMAL_LOGGING ? log : "")); - } - } - return false; - }, - load: function (scene, data, rootUrl, onError) { - // Entire method running in try block, so ALWAYS logs as far as it got, only actually writes details - // when SceneLoader.debugLogging = true (default), or exception encountered. - // Everything stored in var log instead of writing separate lines to support only writing in exception, - // and avoid problems with multiple concurrent .babylon loads. - var log = "importScene has failed JSON parse"; - try { - var parsedData = JSON.parse(data); - log = ""; - if (parsedData.clearColor !== undefined && parsedData.clearColor !== null) { - scene.clearColor = Color4.FromArray(parsedData.clearColor); - } - var container = loadAssetContainer(scene, data, rootUrl, onError, true); - if (!container) { - return false; - } - // Finish - return true; - } - catch (err) { - var msg = logOperation("importScene", parsedData ? parsedData.producer : "Unknown") + log; - if (onError) { - onError(msg, err); - } - else { - Logger.Log(msg); - throw err; - } - } - finally { - if (log !== null && SceneLoader.loggingLevel !== SceneLoader.NO_LOGGING) { - Logger.Log(logOperation("importScene", parsedData ? parsedData.producer : "Unknown") + (SceneLoader.loggingLevel !== SceneLoader.MINIMAL_LOGGING ? log : "")); - } + // Connecting parents and lods + var currentMesh; + for (index = 0, cache = scene.meshes.length; index < cache; index++) { + currentMesh = scene.meshes[index]; + if (currentMesh._waitingParentId) { + currentMesh.parent = scene.getLastEntryByID( + currentMesh._waitingParentId + ); + currentMesh._waitingParentId = null; + } + currentMesh.computeWorldMatrix(true); } + } + + return true; + } catch (err) { + var msg = + logOperation( + "importMesh", + parsedData ? parsedData.producer : "Unknown" + ) + log; + if (onError) { + onError(msg, err); + } else { + Logger.Log(msg); + throw err; + } + } finally { + if (log !== null && SceneLoader.loggingLevel !== SceneLoader.NO_LOGGING) { + Logger.Log( + logOperation( + "importMesh", + parsedData ? parsedData.producer : "Unknown" + ) + + (SceneLoader.loggingLevel !== SceneLoader.MINIMAL_LOGGING + ? log + : "") + ); + } + } + return false; + }, + load: function (scene, data, rootUrl, onError) { + // Entire method running in try block, so ALWAYS logs as far as it got, only actually writes details + // when SceneLoader.debugLogging = true (default), or exception encountered. + // Everything stored in var log instead of writing separate lines to support only writing in exception, + // and avoid problems with multiple concurrent .babylon loads. + var log = "importScene has failed JSON parse"; + try { + var parsedData = JSON.parse(data); + log = ""; + if ( + parsedData.clearColor !== undefined && + parsedData.clearColor !== null + ) { + scene.clearColor = Color4.FromArray(parsedData.clearColor); + } + var container = loadAssetContainer(scene, data, rootUrl, onError, true); + if (!container) { return false; - }, - loadAssetContainer: function (scene, data, rootUrl, onError) { - var container = loadAssetContainer(scene, data, rootUrl, onError); - return container; + } + // Finish + return true; + } catch (err) { + var msg = + logOperation( + "importScene", + parsedData ? parsedData.producer : "Unknown" + ) + log; + if (onError) { + onError(msg, err); + } else { + Logger.Log(msg); + throw err; + } + } finally { + if (log !== null && SceneLoader.loggingLevel !== SceneLoader.NO_LOGGING) { + Logger.Log( + logOperation( + "importScene", + parsedData ? parsedData.producer : "Unknown" + ) + + (SceneLoader.loggingLevel !== SceneLoader.MINIMAL_LOGGING + ? log + : "") + ); + } } -}); \ No newline at end of file + return false; + }, + loadAssetContainer: function (scene, data, rootUrl, onError) { + var container = loadAssetContainer(scene, data, rootUrl, onError); + return container; + }, +}); diff --git a/src/helpers/index.js b/src/helpers/index.js index 0ddeaef..aff0f81 100644 --- a/src/helpers/index.js +++ b/src/helpers/index.js @@ -3,29 +3,32 @@ export function lerp(a, b, alpha) { } /** - * Create UUIDs + * Create UUIDs * @return {string} Unique UUID */ export const createUUID = () => { - return ([1e7]+-1e3+-4e3+-8e3+-1e11).replace(/[018]/g, c => { - const crypto = window.crypto || window.msCrypto + return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, (c) => { + const crypto = window.crypto || window.msCrypto; //eslint-disable-next-line - return (c ^ crypto.getRandomValues(new Uint8Array(1))[0] & 15 >> c / 4).toString(16) - }) -} + return ( + c ^ + (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4))) + ).toString(16); + }); +}; export const recursiveSearch = (obj, searchKey, results = []) => { - const r = results; - Object.keys(obj).forEach(key => { - const value = obj[key]; - // if(key === searchKey && typeof value !== 'object'){ - if(key === searchKey){ - r.push(value); - } else if(value && typeof value === 'object'){ - recursiveSearch(value, searchKey, r); - } - }); - return r; + const r = results; + Object.keys(obj).forEach((key) => { + const value = obj[key]; + // if(key === searchKey && typeof value !== 'object'){ + if (key === searchKey) { + r.push(value); + } else if (value && typeof value === "object") { + recursiveSearch(value, searchKey, r); + } + }); + return r; }; /** @@ -34,27 +37,23 @@ export const recursiveSearch = (obj, searchKey, results = []) => { * @param {Function} fn The function to debounce */ export const debounce = (fn) => { - - // Setup a timer - let timeout; - - // Return a function to run debounced - return function () { - - // Setup the arguments - let context = this; - let args = arguments; - - // If there's a timer, cancel it - if (timeout) { - window.cancelAnimationFrame(timeout); - } - - // Setup the new requestAnimationFrame() - timeout = window.requestAnimationFrame(function () { - fn.apply(context, args); - }); - - }; - -} \ No newline at end of file + // Setup a timer + let timeout; + + // Return a function to run debounced + return function () { + // Setup the arguments + let context = this; + let args = arguments; + + // If there's a timer, cancel it + if (timeout) { + window.cancelAnimationFrame(timeout); + } + + // Setup the new requestAnimationFrame() + timeout = window.requestAnimationFrame(function () { + fn.apply(context, args); + }); + }; +}; diff --git a/src/index.js b/src/index.js index 9469895..51e2268 100644 --- a/src/index.js +++ b/src/index.js @@ -1 +1 @@ -export { default } from "./World" \ No newline at end of file +export { default } from "./World"; diff --git a/vite.config.js b/vite.config.js index 1bd8a54..4414061 100644 --- a/vite.config.js +++ b/vite.config.js @@ -1,49 +1,51 @@ -const path = require('path') -const { defineConfig } = require('vite') -const copy = require('rollup-plugin-copy') +const path = require("path"); +const { defineConfig } = require("vite"); +const copy = require("rollup-plugin-copy"); // const { visualizer } = require('rollup-plugin-visualizer'); module.exports = defineConfig({ - base: process.env.NODE_ENV === 'production' ? './' : './src', + base: process.env.NODE_ENV === "production" ? "./" : "./src", build: { lib: { - entry: path.resolve(__dirname, 'src/index.js'), - name: 'dice-box', - fileName: (format) => `dice-box.${format}.js` + entry: path.resolve(__dirname, "src/index.js"), + name: "dice-box", + fileName: (format) => `dice-box.${format}.js`, }, - assetsDir: 'assets/dice-box', + assetsDir: "assets/dice-box", rollupOptions: { - preserveEntrySignatures: "allow-extension", + preserveEntrySignatures: "allow-extension", input: { - main: path.resolve(__dirname, 'src/index.js') - }, - output: [{ - format: "es", - manualChunks: { - // babylon: ['@babylonjs/core','@babylonjs/loaders','@babylonjs/materials'] - }, - sourcemap: false, - }], - plugins: [ - copy({ - targets: [ - { - // src: path.resolve(__dirname, 'src/assets/*'), - src: [ - path.resolve(__dirname, 'src/assets/ammo'), - path.resolve(__dirname, 'src/assets/models'), - path.resolve(__dirname, 'src/assets/themes') - ], - dest: path.resolve(__dirname, 'dist/assets/dice-box') - } - ], - hook: "writeBundle" - }), - // visualizer({ - // open: true, - // brotliSize: true - // }) - ] + main: path.resolve(__dirname, "src/index.js"), + }, + output: [ + { + format: "es", + manualChunks: { + // babylon: ['@babylonjs/core','@babylonjs/loaders','@babylonjs/materials'] + }, + sourcemap: false, + }, + ], + plugins: [ + copy({ + targets: [ + { + // src: path.resolve(__dirname, 'src/assets/*'), + src: [ + path.resolve(__dirname, "src/assets/ammo"), + path.resolve(__dirname, "src/assets/models"), + path.resolve(__dirname, "src/assets/themes"), + ], + dest: path.resolve(__dirname, "dist/assets/dice-box"), + }, + ], + hook: "writeBundle", + }), + // visualizer({ + // open: true, + // brotliSize: true + // }) + ], }, - } -}) \ No newline at end of file + }, +});