Skip to content

Commit dbc7d8c

Browse files
committed
feat: Handle map scale and cropping for cell coordinates
1 parent 814f837 commit dbc7d8c

11 files changed

Lines changed: 273 additions & 60 deletions

File tree

index.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ function run(string $path, array $get, Closure $sender): void {
327327
$map = $dmp->load($mapId);
328328

329329
$triggers = array_map(function ($trigger) use ($map) {
330-
$cell = CellShape::fromCellId($map, (int)$trigger['CELL_ID']);
330+
$cell = CellShape::fromCellId($map, (int)$trigger['CELL_ID'])->toDisplayPosition($map);
331331

332332
return [
333333
'x' => $cell->x,

src/Renderer/CellShape.php

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44

55
use Arakne\MapParser\Loader\Map;
66
use Arakne\MapParser\Parser\Cell;
7+
use Arakne\MapParser\Tile\Coordinate\Point;
78

89
/**
910
* A cell with position in pixel
@@ -25,8 +26,38 @@ private function __construct(
2526
* Base cell object
2627
*/
2728
public Cell $data,
29+
30+
/**
31+
* The map this cell belongs to
32+
*/
33+
private Map $map,
2834
) {}
2935

36+
/**
37+
* Get the pixel coordinates of the cell on the display image
38+
*
39+
* This method takes in account the scaling + cropping of the final image
40+
* if its dimensions are different from the default ones.
41+
*
42+
* Null will be returned if the cell is outside the display area.
43+
*
44+
* @return Point|null The pixel coordinates on the display image, or null if outside
45+
*/
46+
public function toDisplayPosition(): ?Point
47+
{
48+
$point = MapScale::for($this->map)->applyToCoordinates($this->x, $this->y);
49+
50+
if ($point->x < 0
51+
|| $point->x > MapRenderer::DISPLAY_WIDTH
52+
|| $point->y < 0
53+
|| $point->y > MapRenderer::DISPLAY_HEIGHT
54+
) {
55+
return null;
56+
}
57+
58+
return $point;
59+
}
60+
3061
/**
3162
* Parse a single cell data to cell shape from its cell id
3263
*
@@ -54,7 +85,7 @@ public static function fromCellId(Map $map, int $cellId): ?self
5485
$x = (int) ($column * MapRenderer::CELL_WIDTH + $subLine * MapRenderer::CELL_HALF_WIDTH);
5586
$y = (int) ($line * MapRenderer::CELL_HEIGHT + $subLine * MapRenderer::CELL_HALF_HEIGHT - MapRenderer::LEVEL_HEIGHT * ($cell->ground->level - 7));
5687

57-
return new self($x, $y, $cell);
88+
return new self($x, $y, $cell, $map);
5889
}
5990

6091
/**
@@ -94,7 +125,7 @@ public static function fromMap(Map $map, bool $ignoreInactive = true): array
94125
$y = (int) ($_loc10 * MapRenderer::CELL_HALF_HEIGHT - MapRenderer::LEVEL_HEIGHT * ($cell->ground->level - 7));
95126

96127
if (!$ignoreInactive || $cell->active) {
97-
$shapes[] = new CellShape($x, $y, $cell);
128+
$shapes[] = new CellShape($x, $y, $cell, $map);
98129
}
99130
}
100131

src/Renderer/MapRenderer.php

Lines changed: 1 addition & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -10,10 +10,7 @@
1010
use GdImage;
1111
use Override;
1212

13-
use function assert;
1413
use function imagecreatetruecolor;
15-
use function imagescale;
16-
use function imagesx;
1714

1815
/**
1916
* Base dofus map renderer
@@ -84,55 +81,9 @@ public function render(Map $map): GdImage
8481
}
8582

8683
if ($hasCustomSize) {
87-
$img = $this->rescaleMap($map, $img);
84+
$img = MapScale::for($map)->applyToImage($img);
8885
}
8986

9087
return $img;
9188
}
92-
93-
/**
94-
* Resize the map to fit in the display area
95-
*
96-
* @param Map $map
97-
* @param GdImage $img
98-
*
99-
* @return GdImage
100-
*
101-
* @see https://github.com/Emudofus/Dofus/blob/1.29/ank/battlefield/mc/Container.as#L154
102-
*/
103-
private function rescaleMap(Map $map, GdImage $img): GdImage
104-
{
105-
$actualWidth = imagesx($img);
106-
$actualHeight = imagesy($img);
107-
108-
// Scaling is only applied if both dimensions are greater than the default size
109-
// Otherwise, the map is displayed at its original size and simply cropped/centered
110-
if ($map->height > self::DEFAULT_HEIGHT && $map->width > self::DEFAULT_WIDTH) {
111-
$scale = $map->height > $map->width
112-
? self::DISPLAY_WIDTH / (($map->width - 1) * self::CELL_WIDTH)
113-
: self::DISPLAY_HEIGHT / (($map->height - 1) * self::CELL_HEIGHT)
114-
;
115-
116-
$actualWidth = (int) (($map->width - 1) * self::CELL_WIDTH * $scale);
117-
$actualHeight = (int) (($map->height - 1) * self::CELL_HEIGHT * $scale);
118-
119-
$img = imagescale($img, $actualWidth, $actualHeight);
120-
assert($img !== false);
121-
}
122-
123-
// Map has the correct size, no need to crop
124-
if ($actualWidth === self::DISPLAY_WIDTH && $actualHeight === self::DISPLAY_HEIGHT) {
125-
return $img;
126-
}
127-
128-
$result = imagecreatetruecolor(self::DISPLAY_WIDTH, self::DISPLAY_HEIGHT);
129-
assert($result !== false);
130-
131-
$offsetX = (self::DISPLAY_WIDTH - $actualWidth) / 2;
132-
$offsetY = (self::DISPLAY_HEIGHT - $actualHeight) / 2;
133-
134-
imagecopy($result, $img, (int) $offsetX, (int) $offsetY, 0, 0, $actualWidth, $actualHeight);
135-
136-
return $result;
137-
}
13889
}

src/Renderer/MapScale.php

Lines changed: 137 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,137 @@
1+
<?php
2+
3+
namespace Arakne\MapParser\Renderer;
4+
5+
use Arakne\MapParser\Loader\Map;
6+
use Arakne\MapParser\Tile\Coordinate\Point;
7+
use GdImage;
8+
9+
use function assert;
10+
use function imagecopyresampled;
11+
use function imagecreatetruecolor;
12+
use function imagesx;
13+
use function imagesy;
14+
15+
/**
16+
* Handle the scaling and offset to apply to a map when rendering it
17+
*/
18+
final readonly class MapScale
19+
{
20+
public function __construct(
21+
/**
22+
* The scaling factor to apply to the map (1.0 = 100%)
23+
*/
24+
public float $scale,
25+
26+
/**
27+
* The horizontal offset to apply to the map in pixels
28+
* This offset can be negative if the map is wider than the display area
29+
*/
30+
public int $offsetX,
31+
32+
/**
33+
* The vertical offset to apply to the map in pixels
34+
* This offset can be negative if the map is taller than the display area
35+
*/
36+
public int $offsetY,
37+
38+
/**
39+
* The actual width of the map after scaling, in pixels, but before cropping
40+
*
41+
* @var non-negative-int
42+
*/
43+
public int $scaledWidth,
44+
45+
/**
46+
* The actual height of the map after scaling, in pixels, but before cropping
47+
*
48+
* @var non-negative-int
49+
*/
50+
public int $scaledHeight,
51+
) {}
52+
53+
/**
54+
* Transform the given pixel coordinates by applying the scaling + offset
55+
*
56+
* @param int $x
57+
* @param int $y
58+
*
59+
* @return Point
60+
*/
61+
public function applyToCoordinates(int $x, int $y): Point
62+
{
63+
return new Point(
64+
(int) ($x * $this->scale) + $this->offsetX,
65+
(int) ($y * $this->scale) + $this->offsetY,
66+
);
67+
}
68+
69+
/**
70+
* Resize and crop the given image to fit in the display area
71+
*
72+
* @param GdImage $img The original map image
73+
* @return GdImage The resized and cropped image
74+
*/
75+
public function applyToImage(GdImage $img): GdImage
76+
{
77+
$result = imagecreatetruecolor(MapRenderer::DISPLAY_WIDTH, MapRenderer::DISPLAY_HEIGHT);
78+
assert($result !== false);
79+
80+
imagecopyresampled(
81+
$result,
82+
$img,
83+
$this->offsetX,
84+
$this->offsetY,
85+
0,
86+
0,
87+
$this->scaledWidth,
88+
$this->scaledHeight,
89+
imagesx($img),
90+
imagesy($img),
91+
);
92+
93+
return $result;
94+
}
95+
96+
/**
97+
* Get the scaling + offset to apply to a map of arbitrary size to fit in the display area
98+
*
99+
* @param Map $map The map to compute the scale for
100+
*
101+
* @see https://github.com/Emudofus/Dofus/blob/1.29/ank/battlefield/mc/Container.as#L154
102+
*/
103+
public static function for(Map $map): MapScale
104+
{
105+
if ($map->height === MapRenderer::DEFAULT_HEIGHT && $map->width === MapRenderer::DEFAULT_WIDTH) {
106+
return new MapScale(1.0, 0, 0, MapRenderer::DISPLAY_WIDTH, MapRenderer::DISPLAY_HEIGHT);
107+
}
108+
109+
// Scaling is only applied if both dimensions are greater than the default size
110+
// Otherwise, the map is displayed at its original size and simply cropped/centered
111+
if ($map->height > MapRenderer::DEFAULT_HEIGHT && $map->width > MapRenderer::DEFAULT_WIDTH) {
112+
$scale = $map->height > $map->width
113+
? MapRenderer::DISPLAY_WIDTH / (($map->width - 1) * MapRenderer::CELL_WIDTH)
114+
: MapRenderer::DISPLAY_HEIGHT / (($map->height - 1) * MapRenderer::CELL_HEIGHT)
115+
;
116+
117+
$actualWidth = (int) (($map->width - 1) * MapRenderer::CELL_WIDTH * $scale);
118+
$actualHeight = (int) (($map->height - 1) * MapRenderer::CELL_HEIGHT * $scale);
119+
} else {
120+
$scale = 1.0;
121+
$actualWidth = ($map->width - 1) * MapRenderer::CELL_WIDTH;
122+
$actualHeight = ($map->height - 1) * MapRenderer::CELL_HEIGHT;
123+
}
124+
125+
// Map has the correct size, no need to crop
126+
if ($actualWidth === MapRenderer::DISPLAY_WIDTH && $actualHeight === MapRenderer::DISPLAY_HEIGHT) {
127+
return new MapScale($scale, 0, 0, $actualWidth, $actualHeight);
128+
}
129+
130+
assert($actualWidth >= 0 && $actualHeight >= 0);
131+
132+
$offsetX = (MapRenderer::DISPLAY_WIDTH - $actualWidth) / 2;
133+
$offsetY = (MapRenderer::DISPLAY_HEIGHT - $actualHeight) / 2;
134+
135+
return new MapScale($scale, (int) $offsetX, (int) $offsetY, $actualWidth, $actualHeight);
136+
}
137+
}

src/Tile/Coordinate/CoordinateSystem.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -134,7 +134,7 @@ public function cellToLatLong(Map $map, int $cellId): ?LatLong
134134
*/
135135
public function cellToPoint(Map $map, int $cellId): ?Point
136136
{
137-
$cell = CellShape::fromCellId($map, $cellId);
137+
$cell = CellShape::fromCellId($map, $cellId)?->toDisplayPosition();
138138

139139
if (!$cell) {
140140
return null;

tests/Renderer/CellShapeTest.php

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@
66
use Arakne\MapParser\Loader\MapLoader;
77
use Arakne\MapParser\Loader\MapStructure;
88
use Arakne\MapParser\Renderer\CellShape;
9+
use Arakne\MapParser\Tile\Coordinate\Point;
910
use Arakne\Swf\SwfFile;
1011
use PHPUnit\Framework\Attributes\Test;
1112
use PHPUnit\Framework\Attributes\TestWith;
@@ -60,4 +61,48 @@ public function fromCellIdXY()
6061
$this->assertSame(583, CellShape::fromCellId($map, 388)->x);
6162
$this->assertSame(351, CellShape::fromCellId($map, 388)->y);
6263
}
64+
65+
#[Test]
66+
public function toDisplayPositionOnMapWithDefaultSizeShouldBeIdenticalToXY()
67+
{
68+
$mapStructure = MapStructure::fromSwfFile(new SwfFile(__DIR__ . '/../_files/10302_0709271842X.swf'));
69+
$map = new MapLoader()->load($mapStructure, MapKey::fromFile(__DIR__ . '/../_files/10302.key'));
70+
71+
$cells = CellShape::fromMap($map);
72+
73+
foreach ($cells as $cellId => $cell) {
74+
$this->assertSame($cell->x, $cell->toDisplayPosition()->x, "Cell $cellId x");
75+
$this->assertSame($cell->y, $cell->toDisplayPosition()->y, "Cell $cellId y");
76+
}
77+
}
78+
79+
#[Test]
80+
public function toDisplayPositionOnBiggerMap()
81+
{
82+
$mapStructure = MapStructure::fromSwfFile(new SwfFile(__DIR__ . '/../_files/4208_0706131721X.swf'));
83+
$map = new MapLoader()->load($mapStructure, MapKey::fromFile(__DIR__ . '/../_files/4208.key'));
84+
85+
$cells = CellShape::fromMap($map, false);
86+
87+
$this->assertNull($cells[0]->toDisplayPosition());
88+
$this->assertNull($cells[777]->toDisplayPosition());
89+
$this->assertNull($cells[780]->toDisplayPosition());
90+
91+
$this->assertNotEquals($cells[430]->x, $cells[430]->toDisplayPosition()->x);
92+
$this->assertNotEquals($cells[430]->y, $cells[430]->toDisplayPosition()->y);
93+
$this->assertEquals(new Point(185, 237), $cells[430]->toDisplayPosition());
94+
}
95+
96+
#[Test]
97+
public function toDisplayPositionOnSmallerMap()
98+
{
99+
$mapStructure = MapStructure::fromSwfFile(new SwfFile(__DIR__ . '/../_files/703_0706131721X.swf'));
100+
$map = new MapLoader()->load($mapStructure, MapKey::fromFile(__DIR__ . '/../_files/703.key'));
101+
102+
$cells = CellShape::fromMap($map, false);
103+
104+
$this->assertNotEquals($cells[56]->x, $cells[56]->toDisplayPosition()->x);
105+
$this->assertNotEquals($cells[56]->y, $cells[56]->toDisplayPosition()->y);
106+
$this->assertEquals(new Point(370, 229), $cells[56]->toDisplayPosition());
107+
}
63108
}

tests/Renderer/MapRenderTest.php

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -83,13 +83,13 @@ public function renderBiggerDimensions()
8383

8484
$img = $this->renderer->render($map);
8585

86-
imagepng($img, __DIR__ . '/_files/37.png');
86+
imagepng($img, __DIR__ . '/_files/render.png');
8787

8888
$this->assertEquals(MapRenderer::DISPLAY_HEIGHT, imagesy($img));
8989
$this->assertEquals(MapRenderer::DISPLAY_WIDTH, imagesx($img));
9090

91-
$this->assertImages(__DIR__.'/_files/4208.png', __DIR__ . '/_files/37.png');
92-
unlink(__DIR__ . '/_files/37.png');
91+
$this->assertImages(__DIR__.'/_files/4208.png', __DIR__ . '/_files/render.png');
92+
unlink(__DIR__ . '/_files/render.png');
9393
}
9494

9595
#[Test]

0 commit comments

Comments
 (0)