FloorVisualizer/internal/handler/calc_handler.go

231 lines
6.5 KiB
Go
Raw Normal View History

package handler
import (
"encoding/json"
"math"
"net/http"
)
// ── Calculator models ───────────────────────────────────────
type CalcRoomInput struct {
Name string `json:"name"`
Length float64 `json:"length"`
Width float64 `json:"width"`
Deductions []CalcDeduction `json:"deductions"`
}
type CalcDeduction struct {
Name string `json:"name"`
Shape string `json:"shape"` // rectangle, circle, custom
Length float64 `json:"length"`
Width float64 `json:"width"`
Radius float64 `json:"radius"`
Area float64 `json:"area"` // custom area override
}
type CalcInput struct {
FloorType string `json:"floor_type"` // wood, tile
Rooms []CalcRoomInput `json:"rooms"`
PatternIndex int `json:"pattern_index"`
PieceLength float64 `json:"piece_length"`
PieceWidth float64 `json:"piece_width"`
PiecesPerBox int `json:"pieces_per_box"`
PricePerSqm float64 `json:"price_per_sqm"`
JointWidth float64 `json:"joint_width"`
}
type CalcRoomResult struct {
Name string `json:"name"`
Length float64 `json:"length"`
Width float64 `json:"width"`
Area float64 `json:"area"`
Deductions []CalcDeduction `json:"deductions"`
DeductionTotal float64 `json:"deduction_total"`
NetArea float64 `json:"net_area"`
}
type CalcResult struct {
FloorType string `json:"floor_type"`
Rooms []CalcRoomResult `json:"rooms"`
TotalRoomArea float64 `json:"total_room_area"`
TotalDeduct float64 `json:"total_deduct"`
TotalNetArea float64 `json:"total_net_area"`
WasteRate float64 `json:"waste_rate"`
WasteArea float64 `json:"waste_area"`
TotalArea float64 `json:"total_area"`
PiecesNeeded int `json:"pieces_needed"`
BoxesNeeded *float64 `json:"boxes_needed,omitempty"`
JointWidth float64 `json:"joint_width"`
GroutKg *float64 `json:"grout_kg,omitempty"`
GroutBags *int `json:"grout_bags,omitempty"`
PricePerSqm float64 `json:"price_per_sqm"`
TotalPrice *float64 `json:"total_price,omitempty"`
PatternName string `json:"pattern_name"`
PieceLength float64 `json:"piece_length"`
PieceWidth float64 `json:"piece_width"`
}
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 handleCalc(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
writeErr(w, 405, "Method not allowed")
return
}
var input 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 CalcInput) 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 []CalcRoomResult
totalRoomArea := 0.0
totalDeduct := 0.0
for _, room := range input.Rooms {
area := room.Length * room.Width
deductTotal := 0.0
var dedResults []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, 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 := 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 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
}