diff --git a/backend/models/taxi/taxiMapTile.js b/backend/models/taxi/taxiMapTile.js
index e3f61c5..9fa24d2 100644
--- a/backend/models/taxi/taxiMapTile.js
+++ b/backend/models/taxi/taxiMapTile.js
@@ -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'] },
]
});
diff --git a/backend/models/type/user_right.js b/backend/models/type/user_right.js
index 3cf4741..ffa2450 100644
--- a/backend/models/type/user_right.js
+++ b/backend/models/type/user_right.js
@@ -7,7 +7,7 @@ const UserRightType = sequelize.define('user_right_type', {
allowNull: false
}
}, {
- tableName: 'user_right',
+ tableName: 'user_right_type',
schema: 'type',
underscored: true
});
diff --git a/backend/services/taxiMapService.js b/backend/services/taxiMapService.js
index dc38bb7..97f1101 100644
--- a/backend/services/taxiMapService.js
+++ b/backend/services/taxiMapService.js
@@ -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 };
}
diff --git a/backend/utils/sequelize.js b/backend/utils/sequelize.js
index 03ac962..afd4338 100644
--- a/backend/utils/sequelize.js
+++ b/backend/utils/sequelize.js
@@ -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) => {
diff --git a/backend/utils/syncDatabase.js b/backend/utils/syncDatabase.js
index ba204ab..e6324f6 100644
--- a/backend/utils/syncDatabase.js
+++ b/backend/utils/syncDatabase.js
@@ -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();
diff --git a/frontend/public/images/taxi/passenger1.png b/frontend/public/images/taxi/passenger1.png
new file mode 100644
index 0000000..352dd54
Binary files /dev/null and b/frontend/public/images/taxi/passenger1.png differ
diff --git a/frontend/public/images/taxi/passenger2.png b/frontend/public/images/taxi/passenger2.png
new file mode 100644
index 0000000..16a8580
Binary files /dev/null and b/frontend/public/images/taxi/passenger2.png differ
diff --git a/frontend/public/images/taxi/passenger3.png b/frontend/public/images/taxi/passenger3.png
new file mode 100644
index 0000000..775e0db
Binary files /dev/null and b/frontend/public/images/taxi/passenger3.png differ
diff --git a/frontend/public/images/taxi/passenger4.png b/frontend/public/images/taxi/passenger4.png
new file mode 100644
index 0000000..9fcfa1a
Binary files /dev/null and b/frontend/public/images/taxi/passenger4.png differ
diff --git a/frontend/public/images/taxi/passenger5.png b/frontend/public/images/taxi/passenger5.png
new file mode 100644
index 0000000..0b155d4
Binary files /dev/null and b/frontend/public/images/taxi/passenger5.png differ
diff --git a/frontend/public/images/taxi/passenger6.png b/frontend/public/images/taxi/passenger6.png
new file mode 100644
index 0000000..0e1684a
Binary files /dev/null and b/frontend/public/images/taxi/passenger6.png differ
diff --git a/frontend/src/i18n/locales/de/falukant.json b/frontend/src/i18n/locales/de/falukant.json
index 3a89e87..7fd2244 100644
--- a/frontend/src/i18n/locales/de/falukant.json
+++ b/frontend/src/i18n/locales/de/falukant.json
@@ -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",
diff --git a/frontend/src/views/admin/TaxiToolsView.vue b/frontend/src/views/admin/TaxiToolsView.vue
index 30e642f..6970947 100644
--- a/frontend/src/views/admin/TaxiToolsView.vue
+++ b/frontend/src/views/admin/TaxiToolsView.vue
@@ -98,6 +98,7 @@
+
+ 🚦
+ {{ redLightViolations }}
+
⏱️
{{ taxi.speed * 5 }}
km/h
@@ -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;
}