fix: scene clear race, path editor anchors, avatar cover, team color crash

Clear scene now patches traps/tokens/npcTokens atomically and updateScene merges against a fresh scene snapshot. Align NPC path-editor previews on token center, default new paths to fixed facing, size avatars by natural aspect for cover, and read team color synchronously from the picker.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Ivan Fontosh
2026-08-29 15:30:28 +08:00
parent 028c1be059
commit 0a9168b320
10 changed files with 189 additions and 95 deletions
@@ -34,13 +34,15 @@
box-shadow: 0 0 0 3px var(--accent-fill-soft, rgba(167, 139, 250, 0.28));
}
/* Width/height set in JS (syncAvatarCoverSize) for aspect-preserving cover.
Do not force a square img box — that cropped tall/wide photos before pan/zoom. */
.avatar {
position: absolute;
left: 50%;
top: 50%;
width: 140%;
height: 140%;
object-fit: cover;
max-width: none;
max-height: none;
object-fit: fill;
pointer-events: none;
}
@@ -22,6 +22,17 @@ function writeAvatarTransform(
img.style.transform = `translate(-50%, -50%) translate(${String(offset.x * 100)}%, ${String(offset.y * 100)}%) scale(${String(scale)})`;
}
/** Size the <img> so it covers the circle while keeping natural aspect (no square pre-crop). */
function syncAvatarCoverSize(img: HTMLImageElement | null, circlePx: number) {
if (!img || circlePx <= 0) return;
const nw = img.naturalWidth;
const nh = img.naturalHeight;
if (nw <= 0 || nh <= 0) return;
const cover = Math.max(circlePx / nw, circlePx / nh);
img.style.width = `${String(nw * cover)}px`;
img.style.height = `${String(nh * cover)}px`;
}
export type PlayerTokenViewProps = {
name: string;
imageUrl: string | null;
@@ -134,6 +145,7 @@ export function PlayerTokenView({
}, [scale]);
useLayoutEffect(() => {
syncAvatarCoverSize(imgRef.current, sizePx);
writeAvatarTransform(imgRef.current, visualOffsetRef.current, visualScaleRef.current);
});
@@ -259,6 +271,10 @@ export function PlayerTokenView({
alt=""
draggable={false}
style={inactive ? { filter: 'grayscale(1)', opacity: 0.85 } : undefined}
onLoad={() => {
syncAvatarCoverSize(imgRef.current, sizePx);
writeAvatarTransform(imgRef.current, visualOffsetRef.current, visualScaleRef.current);
}}
/>
) : (
<div className={[styles.avatarEmpty, emptyContent ? styles.avatarEmptySolid : ''].filter(Boolean).join(' ')}>