Fix coastline: 2-edge only, explicit waterSide, flip control
Coastline analysis and fix: - Coastlines now only show 2-edge patterns (straight, wide curve, sharp bend) — Y-junctions and crossroads removed since a coast always divides exactly two sides - Added waterSide property to HexFeature (1=CW, -1=CCW) — stored explicitly instead of computed by broken center-distance heuristic that flipped water/land at rotation midpoint - F key or "Flip water side" button in palette to toggle which side is water before placing - Inspector shows flip button (swap arrows) for placed coastlines - Rotation preserves waterSide — no more land/water swaps - Separate pattern lists: GENERAL_PATTERNS for road/river, COASTLINE_PATTERNS for coastline (core/tile-patterns.ts) Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -42,13 +42,14 @@ export class HexMap {
|
||||
}
|
||||
|
||||
/** Add or update a linear feature on a hex */
|
||||
setFeature(coord: AxialCoord, terrainId: string, edgeMask: EdgeMask): void {
|
||||
setFeature(coord: AxialCoord, terrainId: string, edgeMask: EdgeMask, waterSide?: 1 | -1): void {
|
||||
const terrain = this.getTerrain(coord);
|
||||
const existing = terrain.features.find(f => f.terrainId === terrainId);
|
||||
if (existing) {
|
||||
existing.edgeMask = edgeMask;
|
||||
if (waterSide !== undefined) existing.waterSide = waterSide;
|
||||
} else {
|
||||
terrain.features.push({ terrainId, edgeMask });
|
||||
terrain.features.push({ terrainId, edgeMask, ...(waterSide !== undefined ? { waterSide } : {}) });
|
||||
}
|
||||
// Remove features with empty mask
|
||||
terrain.features = terrain.features.filter(f => f.edgeMask !== 0);
|
||||
|
||||
@@ -1,84 +1,107 @@
|
||||
/**
|
||||
* 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.
|
||||
* Each pattern has a base edge mask and can be rotated in 60° increments.
|
||||
* Coastlines only use 2-edge patterns and have an additional waterSide property.
|
||||
*/
|
||||
|
||||
import type { EdgeMask } from './types.js';
|
||||
import { HexEdge } from './types.js';
|
||||
import { edgeMask, rotateMask, edgeCount } from './edge-connectivity.js';
|
||||
import { edgeMask, rotateMask } from './edge-connectivity.js';
|
||||
|
||||
export interface TilePattern {
|
||||
id: string;
|
||||
name: string;
|
||||
baseMask: EdgeMask;
|
||||
/** Number of distinct rotations (accounting for symmetry) */
|
||||
/** Max distinct rotations (accounting for symmetry) */
|
||||
rotations: number;
|
||||
/** Number of connected edges */
|
||||
edgeCount: number;
|
||||
}
|
||||
|
||||
/** All canonical patterns for linear features */
|
||||
export const TILE_PATTERNS: TilePattern[] = [
|
||||
/** General patterns for road/river (all edge counts) */
|
||||
export const GENERAL_PATTERNS: TilePattern[] = [
|
||||
{
|
||||
id: 'dead-end',
|
||||
name: 'Dead End',
|
||||
baseMask: edgeMask(HexEdge.E),
|
||||
rotations: 6,
|
||||
edgeCount: 1,
|
||||
},
|
||||
{
|
||||
id: 'straight',
|
||||
name: 'Straight',
|
||||
baseMask: edgeMask(HexEdge.E, HexEdge.W),
|
||||
rotations: 3, // Symmetric: E-W = same as W-E
|
||||
rotations: 3,
|
||||
edgeCount: 2,
|
||||
},
|
||||
{
|
||||
id: 'gentle-curve',
|
||||
name: 'Wide Curve',
|
||||
baseMask: edgeMask(HexEdge.E, HexEdge.SW),
|
||||
rotations: 6,
|
||||
edgeCount: 2,
|
||||
},
|
||||
{
|
||||
id: 'sharp-bend',
|
||||
name: 'Sharp Bend',
|
||||
baseMask: edgeMask(HexEdge.E, HexEdge.SE),
|
||||
rotations: 6,
|
||||
edgeCount: 2,
|
||||
},
|
||||
{
|
||||
id: 'y-junction',
|
||||
name: 'Y Split',
|
||||
baseMask: edgeMask(HexEdge.NE, HexEdge.SE, HexEdge.W),
|
||||
rotations: 6,
|
||||
edgeCount: 3,
|
||||
},
|
||||
{
|
||||
id: 'y-junction-wide',
|
||||
name: 'Y Wide',
|
||||
baseMask: edgeMask(HexEdge.NE, HexEdge.SW, HexEdge.SE),
|
||||
rotations: 6,
|
||||
edgeCount: 3,
|
||||
},
|
||||
{
|
||||
id: 'crossroads',
|
||||
name: 'Cross',
|
||||
baseMask: edgeMask(HexEdge.NE, HexEdge.E, HexEdge.SW, HexEdge.W),
|
||||
rotations: 3,
|
||||
edgeCount: 4,
|
||||
},
|
||||
];
|
||||
|
||||
/** Coastline patterns — only 2-edge (a coast is always a line from A to B) */
|
||||
export const COASTLINE_PATTERNS: TilePattern[] = [
|
||||
{
|
||||
id: 'straight',
|
||||
name: 'Straight',
|
||||
baseMask: edgeMask(HexEdge.E, HexEdge.W),
|
||||
rotations: 3,
|
||||
edgeCount: 2,
|
||||
},
|
||||
{
|
||||
id: 'gentle-curve',
|
||||
name: 'Wide Curve',
|
||||
baseMask: edgeMask(HexEdge.E, HexEdge.SW),
|
||||
rotations: 6,
|
||||
edgeCount: 2,
|
||||
},
|
||||
{
|
||||
id: 'sharp-bend',
|
||||
name: 'Sharp Bend',
|
||||
baseMask: edgeMask(HexEdge.E, HexEdge.SE),
|
||||
rotations: 6,
|
||||
edgeCount: 2,
|
||||
},
|
||||
];
|
||||
|
||||
/**
|
||||
* Get all unique rotations for a pattern.
|
||||
* Returns array of { mask, rotation } where rotation is the number of 60° steps.
|
||||
* Get the appropriate patterns for a terrain type.
|
||||
*/
|
||||
export function getPatternRotations(pattern: TilePattern): Array<{ mask: EdgeMask; rotation: number }> {
|
||||
const seen = new Set<EdgeMask>();
|
||||
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;
|
||||
export function getPatternsForTerrain(terrainId: string): TilePattern[] {
|
||||
if (terrainId === 'coastline') return COASTLINE_PATTERNS;
|
||||
return GENERAL_PATTERNS;
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -69,6 +69,13 @@ export interface TerrainType {
|
||||
export interface HexFeature {
|
||||
terrainId: string;
|
||||
edgeMask: EdgeMask;
|
||||
/**
|
||||
* For coastline only: which side of the curve is water.
|
||||
* 1 = clockwise side (right side looking from first edge to second)
|
||||
* -1 = counter-clockwise side (left side)
|
||||
* Undefined for non-coastline features.
|
||||
*/
|
||||
waterSide?: 1 | -1;
|
||||
}
|
||||
|
||||
/** Complete terrain state for a single hex */
|
||||
|
||||
22
src/main.ts
22
src/main.ts
@@ -58,9 +58,20 @@ createToolbar(toolbar, (mode) => {
|
||||
const hexInspectorUI = createHexInspector(hexInspector, (event: FeatureRotateEvent) => {
|
||||
if (event.newMask === 0) {
|
||||
hexMap.removeFeature(event.coord, event.terrainId);
|
||||
} else if (event.flipWater && event.terrainId === 'coastline') {
|
||||
// Flip water side on coastline
|
||||
const terrain = hexMap.getTerrain(event.coord);
|
||||
const feature = terrain.features.find(f => f.terrainId === 'coastline');
|
||||
if (feature) {
|
||||
const newSide: 1 | -1 = (feature.waterSide ?? 1) === 1 ? -1 : 1;
|
||||
hexMap.setFeature(event.coord, event.terrainId, event.newMask, newSide);
|
||||
}
|
||||
} else {
|
||||
hexMap.setFeature(event.coord, event.terrainId, event.newMask);
|
||||
// Re-enforce constraints after rotation
|
||||
// Rotate: preserve waterSide
|
||||
const terrain = hexMap.getTerrain(event.coord);
|
||||
const feature = terrain.features.find(f => f.terrainId === event.terrainId);
|
||||
const waterSide = feature?.waterSide;
|
||||
hexMap.setFeature(event.coord, event.terrainId, event.newMask, waterSide);
|
||||
const actions = enforceEdgeConstraints(hexMap, event.coord, event.terrainId, event.newMask);
|
||||
applyConstraintActions(hexMap, actions);
|
||||
}
|
||||
@@ -153,8 +164,9 @@ function handleFeaturePlacement(event: HexClickEvent) {
|
||||
if (!selectedPattern) return;
|
||||
const coord = event.coord;
|
||||
|
||||
// Place the selected pattern on this hex
|
||||
hexMap.setFeature(coord, selectedPattern.terrainId, selectedPattern.mask);
|
||||
// Place the selected pattern on this hex (with waterSide for coastline)
|
||||
const waterSide = selectedPattern.terrainId === 'coastline' ? selectedPattern.waterSide : undefined;
|
||||
hexMap.setFeature(coord, selectedPattern.terrainId, selectedPattern.mask, waterSide);
|
||||
|
||||
// Enforce edge constraints on neighbors
|
||||
const actions = enforceEdgeConstraints(hexMap, coord, selectedPattern.terrainId, selectedPattern.mask);
|
||||
@@ -187,6 +199,8 @@ document.addEventListener('keydown', (e) => {
|
||||
terrainPickerUI.rotateCCW();
|
||||
} else if (e.key === 'e' || e.key === 'E') {
|
||||
terrainPickerUI.rotateCW();
|
||||
} else if (e.key === 'f' || e.key === 'F') {
|
||||
terrainPickerUI.flipWaterSide();
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
@@ -273,3 +273,24 @@ html, body {
|
||||
background: #4a2020;
|
||||
border-color: #6a3030;
|
||||
}
|
||||
|
||||
.palette-controls {
|
||||
margin-top: 4px;
|
||||
}
|
||||
|
||||
.palette-flip-btn {
|
||||
width: 100%;
|
||||
padding: 4px 8px;
|
||||
margin-top: 4px;
|
||||
border: 1px solid #2a2a4a;
|
||||
border-radius: 4px;
|
||||
background: #1a1a2e;
|
||||
color: #ccc;
|
||||
cursor: pointer;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.palette-flip-btn:hover {
|
||||
background: #2a2a4a;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -249,13 +249,14 @@ function drawLinearFeature(
|
||||
edges: HexEdge[],
|
||||
color: string,
|
||||
size: number,
|
||||
waterSide: 1 | -1 = 1,
|
||||
): void {
|
||||
if (edges.length === 0) return;
|
||||
|
||||
const { cx, cy, edgeMidpoints, vertices } = geom;
|
||||
|
||||
if (terrainId === 'coastline') {
|
||||
drawCoastlineFeature(ctx, geom, edges, size);
|
||||
drawCoastlineFeature(ctx, geom, edges, waterSide, size);
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -375,14 +376,15 @@ function drawBezierRoute(
|
||||
}
|
||||
|
||||
/**
|
||||
* Coastline: routes edge-to-edge like road/river, but fills one side with water.
|
||||
* The water side is the side AWAY from the hex center (the "outside" of the curve).
|
||||
* We determine this using a cross-product test on the bezier midpoint.
|
||||
* Coastline: routes edge-to-edge, fills one side with water.
|
||||
* waterSide is stored on the feature: 1 = CW side, -1 = CCW side.
|
||||
* This is stable across rotations — no heuristic needed.
|
||||
*/
|
||||
function drawCoastlineFeature(
|
||||
ctx: CanvasRenderingContext2D,
|
||||
geom: HexGeometry,
|
||||
edges: HexEdge[],
|
||||
waterSide: 1 | -1,
|
||||
size: number,
|
||||
): void {
|
||||
const { cx, cy, edgeMidpoints, vertices } = geom;
|
||||
@@ -397,37 +399,13 @@ function drawCoastlineFeature(
|
||||
if (pair.length === 2) {
|
||||
const p1 = edgeMidpoints[pair[0]];
|
||||
const p2 = edgeMidpoints[pair[1]];
|
||||
|
||||
// Control point for the quadratic bezier (through center)
|
||||
const cp = { x: cx, y: cy };
|
||||
|
||||
// Determine which side of the curve is "away" from center
|
||||
// Sample the bezier midpoint, then offset perpendicular
|
||||
const bezMidX = 0.25 * p1.x + 0.5 * cp.x + 0.25 * p2.x;
|
||||
const bezMidY = 0.25 * p1.y + 0.5 * cp.y + 0.25 * p2.y;
|
||||
|
||||
// Tangent at midpoint (derivative of quadratic bezier at t=0.5)
|
||||
const tanX = (p2.x - p1.x);
|
||||
const tanY = (p2.y - p1.y);
|
||||
|
||||
// Perpendicular (rotated 90° CW)
|
||||
const perpX = tanY;
|
||||
const perpY = -tanX;
|
||||
|
||||
// The "water side" offset: we pick the side that is FARTHER from center
|
||||
const testX = bezMidX + perpX * 0.1;
|
||||
const testY = bezMidY + perpY * 0.1;
|
||||
const distFromCenter = Math.hypot(testX - cx, testY - cy);
|
||||
const distOther = Math.hypot(bezMidX - perpX * 0.1 - cx, bezMidY - perpY * 0.1 - cy);
|
||||
|
||||
const waterSide = distFromCenter > distOther ? 1 : -1;
|
||||
|
||||
// Fill the water side: build a path from the curve to the hex boundary on that side
|
||||
// Fill the water side using the explicit waterSide value
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.quadraticCurveTo(cp.x, cp.y, p2.x, p2.y);
|
||||
|
||||
// Walk hex vertices on the water side from p2's edge to p1's edge
|
||||
const waterVerts = getVerticesOnSide(pair[0], pair[1], waterSide, vertices);
|
||||
for (const v of waterVerts) {
|
||||
ctx.lineTo(v.x, v.y);
|
||||
@@ -445,15 +423,6 @@ function drawCoastlineFeature(
|
||||
ctx.strokeStyle = coastColor;
|
||||
ctx.lineWidth = Math.max(1.5, size / 10);
|
||||
ctx.stroke();
|
||||
} else {
|
||||
// Dead-end: just draw as a line to center
|
||||
const mp = edgeMidpoints[pair[0]];
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(mp.x, mp.y);
|
||||
ctx.lineTo(cx, cy);
|
||||
ctx.strokeStyle = coastColor;
|
||||
ctx.lineWidth = Math.max(1.5, size / 10);
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -548,7 +517,7 @@ export function renderHex(
|
||||
if (edges.length === 0) continue;
|
||||
|
||||
ctx.globalAlpha = 0.9;
|
||||
drawLinearFeature(ctx, geom, feature.terrainId, edges, type.color, size);
|
||||
drawLinearFeature(ctx, geom, feature.terrainId, edges, type.color, size, feature.waterSide ?? 1);
|
||||
ctx.globalAlpha = 1;
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,8 @@ export interface FeatureRotateEvent {
|
||||
coord: AxialCoord;
|
||||
terrainId: string;
|
||||
newMask: number;
|
||||
/** For coastline: flip water side */
|
||||
flipWater?: boolean;
|
||||
}
|
||||
|
||||
export function createHexInspector(
|
||||
@@ -97,6 +99,23 @@ export function createHexInspector(
|
||||
});
|
||||
});
|
||||
|
||||
// Flip water side button for coastlines
|
||||
if (feature.terrainId === 'coastline') {
|
||||
const flipBtn = document.createElement('button');
|
||||
flipBtn.className = 'rotate-btn';
|
||||
flipBtn.textContent = '\u21C4'; // ⇄
|
||||
flipBtn.title = 'Flip water side';
|
||||
flipBtn.addEventListener('click', () => {
|
||||
onRotateFeature({
|
||||
coord: currentCoord!,
|
||||
terrainId: feature.terrainId,
|
||||
newMask: feature.edgeMask,
|
||||
flipWater: true,
|
||||
});
|
||||
});
|
||||
controls.appendChild(flipBtn);
|
||||
}
|
||||
|
||||
const removeBtn = document.createElement('button');
|
||||
removeBtn.className = 'rotate-btn remove-btn';
|
||||
removeBtn.textContent = '\u2715'; // ✕
|
||||
|
||||
@@ -93,5 +93,6 @@ export function createTerrainPicker(
|
||||
getSelectedArea: () => selectedArea,
|
||||
rotateCW() { palette?.rotateCW(); },
|
||||
rotateCCW() { palette?.rotateCCW(); },
|
||||
flipWaterSide() { palette?.flipWaterSide(); },
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
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 { getLinearTerrains } from '../../core/terrain.js';
|
||||
import { getPatternsForTerrain, rotatePattern } from '../../core/tile-patterns.js';
|
||||
import { computeHexGeometry } from '../../core/coords.js';
|
||||
import { renderHex } from '../svg/renderer.js';
|
||||
|
||||
@@ -9,6 +9,8 @@ export interface SelectedPattern {
|
||||
mask: EdgeMask;
|
||||
rotation: number;
|
||||
patternId: string;
|
||||
/** For coastline: which side is water (1=CW, -1=CCW) */
|
||||
waterSide: 1 | -1;
|
||||
}
|
||||
|
||||
export function createTilePalette(
|
||||
@@ -18,9 +20,9 @@ export function createTilePalette(
|
||||
getSelected: () => SelectedPattern | null;
|
||||
rotateCW: () => void;
|
||||
rotateCCW: () => void;
|
||||
flipWaterSide: () => void;
|
||||
} {
|
||||
let selected: SelectedPattern | null = null;
|
||||
let currentTerrainId: string | null = null;
|
||||
|
||||
function render() {
|
||||
container.innerHTML = '';
|
||||
@@ -28,6 +30,8 @@ export function createTilePalette(
|
||||
const linearTerrains = getLinearTerrains();
|
||||
|
||||
for (const terrain of linearTerrains) {
|
||||
const patterns = getPatternsForTerrain(terrain.id);
|
||||
|
||||
// Terrain header
|
||||
const header = document.createElement('div');
|
||||
header.className = 'palette-terrain-header';
|
||||
@@ -38,7 +42,7 @@ export function createTilePalette(
|
||||
const grid = document.createElement('div');
|
||||
grid.className = 'palette-grid';
|
||||
|
||||
for (const pattern of TILE_PATTERNS) {
|
||||
for (const pattern of patterns) {
|
||||
const item = document.createElement('div');
|
||||
item.className = 'palette-item';
|
||||
|
||||
@@ -53,10 +57,14 @@ export function createTilePalette(
|
||||
canvas.height = previewSize * 2 + 4;
|
||||
|
||||
const mask = isSelected && selected
|
||||
? rotatePattern(pattern.baseMask, selected.rotation)
|
||||
? selected.mask
|
||||
: pattern.baseMask;
|
||||
const waterSide = isSelected && selected
|
||||
? selected.waterSide
|
||||
: 1;
|
||||
|
||||
renderPreview(canvas, terrain, mask, previewSize);
|
||||
renderPreview(canvas, terrain, mask, previewSize,
|
||||
terrain.id === 'coastline' ? waterSide : undefined);
|
||||
item.appendChild(canvas);
|
||||
|
||||
// Label
|
||||
@@ -67,17 +75,15 @@ export function createTilePalette(
|
||||
|
||||
item.addEventListener('click', () => {
|
||||
if (isSelected) {
|
||||
// Deselect
|
||||
selected = null;
|
||||
currentTerrainId = null;
|
||||
} else {
|
||||
selected = {
|
||||
terrainId: terrain.id,
|
||||
mask: pattern.baseMask,
|
||||
rotation: 0,
|
||||
patternId: pattern.id,
|
||||
waterSide: 1,
|
||||
};
|
||||
currentTerrainId = terrain.id;
|
||||
}
|
||||
onSelect(selected);
|
||||
render();
|
||||
@@ -89,33 +95,53 @@ export function createTilePalette(
|
||||
container.appendChild(grid);
|
||||
}
|
||||
|
||||
// Rotation hint if something is selected
|
||||
// Controls when something is selected
|
||||
if (selected) {
|
||||
const controls = document.createElement('div');
|
||||
controls.className = 'palette-controls';
|
||||
|
||||
const hint = document.createElement('div');
|
||||
hint.className = 'palette-hint';
|
||||
hint.textContent = 'Q/E or scroll to rotate before placing';
|
||||
container.appendChild(hint);
|
||||
hint.textContent = 'Q/E or scroll: rotate';
|
||||
controls.appendChild(hint);
|
||||
|
||||
if (selected.terrainId === 'coastline') {
|
||||
const flipBtn = document.createElement('button');
|
||||
flipBtn.className = 'palette-flip-btn';
|
||||
flipBtn.textContent = 'Flip water side (F)';
|
||||
flipBtn.addEventListener('click', () => flipWaterSide());
|
||||
controls.appendChild(flipBtn);
|
||||
}
|
||||
|
||||
container.appendChild(controls);
|
||||
}
|
||||
}
|
||||
|
||||
function getBaseMask(): EdgeMask {
|
||||
if (!selected) return 0;
|
||||
return getPatternsForTerrain(selected.terrainId)
|
||||
.find(p => p.id === selected!.patternId)!.baseMask;
|
||||
}
|
||||
|
||||
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,
|
||||
);
|
||||
selected.mask = rotatePattern(getBaseMask(), 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,
|
||||
);
|
||||
selected.rotation = (selected.rotation + 5) % 6;
|
||||
selected.mask = rotatePattern(getBaseMask(), selected.rotation);
|
||||
onSelect(selected);
|
||||
render();
|
||||
}
|
||||
|
||||
function flipWaterSide() {
|
||||
if (!selected || selected.terrainId !== 'coastline') return;
|
||||
selected.waterSide = selected.waterSide === 1 ? -1 : 1;
|
||||
onSelect(selected);
|
||||
render();
|
||||
}
|
||||
@@ -126,6 +152,7 @@ export function createTilePalette(
|
||||
getSelected: () => selected,
|
||||
rotateCW,
|
||||
rotateCCW,
|
||||
flipWaterSide,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -134,6 +161,7 @@ function renderPreview(
|
||||
terrain: TerrainType,
|
||||
mask: EdgeMask,
|
||||
size: number,
|
||||
waterSide?: 1 | -1,
|
||||
): void {
|
||||
const ctx = canvas.getContext('2d')!;
|
||||
const cx = canvas.width / 2;
|
||||
@@ -143,7 +171,11 @@ function renderPreview(
|
||||
|
||||
renderHex(ctx, geom, {
|
||||
base: 'plains',
|
||||
features: [{ terrainId: terrain.id, edgeMask: mask }],
|
||||
features: [{
|
||||
terrainId: terrain.id,
|
||||
edgeMask: mask,
|
||||
...(waterSide !== undefined ? { waterSide } : {}),
|
||||
}],
|
||||
}, {
|
||||
opacity: 0.9,
|
||||
showGrid: true,
|
||||
|
||||
Reference in New Issue
Block a user