Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
The table of contents is too big for display.
Diff view
Diff view
  •  
  •  
  •  
2 changes: 2 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

116 changes: 105 additions & 11 deletions MODS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1046,10 +1046,19 @@ look like. `mods/example/signals/ks_main.ron`:
)
```

A line references the type by name:
A line references the type by name. Every signal placement also carries its
operational designation and the interlocking which controls it:

```ron
signals: [(kind: Main, system: Ks, device: 1, guarded: [0], signal_type: Some("example:ks_main"))],
signals: [(
designation: "24P3",
interlocking: "Lf",
kind: Main,
system: Ks,
device: 1,
guarded: [0],
signal_type: Some("example:ks_main"),
)],
```

### Signal models
Expand Down Expand Up @@ -1082,23 +1091,49 @@ script's `lamps` (the Zs1 below) lights them the same way.

**Moving parts — semaphore signals.** `motions` binds a lamp-image string to a node
that *travels* instead of switching: while the string is in the lamp image the node
moves to full travel, without it back to rest, linearly over `seconds` — a quick
aspect change swings a semaphore arm through its real intermediate positions. The
moves to full travel, without it back to rest. The default `profile: Linear` covers
generic mechanisms. `profile: Semaphore(...)` drives an arm upwards with finite
acceleration, then lets it fall back under gravity and rebound with diminishing height
at its mechanical stop. `seconds` is the driven raising time, `fall_seconds` the free
return from full travel, and `rebound` the coefficient of restitution (0 = no bounce).
The small physics steps are independent of the render frame rate. The
strings name the moved elements, so an aspect that moves two arms lists two of them
(`signals/hv_form.ron` + `signal_models/form_hp.ron`):

```ron
motions: [
(lamp: "fluegel1", node: "fluegel1",
motion: Rotate(axis: (0.0, 0.0, 1.0), degrees: 45.0), seconds: 1.8),
motion: Rotate(axis: (0.0, 0.0, 1.0), degrees: 45.0), seconds: 1.8,
profile: Semaphore(fall_seconds: 0.75, rebound: 0.36)),
(lamp: "fluegel2", node: "fluegel2",
motion: Rotate(axis: (0.0, 0.0, 1.0), degrees: 135.0), seconds: 1.8),
motion: Rotate(axis: (0.0, 0.0, 1.0), degrees: -45.0), seconds: 1.8,
profile: Semaphore(fall_seconds: 0.85, rebound: 0.34)),
]
```

`motion` takes the same `Rotate`/`Translate`/`Visibility` as vehicle parts; one
binding per node. Rest pose (travel 0) is the stop position.

The example mod also ships the full-size German mechanical family: Hp masts at
6/8/10/12/14 m nominal height from rail underside to upper blade pivot in lattice and
narrow construction (one or two arms, long or shortened),
Vr discs at 2.76/4.87/5.37 m (two or three aspects), and low/high Sh rotating-disc
signals. Complete DB pale-green and iron-grey catalogues plus historical painted-mast
and negative-blade examples make 188 selectable geometric models. The 14-m Hp models
are historical special-height variants; later installation principles preferred at
most 12 m. The distant-signal subset
also covers gas or modern LED night signs, attached or freestanding Ne 2 boards,
green or iron-grey masts and three decreasing geometry LODs. The generated
`example:formsignal_showcase` line places every model in one row.
On the gas variants, two separate 180-degree colour-selector motions exchange amber
and green in opposite directions in front of fixed lanterns; Vr 2 moves only the right
selector. The gas cylinders remain on their service lift and do not move during an
aspect change. LED variants deliberately have neither those selector motions nor gas
cylinders.
Their embedded PBR maps, macro weathering and matching catalogue files are rebuilt with
`python tools/gen_form_signals.py`; the researched dimensions, scope and naming are listed
in [`mods/example/FORMSIGNALE.md`](mods/example/FORMSIGNALE.md).

**Levels of detail.** An optional `lods` table switches nodes named
`<name>_LOD<level>` by camera distance, exactly like vehicles: coarsest last,
beyond the last distance the LOD nodes disappear; nodes without the suffix are
Expand All @@ -1119,6 +1154,64 @@ look, e.g. an emissive factor — switching is pure visibility). The
**signal editor** (`trainsim-signal-editor`) assembles parts, binds lamps with
suggestions from these names, and lights any lamp image in its preview.

### Per-placement designation plates and subsidiary signals

`designation` is rendered onto a real plate at runtime, so A, N1 and every other
placement can share the same mast model. It uses a bundled DIN-style condensed font,
not letter meshes. The plate follows the 285 × 300 mm field: a location prefix in a
long name is stacked over the operational name (`24` over `P3`) inside that one field
instead of widening the plate. `interlocking` is operational metadata and is not painted onto the mast.
Both fields are edited in the route editor and the checker reports either one missing.

`addons` describes only equipment physically carried by this signal. A dense but valid
example is:

```ron
addons: (
zs1: Some(ThreeLights),
zs2: Some(["K", "S"]),
zs3: Some((values: [4, 6], construction: Light)),
zs6: Some(Form),
zs12: true,
)
```

The editor offers the prototype constructions rather than arbitrary geometry:

| field | selectable construction / value |
|---|---|
| `zs1` | `ThreeLights` (A) or `BlinkingLight` |
| `zs2`, `zs2v` | one identifying letter per route-dependent value; several values share the fitted dot-matrix display |
| `zs3`, `zs3v` | fixed `Form` board with one code, or a `Light` display with several codes (1 … 16; ×10 km/h) |
| `zs6` | `Form` board or `Light` strip |
| `zs7` | three yellow lights in a V |
| `zs8` | `ThreeLights` (flashing A) or `LightStrip` |
| `zs12` | fixed M board |
| `zs13` | fixed `Form` board or `Light` display |
| `zs103` | historical DV-301 board with six white diamonds |

Main-signal fittings are accepted on `Main`/`Combined`, advance indicators on
`Distant`/`Combined`. Zs 13 cannot be combined with a Zs 3 code 1, 2 or 3. Zs 1 and
Zs 8 in A form share one three-lamp housing when both are fitted; likewise light-strip
Zs 6 and Zs 8 share their physical display. Light faces listen to `zs1`, `zs2_K`,
`zs2v_K`, `zs3_4`, `zs3v_4`, `zs6`, `zs7`, `zs8` and `zs13`; where a display has only
one possible value the generic `zs2`/`zs3`/`zs2v`/`zs3v` alias works too. Form boards,
Zs 12 and Zs 103 are permanently visible.

Zs 9 and Zs 10 are deliberately not in `addons`: Zs 9 stands *before* the applicable
light main signal, while Zs 10 is a separate signal inside the points area. Modelling
either as mast furniture would create a placement the signal book does not permit.

The dedicated line `example:zusatzsignal_showcase` places every fitting, both shared
housings, form/light alternatives, multi-value displays and combined examples in one
row. A reproducible front overview is:

```sh
cargo run -p app -- --line example:zusatzsignal_showcase --loco example:br101_afb \
--camera fly --fly 0,18,280 --look 0,5,458 --hud off \
--window 1920x1080 --frames 70 --screenshot /tmp/zusatzsignal-showcase.png
```

### Aspect hook `aspect(ctx)`

Runs after the rule table and sees its result. `nil` keeps that result.
Expand All @@ -1127,9 +1220,9 @@ Runs after the rule table and sees its result. `nil` keeps that result.
|---|---|
| `signal`, `time` | signal index and simulation time [s] |
| `clear`, `route`, `diverging`, `next_stop`, `next_slow` | the situation, as above |
| `main`, `distant`, `speed` | what the table decided (`"stop"`, `"proceed"`, `"proceed_slow"`, `"substitute"`, `"dark"`; `"expect_stop"`, `"expect_proceed"`, `"expect_slow"`) |
| `main`, `distant`, `shunt`, `speed` | what the table decided (`"stop"`, `"proceed"`, `"proceed_slow"`, `"substitute"`, `"dark"`; `"expect_stop"`, `"expect_proceed"`, `"expect_slow"`; Sh `"stop"`/`"proceed"`) |

The return table takes the same `main`, `distant`, `speed` and `lamps`.
The return table takes the same `main`, `distant`, `shunt`, `speed` and `lamps`.

`mods/example/scripts/zs1.lua` — the substitute signal, a case that needs memory:

Expand Down Expand Up @@ -1180,9 +1273,10 @@ that compiles but fails on the line — a distant signal without its 1000 Hz mag
beyond its track, a boundary on a node that is no buffer.

The interlocking tables are edited there as well, so none of them has to be typed as RON:
a placed `Signal` device gets its **signal table entry** in the selection panel (kind, system,
the signal it announces, guarded sections, whether it needs a route, diverging speed, signal
type and 3D model) together with **the routes that start at it** — where each one ends and
a placed `Signal` device gets its **signal table entry** in the selection panel (designation,
controlling interlocking, kind, system, the signal it announces, guarded sections, whether it
needs a route, diverging speed, signal type, 3D model and optional Zs fittings) together with
**the routes that start at it** — where each one ends and
what it locks, and *Find routes*, which runs out over the track and offers one route per leg
of every turnout ahead, each ending at the next signal on it. Routes already in the file stay
as they are, so finding again after a change adds what is new and touches nothing else.
Expand Down
27 changes: 25 additions & 2 deletions STATUS.md
Original file line number Diff line number Diff line change
Expand Up @@ -1027,7 +1027,18 @@ As of 2026-08-31 · `cargo test --workspace`: **1136 tests green** · clippy and
that ask for it on the terrain surface), and the **signal assemblies** on
their mount points. The shared `world-render` crate is that code, used by
both programs, so a stroke, a wood, a signal box or a signal mast is judged
where it is set instead of only in the run. Tiles are
where it is set instead of only in the run.
Every placed signal now carries its **operational designation and controlling
interlocking** as editable route data. The renderer rasterises its DIN-style
designation plate per placement (including names such as `24P3`, stacked within
the 285 × 300 mm standard field)
and assembles optional prototype Zs fittings without multiplying the base glTF:
Zs 1, 2/2v, 3/3v, 6, 7, 8, 12, 13 and historical Zs 103, including form/light,
shared-housing and route-dependent multi-value variants. The route checker
rejects fittings on the wrong signal function and the Zs-13/Zs-3(1–3) conflict.
Zs 9 and Zs 10 remain separate placements, as prescribed, rather than fake
mast attachments.
Tiles are
built on the task pool around the view point (3 km radius with a 25 %
unload hysteresis, capped at 64 tiles); an edit is **diffed** against the
last state (`main.rs::diff`) into what it reached — a stroke the ground of
Expand Down Expand Up @@ -1705,7 +1716,19 @@ As of 2026-08-31 · `cargo test --workspace`: **1136 tests green** · clippy and
and the script-lit Zs1 included. **Semaphore signals** come out of the same strings:
`motions` bindings make a node *travel* (rotate/translate over a travel time) while
its string is in the lamp image — the example line's Form signal swings its arms
through the real intermediate positions. Optional **`lods`** switch `_LOD<n>` nodes
through the real intermediate positions. The example mod now ships the full current
H/V form family as 140 original-scale, weathered PBR geometric models: one- and two-arm
Hp signals on 6/8/10/12/14 m nominal-SU-height lattice and narrow masts (14 m is the
historical special height), two- and three-aspect Vr signals at
2.76/4.87/5.37 m with pale-green or iron-grey masts, gas or LED night signs and
attached or freestanding Ne 2 boards, plus low/high rotating Sh signals. Ne 2 uses a
sharp 512 px face/normal/ORM texture at the S 525.1 sizes 750 × 480 or 450 × 300 mm;
Vr and freestanding Ne 2 models carry three decreasing geometry LODs. Their arms use frame-rate-independent mechanics:
finite-acceleration powered raising, gravity-driven return and a diminishing rebound
at the stop (a separately tuned fall for the lower arm); the heavy Vr disc rebounds less.
Every asset embeds base-colour, ORM and normal maps;
`tools/gen_form_signals.py` regenerates and validates the complete catalogue. Optional
**`lods`** switch `_LOD<n>` nodes
by camera distance, like vehicles. The model comes from the signal type's `model`
default or a per-placement override; a signal without one gets a placeholder mast
whose light follows the aspect, so every line shows its signals. The **signal editor**
Expand Down
9 changes: 9 additions & 0 deletions THIRD_PARTY_LICENSES.md
Original file line number Diff line number Diff line change
Expand Up @@ -168,6 +168,15 @@ SOFTWARE.
The `egui-phosphor` crate that bundles the font (© 2023 Romet Tagobert) is MIT
OR Apache-2.0; the font file it ships is compiled into the editor binaries.

## DINish signal-plate font

`crates/world-render/fonts/DINishCondensed-Bold.ttf` is from the
[DINish project](https://github.com/playbeing/dinish), copyright 2021 The DINish
Project Authors. It is bundled under the SIL Open Font License 1.1 and is used to
rasterise per-placement signal designation plates and illuminated indicators at
runtime. The complete licence accompanies the font in
[`crates/world-render/fonts/LICENSE-DINish.txt`](crates/world-render/fonts/LICENSE-DINish.txt).

## People in `mods/people/`

The characters — the walker's body and the passengers — are generated out of
Expand Down
4 changes: 4 additions & 0 deletions crates/app/src/loading.rs
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,7 @@ pub(crate) fn load_track(
mut commands: Commands,
mut meshes: ResMut<Assets<Mesh>>,
world_materials: render::WorldMaterials,
mut images: ResMut<Assets<Image>>,
assets: Res<AssetServer>,
mods: Res<Mods>,
stash: Res<LoadingStash>,
Expand Down Expand Up @@ -646,12 +647,15 @@ pub(crate) fn load_track(
kind: s.kind,
aspect: s.aspect,
model: signal_models.get(i).and_then(|m| m.as_ref()),
designation: &line_source.signals[i].designation,
addons: &line_source.signals[i].addons,
})
.collect();
let aspect_materials = world_render::spawn_signals(
&mut commands,
&mut meshes,
&mut materials,
&mut images,
&assets,
&world.sim.net,
&views,
Expand Down
Loading
Loading