231 lines
6.5 KiB
JavaScript
231 lines
6.5 KiB
JavaScript
import {useEffect, useRef, useState} from 'react';
|
|
|
|
import {useClient} from '@/context/client.js';
|
|
import {useEcs} from '@/context/ecs.js';
|
|
import useRect from '@/util/react-hooks/use-rect.js';
|
|
|
|
import styles from './tiles.module.css';
|
|
|
|
export default function Tiles({eventsChannel}) {
|
|
const client = useClient();
|
|
const wrapperRef = useRef();
|
|
const imageRef = useRef();
|
|
const imageRect = useRect(imageRef);
|
|
const [indices, setIndices] = useState([]);
|
|
const [selection, setSelection] = useState({x: 0, y: 0, w: 1, h: 1});
|
|
const [moveStart, setMoveStart] = useState();
|
|
const [layer, setLayer] = useState(0);
|
|
const [brush, setBrush] = useState(0);
|
|
const [stamp, setStamp] = useState([]);
|
|
const [ecs] = useEcs();
|
|
useEffect(() => {
|
|
if (!ecs) {
|
|
return false;
|
|
}
|
|
const master = ecs.get(1);
|
|
if (!master) {
|
|
return false;
|
|
}
|
|
const {TileLayers} = master;
|
|
const {area, tileSize} = TileLayers.layer(0);
|
|
function onClick({x, y}) {
|
|
const at = {
|
|
x: Math.floor(x / tileSize.x),
|
|
y: Math.floor(y / tileSize.y),
|
|
};
|
|
if (at.x < 0 || at.y < 0 || at.x >= area.x || at.y >= area.y) {
|
|
return;
|
|
}
|
|
client.send({
|
|
type: 'AdminAction',
|
|
payload: {
|
|
type: 'paint',
|
|
value: {
|
|
brush,
|
|
layer,
|
|
stamp: {
|
|
at,
|
|
data: stamp,
|
|
},
|
|
},
|
|
},
|
|
});
|
|
}
|
|
eventsChannel.addListener('click', onClick);
|
|
return () => {
|
|
eventsChannel.removeListener('click', onClick);
|
|
};
|
|
});
|
|
if (!ecs) {
|
|
return false;
|
|
}
|
|
const master = ecs.get(1);
|
|
if (!master) {
|
|
return false;
|
|
}
|
|
const {TileLayers} = master;
|
|
const {sourceJson, tileSize} = TileLayers.layer(0);
|
|
const {w, h} = sourceJson.meta.size;
|
|
const factor = (imageRect?.width ?? 1) / w;
|
|
const wt = w / tileSize.x;
|
|
return (
|
|
<div className={styles.tiles}>
|
|
<form>
|
|
<div className={styles.paintBar}>
|
|
<div className={styles.layer}>
|
|
<label>
|
|
<span>Layer</span>
|
|
<select
|
|
onChange={(event) => {
|
|
setLayer(event.target.value)
|
|
}}
|
|
value={layer}
|
|
>
|
|
{
|
|
Object.keys(TileLayers.layers)
|
|
.map((layer, i) => (
|
|
<option
|
|
key={i}
|
|
value={i}
|
|
>
|
|
{i}
|
|
</option>
|
|
))
|
|
}
|
|
</select>
|
|
</label>
|
|
</div>
|
|
<div className={styles.brush}>
|
|
<label>
|
|
<span>Brush</span>
|
|
<select
|
|
onChange={(event) => {
|
|
setBrush(event.target.value)
|
|
}}
|
|
value={brush}
|
|
>
|
|
<option>Paint</option>
|
|
</select>
|
|
</label>
|
|
</div>
|
|
</div>
|
|
</form>
|
|
{/* eslint-disable-next-line jsx-a11y/no-static-element-interactions */}
|
|
<div
|
|
onMouseDown={(event) => {
|
|
const wrapperRect = wrapperRef.current.getBoundingClientRect();
|
|
const {left, top} = wrapperRect;
|
|
const c = {
|
|
x: event.clientX,
|
|
y: event.clientY + wrapperRef.current.scrollTop,
|
|
};
|
|
if (
|
|
c.x - left >= (w * factor)
|
|
|| c.y - top >= (h * factor)
|
|
) {
|
|
return;
|
|
}
|
|
const x = Math.floor((c.x - left) / (tileSize.x * factor));
|
|
const y = Math.floor((c.y - top) / (tileSize.y * factor));
|
|
setMoveStart({x, y});
|
|
setSelection({x, y, w: 1, h: 1});
|
|
setStamp([[y * wt + x]]);
|
|
setIndices([y * wt + x]);
|
|
}}
|
|
onMouseMove={(event) => {
|
|
if (0 === event.buttons) {
|
|
setMoveStart();
|
|
return;
|
|
}
|
|
if (!moveStart) {
|
|
return;
|
|
}
|
|
const {x: sx, y: sy} = moveStart;
|
|
const wrapperRect = wrapperRef.current.getBoundingClientRect();
|
|
const {left, top} = wrapperRect;
|
|
const c = {
|
|
x: event.clientX,
|
|
y: event.clientY + wrapperRef.current.scrollTop,
|
|
};
|
|
const x = Math.floor(
|
|
Math.max(0, Math.min((w * factor) - 1, (c.x - left)) / (tileSize.x * factor)),
|
|
);
|
|
const y = Math.floor(
|
|
Math.max(0, Math.min((h * factor) - 1, (c.y - top)) / (tileSize.y * factor)),
|
|
);
|
|
const mx = Math.min(sx, x);
|
|
const my = Math.min(sy, y);
|
|
const sw = Math.abs(sx - x) + 1;
|
|
const sh = Math.abs(sy - y) + 1;
|
|
const newSelection = {
|
|
x: mx,
|
|
y: my,
|
|
w: sw,
|
|
h: sh,
|
|
};
|
|
if (
|
|
selection.x === newSelection.x
|
|
&& selection.y === newSelection.y
|
|
&& selection.w === newSelection.w
|
|
&& selection.h === newSelection.h
|
|
) {
|
|
return;
|
|
}
|
|
setSelection(newSelection);
|
|
const stamp = [];
|
|
for (let iy = 0; iy < sh; ++iy) {
|
|
const row = [];
|
|
for (let ix = 0; ix < sw; ++ix) {
|
|
row.push((my + iy) * wt + mx + ix);
|
|
}
|
|
stamp.push(row);
|
|
}
|
|
setStamp(stamp);
|
|
const indices = [];
|
|
for (let sy = 0; sy < newSelection.h; ++sy) {
|
|
for (let sx = 0; sx < newSelection.w; ++sx) {
|
|
indices.push(((newSelection.y + sy) * wt) + (newSelection.x + sx));
|
|
}
|
|
}
|
|
setIndices(indices);
|
|
}}
|
|
className={styles.selectionWrapper}
|
|
ref={wrapperRef}
|
|
>
|
|
<div
|
|
className={styles.selection}
|
|
style={{
|
|
top: selection.y * tileSize.y * factor,
|
|
left: selection.x * tileSize.x * factor,
|
|
height: selection.h * tileSize.x * factor,
|
|
width: selection.w * tileSize.y * factor,
|
|
}}
|
|
/>
|
|
<img
|
|
alt="tileset"
|
|
ref={imageRef}
|
|
src={TileLayers.layer(0).source.replace('.json', '.png')}
|
|
/>
|
|
</div>
|
|
<div
|
|
className={styles.status}
|
|
>
|
|
<code className={styles.selectionStatus}>
|
|
Sel:
|
|
{' {'}
|
|
{selection.x}{', '}
|
|
{selection.y}{', '}
|
|
{selection.w}{', '}
|
|
{selection.h}
|
|
{'}'}
|
|
</code>
|
|
<code>
|
|
Idx:
|
|
{' ['}
|
|
{indices.join(', ')}
|
|
{']'}
|
|
</code>
|
|
</div>
|
|
</div>
|
|
);
|
|
} |