Rework Gothic hall as a stone nave interior.

Add gothic ashlar/vault/floor textures, compound piers with transverse ribs, proper lancet arches, textured door-flanking panels, and lightScale for shaft-lit naves. Docs updated in basics and data-and-images.

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Danila KhodjaefandCursor committed 2026-08-03 17:16:54 +03:00
1 parent 1cb9eef935
commit f9b0fb0496
7 files changed
+501 -61

No files matched your search

+50 -4
View File
@@ -69,10 +69,56 @@ function WindowFrame({
</mesh>
{arch && style === 'gothic-lancet' && (
<mesh position={[0, height / 2 + 0.15, -depth / 2 + 0.02]}>
<coneGeometry args={[width / 2 + frameW, 0.5, 4]} />
<meshStandardMaterial color={trimColor} roughness={0.4} metalness={0.35} />
</mesh>
<>
{/* Two-centred arch head built from short chords */}
{Array.from({ length: 14 }, (_, i) => {
const segs = 14;
const rise = width * 0.62;
const pt = (t: number): [number, number] => {
const x = (t - 0.5) * (width + frameW * 2);
const y = Math.pow(Math.cos((t - 0.5) * Math.PI), 0.72) * rise;
return [x, y];
};
const [x0, y0] = pt(i / segs);
const [x1, y1] = pt((i + 1) / segs);
const len = Math.hypot(x1 - x0, y1 - y0);
return (
<mesh
key={i}
position={[(x0 + x1) / 2, height / 2 + (y0 + y1) / 2, -depth / 2 + 0.02]}
rotation={[0, 0, Math.atan2(y1 - y0, x1 - x0)]}
>
<boxGeometry args={[len * 1.15, frameW * 1.4, depth]} />
<meshStandardMaterial color={trimColor} roughness={0.55} metalness={0.15} />
</mesh>
);
})}
{/* Glazed spandrel filling the arch head */}
<mesh position={[0, height / 2 + width * 0.16, 0.01]}>
<planeGeometry args={[width * 0.72, width * 0.5]} />
<meshStandardMaterial
color="#9fc4ef"
emissive="#9fc4ef"
emissiveIntensity={0.6}
toneMapped={false}
transparent
opacity={0.8}
/>
</mesh>
{/* Mullions and transoms */}
{[-width / 4, width / 4].map((ox) => (
<mesh key={ox} position={[ox, 0, -depth / 2 + 0.03]}>
<boxGeometry args={[0.045, height, depth]} />
<meshStandardMaterial color={trimColor} roughness={0.55} metalness={0.15} />
</mesh>
))}
{[height / 3, -height / 6].map((oy) => (
<mesh key={oy} position={[0, oy, -depth / 2 + 0.03]}>
<boxGeometry args={[width, 0.035, depth]} />
<meshStandardMaterial color={trimColor} roughness={0.55} metalness={0.15} />
</mesh>
))}
</>
)}
{style === 'roman-arch' && (
+82 -29
View File
@@ -100,41 +100,94 @@ function BaroqueDetails({ width, depth, halfW, halfD, trim }: Props & { trim: st
);
}
function MedievalDetails({ halfW, halfD }: Pick<Props, 'halfW' | 'halfD'>) {
const torchPositions = useMemo(
() =>
[
[-halfW + 0.2, -halfD * 0.5],
[halfW - 0.2, -halfD * 0.5],
[-halfW + 0.2, halfD * 0.3],
[halfW - 0.2, halfD * 0.3],
] as [number, number][],
[halfW, halfD]
);
function GothicDetails({ width, depth, halfW, halfD, trim }: Props & { trim: string }) {
// Compound piers along the side walls, spaced by bay, each rising into the
// vault springer. Shafts stay flush against the wall (0.16 m proud) so they
// never intrude on the ~0.7 m frame hang margin.
const bays = useMemo(() => {
const spacing = 4.2;
const count = Math.max(2, Math.min(9, Math.floor(depth / spacing)));
const step = depth / (count + 1);
return Array.from({ length: count }, (_, i) => -halfD + step * (i + 1));
}, [depth, halfD]);
const shaftHeight = 3.5;
return (
<group>
{torchPositions.map(([x, z], i) => (
<group key={i} position={[x, 2.2, z]}>
<mesh>
<boxGeometry args={[0.08, 0.35, 0.12]} />
<meshStandardMaterial color="#3a3028" roughness={0.9} />
</mesh>
<pointLight position={[0, 0.15, 0.08]} intensity={0.65} distance={5} color="#ff9830" />
<mesh position={[0, 0.2, 0.06]}>
<sphereGeometry args={[0.06, 8, 8]} />
<meshStandardMaterial color="#ffb040" emissive="#ff8010" emissiveIntensity={0.8} toneMapped={false} />
</mesh>
{bays.map((z, i) => (
<group key={i}>
{[-1, 1].map((side) => (
<group key={side} position={[side * (halfW - 0.09), 0, z]}>
{/* Compound pier: a heavier centre shaft flanked by colonnettes */}
<mesh position={[0, shaftHeight / 2, 0]}>
<cylinderGeometry args={[0.1, 0.115, shaftHeight, 12]} />
<meshStandardMaterial color="#a89e88" roughness={0.78} metalness={0.04} />
</mesh>
{[-0.17, 0.17].map((dz) => (
<mesh key={dz} position={[0, shaftHeight / 2, dz]}>
<cylinderGeometry args={[0.052, 0.06, shaftHeight, 10]} />
<meshStandardMaterial color="#9e9482" roughness={0.82} metalness={0.03} />
</mesh>
))}
{/* Moulded base and foliate capital */}
<mesh position={[0, 0.1, 0]}>
<boxGeometry args={[0.28, 0.2, 0.52]} />
<meshStandardMaterial color="#978d79" roughness={0.85} />
</mesh>
<mesh position={[0, shaftHeight + 0.12, 0]}>
<boxGeometry args={[0.3, 0.24, 0.56]} />
<meshStandardMaterial color="#b3a892" roughness={0.7} metalness={0.05} />
</mesh>
{/* Vault springer resting on the capital */}
<mesh position={[0, shaftHeight + 0.36, 0]} rotation={[0, 0, Math.PI / 4]}>
<boxGeometry args={[0.17, 0.17, 0.46]} />
<meshStandardMaterial color={trim} roughness={0.72} metalness={0.06} />
</mesh>
</group>
))}
{/* Transverse rib arching across the nave between opposite piers */}
<group position={[0, shaftHeight + 0.42, z]}>
{Array.from({ length: 9 }, (_, s) => {
const segs = 9;
const t0 = s / segs;
const t1 = (s + 1) / segs;
const xAt = (t: number) => -halfW + 0.09 + t * (halfW - 0.09) * 2;
const yAt = (t: number) => Math.sin(Math.PI * t) * 0.34;
const x0 = xAt(t0);
const x1 = xAt(t1);
const y0 = yAt(t0);
const y1 = yAt(t1);
const len = Math.hypot(x1 - x0, y1 - y0);
return (
<mesh
key={s}
position={[(x0 + x1) / 2, (y0 + y1) / 2, 0]}
rotation={[0, 0, Math.atan2(y1 - y0, x1 - x0)]}
>
<boxGeometry args={[len * 1.06, 0.13, 0.16]} />
<meshStandardMaterial color="#b0a58f" roughness={0.74} metalness={0.05} />
</mesh>
);
})}
</group>
</group>
))}
{/* Rough stone courses */}
{[-halfD + 0.08, halfD - 0.08].map((z, i) => (
<mesh key={i} position={[0, 1.5, z]}>
<boxGeometry args={[0.04, 3, 0.04]} />
<meshStandardMaterial color="#6a6458" roughness={0.98} />
{/* Moulded string course running the length of both side walls */}
{[-1, 1].map((side) => (
<mesh key={side} position={[side * (halfW - 0.05), 2.62, 0]}>
<boxGeometry args={[0.12, 0.1, depth - 0.2]} />
<meshStandardMaterial color="#b3a892" roughness={0.7} metalness={0.05} />
</mesh>
))}
{/* Chancel-style cornice on the end wall */}
<mesh position={[0, 3.96, -halfD + 0.1]}>
<boxGeometry args={[Math.max(2.4, width - 0.4), 0.16, 0.18]} />
<meshStandardMaterial color="#b0a58f" roughness={0.7} metalness={0.05} />
</mesh>
</group>
);
}
@@ -324,8 +377,8 @@ export default function MovementHallDetails({ style, width, depth, halfW, halfD
return <PalazzoDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
case 'baroque':
return <BaroqueDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
case 'medieval':
return <MedievalDetails halfW={halfW} halfD={halfD} />;
case 'gothic':
return <GothicDetails style={style} width={width} depth={depth} halfW={halfW} halfD={halfD} trim={trim} />;
case 'byzantine':
return <ByzantineDetails halfW={halfW} halfD={halfD} trim={trim} />;
case 'neoclassical':
+46 -6
View File
@@ -14,7 +14,7 @@ import type {
} from '../types';
import { galleryImageUrlCandidates, imageUrl, api } from '../api/client';
import { comparePaintingsChronological, paintingHasCuratorNotes, paintingHasInfluenceLinks, paintingWallCaption } from '../utils/paintingUtils';
import { cloneSurfaceTexture, getSurfaceTexture } from '../utils/galleryProceduralTextures';
import { cloneSurfaceTexture, getSurfaceTexture, type SurfaceTextureKind } from '../utils/galleryProceduralTextures';
import { resolveMovementInteriorStyle, type MovementInteriorStyle, type GalleryWindowSpec } from '../data/movement-interior-styles';
import { useTexturedMaterial } from '../hooks/useTexturedMaterial';
import MovementHallDetails from './MovementHallDetails';
@@ -1059,13 +1059,31 @@ function GalleryWall({
size,
rotation = [0, 0, 0],
color = '#ebe4d8',
kind,
tint,
}: {
position: [number, number, number];
size: [number, number];
rotation?: [number, number, number];
color?: string;
/** When set, the panel is textured like the rest of the hall instead of flat colour. */
kind?: SurfaceTextureKind;
tint?: string;
}) {
const [w, h] = size;
// Door-flanking panels would otherwise read as flat single-colour blocks
// beside fully textured walls.
if (kind) {
return (
<TexturedWall
kind={kind}
tint={tint ?? color}
position={position}
size={[w, h, WALL_THICKNESS]}
rotation={rotation}
/>
);
}
return (
<mesh position={position} rotation={rotation} renderOrder={0}>
<boxGeometry args={[w, h, WALL_THICKNESS]} />
@@ -1563,16 +1581,22 @@ function ArtistHall({
position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]}
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall}
/>
<GalleryWall
position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]}
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall}
/>
<GalleryWall
position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, halfD]}
size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]}
color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall}
/>
<HallPassage
position={[0, 0, halfD - WALL_THICKNESS / 2 - 0.02]}
@@ -1584,12 +1608,20 @@ function ArtistHall({
/>
</>
) : movementMode && endWallHasDoor ? (
<GalleryWall position={[0, WALL_HEIGHT / 2, halfD]} size={[width, WALL_HEIGHT]} color={walls.main} />
<GalleryWall position={[0, WALL_HEIGHT / 2, halfD]} size={[width, WALL_HEIGHT]} color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall} />
) : (
<>
<GalleryWall position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]} size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]} color={walls.main} />
<GalleryWall position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]} size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]} color={walls.main} />
<GalleryWall position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, halfD]} size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]} color={walls.main} />
<GalleryWall position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]} size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]} color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall} />
<GalleryWall position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, halfD]} size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]} color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall} />
<GalleryWall position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, halfD]} size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]} color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall} />
<ExitPortal
position={[0, 0, halfD - WALL_THICKNESS / 2 - 0.02]}
active={nearExit}
@@ -1608,16 +1640,22 @@ function ArtistHall({
position={[-(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, -halfD]}
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall}
/>
<GalleryWall
position={[(DOOR_WIDTH / 2 + (width - DOOR_WIDTH) / 4), WALL_HEIGHT / 2, -halfD]}
size={[(width - DOOR_WIDTH) / 2, WALL_HEIGHT]}
color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall}
/>
<GalleryWall
position={[0, DOOR_HEIGHT + (WALL_HEIGHT - DOOR_HEIGHT) / 2, -halfD]}
size={[DOOR_WIDTH, WALL_HEIGHT - DOOR_HEIGHT]}
color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall}
/>
<group position={[0, 0, -halfD + WALL_THICKNESS / 2 + 0.02]} rotation={[0, Math.PI, 0]}>
<ExitPortal
@@ -1639,7 +1677,9 @@ function ArtistHall({
size={[width, WALL_HEIGHT, WALL_THICKNESS]}
/>
) : (
<GalleryWall position={[0, WALL_HEIGHT / 2, -halfD]} size={[width, WALL_HEIGHT]} color={walls.main} />
<GalleryWall position={[0, WALL_HEIGHT / 2, -halfD]} size={[width, WALL_HEIGHT]} color={walls.main}
kind={interiorStyle?.surfaces.wall}
tint={interiorStyle?.tints.wall} />
)
) : null}