Add movement artist filter modal and fix large-hall texture blanks

Clicking a movement opens ArtistFilterModal to choose artists before the gallery. Large halls no longer permanently blank frames when texture loads exceed the overlay deadline; fall back thumb to full to on-demand API.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-07-29 19:05:54 +03:00
co-authored by Cursor
parent ad1572b3aa
commit 0a5918c4dd
9 changed files with 589 additions and 23 deletions
+35 -13
View File
@@ -12,7 +12,7 @@ import type {
MovementArtistGroup,
TourGalleryDetail,
} from '../types';
import { galleryImageUrlWithRevision, imageUrl, api } from '../api/client';
import { galleryImageUrlCandidates, imageUrl, api } from '../api/client';
import { comparePaintingsChronological, paintingHasCuratorNotes, paintingHasInfluenceLinks, paintingWallCaption } from '../utils/paintingUtils';
import { cloneSurfaceTexture, getSurfaceTexture } from '../utils/galleryProceduralTextures';
import { resolveMovementInteriorStyle, type MovementInteriorStyle, type GalleryWindowSpec } from '../data/movement-interior-styles';
@@ -647,20 +647,32 @@ function CanvasCover({
);
}
function usePaintingTexture(url: string | null) {
function usePaintingTexture(urls: string[] | string | null) {
const candidates = useMemo(() => {
const list = Array.isArray(urls) ? urls.filter(Boolean) : urls ? [urls] : [];
return list;
}, [Array.isArray(urls) ? urls.join('|') : urls ?? '']);
const candidateKey = candidates.join('|');
const [urlIndex, setUrlIndex] = useState(0);
const url = candidates[urlIndex] ?? null;
const [texture, setTexture] = useState<THREE.Texture | null>(null);
const [failed, setFailed] = useState(!url);
const [failed, setFailed] = useState(candidates.length === 0);
const textureLoad = useContext(GalleryTextureLoadContext);
const { gl } = useThree();
useEffect(() => {
setUrlIndex(0);
}, [candidateKey]);
useEffect(() => {
if (!url) {
setTexture(null);
setFailed(true);
setFailed(candidates.length === 0 || urlIndex >= candidates.length);
return;
}
setFailed(false);
setTexture(null);
let disposed = false;
let loaded: THREE.Texture | null = null;
let settled = false;
@@ -674,9 +686,12 @@ function usePaintingTexture(url: string | null) {
};
textureLoad?.begin();
// Release the loading overlay after a deadline, but do NOT mark the texture
// failed — large halls (e.g. Byzantine, 45 works) queue behind ~6 browser
// connections and often finish after 10s. Marking failed permanently left
// blank canvases even when the image arrived later.
const loadTimeout = window.setTimeout(() => {
if (settled || disposed) return;
setFailed(true);
finish();
}, TEXTURE_LOAD_TIMEOUT_MS);
@@ -692,17 +707,21 @@ function usePaintingTexture(url: string | null) {
const img = tex.image as HTMLImageElement | undefined;
if (!img || img.width < 4 || img.height < 4) {
tex.dispose();
setFailed(true);
finish();
if (!disposed) {
if (urlIndex + 1 < candidates.length) setUrlIndex((i) => i + 1);
else setFailed(true);
}
return;
}
loaded = tex;
tex.colorSpace = THREE.SRGBColorSpace;
tex.anisotropy = 4;
// Release the hall overlay counter before GPU upload — large halls
// (50+ works) otherwise stay on "Loading paintings…" for a long time.
finish();
if (!disposed) setTexture(tex);
if (!disposed) {
setFailed(false);
setTexture(tex);
}
try {
if (!disposed) gl.initTexture(tex);
} catch {
@@ -713,7 +732,10 @@ function usePaintingTexture(url: string | null) {
() => {
window.clearTimeout(loadTimeout);
finish();
if (!disposed) setFailed(true);
if (!disposed) {
if (urlIndex + 1 < candidates.length) setUrlIndex((i) => i + 1);
else setFailed(true);
}
}
);
@@ -724,7 +746,7 @@ function usePaintingTexture(url: string | null) {
loaded?.dispose();
setTexture(null);
};
}, [url, textureLoad, gl]);
}, [url, urlIndex, candidates.length, textureLoad, gl]);
return { texture, failed };
}
@@ -877,8 +899,8 @@ function PaintingFrame({
const reviewed = paintingIsReviewed(painting);
const { matBorder, rail, depth: frameDepth } = frameDimsForReviewed(reviewed);
const hasImage = paintingHasGalleryImage(painting);
const url = hasImage ? galleryImageUrlWithRevision(painting, imageRevision) : null;
const { texture, failed } = usePaintingTexture(url);
const urls = hasImage ? galleryImageUrlCandidates(painting, imageRevision) : [];
const { texture, failed } = usePaintingTexture(urls);
const showImage = hasImage && !failed && !!texture;
const showCanvas = !showImage;
const hasInfluenceLinks = paintingHasInfluenceLinks(painting);