hgg-core-0.2.0.0: src/Graphics/Hgg/Unit.hs
-- |
-- Module : Graphics.Hgg.Unit
-- Description : The length unit system — pt authoring with a dpi rendering boundary
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- [日本語]: hgg は SVG / Canvas / PNG / PDF の複数 backend を持つ。PDF は
-- point (1/72 inch) ネイティブなので、オーサリングは物理単位 (mm/cm/inch/pt) を
-- 主とし、px 出力境界で一度だけ @px = pt × dpi/72@ を掛ける。本 module は最下層の
-- 純 value 層で、Spec / Layout から参照される (Spec には依存しない = 循環回避)。
--
-- 単位は値と一体 ('Length')。混在は許さず、各値が自分の単位を持つ。px は dpi
-- 依存なので 'toPt' では変換できず ('Nothing')、dpi を受け取る 'lengthToPt' で
-- 解決する。
-- [English]: hgg has multiple backends — SVG / Canvas / PNG / PDF.
-- Since PDF is native in points (1/72 inch), authoring primarily uses
-- physical units (mm/cm/inch/pt), and @px = pt × dpi/72@ is applied exactly
-- once at the px output boundary. This module is the lowest, pure-value
-- layer; it is referenced from Spec / Layout but does not depend on Spec
-- itself, to avoid a cycle.
--
-- A unit travels together with its value ('Length'); mixing is not
-- allowed, so each value carries its own unit. Since px is dpi-dependent,
-- 'toPt' cannot convert it ('Nothing'); resolving it requires 'lengthToPt',
-- which takes a dpi.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Graphics.Hgg.Unit
( LUnit(..)
, Length(..)
, mm, cm, inch, pt', px
, (*~)
, mmToPt
, toPt
, lengthToPt
-- * 座標: 相対単位込みの位置型 + resolver 別名
, Pos(..)
, resolveLen
) where
import Data.Aeson (FromJSON (..), ToJSON (..), Value (..), object,
pairs, withObject, (.:), (.=))
import Data.Text (Text)
import qualified Data.Text as T
import GHC.Generics (Generic)
-- === 型 ===
-- | [日本語]: 長さの単位。Mm/Cm/In/Pt は dpi 非依存の物理単位、Px は device 依存。
-- [English]: A length unit. Mm/Cm/In/Pt are dpi-independent physical
-- units; Px is device-dependent.
data LUnit = Mm | Cm | In | Pt | Px
deriving (Eq, Show, Generic)
-- | [日本語]: 値と単位を一体に保持する長さ。
-- [English]: A length that carries its value and unit together.
data Length = Length !Double !LUnit
deriving (Eq, Show, Generic)
-- | [日本語]: 軸に沿った「座標」(サイズ 'Length' ではない)。注釈・参照線・inset
-- の自由配置に使う。相対単位 (npc/native) の意味は panel rect / scale が
-- 決めるので、解決は @UCtx@ を受け取る Layout 側 resolver (@resolvePosX/Y@)
-- が担う (本 module には型と Codec だけ置き、Rect/Scale への依存を避ける =
-- 循環回避)。
-- [English]: A "coordinate" along an axis (not a size, unlike 'Length').
-- Used for freely positioning annotations, reference lines, and insets.
-- The meaning of the relative units (npc/native) is determined by the
-- panel rect / scale, so resolution is handled by the Layout-side
-- resolver (@resolvePosX/Y@), which takes a @UCtx@. This module holds only
-- the type and its Codec, avoiding a dependency on Rect/Scale (to prevent
-- a cycle).
data Pos
= PAbs !Length -- ^ [日本語]: 物理長オフセット (pt/mm/in/px)。panel 原点基準。
-- [English]: A physical-length offset (pt/mm/in/px), relative to the panel origin.
| PNpc !Double -- ^ [日本語]: panel 正規化座標 0..1 (0=左/下端, 1=右/上端)。
-- [English]: A panel-normalised coordinate in 0..1 (0 = left/bottom edge, 1 = right/top edge).
| PNative !Double -- ^ [日本語]: data 座標 (scale 経由で pt 化)。
-- [English]: A data coordinate (converted to pt via the scale).
deriving (Eq, Show, Generic)
-- === 構築 (単位量 + スカラ倍) ===
-- | [日本語]: 各単位の「1 単位」を表す単位量。@7 *~ inch@ のように使う。
-- [English]: A unit quantity representing "one unit" of each unit; used as
-- in @7 *~ inch@.
mm, cm, inch, pt', px :: Length
mm = Length 1 Mm
cm = Length 1 Cm
inch = Length 1 In
pt' = Length 1 Pt
px = Length 1 Px
-- | [日本語]: スカラ倍 (単位保存)。@k *~ (n 単位) = (k*n) 単位@。
-- [English]: Scalar multiplication (unit-preserving). @k *~ (n units) =
-- (k*n) units@.
infixl 7 *~
(*~) :: Double -> Length -> Length
k *~ Length n u = Length (k * n) u
-- | [日本語]: 数値リテラルを 'Length' として解釈するための 'Num' / 'Fractional'
-- instance。狙いは @width 624@ のような __bare 数値リテラル = pt__ を成立させ、
-- かつ @width (7 *~ inch)@ の単位付きも同じ引数型で受けること。
--
-- ★ なぜ型クラス (@ToLength@ 案) でなくこちら: @ToLength a => a -> _@ だと
-- @width 624@ が @(Num a, ToLength a) => a@ で曖昧化し、ToLength が標準クラスで
-- ないため Haskell2010 の defaulting が効かず__コンパイル不可__ (実測検証済)。
-- @Num Length@ なら @624 :: Length = fromInteger 624 = Length 624 Pt@ と確定し
-- 曖昧化しない (CSS length ライブラリ = clay/diagrams と同じ慣用)。
--
-- 算術 (@+@/@-@/@*@) は __同一単位の被演算子__を想定し、左辺の単位を保存して
-- 数値だけ合成する (主用途はリテラル overloading なので cross-unit 演算は
-- 非対象)。
-- [English]: The 'Num' / 'Fractional' instances that let a numeric literal
-- be interpreted as a 'Length'. The goal is to make __a bare numeric literal mean pt__,
-- as in @width 624@, while also accepting a unit
-- annotation such as @width (7 *~ inch)@ at the same argument type.
--
-- Why this rather than a type class (the @ToLength@ idea): with
-- @ToLength a => a -> _@, @width 624@ becomes ambiguous at
-- @(Num a, ToLength a) => a@, and since ToLength is not a standard class,
-- Haskell2010 defaulting does not kick in, so it __fails to compile__
-- (verified by measurement). With @Num Length@, @624 :: Length@ resolves
-- unambiguously to @fromInteger 624 = Length 624 Pt@ (the same idiom used
-- by CSS-length libraries such as clay/diagrams).
--
-- Arithmetic (@+@/@-@/@*@) assumes __operands of the same unit__: it keeps
-- the left operand's unit and only combines the numbers (the primary use
-- case is literal overloading, so cross-unit arithmetic is out of scope).
instance Num Length where
fromInteger n = Length (fromInteger n) Pt
Length a u + Length b _ = Length (a + b) u
Length a u - Length b _ = Length (a - b) u
Length a u * Length b _ = Length (a * b) u
abs (Length a u) = Length (abs a) u
signum (Length a u) = Length (signum a) u
negate (Length a u) = Length (negate a) u
instance Fractional Length where
fromRational r = Length (fromRational r) Pt
Length a u / Length b _ = Length (a / b) u
-- === pt への正規化 ===
-- | [日本語]: mm → pt 変換定数 (72pt / 25.4mm ≈ 2.8346)。ggplot の
-- @.pt=72.27/25.4@ / @.stroke=96/25.4@ の基準混在は採らず、全部 72pt/inch
-- に統一する。
-- [English]: The mm to pt conversion constant (72pt / 25.4mm ≈ 2.8346).
-- Rather than mixing bases like ggplot's @.pt=72.27/25.4@ /
-- @.stroke=96/25.4@, everything here is unified to 72pt/inch.
mmToPt :: Double
mmToPt = 72 / 25.4
-- | [日本語]: dpi 非依存単位を pt 化。'Px' は dpi が要るので 'Nothing' (型で表現)。
-- [English]: Converts dpi-independent units to pt. 'Px' requires a dpi, so
-- it yields 'Nothing' (expressed at the type level).
toPt :: Length -> Maybe Double
toPt (Length n u) = case u of
Pt -> Just n
In -> Just (n * 72)
Cm -> Just (n * 10 * mmToPt)
Mm -> Just (n * mmToPt)
Px -> Nothing
-- | [日本語]: dpi を受け取り全単位を pt 化。'Px' のみ @n * 72/dpi@。
-- computeLayout 入口で figure size を解決する本命関数。
-- [English]: Converts every unit to pt, given a dpi; only 'Px' uses
-- @n * 72/dpi@. This is the primary function that resolves figure size at
-- the computeLayout entry point.
lengthToPt :: Double -> Length -> Double
lengthToPt dpi (Length n u) = case u of
Pt -> n
In -> n * 72
Cm -> n * 10 * mmToPt
Mm -> n * mmToPt
Px -> n * 72 / dpi
-- | [日本語]: 'Length' を pt 化する resolver 別名 ('lengthToPt' と同一)。Pos
-- resolver (@resolvePosX/Y@) と対で「単位を pt へ解く」API を一様に呼ぶための
-- 名前。
-- [English]: A resolver alias for converting 'Length' to pt (identical to
-- 'lengthToPt'). Paired with the Pos resolver (@resolvePosX/Y@) to give a
-- uniform name for "resolve a unit to pt" APIs.
resolveLen :: Double -> Length -> Double
resolveLen = lengthToPt
-- === JSON Codec ===
-- @{ "v": Double, "u": String }@。key 順 v→u を toEncoding で固定し、PS argonaut と
-- byte 一致させる。tag は小文字 "mm"|"cm"|"in"|"pt"|"px"。
lunitTag :: LUnit -> Text
lunitTag u = case u of
Mm -> "mm"; Cm -> "cm"; In -> "in"; Pt -> "pt"; Px -> "px"
instance ToJSON Length where
toJSON (Length v u) = object ["v" .= v, "u" .= lunitTag u]
toEncoding (Length v u) = pairs ("v" .= v <> "u" .= lunitTag u)
instance FromJSON Length where
-- 後方互換 (Phase 33 移行): 旧来の px Int/Number 形式を px Length として読む。
parseJSON (Number n) = pure (Length (realToFrac n) Px)
parseJSON other = flip (withObject "Length") other $ \o -> do
v <- o .: "v"
uStr <- o .: "u"
u <- case (uStr :: Text) of
"mm" -> pure Mm
"cm" -> pure Cm
"in" -> pure In
"pt" -> pure Pt
"px" -> pure Px
_ -> fail ("Graphics.Hgg.Unit: unknown LUnit tag " <> T.unpack uStr)
pure (Length v u)
-- | [日本語]: 'Pos' の Codec。tag 付き @{ "t": "abs"|"npc"|"native", ... }@。
-- "abs" は @"l"@ に 'Length'、"npc"/"native" は @"p"@ に Double。key 順は
-- toEncoding (t→payload) で固定し PS argonaut と byte 一致させる。
-- [English]: The Codec for 'Pos': tagged @{ "t": "abs"|"npc"|"native", ... }@.
-- "abs" carries a 'Length' at @"l"@; "npc"/"native" carry a Double at
-- @"p"@. Key order is fixed by toEncoding (t then payload) to byte-match
-- PureScript's argonaut.
instance ToJSON Pos where
toJSON p = case p of
PAbs l -> object ["t" .= ("abs" :: Text), "l" .= l]
PNpc x -> object ["t" .= ("npc" :: Text), "p" .= x]
PNative x -> object ["t" .= ("native" :: Text), "p" .= x]
toEncoding p = case p of
PAbs l -> pairs ("t" .= ("abs" :: Text) <> "l" .= l)
PNpc x -> pairs ("t" .= ("npc" :: Text) <> "p" .= x)
PNative x -> pairs ("t" .= ("native" :: Text) <> "p" .= x)
instance FromJSON Pos where
parseJSON = withObject "Pos" $ \o -> do
t <- o .: "t"
case (t :: Text) of
"abs" -> PAbs <$> o .: "l"
"npc" -> PNpc <$> o .: "p"
"native" -> PNative <$> o .: "p"
_ -> fail ("Graphics.Hgg.Unit: unknown Pos tag " <> T.unpack t)