Skip to content

Fix add() method firing callbacks before animation completes - #21

Open
hughescr wants to merge 5 commits into
3d-dice:mainfrom
hughescr:main
Open

Fix add() method firing callbacks before animation completes#21
hughescr wants to merge 5 commits into
3d-dice:mainfrom
hughescr:main

Conversation

@hughescr

@hughescr hughescr commented Jan 6, 2026

Copy link
Copy Markdown

Summary

Fix add() method firing completion handlers before dice animation completes.

Problem

When calling add() to add dice to an existing roll, the completion handlers (onAddDiceComplete callback, addDiceComplete event, and Promise resolution) fire immediately on the first animation frame instead of waiting for the dice to settle.

Root Cause

In the add() method:

  1. simulateThrow() pre-simulates the physics to completion, leaving newly added dice in SLEEPING state
  2. Dice are re-spawned for visual animation
  3. animateThrow() starts with the completion callback
  4. On the first frame, throwFinished() checks if all dice have sleepState >= SLEEPING — they do (from pre-simulation) — so the callback fires immediately

Why roll() works correctly

The roll() method calls clearDice() first, which removes all existing dice. Fresh dice start in an active state and need time to settle, so throwFinished() correctly waits.

The Fix

After spawning dice and before starting animation, wake up the newly added dice so they need time to settle during the visible animation:

  for (const idx of diceIdArray) {
      const die = this.diceList[idx];
      if (die && die.body) {
          die.body.wakeUp();
      }
  }

This ensures throwFinished() won't return true until the new dice actually settle.

rollMulti(specs): place multiple dice with per-die colorsets in a single
physics throw; each returned set is tagged with its originating colorset so
callers can attribute results per die. roll()/add()/reroll()/remove() behavior
is unchanged (their results gain colorset:null/label:null fields).

create(type, colordata): apply an optional per-die colorset override before
baking the material.

Environment reflections: stop hardcoding envMapIntensity=0 on PBR dice
materials (respect each material type's configured intensity), and set a soft
default scene.environment (RoomEnvironment via PMREMGenerator) so metallic and
glossy dice render with reflections instead of appearing dark.

Claude-Session: https://claude.ai/code/session_01S953CfDEvtvxEZomKTJBWS
…rset redesign

Migrate sound playback from HTMLAudioElement to the Web Audio API
(AudioContext/AudioBufferSourceNode), fixing animation stutter on iOS and
desktop when sounds are enabled. AudioContext is created lazily and resumed
from user-gesture call stacks so playback isn't silently blocked by
autoplay/gesture restrictions.

Physics performance: switch to CANNON.SAPBroadphase for collision
broadphase, cap the per-frame catch-up loop so a stalled/backgrounded tab
can't spiral into running many world.step() calls back-to-back, and make
the pre-throw settle simulation yield to the event loop periodically
instead of blocking the main thread.

Correctness fixes: updateConfig() now applies merged options via
Object.assign and only touches theme fields the caller actually passed
(via property-presence checks) instead of wiping theme_customColorset back
to null; rollMulti() claims ownership synchronously before its prewarm
await to fix a supersession race; destroy() mid-add() now rejects add()'s
Promise instead of silently resolving it; per-set +/- operators in dice
notation (e.g. 2d6-1d4) are respected; signed modifier merging nets out
correctly including exact cancellation to zero; forced/predetermined d4
results now produce the correct face; disotope's explicit per-face values
are no longer collapsed into a min/max/step range; malformed forced-result
(@) suffixes are rejected as malformed notation instead of silently
dropping tokens. Malformed/unparseable notation now rejects the returned
Promise with a descriptive Error, and a roll superseded by a newer
roll()/rollMulti()/reroll()/add() call now rejects instead of hanging
forever.

Memory/lifecycle: add a destroy() method to fully tear down a DiceBox
instance (stops in-flight rolls/animation, removes the resize listener,
settles pending Promises, disposes GPU resources, closes the AudioContext,
removes the canvas), add an LRU cache with deferred disposal for composite
material textures, and cache/dispose bump normal-map textures per-
DiceFactory instance rather than on a shared static registry.

Colorsets redesigned for WCAG AAA text legibility (numeral/outline
contrast against each die's body color), with several damage-type
colorsets conceptually rethemed; colorset names and the theme_colorset
selection API are unchanged.

Packaging fixes: corrected the npm "files" allowlist, marked the package
"type": "module" with a proper "exports" map (ESM-only, no CommonJS
build), and removed .npmignore now that "files" governs the published
tarball. Expanded README and added this CHANGELOG.md.

Added src/index.html + src/demo.js, a dev demo page exercising
roll/add/reroll/rollMulti/colorset switching for local development and
manual testing.

Claude-Session: https://claude.ai/code/session_018pN5dERVrqvBKQXFsoPYzv
The coin (d2) die's face labels and bump maps reference public/textures/silvercoin/, which was missing from the repo entirely (lost upstream; restored from the ancestor MajorVictory/3DDiceRoller project) — coin faces rendered blank without them. Also rename the dc preset's data key setBumpMaps -> bumpMaps so DicePreset's constructor actually loads the bump maps; the key was named after the setter method instead of the property it checks. Visual effect of the bump maps is subtle (the procedural label bump and metal composite dominate), but the preset data is now functional instead of dead.

Claude-Session: https://claude.ai/code/session_018pN5dERVrqvBKQXFsoPYzv
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant