/
hamster75
/
Life
Обзор
Документация
Войти
/
hamster75
/
Life
Код
Запросы
0
Задачи
Вики
Пакеты
0
Релизы
6
CI/CD
Аналитика
Безопасность
master
js/editor.js
552 строки
19 KB
Dmitry Smirnov
undo completed
28 май 2026, 11:50
28 май 2026, 11:50
558a20d
Код
Авторство
О чём код?
import { SimState } from './simulation.js'; import { markCellDrawn } from './quest-events.js'; import { rotate90, flipH } from './patterns.js'; export class Editor { constructor(canvas, grid, camera, simulation, renderer) { this.canvas = canvas; this.grid = grid; this.camera = camera; this.sim = simulation; this.renderer = renderer; this.activePattern = null; this._lastCell = null; this._lastMouseCell = null; this._touchDrawing = false; this._touchDisabled = false; this.onEraserChange = null; this.onPatternChange = null; this.brushRadius = 0; // r=0 → 1 кл, r=1 → 5 кл, r=2 → 13... this.brushShape = 'round'; // 'round' | 'square' this.eraserMode = false; this._brushOffsets = this._calcBrush(0); this.onModeChange = null; // callback(label) для статусной строки this.onEnsureEditable = null; // callback() → Promise|undefined — вызывается перед первым штрихом this._pendingEdit = null; // Promise, пока идёт выгрузка паттерна из Worker this.selection = null; // экземпляр Selection (подключается снаружи) this.overlay = null; // экземпляр Overlay (подключается снаружи) this._clipboard = null; // { cells: [[dx,dy]], originX, originY } this._selDragging = false; // флаг: тащим выделение прямо сейчас this._undoStack = []; // [{alive: Set, overlayCells: Map}, ...] canvas.addEventListener('mousedown', this._onMouseDown.bind(this)); canvas.addEventListener('mousemove', this._onMouseMove.bind(this)); canvas.addEventListener('mouseleave', () => { this.renderer.clearPreview(); }); canvas.addEventListener('touchstart', this._onTouchStart.bind(this), { passive: false }); canvas.addEventListener('touchmove', this._onTouchMove.bind(this), { passive: false }); canvas.addEventListener('touchend', this._onTouchEnd.bind(this), { passive: false }); window.addEventListener('keydown', this._onKeyDown.bind(this)); } _calcBrush(r) { const out = []; for (let dy = -r; dy <= r; dy++) for (let dx = -r; dx <= r; dx++) if (this.brushShape === 'square' || dx * dx + dy * dy <= r * r + 0.5) out.push([dx, dy]); return out; } _setBrushRadius(r) { this.brushRadius = Math.max(0, Math.min(7, r)); this._brushOffsets = this._calcBrush(this.brushRadius); this._notifyMode(); } _toggleBrushShape() { this.brushShape = this.brushShape === 'round' ? 'square' : 'round'; this._brushOffsets = this._calcBrush(this.brushRadius); this._notifyMode(); } disableTouchDrawing() { this._touchDisabled = true; } _setEraserMode(on) { this.eraserMode = on; this._notifyMode(); this.canvas.classList.toggle('erase-cursor', on); if (this.onEraserChange) this.onEraserChange(on); } _notifyMode() { if (this.onModeChange) { if (this.selection?.active) { this.onModeChange('SELECT'); return; } const size = 2 * this.brushRadius + 1; const shape = this.brushShape === 'round' ? '●' : '■'; const color = this.overlay?.activeColor ?? 0; if (color > 0) { const mode = this.eraserMode ? 'ERASE' : 'MARKS'; this.onModeChange(`${mode} ${size} ${shape}`, color); } else { const mode = this.eraserMode ? 'ERASE' : 'DRAW'; this.onModeChange(`${mode} ${size} ${shape}`, 0); } } } setPattern(cells) { this.activePattern = cells; if (this._lastMouseCell) { this.renderer.setPreview(cells, this._lastMouseCell, 'stamp'); } if (this.onPatternChange) this.onPatternChange(cells); } clearPattern() { this.activePattern = null; this.renderer.clearPreview(); if (this.onPatternChange) this.onPatternChange(null); } _canEdit() { return this.sim.state !== SimState.RUNNING; } // Возвращает активное хранилище: overlay (если цвет выбран) или grid _activeStore() { return (this.overlay?.activeColor ?? 0) > 0 ? this.overlay : this.grid; } _captureUndo() { const r = this.selection?.rect; this._undoStack.push({ alive: new Set(this.grid._alive), overlayCells: this.overlay ? new Map(this.overlay._cells) : null, selRect: r ? { ...r } : null, }); if (this._undoStack.length > 50) this._undoStack.shift(); } undo() { if (!this._canEdit()) return; const snap = this._undoStack.pop(); if (!snap) return; this.grid.loadAlive(snap.alive); if (this.overlay && snap.overlayCells !== null) this.overlay._cells = snap.overlayCells; if (this.selection) this.selection.rect = snap.selRect; } _cellFromEvent(e) { const rect = this.canvas.getBoundingClientRect(); return this.camera.screenToCell(e.clientX - rect.left, e.clientY - rect.top); } _applyBrush(cx, cy) { const r = this.brushRadius; const color = this.overlay?.activeColor ?? 0; if (color > 0) { // Пишем в слой меток if (this.eraserMode) { for (const [dx, dy] of this._brushOffsets) this.overlay.delete(cx + dx, cy + dy); } else if (r === 0) { this.overlay.toggle(cx, cy, color); } else { for (const [dx, dy] of this._brushOffsets) this.overlay.set(cx + dx, cy + dy, color); } } else { // Основной слой if (this.eraserMode) { for (const [dx, dy] of this._brushOffsets) this.grid.set(cx + dx, cy + dy, false); } else if (r === 0) { this.grid.toggle(cx, cy); } else { for (const [dx, dy] of this._brushOffsets) this.grid.set(cx + dx, cy + dy, true); } } } _onMouseDown(e) { if (e.button !== 0) return; if (!this._canEdit()) return; if (this.camera._spaceDown) return; const cell = this._cellFromEvent(e); // ── Режим выделения ────────────────────────────────────────────────────── if (this.selection?.active) { if (this.selection.rect && this.selection.containsCell(cell)) { // Клик внутри существующего прямоугольника → drag this._startSelectionMove(cell, e); } else { // Клик вне → начать новый rubber band this.selection.startDrag(cell); const onMove = (ev) => { this.selection.updateDrag(this._cellFromEvent(ev)); }; const onUp = () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); this.selection.finishDrag(); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); } return; } if (this.onEnsureEditable) { if (!this._pendingEdit) { const result = this.onEnsureEditable(); this._pendingEdit = result instanceof Promise ? result : Promise.resolve(); this._pendingEdit.finally(() => { this._pendingEdit = null; }); } this._pendingEdit.then(() => { if (!this._canEdit()) return; this._startStroke(cell); }); return; } this._startStroke(cell); } _startStroke(cell) { this._captureUndo(); if (this.activePattern) { const color = this.overlay?.activeColor ?? 0; if (color > 0) this.overlay.stampXor(this.activePattern, cell.x, cell.y, color); else this.grid.stampXor(this.activePattern, cell.x, cell.y); this._lastCell = null; } else { if (!this.eraserMode) markCellDrawn(); this._applyBrush(cell.x, cell.y); this._lastCell = cell; const onMove = (ev) => { if (!this._canEdit()) return; const c = this._cellFromEvent(ev); if (!this._lastCell || c.x !== this._lastCell.x || c.y !== this._lastCell.y) { this._applyBrush(c.x, c.y); this._lastCell = c; } }; const onUp = () => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); this._lastCell = null; }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); } } _onMouseMove(e) { const cell = this._cellFromEvent(e); this._lastMouseCell = cell; if (this.activePattern) { if (this._canEdit()) this.renderer.setPreview(this.activePattern, cell, 'stamp'); return; } if (this._canEdit()) { const mode = this.eraserMode ? 'erase' : 'draw'; this.renderer.setPreview(this._brushOffsets, cell, mode); } } _onKeyDown(e) { if (e.target.tagName === 'INPUT' || e.target.tagName === 'TEXTAREA') return; if ((e.code === 'KeyZ') && (e.ctrlKey || e.metaKey) && !e.shiftKey) { e.preventDefault(); this.undo(); return; } // ── Клавиши режима выделения ──────────────────────────────────────────── if (this.selection?.active) { if (e.code === 'Escape') { this.selection.clearRect(); this.selection.deactivate(); this._notifyMode(); this.canvas.classList.remove('selection-mode'); this.clearPattern(); return; } if (e.code === 'Delete' || e.code === 'Backspace') { e.preventDefault(); this._captureUndo(); this.selection.clearCells(this._activeStore()); this.selection.clearRect(); return; } if ((e.code === 'KeyC') && (e.ctrlKey || e.metaKey)) { e.preventDefault(); if (this.selection.rect) { const cells = this.selection.extractCells(this._activeStore()); this._clipboard = { cells, originX: this.selection.rect.x1, originY: this.selection.rect.y1 }; } return; } if ((e.code === 'KeyX') && (e.ctrlKey || e.metaKey)) { e.preventDefault(); if (this.selection.rect) { this._captureUndo(); const store = this._activeStore(); const cells = this.selection.extractCells(store); this._clipboard = { cells, originX: this.selection.rect.x1, originY: this.selection.rect.y1 }; this.selection.clearCells(store); this.selection.clearRect(); } return; } if ((e.code === 'KeyV') && (e.ctrlKey || e.metaKey)) { e.preventDefault(); if (this._clipboard?.cells.length) { const cells = this._clipboard.cells; const maxX = Math.max(...cells.map(([x]) => x)); const maxY = Math.max(...cells.map(([, y]) => y)); const centered = cells.map(([x, y]) => [x - Math.floor(maxX / 2), y - Math.floor(maxY / 2)]); this.setPattern(centered); this.selection.deactivate(); this.canvas.classList.remove('selection-mode'); this._notifyMode(); } return; } // R / F — rotate/flip выделения на месте (перехватываем до palette.js) if (e.code === 'KeyR' && !e.ctrlKey && !e.metaKey) { e.stopImmediatePropagation(); this._captureUndo(); this._rotateSelection(); return; } if (e.code === 'KeyF' && !e.ctrlKey && !e.metaKey) { e.stopImmediatePropagation(); this._captureUndo(); this._flipSelection(); return; } } // ── M — переключить режим выделения ──────────────────────────────────── // R/F для паттерна на курсоре (включая вставленный из буфера) if (this.activePattern && !this.selection?.active) { if (e.code === 'KeyR' && !e.ctrlKey && !e.metaKey) { e.stopImmediatePropagation(); this.activePattern = rotate90(this.activePattern); if (this._lastMouseCell) this.renderer.setPreview(this.activePattern, this._lastMouseCell, 'stamp'); return; } if (e.code === 'KeyF' && !e.ctrlKey && !e.metaKey) { e.stopImmediatePropagation(); this.activePattern = flipH(this.activePattern); if (this._lastMouseCell) this.renderer.setPreview(this.activePattern, this._lastMouseCell, 'stamp'); return; } } if (e.code === 'KeyM' && !e.ctrlKey && !e.metaKey) { if (this.selection) { if (this.selection.active) { this.selection.deactivate(); this.canvas.classList.remove('selection-mode'); } else { this.selection.activate(); this.canvas.classList.add('selection-mode'); this.clearPattern(); } this._notifyMode(); } return; } if (e.code === 'Escape') { this.clearPattern(); } else if (e.code === 'KeyE' && !e.ctrlKey && !e.metaKey) { this._setEraserMode(true); } else if (e.code === 'KeyD' && !e.ctrlKey && !e.metaKey) { this._setEraserMode(false); } else if (e.code === 'KeyS' && !e.ctrlKey && !e.metaKey) { this._toggleBrushShape(); if (this._lastMouseCell && !this.activePattern) this.renderer.setPreview(this._brushOffsets, this._lastMouseCell, this.eraserMode ? 'erase' : 'draw'); } else if (e.code === 'BracketLeft') { this._setBrushRadius(this.brushRadius - 1); if (this._lastMouseCell && !this.activePattern) this.renderer.setPreview(this._brushOffsets, this._lastMouseCell, this.eraserMode ? 'erase' : 'draw'); } else if (e.code === 'BracketRight') { this._setBrushRadius(this.brushRadius + 1); if (this._lastMouseCell && !this.activePattern) this.renderer.setPreview(this._brushOffsets, this._lastMouseCell, this.eraserMode ? 'erase' : 'draw'); } } _cellFromTouch(t) { const rect = this.canvas.getBoundingClientRect(); return this.camera.screenToCell(t.clientX - rect.left, t.clientY - rect.top); } _onTouchStart(e) { if (this._touchDisabled) return; if (e.touches.length > 1) { this._touchDrawing = false; this._lastCell = null; return; } e.preventDefault(); if (!this._canEdit()) return; const cell = this._cellFromTouch(e.touches[0]); this._lastMouseCell = cell; if (this.onEnsureEditable) { if (!this._pendingEdit) { const result = this.onEnsureEditable(); this._pendingEdit = result instanceof Promise ? result : Promise.resolve(); this._pendingEdit.finally(() => { this._pendingEdit = null; }); } this._pendingEdit.then(() => { if (!this._canEdit()) return; this._startTouchStroke(cell); }); return; } this._startTouchStroke(cell); } _startTouchStroke(cell) { if (this.activePattern) { this.renderer.setPreview(this.activePattern, cell, 'stamp'); const color = this.overlay?.activeColor ?? 0; if (color > 0) this.overlay.stampXor(this.activePattern, cell.x, cell.y, color); else this.grid.stampXor(this.activePattern, cell.x, cell.y); } else { this._touchDrawing = true; if (!this.eraserMode) markCellDrawn(); this._applyBrush(cell.x, cell.y); this._lastCell = cell; this.renderer.setPreview(this._brushOffsets, cell, this.eraserMode ? 'erase' : 'draw'); } } _onTouchMove(e) { if (this._touchDisabled) return; if (e.touches.length !== 1) return; e.preventDefault(); const cell = this._cellFromTouch(e.touches[0]); this._lastMouseCell = cell; if (this.activePattern) { if (this._canEdit()) this.renderer.setPreview(this.activePattern, cell, 'stamp'); } else if (this._touchDrawing && this._canEdit()) { if (!this._lastCell || cell.x !== this._lastCell.x || cell.y !== this._lastCell.y) { this._applyBrush(cell.x, cell.y); this._lastCell = cell; this.renderer.setPreview(this._brushOffsets, cell, this.eraserMode ? 'erase' : 'draw'); } } } _onTouchEnd(e) { if (this._touchDisabled) return; e.preventDefault(); this._touchDrawing = false; this._lastCell = null; } // ── Перемещение выделения drag ──────────────────────────────────────────── _startSelectionMove(startCell, e) { const sel = this.selection; const store = this._activeStore(); const origRect = { ...sel.rect }; this._captureUndo(); const cells = sel.extractCells(store); sel.clearCells(store); // Показываем ghost через preview this.renderer.setPreview(cells, { x: origRect.x1, y: origRect.y1 }, 'stamp'); const onMove = (ev) => { const c = this._cellFromEvent(ev); const dx = c.x - startCell.x; const dy = c.y - startCell.y; sel.rect = { x1: origRect.x1 + dx, y1: origRect.y1 + dy, x2: origRect.x2 + dx, y2: origRect.y2 + dy, }; this.renderer.setPreview(cells, { x: sel.rect.x1, y: sel.rect.y1 }, 'stamp'); }; const onUp = (ev) => { window.removeEventListener('mousemove', onMove); window.removeEventListener('mouseup', onUp); const c = this._cellFromEvent(ev); const dx = c.x - startCell.x; const dy = c.y - startCell.y; sel.rect = { x1: origRect.x1 + dx, y1: origRect.y1 + dy, x2: origRect.x2 + dx, y2: origRect.y2 + dy, }; const color = this.overlay?.activeColor ?? 0; for (const [ox, oy] of cells) color > 0 ? store.set(sel.rect.x1 + ox, sel.rect.y1 + oy, color) : store.set(sel.rect.x1 + ox, sel.rect.y1 + oy, true); this.renderer.clearPreview(); }; window.addEventListener('mousemove', onMove); window.addEventListener('mouseup', onUp); } // ── Rotate / Flip выделения на месте ───────────────────────────────────── _rotateSelection() { const sel = this.selection; const store = this._activeStore(); if (!sel?.rect) return; const cells = sel.extractCells(store); if (!cells.length) return; sel.clearCells(store); const w = sel.rect.x2 - sel.rect.x1; const h = sel.rect.y2 - sel.rect.y1; const rotated = rotate90(cells); const color = this.overlay?.activeColor ?? 0; for (const [dx, dy] of rotated) color > 0 ? store.set(sel.rect.x1 + dx, sel.rect.y1 + dy, color) : store.set(sel.rect.x1 + dx, sel.rect.y1 + dy, true); sel.rect = { x1: sel.rect.x1, y1: sel.rect.y1, x2: sel.rect.x1 + h, y2: sel.rect.y1 + w }; } _flipSelection() { const sel = this.selection; const store = this._activeStore(); if (!sel?.rect) return; const cells = sel.extractCells(store); if (!cells.length) return; sel.clearCells(store); const flipped = flipH(cells); const color = this.overlay?.activeColor ?? 0; for (const [dx, dy] of flipped) color > 0 ? store.set(sel.rect.x1 + dx, sel.rect.y1 + dy, color) : store.set(sel.rect.x1 + dx, sel.rect.y1 + dy, true); } }