Skip to content

Commit ca837be

Browse files
committed
feat: refactor spacecraft components to enhance structure and introduce tracking markers
1 parent 4ce8c6e commit ca837be

15 files changed

Lines changed: 298 additions & 140 deletions

File tree

rpo-app/src/features/configuration/ConfigurationStep1.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,8 @@ import { SpacecraftPanel } from './SpacecraftPanel';
33
export function ConfigurationStep1() {
44
return (
55
<section className="grid gap-6 md:grid-cols-2">
6-
<SpacecraftPanel vehicle="deputy" />
76
<SpacecraftPanel vehicle="chief" />
7+
<SpacecraftPanel vehicle="deputy" />
88
</section>
99
);
1010
}

rpo-app/src/features/configuration/ConfigurationStep2.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,8 +22,8 @@ export function ConfigurationStep2() {
2222
return (
2323
<section className="flex flex-col gap-4">
2424
<div className="grid gap-6 md:grid-cols-2">
25-
<VehicleStatePanel vehicle="deputy" />
2625
<VehicleStatePanel vehicle="chief" />
26+
<VehicleStatePanel vehicle="deputy" />
2727
</div>
2828
{epochMismatch && chiefVector && deputyVector ? (
2929
<Callout tone="abort">

rpo-app/src/viewport3d/far-field/FarFieldViewport.tsx

Lines changed: 7 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useMemo } from 'react';
12
import { OrbitControls } from '@react-three/drei';
23
import { Canvas } from '@react-three/fiber';
34
import { useShallow } from 'zustand/react/shallow';
@@ -34,15 +35,18 @@ function VehicleMesh({
3435
config: SpacecraftConfig;
3536
vector: StateVectorInput;
3637
}) {
37-
const props = configToSpacecraftProps(config, vehicle);
38-
const position = eciKmToScenePosition(vector.position_eci_km);
38+
const props = useMemo(() => configToSpacecraftProps(config, vehicle), [config, vehicle]);
39+
const position = useMemo(
40+
() => eciKmToScenePosition(vector.position_eci_km),
41+
[vector.position_eci_km],
42+
);
3943
return (
4044
<Spacecraft
4145
{...props}
4246
position={position}
4347
scale={FARFIELD_SPACECRAFT_SCALE}
4448
label={VEHICLE_LABELS[vehicle]}
45-
labelColor={TINTS[vehicle].emissive}
49+
labelColor={TINTS[vehicle].accent}
4650
labelFontSize={FARFIELD_LABEL_FONT_SIZE}
4751
labelOffsetY={FARFIELD_LABEL_OFFSET_Y}
4852
/>

rpo-app/src/viewport3d/proximity/ProximityViewport.tsx

Lines changed: 6 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
import { useMemo } from 'react';
12
import { OrbitControls } from '@react-three/drei';
23
import { Canvas } from '@react-three/fiber';
34

@@ -14,9 +15,9 @@ const DEFAULT_CHIEF = { preset: 'Servicer 500kg' as const, ...PRESETS['Servicer
1415
const DEFAULT_DEPUTY = { preset: 'Servicer 500kg' as const, ...PRESETS['Servicer 500kg'] };
1516

1617
export default function ProximityViewport() {
17-
const chiefProps = configToSpacecraftProps(DEFAULT_CHIEF, 'chief');
18-
const deputyProps = configToSpacecraftProps(DEFAULT_DEPUTY, 'deputy');
19-
const deputyPosition = ricToPosition(DEMO_DEPUTY_OFFSET_RIC_M);
18+
const chiefProps = useMemo(() => configToSpacecraftProps(DEFAULT_CHIEF, 'chief'), []);
19+
const deputyProps = useMemo(() => configToSpacecraftProps(DEFAULT_DEPUTY, 'deputy'), []);
20+
const deputyPosition = useMemo(() => ricToPosition(DEMO_DEPUTY_OFFSET_RIC_M), []);
2021

2122
return (
2223
<div className="h-full w-full">
@@ -34,16 +35,12 @@ export default function ProximityViewport() {
3435
<directionalLight position={[10, 5, 10]} intensity={1.0} />
3536
<Grid />
3637
<RICAxes />
37-
<Spacecraft
38-
{...chiefProps}
39-
label={VEHICLE_LABELS.chief}
40-
labelColor={TINTS.chief.emissive}
41-
/>
38+
<Spacecraft {...chiefProps} label={VEHICLE_LABELS.chief} labelColor={TINTS.chief.accent} />
4239
<Spacecraft
4340
{...deputyProps}
4441
position={deputyPosition}
4542
label={VEHICLE_LABELS.deputy}
46-
labelColor={TINTS.deputy.emissive}
43+
labelColor={TINTS.deputy.accent}
4744
/>
4845
<OrbitControls
4946
enablePan
Lines changed: 82 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,87 @@
1+
import {
2+
ACCENT_STRIPE_HEIGHT,
3+
ACCENT_STRIPE_INTENSITY,
4+
BUS_COLOR,
5+
DOCKING_COLLAR_COLOR,
6+
DOCKING_COLLAR_LENGTH,
7+
DOCKING_COLLAR_RADIUS_FRAC,
8+
THRUSTER_COLOR,
9+
THRUSTER_COUNT,
10+
THRUSTER_NOZZLE_APEX_RADIUS,
11+
THRUSTER_NOZZLE_BASE_RADIUS,
12+
THRUSTER_NOZZLE_LENGTH,
13+
THRUSTER_PLACEMENT_RADIUS_FRAC,
14+
} from '../config/constants';
115
import type { MainBodyProps } from '../types';
216

3-
export function MainBody({
4-
width,
5-
height,
6-
depth,
7-
color,
8-
emissive,
9-
emissiveIntensity = 0.3,
10-
metalness = 0.6,
11-
roughness = 0.4,
12-
}: MainBodyProps) {
17+
const RADIAL_SEGMENTS = 48;
18+
const NOZZLE_SEGMENTS = 20;
19+
const STRIPE_OVERHANG = 0.05;
20+
21+
// cylinderGeometry's native axis is +Y. The outer group rotates +π/2 around X
22+
// so inside the group local +Y maps to world +Z; end-cap positions below use
23+
// local Y for "along the tube axis."
24+
25+
export function MainBody({ width, height, depth, accent }: MainBodyProps) {
26+
const halfWidth = width / 2;
27+
const halfHeight = height / 2;
28+
29+
const busScale = [halfWidth, 1, halfHeight] satisfies [number, number, number];
30+
const stripeScale = [(width + STRIPE_OVERHANG) / 2, 1, (height + STRIPE_OVERHANG) / 2] satisfies [
31+
number,
32+
number,
33+
number,
34+
];
35+
36+
const collarRadiusX = halfWidth * DOCKING_COLLAR_RADIUS_FRAC;
37+
const collarRadiusZ = halfHeight * DOCKING_COLLAR_RADIUS_FRAC;
38+
const collarY = depth / 2 + DOCKING_COLLAR_LENGTH / 2;
39+
40+
const thrusterRadiusX = halfWidth * THRUSTER_PLACEMENT_RADIUS_FRAC;
41+
const thrusterRadiusZ = halfHeight * THRUSTER_PLACEMENT_RADIUS_FRAC;
42+
const thrusterY = -depth / 2 - THRUSTER_NOZZLE_LENGTH / 2;
43+
1344
return (
14-
<mesh>
15-
<boxGeometry args={[width, height, depth]} />
16-
<meshStandardMaterial
17-
color={color}
18-
metalness={metalness}
19-
roughness={roughness}
20-
emissive={emissive}
21-
emissiveIntensity={emissive ? emissiveIntensity : 0}
22-
transparent
23-
opacity={1}
24-
depthWrite={true}
25-
/>
26-
</mesh>
45+
<group rotation={[Math.PI / 2, 0, 0]}>
46+
<mesh scale={busScale}>
47+
<cylinderGeometry args={[1, 1, depth, RADIAL_SEGMENTS]} />
48+
<meshStandardMaterial color={BUS_COLOR} metalness={0.05} roughness={0.75} />
49+
</mesh>
50+
51+
<mesh scale={stripeScale}>
52+
<cylinderGeometry args={[1, 1, ACCENT_STRIPE_HEIGHT, RADIAL_SEGMENTS]} />
53+
<meshStandardMaterial
54+
color={accent}
55+
emissive={accent}
56+
emissiveIntensity={ACCENT_STRIPE_INTENSITY}
57+
metalness={0.1}
58+
roughness={0.6}
59+
/>
60+
</mesh>
61+
62+
<mesh position={[0, collarY, 0]} scale={[collarRadiusX, 1, collarRadiusZ]}>
63+
<cylinderGeometry args={[1, 1, DOCKING_COLLAR_LENGTH, RADIAL_SEGMENTS]} />
64+
<meshStandardMaterial color={DOCKING_COLLAR_COLOR} metalness={0.7} roughness={0.35} />
65+
</mesh>
66+
67+
{Array.from({ length: THRUSTER_COUNT }, (_, i) => {
68+
const angle = (i / THRUSTER_COUNT) * Math.PI * 2;
69+
const x = thrusterRadiusX * Math.cos(angle);
70+
const z = thrusterRadiusZ * Math.sin(angle);
71+
return (
72+
<mesh key={`nozzle-${i}`} position={[x, thrusterY, z]}>
73+
<cylinderGeometry
74+
args={[
75+
THRUSTER_NOZZLE_APEX_RADIUS,
76+
THRUSTER_NOZZLE_BASE_RADIUS,
77+
THRUSTER_NOZZLE_LENGTH,
78+
NOZZLE_SEGMENTS,
79+
]}
80+
/>
81+
<meshStandardMaterial color={THRUSTER_COLOR} metalness={0.5} roughness={0.4} />
82+
</mesh>
83+
);
84+
})}
85+
</group>
2786
);
2887
}

rpo-app/src/viewport3d/spacecraft/components/NavigationLight.tsx

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 74 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import { DoubleSide, ShaderMaterial } from 'three';
2+
3+
const VERTEX_SHADER = /* glsl */ `
4+
varying vec2 vUv;
5+
void main() {
6+
vUv = uv;
7+
gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0);
8+
}
9+
`;
10+
11+
// Unlit on purpose: solar panels in space face direct sun, so we fake the
12+
// dark-silicon + busbar look rather than rely on a scene light that may not
13+
// hit the correct face. fwidth()-based AA keeps gridlines readable from
14+
// thumbnail (~60 px) to proximity zoom.
15+
const FRAGMENT_SHADER = /* glsl */ `
16+
varying vec2 vUv;
17+
18+
const vec3 CELL_COLOR = vec3(0.04, 0.10, 0.20);
19+
const vec3 GRID_COLOR = vec3(0.01, 0.01, 0.01);
20+
const vec3 BUSBAR_COLOR = vec3(0.68, 0.68, 0.72);
21+
const vec3 SUBSTRATE_COLOR = vec3(0.42, 0.42, 0.44);
22+
const vec3 OUTER_GRID_COLOR = vec3(0.05);
23+
const vec2 CELLS = vec2(6.0, 4.0);
24+
const float GRID_WIDTH = 0.04;
25+
const float BUSBAR_WIDTH = 0.018;
26+
const float BUSBAR_MIX = 0.70;
27+
const float OUTER_GRID_WIDTH = 0.012;
28+
29+
void main() {
30+
if (!gl_FrontFacing) {
31+
gl_FragColor = vec4(SUBSTRATE_COLOR, 1.0);
32+
return;
33+
}
34+
35+
vec2 uvCells = vUv * CELLS;
36+
vec2 f = fract(uvCells);
37+
vec2 eps = fwidth(uvCells);
38+
39+
vec2 d = min(f, 1.0 - f);
40+
float gx = 1.0 - smoothstep(GRID_WIDTH - eps.x, GRID_WIDTH + eps.x, d.x);
41+
float gy = 1.0 - smoothstep(GRID_WIDTH - eps.y, GRID_WIDTH + eps.y, d.y);
42+
float grid = max(gx, gy);
43+
44+
float busbarDist = abs(f.y - 0.5);
45+
float busbar = 1.0 - smoothstep(BUSBAR_WIDTH - eps.y, BUSBAR_WIDTH + eps.y, busbarDist);
46+
47+
vec3 col = CELL_COLOR;
48+
col = mix(col, GRID_COLOR, grid);
49+
col = mix(col, BUSBAR_COLOR, busbar * BUSBAR_MIX);
50+
51+
// Sub-panel grid drawn last so tile boundaries mask cells/busbar at the seam.
52+
float distX = abs(vUv.x - 0.5);
53+
float distY = abs(vUv.y - 0.5);
54+
vec2 outerEps = fwidth(vUv);
55+
float outerGx = 1.0 - smoothstep(OUTER_GRID_WIDTH - outerEps.x, OUTER_GRID_WIDTH + outerEps.x, distX);
56+
float outerGy = 1.0 - smoothstep(OUTER_GRID_WIDTH - outerEps.y, OUTER_GRID_WIDTH + outerEps.y, distY);
57+
float outerGrid = max(outerGx, outerGy);
58+
col = mix(col, OUTER_GRID_COLOR, outerGrid);
59+
60+
gl_FragColor = vec4(col, 1.0);
61+
}
62+
`;
63+
64+
// Module-level singleton: no instance state, no props, shared across all
65+
// panels. Avoids GLSL recompile and material diffing on every re-render.
66+
const SOLAR_CELL_MATERIAL = new ShaderMaterial({
67+
vertexShader: VERTEX_SHADER,
68+
fragmentShader: FRAGMENT_SHADER,
69+
side: DoubleSide,
70+
});
71+
72+
export function SolarCellMaterial() {
73+
return <primitive object={SOLAR_CELL_MATERIAL} attach="material" />;
74+
}
Lines changed: 12 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,28 +1,17 @@
11
import type { SolarPanelProps } from '../types';
22

3-
export function SolarPanel({
4-
width,
5-
height,
6-
depth,
7-
position,
8-
rotation = [0, 0, 0],
9-
color = '#d0d0d0',
10-
emissive = '#444444',
11-
emissiveIntensity = 0.2,
12-
}: SolarPanelProps) {
3+
import { SolarCellMaterial } from './SolarCellMaterial';
4+
5+
// The shader handles both plane faces via gl_FrontFacing: +Y = cell grid,
6+
// -Y = honeycomb substrate.
7+
8+
export function SolarPanel({ width, depth, position, rotation = [0, 0, 0] }: SolarPanelProps) {
139
return (
14-
<mesh position={position} rotation={rotation}>
15-
<boxGeometry args={[width, height, depth]} />
16-
<meshStandardMaterial
17-
color={color}
18-
metalness={0.8}
19-
roughness={0.2}
20-
emissive={emissive}
21-
emissiveIntensity={emissiveIntensity}
22-
transparent
23-
opacity={1}
24-
depthWrite={true}
25-
/>
26-
</mesh>
10+
<group position={position} rotation={rotation}>
11+
<mesh rotation={[-Math.PI / 2, 0, 0]}>
12+
<planeGeometry args={[width, depth]} />
13+
<SolarCellMaterial />
14+
</mesh>
15+
</group>
2716
);
2817
}

rpo-app/src/viewport3d/spacecraft/components/Spacecraft.tsx

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -2,9 +2,9 @@ import type { SpacecraftProps } from '../types';
22

33
import { Arm } from './Arm';
44
import { MainBody } from './MainBody';
5-
import { NavigationLight } from './NavigationLight';
65
import { SolarPanel } from './SolarPanel';
76
import { SpacecraftLabel } from './SpacecraftLabel';
7+
import { TrackingMarker } from './TrackingMarker';
88

99
const LABEL_OFFSET_PAD = 8;
1010

@@ -15,7 +15,7 @@ export function Spacecraft({
1515
mainBody,
1616
solarPanels,
1717
arms = [],
18-
navigationLights = [],
18+
trackingMarkers = [],
1919
label,
2020
labelColor,
2121
labelFontSize,
@@ -37,8 +37,8 @@ export function Spacecraft({
3737
<SolarPanel key={`panel-${index}`} {...panel} />
3838
))}
3939

40-
{navigationLights.map((light, index) => (
41-
<NavigationLight key={`light-${index}`} {...light} />
40+
{trackingMarkers.map((marker, index) => (
41+
<TrackingMarker key={`marker-${index}`} {...marker} />
4242
))}
4343
</group>
4444

0 commit comments

Comments
 (0)