-
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
-
545
-
546
-
547
-
548
-
549
-
550
-
551
-
552
-
553
-
554
-
555
-
556
-
557
-
558
-
559
-
560
-
561
-
562
-
563
-
564
-
565
-
566
-
567
-
568
-
569
-
570
-
571
-
572
-
573
-
574
-
575
-
576
-
577
-
578
-
579
-
580
-
581
-
582
-
583
-
584
-
585
-
586
-
587
-
588
-
589
-
590
-
591
-
592
-
593
-
594
-
595
-
596
-
597
-
598
-
599
-
600
-
601
-
602
-
603
-
604
-
605
-
606
-
607
-
608
-
609
-
610
-
611
-
612
-
613
-
614
-
615
-
616
-
617
-
618
-
619
-
620
-
621
-
622
-
623
-
624
-
625
-
626
-
627
-
628
-
629
-
630
-
631
-
632
-
633
-
634
-
635
-
636
-
637
-
638
-
639
-
640
-
641
-
642
-
643
-
644
-
645
-
646
-
647
-
648
-
649
-
650
-
651
-
652
-
653
-
654
-
655
-
656
-
657
-
658
-
659
-
660
-
661
-
662
-
663
-
664
-
665
-
666
-
667
-
668
-
669
-
670
-
671
-
672
-
673
-
674
-
675
-
676
-
677
-
678
-
679
-
680
-
681
-
682
-
683
-
684
-
685
-
686
-
687
-
688
-
689
-
690
-
691
-
692
-
693
-
694
-
695
-
696
-
697
-
698
-
699
-
700
-
701
-
702
-
703
-
704
-
705
-
706
-
707
-
708
-
709
-
710
-
711
-
712
-
713
-
714
-
715
-
716
-
717
-
718
-
719
-
720
-
721
-
722
-
723
-
724
-
725
-
726
-
727
-
728
-
729
-
730
-
731
-
732
-
733
-
734
-
735
-
736
-
737
-
738
-
739
-
740
-- 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 Template.Cuts exposing (cuts)
import Length exposing (Length, toMM)
import Parameters exposing (LoopStyle(..), Parameters, Profile(..), TipStyle(..))
import Parameters.Key exposing (Key(..))
import Svg exposing (..)
import Svg.Attributes exposing (..)
import Svg.Path as Path exposing (..)
import Template.Layout.Container as Container exposing (aligned, columns, gapped, noGrow, padded, rows, sized)
import Template.Layout.Coordinate exposing (Request(..))
import Template.Layout.Item exposing (Item)
highlightStroke : Bool -> Svg.Attribute msg
highlightStroke enabled =
if enabled then
class "highlight-stroke"
else
class "highlight-none"
cuts : Parameters -> Maybe Key -> Item msg
cuts params highlighting =
-- TODO: Pack items more smartly, so it won't push infoArea out
rows
|> sized (AtLeast 0) (AtLeast 0)
|> aligned Container.Start
|> padded (toMM params.rendering.gap / 2)
|> gapped (toMM params.rendering.gap)
|> Container.build
([ columns
|> gapped (toMM params.rendering.gap * 2)
|> Container.build
[ surfacePieces params highlighting |> noGrow
, paddings params highlighting |> noGrow
]
|> Just
, loops params highlighting
]
|> List.filterMap identity
|> List.map noGrow
)
surfacePieces : Parameters -> Maybe Key -> Item msg
surfacePieces params highlighting =
rows
|> aligned Container.Center
|> gapped (toMM params.rendering.gap / 2)
|> Container.build
[ Item
{ width = Exactly 50, height = Exactly 5 }
(\p size ->
text_
[ x (String.fromFloat (p.x + size.width / 2))
, y (String.fromFloat p.y)
, fontSize "5"
, fontWeight "100"
, textAnchor "middle"
, dominantBaseline "hanging"
, fill "currentColor"
]
[ text "Main pieces" ]
)
, columns
|> gapped (toMM params.rendering.gap)
|> Container.build
[ longPiece params highlighting, shortPiece params highlighting ]
]
guideStroke : List (Svg.Attribute msg)
guideStroke =
[ fill "none"
, stroke "currentColor"
, strokeWidth "0.1"
, strokeDasharray "0.5 1"
]
type alias LongPieceShapeProps =
{ x : Float
, y : Float
, offset : Float
, highlighting : Maybe Key
, parameters : Parameters
}
longPieceShape : LongPieceShapeProps -> Svg msg
longPieceShape { x, y, offset, highlighting, parameters } =
let
shoulderWidth =
toMM parameters.shoulderWidth - offset * 2
length =
toMM parameters.longPiece.length - offset
taperUntil =
Basics.min
(toMM parameters.longPiece.buckleHole.distance
- (toMM parameters.longPiece.buckleHole.interval * toFloat (parameters.longPiece.buckleHole.adjustments + 1))
)
(length / 2)
taper =
case parameters.profile of
Straight ->
0.0
Tapered to ->
shoulderWidth - (toMM to - offset * 2)
buckleWidth =
shoulderWidth - taper
curvedSectionLength =
case parameters.longPiece.tip of
Round ->
buckleWidth / 2
Pointed _ ->
buckleWidth
in
Svg.g
[]
[ Svg.path
[ Path.d
(MoveTo Absolute ( x, y )
:: LineTo Relative ( taper / 2, taperUntil )
:: VerticalLineTo Relative (length - curvedSectionLength - taperUntil)
:: (case parameters.longPiece.tip of
Round ->
[ EllipticalArcCurve Relative
{ rx = buckleWidth / 2
, ry = buckleWidth / 2
, angle = 0.0
, largeArcFlag = LargeArc
, sweepFlag = Counterclockwise
, x = buckleWidth
, y = 0
}
[]
]
Pointed sharpness ->
let
sy =
buckleWidth * (toFloat (100 - sharpness) / 100)
in
[ CubicBezierCurve
Relative
( ( 0, sy )
, ( buckleWidth / 2, buckleWidth )
, ( buckleWidth / 2, buckleWidth )
)
[]
, CubicBezierCurve
Relative
( ( 0, 0 )
, ( buckleWidth / 2, -(buckleWidth - sy) )
, ( buckleWidth / 2, -buckleWidth )
)
[]
]
)
++ [ VerticalLineTo Relative -(length - curvedSectionLength - taperUntil)
, LineTo Relative ( taper / 2, -taperUntil )
, ClosePath
]
)
, fill "none"
, stroke "currentColor"
, strokeWidth (toMM parameters.rendering.lineWidth |> String.fromFloat)
, highlightStroke
(highlighting
== Just LongPieceLength
|| highlighting
== Just ShoulderWidth
|| highlighting
== Just Profile
|| highlighting
== Just TaperTo
|| (case parameters.profile of
Straight ->
False
Tapered _ ->
highlighting
== Just BuckleHoleDistance
|| highlighting
== Just BuckleHoleInterval
)
)
]
[]
, Svg.path
(Path.d
[ MoveTo Absolute ( x + shoulderWidth / 2, y )
, VerticalLineTo Relative length
]
:: highlightStroke (highlighting == Just LongPieceLength)
:: guideStroke
)
[]
, case parameters.longPiece.tip of
Round ->
g [] []
Pointed _ ->
Svg.path
(Path.d
[ MoveTo Absolute ( x + taper / 2, y + (length - curvedSectionLength) )
, HorizontalLineTo Relative buckleWidth
]
:: guideStroke
)
[]
, case parameters.profile of
Straight ->
g [] []
Tapered _ ->
Svg.path
(Path.d
[ MoveTo Absolute ( x + taper / 2, y + taperUntil )
, HorizontalLineTo Relative buckleWidth
]
:: highlightStroke
(highlighting
== Just ShoulderWidth
|| highlighting
== Just TaperTo
|| highlighting
== Just BuckleHoleDistance
|| highlighting
== Just BuckleHoleAdjustments
|| highlighting
== Just BuckleHoleInterval
|| highlighting
== Just BuckleHoleDiameter
)
:: guideStroke
)
[]
]
longPiece : Parameters -> Maybe Key -> Item msg
longPiece params highlighting =
let
shoulderWidth =
toMM params.shoulderWidth
length =
toMM params.longPiece.length
in
{ size = { width = Exactly shoulderWidth, height = Exactly length }
, element =
\p _ ->
g
[]
(longPieceShape
{ x = p.x
, y = p.y
, offset = 0
, highlighting = highlighting
, parameters = params
}
:: (List.range 0 (params.longPiece.buckleHole.adjustments * 2)
|> List.map
(\index ->
let
{ buckleHole } =
params.longPiece
distance =
toMM buckleHole.distance
interval =
toMM buckleHole.interval
in
hole
params
buckleHole.diameter
( p.x + shoulderWidth / 2
, p.y + distance + interval * toFloat (index - params.longPiece.buckleHole.adjustments)
)
[ highlightStroke
(highlighting
== Just BuckleHoleDiameter
|| highlighting
== Just BuckleHoleDistance
|| highlighting
== Just BuckleHoleInterval
|| highlighting
== Just BuckleHoleAdjustments
)
]
)
)
)
}
hole : Parameters -> Length -> ( Float, Float ) -> List (Svg.Attribute msg) -> Svg msg
hole params diameter ( cx, cy ) attrs =
let
radius =
toMM diameter / 2
in
g
[]
[ circle
(Svg.Attributes.cx (String.fromFloat cx)
:: Svg.Attributes.cy (String.fromFloat cy)
:: r (String.fromFloat radius)
:: fill "none"
:: stroke "currentColor"
:: strokeWidth (toMM params.rendering.lineWidth |> String.fromFloat)
:: attrs
)
[]
, g
[ fill "none"
, stroke "currentColor"
, strokeWidth "0.1"
]
[ Svg.path
[ Path.d
[ MoveTo Absolute ( cx - radius / 2, cy )
, HorizontalLineTo Relative radius
]
]
[]
, Svg.path
[ Path.d
[ MoveTo Absolute ( cx, cy - radius / 2 )
, VerticalLineTo Relative radius
]
]
[]
]
]
type alias ShortPieceShapeProps =
LongPieceShapeProps
shortPieceShape : ShortPieceShapeProps -> Svg msg
shortPieceShape { x, y, offset, parameters, highlighting } =
let
shoulderWidth =
toMM parameters.shoulderWidth - offset * 2
length =
toMM parameters.shortPiece.length - offset
taperUntil =
Basics.min
(toMM parameters.longPiece.buckleHole.distance
- (toMM parameters.longPiece.buckleHole.interval * toFloat (parameters.longPiece.buckleHole.adjustments + 1))
)
(length - 10)
taper =
case parameters.profile of
Straight ->
0.0
Tapered to ->
shoulderWidth - (toMM to - offset * 2)
in
g
[]
[ Svg.path
[ Path.d
([ Just (MoveTo Absolute ( x, y ))
, Just (LineTo Relative ( taper / 2, taperUntil ))
, Just (VerticalLineTo Relative (length - taperUntil))
, Just (HorizontalLineTo Relative (shoulderWidth - taper))
, Just (VerticalLineTo Relative -(length - taperUntil))
, Just (LineTo Relative ( taper / 2, -taperUntil ))
, Just ClosePath
]
|> List.filterMap identity
)
, fill "none"
, stroke "currentColor"
, strokeWidth (toMM parameters.rendering.lineWidth |> String.fromFloat)
, highlightStroke
(highlighting
== Just ShortPieceLength
|| highlighting
== Just ShoulderWidth
|| highlighting
== Just Profile
|| highlighting
== Just TaperTo
|| (case parameters.profile of
Straight ->
False
Tapered _ ->
highlighting
== Just BuckleHoleDistance
|| highlighting
== Just BuckleHoleInterval
)
)
]
[]
, Svg.path
(Path.d
[ MoveTo Absolute ( x + shoulderWidth / 2, y )
, VerticalLineTo Relative length
]
:: highlightStroke (highlighting == Just ShortPieceLength)
:: guideStroke
)
[]
, case parameters.profile of
Straight ->
g [] []
Tapered _ ->
Svg.path
(Path.d
[ MoveTo Absolute ( x + taper / 2, y + taperUntil )
, HorizontalLineTo Relative (shoulderWidth - taper)
]
:: guideStroke
)
[]
]
shortPiece : Parameters -> Maybe Key -> Item msg
shortPiece params highlighting =
let
shoulderWidth =
toMM params.shoulderWidth
length =
toMM params.shortPiece.length
in
{ size =
{ width = Exactly shoulderWidth, height = Exactly length }
, element =
\p _ ->
shortPieceShape
{ x = p.x
, y = p.y
, offset = 0
, parameters = params
, highlighting = highlighting
}
}
loops : Parameters -> Maybe Key -> Maybe (Item msg)
loops params highlighting =
let
root =
params.shortPiece.loops
in
case ( root.fixed, root.free ) of
( Nothing, Nothing ) ->
Nothing
_ ->
rows
|> aligned Container.Center
|> gapped (toMM params.rendering.gap / 2)
|> Container.build
[ Item
{ width = Exactly 40, height = Exactly 5 }
(\p size ->
text_
[ x (String.fromFloat (p.x + size.width / 2))
, y (String.fromFloat p.y)
, fontSize "5"
, fontWeight "100"
, textAnchor "middle"
, dominantBaseline "hanging"
, fill "currentColor"
]
[ text "Loops" ]
)
, columns
|> gapped (toMM params.rendering.gap)
|> aligned Container.Center
|> Container.build
([ fixedLoop params highlighting
, freeLoop params highlighting
]
|> List.filterMap identity
)
]
|> Just
fixedLoop : Parameters -> Maybe Key -> Maybe (Item msg)
fixedLoop params highlighting =
Maybe.map
(\{ width, length } ->
{ size =
{ width = Exactly (toMM length)
, height =
Exactly
(case params.shortPiece.loops.style of
Simple ->
toMM width
Folded ->
toMM width * 2
)
}
, element =
\p size ->
g
[]
[ Svg.path
[ Path.d
[ MoveTo Absolute ( p.x, p.y )
, HorizontalLineTo Relative size.width
, VerticalLineTo Relative size.height
, HorizontalLineTo Relative -size.width
, ClosePath
]
, fill "none"
, stroke "currentColor"
, strokeWidth (toMM params.rendering.lineWidth |> String.fromFloat)
, highlightStroke (highlighting == Just FixedLoopWidth || highlighting == Just FixedLoopLength)
]
[]
, case params.shortPiece.loops.style of
Simple ->
g [] []
Folded ->
g
[ fill "none"
, stroke "currentColor"
, strokeWidth (toMM params.rendering.lineWidth |> String.fromFloat)
, strokeDasharray "1 0.5"
]
[ Svg.path
[ Path.d
[ MoveTo Absolute ( p.x, p.y + toMM width / 2 )
, HorizontalLineTo Relative size.width
]
]
[]
, Svg.path
[ Path.d
[ MoveTo Absolute ( p.x, p.y + toMM width * 1.5 )
, HorizontalLineTo Relative size.width
]
]
[]
]
]
}
)
params.shortPiece.loops.fixed
freeLoop : Parameters -> Maybe Key -> Maybe (Item msg)
freeLoop params highlighting =
Maybe.map
(\{ width, length, overlap } ->
{ size =
{ width = Exactly (toMM length + toMM overlap)
, height =
Exactly
(case params.shortPiece.loops.style of
Simple ->
toMM width
Folded ->
toMM width * 2
)
}
, element =
\p size ->
g
[]
[ Svg.path
[ Path.d
[ MoveTo Absolute ( p.x, p.y )
, HorizontalLineTo Relative size.width
, VerticalLineTo Relative size.height
, HorizontalLineTo Relative -size.width
, ClosePath
]
, fill "none"
, stroke "currentColor"
, strokeWidth (toMM params.rendering.lineWidth |> String.fromFloat)
, highlightStroke
(highlighting
== Just FreeLoopWidth
|| highlighting
== Just FreeLoopLength
|| highlighting
== Just FreeLoopOverlap
)
]
[]
, g
[ fill "none"
, stroke "currentColor"
, strokeWidth (toMM params.rendering.lineWidth |> String.fromFloat)
, strokeDasharray "1 0.5"
]
[ Svg.path
[ Path.d
[ MoveTo Absolute ( p.x + toMM overlap, p.y )
, VerticalLineTo Relative size.height
]
]
[]
, Svg.path
[ Path.d
[ MoveTo Absolute ( p.x + size.width - toMM overlap, p.y )
, VerticalLineTo Relative size.height
]
]
[]
]
, case params.shortPiece.loops.style of
Simple ->
g [] []
Folded ->
g
[ fill "none"
, stroke "currentColor"
, strokeWidth (toMM params.rendering.lineWidth |> String.fromFloat)
, strokeDasharray "1 0.5"
]
[ Svg.path
[ Path.d
[ MoveTo Absolute ( p.x, p.y + toMM width / 2 )
, HorizontalLineTo Relative size.width
]
]
[]
, Svg.path
[ Path.d
[ MoveTo Absolute ( p.x, p.y + toMM width * 1.5 )
, HorizontalLineTo Relative size.width
]
]
[]
]
]
}
)
params.shortPiece.loops.free
paddings : Parameters -> Maybe Key -> Item msg
paddings params highlighting =
let
offset =
toMM params.paddingOffset
narrowestWidth =
case params.profile of
Straight ->
toMM params.shoulderWidth
Tapered to ->
toMM to
in
if offset == 0 || (offset * 2) >= narrowestWidth then
Item { width = Exactly 0, height = Exactly 0 } (\_ _ -> g [] [])
else
rows
|> aligned Container.Center
|> gapped (toMM params.rendering.gap / 2)
|> Container.build
[ Item
{ width = Exactly 21, height = Exactly 5 }
(\p size ->
text_
[ x (String.fromFloat (p.x + size.width / 2))
, y (String.fromFloat p.y)
, fontSize (String.fromFloat size.height)
, fontWeight "100"
, textAnchor "middle"
, dominantBaseline "hanging"
, fill "currentColor"
]
[ text "Paddings" ]
)
, columns
|> gapped (toMM params.rendering.gap)
|> Container.build
[ Item
{ width = Exactly (toMM params.shoulderWidth - offset * 2)
, height = Exactly (toMM params.longPiece.length - offset)
}
(\p _ ->
longPieceShape
{ x = p.x
, y = p.y
, offset = offset
, parameters = params
, highlighting = highlighting
}
)
, Item
{ width = Exactly (toMM params.shoulderWidth - offset * 2)
, height = Exactly (toMM params.shortPiece.length - offset)
}
(\p _ ->
shortPieceShape
{ x = p.x
, y = p.y
, offset = offset
, parameters = params
, highlighting = highlighting
}
)
]
]