ship: apply scale + gate approximate external parts behind a toggle

Two changes after user feedback that externals sit near-but-wrong (shield inset
into the hull, thruster floating close):

- Scale: the joint table's slots 5–7 are per-axis scale (a GN_ShieldG frame ships
  at 0.5, GN_Jet FX at 2.4–6.5) and were being ignored, rendering scaled parts at
  the wrong size. ScenePart now carries `s` and applies R·(S·v)+T.
- External placement is fundamentally approximate: a GN_* frame is the mount
  PIVOT (f105's two GN_ShieldG frames are both on the centreline), while the
  part's outboard/rotational offset lives in a detail sub-rig / runtime code this
  static pass can't recover. So assemble_ship gains `include_external`: the hull
  tier (rou_ nodes) is exact and always drawn; the external tier (bridge / shield
  / engine at GN_ frames) is opt-in via a "Show external parts" checkbox in the
  Ships browser, off by default so the clean, correct hull is the default view.

The module doc is corrected to the scene-graph mechanism (the old "draw parts
untransformed" claim was wrong).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
MechaCat02
2026-07-23 22:11:47 +02:00
parent 8c8e2dbeb4
commit 79c78cc7e1
4 changed files with 72 additions and 46 deletions

View File

@@ -624,6 +624,9 @@ pub struct ShipBrowser {
pub rows: Vec<ShipRow>,
/// Family id of the ship currently rendered (for row highlight).
pub selected: Option<String>,
/// Also place the approximate external parts (bridge / shield gen / engines at
/// their `GN_*` hardpoint frames). Off by default → the exact hull only.
pub show_external: bool,
}
/// Ask the loader to scan the stage containers and build the ship catalog.
@@ -637,6 +640,8 @@ pub struct RequestShipRender {
pub file: String,
/// Ship family id (`e106`) — drives the scene-graph assembly.
pub id: String,
/// Also place the approximate external hardpoint parts.
pub external: bool,
/// Display label for the model info line.
pub label: String,
}
@@ -3904,7 +3909,8 @@ fn handle_ship_render_request(
let Some(req) = events.read().last() else {
return;
};
let (file, id, label) = (req.file.clone(), req.id.clone(), req.label.clone());
let (file, id, external, label) =
(req.file.clone(), req.id.clone(), req.external, req.label.clone());
// Supersede any in-flight XPR/stage decode, exactly like a file selection.
xpr_load.generation = xpr_load.generation.wrapping_add(1);
@@ -3919,7 +3925,7 @@ fn handle_ship_render_request(
let source = iso_state.source_kind.clone();
let sender = channels.sender.clone();
std::thread::spawn(move || {
let prepared = build_ship_model(&source, &file, &id, &label, &cancel);
let prepared = build_ship_model(&source, &file, &id, external, &label, &cancel);
let _ = sender.send(IsoLoaderMsg::XprPrepared {
generation,
prepared: Box::new(prepared),
@@ -3935,6 +3941,7 @@ fn build_ship_model(
source: &SourceKind,
file: &str,
id: &str,
external: bool,
label: &str,
cancel: &Arc<AtomicBool>,
) -> PreparedXpr {
@@ -3947,7 +3954,7 @@ fn build_ship_model(
// Scene-graph placement: which resources to draw and where (bridge fore,
// engines aft, hardpoints on their frames).
let placed = sylpheed_formats::ship::assemble_ship(&bytes, id);
let placed = sylpheed_formats::ship::assemble_ship(&bytes, id, external);
if placed.is_empty() {
return PreparedXpr::Info(format!("No parts found for {label}."));
}

View File

@@ -1589,9 +1589,18 @@ fn draw_ships_ui(
ships.filter.clear();
}
});
ui.horizontal(|ui| {
ui.checkbox(&mut ships.show_external, "Show external parts");
ui.label(
egui::RichText::new("(bridge / shield gens / engines — approximate placement)")
.weak()
.small(),
);
});
ui.separator();
let ShipBrowser { rows, filter, faction, selected, .. } = &mut *ships;
let ShipBrowser { rows, filter, faction, selected, show_external, .. } = &mut *ships;
let show_external = *show_external;
let needle = filter.to_lowercase();
let mut to_render: Option<(String, String, String)> = None;
@@ -1677,7 +1686,7 @@ fn draw_ships_ui(
if let Some((id, file, label)) = to_render {
let id2 = id.clone();
*selected = Some(id);
render.send(RequestShipRender { file, id: id2, label });
render.send(RequestShipRender { file, id: id2, external: show_external, label });
}
});
ships.open &= open;