Added signs attaching

+Added an attachSigns function that will attach signs near signals to these signals so in the future their icons can be rendered without overlapping
+Added Vector3.distanceSqExcludeY, .toLocal and .negate
*Fixed SceneryObject not returning valid render bounds
This commit is contained in:
izawartka 2025-06-27 17:40:56 +02:00
parent 05bacafb67
commit 2d655676c9
6 changed files with 118 additions and 1 deletions

98
src/model/attach-signs.js Normal file
View File

@ -0,0 +1,98 @@
import AngleHelper from "../helpers/angleHelper";
import Constants from "../helpers/constants";
import SceneryParserLog from "./scenery-parser-log";
import Vector3 from "./vector3";
function getGridSize(scenery) {
const sceneryBounds = scenery.getBounds();
const gridSizeX = Math.floor((sceneryBounds.maxX - sceneryBounds.minX) / Constants.parser.attachSignsGridSize) + 1;
const gridSizeZ = Math.floor((sceneryBounds.maxZ - sceneryBounds.minZ) / Constants.parser.attachSignsGridSize) + 1;
return { x: gridSizeX, z: gridSizeZ };
}
function getGridCellPositions(pos, scenery, gridSize) {
const sceneryBounds = scenery.getBounds();
const gx = Math.floor((pos.x - sceneryBounds.minX) / Constants.parser.attachSignsGridSize);
const gz = Math.floor((pos.z - sceneryBounds.minZ) / Constants.parser.attachSignsGridSize);
if(gx < 0 || gx >= gridSize.x || gz < 0 || gz >= gridSize.z) {
SceneryParserLog.warn('attachSignsOutOfBounds', `TrackObject at position (${pos.x}, ${pos.z}) is out of bounds of the scenery grid. Grid size: ${gridSize.x}x${gridSize.z}, position: (${gx}, ${gz})`);
return [];
}
const positions = [{ x: gx, z: gz }];
const inCellX = pos.x - (sceneryBounds.minX + gx * Constants.parser.attachSignsGridSize);
const inCellZ = pos.z - (sceneryBounds.minZ + gz * Constants.parser.attachSignsGridSize);
const isLeftEdge = inCellX <= Constants.parser.attachSignsMaxDistanceZ && gx > 0;
const isRightEdge = inCellX >= Constants.parser.attachSignsGridSize - Constants.parser.attachSignsMaxDistanceZ && gx < gridSize.x - 1;
const isTopEdge = inCellZ <= Constants.parser.attachSignsMaxDistanceZ && gz > 0;
const isBottomEdge = inCellZ >= Constants.parser.attachSignsGridSize - Constants.parser.attachSignsMaxDistanceZ && gz < gridSize.z - 1;
if(isLeftEdge) positions.push({ x: gx - 1, z: gz });
if(isRightEdge) positions.push({ x: gx + 1, z: gz });
if(isTopEdge) positions.push({ x: gx, z: gz - 1 });
if(isBottomEdge) positions.push({ x: gx, z: gz + 1 });
if(isLeftEdge && isTopEdge) positions.push({ x: gx - 1, z: gz - 1 });
if(isLeftEdge && isBottomEdge) positions.push({ x: gx - 1, z: gz + 1 });
if(isRightEdge && isTopEdge) positions.push({ x: gx + 1, z: gz - 1 });
if(isRightEdge && isBottomEdge) positions.push({ x: gx + 1, z: gz + 1 });
return positions;
}
export function attachSigns(scenery) {
const maxDistSq = Constants.parser.attachSignsMaxDistanceZ ** 2 + Constants.parser.attachSignsMaxDistanceX ** 2;
const trackObjectsArr = Object.values(scenery.objects['track-objects'] || {});
const gridSize = getGridSize(scenery);
const posGrid = new Array(gridSize.x)
.fill(new Array(gridSize.z)
.fill([]));
for(const trackObject of trackObjectsArr) {
if(trackObject.type !== 'Signal') continue;
const positions = getGridCellPositions(trackObject.pos, scenery, gridSize);
for(const pos of positions) {
posGrid[pos.x][pos.z].push(trackObject);
}
}
for(const trackObject of trackObjectsArr) {
if(trackObject.type !== 'Sign') continue;
const positions = getGridCellPositions(trackObject.pos, scenery, gridSize);
const signals = positions.flatMap(pos => posGrid[pos.x][pos.z]);
let closestSignal = null;
let closestDistanceSq = Infinity;
for(const signal of signals) {
if(Constants.parser.attachSignsNeedsSameTrack && signal.track_id !== trackObject.track_id) continue; // skip signals on different tracks
const distanceSq = signal.pos.distanceSqExcludeY(trackObject.pos);
if(distanceSq > maxDistSq) continue; // skip too far signals
const signalAngleRad = AngleHelper.degToRad(signal.rot.y);
const localPos = trackObject.pos.toLocal(signal.pos, new Vector3(0, signalAngleRad, 0));
if(Math.abs(localPos.z) > Constants.parser.attachSignsMaxDistanceZ) continue; // skip signals that are too far in front or behind
if(Math.abs(localPos.x) > Constants.parser.attachSignsMaxDistanceX) continue; // skip signals that are too far to the side
if(distanceSq < closestDistanceSq) {
closestDistanceSq = distanceSq;
closestSignal = signal;
}
}
if(!closestSignal) continue;
if(Constants.parser.logAttachedSigns) {
console.log(`Attaching sign ${trackObject.name} (${trackObject.id}) to signal ${closestSignal.name} (${closestSignal.id}) at distance ${Math.sqrt(closestDistanceSq).toFixed(2)} m`);
}
trackObject.attached_to = closestSignal;
closestSignal.attached_signs.push(trackObject);
}
}

