FloorVisualizer/internal/handler/calc_handler.go
dindang cb7e595c27 Fix lint warnings and code cleanup
- 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>
2026-07-16 10:29:11 +08:00

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
}