Changes
8 changed files (+398/-229)
-
-
@@ -32,6 +32,7 @@ fn buildEditor(b: *std.Build, params: BuildEditorParameters) Elm {compile.addElmSourceFile(b.path("src/Template/Layout/Container.elm")); compile.addElmSourceFile(b.path("src/Template/Layout/Coordinate.elm")); compile.addElmSourceFile(b.path("src/Template/Layout/Item.elm")); compile.addElmSourceFile(b.path("src/Template/Layout/Packer.elm")); compile.addElmSourceFile(b.path("src/Svg/Path.elm")); compile.addElmSourceFile(b.path("src/Url/SearchParams.elm"));
-
-
-
@@ -214,12 +214,12 @@ view model ="print" ) ] [ template (template model.url parameters.parameters highlighting [] ] ) , node "x-toolbar" [ slot "toolbar" ] [ a
-
-
-
@@ -19,11 +19,11 @@ import Svg.Attributes exposing (..)import Svg.Path exposing (LargeArcFlag(..), PathCommand(..), PointMode(..), SweepFlag(..)) import Template.Layout as Layout import Template.Layout.Container as Container import Template.Layout.Coordinate exposing (Request(..)) import Template.Layout.Coordinate exposing (Request(..), SizeRequest, minSizeOf) import Url exposing (Url) template : Url -> Parameters -> Maybe Key -> List (Svg.Attribute msg) -> Svg.Svg msg template : Url -> Parameters -> Maybe Key -> List (Svg.Attribute msg) -> List (Svg.Svg msg) template url params highlighting attrs = let ( canvasWidth, canvasHeight ) =
-
@@ -32,20 +32,42 @@ template url params highlighting attrs =margin = toMM params.rendering.margin gap = toMM params.rendering.gap info = infoArea url params cutsArea : SizeRequest cutsArea = let infoSize = minSizeOf info.size in { width = AtLeast (canvasWidth - margin * 2 - gap) , height = AtLeast (canvasHeight - margin * 2 - gap * 2 - infoSize.height) } in svg (viewBox (String.join " " [ "0", "0", String.fromInt (ceiling canvasWidth), String.fromInt (ceiling canvasHeight) ]) :: class "print" :: attrs ) [ Container.rows |> Container.sized (AtLeast 0) (AtLeast 0) |> Container.gapped 10 |> Container.aligned Container.Center |> Container.padded margin |> Container.build [ cuts params highlighting , infoArea url params ] |> Layout.render { width = canvasWidth, height = canvasHeight } ] cuts cutsArea params highlighting |> List.map (\cut -> svg (viewBox (String.join " " [ "0", "0", String.fromInt (ceiling canvasWidth), String.fromInt (ceiling canvasHeight) ]) :: class "print" :: attrs ) [ Container.rows |> Container.sized (AtLeast 0) (AtLeast 0) |> Container.gapped gap |> Container.aligned Container.Center |> Container.padded margin |> Container.build [ Container.columns |> Container.padded (gap / 2) |> Container.build [ cut ] , info ] |> Layout.render { width = canvasWidth, height = canvasHeight } ] )
-
-
-
@@ -15,9 +15,10 @@ import Models.PWS01.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.Container as Container exposing (columns, rows) import Template.Layout.Coordinate exposing (Request(..), SizeRequest, minSizeOf) import Template.Layout.Item exposing (Item) import Template.Layout.Packer exposing (pack) highlightStroke : Bool -> Svg.Attribute msg
-
@@ -29,108 +30,139 @@ highlightStroke enabled =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) |> Container.build [ surfacePieces params highlighting |> noGrow , liningPieces params highlighting |> noGrow , paddings params highlighting |> noGrow ] |> Just , loops params highlighting ] |> List.filterMap identity |> List.map noGrow ) cuts : SizeRequest -> Parameters -> Maybe Key -> List (Item msg) cuts container params highlighting = let empty = { size = { width = Exactly 0, height = Exactly 0 }, element = \_ _ -> g [] [] } in pack container (toMM params.rendering.gap) [ if toMM params.caseSideFlap > 0 then tailPiece { offset = 0 , highlighting = highlighting , parameters = params , renderFlap = True , renderHoles = True } |> labelled "Tail Top (A1)" else empty , if toMM params.caseSideFlap > 0 || toMM params.shortPiece.buckleSideFlap > 0 then headPiece { offset = 0 , parameters = params , highlighting = highlighting , renderFlaps = True } |> labelled "Head Top (A2)" else empty -- Linings , tailPiece { offset = 0 , highlighting = highlighting , parameters = params , renderFlap = False , renderHoles = True } |> labelled "Tail Lining (B1)" , headPiece { offset = 0 , parameters = params , highlighting = highlighting , renderFlaps = False } |> labelled "Head Lining (B2)" -- Paddings , if toMM params.paddingOffset > 0 then tailPiece { offset = toMM params.paddingOffset , parameters = params , highlighting = highlighting , renderFlap = False , renderHoles = False } |> labelled "Tail Padding (C1)" else empty , if toMM params.paddingOffset > 0 then headPiece { offset = toMM params.paddingOffset , parameters = params , highlighting = highlighting , renderFlaps = False } |> labelled "Head Padding (C2)" 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 "Top pieces" ] ) , columns |> gapped (toMM params.rendering.gap) |> Container.build [ tailPiece { offset = 0 , highlighting = highlighting , parameters = params , renderFlap = True , renderHoles = True } , headPiece { offset = 0 , parameters = params , highlighting = highlighting , renderFlaps = True } ] ] else empty , fixedLoop params highlighting |> Maybe.map (labelled "Fixed Loop (D3)") |> Maybe.withDefault empty , freeLoop params highlighting |> Maybe.map (labelled "Free Loop (D4)") |> Maybe.withDefault empty ] liningPieces : Parameters -> Maybe Key -> Item msg liningPieces params highlighting = rows |> aligned Container.Center |> gapped (toMM params.rendering.gap / 2) |> Container.build [ Item { width = Exactly 40, height = Exactly 5 } labelled : String -> Item msg -> Item msg labelled text item = let itemSize = minSizeOf item.size rotated = itemSize.height > itemSize.width label = let width = Exactly (Basics.max itemSize.width itemSize.height) height = Exactly 3 in Item (if rotated then { width = height, height = width } else { width = width, height = height } ) (\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 "Linings" ] Svg.text_ (x (String.fromFloat p.x) :: y (String.fromFloat p.y) :: fontWeight "100" :: textAnchor "left" :: dominantBaseline "hanging" :: fill "currentColor" :: (if rotated then [ fontSize (String.fromFloat size.width) , transform ([ "rotate(90 " ++ String.fromFloat p.x ++ " " ++ String.fromFloat (p.y + size.width) ++ ")" , "translate(" ++ String.fromFloat -size.width ++ " 0)" ] |> String.join " " ) ] else [ fontSize (String.fromFloat size.height) ] ) ) [ Svg.text text ] ) , columns |> gapped (toMM params.rendering.gap) |> Container.build [ tailPiece { offset = 0 , highlighting = highlighting , parameters = params , renderFlap = False , renderHoles = True } , headPiece { offset = 0 , parameters = params , highlighting = highlighting , renderFlaps = False } ] ] in if rotated then columns |> Container.gapped 2 |> Container.build [ item, label ] else rows |> Container.gapped 2 |> Container.build [ label, item ] guideStroke : List (Svg.Attribute msg)
-
@@ -673,48 +705,6 @@ headPiece props =} 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
-
@@ -873,59 +863,3 @@ freeLoop params highlighting =} ) 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 [ tailPiece { offset = offset , parameters = params , highlighting = highlighting , renderFlap = False , renderHoles = False } , headPiece { offset = offset , parameters = params , highlighting = highlighting , renderFlaps = False } ] ]
-
-
-
@@ -18,11 +18,28 @@ type alias Size ={ width : Float, height : Float } {-| Returns `True` if `a` contains `b`. -} contains : Size -> Size -> Bool contains a b = a.width >= b.width && a.height >= b.height type Request a = Exactly a | AtLeast a mapRequest : (a -> b) -> Request a -> Request b mapRequest f r = case r of Exactly a -> Exactly (f a) AtLeast a -> AtLeast (f a) type alias SizeRequest = { width : Request Float, height : Request Float }
-
@@ -44,3 +61,16 @@ minSizeOf s =AtLeast h -> h } {-| Clip `a` with `b`. -} clip : SizeRequest -> SizeRequest -> SizeRequest clip a b = let size = minSizeOf b in { width = mapRequest (min size.width) a.width , height = mapRequest (min size.height) a.height }
-
-
-
@@ -0,0 +1,164 @@-- 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.Layout.Packer exposing (pack) import List import Svg exposing (..) import Svg.Attributes as Attrs import Template.Layout.Coordinate exposing (..) import Template.Layout.Item exposing (Item) {-| Packs multiple items into specified sized containers. This function packs given `items` using stupid bin packing algorithm so they won't overlap or overflow. As we're dealing with printable leather template, we can afford multiple containers to be packed. Items larger than the container will be placed as-is, and won't change container's size. Caller is responsible for pre-splitting items prior to passing them to this function if a piece is expected to be larger than the container. This function treats each item's `AtLeast` size request as `Exactly`. -} pack : SizeRequest -> Float -> List (Item msg) -> List (Item msg) pack container gap items = case items of [] -> [] _ -> items -- Clip items larger than the container, otherwise `packPages` goes into -- infinite recursion. |> List.map (\item -> { item | size = clip item.size container }) |> List.sortBy (\item -> let { width, height } = minSizeOf item.size in -(max width height) ) |> packPages container gap packPages : SizeRequest -> Float -> List (Item msg) -> List (Item msg) packPages size gap items = case items of [] -> [] _ -> let ( children, rest ) = tryPackInto { x = 0, y = 0 } (minSizeOf size) gap items in { size = size , element = \p s -> children |> List.map (\item -> item.element p s) |> g [] } :: packPages size gap rest {-| Tries to pack items into a rectangular, and returns packed items and items not fit into the rectangular. -} tryPackInto : Point2D -> Size -> Float -> List (Item msg) -> ( List (Item msg), List (Item msg) ) tryPackInto p size gap items = case items of [] -> ( [], [] ) item :: xs -> let itemSize = minSizeOf item.size in if contains size itemSize then let ( topItems, rest ) = tryPackInto { x = p.x + itemSize.width + gap, y = p.y } { width = size.width - itemSize.width - gap, height = itemSize.height } gap xs |> Tuple.mapFirst ((::) { size = item.size , element = \q _ -> item.element { x = p.x + q.x, y = p.y + q.y } itemSize } ) in tryPackInto { x = p.x, y = p.y + itemSize.height + gap } { width = size.width, height = size.height - itemSize.height - gap } gap rest |> Tuple.mapFirst ((++) topItems) else if contains { width = size.height, height = size.width } itemSize then let ( topItems, rest ) = tryPackInto { x = p.x + itemSize.height + gap, y = p.y } { width = size.width - itemSize.height - gap, height = itemSize.width } gap xs |> Tuple.mapFirst ((::) { size = item.size , element = \q _ -> let ox = p.x + q.x oy = p.y + q.y in g [ Attrs.transform ((if itemSize.height > itemSize.width then [ "rotate(-90 " ++ String.fromFloat ox ++ " " ++ String.fromFloat oy ++ ")" , "translate(" ++ String.fromFloat -itemSize.width ++ " 0)" ] else [ "rotate(90 " ++ String.fromFloat ox ++ " " ++ String.fromFloat (oy + itemSize.height) ++ ")" , "translate(" ++ String.fromFloat -itemSize.height ++ " 0)" ] ) |> String.join " " ) ] [ item.element { x = ox, y = oy } itemSize ] } ) in tryPackInto { x = p.x, y = p.y + itemSize.width + gap } { width = size.width, height = size.height - itemSize.width - gap } gap rest |> Tuple.mapFirst ((++) topItems) else tryPackInto p size gap xs |> Tuple.mapSecond ((::) item)
-
-
-
@@ -10,10 +10,20 @@:host { position: relative; } .items { box-sizing: border-box; margin: auto; height: 100%; padding: min(5dvh, 5vw); aspect-ratio: 210 / 297; display: flex; justify-content: center; justify-content: start; align-items: center; padding: min(5dvh, 5vw); gap: min(3dvh, 5vw); overflow: visible; } ::slotted(svg) {
-
@@ -28,6 +38,9 @@--_preview-fg: var(--_print-fg); font-family: "Barlow", sans-serif; flex-basis: 100%; flex-shrink: 0; flex-grow: 0; border: 1px solid #0002; max-width: 100%; max-height: 100%;
-
@@ -66,7 +79,7 @@} @media print { :host { :host, .items { display: contents; }
-
@@ -75,18 +88,21 @@} ::slotted(svg) { position: fixed; inset: 0; display: block; width: 210mm; height: 297mm; margin: 0; border: none; padding: 0; border: 0; outline: 0; background-color: var(--_print-bg); border-radius: 0; box-shadow: none; color: var(--_print-fg); } transform: none !important; ::slotted(svg:not(:first-child)) { break-before: page; } }
-
-
-
@@ -49,7 +49,7 @@ function scaling(distance, scale) {} export class XPreview extends HTMLElement { #slot = document.createElement("slot"); #items = document.createElement("div"); #gesturePlane = document.createElement("div"); #size = null;
-
@@ -79,7 +79,11 @@ export class XPreview extends HTMLElement {style.textContent = css; shadow.appendChild(style); shadow.appendChild(this.#slot); this.#items.classList.add("items"); shadow.appendChild(this.#items); const slot = document.createElement("slot"); this.#items.appendChild(slot); this.#gesturePlane.classList.add("gesture-plane"); shadow.appendChild(this.#gesturePlane);
-
@@ -215,9 +219,7 @@ export class XPreview extends HTMLElement {return; } for (const element of this.#slot.assignedElements()) { element.style.transform = `scale(${this.#scale}) translate(${this.#dx}px, ${this.#dy}px)`; } this.#items.style.transform = `scale(${this.#scale}) translate(${this.#dx}px, ${this.#dy}px)`; this.#isRenderScheduled = false; });
-