hgg-core-0.2.0.0: src/Graphics/Hgg/Primitive.hs
-- |
-- Module : Graphics.Hgg.Primitive
-- Description : Backend-agnostic drawing primitives, geometry, and style leaf types
-- Copyright : (c) 2026 Aelysce Project (Toshiaki Honda)
-- License : BSD-3-Clause
--
-- [日本語]: 描画 primitive (Point/Rect/style/PathSegment/Transform/Primitive) を
-- Spec/Layout/Render に依存しない __leaf module__ へ集約。 これらは元々
-- 'Graphics.Hgg.Render.Common' (Spec/Layout を import する上位) に置かれていたため、
-- 「'Spec.Layer' が draw closure (@RenderCtx -> [Primitive]@) を保持する」 拡張 (custom
-- mark) が __module 循環__で不能だった。 primitive は概念的に幾何 + Text のみに依存する
-- 基盤型ゆえ、 正しい層 (= 最下層 leaf) へ戻す。 挙動・出力は完全に不変 (純粋な型移動)。
-- 'Graphics.Hgg.Render.Common' / 'Graphics.Hgg.Render' が本 module を re-export するので
-- 既存の import 経路は不変。
-- [English]: Consolidates the drawing primitives (Point/Rect/style/PathSegment/
-- Transform/Primitive) into a __leaf module__ with no dependency on Spec/Layout/
-- Render. These originally lived in 'Graphics.Hgg.Render.Common' (an upper layer
-- that imports Spec/Layout), which made it impossible to extend 'Spec.Layer' to
-- hold a draw closure (@RenderCtx -> [Primitive]@) for custom marks, due to a
-- __module cycle__. Since primitives conceptually depend only on geometry and
-- Text, they belong in the correct (lowest, leaf) layer. Behaviour and output
-- are completely unchanged (a pure type relocation). 'Graphics.Hgg.Render.Common'
-- and 'Graphics.Hgg.Render' re-export this module, so existing import paths are
-- unaffected.
{-# LANGUAGE DeriveGeneric #-}
{-# LANGUAGE OverloadedStrings #-}
module Graphics.Hgg.Primitive
( -- * 幾何
Point(..)
, Rect(..)
-- * スタイル
, LineStyle(..)
, solid
, FillStyle(..)
, StrokeStyle(..)
, TextStyle(..)
, TextAnchor(..)
, Transform(..)
, PathSegment(..)
-- * Primitive
, Primitive(..)
-- * pt→device scale (backend の唯一の dpi 適用点)
, scalePrimitives
) where
import Data.Aeson (FromJSON, ToJSON)
import Data.Text (Text)
import GHC.Generics (Generic)
-- ===========================================================================
-- 幾何
-- ===========================================================================
data Point = Point !Double !Double deriving (Show, Eq)
-- | [日本語]: plot 領域や clip 矩形。 (x,y) 左上 + 幅高 (pt 空間)。
-- [English]: A plot area or clip rectangle: (x,y) top-left plus width and
-- height, in pt space.
data Rect = Rect { rX :: !Double, rY :: !Double, rW :: !Double, rH :: !Double }
deriving (Show, Eq, Generic)
instance ToJSON Rect
instance FromJSON Rect
-- ===========================================================================
-- スタイル
-- ===========================================================================
-- | [日本語]: 線スタイル。 'lsDash' = SVG stroke-dasharray / Canvas setLineDash 用 px 配列。
-- 既定 (= 実線) は空配列 []。 'solid' ヘルパで作ると常に実線。
-- [English]: A line style. 'lsDash' is the px array used by SVG
-- stroke-dasharray / Canvas setLineDash. The default (solid) is the empty
-- array []. Building it via the 'solid' helper always yields a solid line.
data LineStyle = LineStyle { lsColor :: !Text, lsWidth :: !Double, lsDash :: ![Double] } deriving (Show, Eq)
-- | [日本語]: 実線 'LineStyle' の簡易構築 (= 旧 2 引数 LineStyle と同一)。
-- dash を持たない既存呼出は全てこれに置換 (出力完全不変)。
-- [English]: A convenience constructor for a solid 'LineStyle' (equivalent
-- to the old 2-argument LineStyle). All existing call sites without a dash
-- are replaced by this (output is completely unchanged).
solid :: Text -> Double -> LineStyle
solid c w = LineStyle c w []
data FillStyle = FillStyle { fsColor :: !Text, fsOpacity :: !Double } deriving (Show, Eq)
data StrokeStyle = StrokeStyle { ssColor :: !Text, ssWidth :: !Double } deriving (Show, Eq)
data TextStyle = TextStyle
{ tsColor :: !Text
, tsSize :: !Double
, tsFamily :: !Text
, tsAnchor :: !TextAnchor
, tsRotate :: !Double -- degrees **CCW** (canonical・R/ggplot 準拠)、 0 = 水平。
-- device (SVG/canvas/rasterific=CW) への符号変換は各 backend emit で 1 回 (PDF=y-up ゆえ恒等)。
, tsWeight :: !Text -- ★ TODO-10 (2026-05-29): "normal" / "bold" 等
, tsItalic :: !Bool -- ★ TODO-10: italic on/off
} deriving (Show, Eq)
data TextAnchor = AnchorStart | AnchorMiddle | AnchorEnd
deriving (Show, Eq)
data Transform = TranslateT !Double !Double | ScaleT !Double !Double
deriving (Show, Eq)
data PathSegment
= MoveTo !Point
| LineTo !Point
| CurveTo !Point !Point !Point
| ClosePath
deriving (Show, Eq)
-- ===========================================================================
-- Primitive
-- ===========================================================================
-- | [日本語]: backend 非依存の描画 primitive。 各 backend は drawPrimitives で
-- これを順に解釈するだけ。
-- [English]: A backend-agnostic drawing primitive. Each backend simply
-- interprets these in sequence via drawPrimitives.
data Primitive
= PLine !Point !Point !LineStyle
| PRect !Rect !FillStyle (Maybe StrokeStyle)
-- | [日本語]: 'PCircle' は最終フィールドに optional hover label。 SVG backend は
-- <title> 要素として埋め込み、 ブラウザ native の hover tooltip に。
-- JS 不要。
-- [English]: 'PCircle' carries an optional hover label as its final
-- field. The SVG backend embeds it as a @\<title\>@ element, giving a
-- browser-native hover tooltip with no JS required.
| PCircle !Point !Double !FillStyle (Maybe StrokeStyle) (Maybe Text)
| PPath ![PathSegment] !FillStyle (Maybe StrokeStyle)
| PText !Point !Text !TextStyle
| PClipPush !Rect
-- | [日本語]: 多角形 clip (Phase 64 §2)。 頂点列は 'PRect' と同じ左上原点 y-down
-- 空間で、 **最後の頂点から最初の頂点へ暗黙に閉じる** (明示 close 不要)。
-- 矩形 clip は高速経路として 'PClipPush' を使い続ける (本 primitive は
-- polar の外周・ternary の三角形など矩形で表せない panel 用)。
-- ★ 頂点が 3 点未満の退化列は **clip 無し (素通し)** として扱う — 全 backend で
-- 統一。 「全消し」 にすると図が黙って白紙になるので fail-open を採る。
-- [English]: Polygon clip (Phase 64 §2). The vertex list lives in the
-- same top-left-origin, y-down space as 'PRect', and is **implicitly
-- closed** from the last vertex back to the first. Rectangular clips
-- keep using 'PClipPush' as the fast path; this primitive is for panels
-- that a rectangle cannot express (a polar boundary, a ternary
-- triangle, ...). A degenerate list of fewer than 3 vertices is treated
-- as __no clip at all__ (pass-through) in every backend: failing open
-- avoids silently blanking a figure.
| PClipPath ![Point]
| PClipPop
| PTransformPush !Transform
| PTransformPop
deriving (Show, Eq)
-- | [日本語]: pt 空間の primitive を device 単位へ一括 scale (k = dpi/72)。
-- ★ raster/vector backend で __唯一の dpi 適用点__。Layout/Render は
-- 純 pt を出力し、ここで一度だけ k を掛ける。PDF は k=1 (pt 直結・恒等) を渡す。
-- 座標・サイズ・線幅・font size・dash 配列を全て k 倍する。'ScaleT' は比率ゆえ不変。
-- [English]: Bulk-scales primitives from pt space to device units
-- (k = dpi/72). This is the __sole point where dpi is applied__ across
-- the raster/vector backends: Layout/Render emit pure pt values, and k is
-- applied exactly once here. PDF passes k=1 (a pt-direct identity).
-- Coordinates, sizes, line widths, font sizes, and dash arrays are all
-- scaled by k; 'ScaleT' is unaffected since it is a ratio.
scalePrimitives :: Double -> [Primitive] -> [Primitive]
scalePrimitives k
| k == 1 = id
| otherwise = map go
where
sp (Point x y) = Point (x * k) (y * k)
sr (Rect x y w h) = Rect (x * k) (y * k) (w * k) (h * k)
sl (LineStyle c w d) = LineStyle c (w * k) (map (* k) d)
sst (StrokeStyle c w) = StrokeStyle c (w * k)
sts ts = ts { tsSize = tsSize ts * k }
sseg seg = case seg of
MoveTo p -> MoveTo (sp p)
LineTo p -> LineTo (sp p)
CurveTo a b c -> CurveTo (sp a) (sp b) (sp c)
ClosePath -> ClosePath
str (TranslateT dx dy) = TranslateT (dx * k) (dy * k)
str t@(ScaleT _ _) = t
go p = case p of
PLine a b ls -> PLine (sp a) (sp b) (sl ls)
PRect r fs mss -> PRect (sr r) fs (fmap sst mss)
PCircle c rad fs mss t -> PCircle (sp c) (rad * k) fs (fmap sst mss) t
PPath segs fs mss -> PPath (map sseg segs) fs (fmap sst mss)
PText pt txt ts -> PText (sp pt) txt (sts ts)
PClipPush r -> PClipPush (sr r)
PClipPath ps -> PClipPath (map sp ps)
PClipPop -> PClipPop
PTransformPush tr -> PTransformPush (str tr)
PTransformPop -> PTransformPop