From 2d655676c9bcafbaaccd0f2655619939bec5b38b Mon Sep 17 00:00:00 2001 From: izawartka <59137928+izawartka@users.noreply.github.com> Date: Fri, 27 Jun 2025 17:40:56 +0200 Subject: [PATCH] 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 --- src/model/attach-signs.js | 98 +++++++++++++++++++++++++++++++ src/model/scenery-object.js | 2 +- src/model/scenery-parser.js | 2 + src/model/track-objects/sign.js | 1 + src/model/track-objects/signal.js | 1 + src/model/vector3.js | 15 +++++ 6 files changed, 118 insertions(+), 1 deletion(-) create mode 100644 src/model/attach-signs.js diff --git a/src/model/attach-signs.js b/src/model/attach-signs.js new file mode 100644 index 0000000..063572c --- /dev/null +++ b/src/model/attach-signs.js @@ -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); + } +} \ No newline at end of file diff --git a/src/model/scenery-object.js b/src/model/scenery-object.js index 7dad410..e60862d 100644 --- a/src/model/scenery-object.js +++ b/src/model/scenery-object.js @@ -21,6 +21,6 @@ export default class SceneryObject { } getRenderBounds() { - return this.pos; + return [this.pos]; } } \ No newline at end of file diff --git a/src/model/scenery-parser.js b/src/model/scenery-parser.js index 07ff8ec..27ef58a 100644 --- a/src/model/scenery-parser.js +++ b/src/model/scenery-parser.js @@ -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; diff --git a/src/model/track-objects/sign.js b/src/model/track-objects/sign.js index d12d413..be497e0 100644 --- a/src/model/track-objects/sign.js +++ b/src/model/track-objects/sign.js @@ -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); diff --git a/src/model/track-objects/signal.js b/src/model/track-objects/signal.js index 18acf2c..1b8716c 100644 --- a/src/model/track-objects/signal.js +++ b/src/model/track-objects/signal.js @@ -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); diff --git a/src/model/vector3.js b/src/model/vector3.js index 107ddbe..1f56871 100644 --- a/src/model/vector3.js +++ b/src/model/vector3.js @@ -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); }