Skip to content

Commit

Permalink
World stencil coloring
Browse files Browse the repository at this point in the history
  • Loading branch information
b-j-roberts committed Dec 16, 2024
1 parent 5579e78 commit 4eacb76
Show file tree
Hide file tree
Showing 7 changed files with 109 additions and 192 deletions.
114 changes: 11 additions & 103 deletions backend/routes/pixel.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,6 @@ package routes

import (
"context"
"encoding/json"
"fmt"
"net/http"
"os"
"os/exec"
Expand Down Expand Up @@ -141,130 +139,40 @@ func placePixelDevnet(w http.ResponseWriter, r *http.Request) {
}

type ExtraPixelJson struct {
ExtraPixels []struct {
Position int `json:"position"`
ColorId int `json:"colorId"`
} `json:"extraPixels"`
Timestamp int `json:"timestamp"`
ExtraPixels []map[string]int `json:"extraPixels"`
Timestamp int `json:"timestamp"`
}

func placeExtraPixelsDevnet(w http.ResponseWriter, r *http.Request) {
// Handle CORS
w.Header().Set("Access-Control-Allow-Origin", "*")
w.Header().Set("Access-Control-Allow-Methods", "POST, OPTIONS")
w.Header().Set("Access-Control-Allow-Headers", "Content-Type")

if r.Method == "OPTIONS" {
w.WriteHeader(http.StatusOK)
return
}

// Disable this in production
if routeutils.NonProductionMiddleware(w, r) {
return
}

jsonBody, err := routeutils.ReadJsonBody[ExtraPixelJson](r)
if err != nil {
routeutils.WriteErrorJson(w, http.StatusBadRequest, fmt.Sprintf("Invalid JSON request body: %v", err))
return
}

// Validate input
if len(jsonBody.ExtraPixels) == 0 {
routeutils.WriteErrorJson(w, http.StatusBadRequest, "No pixels provided")
routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid JSON request body")
return
}

shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.PlaceExtraPixelsDevnet
contract := os.Getenv("ART_PEACE_CONTRACT_ADDRESS")

if contract == "" {
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Contract address not configured")
return
}

// Format calldata for the contract
// The contract expects: [pixel_count, pos1, pos2, ..., posN, color1, color2, ..., colorN, timestamp]
var args []string
args = append(args, contract, "place_extra_pixels")

// Add pixel count
pixelCount := len(jsonBody.ExtraPixels)
args = append(args, strconv.Itoa(pixelCount))

// Add positions
positions := strconv.Itoa(len(jsonBody.ExtraPixels))
colors := strconv.Itoa(len(jsonBody.ExtraPixels))
for _, pixel := range jsonBody.ExtraPixels {
args = append(args, strconv.Itoa(pixel.Position))
positions += " " + strconv.Itoa(pixel["position"])
colors += " " + strconv.Itoa(pixel["colorId"])
}

// Add colors
for _, pixel := range jsonBody.ExtraPixels {
args = append(args, strconv.Itoa(pixel.ColorId))
}

// Add timestamp
args = append(args, strconv.Itoa(jsonBody.Timestamp))

// Execute the command
cmd := exec.Command(shellCmd, args...)
output, err := cmd.CombinedOutput()
cmd := exec.Command(shellCmd, contract, "place_extra_pixels", positions, colors, strconv.Itoa(jsonBody.Timestamp))
_, err = cmd.Output()
if err != nil {
routeutils.WriteErrorJson(w, http.StatusInternalServerError,
fmt.Sprintf("Failed to place extra pixels on devnet: %v - Output: %s", err, string(output)))
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to place extra pixels on devnet")
return
}

// Create response structure
response := struct {
Message string `json:"message"`
Data struct {
PixelsPlaced int `json:"pixelsPlaced"`
Positions []int `json:"positions"`
Colors []int `json:"colors"`
Timestamp int `json:"timestamp"`
} `json:"data"`
}{
Message: "Extra pixels placed successfully",
Data: struct {
PixelsPlaced int `json:"pixelsPlaced"`
Positions []int `json:"positions"`
Colors []int `json:"colors"`
Timestamp int `json:"timestamp"`
}{
PixelsPlaced: pixelCount,
Positions: extractPositions(jsonBody.ExtraPixels),
Colors: extractColors(jsonBody.ExtraPixels),
Timestamp: jsonBody.Timestamp,
},
}

// Write response
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(response)
}

// Helper functions to extract positions and colors
func extractPositions(pixels []struct {
Position int `json:"position"`
ColorId int `json:"colorId"`
}) []int {
positions := make([]int, len(pixels))
for i, pixel := range pixels {
positions[i] = pixel.Position
}
return positions
}

func extractColors(pixels []struct {
Position int `json:"position"`
ColorId int `json:"colorId"`
}) []int {
colors := make([]int, len(pixels))
for i, pixel := range pixels {
colors[i] = pixel.ColorId
}
return colors
routeutils.WriteResultJson(w, "Extra pixels placed")
}

func placePixelRedis(w http.ResponseWriter, r *http.Request) {
Expand Down
47 changes: 44 additions & 3 deletions backend/routes/stencils.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,13 +28,13 @@ func InitStencilsRoutes() {
http.HandleFunc("/get-hot-stencils", getHotStencils)
http.HandleFunc("/add-stencil-img", addStencilImg)
http.HandleFunc("/add-stencil-data", addStencilData)
http.HandleFunc("/get-stencil-pixel-data", getStencilPixelData)
if !core.ArtPeaceBackend.BackendConfig.Production {
http.HandleFunc("/add-stencil-devnet", addStencilDevnet)
http.HandleFunc("/remove-stencil-devnet", removeStencilDevnet)
http.HandleFunc("/favorite-stencil-devnet", favoriteStencilDevnet)
http.HandleFunc("/unfavorite-stencil-devnet", unfavoriteStencilDevnet)
}
http.HandleFunc("/get-stencil-pixel-data", getStencilPixelData)
}

func InitStencilsStaticRoutes() {
Expand Down Expand Up @@ -405,7 +405,7 @@ func addStencilImg(w http.ResponseWriter, r *http.Request) {

r.Body.Close()

imageData, err := imageToPixelData(fileBytes, 1)
imageData, err := worldImageToPixelData(fileBytes, 1, 0)
if err != nil {
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to convert image to pixel data")
return
Expand Down Expand Up @@ -712,6 +712,47 @@ func unfavoriteStencilDevnet(w http.ResponseWriter, r *http.Request) {
routeutils.WriteResultJson(w, "Stencil unfavorited in devnet")
}

func worldImageToPixelData(imageData []byte, scaleFactor int, worldId int) ([]int, error) {
img, _, err := image.Decode(bytes.NewReader(imageData))
if err != nil {
return nil, err
}

colors, err := core.PostgresQuery[ColorType]("SELECT hex FROM WorldsColors WHERE world_id = $1 ORDER BY color_key", worldId)
if err != nil {
return nil, err
}

colorCount := len(colors)
palette := make([]color.Color, colorCount)
for i := 0; i < colorCount; i++ {
colorHex := colors[i]
palette[i] = hexToRGBA(colorHex)
}

bounds := img.Bounds()
width, height := bounds.Max.X, bounds.Max.Y
scaledWidth := width / scaleFactor
scaledHeight := height / scaleFactor
pixelData := make([]int, scaledWidth*scaledHeight)

for y := 0; y < height; y += scaleFactor {
for x := 0; x < width; x += scaleFactor {
newX := x / scaleFactor
newY := y / scaleFactor
rgba := color.RGBAModel.Convert(img.At(x, y)).(color.RGBA)
if rgba.A < 128 { // Consider pixels with less than 50% opacity as transparent
pixelData[newY*scaledWidth+newX] = 0xFF
} else {
closestIndex := findClosestColor(rgba, palette)
pixelData[newY*scaledWidth+newX] = closestIndex
}
}
}

return pixelData, nil
}

func getStencilPixelData(w http.ResponseWriter, r *http.Request) {
// Get stencil hash from query params
hash := r.URL.Query().Get("hash")
Expand All @@ -729,7 +770,7 @@ func getStencilPixelData(w http.ResponseWriter, r *http.Request) {
}

// Convert image to pixel data
pixelData, err := imageToPixelData(fileBytes, 1)
pixelData, err := worldImageToPixelData(fileBytes, 1, 0)
if err != nil {
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image")
return
Expand Down
2 changes: 1 addition & 1 deletion backend/routes/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,7 @@ func InitTemplateRoutes() {
http.HandleFunc("/add-template-data", addTemplateData)
http.HandleFunc("/get-template-pixel-data", getTemplatePixelData)
if !core.ArtPeaceBackend.BackendConfig.Production {
http.HandleFunc("/add-template-devnet", addTemplateDevnet)
// http.HandleFunc("/add-template-devnet", addTemplateDevnet)
http.HandleFunc("/add-faction-template-devnet", addFactionTemplateDevnet)
http.HandleFunc("/remove-faction-template-devnet", removeFactionTemplateDevnet)
http.HandleFunc("/add-chain-faction-template-devnet", addChainFactionTemplateDevnet)
Expand Down
80 changes: 38 additions & 42 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -625,27 +625,7 @@ function App() {
};

const extraPixelPlaceCall = async (positions, colors, now) => {
if (devnetMode) {
try {
const extraPixels = positions.map((pos, idx) => ({
position: pos,
colorId: colors[idx]
}));

const response = await fetchWrapper('place-extra-pixels-devnet', {
mode: 'cors',
method: 'POST',
body: JSON.stringify({
extraPixels: extraPixels,
timestamp: now
})
});
return response;
} catch (error) {
console.error('Failed to place extra pixels:', error);
throw error;
}
}
if (devnetMode) return;
if (!address || !artPeaceContract || !account) return;
// TODO: Validate inputs
const placeExtraPixelsCallData = artPeaceContract.populate(
Expand Down Expand Up @@ -731,33 +711,49 @@ function App() {
let timestamp = Math.floor(Date.now() / 1000);
if (!devnetMode) {
await extraPixelPlaceCall(
extraPixelsData.map(
(pixel) => pixel.x + pixel.y * canvasConfig.canvas.width
),
extraPixelsData.map((pixel) => pixel.x + pixel.y * width),
extraPixelsData.map((pixel) => pixel.colorId),
timestamp
);
} else {
const formattedData = {
extraPixels: extraPixelsData.map((pixel) => ({
position: pixel.x + pixel.y * width,
colorId: pixel.colorId
})),
timestamp: timestamp
};

const response = await fetchWrapper('place-extra-pixels-devnet', {
mode: 'cors',
method: 'POST',
body: JSON.stringify(formattedData)
});
if (response.result) {
console.log(response.result);
if (worldsMode) {
const firstPixel = extraPixelsData[0];
const formattedData = {
worldId: openedWorldId.toString(),
position: (firstPixel.x + firstPixel.y * width).toString(),
color: firstPixel.colorId.toString(),
timestamp: timestamp.toString()
};

const response = await fetchWrapper('place-world-pixel-devnet', {
mode: 'cors',
method: 'POST',
body: JSON.stringify(formattedData)
});
if (response.result) {
console.log(response.result);
}
} else {
const formattedData = {
extraPixels: extraPixelsData.map((pixel) => ({
position: pixel.x + pixel.y * width,
colorId: pixel.colorId
})),
timestamp: timestamp
};

const response = await fetchWrapper('place-extra-pixels-devnet', {
mode: 'cors',
method: 'POST',
body: JSON.stringify(formattedData)
});
if (response.result) {
console.log(response.result);
}
}
}
for (let i = 0; i < extraPixelsData.length; i++) {
let position =
extraPixelsData[i].x + extraPixelsData[i].y * canvasConfig.canvas.width;
let position = extraPixelsData[i].x + extraPixelsData[i].y * width;
colorPixel(position, extraPixelsData[i].colorId);
}
if (basePixelUsed) {
Expand Down Expand Up @@ -1457,7 +1453,7 @@ function App() {
isMobile={isMobile}
overlayTemplate={overlayTemplate}
templatePixels={templatePixels}
width={canvasConfig.canvas.width}
width={width}
canvasRef={canvasRef}
addExtraPixel={addExtraPixel}
addExtraPixels={addExtraPixels}
Expand Down
2 changes: 1 addition & 1 deletion frontend/src/tabs/stencils/StencilCreationPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -121,7 +121,7 @@ const StencilCreationPanel = (props) => {
});
props.setTemplateOverlayMode(true);
closePanel();
props.setActiveTab('Canvas');
props.setActiveTab('Stencils');
}
return;
}
Expand Down
Loading

0 comments on commit 4eacb76

Please sign in to comment.