Änderung: Hinzufügung der Ampel-Logik zur Taxi-Map

Änderungen:
- Erweiterung des TaxiMapTile-Modells um die Spalte trafficLight zur Verwaltung von Ampelzuständen.
- Anpassung der TaxiMapService-Logik zur Unterstützung der Ampel-Updates und -Zustände.
- Implementierung von Methoden zur Steuerung und Anzeige von Ampeln in der Benutzeroberfläche, einschließlich der neuen Funktionen in TaxiToolsView.vue und TaxiGame.vue.
- Verbesserung der Darstellung und Logik zur Ampelsteuerung im Spiel, einschließlich der visuellen Darstellung und der Interaktion mit Ampeln.

Diese Anpassungen verbessern die Funktionalität und Benutzererfahrung im Taxi-Minispiel erheblich, indem sie eine realistischere Verkehrssteuerung ermöglichen.
This commit is contained in:
Torsten Schulz (local)
2025-09-18 18:48:36 +02:00
parent f56e26a9b4
commit 5142243a88
14 changed files with 327 additions and 17 deletions

View File

@@ -23,6 +23,12 @@ const TaxiMapTile = sequelize.define('TaxiMapTile', {
type: DataTypes.STRING(50),
allowNull: false,
},
trafficLight: {
type: DataTypes.BOOLEAN,
allowNull: false,
defaultValue: false,
comment: 'Whether this tile has a traffic light'
},
meta: {
type: DataTypes.JSON,
allowNull: true,
@@ -36,6 +42,7 @@ const TaxiMapTile = sequelize.define('TaxiMapTile', {
{ unique: true, fields: ['map_id','x','y'] },
{ fields: ['map_id'] },
{ fields: ['tile_type'] },
{ fields: ['traffic_light'] },
]
});

View File

@@ -7,7 +7,7 @@ const UserRightType = sequelize.define('user_right_type', {
allowNull: false
}
}, {
tableName: 'user_right',
tableName: 'user_right_type',
schema: 'type',
underscored: true
});

View File

@@ -209,7 +209,9 @@ class TaxiMapService extends BaseService {
where: { mapId, x, y },
defaults: { mapId, x, y, tileType, meta: meta || null }
});
await row.update({ tileType, meta: meta || null });
// trafficLight kann in meta.trafficLight oder künftig in eigener Spalte liegen
const trafficLight = !!(meta && meta.trafficLight);
await row.update({ tileType, meta: meta && Object.keys(meta).length ? meta : null, trafficLight });
}
return { success: true };
}

View File

