Compare commits
7 Commits
a926727b51
...
v1.15.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
99d1370461
|
|||
|
01b01b727f
|
|||
|
46bde27514
|
|||
| 739c2b21d7 | |||
| 0d4ef75934 | |||
| b6e685d351 | |||
|
44b0f6443f
|
2
.env.example
Normal file
2
.env.example
Normal file
@@ -0,0 +1,2 @@
|
|||||||
|
# Optional: Set a custom container name to run multiple instances
|
||||||
|
# CONTAINER_NAME=nonograms-dev
|
||||||
@@ -82,7 +82,7 @@ define(['./workbox-7a5e81cd'], (function (workbox) { 'use strict';
|
|||||||
*/
|
*/
|
||||||
workbox.precacheAndRoute([{
|
workbox.precacheAndRoute([{
|
||||||
"url": "index.html",
|
"url": "index.html",
|
||||||
"revision": "0.n1n8rjsg38"
|
"revision": "0.b79gmi6tt88"
|
||||||
}], {});
|
}], {});
|
||||||
workbox.cleanupOutdatedCaches();
|
workbox.cleanupOutdatedCaches();
|
||||||
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
workbox.registerRoute(new workbox.NavigationRoute(workbox.createHandlerBoundToURL("index.html"), {
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ version: '3.8'
|
|||||||
|
|
||||||
services:
|
services:
|
||||||
nonograms:
|
nonograms:
|
||||||
container_name: nonograms
|
container_name: ${CONTAINER_NAME:-nonograms}
|
||||||
build:
|
build:
|
||||||
context: .
|
context: .
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
|
|||||||
15
index.html
Normal file
15
index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="pl">
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" type="image/svg+xml" href="/nonograms.svg" />
|
||||||
|
<link rel="apple-touch-icon" href="/apple-touch-icon.png" />
|
||||||
|
<link rel="mask-icon" href="/nonograms.svg" color="#00f2fe" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
|
||||||
|
<title>Nonograms Pro - Vue 3 SOLID</title>
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
4
package-lock.json
generated
4
package-lock.json
generated
@@ -1,12 +1,12 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.15.1",
|
"version": "1.15.4",
|
||||||
"lockfileVersion": 3,
|
"lockfileVersion": 3,
|
||||||
"requires": true,
|
"requires": true,
|
||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.15.1",
|
"version": "1.15.4",
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@capacitor/android": "^8.1.0",
|
"@capacitor/android": "^8.1.0",
|
||||||
"@capacitor/cli": "^8.1.0",
|
"@capacitor/cli": "^8.1.0",
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "vue-nonograms-solid",
|
"name": "vue-nonograms-solid",
|
||||||
"version": "1.15.1",
|
"version": "1.15.4",
|
||||||
"homepage": "https://nonograms.7u.pl/",
|
"homepage": "https://nonograms.7u.pl/",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
"scripts": {
|
"scripts": {
|
||||||
|
|||||||
@@ -1,9 +1,10 @@
|
|||||||
<script setup>
|
<script setup>
|
||||||
import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
import { ref, computed, onMounted, watch } from 'vue';
|
||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { calculateDifficulty } from '@/utils/puzzleUtils';
|
import { calculateDifficulty } from '@/utils/puzzleUtils';
|
||||||
import { HelpCircle } from 'lucide-vue-next';
|
import { HelpCircle } from 'lucide-vue-next';
|
||||||
|
import DifficultyMap from './DifficultyMap.vue';
|
||||||
|
|
||||||
const emit = defineEmits(['close', 'open-simulation']);
|
const emit = defineEmits(['close', 'open-simulation']);
|
||||||
const store = usePuzzleStore();
|
const store = usePuzzleStore();
|
||||||
@@ -12,161 +13,11 @@ const { t } = useI18n();
|
|||||||
const customSize = ref(10);
|
const customSize = ref(10);
|
||||||
const fillRate = ref(50);
|
const fillRate = ref(50);
|
||||||
const errorMsg = ref('');
|
const errorMsg = ref('');
|
||||||
const difficultyCanvas = ref(null);
|
|
||||||
const isDragging = ref(false);
|
|
||||||
const cachedBackground = ref(null);
|
|
||||||
|
|
||||||
const drawMap = () => {
|
const showAdvanced = ref(true);
|
||||||
const canvas = difficultyCanvas.value;
|
|
||||||
if (!canvas) return;
|
|
||||||
const ctx = canvas.getContext('2d');
|
|
||||||
const width = canvas.width;
|
|
||||||
const height = canvas.height;
|
|
||||||
|
|
||||||
// Clear
|
|
||||||
ctx.clearRect(0, 0, width, height);
|
|
||||||
|
|
||||||
// Use cached background if available
|
|
||||||
if (cachedBackground.value) {
|
|
||||||
ctx.putImageData(cachedBackground.value, 0, 0);
|
|
||||||
} else {
|
|
||||||
// Draw Gradient Background (Heavy calculation)
|
|
||||||
const imgData = ctx.createImageData(width, height);
|
|
||||||
const data = imgData.data;
|
|
||||||
|
|
||||||
// Ranges:
|
|
||||||
// X: Fill Rate 10% -> 90%
|
|
||||||
// Y: Size 5 -> 80
|
|
||||||
|
|
||||||
for (let y = 0; y < height; y++) {
|
|
||||||
for (let x = 0; x < width; x++) {
|
|
||||||
const normalizedX = x / width;
|
|
||||||
const normalizedY = 1 - (y / height); // 0 at bottom, 1 at top
|
|
||||||
|
|
||||||
const fRate = 0.1 + normalizedX * 0.8; // 0.1 to 0.9
|
|
||||||
const sSize = 5 + normalizedY * 75; // 5 to 80
|
|
||||||
|
|
||||||
const { value } = calculateDifficulty(fRate, sSize);
|
|
||||||
|
|
||||||
// Color Mapping
|
|
||||||
const hue = 120 * (1 - value / 100);
|
|
||||||
const [r, g, b] = hslToRgb(hue / 360, 1, 0.5);
|
|
||||||
|
|
||||||
const index = (y * width + x) * 4;
|
|
||||||
data[index] = r;
|
|
||||||
data[index + 1] = g;
|
|
||||||
data[index + 2] = b;
|
|
||||||
data[index + 3] = 255; // Alpha
|
|
||||||
}
|
|
||||||
}
|
|
||||||
ctx.putImageData(imgData, 0, 0);
|
|
||||||
cachedBackground.value = imgData;
|
|
||||||
}
|
|
||||||
|
|
||||||
// Draw current position
|
|
||||||
// Map current fillRate/size to x,y
|
|
||||||
// Fill: 10..90. Size: 5..80.
|
|
||||||
const currentFill = Math.max(10, Math.min(90, fillRate.value));
|
|
||||||
const currentSize = Math.max(5, Math.min(80, customSize.value));
|
|
||||||
|
|
||||||
const posX = ((currentFill - 10) / 80) * width;
|
|
||||||
const posY = (1 - (currentSize - 5) / 75) * height;
|
|
||||||
|
|
||||||
// Draw Crosshair/Circle
|
|
||||||
ctx.beginPath();
|
|
||||||
ctx.arc(posX, posY, 6, 0, Math.PI * 2);
|
|
||||||
ctx.fillStyle = '#fff';
|
|
||||||
ctx.fill();
|
|
||||||
ctx.lineWidth = 2;
|
|
||||||
ctx.strokeStyle = '#000';
|
|
||||||
ctx.stroke();
|
|
||||||
};
|
|
||||||
|
|
||||||
const hslToRgb = (h, s, l) => {
|
|
||||||
let r, g, b;
|
|
||||||
if (s === 0) {
|
|
||||||
r = g = b = l; // achromatic
|
|
||||||
} else {
|
|
||||||
const hue2rgb = (p, q, t) => {
|
|
||||||
if (t < 0) t += 1;
|
|
||||||
if (t > 1) t -= 1;
|
|
||||||
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
|
||||||
if (t < 1 / 2) return q;
|
|
||||||
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
|
||||||
return p;
|
|
||||||
};
|
|
||||||
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
|
||||||
const p = 2 * l - q;
|
|
||||||
r = hue2rgb(p, q, h + 1 / 3);
|
|
||||||
g = hue2rgb(p, q, h);
|
|
||||||
b = hue2rgb(p, q, h - 1 / 3);
|
|
||||||
}
|
|
||||||
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
|
||||||
};
|
|
||||||
|
|
||||||
const updateFromEvent = (e) => {
|
|
||||||
const canvas = difficultyCanvas.value;
|
|
||||||
if (!canvas) return;
|
|
||||||
const rect = canvas.getBoundingClientRect();
|
|
||||||
|
|
||||||
// Handle Touch or Mouse
|
|
||||||
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
|
||||||
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
|
||||||
|
|
||||||
let x = clientX - rect.left;
|
|
||||||
let y = clientY - rect.top;
|
|
||||||
|
|
||||||
// Clamp
|
|
||||||
x = Math.max(0, Math.min(rect.width, x));
|
|
||||||
y = Math.max(0, Math.min(rect.height, y));
|
|
||||||
|
|
||||||
// Reverse Map
|
|
||||||
// x / width -> fillRate (10..90)
|
|
||||||
// 1 - y / height -> size (5..80)
|
|
||||||
|
|
||||||
const normalizedX = x / rect.width;
|
|
||||||
const normalizedY = 1 - (y / rect.height);
|
|
||||||
|
|
||||||
const newFill = 10 + normalizedX * 80;
|
|
||||||
const newSize = 5 + normalizedY * 75;
|
|
||||||
|
|
||||||
fillRate.value = Math.round(newFill);
|
|
||||||
customSize.value = Math.round(newSize);
|
|
||||||
};
|
|
||||||
|
|
||||||
const startDrag = (e) => {
|
|
||||||
isDragging.value = true;
|
|
||||||
updateFromEvent(e);
|
|
||||||
// Add global listeners for mouse to handle dragging outside canvas
|
|
||||||
window.addEventListener('mousemove', onDrag);
|
|
||||||
window.addEventListener('mouseup', stopDrag);
|
|
||||||
};
|
|
||||||
|
|
||||||
const onDrag = (e) => {
|
|
||||||
if (!isDragging.value) return;
|
|
||||||
updateFromEvent(e);
|
|
||||||
};
|
|
||||||
|
|
||||||
const stopDrag = () => {
|
|
||||||
isDragging.value = false;
|
|
||||||
window.removeEventListener('mousemove', onDrag);
|
|
||||||
window.removeEventListener('mouseup', stopDrag);
|
|
||||||
};
|
|
||||||
|
|
||||||
onUnmounted(() => {
|
|
||||||
window.removeEventListener('mousemove', onDrag);
|
|
||||||
window.removeEventListener('mouseup', stopDrag);
|
|
||||||
});
|
|
||||||
|
|
||||||
const showAdvanced = ref(false);
|
|
||||||
|
|
||||||
const toggleAdvanced = () => {
|
const toggleAdvanced = () => {
|
||||||
showAdvanced.value = !showAdvanced.value;
|
showAdvanced.value = !showAdvanced.value;
|
||||||
if (showAdvanced.value) {
|
|
||||||
// Reset cache when opening to ensure size is correct if canvas resized
|
|
||||||
cachedBackground.value = null;
|
|
||||||
nextTick(drawMap);
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
@@ -179,14 +30,6 @@ onMounted(() => {
|
|||||||
if (savedFillRate && !isNaN(savedFillRate)) {
|
if (savedFillRate && !isNaN(savedFillRate)) {
|
||||||
fillRate.value = Math.max(10, Math.min(90, Number(savedFillRate)));
|
fillRate.value = Math.max(10, Math.min(90, Number(savedFillRate)));
|
||||||
}
|
}
|
||||||
|
|
||||||
// Don't draw map initially if hidden
|
|
||||||
});
|
|
||||||
|
|
||||||
watch([customSize, fillRate], () => {
|
|
||||||
if (showAdvanced.value) {
|
|
||||||
drawMap();
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
|
|
||||||
watch(customSize, (newVal) => {
|
watch(customSize, (newVal) => {
|
||||||
@@ -245,7 +88,7 @@ const confirm = () => {
|
|||||||
<div class="range-value">{{ customSize }}</div>
|
<div class="range-value">{{ customSize }}</div>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
v-model="customSize"
|
v-model.number="customSize"
|
||||||
min="5"
|
min="5"
|
||||||
max="80"
|
max="80"
|
||||||
step="1"
|
step="1"
|
||||||
@@ -264,7 +107,7 @@ const confirm = () => {
|
|||||||
<div class="range-value">{{ fillRate }}%</div>
|
<div class="range-value">{{ fillRate }}%</div>
|
||||||
<input
|
<input
|
||||||
type="range"
|
type="range"
|
||||||
v-model="fillRate"
|
v-model.number="fillRate"
|
||||||
min="10"
|
min="10"
|
||||||
max="90"
|
max="90"
|
||||||
step="1"
|
step="1"
|
||||||
@@ -278,15 +121,14 @@ const confirm = () => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="map-section" v-if="showAdvanced">
|
<div class="map-section" v-if="showAdvanced">
|
||||||
<canvas
|
<DifficultyMap
|
||||||
ref="difficultyCanvas"
|
v-model:size="customSize"
|
||||||
width="400"
|
v-model:density="fillRate"
|
||||||
height="400"
|
:interactive="true"
|
||||||
@mousedown="startDrag"
|
:width="400"
|
||||||
@touchstart.prevent="startDrag"
|
:height="400"
|
||||||
@touchmove.prevent="onDrag"
|
class="difficulty-map-canvas"
|
||||||
@touchend="stopDrag"
|
/>
|
||||||
></canvas>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
|||||||
293
src/components/DifficultyMap.vue
Normal file
293
src/components/DifficultyMap.vue
Normal file
@@ -0,0 +1,293 @@
|
|||||||
|
<script setup>
|
||||||
|
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
||||||
|
import { calculateDifficulty } from '@/utils/puzzleUtils';
|
||||||
|
import { useI18n } from '@/composables/useI18n';
|
||||||
|
|
||||||
|
const props = defineProps({
|
||||||
|
size: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
density: {
|
||||||
|
type: Number,
|
||||||
|
required: true
|
||||||
|
},
|
||||||
|
actualDifficulty: {
|
||||||
|
type: Number,
|
||||||
|
default: null
|
||||||
|
},
|
||||||
|
interactive: {
|
||||||
|
type: Boolean,
|
||||||
|
default: false
|
||||||
|
},
|
||||||
|
width: {
|
||||||
|
type: Number,
|
||||||
|
default: 300
|
||||||
|
},
|
||||||
|
height: {
|
||||||
|
type: Number,
|
||||||
|
default: 200
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const emit = defineEmits(['update:size', 'update:density']);
|
||||||
|
const { t } = useI18n();
|
||||||
|
|
||||||
|
const canvasRef = ref(null);
|
||||||
|
let cachedBackground = null;
|
||||||
|
const isDragging = ref(false);
|
||||||
|
|
||||||
|
// Constants for ranges
|
||||||
|
const MIN_SIZE = 5;
|
||||||
|
const MAX_SIZE = 80;
|
||||||
|
const MIN_DENSITY = 10;
|
||||||
|
const MAX_DENSITY = 90;
|
||||||
|
|
||||||
|
const hslToRgb = (h, s, l) => {
|
||||||
|
let r, g, b;
|
||||||
|
if (s === 0) {
|
||||||
|
r = g = b = l; // achromatic
|
||||||
|
} else {
|
||||||
|
const hue2rgb = (p, q, t) => {
|
||||||
|
if (t < 0) t += 1;
|
||||||
|
if (t > 1) t -= 1;
|
||||||
|
if (t < 1 / 6) return p + (q - p) * 6 * t;
|
||||||
|
if (t < 1 / 2) return q;
|
||||||
|
if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
|
||||||
|
return p;
|
||||||
|
};
|
||||||
|
const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
|
||||||
|
const p = 2 * l - q;
|
||||||
|
r = hue2rgb(p, q, h + 1 / 3);
|
||||||
|
g = hue2rgb(p, q, h);
|
||||||
|
b = hue2rgb(p, q, h - 1 / 3);
|
||||||
|
}
|
||||||
|
return [Math.round(r * 255), Math.round(g * 255), Math.round(b * 255)];
|
||||||
|
};
|
||||||
|
|
||||||
|
const drawMap = () => {
|
||||||
|
const canvas = canvasRef.value;
|
||||||
|
if (!canvas) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const ctx = canvas.getContext('2d');
|
||||||
|
const width = canvas.width;
|
||||||
|
const height = canvas.height;
|
||||||
|
|
||||||
|
// Clear
|
||||||
|
ctx.clearRect(0, 0, width, height);
|
||||||
|
|
||||||
|
// Use cached background if available
|
||||||
|
if (cachedBackground) {
|
||||||
|
ctx.drawImage(cachedBackground, 0, 0, width, height);
|
||||||
|
} else {
|
||||||
|
// Draw Gradient Background (Optimized)
|
||||||
|
// Use a smaller buffer to reduce calculations
|
||||||
|
const bufferWidth = 40;
|
||||||
|
const bufferHeight = 40;
|
||||||
|
|
||||||
|
const bufferCanvas = document.createElement('canvas');
|
||||||
|
bufferCanvas.width = bufferWidth;
|
||||||
|
bufferCanvas.height = bufferHeight;
|
||||||
|
const bufferCtx = bufferCanvas.getContext('2d');
|
||||||
|
|
||||||
|
const imgData = bufferCtx.createImageData(bufferWidth, bufferHeight);
|
||||||
|
const data = imgData.data;
|
||||||
|
|
||||||
|
for (let y = 0; y < bufferHeight; y++) {
|
||||||
|
for (let x = 0; x < bufferWidth; x++) {
|
||||||
|
const normalizedX = x / bufferWidth;
|
||||||
|
const normalizedY = 1 - (y / bufferHeight); // 0 at bottom, 1 at top
|
||||||
|
|
||||||
|
const fRate = (MIN_DENSITY + normalizedX * (MAX_DENSITY - MIN_DENSITY)) / 100; // 0.1 to 0.9
|
||||||
|
const sSize = MIN_SIZE + normalizedY * (MAX_SIZE - MIN_SIZE); // 5 to 80
|
||||||
|
|
||||||
|
const { value } = calculateDifficulty(fRate, sSize);
|
||||||
|
|
||||||
|
// Color Mapping
|
||||||
|
const hue = 120 * (1 - value / 100);
|
||||||
|
const [r, g, b] = hslToRgb(hue / 360, 1, 0.5);
|
||||||
|
|
||||||
|
const index = (y * bufferWidth + x) * 4;
|
||||||
|
data[index] = r;
|
||||||
|
data[index + 1] = g;
|
||||||
|
data[index + 2] = b;
|
||||||
|
data[index + 3] = 255; // Alpha
|
||||||
|
}
|
||||||
|
}
|
||||||
|
bufferCtx.putImageData(imgData, 0, 0);
|
||||||
|
|
||||||
|
// Draw scaled up
|
||||||
|
ctx.imageSmoothingEnabled = true;
|
||||||
|
ctx.imageSmoothingQuality = 'high';
|
||||||
|
ctx.drawImage(bufferCanvas, 0, 0, width, height);
|
||||||
|
|
||||||
|
cachedBackground = bufferCanvas;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Draw current position
|
||||||
|
// Clamp values
|
||||||
|
const currentFill = Math.max(MIN_DENSITY, Math.min(MAX_DENSITY, props.density));
|
||||||
|
const currentSize = Math.max(MIN_SIZE, Math.min(MAX_SIZE, props.size));
|
||||||
|
|
||||||
|
const posX = ((currentFill - MIN_DENSITY) / (MAX_DENSITY - MIN_DENSITY)) * width;
|
||||||
|
const posY = (1 - (currentSize - MIN_SIZE) / (MAX_SIZE - MIN_SIZE)) * height;
|
||||||
|
|
||||||
|
// Draw Point
|
||||||
|
ctx.beginPath();
|
||||||
|
ctx.arc(posX, posY, 6, 0, Math.PI * 2);
|
||||||
|
|
||||||
|
// Use actual difficulty color if provided, otherwise white
|
||||||
|
if (props.actualDifficulty !== null) {
|
||||||
|
const hue = 120 * (1 - Math.max(0, Math.min(100, props.actualDifficulty)) / 100);
|
||||||
|
const [r, g, b] = hslToRgb(hue / 360, 1, 0.5);
|
||||||
|
ctx.fillStyle = `rgb(${r},${g},${b})`;
|
||||||
|
ctx.lineWidth = 3; // Thicker border for visibility
|
||||||
|
ctx.strokeStyle = '#fff'; // White border to make it pop
|
||||||
|
} else {
|
||||||
|
ctx.fillStyle = '#fff';
|
||||||
|
ctx.lineWidth = 2;
|
||||||
|
ctx.strokeStyle = '#000';
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx.fill();
|
||||||
|
ctx.stroke();
|
||||||
|
} catch (e) {
|
||||||
|
console.error("Error drawing difficulty map:", e);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const updateFromEvent = (e) => {
|
||||||
|
if (!props.interactive) return;
|
||||||
|
|
||||||
|
const canvas = canvasRef.value;
|
||||||
|
if (!canvas) return;
|
||||||
|
const rect = canvas.getBoundingClientRect();
|
||||||
|
|
||||||
|
// Handle Touch or Mouse
|
||||||
|
const clientX = e.touches ? e.touches[0].clientX : e.clientX;
|
||||||
|
const clientY = e.touches ? e.touches[0].clientY : e.clientY;
|
||||||
|
|
||||||
|
let x = clientX - rect.left;
|
||||||
|
let y = clientY - rect.top;
|
||||||
|
|
||||||
|
// Clamp
|
||||||
|
x = Math.max(0, Math.min(rect.width, x));
|
||||||
|
y = Math.max(0, Math.min(rect.height, y));
|
||||||
|
|
||||||
|
const normalizedX = x / rect.width;
|
||||||
|
const normalizedY = 1 - (y / rect.height);
|
||||||
|
|
||||||
|
const newFill = MIN_DENSITY + normalizedX * (MAX_DENSITY - MIN_DENSITY);
|
||||||
|
const newSize = MIN_SIZE + normalizedY * (MAX_SIZE - MIN_SIZE);
|
||||||
|
|
||||||
|
emit('update:density', Math.round(newFill));
|
||||||
|
emit('update:size', Math.round(newSize));
|
||||||
|
};
|
||||||
|
|
||||||
|
const startDrag = (e) => {
|
||||||
|
if (!props.interactive) return;
|
||||||
|
isDragging.value = true;
|
||||||
|
updateFromEvent(e);
|
||||||
|
window.addEventListener('mousemove', onDrag);
|
||||||
|
window.addEventListener('touchmove', onDrag, { passive: false });
|
||||||
|
window.addEventListener('mouseup', stopDrag);
|
||||||
|
window.addEventListener('touchend', stopDrag);
|
||||||
|
};
|
||||||
|
|
||||||
|
const onDrag = (e) => {
|
||||||
|
if (!isDragging.value) return;
|
||||||
|
if (e.cancelable) e.preventDefault(); // Prevent scrolling on touch
|
||||||
|
updateFromEvent(e);
|
||||||
|
};
|
||||||
|
|
||||||
|
const stopDrag = () => {
|
||||||
|
isDragging.value = false;
|
||||||
|
window.removeEventListener('mousemove', onDrag);
|
||||||
|
window.removeEventListener('touchmove', onDrag);
|
||||||
|
window.removeEventListener('mouseup', stopDrag);
|
||||||
|
window.removeEventListener('touchend', stopDrag);
|
||||||
|
};
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
nextTick(drawMap);
|
||||||
|
});
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
stopDrag(); // Cleanup just in case
|
||||||
|
});
|
||||||
|
|
||||||
|
watch(() => [props.size, props.density, props.actualDifficulty, props.width, props.height], () => {
|
||||||
|
requestAnimationFrame(drawMap);
|
||||||
|
});
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<div class="difficulty-map-container">
|
||||||
|
<canvas
|
||||||
|
ref="canvasRef"
|
||||||
|
:width="width"
|
||||||
|
:height="height"
|
||||||
|
@mousedown="startDrag"
|
||||||
|
@touchstart="startDrag"
|
||||||
|
:class="{ 'interactive': interactive }"
|
||||||
|
></canvas>
|
||||||
|
|
||||||
|
<div class="axis-labels">
|
||||||
|
<span class="y-label">{{ t('difficultyMap.size') }}</span>
|
||||||
|
<span class="x-label">{{ t('difficultyMap.density') }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.difficulty-map-container {
|
||||||
|
position: relative;
|
||||||
|
display: inline-block;
|
||||||
|
margin: 10px 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas {
|
||||||
|
border: 1px solid var(--panel-border, #444);
|
||||||
|
border-radius: 8px;
|
||||||
|
background: transparent;
|
||||||
|
cursor: default;
|
||||||
|
display: block; /* Remove inline gap */
|
||||||
|
width: 100%; /* Responsive */
|
||||||
|
max-width: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
canvas.interactive {
|
||||||
|
cursor: crosshair;
|
||||||
|
}
|
||||||
|
|
||||||
|
.axis-labels {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.y-label {
|
||||||
|
position: absolute;
|
||||||
|
left: 5px;
|
||||||
|
top: 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(0,0,0,0.5);
|
||||||
|
transform-origin: top left;
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 2px rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.x-label {
|
||||||
|
position: absolute;
|
||||||
|
right: 5px;
|
||||||
|
bottom: 5px;
|
||||||
|
font-size: 10px;
|
||||||
|
color: rgba(0,0,0,0.5);
|
||||||
|
font-weight: bold;
|
||||||
|
text-shadow: 0 0 2px rgba(255,255,255,0.8);
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -3,6 +3,7 @@ import { ref, computed, onMounted, onUnmounted, watch, nextTick } from 'vue';
|
|||||||
import { usePuzzleStore } from '@/stores/puzzle';
|
import { usePuzzleStore } from '@/stores/puzzle';
|
||||||
import { useI18n } from '@/composables/useI18n';
|
import { useI18n } from '@/composables/useI18n';
|
||||||
import { getWorkerPool } from '@/utils/workerPool';
|
import { getWorkerPool } from '@/utils/workerPool';
|
||||||
|
import DifficultyMap from './DifficultyMap.vue';
|
||||||
import { Upload, Image as ImageIcon, X, AlertTriangle, Camera, RefreshCw } from 'lucide-vue-next';
|
import { Upload, Image as ImageIcon, X, AlertTriangle, Camera, RefreshCw } from 'lucide-vue-next';
|
||||||
|
|
||||||
const emit = defineEmits(['close']);
|
const emit = defineEmits(['close']);
|
||||||
@@ -386,6 +387,7 @@ const capturePhoto = () => {
|
|||||||
|
|
||||||
onUnmounted(() => {
|
onUnmounted(() => {
|
||||||
stopCameraStream();
|
stopCameraStream();
|
||||||
|
getWorkerPool().cancelAll();
|
||||||
});
|
});
|
||||||
|
|
||||||
</script>
|
</script>
|
||||||
@@ -492,6 +494,17 @@ onUnmounted(() => {
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="imageLoaded" class="stats-panel">
|
<div v-if="imageLoaded" class="stats-panel">
|
||||||
|
<div class="difficulty-map-wrapper">
|
||||||
|
<DifficultyMap
|
||||||
|
:size="maxDimension"
|
||||||
|
:density="threshold"
|
||||||
|
:actual-difficulty="difficulty > 0 ? difficulty : null"
|
||||||
|
:interactive="false"
|
||||||
|
:width="200"
|
||||||
|
:height="200"
|
||||||
|
class="difficulty-map-mini"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
<div v-if="processing" class="loading-stats">
|
<div v-if="processing" class="loading-stats">
|
||||||
<div class="spinner"></div>
|
<div class="spinner"></div>
|
||||||
<span>{{ t('image.calculatingSolvability') || 'Calculating solvability...' }} {{ processingProgress }}%</span>
|
<span>{{ t('image.calculatingSolvability') || 'Calculating solvability...' }} {{ processingProgress }}%</span>
|
||||||
@@ -531,6 +544,24 @@ onUnmounted(() => {
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
|
.difficulty-map-wrapper {
|
||||||
|
display: flex;
|
||||||
|
justify-content: center;
|
||||||
|
margin-bottom: 15px;
|
||||||
|
background: #000;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px;
|
||||||
|
border: 1px solid var(--border-color);
|
||||||
|
}
|
||||||
|
|
||||||
|
.difficulty-map-mini :deep(canvas) {
|
||||||
|
width: 100% !important;
|
||||||
|
max-width: 200px;
|
||||||
|
height: auto !important;
|
||||||
|
aspect-ratio: 1;
|
||||||
|
border-radius: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
.loading-stats {
|
.loading-stats {
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
|||||||
@@ -197,7 +197,9 @@ const messages = {
|
|||||||
'simulation.table.size': 'Rozmiar',
|
'simulation.table.size': 'Rozmiar',
|
||||||
'simulation.table.density': 'Gęstość',
|
'simulation.table.density': 'Gęstość',
|
||||||
'simulation.table.solved': 'Rozwiązano (Logika)',
|
'simulation.table.solved': 'Rozwiązano (Logika)',
|
||||||
'simulation.empty': 'Naciśnij Start, aby uruchomić symulację Monte Carlo'
|
'simulation.empty': 'Naciśnij Start, aby uruchomić symulację Monte Carlo',
|
||||||
|
'difficultyMap.size': 'Rozmiar',
|
||||||
|
'difficultyMap.density': 'Gęstość'
|
||||||
},
|
},
|
||||||
en: {
|
en: {
|
||||||
'app.title': 'Nonograms',
|
'app.title': 'Nonograms',
|
||||||
@@ -395,7 +397,9 @@ const messages = {
|
|||||||
'simulation.table.solved': 'Solved (Logic)',
|
'simulation.table.solved': 'Solved (Logic)',
|
||||||
'simulation.empty': 'Press Start to run Monte Carlo simulation',
|
'simulation.empty': 'Press Start to run Monte Carlo simulation',
|
||||||
'custom.hideMap': 'Hide difficulty map',
|
'custom.hideMap': 'Hide difficulty map',
|
||||||
'custom.showMap': 'Show difficulty map'
|
'custom.showMap': 'Show difficulty map',
|
||||||
|
'difficultyMap.size': 'Size',
|
||||||
|
'difficultyMap.density': 'Density'
|
||||||
},
|
},
|
||||||
zh: {
|
zh: {
|
||||||
'app.title': 'Nonograms',
|
'app.title': 'Nonograms',
|
||||||
|
|||||||
@@ -1,27 +0,0 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { solvePuzzle } from './solver';
|
|
||||||
import { calculateHints } from './puzzleUtils';
|
|
||||||
|
|
||||||
describe('Debug Solver', () => {
|
|
||||||
it('should solve the broken grid', () => {
|
|
||||||
const grid = [
|
|
||||||
[0,1,1,1,0,0,1,0,1,1],
|
|
||||||
[1,1,1,0,0,1,1,1,0,0],
|
|
||||||
[1,0,1,0,1,0,0,1,0,0],
|
|
||||||
[1,0,0,0,1,1,1,1,0,1],
|
|
||||||
[1,1,0,1,0,0,0,1,0,1],
|
|
||||||
[1,0,1,0,1,0,0,0,1,0],
|
|
||||||
[1,1,1,0,0,1,1,0,0,0],
|
|
||||||
[0,1,0,0,1,0,1,0,0,0],
|
|
||||||
[0,0,0,1,1,0,0,0,1,0],
|
|
||||||
[1,0,1,1,0,0,1,0,1,1]
|
|
||||||
];
|
|
||||||
|
|
||||||
const { rowHints, colHints } = calculateHints(grid);
|
|
||||||
const result = solvePuzzle(rowHints, colHints);
|
|
||||||
|
|
||||||
console.log('Solve Result:', result);
|
|
||||||
expect(result.percentSolved).toBe(100);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,44 +0,0 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { solvePuzzle } from './solver.js';
|
|
||||||
|
|
||||||
describe('Large Grid Solver', () => {
|
|
||||||
it('should solve a large 55x28 grid without crashing', () => {
|
|
||||||
const rows = 28;
|
|
||||||
const cols = 55;
|
|
||||||
// Create a simple pattern: checkerboard or lines
|
|
||||||
const grid = Array(rows).fill().map((_, r) =>
|
|
||||||
Array(cols).fill().map((_, c) => (r + c) % 2 === 0 ? 1 : 0)
|
|
||||||
);
|
|
||||||
|
|
||||||
// Calculate hints
|
|
||||||
const rowHints = grid.map(row => {
|
|
||||||
const hints = [];
|
|
||||||
let current = 0;
|
|
||||||
row.forEach(cell => {
|
|
||||||
if (cell === 1) current++;
|
|
||||||
else if (current > 0) { hints.push(current); current = 0; }
|
|
||||||
});
|
|
||||||
if (current > 0) hints.push(current);
|
|
||||||
return hints.length ? hints : [0];
|
|
||||||
});
|
|
||||||
|
|
||||||
const colHints = Array(cols).fill().map((_, c) => {
|
|
||||||
const hints = [];
|
|
||||||
let current = 0;
|
|
||||||
for(let r=0; r<rows; r++) {
|
|
||||||
if (grid[r][c] === 1) current++;
|
|
||||||
else if (current > 0) { hints.push(current); current = 0; }
|
|
||||||
}
|
|
||||||
if (current > 0) hints.push(current);
|
|
||||||
return hints.length ? hints : [0];
|
|
||||||
});
|
|
||||||
|
|
||||||
console.log('Starting solve...');
|
|
||||||
const result = solvePuzzle(rowHints, colHints, (p) => console.log(`Progress: ${p}%`));
|
|
||||||
console.log('Result:', result);
|
|
||||||
|
|
||||||
expect(result.percentSolved).toBeGreaterThan(0);
|
|
||||||
expect(result.difficultyScore).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -61,7 +61,6 @@ export function generateRandomGrid(size, density = 0.5) {
|
|||||||
return grid;
|
return grid;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function calculateDifficulty(density, size = 10) {
|
|
||||||
// Data derived from Monte Carlo Simulation (Logical Solver)
|
// Data derived from Monte Carlo Simulation (Logical Solver)
|
||||||
// Format: { size: [solved_pct_at_0.1, ..., solved_pct_at_0.9] }
|
// Format: { size: [solved_pct_at_0.1, ..., solved_pct_at_0.9] }
|
||||||
const SIM_DATA = {
|
const SIM_DATA = {
|
||||||
@@ -83,17 +82,22 @@ export function calculateDifficulty(density, size = 10) {
|
|||||||
80: [0, 0, 0, 0, 0, 4, 100, 100, 100]
|
80: [0, 0, 0, 0, 0, 4, 100, 100, 100]
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const SIM_SIZES = Object.keys(SIM_DATA).map(Number).sort((a, b) => a - b);
|
||||||
|
|
||||||
|
export function calculateDifficulty(density, size = 10) {
|
||||||
|
density = Number(density);
|
||||||
|
size = Number(size);
|
||||||
|
|
||||||
// Helper to get interpolated value from array
|
// Helper to get interpolated value from array
|
||||||
const getSimulatedSolvedPct = (s, d) => {
|
const getSimulatedSolvedPct = (s, d) => {
|
||||||
// Find closest sizes
|
// Find closest sizes
|
||||||
const sizes = Object.keys(SIM_DATA).map(Number).sort((a, b) => a - b);
|
let sLower = SIM_SIZES[0];
|
||||||
let sLower = sizes[0];
|
let sUpper = SIM_SIZES[SIM_SIZES.length - 1];
|
||||||
let sUpper = sizes[sizes.length - 1];
|
|
||||||
|
|
||||||
for (let i = 0; i < sizes.length - 1; i++) {
|
for (let i = 0; i < SIM_SIZES.length - 1; i++) {
|
||||||
if (s >= sizes[i] && s <= sizes[i+1]) {
|
if (s >= SIM_SIZES[i] && s <= SIM_SIZES[i+1]) {
|
||||||
sLower = sizes[i];
|
sLower = SIM_SIZES[i];
|
||||||
sUpper = sizes[i+1];
|
sUpper = SIM_SIZES[i+1];
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,49 +0,0 @@
|
|||||||
|
|
||||||
import { describe, it, expect } from 'vitest';
|
|
||||||
import { solvePuzzle } from './solver';
|
|
||||||
import { calculateHints } from './puzzleUtils';
|
|
||||||
|
|
||||||
describe('Solver Repro', () => {
|
|
||||||
it('should solve a simple generated puzzle', () => {
|
|
||||||
const grid = [
|
|
||||||
[1, 0, 1, 1, 0],
|
|
||||||
[1, 1, 0, 0, 1],
|
|
||||||
[0, 0, 1, 0, 0],
|
|
||||||
[1, 1, 1, 1, 1],
|
|
||||||
[0, 1, 0, 1, 0]
|
|
||||||
];
|
|
||||||
const { rowHints, colHints } = calculateHints(grid);
|
|
||||||
|
|
||||||
const result = solvePuzzle(rowHints, colHints);
|
|
||||||
expect(result.percentSolved).toBe(100);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not fail on random valid lines', () => {
|
|
||||||
// Test solveLine indirectly via solvePuzzle on small grids
|
|
||||||
for (let i = 0; i < 100; i++) {
|
|
||||||
const size = 10;
|
|
||||||
const grid = [];
|
|
||||||
for(let r=0; r<size; r++) {
|
|
||||||
const row = [];
|
|
||||||
for(let c=0; c<size; c++) row.push(Math.random() > 0.5 ? 1 : 0);
|
|
||||||
grid.push(row);
|
|
||||||
}
|
|
||||||
|
|
||||||
const { rowHints, colHints } = calculateHints(grid);
|
|
||||||
const result = solvePuzzle(rowHints, colHints);
|
|
||||||
|
|
||||||
// It might not be 100% solvable without guessing (logic only),
|
|
||||||
// but since our solver HAS backtracking, it MUST be 100% solvable
|
|
||||||
// (unless timeout/max depth reached, but for 10x10 it should solve).
|
|
||||||
|
|
||||||
// If it returns 0% or low %, it implies it failed to find the solution
|
|
||||||
// or found a contradiction (which shouldn't happen for valid hints).
|
|
||||||
|
|
||||||
if (result.percentSolved < 100) {
|
|
||||||
console.log('Failed Grid:', JSON.stringify(grid));
|
|
||||||
console.log('Result:', result);
|
|
||||||
}
|
|
||||||
expect(result.percentSolved).toBe(100);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
@@ -1,3 +1,8 @@
|
|||||||
|
import { describe, it, expect } from 'vitest';
|
||||||
|
import { solvePuzzle } from './solver';
|
||||||
|
import { calculateHints, generateRandomGrid } from './puzzleUtils';
|
||||||
|
|
||||||
|
describe('Solver', () => {
|
||||||
it('solves a puzzle requiring guessing (Backtracking)', () => {
|
it('solves a puzzle requiring guessing (Backtracking)', () => {
|
||||||
// A puzzle that logic alone cannot start usually has multiple solutions or requires a guess.
|
// A puzzle that logic alone cannot start usually has multiple solutions or requires a guess.
|
||||||
// Example: The "domino" or "ambiguous" pattern, but we need a unique solution that requires lookahead.
|
// Example: The "domino" or "ambiguous" pattern, but we need a unique solution that requires lookahead.
|
||||||
@@ -61,4 +66,81 @@
|
|||||||
expect(result.percentSolved).toBe(100);
|
expect(result.percentSolved).toBe(100);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Merged from repro_solver.test.js
|
||||||
|
it('should solve a simple generated puzzle', () => {
|
||||||
|
const grid = [
|
||||||
|
[1, 0, 1, 1, 0],
|
||||||
|
[1, 1, 0, 0, 1],
|
||||||
|
[0, 0, 1, 0, 0],
|
||||||
|
[1, 1, 1, 1, 1],
|
||||||
|
[0, 1, 0, 1, 0]
|
||||||
|
];
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merged from debug_solver.test.js
|
||||||
|
it('should solve the broken grid (debug case)', () => {
|
||||||
|
const grid = [
|
||||||
|
[0,1,1,1,0,0,1,0,1,1],
|
||||||
|
[1,1,1,0,0,1,1,1,0,0],
|
||||||
|
[1,0,1,0,1,0,0,1,0,0],
|
||||||
|
[1,0,0,0,1,1,1,1,0,1],
|
||||||
|
[1,1,0,1,0,0,0,1,0,1],
|
||||||
|
[1,0,1,0,1,0,0,0,1,0],
|
||||||
|
[1,1,1,0,0,1,1,0,0,0],
|
||||||
|
[0,1,0,0,1,0,1,0,0,0],
|
||||||
|
[0,0,0,1,1,0,0,0,1,0],
|
||||||
|
[1,0,1,1,0,0,1,0,1,1]
|
||||||
|
];
|
||||||
|
|
||||||
|
const { rowHints, colHints } = calculateHints(grid);
|
||||||
|
const result = solvePuzzle(rowHints, colHints);
|
||||||
|
|
||||||
|
// console.log('Solve Result:', result);
|
||||||
|
expect(result.percentSolved).toBe(100);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Merged from large_grid_solver.test.js
|
||||||
|
it('should solve a large 55x28 grid without crashing', () => {
|
||||||
|
const rows = 28;
|
||||||
|
const cols = 55;
|
||||||
|
// Create a simple pattern: checkerboard or lines
|
||||||
|
const grid = Array(rows).fill().map((_, r) =>
|
||||||
|
Array(cols).fill().map((_, c) => (r + c) % 2 === 0 ? 1 : 0)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Calculate hints
|
||||||
|
const rowHints = grid.map(row => {
|
||||||
|
const hints = [];
|
||||||
|
let current = 0;
|
||||||
|
row.forEach(cell => {
|
||||||
|
if (cell === 1) current++;
|
||||||
|
else if (current > 0) { hints.push(current); current = 0; }
|
||||||
|
});
|
||||||
|
if (current > 0) hints.push(current);
|
||||||
|
return hints.length ? hints : [0];
|
||||||
|
});
|
||||||
|
|
||||||
|
const colHints = Array(cols).fill().map((_, c) => {
|
||||||
|
const hints = [];
|
||||||
|
let current = 0;
|
||||||
|
for(let r=0; r<rows; r++) {
|
||||||
|
if (grid[r][c] === 1) current++;
|
||||||
|
else if (current > 0) { hints.push(current); current = 0; }
|
||||||
|
}
|
||||||
|
if (current > 0) hints.push(current);
|
||||||
|
return hints.length ? hints : [0];
|
||||||
|
});
|
||||||
|
|
||||||
|
// console.log('Starting solve...');
|
||||||
|
const result = solvePuzzle(rowHints, colHints); // Removed console.log callback to reduce noise
|
||||||
|
// console.log('Result:', result);
|
||||||
|
|
||||||
|
expect(result.percentSolved).toBeGreaterThan(0);
|
||||||
|
expect(result.difficultyScore).toBeDefined();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
107
verify_i18n.js
107
verify_i18n.js
@@ -1,107 +0,0 @@
|
|||||||
|
|
||||||
import fs from 'fs';
|
|
||||||
import path from 'path';
|
|
||||||
import { fileURLToPath } from 'url';
|
|
||||||
import { execSync } from 'child_process';
|
|
||||||
|
|
||||||
const __filename = fileURLToPath(import.meta.url);
|
|
||||||
const __dirname = path.dirname(__filename);
|
|
||||||
|
|
||||||
// 1. Parse useI18n.js to get keys
|
|
||||||
const i18nPath = path.join(__dirname, 'src/composables/useI18n.js');
|
|
||||||
const i18nContent = fs.readFileSync(i18nPath, 'utf8');
|
|
||||||
|
|
||||||
function extractKeys(lang) {
|
|
||||||
// Find start of lang block: " en: {"
|
|
||||||
const startRegex = new RegExp(`\\s+${lang}:\\s*\\{`);
|
|
||||||
const startMatch = i18nContent.match(startRegex);
|
|
||||||
if (!startMatch) return new Set();
|
|
||||||
|
|
||||||
const startIndex = startMatch.index + startMatch[0].length;
|
|
||||||
let braceCount = 1;
|
|
||||||
let inString = false;
|
|
||||||
let stringChar = '';
|
|
||||||
let endIndex = -1;
|
|
||||||
|
|
||||||
for (let i = startIndex; i < i18nContent.length; i++) {
|
|
||||||
const char = i18nContent[i];
|
|
||||||
|
|
||||||
if (inString) {
|
|
||||||
if (char === stringChar && i18nContent[i-1] !== '\\') {
|
|
||||||
inString = false;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
if (char === "'" || char === '"' || char === '`') {
|
|
||||||
inString = true;
|
|
||||||
stringChar = char;
|
|
||||||
} else if (char === '{') {
|
|
||||||
braceCount++;
|
|
||||||
} else if (char === '}') {
|
|
||||||
braceCount--;
|
|
||||||
if (braceCount === 0) {
|
|
||||||
endIndex = i;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (endIndex === -1) return new Set();
|
|
||||||
|
|
||||||
const block = i18nContent.substring(startIndex, endIndex);
|
|
||||||
const keys = new Set();
|
|
||||||
const keyRegex = /['"]([\w.-]+)['"]\s*:/g;
|
|
||||||
let match;
|
|
||||||
while ((match = keyRegex.exec(block)) !== null) {
|
|
||||||
keys.add(match[1]);
|
|
||||||
}
|
|
||||||
return keys;
|
|
||||||
}
|
|
||||||
|
|
||||||
const enKeys = extractKeys('en');
|
|
||||||
console.log(`Found ${enKeys.size} keys in en block.`);
|
|
||||||
|
|
||||||
if (enKeys.has('image.title')) {
|
|
||||||
console.log("'image.title' IS present in en block.");
|
|
||||||
} else {
|
|
||||||
console.log("'image.title' is MISSING in en block.");
|
|
||||||
}
|
|
||||||
|
|
||||||
// 2. Scan src for usages
|
|
||||||
try {
|
|
||||||
const grepOutput = execSync(`grep -r "t(['\\"]" src | grep -v "node_modules"`, { encoding: 'utf8' });
|
|
||||||
|
|
||||||
const usedKeys = new Set();
|
|
||||||
const usageRegex = /t\(['"]([\w.-]+)['"]/g;
|
|
||||||
|
|
||||||
const lines = grepOutput.split('\n');
|
|
||||||
for (const line of lines) {
|
|
||||||
let m;
|
|
||||||
while ((m = usageRegex.exec(line)) !== null) {
|
|
||||||
usedKeys.add(m[1]);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
console.log(`Found ${usedKeys.size} used keys in src.`);
|
|
||||||
|
|
||||||
// 3. Compare
|
|
||||||
const missingKeys = [];
|
|
||||||
for (const key of usedKeys) {
|
|
||||||
// Skip dynamic keys or composed keys if any (heuristic)
|
|
||||||
if (key.includes('${')) continue;
|
|
||||||
|
|
||||||
if (!enKeys.has(key)) {
|
|
||||||
missingKeys.push(key);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if (missingKeys.length > 0) {
|
|
||||||
console.log("Missing translations in en:");
|
|
||||||
missingKeys.forEach(k => console.log(` - ${k}`));
|
|
||||||
} else {
|
|
||||||
console.log("No missing translations found in en.");
|
|
||||||
}
|
|
||||||
|
|
||||||
} catch (e) {
|
|
||||||
console.error("Error running grep:", e);
|
|
||||||
}
|
|
||||||
Reference in New Issue
Block a user