nightshade-editor 0.28.0

Interactive map editor for the Nightshade game engine
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
#[cfg(not(target_arch = "wasm32"))]
use crate::systems::kenney_assets::KenneyBrowser;
use crate::systems::loading::PendingImport;
use crate::systems::polyhaven::PolyhavenBrowser;
use crate::systems::sample_assets::SampleBrowser;
#[cfg(not(target_arch = "wasm32"))]
use crate::systems::sketchfab::SketchfabBrowser;
use nightshade::ecs::scene::{AssetUuid, Scene};
use nightshade::prelude::{Atmosphere, Entity, SharedTextureQueue, Vec2, Vec3};
use std::collections::HashMap;

#[derive(Clone, Copy)]
pub struct ThumbnailSlot {
    pub layer: u32,
    pub uv_min: Vec2,
    pub uv_max: Vec2,
    pub aspect: Vec2,
}

#[derive(Default)]
pub struct PrefabInstanceLinks {
    /// Live entity → original uuid inside the source `.nsprefab`. Set
    /// when an instance is spawned and used by refresh-from-source to
    /// pair live entities with their source `SceneEntity` for override
    /// diffing.
    pub entity_to_source: HashMap<Entity, AssetUuid>,
    /// In-memory copy of every prefab `Scene` the user has imported in
    /// this session, keyed by `header.prefab_id`. Refresh-from-source
    /// uses this as the baseline so the editor doesn't need to touch
    /// disk again after the initial import.
    pub imported_scenes: HashMap<AssetUuid, Scene>,
}

pub struct LoadingState {
    pub model_entities: Vec<Entity>,
    /// Roots spawned by the dev tools menu (lines, mesh stress test,
    /// 3D text, text lattice). Tracked separately from model_entities
    /// so `clear_scene` can drop them without affecting the loaded
    /// glTF tree.
    pub dev_tool_entities: Vec<Entity>,
    pub loaded: bool,
    pub pending_imports: Vec<PendingImport>,
    pub pending_fit_frames: u32,
    pub pending_fit_roots: Vec<Entity>,
    pub current_model_name: Option<String>,
    pub last_dropped_error: Option<String>,
    pub viewer_mode: bool,
    /// Entities the editor temporarily hid for a prefab thumbnail
    /// capture. Restored after the render frame has consumed the
    /// screenshot command, which takes one render pass.
    pub pending_thumbnail_restore: Vec<Entity>,
    pub pending_thumbnail_restore_frames: u32,
}

impl Default for LoadingState {
    fn default() -> Self {
        Self {
            model_entities: Vec::new(),
            dev_tool_entities: Vec::new(),
            loaded: false,
            pending_imports: Vec::new(),
            pending_fit_frames: 0,
            pending_fit_roots: Vec::new(),
            current_model_name: None,
            last_dropped_error: None,
            viewer_mode: true,
            pending_thumbnail_restore: Vec::new(),
            pending_thumbnail_restore_frames: 0,
        }
    }
}

pub struct CameraState {
    pub camera_entity: Option<Entity>,
    pub reset_camera_on_load: bool,
    pub reset_camera_was_pressed: bool,
    pub fly_mode: bool,
    pub fly_toggle_was_pressed: bool,
}

impl Default for CameraState {
    fn default() -> Self {
        Self {
            camera_entity: None,
            reset_camera_on_load: true,
            reset_camera_was_pressed: false,
            fly_mode: false,
            fly_toggle_was_pressed: false,
        }
    }
}

pub struct SunState {
    pub sun_entity: Option<Entity>,
    pub day_night_hour: f32,
    pub last_ibl_hour: f32,
    pub previous_atmosphere: Atmosphere,
    pub left_arrow_was_pressed: bool,
    pub right_arrow_was_pressed: bool,
    pub auto_cycle: bool,
}

impl Default for SunState {
    fn default() -> Self {
        Self {
            sun_entity: None,
            day_night_hour: 12.0,
            last_ibl_hour: 12.0,
            previous_atmosphere: Atmosphere::Sky,
            left_arrow_was_pressed: false,
            right_arrow_was_pressed: false,
            auto_cycle: true,
        }
    }
}

