Initial commit
This commit is contained in:
commit
d78e72fc49
2
.gitattributes
vendored
Normal file
2
.gitattributes
vendored
Normal file
@ -0,0 +1,2 @@
|
||||
# Auto detect text files and perform LF normalization
|
||||
* text=auto
|
||||
23
.gitignore
vendored
Normal file
23
.gitignore
vendored
Normal file
@ -0,0 +1,23 @@
|
||||
# See https://help.github.com/articles/ignoring-files/ for more about ignoring files.
|
||||
|
||||
# dependencies
|
||||
/node_modules
|
||||
/.pnp
|
||||
.pnp.js
|
||||
|
||||
# testing
|
||||
/coverage
|
||||
|
||||
# production
|
||||
/build
|
||||
|
||||
# misc
|
||||
.DS_Store
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
11
README.md
Normal file
11
README.md
Normal file
@ -0,0 +1,11 @@
|
||||
# Visualizer2
|
||||
|
||||
Narzędzie do wizualizacji plików scenerii Train Driver 2.
|
||||
Przed załadowaniem pliku scenerii, najpierw przekonwertuj go do lżejszego formatu używając skryptu `tools/scp.py`.
|
||||
|
||||
A tool for visualizing Train Driver 2 scenery files.
|
||||
Before loading a scenery file, make sure to first convert it to lightweight format using `tools/scp.py` script.
|
||||
|
||||
## Author
|
||||
|
||||
masuo / izawartka
|
||||
17558
package-lock.json
generated
Normal file
17558
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
41
package.json
Normal file
41
package.json
Normal file
@ -0,0 +1,41 @@
|
||||
{
|
||||
"name": "visualizer2",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"dependencies": {
|
||||
"-": "^0.0.1",
|
||||
"@testing-library/dom": "^10.4.0",
|
||||
"@testing-library/jest-dom": "^6.6.3",
|
||||
"@testing-library/react": "^16.2.0",
|
||||
"@testing-library/user-event": "^13.5.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0",
|
||||
"react-leaflet": "^5.0.0",
|
||||
"react-scripts": "^5.0.1"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "react-scripts start",
|
||||
"build": "react-scripts build",
|
||||
"test": "react-scripts test",
|
||||
"eject": "react-scripts eject"
|
||||
},
|
||||
"eslintConfig": {
|
||||
"extends": [
|
||||
"react-app",
|
||||
"react-app/jest"
|
||||
]
|
||||
},
|
||||
"browserslist": {
|
||||
"production": [
|
||||
">0.2%",
|
||||
"not dead",
|
||||
"not op_mini all"
|
||||
],
|
||||
"development": [
|
||||
"last 1 chrome version",
|
||||
"last 1 firefox version",
|
||||
"last 1 safari version"
|
||||
]
|
||||
}
|
||||
}
|
||||
BIN
public/favicon.png
Normal file
BIN
public/favicon.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 26 KiB |
20
public/index.html
Normal file
20
public/index.html
Normal file
@ -0,0 +1,20 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="utf-8" />
|
||||
<link rel="icon" href="%PUBLIC_URL%/favicon.png" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1" />
|
||||
<meta name="theme-color" content="#000000" />
|
||||
<title>Visualizer2</title>
|
||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css"
|
||||
integrity="sha256-p4NxAoJBhIIN+hmNHrzRCf9tD/miZyoHS5obTRR9BMY="
|
||||
crossorigin=""/>
|
||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"
|
||||
integrity="sha256-20nQCchB9co0qIjJZRGuk2/Z9VM+kNiyxNV1lvTlZBo="
|
||||
crossorigin=""></script>
|
||||
</head>
|
||||
<body>
|
||||
<noscript>You need to enable JavaScript to run this app.</noscript>
|
||||
<div id="root"></div>
|
||||
</body>
|
||||
</html>
|
||||
15
src/components/App.css
Normal file
15
src/components/App.css
Normal file
@ -0,0 +1,15 @@
|
||||
.App {
|
||||
display: flex;
|
||||
width: 100vw;
|
||||
height: 100vh;
|
||||
flex-direction: column;
|
||||
flex-wrap: nowrap;
|
||||
}
|
||||
|
||||
.side-menu-split {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
34
src/components/App.js
Normal file
34
src/components/App.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { useState } from 'react';
|
||||
import Map from './map/Map';
|
||||
import Toolbar from './toolbar/Toolbar';
|
||||
import MainContext from '../contexts/MainContext';
|
||||
import './App.css';
|
||||
import SideMenu from './sidemenu/SideMenu';
|
||||
import SettingsManager from './SettingsManager';
|
||||
|
||||
function App() {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [scenery, setScenery] = useState(null);
|
||||
const [sideMenuOpen, setSideMenuOpen] = useState(false);
|
||||
|
||||
return (
|
||||
<SettingsManager>
|
||||
<MainContext.Provider value={{
|
||||
scenery, setScenery,
|
||||
isLoading, setIsLoading,
|
||||
sideMenuOpen, setSideMenuOpen
|
||||
}}>
|
||||
<div className="App">
|
||||
<Toolbar />
|
||||
<div className='side-menu-split'>
|
||||
<Map />
|
||||
<SideMenu />
|
||||
</div>
|
||||
</div>
|
||||
</MainContext.Provider>
|
||||
</SettingsManager>
|
||||
);
|
||||
}
|
||||
|
||||
export default App;
|
||||
|
||||
30
src/components/SettingsManager.js
Normal file
30
src/components/SettingsManager.js
Normal file
@ -0,0 +1,30 @@
|
||||
import { useCallback, useEffect, useState } from "react";
|
||||
import SettingsContext from "../contexts/SettingsContext";
|
||||
import Constants from "../helpers/constants";
|
||||
|
||||
export default function SettingsManager(props) {
|
||||
const [ layers, setLayers ] = useState([]);
|
||||
const [ settingsLoaded, setSettingsLoaded ] = useState(false);
|
||||
|
||||
const loadLayersSettings = useCallback(() => {
|
||||
setLayers(Constants.layers.filter(layer => layer.default === true).map(layer => layer.id).reduce((acc, layerId) => {
|
||||
acc[layerId] = true;
|
||||
return acc;
|
||||
}, {}));
|
||||
}, [setLayers]);
|
||||
|
||||
useEffect(() => {
|
||||
loadLayersSettings();
|
||||
setSettingsLoaded(true);
|
||||
}, [loadLayersSettings]);
|
||||
|
||||
if (!settingsLoaded) return null;
|
||||
|
||||
return (
|
||||
<SettingsContext.Provider value={{
|
||||
layers, setLayers
|
||||
}}>
|
||||
{ props.children }
|
||||
</SettingsContext.Provider>
|
||||
);
|
||||
}
|
||||
36
src/components/map/Map.css
Normal file
36
src/components/map/Map.css
Normal file
@ -0,0 +1,36 @@
|
||||
.map {
|
||||
flex: 1;
|
||||
background-color: black;
|
||||
}
|
||||
|
||||
.map svg path.track {
|
||||
stroke-width: 1.44px;
|
||||
stroke: #aaa;
|
||||
}
|
||||
|
||||
.map svg text {
|
||||
font-size: 2px;
|
||||
text-shadow: 0 0 16px #000;
|
||||
fill: #eee;
|
||||
}
|
||||
|
||||
.map svg text.switch-name {
|
||||
fill: #eee;
|
||||
}
|
||||
|
||||
.map svg text.isolation-id {
|
||||
fill: #ffa;
|
||||
}
|
||||
|
||||
.map svg g.track-object circle {
|
||||
fill: #eee;
|
||||
}
|
||||
|
||||
.map svg g.track-object text {
|
||||
fill: #eee;
|
||||
display: none;
|
||||
}
|
||||
|
||||
.map svg g.track-object:hover text {
|
||||
display: block;
|
||||
}
|
||||
31
src/components/map/Map.js
Normal file
31
src/components/map/Map.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { useContext, useMemo } from 'react';
|
||||
import MainContext from '../../contexts/MainContext';
|
||||
import ZoomPanWrapper from './ZoomPanWrapper';
|
||||
import './Map.css';
|
||||
import SceneryRoot from './SceneryRoot';
|
||||
|
||||
export default function Map(props) {
|
||||
const {scenery} = useContext(MainContext);
|
||||
|
||||
const sceneryRoot = useMemo(() => {
|
||||
if (!scenery) return null;
|
||||
const bounds = scenery.getBounds();
|
||||
const width = (bounds.maxX - bounds.minX);
|
||||
const height = (bounds.maxZ - bounds.minZ);
|
||||
|
||||
return (
|
||||
<ZoomPanWrapper
|
||||
contentWidth={width}
|
||||
contentHeight={height}
|
||||
>
|
||||
<SceneryRoot />
|
||||
</ZoomPanWrapper>
|
||||
);
|
||||
}, [scenery]);
|
||||
|
||||
return (
|
||||
<div className='map'>
|
||||
{sceneryRoot}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
34
src/components/map/ObjectsRenderQueue.js
Normal file
34
src/components/map/ObjectsRenderQueue.js
Normal file
@ -0,0 +1,34 @@
|
||||
import TrackRenderer from './object-renderers/TrackRenderer';
|
||||
import SwitchNameRenderer from './object-renderers/SwitchNameRenderer';
|
||||
import IsolationIdRenderer from './object-renderers/IsolationIdRenderer';
|
||||
import TrackObjectRenderer from './object-renderers/TrackObjectRenderer';
|
||||
|
||||
const ObjectRendererQueue = [
|
||||
{
|
||||
'name': 'tracks',
|
||||
'category': 'tracks',
|
||||
'renderer': TrackRenderer,
|
||||
'cond': (settings) => settings.layers['tracks']
|
||||
},
|
||||
{
|
||||
'name': 'isolations-ids',
|
||||
'category': 'tracks',
|
||||
'renderer': IsolationIdRenderer,
|
||||
'cond': (settings) => settings.layers['isolations-ids']
|
||||
},
|
||||
{
|
||||
'name': 'switches-names',
|
||||
'category': 'switches',
|
||||
'type': 'Switch',
|
||||
'renderer': SwitchNameRenderer,
|
||||
'cond': (settings) => settings.layers['switches-names']
|
||||
},
|
||||
{
|
||||
'name': 'track-objects',
|
||||
'category': 'track-objects',
|
||||
'renderer': TrackObjectRenderer,
|
||||
'cond': (settings) => settings.layers['track-objects']
|
||||
}
|
||||
]
|
||||
|
||||
export default ObjectRendererQueue;
|
||||
34
src/components/map/SceneryRoot.js
Normal file
34
src/components/map/SceneryRoot.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { useContext, useMemo } from "react";
|
||||
import MainContext from "../../contexts/MainContext";
|
||||
import ObjectsRenderQueue from "./ObjectsRenderQueue";
|
||||
import SettingsContext from "../../contexts/SettingsContext";
|
||||
|
||||
export default function SceneryRoot(props) {
|
||||
const { scenery } = useContext(MainContext);
|
||||
const settings = useContext(SettingsContext);
|
||||
|
||||
const content = useMemo(() => {
|
||||
if(!scenery) return null;
|
||||
|
||||
return ObjectsRenderQueue.map((queueItem) => {
|
||||
const { name, category, type, types, cond} = queueItem;
|
||||
const RendererComponent = queueItem.renderer;
|
||||
if(cond && !cond(settings)) return [null];
|
||||
|
||||
const objects = scenery.objects[category] ?? null;
|
||||
if(!objects) return [null];
|
||||
|
||||
return Object.values(objects).map((object) => {
|
||||
if((type !== undefined && type !== object.type) || (types !== undefined && !types.includes(object.type))) return null;
|
||||
|
||||
return <RendererComponent key={`${name}-${object.id}`} object={object} />;
|
||||
});
|
||||
}).flat();
|
||||
}, [scenery, settings]);
|
||||
|
||||
return (
|
||||
<g className='scenery-root'>
|
||||
{content}
|
||||
</g>
|
||||
)
|
||||
}
|
||||
11
src/components/map/ZoomPanWrapper.css
Normal file
11
src/components/map/ZoomPanWrapper.css
Normal file
@ -0,0 +1,11 @@
|
||||
.zoom-pan-wrapper {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.zoom-pan-wrapper svg {
|
||||
cursor: grab;
|
||||
}
|
||||
|
||||
.zoom-pan-wrapper svg:active {
|
||||
cursor: grabbing;
|
||||
}
|
||||
79
src/components/map/ZoomPanWrapper.js
Normal file
79
src/components/map/ZoomPanWrapper.js
Normal file
@ -0,0 +1,79 @@
|
||||
import React, { useState, useRef } from 'react';
|
||||
import './ZoomPanWrapper.css';
|
||||
import Constants from '../../helpers/constants';
|
||||
|
||||
export default function ZoomPanWrapper({
|
||||
contentWidth = 1000,
|
||||
contentHeight = 1000,
|
||||
children,
|
||||
}) {
|
||||
const [viewBox, setViewBox] = useState({ x: 0, y: 0, w: contentWidth, h: contentHeight });
|
||||
const svgRef = useRef(null);
|
||||
const isPanning = useRef(false);
|
||||
const panStart = useRef({ x: 0, y: 0 });
|
||||
const vbStart = useRef(viewBox);
|
||||
|
||||
const onWheel = (e) => {
|
||||
const { x, y, w, h } = viewBox;
|
||||
const svgRect = svgRef.current.getBoundingClientRect();
|
||||
const offsetX = e.clientX - svgRect.left;
|
||||
const offsetY = e.clientY - svgRect.top;
|
||||
|
||||
// mouse position in svg coordinates
|
||||
const mx = (offsetX / svgRect.width) * w + x;
|
||||
const my = (offsetY / svgRect.height) * h + y;
|
||||
|
||||
// zoom factor
|
||||
const scale = 1.0 + e.deltaY * Constants.map.zoomSensitivity;
|
||||
const newW = w * scale;
|
||||
const newH = h * scale;
|
||||
|
||||
// adjust x,y so zoom centers at mouse
|
||||
const newX = mx - (offsetX / svgRect.width) * newW;
|
||||
const newY = my - (offsetY / svgRect.height) * newH;
|
||||
|
||||
setViewBox({ x: newX, y: newY, w: newW, h: newH });
|
||||
};
|
||||
|
||||
const onMouseDown = (e) => {
|
||||
e.preventDefault();
|
||||
isPanning.current = true;
|
||||
panStart.current = { x: e.clientX, y: e.clientY };
|
||||
vbStart.current = viewBox;
|
||||
};
|
||||
|
||||
const onMouseMove = (e) => {
|
||||
if (!isPanning.current) return;
|
||||
e.preventDefault();
|
||||
const dx = ((e.clientX - panStart.current.x) / svgRef.current.clientWidth) * vbStart.current.w;
|
||||
const dy = ((e.clientY - panStart.current.y) / svgRef.current.clientHeight) * vbStart.current.h;
|
||||
setViewBox({
|
||||
x: vbStart.current.x - dx,
|
||||
y: vbStart.current.y - dy,
|
||||
w: vbStart.current.w,
|
||||
h: vbStart.current.h,
|
||||
});
|
||||
};
|
||||
|
||||
const onMouseUp = () => {
|
||||
isPanning.current = false;
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="zoom-pan-wrapper">
|
||||
<svg
|
||||
ref={svgRef}
|
||||
width="100%"
|
||||
height="100%"
|
||||
viewBox={`${viewBox.x} ${viewBox.y} ${viewBox.w} ${viewBox.h}`}
|
||||
onWheel={onWheel}
|
||||
onMouseDown={onMouseDown}
|
||||
onMouseMove={onMouseMove}
|
||||
onMouseUp={onMouseUp}
|
||||
onMouseLeave={onMouseUp}
|
||||
>
|
||||
{children}
|
||||
</svg>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
24
src/components/map/object-renderers/IsolationIdRenderer.js
Normal file
24
src/components/map/object-renderers/IsolationIdRenderer.js
Normal file
@ -0,0 +1,24 @@
|
||||
export default function IsolationIdRenderer(props) {
|
||||
const { object } = props;
|
||||
|
||||
const x1 = object.points?.x1;
|
||||
const z1 = -object.points?.z1;
|
||||
const x2 = object.points?.x2 ?? x1;
|
||||
const z2 = -object.points?.z2 ?? z1;
|
||||
const x = (x1 + x2) / 2;
|
||||
const z = (z1 + z2) / 2;
|
||||
const text = object.id_isolation ?? '';
|
||||
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={z}
|
||||
textAnchor='middle'
|
||||
id={`isolation-id-${object.id}`}
|
||||
style={{ userSelect: "none" }}
|
||||
className="isolation-id"
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
);
|
||||
}
|
||||
20
src/components/map/object-renderers/SwitchNameRenderer.js
Normal file
20
src/components/map/object-renderers/SwitchNameRenderer.js
Normal file
@ -0,0 +1,20 @@
|
||||
export default function SwitchNameRenderer(props) {
|
||||
const { object } = props;
|
||||
|
||||
const x = object.x;
|
||||
const z = -object.z;
|
||||
const text = object.id_switch;
|
||||
|
||||
return (
|
||||
<text
|
||||
x={x}
|
||||
y={z}
|
||||
textAnchor='middle'
|
||||
id={`switch-${object.id}`}
|
||||
style={{ userSelect: "none" }}
|
||||
className="switch-name"
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
);
|
||||
}
|
||||
26
src/components/map/object-renderers/TrackObjectRenderer.js
Normal file
26
src/components/map/object-renderers/TrackObjectRenderer.js
Normal file
@ -0,0 +1,26 @@
|
||||
export default function TrackObjectRenderer(props) {
|
||||
const { object } = props;
|
||||
|
||||
const x = object.x;
|
||||
const z = -object.z;
|
||||
const text = `${object.prefab_name};${object.name}`
|
||||
|
||||
return (
|
||||
<g className="track-object">
|
||||
<circle
|
||||
cx={x}
|
||||
cy={z}
|
||||
r='1px'
|
||||
></circle>
|
||||
<text
|
||||
x={x}
|
||||
y={z}
|
||||
id={`track-object-${object.id}`}
|
||||
style={{ userSelect: "none" }}
|
||||
enableBackground="new 0 0 100 100"
|
||||
>
|
||||
{text}
|
||||
</text>
|
||||
</g>
|
||||
);
|
||||
}
|
||||
74
src/components/map/object-renderers/TrackRenderer.js
Normal file
74
src/components/map/object-renderers/TrackRenderer.js
Normal file
@ -0,0 +1,74 @@
|
||||
|
||||
import Constants from "../../../helpers/constants";
|
||||
|
||||
export default function TrackRenderer(props) {
|
||||
const { object } = props;
|
||||
|
||||
switch (object.type) {
|
||||
case "StandardTrack":
|
||||
case "PointTrack":
|
||||
return <StandardTrackR {...props} />;
|
||||
case "BezierTrack":
|
||||
return <BezierTrackR {...props} />;
|
||||
default:
|
||||
console.warn(`No renderer component for track type: ${object.type}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function StandardTrackR(props) {
|
||||
const { object } = props;
|
||||
|
||||
const x1 = object.points.x1;
|
||||
const z1 = -object.points.z1;
|
||||
const x2 = object.points.x2;
|
||||
const z2 = -object.points.z2;
|
||||
const ra = Math.abs(object.r);
|
||||
|
||||
let d;
|
||||
if (object.r < 0) {
|
||||
d = `M${x1},${z1} A${ra} ${ra} 0 0 1 ${x2},${z2}`;
|
||||
} else if (object.r > 0) {
|
||||
d = `M${x2},${z2} A${ra} ${ra} 0 0 1 ${x1},${z1}`;
|
||||
} else {
|
||||
d = `M${x1},${z1} L${x2},${z2}`;
|
||||
}
|
||||
|
||||
const color = Constants.map.useTrackColors ? (
|
||||
object.r === 0 ? "rgb(255, 255, 255)" : "rgb(255, 0, 255)"
|
||||
): undefined;
|
||||
|
||||
return (
|
||||
<path
|
||||
d={d}
|
||||
id={`track-${object.id}`}
|
||||
stroke={color}
|
||||
className="track"
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function BezierTrackR(props) {
|
||||
const { object } = props;
|
||||
|
||||
const x1 = object.points.x1;
|
||||
const z1 = -object.points.z1;
|
||||
const cx1 = object.points.cx1+object.points.x1;
|
||||
const cz1 = -object.points.cz1-object.points.z1;
|
||||
const cx2 = object.points.cx2+object.points.x2;
|
||||
const cz2 = -object.points.cz2-object.points.z2;
|
||||
const x2 = object.points.x2;
|
||||
const z2 = -object.points.z2;
|
||||
|
||||
const d = `M${x1},${z1} C${cx1},${cz1} ${cx2},${cz2} ${x2},${z2}`;
|
||||
const color = Constants.map.useTrackColors ? "rgb(0, 255, 0)" : undefined;
|
||||
|
||||
return (
|
||||
<path
|
||||
d={d}
|
||||
id={`btrack-${object.id}`}
|
||||
stroke={color}
|
||||
className="track"
|
||||
/>
|
||||
);
|
||||
}
|
||||
34
src/components/sidemenu/LayersMenu.js
Normal file
34
src/components/sidemenu/LayersMenu.js
Normal file
@ -0,0 +1,34 @@
|
||||
import { useContext } from "react";
|
||||
import SettingsContext from "../../contexts/SettingsContext";
|
||||
import Constants from "../../helpers/constants";
|
||||
|
||||
export default function LayersMenu(props) {
|
||||
const { layers, setLayers } = useContext(SettingsContext);
|
||||
|
||||
const toggleLayer = (layer) => {
|
||||
setLayers((prevLayers) => ({
|
||||
...prevLayers,
|
||||
[layer]: !prevLayers[layer]
|
||||
}));
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="layers-menu">
|
||||
<h3>Layers</h3>
|
||||
<ul>
|
||||
{ Constants.layers.map((layer) => (
|
||||
<li key={layer.id}>
|
||||
<label>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={layers[layer.id] || false}
|
||||
onChange={() => toggleLayer(layer.id)}
|
||||
/>
|
||||
{layer.name}
|
||||
</label>
|
||||
</li>
|
||||
)) }
|
||||
</ul>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
13
src/components/sidemenu/SideMenu.css
Normal file
13
src/components/sidemenu/SideMenu.css
Normal file
@ -0,0 +1,13 @@
|
||||
.side-menu-wrapper {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.side-menu {
|
||||
border-left: 1px solid #333;
|
||||
padding: 10px;
|
||||
position: fixed;
|
||||
right: 0;
|
||||
height: 100%;
|
||||
background-color: #111;
|
||||
z-index: 2000;
|
||||
}
|
||||
20
src/components/sidemenu/SideMenu.js
Normal file
20
src/components/sidemenu/SideMenu.js
Normal file
@ -0,0 +1,20 @@
|
||||
import {useContext} from 'react';
|
||||
import MainContext from '../../contexts/MainContext';
|
||||
import './SideMenu.css';
|
||||
import LayersMenu from './LayersMenu';
|
||||
|
||||
export default function SideMenu() {
|
||||
const { sideMenuOpen } = useContext(MainContext);
|
||||
|
||||
if (!sideMenuOpen) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='side-menu-wrapper'>
|
||||
<div className='side-menu'>
|
||||
<LayersMenu />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
31
src/components/toolbar/FileSelect.js
Normal file
31
src/components/toolbar/FileSelect.js
Normal file
@ -0,0 +1,31 @@
|
||||
import { useContext } from "react";
|
||||
import MainContext from "../../contexts/MainContext";
|
||||
import Scenery from "../../model/scenery";
|
||||
|
||||
export default function FileSelect() {
|
||||
const {setScenery, setIsLoading} = useContext(MainContext);
|
||||
|
||||
const handleFileChange = (event) => {
|
||||
const file = event.target.files[0];
|
||||
if (!file) return;
|
||||
|
||||
setIsLoading(true);
|
||||
const reader = new FileReader();
|
||||
reader.readAsText(file);
|
||||
reader.onload = (e) => {
|
||||
setIsLoading(false);
|
||||
if(!e.target || !e.target.result) {
|
||||
console.error("File reading failed or no content found.");
|
||||
}
|
||||
|
||||
const scenery = Scenery.fromText(e.target.result);
|
||||
setScenery(scenery);
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className='file-select'>
|
||||
<input type='file' id='file-input' onChange={handleFileChange} />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
15
src/components/toolbar/LoadingIndicator.js
Normal file
15
src/components/toolbar/LoadingIndicator.js
Normal file
@ -0,0 +1,15 @@
|
||||
import { useContext } from 'react'
|
||||
import MainContext from '../../contexts/MainContext'
|
||||
|
||||
export default function LoadingIndicator() {
|
||||
const { isLoading } = useContext(MainContext);
|
||||
|
||||
const loadingText = isLoading ? 'Loading...' : 'Ready';
|
||||
const className = 'loading-indicator ' + (isLoading ? 'loading' : 'ready');
|
||||
|
||||
return (
|
||||
<div className={className}>
|
||||
{loadingText}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
18
src/components/toolbar/SideMenuToggle.js
Normal file
18
src/components/toolbar/SideMenuToggle.js
Normal file
@ -0,0 +1,18 @@
|
||||
import { useContext } from 'react';
|
||||
import MainContext from '../../contexts/MainContext';
|
||||
|
||||
export default function SideMenuToggle() {
|
||||
const { sideMenuOpen, setSideMenuOpen } = useContext(MainContext);
|
||||
|
||||
const toggleSideMenu = () => {
|
||||
setSideMenuOpen((prev) => !prev);
|
||||
}
|
||||
|
||||
return (
|
||||
<div className='side-menu-toggle'>
|
||||
<button onClick={toggleSideMenu}>
|
||||
{sideMenuOpen ? 'Hide Menu' : 'Show Menu'}
|
||||
</button>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
48
src/components/toolbar/Toolbar.css
Normal file
48
src/components/toolbar/Toolbar.css
Normal file
@ -0,0 +1,48 @@
|
||||
.toolbar {
|
||||
display: flex;
|
||||
flex-direction: row;
|
||||
flex-wrap: nowrap;
|
||||
gap: 10px;
|
||||
padding: 10px;
|
||||
border-bottom: 1px solid #333;
|
||||
}
|
||||
|
||||
.toolbar-spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.toolbar-cont {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
@keyframes fadeOut {
|
||||
0% { opacity: 1; pointer-events: auto; }
|
||||
100% { opacity: 0; pointer-events: none; }
|
||||
}
|
||||
|
||||
.loading-indicator-cont {
|
||||
height: 0;
|
||||
}
|
||||
|
||||
.loading-indicator {
|
||||
height: fit-content;
|
||||
padding: 4px 8px;
|
||||
color: white;
|
||||
text-align: center;
|
||||
opacity: 0;
|
||||
transition: opacity 1s ease-in-out;
|
||||
z-index: 1000;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.loading-indicator.loading {
|
||||
background-color: rgb(151, 105, 19);
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
.loading-indicator.ready {
|
||||
background-color: rgb(85, 204, 85);
|
||||
opacity: 1;
|
||||
animation: fadeOut 1s ease-in-out 2s forwards; /* Start fading out after 2 seconds */
|
||||
}
|
||||
20
src/components/toolbar/Toolbar.js
Normal file
20
src/components/toolbar/Toolbar.js
Normal file
@ -0,0 +1,20 @@
|
||||
import React from 'react';
|
||||
import './Toolbar.css';
|
||||
import LoadingIndicator from './LoadingIndicator';
|
||||
import FileSelect from './FileSelect';
|
||||
import SideMenuToggle from './SideMenuToggle';
|
||||
|
||||
export default function Toolbar() {
|
||||
return (
|
||||
<div className='toolbar-cont'>
|
||||
<div className='toolbar'>
|
||||
<FileSelect />
|
||||
<div className='toolbar-spacer'></div>
|
||||
<SideMenuToggle />
|
||||
</div>
|
||||
<div className='loading-indicator-cont'>
|
||||
<LoadingIndicator />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
4
src/contexts/MainContext.js
Normal file
4
src/contexts/MainContext.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
const MainContext = createContext();
|
||||
export default MainContext;
|
||||
4
src/contexts/SettingsContext.js
Normal file
4
src/contexts/SettingsContext.js
Normal file
@ -0,0 +1,4 @@
|
||||
import { createContext } from 'react';
|
||||
|
||||
const SettingsContext = createContext();
|
||||
export default SettingsContext;
|
||||
30
src/helpers/constants.js
Normal file
30
src/helpers/constants.js
Normal file
@ -0,0 +1,30 @@
|
||||
const Constants = {
|
||||
map: {
|
||||
useTrackColors: false,
|
||||
zoomSensitivity: 0.002,
|
||||
},
|
||||
layers: [
|
||||
{
|
||||
id: 'tracks',
|
||||
name: 'Tracks',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'switches-names',
|
||||
name: 'Switches names',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'isolations-ids',
|
||||
name: 'Isolations IDs',
|
||||
default: true,
|
||||
},
|
||||
{
|
||||
id: 'track-objects',
|
||||
name: 'Track objects',
|
||||
default: false,
|
||||
}
|
||||
]
|
||||
};
|
||||
|
||||
export default Constants;
|
||||
8
src/index.css
Normal file
8
src/index.css
Normal file
@ -0,0 +1,8 @@
|
||||
body {
|
||||
padding: 0;
|
||||
margin: 0;
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
|
||||
overflow: hidden;
|
||||
background-color: #111;
|
||||
color: #eee;
|
||||
}
|
||||
9
src/index.js
Normal file
9
src/index.js
Normal file
@ -0,0 +1,9 @@
|
||||
import React from 'react';
|
||||
import ReactDOM from 'react-dom/client';
|
||||
import './index.css';
|
||||
import App from './components/App';
|
||||
|
||||
const root = ReactDOM.createRoot(document.getElementById('root'));
|
||||
root.render(
|
||||
<App />
|
||||
);
|
||||
62
src/model/bezier-track.js
Normal file
62
src/model/bezier-track.js
Normal file
@ -0,0 +1,62 @@
|
||||
import Track from "./track";
|
||||
|
||||
export default class BezierTrack extends Track
|
||||
{
|
||||
type = "BezierTrack";
|
||||
points = {
|
||||
x1: 0, y1: 0, z1: 0, // start
|
||||
cx1: 0, cy1: 0, cz1: 0, // control 1
|
||||
cx2: 0, cy2: 0, cz2: 0, // control 2
|
||||
x2: 0, y2: 0, z2: 0 // end
|
||||
};
|
||||
|
||||
constructor(id, x1, y1, z1, cx1, cy1, cz1, x2, y2, z2, cx2, cy2, cz2, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed) {
|
||||
super(id, x1, y1, z1, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed);
|
||||
|
||||
Object.assign(this.points, {
|
||||
x1, y1, z1, // start
|
||||
cx1, cy1, cz1, // control 1
|
||||
cx2, cy2, cz2, // control 2
|
||||
x2, y2, z2 // end
|
||||
});
|
||||
}
|
||||
|
||||
static fromText(text) {
|
||||
const values = text.split(";");
|
||||
const track = new BezierTrack(
|
||||
values[1], // id
|
||||
parseFloat(values[3]), // x1
|
||||
parseFloat(values[4]), // y1
|
||||
parseFloat(values[5]), // z1
|
||||
parseFloat(values[6]), // cx1
|
||||
parseFloat(values[7]), // cy1
|
||||
parseFloat(values[8]), // cz1
|
||||
parseFloat(values[9]), // x2
|
||||
parseFloat(values[10]), // y2
|
||||
parseFloat(values[11]), // z2
|
||||
parseFloat(values[12]), // cx2
|
||||
parseFloat(values[13]), // cy2
|
||||
parseFloat(values[14]), // cz2
|
||||
0, // rot ??
|
||||
0, // len ??
|
||||
0, // r ??
|
||||
values[15], // previd
|
||||
values[16], // nextid
|
||||
values[17], // id_station
|
||||
values[21], // id_isolation
|
||||
parseFloat(values[24]), // maxspeed
|
||||
parseFloat(values[25]) // derailspeed
|
||||
);
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
getRenderBounds() {
|
||||
return [
|
||||
{ x: this.points.x1, z: this.points.z1 }, // start
|
||||
{ x: this.points.x2, z: this.points.z2 }, // end
|
||||
{ x: this.points.cx1, z: this.points.cz1 }, // control 1
|
||||
{ x: this.points.cx2, z: this.points.cz2 } // control 2
|
||||
];
|
||||
}
|
||||
}
|
||||
30
src/model/point-track.js
Normal file
30
src/model/point-track.js
Normal file
@ -0,0 +1,30 @@
|
||||
import Track from './track';
|
||||
|
||||
export default class PointTrack extends Track
|
||||
{
|
||||
type = "PointTrack";
|
||||
points = {
|
||||
x1: 0, y1: 0, z1: 0, // start
|
||||
x2: 0, y2: 0, z2: 0 // end
|
||||
};
|
||||
|
||||
constructor(id, x1, y1, z1, x2, y2, z2, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed) {
|
||||
const rot = Math.atan2(x2-x1, z2-z1);
|
||||
// length is unknown
|
||||
super(id, x1, y1, z1, rot, null, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed);
|
||||
|
||||
Object.assign(this.points, {
|
||||
x1, y1, z1,
|
||||
x2, y2, z2
|
||||
});
|
||||
|
||||
this.type="PointTrack";
|
||||
}
|
||||
|
||||
getRenderBounds() {
|
||||
return [
|
||||
{ x: this.points.x1, z: this.points.z1 }, // start
|
||||
{ x: this.points.x2, z: this.points.z2 } // end
|
||||
];
|
||||
}
|
||||
}
|
||||
107
src/model/scenery.js
Normal file
107
src/model/scenery.js
Normal file
@ -0,0 +1,107 @@
|
||||
import Switch from './switch';
|
||||
import StandardTrack from './standard-track';
|
||||
import BezierTrack from './bezier-track';
|
||||
import TrackObject from './track-object';
|
||||
|
||||
export default class Scenery
|
||||
{
|
||||
objects = {};
|
||||
bounds = { minX: Infinity, minZ: Infinity, maxX: -Infinity, maxZ: -Infinity };
|
||||
|
||||
getBounds () {
|
||||
return { ...this.bounds };
|
||||
}
|
||||
|
||||
static fromText(scText) {
|
||||
const lines = scText.split("\n").map(line => line.trim());
|
||||
const scenery = new Scenery();
|
||||
|
||||
lines.forEach(line => {
|
||||
if(!line?.length) return;
|
||||
const object = Scenery._parseObject(line);
|
||||
if(!object) return; // skip null objects
|
||||
scenery.addObject(object);
|
||||
});
|
||||
|
||||
scenery.applySwitches();
|
||||
console.log(scenery);
|
||||
|
||||
return scenery;
|
||||
}
|
||||
|
||||
addObject(object) {
|
||||
const category = object.category || 'misc';
|
||||
if(!this.objects[category]) {
|
||||
this.objects[category] = {};
|
||||
}
|
||||
|
||||
this.objects[category][object.id] = object;
|
||||
|
||||
if(object.getRenderBounds) {
|
||||
this._updateBounds(object.getRenderBounds());
|
||||
}
|
||||
}
|
||||
|
||||
applySwitches() {
|
||||
Object.values(this.objects.switches ?? []).forEach(sw => {
|
||||
sw.applySwitch(this);
|
||||
});
|
||||
}
|
||||
|
||||
getObject(category, id) {
|
||||
if(!this.objects[category]) return null;
|
||||
return this.objects[category][id] ?? null;
|
||||
}
|
||||
|
||||
static _parseObject(text) {
|
||||
const type = text.split(";", 2)[0];
|
||||
switch(type) {
|
||||
case 'Track':
|
||||
return Scenery._parseTrack(text);
|
||||
case 'TrackStructure':
|
||||
return Switch.fromText(text);
|
||||
case 'TrackObject':
|
||||
return Scenery._parseTrackObject(text);
|
||||
default:
|
||||
console.warn(`Unknown object type: ${type}`);
|
||||
return null;
|
||||
};
|
||||
}
|
||||
|
||||
static _parseTrack(text) {
|
||||
/// TODO: remove
|
||||
const values = text.split(";");
|
||||
if(values.includes("none-slp,trans-mat,trans-mat")) return null; // skip invisible tracks
|
||||
|
||||
const trackType = text.split(';', 4)[2];
|
||||
switch(trackType) {
|
||||
case 'Track':
|
||||
return StandardTrack.fromText(text);
|
||||
case 'BTrack':
|
||||
return BezierTrack.fromText(text);
|
||||
default:
|
||||
console.warn(`Unknown track type: ${trackType}`);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
static _parseTrackObject(text) {
|
||||
const prefabName = text.split(";", 4)[2];
|
||||
switch(prefabName) {
|
||||
default:
|
||||
return TrackObject.fromText(text);
|
||||
}
|
||||
}
|
||||
|
||||
_updateBounds(objBounds) {
|
||||
if(!objBounds || !Array.isArray(objBounds)) return;
|
||||
|
||||
objBounds.forEach(point => {
|
||||
if(point.x < this.bounds.minX) this.bounds.minX = point.x;
|
||||
if(point.z < this.bounds.minZ) this.bounds.minZ = point.z;
|
||||
if(point.x > this.bounds.maxX) this.bounds.maxX = point.x;
|
||||
if(point.z > this.bounds.maxZ) this.bounds.maxZ = point.z;
|
||||
}
|
||||
);
|
||||
}
|
||||
}
|
||||
65
src/model/standard-track.js
Normal file
65
src/model/standard-track.js
Normal file
@ -0,0 +1,65 @@
|
||||
import Track from "./track";
|
||||
|
||||
export default class StandardTrack extends Track
|
||||
{
|
||||
type = "StandardTrack";
|
||||
points = {
|
||||
x1: 0, z1: 0, // start
|
||||
x2: 0, z2: 0, // end
|
||||
cx: 0, cz: 0 // circle center
|
||||
};
|
||||
|
||||
constructor(id, x, y, z, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed) {
|
||||
super(id, x, y, z, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed);
|
||||
this._calcPoints();
|
||||
}
|
||||
|
||||
static fromText(text) {
|
||||
const values = text.split(";");
|
||||
const track = new StandardTrack(
|
||||
values[1], // id
|
||||
parseFloat(values[3]), // x
|
||||
parseFloat(values[4]), // y
|
||||
parseFloat(values[5]), // z
|
||||
parseFloat(values[7]), // rot
|
||||
parseFloat(values[9]), // len
|
||||
parseFloat(values[10]), // r
|
||||
values[11], // previd
|
||||
values[12], // nextid
|
||||
values[13], // id_station
|
||||
values[17], // id_isolation
|
||||
parseFloat(values[20]), // maxspeed
|
||||
parseFloat(values[21]) // derailspeed
|
||||
);
|
||||
|
||||
return track;
|
||||
}
|
||||
|
||||
_calcPoints() {
|
||||
const rotRad = this.rot * Math.PI / 180;
|
||||
|
||||
this.points = {
|
||||
x1: this.x, // start
|
||||
z1: this.z,
|
||||
x2: this.x + this.len * Math.sin(rotRad), // end
|
||||
z2: this.z + this.len * Math.cos(rotRad),
|
||||
cx: this.x, // circle center (default)
|
||||
cz: this.z // circle center (default)
|
||||
};
|
||||
|
||||
if (this.r !== 0) {
|
||||
let trp2 = rotRad + Math.PI / 2;
|
||||
this.points.cx = this.x - this.r * Math.sin(trp2); // circle center
|
||||
this.points.cz = this.z - this.r * Math.cos(trp2);
|
||||
this.points.x2 = this.points.cx + this.r * Math.sin(trp2 - this.len / this.r); // end (modify)
|
||||
this.points.z2 = this.points.cz + this.r * Math.cos(trp2 - this.len / this.r);
|
||||
}
|
||||
}
|
||||
|
||||
gerRenderBounds() {
|
||||
return [
|
||||
{ x: this.points.x1, z: this.points.z1 }, // start
|
||||
{ x: this.points.x2, z: this.points.z2 }, // end
|
||||
];
|
||||
}
|
||||
}
|
||||
126
src/model/switch.js
Normal file
126
src/model/switch.js
Normal file
@ -0,0 +1,126 @@
|
||||
import PointTrack from "./point-track.js";
|
||||
|
||||
export default class Switch {
|
||||
id;
|
||||
model;
|
||||
x;
|
||||
y;
|
||||
z;
|
||||
rot;
|
||||
data;
|
||||
id_isolation;
|
||||
id_switch;
|
||||
maxspeed;
|
||||
derailspeed;
|
||||
category = "switches";
|
||||
type = "Switch";
|
||||
outs = [];
|
||||
applied = false;
|
||||
trackA;
|
||||
trackB;
|
||||
|
||||
constructor(id, model, x, y, z, rot, data, id_isolation, id_switch, maxspeed, derailspeed) {
|
||||
Object.assign(this, {
|
||||
id, model,
|
||||
x, y, z,
|
||||
rot, data,
|
||||
id_switch, id_isolation,
|
||||
maxspeed, derailspeed
|
||||
});
|
||||
}
|
||||
|
||||
static fromText(text) {
|
||||
const values = text.split(";");
|
||||
|
||||
const sw = new Switch(
|
||||
values[1], // id
|
||||
values[2], // model
|
||||
parseFloat(values[3]), // x
|
||||
parseFloat(values[4]), // y
|
||||
parseFloat(values[5]), // z
|
||||
parseFloat(values[7]), // rot
|
||||
values[9], // data
|
||||
values[10], // id_isolation
|
||||
values[11], // id_switch
|
||||
parseFloat(values[13]), // maxspeed
|
||||
parseFloat(values[14]) // derailspeed
|
||||
);
|
||||
|
||||
return sw;
|
||||
}
|
||||
|
||||
applySwitch(scenery) {
|
||||
if(this.applied) return; // already applied
|
||||
|
||||
this.outs = Switch.getOutsFromData(this.data);
|
||||
const r = Switch.getRadiusFromModel(this.model);
|
||||
|
||||
this.trackA = this._createSwitchTrack(scenery, this.id+"A", this.outs[0], this.outs[2], 0);
|
||||
this.trackB = this._createSwitchTrack(scenery, this.id+"B", this.outs[1], this.outs[3], r);
|
||||
|
||||
if(!this.trackA || !this.trackB) {
|
||||
console.error("Couldn't create switch tracks for switch #"+this.id);
|
||||
return;
|
||||
}
|
||||
|
||||
scenery.addObject(this.trackA);
|
||||
scenery.addObject(this.trackB);
|
||||
}
|
||||
|
||||
static getOutsFromData(data) {
|
||||
const cts = data.split(",");
|
||||
|
||||
const tou = [];
|
||||
cts.forEach((ct, i) => {
|
||||
cts[i] = ct.split(":");
|
||||
if(cts[i][1] === "") cts[i][1] = cts[i][2];
|
||||
tou.push(cts[i][1]);
|
||||
});
|
||||
|
||||
return [
|
||||
tou[0],
|
||||
tou[1] || tou[0],
|
||||
tou[2] || tou[3] || tou[5],
|
||||
tou[4] || tou[6] || tou[3]
|
||||
];
|
||||
}
|
||||
|
||||
static getRadiusFromModel(model) {
|
||||
if(/Rkpd|Crossing/.test(model)) return 0;
|
||||
|
||||
const radiusText = model.replace(/(Rz 60E1-)|-1_.*/g, "");
|
||||
const radius = parseInt(radiusText) || 0;
|
||||
const side = model.split(",")[0];
|
||||
const isRight = side[side.length-1] === "R";
|
||||
|
||||
return isRight ? -radius : radius;
|
||||
}
|
||||
|
||||
_createSwitchTrack(scenery, name, from, to, r) {
|
||||
const startTrack = scenery.getObject("tracks", from);
|
||||
const endTrack = scenery.getObject("tracks", to);
|
||||
|
||||
if(!startTrack || !endTrack) {
|
||||
console.error("Couldn't create track from #"+from+" to #"+to+". One of them doesn't exist!");
|
||||
return;
|
||||
}
|
||||
|
||||
const startTrackEnd = startTrack.getCloserEnd(this.x, this.y, this.z);
|
||||
const endTrackEnd = endTrack.getCloserEnd(this.x, this.y, this.z);
|
||||
|
||||
let trackObj = new PointTrack(
|
||||
name,
|
||||
...startTrackEnd,
|
||||
...endTrackEnd,
|
||||
r,
|
||||
from,
|
||||
to,
|
||||
this.id_switch,
|
||||
this.id_isolation,
|
||||
this.maxspeed,
|
||||
this.derailspeed
|
||||
);
|
||||
|
||||
return trackObj;
|
||||
}
|
||||
}
|
||||
47
src/model/track-object.js
Normal file
47
src/model/track-object.js
Normal file
@ -0,0 +1,47 @@
|
||||
export default class TrackObject {
|
||||
id;
|
||||
prefab_name;
|
||||
x;
|
||||
y;
|
||||
z;
|
||||
rot;
|
||||
track_id;
|
||||
name;
|
||||
category = "track-objects";
|
||||
type = "TrackObject";
|
||||
track;
|
||||
|
||||
constructor(id, prefab_name, x, y, z, rot, track_id, name) {
|
||||
Object.assign(this, {
|
||||
id, prefab_name,
|
||||
x, y, z,
|
||||
rot, track_id, name
|
||||
});
|
||||
}
|
||||
|
||||
static fromText(text) {
|
||||
const values = text.split(";");
|
||||
const obj = new TrackObject(
|
||||
values[1], // id
|
||||
values[2], // prefab_name
|
||||
parseFloat(values[3]), // x
|
||||
parseFloat(values[4]), // y
|
||||
parseFloat(values[5]), // z
|
||||
parseFloat(values[7]), // rot
|
||||
values[9], // track_id
|
||||
values[11] // name
|
||||
);
|
||||
|
||||
return obj;
|
||||
}
|
||||
|
||||
applyObject(scenery) {
|
||||
/* TODO */
|
||||
}
|
||||
|
||||
getRenderBounds() {
|
||||
return [
|
||||
{ x: this.x, z: this.z }
|
||||
];
|
||||
}
|
||||
}
|
||||
44
src/model/track.js
Normal file
44
src/model/track.js
Normal file
@ -0,0 +1,44 @@
|
||||
export default class Track {
|
||||
id;
|
||||
x;
|
||||
y;
|
||||
z;
|
||||
rot;
|
||||
len;
|
||||
r;
|
||||
id_station;
|
||||
id_isolation;
|
||||
maxspeed;
|
||||
derailspeed;
|
||||
category = "tracks";
|
||||
type = "Track";
|
||||
outs = [];
|
||||
|
||||
constructor(id, x, y, z, rot, len, r, previd, nextid, id_station, id_isolation, maxspeed, derailspeed) {
|
||||
Object.assign(this, {
|
||||
id,
|
||||
x, y, z,
|
||||
rot, len, r,
|
||||
id_station, id_isolation,
|
||||
maxspeed, derailspeed
|
||||
});
|
||||
|
||||
this.outs = [previd, nextid];
|
||||
}
|
||||
|
||||
getCloserEnd(x, y, z) {
|
||||
function pow2(val) {
|
||||
return val*val;
|
||||
}
|
||||
|
||||
const dist1 = pow2(x-this.points.x1)+pow2(z-this.points.z1);
|
||||
const dist2 = pow2(x-this.points.x2)+pow2(z-this.points.z2);
|
||||
return (dist1 < dist2) ? [this.points.x1, y, this.points.z1] : [this.points.x2, y, this.points.z2];
|
||||
}
|
||||
|
||||
getRenderBounds() {
|
||||
return [
|
||||
{ x: this.x, z: this.z }
|
||||
];
|
||||
}
|
||||
}
|
||||
54
tools/scp.py
Normal file
54
tools/scp.py
Normal file
@ -0,0 +1,54 @@
|
||||
import os
|
||||
import sys
|
||||
|
||||
#
|
||||
# [ TD2 SCENERY PROCESSOR ]
|
||||
# by masuo
|
||||
# v1.2
|
||||
#
|
||||
|
||||
BAD_WORDS = ['Misc', 'Fence', 'TerrainPoint', 'Wires']
|
||||
|
||||
def should_exclude(line: str) -> bool:
|
||||
return any(bad_word in line for bad_word in BAD_WORDS)
|
||||
|
||||
def process_file(file_path: str):
|
||||
if not file_path.endswith(".sc"):
|
||||
print(f"Skipping non-.sc file: {file_path}")
|
||||
return
|
||||
|
||||
output_path = file_path[:-3] + ".lite.sc"
|
||||
print(f"Processing file: {file_path} -> {output_path}")
|
||||
|
||||
try:
|
||||
with open(file_path, encoding="utf8") as infile, open(output_path, 'w', encoding="utf8") as outfile:
|
||||
for line in infile:
|
||||
if not should_exclude(line):
|
||||
outfile.write(line)
|
||||
except Exception as e:
|
||||
print(f"Error processing {file_path}: {e}")
|
||||
|
||||
def process_directory(directory: str):
|
||||
print(f"Processing directory: {directory}")
|
||||
for entry in os.listdir(directory):
|
||||
full_path = os.path.join(directory, entry)
|
||||
if os.path.isfile(full_path):
|
||||
process_file(full_path)
|
||||
|
||||
def main():
|
||||
if len(sys.argv) < 2:
|
||||
print("Usage: python scp.py <file_or_directory> [<file_or_directory> ...]")
|
||||
return
|
||||
|
||||
for path in sys.argv[1:]:
|
||||
if os.path.isdir(path):
|
||||
process_directory(path)
|
||||
elif os.path.isfile(path):
|
||||
process_file(path)
|
||||
else:
|
||||
print(f"Invalid path: {path}")
|
||||
|
||||
print("Done!")
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
Loading…
Reference in New Issue
Block a user