diff --git a/backend/routes/stencils.go b/backend/routes/stencils.go index 2830d3ff..2f1f48c4 100644 --- a/backend/routes/stencils.go +++ b/backend/routes/stencils.go @@ -1,12 +1,13 @@ package routes import ( + "bytes" + "encoding/json" "fmt" "image" "image/color" "image/png" "io" - "log" "net/http" "os" "os/exec" @@ -27,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) @@ -403,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 @@ -590,28 +592,16 @@ func addStencilDevnet(w http.ResponseWriter, r *http.Request) { return } - // Add the stencil shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.AddStencilDevnet contract := os.Getenv("CANVAS_FACTORY_CONTRACT_ADDRESS") cmd := exec.Command(shellCmd, contract, "add_stencil", strconv.Itoa(worldId), hash, strconv.Itoa(width), strconv.Itoa(height), strconv.Itoa(position)) - output, err := cmd.CombinedOutput() + _, err = cmd.Output() if err != nil { - log.Printf("Add stencil command failed: %v\nOutput: %s\nCommand: %v", err, string(output), cmd.String()) routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to add stencil to devnet") return } - // Favorite the newly created stencil - shellCmd = core.ArtPeaceBackend.BackendConfig.Scripts.FavoriteStencilDevnet - cmd = exec.Command(shellCmd, contract, "favorite_stencil", strconv.Itoa(worldId), strconv.Itoa(position)) - output, err = cmd.CombinedOutput() - if err != nil { - log.Printf("Favorite stencil command failed: %v\nOutput: %s\nCommand: %v", err, string(output), cmd.String()) - routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to favorite newly created stencil") - return - } - - routeutils.WriteResultJson(w, "Stencil added and favorited") + routeutils.WriteResultJson(w, "Stencil added to devnet") } func removeStencilDevnet(w http.ResponseWriter, r *http.Request) { @@ -721,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/worlds.go b/backend/routes/worlds.go index 94e042b5..cc62a700 100644 --- a/backend/routes/worlds.go +++ b/backend/routes/worlds.go @@ -2,8 +2,6 @@ package routes import ( "context" - "fmt" - "log" "net/http" "os" "os/exec" @@ -542,24 +540,13 @@ func createCanvasDevnet(w http.ResponseWriter, r *http.Request) { contract := os.Getenv("CANVAS_FACTORY_CONTRACT_ADDRESS") cmd := exec.Command(shellCmd, contract, "create_canvas", host, name, uniqueName, strconv.Itoa(width), strconv.Itoa(height), strconv.Itoa(timer), strconv.Itoa(len(palette)), paletteInput, strconv.Itoa(startTime), strconv.Itoa(endTime)) - output, err := cmd.CombinedOutput() - if err != nil { - log.Printf("Create canvas command failed: %v\nOutput: %s\nCommand: %v", err, string(output), cmd.String()) - routeutils.WriteErrorJson(w, http.StatusInternalServerError, fmt.Sprintf("Failed to create canvas: %v", err)) - return - } - - // Favorite the newly created canvas - shellCmd = core.ArtPeaceBackend.BackendConfig.Scripts.FavoriteWorldDevnet - cmd = exec.Command(shellCmd, contract, "favorite_canvas", uniqueName) - output, err = cmd.CombinedOutput() + _, err = cmd.Output() if err != nil { - log.Printf("Favorite canvas command failed: %v\nOutput: %s\nCommand: %v", err, string(output), cmd.String()) - routeutils.WriteErrorJson(w, http.StatusInternalServerError, fmt.Sprintf("Failed to favorite newly created canvas: %v", err)) + routeutils.WriteErrorJson(w, http.StatusInternalServerError, "Failed to create canvas") return } - routeutils.WriteResultJson(w, "Canvas created and favorited") + routeutils.WriteResultJson(w, "Canvas created") } func favoriteWorldDevnet(w http.ResponseWriter, r *http.Request) { @@ -574,22 +561,7 @@ func favoriteWorldDevnet(w http.ResponseWriter, r *http.Request) { return } - // Try to get worldId either directly or from worldName - var worldId string - if (*jsonBody)["worldId"] != "" { - worldId = (*jsonBody)["worldId"] - } else if (*jsonBody)["worldName"] != "" { - // Get worldId from worldName - id, err := core.PostgresQueryOne[int]("SELECT world_id FROM worlds WHERE unique_name = $1", (*jsonBody)["worldName"]) - if err != nil { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid world name") - return - } - worldId = strconv.Itoa(*id) - } else { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "Must provide either worldId or worldName") - return - } + worldId := (*jsonBody)["worldId"] shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.FavoriteWorldDevnet contract := os.Getenv("CANVAS_FACTORY_CONTRACT_ADDRESS") @@ -616,22 +588,7 @@ func unfavoriteWorldDevnet(w http.ResponseWriter, r *http.Request) { return } - // Try to get worldId either directly or from worldName - var worldId string - if (*jsonBody)["worldId"] != "" { - worldId = (*jsonBody)["worldId"] - } else if (*jsonBody)["worldName"] != "" { - // Get worldId from worldName - id, err := core.PostgresQueryOne[int]("SELECT world_id FROM worlds WHERE unique_name = $1", (*jsonBody)["worldName"]) - if err != nil { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "Invalid world name") - return - } - worldId = strconv.Itoa(*id) - } else { - routeutils.WriteErrorJson(w, http.StatusBadRequest, "Must provide either worldId or worldName") - return - } + worldId := (*jsonBody)["worldId"] shellCmd := core.ArtPeaceBackend.BackendConfig.Scripts.UnfavoriteWorldDevnet contract := os.Getenv("CANVAS_FACTORY_CONTRACT_ADDRESS") diff --git a/onchain/src/canvas_factory.cairo b/onchain/src/canvas_factory.cairo index 4dcc4462..daccfd78 100644 --- a/onchain/src/canvas_factory.cairo +++ b/onchain/src/canvas_factory.cairo @@ -113,30 +113,29 @@ pub mod CanvasFactory { fn create_canvas( ref self: ContractState, init_params: super::Canvas::InitParams ) -> (ContractAddress, u64) { + // TODO: Serialize before calling this function to defer serialization to the contract input let mut init_params_serialized = array![]; init_params.serialize(ref init_params_serialized); let deploy_res = deploy_syscall( self.canvas_class_hash.read(), - get_caller_address().into(), + self.canvas_count.read().into(), init_params_serialized.span(), - false + true ); - let (canvas_address, _) = deploy_res.unwrap(); + if deploy_res.is_err() { + panic!("Failed to deploy canvas contract"); + } + let (addr, _response) = deploy_res.unwrap(); let canvas_id = self.canvas_count.read(); - self.canvases.write(canvas_id, canvas_address); + self.canvases.write(canvas_id, addr); self.canvas_count.write(canvas_id + 1); - - // Auto-favorite the canvas for the creator - let caller = get_caller_address(); - self.canvas_favorites.write((canvas_id, caller), true); - self.emit(Event::CanvasFavorited(CanvasFavorited { canvas_id, user: caller })); - - // Emit the canvas created event self .emit( - Event::CanvasCreated(CanvasCreated { canvas_id, canvas_address, init_params }) + Event::CanvasCreated( + CanvasCreated { canvas_id, canvas_address: addr, init_params, } + ) ); - (canvas_address, canvas_id) + (addr, canvas_id) } fn get_canvas(self: @ContractState, canvas_id: u64) -> ContractAddress { diff --git a/onchain/src/multi_canvas.cairo b/onchain/src/multi_canvas.cairo index 11a403fa..676b12af 100644 --- a/onchain/src/multi_canvas.cairo +++ b/onchain/src/multi_canvas.cairo @@ -328,14 +328,12 @@ pub mod MultiCanvas { }; self.canvas_count.write(canvas_id + 1); self.unique_names.write(init_params.unique_name, true); + self.emit(CanvasCreated { canvas_id, init_params }); // Auto-favorite the canvas for the creator let caller = get_caller_address(); self.canvas_favorites.write((canvas_id, caller), true); self.emit(Event::CanvasFavorited(CanvasFavorited { canvas_id, user: caller })); - - // Emit canvas created event - self.emit(CanvasCreated { canvas_id, init_params }); canvas_id } @@ -498,14 +496,13 @@ pub mod MultiCanvas { assert(stencil.height <= MAX_STENCIL_SIZE, 'Stencil too large'); self.stencils.write((canvas_id, stencil_id), stencil.clone()); self.stencil_counts.write(canvas_id, stencil_id + 1); + self.emit(StencilAdded { canvas_id, stencil_id, stencil }); // Auto-favorite the stencil for the creator let caller = get_caller_address(); self.stencil_favorites.write((canvas_id, stencil_id, caller), true); self.emit(StencilFavorited { canvas_id, stencil_id, user: caller }); - // Emit the stencil added event - self.emit(StencilAdded { canvas_id, stencil_id, stencil }); stencil_id }