inventory/inventory/templates/testing.html
2025-12-08 16:32:53 -06:00

546 lines
No EOL
20 KiB
HTML

{% extends 'base.html' %}
{% set dot_size = [grid_size * 1.25, 32]|max|int %}
{% block style %}
:root {
--grid: {{ grid_size }}px;
}
#grid {
background-image:
linear-gradient(to right, #ccc 1px, transparent 1px),
linear-gradient(to bottom, #ccc 1px, transparent 1px);
background-size: var(--grid) var(--grid);
cursor: crosshair;
height: 80vh;
width: 100%;
}
@supports (height: calc(round(nearest, 80vh, {{ grid_size }}px))) {
#grid {
height: calc(round(nearest, 80vh, var(--grid)) + 1px);
width: calc(round(nearest, 100%, var(--grid)) + 1px);
}
}
#toolBar {
top: 10px;
transform: translateX(-50%);
z-index: 10000;
}
#coords {
bottom: 10px;
pointer-events: none;
left: 10px;
}
#overlay {
z-index: 9999;
pointer-events: none;
inset: 0;
}
{% endblock %}
{% block main %}
<div class="container">
<div id="grid" class="position-relative overflow-hidden">
<div id="toolBar"
class="btn-toolbar bg-light position-absolute border border-secondary-subtle rounded start-50 p-1">
<input type="color" class="form-control form-control-sm form-control-color" id="color">
<div class="vr mx-1"></div>
<div class="btn-group">
<input type="radio" class="btn-check" id="outline" name="tool" checked>
<label for="outline"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-square" viewBox="0 0 16 16">
<path
d="M14 1a1 1 0 0 1 1 1v12a1 1 0 0 1-1 1H2a1 1 0 0 1-1-1V2a1 1 0 0 1 1-1zM2 0a2 2 0 0 0-2 2v12a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V2a2 2 0 0 0-2-2z" />
</svg>
</label>
<input type="radio" class="btn-check" id="filled" name="tool">
<label for="filled"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-square-fill" viewBox="0 0 16 16">
<path d="M0 2a2 2 0 0 1 2-2h12a2 2 0 0 1 2 2v12a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2z" />
</svg>
</label>
<input type="radio" class="btn-check" id="line" name="tool">
<label for="line"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
</label>
</div>
<div class="vr mx-1"></div>
<div class="btn-group">
<input type="radio" class="btn-check" name="gridType" id="fullGrid">
<label for="fullGrid"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-border-all" viewBox="0 0 16 16">
<path d="M0 0h16v16H0zm1 1v6.5h6.5V1zm7.5 0v6.5H15V1zM15 8.5H8.5V15H15zM7.5 15V8.5H1V15z" />
</svg>
</label>
<input type="radio" class="btn-check" name="gridType" id="horizontalGrid">
<label for="horizontalGrid"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-border-center" viewBox="0 0 16 16">
<path
d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM7.531.969V1h.938V.969H8.5V.5h-.031V0H7.53v.5H7.5v.469zM9.406 1h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM1 4.719V3.78H0v.938h1zm6.5-.938v.938h1V3.78h-1zm7.5 0v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm6.5-.938v.938h1v-.938zm7.5 0v.938h1v-.938zM0 8.5v-1h16v1zm0 .906v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zm-16 .937v.938h1v-.938zm7.5 0v.938h1v-.938zm8.5.938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm1.875-.5v.5h.938v-.5H8.5v-.469h-.031V15H7.53v.031H7.5v.469zm1.875.5h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" />
</svg>
</label>
<input type="radio" class="btn-check" name="gridType" id="verticalGrid">
<label for="verticalGrid"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-border-middle" viewBox="0 0 16 16">
<path
d="M.969 0H0v.969h.5V1h.469V.969H1V.5H.969zm.937 1h.938V0h-.938zm1.875 0h.938V0H3.78v1zm1.875 0h.938V0h-.938zM8.5 16h-1V0h1zm.906-15h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.938V0h-.938zm1.875 0h.469V.969h.5V0h-.969v.5H15v.469h.031zM1 2.844v-.938H0v.938zm14-.938v.938h1v-.938zM1 4.719V3.78H0v.938h1zm14-.938v.938h1V3.78h-1zM1 6.594v-.938H0v.938zm14-.938v.938h1v-.938zM.5 8.5h.469v-.031H1V7.53H.969V7.5H.5v.031H0v.938h.5zm1.406 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.469v-.031h.5V7.53h-.5V7.5h-.469v.031H15v.938h.031zM0 9.406v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zm-16 .937v.938h1v-.938zm16 .938v-.938h-1v.938zM0 16h.969v-.5H1v-.469H.969V15H.5v.031H0zm1.906 0h.938v-1h-.938zm1.875 0h.938v-1H3.78v1zm1.875 0h.938v-1h-.938zm3.75 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875 0h.938v-1h-.938zm1.875-.5v.5H16v-.969h-.5V15h-.469v.031H15v.469z" />
</svg>
</label>
<input type="radio" class="btn-check" name="gridType" id="noGrid">
<label for="noGrid"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-border" viewBox="0 0 16 16">
<path
d="M0 0h.969v.5H1v.469H.969V1H.5V.969H0zm2.844 1h-.938V0h.938zm1.875 0H3.78V0h.938v1zm1.875 0h-.938V0h.938zm.937 0V.969H7.5V.5h.031V0h.938v.5H8.5v.469h-.031V1zm2.813 0h-.938V0h.938zm1.875 0h-.938V0h.938zm1.875 0h-.938V0h.938zM15.5 1h-.469V.969H15V.5h.031V0H16v.969h-.5zM1 1.906v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM1 3.78v.938H0V3.78zm6.5.938V3.78h1v.938zm7.5 0V3.78h1v.938zM1 5.656v.938H0v-.938zm6.5.938v-.938h1v.938zm7.5 0v-.938h1v.938zM.969 8.5H.5v-.031H0V7.53h.5V7.5h.469v.031H1v.938H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm1.875-.031V8.5H7.53v-.031H7.5V7.53h.031V7.5h.938v.031H8.5v.938zm1.875.031h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.406 0h-.469v-.031H15V7.53h.031V7.5h.469v.031h.5v.938h-.5zM0 10.344v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 12.22v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM0 14.094v-.938h1v.938zm7.5 0v-.938h1v.938zm8.5-.938v.938h-1v-.938zM.969 16H0v-.969h.5V15h.469v.031H1v.469H.969zm1.875 0h-.938v-1h.938zm1.875 0H3.78v-1h.938v1zm1.875 0h-.938v-1h.938zm.937 0v-.5H7.5v-.469h.031V15h.938v.031H8.5v.469h-.031v.5zm2.813 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm1.875 0h-.938v-1h.938zm.937 0v-.5H15v-.469h.031V15h.469v.031h.5V16z" />
</svg>
</label>
</div>
<div class="vr mx-1"></div>
<div class="btn-group">
<button type="button"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
id="export">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-download" viewBox="0 0 16 16">
<path
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />
<path
d="M7.646 11.854a.5.5 0 0 0 .708 0l3-3a.5.5 0 0 0-.708-.708L8.5 10.293V1.5a.5.5 0 0 0-1 0v8.793L5.354 8.146a.5.5 0 1 0-.708.708z" />
</svg>
</button>
<input type="file" id="import" accept="application/json" class="d-none">
<button type="button"
class="btn btn-sm btn-light border d-inline-flex align-items-center justify-content-center"
id="importButton">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-upload" viewBox="0 0 16 16">
<path
d="M.5 9.9a.5.5 0 0 1 .5.5v2.5a1 1 0 0 0 1 1h12a1 1 0 0 0 1-1v-2.5a.5.5 0 0 1 1 0v2.5a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2v-2.5a.5.5 0 0 1 .5-.5" />
<path
d="M7.646 1.146a.5.5 0 0 1 .708 0l3 3a.5.5 0 0 1-.708.708L8.5 2.707V11.5a.5.5 0 0 1-1 0V2.707L5.354 4.854a.5.5 0 1 1-.708-.708z" />
</svg>
</button>
<button type="button"
class="btn btn-sm btn-danger border d-inline-flex align-items-center justify-content-center"
id="clear">
<svg xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor"
class="bi bi-x-lg" viewBox="0 0 16 16">
<path
d="M2.146 2.854a.5.5 0 1 1 .708-.708L8 7.293l5.146-5.147a.5.5 0 0 1 .708.708L8.707 8l5.147 5.146a.5.5 0 0 1-.708.708L8 8.707l-5.146 5.147a.5.5 0 0 1-.708-.708L7.293 8z" />
</svg>
</button>
</div>
</div>
<span id="dot" class="position-absolute p-0 m-0 d-none">
<svg xmlns="http://www.w3.org/2000/svg" width="{{ dot_size }}" height="{{ dot_size }}" viewBox="0 0 32 32"
id="dotSVG">
<circle cx="16" cy="16" r="4" fill="black" />
</svg>
</span>
<div id="coords"
class="border border-black position-absolute d-none bg-warning-subtle px-1 py-0 user-select-none"></div>
<canvas id="overlay" class="w-100 h-100"></canvas>
</div>
</div>
{% endblock %}
{% block script %}
const canvasEl = document.getElementById('overlay');
const clearEl = document.getElementById('clear');
const colorEl = document.getElementById('color');
const coordsEl = document.getElementById('coords');
const dotEl = document.getElementById('dot');
const dotSVGEl = document.getElementById('dotSVG');
const exportEl = document.getElementById('export');
const gridEl = document.getElementById('grid');
const importButtonEl = document.getElementById('importButton');
const importEl = document.getElementById('import');
let ctx;
let dpr = 1;
let selectedColor;
let currentShape = null;
let shapes = loadShapes();
const savedTool = localStorage.getItem('gridTool');
if (savedTool) {
setActiveTool(savedTool);
}
resizeAndSetupCanvas();
window.addEventListener('resize', resizeAndSetupCanvas);
function getActiveTool() {
const checked = document.querySelector('input[name="tool"]:checked');
return checked ? checked.id : 'outline';
}
function setActiveTool(toolId) {
const el = document.getElementById(toolId);
if (el) {
el.checked = true;
localStorage.setItem('gridTool', toolId);
}
}
function snapToGrid(x, y) {
const rect = gridEl.getBoundingClientRect();
const clampedX = Math.min(Math.max(x, rect.left), rect.right);
const clampedY = Math.min(Math.max(y, rect.top), rect.bottom);
const localX = clampedX - rect.left;
const localY = clampedY - rect.top;
const maxIx = Math.floor(rect.width / {{ grid_size }});
const maxIy = Math.floor(rect.height / {{ grid_size }});
const ix = Math.min(Math.max(Math.round(localX / {{ grid_size }}), 0), maxIx);
const iy = Math.min(Math.max(Math.round(localY / {{ grid_size }}), 0), maxIy);
return {
ix,
iy,
x: ix * {{ grid_size }},
y: iy * {{ grid_size }}
};
}
function normalizeRect(shape) {
const ix1 = shape.x1 / {{ grid_size }};
const iy1 = shape.y1 / {{ grid_size }};
const ix2 = shape.x2 / {{ grid_size }};
const iy2 = shape.y2 / {{ grid_size }};
const ix = Math.min(ix1, ix2);
const iy = Math.min(iy1, iy2);
const iw = Math.abs(ix2 - ix1);
const ih = Math.abs(iy2 - iy1);
return {
type: 'rect',
ix,
iy,
iw,
ih,
color: shape.color,
fill: shape.fill
};
}
function normalizeLine(shape) {
const ix1 = shape.x1 / {{ grid_size }};
const iy1 = shape.y1 / {{ grid_size }};
const ix2 = shape.x2 / {{ grid_size }};
const iy2 = shape.y2 / {{ grid_size }};
return {
type: 'line',
ix1,
iy1,
ix2,
iy2,
color: shape.color
};
}
function resizeAndSetupCanvas() {
dpr = window.devicePixelRatio || 1;
const rect = canvasEl.getBoundingClientRect();
canvasEl.width = rect.width * dpr;
canvasEl.height = rect.height * dpr;
ctx = canvasEl.getContext('2d');
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
selectedColor = colorEl.value || '#000000';
const circle = dotSVGEl.querySelector('circle');
if (circle) {
circle.setAttribute('fill', selectedColor);
}
redrawAll();
}
function redrawAll() {
if (!ctx || !shapes) return;
clearCanvas();
shapes.forEach(drawShape);
}
function drawShape(shape) {
if (!ctx) return;
ctx.save();
ctx.strokeStyle = shape.color || '#000000';
if (shape.type === 'rect') {
const x = shape.ix * {{ grid_size }};
const y = shape.iy * {{ grid_size }};
const w = shape.iw * {{ grid_size }};
const h = shape.ih * {{ grid_size }};
ctx.strokeRect(x, y, w, h);
if (shape.fill) {
ctx.globalAlpha = 0.15;
ctx.fillStyle = shape.color;
ctx.fillRect(x, y, w, h);
ctx.globalAlpha = 1;
}
} else if (shape.type === 'line') {
const x1 = shape.ix1 * {{ grid_size }};
const y1 = shape.iy1 * {{ grid_size }};
const x2 = shape.ix2 * {{ grid_size }};
const y2 = shape.iy2 * {{ grid_size }};
ctx.beginPath();
ctx.moveTo(x1, y1);
ctx.lineTo(x2, y2);
ctx.stroke();
}
ctx.restore();
}
function clearCanvas() {
if (!ctx) return;
ctx.clearRect(0, 0, canvasEl.width / dpr, canvasEl.height / dpr);
}
function loadShapes() {
try {
const raw = localStorage.getItem('gridShapes');
if (!raw) return [];
return JSON.parse(raw);
} catch {
return [];
}
}
function saveShapes() {
try {
localStorage.setItem('gridShapes', JSON.stringify(shapes));
} catch {}
}
document.querySelectorAll('input[name="tool"]').forEach(input => {
input.addEventListener('change', () => {
if (input.checked) {
localStorage.setItem('gridTool', input.id);
}
});
});
importButtonEl.addEventListener('click', () => importEl.click());
importEl.addEventListener('change', (e) => {
const file = e.target.files[0];
if (!file) return;
const reader = new FileReader();
reader.onload = () => {
try {
const data = JSON.parse(reader.result);
const loadedShapes = Array.isArray(data) ? data : data.shapes;
if (!Array.isArray(loadedShapes)) return;
shapes = loadedShapes;
saveShapes();
redrawAll();
} catch {
toastMessage('Failed to load data from JSON file.', 'danger');
}
};
reader.readAsText(file);
});
exportEl.addEventListener('click', () => {
const payload = {
version: 1,
gridSize: {{ grid_size }},
shapes
};
const blob = new Blob([JSON.stringify(payload, null,2)], {type: 'application/json'});
const url = URL.createObjectURL(blob);
const a = document.createElement('a');
a.href = url;
a.download = 'grid-shapes.json';
a.click();
URL.revokeObjectURL(url);
});
clearEl.addEventListener('click', () => {
shapes = [];
saveShapes();
redrawAll();
});
colorEl.addEventListener('input', () => {
selectedColor = document.getElementById('color').value;
const circle = dotSVGEl.querySelector('circle');
if (circle) {
circle.setAttribute('fill', selectedColor);
}
});
document.addEventListener('keydown', (e) => {
const key = e.key.toLowerCase();
if ((e.ctrlKey || e.metaKey) && key === 'z') {
e.preventDefault();
if (shapes.length > 0) {
shapes.pop();
saveShapes();
redrawAll();
}
}
if (key === 'escape' && currentShape) {
currentShape = null;
redrawAll();
}
});
gridEl.addEventListener('pointermove', (e) => {
if (!ctx) return;
const { ix, iy, x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
coordsEl.innerText = `(${ix}, ${iy})`;
const renderX = snapX - {{ dot_size }} / 2;
const renderY = snapY - {{ dot_size }} / 2;
coordsEl.classList.remove('d-none');
dotEl.classList.remove('d-none');
dotEl.style.top = `${renderY}px`;
dotEl.style.left = `${renderX}px`;
if (currentShape) {
const tool = currentShape.tool;
clearCanvas();
shapes.forEach(drawShape);
ctx.save();
ctx.setLineDash([5, 3]);
if (tool === 'line') {
const previewLine = normalizeLine({
type: 'line',
x1: currentShape.x1,
y1: currentShape.y1,
x2: snapX,
y2: snapY,
color: currentShape.color
});
drawShape(previewLine);
} else {
const previewRect = normalizeRect({
...currentShape,
x2: snapX,
y2: snapY
});
drawShape(previewRect);
}
ctx.setLineDash([]);
ctx.restore();
}
});
gridEl.addEventListener('pointerleave', (e) => {
coordsEl.classList.add('d-none');
dotEl.classList.add('d-none');
});
gridEl.addEventListener('pointerdown', (e) => {
if (e.button !== 0) return;
e.preventDefault();
if (e.target.closest('#toolBar')) return;
const { x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
const tool = getActiveTool();
if (tool === 'line') {
currentShape = {
tool,
type: 'line',
x1: snapX,
y1: snapY,
x2: snapX,
y2: snapY,
color: selectedColor
};
} else {
currentShape = {
tool,
x1: snapX,
y1: snapY,
x2: snapX,
y2: snapY,
color: selectedColor,
fill: document.getElementById('filled').checked
};
}
});
window.addEventListener('pointerup', (e) => {
if (!currentShape) return;
const { x: snapX, y: snapY } = snapToGrid(e.clientX, e.clientY);
currentShape.x2 = snapX;
currentShape.y2 = snapY;
let finalShape = null;
if (currentShape.tool === 'line') {
const line = normalizeLine(currentShape);
if (line.ix1 !== line.ix2 || line.iy1 !== line.iy2) {
finalShape = line;
}
} else {
const rect = normalizeRect(currentShape);
if (rect.iw > 0 && rect.ih > 0) {
finalShape = rect;
}
}
if (finalShape) {
shapes.push(finalShape);
saveShapes();
}
clearCanvas();
shapes.forEach(drawShape);
currentShape = null;
});
{% endblock %}