packages feed

hgg-3d (empty) → 0.1.0.0

raw patch · 21 files changed

+10869/−0 lines, 21 filesdep +aesondep +basedep +bytestring

Dependencies added: aeson, base, bytestring, containers, directory, filepath, hgg-3d, hgg-core, hgg-frame, hgg-pdf, hgg-rasterific, hgg-svg, hspec, process, text, vector

Files

+ CHANGELOG.md view
@@ -0,0 +1,7 @@+# Changelog for `hgg-3d`++## 0.1.0.0 — 2026-07-18++First public release on Hackage.++- 3D plotting (CPU projection): surface / scatter / bar / wireframe / quiver / trisurf; works with all 2D backends.
+ LICENSE view
@@ -0,0 +1,30 @@+BSD 3-Clause License++Copyright (c) 2026, Toshiaki Honda+All rights reserved.++Redistribution and use in source and binary forms, with or without+modification, are permitted provided that the following conditions are met:++1. Redistributions of source code must retain the above copyright notice,+   this list of conditions and the following disclaimer.++2. Redistributions in binary form must reproduce the above copyright notice,+   this list of conditions and the following disclaimer in the documentation+   and/or other materials provided with the distribution.++3. Neither the name of the copyright holder nor the names of its+   contributors may be used to endorse or promote products derived from this+   software without specific prior written permission.++THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE+IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE+ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE+LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF THE+POSSIBILITY OF SUCH DAMAGE.
+ data/webgl-spec.js view
@@ -0,0 +1,5994 @@+/* Generated by Spago v0.93.45 */+(() => {+  // output/Examples.WebGLPlot/foreign.js+  function canvasDimsFFI(canvas) {+    return function() {+      var rect = canvas.getBoundingClientRect();+      var w = canvas.width || rect.width;+      var h = canvas.height || rect.height;+      return { w, h };+    };+  }+  function truncateFFI(n) {+    return n | 0;+  }+  function _attachOrbitHandlers(canvas, handlers) {+    var dragging = null;+    var lastX = 0;+    var lastY = 0;+    canvas.addEventListener("contextmenu", function(e) {+      e.preventDefault();+    });+    canvas.addEventListener("mousedown", function(e) {+      e.preventDefault();+      dragging = e.button === 2 ? "pan" : "rotate";+      lastX = e.clientX;+      lastY = e.clientY;+    });+    window.addEventListener("mouseup", function() {+      dragging = null;+    });+    window.addEventListener("mousemove", function(e) {+      if (!dragging) return;+      var dx = e.clientX - lastX;+      var dy = e.clientY - lastY;+      lastX = e.clientX;+      lastY = e.clientY;+      if (dragging === "rotate") {+        handlers.onRotate(-dx * 0.01, dy * 0.01);+      } else {+        handlers.onPan(-dx * 5e-3, dy * 5e-3);+      }+    });+    canvas.addEventListener("wheel", function(e) {+      e.preventDefault();+      var factor = e.deltaY > 0 ? 1.1 : 1 / 1.1;+      handlers.onZoom(factor);+    }, { passive: false });+  }+  function exposeGlobal(renderFn) {+    return function() {+      if (typeof window !== "undefined") {+        window.hggPlot = window.hggPlot || {};+        window.hggPlot.renderFromSpec = function(canvasId, specJson) {+          renderFn(canvasId, specJson);+        };+      }+    };+  }++  // output/Data.Eq/foreign.js+  var refEq = function(r1) {+    return function(r2) {+      return r1 === r2;+    };+  };+  var eqIntImpl = refEq;+  var eqNumberImpl = refEq;+  var eqCharImpl = refEq;++  // output/Data.Eq/index.js+  var eqNumber = {+    eq: eqNumberImpl+  };+  var eqInt = {+    eq: eqIntImpl+  };+  var eqChar = {+    eq: eqCharImpl+  };+  var eq = function(dict) {+    return dict.eq;+  };++  // output/Data.Functor/foreign.js+  var arrayMap = function(f) {+    return function(arr) {+      var l = arr.length;+      var result = new Array(l);+      for (var i = 0; i < l; i++) {+        result[i] = f(arr[i]);+      }+      return result;+    };+  };++  // output/Control.Semigroupoid/index.js+  var semigroupoidFn = {+    compose: function(f) {+      return function(g) {+        return function(x) {+          return f(g(x));+        };+      };+    }+  };++  // output/Control.Category/index.js+  var identity = function(dict) {+    return dict.identity;+  };+  var categoryFn = {+    identity: function(x) {+      return x;+    },+    Semigroupoid0: function() {+      return semigroupoidFn;+    }+  };++  // output/Data.Boolean/index.js+  var otherwise = true;++  // output/Data.Function/index.js+  var flip = function(f) {+    return function(b) {+      return function(a) {+        return f(a)(b);+      };+    };+  };+  var $$const = function(a) {+    return function(v) {+      return a;+    };+  };+  var applyFlipped = function(x) {+    return function(f) {+      return f(x);+    };+  };++  // output/Data.Unit/foreign.js+  var unit = void 0;++  // output/Data.Functor/index.js+  var map = function(dict) {+    return dict.map;+  };+  var $$void = function(dictFunctor) {+    return map(dictFunctor)($$const(unit));+  };+  var functorArray = {+    map: arrayMap+  };++  // output/Data.Semigroup/foreign.js+  var concatArray = function(xs) {+    return function(ys) {+      if (xs.length === 0) return ys;+      if (ys.length === 0) return xs;+      return xs.concat(ys);+    };+  };++  // output/Data.Semigroup/index.js+  var semigroupArray = {+    append: concatArray+  };+  var append = function(dict) {+    return dict.append;+  };++  // output/Control.Apply/foreign.js+  var arrayApply = function(fs) {+    return function(xs) {+      var l = fs.length;+      var k = xs.length;+      var result = new Array(l * k);+      var n = 0;+      for (var i = 0; i < l; i++) {+        var f = fs[i];+        for (var j = 0; j < k; j++) {+          result[n++] = f(xs[j]);+        }+      }+      return result;+    };+  };++  // output/Control.Apply/index.js+  var identity2 = /* @__PURE__ */ identity(categoryFn);+  var applyArray = {+    apply: arrayApply,+    Functor0: function() {+      return functorArray;+    }+  };+  var apply = function(dict) {+    return dict.apply;+  };+  var applySecond = function(dictApply) {+    var apply1 = apply(dictApply);+    var map12 = map(dictApply.Functor0());+    return function(a) {+      return function(b) {+        return apply1(map12($$const(identity2))(a))(b);+      };+    };+  };++  // output/Control.Applicative/index.js+  var pure = function(dict) {+    return dict.pure;+  };+  var when = function(dictApplicative) {+    var pure1 = pure(dictApplicative);+    return function(v) {+      return function(v1) {+        if (v) {+          return v1;+        }+        ;+        if (!v) {+          return pure1(unit);+        }+        ;+        throw new Error("Failed pattern match at Control.Applicative (line 63, column 1 - line 63, column 63): " + [v.constructor.name, v1.constructor.name]);+      };+    };+  };+  var liftA1 = function(dictApplicative) {+    var apply2 = apply(dictApplicative.Apply0());+    var pure1 = pure(dictApplicative);+    return function(f) {+      return function(a) {+        return apply2(pure1(f))(a);+      };+    };+  };++  // output/Data.Bounded/foreign.js+  var topInt = 2147483647;+  var bottomInt = -2147483648;+  var topChar = String.fromCharCode(65535);+  var bottomChar = String.fromCharCode(0);+  var topNumber = Number.POSITIVE_INFINITY;+  var bottomNumber = Number.NEGATIVE_INFINITY;++  // output/Data.Ord/foreign.js+  var unsafeCompareImpl = function(lt) {+    return function(eq2) {+      return function(gt) {+        return function(x) {+          return function(y) {+            return x < y ? lt : x === y ? eq2 : gt;+          };+        };+      };+    };+  };+  var ordIntImpl = unsafeCompareImpl;+  var ordNumberImpl = unsafeCompareImpl;+  var ordCharImpl = unsafeCompareImpl;++  // output/Data.Ordering/index.js+  var LT = /* @__PURE__ */ (function() {+    function LT2() {+    }+    ;+    LT2.value = new LT2();+    return LT2;+  })();+  var GT = /* @__PURE__ */ (function() {+    function GT2() {+    }+    ;+    GT2.value = new GT2();+    return GT2;+  })();+  var EQ = /* @__PURE__ */ (function() {+    function EQ2() {+    }+    ;+    EQ2.value = new EQ2();+    return EQ2;+  })();+  var eqOrdering = {+    eq: function(v) {+      return function(v1) {+        if (v instanceof LT && v1 instanceof LT) {+          return true;+        }+        ;+        if (v instanceof GT && v1 instanceof GT) {+          return true;+        }+        ;+        if (v instanceof EQ && v1 instanceof EQ) {+          return true;+        }+        ;+        return false;+      };+    }+  };++  // output/Data.Ring/foreign.js+  var intSub = function(x) {+    return function(y) {+      return x - y | 0;+    };+  };++  // output/Data.Semiring/foreign.js+  var intAdd = function(x) {+    return function(y) {+      return x + y | 0;+    };+  };+  var intMul = function(x) {+    return function(y) {+      return x * y | 0;+    };+  };++  // output/Data.Semiring/index.js+  var semiringInt = {+    add: intAdd,+    zero: 0,+    mul: intMul,+    one: 1+  };++  // output/Data.Ring/index.js+  var ringInt = {+    sub: intSub,+    Semiring0: function() {+      return semiringInt;+    }+  };++  // output/Data.Ord/index.js+  var ordNumber = /* @__PURE__ */ (function() {+    return {+      compare: ordNumberImpl(LT.value)(EQ.value)(GT.value),+      Eq0: function() {+        return eqNumber;+      }+    };+  })();+  var ordInt = /* @__PURE__ */ (function() {+    return {+      compare: ordIntImpl(LT.value)(EQ.value)(GT.value),+      Eq0: function() {+        return eqInt;+      }+    };+  })();+  var ordChar = /* @__PURE__ */ (function() {+    return {+      compare: ordCharImpl(LT.value)(EQ.value)(GT.value),+      Eq0: function() {+        return eqChar;+      }+    };+  })();+  var compare = function(dict) {+    return dict.compare;+  };+  var max = function(dictOrd) {+    var compare3 = compare(dictOrd);+    return function(x) {+      return function(y) {+        var v = compare3(x)(y);+        if (v instanceof LT) {+          return y;+        }+        ;+        if (v instanceof EQ) {+          return x;+        }+        ;+        if (v instanceof GT) {+          return x;+        }+        ;+        throw new Error("Failed pattern match at Data.Ord (line 181, column 3 - line 184, column 12): " + [v.constructor.name]);+      };+    };+  };+  var min = function(dictOrd) {+    var compare3 = compare(dictOrd);+    return function(x) {+      return function(y) {+        var v = compare3(x)(y);+        if (v instanceof LT) {+          return x;+        }+        ;+        if (v instanceof EQ) {+          return x;+        }+        ;+        if (v instanceof GT) {+          return y;+        }+        ;+        throw new Error("Failed pattern match at Data.Ord (line 172, column 3 - line 175, column 12): " + [v.constructor.name]);+      };+    };+  };++  // output/Data.Bounded/index.js+  var top = function(dict) {+    return dict.top;+  };+  var boundedInt = {+    top: topInt,+    bottom: bottomInt,+    Ord0: function() {+      return ordInt;+    }+  };+  var boundedChar = {+    top: topChar,+    bottom: bottomChar,+    Ord0: function() {+      return ordChar;+    }+  };+  var bottom = function(dict) {+    return dict.bottom;+  };++  // output/Data.Show/foreign.js+  var showIntImpl = function(n) {+    return n.toString();+  };+  var showNumberImpl = function(n) {+    var str = n.toString();+    return isNaN(str + ".0") ? str : str + ".0";+  };+  var showStringImpl = function(s) {+    var l = s.length;+    return '"' + s.replace(+      /[\0-\x1F\x7F"\\]/g,+      // eslint-disable-line no-control-regex+      function(c, i) {+        switch (c) {+          case '"':+          case "\\":+            return "\\" + c;+          case "\x07":+            return "\\a";+          case "\b":+            return "\\b";+          case "\f":+            return "\\f";+          case "\n":+            return "\\n";+          case "\r":+            return "\\r";+          case "	":+            return "\\t";+          case "\v":+            return "\\v";+        }+        var k = i + 1;+        var empty4 = k < l && s[k] >= "0" && s[k] <= "9" ? "\\&" : "";+        return "\\" + c.charCodeAt(0).toString(10) + empty4;+      }+    ) + '"';+  };++  // output/Data.Show/index.js+  var showString = {+    show: showStringImpl+  };+  var showNumber = {+    show: showNumberImpl+  };+  var showInt = {+    show: showIntImpl+  };+  var show = function(dict) {+    return dict.show;+  };++  // output/Data.Maybe/index.js+  var identity3 = /* @__PURE__ */ identity(categoryFn);+  var Nothing = /* @__PURE__ */ (function() {+    function Nothing2() {+    }+    ;+    Nothing2.value = new Nothing2();+    return Nothing2;+  })();+  var Just = /* @__PURE__ */ (function() {+    function Just2(value0) {+      this.value0 = value0;+    }+    ;+    Just2.create = function(value0) {+      return new Just2(value0);+    };+    return Just2;+  })();+  var maybe = function(v) {+    return function(v1) {+      return function(v2) {+        if (v2 instanceof Nothing) {+          return v;+        }+        ;+        if (v2 instanceof Just) {+          return v1(v2.value0);+        }+        ;+        throw new Error("Failed pattern match at Data.Maybe (line 237, column 1 - line 237, column 51): " + [v.constructor.name, v1.constructor.name, v2.constructor.name]);+      };+    };+  };+  var isNothing = /* @__PURE__ */ maybe(true)(/* @__PURE__ */ $$const(false));+  var functorMaybe = {+    map: function(v) {+      return function(v1) {+        if (v1 instanceof Just) {+          return new Just(v(v1.value0));+        }+        ;+        return Nothing.value;+      };+    }+  };+  var map2 = /* @__PURE__ */ map(functorMaybe);+  var fromMaybe = function(a) {+    return maybe(a)(identity3);+  };+  var fromJust = function() {+    return function(v) {+      if (v instanceof Just) {+        return v.value0;+      }+      ;+      throw new Error("Failed pattern match at Data.Maybe (line 288, column 1 - line 288, column 46): " + [v.constructor.name]);+    };+  };+  var eqMaybe = function(dictEq) {+    var eq2 = eq(dictEq);+    return {+      eq: function(x) {+        return function(y) {+          if (x instanceof Nothing && y instanceof Nothing) {+            return true;+          }+          ;+          if (x instanceof Just && y instanceof Just) {+            return eq2(x.value0)(y.value0);+          }+          ;+          return false;+        };+      }+    };+  };+  var applyMaybe = {+    apply: function(v) {+      return function(v1) {+        if (v instanceof Just) {+          return map2(v.value0)(v1);+        }+        ;+        if (v instanceof Nothing) {+          return Nothing.value;+        }+        ;+        throw new Error("Failed pattern match at Data.Maybe (line 67, column 1 - line 69, column 30): " + [v.constructor.name, v1.constructor.name]);+      };+    },+    Functor0: function() {+      return functorMaybe;+    }+  };+  var bindMaybe = {+    bind: function(v) {+      return function(v1) {+        if (v instanceof Just) {+          return v1(v.value0);+        }+        ;+        if (v instanceof Nothing) {+          return Nothing.value;+        }+        ;+        throw new Error("Failed pattern match at Data.Maybe (line 125, column 1 - line 127, column 28): " + [v.constructor.name, v1.constructor.name]);+      };+    },+    Apply0: function() {+      return applyMaybe;+    }+  };++  // output/Data.Number/foreign.js+  var isFiniteImpl = isFinite;+  var ceil = Math.ceil;+  var cos = Math.cos;+  var floor = Math.floor;+  var log = Math.log;+  var pow = function(n) {+    return function(p) {+      return Math.pow(n, p);+    };+  };+  var round = Math.round;+  var sin = Math.sin;+  var sqrt = Math.sqrt;+  var tan = Math.tan;++  // output/Data.Number/index.js+  var pi = 3.141592653589793;++  // output/Hgg.Plot.ThreeD/index.js+  var Vec3 = /* @__PURE__ */ (function() {+    function Vec32(value0, value1, value2) {+      this.value0 = value0;+      this.value1 = value1;+      this.value2 = value2;+    }+    ;+    Vec32.create = function(value0) {+      return function(value1) {+        return function(value2) {+          return new Vec32(value0, value1, value2);+        };+      };+    };+    return Vec32;+  })();+  var Orthographic = /* @__PURE__ */ (function() {+    function Orthographic2(value0) {+      this.value0 = value0;+    }+    ;+    Orthographic2.create = function(value0) {+      return new Orthographic2(value0);+    };+    return Orthographic2;+  })();+  var Perspective = /* @__PURE__ */ (function() {+    function Perspective2(value0) {+      this.value0 = value0;+    }+    ;+    Perspective2.create = function(value0) {+      return new Perspective2(value0);+    };+    return Perspective2;+  })();+  var Point3 = /* @__PURE__ */ (function() {+    function Point32(value0, value1, value2) {+      this.value0 = value0;+      this.value1 = value1;+      this.value2 = value2;+    }+    ;+    Point32.create = function(value0) {+      return function(value1) {+        return function(value2) {+          return new Point32(value0, value1, value2);+        };+      };+    };+    return Point32;+  })();+  var Mat4 = /* @__PURE__ */ (function() {+    function Mat42(value0, value1, value2, value3, value4, value5, value6, value7, value8, value9, value10, value11, value12, value13, value14, value15) {+      this.value0 = value0;+      this.value1 = value1;+      this.value2 = value2;+      this.value3 = value3;+      this.value4 = value4;+      this.value5 = value5;+      this.value6 = value6;+      this.value7 = value7;+      this.value8 = value8;+      this.value9 = value9;+      this.value10 = value10;+      this.value11 = value11;+      this.value12 = value12;+      this.value13 = value13;+      this.value14 = value14;+      this.value15 = value15;+    }+    ;+    Mat42.create = function(value0) {+      return function(value1) {+        return function(value2) {+          return function(value3) {+            return function(value4) {+              return function(value5) {+                return function(value6) {+                  return function(value7) {+                    return function(value8) {+                      return function(value9) {+                        return function(value10) {+                          return function(value11) {+                            return function(value12) {+                              return function(value13) {+                                return function(value14) {+                                  return function(value15) {+                                    return new Mat42(value0, value1, value2, value3, value4, value5, value6, value7, value8, value9, value10, value11, value12, value13, value14, value15);+                                  };+                                };+                              };+                            };+                          };+                        };+                      };+                    };+                  };+                };+              };+            };+          };+        };+      };+    };+    return Mat42;+  })();+  var zUp = /* @__PURE__ */ (function() {+    return new Vec3(0, 0, 1);+  })();+  var subV3 = function(v) {+    return function(v1) {+      return new Vec3(v.value0 - v1.value0, v.value1 - v1.value1, v.value2 - v1.value2);+    };+  };+  var scaleV3 = function(s) {+    return function(v) {+      return new Vec3(s * v.value0, s * v.value1, s * v.value2);+    };+  };+  var dotV3 = function(v) {+    return function(v1) {+      return v.value0 * v1.value0 + v.value1 * v1.value1 + v.value2 * v1.value2;+    };+  };+  var lengthV3 = function(v) {+    return sqrt(dotV3(v)(v));+  };+  var normalizeV3 = function(v) {+    var l = lengthV3(v);+    var $222 = l < 1e-12;+    if ($222) {+      return new Vec3(0, 0, 0);+    }+    ;+    return scaleV3(1 / l)(v);+  };+  var crossV3 = function(v) {+    return function(v1) {+      return new Vec3(v.value1 * v1.value2 - v.value2 * v1.value1, v.value2 * v1.value0 - v.value0 * v1.value2, v.value0 * v1.value1 - v.value1 * v1.value0);+    };+  };++  // output/Data.Array/foreign.js+  var rangeImpl = function(start, end) {+    var step = start > end ? -1 : 1;+    var result = new Array(step * (end - start) + 1);+    var i = start, n = 0;+    while (i !== end) {+      result[n++] = i;+      i += step;+    }+    result[n] = i;+    return result;+  };+  var replicateFill = function(count, value) {+    if (count < 1) {+      return [];+    }+    var result = new Array(count);+    return result.fill(value);+  };+  var replicatePolyfill = function(count, value) {+    var result = [];+    var n = 0;+    for (var i = 0; i < count; i++) {+      result[n++] = value;+    }+    return result;+  };+  var replicateImpl = typeof Array.prototype.fill === "function" ? replicateFill : replicatePolyfill;+  var length = function(xs) {+    return xs.length;+  };+  var unconsImpl = function(empty4, next, xs) {+    return xs.length === 0 ? empty4({}) : next(xs[0])(xs.slice(1));+  };+  var indexImpl = function(just, nothing, xs, i) {+    return i < 0 || i >= xs.length ? nothing : just(xs[i]);+  };+  var filterImpl = function(f, xs) {+    return xs.filter(f);+  };+  var zipWithImpl = function(f, xs, ys) {+    var l = xs.length < ys.length ? xs.length : ys.length;+    var result = new Array(l);+    for (var i = 0; i < l; i++) {+      result[i] = f(xs[i])(ys[i]);+    }+    return result;+  };++  // output/Control.Bind/foreign.js+  var arrayBind = typeof Array.prototype.flatMap === "function" ? function(arr) {+    return function(f) {+      return arr.flatMap(f);+    };+  } : function(arr) {+    return function(f) {+      var result = [];+      var l = arr.length;+      for (var i = 0; i < l; i++) {+        var xs = f(arr[i]);+        var k = xs.length;+        for (var j = 0; j < k; j++) {+          result.push(xs[j]);+        }+      }+      return result;+    };+  };++  // output/Control.Bind/index.js+  var bindArray = {+    bind: arrayBind,+    Apply0: function() {+      return applyArray;+    }+  };+  var bind = function(dict) {+    return dict.bind;+  };+  var bindFlipped = function(dictBind) {+    return flip(bind(dictBind));+  };+  var composeKleisliFlipped = function(dictBind) {+    var bindFlipped1 = bindFlipped(dictBind);+    return function(f) {+      return function(g) {+        return function(a) {+          return bindFlipped1(f)(g(a));+        };+      };+    };+  };++  // output/Control.Monad/index.js+  var ap = function(dictMonad) {+    var bind4 = bind(dictMonad.Bind1());+    var pure5 = pure(dictMonad.Applicative0());+    return function(f) {+      return function(a) {+        return bind4(f)(function(f$prime) {+          return bind4(a)(function(a$prime) {+            return pure5(f$prime(a$prime));+          });+        });+      };+    };+  };++  // output/Data.Either/index.js+  var Left = /* @__PURE__ */ (function() {+    function Left2(value0) {+      this.value0 = value0;+    }+    ;+    Left2.create = function(value0) {+      return new Left2(value0);+    };+    return Left2;+  })();+  var Right = /* @__PURE__ */ (function() {+    function Right2(value0) {+      this.value0 = value0;+    }+    ;+    Right2.create = function(value0) {+      return new Right2(value0);+    };+    return Right2;+  })();+  var note = function(a) {+    return maybe(new Left(a))(Right.create);+  };+  var functorEither = {+    map: function(f) {+      return function(m) {+        if (m instanceof Left) {+          return new Left(m.value0);+        }+        ;+        if (m instanceof Right) {+          return new Right(f(m.value0));+        }+        ;+        throw new Error("Failed pattern match at Data.Either (line 0, column 0 - line 0, column 0): " + [m.constructor.name]);+      };+    }+  };+  var map3 = /* @__PURE__ */ map(functorEither);+  var either = function(v) {+    return function(v1) {+      return function(v2) {+        if (v2 instanceof Left) {+          return v(v2.value0);+        }+        ;+        if (v2 instanceof Right) {+          return v1(v2.value0);+        }+        ;+        throw new Error("Failed pattern match at Data.Either (line 208, column 1 - line 208, column 64): " + [v.constructor.name, v1.constructor.name, v2.constructor.name]);+      };+    };+  };+  var applyEither = {+    apply: function(v) {+      return function(v1) {+        if (v instanceof Left) {+          return new Left(v.value0);+        }+        ;+        if (v instanceof Right) {+          return map3(v.value0)(v1);+        }+        ;+        throw new Error("Failed pattern match at Data.Either (line 70, column 1 - line 72, column 30): " + [v.constructor.name, v1.constructor.name]);+      };+    },+    Functor0: function() {+      return functorEither;+    }+  };+  var bindEither = {+    bind: /* @__PURE__ */ either(function(e) {+      return function(v) {+        return new Left(e);+      };+    })(function(a) {+      return function(f) {+        return f(a);+      };+    }),+    Apply0: function() {+      return applyEither;+    }+  };+  var applicativeEither = /* @__PURE__ */ (function() {+    return {+      pure: Right.create,+      Apply0: function() {+        return applyEither;+      }+    };+  })();++  // output/Data.EuclideanRing/foreign.js+  var intDegree = function(x) {+    return Math.min(Math.abs(x), 2147483647);+  };+  var intDiv = function(x) {+    return function(y) {+      if (y === 0) return 0;+      return y > 0 ? Math.floor(x / y) : -Math.floor(x / -y);+    };+  };+  var intMod = function(x) {+    return function(y) {+      if (y === 0) return 0;+      var yy = Math.abs(y);+      return (x % yy + yy) % yy;+    };+  };++  // output/Data.CommutativeRing/index.js+  var commutativeRingInt = {+    Ring0: function() {+      return ringInt;+    }+  };++  // output/Data.EuclideanRing/index.js+  var euclideanRingInt = {+    degree: intDegree,+    div: intDiv,+    mod: intMod,+    CommutativeRing0: function() {+      return commutativeRingInt;+    }+  };+  var div = function(dict) {+    return dict.div;+  };++  // output/Data.Monoid/index.js+  var mempty = function(dict) {+    return dict.mempty;+  };++  // output/Effect/foreign.js+  var pureE = function(a) {+    return function() {+      return a;+    };+  };+  var bindE = function(a) {+    return function(f) {+      return function() {+        return f(a())();+      };+    };+  };++  // output/Effect/index.js+  var $runtime_lazy = function(name, moduleName, init3) {+    var state = 0;+    var val;+    return function(lineNumber) {+      if (state === 2) return val;+      if (state === 1) throw new ReferenceError(name + " was needed before it finished initializing (module " + moduleName + ", line " + lineNumber + ")", moduleName, lineNumber);+      state = 1;+      val = init3();+      state = 2;+      return val;+    };+  };+  var monadEffect = {+    Applicative0: function() {+      return applicativeEffect;+    },+    Bind1: function() {+      return bindEffect;+    }+  };+  var bindEffect = {+    bind: bindE,+    Apply0: function() {+      return $lazy_applyEffect(0);+    }+  };+  var applicativeEffect = {+    pure: pureE,+    Apply0: function() {+      return $lazy_applyEffect(0);+    }+  };+  var $lazy_functorEffect = /* @__PURE__ */ $runtime_lazy("functorEffect", "Effect", function() {+    return {+      map: liftA1(applicativeEffect)+    };+  });+  var $lazy_applyEffect = /* @__PURE__ */ $runtime_lazy("applyEffect", "Effect", function() {+    return {+      apply: ap(monadEffect),+      Functor0: function() {+        return $lazy_functorEffect(0);+      }+    };+  });+  var functorEffect = /* @__PURE__ */ $lazy_functorEffect(20);++  // output/Effect.Ref/foreign.js+  var _new = function(val) {+    return function() {+      return { value: val };+    };+  };+  var read = function(ref) {+    return function() {+      return ref.value;+    };+  };+  var modifyImpl = function(f) {+    return function(ref) {+      return function() {+        var t = f(ref.value);+        ref.value = t.state;+        return t.value;+      };+    };+  };++  // output/Effect.Ref/index.js+  var $$void2 = /* @__PURE__ */ $$void(functorEffect);+  var $$new = _new;+  var modify$prime = modifyImpl;+  var modify = function(f) {+    return modify$prime(function(s) {+      var s$prime = f(s);+      return {+        state: s$prime,+        value: s$prime+      };+    });+  };+  var modify_ = function(f) {+    return function(s) {+      return $$void2(modify(f)(s));+    };+  };++  // output/Data.Foldable/foreign.js+  var foldrArray = function(f) {+    return function(init3) {+      return function(xs) {+        var acc = init3;+        var len = xs.length;+        for (var i = len - 1; i >= 0; i--) {+          acc = f(xs[i])(acc);+        }+        return acc;+      };+    };+  };+  var foldlArray = function(f) {+    return function(init3) {+      return function(xs) {+        var acc = init3;+        var len = xs.length;+        for (var i = 0; i < len; i++) {+          acc = f(acc)(xs[i]);+        }+        return acc;+      };+    };+  };++  // output/Data.Tuple/index.js+  var Tuple = /* @__PURE__ */ (function() {+    function Tuple2(value0, value1) {+      this.value0 = value0;+      this.value1 = value1;+    }+    ;+    Tuple2.create = function(value0) {+      return function(value1) {+        return new Tuple2(value0, value1);+      };+    };+    return Tuple2;+  })();+  var uncurry = function(f) {+    return function(v) {+      return f(v.value0)(v.value1);+    };+  };+  var snd = function(v) {+    return v.value1;+  };+  var fst = function(v) {+    return v.value0;+  };++  // output/Data.Bifunctor/index.js+  var identity4 = /* @__PURE__ */ identity(categoryFn);+  var bimap = function(dict) {+    return dict.bimap;+  };+  var lmap = function(dictBifunctor) {+    var bimap1 = bimap(dictBifunctor);+    return function(f) {+      return bimap1(f)(identity4);+    };+  };+  var bifunctorEither = {+    bimap: function(v) {+      return function(v1) {+        return function(v2) {+          if (v2 instanceof Left) {+            return new Left(v(v2.value0));+          }+          ;+          if (v2 instanceof Right) {+            return new Right(v1(v2.value0));+          }+          ;+          throw new Error("Failed pattern match at Data.Bifunctor (line 38, column 1 - line 40, column 36): " + [v.constructor.name, v1.constructor.name, v2.constructor.name]);+        };+      };+    }+  };++  // output/Data.Foldable/index.js+  var eq12 = /* @__PURE__ */ eq(eqOrdering);+  var foldr = function(dict) {+    return dict.foldr;+  };+  var traverse_ = function(dictApplicative) {+    var applySecond2 = applySecond(dictApplicative.Apply0());+    var pure5 = pure(dictApplicative);+    return function(dictFoldable) {+      var foldr22 = foldr(dictFoldable);+      return function(f) {+        return foldr22(function($454) {+          return applySecond2(f($454));+        })(pure5(unit));+      };+    };+  };+  var for_ = function(dictApplicative) {+    var traverse_1 = traverse_(dictApplicative);+    return function(dictFoldable) {+      return flip(traverse_1(dictFoldable));+    };+  };+  var foldl = function(dict) {+    return dict.foldl;+  };+  var maximumBy = function(dictFoldable) {+    var foldl22 = foldl(dictFoldable);+    return function(cmp) {+      var max$prime = function(v) {+        return function(v1) {+          if (v instanceof Nothing) {+            return new Just(v1);+          }+          ;+          if (v instanceof Just) {+            return new Just((function() {+              var $303 = eq12(cmp(v.value0)(v1))(GT.value);+              if ($303) {+                return v.value0;+              }+              ;+              return v1;+            })());+          }+          ;+          throw new Error("Failed pattern match at Data.Foldable (line 441, column 3 - line 441, column 27): " + [v.constructor.name, v1.constructor.name]);+        };+      };+      return foldl22(max$prime)(Nothing.value);+    };+  };+  var maximum = function(dictOrd) {+    var compare2 = compare(dictOrd);+    return function(dictFoldable) {+      return maximumBy(dictFoldable)(compare2);+    };+  };+  var minimumBy = function(dictFoldable) {+    var foldl22 = foldl(dictFoldable);+    return function(cmp) {+      var min$prime = function(v) {+        return function(v1) {+          if (v instanceof Nothing) {+            return new Just(v1);+          }+          ;+          if (v instanceof Just) {+            return new Just((function() {+              var $307 = eq12(cmp(v.value0)(v1))(LT.value);+              if ($307) {+                return v.value0;+              }+              ;+              return v1;+            })());+          }+          ;+          throw new Error("Failed pattern match at Data.Foldable (line 454, column 3 - line 454, column 27): " + [v.constructor.name, v1.constructor.name]);+        };+      };+      return foldl22(min$prime)(Nothing.value);+    };+  };+  var minimum = function(dictOrd) {+    var compare2 = compare(dictOrd);+    return function(dictFoldable) {+      return minimumBy(dictFoldable)(compare2);+    };+  };+  var foldMapDefaultR = function(dictFoldable) {+    var foldr22 = foldr(dictFoldable);+    return function(dictMonoid) {+      var append5 = append(dictMonoid.Semigroup0());+      var mempty2 = mempty(dictMonoid);+      return function(f) {+        return foldr22(function(x) {+          return function(acc) {+            return append5(f(x))(acc);+          };+        })(mempty2);+      };+    };+  };+  var foldableArray = {+    foldr: foldrArray,+    foldl: foldlArray,+    foldMap: function(dictMonoid) {+      return foldMapDefaultR(foldableArray)(dictMonoid);+    }+  };++  // output/Data.Function.Uncurried/foreign.js+  var runFn2 = function(fn) {+    return function(a) {+      return function(b) {+        return fn(a, b);+      };+    };+  };+  var runFn3 = function(fn) {+    return function(a) {+      return function(b) {+        return function(c) {+          return fn(a, b, c);+        };+      };+    };+  };+  var runFn4 = function(fn) {+    return function(a) {+      return function(b) {+        return function(c) {+          return function(d) {+            return fn(a, b, c, d);+          };+        };+      };+    };+  };++  // output/Data.FunctorWithIndex/foreign.js+  var mapWithIndexArray = function(f) {+    return function(xs) {+      var l = xs.length;+      var result = Array(l);+      for (var i = 0; i < l; i++) {+        result[i] = f(i)(xs[i]);+      }+      return result;+    };+  };++  // output/Data.FunctorWithIndex/index.js+  var mapWithIndex = function(dict) {+    return dict.mapWithIndex;+  };+  var functorWithIndexArray = {+    mapWithIndex: mapWithIndexArray,+    Functor0: function() {+      return functorArray;+    }+  };++  // output/Data.Traversable/foreign.js+  var traverseArrayImpl = /* @__PURE__ */ (function() {+    function array1(a) {+      return [a];+    }+    function array2(a) {+      return function(b) {+        return [a, b];+      };+    }+    function array3(a) {+      return function(b) {+        return function(c) {+          return [a, b, c];+        };+      };+    }+    function concat2(xs) {+      return function(ys) {+        return xs.concat(ys);+      };+    }+    return function(apply2) {+      return function(map12) {+        return function(pure5) {+          return function(f) {+            return function(array) {+              function go(bot, top3) {+                switch (top3 - bot) {+                  case 0:+                    return pure5([]);+                  case 1:+                    return map12(array1)(f(array[bot]));+                  case 2:+                    return apply2(map12(array2)(f(array[bot])))(f(array[bot + 1]));+                  case 3:+                    return apply2(apply2(map12(array3)(f(array[bot])))(f(array[bot + 1])))(f(array[bot + 2]));+                  default:+                    var pivot = bot + Math.floor((top3 - bot) / 4) * 2;+                    return apply2(map12(concat2)(go(bot, pivot)))(go(pivot, top3));+                }+              }+              return go(0, array.length);+            };+          };+        };+      };+    };+  })();++  // output/Data.Traversable/index.js+  var identity5 = /* @__PURE__ */ identity(categoryFn);+  var traverse = function(dict) {+    return dict.traverse;+  };+  var sequenceDefault = function(dictTraversable) {+    var traverse22 = traverse(dictTraversable);+    return function(dictApplicative) {+      return traverse22(dictApplicative)(identity5);+    };+  };+  var traversableArray = {+    traverse: function(dictApplicative) {+      var Apply0 = dictApplicative.Apply0();+      return traverseArrayImpl(apply(Apply0))(map(Apply0.Functor0()))(pure(dictApplicative));+    },+    sequence: function(dictApplicative) {+      return sequenceDefault(traversableArray)(dictApplicative);+    },+    Functor0: function() {+      return functorArray;+    },+    Foldable1: function() {+      return foldableArray;+    }+  };+  var sequence = function(dict) {+    return dict.sequence;+  };++  // output/Data.Unfoldable/foreign.js+  var unfoldrArrayImpl = function(isNothing2) {+    return function(fromJust4) {+      return function(fst2) {+        return function(snd2) {+          return function(f) {+            return function(b) {+              var result = [];+              var value = b;+              while (true) {+                var maybe2 = f(value);+                if (isNothing2(maybe2)) return result;+                var tuple = fromJust4(maybe2);+                result.push(fst2(tuple));+                value = snd2(tuple);+              }+            };+          };+        };+      };+    };+  };++  // output/Data.Unfoldable1/foreign.js+  var unfoldr1ArrayImpl = function(isNothing2) {+    return function(fromJust4) {+      return function(fst2) {+        return function(snd2) {+          return function(f) {+            return function(b) {+              var result = [];+              var value = b;+              while (true) {+                var tuple = f(value);+                result.push(fst2(tuple));+                var maybe2 = snd2(tuple);+                if (isNothing2(maybe2)) return result;+                value = fromJust4(maybe2);+              }+            };+          };+        };+      };+    };+  };++  // output/Data.Unfoldable1/index.js+  var fromJust2 = /* @__PURE__ */ fromJust();+  var unfoldable1Array = {+    unfoldr1: /* @__PURE__ */ unfoldr1ArrayImpl(isNothing)(fromJust2)(fst)(snd)+  };++  // output/Data.Unfoldable/index.js+  var fromJust3 = /* @__PURE__ */ fromJust();+  var unfoldr = function(dict) {+    return dict.unfoldr;+  };+  var unfoldableArray = {+    unfoldr: /* @__PURE__ */ unfoldrArrayImpl(isNothing)(fromJust3)(fst)(snd),+    Unfoldable10: function() {+      return unfoldable1Array;+    }+  };++  // output/Data.Array/index.js+  var zipWith = /* @__PURE__ */ runFn3(zipWithImpl);+  var zip = /* @__PURE__ */ (function() {+    return zipWith(Tuple.create);+  })();+  var uncons = /* @__PURE__ */ (function() {+    return runFn3(unconsImpl)($$const(Nothing.value))(function(x) {+      return function(xs) {+        return new Just({+          head: x,+          tail: xs+        });+      };+    });+  })();+  var singleton2 = function(a) {+    return [a];+  };+  var replicate = /* @__PURE__ */ runFn2(replicateImpl);+  var range2 = /* @__PURE__ */ runFn2(rangeImpl);+  var mapWithIndex2 = /* @__PURE__ */ mapWithIndex(functorWithIndexArray);+  var index = /* @__PURE__ */ (function() {+    return runFn4(indexImpl)(Just.create)(Nothing.value);+  })();+  var filter = /* @__PURE__ */ runFn2(filterImpl);+  var concatMap = /* @__PURE__ */ flip(/* @__PURE__ */ bind(bindArray));+  var mapMaybe = function(f) {+    return concatMap((function() {+      var $189 = maybe([])(singleton2);+      return function($190) {+        return $189(f($190));+      };+    })());+  };++  // output/Data.Int/foreign.js+  var fromNumberImpl = function(just) {+    return function(nothing) {+      return function(n) {+        return (n | 0) === n ? just(n) : nothing;+      };+    };+  };+  var toNumber = function(n) {+    return n;+  };+  var fromStringAsImpl = function(just) {+    return function(nothing) {+      return function(radix) {+        var digits;+        if (radix < 11) {+          digits = "[0-" + (radix - 1).toString() + "]";+        } else if (radix === 11) {+          digits = "[0-9a]";+        } else {+          digits = "[0-9a-" + String.fromCharCode(86 + radix) + "]";+        }+        var pattern = new RegExp("^[\\+\\-]?" + digits + "+$", "i");+        return function(s) {+          if (pattern.test(s)) {+            var i = parseInt(s, radix);+            return (i | 0) === i ? just(i) : nothing;+          } else {+            return nothing;+          }+        };+      };+    };+  };++  // output/Data.Int/index.js+  var top2 = /* @__PURE__ */ top(boundedInt);+  var bottom2 = /* @__PURE__ */ bottom(boundedInt);+  var hexadecimal = 16;+  var fromStringAs = /* @__PURE__ */ (function() {+    return fromStringAsImpl(Just.create)(Nothing.value);+  })();+  var fromNumber = /* @__PURE__ */ (function() {+    return fromNumberImpl(Just.create)(Nothing.value);+  })();+  var unsafeClamp = function(x) {+    if (!isFiniteImpl(x)) {+      return 0;+    }+    ;+    if (x >= toNumber(top2)) {+      return top2;+    }+    ;+    if (x <= toNumber(bottom2)) {+      return bottom2;+    }+    ;+    if (otherwise) {+      return fromMaybe(0)(fromNumber(x));+    }+    ;+    throw new Error("Failed pattern match at Data.Int (line 72, column 1 - line 72, column 29): " + [x.constructor.name]);+  };+  var round2 = function($37) {+    return unsafeClamp(round($37));+  };+  var floor2 = function($39) {+    return unsafeClamp(floor($39));+  };+  var ceil2 = function($40) {+    return unsafeClamp(ceil($40));+  };++  // output/Data.Number.Format/foreign.js+  function wrap(method) {+    return function(d) {+      return function(num) {+        return method.apply(num, [d]);+      };+    };+  }+  var toPrecisionNative = wrap(Number.prototype.toPrecision);+  var toFixedNative = wrap(Number.prototype.toFixed);+  var toExponentialNative = wrap(Number.prototype.toExponential);++  // output/Hgg.Plot.Backend.WebGL.Axes/index.js+  var axesBoxEdges = function(v) {+    var p111 = new Point3(v.xMax, v.yMax, v.zMax);+    var p110 = new Point3(v.xMax, v.yMax, v.zMin);+    var p101 = new Point3(v.xMax, v.yMin, v.zMax);+    var p100 = new Point3(v.xMax, v.yMin, v.zMin);+    var p011 = new Point3(v.xMin, v.yMax, v.zMax);+    var p010 = new Point3(v.xMin, v.yMax, v.zMin);+    var p001 = new Point3(v.xMin, v.yMin, v.zMax);+    var p000 = new Point3(v.xMin, v.yMin, v.zMin);+    return [p000, p100, p100, p110, p110, p010, p010, p000, p001, p101, p101, p111, p111, p011, p011, p001, p000, p001, p100, p101, p110, p111, p010, p011];+  };++  // output/Hgg.Plot.Backend.WebGL.Raw/foreign.js+  function _toMaybe(n) {+    if (n === null || n === void 0) return { isNull: true, value: null };+    return { isNull: false, value: n };+  }+  function _canvasById(id2) {+    var el = document.getElementById(id2);+    if (el && el.tagName === "CANVAS") return el;+    return null;+  }+  function _getWebGL2Context(canvas) {+    return canvas.getContext("webgl2");+  }+  function _createShader(gl, type, src) {+    var sh = gl.createShader(type);+    if (!sh) return null;+    gl.shaderSource(sh, src);+    gl.compileShader(sh);+    if (!gl.getShaderParameter(sh, gl.COMPILE_STATUS)) {+      var log5 = gl.getShaderInfoLog(sh);+      console.error("[hgg WebGL] shader compile error:\n" + log5 + "\n--- source ---\n" + src);+      gl.deleteShader(sh);+      return null;+    }+    return sh;+  }+  function _createProgram(gl, vs, fs) {+    var p = gl.createProgram();+    if (!p) return null;+    gl.attachShader(p, vs);+    gl.attachShader(p, fs);+    gl.linkProgram(p);+    if (!gl.getProgramParameter(p, gl.LINK_STATUS)) {+      var log5 = gl.getProgramInfoLog(p);+      console.error("[hgg WebGL] program link error:\n" + log5);+      gl.deleteProgram(p);+      return null;+    }+    return p;+  }+  function _useProgram(gl, p) {+    gl.useProgram(p);+  }+  function _getUniformLocation(gl, p, name) {+    var loc = gl.getUniformLocation(p, name);+    return loc;+  }+  function _createBuffer(gl) {+    return gl.createBuffer();+  }+  function _bindArrayBuffer(gl, buf) {+    gl.bindBuffer(gl.ARRAY_BUFFER, buf);+  }+  function _bufferDataFloat32(gl, arr) {+    gl.bufferData(gl.ARRAY_BUFFER, new Float32Array(arr), gl.STATIC_DRAW);+  }+  function _vertexAttribPointerFloat(gl, loc, size3, stride, offset) {+    gl.vertexAttribPointer(loc, size3, gl.FLOAT, false, stride, offset);+  }+  function _enableVertexAttribArray(gl, loc) {+    gl.enableVertexAttribArray(loc);+  }+  function _getAttribLocation(gl, p, name) {+    return gl.getAttribLocation(p, name);+  }+  function _uniformMatrix4fv(gl, loc, arr) {+    gl.uniformMatrix4fv(loc, false, new Float32Array(arr));+  }+  function _uniform1f(gl, loc, v) {+    gl.uniform1f(loc, v);+  }+  function _uniform3f(gl, loc, a, b, c) {+    gl.uniform3f(loc, a, b, c);+  }+  function _uniform4f(gl, loc, a, b, c, d) {+    gl.uniform4f(loc, a, b, c, d);+  }+  function _clearColor(gl, r, g, b, a) {+    gl.clearColor(r, g, b, a);+  }+  function _clear(gl, mask) {+    gl.clear(mask);+  }+  function _drawArrays(gl, mode, first, count) {+    gl.drawArrays(mode, first, count);+  }+  function _viewport(gl, x, y, w, h) {+    gl.viewport(x, y, w, h);+  }+  function _enableBlend(gl) {+    gl.enable(gl.BLEND);+    gl.blendFunc(gl.SRC_ALPHA, gl.ONE_MINUS_SRC_ALPHA);+  }+  function _enableDepthTest(gl) {+    gl.enable(gl.DEPTH_TEST);+    gl.depthFunc(gl.LEQUAL);+  }+  function _depthMask(gl, flag) {+    gl.depthMask(flag);+  }++  // output/Effect.Uncurried/foreign.js+  var mkEffectFn2 = function mkEffectFn22(fn) {+    return function(a, b) {+      return fn(a)(b)();+    };+  };+  var runEffectFn1 = function runEffectFn12(fn) {+    return function(a) {+      return function() {+        return fn(a);+      };+    };+  };+  var runEffectFn2 = function runEffectFn22(fn) {+    return function(a) {+      return function(b) {+        return function() {+          return fn(a, b);+        };+      };+    };+  };+  var runEffectFn3 = function runEffectFn32(fn) {+    return function(a) {+      return function(b) {+        return function(c) {+          return function() {+            return fn(a, b, c);+          };+        };+      };+    };+  };+  var runEffectFn4 = function runEffectFn42(fn) {+    return function(a) {+      return function(b) {+        return function(c) {+          return function(d) {+            return function() {+              return fn(a, b, c, d);+            };+          };+        };+      };+    };+  };+  var runEffectFn5 = function runEffectFn52(fn) {+    return function(a) {+      return function(b) {+        return function(c) {+          return function(d) {+            return function(e) {+              return function() {+                return fn(a, b, c, d, e);+              };+            };+          };+        };+      };+    };+  };+  var runEffectFn6 = function runEffectFn62(fn) {+    return function(a) {+      return function(b) {+        return function(c) {+          return function(d) {+            return function(e) {+              return function(f) {+                return function() {+                  return fn(a, b, c, d, e, f);+                };+              };+            };+          };+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Raw/index.js+  var map4 = /* @__PURE__ */ map(functorEffect);+  var viewport = /* @__PURE__ */ runEffectFn5(_viewport);+  var vertexAttribPointerFloat = /* @__PURE__ */ runEffectFn5(_vertexAttribPointerFloat);+  var useProgram = /* @__PURE__ */ runEffectFn2(_useProgram);+  var uniformMatrix4fv = /* @__PURE__ */ runEffectFn3(_uniformMatrix4fv);+  var uniform4f = /* @__PURE__ */ runEffectFn6(_uniform4f);+  var uniform3f = /* @__PURE__ */ runEffectFn5(_uniform3f);+  var uniform1f = /* @__PURE__ */ runEffectFn3(_uniform1f);+  var toMaybe = function(n) {+    var v = _toMaybe(n);+    if (v.isNull) {+      return Nothing.value;+    }+    ;+    return new Just(v.value);+  };+  var glVERTEX_SHADER = 35633;+  var glTRIANGLES = 4;+  var glPOINTS = 0;+  var glLINE_STRIP = 3;+  var glLINES = 1;+  var glFRAGMENT_SHADER = 35632;+  var glDEPTH_BUFFER_BIT = 256;+  var glCOLOR_BUFFER_BIT = 16384;+  var getWebGL2Context = function(c) {+    return map4(toMaybe)(function() {+      return _getWebGL2Context(c);+    });+  };+  var getUniformLocation = function(ctx) {+    return function(p) {+      return function(name) {+        return map4(toMaybe)(function() {+          return _getUniformLocation(ctx, p, name);+        });+      };+    };+  };+  var getAttribLocation = /* @__PURE__ */ runEffectFn3(_getAttribLocation);+  var enableVertexAttribArray = /* @__PURE__ */ runEffectFn2(_enableVertexAttribArray);+  var enableDepthTest = /* @__PURE__ */ runEffectFn1(_enableDepthTest);+  var enableBlend = /* @__PURE__ */ runEffectFn1(_enableBlend);+  var drawArrays = /* @__PURE__ */ runEffectFn4(_drawArrays);+  var depthMask = /* @__PURE__ */ runEffectFn2(_depthMask);+  var createShader = function(ctx) {+    return function(ty) {+      return function(src) {+        return map4(toMaybe)(function() {+          return _createShader(ctx, ty, src);+        });+      };+    };+  };+  var createProgram = function(ctx) {+    return function(vs) {+      return function(fs) {+        return map4(toMaybe)(function() {+          return _createProgram(ctx, vs, fs);+        });+      };+    };+  };+  var createBuffer = /* @__PURE__ */ runEffectFn1(_createBuffer);+  var clearColor = /* @__PURE__ */ runEffectFn5(_clearColor);+  var clear = /* @__PURE__ */ runEffectFn2(_clear);+  var canvasById = function(s) {+    return map4(toMaybe)(function() {+      return _canvasById(s);+    });+  };+  var bufferDataFloat32 = /* @__PURE__ */ runEffectFn2(_bufferDataFloat32);+  var bindArrayBuffer = /* @__PURE__ */ runEffectFn2(_bindArrayBuffer);++  // output/Data.String.CodePoints/foreign.js+  var hasArrayFrom = typeof Array.from === "function";+  var hasStringIterator = typeof Symbol !== "undefined" && Symbol != null && typeof Symbol.iterator !== "undefined" && typeof String.prototype[Symbol.iterator] === "function";+  var hasFromCodePoint = typeof String.prototype.fromCodePoint === "function";+  var hasCodePointAt = typeof String.prototype.codePointAt === "function";+  var _unsafeCodePointAt0 = function(fallback) {+    return hasCodePointAt ? function(str) {+      return str.codePointAt(0);+    } : fallback;+  };+  var _toCodePointArray = function(fallback) {+    return function(unsafeCodePointAt02) {+      if (hasArrayFrom) {+        return function(str) {+          return Array.from(str, unsafeCodePointAt02);+        };+      }+      return fallback;+    };+  };++  // output/Data.Enum/foreign.js+  function toCharCode(c) {+    return c.charCodeAt(0);+  }+  function fromCharCode(c) {+    return String.fromCharCode(c);+  }++  // output/Data.Enum/index.js+  var bottom1 = /* @__PURE__ */ bottom(boundedChar);+  var top1 = /* @__PURE__ */ top(boundedChar);+  var fromEnum = function(dict) {+    return dict.fromEnum;+  };+  var defaultSucc = function(toEnum$prime) {+    return function(fromEnum$prime) {+      return function(a) {+        return toEnum$prime(fromEnum$prime(a) + 1 | 0);+      };+    };+  };+  var defaultPred = function(toEnum$prime) {+    return function(fromEnum$prime) {+      return function(a) {+        return toEnum$prime(fromEnum$prime(a) - 1 | 0);+      };+    };+  };+  var charToEnum = function(v) {+    if (v >= toCharCode(bottom1) && v <= toCharCode(top1)) {+      return new Just(fromCharCode(v));+    }+    ;+    return Nothing.value;+  };+  var enumChar = {+    succ: /* @__PURE__ */ defaultSucc(charToEnum)(toCharCode),+    pred: /* @__PURE__ */ defaultPred(charToEnum)(toCharCode),+    Ord0: function() {+      return ordChar;+    }+  };+  var boundedEnumChar = /* @__PURE__ */ (function() {+    return {+      cardinality: toCharCode(top1) - toCharCode(bottom1) | 0,+      toEnum: charToEnum,+      fromEnum: toCharCode,+      Bounded0: function() {+        return boundedChar;+      },+      Enum1: function() {+        return enumChar;+      }+    };+  })();++  // output/Data.String.CodeUnits/foreign.js+  var length2 = function(s) {+    return s.length;+  };+  var take = function(n) {+    return function(s) {+      return s.substr(0, n);+    };+  };+  var drop = function(n) {+    return function(s) {+      return s.substring(n);+    };+  };++  // output/Data.String.Unsafe/foreign.js+  var charAt = function(i) {+    return function(s) {+      if (i >= 0 && i < s.length) return s.charAt(i);+      throw new Error("Data.String.Unsafe.charAt: Invalid index.");+    };+  };++  // output/Data.String.CodePoints/index.js+  var fromEnum2 = /* @__PURE__ */ fromEnum(boundedEnumChar);+  var map5 = /* @__PURE__ */ map(functorMaybe);+  var unfoldr2 = /* @__PURE__ */ unfoldr(unfoldableArray);+  var unsurrogate = function(lead) {+    return function(trail) {+      return (((lead - 55296 | 0) * 1024 | 0) + (trail - 56320 | 0) | 0) + 65536 | 0;+    };+  };+  var isTrail = function(cu) {+    return 56320 <= cu && cu <= 57343;+  };+  var isLead = function(cu) {+    return 55296 <= cu && cu <= 56319;+  };+  var uncons2 = function(s) {+    var v = length2(s);+    if (v === 0) {+      return Nothing.value;+    }+    ;+    if (v === 1) {+      return new Just({+        head: fromEnum2(charAt(0)(s)),+        tail: ""+      });+    }+    ;+    var cu1 = fromEnum2(charAt(1)(s));+    var cu0 = fromEnum2(charAt(0)(s));+    var $43 = isLead(cu0) && isTrail(cu1);+    if ($43) {+      return new Just({+        head: unsurrogate(cu0)(cu1),+        tail: drop(2)(s)+      });+    }+    ;+    return new Just({+      head: cu0,+      tail: drop(1)(s)+    });+  };+  var unconsButWithTuple = function(s) {+    return map5(function(v) {+      return new Tuple(v.head, v.tail);+    })(uncons2(s));+  };+  var toCodePointArrayFallback = function(s) {+    return unfoldr2(unconsButWithTuple)(s);+  };+  var unsafeCodePointAt0Fallback = function(s) {+    var cu0 = fromEnum2(charAt(0)(s));+    var $47 = isLead(cu0) && length2(s) > 1;+    if ($47) {+      var cu1 = fromEnum2(charAt(1)(s));+      var $48 = isTrail(cu1);+      if ($48) {+        return unsurrogate(cu0)(cu1);+      }+      ;+      return cu0;+    }+    ;+    return cu0;+  };+  var unsafeCodePointAt0 = /* @__PURE__ */ _unsafeCodePointAt0(unsafeCodePointAt0Fallback);+  var toCodePointArray = /* @__PURE__ */ _toCodePointArray(toCodePointArrayFallback)(unsafeCodePointAt0);+  var length3 = function($74) {+    return length(toCodePointArray($74));+  };++  // output/Hgg.Plot.Backend.WebGL.Common/index.js+  var uploadAttribVec3 = function(ctx) {+    return function(buf) {+      return function(loc) {+        return function(arr) {+          return function __do() {+            bindArrayBuffer(ctx)(buf)();+            bufferDataFloat32(ctx)(arr)();+            enableVertexAttribArray(ctx)(loc)();+            return vertexAttribPointerFloat(ctx)(loc)(3)(0)(0)();+          };+        };+      };+    };+  };+  var uploadAttrib1 = function(ctx) {+    return function(buf) {+      return function(loc) {+        return function(arr) {+          return function __do() {+            bindArrayBuffer(ctx)(buf)();+            bufferDataFloat32(ctx)(arr)();+            enableVertexAttribArray(ctx)(loc)();+            return vertexAttribPointerFloat(ctx)(loc)(1)(0)(0)();+          };+        };+      };+    };+  };+  var parseHexColor = function(s) {+    var toF = function(x) {+      return toNumber(x) / 255;+    };+    var body = (function() {+      var $5 = take(1)(s) === "#";+      if ($5) {+        return drop(1)(s);+      }+      ;+      return s;+    })();+    var n = length3(body);+    var parseByte = function(i) {+      return fromMaybe(0)(fromStringAs(hexadecimal)(take(2)(drop(i)(body))));+    };+    if (n === 6) {+      return {+        r: toF(parseByte(0)),+        g: toF(parseByte(2)),+        b: toF(parseByte(4)),+        a: 1+      };+    }+    ;+    if (n === 8) {+      return {+        r: toF(parseByte(0)),+        g: toF(parseByte(2)),+        b: toF(parseByte(4)),+        a: toF(parseByte(6))+      };+    }+    ;+    return {+      r: 0,+      g: 0,+      b: 0,+      a: 1+    };+  };+  var flattenPoints3 = /* @__PURE__ */ concatMap(function(v) {+    return [v.value0, v.value1, v.value2];+  });++  // output/Hgg.Plot.Backend.WebGL.Context/index.js+  var translateMat4 = function(x) {+    return function(y) {+      return function(z) {+        return new Mat4(1, 0, 0, x, 0, 1, 0, y, 0, 0, 1, z, 0, 0, 0, 1);+      };+    };+  };+  var scaleMat4 = function(x) {+    return function(y) {+      return function(z) {+        return new Mat4(x, 0, 0, 0, 0, y, 0, 0, 0, 0, z, 0, 0, 0, 0, 1);+      };+    };+  };+  var perspectiveMat4 = function(fov) {+    return function(aspect) {+      return function(near) {+        return function(far) {+          var nf = 1 / (near - far);+          var f = 1 / tan(fov / 2);+          return new Mat4(f / aspect, 0, 0, 0, 0, f, 0, 0, 0, 0, (far + near) * nf, 2 * far * near * nf, 0, 0, -1, 0);+        };+      };+    };+  };+  var orthographicMat4 = function(xHalf) {+    return function(yHalf) {+      return function(near) {+        return function(far) {+          var fn = far - near;+          return new Mat4(1 / xHalf, 0, 0, 0, 0, 1 / yHalf, 0, 0, 0, 0, -2 / fn, -(far + near) / fn, 0, 0, 0, 1);+        };+      };+    };+  };+  var multMat4 = function(v) {+    return function(v1) {+      return new Mat4(v.value0 * v1.value0 + v.value1 * v1.value4 + v.value2 * v1.value8 + v.value3 * v1.value12, v.value0 * v1.value1 + v.value1 * v1.value5 + v.value2 * v1.value9 + v.value3 * v1.value13, v.value0 * v1.value2 + v.value1 * v1.value6 + v.value2 * v1.value10 + v.value3 * v1.value14, v.value0 * v1.value3 + v.value1 * v1.value7 + v.value2 * v1.value11 + v.value3 * v1.value15, v.value4 * v1.value0 + v.value5 * v1.value4 + v.value6 * v1.value8 + v.value7 * v1.value12, v.value4 * v1.value1 + v.value5 * v1.value5 + v.value6 * v1.value9 + v.value7 * v1.value13, v.value4 * v1.value2 + v.value5 * v1.value6 + v.value6 * v1.value10 + v.value7 * v1.value14, v.value4 * v1.value3 + v.value5 * v1.value7 + v.value6 * v1.value11 + v.value7 * v1.value15, v.value8 * v1.value0 + v.value9 * v1.value4 + v.value10 * v1.value8 + v.value11 * v1.value12, v.value8 * v1.value1 + v.value9 * v1.value5 + v.value10 * v1.value9 + v.value11 * v1.value13, v.value8 * v1.value2 + v.value9 * v1.value6 + v.value10 * v1.value10 + v.value11 * v1.value14, v.value8 * v1.value3 + v.value9 * v1.value7 + v.value10 * v1.value11 + v.value11 * v1.value15, v.value12 * v1.value0 + v.value13 * v1.value4 + v.value14 * v1.value8 + v.value15 * v1.value12, v.value12 * v1.value1 + v.value13 * v1.value5 + v.value14 * v1.value9 + v.value15 * v1.value13, v.value12 * v1.value2 + v.value13 * v1.value6 + v.value14 * v1.value10 + v.value15 * v1.value14, v.value12 * v1.value3 + v.value13 * v1.value7 + v.value14 * v1.value11 + v.value15 * v1.value15);+    };+  };+  var mat4ToArrayColMajor = function(v) {+    return [v.value0, v.value4, v.value8, v.value12, v.value1, v.value5, v.value9, v.value13, v.value2, v.value6, v.value10, v.value14, v.value3, v.value7, v.value11, v.value15];+  };+  var lookAtMat4 = function(v) {+    var toVec3 = function(a) {+      return function(b) {+        return function(c) {+          return new Vec3(a, b, c);+        };+      };+    };+    var f = normalizeV3(subV3(toVec3(v.target.value0)(v.target.value1)(v.target.value2))(toVec3(v.eye.value0)(v.eye.value1)(v.eye.value2)));+    var s = normalizeV3(crossV3(f)(v.up));+    var u = crossV3(s)(f);+    var tzT = f.value0 * v.eye.value0 + f.value1 * v.eye.value1 + f.value2 * v.eye.value2;+    var tyT = -(u.value0 * v.eye.value0 + u.value1 * v.eye.value1 + u.value2 * v.eye.value2);+    var txT = -(s.value0 * v.eye.value0 + s.value1 * v.eye.value1 + s.value2 * v.eye.value2);+    return new Mat4(s.value0, s.value1, s.value2, txT, u.value0, u.value1, u.value2, tyT, -f.value0, -f.value1, -f.value2, tzT, 0, 0, 0, 1);+  };+  var initWebGL = function(canvas) {+    return function __do() {+      var mc = getWebGL2Context(canvas)();+      if (mc instanceof Nothing) {+        return new Left("WebGL2 context unavailable (= canvas.getContext('webgl2') == null)");+      }+      ;+      if (mc instanceof Just) {+        enableDepthTest(mc.value0)();+        enableBlend(mc.value0)();+        return new Right(mc.value0);+      }+      ;+      throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Context (line 58, column 3 - line 63, column 23): " + [mc.constructor.name]);+    };+  };+  var computeMVP = function(cam) {+    return function(proj) {+      var view = lookAtMat4(cam);+      var p = (function() {+        if (proj instanceof Perspective) {+          return perspectiveMat4(proj.value0.fov)(proj.value0.aspect)(proj.value0.near)(proj.value0.far);+        }+        ;+        if (proj instanceof Orthographic) {+          return orthographicMat4(proj.value0.xHalf)(proj.value0.yHalf)(proj.value0.near)(proj.value0.far);+        }+        ;+        throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Context (line 214, column 9 - line 216, column 70): " + [proj.constructor.name]);+      })();+      return multMat4(p)(view);+    };+  };+  var compileProgram = function(ctx) {+    return function(vsSrc) {+      return function(fsSrc) {+        return function __do() {+          var mvs = createShader(ctx)(glVERTEX_SHADER)(vsSrc)();+          if (mvs instanceof Nothing) {+            return new Left("vertex shader compile failed (= console.error \u53C2\u7167)");+          }+          ;+          if (mvs instanceof Just) {+            var mfs = createShader(ctx)(glFRAGMENT_SHADER)(fsSrc)();+            if (mfs instanceof Nothing) {+              return new Left("fragment shader compile failed");+            }+            ;+            if (mfs instanceof Just) {+              var mp = createProgram(ctx)(mvs.value0)(mfs.value0)();+              if (mp instanceof Nothing) {+                return new Left("program link failed");+              }+              ;+              if (mp instanceof Just) {+                return new Right(mp.value0);+              }+              ;+              throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Context (line 85, column 11 - line 87, column 37): " + [mp.constructor.name]);+            }+            ;+            throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Context (line 81, column 7 - line 87, column 37): " + [mfs.constructor.name]);+          }+          ;+          throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Context (line 77, column 3 - line 87, column 37): " + [mvs.constructor.name]);+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Shaders/index.js+  var pointVS = "#version 300 es\nin vec3 aPosition;\nuniform mat4 uMVP;\nuniform float uPointSize;\nvoid main() {\n  gl_Position = uMVP * vec4(aPosition, 1.0);\n  gl_PointSize = uPointSize;\n}\n";+  var pointFS = "#version 300 es\nprecision mediump float;\nuniform vec4 uColor;\nout vec4 outColor;\nvoid main() {\n  vec2 c = gl_PointCoord - vec2(0.5);\n  float d = dot(c, c);\n  if (d > 0.25) discard;\n  // soft edge (= \u534A\u5F84 0.5 \u3067 alpha fade)\n  float a = 1.0 - smoothstep(0.2, 0.25, d);\n  outColor = vec4(uColor.rgb, uColor.a * a);\n}\n";+  var pointColorVS = "#version 300 es\nin vec3 aPosition;\nin vec3 aColor;\nin float aSize;\nuniform mat4 uMVP;\nout vec3 vColor;\nvoid main() {\n  gl_Position = uMVP * vec4(aPosition, 1.0);\n  gl_PointSize = aSize;\n  vColor = aColor;\n}\n";+  var pointColorFS = "#version 300 es\nprecision mediump float;\nin vec3 vColor;\nuniform float uAlpha;\nout vec4 outColor;\nvoid main() {\n  vec2 c = gl_PointCoord - vec2(0.5);\n  float d = dot(c, c);\n  if (d > 0.25) discard;\n  float a = 1.0 - smoothstep(0.2, 0.25, d);\n  outColor = vec4(vColor, uAlpha * a);\n}\n";+  var lineVS = "#version 300 es\nin vec3 aPosition;\nuniform mat4 uMVP;\nvoid main() {\n  gl_Position = uMVP * vec4(aPosition, 1.0);\n}\n";+  var lineFS = "#version 300 es\nprecision mediump float;\nuniform vec4 uColor;\nout vec4 outColor;\nvoid main() {\n  outColor = uColor;\n}\n";+  var lambertVS = "#version 300 es\nin vec3 aPosition;\nin vec3 aNormal;\nuniform mat4 uMVP;\nout vec3 vNormal;\nvoid main() {\n  gl_Position = uMVP * vec4(aPosition, 1.0);\n  vNormal = aNormal;\n}\n";+  var lambertFS = "#version 300 es\nprecision mediump float;\nin vec3 vNormal;\nuniform vec4 uColor;\nuniform vec3 uLightDir;\nout vec4 outColor;\nvoid main() {\n  vec3 n = normalize(vNormal);\n  vec3 l = normalize(uLightDir);\n  float diff = max(dot(n, l), 0.0);\n  float intensity = 0.3 + 0.7 * diff;\n  outColor = vec4(uColor.rgb * intensity, uColor.a);\n}\n";+  var flatVS = "#version 300 es\nin vec3 aPosition;\nuniform mat4 uMVP;\nvoid main() {\n  gl_Position = uMVP * vec4(aPosition, 1.0);\n}\n";+  var flatFS = "#version 300 es\nprecision mediump float;\nuniform vec4 uColor;\nout vec4 outColor;\nvoid main() {\n  outColor = uColor;\n}\n";++  // output/Hgg.Plot.Backend.WebGL.Line/index.js+  var mkLineProgram = function(ctx) {+    return function __do() {+      var ep = compileProgram(ctx)(lineVS)(lineFS)();+      if (ep instanceof Left) {+        return new Left("line: " + ep.value0);+      }+      ;+      if (ep instanceof Right) {+        var pos = getAttribLocation(ctx)(ep.value0)("aPosition")();+        var muvp = getUniformLocation(ctx)(ep.value0)("uMVP")();+        var mc = getUniformLocation(ctx)(ep.value0)("uColor")();+        if (muvp instanceof Just && mc instanceof Just) {+          var buf = createBuffer(ctx)();+          return new Right({+            program: ep.value0,+            aPosition: pos,+            uMVP: muvp.value0,+            uColor: mc.value0,+            buffer: buf+          });+        }+        ;+        return new Left("line: uniform lookup failed");+      }+      ;+      throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Line (line 39, column 3 - line 50, column 58): " + [ep.constructor.name]);+    };+  };+  var indexPair = function(pts) {+    return function(i) {+      return function(j) {+        var v = index(pts)(j);+        var v1 = index(pts)(i);+        if (v1 instanceof Just && v instanceof Just) {+          return [v1.value0, v.value0];+        }+        ;+        return [];+      };+    };+  };+  var drawWireframe = function(ctx) {+    return function(v) {+      return function(mvp) {+        return function(v1) {+          return function __do() {+            useProgram(ctx)(v.program)();+            uniformMatrix4fv(ctx)(v.uMVP)(mat4ToArrayColMajor(mvp))();+            var c = parseHexColor(v1.color);+            uniform4f(ctx)(v.uColor)(c.r)(c.g)(c.b)(1)();+            var expanded = concatMap(function(e) {+              return indexPair(v1.points)(e.i)(e.j);+            })(v1.edges);+            var flat = flattenPoints3(expanded);+            uploadAttribVec3(ctx)(v.buffer)(v.aPosition)(flat)();+            return drawArrays(ctx)(glLINES)(0)(length(expanded))();+          };+        };+      };+    };+  };+  var drawLine = function(ctx) {+    return function(v) {+      return function(mvp) {+        return function(v1) {+          return function __do() {+            useProgram(ctx)(v.program)();+            uniformMatrix4fv(ctx)(v.uMVP)(mat4ToArrayColMajor(mvp))();+            var c = parseHexColor(v1.color);+            uniform4f(ctx)(v.uColor)(c.r)(c.g)(c.b)(1)();+            var flat = flattenPoints3(v1.points);+            uploadAttribVec3(ctx)(v.buffer)(v.aPosition)(flat)();+            return drawArrays(ctx)(glLINE_STRIP)(0)(length(v1.points))();+          };+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Surface/index.js+  var when2 = /* @__PURE__ */ when(applicativeEffect);+  var div2 = /* @__PURE__ */ div(euclideanRingInt);+  var append1 = /* @__PURE__ */ append(semigroupArray);+  var bind1 = /* @__PURE__ */ bind(bindMaybe);+  var sqrtNum = sqrt;+  var mkSurfaceProgram = function(ctx) {+    return function __do() {+      var ef = compileProgram(ctx)(flatVS)(flatFS)();+      var el = compileProgram(ctx)(lambertVS)(lambertFS)();+      if (ef instanceof Right && el instanceof Right) {+        var fpos = getAttribLocation(ctx)(ef.value0)("aPosition")();+        var mfmvp = getUniformLocation(ctx)(ef.value0)("uMVP")();+        var mfc = getUniformLocation(ctx)(ef.value0)("uColor")();+        var lpos = getAttribLocation(ctx)(el.value0)("aPosition")();+        var lnor = getAttribLocation(ctx)(el.value0)("aNormal")();+        var mlmvp = getUniformLocation(ctx)(el.value0)("uMVP")();+        var mlc = getUniformLocation(ctx)(el.value0)("uColor")();+        var mld = getUniformLocation(ctx)(el.value0)("uLightDir")();+        if (mfmvp instanceof Just && (mfc instanceof Just && (mlmvp instanceof Just && (mlc instanceof Just && mld instanceof Just)))) {+          var bp = createBuffer(ctx)();+          var bn = createBuffer(ctx)();+          return new Right({+            flat: {+              program: ef.value0,+              aPosition: fpos,+              uMVP: mfmvp.value0,+              uColor: mfc.value0+            },+            lambert: {+              program: el.value0,+              aPosition: lpos,+              aNormal: lnor,+              uMVP: mlmvp.value0,+              uColor: mlc.value0,+              uLightDir: mld.value0+            },+            bufferPos: bp,+            bufferNorm: bn+          });+        }+        ;+        return new Left("surface: uniform lookup failed");+      }+      ;+      if (ef instanceof Left) {+        return new Left("surface flat: " + ef.value0);+      }+      ;+      if (el instanceof Left) {+        return new Left("surface lambert: " + el.value0);+      }+      ;+      throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Surface (line 63, column 3 - line 86, column 56): " + [ef.constructor.name, el.constructor.name]);+    };+  };+  var drawTriMesh = function(ctx) {+    return function(v) {+      return function(mvp) {+        return function(m) {+          var c = parseHexColor(m.color);+          var transparent = m.alpha < 1;+          return function __do() {+            when2(transparent)(depthMask(ctx)(false))();+            (function() {+              if (m.shaded) {+                useProgram(ctx)(v.lambert.program)();+                uniformMatrix4fv(ctx)(v.lambert.uMVP)(mat4ToArrayColMajor(mvp))();+                uniform4f(ctx)(v.lambert.uColor)(c.r)(c.g)(c.b)(m.alpha)();+                uniform3f(ctx)(v.lambert.uLightDir)(0.5)(0.5)(1)();+                uploadAttribVec3(ctx)(v.bufferPos)(v.lambert.aPosition)(m.positions)();+                uploadAttribVec3(ctx)(v.bufferNorm)(v.lambert.aNormal)(m.normals)();+                return drawArrays(ctx)(glTRIANGLES)(0)(div2(length(m.positions))(3))();+              }+              ;+              useProgram(ctx)(v.flat.program)();+              uniformMatrix4fv(ctx)(v.flat.uMVP)(mat4ToArrayColMajor(mvp))();+              uniform4f(ctx)(v.flat.uColor)(c.r)(c.g)(c.b)(m.alpha)();+              uploadAttribVec3(ctx)(v.bufferPos)(v.flat.aPosition)(m.positions)();+              return drawArrays(ctx)(glTRIANGLES)(0)(div2(length(m.positions))(3))();+            })();+            return when2(transparent)(depthMask(ctx)(true))();+          };+        };+      };+    };+  };+  var concatRange = function(lo) {+    return function(hi) {+      return function(f) {+        var go = function($copy_i) {+          return function($copy_acc) {+            var $tco_var_i = $copy_i;+            var $tco_done = false;+            var $tco_result;+            function $tco_loop(i, acc) {+              if (i >= hi) {+                $tco_done = true;+                return acc;+              }+              ;+              if (otherwise) {+                $tco_var_i = i + 1 | 0;+                $copy_acc = append1(acc)(f(i));+                return;+              }+              ;+              throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Surface (line 214, column 5 - line 216, column 44): " + [i.constructor.name, acc.constructor.name]);+            }+            ;+            while (!$tco_done) {+              $tco_result = $tco_loop($tco_var_i, $copy_acc);+            }+            ;+            return $tco_result;+          };+        };+        return go(lo)([]);+      };+    };+  };+  var clamp_ = function(lo) {+    return function(hi) {+      return function(x) {+        var $48 = x < lo;+        if ($48) {+          return lo;+        }+        ;+        var $49 = x > hi;+        if ($49) {+          return hi;+        }+        ;+        return x;+      };+    };+  };+  var buildSurfaceVBO = function(s) {+    var zAt = function(i) {+      return function(j) {+        return fromMaybe(0)(bind1(index(s.grid)(i))(function(row) {+          return index(row)(j);+        }));+      };+    };+    var ny = length(s.grid);+    var yAt = function(i) {+      var $50 = ny <= 1;+      if ($50) {+        return s.yRange.min;+      }+      ;+      return s.yRange.min + (s.yRange.max - s.yRange.min) * toNumber(i) / toNumber(ny - 1 | 0);+    };+    var nx = (function() {+      var v = index(s.grid)(0);+      if (v instanceof Just) {+        return length(v.value0);+      }+      ;+      if (v instanceof Nothing) {+        return 0;+      }+      ;+      throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Surface (line 151, column 10 - line 153, column 20): " + [v.constructor.name]);+    })();+    var xAt = function(j) {+      var $53 = nx <= 1;+      if ($53) {+        return s.xRange.min;+      }+      ;+      return s.xRange.min + (s.xRange.max - s.xRange.min) * toNumber(j) / toNumber(nx - 1 | 0);+    };+    var dy = (function() {+      var $54 = ny <= 1;+      if ($54) {+        return 1;+      }+      ;+      return (s.yRange.max - s.yRange.min) / toNumber(ny - 1 | 0);+    })();+    var dx = (function() {+      var $55 = nx <= 1;+      if ($55) {+        return 1;+      }+      ;+      return (s.xRange.max - s.xRange.min) / toNumber(nx - 1 | 0);+    })();+    var clampJ = clamp_(0)(nx - 1 | 0);+    var clampI = clamp_(0)(ny - 1 | 0);+    var normalAt = function(i) {+      return function(j) {+        var dzdy = (zAt(clampI(i + 1 | 0))(j) - zAt(clampI(i - 1 | 0))(j)) / (2 * dy);+        var ny0 = -dzdy;+        var dzdx = (zAt(i)(clampJ(j + 1 | 0)) - zAt(i)(clampJ(j - 1 | 0))) / (2 * dx);+        var nx0 = -dzdx;+        var l = sqrtNum(nx0 * nx0 + ny0 * ny0 + 1 * 1);+        var $56 = l < 1e-12;+        if ($56) {+          return [0, 0, 1];+        }+        ;+        return [nx0 / l, ny0 / l, 1 / l];+      };+    };+    var cellPositions = function(i) {+      return function(j) {+        var v11 = [xAt(j + 1 | 0), yAt(i + 1 | 0), zAt(i + 1 | 0)(j + 1 | 0)];+        var v10 = [xAt(j), yAt(i + 1 | 0), zAt(i + 1 | 0)(j)];+        var v01 = [xAt(j + 1 | 0), yAt(i), zAt(i)(j + 1 | 0)];+        var v00 = [xAt(j), yAt(i), zAt(i)(j)];+        return append1(v00)(append1(v01)(append1(v10)(append1(v01)(append1(v11)(v10)))));+      };+    };+    var positions = concatRange(0)(ny - 1 | 0)(function(i) {+      return concatRange(0)(nx - 1 | 0)(function(j) {+        return cellPositions(i)(j);+      });+    });+    var cellNormals = function(i) {+      return function(j) {+        var n11 = normalAt(i + 1 | 0)(j + 1 | 0);+        var n10 = normalAt(i + 1 | 0)(j);+        var n01 = normalAt(i)(j + 1 | 0);+        var n00 = normalAt(i)(j);+        return append1(n00)(append1(n01)(append1(n10)(append1(n01)(append1(n11)(n10)))));+      };+    };+    var normals = concatRange(0)(ny - 1 | 0)(function(i) {+      return concatRange(0)(nx - 1 | 0)(function(j) {+        return cellNormals(i)(j);+      });+    });+    return {+      positions,+      normals+    };+  };+  var drawSurface = function(ctx) {+    return function(sp) {+      return function(mvp) {+        return function(alpha) {+          return function(v) {+            var v1 = buildSurfaceVBO(v);+            return drawTriMesh(ctx)(sp)(mvp)({+              positions: v1.positions,+              normals: v1.normals,+              color: v.color,+              alpha,+              shaded: v.shaded+            });+          };+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Bar/index.js+  var append2 = /* @__PURE__ */ append(semigroupArray);+  var when3 = /* @__PURE__ */ when(applicativeEffect);+  var sub3 = function(p) {+    return function(q) {+      var at = function(arr) {+        return function(i) {+          return fromMaybe(0)(index(arr)(i));+        };+      };+      return {+        x: at(p)(0) - at(q)(0),+        y: at(p)(1) - at(q)(1),+        z: at(p)(2) - at(q)(2)+      };+    };+  };+  var faceNormal = function(a) {+    return function(b) {+      return function(c) {+        var v = sub3(c)(a);+        var u = sub3(b)(a);+        var cz = u.x * v.y - u.y * v.x;+        var cy = u.z * v.x - u.x * v.z;+        var cx = u.y * v.z - u.z * v.y;+        var l = sqrt(cx * cx + cy * cy + cz * cz);+        var $20 = l < 1e-12;+        if ($20) {+          return [0, 0, 1];+        }+        ;+        return [cx / l, cy / l, cz / l];+      };+    };+  };+  var quadTris = function(v) {+    var n = faceNormal(v.value0)(v.value1.value0)(v.value1.value1.value0);+    return [{+      pos: append2(v.value0)(append2(v.value1.value0)(v.value1.value1.value0)),+      nor: append2(n)(append2(n)(n))+    }, {+      pos: append2(v.value0)(append2(v.value1.value1.value0)(v.value1.value1.value1)),+      nor: append2(n)(append2(n)(n))+    }];+  };+  var cuboidTris = function(zb) {+    return function(wx) {+      return function(wy) {+        return function(v) {+          var t3 = [v.value0 - wx, v.value1 + wy, v.value2];+          var t2 = [v.value0 + wx, v.value1 + wy, v.value2];+          var t1 = [v.value0 + wx, v.value1 - wy, v.value2];+          var t0 = [v.value0 - wx, v.value1 - wy, v.value2];+          var b3 = [v.value0 - wx, v.value1 + wy, zb];+          var b2 = [v.value0 + wx, v.value1 + wy, zb];+          var b1 = [v.value0 + wx, v.value1 - wy, zb];+          var b0 = [v.value0 - wx, v.value1 - wy, zb];+          var quads = [new Tuple(t0, new Tuple(t1, new Tuple(t2, t3))), new Tuple(b0, new Tuple(b1, new Tuple(t1, t0))), new Tuple(b1, new Tuple(b2, new Tuple(t2, t1))), new Tuple(b2, new Tuple(b3, new Tuple(t3, t2))), new Tuple(b3, new Tuple(b0, new Tuple(t0, t3)))];+          return concatMap(quadTris)(quads);+        };+      };+    };+  };+  var buildBars = function(d) {+    var tris = concatMap(cuboidTris(d.baseZ)(d.halfWx)(d.halfWy))(d.tops);+    return {+      positions: concatMap(function(v) {+        return v.pos;+      })(tris),+      normals: concatMap(function(v) {+        return v.nor;+      })(tris)+    };+  };+  var drawBar = function(ctx) {+    return function(surfP) {+      return function(lineP) {+        return function(mvp) {+          return function(d) {+            var errBarSegs = function(v2) {+              var lo = new Point3(v2.value0.value0, v2.value0.value1, v2.value0.value2 - v2.value1);+              var hi = new Point3(v2.value0.value0, v2.value0.value1, v2.value0.value2 + v2.value1);+              var capX = d.halfWx * 0.5;+              var hiL = new Point3(v2.value0.value0 - capX, v2.value0.value1, v2.value0.value2 + v2.value1);+              var hiR = new Point3(v2.value0.value0 + capX, v2.value0.value1, v2.value0.value2 + v2.value1);+              var loL = new Point3(v2.value0.value0 - capX, v2.value0.value1, v2.value0.value2 - v2.value1);+              var loR = new Point3(v2.value0.value0 + capX, v2.value0.value1, v2.value0.value2 - v2.value1);+              return [new Tuple(lo, hi), new Tuple(loL, loR), new Tuple(hiL, hiR)];+            };+            var v = buildBars(d);+            return function __do() {+              drawTriMesh(ctx)(surfP)(mvp)({+                positions: v.positions,+                normals: v.normals,+                color: d.color,+                alpha: d.alpha,+                shaded: true+              })();+              var errSegs = concatMap(errBarSegs)(zip(d.tops)(d.errs));+              return when3(length(errSegs) > 0)((function() {+                var pts = concatMap(function(v1) {+                  return [v1.value0, v1.value1];+                })(errSegs);+                var edges = mapWithIndex2(function(k) {+                  return function(v1) {+                    return {+                      i: 2 * k | 0,+                      j: (2 * k | 0) + 1 | 0+                    };+                  };+                })(errSegs);+                return drawWireframe(ctx)(lineP)(mvp)({+                  points: pts,+                  edges,+                  color: "#333333",+                  width: 1+                });+              })())();+            };+          };+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Camera/index.js+  var sqrtNum2 = sqrt;+  var pi2 = 3.141592653589793;+  var orbitToCamera = function(v) {+    var sTh = sin(v.theta);+    var sPhi = sin(v.phi);+    var cTh = cos(v.theta);+    var cPhi = cos(v.phi);+    var isZUp = v.upAxis.value2 > 0.5;+    var v1 = (function() {+      if (isZUp) {+        return {+          ex: v.target.value0 + v.distance * cPhi * cTh,+          ey: v.target.value1 + v.distance * cPhi * sTh,+          ez: v.target.value2 + v.distance * sPhi+        };+      }+      ;+      return {+        ex: v.target.value0 + v.distance * cPhi * sTh,+        ey: v.target.value1 + v.distance * sPhi,+        ez: v.target.value2 + v.distance * cPhi * cTh+      };+    })();+    return {+      eye: new Point3(v1.ex, v1.ey, v1.ez),+      target: v.target,+      up: v.upAxis+    };+  };+  var panOrbit = function(dx) {+    return function(dy) {+      return function(v) {+        var v1 = orbitToCamera(v);+        var fz = v1.target.value2 - v1.eye.value2;+        var fy = v1.target.value1 - v1.eye.value1;+        var fx = v1.target.value0 - v1.eye.value0;+        var fl = sqrtNum2(fx * fx + fy * fy + fz * fz);+        var fnx = (function() {+          var $44 = fl < 1e-12;+          if ($44) {+            return 0;+          }+          ;+          return fx / fl;+        })();+        var fny = (function() {+          var $45 = fl < 1e-12;+          if ($45) {+            return 0;+          }+          ;+          return fy / fl;+        })();+        var fnz = (function() {+          var $46 = fl < 1e-12;+          if ($46) {+            return 0;+          }+          ;+          return fz / fl;+        })();+        var sz0 = fnx * v1.up.value1 - fny * v1.up.value0;+        var sy0 = fnz * v1.up.value0 - fnx * v1.up.value2;+        var sx0 = fny * v1.up.value2 - fnz * v1.up.value1;+        var sl = sqrtNum2(sx0 * sx0 + sy0 * sy0 + sz0 * sz0);+        var sx = (function() {+          var $48 = sl < 1e-12;+          if ($48) {+            return 0;+          }+          ;+          return sx0 / sl;+        })();+        var sy = (function() {+          var $49 = sl < 1e-12;+          if ($49) {+            return 0;+          }+          ;+          return sy0 / sl;+        })();+        var tuz = sx * fny - sy * fnx;+        var sz = (function() {+          var $50 = sl < 1e-12;+          if ($50) {+            return 0;+          }+          ;+          return sz0 / sl;+        })();+        var tux = sy * fnz - sz * fny;+        var tx$prime = v1.target.value0 + dx * sx + dy * tux;+        var tuy = sz * fnx - sx * fnz;+        var ty$prime = v1.target.value1 + dx * sy + dy * tuy;+        var tz$prime = v1.target.value2 + dx * sz + dy * tuz;+        return {+          theta: v.theta,+          phi: v.phi,+          distance: v.distance,+          upAxis: v.upAxis,+          target: new Point3(tx$prime, ty$prime, tz$prime)+        };+      };+    };+  };+  var maxNum = function(a) {+    return function(b) {+      var $60 = a > b;+      if ($60) {+        return a;+      }+      ;+      return b;+    };+  };+  var zoomOrbit = function(factor) {+    return function(v) {+      return {+        target: v.target,+        theta: v.theta,+        phi: v.phi,+        upAxis: v.upAxis,+        distance: maxNum(1e-3)(v.distance * factor)+      };+    };+  };+  var defaultOrbitZUp = function(d) {+    return {+      target: new Point3(0, 0, 0),+      theta: pi2 / 4,+      phi: pi2 / 6,+      distance: d,+      upAxis: zUp+    };+  };+  var clamp_2 = function(lo) {+    return function(hi) {+      return function(x) {+        var $65 = x < lo;+        if ($65) {+          return lo;+        }+        ;+        var $66 = x > hi;+        if ($66) {+          return hi;+        }+        ;+        return x;+      };+    };+  };+  var rotateOrbit = function(dTheta) {+    return function(dPhi) {+      return function(v) {+        var halfPi = pi2 / 2;+        var phi$prime = clamp_2(-(halfPi - 1e-3))(halfPi - 1e-3)(v.phi + dPhi);+        return {+          target: v.target,+          distance: v.distance,+          upAxis: v.upAxis,+          theta: v.theta + dTheta,+          phi: phi$prime+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Panes/index.js+  var append3 = /* @__PURE__ */ append(semigroupArray);+  var map6 = /* @__PURE__ */ map(functorArray);+  var quadTris2 = function(q) {+    return append3(q.p0)(append3(q.p1)(append3(q.p2)(append3(q.p0)(append3(q.p2)(q.p3)))));+  };+  var paneFill = "#f0f0f0";+  var paneAlpha = 0.55;+  var gridColor = "#dddddd";+  var drawPanes = function(ctx) {+    return function(surfP) {+      return function(lineP) {+        return function(mvp) {+          return function(v) {+            return function(tk) {+              var seg = function(p) {+                return function(q) {+                  return new Tuple(p, q);+                };+              };+              var quads = concatMap(quadTris2)([{+                p0: [v.xMin, v.yMin, v.zMin],+                p1: [v.xMax, v.yMin, v.zMin],+                p2: [v.xMax, v.yMax, v.zMin],+                p3: [v.xMin, v.yMax, v.zMin]+              }, {+                p0: [v.xMin, v.yMax, v.zMin],+                p1: [v.xMax, v.yMax, v.zMin],+                p2: [v.xMax, v.yMax, v.zMax],+                p3: [v.xMin, v.yMax, v.zMax]+              }, {+                p0: [v.xMin, v.yMin, v.zMin],+                p1: [v.xMin, v.yMax, v.zMin],+                p2: [v.xMin, v.yMax, v.zMax],+                p3: [v.xMin, v.yMin, v.zMax]+              }]);+              return function __do() {+                drawTriMesh(ctx)(surfP)(mvp)({+                  positions: quads,+                  normals: [],+                  color: paneFill,+                  alpha: paneAlpha,+                  shaded: false+                })();+                var sideL = append3(map6(function(y) {+                  return seg(new Point3(v.xMin, y, v.zMin))(new Point3(v.xMin, y, v.zMax));+                })(tk.ys))(map6(function(z) {+                  return seg(new Point3(v.xMin, v.yMin, z))(new Point3(v.xMin, v.yMax, z));+                })(tk.zs));+                var floorL = append3(map6(function(x) {+                  return seg(new Point3(x, v.yMin, v.zMin))(new Point3(x, v.yMax, v.zMin));+                })(tk.xs))(map6(function(y) {+                  return seg(new Point3(v.xMin, y, v.zMin))(new Point3(v.xMax, y, v.zMin));+                })(tk.ys));+                var backL = append3(map6(function(x) {+                  return seg(new Point3(x, v.yMax, v.zMin))(new Point3(x, v.yMax, v.zMax));+                })(tk.xs))(map6(function(z) {+                  return seg(new Point3(v.xMin, v.yMax, z))(new Point3(v.xMax, v.yMax, z));+                })(tk.zs));+                var segs = append3(floorL)(append3(backL)(sideL));+                var edges = mapWithIndex2(function(k) {+                  return function(v1) {+                    return {+                      i: 2 * k | 0,+                      j: (2 * k | 0) + 1 | 0+                    };+                  };+                })(segs);+                var pts = concatMap(function(v1) {+                  return [v1.value0, v1.value1];+                })(segs);+                return drawWireframe(ctx)(lineP)(mvp)({+                  points: pts,+                  edges,+                  color: gridColor,+                  width: 1+                })();+              };+            };+          };+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Quiver/index.js+  var map7 = /* @__PURE__ */ map(functorArray);+  var maximum2 = /* @__PURE__ */ maximum(ordNumber)(foldableArray);+  var vecLen = function(v) {+    return sqrt(v.value0 * v.value0 + v.value1 * v.value1 + v.value2 * v.value2);+  };+  var absN = function(x) {+    var $21 = x < 0;+    if ($21) {+      return -x;+    }+    ;+    return x;+  };+  var arrowSegs = function(s) {+    return function(v) {+      var start = new Point3(v.value0.value0, v.value0.value1, v.value0.value2);+      var ez = v.value0.value2 + s * v.value1.value2;+      var ey = v.value0.value1 + s * v.value1.value1;+      var ex = v.value0.value0 + s * v.value1.value0;+      var tip = new Point3(ex, ey, ez);+      var adz = ez - v.value0.value2;+      var ady = ey - v.value0.value1;+      var adx = ex - v.value0.value0;+      var alen = sqrt(adx * adx + ady * ady + adz * adz);+      var $24 = alen < 1e-9;+      if ($24) {+        return [];+      }+      ;+      var uz = adz / alen;+      var uy = ady / alen;+      var ux = adx / alen;+      var ref = (function() {+        var $25 = absN(uz) < 0.9;+        if ($25) {+          return new Vec3(0, 0, 1);+        }+        ;+        return new Vec3(1, 0, 0);+      })();+      var c1z = ux * ref.value1 - uy * ref.value0;+      var c1y = uz * ref.value0 - ux * ref.value2;+      var c1x = uy * ref.value2 - uz * ref.value1;+      var c1l = sqrt(c1x * c1x + c1y * c1y + c1z * c1z);+      var p1x = c1x / c1l;+      var p1y = c1y / c1l;+      var p2z = ux * p1y - uy * p1x;+      var p1z = c1z / c1l;+      var p2x = uy * p1z - uz * p1y;+      var p2y = uz * p1x - ux * p1z;+      var ah = 0.22 * alen;+      var bx = ex - ux * ah;+      var by = ey - uy * ah;+      var bz = ez - uz * ah;+      var barb = function(qx) {+        return function(qy) {+          return function(qz) {+            return new Tuple(tip, new Point3(bx + qx * 0.45 * ah, by + qy * 0.45 * ah, bz + qz * 0.45 * ah));+          };+        };+      };+      return [new Tuple(start, tip), barb(p1x)(p1y)(p1z), barb(-p1x)(-p1y)(-p1z), barb(p2x)(p2y)(p2z), barb(-p2x)(-p2y)(-p2z)];+    };+  };+  var drawQuiver = function(ctx) {+    return function(lineP) {+      return function(mvp) {+        return function(d) {+          var lens = map7(vecLen)(d.vectors);+          var maxLen = fromMaybe(0)(maximum2(lens));+          var autoS = (function() {+            var $38 = maxLen <= 0 || d.spanMax <= 0;+            if ($38) {+              return 0;+            }+            ;+            return 0.35 * d.spanMax / maxLen;+          })();+          var s = autoS * d.vecScale;+          var segs = concatMap(arrowSegs(s))(zip(d.starts)(d.vectors));+          var edges = mapWithIndex2(function(k) {+            return function(v) {+              return {+                i: 2 * k | 0,+                j: (2 * k | 0) + 1 | 0+              };+            };+          })(segs);+          var pts = concatMap(function(v) {+            return [v.value0, v.value1];+          })(segs);+          return drawWireframe(ctx)(lineP)(mvp)({+            points: pts,+            edges,+            color: d.color,+            width: 1+          });+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Scatter/index.js+  var mkScatterProgram = function(ctx) {+    return function __do() {+      var ep = compileProgram(ctx)(pointVS)(pointFS)();+      if (ep instanceof Left) {+        return new Left("scatter: " + ep.value0);+      }+      ;+      if (ep instanceof Right) {+        var pos = getAttribLocation(ctx)(ep.value0)("aPosition")();+        var muvp = getUniformLocation(ctx)(ep.value0)("uMVP")();+        var mc = getUniformLocation(ctx)(ep.value0)("uColor")();+        var mps = getUniformLocation(ctx)(ep.value0)("uPointSize")();+        if (muvp instanceof Just && (mc instanceof Just && mps instanceof Just)) {+          var buf = createBuffer(ctx)();+          return new Right({+            program: ep.value0,+            aPosition: pos,+            uMVP: muvp.value0,+            uColor: mc.value0,+            uPointSize: mps.value0,+            buffer: buf+          });+        }+        ;+        return new Left("scatter: uniform lookup failed");+      }+      ;+      throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Scatter (line 39, column 3 - line 57, column 64): " + [ep.constructor.name]);+    };+  };+  var mkScatterColorProgram = function(ctx) {+    return function __do() {+      var ep = compileProgram(ctx)(pointColorVS)(pointColorFS)();+      if (ep instanceof Left) {+        return new Left("scatterColor: " + ep.value0);+      }+      ;+      if (ep instanceof Right) {+        var pos = getAttribLocation(ctx)(ep.value0)("aPosition")();+        var col = getAttribLocation(ctx)(ep.value0)("aColor")();+        var sz = getAttribLocation(ctx)(ep.value0)("aSize")();+        var muvp = getUniformLocation(ctx)(ep.value0)("uMVP")();+        var ma = getUniformLocation(ctx)(ep.value0)("uAlpha")();+        if (muvp instanceof Just && ma instanceof Just) {+          var bp = createBuffer(ctx)();+          var bc = createBuffer(ctx)();+          var bs = createBuffer(ctx)();+          return new Right({+            program: ep.value0,+            aPosition: pos,+            aColor: col,+            aSize: sz,+            uMVP: muvp.value0,+            uAlpha: ma.value0,+            bufPos: bp,+            bufCol: bc,+            bufSize: bs+          });+        }+        ;+        return new Left("scatterColor: uniform lookup failed");+      }+      ;+      throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Scatter (line 89, column 3 - line 105, column 66): " + [ep.constructor.name]);+    };+  };+  var drawScatterColored = function(ctx) {+    return function(v) {+      return function(mvp) {+        return function(d) {+          var rgbOf = function(hex) {+            var c = parseHexColor(hex);+            return [c.r, c.g, c.b];+          };+          return function __do() {+            useProgram(ctx)(v.program)();+            uniformMatrix4fv(ctx)(v.uMVP)(mat4ToArrayColMajor(mvp))();+            uniform1f(ctx)(v.uAlpha)(d.alpha)();+            uploadAttribVec3(ctx)(v.bufPos)(v.aPosition)(flattenPoints3(d.points))();+            uploadAttribVec3(ctx)(v.bufCol)(v.aColor)(concatMap(rgbOf)(d.colors))();+            uploadAttrib1(ctx)(v.bufSize)(v.aSize)(d.sizes)();+            return drawArrays(ctx)(glPOINTS)(0)(length(d.points))();+          };+        };+      };+    };+  };+  var drawScatter = function(ctx) {+    return function(v) {+      return function(mvp) {+        return function(v1) {+          return function __do() {+            useProgram(ctx)(v.program)();+            uniformMatrix4fv(ctx)(v.uMVP)(mat4ToArrayColMajor(mvp))();+            var c = parseHexColor(v1.color);+            uniform4f(ctx)(v.uColor)(c.r)(c.g)(c.b)(c.a * v1.alpha)();+            uniform1f(ctx)(v.uPointSize)(v1.size)();+            var flat = flattenPoints3(v1.points);+            uploadAttribVec3(ctx)(v.buffer)(v.aPosition)(flat)();+            return drawArrays(ctx)(glPOINTS)(0)(length(v1.points))();+          };+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Stem/index.js+  var drawStem = function(ctx) {+    return function(lineP) {+      return function(scatP) {+        return function(mvp) {+          return function(d) {+            var segPts = concatMap(function(v) {+              return [new Point3(v.value0, v.value1, d.baseZ), new Point3(v.value0, v.value1, v.value2)];+            })(d.points);+            var segEdges = mapWithIndex2(function(k) {+              return function(v) {+                return {+                  i: 2 * k | 0,+                  j: (2 * k | 0) + 1 | 0+                };+              };+            })(d.points);+            return function __do() {+              drawWireframe(ctx)(lineP)(mvp)({+                points: segPts,+                edges: segEdges,+                color: d.color,+                width: 1+              })();+              return drawScatter(ctx)(scatP)(mvp)({+                points: d.points,+                color: d.color,+                size: d.markerSize,+                alpha: d.alpha+              })();+            };+          };+        };+      };+    };+  };++  // output/Hgg.Plot.Backend.WebGL.Text/foreign.js+  function ensureOverlay(webglCanvas) {+    return function() {+      var overlay = webglCanvas.__textOverlay;+      if (!overlay) {+        overlay = document.createElement("canvas");+        overlay.style.position = "absolute";+        overlay.style.pointerEvents = "none";+        overlay.style.zIndex = "10";+        document.body.appendChild(overlay);+        webglCanvas.__textOverlay = overlay;+      }+      var rect = webglCanvas.getBoundingClientRect();+      overlay.width = webglCanvas.width;+      overlay.height = webglCanvas.height;+      overlay.style.left = window.scrollX + rect.left + "px";+      overlay.style.top = window.scrollY + rect.top + "px";+      overlay.style.width = rect.width + "px";+      overlay.style.height = rect.height + "px";+      return overlay.getContext("2d");+    };+  }+  function clearOverlayFFI(ctx) {+    return function() {+      ctx.clearRect(0, 0, ctx.canvas.width, ctx.canvas.height);+    };+  }+  function drawLabelsFFI(ctx) {+    return function(labels) {+      return function() {+        ctx.textAlign = "center";+        ctx.textBaseline = "middle";+        for (var i = 0; i < labels.length; i++) {+          var lb = labels[i];+          ctx.font = lb.size + "px -apple-system, 'Hiragino Sans', 'Yu Gothic UI', sans-serif";+          ctx.fillStyle = lb.color;+          ctx.fillText(lb.text, lb.x, lb.y);+        }+      };+    };+  }+  function drawLegendFFI(ctx) {+    return function(items) {+      return function() {+        var pad = 14, sw = 16, gap = 8, lh = 22;+        var w = ctx.canvas.width;+        var fontPx = 15;+        ctx.font = fontPx + "px -apple-system, 'Hiragino Sans', 'Yu Gothic UI', sans-serif";+        ctx.textBaseline = "middle";+        var maxLabel = 0;+        for (var i = 0; i < items.length; i++) {+          maxLabel = Math.max(maxLabel, ctx.measureText(items[i].label).width);+        }+        var boxW = sw + gap + maxLabel + pad * 2;+        var boxH = items.length * lh + pad * 2;+        var x0 = w - boxW - pad;+        var y0 = pad;+        ctx.fillStyle = "rgba(255,255,255,0.85)";+        ctx.fillRect(x0, y0, boxW, boxH);+        ctx.strokeStyle = "#cccccc";+        ctx.lineWidth = 1;+        ctx.strokeRect(x0 + 0.5, y0 + 0.5, boxW, boxH);+        ctx.textAlign = "left";+        for (var j = 0; j < items.length; j++) {+          var cy = y0 + pad + j * lh + lh / 2;+          ctx.fillStyle = items[j].color;+          ctx.fillRect(x0 + pad, cy - sw / 2, sw, sw);+          ctx.fillStyle = "#333333";+          ctx.fillText(items[j].label, x0 + pad + sw + gap, cy);+        }+      };+    };+  }+  function drawColorbarFFI(ctx) {+    return function(cb) {+      return function() {+        var w = ctx.canvas.width, h = ctx.canvas.height;+        var barW = 18, barH = Math.min(240, h * 0.4), pad = 16;+        var x0 = w - barW - pad - 56;+        var y0 = (h - barH) / 2;+        var stops = cb.stops;+        var n = stops.length;+        if (n === 0) return;+        var grad = ctx.createLinearGradient(0, y0 + barH, 0, y0);+        for (var i = 0; i < n; i++) {+          grad.addColorStop(n === 1 ? 0 : i / (n - 1), stops[i]);+        }+        ctx.fillStyle = grad;+        ctx.fillRect(x0, y0, barW, barH);+        ctx.strokeStyle = "#cccccc";+        ctx.lineWidth = 1;+        ctx.strokeRect(x0 + 0.5, y0 + 0.5, barW, barH);+        ctx.fillStyle = "#333333";+        ctx.font = "13px -apple-system, 'Hiragino Sans', 'Yu Gothic UI', sans-serif";+        ctx.textAlign = "left";+        ctx.textBaseline = "middle";+        var fmt = function(v) {+          return (Math.round(v * 100) / 100).toString();+        };+        ctx.fillText(fmt(cb.vMax), x0 + barW + 6, y0);+        ctx.fillText(fmt((cb.vMin + cb.vMax) / 2), x0 + barW + 6, y0 + barH / 2);+        ctx.fillText(fmt(cb.vMin), x0 + barW + 6, y0 + barH);+      };+    };+  }++  // output/Hgg.Plot.Backend.WebGL.Text/index.js+  var projectToScreen = function(mvp) {+    return function(w) {+      return function(h) {+        return function(v) {+          var el = function(i) {+            return fromMaybe(0)(index(mvp)(i));+          };+          var cy = el(1) * v.value0 + el(5) * v.value1 + el(9) * v.value2 + el(13);+          var cx = el(0) * v.value0 + el(4) * v.value1 + el(8) * v.value2 + el(12);+          var cw = el(3) * v.value0 + el(7) * v.value1 + el(11) * v.value2 + el(15);+          var $11 = cw <= 0;+          if ($11) {+            return Nothing.value;+          }+          ;+          var ndcY = cy / cw;+          var sy = (1 - (ndcY * 0.5 + 0.5)) * h;+          var ndcX = cx / cw;+          var sx = (ndcX * 0.5 + 0.5) * w;+          return new Just({+            x: sx,+            y: sy+          });+        };+      };+    };+  };+  var drawLegend = drawLegendFFI;+  var drawLabels = function(ov) {+    return function(mvp) {+      return function(w) {+        return function(h) {+          return function(labels) {+            var project = function(lb) {+              var v = projectToScreen(mvp)(w)(h)(lb.point);+              if (v instanceof Nothing) {+                return Nothing.value;+              }+              ;+              if (v instanceof Just) {+                return new Just({+                  x: v.value0.x,+                  y: v.value0.y,+                  text: lb.text,+                  color: lb.color,+                  size: lb.size+                });+              }+              ;+              throw new Error("Failed pattern match at Hgg.Plot.Backend.WebGL.Text (line 70, column 16 - line 72, column 87): " + [v.constructor.name]);+            };+            return drawLabelsFFI(ov)(mapMaybe(project)(labels));+          };+        };+      };+    };+  };+  var drawColorbar = drawColorbarFFI;+  var clearOverlay = clearOverlayFFI;++  // output/Hgg.Plot.Layout/index.js+  var foldl2 = /* @__PURE__ */ foldl(foldableArray);+  var max3 = /* @__PURE__ */ max(ordNumber);+  var min3 = /* @__PURE__ */ min(ordNumber);+  var map8 = /* @__PURE__ */ map(functorArray);+  var extendedBreaks = function(m) {+    return function(dmin0) {+      return function(dmax0) {+        var sq = function(x) {+          return x * x;+        };+        var qs = [1, 5, 2, 2.5, 4, 3];+        var qIdx = function(q) {+          var go = function($copy_i) {+            return function($copy_arr) {+              var $tco_var_i = $copy_i;+              var $tco_done = false;+              var $tco_result;+              function $tco_loop(i, arr) {+                var v = uncons(arr);+                if (v instanceof Just) {+                  var $426 = v.value0.head === q;+                  if ($426) {+                    $tco_done = true;+                    return i;+                  }+                  ;+                  $tco_var_i = i + 1 | 0;+                  $copy_arr = v.value0.tail;+                  return;+                }+                ;+                if (v instanceof Nothing) {+                  $tco_done = true;+                  return i;+                }+                ;+                throw new Error("Failed pattern match at Hgg.Plot.Layout (line 1116, column 20 - line 1118, column 23): " + [v.constructor.name]);+              }+              ;+              while (!$tco_done) {+                $tco_result = $tco_loop($tco_var_i, $copy_arr);+              }+              ;+              return $tco_result;+            };+          };+          return go(1)(qs);+        };+        var simplicityMax = function(q) {+          return function(j) {+            return (6 - toNumber(qIdx(q))) / (6 - 1) + 1 - toNumber(j);+          };+        };+        var mD = toNumber(m);+        var genSeq = function(lo) {+          return function(hi) {+            return function(st) {+              var $430 = st <= 0;+              if ($430) {+                return [lo];+              }+              ;+              var cnt = round2((hi - lo) / st);+              return map8(function(i) {+                return lo + toNumber(i) * st;+              })(range2(0)(cnt));+            };+          };+        };+        var fmod$prime = function(a) {+          return function(b) {+            return a - b * toNumber(floor2(a / b));+          };+        };+        var eps = 2220446049250313e-29 * 100;+        var simplicity = function(q) {+          return function(j) {+            return function(lmin) {+              return function(lmax) {+                return function(lstep) {+                  var mlt = fmod$prime(lmin)(lstep);+                  var v = (function() {+                    var $431 = (mlt < eps || lstep - mlt < eps) && (lmin <= 0 && lmax >= 0);+                    if ($431) {+                      return 1;+                    }+                    ;+                    return 0;+                  })();+                  return (6 - toNumber(qIdx(q))) / (6 - 1) + v - toNumber(j);+                };+              };+            };+          };+        };+        var dmin = (function() {+          var $432 = dmin0 > dmax0;+          if ($432) {+            return dmax0;+          }+          ;+          return dmin0;+        })();+        var dmax = (function() {+          var $433 = dmin0 > dmax0;+          if ($433) {+            return dmin0;+          }+          ;+          return dmax0;+        })();+        var densityMax = function(k) {+          var $434 = k >= m;+          if ($434) {+            return 2 - (toNumber(k) - 1) / (mD - 1);+          }+          ;+          return 1;+        };+        var densityF = function(k) {+          return function(lmin) {+            return function(lmax) {+              var rt = (mD - 1) / (max3(lmax)(dmax) - min3(dmin)(lmin));+              var r = (toNumber(k) - 1) / (lmax - lmin);+              return 2 - max3(r / rt)(rt / r);+            };+          };+        };+        var coverageMax = function(spn) {+          var rng = dmax - dmin;+          var $435 = spn > rng;+          if ($435) {+            var half = (spn - rng) / 2;+            return 1 - 0.5 * (sq(half) + sq(half)) / sq(0.1 * rng);+          }+          ;+          return 1;+        };+        var coverage = function(lmin) {+          return function(lmax) {+            var rng = dmax - dmin;+            return 1 - 0.5 * (sq(dmax - lmax) + sq(dmin - lmin)) / sq(0.1 * rng);+          };+        };+        var goStart = function(q) {+          return function(j) {+            return function(k) {+              return function(step) {+                return function(minStart) {+                  return function(maxStart) {+                    return function(b) {+                      var unit2 = step / toNumber(j);+                      var upd = function(acc) {+                        return function(start) {+                          var lmin = toNumber(start) * unit2;+                          var lmax = lmin + step * toNumber(k - 1 | 0);+                          var s = simplicity(q)(j)(lmin)(lmax)(step);+                          var d = densityF(k)(lmin)(lmax);+                          var c = coverage(lmin)(lmax);+                          var score = 0.25 * s + 0.2 * c + 0.5 * d + 0.05 * 1;+                          var $436 = score > acc.score;+                          if ($436) {+                            return {+                              lmin,+                              lmax,+                              lstep: step,+                              score+                            };+                          }+                          ;+                          return acc;+                        };+                      };+                      return foldl2(upd)(b)(range2(minStart)(maxStart));+                    };+                  };+                };+              };+            };+          };+        };+        var goZ = function($copy_q) {+          return function($copy_sm) {+            return function($copy_j) {+              return function($copy_k) {+                return function($copy_dm) {+                  return function($copy_fuel) {+                    return function($copy_z) {+                      return function($copy_b) {+                        var $tco_var_q = $copy_q;+                        var $tco_var_sm = $copy_sm;+                        var $tco_var_j = $copy_j;+                        var $tco_var_k = $copy_k;+                        var $tco_var_dm = $copy_dm;+                        var $tco_var_fuel = $copy_fuel;+                        var $tco_var_z = $copy_z;+                        var $tco_done1 = false;+                        var $tco_result;+                        function $tco_loop(q, sm, j, k, dm, fuel, z, b) {+                          var $437 = fuel <= 0;+                          if ($437) {+                            $tco_done1 = true;+                            return b;+                          }+                          ;+                          var step = toNumber(j) * q * pow(10)(toNumber(z));+                          var cm = coverageMax(step * toNumber(k - 1 | 0));+                          var $438 = 0.25 * sm + 0.2 * cm + 0.5 * dm + 0.05 < b.score;+                          if ($438) {+                            $tco_done1 = true;+                            return b;+                          }+                          ;+                          var minStart = (floor2(dmax / step) * j | 0) - ((k - 1 | 0) * j | 0) | 0;+                          var maxStart = ceil2(dmin / step) * j | 0;+                          var b$prime = (function() {+                            var $439 = minStart > maxStart;+                            if ($439) {+                              return b;+                            }+                            ;+                            return goStart(q)(j)(k)(step)(minStart)(maxStart)(b);+                          })();+                          $tco_var_q = q;+                          $tco_var_sm = sm;+                          $tco_var_j = j;+                          $tco_var_k = k;+                          $tco_var_dm = dm;+                          $tco_var_fuel = fuel - 1 | 0;+                          $tco_var_z = z + 1 | 0;+                          $copy_b = b$prime;+                          return;+                        }+                        ;+                        while (!$tco_done1) {+                          $tco_result = $tco_loop($tco_var_q, $tco_var_sm, $tco_var_j, $tco_var_k, $tco_var_dm, $tco_var_fuel, $tco_var_z, $copy_b);+                        }+                        ;+                        return $tco_result;+                      };+                    };+                  };+                };+              };+            };+          };+        };+        var goK = function($copy_q) {+          return function($copy_sm) {+            return function($copy_j) {+              return function($copy_k) {+                return function($copy_b) {+                  var $tco_var_q = $copy_q;+                  var $tco_var_sm = $copy_sm;+                  var $tco_var_j = $copy_j;+                  var $tco_var_k = $copy_k;+                  var $tco_done2 = false;+                  var $tco_result;+                  function $tco_loop(q, sm, j, k, b) {+                    var $440 = k > ((2 * m | 0) + 6 | 0);+                    if ($440) {+                      $tco_done2 = true;+                      return b;+                    }+                    ;+                    var dm = densityMax(k);+                    var $441 = 0.25 * sm + 0.2 + 0.5 * dm + 0.05 < b.score;+                    if ($441) {+                      $tco_done2 = true;+                      return b;+                    }+                    ;+                    var delta = (dmax - dmin) / toNumber(k + 1 | 0) / toNumber(j) / q;+                    var z0 = ceil2(log(delta) / log(10));+                    $tco_var_q = q;+                    $tco_var_sm = sm;+                    $tco_var_j = j;+                    $tco_var_k = k + 1 | 0;+                    $copy_b = goZ(q)(sm)(j)(k)(dm)(60)(z0)(b);+                    return;+                  }+                  ;+                  while (!$tco_done2) {+                    $tco_result = $tco_loop($tco_var_q, $tco_var_sm, $tco_var_j, $tco_var_k, $copy_b);+                  }+                  ;+                  return $tco_result;+                };+              };+            };+          };+        };+        var goQ = function($copy_arr) {+          return function($copy_j) {+            return function($copy_b) {+              var $tco_var_arr = $copy_arr;+              var $tco_var_j = $copy_j;+              var $tco_done3 = false;+              var $tco_result;+              function $tco_loop(arr, j, b) {+                var v = uncons(arr);+                if (v instanceof Nothing) {+                  $tco_done3 = true;+                  return {+                    b,+                    stop: false+                  };+                }+                ;+                if (v instanceof Just) {+                  var sm = simplicityMax(v.value0.head)(j);+                  var $443 = 0.25 * sm + 0.2 + 0.5 + 0.05 < b.score;+                  if ($443) {+                    $tco_done3 = true;+                    return {+                      b,+                      stop: true+                    };+                  }+                  ;+                  $tco_var_arr = v.value0.tail;+                  $tco_var_j = j;+                  $copy_b = goK(v.value0.head)(sm)(j)(2)(b);+                  return;+                }+                ;+                throw new Error("Failed pattern match at Hgg.Plot.Layout (line 1150, column 19 - line 1156, column 47): " + [v.constructor.name]);+              }+              ;+              while (!$tco_done3) {+                $tco_result = $tco_loop($tco_var_arr, $tco_var_j, $copy_b);+              }+              ;+              return $tco_result;+            };+          };+        };+        var goJ = function($copy_j) {+          return function($copy_b) {+            var $tco_var_j = $copy_j;+            var $tco_done4 = false;+            var $tco_result;+            function $tco_loop(j, b) {+              var $447 = j > 30;+              if ($447) {+                $tco_done4 = true;+                return b;+              }+              ;+              var res = goQ(qs)(j)(b);+              if (res.stop) {+                $tco_done4 = true;+                return res.b;+              }+              ;+              $tco_var_j = j + 1 | 0;+              $copy_b = res.b;+              return;+            }+            ;+            while (!$tco_done4) {+              $tco_result = $tco_loop($tco_var_j, $copy_b);+            }+            ;+            return $tco_result;+          };+        };+        var best = goJ(1)({+          lmin: 0,+          lmax: 0,+          lstep: 1,+          score: -2+        });+        var $449 = !(dmax - dmin >= eps);+        if ($449) {+          return [dmin];+        }+        ;+        var $450 = best.score <= -2;+        if ($450) {+          return [dmin, dmax];+        }+        ;+        return genSeq(best.lmin)(best.lmax)(best.lstep);+      };+    };+  };++  // output/Data.Argonaut.Core/foreign.js+  function stringify(j) {+    return JSON.stringify(j);+  }+  function _caseJson(isNull2, isBool, isNum, isStr, isArr, isObj, j) {+    if (j == null) return isNull2();+    else if (typeof j === "boolean") return isBool(j);+    else if (typeof j === "number") return isNum(j);+    else if (typeof j === "string") return isStr(j);+    else if (Object.prototype.toString.call(j) === "[object Array]")+      return isArr(j);+    else return isObj(j);+  }++  // output/Foreign.Object/foreign.js+  function _copyST(m) {+    return function() {+      var r = {};+      for (var k in m) {+        if (hasOwnProperty.call(m, k)) {+          r[k] = m[k];+        }+      }+      return r;+    };+  }+  var empty2 = {};+  function runST(f) {+    return f();+  }+  function _fmapObject(m0, f) {+    var m = {};+    for (var k in m0) {+      if (hasOwnProperty.call(m0, k)) {+        m[k] = f(m0[k]);+      }+    }+    return m;+  }+  function _mapWithKey(m0, f) {+    var m = {};+    for (var k in m0) {+      if (hasOwnProperty.call(m0, k)) {+        m[k] = f(k)(m0[k]);+      }+    }+    return m;+  }+  function _foldM(bind4) {+    return function(f) {+      return function(mz) {+        return function(m) {+          var acc = mz;+          function g(k2) {+            return function(z) {+              return f(z)(k2)(m[k2]);+            };+          }+          for (var k in m) {+            if (hasOwnProperty.call(m, k)) {+              acc = bind4(acc)(g(k));+            }+          }+          return acc;+        };+      };+    };+  }+  function _lookup(no, yes, k, m) {+    return k in m ? yes(m[k]) : no;+  }+  function toArrayWithKey(f) {+    return function(m) {+      var r = [];+      for (var k in m) {+        if (hasOwnProperty.call(m, k)) {+          r.push(f(k)(m[k]));+        }+      }+      return r;+    };+  }+  var keys = Object.keys || toArrayWithKey(function(k) {+    return function() {+      return k;+    };+  });++  // output/Data.FoldableWithIndex/index.js+  var foldr8 = /* @__PURE__ */ foldr(foldableArray);+  var mapWithIndex3 = /* @__PURE__ */ mapWithIndex(functorWithIndexArray);+  var foldl8 = /* @__PURE__ */ foldl(foldableArray);+  var foldrWithIndex = function(dict) {+    return dict.foldrWithIndex;+  };+  var foldMapWithIndexDefaultR = function(dictFoldableWithIndex) {+    var foldrWithIndex1 = foldrWithIndex(dictFoldableWithIndex);+    return function(dictMonoid) {+      var append5 = append(dictMonoid.Semigroup0());+      var mempty2 = mempty(dictMonoid);+      return function(f) {+        return foldrWithIndex1(function(i) {+          return function(x) {+            return function(acc) {+              return append5(f(i)(x))(acc);+            };+          };+        })(mempty2);+      };+    };+  };+  var foldableWithIndexArray = {+    foldrWithIndex: function(f) {+      return function(z) {+        var $291 = foldr8(function(v) {+          return function(y) {+            return f(v.value0)(v.value1)(y);+          };+        })(z);+        var $292 = mapWithIndex3(Tuple.create);+        return function($293) {+          return $291($292($293));+        };+      };+    },+    foldlWithIndex: function(f) {+      return function(z) {+        var $294 = foldl8(function(y) {+          return function(v) {+            return f(v.value0)(y)(v.value1);+          };+        })(z);+        var $295 = mapWithIndex3(Tuple.create);+        return function($296) {+          return $294($295($296));+        };+      };+    },+    foldMapWithIndex: function(dictMonoid) {+      return foldMapWithIndexDefaultR(foldableWithIndexArray)(dictMonoid);+    },+    Foldable0: function() {+      return foldableArray;+    }+  };++  // output/Data.TraversableWithIndex/index.js+  var traverseWithIndexDefault = function(dictTraversableWithIndex) {+    var sequence2 = sequence(dictTraversableWithIndex.Traversable2());+    var mapWithIndex4 = mapWithIndex(dictTraversableWithIndex.FunctorWithIndex0());+    return function(dictApplicative) {+      var sequence12 = sequence2(dictApplicative);+      return function(f) {+        var $174 = mapWithIndex4(f);+        return function($175) {+          return sequence12($174($175));+        };+      };+    };+  };+  var traverseWithIndex = function(dict) {+    return dict.traverseWithIndex;+  };+  var traversableWithIndexArray = {+    traverseWithIndex: function(dictApplicative) {+      return traverseWithIndexDefault(traversableWithIndexArray)(dictApplicative);+    },+    FunctorWithIndex0: function() {+      return functorWithIndexArray;+    },+    FoldableWithIndex1: function() {+      return foldableWithIndexArray;+    },+    Traversable2: function() {+      return traversableArray;+    }+  };++  // output/Foreign.Object.ST/foreign.js+  function poke2(k) {+    return function(v) {+      return function(m) {+        return function() {+          m[k] = v;+          return m;+        };+      };+    };+  }++  // output/Foreign.Object/index.js+  var foldr2 = /* @__PURE__ */ foldr(foldableArray);+  var identity6 = /* @__PURE__ */ identity(categoryFn);+  var values = /* @__PURE__ */ toArrayWithKey(function(v) {+    return function(v1) {+      return v1;+    };+  });+  var thawST = _copyST;+  var mutate = function(f) {+    return function(m) {+      return runST(function __do() {+        var s = thawST(m)();+        f(s)();+        return s;+      });+    };+  };+  var mapWithKey = function(f) {+    return function(m) {+      return _mapWithKey(m, f);+    };+  };+  var lookup = /* @__PURE__ */ (function() {+    return runFn4(_lookup)(Nothing.value)(Just.create);+  })();+  var insert = function(k) {+    return function(v) {+      return mutate(poke2(k)(v));+    };+  };+  var functorObject = {+    map: function(f) {+      return function(m) {+        return _fmapObject(m, f);+      };+    }+  };+  var functorWithIndexObject = {+    mapWithIndex: mapWithKey,+    Functor0: function() {+      return functorObject;+    }+  };+  var fold2 = /* @__PURE__ */ _foldM(applyFlipped);+  var foldMap2 = function(dictMonoid) {+    var append12 = append(dictMonoid.Semigroup0());+    var mempty2 = mempty(dictMonoid);+    return function(f) {+      return fold2(function(acc) {+        return function(k) {+          return function(v) {+            return append12(acc)(f(k)(v));+          };+        };+      })(mempty2);+    };+  };+  var foldableObject = {+    foldl: function(f) {+      return fold2(function(z) {+        return function(v) {+          return f(z);+        };+      });+    },+    foldr: function(f) {+      return function(z) {+        return function(m) {+          return foldr2(f)(z)(values(m));+        };+      };+    },+    foldMap: function(dictMonoid) {+      var foldMap12 = foldMap2(dictMonoid);+      return function(f) {+        return foldMap12($$const(f));+      };+    }+  };+  var foldableWithIndexObject = {+    foldlWithIndex: function(f) {+      return fold2(flip(f));+    },+    foldrWithIndex: function(f) {+      return function(z) {+        return function(m) {+          return foldr2(uncurry(f))(z)(toArrayWithKey(Tuple.create)(m));+        };+      };+    },+    foldMapWithIndex: function(dictMonoid) {+      return foldMap2(dictMonoid);+    },+    Foldable0: function() {+      return foldableObject;+    }+  };+  var traversableWithIndexObject = {+    traverseWithIndex: function(dictApplicative) {+      var Apply0 = dictApplicative.Apply0();+      var apply2 = apply(Apply0);+      var map12 = map(Apply0.Functor0());+      var pure1 = pure(dictApplicative);+      return function(f) {+        return function(ms) {+          return fold2(function(acc) {+            return function(k) {+              return function(v) {+                return apply2(map12(flip(insert(k)))(acc))(f(k)(v));+              };+            };+          })(pure1(empty2))(ms);+        };+      };+    },+    FunctorWithIndex0: function() {+      return functorWithIndexObject;+    },+    FoldableWithIndex1: function() {+      return foldableWithIndexObject;+    },+    Traversable2: function() {+      return traversableObject;+    }+  };+  var traversableObject = {+    traverse: function(dictApplicative) {+      var $96 = traverseWithIndex(traversableWithIndexObject)(dictApplicative);+      return function($97) {+        return $96($$const($97));+      };+    },+    sequence: function(dictApplicative) {+      return traverse(traversableObject)(dictApplicative)(identity6);+    },+    Functor0: function() {+      return functorObject;+    },+    Foldable1: function() {+      return foldableObject;+    }+  };++  // output/Data.Argonaut.Core/index.js+  var verbJsonType = function(def) {+    return function(f) {+      return function(g) {+        return g(def)(f);+      };+    };+  };+  var toJsonType = /* @__PURE__ */ (function() {+    return verbJsonType(Nothing.value)(Just.create);+  })();+  var isJsonType = /* @__PURE__ */ verbJsonType(false)(/* @__PURE__ */ $$const(true));+  var caseJsonString = function(d) {+    return function(f) {+      return function(j) {+        return _caseJson($$const(d), $$const(d), $$const(d), f, $$const(d), $$const(d), j);+      };+    };+  };+  var caseJsonObject = function(d) {+    return function(f) {+      return function(j) {+        return _caseJson($$const(d), $$const(d), $$const(d), $$const(d), $$const(d), f, j);+      };+    };+  };+  var toObject = /* @__PURE__ */ toJsonType(caseJsonObject);+  var caseJsonNumber = function(d) {+    return function(f) {+      return function(j) {+        return _caseJson($$const(d), $$const(d), f, $$const(d), $$const(d), $$const(d), j);+      };+    };+  };+  var caseJsonNull = function(d) {+    return function(f) {+      return function(j) {+        return _caseJson(f, $$const(d), $$const(d), $$const(d), $$const(d), $$const(d), j);+      };+    };+  };+  var isNull = /* @__PURE__ */ isJsonType(caseJsonNull);+  var caseJsonBoolean = function(d) {+    return function(f) {+      return function(j) {+        return _caseJson($$const(d), f, $$const(d), $$const(d), $$const(d), $$const(d), j);+      };+    };+  };+  var caseJsonArray = function(d) {+    return function(f) {+      return function(j) {+        return _caseJson($$const(d), $$const(d), $$const(d), $$const(d), f, $$const(d), j);+      };+    };+  };+  var toArray = /* @__PURE__ */ toJsonType(caseJsonArray);++  // output/Data.Argonaut.Decode.Error/index.js+  var show2 = /* @__PURE__ */ show(showString);+  var show1 = /* @__PURE__ */ show(showInt);+  var TypeMismatch = /* @__PURE__ */ (function() {+    function TypeMismatch2(value0) {+      this.value0 = value0;+    }+    ;+    TypeMismatch2.create = function(value0) {+      return new TypeMismatch2(value0);+    };+    return TypeMismatch2;+  })();+  var UnexpectedValue = /* @__PURE__ */ (function() {+    function UnexpectedValue2(value0) {+      this.value0 = value0;+    }+    ;+    UnexpectedValue2.create = function(value0) {+      return new UnexpectedValue2(value0);+    };+    return UnexpectedValue2;+  })();+  var AtIndex = /* @__PURE__ */ (function() {+    function AtIndex2(value0, value1) {+      this.value0 = value0;+      this.value1 = value1;+    }+    ;+    AtIndex2.create = function(value0) {+      return function(value1) {+        return new AtIndex2(value0, value1);+      };+    };+    return AtIndex2;+  })();+  var AtKey = /* @__PURE__ */ (function() {+    function AtKey2(value0, value1) {+      this.value0 = value0;+      this.value1 = value1;+    }+    ;+    AtKey2.create = function(value0) {+      return function(value1) {+        return new AtKey2(value0, value1);+      };+    };+    return AtKey2;+  })();+  var Named = /* @__PURE__ */ (function() {+    function Named2(value0, value1) {+      this.value0 = value0;+      this.value1 = value1;+    }+    ;+    Named2.create = function(value0) {+      return function(value1) {+        return new Named2(value0, value1);+      };+    };+    return Named2;+  })();+  var MissingValue = /* @__PURE__ */ (function() {+    function MissingValue2() {+    }+    ;+    MissingValue2.value = new MissingValue2();+    return MissingValue2;+  })();+  var showJsonDecodeError = {+    show: function(v) {+      if (v instanceof TypeMismatch) {+        return "(TypeMismatch " + (show2(v.value0) + ")");+      }+      ;+      if (v instanceof UnexpectedValue) {+        return "(UnexpectedValue " + (stringify(v.value0) + ")");+      }+      ;+      if (v instanceof AtIndex) {+        return "(AtIndex " + (show1(v.value0) + (" " + (show(showJsonDecodeError)(v.value1) + ")")));+      }+      ;+      if (v instanceof AtKey) {+        return "(AtKey " + (show2(v.value0) + (" " + (show(showJsonDecodeError)(v.value1) + ")")));+      }+      ;+      if (v instanceof Named) {+        return "(Named " + (show2(v.value0) + (" " + (show(showJsonDecodeError)(v.value1) + ")")));+      }+      ;+      if (v instanceof MissingValue) {+        return "MissingValue";+      }+      ;+      throw new Error("Failed pattern match at Data.Argonaut.Decode.Error (line 24, column 10 - line 30, column 35): " + [v.constructor.name]);+    }+  };++  // output/Data.Argonaut.Decode.Decoders/index.js+  var pure2 = /* @__PURE__ */ pure(applicativeEither);+  var map9 = /* @__PURE__ */ map(functorEither);+  var lmap2 = /* @__PURE__ */ lmap(bifunctorEither);+  var composeKleisliFlipped2 = /* @__PURE__ */ composeKleisliFlipped(bindEither);+  var traverse5 = /* @__PURE__ */ traverse(traversableObject)(applicativeEither);+  var traverseWithIndex2 = /* @__PURE__ */ traverseWithIndex(traversableWithIndexArray)(applicativeEither);+  var getFieldOptional$prime = function(decoder) {+    return function(obj) {+      return function(str) {+        var decode = function(json) {+          var $35 = isNull(json);+          if ($35) {+            return pure2(Nothing.value);+          }+          ;+          return map9(Just.create)(lmap2(AtKey.create(str))(decoder(json)));+        };+        return maybe(pure2(Nothing.value))(decode)(lookup(str)(obj));+      };+    };+  };+  var getField = function(decoder) {+    return function(obj) {+      return function(str) {+        return maybe(new Left(new AtKey(str, MissingValue.value)))((function() {+          var $48 = lmap2(AtKey.create(str));+          return function($49) {+            return $48(decoder($49));+          };+        })())(lookup(str)(obj));+      };+    };+  };+  var decodeString = /* @__PURE__ */ (function() {+    return caseJsonString(new Left(new TypeMismatch("String")))(Right.create);+  })();+  var decodeNumber = /* @__PURE__ */ (function() {+    return caseJsonNumber(new Left(new TypeMismatch("Number")))(Right.create);+  })();+  var decodeJObject = /* @__PURE__ */ (function() {+    var $50 = note(new TypeMismatch("Object"));+    return function($51) {+      return $50(toObject($51));+    };+  })();+  var decodeJArray = /* @__PURE__ */ (function() {+    var $52 = note(new TypeMismatch("Array"));+    return function($53) {+      return $52(toArray($53));+    };+  })();+  var decodeInt = /* @__PURE__ */ composeKleisliFlipped2(/* @__PURE__ */ (function() {+    var $84 = note(new TypeMismatch("Integer"));+    return function($85) {+      return $84(fromNumber($85));+    };+  })())(decodeNumber);+  var decodeForeignObject = function(decoder) {+    return composeKleisliFlipped2((function() {+      var $86 = lmap2(Named.create("ForeignObject"));+      var $87 = traverse5(decoder);+      return function($88) {+        return $86($87($88));+      };+    })())(decodeJObject);+  };+  var decodeBoolean = /* @__PURE__ */ (function() {+    return caseJsonBoolean(new Left(new TypeMismatch("Boolean")))(Right.create);+  })();+  var decodeArray = function(decoder) {+    return composeKleisliFlipped2((function() {+      var $89 = lmap2(Named.create("Array"));+      var $90 = traverseWithIndex2(function(i) {+        var $92 = lmap2(AtIndex.create(i));+        return function($93) {+          return $92(decoder($93));+        };+      });+      return function($91) {+        return $89($90($91));+      };+    })())(decodeJArray);+  };++  // output/Data.Argonaut.Decode.Class/index.js+  var decodeJsonString = {+    decodeJson: decodeString+  };+  var decodeJsonNumber = {+    decodeJson: decodeNumber+  };+  var decodeJsonJson = /* @__PURE__ */ (function() {+    return {+      decodeJson: Right.create+    };+  })();+  var decodeJsonInt = {+    decodeJson: decodeInt+  };+  var decodeJsonBoolean = {+    decodeJson: decodeBoolean+  };+  var decodeJson = function(dict) {+    return dict.decodeJson;+  };+  var decodeForeignObject2 = function(dictDecodeJson) {+    return {+      decodeJson: decodeForeignObject(decodeJson(dictDecodeJson))+    };+  };+  var decodeArray2 = function(dictDecodeJson) {+    return {+      decodeJson: decodeArray(decodeJson(dictDecodeJson))+    };+  };++  // output/Data.Argonaut.Decode.Combinators/index.js+  var getFieldOptional$prime2 = function(dictDecodeJson) {+    return getFieldOptional$prime(decodeJson(dictDecodeJson));+  };+  var getField2 = function(dictDecodeJson) {+    return getField(decodeJson(dictDecodeJson));+  };++  // output/Hgg.Plot.ThreeD.Codec/index.js+  var bind2 = /* @__PURE__ */ bind(bindEither);+  var decodeJson2 = /* @__PURE__ */ decodeJson(/* @__PURE__ */ decodeForeignObject2(decodeJsonJson));+  var getFieldOptional$prime3 = /* @__PURE__ */ getFieldOptional$prime2(decodeJsonJson);+  var pure3 = /* @__PURE__ */ pure(applicativeEither);+  var map10 = /* @__PURE__ */ map(functorEither);+  var decodeArray3 = /* @__PURE__ */ decodeArray2(decodeJsonNumber);+  var decodeJson1 = /* @__PURE__ */ decodeJson(decodeArray3);+  var getField3 = /* @__PURE__ */ getField2(decodeJsonString);+  var getField1 = /* @__PURE__ */ getField2(decodeJsonNumber);+  var decodeJson22 = /* @__PURE__ */ decodeJson(/* @__PURE__ */ decodeArray2(decodeJsonString));+  var decodeJson3 = /* @__PURE__ */ decodeJson(/* @__PURE__ */ decodeArray2(decodeJsonInt));+  var decodeJson4 = /* @__PURE__ */ decodeJson(decodeJsonString);+  var decodeJson5 = /* @__PURE__ */ decodeJson(/* @__PURE__ */ decodeArray2(decodeArray3));+  var decodeJson6 = /* @__PURE__ */ decodeJson(/* @__PURE__ */ decodeArray2(decodeJsonJson));+  var decodeJson7 = /* @__PURE__ */ decodeJson(decodeJsonNumber);+  var getField22 = /* @__PURE__ */ getField2(decodeJsonJson);+  var decodeJson8 = /* @__PURE__ */ decodeJson(/* @__PURE__ */ decodeArray2(decodeJsonBoolean));+  var getField32 = /* @__PURE__ */ getField2(decodeJsonInt);+  var traverse2 = /* @__PURE__ */ traverse(traversableArray)(applicativeEither);+  var decodeJson9 = /* @__PURE__ */ decodeJson(decodeJsonBoolean);+  var decodeJson10 = /* @__PURE__ */ decodeJson(decodeJsonInt);+  var M3Scatter = /* @__PURE__ */ (function() {+    function M3Scatter2() {+    }+    ;+    M3Scatter2.value = new M3Scatter2();+    return M3Scatter2;+  })();+  var M3Line = /* @__PURE__ */ (function() {+    function M3Line2() {+    }+    ;+    M3Line2.value = new M3Line2();+    return M3Line2;+  })();+  var M3Wireframe = /* @__PURE__ */ (function() {+    function M3Wireframe2() {+    }+    ;+    M3Wireframe2.value = new M3Wireframe2();+    return M3Wireframe2;+  })();+  var M3Surface = /* @__PURE__ */ (function() {+    function M3Surface2() {+    }+    ;+    M3Surface2.value = new M3Surface2();+    return M3Surface2;+  })();+  var M3Bar = /* @__PURE__ */ (function() {+    function M3Bar2() {+    }+    ;+    M3Bar2.value = new M3Bar2();+    return M3Bar2;+  })();+  var M3Text = /* @__PURE__ */ (function() {+    function M3Text2() {+    }+    ;+    M3Text2.value = new M3Text2();+    return M3Text2;+  })();+  var M3Stem = /* @__PURE__ */ (function() {+    function M3Stem2() {+    }+    ;+    M3Stem2.value = new M3Stem2();+    return M3Stem2;+  })();+  var M3Quiver = /* @__PURE__ */ (function() {+    function M3Quiver2() {+    }+    ;+    M3Quiver2.value = new M3Quiver2();+    return M3Quiver2;+  })();+  var M3Trisurf = /* @__PURE__ */ (function() {+    function M3Trisurf2() {+    }+    ;+    M3Trisurf2.value = new M3Trisurf2();+    return M3Trisurf2;+  })();+  var optionalAt = function(dec) {+    return function(j) {+      return function(key) {+        return bind2(decodeJson2(j))(function(obj) {+          return bind2(getFieldOptional$prime3(obj)(key))(function(mv) {+            if (mv instanceof Nothing) {+              return pure3(Nothing.value);+            }+            ;+            if (mv instanceof Just) {+              var $45 = isNull(mv.value0);+              if ($45) {+                return pure3(Nothing.value);+              }+              ;+              return map10(Just.create)(dec(mv.value0));+            }+            ;+            throw new Error("Failed pattern match at Hgg.Plot.ThreeD.Codec (line 189, column 3 - line 193, column 37): " + [mv.constructor.name]);+          });+        });+      };+    };+  };+  var eqMark3DKind = {+    eq: function(x) {+      return function(y) {+        if (x instanceof M3Scatter && y instanceof M3Scatter) {+          return true;+        }+        ;+        if (x instanceof M3Line && y instanceof M3Line) {+          return true;+        }+        ;+        if (x instanceof M3Wireframe && y instanceof M3Wireframe) {+          return true;+        }+        ;+        if (x instanceof M3Surface && y instanceof M3Surface) {+          return true;+        }+        ;+        if (x instanceof M3Bar && y instanceof M3Bar) {+          return true;+        }+        ;+        if (x instanceof M3Text && y instanceof M3Text) {+          return true;+        }+        ;+        if (x instanceof M3Stem && y instanceof M3Stem) {+          return true;+        }+        ;+        if (x instanceof M3Quiver && y instanceof M3Quiver) {+          return true;+        }+        ;+        if (x instanceof M3Trisurf && y instanceof M3Trisurf) {+          return true;+        }+        ;+        return false;+      };+    }+  };+  var decodeVec3 = function(j) {+    return bind2(decodeJson1(j))(function(v) {+      if (v.length === 3) {+        return pure3(new Vec3(v[0], v[1], v[2]));+      }+      ;+      return new Left(new TypeMismatch("Vec3 expects [x, y, z]"));+    });+  };+  var decodeProjection3D = function(j) {+    return bind2(decodeJson2(j))(function(obj) {+      return bind2(getField3(obj)("tag"))(function(v) {+        if (v === "Perspective") {+          return bind2(getField1(obj)("perspFov"))(function(fov) {+            return bind2(getField1(obj)("perspAspect"))(function(aspect) {+              return bind2(getField1(obj)("perspNear"))(function(near) {+                return bind2(getField1(obj)("perspFar"))(function(far) {+                  return pure3(new Perspective({+                    fov,+                    aspect,+                    near,+                    far+                  }));+                });+              });+            });+          });+        }+        ;+        if (v === "Orthographic") {+          return bind2(getField1(obj)("orthoXHalf"))(function(xHalf) {+            return bind2(getField1(obj)("orthoYHalf"))(function(yHalf) {+              return bind2(getField1(obj)("orthoNear"))(function(near) {+                return bind2(getField1(obj)("orthoFar"))(function(far) {+                  return pure3(new Orthographic({+                    xHalf,+                    yHalf,+                    near,+                    far+                  }));+                });+              });+            });+          });+        }+        ;+        return new Left(new TypeMismatch("unknown Projection3D tag: " + v));+      });+    });+  };+  var decodePoint3 = function(j) {+    return bind2(decodeJson1(j))(function(v) {+      if (v.length === 3) {+        return pure3(new Point3(v[0], v[1], v[2]));+      }+      ;+      return new Left(new TypeMismatch("Point3 expects [x, y, z]"));+    });+  };+  var decodePairSS = function(j) {+    return bind2(decodeJson22(j))(function(v) {+      if (v.length === 2) {+        return pure3(new Tuple(v[0], v[1]));+      }+      ;+      return new Left(new TypeMismatch("(String, String) expects [a, b]"));+    });+  };+  var decodePairNN = function(j) {+    return bind2(decodeJson1(j))(function(v) {+      if (v.length === 2) {+        return pure3(new Tuple(v[0], v[1]));+      }+      ;+      return new Left(new TypeMismatch("(Number, Number) expects [a, b]"));+    });+  };+  var decodePairII = function(j) {+    return bind2(decodeJson3(j))(function(v) {+      if (v.length === 2) {+        return pure3(new Tuple(v[0], v[1]));+      }+      ;+      return new Left(new TypeMismatch("Tuple expects [a, b]"));+    });+  };+  var decodeMark3DKind = function(j) {+    return bind2(decodeJson4(j))(function(v) {+      if (v === "M3Scatter") {+        return new Right(M3Scatter.value);+      }+      ;+      if (v === "M3Line") {+        return new Right(M3Line.value);+      }+      ;+      if (v === "M3Wireframe") {+        return new Right(M3Wireframe.value);+      }+      ;+      if (v === "M3Surface") {+        return new Right(M3Surface.value);+      }+      ;+      if (v === "M3Bar") {+        return new Right(M3Bar.value);+      }+      ;+      if (v === "M3Text") {+        return new Right(M3Text.value);+      }+      ;+      if (v === "M3Stem") {+        return new Right(M3Stem.value);+      }+      ;+      if (v === "M3Quiver") {+        return new Right(M3Quiver.value);+      }+      ;+      if (v === "M3Trisurf") {+        return new Right(M3Trisurf.value);+      }+      ;+      return new Left(new TypeMismatch("unknown Mark3DKind: " + v));+    });+  };+  var decodeGrid = function(j) {+    return decodeJson5(j);+  };+  var decodeFace3 = function(j) {+    return bind2(decodeJson3(j))(function(v) {+      if (v.length === 3) {+        return pure3({+          a: v[0],+          b: v[1],+          c: v[2]+        });+      }+      ;+      return new Left(new TypeMismatch("Face3 expects [a, b, c]"));+    });+  };+  var decodeColorbar3 = function(j) {+    return bind2(decodeJson6(j))(function(v) {+      if (v.length === 3) {+        return bind2(decodeJson22(v[0]))(function(stops) {+          return bind2(decodeJson7(v[1]))(function(vMin) {+            return bind2(decodeJson7(v[2]))(function(vMax) {+              return pure3({+                stops,+                vMin,+                vMax+              });+            });+          });+        });+      }+      ;+      return new Left(new TypeMismatch("Colorbar3 expects [stops, min, max]"));+    });+  };+  var decodeCamera3D = function(j) {+    return bind2(decodeJson2(j))(function(obj) {+      return bind2(bind2(getField22(obj)("cameraEye"))(decodePoint3))(function(eye) {+        return bind2(bind2(getField22(obj)("cameraTarget"))(decodePoint3))(function(tgt) {+          return bind2(getField22(obj)("cameraUp"))(function(upJ) {+            return bind2(decodeVec3(upJ))(function(up) {+              return pure3({+                eye,+                target: tgt,+                up+              });+            });+          });+        });+      });+    });+  };+  var decodeBool3 = function(j) {+    return bind2(decodeJson8(j))(function(v) {+      if (v.length === 3) {+        return pure3({+          x: v[0],+          y: v[1],+          z: v[2]+        });+      }+      ;+      return new Left(new TypeMismatch("(Bool,Bool,Bool) expects [x, y, z]"));+    });+  };+  var decodeAxes3D = function(j) {+    return bind2(decodeJson2(j))(function(obj) {+      return bind2(getField1(obj)("axesXMin"))(function(xMin) {+        return bind2(getField1(obj)("axesXMax"))(function(xMax) {+          return bind2(getField1(obj)("axesYMin"))(function(yMin) {+            return bind2(getField1(obj)("axesYMax"))(function(yMax) {+              return bind2(getField1(obj)("axesZMin"))(function(zMin) {+                return bind2(getField1(obj)("axesZMax"))(function(zMax) {+                  return bind2(getField32(obj)("axesNTicks"))(function(nTicks) {+                    return pure3({+                      xMin,+                      xMax,+                      yMin,+                      yMax,+                      zMin,+                      zMax,+                      nTicks+                    });+                  });+                });+              });+            });+          });+        });+      });+    });+  };+  var decodeArrayOf = function(dec) {+    return function(j) {+      return bind2(decodeJson6(j))(function(v) {+        return traverse2(dec)(v);+      });+    };+  };+  var decodeLayer3D = function(j) {+    return bind2(optionalAt(decodeMark3DKind)(j)("lyr3Kind"))(function(kind) {+      return bind2(optionalAt(decodeArrayOf(decodePoint3))(j)("lyr3Points"))(function(points) {+        return bind2(optionalAt(decodeJson4)(j)("lyr3Color"))(function(color) {+          return bind2(optionalAt(decodeJson7)(j)("lyr3Size"))(function(size3) {+            return bind2(optionalAt(decodeJson7)(j)("lyr3Alpha"))(function(alpha) {+              return bind2(optionalAt(decodeJson7)(j)("lyr3Width"))(function(width) {+                return bind2(optionalAt(decodeArrayOf(decodePairII))(j)("lyr3Edges"))(function(edges) {+                  return bind2(optionalAt(decodeGrid)(j)("lyr3Grid"))(function(grid) {+                    return bind2(optionalAt(decodePairNN)(j)("lyr3XRange"))(function(xRange) {+                      return bind2(optionalAt(decodePairNN)(j)("lyr3YRange"))(function(yRange) {+                        return bind2(optionalAt(decodeJson9)(j)("lyr3Shaded"))(function(shaded) {+                          return bind2(optionalAt(decodeJson9)(j)("lyr3SurfaceWire"))(function(surfaceWire) {+                            return bind2(optionalAt(decodeJson4)(j)("lyr3EdgeColor"))(function(edgeColor) {+                              return bind2(optionalAt(decodeArrayOf(decodeJson4))(j)("lyr3PtColors"))(function(ptColors) {+                                return bind2(optionalAt(decodeArrayOf(decodePairSS))(j)("lyr3Legend"))(function(legend) {+                                  return bind2(optionalAt(decodeColorbar3)(j)("lyr3Colorbar"))(function(colorbar) {+                                    return bind2(optionalAt(decodeArrayOf(decodeJson7))(j)("lyr3PtSizes"))(function(ptSizes) {+                                      return bind2(optionalAt(decodeArrayOf(decodeJson7))(j)("lyr3PtErrs"))(function(ptErrs) {+                                        return bind2(optionalAt(decodeArrayOf(decodeJson4))(j)("lyr3Labels"))(function(labels) {+                                          return bind2(optionalAt(decodeArrayOf(decodeVec3))(j)("lyr3Vectors"))(function(vectors) {+                                            return bind2(optionalAt(decodeJson7)(j)("lyr3VecScale"))(function(vecScale) {+                                              return bind2(optionalAt(decodeJson7)(j)("lyr3StemBaseZ"))(function(stemBaseZ) {+                                                return bind2(optionalAt(decodeJson7)(j)("lyr3BarBaseZ"))(function(barBaseZ) {+                                                  return bind2(optionalAt(decodeJson4)(j)("lyr3BarStyle"))(function(barStyle) {+                                                    return bind2(optionalAt(decodeJson7)(j)("lyr3BarWidth"))(function(barWidth) {+                                                      return bind2(optionalAt(decodeArrayOf(decodeFace3))(j)("lyr3Faces"))(function(faces) {+                                                        return pure3({+                                                          kind,+                                                          points,+                                                          color,+                                                          size: size3,+                                                          alpha,+                                                          width,+                                                          edges,+                                                          grid,+                                                          xRange,+                                                          yRange,+                                                          shaded,+                                                          surfaceWire,+                                                          edgeColor,+                                                          ptColors,+                                                          legend,+                                                          colorbar,+                                                          ptSizes,+                                                          ptErrs,+                                                          labels,+                                                          vectors,+                                                          vecScale,+                                                          stemBaseZ,+                                                          barBaseZ,+                                                          barStyle,+                                                          barWidth,+                                                          faces+                                                        });+                                                      });+                                                    });+                                                  });+                                                });+                                              });+                                            });+                                          });+                                        });+                                      });+                                    });+                                  });+                                });+                              });+                            });+                          });+                        });+                      });+                    });+                  });+                });+              });+            });+          });+        });+      });+    });+  };+  var decodeVisualSpec3D = function(j) {+    return bind2(decodeJson2(j))(function(obj) {+      return bind2(getField22(obj)("vs3Layers"))(function(layersJ) {+        return bind2(decodeArrayOf(decodeLayer3D)(layersJ))(function(layers) {+          return bind2(optionalAt(decodeJson4)(j)("vs3Title"))(function(title) {+            return bind2(optionalAt(decodeCamera3D)(j)("vs3Camera"))(function(camera) {+              return bind2(optionalAt(decodeProjection3D)(j)("vs3Proj"))(function(proj) {+                return bind2(optionalAt(decodeAxes3D)(j)("vs3Axes"))(function(axes) {+                  return bind2(optionalAt(decodeJson10)(j)("vs3Width"))(function(width) {+                    return bind2(optionalAt(decodeJson10)(j)("vs3Height"))(function(height) {+                      return bind2(optionalAt(decodeJson7)(j)("vs3XAspect"))(function(xAspect) {+                        return bind2(optionalAt(decodeJson7)(j)("vs3YAspect"))(function(yAspect) {+                          return bind2(optionalAt(decodeJson7)(j)("vs3ZAspect"))(function(zAspect) {+                            return bind2(optionalAt(decodeBool3)(j)("vs3Log"))(function(logScale) {+                              return pure3({+                                layers,+                                title,+                                camera,+                                proj,+                                axes,+                                width,+                                height,+                                xAspect,+                                yAspect,+                                zAspect,+                                logScale+                              });+                            });+                          });+                        });+                      });+                    });+                  });+                });+              });+            });+          });+        });+      });+    });+  };++  // output/Effect.Class/index.js+  var monadEffectEffect = {+    liftEffect: /* @__PURE__ */ identity(categoryFn),+    Monad0: function() {+      return monadEffect;+    }+  };+  var liftEffect = function(dict) {+    return dict.liftEffect;+  };++  // output/Effect.Console/foreign.js+  var log2 = function(s) {+    return function() {+      console.log(s);+    };+  };++  // output/Effect.Class.Console/index.js+  var log3 = function(dictMonadEffect) {+    var $67 = liftEffect(dictMonadEffect);+    return function($68) {+      return $67(log2($68));+    };+  };++  // output/Examples.WebGLPlot/index.js+  var map11 = /* @__PURE__ */ map(functorArray);+  var bind3 = /* @__PURE__ */ bind(bindMaybe);+  var append4 = /* @__PURE__ */ append(semigroupArray);+  var map1 = /* @__PURE__ */ map(functorMaybe);+  var show3 = /* @__PURE__ */ show(showInt);+  var show12 = /* @__PURE__ */ show(showNumber);+  var log4 = /* @__PURE__ */ log3(monadEffectEffect);+  var pure4 = /* @__PURE__ */ pure(applicativeEffect);+  var div1 = /* @__PURE__ */ div(euclideanRingInt);+  var minimum2 = /* @__PURE__ */ minimum(ordNumber)(foldableArray);+  var maximum3 = /* @__PURE__ */ maximum(ordNumber)(foldableArray);+  var bindFlipped2 = /* @__PURE__ */ bindFlipped(bindMaybe);+  var for_2 = /* @__PURE__ */ for_(applicativeEffect)(foldableArray);+  var when4 = /* @__PURE__ */ when(applicativeEffect);+  var eq13 = /* @__PURE__ */ eq(/* @__PURE__ */ eqMaybe(eqMark3DKind));+  var show22 = /* @__PURE__ */ show(showJsonDecodeError);+  var unsafeIndex2 = index;+  var truncate = truncateFFI;+  var toWireframe = function(l) {+    return {+      points: fromMaybe([])(l.points),+      edges: map11(function(v) {+        return {+          i: v.value0,+          j: v.value1+        };+      })(fromMaybe([])(l.edges)),+      color: fromMaybe("#2ca02c")(l.color),+      width: fromMaybe(1)(l.width)+    };+  };+  var toTrisurfEdges = function(l) {+    return {+      points: fromMaybe([])(l.points),+      edges: concatMap(function(f) {+        return [{+          i: f.a,+          j: f.b+        }, {+          i: f.b,+          j: f.c+        }, {+          i: f.c,+          j: f.a+        }];+      })(fromMaybe([])(l.faces)),+      color: "#2c3e50",+      width: 1+    };+  };+  var toTextLabels = function(l) {+    var sz = fromMaybe(12)(l.size);+    var pts = fromMaybe([])(l.points);+    var labels = fromMaybe([])(l.labels);+    var col = fromMaybe("#333333")(l.color);+    return map11(function(v) {+      return {+        point: v.value0,+        text: v.value1,+        color: col,+        size: sz+      };+    })(zip(pts)(labels));+  };+  var toSurfaceWire = function(l) {+    var grid = fromMaybe([])(l.grid);+    var nx = (function() {+      var v2 = index(grid)(0);+      if (v2 instanceof Just) {+        return length(v2.value0);+      }+      ;+      if (v2 instanceof Nothing) {+        return 0;+      }+      ;+      throw new Error("Failed pattern match at Examples.WebGLPlot (line 385, column 12 - line 387, column 27): " + [v2.constructor.name]);+    })();+    var ny = length(grid);+    var v = fromMaybe(new Tuple(-1, 1))(l.xRange);+    var v1 = fromMaybe(new Tuple(-1, 1))(l.yRange);+    var zAt = function(i) {+      return function(j) {+        return fromMaybe(0)(bind3(index(grid)(i))(function(row) {+          return index(row)(j);+        }));+      };+    };+    var yAt = function(i) {+      var $94 = ny <= 1;+      if ($94) {+        return v1.value0;+      }+      ;+      return v1.value0 + (v1.value1 - v1.value0) * toNumber(i) / toNumber(ny - 1 | 0);+    };+    var xAt = function(j) {+      var $95 = nx <= 1;+      if ($95) {+        return v.value0;+      }+      ;+      return v.value0 + (v.value1 - v.value0) * toNumber(j) / toNumber(nx - 1 | 0);+    };+    var upto = function(lo) {+      return function(hi) {+        var $96 = hi < lo;+        if ($96) {+          return [];+        }+        ;+        return range2(lo)(hi);+      };+    };+    var pts = concatMap(function(i) {+      return map11(function(j) {+        return new Point3(xAt(j), yAt(i), zAt(i)(j));+      })(upto(0)(nx - 1 | 0));+    })(upto(0)(ny - 1 | 0));+    var idx = function(i) {+      return function(j) {+        return (i * nx | 0) + j | 0;+      };+    };+    var vEdges = concatMap(function(i) {+      return map11(function(j) {+        return {+          i: idx(i)(j),+          j: idx(i + 1 | 0)(j)+        };+      })(upto(0)(nx - 1 | 0));+    })(upto(0)(ny - 2 | 0));+    var hEdges = concatMap(function(i) {+      return map11(function(j) {+        return {+          i: idx(i)(j),+          j: idx(i)(j + 1 | 0)+        };+      })(upto(0)(nx - 2 | 0));+    })(upto(0)(ny - 1 | 0));+    return {+      points: pts,+      edges: append4(hEdges)(vEdges),+      color: fromMaybe("#5b9bd5")(l.color),+      width: 1+    };+  };+  var toSurface = function(l) {+    return {+      grid: fromMaybe([])(l.grid),+      xRange: (function() {+        var v = fromMaybe(new Tuple(-1, 1))(l.xRange);+        return {+          min: v.value0,+          max: v.value1+        };+      })(),+      yRange: (function() {+        var v = fromMaybe(new Tuple(-1, 1))(l.yRange);+        return {+          min: v.value0,+          max: v.value1+        };+      })(),+      color: fromMaybe("#5b9bd5")(l.color),+      edgeColor: fromMaybe("#3a73a6")(l.edgeColor),+      shaded: fromMaybe(true)(l.shaded)+    };+  };+  var toStem = function(l) {+    return {+      points: fromMaybe([])(l.points),+      baseZ: fromMaybe(0)(l.stemBaseZ),+      color: fromMaybe("#1f77b4")(l.color),+      markerSize: fromMaybe(5)(l.size),+      alpha: fromMaybe(0.95)(l.alpha)+    };+  };+  var toScatterColored = function(l) {+    return function(cols) {+      var sz = fromMaybe(5)(l.size);+      var pts = fromMaybe([])(l.points);+      var sizes = (function() {+        if (l.ptSizes instanceof Just) {+          return l.ptSizes.value0;+        }+        ;+        if (l.ptSizes instanceof Nothing) {+          return replicate(length(pts))(sz);+        }+        ;+        throw new Error("Failed pattern match at Examples.WebGLPlot (line 250, column 15 - line 252, column 45): " + [l.ptSizes.constructor.name]);+      })();+      return {+        points: pts,+        colors: cols,+        sizes,+        alpha: fromMaybe(0.85)(l.alpha)+      };+    };+  };+  var toScatter = function(l) {+    return {+      points: fromMaybe([])(l.points),+      color: fromMaybe("#1f77b4")(l.color),+      size: fromMaybe(5)(l.size),+      alpha: fromMaybe(0.85)(l.alpha)+    };+  };+  var toQuiver = function(v) {+    return function(l) {+      var maxNum2 = function(x) {+        return function(y) {+          var $111 = x >= y;+          if ($111) {+            return x;+          }+          ;+          return y;+        };+      };+      return {+        starts: fromMaybe([])(l.points),+        vectors: fromMaybe([])(l.vectors),+        vecScale: fromMaybe(1)(l.vecScale),+        color: fromMaybe("#1f77b4")(l.color),+        spanMax: maxNum2(v.xMax - v.xMin)(maxNum2(v.yMax - v.yMin)(v.zMax - v.zMin))+      };+    };+  };+  var toLine = function(l) {+    return {+      points: fromMaybe([])(l.points),+      color: fromMaybe("#1f77b4")(l.color),+      width: fromMaybe(1.5)(l.width)+    };+  };+  var toBar = function(v) {+    return function(l) {+      var frac = fromMaybe(0.04)(l.barWidth);+      return {+        tops: fromMaybe([])(l.points),+        baseZ: fromMaybe(0)(l.barBaseZ),+        halfWx: frac * (v.xMax - v.xMin),+        halfWy: frac * (v.yMax - v.yMin),+        color: fromMaybe("#5b9bd5")(l.color),+        alpha: fromMaybe(1)(l.alpha),+        errs: fromMaybe([])(l.ptErrs)+      };+    };+  };+  var rangeArr = range2;+  var niceTicksFor = function(isLog) {+    return function(lo) {+      return function(hi) {+        if (isLog) {+          var k1 = round2(floor(hi));+          var k0 = round2(ceil(lo));+          var $115 = k1 >= k0;+          if ($115) {+            return map11(toNumber)(range2(k0)(k1));+          }+          ;+          return extendedBreaks(5)(lo)(hi);+        }+        ;+        var eps = (hi - lo) * 1e-6;+        return filter(function(t) {+          return t >= lo - eps && t <= hi + eps;+        })(extendedBreaks(5)(lo)(hi));+      };+    };+  };+  var lg10 = function(v) {+    return log((function() {+      var $116 = v < 1e-12;+      if ($116) {+        return 1e-12;+      }+      ;+      return v;+    })()) / log(10);+  };+  var lgIf = function(flag) {+    return function(v) {+      if (flag) {+        return lg10(v);+      }+      ;+      return v;+    };+  };+  var logAxes = function(f) {+    return function(v) {+      return {+        nTicks: v.nTicks,+        xMin: lgIf(f.x)(v.xMin),+        xMax: lgIf(f.x)(v.xMax),+        yMin: lgIf(f.y)(v.yMin),+        yMax: lgIf(f.y)(v.yMax),+        zMin: lgIf(f.z)(v.zMin),+        zMax: lgIf(f.z)(v.zMax)+      };+    };+  };+  var logPoint = function(f) {+    return function(v) {+      return new Point3(lgIf(f.x)(v.value0), lgIf(f.y)(v.value1), lgIf(f.z)(v.value2));+    };+  };+  var logLayer = function(f) {+    return function(v) {+      var logTuple = function(v1) {+        return new Tuple(lg10(v1.value0), lg10(v1.value1));+      };+      return {+        kind: v.kind,+        color: v.color,+        size: v.size,+        alpha: v.alpha,+        width: v.width,+        edges: v.edges,+        shaded: v.shaded,+        surfaceWire: v.surfaceWire,+        edgeColor: v.edgeColor,+        ptColors: v.ptColors,+        legend: v.legend,+        colorbar: v.colorbar,+        ptSizes: v.ptSizes,+        ptErrs: v.ptErrs,+        labels: v.labels,+        vectors: v.vectors,+        vecScale: v.vecScale,+        stemBaseZ: v.stemBaseZ,+        barBaseZ: v.barBaseZ,+        barStyle: v.barStyle,+        barWidth: v.barWidth,+        faces: v.faces,+        points: map1(map11(logPoint(f)))(v.points),+        grid: (function() {+          if (f.z) {+            return map1(map11(map11(lg10)))(v.grid);+          }+          ;+          return v.grid;+        })(),+        xRange: (function() {+          if (f.x) {+            return map1(logTuple)(v.xRange);+          }+          ;+          return v.xRange;+        })(),+        yRange: (function() {+          if (f.y) {+            return map1(logTuple)(v.yRange);+          }+          ;+          return v.yRange;+        })()+      };+    };+  };+  var fmtNum = function(v) {+    var ri = round2(v);+    var d = v - toNumber(ri);+    var $133 = d < 5e-3 && d > -5e-3;+    if ($133) {+      return show3(ri);+    }+    ;+    return show12(toNumber(round2(v * 100)) / 100);+  };+  var fmtAxis = function(isLog) {+    return function(v) {+      if (isLog) {+        return fmtNum(pow(10)(v));+      }+      ;+      return fmtNum(v);+    };+  };+  var faceNormal3 = function(ax) {+    return function(ay) {+      return function(az) {+        return function(bx) {+          return function(by) {+            return function(bz) {+              return function(cx) {+                return function(cy) {+                  return function(cz) {+                    var vz = cz - az;+                    var vy = cy - ay;+                    var vx = cx - ax;+                    var uz = bz - az;+                    var uy = by - ay;+                    var ux = bx - ax;+                    var nz = ux * vy - uy * vx;+                    var ny = uz * vx - ux * vz;+                    var nx = uy * vz - uz * vy;+                    var ln = sqrt(nx * nx + ny * ny + nz * nz);+                    var $135 = ln < 1e-12;+                    if ($135) {+                      return [0, 0, 1];+                    }+                    ;+                    return [nx / ln, ny / ln, nz / ln];+                  };+                };+              };+            };+          };+        };+      };+    };+  };+  var toTrisurfMesh = function(l) {+    var pts = fromMaybe([])(l.points);+    var tri = function(f) {+      var v = index(pts)(f.c);+      var v1 = index(pts)(f.b);+      var v2 = index(pts)(f.a);+      if (v2 instanceof Just && (v1 instanceof Just && v instanceof Just)) {+        var nrm = faceNormal3(v2.value0.value0)(v2.value0.value1)(v2.value0.value2)(v1.value0.value0)(v1.value0.value1)(v1.value0.value2)(v.value0.value0)(v.value0.value1)(v.value0.value2);+        return {+          pos: [v2.value0.value0, v2.value0.value1, v2.value0.value2, v1.value0.value0, v1.value0.value1, v1.value0.value2, v.value0.value0, v.value0.value1, v.value0.value2],+          nor: append4(nrm)(append4(nrm)(nrm))+        };+      }+      ;+      return {+        pos: [],+        nor: []+      };+    };+    var faces = fromMaybe([])(l.faces);+    var tris = map11(tri)(faces);+    return {+      positions: concatMap(function(v) {+        return v.pos;+      })(tris),+      normals: concatMap(function(v) {+        return v.nor;+      })(tris),+      color: fromMaybe("#5b9bd5")(l.color),+      alpha: fromMaybe(1)(l.alpha),+      shaded: true+    };+  };+  var errPoints = function(l) {+    return function(pts) {+      if (l.ptErrs instanceof Nothing) {+        return [];+      }+      ;+      if (l.ptErrs instanceof Just) {+        return concatMap(function(v) {+          return [new Point3(v.value0.value0, v.value0.value1, v.value0.value2 - v.value1), new Point3(v.value0.value0, v.value0.value1, v.value0.value2 + v.value1)];+        })(zip(pts)(l.ptErrs.value0));+      }+      ;+      throw new Error("Failed pattern match at Examples.WebGLPlot (line 614, column 19 - line 617, column 40): " + [l.ptErrs.constructor.name]);+    };+  };+  var drawLayer = function(ctx) {+    return function(mvp) {+      return function(axes) {+        return function(v) {+          if (v.kind instanceof Just && v.kind.value0 instanceof M3Scatter) {+            if (v.ptColors instanceof Just) {+              return function __do() {+                var ep = mkScatterColorProgram(ctx)();+                if (ep instanceof Left) {+                  return log4("[plot] scatter color program: " + ep.value0)();+                }+                ;+                if (ep instanceof Right) {+                  return drawScatterColored(ctx)(ep.value0)(mvp)(toScatterColored(v)(v.ptColors.value0))();+                }+                ;+                throw new Error("Failed pattern match at Examples.WebGLPlot (line 159, column 7 - line 161, column 74): " + [ep.constructor.name]);+              };+            }+            ;+            if (v.ptColors instanceof Nothing) {+              return function __do() {+                var ep = mkScatterProgram(ctx)();+                if (ep instanceof Left) {+                  return log4("[plot] scatter program: " + ep.value0)();+                }+                ;+                if (ep instanceof Right) {+                  return drawScatter(ctx)(ep.value0)(mvp)(toScatter(v))();+                }+                ;+                throw new Error("Failed pattern match at Examples.WebGLPlot (line 164, column 7 - line 166, column 55): " + [ep.constructor.name]);+              };+            }+            ;+            throw new Error("Failed pattern match at Examples.WebGLPlot (line 156, column 21 - line 166, column 55): " + [v.ptColors.constructor.name]);+          }+          ;+          if (v.kind instanceof Just && v.kind.value0 instanceof M3Line) {+            return function __do() {+              var ep = mkLineProgram(ctx)();+              if (ep instanceof Left) {+                return log4("[plot] line program: " + ep.value0)();+              }+              ;+              if (ep instanceof Right) {+                return drawLine(ctx)(ep.value0)(mvp)(toLine(v))();+              }+              ;+              throw new Error("Failed pattern match at Examples.WebGLPlot (line 169, column 5 - line 171, column 47): " + [ep.constructor.name]);+            };+          }+          ;+          if (v.kind instanceof Just && v.kind.value0 instanceof M3Wireframe) {+            return function __do() {+              var ep = mkLineProgram(ctx)();+              if (ep instanceof Left) {+                return log4("[plot] wireframe program: " + ep.value0)();+              }+              ;+              if (ep instanceof Right) {+                return drawWireframe(ctx)(ep.value0)(mvp)(toWireframe(v))();+              }+              ;+              throw new Error("Failed pattern match at Examples.WebGLPlot (line 174, column 5 - line 176, column 57): " + [ep.constructor.name]);+            };+          }+          ;+          if (v.kind instanceof Just && v.kind.value0 instanceof M3Surface) {+            if (v.surfaceWire instanceof Just && v.surfaceWire.value0) {+              return function __do() {+                var ep = mkLineProgram(ctx)();+                if (ep instanceof Left) {+                  return log4("[plot] surface wire program: " + ep.value0)();+                }+                ;+                if (ep instanceof Right) {+                  return drawWireframe(ctx)(ep.value0)(mvp)(toSurfaceWire(v))();+                }+                ;+                throw new Error("Failed pattern match at Examples.WebGLPlot (line 183, column 7 - line 185, column 61): " + [ep.constructor.name]);+              };+            }+            ;+            return function __do() {+              var ep = mkSurfaceProgram(ctx)();+              if (ep instanceof Left) {+                return log4("[plot] surface program: " + ep.value0)();+              }+              ;+              if (ep instanceof Right) {+                return drawSurface(ctx)(ep.value0)(mvp)(fromMaybe(1)(v.alpha))(toSurface(v))();+              }+              ;+              throw new Error("Failed pattern match at Examples.WebGLPlot (line 188, column 7 - line 190, column 79): " + [ep.constructor.name]);+            };+          }+          ;+          if (v.kind instanceof Just && v.kind.value0 instanceof M3Stem) {+            return function __do() {+              var elp = mkLineProgram(ctx)();+              var esp = mkScatterProgram(ctx)();+              if (elp instanceof Right && esp instanceof Right) {+                return drawStem(ctx)(elp.value0)(esp.value0)(mvp)(toStem(v))();+              }+              ;+              if (elp instanceof Left) {+                return log4("[plot] stem line program: " + elp.value0)();+              }+              ;+              if (esp instanceof Left) {+                return log4("[plot] stem scatter program: " + esp.value0)();+              }+              ;+              throw new Error("Failed pattern match at Examples.WebGLPlot (line 196, column 5 - line 199, column 62): " + [elp.constructor.name, esp.constructor.name]);+            };+          }+          ;+          if (v.kind instanceof Just && v.kind.value0 instanceof M3Quiver) {+            return function __do() {+              var ep = mkLineProgram(ctx)();+              if (ep instanceof Left) {+                return log4("[plot] quiver line program: " + ep.value0)();+              }+              ;+              if (ep instanceof Right) {+                return drawQuiver(ctx)(ep.value0)(mvp)(toQuiver(axes)(v))();+              }+              ;+              throw new Error("Failed pattern match at Examples.WebGLPlot (line 203, column 5 - line 205, column 56): " + [ep.constructor.name]);+            };+          }+          ;+          if (v.kind instanceof Just && v.kind.value0 instanceof M3Bar) {+            return function __do() {+              var esp = mkSurfaceProgram(ctx)();+              var elp = mkLineProgram(ctx)();+              if (esp instanceof Right && elp instanceof Right) {+                return drawBar(ctx)(esp.value0)(elp.value0)(mvp)(toBar(axes)(v))();+              }+              ;+              if (esp instanceof Left) {+                return log4("[plot] bar surface program: " + esp.value0)();+              }+              ;+              if (elp instanceof Left) {+                return log4("[plot] bar line program: " + elp.value0)();+              }+              ;+              throw new Error("Failed pattern match at Examples.WebGLPlot (line 210, column 5 - line 213, column 58): " + [esp.constructor.name, elp.constructor.name]);+            };+          }+          ;+          if (v.kind instanceof Just && v.kind.value0 instanceof M3Trisurf) {+            return function __do() {+              var esp = mkSurfaceProgram(ctx)();+              var elp = mkLineProgram(ctx)();+              (function() {+                if (esp instanceof Left) {+                  return log4("[plot] trisurf surface program: " + esp.value0)();+                }+                ;+                if (esp instanceof Right) {+                  return drawTriMesh(ctx)(esp.value0)(mvp)(toTrisurfMesh(v))();+                }+                ;+                throw new Error("Failed pattern match at Examples.WebGLPlot (line 220, column 5 - line 222, column 57): " + [esp.constructor.name]);+              })();+              if (elp instanceof Left) {+                return log4("[plot] trisurf line program: " + elp.value0)();+              }+              ;+              if (elp instanceof Right) {+                return drawWireframe(ctx)(elp.value0)(mvp)(toTrisurfEdges(v))();+              }+              ;+              throw new Error("Failed pattern match at Examples.WebGLPlot (line 223, column 5 - line 225, column 62): " + [elp.constructor.name]);+            };+          }+          ;+          if (v.kind instanceof Just && v.kind.value0 instanceof M3Text) {+            return pure4(unit);+          }+          ;+          if (v.kind instanceof Nothing) {+            return log4("[plot] layer with no kind, skipped");+          }+          ;+          throw new Error("Failed pattern match at Examples.WebGLPlot (line 153, column 44 - line 229, column 54): " + [v.kind.constructor.name]);+        };+      };+    };+  };+  var drawAxes = function(ctx) {+    return function(mvp) {+      return function(ax) {+        return function __do() {+          var ep = compileProgram(ctx)(lineVS)(lineFS)();+          if (ep instanceof Left) {+            return unit;+          }+          ;+          if (ep instanceof Right) {+            useProgram(ctx)(ep.value0)();+            var aPos = getAttribLocation(ctx)(ep.value0)("aPosition")();+            var muvp = getUniformLocation(ctx)(ep.value0)("uMVP")();+            var mc = getUniformLocation(ctx)(ep.value0)("uColor")();+            if (muvp instanceof Just && mc instanceof Just) {+              uniformMatrix4fv(ctx)(muvp.value0)(mat4ToArrayColMajor(mvp))();+              var col = parseHexColor("#444444");+              uniform4f(ctx)(mc.value0)(col.r)(col.g)(col.b)(1)();+              var buf = createBuffer(ctx)();+              var flat = concatMap(function(v) {+                return [v.value0, v.value1, v.value2];+              })(axesBoxEdges(ax));+              uploadAttribVec3(ctx)(buf)(aPos)(flat)();+              return drawArrays(ctx)(glLINES)(0)(div1(length(flat))(3))();+            }+            ;+            return unit;+          }+          ;+          throw new Error("Failed pattern match at Examples.WebGLPlot (line 516, column 3 - line 535, column 26): " + [ep.constructor.name]);+        };+      };+    };+  };+  var defaultPerspective = function(d) {+    return new Perspective({+      fov: pi / 4,+      aspect: d.w / d.h,+      near: 0.1,+      far: 100+    });+  };+  var concatMapA = concatMap;+  var layerPoints = function(v) {+    if (v.kind instanceof Just && v.kind.value0 instanceof M3Surface) {+      var grid = fromMaybe([])(v.grid);+      var v1 = fromMaybe(new Tuple(-1, 1))(v.xRange);+      var v2 = fromMaybe(new Tuple(-1, 1))(v.yRange);+      var ny = length(grid);+      return concatMapA(function(rowIx) {+        var v3 = unsafeIndex2(grid)(rowIx);+        if (v3 instanceof Just) {+          var yAt = (function() {+            var $232 = ny <= 1;+            if ($232) {+              return v2.value0;+            }+            ;+            return v2.value0 + (v2.value1 - v2.value0) * toNumber(rowIx) / toNumber(ny - 1 | 0);+          })();+          var nx = length(v3.value0);+          return concatMapA(function(colIx) {+            var v4 = unsafeIndex2(v3.value0)(colIx);+            if (v4 instanceof Just) {+              var xAt = (function() {+                var $234 = nx <= 1;+                if ($234) {+                  return v1.value0;+                }+                ;+                return v1.value0 + (v1.value1 - v1.value0) * toNumber(colIx) / toNumber(nx - 1 | 0);+              })();+              return [new Point3(xAt, yAt, v4.value0)];+            }+            ;+            if (v4 instanceof Nothing) {+              return [];+            }+            ;+            throw new Error("Failed pattern match at Examples.WebGLPlot (line 586, column 37 - line 591, column 30): " + [v4.constructor.name]);+          })(rangeArr(0)(nx - 1 | 0));+        }+        ;+        if (v3 instanceof Nothing) {+          return [];+        }+        ;+        throw new Error("Failed pattern match at Examples.WebGLPlot (line 581, column 30 - line 592, column 23): " + [v3.constructor.name]);+      })(rangeArr(0)(ny - 1 | 0));+    }+    ;+    if (v.kind instanceof Just && v.kind.value0 instanceof M3Bar) {+      var tops = fromMaybe([])(v.points);+      var bz = fromMaybe(0)(v.barBaseZ);+      var bases = map11(function(v12) {+        return new Point3(v12.value0, v12.value1, bz);+      })(tops);+      return append4(tops)(append4(bases)(errPoints(v)(tops)));+    }+    ;+    if (v.kind instanceof Just && v.kind.value0 instanceof M3Stem) {+      var tops = fromMaybe([])(v.points);+      var bz = fromMaybe(0)(v.stemBaseZ);+      var bases = map11(function(v12) {+        return new Point3(v12.value0, v12.value1, bz);+      })(tops);+      return append4(tops)(append4(bases)(errPoints(v)(tops)));+    }+    ;+    if (v.kind instanceof Just && v.kind.value0 instanceof M3Quiver) {+      return fromMaybe([])(v.points);+    }+    ;+    var pts = fromMaybe([])(v.points);+    return append4(pts)(errPoints(v)(pts));+  };+  var canvasDims = canvasDimsFFI;+  var cameraDistance = function(v) {+    return Nothing.value;+  };+  var axisTicksOf = function(v) {+    return function(f) {+      return {+        xs: niceTicksFor(f.x)(v.xMin)(v.xMax),+        ys: niceTicksFor(f.y)(v.yMin)(v.yMax),+        zs: niceTicksFor(f.z)(v.zMin)(v.zMax)+      };+    };+  };+  var axisTickLabels3D = function(v) {+    return function(f) {+      return function(t) {+        var maxN3 = function(p) {+          return function(q) {+            return function(r) {+              var $258 = p >= q;+              if ($258) {+                var $259 = p >= r;+                if ($259) {+                  return p;+                }+                ;+                return r;+              }+              ;+              var $260 = q >= r;+              if ($260) {+                return q;+              }+              ;+              return r;+            };+          };+        };+        var span_ = maxN3(v.xMax - v.xMin)(v.yMax - v.yMin)(v.zMax - v.zMin);+        var off = 0.05 * span_;+        var mk = function(world) {+          return function(txt) {+            return {+              point: world,+              text: txt,+              color: "#333333",+              size: 11+            };+          };+        };+        var xL = map11(function(x) {+          return mk(new Point3(x, v.yMin - off, v.zMin))(fmtAxis(f.x)(x));+        })(t.xs);+        var yL = map11(function(y) {+          return mk(new Point3(v.xMin - off, y, v.zMin))(fmtAxis(f.y)(y));+        })(t.ys);+        var zL = map11(function(z) {+          return mk(new Point3(v.xMin - off, v.yMin - off, z))(fmtAxis(f.z)(z));+        })(t.zs);+        return append4(xL)(append4(yL)(zL));+      };+    };+  };+  var autoAxesFromLayers = function(ls) {+    var pts = concatMapA(layerPoints)(ls);+    if (pts.length === 0) {+      return {+        xMin: -1,+        xMax: 1,+        yMin: -1,+        yMax: 1,+        zMin: -1,+        zMax: 1,+        nTicks: 5+      };+    }+    ;+    var zs = map11(function(v) {+      return v.value2;+    })(pts);+    var ys = map11(function(v) {+      return v.value1;+    })(pts);+    var xs = map11(function(v) {+      return v.value0;+    })(pts);+    return {+      xMin: fromMaybe(-1)(minimum2(xs)),+      xMax: fromMaybe(1)(maximum3(xs)),+      yMin: fromMaybe(-1)(minimum2(ys)),+      yMax: fromMaybe(1)(maximum3(ys)),+      zMin: fromMaybe(-1)(minimum2(zs)),+      zMax: fromMaybe(1)(maximum3(zs)),+      nTicks: 5+    };+  };+  var attachOrbitHandlers = function(c) {+    return function(h) {+      return function() {+        return _attachOrbitHandlers(c, h);+      };+    };+  };+  var applyLogSpec = function(f) {+    return function(vs) {+      return {+        title: vs.title,+        camera: vs.camera,+        proj: vs.proj,+        width: vs.width,+        height: vs.height,+        xAspect: vs.xAspect,+        yAspect: vs.yAspect,+        zAspect: vs.zAspect,+        logScale: vs.logScale,+        layers: map11(logLayer(f))(vs.layers),+        axes: map1(logAxes(f))(vs.axes)+      };+    };+  };+  var applyAspect = function(v) {+    return function(sx) {+      return function(sy) {+        return function(sz) {+          return function(mvp) {+            var $279 = sx === 1 && (sy === 1 && sz === 1);+            if ($279) {+              return mvp;+            }+            ;+            var cz = (v.zMin + v.zMax) / 2;+            var cy = (v.yMin + v.yMax) / 2;+            var cx = (v.xMin + v.xMax) / 2;+            var model = multMat4(translateMat4(cx)(cy)(cz))(multMat4(scaleMat4(sx)(sy)(sz))(translateMat4(-cx)(-cy)(-cz)));+            return multMat4(mvp)(model);+          };+        };+      };+    };+  };+  var setupAndRender = function(canvas) {+    return function(v) {+      var vs = (function() {+        if (v.logScale instanceof Just && (v.logScale.value0.x || (v.logScale.value0.y || v.logScale.value0.z))) {+          return applyLogSpec(v.logScale.value0)(v);+        }+        ;+        return v;+      })();+      return function __do() {+        var ec = initWebGL(canvas)();+        if (ec instanceof Left) {+          return log4("[plot] WebGL init: " + ec.value0)();+        }+        ;+        if (ec instanceof Right) {+          var initialDist = fromMaybe(3)(bindFlipped2(cameraDistance)(vs.camera));+          var orbitRef = $$new(defaultOrbitZUp(initialDist))();+          var initDims = canvasDims(canvas)();+          var proj = fromMaybe(defaultPerspective(initDims))(vs.proj);+          var logF = fromMaybe({+            x: false,+            y: false,+            z: false+          })(vs.logScale);+          var axes = fromMaybe(autoAxesFromLayers(vs.layers))(vs.axes);+          var ticks3 = axisTicksOf(axes)(logF);+          var aspZ = fromMaybe(1)(vs.zAspect);+          var aspY = fromMaybe(1)(vs.yAspect);+          var aspX = fromMaybe(1)(vs.xAspect);+          var redraw = function __do2() {+            var d = canvasDims(canvas)();+            viewport(ec.value0)(0)(0)(truncate(d.w))(truncate(d.h))();+            clearColor(ec.value0)(1)(1)(1)(1)();+            clear(ec.value0)(glCOLOR_BUFFER_BIT + glDEPTH_BUFFER_BIT | 0)();+            var orbit = read(orbitRef)();+            var cam = orbitToCamera(orbit);+            var mvp = applyAspect(axes)(aspX)(aspY)(aspZ)(computeMVP(cam)(proj));+            var epp = mkSurfaceProgram(ec.value0)();+            var elp = mkLineProgram(ec.value0)();+            (function() {+              if (epp instanceof Right && elp instanceof Right) {+                return drawPanes(ec.value0)(epp.value0)(elp.value0)(mvp)(axes)(ticks3)();+              }+              ;+              return unit;+            })();+            drawAxes(ec.value0)(mvp)(axes)();+            for_2(vs.layers)(drawLayer(ec.value0)(mvp)(axes))();+            var ov = ensureOverlay(canvas)();+            clearOverlay(ov)();+            var mvpArr = mat4ToArrayColMajor(mvp);+            drawLabels(ov)(mvpArr)(d.w)(d.h)(axisTickLabels3D(axes)(logF)(ticks3))();+            return for_2(vs.layers)(function(v1) {+              return function __do3() {+                when4(eq13(v1.kind)(new Just(M3Text.value)))(drawLabels(ov)(mvpArr)(d.w)(d.h)(toTextLabels(v1)))();+                (function() {+                  if (v1.legend instanceof Just) {+                    return drawLegend(ov)(map11(function(v2) {+                      return {+                        label: v2.value0,+                        color: v2.value1+                      };+                    })(v1.legend.value0))();+                  }+                  ;+                  if (v1.legend instanceof Nothing) {+                    return unit;+                  }+                  ;+                  throw new Error("Failed pattern match at Examples.WebGLPlot (line 129, column 17 - line 131, column 40): " + [v1.legend.constructor.name]);+                })();+                if (v1.colorbar instanceof Just) {+                  return drawColorbar(ov)(v1.colorbar.value0)();+                }+                ;+                if (v1.colorbar instanceof Nothing) {+                  return unit;+                }+                ;+                throw new Error("Failed pattern match at Examples.WebGLPlot (line 132, column 17 - line 134, column 39): " + [v1.colorbar.constructor.name]);+              };+            })();+          };+          redraw();+          return attachOrbitHandlers(canvas)({+            onRotate: function(dT, dP) {+              modify_(rotateOrbit(dT)(dP))(orbitRef)();+              return redraw();+            },+            onZoom: function(factor) {+              modify_(zoomOrbit(factor))(orbitRef)();+              return redraw();+            },+            onPan: function(dx, dy) {+              modify_(panOrbit(dx)(dy))(orbitRef)();+              return redraw();+            }+          })();+        }+        ;+        throw new Error("Failed pattern match at Examples.WebGLPlot (line 81, column 3 - line 146, column 10): " + [ec.constructor.name]);+      };+    };+  };+  var renderFromSpec = function(canvasId) {+    return function(json) {+      var v = decodeVisualSpec3D(json);+      if (v instanceof Left) {+        return log4("[plot] decode failed: " + show22(v.value0));+      }+      ;+      if (v instanceof Right) {+        return function __do() {+          var mc = canvasById(canvasId)();+          if (mc instanceof Nothing) {+            return log4("[plot] canvas not found: " + canvasId)();+          }+          ;+          if (mc instanceof Just) {+            return setupAndRender(mc.value0)(v.value0)();+          }+          ;+          throw new Error("Failed pattern match at Examples.WebGLPlot (line 69, column 5 - line 71, column 48): " + [mc.constructor.name]);+        };+      }+      ;+      throw new Error("Failed pattern match at Examples.WebGLPlot (line 65, column 32 - line 71, column 48): " + [v.constructor.name]);+    };+  };+  var main = /* @__PURE__ */ exposeGlobal(/* @__PURE__ */ mkEffectFn2(renderFromSpec));++  // <stdin>+  main();+})();
+ examples/Browser3DDemo.hs view
@@ -0,0 +1,43 @@+-- | Phase 5 A8 showBrowser demo。+--+-- @+-- cabal run browser-3d-demo+-- @+--+-- → tmp HTML 生成 → xdg-open でブラウザ起動 → WebGL 3D が interactive 表示。+{-# LANGUAGE OverloadedStrings #-}+module Main where++import           Graphics.Hgg.ThreeD          (defaultCameraZUp, defaultPerspective,+                                                Point3 (..))+import           Graphics.Hgg.ThreeD.Axes     (defaultAxes3D)+import           Graphics.Hgg.ThreeD.Browser  (showBrowser)+import           Graphics.Hgg.ThreeD.Spec+import           Graphics.Hgg.Color           (fromHex)++main :: IO ()+main = showBrowser $+     purePlot3D+  <> layer3D (scatter3DPoints helix     <> color3D (fromHex "#56B4E9") <> size3D 6  <> alpha3D 0.9)+  <> layer3D (line3DPoints helixLine <> color3D (fromHex "#009E73") <> width3D 1.5)+  <> camera     (defaultCameraZUp 3)+  <> projection defaultPerspective+  <> axes3D     defaultAxes3D+  <> title3D    "Phase 5 browser display demo"+  where+    -- 200 点 helix scatter+    helix =+      [ Point3 (cos (t * 4 * pi) * 0.8)+               (sin (t * 4 * pi) * 0.8)+               (t * 2 - 1)+      | i <- [0 .. 199 :: Int]+      , let t = fromIntegral i / 199.0+      ]+    -- 100 点 helix line (= 別の螺旋)+    helixLine =+      [ Point3 (cos (t * 6 * pi) * 0.5)+               (sin (t * 6 * pi) * 0.5)+               (t - 0.5)+      | i <- [0 .. 99 :: Int]+      , let t = fromIntegral i / 99.0+      ]
+ examples/Plot3DDemo.hs view
@@ -0,0 +1,564 @@+-- | Phase 3 demo: hgg-3d を使った 3D plot SVG 生成 (= 全 5 case)。+--+-- @+-- cabal run plot3d-demo+-- @+-- → design/3d/ に 5 SVG (scatter helix/cube、 line helix、 wireframe cube、 surface sinc)。+--+-- A8 で hgg-svg の savePrimitivesSVG (= 新公開 helper) に統合済。+{-# LANGUAGE OverloadedStrings #-}+module Main where++import           System.Directory                (createDirectoryIfMissing)+import           Data.Text                       (Text)+import qualified Data.Text                       as T++import           Graphics.Hgg.Backend.SVG        (savePrimitivesSVG)++import           Graphics.Hgg.ThreeD.Axes        (defaultAxes3D, Axes3D (..),+                                                  renderAxes3D)+import           Graphics.Hgg.ThreeD.Projection  (Viewport (..))+import           Graphics.Hgg.ThreeD.Line        (defaultLine3D, defaultWireframe3D,+                                                  renderLine3D, renderWireframe3D)+import           Graphics.Hgg.ThreeD.Scatter     (defaultScatter3D, Scatter3D (..),+                                                  renderScatter3D)+import           Graphics.Hgg.ThreeD.Surface     (renderSurface3D, surfaceFromFunction,+                                                  sf3Grid, sf3XRange, sf3YRange)+import           Graphics.Hgg.ThreeD.Spec        (layer3D, surface3DGrid, colormap3D,+                                                  surfaceWire, color3D, xRange3D, yRange3D, layerToSurface,+                                                  contourX, contourY, contourZ,+                                                  axisTitles3D, zAspect3D,+                                                  scatter3DPoints, scatter3D, colorBy3D,+                                                  colorContinuousBy3D, sizeBy3D, sizeRange3D,+                                                  bar3D, errorBar3D, barWidth3D,+                                                  stem3D, stemBaseZ,+                                                  quiver3D, vecScale3D,+                                                  trisurf,+                                                  text3DPoints, annotate3D,+                                                  line3DPoints, logScale3D, xAspect3D,+                                                  color3D, size3D, alpha3D,+                                                  xRange3D, yRange3D, camera, title3D,+                                                  axes3D, pane3D, width3DV, height3DV)+import           Graphics.Hgg.ThreeD.Easy        (saveSVG3D, saveSVG3DFacet,+                                                  savePDF3D, savePNG3D)+import           Graphics.Hgg.ThreeD.Bound       (saveSVG3DBound)+import           Graphics.Hgg.Frame              ((|>>))+import           Graphics.Hgg.Spec               (ColData (..))+import           Graphics.Hgg.Color              (fromHex)+import qualified Data.Vector                     as V+import           Graphics.Hgg.ThreeD.Types++main :: IO ()+main = do+  let outDir = "design/3d"+  createDirectoryIfMissing True outDir++  -- z-up convention (= defaultCameraZUp helper、 mplot3d 慣例)+  let cam   = defaultCameraZUp 3        -- (3, -3, 1.8) → origin+      proj  = defaultPerspective        -- fov 45° / aspect 1:1+      vp    = Viewport 50 50 600 600++  -- ===========================================================================+  -- Case 1: scatter3D helix (= 螺旋曲線 200 点)+  -- ===========================================================================+  let axes  = defaultAxes3D { axesXMin = -1, axesXMax = 1+                            , axesYMin = -1, axesYMax = 1+                            , axesZMin = 0,  axesZMax = 2 }+      helixPts =+        [ Point3 (cos (t * 4 * pi) * 0.8)+                 (sin (t * 4 * pi) * 0.8)+                 (t * 2)+        | i <- [0 .. 199 :: Int]+        , let t = fromIntegral i / 199.0 ]+      sc = defaultScatter3D helixPts+      prims = renderAxes3D cam proj vp axes+           <> renderScatter3D cam proj vp sc+  savePrimitivesSVG (outDir <> "/scatter3d-helix.svg") 700 700+    "3D scatter: helix (200 pts)" prims+  putStrLn "  helix → scatter3d-helix.svg"++  -- ===========================================================================+  -- Case 2: scatter3D cube random (= 立方体内 500 点)+  -- ===========================================================================+  let cubePts =+        [ Point3 (frac (i * 0.317) * 2 - 1)+                 (frac (i * 0.793) * 2 - 1)+                 (frac (i * 0.491) * 2 - 1)+        | i <- [1 .. 500] :: [Double] ]+      sc2 = (defaultScatter3D cubePts) { sc3Color = "#d62728", sc3Size = 4 }+      prims2 = renderAxes3D cam proj vp defaultAxes3D+            <> renderScatter3D cam proj vp sc2+  savePrimitivesSVG (outDir <> "/scatter3d-cube.svg") 700 700+    "3D scatter: cube random (500 pts)" prims2+  putStrLn "  cube → scatter3d-cube.svg"++  -- ===========================================================================+  -- Case 3: line3D helix+  -- ===========================================================================+  let ln = defaultLine3D helixPts+      prims3 = renderAxes3D cam proj vp axes+            <> renderLine3D cam proj vp ln+  savePrimitivesSVG (outDir <> "/line3d-helix.svg") 700 700+    "3D line: helix (200 pts as continuous line)" prims3+  putStrLn "  line helix → line3d-helix.svg"++  -- ===========================================================================+  -- Case 4: wireframe3D cube (= 8 corners + 12 edges)+  -- ===========================================================================+  let cubeCorners =+        [ Point3 (-1) (-1) (-1), Point3 1 (-1) (-1), Point3 1 1 (-1), Point3 (-1) 1 (-1)+        , Point3 (-1) (-1)   1 , Point3 1 (-1)   1 , Point3 1 1   1 , Point3 (-1) 1   1+        ]+      cubeEdges = [(0,1),(1,2),(2,3),(3,0),(4,5),(5,6),(6,7),(7,4),(0,4),(1,5),(2,6),(3,7)]+      wf = defaultWireframe3D cubeCorners cubeEdges+      prims4 = renderAxes3D cam proj vp defaultAxes3D+            <> renderWireframe3D cam proj vp wf+  savePrimitivesSVG (outDir <> "/wireframe3d-cube.svg") 700 700+    "3D wireframe: cube (8 corners, 12 edges)" prims4+  putStrLn "  wireframe cube → wireframe3d-cube.svg"++  -- ===========================================================================+  -- Case 5: surface3DGrid sinc-like (= z = sin(3r)/r / 2、 40x40 grid + Lambert)+  -- z-up: 膜が +z 方向に立ち上がる (= defaultCameraZUp 経由)+  -- ===========================================================================+  let surfCam = defaultCameraZUp 5      -- (5, -5, 3) → origin+      surfAxes = defaultAxes3D { axesXMin = -3, axesXMax = 3+                               , axesYMin = -3, axesYMax = 3+                               , axesZMin = -0.3, axesZMax = 1 }+      surf = surfaceFromFunction 40 40 (-3) 3 (-3) 3+               (\x y -> let r = sqrt (x*x + y*y) + 1e-6 in sin (r * 3) / r * 0.5)+      prims5 = renderAxes3D surfCam proj vp surfAxes+            <> renderSurface3D surfCam proj vp surf+  savePrimitivesSVG (outDir <> "/surface3d-sinc.svg") 700 700+    "3D surface: z = sin(3r)/r / 2 (40x40 grid, Lambert shading, z-up)" prims5+  putStrLn "  surface sinc (z-up) → surface3d-sinc.svg"++  -- Case 5b: 同 surface を surfaceWire で線メッシュ描画 (plot_wireframe 相当・spec 経路)+  let wN      = 18+      wAxis   = [ -3 + 6 * fromIntegral i / fromIntegral wN | i <- [0 .. wN] ]+      wGrid   = [ [ let r = sqrt (x*x + y*y) + 1e-6 in sin (r * 3) / r * 0.5 | x <- wAxis ] | y <- wAxis ]+      wireSurf = layerToSurface ( surface3DGrid wGrid <> surfaceWire <> color3D (fromHex "#2563eb")+                                  <> xRange3D (-3, 3) <> yRange3D (-3, 3) )+      prims5b = renderAxes3D surfCam proj vp surfAxes+             <> renderSurface3D surfCam proj vp wireSurf+  savePrimitivesSVG (outDir <> "/surface3d-wire.svg") 700 700+    "3D surface wireframe (surfaceWire = plot_wireframe 相当)" prims5b+  putStrLn "  surface wireframe → surface3d-wire.svg"++  -- ===========================================================================+  -- Case 6: 同 surface を y-up convention で描画 (= OpenGL 慣例の比較用)+  -- 同データだが camera up = (0, 1, 0) → 「z が水平、 y が縦」 の見え方+  -- ===========================================================================+  let surfCamYUp = defaultCameraYUp 5    -- (5, 3, 5) → origin、 y = up+      prims6 = renderAxes3D surfCamYUp proj vp surfAxes+            <> renderSurface3D surfCamYUp proj vp surf+  savePrimitivesSVG (outDir <> "/surface3d-sinc-yup.svg") 700 700+    "Same surface but y-up convention (OpenGL/Unity, for comparison)" prims6+  putStrLn "  surface sinc (y-up) → surface3d-sinc-yup.svg"++  -- ===========================================================================+  -- Case 7 (Phase 24 A2): 同 surface を z colormap (viridis) + colorbar で。+  -- saveSVG3D (Spec Monoid 経路) を使う = colorbar が自動で付く+  -- ===========================================================================+  saveSVG3D (outDir <> "/surface3d-sinc-colormap.svg")+    (layer3D (surface3DGrid (sf3Grid surf)+                <> xRange3D (sf3XRange surf) <> yRange3D (sf3YRange surf)+                <> colormap3D)+       <> camera surfCam+       <> title3D "3D surface: z colormap (viridis) + colorbar")+  putStrLn "  surface sinc (colormap) → surface3d-sinc-colormap.svg"++  -- ===========================================================================+  -- Case 8 (Phase 24 A5): colormap surface + 床面投影 contour (plotly contours_z)+  -- floorContour3D を足すだけで床に等高線が落ちる+  -- ===========================================================================+  saveSVG3D (outDir <> "/surface3d-sinc-floor-contour.svg")+    (layer3D (surface3DGrid (sf3Grid surf)+                <> xRange3D (sf3XRange surf) <> yRange3D (sf3YRange surf)+                <> colormap3D <> contourZ 8)+       <> camera surfCam+       <> title3D "3D surface + floor-projected contour (contourZ)")+  putStrLn "  surface sinc (floor contour) → surface3d-sinc-floor-contour.svg"++  -- ===========================================================================+  -- Case 8b (#2): 壁面投影 contour (matplotlib contour zdir='x'/'y'/'z' 同型)。+  -- 1 つの surface に contourX/Y/Z を <> で合成するだけ。 投影壁はカメラから+  -- 遠い面に自動固定 (床=z 等値面・左右/前後壁=断面プロファイル)。+  -- ===========================================================================+  -- 軸を data (±3) より広く取り、 壁を曲面から離して断面を見やすく。+  let wallAxes = defaultAxes3D { axesXMin = -4.5, axesXMax = 4.5+                               , axesYMin = -4.5, axesYMax = 4.5+                               , axesZMin = -0.6, axesZMax = 1.8 }+  saveSVG3D (outDir <> "/surface3d-wall-contour.svg")+    (  layer3D ( surface3DGrid (sf3Grid surf)+              <> xRange3D (sf3XRange surf) <> yRange3D (sf3YRange surf)+              <> colormap3D+              <> contourX 8 <> contourY 8 <> contourZ 8 )+    <> axes3D wallAxes+    <> camera surfCam+    <> title3D "3D surface + wall-projected contour (zdir x/y/z)")+  putStrLn "  surface sinc (wall contour x/y/z) → surface3d-wall-contour.svg"++  -- ===========================================================================+  -- Case 9 (Phase 24 A7): 群別曲面の並置 (facet)。 2 群 (peak 鋭い / 緩い) の+  -- 応答曲面を colormap + colorbar 付きで並置+  -- ===========================================================================+  let mkSurf k = surfaceFromFunction 40 40 (-3) 3 (-3) 3+                   (\x y -> let r = sqrt (x*x + y*y) + 1e-6 in sin (r * 3) / r * k)+      panel label k =+        ( label+        , layer3D (surface3DGrid (sf3Grid (mkSurf k))+                     <> xRange3D (-3, 3) <> yRange3D (-3, 3) <> colormap3D)+            <> camera surfCam )+  saveSVG3DFacet (outDir <> "/surface3d-facet.svg")+    [ panel "group A (k=0.5)" 0.5, panel "group B (k=0.25)" 0.25 ]+  putStrLn "  surface facet (2 群) → surface3d-facet.svg"++  -- ===========================================================================+  -- Case 10 (Phase 24 A8): 軸タイトル + z aspect + 視点 preset。 RSM 風の応答に+  -- 物理量の軸名を付け、 z を 1.6 倍に縦伸ばし、 iso 視点で+  -- ===========================================================================+  let rsm = layer3D (surface3DGrid (sf3Grid surf)+                       <> xRange3D (sf3XRange surf) <> yRange3D (sf3YRange surf)+                       <> colormap3D)+              <> camera (cameraIso 5)+              <> axisTitles3D "温度 [°C]" "圧力 [bar]" "収率 [%]"+              <> zAspect3D 1.6+              <> title3D "応答曲面: 軸名 + z aspect 1.6"+  saveSVG3D    (outDir <> "/surface3d-a8-axes-aspect.svg") rsm+  savePNG3D    (outDir <> "/surface3d-a8-axes-aspect.png") rsm   -- 日本語ラベル可+  putStrLn "  surface A8 (軸名+aspect+PNG) → surface3d-a8-axes-aspect.{svg,png}"++  -- Case 11 (Phase 24 A8): PDF glue (Latin ラベルで). 視点 preset = top+  let rsmEn = layer3D (surface3DGrid (sf3Grid surf)+                         <> xRange3D (sf3XRange surf) <> yRange3D (sf3YRange surf)+                         <> colormap3D)+                <> camera (cameraIso 5)+                <> axisTitles3D "temp" "pressure" "yield"+                <> title3D "Response surface (PDF)"+  savePDF3D (outDir <> "/surface3d-a8.pdf") rsmEn+  putStrLn "  surface A8 (PDF) → surface3d-a8.pdf"++  -- ===========================================================================+  -- Case 12 (Phase 24 A8): 層間 depth 統合。 surface (半透明) + 実測点。+  -- 膜の奥の点は隠れ、 手前の点は前に出る (旧実装は全点が膜の前に浮いていた)+  -- ===========================================================================+  let dome = surfaceFromFunction 30 30 (-2) 2 (-2) 2+               (\x y -> 1.2 - 0.3 * (x*x + y*y))   -- 上に膨らむドーム+      -- r=1.2 の輪に沿って z を膜 (= domeZ 0.77) の上下に大きく振る:+      -- 膜より上の点は見え、 下の点は膜に隠れる+      obsPts = [ Point3 (cos t * 1.2) (sin t * 1.2) (0.77 + 0.6 * sin (t * 2))+               | k <- [0 .. 23 :: Int], let t = fromIntegral k / 24 * 2 * pi ]+      depthSpec = layer3D (surface3DGrid (sf3Grid dome)+                             <> xRange3D (-2, 2) <> yRange3D (-2, 2)+                             <> colormap3D)+               <> layer3D (scatter3DPoints obsPts <> color3D (fromHex "#d62728") <> size3D 6)+               <> camera (cameraIso 5)+               <> title3D "depth 統合: surface 膜の奥の点は隠れ手前は前に"+  saveSVG3D (outDir <> "/surface3d-a8-depth.svg") depthSpec+  putStrLn "  surface A8 (depth 統合) → surface3d-a8-depth.svg"++  -- ===========================================================================+  -- Case 13 (Phase 25 A2): 群色分け 3D 散布 + 離散凡例。 3 群のクラスタを+  -- df |>> (scatter3D ColRef + colorBy3D) で色分け + 自動凡例+  -- ===========================================================================+  let clusterAt cx cy cz k =+        [ ( cx + 0.6 * cos (t*5) * frac (fromIntegral k * 1.7 + t)+          , cy + 0.6 * sin (t*5) * frac (fromIntegral k * 2.3 + t)+          , cz + 0.5 * (frac (fromIntegral k * 3.1 + t) - 0.5) )+        | i <- [0 .. 14 :: Int], let t = fromIntegral i / 15 ]+      clA = clusterAt 0   0   0   1+      clB = clusterAt 2   1.5 1   2+      clC = clusterAt 1   2.5 (-1) 3+      allPts = clA ++ clB ++ clC+      labels = replicate (length clA) "cluster A"+            ++ replicate (length clB) "cluster B"+            ++ replicate (length clC) "cluster C"+      gdf = [ ("x",     NumData (V.fromList [ x | (x,_,_) <- allPts ]))+            , ("y",     NumData (V.fromList [ y | (_,y,_) <- allPts ]))+            , ("z",     NumData (V.fromList [ z | (_,_,z) <- allPts ]))+            , ("group", TxtData (V.fromList labels)) ] :: [(Text, ColData)]+      gspec = layer3D (scatter3D "x" "y" "z" <> colorBy3D "group" <> size3D 6)+           <> camera (defaultCameraZUp 5)+           <> axisTitles3D "PC1" "PC2" "PC3"+           <> width3DV 640 <> height3DV 600+           <> title3D "群色分け 3D 散布 + 離散凡例"+  saveSVG3DBound (outDir <> "/scatter3d-groups.svg") (gdf |>> gspec)+  putStrLn "  scatter 群色分け + 凡例 → scatter3d-groups.svg"++  -- ===========================================================================+  -- Case 14 (Phase 25 A3): 連続色マップ + size マップ (bubble)。 螺旋上の点を+  -- 値列 v で colorContinuousBy3D (viridis + colorbar)、 m で sizeBy3D (bubble)。+  -- ===========================================================================+  let nB    = 120 :: Int+      bt i  = fromIntegral i / fromIntegral nB+      bxs   = [ 1.4 * cos (bt i * 6.0) | i <- [0 .. nB - 1] ]+      bys   = [ 1.4 * sin (bt i * 6.0) | i <- [0 .. nB - 1] ]+      bzs   = [ 2.0 * bt i - 1.0       | i <- [0 .. nB - 1] ]+      bvs   = [ bt i                   | i <- [0 .. nB - 1] ]   -- 高さ方向の連続値+      bms   = [ 0.5 + frac (fromIntegral i * 1.3) | i <- [0 .. nB - 1] ]  -- size 値+      bdf   = [ ("x", NumData (V.fromList bxs))+              , ("y", NumData (V.fromList bys))+              , ("z", NumData (V.fromList bzs))+              , ("v", NumData (V.fromList bvs))+              , ("m", NumData (V.fromList bms)) ] :: [(Text, ColData)]+      bspec = layer3D (scatter3D "x" "y" "z"+                        <> colorContinuousBy3D "v"+                        <> sizeBy3D "m" <> sizeRange3D (3, 16))+           <> camera (defaultCameraZUp 4)+           <> axisTitles3D "x" "y" "height"+           <> width3DV 680 <> height3DV 600+           <> title3D "連続色 + size マップ bubble"+  saveSVG3DBound (outDir <> "/scatter3d-value-bubble.svg") (bdf |>> bspec)+  putStrLn "  連続色 + bubble → scatter3d-value-bubble.svg"++  -- ===========================================================================+  -- Case 15 (Phase 25 A4): 半透明 surface。 Case 12 と同じドーム + 点群だが+  -- surface を alpha3D 0.45 にして、 膜の奥の点が「透けて薄く」見える+  -- (A8 の不透明膜は奥の点を完全に隠す。 alpha で中間表現が可能に)。+  -- ===========================================================================+  let domeA  = surfaceFromFunction 30 30 (-2) 2 (-2) 2+                 (\x y -> 1.2 - 0.3 * (x*x + y*y))+      obsA   = [ Point3 (cos t * 1.2) (sin t * 1.2) (0.77 + 0.6 * sin (t * 2))+               | k <- [0 .. 23 :: Int], let t = fromIntegral k / 24 * 2 * pi ]+      alphaSpec = layer3D (surface3DGrid (sf3Grid domeA)+                            <> xRange3D (-2, 2) <> yRange3D (-2, 2)+                            <> colormap3D <> alpha3D 0.45)+               <> layer3D (scatter3DPoints obsA <> color3D (fromHex "#d62728") <> size3D 6)+               <> camera (cameraIso 5)+               <> title3D "半透明 surface (alpha 0.45): 奥の点が透ける"+  saveSVG3D (outDir <> "/surface3d-a4-translucent.svg") alphaSpec+  putStrLn "  半透明 surface → surface3d-a4-translucent.svg"++  -- ===========================================================================+  -- Case 16 (Phase 25 A5): DoE 風 3D 棒グラフ + 誤差棒。 2 因子 (3x3) の応答を+  -- 直方体 bar で、 標準誤差を z 方向 ±se の誤差棒で。 df |>> (bar3D + errorBar3D)。+  -- ===========================================================================+  let factorX = [0, 1, 2] :: [Double]+      factorY = [0, 1, 2] :: [Double]+      cellsXY = [ (fx, fy) | fy <- factorY, fx <- factorX ]+      respAt fx fy = 4 + 2 * fx + 1.5 * fy - 0.4 * fx * fy   -- 応答 (交互作用あり)+      seAt   fx fy = 0.4 + 0.15 * (fx + fy)                  -- ばらつき (右奥ほど大)+      bardf = [ ("fx", NumData (V.fromList [ fx       | (fx, _ ) <- cellsXY ]))+              , ("fy", NumData (V.fromList [ fy       | (_,  fy) <- cellsXY ]))+              , ("resp", NumData (V.fromList [ respAt fx fy | (fx, fy) <- cellsXY ]))+              , ("se",   NumData (V.fromList [ seAt   fx fy | (fx, fy) <- cellsXY ])) ]+              :: [(Text, ColData)]+      barSpec = layer3D (bar3D "fx" "fy" "resp"+                          <> color3D (fromHex "#5b9bd5") <> barWidth3D 0.08+                          <> errorBar3D "se")+             <> camera (cameraIso 5)+             <> axisTitles3D "factor X" "factor Y" "response"+             <> width3DV 680 <> height3DV 600+             <> title3D "DoE 応答の 3D 棒 + 標準誤差棒"+  saveSVG3DBound (outDir <> "/bar3d-doe-errorbar.svg") (bardf |>> barSpec)+  putStrLn "  DoE 3D 棒 + 誤差棒 → bar3d-doe-errorbar.svg"++  -- ===========================================================================+  -- Case 17 (Phase 25 A6): 壁面 pane + gridline (mplot3d 標準の axes box)。+  -- 既定 ON。 同じ surface+点群を pane ON / OFF で並べ、 奥 3 壁の薄灰 pane ++  -- 白格子線が背面に出ること (回転追従) を確認。 PNG も出す (目視用)。+  -- ===========================================================================+  let domeP  = surfaceFromFunction 30 30 (-2) 2 (-2) 2+                 (\x y -> 1.2 - 0.3 * (x*x + y*y))+      obsP   = [ Point3 (cos t * 1.4) (sin t * 1.4) (0.3 + 0.6 * sin (t * 2))+               | k <- [0 .. 29 :: Int], let t = fromIntegral k / 30 * 2 * pi ]+      paneBase = layer3D (surface3DGrid (sf3Grid domeP)+                           <> xRange3D (-2, 2) <> yRange3D (-2, 2) <> colormap3D)+              <> layer3D (scatter3DPoints obsP <> color3D (fromHex "#d62728") <> size3D 5)+              <> camera (cameraIso 5)+              <> axisTitles3D "x" "y" "z"+      paneOnSpec  = paneBase+                 <> title3D "壁面 pane ON (mplot3d 標準・既定): 背面 3 壁 + 白格子"+      paneOffSpec = paneBase <> pane3D False+                 <> title3D "壁面 pane OFF: 従来の cube wireframe + tick のみ"+  saveSVG3D (outDir <> "/surface3d-a6-pane-on.svg")  paneOnSpec+  savePNG3D (outDir <> "/surface3d-a6-pane-on.png")  paneOnSpec+  saveSVG3D (outDir <> "/surface3d-a6-pane-off.svg") paneOffSpec+  putStrLn "  壁面 pane ON/OFF → surface3d-a6-pane-{on,off}.svg (+ on.png)"++  -- ===========================================================================+  -- Case 18 (Phase 25 A7): 3D テキスト注釈 (text3D / annotate3D)。 ドーム surface の+  -- 頂点と 3 コーナーにラベルを置く。 annotate3D で頂点を赤・大きめに強調。+  -- ===========================================================================+  let domeT = surfaceFromFunction 30 30 (-2) 2 (-2) 2+                (\x y -> 1.2 - 0.3 * (x*x + y*y))+      annSpec = layer3D (surface3DGrid (sf3Grid domeT)+                          <> xRange3D (-2, 2) <> yRange3D (-2, 2) <> colormap3D)+             <> layer3D (annotate3D (Point3 0 0 1.2) "peak"+                          <> color3D (fromHex "#d62728") <> size3D 14)+             <> layer3D (text3DPoints [ (Point3 (-2) (-2) (-1.2), "(-2,-2)")+                                 , (Point3 2 2 (-1.2), "(2,2)")+                                 , (Point3 (-2) 2 (-1.2), "(-2,2)") ]+                          <> size3D 10)+             <> camera (cameraIso 5)+             <> axisTitles3D "x" "y" "z"+             <> title3D "3D テキスト注釈: 頂点 + コーナーラベル"+  saveSVG3D (outDir <> "/surface3d-a7-annotate.svg") annSpec+  savePNG3D (outDir <> "/surface3d-a7-annotate.png") annSpec   -- 日本語ラベル可+  putStrLn "  3D テキスト注釈 → surface3d-a7-annotate.svg (+ png)"++  -- ===========================================================================+  -- Case 19 (Phase 25 A8): log-z 軸。 z が decade (1..1000) を跨ぐ指数的データを+  -- 片対数 (logScale3D False False True) で。 z tick は 10 の冪・等間隔に並ぶ。+  -- ===========================================================================+  let logPts = [ Point3 (fromIntegral k / 5) (fromIntegral k / 5)+                        (10 ** (fromIntegral k / 5 - 1))   -- z: 0.1 .. 1000+               | k <- [0 .. 20 :: Int] ]+      logSpec = layer3D (line3DPoints logPts <> color3D (fromHex "#1f77b4"))+             <> layer3D (scatter3DPoints logPts <> color3D (fromHex "#d62728") <> size3D 5)+             <> logScale3D False False True+             <> camera (cameraIso 5)+             <> axisTitles3D "x" "y" "z (log)"+             <> title3D "log-z 軸 (片対数): z tick が 10 の冪で等間隔"+  saveSVG3D (outDir <> "/scatter3d-a8-logz.svg") logSpec+  savePNG3D (outDir <> "/scatter3d-a8-logz.png") logSpec+  putStrLn "  log-z 軸 → scatter3d-a8-logz.svg (+ png)"++  -- ===========================================================================+  -- Case 20 (Phase 25 A8): box アスペクト。 同じドームを x 方向に 1.6 倍・z を+  -- 0.6 倍に潰した box で (xAspect3D / zAspect3D)。 mplot3d set_box_aspect 相当。+  -- ===========================================================================+  let domeAsp = surfaceFromFunction 30 30 (-2) 2 (-2) 2+                  (\x y -> 1.2 - 0.3 * (x*x + y*y))+      aspSpec = layer3D (surface3DGrid (sf3Grid domeAsp)+                          <> xRange3D (-2, 2) <> yRange3D (-2, 2) <> colormap3D)+             <> xAspect3D 1.6 <> zAspect3D 0.6+             <> camera (cameraIso 5)+             <> axisTitles3D "x" "y" "z"+             <> title3D "box アスペクト: x×1.6 / z×0.6"+  saveSVG3D (outDir <> "/surface3d-a8-aspect.svg") aspSpec+  savePNG3D (outDir <> "/surface3d-a8-aspect.png") aspSpec+  putStrLn "  box アスペクト → surface3d-a8-aspect.svg (+ png)"++  -- ===========================================================================+  -- Case 21 (Phase 25 A9 統合): クラスタ分類の 3D 散布。 群色 (colorBy3D) ++  -- 各クラスタ重心に text3D ラベル + pane。 「分類結果の 3D 可視化」 の定番。+  -- ===========================================================================+  let cluster cx cy cz n = [ Point3 (cx + 0.5 * cos t) (cy + 0.5 * sin t) (cz + 0.4 * cos (2*t))+                           | i <- [0 .. n-1 :: Int], let t = fromIntegral i / fromIntegral n * 2 * pi ]+      cA = cluster 0    0    0    14+      cB = cluster 2.2  1.5  1.0  14+      cC = cluster 1.0  2.5 (-1.0) 14+      clusterDf = [ ("x", NumData (V.fromList [ x | Point3 x _ _ <- cA++cB++cC ]))+                  , ("y", NumData (V.fromList [ y | Point3 _ y _ <- cA++cB++cC ]))+                  , ("z", NumData (V.fromList [ z | Point3 _ _ z <- cA++cB++cC ]))+                  , ("cluster", TxtData (V.fromList (replicate 14 "A" ++ replicate 14 "B" ++ replicate 14 "C"))) ]+                  :: [(Text, ColData)]+      clusterSpec = layer3D (scatter3D "x" "y" "z" <> colorBy3D "cluster" <> size3D 5)+                 <> layer3D (text3DPoints [ (Point3 0 0 0.7, "A"), (Point3 2.2 1.5 1.7, "B")+                                    , (Point3 1.0 2.5 (-0.3), "C") ]+                              <> size3D 16 <> color3D (fromHex "#222222"))+                 <> camera (cameraIso 5)+                 <> axisTitles3D "PC1" "PC2" "PC3"+                 <> title3D "クラスタ分類の 3D 散布 (群色 + 重心ラベル)"+  saveSVG3DBound (outDir <> "/scatter3d-a9-clusters.svg") (clusterDf |>> clusterSpec)+  putStrLn "  クラスタ散布 → scatter3d-a9-clusters.svg"++  -- ===========================================================================+  -- Case 22 (Phase 25 A9 統合): 濃度-応答の bubble。 連続色 (colorContinuousBy3D) ++  -- size マップ (sizeBy3D) + log-x 軸 (濃度が decade を跨ぐ)。 用量反応の定番。+  -- ===========================================================================+  let doses  = [ 10 ** (fromIntegral k / 4 - 1) | k <- [0 .. 12 :: Int] ]  -- 0.1 .. ~1000+      respD  = [ 100 / (1 + (50 / d)) | d <- doses ]                        -- Hill 風+      doseDf = [ ("dose", NumData (V.fromList doses))+               , ("time", NumData (V.fromList [ fromIntegral k | k <- [0 .. 12 :: Int] ]))+               , ("resp", NumData (V.fromList respD))+               , ("effect", NumData (V.fromList respD)) ]+               :: [(Text, ColData)]+      doseSpec = layer3D (scatter3D "dose" "time" "resp"+                           <> colorContinuousBy3D "effect" <> sizeBy3D "effect" <> sizeRange3D (5, 22))+              <> logScale3D True False False        -- x (濃度) を log+              <> camera (cameraIso 5)+              <> axisTitles3D "dose (log)" "time" "response"+              <> title3D "用量反応 bubble (連続色 + size + log-dose)"+  saveSVG3DBound (outDir <> "/scatter3d-a9-dose-bubble.svg") (doseDf |>> doseSpec)+  putStrLn "  用量反応 bubble → scatter3d-a9-dose-bubble.svg"++  -- ===========================================================================+  -- Case 23 (Phase 25 A9 統合): DoE 応答の 3D 棒 + 各セルに応答値ラベル (text3D)。+  -- bar3d + 誤差棒 + 数値注釈 + 軸名。 実務的な要因配置実験のまとめ図。+  -- ===========================================================================+  let cells   = [ (fx, fy) | fy <- [0,1,2::Double], fx <- [0,1,2::Double] ]+      respB fx fy = 4 + 2*fx + 1.5*fy - 0.4*fx*fy+      barDf2  = [ ("fx", NumData (V.fromList [ fx | (fx,_) <- cells ]))+                , ("fy", NumData (V.fromList [ fy | (_,fy) <- cells ]))+                , ("r",  NumData (V.fromList [ respB fx fy | (fx,fy) <- cells ]))+                , ("se", NumData (V.fromList [ 0.3 + 0.1*(fx+fy) | (fx,fy) <- cells ])) ]+                :: [(Text, ColData)]+      labels23 = [ (Point3 fx fy (respB fx fy + 1.2), tShow (respB fx fy)) | (fx,fy) <- cells ]+      barSpec2 = layer3D (bar3D "fx" "fy" "r" <> color3D (fromHex "#5b9bd5") <> barWidth3D 0.08 <> errorBar3D "se")+              <> layer3D (text3DPoints labels23 <> size3D 10 <> color3D (fromHex "#222222"))+              <> camera (cameraIso 5)+              <> axisTitles3D "factor X" "factor Y" "response"+              <> width3DV 700 <> height3DV 620+              <> title3D "DoE 応答 3D 棒 + 誤差棒 + 値ラベル"+  saveSVG3DBound (outDir <> "/bar3d-a9-doe-labeled.svg") (barDf2 |>> barSpec2)+  putStrLn "  DoE 棒 + 値ラベル → bar3d-a9-doe-labeled.svg"++  -- ===========================================================================+  -- Case 24 (Phase 26 A4): 3D stem (lollipop)。 離散系列を底面 z0 への細い垂線 ++  -- 先端マーカーで。 1 群は単色 stem3D、 もう 1 群は群色 + 別 baseZ で重畳。+  -- ===========================================================================+  let stemXs  = [0,0.5..3] :: [Double]+      stemY1  = 1.0 :: Double+      stemY2  = 2.2 :: Double+      respS x = 2 + 1.5 * sin (x * 1.3)+      stemDf  = [ ("x",  NumData (V.fromList stemXs))+                , ("y1", NumData (V.fromList (replicate (length stemXs) stemY1)))+                , ("z1", NumData (V.fromList (map respS stemXs)))+                , ("y2", NumData (V.fromList (replicate (length stemXs) stemY2)))+                , ("z2", NumData (V.fromList (map (\x -> 1 + cos (x * 0.9)) stemXs))) ]+                :: [(Text, ColData)]+      stemSpec = layer3D (stem3D "x" "y1" "z1" <> color3D (fromHex "#d62728") <> size3D 5 <> stemBaseZ 0)+              <> layer3D (stem3D "x" "y2" "z2" <> color3D (fromHex "#1f77b4") <> size3D 5 <> stemBaseZ 0)+              <> camera (cameraIso 5)+              <> axisTitles3D "t" "series" "value"+              <> width3DV 700 <> height3DV 600+              <> title3D "3D stem (lollipop) — 2 系列"+  saveSVG3DBound (outDir <> "/stem3d-a4.svg") (stemDf |>> stemSpec)+  putStrLn "  3D stem → stem3d-a4.svg"++  -- ===========================================================================+  -- Case 25 (Phase 26 A3): 3D vector field (quiver3D)。 3D 格子点に渦巻き状の+  -- 流れ場ベクトルの矢印 (投影後 2D 矢じり)。 magnitude で色は付けず単色。+  -- ===========================================================================+  let qGrid = [ Point3 gx gy gz | gx <- [-1,0,1], gy <- [-1,0,1], gz <- [-1,0,1::Double] ]+      qVecF (Point3 x y z) = Vec3 (-y) x (0.3 * z)   -- z 軸まわりの回転 + 上昇+      qItems = [ (p, qVecF p) | p <- qGrid ]+      quiverSpec = layer3D (quiver3D qItems <> color3D (fromHex "#1f77b4") <> vecScale3D 1.0)+                <> camera (cameraIso 5)+                <> axisTitles3D "x" "y" "z"+                <> width3DV 680 <> height3DV 620+                <> title3D "3D vector field (quiver3D) — 回転流"+  saveSVG3D (outDir <> "/quiver3d-a3.svg") quiverSpec+  putStrLn "  3D vector field → quiver3d-a3.svg"++  -- ===========================================================================+  -- Case 26 (Phase 26 A5): trisurf。 不規則 (非 grid) なサンプル点を Delaunay+  -- 三角分割して曲面化。 z 連続色 (colormap3D)。 散布点位置は擬似乱数で散らす。+  -- ===========================================================================+  let triN = 90 :: Int+      triPts = [ Point3 px py (peak px py)+               | i <- [0 .. triN - 1]+               , let px = frac (fromIntegral i * 0.6131) * 4 - 2+               , let py = frac (fromIntegral i * 0.2971 + 0.13) * 4 - 2+               , let peak x y = exp (negate ((x - 0.3)**2 + (y + 0.2)**2) / 1.2) ]+      triSpec = layer3D (trisurf triPts <> colormap3D)+             <> camera (cameraIso 5)+             <> axisTitles3D "x" "y" "z"+             <> width3DV 680 <> height3DV 620+             <> title3D "trisurf — 不規則点群を Delaunay 三角分割"+  saveSVG3D (outDir <> "/trisurf-a5.svg") triSpec+  putStrLn "  trisurf → trisurf-a5.svg"++  putStrLn ""+  putStrLn "Wrote 31 SVGs/PNG/PDF to design/3d/"++-- | Double を小数 1 桁の Text に (= text3D 値ラベル用)。+tShow :: Double -> Text+tShow x = T.pack (show (fromIntegral (round (x * 10) :: Int) / 10.0 :: Double))++-- | 簡易 hash で擬似乱数 (= 決定論的、 seed なし)。+frac :: Double -> Double+frac x = let y = x - fromIntegral (floor x :: Int) in if y < 0 then y + 1 else y
+ examples/PrintJson.hs view
@@ -0,0 +1,50 @@+-- | 各 3D 型の JSON 出力を確認する (= Phase 5 A4 PS Argonaut codec 作成前の wire format 確定用)。+{-# LANGUAGE OverloadedStrings #-}+module Main where++import           Data.Aeson                  (encode)+import qualified Data.ByteString.Lazy.Char8  as BL+import           Graphics.Hgg.ThreeD.Axes+import           Graphics.Hgg.ThreeD.Line+import           Graphics.Hgg.ThreeD.Scatter+import           Graphics.Hgg.ThreeD.Spec+import           Graphics.Hgg.Color          (fromHex)+import           Graphics.Hgg.ThreeD.Surface+import           Graphics.Hgg.ThreeD.Types++main :: IO ()+main = do+  putStrLn "-- Point3 --"+  BL.putStrLn $ encode (Point3 1 2 3)+  putStrLn "-- Vec3 --"+  BL.putStrLn $ encode (Vec3 0 0 1)+  putStrLn "-- Mat4 --"+  BL.putStrLn $ encode (Mat4 1 0 0 0  0 1 0 0  0 0 1 0  0 0 0 1)+  putStrLn "-- Camera3D --"+  BL.putStrLn $ encode (defaultCameraZUp 3)+  putStrLn "-- Projection3D (Perspective) --"+  BL.putStrLn $ encode defaultPerspective+  putStrLn "-- Projection3D (Orthographic) --"+  BL.putStrLn $ encode (Orthographic 1 1 0.1 100)+  putStrLn "-- Axes3D --"+  BL.putStrLn $ encode defaultAxes3D+  putStrLn "-- Scatter3D --"+  BL.putStrLn $ encode (defaultScatter3D [Point3 0 0 0, Point3 1 1 1])+  putStrLn "-- Line3D --"+  BL.putStrLn $ encode (defaultLine3D [Point3 0 0 0, Point3 1 1 1])+  putStrLn "-- Wireframe3D --"+  BL.putStrLn $ encode (defaultWireframe3D [Point3 0 0 0, Point3 1 0 0] [(0,1)])+  putStrLn "-- Surface3D --"+  BL.putStrLn $ encode (defaultSurface3D [[0,1],[1,0]])+  putStrLn "-- Mark3DKind --"+  BL.putStrLn $ encode M3Scatter+  putStrLn "-- Layer3D (scatter + color + size) --"+  BL.putStrLn $ encode (scatter3DPoints [Point3 0 0 0] <> color3D (fromHex "#56B4E9") <> size3D 6)+  putStrLn "-- VisualSpec3D --"+  BL.putStrLn $ encode $+       purePlot3D+    <> layer3D (scatter3DPoints [Point3 0 0 0, Point3 1 1 1] <> color3D (fromHex "#56B4E9") <> size3D 6)+    <> camera     (defaultCameraZUp 3)+    <> projection defaultPerspective+    <> axes3D     defaultAxes3D+    <> title3D    "demo"
+ hgg-3d.cabal view
@@ -0,0 +1,112 @@+cabal-version:      3.0+name:               hgg-3d+version:            0.1.0.0+extra-doc-files:    CHANGELOG.md+synopsis:           3D plot (CPU projection) for hgg; works with all 2D backends+description:+  A 3D plotting library for hgg, comparable to matplotlib's mplot3d.+  CPU-based projection plus the painter's algorithm let the existing hgg+  2D backends (SVG / PDF / Rasterific) render 3D scatter / line /+  wireframe / surface plots as-is.+  .+  No WebGL required — pure Haskell end to end. Interactive 3D (camera+  rotation etc.) in web environments is handled by a separate+  PureScript / WebGL frontend outside this package, connected through the+  pre-bundled @data/webgl-spec.js@.+license:            BSD-3-Clause+homepage:           https://github.com/frenzieddoll/hgg+license-file:       LICENSE+author:             Toshiaki Honda+maintainer:         frenzieddoll@gmail.com+copyright:          2026 Aelysce Project (Toshiaki Honda)+category:           Graphics+build-type:         Simple+data-files:         data/webgl-spec.js++common warnings+  ghc-options:        -Wall -Wcompat -Widentities -Wincomplete-record-updates+                      -Wincomplete-uni-patterns -Wpartial-fields+                      -Wredundant-constraints++library+  import:           warnings+  exposed-modules:  Graphics.Hgg.ThreeD+                    Graphics.Hgg.ThreeD.Bound+                    Graphics.Hgg.ThreeD.Types+                    Graphics.Hgg.ThreeD.Projection+                    Graphics.Hgg.ThreeD.Axes+                    Graphics.Hgg.ThreeD.Scatter+                    Graphics.Hgg.ThreeD.Line+                    Graphics.Hgg.ThreeD.Surface+                    Graphics.Hgg.ThreeD.Delaunay+                    Graphics.Hgg.ThreeD.Bar+                    Graphics.Hgg.ThreeD.Spec+                    Graphics.Hgg.ThreeD.Browser+                    Graphics.Hgg.ThreeD.Easy+  other-modules:    Paths_hgg_3d+  autogen-modules:  Paths_hgg_3d+  hs-source-dirs:   src+  build-depends:    base                 >= 4.17 && < 5+                  , text                 >= 2.0  && < 2.2+                  , containers           >= 0.6  && < 0.8+                  , vector               >= 0.13 && < 0.14+                  , hgg-core        ^>= 0.1+                  , hgg-svg         ^>= 0.1+                  , hgg-pdf         ^>= 0.1+                  , hgg-rasterific  ^>= 0.1+                  , hgg-frame       ^>= 0.1+                  , aeson                >= 2.1  && < 2.3+                  , bytestring           >= 0.11 && < 0.13+                  , directory            >= 1.3  && < 1.4+                  , filepath             >= 1.4  && < 1.6+                  , process              >= 1.6  && < 1.7+  default-language: Haskell2010++executable plot3d-demo+  import:           warnings+  main-is:          Plot3DDemo.hs+  hs-source-dirs:   examples+  build-depends:    base+                  , text+                  , vector+                  , directory+                  , hgg-3d+                  , hgg-core+                  , hgg-svg+                  , hgg-frame+  default-language: Haskell2010++executable browser-3d-demo+  import:           warnings+  main-is:          Browser3DDemo.hs+  hs-source-dirs:   examples+  build-depends:    base+                  , hgg-3d+                  , hgg-core+  default-language: Haskell2010++executable plot3d-printjson+  import:           warnings+  main-is:          PrintJson.hs+  hs-source-dirs:   examples+  build-depends:    base+                  , aeson+                  , bytestring+                  , hgg-3d+                  , hgg-core+  default-language: Haskell2010++test-suite hgg-3d-tests+  import:           warnings+  type:             exitcode-stdio-1.0+  main-is:          Spec.hs+  hs-source-dirs:   test+  build-depends:    base+                  , text+                  , vector+                  , directory+                  , hgg-3d+                  , hgg-core+                  , hgg-frame+                  , hspec                >= 2.10 && < 2.12+  default-language: Haskell2010
+ src/Graphics/Hgg/ThreeD.hs view
@@ -0,0 +1,29 @@+-- |+-- Module      : Graphics.Hgg.ThreeD+-- Description : hgg-3d 公開エントリ (= 主要 API の再エクスポート)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- 3D plot library (CPU projection + painter's algorithm)。 既存の hgg+-- 2D backend (SVG / PDF / Rasterific) でそのまま使える。+--+-- 主要型と関数を本 module で再エクスポート。 細部は子 module を参照:+--+-- * "Graphics.Hgg.ThreeD.Types" — Point3 / Vec3 / Mat4 / Camera3D / Projection3D+-- * "Graphics.Hgg.ThreeD.Projection" — project3D / lookAt 等の純粋関数+-- * "Graphics.Hgg.ThreeD.Axes" — Axes3D (= 立方体 wireframe + 3 軸 tick)+-- * "Graphics.Hgg.ThreeD.Scatter" — MScatter3D+-- * "Graphics.Hgg.ThreeD.Line" — MLine3D / MWireframe3D+-- * "Graphics.Hgg.ThreeD.Surface" — MSurface3D+-- * "Graphics.Hgg.ThreeD.Easy" — matplotlib mplot3d 風 helper+module Graphics.Hgg.ThreeD+  ( -- * 中核型 (= A3 で実装)+    module Graphics.Hgg.ThreeD.Types+    -- * Projection (= A3 で実装)+  , module Graphics.Hgg.ThreeD.Projection+    -- * Axes3D (= A4 で実装)+    -- * Scatter / Line / Wireframe / Surface (= A5-A7 で実装)+  ) where++import Graphics.Hgg.ThreeD.Types+import Graphics.Hgg.ThreeD.Projection
+ src/Graphics/Hgg/ThreeD/Axes.hs view
@@ -0,0 +1,321 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Axes+-- Description : 3D 軸 (立方体 wireframe + 3 軸 tick / label) (Phase 3 A4)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- mplot3d 風の axes: data bounding box (= xMin..zMax) を立方体 wireframe で+-- 囲み、 各軸に等間隔の tick を 1 列描画。 ラベルは tick の少し外側へ。+--+-- 設計判断:+--+--   * Phase 3 では「奥側面のみ」 を厳密判定せず、 12 辺全て描画 (= 多少視認性+--     落ちるが mplot3d 流の hairy wireframe より単純)+--   * tick は (xMin..xMax 等) の niceTicks3D で 5 点 default+--   * label: tick 値 + 軸名 (= "x"/"y"/"z")+--   * 出力は '[Primitive]' (= hgg-core の 2D primitive)、 既存 backend で描画+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+module Graphics.Hgg.ThreeD.Axes+  ( Axes3D (..)+  , defaultAxes3D+  , niceTicks3D+  , logTicks3D+  , axisTicks3D+  , renderAxes3D+  , renderAxes3DWith+  , renderAxes3DWithLabels+    -- * 壁面 pane + gridline (Phase 25 A6)+  , PaneStyle3D (..)+  , defaultPaneStyle3D+  , renderAxes3DPanes+  ) where++import           Data.Aeson                      (FromJSON, ToJSON)+import           Data.List                       (minimumBy)+import           Data.Ord                        (comparing)+import           Data.Text                       (Text)+import qualified Data.Text                       as T+import           GHC.Generics                    (Generic)++import           Graphics.Hgg.Render             (FillStyle (..), LineStyle (..),+                                                  PathSegment (..), Point (..),+                                                  Primitive (..),+                                                  TextAnchor (..),+                                                  TextStyle (..))+import           Graphics.Hgg.ThreeD.Projection  (Projected (..), Viewport,+                                                  project3D)+import           Graphics.Hgg.ThreeD.Types++-- | 3D 軸の bounding box + tick 数。+data Axes3D = Axes3D+  { axesXMin  :: !Double+  , axesXMax  :: !Double+  , axesYMin  :: !Double+  , axesYMax  :: !Double+  , axesZMin  :: !Double+  , axesZMax  :: !Double+  , axesNTicks :: !Int       -- ^ 軸あたり tick 数 (= default 5)+  , axesXLog  :: !Bool       -- ^ Phase 25 A8: x 軸を log scale に (既定 False)+  , axesYLog  :: !Bool       -- ^ Phase 25 A8: y 軸を log scale に (既定 False)+  , axesZLog  :: !Bool       -- ^ Phase 25 A8: z 軸を log scale に (既定 False)+  } deriving (Show, Eq, Generic)+instance ToJSON   Axes3D+instance FromJSON Axes3D++-- | 単位 cube (= [-1, 1]^3) + tick 5。+defaultAxes3D :: Axes3D+defaultAxes3D = Axes3D+  { axesXMin = -1, axesXMax = 1+  , axesYMin = -1, axesYMax = 1+  , axesZMin = -1, axesZMax = 1+  , axesNTicks = 5+  , axesXLog = False, axesYLog = False, axesZLog = False+  }++-- | 等間隔 tick 位置を 'n' 個生成。 niceNumbers アルゴリズム ではなく素朴な等間隔+-- (= mplot3d 風)。+niceTicks3D :: Int -> Double -> Double -> [Double]+niceTicks3D n lo hi+  | n <= 1 || abs (hi - lo) < 1e-12 = [lo]+  | otherwise =+      let step = (hi - lo) / fromIntegral (n - 1)+      in [ lo + step * fromIntegral i | i <- [0 .. n - 1] ]++-- | Phase 25 A8: log scale 軸の tick 位置 = @[lo, hi]@ 区間内の 10 の冪+-- (decade)。 端 (lo/hi) も含め、 冪が 1 個以下なら端 2 点で補う。 値は正前提+-- (lo<=0 は 1e-12 に clamp)。 軸ラベルは元の値のまま 'formatNum' で出る。+logTicks3D :: Double -> Double -> [Double]+logTicks3D lo0 hi0 =+  let lo = max 1e-12 lo0+      hi = max (lo * 10) hi0+      -- ±1e-9 の余裕で境界の冪 (log10 1000 = 2.9999.. 等の floor 漏れ) を拾う。+      k0 = ceiling (logBase 10 lo - 1e-9) :: Int+      k1 = floor   (logBase 10 hi + 1e-9) :: Int+      -- 10 ^^ k は反復乗算で exact (10 ** 3 = 1000.0000..1 を避ける)。+      decades = [ 10 ^^ k | k <- [k0 .. k1] ]+      inRange = filter (\v -> v >= lo * (1 - 1e-9) && v <= hi * (1 + 1e-9)) decades+  in case inRange of+       (_:_:_) -> inRange+       _       -> [lo, hi]   -- 冪が足りなければ端 2 点++-- | Phase 25 A8: log フラグで tick 生成を切替える (log = 'logTicks3D'・線形 =+-- 'niceTicks3D')。 両 axes renderer が共有。+axisTicks3D :: Bool -> Int -> Double -> Double -> [Double]+axisTicks3D isLog n lo hi+  | isLog     = logTicks3D lo hi+  | otherwise = niceTicks3D n lo hi++-- ===========================================================================+-- 描画+-- ===========================================================================++-- | Axes3D を 2D Primitive 列にレンダリング (= cube wireframe + 3 軸 tick + label)。+renderAxes3D :: Camera3D -> Projection3D -> Viewport -> Axes3D -> [Primitive]+renderAxes3D = renderAxes3DWith id++-- | Phase 24 A3: 投影前に座標変換 @f@ を合成する版。 @f@ に正規化+-- (データ bbox → [-1,1]^3) を渡すと、 **tick ラベルは元のデータ値のまま**+-- 形状だけ view box に収まる (saveSVG3D の正規化 pipeline 用)。+renderAxes3DWith :: (Point3 -> Point3)+                 -> Camera3D -> Projection3D -> Viewport -> Axes3D -> [Primitive]+renderAxes3DWith f = renderAxes3DWithLabels f ("x", "y", "z")++-- | Phase 24 A8: 軸名を任意指定する版 ('renderAxes3DWith' = @("x","y","z")@)。+renderAxes3DWithLabels :: (Point3 -> Point3) -> (Text, Text, Text)+                       -> Camera3D -> Projection3D -> Viewport -> Axes3D -> [Primitive]+renderAxes3DWithLabels f (xName, yName, zName) cam proj vp ax =+  let project = project3D cam proj vp . f+      toScreen p = let Projected sx sy _ = project p in Point sx sy++      xLo = axesXMin ax; xHi = axesXMax ax+      yLo = axesYMin ax; yHi = axesYMax ax+      zLo = axesZMin ax; zHi = axesZMax ax+      corners =+        [ Point3 xLo yLo zLo  -- 0+        , Point3 xHi yLo zLo  -- 1+        , Point3 xHi yHi zLo  -- 2+        , Point3 xLo yHi zLo  -- 3+        , Point3 xLo yLo zHi  -- 4+        , Point3 xHi yLo zHi  -- 5+        , Point3 xHi yHi zHi  -- 6+        , Point3 xLo yHi zHi  -- 7+        ]+      pts2d = map toScreen corners++      edges_ =+        [ (0,1), (1,2), (2,3), (3,0)+        , (4,5), (5,6), (6,7), (7,4)+        , (0,4), (1,5), (2,6), (3,7)+        ]++      cubeStyle = LineStyle "#bbbbbb" 1.0 []+      cubeLines =+        [ PLine (pts2d !! i) (pts2d !! j) cubeStyle+        | (i, j) <- edges_ ]++      xTicks = axisTicks3D (axesXLog ax) (axesNTicks ax) xLo xHi+      yTicks = axisTicks3D (axesYLog ax) (axesNTicks ax) yLo yHi+      zTicks = axisTicks3D (axesZLog ax) (axesNTicks ax) zLo zHi++      tickStyle = LineStyle "#888888" 1.0 []+      -- ★軸ごとのレンジに比例した offset (正規化 pipeline 対応)。 旧実装は+      -- 全軸共通 @0.04 * max(range)@ をデータ空間で使っていたため、 レンジが+      -- 軸間で大きく異なるデータ (例: z=収率 40・x/y=±1.4) で z レンジに+      -- 引っ張られ、 正規化後に tick/軸名が cube から大きく外れていた。+      -- 各 offset を「その方向の軸レンジ」 に比例させると、 正規化後 (各軸+      -- [-1,1]) で一律 0.08 になる。 均一 cube ([-1,1]^3) では旧実装と同値。+      offX = 0.04 * (xHi - xLo)   -- x 方向の offset 量+      offY = 0.04 * (yHi - yLo)   -- y 方向の offset 量++      xMid = (xLo + xHi) / 2; yMid = (yLo + yHi) / 2; zMid = (zLo + zHi) / 2++      -- ★camera-aware なエッジ選択 (Phase 24 A9)。 旧実装は tick/軸名を固定+      -- エッジ (x→yLo・y→xHi・z→(xHi,yLo)) に置いていたが、 視点を回すと+      -- 裏エッジに乗ったり、 z tick が最近接の鉛直エッジ (= 図の中央を縦断) に+      -- 重なって読めなかった。 投影スクリーン座標から「前面下エッジ」 と+      -- 「左 silhouette 鉛直エッジ」 を選び、 回転に追従させる。+      screenXY cx cy cz = let Point sx sy = toScreen (Point3 cx cy cz) in (sx, sy)+      midScrY (a1,b1) (a2,b2) = let (_,y1) = screenXY a1 b1 zLo+                                    (_,y2) = screenXY a2 b2 zLo+                                in (y1 + y2) / 2+      -- x tick を載せる y (= yLo/yHi のうち画面で前 = 下にくる方)+      xTickY = if midScrY (xLo,yLo) (xHi,yLo) >= midScrY (xLo,yHi) (xHi,yHi)+                 then yLo else yHi+      inYx   = if xTickY == yLo then offY else negate offY   -- cube 内向き+      -- y tick を載せる x (= xLo/xHi のうち前)+      yTickX = if midScrY (xLo,yLo) (xLo,yHi) >= midScrY (xHi,yLo) (xHi,yHi)+                 then xLo else xHi+      inXy   = if yTickX == xLo then offX else negate offX+      -- z tick/軸名を載せる鉛直エッジ = 画面で最も左の底コーナー (silhouette・+      -- 右の colorbar と干渉しない)+      (zcx, zcy) = minimumBy (comparing (\(cx,cy) -> fst (screenXY cx cy zLo)))+                     [(xLo,yLo), (xHi,yLo), (xHi,yHi), (xLo,yHi)]+      inXz = if zcx < xMid then offX else negate offX        -- cube 内向き+      inYz = if zcy < yMid then offY else negate offY++      ts = TextStyle "#444444" 9 "sans-serif" AnchorMiddle 0 "normal" False++      xTickPrims = concat+        [ let p1 = toScreen (Point3 xi xTickY zLo)+              p2 = toScreen (Point3 xi (xTickY + inYx) zLo)+              Point lx ly = toScreen (Point3 xi (xTickY - inYx) zLo)+          in [ PLine p1 p2 tickStyle+             , PText (Point lx (ly + 10)) (formatNum xi) ts ]+        | xi <- xTicks ]++      yTickPrims = concat+        [ let p1 = toScreen (Point3 yTickX yi zLo)+              p2 = toScreen (Point3 (yTickX + inXy) yi zLo)+              Point lx ly = toScreen (Point3 (yTickX - inXy) yi zLo)+          in [ PLine p1 p2 tickStyle+             , PText (Point lx (ly + 4)) (formatNum yi) ts ]+        | yi <- yTicks ]++      zTickPrims = concat+        [ let p1 = toScreen (Point3 zcx zcy zi)+              p2 = toScreen (Point3 (zcx + inXz) (zcy + inYz) zi)+              Point lx ly = toScreen (Point3 (zcx - inXz) (zcy - inYz) zi)+          in [ PLine p1 p2 tickStyle+             , PText (Point lx (ly + 4)) (formatNum zi) ts ]+        | zi <- zTicks ]++      tsAxis = TextStyle "#333333" 11 "sans-serif" AnchorMiddle 0 "bold" False+      axisNames =+        [ let Point lx ly = toScreen (Point3 xMid (xTickY - inYx * 3) zLo)+          in PText (Point lx (ly + 14)) xName tsAxis+        , let Point lx ly = toScreen (Point3 (yTickX - inXy * 3) yMid zLo)+          in PText (Point lx ly) yName tsAxis+        -- z 軸名も z tick と同じ左 silhouette 鉛直エッジ沿い (中央高さ・外側)。+        , let Point lx ly = toScreen (Point3 (zcx - inXz * 2) (zcy - inYz * 2) zMid)+          in PText (Point lx ly) zName tsAxis+        ]++  in cubeLines <> xTickPrims <> yTickPrims <> zTickPrims <> axisNames++-- ===========================================================================+-- 壁面 pane + gridline (Phase 25 A6 = G5)+-- ===========================================================================++-- | Phase 25 A6: 壁面 pane (= mplot3d の背面 3 壁) のスタイル。+-- pane = 薄灰の塗り面、 gridline = 壁面に引く tick 格子線 (mplot3d 既定は白)。+data PaneStyle3D = PaneStyle3D+  { paneFill    :: !Text    -- ^ 壁面塗り色+  , paneOpacity :: !Double  -- ^ 塗り不透明度 (0..1)+  , paneGrid    :: !Text    -- ^ 格子線色+  } deriving (Show, Eq, Generic)+instance ToJSON   PaneStyle3D+instance FromJSON PaneStyle3D++-- | mplot3d 風 default (薄灰 pane + 白格子線)。+defaultPaneStyle3D :: PaneStyle3D+defaultPaneStyle3D = PaneStyle3D "#eaeaea" 1.0 "#ffffff"++-- | Phase 25 A6: 3 つの「奥壁」 を薄灰 pane で塗り、 各壁に tick 格子線を引く+-- (mplot3d 標準の axes pane)。 出力は @pane 塗り → gridline@ の順なので、+-- 'renderAxes3DWithLabels' (cube wireframe + tick) の **前** に置けば最背面に+-- なる (= データ・wireframe が pane の手前に来る)。+--+-- 奥壁判定: 各軸の対向 2 面のうち、 面中心の投影 depth ('projDepth'、 +1 が奥)+-- が大きい方を奥壁に採る。 視点回転に追従する。 @f@ は 'renderAxes3DWith' と+-- 同じ正規化変換 (データ bbox → [-1,1]^3)。+renderAxes3DPanes :: (Point3 -> Point3) -> PaneStyle3D+                  -> Camera3D -> Projection3D -> Viewport -> Axes3D -> [Primitive]+renderAxes3DPanes f sty cam proj vp ax =+  let project    = project3D cam proj vp . f+      toScreen p = let Projected sx sy _ = project p in Point sx sy+      depthAt p  = let Projected _ _ sz = project p in sz++      xLo = axesXMin ax; xHi = axesXMax ax+      yLo = axesYMin ax; yHi = axesYMax ax+      zLo = axesZMin ax; zHi = axesZMax ax+      xMid = (xLo + xHi) / 2; yMid = (yLo + yHi) / 2; zMid = (zLo + zHi) / 2++      -- 各軸の奥壁座標 (= 面中心 depth が大きい = 奥の面)+      xWall = if depthAt (Point3 xLo yMid zMid) >= depthAt (Point3 xHi yMid zMid)+                then xLo else xHi+      yWall = if depthAt (Point3 xMid yLo zMid) >= depthAt (Point3 xMid yHi zMid)+                then yLo else yHi+      zWall = if depthAt (Point3 xMid yMid zLo) >= depthAt (Point3 xMid yMid zHi)+                then zLo else zHi++      xTicks = axisTicks3D (axesXLog ax) (axesNTicks ax) xLo xHi+      yTicks = axisTicks3D (axesYLog ax) (axesNTicks ax) yLo yHi+      zTicks = axisTicks3D (axesZLog ax) (axesNTicks ax) zLo zHi++      fillSty = FillStyle (paneFill sty) (paneOpacity sty)+      gridSty = LineStyle (paneGrid sty) 1.0 []++      quad a b c d =+        PPath [ MoveTo (toScreen a), LineTo (toScreen b)+              , LineTo (toScreen c), LineTo (toScreen d), ClosePath ]+              fillSty Nothing+      gline a b = PLine (toScreen a) (toScreen b) gridSty++      -- x 奥壁 (x=xWall の y×z 面)+      xPane = quad (Point3 xWall yLo zLo) (Point3 xWall yHi zLo)+                   (Point3 xWall yHi zHi) (Point3 xWall yLo zHi)+      xGrid = [ gline (Point3 xWall yi zLo) (Point3 xWall yi zHi) | yi <- yTicks ]+           <> [ gline (Point3 xWall yLo zi) (Point3 xWall yHi zi) | zi <- zTicks ]+      -- y 奥壁 (y=yWall の x×z 面)+      yPane = quad (Point3 xLo yWall zLo) (Point3 xHi yWall zLo)+                   (Point3 xHi yWall zHi) (Point3 xLo yWall zHi)+      yGrid = [ gline (Point3 xi yWall zLo) (Point3 xi yWall zHi) | xi <- xTicks ]+           <> [ gline (Point3 xLo yWall zi) (Point3 xHi yWall zi) | zi <- zTicks ]+      -- z 奥壁 (z=zWall の x×y 面 = 床 or 天井)+      zPane = quad (Point3 xLo yLo zWall) (Point3 xHi yLo zWall)+                   (Point3 xHi yHi zWall) (Point3 xLo yHi zWall)+      zGrid = [ gline (Point3 xi yLo zWall) (Point3 xi yHi zWall) | xi <- xTicks ]+           <> [ gline (Point3 xLo yi zWall) (Point3 xHi yi zWall) | yi <- yTicks ]++  in [xPane, yPane, zPane] <> xGrid <> yGrid <> zGrid++-- | 数値を短く整形 (= 小数 1 桁、 末尾 0 と . を除去)。 ★末尾 0 除去は **小数点が+-- ある場合のみ** (整数の末尾 0 を削ると 10→1・100→1 になるバグを Phase 25 A8 で+-- 修正。 log tick の 10/100/1000 で顕在化)。+formatNum :: Double -> Text+formatNum x =+  let s = T.pack (show (fromIntegral (round (x * 10) :: Int) / 10.0 :: Double))+  in if T.any (== '.') s+       then T.dropWhileEnd (== '.') (T.dropWhileEnd (== '0') s)  -- "10.0"→"10."→"10"+       else s
+ src/Graphics/Hgg/ThreeD/Bar.hs view
@@ -0,0 +1,246 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Bar+-- Description : 3D bar (直方体 / stick) + 誤差棒 (Phase 25 A5)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- DoE / 実験データ定番の 3D 棒グラフ。 各 (x, y) 位置に底面 (base z) から高さ+-- (top z) までの棒を立てる。 2 スタイル:+--+--   * 'BarCuboid' — 直方体 (top + 4 側面の 5 quad)。 surface 同様 painter's の+--     depth 統合 ('barFacesDepth') に乗せ、 scatter/surface と層横断ソートされる。+--   * 'BarStick'  — 太い縦線 1 本 (面なし・軽量)。 line/wireframe 同様 depth 統合+--     対象外で前面に描く ('renderBarSticks')。+--+-- 誤差棒 ('renderErrorBars3D') は棒/点の頂点に z 方向 ±err の縦線 + 端キャップ。+-- bar・scatter どちらの layer にも付けられる (層の per-point err 列で駆動)。+--+-- ⚠ 入力 ('Bar3D' の点・base・half-width・err) はすべて**正規化済**座標+-- ([-1,1]^3・z-aspect 適用後) を渡す前提。 正規化は 'Easy.normLayer3D' /+-- 'scaleZLayer' が担い、 本 module は投影 + 幾何のみ。+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+module Graphics.Hgg.ThreeD.Bar+  ( BarStyle3D (..)+  , Bar3D (..)+  , defaultBar3D+  , barFacesDepth+  , renderBarSticks+  , renderStems3D+  , renderErrorBars3D+  ) where++import           Data.Aeson                      (FromJSON, ToJSON)+import           Data.Text                       (Text)+import qualified Data.Text                       as T+import           GHC.Generics                    (Generic)++import           Graphics.Hgg.Render             (FillStyle (..),+                                                  PathSegment (..),+                                                  Point (..), Primitive (..),+                                                  StrokeStyle (..), solid)+import           Graphics.Hgg.ThreeD.Projection  (Projected (..), Viewport,+                                                  project3D)+import           Graphics.Hgg.ThreeD.Types++-- | 棒のスタイル。+data BarStyle3D+  = BarCuboid   -- ^ 直方体 (top + 4 側面・depth 統合)+  | BarStick    -- ^ 太い縦線 (面なし・前面描画)+  deriving (Show, Eq, Generic)+instance ToJSON   BarStyle3D+instance FromJSON BarStyle3D++-- | 3D bar 1 series 分の設定 (= 全点共通スタイル・点ごとに棒を立てる)。+data Bar3D = Bar3D+  { br3Tops   :: ![Point3]    -- ^ 各棒の頂点 (x, y, 高さ z)・正規化済+  , br3BaseZ  :: !Double      -- ^ 底面 z (正規化済・通常 data z=0 の正規化値)+  , br3HalfW  :: !Double      -- ^ footprint 半幅 (正規化空間・x/y 共通)+  , br3Style  :: !BarStyle3D+  , br3Color  :: !Text+  , br3Alpha  :: !Double      -- ^ 面/線の不透明度 (0..1)+  , br3Width  :: !Double      -- ^ stick 線幅 px ('BarStick' 時)+  } deriving (Show, Eq, Generic)+instance ToJSON   Bar3D+instance FromJSON Bar3D++-- | default: 青、 base 0、 半幅 0.04、 直方体、 alpha 1、 stick 幅 6。+defaultBar3D :: [Point3] -> Bar3D+defaultBar3D tops = Bar3D+  { br3Tops  = tops+  , br3BaseZ = 0+  , br3HalfW = 0.04+  , br3Style = BarCuboid+  , br3Color = "#5b9bd5"+  , br3Alpha = 1.0+  , br3Width = 6+  }++-- ===========================================================================+-- 直方体 face の depth 列 (= surfaceFacesDepth と同型・大域ソートに混ぜる)+-- ===========================================================================++-- | 各棒を直方体の 5 quad (top + 4 側面・底面は隠れるので省く) に展開し、+-- @(投影 depth, PPath)@ で**未ソート**返す。 'BarStick' 時は @[]@ (= 'renderBarSticks'+-- が描く)。 簡易 Lambert shading (surface と同じ light) で立体感を付ける。+barFacesDepth+  :: Camera3D -> Projection3D -> Viewport+  -> Bar3D -> [(Double, Primitive)]+barFacesDepth cam proj vp br+  | br3Style br /= BarCuboid = []+  | otherwise = concatMap barQuads (br3Tops br)+  where+    project = project3D cam proj vp+    w  = br3HalfW br+    zb = br3BaseZ br+    lightDir = normalizeV3 (Vec3 0.5 0.5 (-1.0))+    barQuads (Point3 cx cy zt) =+      let -- 8 corners (b* = 底面, t* = 上面)+          b0 = Point3 (cx - w) (cy - w) zb+          b1 = Point3 (cx + w) (cy - w) zb+          b2 = Point3 (cx + w) (cy + w) zb+          b3 = Point3 (cx - w) (cy + w) zb+          t0 = Point3 (cx - w) (cy - w) zt+          t1 = Point3 (cx + w) (cy - w) zt+          t2 = Point3 (cx + w) (cy + w) zt+          t3 = Point3 (cx - w) (cy + w) zt+          faces = [ (t0, t1, t2, t3)   -- top+                  , (b0, b1, t1, t0)   -- front (y-)+                  , (b1, b2, t2, t1)   -- right (x+)+                  , (b2, b3, t3, t2)   -- back  (y+)+                  , (b3, b0, t0, t3) ] -- left  (x-)+      in map quadPrim faces+    quadPrim (a, b, c, d) =+      let pa = project a; pb = project b; pc = project c; pd = project d+          depth = (projDepth pa + projDepth pb + projDepth pc + projDepth pd) / 4+          n = normalizeV3 (crossV3 (sub b a) (sub c a))+          ndotl = max 0 (dotV3 n lightDir)+          intensity = 0.55 + 0.45 * ndotl+          fill = mulColor (br3Color br) intensity+          path = [ MoveTo (toPt pa), LineTo (toPt pb)+                 , LineTo (toPt pc), LineTo (toPt pd), ClosePath ]+      in (depth, PPath path (FillStyle fill (br3Alpha br))+                            (Just (StrokeStyle "#33333355" 0.3)))+    toPt (Projected x y _) = Point x y+    sub (Point3 bx by bz) (Point3 ax ay az) = Vec3 (bx - ax) (by - ay) (bz - az)++-- ===========================================================================+-- stick スタイル (太い縦線・前面描画)+-- ===========================================================================++-- | 'BarStick' 時、 各棒を底面→頂点の太い縦線で描く。 'BarCuboid' 時は @[]@。+renderBarSticks+  :: Camera3D -> Projection3D -> Viewport+  -> Bar3D -> [Primitive]+renderBarSticks cam proj vp br+  | br3Style br /= BarStick = []+  | otherwise = map stick (br3Tops br)+  where+    project = project3D cam proj vp+    zb = br3BaseZ br+    stick (Point3 cx cy zt) =+      let pb = project (Point3 cx cy zb)+          pt = project (Point3 cx cy zt)+      in PLine (Point (projX pb) (projY pb)) (Point (projX pt) (projY pt))+               (solid (br3Color br) (br3Width br))++-- ===========================================================================+-- stem (Phase 26 A4・3D lollipop = 細い垂線 + 先端マーカー・前面描画)+-- ===========================================================================++-- | Phase 26 A4: 各点を底面 ('br3BaseZ') → 先端の細い縦線 + 先端の円マーカーで+-- 描く (3D lollipop)。 'renderBarSticks' (太線・マーカー無し) と違い stem は+-- 細線 + マーカー。 線色/幅/alpha は 'Bar3D' から、 マーカー半径は引数 @markerR@+-- (px)。 depth 統合外の前面 overlay。 先端の depth cue で僅かに半径を変える+-- (scatter と同じ・近側が大きい)。+renderStems3D+  :: Camera3D -> Projection3D -> Viewport+  -> Bar3D -> Double          -- ^ マーカー基本半径 px+  -> [Primitive]+renderStems3D cam proj vp br markerR =+  concatMap stem (br3Tops br)+  where+    project = project3D cam proj vp+    zb = br3BaseZ br+    stroke = Just (StrokeStyle "#333333" 0.5)+    stem (Point3 cx cy zt) =+      let pb = project (Point3 cx cy zb)+          pt = project (Point3 cx cy zt)+          line = PLine (Point (projX pb) (projY pb)) (Point (projX pt) (projY pt))+                       (solid (br3Color br) (br3Width br))+          depthCue = max 0 (min 1 ((1 - projDepth pt) / 2))+          r = markerR * (0.55 + 0.45 * depthCue)+          marker = PCircle (Point (projX pt) (projY pt)) r+                           (FillStyle (br3Color br) (br3Alpha br))+                           stroke+                           Nothing+      in [line, marker]++-- ===========================================================================+-- 誤差棒 (z 方向 ±err・縦線 + 端キャップ)+-- ===========================================================================++-- | 各頂点 @(x,y,z)@ に z 方向 ±err の縦線 + 上下端の水平キャップ (画面空間の+-- 短い横線) を描く。 err は正規化済 z 量 (= 'normLayer3D' で換算済)。 bar/scatter+-- どちらの頂点列にも使える。 err <= 0 の点はスキップ。 線色 'col'・幅 'lw'。+renderErrorBars3D+  :: Camera3D -> Projection3D -> Viewport+  -> Text -> Double          -- ^ 色・線幅 px+  -> [(Point3, Double)]      -- ^ (頂点, 正規化 err)+  -> [Primitive]+renderErrorBars3D cam proj vp col lw pes =+  concatMap whisker pes+  where+    project = project3D cam proj vp+    capW = 3.5 :: Double      -- キャップ半幅 px+    whisker (Point3 x y z, e)+      | e <= 0    = []+      | otherwise =+          let pHi = project (Point3 x y (z + e))+              pLo = project (Point3 x y (z - e))+              (hx, hy) = (projX pHi, projY pHi)+              (lx, ly) = (projX pLo, projY pLo)+          in [ PLine (Point lx ly) (Point hx hy) (solid col lw)+             , PLine (Point (hx - capW) hy) (Point (hx + capW) hy) (solid col lw)+             , PLine (Point (lx - capW) ly) (Point (lx + capW) ly) (solid col lw) ]++-- ===========================================================================+-- 簡易色操作 (= Surface.hs と同じ hex #RRGGBB を intensity 倍)+-- ===========================================================================++mulColor :: Text -> Double -> Text+mulColor hex intensity =+  let s = T.unpack hex+      body = case s of ('#':rest) -> rest; other -> other+      r = parseHex2 (take 2 body)+      g = parseHex2 (take 2 (drop 2 body))+      b = parseHex2 (take 2 (drop 4 body))+      clampMul i = max 0 (min 255 (round (fromIntegral i * intensity :: Double) :: Int))+      toHex2 i = pad2 (decToHex (clampMul i))+  in T.pack ("#" <> toHex2 r <> toHex2 g <> toHex2 b)++parseHex2 :: String -> Int+parseHex2 [c1, c2] = hexVal c1 * 16 + hexVal c2+parseHex2 _        = 0++hexVal :: Char -> Int+hexVal c+  | c >= '0' && c <= '9' = fromEnum c - fromEnum '0'+  | c >= 'a' && c <= 'f' = fromEnum c - fromEnum 'a' + 10+  | c >= 'A' && c <= 'F' = fromEnum c - fromEnum 'A' + 10+  | otherwise            = 0++decToHex :: Int -> String+decToHex n+  | n < 16    = [hexChar n]+  | otherwise = decToHex (n `div` 16) <> [hexChar (n `mod` 16)]++hexChar :: Int -> Char+hexChar v+  | v < 10    = toEnum (fromEnum '0' + v)+  | otherwise = toEnum (fromEnum 'a' + v - 10)++pad2 :: String -> String+pad2 s+  | length s < 2 = '0' : s+  | otherwise    = s
+ src/Graphics/Hgg/ThreeD/Bound.hs view
@@ -0,0 +1,88 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Bound+-- Description : 3D の df バインド (Phase 24 A6 = 2D `df |>> spec` の 3D 対応)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- 2D と同じ書き味で 3D に列名バインドを通す:+--+-- > df |>> (layer3D (scatter3D "x" "y" "z" <> colormap3D) <> title3D "...")+-- >   :: BoundPlot3D+-- > saveSVG3DBound "out.svg" bound+--+-- 'BindableSpec' (hgg-frame) の 'VisualSpec3D' instance を本 module+-- で与える (型の定義 package 側 = 非 orphan)。 検証は 2D と同方針で+-- **バインド時に値として** 'PlotDiagnostic' に格納し、 例外は投げない。+{-# LANGUAGE OverloadedStrings #-}+{-# LANGUAGE TypeFamilies      #-}+module Graphics.Hgg.ThreeD.Bound+  ( BoundPlot3D (..)+  , saveSVG3DBound+  , saveHTML3DBound+  , showBrowser3DBound+  , unBound3D+  ) where++import           Data.Maybe                     (mapMaybe)+import           Data.Text                      (Text)+import           Data.Monoid                    (Last (..))++import           Graphics.Hgg.Spec              (ColRef (..), Resolver)+import           Graphics.Hgg.Validate          (DiagnosticContext (..),+                                                 PlotDiagnostic (..),+                                                 PlotErrorKind (..), suggest)+import           Graphics.Hgg.Frame             (BindableSpec (..),+                                                 PlotData (..),+                                                 emptyDfDiagnostics)++import           Graphics.Hgg.ThreeD.Easy       (saveSVG3D)+import           Graphics.Hgg.ThreeD.Browser    (saveHTML3D, showBrowser)+import           Graphics.Hgg.ThreeD.Spec       (Layer3D, VisualSpec3D (..),+                                                 lyr3EncX, lyr3EncY, lyr3EncZ,+                                                 lyr3TextBy, resolveSpec3D)++-- | df バインド済の 3D plot (2D 'Graphics.Hgg.Frame.BoundPlot' の 3D 版)。+data BoundPlot3D = BoundPlot3D+  { bp3Resolver    :: Resolver+  , bp3Spec        :: VisualSpec3D+  , bp3Diagnostics :: [PlotDiagnostic]+  }++instance BindableSpec VisualSpec3D where+  type BoundOf VisualSpec3D = BoundPlot3D+  bindData df spec =+    BoundPlot3D (toResolver df) spec+                (emptyDfDiagnostics df ++ checkColumns (columnNames df) spec)++-- | spec 中の 'ColByName' を df の列名と突合する (編集距離 suggestion 付き)。+checkColumns :: [Text] -> VisualSpec3D -> [PlotDiagnostic]+checkColumns known spec =+  [ PlotError (ColumnNotFound nm (suggest known nm)) (DiagnosticContext (Just i) Nothing)+  | (i, l) <- zip [0 ..] (vs3Layers spec)+  , nm <- mapMaybe byName [ getLast (lyr3EncX l)+                          , getLast (lyr3EncY l)+                          , getLast (lyr3EncZ l)+                          , getLast (lyr3TextBy l) ]+  , nm `notElem` known+  ]+ where+  byName (Just (ColByName nm)) = Just nm+  byName _                     = Nothing++-- | バインド済 3D plot を SVG 保存 (列参照を解決してから 'saveSVG3D')。+saveSVG3DBound :: FilePath -> BoundPlot3D -> IO ()+saveSVG3DBound path b = saveSVG3D path (resolveSpec3D (bp3Resolver b) (bp3Spec b))++-- | バインド済 3D plot を **WebGL self-contained HTML** として保存+--   (= 'saveSVG3DBound' の interactive 版・列参照と resolve 産物を解決してから+--   'saveHTML3D')。 df 連携 3D の browser 経路欠落を埋める (Phase 27 A2)。+saveHTML3DBound :: FilePath -> BoundPlot3D -> IO ()+saveHTML3DBound path b = saveHTML3D path (resolveSpec3D (bp3Resolver b) (bp3Spec b))++-- | バインド済 3D plot を **ブラウザで interactive 表示** ('showBrowser' の df 連携版)。+showBrowser3DBound :: BoundPlot3D -> IO ()+showBrowser3DBound b = showBrowser (resolveSpec3D (bp3Resolver b) (bp3Spec b))++-- | (Resolver, 解決済み spec) を取り出す raw 経路 (2D 'unBound' 同型)。+unBound3D :: BoundPlot3D -> (Resolver, VisualSpec3D)+unBound3D b = (bp3Resolver b, resolveSpec3D (bp3Resolver b) (bp3Spec b))
+ src/Graphics/Hgg/ThreeD/Browser.hs view
@@ -0,0 +1,156 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Browser+-- Description : HS-side browser display driver (Phase 5 A5)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- @+-- import Graphics.Hgg.ThreeD.Spec+-- import Graphics.Hgg.ThreeD.Browser+-- main = showBrowser $ purePlot3D <> layer3D (scatter3D pts) <> camera (defaultCameraZUp 3)+-- @+--+-- 中身: spec を aeson で JSON 化、 PS WebGL bundle (= data-files) と inline 結合した+-- self-contained HTML を tmp に出力、 OS 別 browser-open コマンド (xdg-open / open / start) で起動。+--+-- 設計判断 (= phase-5 計画 md §2.4):+--+--   * **bundle embed 方式**: cabal data-files 採用 (= dev iteration 速さ重視、+--     PS 側更新時 HS 再 build 不要)。 将来 TH 'embedFile' 切替時は 'getBundleJS'+--     の中身だけ差替えで済む。 詳細 → @design\/bundle-embed-choice.md@+--   * **3 つの出力経路**: 'showBrowser' (= tmp + open)、 'saveHTML3D' (= 配布用、 単一 HTML)、+--     'Graphics.Hgg.ThreeD.Easy.saveSVG3D' (= 静的 SVG、 Phase 3 CPU projection 経路)+{-# LANGUAGE OverloadedStrings #-}+module Graphics.Hgg.ThreeD.Browser+  ( -- * 主要 API+    showBrowser+  , saveHTML3D+    -- * 内部 (= bundle 取得、 swap 用に export)+  , getBundleJS+  ) where++import           Data.Aeson                   (encode)+import qualified Data.ByteString              as BS+import qualified Data.ByteString.Lazy         as LBS+import qualified Data.ByteString.Lazy.Char8   as LBS8+import           Paths_hgg_3d        (getDataFileName)+import           System.Directory             (getTemporaryDirectory)+import           System.FilePath              ((</>))+import           System.Info                  (os)+import           System.Process               (callCommand)++import           Graphics.Hgg.ThreeD.Spec     (VisualSpec3D)++-- ===========================================================================+-- bundle 取得 (= 絶縁レイヤ、 後で TH に swap 可能)+-- ===========================================================================++-- | PS WebGL bundle (= data/webgl-spec.js) を ByteString で取得。+--+-- 現在は @cabal data-files@ 経由 ('Paths_hgg_3d.getDataFileName')。+-- 将来 TH 'embedFile' に切替時はこの 1 関数のみ差替えれば済むよう設計+-- (= 切替手順は @design\/bundle-embed-choice.md@)。+getBundleJS :: IO BS.ByteString+getBundleJS = do+  path <- getDataFileName "data/webgl-spec.js"+  BS.readFile path++-- ===========================================================================+-- showBrowser / saveHTML3D+-- ===========================================================================++-- | spec を **ブラウザで interactive 表示**。 tmp HTML 生成 + OS 別 browser-open。+--+-- WebGL2 backend で描画、 mouse drag で camera orbit、 wheel で zoom、+-- 右 drag で pan。 操作仕様は Phase 4 demo と同一。+showBrowser :: VisualSpec3D -> IO ()+showBrowser spec = do+  tmpDir <- getTemporaryDirectory+  let path = tmpDir </> "hgg-3d-tmp.html"+  saveHTML3D path spec+  openInBrowser path++-- | spec を **self-contained HTML として保存** (= 配布用、 bundle inline 埋込)。+--+-- 出力ファイルは外部依存無し、 ブラウザで直接開ける。+saveHTML3D :: FilePath -> VisualSpec3D -> IO ()+saveHTML3D path spec = do+  bundleJS <- getBundleJS+  let specJSON = encode spec+      html     = mkHTML bundleJS specJSON+  LBS.writeFile path html++-- ===========================================================================+-- HTML 生成+-- ===========================================================================++-- | bundle JS + spec JSON を 1 HTML に埋込。 ESM ではなく通常 script 経路+-- (= spago bundle 出力は IIFE、 ESM import すると export named 'main' 不在で失敗、+--    Phase 4 で確認済の罠)。+mkHTML :: BS.ByteString -> LBS.ByteString -> LBS.ByteString+mkHTML bundleJS specJSON = LBS.concat+  [ "<!DOCTYPE html>\n"+  , "<html lang=\"ja\">\n"+  , "<head>\n"+  , "<meta charset=\"UTF-8\">\n"+  , "<title>hgg 3D</title>\n"+  , "<style>\n"+  -- Phase 27 A8: 2D / CPU SVG plot (tpBackground=#ffffff) と背景を揃える (白)+  , "  body { margin: 0; background: #ffffff; color: #333333;\n"+  , "         font-family: -apple-system, \"Hiragino Sans\", \"Yu Gothic UI\", sans-serif; }\n"+  , "  #plot-container { width: 100vw; height: 100vh; display: flex;\n"+  , "                    flex-direction: column; }\n"+  , "  #plot { flex: 1; cursor: grab; background: #ffffff; }\n"+  , "  #plot:active { cursor: grabbing; }\n"+  , "  #info { padding: 8px 16px; font-size: 12px; color: #666666;\n"+  , "          border-top: 1px solid #dddddd; }\n"+  , "</style>\n"+  , "</head>\n"+  , "<body>\n"+  , "<div id=\"plot-container\">\n"+  , "  <canvas id=\"plot\" width=\"1024\" height=\"720\"></canvas>\n"+  , "  <div id=\"info\">左ドラッグ orbit / 右ドラッグ pan / wheel zoom</div>\n"+  , "</div>\n"+  , "<script>\n"+  , LBS.fromStrict bundleJS+  , "\n</script>\n"+  , "<script>\n"+  , "  // spec JSON を inline 埋込、 PS Examples.WebGLPlot.renderFromSpec で描画\n"+  , "  var SPEC = "+  , specJSON+  , ";\n"+  , "  if (window.hggPlot && window.hggPlot.renderFromSpec) {\n"+  , "    window.hggPlot.renderFromSpec(\"plot\", SPEC);\n"+  , "  } else {\n"+  , "    console.error(\"[hgg] window.hggPlot not initialized\");\n"+  , "  }\n"+  , "</script>\n"+  , "</body>\n"+  , "</html>\n"+  ]+  where+    _unused = LBS8.pack ""  -- LBS8 import を保持 (= 将来 debug println で使用)++-- ===========================================================================+-- OS 別 browser-open+-- ===========================================================================++-- | OS 検出して xdg-open (Linux) / open (macOS) / start (Windows) を呼ぶ。+--+-- WSL は @os == \"linux\"@ で xdg-open 経路。 wslview などインストール済なら動作。+-- 失敗時は path を stdout に出すので手動で開ける。+openInBrowser :: FilePath -> IO ()+openInBrowser path = case os of+  "linux"   -> safeCall ("xdg-open " <> quote path)+  "darwin"  -> safeCall ("open "     <> quote path)+  "mingw32" -> safeCall ("start "    <> quote path)  -- Windows GHC+  other     -> do+    putStrLn $ "[hgg] unknown OS '" <> other <> "', open manually: " <> path+  where+    safeCall cmd = do+      putStrLn $ "[hgg] " <> cmd+      callCommand cmd++    -- path にスペース含むケースに備えて double quote。 簡略化のため escape 無し+    -- (= tmp dir path に通常スペースは無いが念のため)+    quote p = "\"" <> p <> "\""
+ src/Graphics/Hgg/ThreeD/Delaunay.hs view
@@ -0,0 +1,78 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Delaunay+-- Description : 2D Delaunay 三角分割 (Phase 26 A5・trisurf 用)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- 不規則 (非 grid) 点群を 'trisurf' で曲面化するための **純 Haskell** 2D Delaunay+-- 三角分割。 外部ライブラリ / バイナリ依存ゼロ (= spec §10.7・graphviz parity と+-- 同じ「自己完結」 方針)。 アルゴリズムは Bowyer-Watson の素朴版 (O(n²) 程度・+-- trisurf の点数は数十〜低数百なので十分)。+--+-- 入力は (x,y) 列。 z は呼び出し側で分割後に各頂点へ持ち上げる (= trisurf は+-- (x,y) 平面の 2D 分割だけで足り、 3D 四面体分割は不要)。 退化 (共円・共線) は+-- incircle 述語の符号で素朴に扱う (demo 用途では実害なし)。+{-# LANGUAGE BangPatterns #-}+module Graphics.Hgg.ThreeD.Delaunay+  ( delaunay2D+  ) where++import           Data.List       (foldl')+import qualified Data.Map.Strict as M++-- | (x,y) 点列を 2D Delaunay 三角分割し、 三角形を元の点配列への index 三つ組+--   @(i,j,k)@ で返す。 点が 3 未満 / 全点共線などで三角形が作れない時は @[]@。+delaunay2D :: [(Double, Double)] -> [(Int, Int, Int)]+delaunay2D pts0+  | n < 3     = []+  | otherwise = [ t | t@(a, b, c) <- finalTris, a < n, b < n, c < n ]+  where+    n = length pts0+    xs = map fst pts0+    ys = map snd pts0+    minx = minimum xs; maxx = maximum xs+    miny = minimum ys; maxy = maximum ys+    dmax = max (maxx - minx) (maxy - miny)+    d    = if dmax <= 0 then 1 else dmax+    midx = (minx + maxx) / 2+    midy = (miny + maxy) / 2+    -- super-triangle = 全点を内包する十分大きな三角形。 頂点 index は n, n+1, n+2。+    super = [ (midx - 20 * d, midy - d)+            , (midx,          midy + 20 * d)+            , (midx + 20 * d, midy - d) ]+    allPts = M.fromList (zip [0 ..] (pts0 ++ super))+    pAt i  = allPts M.! i++    initTris = [(n, n + 1, n + 2)]+    finalTris = foldl' addPoint initTris [0 .. n - 1]++    -- 点 i を挿入: bad triangle (外接円に i を含む) を抜き、 空いた多角形穴の+    -- 境界辺ごとに i と結ぶ三角形を張る。+    addPoint tris i =+      let (bad, good) = partitionBad i tris+          edges       = concatMap triEdges bad+          -- 境界辺 = 逆向き辺が無い (= 2 三角形で共有されていない) 辺+          boundary    = [ (a, b) | (a, b) <- edges, (b, a) `notElem` edges ]+          newTris     = [ (a, b, i) | (a, b) <- boundary ]+      in good ++ newTris++    partitionBad i = foldr step ([], [])+      where+        step t@(a, b, c) (bs, gs)+          | inCircle (pAt a) (pAt b) (pAt c) (pAt i) = (t : bs, gs)+          | otherwise                                = (bs, t : gs)++    triEdges (a, b, c) = [(a, b), (b, c), (c, a)]++-- | 点 d が三角形 (a,b,c) の外接円の内部か。 三角形の向き (CCW/CW) を符号で吸収。+inCircle :: (Double, Double) -> (Double, Double) -> (Double, Double)+         -> (Double, Double) -> Bool+inCircle (ax, ay) (bx, by) (cx, cy) (dx, dy) =+  let orient = (bx - ax) * (cy - ay) - (by - ay) * (cx - ax)+      adx = ax - dx; ady = ay - dy+      bdx = bx - dx; bdy = by - dy+      cdx = cx - dx; cdy = cy - dy+      det = (adx * adx + ady * ady) * (bdx * cdy - cdx * bdy)+          - (bdx * bdx + bdy * bdy) * (adx * cdy - cdx * ady)+          + (cdx * cdx + cdy * cdy) * (adx * bdy - bdx * ady)+  in if orient > 0 then det > 0 else det < 0
+ src/Graphics/Hgg/ThreeD/Easy.hs view
@@ -0,0 +1,545 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Easy+-- Description : 3D 出力経路の薄い wrap (Phase 5 A6 saveSVG3D)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- 'VisualSpec3D' から各出力経路への dispatch helper。 3 経路:+--+--   * 'saveSVG3D'   ─ Phase 3 CPU projection (= 静的 SVG 出力、 本 module 実装)+--   * 'saveHTML3D'  ─ Phase 4 WebGL bundle 埋込 HTML (= 'Graphics.Hgg.ThreeD.Browser')+--   * 'showBrowser' ─ tmp HTML + xdg-open (= 'Graphics.Hgg.ThreeD.Browser')+--+-- 同 spec を 3 経路に出し分けられる。 用途で使い分け:+--+--   * 印刷 / doc 埋込 → 'saveSVG3D'+--   * 配布 / 単一 HTML 配布 → 'saveHTML3D'+--   * 開発時 interactive 確認 → 'showBrowser'+{-# LANGUAGE OverloadedStrings #-}+module Graphics.Hgg.ThreeD.Easy+  ( saveSVG3D+    -- * PDF / PNG 出力 (Phase 24 A8 glue)+  , savePDF3D+  , savePNG3D+  , spec3DToPrimitives+    -- * 群別タイル配置 (Phase 24 A7)+  , saveSVG3DFacet+  , renderSpec3DInPanel+    -- * 正規化 pipeline (Phase 24 A3・test 用に公開)+  , padAxes3D+  , normPoint3D+  , normLayer3D+    -- * 投影 contour (Phase 24 A5・test 用に公開)+  , renderProjectedContour3D+  ) where++import           Data.List                        (nub, sortOn)+import           Data.Maybe                       (fromMaybe, listToMaybe)+import           Data.Monoid                      (First (..), Last (..))+import           Data.Text                        (Text)+import qualified Data.Text                        as T++import           Graphics.Hgg.Backend.SVG         (savePrimitivesSVG)+import           Graphics.Hgg.Backend.PDF         (savePrimitivesPDF)+import           Graphics.Hgg.Backend.Rasterific  (savePrimitivesPNG, defaultPNGConfig)+import           Graphics.Hgg.Render              (FillStyle (..), Point (..),+                                                   Primitive (..), solid,+                                                   StrokeStyle (..),+                                                   TextAnchor (..),+                                                   TextStyle (..))+import           Graphics.Hgg.Render.Common       (continuousColor)+import           Graphics.Hgg.Math.Griddata       (innerLevels, marchingSegments)+import           Graphics.Hgg.Layout              (Rect (..))++import           Graphics.Hgg.ThreeD.Axes         (Axes3D (..),+                                                    defaultPaneStyle3D,+                                                    renderAxes3DPanes,+                                                    renderAxes3DWithLabels)+import           Graphics.Hgg.ThreeD.Line         (renderLine3D, renderWireframe3D,+                                                    renderQuiver3D)+import           Graphics.Hgg.ThreeD.Projection   (Projected (..), Viewport (..),+                                                    project3D)+import           Graphics.Hgg.ThreeD.Scatter      (scatterPointsDepth)+import           Graphics.Hgg.ThreeD.Spec         (Layer3D, Mark3DKind (..),+                                                    VisualSpec3D (..),+                                                    autoAxes3D, layerToLine,+                                                    layerToScatter, layerToQuiver,+                                                    layerToSurface, layerToBar,+                                                    layerToWireframe, lyr3Kind,+                                                    lyr3Color, lyr3Size,+                                                    lyr3Colormap, lyr3Colorbar,+                                                    lyr3Contours,+                                                    ContourDir (..),+                                                    lyr3Grid, lyr3Labels,+                                                    lyr3Legend,+                                                    lyr3Points, lyr3Annots,+                                                    lyr3XRange,+                                                    lyr3YRange, lyr3BarBaseZ,+                                                    lyr3Width, lyr3StemBaseZ,+                                                    lyr3Vectors, lyr3VecScale,+                                                    lyr3EdgeColor, lyr3Shaded,+                                                    lyr3Alpha,+                                                    lyr3PtErrs, viridisStops3D,+                                                    vs3Axes, vs3AxisTitles,+                                                    vs3Camera, vs3Height,+                                                    vs3Layers, vs3Log, vs3Pane,+                                                    vs3Proj, vs3Title,+                                                    vs3Width, vs3XAspect,+                                                    vs3YAspect, vs3ZAspect)+import qualified Data.Vector                      as V+import           Graphics.Hgg.ThreeD.Surface      (surfaceFacesDepth,+                                                    trianglesFacesDepth)+import           Graphics.Hgg.ThreeD.Delaunay     (delaunay2D)+import           Graphics.Hgg.ThreeD.Bar          (Bar3D (..), barFacesDepth,+                                                    renderBarSticks, renderStems3D,+                                                    renderErrorBars3D)+import           Graphics.Hgg.ThreeD.Types++-- | 'VisualSpec3D' を Phase 3 CPU projection で SVG に保存。+--+-- defaults:+--+--   * camera     → 'defaultCameraZUp' 3+--   * projection → 'defaultPerspective'+--   * axes       → 'autoAxes3D' (= layers の Point3 集合から bbox 算出)+--   * width / height → 700 / 700+--   * title      → ""+saveSVG3D :: FilePath -> VisualSpec3D -> IO ()+saveSVG3D path spec =+  let (w, h, title, prims) = spec3DToPrimitives spec+  in savePrimitivesSVG path w h title prims++-- | Phase 24 A8: 'VisualSpec3D' を (幅, 高さ, タイトル, [Primitive]) に落とす+-- 共通核。 各 backend (SVG/PDF/PNG) の出力関数が共有する。 prims は+-- 'renderSpec3DInPanel' 単一 panel (= キャンバス全域)。 title は別返し+-- (SVG は backend が <text> 化・PDF/PNG は 'savePDF3D'/'savePNG3D' が PText 化)。+spec3DToPrimitives :: VisualSpec3D -> (Int, Int, Text, [Primitive])+spec3DToPrimitives spec =+  let w     = fromMaybe 700 (getLast (vs3Width  spec))+      h     = fromMaybe 700 (getLast (vs3Height spec))+      title = fromMaybe ""  (getLast (vs3Title  spec))+      prims = renderSpec3DInPanel 0 0 (fromIntegral w) (fromIntegral h) spec+  in (w, h, title, prims)++-- | Phase 24 A8: 3D 図を PDF に保存 ('saveSVG3D' の PDF 版)。 [Primitive] を+-- 既存 PDF backend ('savePrimitivesPDF') へ配線。 title は上部中央の PText に+-- する (PDF backend は title 引数を持たないため)。 ⚠ PDF 標準フォントは+-- Latin-1 のみ (日本語ラベルは 'savePNG3D')。+savePDF3D :: FilePath -> VisualSpec3D -> IO ()+savePDF3D path spec =+  let (w, h, title, prims) = spec3DToPrimitives spec+  in savePrimitivesPDF path w h (titlePrims w title <> prims)++-- | Phase 24 A8: 3D 図を PNG に保存 ('saveSVG3D' の PNG 版・日本語ラベル可)。+-- [Primitive] を既存 Rasterific backend ('savePrimitivesPNG') へ配線。+savePNG3D :: FilePath -> VisualSpec3D -> IO ()+savePNG3D path spec =+  let (w, h, title, prims) = spec3DToPrimitives spec+  in savePrimitivesPNG defaultPNGConfig path w h (titlePrims w title <> prims)++-- | タイトルを上部中央の PText に (空なら無し)。 SVG backend の title 描画+-- (font-size 16・中央・y≈24) に概ね合わせる。+titlePrims :: Int -> Text -> [Primitive]+titlePrims w title+  | T.null title = []+  | otherwise    =+      [ PText (Point (fromIntegral w / 2) 24) title+              (TextStyle "#333333" 16 "sans-serif" AnchorMiddle 0 "normal" False) ]++-- | Phase 24 A7: 'VisualSpec3D' を**任意のパネル矩形** @(px,py,pw,ph)@ 内に+-- 描画する純粋核 ('saveSVG3D' と 'saveSVG3DFacet' が共有)。 軸 box・正規化+-- pipeline・layer・colorbar をパネル局所座標で出す。 @(0,0,w,h)@ で呼ぶと+-- 旧 'saveSVG3D' とビット同一 (margin 50・colorbar 右端)。+renderSpec3DInPanel :: Double -> Double -> Double -> Double -> VisualSpec3D -> [Primitive]+renderSpec3DInPanel px py pw ph spec =+  let layers = vs3Layers spec+      cam    = fromMaybe (defaultCameraZUp 3)   (getLast (vs3Camera spec))+      proj   = fromMaybe defaultPerspective     (getLast (vs3Proj   spec))+      -- Phase 25 A8: log scale flag を Axes3D に注入 (auto or user の bbox に上書き)。+      -- normPoint3D / 軸 renderer / surface 正規化が ax の axes*Log を読む。+      (lx, ly, lz) = fromMaybe (False, False, False) (getLast (vs3Log spec))+      ax0    = padAxes3D (fromMaybe (autoAxes3D layers) (getLast (vs3Axes spec)))+      ax     = ax0 { axesXLog = lx, axesYLog = ly, axesZLog = lz }+      -- Phase 24 A2: colormap 付き surface があれば右に colorbar を出す+      -- (z range はその layer の grid 全体)。+      -- Phase 25 A3: 連続色 scatter ('colorContinuousBy3D') の colorbar 情報+      -- ('lyr3Colorbar' = resolve 産物) も同じ場所に出す (surface 優先で先頭採用)。+      cbInfo = listToMaybe+        (  [ (stops, minimum zs, maximum zs)+           | l <- layers+           , getFirst (lyr3Kind l) == Just M3Surface+           , Just stops <- [getLast (lyr3Colormap l)]+           , Just grid  <- [getLast (lyr3Grid l)]+           , let zs = concat grid+           , not (null zs) ]+        ++ [ cb | l <- layers, Just cb <- [getLast (lyr3Colorbar l)] ])+      cbW    = case cbInfo of { Just _ -> 70; Nothing -> 0 }+      -- Phase 25 A2: 群色分けの離散凡例 (lyr3Legend・初出順で union)+      legendEntries = nub (concatMap (fromMaybe [] . getLast . lyr3Legend) layers)+      lgW    = if null legendEntries then 0 else 100+      m      = 50   -- パネル内側 margin (colorbar/凡例 分は右を空ける)+      vp     = Viewport (px + m) (py + m) (pw - 2*m - cbW - lgW) (ph - 2*m)+      -- Phase 24 A8 / 25 A8: box aspect (正規化後 x/y/z を各 a 倍) と軸名。+      -- za は従来 (z のみ)、 Phase 25 A8 で x/y も追加 (既定 1 = 恒等)。+      xa     = fromMaybe 1 (getLast (vs3XAspect spec))+      ya     = fromMaybe 1 (getLast (vs3YAspect spec))+      za     = fromMaybe 1 (getLast (vs3ZAspect spec))+      labels = fromMaybe ("x", "y", "z") (getLast (vs3AxisTitles spec))+      applyAspect (Point3 x y z) = Point3 (x * xa) (y * ya) (z * za)+      -- Phase 24 A8: 全 layer を正規化 + aspect した上で、 **surface 面と+      -- scatter 点を層横断で 1 つの depth リストに混ぜて大域ソート** する+      -- (= scatter 点が surface 膜を透ける問題の解消)。 床面 contour は床に+      -- 固定なので先 (奥)、 line/wireframe は depth 統合せず後 (前) に描く。+      scaled      = map (scaleAspectLayer xa ya za . normLayer3D ax) layers+      floorPrims  = concatMap (renderProjectedContour3D (xa, ya, za) cam proj vp) scaled+      depthItems  = concatMap (depthItemsOf cam proj vp) scaled+      otherPrims  = concatMap (otherLayerPrims cam proj vp) scaled+      errBarPrims = concatMap (errorBarPrimsOf cam proj vp) scaled   -- Phase 25 A5+      textPrims   = concatMap (textPrimsOf     cam proj vp) scaled   -- Phase 25 A7+      mergedDepth = map snd (sortOn (negate . fst) depthItems)+      -- Phase 24 A3: データ座標を [-1,1]^3 に正規化してから投影する。+      -- 既定 camera (距離 3-5) は単位スケール前提のため、 実データ+      -- (z ≈ 80 等) をそのまま投影すると視界外になる。 軸ラベルは+      -- 元のデータ値のまま ('renderAxes3DWithLabels')。 mplot3d の axes box 同型。+      -- Phase 25 A6: 壁面 pane + gridline (既定 ON)。 cube wireframe より前 =+      -- 最背面に置き、 データ・wireframe を pane の手前に出す。+      paneOn   = fromMaybe True (getLast (vs3Pane spec))+      panePrims = if paneOn+                    then renderAxes3DPanes (applyAspect . normPoint3D ax)+                           defaultPaneStyle3D cam proj vp ax+                    else []+  in panePrims+       <> renderAxes3DWithLabels (applyAspect . normPoint3D ax) labels cam proj vp ax+       <> floorPrims+       <> mergedDepth+       <> otherPrims+       <> errBarPrims+       <> textPrims+       <> maybe [] (renderColorbar3D px py pw ph) cbInfo+       <> renderLegend3D px py pw ph legendEntries++-- | Phase 25 A2: 群色分けの離散凡例 (色チップ + ラベル) をパネル右端に縦並び。+-- colorbar があればその左、 無ければ右端。 entries = (カテゴリ, 色) の初出順。+renderLegend3D :: Double -> Double -> Double -> Double -> [(Text, Text)] -> [Primitive]+renderLegend3D _  _  _  _  []      = []+renderLegend3D px py pw ph entries =+  let chip = 12 :: Double+      rowH = 20 :: Double+      n    = length entries+      x0   = px + pw - 96+      y0   = py + (ph - fromIntegral n * rowH) / 2+      tsL  = TextStyle "#333333" 11 "sans-serif" AnchorStart 0 "normal" False+      row (i, (cat, col)) =+        let y = y0 + fromIntegral i * rowH+        in [ PRect (Rect x0 y chip chip)+                   (FillStyle col 1.0) (Just (StrokeStyle "#333333" 0.5))+           , PText (Point (x0 + chip + 6) (y + chip - 2)) cat tsL ]+  in concatMap row (zip [0 :: Int ..] entries)++-- | Phase 24 A8: depth 統合対象 (surface 面・scatter 点) を @(depth, Primitive)@+-- で返す。 line/wireframe/floor は対象外 ([])。+depthItemsOf :: Camera3D -> Projection3D -> Viewport -> Layer3D -> [(Double, Primitive)]+depthItemsOf cam proj vp l = case getFirst (lyr3Kind l) of+  Just M3Surface -> surfaceFacesDepth  cam proj vp (layerToSurface l)+  Just M3Scatter -> scatterPointsDepth cam proj vp (layerToScatter l)+  Just M3Bar     -> barFacesDepth      cam proj vp (layerToBar     l)  -- Phase 25 A5 (cuboid のみ)+  -- Phase 26 A5: trisurf = 正規化済点群を (x,y) で Delaunay 三角分割し face を depth 統合+  Just M3Trisurf ->+    let pts  = fromMaybe [] (getLast (lyr3Points l))+        tris = delaunay2D [ (x, y) | Point3 x y _ <- pts ]+        vpts = V.fromList pts+        triPts = [ (vpts V.! a, vpts V.! b, vpts V.! c) | (a, b, c) <- tris ]+        col    = fromMaybe "#5b9bd5" (getLast (lyr3Color     l))+        ecol   = fromMaybe "#3a73a6" (getLast (lyr3EdgeColor l))+        shaded = fromMaybe True      (getLast (lyr3Shaded    l))+        cmap   = getLast (lyr3Colormap l)+        alpha  = fromMaybe 1         (getLast (lyr3Alpha     l))+    in trianglesFacesDepth cam proj vp col ecol shaded cmap alpha triPts+  _              -> []++-- | Phase 24 A8: depth 統合対象外の layer (line/wireframe・bar stick) を描く。+otherLayerPrims :: Camera3D -> Projection3D -> Viewport -> Layer3D -> [Primitive]+otherLayerPrims cam proj vp l = case getFirst (lyr3Kind l) of+  Just M3Line      -> renderLine3D      cam proj vp (layerToLine      l)+  Just M3Wireframe -> renderWireframe3D cam proj vp (layerToWireframe l)+  Just M3Bar       -> renderBarSticks   cam proj vp (layerToBar       l)  -- Phase 25 A5 (stick のみ)+  -- Phase 26 A4: stem は細い垂線 + 先端マーカー (前面 overlay)。 線幅既定 2・+  -- マーカー半径既定 5 (lyr3Width / lyr3Size で上書き可)。+  Just M3Stem      ->+    let bar = (layerToBar l) { br3Width = fromMaybe 2 (getLast (lyr3Width l)) }+        markerR = fromMaybe 5 (getLast (lyr3Size l))+    in renderStems3D cam proj vp bar markerR+  -- Phase 26 A3: 3D vector field (前面 overlay・投影後 2D 矢印)+  Just M3Quiver    -> renderQuiver3D cam proj vp (layerToQuiver l)+  _                -> []++-- | Phase 25 A5: 層の per-point err (正規化済) があれば誤差棒を描く (bar/scatter+-- 共通)。 頂点列は正規化済 'lyr3Points'。 最前面に描くため depth 統合外。+errorBarPrimsOf :: Camera3D -> Projection3D -> Viewport -> Layer3D -> [Primitive]+errorBarPrimsOf cam proj vp l =+  case (getLast (lyr3PtErrs l), getLast (lyr3Points l)) of+    (Just es, Just pts) | not (null es) ->+      renderErrorBars3D cam proj vp "#333333" 1.0 (zip pts es)+    _ -> []++-- | Phase 25 A7: テキスト注釈 layer (M3Text) を描く。 正規化済 'lyr3Points' を+-- 投影し、 各点に 'lyr3Labels' の文字列を PText で置く (depth 統合外の前面 overlay)。+-- 文字色 = 'lyr3Color' (既定 @#333333@)、 サイズ = 'lyr3Size' (既定 11)。 点の+-- 少し上 (-6px) に中央寄せ。+textPrimsOf :: Camera3D -> Projection3D -> Viewport -> Layer3D -> [Primitive]+textPrimsOf cam proj vp l = case getFirst (lyr3Kind l) of+  Just M3Text ->+    let pts = fromMaybe [] (getLast (lyr3Points l))+        lbs = fromMaybe [] (getLast (lyr3Labels l))+        col = fromMaybe "#333333" (getLast (lyr3Color l))+        sz  = fromMaybe 11        (getLast (lyr3Size  l))+        ts  = TextStyle col sz "sans-serif" AnchorMiddle 0 "normal" False+        toScreen p = let Projected sx sy _ = project3D cam proj vp p in Point sx sy+        place p t  = let Point lx ly = toScreen p in PText (Point lx (ly - 6)) t ts+    in zipWith place pts lbs+  _ -> []++-- | Phase 24 A8 / 25 A8: 正規化済 layer を box aspect @(xa, ya, za)@ 倍する。+-- surface は grid z (za) と x/y range (xa/ya)、 それ以外は点列の各成分。+-- @(1,1,1)@ は恒等 (= 旧出力ビット不変)。 surface の grid x/y は xRange を縮める+-- ことで反映 (Surface.hs が xRange を線形補間して頂点を置くため)。+scaleAspectLayer :: Double -> Double -> Double -> Layer3D -> Layer3D+scaleAspectLayer xa ya za l+  | xa == 1 && ya == 1 && za == 1 = l+  | otherwise = case getFirst (lyr3Kind l) of+      Just M3Surface ->+        l { lyr3Grid   = Last (fmap (map (map (* za))) (getLast (lyr3Grid l)))+          , lyr3XRange = Last (fmap (scaleRange xa) (getLast (lyr3XRange l)))+          , lyr3YRange = Last (fmap (scaleRange ya) (getLast (lyr3YRange l)))+          }+      _ ->+        -- Phase 25 A5: bar 底面 z・err (z 量) も同じ za で縮める+        -- Phase 26 A3: quiver ベクトルも各成分を aspect 倍 (始点 + vec が aspect 後点と一致)+        l { lyr3Points   = Last (fmap (map sp) (getLast (lyr3Points l)))+          , lyr3BarBaseZ = Last (fmap (* za) (getLast (lyr3BarBaseZ l)))+          , lyr3PtErrs   = Last (fmap (map (* za)) (getLast (lyr3PtErrs l)))+          , lyr3Vectors  = Last (fmap (map sv) (getLast (lyr3Vectors l)))+          }+  where sp (Point3 x y z) = Point3 (x * xa) (y * ya) (z * za)+        sv (Vec3 vx vy vz) = Vec3 (vx * xa) (vy * ya) (vz * za)+        scaleRange a (lo, hi) = (lo * a, hi * a)++-- | Phase 24 A7: 群別 3D 図のタイル配置 (= 「群別曲面の並置」)。 N 個の+-- @(群ラベル, spec)@ を near-square グリッド (@ncol = ceil(√n)@) に並べ、 各+-- パネルを 'renderSpec3DInPanel' で描き、 上部に群ラベルを置く。 colorbar は+-- panel 毎 (各 spec の colormap surface に追従)。 camera/axes は各 spec が個別に+-- 持つ前提 (analyze の群別 'surfaceOf' 出力をそのまま渡せる)。 1 パネルの+-- 既定サイズは 380×380 px (spec が 'width3DV'/'height3DV' を持てばそれを使う)。+saveSVG3DFacet :: FilePath -> [(Text, VisualSpec3D)] -> IO ()+saveSVG3DFacet _    []     = pure ()+saveSVG3DFacet path panels = do+  let n      = length panels+      ncol   = ceiling (sqrt (fromIntegral n :: Double)) :: Int+      nrow   = (n + ncol - 1) `div` ncol+      -- パネルサイズ = 先頭 spec の width/height (なければ 380)+      panelW = fromIntegral (fromMaybe 380 (getLast (vs3Width  (snd (head panels)))))+      panelH = fromIntegral (fromMaybe 380 (getLast (vs3Height (snd (head panels))))) :: Double+      wPx    = round (panelW * fromIntegral ncol) :: Int+      hPx    = round (panelH * fromIntegral nrow) :: Int+      tsLab  = TextStyle "#333333" 13 "sans-serif" AnchorMiddle 0 "bold" False+      panelPrims k (label, spec) =+        let row = k `div` ncol+            col = k `mod` ncol+            px  = fromIntegral col * panelW+            py  = fromIntegral row * panelH+            lbl = PText (Point (px + panelW / 2) (py + 22)) label tsLab+        in lbl : renderSpec3DInPanel px py panelW panelH spec+      prims = concat (zipWith panelPrims [0 ..] panels)+  savePrimitivesSVG path wPx hPx "" prims++-- | Phase 24 A3: 退化した軸 (min == max) を ±0.5 に広げる (正規化の 0 割り防止)。+padAxes3D :: Axes3D -> Axes3D+padAxes3D ax =+  let pad lo hi | hi > lo   = (lo, hi)+                | otherwise = (lo - 0.5, lo + 0.5)+      (x0, x1) = pad (axesXMin ax) (axesXMax ax)+      (y0, y1) = pad (axesYMin ax) (axesYMax ax)+      (z0, z1) = pad (axesZMin ax) (axesZMax ax)+  in ax { axesXMin = x0, axesXMax = x1+        , axesYMin = y0, axesYMax = y1+        , axesZMin = z0, axesZMax = z1 }++-- | Phase 24 A3 / 25 A8: 軸 bbox を [-1,1]^3 へ写す正規化 (各軸独立)。 log 軸+-- ('axes*Log') は log10 空間で affine、 線形軸はそのまま affine。+normPoint3D :: Axes3D -> Point3 -> Point3+normPoint3D ax (Point3 x y z) =+  Point3 (normCoord (axesXLog ax) (axesXMin ax) (axesXMax ax) x)+         (normCoord (axesYLog ax) (axesYMin ax) (axesYMax ax) y)+         (normCoord (axesZLog ax) (axesZMin ax) (axesZMax ax) z)++-- | Phase 25 A8: 1 軸の正規化 (log/線形)。 @[lo,hi]@ → @[-1,1]@。 log は値・端を+-- 1e-12 に clamp してから log10 空間で affine。+normCoord :: Bool -> Double -> Double -> Double -> Double+normCoord isLog lo hi v+  | isLog =+      let lo' = logBase 10 (max 1e-12 lo)+          hi' = logBase 10 (max 1e-12 hi)+          v'  = logBase 10 (max 1e-12 v)+      in if hi' <= lo' then 0 else -1 + 2 * (v' - lo') / (hi' - lo')+  | hi <= lo  = 0+  | otherwise = -1 + 2 * (v - lo) / (hi - lo)++-- | Phase 24 A3: layer のデータを正規化座標に写す。 surface は grid の z と+-- x/y range、 それ以外は点列。 colormap の色は z 正規化に対して不変 (affine)。+normLayer3D :: Axes3D -> Layer3D -> Layer3D+normLayer3D ax l0 = case getFirst (lyr3Kind l) of+  Just M3Surface ->+    -- Phase 25 A8: surface の z は log-z 対応 (axesZLog)。 surface の x/y log は+    -- 未対応 (grid x/y は xRange を線形補間する Surface.hs 都合)。+    let nmz = normCoord (axesZLog ax) (axesZMin ax) (axesZMax ax)+    in l { lyr3XRange = Last (Just (-1, 1))+         , lyr3YRange = Last (Just (-1, 1))+         , lyr3Grid   = Last (fmap (map (map nmz)) (getLast (lyr3Grid l)))+         }+  kind ->+    -- Phase 25 A5: 点に加え err (z 量・delta) と bar 底面 z も正規化する。+    let zspan  = axesZMax ax - axesZMin ax+        zscale = if zspan <= 0 then 0 else 2 / zspan          -- delta 用 (±err)+        nmz v  = if zspan <= 0 then 0 else -1 + 2 * (v - axesZMin ax) / zspan+        clampN = max (-1) . min 1+        l1 = l { lyr3Points = Last (fmap (map (normPoint3D ax)) (getLast (lyr3Points l)))+               , lyr3PtErrs = Last (fmap (map (* zscale)) (getLast (lyr3PtErrs l)))+               }+    in case kind of+         Just M3Bar  -> l1 { lyr3BarBaseZ = Last (Just (clampN (nmz 0))) }+         -- Phase 26 A4: stem は stemBaseZ (data 空間・既定 0) を正規化して底面に使う+         Just M3Stem -> l1 { lyr3BarBaseZ = Last (Just (clampN (nmz (fromMaybe 0 (getLast (lyr3StemBaseZ l)))))) }+         -- Phase 26 A3: quiver のベクトルは per-axis の affine 微分 (= 2/span) で正規化+         -- (正規化は affine ゆえ norm(p+v)-norm(p) = v * 2/span)。+         Just M3Quiver ->+           let xspan = axesXMax ax - axesXMin ax+               yspan = axesYMax ax - axesYMin ax+               sx = if xspan <= 0 then 0 else 2 / xspan+               sy = if yspan <= 0 then 0 else 2 / yspan+               sz = zscale+               nv (Vec3 vx vy vz) = Vec3 (vx * sx) (vy * sy) (vz * sz)+           in l1 { lyr3Vectors = Last (fmap (map nv) (getLast (lyr3Vectors l))) }+         _           -> l1+  where+    -- Phase 30 A6: inline 注釈 ('lyr3Annots') を持つ M3Text は、 正規化前に+    --   lyr3Points/lyr3Labels へ materialize する (= 以降の正規化・renderer は無改修。+    --   列駆動 text3D は resolveLayer3D で既に lyr3Points/Labels が入るので annots は空)。+    l = case (getFirst (lyr3Kind l0), lyr3Annots l0) of+          (Just M3Text, annots@(_:_)) ->+            l0 { lyr3Points = Last (Just (map fst annots))+               , lyr3Labels = Last (Just (map snd annots)) }+          _ -> l0++-- | Phase 24 A2: パネル右端の縦 colorbar (gradient strip + min/mid/max ラベル)。+--   2D の gradient bar 凡例と同じ 'continuousColor' 補間 = palette 整合。+--   Phase 24 A7: パネル矩形 @(px,py,pw,ph)@ 局所座標で配置 (facet 対応)。+renderColorbar3D :: Double -> Double -> Double -> Double -> ([Text], Double, Double) -> [Primitive]+renderColorbar3D px py pw ph (stops, zMin, zMax) =+  let barW = 14 :: Double+      barH = ph * 0.45+      x0   = px + pw - 58+      y0   = py + (ph - barH) / 2+      nStrip = 48 :: Int+      stripH = barH / fromIntegral nStrip+      strip k =+        let t = fromIntegral k / fromIntegral (nStrip - 1)+            -- t=1 (zMax) が上。 +0.5 はストライプ間の継ぎ目消し+            y = y0 + barH - stripH * fromIntegral (k + 1)+        in PRect (Rect x0 y barW (stripH + 0.5))+                 (FillStyle (continuousColor stops t) 1.0)+                 Nothing+      ts = TextStyle "#444444" 9 "sans-serif" AnchorStart 0 "normal" False+      tickAt v =+        let t = if zMax <= zMin then 0.5 else (v - zMin) / (zMax - zMin)+        in y0 + barH * (1 - t)+      tick v = PText (Point (x0 + barW + 4) (tickAt v + 3)) (fmtNum3D v) ts+  in map strip [0 .. nStrip - 1]+     <> map tick [zMin, (zMin + zMax) / 2, zMax]++-- | tick 値の短い整形 (Axes3D の formatNum と同形)。+fmtNum3D :: Double -> Text+fmtNum3D x =+  let s = T.pack (show (fromIntegral (round (x * 10) :: Int) / 10.0 :: Double))+  in T.dropWhileEnd (== '.') (T.dropWhileEnd (== '0') s)++-- | Phase 24 A5 / #2: surface の contour を壁面へ投影する (matplotlib+-- @contour(..., zdir=)@ 相当)。 'lyr3Contours' の各 @(dir, n)@ を描く:+--+--   * 'ContourZ' (床) = z 等値面 level set @{f=z_k}@ を 'marchingSegments' で抽出し+--     floor へ落とす (topographic map)。+--   * 'ContourX' (左右壁) = x 軸の @n@ 位置で曲面を切り、 断面プロファイル+--     @z = f(x_k, y)@ を yz 壁へ描く。+--   * 'ContourY' (前後壁) = y 軸の @n@ 位置で断面 @z = f(x, y_k)@ を xz 壁へ描く。+--+-- 投影壁は **カメラから遠い面に自動固定** (eye と target の各軸符号で min/max 面を選ぶ・+-- 曲面の手前に出て遮らないように)。 grid・aspect 正規化済 layer を受ける。 mpl の+-- rotate_axes 経路 (= dir⊥平面で切った断面を壁へ投影) を再現 (実測で数値突合済)。+renderProjectedContour3D+  :: (Double, Double, Double)+  -> Camera3D -> Projection3D -> Viewport -> Layer3D -> [Primitive]+renderProjectedContour3D (xa, ya, za) cam proj vp l =+  case lyr3Contours l of+    []  -> []+    cs  ->+      let grid = fromMaybe [] (getLast (lyr3Grid l))+          ny   = length grid+          nx   = case grid of { row : _ -> length row; [] -> 0 }+      in if nx < 2 || ny < 2 then [] else+        let (xMin, xMax) = fromMaybe (-1, 1) (getLast (lyr3XRange l))   -- cube [-xa, xa]+            (yMin, yMax) = fromMaybe (-1, 1) (getLast (lyr3YRange l))   -- cube [-ya, ya]+            xNodes = [ xMin + (xMax - xMin) * fromIntegral j / fromIntegral (nx - 1)+                     | j <- [0 .. nx - 1] ]+            yNodes = [ yMin + (yMax - yMin) * fromIntegral i / fromIntegral (ny - 1)+                     | i <- [0 .. ny - 1] ]+            allZ   = concat grid+            zmin   = minimum allZ+            zmax   = maximum allZ+            stops  = fromMaybe viridisStops3D (getLast (lyr3Colormap l))+            frac lo hi v = if hi <= lo then 0.5 else (v - lo) / (hi - lo)+            colAt t = continuousColor stops (max 0 (min 1 t))+            projPt p = let pr = project3D cam proj vp p in Point (projX pr) (projY pr)+            polyline col pts = [ PLine (projPt p) (projPt q) (solid col 1.2)+                               | (p, q) <- zip pts (drop 1 pts) ]+            -- 投影壁 = カメラから遠い面。 eye と target の各軸差の符号で min/max を選ぶ+            -- (eye が +側なら −側の壁を使い、 曲面の奥に投影が回る)。+            Point3 ex ey ez = cameraEye cam+            Point3 tx ty tz = cameraTarget cam+            farWall a e t = if e >= t then negate a else a+            one (ContourZ, nLev) =+              let offset = farWall za ez tz+                  levels = innerLevels nLev zmin zmax+                  drawLevel lv =+                    [ PLine (projPt (Point3 ax' ay offset)) (projPt (Point3 bx by offset))+                            (solid (colAt (frac zmin zmax lv)) 1.2)+                    | ((ax', ay), (bx, by)) <- marchingSegments xNodes yNodes grid lv ]+              in if zmax <= zmin then [] else concatMap drawLevel levels+            one (ContourX, nLev) =+              -- x 軸を等分した位置で断面 z=f(x_k, y)。 各 y 行で grid を x 方向に補間。+              let offset = farWall xa ex tx+                  section x0 =+                    polyline (colAt (frac xMin xMax x0))+                      [ Point3 offset (yNodes !! i) (interp1 xNodes (grid !! i) x0)+                      | i <- [0 .. ny - 1] ]+              in concatMap section (innerLevels nLev xMin xMax)+            one (ContourY, nLev) =+              -- y 軸を等分した位置で断面 z=f(x, y_k)。 各 x 列で grid を y 方向に補間。+              let offset = farWall ya ey ty+                  colZ j = [ (grid !! i) !! j | i <- [0 .. ny - 1] ]+                  section y0 =+                    polyline (colAt (frac yMin yMax y0))+                      [ Point3 (xNodes !! j) offset (interp1 yNodes (colZ j) y0)+                      | j <- [0 .. nx - 1] ]+              in concatMap section (innerLevels nLev yMin yMax)+        in concatMap one cs++-- | 1 次元線形補間。 @interp1 nodes vals p@ は昇順 @nodes@ 上の値 @vals@ を位置 @p@ で+-- 補間 (範囲外は端値で clamp・隣接 2 点で線形)。 断面 contour の grid 補間に使う。+interp1 :: [Double] -> [Double] -> Double -> Double+interp1 nodes vals p = go (zip nodes vals)+  where+    go [] = 0+    go [(_, v)] = v+    go ((a, va) : rest@((b, vb) : _))+      | p <= a    = va+      | p <= b    = va + (vb - va) * (p - a) / (b - a)+      | otherwise = go rest
+ src/Graphics/Hgg/ThreeD/Line.hs view
@@ -0,0 +1,133 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Line+-- Description : 3D 線 / wireframe (Phase 3 A6)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+module Graphics.Hgg.ThreeD.Line+  ( Line3D (..)+  , defaultLine3D+  , renderLine3D+  , Wireframe3D (..)+  , defaultWireframe3D+  , renderWireframe3D+  , Quiver3D (..)+  , defaultQuiver3D+  , renderQuiver3D+  ) where++import           Data.Aeson                      (FromJSON, ToJSON)+import           Data.List                       (sortOn)+import           Data.Text                       (Text)+import           GHC.Generics                    (Generic)++import           Graphics.Hgg.Render             (LineStyle (..), Point (..),+                                                  Primitive (..))+import           Graphics.Hgg.ThreeD.Projection  (Projected (..), Viewport,+                                                  project3D)+import           Graphics.Hgg.ThreeD.Types++-- 3D 連続線+data Line3D = Line3D+  { l3Points :: ![Point3]+  , l3Color  :: !Text+  , l3Width  :: !Double+  } deriving (Show, Eq, Generic)+instance ToJSON   Line3D+instance FromJSON Line3D++defaultLine3D :: [Point3] -> Line3D+defaultLine3D ps = Line3D ps "#1f77b4" 1.5++renderLine3D :: Camera3D -> Projection3D -> Viewport -> Line3D -> [Primitive]+renderLine3D cam proj vp ln =+  let project = project3D cam proj vp+      projected = [ project p | p <- l3Points ln ]+      pairs = zip projected (drop 1 projected)+      style = LineStyle (l3Color ln) (l3Width ln) []+      mid (Projected _ _ d1) (Projected _ _ d2) = (d1 + d2) / 2+      sorted = sortOn (\(p1, p2) -> negate (mid p1 p2)) pairs+      mkLine (Projected x1 y1 _, Projected x2 y2 _) =+        PLine (Point x1 y1) (Point x2 y2) style+  in map mkLine sorted++-- wireframe (= 任意 segment 群)+data Wireframe3D = Wireframe3D+  { wfPoints :: ![Point3]+  , wfEdges  :: ![(Int, Int)]+  , wfColor  :: !Text+  , wfWidth  :: !Double+  } deriving (Show, Eq, Generic)+instance ToJSON   Wireframe3D+instance FromJSON Wireframe3D++defaultWireframe3D :: [Point3] -> [(Int, Int)] -> Wireframe3D+defaultWireframe3D pts es = Wireframe3D pts es "#2ca02c" 1.0++renderWireframe3D :: Camera3D -> Projection3D -> Viewport -> Wireframe3D -> [Primitive]+renderWireframe3D cam proj vp wf =+  let project = project3D cam proj vp+      pts = wfPoints wf+      projAll = [ project p | p <- pts ]+      style = LineStyle (wfColor wf) (wfWidth wf) []+      validEdges = [ (i, j) | (i, j) <- wfEdges wf+                            , i >= 0, j >= 0+                            , i < length pts, j < length pts ]+      mkSeg (i, j) =+        let p1 = projAll !! i+            p2 = projAll !! j+            d = (projDepth p1 + projDepth p2) / 2+        in (d, p1, p2)+      segs = map mkSeg validEdges+      sorted = sortOn (\(d, _, _) -> negate d) segs+      mkLine (_, Projected x1 y1 _, Projected x2 y2 _) =+        PLine (Point x1 y1) (Point x2 y2) style+  in map mkLine sorted++-- ===========================================================================+-- Phase 26 A3: 3D vector field (quiver3D)+-- ===========================================================================++-- | 3D vector field。 各矢印は始点 'q3Starts' → 終点 'q3Ends' (= 正規化済・+-- autoscale + aspect 適用後の点列)。 描画は両端を投影して 2D 矢印 (本線 + 矢じり)+-- にする (= mplot3d も実質 2D 矢じり)。 depth 統合外の前面 overlay。+data Quiver3D = Quiver3D+  { q3Starts :: ![Point3]   -- ^ 矢印の始点 (正規化済)+  , q3Ends   :: ![Point3]   -- ^ 矢印の終点 (正規化済・始点 + scaled vec)+  , q3Color  :: !Text+  , q3Width  :: !Double+  } deriving (Show, Eq, Generic)+instance ToJSON   Quiver3D+instance FromJSON Quiver3D++defaultQuiver3D :: [Point3] -> [Point3] -> Quiver3D+defaultQuiver3D ss es = Quiver3D ss es "#1f77b4" 1.5++renderQuiver3D :: Camera3D -> Projection3D -> Viewport -> Quiver3D -> [Primitive]+renderQuiver3D cam proj vp q =+  concatMap arrow (zip (q3Starts q) (q3Ends q))+  where+    project = project3D cam proj vp+    style = LineStyle (q3Color q) (q3Width q) []+    arrow (s, e) =+      let Projected sx sy _ = project s+          Projected ex ey _ = project e+      in arrowHead2D sx sy ex ey style++-- | 投影後の 2D 矢印 (本線 + 矢じり 2 本)。 矢じり形状は 2D quiver+-- (core Render.Basic drawArrow2D) と同じ (長さ 9px・開き比 0.5)。+arrowHead2D :: Double -> Double -> Double -> Double -> LineStyle -> [Primitive]+arrowHead2D px1 py1 px2 py2 ls =+  let dx = px2 - px1; dy = py2 - py1+      len = sqrt (dx * dx + dy * dy)+  in if len < 1e-9 then []+     else+       let ux = dx / len; uy = dy / len+           ah = 9; aw = 0.5+           bx = px2 - ux * ah; by = py2 - uy * ah+           lx = bx - uy * ah * aw; ly = by + ux * ah * aw+           rx = bx + uy * ah * aw; ry = by - ux * ah * aw+       in [ PLine (Point px1 py1) (Point px2 py2) ls+          , PLine (Point px2 py2) (Point lx ly) ls+          , PLine (Point px2 py2) (Point rx ry) ls ]
+ src/Graphics/Hgg/ThreeD/Projection.hs view
@@ -0,0 +1,185 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Projection+-- Description : 3D → 2D 投影 (Phase 3 A3)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- 標準 3D グラフィックス pipeline:+--+--   world space → (view 行列、 lookAt) → camera space+--               → (projection 行列、 ortho or persp) → clip space+--               → (perspective divide) → NDC (= [-1, 1]^3)+--               → (viewport 変換) → 2D screen 座標+--+-- 'project3D' が全 step をまとめて (Point3 world → Point2D screen + zClip) を返す。+-- z は depth cue (= painter's algorithm の sort key) に使う。+{-# LANGUAGE BangPatterns #-}+module Graphics.Hgg.ThreeD.Projection+  ( -- * Matrix 演算+    identityM+  , multM+  , transformPoint+    -- * View / Projection 行列+  , viewMatrix+  , projectionMatrix+    -- * Projection (= 1 ショット)+  , Projected (..)+  , project3D+    -- * Viewport (= NDC → screen pixel)+  , Viewport (..)+  , viewportTransform+  ) where++import           Graphics.Hgg.ThreeD.Types++-- ===========================================================================+-- Matrix 基本演算 (= 4x4)+-- ===========================================================================++-- | 単位行列。+identityM :: Mat4+identityM = Mat4 1 0 0 0  0 1 0 0  0 0 1 0  0 0 0 1++-- | 行列積 (= row-major、 自分 × 引数)。+multM :: Mat4 -> Mat4 -> Mat4+multM+  (Mat4 a11 a12 a13 a14+        a21 a22 a23 a24+        a31 a32 a33 a34+        a41 a42 a43 a44)+  (Mat4 b11 b12 b13 b14+        b21 b22 b23 b24+        b31 b32 b33 b34+        b41 b42 b43 b44) =+  Mat4+    (a11*b11 + a12*b21 + a13*b31 + a14*b41)+    (a11*b12 + a12*b22 + a13*b32 + a14*b42)+    (a11*b13 + a12*b23 + a13*b33 + a14*b43)+    (a11*b14 + a12*b24 + a13*b34 + a14*b44)++    (a21*b11 + a22*b21 + a23*b31 + a24*b41)+    (a21*b12 + a22*b22 + a23*b32 + a24*b42)+    (a21*b13 + a22*b23 + a23*b33 + a24*b43)+    (a21*b14 + a22*b24 + a23*b34 + a24*b44)++    (a31*b11 + a32*b21 + a33*b31 + a34*b41)+    (a31*b12 + a32*b22 + a33*b32 + a34*b42)+    (a31*b13 + a32*b23 + a33*b33 + a34*b43)+    (a31*b14 + a32*b24 + a33*b34 + a34*b44)++    (a41*b11 + a42*b21 + a43*b31 + a44*b41)+    (a41*b12 + a42*b22 + a43*b32 + a44*b42)+    (a41*b13 + a42*b23 + a43*b33 + a44*b43)+    (a41*b14 + a42*b24 + a43*b34 + a44*b44)++-- | Point3 を 4x4 行列で変換 (= 同次座標で w=1 として扱い、 結果も Point3 返す)。+-- w 成分は別途扱うので 'project3D' を経由するのが通常。+transformPoint :: Mat4 -> Point3 -> Point3+transformPoint+  (Mat4 m11 m12 m13 m14+        m21 m22 m23 m24+        m31 m32 m33 m34+        _ _ _ _)+  (Point3 x y z) =+  Point3+    (m11 * x + m12 * y + m13 * z + m14)+    (m21 * x + m22 * y + m23 * z + m24)+    (m31 * x + m32 * y + m33 * z + m34)++-- ===========================================================================+-- View 行列 (= lookAt) / Projection 行列+-- ===========================================================================++-- | lookAt: camera の view 行列を生成。 右手系、 OpenGL 流。+--+-- 結果: world space の点 P に対し viewMatrix * P が camera space の P' を返す。+-- camera space では camera が原点、 -z 方向を向く (= 右手系 OpenGL 慣例)。+viewMatrix :: Camera3D -> Mat4+viewMatrix (Camera3D (Point3 ex ey ez) (Point3 tx ty tz) up) =+  let -- forward = normalize(target - eye)、 ただし camera は -z を向くので f は実は反転して使う+      f@(Vec3 fx fy fz) = normalizeV3 (Vec3 (tx - ex) (ty - ey) (tz - ez))+      -- right = normalize(forward × up_raw)+      r@(Vec3 rx ry rz) = normalizeV3 (crossV3 f up)+      -- up = right × forward (= 直交化)+      Vec3 ux uy uz = crossV3 r f+      -- camera space: x → right、 y → up、 z → -forward+      tx' = - (rx * ex + ry * ey + rz * ez)+      ty' = - (ux * ex + uy * ey + uz * ez)+      tz' =   (fx * ex + fy * ey + fz * ez)  -- 負号は -forward 経由で打消し+  in Mat4+       rx       ry       rz       tx'+       ux       uy       uz       ty'+       (-fx)    (-fy)    (-fz)    tz'+       0        0        0        1++-- | Projection 行列を生成。 'Orthographic' / 'Perspective' どちらも対応。+--+-- 出力: camera space の点 P → clip space の P' (= w 成分も含む)。+-- 'project3D' で perspective divide (= x,y,z を w で割る) を続けて NDC へ。+projectionMatrix :: Projection3D -> Mat4+projectionMatrix (Orthographic xH yH n f) =+  let !sx = 1 / xH+      !sy = 1 / yH+      !sz = -2 / (f - n)+      !tz = -(f + n) / (f - n)+  in Mat4 sx 0  0  0   0  sy 0  0   0  0  sz tz   0 0 0 1+projectionMatrix (Perspective fov aspect n f) =+  let !ft  = 1 / tan (fov / 2)         -- focal term (= cot(fov/2))+      !sx  = ft / aspect+      !sy  = ft+      !sz  = -(f + n) / (f - n)+      !tz  = -(2 * f * n) / (f - n)+  in Mat4 sx 0  0  0   0  sy 0  0   0  0  sz tz   0 0 (-1) 0++-- ===========================================================================+-- Projection 一発+-- ===========================================================================++-- | Viewport: NDC (= [-1, 1]^2) → screen pixel への変換パラメタ。+-- 通常 Layout 領域に合わせて (xMin, yMin, width, height) で指定。+data Viewport = Viewport+  { vpX :: !Double+  , vpY :: !Double+  , vpW :: !Double+  , vpH :: !Double+  } deriving (Show, Eq)++-- | NDC [-1,1] の (x, y) → screen pixel への変換。 y は反転 (= SVG 慣例で y 下方向)。+viewportTransform :: Viewport -> Double -> Double -> (Double, Double)+viewportTransform (Viewport vx vy vw vh) ndcX ndcY =+  let sx = vx + (ndcX + 1) / 2 * vw+      sy = vy + (1 - (ndcY + 1) / 2) * vh+  in (sx, sy)++-- | 投影結果。 screen 座標 (= viewport 変換後) + z (= depth cue / sort 用、 NDC z)。+data Projected = Projected+  { projX     :: !Double  -- ^ screen pixel x+  , projY     :: !Double  -- ^ screen pixel y+  , projDepth :: !Double  -- ^ NDC z ∈ [-1, 1]、 -1 が手前、 +1 が奥+  } deriving (Show, Eq)++-- | World point → Projected (screen + depth)。+--+-- pipeline: world → view → projection → perspective divide → viewport。+project3D :: Camera3D -> Projection3D -> Viewport -> Point3 -> Projected+project3D cam proj vp p =+  let view = viewMatrix cam+      pj   = projectionMatrix proj+      mvp  = pj `multM` view+      -- 同次座標で 4 成分計算 (= w が必要)+      Mat4 m11 m12 m13 m14+           m21 m22 m23 m24+           m31 m32 m33 m34+           m41 m42 m43 m44 = mvp+      Point3 x y z = p+      cx = m11 * x + m12 * y + m13 * z + m14+      cy = m21 * x + m22 * y + m23 * z + m24+      cz = m31 * x + m32 * y + m33 * z + m34+      cw = m41 * x + m42 * y + m43 * z + m44+      -- perspective divide+      w' = if abs cw < 1e-12 then 1 else cw+      ndcX = cx / w'+      ndcY = cy / w'+      ndcZ = cz / w'+      (sx, sy) = viewportTransform vp ndcX ndcY+  in Projected sx sy ndcZ
+ src/Graphics/Hgg/ThreeD/Scatter.hs view
@@ -0,0 +1,87 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Scatter+-- Description : 3D scatter plot (Phase 3 A5)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- 3D 点群を camera + projection で 2D に変換し、 painter's algorithm (= z で sort、+-- 奥から手前へ) で描画。 depth cue として size と alpha を z で線形に減衰させる。+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+module Graphics.Hgg.ThreeD.Scatter+  ( Scatter3D (..)+  , defaultScatter3D+  , renderScatter3D+  , scatterPointsDepth+  ) where++import           Data.Aeson                      (FromJSON, ToJSON)+import           Data.List                       (sortOn)+import           Data.Text                       (Text)+import           GHC.Generics                    (Generic)++import           Graphics.Hgg.Render             (FillStyle (..),+                                                  Point (..), Primitive (..),+                                                  StrokeStyle (..))+import           Graphics.Hgg.ThreeD.Projection  (Projected (..), Viewport,+                                                  project3D)+import           Graphics.Hgg.ThreeD.Types++-- | 3D scatter 1 series 分の設定。+data Scatter3D = Scatter3D+  { sc3Points :: ![Point3]   -- ^ 3D 点群+  , sc3Color  :: !Text       -- ^ 色 (= hex 例 "#1f77b4")+  , sc3Size   :: !Double     -- ^ 基本 size (= 近側の半径 px、 奥は depth cue で小さく)+  , sc3Alpha  :: !Double     -- ^ 基本 alpha (= 近側の値、 奥は depth cue で薄く)+  , sc3Colors :: !(Maybe [Text])+    -- ^ Phase 25 A2: 点ごとの色 (= 群色分け / 連続色マップ)。 @Just cs@ で+    --   @cs !! i@ を点 i に使う (長さが点数未満なら超過点は 'sc3Color')。+    --   @Nothing@ = 全点 'sc3Color' (後方互換)。+  , sc3Sizes  :: !(Maybe [Double])+    -- ^ Phase 25 A3: 点ごとの基本 size (= 値マップ bubble)。 @Just ss@ で+    --   @ss !! i@ を点 i の基本半径に使う (長さが点数未満なら超過点は 'sc3Size')。+    --   @Nothing@ = 全点 'sc3Size' (後方互換)。 depth cue は基本 size に乗算。+  } deriving (Show, Eq, Generic)+instance ToJSON   Scatter3D+instance FromJSON Scatter3D++-- | default: 青、 size 5、 alpha 0.85、 単色・単 size (per-point 無し)。+defaultScatter3D :: [Point3] -> Scatter3D+defaultScatter3D ps = Scatter3D ps "#1f77b4" 5 0.85 Nothing Nothing++-- | 3D scatter を 2D Primitive (= PCircle) 列に変換。+-- z で sort して painter's (= 奥から描画)、 size / alpha は depth cue で減衰。+renderScatter3D+  :: Camera3D -> Projection3D -> Viewport+  -> Scatter3D -> [Primitive]+renderScatter3D cam proj vp sc =+  map snd (sortOn (negate . fst) (scatterPointsDepth cam proj vp sc))++-- | Phase 24 A8 (depth 統合): 各点を @(投影 depth, PCircle)@ として**未ソート**で+-- 返す (= 'renderScatter3D' は @sortOn (negate.fst)@ → @map snd@・ビット同一)。+-- surface 面との層横断 depth 統合に使う (scatter 点が膜を透ける問題の解消)。+scatterPointsDepth+  :: Camera3D -> Projection3D -> Viewport+  -> Scatter3D -> [(Double, Primitive)]+scatterPointsDepth cam proj vp sc =+  let project = project3D cam proj vp+      stroke = Just (StrokeStyle "#333333" 0.5)+      -- Phase 25 A2: 点ごとの色 (無ければ単色)。 index で引く。+      colorAt i = case sc3Colors sc of+        Just cs | i < length cs -> cs !! i+        _                       -> sc3Color sc+      -- Phase 25 A3: 点ごとの基本 size (無ければ単 size)。 index で引く。+      sizeAt i = case sc3Sizes sc of+        Just ss | i < length ss -> ss !! i+        _                       -> sc3Size sc+      mkCircle baseR col (Projected sx sy d) =+        let depthCue = max 0 (min 1 ((1 - d) / 2))+            r = baseR * (0.55 + 0.45 * depthCue)+            a = sc3Alpha sc * (0.4 + 0.6 * depthCue)+        in PCircle (Point sx sy) r+                   (FillStyle col a)+                   stroke+                   Nothing+  in [ (d, mkCircle (sizeAt i) (colorAt i) pr)+     | (i, p) <- zip [0 ..] (sc3Points sc)+     , let pr@(Projected _ _ d) = project p ]
+ src/Graphics/Hgg/ThreeD/Spec.hs view
@@ -0,0 +1,974 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Spec+-- Description : 3D VisualSpec / Layer + Monoid (Phase 5 A3)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- 2D 同型 (= Graphics.Hgg.Spec の 'VisualSpec' / 'Layer' / Monoid 構成) を踏襲した+-- 3D 用 spec API。 ユーザ視点で 2D と同じ構文で組める:+--+-- @+-- -- 2D+-- purePlot <> layer (scatter x y <> color (fromHex "#ff0000") <> alpha 0.7) <> title "..."+--+-- -- 3D (= 構文 100% 同型)+-- purePlot3D <> layer3D (scatter3D pts <> color3D (fromHex "#ff0000") <> alpha3D 0.7)+--            <> camera (defaultCameraZUp 3) <> axes3D defaultAxes3D <> title3D "..."+-- @+--+-- 値型は分離 (= 'Layer' と 'Layer3D' は別)。 理由は phase-5 計画 md §2.2。+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+module Graphics.Hgg.ThreeD.Spec+  ( -- * MarkKind3D+    Mark3DKind (..)+    -- * Bar スタイル (Phase 25 A5・Bar module 再 export)+  , BarStyle3D (..)+    -- * Layer3D (= 1 layer の per-field Monoid)+  , Layer3D (..)+    -- * VisualSpec3D+  , VisualSpec3D (..)+    -- * 純粋起点+  , purePlot3D+    -- * Layer 起点 (= 2D 'layer' 同型)+  , layer3D+    -- * Mark コンストラクタ (= Layer3D 返却)+  , scatter3D+  , scatter3DPoints+  , line3D+  , line3DPoints+  , wireframe3D+  , surface3D+  , surface3DGrid+  , bar3D+  , bar3DPoints+  , stem3D+  , stem3DPoints+  , stemBaseZ+  , quiver3D+  , vecScale3D+  , trisurf+  , text3D+  , text3DPoints+  , annotate3D+    -- * per-layer 属性 (= Layer3D 返却)+  , color3D+  , colorRGBA3D+  , colorRGBA3DMaybe+  , colorBy3D+  , colorContinuousBy3D+  , sizeBy3D+  , sizeRange3D+  , barStyle3D+  , barWidth3D+  , errorBar3D+  , edgeColor3D+  , size3D+  , alpha3D+  , width3D+  , shaded3D+  , colormap3D+  , colormapWith3D+  , contourX+  , contourY+  , contourZ+  , ContourDir (..)+  , surfaceWire+  , viridisStops3D+  , xRange3D+  , yRange3D+    -- * VisualSpec3D 属性+  , camera+  , projection+  , axes3D+  , title3D+  , axisTitles3D+  , zAspect3D+  , pane3D+  , xAspect3D+  , yAspect3D+  , logScale3D+  , width3DV+  , height3DV+    -- * 変換 (= Layer3D を既存 Scatter3D/... に展開、 render 経路で使う)+  , layerToScatter+  , layerToLine+  , layerToWireframe+  , layerToSurface+  , layerToBar+  , layerToQuiver+    -- * 列参照の解決 (Phase 24 A6)+  , resolveLayer3D+  , resolveSpec3D+    -- * Helper: layers から bounding box 自動算出 (= Axes 未指定時の default)+  , autoAxes3D+  , layerPoints+  ) where++import           Data.Aeson                     (FromJSON, ToJSON)+import           Data.Maybe                     (fromMaybe)+import           Data.Monoid                    (First (..), Last (..))+import           Data.Text                      (Text)+import           GHC.Generics                   (Generic)++import           Graphics.Hgg.ThreeD.Axes       (Axes3D (..), defaultAxes3D)+import           Graphics.Hgg.ThreeD.Line       (Line3D (..), Wireframe3D (..),+                                                 defaultLine3D, defaultWireframe3D,+                                                 Quiver3D (..), defaultQuiver3D)+import           Graphics.Hgg.ThreeD.Scatter    (Scatter3D (..), defaultScatter3D)+import           Graphics.Hgg.ThreeD.Surface    (Surface3D (..), defaultSurface3D)+import           Graphics.Hgg.ThreeD.Bar        (Bar3D (..), BarStyle3D (..),+                                                 defaultBar3D)+import           Graphics.Hgg.ThreeD.Delaunay   (delaunay2D)+import           Graphics.Hgg.Spec              (ColRef, Resolver, resolveNum, resolveCol,+                                                 ColData (..))+import           Graphics.Hgg.Color             (Color, toCss, fromHexA, fromHexAMaybe)+import           Graphics.Hgg.Palette           (ggplotHue)+import           Graphics.Hgg.Render.Common     (continuousColor)+import           Data.List                      (nub)+import qualified Data.Map.Strict                as M+import qualified Data.Text                      as T+import qualified Data.Vector                    as V+import           Graphics.Hgg.ThreeD.Types++-- ===========================================================================+-- MarkKind3D+-- ===========================================================================++-- | 3D layer の幾何種別 (= 2D 'MarkKind' の 3D 版)。+data Mark3DKind+  = M3Scatter+  | M3Line+  | M3Wireframe+  | M3Surface+  | M3Bar          -- Phase 25 A5: 3D 棒 (直方体 / stick)+  | M3Text         -- Phase 25 A7: 3D テキスト注釈 (任意点にラベル)+  | M3Stem         -- Phase 26 A4: 3D stem (底面 z0 への垂線 + 先端マーカー・3D lollipop)+  | M3Quiver       -- Phase 26 A3: 3D vector field (各点に成分ベクトルの矢印)+  | M3Trisurf      -- Phase 26 A5: 不規則点群を Delaunay 三角分割して曲面化+  deriving (Show, Eq, Generic)+instance ToJSON   Mark3DKind+instance FromJSON Mark3DKind++-- ===========================================================================+-- Layer3D (= 2D 'Layer' 同型、 per-field First/Last Monoid)+-- ===========================================================================++-- | 3D 1 layer の rolled-up 表現。 全 field が 'First' or 'Last' なので per-field+--   '(<>)' で自然に合成可能 (= 2D 'Layer' と同じ規律)。+--+--   * 'lyr3Kind' は 'First' (= 最初に設定した kind が勝つ、 2D 'lyKind' と同じ)+--   * その他属性は 'Last' (= 後勝ち、 2D 'lyColor' 等と同じ)+data Layer3D = Layer3D+  { lyr3Kind      :: !(First Mark3DKind)+  , lyr3Points    :: !(Last [Point3])+  , lyr3Color     :: !(Last Text)+  , lyr3Size      :: !(Last Double)+  , lyr3Alpha     :: !(Last Double)+  , lyr3Width     :: !(Last Double)+  , lyr3Edges     :: !(Last [(Int, Int)])    -- Wireframe 用+  , lyr3Grid      :: !(Last [[Double]])      -- Surface 用+  , lyr3XRange    :: !(Last (Double, Double))+  , lyr3YRange    :: !(Last (Double, Double))+  , lyr3Shaded    :: !(Last Bool)            -- Surface 用+  , lyr3EdgeColor :: !(Last Text)            -- Surface 用+  , lyr3Colormap  :: !(Last [Text])          -- Surface 用 (Phase 24 A2: z 連続色 stops)+  , lyr3Contours  :: ![(ContourDir, Int)]    -- Surface 用: 投影 contour 群 (各 (軸, 本数))。 X/Y 壁=断面・+                                             --   Z 床=等値面。 同一 surface に複数合成可・投影壁はカメラから遠い面に自動固定+  , lyr3SurfaceWire :: !(Last Bool)          -- Surface 用: 面を塗らず格子線メッシュで描く (matplotlib plot_wireframe 相当)+  , lyr3EncX      :: !(Last ColRef)          -- Phase 24 A6: 列参照 (x)+  , lyr3EncY      :: !(Last ColRef)          -- Phase 24 A6: 列参照 (y)+  , lyr3EncZ      :: !(Last ColRef)          -- Phase 24 A6: 列参照 (z)+  , lyr3ColorBy   :: !(Last ColRef)          -- Phase 25 A2: 群色分けのカテゴリ列+  , lyr3PtColors  :: !(Last [Text])          -- Phase 25 A2/A3: 解決後の点ごと色 (resolve 産物)+  , lyr3Legend    :: !(Last [(Text, Text)])  -- Phase 25 A2: 離散凡例 (カテゴリ, 色)+  , lyr3ColorByV  :: !(Last ColRef)          -- Phase 25 A3: 連続色マップの数値列+  , lyr3SizeBy    :: !(Last ColRef)          -- Phase 25 A3: size マップ (bubble) の数値列+  , lyr3SizeRange :: !(Last (Double, Double))-- Phase 25 A3: size マップの px 範囲 (既定 (4,18))+  , lyr3PtSizes   :: !(Last [Double])        -- Phase 25 A3: 解決後の点ごと基本 size (resolve 産物)+  , lyr3Colorbar  :: !(Last ([Text], Double, Double))+                                             -- Phase 25 A3: 連続色 colorbar (stops, min, max・resolve 産物)+  , lyr3BarStyle  :: !(Last BarStyle3D)      -- Phase 25 A5: bar スタイル (既定 BarCuboid)+  , lyr3BarWidth  :: !(Last Double)          -- Phase 25 A5: bar footprint 半幅 (軸 span 比・既定 0.04)+  , lyr3BarBaseZ  :: !(Last Double)          -- Phase 25 A5: bar 底面 z (正規化値・normLayer3D 産物)+  , lyr3ErrBy     :: !(Last ColRef)          -- Phase 25 A5: 誤差棒の err 数値列+  , lyr3PtErrs    :: !(Last [Double])        -- Phase 25 A5: 解決後の点ごと err (data→正規化は normLayer3D)+  , lyr3Labels    :: !(Last [Text])          -- Phase 25 A7: テキスト注釈ラベル (lyr3Points と整列)+  , lyr3StemBaseZ :: !(Last Double)          -- Phase 26 A4: stem の底面 z (data 空間・既定 0。 normLayer3D が lyr3BarBaseZ へ正規化)+  , lyr3Vectors   :: !(Last [Vec3])          -- Phase 26 A3: quiver3D の各点ベクトル (data 空間→normLayer3D で per-axis 正規化)+  , lyr3VecScale  :: !(Last Double)          -- Phase 26 A3: quiver3D 矢印長の倍率 (autoscale × この値・既定 1)+  , lyr3Faces     :: !(Last [(Int, Int, Int)])+                                             -- Phase 27 A2: trisurf の face index (resolve 産物・座標非依存)+                                             --   WebGL は z-buffer ゆえ CPU の depth ソート不要。 face は (x,y) Delaunay で前計算。+  , lyr3TextBy    :: !(Last ColRef)          -- Phase 30 A6: 列駆動 'text3D' の label 列 (resolveLayer3D で lyr3Labels へ)+  , lyr3Annots    :: ![(Point3, Text)]       -- Phase 30 A6: inline 注釈 ('text3DPoints'/'annotate3D')。+                                             --   plain list = concat Monoid ゆえ 'annotate3D' を <> で畳める (lyr3Points/Labels は Last で後勝ち・畳めない)。+                                             --   normLayer3D の M3Text で lyr3Points/lyr3Labels に materialize → renderer 無改修。+  } deriving (Show, Eq, Generic)+instance ToJSON   Layer3D+instance FromJSON Layer3D++-- 25 field と多いので positional でなく record 構文で per-field '(<>)' する+-- (= field 追加時の取り違え防止。 Phase 25 A3 で 20→25 field に拡張した際に変更)。+instance Semigroup Layer3D where+  x <> y = Layer3D+    { lyr3Kind      = lyr3Kind      x <> lyr3Kind      y+    , lyr3Points    = lyr3Points    x <> lyr3Points    y+    , lyr3Color     = lyr3Color     x <> lyr3Color     y+    , lyr3Size      = lyr3Size      x <> lyr3Size      y+    , lyr3Alpha     = lyr3Alpha     x <> lyr3Alpha     y+    , lyr3Width     = lyr3Width     x <> lyr3Width     y+    , lyr3Edges     = lyr3Edges     x <> lyr3Edges     y+    , lyr3Grid      = lyr3Grid      x <> lyr3Grid      y+    , lyr3XRange    = lyr3XRange    x <> lyr3XRange    y+    , lyr3YRange    = lyr3YRange    x <> lyr3YRange    y+    , lyr3Shaded    = lyr3Shaded    x <> lyr3Shaded    y+    , lyr3EdgeColor = lyr3EdgeColor x <> lyr3EdgeColor y+    , lyr3Colormap  = lyr3Colormap  x <> lyr3Colormap  y+    , lyr3Contours  = lyr3Contours  x <> lyr3Contours  y+    , lyr3SurfaceWire = lyr3SurfaceWire x <> lyr3SurfaceWire y+    , lyr3EncX      = lyr3EncX      x <> lyr3EncX      y+    , lyr3EncY      = lyr3EncY      x <> lyr3EncY      y+    , lyr3EncZ      = lyr3EncZ      x <> lyr3EncZ      y+    , lyr3ColorBy   = lyr3ColorBy   x <> lyr3ColorBy   y+    , lyr3PtColors  = lyr3PtColors  x <> lyr3PtColors  y+    , lyr3Legend    = lyr3Legend    x <> lyr3Legend    y+    , lyr3ColorByV  = lyr3ColorByV  x <> lyr3ColorByV  y+    , lyr3SizeBy    = lyr3SizeBy    x <> lyr3SizeBy    y+    , lyr3SizeRange = lyr3SizeRange x <> lyr3SizeRange y+    , lyr3PtSizes   = lyr3PtSizes   x <> lyr3PtSizes   y+    , lyr3Colorbar  = lyr3Colorbar  x <> lyr3Colorbar  y+    , lyr3BarStyle  = lyr3BarStyle  x <> lyr3BarStyle  y+    , lyr3BarWidth  = lyr3BarWidth  x <> lyr3BarWidth  y+    , lyr3BarBaseZ  = lyr3BarBaseZ  x <> lyr3BarBaseZ  y+    , lyr3ErrBy     = lyr3ErrBy     x <> lyr3ErrBy     y+    , lyr3PtErrs    = lyr3PtErrs    x <> lyr3PtErrs    y+    , lyr3Labels    = lyr3Labels    x <> lyr3Labels    y+    , lyr3StemBaseZ = lyr3StemBaseZ x <> lyr3StemBaseZ y+    , lyr3Vectors   = lyr3Vectors   x <> lyr3Vectors   y+    , lyr3VecScale  = lyr3VecScale  x <> lyr3VecScale  y+    , lyr3Faces     = lyr3Faces     x <> lyr3Faces     y+    , lyr3TextBy    = lyr3TextBy    x <> lyr3TextBy    y+    , lyr3Annots    = lyr3Annots    x <> lyr3Annots    y+    }++instance Monoid Layer3D where+  mempty = Layer3D mempty mempty mempty mempty mempty mempty+                   mempty mempty mempty mempty mempty mempty mempty+                   mempty mempty mempty mempty mempty mempty mempty+                   mempty mempty mempty mempty mempty+                   mempty mempty mempty mempty mempty mempty mempty+                   mempty mempty mempty mempty mempty mempty++-- ===========================================================================+-- VisualSpec3D (= 2D 'VisualSpec' 同型、 layers ++ + per-field Last)+-- ===========================================================================++-- | 3D 図全体の spec。 'layers' は append、 その他は 'Last' (= 後勝ち)。+data VisualSpec3D = VisualSpec3D+  { vs3Layers     :: ![Layer3D]+  , vs3Title      :: !(Last Text)+  , vs3Camera     :: !(Last Camera3D)+  , vs3Proj       :: !(Last Projection3D)+  , vs3Axes       :: !(Last Axes3D)+  , vs3Width      :: !(Last Int)+  , vs3Height     :: !(Last Int)+  , vs3AxisTitles :: !(Last (Text, Text, Text))  -- Phase 24 A8: 軸名 (既定 "x"/"y"/"z")+  , vs3ZAspect    :: !(Last Double)               -- Phase 24 A8: z 軸の box 縦横比 (既定 1)+  , vs3Pane       :: !(Last Bool)                 -- Phase 25 A6: 壁面 pane + gridline (既定 ON)+  , vs3XAspect    :: !(Last Double)               -- Phase 25 A8: x 軸の box 縦横比 (既定 1)+  , vs3YAspect    :: !(Last Double)               -- Phase 25 A8: y 軸の box 縦横比 (既定 1)+  , vs3Log        :: !(Last (Bool, Bool, Bool))   -- Phase 25 A8: 軸 log scale (x,y,z・既定 全 False)+  } deriving (Show, Eq, Generic)+instance ToJSON   VisualSpec3D+instance FromJSON VisualSpec3D++instance Semigroup VisualSpec3D where+  VisualSpec3D l1 t1 c1 p1 a1 w1 h1 at1 za1 pn1 xa1 ya1 lg1+    <> VisualSpec3D l2 t2 c2 p2 a2 w2 h2 at2 za2 pn2 xa2 ya2 lg2 =+      VisualSpec3D (l1 <> l2) (t1 <> t2) (c1 <> c2)+                   (p1 <> p2) (a1 <> a2) (w1 <> w2) (h1 <> h2)+                   (at1 <> at2) (za1 <> za2) (pn1 <> pn2)+                   (xa1 <> xa2) (ya1 <> ya2) (lg1 <> lg2)++instance Monoid VisualSpec3D where+  mempty = VisualSpec3D [] mempty mempty mempty mempty mempty mempty mempty mempty mempty+                        mempty mempty mempty++-- ===========================================================================+-- 純粋起点 + lift helpers (= 2D 'purePlot' / 'layer' 同型)+-- ===========================================================================++-- | 'VisualSpec3D' 起点 (= 2D 'purePlot' 同型、 'mempty' alias)。+purePlot3D :: VisualSpec3D+purePlot3D = mempty++-- | 'Layer3D' を 'VisualSpec3D' に lift (= 2D 'layer' 同型)。+layer3D :: Layer3D -> VisualSpec3D+layer3D l = mempty { vs3Layers = [l] }++-- ===========================================================================+-- Mark コンストラクタ (= Layer3D を返却、 後で `<> color3D ...` で属性 append)+-- ===========================================================================++-- | 3D scatter (= M3Scatter)。 Phase 24 A6: 2D 'Graphics.Hgg.Spec.scatter' と+-- 対称の **ColRef 3 つ** (x, y, z)。 列名 (OverloadedStrings) か 'inline' の+-- 生値を渡す。 旧 [Point3] 直入れは 'scatter3DPoints'。+scatter3D :: ColRef -> ColRef -> ColRef -> Layer3D+scatter3D x y z = mempty+  { lyr3Kind = First (Just M3Scatter)+  , lyr3EncX = Last (Just x), lyr3EncY = Last (Just y), lyr3EncZ = Last (Just z)+  }++-- | 3D scatter ([Point3] 直入れ・旧 'scatter3D')。+scatter3DPoints :: [Point3] -> Layer3D+scatter3DPoints pts = mempty+  { lyr3Kind   = First (Just M3Scatter)+  , lyr3Points = Last  (Just pts)+  }++-- | 3D line (= MLine3D、 連続折れ線)。+line3D :: ColRef -> ColRef -> ColRef -> Layer3D+line3D x y z = mempty+  { lyr3Kind = First (Just M3Line)+  , lyr3EncX = Last (Just x), lyr3EncY = Last (Just y), lyr3EncZ = Last (Just z)+  }++-- | 3D line ([Point3] 直入れ・旧 'line3D')。+line3DPoints :: [Point3] -> Layer3D+line3DPoints pts = mempty+  { lyr3Kind   = First (Just M3Line)+  , lyr3Points = Last  (Just pts)+  }++-- | 3D wireframe (= MWireframe3D、 任意 edge 群)。+wireframe3D :: [Point3] -> [(Int, Int)] -> Layer3D+wireframe3D pts es = mempty+  { lyr3Kind   = First (Just M3Wireframe)+  , lyr3Points = Last  (Just pts)+  , lyr3Edges  = Last  (Just es)+  }++-- | Phase 30 A6: 列駆動 3D surface (案C・= 2D 系と対称の ColRef 3 つ x/y/z)。 df の+--   long 形 (各行 = 格子点 (x,y,z)) を受け、 'resolveLayer3D' で **内部 pivot** して+--   grid mesh ('lyr3Grid' + xRange/yRange) に落とす。 grid は x/y が規則格子である前提+--   (= 全 (x_i,y_j) 組が揃う・欠損は NaN 穴)。 既に行列を持っている場合は 'surface3DGrid'。+--+--   @df |>> layer3D (surface3D \"x\" \"y\" \"z\" <> colormap3D)@+surface3D :: ColRef -> ColRef -> ColRef -> Layer3D+surface3D x y z = mempty+  { lyr3Kind = First (Just M3Surface)+  , lyr3EncX = Last (Just x), lyr3EncY = Last (Just y), lyr3EncZ = Last (Just z)+  }++-- | 3D surface (= MSurface3D、 grid mesh・行列直入れ・旧 'surface3D')。 z 値の+--   2 次元配列を受け、 x/y 範囲は 'xRange3D'/'yRange3D' (既定 (-1,1)) で与える。+surface3DGrid :: [[Double]] -> Layer3D+surface3DGrid grid = mempty+  { lyr3Kind = First (Just M3Surface)+  , lyr3Grid = Last  (Just grid)+  }++-- | Phase 25 A5: 3D bar (= M3Bar)。 'scatter3D' と対称の ColRef 3 つ (x, y,+--   高さ z)。 各 (x,y) に底面 (data z=0) から高さ z までの棒を立てる。 既定は+--   直方体 ('barStyle3D' で stick へ)。 [Point3] 直入れは 'bar3DPoints'。+bar3D :: ColRef -> ColRef -> ColRef -> Layer3D+bar3D x y z = mempty+  { lyr3Kind = First (Just M3Bar)+  , lyr3EncX = Last (Just x), lyr3EncY = Last (Just y), lyr3EncZ = Last (Just z)+  }++-- | Phase 25 A5: 3D bar ([Point3] 直入れ・各点の z = 棒の高さ)。+bar3DPoints :: [Point3] -> Layer3D+bar3DPoints tops = mempty+  { lyr3Kind   = First (Just M3Bar)+  , lyr3Points = Last  (Just tops)+  }++-- | Phase 26 A4: 3D stem (= M3Stem)。 'bar3D' と対称の ColRef 3 つ (x, y, z)。+--   各 (x,y) に底面 ('stemBaseZ'・既定 0) から z までの細い垂線 + 先端マーカーを+--   描く (3D lollipop)。 [Point3] 直入れは 'stem3DPoints'。 底面は 'stemBaseZ' で変更。+--+--   @stem3D \"x\" \"y\" \"z\" <> color3D (fromHex \"#d62728\") <> stemBaseZ 0@+stem3D :: ColRef -> ColRef -> ColRef -> Layer3D+stem3D x y z = mempty+  { lyr3Kind = First (Just M3Stem)+  , lyr3EncX = Last (Just x), lyr3EncY = Last (Just y), lyr3EncZ = Last (Just z)+  }++-- | Phase 26 A4: 3D stem ([Point3] 直入れ・各点の z = stem の先端高さ)。+stem3DPoints :: [Point3] -> Layer3D+stem3DPoints tops = mempty+  { lyr3Kind   = First (Just M3Stem)+  , lyr3Points = Last  (Just tops)+  }++-- | Phase 26 A4: stem の底面 z (data 空間・既定 0)。 mplot3d @stem(..., bottom=)@ 相当。+--   'normLayer3D' が axes に基づき正規化して 'lyr3BarBaseZ' へ落とす。+stemBaseZ :: Double -> Layer3D+stemBaseZ z = mempty { lyr3StemBaseZ = Last (Just z) }++-- | Phase 26 A3: 3D vector field (= M3Quiver)。 各 @(位置, ベクトル)@ に矢印を描く+--   (= mplot3d @quiver@)。 矢印長は autoscale (= 最長矢印が cube の ~35%) に+--   'vecScale3D' 倍を掛けた長さ。 描画は投影後の 2D 矢印 (本線 + 矢じり)。+--+--   @quiver3D [(Point3 0 0 0, Vec3 1 0 0), (Point3 1 1 1, Vec3 0 0 1)]@+quiver3D :: [(Point3, Vec3)] -> Layer3D+quiver3D items = mempty+  { lyr3Kind    = First (Just M3Quiver)+  , lyr3Points  = Last  (Just (map fst items))+  , lyr3Vectors = Last  (Just (map snd items))+  }++-- | Phase 26 A3: quiver3D 矢印長の倍率 (autoscale × この値・既定 1)。+vecScale3D :: Double -> Layer3D+vecScale3D s = mempty { lyr3VecScale = Last (Just s) }++-- | Phase 26 A5: trisurf (= M3Trisurf)。 不規則 (非 grid) な 3D 点群を (x,y) 平面で+--   Delaunay 三角分割して曲面化する (= mplot3d @plot_trisurf@)。 散らばった観測点・+--   GP 事後点など、 規則 grid でない曲面を描ける。 z 連続色は 'colormap3D' で。+--+--   @trisurf pts <> colormap3D viridisStops3D@+trisurf :: [Point3] -> Layer3D+trisurf pts = mempty+  { lyr3Kind   = First (Just M3Trisurf)+  , lyr3Points = Last  (Just pts)+  }++-- | Phase 30 A6: 列駆動の 3D テキスト注釈 (= 2D 'Graphics.Hgg.Spec.text' と対称・+--   ColRef 4 つ x/y/z + **label 列**)。 df の各行を投影し、 label 列の文字を PText で+--   置く。 解決は 'resolveLayer3D' (x/y/z → 'lyr3Points'・label → 'lyr3Labels')。+--   inline 生値版は 'text3DPoints'。 文字色は 'color3D' (既定 @#333333@)、 サイズは+--   'size3D' (既定 11)。+--+--   @text3D \"x\" \"y\" \"z\" \"name\"@+text3D :: ColRef -> ColRef -> ColRef -> ColRef -> Layer3D+text3D x y z lab = mempty+  { lyr3Kind   = First (Just M3Text)+  , lyr3EncX = Last (Just x), lyr3EncY = Last (Just y), lyr3EncZ = Last (Just z)+  , lyr3TextBy = Last (Just lab)+  }++-- | Phase 30 A6: 3D テキスト注釈 (inline・[(点, 文字列)] 直入れ・旧 'text3D')。 各+--   @(点, 文字列)@ を投影して PText を出す (depth 統合外の前面 overlay)。 位置は他+--   mark と同じ正規化 / z-aspect pipeline を通る。 'lyr3Annots' (concat Monoid) に+--   積むので 'annotate3D' と @<>@ で畳める。+--+--   @text3DPoints [(Point3 0 0 1, \"peak\"), (Point3 1 1 0, \"corner\")]@+text3DPoints :: [(Point3, Text)] -> Layer3D+text3DPoints items = mempty+  { lyr3Kind   = First (Just M3Text)+  , lyr3Annots = items+  }++-- | Phase 25 A7 / 30 A6: 単一ラベルの注釈。 'lyr3Annots' に 1 件積むので+--   @annotate3D a \"A\" <> annotate3D b \"B\"@ のように **畳める** (= 複数注釈が累積。+--   旧実装は 'lyr3Labels' が Last で後勝ち → 畳めなかった)。+--+--   @annotate3D (Point3 0 0 1) \"max\" <> color3D (fromHex \"#d62728\") <> size3D 13@+annotate3D :: Point3 -> Text -> Layer3D+annotate3D p t = text3DPoints [(p, t)]++-- ===========================================================================+-- per-layer 属性 (= 2D 'color' / 'size' / 'alpha' 同型、 Layer3D 返却)+-- ===========================================================================++-- | Phase 30 A5: 固定色 (= 2D 'Graphics.Hgg.Spec.color' 同型・型安全な 'Color')。+--   @scatter3D pts <> color3D (fromHex "#56B4E9")@。 ワイヤは従来通り Text なので+--   入口で 'toCss' 変換して格納する (Render / PS / JSON は無改修)。+color3D :: Color -> Layer3D+color3D c = mempty { lyr3Color = Last (Just (toCss c)) }++-- | 便利関数 (2D 'Graphics.Hgg.Spec.colorRGBA' の 3D 双子): 8 桁 RGBA hex+--   (@"#rrggbbaa"@ / 4 桁 @"#rgba"@) を @color3D (fromHex …) <> alpha3D …@ に展開。+--   不正入力は 'error' (total 版は 'colorRGBA3DMaybe')。+colorRGBA3D :: Text -> Layer3D+colorRGBA3D t = let (c, a) = fromHexA t in color3D c <> alpha3D a++-- | 'colorRGBA3D' の total 版。 不正な hex は 'Nothing'。+colorRGBA3DMaybe :: Text -> Maybe Layer3D+colorRGBA3DMaybe t = (\(c, a) -> color3D c <> alpha3D a) <$> fromHexAMaybe t++-- | Phase 25 A2: scatter/line を**カテゴリ列**で群色分けする (+ 離散凡例)。+--   @scatter3D "x" "y" "z" <> colorBy3D "group"@。 色は 2D 同型の ggplot 既定+--   palette ('ggplotHue')。 解決は 'resolveLayer3D' で点ごと色 + 凡例 mapping に+--   落とす (カテゴリは初出順)。+colorBy3D :: ColRef -> Layer3D+colorBy3D c = mempty { lyr3ColorBy = Last (Just c) }++-- | Phase 25 A3: scatter を**数値列**の連続色 (viridis) でマップする (+ colorbar)。+--   @scatter3D "x" "y" "z" <> colorContinuousBy3D "temp"@。 A2 'colorBy3D' (カテゴリ+--   →離散凡例) と対。 解決は 'resolveLayer3D' で点ごと色 + colorbar 情報+--   (stops, min, max) に落とす。 stops は surface と共有の 'viridisStops3D'。+--   (Phase 30 A5: 2D 'colorContinuousBy' と命名対称化。 旧名 @colorByValue3D@。)+colorContinuousBy3D :: ColRef -> Layer3D+colorContinuousBy3D c = mempty { lyr3ColorByV = Last (Just c) }++-- | Phase 25 A3: scatter の点サイズ (= 基本半径 px) を**数値列**でマップする+--   (bubble chart)。 @scatter3D "x" "y" "z" <> sizeBy3D "mass"@。 px 範囲は+--   既定 @(4, 18)@、 変えたい時は 'sizeRange3D'。 値→size は線形 (min→範囲下端、+--   max→範囲上端)。 depth cue は基本 size に乗算される。+sizeBy3D :: ColRef -> Layer3D+sizeBy3D c = mempty { lyr3SizeBy = Last (Just c) }++-- | Phase 25 A3: 'sizeBy3D' の出力 px 範囲を明示指定 (下端, 上端)。 単独では+--   無効 ('sizeBy3D' と併用)。 未指定時の既定は @(4, 18)@。+sizeRange3D :: (Double, Double) -> Layer3D+sizeRange3D r = mempty { lyr3SizeRange = Last (Just r) }++-- | Phase 25 A5: bar スタイル (直方体 'BarCuboid' / 縦線 'BarStick')。 既定は+--   'BarCuboid'。 @bar3D "x" "y" "z" <> barStyle3D BarStick@。+barStyle3D :: BarStyle3D -> Layer3D+barStyle3D s = mempty { lyr3BarStyle = Last (Just s) }++-- | Phase 25 A5: bar footprint の半幅 (= 軸 span に対する比・既定 0.04)。+--   正規化空間 ([-1,1]) では x/y 共通でこの値がそのまま半幅になる。+barWidth3D :: Double -> Layer3D+barWidth3D w = mempty { lyr3BarWidth = Last (Just w) }++-- | Phase 25 A5: 誤差棒 (z 方向 ±err) を**数値列**で付ける。 bar・scatter+--   どちらの layer にも付けられる (頂点に縦線 + 端キャップ)。+--   @bar3D "x" "y" "z" <> errorBar3D "se"@。+errorBar3D :: ColRef -> Layer3D+errorBar3D c = mempty { lyr3ErrBy = Last (Just c) }++-- | Phase 30 A5: surface 等のエッジ線色 (固定色・型安全な 'Color')。+--   ワイヤは Text 維持 ('toCss' 変換して格納)。+edgeColor3D :: Color -> Layer3D+edgeColor3D c = mempty { lyr3EdgeColor = Last (Just (toCss c)) }++size3D :: Double -> Layer3D+size3D s = mempty { lyr3Size = Last (Just s) }++alpha3D :: Double -> Layer3D+alpha3D a = mempty { lyr3Alpha = Last (Just a) }++width3D :: Double -> Layer3D+width3D w = mempty { lyr3Width = Last (Just w) }++shaded3D :: Bool -> Layer3D+shaded3D b = mempty { lyr3Shaded = Last (Just b) }++-- | Phase 24 A2: surface 面色を z 値の連続色 (viridis) にする。+--   @layer3D (surface3DGrid grid <> colormap3D)@。 stops を変えたい時は+--   'colormapWith3D'。+colormap3D :: Layer3D+colormap3D = colormapWith3D viridisStops3D++-- | Phase 24 A2: 任意 gradient stops の colormap (hex 色の線形補間)。+colormapWith3D :: [Text] -> Layer3D+colormapWith3D stops = mempty { lyr3Colormap = Last (Just stops) }++-- | surface を**面なしの格子線メッシュ**で描く (matplotlib @plot_wireframe@ 相当)。+--   @surface3D@ に @\<>@ で合成: @layer3D (surface3DGrid grid \<> surfaceWire \<> color3D (fromHex "#2563eb"))@。+--   面を塗らないので 'colormap3D' / 'shaded3D' は無効 (線色は 'color3D')。 任意エッジの+--   'wireframe3D' とは別 (こちらは grid から行/列の線メッシュを自動生成)。+surfaceWire :: Layer3D+surfaceWire = mempty { lyr3SurfaceWire = Last (Just True) }++-- | 投影 contour の軸 (内部表現)。 'contourX' / 'contourY' / 'contourZ' が設定する。+--   matplotlib @contour(..., zdir=)@ 相当 (= dir に垂直な平面で曲面を切り、 壁へ投影)。+data ContourDir = ContourX | ContourY | ContourZ+  deriving (Show, Eq, Generic)+instance ToJSON   ContourDir+instance FromJSON ContourDir++-- | surface の **x 断面** @n@ 本を左右の壁 (yz 平面) へ投影する (matplotlib+--   @contour(..., zdir='x')@ / plotly @contours.x.project@ 相当)。 x 軸を等分した+--   @n@ 位置で曲面を切り、 各断面プロファイル @z = f(x_k, y)@ を壁に描く。 投影壁は+--   **カメラから遠い面に自動固定**。 同じ surface に 'contourY' / 'contourZ' を @\<>@ で+--   合成可。 線色は colormap (既定 viridis) の x 連続色。+--   @surface3DGrid grid \<> colormap3D \<> contourX 8 \<> contourY 8 \<> contourZ 8@。+contourX :: Int -> Layer3D+contourX n = mempty { lyr3Contours = [(ContourX, n)] }++-- | surface の **y 断面** @n@ 本を前後の壁 (xz 平面) へ投影する (matplotlib+--   @contour(..., zdir='y')@ 相当)。 各断面プロファイル @z = f(x, y_k)@ を壁に描く。+--   投影壁はカメラから遠い面に自動固定。 'contourX' / 'contourZ' と合成可。+contourY :: Int -> Layer3D+contourY n = mempty { lyr3Contours = [(ContourY, n)] }++-- | surface の **等高線 (z 等値面)** @n@ 本を床 (xy 平面) へ投影する (matplotlib+--   @contour(..., zdir='z')@ / plotly @contours_z@ 相当)。 z 軸を等分した @n@ 値で+--   level set @{f = z_k}@ を抽出し床へ落とす (topographic map)。 投影面はカメラから+--   遠い面 (通常は床) に自動固定。 'contourX' / 'contourY' と合成可。+contourZ :: Int -> Layer3D+contourZ n = mempty { lyr3Contours = [(ContourZ, n)] }++-- | 2D 'ColorByContinuous' と同じ viridis 5-stop (palette 共有)。+viridisStops3D :: [Text]+viridisStops3D = ["#440154", "#3B528B", "#21918C", "#5EC962", "#FDE725"]++xRange3D :: (Double, Double) -> Layer3D+xRange3D r = mempty { lyr3XRange = Last (Just r) }++yRange3D :: (Double, Double) -> Layer3D+yRange3D r = mempty { lyr3YRange = Last (Just r) }++-- ===========================================================================+-- VisualSpec3D 属性+-- ===========================================================================++-- | camera 設定 (= 後勝ち、 'Last')。+camera :: Camera3D -> VisualSpec3D+camera c = mempty { vs3Camera = Last (Just c) }++-- | projection 設定。+projection :: Projection3D -> VisualSpec3D+projection p = mempty { vs3Proj = Last (Just p) }++-- | axes 設定。 名前は 'axes3D' で 2D 'theme' / 'facet' と同型語感。+axes3D :: Axes3D -> VisualSpec3D+axes3D a = mempty { vs3Axes = Last (Just a) }++-- | title (= 2D 'title' 同型、 ただし `3D` suffix で衝突回避)。+title3D :: Text -> VisualSpec3D+title3D t = mempty { vs3Title = Last (Just t) }++-- | Phase 24 A8: 軸名 (x, y, z) を任意指定 (既定 "x"/"y"/"z")。+axisTitles3D :: Text -> Text -> Text -> VisualSpec3D+axisTitles3D x y z = mempty { vs3AxisTitles = Last (Just (x, y, z)) }++-- | Phase 24 A8: z 軸の box 縦横比 (正規化後の z スケール係数・既定 1)。+--   @< 1@ で扁平、 @> 1@ で縦長。 軸 box・surface・scatter・床面 contour すべてに+--   一貫適用される。+zAspect3D :: Double -> VisualSpec3D+zAspect3D a = mempty { vs3ZAspect = Last (Just a) }++-- | Phase 25 A6: 壁面 pane + gridline の on/off (mplot3d 標準の薄灰 3 壁・既定 ON)。+--   @pane3D False@ で従来の cube wireframe + tick のみに戻す。+pane3D :: Bool -> VisualSpec3D+pane3D b = mempty { vs3Pane = Last (Just b) }++-- | Phase 25 A8: x 軸の box 縦横比 (正規化後 x スケール係数・既定 1。 'zAspect3D' の x 版)。+xAspect3D :: Double -> VisualSpec3D+xAspect3D a = mempty { vs3XAspect = Last (Just a) }++-- | Phase 25 A8: y 軸の box 縦横比 (正規化後 y スケール係数・既定 1。 'zAspect3D' の y 版)。+yAspect3D :: Double -> VisualSpec3D+yAspect3D a = mempty { vs3YAspect = Last (Just a) }++-- | Phase 25 A8: 軸を log scale に (x, y, z の順で flag 指定・既定 全 False)。+--   log 軸はデータが正の前提 (非正は 1e-12 に clamp)。 tick は 10 の冪 (decade)、+--   ラベルは元の値のまま。 surface の log-z は対応、 surface の log-x/y は現状未対応+--   (point 系 mark = scatter/line/bar は全軸 log 可)。+--+--   @logScale3D False False True@  -- z だけ log (片対数)+logScale3D :: Bool -> Bool -> Bool -> VisualSpec3D+logScale3D x y z = mempty { vs3Log = Last (Just (x, y, z)) }++-- | width (= canvas 幅 px、 2D 'vsWidth' 同型)。+width3DV :: Int -> VisualSpec3D+width3DV w = mempty { vs3Width = Last (Just w) }++-- | height (= canvas 高さ px、 2D 'vsHeight' 同型)。+height3DV :: Int -> VisualSpec3D+height3DV h = mempty { vs3Height = Last (Just h) }++-- ===========================================================================+-- Layer3D → 既存 Scatter3D / Line3D / Wireframe3D / Surface3D 変換+-- (= render 経路で使う。 defaults は existing default*3D 関数を経由)+-- ===========================================================================++-- | Layer3D (= rolled-up) を 'Scatter3D' に変換。 'lyr3Kind' が 'M3Scatter' か+--   未指定の時に意味あり。 必須項目 ('lyr3Points') 未指定なら空 points で。+layerToScatter :: Layer3D -> Scatter3D+layerToScatter l =+  let pts = fromMaybe [] (getLast (lyr3Points l))+      base = defaultScatter3D pts+  in base+       { sc3Color  = fromMaybe (sc3Color base) (getLast (lyr3Color l))+       , sc3Size   = fromMaybe (sc3Size  base) (getLast (lyr3Size  l))+       , sc3Alpha  = fromMaybe (sc3Alpha base) (getLast (lyr3Alpha l))+       , sc3Colors = getLast (lyr3PtColors l)   -- Phase 25 A2/A3: per-point 色+       , sc3Sizes  = getLast (lyr3PtSizes  l)   -- Phase 25 A3: per-point size (bubble)+       }++layerToLine :: Layer3D -> Line3D+layerToLine l =+  let pts = fromMaybe [] (getLast (lyr3Points l))+      base = defaultLine3D pts+  in base+       { Graphics.Hgg.ThreeD.Line.l3Color = fromMaybe (Graphics.Hgg.ThreeD.Line.l3Color base) (getLast (lyr3Color l))+       , Graphics.Hgg.ThreeD.Line.l3Width = fromMaybe (Graphics.Hgg.ThreeD.Line.l3Width base) (getLast (lyr3Width l))+       }++layerToWireframe :: Layer3D -> Wireframe3D+layerToWireframe l =+  let pts = fromMaybe [] (getLast (lyr3Points l))+      es  = fromMaybe [] (getLast (lyr3Edges  l))+      base = defaultWireframe3D pts es+  in base+       { wfColor = fromMaybe (wfColor base) (getLast (lyr3Color l))+       , wfWidth = fromMaybe (wfWidth base) (getLast (lyr3Width l))+       }++layerToSurface :: Layer3D -> Surface3D+layerToSurface l =+  let grid = fromMaybe [] (getLast (lyr3Grid l))+      base = defaultSurface3D grid+  in base+       { sf3Color     = fromMaybe (sf3Color     base) (getLast (lyr3Color     l))+       , sf3EdgeColor = fromMaybe (sf3EdgeColor base) (getLast (lyr3EdgeColor l))+       , sf3Shaded    = fromMaybe (sf3Shaded    base) (getLast (lyr3Shaded    l))+       , sf3XRange    = fromMaybe (sf3XRange    base) (getLast (lyr3XRange    l))+       , sf3YRange    = fromMaybe (sf3YRange    base) (getLast (lyr3YRange    l))+       , sf3Colormap  = getLast (lyr3Colormap l)+       , sf3Alpha     = fromMaybe (sf3Alpha     base) (getLast (lyr3Alpha     l))  -- Phase 25 A4+       , sf3Wire      = fromMaybe (sf3Wire      base) (getLast (lyr3SurfaceWire l))+       }++-- | Phase 25 A5: Layer3D を 'Bar3D' に変換 (render 経路で使う)。 点・base・half-width+--   は正規化済前提 ('normLayer3D' / 'scaleZLayer' が事前に処理)。 base は+--   'lyr3BarBaseZ' (未設定なら 0)、 半幅は 'lyr3BarWidth' (既定 0.04)。+layerToBar :: Layer3D -> Bar3D+layerToBar l =+  let tops = fromMaybe [] (getLast (lyr3Points l))+      base = defaultBar3D tops+  in base+       { br3BaseZ = fromMaybe (br3BaseZ base) (getLast (lyr3BarBaseZ l))+       , br3HalfW = fromMaybe (br3HalfW base) (getLast (lyr3BarWidth l))+       , br3Style = fromMaybe (br3Style base) (getLast (lyr3BarStyle l))+       , br3Color = fromMaybe (br3Color base) (getLast (lyr3Color    l))+       , br3Alpha = fromMaybe (br3Alpha base) (getLast (lyr3Alpha    l))+       , br3Width = fromMaybe (br3Width base) (getLast (lyr3Width    l))+       }++-- | Phase 26 A3: Layer3D を 'Quiver3D' に変換 (render 経路で使う)。 点・ベクトルは+--   正規化済前提 ('normLayer3D' / 'scaleAspectLayer' が事前に処理)。 autoscale =+--   最長矢印が cube の 35% になるよう正規化ベクトル長で割り、 'lyr3VecScale' を掛ける。+--   終点 = 始点 + scale × vec。+layerToQuiver :: Layer3D -> Quiver3D+layerToQuiver l =+  let starts = fromMaybe [] (getLast (lyr3Points  l))+      vecs   = fromMaybe [] (getLast (lyr3Vectors l))+      userS  = fromMaybe 1  (getLast (lyr3VecScale l))+      lens   = [ sqrt (vx*vx + vy*vy + vz*vz) | Vec3 vx vy vz <- vecs ]+      maxLen = if null lens then 0 else maximum lens+      autoS  = if maxLen <= 0 then 0 else 0.35 / maxLen+      s      = autoS * userS+      ends   = [ Point3 (px + s*vx) (py + s*vy) (pz + s*vz)+               | (Point3 px py pz, Vec3 vx vy vz) <- zip starts vecs ]+      base = defaultQuiver3D starts ends+  in base+       { q3Color = fromMaybe (q3Color base) (getLast (lyr3Color l))+       , q3Width = fromMaybe (q3Width base) (getLast (lyr3Width l))+       }++-- ===========================================================================+-- autoAxes3D: layers から bounding box を自動算出 (= Axes 未指定時の default)+-- ===========================================================================++-- | 全 layer の Point3 集合から min/max を取って 'Axes3D' を生成。+--   surface3D は xRange/yRange + grid 高さで Point3 集合を構成。+--   layer が無い / 全 Point3 が空なら 'defaultAxes3D' (= 単位 cube)。+-- Phase 24 A6: 列参照 (Enc) の解決 — Resolver で [Point3] に落とす++-- | 層の列参照 (x,y,z) を 'Resolver' で解決して 'lyr3Points' に格納する。+--   3 列が全部解決できた時だけ上書き ('inline' 生値は resolver 不要で解決)。+--   Enc が無い層 (旧 [Point3] 直入れ・surface 等) は素通し。+resolveLayer3D :: Resolver -> Layer3D -> Layer3D+resolveLayer3D r l0 =+  let -- まず x/y/z 列参照 → 点列 (Phase 24 A6)+      l1 = case (getLast (lyr3EncX l0), getLast (lyr3EncY l0), getLast (lyr3EncZ l0)) of+        (Just cx, Just cy, Just cz)+          | Just xs <- resolveNum r cx+          , Just ys <- resolveNum r cy+          , Just zs <- resolveNum r cz ->+              let n   = minimum [V.length xs, V.length ys, V.length zs]+              -- Phase 30 A6: surface (案C) は long 形 (x,y,z) を pivot して grid へ。+              --   それ以外 (scatter/line/bar/stem/text…) は従来通り点列に落とす。+              in case getFirst (lyr3Kind l0) of+                   Just M3Surface ->+                     let (grid, xr, yr) = pivotSurface3D+                           (take n (V.toList xs)) (take n (V.toList ys)) (take n (V.toList zs))+                     in l0 { lyr3Grid   = Last (Just grid)+                           , lyr3XRange = Last (Just xr)+                           , lyr3YRange = Last (Just yr) }+                   _ ->+                     let pts = [ Point3 (xs V.! i) (ys V.! i) (zs V.! i) | i <- [0 .. n - 1] ]+                     in l0 { lyr3Points = Last (Just pts) }+        _ -> l0+      -- Phase 27 A2: trisurf の face index を前計算 (= (x,y) Delaunay)。+      --   座標非依存ゆえ WebGL/CPU 両経路で同一。 WebGL は z-buffer なので depth ソート不要。+      l2 = case getFirst (lyr3Kind l1) of+        Just M3Trisurf ->+          let pts = fromMaybe [] (getLast (lyr3Points l1))+          in if null pts then l1+             else l1 { lyr3Faces = Last (Just (delaunay2D [ (x, y) | Point3 x y _ <- pts ])) }+        _ -> l1+  in resolveTextBy3D r (resolveErrBy3D r (resolveSizeBy3D r (resolveColorByValue3D r (resolveColorBy3D r l2))))++-- | Phase 30 A6: 列駆動 'text3D' の label 列 ('lyr3TextBy') を解決して 'lyr3Labels' に+--   落とす (x/y/z は 'resolveLayer3D' 先頭で既に 'lyr3Points' へ解決済)。 数値列は+--   'numLabel' で文字化。 未指定なら素通し。+resolveTextBy3D :: Resolver -> Layer3D -> Layer3D+resolveTextBy3D r l = case getLast (lyr3TextBy l) of+  Nothing -> l+  Just c  ->+    let labels = case resolveCol r c of+          Just (TxtData v) -> V.toList v+          Just (NumData v) -> map numLabel (V.toList v)+          Nothing          -> []+    in if null labels then l else l { lyr3Labels = Last (Just labels) }++-- | Phase 30 A6: surface 案C の pivot。 long 形の (xs, ys, zs) を規則格子の z 行列に+--   畳む。 列 = x の昇順 unique・行 = y の昇順 unique。 grid[i][j] = (uxs[j], uys[i]) の z。+--   欠損 (組が無い) セルは NaN (= 描画穴)。 重複 (同一 (x,y)) は後勝ち。 xRange/yRange は+--   実データの min/max (空なら (-1,1))。+pivotSurface3D :: [Double] -> [Double] -> [Double]+               -> ([[Double]], (Double, Double), (Double, Double))+pivotSurface3D xs ys zs =+  let m    = M.fromList (zip (zip xs ys) zs)       -- (x,y) → z (後勝ち)+      uxs  = M.keys (M.fromList [ (x, ()) | x <- xs ])  -- 昇順 unique x (列)+      uys  = M.keys (M.fromList [ (y, ()) | y <- ys ])  -- 昇順 unique y (行)+      nan  = 0 / 0 :: Double+      grid = [ [ M.findWithDefault nan (x, y) m | x <- uxs ] | y <- uys ]+      rng vs = if null vs then (-1, 1) else (minimum vs, maximum vs)+  in (grid, rng xs, rng ys)++-- | Phase 25 A2: カテゴリ列 ('lyr3ColorBy') を解決して点ごと色 + 凡例 mapping に+--   落とす。 カテゴリは初出順、 色は 'ggplotHue'。 未指定なら素通し。+resolveColorBy3D :: Resolver -> Layer3D -> Layer3D+resolveColorBy3D r l = case getLast (lyr3ColorBy l) of+  Nothing -> l+  Just c  ->+    let labels = case resolveCol r c of+          Just (TxtData v) -> V.toList v+          Just (NumData v) -> map numLabel (V.toList v)+          Nothing          -> []+        cats     = nub labels                       -- 初出順+        palette  = ggplotHue (length cats)+        catColor = zip cats (palette ++ repeat "#333333")+        colOf lbl = maybe "#333333" id (lookup lbl catColor)+    in if null labels then l+       else l { lyr3PtColors = Last (Just (map colOf labels))+              , lyr3Legend   = Last (Just catColor) }++-- | Phase 25 A3: 数値列 ('lyr3ColorByV') を解決して点ごと連続色 (viridis) ++--   colorbar 情報 (stops, min, max) に落とす。 値→色は線形正規化 + 'continuousColor'+--   (= 2D gradient 凡例 / surface colormap と同じ補間)。 未指定 / 非数値なら素通し。+--   全値同一なら中央色 (t=0.5)。+resolveColorByValue3D :: Resolver -> Layer3D -> Layer3D+resolveColorByValue3D r l = case getLast (lyr3ColorByV l) of+  Nothing -> l+  Just c  -> case resolveNum r c of+    Nothing -> l+    Just v  ->+      let vals = V.toList v+      in if null vals then l else+        let vMin = minimum vals+            vMax = maximum vals+            stops = viridisStops3D+            tOf x = if vMax <= vMin then 0.5 else (x - vMin) / (vMax - vMin)+            cols  = map (continuousColor stops . tOf) vals+        in l { lyr3PtColors = Last (Just cols)+             , lyr3Colorbar = Last (Just (stops, vMin, vMax)) }++-- | Phase 25 A3: 数値列 ('lyr3SizeBy') を解決して点ごと基本 size (px) に落とす+--   (bubble)。 値→size は線形 (min→範囲下端、 max→範囲上端)。 範囲は+--   'lyr3SizeRange' (既定 (4, 18))。 未指定 / 非数値なら素通し。 全値同一なら+--   範囲の中点。+resolveSizeBy3D :: Resolver -> Layer3D -> Layer3D+resolveSizeBy3D r l = case getLast (lyr3SizeBy l) of+  Nothing -> l+  Just c  -> case resolveNum r c of+    Nothing -> l+    Just v  ->+      let vals = V.toList v+      in if null vals then l else+        let (sLo, sHi) = fromMaybe (4, 18) (getLast (lyr3SizeRange l))+            vMin = minimum vals+            vMax = maximum vals+            sizeOf x = if vMax <= vMin then (sLo + sHi) / 2+                       else sLo + (sHi - sLo) * (x - vMin) / (vMax - vMin)+        in l { lyr3PtSizes = Last (Just (map sizeOf vals)) }++-- | Phase 25 A5: 誤差棒の err 列 ('lyr3ErrBy') を解決して点ごと err (data 単位)+--   に落とす。 正規化 (data z → [-1,1]) は 'Easy.normLayer3D' が行う。 未指定 /+--   非数値なら素通し。+resolveErrBy3D :: Resolver -> Layer3D -> Layer3D+resolveErrBy3D r l = case getLast (lyr3ErrBy l) of+  Nothing -> l+  Just c  -> case resolveNum r c of+    Nothing -> l+    Just v  -> let vals = V.toList v+               in if null vals then l+                  else l { lyr3PtErrs = Last (Just vals) }++-- | 数値カテゴリのラベル整形 (整数なら末尾 .0 を落とす)。+numLabel :: Double -> Text+numLabel x =+  let r = fromIntegral (round x :: Int) :: Double+  in if r == x then T.pack (show (round x :: Int)) else T.pack (show x)++-- | spec 内の全層の列参照を解決する ('saveSVG3DBound' / bind 経路の正本)。+resolveSpec3D :: Resolver -> VisualSpec3D -> VisualSpec3D+resolveSpec3D r spec = spec { vs3Layers = map (resolveLayer3D r) (vs3Layers spec) }++autoAxes3D :: [Layer3D] -> Axes3D+autoAxes3D ls =+  let pts = concatMap layerPoints ls+  in if null pts+       then defaultAxes3D+       else+         let xs = [x | Point3 x _ _ <- pts]+             ys = [y | Point3 _ y _ <- pts]+             zs = [z | Point3 _ _ z <- pts]+         in Axes3D+              { axesXMin  = minimum xs+              , axesXMax  = maximum xs+              , axesYMin  = minimum ys+              , axesYMax  = maximum ys+              , axesZMin  = minimum zs+              , axesZMax  = maximum zs+              , axesNTicks = 5+              , axesXLog = False, axesYLog = False, axesZLog = False+              }++-- | Layer から Point3 集合を抽出 (= Surface は grid を Point3 に展開)。+-- Phase 25 A5: bar は底面 (z=0) を、 誤差棒のある層は z±err 端を含めて axis box が+-- 棒の根本 / whisker を覆うようにする。+layerPoints :: Layer3D -> [Point3]+layerPoints l = case getFirst (lyr3Kind l) of+  Just M3Surface ->+    let grid = fromMaybe [] (getLast (lyr3Grid l))+        (xMin, xMax) = fromMaybe (-1, 1) (getLast (lyr3XRange l))+        (yMin, yMax) = fromMaybe (-1, 1) (getLast (lyr3YRange l))+        ny = length grid+        nx = case grid of+          row:_ -> length row+          []    -> 0+        xAt j = if nx <= 1 then xMin+                else xMin + (xMax - xMin) * fromIntegral j / fromIntegral (nx - 1)+        yAt i = if ny <= 1 then yMin+                else yMin + (yMax - yMin) * fromIntegral i / fromIntegral (ny - 1)+    in [ Point3 (xAt j) (yAt i) z+       | (i, row) <- zip [0 :: Int ..] grid+       , (j, z)   <- zip [0 :: Int ..] row+       ]+  Just M3Bar ->+    let tops  = fromMaybe [] (getLast (lyr3Points l))+        bases = [ Point3 x y 0 | Point3 x y _ <- tops ]+    in tops ++ bases ++ errPoints l tops+  -- Phase 26 A4: stem は底面 (stemBaseZ・data 空間) を axis box に含める+  Just M3Stem ->+    let tops  = fromMaybe [] (getLast (lyr3Points l))+        bz    = fromMaybe 0 (getLast (lyr3StemBaseZ l))+        bases = [ Point3 x y bz | Point3 x y _ <- tops ]+    in tops ++ bases ++ errPoints l tops+  -- Phase 26 A3: quiver は始点 (位置) のみで axis box (矢印は autoscale で cube 内)+  Just M3Quiver -> fromMaybe [] (getLast (lyr3Points l))+  -- Phase 30 A6: text は inline 注釈 ('lyr3Annots') の点も axis box に含める+  --   (= 列駆動は lyr3Points・inline は lyr3Annots。 normLayer3D 前なので両方見る)。+  Just M3Text ->+    fromMaybe [] (getLast (lyr3Points l)) ++ map fst (lyr3Annots l)+  _ -> let pts = fromMaybe [] (getLast (lyr3Points l))+       in pts ++ errPoints l pts++-- | Phase 25 A5: 誤差棒のある層で、 各点の z±err 端点を返す (axis box 拡張用)。+errPoints :: Layer3D -> [Point3] -> [Point3]+errPoints l pts = case getLast (lyr3PtErrs l) of+  Nothing -> []+  Just es -> concat [ [Point3 x y (z + e), Point3 x y (z - e)]+                    | (Point3 x y z, e) <- zip pts es ]
+ src/Graphics/Hgg/ThreeD/Surface.hs view
@@ -0,0 +1,270 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Surface+-- Description : 3D surface plot (grid mesh + painter's algorithm) (Phase 3 A7)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- 規則 grid (= 2D 配列の z 値) から triangle mesh を作り、 painter's algorithm+-- で奥から塗り潰す。 optional Lambert shading (= flat、 1 directional light)。+{-# LANGUAGE DeriveGeneric     #-}+{-# LANGUAGE OverloadedStrings #-}+module Graphics.Hgg.ThreeD.Surface+  ( Surface3D (..)+  , defaultSurface3D+  , surfaceFromFunction+  , renderSurface3D+  , surfaceFacesDepth+  , trianglesFacesDepth+  ) where++import           Data.Aeson                      (FromJSON, ToJSON)+import           Data.List                       (sortOn)+import           Data.Text                       (Text)+import qualified Data.Text                       as T+import           GHC.Generics                    (Generic)++import           Graphics.Hgg.Render             (FillStyle (..),+                                                  LineStyle (..),+                                                  PathSegment (..),+                                                  Point (..), Primitive (..),+                                                  StrokeStyle (..))+import           Graphics.Hgg.Render.Common      (continuousColor)+import           Graphics.Hgg.ThreeD.Projection  (Projected (..), Viewport,+                                                  project3D)+import           Graphics.Hgg.ThreeD.Types++data Surface3D = Surface3D+  { sf3Grid       :: ![[Double]]+  , sf3XRange     :: !(Double, Double)+  , sf3YRange     :: !(Double, Double)+  , sf3Color      :: !Text+  , sf3EdgeColor  :: !Text+  , sf3Shaded     :: !Bool+  , sf3Colormap   :: !(Maybe [Text])+    -- ^ Phase 24 A2: @Just stops@ で面色を z 値の連続色 (gradient stops の+    --   線形補間・2D 'ColorByContinuous' と同じ 'continuousColor') にする。+    --   @Nothing@ = 従来の単色 'sf3Color' (後方互換)。+  , sf3Alpha      :: !Double+    -- ^ Phase 25 A4: 面の不透明度 (0..1・既定 1)。 @< 1@ で半透明 surface。+    --   face は depth ソート済 (painter's・A8 で scatter と大域マージ) なので+    --   back-to-front 合成が正しく重なる。+  , sf3Wire       :: !Bool+    -- ^ surfaceWire: 面を塗らず grid の行/列を**格子線メッシュ**で描く+    --   (matplotlib @plot_wireframe@ 相当)。 True で fill 三角形の代わりに+    --   隣接 grid 点を結ぶ line segment を depth 統合付きで返す。 colormap/shaded は無効。+  } deriving (Show, Eq, Generic)+instance ToJSON   Surface3D+instance FromJSON Surface3D++defaultSurface3D :: [[Double]] -> Surface3D+defaultSurface3D grid = Surface3D+  { sf3Grid      = grid+  , sf3XRange    = (-1, 1)+  , sf3YRange    = (-1, 1)+  , sf3Color     = "#5b9bd5"+  , sf3EdgeColor = "#3a73a6"+  , sf3Shaded    = True+  , sf3Colormap  = Nothing+  , sf3Alpha     = 1.0+  , sf3Wire      = False+  }++surfaceFromFunction+  :: Int -> Int+  -> Double -> Double+  -> Double -> Double+  -> (Double -> Double -> Double)+  -> Surface3D+surfaceFromFunction nx ny xMin xMax yMin yMax f =+  let xs = [ xMin + (xMax - xMin) * fromIntegral i / fromIntegral (nx - 1)+           | i <- [0 .. nx - 1] ]+      ys = [ yMin + (yMax - yMin) * fromIntegral j / fromIntegral (ny - 1)+           | j <- [0 .. ny - 1] ]+      grid = [ [ f x y | x <- xs ] | y <- ys ]+  in (defaultSurface3D grid)+       { sf3XRange = (xMin, xMax)+       , sf3YRange = (yMin, yMax)+       }++renderSurface3D+  :: Camera3D -> Projection3D -> Viewport+  -> Surface3D -> [Primitive]+renderSurface3D cam proj vp sf =+  map snd (sortOn (negate . fst) (surfaceFacesDepth cam proj vp sf))++-- | Phase 24 A8 (depth 統合): 各 face を @(投影 depth, poly Primitive)@ として+-- **未ソート**で返す (= 'renderSurface3D' は @sortOn (negate.fst)@ してから+-- @map snd@・ビット同一)。 scatter 点との層横断 depth 統合に使う。+surfaceFacesDepth+  :: Camera3D -> Projection3D -> Viewport+  -> Surface3D -> [(Double, Primitive)]+surfaceFacesDepth cam proj vp sf =+  let project = project3D cam proj vp+      grid = sf3Grid sf+      nRows = length grid+      nCols = if null grid then 0 else length (head grid)+      (xMin, xMax) = sf3XRange sf+      (yMin, yMax) = sf3YRange sf+      xAt c | nCols <= 1 = xMin+            | otherwise  = xMin + (xMax - xMin) * fromIntegral c+                                                    / fromIntegral (nCols - 1)+      yAt r | nRows <= 1 = yMin+            | otherwise  = yMin + (yMax - yMin) * fromIntegral r+                                                    / fromIntegral (nRows - 1)+      zAt r c = (grid !! r) !! c+      pt r c = Point3 (xAt c) (yAt r) (zAt r c)++      cells = [ (r, c) | r <- [0 .. nRows - 2], c <- [0 .. nCols - 2] ]+      mkTriangles (r, c) =+        let p00 = pt r       c+            p01 = pt r       (c + 1)+            p10 = pt (r + 1) c+            p11 = pt (r + 1) (c + 1)+        in [ (p00, p01, p11), (p00, p11, p10) ]+      triangles = concatMap mkTriangles cells++      lightDir = normalizeV3 (Vec3 0.5 0.5 (-1.0))+      -- Phase 24 A2: colormap 用の z 正規化 (grid 全体の z range)+      allZ = concat grid+      zMin = if null allZ then 0 else minimum allZ+      zMax = if null allZ then 1 else maximum allZ+      zT z | zMax <= zMin = 0.5+           | otherwise    = (z - zMin) / (zMax - zMin)+      faceData (a, b, c) =+        let pa = project a+            pb = project b+            pc = project c+            d  = (projDepth pa + projDepth pb + projDepth pc) / 3+            n  = normalizeV3 (crossV3 (subAsVec b a) (subAsVec c a))+            ndotl = max 0 (dotV3 n lightDir)+            -- colormap 時は shading を弱める (0.75-1.0): 低 z 側の dark stop が+            -- 黒潰れせず、 色 = z の読みが主・陰影 = 立体感の補助に留める+            intensity+              | not (sf3Shaded sf)           = 1.0+              | Just _ <- sf3Colormap sf     = 0.75 + 0.25 * ndotl+              | otherwise                    = 0.4 + 0.6 * ndotl+            -- 面の代表 z (3 頂点平均)。 colormap の色決め用+            zAvg = let Point3 _ _ za = a; Point3 _ _ zb = b; Point3 _ _ zc = c+                   in (za + zb + zc) / 3+        in (d, intensity, zAvg, pa, pb, pc)+      facesData = map faceData triangles++      baseColorAt zAvg = case sf3Colormap sf of+        Just stops -> continuousColor stops (zT zAvg)+        Nothing    -> sf3Color sf+      edgeStroke = Just (StrokeStyle (sf3EdgeColor sf) 0.3)+      mkPoly (_, intensity, zAvg, Projected x1 y1 _, Projected x2 y2 _, Projected x3 y3 _) =+        let base = baseColorAt zAvg+            fill = if sf3Shaded sf+                     then mulColor base intensity+                     else base+        in PPath+             [ MoveTo (Point x1 y1)+             , LineTo (Point x2 y2)+             , LineTo (Point x3 y3)+             , ClosePath+             ]+             (FillStyle fill (sf3Alpha sf))+             edgeStroke+      -- surfaceWire: 面を塗らず grid の行/列を線メッシュで描く (plot_wireframe 相当)+      wireStyle = LineStyle (sf3Color sf) 1.0 []+      gridEdges = [ (pt r c, pt r (c + 1)) | r <- [0 .. nRows - 1], c <- [0 .. nCols - 2] ]+               ++ [ (pt r c, pt (r + 1) c) | r <- [0 .. nRows - 2], c <- [0 .. nCols - 1] ]+      mkSeg (a, b) = case (project a, project b) of+        (Projected x1 y1 d1, Projected x2 y2 d2) ->+          ((d1 + d2) / 2, PLine (Point x1 y1) (Point x2 y2) wireStyle)+  in if sf3Wire sf+       then map mkSeg gridEdges+       else [ (d, mkPoly fd) | fd@(d, _, _, _, _, _) <- facesData ]+  where+    subAsVec (Point3 bx by bz) (Point3 ax ay az) =+      Vec3 (bx - ax) (by - ay) (bz - az)++-- | Phase 26 A5 (trisurf): grid 非依存に **任意の三角形 face 列**を depth ++-- Lambert shading + (任意) colormap で @(投影 depth, PPath)@ 化する+-- ('surfaceFacesDepth' の grid 専用ロジックを triangle 列入力へ一般化した版)。+-- 頂点は正規化済前提。 colormap の z range は全 face 頂点 z から算出。+trianglesFacesDepth+  :: Camera3D -> Projection3D -> Viewport+  -> Text          -- ^ 基本色 (colormap 無しの単色)+  -> Text          -- ^ edge 色+  -> Bool          -- ^ shaded (Lambert)+  -> Maybe [Text]  -- ^ colormap stops (Just で z 連続色)+  -> Double        -- ^ alpha+  -> [(Point3, Point3, Point3)]  -- ^ 三角形 (正規化済)+  -> [(Double, Primitive)]+trianglesFacesDepth cam proj vp baseCol edgeCol shaded cmap alpha triangles =+  let project = project3D cam proj vp+      lightDir = normalizeV3 (Vec3 0.5 0.5 (-1.0))+      allZ = concat [ [za, zb, zc]+                    | (Point3 _ _ za, Point3 _ _ zb, Point3 _ _ zc) <- triangles ]+      zMin = if null allZ then 0 else minimum allZ+      zMax = if null allZ then 1 else maximum allZ+      zT z | zMax <= zMin = 0.5+           | otherwise    = (z - zMin) / (zMax - zMin)+      subAsVec (Point3 bx by bz) (Point3 ax ay az) =+        Vec3 (bx - ax) (by - ay) (bz - az)+      edgeStroke = Just (StrokeStyle edgeCol 0.3)+      baseColorAt zAvg = case cmap of+        Just stops -> continuousColor stops (zT zAvg)+        Nothing    -> baseCol+      faceData (a, b, c) =+        let pa = project a; pb = project b; pc = project c+            dd = (projDepth pa + projDepth pb + projDepth pc) / 3+            nrm = normalizeV3 (crossV3 (subAsVec b a) (subAsVec c a))+            ndotl = max 0 (dotV3 nrm lightDir)+            intensity+              | not shaded         = 1.0+              | Just _ <- cmap     = 0.75 + 0.25 * ndotl+              | otherwise          = 0.4 + 0.6 * ndotl+            zAvg = let Point3 _ _ za = a; Point3 _ _ zb = b; Point3 _ _ zc = c+                   in (za + zb + zc) / 3+        in (dd, intensity, zAvg, pa, pb, pc)+      mkPoly (_, intensity, zAvg, Projected x1 y1 _, Projected x2 y2 _, Projected x3 y3 _) =+        let base = baseColorAt zAvg+            fill = if shaded then mulColor base intensity else base+        in PPath [ MoveTo (Point x1 y1), LineTo (Point x2 y2)+                 , LineTo (Point x3 y3), ClosePath ]+                 (FillStyle fill alpha) edgeStroke+  in [ (d, mkPoly fd) | fd@(d, _, _, _, _, _) <- map faceData triangles ]++-- ===========================================================================+-- 簡易色操作 (= hex #RRGGBB を intensity 倍)+-- ===========================================================================++mulColor :: Text -> Double -> Text+mulColor hex intensity =+  let s = T.unpack hex+      body = case s of ('#':rest) -> rest; other -> other+      r = parseHex2 (take 2 body)+      g = parseHex2 (take 2 (drop 2 body))+      b = parseHex2 (take 2 (drop 4 body))+      clampMul i = max 0 (min 255 (round (fromIntegral i * intensity :: Double) :: Int))+      toHex2 i = pad2 (decToHex (clampMul i))+  in T.pack ("#" <> toHex2 r <> toHex2 g <> toHex2 b)++parseHex2 :: String -> Int+parseHex2 [c1, c2] = hexVal c1 * 16 + hexVal c2+parseHex2 _        = 0++hexVal :: Char -> Int+hexVal c+  | c >= '0' && c <= '9' = fromEnum c - fromEnum '0'+  | c >= 'a' && c <= 'f' = fromEnum c - fromEnum 'a' + 10+  | c >= 'A' && c <= 'F' = fromEnum c - fromEnum 'A' + 10+  | otherwise            = 0++decToHex :: Int -> String+decToHex n+  | n < 16    = [hexChar n]+  | otherwise = decToHex (n `div` 16) <> [hexChar (n `mod` 16)]++hexChar :: Int -> Char+hexChar v+  | v < 10    = toEnum (fromEnum '0' + v)+  | otherwise = toEnum (fromEnum 'a' + v - 10)++pad2 :: String -> String+pad2 s+  | length s < 2 = '0' : s+  | otherwise    = s
+ src/Graphics/Hgg/ThreeD/Types.hs view
@@ -0,0 +1,245 @@+-- |+-- Module      : Graphics.Hgg.ThreeD.Types+-- Description : 3D の中核型 (Point3 / Vec3 / Mat4 / Camera3D / Projection3D)+-- Copyright   : (c) 2026 Aelysce Project (Toshiaki Honda)+-- License     : BSD-3-Clause+--+-- Phase 3 A2 段階: 中核型のみ定義。 関数 (= project3D / lookAt 等) は A3+-- (Graphics.Hgg.ThreeD.Projection) で実装。+--+-- 設計判断: linear package 等の外部依存を入れず、 自前 Vec3 / Mat4 を持つ。+-- これで core (= base/vector/text/containers のみ) の依存戦略と整合する。+-- 行列演算は 4x4 で十分 (= camera + projection)、 性能不問の前提で simple list 実装。+{-# LANGUAGE DeriveGeneric #-}+module Graphics.Hgg.ThreeD.Types+  ( -- * 幾何プリミティブ+    Point3 (..)+  , Vec3 (..)+    -- * 行列 (= 4x4 同次座標)+  , Mat4 (..)+    -- * Camera + Projection+  , Camera3D (..)+  , Projection3D (..)+    -- * Convention (= z-up / y-up を両対応、 default は z-up)+    -- $convention+  , zUp+  , yUp+  , defaultCameraZUp+  , defaultCameraYUp+    -- * 視点 preset (Phase 24 A8・z-up)+  , cameraIso+  , cameraTop+  , cameraFront+  , cameraSide+  , defaultPerspective+    -- * helper: 基本ベクトル演算+  , addV3+  , subV3+  , scaleV3+  , dotV3+  , crossV3+  , lengthV3+  , normalizeV3+  ) where++import           Data.Aeson      (FromJSON, ToJSON)+import           GHC.Generics    (Generic)++-- | 3D 点 (= world / camera space どちらでも使う)。+--+-- JSON: positional fields → array `[x, y, z]` (= aeson Generic デフォルト挙動)。+-- PS Argonaut 側も同形式で decode。+data Point3 = Point3 !Double !Double !Double+  deriving (Show, Eq, Generic)+instance ToJSON   Point3+instance FromJSON Point3++-- | 3D ベクトル (= 方向 / 法線)。 JSON は Point3 と同じく array `[x, y, z]`。+data Vec3 = Vec3 !Double !Double !Double+  deriving (Show, Eq, Generic)+instance ToJSON   Vec3+instance FromJSON Vec3++-- $convention+-- hgg-3d は **z-up と y-up の両方をサポート** する。 default は **z-up**+-- (= mplot3d / 工学慣例)。 user は用途に応じて切替可能。+--+-- == 業界の使い分け (= 2026 時点の調査)+--+-- +-------------------+-------------------------------------------------+------------++-- | 分野              | 主要ライブラリ                                  | up         |+-- +===================+=================================================+============++-- | __data viz__      | matplotlib mplot3d / plotly / Mathematica /     | __z-up__   |+-- |                   | gnuplot splot / mayavi / R rgl                  |            |+-- +-------------------+-------------------------------------------------+------------++-- | __工学 / CAD__    | AutoCAD / SolidWorks / Inventor / Unreal Engine | __z-up__   |+-- +-------------------+-------------------------------------------------+------------++-- | __game / CG__     | Unity / Godot 3D / OpenGL 慣例 / DirectX 慣例   | __y-up__   |+-- +-------------------+-------------------------------------------------+------------++-- | __3D modeling__   | Blender (= z-up に切替済) / Maya (= y-up)       | 分裂       |+-- +-------------------+-------------------------------------------------+------------++--+-- == hgg-3d の default = z-up を選んだ理由+--+-- * メインターゲットが data viz (= hanalyze の HBM 等)+-- * mplot3d / plotly の慣例と整合 → 既存 user 知識が活きる+-- * @z = f(x, y)@ の数学慣例と一致 → 'Graphics.Hgg.ThreeD.Surface.Surface3D' が直感的+-- * z = 物理量の高さ (= 確率密度 / 計測値 / 標高) の伝統+--+-- == どちらを使うべきか+--+-- * __z-up__ ('zUp' / 'defaultCameraZUp'): data viz、 工学解析、 統計分布、 surface plot。 default 推奨+-- * __y-up__ ('yUp' / 'defaultCameraYUp'): game-like view、 OpenGL 系 sample との整合、 既存 CG 知識の流用+--+-- 両 helper とも up vector を切替えるだけで他は同じ。 任意の up vector が必要なら+-- 'Camera3D' を直接構築する。++-- | z-up convention の up vector (= 'Vec3' 0 0 1)。 data viz / mplot3d 慣例。+zUp :: Vec3+zUp = Vec3 0 0 1++-- | y-up convention の up vector (= 'Vec3' 0 1 0)。 OpenGL / game engine 慣例。+yUp :: Vec3+yUp = Vec3 0 1 0++-- | z-up convention の default camera。 'eye' を data の bounding box 寄りに配置し、+-- 'target' を原点、 up を 'zUp' に。 typical 3/4 view (= 上前方斜め見下ろし)。+--+-- @+-- defaultCameraZUp 5  -- camera at (5, -5, 3) → origin (= 上前方斜め)+-- @+defaultCameraZUp :: Double -> Camera3D+defaultCameraZUp dist = Camera3D+  { cameraEye    = Point3 dist (negate dist) (dist * 0.6)+  , cameraTarget = Point3 0 0 0+  , cameraUp     = zUp+  }++-- | y-up convention の default camera。 OpenGL / Unity 慣例に整合する 3/4 view+-- (= y = 縦軸、 x/z = 床平面、 camera は y > 0 から見下ろし)。 game-like 表現や+-- OpenGL 系 sample との整合に。+--+-- @+-- defaultCameraYUp 5  -- camera at (5, 3, 5) → origin (= 上前方斜め、 y = up)+-- @+defaultCameraYUp :: Double -> Camera3D+defaultCameraYUp dist = Camera3D+  { cameraEye    = Point3 dist (dist * 0.6) dist+  , cameraTarget = Point3 0 0 0+  , cameraUp     = yUp+  }++-- | Phase 24 A8: 視点 preset (z-up・target = 原点)。 'cameraIso' = 'defaultCameraZUp'+-- (上前方斜め見下ろし)。 @dist@ は eye と原点の距離スケール。+cameraIso :: Double -> Camera3D+cameraIso = defaultCameraZUp++-- | Phase 24 A8: 真上から見下ろす視点 (xy 平面を正対・z は奥行き)。 up は y 軸+-- (見下ろし時に zUp は eye 方向と平行になり退化するため)。+cameraTop :: Double -> Camera3D+cameraTop dist = Camera3D+  { cameraEye    = Point3 0 0 dist+  , cameraTarget = Point3 0 0 0+  , cameraUp     = yUp+  }++-- | Phase 24 A8: 正面 (−y 方向から xz 面を正対・z = 縦)。+cameraFront :: Double -> Camera3D+cameraFront dist = Camera3D+  { cameraEye    = Point3 0 (negate dist) 0+  , cameraTarget = Point3 0 0 0+  , cameraUp     = zUp+  }++-- | Phase 24 A8: 真横 (+x 方向から yz 面を正対・z = 縦)。+cameraSide :: Double -> Camera3D+cameraSide dist = Camera3D+  { cameraEye    = Point3 dist 0 0+  , cameraTarget = Point3 0 0 0+  , cameraUp     = zUp+  }++-- | aspect 1:1 / fov 45° / near 0.1 / far 100 の sane default。+defaultPerspective :: Projection3D+defaultPerspective = Perspective (pi / 4) 1.0 0.1 100++-- | 4x4 同次座標行列 (= row-major、 16 Double を直接持つ)。+-- camera transform / perspective projection / model matrix で使う。+data Mat4 = Mat4+  !Double !Double !Double !Double+  !Double !Double !Double !Double+  !Double !Double !Double !Double+  !Double !Double !Double !Double+  deriving (Show, Eq, Generic)+-- JSON: 16 要素 array (= aeson Generic で positional fields は array 化)。+instance ToJSON   Mat4+instance FromJSON Mat4++-- | Camera 設定。 eye (= 視点)、 target (= 注視点)、 up (= 上方向、 通常 (0,1,0))。+-- 'Graphics.Hgg.ThreeD.Projection.lookAt' で view 行列を生成する。+data Camera3D = Camera3D+  { cameraEye    :: !Point3+  , cameraTarget :: !Point3+  , cameraUp     :: !Vec3+  } deriving (Show, Eq, Generic)+instance ToJSON   Camera3D+instance FromJSON Camera3D++-- | Projection 設定。 orthographic / perspective の 2 種。+--+--   * 'Orthographic': 平行投影。 box の半幅 (xHalf, yHalf, near, far) で領域指定+--   * 'Perspective': 透視投影。 fov (= 縦方向視野角、 radians) + aspect ratio ++--     near + far クリップ面+data Projection3D+  = Orthographic+      { orthoXHalf :: !Double  -- ^ x 方向半幅+      , orthoYHalf :: !Double  -- ^ y 方向半幅+      , orthoNear  :: !Double+      , orthoFar   :: !Double+      }+  | Perspective+      { perspFov    :: !Double  -- ^ 縦方向 FOV、 radians+      , perspAspect :: !Double  -- ^ aspect ratio (= width / height)+      , perspNear   :: !Double+      , perspFar    :: !Double+      }+  deriving (Show, Eq, Generic)+-- JSON: aeson Generic デフォルト (= `{"tag": "Orthographic", "orthoXHalf": ..., ...}` or+--       `{"tag": "Perspective", "perspFov": ..., ...}`)。 PS Argonaut 側で同形式 decode。+instance ToJSON   Projection3D+instance FromJSON Projection3D++-- ===========================================================================+-- 基本ベクトル演算+-- ===========================================================================++-- | 加算。+addV3 :: Vec3 -> Vec3 -> Vec3+addV3 (Vec3 ax ay az) (Vec3 bx by bz) = Vec3 (ax + bx) (ay + by) (az + bz)++-- | 減算。+subV3 :: Vec3 -> Vec3 -> Vec3+subV3 (Vec3 ax ay az) (Vec3 bx by bz) = Vec3 (ax - bx) (ay - by) (az - bz)++-- | スカラー倍。+scaleV3 :: Double -> Vec3 -> Vec3+scaleV3 s (Vec3 x y z) = Vec3 (s * x) (s * y) (s * z)++-- | 内積。+dotV3 :: Vec3 -> Vec3 -> Double+dotV3 (Vec3 ax ay az) (Vec3 bx by bz) = ax * bx + ay * by + az * bz++-- | 外積 (= 右手系)。+crossV3 :: Vec3 -> Vec3 -> Vec3+crossV3 (Vec3 ax ay az) (Vec3 bx by bz) =+  Vec3 (ay * bz - az * by) (az * bx - ax * bz) (ax * by - ay * bx)++-- | L2 ノルム。+lengthV3 :: Vec3 -> Double+lengthV3 v = sqrt (dotV3 v v)++-- | 正規化 (= 単位ベクトル化)。 ゼロベクトルは (0,0,0) 返す (= 例外無し)。+normalizeV3 :: Vec3 -> Vec3+normalizeV3 v =+  let l = lengthV3 v+  in if l < 1e-12 then Vec3 0 0 0 else scaleV3 (1 / l) v
+ test/Spec.hs view
@@ -0,0 +1,712 @@+-- | Phase 3 A2 段階の test entry。+-- 中核型 (Vec3 / Mat4) の基本演算のみテスト。 projection 等は A3 以降。+{-# LANGUAGE OverloadedStrings #-}+module Main where++import Graphics.Hgg.ThreeD.Types+import Graphics.Hgg.ThreeD.Projection+import Graphics.Hgg.ThreeD.Axes+import Graphics.Hgg.ThreeD.Surface (Surface3D (..), defaultSurface3D, renderSurface3D)+import Graphics.Hgg.ThreeD.Spec (surface3DGrid, surface3D, colormap3D, viridisStops3D, layerToSurface,+                                 xRange3D, yRange3D, lyr3XRange, lyr3YRange, lyr3Grid,+                                 contourX, contourY, contourZ, lyr3Contours,+                                 ContourDir(..), text3D, lyr3Labels,+                                 alpha3D)+import Graphics.Hgg.ThreeD.Easy (padAxes3D, normPoint3D, normLayer3D, renderProjectedContour3D,+                                 saveSVG3DFacet, renderSpec3DInPanel,+                                 savePDF3D, savePNG3D)+import Graphics.Hgg.ThreeD.Spec (axisTitles3D, zAspect3D, pane3D, camera, title3D, width3DV, height3DV)+import Graphics.Hgg.ThreeD.Spec (colorBy3D, lyr3PtColors, lyr3Legend, layerToScatter,+                                 colorContinuousBy3D, sizeBy3D, sizeRange3D,+                                 lyr3PtSizes, lyr3Colorbar)+import Graphics.Hgg.ThreeD.Scatter (Scatter3D (..), defaultScatter3D)+import Graphics.Hgg.ThreeD.Spec (bar3D, bar3DPoints, barStyle3D, barWidth3D, errorBar3D,+                                 layerToBar, BarStyle3D (..), lyr3BarBaseZ, lyr3PtErrs)+import Graphics.Hgg.ThreeD.Spec (text3DPoints, annotate3D, color3D, size3D, colorRGBA3D)+import Graphics.Hgg.ThreeD.Spec (xAspect3D, yAspect3D, logScale3D)+import Graphics.Hgg.ThreeD.Spec (stem3D, stem3DPoints, stemBaseZ, layerPoints,+                                 Mark3DKind (..), lyr3Kind)+import Graphics.Hgg.ThreeD.Spec (quiver3D, vecScale3D, layerToQuiver, trisurf)+import Graphics.Hgg.ThreeD.Line (Quiver3D (..), defaultQuiver3D, renderQuiver3D)+import Graphics.Hgg.ThreeD.Delaunay (delaunay2D)+import Graphics.Hgg.ThreeD.Surface (trianglesFacesDepth)+import Graphics.Hgg.ThreeD.Bar (Bar3D (..), defaultBar3D, barFacesDepth,+                                renderBarSticks, renderStems3D, renderErrorBars3D)+import Graphics.Hgg.Palette (ggplotHue)+import Graphics.Hgg.Render.Common (continuousColor)+import Data.List (isInfixOf)+import System.Directory (getFileSize, removeFile, doesFileExist)+import Control.Monad (when)+import Graphics.Hgg.ThreeD.Bound (BoundPlot3D (..), unBound3D)+import Graphics.Hgg.ThreeD.Spec (layer3D, scatter3D, scatter3DPoints, line3DPoints,+                                 resolveLayer3D, lyr3Points, vs3Layers, VisualSpec3D (..))+import Graphics.Hgg.Frame ((|>>))+import Graphics.Hgg.Spec (ColData (..), inline, emptyResolver)+import Graphics.Hgg.Color (fromHex)+import qualified Graphics.Hgg.Validate+import qualified Data.Text as T+import qualified Data.Vector as V+import Data.Monoid (Last (..), First (..))+import Data.List (nub)+import qualified Graphics.Hgg.Render+import Test.Hspec++main :: IO ()+main = hspec $ do+  describe "Phase 3 A2: Vec3 基本演算" $ do+    it "addV3: (1,2,3) + (4,5,6) = (5,7,9)" $+      addV3 (Vec3 1 2 3) (Vec3 4 5 6) `shouldBe` Vec3 5 7 9+    it "subV3: (4,5,6) - (1,2,3) = (3,3,3)" $+      subV3 (Vec3 4 5 6) (Vec3 1 2 3) `shouldBe` Vec3 3 3 3+    it "scaleV3: 2 * (1,2,3) = (2,4,6)" $+      scaleV3 2 (Vec3 1 2 3) `shouldBe` Vec3 2 4 6+    it "dotV3: (1,2,3) · (4,5,6) = 32" $+      dotV3 (Vec3 1 2 3) (Vec3 4 5 6) `shouldBe` 32++    it "crossV3: (1,0,0) × (0,1,0) = (0,0,1) (= 右手系)" $+      crossV3 (Vec3 1 0 0) (Vec3 0 1 0) `shouldBe` Vec3 0 0 1+    it "crossV3: (0,1,0) × (0,0,1) = (1,0,0)" $+      crossV3 (Vec3 0 1 0) (Vec3 0 0 1) `shouldBe` Vec3 1 0 0+    it "crossV3 反対称: a × b = -(b × a)" $+      let a = Vec3 2 3 4; b = Vec3 5 6 7+          Vec3 abx aby abz = crossV3 a b+          Vec3 bax bay baz = crossV3 b a+      in [abx, aby, abz] `shouldBe` [- bax, - bay, - baz]++    it "lengthV3: (3,4,0) = 5" $+      lengthV3 (Vec3 3 4 0) `shouldBe` 5+    it "normalizeV3: (3,0,0) → (1,0,0)" $+      normalizeV3 (Vec3 3 0 0) `shouldBe` Vec3 1 0 0+    it "normalizeV3: zero ベクトルは zero (= 例外無し)" $+      normalizeV3 (Vec3 0 0 0) `shouldBe` Vec3 0 0 0++  describe "Phase 3 A2: Camera3D / Projection3D 型" $ do+    it "Camera3D を構築できる" $+      let c = Camera3D (Point3 0 0 5) (Point3 0 0 0) (Vec3 0 1 0)+      in cameraTarget c `shouldBe` Point3 0 0 0+    it "Orthographic projection を構築できる" $+      let p = Orthographic 1 1 0.1 100+      in orthoFar p `shouldBe` 100+    it "Perspective projection を構築できる" $+      let p = Perspective (pi / 4) 1.5 0.1 100+      in perspAspect p `shouldBe` 1.5++  describe "Phase 3 z-up convention helpers" $ do+    it "zUp = Vec3 0 0 1" $+      zUp `shouldBe` Vec3 0 0 1+    it "defaultCameraZUp dist で up = zUp、 eye の z > 0" $+      let c = defaultCameraZUp 5+          Point3 _ _ ez = cameraEye c+      in do+           cameraUp c `shouldBe` zUp+           (ez > 0) `shouldBe` True+    it "defaultPerspective: fov 45°、 aspect 1.0" $+      case defaultPerspective of+        Perspective fov a _ _ -> do+          abs (fov - pi/4) `shouldSatisfy` (< 1e-9)+          a `shouldBe` 1.0+        _ -> expectationFailure "expected Perspective"++    it "yUp = Vec3 0 1 0 (= OpenGL 慣例)" $+      yUp `shouldBe` Vec3 0 1 0+    it "defaultCameraYUp dist で up = yUp、 eye の y > 0 (= 上方視点)" $+      let c = defaultCameraYUp 5+          Point3 _ ey _ = cameraEye c+      in do+           cameraUp c `shouldBe` yUp+           (ey > 0) `shouldBe` True++    it "z-up と y-up は up vector のみ異なる (= 構造同型)" $+      let cz = defaultCameraZUp 5+          cy = defaultCameraYUp 5+      in do+           cameraTarget cz `shouldBe` cameraTarget cy+           cameraUp cz `shouldNotBe` cameraUp cy++  describe "Phase 3 A3: Mat4 / Matrix 演算" $ do+    it "identityM × identityM = identityM" $+      multM identityM identityM `shouldBe` identityM+    it "identityM × P = P" $+      let p = Point3 2 3 5+      in transformPoint identityM p `shouldBe` p++  describe "Phase 3 A3: viewMatrix (= lookAt)" $ do+    it "camera が eye=(0,0,5)、 target=(0,0,0)、 up=(0,1,0) のとき world 原点は camera 前 5 (= cz = -5)" $+      let cam = Camera3D (Point3 0 0 5) (Point3 0 0 0) (Vec3 0 1 0)+          view = viewMatrix cam+          Point3 _ _ cz = transformPoint view (Point3 0 0 0)+      in abs (cz - (-5)) `shouldSatisfy` (< 1e-9)+    it "camera 前の点 (= world (0,0,0)) は camera space で y=0 / x=0" $+      let cam = Camera3D (Point3 0 0 5) (Point3 0 0 0) (Vec3 0 1 0)+          view = viewMatrix cam+          Point3 cx cy _ = transformPoint view (Point3 0 0 0)+      in (abs cx + abs cy) `shouldSatisfy` (< 1e-9)++  describe "Phase 3 A3: project3D (= 一括投影)" $ do+    let cam = Camera3D (Point3 0 0 5) (Point3 0 0 0) (Vec3 0 1 0)+        ortho = Orthographic 2 2 0.1 100+        persp = Perspective (pi/4) 1.0 0.1 100+        vp = Viewport 0 0 200 200++    it "Orthographic: 原点 (0,0,0) → screen 中央 (100, 100)" $+      let pr = project3D cam ortho vp (Point3 0 0 0)+      in (abs (projX pr - 100) + abs (projY pr - 100)) `shouldSatisfy` (< 1e-9)++    it "Orthographic: 右上 (1, 1, 0) → screen 右上 (= ortho 半幅 2 で 1/2 倍位置)" $+      let pr = project3D cam ortho vp (Point3 1 1 0)+      in do+           -- x: (1/2 + 0.5) * 200 = 150+           abs (projX pr - 150) `shouldSatisfy` (< 1e-9)+           -- y: (1 - (1/2 + 0.5)/1) * 200... 反転で 50+           abs (projY pr - 50) `shouldSatisfy` (< 1e-9)++    it "Perspective: 同じ x,y の point でも z が違うと scale 異なる (= 遠近感)" $+      let p1 = project3D cam persp vp (Point3 1 0 0)+          p2 = project3D cam persp vp (Point3 1 0 (-2))  -- 奥+      in projX p1 `shouldNotBe` projX p2++    it "depth (NDC z): 手前の点は -1 寄り、 奥は +1 寄り" $+      let near = project3D cam persp vp (Point3 0 0 4)   -- camera 近く+          far  = project3D cam persp vp (Point3 0 0 (-4)) -- 奥+      in projDepth near < projDepth far `shouldBe` True++    it "viewport y 反転: world 上 (y > 0) → screen 上 (y < 中央)" $+      let upper = project3D cam ortho vp (Point3 0 1 0)+      in projY upper < 100 `shouldBe` True++  describe "Phase 3 A4: Axes3D + niceTicks3D + renderAxes3D" $ do+    it "niceTicks3D 5 0 10 = [0, 2.5, 5, 7.5, 10]" $+      niceTicks3D 5 0 10 `shouldBe` [0, 2.5, 5, 7.5, 10]+    it "niceTicks3D 1 0 10 = [0] (= n=1 で 1 点)" $+      niceTicks3D 1 0 10 `shouldBe` [0]+    it "defaultAxes3D は単位 cube" $ do+      axesXMin defaultAxes3D `shouldBe` (-1)+      axesXMax defaultAxes3D `shouldBe` 1+    it "renderAxes3D: 12 cube edges + 3軸 tick + 3 軸名 = それなりの数の Primitive" $+      let cam = Camera3D (Point3 3 3 3) (Point3 0 0 0) (Vec3 0 1 0)+          proj = Perspective (pi/4) 1 0.1 100+          vp = Viewport 0 0 400 400+          prims = renderAxes3D cam proj vp defaultAxes3D+      in length prims `shouldSatisfy` (> 15)+    it "renderAxes3D: PLine と PText 両方を含む" $+      let cam = Camera3D (Point3 3 3 3) (Point3 0 0 0) (Vec3 0 1 0)+          proj = Perspective (pi/4) 1 0.1 100+          vp = Viewport 0 0 400 400+          prims = renderAxes3D cam proj vp defaultAxes3D+          nLines = length [() | Graphics.Hgg.Render.PLine{} <- prims]+          nTexts = length [() | Graphics.Hgg.Render.PText{} <- prims]+      in (nLines > 0 && nTexts > 0) `shouldBe` True++  describe "Phase 24 A2: surface z colormap + colorbar" $ do+    let cam  = Camera3D (Point3 3 3 3) (Point3 0 0 0) (Vec3 0 0 1)+        proj = Perspective (pi/4) 1 0.1 100+        vp   = Viewport 0 0 400 400+        -- z が 0..1 へ単調増加する 3x3 grid+        grid = [[0, 0.1, 0.2], [0.3, 0.4, 0.5], [0.6, 0.8, 1.0]]+        fills sf = [ c | Graphics.Hgg.Render.PPath _ (Graphics.Hgg.Render.FillStyle c _) _+                           <- renderSurface3D cam proj vp sf ]+    it "colormap なし (shading off) = 全面同色 (後方互換)" $+      let sf = (defaultSurface3D grid) { sf3Shaded = False }+      in nub (fills sf) `shouldBe` ["#5b9bd5"]+    it "colormap あり (shading off) = z で面色が変わる (低 z ≠ 高 z)" $+      let sf = (defaultSurface3D grid) { sf3Shaded = False+                                       , sf3Colormap = Just viridisStops3D }+          cs = fills sf+      in (length (nub cs) > 3, head cs /= last cs) `shouldBe` (True, True)+    it "layerToSurface: colormap3D が sf3Colormap に渡る" $+      sf3Colormap (layerToSurface (surface3DGrid grid <> colormap3D))+        `shouldBe` Just viridisStops3D+    it "layerToSurface: colormap 未指定は Nothing (従来単色)" $+      sf3Colormap (layerToSurface (surface3DGrid grid)) `shouldBe` Nothing++  describe "Phase 25 A4: surface 透過 (alpha3D 配線)" $ do+    let cam  = Camera3D (Point3 3 3 3) (Point3 0 0 0) (Vec3 0 0 1)+        proj = Perspective (pi/4) 1 0.1 100+        vp   = Viewport 0 0 400 400+        grid = [[0, 0.1], [0.2, 0.3]]+        alphas sf = nub [ a | Graphics.Hgg.Render.PPath _ (Graphics.Hgg.Render.FillStyle _ a) _+                                <- renderSurface3D cam proj vp sf ]+    it "既定 sf3Alpha = 1 (後方互換・全面不透明)" $+      alphas (defaultSurface3D grid) `shouldBe` [1.0]+    it "layerToSurface: alpha3D が sf3Alpha に渡る" $+      sf3Alpha (layerToSurface (surface3DGrid grid <> alpha3D 0.5)) `shouldBe` 0.5+    it "alpha3D 未指定は既定 1 (素通し)" $+      sf3Alpha (layerToSurface (surface3DGrid grid)) `shouldBe` 1.0+    it "renderSurface3D: 面 fill alpha が sf3Alpha を反映する" $+      alphas ((defaultSurface3D grid) { sf3Alpha = 0.4 }) `shouldBe` [0.4]+    it "colorRGBA3D \"#00887766\" == color3D (fromHex \"#008877\") <> alpha3D (0x66/255)" $+      colorRGBA3D "#00887766"+        `shouldBe` (color3D (fromHex "#008877") <> alpha3D (102/255))++  describe "Phase 25 A5: bar3d (直方体 / stick) + 誤差棒" $ do+    let cam  = Camera3D (Point3 3 3 3) (Point3 0 0 0) (Vec3 0 0 1)+        proj = Perspective (pi/4) 1 0.1 100+        vp   = Viewport 0 0 400 400+        tops = [Point3 0 0 0.5, Point3 0.5 0.5 1.0]+    it "bar3DPoints: layerToBar の既定 (BarCuboid・半幅 0.04・base 0)" $ do+      let b = layerToBar (bar3DPoints tops)+      (br3Style b, br3HalfW b, br3BaseZ b) `shouldBe` (BarCuboid, 0.04, 0.0)+    it "barStyle3D / barWidth3D が layerToBar に渡る" $ do+      let b = layerToBar (bar3DPoints tops <> barStyle3D BarStick <> barWidth3D 0.1)+      (br3Style b, br3HalfW b) `shouldBe` (BarStick, 0.1)+    it "barFacesDepth: cuboid は棒あたり 5 quad (PPath)" $+      let prims = barFacesDepth cam proj vp (defaultBar3D tops)+      in length prims `shouldBe` 5 * length tops+    it "barFacesDepth: stick は face を出さない ([])" $+      barFacesDepth cam proj vp ((defaultBar3D tops) { br3Style = BarStick }) `shouldBe` []+    it "renderBarSticks: stick は棒あたり 1 PLine" $+      let prims = renderBarSticks cam proj vp ((defaultBar3D tops) { br3Style = BarStick })+      in length prims `shouldBe` length tops+    it "renderBarSticks: cuboid 時は線を出さない ([])" $+      renderBarSticks cam proj vp (defaultBar3D tops) `shouldBe` []+    it "renderErrorBars3D: 点あたり 3 PLine (whisker + 上下キャップ)・err<=0 はスキップ" $+      let prims = renderErrorBars3D cam proj vp "#333" 1.0+                    [(Point3 0 0 0.5, 0.2), (Point3 0.5 0.5 1.0, 0)]+      in length prims `shouldBe` 3   -- 2 点目 (err=0) はスキップ++  describe "Phase 25 A5: bar/err の正規化パイプライン (|>> + normLayer3D)" $ do+    let ax = Axes3D { axesXMin = 0, axesXMax = 2, axesYMin = 0, axesYMax = 2+                    , axesZMin = 0, axesZMax = 10, axesNTicks = 5+                    , axesXLog = False, axesYLog = False, axesZLog = False }+        df = [ ("x", NumData (V.fromList [0, 1, 2]))+             , ("y", NumData (V.fromList [0, 1, 2]))+             , ("h", NumData (V.fromList [0, 5, 10]))+             , ("se", NumData (V.fromList [1, 1, 1])) ] :: [(T.Text, ColData)]+        spec = layer3D (bar3D "x" "y" "h" <> errorBar3D "se")+        (_, resolved) = unBound3D (df |>> spec)+        l0 = head (vs3Layers resolved)+        l  = normLayer3D ax l0+    it "bar 底面 z = data 0 の正規化値 (zMin=0 → -1)" $+      getLast (lyr3BarBaseZ l) `shouldBe` Just (-1.0)+    it "err は z 量として正規化される (data 1 / z-span 10 → 0.2)" $+      getLast (lyr3PtErrs l) `shouldBe` Just [0.2, 0.2, 0.2]+    it "renderSpec3DInPanel: bar 直方体 (PPath) + 誤差棒 (PLine) が出る" $+      let prims = renderSpec3DInPanel 0 0 400 400 (resolved <> camera (defaultCameraZUp 4))+          paths = [ () | Graphics.Hgg.Render.PPath{} <- prims ]+          lines = [ () | Graphics.Hgg.Render.PLine{} <- prims ]+      in (length paths >= 15, length lines >= 9) `shouldBe` (True, True)++  describe "Phase 26 A4: 3D stem (lollipop)" $ do+    let cam  = Camera3D (Point3 3 3 3) (Point3 0 0 0) (Vec3 0 0 1)+        proj = Perspective (pi/4) 1 0.1 100+        vp   = Viewport 0 0 400 400+        tops = [Point3 0 0 0.5, Point3 0.5 0.5 1.0]+    it "stem3D / stem3DPoints は M3Stem kind を立てる" $ do+      lyr3Kind (stem3DPoints tops) `shouldBe` First (Just M3Stem)+      lyr3Kind (stem3D "x" "y" "z") `shouldBe` First (Just M3Stem)+    it "renderStems3D: stem あたり 1 PLine + 1 PCircle (= 2 prim)" $+      let prims = renderStems3D cam proj vp (defaultBar3D tops) 5+      in length prims `shouldBe` 2 * length tops+    it "stemBaseZ は data 空間で正規化され lyr3BarBaseZ に落ちる (zMin=0,zMax=10,base=2 → -0.6)" $+      let ax = Axes3D { axesXMin = 0, axesXMax = 2, axesYMin = 0, axesYMax = 2+                      , axesZMin = 0, axesZMax = 10, axesNTicks = 5+                      , axesXLog = False, axesYLog = False, axesZLog = False }+          df = [ ("x", NumData (V.fromList [0, 1, 2]))+               , ("y", NumData (V.fromList [0, 1, 2]))+               , ("z", NumData (V.fromList [3, 5, 8])) ] :: [(T.Text, ColData)]+          spec = layer3D (stem3D "x" "y" "z" <> stemBaseZ 2)+          (_, resolved) = unBound3D (df |>> spec)+          l = normLayer3D ax (head (vs3Layers resolved))+      in getLast (lyr3BarBaseZ l) `shouldBe` Just (-0.6)+    it "layerPoints: stem は底面 (stemBaseZ) を axis box に含める" $+      -- tops 2 点 + bases 2 点 (z=base) = 4 点 (err 無し)+      let l = stem3DPoints tops <> stemBaseZ 0+      in length (layerPoints l) `shouldBe` 4++  describe "Phase 26 A3: 3D vector field (quiver3D)" $ do+    let cam  = Camera3D (Point3 3 3 3) (Point3 0 0 0) (Vec3 0 0 1)+        proj = Perspective (pi/4) 1 0.1 100+        vp   = Viewport 0 0 400 400+        items = [ (Point3 0 0 0, Vec3 1 0 0), (Point3 1 1 1, Vec3 0 0 2) ]+    it "quiver3D は M3Quiver kind + 位置/ベクトルを格納" $+      lyr3Kind (quiver3D items) `shouldBe` First (Just M3Quiver)+    it "layerToQuiver: 始点 N 個・終点 N 個 (autoscale で終点 = 始点 + scaled vec)" $+      let q = layerToQuiver (quiver3D items)+      in (length (q3Starts q), length (q3Ends q)) `shouldBe` (2, 2)+    it "layerToQuiver: 最長ベクトルの矢印長 = cube の 0.35 (autoscale)" $+      -- items の最長 vec は (0,0,2) で長さ 2。 autoS = 0.35/2。 終点 z = 1 + 0.35。+      let q = layerToQuiver (quiver3D items)+          Point3 _ _ ez = q3Ends q !! 1+      in abs (ez - (1 + 0.35)) < 1e-9 `shouldBe` True+    it "renderQuiver3D: 矢印 1 本につき 3 PLine (本線 + 矢じり 2)" $+      let q = defaultQuiver3D [Point3 0 0 0, Point3 1 0 0]+                              [Point3 0.3 0 0, Point3 1 0.3 0]+          prims = renderQuiver3D cam proj vp q+      in length prims `shouldBe` 3 * 2++  describe "Phase 26 A5: trisurf (Delaunay 三角分割)" $ do+    let cam  = Camera3D (Point3 3 3 3) (Point3 0 0 0) (Vec3 0 0 1)+        proj = Perspective (pi/4) 1 0.1 100+        vp   = Viewport 0 0 400 400+    it "delaunay2D: 3 点未満は [] (三角形を作れない)" $ do+      delaunay2D [] `shouldBe` []+      delaunay2D [(0,0),(1,0)] `shouldBe` []+    it "delaunay2D: 正方形 4 隅 → 2 三角形" $+      length (delaunay2D [(0,0),(1,0),(1,1),(0,1)]) `shouldBe` 2+    it "delaunay2D: 正方形 + 中心 (5 点) → 4 三角形" $+      length (delaunay2D [(0,0),(1,0),(1,1),(0,1),(0.5,0.5)]) `shouldBe` 4+    it "delaunay2D: 全 index が元の点数未満 (super-triangle 由来は除去)" $+      let n = 5+          tris = delaunay2D [(0,0),(1,0),(1,1),(0,1),(0.5,0.5)]+      in all (\(a,b,c) -> all (< n) [a,b,c] && all (>= 0) [a,b,c]) tris+           `shouldBe` True+    it "trisurf: M3Trisurf kind を立てる" $+      lyr3Kind (trisurf [Point3 0 0 0]) `shouldBe` First (Just M3Trisurf)+    it "trianglesFacesDepth: 三角形 1 つにつき 1 PPath" $+      let tris = [ (Point3 0 0 0, Point3 1 0 0, Point3 0 1 1)+                 , (Point3 1 0 0, Point3 1 1 0, Point3 0 1 1) ]+          prims = trianglesFacesDepth cam proj vp "#5b9bd5" "#333" True Nothing 1.0 tris+      in length prims `shouldBe` 2++  describe "Phase 25 A6: 壁面 pane + gridline (G5)" $ do+    let cam  = Camera3D (Point3 3 (-3) 2) (Point3 0 0 0) (Vec3 0 0 1)+        proj = Perspective (pi/4) 1 0.1 100+        vp   = Viewport 0 0 400 400+        ax   = defaultAxes3D+    it "renderAxes3DPanes: 奥壁 3 面 (PPath) + tick 格子線 (PLine) を出す" $+      let prims = renderAxes3DPanes id defaultPaneStyle3D cam proj vp ax+          paths = [ () | Graphics.Hgg.Render.PPath{}  <- prims ]+          lines = [ () | Graphics.Hgg.Render.PLine{}  <- prims ]+      in (length paths, length lines >= 6) `shouldBe` (3, True)+    it "renderAxes3DPanes: pane 塗りは defaultPaneStyle3D の色" $+      let prims = renderAxes3DPanes id defaultPaneStyle3D cam proj vp ax+          fills = nub [ c | Graphics.Hgg.Render.PPath _ (Graphics.Hgg.Render.FillStyle c _) _ <- prims ]+      in fills `shouldBe` [paneFill defaultPaneStyle3D]+    it "renderSpec3DInPanel: 既定 (pane ON) は pane OFF より PPath/PLine が多い" $+      let base = layer3D (scatter3DPoints [Point3 0 0 0, Point3 1 1 1])+                   <> camera cam+          onP  = renderSpec3DInPanel 0 0 400 400 base+          offP = renderSpec3DInPanel 0 0 400 400 (base <> pane3D False)+          npath p = length [ () | Graphics.Hgg.Render.PPath{} <- p ]+          nline p = length [ () | Graphics.Hgg.Render.PLine{} <- p ]+      in (npath onP > npath offP, nline onP > nline offP) `shouldBe` (True, True)++  describe "Phase 25 A7 / 30 A6: 3D テキスト注釈 (text3DPoints / annotate3D)" $ do+    let cam = Camera3D (Point3 3 (-3) 2) (Point3 0 0 0) (Vec3 0 0 1)+    it "text3DPoints: 各点ごとに PText が出る (ラベル文字列を含む)" $+      let spec = layer3D (text3DPoints [(Point3 0 0 1, "peak"), (Point3 1 1 0, "corner")])+                   <> camera cam+          prims = renderSpec3DInPanel 0 0 400 400 spec+          texts = [ t | Graphics.Hgg.Render.PText _ t _ <- prims ]+      in (("peak" `elem` texts), ("corner" `elem` texts)) `shouldBe` (True, True)+    it "annotate3D: 単一ラベルは text3DPoints [(p,t)] と同値" $+      annotate3D (Point3 0 0 1) "x" `shouldBe` text3DPoints [(Point3 0 0 1, "x")]+    it "Phase 30 A6: annotate3D は <> で畳める (複数注釈が累積)" $+      let spec = layer3D (annotate3D (Point3 0 0 1) "A"+                            <> annotate3D (Point3 1 1 0) "B"+                            <> annotate3D (Point3 (-1) (-1) 0) "C") <> camera cam+          prims = renderSpec3DInPanel 0 0 400 400 spec+          texts = [ t | Graphics.Hgg.Render.PText _ t _ <- prims ]+      in (all (`elem` texts) ["A", "B", "C"]) `shouldBe` True+    it "color3D/size3D で文字色・サイズを上書きできる" $+      let spec = layer3D (annotate3D (Point3 0 0 0) "hi"+                            <> color3D (fromHex "#d62728") <> size3D 15) <> camera cam+          prims = renderSpec3DInPanel 0 0 400 400 spec+          sty   = [ (c, s) | Graphics.Hgg.Render.PText _ "hi"+                               (Graphics.Hgg.Render.TextStyle c s _ _ _ _ _) <- prims ]+      in sty `shouldBe` [("#d62728", 15)]++  describe "Phase 25 A8: log 軸 + x/y アスペクト (G7)" $ do+    let axZLog = defaultAxes3D { axesZMin = 1, axesZMax = 100, axesZLog = True }+    it "logTicks3D 1 1000 = 10 の冪 [1,10,100,1000]" $+      logTicks3D 1 1000 `shouldBe` [1, 10, 100, 1000]+    it "normPoint3D (log z): 幾何平均 z=10 (1..100) は中央 0 に写る" $+      let Point3 _ _ z = normPoint3D axZLog (Point3 0 0 10)+      in abs z `shouldSatisfy` (< 1e-9)+    it "normPoint3D (log z): 端 z=1 → -1、 z=100 → +1" $ do+      let Point3 _ _ zlo = normPoint3D axZLog (Point3 0 0 1)+          Point3 _ _ zhi = normPoint3D axZLog (Point3 0 0 100)+      abs (zlo + 1) `shouldSatisfy` (< 1e-9)+      abs (zhi - 1) `shouldSatisfy` (< 1e-9)+    it "logScale3D は vs3Log を設定する" $+      getLast (vs3Log (logScale3D False False True)) `shouldBe` Just (False, False, True)+    it "xAspect3D 2 は軸 box を x 方向に広げる (PLine の x 範囲が拡大)" $+      let base = layer3D (scatter3DPoints [Point3 (-1) (-1) (-1), Point3 1 1 1])+                   <> camera (defaultCameraZUp 4)+          xspan p = let xs = [ x | Graphics.Hgg.Render.PLine (Graphics.Hgg.Render.Point x _) _ _ <- p ]+                             ++ [ x | Graphics.Hgg.Render.PLine _ (Graphics.Hgg.Render.Point x _) _ <- p ]+                    in maximum xs - minimum xs+          p1 = renderSpec3DInPanel 0 0 400 400 base+          p2 = renderSpec3DInPanel 0 0 400 400 (base <> xAspect3D 2)+      in (xspan p2 > xspan p1) `shouldBe` True++  describe "Phase 24 A3: 正規化 pipeline (実スケールデータ → [-1,1]^3)" $ do+    let ax = Axes3D { axesXMin = 0, axesXMax = 4, axesYMin = 0, axesYMax = 6+                    , axesZMin = 50, axesZMax = 80, axesNTicks = 5+                    , axesXLog = False, axesYLog = False, axesZLog = False }+    it "normPoint3D: bbox の min 角 → (-1,-1,-1)、 max 角 → (1,1,1)" $ do+      normPoint3D ax (Point3 0 0 50) `shouldBe` Point3 (-1) (-1) (-1)+      normPoint3D ax (Point3 4 6 80) `shouldBe` Point3 1 1 1+    it "normPoint3D: 中心 → 原点" $+      normPoint3D ax (Point3 2 3 65) `shouldBe` Point3 0 0 0+    it "padAxes3D: 退化軸 (min = max) を ±0.5 に広げる" $ do+      let d = padAxes3D ax { axesZMin = 7, axesZMax = 7 }+      (axesZMin d, axesZMax d) `shouldBe` (6.5, 7.5)+      (axesXMin d, axesXMax d) `shouldBe` (0, 4)   -- 非退化軸は不変+    it "normLayer3D: surface の grid z が [-1,1]・x/y range が (-1,1) になる" $ do+      let l  = surface3DGrid [[50, 65], [65, 80]]+                 <> xRange3D (0, 4) <> yRange3D (0, 6)+          l' = normLayer3D ax l+      getLast (lyr3XRange l') `shouldBe` Just (-1, 1)+      getLast (lyr3YRange l') `shouldBe` Just (-1, 1)+      getLast (lyr3Grid l') `shouldBe` Just [[-1, 0], [0, 1]]++  describe "Phase 24 A5 / #2: 投影 contour (床 = 等値面 / 壁 = 断面)" $ do+    -- eye = (3,-3,3) → far wall は x=-1 / y=+1 / z=-1 (= 自動固定先)+    let cam  = Camera3D (Point3 3 (-3) 3) (Point3 0 0 0) (Vec3 0 0 1)+        proj = Perspective (pi/4) 1 0.1 100+        vp   = Viewport 0 0 400 400+        -- 正規化済を想定した grid (z ∈ [-1,1]、 x/y range = (-1,1))+        grid = [ [ fromIntegral (i + j) / 4 - 1 | i <- [0 .. 4 :: Int] ]+               | j <- [0 .. 4 :: Int] ]+        surfC c = surface3DGrid grid <> xRange3D (-1, 1) <> yRange3D (-1, 1) <> c+        render l = renderProjectedContour3D (1, 1, 1) cam proj vp l+        nLines l = length [ () | Graphics.Hgg.Render.PLine{} <- render l ]+    it "contour 未指定 = 線なし (OFF)" $+      render (surface3DGrid grid <> xRange3D (-1,1) <> yRange3D (-1,1)) `shouldBe` []+    it "contourZ (床等値面) = PLine 群を描く" $+      (nLines (surfC (contourZ 8)) > 0) `shouldBe` True+    it "contourX / contourY (壁断面) = PLine 群を描く" $ do+      (nLines (surfC (contourX 8)) > 0) `shouldBe` True+      (nLines (surfC (contourY 8)) > 0) `shouldBe` True+    it "本数を増やすと線分が増える (単調・全 dir)" $ do+      (nLines (surfC (contourZ 4)) <= nLines (surfC (contourZ 12))) `shouldBe` True+      (nLines (surfC (contourX 2)) <= nLines (surfC (contourX 9))) `shouldBe` True+    it "contourX/Y/Z は (dir, 本数) を lyr3Contours に積む" $ do+      lyr3Contours (contourZ 8) `shouldBe` [(ContourZ, 8)]+      lyr3Contours (contourX 5) `shouldBe` [(ContourX, 5)]+    it "1 layer に contourX <> contourY <> contourZ を合成できる (3 件)" $+      lyr3Contours (contourX 8 <> contourY 6 <> contourZ 4)+        `shouldBe` [(ContourX, 8), (ContourY, 6), (ContourZ, 4)]+    it "合成した 3 dir は単一 layer から全て描かれる (各 dir 単独の和以上)" $+      let n3 = nLines (surfC (contourX 8 <> contourY 8 <> contourZ 8))+      in (n3 >= nLines (surfC (contourX 8))) `shouldBe` True++  describe "Phase 24 A7: 群別タイル配置 (facet)" $ do+    let spec = layer3D (scatter3DPoints [Point3 0 0 0, Point3 1 1 1])+        xsOf p = [ x | Graphics.Hgg.Render.PLine (Graphics.Hgg.Render.Point x1 _)+                                                 (Graphics.Hgg.Render.Point x2 _) _+                         <- renderSpec3DInPanel p 0 400 400 spec, x <- [x1, x2] ]+        ysOf p = [ y | Graphics.Hgg.Render.PLine (Graphics.Hgg.Render.Point _ y1)+                                                 (Graphics.Hgg.Render.Point _ y2) _+                         <- renderSpec3DInPanel p 0 400 400 spec, y <- [y1, y2] ]+    it "renderSpec3DInPanel: パネルを px ずらすと全 PLine x が px 平行移動 (y 不変)" $+      let dxs = zipWith (-) (xsOf 300) (xsOf 0)+          dys = zipWith (-) (ysOf 300) (ysOf 0)+      in ( all (\d -> abs (d - 300) < 1e-6) dxs+         , all (\d -> abs d < 1e-6) dys+         , not (null dxs) )+        `shouldBe` (True, True, True)+    it "renderSpec3DInPanel: 軸 box の PLine が出る (非空)" $+      (length (xsOf 0) > 0) `shouldBe` True+    it "saveSVG3DFacet: 2 群を書き出し各群ラベルが SVG に入る" $ do+      let panels = [ ("GrpA", layer3D (scatter3DPoints [Point3 0 0 0]))+                   , ("GrpB", layer3D (scatter3DPoints [Point3 1 1 1])) ]+      saveSVG3DFacet "/tmp/hgg-facet-test.svg" panels+      svg <- readFile "/tmp/hgg-facet-test.svg"+      (("GrpA" `isInfixOf` svg) && ("GrpB" `isInfixOf` svg)) `shouldBe` True+    it "saveSVG3DFacet: 空リストは no-op (例外なし)" $+      saveSVG3DFacet "/tmp/hgg-facet-empty.svg" [] `shouldReturn` ()++  describe "Phase 24 A8: 視点 preset" $ do+    it "cameraTop: eye が +z 真上・target 原点" $+      let c = cameraTop 5; Point3 ex ey ez = cameraEye c+      in (ex, ey, ez > 0) `shouldBe` (0, 0, True)+    it "cameraFront: eye が -y 方向" $+      let Point3 ex ey ez = cameraEye (cameraFront 5)+      in (ex, ey < 0, ez) `shouldBe` (0, True, 0)+    it "cameraSide: eye が +x 方向" $+      let Point3 ex ey ez = cameraEye (cameraSide 5)+      in (ex > 0, ey, ez) `shouldBe` (True, 0, 0)+    it "cameraIso = defaultCameraZUp (同一)" $+      cameraEye (cameraIso 5) `shouldBe` cameraEye (defaultCameraZUp 5)++  describe "Phase 24 A8: 軸タイトル" $ do+    let cam  = Camera3D (Point3 3 3 3) (Point3 0 0 0) (Vec3 0 0 1)+        proj = Perspective (pi/4) 1 0.1 100+        vp   = Viewport 0 0 400 400+    it "renderAxes3DWithLabels: 指定した軸名が PText に出る" $+      let prims = renderAxes3DWithLabels id ("深さ", "幅", "高さ") cam proj vp defaultAxes3D+          texts = [ s | Graphics.Hgg.Render.PText _ s _ <- prims ]+      in (("深さ" `elem` texts), ("幅" `elem` texts), ("高さ" `elem` texts))+           `shouldBe` (True, True, True)+    it "renderAxes3DWith (= ラベル無版) は \"x\"/\"y\"/\"z\" (後方互換)" $+      let prims = renderAxes3D cam proj vp defaultAxes3D+          texts = [ s | Graphics.Hgg.Render.PText _ s _ <- prims ]+      in (("x" `elem` texts), ("y" `elem` texts), ("z" `elem` texts))+           `shouldBe` (True, True, True)++  describe "Phase 24 A8: z aspect box" $ do+    let surf = layer3D (surface3DGrid [[0, 0.5], [0.5, 1]] <> xRange3D (-1,1) <> yRange3D (-1,1))+                 <> camera (defaultCameraZUp 4)+        ysOf za = [ y | Graphics.Hgg.Render.PPath segs _ _+                          <- renderSpec3DInPanel 0 0 400 400 (surf <> zAspect3D za)+                      , Graphics.Hgg.Render.MoveTo (Graphics.Hgg.Render.Point _ y) <- segs ]+    it "zAspect 1 と 2 で surface の投影 y が変わる (= z スケール反映)" $+      (ysOf 1 /= ysOf 2) `shouldBe` True+    it "zAspect3D 既定 (未指定) = aspect 1 と同一出力" $+      renderSpec3DInPanel 0 0 400 400 surf+        `shouldBe` renderSpec3DInPanel 0 0 400 400 (surf <> zAspect3D 1)++  describe "Phase 24 A8: 層間 depth 統合" $ do+    let cam   = defaultCameraZUp 4+        -- z が -1..1 に傾いた平面 (= 面の depth に幅がある)+        tilt  = [ [ fromIntegral (i + j) / 2 - 1 | i <- [0 .. 2 :: Int] ]+                | j <- [0 .. 2 :: Int] ]+        spec  = layer3D (surface3DGrid tilt <> xRange3D (-1, 1) <> yRange3D (-1, 1))+             <> layer3D (scatter3DPoints [Point3 0 0 0])   -- 中央 (= 中間 depth)+             <> camera cam+        prims = renderSpec3DInPanel 0 0 400 400 spec+        tagged = zip [0 :: Int ..] prims+        circleIdx = [ i | (i, Graphics.Hgg.Render.PCircle{}) <- tagged ]+        pathIdx   = [ i | (i, Graphics.Hgg.Render.PPath{})   <- tagged ]+    it "surface 面と scatter 点が depth で interleave する (点が全部最前面でない)" $+      let ci = head circleIdx+      in ( not (null circleIdx)+         , any (< ci) pathIdx    -- 円より奥の面が前にある+         , any (> ci) pathIdx )  -- 円より手前の面が後にある+        `shouldBe` (True, True, True)++  describe "Phase 24 A8: 3D PDF/PNG glue" $ do+    let spec = layer3D (surface3DGrid [[0,0.5],[0.5,1]] <> colormap3D+                          <> xRange3D (-1,1) <> yRange3D (-1,1))+                 <> camera (defaultCameraZUp 4) <> width3DV 300 <> height3DV 300+                 <> title3D "test"+        nonEmpty path = do exists <- doesFileExist path+                           sz <- if exists then getFileSize path else pure 0+                           when exists (removeFile path)+                           pure (sz > 0)+    it "savePDF3D: 非空 PDF を書き出す" $ do+      savePDF3D "/tmp/hgg-3d-test.pdf" spec+      nonEmpty "/tmp/hgg-3d-test.pdf" `shouldReturn` True+    it "savePNG3D: 非空 PNG を書き出す" $ do+      savePNG3D "/tmp/hgg-3d-test.png" spec+      nonEmpty "/tmp/hgg-3d-test.png" `shouldReturn` True++  describe "Phase 25 A2: 群色分け (colorBy3D) + 離散凡例" $ do+    let hue = ggplotHue 2+        df = [ ("x", NumData (V.fromList [1, 2, 3]))+             , ("y", NumData (V.fromList [4, 5, 6]))+             , ("z", NumData (V.fromList [7, 8, 9]))+             , ("group", TxtData (V.fromList ["A", "A", "B"])) ] :: [(T.Text, ColData)]+        spec = layer3D (scatter3D "x" "y" "z" <> colorBy3D "group")+        (_, resolved) = unBound3D (df |>> spec)+        l = head (vs3Layers resolved)+    it "カテゴリ列 → 点ごと色 (ggplotHue・初出順 A,A,B)" $+      getLast (lyr3PtColors l) `shouldBe` Just [hue !! 0, hue !! 0, hue !! 1]+    it "凡例 = (カテゴリ, 色) を初出順で" $+      getLast (lyr3Legend l) `shouldBe` Just [("A", hue !! 0), ("B", hue !! 1)]+    it "renderSpec3DInPanel: 凡例チップ (PRect) が出て scatter 点色が 2 種" $+      let prims = renderSpec3DInPanel 0 0 400 400 (resolved <> camera (defaultCameraZUp 4))+          rects = [ () | Graphics.Hgg.Render.PRect{} <- prims ]+          cols  = nub [ c | Graphics.Hgg.Render.PCircle _ _ (Graphics.Hgg.Render.FillStyle c _) _ _+                              <- prims ]+      in (length rects >= 2, length cols) `shouldBe` (True, 2)+    it "sc3Colors: layerToScatter が per-point 色を渡す" $+      sc3Colors (layerToScatter l) `shouldBe` Just [hue !! 0, hue !! 0, hue !! 1]++  describe "Phase 25 A3: 連続色マップ (colorContinuousBy3D) + colorbar" $ do+    let df = [ ("x", NumData (V.fromList [1, 2, 3]))+             , ("y", NumData (V.fromList [4, 5, 6]))+             , ("z", NumData (V.fromList [7, 8, 9]))+             , ("v", NumData (V.fromList [0, 5, 10])) ] :: [(T.Text, ColData)]+        spec = layer3D (scatter3D "x" "y" "z" <> colorContinuousBy3D "v")+        (_, resolved) = unBound3D (df |>> spec)+        l = head (vs3Layers resolved)+    it "数値列 → 点ごと連続色 (viridis・min/mid/max)" $+      getLast (lyr3PtColors l)+        `shouldBe` Just [ continuousColor viridisStops3D 0+                        , continuousColor viridisStops3D 0.5+                        , continuousColor viridisStops3D 1 ]+    it "colorbar 情報 = (viridis stops, min, max)" $+      getLast (lyr3Colorbar l) `shouldBe` Just (viridisStops3D, 0, 10)+    it "sc3Colors: layerToScatter が per-point 連続色を渡す" $+      sc3Colors (layerToScatter l)+        `shouldBe` Just [ continuousColor viridisStops3D 0+                        , continuousColor viridisStops3D 0.5+                        , continuousColor viridisStops3D 1 ]+    it "renderSpec3DInPanel: colorbar strip (PRect) が出る" $+      let prims = renderSpec3DInPanel 0 0 400 400 (resolved <> camera (defaultCameraZUp 4))+          rects = [ () | Graphics.Hgg.Render.PRect{} <- prims ]+      in length rects >= 10 `shouldBe` True++  describe "Phase 30 A6: surface 案C (列駆動 surface3D・long → grid pivot)" $ do+    -- 2x2 規則格子を long 形で: (x,y,z) = (0,0,10)(1,0,20)(0,1,30)(1,1,40)+    let df = [ ("x", NumData (V.fromList [0, 1, 0, 1]))+             , ("y", NumData (V.fromList [0, 0, 1, 1]))+             , ("z", NumData (V.fromList [10, 20, 30, 40])) ] :: [(T.Text, ColData)]+        spec = layer3D (surface3D "x" "y" "z")+        (_, resolved) = unBound3D (df |>> spec)+        l = head (vs3Layers resolved)+    it "long (x,y,z) を pivot して z 行列に畳む (列=x昇順・行=y昇順)" $+      getLast (lyr3Grid l) `shouldBe` Just [[10, 20], [30, 40]]+    it "xRange/yRange = データの min/max" $+      (getLast (lyr3XRange l), getLast (lyr3YRange l))+        `shouldBe` (Just (0, 1), Just (0, 1))+    it "surface3DGrid (行列直入れ・旧 API) は lyr3Grid に直入れ (後方互換)" $+      getLast (lyr3Grid (surface3DGrid [[10,20],[30,40]]))+        `shouldBe` Just [[10, 20], [30, 40]]++  describe "Phase 30 A6: 列駆動 text3D (x/y/z/label列 → lyr3Points + lyr3Labels)" $ do+    let df = [ ("x", NumData (V.fromList [0, 1]))+             , ("y", NumData (V.fromList [0, 1]))+             , ("z", NumData (V.fromList [0, 1]))+             , ("name", TxtData (V.fromList ["a", "b"])) ] :: [(T.Text, ColData)]+        spec = layer3D (text3D "x" "y" "z" "name")+        (_, resolved) = unBound3D (df |>> spec)+        l = head (vs3Layers resolved)+    it "x/y/z → lyr3Points" $+      getLast (lyr3Points l) `shouldBe` Just [Point3 0 0 0, Point3 1 1 1]+    it "label 列 → lyr3Labels" $+      getLast (lyr3Labels l) `shouldBe` Just ["a", "b"]++  describe "Phase 25 A3: size マップ (sizeBy3D / sizeRange3D)" $ do+    let df = [ ("x", NumData (V.fromList [1, 2, 3]))+             , ("y", NumData (V.fromList [4, 5, 6]))+             , ("z", NumData (V.fromList [7, 8, 9]))+             , ("m", NumData (V.fromList [0, 5, 10])) ] :: [(T.Text, ColData)]+    it "数値列 → 点ごと size (既定範囲 (4,18)・線形)" $+      let spec = layer3D (scatter3D "x" "y" "z" <> sizeBy3D "m")+          (_, resolved) = unBound3D (df |>> spec)+          l = head (vs3Layers resolved)+      in getLast (lyr3PtSizes l) `shouldBe` Just [4, 11, 18]+    it "sizeRange3D で px 範囲を明示指定" $+      let spec = layer3D (scatter3D "x" "y" "z" <> sizeBy3D "m" <> sizeRange3D (2, 22))+          (_, resolved) = unBound3D (df |>> spec)+          l = head (vs3Layers resolved)+      in getLast (lyr3PtSizes l) `shouldBe` Just [2, 12, 22]+    it "sc3Sizes: layerToScatter が per-point size を渡す" $+      let spec = layer3D (scatter3D "x" "y" "z" <> sizeBy3D "m")+          (_, resolved) = unBound3D (df |>> spec)+          l = head (vs3Layers resolved)+      in sc3Sizes (layerToScatter l) `shouldBe` Just [4, 11, 18]++  describe "Phase 24 A6: 3D 列名バインド (ColRef + |>>)" $ do+    let df = [ ("x", NumData (V.fromList [1, 2, 3]))+             , ("y", NumData (V.fromList [4, 5, 6]))+             , ("z", NumData (V.fromList [7, 8, 9])) ] :: [(T.Text, ColData)]+        spec3 = layer3D (scatter3D "x" "y" "z")+    it "df |>> spec3d → BoundPlot3D・unBound3D で列が [Point3] に解決される" $ do+      let b = df |>> spec3+          (_, resolved) = unBound3D b+      bp3Diagnostics b `shouldBe` []+      getLast (lyr3Points (head (vs3Layers resolved)))+        `shouldBe` Just [Point3 1 4 7, Point3 2 5 8, Point3 3 6 9]+    it "存在しない列は ColumnNotFound (編集距離 suggestion 付き)" $ do+      let b = df |>> layer3D (scatter3D "x" "y" "zz")+      length (bp3Diagnostics b) `shouldBe` 1+      case head (bp3Diagnostics b) of+        Graphics.Hgg.Validate.PlotError+          (Graphics.Hgg.Validate.ColumnNotFound nm sugg) _ -> do+            nm `shouldBe` "zz"+            head sugg `shouldBe` "z"   -- 最近傍が先頭 (他候補も距離 2 以内で続く)+        d -> expectationFailure ("ColumnNotFound でない: " <> show d)+    it "inline 生値は resolver 不要で解決 (2D と同じ書き味)" $ do+      let l  = scatter3D (inline [1, 2 :: Double]) (inline [3, 4 :: Double])+                         (inline [5, 6 :: Double])+          l' = resolveLayer3D emptyResolver l+      getLast (lyr3Points l') `shouldBe` Just [Point3 1 3 5, Point3 2 4 6]+    it "scatter3DPoints (旧直入れ) は素通し (後方互換)" $ do+      let l  = scatter3DPoints [Point3 9 9 9]+          l' = resolveLayer3D emptyResolver l+      getLast (lyr3Points l') `shouldBe` Just [Point3 9 9 9]