Added SpawnPoints

+Added SpawnPoint object parser, layer and icon
*Renamed Scenery.addSpawnSignal to .addSpawnPoint
*Renamed Scenery.spawnSignals to .spawnPoints
*Minor name changes
This commit is contained in:
izawartka 2025-06-22 16:30:17 +02:00
parent 197cb2dcde
commit 38ecdbc2e3
10 changed files with 153 additions and 15 deletions

8
public/assets/spawn.svg Normal file
View File

@ -0,0 +1,8 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg width="100mm" height="100mm" version="1.1" viewBox="0 0 100 100" xmlns="http://www.w3.org/2000/svg">
<path d="m33.8 15.3c-5.9209 0-10.8 4.8791-10.8 10.8v37.8c0 5.9209 4.8791 10.8 10.8 10.8h32.4c5.9209 0 10.8-4.8791 10.8-10.8v-37.8c0-5.9209-4.8791-10.8-10.8-10.8z" class="background" />
<path d="m32 12c-6.5788 0-12 5.4215-12 12v42c0 5.3408 3.5732 9.9187 8.4377 11.453l-2.2732 6.567a3 3 0 0 0 1.8552 3.8163 3 3 0 0 0 3.8163-1.8557l2.7616-7.9804h30.805l2.7631 7.9804a3 3 0 0 0 3.8163 1.8557 3 3 0 0 0 1.8536-3.8163l-2.2732-6.567c4.8645-1.5338 8.4372-6.1117 8.4372-11.453v-42c0-6.5788-5.421-12-12-12h-36zm0 8h36c2.2852 0 4.0003 1.7151 4.0003 4.0003v42c0 2.2852-1.7151 3.9998-4.0003 3.9998h-36c-2.2852 0-4.0003-1.7146-4.0003-3.9998v-42c0-2.2852 1.7151-4.0003 4.0003-4.0003z" stop-color="#000000" stroke-linecap="round" stroke-linejoin="round"/>
<path d="m40 54a4 4 0 0 1-4 4 4 4 0 0 1-4-4 4 4 0 0 1 4-4 4 4 0 0 1 4 4z" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"/>
<path d="m68 54a4 4 0 0 1-4 4 4 4 0 0 1-4-4 4 4 0 0 1 4-4 4 4 0 0 1 4 4z" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"/>
<path d="m34 24h32.38c1.108 0 2 0.892 2 2v12.801c0 1.108-0.892 2-2 2h-32.38c-1.108 0-2-0.892-2-2v-12.801c0-1.108 0.892-2 2-2z" stroke-linecap="round" stroke-linejoin="round" stroke-width="8"/>
</svg>

After

Width:  |  Height:  |  Size: 1.3 KiB

View File

