diff --git a/client/blueprints/maker.py b/client/blueprints/maker.py index 1d616b7..f0dd07e 100644 --- a/client/blueprints/maker.py +++ b/client/blueprints/maker.py @@ -107,6 +107,30 @@ def task_editor(recipe_id: int): return render_template("maker/task_editor.html", recipe=recipe) +@maker_bp.route("/recipes//tasks//drawing") +@login_required +@role_required("Maker") +def task_drawing(recipe_id: int, task_id: int): + """Annotation editor for a specific task.""" + # Load recipe for breadcrumb context + recipe_resp = api_client.get(f"/api/recipes/{recipe_id}") + if recipe_resp.get("error"): + flash(_("Errore nel caricamento della ricetta: %(error)s", error=recipe_resp.get("detail", "")), "error") + return redirect(url_for("maker.recipe_list")) + + # Load task details + task_resp = api_client.get(f"/api/tasks/{task_id}") + if task_resp.get("error"): + flash(_("Task non trovato: %(error)s", error=task_resp.get("detail", "")), "error") + return redirect(url_for("maker.task_editor", recipe_id=recipe_id)) + + return render_template( + "maker/task_drawing.html", + recipe=recipe_resp, + task=task_resp, + ) + + @maker_bp.route("/recipes//preview") @login_required @role_required("Maker") diff --git a/client/blueprints/measure.py b/client/blueprints/measure.py index fc8ad9c..8206d2d 100644 --- a/client/blueprints/measure.py +++ b/client/blueprints/measure.py @@ -1,11 +1,14 @@ """MeasurementTec blueprint - recipe selection and measurement execution.""" +import requests as http_requests + from flask import ( - Blueprint, flash, jsonify, redirect, render_template, + Blueprint, Response, flash, jsonify, redirect, render_template, request, session, url_for, ) from flask_babel import gettext as _ from blueprints.auth import login_required, role_required +from config import Config from services.api_client import api_client measure_bp = Blueprint("measure", __name__) @@ -272,3 +275,25 @@ def save_measurement(): }), status_code if status_code >= 400 else 500 return jsonify(resp), 201 + + +# --------------------------------------------------------------------------- +# Route: File proxy (browser can't send X-API-Key directly) +# --------------------------------------------------------------------------- +@measure_bp.route("/api/files/", methods=["GET"]) +@login_required +def api_get_file(file_path: str): + """Proxy: Serve file from API server (browser can't send X-API-Key).""" + api_key = session.get("api_key", "") + base_url = Config.API_SERVER_URL.rstrip("/") + resp = http_requests.get( + f"{base_url}/api/files/{file_path}", + headers={"X-API-Key": api_key}, + timeout=30, + ) + if resp.status_code != 200: + return Response(resp.text, status=resp.status_code) + return Response( + resp.content, + content_type=resp.headers.get("content-type", "application/octet-stream"), + ) diff --git a/client/static/js/annotation-editor.js b/client/static/js/annotation-editor.js index c77ffd2..1b2b500 100644 --- a/client/static/js/annotation-editor.js +++ b/client/static/js/annotation-editor.js @@ -52,21 +52,27 @@ function annotationEditor() { /** @type {HTMLCanvasElement|null} */ canvasEl: null, - /** Current interaction mode: 'select' | 'marker' | 'arrow' | 'rect' | 'pan' */ + /** Current interaction mode: 'select' | 'marker' | 'arrow' | 'rect' */ activeMode: 'select', /** Next auto-assigned marker number */ nextMarkerNumber: 1, - /** Current zoom level (1 = 100%) */ - zoomLevel: 1, - /** Whether a background image has been loaded */ imageLoaded: false, /** Whether the canvas has unsaved changes */ isDirty: false, + /** Current color for new annotations */ + currentColor: '#2563EB', + + /** Current stroke width for arrows and rectangles */ + currentStrokeWidth: 2, + + /** Current line dash pattern: [] = solid, [5,3] = dashed, [2,2] = dotted */ + currentLineDash: [], + // ---------------------------------------------------------------- // Initialization // ---------------------------------------------------------------- @@ -82,11 +88,21 @@ function annotationEditor() { return; } - this.canvas = new fabric.Canvas(this.canvasEl, { - selection: true, - preserveObjectStacking: true, - backgroundColor: '#f1f5f9', - }); + if (typeof fabric === 'undefined') { + console.error('AnnotationEditor: Fabric.js not loaded'); + return; + } + + try { + this.canvas = new fabric.Canvas(this.canvasEl, { + selection: true, + preserveObjectStacking: true, + backgroundColor: '#f1f5f9', + }); + } catch (err) { + console.error('AnnotationEditor: failed to create canvas:', err); + return; + } this.setupEvents(); this.setupKeyboard(); @@ -109,19 +125,76 @@ function annotationEditor() { }; window.addEventListener('anno-delete-selected', this._onDeleteSelected); - this._onZoom = function (e) { - if (e.detail && e.detail.delta > 0) { - self.zoomIn(); - } else { - self.zoomOut(); + this._onColorChange = function (e) { + if (e.detail && e.detail.color) { + self.currentColor = e.detail.color; + // Update selected object + var active = self.canvas ? self.canvas.getActiveObject() : null; + if (active) { + if (active.objectType === 'marker') { + var items = active.getObjects(); + if (items[0]) items[0].set('fill', e.detail.color); + active.markerColor = e.detail.color; + } else if (active.objectType === 'arrow') { + var items = active.getObjects(); + if (items[0]) items[0].set('stroke', e.detail.color); + if (items[1]) items[1].set('fill', e.detail.color); + active.arrowColor = e.detail.color; + } else if (active.objectType === 'area') { + active.set('stroke', e.detail.color); + active.areaColor = e.detail.color; + } + self.canvas.renderAll(); + self.isDirty = true; + window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: self.getAnnotationsJson() } })); + } } }; - window.addEventListener('anno-zoom', this._onZoom); + window.addEventListener('anno-color-change', this._onColorChange); - this._onZoomReset = function () { - self.resetZoom(); + this._onStrokeWidthChange = function (e) { + if (e.detail && e.detail.width) { + self.currentStrokeWidth = e.detail.width; + var active = self.canvas ? self.canvas.getActiveObject() : null; + if (active) { + if (active.objectType === 'arrow') { + var items = active.getObjects(); + if (items[0]) items[0].set('strokeWidth', e.detail.width); + if (items[1]) items[1].set({ width: 8 + e.detail.width * 2, height: 8 + e.detail.width * 2 }); + active.arrowStrokeWidth = e.detail.width; + } else if (active.objectType === 'area') { + active.set('strokeWidth', e.detail.width); + active.areaStrokeWidth = e.detail.width; + } + self.canvas.renderAll(); + self.isDirty = true; + window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: self.getAnnotationsJson() } })); + } + } }; - window.addEventListener('anno-zoom-reset', this._onZoomReset); + window.addEventListener('anno-stroke-width-change', this._onStrokeWidthChange); + + this._onLineDashChange = function (e) { + if (e.detail) { + self.currentLineDash = e.detail.dash || []; + var active = self.canvas ? self.canvas.getActiveObject() : null; + if (active) { + var dashArr = self.currentLineDash.length > 0 ? self.currentLineDash.slice() : null; + if (active.objectType === 'arrow') { + var items = active.getObjects(); + if (items[0]) items[0].set('strokeDashArray', dashArr); + active.arrowLineDash = self.currentLineDash.slice(); + } else if (active.objectType === 'area') { + active.set('strokeDashArray', dashArr); + active.areaLineDash = self.currentLineDash.slice(); + } + self.canvas.renderAll(); + self.isDirty = true; + window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: self.getAnnotationsJson() } })); + } + } + }; + window.addEventListener('anno-line-dash-change', this._onLineDashChange); this._onImageLoaded = function (e) { if (e.detail && e.detail.path) { @@ -307,7 +380,7 @@ function annotationEditor() { var circle = new fabric.Circle({ radius: 16, - fill: '#2563EB', + fill: this.currentColor, originX: 'center', originY: 'center', stroke: '#ffffff', @@ -334,6 +407,7 @@ function annotationEditor() { // Custom properties objectType: 'marker', markerNumber: markerNumber, + markerColor: this.currentColor, }); this.canvas.add(marker); @@ -358,8 +432,9 @@ function annotationEditor() { */ addArrow(x1, y1, x2, y2) { var line = new fabric.Line([x1, y1, x2, y2], { - stroke: '#DC2626', - strokeWidth: 2, + stroke: this.currentColor, + strokeWidth: this.currentStrokeWidth, + strokeDashArray: this.currentLineDash.length > 0 ? this.currentLineDash.slice() : null, originX: 'center', originY: 'center', }); @@ -371,9 +446,9 @@ function annotationEditor() { top: y2, originX: 'center', originY: 'center', - width: 12, - height: 12, - fill: '#DC2626', + width: 8 + this.currentStrokeWidth * 2, + height: 8 + this.currentStrokeWidth * 2, + fill: this.currentColor, angle: angle + 90, }); @@ -385,6 +460,9 @@ function annotationEditor() { arrowY1: y1, arrowX2: x2, arrowY2: y2, + arrowColor: this.currentColor, + arrowStrokeWidth: this.currentStrokeWidth, + arrowLineDash: this.currentLineDash.slice(), }); this.canvas.add(arrow); @@ -412,12 +490,15 @@ function annotationEditor() { top: y, width: width || 150, height: height || 100, - fill: 'rgba(37, 99, 235, 0.1)', - stroke: '#2563EB', - strokeWidth: 2, - strokeDashArray: [5, 3], + fill: 'rgba(0, 0, 0, 0)', + stroke: this.currentColor, + strokeWidth: this.currentStrokeWidth, + strokeDashArray: this.currentLineDash.length > 0 ? this.currentLineDash.slice() : null, selectable: true, objectType: 'area', + areaColor: this.currentColor, + areaStrokeWidth: this.currentStrokeWidth, + areaLineDash: this.currentLineDash.slice(), }); this.canvas.add(rect); @@ -433,7 +514,7 @@ function annotationEditor() { /** * Set the current interaction mode. * - * @param {'select'|'marker'|'arrow'|'rect'|'pan'} mode + * @param {'select'|'marker'|'arrow'|'rect'} mode */ setMode(mode) { this.activeMode = mode; @@ -444,12 +525,6 @@ function annotationEditor() { this.canvas.forEachObject(function (o) { o.selectable = true; }); - } else if (mode === 'pan') { - this.canvas.selection = false; - this.canvas.defaultCursor = 'grab'; - this.canvas.forEachObject(function (o) { - o.selectable = false; - }); } else { // marker, arrow, rect this.canvas.selection = false; @@ -465,8 +540,7 @@ function annotationEditor() { // ---------------------------------------------------------------- /** - * Wire up all Fabric.js mouse events for drawing, panning, selection, - * and zoom. + * Wire up all Fabric.js mouse events for drawing and selection. */ setupEvents() { var self = this; @@ -475,9 +549,6 @@ function annotationEditor() { var drawStartY = 0; var tempRect = null; var tempLine = null; - var isPanning = false; - var lastPanX = 0; - var lastPanY = 0; // ---- mouse:down ---- @@ -487,6 +558,7 @@ function annotationEditor() { if (self.activeMode === 'marker') { self.addMarker(pointer.x, pointer.y); self.setMode('select'); + window.dispatchEvent(new CustomEvent('anno-mode-changed', { detail: { mode: 'select' } })); } else if (self.activeMode === 'arrow') { isDrawing = true; drawStartX = pointer.x; @@ -523,27 +595,12 @@ function annotationEditor() { objectType: '_temp', }); self.canvas.add(tempRect); - } else if (self.activeMode === 'pan') { - isPanning = true; - lastPanX = opt.e.clientX; - lastPanY = opt.e.clientY; - self.canvas.defaultCursor = 'grabbing'; } }); // ---- mouse:move ---- this.canvas.on('mouse:move', function (opt) { - if (isPanning) { - var vpt = self.canvas.viewportTransform; - vpt[4] += opt.e.clientX - lastPanX; - vpt[5] += opt.e.clientY - lastPanY; - lastPanX = opt.e.clientX; - lastPanY = opt.e.clientY; - self.canvas.requestRenderAll(); - return; - } - if (!isDrawing) return; var pointer = self.canvas.getPointer(opt.e); @@ -595,18 +652,16 @@ function annotationEditor() { isDrawing = false; self.setMode('select'); - } - - if (isPanning) { - isPanning = false; - self.canvas.defaultCursor = 'grab'; + window.dispatchEvent(new CustomEvent('anno-mode-changed', { detail: { mode: 'select' } })); } }); // ---- Selection events ---- this.canvas.on('selection:created', function (e) { - window.dispatchEvent(new CustomEvent('annotation-selected', { detail: { selected: true } })); + var sel = e.selected && e.selected[0]; + var objType = sel ? sel.objectType : null; + window.dispatchEvent(new CustomEvent('annotation-selected', { detail: { selected: true, objectType: objType } })); var selected = e.selected; if (selected && selected.length === 1 && selected[0].objectType === 'marker') { window.dispatchEvent(new CustomEvent('marker-selected', { @@ -616,7 +671,9 @@ function annotationEditor() { }); this.canvas.on('selection:updated', function (e) { - window.dispatchEvent(new CustomEvent('annotation-selected', { detail: { selected: true } })); + var sel = e.selected && e.selected[0]; + var objType = sel ? sel.objectType : null; + window.dispatchEvent(new CustomEvent('annotation-selected', { detail: { selected: true, objectType: objType } })); var selected = e.selected; if (selected && selected.length === 1 && selected[0].objectType === 'marker') { window.dispatchEvent(new CustomEvent('marker-selected', { @@ -642,21 +699,6 @@ function annotationEditor() { window.dispatchEvent(new CustomEvent('annotations-changed', { detail: { json: self.getAnnotationsJson() } })); }); - // ---- Zoom with mouse wheel ---- - - this.canvas.on('mouse:wheel', function (opt) { - var delta = opt.e.deltaY; - var zoom = self.canvas.getZoom(); - zoom *= Math.pow(0.999, delta); - zoom = Math.max(0.25, Math.min(5, zoom)); - self.canvas.zoomToPoint( - { x: opt.e.offsetX, y: opt.e.offsetY }, - zoom - ); - self.zoomLevel = zoom; - opt.e.preventDefault(); - opt.e.stopPropagation(); - }); }, // ---------------------------------------------------------------- @@ -728,37 +770,6 @@ function annotationEditor() { this.canvas.renderAll(); }, - // ---------------------------------------------------------------- - // Zoom controls - // ---------------------------------------------------------------- - - /** - * Zoom in by 20%. - */ - zoomIn() { - var zoom = Math.min(this.canvas.getZoom() * 1.2, 5); - this.canvas.setZoom(zoom); - this.zoomLevel = zoom; - }, - - /** - * Zoom out by 20%. - */ - zoomOut() { - var zoom = Math.max(this.canvas.getZoom() / 1.2, 0.25); - this.canvas.setZoom(zoom); - this.zoomLevel = zoom; - }, - - /** - * Reset zoom and pan to default (1:1, origin at top-left). - */ - resetZoom() { - this.canvas.setViewportTransform([1, 0, 0, 1, 0, 0]); - this.canvas.setZoom(1); - this.zoomLevel = 1; - }, - // ---------------------------------------------------------------- // Canvas sizing // ---------------------------------------------------------------- @@ -811,6 +822,15 @@ function annotationEditor() { markerNumber: obj.markerNumber || null, }; + // Color and stroke width + if (obj.markerColor) data.fill = obj.markerColor; + if (obj.arrowColor) data.stroke = obj.arrowColor; + if (obj.areaColor) data.stroke = obj.areaColor; + if (obj.arrowStrokeWidth) data.strokeWidth = obj.arrowStrokeWidth; + if (obj.areaStrokeWidth) data.strokeWidth = obj.areaStrokeWidth; + if (obj.arrowLineDash) data.lineDash = obj.arrowLineDash; + if (obj.areaLineDash) data.lineDash = obj.areaLineDash; + // Arrow-specific: store original endpoints if (obj.objectType === 'arrow') { data.x1 = obj.arrowX1 != null ? Math.round(obj.arrowX1 * 100) / 100 : null; @@ -860,18 +880,41 @@ function annotationEditor() { var obj = objects[i]; if (obj.type === 'marker') { + var savedColor = this.currentColor; + if (obj.fill) this.currentColor = obj.fill; this.addMarker(obj.left, obj.top, obj.markerNumber); + this.currentColor = savedColor; } else if (obj.type === 'arrow') { + var savedColor2 = this.currentColor; + var savedWidth = this.currentStrokeWidth; + var savedDash = this.currentLineDash.slice(); + if (obj.stroke) this.currentColor = obj.stroke; + if (obj.strokeWidth) this.currentStrokeWidth = obj.strokeWidth; + if (obj.lineDash) this.currentLineDash = obj.lineDash; + else this.currentLineDash = []; // Reconstruct arrow from stored endpoints or from position/size var x1 = obj.x1 != null ? obj.x1 : obj.left; var y1 = obj.y1 != null ? obj.y1 : obj.top; var x2 = obj.x2 != null ? obj.x2 : obj.left + (obj.width || 100); var y2 = obj.y2 != null ? obj.y2 : obj.top + (obj.height || 50); this.addArrow(x1, y1, x2, y2); + this.currentColor = savedColor2; + this.currentStrokeWidth = savedWidth; + this.currentLineDash = savedDash; } else if (obj.type === 'area') { + var savedColor3 = this.currentColor; + var savedWidth2 = this.currentStrokeWidth; + var savedDash2 = this.currentLineDash.slice(); + if (obj.stroke) this.currentColor = obj.stroke; + if (obj.strokeWidth) this.currentStrokeWidth = obj.strokeWidth; + if (obj.lineDash) this.currentLineDash = obj.lineDash; + else this.currentLineDash = []; var w = (obj.width || 150) * (obj.scaleX || 1); var h = (obj.height || 100) * (obj.scaleY || 1); this.addRect(obj.left, obj.top, w, h); + this.currentColor = savedColor3; + this.currentStrokeWidth = savedWidth2; + this.currentLineDash = savedDash2; } } @@ -959,11 +1002,14 @@ function annotationEditor() { if (this._onDeleteSelected) { window.removeEventListener('anno-delete-selected', this._onDeleteSelected); } - if (this._onZoom) { - window.removeEventListener('anno-zoom', this._onZoom); + if (this._onColorChange) { + window.removeEventListener('anno-color-change', this._onColorChange); } - if (this._onZoomReset) { - window.removeEventListener('anno-zoom-reset', this._onZoomReset); + if (this._onStrokeWidthChange) { + window.removeEventListener('anno-stroke-width-change', this._onStrokeWidthChange); + } + if (this._onLineDashChange) { + window.removeEventListener('anno-line-dash-change', this._onLineDashChange); } if (this._onImageLoaded) { window.removeEventListener('image-loaded', this._onImageLoaded); diff --git a/client/static/js/annotation-viewer.js b/client/static/js/annotation-viewer.js index 41995d4..125a457 100644 --- a/client/static/js/annotation-viewer.js +++ b/client/static/js/annotation-viewer.js @@ -314,3 +314,197 @@ function annotationViewer() { } }; } + +/** + * Standalone function to render an image with annotations overlay. + * For use in read-only previews (e.g. task cards) without Alpine.js. + * + * @param {HTMLCanvasElement} canvas - the canvas element to draw on + * @param {string} imageUrl - URL of the image to load + * @param {Object|string|null} annotationsJson - annotations data from the editor + * @param {Object} [options] - optional settings + * @param {number} [options.maxHeight=192] - max canvas height in pixels + */ +function renderAnnotationPreview(canvas, imageUrl, annotationsJson, options) { + if (!canvas || !imageUrl) return; + + var opts = options || {}; + var maxHeight = opts.maxHeight || 192; + var ctx = canvas.getContext('2d'); + + var annotations = null; + if (annotationsJson) { + try { + annotations = (typeof annotationsJson === 'string') + ? JSON.parse(annotationsJson) + : annotationsJson; + } catch (e) { + console.error('renderAnnotationPreview: invalid annotations JSON'); + } + } + + var isPdf = imageUrl.toLowerCase().replace(/\?.*$/, '').endsWith('.pdf'); + + if (isPdf && typeof pdfjsLib !== 'undefined') { + pdfjsLib.getDocument(imageUrl).promise.then(function (pdf) { + return pdf.getPage(1); + }).then(function (page) { + var viewport = page.getViewport({ scale: 2 }); + var offCanvas = document.createElement('canvas'); + offCanvas.width = viewport.width; + offCanvas.height = viewport.height; + var offCtx = offCanvas.getContext('2d'); + page.render({ canvasContext: offCtx, viewport: viewport }).promise.then(function () { + _drawPreview(canvas, ctx, offCanvas, viewport.width, viewport.height, annotations, maxHeight); + }); + }).catch(function (err) { + console.error('renderAnnotationPreview: PDF load failed:', err); + }); + } else { + var img = new Image(); + img.crossOrigin = 'anonymous'; + img.onload = function () { + _drawPreview(canvas, ctx, img, img.width, img.height, annotations, maxHeight); + }; + img.onerror = function () { + console.error('renderAnnotationPreview: image load failed:', imageUrl); + }; + img.src = imageUrl; + } +} + +/** + * Internal: draw image + annotations on canvas, scaled to fit. + */ +function _drawPreview(canvas, ctx, source, srcW, srcH, annotations, maxHeight) { + var container = canvas.parentElement; + var containerWidth = container ? container.clientWidth : 400; + var scale = Math.min(containerWidth / srcW, maxHeight / srcH, 1); + + canvas.width = Math.round(srcW * scale); + canvas.height = Math.round(srcH * scale); + + ctx.clearRect(0, 0, canvas.width, canvas.height); + ctx.drawImage(source, 0, 0, canvas.width, canvas.height); + + if (annotations && annotations.objects) { + var annoScale = canvas.width / (annotations.width || srcW); + _drawEditorAnnotations(ctx, annotations, annoScale); + } else if (annotations && annotations.markers) { + _drawLegacyAnnotations(ctx, annotations, scale); + } +} + +/** + * Draw annotations in the editor format (objects: marker/arrow/area). + */ +function _drawEditorAnnotations(ctx, data, scale) { + var objects = data.objects || []; + + // Calculate annotation scale: annotations were placed on a canvas of + // data.width x data.height, but we're drawing on a scaled canvas. + // The image was scaled to fit data.width x data.height in the editor, + // and now we scale again for preview. Use overall scale factor. + var annoScale = scale; + + for (var i = 0; i < objects.length; i++) { + var obj = objects[i]; + + if (obj.type === 'marker') { + _drawPreviewMarker(ctx, obj, annoScale); + } else if (obj.type === 'arrow') { + _drawPreviewArrow(ctx, obj, annoScale); + } else if (obj.type === 'area') { + _drawPreviewArea(ctx, obj, annoScale); + } + } +} + +function _drawPreviewMarker(ctx, obj, scale) { + var x = obj.left * scale; + var y = obj.top * scale; + var color = obj.fill || '#2563EB'; + var radius = 14 * Math.min(scale, 1); + + ctx.beginPath(); + ctx.arc(x, y, radius, 0, Math.PI * 2); + ctx.fillStyle = color; + ctx.globalAlpha = 0.85; + ctx.fill(); + ctx.globalAlpha = 1; + + ctx.strokeStyle = '#ffffff'; + ctx.lineWidth = 2; + ctx.stroke(); + + if (obj.markerNumber) { + ctx.fillStyle = '#ffffff'; + ctx.font = 'bold ' + Math.max(10, Math.round(12 * Math.min(scale, 1))) + 'px Inter, sans-serif'; + ctx.textAlign = 'center'; + ctx.textBaseline = 'middle'; + ctx.fillText(obj.markerNumber.toString(), x, y); + } +} + +function _drawPreviewArrow(ctx, obj, scale) { + var x1 = (obj.x1 != null ? obj.x1 : obj.left) * scale; + var y1 = (obj.y1 != null ? obj.y1 : obj.top) * scale; + var x2 = (obj.x2 != null ? obj.x2 : (obj.left + (obj.width || 100))) * scale; + var y2 = (obj.y2 != null ? obj.y2 : (obj.top + (obj.height || 50))) * scale; + var color = obj.stroke || '#DC2626'; + var lineWidth = obj.strokeWidth || 2; + + var lineDash = obj.lineDash || []; + ctx.setLineDash(lineDash); + + ctx.beginPath(); + ctx.moveTo(x1, y1); + ctx.lineTo(x2, y2); + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.stroke(); + + ctx.setLineDash([]); + + // Arrowhead + var angle = Math.atan2(y2 - y1, x2 - x1); + var headLen = 8 + lineWidth * 2; + ctx.beginPath(); + ctx.moveTo(x2, y2); + ctx.lineTo(x2 - headLen * Math.cos(angle - Math.PI / 6), y2 - headLen * Math.sin(angle - Math.PI / 6)); + ctx.lineTo(x2 - headLen * Math.cos(angle + Math.PI / 6), y2 - headLen * Math.sin(angle + Math.PI / 6)); + ctx.closePath(); + ctx.fillStyle = color; + ctx.fill(); +} + +function _drawPreviewArea(ctx, obj, scale) { + var x = obj.left * scale; + var y = obj.top * scale; + var w = (obj.width || 150) * (obj.scaleX || 1) * scale; + var h = (obj.height || 100) * (obj.scaleY || 1) * scale; + var color = obj.stroke || '#2563EB'; + var lineWidth = obj.strokeWidth || 2; + + var lineDash = obj.lineDash || [5, 3]; + ctx.strokeStyle = color; + ctx.lineWidth = lineWidth; + ctx.setLineDash(lineDash); + ctx.strokeRect(x, y, w, h); + ctx.setLineDash([]); +} + +/** + * Draw annotations in the legacy format (markers: point/rectangle). + */ +function _drawLegacyAnnotations(ctx, data, scale) { + var markers = data.markers || []; + for (var i = 0; i < markers.length; i++) { + var m = markers[i]; + if (m.type === 'point') { + _drawPreviewMarker(ctx, { left: m.x, top: m.y, markerNumber: m.marker_number, fill: '#64748B' }, scale); + } else if (m.type === 'rectangle') { + _drawPreviewArea(ctx, { left: m.x, top: m.y, width: m.width, height: m.height, stroke: '#64748B' }, scale); + } + } +} diff --git a/client/tailwind.config.js b/client/tailwind.config.js index dc7e00d..64c932f 100644 --- a/client/tailwind.config.js +++ b/client/tailwind.config.js @@ -12,11 +12,31 @@ module.exports = { DEFAULT: '#2563EB', dark: '#1E40AF', light: '#3B82F6', + 50: '#EFF6FF', + 100: '#DBEAFE', + 200: '#BFDBFE', + 300: '#93C5FD', + 400: '#60A5FA', + 500: '#3B82F6', + 600: '#2563EB', + 700: '#1D4ED8', + 800: '#1E40AF', + 900: '#1E3A8A', }, steel: { DEFAULT: '#64748B', light: '#94A3B8', dark: '#475569', + 50: '#F8FAFC', + 100: '#F1F5F9', + 200: '#E2E8F0', + 300: '#CBD5E1', + 400: '#94A3B8', + 500: '#64748B', + 600: '#475569', + 700: '#334155', + 800: '#1E293B', + 900: '#0F172A', }, measure: { pass: '#059669', diff --git a/client/templates/base.html b/client/templates/base.html index 170b175..47a17a9 100644 --- a/client/templates/base.html +++ b/client/templates/base.html @@ -18,56 +18,8 @@ - - - + + diff --git a/client/templates/maker/recipe_editor.html b/client/templates/maker/recipe_editor.html index 17dbc39..d8b05d7 100644 --- a/client/templates/maker/recipe_editor.html +++ b/client/templates/maker/recipe_editor.html @@ -10,7 +10,6 @@ {% block extra_head %} {% endblock %} @@ -131,7 +86,7 @@

