diff --git a/core/tile-patterns.ts b/core/tile-patterns.ts new file mode 100644 index 0000000..ebf7edb --- /dev/null +++ b/core/tile-patterns.ts @@ -0,0 +1,89 @@ +/** + * Canonical tile patterns for linear features. + * Each pattern is defined by a base edge mask and a human-readable name. + * Patterns can be rotated by 60° increments to produce all placements. + */ + +import type { EdgeMask } from './types.js'; +import { HexEdge } from './types.js'; +import { edgeMask, rotateMask, edgeCount } from './edge-connectivity.js'; + +export interface TilePattern { + id: string; + name: string; + baseMask: EdgeMask; + /** Number of distinct rotations (accounting for symmetry) */ + rotations: number; +} + +/** All canonical patterns for linear features */ +export const TILE_PATTERNS: TilePattern[] = [ + { + id: 'dead-end', + name: 'Dead End', + baseMask: edgeMask(HexEdge.E), + rotations: 6, + }, + { + id: 'straight', + name: 'Straight', + baseMask: edgeMask(HexEdge.E, HexEdge.W), + rotations: 3, // Symmetric: E-W = same as W-E + }, + { + id: 'gentle-curve', + name: 'Wide Curve', + baseMask: edgeMask(HexEdge.E, HexEdge.SW), + rotations: 6, + }, + { + id: 'sharp-bend', + name: 'Sharp Bend', + baseMask: edgeMask(HexEdge.E, HexEdge.SE), + rotations: 6, + }, + { + id: 'y-junction', + name: 'Y Split', + baseMask: edgeMask(HexEdge.NE, HexEdge.SE, HexEdge.W), + rotations: 6, + }, + { + id: 'y-junction-wide', + name: 'Y Wide', + baseMask: edgeMask(HexEdge.NE, HexEdge.SW, HexEdge.SE), + rotations: 6, + }, + { + id: 'crossroads', + name: 'Cross', + baseMask: edgeMask(HexEdge.NE, HexEdge.E, HexEdge.SW, HexEdge.W), + rotations: 3, + }, +]; + +/** + * Get all unique rotations for a pattern. + * Returns array of { mask, rotation } where rotation is the number of 60° steps. + */ +export function getPatternRotations(pattern: TilePattern): Array<{ mask: EdgeMask; rotation: number }> { + const seen = new Set(); + const result: Array<{ mask: EdgeMask; rotation: number }> = []; + + for (let r = 0; r < 6; r++) { + const mask = rotateMask(pattern.baseMask, r); + if (!seen.has(mask)) { + seen.add(mask); + result.push({ mask, rotation: r }); + } + } + + return result; +} + +/** + * Rotate a mask by N steps (each step = 60° clockwise). + */ +export function rotatePattern(mask: EdgeMask, steps: number): EdgeMask { + return rotateMask(mask, steps); +} diff --git a/src/main.ts b/src/main.ts index 02747d7..791516c 100644 --- a/src/main.ts +++ b/src/main.ts @@ -7,11 +7,10 @@ import type { AxialCoord, TerrainType } from '../core/types.js'; import { createSidebar } from './ui/sidebar.js'; import { createToolbar, type ToolMode } from './ui/toolbar.js'; import { createTerrainPicker } from './ui/terrain-picker.js'; -import { createHexInspector } from './ui/hex-inspector.js'; +import { createHexInspector, type FeatureRotateEvent } from './ui/hex-inspector.js'; import { createMapSettings } from './ui/map-settings.js'; +import type { SelectedPattern } from './ui/tile-palette.js'; import { - edgeMask, - toggleEdge, enforceEdgeConstraints, applyConstraintActions, } from '../core/edge-connectivity.js'; @@ -21,9 +20,10 @@ const STORAGE_KEY = 'hexifyer_map'; // --- State --- const hexMap = new HexMap(); let currentMode: ToolMode = 'select'; -let selectedTerrain: TerrainType | null = null; +let selectedAreaTerrain: TerrainType | null = null; +let selectedPattern: SelectedPattern | null = null; let selectedHex: AxialCoord | null = null; -const hexSize = 48; // Fixed per map — set once +const hexSize = 48; const origin = { x: 0, y: 0 }; let saveTimeout: ReturnType | null = null; @@ -44,30 +44,43 @@ hexLayer.addTo(map); const sidebarEl = document.getElementById('sidebar')!; const { toolbar, terrainPicker, hexInspector, settings } = createSidebar(sidebarEl); -const terrainPickerUI = createTerrainPicker(terrainPicker, (terrain) => { - selectedTerrain = terrain; -}); +const terrainPickerUI = createTerrainPicker( + terrainPicker, + (terrain) => { selectedAreaTerrain = terrain; }, + (pattern) => { selectedPattern = pattern; }, +); createToolbar(toolbar, (mode) => { currentMode = mode; terrainPickerUI.setMode(mode); }); -const hexInspectorUI = createHexInspector(hexInspector); +const hexInspectorUI = createHexInspector(hexInspector, (event: FeatureRotateEvent) => { + if (event.newMask === 0) { + hexMap.removeFeature(event.coord, event.terrainId); + } else { + hexMap.setFeature(event.coord, event.terrainId, event.newMask); + // Re-enforce constraints after rotation + const actions = enforceEdgeConstraints(hexMap, event.coord, event.terrainId, event.newMask); + applyConstraintActions(hexMap, actions); + } + hexLayer.redraw(); + hexInspectorUI.update(event.coord, hexMap.getTerrain(event.coord)); + scheduleSave(); +}); createMapSettings(settings, { showGrid: true, opacity: 0.7 }, (s) => { hexLayer.setShowGrid(s.showGrid); hexLayer.setHexOpacity(s.opacity); }); -// --- Auto-save to localStorage (debounced) --- +// --- Auto-save to localStorage --- function scheduleSave() { if (saveTimeout) clearTimeout(saveTimeout); saveTimeout = setTimeout(() => { if (!hexMap.dirty) return; try { - const data = hexMap.serialize(); - localStorage.setItem(STORAGE_KEY, JSON.stringify(data)); + localStorage.setItem(STORAGE_KEY, JSON.stringify(hexMap.serialize())); hexMap.markClean(); } catch (err) { console.error('[save] localStorage failed:', err); @@ -77,8 +90,7 @@ function scheduleSave() { // --- File export/import --- function exportMap() { - const data = hexMap.serialize(); - const json = JSON.stringify(data, null, 2); + const json = JSON.stringify(hexMap.serialize(), null, 2); const blob = new Blob([json], { type: 'application/json' }); const url = URL.createObjectURL(blob); const a = document.createElement('a'); @@ -118,7 +130,6 @@ function importMap() { input.click(); } -// Expose to sidebar buttons (window as any).__hexifyer = { exportMap, importMap }; // --- Interaction handlers --- @@ -130,29 +141,24 @@ function handleSelect(event: HexClickEvent) { } function handlePaint(event: HexClickEvent) { - if (!selectedTerrain) return; - hexMap.setBase(event.coord, selectedTerrain.id); + if (!selectedAreaTerrain) return; + hexMap.setBase(event.coord, selectedAreaTerrain.id); hexLayer.redraw(); selectedHex = event.coord; hexInspectorUI.update(selectedHex, hexMap.getTerrain(selectedHex)); scheduleSave(); } -function handleFeature(event: HexClickEvent) { - if (!selectedTerrain) return; +function handleFeaturePlacement(event: HexClickEvent) { + if (!selectedPattern) return; const coord = event.coord; - const terrain = hexMap.getTerrain(coord); - const existing = terrain.features.find(f => f.terrainId === selectedTerrain!.id); - const currentMask = existing?.edgeMask ?? 0; - const newMask = toggleEdge(currentMask, event.edge); - hexMap.setFeature(coord, selectedTerrain.id, newMask); + // Place the selected pattern on this hex + hexMap.setFeature(coord, selectedPattern.terrainId, selectedPattern.mask); - if (newMask > currentMask) { - const addedEdgeMask = edgeMask(event.edge); - const actions = enforceEdgeConstraints(hexMap, coord, selectedTerrain.id, addedEdgeMask); - applyConstraintActions(hexMap, actions); - } + // Enforce edge constraints on neighbors + const actions = enforceEdgeConstraints(hexMap, coord, selectedPattern.terrainId, selectedPattern.mask); + applyConstraintActions(hexMap, actions); selectedHex = coord; hexLayer.setSelectedHex(selectedHex); @@ -167,13 +173,34 @@ attachHexInteraction( (event) => { if (currentMode === 'select') handleSelect(event); else if (currentMode === 'paint') handlePaint(event); - else if (currentMode === 'feature') handleFeature(event); + else if (currentMode === 'feature') handleFeaturePlacement(event); }, (event) => { if (currentMode === 'paint') handlePaint(event); }, ); +// --- Keyboard rotation (Q/E) --- +document.addEventListener('keydown', (e) => { + if (currentMode !== 'feature') return; + if (e.key === 'q' || e.key === 'Q') { + terrainPickerUI.rotateCCW(); + } else if (e.key === 'e' || e.key === 'E') { + terrainPickerUI.rotateCW(); + } +}); + +// --- Mouse wheel rotation --- +document.getElementById('map')?.addEventListener('wheel', (e) => { + if (currentMode !== 'feature' || !selectedPattern) return; + e.preventDefault(); + if (e.deltaY > 0) { + terrainPickerUI.rotateCW(); + } else { + terrainPickerUI.rotateCCW(); + } +}, { passive: false }); + // --- Load from localStorage --- try { const saved = localStorage.getItem(STORAGE_KEY); @@ -187,7 +214,6 @@ try { } hexMap.markClean(); hexLayer.redraw(); - console.log(`[init] Loaded ${data.hexes.length} hexes from localStorage`); } } catch (err) { console.warn('[init] Failed to load from localStorage:', err); diff --git a/src/style/main.css b/src/style/main.css index 4121ea9..b0da0dc 100644 --- a/src/style/main.css +++ b/src/style/main.css @@ -168,3 +168,108 @@ html, body { color: #666; margin: 8px 0 4px 0; } + +/* Tile Palette */ +.palette-terrain-header { + display: flex; + align-items: center; + gap: 6px; + font-size: 11px; + font-weight: 600; + color: #ccc; + margin: 8px 0 4px; +} + +.palette-terrain-header .terrain-swatch { + width: 10px; + height: 10px; + border-radius: 2px; + display: inline-block; +} + +.palette-grid { + display: grid; + grid-template-columns: repeat(4, 1fr); + gap: 3px; + margin-bottom: 8px; +} + +.palette-item { + display: flex; + flex-direction: column; + align-items: center; + gap: 2px; + padding: 4px 2px; + border: 1px solid transparent; + border-radius: 4px; + cursor: pointer; + transition: all 0.1s; +} + +.palette-item:hover { + background: #2a2a4a; + border-color: #3a3a6a; +} + +.palette-item.selected { + background: #2a2a5a; + border-color: #7a7aff; +} + +.palette-item canvas { + display: block; +} + +.palette-label { + font-size: 8px; + color: #888; + text-align: center; + line-height: 1.1; +} + +.palette-hint { + font-size: 10px; + color: #666; + text-align: center; + padding: 4px; + margin-top: 4px; + border-top: 1px solid #2a2a4a; +} + +/* Feature rotation controls */ +.feature-row { + display: flex; + align-items: center; + justify-content: space-between; + margin-top: 2px; +} + +.rotate-controls { + display: flex; + gap: 2px; +} + +.rotate-btn { + width: 22px; + height: 22px; + padding: 0; + border: 1px solid #2a2a4a; + border-radius: 3px; + background: #1a1a2e; + color: #ccc; + cursor: pointer; + font-size: 12px; + display: flex; + align-items: center; + justify-content: center; +} + +.rotate-btn:hover { + background: #2a2a4a; + color: #fff; +} + +.rotate-btn.remove-btn:hover { + background: #4a2020; + border-color: #6a3030; +} diff --git a/src/ui/hex-inspector.ts b/src/ui/hex-inspector.ts index 5b51b1a..9789f7d 100644 --- a/src/ui/hex-inspector.ts +++ b/src/ui/hex-inspector.ts @@ -1,6 +1,6 @@ import type { AxialCoord, HexTerrain } from '../../core/types.js'; import { getTerrainType } from '../../core/terrain.js'; -import { connectedEdges } from '../../core/edge-connectivity.js'; +import { connectedEdges, rotateMask } from '../../core/edge-connectivity.js'; import { HexEdge } from '../../core/types.js'; const EDGE_NAMES: Record = { @@ -12,39 +12,118 @@ const EDGE_NAMES: Record = { [HexEdge.NW]: 'NW', }; -export function createHexInspector(container: HTMLElement): { +export interface FeatureRotateEvent { + coord: AxialCoord; + terrainId: string; + newMask: number; +} + +export function createHexInspector( + container: HTMLElement, + onRotateFeature?: (event: FeatureRotateEvent) => void, +): { update: (coord: AxialCoord | null, terrain: HexTerrain | null) => void; } { - function update(coord: AxialCoord | null, terrain: HexTerrain | null) { - if (!coord || !terrain) { + let currentCoord: AxialCoord | null = null; + let currentTerrain: HexTerrain | null = null; + + function render() { + if (!currentCoord || !currentTerrain) { container.innerHTML = '
No hex selected
'; return; } - const baseType = getTerrainType(terrain.base); - let html = '
'; - html += `
q: ${coord.q}, r: ${coord.r}
`; - html += `
`; - html += ``; - html += `${baseType?.name ?? terrain.base}`; - html += `
`; + const baseType = getTerrainType(currentTerrain.base); + container.innerHTML = ''; - if (terrain.features.length > 0) { - html += '
Features:
'; - for (const feature of terrain.features) { + // Coordinate + const coordEl = document.createElement('div'); + coordEl.className = 'coord'; + coordEl.textContent = `q: ${currentCoord.q}, r: ${currentCoord.r}`; + container.appendChild(coordEl); + + // Base terrain + const baseEl = document.createElement('div'); + baseEl.className = 'terrain-label'; + baseEl.innerHTML = ` ${baseType?.name ?? currentTerrain.base}`; + container.appendChild(baseEl); + + // Features with rotation controls + if (currentTerrain.features.length > 0) { + const featHeader = document.createElement('div'); + featHeader.style.cssText = 'margin-top:6px;font-size:11px;color:#888'; + featHeader.textContent = 'Features:'; + container.appendChild(featHeader); + + for (const feature of currentTerrain.features) { const type = getTerrainType(feature.terrainId); - const edges = connectedEdges(feature.edgeMask) - .map(e => EDGE_NAMES[e]) - .join(', '); - html += `
`; - html += ``; - html += `${type?.name ?? feature.terrainId}: ${edges}`; - html += `
`; + const edges = connectedEdges(feature.edgeMask).map(e => EDGE_NAMES[e]).join(', '); + + const row = document.createElement('div'); + row.className = 'feature-row'; + + const label = document.createElement('div'); + label.className = 'terrain-label'; + label.innerHTML = ` ${type?.name ?? feature.terrainId}: ${edges}`; + row.appendChild(label); + + if (onRotateFeature) { + const controls = document.createElement('div'); + controls.className = 'rotate-controls'; + + const ccwBtn = document.createElement('button'); + ccwBtn.className = 'rotate-btn'; + ccwBtn.textContent = '\u21B6'; // ↶ + ccwBtn.title = 'Rotate CCW'; + ccwBtn.addEventListener('click', () => { + const newMask = rotateMask(feature.edgeMask, 5); // -1 = +5 mod 6 + onRotateFeature({ + coord: currentCoord!, + terrainId: feature.terrainId, + newMask, + }); + }); + + const cwBtn = document.createElement('button'); + cwBtn.className = 'rotate-btn'; + cwBtn.textContent = '\u21B7'; // ↷ + cwBtn.title = 'Rotate CW'; + cwBtn.addEventListener('click', () => { + const newMask = rotateMask(feature.edgeMask, 1); + onRotateFeature({ + coord: currentCoord!, + terrainId: feature.terrainId, + newMask, + }); + }); + + const removeBtn = document.createElement('button'); + removeBtn.className = 'rotate-btn remove-btn'; + removeBtn.textContent = '\u2715'; // ✕ + removeBtn.title = 'Remove'; + removeBtn.addEventListener('click', () => { + onRotateFeature({ + coord: currentCoord!, + terrainId: feature.terrainId, + newMask: 0, // 0 = remove + }); + }); + + controls.appendChild(ccwBtn); + controls.appendChild(cwBtn); + controls.appendChild(removeBtn); + row.appendChild(controls); + } + + container.appendChild(row); } } + } - html += '
'; - container.innerHTML = html; + function update(coord: AxialCoord | null, terrain: HexTerrain | null) { + currentCoord = coord; + currentTerrain = terrain; + render(); } update(null, null); diff --git a/src/ui/terrain-picker.ts b/src/ui/terrain-picker.ts index a54a578..7f22bd2 100644 --- a/src/ui/terrain-picker.ts +++ b/src/ui/terrain-picker.ts @@ -1,16 +1,21 @@ import type { TerrainType } from '../../core/types.js'; -import { getAreaTerrains, getLinearTerrains } from '../../core/terrain.js'; +import { getAreaTerrains } from '../../core/terrain.js'; import type { ToolMode } from './toolbar.js'; +import { createTilePalette, type SelectedPattern } from './tile-palette.js'; export function createTerrainPicker( container: HTMLElement, - onChange: (terrain: TerrainType) => void, + onAreaSelect: (terrain: TerrainType) => void, + onPatternSelect: (pattern: SelectedPattern | null) => void, ): { setMode: (mode: ToolMode) => void; - getSelected: () => TerrainType | null; + getSelectedArea: () => TerrainType | null; + rotateCW: () => void; + rotateCCW: () => void; } { - let selected: TerrainType | null = null; + let selectedArea: TerrainType | null = null; let currentMode: ToolMode = 'select'; + let palette: ReturnType | null = null; function render() { container.innerHTML = ''; @@ -20,50 +25,59 @@ export function createTerrainPicker( return; } - const terrains = currentMode === 'paint' ? getAreaTerrains() : getLinearTerrains(); - const label = currentMode === 'paint' ? 'Area Terrain' : 'Linear Features'; + if (currentMode === 'paint') { + const terrains = getAreaTerrains(); - const sectionLabel = document.createElement('div'); - sectionLabel.className = 'terrain-section-label'; - sectionLabel.textContent = label; - container.appendChild(sectionLabel); + const sectionLabel = document.createElement('div'); + sectionLabel.className = 'terrain-section-label'; + sectionLabel.textContent = 'Area Terrain'; + container.appendChild(sectionLabel); - const grid = document.createElement('div'); - grid.className = 'terrain-grid'; + const grid = document.createElement('div'); + grid.className = 'terrain-grid'; - for (const terrain of terrains) { - const btn = document.createElement('button'); - btn.className = 'terrain-btn'; - if (selected?.id === terrain.id) btn.classList.add('selected'); + for (const terrain of terrains) { + const btn = document.createElement('button'); + btn.className = 'terrain-btn'; + if (selectedArea?.id === terrain.id) btn.classList.add('selected'); - const swatch = document.createElement('span'); - swatch.className = 'terrain-swatch'; - swatch.style.backgroundColor = terrain.color; + const swatch = document.createElement('span'); + swatch.className = 'terrain-swatch'; + swatch.style.backgroundColor = terrain.color; - const name = document.createElement('span'); - name.textContent = terrain.name; + const name = document.createElement('span'); + name.textContent = terrain.name; - btn.appendChild(swatch); - btn.appendChild(name); + btn.appendChild(swatch); + btn.appendChild(name); - btn.addEventListener('click', () => { - selected = terrain; - onChange(terrain); - render(); - }); + btn.addEventListener('click', () => { + selectedArea = terrain; + onAreaSelect(terrain); + render(); + }); - grid.appendChild(btn); + grid.appendChild(btn); + } + + container.appendChild(grid); + + // Auto-select first + if (!selectedArea || !terrains.find(t => t.id === selectedArea!.id)) { + selectedArea = terrains[0] ?? null; + if (selectedArea) onAreaSelect(selectedArea); + grid.querySelector('.terrain-btn')?.classList.add('selected'); + } + + return; } - container.appendChild(grid); - - // Auto-select first if nothing selected - if (!selected || !terrains.find(t => t.id === selected!.id)) { - selected = terrains[0] ?? null; - if (selected) onChange(selected); - // Re-render to show selection - const firstBtn = grid.querySelector('.terrain-btn'); - firstBtn?.classList.add('selected'); + if (currentMode === 'feature') { + // Tile palette with previews + const paletteContainer = document.createElement('div'); + container.appendChild(paletteContainer); + palette = createTilePalette(paletteContainer, onPatternSelect); + return; } } @@ -72,9 +86,12 @@ export function createTerrainPicker( return { setMode(mode: ToolMode) { currentMode = mode; - selected = null; + selectedArea = null; + palette = null; render(); }, - getSelected: () => selected, + getSelectedArea: () => selectedArea, + rotateCW() { palette?.rotateCW(); }, + rotateCCW() { palette?.rotateCCW(); }, }; } diff --git a/src/ui/tile-palette.ts b/src/ui/tile-palette.ts new file mode 100644 index 0000000..0e44b2a --- /dev/null +++ b/src/ui/tile-palette.ts @@ -0,0 +1,152 @@ +import type { TerrainType, EdgeMask } from '../../core/types.js'; +import { getLinearTerrains, getTerrainType } from '../../core/terrain.js'; +import { TILE_PATTERNS, rotatePattern } from '../../core/tile-patterns.js'; +import { computeHexGeometry } from '../../core/coords.js'; +import { renderHex } from '../svg/renderer.js'; + +export interface SelectedPattern { + terrainId: string; + mask: EdgeMask; + rotation: number; + patternId: string; +} + +export function createTilePalette( + container: HTMLElement, + onSelect: (pattern: SelectedPattern | null) => void, +): { + getSelected: () => SelectedPattern | null; + rotateCW: () => void; + rotateCCW: () => void; +} { + let selected: SelectedPattern | null = null; + let currentTerrainId: string | null = null; + + function render() { + container.innerHTML = ''; + + const linearTerrains = getLinearTerrains(); + + for (const terrain of linearTerrains) { + // Terrain header + const header = document.createElement('div'); + header.className = 'palette-terrain-header'; + header.innerHTML = `${terrain.name}`; + container.appendChild(header); + + // Pattern grid + const grid = document.createElement('div'); + grid.className = 'palette-grid'; + + for (const pattern of TILE_PATTERNS) { + const item = document.createElement('div'); + item.className = 'palette-item'; + + const isSelected = selected?.terrainId === terrain.id && + selected?.patternId === pattern.id; + if (isSelected) item.classList.add('selected'); + + // Render preview hex + const canvas = document.createElement('canvas'); + const previewSize = 18; + canvas.width = previewSize * 2 + 4; + canvas.height = previewSize * 2 + 4; + + const mask = isSelected && selected + ? rotatePattern(pattern.baseMask, selected.rotation) + : pattern.baseMask; + + renderPreview(canvas, terrain, mask, previewSize); + item.appendChild(canvas); + + // Label + const label = document.createElement('span'); + label.className = 'palette-label'; + label.textContent = pattern.name; + item.appendChild(label); + + item.addEventListener('click', () => { + if (isSelected) { + // Deselect + selected = null; + currentTerrainId = null; + } else { + selected = { + terrainId: terrain.id, + mask: pattern.baseMask, + rotation: 0, + patternId: pattern.id, + }; + currentTerrainId = terrain.id; + } + onSelect(selected); + render(); + }); + + grid.appendChild(item); + } + + container.appendChild(grid); + } + + // Rotation hint if something is selected + if (selected) { + const hint = document.createElement('div'); + hint.className = 'palette-hint'; + hint.textContent = 'Q/E or scroll to rotate before placing'; + container.appendChild(hint); + } + } + + function rotateCW() { + if (!selected) return; + selected.rotation = (selected.rotation + 1) % 6; + selected.mask = rotatePattern( + TILE_PATTERNS.find(p => p.id === selected!.patternId)!.baseMask, + selected.rotation, + ); + onSelect(selected); + render(); + } + + function rotateCCW() { + if (!selected) return; + selected.rotation = (selected.rotation + 5) % 6; // +5 = -1 mod 6 + selected.mask = rotatePattern( + TILE_PATTERNS.find(p => p.id === selected!.patternId)!.baseMask, + selected.rotation, + ); + onSelect(selected); + render(); + } + + render(); + + return { + getSelected: () => selected, + rotateCW, + rotateCCW, + }; +} + +function renderPreview( + canvas: HTMLCanvasElement, + terrain: TerrainType, + mask: EdgeMask, + size: number, +): void { + const ctx = canvas.getContext('2d')!; + const cx = canvas.width / 2; + const cy = canvas.height / 2; + + const geom = computeHexGeometry(cx, cy, size); + + renderHex(ctx, geom, { + base: 'plains', + features: [{ terrainId: terrain.id, edgeMask: mask }], + }, { + opacity: 0.9, + showGrid: true, + selected: false, + }); +}