Align influence lamps 40cm above the tallest hall frame.

Compute the highest allocated frame top per hall and place every influence lamp on that shared rail so markers line up across different canvas sizes.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila Khodjaef
2026-08-09 22:42:23 +03:00
co-authored by Cursor
parent cfee69c9a6
commit f20f811f21
2 changed files with 33 additions and 10 deletions
+1 -1
View File
@@ -301,7 +301,7 @@ Each artist has **exactly one hall**. The hall is a rectangular room sized to fi
| Floor | Herringbone **parquet** procedural texture (`parquetFloorTexture.ts`) | | Floor | Herringbone **parquet** procedural texture (`parquetFloorTexture.ts`) |
| Wall tint | Gallery walls blend the artists **movement colour** into cream plaster tones | | Wall tint | Gallery walls blend the artists **movement colour** into cream plaster tones |
| Frame finish | **Unchecked** works: black moulding; **Reviewed** (`checkup_checked`): bright gold moulding at **double width** | | Frame finish | **Unchecked** works: black moulding; **Reviewed** (`checkup_checked`): bright gold moulding at **double width** |
| Influence lamps | A golden picture light appears **above frames** whose work has any influence-graph edge (`has_influence_links` from the API); fixture is mounted **upside down** and is **emissive-only** (no per-frame lights — see light budget below) | | Influence lamps | A golden picture light marks works with any influence-graph edge (`has_influence_links` from the API). All lamps in a hall share one rail height: **40 cm above the tallest allocated frame** in that hall (so they line up regardless of canvas size). Fixture is mounted **upside down** and is **emissive-only** (no per-frame lights — see light budget below) |
| Curator-note plates | A small brass plate hangs **beneath frames** that have non-empty `curator_notes` | | Curator-note plates | A small brass plate hangs **beneath frames** that have non-empty `curator_notes` |
| Eye-level viewing | Frame centres sit at **eye height (~1.65 m)**; the camera stays **level with the floor** (no pitch up/down) | | Eye-level viewing | Frame centres sit at **eye height (~1.65 m)**; the camera stays **level with the floor** (no pitch up/down) |
| Open centre | Floor and ceiling only — no freestanding columns or pedestals in the walkway | | Open centre | Floor and ceiling only — no freestanding columns or pedestals in the walkway |
+32 -9
View File
@@ -191,8 +191,8 @@ function galleryWallColors(movementColor?: string) {
}; };
} }
/** Height above frame top edge — keeps fixture out of the viewer's line of sight. */ /** Clearance above the tallest frame in the hall for influence lamps (metres). */
const INFLUENCE_LAMP_ABOVE_FRAME = 0.46; const INFLUENCE_LAMP_ABOVE_HIGHEST_M = 0.4;
type WallSide = 'back' | 'left' | 'right'; type WallSide = 'back' | 'left' | 'right';
@@ -304,6 +304,20 @@ function frameOuterH(canvasH: number, reviewed: boolean): number {
return canvasH + matBorder * 2 + rail; return canvasH + matBorder * 2 + rail;
} }
/** World Y of the top edge of the tallest allocated frame in the hall. */
function highestPaintingTopY(segments: WallSegment[]): number {
let maxTop = EYE_HEIGHT;
for (const seg of segments) {
for (let i = 0; i < seg.paintings.length; i++) {
const slot = seg.slots[i];
if (!slot) continue;
const top = slot.position[1] + frameOuterH(slot.maxH, paintingIsReviewed(seg.paintings[i])) / 2;
if (top > maxTop) maxTop = top;
}
}
return maxTop;
}
function layoutRow(paintings: Painting[], span: number) { function layoutRow(paintings: Painting[], span: number) {
const count = paintings.length; const count = paintings.length;
if (count === 0) return { slots: [], spanNeeded: span }; if (count === 0) return { slots: [], spanNeeded: span };
@@ -803,17 +817,19 @@ function usePaintingTexture(urls: string[] | string | null) {
} }
function InfluencePictureLamp({ function InfluencePictureLamp({
frameHeight, frameWorldY,
matBorder, lampWorldY,
frameDepth, frameDepth,
highlighted, highlighted,
}: { }: {
frameHeight: number; /** Painting group world Y — local lamp offset = lampWorldY frameWorldY. */
matBorder: number; frameWorldY: number;
/** Shared hall rail: 40 cm above the tallest frame. */
lampWorldY: number;
frameDepth: number; frameDepth: number;
highlighted: boolean; highlighted: boolean;
}) { }) {
const mountY = frameHeight / 2 + matBorder + INFLUENCE_LAMP_ABOVE_FRAME; const mountY = lampWorldY - frameWorldY;
const glow = highlighted ? 1.6 : 1.15; const glow = highlighted ? 1.6 : 1.15;
const scale = 1.35; const scale = 1.35;
@@ -918,6 +934,7 @@ function PaintingFrame({
wallSide, wallSide,
imageRevision, imageRevision,
caption, caption,
influenceLampWorldY,
onClick, onClick,
}: { }: {
painting: Painting; painting: Painting;
@@ -928,6 +945,7 @@ function PaintingFrame({
wallSide: WallSide; wallSide: WallSide;
imageRevision?: number; imageRevision?: number;
caption?: string; caption?: string;
influenceLampWorldY: number;
onClick: () => void; onClick: () => void;
}) { }) {
const [hovered, setHovered] = useState(false); const [hovered, setHovered] = useState(false);
@@ -1018,8 +1036,8 @@ function PaintingFrame({
{hasInfluenceLinks && ( {hasInfluenceLinks && (
<InfluencePictureLamp <InfluencePictureLamp
frameHeight={height} frameWorldY={position[1]}
matBorder={matBorder} lampWorldY={influenceLampWorldY}
frameDepth={frameDepth} frameDepth={frameDepth}
highlighted={hovered} highlighted={hovered}
/> />
@@ -1505,6 +1523,10 @@ function ArtistHall({
const hallLightScale = interiorStyle?.lightScale ?? 1; const hallLightScale = interiorStyle?.lightScale ?? 1;
const wallRoughness = interiorStyle ? 0.75 : 0.92; const wallRoughness = interiorStyle ? 0.75 : 0.92;
const wallMetalness = interiorStyle ? 0.08 : 0.06; const wallMetalness = interiorStyle ? 0.08 : 0.06;
const influenceLampWorldY = useMemo(
() => highestPaintingTopY(segments) + INFLUENCE_LAMP_ABOVE_HIGHEST_M,
[segments]
);
const wallMaterial = (color: string) => ( const wallMaterial = (color: string) => (
<meshStandardMaterial color={color} roughness={wallRoughness} metalness={wallMetalness} /> <meshStandardMaterial color={color} roughness={wallRoughness} metalness={wallMetalness} />
@@ -1752,6 +1774,7 @@ function ArtistHall({
wallSide={seg.slots[i].side} wallSide={seg.slots[i].side}
imageRevision={imageRevisions?.[painting.id]} imageRevision={imageRevisions?.[painting.id]}
caption={showCaptions ? paintingWallCaption(painting) : undefined} caption={showCaptions ? paintingWallCaption(painting) : undefined}
influenceLampWorldY={influenceLampWorldY}
onClick={() => onPaintingClick(painting.id)} onClick={() => onPaintingClick(painting.id)}
/> />
))} ))}