Images are uploaded to a content-addressed blob endpoint and stored as data/textures/<sha256>.<ext>; the project only keeps the file name. The sprite points at a texture asset through an ordinary property of the new kind `asset`, so instance overrides, "Apply to container", "Reset to asset", undo and `Set property` work without any extra code. The new kind also replaces the ad-hoc asset picker the graph nodes faked as an enum. Import goes through + -> Texture, drag & drop onto the canvas or Ctrl+V. Images wider than 2048px are downscaled client-side and re-encoded to WebP if needed, because nginx caps uploads at 1MB; an image that already fits is uploaded byte for byte so pixel art is never resampled. The sprite colour default changes from red to white, since colour tints the texture multiplicatively and a tint of white shows an image as it is. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
54 lines
1.6 KiB
PHP
54 lines
1.6 KiB
PHP
<?php
|
||
declare(strict_types=1);
|
||
|
||
/** JSON response helpers. Every API answer goes through here. */
|
||
final class Response
|
||
{
|
||
public static function json(mixed $data, int $status = 200): never
|
||
{
|
||
http_response_code($status);
|
||
header('Content-Type: application/json; charset=utf-8');
|
||
header('Cache-Control: no-store');
|
||
echo json_encode($data, JSON_UNESCAPED_SLASHES | JSON_UNESCAPED_UNICODE);
|
||
exit;
|
||
}
|
||
|
||
public static function error(string $message, int $status, array $extra = []): never
|
||
{
|
||
self::json(['error' => $message] + $extra, $status);
|
||
}
|
||
|
||
public static function noContent(): never
|
||
{
|
||
http_response_code(204);
|
||
header('Cache-Control: no-store');
|
||
exit;
|
||
}
|
||
|
||
/** Decoded JSON request body or an error response when it is not an object. */
|
||
public static function body(): array
|
||
{
|
||
$data = json_decode(self::rawBody(), true);
|
||
if (!is_array($data)) {
|
||
self::error('Request body must be a JSON object', 400);
|
||
}
|
||
return $data;
|
||
}
|
||
|
||
/**
|
||
* The request body as bytes (uploads send images, not JSON).
|
||
*
|
||
* An empty body is answered here rather than by every caller, because PHP also reports
|
||
* a body that exceeded `post_max_size` as empty – a truncated upload must not look like
|
||
* a malformed one.
|
||
*/
|
||
public static function rawBody(): string
|
||
{
|
||
$raw = file_get_contents('php://input');
|
||
if ($raw === false || $raw === '') {
|
||
self::error('Request body is empty or exceeds the upload limit', 400);
|
||
}
|
||
return $raw;
|
||
}
|
||
}
|