@ -17,16 +17,17 @@ export default function LocList(props) {
});
}, [scenery?.signalBoxes]);
const spawnSignals = useMemo(() => {
if (!scenery?.spawnSignals) return null;
return scenery.spawnSignals.map(signal => {
const pos = signal.pos.toSVGCoords();
const name = signal.spawn_info.name || signal.getPrintableSignalName();
const spawnPoints = useMemo(() => {
if (!scenery?.spawnPoints) return null;
return scenery.spawnPoints.map(trackObject => {
const pos = trackObject.pos.toSVGCoords();
const isSignal = trackObject.type === 'Signal';
const name = trackObject.spawn_info.name || (isSignal ? trackObject.getPrintableSignalName() : trackObject.getPrintableSpawnPointName());
return (
<ClickableLocation name={name} pos={pos} key={signal.id} />
<ClickableLocation name={name} pos={pos} key={trackObject.id} />
)
});
}, [scenery?.spawnSignals]);
}, [scenery?.spawnPoints]);
return (
<div className="loclist">
@ -34,7 +35,7 @@ export default function LocList(props) {
{ signalBoxes }
</div>
<div className="loclist-section">
{ spawnSignals }
{ spawnPoints }
</div>
</div>
);

View File

@ -82,3 +82,11 @@
.map svg g.derailer path {
fill: #eee;
}
.map svg g.spawn-point path {
fill: #eee;
}
.map svg g.spawn-point path.background {
fill: #111;
}

View File

@ -9,6 +9,7 @@ import SignRenderer from './object-renderers/SignRenderer';
import NEVPRenderer from './object-renderers/NEVPRenderer';
import DerailerRenderer from './object-renderers/DerailerRenderer';
import ElectrificationStatusPopup from './additional-layer-components/ElectrificationStatusPopup';
import SpawnPointRenderer from './object-renderers/SpawnPointRenderer';
const ObjectRendererQueue = [
{
@ -61,6 +62,13 @@ const ObjectRendererQueue = [
'renderer': SignalRenderer,
'cond': (layers) => layers['signals']
},
{
'name': 'spawn-points',
'category': 'track-objects',
'type': 'SpawnPoint',
'renderer': SpawnPointRenderer,
'cond': (layers) => layers['spawn-points'],
},
{
'name': 'signalboxes',
'category': 'misc',

View File

@ -0,0 +1,33 @@
import { ReactSVG } from 'react-svg'
import AngleHelper from '../../../helpers/angleHelper';
export default function SpawnPointRenderer(props) {
const { object } = props;
const [x, y] = object.pos.toSVGCoords();
const rot = AngleHelper.normalizeDegAngle(object.rot.y);
const upsideDown = rot >= 180;
const upsideDownRot = upsideDown ? 90 : -90;
const anchor = upsideDown ? "start" : "end";
return (
<g className="spawn-point" transform={`translate(${x}, ${y}) rotate(${object.rot.y})`}>
<g className="signal-icon" transform={`rotate(180) translate(-2.835, -2.835)`}>
<ReactSVG
src={`${process.env.PUBLIC_URL}/assets/spawn.svg`}
wrapper='svg'
beforeInjection={(svg) => {
svg.setAttribute('width', '1.5mm');
svg.setAttribute('height', '1.5mm');
}}
/>
</g>
<g transform={`translate(0, 3) rotate(${upsideDownRot})`}>
<text x="0" y="0" textAnchor={anchor} dominantBaseline="middle">
{object.getPrintableSpawnPointName()}
</text>
</g>
</g>
);
}

View File

@ -21,7 +21,7 @@ const Constants = {
switchInvalidTrackConnection: true,
unknownObjectType: true,
unknownTrackType: true,
signalWithoutSpawnInfo: true,
spawnWithoutSpawnInfo: true,
trackAliasAlreadyExists: true,
trackAliasNoTrack: true,
switchUndefinedModel: true,
@ -50,7 +50,7 @@ const Constants = {
{
id: 'switches-names',
name: 'Switches names',
default: true,
default: false,
},
{
id: 'isolations-ids',
@ -67,6 +67,11 @@ const Constants = {
name: 'Signals',
default: true,
},
{
id: 'spawn-points',
name: 'SpawnPoints',
default: true,
},
{
id: 'derailers',
name: 'Derailers',

View File

@ -17,6 +17,7 @@ import MainCamera from './main-camera';
import ElectrificationResolver from './electrification-resolver';
import NEVP from './nevp';
import Derailer from './derailer';
import SpawnPoint from './spawn-point';
/*
TODO: add support for WorldRotation and WorldTranslation
@ -129,6 +130,8 @@ export default class SceneryParser {
return Derailer.fromText(text);
} else if(NEVP.isNEVP(text)) {
return NEVP.fromText(text);
} else if(SpawnPoint.isSpawnPoint(text)) {
return SpawnPoint.fromText(text);
} else {
return TrackObject.fromText(text);
}

View File

@ -5,7 +5,7 @@ export default class Scenery
{
objects = {};
signalBoxes = [];
spawnSignals = [];
spawnPoints = [];
bounds = { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity };
trackAliases = {};
electrificationResolved = ElectrificationResolutionStatus.NOT_RESOLVED;
@ -19,8 +19,8 @@ export default class Scenery
this.signalBoxes.push(signalBox);
}
addSpawnSignal(signal) {
this.spawnSignals.push(signal);
addSpawnPoint(trackObject) {
this.spawnPoints.push(trackObject);
}
addObject(object) {

View File

@ -20,7 +20,7 @@ export default class Signal extends TrackObject {
});
if (this.is_spawn && !this.spawn_info) {
SceneryParserLog.warn('signalWithoutSpawnInfo', `Signal ${this.id} is marked as spawn but has no spawn info`);
SceneryParserLog.warn('spawnWithoutSpawnInfo', `Signal ${this.id} is marked as spawn but has no spawn info`);
}
}
@ -86,7 +86,7 @@ export default class Signal extends TrackObject {
this.applied = true;
if (this.is_spawn) {
scenery.addSpawnSignal(this);
scenery.addSpawnPoint(this);
}
}

72
src/model/spawn-point.js Normal file
View File

@ -0,0 +1,72 @@
import SceneryParserLog from "./scenery-parser-log";
import SpawnInfo from "./spawn-info";
import TrackObject from "./track-object";
import Vector3 from "./vector3";
export default class SpawnPoint extends TrackObject {
is_spawn;
spawn_info;
spawn_point_name;
type = "SpawnPoint";
applied = false;
constructor(id, prefab_name, pos, rot, track_id, name, is_spawn, spawn_info, signal_name) {
super(id, prefab_name, pos, rot, track_id, name);
Object.assign(this, {
is_spawn,
spawn_info,
signal_name
});
if (this.is_spawn && !this.spawn_info) {
SceneryParserLog.warn('spawnWithoutSpawnInfo', `SpawnPoint ${this.id} is marked as spawn but has no spawn info`);
}
}
getPrintableSpawnPointName() {
return this.spawn_point_name?.trim() || this.name?.trim() || "Spawn Point";
}
/*
Check if a TrackObject text is a SpawnPoint.
@param {string} text - The text representation of the TrackObject.
@returns {boolean} - Returns true if the text represents a SpawnPoint, false otherwise.
@static
@memberof Signal
*/
static isSpawnPoint(text) {
const values = text.split(";");
if(values.length < 29) return false;
const prefabName = values[2];
return prefabName === "Spawn Point";
}
static fromText(text) {
const values = text.split(";");
const signal = new SpawnPoint(
values[1], // id
values[2], // prefab_name
Vector3.fromValuesArray(values, 3), // pos
Vector3.fromValuesArray(values, 6), // rot
values[9], // track_id
values[11], // name,
values[12] === "Spawn Signal", // is_spawn
SpawnInfo.fromText(values[15]), // spawn_info
values[29]
);
return signal;
}
applyObject(scenery) {
if(this.applied) return; // already applied
this.applied = true;
if (this.is_spawn) {
scenery.addSpawnPoint(this);
}
}
}