Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Defend stencil #272

Merged
merged 7 commits into from
Dec 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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
Loading