// SPDX-License-Identifier: MIT// OpenZeppelin Contracts (last updated v5.1.0) (utils/Base64.sol)pragmasolidity ^0.8.20;/**
* @dev Provides a set of functions to operate with Base64 strings.
*/libraryBase64{
/**
* @dev Base64 Encoding/Decoding Table
* See sections 4 and 5 of https://datatracker.ietf.org/doc/html/rfc4648
*/stringinternalconstant _TABLE ="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
stringinternalconstant _TABLE_URL ="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-_";
/**
* @dev Converts a `bytes` to its Bytes64 `string` representation.
*/functionencode(bytesmemory data) internalpurereturns (stringmemory) {
return _encode(data, _TABLE, true);
}
/**
* @dev Converts a `bytes` to its Bytes64Url `string` representation.
* Output is not padded with `=` as specified in https://www.rfc-editor.org/rfc/rfc4648[rfc4648].
*/functionencodeURL(bytesmemory data) internalpurereturns (stringmemory) {
return _encode(data, _TABLE_URL, false);
}
/**
* @dev Internal table-agnostic conversion
*/function_encode(bytesmemory data, stringmemory table, bool withPadding) privatepurereturns (stringmemory) {
/**
* Inspired by Brecht Devos (Brechtpd) implementation - MIT licence
* https://github.com/Brechtpd/base64/blob/e78d9fd951e7b0977ddca77d92dc85183770daf4/base64.sol
*/if (data.length==0) return"";
// If padding is enabled, the final length should be `bytes` data length divided by 3 rounded up and then// multiplied by 4 so that it leaves room for padding the last chunk// - `data.length + 2` -> Prepare for division rounding up// - `/ 3` -> Number of 3-bytes chunks (rounded up)// - `4 *` -> 4 characters for each chunk// This is equivalent to: 4 * Math.ceil(data.length / 3)//// If padding is disabled, the final length should be `bytes` data length multiplied by 4/3 rounded up as// opposed to when padding is required to fill the last chunk.// - `4 * data.length` -> 4 characters for each chunk// - ` + 2` -> Prepare for division rounding up// - `/ 3` -> Number of 3-bytes chunks (rounded up)// This is equivalent to: Math.ceil((4 * data.length) / 3)uint256 resultLength = withPadding ? 4* ((data.length+2) /3) : (4* data.length+2) /3;
stringmemory result =newstring(resultLength);
assembly ("memory-safe") {
// Prepare the lookup table (skip the first "length" byte)let tablePtr :=add(table, 1)
// Prepare result pointer, jump over lengthlet resultPtr :=add(result, 0x20)
let dataPtr := data
let endPtr :=add(data, mload(data))
// In some cases, the last iteration will read bytes after the end of the data. We cache the value, and// set it to zero to make sure no dirty bytes are read in that section.let afterPtr :=add(endPtr, 0x20)
let afterCache :=mload(afterPtr)
mstore(afterPtr, 0x00)
// Run over the input, 3 bytes at a timefor {
} lt(dataPtr, endPtr) {
} {
// Advance 3 bytes
dataPtr :=add(dataPtr, 3)
let input :=mload(dataPtr)
// To write each character, shift the 3 byte (24 bits) chunk// 4 times in blocks of 6 bits for each character (18, 12, 6, 0)// and apply logical AND with 0x3F to bitmask the least significant 6 bits.// Use this as an index into the lookup table, mload an entire word// so the desired character is in the least significant byte, and// mstore8 this least significant byte into the result and continue.mstore8(resultPtr, mload(add(tablePtr, and(shr(18, input), 0x3F))))
resultPtr :=add(resultPtr, 1) // Advancemstore8(resultPtr, mload(add(tablePtr, and(shr(12, input), 0x3F))))
resultPtr :=add(resultPtr, 1) // Advancemstore8(resultPtr, mload(add(tablePtr, and(shr(6, input), 0x3F))))
resultPtr :=add(resultPtr, 1) // Advancemstore8(resultPtr, mload(add(tablePtr, and(input, 0x3F))))
resultPtr :=add(resultPtr, 1) // Advance
}
// Reset the value that was cachedmstore(afterPtr, afterCache)
if withPadding {
// When data `bytes` is not exactly 3 bytes long// it is padded with `=` characters at the endswitchmod(mload(data), 3)
case1 {
mstore8(sub(resultPtr, 1), 0x3d)
mstore8(sub(resultPtr, 2), 0x3d)
}
case2 {
mstore8(sub(resultPtr, 1), 0x3d)
}
}
}
return result;
}
}
// SPDX-License-Identifier: MITpragmasolidity ^0.8.25;import"./IERC721.sol";
import"./ERC721Events.sol";
import"./IERC721TokenReceiver.sol";
import"../../canvas/Canvas.sol";
import"@openzeppelin/contracts/utils/Base64.sol";
import"../../lib/StringConverter.sol";
import"../../lib/String.sol";
uint32constant MAX_MINTED_TOKENS_COUNT =1024;
abstractcontractERC721TokenisIERC721, Canvas{
mapping(uint256 tokenId =>address) public ownerOf;
mapping(uint256=>address) private _nftApprovals;
mapping(address=>uint32[]) public ownedNFTs;
mapping(address account =>uint count) public balanceOfNft;
mapping(uint32=>uint256) private idToIndex;
mapping(address=>mapping(address=>bool)) private _operatorApprovals;
uint32public minted; // number of ID mintsuint32public mint_seed;
errorUnsupportedReceiver();
constructor() {
// mint picture
minted =1;
ERC721Events.emitTransfer(address(0), address(this), 1);
ownerOf[1] =address(this);
}
functionpool_address() publicviewvirtualreturns (address);
// Handles the transfer of an ERC721 token, ensuring proper ownership and event emissionfunction_transfer_erc721(addressfrom,
address to,
uint32 tokenId
) internal{
if (to == pool_address()) {
_burn_erc721(from, tokenId);
return;
}
_transfer_erc721_internal(from, to, tokenId);
}
function_transfer_erc721_internal(addressfrom,
address to,
uint32 tokenId
) private{
require(from== ownerOf[tokenId], "ERC721: Incorrect owner"); // Ensure 'from' is the current ownerdelete _nftApprovals[tokenId]; // Clear any approvals for this token
ownerOf[tokenId] = to; // Transfer ownership of the token to 'to'
(uint x, uint y) = nft_id_to_pixel(tokenId);
_set_pixel_owner(x, y, to);
_updateOwnedNFTs(from, to, tokenId, false); // Update ownership tracking structures
ERC721Events.emitTransfer(from, to, tokenId); // Emit an ERC721 transfer event
}
function_burn_erc721(addressfrom, uint32 tokenId) internal{
require(from== ownerOf[tokenId], "ERC721: Incorrect owner"); // Ensure 'from' is the current ownerdelete _nftApprovals[tokenId]; // Clear any approvals for this token
ownerOf[tokenId] =address(0); // Transfer ownership of the token to 'to'
(uint x, uint y) = nft_id_to_pixel(tokenId);
_set_pixel_owner(x, y, address(0));
_burn_pixel(x, y);
_updateOwnedNFTs(from, address(0), tokenId, true); // Update ownership tracking structures
ERC721Events.emitTransfer(from, address(0), tokenId); // Emit an ERC721 transfer event
}
// erc721functionsafeTransferFrom(addressfrom,
address to,
uint256 tokenId
) publicpayablevirtual;
// erc721functionsafeTransferFrom(addressfrom,
address to,
uint256 tokenId,
bytesmemory data
) publicpayablevirtual;
functiongetApproved(uint256 tokenId) publicviewreturns (address) {
if (ownerOf[tokenId] ==address(0)) revert();
return _nftApprovals[tokenId];
}
functionisApprovedForAll(address owner,
address operator
) publicviewoverridereturns (bool) {
return _operatorApprovals[owner][operator];
}
functionsetApprovalForAll(address operator,
bool approved
) publicoverride{
_operatorApprovals[msg.sender][operator] = approved;
ERC721Events.emitApprovalForAll(msg.sender, operator, approved);
}
// payable removed for erc20 etherscan compatibilityfunctionapprove_erc721(address spender,
uint256 tokenId
) internalreturns (bool) {
address owner = ownerOf[tokenId]; // getting the owner of token ID via the `amount` inputif (owner ==address(0)) returnfalse;
if (msg.sender!= owner &&!isApprovedForAll(owner, msg.sender))
revert("TOKEN: You are not approved");
_nftApprovals[tokenId] = spender; // calling nft approval for the token and spender
ERC721Events.emitApproval(owner, spender, tokenId);
returntrue;
}
functionpicture_pixels_state() internalviewreturns (stringmemory) {
returnstring(
abi.encodePacked(
StringConverter.to_string(minted >0 ? minted -1 : minted),
"/",
StringConverter.to_string(MAX_MINTED_TOKENS_COUNT)
)
);
}
functionpicture_meta() privateviewreturns (bytesmemory) {
returnabi.encodePacked(
'{"name": "Shit Painting", "description": "Own a piece of Doge\'s shit on the blockchain. A piece of pure decentralized art online that no one can take down or steal from its collective owners.", "attributes": [{"trait_type": "Pixels", "value": "',
picture_pixels_state(),
'"}], "image": "data:image/svg+xml;base64,',
Base64.encode(bytes(get_svg())),
'"}'
);
}
functionpixel_meta(uint32 tokenId) privateviewreturns (bytesmemory) {
(uint x, uint y) = nft_id_to_pixel(tokenId);
PixelData memory data = get_pixel_data(x, y);
returnabi.encodePacked(
abi.encodePacked(
'{"name": "',
pixel_string_name(data),
" #",
StringConverter.to_string(tokenId),
'", "description": "',
pixel_string_description(data)
),
'", "attributes": [{"trait_type": "Color", "value": "#',
data.pixel.color,
'"},',
'{"trait_type": "X", "value": "',
StringConverter.to_string(x),
'"},',
'{"trait_type": "Y", "value": "',
StringConverter.to_string(y),
'"}',
'], "image": "data:image/svg+xml;base64,',
Base64.encode(bytes(get_pixel_svg(data.pixel.color))),
'"}'
);
}
functionpixel_string_description(
PixelData memory data
) privatepurereturns (bytesmemory) {
if (
StringLib.equals(data.pixel.color, "FFD777") ||
StringLib.equals(data.pixel.color, "EBA61E")
) return"This is a piece of Doge.";
if (
StringLib.equals(data.pixel.color, "7B542B") ||
StringLib.equals(data.pixel.color, "583231")
) return"This is a piece of shit.";
if (
StringLib.equals(data.pixel.color, "E1D293") ||
StringLib.equals(data.pixel.color, "E1d293 ") ||
StringLib.equals(data.pixel.color, "F2E29d ")
) return"This is a piece of sand.";
if (StringLib.equals(data.pixel.color, "B98552"))
return"This is a piece of Doge's penis.";
if (StringLib.equals(data.pixel.color, "6D570D"))
return"This is a piece of Doge's foot.";
if (StringLib.equals(data.pixel.color, "82BDD0"))
return"This is a piece of sky.";
return"Piece";
}
functionpixel_string_name(
PixelData memory data
) privatepurereturns (bytesmemory) {
if (
StringLib.equals(data.pixel.color, "FFD777") ||
StringLib.equals(data.pixel.color, "EBA61E")
) return"Piece of Doge";
if (
StringLib.equals(data.pixel.color, "7B542B") ||
StringLib.equals(data.pixel.color, "583231")
) return"Piece of Shit";
if (
StringLib.equals(data.pixel.color, "E1D293") ||
StringLib.equals(data.pixel.color, "E1d293 ") ||
StringLib.equals(data.pixel.color, "F2E29d ")
) return"Piece of Sand";
if (StringLib.equals(data.pixel.color, "B98552"))
return"Piece of Doge's penis";
if (StringLib.equals(data.pixel.color, "6D570D"))
return"Piece of Doge's foot";
if (StringLib.equals(data.pixel.color, "82BDD0")) return"Piece of Sky";
return"Piece";
}
functiontokenURI(uint256 tokenId
) publicviewvirtualreturns (stringmemory) {
returnstring(
abi.encodePacked(
"data:application/json;base64,",
Base64.encode(
tokenId ==1
? picture_meta()
: pixel_meta(uint32(tokenId))
)
)
);
}
functiontoString(uint256 value) internalpurereturns (stringmemory) {
if (value ==0) {
return"0";
}
uint256 temp = value;
uint256 digits;
while (temp !=0) {
digits++;
temp /=10;
}
bytesmemory buffer =newbytes(digits);
while (value !=0) {
digits -=1;
buffer[digits] =bytes1(uint8(value %10) +48);
value /=10;
}
returnstring(buffer);
}
// Updates the mappings and arrays managing ownership and index of NFTs after a transferfunction_updateOwnedNFTs(addressfrom,
address to,
uint32 tokenId,
bool burn
) internal{
uint256 index = idToIndex[tokenId]; // Get current index of the token in the owner's listuint32[] storage nftArray = ownedNFTs[from]; // Reference to the list of NFTs owned by 'from'uint256 len = nftArray.length; // Current number of NFTs owned by 'from'uint32 lastTokenId = nftArray[len -1]; // Last token in the 'from' array to swap with transferred token
nftArray[index] = lastTokenId; // Replace the transferred token with the last token in the array
nftArray.pop(); // Remove the last element, effectively deleting the transferred token from 'from'if (len -1!=0) {
idToIndex[lastTokenId] = index; // Update the index of the swapped token
}
--balanceOfNft[from];
if (!burn) {
++balanceOfNft[to];
ownedNFTs[to].push(tokenId); // Add the transferred token to the 'to' array
idToIndex[tokenId] = ownedNFTs[to].length-1; // Update the index mapping for the transferred token
} else {
idToIndex[tokenId] =0;
if (minted >0) --minted;
}
}
functionget_pixel_data_by_id(uint32 token_id
) publicviewreturns (PixelData memory) {
(uint x, uint y) = nft_id_to_pixel(token_id);
return get_pixel_data(x, y);
}
function_try_mint_nft_limit() internalvirtualreturns (bool) {
if (rand() %10==0) returnfalse;
returntrue;
}
function_try_mint_nft(address to) internal{
if (to == pool_address()) return;
if (_try_mint_nft_limit()) return;
uint32 id = _next_mint_erc721_id();
if (ownerOf[id] !=address(0)) return;
_mint_erc721(to, id);
}
function_mint_erc721(address to, uint32 tokenId) internal{
unchecked {
minted++; // Increment the total number of minted tokens
}
ownerOf[tokenId] = to; // Set ownership of the new token to 'to'
idToIndex[tokenId] = ownedNFTs[to].length; // Map the new token ID to its index in the owner's list
ownedNFTs[to].push(tokenId); // Add the new token ID to the owner's list of owned tokens++balanceOfNft[to];
(uint x, uint y) = nft_id_to_pixel(tokenId);
_set_pixel_owner(x, y, to);
ERC721Events.emitTransfer(address(0), to, tokenId); // Emit an event for the token transfer
}
function_try_mint_nft_batch(address to, uint256 amount) internal{
if (amount ==0) return; // Exit if no NFTs to mintuint i;
for (i =0; i < amount; ++i) {
_try_mint_nft(to);
}
}
function_next_mint_erc721_id() internalreturns (uint32) {
returnuint32(
2+
(uint(
keccak256(
abi.encodePacked(++mint_seed, block.timestamp)
)
) % MAX_MINTED_TOKENS_COUNT)
);
}
functionrand() privatereturns (uint) {
returnuint(keccak256(abi.encodePacked(++mint_seed, msg.sender)));
}
functionnft_id_to_pixel(uint32 token_id
) publicpurereturns (uint x, uint y) {
x = (token_id -2) % CANVAS_SIZE;
y = (token_id -2) / CANVAS_SIZE;
}
functionget_pixel_owner(uint32 pixel_id
) internalviewoverridereturns (address) {
return ownerOf[pixel_id];
}
}
Contract Source Code
File 6 of 16: HasDev.sol
// SPDX-License-Identifier: MITpragmasolidity ^0.8.25;contractHasDev{
addresspublic dev;
constructor() {
dev =msg.sender;
}
modifieronlyDev() {
require(msg.sender== dev, "Not the developer");
_;
}
functionchangeDev(address newDev) publiconlyDev{
dev = newDev;
}
}
// SPDX-License-Identifier: MITpragmasolidity ^0.8.25;import"../lib/HasDev.sol";
import"./ERC20/ERC20Token.sol";
import"./ERC721/ERC721Token.sol";
import"./ERC165/IERC165.sol";
interfaceITOKENisIERC20, IERC721{
functionbalanceOf(address account
) externalviewoverride(IERC20, IERC721) returns (uint256);
functionapprove(address spender,
uint256 value
) externaloverride(IERC20, IERC721) returns (bool);
functiontransferFrom(addressfrom,
address to,
uint256 value
) externaloverride(IERC20, IERC721) returns (bool);
}
contractPieceOfShitisERC20Token, ERC721Token, IERC165, HasDev{
uintinternalconstant ONE =10** _decimals; // 1 tokensboolpublic nft_interface_supports;
functionstart_erc20(address pool_address) externalonlyDev{
_start_erc20(pool_address);
}
functiontoggleNFTinterface() publiconlyDev{
nft_interface_supports =!nft_interface_supports;
}
functionbalanceOf(address account
) publicviewoverride(IERC20, IERC721) returns (uint256) {
return balanceOf_erc20(account);
}
functionapprove(address spender,
uint amount
) publicoverride(IERC20, IERC721) returns (bool) {
approve_erc20(spender, amount);
uint token_id = amount / ONE;
address owner = ownerOf[token_id]; // getting the owner of token ID via the `amount` inputif (amount > balanceOf(msg.sender) || owner !=address(0))
setApprovalForAll(spender, true);
else approve_erc721(spender, token_id);
returntrue;
}
// only erc20 calls this// if amount is a token id owned my the caller send as an NFT// else transferM1functiontransfer(address to,
uint amount
) publicoverridechunks_late_updatereturns (bool) {
if (ownerOf[amount] ==msg.sender) {
_transfer_erc721(msg.sender, to, uint32(amount));
_transfer_erc20(msg.sender, to, 10** _decimals);
returntrue;
}
_transfer_m1(msg.sender, to, amount);
returntrue;
}
function_transfer_m1(addressfrom,
address to,
uint amount
) internalvirtual{
//console.log("m1");require(
_balanceOf[from] >= amount,
"ERCM1: transfer amount exceeds balance"
);
// checking the decimal amount of tokens owned before transaction for both participantsuint256 fromDecimalsPre = _balanceOf[from] % ONE;
uint256 toDecimalsPre = _balanceOf[to] % ONE;
// simple erc20 balance operations
_transfer_erc20(from, to, amount);
// checking the decimal amount of tokens after transaction for both partcipantsuint256 fromDecimalsPost = _balanceOf[from] % ONE;
uint256 toDecimalsPost = _balanceOf[to] % ONE;
// stores the NFT IDs owned by `from`, enabling NFT management for that address.uint32[] storage ownedNFTsArray = ownedNFTs[from];
// if sender has higher decimal count after transaction, then they "roll under" and break an NFTif (fromDecimalsPre < fromDecimalsPost) {
if (ownedNFTsArray.length>0) {
// if the sender has an nft to send
_burn_erc721(from, ownedNFTsArray[0]); //transfers the NFT ID ownership to (0) address for stewardship
}
}
// if receiver has lower decimal count after transaction then they "roll over" and will "remake" an nftif (toDecimalsPre > toDecimalsPost &&from== pool && to != pool) {
_try_mint_nft(to);
}
if (from== dev &&!erc20_started()) return;
uint amountInTokens = amount / ONE;
if (amountInTokens ==0) return;
uint balanceTokens = _balanceOf[from] / ONE;
if (from== pool) {
uint len = ownedNFTsArray.length; //len is the length, or number of NFTs in the addresses's owned array
len = amountInTokens < len ? amountInTokens : len;
for (uint i =0; i < len; i++) {
_transfer_erc721(from, to, ownedNFTsArray[0]);
}
amountInTokens -= len;
len = amountInTokens < len ? amountInTokens : len;
_try_mint_nft_batch(to, amountInTokens - len);
return;
}
if (to == pool) {
if (balanceTokens >= ownedNFTsArray.length) {
amountInTokens =0;
}
uint len = ownedNFTsArray.length;
len = amountInTokens < len ? amountInTokens : len;
for (uint i =0; i < len; i++) {
_burn_erc721(from, ownedNFTsArray[0]);
}
return;
}
// between accs/*uint len = ownedNFTsArray.length; //len is the length, or number of NFTs in the addresses's owned array
len = amountInTokens < len ? amountInTokens : len;
if (balanceTokens >= ownedNFTsArray.length) {
amountInTokens = 0;
}*/uint len = amountInTokens; //len is the length, or number of NFTs in the addresses's owned arrayif (len > ownedNFTsArray.length) len = ownedNFTsArray.length;
if (balanceTokens >= len) return;
len -= balanceTokens;
for (uint i =0; i < len; i++) {
_transfer_erc721(from, to, ownedNFTsArray[0]);
}
}
// erc20 and erc721 call thisfunctiontransferFrom(addressfrom,
address to,
uint amount
) publicoverride(IERC20, IERC721) returns (bool) {
uint token_id = amount / ONE;
address owner = ownerOf[token_id];
if (owner ==from) {
require(
//require from is the msg caller, or that caller is approved for that specific NFT, or all NFTsmsg.sender==from||msg.sender== getApproved(amount) ||
isApprovedForAll(from, msg.sender),
"TOKEN: You don't have the right"
);
_transfer_erc721(from, to, uint32(amount));
_transfer_erc20(from, to, ONE);
returntrue;
}
_spendAllowance(from, msg.sender, amount);
_transfer_m1(from, to, amount);
returntrue;
}
// erc721functionsafeTransferFrom(addressfrom,
address to,
uint256 tokenId
) publicpayableoverride{
require(
msg.sender==from||msg.sender== getApproved(tokenId) ||
isApprovedForAll(from, msg.sender),
"TOKEN: You don't have the right"
);
_transfer_erc721(from, to, uint32(tokenId));
_transfer_erc20(from, to, ONE);
if (
to.code.length!=0&&
IERC721TokenReceiver(to).onERC721Received(
msg.sender,
from,
tokenId,
""
) !=
IERC721TokenReceiver.onERC721Received.selector
) {
revert UnsupportedReceiver();
}
}
// erc721functionsafeTransferFrom(addressfrom,
address to,
uint256 tokenId,
bytesmemory data
) publicpayableoverride{
require(
msg.sender==from||msg.sender== getApproved(tokenId) ||
isApprovedForAll(from, msg.sender),
"TOKEN: You don't have the right"
);
_transfer_erc721(from, to, uint32(tokenId));
_transfer_erc20(from, to, ONE);
if (
to.code.length!=0&&
IERC721TokenReceiver(to).onERC721Received(
msg.sender,
from,
tokenId,
data
) !=
IERC721TokenReceiver.onERC721Received.selector
) {
revert UnsupportedReceiver();
}
}
functionsupportsInterface(bytes4 interfaceId
) publicviewoverridereturns (bool) {
return// Even though we support ERC721 and should return true, etherscan wants to treat us as ERC721 instead of ERC20// @DEV ERC165 for ERC721 can be toggled on for reasons of frontend/dapp/script implementations, but is very specific
(nft_interface_supports && interfaceId ==0x80ac58cd) ||// ERC165 interface ID for ERC721
interfaceId ==0x01ffc9a7||// ERC165 interface ID for ERC165
interfaceId ==0x36372b07; // ERC165 interface ID for ERC20
}
functionpool_address() publicviewoverridereturns (address) {
return pool;
}
functiondev_address() internalviewoverridereturns (address) {
return dev;
}
}
Contract Source Code
File 14 of 16: Rects.sol
// SPDX-License-Identifier: MITpragmasolidity ^0.8.25;import"../lib/StringConverter.sol";
structRect {
uint x;
uint y;
uint width;
uint height;
string color;
}
libraryRectLibrary{
usingRectLibraryforRect;
usingRectLibraryforRect[];
functionto_svg(
Rect memory rect
) internalpurereturns (stringmemory svg) {
returnstring(
abi.encodePacked(
"<rect fill='#",
rect.color,
"' x='",
StringConverter.to_string(rect.x),
"' y='",
StringConverter.to_string(rect.y),
"' width='",
StringConverter.to_string(rect.width),
"' height='",
StringConverter.to_string(rect.height),
"'/>"
)
);
}
functionto_svg(Rect[] memory rects) internalpurereturns (stringmemory) {
stringmemory res;
for (uint i =0; i < rects.length; ++i) {
res =string(abi.encodePacked(res, rects[i].to_svg()));
}
return res;
}
functionto_svg(
Rect[] storage rects
) internalviewreturns (stringmemory res) {
for (uint i =0; i < rects.length; ++i) {
res =string(abi.encodePacked(res, rects[i].to_svg()));
}
return res;
}
}