diff --git a/docs/development/documentation.md b/docs/development/documentation.md index ecc8543c..21532f2b 100644 --- a/docs/development/documentation.md +++ b/docs/development/documentation.md @@ -57,6 +57,18 @@ Rules for the generated files: pull request. - A change to the menu layout needs no code change here: run `./gradlew generateDocsImages` and commit the menu SVGs. +- The blocks are drawn in Minecraft's colours and patterns, but no Minecraft texture is used. Each material has a + representative colour and an 8x8 pixel tile that the generator computes from the material name (`BlockLooks`); + pixel icons for menu items that are not cubes are shapes written in code (`ItemIcons`). The generator never + downloads, copies, embeds or reads a Minecraft texture, a resource pack or any other third-party texture file, + because the licence of those files does not allow it. Do not add one to `src/docsGen` or to the pictures. +- A new block material needs an entry in `BlockPalette` (the fixture it stands for) and in `BlockLooks`; a new + non-cube menu item needs an icon in `ItemIcons`. Until then the generator fails and names the material. +- Reading the brush pictures: a block the brush changed is drawn at full colour with a white border, a block it did + not change is faded, air is the light blue sky, and the clicked block is outlined. The legend names every material + by its Minecraft name. The paint brush numbers its clicked points in click order and joins them with a line. In the + menu, a numbered badge marks each functional slot and the legend gives the number, the slot and the English text; + filler panes have no badge. - The generator reads no clock and no network. Fixed terrain comes from the test fixtures (`MemoryExtent`, `StrokeBuilder`, `Terrain`); the generator contains no copy of a brush algorithm. - Every builder page needs an image or a link to the screenshot shotlist; `DocsLinkChecker` fails the diff --git a/docs/images/brushes/angle-brush.svg b/docs/images/brushes/angle-brush.svg index a2fdc0cd..adeb6457 100644 --- a/docs/images/brushes/angle-brush.svg +++ b/docs/images/brushes/angle-brush.svg @@ -1,124 +1,921 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Angle Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - gold block - - clicked block + + Grass Block + + Dirt + + Stone + + Gold Block + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/brushes/bucket-brush.svg b/docs/images/brushes/bucket-brush.svg index 1e49afd9..44691857 100644 --- a/docs/images/brushes/bucket-brush.svg +++ b/docs/images/brushes/bucket-brush.svg @@ -1,142 +1,831 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Bucket Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - gold block - - clicked block + + Grass Block + + Dirt + + Gold Block + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/brushes/disc-brush.svg b/docs/images/brushes/disc-brush.svg index 615afdaa..602d3581 100644 --- a/docs/images/brushes/disc-brush.svg +++ b/docs/images/brushes/disc-brush.svg @@ -1,96 +1,855 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Disc Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - gold block - - clicked block + + Grass Block + + Dirt + + Stone + + Gold Block + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/brushes/fracture-brush.svg b/docs/images/brushes/fracture-brush.svg index 3986b63b..6c7023a2 100644 --- a/docs/images/brushes/fracture-brush.svg +++ b/docs/images/brushes/fracture-brush.svg @@ -1,96 +1,855 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Fracture Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - gold block - - clicked block + + Grass Block + + Dirt + + Stone + + Gold Block + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/brushes/gradient-brush.svg b/docs/images/brushes/gradient-brush.svg index 7bd8512f..79462e12 100644 --- a/docs/images/brushes/gradient-brush.svg +++ b/docs/images/brushes/gradient-brush.svg @@ -1,162 +1,1261 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Gradient Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - stone block - - andesite block - - clicked block + + + Grass Block + + Dirt + + Stone + + Andesite + + Cobblestone + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/brushes/overlay-brush.svg b/docs/images/brushes/overlay-brush.svg index 3577813b..bd42fa05 100644 --- a/docs/images/brushes/overlay-brush.svg +++ b/docs/images/brushes/overlay-brush.svg @@ -1,151 +1,854 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Overlay Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - gold block - - clicked block + + Grass Block + + Dirt + + Gold Block + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/brushes/paint-brush.svg b/docs/images/brushes/paint-brush.svg index 6ee34ccf..715ce4ba 100644 --- a/docs/images/brushes/paint-brush.svg +++ b/docs/images/brushes/paint-brush.svg @@ -1,496 +1,1175 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Paint Brush Top view - Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - gold block - - clicked block + Side cross-section + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + Grass Block + + Dirt + + Gold Block + + air + + + changed by the brush + + unchanged (faded) + + clicked block + + numbered: click order diff --git a/docs/images/brushes/sphere-brush.svg b/docs/images/brushes/sphere-brush.svg index a396bea3..a3c7affb 100644 --- a/docs/images/brushes/sphere-brush.svg +++ b/docs/images/brushes/sphere-brush.svg @@ -1,164 +1,1275 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Sphere Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - stone block - - andesite block - - gold block - - clicked block + + Grass Block + + Dirt + + Stone + + Andesite + + Cobblestone + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/brushes/splatter-brush.svg b/docs/images/brushes/splatter-brush.svg index c94d66f8..857cc70c 100644 --- a/docs/images/brushes/splatter-brush.svg +++ b/docs/images/brushes/splatter-brush.svg @@ -1,160 +1,1009 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Splatter Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - gold block - - clicked block + + Grass Block + + Dirt + + Stone + + Gold Block + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/brushes/spray-brush.svg b/docs/images/brushes/spray-brush.svg index 4b967649..a8ba3f92 100644 --- a/docs/images/brushes/spray-brush.svg +++ b/docs/images/brushes/spray-brush.svg @@ -1,160 +1,997 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Spray Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - gold block - - clicked block + + Grass Block + + Dirt + + Stone + + Gold Block + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/brushes/underlay-brush.svg b/docs/images/brushes/underlay-brush.svg index 22bf7f16..c8efad86 100644 --- a/docs/images/brushes/underlay-brush.svg +++ b/docs/images/brushes/underlay-brush.svg @@ -1,114 +1,895 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Underlay Brush Top view Side cross-section - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - unchanged block - - air - - gold block - - clicked block + + Grass Block + + Dirt + + Stone + + Gold Block + + air + + + changed by the brush + + unchanged (faded) + + clicked block diff --git a/docs/images/menu/menu-angle.svg b/docs/images/menu/menu-angle.svg index cc881ccf..1460bcdc 100644 --- a/docs/images/menu/menu-angle.svg +++ b/docs/images/menu/menu-angle.svg @@ -1,265 +1,719 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Brush menu: Angle Brush, surface disabled - - Slot 0 - gray stained glass - pane - filler - - Slot 1 - lime stained glass - pane - filler - - Slot 2 - orange stained - glass pane - filler - - Slot 3 - white stained - glass pane - filler - - Slot 4 - white stained - glass pane - filler - - Slot 5 - white stained - glass pane - filler - - Slot 6 - lime stained glass - pane - filler - - Slot 7 - red stained glass - pane - filler - - Slot 8 - gray stained glass - pane - filler - - Slot 9 - gray stained glass - pane - filler - - Slot 10 - feather - goPaint Brush - - Slot 11 - player head - Selected Brush - type - - Slot 12 - daylight detector - Angle Check - Distance: 2 - - Slot 13 - blaze rod - Maximum Angle: - 40.0° - - Slot 14 - brown mushroom - Brush Size: 10 - - Slot 15 - jack o lantern - Mask - - Slot 16 - light weighted - pressure plate - Surface Mode - - Slot 17 - gray stained glass - pane - filler - - Slot 18 - gray stained glass - pane - filler - - Slot 19 - lime stained glass - pane - filler - - Slot 20 - orange stained - glass pane - filler - - Slot 21 - white stained - glass pane - filler - - Slot 22 - white stained - glass pane - filler - - Slot 23 - white stained - glass pane - filler - - Slot 24 - lime stained glass - pane - filler - - Slot 25 - red stained glass - pane - filler - - Slot 26 - gray stained glass - pane - filler - - Slot 27 - gray stained glass - pane - filler - - Slot 28 - gray stained glass - pane - filler - - Slot 29 - gray stained glass - pane - filler - - Slot 30 - gray stained glass - pane - filler - - Slot 31 - gray stained glass - pane - filler - - Slot 32 - gray stained glass - pane - filler - - Slot 33 - gray stained glass - pane - filler - - Slot 34 - gray stained glass - pane - filler - - Slot 35 - gray stained glass - pane - filler - - Slot 36 - gray stained glass - pane - filler - - Slot 37 - yellow stained - glass pane - filler - - Slot 38 - yellow stained - glass pane - filler - - Slot 39 - yellow stained - glass pane - filler - - Slot 40 - yellow stained - glass pane - filler - - Slot 41 - yellow stained - glass pane - filler - - Slot 42 - gray stained glass - pane - filler - - Slot 43 - yellow stained - glass pane - filler - - Slot 44 - gray stained glass - pane - filler - - Slot 45 - gray stained glass - pane - filler - - Slot 46 - stone - Slot 1 100% - - Slot 47 - barrier - Empty Slot - - Slot 48 - barrier - Empty Slot - - Slot 49 - barrier - Empty Slot - - Slot 50 - barrier - Empty Slot - - Slot 51 - gray stained glass - pane - filler - - Slot 52 - sponge - Current Mask - - Slot 53 - gray stained glass - pane - filler + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 + + 10 + + 11 + + 12 + + 13 + + 1 + Slot 10: goPaint Brush + + 2 + Slot 11: Selected Brush type + + 3 + Slot 12: Angle Check Distance: 2 + + 4 + Slot 13: Maximum Angle: 40.0° + + 5 + Slot 14: Brush Size: 10 + + 6 + Slot 15: Mask + + 7 + Slot 16: Surface Mode + + 8 + Slot 46: Slot 1 100% + + 9 + Slot 47: Empty Slot + + 10 + Slot 48: Empty Slot + + 11 + Slot 49: Empty Slot + + 12 + Slot 50: Empty Slot + + 13 + Slot 52: Current Mask diff --git a/docs/images/menu/menu-sphere-default.svg b/docs/images/menu/menu-sphere-default.svg index dd801716..5bfd0668 100644 --- a/docs/images/menu/menu-sphere-default.svg +++ b/docs/images/menu/menu-sphere-default.svg @@ -1,265 +1,641 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Brush menu: Sphere Brush, default settings - - Slot 0 - gray stained glass - pane - filler - - Slot 1 - lime stained glass - pane - filler - - Slot 2 - orange stained - glass pane - filler - - Slot 3 - gray stained glass - pane - filler - - Slot 4 - gray stained glass - pane - filler - - Slot 5 - white stained - glass pane - filler - - Slot 6 - lime stained glass - pane - filler - - Slot 7 - lime stained glass - pane - filler - - Slot 8 - gray stained glass - pane - filler - - Slot 9 - gray stained glass - pane - filler - - Slot 10 - feather - goPaint Brush - - Slot 11 - player head - Selected Brush - type - - Slot 12 - gray stained glass - pane - filler - - Slot 13 - gray stained glass - pane - filler - - Slot 14 - brown mushroom - Brush Size: 10 - - Slot 15 - jack o lantern - Mask - - Slot 16 - light weighted - pressure plate - Surface Mode - - Slot 17 - gray stained glass - pane - filler - - Slot 18 - gray stained glass - pane - filler - - Slot 19 - lime stained glass - pane - filler - - Slot 20 - orange stained - glass pane - filler - - Slot 21 - gray stained glass - pane - filler - - Slot 22 - gray stained glass - pane - filler - - Slot 23 - white stained - glass pane - filler - - Slot 24 - lime stained glass - pane - filler - - Slot 25 - lime stained glass - pane - filler - - Slot 26 - gray stained glass - pane - filler - - Slot 27 - gray stained glass - pane - filler - - Slot 28 - gray stained glass - pane - filler - - Slot 29 - gray stained glass - pane - filler - - Slot 30 - gray stained glass - pane - filler - - Slot 31 - gray stained glass - pane - filler - - Slot 32 - gray stained glass - pane - filler - - Slot 33 - gray stained glass - pane - filler - - Slot 34 - gray stained glass - pane - filler - - Slot 35 - gray stained glass - pane - filler - - Slot 36 - gray stained glass - pane - filler - - Slot 37 - yellow stained - glass pane - filler - - Slot 38 - yellow stained - glass pane - filler - - Slot 39 - yellow stained - glass pane - filler - - Slot 40 - yellow stained - glass pane - filler - - Slot 41 - yellow stained - glass pane - filler - - Slot 42 - gray stained glass - pane - filler - - Slot 43 - yellow stained - glass pane - filler - - Slot 44 - gray stained glass - pane - filler - - Slot 45 - gray stained glass - pane - filler - - Slot 46 - stone - Slot 1 100% - - Slot 47 - barrier - Empty Slot - - Slot 48 - barrier - Empty Slot - - Slot 49 - barrier - Empty Slot - - Slot 50 - barrier - Empty Slot - - Slot 51 - gray stained glass - pane - filler - - Slot 52 - sponge - Current Mask - - Slot 53 - gray stained glass - pane - filler + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 + + 10 + + 11 + + 1 + Slot 10: goPaint Brush + + 2 + Slot 11: Selected Brush type + + 3 + Slot 14: Brush Size: 10 + + 4 + Slot 15: Mask + + 5 + Slot 16: Surface Mode + + 6 + Slot 46: Slot 1 100% + + 7 + Slot 47: Empty Slot + + 8 + Slot 48: Empty Slot + + 9 + Slot 49: Empty Slot + + 10 + Slot 50: Empty Slot + + 11 + Slot 52: Current Mask diff --git a/docs/images/menu/menu-spray-disabled.svg b/docs/images/menu/menu-spray-disabled.svg index cc81f878..a8a83e59 100644 --- a/docs/images/menu/menu-spray-disabled.svg +++ b/docs/images/menu/menu-spray-disabled.svg @@ -1,264 +1,744 @@ - - + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + Brush menu: Spray Brush, disabled, two palette blocks, dirt mask - - Slot 0 - gray stained glass - pane - filler - - Slot 1 - red stained glass - pane - filler - - Slot 2 - orange stained - glass pane - filler - - Slot 3 - white stained - glass pane - filler - - Slot 4 - gray stained glass - pane - filler - - Slot 5 - white stained - glass pane - filler - - Slot 6 - red stained glass - pane - filler - - Slot 7 - orange stained - glass pane - filler - - Slot 8 - gray stained glass - pane - filler - - Slot 9 - gray stained glass - pane - filler - - Slot 10 - feather - goPaint Brush - - Slot 11 - player head - Selected Brush - type - - Slot 12 - gold nugget - Place chance: 50% - - Slot 13 - gray stained glass - pane - filler - - Slot 14 - brown mushroom - Brush Size: 10 - - Slot 15 - carved pumpkin - Mask - - Slot 16 - light weighted - pressure plate - Surface Mode - - Slot 17 - gray stained glass - pane - filler - - Slot 18 - gray stained glass - pane - filler - - Slot 19 - red stained glass - pane - filler - - Slot 20 - orange stained - glass pane - filler - - Slot 21 - white stained - glass pane - filler - - Slot 22 - gray stained glass - pane - filler - - Slot 23 - white stained - glass pane - filler - - Slot 24 - red stained glass - pane - filler - - Slot 25 - orange stained - glass pane - filler - - Slot 26 - gray stained glass - pane - filler - - Slot 27 - gray stained glass - pane - filler - - Slot 28 - gray stained glass - pane - filler - - Slot 29 - gray stained glass - pane - filler - - Slot 30 - gray stained glass - pane - filler - - Slot 31 - gray stained glass - pane - filler - - Slot 32 - gray stained glass - pane - filler - - Slot 33 - gray stained glass - pane - filler - - Slot 34 - gray stained glass - pane - filler - - Slot 35 - gray stained glass - pane - filler - - Slot 36 - gray stained glass - pane - filler - - Slot 37 - yellow stained - glass pane - filler - - Slot 38 - yellow stained - glass pane - filler - - Slot 39 - yellow stained - glass pane - filler - - Slot 40 - yellow stained - glass pane - filler - - Slot 41 - yellow stained - glass pane - filler - - Slot 42 - gray stained glass - pane - filler - - Slot 43 - yellow stained - glass pane - filler - - Slot 44 - gray stained glass - pane - filler - - Slot 45 - gray stained glass - pane - filler - - Slot 46 - stone - Slot 1 50% - - Slot 47 - grass block - Slot 2 50% - - Slot 48 - barrier - Empty Slot - - Slot 49 - barrier - Empty Slot - - Slot 50 - barrier - Empty Slot - - Slot 51 - gray stained glass - pane - filler - - Slot 52 - dirt - Current Mask - - Slot 53 - gray stained glass - pane - filler + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + 1 + + 2 + + 3 + + 4 + + 5 + + 6 + + 7 + + 8 + + 9 + + 10 + + 11 + + 12 + + 1 + Slot 10: goPaint Brush + + 2 + Slot 11: Selected Brush type + + 3 + Slot 12: Place chance: 50% + + 4 + Slot 14: Brush Size: 10 + + 5 + Slot 15: Mask + + 6 + Slot 16: Surface Mode + + 7 + Slot 46: Slot 1 50% + + 8 + Slot 47: Slot 2 50% + + 9 + Slot 48: Empty Slot + + 10 + Slot 49: Empty Slot + + 11 + Slot 50: Empty Slot + + 12 + Slot 52: Current Mask diff --git a/openspec/changes/minecraft-style-illustrations/.openspec.yaml b/openspec/changes/minecraft-style-illustrations/.openspec.yaml new file mode 100644 index 00000000..53f62d70 --- /dev/null +++ b/openspec/changes/minecraft-style-illustrations/.openspec.yaml @@ -0,0 +1,2 @@ +schema: spec-driven +created: 2026-10-10 diff --git a/openspec/changes/minecraft-style-illustrations/design.md b/openspec/changes/minecraft-style-illustrations/design.md new file mode 100644 index 00000000..2d1a5181 --- /dev/null +++ b/openspec/changes/minecraft-style-illustrations/design.md @@ -0,0 +1,56 @@ +# Design + +## Tiles as shared definitions + +Every block face and every item icon is an 8x8 pixel tile. The SVG writes each tile once, as a `` with +one `` per pixel in ``, and draws each cell as one `` with an integer position. A +picture with a few hundred cells then stays at a few hundred kilobytes, and no pattern tile has to align with +the grid. The tiles are sorted by id, so the output does not depend on the order in which cells are drawn. + +## Procedural pattern + +The shade of each pixel is the base colour of the material, lightened or darkened by a fixed step. A +`java.util.Random` seeded from the material name chooses the step of each pixel, and the probabilities of +light and dark pixels are fixed per material. The same name always gives the same tile, on every operating +system, and two materials give different tiles. The grass block side is a special case: two rows of grass +green, one jagged row, then dirt. + +Base colours are representative averages of the Minecraft block faces, chosen by hand: + +| Material | Base colour | +|---|---| +| `GRASS_BLOCK` (top) | `#7CBD6B` | +| `DIRT` (and the grass side below the strip) | `#866043` | +| `STONE` | `#7D7D7D` | +| `ANDESITE` | `#8A8B8C` | +| `COBBLESTONE` | `#6E6E6E` | +| `GOLD_BLOCK` | `#F9D849` | +| `SPONGE` | `#C3C04A` | + +No two materials share a base colour; a unit test checks it. + +## Block drawing + +- Top view: a column shows its highest changed block at full opacity with a highlight border. A column the + brush did not change shows its top block, faded. +- Side cross-section: a cell shows the side tile of its block (the grass side has the green strip on top); + changed blocks are at full opacity, unchanged blocks faded, air in the sky tint. +- Clicked block: a two-pixel dark outline on the cell. +- Paint brush: the control points are numbered in click order, connected by a polyline. The brush size is + the radius of the ball the stroke is swept through, so a size of 1 keeps the stroke thin. + +## Scenes + +Every scene stands on grass over three dirt blocks over stone. The scenes that need a palette mix three +stone-family blocks. The paint scene has six control points on the top layer, with the last one as the +target. + +## Menu + +The menu is drawn as the Minecraft 6x9 chest container at twice the inventory scale: 36 px per slot with a +2 px bevel, a 14 px light grey border, and the slot grid of `GUI.layout()` unchanged. Block items are block +tiles, glass panes are tinted translucent tiles, and other items are 8x8 pixel icons written in code. +A numbered badge marks each functional slot, and a legend beside the chest maps numbers to the English +label and the slot number. Filler panes have no badge. + +Unknown items fail the generator and name the material, as an unknown block does today. diff --git a/openspec/changes/minecraft-style-illustrations/proposal.md b/openspec/changes/minecraft-style-illustrations/proposal.md new file mode 100644 index 00000000..df210389 --- /dev/null +++ b/openspec/changes/minecraft-style-illustrations/proposal.md @@ -0,0 +1,62 @@ +# Proposal + +## Why + +Maintainer review of the generated illustrations found that they do not look like Minecraft. Andesite is +teal, stone is nearly black, every block the brush did not change is one flat grey, so builders cannot see +the grass and dirt they are painting on. The paint stroke is a solid blob with no visible path, and the +brush menu is a grid of text cells that does not resemble the inventory a builder sees. The 2D layout (top +view, side cross-section, legend; the 54-slot grid) is liked and stays. + +## What Changes + +- Colour every block with the representative colour of its Minecraft material (grass block, dirt, stone, + andesite, cobblestone, gold block, sponge). The generator maps each test fixture to its real material name + and no longer colours blocks by object identity, so the legend names the block ("Grass Block", "Gold Block"). +- Draw every block face as a deterministic 8x8 pixel tile that the generator computes from the material name + with a fixed seed: shades of the material's base colour. No Minecraft or third-party texture file is + downloaded, copied, embedded or read. +- Draw blocks the brush did not change with their material tile at reduced opacity, blocks the brush changed + at full opacity with a thin highlight border, air in a sky tint (`#dbeafe`), and the clicked block outlined. +- Build the brush scenes on grass over dirt over stone. The sphere and gradient scenes mix stone, andesite and + cobblestone; the paint scene uses a thin brush so the stroke reads as a path through its six clicked points, + which are numbered 1 to 6 in click order. +- Draw the brush menu as a Minecraft 6x9 chest container: a light grey panel, bevelled inset slots, glass panes + as tinted translucent tiles, block items as block tiles, and the other items as small pixel icons defined in + code. Numbered callouts sit on functional slots only, never on filler panes, and a legend gives number to + English label (from `bettergopaint.properties`) with the slot number, because the pages refer to slots. +- Regenerate the eleven brush and three menu SVGs. The existing tasks are unchanged: `./gradlew + generateDocsImages` writes them, `./gradlew checkDocsImages` and `./gradlew docsGenTest` run inside `check`. +- Document the procedural tiles and the rule against third-party or Mojang texture files in + `docs/development/documentation.md`. + +Commit type and scope: `build(docs)` for the generator, the tests and the images; `docs(development)` for the +documentation page; `docs(openspec)` for this proposal. Pull request title: `build(docs): draw illustrations +with minecraft style blocks`. + +Not breaking: no command, permission, config key, brush, menu layout or supported version changes, so no +server owner has to act. The plugin jar does not change; the generator stays in the `docsGen` source set. + +## Capabilities + +### Modified Capabilities +- `docs-illustrations`: material colours, procedural block tiles without third-party assets, legend names, + unchanged blocks shown with their material, paint control points, and the chest-style menu with callouts. + +## Impact + +- `src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/**`: block looks and tiles, item icons, scene + terrain and paint stroke, brush and menu rendering, SVG model with definitions, opacity, paths and circles. +- `src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/**`: unit tests for each of the above. +- `src/test/java/net/onelitefeather/bettergopaint/support/Terrain.java`: one more block fixture + (`cobblestone`), used only by the generator. +- `docs/images/brushes/*.svg` (11 files) and `docs/images/menu/*.svg` (3 files), regenerated. +- `docs/development/documentation.md`: the Illustrations section. + +## Out of scope + +- Any Minecraft texture, resource pack or third-party texture, in any form. The tiles are computed. +- Changing brush behaviour or the menu layout. `GUI.layout()` and the brush code are read as they are. +- Rewriting the user pages. Their captions and alt texts stay; only a caption that would become wrong changes. +- PNG or other raster output, animation and interactive pictures. +- Archiving this change. It is archived after the pull request is reviewed. diff --git a/openspec/changes/minecraft-style-illustrations/specs/docs-illustrations/spec.md b/openspec/changes/minecraft-style-illustrations/specs/docs-illustrations/spec.md new file mode 100644 index 00000000..40d90aa5 --- /dev/null +++ b/openspec/changes/minecraft-style-illustrations/specs/docs-illustrations/spec.md @@ -0,0 +1,90 @@ +## ADDED Requirements + +### Requirement: Blocks are drawn in their Minecraft material colours +The generator SHALL draw every block with the representative colour of its Minecraft material: grass block +top `#7CBD6B`, dirt `#866043`, stone `#7D7D7D`, andesite `#8A8B8C`, cobblestone `#6E6E6E`, gold block +`#F9D849`, sponge `#C3C04A`, and any other material the scenes or menus use in the same way. Two different +materials SHALL NOT share a base colour. The generator SHALL map each test fixture block to its real material +name and SHALL NOT colour blocks by object identity. + +#### Scenario: Two materials share a colour +- **WHEN** a material is added to the palette with the base colour of another material +- **THEN** `./gradlew docsGenTest` fails and names both materials + +#### Scenario: Unchanged grass +- **WHEN** `./gradlew generateDocsImages` renders the sphere brush +- **THEN** a grass block the brush did not change is drawn with the grass block tile, not with a grey fill + +### Requirement: Block tiles are computed without third-party assets +Every block face and item icon SHALL be an 8x8 pixel tile that the generator computes from the material name +with a fixed seed, using shades of the material's base colour. The generator SHALL NOT download, copy, embed or +read any Minecraft texture, resource pack or other third-party texture file. The same material name SHALL give +the same tile on every run, and two materials SHALL give different tiles. + +#### Scenario: Tile of the same material twice +- **WHEN** the tile of `STONE` is computed twice in one run or in two runs +- **THEN** both tiles have the same 64 pixel colours + +#### Scenario: Texture files in the sources +- **WHEN** the sources of `src/docsGen` are searched for image or texture file references +- **THEN** none is found, because the tiles are computed in code + +### Requirement: Unchanged blocks show their material and changed blocks stand out +Blocks the brush did not change SHALL be drawn with their material tile at reduced opacity, so builders see +the grass, dirt and stone they paint on. Blocks the brush changed SHALL be drawn at full opacity with a thin +highlight border. Air SHALL be drawn in the sky tint `#dbeafe`. The clicked block SHALL be outlined. + +#### Scenario: Changed block next to unchanged block +- **WHEN** the sphere brush changes a block next to an unchanged block in the top view +- **THEN** the changed block is drawn at full opacity with a highlight border and the unchanged block is drawn faded + +#### Scenario: Air +- **WHEN** a cell of the picture is air +- **THEN** the cell is filled with `#dbeafe` + +### Requirement: Legend names the materials of the picture +The legend of a brush illustration SHALL list every material shown in the picture by its Minecraft name, for +example "Grass Block", "Dirt", "Gold Block", and SHALL list the markers "changed by the brush", "unchanged +(faded)", "air" and "clicked block". The paint brush legend SHALL also list the numbered control points. + +#### Scenario: Sphere legend +- **WHEN** `./gradlew generateDocsImages` renders the sphere brush +- **THEN** the legend of `docs/images/brushes/sphere-brush.svg` contains the text "Grass Block" + +### Requirement: Paint brush shows its control points and path +The paint brush illustration SHALL number each clicked control point in click order, starting at 1, SHALL +connect the points with the path of the stroke, and SHALL paint blocks along a thin path rather than a solid +area, so that the curve is visible. + +#### Scenario: Control points +- **WHEN** `./gradlew generateDocsImages` renders the paint brush +- **THEN** `docs/images/brushes/paint-brush.svg` contains the numbers 1 to 6, one for each control point + +#### Scenario: Stroke is a path +- **WHEN** `./gradlew docsGenTest` renders the paint scene +- **THEN** the painted blocks fill less than half of their bounding box in the top view; a stroke that fills more fails the test + +## MODIFIED Requirements + +### Requirement: Menu map matches the layout +The generator SHALL render the 54 slots of `GUI.layout()` for three brush states into `docs/images/menu/`, as a +Minecraft 6x9 chest container: a light grey panel, bevelled inset slots, glass panes as tinted translucent +tiles, block items as block tiles and other items as pixel icons defined in code. A numbered callout SHALL mark +each functional slot and SHALL NOT mark a filler pane. A legend SHALL map each callout number to the English +text of the slot's translation key from `bettergopaint.properties` and to the slot number. + +#### Scenario: Slot moved +- **WHEN** a pull request moves the mask toggle from slot 6 to another slot +- **THEN** `./gradlew checkDocsImages` fails on the menu images until they are regenerated + +#### Scenario: Missing translation +- **WHEN** a menu slot uses a key that `bettergopaint.properties` does not define +- **THEN** the generator fails and names the key instead of writing the raw key into the image + +#### Scenario: Filler panes have no callout +- **WHEN** the menu of the sphere brush is rendered +- **THEN** the callouts are on the functional slots only, one per slot, and no callout is on a filler pane + +#### Scenario: Item without an icon +- **WHEN** a menu slot holds a non-block item that has no pixel icon in the generator +- **THEN** the generator fails and names the material diff --git a/openspec/changes/minecraft-style-illustrations/tasks.md b/openspec/changes/minecraft-style-illustrations/tasks.md new file mode 100644 index 00000000..12fb837b --- /dev/null +++ b/openspec/changes/minecraft-style-illustrations/tasks.md @@ -0,0 +1,36 @@ +## 1. Proposal (docs(openspec)) + +- [x] 1.1 Write the proposal, design and delta spec of `docs-illustrations`; `openspec validate minecraft-style-illustrations --strict` passes + +## 2. Red tests (build(docs)) + +- [x] 2.1 Add failing tests in `src/docsGenTest` for: distinct base colours and real material names in the legend, tile determinism and the grass side, faded unchanged blocks and the sky tint, paint control points and a thin stroke, menu callouts on functional slots only, unknown items naming their material, and the SVG model elements (definitions, use, opacity, polyline, circle, centred text) +- [x] 2.2 Run `./gradlew docsGenTest` and confirm the new tests fail for the stated reasons before any production code changes + +## 3. Block looks and tiles (build(docs)) + +- [x] 3.1 Green: `BlockLooks` with the base colours and procedural 8x8 tiles, `BlockPalette` mapping each fixture to its material name, `Terrain.cobblestone` fixture +- [x] 3.2 Green: SVG model with definitions, `use`, opacity, polyline and circle elements, and the writer for them + +## 4. Scenes (build(docs)) + +- [x] 4.1 Green: grass over dirt over stone ground for every scene, sphere and gradient with stone, andesite and cobblestone, paint scene with six numbered control points and a thin size + +## 5. Brush and menu pictures (build(docs)) + +- [x] 5.1 Green: brush picture with faded unchanged blocks, highlight border on changed blocks, sky tint, named legend, numbered control points and path +- [x] 5.2 Green: menu picture as a chest container with glass panes, block tiles, pixel icons, callouts on functional slots and a legend + +## 6. Images (build(docs)) + +- [x] 6.1 Run `./gradlew generateDocsImages`, rasterise the 14 SVGs at 800 px into the scratch folder outside the repository, inspect each picture and iterate until each is readable and looks like Minecraft blocks +- [x] 6.2 Run `./gradlew generateDocsImages` twice and confirm `git status --porcelain docs/images` changes only with the sources + +## 7. Documentation of the tooling (docs(development)) + +- [x] 7.1 Update the Illustrations section of `docs/development/documentation.md`: the material colours, the computed tiles, the rule against Minecraft or third-party texture files, the legend, the callouts + +## 8. Delivery + +- [x] 8.1 Run `./gradlew spotlessApply` and `./gradlew check`; both pass with no sleeps, clock reads or shared static state in the new tests +- [x] 8.2 Push the branch and open the pull request titled `build(docs): draw illustrations with minecraft style blocks`; the body ends with the session link diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BlockLooks.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BlockLooks.java new file mode 100644 index 00000000..d2b06751 --- /dev/null +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BlockLooks.java @@ -0,0 +1,236 @@ +/* + * goPaint is designed to simplify painting inside of Minecraft. + * Copyright (C) Arcaniax-Development + * Copyright (C) Arcaniax team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.onelitefeather.bettergopaint.docsgen; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Random; + +/** + * The look of the Minecraft blocks in the illustrations: a representative base colour per material and an 8x8 pixel + * tile for each face, computed in code. The tiles are shades of the base colour with a seed taken from the material + * name, so they are the same on every run. No texture file of Minecraft or of any other project is read, copied or + * embedded; the colours are averages chosen by hand. + */ +final class BlockLooks { + + /** + * The pixels of a tile along one side. + */ + static final int SIZE = 8; + + private static final String DIRT = "DIRT"; + private static final int MORTAR_DARKEN = 28; + private static final int RIM_DARKEN = 10; + private static final List LOOKS = List.of( + new BlockLook("GRASS_BLOCK", "#7CBD6B", 14, 10, 8, true, Structure.NONE), + new BlockLook(DIRT, "#866043", 12, 18, 8, false, Structure.NONE), + new BlockLook("STONE", "#7D7D7D", 16, 26, 14, false, Structure.NONE), + new BlockLook("ANDESITE", "#8A8B8C", 30, 6, 12, false, Structure.NONE), + new BlockLook("COBBLESTONE", "#6E6E6E", 14, 20, 8, false, Structure.MORTAR), + new BlockLook("GOLD_BLOCK", "#F9D849", 14, 4, 6, false, Structure.RIM), + new BlockLook("SPONGE", "#C3C04A", 12, 26, 12, false, Structure.NONE)); + + /** + * A pattern drawn over the noise of a material, so that materials of similar colour read as different blocks. + */ + enum Structure { + /** Only the noise. */ + NONE, + /** Dark mortar lines of a cobblestone wall. */ + MORTAR, + /** A darker outline on the edge pixels of the block. */ + RIM + } + + private BlockLooks() { + } + + /** + * @return the materials with a look, in the order of the legend + */ + static List materials() { + return LOOKS.stream().map(BlockLook::material).toList(); + } + + /** + * @param material the name of the material, for example {@code GRASS_BLOCK} + * @return whether the material has a look + */ + static boolean has(String material) { + return LOOKS.stream().anyMatch(look -> look.material().equals(material)); + } + + /** + * @param material the name of the material, for example {@code GRASS_BLOCK} + * @return the look of the material + * @throws IllegalStateException if the material has no look; the message names the material + */ + static BlockLook look(String material) { + return LOOKS.stream() + .filter(look -> look.material().equals(material)) + .findFirst() + .orElseThrow(() -> new IllegalStateException("no block look for " + material + "; add it to BlockLooks")); + } + + /** + * The face of a block that a tile shows. + */ + enum Face { + /** The top of a block, seen from above. */ + TOP, + /** A side of a block. A grass block has its green strip on top of this face. */ + SIDE + } + + /** + * The look of one material. + * + * @param material the Minecraft name of the material + * @param base the representative colour, {@code #rrggbb} + * @param lightPercent the share of pixels that are lighter than the base colour, in percent + * @param darkPercent the share of pixels that are darker than the base colour, in percent + * @param spread how far a light or dark pixel differs from the base colour, in percent of the channel + * @param grassStrip whether the side is a grass side: a green strip over dirt + * @param structure the pattern drawn over the noise + */ + record BlockLook(String material, String base, int lightPercent, int darkPercent, int spread, + boolean grassStrip, Structure structure) { + + /** + * @return the name in the legend, the Minecraft name, for example {@code Grass Block} + */ + String label() { + StringBuilder label = new StringBuilder(); + for (String word : material.split("_")) { + if (!label.isEmpty()) { + label.append(' '); + } + label.append(word.charAt(0)).append(word.substring(1).toLowerCase(Locale.ROOT)); + } + return label.toString(); + } + + /** + * @param face the face of the block + * @return the {@link #SIZE} by {@link #SIZE} pixels of the tile, row by row, each {@code #rrggbb} + */ + List pixels(Face face) { + Random random = new Random(seed(face)); + List pixels = new ArrayList<>(SIZE * SIZE); + for (int row = 0; row < SIZE; row++) { + for (int column = 0; column < SIZE; column++) { + pixels.add(pixel(random, row, column, face)); + } + } + return List.copyOf(pixels); + } + + private String pixel(Random random, int row, int column, Face face) { + String colour; + if (grassStrip && face == Face.SIDE && !(row < 2 || (row == 2 && random.nextBoolean()))) { + colour = look(DIRT).shade(random, look(DIRT).base()); + } else { + colour = shade(random, base); + } + return switch (structure) { + case NONE -> colour; + case MORTAR -> mortar(row, column) ? Rgb.of(colour).shifted(-MORTAR_DARKEN).hex() : colour; + case RIM -> row == 0 || column == 0 || row == SIZE - 1 || column == SIZE - 1 + ? Rgb.of(colour).shifted(-RIM_DARKEN).hex() : colour; + }; + } + + /** + * The mortar of a cobblestone wall: two full rows, and a staggered joint in each band between them. + */ + private static boolean mortar(int row, int column) { + return row == 3 || row == 7 || (row < 3 && column == 3) || (row > 3 && row < 7 && column == 6); + } + + /** + * One pixel: most pixels keep the base colour, the others are lighter or darker by the spread, and every pixel + * varies a little. + */ + private String shade(Random random, String colour) { + int roll = random.nextInt(100); + int step; + if (roll < darkPercent) { + step = -spread; + } else if (roll < darkPercent + lightPercent) { + step = spread; + } else { + step = random.nextInt(spread + 1) - spread / 2; + } + return Rgb.of(colour).shifted(step).hex(); + } + + private long seed(Face face) { + long seed = 17; + for (char character : (material + "/" + face.name()).toCharArray()) { + seed = seed * 31 + character; + } + return seed; + } + } + + /** + * An RGB colour with 8 bits per channel. + * + * @param red the red channel + * @param green the green channel + * @param blue the blue channel + */ + record Rgb(int red, int green, int blue) { + + /** + * @param hex a colour as {@code #rrggbb}, in any letter case + * @return the colour + */ + static Rgb of(String hex) { + return new Rgb( + Integer.parseInt(hex.substring(1, 3), 16), + Integer.parseInt(hex.substring(3, 5), 16), + Integer.parseInt(hex.substring(5, 7), 16)); + } + + /** + * @param percent the change in percent of the channel range; positive moves towards white, negative towards black + * @return the colour changed by the percent in each channel + */ + Rgb shifted(int percent) { + return new Rgb(channel(red, percent), channel(green, percent), channel(blue, percent)); + } + + /** + * @return the colour as lower case {@code #rrggbb} + */ + String hex() { + return String.format(Locale.ROOT, "#%02x%02x%02x", red, green, blue); + } + + private static int channel(int value, int percent) { + if (percent >= 0) { + return value + (255 - value) * percent / 100; + } + return value + value * percent / 100; + } + } +} diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BlockPalette.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BlockPalette.java index 9ac1b1c3..439c0aa6 100644 --- a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BlockPalette.java +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BlockPalette.java @@ -25,12 +25,17 @@ import java.util.List; /** - * The colour and the name of every block the scenes contain. Blocks are matched by identity, because the test - * fixtures are mocks that share no state. A block that is not listed fails the generator with its position; the - * generator never guesses a colour. + * The Minecraft material of every block the scenes contain. Blocks are matched by identity, because the test + * fixtures are mocks that share no state, and each fixture is named after the real material it stands for. A block + * that is not listed fails the generator with its position; the generator never guesses a material. */ final class BlockPalette { + /** + * The material name of air, which is never drawn as a block. + */ + static final String AIR = "AIR"; + private final List entries; private BlockPalette(List entries) { @@ -43,12 +48,13 @@ private BlockPalette(List entries) { */ static BlockPalette of(Terrain terrain) { return new BlockPalette(List.of( - new Entry(terrain.air, "air", null), - new Entry(terrain.stone, "stone", "#374151"), - new Entry(terrain.andesite, "andesite", "#0d9488"), - new Entry(terrain.gold, "gold", "#ca8a04"), - new Entry(terrain.grass, "grass", "#16a34a"), - new Entry(terrain.dirt, "dirt", "#92400e"))); + new Entry(terrain.air, AIR), + new Entry(terrain.grass, "GRASS_BLOCK"), + new Entry(terrain.dirt, "DIRT"), + new Entry(terrain.stone, "STONE"), + new Entry(terrain.andesite, "ANDESITE"), + new Entry(terrain.cobblestone, "COBBLESTONE"), + new Entry(terrain.gold, "GOLD_BLOCK"))); } /** @@ -63,7 +69,7 @@ Entry entry(BlockType type, BlockVector3 position) { return entry; } } - throw new IllegalStateException("no colour for the block " + type + " at " + position + throw new IllegalStateException("no material for the block " + type + " at " + position + "; add it to BlockPalette"); } @@ -77,10 +83,16 @@ List entries() { /** * One block of the palette. * - * @param type the block type, matched by identity - * @param label the name shown in the legend - * @param colour the fill colour, or {@code null} for air, which is not drawn + * @param type the block type, matched by identity + * @param material the Minecraft name of the material, for example {@code GRASS_BLOCK}, or {@link #AIR} */ - record Entry(BlockType type, String label, String colour) { + record Entry(BlockType type, String material) { + + /** + * @return whether the block is air + */ + boolean air() { + return material.equals(AIR); + } } } diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushIllustration.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushIllustration.java index b6ab2f8a..dc41a193 100644 --- a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushIllustration.java +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushIllustration.java @@ -19,33 +19,34 @@ package net.onelitefeather.bettergopaint.docsgen; import com.sk89q.worldedit.math.BlockVector3; -import com.sk89q.worldedit.world.block.BlockType; import net.onelitefeather.bettergopaint.support.MemoryExtent; import java.util.ArrayList; -import java.util.Collections; import java.util.HashMap; import java.util.HashSet; -import java.util.IdentityHashMap; import java.util.List; import java.util.Map; import java.util.Set; /** - * Draws one brush scene as an SVG: a top view, a cross-section through the clicked block, and a legend. Changed blocks - * take their palette colour. Blocks the stroke did not change are grey and air is white, so the painted part stands out - * on the light card that is the same in both GitBook themes. Every position is a whole number of cells, and air is drawn - * too, so both panels show the same grid. + * Draws one brush scene as an SVG: a top view, a cross-section through the clicked block, and a legend. Blocks are + * drawn with the tile of their Minecraft material. A block the brush changed is drawn at full opacity with a highlight + * border; a block it did not change is faded, so the painted part stands out and the ground stays recognisable. Air + * is the sky tint. Every position is a whole number of cells, and air is drawn too, so both panels share one grid. */ final class BrushIllustration { static final String CARD = "#f8fafc"; - private static final String AIR = "#ffffff"; - private static final String GRID = "#e5e7eb"; + private static final String SKY = "#dbeafe"; + private static final String GRID = "#0f172a"; + private static final int GRID_OPACITY = 22; private static final String INK = "#111827"; - private static final String UNCHANGED = "#9ca3af"; - private static final String MARKER = "#111827"; + private static final String HIGHLIGHT = "#ffffff"; private static final int CELL = 24; + private static final int PIXEL = CELL / BlockLooks.SIZE; + private static final int LEGEND_PIXEL = 2; + private static final int SWATCH = 16; + private static final int FADED = 55; private static final int MARGIN = 2; private static final int PADDING = 16; private static final int PANEL_GAP = 40; @@ -54,26 +55,30 @@ final class BrushIllustration { private static final int PANEL_TOP = 72; private static final int LEGEND_GAP = 32; private static final int LEGEND_COLUMNS = 4; - private static final int LEGEND_COLUMN_WIDTH = 150; - private static final int LEGEND_ROW_HEIGHT = 22; + private static final int LEGEND_COLUMN_WIDTH = 180; + private static final int LEGEND_ROW_HEIGHT = 26; + private static final int POINT_RADIUS = 8; private BrushIllustration() { } /** * @param scene the scene to draw, with the stroke applied - * @param palette the colours of the blocks of the scene + * @param palette the materials of the blocks of the scene * @return the picture, without any timestamp or generator version - * @throws IllegalStateException if a block of the scene is not in the palette + * @throws IllegalStateException if a block of the scene is not in the palette or has no look */ static SvgDocument render(BrushScene scene, BlockPalette palette) { List changes = scene.changes(); - Box box = Box.around(changes, scene.target(), MARGIN); + Box box = Box.around(changes, scene.points(), scene.target(), MARGIN); Set changed = new HashSet<>(); changes.forEach(change -> changed.add(change.position())); - List legend = legend(palette, changes); + List top = topCells(scene, palette, box); + List side = sideCells(scene, palette, box, changed); + List legend = legend(palette, top, side, scene.points().size() > 1); - int panelWidth = (box.maxX() - box.minX() + 1) * CELL; + int columns = box.maxX() - box.minX() + 1; + int panelWidth = columns * CELL; int panelHeight = Math.max(box.maxZ() - box.minZ() + 1, box.maxY() - box.minY() + 1) * CELL; int sideOrigin = PADDING + panelWidth + PANEL_GAP; int legendRows = (legend.size() + LEGEND_COLUMNS - 1) / LEGEND_COLUMNS; @@ -87,117 +92,208 @@ static SvgDocument render(BrushScene scene, BlockPalette palette) { .text(PADDING, TITLE_BASELINE, scene.brush().getName(), 18, INK, true) .text(PADDING, LABEL_BASELINE, "Top view", 13, INK, false) .text(sideOrigin, LABEL_BASELINE, "Side cross-section", 13, INK, false); - drawTopView(document, scene, palette, box); - drawSideView(document, scene, palette, box, changed, sideOrigin); - drawLegend(document, legend, legendTop); + Sprites sprites = new Sprites(); + drawPanel(document, sprites, top, columns, PADDING, BlockLooks.Face.TOP); + drawPanel(document, sprites, side, columns, sideOrigin, BlockLooks.Face.SIDE); + BlockVector3 target = scene.target(); + marker(document, PADDING, target.x() - box.minX(), target.z() - box.minZ()); + marker(document, sideOrigin, target.x() - box.minX(), box.maxY() - target.y()); + if (scene.points().size() > 1) { + drawPoints(document, scene, box); + } + drawLegend(document, sprites, legend, legendTop); + sprites.writeTo(document); return document; } /** - * One cell per column of x and z. A column shows its highest changed block; a column the stroke did not change - * shows its top block in grey. + * One cell per column of x and z, row by row from the lowest z. A column the brush changed shows its highest + * changed block at full opacity; any other column shows its top block, faded, or the sky when it has none. */ - private static void drawTopView(SvgDocument document, BrushScene scene, BlockPalette palette, Box box) { + private static List topCells(BrushScene scene, BlockPalette palette, Box box) { Map highest = new HashMap<>(); for (BrushScene.Change change : scene.changes()) { highest.merge(new Column(change.position().x(), change.position().z()), change, (first, second) -> first.position().y() >= second.position().y() ? first : second); } + List cells = new ArrayList<>(); for (int z = box.minZ(); z <= box.maxZ(); z++) { for (int x = box.minX(); x <= box.maxX(); x++) { - String fill = topFill(scene, palette, highest.get(new Column(x, z)), x, z); - cell(document, PADDING, x - box.minX(), z - box.minZ(), fill); + BrushScene.Change change = highest.get(new Column(x, z)); + if (change != null) { + cells.add(cellOf(palette.entry(change.after(), change.position()), true)); + } else { + cells.add(topmost(scene.after(), palette, x, z)); + } } } - BlockVector3 target = scene.target(); - marker(document, PADDING, target.x() - box.minX(), target.z() - box.minZ()); + return cells; } - /** - * @return the fill of one column of the top view; air is drawn as an empty cell, so the grid keeps its scale - */ - private static String topFill(BrushScene scene, BlockPalette palette, BrushScene.Change changed, int x, int z) { - if (changed != null) { - BlockVector3 position = BlockVector3.at(x, changed.position().y(), z); - return colourOrAir(palette.entry(changed.after(), position)); - } - MemoryExtent after = scene.after(); + private static Cell topmost(MemoryExtent after, BlockPalette palette, int x, int z) { for (int y = after.getMaximumPoint().y(); y >= after.getMinimumPoint().y(); y--) { BlockVector3 position = BlockVector3.at(x, y, z); - if (palette.entry(after.typeAt(position), position).colour() != null) { - return UNCHANGED; + BlockPalette.Entry entry = palette.entry(after.typeAt(position), position); + if (!entry.air()) { + return cellOf(entry, false); } } - return AIR; - } - - private static String colourOrAir(BlockPalette.Entry entry) { - return entry.colour() == null ? AIR : entry.colour(); + return Cell.AIR; } /** - * One cell per column of x and y, in the plane of the clicked block. The highest y is drawn first. + * One cell per x and y in the plane of the clicked block, the highest y first. */ - private static void drawSideView(SvgDocument document, BrushScene scene, BlockPalette palette, Box box, - Set changed, int originX) { + private static List sideCells(BrushScene scene, BlockPalette palette, Box box, Set changed) { int planeZ = scene.target().z(); + List cells = new ArrayList<>(); for (int y = box.maxY(); y >= box.minY(); y--) { for (int x = box.minX(); x <= box.maxX(); x++) { BlockVector3 position = BlockVector3.at(x, y, planeZ); BlockPalette.Entry entry = palette.entry(scene.after().typeAt(position), position); - String fill; - if (entry.colour() == null) { - fill = AIR; - } else { - fill = changed.contains(position) ? entry.colour() : UNCHANGED; - } - cell(document, originX, x - box.minX(), box.maxY() - y, fill); + cells.add(cellOf(entry, changed.contains(position))); } } - BlockVector3 target = scene.target(); - marker(document, originX, target.x() - box.minX(), box.maxY() - target.y()); + return cells; } - private static void drawLegend(SvgDocument document, List legend, int top) { - for (int index = 0; index < legend.size(); index++) { - Legend item = legend.get(index); - int x = PADDING + (index % LEGEND_COLUMNS) * LEGEND_COLUMN_WIDTH; - int baseline = top + (index / LEGEND_COLUMNS) * LEGEND_ROW_HEIGHT; - if (item.marker()) { - document.rect(x, baseline - 11, 14, 14, item.fill(), MARKER, 2); - } else { - document.rect(x, baseline - 11, 14, 14, item.fill(), "none", 0); + private static Cell cellOf(BlockPalette.Entry entry, boolean changed) { + return entry.air() ? Cell.AIR : new Cell(entry.material(), changed); + } + + private static void drawPanel(SvgDocument document, Sprites sprites, List cells, int columns, int originX, + BlockLooks.Face face) { + for (int index = 0; index < cells.size(); index++) { + drawCell(document, sprites, originX + (index % columns) * CELL, PANEL_TOP + (index / columns) * CELL, + cells.get(index), face); + } + } + + private static void drawCell(SvgDocument document, Sprites sprites, int x, int y, Cell cell, BlockLooks.Face face) { + if (cell.air()) { + document.rect(x, y, CELL, CELL, SKY, "none", 0); + } else { + document.use(sprites.block(cell.material(), face, PIXEL), x, y, cell.changed() ? 100 : FADED); + if (cell.changed()) { + highlight(document, x, y, CELL, 70); } - document.text(x + 22, baseline, item.label(), 13, INK, false); } + document.rect(x, y, CELL, CELL, "none", GRID, 1, GRID_OPACITY); + } + + /** + * The thin highlight border of a changed block: a white line one pixel inside the edge of the cell. + */ + private static void highlight(SvgDocument document, int x, int y, int size, int opacity) { + document.rect(x + 1, y + 1, size - 2, size - 2, "none", HIGHLIGHT, 1, opacity); + } + + /** + * The outline of the clicked block. + */ + private static void marker(SvgDocument document, int originX, int column, int row) { + document.rect(originX + column * CELL, PANEL_TOP + row * CELL, CELL, CELL, "none", INK, 2); } /** - * The legend: the unchanged grey, each palette block that the picture shows, and the marker of the clicked block. - * Palette blocks follow the palette order, so the legend does not depend on the order of the changes. + * The stroke as a path through its clicked points, with each point numbered in click order. */ - private static List legend(BlockPalette palette, List changes) { - Set used = Collections.newSetFromMap(new IdentityHashMap<>()); - changes.forEach(change -> used.add(change.after())); + private static void drawPoints(SvgDocument document, BrushScene scene, Box box) { + List centres = new ArrayList<>(); + for (BlockVector3 point : scene.points()) { + centres.add(new SvgDocument.Point(PADDING + (point.x() - box.minX()) * CELL + CELL / 2, + PANEL_TOP + (point.z() - box.minZ()) * CELL + CELL / 2)); + } + document.polyline(centres, INK, 2); + for (int index = 0; index < centres.size(); index++) { + SvgDocument.Point centre = centres.get(index); + document.circle(centre.x(), centre.y(), POINT_RADIUS, HIGHLIGHT, INK, 2); + document.centredText(centre.x(), centre.y() + 4, String.valueOf(index + 1), 11, INK, true); + } + } + /** + * The legend: each material of the picture by its Minecraft name, in palette order, then the markers. + */ + private static List legend(BlockPalette palette, List top, List side, boolean numbered) { + Set materials = new HashSet<>(); + boolean air = false; + Cell changedSample = null; + Cell fadedSample = null; + List all = new ArrayList<>(top); + all.addAll(side); + for (Cell cell : all) { + if (cell.air()) { + air = true; + } else { + materials.add(cell.material()); + if (cell.changed() && changedSample == null) { + changedSample = cell; + } + if (!cell.changed() && fadedSample == null) { + fadedSample = cell; + } + } + } List items = new ArrayList<>(); - items.add(new Legend("unchanged block", UNCHANGED, false)); - items.add(new Legend("air", AIR, false)); for (BlockPalette.Entry entry : palette.entries()) { - if (entry.colour() != null && used.contains(entry.type())) { - items.add(new Legend(entry.label() + " block", entry.colour(), false)); + if (!entry.air() && materials.contains(entry.material())) { + items.add(new Legend(BlockLooks.look(entry.material()).label(), Marker.MATERIAL, entry.material())); } } - items.add(new Legend("clicked block", CARD, true)); + if (air) { + items.add(new Legend("air", Marker.AIR, null)); + } + if (changedSample != null) { + items.add(new Legend("changed by the brush", Marker.CHANGED, changedSample.material())); + } + if (fadedSample != null) { + items.add(new Legend("unchanged (faded)", Marker.FADED, fadedSample.material())); + } + items.add(new Legend("clicked block", Marker.CLICKED, null)); + if (numbered) { + items.add(new Legend("numbered: click order", Marker.POINT, null)); + } return items; } - private static void cell(SvgDocument document, int originX, int column, int row, String fill) { - document.rect(originX + column * CELL, PANEL_TOP + row * CELL, CELL, CELL, fill, GRID, 1); + private static void drawLegend(SvgDocument document, Sprites sprites, List legend, int top) { + for (int index = 0; index < legend.size(); index++) { + Legend item = legend.get(index); + int x = PADDING + (index % LEGEND_COLUMNS) * LEGEND_COLUMN_WIDTH; + int y = top + (index / LEGEND_COLUMNS) * LEGEND_ROW_HEIGHT; + drawSwatch(document, sprites, item, x, y); + document.text(x + SWATCH + 6, y + 12, item.label(), 13, INK, false); + } } - private static void marker(SvgDocument document, int originX, int column, int row) { - document.rect(originX + column * CELL, PANEL_TOP + row * CELL, CELL, CELL, "none", MARKER, 2); + private static void drawSwatch(SvgDocument document, Sprites sprites, Legend item, int x, int y) { + switch (item.marker()) { + case MATERIAL -> document.use(sprites.block(item.material(), BlockLooks.Face.TOP, LEGEND_PIXEL), x, y, 100); + case AIR -> document.rect(x, y, SWATCH, SWATCH, SKY, GRID, 1); + case CHANGED -> { + document.use(sprites.block(item.material(), BlockLooks.Face.TOP, LEGEND_PIXEL), x, y, 100); + highlight(document, x, y, SWATCH, 70); + } + case FADED -> document.use(sprites.block(item.material(), BlockLooks.Face.TOP, LEGEND_PIXEL), x, y, FADED); + case CLICKED -> document.rect(x, y, SWATCH, SWATCH, "none", INK, 2); + case POINT -> document.circle(x + SWATCH / 2, y + SWATCH / 2, 6, HIGHLIGHT, INK, 2); + } + } + + /** + * One cell of a panel. + * + * @param material the Minecraft name of the block, or {@code null} for air + * @param changed whether the brush changed the block + */ + private record Cell(String material, boolean changed) { + + static final Cell AIR = new Cell(null, false); + + boolean air() { + return material == null; + } } /** @@ -209,27 +305,37 @@ private record Column(int x, int z) { /** * One legend line. * - * @param label the text of the line - * @param fill the fill of the swatch - * @param marker whether the swatch is the outline of the clicked block + * @param label the text of the line + * @param marker what the swatch shows + * @param material the material of the swatch, or {@code null} when the marker needs none + */ + private record Legend(String label, Marker marker, String material) { + } + + /** + * What a legend swatch shows. */ - private record Legend(String label, String fill, boolean marker) { + private enum Marker { + MATERIAL, AIR, CHANGED, FADED, CLICKED, POINT } /** - * The smallest box of whole blocks that holds every change and the clicked block, grown by a margin. + * The smallest box of whole blocks that holds every change, the clicked block and every control point, grown by a + * margin. */ private record Box(int minX, int minY, int minZ, int maxX, int maxY, int maxZ) { - static Box around(List changes, BlockVector3 target, int margin) { - int minX = target.x(); - int minY = target.y(); - int minZ = target.z(); - int maxX = target.x(); - int maxY = target.y(); - int maxZ = target.z(); - for (BrushScene.Change change : changes) { - BlockVector3 position = change.position(); + static Box around(List changes, List points, BlockVector3 target, int margin) { + List positions = new ArrayList<>(points); + positions.add(target); + changes.forEach(change -> positions.add(change.position())); + int minX = Integer.MAX_VALUE; + int minY = Integer.MAX_VALUE; + int minZ = Integer.MAX_VALUE; + int maxX = Integer.MIN_VALUE; + int maxY = Integer.MIN_VALUE; + int maxZ = Integer.MIN_VALUE; + for (BlockVector3 position : positions) { minX = Math.min(minX, position.x()); minY = Math.min(minY, position.y()); minZ = Math.min(minZ, position.z()); diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushScene.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushScene.java index 2db0027a..5ed8cbb9 100644 --- a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushScene.java +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushScene.java @@ -36,8 +36,10 @@ * @param after the terrain after the stroke * @param target the position the stroke is centred on * @param palette the blocks the stroke could place, in the order they were configured + * @param points the clicked control points in click order; a single point for every brush but the paint brush */ -record BrushScene(Brush brush, MemoryExtent before, MemoryExtent after, BlockVector3 target, List palette) { +record BrushScene(Brush brush, MemoryExtent before, MemoryExtent after, BlockVector3 target, List palette, + List points) { private static final Comparator ORDER = Comparator .comparingInt(BlockVector3::x) diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushScenes.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushScenes.java index 34fc5a24..a33c6e8a 100644 --- a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushScenes.java +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/BrushScenes.java @@ -43,10 +43,16 @@ /** * The fixed scene of every brush: the terrain it paints on and the settings of its stroke. The brush itself runs its - * own {@link Brush#apply}; a scene only describes the input. + * own {@link Brush#apply}; a scene only describes the input. Every scene stands on grass over dirt over stone, so the + * pictures show the blocks a builder paints on. */ final class BrushScenes { + /** + * How far the terrain reaches from the origin in x and z. + */ + private static final int RADIUS = 12; + private BrushScenes() { } @@ -74,7 +80,7 @@ static BrushScene sceneOf(Brush brush, Terrain terrain) { brush.apply(after, context); - BrushScene scene = new BrushScene(brush, before, after, context.target(), context.blocks()); + BrushScene scene = new BrushScene(brush, before, after, context.target(), context.blocks(), context.points()); if (scene.changes().isEmpty()) { throw new IllegalStateException("the illustration scene of " + brush.getName() + " changes no block"); } @@ -83,56 +89,84 @@ static BrushScene sceneOf(Brush brush, Terrain terrain) { private static Recipe recipeOf(Brush brush) { return switch (brush) { - case SphereBrush _ -> new Recipe(BrushScenes::dirtGround, - terrain -> stroke().blocks(terrain.stone, terrain.andesite, terrain.gold).size(5).build()); - case SprayBrush _ -> new Recipe(Terrain::flat, + case SphereBrush _ -> new Recipe(BrushScenes::grassGround, + terrain -> stroke().blocks(terrain.stone, terrain.andesite, terrain.cobblestone).size(5).build()); + case SprayBrush _ -> new Recipe(BrushScenes::grassGround, terrain -> stroke().blocks(terrain.gold).size(5).chance(50).build()); - case SplatterBrush _ -> new Recipe(Terrain::flat, + case SplatterBrush _ -> new Recipe(BrushScenes::grassGround, terrain -> stroke().blocks(terrain.gold).size(5).falloff(2).build()); - case DiscBrush _ -> new Recipe(Terrain::flat, + case DiscBrush _ -> new Recipe(BrushScenes::grassGround, terrain -> stroke().blocks(terrain.gold).size(5).axis(Axis.X).build()); - case BucketBrush _ -> new Recipe(terrain -> terrain.flatWithTop(terrain.grass), + case BucketBrush _ -> new Recipe(BrushScenes::grassGround, terrain -> stroke().blocks(terrain.gold).size(5).build()); - case AngleBrush _ -> new Recipe(Terrain::step, + case AngleBrush _ -> new Recipe(BrushScenes::step, terrain -> stroke().blocks(terrain.gold).size(6).angle(2, 40).build()); - case OverlayBrush _ -> new Recipe(terrain -> terrain.flatWithTop(terrain.grass), + case OverlayBrush _ -> new Recipe(BrushScenes::grassGround, terrain -> stroke().blocks(terrain.gold).size(5).thickness(2).build()); - case UnderlayBrush _ -> new Recipe(terrain -> terrain.flatWithTop(terrain.grass), + case UnderlayBrush _ -> new Recipe(BrushScenes::grassGround, terrain -> stroke().blocks(terrain.gold).size(5).thickness(2).build()); - case FractureBrush _ -> new Recipe(Terrain::trench, + case FractureBrush _ -> new Recipe(BrushScenes::trench, terrain -> stroke().target(BlockVector3.at(0, Terrain.SURFACE - 3, 0)).blocks(terrain.gold).size(5) .fractureDistance(2).build()); - case GradientBrush _ -> new Recipe(BrushScenes::dirtGround, - terrain -> stroke().blocks(terrain.stone, terrain.andesite).size(5).mixing(2).falloff(2).build()); - case PaintBrush _ -> new Recipe(Terrain::flat, BrushScenes::paintStroke); + // The bands run from the bottom up. Stone comes last: the ground is stone below the dirt, and placing stone on + // stone changes no block, so a stone band at the bottom would not show. + case GradientBrush _ -> new Recipe(BrushScenes::grassGround, + terrain -> stroke().target(BlockVector3.at(0, Terrain.SURFACE - 3, 0)) + .blocks(terrain.cobblestone, terrain.andesite, terrain.stone).size(6).mixing(2).falloff(2) + .build()); + case PaintBrush _ -> new Recipe(BrushScenes::grassGround, BrushScenes::paintStroke); default -> throw new IllegalStateException("no illustration scene for the brush " + brush.getName()); }; } /** - * Dirt instead of stone, as deep as the flat terrain. The brushes that place stone or andesite need a block they - * can replace: stone placed on stone changes nothing. Unchanged blocks are drawn grey whatever their type, so - * the picture only shows the difference this makes to the stone palette. + * Grass on three dirt blocks on stone, as flat as the top layer of {@link Terrain#SURFACE}. + */ + private static MemoryExtent grassGround(Terrain terrain) { + return layers(terrain.empty(), terrain, -RADIUS, RADIUS, Terrain.SURFACE); + } + + /** + * The ground of the angle scene: flat grass for x up to zero and the same ground four blocks higher beyond it. + */ + private static MemoryExtent step(Terrain terrain) { + MemoryExtent world = terrain.empty(); + layers(world, terrain, -RADIUS, 0, Terrain.SURFACE); + return layers(world, terrain, 1, RADIUS, Terrain.SURFACE + 4); + } + + /** + * The grass ground with a one block wide trench along z at x = 0, three blocks deep. + */ + private static MemoryExtent trench(Terrain terrain) { + return grassGround(terrain).fill(BlockVector3.at(0, Terrain.SURFACE - 2, -RADIUS), + BlockVector3.at(0, Terrain.SURFACE, RADIUS), terrain.air); + } + + /** + * Stone up to four blocks under {@code grassY}, three dirt blocks, then the grass block at {@code grassY}, for + * every x from {@code fromX} to {@code toX}. */ - private static MemoryExtent dirtGround(Terrain terrain) { - return terrain.empty().fill(BlockVector3.at(-12, Terrain.MIN_Y, -12), BlockVector3.at(12, Terrain.SURFACE, 12), - terrain.dirt); + private static MemoryExtent layers(MemoryExtent world, Terrain terrain, int fromX, int toX, int grassY) { + world.fill(BlockVector3.at(fromX, Terrain.MIN_Y, -RADIUS), BlockVector3.at(toX, grassY - 4, RADIUS), terrain.stone); + world.fill(BlockVector3.at(fromX, grassY - 3, -RADIUS), BlockVector3.at(toX, grassY - 1, RADIUS), terrain.dirt); + return world.fill(BlockVector3.at(fromX, grassY, -RADIUS), BlockVector3.at(toX, grassY, RADIUS), terrain.grass); } /** - * A paint stroke of six clicks along a fixed curve on the top layer of the flat terrain. The last point is the - * target. + * A paint stroke of six clicks along a wave on the top layer of the grass ground. The last point is the target. + * The brush size is one block, so the stroke is a thin path through the points and not a solid area. */ private static StrokeContext paintStroke(Terrain terrain) { List points = List.of( - BlockVector3.at(-6, Terrain.SURFACE, -4), - BlockVector3.at(-3, Terrain.SURFACE, -1), - BlockVector3.at(-1, Terrain.SURFACE, 3), - BlockVector3.at(2, Terrain.SURFACE, 2), - BlockVector3.at(4, Terrain.SURFACE, -2), - BlockVector3.at(6, Terrain.SURFACE, 0)); + BlockVector3.at(-5, Terrain.SURFACE, -4), + BlockVector3.at(-3, Terrain.SURFACE, 1), + BlockVector3.at(-1, Terrain.SURFACE, -3), + BlockVector3.at(1, Terrain.SURFACE, 3), + BlockVector3.at(3, Terrain.SURFACE, -2), + BlockVector3.at(5, Terrain.SURFACE, 1)); return stroke().points(points.toArray(BlockVector3[]::new)).target(points.getLast()) - .blocks(terrain.gold).size(5).falloff(2).build(); + .blocks(terrain.gold).size(1).falloff(2).build(); } /** diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/ItemIcons.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/ItemIcons.java new file mode 100644 index 00000000..c8e5323f --- /dev/null +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/ItemIcons.java @@ -0,0 +1,245 @@ +/* + * goPaint is designed to simplify painting inside of Minecraft. + * Copyright (C) Arcaniax-Development + * Copyright (C) Arcaniax team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.onelitefeather.bettergopaint.docsgen; + +import net.onelitefeather.bettergopaint.docsgen.SvgDocument.Rect; + +import java.util.ArrayList; +import java.util.List; +import java.util.Map; + +/** + * The pictures of the menu items that are not cubes: small pixel icons drawn in code, and the dye colours of the + * stained glass panes. Each icon is a grid of characters, one character per pixel, where {@code .} is transparent and + * every other character is a colour of the icon. The shapes are simple and hand-drawn; no item texture is used. + */ +final class ItemIcons { + + private static final Map ICONS = Map.ofEntries( + Map.entry("FEATHER", new Icon(List.of( + ".......w", + "......wg", + ".....wgg", + "....wgd.", + "...wgd..", + "..wgd...", + ".wgd....", + "wd......"), Map.of('w', "#f4f4f4", 'g', "#bdbdbd", 'd', "#7d7d7d"))), + Map.entry("PLAYER_HEAD", new Icon(List.of( + "oooooooo", + "ohhhhhho", + "osssssso", + "osesseso", + "osssssso", + "osssssso", + "ossmmsso", + "oooooooo"), Map.of('o', "#3b2a1a", 'h', "#5b3a1e", 's', "#e8b98a", 'e', "#2b2b2b", + 'm', "#b5654a"))), + Map.entry("GOLD_NUGGET", new Icon(List.of( + "........", + "...oo...", + "..oYYo..", + ".oYyyyo.", + "oYyyyyyo", + ".oyyyyo.", + "..oooo..", + "........"), Map.of('o', "#b8860b", 'y', "#f9d849", 'Y', "#fff2a8"))), + Map.entry("BROWN_MUSHROOM", new Icon(List.of( + "........", + "..bbbb..", + ".bbbbbb.", + "bdbbbdbb", + "bbbbbbbb", + "...ss...", + "..ssss..", + "........"), Map.of('b', "#8f5f35", 'd', "#5c3b1f", 's', "#e8dcc0"))), + Map.entry("CARVED_PUMPKIN", pumpkin("#2a1404")), + Map.entry("JACK_O_LANTERN", pumpkin("#ffe066")), + Map.entry("LIGHT_WEIGHTED_PRESSURE_PLATE", new Icon(List.of( + "........", + "........", + "........", + "yyyyyyyy", + "yYYYYYYy", + "oooooooo", + "........", + "........"), Map.of('y', "#f9d849", 'Y', "#fff2a8", 'o', "#b8860b"))), + Map.entry("BARRIER", new Icon(List.of( + "..rrrr..", + ".rr..rr.", + "rr.r..rr", + "r..r...r", + "r...r..r", + "rr..r.rr", + ".rr..rr.", + "..rrrr.."), Map.of('r', "#d62424"))), + Map.entry("COMPASS", new Icon(List.of( + "..gggg..", + ".g....g.", + "g..rr..g", + "g..rr..g", + "g..ww..g", + "g..ww..g", + ".g....g.", + "..gggg.."), Map.of('g', "#8c8c8c", 'r', "#d62424", 'w', "#e0e0e0"))), + Map.entry("BOOK", new Icon(List.of( + "........", + ".bbbbbb.", + ".bppppb.", + ".bppppb.", + ".bppppb.", + ".bbbbbb.", + "........", + "........"), Map.of('b', "#6b4423", 'p', "#f4ecd8"))), + Map.entry("DAYLIGHT_DETECTOR", new Icon(List.of( + "........", + "..yyyy..", + ".yyyyyy.", + "bbbbbbbb", + "dddddddd", + "bbbbbbbb", + "dddddddd", + "........"), Map.of('y', "#f9d849", 'b', "#a8824f", 'd', "#6d4f2b"))), + Map.entry("BLAZE_ROD", new Icon(List.of( + "......yo", + ".....yyo", + "....yyo.", + "...yyo..", + "..yyo...", + ".yyo....", + "yyo.....", + "oo......"), Map.of('y', "#f5b50a", 'o', "#c96a0a"))), + Map.entry("MAGMA_CREAM", new Icon(List.of( + "........", + "...cc...", + "..cccc..", + ".ccdccc.", + ".cccccc.", + "..cccd..", + "...cc...", + "........"), Map.of('c', "#f7b264", 'd', "#b35a1f"))), + Map.entry("BLAZE_POWDER", new Icon(List.of( + "........", + "..y..y..", + "...yy...", + ".yyyyyy.", + "..yyyy..", + ".y.yy.y.", + "..y..y..", + "........"), Map.of('y', "#f5c12e")))); + + private static final Map GLASS_TINTS = Map.of( + "WHITE_STAINED_GLASS_PANE", "#f9fffe", + "ORANGE_STAINED_GLASS_PANE", "#f9801d", + "YELLOW_STAINED_GLASS_PANE", "#fed83d", + "LIME_STAINED_GLASS_PANE", "#80c71f", + "RED_STAINED_GLASS_PANE", "#b02e26", + "GRAY_STAINED_GLASS_PANE", "#474f52"); + + private ItemIcons() { + } + + /** + * @param material the name of the item, for example {@code FEATHER} + * @return whether the item has a pixel icon + */ + static boolean has(String material) { + return ICONS.containsKey(material); + } + + /** + * @return the names of the items with a pixel icon, sorted + */ + static List materials() { + return ICONS.keySet().stream().sorted().toList(); + } + + /** + * @param material the name of the item + * @return the icon of the item + * @throws IllegalStateException if the item has no icon; the message names the item + */ + static Icon icon(String material) { + Icon icon = ICONS.get(material); + if (icon == null) { + throw new IllegalStateException("no pixel icon for the menu item " + material + "; add it to ItemIcons"); + } + return icon; + } + + /** + * @param material the name of the item + * @return whether the item is a stained glass pane, which the menu draws as a tinted tile + */ + static boolean isGlassPane(String material) { + return GLASS_TINTS.containsKey(material); + } + + /** + * @param material the name of a stained glass pane + * @return the tint of the pane as {@code #rrggbb} + * @throws IllegalStateException if the item is not a stained glass pane the menu knows + */ + static String glassTint(String material) { + String tint = GLASS_TINTS.get(material); + if (tint == null) { + throw new IllegalStateException("no glass tint for the menu item " + material + "; add it to ItemIcons"); + } + return tint; + } + + private static Icon pumpkin(String face) { + return new Icon(List.of( + "...gg...", + ".oooooo.", + "ookooook", + "oookkooo", + "ookoook.", + "oookkkoo", + ".oooooo.", + "........"), Map.of('o', "#e0802b", 'g', "#4f7a2a", 'k', face)); + } + + /** + * A pixel icon. + * + * @param rows the rows of the icon, each as long as the others; {@code .} is transparent + * @param colours the colour of each character other than {@code .}, as {@code #rrggbb} + */ + record Icon(List rows, Map colours) { + + /** + * @param pixel the side of one pixel in the picture + * @return one rectangle per opaque pixel, in icon coordinates + */ + List pixels(int pixel) { + List rects = new ArrayList<>(); + for (int row = 0; row < rows.size(); row++) { + String line = rows.get(row); + for (int column = 0; column < line.length(); column++) { + char character = line.charAt(column); + if (character != '.') { + rects.add(new Rect(column * pixel, row * pixel, pixel, pixel, colours.get(character), "none", 0, 100)); + } + } + } + return rects; + } + } +} diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/MenuMap.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/MenuMap.java index 6f519f26..b8c623f9 100644 --- a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/MenuMap.java +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/MenuMap.java @@ -23,30 +23,41 @@ import java.util.ArrayList; import java.util.List; -import java.util.Locale; /** - * Draws the brush menu as a grid of six rows of nine cells. Every cell shows its slot number, the material of the - * item and the English text of the item name. The slots come from {@code GUI.layout()}, so the picture cannot drift - * from the menu the plugin builds. + * The picture of a brush menu: the slots of {@code GUI.layout()} drawn as the Minecraft 6x9 chest container, at twice the + * scale of the inventory. Block items are block tiles, stained glass panes are tinted translucent tiles and the other + * items are pixel icons. Each functional slot has a numbered badge, and a legend beside the container maps the numbers + * to the English text of the slot. A filler pane has no badge, so the picture shows what a builder can use. */ final class MenuMap { private static final int COLUMNS = 9; - private static final int CELL_WIDTH = 128; - private static final int CELL_HEIGHT = 104; + private static final int ROWS = 6; + private static final int SLOT = 36; + private static final int BEVEL = 2; + private static final int ITEM = SLOT - 2 * BEVEL; + private static final int ITEM_PIXEL = ITEM / BlockLooks.SIZE; + private static final int FRAME = 14; + private static final int PANEL_WIDTH = COLUMNS * SLOT + 2 * FRAME; + private static final int PANEL_HEIGHT = ROWS * SLOT + 2 * FRAME; private static final int PADDING = 16; private static final int TITLE_BASELINE = 36; - private static final int GRID_TOP = 56; - private static final int LINE_HEIGHT = 14; - private static final int TEXT_CHARACTERS = 18; - private static final int MATERIAL_LINES = 2; - private static final int PURPOSE_LINES = 3; + private static final int PANEL_TOP = 56; + private static final int LEGEND_GAP = 28; + private static final int LEGEND_WIDTH = 380; + private static final int LEGEND_LINE = 24; + private static final int BADGE_RADIUS = 9; + private static final int BADGE_OFFSET = 10; + private static final int GLASS_OPACITY = 80; private static final String CARD = "#f8fafc"; - private static final String CELL = "#ffffff"; - private static final String BORDER = "#d1d5db"; + private static final String CONTAINER = "#c6c6c6"; + private static final String CONTAINER_DARK = "#555555"; + private static final String SLOT_DARK = "#373737"; + private static final String SLOT_LIGHT = "#ffffff"; + private static final String SLOT_FILL = "#8b8b8b"; private static final String INK = "#111827"; - private static final String MUTED = "#4b5563"; + private static final String WHITE = "#ffffff"; private MenuMap() { } @@ -61,6 +72,16 @@ private MenuMap() { record MenuCell(int slot, String material, String purpose) { } + /** + * The numbered badge of a functional slot. + * + * @param number the number of the badge, from 1 in slot order + * @param slot the slot the badge marks + * @param label the English text of the slot + */ + record Callout(int number, int slot, String label) { + } + /** * @param layout the items of the menu, one per slot * @param text the English text of the translation keys @@ -81,69 +102,94 @@ static List cells(MenuItem[] layout, EnglishText text) { } /** - * @param title the title shown above the grid + * @param cells the cells of the menu, in slot order + * @return one callout per functional slot, numbered from 1 in slot order; filler panes get none + */ + static List callouts(List cells) { + List callouts = new ArrayList<>(); + for (MenuCell cell : cells) { + if (!cell.purpose().isEmpty()) { + callouts.add(new Callout(callouts.size() + 1, cell.slot(), cell.purpose())); + } + } + return callouts; + } + + /** + * @param title the title shown above the container * @param cells the cells of the menu, in slot order * @return the picture of the menu + * @throws IllegalStateException if an item is neither a block with a look, a glass pane nor an item with an icon */ static SvgDocument render(String title, List cells) { - int width = 2 * PADDING + COLUMNS * CELL_WIDTH; - int height = GRID_TOP + (cells.size() / COLUMNS) * CELL_HEIGHT + PADDING; + List callouts = callouts(cells); + int legendHeight = callouts.size() * LEGEND_LINE; + int width = PADDING + PANEL_WIDTH + LEGEND_GAP + LEGEND_WIDTH + PADDING; + int height = PANEL_TOP + Math.max(PANEL_HEIGHT, legendHeight) + PADDING; SvgDocument document = new SvgDocument(width, height) .rect(0, 0, width, height, CARD, "none", 0) .text(PADDING, TITLE_BASELINE, title, 18, INK, true); + drawContainer(document, PADDING, PANEL_TOP); + Sprites sprites = new Sprites(); for (MenuCell cell : cells) { - int x = PADDING + (cell.slot() % COLUMNS) * CELL_WIDTH + 2; - int y = GRID_TOP + (cell.slot() / COLUMNS) * CELL_HEIGHT + 2; - document.rect(x, y, CELL_WIDTH - 4, CELL_HEIGHT - 4, CELL, BORDER, 1); - drawCell(document, cell, x + 8, y + 18); + int x = slotX(cell.slot()); + int y = slotY(cell.slot()); + drawSlot(document, sprites, cell, x, y); + } + for (Callout callout : callouts) { + badge(document, slotX(callout.slot()) + BADGE_OFFSET, slotY(callout.slot()) + BADGE_OFFSET, callout.number()); } + int legendX = PADDING + PANEL_WIDTH + LEGEND_GAP; + for (Callout callout : callouts) { + int baseline = PANEL_TOP + LEGEND_LINE / 2 + (callout.number() - 1) * LEGEND_LINE + 4; + badge(document, legendX + BADGE_RADIUS, baseline - 4, callout.number()); + document.text(legendX + 2 * BADGE_RADIUS + 8, baseline, "Slot " + callout.slot() + ": " + callout.label(), + 13, INK, false); + } + sprites.writeTo(document); return document; } - private static void drawCell(SvgDocument document, MenuCell cell, int x, int slotBaseline) { - document.text(x, slotBaseline, "Slot " + cell.slot(), 11, MUTED, true); - int line = slotBaseline; - for (String part : wrap(cell.material().toLowerCase(Locale.ROOT).replace('_', ' '), MATERIAL_LINES)) { - line += LINE_HEIGHT; - document.text(x, line, part, 11, MUTED, false); - } - String purpose = cell.purpose().isEmpty() ? "filler" : cell.purpose(); - for (String part : wrap(purpose, PURPOSE_LINES)) { - line += LINE_HEIGHT; - document.text(x, line, part, 11, INK, !cell.purpose().isEmpty()); - } + private static int slotX(int slot) { + return PADDING + FRAME + (slot % COLUMNS) * SLOT; + } + + private static int slotY(int slot) { + return PANEL_TOP + FRAME + (slot / COLUMNS) * SLOT; } /** - * Breaks the text at spaces into at most {@code maxLines} lines of {@link #TEXT_CHARACTERS} characters. Text that - * does not fit ends with {@code ...}. + * The grey container with its light top and left edge and its dark bottom and right edge. */ - static List wrap(String text, int maxLines) { - List lines = new ArrayList<>(); - StringBuilder line = new StringBuilder(); - for (String word : text.split(" ")) { - if (word.isEmpty()) { - continue; - } - if (line.isEmpty()) { - line.append(word); - } else if (line.length() + 1 + word.length() <= TEXT_CHARACTERS) { - line.append(' ').append(word); - } else { - lines.add(line.toString()); - line.setLength(0); - line.append(word); - } - } - if (!line.isEmpty()) { - lines.add(line.toString()); - } - if (lines.size() > maxLines) { - List kept = new ArrayList<>(lines.subList(0, maxLines)); - String last = kept.get(maxLines - 1); - kept.set(maxLines - 1, last.substring(0, Math.min(last.length(), TEXT_CHARACTERS - 3)) + "..."); - return kept; + private static void drawContainer(SvgDocument document, int x, int y) { + document.rect(x, y, PANEL_WIDTH, PANEL_HEIGHT, CONTAINER_DARK, "none", 0); + document.rect(x, y, PANEL_WIDTH - BEVEL, PANEL_HEIGHT - BEVEL, SLOT_LIGHT, "none", 0); + document.rect(x + BEVEL, y + BEVEL, PANEL_WIDTH - 2 * BEVEL, PANEL_HEIGHT - 2 * BEVEL, CONTAINER, "none", 0); + } + + /** + * An inventory slot: a dark inset with a light bottom and right edge, and the item in its middle. + */ + private static void drawSlot(SvgDocument document, Sprites sprites, MenuCell cell, int x, int y) { + document.rect(x, y, SLOT, SLOT, SLOT_LIGHT, "none", 0); + document.rect(x, y, SLOT - BEVEL, SLOT - BEVEL, SLOT_DARK, "none", 0); + document.rect(x + BEVEL, y + BEVEL, ITEM, ITEM, SLOT_FILL, "none", 0); + int itemX = x + BEVEL; + int itemY = y + BEVEL; + if (BlockLooks.has(cell.material())) { + document.use(sprites.block(cell.material(), BlockLooks.Face.TOP, ITEM_PIXEL), itemX, itemY, 100); + } else if (ItemIcons.isGlassPane(cell.material())) { + document.rect(itemX, itemY, ITEM, ITEM, ItemIcons.glassTint(cell.material()), "none", 0, GLASS_OPACITY); + } else { + document.use(sprites.icon(cell.material(), ITEM_PIXEL), itemX, itemY, 100); } - return lines; + } + + /** + * A numbered badge: a dark disc with a white number. + */ + private static void badge(SvgDocument document, int centreX, int centreY, int number) { + document.circle(centreX, centreY, BADGE_RADIUS, INK, WHITE, 1); + document.centredText(centreX, centreY + 4, String.valueOf(number), 11, WHITE, true); } } diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/Sprites.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/Sprites.java new file mode 100644 index 00000000..355669de --- /dev/null +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/Sprites.java @@ -0,0 +1,82 @@ +/* + * goPaint is designed to simplify painting inside of Minecraft. + * Copyright (C) Arcaniax-Development + * Copyright (C) Arcaniax team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.onelitefeather.bettergopaint.docsgen; + +import net.onelitefeather.bettergopaint.docsgen.SvgDocument.Rect; + +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; +import java.util.Map; +import java.util.TreeMap; + +/** + * The symbols of one picture: a block tile or an item icon at one pixel size, defined on first use. The symbols are + * written in the order of their ids, so the picture does not depend on the order in which the cells are drawn. + */ +final class Sprites { + + private final Map> symbols = new TreeMap<>(); + + /** + * @param material the Minecraft name of the block, for example {@code GRASS_BLOCK} + * @param face the face of the block the tile shows + * @param pixel the side of one pixel of the tile in the picture + * @return the id of the symbol of the tile + */ + String block(String material, BlockLooks.Face face, int pixel) { + String id = "block-" + slug(material) + "-" + face.name().toLowerCase(Locale.ROOT) + "-" + pixel; + symbols.computeIfAbsent(id, key -> tile(BlockLooks.look(material).pixels(face), pixel)); + return id; + } + + /** + * @param material the name of the item, for example {@code FEATHER} + * @param pixel the side of one pixel of the icon in the picture + * @return the id of the symbol of the icon + */ + String icon(String material, int pixel) { + String id = "icon-" + slug(material) + "-" + pixel; + symbols.computeIfAbsent(id, key -> ItemIcons.icon(material).pixels(pixel)); + return id; + } + + /** + * Defines every symbol used so far in the picture. + * + * @param document the picture the symbols belong to + */ + void writeTo(SvgDocument document) { + symbols.forEach((id, pixels) -> document.symbol(id, pixels)); + } + + private static List tile(List colours, int pixel) { + List rects = new ArrayList<>(colours.size()); + for (int index = 0; index < colours.size(); index++) { + int column = index % BlockLooks.SIZE; + int row = index / BlockLooks.SIZE; + rects.add(new Rect(column * pixel, row * pixel, pixel, pixel, colours.get(index), "none", 0, 100)); + } + return rects; + } + + private static String slug(String material) { + return material.toLowerCase(Locale.ROOT).replace('_', '-'); + } +} diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/SvgDocument.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/SvgDocument.java index 59b205da..50f08fac 100644 --- a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/SvgDocument.java +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/SvgDocument.java @@ -23,14 +23,17 @@ import java.util.Objects; /** - * A picture as plain data: its size and its elements in drawing order. Every coordinate and size is a whole number, - * so {@link SvgWriter} never has to format a fraction, and the same model always produces the same bytes. + * A picture as plain data: its size, its symbols and its elements in drawing order. Every coordinate and size is a + * whole number, so {@link SvgWriter} never has to format a fraction, and the same model always produces the same + * bytes. A symbol is a group of pixels that is written once in {@code defs} and drawn any number of times with + * {@link #use}, which keeps a picture of hundreds of block faces small. */ public final class SvgDocument { private final int width; private final int height; private final List elements = new ArrayList<>(); + private final List symbols = new ArrayList<>(); public SvgDocument(int width, int height) { if (width <= 0 || height <= 0) { @@ -44,7 +47,18 @@ public SvgDocument(int width, int height) { * Adds a rectangle. Pass {@code "none"} as the fill or the stroke to leave it out. */ public SvgDocument rect(int x, int y, int width, int height, String fill, String stroke, int strokeWidth) { - elements.add(new Rect(x, y, width, height, Objects.requireNonNull(fill), Objects.requireNonNull(stroke), strokeWidth)); + return rect(x, y, width, height, fill, stroke, strokeWidth, 100); + } + + /** + * Adds a rectangle that is drawn with the given opacity. + * + * @param opacity the opacity in percent, from 0 to 100 + */ + public SvgDocument rect(int x, int y, int width, int height, String fill, String stroke, int strokeWidth, + int opacity) { + elements.add(new Rect(x, y, width, height, Objects.requireNonNull(fill), Objects.requireNonNull(stroke), + strokeWidth, checkOpacity(opacity))); return this; } @@ -52,7 +66,63 @@ public SvgDocument rect(int x, int y, int width, int height, String fill, String * Adds a line of text whose start is at {@code x} and whose baseline is at {@code y}. */ public SvgDocument text(int x, int y, String content, int fontSize, String fill, boolean bold) { - elements.add(new Text(x, y, Objects.requireNonNull(content), fontSize, Objects.requireNonNull(fill), bold)); + elements.add(new Text(x, y, Objects.requireNonNull(content), fontSize, Objects.requireNonNull(fill), bold, + false)); + return this; + } + + /** + * Adds a line of text whose middle is at {@code x} and whose baseline is at {@code y}. + */ + public SvgDocument centredText(int x, int y, String content, int fontSize, String fill, boolean bold) { + elements.add(new Text(x, y, Objects.requireNonNull(content), fontSize, Objects.requireNonNull(fill), bold, + true)); + return this; + } + + /** + * Adds a circle. + */ + public SvgDocument circle(int centreX, int centreY, int radius, String fill, String stroke, int strokeWidth) { + elements.add(new Circle(centreX, centreY, radius, Objects.requireNonNull(fill), Objects.requireNonNull(stroke), + strokeWidth)); + return this; + } + + /** + * Adds an open line through the points, in the given order, without a fill. + */ + public SvgDocument polyline(List points, String stroke, int strokeWidth) { + elements.add(new Polyline(List.copyOf(points), Objects.requireNonNull(stroke), strokeWidth)); + return this; + } + + /** + * Defines a symbol: a group of pixels drawn at the origin of the symbol. The symbols are written in the order of + * their ids. + * + * @param id the id of the symbol, unique in the picture + * @param pixels the rectangles of the symbol, in symbol coordinates + * @throws IllegalStateException if a symbol with the same id exists + */ + public SvgDocument symbol(String id, List pixels) { + for (Symbol existing : symbols) { + if (existing.id().equals(id)) { + throw new IllegalStateException("the picture already defines the symbol " + id); + } + } + symbols.add(new Symbol(Objects.requireNonNull(id), List.copyOf(pixels))); + return this; + } + + /** + * Draws a symbol at a position. + * + * @param target the id of a symbol of this picture + * @param opacity the opacity in percent, from 0 to 100 + */ + public SvgDocument use(String target, int x, int y, int opacity) { + elements.add(new Use(Objects.requireNonNull(target), x, y, checkOpacity(opacity))); return this; } @@ -71,21 +141,71 @@ public List elements() { return List.copyOf(elements); } + /** + * @return the symbols in the order they were defined + */ + public List symbols() { + return List.copyOf(symbols); + } + + private static int checkOpacity(int opacity) { + if (opacity < 0 || opacity > 100) { + throw new IllegalArgumentException("an opacity is a percentage from 0 to 100, got " + opacity); + } + return opacity; + } + /** * One drawn element. */ - public sealed interface Element permits Rect, Text { + public sealed interface Element permits Rect, Text, Use, Polyline, Circle { + } + + /** + * A rectangle. A zero stroke width draws no outline; an opacity of 100 is the default and is not written. + */ + public record Rect(int x, int y, int width, int height, String fill, String stroke, int strokeWidth, + int opacity) implements Element { + } + + /** + * A line of sans-serif text, starting at {@code x} or centred on it. + */ + public record Text(int x, int y, String content, int fontSize, String fill, boolean bold, boolean centred) + implements Element { + } + + /** + * A drawn symbol. + */ + public record Use(String target, int x, int y, int opacity) implements Element { + } + + /** + * An open line through whole-number points. + */ + public record Polyline(List points, String stroke, int strokeWidth) implements Element { + } + + /** + * A circle. + */ + public record Circle(int centreX, int centreY, int radius, String fill, String stroke, int strokeWidth) + implements Element { } /** - * A rectangle. A zero stroke width draws no outline. + * A point with whole-number coordinates. */ - public record Rect(int x, int y, int width, int height, String fill, String stroke, int strokeWidth) implements Element { + public record Point(int x, int y) { } /** - * A line of sans-serif text. + * A symbol: the pixels that {@link SvgDocument#use} draws. + * + * @param id the id of the symbol + * @param pixels the rectangles of the symbol in symbol coordinates */ - public record Text(int x, int y, String content, int fontSize, String fill, boolean bold) implements Element { + public record Symbol(String id, List pixels) { } } diff --git a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/SvgWriter.java b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/SvgWriter.java index c1d8db18..ecc24f20 100644 --- a/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/SvgWriter.java +++ b/src/docsGen/java/net/onelitefeather/bettergopaint/docsgen/SvgWriter.java @@ -19,6 +19,7 @@ package net.onelitefeather.bettergopaint.docsgen; import java.nio.charset.StandardCharsets; +import java.util.List; import java.util.Locale; /** @@ -38,22 +39,67 @@ public static String write(SvgDocument document) { StringBuilder svg = new StringBuilder(); svg.append(format("\n", document.width(), document.height(), document.width(), document.height())); - for (SvgDocument.Element element : document.elements()) { - switch (element) { - case SvgDocument.Rect rect -> svg.append(format( - " \n", - rect.x(), rect.y(), rect.width(), rect.height(), escape(rect.fill()), escape(rect.stroke()), - rect.strokeWidth())); - case SvgDocument.Text text -> svg.append(format( - " %s\n", - text.x(), text.y(), text.fontSize(), text.bold() ? "bold" : "normal", escape(text.fill()), - escape(text.content()))); + if (!document.symbols().isEmpty()) { + svg.append("\n"); + for (SvgDocument.Symbol symbol : document.symbols()) { + svg.append(format(" \n", escape(symbol.id()))); + for (SvgDocument.Rect pixel : symbol.pixels()) { + svg.append(" ").append(rect(pixel)).append('\n'); + } + svg.append(" \n"); } + svg.append("\n"); + } + for (SvgDocument.Element element : document.elements()) { + svg.append(" ").append(switch (element) { + case SvgDocument.Rect rect -> rect(rect); + case SvgDocument.Text text -> text(text); + case SvgDocument.Use use -> format("", + escape(use.target()), use.x(), use.y(), opacity(use.opacity())); + case SvgDocument.Polyline line -> format( + "", + points(line.points()), escape(line.stroke()), line.strokeWidth()); + case SvgDocument.Circle circle -> format( + "", + circle.centreX(), circle.centreY(), circle.radius(), escape(circle.fill()), + escape(circle.stroke()), circle.strokeWidth()); + }).append('\n'); } svg.append("\n"); return svg.toString(); } + private static String rect(SvgDocument.Rect rect) { + return format("", + rect.x(), rect.y(), rect.width(), rect.height(), escape(rect.fill()), escape(rect.stroke()), + rect.strokeWidth(), opacity(rect.opacity())); + } + + private static String text(SvgDocument.Text text) { + return format("%s", + text.x(), text.y(), text.centred() ? " text-anchor=\"middle\"" : "", text.fontSize(), + text.bold() ? "bold" : "normal", escape(text.fill()), escape(text.content())); + } + + /** + * @return the opacity attribute, empty for a fully opaque element; the fraction has two digits, as {@code 0.60} + */ + private static String opacity(int percent) { + return percent == 100 ? "" : format(" opacity=\"%s\"", format("%.2f", percent / 100.0)); + } + + private static String points(List points) { + StringBuilder joined = new StringBuilder(); + for (SvgDocument.Point point : points) { + if (!joined.isEmpty()) { + joined.append(' '); + } + joined.append(format("%d,%d", point.x(), point.y())); + } + return joined.toString(); + } + /** * @param document the picture to write * @return the UTF-8 bytes of {@link #write(SvgDocument)} diff --git a/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BlockLooksTest.java b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BlockLooksTest.java new file mode 100644 index 00000000..4efb4dc9 --- /dev/null +++ b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BlockLooksTest.java @@ -0,0 +1,102 @@ +/* + * goPaint is designed to simplify painting inside of Minecraft. + * Copyright (C) Arcaniax-Development + * Copyright (C) Arcaniax team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.onelitefeather.bettergopaint.docsgen; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +class BlockLooksTest { + + @Test + @DisplayName("no two materials share a base colour") + void baseColoursAreDistinct() { + List colours = BlockLooks.materials().stream() + .map(material -> BlockLooks.look(material).base()) + .toList(); + + assertThat(Set.copyOf(colours)).as("the distinct base colours of the materials") + .hasSize(BlockLooks.materials().size()); + } + + @Test + @DisplayName("the legend name of a material is its Minecraft name") + void labelIsTheMinecraftName() { + assertThat(BlockLooks.look("GRASS_BLOCK").label()).as("the label of grass block").isEqualTo("Grass Block"); + assertThat(BlockLooks.look("GOLD_BLOCK").label()).as("the label of gold block").isEqualTo("Gold Block"); + } + + @Test + @DisplayName("a tile has 64 pixels and every pixel is a six digit hex colour") + void tileHasSixtyFourHexPixels() { + List pixels = BlockLooks.look("STONE").pixels(BlockLooks.Face.TOP); + + assertThat(pixels).as("the pixels of a stone tile").hasSize(64) + .allMatch(colour -> colour.matches("#[0-9a-f]{6}"), "a six digit lower case hex colour"); + } + + @Test + @DisplayName("the tile of a material is the same every time it is computed") + void tileIsDeterministic() { + assertThat(BlockLooks.look("STONE").pixels(BlockLooks.Face.TOP)) + .as("the tile of stone computed by a second look at the material") + .isEqualTo(BlockLooks.look("STONE").pixels(BlockLooks.Face.TOP)); + } + + @Test + @DisplayName("two materials never share a tile") + void tilesDifferBetweenMaterials() { + Set> tiles = new HashSet<>(); + for (String material : BlockLooks.materials()) { + tiles.add(BlockLooks.look(material).pixels(BlockLooks.Face.TOP)); + } + + assertThat(tiles).as("the distinct tiles of the materials").hasSize(BlockLooks.materials().size()); + } + + @Test + @DisplayName("the top row of a grass side is green and the bottom row is dirt") + void grassSideHasGreenStripOverDirt() { + List side = BlockLooks.look("GRASS_BLOCK").pixels(BlockLooks.Face.SIDE); + + for (int column = 0; column < 8; column++) { + assertThat(BlockLooks.Rgb.of(side.get(column)).green()) + .as("the green of pixel %d of the top row", column) + .isGreaterThan(BlockLooks.Rgb.of(side.get(column)).red()); + assertThat(BlockLooks.Rgb.of(side.get(56 + column)).red()) + .as("the red of pixel %d of the bottom row", column) + .isGreaterThan(BlockLooks.Rgb.of(side.get(56 + column)).green()); + } + } + + @Test + @DisplayName("a material without a look fails and names the material") + void unknownMaterialIsNamed() { + assertThatThrownBy(() -> BlockLooks.look("MAGMA_BLOCK")) + .as("the look of a material the generator does not know") + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MAGMA_BLOCK"); + } +} diff --git a/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BrushIllustrationTest.java b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BrushIllustrationTest.java new file mode 100644 index 00000000..04f41436 --- /dev/null +++ b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BrushIllustrationTest.java @@ -0,0 +1,115 @@ +/* + * goPaint is designed to simplify painting inside of Minecraft. + * Copyright (C) Arcaniax-Development + * Copyright (C) Arcaniax team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.onelitefeather.bettergopaint.docsgen; + +import net.onelitefeather.bettergopaint.brush.PlayerBrushManager; +import net.onelitefeather.bettergopaint.objects.brush.Brush; +import net.onelitefeather.bettergopaint.objects.brush.PaintBrush; +import net.onelitefeather.bettergopaint.objects.brush.SphereBrush; +import net.onelitefeather.bettergopaint.settings.SettingsHolder; +import net.onelitefeather.bettergopaint.support.Terrain; +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class BrushIllustrationTest { + + private final SettingsHolder settings = GeneratorFixtures.settings(); + private final PlayerBrushManager manager = GeneratorFixtures.brushManager(settings); + + private SvgDocument render(Class type) { + Brush brush = manager.getBrushes().stream().filter(type::isInstance).findFirst().orElseThrow(); + Terrain terrain = new Terrain(); + return BrushIllustration.render(BrushScenes.sceneOf(brush, terrain), BlockPalette.of(terrain)); + } + + private static List texts(SvgDocument document) { + return document.elements().stream() + .filter(SvgDocument.Text.class::isInstance) + .map(element -> ((SvgDocument.Text) element).content()) + .toList(); + } + + private static List uses(SvgDocument document) { + return document.elements().stream() + .filter(SvgDocument.Use.class::isInstance) + .map(SvgDocument.Use.class::cast) + .toList(); + } + + @Test + @DisplayName("the legend of the sphere picture names the Minecraft materials it shows") + void legendNamesTheRealMaterials() { + assertThat(texts(render(SphereBrush.class))).as("the texts of the sphere picture") + .contains("Grass Block", "Dirt", "Stone", "Andesite", "Cobblestone"); + } + + @Test + @DisplayName("the legend explains the changed, unchanged, air and clicked markers") + void legendExplainsTheMarkers() { + assertThat(texts(render(SphereBrush.class))).as("the texts of the sphere picture") + .contains("changed by the brush", "unchanged (faded)", "air", "clicked block"); + } + + @Test + @DisplayName("an unchanged grass block is drawn with the grass tile at reduced opacity") + void unchangedBlocksAreFaded() { + assertThat(uses(render(SphereBrush.class))).as("the tiles of the sphere picture") + .anyMatch(use -> use.target().startsWith("block-grass-block-top") && use.opacity() < 100); + } + + @Test + @DisplayName("a block placed by the brush is drawn at full opacity") + void changedBlocksAreNotFaded() { + assertThat(uses(render(SphereBrush.class))).as("the tiles of the sphere picture") + .anyMatch(use -> use.target().startsWith("block-stone-") && use.opacity() == 100); + } + + @Test + @DisplayName("air is drawn in the sky tint") + void airIsSkyTint() { + assertThat(render(SphereBrush.class).elements()).as("the rectangles of the sphere picture") + .anyMatch(element -> element instanceof SvgDocument.Rect rect && rect.fill().equals("#dbeafe")); + } + + @Test + @DisplayName("the clicked block is outlined in the picture") + void clickedBlockIsOutlined() { + assertThat(render(SphereBrush.class).elements()).as("the rectangles of the sphere picture") + .anyMatch(element -> element instanceof SvgDocument.Rect rect + && rect.stroke().equals("#111827") && rect.strokeWidth() == 2); + } + + @Test + @DisplayName("the paint picture numbers every control point from 1 to 6") + void paintPictureNumbersEveryControlPoint() { + assertThat(texts(render(PaintBrush.class))).as("the texts of the paint picture") + .contains("1", "2", "3", "4", "5", "6"); + } + + @Test + @DisplayName("the paint picture draws the stroke as a path through the control points") + void paintPictureDrawsThePath() { + assertThat(render(PaintBrush.class).elements()).as("the elements of the paint picture") + .anyMatch(element -> element instanceof SvgDocument.Polyline line && line.points().size() == 6); + } +} diff --git a/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BrushScenesTest.java b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BrushScenesTest.java index 74e23246..882da8f6 100644 --- a/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BrushScenesTest.java +++ b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/BrushScenesTest.java @@ -19,9 +19,12 @@ package net.onelitefeather.bettergopaint.docsgen; import com.sk89q.worldedit.extent.Extent; +import com.sk89q.worldedit.math.BlockVector3; import net.onelitefeather.bettergopaint.brush.PlayerBrushManager; import net.onelitefeather.bettergopaint.brush.StrokeContext; import net.onelitefeather.bettergopaint.objects.brush.Brush; +import net.onelitefeather.bettergopaint.objects.brush.GradientBrush; +import net.onelitefeather.bettergopaint.objects.brush.PaintBrush; import net.onelitefeather.bettergopaint.objects.brush.SphereBrush; import net.onelitefeather.bettergopaint.settings.SettingsHolder; import net.onelitefeather.bettergopaint.support.Terrain; @@ -29,6 +32,8 @@ import org.junit.jupiter.api.Test; import java.util.List; +import java.util.Set; +import java.util.stream.Collectors; import static org.assertj.core.api.Assertions.assertThat; import static org.assertj.core.api.Assertions.assertThatThrownBy; @@ -91,4 +96,50 @@ void sceneRendersAreEqual() { assertThat(secondSvg).as("the svg of the sphere scene rendered twice").isEqualTo(firstSvg); } + + @Test + @DisplayName("the sphere scene places stone, andesite and cobblestone") + void sphereSceneMixesThreeStoneFamilyBlocks() { + BrushScene scene = BrushScenes.sceneOf(brush(SphereBrush.class), terrain); + + assertThat(scene.changes()).as("the changes of the sphere scene") + .extracting(BrushScene.Change::after) + .contains(terrain.stone, terrain.andesite, terrain.cobblestone); + } + + @Test + @DisplayName("the gradient scene places stone, andesite and cobblestone") + void gradientSceneMixesThreeStoneFamilyBlocks() { + BrushScene scene = BrushScenes.sceneOf(brush(GradientBrush.class), terrain); + + assertThat(scene.changes()).as("the changes of the gradient scene") + .extracting(BrushScene.Change::after) + .contains(terrain.stone, terrain.andesite, terrain.cobblestone); + } + + @Test + @DisplayName("the paint scene has six control points and its last point is the clicked block") + void paintSceneHasSixControlPoints() { + BrushScene scene = BrushScenes.sceneOf(brush(PaintBrush.class), terrain); + + assertThat(scene.points()).as("the control points of the paint scene").hasSize(6); + assertThat(scene.points().getLast()).as("the last control point").isEqualTo(scene.target()); + } + + @Test + @DisplayName("the paint stroke fills less than half of its bounding box in the top view") + void paintStrokeIsAPathNotABlob() { + BrushScene scene = BrushScenes.sceneOf(brush(PaintBrush.class), terrain); + Set columns = scene.changes().stream() + .map(change -> BlockVector3.at(change.position().x(), 0, change.position().z())) + .collect(Collectors.toSet()); + int minX = columns.stream().mapToInt(BlockVector3::x).min().orElseThrow(); + int maxX = columns.stream().mapToInt(BlockVector3::x).max().orElseThrow(); + int minZ = columns.stream().mapToInt(BlockVector3::z).min().orElseThrow(); + int maxZ = columns.stream().mapToInt(BlockVector3::z).max().orElseThrow(); + double area = (maxX - minX + 1.0) * (maxZ - minZ + 1.0); + + assertThat(columns.size() / area).as("the share of the bounding box the paint stroke fills") + .isLessThan(0.5); + } } diff --git a/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/ItemIconsTest.java b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/ItemIconsTest.java new file mode 100644 index 00000000..4ae876e1 --- /dev/null +++ b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/ItemIconsTest.java @@ -0,0 +1,66 @@ +/* + * goPaint is designed to simplify painting inside of Minecraft. + * Copyright (C) Arcaniax-Development + * Copyright (C) Arcaniax team and contributors + * + * This program is free software: you can redistribute it and/or modify + * it under the terms of the GNU General Public License as published by + * the Free Software Foundation, either version 3 of the License, or + * (at your option) any later version. + * + * This program is distributed in the hope that it will be useful, + * but WITHOUT ANY WARRANTY; without even the implied warranty of + * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + * GNU General Public License for more details. + * + * You should have received a copy of the GNU General Public License + * along with this program. If not, see . + */ +package net.onelitefeather.bettergopaint.docsgen; + +import org.junit.jupiter.api.DisplayName; +import org.junit.jupiter.api.Test; + +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; + +class ItemIconsTest { + + @Test + @DisplayName("every pixel icon is an eight by eight grid whose colours are all defined") + void everyIconIsAnEightByEightGrid() { + for (String material : ItemIcons.materials()) { + ItemIcons.Icon icon = ItemIcons.icon(material); + + assertThat(icon.rows()).as("the rows of the icon of %s", material).hasSize(8) + .allMatch(row -> row.length() == 8, "eight pixels per row"); + for (String row : icon.rows()) { + for (char pixel : row.toCharArray()) { + boolean known = pixel == '.' || icon.colours().containsKey(pixel); + assertThat(known).as("the pixel %s of the icon of %s has a colour", pixel, material).isTrue(); + } + } + assertThat(icon.colours().values()).as("the colours of the icon of %s", material) + .allMatch(colour -> colour.matches("#[0-9a-f]{6}")); + } + } + + @Test + @DisplayName("the icons cover the non-cube items of the brush menu") + void iconsCoverTheMenuItems() { + assertThat(ItemIcons.materials()).as("the items with a pixel icon") + .containsAll(List.of("FEATHER", "PLAYER_HEAD", "GOLD_NUGGET", "BROWN_MUSHROOM", "CARVED_PUMPKIN", + "JACK_O_LANTERN", "LIGHT_WEIGHTED_PRESSURE_PLATE", "BARRIER", "DAYLIGHT_DETECTOR", "BLAZE_ROD")); + } + + @Test + @DisplayName("every stained glass pane of the menu has a tint and no block look") + void glassPanesAreTintedNotBlocks() { + for (String pane : List.of("WHITE_STAINED_GLASS_PANE", "GRAY_STAINED_GLASS_PANE", "LIME_STAINED_GLASS_PANE")) { + assertThat(ItemIcons.isGlassPane(pane)).as("%s is a glass pane", pane).isTrue(); + assertThat(ItemIcons.glassTint(pane)).as("the tint of %s", pane).matches("#[0-9a-f]{6}"); + assertThat(BlockLooks.has(pane)).as("%s has a block look", pane).isFalse(); + } + } +} diff --git a/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/MenuMapTest.java b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/MenuMapTest.java index 98a2ef9f..2860d8bc 100644 --- a/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/MenuMapTest.java +++ b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/MenuMapTest.java @@ -69,4 +69,70 @@ void unknownKeyFailsWithItsName() { .isInstanceOf(IllegalStateException.class) .hasMessageContaining("bettergopaint.menu.brush.name"); } + + @Test + @DisplayName("the callouts are numbered in slot order and cover exactly the functional slots") + void calloutsCoverTheFunctionalSlots() { + List cells = MenuMap.cells(MenuStates.sphereDefault(), EnglishText.fromClasspath()); + List functional = cells.stream() + .filter(cell -> !cell.purpose().isEmpty()) + .map(MenuMap.MenuCell::slot) + .toList(); + + List callouts = MenuMap.callouts(cells); + + assertThat(callouts).as("the slots of the callouts").extracting(MenuMap.Callout::slot) + .containsExactlyElementsOf(functional); + assertThat(callouts).as("the numbers of the callouts") + .extracting(MenuMap.Callout::number) + .containsExactlyElementsOf(IntStream.rangeClosed(1, functional.size()).boxed().toList()); + } + + @Test + @DisplayName("a filler pane gets no callout") + void fillerPaneHasNoCallout() { + List callouts = MenuMap.callouts(MenuMap.cells(MenuStates.sphereDefault(), EnglishText.fromClasspath())); + + assertThat(callouts).as("the slots of the callouts").extracting(MenuMap.Callout::slot).doesNotContain(0, 1, 3); + } + + @Test + @DisplayName("a block item is drawn with its block tile") + void blockItemIsDrawnWithItsTile() { + SvgDocument menu = MenuMap.render("title", MenuMap.cells(MenuStates.sphereDefault(), EnglishText.fromClasspath())); + + assertThat(menu.elements()).as("the tiles of the sphere menu") + .anyMatch(element -> element instanceof SvgDocument.Use use && use.target().startsWith("block-stone-")); + } + + @Test + @DisplayName("a non-block item is drawn with its pixel icon") + void nonBlockItemIsDrawnWithItsIcon() { + SvgDocument menu = MenuMap.render("title", MenuMap.cells(MenuStates.sphereDefault(), EnglishText.fromClasspath())); + + assertThat(menu.elements()).as("the tiles of the sphere menu") + .anyMatch(element -> element instanceof SvgDocument.Use use && use.target().startsWith("icon-feather")); + } + + @Test + @DisplayName("a filler glass pane is a tinted translucent tile") + void fillerGlassPaneIsTranslucent() { + SvgDocument menu = MenuMap.render("title", MenuMap.cells(MenuStates.sphereDefault(), EnglishText.fromClasspath())); + String tint = ItemIcons.glassTint("GRAY_STAINED_GLASS_PANE"); + + assertThat(menu.elements()).as("the rectangles of the sphere menu") + .anyMatch(element -> element instanceof SvgDocument.Rect rect + && rect.fill().equals(tint) && rect.opacity() < 100); + } + + @Test + @DisplayName("a menu item that is neither a block tile nor an icon fails and names its material") + void itemWithoutTileOrIconIsNamed() { + List cells = List.of(new MenuMap.MenuCell(0, "MAGMA_BLOCK", "Magma")); + + assertThatThrownBy(() -> MenuMap.render("title", cells)) + .as("the rendering of an item the generator cannot draw") + .isInstanceOf(IllegalStateException.class) + .hasMessageContaining("MAGMA_BLOCK"); + } } diff --git a/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/SvgWriterTest.java b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/SvgWriterTest.java index 5f4ef1a9..404c233b 100644 --- a/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/SvgWriterTest.java +++ b/src/docsGenTest/java/net/onelitefeather/bettergopaint/docsgen/SvgWriterTest.java @@ -26,6 +26,7 @@ import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; +import java.util.List; import java.util.Set; import java.util.regex.Matcher; import java.util.regex.Pattern; @@ -108,6 +109,95 @@ void utf8Bytes() { .isEqualTo(SvgWriter.write(document).getBytes(StandardCharsets.UTF_8)); } + @Test + @DisplayName("a symbol is written once in defs, before the drawing") + void symbolsAreWrittenInDefsBeforeTheDrawing() { + SvgDocument document = new SvgDocument(10, 10) + .symbol("tile", List.of(new SvgDocument.Rect(0, 0, 3, 3, "#7d7d7d", "none", 0, 100))) + .rect(0, 0, 10, 10, "#ffffff", "none", 0); + + String svg = SvgWriter.write(document); + + assertThat(svg).as("the svg with one symbol and one rectangle") + .contains("\n \n \n \n\n") + .containsOnlyOnce(""); + assertThat(svg.indexOf("")).as("the position of defs").isLessThan(svg.indexOf("\n") + .contains(" \n"); + } + + @Test + @DisplayName("a rectangle with less than full opacity writes its opacity as a fraction") + void fadedRectangleWritesOpacity() { + String svg = SvgWriter.write(new SvgDocument(40, 40).rect(1, 2, 10, 12, "#866043", "none", 0, 35)); + + assertThat(svg).as("the faded rectangle").contains("stroke-width=\"0\" opacity=\"0.35\"/>"); + } + + @Test + @DisplayName("a polyline writes its points as integer pairs and draws no fill") + void polylineWritesIntegerPoints() { + String svg = SvgWriter.write(new SvgDocument(40, 40).polyline( + List.of(new SvgDocument.Point(12, 12), new SvgDocument.Point(36, 24)), "#111827", 2)); + + assertThat(svg).as("the polyline of the svg") + .contains(""); + } + + @Test + @DisplayName("a circle writes its centre and radius as integers") + void circleWritesIntegerGeometry() { + String svg = SvgWriter.write(new SvgDocument(40, 40).circle(20, 16, 8, "#ffffff", "#111827", 2)); + + assertThat(svg).as("the circle of the svg") + .contains(""); + } + + @Test + @DisplayName("centred text is anchored in the middle") + void centredTextIsAnchoredInTheMiddle() { + String svg = SvgWriter.write(new SvgDocument(40, 40).centredText(20, 30, "10", 11, "#111827", true)); + + assertThat(svg).as("the centred text of the svg") + .contains("10"); + } + + @Test + @DisplayName("the coordinates of the new elements are integers") + void newElementsHaveIntegerGeometry() { + String svg = SvgWriter.write(new SvgDocument(40, 40) + .symbol("tile", List.of(new SvgDocument.Rect(1, 2, 3, 4, "#000000", "none", 0, 100))) + .use("tile", 5, 6, 100) + .polyline(List.of(new SvgDocument.Point(7, 8), new SvgDocument.Point(9, 10)), "#000000", 1) + .circle(11, 12, 13, "#000000", "none", 0) + .centredText(14, 15, "1", 11, "#000000", false)); + + Matcher matcher = ATTRIBUTE.matcher(svg); + int checked = 0; + while (matcher.find()) { + if (Set.of("x", "y", "width", "height", "cx", "cy", "r").contains(matcher.group(1))) { + checked++; + assertThat(matcher.group(2)).as("the value of %s", matcher.group(1)).matches("-?\\d+"); + } else if (matcher.group(1).equals("points")) { + checked++; + assertThat(matcher.group(2)).as("the points").matches("-?\\d+,-?\\d+( -?\\d+,-?\\d+)*"); + } + } + assertThat(checked).as("the geometry attributes of the new elements").isEqualTo(14); + } + @Test @DisplayName("writing a document creates no file in the folder") void writesNothingToDisk() throws IOException { diff --git a/src/test/java/net/onelitefeather/bettergopaint/support/Terrain.java b/src/test/java/net/onelitefeather/bettergopaint/support/Terrain.java index 206d68a6..fb3b2f83 100644 --- a/src/test/java/net/onelitefeather/bettergopaint/support/Terrain.java +++ b/src/test/java/net/onelitefeather/bettergopaint/support/Terrain.java @@ -39,6 +39,7 @@ public final class Terrain { public final BlockType grass = TestBlocks.solid("grass"); public final BlockType dirt = TestBlocks.solid("dirt"); public final BlockType andesite = TestBlocks.solid("andesite"); + public final BlockType cobblestone = TestBlocks.solid("cobblestone"); public final BlockType gold = TestBlocks.solid("gold"); public MemoryExtent empty() {