@@ -45,8 +45,10 @@ const createSchemas = async () => {
const initializeDatabase = async () => {
await createSchemas();
const { default: models } = await import('../models/index.js');
await syncModels(models);
// Modelle nur laden, aber an dieser Stelle NICHT syncen.
// Das Syncing (inkl. alter: true bei Bedarf) wird anschließend zentral
// über syncModelsWithUpdates()/syncModelsAlways gesteuert.
await import('../models/index.js');
};
const syncModels = async (models) => {

View File

@@ -1,6 +1,6 @@
// syncDatabase.js
import { initializeDatabase, syncModelsWithUpdates, syncModelsAlways } from './sequelize.js';
import { initializeDatabase, syncModelsWithUpdates, syncModelsAlways, sequelize } from './sequelize.js';
import initializeTypes from './initializeTypes.js';
import initializeSettings from './initializeSettings.js';
import initializeUserRights from './initializeUserRights.js';
@@ -33,11 +33,32 @@ const syncDatabase = async () => {
console.log("Initializing database schemas...");
await initializeDatabase();
console.log("Synchronizing models...");
await syncModelsWithUpdates(models);
// Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt
console.log("Pre-ensure Taxi columns (traffic_light) ...");
try {
await sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'taxi' AND table_name = 'taxi_map_tile' AND column_name = 'traffic_light'
) THEN
ALTER TABLE taxi.taxi_map_tile
ADD COLUMN traffic_light BOOLEAN NOT NULL DEFAULT false;
END IF;
END
$$;
`);
console.log("✅ traffic_light-Spalte ist vorhanden");
} catch (e) {
console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e);
}
console.log("Setting up associations...");
setupAssociations();
setupAssociations();
console.log("Synchronizing models...");
await syncModelsWithUpdates(models);
console.log("Initializing settings...");
await initializeSettings();
@@ -91,11 +112,32 @@ const syncDatabaseForDeployment = async () => {
console.log("Initializing database schemas...");
await initializeDatabase();
console.log("Synchronizing models with schema updates...");
await syncModelsAlways(models);
// Vorab: Stelle kritische Spalten sicher, damit Index-Erstellung nicht fehlschlägt
console.log("Pre-ensure Taxi columns (traffic_light) ...");
try {
await sequelize.query(`
DO $$
BEGIN
IF NOT EXISTS (
SELECT 1 FROM information_schema.columns
WHERE table_schema = 'taxi' AND table_name = 'taxi_map_tile' AND column_name = 'traffic_light'
) THEN
ALTER TABLE taxi.taxi_map_tile
ADD COLUMN traffic_light BOOLEAN NOT NULL DEFAULT false;
END IF;
END
$$;
`);
console.log("✅ traffic_light-Spalte ist vorhanden");
} catch (e) {
console.warn('⚠️ Konnte traffic_light-Spalte nicht vorab sicherstellen:', e?.message || e);
}
console.log("Setting up associations...");
setupAssociations();
setupAssociations();
console.log("Synchronizing models with schema updates...");
await syncModelsAlways(models);
console.log("Initializing settings...");
await initializeSettings();

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 126 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 181 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 175 KiB

View File

@@ -356,8 +356,8 @@
},
"gifts": {
"Gold Coin": "Goldmünze",
"Silk Scarf": "Seidenschale",
"Exotic Perfume": "Exotisches Parfum",
"Silk Scarf": "Seidenschal",
"Exotic Perfume": "Exotisches Parfüm",
"Crystal Pendant": "Kristallanhänger",
"Leather Journal": "Lederjournal",
"Fine Wine": "Feiner Wein",

View File

@@ -98,6 +98,7 @@
<div class="house-door" :class="doorClass(rotation)"></div>
</div>
</div>
<img v-if="getCellAtPosition(x, y)?.extraTrafficLight" src="/images/taxi/redlight.svg" class="traffic-overlay" alt="Ampel" />
</div>
</div>
</div>
@@ -178,6 +179,11 @@
</div>
</div>
</div>
<div class="trafficlight-row">
<button class="trafficlight-btn" @click="toggleTrafficLight">
<img src="/images/taxi/redlight.svg" alt="Ampel" class="trafficlight-icon" />
</button>
</div>
</div>
</div>
</div>
@@ -249,6 +255,7 @@
:alt="getCellAtPosition(x, y).tileType"
class="tile-image"
/>
<img v-if="getCellAtPosition(x, y)?.extraTrafficLight" src="/images/taxi/redlight.svg" class="traffic-overlay" alt="Ampel" />
<div v-if="getCellAtPosition(x, y)?.extraHouses" class="house-overlay">
<div
v-for="(rotation, corner) in getCellAtPosition(x, y).extraHouses"
@@ -339,6 +346,11 @@
</div>
</div>
</div>
<div class="trafficlight-row">
<button class="trafficlight-btn" @click="toggleTrafficLight">
<img src="/images/taxi/redlight.svg" alt="Ampel" class="trafficlight-icon" />
</button>
</div>
</div>
</div>
</div>
@@ -795,6 +807,9 @@ export default {
// Neue Struktur: Mapping corner -> rotation
this.boardCells[key].extraHouses = { ...t.meta.houses };
}
if (t.meta && t.meta.trafficLight) {
this.boardCells[key].extraTrafficLight = true;
}
}
// Straßennamen aus tileStreets übernehmen
if (Array.isArray(map.tileStreets)) {
@@ -867,6 +882,13 @@ export default {
this.computeAllowedHouseCorners();
},
toggleTrafficLight() {
if (!this.selectedCellKey) return;
const cell = this.boardCells[this.selectedCellKey] || { x: 0, y: 0, tileType: null };
const updated = { ...cell, extraTrafficLight: !cell.extraTrafficLight };
this.$set ? this.$set(this.boardCells, this.selectedCellKey, updated) : (this.boardCells[this.selectedCellKey] = updated);
},
startPlaceHouse(rotationDeg) {
// Neuer Flow: Rotation wählen, nachdem Ecke gewählt wurde
if (!this.selectedCellKey || !this.pendingCorner) return;
@@ -1155,7 +1177,10 @@ export default {
x: c.x,
y: c.y,
tileType: c.tileType,
meta: c.extraHouses ? { houses: { ...c.extraHouses } } : null
meta: {
...(c.extraHouses ? { houses: { ...c.extraHouses } } : {}),
...(c.extraTrafficLight ? { trafficLight: true } : {})
}
}));
const tileStreetNames = this.collectTileStreetNames();
const tileHouses = this.collectTileHouses();
@@ -1478,10 +1503,15 @@ export default {
.house-square.corner-ro { position: absolute; top: 3px; right: 3px; }
.house-square.corner-lu { position: absolute; bottom: 3px; left: 3px; }
.house-square.corner-ru { position: absolute; bottom: 3px; right: 3px; }
.traffic-overlay { position: absolute; width: 25px; height: 25px; left: 50%; top: 50%; transform: translate(-50%, -50%); pointer-events: none; }
.corner-chooser { margin-top: 8px; display: flex; gap: 6px; }
.corner-btn { padding: 3px; border: 1px solid #ccc; border-radius: 4px; background: #f7f7f7; cursor: pointer; }
.corner-btn:hover { background: #eee; }
.trafficlight-row { margin-top: 10px; }
.trafficlight-btn { padding: 4px; border: 1px solid #ccc; border-radius: 6px; background: #fff; cursor: pointer; }
.trafficlight-icon { width: 40px; height: 40px; }
.corner-preview { width: 18px; height: 18px; display: grid; grid-template-columns: 1fr 1fr; grid-template-rows: 1fr 1fr; }
.corner-preview .q { width: 100%; height: 100%; background: #000; display: block; }
.corner-preview .q.active { background: #d60000; }

View File

@@ -66,6 +66,10 @@
<!-- Tacho-Display -->
<div class="tacho-display">
<div class="tacho-speed">
<span class="redlight-counter" title="Rote Ampeln überfahren">
<span class="redlight-icon">🚦</span>
<span class="redlight-value">{{ redLightViolations }}</span>
</span>
<span class="tacho-icon"></span>
<span class="speed-value">{{ taxi.speed * 5 }}</span>
<span class="speed-unit">km/h</span>
@@ -236,6 +240,9 @@ export default {
images: {}
},
houseImage: null,
trafficLightStates: {},
lastTrafficLightTick: 0,
redLightViolations: 0,
maps: [], // Geladene Maps aus der Datenbank
currentMap: null, // Aktuell verwendete Map
selectedMapId: null, // ID der ausgewählten Map
@@ -293,11 +300,43 @@ export default {
this.setupEventListeners();
await this.initializeMotorSound();
this.setupAudioUnlockHandlers();
this.lastTrafficLightTick = Date.now();
},
beforeUnmount() {
this.cleanup();
},
methods: {
// Ampelschaltung: sekündliche Phasen-Updates; pro Tile ein State
updateTrafficLights() {
const now = Date.now();
if (now - this.lastTrafficLightTick < 1000) return;
this.lastTrafficLightTick = now;
if (!this.currentMap || !Array.isArray(this.currentMap.tiles)) return;
const tiles = this.currentMap.tiles.filter(t => t.trafficLight || (t.meta && t.meta.trafficLight));
for (const t of tiles) {
const key = `${t.x},${t.y}`;
let st = this.trafficLightStates[key];
if (!st) {
// initialisieren: Phase starten, zufällige Grün/Rot-Dauern 5..15s
const r = (min,max)=> Math.floor(min + Math.random()*(max-min+1));
st = {
phase: 0, // 0:Hgrün/Vrot, 1:Hgelb/Vrotgelb, 2:Hrot/Vgrün, 3:Hrotgelb/Vgelb
remaining: r(5,15), // Sekunden bis zur nächsten Umschaltung
hDur: r(5,15), // Dauer H-grün (Phase 0)
vDur: r(5,15) // Dauer V-grün (Phase 2)
};
this.trafficLightStates[key] = st;
}
st.remaining -= 1;
if (st.remaining <= 0) {
// Phase vorwärts schalten, Zwischenphasen 1s
if (st.phase === 0) { st.phase = 1; st.remaining = 1; }
else if (st.phase === 1) { st.phase = 2; st.remaining = st.vDur; }
else if (st.phase === 2) { st.phase = 3; st.remaining = 1; }
else { st.phase = 0; st.remaining = st.hDur; }
}
}
},
// Hilfsfunktion: Liefert laufende Nummer für gegebenen Straßennamen
getStreetNumberByName(name) {
const item = this.streetLegend.find(it => it.name === name);
@@ -681,6 +720,9 @@ export default {
return;
}
// Ampelschaltung tick
this.updateTrafficLights();
this.updateTaxi();
this.handlePassengerActions();
this.checkCollisions();
@@ -1152,8 +1194,70 @@ export default {
// Häuser zeichnen (aus tileHouses)
const absCol = this.currentTile.col + (this.currentMap.offsetX || 0);
const absRow = this.currentTile.row + (this.currentMap.offsetY || 0);
// Haltelinien (falls Ampel vorhanden) 120px vom Rand, 5px dick, 40px breit
if (this.getTrafficLightFor(absCol, absRow)) {
const approaches = this.getTileApproaches(tileType);
this.drawStopLinesOnMainCanvas(this.ctx, tileSize, approaches);
}
this.drawHousesOnMainCanvas(this.ctx, tileSize, absCol, absRow);
// Straßennamen auf Haupt-Canvas zeichnen
// Ampeln an jeder Ecke des Tiles (über den Häusern, unter den Namen)
if (this.getTrafficLightFor(absCol, absRow)) {
const phase = this.getTrafficLightPhase(absCol, absRow); // 0,1,2,3
// Phase-Mapping je Ecke laut Vorgabe:
// 0: H grün / V rot
// 1: H gelb / V rot-gelb
// 2: H rot / V grün
// 3: H rot-gelb / V gelb
const imgGreen = this.tiles.images['trafficlight-green'];
const imgYellow = this.tiles.images['trafficlight-yellow'];
const imgRed = this.tiles.images['trafficlight-red'];
const imgRedYellow = this.tiles.images['trafficlight-redyellow'];
{
const sTL = tileSize * 0.16; // um 20% kleiner als zuvor
// Positionierung nach Vorgabe:
// X: 180px vom linken bzw. rechten Rand, dann je 60px zum näheren Rand und 5px zurück
let leftX = 180;
let rightX = tileSize - 180 - sTL;
leftX = Math.max(0, leftX - 60);
rightX = Math.min(tileSize - sTL, rightX + 60);
leftX = Math.min(tileSize - sTL, Math.max(0, leftX + 5));
rightX = Math.min(tileSize - sTL, Math.max(0, rightX - 5));
// Y: oben korrekt, unten 60px weiter nach oben
const topY = 180 - sTL;
const bottomY = tileSize - 180 - 60;
// Sichtbarkeit pro Seite bestimmen und auf Ecken mappen
const ap = this.getTileApproaches(tileType);
let showTL = false, showTR = false, showBL = false, showBR = false;
if (ap.top) { showTL = true; showTR = true; }
if (ap.bottom) { showBL = true; showBR = true; }
if (ap.left) { showTL = true; showBL = true; }
if (ap.right) { showTR = true; showBR = true; }
// Tile-spezifische Ausnahmen (z.B. T-Kreuzungen)
const hide = this.getCornerHidesForTile(tileType);
if (hide.TL) showTL = false;
if (hide.TR) showTR = false;
if (hide.BL) showBL = false;
if (hide.BR) showBR = false;
const drawCorner = (corner, x, y) => {
let img = imgRed;
if (corner === 'top') {
if (phase === 0) img = imgGreen; else if (phase === 1) img = imgYellow; else if (phase === 2) img = imgRed; else img = imgRedYellow;
} else if (corner === 'bottom') {
if (phase === 0) img = imgRed; else if (phase === 1) img = imgRedYellow; else if (phase === 2) img = imgGreen; else img = imgYellow;
} else if (corner === 'left') {
if (phase === 0) img = imgGreen; else if (phase === 1) img = imgYellow; else if (phase === 2) img = imgRed; else img = imgRedYellow;
} else if (corner === 'right') {
if (phase === 0) img = imgRed; else if (phase === 1) img = imgRedYellow; else if (phase === 2) img = imgGreen; else img = imgYellow;
}
if (img && img.complete) this.ctx.drawImage(img, x, y, sTL, sTL);
};
if (showTL) drawCorner('left', leftX, topY);
if (showTR) drawCorner('right', rightX, topY);
if (showBL) drawCorner('left', leftX, bottomY);
if (showBR) drawCorner('right', rightX, bottomY);
}
}
this.drawStreetNamesOnMainCanvas(this.ctx, tileSize, tileType, absCol, absRow);
}
}
@@ -1172,6 +1276,18 @@ export default {
}
}
},
getTrafficLightFor(col, row) {
// Prüfe Tiles-Array, ob trafficLight-Spalte oder meta.trafficLight gesetzt ist
if (!this.currentMap || !Array.isArray(this.currentMap.tiles)) return false;
const found = this.currentMap.tiles.find(t => t.x === col && t.y === row && (t.trafficLight === true || (t.meta && t.meta.trafficLight)));
return !!found;
},
getTrafficLightPhase(col, row) {
const key = `${col},${row}`;
const st = this.trafficLightStates[key];
if (!st) return 0; // default
return st.phase || 0;
},
drawStreetNamesOnMainCanvas(ctx, size, tileType, col, row) {
if (!this.currentMap || !Array.isArray(this.currentMap.tileStreets)) return;
@@ -1260,6 +1376,55 @@ export default {
ctx.restore();
}
},
// Haltelinien: 120px vom Rand, 5px dick, 40px breit, an allen vier Zufahrten
drawStopLinesOnMainCanvas(ctx, size, approaches = { top:true, right:true, bottom:true, left:true }) {
const margin = 120; // Basis-Abstand vom Rand
const extra = 30; // 10px zurück (von +40 auf +30)
const m = margin + extra;
const thickness = 5;
const width = 60; // +20px breiter (vorher 40)
ctx.save();
ctx.fillStyle = '#ffffff';
// Oben (horizontale Linie, zentriert)
if (approaches.top) ctx.fillRect((size - width) / 2 - 30, m - thickness, width, thickness);
// Unten
if (approaches.bottom) ctx.fillRect((size - width) / 2 + 30, size - m, width, thickness);
// Links (vertikale Linie, zentriert)
if (approaches.left) ctx.fillRect(m - thickness, (size - width) / 2 + 30, thickness, width);
// Rechts
if (approaches.right) ctx.fillRect(size - m, (size - width) / 2 - 30, thickness, width);
ctx.restore();
},
// Liefert, von welchen Seiten eine Straße an dieses Tile anbindet
getTileApproaches(tileType) {
switch (tileType) {
case 'horizontal': return { top:false, right:true, bottom:false, left:true };
case 'vertical': return { top:true, right:false, bottom:true, left:false };
case 'cross': return { top:true, right:true, bottom:true, left:true };
case 'cornertopleft': return { top:true, right:false, bottom:false, left:true };
case 'cornertopright': return { top:true, right:true, bottom:false, left:false };
case 'cornerbottomleft': return { top:false, right:false, bottom:true, left:true };
case 'cornerbottomright': return { top:false, right:true, bottom:true, left:false };
case 'tup': return { top:true, right:true, bottom:false, left:true }; // T-oben: unten gesperrt
case 'tdown': return { top:false, right:true, bottom:true, left:true }; // T-unten: oben gesperrt
case 'tleft': return { top:true, right:false, bottom:true, left:true }; // T-links: rechts gesperrt
case 'tright': return { top:true, right:true, bottom:true, left:false }; // T-rechts: links gesperrt
case 'fuelhorizontal': return { top:false, right:true, bottom:false, left:true };
case 'fuelvertical': return { top:true, right:false, bottom:true, left:false };
default: return { top:true, right:true, bottom:true, left:true };
}
},
// Ecken-spezifische Ausblendungen je Tiletyp (zusätzlich zu Seiten-Logik)
getCornerHidesForTile(tileType) {
// TL=oben links, TR=oben rechts, BL=unten links, BR=unten rechts
switch (tileType) {
case 'tdown': return { TL: true, TR: false, BL: false, BR: false }; // oben gesperrt -> oben links weg
case 'tup': return { TL: false, TR: false, BL: false, BR: true }; // unten gesperrt -> unten rechts weg
case 'tleft': return { TL: false, TR: true, BL: false, BR: false }; // rechts gesperrt -> oben rechts weg
case 'tright': return { TL: false, TR: false, BL: true, BR: false }; // links gesperrt -> unten links weg
default: return { TL: false, TR: false, BL: false, BR: false };
}
},
getTileType(row, col, rows, cols) {
// Ecken
@@ -1410,6 +1575,19 @@ export default {
img.src = `/images/taxi/${tileName}.svg`;
this.tiles.images[tileName] = img;
}
// Lade Ampel-Icon (zentral, 50% Größe)
const red = new Image();
red.src = '/images/taxi/redlight.svg';
this.tiles.images['redlight-svg'] = red;
// Lade Trafficlight-Zustände (für großes Tile an den Ecken)
const tlStates = ['trafficlight-red','trafficlight-yellow','trafficlight-redyellow','trafficlight-green'];
for (const key of tlStates) {
const img = new Image();
img.src = `/images/taxi/${key}.svg`;
this.tiles.images[key] = img;
}
},
loadTaxiImage() {
@@ -1539,6 +1717,15 @@ export default {
// Straßennamen-Nummern zeichnen, basierend auf tileStreets (nur einmal pro Name)
this.drawStreetNumbersOnMinimap(ctx, x, y, tileSize, tileType, absCol, absRow, drawnNames);
// Ampel im Minimap zentriert (50% Größe)
if (this.getTrafficLightFor(absCol, absRow)) {
const img = this.tiles.images['redlight-svg'];
if (img && img.complete) {
const s = tileSize * 0.3; // 60% der bisherigen 50%-Größe
ctx.drawImage(img, x + (tileSize - s) / 2, y + (tileSize - s) / 2, s, s);
}
}
}
}
}
@@ -1748,7 +1935,36 @@ export default {
if (t.h && t.v) { addEdge(keyH, keyV); }
}
if (nodes.size === 0) continue;
let startKey = null; for (const k of nodes.keys()) { if ((adj.get(k)?.size || 0) === 1) { startKey = k; break; } } if (!startKey) startKey = Array.from(nodes.keys()).sort()[0];
// Startknoten deterministisch wählen:
// 1) bevorzuge Endpunkte (Grad=1)
// 2) bevorzuge horizontale Segmente (axis==='h') und wähle den mit kleinstem x (links)
// 3) sonst vertikale Segmente (axis==='v') mit kleinstem y (oben)
const nodeEntries = Array.from(nodes.entries());
const deg = (k) => (adj.get(k)?.size || 0);
let candidates = nodeEntries.filter(([k]) => deg(k) === 1);
if (candidates.length === 0) candidates = nodeEntries;
let horiz = candidates.filter(([, n]) => n.axis === 'h');
let startPair = null;
if (horiz.length > 0) {
// Links-vorne nimmt Vorrang (kleinstes x, dann kleinstes y)
startPair = horiz.reduce((best, curr) => {
if (!best) return curr;
const bn = best[1], cn = curr[1];
if (cn.x < bn.x) return curr;
if (cn.x === bn.x && cn.y < bn.y) return curr;
return best;
}, null);
} else {
const vert = candidates.filter(([, n]) => n.axis === 'v');
startPair = vert.reduce((best, curr) => {
if (!best) return curr;
const bn = best[1], cn = curr[1];
if (cn.y < bn.y) return curr;
if (cn.y === bn.y && cn.x < bn.x) return curr;
return best;
}, null) || candidates[0];
}
let startKey = startPair ? startPair[0] : (nodeEntries[0] && nodeEntries[0][0]);
const visited = new Set(); let odd = 1, even = 2; let prev = null; let curr = startKey; let currOddSide = (nodes.get(curr).axis === 'h') ? 'top' : 'left';
while (curr) {
visited.add(curr);
@@ -1773,8 +1989,15 @@ export default {
});
if (!housesFiltered.length) { prev = curr; curr = next || null; continue; }
const orderCorners = (list) => {
if (axis === 'h') { const movingRight = dir.dx > 0 || (dir.dx === 0 && !prev); const seq = movingRight ? ['lo','ro','lu','ru'] : ['ro','lo','ru','lu']; return list.slice().sort((a,b)=> seq.indexOf(a) - seq.indexOf(b)); }
else { const movingDown = dir.dy > 0 || (dir.dy === 0 && !prev); const seq = movingDown ? ['lo','lu','ro','ru'] : ['lu','lo','ru','ro']; return list.slice().sort((a,b)=> seq.indexOf(a) - seq.indexOf(b)); }
if (axis === 'h') {
// Erzwinge Nummerierung beginnend von links nach rechts
const seq = ['lo','ro','lu','ru'];
return list.slice().sort((a,b)=> seq.indexOf(a) - seq.indexOf(b));
} else {
const movingDown = dir.dy > 0 || (dir.dy === 0 && !prev);
const seq = movingDown ? ['lo','lu','ro','ru'] : ['lu','lo','ru','ro'];
return list.slice().sort((a,b)=> seq.indexOf(a) - seq.indexOf(b));
}
};
const oddCorners = (axis === 'h') ? (currOddSide === 'top' ? ['lo','ro'] : ['lu','ru']) : (currOddSide === 'left' ? ['lo','lu'] : ['ro','ru']);
const evenCorners = (axis === 'h') ? (currOddSide === 'top' ? ['lu','ru'] : ['lo','ro']) : (currOddSide === 'left' ? ['ro','ru'] : ['lo','lu']);
@@ -1854,6 +2077,10 @@ export default {
font-weight: 500;
}
.redlight-counter { display: inline-flex; align-items: center; gap: 4px; margin-right: 8px; }
.redlight-icon { font-size: 10pt; }
.redlight-value { font-weight: 700; min-width: 16px; text-align: right; }
.tacho-icon {
font-size: 10pt;
}