diff --git a/Documentation/basics.md b/Documentation/basics.md index 0cdc4b3..10f4f1a 100644 --- a/Documentation/basics.md +++ b/Documentation/basics.md @@ -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`) | | Wall tint | Gallery walls blend the artist’s **movement colour** into cream plaster tones | | 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` | | 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 | diff --git a/client/src/components/VirtualGallery.tsx b/client/src/components/VirtualGallery.tsx index dd791d8..d7c4529 100644 --- a/client/src/components/VirtualGallery.tsx +++ b/client/src/components/VirtualGallery.tsx @@ -191,8 +191,8 @@ function galleryWallColors(movementColor?: string) { }; } -/** Height above frame top edge — keeps fixture out of the viewer's line of sight. */ -const INFLUENCE_LAMP_ABOVE_FRAME = 0.46; +/** Clearance above the tallest frame in the hall for influence lamps (metres). */ +const INFLUENCE_LAMP_ABOVE_HIGHEST_M = 0.4; type WallSide = 'back' | 'left' | 'right'; @@ -304,6 +304,20 @@ function frameOuterH(canvasH: number, reviewed: boolean): number { 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) { const count = paintings.length; if (count === 0) return { slots: [], spanNeeded: span }; @@ -803,17 +817,19 @@ function usePaintingTexture(urls: string[] | string | null) { } function InfluencePictureLamp({ - frameHeight, - matBorder, + frameWorldY, + lampWorldY, frameDepth, highlighted, }: { - frameHeight: number; - matBorder: number; + /** Painting group world Y — local lamp offset = lampWorldY − frameWorldY. */ + frameWorldY: number; + /** Shared hall rail: 40 cm above the tallest frame. */ + lampWorldY: number; frameDepth: number; highlighted: boolean; }) { - const mountY = frameHeight / 2 + matBorder + INFLUENCE_LAMP_ABOVE_FRAME; + const mountY = lampWorldY - frameWorldY; const glow = highlighted ? 1.6 : 1.15; const scale = 1.35; @@ -918,6 +934,7 @@ function PaintingFrame({ wallSide, imageRevision, caption, + influenceLampWorldY, onClick, }: { painting: Painting; @@ -928,6 +945,7 @@ function PaintingFrame({ wallSide: WallSide; imageRevision?: number; caption?: string; + influenceLampWorldY: number; onClick: () => void; }) { const [hovered, setHovered] = useState(false); @@ -1018,8 +1036,8 @@ function PaintingFrame({ {hasInfluenceLinks && ( @@ -1505,6 +1523,10 @@ function ArtistHall({ const hallLightScale = interiorStyle?.lightScale ?? 1; const wallRoughness = interiorStyle ? 0.75 : 0.92; const wallMetalness = interiorStyle ? 0.08 : 0.06; + const influenceLampWorldY = useMemo( + () => highestPaintingTopY(segments) + INFLUENCE_LAMP_ABOVE_HIGHEST_M, + [segments] + ); const wallMaterial = (color: string) => ( @@ -1752,6 +1774,7 @@ function ArtistHall({ wallSide={seg.slots[i].side} imageRevision={imageRevisions?.[painting.id]} caption={showCaptions ? paintingWallCaption(painting) : undefined} + influenceLampWorldY={influenceLampWorldY} onClick={() => onPaintingClick(painting.id)} /> ))}