- Remove duplicate calc structs, use model.CalcXxx - Replace log.Printf with logger in recommend handler - Remove dead code (unused var, dummy imports) - Clean up response helpers - Add api/ package with route registration Co-Authored-By: Claude <noreply@anthropic.com>
179 lines
4.2 KiB
Go
179 lines
4.2 KiB
Go
package handler
|
|
|
|
import (
|
|
"encoding/json"
|
|
"math"
|
|
"net/http"
|
|
|
|
"floorvisualizer/internal/model"
|
|
)
|
|
|
|
var woodPatternsCalc = []struct {
|
|
Name string
|
|
Waste float64
|
|
}{
|
|
{"Straight Lay", 0.05},
|
|
{"Offset Lay", 0.08},
|
|
{"Herringbone", 0.12},
|
|
{"Diagonal 45deg", 0.15},
|
|
}
|
|
|
|
var tilePatternsCalc = []struct {
|
|
Name string
|
|
Waste float64
|
|
}{
|
|
{"Straight Grid", 0.05},
|
|
{"Running Bond", 0.07},
|
|
{"1/3 Offset", 0.08},
|
|
{"Hexagonal", 0.10},
|
|
{"Diagonal 45deg", 0.15},
|
|
{"Versailles", 0.20},
|
|
}
|
|
|
|
// ── Handler ─────────────────────────────────────────────────
|
|
|
|
func Calc(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != http.MethodPost {
|
|
writeErr(w, 405, "Method not allowed")
|
|
return
|
|
}
|
|
|
|
var input model.CalcInput
|
|
if err := json.NewDecoder(r.Body).Decode(&input); err != nil {
|
|
writeJSON(w, 400, map[string]string{"error": "invalid JSON: " + err.Error()})
|
|
return
|
|
}
|
|
|
|
if len(input.Rooms) == 0 {
|
|
writeJSON(w, 400, map[string]string{"error": "at least one room required"})
|
|
return
|
|
}
|
|
if input.PieceLength <= 0 || input.PieceWidth <= 0 {
|
|
writeJSON(w, 400, map[string]string{"error": "piece dimensions required"})
|
|
return
|
|
}
|
|
|
|
result := calculate(input)
|
|
writeJSON(w, 200, result)
|
|
}
|
|
|
|
// ── Calculation ─────────────────────────────────────────────
|
|
|
|
func calculate(input model.CalcInput) model.CalcResult {
|
|
patterns := woodPatternsCalc
|
|
if input.FloorType == "tile" {
|
|
patterns = tilePatternsCalc
|
|
}
|
|
if input.PatternIndex < 0 || input.PatternIndex >= len(patterns) {
|
|
input.PatternIndex = 0
|
|
}
|
|
wasteRate := patterns[input.PatternIndex].Waste
|
|
|
|
var roomResults []model.CalcRoomResult
|
|
totalRoomArea := 0.0
|
|
totalDeduct := 0.0
|
|
|
|
for _, room := range input.Rooms {
|
|
area := room.Length * room.Width
|
|
deductTotal := 0.0
|
|
var dedResults []model.CalcDeduction
|
|
|
|
for _, d := range room.Deductions {
|
|
dedArea := deductionArea(d)
|
|
deductTotal += dedArea
|
|
d.Area = round2(dedArea)
|
|
dedResults = append(dedResults, d)
|
|
}
|
|
|
|
netArea := area - deductTotal
|
|
if netArea < 0 {
|
|
netArea = 0
|
|
}
|
|
|
|
roomResults = append(roomResults, model.CalcRoomResult{
|
|
Name: room.Name, Length: room.Length, Width: room.Width,
|
|
Area: round2(area), Deductions: dedResults,
|
|
DeductionTotal: round2(deductTotal), NetArea: round2(netArea),
|
|
})
|
|
totalRoomArea += area
|
|
totalDeduct += deductTotal
|
|
}
|
|
|
|
totalNetArea := totalRoomArea - totalDeduct
|
|
if totalNetArea < 0 {
|
|
totalNetArea = 0
|
|
}
|
|
|
|
// Adjust piece for joint
|
|
effLength := input.PieceLength
|
|
effWidth := input.PieceWidth
|
|
if input.FloorType == "tile" && input.JointWidth > 0 {
|
|
effLength += input.JointWidth
|
|
effWidth += input.JointWidth
|
|
}
|
|
|
|
pieceArea := effLength * effWidth
|
|
if pieceArea <= 0 {
|
|
pieceArea = 1
|
|
}
|
|
|
|
totalPieces := totalNetArea / pieceArea
|
|
wasteArea := totalNetArea * wasteRate
|
|
totalArea := totalNetArea + wasteArea
|
|
piecesNeeded := int(math.Ceil(totalArea / pieceArea))
|
|
|
|
result := model.CalcResult{
|
|
FloorType: input.FloorType,
|
|
Rooms: roomResults,
|
|
TotalRoomArea: round2(totalRoomArea),
|
|
TotalDeduct: round2(totalDeduct),
|
|
TotalNetArea: round2(totalNetArea),
|
|
WasteRate: round2(wasteRate * 100),
|
|
WasteArea: round2(wasteArea),
|
|
TotalArea: round2(totalArea),
|
|
PiecesNeeded: piecesNeeded,
|
|
JointWidth: input.JointWidth,
|
|
PricePerSqm: input.PricePerSqm,
|
|
PatternName: patterns[input.PatternIndex].Name,
|
|
PieceLength: input.PieceLength,
|
|
PieceWidth: input.PieceWidth,
|
|
}
|
|
|
|
// Boxes
|
|
if input.PiecesPerBox > 0 {
|
|
b := math.Ceil(float64(piecesNeeded) / float64(input.PiecesPerBox))
|
|
result.BoxesNeeded = &b
|
|
}
|
|
|
|
// Grout (tile only)
|
|
if input.FloorType == "tile" && totalPieces > 0 {
|
|
kg := totalNetArea * 0.1
|
|
result.GroutKg = &kg
|
|
bags := int(math.Ceil(kg / 2.0))
|
|
result.GroutBags = &bags
|
|
}
|
|
|
|
// Price
|
|
if input.PricePerSqm > 0 {
|
|
p := totalArea * input.PricePerSqm
|
|
result.TotalPrice = &p
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
func deductionArea(d model.CalcDeduction) float64 {
|
|
switch d.Shape {
|
|
case "circle":
|
|
return math.Pi * d.Radius * d.Radius
|
|
case "custom":
|
|
return d.Area
|
|
default: // rectangle
|
|
return d.Length * d.Width
|
|
}
|
|
}
|
|
|
|
func round2(v float64) float64 {
|
|
return math.Round(v*100) / 100
|
|
}
|