{% else %}

- {{ _('Compila i dati della ricetta e carica il disegno tecnico') }} + {{ _('Compila i dati della ricetta') }}

{% endif %} @@ -281,7 +236,7 @@
@@ -290,151 +245,36 @@ - {{ _('Disegno Tecnico e Annotazioni') }} + {{ _('Immagine Anteprima') }}
- - + + + + + {{ _('Immagine caricata') }} +
- +
- - - - - - - - - - - - - - -
- - - - - -
- - - - - - - - -
- - -
- -
- -
- - +
- +
+ + + + + {{ _('Caricamento in corso...') }} +
- +
+ + {{ _('Carica Immagine o PDF') }} + + + {{ _('Sostituisci Immagine') }} + +

+ {{ _('Trascina qui oppure clicca. Formati: PNG, JPG, WebP, PDF (max 50MB)') }} +

+

+ {{ _('Usata come thumbnail nella lista ricette') }} +

+
+
@@ -549,7 +386,7 @@ {% endif %}
+ + + + + + {{ _('Torna ai Task') }} + +
+ + + + +
+ + + + + +
+ +
+ + + + +
+ + +
+
+ +
+ + + + + + + + + + + + + +
+ + + + +
+ + +
+ + {{ _('Colore') }}: + + + +
+ + + {{ _('Spessore') }}: + + + +
+ + + {{ _('Linea') }}: + +
+
+
+ + +
+
+
+ +
+
+
+ + +{% endblock %} + +{% block extra_js %} + + + + + +{% endblock %} diff --git a/client/templates/maker/task_editor.html b/client/templates/maker/task_editor.html index 655eee9..95e4da2 100644 --- a/client/templates/maker/task_editor.html +++ b/client/templates/maker/task_editor.html @@ -432,6 +432,59 @@
+ +
+

+ {{ _('Disegno Tecnico') }} +

+ + + + + + +
+