View File

@ -21,6 +21,6 @@ export default class SceneryObject {
}
getRenderBounds() {
return this.pos;
return [this.pos];
}
}

View File

@ -18,6 +18,7 @@ import ElectrificationResolver from './electrification-resolver';
import NEVP from './track-objects/nevp';
import Derailer from './track-objects/derailer';
import SpawnPoint from './track-objects/spawn-point';
import { attachSigns } from './attach-signs';
/*
TODO: add support for WorldRotation and WorldTranslation
@ -45,6 +46,7 @@ export default class SceneryParser {
if(Constants.parser.resolveElectrification) ElectrificationResolver.resolveScenery(scenery);
if(Constants.parser.runTracksConnectionTest) tracksConnectionTest(scenery);
if(Constants.parser.attachSigns) attachSigns(scenery);
if(Constants.parser.logSceneryAfterFinished) console.log(scenery);
return scenery;

View File

@ -7,6 +7,7 @@ export default class Sign extends TrackObject {
type = "Sign";
data;
def;
attached_to = null;
constructor(id, prefab_name, pos, rot, track_id, name, data) {
super(id, prefab_name, pos, rot, track_id, name);

View File

@ -11,6 +11,7 @@ export default class Signal extends TrackObject {
signal_elements;
type = "Signal";
applied = false;
attached_signs = [];
constructor(id, prefab_name, pos, rot, track_id, name, is_spawn, spawn_info, signal_name, signal_elements) {
super(id, prefab_name, pos, rot, track_id, name);

View File

@ -114,6 +114,12 @@ export default class Vector3 {
return dx * dx + dy * dy + dz * dz;
}
distanceSqExcludeY(other) {
const dx = this.x - other.x;
const dz = this.z - other.z;
return dx * dx + dz * dz;
}
distance(other) {
return Math.sqrt(this.distanceSq(other));
}
@ -122,6 +128,15 @@ export default class Vector3 {
return Math.abs(this.x - other.x) + Math.abs(this.y - other.y) + Math.abs(this.z - other.z);
}
toLocal(parentPos, parentRot = Vector3.zero()) {
const localPos = this.sub(parentPos);
return localPos.rotate(parentRot.negate());
}
negate() {
return new Vector3(-this.x, -this.y, -this.z);
}
clone() {
return new Vector3(this.x, this.y, this.z);
}