-
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
-- 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 (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 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
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
, parseLongPiece fields
, parseCanvas fields
)
of
( Ok lugWidth, Ok longPiece, Ok rendering ) ->
let
base =
Parameters.default
in
Ok { base | lugWidth = lugWidth, longPiece = longPiece, rendering = rendering }
( lugWidth, longPiece, rendering ) ->
[ mkErrors [ ( LugWidth, getError lugWidth ) ]
, 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 )
-- 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 FieldProps msg =
{ key : Key
, title : List (Html.Html msg)
, description : List (Html.Html msg)
, unit : Maybe String
, attrs : List (Html.Attribute msg)
}
field : Model -> FieldProps Msg -> Html.Html Msg
field model { key, title, description, attrs, unit } =
node "x-field"
[]
[ 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 ]
:: 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 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 }
[ field 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"
, attrs = step "1.0" :: lengthFieldAttrs constraints.lugWidth
}
]
, hr [] []
, group
{ title = [ text "Buckle / Clasp" ], description = Nothing }
[ field model
{ key = BuckleHoleCount
, title = [ text "Hole Count" ]
, description = [ text "Set 0 to disable buckle holes generation." ]
, unit = Nothing
, attrs = step "1" :: intFieldAttrs constraints.longPiece.buckleHole.count
}
, field 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"
, attrs =
step "1.0"
:: disabled (model.parameters.longPiece.buckleHole.count == 0)
:: lengthFieldAttrs constraints.longPiece.buckleHole.diameter
}
]
, hr [] []
, group
{ title = [ text "Rendering" ], description = Nothing }
[ field model
{ key = CanvasMargin
, title = [ text "Print Margin" ]
, description =
[ text "Lower values can cause printing problems depending on your printer." ]
, unit = Just "mm"
, attrs = step "1.0" :: lengthFieldAttrs constraints.rendering.margin
}
, field model
{ key = LineWidth
, title = [ text "Line Width" ]
, description =
[ text "Stroke width (thickness) of the cutting lines and seam lines." ]
, unit = Just "mm"
, attrs = step "0.1" :: lengthFieldAttrs constraints.rendering.lineWidth
}
]
]