Skip to content

Commit

Permalink
Merge pull request #272 from supreme2580/defend-stencil
Browse files Browse the repository at this point in the history
Defend stencil
  • Loading branch information
b-j-roberts authored Dec 16, 2024
2 parents fc7bb19 + 9ca2cc2 commit 00a99c0
Show file tree
Hide file tree
Showing 6 changed files with 161 additions and 23 deletions.
98 changes: 97 additions & 1 deletion backend/routes/stencils.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,8 @@
package routes

import (
"bytes"
"encoding/json"
"fmt"
"image"
"image/color"
Expand All @@ -26,6 +28,7 @@ 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)
Expand Down Expand Up @@ -402,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 @@ -708,3 +711,96 @@ 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")
if hash == "" {
routeutils.WriteErrorJson(w, http.StatusBadRequest, "Hash parameter is required")
return
}

// Read the stencil image file
filename := fmt.Sprintf("stencils/stencil-%s.png", hash)
fileBytes, err := os.ReadFile(filename)
if err != nil {
routeutils.WriteErrorJson(w, http.StatusNotFound, "Stencil not found")
return
}

// Convert image to pixel data
pixelData, err := worldImageToPixelData(fileBytes, 1, 0)
if err != nil {
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image")
return
}

// Get image dimensions
img, _, err := image.Decode(bytes.NewReader(fileBytes))
if err != nil {
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to decode image")
return
}
bounds := img.Bounds()
width, height := bounds.Max.X, bounds.Max.Y

// Create response structure
response := struct {
Width int `json:"width"`
Height int `json:"height"`
PixelData []int `json:"pixelData"`
}{
Width: width,
Height: height,
PixelData: pixelData,
}

jsonResponse, err := json.Marshal(response)
if err != nil {
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to create response")
return
}

routeutils.WriteDataJson(w, string(jsonResponse))
}
3 changes: 2 additions & 1 deletion backend/routes/templates.go
Original file line number Diff line number Diff line change
Expand Up @@ -462,6 +462,7 @@ func addTemplateData(w http.ResponseWriter, r *http.Request) {
}

func getTemplatePixelData(w http.ResponseWriter, r *http.Request) {
// Get template hash from query params
hash := r.URL.Query().Get("hash")
if hash == "" {
routeutils.WriteErrorJson(w, http.StatusBadRequest, "Hash parameter is required")
Expand All @@ -477,7 +478,7 @@ func getTemplatePixelData(w http.ResponseWriter, r *http.Request) {
}

// Convert image to pixel data using existing function
pixelData, err := imageToPixelData(fileBytes, 1)
pixelData, err := imageToPixelData(fileBytes, 1) // Scale factor 1 for templates
if err != nil {
routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to process image")
return
Expand Down
66 changes: 50 additions & 16 deletions frontend/src/App.js
Original file line number Diff line number Diff line change
Expand Up @@ -711,32 +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 {
let placeExtraPixelsEndpoint = 'place-extra-pixels-devnet';
const response = await fetchWrapper(placeExtraPixelsEndpoint, {
mode: 'cors',
method: 'POST',
body: JSON.stringify({
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 * canvasConfig.canvas.width,
position: pixel.x + pixel.y * width,
colorId: pixel.colorId
})),
timestamp: timestamp
})
});
if (response.result) {
console.log(response.result);
};

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 @@ -1125,6 +1142,16 @@ function App() {
return [];
};

const getStencilPixelData = async (hash) => {
if (hash !== null) {
const response = await fetchWrapper(
`get-stencil-pixel-data?hash=${hash}`
);
return response.data;
}
return [];
};

const getNftPixelData = async (tokenId) => {
if (tokenId !== null) {
const response = await fetchWrapper(
Expand Down Expand Up @@ -1153,6 +1180,13 @@ function App() {
return;
}

// Handle stencil overlay case
if (overlayTemplate.isStencil && overlayTemplate.hash) {
const data = await getStencilPixelData(overlayTemplate.hash);
setTemplatePixels(data);
return;
}

// Handle template overlay case
if (overlayTemplate.hash) {
const data = await getTemplatePixelData(overlayTemplate.hash);
Expand Down Expand Up @@ -1419,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: 2 additions & 0 deletions frontend/src/tabs/TabPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -182,6 +182,8 @@ const TabPanel = (props) => {
stencilCreationSelected={props.stencilCreationSelected}
setStencilCreationSelected={props.setStencilCreationSelected}
canvasWidth={props.width}
setTemplateOverlayMode={props.setTemplateOverlayMode}
setOverlayTemplate={props.setOverlayTemplate}
/>
</CSSTransition>
<SwitchTransition mode='out-in'>
Expand Down
12 changes: 8 additions & 4 deletions frontend/src/tabs/stencils/StencilCreationPanel.js
Original file line number Diff line number Diff line change
Expand Up @@ -112,10 +112,14 @@ const StencilCreationPanel = (props) => {
})
});
if (addResponse.result) {
// TODO: after tx done, add stencil to backend
// TODO: Double check hash match
// TODO: Update UI optimistically & go to specific faction in factions tab
console.log(addResponse.result);
props.setOverlayTemplate({
hash: hash,
width: props.stencilImage.width,
height: props.stencilImage.height,
image: props.stencilImage.image,
isStencil: true
});
props.setTemplateOverlayMode(true);
closePanel();
props.setActiveTab('Stencils');
}
Expand Down
3 changes: 2 additions & 1 deletion frontend/src/tabs/stencils/StencilItem.js
Original file line number Diff line number Diff line change
Expand Up @@ -164,7 +164,8 @@ const StencilItem = (props) => {
width: props.width,
height: props.height,
position: props.position,
image: props.image
image: props.image,
isStencil: true
};
props.setTemplateOverlayMode(true);
props.setOverlayTemplate(template);
Expand Down

0 comments on commit 00a99c0

Please sign in to comment.