@principal-ai/file-city-react 0.5.14 → 0.5.16
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/components/CityViewWithReactFlow.d.ts +1 -1
- package/dist/components/CityViewWithReactFlow.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.d.ts +67 -1
- package/dist/components/FileCity3D/FileCity3D.d.ts.map +1 -1
- package/dist/components/FileCity3D/FileCity3D.js +482 -93
- package/dist/components/FileCity3D/index.d.ts +2 -2
- package/dist/components/FileCity3D/index.d.ts.map +1 -1
- package/dist/components/FileCity3D/index.js +1 -1
- package/package.json +11 -4
- package/src/components/CityViewWithReactFlow.tsx +1 -2
- package/src/components/FileCity3D/FileCity3D.tsx +625 -104
- package/src/components/FileCity3D/index.ts +2 -1
- package/src/stories/CityViewWithReactFlow.stories.tsx +1 -1
- package/src/stories/FileCity3D.stories.tsx +1018 -2
|
@@ -2,6 +2,16 @@ import React from 'react';
|
|
|
2
2
|
import type { Meta, StoryObj } from '@storybook/react';
|
|
3
3
|
import {
|
|
4
4
|
FileCity3D,
|
|
5
|
+
resetCamera,
|
|
6
|
+
rotateCameraTo,
|
|
7
|
+
rotateCameraBy,
|
|
8
|
+
tiltCameraTo,
|
|
9
|
+
tiltCameraBy,
|
|
10
|
+
moveCameraTo,
|
|
11
|
+
setCameraTarget,
|
|
12
|
+
getCameraTarget,
|
|
13
|
+
getCameraAngle,
|
|
14
|
+
getCameraTilt,
|
|
5
15
|
type CityData,
|
|
6
16
|
type CityBuilding,
|
|
7
17
|
type CityDistrict,
|
|
@@ -130,7 +140,7 @@ const sampleCityData: CityData = {
|
|
|
130
140
|
},
|
|
131
141
|
],
|
|
132
142
|
bounds: { minX: -5, maxX: 85, minZ: -5, maxZ: 80 },
|
|
133
|
-
metadata: { totalFiles: 31, totalDirectories: 4, rootPath: '/project' },
|
|
143
|
+
metadata: { totalFiles: 31, totalDirectories: 4, rootPath: '/project', analyzedAt: new Date() },
|
|
134
144
|
};
|
|
135
145
|
|
|
136
146
|
// Large city for stress testing
|
|
@@ -181,6 +191,7 @@ function generateLargeCityData(): CityData {
|
|
|
181
191
|
totalFiles: buildings.length,
|
|
182
192
|
totalDirectories: districts.length,
|
|
183
193
|
rootPath: '/large-project',
|
|
194
|
+
analyzedAt: new Date(),
|
|
184
195
|
},
|
|
185
196
|
};
|
|
186
197
|
}
|
|
@@ -232,6 +243,7 @@ function generateMonorepoCityData(): CityData {
|
|
|
232
243
|
totalFiles: buildings.length,
|
|
233
244
|
totalDirectories: districts.length,
|
|
234
245
|
rootPath: '/monorepo',
|
|
246
|
+
analyzedAt: new Date(),
|
|
235
247
|
},
|
|
236
248
|
};
|
|
237
249
|
}
|
|
@@ -925,7 +937,7 @@ const DirectorySelectionTemplate: React.FC = () => {
|
|
|
925
937
|
}}
|
|
926
938
|
>
|
|
927
939
|
<div style={{ marginBottom: 12, fontSize: 12, color: '#64748b' }}>
|
|
928
|
-
Click a directory to focus (collapse others). Click again or
|
|
940
|
+
Click a directory to focus (collapse others). Click again or "Show All" to reset.
|
|
929
941
|
</div>
|
|
930
942
|
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 8 }}>
|
|
931
943
|
<button
|
|
@@ -985,3 +997,1007 @@ const DirectorySelectionTemplate: React.FC = () => {
|
|
|
985
997
|
export const DirectorySelection: Story = {
|
|
986
998
|
render: () => <DirectorySelectionTemplate />,
|
|
987
999
|
};
|
|
1000
|
+
|
|
1001
|
+
/**
|
|
1002
|
+
* Tour Scenario Tester - Test all combinations of focusDirectory and highlightLayers
|
|
1003
|
+
*
|
|
1004
|
+
* This story allows testing the animation system across all documented scenarios:
|
|
1005
|
+
* - Baseline (no focus, no highlights)
|
|
1006
|
+
* - Focus only
|
|
1007
|
+
* - Highlight only
|
|
1008
|
+
* - Focus + Highlight combinations
|
|
1009
|
+
* - Transitions between states
|
|
1010
|
+
*
|
|
1011
|
+
* See docs/TOUR_TEST_SCENARIOS.md for full documentation.
|
|
1012
|
+
*/
|
|
1013
|
+
interface TestScenario {
|
|
1014
|
+
id: string;
|
|
1015
|
+
name: string;
|
|
1016
|
+
description: string;
|
|
1017
|
+
focusDirectory: string | null;
|
|
1018
|
+
focusColor?: string | null;
|
|
1019
|
+
highlightLayers: HighlightLayer[];
|
|
1020
|
+
isolationMode: IsolationMode;
|
|
1021
|
+
}
|
|
1022
|
+
|
|
1023
|
+
const testScenarios: TestScenario[] = [
|
|
1024
|
+
// Base scenarios
|
|
1025
|
+
{
|
|
1026
|
+
id: 'S1-baseline',
|
|
1027
|
+
name: 'S1: Baseline',
|
|
1028
|
+
description: 'Full city view, no focus, no highlights',
|
|
1029
|
+
focusDirectory: null,
|
|
1030
|
+
highlightLayers: [],
|
|
1031
|
+
isolationMode: 'none',
|
|
1032
|
+
},
|
|
1033
|
+
{
|
|
1034
|
+
id: 'S2-focus-only',
|
|
1035
|
+
name: 'S2: Focus Only (src)',
|
|
1036
|
+
description: 'Camera zooms to src, others collapse, focused area highlighted',
|
|
1037
|
+
focusDirectory: 'auth-server/src',
|
|
1038
|
+
focusColor: '#3b82f6',
|
|
1039
|
+
highlightLayers: [],
|
|
1040
|
+
isolationMode: 'collapse',
|
|
1041
|
+
},
|
|
1042
|
+
{
|
|
1043
|
+
id: 'S2b-focus-only-tests',
|
|
1044
|
+
name: 'S2b: Focus Only (bruno)',
|
|
1045
|
+
description: 'Camera zooms to bruno directory, highlighted in green',
|
|
1046
|
+
focusDirectory: 'auth-server/bruno',
|
|
1047
|
+
focusColor: '#22c55e',
|
|
1048
|
+
highlightLayers: [],
|
|
1049
|
+
isolationMode: 'collapse',
|
|
1050
|
+
},
|
|
1051
|
+
{
|
|
1052
|
+
id: 'S3-highlight-only',
|
|
1053
|
+
name: 'S3: Highlight Only',
|
|
1054
|
+
description: 'Full view with highlight layer, non-highlighted collapse',
|
|
1055
|
+
focusDirectory: null,
|
|
1056
|
+
highlightLayers: [
|
|
1057
|
+
{
|
|
1058
|
+
id: 'api-layer',
|
|
1059
|
+
name: 'API Routes',
|
|
1060
|
+
enabled: true,
|
|
1061
|
+
color: '#22c55e',
|
|
1062
|
+
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
1063
|
+
},
|
|
1064
|
+
],
|
|
1065
|
+
isolationMode: 'collapse',
|
|
1066
|
+
},
|
|
1067
|
+
{
|
|
1068
|
+
id: 'S4-focus-highlight-same',
|
|
1069
|
+
name: 'S4: Focus + Highlight (same directory)',
|
|
1070
|
+
description: 'Focus and highlight on same directory',
|
|
1071
|
+
focusDirectory: 'auth-server/src/app/api',
|
|
1072
|
+
highlightLayers: [
|
|
1073
|
+
{
|
|
1074
|
+
id: 'api-layer',
|
|
1075
|
+
name: 'API Routes',
|
|
1076
|
+
enabled: true,
|
|
1077
|
+
color: '#3b82f6',
|
|
1078
|
+
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
1079
|
+
},
|
|
1080
|
+
],
|
|
1081
|
+
isolationMode: 'collapse',
|
|
1082
|
+
},
|
|
1083
|
+
{
|
|
1084
|
+
id: 'S5-focus-highlight-subset',
|
|
1085
|
+
name: 'S5: Focus + Highlight (subset)',
|
|
1086
|
+
description: 'Focus on src, highlight only components subset',
|
|
1087
|
+
focusDirectory: 'auth-server/src',
|
|
1088
|
+
highlightLayers: [
|
|
1089
|
+
{
|
|
1090
|
+
id: 'lib-layer',
|
|
1091
|
+
name: 'Libraries',
|
|
1092
|
+
enabled: true,
|
|
1093
|
+
color: '#8b5cf6',
|
|
1094
|
+
items: [{ path: 'auth-server/src/lib', type: 'directory' as const }],
|
|
1095
|
+
},
|
|
1096
|
+
],
|
|
1097
|
+
isolationMode: 'collapse',
|
|
1098
|
+
},
|
|
1099
|
+
{
|
|
1100
|
+
id: 'S6-multiple-highlights-focus',
|
|
1101
|
+
name: 'S6: Multiple Highlights (with focus)',
|
|
1102
|
+
description: 'Two highlight layers within focused area',
|
|
1103
|
+
focusDirectory: 'auth-server/src',
|
|
1104
|
+
highlightLayers: [
|
|
1105
|
+
{
|
|
1106
|
+
id: 'api-layer',
|
|
1107
|
+
name: 'API Routes',
|
|
1108
|
+
enabled: true,
|
|
1109
|
+
color: '#22c55e',
|
|
1110
|
+
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
1111
|
+
},
|
|
1112
|
+
{
|
|
1113
|
+
id: 'lib-layer',
|
|
1114
|
+
name: 'Libraries',
|
|
1115
|
+
enabled: true,
|
|
1116
|
+
color: '#f59e0b',
|
|
1117
|
+
items: [{ path: 'auth-server/src/lib', type: 'directory' as const }],
|
|
1118
|
+
},
|
|
1119
|
+
],
|
|
1120
|
+
isolationMode: 'collapse',
|
|
1121
|
+
},
|
|
1122
|
+
{
|
|
1123
|
+
id: 'S7-multiple-highlights-no-focus',
|
|
1124
|
+
name: 'S7: Multiple Highlights (no focus)',
|
|
1125
|
+
description: 'Two highlight layers, non-highlighted collapse',
|
|
1126
|
+
focusDirectory: null,
|
|
1127
|
+
highlightLayers: [
|
|
1128
|
+
{
|
|
1129
|
+
id: 'api-layer',
|
|
1130
|
+
name: 'API Routes',
|
|
1131
|
+
enabled: true,
|
|
1132
|
+
color: '#3b82f6',
|
|
1133
|
+
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
1134
|
+
},
|
|
1135
|
+
{
|
|
1136
|
+
id: 'bruno-layer',
|
|
1137
|
+
name: 'Bruno Tests',
|
|
1138
|
+
enabled: true,
|
|
1139
|
+
color: '#ef4444',
|
|
1140
|
+
items: [{ path: 'auth-server/bruno', type: 'directory' as const }],
|
|
1141
|
+
},
|
|
1142
|
+
],
|
|
1143
|
+
isolationMode: 'collapse',
|
|
1144
|
+
},
|
|
1145
|
+
// Edge cases
|
|
1146
|
+
{
|
|
1147
|
+
id: 'E3-overlapping-highlights',
|
|
1148
|
+
name: 'E3: Overlapping Highlights',
|
|
1149
|
+
description: 'Two layers highlight overlapping paths',
|
|
1150
|
+
focusDirectory: 'auth-server/src',
|
|
1151
|
+
highlightLayers: [
|
|
1152
|
+
{
|
|
1153
|
+
id: 'src-layer',
|
|
1154
|
+
name: 'All Source',
|
|
1155
|
+
enabled: true,
|
|
1156
|
+
color: '#22c55e',
|
|
1157
|
+
items: [{ path: 'auth-server/src', type: 'directory' as const }],
|
|
1158
|
+
},
|
|
1159
|
+
{
|
|
1160
|
+
id: 'api-layer',
|
|
1161
|
+
name: 'API Only',
|
|
1162
|
+
enabled: true,
|
|
1163
|
+
color: '#ef4444',
|
|
1164
|
+
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
1165
|
+
},
|
|
1166
|
+
],
|
|
1167
|
+
isolationMode: 'collapse',
|
|
1168
|
+
},
|
|
1169
|
+
{
|
|
1170
|
+
id: 'E4-focus-inside-highlight',
|
|
1171
|
+
name: 'E4: Focus Inside Highlight',
|
|
1172
|
+
description: 'Focus on subset of highlighted area',
|
|
1173
|
+
focusDirectory: 'auth-server/src/app/api/auth',
|
|
1174
|
+
highlightLayers: [
|
|
1175
|
+
{
|
|
1176
|
+
id: 'api-layer',
|
|
1177
|
+
name: 'All API',
|
|
1178
|
+
enabled: true,
|
|
1179
|
+
color: '#8b5cf6',
|
|
1180
|
+
items: [{ path: 'auth-server/src/app/api', type: 'directory' as const }],
|
|
1181
|
+
},
|
|
1182
|
+
],
|
|
1183
|
+
isolationMode: 'collapse',
|
|
1184
|
+
},
|
|
1185
|
+
];
|
|
1186
|
+
|
|
1187
|
+
const TourScenarioTesterTemplate: React.FC = () => {
|
|
1188
|
+
const [currentScenarioIndex, setCurrentScenarioIndex] = React.useState(0);
|
|
1189
|
+
const [transitionLog, setTransitionLog] = React.useState<string[]>([]);
|
|
1190
|
+
const prevScenarioRef = React.useRef<TestScenario | null>(null);
|
|
1191
|
+
|
|
1192
|
+
const scenario = testScenarios[currentScenarioIndex];
|
|
1193
|
+
|
|
1194
|
+
// Log transitions
|
|
1195
|
+
React.useEffect(() => {
|
|
1196
|
+
const prev = prevScenarioRef.current;
|
|
1197
|
+
if (prev && prev.id !== scenario.id) {
|
|
1198
|
+
const logEntry = `${new Date().toLocaleTimeString()} | ${prev.name} → ${scenario.name}`;
|
|
1199
|
+
setTransitionLog(logs => [...logs.slice(-9), logEntry]);
|
|
1200
|
+
console.log('[TourScenario]', logEntry);
|
|
1201
|
+
console.log(' From:', { focus: prev.focusDirectory, layers: prev.highlightLayers.length });
|
|
1202
|
+
console.log(' To:', {
|
|
1203
|
+
focus: scenario.focusDirectory,
|
|
1204
|
+
layers: scenario.highlightLayers.length,
|
|
1205
|
+
});
|
|
1206
|
+
}
|
|
1207
|
+
prevScenarioRef.current = scenario;
|
|
1208
|
+
}, [scenario]);
|
|
1209
|
+
|
|
1210
|
+
const goToScenario = (index: number) => {
|
|
1211
|
+
if (index >= 0 && index < testScenarios.length) {
|
|
1212
|
+
setCurrentScenarioIndex(index);
|
|
1213
|
+
}
|
|
1214
|
+
};
|
|
1215
|
+
|
|
1216
|
+
return (
|
|
1217
|
+
<div
|
|
1218
|
+
style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}
|
|
1219
|
+
>
|
|
1220
|
+
{/* 3D City */}
|
|
1221
|
+
<FileCity3D
|
|
1222
|
+
cityData={authServerCityData as CityData}
|
|
1223
|
+
height="100%"
|
|
1224
|
+
heightScaling="linear"
|
|
1225
|
+
linearScale={0.5}
|
|
1226
|
+
focusDirectory={scenario.focusDirectory}
|
|
1227
|
+
focusColor={scenario.focusColor}
|
|
1228
|
+
highlightLayers={scenario.highlightLayers}
|
|
1229
|
+
isolationMode={scenario.isolationMode}
|
|
1230
|
+
dimOpacity={0.12}
|
|
1231
|
+
animation={{
|
|
1232
|
+
startFlat: false,
|
|
1233
|
+
staggerDelay: 8,
|
|
1234
|
+
tension: 140,
|
|
1235
|
+
friction: 14,
|
|
1236
|
+
}}
|
|
1237
|
+
showControls={true}
|
|
1238
|
+
/>
|
|
1239
|
+
|
|
1240
|
+
{/* Scenario selector panel */}
|
|
1241
|
+
<div
|
|
1242
|
+
style={{
|
|
1243
|
+
position: 'absolute',
|
|
1244
|
+
top: 16,
|
|
1245
|
+
left: 16,
|
|
1246
|
+
zIndex: 100,
|
|
1247
|
+
background: 'rgba(15, 23, 42, 0.95)',
|
|
1248
|
+
border: '1px solid #334155',
|
|
1249
|
+
borderRadius: 8,
|
|
1250
|
+
padding: 16,
|
|
1251
|
+
color: '#e2e8f0',
|
|
1252
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1253
|
+
maxWidth: 360,
|
|
1254
|
+
}}
|
|
1255
|
+
>
|
|
1256
|
+
<h3 style={{ margin: '0 0 12px', fontSize: 14, fontWeight: 600 }}>
|
|
1257
|
+
Tour Scenario Tester
|
|
1258
|
+
</h3>
|
|
1259
|
+
|
|
1260
|
+
{/* Scenario dropdown */}
|
|
1261
|
+
<select
|
|
1262
|
+
value={currentScenarioIndex}
|
|
1263
|
+
onChange={e => goToScenario(Number(e.target.value))}
|
|
1264
|
+
style={{
|
|
1265
|
+
width: '100%',
|
|
1266
|
+
padding: '8px 12px',
|
|
1267
|
+
background: '#1e293b',
|
|
1268
|
+
border: '1px solid #475569',
|
|
1269
|
+
borderRadius: 6,
|
|
1270
|
+
color: '#e2e8f0',
|
|
1271
|
+
fontSize: 13,
|
|
1272
|
+
marginBottom: 12,
|
|
1273
|
+
}}
|
|
1274
|
+
>
|
|
1275
|
+
{testScenarios.map((s, i) => (
|
|
1276
|
+
<option key={s.id} value={i}>
|
|
1277
|
+
{s.name}
|
|
1278
|
+
</option>
|
|
1279
|
+
))}
|
|
1280
|
+
</select>
|
|
1281
|
+
|
|
1282
|
+
{/* Scenario description */}
|
|
1283
|
+
<p style={{ margin: '0 0 12px', fontSize: 12, color: '#94a3b8' }}>
|
|
1284
|
+
{scenario.description}
|
|
1285
|
+
</p>
|
|
1286
|
+
|
|
1287
|
+
{/* Current state */}
|
|
1288
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>
|
|
1289
|
+
<div style={{ display: 'flex', alignItems: 'center', gap: 4 }}>
|
|
1290
|
+
<strong>focusDirectory:</strong>{' '}
|
|
1291
|
+
<code style={{ color: '#22c55e' }}>{scenario.focusDirectory ?? 'null'}</code>
|
|
1292
|
+
{scenario.focusColor && (
|
|
1293
|
+
<span
|
|
1294
|
+
style={{
|
|
1295
|
+
width: 10,
|
|
1296
|
+
height: 10,
|
|
1297
|
+
borderRadius: 2,
|
|
1298
|
+
background: scenario.focusColor,
|
|
1299
|
+
marginLeft: 4,
|
|
1300
|
+
}}
|
|
1301
|
+
/>
|
|
1302
|
+
)}
|
|
1303
|
+
</div>
|
|
1304
|
+
<div>
|
|
1305
|
+
<strong>highlightLayers:</strong>{' '}
|
|
1306
|
+
<code style={{ color: '#3b82f6' }}>{scenario.highlightLayers.length} layer(s)</code>
|
|
1307
|
+
</div>
|
|
1308
|
+
<div>
|
|
1309
|
+
<strong>isolationMode:</strong>{' '}
|
|
1310
|
+
<code style={{ color: '#f59e0b' }}>{scenario.isolationMode}</code>
|
|
1311
|
+
</div>
|
|
1312
|
+
</div>
|
|
1313
|
+
|
|
1314
|
+
{/* Highlight layer details */}
|
|
1315
|
+
{scenario.highlightLayers.length > 0 && (
|
|
1316
|
+
<div style={{ fontSize: 10, color: '#64748b', marginBottom: 8 }}>
|
|
1317
|
+
{scenario.highlightLayers.map(layer => (
|
|
1318
|
+
<div key={layer.id} style={{ display: 'flex', alignItems: 'center', gap: 6 }}>
|
|
1319
|
+
<span
|
|
1320
|
+
style={{
|
|
1321
|
+
width: 10,
|
|
1322
|
+
height: 10,
|
|
1323
|
+
borderRadius: 2,
|
|
1324
|
+
background: layer.color,
|
|
1325
|
+
}}
|
|
1326
|
+
/>
|
|
1327
|
+
<span>{layer.name}</span>
|
|
1328
|
+
</div>
|
|
1329
|
+
))}
|
|
1330
|
+
</div>
|
|
1331
|
+
)}
|
|
1332
|
+
|
|
1333
|
+
{/* Quick navigation */}
|
|
1334
|
+
<div style={{ display: 'flex', gap: 8, marginTop: 12 }}>
|
|
1335
|
+
<button
|
|
1336
|
+
onClick={() => goToScenario(currentScenarioIndex - 1)}
|
|
1337
|
+
disabled={currentScenarioIndex === 0}
|
|
1338
|
+
style={{
|
|
1339
|
+
flex: 1,
|
|
1340
|
+
padding: '6px 12px',
|
|
1341
|
+
background: currentScenarioIndex === 0 ? '#1e293b' : '#334155',
|
|
1342
|
+
border: '1px solid #475569',
|
|
1343
|
+
borderRadius: 4,
|
|
1344
|
+
color: currentScenarioIndex === 0 ? '#475569' : '#e2e8f0',
|
|
1345
|
+
cursor: currentScenarioIndex === 0 ? 'not-allowed' : 'pointer',
|
|
1346
|
+
fontSize: 12,
|
|
1347
|
+
}}
|
|
1348
|
+
>
|
|
1349
|
+
← Prev
|
|
1350
|
+
</button>
|
|
1351
|
+
<button
|
|
1352
|
+
onClick={() => goToScenario(0)}
|
|
1353
|
+
style={{
|
|
1354
|
+
padding: '6px 12px',
|
|
1355
|
+
background: '#334155',
|
|
1356
|
+
border: '1px solid #475569',
|
|
1357
|
+
borderRadius: 4,
|
|
1358
|
+
color: '#e2e8f0',
|
|
1359
|
+
cursor: 'pointer',
|
|
1360
|
+
fontSize: 12,
|
|
1361
|
+
}}
|
|
1362
|
+
>
|
|
1363
|
+
Reset
|
|
1364
|
+
</button>
|
|
1365
|
+
<button
|
|
1366
|
+
onClick={() => goToScenario(currentScenarioIndex + 1)}
|
|
1367
|
+
disabled={currentScenarioIndex === testScenarios.length - 1}
|
|
1368
|
+
style={{
|
|
1369
|
+
flex: 1,
|
|
1370
|
+
padding: '6px 12px',
|
|
1371
|
+
background:
|
|
1372
|
+
currentScenarioIndex === testScenarios.length - 1 ? '#1e293b' : '#334155',
|
|
1373
|
+
border: '1px solid #475569',
|
|
1374
|
+
borderRadius: 4,
|
|
1375
|
+
color:
|
|
1376
|
+
currentScenarioIndex === testScenarios.length - 1 ? '#475569' : '#e2e8f0',
|
|
1377
|
+
cursor:
|
|
1378
|
+
currentScenarioIndex === testScenarios.length - 1 ? 'not-allowed' : 'pointer',
|
|
1379
|
+
fontSize: 12,
|
|
1380
|
+
}}
|
|
1381
|
+
>
|
|
1382
|
+
Next →
|
|
1383
|
+
</button>
|
|
1384
|
+
</div>
|
|
1385
|
+
</div>
|
|
1386
|
+
|
|
1387
|
+
{/* Transition log */}
|
|
1388
|
+
{transitionLog.length > 0 && (
|
|
1389
|
+
<div
|
|
1390
|
+
style={{
|
|
1391
|
+
position: 'absolute',
|
|
1392
|
+
bottom: 16,
|
|
1393
|
+
left: 16,
|
|
1394
|
+
zIndex: 100,
|
|
1395
|
+
background: 'rgba(15, 23, 42, 0.9)',
|
|
1396
|
+
border: '1px solid #334155',
|
|
1397
|
+
borderRadius: 8,
|
|
1398
|
+
padding: 12,
|
|
1399
|
+
color: '#94a3b8',
|
|
1400
|
+
fontFamily: 'monospace',
|
|
1401
|
+
fontSize: 10,
|
|
1402
|
+
maxWidth: 400,
|
|
1403
|
+
}}
|
|
1404
|
+
>
|
|
1405
|
+
<div style={{ marginBottom: 4, color: '#64748b', fontWeight: 600 }}>
|
|
1406
|
+
Transition Log:
|
|
1407
|
+
</div>
|
|
1408
|
+
{transitionLog.map((log, i) => (
|
|
1409
|
+
<div key={i} style={{ opacity: 0.5 + (i / transitionLog.length) * 0.5 }}>
|
|
1410
|
+
{log}
|
|
1411
|
+
</div>
|
|
1412
|
+
))}
|
|
1413
|
+
</div>
|
|
1414
|
+
)}
|
|
1415
|
+
|
|
1416
|
+
{/* Scenario indicator pills */}
|
|
1417
|
+
<div
|
|
1418
|
+
style={{
|
|
1419
|
+
position: 'absolute',
|
|
1420
|
+
top: 16,
|
|
1421
|
+
right: 16,
|
|
1422
|
+
zIndex: 100,
|
|
1423
|
+
display: 'flex',
|
|
1424
|
+
gap: 4,
|
|
1425
|
+
}}
|
|
1426
|
+
>
|
|
1427
|
+
{testScenarios.map((s, i) => (
|
|
1428
|
+
<button
|
|
1429
|
+
key={s.id}
|
|
1430
|
+
onClick={() => goToScenario(i)}
|
|
1431
|
+
title={s.name}
|
|
1432
|
+
style={{
|
|
1433
|
+
width: i === currentScenarioIndex ? 24 : 8,
|
|
1434
|
+
height: 8,
|
|
1435
|
+
borderRadius: 4,
|
|
1436
|
+
border: 'none',
|
|
1437
|
+
background: i === currentScenarioIndex ? '#3b82f6' : '#475569',
|
|
1438
|
+
cursor: 'pointer',
|
|
1439
|
+
transition: 'all 0.2s',
|
|
1440
|
+
}}
|
|
1441
|
+
/>
|
|
1442
|
+
))}
|
|
1443
|
+
</div>
|
|
1444
|
+
</div>
|
|
1445
|
+
);
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
export const TourScenarioTester: Story = {
|
|
1449
|
+
render: () => <TourScenarioTesterTemplate />,
|
|
1450
|
+
parameters: {
|
|
1451
|
+
docs: {
|
|
1452
|
+
description: {
|
|
1453
|
+
story:
|
|
1454
|
+
'Interactive tester for all tour scenarios. See docs/TOUR_TEST_SCENARIOS.md for documentation.',
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
1457
|
+
},
|
|
1458
|
+
};
|
|
1459
|
+
|
|
1460
|
+
/**
|
|
1461
|
+
* Camera Controls - Test programmatic camera rotation and movement
|
|
1462
|
+
*/
|
|
1463
|
+
const CameraControlsTemplate: React.FC = () => {
|
|
1464
|
+
const [currentAngle, setCurrentAngle] = React.useState<number | null>(null);
|
|
1465
|
+
const [currentTilt, setCurrentTilt] = React.useState<number | null>(null);
|
|
1466
|
+
const [currentTarget, setCurrentTarget] = React.useState<{ x: number; y: number; z: number } | null>(null);
|
|
1467
|
+
const [customAngle, setCustomAngle] = React.useState(0);
|
|
1468
|
+
const [duration, setDuration] = React.useState<number | undefined>(undefined);
|
|
1469
|
+
|
|
1470
|
+
// Update angle, tilt, and target display periodically
|
|
1471
|
+
React.useEffect(() => {
|
|
1472
|
+
const interval = setInterval(() => {
|
|
1473
|
+
const angle = getCameraAngle();
|
|
1474
|
+
const tilt = getCameraTilt();
|
|
1475
|
+
const target = getCameraTarget();
|
|
1476
|
+
if (angle !== null) {
|
|
1477
|
+
setCurrentAngle(Math.round(angle));
|
|
1478
|
+
}
|
|
1479
|
+
if (tilt !== null) {
|
|
1480
|
+
setCurrentTilt(Math.round(tilt));
|
|
1481
|
+
}
|
|
1482
|
+
if (target !== null) {
|
|
1483
|
+
setCurrentTarget({
|
|
1484
|
+
x: Math.round(target.x),
|
|
1485
|
+
y: Math.round(target.y),
|
|
1486
|
+
z: Math.round(target.z),
|
|
1487
|
+
});
|
|
1488
|
+
}
|
|
1489
|
+
}, 100);
|
|
1490
|
+
return () => clearInterval(interval);
|
|
1491
|
+
}, []);
|
|
1492
|
+
|
|
1493
|
+
// Get rotation options based on current duration setting
|
|
1494
|
+
const getRotateOptions = () => duration ? { duration } : undefined;
|
|
1495
|
+
|
|
1496
|
+
const buttonStyle = {
|
|
1497
|
+
padding: '10px 16px',
|
|
1498
|
+
background: '#334155',
|
|
1499
|
+
border: '1px solid #475569',
|
|
1500
|
+
borderRadius: 6,
|
|
1501
|
+
color: '#e2e8f0',
|
|
1502
|
+
cursor: 'pointer',
|
|
1503
|
+
fontSize: 13,
|
|
1504
|
+
fontWeight: 500 as const,
|
|
1505
|
+
minWidth: 80,
|
|
1506
|
+
};
|
|
1507
|
+
|
|
1508
|
+
const directionButtonStyle = {
|
|
1509
|
+
...buttonStyle,
|
|
1510
|
+
width: 60,
|
|
1511
|
+
height: 60,
|
|
1512
|
+
display: 'flex',
|
|
1513
|
+
alignItems: 'center',
|
|
1514
|
+
justifyContent: 'center',
|
|
1515
|
+
flexDirection: 'column' as const,
|
|
1516
|
+
gap: 2,
|
|
1517
|
+
};
|
|
1518
|
+
|
|
1519
|
+
return (
|
|
1520
|
+
<div
|
|
1521
|
+
style={{ height: '100vh', display: 'flex', flexDirection: 'column', position: 'relative' }}
|
|
1522
|
+
>
|
|
1523
|
+
{/* 3D City */}
|
|
1524
|
+
<FileCity3D
|
|
1525
|
+
cityData={authServerCityData as CityData}
|
|
1526
|
+
height="100%"
|
|
1527
|
+
heightScaling="linear"
|
|
1528
|
+
linearScale={0.5}
|
|
1529
|
+
animation={{
|
|
1530
|
+
startFlat: true,
|
|
1531
|
+
autoStartDelay: 600,
|
|
1532
|
+
staggerDelay: 8,
|
|
1533
|
+
tension: 140,
|
|
1534
|
+
friction: 14,
|
|
1535
|
+
}}
|
|
1536
|
+
showControls={false}
|
|
1537
|
+
/>
|
|
1538
|
+
|
|
1539
|
+
{/* Camera controls panel */}
|
|
1540
|
+
<div
|
|
1541
|
+
style={{
|
|
1542
|
+
position: 'absolute',
|
|
1543
|
+
top: 16,
|
|
1544
|
+
left: 16,
|
|
1545
|
+
zIndex: 100,
|
|
1546
|
+
background: 'rgba(15, 23, 42, 0.95)',
|
|
1547
|
+
border: '1px solid #334155',
|
|
1548
|
+
borderRadius: 8,
|
|
1549
|
+
padding: 16,
|
|
1550
|
+
color: '#e2e8f0',
|
|
1551
|
+
fontFamily: 'system-ui, sans-serif',
|
|
1552
|
+
minWidth: 280,
|
|
1553
|
+
}}
|
|
1554
|
+
>
|
|
1555
|
+
<h3 style={{ margin: '0 0 16px', fontSize: 14, fontWeight: 600 }}>
|
|
1556
|
+
Camera Controls
|
|
1557
|
+
</h3>
|
|
1558
|
+
|
|
1559
|
+
{/* Current angle and tilt display */}
|
|
1560
|
+
<div
|
|
1561
|
+
style={{
|
|
1562
|
+
display: 'flex',
|
|
1563
|
+
gap: 8,
|
|
1564
|
+
marginBottom: 16,
|
|
1565
|
+
}}
|
|
1566
|
+
>
|
|
1567
|
+
<div
|
|
1568
|
+
style={{
|
|
1569
|
+
flex: 1,
|
|
1570
|
+
background: '#1e293b',
|
|
1571
|
+
padding: '8px 12px',
|
|
1572
|
+
borderRadius: 6,
|
|
1573
|
+
textAlign: 'center',
|
|
1574
|
+
}}
|
|
1575
|
+
>
|
|
1576
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 4 }}>Rotation</div>
|
|
1577
|
+
<div style={{ fontSize: 20, fontWeight: 600, color: '#3b82f6' }}>
|
|
1578
|
+
{currentAngle !== null ? `${currentAngle}°` : '—'}
|
|
1579
|
+
</div>
|
|
1580
|
+
</div>
|
|
1581
|
+
<div
|
|
1582
|
+
style={{
|
|
1583
|
+
flex: 1,
|
|
1584
|
+
background: '#1e293b',
|
|
1585
|
+
padding: '8px 12px',
|
|
1586
|
+
borderRadius: 6,
|
|
1587
|
+
textAlign: 'center',
|
|
1588
|
+
}}
|
|
1589
|
+
>
|
|
1590
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 4 }}>Tilt</div>
|
|
1591
|
+
<div style={{ fontSize: 20, fontWeight: 600, color: '#22c55e' }}>
|
|
1592
|
+
{currentTilt !== null ? `${currentTilt}°` : '—'}
|
|
1593
|
+
</div>
|
|
1594
|
+
</div>
|
|
1595
|
+
</div>
|
|
1596
|
+
|
|
1597
|
+
{/* Duration control */}
|
|
1598
|
+
<div style={{ marginBottom: 16 }}>
|
|
1599
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>
|
|
1600
|
+
Animation Duration: {duration ? `${duration}ms` : 'Spring (default)'}
|
|
1601
|
+
</div>
|
|
1602
|
+
<div style={{ display: 'flex', gap: 6, flexWrap: 'wrap' }}>
|
|
1603
|
+
{[undefined, 500, 1000, 2000, 3000, 5000].map((d) => (
|
|
1604
|
+
<button
|
|
1605
|
+
key={d ?? 'spring'}
|
|
1606
|
+
onClick={() => setDuration(d)}
|
|
1607
|
+
style={{
|
|
1608
|
+
padding: '6px 10px',
|
|
1609
|
+
background: duration === d ? '#3b82f6' : '#1e293b',
|
|
1610
|
+
border: '1px solid #475569',
|
|
1611
|
+
borderRadius: 4,
|
|
1612
|
+
color: duration === d ? '#ffffff' : '#94a3b8',
|
|
1613
|
+
cursor: 'pointer',
|
|
1614
|
+
fontSize: 11,
|
|
1615
|
+
}}
|
|
1616
|
+
>
|
|
1617
|
+
{d ? `${d}ms` : 'Spring'}
|
|
1618
|
+
</button>
|
|
1619
|
+
))}
|
|
1620
|
+
</div>
|
|
1621
|
+
</div>
|
|
1622
|
+
|
|
1623
|
+
{/* Cardinal direction buttons */}
|
|
1624
|
+
<div style={{ marginBottom: 16 }}>
|
|
1625
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>Cardinal Directions</div>
|
|
1626
|
+
<div
|
|
1627
|
+
style={{
|
|
1628
|
+
display: 'grid',
|
|
1629
|
+
gridTemplateColumns: 'repeat(3, 60px)',
|
|
1630
|
+
gridTemplateRows: 'repeat(3, 60px)',
|
|
1631
|
+
gap: 4,
|
|
1632
|
+
justifyContent: 'center',
|
|
1633
|
+
}}
|
|
1634
|
+
>
|
|
1635
|
+
<div /> {/* Empty top-left */}
|
|
1636
|
+
<button
|
|
1637
|
+
onClick={() => rotateCameraTo('north', getRotateOptions())}
|
|
1638
|
+
style={directionButtonStyle}
|
|
1639
|
+
title="North (180°)"
|
|
1640
|
+
>
|
|
1641
|
+
<span style={{ fontSize: 16 }}>N</span>
|
|
1642
|
+
<span style={{ fontSize: 9, color: '#64748b' }}>180°</span>
|
|
1643
|
+
</button>
|
|
1644
|
+
<div /> {/* Empty top-right */}
|
|
1645
|
+
|
|
1646
|
+
<button
|
|
1647
|
+
onClick={() => rotateCameraTo('west', getRotateOptions())}
|
|
1648
|
+
style={directionButtonStyle}
|
|
1649
|
+
title="West (90°)"
|
|
1650
|
+
>
|
|
1651
|
+
<span style={{ fontSize: 16 }}>W</span>
|
|
1652
|
+
<span style={{ fontSize: 9, color: '#64748b' }}>90°</span>
|
|
1653
|
+
</button>
|
|
1654
|
+
<button
|
|
1655
|
+
onClick={() => resetCamera()}
|
|
1656
|
+
style={{
|
|
1657
|
+
...directionButtonStyle,
|
|
1658
|
+
background: '#1e293b',
|
|
1659
|
+
}}
|
|
1660
|
+
title="Reset Camera"
|
|
1661
|
+
>
|
|
1662
|
+
<span style={{ fontSize: 12 }}>Reset</span>
|
|
1663
|
+
</button>
|
|
1664
|
+
<button
|
|
1665
|
+
onClick={() => rotateCameraTo('east', getRotateOptions())}
|
|
1666
|
+
style={directionButtonStyle}
|
|
1667
|
+
title="East (270°)"
|
|
1668
|
+
>
|
|
1669
|
+
<span style={{ fontSize: 16 }}>E</span>
|
|
1670
|
+
<span style={{ fontSize: 9, color: '#64748b' }}>270°</span>
|
|
1671
|
+
</button>
|
|
1672
|
+
|
|
1673
|
+
<div /> {/* Empty bottom-left */}
|
|
1674
|
+
<button
|
|
1675
|
+
onClick={() => rotateCameraTo('south', getRotateOptions())}
|
|
1676
|
+
style={directionButtonStyle}
|
|
1677
|
+
title="South (0°)"
|
|
1678
|
+
>
|
|
1679
|
+
<span style={{ fontSize: 16 }}>S</span>
|
|
1680
|
+
<span style={{ fontSize: 9, color: '#64748b' }}>0°</span>
|
|
1681
|
+
</button>
|
|
1682
|
+
<div /> {/* Empty bottom-right */}
|
|
1683
|
+
</div>
|
|
1684
|
+
</div>
|
|
1685
|
+
|
|
1686
|
+
{/* Custom angle input */}
|
|
1687
|
+
<div style={{ marginBottom: 16 }}>
|
|
1688
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>Custom Angle</div>
|
|
1689
|
+
<div style={{ display: 'flex', gap: 8 }}>
|
|
1690
|
+
<input
|
|
1691
|
+
type="range"
|
|
1692
|
+
min={0}
|
|
1693
|
+
max={360}
|
|
1694
|
+
value={customAngle}
|
|
1695
|
+
onChange={e => setCustomAngle(Number(e.target.value))}
|
|
1696
|
+
style={{ flex: 1 }}
|
|
1697
|
+
/>
|
|
1698
|
+
<input
|
|
1699
|
+
type="number"
|
|
1700
|
+
min={0}
|
|
1701
|
+
max={360}
|
|
1702
|
+
value={customAngle}
|
|
1703
|
+
onChange={e => setCustomAngle(Number(e.target.value))}
|
|
1704
|
+
style={{
|
|
1705
|
+
width: 60,
|
|
1706
|
+
padding: '4px 8px',
|
|
1707
|
+
background: '#1e293b',
|
|
1708
|
+
border: '1px solid #475569',
|
|
1709
|
+
borderRadius: 4,
|
|
1710
|
+
color: '#e2e8f0',
|
|
1711
|
+
fontSize: 13,
|
|
1712
|
+
textAlign: 'center',
|
|
1713
|
+
}}
|
|
1714
|
+
/>
|
|
1715
|
+
<button
|
|
1716
|
+
onClick={() => rotateCameraTo(customAngle, getRotateOptions())}
|
|
1717
|
+
style={{ ...buttonStyle, minWidth: 50 }}
|
|
1718
|
+
>
|
|
1719
|
+
Go
|
|
1720
|
+
</button>
|
|
1721
|
+
</div>
|
|
1722
|
+
</div>
|
|
1723
|
+
|
|
1724
|
+
{/* Quick angle presets */}
|
|
1725
|
+
<div style={{ marginBottom: 16 }}>
|
|
1726
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>Quick Angles (shortest path)</div>
|
|
1727
|
+
<div style={{ display: 'flex', flexWrap: 'wrap', gap: 6 }}>
|
|
1728
|
+
{[0, 45, 90, 135, 180, 225, 270, 315].map(angle => (
|
|
1729
|
+
<button
|
|
1730
|
+
key={angle}
|
|
1731
|
+
onClick={() => rotateCameraTo(angle, getRotateOptions())}
|
|
1732
|
+
style={{
|
|
1733
|
+
padding: '6px 10px',
|
|
1734
|
+
background: '#1e293b',
|
|
1735
|
+
border: '1px solid #475569',
|
|
1736
|
+
borderRadius: 4,
|
|
1737
|
+
color: '#94a3b8',
|
|
1738
|
+
cursor: 'pointer',
|
|
1739
|
+
fontSize: 11,
|
|
1740
|
+
}}
|
|
1741
|
+
>
|
|
1742
|
+
{angle}°
|
|
1743
|
+
</button>
|
|
1744
|
+
))}
|
|
1745
|
+
</div>
|
|
1746
|
+
</div>
|
|
1747
|
+
|
|
1748
|
+
{/* Relative rotation */}
|
|
1749
|
+
<div style={{ marginBottom: 16 }}>
|
|
1750
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>Rotate (horizontal arc)</div>
|
|
1751
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
1752
|
+
<button
|
|
1753
|
+
onClick={() => rotateCameraBy(-90, getRotateOptions())}
|
|
1754
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1755
|
+
title="Counter-clockwise 90°"
|
|
1756
|
+
>
|
|
1757
|
+
-90° CCW
|
|
1758
|
+
</button>
|
|
1759
|
+
<button
|
|
1760
|
+
onClick={() => rotateCameraBy(-45, getRotateOptions())}
|
|
1761
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1762
|
+
title="Counter-clockwise 45°"
|
|
1763
|
+
>
|
|
1764
|
+
-45°
|
|
1765
|
+
</button>
|
|
1766
|
+
<button
|
|
1767
|
+
onClick={() => rotateCameraBy(45, getRotateOptions())}
|
|
1768
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1769
|
+
title="Clockwise 45°"
|
|
1770
|
+
>
|
|
1771
|
+
+45°
|
|
1772
|
+
</button>
|
|
1773
|
+
<button
|
|
1774
|
+
onClick={() => rotateCameraBy(90, getRotateOptions())}
|
|
1775
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1776
|
+
title="Clockwise 90°"
|
|
1777
|
+
>
|
|
1778
|
+
+90° CW
|
|
1779
|
+
</button>
|
|
1780
|
+
</div>
|
|
1781
|
+
<div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
|
|
1782
|
+
<button
|
|
1783
|
+
onClick={() => rotateCameraBy(-180, getRotateOptions())}
|
|
1784
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1785
|
+
title="Counter-clockwise 180°"
|
|
1786
|
+
>
|
|
1787
|
+
-180° CCW
|
|
1788
|
+
</button>
|
|
1789
|
+
<button
|
|
1790
|
+
onClick={() => rotateCameraBy(180, getRotateOptions())}
|
|
1791
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1792
|
+
title="Clockwise 180°"
|
|
1793
|
+
>
|
|
1794
|
+
+180° CW
|
|
1795
|
+
</button>
|
|
1796
|
+
<button
|
|
1797
|
+
onClick={() => rotateCameraBy(360, getRotateOptions())}
|
|
1798
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1799
|
+
title="Full rotation clockwise"
|
|
1800
|
+
>
|
|
1801
|
+
+360° Full
|
|
1802
|
+
</button>
|
|
1803
|
+
</div>
|
|
1804
|
+
</div>
|
|
1805
|
+
|
|
1806
|
+
{/* Tilt presets */}
|
|
1807
|
+
<div style={{ marginBottom: 16 }}>
|
|
1808
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>Tilt Presets (vertical arc)</div>
|
|
1809
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
1810
|
+
<button
|
|
1811
|
+
onClick={() => tiltCameraTo('top', getRotateOptions())}
|
|
1812
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1813
|
+
title="Top-down view (15°)"
|
|
1814
|
+
>
|
|
1815
|
+
Top (15°)
|
|
1816
|
+
</button>
|
|
1817
|
+
<button
|
|
1818
|
+
onClick={() => tiltCameraTo('high', getRotateOptions())}
|
|
1819
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1820
|
+
title="High angle (35°)"
|
|
1821
|
+
>
|
|
1822
|
+
High (35°)
|
|
1823
|
+
</button>
|
|
1824
|
+
<button
|
|
1825
|
+
onClick={() => tiltCameraTo('low', getRotateOptions())}
|
|
1826
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1827
|
+
title="Low angle (60°)"
|
|
1828
|
+
>
|
|
1829
|
+
Low (60°)
|
|
1830
|
+
</button>
|
|
1831
|
+
<button
|
|
1832
|
+
onClick={() => tiltCameraTo('level', getRotateOptions())}
|
|
1833
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1834
|
+
title="Level view (80°)"
|
|
1835
|
+
>
|
|
1836
|
+
Level (80°)
|
|
1837
|
+
</button>
|
|
1838
|
+
</div>
|
|
1839
|
+
</div>
|
|
1840
|
+
|
|
1841
|
+
{/* Relative tilt */}
|
|
1842
|
+
<div style={{ marginBottom: 16 }}>
|
|
1843
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>Tilt By (+ = down, - = up)</div>
|
|
1844
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
1845
|
+
<button
|
|
1846
|
+
onClick={() => tiltCameraBy(-30, getRotateOptions())}
|
|
1847
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1848
|
+
title="Tilt up 30°"
|
|
1849
|
+
>
|
|
1850
|
+
-30° Up
|
|
1851
|
+
</button>
|
|
1852
|
+
<button
|
|
1853
|
+
onClick={() => tiltCameraBy(-15, getRotateOptions())}
|
|
1854
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1855
|
+
title="Tilt up 15°"
|
|
1856
|
+
>
|
|
1857
|
+
-15°
|
|
1858
|
+
</button>
|
|
1859
|
+
<button
|
|
1860
|
+
onClick={() => tiltCameraBy(15, getRotateOptions())}
|
|
1861
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1862
|
+
title="Tilt down 15°"
|
|
1863
|
+
>
|
|
1864
|
+
+15°
|
|
1865
|
+
</button>
|
|
1866
|
+
<button
|
|
1867
|
+
onClick={() => tiltCameraBy(30, getRotateOptions())}
|
|
1868
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1869
|
+
title="Tilt down 30°"
|
|
1870
|
+
>
|
|
1871
|
+
+30° Down
|
|
1872
|
+
</button>
|
|
1873
|
+
</div>
|
|
1874
|
+
</div>
|
|
1875
|
+
|
|
1876
|
+
{/* Camera target */}
|
|
1877
|
+
<div style={{ marginBottom: 16 }}>
|
|
1878
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>
|
|
1879
|
+
Camera Target: {currentTarget ? `(${currentTarget.x}, ${currentTarget.y}, ${currentTarget.z})` : '—'}
|
|
1880
|
+
</div>
|
|
1881
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
1882
|
+
<button
|
|
1883
|
+
onClick={() => setCameraTarget(0, 0, 0, getRotateOptions())}
|
|
1884
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1885
|
+
title="Look at center"
|
|
1886
|
+
>
|
|
1887
|
+
Center
|
|
1888
|
+
</button>
|
|
1889
|
+
<button
|
|
1890
|
+
onClick={() => setCameraTarget(-40, 0, -40, getRotateOptions())}
|
|
1891
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1892
|
+
title="Look at top-left area"
|
|
1893
|
+
>
|
|
1894
|
+
Top-Left
|
|
1895
|
+
</button>
|
|
1896
|
+
<button
|
|
1897
|
+
onClick={() => setCameraTarget(40, 0, 40, getRotateOptions())}
|
|
1898
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1899
|
+
title="Look at bottom-right area"
|
|
1900
|
+
>
|
|
1901
|
+
Bottom-Right
|
|
1902
|
+
</button>
|
|
1903
|
+
</div>
|
|
1904
|
+
<div style={{ display: 'flex', gap: 6, marginTop: 6 }}>
|
|
1905
|
+
<button
|
|
1906
|
+
onClick={() => setCameraTarget(20, 0, 0, getRotateOptions())}
|
|
1907
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1908
|
+
title="Look at right side"
|
|
1909
|
+
>
|
|
1910
|
+
Right (+X)
|
|
1911
|
+
</button>
|
|
1912
|
+
<button
|
|
1913
|
+
onClick={() => setCameraTarget(-20, 0, 0, getRotateOptions())}
|
|
1914
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1915
|
+
title="Look at left side"
|
|
1916
|
+
>
|
|
1917
|
+
Left (-X)
|
|
1918
|
+
</button>
|
|
1919
|
+
<button
|
|
1920
|
+
onClick={() => setCameraTarget(0, 0, 30, getRotateOptions())}
|
|
1921
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1922
|
+
title="Look at front"
|
|
1923
|
+
>
|
|
1924
|
+
Front (+Z)
|
|
1925
|
+
</button>
|
|
1926
|
+
</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
|
|
1929
|
+
{/* Move to coordinates */}
|
|
1930
|
+
<div>
|
|
1931
|
+
<div style={{ fontSize: 11, color: '#64748b', marginBottom: 8 }}>Move to Position</div>
|
|
1932
|
+
<div style={{ display: 'flex', gap: 6 }}>
|
|
1933
|
+
<button
|
|
1934
|
+
onClick={() => moveCameraTo(0, 0, 50)}
|
|
1935
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1936
|
+
>
|
|
1937
|
+
Center
|
|
1938
|
+
</button>
|
|
1939
|
+
<button
|
|
1940
|
+
onClick={() => moveCameraTo(-50, -50, 40)}
|
|
1941
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1942
|
+
>
|
|
1943
|
+
Top-Left
|
|
1944
|
+
</button>
|
|
1945
|
+
<button
|
|
1946
|
+
onClick={() => moveCameraTo(50, 50, 40)}
|
|
1947
|
+
style={{ ...buttonStyle, flex: 1, fontSize: 11 }}
|
|
1948
|
+
>
|
|
1949
|
+
Bottom-Right
|
|
1950
|
+
</button>
|
|
1951
|
+
</div>
|
|
1952
|
+
</div>
|
|
1953
|
+
</div>
|
|
1954
|
+
|
|
1955
|
+
{/* Angle reference */}
|
|
1956
|
+
<div
|
|
1957
|
+
style={{
|
|
1958
|
+
position: 'absolute',
|
|
1959
|
+
bottom: 16,
|
|
1960
|
+
right: 16,
|
|
1961
|
+
zIndex: 100,
|
|
1962
|
+
background: 'rgba(15, 23, 42, 0.9)',
|
|
1963
|
+
border: '1px solid #334155',
|
|
1964
|
+
borderRadius: 8,
|
|
1965
|
+
padding: 12,
|
|
1966
|
+
color: '#94a3b8',
|
|
1967
|
+
fontFamily: 'monospace',
|
|
1968
|
+
fontSize: 11,
|
|
1969
|
+
maxWidth: 200,
|
|
1970
|
+
}}
|
|
1971
|
+
>
|
|
1972
|
+
<div style={{ fontWeight: 600, marginBottom: 8, color: '#3b82f6' }}>Rotation (horizontal)</div>
|
|
1973
|
+
<div>0° = South (default)</div>
|
|
1974
|
+
<div>90° = West</div>
|
|
1975
|
+
<div>180° = North</div>
|
|
1976
|
+
<div>270° = East</div>
|
|
1977
|
+
<div style={{ marginTop: 8, borderTop: '1px solid #334155', paddingTop: 8 }}>
|
|
1978
|
+
<div style={{ fontWeight: 600, marginBottom: 4, color: '#22c55e' }}>Tilt (vertical)</div>
|
|
1979
|
+
<div>0° = Top-down</div>
|
|
1980
|
+
<div>45° = Diagonal</div>
|
|
1981
|
+
<div>90° = Level/Horizontal</div>
|
|
1982
|
+
</div>
|
|
1983
|
+
<div style={{ marginTop: 8, borderTop: '1px solid #334155', paddingTop: 8 }}>
|
|
1984
|
+
<div style={{ color: '#e2e8f0', marginBottom: 4 }}>Direction</div>
|
|
1985
|
+
<div>Rotate: + = CW, - = CCW</div>
|
|
1986
|
+
<div>Tilt: + = down, - = up</div>
|
|
1987
|
+
</div>
|
|
1988
|
+
</div>
|
|
1989
|
+
</div>
|
|
1990
|
+
);
|
|
1991
|
+
};
|
|
1992
|
+
|
|
1993
|
+
export const CameraControls: Story = {
|
|
1994
|
+
render: () => <CameraControlsTemplate />,
|
|
1995
|
+
parameters: {
|
|
1996
|
+
docs: {
|
|
1997
|
+
description: {
|
|
1998
|
+
story:
|
|
1999
|
+
'Test programmatic camera rotation and movement. Use cardinal direction buttons, custom angles, or position buttons to control the camera.',
|
|
2000
|
+
},
|
|
2001
|
+
},
|
|
2002
|
+
},
|
|
2003
|
+
};
|