Code refactor to start working on viewer.
This commit is contained in:
parent
429e993009
commit
585c4abb25
2 changed files with 267 additions and 248 deletions
|
|
@ -10,8 +10,10 @@ document.addEventListener('keydown', (e) => {
|
|||
});
|
||||
|
||||
function initGridWidget(root, opts = {}) {
|
||||
const mode = opts.mode || 'editor';
|
||||
const storageKey = opts.storageKey ?? 'gridDoc';
|
||||
|
||||
const DEFAULT_DOC = { version: 1, cellSize: 25, shapes: [] };
|
||||
const MAX_HISTORY = 100;
|
||||
const SHAPE_DEFAULTS = {
|
||||
strokeWidth: 0.12,
|
||||
strokeOpacity: 1,
|
||||
|
|
@ -19,17 +21,226 @@ function initGridWidget(root, opts = {}) {
|
|||
};
|
||||
|
||||
const canvasEl = root.querySelector('[data-canvas]');
|
||||
const gridEl = root.querySelector('[data-grid]');
|
||||
const gridWrapEl = root.querySelector('[data-grid-wrap]');
|
||||
|
||||
if (!canvasEl || !gridEl || !gridWrapEl) {
|
||||
throw new Error("Grid widget: missing required viewer elements.");
|
||||
}
|
||||
|
||||
let doc = loadDoc();
|
||||
let shapes = sanitizeShapes(Array.isArray(doc.shapes) ? doc.shapes : []);
|
||||
let cellSize = Number(doc.cellSize) || 25;
|
||||
|
||||
let ctx;
|
||||
let dpr = 1;
|
||||
|
||||
function clamp01(n, fallback = 1) {
|
||||
const x = Number(n);
|
||||
return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback;
|
||||
}
|
||||
|
||||
function isFiniteNum(n) { return Number.isFinite(Number(n)); }
|
||||
|
||||
function sanitizeShapes(list) {
|
||||
const allowed = new Set(['rect', 'ellipse', 'line', 'path']);
|
||||
|
||||
const normStroke = (v, fallback = 0.12) => {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
return Math.max(0, n);
|
||||
};
|
||||
|
||||
return list.flatMap((s) => {
|
||||
if (!s || typeof s !== 'object' || !allowed.has(s.type)) return [];
|
||||
|
||||
const color = typeof s.color === 'string' ? s.color : '#000000';
|
||||
const fillOpacity = clamp01(s.fillOpacity, SHAPE_DEFAULTS.fillOpacity);
|
||||
const strokeOpacity = clamp01(s.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
|
||||
|
||||
if (s.type === 'line') {
|
||||
if (!['x1', 'y1', 'x2', 'y2'].every(k => isFiniteNum(s[k]))) return [];
|
||||
return [{
|
||||
type: 'line',
|
||||
x1: +s.x1, y1: +s.y1, x2: +s.x2, y2: +s.y2,
|
||||
color,
|
||||
strokeWidth: normStroke(s.strokeWidth, SHAPE_DEFAULTS.strokeWidth),
|
||||
strokeOpacity
|
||||
}];
|
||||
}
|
||||
|
||||
if (s.type === 'path') {
|
||||
if (!Array.isArray(s.points) || s.points.length < 2) return [];
|
||||
const points = s.points.flatMap(p => {
|
||||
if (!p || !isFiniteNum(p.x) || !isFiniteNum(p.y)) return [];
|
||||
return [{ x: +p.x, y: +p.y }];
|
||||
});
|
||||
if (points.length < 2) return [];
|
||||
|
||||
return [{
|
||||
type: 'path',
|
||||
points,
|
||||
color,
|
||||
strokeWidth: normStroke(s.strokeWidth, SHAPE_DEFAULTS.strokeWidth),
|
||||
strokeOpacity
|
||||
}];
|
||||
}
|
||||
|
||||
if (!['x', 'y', 'w', 'h'].every(k => isFiniteNum(s[k]))) return [];
|
||||
return [{
|
||||
type: s.type,
|
||||
x: +s.x, y: +s.y, w: +s.w, h: +s.h,
|
||||
color,
|
||||
fill: !!s.fill,
|
||||
fillOpacity,
|
||||
strokeOpacity,
|
||||
strokeWidth: normStroke(s.strokeWidth, SHAPE_DEFAULTS.strokeWidth)
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function loadDoc() {
|
||||
try { return JSON.parse(localStorage.getItem(storageKey)) || structuredClone(DEFAULT_DOC); }
|
||||
catch { return structuredClone(DEFAULT_DOC); }
|
||||
}
|
||||
|
||||
function saveDoc(nextDoc = doc) {
|
||||
doc = nextDoc;
|
||||
try { localStorage.setItem(storageKey, JSON.stringify(nextDoc)); } catch { }
|
||||
}
|
||||
|
||||
function resizeAndSetupCanvas() {
|
||||
dpr = window.devicePixelRatio || 1;
|
||||
|
||||
const w = gridEl.clientWidth;
|
||||
const h = gridEl.clientHeight;
|
||||
|
||||
canvasEl.width = Math.round(w * dpr);
|
||||
canvasEl.height = Math.round(h * dpr);
|
||||
|
||||
canvasEl.style.width = `${w}px`;
|
||||
canvasEl.style.height = `${h}px`;
|
||||
|
||||
ctx = canvasEl.getContext('2d');
|
||||
ctx.setTransform(dpr, 0, 0, dpr, 0, 0);
|
||||
|
||||
redrawAll();
|
||||
}
|
||||
|
||||
function clearCanvas() {
|
||||
if (!ctx) return;
|
||||
ctx.clearRect(0, 0, canvasEl.width / dpr, canvasEl.height / dpr);
|
||||
}
|
||||
|
||||
function drawShape(shape) {
|
||||
if (!ctx) return;
|
||||
const toPx = (v) => v * cellSize;
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = shape.color || '#000000';
|
||||
ctx.lineWidth = Math.max(1, toPx(shape.strokeWidth ?? SHAPE_DEFAULTS.strokeWidth));
|
||||
|
||||
if (shape.type === 'rect' || shape.type === 'ellipse') {
|
||||
const x = toPx(shape.x);
|
||||
const y = toPx(shape.y);
|
||||
const w = toPx(shape.w);
|
||||
const h = toPx(shape.h);
|
||||
|
||||
ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
|
||||
if (shape.type === 'rect') {
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
} else {
|
||||
const cx = x + w / 2;
|
||||
const cy = y + h / 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
if (shape.fill) {
|
||||
ctx.globalAlpha = clamp01(shape.fillOpacity, SHAPE_DEFAULTS.fillOpacity);
|
||||
ctx.fillStyle = shape.color;
|
||||
if (shape.type === 'rect') {
|
||||
ctx.fillRect(x, y, w, h);
|
||||
} else {
|
||||
const cx = x + w / 2;
|
||||
const cy = y + h / 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2);
|
||||
ctx.fill()
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
} else if (shape.type === 'line') {
|
||||
const x1 = toPx(shape.x1);
|
||||
const y1 = toPx(shape.y1);
|
||||
const x2 = toPx(shape.x2);
|
||||
const y2 = toPx(shape.y2);
|
||||
|
||||
ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
} else if (shape.type === 'path') {
|
||||
const toPx = (v) => v * cellSize;
|
||||
|
||||
ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
|
||||
|
||||
ctx.lineWidth = Math.max(1, toPx(shape.strokeWidth ?? SHAPE_DEFAULTS.strokeWidth));
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
const pts = shape.points;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(toPx(pts[0].x), toPx(pts[0].y));
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
ctx.lineTo(toPx(pts[i].x), toPx(pts[i].y));
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function redrawAll() {
|
||||
if (!ctx || !shapes) return;
|
||||
|
||||
clearCanvas();
|
||||
shapes.forEach(drawShape);
|
||||
}
|
||||
|
||||
function setDoc(nextDoc) {
|
||||
const d = nextDoc && typeof nextDoc === 'object' ? nextDoc : DEFAULT_DOC;
|
||||
cellSize = Number(d.cellSize) || 25;
|
||||
shapes = sanitizeShapes(Array.isArray(d.shapes) ? d.shapes : []);
|
||||
saveDoc({ version: Number(d.version) || 1, cellSize, shapes });
|
||||
resizeAndSetupCanvas();
|
||||
}
|
||||
|
||||
resizeAndSetupCanvas();
|
||||
redrawAll();
|
||||
|
||||
if (mode !== 'editor') {
|
||||
return { setDoc, redraw: redrawAll };
|
||||
}
|
||||
|
||||
const MAX_HISTORY = 100;
|
||||
|
||||
const clearEl = root.querySelector('[data-clear]');
|
||||
const colorEl = root.querySelector('[data-color]');
|
||||
const coordsEl = root.querySelector('[data-coords]');
|
||||
const dotEl = root.querySelector('[data-dot]');
|
||||
const dotSVGEl = root.querySelector('[data-dot-svg]');
|
||||
const exportEl = root.querySelector('[data-export]');
|
||||
const gridEl = root.querySelector('[data-grid]');
|
||||
const importButtonEl = root.querySelector('[data-import-button]');
|
||||
const importEl = root.querySelector('[data-import]');
|
||||
const cellSizeEl = root.querySelector('[data-cell-size]');
|
||||
const gridWrapEl = root.querySelector('[data-grid-wrap]');
|
||||
const toolBarEl = root.querySelector('[data-toolbar]');
|
||||
const fillOpacityEl = root.querySelector('[data-fill-opacity]');
|
||||
const strokeOpacityEl = root.querySelector('[data-stroke-opacity]');
|
||||
|
|
@ -44,10 +255,6 @@ function initGridWidget(root, opts = {}) {
|
|||
if (strokeOpacityEl && strokeValEl) bindRangeWithLabel(strokeOpacityEl, strokeValEl, v => `${parseInt(Number(v) * 100)}%`);
|
||||
if (strokeWidthEl && widthValEl) bindRangeWithLabel(strokeWidthEl, widthValEl, v => `${Math.round(Number(v) * Number(cellSizeEl.value || 0))}px`);
|
||||
|
||||
const storageKey = opts.storageKey ?? 'gridDoc';
|
||||
|
||||
let doc = loadDoc();
|
||||
let shapes = sanitizeShapes(Array.isArray(doc.shapes) ? doc.shapes : []);
|
||||
saveDoc({ ...doc, shapes });
|
||||
|
||||
const savedTool = localStorage.getItem(`${storageKey}:tool`);
|
||||
|
|
@ -56,12 +263,9 @@ function initGridWidget(root, opts = {}) {
|
|||
const savedType = localStorage.getItem(`${storageKey}:gridType`);
|
||||
if (savedType) setActiveType(savedType);
|
||||
|
||||
let cellSize = Number(doc.cellSize) || 25;
|
||||
cellSizeEl.value = cellSize;
|
||||
let dotSize = Math.floor(Math.max(cellSize * 1.25, 32));
|
||||
|
||||
let ctx;
|
||||
let dpr = 1;
|
||||
let selectedColor;
|
||||
let currentFillOpacity = clamp01(fillOpacityEl?.value ?? 1, 1);
|
||||
let currentStrokeOpacity = clamp01(strokeOpacityEl?.value ?? 1, 1);
|
||||
|
|
@ -77,8 +281,6 @@ function initGridWidget(root, opts = {}) {
|
|||
const ro = new ResizeObserver(scheduleSnappedCellSize);
|
||||
ro.observe(gridWrapEl);
|
||||
|
||||
resizeAndSetupCanvas();
|
||||
|
||||
setGrid();
|
||||
scheduleSnappedCellSize();
|
||||
|
||||
|
|
@ -143,6 +345,53 @@ function initGridWidget(root, opts = {}) {
|
|||
activeGridWidget = api;
|
||||
}, { capture: true });
|
||||
|
||||
function setGrid() {
|
||||
const type = getActiveType();
|
||||
|
||||
gridEl.style.backgroundImage = "";
|
||||
gridEl.style.backgroundSize = "";
|
||||
gridEl.style.backgroundPosition = "";
|
||||
gridEl.style.boxShadow = "none";
|
||||
dotEl.classList.add('d-none');
|
||||
|
||||
// Minor dots
|
||||
const dotPx = Math.max(1, Math.round(cellSize * 0.08));
|
||||
const minorColor = '#ddd';
|
||||
|
||||
// Major dots (every 5 cells)
|
||||
const majorStep = cellSize * 5;
|
||||
const majorDotPx = Math.max(dotPx + 1, Math.round(cellSize * 0.12));
|
||||
const majorColor = '#c4c4c4';
|
||||
|
||||
const minorLayer = `radial-gradient(circle, ${minorColor} ${dotPx}px, transparent ${dotPx}px)`;
|
||||
const majorLayer = `radial-gradient(circle, ${majorColor} ${majorDotPx}px, transparent ${majorDotPx}px)`;
|
||||
|
||||
if (type === 'fullGrid') {
|
||||
gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`;
|
||||
gridEl.style.backgroundSize = `${majorStep}px ${majorStep}px, ${cellSize}px ${cellSize}px`;
|
||||
gridEl.style.backgroundPosition =
|
||||
`${majorStep / 2}px ${majorStep / 2}px, ${cellSize / 2}px ${cellSize / 2}px`;
|
||||
gridEl.style.boxShadow = "inset 0 0 0 1px #ccc";
|
||||
|
||||
} else if (type === 'verticalGrid') {
|
||||
gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`;
|
||||
gridEl.style.backgroundSize = `${majorStep}px 100%, ${cellSize}px 100%`;
|
||||
gridEl.style.backgroundPosition =
|
||||
`${majorStep / 2}px 0px, ${cellSize / 2}px 0px`;
|
||||
gridEl.style.boxShadow = "inset 0 1px 0 0 #ccc, inset 0 -1px 0 0 #ccc";
|
||||
|
||||
} else if (type === 'horizontalGrid') {
|
||||
gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`;
|
||||
gridEl.style.backgroundSize = `100% ${majorStep}px, 100% ${cellSize}px`;
|
||||
gridEl.style.backgroundPosition =
|
||||
`0px ${majorStep / 2}px, 0px ${cellSize / 2}px`;
|
||||
gridEl.style.boxShadow = "inset 1px 0 0 0 #ccc, inset -1px 0 0 0 #ccc";
|
||||
|
||||
} else { // noGrid
|
||||
gridEl.style.boxShadow = "inset 0 0 0 1px #ccc";
|
||||
}
|
||||
}
|
||||
|
||||
function isInsideRect(clientX, clientY, rect) {
|
||||
return clientX >= rect.left && clientX <= rect.right &&
|
||||
clientY >= rect.top && clientY <= rect.bottom;
|
||||
|
|
@ -285,80 +534,6 @@ function initGridWidget(root, opts = {}) {
|
|||
redrawAll();
|
||||
}
|
||||
|
||||
function clamp01(n, fallback = 1) {
|
||||
const x = Number(n);
|
||||
return Number.isFinite(x) ? Math.min(1, Math.max(0, x)) : fallback;
|
||||
}
|
||||
|
||||
function isFiniteNum(n) { return Number.isFinite(Number(n)); }
|
||||
|
||||
function sanitizeShapes(list) {
|
||||
const allowed = new Set(['rect', 'ellipse', 'line', 'path']);
|
||||
|
||||
const normStroke = (v, fallback = 0.12) => {
|
||||
const n = Number(v);
|
||||
if (!Number.isFinite(n)) return fallback;
|
||||
return Math.max(0, n);
|
||||
};
|
||||
|
||||
return list.flatMap((s) => {
|
||||
if (!s || typeof s !== 'object' || !allowed.has(s.type)) return [];
|
||||
|
||||
const color = typeof s.color === 'string' ? s.color : '#000000';
|
||||
const fillOpacity = clamp01(s.fillOpacity, SHAPE_DEFAULTS.fillOpacity);
|
||||
const strokeOpacity = clamp01(s.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
|
||||
|
||||
if (s.type === 'line') {
|
||||
if (!['x1', 'y1', 'x2', 'y2'].every(k => isFiniteNum(s[k]))) return [];
|
||||
return [{
|
||||
type: 'line',
|
||||
x1: +s.x1, y1: +s.y1, x2: +s.x2, y2: +s.y2,
|
||||
color,
|
||||
strokeWidth: normStroke(s.strokeWidth, SHAPE_DEFAULTS.strokeWidth),
|
||||
strokeOpacity
|
||||
}];
|
||||
}
|
||||
|
||||
if (s.type === 'path') {
|
||||
if (!Array.isArray(s.points) || s.points.length < 2) return [];
|
||||
const points = s.points.flatMap(p => {
|
||||
if (!p || !isFiniteNum(p.x) || !isFiniteNum(p.y)) return [];
|
||||
return [{ x: +p.x, y: +p.y }];
|
||||
});
|
||||
if (points.length < 2) return [];
|
||||
|
||||
return [{
|
||||
type: 'path',
|
||||
points,
|
||||
color,
|
||||
strokeWidth: normStroke(s.strokeWidth, SHAPE_DEFAULTS.strokeWidth),
|
||||
strokeOpacity
|
||||
}];
|
||||
}
|
||||
|
||||
if (!['x', 'y', 'w', 'h'].every(k => isFiniteNum(s[k]))) return [];
|
||||
return [{
|
||||
type: s.type,
|
||||
x: +s.x, y: +s.y, w: +s.w, h: +s.h,
|
||||
color,
|
||||
fill: !!s.fill,
|
||||
fillOpacity,
|
||||
strokeOpacity,
|
||||
strokeWidth: normStroke(s.strokeWidth, SHAPE_DEFAULTS.strokeWidth)
|
||||
}];
|
||||
});
|
||||
}
|
||||
|
||||
function loadDoc() {
|
||||
try { return JSON.parse(localStorage.getItem(storageKey)) || structuredClone(DEFAULT_DOC); }
|
||||
catch { return structuredClone(DEFAULT_DOC); }
|
||||
}
|
||||
|
||||
function saveDoc(nextDoc = doc) {
|
||||
doc = nextDoc;
|
||||
try { localStorage.setItem(storageKey, JSON.stringify(nextDoc)); } catch { }
|
||||
}
|
||||
|
||||
function snapDown(n, step) {
|
||||
return Math.floor(n / step) * step;
|
||||
}
|
||||
|
|
@ -528,165 +703,6 @@ function initGridWidget(root, opts = {}) {
|
|||
};
|
||||
}
|
||||
|
||||
function resizeAndSetupCanvas() {
|
||||
dpr = window.devicePixelRatio || 1;
|
||||
|
||||
const w = gridEl.clientWidth;
|
||||
const h = gridEl.clientHeight;
|
||||
|
||||
canvasEl.width = Math.round(w * dpr);
|
||||
canvasEl.height = Math.round(h * dpr);
|
||||
|
||||
canvasEl.style.width = `${w}px`;
|
||||
canvasEl.style.height = `${h}px`;
|
||||
|
||||
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;
|
||||
const toPx = (v) => v * cellSize;
|
||||
|
||||
ctx.save();
|
||||
ctx.strokeStyle = shape.color || '#000000';
|
||||
ctx.lineWidth = Math.max(1, toPx(shape.strokeWidth ?? SHAPE_DEFAULTS.strokeWidth));
|
||||
|
||||
if (shape.type === 'rect' || shape.type === 'ellipse') {
|
||||
const x = toPx(shape.x);
|
||||
const y = toPx(shape.y);
|
||||
const w = toPx(shape.w);
|
||||
const h = toPx(shape.h);
|
||||
|
||||
ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
|
||||
if (shape.type === 'rect') {
|
||||
ctx.strokeRect(x, y, w, h);
|
||||
} else {
|
||||
const cx = x + w / 2;
|
||||
const cy = y + h / 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2);
|
||||
ctx.stroke();
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
|
||||
if (shape.fill) {
|
||||
ctx.globalAlpha = clamp01(shape.fillOpacity, SHAPE_DEFAULTS.fillOpacity);
|
||||
ctx.fillStyle = shape.color;
|
||||
if (shape.type === 'rect') {
|
||||
ctx.fillRect(x, y, w, h);
|
||||
} else {
|
||||
const cx = x + w / 2;
|
||||
const cy = y + h / 2;
|
||||
ctx.beginPath();
|
||||
ctx.ellipse(cx, cy, Math.abs(w / 2), Math.abs(h / 2), 0, 0, Math.PI * 2);
|
||||
ctx.fill()
|
||||
}
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
} else if (shape.type === 'line') {
|
||||
const x1 = toPx(shape.x1);
|
||||
const y1 = toPx(shape.y1);
|
||||
const x2 = toPx(shape.x2);
|
||||
const y2 = toPx(shape.y2);
|
||||
|
||||
ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(x1, y1);
|
||||
ctx.lineTo(x2, y2);
|
||||
ctx.stroke();
|
||||
ctx.globalAlpha = 1;
|
||||
} else if (shape.type === 'path') {
|
||||
const toPx = (v) => v * cellSize;
|
||||
|
||||
ctx.globalAlpha = clamp01(shape.strokeOpacity, SHAPE_DEFAULTS.strokeOpacity);
|
||||
|
||||
ctx.lineWidth = Math.max(1, toPx(shape.strokeWidth ?? SHAPE_DEFAULTS.strokeWidth));
|
||||
ctx.lineJoin = 'round';
|
||||
ctx.lineCap = 'round';
|
||||
|
||||
const pts = shape.points;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(toPx(pts[0].x), toPx(pts[0].y));
|
||||
for (let i = 1; i < pts.length; i++) {
|
||||
ctx.lineTo(toPx(pts[i].x), toPx(pts[i].y));
|
||||
}
|
||||
ctx.stroke();
|
||||
}
|
||||
|
||||
ctx.restore();
|
||||
}
|
||||
|
||||
function clearCanvas() {
|
||||
if (!ctx) return;
|
||||
ctx.clearRect(0, 0, canvasEl.width / dpr, canvasEl.height / dpr);
|
||||
}
|
||||
|
||||
function setGrid() {
|
||||
const type = getActiveType();
|
||||
|
||||
gridEl.style.backgroundImage = "";
|
||||
gridEl.style.backgroundSize = "";
|
||||
gridEl.style.backgroundPosition = "";
|
||||
gridEl.style.boxShadow = "none";
|
||||
dotEl.classList.add('d-none');
|
||||
|
||||
// Minor dots
|
||||
const dotPx = Math.max(1, Math.round(cellSize * 0.08));
|
||||
const minorColor = '#ddd';
|
||||
|
||||
// Major dots (every 5 cells)
|
||||
const majorStep = cellSize * 5;
|
||||
const majorDotPx = Math.max(dotPx + 1, Math.round(cellSize * 0.12));
|
||||
const majorColor = '#c4c4c4';
|
||||
|
||||
const minorLayer = `radial-gradient(circle, ${minorColor} ${dotPx}px, transparent ${dotPx}px)`;
|
||||
const majorLayer = `radial-gradient(circle, ${majorColor} ${majorDotPx}px, transparent ${majorDotPx}px)`;
|
||||
|
||||
if (type === 'fullGrid') {
|
||||
gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`;
|
||||
gridEl.style.backgroundSize = `${majorStep}px ${majorStep}px, ${cellSize}px ${cellSize}px`;
|
||||
gridEl.style.backgroundPosition =
|
||||
`${majorStep / 2}px ${majorStep / 2}px, ${cellSize / 2}px ${cellSize / 2}px`;
|
||||
gridEl.style.boxShadow = "inset 0 0 0 1px #ccc";
|
||||
|
||||
} else if (type === 'verticalGrid') {
|
||||
gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`;
|
||||
gridEl.style.backgroundSize = `${majorStep}px 100%, ${cellSize}px 100%`;
|
||||
gridEl.style.backgroundPosition =
|
||||
`${majorStep / 2}px 0px, ${cellSize / 2}px 0px`;
|
||||
gridEl.style.boxShadow = "inset 0 1px 0 0 #ccc, inset 0 -1px 0 0 #ccc";
|
||||
|
||||
} else if (type === 'horizontalGrid') {
|
||||
gridEl.style.backgroundImage = `${majorLayer}, ${minorLayer}`;
|
||||
gridEl.style.backgroundSize = `100% ${majorStep}px, 100% ${cellSize}px`;
|
||||
gridEl.style.backgroundPosition =
|
||||
`0px ${majorStep / 2}px, 0px ${cellSize / 2}px`;
|
||||
gridEl.style.boxShadow = "inset 1px 0 0 0 #ccc, inset -1px 0 0 0 #ccc";
|
||||
|
||||
} else { // noGrid
|
||||
gridEl.style.boxShadow = "inset 0 0 0 1px #ccc";
|
||||
}
|
||||
}
|
||||
|
||||
function onPointerUp(e) {
|
||||
if (!currentShape) return;
|
||||
|
||||
|
|
@ -915,7 +931,7 @@ function initGridWidget(root, opts = {}) {
|
|||
// PEN: mutate points and preview the same shape object
|
||||
if (currentShape.tool === 'pen') {
|
||||
penAddPoint(currentShape, e.clientX, e.clientY, 0.02);
|
||||
renderAllWithPreview(currentShape, true);
|
||||
renderAllWithPreview(currentShape, false);
|
||||
return;
|
||||
}
|
||||
|
||||
|
|
@ -938,7 +954,7 @@ function initGridWidget(root, opts = {}) {
|
|||
preview = normalizeEllipse({ ...currentShape, x2: snapX, y2: snapY });
|
||||
}
|
||||
|
||||
renderAllWithPreview(preview, true);
|
||||
renderAllWithPreview(preview, currentShape.tool !== 'pen');
|
||||
});
|
||||
|
||||
gridEl.addEventListener('pointerleave', (e) => {
|
||||
|
|
|
|||
|
|
@ -242,3 +242,6 @@
|
|||
</div>
|
||||
</div>
|
||||
{% endmacro %}
|
||||
|
||||
{% macro viewWidget(uid, json) %}
|
||||
{% endmacro %}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue