Skip to content

Commit 34dc8cf

Browse files
committed
bitmap 3D PFP: bypass Angular CD entirely with classList toggles
Two days of "controls don't show" debugging finally converged on "Angular's class binding update isn't reaching the host element on the user's device". Theories ruled out: - The classProp call IS compiled into the bundle (verified via grep on the deployed main.js: AVh("touch-ui", showTouchUi) is there) - showTouchUi IS getting set true (no path sets it back to false in the failure case) - The CSS selectors ARE correct (.bitmap3d-host.touch-ui ...) - The deploy IS landing (build artifact matches local SHA) What remains: Angular's CD chain inside zone.run inside an outside-the-zone rAF callback is silently not propagating the class binding on at least one mobile browser. Reproducing it requires the exact device; I can't from here. Fix: stop relying on Angular's binding system for this. The host's classList is now toggled by direct DOM manipulation (host.classList.toggle('pfp-on', ...)) at the same state-machine transition points. CSS gates visibility on `.bitmap3d-host.pfp-on.touch-on`. Same effect, but the path from "state transitions to pfp" to "class appears on the DOM" is now one synchronous classList call -- no zone.run, no detectChanges, no OnPush walking. Can't be lost. Kept showTouchUi as a state tracker (setLastInput still reads it for the if-no-change-skip optimisation), but it no longer drives the template binding.
1 parent 13d11bf commit 34dc8cf

1 file changed

Lines changed: 27 additions & 21 deletions

File tree

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

Lines changed: 27 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E
33
@Component({
44
selector: 'app-bitmap-3d-renderer',
55
template: `
6-
<div #host class="bitmap3d-host" [class.touch-ui]="showTouchUi">
6+
<div #host class="bitmap3d-host">
77
<div #joyBase class="touch-joy-base"></div>
88
<div #joyKnob class="touch-joy-knob"></div>
99
<button type="button" #jumpBtn class="touch-jump" aria-label="Jump">▲</button>
@@ -12,6 +12,13 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E
1212
:host { display: block; width: 100%; aspect-ratio: 1 / 1; max-width: 600px; }
1313
.bitmap3d-host { position: relative; width: 100%; height: 100%; }
1414
.bitmap3d-host > canvas { position: absolute; inset: 0; width: 100% !important; height: 100% !important; display: block; }
15+
16+
/* Touch UI elements default to HIDDEN. Only when the host carries
17+
.pfp-on AND .touch-on does the jump button show. Visibility is
18+
toggled by direct DOM classList manipulation (renderer state
19+
machine writes to host.classList) rather than Angular [class.x]
20+
binding -- bypasses OnPush + zone-run CD entirely so it's
21+
impossible for the binding to silently not propagate. */
1522
.touch-jump,
1623
.touch-joy-base,
1724
.touch-joy-knob {
@@ -56,11 +63,11 @@ import { AfterViewInit, ChangeDetectionStrategy, ChangeDetectorRef, Component, E
5663
height: 56px;
5764
background: rgba(255, 153, 0, 0.75);
5865
}
59-
/* Jump button visible whenever touch UI is on. */
60-
.bitmap3d-host.touch-ui .touch-jump { display: flex; }
66+
/* Jump button visible whenever the host is in PFP + touch mode. */
67+
.bitmap3d-host.pfp-on.touch-on .touch-jump { display: flex; }
6168
/* Joystick base+knob only when ALSO actively touched (showJoy adds 'touch-joy-active'). */
62-
.bitmap3d-host.touch-ui .touch-joy-base.touch-joy-active,
63-
.bitmap3d-host.touch-ui .touch-joy-knob.touch-joy-active { display: block; }
69+
.bitmap3d-host.pfp-on.touch-on .touch-joy-base.touch-joy-active,
70+
.bitmap3d-host.pfp-on.touch-on .touch-joy-knob.touch-joy-active { display: block; }
6471
`],
6572
changeDetection: ChangeDetectionStrategy.OnPush,
6673
standalone: false,
@@ -456,14 +463,17 @@ export class Bitmap3dRendererComponent implements AfterViewInit, OnDestroy {
456463
// reliable than upfront device classification, which had too many
457464
// false-negatives on devices that DO have touch (iPad with
458465
// Pencil/Magic Keyboard, Android with desktop-mode toggles, etc.).
466+
const setTouchClass = (on: boolean) => {
467+
// Direct DOM, no Angular binding -- can't be lost to a missed CD.
468+
hostEl.classList.toggle('touch-on', on);
469+
this.showTouchUi = on;
470+
};
471+
const setPfpClass = (on: boolean) => {
472+
hostEl.classList.toggle('pfp-on', on);
473+
};
459474
const setLastInput = (t: 'kbm' | 'touch') => {
460475
if (state !== 'pfp') return;
461-
const wantTouchUi = (t === 'touch');
462-
if (this.showTouchUi === wantTouchUi) return;
463-
this.zone.run(() => {
464-
this.showTouchUi = wantTouchUi;
465-
this.cdr.detectChanges();
466-
});
476+
setTouchClass(t === 'touch');
467477
};
468478

469479
const keyStates: Record<string, boolean> = {};
@@ -919,21 +929,17 @@ export class Bitmap3dRendererComponent implements AfterViewInit, OnDestroy {
919929
state = 'pfp';
920930
// Touch UI starts visible in every PFP session. Keyboard
921931
// users see it disappear on their first WASD/Space press.
922-
this.zone.run(() => {
923-
this.showTouchUi = true;
924-
this.cdr.detectChanges();
925-
});
932+
setPfpClass(true);
933+
setTouchClass(true);
926934
} else if (flyAfterIso === 'orbit') {
927935
controls.enabled = true;
928936
state = 'orbit';
929-
if (this.showTouchUi) {
930-
this.zone.run(() => { this.showTouchUi = false; this.cdr.detectChanges(); });
931-
}
937+
setPfpClass(false);
938+
setTouchClass(false);
932939
} else {
933940
state = 'exit-done';
934-
if (this.showTouchUi) {
935-
this.zone.run(() => { this.showTouchUi = false; this.cdr.detectChanges(); });
936-
}
941+
setPfpClass(false);
942+
setTouchClass(false);
937943
this.zone.run(() => this.exitDone.emit());
938944
}
939945
}

0 commit comments

Comments
 (0)