From 68d2330646824f3bf20ea08a203e2c7f5e740053 Mon Sep 17 00:00:00 2001 From: Cian Hughes Date: Fri, 4 Apr 2025 18:04:48 +0100 Subject: [PATCH] Wrote simple blender service for rendering models and added to stack --- blender_server/Dockerfile | 15 ++++ blender_server/go.mod | 3 + blender_server/main.go | 167 ++++++++++++++++++++++++++++++++++++ blender_server/main.py.tmpl | 44 ++++++++++ 4 files changed, 229 insertions(+) create mode 100644 blender_server/Dockerfile create mode 100644 blender_server/go.mod create mode 100644 blender_server/main.go create mode 100644 blender_server/main.py.tmpl diff --git a/blender_server/Dockerfile b/blender_server/Dockerfile new file mode 100644 index 0000000..927274a --- /dev/null +++ b/blender_server/Dockerfile @@ -0,0 +1,15 @@ +FROM golang:alpine AS builder +WORKDIR /app +COPY go.mod go.sum* ./ +RUN go mod download +COPY . . +RUN CGO_ENABLED=0 GOOS=linux go build -o server ./main.go + +FROM alpine:latest +RUN apk update +RUN apk add blender +WORKDIR /root/ +COPY --from=builder /app/server . +COPY main.py.tmpl . +EXPOSE 8080 +CMD ["./server"] diff --git a/blender_server/go.mod b/blender_server/go.mod new file mode 100644 index 0000000..dbd8af2 --- /dev/null +++ b/blender_server/go.mod @@ -0,0 +1,3 @@ +module blender_server + +go 1.23.3 diff --git a/blender_server/main.go b/blender_server/main.go new file mode 100644 index 0000000..3b54cf4 --- /dev/null +++ b/blender_server/main.go @@ -0,0 +1,167 @@ +package main + +import ( + "bytes" + "fmt" + "io" + "os" + "os/exec" + "text/template" + "encoding/json" + "log" + "net/http" + "path/filepath" +) + +func main() { + http.HandleFunc("/create_model", handleCreateModel) + + port := os.Getenv("PORT") + if port == "" { + port = "8080" + } + log.Printf("Server starting on port %s", port) + if err := http.ListenAndServe(":"+port, nil); err != nil { + log.Fatalf("Failed to start server: %v", err) + } +} + +const ModelPath = "./model.stl" + +type Request struct { + ModelCode string `json:"model_code"` +} + +func handleCreateModel(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodPost { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + bodyBytes, err := io.ReadAll(r.Body) + if err != nil { + http.Error(w, "Error reading request body", http.StatusBadRequest) + return + } + + log.Printf("Received request body (first 100 chars): %s", truncateString(string(bodyBytes), 100)) + + // Close the original body and create a new one with the same data + r.Body.Close() + r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) + + var req Request + err = json.NewDecoder(r.Body).Decode(&req) + if err != nil { + log.Printf("JSON decode error: %v", err) + http.Error(w, fmt.Sprintf("Invalid request body: %v", err), http.StatusBadRequest) + return + } + + log.Printf("Successfully parsed request: ModelCode (first 50 chars)=%s", truncateString(req.ModelCode, 50)) + + absModelPath, err := filepath.Abs(ModelPath) + if err != nil { + log.Printf("Error getting absolute path: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + + _ = os.Remove(absModelPath) + + model_expression, err := create_stl(req.ModelCode, absModelPath) + if err != nil { + http.Error(w, fmt.Sprintf("Error parsing templates: %v", err), http.StatusInternalServerError) + return + } + + log.Printf("Python expression generated (first 100 chars): %s", truncateString(model_expression, 100)) + + tmpFile, err := os.CreateTemp("", "blender_*.py") + if err != nil { + log.Printf("Error creating temp file: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + defer os.Remove(tmpFile.Name()) + + _, err = tmpFile.WriteString(model_expression) + if err != nil { + log.Printf("Error writing to temp file: %v", err) + http.Error(w, "Internal server error", http.StatusInternalServerError) + return + } + tmpFile.Close() + + cmd := exec.Command("blender", "-b", "--python", tmpFile.Name()) + output, err := cmd.CombinedOutput() + if err != nil { + log.Printf("Command execution failed: %v, Output: %s", err, string(output)) + http.Error(w, fmt.Sprintf("Command execution failed: %v", err), http.StatusInternalServerError) + return + } + + log.Printf("Blender command executed: output (first 200 chars): %s", truncateString(string(output), 200)) + + if _, err := os.Stat(absModelPath); os.IsNotExist(err) { + log.Printf("Error: STL file was not created at %s", absModelPath) + http.Error(w, "Failed to generate STL file", http.StatusInternalServerError) + return + } + + stlData, err := os.ReadFile(absModelPath) + if err != nil { + log.Printf("Error reading STL file: %v", err) + http.Error(w, "Error reading generated STL file", http.StatusInternalServerError) + return + } + + log.Printf("STL file read successfully, size: %d bytes", len(stlData)) + + w.Header().Set("Content-Type", "application/octet-stream") + w.Header().Set("Content-Disposition", fmt.Sprintf("attachment; filename=%s", filepath.Base(ModelPath))) + w.Header().Set("Content-Length", fmt.Sprintf("%d", len(stlData))) + + if _, err := w.Write(stlData); err != nil { + log.Printf("Failed to write response: %v", err) + } else { + log.Printf("STL file sent successfully") + } +} + +type ModelTemplateVars struct { + ModelCode string + Filename string +} + +func create_stl(model_code string, filename string) (string, error) { + t := template.New("main.py.tmpl") + + t, err := t.ParseFiles("main.py.tmpl") + if err != nil { + fmt.Println("Error parsing templates:", err) + return "", err + } + + var buf bytes.Buffer + data := ModelTemplateVars{ + ModelCode: model_code, + Filename: filename, + } + + err = t.ExecuteTemplate(&buf, "main.py.tmpl", data) + if err != nil { + fmt.Println("Error executing template:", err) + return "", err + } + + result := buf.String() + return result, nil +} + +func truncateString(s string, maxLen int) string { + if len(s) <= maxLen { + return s + } + return s[:maxLen] + "..." +} diff --git a/blender_server/main.py.tmpl b/blender_server/main.py.tmpl new file mode 100644 index 0000000..3e6d652 --- /dev/null +++ b/blender_server/main.py.tmpl @@ -0,0 +1,44 @@ +import bpy # type: ignore + + +{{.ModelCode}} + + +def guarded_model() -> bpy.types.Object: + try: + out = model() # type: ignore + if out is None: + raise TypeError("Function `model` cannot return type `None`.") + return out + except NameError: + raise NotImplementedError("No function named `model` was provided!") + + +def export_to_stl(obj: bpy.types.Object): + """ + Export a Blender object as an STL binary blob. + + Parameters: + obj (bpy.types.Object): The object to export + + Returns: + bytes: Binary data of the STL file + """ + import tempfile + import os + + # Ensure the object is the only object, is selected, and is active + bpy.ops.object.select_all(action="SELECT") + obj.select_set(False) + bpy.ops.object.delete() + obj.select_set(True) + bpy.context.view_layer.objects.active = obj + + bpy.ops.wm.stl_export(filepath="{{.Filename}}") + + +def main(): + export_to_stl(guarded_model()) + + +main()