diff --git a/backend/routes/stencils.go b/backend/routes/stencils.go index b8c60e92..2f1f48c4 100644 --- a/backend/routes/stencils.go +++ b/backend/routes/stencils.go @@ -1,6 +1,8 @@ package routes import ( + "bytes" + "encoding/json" "fmt" "image" "image/color" @@ -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) @@ -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 @@ -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)) +} diff --git a/backend/routes/templates.go b/backend/routes/templates.go index 3d3278f7..bf13dac4 100644 --- a/backend/routes/templates.go +++ b/backend/routes/templates.go @@ -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") @@ -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 diff --git a/frontend/src/App.js b/frontend/src/App.js index bb831a15..5bbbe3b7 100644 --- a/frontend/src/App.js +++ b/frontend/src/App.js @@ -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) { @@ -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( @@ -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); @@ -1419,7 +1453,7 @@ function App() { isMobile={isMobile} overlayTemplate={overlayTemplate} templatePixels={templatePixels} - width={canvasConfig.canvas.width} + width={width} canvasRef={canvasRef} addExtraPixel={addExtraPixel} addExtraPixels={addExtraPixels} diff --git a/frontend/src/tabs/TabPanel.js b/frontend/src/tabs/TabPanel.js index e54e55cf..ae7684b2 100644 --- a/frontend/src/tabs/TabPanel.js +++ b/frontend/src/tabs/TabPanel.js @@ -182,6 +182,8 @@ const TabPanel = (props) => { stencilCreationSelected={props.stencilCreationSelected} setStencilCreationSelected={props.setStencilCreationSelected} canvasWidth={props.width} + setTemplateOverlayMode={props.setTemplateOverlayMode} + setOverlayTemplate={props.setOverlayTemplate} /> diff --git a/frontend/src/tabs/stencils/StencilCreationPanel.js b/frontend/src/tabs/stencils/StencilCreationPanel.js index a813325d..997d9136 100644 --- a/frontend/src/tabs/stencils/StencilCreationPanel.js +++ b/frontend/src/tabs/stencils/StencilCreationPanel.js @@ -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'); } diff --git a/frontend/src/tabs/stencils/StencilItem.js b/frontend/src/tabs/stencils/StencilItem.js index af422245..15619775 100644 --- a/frontend/src/tabs/stencils/StencilItem.js +++ b/frontend/src/tabs/stencils/StencilItem.js @@ -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);