#[derive(Default)]
pub struct RotationState {
    pub rotation_speed: f32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RandomKind {
    Model,
    Hdri,
    Both,
    HdriAfterModel,
}

pub struct BrowserState {
    pub sample_browser: SampleBrowser,
    pub polyhaven_browser: PolyhavenBrowser,
    #[cfg(not(target_arch = "wasm32"))]
    pub sketchfab_browser: SketchfabBrowser,
    #[cfg(not(target_arch = "wasm32"))]
    pub kenney_browser: KenneyBrowser,
    pub random_resolution: u32,
    pub pending_random: Option<RandomKind>,
    pub thumbnail_queue: SharedTextureQueue,
    pub thumbnail_signature: u64,
    pub thumbnail_slots: HashMap<String, ThumbnailSlot>,
}

impl Default for BrowserState {
    fn default() -> Self {
        Self {
            sample_browser: SampleBrowser::default(),
            polyhaven_browser: PolyhavenBrowser::default(),
            #[cfg(not(target_arch = "wasm32"))]
            sketchfab_browser: SketchfabBrowser::default(),
            #[cfg(not(target_arch = "wasm32"))]
            kenney_browser: KenneyBrowser::default(),
            random_resolution: 2,
            pending_random: None,
            thumbnail_queue: nightshade::prelude::create_shared_queue(),
            thumbnail_signature: 0,
            thumbnail_slots: HashMap::new(),
        }
    }
}

#[derive(Default)]
pub struct UiState {
    pub show_tree: bool,
    pub show_inspector: bool,
    pub show_fps_label: bool,
    pub selected_entity: Option<Entity>,
    pub selected_entities: Vec<Entity>,
}

#[derive(Default)]
pub struct PickingState {
    pub press_position: Option<nightshade::prelude::Vec2>,
    pub is_dragging: bool,
    pub last_pick_leaf: Option<Entity>,
    pub last_pick_root: Option<Entity>,
    pub cycle_depth: usize,
    pub pending_alt: bool,
    pub pending_shift: bool,
    pub select_all_was_pressed: bool,
    pub deselect_was_pressed: bool,
}

#[derive(Default)]
pub struct LightGizmoState {
    pub entity: Option<Entity>,
}

/// Tracks the world-space camera gizmos: one shared lines entity for all
/// frustums, plus an invisible-ish pickable proxy mesh per camera so a click on
/// a camera visual selects the camera entity it stands in for.
#[derive(Default)]
pub struct CameraGizmoState {
    pub lines: Option<Entity>,
    pub camera_to_proxy: std::collections::HashMap<Entity, Entity>,
}

impl CameraGizmoState {
    /// Returns the camera a proxy mesh stands in for, if `proxy` is one.
    pub fn camera_for_proxy(&self, proxy: Entity) -> Option<Entity> {
        self.camera_to_proxy
            .iter()
            .find(|(_, value)| **value == proxy)
            .map(|(camera, _)| *camera)
    }
}

/// In-editor authoring state for a camera-path cutscene. Each shot has a
/// draggable marker entity (edited with the normal transform gizmo); a system
/// reads the markers back into the shots and tessellates the catmull-rom curve.
#[derive(Default)]
pub struct CutsceneEditState {
    pub shots: Vec<nightshade::ecs::cutscene::CutsceneShot>,
    pub waypoints: Vec<Entity>,
    pub curve_lines: Option<Entity>,
    pub preview_camera: Option<Entity>,
    pub previewing: bool,
    pub restore_camera: Option<Entity>,
}

pub struct SkeletonDebugState {
    pub enabled: bool,
    pub entity: Option<Entity>,
    pub bone_color: nightshade::prelude::Vec4,
    pub joint_color: nightshade::prelude::Vec4,
    pub joint_size: f32,
}

impl Default for SkeletonDebugState {
    fn default() -> Self {
        Self {
            enabled: false,
            entity: None,
            bone_color: nightshade::prelude::vec4(1.0, 0.65, 0.0, 1.0),
            joint_color: nightshade::prelude::vec4(0.2, 0.7, 1.0, 1.0),
            joint_size: 0.04,
        }
    }
}

pub struct SnapState {
    pub enabled: bool,
    pub translation_step: f32,
    pub rotation_step_degrees: f32,
    pub scale_step: f32,
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum EditorMode {
    Object,
    Edit { target: Entity },
}

pub struct ModeState {
    pub mode: EditorMode,
    pub tab_was_pressed: bool,
}

impl Default for ModeState {
    fn default() -> Self {
        Self {
            mode: EditorMode::Object,
            tab_was_pressed: false,
        }
    }
}

impl Default for SnapState {
    fn default() -> Self {
        Self {
            enabled: false,
            translation_step: 1.0,
            rotation_step_degrees: 15.0,
            scale_step: 0.1,
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GrabAxis {
    X,
    Y,
    Z,
}

impl GrabAxis {
    pub fn unit(self) -> Vec3 {
        match self {
            GrabAxis::X => Vec3::new(1.0, 0.0, 0.0),
            GrabAxis::Y => Vec3::new(0.0, 1.0, 0.0),
            GrabAxis::Z => Vec3::new(0.0, 0.0, 1.0),
        }
    }
}

#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GrabConstraint {
    Free,
    Axis(GrabAxis),
    Plane(GrabAxis),
}

#[derive(Clone)]
pub struct GrabEntry {
    pub entity: Entity,
    pub original_local_translation: Vec3,
    pub reference_world_translation: Vec3,
}

#[derive(Clone)]
pub struct GrabSession {
    pub entries: Vec<GrabEntry>,
    pub primary_world_origin: Vec3,
    pub initial_world_hit: Vec3,
    pub plane_normal_world: Vec3,
    pub camera_entity: Entity,
    pub constraint: GrabConstraint,
}

#[derive(Default)]
pub struct GrabState {
    pub session: Option<GrabSession>,
    pub g_was_pressed: bool,
    pub x_was_pressed: bool,
    pub y_was_pressed: bool,
    pub z_was_pressed: bool,
    pub enter_was_pressed: bool,
    pub escape_was_pressed: bool,
    pub left_mouse_was_pressed: bool,
    pub right_mouse_was_pressed: bool,
}

#[derive(Clone)]
pub struct PushPullSession {
    pub entity: Entity,
    pub uuid: AssetUuid,
    pub axis: usize,
    pub sign: f32,
    pub fixed_face: f32,
    pub unit_half: f32,
    pub plane_point: Vec3,
    pub plane_normal: Vec3,
    pub start_face: f32,
    pub original_local: nightshade::prelude::LocalTransform,
}

#[derive(Default)]
pub struct PushPullState {
    pub active: bool,
    pub session: Option<PushPullSession>,
}

/// First-person play-test mode: drop a player into the level and run around.
#[derive(Default)]
pub struct PlayState {
    pub active: bool,
    pub player: Option<Entity>,
    pub camera: Option<Entity>,
    pub ground: Option<Entity>,
    pub restore_camera: Option<Entity>,
    pub escape_was_pressed: bool,
}

pub struct GenerationSettings {
    pub footprint_x: f32,
    pub footprint_z: f32,
    pub stories: u32,
    pub story_height: f32,
    pub wall_thickness: f32,
    pub floor_thickness: f32,
    pub window_spacing: f32,
    pub seed: u32,
    pub variation: f32,
}

impl Default for GenerationSettings {
    fn default() -> Self {
        Self {
            footprint_x: 12.0,
            footprint_z: 9.0,
            stories: 3,
            story_height: 3.2,
            wall_thickness: 0.3,
            floor_thickness: 0.3,
            window_spacing: 3.0,
            seed: 1,
            variation: 0.5,
        }
    }
}

/// General level-authoring state for the `build` tools: the active material
/// palette and the work-plane grid entity. Not greybox-specific.
#[derive(Default)]
pub struct BuildState {
    pub active_palette: usize,
    pub textures_loaded: bool,
    pub grid_lines: Option<Entity>,
}

/// Greybox-only state: whether greybox mode is on and whether brushes are
/// showing their final art.
#[derive(Default)]
pub struct GreyboxState {
    pub enabled: bool,
    pub show_final: bool,
}

#[derive(Clone, Copy, PartialEq, Eq)]
pub enum PlacementShape {
    Cube,
    Plane,
    Cylinder,
    Cone,
    Sphere,
    Torus,
}

/// The draw-on-grid gesture state. `Hover` previews a stamp under the cursor;
/// dragging out a footprint moves to `Footprint`, then releasing moves to
/// `Height` where mouse movement sets the box height before a click commits.
#[derive(Clone, Copy)]
pub enum PlacementPhase {
    Hover,
    Footprint {
        start_corner: Vec3,
    },
    Height {
        footprint_min: Vec3,
        footprint_max: Vec3,
        height: f32,
    },
}

pub struct PlacementState {
    pub active: bool,
    pub shape: PlacementShape,
    pub size: Vec3,
    pub orient_to_normal: bool,
    pub ghost_entity: Option<Entity>,
    pub ghost_shape: Option<PlacementShape>,
    pub phase: PlacementPhase,
    pub press_screen: Option<Vec2>,
}

impl Default for PlacementState {
    fn default() -> Self {
        Self {
            active: false,
            shape: PlacementShape::Cube,
            size: Vec3::new(1.0, 1.0, 1.0),
            orient_to_normal: false,
            ghost_entity: None,
            ghost_shape: None,
            phase: PlacementPhase::Hover,
            press_screen: None,
        }
    }
}