-
1
-
2
-
3
-
4
-
5
-
6
-
7
-
8
-
9
-
10
-
11
-
12
-
13
-
14
-
15
-
16
-
17
-
18
-
19
-
20
-
21
-
22
-
23
-
24
-
25
-
26
-
27
-
28
-
29
-
30
-
31
-
32
-
33
-
34
-
35
-
36
-
37
-
38
-
39
-
40
-
41
-
42
-
43
-
44
-
45
-
46
-
47
-
48
-
49
-
50
-
51
-
52
-
53
-
54
-
55
-
56
-
57
-
58
-
59
-
60
-
61
-
62
-
63
-
64
-
65
-
66
-
67
-
68
-
69
-
70
-
71
-
72
-
73
-
74
-
75
-
76
-
77
-
78
-
79
-
80
-
81
-
82
-
83
-
84
-
85
-
86
-
87
-
88
-
89
-
90
-
91
-
92
-
93
-
94
-
95
-
96
-
97
-
98
-
99
-
100
-
101
-
102
-
103
-
104
-
105
-
106
-
107
-
108
-
109
-
110
-
111
-
112
-
113
-
114
-
115
-
116
-
117
-
118
-
119
-
120
-
121
-
122
-
123
-
124
-
125
-
126
-
127
-
128
-
129
-
130
-
131
-
132
-
133
-
134
-
135
-
136
-
137
-
138
-
139
-
140
-
141
-
142
-
143
-
144
-
145
-
146
-
147
-
148
-
149
-
150
-
151
-
152
-
153
-
154
-
155
-
156
-
157
-
158
-
159
-
160
-
161
-
162
-
163
-
164
-
165
-
166
-
167
-
168
-
169
-
170
-
171
-
172
-
173
-
174
-
175
-
176
-
177
-
178
-
179
-
180
-
181
-
182
-
183
-
184
-
185
-
186
-
187
-
188
-
189
-
190
-
191
-
192
-
193
-
194
-
195
-
196
-
197
-
198
-
199
-
200
-
201
-
202
-
203
-
204
-
205
-
206
-
207
-
208
-
209
-
210
-
211
-
212
-
213
-
214
-
215
-
216
-
217
-
218
-
219
-
220
-
221
-
222
-
223
-
224
-
225
-
226
-
227
-
228
-
229
-
230
-
231
-
232
-
233
-
234
-
235
-
236
-
237
-
238
-
239
-
240
-
241
-
242
-
243
-
244
-
245
-
246
-
247
-
248
-
249
-
250
-
251
-
252
-
253
-
254
-
255
-
256
-
257
-
258
-
259
-
260
-
261
-
262
-
263
-
264
-
265
-
266
-
267
-
268
-
269
-
270
-
271
-
272
-
273
-
274
-
275
-
276
-
277
-
278
-
279
-
280
-
281
-
282
-
283
-
284
-
285
-
286
-
287
-
288
-
289
-
290
-
291
-
292
-
293
-
294
-
295
-
296
-
297
-
298
-
299
-
300
-
301
-
302
-
303
-
304
-
305
-
306
-
307
-
308
-
309
-
310
-
311
-
312
-
313
-
314
-
315
-
316
-
317
-
318
-
319
-
320
-
321
-
322
-
323
-
324
-
325
-
326
-
327
-
328
-
329
-
330
-
331
-
332
-
333
-
334
-
335
-
336
-
337
-
338
-
339
-
340
-
341
-
342
-
343
-
344
-
345
-
346
-
347
-
348
-
349
-
350
-
351
-
352
-
353
-
354
-
355
-
356
-
357
-
358
-
359
-
360
-
361
-
362
-
363
-
364
-
365
-
366
-
367
-
368
-
369
-
370
-
371
-
372
-
373
-
374
-
375
-
376
-
377
-
378
-
379
-
380
-
381
-
382
-
383
-
384
-
385
-
386
-
387
-
388
-
389
-
390
-
391
-
392
-
393
-
394
-
395
-
396
-
397
-
398
-
399
-
400
-
401
-
402
-
403
-
404
-
405
-
406
-
407
-
408
-
409
-
410
-
411
-
412
-
413
-
414
-
415
-
416
-
417
-
418
-
419
-
420
-
421
-
422
-
423
-
424
-
425
-
426
-
427
-
428
-
429
-
430
-
431
-
432
-
433
-
434
-
435
-
436
-
437
-
438
-
439
-
440
-
441
-
442
-
443
-
444
-
445
-
446
-
447
-
448
-
449
-
450
-
451
-
452
-
453
-
454
-
455
-
456
-
457
-
458
-
459
-
460
-
461
-
462
-
463
-
464
-
465
-
466
-
467
-
468
-
469
-
470
-
471
-
472
-
473
-
474
-
475
-
476
-
477
-
478
-
479
-
480
-
481
-
482
-
483
-
484
-
485
-
486
-
487
-
488
-
489
-
490
-
491
-
492
-
493
-
494
-
495
-
496
-
497
-
498
-
499
-
500
-
501
-
502
-
503
-
504
-
505
-
506
-
507
-
508
-
509
-
510
-
511
-
512
-
513
-
514
-
515
-
516
-
517
-
518
-
519
-
520
-
521
-
522
-
523
-
524
-
525
-
526
-
527
-
528
-
529
-
530
-
531
-
532
-
533
-
534
-
535
-
536
-
537
-
538
-
539
-
540
-
541
-
542
-
543
-
544
-- Copyright 2026 Shota FUJI
--
-- This Source Code Form is subject to the terms of the Mozilla Public
-- License, v. 2.0. If a copy of the MPL was not distributed with this
-- file, You can obtain one at https://mozilla.org/MPL/2.0/.
--
-- SPDX-License-Identifier: MPL-2.0
module Parameters.App exposing (Model, Msg, init, update, view)
import Dict exposing (Dict)
import Html exposing (hr, input, label, node, p, span, text)
import Html.Attributes exposing (..)
import Html.Events exposing (onInput)
import Html.LivingStandard exposing (..)
import Length exposing (Length, toMM)
import Parameters exposing (ColorSchema(..), Parameters)
import Parameters.Constraints exposing (NumberConstraints, constraints)
import Parameters.Key as Key exposing (Key(..))
import Parameters.Parser exposing (Error(..), parseInt, parseLength)
-- MODEL
type alias Fields =
Dict String String
type alias Errors =
Dict String Error
type alias Model =
{ fields : Fields
, errors : Errors
, parameters : Parameters
}
init : Parameters -> Model
init params =
let
fields =
Dict.fromList
[ ( Key.toString LugWidth, String.fromFloat (toMM params.lugWidth) )
, ( Key.toString SurfaceThickness, String.fromFloat (toMM params.thickness) )
, ( Key.toString LiningThickness, String.fromFloat (toMM params.lining) )
, ( Key.toString BuckleHoleCount, String.fromInt params.longPiece.buckleHole.count )
, ( Key.toString BuckleHoleDiameter, String.fromFloat (toMM params.longPiece.buckleHole.diameter) )
, ( Key.toString CanvasMargin, String.fromFloat (toMM params.rendering.margin) )
, ( Key.toString LineWidth, String.fromFloat (toMM params.rendering.lineWidth) )
]
in
{ fields = fields
, errors =
case parse fields of
Ok _ ->
Dict.empty
Err errors ->
errors
, parameters = params
}
-- UPDATE
type Msg
= FieldChanged Key String
| ColorSchemaChanged ColorSchema
parseField : Key -> (String -> Result Error a) -> Fields -> Result Error a
parseField key f fields =
Dict.get (Key.toString key) fields
|> Maybe.withDefault ""
|> f
mkErrors : List ( Key, Maybe Error ) -> Errors
mkErrors list =
list
|> List.filterMap (\( key, error ) -> Maybe.map (\e -> ( Key.toString key, e )) error)
|> Dict.fromList
getError : Result a b -> Maybe a
getError r =
case r of
Err e ->
Just e
_ ->
Nothing
parseBuckleHole : Fields -> Result Errors Parameters.BuckleHole
parseBuckleHole fields =
case
( parseField BuckleHoleCount (parseInt constraints.longPiece.buckleHole.count) fields
, parseField BuckleHoleDiameter (parseLength constraints.longPiece.buckleHole.diameter) fields
)
of
( Ok count, Ok diameter ) ->
let
base =
Parameters.default.longPiece.buckleHole
in
Ok { base | count = count, diameter = diameter }
( count, diameter ) ->
Err
(mkErrors
[ ( BuckleHoleCount, getError count )
, ( BuckleHoleDiameter, getError diameter )
]
)
parseLongPiece : Fields -> Result Errors Parameters.LongPiece
parseLongPiece fields =
case parseBuckleHole fields of
Ok buckleHole ->
let
base =
Parameters.default.longPiece
in
Ok { base | buckleHole = buckleHole }
Err errors ->
Err errors
parseCanvas : Fields -> Result Errors Parameters.Rendering
parseCanvas fields =
case
( parseField CanvasMargin (parseLength constraints.rendering.margin) fields
, parseField LineWidth (parseLength constraints.rendering.lineWidth) fields
)
of
( Ok margin, Ok lineWidth ) ->
let
base =
Parameters.default.rendering
in
Ok { base | margin = margin, lineWidth = lineWidth }
( margin, lineWidth ) ->
Err
(mkErrors
[ ( CanvasMargin, getError margin )
, ( LineWidth, getError lineWidth )
]
)
parse : Fields -> Result Errors Parameters
parse fields =
case
( ( parseField LugWidth (parseLength constraints.lugWidth) fields
, parseField LiningThickness (parseLength constraints.lining) fields
, parseField SurfaceThickness (parseLength constraints.thickness) fields
)
, parseLongPiece fields
, parseCanvas fields
)
of
( ( Ok lugWidth, Ok lining, Ok thickness ), Ok longPiece, Ok rendering ) ->
let
base =
Parameters.default
in
Ok
{ base
| lugWidth = lugWidth
, lining = lining
, thickness = thickness
, longPiece = longPiece
, rendering = rendering
}
( ( lugWidth, lining, thickness ), longPiece, rendering ) ->
[ mkErrors [ ( LugWidth, getError lugWidth ) ]
, mkErrors [ ( LiningThickness, getError lining ) ]
, mkErrors [ ( SurfaceThickness, getError thickness ) ]
, getError longPiece |> Maybe.withDefault Dict.empty
, getError rendering |> Maybe.withDefault Dict.empty
]
|> List.map Dict.toList
|> List.concat
|> Dict.fromList
|> Err
update : Msg -> Model -> ( Model, Cmd Msg )
update msg model =
case msg of
FieldChanged key value ->
let
fields =
Dict.insert (Key.toString key) value model.fields
in
case parse fields of
Ok newParams ->
( { model
| fields = fields
, errors = Dict.empty
, parameters = newParams
}
, Cmd.none
)
Err errors ->
( { model | fields = fields, errors = errors }, Cmd.none )
ColorSchemaChanged schema ->
let
{ parameters } =
model
{ rendering } =
parameters
in
( { model | parameters = { parameters | rendering = { rendering | colorSchema = schema } } }
, Cmd.none
)
-- VIEW
lengthFieldAttrs : NumberConstraints Length -> List (Html.Attribute msg)
lengthFieldAttrs { min, max } =
[ case min of
Just x ->
toMM x
|> String.fromFloat
|> Html.Attributes.min
Nothing ->
class ""
, case max of
Just x ->
toMM x
|> String.fromFloat
|> Html.Attributes.max
Nothing ->
class ""
, attribute "inputmode" "numeric"
, required True
]
intFieldAttrs : NumberConstraints Int -> List (Html.Attribute msg)
intFieldAttrs { min, max } =
[ case min of
Just x ->
Html.Attributes.min (String.fromInt x)
Nothing ->
class ""
, case max of
Just x ->
Html.Attributes.max (String.fromInt x)
Nothing ->
class ""
, attribute "inputmode" "numeric"
, required True
]
ariaInvalid : Bool -> Html.Attribute msg
ariaInvalid invalid =
attribute "aria-invalid"
(if invalid then
"true"
else
"false"
)
ariaDescribedBy : List String -> Html.Attribute msg
ariaDescribedBy ids =
case ids of
[] ->
class ""
_ ->
ids
|> String.join " "
|> attribute "aria-describedby"
errorId : Key -> String
errorId key =
Key.toString key ++ "_error"
errorText : Error -> List (Html.Html msg)
errorText e =
case e of
MissingValue ->
[ text "This field is required." ]
BelowMin n ->
[ text ("Value must be greater than or equals to " ++ String.fromFloat n ++ ".") ]
AboveMax n ->
[ text ("Value must be less than or equals to " ++ String.fromFloat n ++ ".") ]
NotALength ->
[ text "Invalid length value. Set an integer or a floating point number." ]
NotAnInt ->
[ text "Invalid integer value. This field only accepts integer value." ]
descriptionId : Key -> String
descriptionId key =
Key.toString key ++ "_description"
type alias NumberFieldProps msg =
{ key : Key
, title : List (Html.Html msg)
, description : List (Html.Html msg)
, unit : Maybe String
, attrs : List (Html.Attribute msg)
, disabled : Bool
}
numberField : Model -> NumberFieldProps Msg -> Html.Html Msg
numberField model { key, title, description, attrs, unit, disabled } =
node "x-field"
[ if disabled then
attribute "disabled" ""
else
class ""
]
[ label [ for (Key.toString key), slot "title" ] title
, p
[ id (descriptionId key), slot "description" ]
description
, node "x-number-input"
[]
[ input
(id (Key.toString key)
:: value (model.fields |> Dict.get (Key.toString key) |> Maybe.withDefault "")
:: onInput (FieldChanged key)
:: ariaInvalid (not (Dict.get (Key.toString key) model.errors == Nothing))
:: ariaDescribedBy [ errorId key, descriptionId key ]
:: Html.Attributes.disabled disabled
:: attrs
)
[]
, case unit of
Just u ->
span [ slot "unit" ] [ text u ]
Nothing ->
text ""
]
, p
[ id (errorId key), slot "error" ]
(Dict.get (Key.toString key) model.errors
|> Maybe.map errorText
|> Maybe.withDefault []
)
]
type alias RadioBoxProps msg =
{ label : List (Html.Html msg)
, description : List (Html.Html msg)
, key : Key
, value : String
, checked : Bool
, onCheck : msg
}
radioBox : RadioBoxProps msg -> List (Html.Attribute msg) -> Html.Html msg
radioBox { label, description, key, value, checked, onCheck } attrs =
let
id =
Key.toString key ++ "_" ++ value
choiceDescriptionId =
id ++ "__description"
in
node "x-radio-box"
[ if checked then
attribute "checked" ""
else
class ""
]
[ Html.label [ for id, slot "label" ] label
, Html.p [ Html.Attributes.id choiceDescriptionId, slot "description" ] description
, input
(type_ "radio"
:: Html.Attributes.id id
:: Html.Attributes.name (Key.toString key)
:: Html.Attributes.value value
:: Html.Attributes.checked checked
:: Html.Events.onCheck (\_ -> onCheck)
:: ariaDescribedBy [ choiceDescriptionId ]
:: attrs
)
[]
]
type alias GroupProps msg =
{ title : List (Html.Html msg)
, description : Maybe (List (Html.Html msg))
}
group : GroupProps Msg -> List (Html.Html Msg) -> Html.Html Msg
group { title, description } children =
node "x-field-group"
[]
(p [ slot "title" ] title
:: (description |> Maybe.map (p [ slot "description" ]) |> Maybe.withDefault (text ""))
:: children
)
view : Model -> List (Html.Html Msg)
view model =
[ group
{ title = [ text "General" ], description = Nothing }
[ numberField model
{ key = LugWidth
, title = [ text "Lug width" ]
, description =
[ text "This will be the final width of your strap. "
, text "You can use a size smaller than your lug width to create a play."
]
, unit = Just "mm"
, disabled = False
, attrs = step "1.0" :: lengthFieldAttrs constraints.lugWidth
}
, numberField model
{ key = SurfaceThickness
, title = [ text "Surface Leather Thickness" ]
, description = [ text "Thickness of surface leather." ]
, unit = Just "mm"
, disabled = False
, attrs = step "0.1" :: lengthFieldAttrs constraints.thickness
}
, numberField model
{ key = LiningThickness
, title = [ text "Lining Leather Thickness" ]
, description =
[ text "Thickness of lining leather. Set to 0 to disable lining generation."
]
, unit = Just "mm"
, disabled = False
, attrs = step "0.1" :: lengthFieldAttrs constraints.lining
}
]
, hr [] []
, group
{ title = [ text "Buckle / Clasp" ], description = Nothing }
[ numberField model
{ key = BuckleHoleCount
, title = [ text "Hole Count" ]
, description = [ text "Set 0 to disable buckle holes generation." ]
, unit = Nothing
, disabled = False
, attrs = step "1" :: intFieldAttrs constraints.longPiece.buckleHole.count
}
, numberField model
{ key = BuckleHoleDiameter
, title = [ text "Hole Diameter" ]
, description =
[ text "Diameter of buckle holes. You can leave the default value if you're going to use the center mark." ]
, unit = Just "mm"
, disabled = model.parameters.longPiece.buckleHole.count == 0
, attrs =
step "1.0" :: lengthFieldAttrs constraints.longPiece.buckleHole.diameter
}
]
, hr [] []
, group
{ title = [ text "Rendering" ], description = Nothing }
[ numberField model
{ key = CanvasMargin
, title = [ text "Print Margin" ]
, description =
[ text "Lower values can cause printing problems depending on your printer." ]
, unit = Just "mm"
, disabled = False
, attrs = step "1.0" :: lengthFieldAttrs constraints.rendering.margin
}
, numberField model
{ key = LineWidth
, title = [ text "Line Width" ]
, description =
[ text "Stroke width (thickness) of the cutting lines and seam lines." ]
, unit = Just "mm"
, disabled = False
, attrs = step "0.1" :: lengthFieldAttrs constraints.rendering.lineWidth
}
, node "x-field"
[]
[ label [ for (Key.toString ColorSchema), slot "title" ] [ text "Color Schema" ]
, p
[ id (descriptionId ColorSchema), slot "description" ]
[ text "Color schema of the template." ]
, radioBox
{ key = ColorSchema
, label = [ text "Black on White" ]
, description = [ text "Select this if you print the template with a regular printer." ]
, value = "bow"
, checked = model.parameters.rendering.colorSchema == BlackOnWhite
, onCheck = ColorSchemaChanged BlackOnWhite
}
[]
, radioBox
{ key = ColorSchema
, label = [ text "White on Black" ]
, description = [ text "For digital output. Printing this with an inkjet printer is not a good idea." ]
, value = "wob"
, checked = model.parameters.rendering.colorSchema == WhiteOnBlack
, onCheck = ColorSchemaChanged WhiteOnBlack
}
[]
]
]
]