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

View File

@ -82,3 +82,11 @@
.map svg g.derailer path { .map svg g.derailer path {
fill: #eee; 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 NEVPRenderer from './object-renderers/NEVPRenderer';
import DerailerRenderer from './object-renderers/DerailerRenderer'; import DerailerRenderer from './object-renderers/DerailerRenderer';
import ElectrificationStatusPopup from './additional-layer-components/ElectrificationStatusPopup'; import ElectrificationStatusPopup from './additional-layer-components/ElectrificationStatusPopup';
import SpawnPointRenderer from './object-renderers/SpawnPointRenderer';
const ObjectRendererQueue = [ const ObjectRendererQueue = [
{ {
@ -61,6 +62,13 @@ const ObjectRendererQueue = [
'renderer': SignalRenderer, 'renderer': SignalRenderer,
'cond': (layers) => layers['signals'] 'cond': (layers) => layers['signals']
}, },
{
'name': 'spawn-points',
'category': 'track-objects',
'type': 'SpawnPoint',
'renderer': SpawnPointRenderer,
'cond': (layers) => layers['spawn-points'],
},
{ {
'name': 'signalboxes', 'name': 'signalboxes',
'category': 'misc', '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, switchInvalidTrackConnection: true,
unknownObjectType: true, unknownObjectType: true,
unknownTrackType: true, unknownTrackType: true,
signalWithoutSpawnInfo: true, spawnWithoutSpawnInfo: true,
trackAliasAlreadyExists: true, trackAliasAlreadyExists: true,
trackAliasNoTrack: true, trackAliasNoTrack: true,
switchUndefinedModel: true, switchUndefinedModel: true,
@ -50,7 +50,7 @@ const Constants = {
{ {
id: 'switches-names', id: 'switches-names',
name: 'Switches names', name: 'Switches names',
default: true, default: false,
}, },
{ {
id: 'isolations-ids', id: 'isolations-ids',
@ -67,6 +67,11 @@ const Constants = {
name: 'Signals', name: 'Signals',
default: true, default: true,
}, },
{
id: 'spawn-points',
name: 'SpawnPoints',
default: true,
},
{ {
id: 'derailers', id: 'derailers',
name: 'Derailers', name: 'Derailers',

View File

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

View File

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

View File

@ -20,7 +20,7 @@ export default class Signal extends TrackObject {
}); });
if (this.is_spawn && !this.spawn_info) { 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; this.applied = true;
if (this.is_spawn) { 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);
}
}
}