Skip to content

Commit 89d5031

Browse files
committed
bitmap 3D PFP: apply 4 patterns lifted from THREE-JS-NIPPLEJS-REFERENCES
Apply the tier-1 findings from the curated repo list: 1. Hubs-Foundation/hubs -- nipplejs hardcodes `z-index: 999` on its rendered UI elements as an inline style; this steals clicks from any modal/dialog/overlay layered above the canvas. Mirror Hubs' workaround: `el.style.removeProperty('z-index')` immediately after each create(). Helper `stripNippleZIndex(mgr)` factored so both sticks get it. 2. rune/rune -- nipplejs issue #64: a touchcancel (system gesture, incoming call, alert dialog) can swallow the touchend and leave the stick partially active. Cached vector freezes, camera keeps moving. Hook document.touchcancel to zero state AND destroy+recreate both sticks. More robust than relying on the visibilitychange path alone. 3. needle-tools/needle-engine-samples -- the asymmetric static-left / dynamic-right pattern. Movement stick stays at fixed position (muscle memory: walking is what the brain does on autopilot). Look stick now `mode: 'dynamic'` -- joystick appears under the thumb anywhere in the right zone. Mirrors console FPS ergonomics (left analogue is fixed by the controller body; right analogue is the active-aim input). 4. runeharlyk/SpotMicroESP32-Leika -- module-scope lifecycle pattern. Already in place structurally: our init / destroy live in the render-loop closure (a function-level scope, NOT in Angular's class-method lifecycle), so the framework's CD can't lose track of them. No code change needed; documenting the alignment here so a future refactor doesn't reintroduce the anti-pattern. Source list: /Users/johanneshoppe/Work/ordpool/THREE-JS-NIPPLEJS-REFERENCES.md
1 parent ac818d3 commit 89d5031

1 file changed

Lines changed: 38 additions & 5 deletions

File tree

frontend/src/app/components/_ordpool/digital-artifact-viewer/bitmap-viewer/bitmap-3d-renderer.component.ts

Lines changed: 38 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -530,18 +530,30 @@ export class Bitmap3dRendererComponent implements AfterViewInit, OnDestroy {
530530
const LOOK_DEADZONE = 0.15;
531531
const INVERT_LOOK_Y = false;
532532

533+
// Helper to strip nipplejs's hardcoded `z-index: 999` inline style off
534+
// the rendered UI so overlay buttons / dialogs stay clickable above
535+
// the joystick visuals. Pattern lifted from Hubs-Foundation/hubs
536+
// (src/components/virtual-gamepad-controls.js). Applied right after
537+
// each stick is created.
538+
const stripNippleZIndex = (mgr: any) => {
539+
try {
540+
const el = mgr?.[0]?.ui?.el;
541+
if (el?.style) el.style.removeProperty('z-index');
542+
} catch { /* noop */ }
543+
};
544+
533545
const initJoysticks = async () => {
534546
if (nippleL && nippleR) return;
535547
const { default: nipplejs } = await import('nipplejs');
536-
// Left stick: movement. Centered in the left zone div (50% across
537-
// the zone from each edge).
548+
// Left stick: movement, STATIC -- fixed origin, muscle memory.
538549
const moveStick: any = (nipplejs as any).create({
539550
zone: this.joyZoneL.nativeElement,
540551
mode: 'static',
541552
position: { left: '50%', top: '50%' },
542553
color: '#FF9900',
543554
size: 120,
544555
});
556+
stripNippleZIndex(moveStick);
545557
moveStick.on('move', (_e: unknown, d: any) => {
546558
// nipplejs vector y is positive UP (screen-inverted from CSS y).
547559
joy.right = d.vector.x;
@@ -550,14 +562,16 @@ export class Bitmap3dRendererComponent implements AfterViewInit, OnDestroy {
550562
moveStick.on('end', () => { joy.fwd = 0; joy.right = 0; });
551563
nippleL = moveStick;
552564

553-
// Right stick: look.
565+
// Right stick: look, DYNAMIC -- appears under the thumb anywhere in
566+
// the right zone. Console-FPS ergonomics: fixed move, free-aim look.
567+
// Pattern from needle-tools/needle-engine-samples FirstPersonController.
554568
const lookStick: any = (nipplejs as any).create({
555569
zone: this.joyZoneR.nativeElement,
556-
mode: 'static',
557-
position: { left: '50%', top: '50%' },
570+
mode: 'dynamic',
558571
color: '#FF9900',
559572
size: 120,
560573
});
574+
stripNippleZIndex(lookStick);
561575
lookStick.on('move', (_e: unknown, d: any) => {
562576
look.x = d.vector.x;
563577
look.y = d.vector.y;
@@ -577,6 +591,24 @@ export class Bitmap3dRendererComponent implements AfterViewInit, OnDestroy {
577591
else if (state === 'pfp') void initJoysticks();
578592
};
579593
document.addEventListener('visibilitychange', onVisibility);
594+
// Touch-cancel defence (nipplejs #64): a system gesture / call /
595+
// alert can swallow the touchend, leaving the stick partially active
596+
// and the cached vector frozen. rune/rune (the multiplayer SDK
597+
// sample) does an explicit destroy+recreate on touchcancel; we
598+
// mirror that. Listen on the document so it catches cancels that
599+
// happen outside the zone divs.
600+
const onTouchCancel = () => {
601+
// Zero cached state immediately so the camera stops moving.
602+
joy.fwd = 0; joy.right = 0;
603+
look.x = 0; look.y = 0;
604+
// Rebuild the sticks so any partially-active internal state in
605+
// nipplejs is discarded. Only when we're still in PFP.
606+
if (state === 'pfp') {
607+
destroyJoysticks();
608+
void initJoysticks();
609+
}
610+
};
611+
document.addEventListener('touchcancel', onTouchCancel);
580612

581613
// Per-frame look integration (rate-of-change). Runs each rAF tick
582614
// while in PFP. Yaw and pitch advance proportional to stick
@@ -606,6 +638,7 @@ export class Bitmap3dRendererComponent implements AfterViewInit, OnDestroy {
606638
document.removeEventListener('mousemove', onMouseMove);
607639
renderer.domElement.removeEventListener('pointerdown', onPointerDown);
608640
document.removeEventListener('visibilitychange', onVisibility);
641+
document.removeEventListener('touchcancel', onTouchCancel);
609642
destroyJoysticks();
610643
jumpEl.removeEventListener('touchstart', triggerJump);
611644
jumpEl.removeEventListener('mousedown', triggerJump);

0 commit comments

Comments
 (0)