diff --git a/.gitignore b/.gitignore index 0dc3382..35df3c0 100644 --- a/.gitignore +++ b/.gitignore @@ -14,3 +14,10 @@ report.html .clj-kondo /target src/app/server/env.clj +docs/ +CLAUDE.md +AGENTS.md +GEMINI.md +.agents/ +.gemini/ +.claude/ diff --git a/components/_extractor.js b/components/_extractor.js new file mode 100644 index 0000000..1038ce8 --- /dev/null +++ b/components/_extractor.js @@ -0,0 +1,146 @@ +/** + * Browser DOM extractor — runs via Claude-in-Chrome javascript_tool. + * Walks a DOM subtree, captures computed styles + bounds, returns a JSON tree + * matching the Design IR shape for ingestion by _css_parsers and _compiler. + * + * Usage: extractComponent('button.my-class') or extractComponent('#card-id') + */ +/** + * Auto-detect the most likely "main content" container. + * Tries semantic landmarks first, then falls back to the largest body child. + */ +function autoDetectSelector() { + var candidates = ['main', 'article', '[role="main"]', '.main-content', '#content']; + for (var i = 0; i < candidates.length; i++) { + var el = document.querySelector(candidates[i]); + if (el && el.children.length > 0 && el.getBoundingClientRect().height > 50) return candidates[i]; + } + // Largest body child, excluding nav/header/footer + var skip = {NAV:1, HEADER:1, FOOTER:1, SCRIPT:1, STYLE:1, NOSCRIPT:1}; + var best = null, bestArea = 0; + for (var j = 0; j < document.body.children.length; j++) { + var child = document.body.children[j]; + if (skip[child.tagName]) continue; + var r = child.getBoundingClientRect(); + if (r.width * r.height > bestArea) { bestArea = r.width * r.height; best = child; } + } + if (best) { + if (best.id) return '#' + best.id; + return 'body > :nth-child(' + (Array.from(document.body.children).indexOf(best) + 1) + ')'; + } + return 'body'; +} + +function extractComponent(selector) { + if (!selector) selector = autoDetectSelector(); + const SKIP_TAGS = new Set(['SCRIPT', 'STYLE', 'META', 'LINK', 'NOSCRIPT', 'BR', 'HR']); + + const STYLE_PROPS = [ + 'backgroundColor', 'backgroundImage', + 'borderTopWidth', 'borderRightWidth', 'borderBottomWidth', 'borderLeftWidth', + 'borderTopColor', 'borderRightColor', 'borderBottomColor', 'borderLeftColor', + 'borderTopLeftRadius', 'borderTopRightRadius', 'borderBottomRightRadius', 'borderBottomLeftRadius', + 'boxShadow', + 'fontSize', 'fontWeight', 'fontFamily', 'color', + 'display', 'flexDirection', 'alignItems', 'justifyContent', 'gap', + 'paddingTop', 'paddingRight', 'paddingBottom', 'paddingLeft', + 'overflow', 'opacity', 'visibility' + ]; + + const root = document.querySelector(selector); + if (!root) return JSON.stringify({ error: 'Element not found: ' + selector }); + + const rootRect = root.getBoundingClientRect(); + + function walkNode(el) { + if (SKIP_TAGS.has(el.tagName)) return null; + + const cs = getComputedStyle(el); + if (cs.display === 'none' || cs.visibility === 'hidden') return null; + + const rect = el.getBoundingClientRect(); + + // Capture style properties + const styles = {}; + for (const prop of STYLE_PROPS) { + styles[prop] = cs[prop]; + } + + // Relative bounds (to root element) + const bounds = { + x: Math.round(rect.left - rootRect.left), + y: Math.round(rect.top - rootRect.top), + w: Math.round(rect.width), + h: Math.round(rect.height) + }; + + // Gather children + const children = []; + for (const child of el.children) { + const node = walkNode(child); + if (node) children.push(node); + } + + // Text content (only on leaf elements with no element children) + let textContent = null; + if (children.length === 0) { + const text = el.textContent?.trim(); + if (text && text.length > 0) textContent = text; + } + + return { + tag: el.tagName.toLowerCase(), + bounds, + styles, + textContent, + children: children.length > 0 ? children : undefined + }; + } + + const tree = walkNode(root); + + // Post-pass: collapse pure wrapper divs + function collapseWrappers(node) { + if (!node) return node; + if (node.children) { + node.children = node.children.map(collapseWrappers); + } + + // Collapse if: single child, div/span, no visual styling + if (node.children && node.children.length === 1 && !node.textContent) { + const s = node.styles; + const isTransparent = !s.backgroundColor || + s.backgroundColor === 'rgba(0, 0, 0, 0)' || + s.backgroundColor === 'transparent'; + const noBorder = (!s.borderTopWidth || parseFloat(s.borderTopWidth) === 0) && + (!s.borderRightWidth || parseFloat(s.borderRightWidth) === 0); + const noShadow = !s.boxShadow || s.boxShadow === 'none'; + const noBackground = !s.backgroundImage || s.backgroundImage === 'none'; + const isWrapper = (node.tag === 'div' || node.tag === 'span'); + + if (isWrapper && isTransparent && noBorder && noShadow && noBackground) { + // Promote the single child, preserving the wrapper's bounds if child has none + const child = node.children[0]; + if (!child.bounds || (child.bounds.w === 0 && child.bounds.h === 0)) { + child.bounds = node.bounds; + } + return child; + } + } + + return node; + } + + const collapsed = collapseWrappers(tree); + + // Capture source HTML for the preview overlay + const sourceHTML = root.outerHTML; + + return JSON.stringify({ + version: 1, + sourceUrl: window.location.href, + rootBounds: { w: Math.round(rootRect.width), h: Math.round(rootRect.height) }, + tree: collapsed, + sourceHTML: sourceHTML + }, null, 2); +} diff --git a/components/_registry.edn b/components/_registry.edn new file mode 100644 index 0000000..c553e2b --- /dev/null +++ b/components/_registry.edn @@ -0,0 +1,72 @@ +;; Component Registry — shadcn v4 +;; Auto-generated 2026-03-01 from ui.shadcn.com/docs/components +;; 59 components, 54 with direct source files + +{:libraries [{:id :shadcn-v4 + :name "shadcn/ui v4" + :repo "shadcn-ui/ui" + :branch "main" + :base-path "apps/v4/registry/bases/base/ui" + :docs-base "https://ui.shadcn.com/docs/components/radix" + :token-map "shadcn-v4.edn"}] + + :components + [{:slug "accordion" :name "Accordion" :has-source? true :category :disclosure} + {:slug "alert" :name "Alert" :has-source? true :category :feedback} + {:slug "alert-dialog" :name "Alert Dialog" :has-source? true :category :overlay} + {:slug "aspect-ratio" :name "Aspect Ratio" :has-source? true :category :layout} + {:slug "avatar" :name "Avatar" :has-source? true :category :data-display} + {:slug "badge" :name "Badge" :has-source? true :category :data-display} + {:slug "breadcrumb" :name "Breadcrumb" :has-source? true :category :navigation} + {:slug "button" :name "Button" :has-source? true :category :action} + {:slug "button-group" :name "Button Group" :has-source? true :category :action} + {:slug "calendar" :name "Calendar" :has-source? true :category :date-time} + {:slug "card" :name "Card" :has-source? true :category :data-display} + {:slug "carousel" :name "Carousel" :has-source? true :category :data-display} + {:slug "chart" :name "Chart" :has-source? true :category :data-display} + {:slug "checkbox" :name "Checkbox" :has-source? true :category :input} + {:slug "collapsible" :name "Collapsible" :has-source? true :category :disclosure} + {:slug "combobox" :name "Combobox" :has-source? true :category :input} + {:slug "command" :name "Command" :has-source? true :category :input} + {:slug "context-menu" :name "Context Menu" :has-source? true :category :overlay} + {:slug "data-table" :name "Data Table" :has-source? false :category :data-display} + {:slug "date-picker" :name "Date Picker" :has-source? false :category :date-time} + {:slug "dialog" :name "Dialog" :has-source? true :category :overlay} + {:slug "direction" :name "Direction" :has-source? true :category :utility} + {:slug "drawer" :name "Drawer" :has-source? true :category :overlay} + {:slug "dropdown-menu" :name "Dropdown Menu" :has-source? true :category :overlay} + {:slug "empty" :name "Empty" :has-source? true :category :feedback} + {:slug "field" :name "Field" :has-source? true :category :input} + {:slug "hover-card" :name "Hover Card" :has-source? true :category :overlay} + {:slug "input" :name "Input" :has-source? true :category :input} + {:slug "input-group" :name "Input Group" :has-source? true :category :input} + {:slug "input-otp" :name "Input OTP" :has-source? true :category :input} + {:slug "item" :name "Item" :has-source? true :category :data-display} + {:slug "kbd" :name "Kbd" :has-source? true :category :data-display} + {:slug "label" :name "Label" :has-source? true :category :input} + {:slug "menubar" :name "Menubar" :has-source? true :category :navigation} + {:slug "native-select" :name "Native Select" :has-source? true :category :input} + {:slug "navigation-menu" :name "Navigation Menu" :has-source? true :category :navigation} + {:slug "pagination" :name "Pagination" :has-source? true :category :navigation} + {:slug "popover" :name "Popover" :has-source? true :category :overlay} + {:slug "progress" :name "Progress" :has-source? true :category :feedback} + {:slug "radio-group" :name "Radio Group" :has-source? true :category :input} + {:slug "resizable" :name "Resizable" :has-source? true :category :layout} + {:slug "scroll-area" :name "Scroll Area" :has-source? true :category :layout} + {:slug "select" :name "Select" :has-source? true :category :input} + {:slug "separator" :name "Separator" :has-source? true :category :layout} + {:slug "sheet" :name "Sheet" :has-source? true :category :overlay} + {:slug "sidebar" :name "Sidebar" :has-source? true :category :navigation} + {:slug "skeleton" :name "Skeleton" :has-source? true :category :feedback} + {:slug "slider" :name "Slider" :has-source? true :category :input} + {:slug "sonner" :name "Sonner" :has-source? true :category :feedback} + {:slug "spinner" :name "Spinner" :has-source? true :category :feedback} + {:slug "switch" :name "Switch" :has-source? true :category :input} + {:slug "table" :name "Table" :has-source? true :category :data-display} + {:slug "tabs" :name "Tabs" :has-source? true :category :navigation} + {:slug "textarea" :name "Textarea" :has-source? true :category :input} + {:slug "toast" :name "Toast" :has-source? false :category :feedback} + {:slug "toggle" :name "Toggle" :has-source? true :category :action} + {:slug "toggle-group" :name "Toggle Group" :has-source? true :category :action} + {:slug "tooltip" :name "Tooltip" :has-source? true :category :overlay} + {:slug "typography" :name "Typography" :has-source? false :category :data-display}]} diff --git a/components/_token_maps/shadcn-v4.edn b/components/_token_maps/shadcn-v4.edn new file mode 100644 index 0000000..a41ade5 --- /dev/null +++ b/components/_token_maps/shadcn-v4.edn @@ -0,0 +1,56 @@ +;; shadcn v4 token map — extracted 2026-03-01 from live ui.shadcn.com +;; Colors converted from lab()/oklab() -> RGBA via canvas getImageData +;; Dark theme (default) + +{:library "shadcn" + :version "v4" + :source "https://ui.shadcn.com" + :extracted "2026-03-01" + + :colors + {:primary [229 229 229 1.0] + :primary-foreground [23 23 23 1.0] + :secondary [38 38 38 1.0] + :secondary-foreground [250 250 250 1.0] + :destructive [255 100 103 1.0] + :destructive-foreground [250 250 250 1.0] + :foreground [250 250 250 1.0] + :background [10 10 10 1.0] + :muted [38 38 38 1.0] + :muted-foreground [163 163 163 1.0] + :accent [64 64 64 1.0] + :accent-foreground [250 250 250 1.0] + :border [255 255 255 0.1] + :ring [212 212 212 1.0] + :input [255 255 255 0.15] + :card [10 10 10 1.0] + :card-foreground [250 250 250 1.0] + :popover [10 10 10 1.0] + :popover-foreground [250 250 250 1.0]} + + :spacing + {:xs 4 :sm 8 :md 12 :lg 16 :xl 24 :2xl 32} + + :radii + {:none 0 :sm 6 :md 8 :lg 10 :xl 12 :full 9999} + + :radius 10 + + :font + {:family "Geist Sans" + :mono "Geist Mono" + :sizes {:xs 12 :sm 14 :md 16 :lg 18 :xl 20 :2xl 24} + :weights {:normal 400 :medium 500 :semibold 600 :bold 700}} + + :shadows + {:sm {:blur 4 :spread 0 :offset-y 1 :color [0 0 0 0.05]} + :md {:blur 8 :spread 0 :offset-y 4 :color [0 0 0 0.1]} + :lg {:blur 16 :spread 0 :offset-y 8 :color [0 0 0 0.15]}} + + :hover-rules + {:default [:alpha 0.8] + :secondary [:alpha 0.8] + :destructive [:alpha-shift 0.1] + :outline [:swap {:bg :muted :fg :foreground}] + :ghost [:swap {:bg :muted :fg :foreground}] + :link [:underline true]}} diff --git a/components/accordion/_source.edn b/components/accordion/_source.edn new file mode 100644 index 0000000..568d3de --- /dev/null +++ b/components/accordion/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/accordion.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/accordion" + :converted nil}} diff --git a/components/alert-dialog/_source.edn b/components/alert-dialog/_source.edn new file mode 100644 index 0000000..9b461e9 --- /dev/null +++ b/components/alert-dialog/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/alert-dialog.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/alert-dialog" + :converted nil}} diff --git a/components/alert/_source.edn b/components/alert/_source.edn new file mode 100644 index 0000000..1d9988b --- /dev/null +++ b/components/alert/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/alert.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/alert" + :converted nil}} diff --git a/components/aspect-ratio/_source.edn b/components/aspect-ratio/_source.edn new file mode 100644 index 0000000..efb6bb7 --- /dev/null +++ b/components/aspect-ratio/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/aspect-ratio.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/aspect-ratio" + :converted nil}} diff --git a/components/avatar/_source.edn b/components/avatar/_source.edn new file mode 100644 index 0000000..7db2b51 --- /dev/null +++ b/components/avatar/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/avatar.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/avatar" + :converted nil}} diff --git a/components/badge/_source.edn b/components/badge/_source.edn new file mode 100644 index 0000000..7a7825b --- /dev/null +++ b/components/badge/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/badge.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/badge" + :converted nil}} diff --git a/components/breadcrumb/_source.edn b/components/breadcrumb/_source.edn new file mode 100644 index 0000000..78fa1c3 --- /dev/null +++ b/components/breadcrumb/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/breadcrumb.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/breadcrumb" + :converted nil}} diff --git a/components/button-group/_source.edn b/components/button-group/_source.edn new file mode 100644 index 0000000..182a91f --- /dev/null +++ b/components/button-group/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/button-group.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/button-group" + :converted nil}} diff --git a/components/button/_source.edn b/components/button/_source.edn new file mode 100644 index 0000000..2565f04 --- /dev/null +++ b/components/button/_source.edn @@ -0,0 +1 @@ +{:shadcn-v4 {:status :ready, :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/button.tsx", :docs "https://ui.shadcn.com/docs/components/radix/button", :converted "2026-03-01"}} \ No newline at end of file diff --git a/components/calendar/_source.edn b/components/calendar/_source.edn new file mode 100644 index 0000000..d4a25c2 --- /dev/null +++ b/components/calendar/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/calendar.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/calendar" + :converted nil}} diff --git a/components/card/_source.edn b/components/card/_source.edn new file mode 100644 index 0000000..500c638 --- /dev/null +++ b/components/card/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/card.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/card" + :converted nil}} diff --git a/components/carousel/_source.edn b/components/carousel/_source.edn new file mode 100644 index 0000000..86b3067 --- /dev/null +++ b/components/carousel/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/carousel.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/carousel" + :converted nil}} diff --git a/components/chart/_source.edn b/components/chart/_source.edn new file mode 100644 index 0000000..70cd2bb --- /dev/null +++ b/components/chart/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/chart.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/chart" + :converted nil}} diff --git a/components/checkbox/_source.edn b/components/checkbox/_source.edn new file mode 100644 index 0000000..c0b8d45 --- /dev/null +++ b/components/checkbox/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/checkbox.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/checkbox" + :converted nil}} diff --git a/components/collapsible/_source.edn b/components/collapsible/_source.edn new file mode 100644 index 0000000..e752962 --- /dev/null +++ b/components/collapsible/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/collapsible.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/collapsible" + :converted nil}} diff --git a/components/combobox/_source.edn b/components/combobox/_source.edn new file mode 100644 index 0000000..7e182f9 --- /dev/null +++ b/components/combobox/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/combobox.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/combobox" + :converted nil}} diff --git a/components/command/_source.edn b/components/command/_source.edn new file mode 100644 index 0000000..7ce9359 --- /dev/null +++ b/components/command/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/command.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/command" + :converted nil}} diff --git a/components/context-menu/_source.edn b/components/context-menu/_source.edn new file mode 100644 index 0000000..8c27997 --- /dev/null +++ b/components/context-menu/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/context-menu.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/context-menu" + :converted nil}} diff --git a/components/data-table/_source.edn b/components/data-table/_source.edn new file mode 100644 index 0000000..a5f0821 --- /dev/null +++ b/components/data-table/_source.edn @@ -0,0 +1,6 @@ +{:shadcn-v4 + {:status :stub + :source nil + :docs "https://ui.shadcn.com/docs/components/radix/data-table" + :note "Composite component - no single source file" + :converted nil}} diff --git a/components/date-picker/_source.edn b/components/date-picker/_source.edn new file mode 100644 index 0000000..ee2d888 --- /dev/null +++ b/components/date-picker/_source.edn @@ -0,0 +1,6 @@ +{:shadcn-v4 + {:status :stub + :source nil + :docs "https://ui.shadcn.com/docs/components/radix/date-picker" + :note "Composite component - no single source file" + :converted nil}} diff --git a/components/dialog/_source.edn b/components/dialog/_source.edn new file mode 100644 index 0000000..564eb38 --- /dev/null +++ b/components/dialog/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/dialog.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/dialog" + :converted nil}} diff --git a/components/direction/_source.edn b/components/direction/_source.edn new file mode 100644 index 0000000..649ca09 --- /dev/null +++ b/components/direction/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/direction.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/direction" + :converted nil}} diff --git a/components/drawer/_source.edn b/components/drawer/_source.edn new file mode 100644 index 0000000..4752765 --- /dev/null +++ b/components/drawer/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/drawer.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/drawer" + :converted nil}} diff --git a/components/dropdown-menu/_source.edn b/components/dropdown-menu/_source.edn new file mode 100644 index 0000000..45fc141 --- /dev/null +++ b/components/dropdown-menu/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/dropdown-menu.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/dropdown-menu" + :converted nil}} diff --git a/components/empty/_source.edn b/components/empty/_source.edn new file mode 100644 index 0000000..2618c8f --- /dev/null +++ b/components/empty/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/empty.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/empty" + :converted nil}} diff --git a/components/field/_source.edn b/components/field/_source.edn new file mode 100644 index 0000000..da5d574 --- /dev/null +++ b/components/field/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/field.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/field" + :converted nil}} diff --git a/components/hover-card/_source.edn b/components/hover-card/_source.edn new file mode 100644 index 0000000..ba6f2d9 --- /dev/null +++ b/components/hover-card/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/hover-card.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/hover-card" + :converted nil}} diff --git a/components/input-group/_source.edn b/components/input-group/_source.edn new file mode 100644 index 0000000..478fff4 --- /dev/null +++ b/components/input-group/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/input-group.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/input-group" + :converted nil}} diff --git a/components/input-otp/_source.edn b/components/input-otp/_source.edn new file mode 100644 index 0000000..755576a --- /dev/null +++ b/components/input-otp/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/input-otp.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/input-otp" + :converted nil}} diff --git a/components/input/_source.edn b/components/input/_source.edn new file mode 100644 index 0000000..c9ca8ab --- /dev/null +++ b/components/input/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/input.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/input" + :converted nil}} diff --git a/components/item/_source.edn b/components/item/_source.edn new file mode 100644 index 0000000..352d0be --- /dev/null +++ b/components/item/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/item.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/item" + :converted nil}} diff --git a/components/kbd/_source.edn b/components/kbd/_source.edn new file mode 100644 index 0000000..057f67f --- /dev/null +++ b/components/kbd/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/kbd.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/kbd" + :converted nil}} diff --git a/components/label/_source.edn b/components/label/_source.edn new file mode 100644 index 0000000..be5830d --- /dev/null +++ b/components/label/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/label.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/label" + :converted nil}} diff --git a/components/menubar/_source.edn b/components/menubar/_source.edn new file mode 100644 index 0000000..e201977 --- /dev/null +++ b/components/menubar/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/menubar.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/menubar" + :converted nil}} diff --git a/components/native-select/_source.edn b/components/native-select/_source.edn new file mode 100644 index 0000000..4b9d7f3 --- /dev/null +++ b/components/native-select/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/native-select.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/native-select" + :converted nil}} diff --git a/components/navigation-menu/_source.edn b/components/navigation-menu/_source.edn new file mode 100644 index 0000000..fc1bcd1 --- /dev/null +++ b/components/navigation-menu/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/navigation-menu.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/navigation-menu" + :converted nil}} diff --git a/components/pagination/_source.edn b/components/pagination/_source.edn new file mode 100644 index 0000000..6229c27 --- /dev/null +++ b/components/pagination/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/pagination.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/pagination" + :converted nil}} diff --git a/components/popover/_source.edn b/components/popover/_source.edn new file mode 100644 index 0000000..8a36168 --- /dev/null +++ b/components/popover/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/popover.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/popover" + :converted nil}} diff --git a/components/progress/_source.edn b/components/progress/_source.edn new file mode 100644 index 0000000..9ae6f33 --- /dev/null +++ b/components/progress/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/progress.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/progress" + :converted nil}} diff --git a/components/radio-group/_source.edn b/components/radio-group/_source.edn new file mode 100644 index 0000000..306a7ac --- /dev/null +++ b/components/radio-group/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/radio-group.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/radio-group" + :converted nil}} diff --git a/components/resizable/_source.edn b/components/resizable/_source.edn new file mode 100644 index 0000000..47c3a68 --- /dev/null +++ b/components/resizable/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/resizable.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/resizable" + :converted nil}} diff --git a/components/scroll-area/_source.edn b/components/scroll-area/_source.edn new file mode 100644 index 0000000..abddb3b --- /dev/null +++ b/components/scroll-area/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/scroll-area.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/scroll-area" + :converted nil}} diff --git a/components/select/_source.edn b/components/select/_source.edn new file mode 100644 index 0000000..9c0dd64 --- /dev/null +++ b/components/select/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/select.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/select" + :converted nil}} diff --git a/components/separator/_source.edn b/components/separator/_source.edn new file mode 100644 index 0000000..f031284 --- /dev/null +++ b/components/separator/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/separator.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/separator" + :converted nil}} diff --git a/components/sheet/_source.edn b/components/sheet/_source.edn new file mode 100644 index 0000000..e1a3201 --- /dev/null +++ b/components/sheet/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/sheet.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/sheet" + :converted nil}} diff --git a/components/sidebar/_source.edn b/components/sidebar/_source.edn new file mode 100644 index 0000000..fa56461 --- /dev/null +++ b/components/sidebar/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/sidebar.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/sidebar" + :converted nil}} diff --git a/components/skeleton/_source.edn b/components/skeleton/_source.edn new file mode 100644 index 0000000..6ecb867 --- /dev/null +++ b/components/skeleton/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/skeleton.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/skeleton" + :converted nil}} diff --git a/components/slider/_source.edn b/components/slider/_source.edn new file mode 100644 index 0000000..25a883b --- /dev/null +++ b/components/slider/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/slider.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/slider" + :converted nil}} diff --git a/components/sonner/_source.edn b/components/sonner/_source.edn new file mode 100644 index 0000000..e9f02b0 --- /dev/null +++ b/components/sonner/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/sonner.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/sonner" + :converted nil}} diff --git a/components/spinner/_source.edn b/components/spinner/_source.edn new file mode 100644 index 0000000..f80a340 --- /dev/null +++ b/components/spinner/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/spinner.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/spinner" + :converted nil}} diff --git a/components/switch/_source.edn b/components/switch/_source.edn new file mode 100644 index 0000000..dd91b4e --- /dev/null +++ b/components/switch/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/switch.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/switch" + :converted nil}} diff --git a/components/table/_source.edn b/components/table/_source.edn new file mode 100644 index 0000000..51f5d5c --- /dev/null +++ b/components/table/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/table.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/table" + :converted nil}} diff --git a/components/tabs/_source.edn b/components/tabs/_source.edn new file mode 100644 index 0000000..e6700bd --- /dev/null +++ b/components/tabs/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/tabs.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/tabs" + :converted nil}} diff --git a/components/textarea/_source.edn b/components/textarea/_source.edn new file mode 100644 index 0000000..9083ffc --- /dev/null +++ b/components/textarea/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/textarea.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/textarea" + :converted nil}} diff --git a/components/toast/_source.edn b/components/toast/_source.edn new file mode 100644 index 0000000..7d11300 --- /dev/null +++ b/components/toast/_source.edn @@ -0,0 +1,6 @@ +{:shadcn-v4 + {:status :stub + :source nil + :docs "https://ui.shadcn.com/docs/components/radix/toast" + :note "Composite component - no single source file" + :converted nil}} diff --git a/components/toggle-group/_source.edn b/components/toggle-group/_source.edn new file mode 100644 index 0000000..d366152 --- /dev/null +++ b/components/toggle-group/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/toggle-group.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/toggle-group" + :converted nil}} diff --git a/components/toggle/_source.edn b/components/toggle/_source.edn new file mode 100644 index 0000000..e293f55 --- /dev/null +++ b/components/toggle/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/toggle.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/toggle" + :converted nil}} diff --git a/components/tooltip/_source.edn b/components/tooltip/_source.edn new file mode 100644 index 0000000..42b0fd8 --- /dev/null +++ b/components/tooltip/_source.edn @@ -0,0 +1,5 @@ +{:shadcn-v4 + {:status :stub + :source "https://github.com/shadcn-ui/ui/blob/main/apps/v4/registry/bases/base/ui/tooltip.tsx" + :docs "https://ui.shadcn.com/docs/components/radix/tooltip" + :converted nil}} diff --git a/components/typography/_source.edn b/components/typography/_source.edn new file mode 100644 index 0000000..2096543 --- /dev/null +++ b/components/typography/_source.edn @@ -0,0 +1,6 @@ +{:shadcn-v4 + {:status :stub + :source nil + :docs "https://ui.shadcn.com/docs/components/radix/typography" + :note "Composite component - no single source file" + :converted nil}} diff --git a/deps.edn b/deps.edn index 0aa32a0..8df1997 100644 --- a/deps.edn +++ b/deps.edn @@ -4,20 +4,18 @@ com.hyperfiddle/rcf {:mvn/version "20220926-202227"} ring/ring {:mvn/version "1.11.0"} ; comes with Jetty - ;; Electric IC version requires 1.12 but rama is fixed on 1.11.1 so I have included the newer changes - ;; by importing data.xml above according to this comment - ;; https://clojurians.slack.com/archives/C7Q9GSHFV/p1704475238085959?thread_ts=1704210542.998019&cid=C7Q9GSHFV com.google.guava/guava {:mvn/version "33.1.0-jre"} org.clojure/data.xml {:mvn/version "0.2.0-alpha8"} org.clojure/data.json {:mvn/version "2.4.0"} - org.clojure/clojure {:mvn/version "1.11.1"} - org.clojure/clojurescript {:mvn/version "1.11.60"} + org.clojure/clojure {:mvn/version "1.12.4"} + org.clojure/clojurescript {:mvn/version "1.11.132"} org.clojure/tools.logging {:mvn/version "1.2.4"} ch.qos.logback/logback-classic {:mvn/version "1.2.11"} datascript/datascript {:mvn/version "1.5.2"} - com.rpl/rama {:mvn/version "0.17.0"} + org.babashka/sci {:mvn/version "0.8.43"} + com.rpl/rama {:mvn/version "1.6.0"} com.roamresearch/backend-sdk {:mvn/version "0.0.4"} - com.rpl/rama-helpers {:mvn/version "0.9.3"} + com.rpl/rama-helpers {:mvn/version "0.10.0"} com.rpl/specter {:mvn/version "1.1.4"} net.clojars.wkok/openai-clojure {:mvn/version "0.14.0"} image-resizer/image-resizer {:mvn/version "0.1.10"} @@ -33,7 +31,7 @@ :extra-deps {io.github.clojure/tools.build {:mvn/version "0.9.6" :exclusions [com.google.guava/guava ; Guava version conflict between tools.build and clojurescript. org.slf4j/slf4j-nop]} ; clashes with app logger} - thheller/shadow-cljs {:mvn/version "2.25.2"}}} + thheller/shadow-cljs {:mvn/version "2.28.23"}}} :nrepl {:extra-deps {cider/cider-nrepl {:mvn/version "0.42.1"} @@ -44,6 +42,8 @@ "--interactive" "--port" "9001"]} + :test {:extra-paths ["test"]} + :prod {:extra-paths ["src-prod"]} ; use `clj -X:build build-client`, NOT -T! build/app classpath contamination cannot be prevented @@ -52,4 +52,4 @@ :extra-deps {io.github.clojure/tools.build {:mvn/version "0.9.6" :exclusions [com.google.guava/guava ; Guava version conflict between tools.build and clojurescript. org.slf4j/slf4j-nop]} ; clashes with app logger} - thheller/shadow-cljs {:mvn/version "2.25.2"}}}}} + thheller/shadow-cljs {:mvn/version "2.28.23"}}}}} diff --git a/src/app/client/background.cljc b/old-infra/src/app/client/background.cljc similarity index 100% rename from src/app/client/background.cljc rename to old-infra/src/app/client/background.cljc diff --git a/src/app/client/core.cljc b/old-infra/src/app/client/core.cljc similarity index 100% rename from src/app/client/core.cljc rename to old-infra/src/app/client/core.cljc diff --git a/src/app/client/editor/core.cljc b/old-infra/src/app/client/editor/core.cljc similarity index 100% rename from src/app/client/editor/core.cljc rename to old-infra/src/app/client/editor/core.cljc diff --git a/src/app/client/editor/events/click.cljc b/old-infra/src/app/client/editor/events/click.cljc similarity index 100% rename from src/app/client/editor/events/click.cljc rename to old-infra/src/app/client/editor/events/click.cljc diff --git a/src/app/client/editor/events/keydown.cljc b/old-infra/src/app/client/editor/events/keydown.cljc similarity index 100% rename from src/app/client/editor/events/keydown.cljc rename to old-infra/src/app/client/editor/events/keydown.cljc diff --git a/src/app/client/editor/events/utils.cljc b/old-infra/src/app/client/editor/events/utils.cljc similarity index 100% rename from src/app/client/editor/events/utils.cljc rename to old-infra/src/app/client/editor/events/utils.cljc diff --git a/src/app/client/editor/parser.cljc b/old-infra/src/app/client/editor/parser.cljc similarity index 100% rename from src/app/client/editor/parser.cljc rename to old-infra/src/app/client/editor/parser.cljc diff --git a/src/app/client/electric_codemirror.cljc b/old-infra/src/app/client/electric_codemirror.cljc similarity index 100% rename from src/app/client/electric_codemirror.cljc rename to old-infra/src/app/client/electric_codemirror.cljc diff --git a/src/app/client/flow_calc.cljc b/old-infra/src/app/client/flow_calc.cljc similarity index 100% rename from src/app/client/flow_calc.cljc rename to old-infra/src/app/client/flow_calc.cljc diff --git a/src/app/client/learn_missionary.cljc b/old-infra/src/app/client/learn_missionary.cljc similarity index 100% rename from src/app/client/learn_missionary.cljc rename to old-infra/src/app/client/learn_missionary.cljc diff --git a/src/app/client/mode.cljc b/old-infra/src/app/client/mode.cljc similarity index 100% rename from src/app/client/mode.cljc rename to old-infra/src/app/client/mode.cljc diff --git a/src/app/client/playground/actions.cljc b/old-infra/src/app/client/playground/actions.cljc similarity index 100% rename from src/app/client/playground/actions.cljc rename to old-infra/src/app/client/playground/actions.cljc diff --git a/src/app/client/quad_tree.cljc b/old-infra/src/app/client/quad_tree.cljc similarity index 100% rename from src/app/client/quad_tree.cljc rename to old-infra/src/app/client/quad_tree.cljc diff --git a/src/app/client/shapes/circle.cljc b/old-infra/src/app/client/shapes/circle.cljc similarity index 100% rename from src/app/client/shapes/circle.cljc rename to old-infra/src/app/client/shapes/circle.cljc diff --git a/src/app/client/shapes/draw_rect.cljc b/old-infra/src/app/client/shapes/draw_rect.cljc similarity index 100% rename from src/app/client/shapes/draw_rect.cljc rename to old-infra/src/app/client/shapes/draw_rect.cljc diff --git a/src/app/client/shapes/line.cljc b/old-infra/src/app/client/shapes/line.cljc similarity index 100% rename from src/app/client/shapes/line.cljc rename to old-infra/src/app/client/shapes/line.cljc diff --git a/src/app/client/shapes/rect.cljc b/old-infra/src/app/client/shapes/rect.cljc similarity index 100% rename from src/app/client/shapes/rect.cljc rename to old-infra/src/app/client/shapes/rect.cljc diff --git a/src/app/client/shapes/util.cljc b/old-infra/src/app/client/shapes/util.cljc similarity index 100% rename from src/app/client/shapes/util.cljc rename to old-infra/src/app/client/shapes/util.cljc diff --git a/src/app/client/style_components/bottom_bar.cljc b/old-infra/src/app/client/style_components/bottom_bar.cljc similarity index 100% rename from src/app/client/style_components/bottom_bar.cljc rename to old-infra/src/app/client/style_components/bottom_bar.cljc diff --git a/src/app/client/style_components/buttons.cljc b/old-infra/src/app/client/style_components/buttons.cljc similarity index 100% rename from src/app/client/style_components/buttons.cljc rename to old-infra/src/app/client/style_components/buttons.cljc diff --git a/src/app/client/style_components/svg_icons.cljc b/old-infra/src/app/client/style_components/svg_icons.cljc similarity index 100% rename from src/app/client/style_components/svg_icons.cljc rename to old-infra/src/app/client/style_components/svg_icons.cljc diff --git a/src/app/client/utils.cljc b/old-infra/src/app/client/utils.cljc similarity index 100% rename from src/app/client/utils.cljc rename to old-infra/src/app/client/utils.cljc diff --git a/src/app/client/webgpu/bind.cljs b/old-infra/src/app/client/webgpu/bind.cljs similarity index 100% rename from src/app/client/webgpu/bind.cljs rename to old-infra/src/app/client/webgpu/bind.cljs diff --git a/src/app/client/webgpu/buffer.cljs b/old-infra/src/app/client/webgpu/buffer.cljs similarity index 100% rename from src/app/client/webgpu/buffer.cljs rename to old-infra/src/app/client/webgpu/buffer.cljs diff --git a/src/app/client/webgpu/compute.cljs b/old-infra/src/app/client/webgpu/compute.cljs similarity index 100% rename from src/app/client/webgpu/compute.cljs rename to old-infra/src/app/client/webgpu/compute.cljs index a01dba9..1ab7fe1 100644 --- a/src/app/client/webgpu/compute.cljs +++ b/old-infra/src/app/client/webgpu/compute.cljs @@ -8,6 +8,19 @@ :entryPoint "modifySquare"})}) +(def vertices-render-shader + (clj->js {:label "vertices render shader descriptor" + :code " + @vertex + fn renderVertices(@location(0) pos: vec2f) -> @builtin(position) vec4 { + return vec4f(pos, 0.0, 1.0); + } + + @fragment + fn renderVerticesFragment() -> @location(0) vec4f { + return vec4f(0.9, 0.9, 0.9, 1); + } + "})) (defn render-new-vertices [context new-vertices device fformat num-rectangles output-buffer] (js/console.log "RENDER NEW VERTICES"(js/Float32Array. new-vertices) fformat) @@ -51,16 +64,3 @@ (.end render-pass) (.submit (.-queue device) [(.finish encoder)]))) -(def vertices-render-shader - (clj->js {:label "vertices render shader descriptor" - :code " - @vertex - fn renderVertices(@location(0) pos: vec2f) -> @builtin(position) vec4 { - return vec4f(pos, 0.0, 1.0); - } - - @fragment - fn renderVerticesFragment() -> @location(0) vec4f { - return vec4f(0.9, 0.9, 0.9, 1); - } - "})) diff --git a/src/app/client/webgpu/core.cljs b/old-infra/src/app/client/webgpu/core.cljs similarity index 57% rename from src/app/client/webgpu/core.cljs rename to old-infra/src/app/client/webgpu/core.cljs index 675c789..c5fffd2 100644 --- a/src/app/client/webgpu/core.cljs +++ b/old-infra/src/app/client/webgpu/core.cljs @@ -9,25 +9,23 @@ (map #(.charCodeAt % 0) s)) +;; In ns app.client.webgpu.core (defn shape-text [texts fsize msdf-atlas] (let [atlas (:atlas msdf-atlas) - atlas-width (:width atlas) - atlas-height (:height atlas) metrics (:metrics msdf-atlas) - line-height (:lineHeight metrics) - glyphs (reduce (fn [acc glyph] - (assoc acc (:unicode glyph) - glyph)) - {} - (:glyphs msdf-atlas)) - font-size (* (/ 1 (:size atlas)) fsize) + + glyphs (reduce (fn [acc glyph] (assoc acc (:unicode glyph) glyph)) {} (:glyphs msdf-atlas)) + atlas-width (or (:width atlas) 1) + atlas-height (or (:height atlas) 1) + line-height (or (:lineHeight metrics) 1.2) + font-size fsize res (atom [])] + (doseq [txt texts] (let [{:keys [text x y]} txt !x (atom x) !y (atom y)] - ;(println "shape text" x y txt) (doseq [ch (seq text)] (let [codepoint (.charCodeAt ch 0)] (cond @@ -35,32 +33,35 @@ (= ch \space) (reset! !x (+ @!x (* font-size 0.5))) :else (let [glyph (get glyphs codepoint)] (when glyph - (let [advance (* font-size (:advance glyph)) - plane-bounds (:planeBounds glyph) + (let [plane-bounds (:planeBounds glyph) atlas-bounds (:atlasBounds glyph) - fw (/ (* font-size (- (get plane-bounds :right) (get plane-bounds :left))) 2) - fh (/ (* font-size (- (get plane-bounds :top) (get plane-bounds :bottom))) 2) - ;; Scale plane bounds by font size - pl @!x - pb (- @!y fh) - pr (+ @!x fw) - pt @!y - positions [[pl pb] [pr pb] [pr pt] [pl pt]] - ;; Calculate texture coordinates - al (/ (get atlas-bounds :left) atlas-width) - ab (/ (get atlas-bounds :bottom) atlas-height) - ar (/ (get atlas-bounds :right) atlas-width) - at (/ (get atlas-bounds :top) atlas-height) - uvs [[al (- 1.0 ab)] [ar (- 1.0 ab)] [ar (- 1.0 at)] [al (- 1.0 at)]]] + pb-left (or (get plane-bounds :left) 0) + pb-right (or (get plane-bounds :right) 0) + pb-top (or (get plane-bounds :top) 0) + pb-bottom (or (get plane-bounds :bottom) 0) + advance (* font-size (or (:advance glyph) 0)) + fw (* font-size (- pb-right pb-left)) + fh (* font-size (- pb-top pb-bottom)) + pl (+ @!x (* font-size pb-left)) + pr (+ pl fw) + pt (+ @!y (* font-size pb-top)) + pb (- pt fh) + positions [[pl pb] [pr pb] [pr pt] [pl pt]] + al (/ (:left atlas-bounds) atlas-width) + ab (/ (:bottom atlas-bounds) atlas-height) + ar (/ (:right atlas-bounds) atlas-width) + at (/ (:top atlas-bounds) atlas-height) + uvs [[ar (- 1.0 ab)] [al (- 1.0 ab)] [al (- 1.0 at)] [ar (- 1.0 at)]]] + ;uvs [[al (- 1.0 ab)] [ar (- 1.0 ab)] [ar (- 1.0 at)] [al (- 1.0 at)]] + (do - (reset! !x (+ @!x (/ advance 2))) - (swap! res conj {:codepoint codepoint - :positions positions - :uvs uvs})))))))))) + (reset! !x (+ @!x advance)) + (swap! res conj {:codepoint codepoint + :positions positions + :uvs uvs})))))))))) @res)) - (defn prepare-vertex-data [shaped-text] (let [vertices (atom []) indices (atom []) @@ -92,8 +93,229 @@ :index-data (js/Uint16Array. (clj->js @indices))})) -(defn render-text [device format context px-range font-size atlas font-bitmap texts] - ;(println "render text ") +;; --- The new, efficient setup function --- +(defn setup-text-renderer [^js/GPUDevice device fformat texts atlas font-bitmap] + (println "PERFORMING EXPENSIVE SETUP. This should only run once!") + + ;; STEP 1: PREPARE CPU DATA (The "Stencil Cutting") + ;; This is the only time we will call these expensive functions. + (let [shaped-texts (shape-text texts 16.0 atlas) ; Using a base font size + {:keys [vertex-data index-data]} (prepare-vertex-data shaped-texts) + num-indices (.-length index-data) + ;; ======================== DEBUGGING LOG ======================== + _ (js/console.log "--- Text Renderer Setup Debug ---") + _ (js/console.log "Number of shaped glyphs:" (count shaped-texts)) + _ (js/console.log "Number of indices to draw:" num-indices) + _ (when (< num-indices 100) + (js/console.log "Vertex Data (first 100 bytes):" (.slice vertex-data 0 100)) + (js/console.log "Index Data (first 20 indices):" (.slice index-data 0 20))) + _ (js/console.log "------------------------------------") + ;; ============================================================= + + ;; STEP 2: CREATE GPU BUFFERS AND UPLOAD STATIC DATA + ;; These buffers will live on the GPU for the lifetime of the app. + vertex-buffer (.createBuffer device + (clj->js {:label "Static Text Vertex Buffer" + :size (.-byteLength vertex-data) + :usage (bit-or js/GPUBufferUsage.VERTEX js/GPUBufferUsage.COPY_DST)})) + + index-buffer (.createBuffer device + (clj->js {:label "Static Text Index Buffer" + :size (.-byteLength index-data) + :usage (bit-or js/GPUBufferUsage.INDEX js/GPUBufferUsage.COPY_DST)})) + camera-uniform-buffer (.createBuffer device + (clj->js {:label "Camera Uniform Buffer" + :size 24 + :usage (bit-or js/GPUBufferUsage.UNIFORM js/GPUBufferUsage.COPY_DST)})) + + ;; NEW: Uniform buffer for the FRAGMENT shader (sizes) + sizes-data (js/Float32Array. (clj->js [16.0 ; px-range + (:size (:atlas atlas)) + 16.0])) ; font-size + sizes-uniform-buffer (.createBuffer device + (clj->js {:label "Sizes Uniform Buffer" + :size (.-byteLength sizes-data) + :usage (bit-or js/GPUBufferUsage.UNIFORM js/GPUBufferUsage.COPY_DST)})) + + + ;; Upload the vertex and index data now. This is a one-time operation. + _ (.writeBuffer (.-queue device) vertex-buffer 0 vertex-data) + _ (.writeBuffer (.-queue device) index-buffer 0 index-data) + _ (.writeBuffer (.-queue device) sizes-uniform-buffer 0 sizes-data) + + + + ;; STEP 3: CREATE TEXTURE AND SAMPLER (One time) + + bitmap-height (.-height font-bitmap) + bitmap-width (.-width font-bitmap) + texture (.createTexture device + (clj->js {:size {:width bitmap-width + :height bitmap-height + :depthOrArrayLayers 1} + :format "rgba8unorm" + :usage (bit-or js/GPUTextureUsage.RENDER_ATTACHMENT + js/GPUTextureUsage.TEXTURE_BINDING + js/GPUTextureUsage.COPY_DST)})) + sampler (.createSampler device (clj->js {:minFilter "linear" + :magFilter "linear" + :mipmapFilter "linear"})) + _ (.copyExternalImageToTexture + ( .-queue device) + ( clj->js {:source font-bitmap}) + ( clj->js {:texture texture + :origin {:x 0 :y 0 :z 0}}) + ( clj->js {:width bitmap-width + :height bitmap-height + :depthOrArrayLayers 1})) + + + ;; STEP 4: COMPILE SHADERS (One time) + ;; Your fragment shader can stay the same. We update the vertex shader. + vertex-shader-with-camera (clj->js {:label "text vertex shader with camera" + :code " + struct Camera { + pan: vec2, + zoom: f32, + // padding + screen_dimensions: vec2, + }; + + struct VertexInput { + @location(0) position: vec2, + @location(1) uv: vec2, + }; + + struct VertexOutput { + @builtin(position) position: vec4, + @location(0) uv: vec2, + }; + + @group(0) @binding(2) var camera: Camera; + + @vertex + fn main(input: VertexInput) -> VertexOutput { + var output: VertexOutput; + + let zoomed_position = input.position * camera.zoom; + let panned_position = zoomed_position + camera.pan; + + // Convert from world pixel coordinates to GPU clip space (-1.0 to 1.0) + let zero_to_two = panned_position / camera.screen_dimensions * 2.0; + let shifted = zero_to_two - vec2(1.0, 1.0); + + output.position = vec4(shifted.x, -shifted.y, 0.0, 1.0); // Flip Y + output.uv = input.uv; + return output; + } + "}) + + shader-module-vertex (.createShaderModule device vertex-shader-with-camera) + shader-module-fragment (.createShaderModule device text-fragment-shader) ; Your existing fragment shader + + + ;; STEP 5: CREATE THE ENTIRE RENDER PIPELINE (The most expensive "one-time" step) + bind-group-layout (.createBindGroupLayout + device + (clj->js {:label "bind group layout" + :entries [{:binding 0 + :visibility js/GPUShaderStage.FRAGMENT + :sampler {:type "filtering"}} + {:binding 1 + :visibility js/GPUShaderStage.FRAGMENT + :texture {:sampleType "float"}} + {:binding 2 + :visibility js/GPUShaderStage.VERTEX + :buffer {:type "uniform"}} + {:binding 3 ; Sizes Uniform + :visibility js/GPUShaderStage.FRAGMENT + :buffer {:type "uniform"}}]})) + + pipeline-layout (.createPipelineLayout device (clj->js {:bindGroupLayouts [bind-group-layout]})) + + pipeline (.createRenderPipeline device + (clj->js {:layout pipeline-layout + :vertex {:module shader-module-vertex + :entryPoint "main" + :buffers (clj->js [{:arrayStride (* 4 4) + :attributes (clj->js + [{:shaderLocation 0 :offset 0 :format "float32x2"} + {:shaderLocation 1 :offset 8 :format "float32x2"}])}])} + :fragment (clj->js + {:module shader-module-fragment + :entryPoint "main" + :targets (clj->js + [{:format fformat + :blend (clj->js {:color (clj->js {:srcFactor "src-alpha" + :dstFactor "one-minus-src-alpha"}) + :alpha (clj->js {:srcFactor "src-alpha" + :dstFactor "one-minus-src-alpha"})})}])})})) + + ;; STEP 6: CREATE THE BIND GROUP (Connects our resources together) + bind-group (.createBindGroup device + (clj->js {:layout bind-group-layout + :entries [{:binding 0, :resource sampler} + {:binding 1, :resource (.createView texture)} + {:binding 2, :resource {:buffer camera-uniform-buffer}} + {:binding 3, :resource {:buffer sizes-uniform-buffer}}]}))] + + ;; STEP 7: RETURN ALL THE REUSABLE GPU OBJECTS IN A MAP + (println "DONE WITH SETUP") + {:pipeline pipeline + :bind-group bind-group + :camera-uniform-buffer camera-uniform-buffer + :num-indices num-indices + :vertex-buffer vertex-buffer + :index-buffer index-buffer})) + + +;; --- The new, fast drawing function --- +(defn draw-text [^js/GPUDevice device ^js/GPUCanvasContext context renderer camera-state] + ;; `renderer` is the map we got from `setup-text-renderer` + ;; `camera-state` is a map like {:pan-x 10, :pan-y 20, :zoom 1.5, :width 800, :height 600} + + (println "DRAW TEXT: ") + (cljs.pprint/pprint renderer) + (cljs.pprint/pprint camera-state) + + ;; STEP 1: UPDATE THE SMALL UNIFORM BUFFER (Very fast) + (let [camera-array (js/Float32Array. + (clj->js [(:pan-x camera-state) + (:pan-y camera-state) + (:zoom camera-state) + 0.0 ; Padding + (:width camera-state) + (:height camera-state)]))] + ;; We only write 24 bytes of data each frame, not megabytes! + (.writeBuffer (.-queue device) (:camera-uniform-buffer renderer) 0 camera-array) + (println "wrote buffer")) + ;; STEP 2: CREATE ENCODER AND RENDER PASS + (let [encoder (.createCommandEncoder device) + ;; NOTE: loadOp is "load" so we draw ON TOP of whatever was there before (like your rectangles) + render-pass (.beginRenderPass encoder + (clj->js {:colorAttachments [{:view (.createView (.getCurrentTexture context)) + :loadOp "load" + :storeOp "store"}]}))] + + ;; STEP 3: ISSUE DRAW COMMANDS USING THE PRE-BUILT OBJECTS + (.setPipeline render-pass (:pipeline renderer)) + (.setBindGroup render-pass 0 (:bind-group renderer)) + (println "pipeline and bindgroup set") + + ;; We don't need to set the vertex/index buffers because they are part of the pipeline state in this setup + ;; (This can vary based on exact pipeline setup, but often they are set with the pipeline) + ;; If you need to set them explicitly: + (.setVertexBuffer render-pass 0 (:vertex-buffer renderer)) + (.setIndexBuffer render-pass (:index-buffer renderer) "uint16") + + (.drawIndexed render-pass (:num-indices renderer)) + + (.end render-pass) + (println "done render pass") + (.submit (.-queue device) [(.finish encoder)]))) + +(defn render-text [^js/GPUDevice device format ^js/GPUCanvasContext context px-range font-size atlas font-bitmap texts] + (println "webgpu render text ") (let [sizes (js/Float32Array. (clj->js [px-range (:size (:atlas atlas)) font-size])) shaped-texts (shape-text texts font-size atlas) ; _ (println "shaped texts" shaped-texts) @@ -102,11 +324,11 @@ ;_ (println 'vertex-data vertex-data) ;_ (println 'index-data index-data) num-indices (.-length index-data) - shader-module-vertex (.createShaderModule - device + shader-module-vertex (.createShaderModule + device text-vertex-shader) shader-module-fragment (.createShaderModule - device + device text-fragment-shader) vertex-buffer (.createBuffer device (clj->js {:size (.-byteLength vertex-data) @@ -116,10 +338,10 @@ (clj->js {:size (.-byteLength index-data) :usage (bit-or js/GPUBufferUsage.INDEX js/GPUBufferUsage.COPY_DST)})) - size-buffer (.createBuffer - device + size-buffer (.createBuffer + device (clj->js {:size (.-byteLength sizes) - :usage (bit-or js/GPUBufferUsage.UNIFORM + :usage (bit-or js/GPUBufferUsage.UNIFORM js/GPUBufferUsage.COPY_DST)})) bitmap-height (.-height font-bitmap) bitmap-width (.-width font-bitmap) @@ -145,10 +367,10 @@ {:binding 1 :visibility js/GPUShaderStage.FRAGMENT :texture {:sampleType "float"}} - {:binding 2 + {:binding 2 :visibility js/GPUShaderStage.FRAGMENT :buffer {:type "uniform"}}]})) - + bind-group (.createBindGroup device (clj->js {:layout bind-group-layout :entries (clj->js @@ -156,11 +378,11 @@ :resource sampler} {:binding 1 :resource texture-view} - {:binding 2 + {:binding 2 :resource {:buffer size-buffer}}])})) ;_ (println "bind group done") pipeline-layout (.createPipelineLayout - device + device (clj->js {:label "pipeline layout" :bindGroupLayouts [bind-group-layout]})) pipeline (.createRenderPipeline device @@ -171,23 +393,23 @@ :attributes (clj->js [{:shaderLocation 0 :offset 0 :format "float32x2"} {:shaderLocation 1 :offset 8 :format "float32x2"}])}])} - :fragment (clj->js + :fragment (clj->js {:module shader-module-fragment :entryPoint "main" - :targets (clj->js + :targets (clj->js [{:format format :blend (clj->js {:color (clj->js {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"}) :alpha (clj->js {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"})})}])})})) - + encoder (.createCommandEncoder device) bbg {:r 0.0 :g 0.0 :b 0.0 :a 1.0} wbg {:r 1.0 :g 1.0 :b 1.0 :a 1.0} render-pass (.beginRenderPass encoder - (clj->js {:colorAttachments + (clj->js {:colorAttachments (clj->js [{:view (.createView (.getCurrentTexture context)) :loadOp "load" :storeOp "store"}]) @@ -218,14 +440,12 @@ (.drawIndexed render-pass num-indices) (.end render-pass) (.submit (.-queue device) [(.finish encoder)]))) - - -(defn render-rect [from data device fformat context config ids] + + +(defn render-rect [from data ^js/GPUDevice device fformat ^js/GPUCanvasContext context config ids] ;(println 'uplaod-vertices data ":::::::" ids) - (println 'config config) - (let [varray (js/Float32Array. (clj->js data)) ids-array (js/Uint32Array. (clj->js ids)) ;_ (println "IDS ARRAY" ids-array) @@ -344,10 +564,10 @@ :entryPoint "renderVerticesFragment" :targets (clj->js [{:format fformat - :blend (clj->js {:color (clj->js {:srcFactor "src-alpha" - :dstFactor "one-minus-src-alpha"}) - :alpha (clj->js {:srcFactor "src-alpha" - :dstFactor "one-minus-src-alpha"})})}])})}))] + #_#_:blend (clj->js {:color (clj->js {:srcFactor "src-alpha" + :dstFactor "one-minus-src-alpha"}) + :alpha (clj->js {:srcFactor "src-alpha" + :dstFactor "one-minus-src-alpha"})})}])})}))] #_(-> (.getCompilationInfo shader-module) (.then (fn [info] (js/console.log "compute shader info:" info)))) diff --git a/src/app/client/webgpu/data.cljs b/old-infra/src/app/client/webgpu/data.cljs similarity index 100% rename from src/app/client/webgpu/data.cljs rename to old-infra/src/app/client/webgpu/data.cljs diff --git a/src/app/client/webgpu/pipeline.cljs b/old-infra/src/app/client/webgpu/pipeline.cljs similarity index 100% rename from src/app/client/webgpu/pipeline.cljs rename to old-infra/src/app/client/webgpu/pipeline.cljs diff --git a/src/app/client/webgpu/shader.cljs b/old-infra/src/app/client/webgpu/shader.cljs similarity index 97% rename from src/app/client/webgpu/shader.cljs rename to old-infra/src/app/client/webgpu/shader.cljs index 72fee9a..1f4506f 100644 --- a/src/app/client/webgpu/shader.cljs +++ b/old-infra/src/app/client/webgpu/shader.cljs @@ -26,7 +26,7 @@ fn main(input: VertexInput) -> VertexOutput { var output: VertexOutput; output.position = vec4(input.position, 0.0, 1.0); - output.uv = input.uv; +output.uv = vec2(1.0 - input.uv.x, input.uv.y); return output; } "})) @@ -40,7 +40,7 @@ @group(0) @binding(1) var texture0: texture_2d; - @group(0) @binding(2) var sizes:sizing; + @group(0) @binding(3) var sizes:sizing; struct sizing { pxRange: f32, @@ -68,7 +68,7 @@ let screenPxDistance = screenPxRange * (sd - 0.5); let opacity = clamp(screenPxDistance + 0.5, 0.0, 1.0); - let text = vec4(0.0, 0.0, 0.0, opacity); // Transparent background how?? + let text = vec4(1.0, 0.0, 0.0, opacity); // Transparent background how?? return text; } diff --git a/old-infra/src/app/electric_flow_old.cljc b/old-infra/src/app/electric_flow_old.cljc new file mode 100644 index 0000000..ffe2d2c --- /dev/null +++ b/old-infra/src/app/electric_flow_old.cljc @@ -0,0 +1,426 @@ +(ns app.electric-flow-old + (:require [hyperfiddle.electric3 :as e] + [missionary.core :as m] + [hyperfiddle.electric-dom3 :as dom] + [hyperfiddle.electric-svg3] + [hyperfiddle.incseq.mount-impl :refer [mount]] + [hyperfiddle.kvs :as kvs] + [hyperfiddle.domlike :as dl] + [hyperfiddle.incseq :as i] + #?@(:cljs [[app.client.webgpu.core :as wcore :refer + [render-rect + render-text + setup-text-renderer + draw-text]] + [global-flow :refer [await-promise + mouse-down?> + debounce + !canvas + !font-bitmap + !text-renderer + global-client-flow + !adapter + !global-atom + !device + !context + !atlas-data + !command-encoder + !format + !all-rects + !width + !height + !canvas-y + !visible-rects + !dpr + !old-visible-rects + !canvas-x + !zoom-factor + !offset]] + [app.client.webgpu.data :refer [!rects]]]))) + + +(hyperfiddle.rcf/enable!) + + +(e/declare canvas) +(e/declare squares) +(e/declare adapter) +(e/declare device) +(e/declare context) +(e/declare wformat) +(e/declare command-encoder) +(e/declare all-rects) +(e/declare width) +(e/declare height) +(e/declare canvas-y) +(e/declare canvas-x) +(e/declare offset) +(e/declare zoom-factor) +(e/declare rect-ids) +(e/declare visible-rects) +(e/declare old-visible-rects) +(e/declare data-spine) +(e/declare global-atom) +(e/declare font-bitmap) +(e/declare atlas-data) +(e/declare dpr) +(e/declare text-renderer) + + +(defn create-random-rects [rects ch cw] + (let [res (atom {})] + ;(println "RAND" @res rc ch cw) + (doseq [i rects] + (let [height (+ 20.0 (rand-int 60)) + width (+ 145.0 (rand-int 60)) + y (+ 0.1 (rand-int ch)) + x (+ 0.1 (rand-int cw))] + ;(js/console.log "xx" x y) + ;(println i 'Create-random-rects (keyword (str i)) [x y height width]) + (swap! res assoc (keyword (str i)) [x y height width]))) + ;(println "all RECTS" @res) + res)) + +#?(:cljs (defn format-float [inp] + (js/Number (.toFixed inp 3)))) + +#?(:cljs (defn clip-x [x w] (- (* 2 (/ x w)) 1))) +#?(:cljs (defn clip-y [y h] (- 1 (* 2 (/ y h))))) + +#?(:cljs (defn setup-all-text-and-get-renderer [dv fmat atl fnt rct] + + ;; PART A: Prepare a STATIC list of texts wih their ORIGINAL WORLD positions. + ;; This `reduce` loop now only runs ONCE + (js/console.log "SETUP all text :" dv fmat atl fnt rct) + (let [static-texts-in-world-space (reduce + (fn [acc [id data]] + (let [[x y] data] ; We only care about the original x and y + ;; The position is now just the original world position, NOT the final clip-space one. + ;; We add 7 here because your original code did. + (conj acc {:x (+ 7 x) + :y (+ 7 y) + :text (str (name id))}))) + [] + rct) + + ;; PART B: Call the "setup renderer" function we designed before. + ;; It will take this static text data and create the permanent GPU objects. + renderer-objects (setup-text-renderer dv + fmat + static-texts-in-world-space + atl + fnt)] + ;(println "static text rects" dv fmat fnt rct) + renderer-objects + ))) + + + +(e/defn Setup-webgpu [] + (e/client + (when (some? canvas) + (js/console.log canvas) + (let [context (.getContext canvas "webgpu" (clj->js {:alpha true})) + gpu js/navigator.gpu + adapter (e/Task (await-promise (.requestAdapter gpu (clj->js {:requiredFeatures ["validation"]})))) + device (e/Task (await-promise (.requestDevice adapter))) + cformat (.getPreferredCanvasFormat gpu) + config (clj->js {:format cformat + :device device}) + ar (e/snapshot all-rects) + atl (e/snapshot atlas-data) + fnt (e/snapshot font-bitmap)] + (.configure context config) + (reset! !adapter adapter) + (reset! !device device) + (reset! !context context) + (reset! !format cformat) + (reset! !text-renderer (setup-all-text-and-get-renderer device + cformat + atl + fnt + ar + )))))) + + + +(e/defn Mouse-down-cords [node] (e/input (mouse-down?> node))) + + +(e/defn Add-panning [] + (when-some [[start-x start-y] (Mouse-down-cords canvas)] + (let [[off-x off-y] (e/snapshot offset)] + (dom/On "mousemove" + (fn [e] + (.preventDefault e) + (let [end-x (.-clientX e) + end-y (.-clientY e) + new-pan-x (+ off-x (* dpr (- end-x start-x))) + new-pan-y (+ off-y (* dpr (- end-y start-y)))] + (reset! !offset [new-pan-x new-pan-y]) + [new-pan-x new-pan-y])) + "")))) + + + +(e/defn Add-wheel [] + (dom/On "wheel" + (fn [e] (.preventDefault e) + (let [delta (.-deltaY e) + rect (.getBoundingClientRect (.-target e)) + cursor-x (* dpr (- (.-clientX e) (.-left rect))) + cursor-y (* dpr (- (.-clientY e) (.-top rect))) + scale (if (< delta 0) 1.02 0.98) + new-zoom (* zoom-factor scale) + [off-x off-y] offset + pan-zoom (- 1 scale) + current-pan-x (* (- cursor-x off-x) pan-zoom) + current-pan-y (* (- cursor-y off-y) pan-zoom) + total-pan-x (+ off-x current-pan-x) + total-pan-y (+ off-y current-pan-y)] + (println "Wheel" total-pan-x total-pan-y new-zoom 1) + (reset! !offset [total-pan-x total-pan-y]) + (reset! !zoom-factor new-zoom))) + nil + {:passive false})) + +;; This new function does all the setup work. Call it ONCE. + +(e/defn Render-text [cx cy zf] + (println "rendering text") + (let [dv (e/snapshot device) + con (e/snapshot context) + fmat (e/snapshot wformat) + renderer (e/snapshot text-renderer) + ] + (when (every? some? [renderer dv con fmat ]) + (let [camera-state {:pan-x cx + :pan-y cy + :zoom zf + :width width + :height height}] + (println "render text --" dv "--" con "--" renderer "--" camera-state) + (draw-text dv con renderer camera-state)) + #_(let [texts (reduce + (fn [acc [id data]] + (let [[x y dh dw] data + left (clip-x + (+ (* (+ 7 x) zf) cx) + width) + top (clip-y + (+ (* (+ 7 y) zf) cy) + height)] + (conj acc {:x left + :y top + :text (str (name id))}))) + [] + all-rects) + zof (max 17 (* (/ 1 zf) 14))] + (render-text + dv + fmat + con + 16 + zof + atlas-data + font-bitmap + texts))))) + +(e/defn Render-rect [cx cy zf] + (let [dv (e/snapshot device) + fmat (e/snapshot wformat) + con (e/snapshot context) + ] + #_(println "render rect" device wformat context) + (when (every? some? [device wformat context]) + (println "render rect" ) + (let [rects-data (flatten (into [] (vals all-rects))) + [s e] (e/Token offset) + rects-ids (into [] (keys all-rects))] + #_(println "render rect crx" cx cy) + (render-rect + "zoom" + rects-data + dv + fmat + con + [width height cx cy zf] + rects-ids) + #_(when (some? s) + (let [[cx cy] (s (e/Task (m/sleep 25 offset)))] + (Render-text cx cy zf)))) + ))) + + +(e/defn Tap-diffs + ([f! x] + (f! (e/input (e/pure x))) + x) + ([x] (Tap-diffs prn x))) + + + +(e/defn On-node-add [id] + (when-some [[x y h w] (id all-rects)] + ((fn [] + (let [gx (-> global-atom :cords first) + gy (-> global-atom :cords second) + [ox oy zf] offset + cgx (- (clip-x gx width) ox) + cgy (- (clip-y gy height) oy) + cl (clip-x x width) + cr (clip-x (+ x w) width) + ct (clip-y y height) + cb (clip-y (+ y h) height) + zff (or zf zoom-factor) + clicked? (and (<= cgx cr) (>= cgx cl) + (<= cgy ct) (>= cgy cb))] + (println + id + gx gy + zoom-factor + offset + ":R:" + [x y h w] + (format-float (+ ox (* zff cl))) + (format-float (+ oy (* zff ct))))))))) + +(e/defn Canvas-view [] + (e/client + (dom/canvas + (dom/props {:id "top-canvas" + :height height + :width width + :style {:height (str (/ height dpr) "px") + :width (str (/ width dpr) "px")}}) + (reset! !canvas dom/node) + + (when-some [down (Mouse-down-cords dom/node)] + (println "DOWN") + (reset! !global-atom {:cords down})) + #_(e/for-by identity [node (e/as-vec (e/input (e/join (i/items data-spine))))] + + (println node global-atom) + #_(On-node-add node)) + #_(println "NEW SPINE" + (count visible-rects) + (e/input (i/count data-spine)) + visible-rects + (e/as-vec (e/input (e/join (i/items data-spine))))) + (let [mount-items (mount + (fn [element child] (do + (data-spine + child + (fn [_ new] + (keyword (str new))) + child) + (.push element child) + element)) + (fn [element child previous] (do + (let [idx (.indexOf element previous)] + (when (>= idx 0) + (aset element idx child))) + element)) + (fn [element child sibling] (do + (let [idx (.indexOf element sibling)] + (if (>= idx 0) + (.splice element idx 0 child) + (.push element child))) + element)) + (fn [element child] (do + (data-spine + child + (fn [_ new] + (keyword (str new))) + nil) + (let [idx (.indexOf element child)] + (when (>= idx 0) + (.splice element idx 1))) + element)) + (fn [element i] (do + (aget element i)))) + + diff (e/input (e/pure (e/diff-by identity visible-rects)))] + + ((fn [] (when (some? diff) + (mount-items (object-array @!old-visible-rects) diff)))))))) + + + +#?(:cljs (defn load-bitmap-file [] + (println "Load bitmap file") + (-> (js/fetch "/font_atlas.png") + (.then #(.blob %)) + (.then #(js/createImageBitmap %)) + (.then (fn [img] + (reset! !font-bitmap img)))))) + +#?(:cljs + (defn read-json-file [] + (-> (js/fetch "/font_atlas.json") + (.then (fn [response] + (.json response))) + (.then (fn [data] + (reset! !atlas-data (js->clj data :keywordize-keys true))))))) + + +(e/defn main [ring-request] + (e/client + (binding [dom/node js/document.body + canvas (e/watch !canvas) + canvas-x (e/watch !canvas-x) + canvas-y (e/watch !canvas-y) + height (e/watch !height) + width (e/watch !width) + device (e/watch !device) + wformat (e/watch !format) + context (e/watch !context) + all-rects (e/watch !all-rects) + offset (e/watch !offset) + zoom-factor (e/watch !zoom-factor) + visible-rects (e/watch !visible-rects) + old-visible-rects (e/watch !old-visible-rects) + data-spine (i/spine) + rect-ids (vec (range 1 30)) + global-atom (e/watch !global-atom) + font-bitmap (e/watch !font-bitmap) + atlas-data (e/watch !atlas-data) + text-renderer (e/watch !text-renderer) + dpr (e/watch !dpr)] + + (reset! !dpr (.-devicePixelRatio js/window)) + (reset! !width (* dpr (.-clientWidth dom/node))) + (reset! !height (* dpr (.-clientHeight dom/node))) + (reset! !canvas-x 0) + (reset! !canvas-y 0) + (reset! !offset [0 0]) + (reset! !zoom-factor 1) + (load-bitmap-file) + (read-json-file) + (Canvas-view) + (when-not (some nil? [canvas height width]) + (let [rnd (create-random-rects rect-ids height width)] + ;(println "RND" @rnd) + (reset! !all-rects @rnd) + ;(println "all-rects" all-rects) + (when (and (some? font-bitmap) (some? all-rects)) + (do + ;(println "total rncts" all-rects) + ;(println "success canvas" canvas all-rects) + (Setup-webgpu) + (Add-panning) + (Add-wheel) + + (when (some? text-renderer) + (let [[cx cy] offset + zf zoom-factor + [gx gy] (e/Task (m/sleep 40 offset)) + [s e] (e/Token offset)] + (println "GX GY" gx gy) + (let [[gx gy] (e/snapshot offset)] + (println "render text ") + (Render-text cx cy zf) + (Render-rect cx cy zf) + )) + #_(Render-rect cx cy zf)) + ))))))) diff --git a/src/app/server/llm.clj b/old-infra/src/app/server/llm.clj similarity index 100% rename from src/app/server/llm.clj rename to old-infra/src/app/server/llm.clj diff --git a/src/app/server/rama/testing.cljc b/old-infra/src/app/server/rama/testing.cljc similarity index 100% rename from src/app/server/rama/testing.cljc rename to old-infra/src/app/server/rama/testing.cljc diff --git a/src/app/server/rama_module.clj b/old-infra/src/app/server/rama_module.clj similarity index 100% rename from src/app/server/rama_module.clj rename to old-infra/src/app/server/rama_module.clj diff --git a/old-infra/src/components/_design_ir.cljc b/old-infra/src/components/_design_ir.cljc new file mode 100644 index 0000000..cbacb56 --- /dev/null +++ b/old-infra/src/components/_design_ir.cljc @@ -0,0 +1,716 @@ +(ns components._design_ir + "Design IR schema and deterministic validator. + + The validator is intentionally strict: + - rejects malformed node shapes + - rejects unknown keys + - validates recursive children/state nodes" + (:require [clojure.set :as set] + [clojure.string :as str])) + +(def schema-version 1) + +(def allowed-node-roles + #{:container :interactive :decorative :text}) + +(def allowed-node-keys + #{:id :tag :role :bounds :visual :typography :layout :states :slots :children :source-meta}) + +(def allowed-bounds-keys + #{:x :y :w :h}) + +(def allowed-visual-keys + #{:fill :gradient :radius :border :shadow :opacity}) + +(def allowed-fill-keys + #{:type :value :ref :token-ref :token-distance}) + +(def allowed-fill-types + #{:solid :token}) + +(def allowed-gradient-keys + #{:type :angle :stops}) + +(def allowed-gradient-stop-keys + #{:at :value}) + +(def allowed-radius-keys + #{:uniform :corners}) + +(def allowed-border-keys + #{:width :widths :color}) + +(def allowed-shadow-keys + #{:offset :blur :spread :color}) + +(def allowed-typography-keys + #{:content :size :weight :color :family :line-height :letter-spacing :align}) + +(def allowed-layout-keys + #{:display :direction :gap :padding :align-items :justify :justify-content :align :auto-height?}) + +(def allowed-state-keys + #{:visual :typography :layout :slots :children}) + +(def allowed-slot-keys + #{:role :description :bounds}) + +(def allowed-source-meta-keys + #{:library :component :variant :url :extractor :source-path}) + +(def allowed-layout-directions + #{:row :column}) + +(def allowed-layout-displays + #{:flex :block :inline :none}) + +(defn finite-number? + [x] + (and (number? x) + #?(:clj + (let [d (double x)] + (and (not (Double/isNaN d)) + (not (Double/isInfinite d)))) + :cljs + (js/isFinite x)))) + +(defn non-negative-number? + [x] + (and (finite-number? x) (<= 0 x))) + +(defn positive-number? + [x] + (and (finite-number? x) (< 0 x))) + +(defn non-empty-string? + [s] + (and (string? s) (not (str/blank? s)))) + +(defn token-ref? + [x] + (and (sequential? x) + (seq x) + (every? keyword? x))) + +(defn rgba-vector? + [x] + (and (vector? x) + (= 4 (count x)) + (every? finite-number? x))) + +(defn color-value? + [x] + (or (string? x) (rgba-vector? x))) + +(defn padding-value? + [x] + (or (non-negative-number? x) + (and (vector? x) + (contains? #{2 4} (count x)) + (every? non-negative-number? x)))) + +(defn indexed-mapcat + [f coll] + (mapcat identity (map-indexed f coll))) + +(defn validation-error + ([path code message] + {:path path :code code :message message}) + ([path code message details] + {:path path :code code :message message :details details})) + +(defn unknown-key-errors + [m allowed-keys path] + (let [unknown (sort (set/difference (set (keys m)) allowed-keys))] + (mapv (fn [k] + (validation-error (conj path k) + :unknown-key + "Unknown key in Design IR map." + {:allowed (sort allowed-keys)})) + unknown))) + +(declare validate-ir-node) + +(defn validate-bounds + [bounds path] + (cond + (nil? bounds) + [(validation-error path :missing-bounds "Missing required :bounds map.")] + + (not (map? bounds)) + [(validation-error path :invalid-bounds "Expected :bounds to be a map.")] + + :else + (vec + (concat + (unknown-key-errors bounds allowed-bounds-keys path) + (when-not (contains? bounds :w) + [(validation-error (conj path :w) :missing-width "Missing required bounds width :w.")]) + (when-not (contains? bounds :h) + [(validation-error (conj path :h) :missing-height "Missing required bounds height :h.")]) + (when (contains? bounds :w) + (when-not (positive-number? (:w bounds)) + [(validation-error (conj path :w) :invalid-width "Bounds :w must be a positive number.")])) + (when (contains? bounds :h) + (when-not (positive-number? (:h bounds)) + [(validation-error (conj path :h) :invalid-height "Bounds :h must be a positive number.")])) + (for [k [:x :y] + :when (contains? bounds k) + :when (not (finite-number? (get bounds k)))] + (validation-error (conj path k) :invalid-coordinate "Bounds coordinate must be a finite number.")))))) + +(defn validate-fill + [fill path] + (cond + (not (map? fill)) + [(validation-error path :invalid-fill "Expected :fill to be a map.")] + + :else + (let [fill-type (:type fill) + ref (or (:ref fill) (:token-ref fill))] + (vec + (concat + (unknown-key-errors fill allowed-fill-keys path) + (when-not (contains? fill :type) + [(validation-error (conj path :type) :missing-fill-type "Missing required fill :type.")]) + (when (contains? fill :type) + (when-not (contains? allowed-fill-types fill-type) + [(validation-error (conj path :type) + :invalid-fill-type + "Fill :type must be :solid or :token.")])) + (when (= fill-type :solid) + (when-not (color-value? (:value fill)) + [(validation-error (conj path :value) + :invalid-fill-value + "Solid fill requires :value as color string or [r g b a].")])) + (when (= fill-type :token) + (when-not (token-ref? ref) + [(validation-error path + :invalid-token-ref + "Token fill requires :ref or :token-ref as vector of keywords.")])) + (when (contains? fill :value) + (when-not (color-value? (:value fill)) + [(validation-error (conj path :value) + :invalid-fill-value + "Fill :value must be color string or [r g b a].")])) + (when (contains? fill :token-distance) + (when-not (non-negative-number? (:token-distance fill)) + [(validation-error (conj path :token-distance) + :invalid-token-distance + "Fill :token-distance must be a non-negative number.")]))))))) + +(defn validate-gradient-stop + [stop path] + (cond + (not (map? stop)) + [(validation-error path :invalid-gradient-stop "Gradient stop must be a map.")] + + :else + (vec + (concat + (unknown-key-errors stop allowed-gradient-stop-keys path) + (when-not (contains? stop :at) + [(validation-error (conj path :at) :missing-stop-at "Gradient stop missing :at.")]) + (when-not (contains? stop :value) + [(validation-error (conj path :value) :missing-stop-value "Gradient stop missing :value.")]) + (when (contains? stop :at) + (let [at (:at stop)] + (when-not (and (finite-number? at) (<= 0 at 1)) + [(validation-error (conj path :at) + :invalid-stop-at + "Gradient stop :at must be a number in [0,1].")]))) + (when (contains? stop :value) + (when-not (color-value? (:value stop)) + [(validation-error (conj path :value) + :invalid-stop-value + "Gradient stop :value must be a color string or [r g b a].")])))))) + +(defn validate-gradient + [gradient path] + (cond + (not (map? gradient)) + [(validation-error path :invalid-gradient "Expected :gradient to be a map.")] + + :else + (vec + (concat + (unknown-key-errors gradient allowed-gradient-keys path) + (when (contains? gradient :type) + (when-not (= :linear (:type gradient)) + [(validation-error (conj path :type) + :invalid-gradient-type + "Only :linear gradients are currently supported.")])) + (when (contains? gradient :angle) + (when-not (finite-number? (:angle gradient)) + [(validation-error (conj path :angle) + :invalid-gradient-angle + "Gradient :angle must be a finite number.")])) + (let [stops (:stops gradient)] + (when (contains? gradient :stops) + (cond + (not (vector? stops)) + [(validation-error (conj path :stops) + :invalid-gradient-stops + "Gradient :stops must be a vector.")] + + (empty? stops) + [(validation-error (conj path :stops) + :empty-gradient-stops + "Gradient :stops must not be empty.")] + + :else + (indexed-mapcat + (fn [idx stop] + (validate-gradient-stop stop (conj path :stops idx))) + stops)))))))) + +(defn validate-radius + [radius path] + (cond + (not (map? radius)) + [(validation-error path :invalid-radius "Expected :radius to be a map.")] + + :else + (let [uniform (:uniform radius) + corners (:corners radius)] + (vec + (concat + (unknown-key-errors radius allowed-radius-keys path) + (when (and (nil? uniform) (nil? corners)) + [(validation-error path + :missing-radius-shape + "Radius requires :uniform or :corners.")]) + (when (and (some? uniform) (some? corners)) + [(validation-error path + :ambiguous-radius-shape + "Radius cannot define both :uniform and :corners.")]) + (when (some? uniform) + (when-not (non-negative-number? uniform) + [(validation-error (conj path :uniform) + :invalid-uniform-radius + "Radius :uniform must be a non-negative number.")])) + (when (some? corners) + (cond + (not (vector? corners)) + [(validation-error (conj path :corners) + :invalid-corner-radii + "Radius :corners must be a vector.")] + + (not= 4 (count corners)) + [(validation-error (conj path :corners) + :invalid-corner-radii-count + "Radius :corners must contain exactly 4 values.")] + + (not-every? non-negative-number? corners) + [(validation-error (conj path :corners) + :invalid-corner-radii + "All :corners values must be non-negative numbers.")]))))))) + +(defn validate-border + [border path] + (cond + (not (map? border)) + [(validation-error path :invalid-border "Expected :border to be a map.")] + + :else + (let [w (:width border) + ws (:widths border)] + (vec + (concat + (unknown-key-errors border allowed-border-keys path) + (when (and (nil? w) (nil? ws) (nil? (:color border))) + [(validation-error path + :empty-border + "Border map is empty. Provide :width/:widths and/or :color.")]) + (when (and (some? w) (some? ws)) + [(validation-error path + :ambiguous-border-width + "Border cannot define both :width and :widths.")]) + (when (some? w) + (when-not (non-negative-number? w) + [(validation-error (conj path :width) + :invalid-border-width + "Border :width must be a non-negative number.")])) + (when (some? ws) + (cond + (not (vector? ws)) + [(validation-error (conj path :widths) + :invalid-border-widths + "Border :widths must be a vector.")] + + (not= 4 (count ws)) + [(validation-error (conj path :widths) + :invalid-border-widths-count + "Border :widths must contain exactly 4 values.")] + + (not-every? non-negative-number? ws) + [(validation-error (conj path :widths) + :invalid-border-widths + "All border widths must be non-negative numbers.")])) + (when (contains? border :color) + (when-not (color-value? (:color border)) + [(validation-error (conj path :color) + :invalid-border-color + "Border :color must be a color string or [r g b a].")]))))))) + +(defn validate-shadow + [shadow path] + (cond + (not (map? shadow)) + [(validation-error path :invalid-shadow "Expected :shadow to be a map.")] + + :else + (vec + (concat + (unknown-key-errors shadow allowed-shadow-keys path) + (when (contains? shadow :offset) + (let [offset (:offset shadow)] + (cond + (not (vector? offset)) + [(validation-error (conj path :offset) + :invalid-shadow-offset + "Shadow :offset must be [x y].")] + + (not= 2 (count offset)) + [(validation-error (conj path :offset) + :invalid-shadow-offset-count + "Shadow :offset must contain exactly 2 values.")] + + (not-every? finite-number? offset) + [(validation-error (conj path :offset) + :invalid-shadow-offset + "Shadow :offset values must be finite numbers.")]))) + (for [k [:blur :spread] + :when (contains? shadow k) + :when (not (non-negative-number? (get shadow k)))] + (validation-error (conj path k) + :invalid-shadow-size + "Shadow blur/spread must be non-negative numbers.")) + (when (contains? shadow :color) + (when-not (color-value? (:color shadow)) + [(validation-error (conj path :color) + :invalid-shadow-color + "Shadow :color must be a color string or [r g b a].")])))))) + +(defn validate-visual + [visual path] + (cond + (not (map? visual)) + [(validation-error path :invalid-visual "Expected :visual to be a map.")] + + :else + (vec + (concat + (unknown-key-errors visual allowed-visual-keys path) + (when-let [fill (:fill visual)] + (validate-fill fill (conj path :fill))) + (when-let [gradient (:gradient visual)] + (validate-gradient gradient (conj path :gradient))) + (when-let [radius (:radius visual)] + (validate-radius radius (conj path :radius))) + (when-let [border (:border visual)] + (validate-border border (conj path :border))) + (when-let [shadow (:shadow visual)] + (validate-shadow shadow (conj path :shadow))) + (when (contains? visual :opacity) + (let [opacity (:opacity visual)] + (when-not (and (finite-number? opacity) (<= 0 opacity 1)) + [(validation-error (conj path :opacity) + :invalid-opacity + "Visual :opacity must be in [0,1].")]))))))) + +(defn validate-typography + [typography path {:keys [partial?] :or {partial? false}}] + (cond + (not (map? typography)) + [(validation-error path :invalid-typography "Expected :typography to be a map.")] + + :else + (vec + (concat + (unknown-key-errors typography allowed-typography-keys path) + (when (and (not partial?) (not (contains? typography :content))) + [(validation-error (conj path :content) + :missing-typography-content + "Typography requires :content for full node specs.")]) + (when (contains? typography :content) + (when-not (string? (:content typography)) + [(validation-error (conj path :content) + :invalid-typography-content + "Typography :content must be a string.")])) + (for [k [:size :weight :line-height] + :when (contains? typography k) + :when (not (positive-number? (get typography k)))] + (validation-error (conj path k) + :invalid-typography-number + "Typography numeric value must be a positive number.")) + (when (contains? typography :letter-spacing) + (when-not (finite-number? (:letter-spacing typography)) + [(validation-error (conj path :letter-spacing) + :invalid-letter-spacing + "Typography :letter-spacing must be finite.")])) + (when (contains? typography :family) + (when-not (non-empty-string? (:family typography)) + [(validation-error (conj path :family) + :invalid-family + "Typography :family must be a non-empty string.")])) + (when (contains? typography :color) + (when-not (color-value? (:color typography)) + [(validation-error (conj path :color) + :invalid-typography-color + "Typography :color must be a color string or [r g b a].")])) + (when (contains? typography :align) + (when-not (keyword? (:align typography)) + [(validation-error (conj path :align) + :invalid-typography-align + "Typography :align must be a keyword.")])))))) + +(defn validate-layout + [layout path] + (cond + (not (map? layout)) + [(validation-error path :invalid-layout "Expected :layout to be a map.")] + + :else + (vec + (concat + (unknown-key-errors layout allowed-layout-keys path) + (when (contains? layout :display) + (let [display (:display layout)] + (when-not (contains? allowed-layout-displays display) + [(validation-error (conj path :display) + :invalid-layout-display + "Layout :display must be one of #{:flex :block :inline :none}.")]))) + (when (contains? layout :direction) + (let [direction (:direction layout)] + (when-not (contains? allowed-layout-directions direction) + [(validation-error (conj path :direction) + :invalid-layout-direction + "Layout :direction must be :row or :column.")]))) + (when (contains? layout :gap) + (when-not (non-negative-number? (:gap layout)) + [(validation-error (conj path :gap) + :invalid-layout-gap + "Layout :gap must be a non-negative number.")])) + (when (contains? layout :padding) + (when-not (padding-value? (:padding layout)) + [(validation-error (conj path :padding) + :invalid-layout-padding + "Layout :padding must be number or 2/4-value vector.")])) + (when (contains? layout :auto-height?) + (when-not (boolean? (:auto-height? layout)) + [(validation-error (conj path :auto-height?) + :invalid-auto-height + "Layout :auto-height? must be boolean.")])) + (for [k [:align-items :justify :justify-content :align] + :when (contains? layout k) + :when (not (keyword? (get layout k)))] + (validation-error (conj path k) + :invalid-layout-alignment + "Layout alignment values must be keywords.")))))) + +(defn validate-slot + [slot path] + (cond + (not (map? slot)) + [(validation-error path :invalid-slot "Slot definition must be a map.")] + + :else + (vec + (concat + (unknown-key-errors slot allowed-slot-keys path) + (when-not (contains? slot :role) + [(validation-error (conj path :role) + :missing-slot-role + "Slot definition requires :role.")]) + (when (contains? slot :role) + (let [role (:role slot)] + (when-not (contains? allowed-node-roles role) + [(validation-error (conj path :role) + :invalid-slot-role + "Slot :role must be one of allowed node roles.")])) + nil) + (when (contains? slot :description) + (when-not (string? (:description slot)) + [(validation-error (conj path :description) + :invalid-slot-description + "Slot :description must be a string.")])) + (when (contains? slot :bounds) + (validate-bounds (:bounds slot) (conj path :bounds))))))) + +(defn validate-slots + [slots path] + (cond + (not (map? slots)) + [(validation-error path :invalid-slots "Expected :slots to be a map.")] + + :else + (indexed-mapcat + (fn [_ [slot-id slot]] + (let [slot-path (conj path slot-id) + slot-id-errors (when-not (keyword? slot-id) + [(validation-error slot-path + :invalid-slot-id + "Slot id must be a keyword.")])] + (concat slot-id-errors + (validate-slot slot slot-path)))) + (vec slots)))) + +(defn validate-source-meta + [source-meta path] + (cond + (not (map? source-meta)) + [(validation-error path :invalid-source-meta "Expected :source-meta to be a map.")] + + :else + (vec + (concat + (unknown-key-errors source-meta allowed-source-meta-keys path) + (for [k [:library :component :variant :url :source-path] + :when (contains? source-meta k) + :when (not (non-empty-string? (get source-meta k)))] + (validation-error (conj path k) + :invalid-source-meta-value + "Source metadata string must be non-empty.")) + (when (contains? source-meta :extractor) + (let [extractor (:extractor source-meta)] + (when-not (keyword? extractor) + [(validation-error (conj path :extractor) + :invalid-extractor + "Source metadata :extractor must be a keyword.")]))))))) + +(defn validate-state-overrides + [states path] + (cond + (not (map? states)) + [(validation-error path :invalid-states "Expected :states to be a map of state keyword to overrides.")] + + :else + (indexed-mapcat + (fn [_ [state-id override]] + (let [state-path (conj path state-id)] + (concat + (when-not (keyword? state-id) + [(validation-error state-path :invalid-state-id "State id must be a keyword.")]) + (cond + (not (map? override)) + [(validation-error state-path + :invalid-state-override + "State override must be a map.")] + + :else + (concat + (unknown-key-errors override allowed-state-keys state-path) + (when-let [visual (:visual override)] + (validate-visual visual (conj state-path :visual))) + (when-let [typography (:typography override)] + (validate-typography typography (conj state-path :typography) {:partial? true})) + (when-let [layout (:layout override)] + (validate-layout layout (conj state-path :layout))) + (when-let [slots (:slots override)] + (validate-slots slots (conj state-path :slots))) + (when-let [children (:children override)] + (if-not (vector? children) + [(validation-error (conj state-path :children) + :invalid-state-children + "State override :children must be a vector.")] + (indexed-mapcat + (fn [idx child] + (validate-ir-node child (conj state-path :children idx))) + children)))))))) + (vec states)))) + +(defn validate-ir-node + ([node] (validate-ir-node node [:design-ir])) + ([node path] + (cond + (not (map? node)) + [(validation-error path :invalid-node "Design IR node must be a map.")] + + :else + (vec + (concat + (unknown-key-errors node allowed-node-keys path) + (when-not (contains? node :tag) + [(validation-error (conj path :tag) :missing-tag "Design IR node requires :tag.")]) + (when (contains? node :tag) + (when-not (non-empty-string? (:tag node)) + [(validation-error (conj path :tag) + :invalid-tag + "Design IR node :tag must be a non-empty string.")])) + (when-not (contains? node :role) + [(validation-error (conj path :role) :missing-role "Design IR node requires :role.")]) + (when (contains? node :role) + (let [role (:role node)] + (when-not (contains? allowed-node-roles role) + [(validation-error (conj path :role) + :invalid-role + "Design IR node :role must be a supported keyword.")]))) + (when (contains? node :id) + (let [id (:id node)] + (when-not (or (keyword? id) (string? id)) + [(validation-error (conj path :id) + :invalid-id + "Design IR node :id must be keyword or string.")]))) + (validate-bounds (:bounds node) (conj path :bounds)) + (when-let [visual (:visual node)] + (validate-visual visual (conj path :visual))) + (when-let [typography (:typography node)] + (validate-typography typography (conj path :typography) {:partial? false})) + (when-let [layout (:layout node)] + (validate-layout layout (conj path :layout))) + (when-let [slots (:slots node)] + (validate-slots slots (conj path :slots))) + (when-let [source-meta (:source-meta node)] + (validate-source-meta source-meta (conj path :source-meta))) + (when-let [states (:states node)] + (validate-state-overrides states (conj path :states))) + (let [children (:children node)] + (cond + (nil? children) + [] + + (not (vector? children)) + [(validation-error (conj path :children) + :invalid-children + "Design IR :children must be a vector.")] + + :else + (indexed-mapcat + (fn [idx child] + (validate-ir-node child (conj path :children idx))) + children)))))))) + +(defn validate-design-ir + "Validate a design IR document. + + Accepts either: + - a raw Design IR root node map + - a wrapper map containing :design-ir root node + + Returns: + {:schema-version 1 + :valid? boolean + :errors [{:path .. :code .. :message ..}]}" + [doc] + (let [root (if (and (map? doc) (contains? doc :design-ir)) + (:design-ir doc) + doc) + errors (validate-ir-node root [:design-ir])] + {:schema-version schema-version + :valid? (empty? errors) + :errors (vec errors)})) + +(defn valid-design-ir? + [doc] + (:valid? (validate-design-ir doc))) + +(defn assert-valid-design-ir! + "Throws ex-info if the Design IR document is invalid." + [doc] + (let [{:keys [valid? errors]} (validate-design-ir doc)] + (when-not valid? + (throw (ex-info "Invalid Design IR document." + {:type ::invalid-design-ir + :errors errors}))) + doc)) diff --git a/old-infra/src/components/_verifier.cljc b/old-infra/src/components/_verifier.cljc new file mode 100644 index 0000000..6da4c7c --- /dev/null +++ b/old-infra/src/components/_verifier.cljc @@ -0,0 +1,559 @@ +(ns components._verifier + "Deterministic Design IR verifier. + + This module is intentionally authoritative: + - no probabilistic scoring + - no LLM dependency + - explicit thresholds and pass/fail output" + (:require [clojure.set :as set] + [clojure.string :as str] + [components._design_ir :as design-ir])) + +(def default-thresholds + {:geometry {:max-deviation-px 2.0} + :style {:max-color-dist 0.02 + :max-scalar-diff 2.0} + :structure {:similarity-ratio 0.70} + :states {:coverage-ratio 0.60 + :max-color-dist 0.02 + :max-scalar-diff 2.0}}) + +(def verification-report-schema + {:required-keys [:component :timestamp :verdict :metrics] + :metric-keys + {:geometry [:max-deviation-px :mean-deviation-px :threshold :pass?] + :style [:max-color-dist :max-scalar-diff :threshold :pass?] + :structure [:similarity-ratio :threshold :pass?] + :states [:covered :expected :ratio :threshold :pass?]} + :optional-keys [:notes :source-path :extractor]}) + +(defn abs* + [x] + #?(:clj (Math/abs (double x)) + :cljs (js/Math.abs x))) + +(defn sqrt* + [x] + #?(:clj (Math/sqrt (double x)) + :cljs (js/Math.sqrt x))) + +(defn now-iso + [] + #?(:clj (.toString (java.time.Instant/now)) + :cljs (.toISOString (js/Date.)))) + +(defn finite-number? + [x] + (and (number? x) + #?(:clj + (let [d (double x)] + (and (not (Double/isNaN d)) + (not (Double/isInfinite d)))) + :cljs + (js/isFinite x)))) + +(defn safe-parse-double + [s] + (try + #?(:clj (Double/parseDouble (str/trim s)) + :cljs (js/parseFloat (str/trim s))) + (catch #?(:clj Throwable :cljs :default) _ + nil))) + +(defn clamp01 + [v] + (-> v (max 0.0) (min 1.0))) + +(defn hex-byte + [s] + (safe-parse-double (str "0x" s))) + +(defn normalize-rgba-vector + [v] + (let [[r g b a] (if (= 3 (count v)) + [(nth v 0) (nth v 1) (nth v 2) 1.0] + [(nth v 0) (nth v 1) (nth v 2) (nth v 3)]) + rgb-scale (if (or (> r 1.0) (> g 1.0) (> b 1.0)) 255.0 1.0) + alpha-scale (if (> a 1.0) 255.0 1.0)] + [(clamp01 (/ r rgb-scale)) + (clamp01 (/ g rgb-scale)) + (clamp01 (/ b rgb-scale)) + (clamp01 (/ a alpha-scale))])) + +(defn parse-hex-color + [s] + (let [m (re-matches #"^#([0-9a-fA-F]+)$" (str/trim s))] + (when m + (let [h (second m)] + (case (count h) + 3 (let [r (hex-byte (str (nth h 0) (nth h 0))) + g (hex-byte (str (nth h 1) (nth h 1))) + b (hex-byte (str (nth h 2) (nth h 2)))] + (when (every? finite-number? [r g b]) + (normalize-rgba-vector [r g b 255.0]))) + 4 (let [r (hex-byte (str (nth h 0) (nth h 0))) + g (hex-byte (str (nth h 1) (nth h 1))) + b (hex-byte (str (nth h 2) (nth h 2))) + a (hex-byte (str (nth h 3) (nth h 3)))] + (when (every? finite-number? [r g b a]) + (normalize-rgba-vector [r g b a]))) + 6 (let [r (hex-byte (subs h 0 2)) + g (hex-byte (subs h 2 4)) + b (hex-byte (subs h 4 6))] + (when (every? finite-number? [r g b]) + (normalize-rgba-vector [r g b 255.0]))) + 8 (let [r (hex-byte (subs h 0 2)) + g (hex-byte (subs h 2 4)) + b (hex-byte (subs h 4 6)) + a (hex-byte (subs h 6 8))] + (when (every? finite-number? [r g b a]) + (normalize-rgba-vector [r g b a]))) + nil))))) + +(defn parse-rgba-component + [s] + (let [trimmed (str/trim s)] + (if (str/ends-with? trimmed "%") + (let [n (safe-parse-double (subs trimmed 0 (dec (count trimmed))))] + (when (finite-number? n) (* 255.0 (/ n 100.0)))) + (safe-parse-double trimmed)))) + +(defn parse-alpha-component + [s] + (let [trimmed (str/trim s)] + (if (str/ends-with? trimmed "%") + (let [n (safe-parse-double (subs trimmed 0 (dec (count trimmed))))] + (when (finite-number? n) (/ n 100.0))) + (safe-parse-double trimmed)))) + +(defn parse-rgb-function + [s] + (when-let [[_ fn-name body] (re-matches #"^(rgb|rgba)\((.+)\)$" (str/lower-case (str/trim s)))] + (let [parts (mapv str/trim (str/split body #","))] + (cond + (and (= fn-name "rgb") (= 3 (count parts))) + (let [[r g b] (map parse-rgba-component parts)] + (when (every? finite-number? [r g b]) + (normalize-rgba-vector [r g b 255.0]))) + + (and (= fn-name "rgba") (= 4 (count parts))) + (let [r (parse-rgba-component (nth parts 0)) + g (parse-rgba-component (nth parts 1)) + b (parse-rgba-component (nth parts 2)) + a (parse-alpha-component (nth parts 3))] + (when (every? finite-number? [r g b a]) + (normalize-rgba-vector [r g b a]))) + + :else + nil)))) + +(defn color->rgba + [c] + (cond + (nil? c) + nil + + (vector? c) + (when (contains? #{3 4} (count c)) + (when (every? finite-number? c) + (normalize-rgba-vector c))) + + (string? c) + (or (parse-hex-color c) + (parse-rgb-function c)) + + :else + nil)) + +(defn color-distance + [a b] + (let [va (color->rgba a) + vb (color->rgba b)] + (cond + (and va vb) + (sqrt* (reduce + (map (fn [x y] + (let [d (- x y)] + (* d d))) + va vb))) + + (or va vb) + 1.0 + + :else + 0.0))) + +(defn indexed-mapcat + [f coll] + (mapcat identity (map-indexed f coll))) + +(defn flatten-ir + ([node] (flatten-ir node [])) + ([node path] + (let [children (vec (or (:children node) []))] + (into [{:path path :node node}] + (indexed-mapcat + (fn [idx child] + (flatten-ir child (conj path idx))) + children))))) + +(defn index-by-path + [root] + (reduce (fn [acc {:keys [path node]}] + (assoc acc path node)) + {} + (flatten-ir root))) + +(defn node-depth + [root] + (let [flat (flatten-ir root)] + (if (empty? flat) + 0 + (inc (apply max (map (comp count :path) flat)))))) + +(defn canonical-bounds + [node] + (let [b (:bounds node)] + {:x (double (or (:x b) 0.0)) + :y (double (or (:y b) 0.0)) + :w (double (or (:w b) 0.0)) + :h (double (or (:h b) 0.0))})) + +(defn geometry-diff + "Compare geometry by matching nodes on tree path. + Returns metric payload (without threshold/pass?)." + [source-ir converted-ir] + (let [source-map (index-by-path source-ir) + converted-map (index-by-path converted-ir) + common-paths (sort (set/intersection (set (keys source-map)) + (set (keys converted-map)))) + deviations (vec + (mapcat + (fn [path] + (let [sb (canonical-bounds (get source-map path)) + cb (canonical-bounds (get converted-map path))] + (mapv (fn [k] (abs* (- (get sb k) (get cb k)))) + [:x :y :w :h]))) + common-paths)) + max-dev (if (seq deviations) (apply max deviations) nil) + mean-dev (if (seq deviations) (/ (reduce + deviations) (count deviations)) nil)] + {:max-deviation-px max-dev + :mean-deviation-px mean-dev + :nodes-compared (count common-paths) + :source-node-count (count source-map) + :converted-node-count (count converted-map)})) + +(defn node-color-channels + [node] + (let [fill (get-in node [:visual :fill]) + fill-val (if (map? fill) (:value fill) fill)] + (cond-> {} + fill-val (assoc :fill fill-val) + (get-in node [:visual :border :color]) (assoc :border (get-in node [:visual :border :color])) + (get-in node [:visual :shadow :color]) (assoc :shadow (get-in node [:visual :shadow :color])) + (get-in node [:typography :color]) (assoc :text (get-in node [:typography :color]))))) + +(defn node-scalar-channels + [node] + (let [radius (get-in node [:visual :radius]) + border (get-in node [:visual :border]) + shadow (get-in node [:visual :shadow]) + typography (:typography node) + corners (:corners radius) + widths (:widths border) + offset (:offset shadow)] + (cond-> {} + (finite-number? (get-in node [:visual :opacity])) + (assoc :opacity (double (get-in node [:visual :opacity]))) + + (finite-number? (:uniform radius)) + (assoc :radius/uniform (double (:uniform radius))) + + (and (vector? corners) (= 4 (count corners)) (every? finite-number? corners)) + (assoc :radius/tl (double (nth corners 0)) + :radius/tr (double (nth corners 1)) + :radius/br (double (nth corners 2)) + :radius/bl (double (nth corners 3))) + + (finite-number? (:width border)) + (assoc :border/width (double (:width border))) + + (and (vector? widths) (= 4 (count widths)) (every? finite-number? widths)) + (assoc :border/top (double (nth widths 0)) + :border/right (double (nth widths 1)) + :border/bottom (double (nth widths 2)) + :border/left (double (nth widths 3))) + + (and (vector? offset) (= 2 (count offset)) (every? finite-number? offset)) + (assoc :shadow/offset-x (double (nth offset 0)) + :shadow/offset-y (double (nth offset 1))) + + (finite-number? (:blur shadow)) + (assoc :shadow/blur (double (:blur shadow))) + + (finite-number? (:spread shadow)) + (assoc :shadow/spread (double (:spread shadow))) + + (finite-number? (:size typography)) + (assoc :type/size (double (:size typography))) + + (finite-number? (:weight typography)) + (assoc :type/weight (double (:weight typography))) + + (finite-number? (:line-height typography)) + (assoc :type/line-height (double (:line-height typography))) + + (finite-number? (:letter-spacing typography)) + (assoc :type/letter-spacing (double (:letter-spacing typography)))))) + +(defn style-diff + "Compare style channels by matching nodes on tree path. + Returns metric payload (without threshold/pass?)." + [source-ir converted-ir] + (let [source-map (index-by-path source-ir) + converted-map (index-by-path converted-ir) + common-paths (sort (set/intersection (set (keys source-map)) + (set (keys converted-map)))) + per-path + (mapv + (fn [path] + (let [source-node (get source-map path) + converted-node (get converted-map path) + source-colors (node-color-channels source-node) + converted-colors (node-color-channels converted-node) + source-scalars (node-scalar-channels source-node) + converted-scalars (node-scalar-channels converted-node) + color-keys (sort (set/union (set (keys source-colors)) + (set (keys converted-colors)))) + scalar-keys (sort (set/union (set (keys source-scalars)) + (set (keys converted-scalars)))) + color-deltas (mapv (fn [k] + (color-distance (get source-colors k) + (get converted-colors k))) + color-keys) + scalar-deltas (mapv (fn [k] + (let [a (get source-scalars k) + b (get converted-scalars k)] + (cond + (and (finite-number? a) (finite-number? b)) + (abs* (- a b)) + + (or (finite-number? a) (finite-number? b)) + 1.0 + + :else + 0.0))) + scalar-keys)] + {:path path + :max-color (if (seq color-deltas) (apply max color-deltas) 0.0) + :max-scalar (if (seq scalar-deltas) (apply max scalar-deltas) 0.0)})) + common-paths) + max-color (if (seq per-path) (apply max (map :max-color per-path)) nil) + max-scalar (if (seq per-path) (apply max (map :max-scalar per-path)) nil)] + {:max-color-dist max-color + :max-scalar-diff max-scalar + :nodes-compared (count common-paths)})) + +(defn structure-diff + "Compare structure using path coverage, node budget, and depth budget. + Returns metric payload (without threshold/pass?)." + [source-ir converted-ir] + (let [source-flattened (flatten-ir source-ir) + converted-flattened (flatten-ir converted-ir) + source-paths (set (map :path source-flattened)) + converted-paths (set (map :path converted-flattened)) + source-count (count source-paths) + converted-count (count converted-paths) + common-count (count (set/intersection source-paths converted-paths)) + source-depth (node-depth source-ir) + converted-depth (node-depth converted-ir) + path-coverage (if (zero? source-count) + 1.0 + (/ common-count source-count)) + node-budget-ratio (if (zero? converted-count) + 1.0 + (if (<= converted-count source-count) + 1.0 + (/ source-count converted-count))) + depth-budget-ratio (if (zero? converted-depth) + 1.0 + (if (<= converted-depth source-depth) + 1.0 + (/ source-depth converted-depth))) + similarity-ratio (* path-coverage node-budget-ratio depth-budget-ratio)] + {:similarity-ratio similarity-ratio + :path-coverage path-coverage + :node-budget-ratio node-budget-ratio + :depth-budget-ratio depth-budget-ratio + :source-node-count source-count + :converted-node-count converted-count + :source-depth source-depth + :converted-depth converted-depth})) + +(defn state-style-diff + [source-state converted-state] + (let [source-node {:visual (:visual source-state) + :typography (:typography source-state)} + converted-node {:visual (:visual converted-state) + :typography (:typography converted-state)}] + (style-diff source-node converted-node))) + +(defn state-parity-diff + "Compare state coverage and override-style parity. + Returns metric payload (without threshold/pass?)." + [source-ir converted-ir] + (let [source-states (or (:states source-ir) {}) + converted-states (or (:states converted-ir) {}) + expected-keys (set (keys source-states)) + converted-keys (set (keys converted-states)) + covered-keys (sort (set/intersection expected-keys converted-keys)) + expected (count expected-keys) + covered (count covered-keys) + ratio (if (zero? expected) 1.0 (/ covered expected)) + style-metrics (mapv (fn [state-id] + (state-style-diff (get source-states state-id) + (get converted-states state-id))) + covered-keys) + max-style-color (if (seq style-metrics) + (apply max (map #(or (:max-color-dist %) 0.0) style-metrics)) + 0.0) + max-style-scalar (if (seq style-metrics) + (apply max (map #(or (:max-scalar-diff %) 0.0) style-metrics)) + 0.0)] + {:covered covered + :expected expected + :ratio ratio + :missing-states (vec (sort (set/difference expected-keys converted-keys))) + :extra-states (vec (sort (set/difference converted-keys expected-keys))) + :max-style-color-dist max-style-color + :max-style-scalar-diff max-style-scalar})) + +(defn merge-thresholds + [overrides] + (merge-with merge default-thresholds (or overrides {}))) + +(defn with-threshold-and-pass + [metric metric-name thresholds] + (case metric-name + :geometry + (let [threshold (get-in thresholds [:geometry :max-deviation-px]) + pass? (and (finite-number? (:max-deviation-px metric)) + (<= (:max-deviation-px metric) threshold))] + (assoc metric :threshold threshold :pass? pass?)) + + :style + (let [color-threshold (get-in thresholds [:style :max-color-dist]) + scalar-threshold (get-in thresholds [:style :max-scalar-diff]) + pass? (and (finite-number? (:max-color-dist metric)) + (finite-number? (:max-scalar-diff metric)) + (<= (:max-color-dist metric) color-threshold) + (<= (:max-scalar-diff metric) scalar-threshold))] + (assoc metric + :threshold {:max-color-dist color-threshold + :max-scalar-diff scalar-threshold} + :pass? pass?)) + + :structure + (let [threshold (get-in thresholds [:structure :similarity-ratio]) + within-node-budget? (<= (:converted-node-count metric) + (:source-node-count metric)) + pass? (and (finite-number? (:similarity-ratio metric)) + (>= (:similarity-ratio metric) threshold) + within-node-budget?)] + (assoc metric + :threshold threshold + :within-node-budget? within-node-budget? + :pass? pass?)) + + :states + (let [coverage-threshold (get-in thresholds [:states :coverage-ratio]) + color-threshold (get-in thresholds [:states :max-color-dist]) + scalar-threshold (get-in thresholds [:states :max-scalar-diff]) + pass? (and (finite-number? (:ratio metric)) + (>= (:ratio metric) coverage-threshold) + (<= (:max-style-color-dist metric) color-threshold) + (<= (:max-style-scalar-diff metric) scalar-threshold))] + (assoc metric + :threshold {:coverage-ratio coverage-threshold + :max-color-dist color-threshold + :max-scalar-diff scalar-threshold} + :pass? pass?)))) + +(defn verify-component + "End-to-end deterministic verification. + + Inputs: + - source-ir: canonical source Design IR node + - converted-ir: converted Design IR node (or compiled-then-rehydrated IR) + + Optional opts: + {:component \"shadcn/button\" + :extractor :browser | :llm | :figma + :source-path \"...\" + :timestamp \"ISO-8601\" + :notes [\"...\"] + :thresholds {...}} + + Output report is designed to embed directly into component .edn files." + [source-ir converted-ir & {:keys [component extractor source-path timestamp notes thresholds]}] + (let [source-validation (design-ir/validate-design-ir source-ir) + converted-validation (design-ir/validate-design-ir converted-ir) + valid-inputs? (and (:valid? source-validation) + (:valid? converted-validation)) + merged-thresholds (merge-thresholds thresholds) + base-notes (vec (or notes [])) + validation-notes + (vec + (concat + (when-not (:valid? source-validation) + [(str "Source Design IR is invalid (" (count (:errors source-validation)) " errors).")]) + (when-not (:valid? converted-validation) + [(str "Converted Design IR is invalid (" (count (:errors converted-validation)) " errors).")]))) + metrics + (if valid-inputs? + (let [geometry (with-threshold-and-pass (geometry-diff source-ir converted-ir) :geometry merged-thresholds) + style (with-threshold-and-pass (style-diff source-ir converted-ir) :style merged-thresholds) + structure (with-threshold-and-pass (structure-diff source-ir converted-ir) :structure merged-thresholds) + states (with-threshold-and-pass (state-parity-diff source-ir converted-ir) :states merged-thresholds)] + {:geometry geometry + :style style + :structure structure + :states states}) + {:geometry {:max-deviation-px nil + :mean-deviation-px nil + :threshold (get-in merged-thresholds [:geometry :max-deviation-px]) + :pass? false} + :style {:max-color-dist nil + :max-scalar-diff nil + :threshold (get merged-thresholds :style) + :pass? false} + :structure {:similarity-ratio nil + :threshold (get-in merged-thresholds [:structure :similarity-ratio]) + :pass? false} + :states {:covered nil + :expected nil + :ratio nil + :threshold (get merged-thresholds :states) + :pass? false}}) + metric-pass? (every? true? (map :pass? (vals metrics))) + verdict (if metric-pass? :pass :fail) + all-notes + (vec + (concat + base-notes + validation-notes + (when (and valid-inputs? (not (:pass? (get metrics :states)))) + ["State parity did not satisfy coverage/style thresholds."]) + (when (and valid-inputs? (not (:pass? (get metrics :structure)))) + ["Structure similarity threshold not met or converted node count exceeded source."])))] + {:component component + :timestamp (or timestamp (now-iso)) + :verdict verdict + :metrics metrics + :notes all-notes + :source-path source-path + :extractor extractor + :report-schema verification-report-schema + :thresholds merged-thresholds + :deterministic? true + :authoritative? true + :llm-role :optional-assistant-only})) diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..4f1db0d --- /dev/null +++ b/package-lock.json @@ -0,0 +1,5420 @@ +{ + "name": "softland", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "softland", + "version": "0.0.1", + "license": "ISC", + "dependencies": { + "@codemirror/autocomplete": "^6.0.2", + "@codemirror/commands": "^6.2.2", + "@codemirror/lang-markdown": "6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/search": "^6.0.0", + "@codemirror/state": "^6.0.1", + "@codemirror/view": "^6.9.3", + "@lezer/common": "^1.0.0", + "@lezer/generator": "^1.0.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "@lezer/markdown": "^1.0.2", + "@nextjournal/lezer-clojure": "^1.0.0", + "@radix-ui/themes": "^2.0.3", + "lezer-clojure": "0.1.10", + "prop-types": "15.8.1", + "react": "18.2.0", + "react-dom": "18.2.0", + "recharts": "^2.4.1", + "shadow-cljs": "^2.20.1", + "w3c-keyname": "^2.2.4", + "web-tree-sitter": "^0.22.6" + }, + "devDependencies": { + "karma": "6.4.0", + "karma-chrome-launcher": "3.1.1", + "karma-cljs-test": "0.1.0", + "puppeteer": "15.2.0" + } + }, + "node_modules/@babel/runtime": { + "version": "7.26.9", + "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.26.9.tgz", + "integrity": "sha512-aA63XwOkcl4xxQa3HjPMqOP6LiK0ZDv3mUPYEFXkpHbaFjtGggE1A61FjFzJnB+p7/oy2gA8E+rcBNl/zC1tMg==", + "dependencies": { + "regenerator-runtime": "^0.14.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@codemirror/autocomplete": { + "version": "6.18.6", + "resolved": "https://registry.npmjs.org/@codemirror/autocomplete/-/autocomplete-6.18.6.tgz", + "integrity": "sha512-PHHBXFomUs5DF+9tCOM/UoW6XQ4R44lLNNhRaW9PKPTU0D7lIjRg3ElxaJnTwsl/oHiR93WSXDBrekhoUGCPtg==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@codemirror/commands": { + "version": "6.8.0", + "resolved": "https://registry.npmjs.org/@codemirror/commands/-/commands-6.8.0.tgz", + "integrity": "sha512-q8VPEFaEP4ikSlt6ZxjB3zW72+7osfAYW9i8Zu943uqbKuz6utc1+F170hyLUCUltXORjQXRyYQNfkckzA/bPQ==", + "dependencies": { + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.4.0", + "@codemirror/view": "^6.27.0", + "@lezer/common": "^1.1.0" + } + }, + "node_modules/@codemirror/lang-css": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/@codemirror/lang-css/-/lang-css-6.3.1.tgz", + "integrity": "sha512-kr5fwBGiGtmz6l0LSJIbno9QrifNMUusivHbnA1H6Dmqy4HZFte3UAICix1VuKo0lMPKQr2rqB+0BkKi/S3Ejg==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@lezer/common": "^1.0.2", + "@lezer/css": "^1.1.7" + } + }, + "node_modules/@codemirror/lang-html": { + "version": "6.4.9", + "resolved": "https://registry.npmjs.org/@codemirror/lang-html/-/lang-html-6.4.9.tgz", + "integrity": "sha512-aQv37pIMSlueybId/2PVSP6NPnmurFDVmZwzc7jszd2KAF8qd4VBbvNYPXWQq90WIARjsdVkPbw29pszmHws3Q==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/lang-css": "^6.0.0", + "@codemirror/lang-javascript": "^6.0.0", + "@codemirror/language": "^6.4.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/css": "^1.1.0", + "@lezer/html": "^1.3.0" + } + }, + "node_modules/@codemirror/lang-javascript": { + "version": "6.2.3", + "resolved": "https://registry.npmjs.org/@codemirror/lang-javascript/-/lang-javascript-6.2.3.tgz", + "integrity": "sha512-8PR3vIWg7pSu7ur8A07pGiYHgy3hHj+mRYRCSG8q+mPIrl0F02rgpGv+DsQTHRTc30rydOsf5PZ7yjKFg2Ackw==", + "dependencies": { + "@codemirror/autocomplete": "^6.0.0", + "@codemirror/language": "^6.6.0", + "@codemirror/lint": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.17.0", + "@lezer/common": "^1.0.0", + "@lezer/javascript": "^1.0.0" + } + }, + "node_modules/@codemirror/lang-markdown": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/@codemirror/lang-markdown/-/lang-markdown-6.0.0.tgz", + "integrity": "sha512-ozJaO1W4WgGlwWOoYCSYzbVhhM0YM/4lAWLrNsBbmhh5Ztpl0qm4CgEQRl3t8/YcylTZYBIXiskui8sHNGd4dg==", + "dependencies": { + "@codemirror/lang-html": "^6.0.0", + "@codemirror/language": "^6.0.0", + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "@lezer/common": "^1.0.0", + "@lezer/markdown": "^1.0.0" + } + }, + "node_modules/@codemirror/language": { + "version": "6.10.8", + "resolved": "https://registry.npmjs.org/@codemirror/language/-/language-6.10.8.tgz", + "integrity": "sha512-wcP8XPPhDH2vTqf181U8MbZnW+tDyPYy0UzVOa+oHORjyT+mhhom9vBd7dApJwoDz9Nb/a8kHjJIsuA/t8vNFw==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.23.0", + "@lezer/common": "^1.1.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0", + "style-mod": "^4.0.0" + } + }, + "node_modules/@codemirror/lint": { + "version": "6.8.4", + "resolved": "https://registry.npmjs.org/@codemirror/lint/-/lint-6.8.4.tgz", + "integrity": "sha512-u4q7PnZlJUojeRe8FJa/njJcMctISGgPQ4PnWsd9268R4ZTtU+tfFYmwkBvgcrK2+QQ8tYFVALVb5fVJykKc5A==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.35.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/search": { + "version": "6.5.10", + "resolved": "https://registry.npmjs.org/@codemirror/search/-/search-6.5.10.tgz", + "integrity": "sha512-RMdPdmsrUf53pb2VwflKGHEe1XVM07hI7vV2ntgw1dmqhimpatSJKva4VA9h4TLUDOD4EIF02201oZurpnEFsg==", + "dependencies": { + "@codemirror/state": "^6.0.0", + "@codemirror/view": "^6.0.0", + "crelt": "^1.0.5" + } + }, + "node_modules/@codemirror/state": { + "version": "6.5.2", + "resolved": "https://registry.npmjs.org/@codemirror/state/-/state-6.5.2.tgz", + "integrity": "sha512-FVqsPqtPWKVVL3dPSxy8wEF/ymIEuVzF1PK3VbUgrxXpJUSHQWWZz4JMToquRxnkw+36LTamCZG2iua2Ptq0fA==", + "dependencies": { + "@marijn/find-cluster-break": "^1.0.0" + } + }, + "node_modules/@codemirror/view": { + "version": "6.36.3", + "resolved": "https://registry.npmjs.org/@codemirror/view/-/view-6.36.3.tgz", + "integrity": "sha512-N2bilM47QWC8Hnx0rMdDxO2x2ImJ1FvZWXubwKgjeoOrWwEiFrtpA7SFHcuZ+o2Ze2VzbkgbzWVj4+V18LVkeg==", + "dependencies": { + "@codemirror/state": "^6.5.0", + "style-mod": "^4.1.0", + "w3c-keyname": "^2.2.4" + } + }, + "node_modules/@colors/colors": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/@colors/colors/-/colors-1.5.0.tgz", + "integrity": "sha512-ooWCrlZP11i8GImSjTHYHLkvFDP48nS4+204nGb1RiX/WXYHmJA2III9/e2DWVabCESdW7hBAEzHRqUn9OUVvQ==", + "dev": true, + "engines": { + "node": ">=0.1.90" + } + }, + "node_modules/@floating-ui/core": { + "version": "1.6.9", + "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", + "integrity": "sha512-uMXCuQ3BItDUbAMhIXw7UPXRfAlOAvZzdK9BWpE60MCn+Svt3aLn9jsPTi/WNGlRUu2uI0v5S7JiIUsbsvh3fw==", + "dependencies": { + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/dom": { + "version": "1.6.13", + "resolved": "https://registry.npmjs.org/@floating-ui/dom/-/dom-1.6.13.tgz", + "integrity": "sha512-umqzocjDgNRGTuO7Q8CU32dkHkECqI8ZdMZ5Swb6QAM0t5rnlrN3lGo1hdpscRd3WS8T6DKYK4ephgIH9iRh3w==", + "dependencies": { + "@floating-ui/core": "^1.6.0", + "@floating-ui/utils": "^0.2.9" + } + }, + "node_modules/@floating-ui/react-dom": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz", + "integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==", + "dependencies": { + "@floating-ui/dom": "^1.0.0" + }, + "peerDependencies": { + "react": ">=16.8.0", + "react-dom": ">=16.8.0" + } + }, + "node_modules/@floating-ui/utils": { + "version": "0.2.9", + "resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz", + "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" + }, + "node_modules/@lezer/common": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@lezer/common/-/common-1.2.3.tgz", + "integrity": "sha512-w7ojc8ejBqr2REPsWxJjrMFsA/ysDCFICn8zEOR9mrqzOu2amhITYuLD8ag6XZf0CFXDrhKqw7+tW8cX66NaDA==" + }, + "node_modules/@lezer/css": { + "version": "1.1.10", + "resolved": "https://registry.npmjs.org/@lezer/css/-/css-1.1.10.tgz", + "integrity": "sha512-V5/89eDapjeAkWPBpWEfQjZ1Hag3aYUUJOL8213X0dFRuXJ4BXa5NKl9USzOnaLod4AOpmVCkduir2oKwZYZtg==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/generator": { + "version": "1.7.2", + "resolved": "https://registry.npmjs.org/@lezer/generator/-/generator-1.7.2.tgz", + "integrity": "sha512-CwgULPOPPmH54tv4gki18bElLCdJ1+FBC+nGVSVD08vFWDsMjS7KEjNTph9JOypDnet90ujN3LzQiW3CyVODNQ==", + "dependencies": { + "@lezer/common": "^1.1.0", + "@lezer/lr": "^1.3.0" + }, + "bin": { + "lezer-generator": "src/lezer-generator.cjs" + } + }, + "node_modules/@lezer/highlight": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/@lezer/highlight/-/highlight-1.2.1.tgz", + "integrity": "sha512-Z5duk4RN/3zuVO7Jq0pGLJ3qynpxUVsh7IbUbGj88+uV2ApSAn6kWg2au3iJb+0Zi7kKtqffIESgNcRXWZWmSA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/html": { + "version": "1.3.10", + "resolved": "https://registry.npmjs.org/@lezer/html/-/html-1.3.10.tgz", + "integrity": "sha512-dqpT8nISx/p9Do3AchvYGV3qYc4/rKr3IBZxlHmpIKam56P47RSHkSF5f13Vu9hebS1jM0HmtJIwLbWz1VIY6w==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.0.0", + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@lezer/javascript": { + "version": "1.4.21", + "resolved": "https://registry.npmjs.org/@lezer/javascript/-/javascript-1.4.21.tgz", + "integrity": "sha512-lL+1fcuxWYPURMM/oFZLEDm0XuLN128QPV+VuGtKpeaOGdcl9F2LYC3nh1S9LkPqx9M0mndZFdXCipNAZpzIkQ==", + "dependencies": { + "@lezer/common": "^1.2.0", + "@lezer/highlight": "^1.1.3", + "@lezer/lr": "^1.3.0" + } + }, + "node_modules/@lezer/lr": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/lr/-/lr-1.4.2.tgz", + "integrity": "sha512-pu0K1jCIdnQ12aWNaAVU5bzi7Bd1w54J3ECgANPmYLtQKP0HBj2cE/5coBD66MT10xbtIuUr7tg0Shbsvk0mDA==", + "dependencies": { + "@lezer/common": "^1.0.0" + } + }, + "node_modules/@lezer/markdown": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/@lezer/markdown/-/markdown-1.4.2.tgz", + "integrity": "sha512-iYewCigG/517D0xJPQd7RGaCjZAFwROiH8T9h7OTtz0bRVtkxzFhGBFJ9JGKgBBs4uuo1cvxzyQ5iKhDLMcLUQ==", + "dependencies": { + "@lezer/common": "^1.0.0", + "@lezer/highlight": "^1.0.0" + } + }, + "node_modules/@marijn/find-cluster-break": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@marijn/find-cluster-break/-/find-cluster-break-1.0.2.tgz", + "integrity": "sha512-l0h88YhZFyKdXIFNfSWpyjStDjGHwZ/U7iobcK1cQQD8sejsONdQtTVU+1wVN1PBw40PiiHB1vA5S7VTfQiP9g==" + }, + "node_modules/@nextjournal/lezer-clojure": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/@nextjournal/lezer-clojure/-/lezer-clojure-1.0.0.tgz", + "integrity": "sha512-VZyuGu4zw5mkTOwQBTaGVNWmsOZAPw5ZRxu1/Knk/Xfs7EDBIogwIs5UXTYkuECX5ZQB8eOB+wKA2pc7VyqaZQ==", + "dependencies": { + "@lezer/lr": "^1.0.0" + } + }, + "node_modules/@radix-ui/colors": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/@radix-ui/colors/-/colors-3.0.0.tgz", + "integrity": "sha512-FUOsGBkHrYJwCSEtWRCIfQbZG7q1e6DgxCIOe1SUQzDe/7rXXeA47s8yCn6fuTNQAj1Zq4oTFi9Yjp3wzElcxg==" + }, + "node_modules/@radix-ui/number": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", + "integrity": "sha512-V3gRzhVNU1ldS5XhAPTom1fOIo4ccrjjJgmE+LI2h/WaFpHmx0MQApT+KZHnx8abG6Avtfcz4WoEciMnpFT3HQ==" + }, + "node_modules/@radix-ui/primitive": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.1.1.tgz", + "integrity": "sha512-SJ31y+Q/zAyShtXJc8x83i9TYdbAfHZ++tUZnvjJJqFjzsdUnKsxPL6IEtBlxKkU7yzer//GQtZSV4GbldL3YA==" + }, + "node_modules/@radix-ui/react-accessible-icon": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-accessible-icon/-/react-accessible-icon-1.1.2.tgz", + "integrity": "sha512-+rnMO0SEfzkcHr93RshkQVpOA26MtGOv4pcS9QUnLg4F8+GDmCJ8c2FEPhPz5e7arf31EzbTqJxFbzg3qen14g==", + "dependencies": { + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-alert-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-alert-dialog/-/react-alert-dialog-1.1.6.tgz", + "integrity": "sha512-p4XnPqgej8sZAAReCAKgz1REYZEBLR8hU9Pg27wFnCWIMc8g1ccCs0FjBcy05V15VTu8pAePw/VDYeOm/uZ6yQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dialog": "1.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-arrow": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-arrow/-/react-arrow-1.1.2.tgz", + "integrity": "sha512-G+KcpzXHq24iH0uGG/pF8LyzpFJYGD4RfLjCIBfGdSLXvjLHST31RUiRVrupIBMvIppMgSzQ6l66iAxl03tdlg==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-aspect-ratio": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-aspect-ratio/-/react-aspect-ratio-1.1.2.tgz", + "integrity": "sha512-TaJxYoCpxJ7vfEkv2PTNox/6zzmpKXT6ewvCuf2tTOIVN45/Jahhlld29Yw4pciOXS2Xq91/rSGEdmEnUWZCqA==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-avatar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-avatar/-/react-avatar-1.1.3.tgz", + "integrity": "sha512-Paen00T4P8L8gd9bNsRMw7Cbaz85oxiv+hzomsRZgFm2byltPFDtfcoqlWJ8GyZlIBWgLssJlzLCnKU0G0302g==", + "dependencies": { + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-checkbox": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-checkbox/-/react-checkbox-1.1.4.tgz", + "integrity": "sha512-wP0CPAHq+P5I4INKe3hJrIa1WoNqqrejzW+zoU0rOvo1b9gDEJJFl2rYfO1PYJUQCc2H1WZxIJmyv9BS8i5fLw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-collection": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-collection/-/react-collection-1.1.2.tgz", + "integrity": "sha512-9z54IEKRxIa9VityapoEYMuByaG42iSy1ZXlY2KcuLSEtq8x4987/N6m15ppoMffgZX72gER2uHe1D9Y6Unlcw==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-compose-refs": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.1.1.tgz", + "integrity": "sha512-Y9VzoRDSJtgFMUCoiZBDVo084VQ5hfpXxVE+NgkdNsjiDBByiImMZKKhxMwCbdHvhlENG6a833CbFkOQvTricw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.1.1.tgz", + "integrity": "sha512-UASk9zi+crv9WteK/NU4PLvOoL3OuE6BWVKNF6hPRBtYBDXQ2u5iu3O59zUlJiTVvkyuycnqrztsHVJwcK9K+Q==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-context-menu": { + "version": "2.2.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context-menu/-/react-context-menu-2.2.6.tgz", + "integrity": "sha512-aUP99QZ3VU84NPsHeaFt4cQUNgJqFsLLOt/RbbWXszZ6MP0DpDyjkFZORr4RpAEx3sUBk+Kc8h13yGtC5Qw8dg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dialog": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dialog/-/react-dialog-1.1.6.tgz", + "integrity": "sha512-/IVhJV5AceX620DUJ4uYVMymzsipdKBzo3edo+omeskCKGm9FRHM0ebIdbPnlQVJqyuHbuBltQUOG2mOTq2IYw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-direction": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-direction/-/react-direction-1.1.0.tgz", + "integrity": "sha512-BUuBvgThEiAXh2DWu93XsT+a3aWrGqolGlqqw5VU1kG7p/ZH2cuDlM1sRLNnY3QcBS69UIz2mcKhMxDsdewhjg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dismissable-layer": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dismissable-layer/-/react-dismissable-layer-1.1.5.tgz", + "integrity": "sha512-E4TywXY6UsXNRhFrECa5HAvE5/4BFcGyfTyK36gP+pAW1ed7UTK4vKwdr53gAJYwqbfCWC6ATvJa3J3R/9+Qrg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-escape-keydown": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-dropdown-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-dropdown-menu/-/react-dropdown-menu-2.1.6.tgz", + "integrity": "sha512-no3X7V5fD487wab/ZYSHXq3H37u4NVeLDKI/Ks724X/eEFSSEFYZxWgsIlr1UBeEyDaM29HM5x9p1Nv8DuTYPA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-menu": "2.1.6", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-guards": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-guards/-/react-focus-guards-1.1.1.tgz", + "integrity": "sha512-pSIwfrT1a6sIoDASCSpFwOasEwKTZWDw/iBdtnqKO7v6FeOzYJ7U53cPzYFVR3geGGXgVHaH+CdngrrAzqUGxg==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-focus-scope": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-focus-scope/-/react-focus-scope-1.1.2.tgz", + "integrity": "sha512-zxwE80FCU7lcXUGWkdt6XpTTCKPitG1XKOwViTxHVKIJhZl9MvIl2dVHeZENCWD9+EdWv05wlaEkRXUykU27RA==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-form/-/react-form-0.0.3.tgz", + "integrity": "sha512-kgE+Z/haV6fxE5WqIXj05KkaXa3OkZASoTDy25yX2EIp/x0c54rOH/vFr5nOZTg7n7T1z8bSyXmiVIFP9bbhPQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/primitive": "1.0.1", + "@radix-ui/react-compose-refs": "1.0.1", + "@radix-ui/react-context": "1.0.1", + "@radix-ui/react-id": "1.0.1", + "@radix-ui/react-label": "2.0.2", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/primitive": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/primitive/-/primitive-1.0.1.tgz", + "integrity": "sha512-yQ8oGX2GVsEYMWGxcovu1uGWPCxV5BFfeeYxqPmuAzUyLT9qmaMXSAhXpb0WrspIeqYzdJpkh2vHModJPgRIaw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-context": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-context/-/react-context-1.0.1.tgz", + "integrity": "sha512-ebbrdFoYTcuZ0v4wG5tedGnp9tzcV8awzsxYph7gXUyvnNLuTIcCk1q17JEbnVhXAKG9oX3KtchwiMIAYp9NLg==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-id": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.0.1.tgz", + "integrity": "sha512-tI7sT/kqYp8p96yGWY1OAnLHrqDgzHefRBKQ2YAkBS5ja7QLcZ9Z/uY7bEjPUatf8RomoXM8/1sMj1IJaE5UzQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-use-layout-effect": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-form/node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.0.1.tgz", + "integrity": "sha512-v/5RegiJWYdoCvMnITBkNNx6bCj20fiaJnWtRkU18yITptraXjffz5Qbn05uOiQnOvi+dbkznkoaMltz1GnszQ==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-hover-card": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-hover-card/-/react-hover-card-1.1.6.tgz", + "integrity": "sha512-E4ozl35jq0VRlrdc4dhHrNSV0JqBb4Jy73WAhBEK7JoYnQ83ED5r0Rb/XdVKw89ReAJN38N492BAPBZQ57VmqQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-id": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-id/-/react-id-1.1.0.tgz", + "integrity": "sha512-EJUrI8yYh7WOjNOqpoJaf1jlFIH2LvtgAl+YcFqNCa+4hj64ZXmPkAKOFs/ukjz3byN6bdb/AVUqHkI8/uWWMA==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-label/-/react-label-2.0.2.tgz", + "integrity": "sha512-N5ehvlM7qoTLx7nWPodsPYPgMzA5WM8zZChQg8nyFJKnDO5WHdba1vv5/H6IO5LtJMfD2Q3wh1qHFGNtK0w3bQ==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-primitive": "1.0.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-compose-refs": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@radix-ui/react-compose-refs/-/react-compose-refs-1.0.1.tgz", + "integrity": "sha512-fDSBgd44FKHa1FRMU59qBMPFcl2PZE+2nmqunj+BWFyYYjnhIDWL2ItDs3rrbJDQOtzt5nIebLCQc4QRfz6LJw==", + "dependencies": { + "@babel/runtime": "^7.13.10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-primitive": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-1.0.3.tgz", + "integrity": "sha512-yi58uVyoAcK/Nq1inRY56ZSjKypBNKTa/1mcL8qdl6oJeEaDbOldlzrGn7P6Q3Id5d+SYNGc5AJgc4vGhjs5+g==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-slot": "1.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-label/node_modules/@radix-ui/react-slot": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.0.2.tgz", + "integrity": "sha512-YeTpuq4deV+6DusvVUW4ivBgnkHwECUu0BiN43L5UCDFgdhsRUWAghhTF5MbvNTPzmiFOx90asDSUjWuCNapwg==", + "dependencies": { + "@babel/runtime": "^7.13.10", + "@radix-ui/react-compose-refs": "1.0.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-menu": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-menu/-/react-menu-2.1.6.tgz", + "integrity": "sha512-tBBb5CXDJW3t2mo9WlO7r6GTmWV0F0uzHZVFmlRmYpiSK1CDU5IKojP1pm7oknpBOrFZx/YgBRW9oorPO2S/Lg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popover": { + "version": "1.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popover/-/react-popover-1.1.6.tgz", + "integrity": "sha512-NQouW0x4/GnkFJ/pRqsIS3rM/k97VzKnVb2jB7Gq7VEGPy5g7uNV1ykySFt7eWSp3i2uSGFwaJcvIRJBAHmmFg==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-popper": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-popper/-/react-popper-1.2.2.tgz", + "integrity": "sha512-Rvqc3nOpwseCyj/rgjlJDYAgyfw7OC1tTkKn2ivhaMGcYt8FSBlahHOZak2i3QwkRXUXgGgzeEe2RuqeEHuHgA==", + "dependencies": { + "@floating-ui/react-dom": "^2.0.0", + "@radix-ui/react-arrow": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-rect": "1.1.0", + "@radix-ui/react-use-size": "1.1.0", + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-portal": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@radix-ui/react-portal/-/react-portal-1.1.4.tgz", + "integrity": "sha512-sn2O9k1rPFYVyKd5LAJfo96JlSGVFpa1fS6UuBJfrZadudiw5tAmru+n1x7aMRQ84qDM71Zh1+SzK5QwU0tJfA==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-presence": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-presence/-/react-presence-1.1.2.tgz", + "integrity": "sha512-18TFr80t5EVgL9x1SwF/YGtfG+l0BS0PRAlCWBDoBEiDQjeKgnNZRVJp/oVBl24sr3Gbfwc/Qpj4OcWTQMsAEg==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-primitive": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-primitive/-/react-primitive-2.0.2.tgz", + "integrity": "sha512-Ec/0d38EIuvDF+GZjcMU/Ze6MxntVJYO/fRlCPhCaVUyPY9WTalHJw54tp9sXeJo3tlShWpy41vQRgLRGOuz+w==", + "dependencies": { + "@radix-ui/react-slot": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-radio-group": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-radio-group/-/react-radio-group-1.2.3.tgz", + "integrity": "sha512-xtCsqt8Rp09FK50ItqEqTJ7Sxanz8EM8dnkVIhJrc/wkMMomSmXHvYbhv3E7Zx4oXh98aaLt9W679SUYXg4IDA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-roving-focus": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-roving-focus/-/react-roving-focus-1.1.2.tgz", + "integrity": "sha512-zgMQWkNO169GtGqRvYrzb0Zf8NhMHS2DuEB/TiEmVnpr5OqPU3i8lfbxaAmC2J/KYuIQxyoQQ6DxepyXp61/xw==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-scroll-area": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-scroll-area/-/react-scroll-area-1.2.3.tgz", + "integrity": "sha512-l7+NNBfBYYJa9tNqVcP2AGvxdE3lmE6kFTBXdvHgUaZuy+4wGCL1Cl2AfaR7RKyimj7lZURGLwFO59k4eBnDJQ==", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-select": { + "version": "2.1.6", + "resolved": "https://registry.npmjs.org/@radix-ui/react-select/-/react-select-2.1.6.tgz", + "integrity": "sha512-T6ajELxRvTuAMWH0YmRJ1qez+x4/7Nq7QIx7zJ0VK3qaEWdnWpNbEDnmWldG1zBDwqrLy5aLMUWcoGirVj5kMg==", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-focus-guards": "1.1.1", + "@radix-ui/react-focus-scope": "1.1.2", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-callback-ref": "1.1.0", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2", + "aria-hidden": "^1.2.4", + "react-remove-scroll": "^2.6.3" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-separator": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-separator/-/react-separator-1.1.2.tgz", + "integrity": "sha512-oZfHcaAp2Y6KFBX6I5P1u7CQoy4lheCGiYj+pGFrHy8E/VNRb5E39TkTr3JrV520csPBTZjkuKFdEsjS5EUNKQ==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slider": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slider/-/react-slider-1.2.3.tgz", + "integrity": "sha512-nNrLAWLjGESnhqBqcCNW4w2nn7LxudyMzeB6VgdyAnFLC6kfQgnAjSL2v6UkQTnDctJBlxrmxfplWS4iYjdUTw==", + "dependencies": { + "@radix-ui/number": "1.1.0", + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-collection": "1.1.2", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-layout-effect": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-slot": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-slot/-/react-slot-1.1.2.tgz", + "integrity": "sha512-YAKxaiGsSQJ38VzKH86/BPRC4rh+b1Jpa+JneA5LRE7skmLPNAyeG8kPJj/oo4STLvlrs8vkf/iYyc3A5stYCQ==", + "dependencies": { + "@radix-ui/react-compose-refs": "1.1.1" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-switch": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-switch/-/react-switch-1.1.3.tgz", + "integrity": "sha512-1nc+vjEOQkJVsJtWPSiISGT6OKm4SiOdjMo+/icLxo2G4vxz1GntC5MzfL4v8ey9OEfw787QCD1y3mUv0NiFEQ==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-use-previous": "1.1.0", + "@radix-ui/react-use-size": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tabs": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tabs/-/react-tabs-1.1.3.tgz", + "integrity": "sha512-9mFyI30cuRDImbmFF6O2KUJdgEOsGh9Vmx9x/Dh9tOhL7BngmQPQfwW4aejKm5OHpfWIdmeV6ySyuxoOGjtNng==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-direction": "1.1.0", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-roving-focus": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-tooltip": { + "version": "1.1.8", + "resolved": "https://registry.npmjs.org/@radix-ui/react-tooltip/-/react-tooltip-1.1.8.tgz", + "integrity": "sha512-YAA2cu48EkJZdAMHC0dqo9kialOcRStbtiY4nJPaht7Ptrhcvpo+eDChaM6BIs8kL6a8Z5l5poiqLnXcNduOkA==", + "dependencies": { + "@radix-ui/primitive": "1.1.1", + "@radix-ui/react-compose-refs": "1.1.1", + "@radix-ui/react-context": "1.1.1", + "@radix-ui/react-dismissable-layer": "1.1.5", + "@radix-ui/react-id": "1.1.0", + "@radix-ui/react-popper": "1.2.2", + "@radix-ui/react-portal": "1.1.4", + "@radix-ui/react-presence": "1.1.2", + "@radix-ui/react-primitive": "2.0.2", + "@radix-ui/react-slot": "1.1.2", + "@radix-ui/react-use-controllable-state": "1.1.0", + "@radix-ui/react-visually-hidden": "1.1.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-callback-ref": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-callback-ref/-/react-use-callback-ref-1.1.0.tgz", + "integrity": "sha512-CasTfvsy+frcFkbXtSJ2Zu9JHpN8TYKxkgJGWbjiZhFivxaeW7rMeZt7QELGVLaYVfFMsKHjb7Ak0nMEe+2Vfw==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-controllable-state": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-controllable-state/-/react-use-controllable-state-1.1.0.tgz", + "integrity": "sha512-MtfMVJiSr2NjzS0Aa90NPTnvTSg6C/JLCV7ma0W6+OMV78vd8OyRpID+Ng9LxzsPbLeuBnWBA1Nq30AtBIDChw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-escape-keydown": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-escape-keydown/-/react-use-escape-keydown-1.1.0.tgz", + "integrity": "sha512-L7vwWlR1kTTQ3oh7g1O0CBF3YCyyTj8NmhLR+phShpyA50HCfBFKVJTpshm9PzLiKmehsrQzTYTpX9HvmC9rhw==", + "dependencies": { + "@radix-ui/react-use-callback-ref": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-layout-effect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-layout-effect/-/react-use-layout-effect-1.1.0.tgz", + "integrity": "sha512-+FPE0rOdziWSrH9athwI1R0HDVbWlEhd+FR+aSDk4uWGmSJ9Z54sdZVDQPZAinJhJXwfT+qnj969mCsT2gfm5w==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-previous": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-previous/-/react-use-previous-1.1.0.tgz", + "integrity": "sha512-Z/e78qg2YFnnXcW88A4JmTtm4ADckLno6F7OXotmkQfeuCVaKuYzqAATPhVzl3delXE7CxIV8shofPn3jPc5Og==", + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-rect/-/react-use-rect-1.1.0.tgz", + "integrity": "sha512-0Fmkebhr6PiseyZlYAOtLS+nb7jLmpqTrJyv61Pe68MKYW6OWdRE2kI70TaYY27u7H0lajqM3hSMMLFq18Z7nQ==", + "dependencies": { + "@radix-ui/rect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-use-size": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/react-use-size/-/react-use-size-1.1.0.tgz", + "integrity": "sha512-XW3/vWuIXHa+2Uwcc2ABSfcCledmXhhQPlGbfcRXbiUQI5Icjcg19BGCZVKKInYbvUCut/ufbbLLPFC5cbb1hw==", + "dependencies": { + "@radix-ui/react-use-layout-effect": "1.1.0" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/@radix-ui/react-visually-hidden": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@radix-ui/react-visually-hidden/-/react-visually-hidden-1.1.2.tgz", + "integrity": "sha512-1SzA4ns2M1aRlvxErqhLHsBHoS5eI5UUcI2awAMgGUp4LoaoWOKYmvqDY2s/tltuPkh3Yk77YF/r3IRj+Amx4Q==", + "dependencies": { + "@radix-ui/react-primitive": "2.0.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", + "react-dom": "^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@radix-ui/rect": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@radix-ui/rect/-/rect-1.1.0.tgz", + "integrity": "sha512-A9+lCBZoaMJlVKcRBz2YByCG+Cp2t6nAnMnNba+XiWxnj6r4JUFqfsgwocMBZU9LPtdxC6wB56ySYpc7LQIoJg==" + }, + "node_modules/@radix-ui/themes": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/@radix-ui/themes/-/themes-2.0.3.tgz", + "integrity": "sha512-yaXQ8aWT2P1CQ0Xe6YCRD9HXsfMTvKkrIYkrc4aitCzhGTLS0sjtTqKmrxIWMVA+3DIbEuG9K/8aAMRJBhep8g==", + "dependencies": { + "@radix-ui/colors": "^3.0.0", + "@radix-ui/primitive": "^1.0.1", + "@radix-ui/react-accessible-icon": "^1.0.3", + "@radix-ui/react-alert-dialog": "^1.0.5", + "@radix-ui/react-aspect-ratio": "^1.0.3", + "@radix-ui/react-avatar": "^1.0.4", + "@radix-ui/react-checkbox": "^1.0.4", + "@radix-ui/react-context-menu": "^2.1.5", + "@radix-ui/react-dialog": "^1.0.5", + "@radix-ui/react-direction": "^1.0.1", + "@radix-ui/react-dropdown-menu": "^2.0.6", + "@radix-ui/react-form": "^0.0.3", + "@radix-ui/react-hover-card": "^1.0.7", + "@radix-ui/react-popover": "^1.0.7", + "@radix-ui/react-portal": "^1.0.4", + "@radix-ui/react-radio-group": "^1.1.3", + "@radix-ui/react-scroll-area": "^1.0.5", + "@radix-ui/react-select": "^2.0.0", + "@radix-ui/react-separator": "^1.0.3", + "@radix-ui/react-slider": "^1.1.2", + "@radix-ui/react-slot": "^1.0.2", + "@radix-ui/react-switch": "^1.0.3", + "@radix-ui/react-tabs": "^1.0.4", + "@radix-ui/react-tooltip": "^1.0.7", + "@radix-ui/react-use-callback-ref": "^1.0.1", + "@radix-ui/react-visually-hidden": "^1.0.3", + "classnames": "^2.3.2" + }, + "peerDependencies": { + "@types/react": "*", + "@types/react-dom": "*", + "react": "^16.8 || ^17.0 || ^18.0", + "react-dom": "^16.8 || ^17.0 || ^18.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + }, + "@types/react-dom": { + "optional": true + } + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.2.tgz", + "integrity": "sha512-9BCxFwvbGg/RsZK9tjXd8s4UcwR0MWeFQ1XEKIQVVvAGJyINdrqKMcTRyLoK8Rse1GjzLV9cwjWV1olXRWEXVA==", + "dev": true + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dev": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/d3-array": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/@types/d3-array/-/d3-array-3.2.1.tgz", + "integrity": "sha512-Y2Jn2idRrLzUfAKV2LyRImR+y4oa2AntrgID95SHJxuMUrkNXmanDSed71sRNZysveJVt1hLLemQZIady0FpEg==" + }, + "node_modules/@types/d3-color": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/@types/d3-color/-/d3-color-3.1.3.tgz", + "integrity": "sha512-iO90scth9WAbmgv7ogoq57O9YpKmFBbmoEoCHDB2xMBY0+/KVrqAaCDyCE16dUspeOvIxFFRI+0sEtqDqy2b4A==" + }, + "node_modules/@types/d3-ease": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-ease/-/d3-ease-3.0.2.tgz", + "integrity": "sha512-NcV1JjO5oDzoK26oMzbILE6HW7uVXOHLQvHshBUW4UMdZGfiY6v5BeQwh9a9tCzv+CeefZQHJt5SRgK154RtiA==" + }, + "node_modules/@types/d3-interpolate": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-interpolate/-/d3-interpolate-3.0.4.tgz", + "integrity": "sha512-mgLPETlrpVV1YRJIglr4Ez47g7Yxjl1lj7YKsiMCb27VJH9W8NVM6Bb9d8kkpG/uAQS5AmbA48q2IAolKKo1MA==", + "dependencies": { + "@types/d3-color": "*" + } + }, + "node_modules/@types/d3-path": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/@types/d3-path/-/d3-path-3.1.1.tgz", + "integrity": "sha512-VMZBYyQvbGmWyWVea0EHs/BwLgxc+MKi1zLDCONksozI4YJMcTt8ZEuIR4Sb1MMTE8MMW49v0IwI5+b7RmfWlg==" + }, + "node_modules/@types/d3-scale": { + "version": "4.0.9", + "resolved": "https://registry.npmjs.org/@types/d3-scale/-/d3-scale-4.0.9.tgz", + "integrity": "sha512-dLmtwB8zkAeO/juAMfnV+sItKjlsw2lKdZVVy6LRr0cBmegxSABiLEpGVmSJJ8O08i4+sGR6qQtb6WtuwJdvVw==", + "dependencies": { + "@types/d3-time": "*" + } + }, + "node_modules/@types/d3-shape": { + "version": "3.1.7", + "resolved": "https://registry.npmjs.org/@types/d3-shape/-/d3-shape-3.1.7.tgz", + "integrity": "sha512-VLvUQ33C+3J+8p+Daf+nYSOsjB4GXp19/S/aGo60m9h1v6XaxjiT82lKVWJCfzhtuZ3yD7i/TPeC/fuKLLOSmg==", + "dependencies": { + "@types/d3-path": "*" + } + }, + "node_modules/@types/d3-time": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/@types/d3-time/-/d3-time-3.0.4.tgz", + "integrity": "sha512-yuzZug1nkAAaBlBBikKZTgzCeA+k1uy4ZFwWANOfKw5z5LRhV0gNA7gNkKm7HoK+HRN0wX3EkxGk0fpbWhmB7g==" + }, + "node_modules/@types/d3-timer": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/@types/d3-timer/-/d3-timer-3.0.2.tgz", + "integrity": "sha512-Ps3T8E8dZDam6fUyNiMkekK3XUsaUEik+idO9/YjPtfj2qruF8tFBXS7XhtE4iIXBLxhmLjP3SXpLhVf21I9Lw==" + }, + "node_modules/@types/node": { + "version": "22.13.8", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.13.8.tgz", + "integrity": "sha512-G3EfaZS+iOGYWLLRCEAXdWK9my08oHNZ+FHluRiggIYJPOXzhOiDgpVCUHaUvyIC5/fj7C/p637jdzC666AOKQ==", + "dev": true, + "dependencies": { + "undici-types": "~6.20.0" + } + }, + "node_modules/@types/yauzl": { + "version": "2.10.3", + "resolved": "https://registry.npmjs.org/@types/yauzl/-/yauzl-2.10.3.tgz", + "integrity": "sha512-oJoftv0LSuaDZE3Le4DbKX+KS9G36NzOeSap90UIK0yMA/NhKJhqlSGtNDORNRaIbQfzjXDrQa0ytJ6mNRGz/Q==", + "dev": true, + "optional": true, + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dev": true, + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "dev": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/agent-base/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/agent-base/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/aria-hidden": { + "version": "1.2.4", + "resolved": "https://registry.npmjs.org/aria-hidden/-/aria-hidden-1.2.4.tgz", + "integrity": "sha512-y+CcFFwelSXpLZk/7fMB2mUbGtX9lKycf1MWJ7CaTIERyitVlyQx6C+sxcROU2BAJ24OiZyK+8wj2i8AlBoS3A==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/asn1.js": { + "version": "4.10.1", + "resolved": "https://registry.npmjs.org/asn1.js/-/asn1.js-4.10.1.tgz", + "integrity": "sha512-p32cOF5q0Zqs9uBiONKYLm6BClCoBCM5O9JfeUSlnQLBTxYdTK+pW+nXflm8UkKd2UYlEbYz5qEi0JuZR9ckSw==", + "dependencies": { + "bn.js": "^4.0.0", + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/asn1.js/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/assert": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/assert/-/assert-1.5.1.tgz", + "integrity": "sha512-zzw1uCAgLbsKwBfFc8CX78DDg+xZeBksSO3vwVIDDN5i94eOrPsSSyiVhmsSABFDM/OcpE2aagCat9dnWQLG1A==", + "dependencies": { + "object.assign": "^4.1.4", + "util": "^0.10.4" + } + }, + "node_modules/assert/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/assert/node_modules/util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/util/-/util-0.10.4.tgz", + "integrity": "sha512-0Pm9hTQ3se5ll1XihRic3FDIku70C+iHUdT/W926rSgHV5QgXsYbKZN8MSC3tJtSkhuROzvsQjAaFENRXr+19A==", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64-js": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", + "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "dev": true, + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/bl": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", + "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, + "dependencies": { + "buffer": "^5.5.0", + "inherits": "^2.0.4", + "readable-stream": "^3.4.0" + } + }, + "node_modules/bl/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/bl/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/bn.js": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-5.2.1.tgz", + "integrity": "sha512-eXRvHzWyYPBuB4NBy0cmYQjGitUrtqwbvlzP3G6VFnNRbsZQIxQ10PbKKHt8gZ/HW/D/747aDl+QkDqg3KQLMQ==" + }, + "node_modules/body-parser": { + "version": "1.20.3", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", + "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.5", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.13.0", + "raw-body": "2.5.2", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "1.1.11", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", + "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0", + "concat-map": "0.0.1" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/brorand": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/brorand/-/brorand-1.1.0.tgz", + "integrity": "sha512-cKV8tMCEpQs4hK/ik71d6LrPOnpkpGBR0wzxqr68g2m/LB2GxVYQroAjMJZRVM1Y4BCjCKc3vAamxSzOY2RP+w==" + }, + "node_modules/browserify-aes": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/browserify-aes/-/browserify-aes-1.2.0.tgz", + "integrity": "sha512-+7CHXqGuspUn/Sl5aO7Ea0xWGAtETPXNSAjHo48JfLdPWcMng33Xe4znFvQweqc/uzk5zSOI3H52CYnjCfb5hA==", + "dependencies": { + "buffer-xor": "^1.0.3", + "cipher-base": "^1.0.0", + "create-hash": "^1.1.0", + "evp_bytestokey": "^1.0.3", + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + } + }, + "node_modules/browserify-cipher": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/browserify-cipher/-/browserify-cipher-1.0.1.tgz", + "integrity": "sha512-sPhkz0ARKbf4rRQt2hTpAHqn47X3llLkUGn+xEJzLjwY8LRs2p0v7ljvI5EyoRO/mexrNunNECisZs+gw2zz1w==", + "dependencies": { + "browserify-aes": "^1.0.4", + "browserify-des": "^1.0.0", + "evp_bytestokey": "^1.0.0" + } + }, + "node_modules/browserify-des": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/browserify-des/-/browserify-des-1.0.2.tgz", + "integrity": "sha512-BioO1xf3hFwz4kc6iBhI3ieDFompMhrMlnDFC4/0/vd5MokpuAc3R+LYbwTA9A5Yc9pq9UYPqffKpW2ObuwX5A==", + "dependencies": { + "cipher-base": "^1.0.1", + "des.js": "^1.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/browserify-rsa": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/browserify-rsa/-/browserify-rsa-4.1.1.tgz", + "integrity": "sha512-YBjSAiTqM04ZVei6sXighu679a3SqWORA3qZTEqZImnlkDIFtKc6pNutpjyZ8RJTjQtuYfeetkxM11GwoYXMIQ==", + "dependencies": { + "bn.js": "^5.2.1", + "randombytes": "^2.1.0", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/browserify-sign": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/browserify-sign/-/browserify-sign-4.2.3.tgz", + "integrity": "sha512-JWCZW6SKhfhjJxO8Tyiiy+XYB7cqd2S5/+WeYHsKdNKFlCBhKbblba1A/HN/90YwtxKc8tCErjffZl++UNmGiw==", + "dependencies": { + "bn.js": "^5.2.1", + "browserify-rsa": "^4.1.0", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "elliptic": "^6.5.5", + "hash-base": "~3.0", + "inherits": "^2.0.4", + "parse-asn1": "^5.1.7", + "readable-stream": "^2.3.8", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.12" + } + }, + "node_modules/browserify-zlib": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/browserify-zlib/-/browserify-zlib-0.2.0.tgz", + "integrity": "sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==", + "dependencies": { + "pako": "~1.0.5" + } + }, + "node_modules/buffer": { + "version": "4.9.2", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-4.9.2.tgz", + "integrity": "sha512-xq+q3SRMOxGivLhBNaUdC64hDTQwejJ+H0T/NB1XMtTVEwNTrfFF3gAxiyW0Bu/xWEGhjVKgUcMhCrUy2+uCWg==", + "dependencies": { + "base64-js": "^1.0.2", + "ieee754": "^1.1.4", + "isarray": "^1.0.0" + } + }, + "node_modules/buffer-crc32": { + "version": "0.2.13", + "resolved": "https://registry.npmjs.org/buffer-crc32/-/buffer-crc32-0.2.13.tgz", + "integrity": "sha512-VO9Ht/+p3SN7SKWqcrgEzjGbRSJYTx+Q1pTQC0wrWqHx0vpJraQ6GtHx8tvcg1rlK1byhU5gccxgOgj7B0TDkQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/buffer-xor": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/buffer-xor/-/buffer-xor-1.0.3.tgz", + "integrity": "sha512-571s0T7nZWK6vB67HI5dyUF7wXiNcfaPPPTl6zYCNApANjIvYJTg7hlud/+cJpdAhS7dVzqMLmfhfHR3rAcOjQ==" + }, + "node_modules/builtin-status-codes": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/builtin-status-codes/-/builtin-status-codes-3.0.0.tgz", + "integrity": "sha512-HpGFw18DgFWlncDfjTa2rcQ4W88O1mC8e8yZ2AvQY5KDaktSTwo+KRf6nHK6FRI5FyRyb/5T6+TSxfP7QyGsmQ==" + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.8.tgz", + "integrity": "sha512-oKlSFMcMwpUg2ednkhQ454wfWiU/ul3CkJe/PEHcTKuiX6RpbehUiFMXu13HalGZxfUwCQzZG747YXBn1im9ww==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.0", + "es-define-property": "^1.0.0", + "get-intrinsic": "^1.2.4", + "set-function-length": "^1.2.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/call-bind-apply-helpers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", + "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", + "dependencies": { + "es-errors": "^1.3.0", + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/call-bound": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.3.tgz", + "integrity": "sha512-YTd+6wGlNlPxSuri7Y6X8tY2dmm12UMH66RpKMhiX6rsk5wXXnYgbUcOt8kiS31/AjfoTOvCsE+w8nZQLQnzHA==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "get-intrinsic": "^1.2.6" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/chokidar": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", + "integrity": "sha512-7VT13fmjotKpGipCW9JEQAusEPE+Ei8nl6/g4FBAmIm0GOOLMua9NDDo/DWp0ZAxCr3cPq5ZpBqmPAQgDda2Pw==", + "dev": true, + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "funding": { + "url": "https://paulmillr.com/funding/" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/chownr": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", + "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", + "dev": true + }, + "node_modules/cipher-base": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cipher-base/-/cipher-base-1.0.6.tgz", + "integrity": "sha512-3Ek9H3X6pj5TgenXYtNWdaBon1tgYCaebd+XPg0keyjEbEfkD4KkmAxkQ/i1vYvxdcT5nscLBfq9VJRmCBcFSw==", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/classnames": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/classnames/-/classnames-2.5.1.tgz", + "integrity": "sha512-saHYOzhIQs6wy2sVxTM6bUDsQO4F50V9RQ22qBpEdCW+I+/Wmke2HOl6lS6dTpdxVhb88/I6+Hs+438c3lfUow==" + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/clsx": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/clsx/-/clsx-2.1.1.tgz", + "integrity": "sha512-eYm0QWBtUrBWZWG0d386OGAw16Z995PiOVo2B7bjWSbHedGl5e0ZWaq65kOGgUSNesEIDkB9ISbTg/JK9dhCZA==", + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/concat-map": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", + "dev": true + }, + "node_modules/connect": { + "version": "3.7.0", + "resolved": "https://registry.npmjs.org/connect/-/connect-3.7.0.tgz", + "integrity": "sha512-ZqRXc+tZukToSNmh5C2iWMSoV3X1YUcPbqEM4DkEG5tNQXrQUZCNVGGv3IuicnkMtPfGf3Xtp8WCXs295iQ1pQ==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "finalhandler": "1.1.2", + "parseurl": "~1.3.3", + "utils-merge": "1.0.1" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/console-browserify": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/console-browserify/-/console-browserify-1.2.0.tgz", + "integrity": "sha512-ZMkYO/LkF17QvCPqM0gxw8yUzigAOZOSWSHg91FH6orS7vcEj5dVZTidN2fQ14yBSdg97RqhSNwLUXInd52OTA==" + }, + "node_modules/constants-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/constants-browserify/-/constants-browserify-1.0.0.tgz", + "integrity": "sha512-xFxOwqIzR/e1k1gLiWEophSCMqXcwVHIH7akf7b/vxcUeGunlj3hvZaaqxwHsTgn+IndtkQJgSztIDWeumWJDQ==" + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.2.tgz", + "integrity": "sha512-yki5XnKuf750l50uGTllt6kKILY4nQ1eNIQatoXEByZ5dWgnKqbnqmTrBE5B4N7lrMJKQ2ytWMiTO2o0v6Ew/w==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/core-util-is": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz", + "integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dev": true, + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/create-ecdh": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/create-ecdh/-/create-ecdh-4.0.4.tgz", + "integrity": "sha512-mf+TCx8wWc9VpuxfP2ht0iSISLZnt0JgWlrOKZiNqyUZWnjIaCIVNQArMHnCZKfEYRg6IM7A+NeJoN8gf/Ws0A==", + "dependencies": { + "bn.js": "^4.1.0", + "elliptic": "^6.5.3" + } + }, + "node_modules/create-ecdh/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/create-hash": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/create-hash/-/create-hash-1.2.0.tgz", + "integrity": "sha512-z00bCGNHDG8mHAkP7CtT1qVu+bFQUPjYq/4Iv3C3kWjTFV10zIjfSoeqXo9Asws8gwSHDGj/hl2u4OGIjapeCg==", + "dependencies": { + "cipher-base": "^1.0.1", + "inherits": "^2.0.1", + "md5.js": "^1.3.4", + "ripemd160": "^2.0.1", + "sha.js": "^2.4.0" + } + }, + "node_modules/create-hmac": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/create-hmac/-/create-hmac-1.1.7.tgz", + "integrity": "sha512-MJG9liiZ+ogc4TzUwuvbER1JRdgvUFSB5+VR/g5h82fGaIRWMWddtKBHi7/sVhfjQZ6SehlyhvQYrcYkaUIpLg==", + "dependencies": { + "cipher-base": "^1.0.3", + "create-hash": "^1.1.0", + "inherits": "^2.0.1", + "ripemd160": "^2.0.0", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + } + }, + "node_modules/crelt": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/crelt/-/crelt-1.0.6.tgz", + "integrity": "sha512-VQ2MBenTq1fWZUH9DJNGti7kKv6EeAuYr3cLwxUWhIu1baTaXh4Ib5W2CqHVqib4/MqbYGJqiL3Zb8GJZr3l4g==" + }, + "node_modules/cross-fetch": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/cross-fetch/-/cross-fetch-3.1.5.tgz", + "integrity": "sha512-lvb1SBsI0Z7GDwmuid+mU3kWVBwTVUbe7S0H52yaaAdQOXq2YktTCZdlAcNKFzE6QtRz0snpw9bNiPeOIkkQvw==", + "dev": true, + "dependencies": { + "node-fetch": "2.6.7" + } + }, + "node_modules/crypto-browserify": { + "version": "3.12.1", + "resolved": "https://registry.npmjs.org/crypto-browserify/-/crypto-browserify-3.12.1.tgz", + "integrity": "sha512-r4ESw/IlusD17lgQi1O20Fa3qNnsckR126TdUuBgAu7GBYSIPvdNyONd3Zrxh0xCwA4+6w/TDArBPsMvhur+KQ==", + "dependencies": { + "browserify-cipher": "^1.0.1", + "browserify-sign": "^4.2.3", + "create-ecdh": "^4.0.4", + "create-hash": "^1.2.0", + "create-hmac": "^1.1.7", + "diffie-hellman": "^5.0.3", + "hash-base": "~3.0.4", + "inherits": "^2.0.4", + "pbkdf2": "^3.1.2", + "public-encrypt": "^4.0.3", + "randombytes": "^2.1.0", + "randomfill": "^1.0.4" + }, + "engines": { + "node": ">= 0.10" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/csstype": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.1.3.tgz", + "integrity": "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==" + }, + "node_modules/custom-event": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/custom-event/-/custom-event-1.0.1.tgz", + "integrity": "sha512-GAj5FOq0Hd+RsCGVJxZuKaIDXDf3h6GQoNEjFgbLLI/trgtavwUbSnZ5pVfg27DVCaWjIohryS0JFwIJyT2cMg==", + "dev": true + }, + "node_modules/d3-array": { + "version": "3.2.4", + "resolved": "https://registry.npmjs.org/d3-array/-/d3-array-3.2.4.tgz", + "integrity": "sha512-tdQAmyA18i4J7wprpYq8ClcxZy3SC31QMeByyCFyRt7BVHdREQZ5lpzoe5mFEYZUWe+oq8HBvk9JjpibyEV4Jg==", + "dependencies": { + "internmap": "1 - 2" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-color": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-color/-/d3-color-3.1.0.tgz", + "integrity": "sha512-zg/chbXyeBtMQ1LbD/WSoW2DpC3I0mpmPdW+ynRTj/x2DAWYrIY7qeZIHidozwV24m4iavr15lNwIwLxRmOxhA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-ease": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-ease/-/d3-ease-3.0.1.tgz", + "integrity": "sha512-wR/XK3D3XcLIZwpbvQwQ5fK+8Ykds1ip7A2Txe0yxncXSdq1L9skcG7blcedkOX+ZcgxGAmLX1FrRGbADwzi0w==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-format": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-format/-/d3-format-3.1.0.tgz", + "integrity": "sha512-YyUI6AEuY/Wpt8KWLgZHsIU86atmikuoOmCfommt0LYHiQSPjvX2AcFc38PX0CBpr2RCyZhjex+NS/LPOv6YqA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-interpolate": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-interpolate/-/d3-interpolate-3.0.1.tgz", + "integrity": "sha512-3bYs1rOD33uo8aqJfKP3JWPAibgw8Zm2+L9vBKEHJ2Rg+viTR7o5Mmv5mZcieN+FRYaAOWX5SJATX6k1PWz72g==", + "dependencies": { + "d3-color": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-path": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-path/-/d3-path-3.1.0.tgz", + "integrity": "sha512-p3KP5HCf/bvjBSSKuXid6Zqijx7wIfNW+J/maPs+iwR35at5JCbLUT0LzF1cnjbCHWhqzQTIN2Jpe8pRebIEFQ==", + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-scale": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/d3-scale/-/d3-scale-4.0.2.tgz", + "integrity": "sha512-GZW464g1SH7ag3Y7hXjf8RoUuAFIqklOAq3MRl4OaWabTFJY9PN/E1YklhXLh+OQ3fM9yS2nOkCoS+WLZ6kvxQ==", + "dependencies": { + "d3-array": "2.10.0 - 3", + "d3-format": "1 - 3", + "d3-interpolate": "1.2.0 - 3", + "d3-time": "2.1.1 - 3", + "d3-time-format": "2 - 4" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-shape": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/d3-shape/-/d3-shape-3.2.0.tgz", + "integrity": "sha512-SaLBuwGm3MOViRq2ABk3eLoxwZELpH6zhl3FbAoJ7Vm1gofKx6El1Ib5z23NUEhF9AsGl7y+dzLe5Cw2AArGTA==", + "dependencies": { + "d3-path": "^3.1.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/d3-time/-/d3-time-3.1.0.tgz", + "integrity": "sha512-VqKjzBLejbSMT4IgbmVgDjpkYrNWUYJnbCGo874u7MMKIWsILRX+OpX/gTk8MqjpT1A/c6HY2dCA77ZN0lkQ2Q==", + "dependencies": { + "d3-array": "2 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-time-format": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/d3-time-format/-/d3-time-format-4.1.0.tgz", + "integrity": "sha512-dJxPBlzC7NugB2PDLwo9Q8JiTR3M3e4/XANkreKSUxF8vvXKqm1Yfq4Q5dl8budlunRVlUUaDUgFt7eA8D6NLg==", + "dependencies": { + "d3-time": "1 - 3" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/d3-timer": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz", + "integrity": "sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==", + "engines": { + "node": ">=12" + } + }, + "node_modules/date-format": { + "version": "4.0.14", + "resolved": "https://registry.npmjs.org/date-format/-/date-format-4.0.14.tgz", + "integrity": "sha512-39BOQLs9ZjKh0/patS9nrT8wc3ioX3/eA/zgbKNopnF2wCqJEoxywwwElATYvRsXdnOxA/OQeQoFZ3rFjVajhg==", + "dev": true, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dev": true, + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decimal.js-light": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", + "integrity": "sha512-qIMFpTMZmny+MMIitAB6D7iVPEorVw6YQRWkvarTkT4tBeSLLiHzcwj6q0MmYSFCiVpiqPJTJEYIrpcPzVEIvg==" + }, + "node_modules/define-data-property": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", + "integrity": "sha512-rBMvIzlpA8v6E+SJZoo++HAYqsLrkg7MSfIinMPFhmkorw7X+dOXVJQs+QT69zGkzMyfDnIMN2Wid1+NbL3T+A==", + "dependencies": { + "es-define-property": "^1.0.0", + "es-errors": "^1.3.0", + "gopd": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/define-properties": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/define-properties/-/define-properties-1.2.1.tgz", + "integrity": "sha512-8QmQKqEASLd5nx0U1B1okLElbUuuttJ/AnYmRXbbbGDWh6uS208EjD4Xqq/I9wK7u0v6O08XhTWnt5XtEbR6Dg==", + "dependencies": { + "define-data-property": "^1.0.1", + "has-property-descriptors": "^1.0.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/des.js": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/des.js/-/des.js-1.1.0.tgz", + "integrity": "sha512-r17GxjhUCjSRy8aiJpr8/UadFIzMzJGexI3Nmz4ADi9LYSFx4gTBp80+NaX/YsXWWLhpZ7v/v/ubEc/bCNfKwg==", + "dependencies": { + "inherits": "^2.0.1", + "minimalistic-assert": "^1.0.0" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "dev": true, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/detect-node-es": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/detect-node-es/-/detect-node-es-1.1.0.tgz", + "integrity": "sha512-ypdmJU/TbBby2Dxibuv7ZLW3Bs1QEmM7nHjEANfohJLvE0XVujisn1qPJcZxg+qDucsr+bP6fLD1rPS3AhJ7EQ==" + }, + "node_modules/devtools-protocol": { + "version": "0.0.1011705", + "resolved": "https://registry.npmjs.org/devtools-protocol/-/devtools-protocol-0.0.1011705.tgz", + "integrity": "sha512-OKvTvu9n3swmgYshvsyVHYX0+aPzCoYUnyXUacfQMmFtBtBKewV/gT4I9jkAbpTqtTi2E4S9MXLlvzBDUlqg0Q==", + "dev": true + }, + "node_modules/di": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/di/-/di-0.0.1.tgz", + "integrity": "sha512-uJaamHkagcZtHPqCIHZxnFrXlunQXgBOsZSUOWwFw31QJCAbyTBoHMW75YOTur5ZNx8pIeAKgf6GWIgaqqiLhA==", + "dev": true + }, + "node_modules/diffie-hellman": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/diffie-hellman/-/diffie-hellman-5.0.3.tgz", + "integrity": "sha512-kqag/Nl+f3GwyK25fhUMYj81BUOrZ9IuJsjIcDE5icNM9FJHAVm3VcUDxdLPoQtTuUylWm6ZIknYJwwaPxsUzg==", + "dependencies": { + "bn.js": "^4.1.0", + "miller-rabin": "^4.0.0", + "randombytes": "^2.0.0" + } + }, + "node_modules/diffie-hellman/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/dom-helpers": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/dom-helpers/-/dom-helpers-5.2.1.tgz", + "integrity": "sha512-nRCa7CK3VTrM2NmGkIy4cbK7IZlgBE/PYMn55rrXefr5xXDP0LdtfPnblFDoVdcAfslJ7or6iqAUnx0CCGIWQA==", + "dependencies": { + "@babel/runtime": "^7.8.7", + "csstype": "^3.0.2" + } + }, + "node_modules/dom-serialize": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/dom-serialize/-/dom-serialize-2.2.1.tgz", + "integrity": "sha512-Yra4DbvoW7/Z6LBN560ZwXMjoNOSAN2wRsKFGc4iBeso+mpIA6qj1vfdf9HpMaKAqG6wXTy+1SYEzmNpKXOSsQ==", + "dev": true, + "dependencies": { + "custom-event": "~1.0.0", + "ent": "~2.2.0", + "extend": "^3.0.0", + "void-elements": "^2.0.0" + } + }, + "node_modules/domain-browser": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/domain-browser/-/domain-browser-1.2.0.tgz", + "integrity": "sha512-jnjyiM6eRyZl2H+W8Q/zLMA481hzi0eszAaBUzIVnmYVDBbnLxVNnfu1HgEBvCbL+71FrxMl3E6lpKH7Ge3OXA==", + "engines": { + "node": ">=0.4", + "npm": ">=1.2" + } + }, + "node_modules/dunder-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", + "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.1", + "es-errors": "^1.3.0", + "gopd": "^1.2.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", + "dev": true + }, + "node_modules/elliptic": { + "version": "6.6.1", + "resolved": "https://registry.npmjs.org/elliptic/-/elliptic-6.6.1.tgz", + "integrity": "sha512-RaddvvMatK2LJHqFJ+YA4WysVN5Ita9E35botqIYspQ4TkRAlCicdzKOjlyv/1Za5RyTNn7di//eEV0uTAfe3g==", + "dependencies": { + "bn.js": "^4.11.9", + "brorand": "^1.1.0", + "hash.js": "^1.0.0", + "hmac-drbg": "^1.0.1", + "inherits": "^2.0.4", + "minimalistic-assert": "^1.0.1", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/elliptic/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/end-of-stream": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", + "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", + "dev": true, + "dependencies": { + "once": "^1.4.0" + } + }, + "node_modules/engine.io": { + "version": "6.6.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.6.4.tgz", + "integrity": "sha512-ZCkIjSYNDyGn0R6ewHDtXgns/Zre/NT6Agvq1/WobF7JXgFff4SeDroKiCO3fNJreU9YG429Sc81o4w5ok/W5g==", + "dev": true, + "dependencies": { + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.7.2", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.17.1" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-parser": { + "version": "5.2.3", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.3.tgz", + "integrity": "sha512-HqD3yTBfnBxIrbnM1DoD6Pcq8NECnh8d4As1Qgh0z5Gg3jRRIqijury0CL3ghu/edArpUYiYqQiDUQBIs4np3Q==", + "dev": true, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/engine.io/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/ent": { + "version": "2.2.2", + "resolved": "https://registry.npmjs.org/ent/-/ent-2.2.2.tgz", + "integrity": "sha512-kKvD1tO6BM+oK9HzCPpUdRb4vKFQY/FPTFmurMvh6LlN68VMrdj77w8yp51/kDbpkFOS9J8w5W6zIzgM2H8/hw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.3", + "es-errors": "^1.3.0", + "punycode": "^1.4.1", + "safe-regex-test": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-define-property": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", + "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-errors": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", + "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/es-object-atoms": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", + "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "dependencies": { + "es-errors": "^1.3.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", + "dev": true + }, + "node_modules/eventemitter3": { + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/eventemitter3/-/eventemitter3-4.0.7.tgz", + "integrity": "sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==" + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/evp_bytestokey": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/evp_bytestokey/-/evp_bytestokey-1.0.3.tgz", + "integrity": "sha512-/f2Go4TognH/KvCISP7OUsHn85hT9nUkxxA9BEWxFn+Oj9o8ZNLm/40hdlgSLyuOimsrTKLUMEorQexp/aPQeA==", + "dependencies": { + "md5.js": "^1.3.4", + "safe-buffer": "^5.1.1" + } + }, + "node_modules/extend": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz", + "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", + "dev": true + }, + "node_modules/extract-zip": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/extract-zip/-/extract-zip-2.0.1.tgz", + "integrity": "sha512-GDhU9ntwuKyGXdZBUgTIe+vXnWj0fppUEtMDL0+idd5Sta8TGpHssn/eusA9mrPr9qNDym6SxAYZjNvCn/9RBg==", + "dev": true, + "dependencies": { + "debug": "^4.1.1", + "get-stream": "^5.1.0", + "yauzl": "^2.10.0" + }, + "bin": { + "extract-zip": "cli.js" + }, + "engines": { + "node": ">= 10.17.0" + }, + "optionalDependencies": { + "@types/yauzl": "^2.9.1" + } + }, + "node_modules/extract-zip/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/extract-zip/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/fast-equals": { + "version": "5.2.2", + "resolved": "https://registry.npmjs.org/fast-equals/-/fast-equals-5.2.2.tgz", + "integrity": "sha512-V7/RktU11J3I36Nwq2JnZEM7tNm17eBJz+u25qdxBZeCKiX6BkVSZQjwWIr+IobgnZy+ag73tTZgZi7tr0LrBw==", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/fd-slicer": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/fd-slicer/-/fd-slicer-1.1.0.tgz", + "integrity": "sha512-cE1qsB/VwyQozZ+q1dGxR8LBYNZeofhEdUNGSMbQD3Gw2lAzX9Zb3uIU6Ebc/Fmyjo9AWWfnn0AUCHqtevs/8g==", + "dev": true, + "dependencies": { + "pend": "~1.2.0" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.1.2.tgz", + "integrity": "sha512-aAWcW57uxVNrQZqFXjITpW3sIUQmHGG3qSb9mUah9MgMC4NeWhNOlNjXEYq3HjRAvL6arUviZGGJsBg6z0zsWA==", + "dev": true, + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "~2.3.0", + "parseurl": "~1.3.3", + "statuses": "~1.5.0", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/finalhandler/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flatted": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.3.3.tgz", + "integrity": "sha512-GX+ysw4PBCz0PzosHDepZGANEuFCMLrnRTiEy9McGjmkCQYwRq4A/X786G/fjM/+OjsWSU1ZrY5qyARZmO/uwg==", + "dev": true + }, + "node_modules/follow-redirects": { + "version": "1.15.9", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", + "integrity": "sha512-gew4GsXizNgdoRyqmyfMHyAmXsZDk6mHkSxZFCzW9gwlbtOW44CDtYavM+y+72qD/Vq2l550kMF52DT8fOLJqQ==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://github.com/sponsors/RubenVerborgh" + } + ], + "engines": { + "node": ">=4.0" + }, + "peerDependenciesMeta": { + "debug": { + "optional": true + } + } + }, + "node_modules/fs-constants": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", + "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", + "dev": true + }, + "node_modules/fs-extra": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", + "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", + "dev": true, + "dependencies": { + "graceful-fs": "^4.2.0", + "jsonfile": "^4.0.0", + "universalify": "^0.1.0" + }, + "engines": { + "node": ">=6 <7 || >=8" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", + "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", + "dependencies": { + "call-bind-apply-helpers": "^1.0.2", + "es-define-property": "^1.0.1", + "es-errors": "^1.3.0", + "es-object-atoms": "^1.1.1", + "function-bind": "^1.1.2", + "get-proto": "^1.0.1", + "gopd": "^1.2.0", + "has-symbols": "^1.1.0", + "hasown": "^2.0.2", + "math-intrinsics": "^1.1.0" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-nonce": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-nonce/-/get-nonce-1.0.1.tgz", + "integrity": "sha512-FJhYRoDaiatfEkUK8HKlicmu/3SGFD51q3itKDGoSTysQJBnfOcxU5GxnhE1E6soB76MbT0MBtnKJuXyAx+96Q==", + "engines": { + "node": ">=6" + } + }, + "node_modules/get-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", + "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", + "dependencies": { + "dunder-proto": "^1.0.1", + "es-object-atoms": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/get-stream": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.2.0.tgz", + "integrity": "sha512-nBF+F1rAZVCu/p7rjzgA+Yb4lfYXrpl7a6VmJrU8wF9I1CKvP/QwPNZHnOlwbTkY6dvtFIzFMSyQXbLoTQPRpA==", + "dev": true, + "dependencies": { + "pump": "^3.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/glob": { + "version": "7.2.3", + "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", + "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", + "deprecated": "Glob versions prior to v9 are no longer supported", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^3.1.1", + "once": "^1.3.0", + "path-is-absolute": "^1.0.0" + }, + "engines": { + "node": "*" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", + "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true + }, + "node_modules/has-property-descriptors": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.2.tgz", + "integrity": "sha512-55JNKuIW+vq4Ke1BjOTjM2YctQIvCT7GFzHwmfZPGo5wnrgkid0YQtnAleFSqumZm4az3n2BS+erby5ipJdgrg==", + "dependencies": { + "es-define-property": "^1.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", + "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-tostringtag": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/has-tostringtag/-/has-tostringtag-1.0.2.tgz", + "integrity": "sha512-NqADB8VjPFLM2V0VvHUewwwsw0ZWBaIdgo+ieHtK3hasLz4qeCRjYcqfB6AQrBggRKppKF8L52/VqdVsO47Dlw==", + "dev": true, + "dependencies": { + "has-symbols": "^1.0.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hash-base": { + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/hash-base/-/hash-base-3.0.5.tgz", + "integrity": "sha512-vXm0l45VbcHEVlTCzs8M+s0VeYsB2lnlAaThoLKGXr3bE/VWDOelNUnycUPEhKEaXARL2TEFjBOyUiM6+55KBg==", + "dependencies": { + "inherits": "^2.0.4", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/hash.js": { + "version": "1.1.7", + "resolved": "https://registry.npmjs.org/hash.js/-/hash.js-1.1.7.tgz", + "integrity": "sha512-taOaskGt4z4SOANNseOviYDvjEJinIkRgmp7LbKP2YTTmVxWBl87s/uzK9r+44BclBSp2X7K1hqeNfz9JbBeXA==", + "dependencies": { + "inherits": "^2.0.3", + "minimalistic-assert": "^1.0.1" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/hmac-drbg": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/hmac-drbg/-/hmac-drbg-1.0.1.tgz", + "integrity": "sha512-Tti3gMqLdZfhOQY1Mzf/AanLiqh1WTiJgEj26ZuYQ9fbkLomzGchCws4FyrSd4VkpBfiNhaE1On+lOz894jvXg==", + "dependencies": { + "hash.js": "^1.0.3", + "minimalistic-assert": "^1.0.0", + "minimalistic-crypto-utils": "^1.0.1" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dev": true, + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-errors/node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/http-proxy": { + "version": "1.18.1", + "resolved": "https://registry.npmjs.org/http-proxy/-/http-proxy-1.18.1.tgz", + "integrity": "sha512-7mz/721AbnJwIVbnaSv1Cz3Am0ZLT/UBwkC92VlxhXv/k/BBQfM2fXElQNC27BVGr0uwUpplYPQM9LnaBMR5NQ==", + "dev": true, + "dependencies": { + "eventemitter3": "^4.0.0", + "follow-redirects": "^1.0.0", + "requires-port": "^1.0.0" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/https-browserify": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/https-browserify/-/https-browserify-1.0.0.tgz", + "integrity": "sha512-J+FkSdyD+0mA0N+81tMotaRMfSL9SGi+xpD3T6YApKsc3bGSXJlfXri3VyFOeYkfLRQisDk1W+jIFFKBeUBbBg==" + }, + "node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "dev": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/https-proxy-agent/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/https-proxy-agent/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dev": true, + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/ieee754": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", + "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/internmap": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/internmap/-/internmap-2.0.3.tgz", + "integrity": "sha512-5Hh7Y1wQbvY5ooGgPbDaL5iYLAPzMTUrjMulskHLH6wnv/A+1q5rgEaiuqEjB+oxGXIVZs1FF+R/KPN3ZSQYYg==", + "engines": { + "node": ">=12" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-regex": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/is-regex/-/is-regex-1.2.1.tgz", + "integrity": "sha512-MjYsKHO5O7mCsmRGxWcLWheFqN9DJ/2TmngvjKXihe6efViPqc274+Fx/4fYj/r03+ESvBdTXK0V6tA3rgez1g==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "gopd": "^1.2.0", + "has-tostringtag": "^1.0.2", + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/isarray": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz", + "integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ==" + }, + "node_modules/isbinaryfile": { + "version": "4.0.10", + "resolved": "https://registry.npmjs.org/isbinaryfile/-/isbinaryfile-4.0.10.tgz", + "integrity": "sha512-iHrqe5shvBUcFbmZq9zOQHBoeOhZJu6RQGrDpBgenUm/Am+F3JM2MgQj+rK3Z601fzrL5gLZWtAPH2OBaSVcyw==", + "dev": true, + "engines": { + "node": ">= 8.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/gjtorikian/" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==" + }, + "node_modules/jsonfile": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", + "integrity": "sha512-m6F1R3z8jjlf2imQHS2Qez5sjKWQzbuuhuJ/FKYFRZvPE3PuHcSMVZzfsLhGVOkfd20obL5SWEBew5ShlquNxg==", + "dev": true, + "optionalDependencies": { + "graceful-fs": "^4.1.6" + } + }, + "node_modules/karma": { + "version": "6.4.0", + "resolved": "https://registry.npmjs.org/karma/-/karma-6.4.0.tgz", + "integrity": "sha512-s8m7z0IF5g/bS5ONT7wsOavhW4i4aFkzD4u4wgzAQWT4HGUeWI3i21cK2Yz6jndMAeHETp5XuNsRoyGJZXVd4w==", + "dev": true, + "dependencies": { + "@colors/colors": "1.5.0", + "body-parser": "^1.19.0", + "braces": "^3.0.2", + "chokidar": "^3.5.1", + "connect": "^3.7.0", + "di": "^0.0.1", + "dom-serialize": "^2.2.1", + "glob": "^7.1.7", + "graceful-fs": "^4.2.6", + "http-proxy": "^1.18.1", + "isbinaryfile": "^4.0.8", + "lodash": "^4.17.21", + "log4js": "^6.4.1", + "mime": "^2.5.2", + "minimatch": "^3.0.4", + "mkdirp": "^0.5.5", + "qjobs": "^1.2.0", + "range-parser": "^1.2.1", + "rimraf": "^3.0.2", + "socket.io": "^4.4.1", + "source-map": "^0.6.1", + "tmp": "^0.2.1", + "ua-parser-js": "^0.7.30", + "yargs": "^16.1.1" + }, + "bin": { + "karma": "bin/karma" + }, + "engines": { + "node": ">= 10" + } + }, + "node_modules/karma-chrome-launcher": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/karma-chrome-launcher/-/karma-chrome-launcher-3.1.1.tgz", + "integrity": "sha512-hsIglcq1vtboGPAN+DGCISCFOxW+ZVnIqhDQcCMqqCp+4dmJ0Qpq5QAjkbA0X2L9Mi6OBkHi2Srrbmm7pUKkzQ==", + "dev": true, + "dependencies": { + "which": "^1.2.1" + } + }, + "node_modules/karma-cljs-test": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/karma-cljs-test/-/karma-cljs-test-0.1.0.tgz", + "integrity": "sha512-fd4aLynTv3htQCUS+OV1HfoB9UqYfEVFruKxkfTE3zB2aoSCHD966ZitSSgUeVYahWiaCK0XHZp9cB39t65cLQ==", + "dev": true + }, + "node_modules/lezer": { + "version": "0.13.5", + "resolved": "https://registry.npmjs.org/lezer/-/lezer-0.13.5.tgz", + "integrity": "sha512-cAiMQZGUo2BD8mpcz7Nv1TlKzWP7YIdIRrX41CiP5bk5t4GHxskOxWUx2iAOuHlz8dO+ivbuXr0J1bfHsWD+lQ==", + "deprecated": "This package has been replaced by @lezer/lr", + "dependencies": { + "lezer-tree": "^0.13.2" + } + }, + "node_modules/lezer-clojure": { + "version": "0.1.10", + "resolved": "https://registry.npmjs.org/lezer-clojure/-/lezer-clojure-0.1.10.tgz", + "integrity": "sha512-aBDffG323AyhNJXIXKb9joCVviiJqZ8LPfH/W6SwyLxyzfE71ze77uhXnxZjz0F4agsw/TXxImUan2DXQvZ41A==", + "dependencies": { + "lezer": "^0.13.0" + } + }, + "node_modules/lezer-tree": { + "version": "0.13.2", + "resolved": "https://registry.npmjs.org/lezer-tree/-/lezer-tree-0.13.2.tgz", + "integrity": "sha512-15ZxW8TxVNAOkHIo43Iouv4zbSkQQ5chQHBpwXcD2bBFz46RB4jYLEEww5l1V0xyIx9U2clSyyrLes+hAUFrGQ==", + "deprecated": "This package has been replaced by @lezer/common" + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/log4js": { + "version": "6.9.1", + "resolved": "https://registry.npmjs.org/log4js/-/log4js-6.9.1.tgz", + "integrity": "sha512-1somDdy9sChrr9/f4UlzhdaGfDR2c/SaD2a4T7qEkG4jTS57/B3qmnjLYePwQ8cqWnUHZI0iAKxMBpCZICiZ2g==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "flatted": "^3.2.7", + "rfdc": "^1.3.0", + "streamroller": "^3.1.5" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/log4js/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/log4js/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/math-intrinsics": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", + "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/md5.js": { + "version": "1.3.5", + "resolved": "https://registry.npmjs.org/md5.js/-/md5.js-1.3.5.tgz", + "integrity": "sha512-xitP+WxNPcTTOgnTJcrhM0xvdPepipPSf3I8EIpGKeFLjt3PlJLIDG3u8EX53ZIubkb+5U2+3rELYpEhHhzdkg==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/miller-rabin": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/miller-rabin/-/miller-rabin-4.0.1.tgz", + "integrity": "sha512-115fLhvZVqWwHPbClyntxEVfVDfl9DLLTuJvq3g2O/Oxi8AiNouAHvDSzHS0viUJc+V5vm3eq91Xwqn9dp4jRA==", + "dependencies": { + "bn.js": "^4.0.0", + "brorand": "^1.0.1" + }, + "bin": { + "miller-rabin": "bin/miller-rabin" + } + }, + "node_modules/miller-rabin/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/mime": { + "version": "2.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-2.6.0.tgz", + "integrity": "sha512-USPkMeET31rOMiarsBNIHZKLGgvKc/LrjofAnBlOttf5ajRvqiRA8QsenbcooctK6d6Ts6aqZXBA+XbkKthiQg==", + "dev": true, + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4.0.0" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimalistic-assert": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-assert/-/minimalistic-assert-1.0.1.tgz", + "integrity": "sha512-UtJcAD4yEaGtjPezWuO9wC4nwUnVH/8/Im3yEHQP4b67cXlD/Qr9hdITCU1xDbSEXg2XKNaP8jsReV7vQd00/A==" + }, + "node_modules/minimalistic-crypto-utils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/minimalistic-crypto-utils/-/minimalistic-crypto-utils-1.0.1.tgz", + "integrity": "sha512-JIYlbt6g8i5jKfJ3xz7rF0LXmv2TkDxBLUkiBeZ7bAx4GnnNMr8xFpGnOxn6GhTEHx3SjRrZEoU+j04prX1ktg==" + }, + "node_modules/minimatch": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", + "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", + "dev": true, + "dependencies": { + "brace-expansion": "^1.1.7" + }, + "engines": { + "node": "*" + } + }, + "node_modules/minimist": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", + "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/mkdirp": { + "version": "0.5.6", + "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.6.tgz", + "integrity": "sha512-FP+p8RB8OWpF3YZBCrP5gtADmtXApB5AMLn+vdyA+PyxCjrCs00mjyUozssO33cwDeT3wNGdLxJ5M//YqtHAJw==", + "dev": true, + "dependencies": { + "minimist": "^1.2.6" + }, + "bin": { + "mkdirp": "bin/cmd.js" + } + }, + "node_modules/mkdirp-classic": { + "version": "0.5.3", + "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", + "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", + "dev": true + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", + "dev": true + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/node-fetch": { + "version": "2.6.7", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.6.7.tgz", + "integrity": "sha512-ZjMPFEfVx5j+y2yF35Kzx5sF7kDzxuDj6ziH4FFbOp87zKDZNx8yExJIb05OGF4Nlt9IHFIMBkRl41VdvcNdbQ==", + "dev": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-libs-browser": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/node-libs-browser/-/node-libs-browser-2.2.1.tgz", + "integrity": "sha512-h/zcD8H9kaDZ9ALUWwlBUDo6TKF8a7qBSCSEGfjTVIYeqsioSKaAX+BN7NgiMGp6iSIXZ3PxgCu8KS3b71YK5Q==", + "dependencies": { + "assert": "^1.1.1", + "browserify-zlib": "^0.2.0", + "buffer": "^4.3.0", + "console-browserify": "^1.1.0", + "constants-browserify": "^1.0.0", + "crypto-browserify": "^3.11.0", + "domain-browser": "^1.1.1", + "events": "^3.0.0", + "https-browserify": "^1.0.0", + "os-browserify": "^0.3.0", + "path-browserify": "0.0.1", + "process": "^0.11.10", + "punycode": "^1.2.4", + "querystring-es3": "^0.2.0", + "readable-stream": "^2.3.3", + "stream-browserify": "^2.0.1", + "stream-http": "^2.7.2", + "string_decoder": "^1.0.0", + "timers-browserify": "^2.0.4", + "tty-browserify": "0.0.0", + "url": "^0.11.0", + "util": "^0.11.0", + "vm-browserify": "^1.0.1" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.4", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", + "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/object-keys": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/object-keys/-/object-keys-1.1.1.tgz", + "integrity": "sha512-NuAESUOUMrlIXOfHKzD6bpPu3tYt3xvjNdRIQ+FeT0lNb4K8WR70CaDxhuNguS2XG+GjkyMwOzsN5ZktImfhLA==", + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/object.assign": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/object.assign/-/object.assign-4.1.7.tgz", + "integrity": "sha512-nK28WOo+QIjBkDduTINE4JkF/UJJKyf2EJxvJKfblDpyg0Q+pkOHNTL0Qwy6NP6FhE/EnzV73BxxqcJaXY9anw==", + "dependencies": { + "call-bind": "^1.0.8", + "call-bound": "^1.0.3", + "define-properties": "^1.2.1", + "es-object-atoms": "^1.0.0", + "has-symbols": "^1.1.0", + "object-keys": "^1.1.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dev": true, + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/os-browserify": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/os-browserify/-/os-browserify-0.3.0.tgz", + "integrity": "sha512-gjcpUc3clBf9+210TRaDWbf+rZZZEshZ+DlXMRCeAjp0xhTrnQsKHypIy1J3d5hKdUzj69t708EHtU8P6bUn0A==" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pako": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/pako/-/pako-1.0.11.tgz", + "integrity": "sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==" + }, + "node_modules/parse-asn1": { + "version": "5.1.7", + "resolved": "https://registry.npmjs.org/parse-asn1/-/parse-asn1-5.1.7.tgz", + "integrity": "sha512-CTM5kuWR3sx9IFamcl5ErfPl6ea/N8IYwiJ+vpeB2g+1iknv7zBl5uPwbMbRVznRVbrNY6lGuDoE5b30grmbqg==", + "dependencies": { + "asn1.js": "^4.10.1", + "browserify-aes": "^1.2.0", + "evp_bytestokey": "^1.0.3", + "hash-base": "~3.0", + "pbkdf2": "^3.1.2", + "safe-buffer": "^5.2.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-browserify": { + "version": "0.0.1", + "resolved": "https://registry.npmjs.org/path-browserify/-/path-browserify-0.0.1.tgz", + "integrity": "sha512-BapA40NHICOS+USX9SN4tyhq+A2RrN/Ws5F0Z5aMHDp98Fl86lX8Oti8B7uN93L4Ifv4fHOEA+pQw87gmMO/lQ==" + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-is-absolute": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", + "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/pbkdf2": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/pbkdf2/-/pbkdf2-3.1.2.tgz", + "integrity": "sha512-iuh7L6jA7JEGu2WxDwtQP1ddOpaJNC4KlDEFfdQajSGgGPNi4OyDc2R7QnbY2bR9QjBVGwgvTdNJZoE7RaxUMA==", + "dependencies": { + "create-hash": "^1.1.2", + "create-hmac": "^1.1.4", + "ripemd160": "^2.0.1", + "safe-buffer": "^5.0.1", + "sha.js": "^2.4.8" + }, + "engines": { + "node": ">=0.12" + } + }, + "node_modules/pend": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/pend/-/pend-1.2.0.tgz", + "integrity": "sha512-F3asv42UuXchdzt+xXqfW1OGlVBe+mxa2mqI0pg5yAHZPvFmY3Y6drSf/GQ1A86WgWEN9Kzh/WrgKa6iGcHXLg==", + "dev": true + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/process": { + "version": "0.11.10", + "resolved": "https://registry.npmjs.org/process/-/process-0.11.10.tgz", + "integrity": "sha512-cdGef/drWFoydD1JsMzuFf8100nZl+GT+yacc2bEced5f9Rjk4z+WtFUTBu9PhOi9j/jfmBPu0mMEY4wIdAF8A==", + "engines": { + "node": ">= 0.6.0" + } + }, + "node_modules/process-nextick-args": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz", + "integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag==" + }, + "node_modules/progress": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/progress/-/progress-2.0.3.tgz", + "integrity": "sha512-7PiHtLll5LdnKIMw100I+8xJXR5gW2QwWYkT6iJva0bXitZKa/XMrSbdmg3r2Xnaidz9Qumd0VPaMrZlF9V9sA==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/prop-types": { + "version": "15.8.1", + "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", + "integrity": "sha512-oj87CgZICdulUohogVAR7AjlC0327U4el4L6eAvOqCeudMDVU0NThNaV+b9Df4dXgSP1gXMTnPdhfe/2qDH5cg==", + "dependencies": { + "loose-envify": "^1.4.0", + "object-assign": "^4.1.1", + "react-is": "^16.13.1" + } + }, + "node_modules/proxy-from-env": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/proxy-from-env/-/proxy-from-env-1.1.0.tgz", + "integrity": "sha512-D+zkORCbA9f1tdWRK0RaCR3GPv50cMxcrz4X8k5LTSUD1Dkw47mKJEZQNunItRTkWwgtaUSo1RVFRIG9ZXiFYg==", + "dev": true + }, + "node_modules/public-encrypt": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/public-encrypt/-/public-encrypt-4.0.3.tgz", + "integrity": "sha512-zVpa8oKZSz5bTMTFClc1fQOnyyEzpl5ozpi1B5YcvBrdohMjH2rfsBtyXcuNuwjsDIXmBYlF2N5FlJYhR29t8Q==", + "dependencies": { + "bn.js": "^4.1.0", + "browserify-rsa": "^4.0.0", + "create-hash": "^1.1.0", + "parse-asn1": "^5.0.0", + "randombytes": "^2.0.1", + "safe-buffer": "^5.1.2" + } + }, + "node_modules/public-encrypt/node_modules/bn.js": { + "version": "4.12.1", + "resolved": "https://registry.npmjs.org/bn.js/-/bn.js-4.12.1.tgz", + "integrity": "sha512-k8TVBiPkPJT9uHLdOKfFpqcfprwBFOAAXXozRubr7R7PfIuKvQlzcI4M0pALeqXN09vdaMbUdUj+pass+uULAg==" + }, + "node_modules/pump": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.2.tgz", + "integrity": "sha512-tUPXtzlGM8FE3P0ZL6DVs/3P58k9nk8/jZeQCurTJylQA8qFYzHFfhBJkuqyE0FifOsQ0uKWekiZ5g8wtr28cw==", + "dev": true, + "dependencies": { + "end-of-stream": "^1.1.0", + "once": "^1.3.1" + } + }, + "node_modules/punycode": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz", + "integrity": "sha512-jmYNElW7yvO7TV33CjSmvSiE2yco3bV2czu/OzDKdMNVZQWfxCblURLhf+47syQRBntjfLdd/H0egrzIG+oaFQ==" + }, + "node_modules/puppeteer": { + "version": "15.2.0", + "resolved": "https://registry.npmjs.org/puppeteer/-/puppeteer-15.2.0.tgz", + "integrity": "sha512-6Mzj5pbq4J4DxJE5o6V+arrOB9Gma0CxOLP1zKYMrMR7AYuNaPzsK7pBrpDwI64W6Mxk5G7NqiLSFTrgSzR1zg==", + "deprecated": "< 22.8.2 is no longer supported", + "dev": true, + "hasInstallScript": true, + "dependencies": { + "cross-fetch": "3.1.5", + "debug": "4.3.4", + "devtools-protocol": "0.0.1011705", + "extract-zip": "2.0.1", + "https-proxy-agent": "5.0.1", + "pkg-dir": "4.2.0", + "progress": "2.0.3", + "proxy-from-env": "1.1.0", + "rimraf": "3.0.2", + "tar-fs": "2.1.1", + "unbzip2-stream": "1.4.3", + "ws": "8.8.0" + }, + "engines": { + "node": ">=14.1.0" + } + }, + "node_modules/puppeteer/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/puppeteer/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/qjobs": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/qjobs/-/qjobs-1.2.0.tgz", + "integrity": "sha512-8YOJEHtxpySA3fFDyCRxA+UUV+fA+rTWnuWvylOK/NCjhY+b4ocCtmu8TtsWb+mYeU+GCHf/S66KZF/AsteKHg==", + "dev": true, + "engines": { + "node": ">=0.9" + } + }, + "node_modules/qs": { + "version": "6.13.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", + "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", + "dependencies": { + "side-channel": "^1.0.6" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/querystring-es3": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/querystring-es3/-/querystring-es3-0.2.1.tgz", + "integrity": "sha512-773xhDQnZBMFobEiztv8LIl70ch5MSF/jUQVlhwFyBILqq96anmoctVIYz+ZRp0qbCKATTn6ev02M3r7Ga5vqA==", + "engines": { + "node": ">=0.4.x" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/randomfill": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/randomfill/-/randomfill-1.0.4.tgz", + "integrity": "sha512-87lcbR8+MhcWcUiQ+9e+Rwx8MyR2P7qnt15ynUlbm3TU/fjbgz4GsvfSUDTemtCCtVCqb4ZcEFlyPNTh9bBTLw==", + "dependencies": { + "randombytes": "^2.0.5", + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", + "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", + "dev": true, + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/react": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react/-/react-18.2.0.tgz", + "integrity": "sha512-/3IjMdb2L9QbBdWiW5e3P2/npwMBaU9mHCSCUzNln0ZCYbcfTsGbTJrU/kGemdH2IWmB2ioZ+zkxtmq6g09fGQ==", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/react-dom": { + "version": "18.2.0", + "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.2.0.tgz", + "integrity": "sha512-6IMTriUmvsjHUjNtEDudZfuDQUoWXVxKHhlEGSk81n4YFS+r/Kl99wXiwlVXtPBtJenozv2P+hxDsw9eA7Xo6g==", + "dependencies": { + "loose-envify": "^1.1.0", + "scheduler": "^0.23.0" + }, + "peerDependencies": { + "react": "^18.2.0" + } + }, + "node_modules/react-is": { + "version": "16.13.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-16.13.1.tgz", + "integrity": "sha512-24e6ynE2H+OKt4kqsOvNd8kBpV65zoxbA4BVsEOB3ARVWQki/DHzaUoC5KuON/BiccDaCCTZBuOcfZs70kR8bQ==" + }, + "node_modules/react-remove-scroll": { + "version": "2.6.3", + "resolved": "https://registry.npmjs.org/react-remove-scroll/-/react-remove-scroll-2.6.3.tgz", + "integrity": "sha512-pnAi91oOk8g8ABQKGF5/M9qxmmOPxaAnopyTHYfqYEwJhyFrbbBtHuSgtKEoH0jpcxx5o3hXqH1mNd9/Oi+8iQ==", + "dependencies": { + "react-remove-scroll-bar": "^2.3.7", + "react-style-singleton": "^2.2.3", + "tslib": "^2.1.0", + "use-callback-ref": "^1.3.3", + "use-sidecar": "^1.1.3" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-remove-scroll-bar": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/react-remove-scroll-bar/-/react-remove-scroll-bar-2.3.8.tgz", + "integrity": "sha512-9r+yi9+mgU33AKcj6IbT9oRCO78WriSj6t/cF8DWBZJ9aOGPOTEDvdUDz1FwKim7QXWwmHqtdHnRJfhAxEG46Q==", + "dependencies": { + "react-style-singleton": "^2.2.2", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-smooth": { + "version": "4.0.4", + "resolved": "https://registry.npmjs.org/react-smooth/-/react-smooth-4.0.4.tgz", + "integrity": "sha512-gnGKTpYwqL0Iii09gHobNolvX4Kiq4PKx6eWBCYYix+8cdw+cGo3do906l1NBPKkSWx1DghC1dlWG9L2uGd61Q==", + "dependencies": { + "fast-equals": "^5.0.1", + "prop-types": "^15.8.1", + "react-transition-group": "^4.4.5" + }, + "peerDependencies": { + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/react-style-singleton": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/react-style-singleton/-/react-style-singleton-2.2.3.tgz", + "integrity": "sha512-b6jSvxvVnyptAiLjbkWLE/lOnR4lfTtDAl+eUC7RZy+QQWc6wRzIV2CE6xBuMmDxc2qIihtDCZD5NPOFl7fRBQ==", + "dependencies": { + "get-nonce": "^1.0.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/react-transition-group": { + "version": "4.4.5", + "resolved": "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz", + "integrity": "sha512-pZcd1MCJoiKiBR2NRxeCRg13uCXbydPnmB4EOeRrY7480qNWO8IIgQG6zlDkm6uRMsURXPuKq0GWtiM59a5Q6g==", + "dependencies": { + "@babel/runtime": "^7.5.5", + "dom-helpers": "^5.0.1", + "loose-envify": "^1.4.0", + "prop-types": "^15.6.2" + }, + "peerDependencies": { + "react": ">=16.6.0", + "react-dom": ">=16.6.0" + } + }, + "node_modules/readable-stream": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.8.tgz", + "integrity": "sha512-8p0AUk4XODgIewSi0l8Epjs+EVnWiK7NoDIEGU0HhE7+ZyY8D1IMY7odu5lRrFXGg71L15KG8QrPmum45RTtdA==", + "dependencies": { + "core-util-is": "~1.0.0", + "inherits": "~2.0.3", + "isarray": "~1.0.0", + "process-nextick-args": "~2.0.0", + "safe-buffer": "~5.1.1", + "string_decoder": "~1.1.1", + "util-deprecate": "~1.0.1" + } + }, + "node_modules/readable-stream/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/readable-stream/node_modules/string_decoder": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.1.1.tgz", + "integrity": "sha512-n/ShnvDi6FHbbVfviro+WojiFzv+s8MPMHBczVePfUpDJLwoLT0ht1l4YwBCbi8pJAveEEdnkHyPyTP/mzRfwg==", + "dependencies": { + "safe-buffer": "~5.1.0" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/readline-sync": { + "version": "1.4.10", + "resolved": "https://registry.npmjs.org/readline-sync/-/readline-sync-1.4.10.tgz", + "integrity": "sha512-gNva8/6UAe8QYepIQH/jQ2qn91Qj0B9sYjMBBs3QOB8F2CXcKgLxQaJRP76sWVRQt+QU+8fAkCbCvjjMFu7Ycw==", + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/recharts": { + "version": "2.15.1", + "resolved": "https://registry.npmjs.org/recharts/-/recharts-2.15.1.tgz", + "integrity": "sha512-v8PUTUlyiDe56qUj82w/EDVuzEFXwEHp9/xOowGAZwfLjB9uAy3GllQVIYMWF6nU+qibx85WF75zD7AjqoT54Q==", + "dependencies": { + "clsx": "^2.0.0", + "eventemitter3": "^4.0.1", + "lodash": "^4.17.21", + "react-is": "^18.3.1", + "react-smooth": "^4.0.4", + "recharts-scale": "^0.4.4", + "tiny-invariant": "^1.3.1", + "victory-vendor": "^36.6.8" + }, + "engines": { + "node": ">=14" + }, + "peerDependencies": { + "react": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", + "react-dom": "^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0" + } + }, + "node_modules/recharts-scale": { + "version": "0.4.5", + "resolved": "https://registry.npmjs.org/recharts-scale/-/recharts-scale-0.4.5.tgz", + "integrity": "sha512-kivNFO+0OcUNu7jQquLXAxz1FIwZj8nrj+YkOKc5694NbjCvcT6aSZiIzNzd2Kul4o4rTto8QVR9lMNtxD4G1w==", + "dependencies": { + "decimal.js-light": "^2.4.1" + } + }, + "node_modules/recharts/node_modules/react-is": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react-is/-/react-is-18.3.1.tgz", + "integrity": "sha512-/LLMVyas0ljjAtoYiPqYiL8VWXzUUdThrmU5+n20DZv+a+ClRoevUzw5JxU+Ieh5/c87ytoTBV9G1FiKfNJdmg==" + }, + "node_modules/regenerator-runtime": { + "version": "0.14.1", + "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", + "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/requires-port": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", + "integrity": "sha512-KigOCHcocU3XODJxsu8i/j8T9tzT4adHiecwORRQ0ZZFcp7ahwXuRU1m+yuO90C5ZUyGeGfocHDI14M3L3yDAQ==", + "dev": true + }, + "node_modules/rfdc": { + "version": "1.4.1", + "resolved": "https://registry.npmjs.org/rfdc/-/rfdc-1.4.1.tgz", + "integrity": "sha512-q1b3N5QkRUWUl7iyylaaj3kOpIT0N2i9MqIEQXP73GVsN9cw3fdx8X63cEmWhJGi2PPCF23Ijp7ktmd39rawIA==", + "dev": true + }, + "node_modules/rimraf": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-3.0.2.tgz", + "integrity": "sha512-JZkJMZkAGFFPP2YqXZXPbMlMBgsxzE8ILs4lMIX/2o0L9UBw9O/Y3o6wFw/i9YLapcUJWwqbi3kdxIPdC62TIA==", + "deprecated": "Rimraf versions prior to v4 are no longer supported", + "dev": true, + "dependencies": { + "glob": "^7.1.3" + }, + "bin": { + "rimraf": "bin.js" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/ripemd160": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/ripemd160/-/ripemd160-2.0.2.tgz", + "integrity": "sha512-ii4iagi25WusVoiC4B4lq7pbXfAp3D9v5CwfkY33vffw2+pkDjY1D8GaN7spsxvCSx8dkPqOZCEZyfxcmJG2IA==", + "dependencies": { + "hash-base": "^3.0.0", + "inherits": "^2.0.1" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safe-regex-test": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/safe-regex-test/-/safe-regex-test-1.1.0.tgz", + "integrity": "sha512-x/+Cz4YrimQxQccJf5mKEbIa1NzeCRNI5Ecl/ekmlYaampdNLPalVyIcCZNNH3MvmqBugV5TMYZXv0ljslUlaw==", + "dev": true, + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "is-regex": "^1.2.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", + "dev": true + }, + "node_modules/scheduler": { + "version": "0.23.2", + "resolved": "https://registry.npmjs.org/scheduler/-/scheduler-0.23.2.tgz", + "integrity": "sha512-UOShsPwz7NrMUqhR6t0hWjFduvOzbtv7toDH1/hIrfRNIDBnnBWd0CwJTGvTpngVlmwGCdP9/Zl/tVrDqcuYzQ==", + "dependencies": { + "loose-envify": "^1.1.0" + } + }, + "node_modules/set-function-length": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", + "integrity": "sha512-pgRc4hJ4/sNjWCSS9AmnS40x3bNMDTknHgL5UaMBTMyJnU90EgWh1Rz+MC9eFu4BuN/UwZjKQuY/1v3rM7HMfg==", + "dependencies": { + "define-data-property": "^1.1.4", + "es-errors": "^1.3.0", + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.4", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setimmediate": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/setimmediate/-/setimmediate-1.0.5.tgz", + "integrity": "sha512-MATJdZp8sLqDl/68LfQmbP8zKPLQNV6BIZoIgrscFDQ+RsvK/BxeDQOgyxKKoh0y/8h3BqVFnCqQ/gd+reiIXA==" + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", + "dev": true + }, + "node_modules/sha.js": { + "version": "2.4.11", + "resolved": "https://registry.npmjs.org/sha.js/-/sha.js-2.4.11.tgz", + "integrity": "sha512-QMEp5B7cftE7APOjk5Y6xgrbWu+WkLVQwk8JNjZ8nKRciZaByEW6MubieAiToS7+dwvrjGhH8jRXz3MVd0AYqQ==", + "dependencies": { + "inherits": "^2.0.1", + "safe-buffer": "^5.0.1" + }, + "bin": { + "sha.js": "bin.js" + } + }, + "node_modules/shadow-cljs": { + "version": "2.28.21", + "resolved": "https://registry.npmjs.org/shadow-cljs/-/shadow-cljs-2.28.21.tgz", + "integrity": "sha512-O5VUJkTh0bWqPBSKoWnQwEe/jfvbxHkzCA7SEx8f1Eavb7nDFcoNFDkgGjJtaAyaaSw/cmABrT2EeksnXw/25g==", + "dependencies": { + "node-libs-browser": "^2.2.1", + "readline-sync": "^1.4.7", + "shadow-cljs-jar": "1.3.4", + "source-map-support": "^0.4.15", + "which": "^1.3.1", + "ws": "^7.4.6" + }, + "bin": { + "shadow-cljs": "cli/runner.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/shadow-cljs-jar": { + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/shadow-cljs-jar/-/shadow-cljs-jar-1.3.4.tgz", + "integrity": "sha512-cZB2pzVXBnhpJ6PQdsjO+j/MksR28mv4QD/hP/2y1fsIa9Z9RutYgh3N34FZ8Ktl4puAXaIGlct+gMCJ5BmwmA==" + }, + "node_modules/shadow-cljs/node_modules/ws": { + "version": "7.5.10", + "resolved": "https://registry.npmjs.org/ws/-/ws-7.5.10.tgz", + "integrity": "sha512-+dbF1tHwZpXcbOJdVOkzLDxZP1ailvSxM6ZweXTegylPny803bFhA+vqBYw4s31NSAk4S2Qz+AKXK9a4wkdjcQ==", + "engines": { + "node": ">=8.3.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/side-channel": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", + "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3", + "side-channel-list": "^1.0.0", + "side-channel-map": "^1.0.1", + "side-channel-weakmap": "^1.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-list": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", + "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", + "dependencies": { + "es-errors": "^1.3.0", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-map": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", + "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/side-channel-weakmap": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", + "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", + "dependencies": { + "call-bound": "^1.0.2", + "es-errors": "^1.3.0", + "get-intrinsic": "^1.2.5", + "object-inspect": "^1.13.3", + "side-channel-map": "^1.0.1" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io": { + "version": "4.8.1", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.8.1.tgz", + "integrity": "sha512-oZ7iUCxph8WYRHHcjBEc9unw3adt5CmSNlppj/5Q4k2RIrhl8Z5yY2Xr4j9zj0+wzVZ0bxmYoGSzKJnRl6A4yg==", + "dev": true, + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.6.0", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.5", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.5.tgz", + "integrity": "sha512-eLDQas5dzPgOWCk9GuuJC2lBqItuhKI4uxGgo9aIV7MYbk2h9Q6uULEh8WBzThoI7l+qU9Ast9fVUmkqPP9wYg==", + "dev": true, + "dependencies": { + "debug": "~4.3.4", + "ws": "~8.17.1" + } + }, + "node_modules/socket.io-adapter/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-adapter/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/socket.io-adapter/node_modules/ws": { + "version": "8.17.1", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.17.1.tgz", + "integrity": "sha512-6XQFvXTkbfUOZOKKILFG1PDK2NDQs4azKQl26T0YS5CxqWLgXajbPZ+h4gZekJyRqFU8pvnbAbbs/3TgRPy+GQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.7", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.7.tgz", + "integrity": "sha512-Er2nc/H7RrMXZBFCEim6TCmMk02Z8vLC2Rbi1KEBggpo0fS6l0S1nnapwmIi3yW/+GOJap1Krg4w0Hg80oCqgQ==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.4.18", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.4.18.tgz", + "integrity": "sha512-try0/JqxPLF9nOjvSta7tVondkP5dwgyLDjVoyMDlmjugT2lRZ1OfsrYTkCd2hkDnJTKRbO/Rl3orm8vlsUzbA==", + "dependencies": { + "source-map": "^0.5.6" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.5.7.tgz", + "integrity": "sha512-LbrmJOMUSdEVxIKvdcJzQC+nQhe8FUZQTXQy6+I75skNgn3OoQ0DZA8YnFa7gp8tqtL3KPf1kmo0R5DoApeSGQ==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/statuses": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-1.5.0.tgz", + "integrity": "sha512-OpZ3zP+jT1PI7I8nemJX4AKmAX070ZkYPVWV/AaKTJl+tXCTGyVdC1a4SL8RUQYEwk/f34ZX8UTykN68FwrqAA==", + "dev": true, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/stream-browserify": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/stream-browserify/-/stream-browserify-2.0.2.tgz", + "integrity": "sha512-nX6hmklHs/gr2FuxYDltq8fJA1GDlxKQCz8O/IM4atRqBH8OORmBNgfvW5gG10GT/qQ9u0CzIvr2X5Pkt6ntqg==", + "dependencies": { + "inherits": "~2.0.1", + "readable-stream": "^2.0.2" + } + }, + "node_modules/stream-http": { + "version": "2.8.3", + "resolved": "https://registry.npmjs.org/stream-http/-/stream-http-2.8.3.tgz", + "integrity": "sha512-+TSkfINHDo4J+ZobQLWiMouQYB+UVYFttRA94FpEzzJ7ZdqcL4uUUQ7WkdkI4DSozGmgBUE/a47L+38PenXhUw==", + "dependencies": { + "builtin-status-codes": "^3.0.0", + "inherits": "^2.0.1", + "readable-stream": "^2.3.6", + "to-arraybuffer": "^1.0.0", + "xtend": "^4.0.0" + } + }, + "node_modules/streamroller": { + "version": "3.1.5", + "resolved": "https://registry.npmjs.org/streamroller/-/streamroller-3.1.5.tgz", + "integrity": "sha512-KFxaM7XT+irxvdqSP1LGLgNWbYN7ay5owZ3r/8t77p+EtSUAfUgtl7be3xtqtOmGUl9K9YPO2ca8133RlTjvKw==", + "dev": true, + "dependencies": { + "date-format": "^4.0.14", + "debug": "^4.3.4", + "fs-extra": "^8.1.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/streamroller/node_modules/debug": { + "version": "4.4.0", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz", + "integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==", + "dev": true, + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/streamroller/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/string_decoder": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", + "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dependencies": { + "safe-buffer": "~5.2.0" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/style-mod": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/style-mod/-/style-mod-4.1.2.tgz", + "integrity": "sha512-wnD1HyVqpJUI2+eKZ+eo1UwghftP6yuFheBqqe+bWCotBjC2K1YnteJILRMs3SM4V/0dLEW1SC27MWP5y+mwmw==" + }, + "node_modules/tar-fs": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.1.tgz", + "integrity": "sha512-V0r2Y9scmbDRLCNex/+hYzvp/zyYjvFbHPNgVTKfQvVrb6guiE/fxP+XblDNR011utopbkex2nM4dHNV6GDsng==", + "dev": true, + "dependencies": { + "chownr": "^1.1.1", + "mkdirp-classic": "^0.5.2", + "pump": "^3.0.0", + "tar-stream": "^2.1.4" + } + }, + "node_modules/tar-stream": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", + "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, + "dependencies": { + "bl": "^4.0.3", + "end-of-stream": "^1.4.1", + "fs-constants": "^1.0.0", + "inherits": "^2.0.3", + "readable-stream": "^3.1.1" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/tar-stream/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/through": { + "version": "2.3.8", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz", + "integrity": "sha512-w89qg7PI8wAdvX60bMDP+bFoD5Dvhm9oLheFp5O4a2QF0cSBGsBX4qZmadPMvVqlLJBBci+WqGGOAPvcDeNSVg==", + "dev": true + }, + "node_modules/timers-browserify": { + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/timers-browserify/-/timers-browserify-2.0.12.tgz", + "integrity": "sha512-9phl76Cqm6FhSX9Xe1ZUAMLtm1BLkKj2Qd5ApyWkXzsMRaA7dgr81kf4wJmQf/hAvg8EEyJxDo3du/0KlhPiKQ==", + "dependencies": { + "setimmediate": "^1.0.4" + }, + "engines": { + "node": ">=0.6.0" + } + }, + "node_modules/tiny-invariant": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/tiny-invariant/-/tiny-invariant-1.3.3.tgz", + "integrity": "sha512-+FbBPE1o9QAYvviau/qC5SE3caw21q3xkvWKBtja5vgqOWIHHJ3ioaq1VPfn/Szqctz2bU/oYeKd9/z5BL+PVg==" + }, + "node_modules/tmp": { + "version": "0.2.3", + "resolved": "https://registry.npmjs.org/tmp/-/tmp-0.2.3.tgz", + "integrity": "sha512-nZD7m9iCPC5g0pYmcaxogYKggSfLsdxl8of3Q/oIbqCqLLIO9IAF0GWjX1z9NZRHPiXv8Wex4yDCaZsgEw0Y8w==", + "dev": true, + "engines": { + "node": ">=14.14" + } + }, + "node_modules/to-arraybuffer": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/to-arraybuffer/-/to-arraybuffer-1.0.1.tgz", + "integrity": "sha512-okFlQcoGTi4LQBG/PgSYblw9VOyptsz2KJZqc6qtgGdes8VktzUQkj4BI2blit072iS8VODNcMA+tvnS9dnuMA==" + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "dev": true, + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "dev": true + }, + "node_modules/tslib": { + "version": "2.8.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", + "integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==" + }, + "node_modules/tty-browserify": { + "version": "0.0.0", + "resolved": "https://registry.npmjs.org/tty-browserify/-/tty-browserify-0.0.0.tgz", + "integrity": "sha512-JVa5ijo+j/sOoHGjw0sxw734b1LhBkQ3bvUGNdxnVXDCX81Yx7TFgnZygxrIIWn23hbfTaMYLwRmAxFyDuFmIw==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dev": true, + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ua-parser-js": { + "version": "0.7.40", + "resolved": "https://registry.npmjs.org/ua-parser-js/-/ua-parser-js-0.7.40.tgz", + "integrity": "sha512-us1E3K+3jJppDBa3Tl0L3MOJiGhe1C6P0+nIvQAFYbxlMAx0h81eOwLmU57xgqToduDDPx3y5QsdjPfDu+FgOQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/ua-parser-js" + }, + { + "type": "paypal", + "url": "https://paypal.me/faisalman" + }, + { + "type": "github", + "url": "https://github.com/sponsors/faisalman" + } + ], + "bin": { + "ua-parser-js": "script/cli.js" + }, + "engines": { + "node": "*" + } + }, + "node_modules/unbzip2-stream": { + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/unbzip2-stream/-/unbzip2-stream-1.4.3.tgz", + "integrity": "sha512-mlExGW4w71ebDJviH16lQLtZS32VKqsSfk80GCfUlwT/4/hNRFsoscrF/c++9xinkMzECL1uL9DDwXqFWkruPg==", + "dev": true, + "dependencies": { + "buffer": "^5.2.1", + "through": "^2.3.8" + } + }, + "node_modules/unbzip2-stream/node_modules/buffer": { + "version": "5.7.1", + "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", + "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "dependencies": { + "base64-js": "^1.3.1", + "ieee754": "^1.1.13" + } + }, + "node_modules/undici-types": { + "version": "6.20.0", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.20.0.tgz", + "integrity": "sha512-Ny6QZ2Nju20vw1SRHe3d9jVu6gJ+4e3+MMpqu7pqE5HT6WsTSlce++GQmK5UXS8mzV8DSYHrQH+Xrf2jVcuKNg==", + "dev": true + }, + "node_modules/universalify": { + "version": "0.1.2", + "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", + "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==", + "dev": true, + "engines": { + "node": ">= 4.0.0" + } + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/url": { + "version": "0.11.4", + "resolved": "https://registry.npmjs.org/url/-/url-0.11.4.tgz", + "integrity": "sha512-oCwdVC7mTuWiPyjLUz/COz5TLk6wgp0RCsN+wHZ2Ekneac9w8uuV0njcbbie2ME+Vs+d6duwmYuR3HgQXs1fOg==", + "dependencies": { + "punycode": "^1.4.1", + "qs": "^6.12.3" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/use-callback-ref": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/use-callback-ref/-/use-callback-ref-1.3.3.tgz", + "integrity": "sha512-jQL3lRnocaFtu3V00JToYz/4QkNWswxijDaCVNZRiRTO3HQDLsdu1ZtmIUvV4yPp+rvWm5j0y0TG/S61cuijTg==", + "dependencies": { + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/use-sidecar": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/use-sidecar/-/use-sidecar-1.1.3.tgz", + "integrity": "sha512-Fedw0aZvkhynoPYlA5WXrMCAMm+nSWdZt6lzJQ7Ok8S6Q+VsHmHpRWndVRJ8Be0ZbkfPc5LRYH+5XrzXcEeLRQ==", + "dependencies": { + "detect-node-es": "^1.1.0", + "tslib": "^2.0.0" + }, + "engines": { + "node": ">=10" + }, + "peerDependencies": { + "@types/react": "*", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc" + }, + "peerDependenciesMeta": { + "@types/react": { + "optional": true + } + } + }, + "node_modules/util": { + "version": "0.11.1", + "resolved": "https://registry.npmjs.org/util/-/util-0.11.1.tgz", + "integrity": "sha512-HShAsny+zS2TZfaXxD9tYj4HQGlBezXZMZuM/S5PKLLoZkShZiGk9o5CzukI1LVHZvjdvZ2Sj1aW/Ndn2NB/HQ==", + "dependencies": { + "inherits": "2.0.3" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==" + }, + "node_modules/util/node_modules/inherits": { + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "integrity": "sha512-x00IRNXNy63jwGkJmzPigoySHbaqpNuzKbBOmzK+g2OdZpQ9w+sxCN+VSB3ja7IAge2OP2qpfxTjeNcyjmW1uw==" + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "dev": true, + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "dev": true, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/victory-vendor": { + "version": "36.9.2", + "resolved": "https://registry.npmjs.org/victory-vendor/-/victory-vendor-36.9.2.tgz", + "integrity": "sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==", + "dependencies": { + "@types/d3-array": "^3.0.3", + "@types/d3-ease": "^3.0.0", + "@types/d3-interpolate": "^3.0.1", + "@types/d3-scale": "^4.0.2", + "@types/d3-shape": "^3.1.0", + "@types/d3-time": "^3.0.0", + "@types/d3-timer": "^3.0.0", + "d3-array": "^3.1.6", + "d3-ease": "^3.0.1", + "d3-interpolate": "^3.0.1", + "d3-scale": "^4.0.2", + "d3-shape": "^3.1.0", + "d3-time": "^3.0.0", + "d3-timer": "^3.0.1" + } + }, + "node_modules/vm-browserify": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vm-browserify/-/vm-browserify-1.1.2.tgz", + "integrity": "sha512-2ham8XPWTONajOR0ohOKOHXkm3+gaBmGut3SRuu75xLd/RRaY6vqgh8NBYYk7+RW3u5AtzPQZG8F10LHkl0lAQ==" + }, + "node_modules/void-elements": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/void-elements/-/void-elements-2.0.1.tgz", + "integrity": "sha512-qZKX4RnBzH2ugr8Lxa7x+0V6XD9Sb/ouARtiasEQCHB1EVU4NXtmHsDDrx1dO4ne5fc3J6EW05BP1Dl0z0iung==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/w3c-keyname": { + "version": "2.2.8", + "resolved": "https://registry.npmjs.org/w3c-keyname/-/w3c-keyname-2.2.8.tgz", + "integrity": "sha512-dpojBhNsCNN7T82Tm7k26A6G9ML3NkhDsnw9n/eoxSRlVBB4CEtIQ/KTCLI2Fwf3ataSXRhYFkQi3SlnFwPvPQ==" + }, + "node_modules/web-tree-sitter": { + "version": "0.22.6", + "resolved": "https://registry.npmjs.org/web-tree-sitter/-/web-tree-sitter-0.22.6.tgz", + "integrity": "sha512-hS87TH71Zd6mGAmYCvlgxeGDjqd9GTeqXNqTT+u0Gs51uIozNIaaq/kUAbV/Zf56jb2ZOyG8BxZs2GG9wbLi6Q==" + }, + "node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "dev": true + }, + "node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "dev": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, + "node_modules/which": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/which/-/which-1.3.1.tgz", + "integrity": "sha512-HxJdYWq1MTIQbJ3nw0cqssHoTNU267KlrDuGZ1WYlxDStUtKUhOaJmh112/TZmHxxUfuJqPXSOm7tDyas0OSIQ==", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "which": "bin/which" + } + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.8.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.8.0.tgz", + "integrity": "sha512-JDAgSYQ1ksuwqfChJusw1LSJ8BizJ2e/vVu5Lxjq3YvNJNlROv1ui4i+c/kUUrPheBvQl4c5UbERhTwKa6QBJQ==", + "dev": true, + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xtend": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.2.tgz", + "integrity": "sha512-LKYU1iAXJXUgAXn9URjiu+MWhyUXHsvfp7mcuYm9dSUKK0/CjtrUwFAxD82/mCWbtLsGjFIad0wIsod4zrTAEQ==", + "engines": { + "node": ">=0.4" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.9", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.9.tgz", + "integrity": "sha512-y11nGElTIV+CT3Zv9t7VKl+Q3hTQoT9a1Qzezhhl6Rp21gJ/IVTW7Z3y9EWXhuUBC2Shnf+DX0antecpAwSP8w==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yauzl": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/yauzl/-/yauzl-2.10.0.tgz", + "integrity": "sha512-p4a9I6X6nu6IhoGmBqAcbJy1mlC4j27vEPZX9F4L4/vZT3Lyq1VkFHw/V/PUcB9Buo+DG3iHkT0x3Qya58zc3g==", + "dev": true, + "dependencies": { + "buffer-crc32": "~0.2.3", + "fd-slicer": "~1.1.0" + } + } + } +} diff --git a/package.json b/package.json index 13749e1..39bec4e 100644 --- a/package.json +++ b/package.json @@ -1,10 +1,10 @@ { "name": "softland", "version": "0.0.1", - "description": "``` $ clj -A:dev -X user/main", + "description": "``` $ clj -M:dev -m dev", "main": "index.js", "scripts": { - "build": "clj -A:dev -X user/main" + "build": "clj -M:dev -m dev" }, "author": "", "license": "ISC", @@ -30,6 +30,7 @@ "react-dom": "18.2.0", "recharts": "^2.4.1", "shadow-cljs": "^2.20.1", + "web-tree-sitter": "^0.22.6", "w3c-keyname": "^2.2.4" }, "devDependencies": { diff --git a/resources/public/JetBrainsMono-Regular.ttf b/resources/public/JetBrainsMono-Regular.ttf new file mode 100644 index 0000000..dff66cc Binary files /dev/null and b/resources/public/JetBrainsMono-Regular.ttf differ diff --git a/resources/public/font_atlas.json b/resources/public/font_atlas.json index 2ca0d75..e3716b5 100644 --- a/resources/public/font_atlas.json +++ b/resources/public/font_atlas.json @@ -1 +1 @@ -{"atlas":{"type":"msdf","distanceRange":16,"distanceRangeMiddle":0,"size":256,"width":8192,"height":8192,"yOrigin":"bottom"},"metrics":{"emSize":1,"lineHeight":1.3200000000000001,"ascender":1.02,"descender":-0.29999999999999999,"underlineY":-0.17999999999999999,"underlineThickness":0.050000000000000003},"glyphs":[{"unicode":33,"advance":0.59999999999999998,"planeBounds":{"left":0.16931066176470588,"bottom":-0.037109375,"right":0.44274816176470588,"top":0.763671875},"atlasBounds":{"left":135.5,"bottom":4791.5,"right":205.5,"top":4996.5}},{"unicode":34,"advance":0.59999999999999998,"planeBounds":{"left":0.15618750000000001,"bottom":0.396484375,"right":0.54681250000000003,"top":0.763671875},"atlasBounds":{"left":2164.5,"bottom":3244.5,"right":2264.5,"top":3338.5}},{"unicode":35,"advance":0.59999999999999998,"planeBounds":{"left":-0.0082656250000000195,"bottom":-0.033203125,"right":0.63626562500000006,"top":0.763671875},"atlasBounds":{"left":8026.5,"bottom":5442.5,"right":8191.5,"top":5646.5}},{"unicode":36,"advance":0.59999999999999998,"planeBounds":{"left":0.029835736424984306,"bottom":-0.173828125,"right":0.58842948642498438,"top":0.904296875},"atlasBounds":{"left":7459.5,"bottom":7508.5,"right":7602.5,"top":7784.5}},{"unicode":37,"advance":0.59999999999999998,"planeBounds":{"left":-0.055140624999999978,"bottom":-0.037109375,"right":0.68314062500000006,"top":0.767578125},"atlasBounds":{"left":4228.5,"bottom":5000.5,"right":4417.5,"top":5206.5}},{"unicode":38,"advance":0.59999999999999998,"planeBounds":{"left":-0.013641237745098067,"bottom":-0.044921875,"right":0.65432751225490193,"top":0.771484375},"atlasBounds":{"left":3283.5,"bottom":5215.5,"right":3454.5,"top":5424.5}},{"unicode":39,"advance":0.59999999999999998,"planeBounds":{"left":0.25970312499999998,"bottom":0.396484375,"right":0.44329687500000003,"top":0.763671875},"atlasBounds":{"left":3000.5,"bottom":3244.5,"right":3047.5,"top":3338.5}},{"unicode":40,"advance":0.59999999999999998,"planeBounds":{"left":0.15742074548192772,"bottom":-0.154296875,"right":0.60663949548192775,"top":0.876953125},"atlasBounds":{"left":3867.5,"bottom":7164.5,"right":3982.5,"top":7428.5}},{"unicode":41,"advance":0.59999999999999998,"planeBounds":{"left":0.019319196428571449,"bottom":-0.154296875,"right":0.46853794642857144,"top":0.876953125},"atlasBounds":{"left":3983.5,"bottom":7164.5,"right":4098.5,"top":7428.5}},{"unicode":42,"advance":0.59999999999999998,"planeBounds":{"left":0.022984375000000008,"bottom":0.080078125,"right":0.60501562500000006,"top":0.654296875},"atlasBounds":{"left":7592.5,"bottom":3496.5,"right":7741.5,"top":3643.5}},{"unicode":43,"advance":0.59999999999999998,"planeBounds":{"left":0.04287499999999999,"bottom":0.064453125,"right":0.574125,"top":0.599609375},"atlasBounds":{"left":1860.5,"bottom":3347.5,"right":1996.5,"top":3484.5}},{"unicode":44,"advance":0.59999999999999998,"planeBounds":{"left":0.11359375000000001,"bottom":-0.193359375,"right":0.37140624999999999,"top":0.177734375},"atlasBounds":{"left":1547.5,"bottom":3243.5,"right":1613.5,"top":3338.5}},{"unicode":45,"advance":0.59999999999999998,"planeBounds":{"left":0.11123437500000001,"bottom":0.267578125,"right":0.505765625,"top":0.392578125},"atlasBounds":{"left":8021.5,"bottom":7520.5,"right":8122.5,"top":7552.5}},{"unicode":46,"advance":0.59999999999999998,"planeBounds":{"left":0.15487839673913045,"bottom":-0.044921875,"right":0.37753464673913045,"top":0.177734375},"atlasBounds":{"left":8103.5,"bottom":3656.5,"right":8160.5,"top":3713.5}},{"unicode":47,"advance":0.59999999999999998,"planeBounds":{"left":-0.0053593749999999675,"bottom":-0.142578125,"right":0.63135937500000006,"top":0.861328125},"atlasBounds":{"left":2689.5,"bottom":6635.5,"right":2852.5,"top":6892.5}},{"unicode":48,"advance":0.59999999999999998,"planeBounds":{"left":0.04024798387096775,"bottom":-0.044921875,"right":0.58712298387096784,"top":0.771484375},"atlasBounds":{"left":3455.5,"bottom":5215.5,"right":3595.5,"top":5424.5}},{"unicode":49,"advance":0.59999999999999998,"planeBounds":{"left":0.019640625000000033,"bottom":-0.033203125,"right":0.53135937499999997,"top":0.763671875},"atlasBounds":{"left":3876.5,"bottom":4792.5,"right":4007.5,"top":4996.5}},{"unicode":50,"advance":0.59999999999999998,"planeBounds":{"left":0.015921874999999985,"bottom":-0.033203125,"right":0.59014062499999997,"top":0.771484375},"atlasBounds":{"left":4418.5,"bottom":5000.5,"right":4565.5,"top":5206.5}},{"unicode":51,"advance":0.59999999999999998,"planeBounds":{"left":0.02946428571428571,"bottom":-0.044921875,"right":0.59196428571428572,"top":0.763671875},"atlasBounds":{"left":1012.5,"bottom":4999.5,"right":1156.5,"top":5206.5}},{"unicode":52,"advance":0.59999999999999998,"planeBounds":{"left":0.029093750000000033,"bottom":-0.033203125,"right":0.53690625000000003,"top":0.763671875},"atlasBounds":{"left":4124.5,"bottom":4792.5,"right":4254.5,"top":4996.5}},{"unicode":53,"advance":0.59999999999999998,"planeBounds":{"left":0.034398437500000011,"bottom":-0.044921875,"right":0.58908593750000005,"top":0.763671875},"atlasBounds":{"left":1157.5,"bottom":4999.5,"right":1299.5,"top":5206.5}},{"unicode":54,"advance":0.59999999999999998,"planeBounds":{"left":0.026600650027808669,"bottom":-0.044921875,"right":0.55394440002780865,"top":0.763671875},"atlasBounds":{"left":1300.5,"bottom":4999.5,"right":1435.5,"top":5206.5}},{"unicode":55,"advance":0.59999999999999998,"planeBounds":{"left":0.095562500000000009,"bottom":-0.033203125,"right":0.64243749999999999,"top":0.763671875},"atlasBounds":{"left":4255.5,"bottom":4792.5,"right":4395.5,"top":4996.5}},{"unicode":56,"advance":0.59999999999999998,"planeBounds":{"left":0.02743989849833145,"bottom":-0.044921875,"right":0.58212739849833151,"top":0.771484375},"atlasBounds":{"left":3596.5,"bottom":5215.5,"right":3738.5,"top":5424.5}},{"unicode":57,"advance":0.59999999999999998,"planeBounds":{"left":0.077994612068965546,"bottom":-0.033203125,"right":0.60533836206896563,"top":0.771484375},"atlasBounds":{"left":4566.5,"bottom":5000.5,"right":4701.5,"top":5206.5}},{"unicode":58,"advance":0.59999999999999998,"planeBounds":{"left":0.15494404644268772,"bottom":-0.044921875,"right":0.44400654644268778,"top":0.591796875},"atlasBounds":{"left":7235.5,"bottom":3821.5,"right":7309.5,"top":3984.5}},{"unicode":59,"advance":0.59999999999999998,"planeBounds":{"left":0.081740327380952396,"bottom":-0.193359375,"right":0.44502157738095244,"top":0.591796875},"atlasBounds":{"left":3887.5,"bottom":4178.5,"right":3980.5,"top":4379.5}},{"unicode":60,"advance":0.59999999999999998,"planeBounds":{"left":0.058421874999999984,"bottom":0.041015625,"right":0.59357812500000007,"top":0.619140625},"atlasBounds":{"left":6244.5,"bottom":3495.5,"right":6381.5,"top":3643.5}},{"unicode":61,"advance":0.59999999999999998,"planeBounds":{"left":0.04287499999999999,"bottom":0.150390625,"right":0.574125,"top":0.509765625},"atlasBounds":{"left":4450.5,"bottom":3246.5,"right":4586.5,"top":3338.5}},{"unicode":62,"advance":0.59999999999999998,"planeBounds":{"left":0.023421874999999988,"bottom":0.041015625,"right":0.55857812500000004,"top":0.619140625},"atlasBounds":{"left":6382.5,"bottom":3495.5,"right":6519.5,"top":3643.5}},{"unicode":63,"advance":0.59999999999999998,"planeBounds":{"left":0.1245308322192514,"bottom":-0.037109375,"right":0.56593708221925132,"top":0.763671875},"atlasBounds":{"left":206.5,"bottom":4791.5,"right":319.5,"top":4996.5}},{"unicode":64,"advance":0.59999999999999998,"planeBounds":{"left":-0.0071684027777777467,"bottom":-0.212890625,"right":0.62173784722222225,"top":0.771484375},"atlasBounds":{"left":7871.5,"bottom":6379.5,"right":8032.5,"top":6631.5}},{"unicode":65,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.033203125,"right":0.52748437500000001,"top":0.763671875},"atlasBounds":{"left":4556.5,"bottom":4792.5,"right":4695.5,"top":4996.5}},{"unicode":66,"advance":0.59999999999999998,"planeBounds":{"left":0.026736895161290301,"bottom":-0.033203125,"right":0.5814243951612903,"top":0.763671875},"atlasBounds":{"left":4837.5,"bottom":4792.5,"right":4979.5,"top":4996.5}},{"unicode":67,"advance":0.59999999999999998,"planeBounds":{"left":0.053439948156682029,"bottom":-0.044921875,"right":0.59250244815668207,"top":0.771484375},"atlasBounds":{"left":3739.5,"bottom":5215.5,"right":3877.5,"top":5424.5}},{"unicode":68,"advance":0.59999999999999998,"planeBounds":{"left":0.027229166666666697,"bottom":-0.033203125,"right":0.57410416666666675,"top":0.763671875},"atlasBounds":{"left":4980.5,"bottom":4792.5,"right":5120.5,"top":4996.5}},{"unicode":69,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.763671875},"atlasBounds":{"left":5121.5,"bottom":4792.5,"right":5270.5,"top":4996.5}},{"unicode":70,"advance":0.59999999999999998,"planeBounds":{"left":0.031171875000000016,"bottom":-0.033203125,"right":0.62882812499999996,"top":0.763671875},"atlasBounds":{"left":5271.5,"bottom":4792.5,"right":5424.5,"top":4996.5}},{"unicode":71,"advance":0.59999999999999998,"planeBounds":{"left":0.049439948156682033,"bottom":-0.044921875,"right":0.58850244815668207,"top":0.771484375},"atlasBounds":{"left":3878.5,"bottom":5215.5,"right":4016.5,"top":5424.5}},{"unicode":72,"advance":0.59999999999999998,"planeBounds":{"left":0.026890625000000005,"bottom":-0.033203125,"right":0.60110937500000006,"top":0.763671875},"atlasBounds":{"left":5584.5,"bottom":4792.5,"right":5731.5,"top":4996.5}},{"unicode":73,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":0.763671875},"atlasBounds":{"left":5732.5,"bottom":4792.5,"right":5873.5,"top":4996.5}},{"unicode":74,"advance":0.59999999999999998,"planeBounds":{"left":0.012122983870967765,"bottom":-0.044921875,"right":0.59024798387096777,"top":0.763671875},"atlasBounds":{"left":1436.5,"bottom":4999.5,"right":1584.5,"top":5206.5}},{"unicode":75,"advance":0.59999999999999998,"planeBounds":{"left":0.025906250000000023,"bottom":-0.033203125,"right":0.64309375000000002,"top":0.763671875},"atlasBounds":{"left":8033.5,"bottom":6427.5,"right":8191.5,"top":6631.5}},{"unicode":76,"advance":0.59999999999999998,"planeBounds":{"left":0.059812499999999991,"bottom":-0.033203125,"right":0.54418750000000005,"top":0.763671875},"atlasBounds":{"left":6161.5,"bottom":4792.5,"right":6285.5,"top":4996.5}},{"unicode":77,"advance":0.59999999999999998,"planeBounds":{"left":0.0073593750000000213,"bottom":-0.033203125,"right":0.62064062500000006,"top":0.763671875},"atlasBounds":{"left":6286.5,"bottom":4792.5,"right":6443.5,"top":4996.5}},{"unicode":78,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.033203125,"right":0.60306250000000006,"top":0.763671875},"atlasBounds":{"left":6444.5,"bottom":4792.5,"right":6592.5,"top":4996.5}},{"unicode":79,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.044921875,"right":0.57938729508196729,"top":0.771484375},"atlasBounds":{"left":4017.5,"bottom":5215.5,"right":4153.5,"top":5424.5}},{"unicode":80,"advance":0.59999999999999998,"planeBounds":{"left":0.026308894230769212,"bottom":-0.033203125,"right":0.61615264423076921,"top":0.763671875},"atlasBounds":{"left":6593.5,"bottom":4792.5,"right":6744.5,"top":4996.5}},{"unicode":81,"advance":0.59999999999999998,"planeBounds":{"left":0.046421875000000015,"bottom":-0.212890625,"right":0.58157812500000006,"top":0.771484375},"atlasBounds":{"left":0.5,"bottom":6121.5,"right":137.5,"top":6373.5}},{"unicode":82,"advance":0.59999999999999998,"planeBounds":{"left":0.026161637931034491,"bottom":-0.033203125,"right":0.60428663793103454,"top":0.763671875},"atlasBounds":{"left":6745.5,"bottom":4792.5,"right":6893.5,"top":4996.5}},{"unicode":83,"advance":0.59999999999999998,"planeBounds":{"left":0.032109375000000002,"bottom":-0.044921875,"right":0.582890625,"top":0.771484375},"atlasBounds":{"left":4291.5,"bottom":5215.5,"right":4432.5,"top":5424.5}},{"unicode":84,"advance":0.59999999999999998,"planeBounds":{"left":0.087703125000000007,"bottom":-0.033203125,"right":0.64629687499999999,"top":0.763671875},"atlasBounds":{"left":8048.5,"bottom":5002.5,"right":8191.5,"top":5206.5}},{"unicode":85,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":0.763671875},"atlasBounds":{"left":1585.5,"bottom":4999.5,"right":1726.5,"top":5206.5}},{"unicode":86,"advance":0.59999999999999998,"planeBounds":{"left":0.10051562499999998,"bottom":-0.033203125,"right":0.643484375,"top":0.763671875},"atlasBounds":{"left":6894.5,"bottom":4792.5,"right":7033.5,"top":4996.5}},{"unicode":87,"advance":0.59999999999999998,"planeBounds":{"left":0.044046875000000027,"bottom":-0.033203125,"right":0.67295312500000004,"top":0.763671875},"atlasBounds":{"left":7034.5,"bottom":4792.5,"right":7195.5,"top":4996.5}},{"unicode":88,"advance":0.59999999999999998,"planeBounds":{"left":-0.028437500000000008,"bottom":-0.033203125,"right":0.6434375,"top":0.763671875},"atlasBounds":{"left":7196.5,"bottom":4792.5,"right":7368.5,"top":4996.5}},{"unicode":89,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.763671875},"atlasBounds":{"left":7369.5,"bottom":4792.5,"right":7517.5,"top":4996.5}},{"unicode":90,"advance":0.59999999999999998,"planeBounds":{"left":0.013078125000000012,"bottom":-0.033203125,"right":0.60292187500000005,"top":0.763671875},"atlasBounds":{"left":7518.5,"bottom":4792.5,"right":7669.5,"top":4996.5}},{"unicode":91,"advance":0.59999999999999998,"planeBounds":{"left":0.12084375,"bottom":-0.142578125,"right":0.56615625000000003,"top":0.861328125},"atlasBounds":{"left":2853.5,"bottom":6635.5,"right":2967.5,"top":6892.5}},{"unicode":92,"advance":0.59999999999999998,"planeBounds":{"left":0.141125,"bottom":-0.142578125,"right":0.484875,"top":0.861328125},"atlasBounds":{"left":2968.5,"bottom":6635.5,"right":3056.5,"top":6892.5}},{"unicode":93,"advance":0.59999999999999998,"planeBounds":{"left":0.059843750000000001,"bottom":-0.142578125,"right":0.50515624999999997,"top":0.861328125},"atlasBounds":{"left":3057.5,"bottom":6635.5,"right":3171.5,"top":6892.5}},{"unicode":94,"advance":0.59999999999999998,"planeBounds":{"left":0.069765625000000012,"bottom":0.306640625,"right":0.55023437500000005,"top":0.763671875},"atlasBounds":{"left":8068.5,"bottom":7311.5,"right":8191.5,"top":7428.5}},{"unicode":95,"advance":0.59999999999999998,"planeBounds":{"left":-0.029843750000000006,"bottom":-0.115234375,"right":0.52484375000000005,"top":0.009765625},"atlasBounds":{"left":8045.5,"bottom":6645.5,"right":8187.5,"top":6677.5}},{"unicode":96,"advance":0.59999999999999998,"planeBounds":{"left":0.22667187499999999,"bottom":0.611328125,"right":0.44932812500000002,"top":0.818359375},"atlasBounds":{"left":8024.5,"bottom":6898.5,"right":8081.5,"top":6951.5}},{"unicode":97,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.591796875},"atlasBounds":{"left":7835.5,"bottom":3821.5,"right":7968.5,"top":3984.5}},{"unicode":98,"advance":0.59999999999999998,"planeBounds":{"left":0.027063068181818143,"bottom":-0.044921875,"right":0.55050056818181814,"top":0.763671875},"atlasBounds":{"left":2046.5,"bottom":4999.5,"right":2180.5,"top":5206.5}},{"unicode":99,"advance":0.59999999999999998,"planeBounds":{"left":0.054383744347088724,"bottom":-0.044921875,"right":0.5621962443470887,"top":0.591796875},"atlasBounds":{"left":0.5,"bottom":3644.5,"right":130.5,"top":3807.5}},{"unicode":100,"advance":0.59999999999999998,"planeBounds":{"left":0.047374431818181795,"bottom":-0.044921875,"right":0.60206193181818191,"top":0.763671875},"atlasBounds":{"left":2181.5,"bottom":4999.5,"right":2323.5,"top":5206.5}},{"unicode":101,"advance":0.59999999999999998,"planeBounds":{"left":0.04740393518518516,"bottom":-0.044921875,"right":0.55521643518518526,"top":0.591796875},"atlasBounds":{"left":431.5,"bottom":3644.5,"right":561.5,"top":3807.5}},{"unicode":102,"advance":0.59999999999999998,"planeBounds":{"left":-0.031578125000000012,"bottom":-0.212890625,"right":0.62857812499999999,"top":0.763671875},"atlasBounds":{"left":1899.5,"bottom":6123.5,"right":2068.5,"top":6373.5}},{"unicode":103,"advance":0.59999999999999998,"planeBounds":{"left":0.056372685185185227,"bottom":-0.212890625,"right":0.57199768518518523,"top":0.591796875},"atlasBounds":{"left":4702.5,"bottom":5000.5,"right":4834.5,"top":5206.5}},{"unicode":104,"advance":0.59999999999999998,"planeBounds":{"left":0.026563068181818143,"bottom":-0.033203125,"right":0.55000056818181819,"top":0.763671875},"atlasBounds":{"left":0.5,"bottom":4585.5,"right":134.5,"top":4789.5}},{"unicode":105,"advance":0.59999999999999998,"planeBounds":{"left":0.011828124999999983,"bottom":-0.033203125,"right":0.53917187499999997,"top":0.802734375},"atlasBounds":{"left":556.5,"bottom":5210.5,"right":691.5,"top":5424.5}},{"unicode":106,"advance":0.59999999999999998,"planeBounds":{"left":-0.0030164473684210196,"bottom":-0.212890625,"right":0.54385855263157901,"top":0.802734375},"atlasBounds":{"left":1855.5,"bottom":6896.5,"right":1995.5,"top":7156.5}},{"unicode":107,"advance":0.59999999999999998,"planeBounds":{"left":0.030937500000000007,"bottom":-0.033203125,"right":0.60906250000000006,"top":0.763671875},"atlasBounds":{"left":543.5,"bottom":4585.5,"right":691.5,"top":4789.5}},{"unicode":108,"advance":0.59999999999999998,"planeBounds":{"left":0.072125000000000022,"bottom":-0.033203125,"right":0.54087499999999999,"top":0.763671875},"atlasBounds":{"left":839.5,"bottom":4585.5,"right":959.5,"top":4789.5}},{"unicode":109,"advance":0.59999999999999998,"planeBounds":{"left":-0.0078995535714285418,"bottom":-0.033203125,"right":0.58975669642857143,"top":0.591796875},"atlasBounds":{"left":5818.5,"bottom":3647.5,"right":5971.5,"top":3807.5}},{"unicode":110,"advance":0.59999999999999998,"planeBounds":{"left":0.026563068181818143,"bottom":-0.033203125,"right":0.55000056818181819,"top":0.591796875},"atlasBounds":{"left":5972.5,"bottom":3647.5,"right":6106.5,"top":3807.5}},{"unicode":111,"advance":0.59999999999999998,"planeBounds":{"left":0.049500000000000044,"bottom":-0.044921875,"right":0.54949999999999999,"top":0.591796875},"atlasBounds":{"left":693.5,"bottom":3644.5,"right":821.5,"top":3807.5}},{"unicode":112,"advance":0.59999999999999998,"planeBounds":{"left":-0.0030619318181818447,"bottom":-0.212890625,"right":0.55162556818181818,"top":0.591796875},"atlasBounds":{"left":4835.5,"bottom":5000.5,"right":4977.5,"top":5206.5}},{"unicode":113,"advance":0.59999999999999998,"planeBounds":{"left":0.048772321428571429,"bottom":-0.212890625,"right":0.57220982142857146,"top":0.591796875},"atlasBounds":{"left":4978.5,"bottom":5000.5,"right":5112.5,"top":5206.5}},{"unicode":114,"advance":0.59999999999999998,"planeBounds":{"left":0.047397836538461566,"bottom":-0.033203125,"right":0.56692908653846152,"top":0.591796875},"atlasBounds":{"left":6270.5,"bottom":3647.5,"right":6403.5,"top":3807.5}},{"unicode":115,"advance":0.59999999999999998,"planeBounds":{"left":0.032550951086956575,"bottom":-0.041015625,"right":0.5598947010869566,"top":0.591796875},"atlasBounds":{"left":3321.5,"bottom":3645.5,"right":3456.5,"top":3807.5}},{"unicode":116,"advance":0.59999999999999998,"planeBounds":{"left":0.058328124999999988,"bottom":-0.033203125,"right":0.58567187500000006,"top":0.736328125},"atlasBounds":{"left":5445.5,"bottom":4182.5,"right":5580.5,"top":4379.5}},{"unicode":117,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.583984375},"atlasBounds":{"left":4662.5,"bottom":3646.5,"right":4796.5,"top":3807.5}},{"unicode":118,"advance":0.59999999999999998,"planeBounds":{"left":0.081281249999999985,"bottom":-0.033203125,"right":0.60471874999999997,"top":0.583984375},"atlasBounds":{"left":2164.5,"bottom":3485.5,"right":2298.5,"top":3643.5}},{"unicode":119,"advance":0.59999999999999998,"planeBounds":{"left":0.051984375000000006,"bottom":-0.033203125,"right":0.63401562499999997,"top":0.583984375},"atlasBounds":{"left":2299.5,"bottom":3485.5,"right":2448.5,"top":3643.5}},{"unicode":120,"advance":0.59999999999999998,"planeBounds":{"left":-0.0087343749999999817,"bottom":-0.033203125,"right":0.59673437500000004,"top":0.583984375},"atlasBounds":{"left":2449.5,"bottom":3485.5,"right":2604.5,"top":3643.5}},{"unicode":121,"advance":0.59999999999999998,"planeBounds":{"left":0.053905681818181776,"bottom":-0.212890625,"right":0.56953068181818178,"top":0.583984375},"atlasBounds":{"left":1095.5,"bottom":4585.5,"right":1227.5,"top":4789.5}},{"unicode":122,"advance":0.59999999999999998,"planeBounds":{"left":0.02201562499999999,"bottom":-0.033203125,"right":0.56498437499999998,"top":0.583984375},"atlasBounds":{"left":2605.5,"bottom":3485.5,"right":2744.5,"top":3643.5}},{"unicode":123,"advance":0.59999999999999998,"planeBounds":{"left":0.060156249999999994,"bottom":-0.142578125,"right":0.61484375000000002,"top":0.861328125},"atlasBounds":{"left":3172.5,"bottom":6635.5,"right":3314.5,"top":6892.5}},{"unicode":124,"advance":0.59999999999999998,"planeBounds":{"left":0.174328125,"bottom":-0.142578125,"right":0.451671875,"top":0.861328125},"atlasBounds":{"left":3315.5,"bottom":6635.5,"right":3386.5,"top":6892.5}},{"unicode":125,"advance":0.59999999999999998,"planeBounds":{"left":0.011156249999999996,"bottom":-0.142578125,"right":0.56584374999999998,"top":0.861328125},"atlasBounds":{"left":3387.5,"bottom":6635.5,"right":3529.5,"top":6892.5}},{"unicode":126,"advance":0.59999999999999998,"planeBounds":{"left":0.045874999999999985,"bottom":0.224609375,"right":0.577125,"top":0.482421875},"atlasBounds":{"left":7150.5,"bottom":3272.5,"right":7286.5,"top":3338.5}},{"unicode":160,"advance":0.59999999999999998},{"unicode":161,"advance":0.59999999999999998,"planeBounds":{"left":0.15629871323529412,"bottom":-0.212890625,"right":0.43364246323529415,"top":0.587890625},"atlasBounds":{"left":477.5,"bottom":4791.5,"right":548.5,"top":4996.5}},{"unicode":162,"advance":0.59999999999999998,"planeBounds":{"left":0.054332743710691855,"bottom":-0.173828125,"right":0.56214524371069186,"top":0.724609375},"atlasBounds":{"left":6702.5,"bottom":5890.5,"right":6832.5,"top":6120.5}},{"unicode":163,"advance":0.59999999999999998,"planeBounds":{"left":-0.00071370967741937719,"bottom":-0.033203125,"right":0.59303629032258065,"top":0.771484375},"atlasBounds":{"left":5113.5,"bottom":5000.5,"right":5265.5,"top":5206.5}},{"unicode":164,"advance":0.59999999999999998,"planeBounds":{"left":0.0035000000000000265,"bottom":0.087890625,"right":0.62850000000000006,"top":0.658203125},"atlasBounds":{"left":7742.5,"bottom":3497.5,"right":7902.5,"top":3643.5}},{"unicode":165,"advance":0.59999999999999998,"planeBounds":{"left":0.0043749999999999848,"bottom":-0.033203125,"right":0.66062500000000002,"top":0.763671875},"atlasBounds":{"left":1228.5,"bottom":4585.5,"right":1396.5,"top":4789.5}},{"unicode":166,"advance":0.59999999999999998,"planeBounds":{"left":0.174328125,"bottom":-0.142578125,"right":0.451671875,"top":0.861328125},"atlasBounds":{"left":3530.5,"bottom":6635.5,"right":3601.5,"top":6892.5}},{"unicode":167,"advance":0.59999999999999998,"planeBounds":{"left":0.0086688194444444373,"bottom":-0.189453125,"right":0.59070006944444453,"top":0.771484375},"atlasBounds":{"left":7734.5,"bottom":6127.5,"right":7883.5,"top":6373.5}},{"unicode":169,"advance":0.59999999999999998,"planeBounds":{"left":0.024117953431372508,"bottom":0.076171875,"right":0.62177420343137246,"top":0.771484375},"atlasBounds":{"left":2381.5,"bottom":3996.5,"right":2534.5,"top":4174.5}},{"unicode":171,"advance":0.59999999999999998,"planeBounds":{"left":0.0094531250000000257,"bottom":0.005859375,"right":0.63054687500000006,"top":0.552734375},"atlasBounds":{"left":1219.5,"bottom":3344.5,"right":1378.5,"top":3484.5}},{"unicode":172,"advance":0.59999999999999998,"planeBounds":{"left":0.076171875000000014,"bottom":0.158203125,"right":0.548828125,"top":0.419921875},"atlasBounds":{"left":6865.5,"bottom":3271.5,"right":6986.5,"top":3338.5}},{"unicode":174,"advance":0.59999999999999998,"planeBounds":{"left":0.024117953431372508,"bottom":0.076171875,"right":0.62177420343137246,"top":0.771484375},"atlasBounds":{"left":2535.5,"bottom":3996.5,"right":2688.5,"top":4174.5}},{"unicode":176,"advance":0.59999999999999998,"planeBounds":{"left":0.17081249999999998,"bottom":0.423828125,"right":0.53018750000000003,"top":0.771484375},"atlasBounds":{"left":5085.5,"bottom":3249.5,"right":5177.5,"top":3338.5}},{"unicode":177,"advance":0.59999999999999998,"planeBounds":{"left":-0.0069687499999999914,"bottom":-0.033203125,"right":0.57896875000000003,"top":0.603515625},"atlasBounds":{"left":1102.5,"bottom":3644.5,"right":1252.5,"top":3807.5}},{"unicode":178,"advance":0.59999999999999998,"planeBounds":{"left":0.14774857954545456,"bottom":0.349609375,"right":0.55009232954545451,"top":0.869140625},"atlasBounds":{"left":4002.5,"bottom":3351.5,"right":4105.5,"top":3484.5}},{"unicode":179,"advance":0.59999999999999998,"planeBounds":{"left":0.15428804347826089,"bottom":0.345703125,"right":0.56053804347826086,"top":0.861328125},"atlasBounds":{"left":5018.5,"bottom":3352.5,"right":5122.5,"top":3484.5}},{"unicode":181,"advance":0.59999999999999998,"planeBounds":{"left":-0.0015624999999999942,"bottom":-0.212890625,"right":0.57656249999999998,"top":0.583984375},"atlasBounds":{"left":1974.5,"bottom":4585.5,"right":2122.5,"top":4789.5}},{"unicode":182,"advance":0.59999999999999998,"planeBounds":{"left":0.081092865566037745,"bottom":-0.212890625,"right":0.60062411556603779,"top":0.763671875},"atlasBounds":{"left":2069.5,"bottom":6123.5,"right":2202.5,"top":6373.5}},{"unicode":183,"advance":0.59999999999999998,"planeBounds":{"left":0.15487839673913045,"bottom":0.228515625,"right":0.37753464673913045,"top":0.451171875},"atlasBounds":{"left":0.5,"bottom":2863.5,"right":57.5,"top":2920.5}},{"unicode":185,"advance":0.59999999999999998,"planeBounds":{"left":0.14459374999999999,"bottom":0.349609375,"right":0.52740624999999997,"top":0.861328125},"atlasBounds":{"left":5223.5,"bottom":3353.5,"right":5321.5,"top":3484.5}},{"unicode":187,"advance":0.59999999999999998,"planeBounds":{"left":-0.030046874999999976,"bottom":0.005859375,"right":0.59104687499999997,"top":0.552734375},"atlasBounds":{"left":1379.5,"bottom":3344.5,"right":1538.5,"top":3484.5}},{"unicode":188,"advance":0.59999999999999998,"planeBounds":{"left":-0.0069218749999999897,"bottom":-0.033203125,"right":0.58292187500000003,"top":0.763671875},"atlasBounds":{"left":2123.5,"bottom":4585.5,"right":2274.5,"top":4789.5}},{"unicode":189,"advance":0.59999999999999998,"planeBounds":{"left":-0.0061750000000000095,"bottom":-0.033203125,"right":0.58757500000000007,"top":0.763671875},"atlasBounds":{"left":2275.5,"bottom":4585.5,"right":2427.5,"top":4789.5}},{"unicode":190,"advance":0.59999999999999998,"planeBounds":{"left":-0.0069218749999999897,"bottom":-0.033203125,"right":0.58292187500000003,"top":0.763671875},"atlasBounds":{"left":2428.5,"bottom":4585.5,"right":2579.5,"top":4789.5}},{"unicode":191,"advance":0.59999999999999998,"planeBounds":{"left":0.033036694004524893,"bottom":-0.212890625,"right":0.47444294400452486,"top":0.587890625},"atlasBounds":{"left":549.5,"bottom":4791.5,"right":662.5,"top":4996.5}},{"unicode":192,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.033203125,"right":0.52748437500000001,"top":0.982421875},"atlasBounds":{"left":1996.5,"bottom":6896.5,"right":2135.5,"top":7156.5}},{"unicode":193,"advance":0.59999999999999998,"planeBounds":{"left":-0.014921874999999989,"bottom":-0.033203125,"right":0.57492187500000003,"top":0.982421875},"atlasBounds":{"left":2136.5,"bottom":6896.5,"right":2287.5,"top":7156.5}},{"unicode":194,"advance":0.59999999999999998,"planeBounds":{"left":-0.015374999999999986,"bottom":-0.033203125,"right":0.57837499999999997,"top":0.982421875},"atlasBounds":{"left":2288.5,"bottom":6896.5,"right":2440.5,"top":7156.5}},{"unicode":195,"advance":0.59999999999999998,"planeBounds":{"left":-0.015640624999999977,"bottom":-0.033203125,"right":0.59764062500000004,"top":0.978515625},"atlasBounds":{"left":775.5,"bottom":6633.5,"right":932.5,"top":6892.5}},{"unicode":196,"advance":0.59999999999999998,"planeBounds":{"left":-0.014993055555555539,"bottom":-0.033203125,"right":0.59438194444444448,"top":0.970703125},"atlasBounds":{"left":3602.5,"bottom":6635.5,"right":3758.5,"top":6892.5}},{"unicode":197,"advance":0.59999999999999998,"planeBounds":{"left":-0.014902573529411782,"bottom":-0.033203125,"right":0.53978492647058818,"top":1.017578125},"atlasBounds":{"left":1482.5,"bottom":7159.5,"right":1624.5,"top":7428.5}},{"unicode":198,"advance":0.59999999999999998,"planeBounds":{"left":-0.044874999999999984,"bottom":-0.033203125,"right":0.673875,"top":0.763671875},"atlasBounds":{"left":2580.5,"bottom":4585.5,"right":2764.5,"top":4789.5}},{"unicode":199,"advance":0.59999999999999998,"planeBounds":{"left":0.053494047619047636,"bottom":-0.232421875,"right":0.59255654761904764,"top":0.771484375},"atlasBounds":{"left":3759.5,"bottom":6635.5,"right":3897.5,"top":6892.5}},{"unicode":200,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.982421875},"atlasBounds":{"left":2441.5,"bottom":6896.5,"right":2590.5,"top":7156.5}},{"unicode":201,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.982421875},"atlasBounds":{"left":2591.5,"bottom":6896.5,"right":2740.5,"top":7156.5}},{"unicode":202,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.982421875},"atlasBounds":{"left":2741.5,"bottom":6896.5,"right":2890.5,"top":7156.5}},{"unicode":203,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.970703125},"atlasBounds":{"left":3898.5,"bottom":6635.5,"right":4047.5,"top":6892.5}},{"unicode":204,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":0.982421875},"atlasBounds":{"left":2891.5,"bottom":6896.5,"right":3032.5,"top":7156.5}},{"unicode":205,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":0.982421875},"atlasBounds":{"left":3033.5,"bottom":6896.5,"right":3174.5,"top":7156.5}},{"unicode":206,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":0.982421875},"atlasBounds":{"left":3175.5,"bottom":6896.5,"right":3316.5,"top":7156.5}},{"unicode":207,"advance":0.59999999999999998,"planeBounds":{"left":0.037397569444444438,"bottom":-0.033203125,"right":0.5959913194444445,"top":0.970703125},"atlasBounds":{"left":4048.5,"bottom":6635.5,"right":4191.5,"top":6892.5}},{"unicode":208,"advance":0.59999999999999998,"planeBounds":{"left":-0.022661458333333283,"bottom":-0.033203125,"right":0.57499479166666678,"top":0.763671875},"atlasBounds":{"left":3369.5,"bottom":4585.5,"right":3522.5,"top":4789.5}},{"unicode":209,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.033203125,"right":0.60306250000000006,"top":0.978515625},"atlasBounds":{"left":933.5,"bottom":6633.5,"right":1081.5,"top":6892.5}},{"unicode":210,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.044921875,"right":0.57938729508196729,"top":0.982421875},"atlasBounds":{"left":5327.5,"bottom":7165.5,"right":5463.5,"top":7428.5}},{"unicode":211,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.044921875,"right":0.57938729508196729,"top":0.982421875},"atlasBounds":{"left":5464.5,"bottom":7165.5,"right":5600.5,"top":7428.5}},{"unicode":212,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.044921875,"right":0.57938729508196729,"top":0.982421875},"atlasBounds":{"left":5601.5,"bottom":7165.5,"right":5737.5,"top":7428.5}},{"unicode":213,"advance":0.59999999999999998,"planeBounds":{"left":0.049718237704918067,"bottom":-0.044921875,"right":0.59659323770491801,"top":0.978515625},"atlasBounds":{"left":1251.5,"bottom":6894.5,"right":1391.5,"top":7156.5}},{"unicode":214,"advance":0.59999999999999998,"planeBounds":{"left":0.048412682149362514,"bottom":-0.044921875,"right":0.59528768214936245,"top":0.970703125},"atlasBounds":{"left":3317.5,"bottom":6896.5,"right":3457.5,"top":7156.5}},{"unicode":215,"advance":0.59999999999999998,"planeBounds":{"left":0.061406250000000002,"bottom":0.107421875,"right":0.55359375,"top":0.556640625},"atlasBounds":{"left":7032.5,"bottom":3369.5,"right":7158.5,"top":3484.5}},{"unicode":216,"advance":0.59999999999999998,"planeBounds":{"left":-0.039062499999999993,"bottom":-0.072265625,"right":0.6640625,"top":0.783203125},"atlasBounds":{"left":3428.5,"bottom":5427.5,"right":3608.5,"top":5646.5}},{"unicode":217,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":0.982421875},"atlasBounds":{"left":5738.5,"bottom":7165.5,"right":5879.5,"top":7428.5}},{"unicode":218,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":0.982421875},"atlasBounds":{"left":5880.5,"bottom":7165.5,"right":6021.5,"top":7428.5}},{"unicode":219,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":0.982421875},"atlasBounds":{"left":6022.5,"bottom":7165.5,"right":6163.5,"top":7428.5}},{"unicode":220,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":0.970703125},"atlasBounds":{"left":3458.5,"bottom":6896.5,"right":3599.5,"top":7156.5}},{"unicode":221,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.982421875},"atlasBounds":{"left":3600.5,"bottom":6896.5,"right":3748.5,"top":7156.5}},{"unicode":222,"advance":0.59999999999999998,"planeBounds":{"left":0.022877520161290306,"bottom":-0.033203125,"right":0.5892837701612903,"top":0.763671875},"atlasBounds":{"left":4592.5,"bottom":4585.5,"right":4737.5,"top":4789.5}},{"unicode":223,"advance":0.59999999999999998,"planeBounds":{"left":-0.1651034482758621,"bottom":-0.212890625,"right":0.5848965517241379,"top":0.771484375},"atlasBounds":{"left":276.5,"bottom":6121.5,"right":468.5,"top":6373.5}},{"unicode":224,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.818359375},"atlasBounds":{"left":4487.5,"bottom":5653.5,"right":4620.5,"top":5874.5}},{"unicode":225,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.818359375},"atlasBounds":{"left":4621.5,"bottom":5653.5,"right":4754.5,"top":5874.5}},{"unicode":226,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.818359375},"atlasBounds":{"left":4755.5,"bottom":5653.5,"right":4888.5,"top":5874.5}},{"unicode":227,"advance":0.59999999999999998,"planeBounds":{"left":0.044490384615384584,"bottom":-0.044921875,"right":0.57574038461538457,"top":0.814453125},"atlasBounds":{"left":1435.5,"bottom":5426.5,"right":1571.5,"top":5646.5}},{"unicode":228,"advance":0.59999999999999998,"planeBounds":{"left":0.044137954059829024,"bottom":-0.044921875,"right":0.57148170405982901,"top":0.802734375},"atlasBounds":{"left":6667.5,"bottom":5429.5,"right":6802.5,"top":5646.5}},{"unicode":229,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.857421875},"atlasBounds":{"left":6433.5,"bottom":5889.5,"right":6566.5,"top":6120.5}},{"unicode":230,"advance":0.59999999999999998,"planeBounds":{"left":-0.019126816860465072,"bottom":-0.044921875,"right":0.61759193313953498,"top":0.591796875},"atlasBounds":{"left":1668.5,"bottom":3644.5,"right":1831.5,"top":3807.5}},{"unicode":231,"advance":0.59999999999999998,"planeBounds":{"left":0.054315883119486756,"bottom":-0.232421875,"right":0.56212838311948676,"top":0.591796875},"atlasBounds":{"left":2489.5,"bottom":5213.5,"right":2619.5,"top":5424.5}},{"unicode":232,"advance":0.59999999999999998,"planeBounds":{"left":0.04740393518518516,"bottom":-0.044921875,"right":0.55521643518518526,"top":0.818359375},"atlasBounds":{"left":4889.5,"bottom":5653.5,"right":5019.5,"top":5874.5}},{"unicode":233,"advance":0.59999999999999998,"planeBounds":{"left":0.047718749999999983,"bottom":-0.044921875,"right":0.55553125000000003,"top":0.818359375},"atlasBounds":{"left":5020.5,"bottom":5653.5,"right":5150.5,"top":5874.5}},{"unicode":234,"advance":0.59999999999999998,"planeBounds":{"left":0.04740393518518516,"bottom":-0.044921875,"right":0.55521643518518526,"top":0.818359375},"atlasBounds":{"left":5151.5,"bottom":5653.5,"right":5281.5,"top":5874.5}},{"unicode":235,"advance":0.59999999999999998,"planeBounds":{"left":0.047553819444444437,"bottom":-0.044921875,"right":0.56708506944444448,"top":0.802734375},"atlasBounds":{"left":6803.5,"bottom":5429.5,"right":6936.5,"top":5646.5}},{"unicode":236,"advance":0.59999999999999998,"planeBounds":{"left":0.011828124999999983,"bottom":-0.033203125,"right":0.53917187499999997,"top":0.818359375},"atlasBounds":{"left":4395.5,"bottom":5428.5,"right":4530.5,"top":5646.5}},{"unicode":237,"advance":0.59999999999999998,"planeBounds":{"left":0.012203124999999997,"bottom":-0.033203125,"right":0.57079687499999998,"top":0.818359375},"atlasBounds":{"left":4531.5,"bottom":5428.5,"right":4674.5,"top":5646.5}},{"unicode":238,"advance":0.59999999999999998,"planeBounds":{"left":0.012656249999999997,"bottom":-0.033203125,"right":0.56734375000000004,"top":0.818359375},"atlasBounds":{"left":4675.5,"bottom":5428.5,"right":4817.5,"top":5646.5}},{"unicode":239,"advance":0.59999999999999998,"planeBounds":{"left":0.012038194444444443,"bottom":-0.033203125,"right":0.58235069444444443,"top":0.802734375},"atlasBounds":{"left":8045.5,"bottom":6678.5,"right":8191.5,"top":6892.5}},{"unicode":240,"advance":0.59999999999999998,"planeBounds":{"left":0.031150862068965501,"bottom":-0.044921875,"right":0.56240086206896545,"top":0.763671875},"atlasBounds":{"left":2457.5,"bottom":4999.5,"right":2593.5,"top":5206.5}},{"unicode":241,"advance":0.59999999999999998,"planeBounds":{"left":0.026109374999999994,"bottom":-0.033203125,"right":0.57689062499999999,"top":0.814453125},"atlasBounds":{"left":6937.5,"bottom":5429.5,"right":7078.5,"top":5646.5}},{"unicode":242,"advance":0.59999999999999998,"planeBounds":{"left":0.049500000000000044,"bottom":-0.044921875,"right":0.54949999999999999,"top":0.818359375},"atlasBounds":{"left":5282.5,"bottom":5653.5,"right":5410.5,"top":5874.5}},{"unicode":243,"advance":0.59999999999999998,"planeBounds":{"left":0.048249487704918055,"bottom":-0.044921875,"right":0.55606198770491799,"top":0.818359375},"atlasBounds":{"left":5411.5,"bottom":5653.5,"right":5541.5,"top":5874.5}},{"unicode":244,"advance":0.59999999999999998,"planeBounds":{"left":0.048702612704918047,"bottom":-0.044921875,"right":0.55260886270491805,"top":0.818359375},"atlasBounds":{"left":5542.5,"bottom":5653.5,"right":5671.5,"top":5874.5}},{"unicode":245,"advance":0.59999999999999998,"planeBounds":{"left":0.048436987704918062,"bottom":-0.044921875,"right":0.571874487704918,"top":0.814453125},"atlasBounds":{"left":1572.5,"bottom":5426.5,"right":1706.5,"top":5646.5}},{"unicode":246,"advance":0.59999999999999998,"planeBounds":{"left":0.050037682149362557,"bottom":-0.044921875,"right":0.56566268214936255,"top":0.802734375},"atlasBounds":{"left":7079.5,"bottom":5429.5,"right":7211.5,"top":5646.5}},{"unicode":247,"advance":0.59999999999999998,"planeBounds":{"left":0.072671875000000011,"bottom":0.029296875,"right":0.54532812500000005,"top":0.638671875},"atlasBounds":{"left":5043.5,"bottom":3487.5,"right":5164.5,"top":3643.5}},{"unicode":248,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.064453125,"right":0.63203125000000004,"top":0.623046875},"atlasBounds":{"left":6081.5,"bottom":3998.5,"right":6251.5,"top":4174.5}},{"unicode":249,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.818359375},"atlasBounds":{"left":5672.5,"bottom":5653.5,"right":5806.5,"top":5874.5}},{"unicode":250,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.818359375},"atlasBounds":{"left":5807.5,"bottom":5653.5,"right":5941.5,"top":5874.5}},{"unicode":251,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.818359375},"atlasBounds":{"left":5942.5,"bottom":5653.5,"right":6076.5,"top":5874.5}},{"unicode":252,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.802734375},"atlasBounds":{"left":7212.5,"bottom":5429.5,"right":7346.5,"top":5646.5}},{"unicode":253,"advance":0.59999999999999998,"planeBounds":{"left":0.053905681818181776,"bottom":-0.212890625,"right":0.56953068181818178,"top":0.818359375},"atlasBounds":{"left":4099.5,"bottom":7164.5,"right":4231.5,"top":7428.5}},{"unicode":254,"advance":0.59999999999999998,"planeBounds":{"left":-0.0050619318181818447,"bottom":-0.212890625,"right":0.54962556818181818,"top":0.763671875},"atlasBounds":{"left":2203.5,"bottom":6123.5,"right":2345.5,"top":6373.5}},{"unicode":255,"advance":0.59999999999999998,"planeBounds":{"left":0.053905681818181776,"bottom":-0.212890625,"right":0.56953068181818178,"top":0.802734375},"atlasBounds":{"left":3749.5,"bottom":6896.5,"right":3881.5,"top":7156.5}},{"unicode":256,"advance":0.59999999999999998,"planeBounds":{"left":-0.014874999999999985,"bottom":-0.033203125,"right":0.57887500000000003,"top":0.935546875},"atlasBounds":{"left":5741.5,"bottom":6125.5,"right":5893.5,"top":6373.5}},{"unicode":257,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.767578125},"atlasBounds":{"left":311.5,"bottom":4998.5,"right":444.5,"top":5206.5}},{"unicode":258,"advance":0.59999999999999998,"planeBounds":{"left":-0.015734374999999981,"bottom":-0.033203125,"right":0.58973437500000003,"top":0.982421875},"atlasBounds":{"left":3882.5,"bottom":6896.5,"right":4037.5,"top":7156.5}},{"unicode":259,"advance":0.59999999999999998,"planeBounds":{"left":0.044396634615384581,"bottom":-0.044921875,"right":0.56783413461538457,"top":0.818359375},"atlasBounds":{"left":6077.5,"bottom":5653.5,"right":6211.5,"top":5874.5}},{"unicode":260,"advance":0.59999999999999998,"planeBounds":{"left":-0.015796875000000005,"bottom":-0.232421875,"right":0.54279687499999996,"top":0.763671875},"atlasBounds":{"left":7039.5,"bottom":6376.5,"right":7182.5,"top":6631.5}},{"unicode":261,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.232421875,"right":0.56438100961538451,"top":0.591796875},"atlasBounds":{"left":2620.5,"bottom":5213.5,"right":2753.5,"top":5424.5}},{"unicode":262,"advance":0.59999999999999998,"planeBounds":{"left":0.053439948156682029,"bottom":-0.044921875,"right":0.59250244815668207,"top":0.982421875},"atlasBounds":{"left":6164.5,"bottom":7165.5,"right":6302.5,"top":7428.5}},{"unicode":263,"advance":0.59999999999999998,"planeBounds":{"left":0.053034067622950778,"bottom":-0.044921875,"right":0.56475281762295082,"top":0.818359375},"atlasBounds":{"left":6212.5,"bottom":5653.5,"right":6343.5,"top":5874.5}},{"unicode":264,"advance":0.59999999999999998,"planeBounds":{"left":0.053439948156682029,"bottom":-0.044921875,"right":0.59250244815668207,"top":0.982421875},"atlasBounds":{"left":6303.5,"bottom":7165.5,"right":6441.5,"top":7428.5}},{"unicode":265,"advance":0.59999999999999998,"planeBounds":{"left":0.054383744347088724,"bottom":-0.044921875,"right":0.5621962443470887,"top":0.818359375},"atlasBounds":{"left":6344.5,"bottom":5653.5,"right":6474.5,"top":5874.5}},{"unicode":266,"advance":0.59999999999999998,"planeBounds":{"left":0.053439948156682029,"bottom":-0.044921875,"right":0.59250244815668207,"top":0.970703125},"atlasBounds":{"left":4038.5,"bottom":6896.5,"right":4176.5,"top":7156.5}},{"unicode":267,"advance":0.59999999999999998,"planeBounds":{"left":0.054383744347088724,"bottom":-0.044921875,"right":0.5621962443470887,"top":0.802734375},"atlasBounds":{"left":7347.5,"bottom":5429.5,"right":7477.5,"top":5646.5}},{"unicode":268,"advance":0.59999999999999998,"planeBounds":{"left":0.05379485887096775,"bottom":-0.044921875,"right":0.6045761088709678,"top":0.982421875},"atlasBounds":{"left":6442.5,"bottom":7165.5,"right":6583.5,"top":7428.5}},{"unicode":269,"advance":0.59999999999999998,"planeBounds":{"left":0.053268442622950794,"bottom":-0.044921875,"right":0.58451844262295083,"top":0.818359375},"atlasBounds":{"left":6475.5,"bottom":5653.5,"right":6611.5,"top":5874.5}},{"unicode":270,"advance":0.59999999999999998,"planeBounds":{"left":0.026390625000000004,"bottom":-0.033203125,"right":0.600609375,"top":0.982421875},"atlasBounds":{"left":4177.5,"bottom":6896.5,"right":4324.5,"top":7156.5}},{"unicode":271,"advance":0.59999999999999998,"planeBounds":{"left":0.02868780637254905,"bottom":-0.044921875,"right":0.70446905637254909,"top":0.763671875},"atlasBounds":{"left":2864.5,"bottom":4999.5,"right":3037.5,"top":5206.5}},{"unicode":272,"advance":0.59999999999999998,"planeBounds":{"left":-0.022661458333333283,"bottom":-0.033203125,"right":0.57499479166666678,"top":0.763671875},"atlasBounds":{"left":5328.5,"bottom":4585.5,"right":5481.5,"top":4789.5}},{"unicode":273,"advance":0.59999999999999998,"planeBounds":{"left":0.049778935185185176,"bottom":-0.044921875,"right":0.68259143518518528,"top":0.763671875},"atlasBounds":{"left":3038.5,"bottom":4999.5,"right":3200.5,"top":5206.5}},{"unicode":274,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.935546875},"atlasBounds":{"left":5894.5,"bottom":6125.5,"right":6043.5,"top":6373.5}},{"unicode":275,"advance":0.59999999999999998,"planeBounds":{"left":0.04740393518518516,"bottom":-0.044921875,"right":0.55521643518518526,"top":0.767578125},"atlasBounds":{"left":445.5,"bottom":4998.5,"right":575.5,"top":5206.5}},{"unicode":276,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.982421875},"atlasBounds":{"left":4325.5,"bottom":6896.5,"right":4474.5,"top":7156.5}},{"unicode":277,"advance":0.59999999999999998,"planeBounds":{"left":0.047812499999999994,"bottom":-0.044921875,"right":0.56343750000000004,"top":0.818359375},"atlasBounds":{"left":6612.5,"bottom":5653.5,"right":6744.5,"top":5874.5}},{"unicode":278,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.970703125},"atlasBounds":{"left":4192.5,"bottom":6635.5,"right":4341.5,"top":6892.5}},{"unicode":279,"advance":0.59999999999999998,"planeBounds":{"left":0.04740393518518516,"bottom":-0.044921875,"right":0.55521643518518526,"top":0.802734375},"atlasBounds":{"left":7478.5,"bottom":5429.5,"right":7608.5,"top":5646.5}},{"unicode":280,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.232421875,"right":0.61801562499999996,"top":0.763671875},"atlasBounds":{"left":7183.5,"bottom":6376.5,"right":7332.5,"top":6631.5}},{"unicode":281,"advance":0.59999999999999998,"planeBounds":{"left":0.049500000000000002,"bottom":-0.232421875,"right":0.54949999999999999,"top":0.591796875},"atlasBounds":{"left":2754.5,"bottom":5213.5,"right":2882.5,"top":5424.5}},{"unicode":282,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.982421875},"atlasBounds":{"left":4475.5,"bottom":6896.5,"right":4624.5,"top":7156.5}},{"unicode":283,"advance":0.59999999999999998,"planeBounds":{"left":0.047953124999999999,"bottom":-0.044921875,"right":0.57529687500000004,"top":0.818359375},"atlasBounds":{"left":6745.5,"bottom":5653.5,"right":6880.5,"top":5874.5}},{"unicode":284,"advance":0.59999999999999998,"planeBounds":{"left":0.049439948156682033,"bottom":-0.044921875,"right":0.58850244815668207,"top":0.982421875},"atlasBounds":{"left":6584.5,"bottom":7165.5,"right":6722.5,"top":7428.5}},{"unicode":285,"advance":0.59999999999999998,"planeBounds":{"left":0.056372685185185227,"bottom":-0.212890625,"right":0.57199768518518523,"top":0.818359375},"atlasBounds":{"left":4232.5,"bottom":7164.5,"right":4364.5,"top":7428.5}},{"unicode":286,"advance":0.59999999999999998,"planeBounds":{"left":0.04970110887096775,"bottom":-0.044921875,"right":0.59266985887096779,"top":0.982421875},"atlasBounds":{"left":6723.5,"bottom":7165.5,"right":6862.5,"top":7428.5}},{"unicode":287,"advance":0.59999999999999998,"planeBounds":{"left":0.056372685185185227,"bottom":-0.212890625,"right":0.57199768518518523,"top":0.818359375},"atlasBounds":{"left":4365.5,"bottom":7164.5,"right":4497.5,"top":7428.5}},{"unicode":288,"advance":0.59999999999999998,"planeBounds":{"left":0.049439948156682033,"bottom":-0.044921875,"right":0.58850244815668207,"top":0.970703125},"atlasBounds":{"left":4625.5,"bottom":6896.5,"right":4763.5,"top":7156.5}},{"unicode":289,"advance":0.59999999999999998,"planeBounds":{"left":0.056372685185185227,"bottom":-0.212890625,"right":0.57199768518518523,"top":0.802734375},"atlasBounds":{"left":4764.5,"bottom":6896.5,"right":4896.5,"top":7156.5}},{"unicode":290,"advance":0.59999999999999998,"planeBounds":{"left":0.049439948156682033,"bottom":-0.271484375,"right":0.58850244815668207,"top":0.771484375},"atlasBounds":{"left":1625.5,"bottom":7161.5,"right":1763.5,"top":7428.5}},{"unicode":291,"advance":0.59999999999999998,"planeBounds":{"left":0.056372685185185227,"bottom":-0.212890625,"right":0.57199768518518523,"top":0.912109375},"atlasBounds":{"left":3287.5,"bottom":7496.5,"right":3419.5,"top":7784.5}},{"unicode":292,"advance":0.59999999999999998,"planeBounds":{"left":0.026890625000000005,"bottom":-0.033203125,"right":0.60110937500000006,"top":0.982421875},"atlasBounds":{"left":4897.5,"bottom":6896.5,"right":5044.5,"top":7156.5}},{"unicode":293,"advance":0.59999999999999998,"planeBounds":{"left":0.014703693181818148,"bottom":-0.033203125,"right":0.54985994318181819,"top":0.978515625},"atlasBounds":{"left":1082.5,"bottom":6633.5,"right":1219.5,"top":6892.5}},{"unicode":294,"advance":0.59999999999999998,"planeBounds":{"left":0.019328124999999988,"bottom":-0.033203125,"right":0.67167187500000003,"top":0.763671875},"atlasBounds":{"left":8024.5,"bottom":6952.5,"right":8191.5,"top":7156.5}},{"unicode":295,"advance":0.59999999999999998,"planeBounds":{"left":0.020516193181818194,"bottom":-0.033203125,"right":0.54004744318181819,"top":0.763671875},"atlasBounds":{"left":6511.5,"bottom":4585.5,"right":6644.5,"top":4789.5}},{"unicode":296,"advance":0.59999999999999998,"planeBounds":{"left":0.038703124999999998,"bottom":-0.033203125,"right":0.59729687500000006,"top":0.978515625},"atlasBounds":{"left":1220.5,"bottom":6633.5,"right":1363.5,"top":6892.5}},{"unicode":297,"advance":0.59999999999999998,"planeBounds":{"left":0.012390625000000004,"bottom":-0.033203125,"right":0.58660937499999999,"top":0.814453125},"atlasBounds":{"left":7609.5,"bottom":5429.5,"right":7756.5,"top":5646.5}},{"unicode":298,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":0.935546875},"atlasBounds":{"left":6225.5,"bottom":6125.5,"right":6366.5,"top":6373.5}},{"unicode":299,"advance":0.59999999999999998,"planeBounds":{"left":0.012656249999999997,"bottom":-0.033203125,"right":0.56734375000000004,"top":0.767578125},"atlasBounds":{"left":808.5,"bottom":4791.5,"right":950.5,"top":4996.5}},{"unicode":300,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":0.982421875},"atlasBounds":{"left":5045.5,"bottom":6896.5,"right":5186.5,"top":7156.5}},{"unicode":301,"advance":0.59999999999999998,"planeBounds":{"left":0.012296875000000001,"bottom":-0.033203125,"right":0.57870312499999998,"top":0.818359375},"atlasBounds":{"left":4818.5,"bottom":5428.5,"right":4963.5,"top":5646.5}},{"unicode":302,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.232421875,"right":0.58939062500000006,"top":0.763671875},"atlasBounds":{"left":7443.5,"bottom":6376.5,"right":7584.5,"top":6631.5}},{"unicode":303,"advance":0.59999999999999998,"planeBounds":{"left":0.011828124999999983,"bottom":-0.232421875,"right":0.53917187499999997,"top":0.802734375},"atlasBounds":{"left":2735.5,"bottom":7163.5,"right":2870.5,"top":7428.5}},{"unicode":304,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":0.970703125},"atlasBounds":{"left":4342.5,"bottom":6635.5,"right":4483.5,"top":6892.5}},{"unicode":305,"advance":0.59999999999999998,"planeBounds":{"left":0.011828124999999983,"bottom":-0.033203125,"right":0.53917187499999997,"top":0.583984375},"atlasBounds":{"left":1131.5,"bottom":3485.5,"right":1266.5,"top":3643.5}},{"unicode":308,"advance":0.59999999999999998,"planeBounds":{"left":0.011810483870967772,"bottom":-0.044921875,"right":0.73056048387096784,"top":0.982421875},"atlasBounds":{"left":6863.5,"bottom":7165.5,"right":7047.5,"top":7428.5}},{"unicode":309,"advance":0.59999999999999998,"planeBounds":{"left":-0.0038593749999999683,"bottom":-0.212890625,"right":0.632859375,"top":0.818359375},"atlasBounds":{"left":4498.5,"bottom":7164.5,"right":4661.5,"top":7428.5}},{"unicode":310,"advance":0.59999999999999998,"planeBounds":{"left":0.025906250000000023,"bottom":-0.271484375,"right":0.64309375000000002,"top":0.763671875},"atlasBounds":{"left":2871.5,"bottom":7163.5,"right":3029.5,"top":7428.5}},{"unicode":311,"advance":0.59999999999999998,"planeBounds":{"left":0.030937500000000007,"bottom":-0.271484375,"right":0.60906250000000006,"top":0.763671875},"atlasBounds":{"left":3030.5,"bottom":7163.5,"right":3178.5,"top":7428.5}},{"unicode":312,"advance":0.59999999999999998,"planeBounds":{"left":0.030937500000000007,"bottom":-0.033203125,"right":0.60906250000000006,"top":0.583984375},"atlasBounds":{"left":577.5,"bottom":3485.5,"right":725.5,"top":3643.5}},{"unicode":313,"advance":0.59999999999999998,"planeBounds":{"left":0.059812499999999991,"bottom":-0.033203125,"right":0.54418750000000005,"top":0.982421875},"atlasBounds":{"left":5187.5,"bottom":6896.5,"right":5311.5,"top":7156.5}},{"unicode":314,"advance":0.59999999999999998,"planeBounds":{"left":0.072125000000000022,"bottom":-0.033203125,"right":0.54087499999999999,"top":0.982421875},"atlasBounds":{"left":5312.5,"bottom":6896.5,"right":5432.5,"top":7156.5}},{"unicode":315,"advance":0.59999999999999998,"planeBounds":{"left":0.059812499999999991,"bottom":-0.271484375,"right":0.54418750000000005,"top":0.763671875},"atlasBounds":{"left":3179.5,"bottom":7163.5,"right":3303.5,"top":7428.5}},{"unicode":316,"advance":0.59999999999999998,"planeBounds":{"left":0.072125000000000022,"bottom":-0.271484375,"right":0.54087499999999999,"top":0.763671875},"atlasBounds":{"left":3304.5,"bottom":7163.5,"right":3424.5,"top":7428.5}},{"unicode":317,"advance":0.59999999999999998,"planeBounds":{"left":0.059812499999999991,"bottom":-0.033203125,"right":0.54418750000000005,"top":0.763671875},"atlasBounds":{"left":6792.5,"bottom":4585.5,"right":6916.5,"top":4789.5}},{"unicode":318,"advance":0.59999999999999998,"planeBounds":{"left":0.072328124999999993,"bottom":-0.033203125,"right":0.59967187499999997,"top":0.763671875},"atlasBounds":{"left":6917.5,"bottom":4585.5,"right":7052.5,"top":4789.5}},{"unicode":319,"advance":0.59999999999999998,"planeBounds":{"left":0.059812499999999991,"bottom":-0.033203125,"right":0.54418750000000005,"top":0.763671875},"atlasBounds":{"left":7186.5,"bottom":4585.5,"right":7310.5,"top":4789.5}},{"unicode":320,"advance":0.59999999999999998,"planeBounds":{"left":0.052468749999999988,"bottom":-0.033203125,"right":0.65403125000000006,"top":0.763671875},"atlasBounds":{"left":8037.5,"bottom":5220.5,"right":8191.5,"top":5424.5}},{"unicode":321,"advance":0.59999999999999998,"planeBounds":{"left":-0.019656249999999997,"bottom":-0.033203125,"right":0.55065624999999996,"top":0.763671875},"atlasBounds":{"left":7311.5,"bottom":4585.5,"right":7457.5,"top":4789.5}},{"unicode":322,"advance":0.59999999999999998,"planeBounds":{"left":0.068171875000000021,"bottom":-0.033203125,"right":0.54082812499999999,"top":0.763671875},"atlasBounds":{"left":7458.5,"bottom":4585.5,"right":7579.5,"top":4789.5}},{"unicode":323,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.033203125,"right":0.60306250000000006,"top":0.982421875},"atlasBounds":{"left":5433.5,"bottom":6896.5,"right":5581.5,"top":7156.5}},{"unicode":324,"advance":0.59999999999999998,"planeBounds":{"left":0.025921874999999987,"bottom":-0.033203125,"right":0.56107812499999998,"top":0.818359375},"atlasBounds":{"left":4964.5,"bottom":5428.5,"right":5101.5,"top":5646.5}},{"unicode":325,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.271484375,"right":0.60306250000000006,"top":0.763671875},"atlasBounds":{"left":3425.5,"bottom":7163.5,"right":3573.5,"top":7428.5}},{"unicode":326,"advance":0.59999999999999998,"planeBounds":{"left":0.026563068181818143,"bottom":-0.271484375,"right":0.55000056818181819,"top":0.591796875},"atlasBounds":{"left":6881.5,"bottom":5653.5,"right":7015.5,"top":5874.5}},{"unicode":327,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.033203125,"right":0.60306250000000006,"top":0.982421875},"atlasBounds":{"left":5582.5,"bottom":6896.5,"right":5730.5,"top":7156.5}},{"unicode":328,"advance":0.59999999999999998,"planeBounds":{"left":0.026156249999999995,"bottom":-0.033203125,"right":0.58084374999999999,"top":0.818359375},"atlasBounds":{"left":5102.5,"bottom":5428.5,"right":5244.5,"top":5646.5}},{"unicode":329,"advance":0.59999999999999998,"planeBounds":{"left":0.021609943181818147,"bottom":-0.033203125,"right":0.54895369318181819,"top":0.822265625},"atlasBounds":{"left":3770.5,"bottom":5427.5,"right":3905.5,"top":5646.5}},{"unicode":330,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.212890625,"right":0.60306250000000006,"top":0.763671875},"atlasBounds":{"left":2346.5,"bottom":6123.5,"right":2494.5,"top":6373.5}},{"unicode":331,"advance":0.59999999999999998,"planeBounds":{"left":0.028516193181818143,"bottom":-0.212890625,"right":0.54804744318181819,"top":0.591796875},"atlasBounds":{"left":6056.5,"bottom":5000.5,"right":6189.5,"top":5206.5}},{"unicode":332,"advance":0.59999999999999998,"planeBounds":{"left":0.048530737704918059,"bottom":-0.044921875,"right":0.579780737704918,"top":0.935546875},"atlasBounds":{"left":1475.5,"bottom":6122.5,"right":1611.5,"top":6373.5}},{"unicode":333,"advance":0.59999999999999998,"planeBounds":{"left":0.048702612704918047,"bottom":-0.044921875,"right":0.55260886270491805,"top":0.767578125},"atlasBounds":{"left":576.5,"bottom":4998.5,"right":705.5,"top":5206.5}},{"unicode":334,"advance":0.59999999999999998,"planeBounds":{"left":0.04962448770491807,"bottom":-0.044921875,"right":0.58868698770491801,"top":0.982421875},"atlasBounds":{"left":7048.5,"bottom":7165.5,"right":7186.5,"top":7428.5}},{"unicode":335,"advance":0.59999999999999998,"planeBounds":{"left":0.048343237704918052,"bottom":-0.044921875,"right":0.563968237704918,"top":0.818359375},"atlasBounds":{"left":7016.5,"bottom":5653.5,"right":7148.5,"top":5874.5}},{"unicode":336,"advance":0.59999999999999998,"planeBounds":{"left":0.049374487704918098,"bottom":-0.044921875,"right":0.65093698770491815,"top":0.982421875},"atlasBounds":{"left":7187.5,"bottom":7165.5,"right":7341.5,"top":7428.5}},{"unicode":337,"advance":0.59999999999999998,"planeBounds":{"left":0.050046362704918079,"bottom":-0.044921875,"right":0.62426511270491813,"top":0.818359375},"atlasBounds":{"left":7149.5,"bottom":5653.5,"right":7296.5,"top":5874.5}},{"unicode":338,"advance":0.59999999999999998,"planeBounds":{"left":-0.01225,"bottom":-0.044921875,"right":0.67525000000000002,"top":0.771484375},"atlasBounds":{"left":5009.5,"bottom":5215.5,"right":5185.5,"top":5424.5}},{"unicode":339,"advance":0.59999999999999998,"planeBounds":{"left":-0.020845999446290085,"bottom":-0.044921875,"right":0.61977900055370994,"top":0.591796875},"atlasBounds":{"left":6941.5,"bottom":3821.5,"right":7105.5,"top":3984.5}},{"unicode":340,"advance":0.59999999999999998,"planeBounds":{"left":0.026161637931034491,"bottom":-0.033203125,"right":0.60428663793103454,"top":0.982421875},"atlasBounds":{"left":5731.5,"bottom":6896.5,"right":5879.5,"top":7156.5}},{"unicode":341,"advance":0.59999999999999998,"planeBounds":{"left":0.047828124999999985,"bottom":-0.033203125,"right":0.575171875,"top":0.818359375},"atlasBounds":{"left":5245.5,"bottom":5428.5,"right":5380.5,"top":5646.5}},{"unicode":342,"advance":0.59999999999999998,"planeBounds":{"left":0.026161637931034491,"bottom":-0.271484375,"right":0.60428663793103454,"top":0.763671875},"atlasBounds":{"left":3574.5,"bottom":7163.5,"right":3722.5,"top":7428.5}},{"unicode":343,"advance":0.59999999999999998,"planeBounds":{"left":-0.070195913461538445,"bottom":-0.271484375,"right":0.56652283653846158,"top":0.591796875},"atlasBounds":{"left":7297.5,"bottom":5653.5,"right":7460.5,"top":5874.5}},{"unicode":344,"advance":0.59999999999999998,"planeBounds":{"left":0.026161637931034491,"bottom":-0.033203125,"right":0.60428663793103454,"top":0.982421875},"atlasBounds":{"left":5880.5,"bottom":6896.5,"right":6028.5,"top":7156.5}},{"unicode":345,"advance":0.59999999999999998,"planeBounds":{"left":0.048062499999999994,"bottom":-0.033203125,"right":0.59493750000000001,"top":0.818359375},"atlasBounds":{"left":5381.5,"bottom":5428.5,"right":5521.5,"top":5646.5}},{"unicode":346,"advance":0.59999999999999998,"planeBounds":{"left":0.032109375000000002,"bottom":-0.044921875,"right":0.582890625,"top":0.982421875},"atlasBounds":{"left":7342.5,"bottom":7165.5,"right":7483.5,"top":7428.5}},{"unicode":347,"advance":0.59999999999999998,"planeBounds":{"left":0.032550951086956575,"bottom":-0.041015625,"right":0.5598947010869566,"top":0.818359375},"atlasBounds":{"left":1707.5,"bottom":5426.5,"right":1842.5,"top":5646.5}},{"unicode":348,"advance":0.59999999999999998,"planeBounds":{"left":0.032109375000000002,"bottom":-0.044921875,"right":0.582890625,"top":0.982421875},"atlasBounds":{"left":7484.5,"bottom":7165.5,"right":7625.5,"top":7428.5}},{"unicode":349,"advance":0.59999999999999998,"planeBounds":{"left":0.032550951086956575,"bottom":-0.041015625,"right":0.5598947010869566,"top":0.818359375},"atlasBounds":{"left":1843.5,"bottom":5426.5,"right":1978.5,"top":5646.5}},{"unicode":350,"advance":0.59999999999999998,"planeBounds":{"left":0.031961226851851834,"bottom":-0.232421875,"right":0.58274247685185188,"top":0.771484375},"atlasBounds":{"left":4484.5,"bottom":6635.5,"right":4625.5,"top":6892.5}},{"unicode":351,"advance":0.59999999999999998,"planeBounds":{"left":0.032550951086956575,"bottom":-0.232421875,"right":0.5598947010869566,"top":0.591796875},"atlasBounds":{"left":2883.5,"bottom":5213.5,"right":3018.5,"top":5424.5}},{"unicode":352,"advance":0.59999999999999998,"planeBounds":{"left":0.033130208333333362,"bottom":-0.044921875,"right":0.59953645833333336,"top":0.982421875},"atlasBounds":{"left":7626.5,"bottom":7165.5,"right":7771.5,"top":7428.5}},{"unicode":353,"advance":0.59999999999999998,"planeBounds":{"left":0.032640625,"bottom":-0.041015625,"right":0.57560937499999998,"top":0.818359375},"atlasBounds":{"left":1979.5,"bottom":5426.5,"right":2118.5,"top":5646.5}},{"unicode":354,"advance":0.59999999999999998,"planeBounds":{"left":0.087703125000000007,"bottom":-0.232421875,"right":0.64629687499999999,"top":0.763671875},"atlasBounds":{"left":7585.5,"bottom":6376.5,"right":7728.5,"top":6631.5}},{"unicode":355,"advance":0.59999999999999998,"planeBounds":{"left":0.058328124999999988,"bottom":-0.232421875,"right":0.58567187500000006,"top":0.736328125},"atlasBounds":{"left":6548.5,"bottom":6125.5,"right":6683.5,"top":6373.5}},{"unicode":356,"advance":0.59999999999999998,"planeBounds":{"left":0.087703125000000007,"bottom":-0.033203125,"right":0.64629687499999999,"top":0.982421875},"atlasBounds":{"left":6029.5,"bottom":6896.5,"right":6172.5,"top":7156.5}},{"unicode":357,"advance":0.59999999999999998,"planeBounds":{"left":0.058328124999999988,"bottom":-0.033203125,"right":0.58567187500000006,"top":0.833984375},"atlasBounds":{"left":3558.5,"bottom":5652.5,"right":3693.5,"top":5874.5}},{"unicode":358,"advance":0.59999999999999998,"planeBounds":{"left":0.087703125000000007,"bottom":-0.033203125,"right":0.64629687499999999,"top":0.763671875},"atlasBounds":{"left":697.5,"bottom":4380.5,"right":840.5,"top":4584.5}},{"unicode":359,"advance":0.59999999999999998,"planeBounds":{"left":0.054374999999999986,"bottom":-0.033203125,"right":0.58562500000000006,"top":0.736328125},"atlasBounds":{"left":5720.5,"bottom":4182.5,"right":5856.5,"top":4379.5}},{"unicode":360,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":0.978515625},"atlasBounds":{"left":1392.5,"bottom":6894.5,"right":1533.5,"top":7156.5}},{"unicode":361,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.814453125},"atlasBounds":{"left":2119.5,"bottom":5426.5,"right":2253.5,"top":5646.5}},{"unicode":362,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":0.935546875},"atlasBounds":{"left":1612.5,"bottom":6122.5,"right":1753.5,"top":6373.5}},{"unicode":363,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.767578125},"atlasBounds":{"left":706.5,"bottom":4998.5,"right":840.5,"top":5206.5}},{"unicode":364,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":0.982421875},"atlasBounds":{"left":7772.5,"bottom":7165.5,"right":7913.5,"top":7428.5}},{"unicode":365,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.818359375},"atlasBounds":{"left":7461.5,"bottom":5653.5,"right":7595.5,"top":5874.5}},{"unicode":366,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":1.017578125},"atlasBounds":{"left":7879.5,"bottom":7512.5,"right":8020.5,"top":7784.5}},{"unicode":367,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.857421875},"atlasBounds":{"left":6567.5,"bottom":5889.5,"right":6701.5,"top":6120.5}},{"unicode":368,"advance":0.59999999999999998,"planeBounds":{"left":0.052765095338983097,"bottom":-0.044921875,"right":0.6504213453389831,"top":0.982421875},"atlasBounds":{"left":7914.5,"bottom":7165.5,"right":8067.5,"top":7428.5}},{"unicode":369,"advance":0.59999999999999998,"planeBounds":{"left":0.051483845338983078,"bottom":-0.044921875,"right":0.62570259533898309,"top":0.818359375},"atlasBounds":{"left":7596.5,"bottom":5653.5,"right":7743.5,"top":5874.5}},{"unicode":370,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.232421875,"right":0.60298384533898308,"top":0.763671875},"atlasBounds":{"left":7729.5,"bottom":6376.5,"right":7870.5,"top":6631.5}},{"unicode":371,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.232421875,"right":0.57481197033898312,"top":0.583984375},"atlasBounds":{"left":5319.5,"bottom":5215.5,"right":5453.5,"top":5424.5}},{"unicode":372,"advance":0.59999999999999998,"planeBounds":{"left":0.044046875000000027,"bottom":-0.033203125,"right":0.67295312500000004,"top":0.982421875},"atlasBounds":{"left":6173.5,"bottom":6896.5,"right":6334.5,"top":7156.5}},{"unicode":373,"advance":0.59999999999999998,"planeBounds":{"left":0.051984375000000006,"bottom":-0.033203125,"right":0.63401562499999997,"top":0.818359375},"atlasBounds":{"left":5522.5,"bottom":5428.5,"right":5671.5,"top":5646.5}},{"unicode":374,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.982421875},"atlasBounds":{"left":6335.5,"bottom":6896.5,"right":6483.5,"top":7156.5}},{"unicode":375,"advance":0.59999999999999998,"planeBounds":{"left":0.053905681818181776,"bottom":-0.212890625,"right":0.56953068181818178,"top":0.818359375},"atlasBounds":{"left":4662.5,"bottom":7164.5,"right":4794.5,"top":7428.5}},{"unicode":376,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.970703125},"atlasBounds":{"left":4626.5,"bottom":6635.5,"right":4774.5,"top":6892.5}},{"unicode":377,"advance":0.59999999999999998,"planeBounds":{"left":0.013078125000000012,"bottom":-0.033203125,"right":0.60292187500000005,"top":0.982421875},"atlasBounds":{"left":6484.5,"bottom":6896.5,"right":6635.5,"top":7156.5}},{"unicode":378,"advance":0.59999999999999998,"planeBounds":{"left":0.02201562499999999,"bottom":-0.033203125,"right":0.56498437499999998,"top":0.818359375},"atlasBounds":{"left":5672.5,"bottom":5428.5,"right":5811.5,"top":5646.5}},{"unicode":379,"advance":0.59999999999999998,"planeBounds":{"left":0.013078125000000012,"bottom":-0.033203125,"right":0.60292187500000005,"top":0.970703125},"atlasBounds":{"left":4775.5,"bottom":6635.5,"right":4926.5,"top":6892.5}},{"unicode":380,"advance":0.59999999999999998,"planeBounds":{"left":0.02201562499999999,"bottom":-0.033203125,"right":0.56498437499999998,"top":0.802734375},"atlasBounds":{"left":863.5,"bottom":5210.5,"right":1002.5,"top":5424.5}},{"unicode":381,"advance":0.59999999999999998,"planeBounds":{"left":0.013078125000000012,"bottom":-0.033203125,"right":0.60292187500000005,"top":0.982421875},"atlasBounds":{"left":6636.5,"bottom":6896.5,"right":6787.5,"top":7156.5}},{"unicode":382,"advance":0.59999999999999998,"planeBounds":{"left":0.021156249999999998,"bottom":-0.033203125,"right":0.57584374999999999,"top":0.818359375},"atlasBounds":{"left":5812.5,"bottom":5428.5,"right":5954.5,"top":5646.5}},{"unicode":383,"advance":0.59999999999999998,"planeBounds":{"left":-0.066468749999999993,"bottom":-0.212890625,"right":0.64446875000000003,"top":0.763671875},"atlasBounds":{"left":2495.5,"bottom":6123.5,"right":2677.5,"top":6373.5}},{"unicode":399,"advance":0.59999999999999998,"planeBounds":{"left":0.050296345338983091,"bottom":-0.044921875,"right":0.57764009533898308,"top":0.771484375},"atlasBounds":{"left":5454.5,"bottom":5215.5,"right":5589.5,"top":5424.5}},{"unicode":400,"advance":0.59999999999999998,"planeBounds":{"left":0.031674596774193611,"bottom":-0.044921875,"right":0.6097995967741936,"top":0.771484375},"atlasBounds":{"left":5590.5,"bottom":5215.5,"right":5738.5,"top":5424.5}},{"unicode":402,"advance":0.59999999999999998,"planeBounds":{"left":-0.10514062499999999,"bottom":-0.212890625,"right":0.63314062500000001,"top":0.763671875},"atlasBounds":{"left":2678.5,"bottom":6123.5,"right":2867.5,"top":6373.5}},{"unicode":416,"advance":0.59999999999999998,"planeBounds":{"left":0.048733862704918092,"bottom":-0.044921875,"right":0.63857761270491809,"top":0.841796875},"atlasBounds":{"left":1632.5,"bottom":5647.5,"right":1783.5,"top":5874.5}},{"unicode":417,"advance":0.59999999999999998,"planeBounds":{"left":0.049999487704918084,"bottom":-0.044921875,"right":0.62031198770491813,"top":0.673828125},"atlasBounds":{"left":1706.5,"bottom":3990.5,"right":1852.5,"top":4174.5}},{"unicode":431,"advance":0.59999999999999998,"planeBounds":{"left":0.05246822033898306,"bottom":-0.044921875,"right":0.70871822033898313,"top":0.873046875},"atlasBounds":{"left":4956.5,"bottom":5885.5,"right":5124.5,"top":6120.5}},{"unicode":432,"advance":0.59999999999999998,"planeBounds":{"left":0.051140095338983053,"bottom":-0.044921875,"right":0.68004634533898312,"top":0.693359375},"atlasBounds":{"left":0.5,"bottom":3985.5,"right":161.5,"top":4174.5}},{"unicode":461,"advance":0.59999999999999998,"planeBounds":{"left":-0.01609374999999998,"bottom":-0.033203125,"right":0.60109374999999998,"top":0.982421875},"atlasBounds":{"left":6788.5,"bottom":6896.5,"right":6946.5,"top":7156.5}},{"unicode":486,"advance":0.59999999999999998,"planeBounds":{"left":0.049341733870967762,"bottom":-0.044921875,"right":0.60402923387096785,"top":0.982421875},"atlasBounds":{"left":0.5,"bottom":6893.5,"right":142.5,"top":7156.5}},{"unicode":487,"advance":0.59999999999999998,"planeBounds":{"left":0.056372685185185227,"bottom":-0.212890625,"right":0.57199768518518523,"top":0.818359375},"atlasBounds":{"left":4795.5,"bottom":7164.5,"right":4927.5,"top":7428.5}},{"unicode":490,"advance":0.59999999999999998,"planeBounds":{"left":0.048345253833950275,"bottom":-0.232421875,"right":0.57959525383395039,"top":0.771484375},"atlasBounds":{"left":4927.5,"bottom":6635.5,"right":5063.5,"top":6892.5}},{"unicode":491,"advance":0.59999999999999998,"planeBounds":{"left":0.049500000000000044,"bottom":-0.232421875,"right":0.54949999999999999,"top":0.591796875},"atlasBounds":{"left":3019.5,"bottom":5213.5,"right":3147.5,"top":5424.5}},{"unicode":500,"advance":0.59999999999999998,"planeBounds":{"left":0.049439948156682033,"bottom":-0.044921875,"right":0.58850244815668207,"top":0.982421875},"atlasBounds":{"left":143.5,"bottom":6893.5,"right":281.5,"top":7156.5}},{"unicode":501,"advance":0.59999999999999998,"planeBounds":{"left":0.056372685185185227,"bottom":-0.212890625,"right":0.57199768518518523,"top":0.818359375},"atlasBounds":{"left":4928.5,"bottom":7164.5,"right":5060.5,"top":7428.5}},{"unicode":508,"advance":0.59999999999999998,"planeBounds":{"left":-0.044874999999999984,"bottom":-0.033203125,"right":0.673875,"top":0.982421875},"atlasBounds":{"left":6947.5,"bottom":6896.5,"right":7131.5,"top":7156.5}},{"unicode":509,"advance":0.59999999999999998,"planeBounds":{"left":-0.019126816860465072,"bottom":-0.044921875,"right":0.61759193313953498,"top":0.818359375},"atlasBounds":{"left":7744.5,"bottom":5653.5,"right":7907.5,"top":5874.5}},{"unicode":510,"advance":0.59999999999999998,"planeBounds":{"left":-0.039062499999999993,"bottom":-0.072265625,"right":0.6640625,"top":0.982421875},"atlasBounds":{"left":701.5,"bottom":7158.5,"right":881.5,"top":7428.5}},{"unicode":511,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.064453125,"right":0.63203125000000004,"top":0.818359375},"atlasBounds":{"left":2269.5,"bottom":5648.5,"right":2439.5,"top":5874.5}},{"unicode":536,"advance":0.59999999999999998,"planeBounds":{"left":0.032109375000000002,"bottom":-0.271484375,"right":0.582890625,"top":0.771484375},"atlasBounds":{"left":1764.5,"bottom":7161.5,"right":1905.5,"top":7428.5}},{"unicode":537,"advance":0.59999999999999998,"planeBounds":{"left":0.032550951086956575,"bottom":-0.271484375,"right":0.5598947010869566,"top":0.591796875},"atlasBounds":{"left":7908.5,"bottom":5653.5,"right":8043.5,"top":5874.5}},{"unicode":538,"advance":0.59999999999999998,"planeBounds":{"left":0.087703125000000007,"bottom":-0.271484375,"right":0.64629687499999999,"top":0.763671875},"atlasBounds":{"left":3723.5,"bottom":7163.5,"right":3866.5,"top":7428.5}},{"unicode":539,"advance":0.59999999999999998,"planeBounds":{"left":0.058328124999999988,"bottom":-0.271484375,"right":0.58567187500000006,"top":0.736328125},"atlasBounds":{"left":1940.5,"bottom":6634.5,"right":2075.5,"top":6892.5}},{"unicode":562,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.935546875},"atlasBounds":{"left":6822.5,"bottom":6125.5,"right":6970.5,"top":6373.5}},{"unicode":567,"advance":0.59999999999999998,"planeBounds":{"left":-0.0029531249999999718,"bottom":-0.212890625,"right":0.500953125,"top":0.583984375},"atlasBounds":{"left":3764.5,"bottom":4380.5,"right":3893.5,"top":4584.5}},{"unicode":601,"advance":0.59999999999999998,"planeBounds":{"left":0.049468220338983043,"bottom":-0.044921875,"right":0.54946822033898313,"top":0.591796875},"atlasBounds":{"left":7106.5,"bottom":3821.5,"right":7234.5,"top":3984.5}},{"unicode":755,"advance":0.59999999999999998,"planeBounds":{"left":0.088446913992869899,"bottom":-0.279296875,"right":0.37750941399286991,"top":-0.021484375},"atlasBounds":{"left":7287.5,"bottom":3272.5,"right":7361.5,"top":3338.5}},{"unicode":759,"advance":0.59999999999999998,"planeBounds":{"left":0.10859375,"bottom":-0.244140625,"right":0.49140624999999999,"top":-0.044921875},"atlasBounds":{"left":8082.5,"bottom":6900.5,"right":8180.5,"top":6951.5}},{"unicode":894,"advance":0.59999999999999998,"planeBounds":{"left":0.081740327380952396,"bottom":-0.193359375,"right":0.44502157738095244,"top":0.591796875},"atlasBounds":{"left":4121.5,"bottom":4178.5,"right":4214.5,"top":4379.5}},{"unicode":902,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.033203125,"right":0.52748437500000001,"top":0.763671875},"atlasBounds":{"left":4194.5,"bottom":4380.5,"right":4333.5,"top":4584.5}},{"unicode":903,"advance":0.59999999999999998,"planeBounds":{"left":0.19887839673913044,"bottom":0.232421875,"right":0.42153464673913044,"top":0.455078125},"atlasBounds":{"left":8124.5,"bottom":3281.5,"right":8181.5,"top":3338.5}},{"unicode":904,"advance":0.59999999999999998,"planeBounds":{"left":-0.065296875000000004,"bottom":-0.033203125,"right":0.61829687499999997,"top":0.763671875},"atlasBounds":{"left":5185.5,"bottom":4380.5,"right":5360.5,"top":4584.5}},{"unicode":905,"advance":0.59999999999999998,"planeBounds":{"left":-0.065984375000000012,"bottom":-0.033203125,"right":0.60198437500000002,"top":0.763671875},"atlasBounds":{"left":5361.5,"bottom":4380.5,"right":5532.5,"top":4584.5}},{"unicode":906,"advance":0.59999999999999998,"planeBounds":{"left":-0.065625000000000017,"bottom":-0.033203125,"right":0.59062500000000007,"top":0.763671875},"atlasBounds":{"left":5683.5,"bottom":4380.5,"right":5851.5,"top":4584.5}},{"unicode":908,"advance":0.59999999999999998,"planeBounds":{"left":-0.034034067622950803,"bottom":-0.044921875,"right":0.5792471823770492,"top":0.771484375},"atlasBounds":{"left":5915.5,"bottom":5215.5,"right":6072.5,"top":5424.5}},{"unicode":910,"advance":0.59999999999999998,"planeBounds":{"left":-0.081593749999999979,"bottom":-0.033203125,"right":0.66059374999999998,"top":0.763671875},"atlasBounds":{"left":6435.5,"bottom":4380.5,"right":6625.5,"top":4584.5}},{"unicode":911,"advance":0.59999999999999998,"planeBounds":{"left":-0.039029233870967732,"bottom":-0.033203125,"right":0.57815826612903232,"top":0.771484375},"atlasBounds":{"left":6190.5,"bottom":5000.5,"right":6348.5,"top":5206.5}},{"unicode":912,"advance":0.59999999999999998,"planeBounds":{"left":0.066470588235294142,"bottom":-0.033203125,"right":0.56647058823529417,"top":0.931640625},"atlasBounds":{"left":7605.5,"bottom":6126.5,"right":7733.5,"top":6373.5}},{"unicode":913,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.033203125,"right":0.52748437500000001,"top":0.763671875},"atlasBounds":{"left":6626.5,"bottom":4380.5,"right":6765.5,"top":4584.5}},{"unicode":914,"advance":0.59999999999999998,"planeBounds":{"left":0.026736895161290301,"bottom":-0.033203125,"right":0.5814243951612903,"top":0.763671875},"atlasBounds":{"left":7215.5,"bottom":4380.5,"right":7357.5,"top":4584.5}},{"unicode":915,"advance":0.59999999999999998,"planeBounds":{"left":0.059625000000000018,"bottom":-0.033203125,"right":0.65337500000000004,"top":0.763671875},"atlasBounds":{"left":7505.5,"bottom":4380.5,"right":7657.5,"top":4584.5}},{"unicode":916,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.033203125,"right":0.52748437500000001,"top":0.763671875},"atlasBounds":{"left":7800.5,"bottom":4380.5,"right":7939.5,"top":4584.5}},{"unicode":917,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.763671875},"atlasBounds":{"left":7940.5,"bottom":4380.5,"right":8089.5,"top":4584.5}},{"unicode":918,"advance":0.59999999999999998,"planeBounds":{"left":0.013078125000000012,"bottom":-0.033203125,"right":0.60292187500000005,"top":0.763671875},"atlasBounds":{"left":0.5,"bottom":4175.5,"right":151.5,"top":4379.5}},{"unicode":919,"advance":0.59999999999999998,"planeBounds":{"left":0.026890625000000005,"bottom":-0.033203125,"right":0.60110937500000006,"top":0.763671875},"atlasBounds":{"left":292.5,"bottom":4175.5,"right":439.5,"top":4379.5}},{"unicode":920,"advance":0.59999999999999998,"planeBounds":{"left":0.048345253833950275,"bottom":-0.044921875,"right":0.57959525383395039,"top":0.771484375},"atlasBounds":{"left":6073.5,"bottom":5215.5,"right":6209.5,"top":5424.5}},{"unicode":921,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":0.763671875},"atlasBounds":{"left":440.5,"bottom":4175.5,"right":581.5,"top":4379.5}},{"unicode":922,"advance":0.59999999999999998,"planeBounds":{"left":0.025906250000000023,"bottom":-0.033203125,"right":0.64309375000000002,"top":0.763671875},"atlasBounds":{"left":582.5,"bottom":4175.5,"right":740.5,"top":4379.5}},{"unicode":923,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.033203125,"right":0.52748437500000001,"top":0.763671875},"atlasBounds":{"left":741.5,"bottom":4175.5,"right":880.5,"top":4379.5}},{"unicode":924,"advance":0.59999999999999998,"planeBounds":{"left":0.0073593750000000213,"bottom":-0.033203125,"right":0.62064062500000006,"top":0.763671875},"atlasBounds":{"left":1033.5,"bottom":4175.5,"right":1190.5,"top":4379.5}},{"unicode":925,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.033203125,"right":0.60306250000000006,"top":0.763671875},"atlasBounds":{"left":1191.5,"bottom":4175.5,"right":1339.5,"top":4379.5}},{"unicode":926,"advance":0.59999999999999998,"planeBounds":{"left":0.0073593750000000221,"bottom":-0.033203125,"right":0.62064062500000006,"top":0.763671875},"atlasBounds":{"left":1340.5,"bottom":4175.5,"right":1497.5,"top":4379.5}},{"unicode":927,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.044921875,"right":0.57938729508196729,"top":0.771484375},"atlasBounds":{"left":6210.5,"bottom":5215.5,"right":6346.5,"top":5424.5}},{"unicode":928,"advance":0.59999999999999998,"planeBounds":{"left":0.026890625000000005,"bottom":-0.033203125,"right":0.60110937500000006,"top":0.763671875},"atlasBounds":{"left":1498.5,"bottom":4175.5,"right":1645.5,"top":4379.5}},{"unicode":929,"advance":0.59999999999999998,"planeBounds":{"left":0.026308894230769212,"bottom":-0.033203125,"right":0.61615264423076921,"top":0.763671875},"atlasBounds":{"left":1798.5,"bottom":4175.5,"right":1949.5,"top":4379.5}},{"unicode":931,"advance":0.59999999999999998,"planeBounds":{"left":0.0048281249999999843,"bottom":-0.033203125,"right":0.65717187499999996,"top":0.763671875},"atlasBounds":{"left":2090.5,"bottom":4175.5,"right":2257.5,"top":4379.5}},{"unicode":932,"advance":0.59999999999999998,"planeBounds":{"left":0.087703125000000007,"bottom":-0.033203125,"right":0.64629687499999999,"top":0.763671875},"atlasBounds":{"left":2258.5,"bottom":4175.5,"right":2401.5,"top":4379.5}},{"unicode":933,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.763671875},"atlasBounds":{"left":2402.5,"bottom":4175.5,"right":2550.5,"top":4379.5}},{"unicode":934,"advance":0.59999999999999998,"planeBounds":{"left":0.016678506299734733,"bottom":-0.056640625,"right":0.61433475629973477,"top":0.802734375},"atlasBounds":{"left":2254.5,"bottom":5426.5,"right":2407.5,"top":5646.5}},{"unicode":935,"advance":0.59999999999999998,"planeBounds":{"left":-0.028437500000000008,"bottom":-0.033203125,"right":0.6434375,"top":0.763671875},"atlasBounds":{"left":2727.5,"bottom":4175.5,"right":2899.5,"top":4379.5}},{"unicode":936,"advance":0.59999999999999998,"planeBounds":{"left":0.041404233870967776,"bottom":-0.033203125,"right":0.64296673387096781,"top":0.763671875},"atlasBounds":{"left":2900.5,"bottom":4175.5,"right":3054.5,"top":4379.5}},{"unicode":937,"advance":0.59999999999999998,"planeBounds":{"left":-0.026669858870967737,"bottom":-0.033203125,"right":0.57879889112903238,"top":0.771484375},"atlasBounds":{"left":6349.5,"bottom":5000.5,"right":6504.5,"top":5206.5}},{"unicode":938,"advance":0.59999999999999998,"planeBounds":{"left":0.037397569444444438,"bottom":-0.033203125,"right":0.5959913194444445,"top":0.970703125},"atlasBounds":{"left":5198.5,"bottom":6635.5,"right":5341.5,"top":6892.5}},{"unicode":939,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.970703125},"atlasBounds":{"left":5342.5,"bottom":6635.5,"right":5490.5,"top":6892.5}},{"unicode":940,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.818359375},"atlasBounds":{"left":0.5,"bottom":5425.5,"right":133.5,"top":5646.5}},{"unicode":941,"advance":0.59999999999999998,"planeBounds":{"left":0.034978478773584891,"bottom":-0.041015625,"right":0.57013472877358495,"top":0.818359375},"atlasBounds":{"left":2408.5,"bottom":5426.5,"right":2545.5,"top":5646.5}},{"unicode":942,"advance":0.59999999999999998,"planeBounds":{"left":0.026874999999999986,"bottom":-0.033203125,"right":0.55812499999999998,"top":0.818359375},"atlasBounds":{"left":5955.5,"bottom":5428.5,"right":6091.5,"top":5646.5}},{"unicode":943,"advance":0.59999999999999998,"planeBounds":{"left":0.065859374999999998,"bottom":-0.033203125,"right":0.55414062500000005,"top":0.818359375},"atlasBounds":{"left":6092.5,"bottom":5428.5,"right":6217.5,"top":5646.5}},{"unicode":944,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.931640625},"atlasBounds":{"left":2868.5,"bottom":6123.5,"right":3002.5,"top":6373.5}},{"unicode":945,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.591796875},"atlasBounds":{"left":7969.5,"bottom":3821.5,"right":8102.5,"top":3984.5}},{"unicode":946,"advance":0.59999999999999998,"planeBounds":{"left":-0.0028773148148148178,"bottom":-0.212890625,"right":0.57524768518518521,"top":0.771484375},"atlasBounds":{"left":831.5,"bottom":6121.5,"right":979.5,"top":6373.5}},{"unicode":947,"advance":0.59999999999999998,"planeBounds":{"left":0.082281249999999986,"bottom":-0.212890625,"right":0.60571874999999997,"top":0.583984375},"atlasBounds":{"left":3205.5,"bottom":4175.5,"right":3339.5,"top":4379.5}},{"unicode":948,"advance":0.59999999999999998,"planeBounds":{"left":0.047231854838709682,"bottom":-0.044921875,"right":0.59410685483870962,"top":0.763671875},"atlasBounds":{"left":3352.5,"bottom":4999.5,"right":3492.5,"top":5206.5}},{"unicode":949,"advance":0.59999999999999998,"planeBounds":{"left":0.034978478773584891,"bottom":-0.041015625,"right":0.57013472877358495,"top":0.591796875},"atlasBounds":{"left":3457.5,"bottom":3645.5,"right":3594.5,"top":3807.5}},{"unicode":950,"advance":0.59999999999999998,"planeBounds":{"left":0.078009640957446821,"bottom":-0.208984375,"right":0.64441589095744678,"top":0.763671875},"atlasBounds":{"left":5595.5,"bottom":6124.5,"right":5740.5,"top":6373.5}},{"unicode":951,"advance":0.59999999999999998,"planeBounds":{"left":0.026563068181818143,"bottom":-0.033203125,"right":0.55000056818181819,"top":0.591796875},"atlasBounds":{"left":7039.5,"bottom":3647.5,"right":7173.5,"top":3807.5}},{"unicode":952,"advance":0.59999999999999998,"planeBounds":{"left":0.049328125000000056,"bottom":-0.044921875,"right":0.57667187500000006,"top":0.763671875},"atlasBounds":{"left":3493.5,"bottom":4999.5,"right":3628.5,"top":5206.5}},{"unicode":953,"advance":0.59999999999999998,"planeBounds":{"left":0.06521875000000002,"bottom":-0.033203125,"right":0.54178124999999999,"top":0.583984375},"atlasBounds":{"left":2880.5,"bottom":3485.5,"right":3002.5,"top":3643.5}},{"unicode":954,"advance":0.59999999999999998,"planeBounds":{"left":0.030937500000000007,"bottom":-0.033203125,"right":0.60906250000000006,"top":0.583984375},"atlasBounds":{"left":7656.5,"bottom":3649.5,"right":7804.5,"top":3807.5}},{"unicode":955,"advance":0.59999999999999998,"planeBounds":{"left":-0.014484375000000011,"bottom":-0.033203125,"right":0.52848437500000001,"top":0.763671875},"atlasBounds":{"left":3584.5,"bottom":4175.5,"right":3723.5,"top":4379.5}},{"unicode":956,"advance":0.59999999999999998,"planeBounds":{"left":-0.0015624999999999942,"bottom":-0.212890625,"right":0.57656249999999998,"top":0.583984375},"atlasBounds":{"left":3435.5,"bottom":4175.5,"right":3583.5,"top":4379.5}},{"unicode":957,"advance":0.59999999999999998,"planeBounds":{"left":0.081281249999999985,"bottom":-0.033203125,"right":0.60471874999999997,"top":0.583984375},"atlasBounds":{"left":2745.5,"bottom":3485.5,"right":2879.5,"top":3643.5}},{"unicode":958,"advance":0.59999999999999998,"planeBounds":{"left":0.053945130683852496,"bottom":-0.208984375,"right":0.61644513068385254,"top":0.771484375},"atlasBounds":{"left":1754.5,"bottom":6122.5,"right":1898.5,"top":6373.5}},{"unicode":959,"advance":0.59999999999999998,"planeBounds":{"left":0.049500000000000044,"bottom":-0.044921875,"right":0.54949999999999999,"top":0.591796875},"atlasBounds":{"left":7310.5,"bottom":3821.5,"right":7438.5,"top":3984.5}},{"unicode":960,"advance":0.59999999999999998,"planeBounds":{"left":0.033578125000000014,"bottom":-0.037109375,"right":0.62342187500000001,"top":0.583984375},"atlasBounds":{"left":7174.5,"bottom":3648.5,"right":7325.5,"top":3807.5}},{"unicode":961,"advance":0.59999999999999998,"planeBounds":{"left":-0.0012296080508474581,"bottom":-0.212890625,"right":0.54955164194915251,"top":0.591796875},"atlasBounds":{"left":6505.5,"bottom":5000.5,"right":6646.5,"top":5206.5}},{"unicode":962,"advance":0.59999999999999998,"planeBounds":{"left":0.057650069619588032,"bottom":-0.208984375,"right":0.56155631961958807,"top":0.591796875},"atlasBounds":{"left":1105.5,"bottom":4791.5,"right":1234.5,"top":4996.5}},{"unicode":963,"advance":0.59999999999999998,"planeBounds":{"left":0.04845261270491806,"bottom":-0.044921875,"right":0.61485886270491807,"top":0.583984375},"atlasBounds":{"left":5227.5,"bottom":3646.5,"right":5372.5,"top":3807.5}},{"unicode":964,"advance":0.59999999999999998,"planeBounds":{"left":0.058328124999999988,"bottom":-0.033203125,"right":0.58567187500000006,"top":0.583984375},"atlasBounds":{"left":866.5,"bottom":3485.5,"right":1001.5,"top":3643.5}},{"unicode":965,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.583984375},"atlasBounds":{"left":4527.5,"bottom":3646.5,"right":4661.5,"top":3807.5}},{"unicode":966,"advance":0.59999999999999998,"planeBounds":{"left":0.012563038793103404,"bottom":-0.212890625,"right":0.58678178879310339,"top":0.587890625},"atlasBounds":{"left":1235.5,"bottom":4791.5,"right":1382.5,"top":4996.5}},{"unicode":967,"advance":0.59999999999999998,"planeBounds":{"left":-0.0087343749999999817,"bottom":-0.033203125,"right":0.59673437500000004,"top":0.583984375},"atlasBounds":{"left":263.5,"bottom":3485.5,"right":418.5,"top":3643.5}},{"unicode":968,"advance":0.59999999999999998,"planeBounds":{"left":0.018671345338983101,"bottom":-0.212890625,"right":0.60851509533898307,"top":0.583984375},"atlasBounds":{"left":1646.5,"bottom":4175.5,"right":1797.5,"top":4379.5}},{"unicode":969,"advance":0.59999999999999998,"planeBounds":{"left":0.0077674811439346764,"bottom":-0.044921875,"right":0.58589248114393466,"top":0.591796875},"atlasBounds":{"left":953.5,"bottom":3644.5,"right":1101.5,"top":3807.5}},{"unicode":970,"advance":0.59999999999999998,"planeBounds":{"left":0.066694444444444473,"bottom":-0.033203125,"right":0.5666944444444445,"top":0.802734375},"atlasBounds":{"left":1003.5,"bottom":5210.5,"right":1131.5,"top":5424.5}},{"unicode":971,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.802734375},"atlasBounds":{"left":7757.5,"bottom":5429.5,"right":7891.5,"top":5646.5}},{"unicode":972,"advance":0.59999999999999998,"planeBounds":{"left":0.049202612704918047,"bottom":-0.044921875,"right":0.55310886270491799,"top":0.818359375},"atlasBounds":{"left":134.5,"bottom":5425.5,"right":263.5,"top":5646.5}},{"unicode":973,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.818359375},"atlasBounds":{"left":264.5,"bottom":5425.5,"right":398.5,"top":5646.5}},{"unicode":974,"advance":0.59999999999999998,"planeBounds":{"left":0.0077674811439346764,"bottom":-0.044921875,"right":0.58589248114393466,"top":0.818359375},"atlasBounds":{"left":399.5,"bottom":5425.5,"right":547.5,"top":5646.5}},{"unicode":975,"advance":0.59999999999999998,"planeBounds":{"left":0.025906250000000023,"bottom":-0.212890625,"right":0.64309375000000002,"top":0.763671875},"atlasBounds":{"left":3003.5,"bottom":6123.5,"right":3161.5,"top":6373.5}},{"unicode":981,"advance":0.59999999999999998,"planeBounds":{"left":-0.029578125000000014,"bottom":-0.212890625,"right":0.63057812499999999,"top":0.763671875},"atlasBounds":{"left":3162.5,"bottom":6123.5,"right":3331.5,"top":6373.5}},{"unicode":982,"advance":0.59999999999999998,"planeBounds":{"left":0.0081461148648648996,"bottom":-0.044921875,"right":0.65658361486486494,"top":0.583984375},"atlasBounds":{"left":4197.5,"bottom":3646.5,"right":4363.5,"top":3807.5}},{"unicode":983,"advance":0.59999999999999998,"planeBounds":{"left":0.028015624999999988,"bottom":-0.212890625,"right":0.57098437499999999,"top":0.583984375},"atlasBounds":{"left":152.5,"bottom":4175.5,"right":291.5,"top":4379.5}},{"unicode":1025,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.970703125},"atlasBounds":{"left":5491.5,"bottom":6635.5,"right":5640.5,"top":6892.5}},{"unicode":1026,"advance":0.59999999999999998,"planeBounds":{"left":0.029105113636363644,"bottom":-0.212890625,"right":0.56816761363636359,"top":0.763671875},"atlasBounds":{"left":3332.5,"bottom":6123.5,"right":3470.5,"top":6373.5}},{"unicode":1027,"advance":0.59999999999999998,"planeBounds":{"left":0.059625000000000018,"bottom":-0.033203125,"right":0.65337500000000004,"top":0.982421875},"atlasBounds":{"left":7132.5,"bottom":6896.5,"right":7284.5,"top":7156.5}},{"unicode":1028,"advance":0.59999999999999998,"planeBounds":{"left":0.053439948156682029,"bottom":-0.044921875,"right":0.59250244815668207,"top":0.771484375},"atlasBounds":{"left":6347.5,"bottom":5215.5,"right":6485.5,"top":5424.5}},{"unicode":1029,"advance":0.59999999999999998,"planeBounds":{"left":0.032109375000000002,"bottom":-0.044921875,"right":0.582890625,"top":0.771484375},"atlasBounds":{"left":6486.5,"bottom":5215.5,"right":6627.5,"top":5424.5}},{"unicode":1030,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":0.763671875},"atlasBounds":{"left":7658.5,"bottom":4380.5,"right":7799.5,"top":4584.5}},{"unicode":1031,"advance":0.59999999999999998,"planeBounds":{"left":0.036897569444444438,"bottom":-0.033203125,"right":0.59549131944444444,"top":0.970703125},"atlasBounds":{"left":5641.5,"bottom":6635.5,"right":5784.5,"top":6892.5}},{"unicode":1032,"advance":0.59999999999999998,"planeBounds":{"left":0.012122983870967765,"bottom":-0.044921875,"right":0.59024798387096777,"top":0.763671875},"atlasBounds":{"left":3800.5,"bottom":4999.5,"right":3948.5,"top":5206.5}},{"unicode":1033,"advance":0.59999999999999998,"planeBounds":{"left":-0.082434782608695648,"bottom":-0.037109375,"right":0.60506521739130437,"top":0.763671875},"atlasBounds":{"left":1690.5,"bottom":4791.5,"right":1866.5,"top":4996.5}},{"unicode":1034,"advance":0.59999999999999998,"planeBounds":{"left":-0.017259640957446767,"bottom":-0.033203125,"right":0.60383410904255319,"top":0.763671875},"atlasBounds":{"left":7055.5,"bottom":4380.5,"right":7214.5,"top":4584.5}},{"unicode":1035,"advance":0.59999999999999998,"planeBounds":{"left":0.029105113636363644,"bottom":-0.033203125,"right":0.56816761363636359,"top":0.763671875},"atlasBounds":{"left":6916.5,"bottom":4380.5,"right":7054.5,"top":4584.5}},{"unicode":1036,"advance":0.59999999999999998,"planeBounds":{"left":0.025906250000000023,"bottom":-0.033203125,"right":0.64309375000000002,"top":0.982421875},"atlasBounds":{"left":7285.5,"bottom":6896.5,"right":7443.5,"top":7156.5}},{"unicode":1038,"advance":0.59999999999999998,"planeBounds":{"left":0.10051562499999998,"bottom":-0.033203125,"right":0.643484375,"top":0.982421875},"atlasBounds":{"left":7444.5,"bottom":6896.5,"right":7583.5,"top":7156.5}},{"unicode":1039,"advance":0.59999999999999998,"planeBounds":{"left":0.026890625000000005,"bottom":-0.158203125,"right":0.60110937500000006,"top":0.763671875},"atlasBounds":{"left":3472.5,"bottom":5884.5,"right":3619.5,"top":6120.5}},{"unicode":1040,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.033203125,"right":0.52748437500000001,"top":0.763671875},"atlasBounds":{"left":6295.5,"bottom":4380.5,"right":6434.5,"top":4584.5}},{"unicode":1041,"advance":0.59999999999999998,"planeBounds":{"left":0.02684375,"bottom":-0.033203125,"right":0.59715625000000006,"top":0.763671875},"atlasBounds":{"left":6148.5,"bottom":4380.5,"right":6294.5,"top":4584.5}},{"unicode":1042,"advance":0.59999999999999998,"planeBounds":{"left":0.026736895161290301,"bottom":-0.033203125,"right":0.5814243951612903,"top":0.763671875},"atlasBounds":{"left":6005.5,"bottom":4380.5,"right":6147.5,"top":4584.5}},{"unicode":1043,"advance":0.59999999999999998,"planeBounds":{"left":0.059625000000000018,"bottom":-0.033203125,"right":0.65337500000000004,"top":0.763671875},"atlasBounds":{"left":5852.5,"bottom":4380.5,"right":6004.5,"top":4584.5}},{"unicode":1044,"advance":0.59999999999999998,"planeBounds":{"left":-0.059171875000000013,"bottom":-0.173828125,"right":0.59317187500000002,"top":0.763671875},"atlasBounds":{"left":833.5,"bottom":5880.5,"right":1000.5,"top":6120.5}},{"unicode":1045,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.763671875},"atlasBounds":{"left":5533.5,"bottom":4380.5,"right":5682.5,"top":4584.5}},{"unicode":1046,"advance":0.59999999999999998,"planeBounds":{"left":-0.065999999999999975,"bottom":-0.033203125,"right":0.68400000000000005,"top":0.763671875},"atlasBounds":{"left":7999.5,"bottom":5916.5,"right":8191.5,"top":6120.5}},{"unicode":1047,"advance":0.59999999999999998,"planeBounds":{"left":0.010890301724137956,"bottom":-0.044921875,"right":0.58120280172413796,"top":0.771484375},"atlasBounds":{"left":6628.5,"bottom":5215.5,"right":6774.5,"top":5424.5}},{"unicode":1048,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.033203125,"right":0.60306250000000006,"top":0.763671875},"atlasBounds":{"left":5036.5,"bottom":4380.5,"right":5184.5,"top":4584.5}},{"unicode":1049,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.033203125,"right":0.60306250000000006,"top":0.982421875},"atlasBounds":{"left":7584.5,"bottom":6896.5,"right":7732.5,"top":7156.5}},{"unicode":1050,"advance":0.59999999999999998,"planeBounds":{"left":0.025906250000000023,"bottom":-0.033203125,"right":0.64309375000000002,"top":0.763671875},"atlasBounds":{"left":4735.5,"bottom":4380.5,"right":4893.5,"top":4584.5}},{"unicode":1051,"advance":0.59999999999999998,"planeBounds":{"left":-0.055125000000000014,"bottom":-0.037109375,"right":0.60112500000000002,"top":0.763671875},"atlasBounds":{"left":1867.5,"bottom":4791.5,"right":2035.5,"top":4996.5}},{"unicode":1052,"advance":0.59999999999999998,"planeBounds":{"left":0.0073593750000000213,"bottom":-0.033203125,"right":0.62064062500000006,"top":0.763671875},"atlasBounds":{"left":4482.5,"bottom":4380.5,"right":4639.5,"top":4584.5}},{"unicode":1053,"advance":0.59999999999999998,"planeBounds":{"left":0.026890625000000005,"bottom":-0.033203125,"right":0.60110937500000006,"top":0.763671875},"atlasBounds":{"left":4334.5,"bottom":4380.5,"right":4481.5,"top":4584.5}},{"unicode":1054,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.044921875,"right":0.57938729508196729,"top":0.771484375},"atlasBounds":{"left":6775.5,"bottom":5215.5,"right":6911.5,"top":5424.5}},{"unicode":1055,"advance":0.59999999999999998,"planeBounds":{"left":0.026890625000000005,"bottom":-0.033203125,"right":0.60110937500000006,"top":0.763671875},"atlasBounds":{"left":4046.5,"bottom":4380.5,"right":4193.5,"top":4584.5}},{"unicode":1056,"advance":0.59999999999999998,"planeBounds":{"left":0.026308894230769212,"bottom":-0.033203125,"right":0.61615264423076921,"top":0.763671875},"atlasBounds":{"left":3894.5,"bottom":4380.5,"right":4045.5,"top":4584.5}},{"unicode":1057,"advance":0.59999999999999998,"planeBounds":{"left":0.053439948156682029,"bottom":-0.044921875,"right":0.59250244815668207,"top":0.771484375},"atlasBounds":{"left":6912.5,"bottom":5215.5,"right":7050.5,"top":5424.5}},{"unicode":1058,"advance":0.59999999999999998,"planeBounds":{"left":0.087703125000000007,"bottom":-0.033203125,"right":0.64629687499999999,"top":0.763671875},"atlasBounds":{"left":3620.5,"bottom":4380.5,"right":3763.5,"top":4584.5}},{"unicode":1059,"advance":0.59999999999999998,"planeBounds":{"left":0.10051562499999998,"bottom":-0.033203125,"right":0.643484375,"top":0.763671875},"atlasBounds":{"left":3480.5,"bottom":4380.5,"right":3619.5,"top":4584.5}},{"unicode":1060,"advance":0.59999999999999998,"planeBounds":{"left":0.016678506299734733,"bottom":-0.056640625,"right":0.61433475629973477,"top":0.802734375},"atlasBounds":{"left":2688.5,"bottom":5426.5,"right":2841.5,"top":5646.5}},{"unicode":1061,"advance":0.59999999999999998,"planeBounds":{"left":-0.028437500000000008,"bottom":-0.033203125,"right":0.6434375,"top":0.763671875},"atlasBounds":{"left":3156.5,"bottom":4380.5,"right":3328.5,"top":4584.5}},{"unicode":1062,"advance":0.59999999999999998,"planeBounds":{"left":0.028656249999999998,"bottom":-0.173828125,"right":0.58334375000000005,"top":0.763671875},"atlasBounds":{"left":1001.5,"bottom":5880.5,"right":1143.5,"top":6120.5}},{"unicode":1063,"advance":0.59999999999999998,"planeBounds":{"left":0.07762023305084749,"bottom":-0.033203125,"right":0.60105773305084742,"top":0.763671875},"atlasBounds":{"left":2870.5,"bottom":4380.5,"right":3004.5,"top":4584.5}},{"unicode":1064,"advance":0.59999999999999998,"planeBounds":{"left":-0.0058124999999999661,"bottom":-0.033203125,"right":0.6348125,"top":0.763671875},"atlasBounds":{"left":2705.5,"bottom":4380.5,"right":2869.5,"top":4584.5}},{"unicode":1065,"advance":0.59999999999999998,"planeBounds":{"left":-0.0078593749999999688,"bottom":-0.173828125,"right":0.628859375,"top":0.763671875},"atlasBounds":{"left":1144.5,"bottom":5880.5,"right":1307.5,"top":6120.5}},{"unicode":1066,"advance":0.59999999999999998,"planeBounds":{"left":0.033267518939393947,"bottom":-0.033203125,"right":0.56061126893939406,"top":0.763671875},"atlasBounds":{"left":2403.5,"bottom":4380.5,"right":2538.5,"top":4584.5}},{"unicode":1067,"advance":0.59999999999999998,"planeBounds":{"left":-0.0063125000000000212,"bottom":-0.033203125,"right":0.63431250000000006,"top":0.763671875},"atlasBounds":{"left":2238.5,"bottom":4380.5,"right":2402.5,"top":4584.5}},{"unicode":1068,"advance":0.59999999999999998,"planeBounds":{"left":0.028314393939393948,"bottom":-0.033203125,"right":0.55956439393939406,"top":0.763671875},"atlasBounds":{"left":2101.5,"bottom":4380.5,"right":2237.5,"top":4584.5}},{"unicode":1069,"advance":0.59999999999999998,"planeBounds":{"left":0.035222718253968256,"bottom":-0.044921875,"right":0.57428521825396817,"top":0.771484375},"atlasBounds":{"left":7051.5,"bottom":5215.5,"right":7189.5,"top":5424.5}},{"unicode":1070,"advance":0.59999999999999998,"planeBounds":{"left":-0.0065287499999999955,"bottom":-0.044921875,"right":0.64190875000000003,"top":0.771484375},"atlasBounds":{"left":7190.5,"bottom":5215.5,"right":7356.5,"top":5424.5}},{"unicode":1071,"advance":0.59999999999999998,"planeBounds":{"left":-0.0096874999999999791,"bottom":-0.033203125,"right":0.59968750000000004,"top":0.763671875},"atlasBounds":{"left":1652.5,"bottom":4380.5,"right":1808.5,"top":4584.5}},{"unicode":1072,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.591796875},"atlasBounds":{"left":7439.5,"bottom":3821.5,"right":7572.5,"top":3984.5}},{"unicode":1073,"advance":0.59999999999999998,"planeBounds":{"left":0.049000000000000002,"bottom":-0.044921875,"right":0.58025000000000004,"top":0.771484375},"atlasBounds":{"left":7357.5,"bottom":5215.5,"right":7493.5,"top":5424.5}},{"unicode":1074,"advance":0.59999999999999998,"planeBounds":{"left":0.02849158653846157,"bottom":-0.033203125,"right":0.55583533653846151,"top":0.583984375},"atlasBounds":{"left":4047.5,"bottom":3485.5,"right":4182.5,"top":3643.5}},{"unicode":1075,"advance":0.59999999999999998,"planeBounds":{"left":0.032806172895500724,"bottom":-0.041015625,"right":0.5601499228955007,"top":0.591796875},"atlasBounds":{"left":3185.5,"bottom":3645.5,"right":3320.5,"top":3807.5}},{"unicode":1076,"advance":0.59999999999999998,"planeBounds":{"left":0.047893679378531076,"bottom":-0.044921875,"right":0.57133117937853106,"top":0.771484375},"atlasBounds":{"left":7494.5,"bottom":5215.5,"right":7628.5,"top":5424.5}},{"unicode":1077,"advance":0.59999999999999998,"planeBounds":{"left":0.04740393518518516,"bottom":-0.044921875,"right":0.55521643518518526,"top":0.591796875},"atlasBounds":{"left":822.5,"bottom":3644.5,"right":952.5,"top":3807.5}},{"unicode":1078,"advance":0.59999999999999998,"planeBounds":{"left":-0.048250000000000001,"bottom":-0.033203125,"right":0.63924999999999998,"top":0.583984375},"atlasBounds":{"left":4306.5,"bottom":3485.5,"right":4482.5,"top":3643.5}},{"unicode":1079,"advance":0.59999999999999998,"planeBounds":{"left":0.02251650943396228,"bottom":-0.041015625,"right":0.55376650943396222,"top":0.591796875},"atlasBounds":{"left":3731.5,"bottom":3645.5,"right":3867.5,"top":3807.5}},{"unicode":1080,"advance":0.59999999999999998,"planeBounds":{"left":0.049999431818181776,"bottom":-0.044921875,"right":0.57343693181818178,"top":0.583984375},"atlasBounds":{"left":5092.5,"bottom":3646.5,"right":5226.5,"top":3807.5}},{"unicode":1081,"advance":0.59999999999999998,"planeBounds":{"left":0.049999431818181776,"bottom":-0.044921875,"right":0.57343693181818178,"top":0.818359375},"atlasBounds":{"left":548.5,"bottom":5425.5,"right":682.5,"top":5646.5}},{"unicode":1082,"advance":0.59999999999999998,"planeBounds":{"left":0.030937500000000007,"bottom":-0.033203125,"right":0.60906250000000006,"top":0.583984375},"atlasBounds":{"left":7805.5,"bottom":3649.5,"right":7953.5,"top":3807.5}},{"unicode":1083,"advance":0.59999999999999998,"planeBounds":{"left":-0.046093749999999982,"bottom":-0.037109375,"right":0.57109375000000007,"top":0.583984375},"atlasBounds":{"left":7326.5,"bottom":3648.5,"right":7484.5,"top":3807.5}},{"unicode":1084,"advance":0.59999999999999998,"planeBounds":{"left":0.010437500000000008,"bottom":-0.033203125,"right":0.58856249999999999,"top":0.583984375},"atlasBounds":{"left":7954.5,"bottom":3649.5,"right":8102.5,"top":3807.5}},{"unicode":1085,"advance":0.59999999999999998,"planeBounds":{"left":0.028015624999999988,"bottom":-0.033203125,"right":0.57098437499999999,"top":0.583984375},"atlasBounds":{"left":726.5,"bottom":3485.5,"right":865.5,"top":3643.5}},{"unicode":1086,"advance":0.59999999999999998,"planeBounds":{"left":0.049500000000000044,"bottom":-0.044921875,"right":0.54949999999999999,"top":0.591796875},"atlasBounds":{"left":302.5,"bottom":3644.5,"right":430.5,"top":3807.5}},{"unicode":1087,"advance":0.59999999999999998,"planeBounds":{"left":0.028015624999999988,"bottom":-0.033203125,"right":0.57098437499999999,"top":0.583984375},"atlasBounds":{"left":1539.5,"bottom":3485.5,"right":1678.5,"top":3643.5}},{"unicode":1088,"advance":0.59999999999999998,"planeBounds":{"left":-0.0030619318181818447,"bottom":-0.212890625,"right":0.55162556818181818,"top":0.591796875},"atlasBounds":{"left":7002.5,"bottom":5000.5,"right":7144.5,"top":5206.5}},{"unicode":1089,"advance":0.59999999999999998,"planeBounds":{"left":0.052383744347088722,"bottom":-0.044921875,"right":0.5601962443470887,"top":0.591796875},"atlasBounds":{"left":1406.5,"bottom":3644.5,"right":1536.5,"top":3807.5}},{"unicode":1090,"advance":0.59999999999999998,"planeBounds":{"left":0.068468749999999981,"bottom":-0.033203125,"right":0.60753124999999997,"top":0.583984375},"atlasBounds":{"left":3155.5,"bottom":3485.5,"right":3293.5,"top":3643.5}},{"unicode":1091,"advance":0.59999999999999998,"planeBounds":{"left":0.053905681818181776,"bottom":-0.212890625,"right":0.56953068181818178,"top":0.583984375},"atlasBounds":{"left":7053.5,"bottom":4585.5,"right":7185.5,"top":4789.5}},{"unicode":1092,"advance":0.59999999999999998,"planeBounds":{"left":-0.029578125000000014,"bottom":-0.212890625,"right":0.63057812499999999,"top":0.763671875},"atlasBounds":{"left":3471.5,"bottom":6123.5,"right":3640.5,"top":6373.5}},{"unicode":1093,"advance":0.59999999999999998,"planeBounds":{"left":-0.0087343749999999817,"bottom":-0.033203125,"right":0.59673437500000004,"top":0.583984375},"atlasBounds":{"left":1837.5,"bottom":3485.5,"right":1992.5,"top":3643.5}},{"unicode":1094,"advance":0.59999999999999998,"planeBounds":{"left":0.025921874999999987,"bottom":-0.173828125,"right":0.56107812499999998,"top":0.583984375},"atlasBounds":{"left":6879.5,"bottom":4185.5,"right":7016.5,"top":4379.5}},{"unicode":1095,"advance":0.59999999999999998,"planeBounds":{"left":0.066641509433962298,"bottom":-0.033203125,"right":0.5666415094339623,"top":0.583984375},"atlasBounds":{"left":1267.5,"bottom":3485.5,"right":1395.5,"top":3643.5}},{"unicode":1096,"advance":0.59999999999999998,"planeBounds":{"left":-0.006640624999999979,"bottom":-0.033203125,"right":0.60664062500000004,"top":0.583984375},"atlasBounds":{"left":419.5,"bottom":3485.5,"right":576.5,"top":3643.5}},{"unicode":1097,"advance":0.59999999999999998,"planeBounds":{"left":-0.0072343749999999821,"bottom":-0.173828125,"right":0.59823437499999998,"top":0.583984375},"atlasBounds":{"left":6723.5,"bottom":4185.5,"right":6878.5,"top":4379.5}},{"unicode":1098,"advance":0.59999999999999998,"planeBounds":{"left":0.0060884533898305157,"bottom":-0.033203125,"right":0.56077595338983055,"top":0.583984375},"atlasBounds":{"left":1396.5,"bottom":3485.5,"right":1538.5,"top":3643.5}},{"unicode":1099,"advance":0.59999999999999998,"planeBounds":{"left":-0.0071406249999999786,"bottom":-0.033203125,"right":0.60614062499999999,"top":0.583984375},"atlasBounds":{"left":1679.5,"bottom":3485.5,"right":1836.5,"top":3643.5}},{"unicode":1100,"advance":0.59999999999999998,"planeBounds":{"left":0.041713453389830506,"bottom":-0.033203125,"right":0.56515095338983057,"top":0.583984375},"atlasBounds":{"left":3756.5,"bottom":3485.5,"right":3890.5,"top":3643.5}},{"unicode":1101,"advance":0.59999999999999998,"planeBounds":{"left":0.044458933905251484,"bottom":-0.044921875,"right":0.54836518390525146,"top":0.591796875},"atlasBounds":{"left":2768.5,"bottom":3644.5,"right":2897.5,"top":3807.5}},{"unicode":1102,"advance":0.59999999999999998,"planeBounds":{"left":-0.0057493351063830035,"bottom":-0.044921875,"right":0.59581316489361702,"top":0.591796875},"atlasBounds":{"left":2613.5,"bottom":3644.5,"right":2767.5,"top":3807.5}},{"unicode":1103,"advance":0.59999999999999998,"planeBounds":{"left":-0.0011093749999999964,"bottom":-0.033203125,"right":0.57310937500000003,"top":0.583984375},"atlasBounds":{"left":115.5,"bottom":3485.5,"right":262.5,"top":3643.5}},{"unicode":1105,"advance":0.59999999999999998,"planeBounds":{"left":0.048053819444444437,"bottom":-0.044921875,"right":0.56758506944444442,"top":0.802734375},"atlasBounds":{"left":7892.5,"bottom":5429.5,"right":8025.5,"top":5646.5}},{"unicode":1106,"advance":0.59999999999999998,"planeBounds":{"left":0.020016193181818197,"bottom":-0.212890625,"right":0.53954744318181813,"top":0.763671875},"atlasBounds":{"left":3641.5,"bottom":6123.5,"right":3774.5,"top":6373.5}},{"unicode":1107,"advance":0.59999999999999998,"planeBounds":{"left":0.032945913461538495,"bottom":-0.041015625,"right":0.58372716346153841,"top":0.818359375},"atlasBounds":{"left":2842.5,"bottom":5426.5,"right":2983.5,"top":5646.5}},{"unicode":1108,"advance":0.59999999999999998,"planeBounds":{"left":0.049127083333333391,"bottom":-0.044921875,"right":0.55693958333333338,"top":0.591796875},"atlasBounds":{"left":1537.5,"bottom":3644.5,"right":1667.5,"top":3807.5}},{"unicode":1109,"advance":0.59999999999999998,"planeBounds":{"left":0.032550951086956575,"bottom":-0.041015625,"right":0.5598947010869566,"top":0.591796875},"atlasBounds":{"left":3595.5,"bottom":3645.5,"right":3730.5,"top":3807.5}},{"unicode":1110,"advance":0.59999999999999998,"planeBounds":{"left":0.011828124999999983,"bottom":-0.033203125,"right":0.53917187499999997,"top":0.802734375},"atlasBounds":{"left":1132.5,"bottom":5210.5,"right":1267.5,"top":5424.5}},{"unicode":1111,"advance":0.59999999999999998,"planeBounds":{"left":0.012038194444444443,"bottom":-0.033203125,"right":0.58235069444444443,"top":0.802734375},"atlasBounds":{"left":1268.5,"bottom":5210.5,"right":1414.5,"top":5424.5}},{"unicode":1112,"advance":0.59999999999999998,"planeBounds":{"left":-0.0030164473684210196,"bottom":-0.212890625,"right":0.54385855263157901,"top":0.802734375},"atlasBounds":{"left":7733.5,"bottom":6896.5,"right":7873.5,"top":7156.5}},{"unicode":1113,"advance":0.59999999999999998,"planeBounds":{"left":-0.072244015957446756,"bottom":-0.037109375,"right":0.59181848404255322,"top":0.583984375},"atlasBounds":{"left":7485.5,"bottom":3648.5,"right":7655.5,"top":3807.5}},{"unicode":1114,"advance":0.59999999999999998,"planeBounds":{"left":-0.0055677083333333342,"bottom":-0.033203125,"right":0.59208854166666669,"top":0.583984375},"atlasBounds":{"left":3602.5,"bottom":3485.5,"right":3755.5,"top":3643.5}},{"unicode":1115,"advance":0.59999999999999998,"planeBounds":{"left":0.020516193181818194,"bottom":-0.033203125,"right":0.54004744318181819,"top":0.763671875},"atlasBounds":{"left":3694.5,"bottom":4585.5,"right":3827.5,"top":4789.5}},{"unicode":1116,"advance":0.59999999999999998,"planeBounds":{"left":0.030937500000000007,"bottom":-0.033203125,"right":0.60906250000000006,"top":0.818359375},"atlasBounds":{"left":6218.5,"bottom":5428.5,"right":6366.5,"top":5646.5}},{"unicode":1118,"advance":0.59999999999999998,"planeBounds":{"left":0.053905681818181776,"bottom":-0.212890625,"right":0.56953068181818178,"top":0.818359375},"atlasBounds":{"left":5061.5,"bottom":7164.5,"right":5193.5,"top":7428.5}},{"unicode":1119,"advance":0.59999999999999998,"planeBounds":{"left":0.028015624999999988,"bottom":-0.158203125,"right":0.57098437499999999,"top":0.583984375},"atlasBounds":{"left":7684.5,"bottom":4189.5,"right":7823.5,"top":4379.5}},{"unicode":1168,"advance":0.59999999999999998,"planeBounds":{"left":0.059312500000000025,"bottom":-0.033203125,"right":0.66868749999999999,"top":0.857421875},"atlasBounds":{"left":7842.5,"bottom":5892.5,"right":7998.5,"top":6120.5}},{"unicode":1169,"advance":0.59999999999999998,"planeBounds":{"left":0.063109374999999995,"bottom":-0.033203125,"right":0.61389062500000002,"top":0.689453125},"atlasBounds":{"left":1564.5,"bottom":3989.5,"right":1705.5,"top":4174.5}},{"unicode":1170,"advance":0.59999999999999998,"planeBounds":{"left":0.014234374999999981,"bottom":-0.033203125,"right":0.65876562500000002,"top":0.763671875},"atlasBounds":{"left":2765.5,"bottom":4585.5,"right":2930.5,"top":4789.5}},{"unicode":1171,"advance":0.59999999999999998,"planeBounds":{"left":0.022078125000000011,"bottom":-0.033203125,"right":0.61192187500000006,"top":0.583984375},"atlasBounds":{"left":3003.5,"bottom":3485.5,"right":3154.5,"top":3643.5}},{"unicode":1178,"advance":0.59999999999999998,"planeBounds":{"left":0.018406250000000023,"bottom":-0.173828125,"right":0.63559375000000007,"top":0.763671875},"atlasBounds":{"left":1308.5,"bottom":5880.5,"right":1466.5,"top":6120.5}},{"unicode":1179,"advance":0.59999999999999998,"planeBounds":{"left":0.025984375000000007,"bottom":-0.173828125,"right":0.60801562500000006,"top":0.583984375},"atlasBounds":{"left":6573.5,"bottom":4185.5,"right":6722.5,"top":4379.5}},{"unicode":1186,"advance":0.59999999999999998,"planeBounds":{"left":0.023890625000000006,"bottom":-0.173828125,"right":0.59810937500000005,"top":0.763671875},"atlasBounds":{"left":1467.5,"bottom":5880.5,"right":1614.5,"top":6120.5}},{"unicode":1187,"advance":0.59999999999999998,"planeBounds":{"left":0.025515624999999993,"bottom":-0.173828125,"right":0.56848437500000004,"top":0.583984375},"atlasBounds":{"left":6433.5,"bottom":4185.5,"right":6572.5,"top":4379.5}},{"unicode":1198,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.763671875},"atlasBounds":{"left":1825.5,"bottom":4585.5,"right":1973.5,"top":4789.5}},{"unicode":1199,"advance":0.59999999999999998,"planeBounds":{"left":0.082281249999999986,"bottom":-0.212890625,"right":0.60571874999999997,"top":0.583984375},"atlasBounds":{"left":1690.5,"bottom":4585.5,"right":1824.5,"top":4789.5}},{"unicode":1200,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.763671875},"atlasBounds":{"left":1541.5,"bottom":4585.5,"right":1689.5,"top":4789.5}},{"unicode":1201,"advance":0.59999999999999998,"planeBounds":{"left":0.046703124999999998,"bottom":-0.212890625,"right":0.60529687500000007,"top":0.583984375},"atlasBounds":{"left":1397.5,"bottom":4585.5,"right":1540.5,"top":4789.5}},{"unicode":1206,"advance":0.59999999999999998,"planeBounds":{"left":0.067620233050847509,"bottom":-0.173828125,"right":0.59105773305084741,"top":0.763671875},"atlasBounds":{"left":1615.5,"bottom":5880.5,"right":1749.5,"top":6120.5}},{"unicode":1207,"advance":0.59999999999999998,"planeBounds":{"left":0.062641509433962281,"bottom":-0.173828125,"right":0.56264150943396229,"top":0.583984375},"atlasBounds":{"left":6304.5,"bottom":4185.5,"right":6432.5,"top":4379.5}},{"unicode":1210,"advance":0.59999999999999998,"planeBounds":{"left":-0.0010854166666666449,"bottom":-0.033203125,"right":0.52235208333333338,"top":0.763671875},"atlasBounds":{"left":960.5,"bottom":4585.5,"right":1094.5,"top":4789.5}},{"unicode":1211,"advance":0.59999999999999998,"planeBounds":{"left":0.033333333333333381,"bottom":-0.033203125,"right":0.53333333333333344,"top":0.583984375},"atlasBounds":{"left":1002.5,"bottom":3485.5,"right":1130.5,"top":3643.5}},{"unicode":1240,"advance":0.59999999999999998,"planeBounds":{"left":0.042218749999999992,"bottom":-0.044921875,"right":0.58128124999999997,"top":0.771484375},"atlasBounds":{"left":7629.5,"bottom":5215.5,"right":7767.5,"top":5424.5}},{"unicode":1241,"advance":0.59999999999999998,"planeBounds":{"left":0.044752827109896844,"bottom":-0.044921875,"right":0.55256532710989692,"top":0.591796875},"atlasBounds":{"left":562.5,"bottom":3644.5,"right":692.5,"top":3807.5}},{"unicode":1244,"advance":0.59999999999999998,"planeBounds":{"left":-0.065999999999999975,"bottom":-0.033203125,"right":0.68400000000000005,"top":0.970703125},"atlasBounds":{"left":5785.5,"bottom":6635.5,"right":5977.5,"top":6892.5}},{"unicode":1245,"advance":0.59999999999999998,"planeBounds":{"left":-0.048250000000000001,"bottom":-0.033203125,"right":0.63924999999999998,"top":0.802734375},"atlasBounds":{"left":1415.5,"bottom":5210.5,"right":1591.5,"top":5424.5}},{"unicode":1246,"advance":0.59999999999999998,"planeBounds":{"left":0.011828819444444463,"bottom":-0.044921875,"right":0.59386006944444447,"top":0.970703125},"atlasBounds":{"left":7874.5,"bottom":6896.5,"right":8023.5,"top":7156.5}},{"unicode":1247,"advance":0.59999999999999998,"planeBounds":{"left":0.021256944444444488,"bottom":-0.041015625,"right":0.56813194444444448,"top":0.802734375},"atlasBounds":{"left":268.5,"bottom":5208.5,"right":408.5,"top":5424.5}},{"unicode":1252,"advance":0.59999999999999998,"planeBounds":{"left":0.024937500000000008,"bottom":-0.033203125,"right":0.60306250000000006,"top":0.970703125},"atlasBounds":{"left":5978.5,"bottom":6635.5,"right":6126.5,"top":6892.5}},{"unicode":1253,"advance":0.59999999999999998,"planeBounds":{"left":0.049999431818181776,"bottom":-0.044921875,"right":0.57343693181818178,"top":0.802734375},"atlasBounds":{"left":0.5,"bottom":5207.5,"right":134.5,"top":5424.5}},{"unicode":1254,"advance":0.59999999999999998,"planeBounds":{"left":0.048412682149362514,"bottom":-0.044921875,"right":0.59528768214936245,"top":0.970703125},"atlasBounds":{"left":0.5,"bottom":6632.5,"right":140.5,"top":6892.5}},{"unicode":1255,"advance":0.59999999999999998,"planeBounds":{"left":0.050037682149362557,"bottom":-0.044921875,"right":0.56566268214936255,"top":0.802734375},"atlasBounds":{"left":135.5,"bottom":5207.5,"right":267.5,"top":5424.5}},{"unicode":1256,"advance":0.59999999999999998,"planeBounds":{"left":0.050297387295081999,"bottom":-0.044921875,"right":0.57764113729508204,"top":0.771484375},"atlasBounds":{"left":7768.5,"bottom":5215.5,"right":7903.5,"top":5424.5}},{"unicode":1257,"advance":0.59999999999999998,"planeBounds":{"left":0.049758333333333335,"bottom":-0.044921875,"right":0.54975833333333335,"top":0.591796875},"atlasBounds":{"left":7573.5,"bottom":3821.5,"right":7701.5,"top":3984.5}},{"unicode":1268,"advance":0.59999999999999998,"planeBounds":{"left":0.07762023305084749,"bottom":-0.033203125,"right":0.60105773305084742,"top":0.970703125},"atlasBounds":{"left":6127.5,"bottom":6635.5,"right":6261.5,"top":6892.5}},{"unicode":1269,"advance":0.59999999999999998,"planeBounds":{"left":0.06683595387840674,"bottom":-0.033203125,"right":0.56683595387840668,"top":0.802734375},"atlasBounds":{"left":1592.5,"bottom":5210.5,"right":1720.5,"top":5424.5}},{"unicode":2794,"advance":0.59999999999999998,"planeBounds":{"left":0.055859374999999996,"bottom":-0.044921875,"right":0.54414062500000004,"top":0.681640625},"atlasBounds":{"left":575.5,"bottom":3988.5,"right":700.5,"top":4174.5}},{"unicode":7808,"advance":0.59999999999999998,"planeBounds":{"left":0.044046875000000027,"bottom":-0.033203125,"right":0.67295312500000004,"top":0.982421875},"atlasBounds":{"left":141.5,"bottom":6632.5,"right":302.5,"top":6892.5}},{"unicode":7809,"advance":0.59999999999999998,"planeBounds":{"left":0.051984375000000006,"bottom":-0.033203125,"right":0.63401562499999997,"top":0.818359375},"atlasBounds":{"left":6367.5,"bottom":5428.5,"right":6516.5,"top":5646.5}},{"unicode":7810,"advance":0.59999999999999998,"planeBounds":{"left":0.044046875000000027,"bottom":-0.033203125,"right":0.67295312500000004,"top":0.982421875},"atlasBounds":{"left":303.5,"bottom":6632.5,"right":464.5,"top":6892.5}},{"unicode":7811,"advance":0.59999999999999998,"planeBounds":{"left":0.051984375000000006,"bottom":-0.033203125,"right":0.63401562499999997,"top":0.818359375},"atlasBounds":{"left":6517.5,"bottom":5428.5,"right":6666.5,"top":5646.5}},{"unicode":7812,"advance":0.59999999999999998,"planeBounds":{"left":0.044046875000000027,"bottom":-0.033203125,"right":0.67295312500000004,"top":0.970703125},"atlasBounds":{"left":6262.5,"bottom":6635.5,"right":6423.5,"top":6892.5}},{"unicode":7813,"advance":0.59999999999999998,"planeBounds":{"left":0.051984375000000006,"bottom":-0.033203125,"right":0.63401562499999997,"top":0.802734375},"atlasBounds":{"left":1721.5,"bottom":5210.5,"right":1870.5,"top":5424.5}},{"unicode":7838,"advance":0.59999999999999998,"planeBounds":{"left":0.027671875000000016,"bottom":-0.033203125,"right":0.62532812500000001,"top":0.763671875},"atlasBounds":{"left":5874.5,"bottom":4792.5,"right":6027.5,"top":4996.5}},{"unicode":7840,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.240234375,"right":0.52748437500000001,"top":0.763671875},"atlasBounds":{"left":6424.5,"bottom":6635.5,"right":6563.5,"top":6892.5}},{"unicode":7841,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.240234375,"right":0.56438100961538451,"top":0.591796875},"atlasBounds":{"left":1871.5,"bottom":5211.5,"right":2004.5,"top":5424.5}},{"unicode":7842,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.033203125,"right":0.52748437500000001,"top":1.009765625},"atlasBounds":{"left":1906.5,"bottom":7161.5,"right":2045.5,"top":7428.5}},{"unicode":7843,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.044921875,"right":0.56438100961538451,"top":0.853515625},"atlasBounds":{"left":6833.5,"bottom":5890.5,"right":6966.5,"top":6120.5}},{"unicode":7844,"advance":0.59999999999999998,"planeBounds":{"left":-0.014953124999999972,"bottom":-0.033203125,"right":0.73895312499999999,"top":1.052734375},"atlasBounds":{"left":4813.5,"bottom":7506.5,"right":5006.5,"top":7784.5}},{"unicode":7845,"advance":0.59999999999999998,"planeBounds":{"left":0.044865384615384599,"bottom":-0.044921875,"right":0.7323653846153847,"top":0.931640625},"atlasBounds":{"left":3775.5,"bottom":6123.5,"right":3951.5,"top":6373.5}},{"unicode":7846,"advance":0.59999999999999998,"planeBounds":{"left":-0.014625000000000015,"bottom":-0.033203125,"right":0.641625,"top":1.052734375},"atlasBounds":{"left":5007.5,"bottom":7506.5,"right":5175.5,"top":7784.5}},{"unicode":7847,"advance":0.59999999999999998,"planeBounds":{"left":0.043193509615384609,"bottom":-0.044921875,"right":0.63303725961538471,"top":0.931640625},"atlasBounds":{"left":3952.5,"bottom":6123.5,"right":4103.5,"top":6373.5}},{"unicode":7848,"advance":0.59999999999999998,"planeBounds":{"left":-0.01470937500000004,"bottom":-0.033203125,"right":0.68450937499999998,"top":1.052734375},"atlasBounds":{"left":5176.5,"bottom":7506.5,"right":5355.5,"top":7784.5}},{"unicode":7849,"advance":0.59999999999999998,"planeBounds":{"left":0.04361538461538457,"bottom":-0.044921875,"right":0.6686153846153845,"top":0.962890625},"atlasBounds":{"left":2076.5,"bottom":6634.5,"right":2236.5,"top":6892.5}},{"unicode":7850,"advance":0.59999999999999998,"planeBounds":{"left":-0.016046874999999974,"bottom":-0.033203125,"right":0.60504687499999998,"top":1.052734375},"atlasBounds":{"left":5356.5,"bottom":7506.5,"right":5515.5,"top":7784.5}},{"unicode":7851,"advance":0.59999999999999998,"planeBounds":{"left":0.04317788461538459,"bottom":-0.044921875,"right":0.59005288461538463,"top":0.958984375},"atlasBounds":{"left":6698.5,"bottom":6635.5,"right":6838.5,"top":6892.5}},{"unicode":7852,"advance":0.59999999999999998,"planeBounds":{"left":-0.015374999999999986,"bottom":-0.240234375,"right":0.57837499999999997,"top":0.982421875},"atlasBounds":{"left":1185.5,"bottom":7471.5,"right":1337.5,"top":7784.5}},{"unicode":7853,"advance":0.59999999999999998,"planeBounds":{"left":0.04484975961538458,"bottom":-0.240234375,"right":0.56438100961538451,"top":0.818359375},"atlasBounds":{"left":0.5,"bottom":7157.5,"right":133.5,"top":7428.5}},{"unicode":7854,"advance":0.59999999999999998,"planeBounds":{"left":-0.014781249999999982,"bottom":-0.033203125,"right":0.58678125000000003,"top":1.052734375},"atlasBounds":{"left":5516.5,"bottom":7506.5,"right":5670.5,"top":7784.5}},{"unicode":7855,"advance":0.59999999999999998,"planeBounds":{"left":0.044443509615384583,"bottom":-0.044921875,"right":0.57178725961538457,"top":0.923828125},"atlasBounds":{"left":6971.5,"bottom":6125.5,"right":7106.5,"top":6373.5}},{"unicode":7856,"advance":0.59999999999999998,"planeBounds":{"left":-0.014781249999999982,"bottom":-0.033203125,"right":0.58678125000000003,"top":1.052734375},"atlasBounds":{"left":5671.5,"bottom":7506.5,"right":5825.5,"top":7784.5}},{"unicode":7857,"advance":0.59999999999999998,"planeBounds":{"left":0.044443509615384583,"bottom":-0.044921875,"right":0.57178725961538457,"top":0.923828125},"atlasBounds":{"left":7107.5,"bottom":6125.5,"right":7242.5,"top":6373.5}},{"unicode":7858,"advance":0.59999999999999998,"planeBounds":{"left":-0.014781249999999982,"bottom":-0.033203125,"right":0.58678125000000003,"top":1.052734375},"atlasBounds":{"left":5826.5,"bottom":7506.5,"right":5980.5,"top":7784.5}},{"unicode":7859,"advance":0.59999999999999998,"planeBounds":{"left":0.044443509615384583,"bottom":-0.044921875,"right":0.57178725961538457,"top":0.962890625},"atlasBounds":{"left":2237.5,"bottom":6634.5,"right":2372.5,"top":6892.5}},{"unicode":7860,"advance":0.59999999999999998,"planeBounds":{"left":-0.016046874999999974,"bottom":-0.033203125,"right":0.60504687499999998,"top":1.052734375},"atlasBounds":{"left":5981.5,"bottom":7506.5,"right":6140.5,"top":7784.5}},{"unicode":7861,"advance":0.59999999999999998,"planeBounds":{"left":0.04317788461538459,"bottom":-0.044921875,"right":0.59005288461538463,"top":0.958984375},"atlasBounds":{"left":6839.5,"bottom":6635.5,"right":6979.5,"top":6892.5}},{"unicode":7862,"advance":0.59999999999999998,"planeBounds":{"left":-0.015734374999999981,"bottom":-0.240234375,"right":0.58973437500000003,"top":0.982421875},"atlasBounds":{"left":1338.5,"bottom":7471.5,"right":1493.5,"top":7784.5}},{"unicode":7863,"advance":0.59999999999999998,"planeBounds":{"left":0.044396634615384581,"bottom":-0.240234375,"right":0.56783413461538457,"top":0.818359375},"atlasBounds":{"left":134.5,"bottom":7157.5,"right":268.5,"top":7428.5}},{"unicode":7864,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.240234375,"right":0.61801562499999996,"top":0.763671875},"atlasBounds":{"left":6980.5,"bottom":6635.5,"right":7129.5,"top":6892.5}},{"unicode":7865,"advance":0.59999999999999998,"planeBounds":{"left":0.04740393518518516,"bottom":-0.240234375,"right":0.55521643518518526,"top":0.591796875},"atlasBounds":{"left":2005.5,"bottom":5211.5,"right":2135.5,"top":5424.5}},{"unicode":7866,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":1.009765625},"atlasBounds":{"left":2046.5,"bottom":7161.5,"right":2195.5,"top":7428.5}},{"unicode":7867,"advance":0.59999999999999998,"planeBounds":{"left":0.04740393518518516,"bottom":-0.044921875,"right":0.55521643518518526,"top":0.853515625},"atlasBounds":{"left":6967.5,"bottom":5890.5,"right":7097.5,"top":6120.5}},{"unicode":7868,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":0.978515625},"atlasBounds":{"left":1364.5,"bottom":6633.5,"right":1513.5,"top":6892.5}},{"unicode":7869,"advance":0.59999999999999998,"planeBounds":{"left":0.047906249999999991,"bottom":-0.044921875,"right":0.57134375000000004,"top":0.814453125},"atlasBounds":{"left":2984.5,"bottom":5426.5,"right":3118.5,"top":5646.5}},{"unicode":7870,"advance":0.59999999999999998,"planeBounds":{"left":0.036578125000000017,"bottom":-0.033203125,"right":0.75142187500000002,"top":1.052734375},"atlasBounds":{"left":6141.5,"bottom":7506.5,"right":6324.5,"top":7784.5}},{"unicode":7871,"advance":0.59999999999999998,"planeBounds":{"left":0.048281250000000005,"bottom":-0.044921875,"right":0.72796875000000005,"top":0.931640625},"atlasBounds":{"left":4104.5,"bottom":6123.5,"right":4278.5,"top":6373.5}},{"unicode":7872,"advance":0.59999999999999998,"planeBounds":{"left":0.034953125000000022,"bottom":-0.033203125,"right":0.65604687500000003,"top":1.052734375},"atlasBounds":{"left":6325.5,"bottom":7506.5,"right":6484.5,"top":7784.5}},{"unicode":7873,"advance":0.59999999999999998,"planeBounds":{"left":0.048562500000000022,"bottom":-0.044921875,"right":0.62668750000000006,"top":0.931640625},"atlasBounds":{"left":4279.5,"bottom":6123.5,"right":4427.5,"top":6373.5}},{"unicode":7874,"advance":0.59999999999999998,"planeBounds":{"left":0.034868749999999997,"bottom":-0.033203125,"right":0.69893125,"top":1.052734375},"atlasBounds":{"left":6485.5,"bottom":7506.5,"right":6655.5,"top":7784.5}},{"unicode":7875,"advance":0.59999999999999998,"planeBounds":{"left":0.048984374999999983,"bottom":-0.044921875,"right":0.66226562499999997,"top":0.962890625},"atlasBounds":{"left":2373.5,"bottom":6634.5,"right":2530.5,"top":6892.5}},{"unicode":7876,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.033203125,"right":0.61801562499999996,"top":1.052734375},"atlasBounds":{"left":6656.5,"bottom":7506.5,"right":6805.5,"top":7784.5}},{"unicode":7877,"advance":0.59999999999999998,"planeBounds":{"left":0.048546875000000003,"bottom":-0.044921875,"right":0.58370312499999999,"top":0.958984375},"atlasBounds":{"left":7130.5,"bottom":6635.5,"right":7267.5,"top":6892.5}},{"unicode":7878,"advance":0.59999999999999998,"planeBounds":{"left":0.035984375000000006,"bottom":-0.240234375,"right":0.61801562499999996,"top":0.982421875},"atlasBounds":{"left":1494.5,"bottom":7471.5,"right":1643.5,"top":7784.5}},{"unicode":7879,"advance":0.59999999999999998,"planeBounds":{"left":0.04740393518518516,"bottom":-0.240234375,"right":0.55521643518518526,"top":0.818359375},"atlasBounds":{"left":269.5,"bottom":7157.5,"right":399.5,"top":7428.5}},{"unicode":7880,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.033203125,"right":0.58939062500000006,"top":1.009765625},"atlasBounds":{"left":2196.5,"bottom":7161.5,"right":2337.5,"top":7428.5}},{"unicode":7881,"advance":0.59999999999999998,"planeBounds":{"left":0.011828124999999983,"bottom":-0.033203125,"right":0.53917187499999997,"top":0.853515625},"atlasBounds":{"left":2008.5,"bottom":5647.5,"right":2143.5,"top":5874.5}},{"unicode":7882,"advance":0.59999999999999998,"planeBounds":{"left":0.038609374999999994,"bottom":-0.240234375,"right":0.58939062500000006,"top":0.763671875},"atlasBounds":{"left":7268.5,"bottom":6635.5,"right":7409.5,"top":6892.5}},{"unicode":7883,"advance":0.59999999999999998,"planeBounds":{"left":0.011828124999999983,"bottom":-0.240234375,"right":0.53917187499999997,"top":0.802734375},"atlasBounds":{"left":2338.5,"bottom":7161.5,"right":2473.5,"top":7428.5}},{"unicode":7884,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.240234375,"right":0.57938729508196729,"top":0.771484375},"atlasBounds":{"left":1514.5,"bottom":6633.5,"right":1650.5,"top":6892.5}},{"unicode":7885,"advance":0.59999999999999998,"planeBounds":{"left":0.049500000000000044,"bottom":-0.240234375,"right":0.54949999999999999,"top":0.591796875},"atlasBounds":{"left":2136.5,"bottom":5211.5,"right":2264.5,"top":5424.5}},{"unicode":7886,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.044921875,"right":0.57938729508196729,"top":1.009765625},"atlasBounds":{"left":882.5,"bottom":7158.5,"right":1018.5,"top":7428.5}},{"unicode":7887,"advance":0.59999999999999998,"planeBounds":{"left":0.049500000000000044,"bottom":-0.044921875,"right":0.54949999999999999,"top":0.853515625},"atlasBounds":{"left":7098.5,"bottom":5890.5,"right":7226.5,"top":6120.5}},{"unicode":7888,"advance":0.59999999999999998,"planeBounds":{"left":0.048452612704918074,"bottom":-0.044921875,"right":0.73985886270491819,"top":1.052734375},"atlasBounds":{"left":4175.5,"bottom":7503.5,"right":4352.5,"top":7784.5}},{"unicode":7889,"advance":0.59999999999999998,"planeBounds":{"left":0.048811987704918076,"bottom":-0.044921875,"right":0.72849948770491812,"top":0.931640625},"atlasBounds":{"left":4428.5,"bottom":6123.5,"right":4602.5,"top":6373.5}},{"unicode":7890,"advance":0.59999999999999998,"planeBounds":{"left":0.048780737704918087,"bottom":-0.044921875,"right":0.64253073770491809,"top":1.052734375},"atlasBounds":{"left":4353.5,"bottom":7503.5,"right":4505.5,"top":7784.5}},{"unicode":7891,"advance":0.59999999999999998,"planeBounds":{"left":0.04909323770491808,"bottom":-0.044921875,"right":0.62721823770491814,"top":0.931640625},"atlasBounds":{"left":4603.5,"bottom":6123.5,"right":4751.5,"top":6373.5}},{"unicode":7892,"advance":0.59999999999999998,"planeBounds":{"left":0.048696362704918068,"bottom":-0.044921875,"right":0.68541511270491806,"top":1.052734375},"atlasBounds":{"left":4506.5,"bottom":7503.5,"right":4669.5,"top":7784.5}},{"unicode":7893,"advance":0.59999999999999998,"planeBounds":{"left":0.049515112704918096,"bottom":-0.044921875,"right":0.66279636270491815,"top":0.962890625},"atlasBounds":{"left":2531.5,"bottom":6634.5,"right":2688.5,"top":6892.5}},{"unicode":7894,"advance":0.59999999999999998,"planeBounds":{"left":0.049311987704918077,"bottom":-0.044921875,"right":0.60399948770491818,"top":1.052734375},"atlasBounds":{"left":4670.5,"bottom":7503.5,"right":4812.5,"top":7784.5}},{"unicode":7895,"advance":0.59999999999999998,"planeBounds":{"left":0.049077612704918061,"bottom":-0.044921875,"right":0.58423386270491806,"top":0.958984375},"atlasBounds":{"left":7410.5,"bottom":6635.5,"right":7547.5,"top":6892.5}},{"unicode":7896,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.240234375,"right":0.57938729508196729,"top":0.982421875},"atlasBounds":{"left":1644.5,"bottom":7471.5,"right":1780.5,"top":7784.5}},{"unicode":7897,"advance":0.59999999999999998,"planeBounds":{"left":0.048702612704918047,"bottom":-0.240234375,"right":0.55260886270491805,"top":0.818359375},"atlasBounds":{"left":400.5,"bottom":7157.5,"right":529.5,"top":7428.5}},{"unicode":7898,"advance":0.59999999999999998,"planeBounds":{"left":0.048733862704918092,"bottom":-0.044921875,"right":0.63857761270491809,"top":0.982421875},"atlasBounds":{"left":282.5,"bottom":6893.5,"right":433.5,"top":7156.5}},{"unicode":7899,"advance":0.59999999999999998,"planeBounds":{"left":0.049999487704918084,"bottom":-0.044921875,"right":0.62031198770491813,"top":0.818359375},"atlasBounds":{"left":683.5,"bottom":5425.5,"right":829.5,"top":5646.5}},{"unicode":7900,"advance":0.59999999999999998,"planeBounds":{"left":0.048733862704918092,"bottom":-0.044921875,"right":0.63857761270491809,"top":0.982421875},"atlasBounds":{"left":434.5,"bottom":6893.5,"right":585.5,"top":7156.5}},{"unicode":7901,"advance":0.59999999999999998,"planeBounds":{"left":0.049999487704918084,"bottom":-0.044921875,"right":0.62031198770491813,"top":0.818359375},"atlasBounds":{"left":830.5,"bottom":5425.5,"right":976.5,"top":5646.5}},{"unicode":7902,"advance":0.59999999999999998,"planeBounds":{"left":0.048733862704918092,"bottom":-0.044921875,"right":0.63857761270491809,"top":1.009765625},"atlasBounds":{"left":1019.5,"bottom":7158.5,"right":1170.5,"top":7428.5}},{"unicode":7903,"advance":0.59999999999999998,"planeBounds":{"left":0.049999487704918084,"bottom":-0.044921875,"right":0.62031198770491813,"top":0.853515625},"atlasBounds":{"left":7227.5,"bottom":5890.5,"right":7373.5,"top":6120.5}},{"unicode":7904,"advance":0.59999999999999998,"planeBounds":{"left":0.048733862704918092,"bottom":-0.044921875,"right":0.63857761270491809,"top":0.978515625},"atlasBounds":{"left":1534.5,"bottom":6894.5,"right":1685.5,"top":7156.5}},{"unicode":7905,"advance":0.59999999999999998,"planeBounds":{"left":0.049999487704918084,"bottom":-0.044921875,"right":0.62031198770491813,"top":0.814453125},"atlasBounds":{"left":3119.5,"bottom":5426.5,"right":3265.5,"top":5646.5}},{"unicode":7906,"advance":0.59999999999999998,"planeBounds":{"left":0.048733862704918092,"bottom":-0.240234375,"right":0.63857761270491809,"top":0.841796875},"atlasBounds":{"left":7238.5,"bottom":7507.5,"right":7389.5,"top":7784.5}},{"unicode":7907,"advance":0.59999999999999998,"planeBounds":{"left":0.049999487704918084,"bottom":-0.240234375,"right":0.62031198770491813,"top":0.673828125},"atlasBounds":{"left":5349.5,"bottom":5886.5,"right":5495.5,"top":6120.5}},{"unicode":7908,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.240234375,"right":0.60298384533898308,"top":0.763671875},"atlasBounds":{"left":7651.5,"bottom":6635.5,"right":7792.5,"top":6892.5}},{"unicode":7909,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.240234375,"right":0.57481197033898312,"top":0.583984375},"atlasBounds":{"left":3148.5,"bottom":5213.5,"right":3282.5,"top":5424.5}},{"unicode":7910,"advance":0.59999999999999998,"planeBounds":{"left":0.052202595338983075,"bottom":-0.044921875,"right":0.60298384533898308,"top":1.009765625},"atlasBounds":{"left":1171.5,"bottom":7158.5,"right":1312.5,"top":7428.5}},{"unicode":7911,"advance":0.59999999999999998,"planeBounds":{"left":0.051374470338983062,"bottom":-0.044921875,"right":0.57481197033898312,"top":0.853515625},"atlasBounds":{"left":7374.5,"bottom":5890.5,"right":7508.5,"top":6120.5}},{"unicode":7912,"advance":0.59999999999999998,"planeBounds":{"left":0.05246822033898306,"bottom":-0.044921875,"right":0.70871822033898313,"top":0.982421875},"atlasBounds":{"left":586.5,"bottom":6893.5,"right":754.5,"top":7156.5}},{"unicode":7913,"advance":0.59999999999999998,"planeBounds":{"left":0.051140095338983053,"bottom":-0.044921875,"right":0.68004634533898312,"top":0.818359375},"atlasBounds":{"left":977.5,"bottom":5425.5,"right":1138.5,"top":5646.5}},{"unicode":7914,"advance":0.59999999999999998,"planeBounds":{"left":0.05246822033898306,"bottom":-0.044921875,"right":0.70871822033898313,"top":0.982421875},"atlasBounds":{"left":755.5,"bottom":6893.5,"right":923.5,"top":7156.5}},{"unicode":7915,"advance":0.59999999999999998,"planeBounds":{"left":0.051140095338983053,"bottom":-0.044921875,"right":0.68004634533898312,"top":0.818359375},"atlasBounds":{"left":1139.5,"bottom":5425.5,"right":1300.5,"top":5646.5}},{"unicode":7916,"advance":0.59999999999999998,"planeBounds":{"left":0.05246822033898306,"bottom":-0.044921875,"right":0.70871822033898313,"top":1.009765625},"atlasBounds":{"left":1313.5,"bottom":7158.5,"right":1481.5,"top":7428.5}},{"unicode":7917,"advance":0.59999999999999998,"planeBounds":{"left":0.051140095338983053,"bottom":-0.044921875,"right":0.68004634533898312,"top":0.853515625},"atlasBounds":{"left":7509.5,"bottom":5890.5,"right":7670.5,"top":6120.5}},{"unicode":7918,"advance":0.59999999999999998,"planeBounds":{"left":0.05246822033898306,"bottom":-0.044921875,"right":0.70871822033898313,"top":0.978515625},"atlasBounds":{"left":1686.5,"bottom":6894.5,"right":1854.5,"top":7156.5}},{"unicode":7919,"advance":0.59999999999999998,"planeBounds":{"left":0.051140095338983053,"bottom":-0.044921875,"right":0.68004634533898312,"top":0.814453125},"atlasBounds":{"left":3266.5,"bottom":5426.5,"right":3427.5,"top":5646.5}},{"unicode":7920,"advance":0.59999999999999998,"planeBounds":{"left":0.05246822033898306,"bottom":-0.240234375,"right":0.70871822033898313,"top":0.873046875},"atlasBounds":{"left":3463.5,"bottom":7499.5,"right":3631.5,"top":7784.5}},{"unicode":7921,"advance":0.59999999999999998,"planeBounds":{"left":0.051140095338983053,"bottom":-0.240234375,"right":0.68004634533898312,"top":0.693359375},"atlasBounds":{"left":3310.5,"bottom":5881.5,"right":3471.5,"top":6120.5}},{"unicode":7922,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.982421875},"atlasBounds":{"left":465.5,"bottom":6632.5,"right":613.5,"top":6892.5}},{"unicode":7923,"advance":0.59999999999999998,"planeBounds":{"left":0.053905681818181776,"bottom":-0.212890625,"right":0.56953068181818178,"top":0.818359375},"atlasBounds":{"left":5194.5,"bottom":7164.5,"right":5326.5,"top":7428.5}},{"unicode":7924,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.240234375,"right":0.6610625,"top":0.763671875},"atlasBounds":{"left":7896.5,"bottom":6635.5,"right":8044.5,"top":6892.5}},{"unicode":7925,"advance":0.59999999999999998,"planeBounds":{"left":0.05407755681818182,"bottom":-0.279296875,"right":0.57360880681818183,"top":0.583984375},"atlasBounds":{"left":1301.5,"bottom":5425.5,"right":1434.5,"top":5646.5}},{"unicode":7926,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":1.009765625},"atlasBounds":{"left":2474.5,"bottom":7161.5,"right":2622.5,"top":7428.5}},{"unicode":7927,"advance":0.59999999999999998,"planeBounds":{"left":0.053905681818181776,"bottom":-0.212890625,"right":0.56953068181818178,"top":0.853515625},"atlasBounds":{"left":7746.5,"bottom":7511.5,"right":7878.5,"top":7784.5}},{"unicode":7928,"advance":0.59999999999999998,"planeBounds":{"left":0.082937499999999997,"bottom":-0.033203125,"right":0.6610625,"top":0.978515625},"atlasBounds":{"left":1651.5,"bottom":6633.5,"right":1799.5,"top":6892.5}},{"unicode":7929,"advance":0.59999999999999998,"planeBounds":{"left":0.054905681818181777,"bottom":-0.212890625,"right":0.57053068181818178,"top":0.814453125},"atlasBounds":{"left":924.5,"bottom":6893.5,"right":1056.5,"top":7156.5}},{"unicode":8208,"advance":0.59999999999999998,"planeBounds":{"left":0.11123437500000001,"bottom":0.267578125,"right":0.505765625,"top":0.392578125},"atlasBounds":{"left":8039.5,"bottom":6135.5,"right":8140.5,"top":6167.5}},{"unicode":8211,"advance":0.59999999999999998,"planeBounds":{"left":0.051187499999999983,"bottom":0.267578125,"right":0.56681250000000005,"top":0.392578125},"atlasBounds":{"left":8038.5,"bottom":3505.5,"right":8170.5,"top":3537.5}},{"unicode":8212,"advance":0.59999999999999998,"planeBounds":{"left":-0.027437500000000007,"bottom":0.267578125,"right":0.6444375,"top":0.392578125},"atlasBounds":{"left":171.5,"bottom":3145.5,"right":343.5,"top":3177.5}},{"unicode":8216,"advance":0.59999999999999998,"planeBounds":{"left":0.22628645833333333,"bottom":0.392578125,"right":0.47238020833333333,"top":0.763671875},"atlasBounds":{"left":1681.5,"bottom":3243.5,"right":1744.5,"top":3338.5}},{"unicode":8217,"advance":0.59999999999999998,"planeBounds":{"left":0.20784598214285716,"bottom":0.392578125,"right":0.4539397321428571,"top":0.763671875},"atlasBounds":{"left":1861.5,"bottom":3243.5,"right":1924.5,"top":3338.5}},{"unicode":8218,"advance":0.59999999999999998,"planeBounds":{"left":0.12784598214285714,"bottom":-0.193359375,"right":0.37393973214285714,"top":0.177734375},"atlasBounds":{"left":1483.5,"bottom":3243.5,"right":1546.5,"top":3338.5}},{"unicode":8220,"advance":0.59999999999999998,"planeBounds":{"left":0.12727524038461538,"bottom":0.392578125,"right":0.57649399038461535,"top":0.763671875},"atlasBounds":{"left":1745.5,"bottom":3243.5,"right":1860.5,"top":3338.5}},{"unicode":8221,"advance":0.59999999999999998,"planeBounds":{"left":0.021330357142857175,"bottom":0.396484375,"right":0.47445535714285719,"top":0.767578125},"atlasBounds":{"left":1925.5,"bottom":3243.5,"right":2041.5,"top":3338.5}},{"unicode":8222,"advance":0.59999999999999998,"planeBounds":{"left":0.019158482142857185,"bottom":-0.193359375,"right":0.4996272321428572,"top":0.177734375},"atlasBounds":{"left":8068.5,"bottom":7215.5,"right":8191.5,"top":7310.5}},{"unicode":8224,"advance":0.59999999999999998,"planeBounds":{"left":0.093046875000000029,"bottom":-0.142578125,"right":0.59695312499999997,"top":0.861328125},"atlasBounds":{"left":0.5,"bottom":6374.5,"right":129.5,"top":6631.5}},{"unicode":8225,"advance":0.59999999999999998,"planeBounds":{"left":0.034703125000000001,"bottom":-0.142578125,"right":0.59329687500000006,"top":0.861328125},"atlasBounds":{"left":130.5,"bottom":6374.5,"right":273.5,"top":6631.5}},{"unicode":8226,"advance":0.59999999999999998,"planeBounds":{"left":0.17530342741935484,"bottom":0.228515625,"right":0.4526471774193549,"top":0.501953125},"atlasBounds":{"left":5482.5,"bottom":3268.5,"right":5553.5,"top":3338.5}},{"unicode":8228,"advance":0.59999999999999998,"planeBounds":{"left":0.15487839673913045,"bottom":-0.044921875,"right":0.37753464673913045,"top":0.177734375},"atlasBounds":{"left":0.5,"bottom":2979.5,"right":57.5,"top":3036.5}},{"unicode":8230,"advance":0.59999999999999998,"planeBounds":{"left":-0.010796875000000011,"bottom":-0.044921875,"right":0.54779687499999996,"top":0.205078125},"atlasBounds":{"left":7662.5,"bottom":3274.5,"right":7805.5,"top":3338.5}},{"unicode":8240,"advance":0.59999999999999998,"planeBounds":{"left":-0.086889616935483874,"bottom":-0.037109375,"right":0.62795413306451608,"top":0.767578125},"atlasBounds":{"left":6647.5,"bottom":5000.5,"right":6830.5,"top":5206.5}},{"unicode":8242,"advance":0.59999999999999998,"planeBounds":{"left":0.11359375000000001,"bottom":0.388671875,"right":0.37140624999999999,"top":0.759765625},"atlasBounds":{"left":1614.5,"bottom":3243.5,"right":1680.5,"top":3338.5}},{"unicode":8243,"advance":0.59999999999999998,"planeBounds":{"left":0.018671875000000015,"bottom":0.388671875,"right":0.491328125,"top":0.759765625},"atlasBounds":{"left":2042.5,"bottom":3243.5,"right":2163.5,"top":3338.5}},{"unicode":8244,"advance":0.59999999999999998,"planeBounds":{"left":-0.061406249999999975,"bottom":0.384765625,"right":0.57140625,"top":0.759765625},"atlasBounds":{"left":1320.5,"bottom":3242.5,"right":1482.5,"top":3338.5}},{"unicode":8249,"advance":0.59999999999999998,"planeBounds":{"left":0.12904687500000001,"bottom":0.005859375,"right":0.50795312500000001,"top":0.552734375},"atlasBounds":{"left":1121.5,"bottom":3344.5,"right":1218.5,"top":3484.5}},{"unicode":8250,"advance":0.59999999999999998,"planeBounds":{"left":0.092546875000000001,"bottom":0.005859375,"right":0.47145312500000003,"top":0.552734375},"atlasBounds":{"left":1023.5,"bottom":3344.5,"right":1120.5,"top":3484.5}},{"unicode":8254,"advance":0.59999999999999998,"planeBounds":{"left":0.035843750000000001,"bottom":0.736328125,"right":0.73115625000000006,"top":0.861328125},"atlasBounds":{"left":347.5,"bottom":3152.5,"right":525.5,"top":3184.5}},{"unicode":8255,"advance":0.59999999999999998,"planeBounds":{"left":-0.016406249999999973,"bottom":-0.212890625,"right":0.61640625000000004,"top":0.033203125},"atlasBounds":{"left":7806.5,"bottom":3275.5,"right":7968.5,"top":3338.5}},{"unicode":8261,"advance":0.59999999999999998,"planeBounds":{"left":0.11784375,"bottom":-0.142578125,"right":0.56315625000000002,"top":0.861328125},"atlasBounds":{"left":274.5,"bottom":6374.5,"right":388.5,"top":6631.5}},{"unicode":8262,"advance":0.59999999999999998,"planeBounds":{"left":0.064843750000000006,"bottom":-0.142578125,"right":0.51015624999999998,"top":0.861328125},"atlasBounds":{"left":389.5,"bottom":6374.5,"right":503.5,"top":6631.5}},{"unicode":8304,"advance":0.59999999999999998,"planeBounds":{"left":0.15082812500000001,"bottom":0.345703125,"right":0.55317187499999998,"top":0.869140625},"atlasBounds":{"left":3055.5,"bottom":3350.5,"right":3158.5,"top":3484.5}},{"unicode":8308,"advance":0.59999999999999998,"planeBounds":{"left":0.1545,"bottom":0.349609375,"right":0.52949999999999997,"top":0.861328125},"atlasBounds":{"left":5322.5,"bottom":3353.5,"right":5418.5,"top":3484.5}},{"unicode":8309,"advance":0.59999999999999998,"planeBounds":{"left":0.14942812500000002,"bottom":0.345703125,"right":0.55177187500000002,"top":0.861328125},"atlasBounds":{"left":4468.5,"bottom":3352.5,"right":4571.5,"top":3484.5}},{"unicode":8310,"advance":0.59999999999999998,"planeBounds":{"left":0.14869726966873703,"bottom":0.345703125,"right":0.53150976966873709,"top":0.861328125},"atlasBounds":{"left":4106.5,"bottom":3352.5,"right":4204.5,"top":3484.5}},{"unicode":8311,"advance":0.59999999999999998,"planeBounds":{"left":0.18864062500000001,"bottom":0.349609375,"right":0.57535937500000001,"top":0.861328125},"atlasBounds":{"left":5419.5,"bottom":3353.5,"right":5518.5,"top":3484.5}},{"unicode":8312,"advance":0.59999999999999998,"planeBounds":{"left":0.14717496294466403,"bottom":0.345703125,"right":0.54951871294466403,"top":0.869140625},"atlasBounds":{"left":3159.5,"bottom":3350.5,"right":3262.5,"top":3484.5}},{"unicode":8313,"advance":0.59999999999999998,"planeBounds":{"left":0.17636744432661719,"bottom":0.349609375,"right":0.55136744432661711,"top":0.869140625},"atlasBounds":{"left":3396.5,"bottom":3351.5,"right":3492.5,"top":3484.5}},{"unicode":8314,"advance":0.59999999999999998,"planeBounds":{"left":0.15021875000000004,"bottom":0.271484375,"right":0.50178124999999996,"top":0.619140625},"atlasBounds":{"left":4904.5,"bottom":3249.5,"right":4994.5,"top":3338.5}},{"unicode":8316,"advance":0.59999999999999998,"planeBounds":{"left":0.1521875,"bottom":0.443359375,"right":0.54281250000000003,"top":0.712890625},"atlasBounds":{"left":5554.5,"bottom":3269.5,"right":5654.5,"top":3338.5}},{"unicode":8320,"advance":0.59999999999999998,"planeBounds":{"left":0.068828125000000004,"bottom":-0.169921875,"right":0.47117187500000002,"top":0.353515625},"atlasBounds":{"left":2704.5,"bottom":3350.5,"right":2807.5,"top":3484.5}},{"unicode":8321,"advance":0.59999999999999998,"planeBounds":{"left":0.14459374999999999,"bottom":-0.166015625,"right":0.52740624999999997,"top":0.349609375},"atlasBounds":{"left":4919.5,"bottom":3352.5,"right":5017.5,"top":3484.5}},{"unicode":8322,"advance":0.59999999999999998,"planeBounds":{"left":0.14774857954545456,"bottom":-0.166015625,"right":0.55009232954545451,"top":0.353515625},"atlasBounds":{"left":3702.5,"bottom":3351.5,"right":3805.5,"top":3484.5}},{"unicode":8323,"advance":0.59999999999999998,"planeBounds":{"left":0.15428804347826089,"bottom":-0.169921875,"right":0.56053804347826086,"top":0.349609375},"atlasBounds":{"left":3597.5,"bottom":3351.5,"right":3701.5,"top":3484.5}},{"unicode":8324,"advance":0.59999999999999998,"planeBounds":{"left":0.072499999999999995,"bottom":-0.166015625,"right":0.44750000000000001,"top":0.349609375},"atlasBounds":{"left":4371.5,"bottom":3352.5,"right":4467.5,"top":3484.5}},{"unicode":8325,"advance":0.59999999999999998,"planeBounds":{"left":0.067428125000000019,"bottom":-0.169921875,"right":0.46977187500000001,"top":0.349609375},"atlasBounds":{"left":3493.5,"bottom":3351.5,"right":3596.5,"top":3484.5}},{"unicode":8326,"advance":0.59999999999999998,"planeBounds":{"left":0.066697269668737069,"bottom":-0.169921875,"right":0.44950976966873707,"top":0.349609375},"atlasBounds":{"left":3806.5,"bottom":3351.5,"right":3904.5,"top":3484.5}},{"unicode":8327,"advance":0.59999999999999998,"planeBounds":{"left":0.106640625,"bottom":-0.166015625,"right":0.49335937499999999,"top":0.349609375},"atlasBounds":{"left":5123.5,"bottom":3352.5,"right":5222.5,"top":3484.5}},{"unicode":8328,"advance":0.59999999999999998,"planeBounds":{"left":0.06417496294466403,"bottom":-0.181640625,"right":0.46651871294466402,"top":0.341796875},"atlasBounds":{"left":2808.5,"bottom":3350.5,"right":2911.5,"top":3484.5}},{"unicode":8329,"advance":0.59999999999999998,"planeBounds":{"left":0.094367444326617173,"bottom":-0.166015625,"right":0.46936744432661714,"top":0.353515625},"atlasBounds":{"left":3905.5,"bottom":3351.5,"right":4001.5,"top":3484.5}},{"unicode":8363,"advance":0.59999999999999998,"planeBounds":{"left":0.011062499999999992,"bottom":-0.091796875,"right":0.68293749999999998,"top":0.763671875},"atlasBounds":{"left":4222.5,"bottom":5427.5,"right":4394.5,"top":5646.5}},{"unicode":8364,"advance":0.59999999999999998,"planeBounds":{"left":-0.0094955357142857046,"bottom":-0.044921875,"right":0.59206696428571437,"top":0.771484375},"atlasBounds":{"left":4704.5,"bottom":5215.5,"right":4858.5,"top":5424.5}},{"unicode":8366,"advance":0.59999999999999998,"planeBounds":{"left":0.040812500000000022,"bottom":-0.033203125,"right":0.65018750000000003,"top":0.763671875},"atlasBounds":{"left":3392.5,"bottom":4792.5,"right":3548.5,"top":4996.5}},{"unicode":8372,"advance":0.59999999999999998,"planeBounds":{"left":0.021984375000000007,"bottom":-0.044921875,"right":0.60401562500000006,"top":0.771484375},"atlasBounds":{"left":4859.5,"bottom":5215.5,"right":5008.5,"top":5424.5}},{"unicode":8381,"advance":0.59999999999999998,"planeBounds":{"left":-0.016648437500000016,"bottom":-0.033203125,"right":0.6161640625,"top":0.763671875},"atlasBounds":{"left":4290.5,"bottom":4585.5,"right":4452.5,"top":4789.5}},{"unicode":8383,"advance":0.59999999999999998,"planeBounds":{"left":0.0265632267441861,"bottom":-0.173828125,"right":0.5812507267441861,"top":0.904296875},"atlasBounds":{"left":7603.5,"bottom":7508.5,"right":7745.5,"top":7784.5}},{"unicode":8450,"advance":0.59999999999999998,"planeBounds":{"left":0.042187499999999982,"bottom":-0.044921875,"right":0.55781250000000004,"top":0.771484375},"atlasBounds":{"left":7904.5,"bottom":5215.5,"right":8036.5,"top":5424.5}},{"unicode":8461,"advance":0.59999999999999998,"planeBounds":{"left":0.042187499999999982,"bottom":-0.033203125,"right":0.55781250000000004,"top":0.763671875},"atlasBounds":{"left":6028.5,"bottom":4792.5,"right":6160.5,"top":4996.5}},{"unicode":8467,"advance":0.59999999999999998,"planeBounds":{"left":0.041417613636363641,"bottom":-0.033203125,"right":0.56485511363636365,"top":0.771484375},"atlasBounds":{"left":7913.5,"bottom":5000.5,"right":8047.5,"top":5206.5}},{"unicode":8469,"advance":0.59999999999999998,"planeBounds":{"left":0.063671875000000017,"bottom":-0.033203125,"right":0.53632812500000004,"top":0.763671875},"atlasBounds":{"left":270.5,"bottom":4585.5,"right":391.5,"top":4789.5}},{"unicode":8470,"advance":0.59999999999999998,"planeBounds":{"left":-0.037030172413793132,"bottom":-0.033203125,"right":0.68171982758620686,"top":0.771484375},"atlasBounds":{"left":7296.5,"bottom":5000.5,"right":7480.5,"top":5206.5}},{"unicode":8473,"advance":0.59999999999999998,"planeBounds":{"left":0.042968749999999986,"bottom":-0.033203125,"right":0.58203125,"top":0.763671875},"atlasBounds":{"left":4453.5,"bottom":4585.5,"right":4591.5,"top":4789.5}},{"unicode":8474,"advance":0.59999999999999998,"planeBounds":{"left":0.042187499999999982,"bottom":-0.212890625,"right":0.55781250000000004,"top":0.771484375},"atlasBounds":{"left":1230.5,"bottom":6121.5,"right":1362.5,"top":6373.5}},{"unicode":8477,"advance":0.59999999999999998,"planeBounds":{"left":0.043203125000000002,"bottom":-0.033203125,"right":0.60179687500000001,"top":0.763671875},"atlasBounds":{"left":1957.5,"bottom":4380.5,"right":2100.5,"top":4584.5}},{"unicode":8482,"advance":0.59999999999999998,"planeBounds":{"left":0.05331250000000002,"bottom":0.337890625,"right":0.66268749999999998,"top":0.763671875},"atlasBounds":{"left":7781.5,"bottom":3375.5,"right":7937.5,"top":3484.5}},{"unicode":8484,"advance":0.59999999999999998,"planeBounds":{"left":0.038281249999999989,"bottom":-0.033203125,"right":0.56171875000000004,"top":0.763671875},"atlasBounds":{"left":135.5,"bottom":4585.5,"right":269.5,"top":4789.5}},{"unicode":8494,"advance":0.59999999999999998,"planeBounds":{"left":0.028515624999999992,"bottom":-0.044921875,"right":0.57148437500000004,"top":0.771484375},"atlasBounds":{"left":0.5,"bottom":4997.5,"right":139.5,"top":5206.5}},{"unicode":8512,"advance":0.59999999999999998,"planeBounds":{"left":0.022109374999999994,"bottom":-0.212890625,"right":0.57289062499999999,"top":0.763671875},"atlasBounds":{"left":4752.5,"bottom":6123.5,"right":4893.5,"top":6373.5}},{"unicode":8586,"advance":0.59999999999999998,"planeBounds":{"left":0.0098593749999999862,"bottom":-0.044921875,"right":0.584078125,"top":0.763671875},"atlasBounds":{"left":4080.5,"bottom":4999.5,"right":4227.5,"top":5206.5}},{"unicode":8587,"advance":0.59999999999999998,"planeBounds":{"left":0.008007812500000008,"bottom":-0.033203125,"right":0.57050781250000004,"top":0.771484375},"atlasBounds":{"left":7633.5,"bottom":5000.5,"right":7777.5,"top":5206.5}},{"unicode":8592,"advance":0.59999999999999998,"planeBounds":{"left":-0.021406249999999967,"bottom":0.017578125,"right":0.61140625000000004,"top":0.642578125},"atlasBounds":{"left":5373.5,"bottom":3647.5,"right":5535.5,"top":3807.5}},{"unicode":8593,"advance":0.59999999999999998,"planeBounds":{"left":0.0070312500000000088,"bottom":-0.033203125,"right":0.59296875000000004,"top":0.771484375},"atlasBounds":{"left":7145.5,"bottom":5000.5,"right":7295.5,"top":5206.5}},{"unicode":8594,"advance":0.59999999999999998,"planeBounds":{"left":-0.011406249999999968,"bottom":0.017578125,"right":0.62140625000000005,"top":0.642578125},"atlasBounds":{"left":6534.5,"bottom":3647.5,"right":6696.5,"top":3807.5}},{"unicode":8595,"advance":0.59999999999999998,"planeBounds":{"left":0.036031250000000008,"bottom":-0.044921875,"right":0.62196874999999996,"top":0.763671875},"atlasBounds":{"left":3201.5,"bottom":4999.5,"right":3351.5,"top":5206.5}},{"unicode":8596,"advance":0.59999999999999998,"planeBounds":{"left":-0.112109375,"bottom":0.017578125,"right":0.71210937500000004,"top":0.642578125},"atlasBounds":{"left":6827.5,"bottom":3647.5,"right":7038.5,"top":3807.5}},{"unicode":8597,"advance":0.59999999999999998,"planeBounds":{"left":0.0070312500000000088,"bottom":-0.205078125,"right":0.59296875000000004,"top":0.931640625},"atlasBounds":{"left":3136.5,"bottom":7493.5,"right":3286.5,"top":7784.5}},{"unicode":8598,"advance":0.59999999999999998,"planeBounds":{"left":0.027609374999999995,"bottom":0.208984375,"right":0.57839062500000005,"top":0.763671875},"atlasBounds":{"left":306.5,"bottom":3342.5,"right":447.5,"top":3484.5}},{"unicode":8599,"advance":0.59999999999999998,"planeBounds":{"left":0.021609374999999993,"bottom":0.208984375,"right":0.57239062500000004,"top":0.763671875},"atlasBounds":{"left":448.5,"bottom":3342.5,"right":589.5,"top":3484.5}},{"unicode":8600,"advance":0.59999999999999998,"planeBounds":{"left":0.021609374999999993,"bottom":-0.033203125,"right":0.57239062500000004,"top":0.517578125},"atlasBounds":{"left":739.5,"bottom":3343.5,"right":880.5,"top":3484.5}},{"unicode":8601,"advance":0.59999999999999998,"planeBounds":{"left":0.027609374999999995,"bottom":-0.033203125,"right":0.57839062500000005,"top":0.517578125},"atlasBounds":{"left":881.5,"bottom":3343.5,"right":1022.5,"top":3484.5}},{"unicode":8605,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.126953125,"right":0.62226562500000004,"top":0.478515625},"atlasBounds":{"left":4587.5,"bottom":3248.5,"right":4752.5,"top":3338.5}},{"unicode":8606,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.060546875,"right":0.62226562500000004,"top":0.486328125},"atlasBounds":{"left":7938.5,"bottom":3375.5,"right":8103.5,"top":3484.5}},{"unicode":8608,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.060546875,"right":0.62226562500000004,"top":0.486328125},"atlasBounds":{"left":0.5,"bottom":3229.5,"right":165.5,"top":3338.5}},{"unicode":8610,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.060546875,"right":0.62226562500000004,"top":0.486328125},"atlasBounds":{"left":166.5,"bottom":3229.5,"right":331.5,"top":3338.5}},{"unicode":8611,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.060546875,"right":0.62226562500000004,"top":0.486328125},"atlasBounds":{"left":7449.5,"bottom":3375.5,"right":7614.5,"top":3484.5}},{"unicode":8613,"advance":0.59999999999999998,"planeBounds":{"left":0.077343750000000003,"bottom":-0.033203125,"right":0.52265625000000004,"top":0.583984375},"atlasBounds":{"left":0.5,"bottom":3485.5,"right":114.5,"top":3643.5}},{"unicode":8614,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.060546875,"right":0.62226562500000004,"top":0.486328125},"atlasBounds":{"left":7615.5,"bottom":3375.5,"right":7780.5,"top":3484.5}},{"unicode":8615,"advance":0.59999999999999998,"planeBounds":{"left":0.077343750000000003,"bottom":-0.033203125,"right":0.52265625000000004,"top":0.583984375},"atlasBounds":{"left":4483.5,"bottom":3485.5,"right":4597.5,"top":3643.5}},{"unicode":8617,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.083984375,"right":0.62226562500000004,"top":0.763671875},"atlasBounds":{"left":1087.5,"bottom":3810.5,"right":1252.5,"top":3984.5}},{"unicode":8618,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.083984375,"right":0.62226562500000004,"top":0.763671875},"atlasBounds":{"left":1429.5,"bottom":3810.5,"right":1594.5,"top":3984.5}},{"unicode":8621,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.060546875,"right":0.62226562500000004,"top":0.486328125},"atlasBounds":{"left":332.5,"bottom":3229.5,"right":497.5,"top":3338.5}},{"unicode":8638,"advance":0.59999999999999998,"planeBounds":{"left":0.223203125,"bottom":-0.212890625,"right":0.53179687500000006,"top":0.873046875},"atlasBounds":{"left":6806.5,"bottom":7506.5,"right":6885.5,"top":7784.5}},{"unicode":8649,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":-0.072265625,"right":0.62226562500000004,"top":0.626953125},"atlasBounds":{"left":2034.5,"bottom":3995.5,"right":2199.5,"top":4174.5}},{"unicode":8656,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.001953125,"right":0.63203125000000004,"top":0.658203125},"atlasBounds":{"left":6635.5,"bottom":3816.5,"right":6805.5,"top":3984.5}},{"unicode":8657,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":-0.212890625,"right":0.64179687500000004,"top":0.873046875},"atlasBounds":{"left":6886.5,"bottom":7506.5,"right":7061.5,"top":7784.5}},{"unicode":8658,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.001953125,"right":0.63203125000000004,"top":0.658203125},"atlasBounds":{"left":6252.5,"bottom":3816.5,"right":6422.5,"top":3984.5}},{"unicode":8659,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":-0.224609375,"right":0.64179687500000004,"top":0.861328125},"atlasBounds":{"left":7062.5,"bottom":7506.5,"right":7237.5,"top":7784.5}},{"unicode":8660,"advance":0.59999999999999998,"planeBounds":{"left":-0.112109375,"bottom":0.001953125,"right":0.71210937500000004,"top":0.658203125},"atlasBounds":{"left":6423.5,"bottom":3816.5,"right":6634.5,"top":3984.5}},{"unicode":8667,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.001953125,"right":0.63203125000000004,"top":0.658203125},"atlasBounds":{"left":5911.5,"bottom":3816.5,"right":6081.5,"top":3984.5}},{"unicode":8670,"advance":0.59999999999999998,"planeBounds":{"left":0.083203125000000003,"bottom":-0.033203125,"right":0.51679687500000004,"top":0.794921875},"atlasBounds":{"left":2377.5,"bottom":5212.5,"right":2488.5,"top":5424.5}},{"unicode":8671,"advance":0.59999999999999998,"planeBounds":{"left":0.083203125000000003,"bottom":-0.064453125,"right":0.51679687500000004,"top":0.763671875},"atlasBounds":{"left":2265.5,"bottom":5212.5,"right":2376.5,"top":5424.5}},{"unicode":8677,"advance":0.59999999999999998,"planeBounds":{"left":-0.03240624999999997,"bottom":0.103515625,"right":0.60040625000000003,"top":0.556640625},"atlasBounds":{"left":6869.5,"bottom":3368.5,"right":7031.5,"top":3484.5}},{"unicode":8679,"advance":0.59999999999999998,"planeBounds":{"left":-0.012499999999999973,"bottom":-0.033203125,"right":0.61250000000000004,"top":0.822265625},"atlasBounds":{"left":3609.5,"bottom":5427.5,"right":3769.5,"top":5646.5}},{"unicode":8680,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.017578125,"right":0.63203125000000004,"top":0.654296875},"atlasBounds":{"left":131.5,"bottom":3644.5,"right":301.5,"top":3807.5}},{"unicode":8682,"advance":0.59999999999999998,"planeBounds":{"left":-0.012499999999999973,"bottom":-0.033203125,"right":0.61250000000000004,"top":0.822265625},"atlasBounds":{"left":3906.5,"bottom":5427.5,"right":4066.5,"top":5646.5}},{"unicode":8704,"advance":0.59999999999999998,"planeBounds":{"left":0.10051562499999998,"bottom":-0.033203125,"right":0.643484375,"top":0.763671875},"atlasBounds":{"left":1950.5,"bottom":4175.5,"right":2089.5,"top":4379.5}},{"unicode":8705,"advance":0.59999999999999998,"planeBounds":{"left":0.13507812499999999,"bottom":-0.044921875,"right":0.47492187499999999,"top":0.591796875},"atlasBounds":{"left":8103.5,"bottom":3821.5,"right":8190.5,"top":3984.5}},{"unicode":8706,"advance":0.59999999999999998,"planeBounds":{"left":0.049510465371127993,"bottom":-0.044921875,"right":0.55732296537112802,"top":0.763671875},"atlasBounds":{"left":3949.5,"bottom":4999.5,"right":4079.5,"top":5206.5}},{"unicode":8707,"advance":0.59999999999999998,"planeBounds":{"left":0.0099843750000000071,"bottom":-0.033203125,"right":0.59201562500000005,"top":0.763671875},"atlasBounds":{"left":6766.5,"bottom":4380.5,"right":6915.5,"top":4584.5}},{"unicode":8708,"advance":0.59999999999999998,"planeBounds":{"left":-0.019093749999999979,"bottom":-0.142578125,"right":0.59809374999999998,"top":0.861328125},"atlasBounds":{"left":504.5,"bottom":6374.5,"right":662.5,"top":6631.5}},{"unicode":8709,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":-0.033203125,"right":0.65579687500000006,"top":0.763671875},"atlasBounds":{"left":2551.5,"bottom":4175.5,"right":2726.5,"top":4379.5}},{"unicode":8710,"advance":0.59999999999999998,"planeBounds":{"left":-0.01548437500000001,"bottom":-0.033203125,"right":0.52748437500000001,"top":0.763671875},"atlasBounds":{"left":841.5,"bottom":4380.5,"right":980.5,"top":4584.5}},{"unicode":8711,"advance":0.59999999999999998,"planeBounds":{"left":0.10051562499999998,"bottom":-0.033203125,"right":0.643484375,"top":0.763671875},"atlasBounds":{"left":422.5,"bottom":4380.5,"right":561.5,"top":4584.5}},{"unicode":8712,"advance":0.59999999999999998,"planeBounds":{"left":0.011520833333333353,"bottom":0.044921875,"right":0.65214583333333342,"top":0.673828125},"atlasBounds":{"left":3868.5,"bottom":3646.5,"right":4032.5,"top":3807.5}},{"unicode":8713,"advance":0.59999999999999998,"planeBounds":{"left":0.01358934294871795,"bottom":-0.091796875,"right":0.65030809294871794,"top":0.814453125},"atlasBounds":{"left":5496.5,"bottom":5888.5,"right":5659.5,"top":6120.5}},{"unicode":8714,"advance":0.59999999999999998,"planeBounds":{"left":0.13312499999999999,"bottom":0.150390625,"right":0.47687499999999999,"top":0.513671875},"atlasBounds":{"left":8103.5,"bottom":3714.5,"right":8191.5,"top":3807.5}},{"unicode":8715,"advance":0.59999999999999998,"planeBounds":{"left":-0.024199596774193543,"bottom":0.044921875,"right":0.61642540322580652,"top":0.673828125},"atlasBounds":{"left":4927.5,"bottom":3646.5,"right":5091.5,"top":3807.5}},{"unicode":8716,"advance":0.59999999999999998,"planeBounds":{"left":-0.022346874999999999,"bottom":-0.091796875,"right":0.61437187500000001,"top":0.814453125},"atlasBounds":{"left":5660.5,"bottom":5888.5,"right":5823.5,"top":6120.5}},{"unicode":8718,"advance":0.59999999999999998,"planeBounds":{"left":0.087109375000000003,"bottom":-0.033203125,"right":0.51289062500000004,"top":0.544921875},"atlasBounds":{"left":7482.5,"bottom":3495.5,"right":7591.5,"top":3643.5}},{"unicode":8719,"advance":0.59999999999999998,"planeBounds":{"left":0.013734374999999981,"bottom":-0.212890625,"right":0.65826562499999997,"top":0.763671875},"atlasBounds":{"left":4894.5,"bottom":6123.5,"right":5059.5,"top":6373.5}},{"unicode":8720,"advance":0.59999999999999998,"planeBounds":{"left":-0.058265625000000022,"bottom":-0.212890625,"right":0.58626562500000001,"top":0.763671875},"atlasBounds":{"left":5060.5,"bottom":6123.5,"right":5225.5,"top":6373.5}},{"unicode":8721,"advance":0.59999999999999998,"planeBounds":{"left":-0.05065625,"bottom":-0.212890625,"right":0.64465625000000004,"top":0.763671875},"atlasBounds":{"left":5226.5,"bottom":6123.5,"right":5404.5,"top":6373.5}},{"unicode":8722,"advance":0.59999999999999998,"planeBounds":{"left":0.11123437500000001,"bottom":0.267578125,"right":0.505765625,"top":0.392578125},"atlasBounds":{"left":8090.5,"bottom":4403.5,"right":8191.5,"top":4435.5}},{"unicode":8723,"advance":0.59999999999999998,"planeBounds":{"left":0.079031250000000011,"bottom":-0.052734375,"right":0.66496875,"top":0.583984375},"atlasBounds":{"left":1832.5,"bottom":3644.5,"right":1982.5,"top":3807.5}},{"unicode":8725,"advance":0.59999999999999998,"planeBounds":{"left":0.019078125000000012,"bottom":-0.033203125,"right":0.60892187500000006,"top":0.763671875},"atlasBounds":{"left":881.5,"bottom":4175.5,"right":1032.5,"top":4379.5}},{"unicode":8728,"advance":0.59999999999999998,"planeBounds":{"left":0.14765625000000002,"bottom":0.177734375,"right":0.45234374999999999,"top":0.482421875},"atlasBounds":{"left":5403.5,"bottom":3260.5,"right":5481.5,"top":3338.5}},{"unicode":8729,"advance":0.59999999999999998,"planeBounds":{"left":0.15487839673913045,"bottom":0.208984375,"right":0.37753464673913045,"top":0.431640625},"atlasBounds":{"left":0.5,"bottom":2921.5,"right":57.5,"top":2978.5}},{"unicode":8730,"advance":0.59999999999999998,"planeBounds":{"left":0.031828124999999985,"bottom":-0.033203125,"right":0.68417187499999998,"top":0.763671875},"atlasBounds":{"left":3549.5,"bottom":4792.5,"right":3716.5,"top":4996.5}},{"unicode":8734,"advance":0.59999999999999998,"planeBounds":{"left":0.0099506801209104032,"bottom":0.126953125,"right":0.60760693012091038,"top":0.537109375},"atlasBounds":{"left":8038.5,"bottom":3538.5,"right":8191.5,"top":3643.5}},{"unicode":8739,"advance":0.59999999999999998,"planeBounds":{"left":0.190953125,"bottom":-0.033203125,"right":0.437046875,"top":0.763671875},"atlasBounds":{"left":8128.5,"bottom":4792.5,"right":8191.5,"top":4996.5}},{"unicode":8740,"advance":0.59999999999999998,"planeBounds":{"left":0.097703125000000002,"bottom":-0.033203125,"right":0.531296875,"top":0.763671875},"atlasBounds":{"left":1540.5,"bottom":4380.5,"right":1651.5,"top":4584.5}},{"unicode":8741,"advance":0.59999999999999998,"planeBounds":{"left":0.091343750000000001,"bottom":-0.033203125,"right":0.53665625000000006,"top":0.763671875},"atlasBounds":{"left":1135.5,"bottom":4380.5,"right":1249.5,"top":4584.5}},{"unicode":8743,"advance":0.59999999999999998,"planeBounds":{"left":0.0022812499999999821,"bottom":0.017578125,"right":0.52571875000000001,"top":0.583984375},"atlasBounds":{"left":0.5,"bottom":3339.5,"right":134.5,"top":3484.5}},{"unicode":8744,"advance":0.59999999999999998,"planeBounds":{"left":0.081281249999999985,"bottom":0.017578125,"right":0.60471874999999997,"top":0.583984375},"atlasBounds":{"left":7903.5,"bottom":3498.5,"right":8037.5,"top":3643.5}},{"unicode":8745,"advance":0.59999999999999998,"planeBounds":{"left":0.0018169642857142866,"bottom":0.044921875,"right":0.58775446428571432,"top":0.626953125},"atlasBounds":{"left":5626.5,"bottom":3494.5,"right":5776.5,"top":3643.5}},{"unicode":8746,"advance":0.59999999999999998,"planeBounds":{"left":0.03121673387096777,"bottom":0.044921875,"right":0.61715423387096779,"top":0.626953125},"atlasBounds":{"left":5935.5,"bottom":3494.5,"right":6085.5,"top":3643.5}},{"unicode":8747,"advance":0.59999999999999998,"planeBounds":{"left":-0.10564062499999997,"bottom":-0.212890625,"right":0.63264062500000007,"top":0.763671875},"atlasBounds":{"left":5405.5,"bottom":6123.5,"right":5594.5,"top":6373.5}},{"unicode":8756,"advance":0.59999999999999998,"planeBounds":{"left":0.0050781250000000114,"bottom":-0.044921875,"right":0.59492187500000004,"top":0.591796875},"atlasBounds":{"left":3033.5,"bottom":3644.5,"right":3184.5,"top":3807.5}},{"unicode":8757,"advance":0.59999999999999998,"planeBounds":{"left":0.035078125000000016,"bottom":-0.044921875,"right":0.62492187499999996,"top":0.591796875},"atlasBounds":{"left":2461.5,"bottom":3644.5,"right":2612.5,"top":3807.5}},{"unicode":8758,"advance":0.59999999999999998,"planeBounds":{"left":0.19062500000000002,"bottom":-0.044921875,"right":0.40937499999999999,"top":0.591796875},"atlasBounds":{"left":2271.5,"bottom":3644.5,"right":2327.5,"top":3807.5}},{"unicode":8759,"advance":0.59999999999999998,"planeBounds":{"left":-0.0072590785573122254,"bottom":-0.044921875,"right":0.59820967144268777,"top":0.544921875},"atlasBounds":{"left":5318.5,"bottom":3492.5,"right":5473.5,"top":3643.5}},{"unicode":8760,"advance":0.59999999999999998,"planeBounds":{"left":0.092734375000000022,"bottom":0.150390625,"right":0.48726562500000004,"top":0.517578125},"atlasBounds":{"left":2707.5,"bottom":3244.5,"right":2808.5,"top":3338.5}},{"unicode":8761,"advance":0.59999999999999998,"planeBounds":{"left":-0.022729166666666696,"bottom":0.076171875,"right":0.66477083333333331,"top":0.583984375},"atlasBounds":{"left":5519.5,"bottom":3354.5,"right":5695.5,"top":3484.5}},{"unicode":8764,"advance":0.59999999999999998,"planeBounds":{"left":0.045874999999999985,"bottom":0.224609375,"right":0.577125,"top":0.482421875},"atlasBounds":{"left":7525.5,"bottom":3272.5,"right":7661.5,"top":3338.5}},{"unicode":8766,"advance":0.59999999999999998,"planeBounds":{"left":0.0065312500000000093,"bottom":0.154296875,"right":0.59246874999999999,"top":0.505859375},"atlasBounds":{"left":4753.5,"bottom":3248.5,"right":4903.5,"top":3338.5}},{"unicode":8771,"advance":0.59999999999999998,"planeBounds":{"left":0.035703125000000002,"bottom":0.150390625,"right":0.59429687500000006,"top":0.595703125},"atlasBounds":{"left":7159.5,"bottom":3370.5,"right":7302.5,"top":3484.5}},{"unicode":8773,"advance":0.59999999999999998,"planeBounds":{"left":0.022031250000000013,"bottom":0.064453125,"right":0.60796875000000006,"top":0.677734375},"atlasBounds":{"left":4892.5,"bottom":3486.5,"right":5042.5,"top":3643.5}},{"unicode":8775,"advance":0.59999999999999998,"planeBounds":{"left":0.022437500000000006,"bottom":-0.134765625,"right":0.6005625,"top":0.814453125},"atlasBounds":{"left":437.5,"bottom":5877.5,"right":585.5,"top":6120.5}},{"unicode":8776,"advance":0.59999999999999998,"planeBounds":{"left":0.024796874999999999,"bottom":0.080078125,"right":0.59120312500000005,"top":0.580078125},"atlasBounds":{"left":5908.5,"bottom":3356.5,"right":6053.5,"top":3484.5}},{"unicode":8777,"advance":0.59999999999999998,"planeBounds":{"left":0.025296875,"bottom":-0.091796875,"right":0.591703125,"top":0.814453125},"atlasBounds":{"left":5824.5,"bottom":5888.5,"right":5969.5,"top":6120.5}},{"unicode":8779,"advance":0.59999999999999998,"planeBounds":{"left":0.012125000000000014,"bottom":0.029296875,"right":0.60587500000000005,"top":0.630859375},"atlasBounds":{"left":5165.5,"bottom":3489.5,"right":5317.5,"top":3643.5}},{"unicode":8781,"advance":0.59999999999999998,"planeBounds":{"left":0.011125000000000013,"bottom":0.087890625,"right":0.60487500000000005,"top":0.572265625},"atlasBounds":{"left":6338.5,"bottom":3360.5,"right":6490.5,"top":3484.5}},{"unicode":8788,"advance":0.59999999999999998,"planeBounds":{"left":-0.041845108695652174,"bottom":0.072265625,"right":0.66127989130434783,"top":0.587890625},"atlasBounds":{"left":4738.5,"bottom":3352.5,"right":4918.5,"top":3484.5}},{"unicode":8791,"advance":0.59999999999999998,"planeBounds":{"left":0.030781249999999982,"bottom":0.064453125,"right":0.55421874999999998,"top":0.740234375},"atlasBounds":{"left":2390.5,"bottom":3811.5,"right":2524.5,"top":3984.5}},{"unicode":8799,"advance":0.59999999999999998,"planeBounds":{"left":0.030781249999999982,"bottom":0.064453125,"right":0.55421874999999998,"top":0.806640625},"atlasBounds":{"left":7373.5,"bottom":4189.5,"right":7507.5,"top":4379.5}},{"unicode":8800,"advance":0.59999999999999998,"planeBounds":{"left":0.039562499999999994,"bottom":-0.091796875,"right":0.58643750000000006,"top":0.814453125},"atlasBounds":{"left":5970.5,"bottom":5888.5,"right":6110.5,"top":6120.5}},{"unicode":8801,"advance":0.59999999999999998,"planeBounds":{"left":0.023343750000000003,"bottom":0.083984375,"right":0.59365625,"top":0.572265625},"atlasBounds":{"left":6054.5,"bottom":3359.5,"right":6200.5,"top":3484.5}},{"unicode":8802,"advance":0.59999999999999998,"planeBounds":{"left":0.023343750000000003,"bottom":-0.087890625,"right":0.59365625,"top":0.755859375},"atlasBounds":{"left":409.5,"bottom":5208.5,"right":555.5,"top":5424.5}},{"unicode":8803,"advance":0.59999999999999998,"planeBounds":{"left":0.030656249999999996,"bottom":0.068359375,"right":0.58534375000000005,"top":0.591796875},"atlasBounds":{"left":2912.5,"bottom":3350.5,"right":3054.5,"top":3484.5}},{"unicode":8804,"advance":0.59999999999999998,"planeBounds":{"left":0.0082656250000000177,"bottom":-0.033203125,"right":0.61373437500000005,"top":0.744140625},"atlasBounds":{"left":4822.5,"bottom":4180.5,"right":4977.5,"top":4379.5}},{"unicode":8805,"advance":0.59999999999999998,"planeBounds":{"left":0.0083437500000000022,"bottom":-0.033203125,"right":0.57865624999999998,"top":0.744140625},"atlasBounds":{"left":4675.5,"bottom":4180.5,"right":4821.5,"top":4379.5}},{"unicode":8810,"advance":0.59999999999999998,"planeBounds":{"left":-0.028968749999999995,"bottom":0.041015625,"right":0.68196875000000001,"top":0.619140625},"atlasBounds":{"left":7107.5,"bottom":3495.5,"right":7289.5,"top":3643.5}},{"unicode":8811,"advance":0.59999999999999998,"planeBounds":{"left":-0.064968749999999992,"bottom":0.041015625,"right":0.64596874999999998,"top":0.619140625},"atlasBounds":{"left":6924.5,"bottom":3495.5,"right":7106.5,"top":3643.5}},{"unicode":8812,"advance":0.59999999999999998,"planeBounds":{"left":0.12631249999999999,"bottom":-0.087890625,"right":0.48568749999999999,"top":0.638671875},"atlasBounds":{"left":1060.5,"bottom":3988.5,"right":1152.5,"top":4174.5}},{"unicode":8813,"advance":0.59999999999999998,"planeBounds":{"left":0.011125000000000013,"bottom":-0.033203125,"right":0.60487500000000005,"top":0.697265625},"atlasBounds":{"left":162.5,"bottom":3987.5,"right":314.5,"top":4174.5}},{"unicode":8814,"advance":0.59999999999999998,"planeBounds":{"left":0.042609374999999991,"bottom":-0.103515625,"right":0.59339062500000006,"top":0.763671875},"atlasBounds":{"left":3054.5,"bottom":5652.5,"right":3195.5,"top":5874.5}},{"unicode":8815,"advance":0.59999999999999998,"planeBounds":{"left":0.024015624999999988,"bottom":-0.103515625,"right":0.56698437499999998,"top":0.763671875},"atlasBounds":{"left":2914.5,"bottom":5652.5,"right":3053.5,"top":5874.5}},{"unicode":8816,"advance":0.59999999999999998,"planeBounds":{"left":0.011718750000000016,"bottom":-0.142578125,"right":0.61328125,"top":0.814453125},"atlasBounds":{"left":7884.5,"bottom":6128.5,"right":8038.5,"top":6373.5}},{"unicode":8817,"advance":0.59999999999999998,"planeBounds":{"left":-0.00056249999999999334,"bottom":-0.142578125,"right":0.57756249999999998,"top":0.814453125},"atlasBounds":{"left":0.5,"bottom":5875.5,"right":148.5,"top":6120.5}},{"unicode":8818,"advance":0.59999999999999998,"planeBounds":{"left":0.00085937500000002093,"bottom":-0.033203125,"right":0.614140625,"top":0.744140625},"atlasBounds":{"left":4362.5,"bottom":4180.5,"right":4519.5,"top":4379.5}},{"unicode":8819,"advance":0.59999999999999998,"planeBounds":{"left":0.0083437500000000039,"bottom":-0.037109375,"right":0.57865624999999998,"top":0.744140625},"atlasBounds":{"left":4215.5,"bottom":4179.5,"right":4361.5,"top":4379.5}},{"unicode":8826,"advance":0.59999999999999998,"planeBounds":{"left":0.041843749999999999,"bottom":0.021484375,"right":0.61215624999999996,"top":0.634765625},"atlasBounds":{"left":4745.5,"bottom":3486.5,"right":4891.5,"top":3643.5}},{"unicode":8827,"advance":0.59999999999999998,"planeBounds":{"left":0.018343750000000002,"bottom":0.021484375,"right":0.58865624999999999,"top":0.634765625},"atlasBounds":{"left":4598.5,"bottom":3486.5,"right":4744.5,"top":3643.5}},{"unicode":8828,"advance":0.59999999999999998,"planeBounds":{"left":-0.0024062499999999692,"bottom":-0.033203125,"right":0.63040625000000006,"top":0.759765625},"atlasBounds":{"left":3724.5,"bottom":4176.5,"right":3886.5,"top":4379.5}},{"unicode":8834,"advance":0.59999999999999998,"planeBounds":{"left":0.021222355769230768,"bottom":0.091796875,"right":0.64231610576923071,"top":0.626953125},"atlasBounds":{"left":1700.5,"bottom":3347.5,"right":1859.5,"top":3484.5}},{"unicode":8835,"advance":0.59999999999999998,"planeBounds":{"left":-0.017795454545454573,"bottom":0.091796875,"right":0.60720454545454539,"top":0.626953125},"atlasBounds":{"left":1539.5,"bottom":3347.5,"right":1699.5,"top":3484.5}},{"unicode":8836,"advance":0.59999999999999998,"planeBounds":{"left":0.01907954545454546,"bottom":-0.091796875,"right":0.64407954545454549,"top":0.814453125},"atlasBounds":{"left":6111.5,"bottom":5888.5,"right":6271.5,"top":6120.5}},{"unicode":8837,"advance":0.59999999999999998,"planeBounds":{"left":-0.017825581395348802,"bottom":-0.091796875,"right":0.60717441860465127,"top":0.814453125},"atlasBounds":{"left":6272.5,"bottom":5888.5,"right":6432.5,"top":6120.5}},{"unicode":8838,"advance":0.59999999999999998,"planeBounds":{"left":-0.014937500000000008,"bottom":-0.033203125,"right":0.65693750000000006,"top":0.708984375},"atlasBounds":{"left":7993.5,"bottom":4189.5,"right":8165.5,"top":4379.5}},{"unicode":8839,"advance":0.59999999999999998,"planeBounds":{"left":-0.036920454545454506,"bottom":-0.033203125,"right":0.61932954545454555,"top":0.708984375},"atlasBounds":{"left":7824.5,"bottom":4189.5,"right":7992.5,"top":4379.5}},{"unicode":8840,"advance":0.59999999999999998,"planeBounds":{"left":-0.10834517045454549,"bottom":-0.142578125,"right":0.6299360795454545,"top":0.861328125},"atlasBounds":{"left":663.5,"bottom":6374.5,"right":852.5,"top":6631.5}},{"unicode":8841,"advance":0.59999999999999998,"planeBounds":{"left":-0.037984375000000008,"bottom":-0.142578125,"right":0.62998437500000004,"top":0.861328125},"atlasBounds":{"left":853.5,"bottom":6374.5,"right":1024.5,"top":6631.5}},{"unicode":8846,"advance":0.59999999999999998,"planeBounds":{"left":0.038281249999999989,"bottom":-0.033203125,"right":0.56171875000000004,"top":0.763671875},"atlasBounds":{"left":6067.5,"bottom":4585.5,"right":6201.5,"top":4789.5}},{"unicode":8847,"advance":0.59999999999999998,"planeBounds":{"left":-0.014171875000000018,"bottom":0.064453125,"right":0.63817187500000006,"top":0.595703125},"atlasBounds":{"left":2099.5,"bottom":3348.5,"right":2266.5,"top":3484.5}},{"unicode":8848,"advance":0.59999999999999998,"planeBounds":{"left":-0.022171875000000018,"bottom":0.064453125,"right":0.63017187500000005,"top":0.595703125},"atlasBounds":{"left":2267.5,"bottom":3348.5,"right":2434.5,"top":3484.5}},{"unicode":8849,"advance":0.59999999999999998,"planeBounds":{"left":-0.032296875000000003,"bottom":-0.033203125,"right":0.651296875,"top":0.708984375},"atlasBounds":{"left":7508.5,"bottom":4189.5,"right":7683.5,"top":4379.5}},{"unicode":8850,"advance":0.59999999999999998,"planeBounds":{"left":-0.032296875000000003,"bottom":-0.033203125,"right":0.651296875,"top":0.708984375},"atlasBounds":{"left":7197.5,"bottom":4189.5,"right":7372.5,"top":4379.5}},{"unicode":8851,"advance":0.59999999999999998,"planeBounds":{"left":0.0028593750000000216,"bottom":0.044921875,"right":0.616140625,"top":0.626953125},"atlasBounds":{"left":5777.5,"bottom":3494.5,"right":5934.5,"top":3643.5}},{"unicode":8852,"advance":0.59999999999999998,"planeBounds":{"left":0.0028593750000000216,"bottom":0.044921875,"right":0.616140625,"top":0.626953125},"atlasBounds":{"left":6086.5,"bottom":3494.5,"right":6243.5,"top":3643.5}},{"unicode":8853,"advance":0.59999999999999998,"planeBounds":{"left":-0.032796875000000003,"bottom":-0.013671875,"right":0.65079687500000005,"top":0.673828125},"atlasBounds":{"left":5377.5,"bottom":3998.5,"right":5552.5,"top":4174.5}},{"unicode":8854,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":-0.013671875,"right":0.64179687500000004,"top":0.673828125},"atlasBounds":{"left":7478.5,"bottom":3998.5,"right":7653.5,"top":4174.5}},{"unicode":8855,"advance":0.59999999999999998,"planeBounds":{"left":-0.032796875000000003,"bottom":-0.013671875,"right":0.65079687500000005,"top":0.673828125},"atlasBounds":{"left":5905.5,"bottom":3998.5,"right":6080.5,"top":4174.5}},{"unicode":8856,"advance":0.59999999999999998,"planeBounds":{"left":-0.032796875000000003,"bottom":-0.013671875,"right":0.65079687500000005,"top":0.673828125},"atlasBounds":{"left":5729.5,"bottom":3998.5,"right":5904.5,"top":4174.5}},{"unicode":8857,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":-0.033203125,"right":0.64179687500000004,"top":0.654296875},"atlasBounds":{"left":3217.5,"bottom":3998.5,"right":3392.5,"top":4174.5}},{"unicode":8859,"advance":0.59999999999999998,"planeBounds":{"left":-0.032796875000000003,"bottom":-0.013671875,"right":0.65079687500000005,"top":0.673828125},"atlasBounds":{"left":7830.5,"bottom":3998.5,"right":8005.5,"top":4174.5}},{"unicode":8860,"advance":0.59999999999999998,"planeBounds":{"left":-0.032796875000000003,"bottom":-0.013671875,"right":0.65079687500000005,"top":0.673828125},"atlasBounds":{"left":7654.5,"bottom":3998.5,"right":7829.5,"top":4174.5}},{"unicode":8861,"advance":0.59999999999999998,"planeBounds":{"left":-0.032796875000000003,"bottom":-0.013671875,"right":0.65079687500000005,"top":0.673828125},"atlasBounds":{"left":3393.5,"bottom":3998.5,"right":3568.5,"top":4174.5}},{"unicode":8862,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.001953125,"right":0.63203125000000004,"top":0.662109375},"atlasBounds":{"left":5299.5,"bottom":3814.5,"right":5469.5,"top":3984.5}},{"unicode":8863,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.001953125,"right":0.63203125000000004,"top":0.662109375},"atlasBounds":{"left":5128.5,"bottom":3814.5,"right":5298.5,"top":3984.5}},{"unicode":8864,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.001953125,"right":0.63203125000000004,"top":0.662109375},"atlasBounds":{"left":5641.5,"bottom":3814.5,"right":5811.5,"top":3984.5}},{"unicode":8865,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.001953125,"right":0.63203125000000004,"top":0.662109375},"atlasBounds":{"left":5470.5,"bottom":3814.5,"right":5640.5,"top":3984.5}},{"unicode":8866,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.634765625},"atlasBounds":{"left":4615.5,"bottom":3813.5,"right":4785.5,"top":3984.5}},{"unicode":8867,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.634765625},"atlasBounds":{"left":4786.5,"bottom":3813.5,"right":4956.5,"top":3984.5}},{"unicode":8868,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.634765625},"atlasBounds":{"left":4102.5,"bottom":3813.5,"right":4272.5,"top":3984.5}},{"unicode":8869,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.634765625},"atlasBounds":{"left":3076.5,"bottom":3813.5,"right":3246.5,"top":3984.5}},{"unicode":8884,"advance":0.59999999999999998,"planeBounds":{"left":0.011718750000000016,"bottom":-0.033203125,"right":0.61328125,"top":0.744140625},"atlasBounds":{"left":4520.5,"bottom":4180.5,"right":4674.5,"top":4379.5}},{"unicode":8888,"advance":0.59999999999999998,"planeBounds":{"left":-0.011406249999999968,"bottom":0.201171875,"right":0.62140625000000005,"top":0.458984375},"atlasBounds":{"left":6987.5,"bottom":3272.5,"right":7149.5,"top":3338.5}},{"unicode":8891,"advance":0.59999999999999998,"planeBounds":{"left":0.011406250000000024,"bottom":-0.033203125,"right":0.62859375000000006,"top":0.728515625},"atlasBounds":{"left":6005.5,"bottom":4184.5,"right":6163.5,"top":4379.5}},{"unicode":8892,"advance":0.59999999999999998,"planeBounds":{"left":0.0067343749999999808,"bottom":0.044921875,"right":0.65126562499999996,"top":0.771484375},"atlasBounds":{"left":315.5,"bottom":3988.5,"right":480.5,"top":4174.5}},{"unicode":8893,"advance":0.59999999999999998,"planeBounds":{"left":0.085296875000000008,"bottom":0.044921875,"right":0.65170312500000005,"top":0.771484375},"atlasBounds":{"left":1418.5,"bottom":3988.5,"right":1563.5,"top":4174.5}},{"unicode":8898,"advance":0.59999999999999998,"planeBounds":{"left":0.026562499999999992,"bottom":-0.033203125,"right":0.57343750000000004,"top":0.763671875},"atlasBounds":{"left":150.5,"bottom":4380.5,"right":290.5,"top":4584.5}},{"unicode":8899,"advance":0.59999999999999998,"planeBounds":{"left":0.026562499999999992,"bottom":-0.033203125,"right":0.57343750000000004,"top":0.763671875},"atlasBounds":{"left":7580.5,"bottom":4585.5,"right":7720.5,"top":4789.5}},{"unicode":8900,"advance":0.59999999999999998,"planeBounds":{"left":0.12617187500000002,"bottom":0.158203125,"right":0.47382812499999999,"top":0.505859375},"atlasBounds":{"left":4995.5,"bottom":3249.5,"right":5084.5,"top":3338.5}},{"unicode":8902,"advance":0.59999999999999998,"planeBounds":{"left":0.14765625000000002,"bottom":0.185546875,"right":0.45234374999999999,"top":0.498046875},"atlasBounds":{"left":5324.5,"bottom":3258.5,"right":5402.5,"top":3338.5}},{"unicode":8904,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.017578125,"right":0.63203125000000004,"top":0.701171875},"atlasBounds":{"left":740.5,"bottom":3809.5,"right":910.5,"top":3984.5}},{"unicode":8905,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.017578125,"right":0.63203125000000004,"top":0.701171875},"atlasBounds":{"left":569.5,"bottom":3809.5,"right":739.5,"top":3984.5}},{"unicode":8906,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.017578125,"right":0.63203125000000004,"top":0.701171875},"atlasBounds":{"left":398.5,"bottom":3809.5,"right":568.5,"top":3984.5}},{"unicode":8910,"advance":0.59999999999999998,"planeBounds":{"left":0.027953125000000027,"bottom":-0.033203125,"right":0.64904687500000002,"top":0.583984375},"atlasBounds":{"left":3442.5,"bottom":3485.5,"right":3601.5,"top":3643.5}},{"unicode":8912,"advance":0.59999999999999998,"planeBounds":{"left":0.013473958333333343,"bottom":0.044921875,"right":0.65019270833333342,"top":0.673828125},"atlasBounds":{"left":4033.5,"bottom":3646.5,"right":4196.5,"top":3807.5}},{"unicode":8930,"advance":0.59999999999999998,"planeBounds":{"left":-0.032296875000000003,"bottom":-0.142578125,"right":0.651296875,"top":0.861328125},"atlasBounds":{"left":1025.5,"bottom":6374.5,"right":1200.5,"top":6631.5}},{"unicode":8942,"advance":0.59999999999999998,"planeBounds":{"left":0.20479687500000002,"bottom":-0.009765625,"right":0.39620312499999999,"top":0.677734375},"atlasBounds":{"left":4273.5,"bottom":3998.5,"right":4322.5,"top":4174.5}},{"unicode":8943,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.236328125,"right":0.64179687500000004,"top":0.431640625},"atlasBounds":{"left":171.5,"bottom":3178.5,"right":346.5,"top":3228.5}},{"unicode":8944,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":-0.009765625,"right":0.64179687500000004,"top":0.677734375},"atlasBounds":{"left":6252.5,"bottom":3998.5,"right":6427.5,"top":4174.5}},{"unicode":8945,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":-0.009765625,"right":0.64179687500000004,"top":0.677734375},"atlasBounds":{"left":8006.5,"bottom":3998.5,"right":8181.5,"top":4174.5}},{"unicode":8962,"advance":0.59999999999999998,"planeBounds":{"left":0.048046875000000031,"bottom":-0.033203125,"right":0.55195312500000004,"top":0.591796875},"atlasBounds":{"left":6404.5,"bottom":3647.5,"right":6533.5,"top":3807.5}},{"unicode":8963,"advance":0.59999999999999998,"planeBounds":{"left":0.016796874999999999,"bottom":0.435546875,"right":0.58320312500000004,"top":0.771484375},"atlasBounds":{"left":5178.5,"bottom":3252.5,"right":5323.5,"top":3338.5}},{"unicode":8964,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":-0.044921875,"right":0.62226562500000004,"top":0.376953125},"atlasBounds":{"left":498.5,"bottom":3230.5,"right":663.5,"top":3338.5}},{"unicode":8965,"advance":0.59999999999999998,"planeBounds":{"left":0.016796874999999999,"bottom":0.318359375,"right":0.58320312500000004,"top":0.763671875},"atlasBounds":{"left":7303.5,"bottom":3370.5,"right":7448.5,"top":3484.5}},{"unicode":8968,"advance":0.59999999999999998,"planeBounds":{"left":0.12084375,"bottom":-0.142578125,"right":0.56615625000000003,"top":0.861328125},"atlasBounds":{"left":1201.5,"bottom":6374.5,"right":1315.5,"top":6631.5}},{"unicode":8969,"advance":0.59999999999999998,"planeBounds":{"left":0.19965625000000001,"bottom":-0.142578125,"right":0.50434374999999998,"top":0.861328125},"atlasBounds":{"left":1316.5,"bottom":6374.5,"right":1394.5,"top":6631.5}},{"unicode":8970,"advance":0.59999999999999998,"planeBounds":{"left":0.12165625000000001,"bottom":-0.142578125,"right":0.42634375000000002,"top":0.861328125},"atlasBounds":{"left":1395.5,"bottom":6374.5,"right":1473.5,"top":6631.5}},{"unicode":8971,"advance":0.59999999999999998,"planeBounds":{"left":0.059843750000000001,"bottom":-0.142578125,"right":0.50515624999999997,"top":0.861328125},"atlasBounds":{"left":1474.5,"bottom":6374.5,"right":1588.5,"top":6631.5}},{"unicode":8984,"advance":0.59999999999999998,"planeBounds":{"left":-0.030078125000000015,"bottom":0.041015625,"right":0.63007812500000004,"top":0.697265625},"atlasBounds":{"left":6082.5,"bottom":3816.5,"right":6251.5,"top":3984.5}},{"unicode":8988,"advance":0.59999999999999998,"planeBounds":{"left":0.20642187500000003,"bottom":0.498046875,"right":0.61657812499999998,"top":0.861328125},"atlasBounds":{"left":4344.5,"bottom":3245.5,"right":4449.5,"top":3338.5}},{"unicode":8989,"advance":0.59999999999999998,"planeBounds":{"left":0.14799999999999999,"bottom":0.498046875,"right":0.52300000000000002,"top":0.861328125},"atlasBounds":{"left":4247.5,"bottom":3245.5,"right":4343.5,"top":3338.5}},{"unicode":8990,"advance":0.59999999999999998,"planeBounds":{"left":0.129,"bottom":-0.142578125,"right":0.504,"top":0.224609375},"atlasBounds":{"left":3493.5,"bottom":3244.5,"right":3589.5,"top":3338.5}},{"unicode":8991,"advance":0.59999999999999998,"planeBounds":{"left":0.041421875000000018,"bottom":-0.142578125,"right":0.451578125,"top":0.224609375},"atlasBounds":{"left":3590.5,"bottom":3244.5,"right":3695.5,"top":3338.5}},{"unicode":8996,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.396484375,"right":0.64179687500000004,"top":0.763671875},"atlasBounds":{"left":3877.5,"bottom":3244.5,"right":4052.5,"top":3338.5}},{"unicode":8997,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":-0.033203125,"right":0.62226562500000004,"top":0.763671875},"atlasBounds":{"left":4738.5,"bottom":4585.5,"right":4903.5,"top":4789.5}},{"unicode":8998,"advance":0.59999999999999998,"planeBounds":{"left":-0.086406249999999976,"bottom":0.033203125,"right":0.79640624999999998,"top":0.708984375},"atlasBounds":{"left":2525.5,"bottom":3811.5,"right":2751.5,"top":3984.5}},{"unicode":9000,"advance":0.59999999999999998,"planeBounds":{"left":-0.11992187499999998,"bottom":0.033203125,"right":0.71992187500000004,"top":0.708984375},"atlasBounds":{"left":1947.5,"bottom":3811.5,"right":2162.5,"top":3984.5}},{"unicode":9003,"advance":0.59999999999999998,"planeBounds":{"left":-0.19640624999999998,"bottom":0.033203125,"right":0.68640625,"top":0.708984375},"atlasBounds":{"left":2163.5,"bottom":3811.5,"right":2389.5,"top":3984.5}},{"unicode":9014,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.763671875},"atlasBounds":{"left":7888.5,"bottom":4585.5,"right":8058.5,"top":4789.5}},{"unicode":9015,"advance":0.59999999999999998,"planeBounds":{"left":0.13789062500000002,"bottom":-0.142578125,"right":0.46210937499999999,"top":0.861328125},"atlasBounds":{"left":1589.5,"bottom":6374.5,"right":1672.5,"top":6631.5}},{"unicode":9016,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":1673.5,"bottom":6374.5,"right":1843.5,"top":6631.5}},{"unicode":9017,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":1844.5,"bottom":6374.5,"right":2014.5,"top":6631.5}},{"unicode":9018,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":2015.5,"bottom":6374.5,"right":2185.5,"top":6631.5}},{"unicode":9019,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":2186.5,"bottom":6374.5,"right":2356.5,"top":6631.5}},{"unicode":9020,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":2357.5,"bottom":6374.5,"right":2527.5,"top":6631.5}},{"unicode":9021,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":-0.142578125,"right":0.65579687500000006,"top":0.861328125},"atlasBounds":{"left":2528.5,"bottom":6374.5,"right":2703.5,"top":6631.5}},{"unicode":9022,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":3041.5,"bottom":3998.5,"right":3216.5,"top":4174.5}},{"unicode":9023,"advance":0.59999999999999998,"planeBounds":{"left":-0.0053593749999999675,"bottom":-0.142578125,"right":0.63135937500000006,"top":0.861328125},"atlasBounds":{"left":2704.5,"bottom":6374.5,"right":2867.5,"top":6631.5}},{"unicode":9024,"advance":0.59999999999999998,"planeBounds":{"left":0.028515624999999992,"bottom":-0.142578125,"right":0.57148437500000004,"top":0.861328125},"atlasBounds":{"left":2868.5,"bottom":6374.5,"right":3007.5,"top":6631.5}},{"unicode":9025,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":3008.5,"bottom":6374.5,"right":3178.5,"top":6631.5}},{"unicode":9026,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":3179.5,"bottom":6374.5,"right":3349.5,"top":6631.5}},{"unicode":9027,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":3350.5,"bottom":6374.5,"right":3520.5,"top":6631.5}},{"unicode":9028,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":3521.5,"bottom":6374.5,"right":3691.5,"top":6631.5}},{"unicode":9029,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":-0.033203125,"right":0.63179687500000004,"top":0.763671875},"atlasBounds":{"left":6202.5,"bottom":4585.5,"right":6377.5,"top":4789.5}},{"unicode":9030,"advance":0.59999999999999998,"planeBounds":{"left":-0.031796875000000002,"bottom":-0.033203125,"right":0.65179687500000005,"top":0.763671875},"atlasBounds":{"left":5623.5,"bottom":4585.5,"right":5798.5,"top":4789.5}},{"unicode":9031,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":3692.5,"bottom":6374.5,"right":3862.5,"top":6631.5}},{"unicode":9032,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":3863.5,"bottom":6374.5,"right":4033.5,"top":6631.5}},{"unicode":9033,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":-0.142578125,"right":0.65579687500000006,"top":0.861328125},"atlasBounds":{"left":4034.5,"bottom":6374.5,"right":4209.5,"top":6631.5}},{"unicode":9034,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.181640625,"right":0.63203125000000004,"top":0.634765625},"atlasBounds":{"left":140.5,"bottom":4997.5,"right":310.5,"top":5206.5}},{"unicode":9035,"advance":0.59999999999999998,"planeBounds":{"left":0.028515624999999989,"bottom":-0.142578125,"right":0.57148437500000004,"top":0.861328125},"atlasBounds":{"left":4210.5,"bottom":6374.5,"right":4349.5,"top":6631.5}},{"unicode":9036,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":4350.5,"bottom":6374.5,"right":4520.5,"top":6631.5}},{"unicode":9037,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":4521.5,"bottom":6374.5,"right":4691.5,"top":6631.5}},{"unicode":9038,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.634765625},"atlasBounds":{"left":3418.5,"bottom":3813.5,"right":3588.5,"top":3984.5}},{"unicode":9039,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.771484375},"atlasBounds":{"left":6831.5,"bottom":5000.5,"right":7001.5,"top":5206.5}},{"unicode":9040,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":4692.5,"bottom":6374.5,"right":4862.5,"top":6631.5}},{"unicode":9041,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.779296875},"atlasBounds":{"left":841.5,"bottom":4998.5,"right":1011.5,"top":5206.5}},{"unicode":9042,"advance":0.59999999999999998,"planeBounds":{"left":0.028515624999999989,"bottom":-0.142578125,"right":0.57148437500000004,"top":0.861328125},"atlasBounds":{"left":4863.5,"bottom":6374.5,"right":5002.5,"top":6631.5}},{"unicode":9043,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":5003.5,"bottom":6374.5,"right":5173.5,"top":6631.5}},{"unicode":9044,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":5174.5,"bottom":6374.5,"right":5344.5,"top":6631.5}},{"unicode":9045,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.634765625},"atlasBounds":{"left":4957.5,"bottom":3813.5,"right":5127.5,"top":3984.5}},{"unicode":9046,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.044921875,"right":0.63203125000000004,"top":0.763671875},"atlasBounds":{"left":3629.5,"bottom":4999.5,"right":3799.5,"top":5206.5}},{"unicode":9047,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":5345.5,"bottom":6374.5,"right":5515.5,"top":6631.5}},{"unicode":9048,"advance":0.59999999999999998,"planeBounds":{"left":0.040828125000000014,"bottom":-0.181640625,"right":0.44317187499999999,"top":0.763671875},"atlasBounds":{"left":729.5,"bottom":5878.5,"right":832.5,"top":6120.5}},{"unicode":9049,"advance":0.59999999999999998,"planeBounds":{"left":0.022656249999999996,"bottom":-0.181640625,"right":0.57734375000000004,"top":0.763671875},"atlasBounds":{"left":586.5,"bottom":5878.5,"right":728.5,"top":6120.5}},{"unicode":9050,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":-0.181640625,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":1456.5,"bottom":5647.5,"right":1631.5,"top":5874.5}},{"unicode":9051,"advance":0.59999999999999998,"planeBounds":{"left":0.10859375,"bottom":-0.181640625,"right":0.49140624999999999,"top":0.482421875},"atlasBounds":{"left":5812.5,"bottom":3814.5,"right":5910.5,"top":3984.5}},{"unicode":9052,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":-0.181640625,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":642.5,"bottom":5647.5,"right":817.5,"top":5874.5}},{"unicode":9053,"advance":0.59999999999999998,"planeBounds":{"left":0.048046875000000031,"bottom":-0.033203125,"right":0.55195312500000004,"top":0.591796875},"atlasBounds":{"left":6697.5,"bottom":3647.5,"right":6826.5,"top":3807.5}},{"unicode":9054,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":5516.5,"bottom":6374.5,"right":5686.5,"top":6631.5}},{"unicode":9055,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":3921.5,"bottom":3998.5,"right":4096.5,"top":4174.5}},{"unicode":9056,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":5687.5,"bottom":6374.5,"right":5857.5,"top":6631.5}},{"unicode":9057,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.802734375},"atlasBounds":{"left":692.5,"bottom":5210.5,"right":862.5,"top":5424.5}},{"unicode":9058,"advance":0.59999999999999998,"planeBounds":{"left":0.028515624999999989,"bottom":-0.033203125,"right":0.57148437500000004,"top":0.970703125},"atlasBounds":{"left":6029.5,"bottom":6374.5,"right":6168.5,"top":6631.5}},{"unicode":9059,"advance":0.59999999999999998,"planeBounds":{"left":0.10273437500000002,"bottom":0.185546875,"right":0.49726562499999999,"top":0.712890625},"atlasBounds":{"left":2435.5,"bottom":3349.5,"right":2536.5,"top":3484.5}},{"unicode":9060,"advance":0.59999999999999998,"planeBounds":{"left":0.10273437500000002,"bottom":0.177734375,"right":0.49726562499999999,"top":0.712890625},"atlasBounds":{"left":1997.5,"bottom":3347.5,"right":2098.5,"top":3484.5}},{"unicode":9061,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.970703125},"atlasBounds":{"left":261.5,"bottom":5876.5,"right":436.5,"top":6120.5}},{"unicode":9062,"advance":0.59999999999999998,"planeBounds":{"left":0.048046875000000031,"bottom":-0.142578125,"right":0.55195312500000004,"top":0.861328125},"atlasBounds":{"left":6169.5,"bottom":6374.5,"right":6298.5,"top":6631.5}},{"unicode":9063,"advance":0.59999999999999998,"planeBounds":{"left":-0.0027343749999999825,"bottom":-0.142578125,"right":0.60273437500000004,"top":0.861328125},"atlasBounds":{"left":6299.5,"bottom":6374.5,"right":6454.5,"top":6631.5}},{"unicode":9064,"advance":0.59999999999999998,"planeBounds":{"left":0.045874999999999985,"bottom":0.224609375,"right":0.577125,"top":0.712890625},"atlasBounds":{"left":6201.5,"bottom":3359.5,"right":6337.5,"top":3484.5}},{"unicode":9065,"advance":0.59999999999999998,"planeBounds":{"left":0.024210069444444489,"bottom":0.041015625,"right":0.56717881944444448,"top":0.802734375},"atlasBounds":{"left":6164.5,"bottom":4184.5,"right":6303.5,"top":4379.5}},{"unicode":9066,"advance":0.59999999999999998,"planeBounds":{"left":0.141234375,"bottom":0.005859375,"right":0.53576562500000002,"top":0.583984375},"atlasBounds":{"left":8090.5,"bottom":4436.5,"right":8191.5,"top":4584.5}},{"unicode":9067,"advance":0.59999999999999998,"planeBounds":{"left":0.028515624999999989,"bottom":-0.033203125,"right":0.57148437500000004,"top":0.978515625},"atlasBounds":{"left":1800.5,"bottom":6633.5,"right":1939.5,"top":6892.5}},{"unicode":9068,"advance":0.59999999999999998,"planeBounds":{"left":0.048137295081967242,"bottom":-0.044921875,"right":0.57938729508196729,"top":0.771484375},"atlasBounds":{"left":4154.5,"bottom":5215.5,"right":4290.5,"top":5424.5}},{"unicode":9069,"advance":0.59999999999999998,"planeBounds":{"left":0.044140625000000031,"bottom":-0.142578125,"right":0.55585937500000004,"top":0.861328125},"atlasBounds":{"left":6455.5,"bottom":6374.5,"right":6586.5,"top":6631.5}},{"unicode":9070,"advance":0.59999999999999998,"planeBounds":{"left":0.040771306818181828,"bottom":-0.181640625,"right":0.47436505681818181,"top":0.771484375},"atlasBounds":{"left":149.5,"bottom":5876.5,"right":260.5,"top":6120.5}},{"unicode":9071,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":6587.5,"bottom":6374.5,"right":6757.5,"top":6631.5}},{"unicode":9072,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":6758.5,"bottom":6374.5,"right":6928.5,"top":6631.5}},{"unicode":9073,"advance":0.59999999999999998,"planeBounds":{"left":0.081281249999999985,"bottom":0.017578125,"right":0.60471874999999997,"top":0.814453125},"atlasBounds":{"left":562.5,"bottom":4380.5,"right":696.5,"top":4584.5}},{"unicode":9074,"advance":0.59999999999999998,"planeBounds":{"left":0.0013437500000000023,"bottom":0.017578125,"right":0.57165624999999998,"top":0.814453125},"atlasBounds":{"left":6645.5,"bottom":4585.5,"right":6791.5,"top":4789.5}},{"unicode":9075,"advance":0.59999999999999998,"planeBounds":{"left":0.06521875000000002,"bottom":-0.033203125,"right":0.54178124999999999,"top":0.583984375},"atlasBounds":{"left":4183.5,"bottom":3485.5,"right":4305.5,"top":3643.5}},{"unicode":9076,"advance":0.59999999999999998,"planeBounds":{"left":-0.0012296080508474581,"bottom":-0.212890625,"right":0.54955164194915251,"top":0.591796875},"atlasBounds":{"left":5779.5,"bottom":5000.5,"right":5920.5,"top":5206.5}},{"unicode":9077,"advance":0.59999999999999998,"planeBounds":{"left":0.00780563186813192,"bottom":-0.044921875,"right":0.58593063186813199,"top":0.591796875},"atlasBounds":{"left":2122.5,"bottom":3644.5,"right":2270.5,"top":3807.5}},{"unicode":9078,"advance":0.59999999999999998,"planeBounds":{"left":-0.012406249999999969,"bottom":-0.181640625,"right":0.62040625000000005,"top":0.591796875},"atlasBounds":{"left":4978.5,"bottom":4181.5,"right":5140.5,"top":4379.5}},{"unicode":9079,"advance":0.59999999999999998,"planeBounds":{"left":0.012703124999999999,"bottom":-0.181640625,"right":0.57129687500000004,"top":0.591796875},"atlasBounds":{"left":5301.5,"bottom":4181.5,"right":5444.5,"top":4379.5}},{"unicode":9080,"advance":0.59999999999999998,"planeBounds":{"left":-0.033109374999999996,"bottom":-0.181640625,"right":0.541109375,"top":0.583984375},"atlasBounds":{"left":5857.5,"bottom":4183.5,"right":6004.5,"top":4379.5}},{"unicode":9081,"advance":0.59999999999999998,"planeBounds":{"left":-0.034832589285714248,"bottom":-0.181640625,"right":0.5862611607142858,"top":0.591796875},"atlasBounds":{"left":5141.5,"bottom":4181.5,"right":5300.5,"top":4379.5}},{"unicode":9082,"advance":0.59999999999999998,"planeBounds":{"left":0.026685000000000007,"bottom":-0.044921875,"right":0.62043500000000007,"top":0.591796875},"atlasBounds":{"left":1253.5,"bottom":3644.5,"right":1405.5,"top":3807.5}},{"unicode":9097,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":2689.5,"bottom":3998.5,"right":2864.5,"top":4174.5}},{"unicode":9098,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":0.5,"bottom":3808.5,"right":175.5,"top":3984.5}},{"unicode":9099,"advance":0.59999999999999998,"planeBounds":{"left":-0.011406249999999968,"bottom":0.052734375,"right":0.62140625000000005,"top":0.681640625},"atlasBounds":{"left":4364.5,"bottom":3646.5,"right":4526.5,"top":3807.5}},{"unicode":9109,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.142578125,"right":0.63203125000000004,"top":0.861328125},"atlasBounds":{"left":5858.5,"bottom":6374.5,"right":6028.5,"top":6631.5}},{"unicode":9115,"advance":0.59999999999999998,"planeBounds":{"left":0.23567187500000003,"bottom":-0.333984375,"right":0.58332812499999998,"top":0.873046875},"atlasBounds":{"left":2064.5,"bottom":7475.5,"right":2153.5,"top":7784.5}},{"unicode":9116,"advance":0.59999999999999998,"planeBounds":{"left":0.23554687500000002,"bottom":-0.333984375,"right":0.36445312499999999,"top":1.052734375},"atlasBounds":{"left":6507.5,"bottom":7836.5,"right":6540.5,"top":8191.5}},{"unicode":9117,"advance":0.59999999999999998,"planeBounds":{"left":0.23567187500000003,"bottom":-0.154296875,"right":0.58332812499999998,"top":1.052734375},"atlasBounds":{"left":2154.5,"bottom":7475.5,"right":2243.5,"top":7784.5}},{"unicode":9118,"advance":0.59999999999999998,"planeBounds":{"left":0.016671875000000013,"bottom":-0.333984375,"right":0.364328125,"top":0.873046875},"atlasBounds":{"left":2244.5,"bottom":7475.5,"right":2333.5,"top":7784.5}},{"unicode":9119,"advance":0.59999999999999998,"planeBounds":{"left":0.23554687500000002,"bottom":-0.333984375,"right":0.36445312499999999,"top":1.052734375},"atlasBounds":{"left":6541.5,"bottom":7836.5,"right":6574.5,"top":8191.5}},{"unicode":9120,"advance":0.59999999999999998,"planeBounds":{"left":0.016671875000000013,"bottom":-0.154296875,"right":0.364328125,"top":1.052734375},"atlasBounds":{"left":2334.5,"bottom":7475.5,"right":2423.5,"top":7784.5}},{"unicode":9121,"advance":0.59999999999999998,"planeBounds":{"left":0.23506250000000001,"bottom":-0.333984375,"right":0.53193750000000006,"top":0.861328125},"atlasBounds":{"left":2424.5,"bottom":7478.5,"right":2500.5,"top":7784.5}},{"unicode":9122,"advance":0.59999999999999998,"planeBounds":{"left":0.23554687500000002,"bottom":-0.333984375,"right":0.36445312499999999,"top":1.052734375},"atlasBounds":{"left":6575.5,"bottom":7836.5,"right":6608.5,"top":8191.5}},{"unicode":9123,"advance":0.59999999999999998,"planeBounds":{"left":0.23506250000000001,"bottom":-0.142578125,"right":0.53193750000000006,"top":1.052734375},"atlasBounds":{"left":2501.5,"bottom":7478.5,"right":2577.5,"top":7784.5}},{"unicode":9124,"advance":0.59999999999999998,"planeBounds":{"left":0.068062499999999984,"bottom":-0.333984375,"right":0.36493750000000003,"top":0.861328125},"atlasBounds":{"left":2578.5,"bottom":7478.5,"right":2654.5,"top":7784.5}},{"unicode":9125,"advance":0.59999999999999998,"planeBounds":{"left":0.23554687500000002,"bottom":-0.333984375,"right":0.36445312499999999,"top":1.052734375},"atlasBounds":{"left":6609.5,"bottom":7836.5,"right":6642.5,"top":8191.5}},{"unicode":9126,"advance":0.59999999999999998,"planeBounds":{"left":0.068062499999999984,"bottom":-0.142578125,"right":0.36493750000000003,"top":1.052734375},"atlasBounds":{"left":2655.5,"bottom":7478.5,"right":2731.5,"top":7784.5}},{"unicode":9127,"advance":0.59999999999999998,"planeBounds":{"left":0.233875,"bottom":-0.333984375,"right":0.51512500000000006,"top":0.861328125},"atlasBounds":{"left":2732.5,"bottom":7478.5,"right":2804.5,"top":7784.5}},{"unicode":9128,"advance":0.59999999999999998,"planeBounds":{"left":0.02507812500000001,"bottom":-0.333984375,"right":0.36492187500000001,"top":1.052734375},"atlasBounds":{"left":6643.5,"bottom":7836.5,"right":6730.5,"top":8191.5}},{"unicode":9129,"advance":0.59999999999999998,"planeBounds":{"left":0.233875,"bottom":-0.142578125,"right":0.51512500000000006,"top":1.052734375},"atlasBounds":{"left":2805.5,"bottom":7478.5,"right":2877.5,"top":7784.5}},{"unicode":9130,"advance":0.59999999999999998,"planeBounds":{"left":0.23554687500000002,"bottom":-0.333984375,"right":0.36445312499999999,"top":1.052734375},"atlasBounds":{"left":6731.5,"bottom":7836.5,"right":6764.5,"top":8191.5}},{"unicode":9131,"advance":0.59999999999999998,"planeBounds":{"left":0.08487500000000002,"bottom":-0.333984375,"right":0.36612500000000003,"top":0.861328125},"atlasBounds":{"left":2878.5,"bottom":7478.5,"right":2950.5,"top":7784.5}},{"unicode":9132,"advance":0.59999999999999998,"planeBounds":{"left":0.235078125,"bottom":-0.333984375,"right":0.57492187500000003,"top":1.052734375},"atlasBounds":{"left":6765.5,"bottom":7836.5,"right":6852.5,"top":8191.5}},{"unicode":9133,"advance":0.59999999999999998,"planeBounds":{"left":0.08487500000000002,"bottom":-0.142578125,"right":0.36612500000000003,"top":1.052734375},"atlasBounds":{"left":2951.5,"bottom":7478.5,"right":3023.5,"top":7784.5}},{"unicode":9166,"advance":0.59999999999999998,"planeBounds":{"left":-0.052109374999999993,"bottom":0.017578125,"right":0.64710937499999999,"top":0.763671875},"atlasBounds":{"left":7017.5,"bottom":4188.5,"right":7196.5,"top":4379.5}},{"unicode":9211,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.044921875,"right":0.65156250000000004,"top":0.759765625},"atlasBounds":{"left":5598.5,"bottom":5000.5,"right":5778.5,"top":5206.5}},{"unicode":9212,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.044921875,"right":0.65156250000000004,"top":0.654296875},"atlasBounds":{"left":2200.5,"bottom":3995.5,"right":2380.5,"top":4174.5}},{"unicode":9213,"advance":0.59999999999999998,"planeBounds":{"left":0.21210937500000002,"bottom":-0.037109375,"right":0.38789062499999999,"top":0.646484375},"atlasBounds":{"left":352.5,"bottom":3809.5,"right":397.5,"top":3984.5}},{"unicode":9214,"advance":0.59999999999999998,"planeBounds":{"left":-0.025937500000000009,"bottom":-0.041015625,"right":0.64593750000000005,"top":0.630859375},"atlasBounds":{"left":2752.5,"bottom":3812.5,"right":2924.5,"top":3984.5}},{"unicode":9216,"advance":0.59999999999999998,"planeBounds":{"left":0.0089062500000000235,"bottom":-0.044921875,"right":0.62609375,"top":0.763671875},"atlasBounds":{"left":1887.5,"bottom":4999.5,"right":2045.5,"top":5206.5}},{"unicode":9217,"advance":0.59999999999999998,"planeBounds":{"left":0.016694078947368407,"bottom":-0.033203125,"right":0.62606907894736841,"top":0.767578125},"atlasBounds":{"left":2036.5,"bottom":4791.5,"right":2192.5,"top":4996.5}},{"unicode":9218,"advance":0.59999999999999998,"planeBounds":{"left":0.018334703947368414,"bottom":-0.033203125,"right":0.63942845394736847,"top":0.767578125},"atlasBounds":{"left":2350.5,"bottom":4791.5,"right":2509.5,"top":4996.5}},{"unicode":9219,"advance":0.59999999999999998,"planeBounds":{"left":0.022406250000000027,"bottom":-0.033203125,"right":0.63959374999999996,"top":0.763671875},"atlasBounds":{"left":3717.5,"bottom":4792.5,"right":3875.5,"top":4996.5}},{"unicode":9220,"advance":0.59999999999999998,"planeBounds":{"left":0.022093750000000034,"bottom":-0.033203125,"right":0.65490625000000002,"top":0.763671875},"atlasBounds":{"left":7670.5,"bottom":4792.5,"right":7832.5,"top":4996.5}},{"unicode":9221,"advance":0.59999999999999998,"planeBounds":{"left":0.021218750000000015,"bottom":-0.091796875,"right":0.62278125000000006,"top":0.763671875},"atlasBounds":{"left":4067.5,"bottom":5427.5,"right":4221.5,"top":5646.5}},{"unicode":9222,"advance":0.59999999999999998,"planeBounds":{"left":-0.018718750000000017,"bottom":-0.033203125,"right":0.62971874999999999,"top":0.763671875},"atlasBounds":{"left":7721.5,"bottom":4585.5,"right":7887.5,"top":4789.5}},{"unicode":9223,"advance":0.59999999999999998,"planeBounds":{"left":0.012484375000000009,"bottom":-0.033203125,"right":0.59451562499999999,"top":0.763671875},"atlasBounds":{"left":0.5,"bottom":4380.5,"right":149.5,"top":4584.5}},{"unicode":9224,"advance":0.59999999999999998,"planeBounds":{"left":0.012185763888888939,"bottom":-0.037109375,"right":0.60984201388888892,"top":0.763671875},"atlasBounds":{"left":951.5,"bottom":4791.5,"right":1104.5,"top":4996.5}},{"unicode":9225,"advance":0.59999999999999998,"planeBounds":{"left":0.010234374999999981,"bottom":-0.033203125,"right":0.65476562500000002,"top":0.763671875},"atlasBounds":{"left":2539.5,"bottom":4380.5,"right":2704.5,"top":4584.5}},{"unicode":9226,"advance":0.59999999999999998,"planeBounds":{"left":0.04603125000000001,"bottom":-0.033203125,"right":0.63196874999999997,"top":0.763671875},"atlasBounds":{"left":3005.5,"bottom":4380.5,"right":3155.5,"top":4584.5}},{"unicode":9227,"advance":0.59999999999999998,"planeBounds":{"left":0.033953125000000028,"bottom":-0.033203125,"right":0.65504687500000003,"top":0.763671875},"atlasBounds":{"left":1380.5,"bottom":4380.5,"right":1539.5,"top":4584.5}},{"unicode":9228,"advance":0.59999999999999998,"planeBounds":{"left":0.032671875000000017,"bottom":-0.033203125,"right":0.63032812500000002,"top":0.763671875},"atlasBounds":{"left":981.5,"bottom":4380.5,"right":1134.5,"top":4584.5}},{"unicode":9229,"advance":0.59999999999999998,"planeBounds":{"left":0.022625000000000013,"bottom":-0.033203125,"right":0.61637500000000001,"top":0.767578125},"atlasBounds":{"left":8039.5,"bottom":6168.5,"right":8191.5,"top":6373.5}},{"unicode":9230,"advance":0.59999999999999998,"planeBounds":{"left":0.017147203947368402,"bottom":-0.037109375,"right":0.62261595394736846,"top":0.767578125},"atlasBounds":{"left":5442.5,"bottom":5000.5,"right":5597.5,"top":5206.5}},{"unicode":9231,"advance":0.59999999999999998,"planeBounds":{"left":0.018194078947368408,"bottom":-0.033203125,"right":0.62756907894736846,"top":0.767578125},"atlasBounds":{"left":320.5,"bottom":4791.5,"right":476.5,"top":4996.5}},{"unicode":9232,"advance":0.59999999999999998,"planeBounds":{"left":0.015265625000000019,"bottom":-0.033203125,"right":0.62073437500000006,"top":0.763671875},"atlasBounds":{"left":4904.5,"bottom":4585.5,"right":5059.5,"top":4789.5}},{"unicode":9233,"advance":0.59999999999999998,"planeBounds":{"left":0.014843750000000005,"bottom":-0.033203125,"right":0.58515625000000004,"top":0.763671875},"atlasBounds":{"left":7358.5,"bottom":4380.5,"right":7504.5,"top":4584.5}},{"unicode":9234,"advance":0.59999999999999998,"planeBounds":{"left":0.014484375000000008,"bottom":-0.033203125,"right":0.59651562499999999,"top":0.763671875},"atlasBounds":{"left":7978.5,"bottom":4792.5,"right":8127.5,"top":4996.5}},{"unicode":9235,"advance":0.59999999999999998,"planeBounds":{"left":0.014249999999999999,"bottom":-0.037109375,"right":0.57674999999999998,"top":0.763671875},"atlasBounds":{"left":663.5,"bottom":4791.5,"right":807.5,"top":4996.5}},{"unicode":9236,"advance":0.59999999999999998,"planeBounds":{"left":0.01525,"bottom":-0.033203125,"right":0.57774999999999999,"top":0.763671875},"atlasBounds":{"left":7833.5,"bottom":4792.5,"right":7977.5,"top":4996.5}},{"unicode":9237,"advance":0.59999999999999998,"planeBounds":{"left":0.0094531250000000257,"bottom":-0.033203125,"right":0.63054687500000006,"top":0.763671875},"atlasBounds":{"left":4396.5,"bottom":4792.5,"right":4555.5,"top":4996.5}},{"unicode":9238,"advance":0.59999999999999998,"planeBounds":{"left":0.016694078947368407,"bottom":-0.033203125,"right":0.62606907894736841,"top":0.767578125},"atlasBounds":{"left":2193.5,"bottom":4791.5,"right":2349.5,"top":4996.5}},{"unicode":9239,"advance":0.59999999999999998,"planeBounds":{"left":0.022111895161290314,"bottom":-0.033203125,"right":0.60804939516129042,"top":0.763671875},"atlasBounds":{"left":392.5,"bottom":4585.5,"right":542.5,"top":4789.5}},{"unicode":9240,"advance":0.59999999999999998,"planeBounds":{"left":0.022718750000000017,"bottom":-0.033203125,"right":0.62428125000000001,"top":0.767578125},"atlasBounds":{"left":3121.5,"bottom":4791.5,"right":3275.5,"top":4996.5}},{"unicode":9241,"advance":0.59999999999999998,"planeBounds":{"left":0.022406250000000027,"bottom":-0.033203125,"right":0.63959374999999996,"top":0.763671875},"atlasBounds":{"left":5425.5,"bottom":4792.5,"right":5583.5,"top":4996.5}},{"unicode":9242,"advance":0.59999999999999998,"planeBounds":{"left":0.018040349108658704,"bottom":-0.033203125,"right":0.6078840991086587,"top":0.767578125},"atlasBounds":{"left":2969.5,"bottom":4791.5,"right":3120.5,"top":4996.5}},{"unicode":9243,"advance":0.59999999999999998,"planeBounds":{"left":0.022718750000000017,"bottom":-0.037109375,"right":0.62428125000000001,"top":0.763671875},"atlasBounds":{"left":2814.5,"bottom":4791.5,"right":2968.5,"top":4996.5}},{"unicode":9244,"advance":0.59999999999999998,"planeBounds":{"left":0.031951388888888932,"bottom":-0.037109375,"right":0.61007638888888893,"top":0.763671875},"atlasBounds":{"left":2665.5,"bottom":4791.5,"right":2813.5,"top":4996.5}},{"unicode":9245,"advance":0.59999999999999998,"planeBounds":{"left":0.019592013888888937,"bottom":-0.037109375,"right":0.60943576388888898,"top":0.767578125},"atlasBounds":{"left":7481.5,"bottom":5000.5,"right":7632.5,"top":5206.5}},{"unicode":9246,"advance":0.59999999999999998,"planeBounds":{"left":0.0092326388888889412,"bottom":-0.037109375,"right":0.61079513888888892,"top":0.763671875},"atlasBounds":{"left":2510.5,"bottom":4791.5,"right":2664.5,"top":4996.5}},{"unicode":9247,"advance":0.59999999999999998,"planeBounds":{"left":0.028683046497584592,"bottom":-0.037109375,"right":0.61071429649758457,"top":0.763671875},"atlasBounds":{"left":1540.5,"bottom":4791.5,"right":1689.5,"top":4996.5}},{"unicode":9248,"advance":0.59999999999999998,"planeBounds":{"left":0.016582967836257345,"bottom":-0.033203125,"right":0.62595796783625735,"top":0.767578125},"atlasBounds":{"left":1383.5,"bottom":4791.5,"right":1539.5,"top":4996.5}},{"unicode":9249,"advance":0.59999999999999998,"planeBounds":{"left":0.013984375000000007,"bottom":-0.033203125,"right":0.59601562500000005,"top":0.763671875},"atlasBounds":{"left":3055.5,"bottom":4175.5,"right":3204.5,"top":4379.5}},{"unicode":9251,"advance":0.59999999999999998,"planeBounds":{"left":-0.059281249999999987,"bottom":-0.212890625,"right":0.54228125000000005,"top":0.033203125},"atlasBounds":{"left":7969.5,"bottom":3275.5,"right":8123.5,"top":3338.5}},{"unicode":9252,"advance":0.59999999999999998,"planeBounds":{"left":0.0095312500000000085,"bottom":-0.033203125,"right":0.59546874999999999,"top":0.763671875},"atlasBounds":{"left":3329.5,"bottom":4380.5,"right":3479.5,"top":4584.5}},{"unicode":9472,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.287109375,"right":0.65156250000000004,"top":0.455078125},"atlasBounds":{"left":347.5,"bottom":3185.5,"right":527.5,"top":3228.5}},{"unicode":9473,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.236328125,"right":0.65156250000000004,"top":0.501953125},"atlasBounds":{"left":5813.5,"bottom":3270.5,"right":5993.5,"top":3338.5}},{"unicode":9474,"advance":0.59999999999999998,"planeBounds":{"left":0.21796875000000002,"bottom":-0.431640625,"right":0.38203124999999999,"top":1.154296875},"atlasBounds":{"left":0.5,"bottom":7785.5,"right":42.5,"top":8191.5}},{"unicode":9475,"advance":0.59999999999999998,"planeBounds":{"left":0.16718750000000002,"bottom":-0.431640625,"right":0.43281249999999999,"top":1.154296875},"atlasBounds":{"left":43.5,"bottom":7785.5,"right":111.5,"top":8191.5}},{"unicode":9476,"advance":0.59999999999999998,"planeBounds":{"left":0.0050781250000000114,"bottom":0.287109375,"right":0.59492187500000004,"top":0.455078125},"atlasBounds":{"left":0.5,"bottom":3125.5,"right":151.5,"top":3168.5}},{"unicode":9477,"advance":0.59999999999999998,"planeBounds":{"left":0.0050781250000000114,"bottom":0.236328125,"right":0.59492187500000004,"top":0.501953125},"atlasBounds":{"left":6713.5,"bottom":3270.5,"right":6864.5,"top":3338.5}},{"unicode":9478,"advance":0.59999999999999998,"planeBounds":{"left":0.21796875000000002,"bottom":-0.251953125,"right":0.38203124999999999,"top":0.970703125},"atlasBounds":{"left":1781.5,"bottom":7471.5,"right":1823.5,"top":7784.5}},{"unicode":9479,"advance":0.59999999999999998,"planeBounds":{"left":0.16718750000000002,"bottom":-0.251953125,"right":0.43281249999999999,"top":0.970703125},"atlasBounds":{"left":1824.5,"bottom":7471.5,"right":1892.5,"top":7784.5}},{"unicode":9480,"advance":0.59999999999999998,"planeBounds":{"left":-0.006640624999999979,"bottom":0.287109375,"right":0.60664062500000004,"top":0.455078125},"atlasBounds":{"left":8033.5,"bottom":6383.5,"right":8190.5,"top":6426.5}},{"unicode":9481,"advance":0.59999999999999998,"planeBounds":{"left":-0.006640624999999979,"bottom":0.236328125,"right":0.60664062500000004,"top":0.501953125},"atlasBounds":{"left":5655.5,"bottom":3270.5,"right":5812.5,"top":3338.5}},{"unicode":9482,"advance":0.59999999999999998,"planeBounds":{"left":0.21796875000000002,"bottom":-0.228515625,"right":0.38203124999999999,"top":0.966796875},"atlasBounds":{"left":3024.5,"bottom":7478.5,"right":3066.5,"top":7784.5}},{"unicode":9483,"advance":0.59999999999999998,"planeBounds":{"left":0.16718750000000002,"bottom":-0.228515625,"right":0.43281249999999999,"top":0.966796875},"atlasBounds":{"left":3067.5,"bottom":7478.5,"right":3135.5,"top":7784.5}},{"unicode":9484,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":-0.431640625,"right":0.65179687500000005,"top":0.455078125},"atlasBounds":{"left":237.5,"bottom":5647.5,"right":348.5,"top":5874.5}},{"unicode":9485,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":-0.431640625,"right":0.65179687500000005,"top":0.501953125},"atlasBounds":{"left":1987.5,"bottom":5881.5,"right":2098.5,"top":6120.5}},{"unicode":9486,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":-0.431640625,"right":0.65218750000000003,"top":0.455078125},"atlasBounds":{"left":112.5,"bottom":5647.5,"right":236.5,"top":5874.5}},{"unicode":9487,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":-0.431640625,"right":0.65218750000000003,"top":0.501953125},"atlasBounds":{"left":1862.5,"bottom":5881.5,"right":1986.5,"top":6120.5}},{"unicode":9488,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":-0.431640625,"right":0.38179687500000004,"top":0.455078125},"atlasBounds":{"left":0.5,"bottom":5647.5,"right":111.5,"top":5874.5}},{"unicode":9489,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":-0.431640625,"right":0.38179687500000004,"top":0.501953125},"atlasBounds":{"left":1750.5,"bottom":5881.5,"right":1861.5,"top":6120.5}},{"unicode":9490,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":-0.431640625,"right":0.4321875,"top":0.455078125},"atlasBounds":{"left":2144.5,"bottom":5647.5,"right":2268.5,"top":5874.5}},{"unicode":9491,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":-0.431640625,"right":0.4321875,"top":0.501953125},"atlasBounds":{"left":3185.5,"bottom":5881.5,"right":3309.5,"top":6120.5}},{"unicode":9492,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":0.287109375,"right":0.65179687500000005,"top":1.154296875},"atlasBounds":{"left":4000.5,"bottom":5652.5,"right":4111.5,"top":5874.5}},{"unicode":9493,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":0.236328125,"right":0.65179687500000005,"top":1.154296875},"atlasBounds":{"left":5125.5,"bottom":5885.5,"right":5236.5,"top":6120.5}},{"unicode":9494,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":0.287109375,"right":0.65218750000000003,"top":1.154296875},"atlasBounds":{"left":3832.5,"bottom":5652.5,"right":3956.5,"top":5874.5}},{"unicode":9495,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":0.236328125,"right":0.65218750000000003,"top":1.154296875},"atlasBounds":{"left":3620.5,"bottom":5885.5,"right":3744.5,"top":6120.5}},{"unicode":9496,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":0.287109375,"right":0.38179687500000004,"top":1.154296875},"atlasBounds":{"left":4375.5,"bottom":5652.5,"right":4486.5,"top":5874.5}},{"unicode":9497,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":0.236328125,"right":0.38179687500000004,"top":1.154296875},"atlasBounds":{"left":5237.5,"bottom":5885.5,"right":5348.5,"top":6120.5}},{"unicode":9498,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":0.287109375,"right":0.4321875,"top":1.154296875},"atlasBounds":{"left":4112.5,"bottom":5652.5,"right":4236.5,"top":5874.5}},{"unicode":9499,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":0.236328125,"right":0.4321875,"top":1.154296875},"atlasBounds":{"left":3926.5,"bottom":5885.5,"right":4050.5,"top":6120.5}},{"unicode":9500,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":-0.431640625,"right":0.65179687500000005,"top":1.154296875},"atlasBounds":{"left":112.5,"bottom":7785.5,"right":223.5,"top":8191.5}},{"unicode":9501,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":-0.431640625,"right":0.65179687500000005,"top":1.154296875},"atlasBounds":{"left":224.5,"bottom":7785.5,"right":335.5,"top":8191.5}},{"unicode":9502,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":-0.431640625,"right":0.65218750000000003,"top":1.154296875},"atlasBounds":{"left":336.5,"bottom":7785.5,"right":460.5,"top":8191.5}},{"unicode":9503,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":-0.431640625,"right":0.65218750000000003,"top":1.154296875},"atlasBounds":{"left":461.5,"bottom":7785.5,"right":585.5,"top":8191.5}},{"unicode":9504,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":-0.431640625,"right":0.65218750000000003,"top":1.154296875},"atlasBounds":{"left":586.5,"bottom":7785.5,"right":710.5,"top":8191.5}},{"unicode":9505,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":-0.431640625,"right":0.65218750000000003,"top":1.154296875},"atlasBounds":{"left":711.5,"bottom":7785.5,"right":835.5,"top":8191.5}},{"unicode":9506,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":-0.431640625,"right":0.65218750000000003,"top":1.154296875},"atlasBounds":{"left":836.5,"bottom":7785.5,"right":960.5,"top":8191.5}},{"unicode":9507,"advance":0.59999999999999998,"planeBounds":{"left":0.1678125,"bottom":-0.431640625,"right":0.65218750000000003,"top":1.154296875},"atlasBounds":{"left":961.5,"bottom":7785.5,"right":1085.5,"top":8191.5}},{"unicode":9508,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":-0.431640625,"right":0.38179687500000004,"top":1.154296875},"atlasBounds":{"left":1086.5,"bottom":7785.5,"right":1197.5,"top":8191.5}},{"unicode":9509,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":-0.431640625,"right":0.38179687500000004,"top":1.154296875},"atlasBounds":{"left":1198.5,"bottom":7785.5,"right":1309.5,"top":8191.5}},{"unicode":9510,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":-0.431640625,"right":0.4321875,"top":1.154296875},"atlasBounds":{"left":1310.5,"bottom":7785.5,"right":1434.5,"top":8191.5}},{"unicode":9511,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":-0.431640625,"right":0.4321875,"top":1.052734375},"atlasBounds":{"left":6382.5,"bottom":7811.5,"right":6506.5,"top":8191.5}},{"unicode":9512,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":-0.431640625,"right":0.4321875,"top":1.154296875},"atlasBounds":{"left":1435.5,"bottom":7785.5,"right":1559.5,"top":8191.5}},{"unicode":9513,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":-0.431640625,"right":0.4321875,"top":1.154296875},"atlasBounds":{"left":1560.5,"bottom":7785.5,"right":1684.5,"top":8191.5}},{"unicode":9514,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":-0.431640625,"right":0.4321875,"top":1.154296875},"atlasBounds":{"left":1685.5,"bottom":7785.5,"right":1809.5,"top":8191.5}},{"unicode":9515,"advance":0.59999999999999998,"planeBounds":{"left":-0.052187500000000012,"bottom":-0.431640625,"right":0.4321875,"top":1.154296875},"atlasBounds":{"left":1810.5,"bottom":7785.5,"right":1934.5,"top":8191.5}},{"unicode":9516,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.455078125},"atlasBounds":{"left":1827.5,"bottom":5647.5,"right":2007.5,"top":5874.5}},{"unicode":9517,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.501953125},"atlasBounds":{"left":3004.5,"bottom":5881.5,"right":3184.5,"top":6120.5}},{"unicode":9518,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.501953125},"atlasBounds":{"left":2823.5,"bottom":5881.5,"right":3003.5,"top":6120.5}},{"unicode":9519,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.501953125},"atlasBounds":{"left":2642.5,"bottom":5881.5,"right":2822.5,"top":6120.5}},{"unicode":9520,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.455078125},"atlasBounds":{"left":1275.5,"bottom":5647.5,"right":1455.5,"top":5874.5}},{"unicode":9521,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.501953125},"atlasBounds":{"left":2461.5,"bottom":5881.5,"right":2641.5,"top":6120.5}},{"unicode":9522,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.501953125},"atlasBounds":{"left":2280.5,"bottom":5881.5,"right":2460.5,"top":6120.5}},{"unicode":9523,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.501953125},"atlasBounds":{"left":2099.5,"bottom":5881.5,"right":2279.5,"top":6120.5}},{"unicode":9524,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.287109375,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":3377.5,"bottom":5652.5,"right":3557.5,"top":5874.5}},{"unicode":9525,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.236328125,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":4775.5,"bottom":5885.5,"right":4955.5,"top":6120.5}},{"unicode":9526,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.236328125,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":4594.5,"bottom":5885.5,"right":4774.5,"top":6120.5}},{"unicode":9527,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.236328125,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":4413.5,"bottom":5885.5,"right":4593.5,"top":6120.5}},{"unicode":9528,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.287109375,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":3196.5,"bottom":5652.5,"right":3376.5,"top":5874.5}},{"unicode":9529,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.236328125,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":4232.5,"bottom":5885.5,"right":4412.5,"top":6120.5}},{"unicode":9530,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.236328125,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":4051.5,"bottom":5885.5,"right":4231.5,"top":6120.5}},{"unicode":9531,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.236328125,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":3745.5,"bottom":5885.5,"right":3925.5,"top":6120.5}},{"unicode":9532,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":1935.5,"bottom":7785.5,"right":2115.5,"top":8191.5}},{"unicode":9533,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":2116.5,"bottom":7785.5,"right":2296.5,"top":8191.5}},{"unicode":9534,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":2297.5,"bottom":7785.5,"right":2477.5,"top":8191.5}},{"unicode":9535,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":2478.5,"bottom":7785.5,"right":2658.5,"top":8191.5}},{"unicode":9536,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":2659.5,"bottom":7785.5,"right":2839.5,"top":8191.5}},{"unicode":9537,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":2840.5,"bottom":7785.5,"right":3020.5,"top":8191.5}},{"unicode":9538,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":3021.5,"bottom":7785.5,"right":3201.5,"top":8191.5}},{"unicode":9539,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":3202.5,"bottom":7785.5,"right":3382.5,"top":8191.5}},{"unicode":9540,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":3383.5,"bottom":7785.5,"right":3563.5,"top":8191.5}},{"unicode":9541,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":3564.5,"bottom":7785.5,"right":3744.5,"top":8191.5}},{"unicode":9542,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":3745.5,"bottom":7785.5,"right":3925.5,"top":8191.5}},{"unicode":9543,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":3926.5,"bottom":7785.5,"right":4106.5,"top":8191.5}},{"unicode":9544,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":4107.5,"bottom":7785.5,"right":4287.5,"top":8191.5}},{"unicode":9545,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":4288.5,"bottom":7785.5,"right":4468.5,"top":8191.5}},{"unicode":9546,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":4469.5,"bottom":7785.5,"right":4649.5,"top":8191.5}},{"unicode":9547,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":4650.5,"bottom":7785.5,"right":4830.5,"top":8191.5}},{"unicode":9548,"advance":0.59999999999999998,"planeBounds":{"left":0.038281249999999989,"bottom":0.287109375,"right":0.56171875000000004,"top":0.455078125},"atlasBounds":{"left":0.5,"bottom":3081.5,"right":134.5,"top":3124.5}},{"unicode":9549,"advance":0.59999999999999998,"planeBounds":{"left":0.042187499999999982,"bottom":0.236328125,"right":0.55781250000000004,"top":0.501953125},"atlasBounds":{"left":6580.5,"bottom":3270.5,"right":6712.5,"top":3338.5}},{"unicode":9550,"advance":0.59999999999999998,"planeBounds":{"left":0.21796875000000002,"bottom":-0.197265625,"right":0.38203124999999999,"top":0.927734375},"atlasBounds":{"left":3420.5,"bottom":7496.5,"right":3462.5,"top":7784.5}},{"unicode":9551,"advance":0.59999999999999998,"planeBounds":{"left":0.16718750000000002,"bottom":-0.177734375,"right":0.43281249999999999,"top":0.904296875},"atlasBounds":{"left":7390.5,"bottom":7507.5,"right":7458.5,"top":7784.5}},{"unicode":9552,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.185546875,"right":0.65156250000000004,"top":0.552734375},"atlasBounds":{"left":3696.5,"bottom":3244.5,"right":3876.5,"top":3338.5}},{"unicode":9553,"advance":0.59999999999999998,"planeBounds":{"left":0.118359375,"bottom":-0.431640625,"right":0.48164062499999999,"top":1.154296875},"atlasBounds":{"left":4831.5,"bottom":7785.5,"right":4924.5,"top":8191.5}},{"unicode":9554,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":-0.431640625,"right":0.65179687500000005,"top":0.552734375},"atlasBounds":{"left":980.5,"bottom":6121.5,"right":1091.5,"top":6373.5}},{"unicode":9555,"advance":0.59999999999999998,"planeBounds":{"left":0.117421875,"bottom":-0.431640625,"right":0.65257812500000001,"top":0.455078125},"atlasBounds":{"left":1137.5,"bottom":5647.5,"right":1274.5,"top":5874.5}},{"unicode":9556,"advance":0.59999999999999998,"planeBounds":{"left":0.117421875,"bottom":-0.431640625,"right":0.65257812500000001,"top":0.552734375},"atlasBounds":{"left":138.5,"bottom":6121.5,"right":275.5,"top":6373.5}},{"unicode":9557,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":-0.431640625,"right":0.38179687500000004,"top":0.552734375},"atlasBounds":{"left":1363.5,"bottom":6121.5,"right":1474.5,"top":6373.5}},{"unicode":9558,"advance":0.59999999999999998,"planeBounds":{"left":-0.052578125000000017,"bottom":-0.431640625,"right":0.48257812500000002,"top":0.455078125},"atlasBounds":{"left":818.5,"bottom":5647.5,"right":955.5,"top":5874.5}},{"unicode":9559,"advance":0.59999999999999998,"planeBounds":{"left":-0.052578125000000017,"bottom":-0.431640625,"right":0.48257812500000002,"top":0.552734375},"atlasBounds":{"left":1092.5,"bottom":6121.5,"right":1229.5,"top":6373.5}},{"unicode":9560,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":0.185546875,"right":0.65179687500000005,"top":1.154296875},"atlasBounds":{"left":7493.5,"bottom":6125.5,"right":7604.5,"top":6373.5}},{"unicode":9561,"advance":0.59999999999999998,"planeBounds":{"left":0.117421875,"bottom":0.287109375,"right":0.65257812500000001,"top":1.154296875},"atlasBounds":{"left":4237.5,"bottom":5652.5,"right":4374.5,"top":5874.5}},{"unicode":9562,"advance":0.59999999999999998,"planeBounds":{"left":0.117421875,"bottom":0.185546875,"right":0.65257812500000001,"top":1.154296875},"atlasBounds":{"left":7355.5,"bottom":6125.5,"right":7492.5,"top":6373.5}},{"unicode":9563,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":0.185546875,"right":0.38179687500000004,"top":1.154296875},"atlasBounds":{"left":7243.5,"bottom":6125.5,"right":7354.5,"top":6373.5}},{"unicode":9564,"advance":0.59999999999999998,"planeBounds":{"left":-0.052578125000000017,"bottom":0.287109375,"right":0.48257812500000002,"top":1.154296875},"atlasBounds":{"left":3694.5,"bottom":5652.5,"right":3831.5,"top":5874.5}},{"unicode":9565,"advance":0.59999999999999998,"planeBounds":{"left":-0.052578125000000017,"bottom":0.185546875,"right":0.48257812500000002,"top":1.154296875},"atlasBounds":{"left":6684.5,"bottom":6125.5,"right":6821.5,"top":6373.5}},{"unicode":9566,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":-0.431640625,"right":0.65179687500000005,"top":1.154296875},"atlasBounds":{"left":4925.5,"bottom":7785.5,"right":5036.5,"top":8191.5}},{"unicode":9567,"advance":0.59999999999999998,"planeBounds":{"left":0.117421875,"bottom":-0.431640625,"right":0.65257812500000001,"top":1.154296875},"atlasBounds":{"left":5037.5,"bottom":7785.5,"right":5174.5,"top":8191.5}},{"unicode":9568,"advance":0.59999999999999998,"planeBounds":{"left":0.117421875,"bottom":-0.431640625,"right":0.65257812500000001,"top":1.154296875},"atlasBounds":{"left":5175.5,"bottom":7785.5,"right":5312.5,"top":8191.5}},{"unicode":9569,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":-0.431640625,"right":0.38179687500000004,"top":1.154296875},"atlasBounds":{"left":5313.5,"bottom":7785.5,"right":5424.5,"top":8191.5}},{"unicode":9570,"advance":0.59999999999999998,"planeBounds":{"left":-0.052578125000000017,"bottom":-0.431640625,"right":0.48257812500000002,"top":1.154296875},"atlasBounds":{"left":5425.5,"bottom":7785.5,"right":5562.5,"top":8191.5}},{"unicode":9571,"advance":0.59999999999999998,"planeBounds":{"left":-0.052578125000000017,"bottom":-0.431640625,"right":0.48257812500000002,"top":1.154296875},"atlasBounds":{"left":5563.5,"bottom":7785.5,"right":5700.5,"top":8191.5}},{"unicode":9572,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.552734375},"atlasBounds":{"left":650.5,"bottom":6121.5,"right":830.5,"top":6373.5}},{"unicode":9573,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.455078125},"atlasBounds":{"left":956.5,"bottom":5647.5,"right":1136.5,"top":5874.5}},{"unicode":9574,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":0.552734375},"atlasBounds":{"left":469.5,"bottom":6121.5,"right":649.5,"top":6373.5}},{"unicode":9575,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.185546875,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":6367.5,"bottom":6125.5,"right":6547.5,"top":6373.5}},{"unicode":9576,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.287109375,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":2664.5,"bottom":5652.5,"right":2844.5,"top":5874.5}},{"unicode":9577,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.185546875,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":6044.5,"bottom":6125.5,"right":6224.5,"top":6373.5}},{"unicode":9578,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":5701.5,"bottom":7785.5,"right":5881.5,"top":8191.5}},{"unicode":9579,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":5882.5,"bottom":7785.5,"right":6062.5,"top":8191.5}},{"unicode":9580,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.431640625,"right":0.65156250000000004,"top":1.154296875},"atlasBounds":{"left":6063.5,"bottom":7785.5,"right":6243.5,"top":8191.5}},{"unicode":9581,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":-0.431640625,"right":0.65179687500000005,"top":0.455078125},"atlasBounds":{"left":461.5,"bottom":5647.5,"right":572.5,"top":5874.5}},{"unicode":9582,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":-0.431640625,"right":0.38179687500000004,"top":0.455078125},"atlasBounds":{"left":349.5,"bottom":5647.5,"right":460.5,"top":5874.5}},{"unicode":9583,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":0.287109375,"right":0.38179687500000004,"top":1.154296875},"atlasBounds":{"left":2552.5,"bottom":5652.5,"right":2663.5,"top":5874.5}},{"unicode":9584,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":0.287109375,"right":0.65179687500000005,"top":1.154296875},"atlasBounds":{"left":2440.5,"bottom":5652.5,"right":2551.5,"top":5874.5}},{"unicode":9585,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.193359375,"right":0.65156250000000004,"top":0.912109375},"atlasBounds":{"left":3632.5,"bottom":7501.5,"right":3812.5,"top":7784.5}},{"unicode":9586,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.193359375,"right":0.65156250000000004,"top":0.912109375},"atlasBounds":{"left":3813.5,"bottom":7501.5,"right":3993.5,"top":7784.5}},{"unicode":9587,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.193359375,"right":0.65156250000000004,"top":0.912109375},"atlasBounds":{"left":3994.5,"bottom":7501.5,"right":4174.5,"top":7784.5}},{"unicode":9588,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":0.287109375,"right":0.38179687500000004,"top":0.455078125},"atlasBounds":{"left":0.5,"bottom":3037.5,"right":111.5,"top":3080.5}},{"unicode":9589,"advance":0.59999999999999998,"planeBounds":{"left":0.21796875000000002,"bottom":0.287109375,"right":0.38203124999999999,"top":1.154296875},"atlasBounds":{"left":3957.5,"bottom":5652.5,"right":3999.5,"top":5874.5}},{"unicode":9590,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":0.287109375,"right":0.65179687500000005,"top":0.455078125},"atlasBounds":{"left":8068.5,"bottom":7171.5,"right":8179.5,"top":7214.5}},{"unicode":9591,"advance":0.59999999999999998,"planeBounds":{"left":0.21796875000000002,"bottom":-0.431640625,"right":0.38203124999999999,"top":0.455078125},"atlasBounds":{"left":1784.5,"bottom":5647.5,"right":1826.5,"top":5874.5}},{"unicode":9592,"advance":0.59999999999999998,"planeBounds":{"left":-0.051796874999999999,"bottom":0.236328125,"right":0.38179687500000004,"top":0.501953125},"atlasBounds":{"left":6175.5,"bottom":3270.5,"right":6286.5,"top":3338.5}},{"unicode":9593,"advance":0.59999999999999998,"planeBounds":{"left":0.16718750000000002,"bottom":0.287109375,"right":0.43281249999999999,"top":1.154296875},"atlasBounds":{"left":2845.5,"bottom":5652.5,"right":2913.5,"top":5874.5}},{"unicode":9594,"advance":0.59999999999999998,"planeBounds":{"left":0.218203125,"bottom":0.236328125,"right":0.65179687500000005,"top":0.501953125},"atlasBounds":{"left":6287.5,"bottom":3270.5,"right":6398.5,"top":3338.5}},{"unicode":9595,"advance":0.59999999999999998,"planeBounds":{"left":0.16718750000000002,"bottom":-0.431640625,"right":0.43281249999999999,"top":0.455078125},"atlasBounds":{"left":573.5,"bottom":5647.5,"right":641.5,"top":5874.5}},{"unicode":9596,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.236328125,"right":0.65156250000000004,"top":0.501953125},"atlasBounds":{"left":6399.5,"bottom":3270.5,"right":6579.5,"top":3338.5}},{"unicode":9597,"advance":0.59999999999999998,"planeBounds":{"left":0.16718750000000002,"bottom":-0.431640625,"right":0.43281249999999999,"top":1.154296875},"atlasBounds":{"left":6244.5,"bottom":7785.5,"right":6312.5,"top":8191.5}},{"unicode":9598,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":0.236328125,"right":0.65156250000000004,"top":0.501953125},"atlasBounds":{"left":5994.5,"bottom":3270.5,"right":6174.5,"top":3338.5}},{"unicode":9599,"advance":0.59999999999999998,"planeBounds":{"left":0.16718750000000002,"bottom":-0.431640625,"right":0.43281249999999999,"top":1.154296875},"atlasBounds":{"left":6313.5,"bottom":7785.5,"right":6381.5,"top":8191.5}},{"unicode":9600,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.326171875,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":1153.5,"bottom":3988.5,"right":1323.5,"top":4174.5}},{"unicode":9601,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":-0.103515625},"atlasBounds":{"left":8021.5,"bottom":7553.5,"right":8191.5,"top":7612.5}},{"unicode":9602,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":0.064453125},"atlasBounds":{"left":773.5,"bottom":3236.5,"right":943.5,"top":3338.5}},{"unicode":9603,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":0.228515625},"atlasBounds":{"left":135.5,"bottom":3340.5,"right":305.5,"top":3484.5}},{"unicode":9604,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":0.392578125},"atlasBounds":{"left":889.5,"bottom":3988.5,"right":1059.5,"top":4174.5}},{"unicode":9605,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":0.556640625},"atlasBounds":{"left":7671.5,"bottom":5892.5,"right":7841.5,"top":6120.5}},{"unicode":9606,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":0.724609375},"atlasBounds":{"left":530.5,"bottom":7157.5,"right":700.5,"top":7428.5}},{"unicode":9607,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":0.888671875},"atlasBounds":{"left":1893.5,"bottom":7471.5,"right":2063.5,"top":7784.5}},{"unicode":9608,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":6853.5,"bottom":7836.5,"right":7023.5,"top":8191.5}},{"unicode":9609,"advance":0.59999999999999998,"planeBounds":{"left":-0.032421874999999989,"bottom":-0.333984375,"right":0.55742187500000007,"top":1.052734375},"atlasBounds":{"left":7024.5,"bottom":7836.5,"right":7175.5,"top":8191.5}},{"unicode":9610,"advance":0.59999999999999998,"planeBounds":{"left":-0.032812500000000022,"bottom":-0.333984375,"right":0.48281250000000003,"top":1.052734375},"atlasBounds":{"left":7176.5,"bottom":7836.5,"right":7308.5,"top":8191.5}},{"unicode":9611,"advance":0.59999999999999998,"planeBounds":{"left":-0.03125,"bottom":-0.333984375,"right":0.40625,"top":1.052734375},"atlasBounds":{"left":7309.5,"bottom":7836.5,"right":7421.5,"top":8191.5}},{"unicode":9612,"advance":0.59999999999999998,"planeBounds":{"left":-0.031640625000000006,"bottom":-0.333984375,"right":0.33164062500000002,"top":1.052734375},"atlasBounds":{"left":7422.5,"bottom":7836.5,"right":7515.5,"top":8191.5}},{"unicode":9613,"advance":0.59999999999999998,"planeBounds":{"left":-0.032812499999999994,"bottom":-0.333984375,"right":0.23281250000000001,"top":1.052734375},"atlasBounds":{"left":7516.5,"bottom":7836.5,"right":7584.5,"top":8191.5}},{"unicode":9614,"advance":0.59999999999999998,"planeBounds":{"left":-0.032421875000000003,"bottom":-0.333984375,"right":0.18242187500000001,"top":1.052734375},"atlasBounds":{"left":7585.5,"bottom":7836.5,"right":7640.5,"top":8191.5}},{"unicode":9615,"advance":0.59999999999999998,"planeBounds":{"left":-0.032812499999999994,"bottom":-0.333984375,"right":0.10781250000000001,"top":1.052734375},"atlasBounds":{"left":7641.5,"bottom":7836.5,"right":7677.5,"top":8191.5}},{"unicode":9616,"advance":0.59999999999999998,"planeBounds":{"left":0.26835937500000001,"bottom":-0.333984375,"right":0.63164062500000007,"top":1.052734375},"atlasBounds":{"left":7678.5,"bottom":7836.5,"right":7771.5,"top":8191.5}},{"unicode":9617,"advance":0.59999999999999998,"planeBounds":{"left":0.027265625000000019,"bottom":-0.271484375,"right":0.63273437499999996,"top":1.052734375},"atlasBounds":{"left":1029.5,"bottom":7445.5,"right":1184.5,"top":7784.5}},{"unicode":9618,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":7772.5,"bottom":7836.5,"right":7942.5,"top":8191.5}},{"unicode":9619,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":7943.5,"bottom":7836.5,"right":8113.5,"top":8191.5}},{"unicode":9620,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.822265625,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":0.5,"bottom":3169.5,"right":170.5,"top":3228.5}},{"unicode":9621,"advance":0.59999999999999998,"planeBounds":{"left":0.4921875,"bottom":-0.333984375,"right":0.6328125,"top":1.052734375},"atlasBounds":{"left":8114.5,"bottom":7836.5,"right":8150.5,"top":8191.5}},{"unicode":9622,"advance":0.59999999999999998,"planeBounds":{"left":-0.031640625000000006,"bottom":-0.333984375,"right":0.33164062500000002,"top":0.392578125},"atlasBounds":{"left":795.5,"bottom":3988.5,"right":888.5,"top":4174.5}},{"unicode":9623,"advance":0.59999999999999998,"planeBounds":{"left":0.26835937500000001,"bottom":-0.333984375,"right":0.63164062500000007,"top":0.392578125},"atlasBounds":{"left":481.5,"bottom":3988.5,"right":574.5,"top":4174.5}},{"unicode":9624,"advance":0.59999999999999998,"planeBounds":{"left":-0.031640625000000006,"bottom":0.326171875,"right":0.33164062500000002,"top":1.052734375},"atlasBounds":{"left":701.5,"bottom":3988.5,"right":794.5,"top":4174.5}},{"unicode":9625,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":0.5,"bottom":7429.5,"right":170.5,"top":7784.5}},{"unicode":9626,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":171.5,"bottom":7429.5,"right":341.5,"top":7784.5}},{"unicode":9627,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":342.5,"bottom":7429.5,"right":512.5,"top":7784.5}},{"unicode":9628,"advance":0.59999999999999998,"planeBounds":{"left":-0.042890625000000009,"bottom":-0.333984375,"right":0.63289062500000004,"top":1.052734375},"atlasBounds":{"left":513.5,"bottom":7429.5,"right":686.5,"top":7784.5}},{"unicode":9629,"advance":0.59999999999999998,"planeBounds":{"left":0.26835937500000001,"bottom":0.326171875,"right":0.63164062500000007,"top":1.052734375},"atlasBounds":{"left":1324.5,"bottom":3988.5,"right":1417.5,"top":4174.5}},{"unicode":9630,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":687.5,"bottom":7429.5,"right":857.5,"top":7784.5}},{"unicode":9631,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.333984375,"right":0.63203125000000004,"top":1.052734375},"atlasBounds":{"left":858.5,"bottom":7429.5,"right":1028.5,"top":7784.5}},{"unicode":9632,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.025390625,"right":0.63203125000000004,"top":0.693359375},"atlasBounds":{"left":8021.5,"bottom":7613.5,"right":8191.5,"top":7784.5}},{"unicode":9633,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.025390625,"right":0.63203125000000004,"top":0.693359375},"atlasBounds":{"left":3931.5,"bottom":3813.5,"right":4101.5,"top":3984.5}},{"unicode":9642,"advance":0.59999999999999998,"planeBounds":{"left":0.118359375,"bottom":0.177734375,"right":0.48164062499999999,"top":0.544921875},"atlasBounds":{"left":2439.5,"bottom":3244.5,"right":2532.5,"top":3338.5}},{"unicode":9643,"advance":0.59999999999999998,"planeBounds":{"left":0.118359375,"bottom":0.177734375,"right":0.48164062499999999,"top":0.544921875},"atlasBounds":{"left":2906.5,"bottom":3244.5,"right":2999.5,"top":3338.5}},{"unicode":9650,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.025390625,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":911.5,"bottom":3810.5,"right":1086.5,"top":3984.5}},{"unicode":9651,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.025390625,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":1253.5,"bottom":3810.5,"right":1428.5,"top":3984.5}},{"unicode":9652,"advance":0.59999999999999998,"planeBounds":{"left":0.118359375,"bottom":0.177734375,"right":0.48164062499999999,"top":0.552734375},"atlasBounds":{"left":1132.5,"bottom":3242.5,"right":1225.5,"top":3338.5}},{"unicode":9653,"advance":0.59999999999999998,"planeBounds":{"left":0.118359375,"bottom":0.177734375,"right":0.48164062499999999,"top":0.552734375},"atlasBounds":{"left":1226.5,"bottom":3242.5,"right":1319.5,"top":3338.5}},{"unicode":9654,"advance":0.59999999999999998,"planeBounds":{"left":-0.032890625000000007,"bottom":0.017578125,"right":0.64289062500000005,"top":0.705078125},"atlasBounds":{"left":7304.5,"bottom":3998.5,"right":7477.5,"top":4174.5}},{"unicode":9655,"advance":0.59999999999999998,"planeBounds":{"left":-0.032890625000000007,"bottom":0.017578125,"right":0.64289062500000005,"top":0.705078125},"atlasBounds":{"left":4675.5,"bottom":3998.5,"right":4848.5,"top":4174.5}},{"unicode":9656,"advance":0.59999999999999998,"planeBounds":{"left":0.11750000000000001,"bottom":0.177734375,"right":0.49249999999999999,"top":0.544921875},"atlasBounds":{"left":2809.5,"bottom":3244.5,"right":2905.5,"top":3338.5}},{"unicode":9657,"advance":0.59999999999999998,"planeBounds":{"left":0.11750000000000001,"bottom":0.177734375,"right":0.49249999999999999,"top":0.544921875},"atlasBounds":{"left":4150.5,"bottom":3244.5,"right":4246.5,"top":3338.5}},{"unicode":9658,"advance":0.59999999999999998,"planeBounds":{"left":-0.032890625000000007,"bottom":0.177734375,"right":0.64289062500000005,"top":0.544921875},"atlasBounds":{"left":2265.5,"bottom":3244.5,"right":2438.5,"top":3338.5}},{"unicode":9659,"advance":0.59999999999999998,"planeBounds":{"left":-0.032890625000000007,"bottom":0.177734375,"right":0.64289062500000005,"top":0.544921875},"atlasBounds":{"left":3222.5,"bottom":3244.5,"right":3395.5,"top":3338.5}},{"unicode":9660,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.693359375},"atlasBounds":{"left":1595.5,"bottom":3811.5,"right":1770.5,"top":3984.5}},{"unicode":9661,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.693359375},"atlasBounds":{"left":1771.5,"bottom":3811.5,"right":1946.5,"top":3984.5}},{"unicode":9662,"advance":0.59999999999999998,"planeBounds":{"left":0.118359375,"bottom":0.166015625,"right":0.48164062499999999,"top":0.544921875},"atlasBounds":{"left":944.5,"bottom":3241.5,"right":1037.5,"top":3338.5}},{"unicode":9663,"advance":0.59999999999999998,"planeBounds":{"left":0.118359375,"bottom":0.166015625,"right":0.48164062499999999,"top":0.544921875},"atlasBounds":{"left":1038.5,"bottom":3241.5,"right":1131.5,"top":3338.5}},{"unicode":9664,"advance":0.59999999999999998,"planeBounds":{"left":-0.042890625000000009,"bottom":0.017578125,"right":0.63289062500000004,"top":0.705078125},"atlasBounds":{"left":6778.5,"bottom":3998.5,"right":6951.5,"top":4174.5}},{"unicode":9665,"advance":0.59999999999999998,"planeBounds":{"left":-0.042890625000000009,"bottom":0.017578125,"right":0.63289062500000004,"top":0.705078125},"atlasBounds":{"left":6604.5,"bottom":3998.5,"right":6777.5,"top":4174.5}},{"unicode":9666,"advance":0.59999999999999998,"planeBounds":{"left":0.1075,"bottom":0.177734375,"right":0.48249999999999998,"top":0.544921875},"atlasBounds":{"left":3396.5,"bottom":3244.5,"right":3492.5,"top":3338.5}},{"unicode":9667,"advance":0.59999999999999998,"planeBounds":{"left":0.1075,"bottom":0.177734375,"right":0.48249999999999998,"top":0.544921875},"atlasBounds":{"left":4053.5,"bottom":3244.5,"right":4149.5,"top":3338.5}},{"unicode":9668,"advance":0.59999999999999998,"planeBounds":{"left":-0.042890625000000009,"bottom":0.177734375,"right":0.63289062500000004,"top":0.544921875},"atlasBounds":{"left":2533.5,"bottom":3244.5,"right":2706.5,"top":3338.5}},{"unicode":9669,"advance":0.59999999999999998,"planeBounds":{"left":-0.042890625000000009,"bottom":0.177734375,"right":0.63289062500000004,"top":0.544921875},"atlasBounds":{"left":3048.5,"bottom":3244.5,"right":3221.5,"top":3338.5}},{"unicode":9670,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":5553.5,"bottom":3998.5,"right":5728.5,"top":4174.5}},{"unicode":9671,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":3569.5,"bottom":3998.5,"right":3744.5,"top":4174.5}},{"unicode":9672,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":4849.5,"bottom":3998.5,"right":5024.5,"top":4174.5}},{"unicode":9673,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":6952.5,"bottom":3998.5,"right":7127.5,"top":4174.5}},{"unicode":9674,"advance":0.59999999999999998,"planeBounds":{"left":0.028515624999999992,"bottom":-0.033203125,"right":0.57148437500000004,"top":0.751953125},"atlasBounds":{"left":3981.5,"bottom":4178.5,"right":4120.5,"top":4379.5}},{"unicode":9675,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":4097.5,"bottom":3998.5,"right":4272.5,"top":4174.5}},{"unicode":9676,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":4323.5,"bottom":3998.5,"right":4498.5,"top":4174.5}},{"unicode":9678,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":5201.5,"bottom":3998.5,"right":5376.5,"top":4174.5}},{"unicode":9679,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":3745.5,"bottom":3998.5,"right":3920.5,"top":4174.5}},{"unicode":9684,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":176.5,"bottom":3808.5,"right":351.5,"top":3984.5}},{"unicode":9685,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":7128.5,"bottom":3998.5,"right":7303.5,"top":4174.5}},{"unicode":9702,"advance":0.59999999999999998,"planeBounds":{"left":0.14765625000000002,"bottom":0.212890625,"right":0.45234374999999999,"top":0.517578125},"atlasBounds":{"left":8104.5,"bottom":3406.5,"right":8182.5,"top":3484.5}},{"unicode":9703,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.025390625,"right":0.63203125000000004,"top":0.693359375},"atlasBounds":{"left":4444.5,"bottom":3813.5,"right":4614.5,"top":3984.5}},{"unicode":9704,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.025390625,"right":0.63203125000000004,"top":0.693359375},"atlasBounds":{"left":3589.5,"bottom":3813.5,"right":3759.5,"top":3984.5}},{"unicode":9705,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.025390625,"right":0.63203125000000004,"top":0.693359375},"atlasBounds":{"left":3760.5,"bottom":3813.5,"right":3930.5,"top":3984.5}},{"unicode":9706,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.025390625,"right":0.63203125000000004,"top":0.693359375},"atlasBounds":{"left":4273.5,"bottom":3813.5,"right":4443.5,"top":3984.5}},{"unicode":9707,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":0.025390625,"right":0.63203125000000004,"top":0.693359375},"atlasBounds":{"left":3247.5,"bottom":3813.5,"right":3417.5,"top":3984.5}},{"unicode":9711,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":6428.5,"bottom":3998.5,"right":6603.5,"top":4174.5}},{"unicode":9718,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":4499.5,"bottom":3998.5,"right":4674.5,"top":4174.5}},{"unicode":9863,"advance":0.59999999999999998,"planeBounds":{"left":-0.027796875000000006,"bottom":0.017578125,"right":0.65579687500000006,"top":0.705078125},"atlasBounds":{"left":5025.5,"bottom":3998.5,"right":5200.5,"top":4174.5}},{"unicode":9888,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":-0.033203125,"right":0.64179687500000004,"top":0.771484375},"atlasBounds":{"left":5266.5,"bottom":5000.5,"right":5441.5,"top":5206.5}},{"unicode":9889,"advance":0.59999999999999998,"planeBounds":{"left":-0.012499999999999973,"bottom":-0.142578125,"right":0.61250000000000004,"top":0.873046875},"atlasBounds":{"left":614.5,"bottom":6632.5,"right":774.5,"top":6892.5}},{"unicode":10003,"advance":0.59999999999999998,"planeBounds":{"left":0.0084375000000000058,"bottom":0.099609375,"right":0.58656249999999999,"top":0.650390625},"atlasBounds":{"left":590.5,"bottom":3343.5,"right":738.5,"top":3484.5}},{"unicode":10005,"advance":0.59999999999999998,"planeBounds":{"left":0.041687499999999982,"bottom":0.072265625,"right":0.55731249999999999,"top":0.591796875},"atlasBounds":{"left":3263.5,"bottom":3351.5,"right":3395.5,"top":3484.5}},{"unicode":10007,"advance":0.59999999999999998,"planeBounds":{"left":0.0050781250000000106,"bottom":0.064453125,"right":0.59492187500000004,"top":0.654296875},"atlasBounds":{"left":5474.5,"bottom":3492.5,"right":5625.5,"top":3643.5}},{"unicode":10038,"advance":0.59999999999999998,"planeBounds":{"left":0.0065312500000000093,"bottom":0.025390625,"right":0.59246874999999999,"top":0.693359375},"atlasBounds":{"left":2925.5,"bottom":3813.5,"right":3075.5,"top":3984.5}},{"unicode":10094,"advance":0.59999999999999998,"planeBounds":{"left":0.11790625,"bottom":-0.033203125,"right":0.48509374999999999,"top":0.763671875},"atlasBounds":{"left":3340.5,"bottom":4175.5,"right":3434.5,"top":4379.5}},{"unicode":10095,"advance":0.59999999999999998,"planeBounds":{"left":0.11490625,"bottom":-0.033203125,"right":0.48209374999999999,"top":0.763671875},"atlasBounds":{"left":4640.5,"bottom":4380.5,"right":4734.5,"top":4584.5}},{"unicode":10096,"advance":0.59999999999999998,"planeBounds":{"left":0.077890625000000005,"bottom":-0.033203125,"right":0.52710937499999999,"top":0.763671875},"atlasBounds":{"left":4008.5,"bottom":4792.5,"right":4123.5,"top":4996.5}},{"unicode":10097,"advance":0.59999999999999998,"planeBounds":{"left":0.072890625000000001,"bottom":-0.033203125,"right":0.52210937499999999,"top":0.763671875},"atlasBounds":{"left":3276.5,"bottom":4792.5,"right":3391.5,"top":4996.5}},{"unicode":10132,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.087890625,"right":0.62226562500000004,"top":0.572265625},"atlasBounds":{"left":6491.5,"bottom":3360.5,"right":6656.5,"top":3484.5}},{"unicode":10140,"advance":0.59999999999999998,"planeBounds":{"left":-0.026718750000000017,"bottom":0.068359375,"right":0.62171874999999999,"top":0.591796875},"atlasBounds":{"left":2537.5,"bottom":3350.5,"right":2703.5,"top":3484.5}},{"unicode":10141,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.072265625,"right":0.62226562500000004,"top":0.587890625},"atlasBounds":{"left":4205.5,"bottom":3352.5,"right":4370.5,"top":3484.5}},{"unicode":10142,"advance":0.59999999999999998,"planeBounds":{"left":-0.022265625000000022,"bottom":0.072265625,"right":0.62226562500000004,"top":0.587890625},"atlasBounds":{"left":4572.5,"bottom":3352.5,"right":4737.5,"top":3484.5}},{"unicode":10204,"advance":0.59999999999999998,"planeBounds":{"left":-0.021406249999999967,"bottom":0.201171875,"right":0.61140625000000004,"top":0.458984375},"atlasBounds":{"left":7362.5,"bottom":3272.5,"right":7524.5,"top":3338.5}},{"unicode":10214,"advance":0.59999999999999998,"planeBounds":{"left":0.12828125000000001,"bottom":-0.142578125,"right":0.52671875000000001,"top":0.861328125},"atlasBounds":{"left":7548.5,"bottom":6635.5,"right":7650.5,"top":6892.5}},{"unicode":10215,"advance":0.59999999999999998,"planeBounds":{"left":0.07328125000000002,"bottom":-0.142578125,"right":0.47171875000000002,"top":0.861328125},"atlasBounds":{"left":7793.5,"bottom":6635.5,"right":7895.5,"top":6892.5}},{"unicode":10216,"advance":0.59999999999999998,"planeBounds":{"left":0.092109375000000007,"bottom":-0.146484375,"right":0.51789062500000005,"top":0.849609375},"atlasBounds":{"left":6929.5,"bottom":6376.5,"right":7038.5,"top":6631.5}},{"unicode":10217,"advance":0.59999999999999998,"planeBounds":{"left":0.082109374999999998,"bottom":-0.146484375,"right":0.50789062500000004,"top":0.849609375},"atlasBounds":{"left":7333.5,"bottom":6376.5,"right":7442.5,"top":6631.5}},{"unicode":10218,"advance":0.59999999999999998,"planeBounds":{"left":0.067234374999999985,"bottom":-0.142578125,"right":0.58676562499999996,"top":0.861328125},"atlasBounds":{"left":5064.5,"bottom":6635.5,"right":5197.5,"top":6892.5}},{"unicode":10219,"advance":0.59999999999999998,"planeBounds":{"left":0.013234374999999979,"bottom":-0.142578125,"right":0.53276562500000002,"top":0.861328125},"atlasBounds":{"left":6564.5,"bottom":6635.5,"right":6697.5,"top":6892.5}},{"unicode":10229,"advance":0.59999999999999998,"planeBounds":{"left":-0.073046874999999969,"bottom":0.041015625,"right":0.67304687500000004,"top":0.619140625},"atlasBounds":{"left":7290.5,"bottom":3495.5,"right":7481.5,"top":3643.5}},{"unicode":10230,"advance":0.59999999999999998,"planeBounds":{"left":-0.073046874999999969,"bottom":0.041015625,"right":0.67304687500000004,"top":0.619140625},"atlasBounds":{"left":6520.5,"bottom":3495.5,"right":6711.5,"top":3643.5}},{"unicode":10231,"advance":0.59999999999999998,"planeBounds":{"left":-0.112109375,"bottom":0.041015625,"right":0.71210937500000004,"top":0.619140625},"atlasBounds":{"left":6712.5,"bottom":3495.5,"right":6923.5,"top":3643.5}},{"unicode":10518,"advance":0.59999999999999998,"planeBounds":{"left":-0.112109375,"bottom":0.041015625,"right":0.71210937500000004,"top":0.509765625},"atlasBounds":{"left":6657.5,"bottom":3364.5,"right":6868.5,"top":3484.5}},{"unicode":10570,"advance":0.59999999999999998,"planeBounds":{"left":-0.112109375,"bottom":0.080078125,"right":0.71210937500000004,"top":0.580078125},"atlasBounds":{"left":5696.5,"bottom":3356.5,"right":5907.5,"top":3484.5}},{"unicode":10631,"advance":0.59999999999999998,"planeBounds":{"left":0.14250000000000002,"bottom":-0.154296875,"right":0.51749999999999996,"top":0.873046875},"atlasBounds":{"left":1154.5,"bottom":6893.5,"right":1250.5,"top":7156.5}},{"unicode":10632,"advance":0.59999999999999998,"planeBounds":{"left":0.082500000000000004,"bottom":-0.154296875,"right":0.45750000000000002,"top":0.873046875},"atlasBounds":{"left":1057.5,"bottom":6893.5,"right":1153.5,"top":7156.5}},{"unicode":10752,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":0.017578125,"right":0.64179687500000004,"top":0.705078125},"atlasBounds":{"left":2865.5,"bottom":3998.5,"right":3040.5,"top":4174.5}},{"unicode":10757,"advance":0.59999999999999998,"planeBounds":{"left":0.026562499999999992,"bottom":-0.033203125,"right":0.57343750000000004,"top":0.763671875},"atlasBounds":{"left":5482.5,"bottom":4585.5,"right":5622.5,"top":4789.5}},{"unicode":10758,"advance":0.59999999999999998,"planeBounds":{"left":0.026562499999999992,"bottom":-0.033203125,"right":0.57343750000000004,"top":0.763671875},"atlasBounds":{"left":4696.5,"bottom":4792.5,"right":4836.5,"top":4996.5}},{"unicode":11096,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999997,"bottom":-0.044921875,"right":0.65156250000000004,"top":0.654296875},"atlasBounds":{"left":1853.5,"bottom":3995.5,"right":2033.5,"top":4174.5}},{"unicode":65122,"advance":0.59999999999999998,"planeBounds":{"left":0.073062499999999989,"bottom":-0.033203125,"right":0.49493750000000003,"top":0.388671875},"atlasBounds":{"left":664.5,"bottom":3230.5,"right":772.5,"top":3338.5}},{"unicode":65533,"advance":0.59999999999999998,"planeBounds":{"left":-0.041796875000000004,"bottom":-0.044921875,"right":0.64179687500000004,"top":0.771484375},"atlasBounds":{"left":5739.5,"bottom":5215.5,"right":5914.5,"top":5424.5}},{"unicode":120120,"advance":0.59999999999999998,"planeBounds":{"left":0.012890625000000005,"bottom":-0.033203125,"right":0.58710937500000004,"top":0.763671875},"atlasBounds":{"left":8044.5,"bottom":5670.5,"right":8191.5,"top":5874.5}},{"unicode":120121,"advance":0.59999999999999998,"planeBounds":{"left":0.043281249999999986,"bottom":-0.033203125,"right":0.56671875000000005,"top":0.763671875},"atlasBounds":{"left":5060.5,"bottom":4585.5,"right":5194.5,"top":4789.5}},{"unicode":120123,"advance":0.59999999999999998,"planeBounds":{"left":0.042687499999999982,"bottom":-0.033203125,"right":0.55831249999999999,"top":0.763671875},"atlasBounds":{"left":5195.5,"bottom":4585.5,"right":5327.5,"top":4789.5}},{"unicode":120124,"advance":0.59999999999999998,"planeBounds":{"left":0.05359375000000003,"bottom":-0.033203125,"right":0.56140625,"top":0.763671875},"atlasBounds":{"left":5936.5,"bottom":4585.5,"right":6066.5,"top":4789.5}},{"unicode":120125,"advance":0.59999999999999998,"planeBounds":{"left":0.058593750000000028,"bottom":-0.033203125,"right":0.56640625,"top":0.763671875},"atlasBounds":{"left":291.5,"bottom":4380.5,"right":421.5,"top":4584.5}},{"unicode":120126,"advance":0.59999999999999998,"planeBounds":{"left":0.042187499999999982,"bottom":-0.044921875,"right":0.55781250000000004,"top":0.771484375},"atlasBounds":{"left":5186.5,"bottom":5215.5,"right":5318.5,"top":5424.5}},{"unicode":120128,"advance":0.59999999999999998,"planeBounds":{"left":0.048046875000000031,"bottom":-0.033203125,"right":0.55195312500000004,"top":0.763671875},"atlasBounds":{"left":1250.5,"bottom":4380.5,"right":1379.5,"top":4584.5}},{"unicode":120129,"advance":0.59999999999999998,"planeBounds":{"left":-0.0045468749999999745,"bottom":-0.044921875,"right":0.61654687500000005,"top":0.763671875},"atlasBounds":{"left":1727.5,"bottom":4999.5,"right":1886.5,"top":5206.5}},{"unicode":120130,"advance":0.59999999999999998,"planeBounds":{"left":0.04284375,"bottom":-0.033203125,"right":0.61315624999999996,"top":0.763671875},"atlasBounds":{"left":692.5,"bottom":4585.5,"right":838.5,"top":4789.5}},{"unicode":120131,"advance":0.59999999999999998,"planeBounds":{"left":0.067187499999999969,"bottom":-0.033203125,"right":0.58281250000000007,"top":0.763671875},"atlasBounds":{"left":8059.5,"bottom":4585.5,"right":8191.5,"top":4789.5}},{"unicode":120132,"advance":0.59999999999999998,"planeBounds":{"left":0.018749999999999999,"bottom":-0.033203125,"right":0.58125000000000004,"top":0.763671875},"atlasBounds":{"left":3076.5,"bottom":4585.5,"right":3220.5,"top":4789.5}},{"unicode":120134,"advance":0.59999999999999998,"planeBounds":{"left":0.042187499999999982,"bottom":-0.044921875,"right":0.55781250000000004,"top":0.771484375},"atlasBounds":{"left":4433.5,"bottom":5215.5,"right":4565.5,"top":5424.5}},{"unicode":120138,"advance":0.59999999999999998,"planeBounds":{"left":0.032421874999999989,"bottom":-0.044921875,"right":0.56757812500000004,"top":0.771484375},"atlasBounds":{"left":4566.5,"bottom":5215.5,"right":4703.5,"top":5424.5}},{"unicode":120139,"advance":0.59999999999999998,"planeBounds":{"left":0.018749999999999999,"bottom":-0.033203125,"right":0.58125000000000004,"top":0.763671875},"atlasBounds":{"left":2931.5,"bottom":4585.5,"right":3075.5,"top":4789.5}},{"unicode":120140,"advance":0.59999999999999998,"planeBounds":{"left":0.042187499999999982,"bottom":-0.044921875,"right":0.55781250000000004,"top":0.763671875},"atlasBounds":{"left":2324.5,"bottom":4999.5,"right":2456.5,"top":5206.5}},{"unicode":120141,"advance":0.59999999999999998,"planeBounds":{"left":0.012890625000000005,"bottom":-0.033203125,"right":0.58710937500000004,"top":0.763671875},"atlasBounds":{"left":3221.5,"bottom":4585.5,"right":3368.5,"top":4789.5}},{"unicode":120142,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.763671875},"atlasBounds":{"left":3523.5,"bottom":4585.5,"right":3693.5,"top":4789.5}},{"unicode":120143,"advance":0.59999999999999998,"planeBounds":{"left":-0.0027343749999999825,"bottom":-0.033203125,"right":0.60273437500000004,"top":0.763671875},"atlasBounds":{"left":3828.5,"bottom":4585.5,"right":3983.5,"top":4789.5}},{"unicode":120144,"advance":0.59999999999999998,"planeBounds":{"left":-0.012499999999999973,"bottom":-0.033203125,"right":0.61250000000000004,"top":0.763671875},"atlasBounds":{"left":3984.5,"bottom":4585.5,"right":4144.5,"top":4789.5}},{"unicode":120146,"advance":0.59999999999999998,"planeBounds":{"left":0.021468749999999991,"bottom":-0.044921875,"right":0.56053125000000004,"top":0.591796875},"atlasBounds":{"left":1983.5,"bottom":3644.5,"right":2121.5,"top":3807.5}},{"unicode":120147,"advance":0.59999999999999998,"planeBounds":{"left":0.043281249999999986,"bottom":-0.044921875,"right":0.56671875000000005,"top":0.763671875},"atlasBounds":{"left":2594.5,"bottom":4999.5,"right":2728.5,"top":5206.5}},{"unicode":120148,"advance":0.59999999999999998,"planeBounds":{"left":0.042187499999999982,"bottom":-0.044921875,"right":0.55781250000000004,"top":0.591796875},"atlasBounds":{"left":2328.5,"bottom":3644.5,"right":2460.5,"top":3807.5}},{"unicode":120149,"advance":0.59999999999999998,"planeBounds":{"left":0.033281249999999984,"bottom":-0.044921875,"right":0.55671875000000004,"top":0.763671875},"atlasBounds":{"left":2729.5,"bottom":4999.5,"right":2863.5,"top":5206.5}},{"unicode":120150,"advance":0.59999999999999998,"planeBounds":{"left":0.038281249999999989,"bottom":-0.044921875,"right":0.56171875000000004,"top":0.591796875},"atlasBounds":{"left":2898.5,"bottom":3644.5,"right":3032.5,"top":3807.5}},{"unicode":120151,"advance":0.59999999999999998,"planeBounds":{"left":0.031874999999999987,"bottom":-0.033203125,"right":0.56312499999999999,"top":0.763671875},"atlasBounds":{"left":5799.5,"bottom":4585.5,"right":5935.5,"top":4789.5}},{"unicode":120152,"advance":0.59999999999999998,"planeBounds":{"left":0.033281249999999984,"bottom":-0.212890625,"right":0.55671875000000004,"top":0.591796875},"atlasBounds":{"left":5921.5,"bottom":5000.5,"right":6055.5,"top":5206.5}},{"unicode":120153,"advance":0.59999999999999998,"planeBounds":{"left":0.043687499999999976,"bottom":-0.033203125,"right":0.55931249999999999,"top":0.763671875},"atlasBounds":{"left":6378.5,"bottom":4585.5,"right":6510.5,"top":4789.5}},{"unicode":120154,"advance":0.59999999999999998,"planeBounds":{"left":0.032109374999999996,"bottom":-0.033203125,"right":0.582890625,"top":0.826171875},"atlasBounds":{"left":2546.5,"bottom":5426.5,"right":2687.5,"top":5646.5}},{"unicode":120155,"advance":0.59999999999999998,"planeBounds":{"left":0.043203125000000002,"bottom":-0.212890625,"right":0.47679687500000001,"top":0.826171875},"atlasBounds":{"left":2623.5,"bottom":7162.5,"right":2734.5,"top":7428.5}},{"unicode":120156,"advance":0.59999999999999998,"planeBounds":{"left":0.043109374999999991,"bottom":-0.033203125,"right":0.59389062500000001,"top":0.763671875},"atlasBounds":{"left":4894.5,"bottom":4380.5,"right":5035.5,"top":4584.5}},{"unicode":120157,"advance":0.59999999999999998,"planeBounds":{"left":0.018749999999999999,"bottom":-0.033203125,"right":0.58125000000000004,"top":0.763671875},"atlasBounds":{"left":4145.5,"bottom":4585.5,"right":4289.5,"top":4789.5}},{"unicode":120158,"advance":0.59999999999999998,"planeBounds":{"left":-0.016406249999999969,"bottom":-0.033203125,"right":0.61640625000000004,"top":0.591796875},"atlasBounds":{"left":6107.5,"bottom":3647.5,"right":6269.5,"top":3807.5}},{"unicode":120159,"advance":0.59999999999999998,"planeBounds":{"left":0.043687499999999976,"bottom":-0.033203125,"right":0.55931249999999999,"top":0.591796875},"atlasBounds":{"left":5685.5,"bottom":3647.5,"right":5817.5,"top":3807.5}},{"unicode":120160,"advance":0.59999999999999998,"planeBounds":{"left":0.038281249999999989,"bottom":-0.044921875,"right":0.56171875000000004,"top":0.591796875},"atlasBounds":{"left":6806.5,"bottom":3821.5,"right":6940.5,"top":3984.5}},{"unicode":120161,"advance":0.59999999999999998,"planeBounds":{"left":0.043281249999999986,"bottom":-0.212890625,"right":0.56671875000000005,"top":0.591796875},"atlasBounds":{"left":7778.5,"bottom":5000.5,"right":7912.5,"top":5206.5}},{"unicode":120162,"advance":0.59999999999999998,"planeBounds":{"left":0.033281249999999984,"bottom":-0.212890625,"right":0.55671875000000004,"top":0.591796875},"atlasBounds":{"left":0.5,"bottom":4790.5,"right":134.5,"top":4996.5}},{"unicode":120163,"advance":0.59999999999999998,"planeBounds":{"left":0.013437500000000007,"bottom":-0.033203125,"right":0.59156249999999999,"top":0.591796875},"atlasBounds":{"left":5536.5,"bottom":3647.5,"right":5684.5,"top":3807.5}},{"unicode":120164,"advance":0.59999999999999998,"planeBounds":{"left":0.042187499999999982,"bottom":-0.044921875,"right":0.55781250000000004,"top":0.591796875},"atlasBounds":{"left":7702.5,"bottom":3821.5,"right":7834.5,"top":3984.5}},{"unicode":120165,"advance":0.59999999999999998,"planeBounds":{"left":0.022968749999999989,"bottom":-0.033203125,"right":0.56203124999999998,"top":0.736328125},"atlasBounds":{"left":5581.5,"bottom":4182.5,"right":5719.5,"top":4379.5}},{"unicode":120166,"advance":0.59999999999999998,"planeBounds":{"left":0.048046875000000031,"bottom":-0.044921875,"right":0.55195312500000004,"top":0.583984375},"atlasBounds":{"left":4797.5,"bottom":3646.5,"right":4926.5,"top":3807.5}},{"unicode":120167,"advance":0.59999999999999998,"planeBounds":{"left":0.012890625000000005,"bottom":-0.033203125,"right":0.58710937500000004,"top":0.583984375},"atlasBounds":{"left":3294.5,"bottom":3485.5,"right":3441.5,"top":3643.5}},{"unicode":120168,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.033203125,"right":0.63203125000000004,"top":0.583984375},"atlasBounds":{"left":1993.5,"bottom":3485.5,"right":2163.5,"top":3643.5}},{"unicode":120169,"advance":0.59999999999999998,"planeBounds":{"left":-0.0027343749999999825,"bottom":-0.033203125,"right":0.60273437500000004,"top":0.583984375},"atlasBounds":{"left":3891.5,"bottom":3485.5,"right":4046.5,"top":3643.5}},{"unicode":120170,"advance":0.59999999999999998,"planeBounds":{"left":0.012890625000000005,"bottom":-0.212890625,"right":0.58710937500000004,"top":0.583984375},"atlasBounds":{"left":1809.5,"bottom":4380.5,"right":1956.5,"top":4584.5}}],"kerning":[]} +{"atlas":{"type":"msdf","distanceRange":8,"distanceRangeMiddle":0,"size":128,"width":1024,"height":1024,"yOrigin":"bottom"},"metrics":{"emSize":1,"lineHeight":1.2,"ascender":0.94000000000000006,"descender":-0.26000000000000001,"underlineY":-0.17699999999999999,"underlineThickness":0.079000000000000001},"glyphs":[{"unicode":32,"advance":0.56000000000000005},{"unicode":33,"advance":0.56000000000000005,"planeBounds":{"left":0.1759375,"bottom":-0.05078125,"right":0.37906250000000002,"top":0.73046875},"atlasBounds":{"left":979.5,"bottom":789.5,"right":1005.5,"top":889.5}},{"unicode":34,"advance":0.56000000000000005,"planeBounds":{"left":0.12325,"bottom":0.45703125,"right":0.43575000000000003,"top":0.79296875},"atlasBounds":{"left":983.5,"bottom":737.5,"right":1023.5,"top":780.5}},{"unicode":35,"advance":0.56000000000000005,"planeBounds":{"left":-0.0012500000000000026,"bottom":-0.03515625,"right":0.56125000000000003,"top":0.73046875},"atlasBounds":{"left":718.5,"bottom":682.5,"right":790.5,"top":780.5}},{"unicode":36,"advance":0.56000000000000005,"planeBounds":{"left":0.034406249999999999,"bottom":-0.14453125,"right":0.52659374999999997,"top":0.80859375},"atlasBounds":{"left":556.5,"bottom":901.5,"right":619.5,"top":1023.5}},{"unicode":37,"advance":0.56000000000000005,"planeBounds":{"left":-0.0090624999999999924,"bottom":-0.05078125,"right":0.56906250000000003,"top":0.74609375},"atlasBounds":{"left":192.5,"bottom":787.5,"right":266.5,"top":889.5}},{"unicode":38,"advance":0.56000000000000005,"planeBounds":{"left":0.0051562499999999959,"bottom":-0.04296875,"right":0.55984374999999997,"top":0.74609375},"atlasBounds":{"left":952.5,"bottom":922.5,"right":1023.5,"top":1023.5}},{"unicode":39,"advance":0.56000000000000005,"planeBounds":{"left":0.20478125,"bottom":0.43359375,"right":0.35321875000000003,"top":0.79296875},"atlasBounds":{"left":335.5,"bottom":435.5,"right":354.5,"top":481.5}},{"unicode":40,"advance":0.56000000000000005,"planeBounds":{"left":0.10271875000000001,"bottom":-0.22265625,"right":0.45428125000000003,"top":0.81640625},"atlasBounds":{"left":0.5,"bottom":890.5,"right":45.5,"top":1023.5}},{"unicode":41,"advance":0.56000000000000005,"planeBounds":{"left":0.10471875000000001,"bottom":-0.22265625,"right":0.45628125000000003,"top":0.81640625},"atlasBounds":{"left":46.5,"bottom":890.5,"right":91.5,"top":1023.5}},{"unicode":42,"advance":0.56000000000000005,"planeBounds":{"left":0.033406249999999992,"bottom":0.25390625,"right":0.52559374999999997,"top":0.73046875},"atlasBounds":{"left":204.5,"bottom":420.5,"right":267.5,"top":481.5}},{"unicode":43,"advance":0.56000000000000005,"planeBounds":{"left":0.019781249999999979,"bottom":0.01171875,"right":0.54321874999999997,"top":0.57421875},"atlasBounds":{"left":0.5,"bottom":409.5,"right":67.5,"top":481.5}},{"unicode":44,"advance":0.56000000000000005,"planeBounds":{"left":0.13996875,"bottom":-0.19140625,"right":0.42903125000000003,"top":0.18359375},"atlasBounds":{"left":980.5,"bottom":532.5,"right":1017.5,"top":580.5}},{"unicode":45,"advance":0.56000000000000005,"planeBounds":{"left":0.1315625,"bottom":0.21484375,"right":0.42843750000000003,"top":0.35546875},"atlasBounds":{"left":983.5,"bottom":718.5,"right":1021.5,"top":736.5}},{"unicode":46,"advance":0.56000000000000005,"planeBounds":{"left":0.171125,"bottom":-0.05078125,"right":0.38987500000000003,"top":0.18359375},"atlasBounds":{"left":423.5,"bottom":451.5,"right":451.5,"top":481.5}},{"unicode":47,"advance":0.56000000000000005,"planeBounds":{"left":0.049531250000000006,"bottom":-0.22265625,"right":0.51046875000000003,"top":0.81640625},"atlasBounds":{"left":227.5,"bottom":890.5,"right":286.5,"top":1023.5}},{"unicode":48,"advance":0.56000000000000005,"planeBounds":{"left":0.022187500000000034,"bottom":-0.05078125,"right":0.53781250000000003,"top":0.74609375},"atlasBounds":{"left":267.5,"bottom":787.5,"right":333.5,"top":889.5}},{"unicode":49,"advance":0.56000000000000005,"planeBounds":{"left":0.058031250000000006,"bottom":-0.03515625,"right":0.51896874999999998,"top":0.73046875},"atlasBounds":{"left":923.5,"bottom":682.5,"right":982.5,"top":780.5}},{"unicode":50,"advance":0.56000000000000005,"planeBounds":{"left":0.034406249999999992,"bottom":-0.03515625,"right":0.52659374999999997,"top":0.74609375},"atlasBounds":{"left":0.5,"bottom":680.5,"right":63.5,"top":780.5}},{"unicode":51,"advance":0.56000000000000005,"planeBounds":{"left":0.039812499999999994,"bottom":-0.05078125,"right":0.52418750000000003,"top":0.74609375},"atlasBounds":{"left":334.5,"bottom":787.5,"right":396.5,"top":889.5}},{"unicode":52,"advance":0.56000000000000005,"planeBounds":{"left":0.011468749999999989,"bottom":-0.03515625,"right":0.55053125000000003,"top":0.73046875},"atlasBounds":{"left":809.5,"bottom":581.5,"right":878.5,"top":679.5}},{"unicode":53,"advance":0.56000000000000005,"planeBounds":{"left":0.050718750000000014,"bottom":-0.05078125,"right":0.52728125000000003,"top":0.73046875},"atlasBounds":{"left":64.5,"bottom":680.5,"right":125.5,"top":780.5}},{"unicode":54,"advance":0.56000000000000005,"planeBounds":{"left":0.027593750000000028,"bottom":-0.05078125,"right":0.53540624999999997,"top":0.73046875},"atlasBounds":{"left":126.5,"bottom":680.5,"right":191.5,"top":780.5}},{"unicode":55,"advance":0.56000000000000005,"planeBounds":{"left":0.043500000000000004,"bottom":-0.03515625,"right":0.54349999999999998,"top":0.73046875},"atlasBounds":{"left":0.5,"bottom":482.5,"right":64.5,"top":580.5}},{"unicode":56,"advance":0.56000000000000005,"planeBounds":{"left":0.029500000000000002,"bottom":-0.05078125,"right":0.52949999999999997,"top":0.74609375},"atlasBounds":{"left":397.5,"bottom":787.5,"right":461.5,"top":889.5}},{"unicode":57,"advance":0.56000000000000005,"planeBounds":{"left":0.024593750000000029,"bottom":-0.03515625,"right":0.53240624999999997,"top":0.74609375},"atlasBounds":{"left":192.5,"bottom":680.5,"right":257.5,"top":780.5}},{"unicode":58,"advance":0.56000000000000005,"planeBounds":{"left":0.171125,"bottom":-0.05078125,"right":0.38987500000000003,"top":0.55078125},"atlasBounds":{"left":489.5,"bottom":503.5,"right":517.5,"top":580.5}},{"unicode":59,"advance":0.56000000000000005,"planeBounds":{"left":0.10796875000000002,"bottom":-0.19140625,"right":0.39703125,"top":0.55078125},"atlasBounds":{"left":65.5,"bottom":485.5,"right":102.5,"top":580.5}},{"unicode":60,"advance":0.56000000000000005,"planeBounds":{"left":0.022281249999999978,"bottom":0.03515625,"right":0.54571875000000003,"top":0.53515625},"atlasBounds":{"left":68.5,"bottom":417.5,"right":135.5,"top":481.5}},{"unicode":61,"advance":0.56000000000000005,"planeBounds":{"left":0.019781249999999979,"bottom":0.12109375,"right":0.54321874999999997,"top":0.47265625},"atlasBounds":{"left":355.5,"bottom":436.5,"right":422.5,"top":481.5}},{"unicode":62,"advance":0.56000000000000005,"planeBounds":{"left":0.023781249999999979,"bottom":0.03515625,"right":0.54721874999999998,"top":0.53515625},"atlasBounds":{"left":136.5,"bottom":417.5,"right":203.5,"top":481.5}},{"unicode":63,"advance":0.56000000000000005,"planeBounds":{"left":0.077875000000000014,"bottom":-0.05078125,"right":0.48412500000000003,"top":0.74609375},"atlasBounds":{"left":462.5,"bottom":787.5,"right":514.5,"top":889.5}},{"unicode":64,"advance":0.56000000000000005,"planeBounds":{"left":0.018968749999999986,"bottom":-0.18359375,"right":0.55803124999999998,"top":0.74609375},"atlasBounds":{"left":620.5,"bottom":904.5,"right":689.5,"top":1023.5}},{"unicode":65,"advance":0.56000000000000005,"planeBounds":{"left":-0.024687499999999984,"bottom":-0.03515625,"right":0.58468750000000003,"top":0.73046875},"atlasBounds":{"left":660.5,"bottom":581.5,"right":738.5,"top":679.5}},{"unicode":66,"advance":0.56000000000000005,"planeBounds":{"left":0.02559375000000003,"bottom":-0.04296875,"right":0.53340624999999997,"top":0.73828125},"atlasBounds":{"left":258.5,"bottom":680.5,"right":323.5,"top":780.5}},{"unicode":67,"advance":0.56000000000000005,"planeBounds":{"left":0.027687499999999979,"bottom":-0.05078125,"right":0.54331249999999998,"top":0.74609375},"atlasBounds":{"left":515.5,"bottom":787.5,"right":581.5,"top":889.5}},{"unicode":68,"advance":0.56000000000000005,"planeBounds":{"left":0.027687499999999979,"bottom":-0.04296875,"right":0.54331249999999998,"top":0.73828125},"atlasBounds":{"left":324.5,"bottom":680.5,"right":390.5,"top":780.5}},{"unicode":69,"advance":0.56000000000000005,"planeBounds":{"left":0.078625000000000014,"bottom":-0.03515625,"right":0.54737500000000006,"top":0.73046875},"atlasBounds":{"left":390.5,"bottom":581.5,"right":450.5,"top":679.5}},{"unicode":70,"advance":0.56000000000000005,"planeBounds":{"left":0.076437500000000005,"bottom":-0.03515625,"right":0.52956250000000005,"top":0.73046875},"atlasBounds":{"left":331.5,"bottom":581.5,"right":389.5,"top":679.5}},{"unicode":71,"advance":0.56000000000000005,"planeBounds":{"left":0.025687500000000033,"bottom":-0.05078125,"right":0.54131249999999997,"top":0.74609375},"atlasBounds":{"left":582.5,"bottom":787.5,"right":648.5,"top":889.5}},{"unicode":72,"advance":0.56000000000000005,"planeBounds":{"left":0.018281249999999982,"bottom":-0.03515625,"right":0.54171875000000003,"top":0.73046875},"atlasBounds":{"left":202.5,"bottom":581.5,"right":269.5,"top":679.5}},{"unicode":73,"advance":0.56000000000000005,"planeBounds":{"left":0.076875000000000013,"bottom":-0.03515625,"right":0.48312500000000003,"top":0.73046875},"atlasBounds":{"left":149.5,"bottom":581.5,"right":201.5,"top":679.5}},{"unicode":74,"advance":0.56000000000000005,"planeBounds":{"left":0.029625000000000012,"bottom":-0.05078125,"right":0.49837500000000001,"top":0.73046875},"atlasBounds":{"left":391.5,"bottom":680.5,"right":451.5,"top":780.5}},{"unicode":75,"advance":0.56000000000000005,"planeBounds":{"left":0.045468749999999988,"bottom":-0.03515625,"right":0.58453125000000006,"top":0.73046875},"atlasBounds":{"left":739.5,"bottom":581.5,"right":808.5,"top":679.5}},{"unicode":76,"advance":0.56000000000000005,"planeBounds":{"left":0.077625000000000013,"bottom":-0.03515625,"right":0.54637500000000006,"top":0.73046875},"atlasBounds":{"left":270.5,"bottom":581.5,"right":330.5,"top":679.5}},{"unicode":77,"advance":0.56000000000000005,"planeBounds":{"left":0.006562499999999992,"bottom":-0.03515625,"right":0.55343750000000003,"top":0.73046875},"atlasBounds":{"left":0.5,"bottom":581.5,"right":70.5,"top":679.5}},{"unicode":78,"advance":0.56000000000000005,"planeBounds":{"left":0.029999999999999995,"bottom":-0.03515625,"right":0.53000000000000003,"top":0.73046875},"atlasBounds":{"left":522.5,"bottom":581.5,"right":586.5,"top":679.5}},{"unicode":79,"advance":0.56000000000000005,"planeBounds":{"left":-0.00025000000000000179,"bottom":-0.05078125,"right":0.56225000000000003,"top":0.74609375},"atlasBounds":{"left":649.5,"bottom":787.5,"right":721.5,"top":889.5}},{"unicode":80,"advance":0.56000000000000005,"planeBounds":{"left":0.054312500000000014,"bottom":-0.03515625,"right":0.53868749999999999,"top":0.73828125},"atlasBounds":{"left":588.5,"bottom":681.5,"right":650.5,"top":780.5}},{"unicode":81,"advance":0.56000000000000005,"planeBounds":{"left":-0.00025000000000000179,"bottom":-0.21484375,"right":0.56225000000000003,"top":0.74609375},"atlasBounds":{"left":483.5,"bottom":900.5,"right":555.5,"top":1023.5}},{"unicode":82,"advance":0.56000000000000005,"planeBounds":{"left":0.028687500000000032,"bottom":-0.03515625,"right":0.54431249999999998,"top":0.73828125},"atlasBounds":{"left":651.5,"bottom":681.5,"right":717.5,"top":780.5}},{"unicode":83,"advance":0.56000000000000005,"planeBounds":{"left":0.033906249999999992,"bottom":-0.05078125,"right":0.52609375000000003,"top":0.74609375},"atlasBounds":{"left":722.5,"bottom":787.5,"right":785.5,"top":889.5}},{"unicode":84,"advance":0.56000000000000005,"planeBounds":{"left":0.014374999999999983,"bottom":-0.03515625,"right":0.54562500000000003,"top":0.73046875},"atlasBounds":{"left":854.5,"bottom":682.5,"right":922.5,"top":780.5}},{"unicode":85,"advance":0.56000000000000005,"planeBounds":{"left":0.022187500000000034,"bottom":-0.05078125,"right":0.53781250000000003,"top":0.73046875},"atlasBounds":{"left":452.5,"bottom":680.5,"right":518.5,"top":780.5}},{"unicode":86,"advance":0.56000000000000005,"planeBounds":{"left":-0.019781249999999986,"bottom":-0.03515625,"right":0.58178125000000003,"top":0.73046875},"atlasBounds":{"left":71.5,"bottom":581.5,"right":148.5,"top":679.5}},{"unicode":87,"advance":0.56000000000000005,"planeBounds":{"left":0.006562499999999992,"bottom":-0.03515625,"right":0.55343750000000003,"top":0.73046875},"atlasBounds":{"left":451.5,"bottom":581.5,"right":521.5,"top":679.5}},{"unicode":88,"advance":0.56000000000000005,"planeBounds":{"left":-0.0012500000000000026,"bottom":-0.03515625,"right":0.56125000000000003,"top":0.73046875},"atlasBounds":{"left":587.5,"bottom":581.5,"right":659.5,"top":679.5}},{"unicode":89,"advance":0.56000000000000005,"planeBounds":{"left":-0.019781249999999986,"bottom":-0.03515625,"right":0.58178125000000003,"top":0.73046875},"atlasBounds":{"left":946.5,"bottom":581.5,"right":1023.5,"top":679.5}},{"unicode":90,"advance":0.56000000000000005,"planeBounds":{"left":0.028687500000000032,"bottom":-0.03515625,"right":0.54431249999999998,"top":0.73046875},"atlasBounds":{"left":879.5,"bottom":581.5,"right":945.5,"top":679.5}},{"unicode":91,"advance":0.56000000000000005,"planeBounds":{"left":0.13734375000000001,"bottom":-0.22265625,"right":0.45765624999999999,"top":0.81640625},"atlasBounds":{"left":287.5,"bottom":890.5,"right":328.5,"top":1023.5}},{"unicode":92,"advance":0.56000000000000005,"planeBounds":{"left":0.052937500000000005,"bottom":-0.22265625,"right":0.50606249999999997,"top":0.81640625},"atlasBounds":{"left":329.5,"bottom":890.5,"right":387.5,"top":1023.5}},{"unicode":93,"advance":0.56000000000000005,"planeBounds":{"left":0.10234375,"bottom":-0.22265625,"right":0.42265625000000001,"top":0.81640625},"atlasBounds":{"left":388.5,"bottom":890.5,"right":429.5,"top":1023.5}},{"unicode":94,"advance":0.56000000000000005,"planeBounds":{"left":0.021187499999999981,"bottom":0.28515625,"right":0.53681250000000003,"top":0.73046875},"atlasBounds":{"left":268.5,"bottom":424.5,"right":334.5,"top":481.5}},{"unicode":95,"advance":0.56000000000000005,"planeBounds":{"left":-0.024687499999999984,"bottom":-0.22265625,"right":0.58468750000000003,"top":-0.08203125},"atlasBounds":{"left":521.5,"bottom":463.5,"right":599.5,"top":481.5}},{"unicode":96,"advance":0.56000000000000005,"planeBounds":{"left":0.15431249999999999,"bottom":0.54296875,"right":0.38868750000000002,"top":0.80859375},"atlasBounds":{"left":983.5,"bottom":683.5,"right":1013.5,"top":717.5}},{"unicode":97,"advance":0.56000000000000005,"planeBounds":{"left":0.037218750000000016,"bottom":-0.05078125,"right":0.51378124999999997,"top":0.56640625},"atlasBounds":{"left":169.5,"bottom":501.5,"right":230.5,"top":580.5}},{"unicode":98,"advance":0.56000000000000005,"planeBounds":{"left":0.051406250000000001,"bottom":-0.05078125,"right":0.54359374999999999,"top":0.80859375},"atlasBounds":{"left":690.5,"bottom":913.5,"right":753.5,"top":1023.5}},{"unicode":99,"advance":0.56000000000000005,"planeBounds":{"left":0.024593750000000029,"bottom":-0.05078125,"right":0.53240624999999997,"top":0.56640625},"atlasBounds":{"left":103.5,"bottom":501.5,"right":168.5,"top":580.5}},{"unicode":100,"advance":0.56000000000000005,"planeBounds":{"left":0.013999999999999997,"bottom":-0.05078125,"right":0.51400000000000001,"top":0.80859375},"atlasBounds":{"left":754.5,"bottom":913.5,"right":818.5,"top":1023.5}},{"unicode":101,"advance":0.56000000000000005,"planeBounds":{"left":0.015781249999999983,"bottom":-0.05078125,"right":0.53921874999999997,"top":0.56640625},"atlasBounds":{"left":292.5,"bottom":501.5,"right":359.5,"top":580.5}},{"unicode":102,"advance":0.56000000000000005,"planeBounds":{"left":0.046781249999999983,"bottom":-0.03515625,"right":0.57021875,"top":0.80859375},"atlasBounds":{"left":884.5,"bottom":915.5,"right":951.5,"top":1023.5}},{"unicode":103,"advance":0.56000000000000005,"planeBounds":{"left":0.011999999999999997,"bottom":-0.22265625,"right":0.51200000000000001,"top":0.56640625},"atlasBounds":{"left":786.5,"bottom":788.5,"right":850.5,"top":889.5}},{"unicode":104,"advance":0.56000000000000005,"planeBounds":{"left":0.051531250000000015,"bottom":-0.03515625,"right":0.51246875000000003,"top":0.80859375},"atlasBounds":{"left":0.5,"bottom":781.5,"right":59.5,"top":889.5}},{"unicode":105,"advance":0.56000000000000005,"planeBounds":{"left":0.031999999999999994,"bottom":-0.05078125,"right":0.53200000000000003,"top":0.79296875},"atlasBounds":{"left":60.5,"bottom":781.5,"right":124.5,"top":889.5}},{"unicode":106,"advance":0.56000000000000005,"planeBounds":{"left":0.053375000000000013,"bottom":-0.22265625,"right":0.45962500000000001,"top":0.79296875},"atlasBounds":{"left":430.5,"bottom":893.5,"right":482.5,"top":1023.5}},{"unicode":107,"advance":0.56000000000000005,"planeBounds":{"left":0.049687500000000037,"bottom":-0.03515625,"right":0.5653125,"top":0.80859375},"atlasBounds":{"left":125.5,"bottom":781.5,"right":191.5,"top":889.5}},{"unicode":108,"advance":0.56000000000000005,"planeBounds":{"left":0.031999999999999994,"bottom":-0.05078125,"right":0.53200000000000003,"top":0.80859375},"atlasBounds":{"left":819.5,"bottom":913.5,"right":883.5,"top":1023.5}},{"unicode":109,"advance":0.56000000000000005,"planeBounds":{"left":0.018374999999999985,"bottom":-0.03515625,"right":0.54962500000000003,"top":0.56640625},"atlasBounds":{"left":518.5,"bottom":503.5,"right":586.5,"top":580.5}},{"unicode":110,"advance":0.56000000000000005,"planeBounds":{"left":0.051531250000000015,"bottom":-0.03515625,"right":0.51246875000000003,"top":0.56640625},"atlasBounds":{"left":429.5,"bottom":503.5,"right":488.5,"top":580.5}},{"unicode":111,"advance":0.56000000000000005,"planeBounds":{"left":0.014374999999999983,"bottom":-0.04296875,"right":0.54562500000000003,"top":0.56640625},"atlasBounds":{"left":360.5,"bottom":502.5,"right":428.5,"top":580.5}},{"unicode":112,"advance":0.56000000000000005,"planeBounds":{"left":0.051406250000000001,"bottom":-0.22265625,"right":0.54359374999999999,"top":0.56640625},"atlasBounds":{"left":851.5,"bottom":788.5,"right":914.5,"top":889.5}},{"unicode":113,"advance":0.56000000000000005,"planeBounds":{"left":0.016406249999999997,"bottom":-0.22265625,"right":0.50859374999999996,"top":0.56640625},"atlasBounds":{"left":915.5,"bottom":788.5,"right":978.5,"top":889.5}},{"unicode":114,"advance":0.56000000000000005,"planeBounds":{"left":0.094062500000000007,"bottom":-0.03515625,"right":0.51593750000000005,"top":0.56640625},"atlasBounds":{"left":587.5,"bottom":503.5,"right":641.5,"top":580.5}},{"unicode":115,"advance":0.56000000000000005,"planeBounds":{"left":0.046125000000000013,"bottom":-0.05078125,"right":0.51487499999999997,"top":0.56640625},"atlasBounds":{"left":231.5,"bottom":501.5,"right":291.5,"top":580.5}},{"unicode":116,"advance":0.56000000000000005,"planeBounds":{"left":0.04631250000000002,"bottom":-0.05078125,"right":0.53068749999999998,"top":0.71484375},"atlasBounds":{"left":791.5,"bottom":682.5,"right":853.5,"top":780.5}},{"unicode":117,"advance":0.56000000000000005,"planeBounds":{"left":0.047531250000000018,"bottom":-0.04296875,"right":0.50846875000000002,"top":0.55078125},"atlasBounds":{"left":642.5,"bottom":504.5,"right":701.5,"top":580.5}},{"unicode":118,"advance":0.56000000000000005,"planeBounds":{"left":0.006562499999999992,"bottom":-0.03515625,"right":0.55343750000000003,"top":0.55078125},"atlasBounds":{"left":702.5,"bottom":505.5,"right":772.5,"top":580.5}},{"unicode":119,"advance":0.56000000000000005,"planeBounds":{"left":-0.0090624999999999942,"bottom":-0.03515625,"right":0.56906250000000003,"top":0.55078125},"atlasBounds":{"left":845.5,"bottom":505.5,"right":919.5,"top":580.5}},{"unicode":120,"advance":0.56000000000000005,"planeBounds":{"left":0.0031562499999999963,"bottom":-0.03515625,"right":0.55784374999999997,"top":0.55078125},"atlasBounds":{"left":773.5,"bottom":505.5,"right":844.5,"top":580.5}},{"unicode":121,"advance":0.56000000000000005,"planeBounds":{"left":0.012874999999999985,"bottom":-0.22265625,"right":0.54412499999999997,"top":0.55078125},"atlasBounds":{"left":519.5,"bottom":681.5,"right":587.5,"top":780.5}},{"unicode":122,"advance":0.56000000000000005,"planeBounds":{"left":0.049031250000000005,"bottom":-0.03515625,"right":0.50996874999999997,"top":0.55078125},"atlasBounds":{"left":920.5,"bottom":505.5,"right":979.5,"top":580.5}},{"unicode":123,"advance":0.56000000000000005,"planeBounds":{"left":0.061343750000000002,"bottom":-0.22265625,"right":0.50665625000000003,"top":0.81640625},"atlasBounds":{"left":169.5,"bottom":890.5,"right":226.5,"top":1023.5}},{"unicode":124,"advance":0.56000000000000005,"planeBounds":{"left":0.2101875,"bottom":-0.22265625,"right":0.35081250000000003,"top":0.81640625},"atlasBounds":{"left":150.5,"bottom":890.5,"right":168.5,"top":1023.5}},{"unicode":125,"advance":0.56000000000000005,"planeBounds":{"left":0.053343750000000002,"bottom":-0.22265625,"right":0.49865625000000002,"top":0.81640625},"atlasBounds":{"left":92.5,"bottom":890.5,"right":149.5,"top":1023.5}},{"unicode":126,"advance":0.56000000000000005,"planeBounds":{"left":0.012874999999999985,"bottom":0.18359375,"right":0.54412499999999997,"top":0.40234375},"atlasBounds":{"left":452.5,"bottom":453.5,"right":520.5,"top":481.5}}],"kerning":[]} diff --git a/resources/public/font_atlas.json.bak b/resources/public/font_atlas.json.bak new file mode 100644 index 0000000..d45d5c0 --- /dev/null +++ b/resources/public/font_atlas.json.bak @@ -0,0 +1 @@ +{"atlas":{"type":"msdf","distanceRange":8,"distanceRangeMiddle":0,"size":64,"width":452,"height":452,"yOrigin":"bottom"},"metrics":{"emSize":1,"lineHeight":1.3200000000000001,"ascender":1.02,"descender":-0.29999999999999999,"underlineY":-0.17999999999999999,"underlineThickness":0.050000000000000003},"glyphs":[{"unicode":32,"advance":0.59999999999999998},{"unicode":33,"advance":0.59999999999999998,"planeBounds":{"left":0.15937500000000002,"bottom":-0.0703125,"right":0.44062499999999999,"top":0.8046875},"atlasBounds":{"left":426.5,"bottom":320.5,"right":444.5,"top":376.5}},{"unicode":34,"advance":0.59999999999999998,"planeBounds":{"left":0.073437500000000003,"bottom":0.3671875,"right":0.52656250000000004,"top":0.8046875},"atlasBounds":{"left":131.5,"bottom":13.5,"right":160.5,"top":41.5}},{"unicode":35,"advance":0.59999999999999998,"planeBounds":{"left":-0.028124999999999987,"bottom":-0.0703125,"right":0.62812500000000004,"top":0.8046875},"atlasBounds":{"left":258.5,"bottom":260.5,"right":300.5,"top":316.5}},{"unicode":36,"advance":0.59999999999999998,"planeBounds":{"left":0.0031249999999999807,"bottom":-0.2109375,"right":0.59687500000000004,"top":0.9453125},"atlasBounds":{"left":0.5,"bottom":377.5,"right":38.5,"top":451.5}},{"unicode":37,"advance":0.59999999999999998,"planeBounds":{"left":-0.051562499999999969,"bottom":-0.0703125,"right":0.65156250000000004,"top":0.8046875},"atlasBounds":{"left":301.5,"bottom":260.5,"right":346.5,"top":316.5}},{"unicode":38,"advance":0.59999999999999998,"planeBounds":{"left":-0.039375000000000014,"bottom":-0.0859375,"right":0.67937500000000006,"top":0.8046875},"atlasBounds":{"left":40.5,"bottom":319.5,"right":86.5,"top":376.5}},{"unicode":39,"advance":0.59999999999999998,"planeBounds":{"left":0.18281250000000002,"bottom":0.3671875,"right":0.41718749999999999,"top":0.8046875},"atlasBounds":{"left":115.5,"bottom":13.5,"right":130.5,"top":41.5}},{"unicode":40,"advance":0.59999999999999998,"planeBounds":{"left":0.11625000000000001,"bottom":-0.1953125,"right":0.55374999999999996,"top":0.9140625},"atlasBounds":{"left":39.5,"bottom":380.5,"right":67.5,"top":451.5}},{"unicode":41,"advance":0.59999999999999998,"planeBounds":{"left":0.046249999999999993,"bottom":-0.1953125,"right":0.48375000000000001,"top":0.9140625},"atlasBounds":{"left":68.5,"bottom":380.5,"right":96.5,"top":451.5}},{"unicode":42,"advance":0.59999999999999998,"planeBounds":{"left":-0.028124999999999987,"bottom":0.0390625,"right":0.62812500000000004,"top":0.6953125},"atlasBounds":{"left":384.5,"bottom":45.5,"right":426.5,"top":87.5}},{"unicode":43,"advance":0.59999999999999998,"planeBounds":{"left":-0.0046875000000000128,"bottom":0.0234375,"right":0.60468750000000004,"top":0.6328125},"atlasBounds":{"left":0.5,"bottom":2.5,"right":39.5,"top":41.5}},{"unicode":44,"advance":0.59999999999999998,"planeBounds":{"left":0.14306250000000004,"bottom":-0.2265625,"right":0.43993750000000004,"top":0.2109375},"atlasBounds":{"left":425.5,"bottom":404.5,"right":444.5,"top":432.5}},{"unicode":45,"advance":0.59999999999999998,"planeBounds":{"left":0.073437500000000003,"bottom":0.2265625,"right":0.52656250000000004,"top":0.4453125},"atlasBounds":{"left":200.5,"bottom":27.5,"right":229.5,"top":41.5}},{"unicode":46,"advance":0.59999999999999998,"planeBounds":{"left":0.15156250000000002,"bottom":-0.0859375,"right":0.44843749999999999,"top":0.2265625},"atlasBounds":{"left":425.5,"bottom":383.5,"right":444.5,"top":403.5}},{"unicode":47,"advance":0.59999999999999998,"planeBounds":{"left":0.010937500000000034,"bottom":-0.1796875,"right":0.58906250000000004,"top":0.8984375},"atlasBounds":{"left":182.5,"bottom":382.5,"right":219.5,"top":451.5}},{"unicode":48,"advance":0.59999999999999998,"planeBounds":{"left":0.010937500000000029,"bottom":-0.0859375,"right":0.58906250000000004,"top":0.8046875},"atlasBounds":{"left":123.5,"bottom":319.5,"right":160.5,"top":376.5}},{"unicode":49,"advance":0.59999999999999998,"planeBounds":{"left":0.025937500000000037,"bottom":-0.0703125,"right":0.60406250000000006,"top":0.8046875},"atlasBounds":{"left":325.5,"bottom":202.5,"right":362.5,"top":258.5}},{"unicode":50,"advance":0.59999999999999998,"planeBounds":{"left":0.012437500000000034,"bottom":-0.0703125,"right":0.59056249999999999,"top":0.8046875},"atlasBounds":{"left":75.5,"bottom":145.5,"right":112.5,"top":201.5}},{"unicode":51,"advance":0.59999999999999998,"planeBounds":{"left":0.00093750000000002848,"bottom":-0.0859375,"right":0.57906250000000004,"top":0.8046875},"atlasBounds":{"left":161.5,"bottom":319.5,"right":198.5,"top":376.5}},{"unicode":52,"advance":0.59999999999999998,"planeBounds":{"left":0.0065625000000000197,"bottom":-0.0703125,"right":0.55343750000000003,"top":0.8046875},"atlasBounds":{"left":183.5,"bottom":145.5,"right":218.5,"top":201.5}},{"unicode":53,"advance":0.59999999999999998,"planeBounds":{"left":0.0059375000000000287,"bottom":-0.0859375,"right":0.58406250000000004,"top":0.8046875},"atlasBounds":{"left":199.5,"bottom":319.5,"right":236.5,"top":376.5}},{"unicode":54,"advance":0.59999999999999998,"planeBounds":{"left":-0.0046875000000000128,"bottom":-0.0859375,"right":0.60468750000000004,"top":0.8046875},"atlasBounds":{"left":237.5,"bottom":319.5,"right":276.5,"top":376.5}},{"unicode":55,"advance":0.59999999999999998,"planeBounds":{"left":0.017124999999999987,"bottom":-0.0703125,"right":0.61087500000000006,"top":0.8046875},"atlasBounds":{"left":0.5,"bottom":88.5,"right":38.5,"top":144.5}},{"unicode":56,"advance":0.59999999999999998,"planeBounds":{"left":0.0031249999999999859,"bottom":-0.0859375,"right":0.59687500000000004,"top":0.8046875},"atlasBounds":{"left":277.5,"bottom":319.5,"right":315.5,"top":376.5}},{"unicode":57,"advance":0.59999999999999998,"planeBounds":{"left":-0.0046875000000000128,"bottom":-0.0703125,"right":0.60468750000000004,"top":0.8046875},"atlasBounds":{"left":111.5,"bottom":88.5,"right":150.5,"top":144.5}},{"unicode":58,"advance":0.59999999999999998,"planeBounds":{"left":0.15156250000000002,"bottom":-0.0859375,"right":0.44843749999999999,"top":0.6328125},"atlasBounds":{"left":348.5,"bottom":98.5,"right":367.5,"top":144.5}},{"unicode":59,"advance":0.59999999999999998,"planeBounds":{"left":0.13875000000000001,"bottom":-0.2265625,"right":0.45124999999999998,"top":0.6328125},"atlasBounds":{"left":427.5,"bottom":261.5,"right":447.5,"top":316.5}},{"unicode":60,"advance":0.59999999999999998,"planeBounds":{"left":0.018750000000000024,"bottom":-0.0078125,"right":0.58125000000000004,"top":0.6640625},"atlasBounds":{"left":310.5,"bottom":44.5,"right":346.5,"top":87.5}},{"unicode":61,"advance":0.59999999999999998,"planeBounds":{"left":0.018750000000000024,"bottom":0.1015625,"right":0.58125000000000004,"top":0.5546875},"atlasBounds":{"left":78.5,"bottom":12.5,"right":114.5,"top":41.5}},{"unicode":62,"advance":0.59999999999999998,"planeBounds":{"left":0.018750000000000024,"bottom":-0.0078125,"right":0.58125000000000004,"top":0.6640625},"atlasBounds":{"left":347.5,"bottom":44.5,"right":383.5,"top":87.5}},{"unicode":63,"advance":0.59999999999999998,"planeBounds":{"left":0.065312499999999996,"bottom":-0.0703125,"right":0.5496875,"top":0.8046875},"atlasBounds":{"left":39.5,"bottom":88.5,"right":70.5,"top":144.5}},{"unicode":64,"advance":0.59999999999999998,"planeBounds":{"left":-0.017812499999999992,"bottom":-0.2578125,"right":0.62281249999999999,"top":0.8046875},"atlasBounds":{"left":345.5,"bottom":383.5,"right":386.5,"top":451.5}},{"unicode":65,"advance":0.59999999999999998,"planeBounds":{"left":-0.012500000000000001,"bottom":-0.0703125,"right":0.61250000000000004,"top":0.8046875},"atlasBounds":{"left":401.5,"bottom":145.5,"right":441.5,"top":201.5}},{"unicode":66,"advance":0.59999999999999998,"planeBounds":{"left":0.03025000000000003,"bottom":-0.0703125,"right":0.59275,"top":0.8046875},"atlasBounds":{"left":364.5,"bottom":145.5,"right":400.5,"top":201.5}},{"unicode":67,"advance":0.59999999999999998,"planeBounds":{"left":0.025750000000000026,"bottom":-0.0859375,"right":0.58825000000000005,"top":0.8046875},"atlasBounds":{"left":352.5,"bottom":319.5,"right":388.5,"top":376.5}},{"unicode":68,"advance":0.59999999999999998,"planeBounds":{"left":0.028562500000000022,"bottom":-0.0703125,"right":0.57543750000000005,"top":0.8046875},"atlasBounds":{"left":292.5,"bottom":145.5,"right":327.5,"top":201.5}},{"unicode":69,"advance":0.59999999999999998,"planeBounds":{"left":0.036562500000000026,"bottom":-0.0703125,"right":0.58343750000000005,"top":0.8046875},"atlasBounds":{"left":256.5,"bottom":145.5,"right":291.5,"top":201.5}},{"unicode":70,"advance":0.59999999999999998,"planeBounds":{"left":0.028750000000000026,"bottom":-0.0703125,"right":0.59125000000000005,"top":0.8046875},"atlasBounds":{"left":219.5,"bottom":145.5,"right":255.5,"top":201.5}},{"unicode":71,"advance":0.59999999999999998,"planeBounds":{"left":0.021750000000000026,"bottom":-0.0859375,"right":0.58425000000000005,"top":0.8046875},"atlasBounds":{"left":389.5,"bottom":319.5,"right":425.5,"top":376.5}},{"unicode":72,"advance":0.59999999999999998,"planeBounds":{"left":0.026562500000000017,"bottom":-0.0703125,"right":0.57343750000000004,"top":0.8046875},"atlasBounds":{"left":147.5,"bottom":145.5,"right":182.5,"top":201.5}},{"unicode":73,"advance":0.59999999999999998,"planeBounds":{"left":0.04218750000000001,"bottom":-0.0703125,"right":0.55781250000000004,"top":0.8046875},"atlasBounds":{"left":113.5,"bottom":145.5,"right":146.5,"top":201.5}},{"unicode":74,"advance":0.59999999999999998,"planeBounds":{"left":-0.019062499999999965,"bottom":-0.0859375,"right":0.55906250000000002,"top":0.8046875},"atlasBounds":{"left":72.5,"bottom":259.5,"right":109.5,"top":316.5}},{"unicode":75,"advance":0.59999999999999998,"planeBounds":{"left":0.029124999999999988,"bottom":-0.0703125,"right":0.62287500000000007,"top":0.8046875},"atlasBounds":{"left":36.5,"bottom":145.5,"right":74.5,"top":201.5}},{"unicode":76,"advance":0.59999999999999998,"planeBounds":{"left":0.066562500000000024,"bottom":-0.0703125,"right":0.61343749999999997,"top":0.8046875},"atlasBounds":{"left":0.5,"bottom":145.5,"right":35.5,"top":201.5}},{"unicode":77,"advance":0.59999999999999998,"planeBounds":{"left":0.0031249999999999807,"bottom":-0.0703125,"right":0.59687500000000004,"top":0.8046875},"atlasBounds":{"left":399.5,"bottom":202.5,"right":437.5,"top":258.5}},{"unicode":78,"advance":0.59999999999999998,"planeBounds":{"left":0.026562500000000024,"bottom":-0.0703125,"right":0.57343750000000004,"top":0.8046875},"atlasBounds":{"left":363.5,"bottom":202.5,"right":398.5,"top":258.5}},{"unicode":79,"advance":0.59999999999999998,"planeBounds":{"left":0.01875000000000002,"bottom":-0.0859375,"right":0.58125000000000004,"top":0.8046875},"atlasBounds":{"left":146.5,"bottom":259.5,"right":182.5,"top":316.5}},{"unicode":80,"advance":0.59999999999999998,"planeBounds":{"left":0.024124999999999983,"bottom":-0.0703125,"right":0.61787500000000006,"top":0.8046875},"atlasBounds":{"left":286.5,"bottom":202.5,"right":324.5,"top":258.5}},{"unicode":81,"advance":0.59999999999999998,"planeBounds":{"left":0.013937500000000031,"bottom":-0.2578125,"right":0.59206250000000005,"top":0.8046875},"atlasBounds":{"left":387.5,"bottom":383.5,"right":424.5,"top":451.5}},{"unicode":82,"advance":0.59999999999999998,"planeBounds":{"left":0.029937499999999978,"bottom":-0.0703125,"right":0.60806250000000006,"top":0.8046875},"atlasBounds":{"left":209.5,"bottom":202.5,"right":246.5,"top":258.5}},{"unicode":83,"advance":0.59999999999999998,"planeBounds":{"left":0.0031249999999999807,"bottom":-0.0859375,"right":0.59687500000000004,"top":0.8046875},"atlasBounds":{"left":183.5,"bottom":259.5,"right":221.5,"top":316.5}},{"unicode":84,"advance":0.59999999999999998,"planeBounds":{"left":-0.012500000000000004,"bottom":-0.0703125,"right":0.61250000000000004,"top":0.8046875},"atlasBounds":{"left":125.5,"bottom":202.5,"right":165.5,"top":258.5}},{"unicode":85,"advance":0.59999999999999998,"planeBounds":{"left":0.026562500000000024,"bottom":-0.0859375,"right":0.57343750000000004,"top":0.8046875},"atlasBounds":{"left":222.5,"bottom":259.5,"right":257.5,"top":316.5}},{"unicode":86,"advance":0.59999999999999998,"planeBounds":{"left":-0.012500000000000001,"bottom":-0.0703125,"right":0.61250000000000004,"top":0.8046875},"atlasBounds":{"left":43.5,"bottom":202.5,"right":83.5,"top":258.5}},{"unicode":87,"advance":0.59999999999999998,"planeBounds":{"left":-0.043749999999999969,"bottom":-0.0703125,"right":0.64375000000000004,"top":0.8046875},"atlasBounds":{"left":151.5,"bottom":88.5,"right":195.5,"top":144.5}},{"unicode":88,"advance":0.59999999999999998,"planeBounds":{"left":-0.028124999999999994,"bottom":-0.0703125,"right":0.62812500000000004,"top":0.8046875},"atlasBounds":{"left":0.5,"bottom":202.5,"right":42.5,"top":258.5}},{"unicode":89,"advance":0.59999999999999998,"planeBounds":{"left":-0.028124999999999987,"bottom":-0.0703125,"right":0.62812500000000004,"top":0.8046875},"atlasBounds":{"left":384.5,"bottom":260.5,"right":426.5,"top":316.5}},{"unicode":90,"advance":0.59999999999999998,"planeBounds":{"left":0.018750000000000024,"bottom":-0.0703125,"right":0.58125000000000004,"top":0.8046875},"atlasBounds":{"left":347.5,"bottom":260.5,"right":383.5,"top":316.5}},{"unicode":91,"advance":0.59999999999999998,"planeBounds":{"left":0.14000000000000001,"bottom":-0.1796875,"right":0.51500000000000001,"top":0.8984375},"atlasBounds":{"left":220.5,"bottom":382.5,"right":244.5,"top":451.5}},{"unicode":92,"advance":0.59999999999999998,"planeBounds":{"left":0.010937500000000034,"bottom":-0.1796875,"right":0.58906250000000004,"top":0.8984375},"atlasBounds":{"left":245.5,"bottom":382.5,"right":282.5,"top":451.5}},{"unicode":93,"advance":0.59999999999999998,"planeBounds":{"left":0.085000000000000006,"bottom":-0.1796875,"right":0.46000000000000002,"top":0.8984375},"atlasBounds":{"left":283.5,"bottom":382.5,"right":307.5,"top":451.5}},{"unicode":94,"advance":0.59999999999999998,"planeBounds":{"left":0.010937500000000029,"bottom":0.2734375,"right":0.58906250000000004,"top":0.8046875},"atlasBounds":{"left":40.5,"bottom":7.5,"right":77.5,"top":41.5}},{"unicode":95,"advance":0.59999999999999998,"planeBounds":{"left":-0.0046875000000000094,"bottom":-0.1640625,"right":0.60468750000000004,"top":0.0390625},"atlasBounds":{"left":230.5,"bottom":28.5,"right":269.5,"top":41.5}},{"unicode":96,"advance":0.59999999999999998,"planeBounds":{"left":0.095125000000000015,"bottom":0.5703125,"right":0.43887500000000002,"top":0.8515625},"atlasBounds":{"left":425.5,"bottom":433.5,"right":447.5,"top":451.5}},{"unicode":97,"advance":0.59999999999999998,"planeBounds":{"left":-0.0015624999999999697,"bottom":-0.0859375,"right":0.57656249999999998,"top":0.6328125},"atlasBounds":{"left":310.5,"bottom":98.5,"right":347.5,"top":144.5}},{"unicode":98,"advance":0.59999999999999998,"planeBounds":{"left":0.029062500000000022,"bottom":-0.0859375,"right":0.57593749999999999,"top":0.8046875},"atlasBounds":{"left":36.5,"bottom":259.5,"right":71.5,"top":316.5}},{"unicode":99,"advance":0.59999999999999998,"planeBounds":{"left":0.024250000000000028,"bottom":-0.0859375,"right":0.58674999999999999,"top":0.6328125},"atlasBounds":{"left":236.5,"bottom":98.5,"right":272.5,"top":144.5}},{"unicode":100,"advance":0.59999999999999998,"planeBounds":{"left":0.024062500000000021,"bottom":-0.0859375,"right":0.57093749999999999,"top":0.8046875},"atlasBounds":{"left":0.5,"bottom":259.5,"right":35.5,"top":316.5}},{"unicode":101,"advance":0.59999999999999998,"planeBounds":{"left":0.018750000000000024,"bottom":-0.0859375,"right":0.58125000000000004,"top":0.6328125},"atlasBounds":{"left":405.5,"bottom":98.5,"right":441.5,"top":144.5}},{"unicode":102,"advance":0.59999999999999998,"planeBounds":{"left":-0.012187500000000011,"bottom":-0.0703125,"right":0.59718749999999998,"top":0.8046875},"atlasBounds":{"left":71.5,"bottom":88.5,"right":110.5,"top":144.5}},{"unicode":103,"advance":0.59999999999999998,"planeBounds":{"left":0.024062500000000021,"bottom":-0.2578125,"right":0.57093749999999999,"top":0.6328125},"atlasBounds":{"left":316.5,"bottom":319.5,"right":351.5,"top":376.5}},{"unicode":104,"advance":0.59999999999999998,"planeBounds":{"left":0.027562500000000021,"bottom":-0.0703125,"right":0.57443750000000005,"top":0.8046875},"atlasBounds":{"left":328.5,"bottom":145.5,"right":363.5,"top":201.5}},{"unicode":105,"advance":0.59999999999999998,"planeBounds":{"left":0.015312499999999988,"bottom":-0.0703125,"right":0.62468750000000006,"top":0.8515625},"atlasBounds":{"left":0.5,"bottom":317.5,"right":39.5,"top":376.5}},{"unicode":106,"advance":0.59999999999999998,"planeBounds":{"left":0.017999999999999995,"bottom":-0.2578125,"right":0.51800000000000002,"top":0.8515625},"atlasBounds":{"left":97.5,"bottom":380.5,"right":129.5,"top":451.5}},{"unicode":107,"advance":0.59999999999999998,"planeBounds":{"left":0.031124999999999986,"bottom":-0.0703125,"right":0.62487499999999996,"top":0.8046875},"atlasBounds":{"left":247.5,"bottom":202.5,"right":285.5,"top":258.5}},{"unicode":108,"advance":0.59999999999999998,"planeBounds":{"left":-0.038124999999999992,"bottom":-0.0703125,"right":0.61812500000000004,"top":0.8046875},"atlasBounds":{"left":166.5,"bottom":202.5,"right":208.5,"top":258.5}},{"unicode":109,"advance":0.59999999999999998,"planeBounds":{"left":-0.0046875000000000094,"bottom":-0.0703125,"right":0.60468750000000004,"top":0.6328125},"atlasBounds":{"left":0.5,"bottom":42.5,"right":39.5,"top":87.5}},{"unicode":110,"advance":0.59999999999999998,"planeBounds":{"left":0.027562500000000021,"bottom":-0.0703125,"right":0.57443750000000005,"top":0.6328125},"atlasBounds":{"left":40.5,"bottom":42.5,"right":75.5,"top":87.5}},{"unicode":111,"advance":0.59999999999999998,"planeBounds":{"left":0.018750000000000024,"bottom":-0.0859375,"right":0.58125000000000004,"top":0.6328125},"atlasBounds":{"left":273.5,"bottom":98.5,"right":309.5,"top":144.5}},{"unicode":112,"advance":0.59999999999999998,"planeBounds":{"left":0.029062500000000022,"bottom":-0.2578125,"right":0.57593749999999999,"top":0.6328125},"atlasBounds":{"left":87.5,"bottom":319.5,"right":122.5,"top":376.5}},{"unicode":113,"advance":0.59999999999999998,"planeBounds":{"left":0.024062500000000021,"bottom":-0.2578125,"right":0.57093749999999999,"top":0.6328125},"atlasBounds":{"left":110.5,"bottom":259.5,"right":145.5,"top":316.5}},{"unicode":114,"advance":0.59999999999999998,"planeBounds":{"left":0.047562500000000021,"bottom":-0.0703125,"right":0.59443750000000006,"top":0.6328125},"atlasBounds":{"left":112.5,"bottom":42.5,"right":147.5,"top":87.5}},{"unicode":115,"advance":0.59999999999999998,"planeBounds":{"left":0.018750000000000024,"bottom":-0.0859375,"right":0.58125000000000004,"top":0.6328125},"atlasBounds":{"left":368.5,"bottom":98.5,"right":404.5,"top":144.5}},{"unicode":116,"advance":0.59999999999999998,"planeBounds":{"left":-0.021187500000000015,"bottom":-0.0703125,"right":0.58818749999999997,"top":0.7734375},"atlasBounds":{"left":196.5,"bottom":90.5,"right":235.5,"top":144.5}},{"unicode":117,"advance":0.59999999999999998,"planeBounds":{"left":0.026562500000000024,"bottom":-0.0859375,"right":0.57343750000000004,"top":0.6171875},"atlasBounds":{"left":76.5,"bottom":42.5,"right":111.5,"top":87.5}},{"unicode":118,"advance":0.59999999999999998,"planeBounds":{"left":-0.012500000000000004,"bottom":-0.0703125,"right":0.61250000000000004,"top":0.6171875},"atlasBounds":{"left":233.5,"bottom":43.5,"right":273.5,"top":87.5}},{"unicode":119,"advance":0.59999999999999998,"planeBounds":{"left":-0.035937499999999983,"bottom":-0.0703125,"right":0.63593750000000004,"top":0.6171875},"atlasBounds":{"left":148.5,"bottom":43.5,"right":191.5,"top":87.5}},{"unicode":120,"advance":0.59999999999999998,"planeBounds":{"left":-0.012500000000000001,"bottom":-0.0703125,"right":0.61250000000000004,"top":0.6171875},"atlasBounds":{"left":192.5,"bottom":43.5,"right":232.5,"top":87.5}},{"unicode":121,"advance":0.59999999999999998,"planeBounds":{"left":-0.012500000000000004,"bottom":-0.2578125,"right":0.61250000000000004,"top":0.6171875},"atlasBounds":{"left":84.5,"bottom":202.5,"right":124.5,"top":258.5}},{"unicode":122,"advance":0.59999999999999998,"planeBounds":{"left":0.026562500000000024,"bottom":-0.0703125,"right":0.57343750000000004,"top":0.6171875},"atlasBounds":{"left":274.5,"bottom":43.5,"right":309.5,"top":87.5}},{"unicode":123,"advance":0.59999999999999998,"planeBounds":{"left":0.0087500000000000251,"bottom":-0.1796875,"right":0.57125000000000004,"top":0.8984375},"atlasBounds":{"left":308.5,"bottom":382.5,"right":344.5,"top":451.5}},{"unicode":124,"advance":0.59999999999999998,"planeBounds":{"left":0.19062500000000002,"bottom":-0.1796875,"right":0.40937499999999999,"top":0.8984375},"atlasBounds":{"left":167.5,"bottom":382.5,"right":181.5,"top":451.5}},{"unicode":125,"advance":0.59999999999999998,"planeBounds":{"left":0.028750000000000026,"bottom":-0.1796875,"right":0.59125000000000005,"top":0.8984375},"atlasBounds":{"left":130.5,"bottom":382.5,"right":166.5,"top":451.5}},{"unicode":126,"advance":0.59999999999999998,"planeBounds":{"left":0.0031249999999999824,"bottom":0.1796875,"right":0.59687500000000004,"top":0.5234375},"atlasBounds":{"left":161.5,"bottom":19.5,"right":199.5,"top":41.5}}],"kerning":[]} diff --git a/resources/public/font_atlas.png b/resources/public/font_atlas.png index 88c88b8..6636857 100644 Binary files a/resources/public/font_atlas.png and b/resources/public/font_atlas.png differ diff --git a/resources/public/font_atlas.png.bak b/resources/public/font_atlas.png.bak new file mode 100644 index 0000000..008c20a Binary files /dev/null and b/resources/public/font_atlas.png.bak differ diff --git a/resources/public/fonts/dejavu_sans_mono.ttf b/resources/public/fonts/dejavu_sans_mono.ttf new file mode 100644 index 0000000..538ee27 Binary files /dev/null and b/resources/public/fonts/dejavu_sans_mono.ttf differ diff --git a/resources/public/fonts/dejavu_sans_mono_atlas.json b/resources/public/fonts/dejavu_sans_mono_atlas.json new file mode 100644 index 0000000..636350a --- /dev/null +++ b/resources/public/fonts/dejavu_sans_mono_atlas.json @@ -0,0 +1 @@ +{"atlas":{"type":"msdf","distanceRange":16,"distanceRangeMiddle":0,"size":64,"width":1024,"height":1024,"yOrigin":"bottom"},"metrics":{"emSize":1,"lineHeight":1.1640625,"ascender":0.92822265625,"descender":-0.23583984375,"underlineY":-0.04150390625,"underlineThickness":0.0439453125},"glyphs":[{"unicode":32,"advance":0.60205078125},{"unicode":33,"advance":0.60205078125,"planeBounds":{"left":0.121826171875,"bottom":-0.1328125,"right":0.481201171875,"top":0.8671875},"atlasBounds":{"left":672.5,"bottom":877.5,"right":695.5,"top":941.5}},{"unicode":34,"advance":0.60205078125,"planeBounds":{"left":0.035400390625,"bottom":0.3203125,"right":0.566650390625,"top":0.8671875},"atlasBounds":{"left":806.5,"bottom":775.5,"right":840.5,"top":810.5}},{"unicode":35,"advance":0.60205078125,"planeBounds":{"left":-0.129150390625,"bottom":-0.1328125,"right":0.730224609375,"top":0.8515625},"atlasBounds":{"left":810.5,"bottom":812.5,"right":865.5,"top":875.5}},{"unicode":36,"advance":0.60205078125,"planeBounds":{"left":-0.033203125,"bottom":-0.2734375,"right":0.669921875,"top":0.8984375},"atlasBounds":{"left":143.5,"bottom":948.5,"right":188.5,"top":1023.5}},{"unicode":37,"advance":0.60205078125,"planeBounds":{"left":-0.113037109375,"bottom":-0.1328125,"right":0.715087890625,"top":0.8359375},"atlasBounds":{"left":912.5,"bottom":813.5,"right":965.5,"top":875.5}},{"unicode":38,"advance":0.60205078125,"planeBounds":{"left":-0.10205078125,"bottom":-0.1484375,"right":0.72607421875,"top":0.8671875},"atlasBounds":{"left":0.5,"bottom":876.5,"right":53.5,"top":941.5}},{"unicode":39,"advance":0.60205078125,"planeBounds":{"left":0.12841796875,"bottom":0.3203125,"right":0.47216796875,"top":0.8671875},"atlasBounds":{"left":972.5,"bottom":775.5,"right":994.5,"top":810.5}},{"unicode":40,"advance":0.60205078125,"planeBounds":{"left":0.077880859375,"bottom":-0.2578125,"right":0.562255859375,"top":0.8984375},"atlasBounds":{"left":189.5,"bottom":949.5,"right":220.5,"top":1023.5}},{"unicode":41,"advance":0.60205078125,"planeBounds":{"left":0.039794921875,"bottom":-0.2578125,"right":0.524169921875,"top":0.8984375},"atlasBounds":{"left":221.5,"bottom":949.5,"right":252.5,"top":1023.5}},{"unicode":42,"advance":0.60205078125,"planeBounds":{"left":-0.050537109375,"bottom":0.1484375,"right":0.652587890625,"top":0.8671875},"atlasBounds":{"left":760.5,"bottom":764.5,"right":805.5,"top":810.5}},{"unicode":43,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":-0.0703125,"right":0.691650390625,"top":0.7109375},"atlasBounds":{"left":607.5,"bottom":760.5,"right":657.5,"top":810.5}},{"unicode":44,"advance":0.60205078125,"planeBounds":{"left":0.071533203125,"bottom":-0.2734375,"right":0.493408203125,"top":0.2734375},"atlasBounds":{"left":944.5,"bottom":775.5,"right":971.5,"top":810.5}},{"unicode":45,"advance":0.60205078125,"planeBounds":{"left":0.043212890625,"bottom":0.1015625,"right":0.558837890625,"top":0.4453125},"atlasBounds":{"left":83.5,"bottom":728.5,"right":116.5,"top":750.5}},{"unicode":46,"advance":0.60205078125,"planeBounds":{"left":0.11279296875,"bottom":-0.1328125,"right":0.48779296875,"top":0.2890625},"atlasBounds":{"left":995.5,"bottom":783.5,"right":1019.5,"top":810.5}},{"unicode":47,"advance":0.60205078125,"planeBounds":{"left":-0.078857421875,"bottom":-0.2265625,"right":0.655517578125,"top":0.8671875},"atlasBounds":{"left":417.5,"bottom":953.5,"right":464.5,"top":1023.5}},{"unicode":48,"advance":0.60205078125,"planeBounds":{"left":-0.066162109375,"bottom":-0.1484375,"right":0.668212890625,"top":0.8671875},"atlasBounds":{"left":104.5,"bottom":876.5,"right":151.5,"top":941.5}},{"unicode":49,"advance":0.60205078125,"planeBounds":{"left":-0.0087890625,"bottom":-0.1328125,"right":0.6630859375,"top":0.8671875},"atlasBounds":{"left":913.5,"bottom":877.5,"right":956.5,"top":941.5}},{"unicode":50,"advance":0.60205078125,"planeBounds":{"left":-0.055908203125,"bottom":-0.1328125,"right":0.647216796875,"top":0.8671875},"atlasBounds":{"left":52.5,"bottom":811.5,"right":97.5,"top":875.5}},{"unicode":51,"advance":0.60205078125,"planeBounds":{"left":-0.0625,"bottom":-0.1484375,"right":0.65625,"top":0.8671875},"atlasBounds":{"left":152.5,"bottom":876.5,"right":198.5,"top":941.5}},{"unicode":52,"advance":0.60205078125,"planeBounds":{"left":-0.080810546875,"bottom":-0.1328125,"right":0.684814453125,"top":0.8671875},"atlasBounds":{"left":145.5,"bottom":811.5,"right":194.5,"top":875.5}},{"unicode":53,"advance":0.60205078125,"planeBounds":{"left":-0.0556640625,"bottom":-0.1484375,"right":0.6474609375,"top":0.8671875},"atlasBounds":{"left":199.5,"bottom":876.5,"right":244.5,"top":941.5}},{"unicode":54,"advance":0.60205078125,"planeBounds":{"left":-0.066162109375,"bottom":-0.1484375,"right":0.668212890625,"top":0.8671875},"atlasBounds":{"left":245.5,"bottom":876.5,"right":292.5,"top":941.5}},{"unicode":55,"advance":0.60205078125,"planeBounds":{"left":-0.06201171875,"bottom":-0.1328125,"right":0.65673828125,"top":0.8671875},"atlasBounds":{"left":390.5,"bottom":811.5,"right":436.5,"top":875.5}},{"unicode":56,"advance":0.60205078125,"planeBounds":{"left":-0.066162109375,"bottom":-0.1484375,"right":0.668212890625,"top":0.8671875},"atlasBounds":{"left":293.5,"bottom":876.5,"right":340.5,"top":941.5}},{"unicode":57,"advance":0.60205078125,"planeBounds":{"left":-0.069091796875,"bottom":-0.1484375,"right":0.665283203125,"top":0.8671875},"atlasBounds":{"left":341.5,"bottom":876.5,"right":388.5,"top":941.5}},{"unicode":58,"advance":0.60205078125,"planeBounds":{"left":0.11279296875,"bottom":-0.1328125,"right":0.48779296875,"top":0.6484375},"atlasBounds":{"left":582.5,"bottom":760.5,"right":606.5,"top":810.5}},{"unicode":59,"advance":0.60205078125,"planeBounds":{"left":0.071533203125,"bottom":-0.2734375,"right":0.493408203125,"top":0.6484375},"atlasBounds":{"left":0.5,"bottom":751.5,"right":27.5,"top":810.5}},{"unicode":60,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":-0.0703125,"right":0.691650390625,"top":0.6953125},"atlasBounds":{"left":658.5,"bottom":761.5,"right":708.5,"top":810.5}},{"unicode":61,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":0.0390625,"right":0.691650390625,"top":0.5859375},"atlasBounds":{"left":893.5,"bottom":775.5,"right":943.5,"top":810.5}},{"unicode":62,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":-0.0703125,"right":0.691650390625,"top":0.6953125},"atlasBounds":{"left":709.5,"bottom":761.5,"right":759.5,"top":810.5}},{"unicode":63,"advance":0.60205078125,"planeBounds":{"left":-0.0068359375,"bottom":-0.1328125,"right":0.6337890625,"top":0.8671875},"atlasBounds":{"left":527.5,"bottom":811.5,"right":568.5,"top":875.5}},{"unicode":64,"advance":0.60205078125,"planeBounds":{"left":-0.112060546875,"bottom":-0.2890625,"right":0.700439453125,"top":0.8203125},"atlasBounds":{"left":364.5,"bottom":952.5,"right":416.5,"top":1023.5}},{"unicode":65,"advance":0.60205078125,"planeBounds":{"left":-0.113037109375,"bottom":-0.1328125,"right":0.715087890625,"top":0.8671875},"atlasBounds":{"left":708.5,"bottom":811.5,"right":761.5,"top":875.5}},{"unicode":66,"advance":0.60205078125,"planeBounds":{"left":-0.049072265625,"bottom":-0.1328125,"right":0.685302734375,"top":0.8671875},"atlasBounds":{"left":762.5,"bottom":811.5,"right":809.5,"top":875.5}},{"unicode":67,"advance":0.60205078125,"planeBounds":{"left":-0.0634765625,"bottom":-0.1484375,"right":0.6552734375,"top":0.8671875},"atlasBounds":{"left":389.5,"bottom":876.5,"right":435.5,"top":941.5}},{"unicode":68,"advance":0.60205078125,"planeBounds":{"left":-0.063720703125,"bottom":-0.1328125,"right":0.670654296875,"top":0.8671875},"atlasBounds":{"left":660.5,"bottom":811.5,"right":707.5,"top":875.5}},{"unicode":69,"advance":0.60205078125,"planeBounds":{"left":-0.034423828125,"bottom":-0.1328125,"right":0.668701171875,"top":0.8671875},"atlasBounds":{"left":614.5,"bottom":811.5,"right":659.5,"top":875.5}},{"unicode":70,"advance":0.60205078125,"planeBounds":{"left":-0.015380859375,"bottom":-0.1328125,"right":0.672119140625,"top":0.8671875},"atlasBounds":{"left":569.5,"bottom":811.5,"right":613.5,"top":875.5}},{"unicode":71,"advance":0.60205078125,"planeBounds":{"left":-0.08056640625,"bottom":-0.1484375,"right":0.66943359375,"top":0.8671875},"atlasBounds":{"left":436.5,"bottom":876.5,"right":484.5,"top":941.5}},{"unicode":72,"advance":0.60205078125,"planeBounds":{"left":-0.058349609375,"bottom":-0.1328125,"right":0.660400390625,"top":0.8671875},"atlasBounds":{"left":480.5,"bottom":811.5,"right":526.5,"top":875.5}},{"unicode":73,"advance":0.60205078125,"planeBounds":{"left":-0.027587890625,"bottom":-0.1328125,"right":0.628662109375,"top":0.8671875},"atlasBounds":{"left":437.5,"bottom":811.5,"right":479.5,"top":875.5}},{"unicode":74,"advance":0.60205078125,"planeBounds":{"left":-0.075927734375,"bottom":-0.1484375,"right":0.595947265625,"top":0.8671875},"atlasBounds":{"left":485.5,"bottom":876.5,"right":528.5,"top":941.5}},{"unicode":75,"advance":0.60205078125,"planeBounds":{"left":-0.05810546875,"bottom":-0.1328125,"right":0.72314453125,"top":0.8671875},"atlasBounds":{"left":339.5,"bottom":811.5,"right":389.5,"top":875.5}},{"unicode":76,"advance":0.60205078125,"planeBounds":{"left":-0.02099609375,"bottom":-0.1328125,"right":0.68212890625,"top":0.8671875},"atlasBounds":{"left":293.5,"bottom":811.5,"right":338.5,"top":875.5}},{"unicode":77,"advance":0.60205078125,"planeBounds":{"left":-0.090087890625,"bottom":-0.1328125,"right":0.691162109375,"top":0.8671875},"atlasBounds":{"left":242.5,"bottom":811.5,"right":292.5,"top":875.5}},{"unicode":78,"advance":0.60205078125,"planeBounds":{"left":-0.058349609375,"bottom":-0.1328125,"right":0.660400390625,"top":0.8671875},"atlasBounds":{"left":195.5,"bottom":811.5,"right":241.5,"top":875.5}},{"unicode":79,"advance":0.60205078125,"planeBounds":{"left":-0.073974609375,"bottom":-0.1484375,"right":0.676025390625,"top":0.8671875},"atlasBounds":{"left":529.5,"bottom":876.5,"right":577.5,"top":941.5}},{"unicode":80,"advance":0.60205078125,"planeBounds":{"left":-0.03271484375,"bottom":-0.1328125,"right":0.68603515625,"top":0.8671875},"atlasBounds":{"left":98.5,"bottom":811.5,"right":144.5,"top":875.5}},{"unicode":81,"advance":0.60205078125,"planeBounds":{"left":-0.073974609375,"bottom":-0.2578125,"right":0.676025390625,"top":0.8671875},"atlasBounds":{"left":315.5,"bottom":951.5,"right":363.5,"top":1023.5}},{"unicode":82,"advance":0.60205078125,"planeBounds":{"left":-0.0625,"bottom":-0.1328125,"right":0.734375,"top":0.8671875},"atlasBounds":{"left":0.5,"bottom":811.5,"right":51.5,"top":875.5}},{"unicode":83,"advance":0.60205078125,"planeBounds":{"left":-0.057373046875,"bottom":-0.1484375,"right":0.661376953125,"top":0.8671875},"atlasBounds":{"left":578.5,"bottom":876.5,"right":624.5,"top":941.5}},{"unicode":84,"advance":0.60205078125,"planeBounds":{"left":-0.105224609375,"bottom":-0.1328125,"right":0.707275390625,"top":0.8671875},"atlasBounds":{"left":971.5,"bottom":959.5,"right":1023.5,"top":1023.5}},{"unicode":85,"advance":0.60205078125,"planeBounds":{"left":-0.05859375,"bottom":-0.1484375,"right":0.66015625,"top":0.8671875},"atlasBounds":{"left":625.5,"bottom":876.5,"right":671.5,"top":941.5}},{"unicode":86,"advance":0.60205078125,"planeBounds":{"left":-0.097412109375,"bottom":-0.1328125,"right":0.699462890625,"top":0.8671875},"atlasBounds":{"left":807.5,"bottom":877.5,"right":858.5,"top":941.5}},{"unicode":87,"advance":0.60205078125,"planeBounds":{"left":-0.128662109375,"bottom":-0.1328125,"right":0.730712890625,"top":0.8671875},"atlasBounds":{"left":751.5,"bottom":877.5,"right":806.5,"top":941.5}},{"unicode":88,"advance":0.60205078125,"planeBounds":{"left":-0.12109375,"bottom":-0.1328125,"right":0.72265625,"top":0.8671875},"atlasBounds":{"left":696.5,"bottom":877.5,"right":750.5,"top":941.5}},{"unicode":89,"advance":0.60205078125,"planeBounds":{"left":-0.113037109375,"bottom":-0.1328125,"right":0.715087890625,"top":0.8671875},"atlasBounds":{"left":859.5,"bottom":877.5,"right":912.5,"top":941.5}},{"unicode":90,"advance":0.60205078125,"planeBounds":{"left":-0.051513671875,"bottom":-0.1328125,"right":0.698486328125,"top":0.8671875},"atlasBounds":{"left":957.5,"bottom":877.5,"right":1005.5,"top":941.5}},{"unicode":91,"advance":0.60205078125,"planeBounds":{"left":0.09521484375,"bottom":-0.2578125,"right":0.56396484375,"top":0.8984375},"atlasBounds":{"left":253.5,"bottom":949.5,"right":283.5,"top":1023.5}},{"unicode":92,"advance":0.60205078125,"planeBounds":{"left":-0.078857421875,"bottom":-0.2265625,"right":0.655517578125,"top":0.8671875},"atlasBounds":{"left":465.5,"bottom":953.5,"right":512.5,"top":1023.5}},{"unicode":93,"advance":0.60205078125,"planeBounds":{"left":0.0380859375,"bottom":-0.2578125,"right":0.5068359375,"top":0.8984375},"atlasBounds":{"left":284.5,"bottom":949.5,"right":314.5,"top":1023.5}},{"unicode":94,"advance":0.60205078125,"planeBounds":{"left":-0.097412109375,"bottom":0.3203125,"right":0.699462890625,"top":0.8671875},"atlasBounds":{"left":841.5,"bottom":775.5,"right":892.5,"top":810.5}},{"unicode":95,"advance":0.60205078125,"planeBounds":{"left":-0.128662109375,"bottom":-0.3671875,"right":0.730712890625,"top":-0.0703125},"atlasBounds":{"left":117.5,"bottom":731.5,"right":172.5,"top":750.5}},{"unicode":96,"advance":0.60205078125,"planeBounds":{"left":0.010986328125,"bottom":0.4765625,"right":0.495361328125,"top":0.9296875},"atlasBounds":{"left":0.5,"bottom":721.5,"right":31.5,"top":750.5}},{"unicode":97,"advance":0.60205078125,"planeBounds":{"left":-0.060546875,"bottom":-0.1484375,"right":0.642578125,"top":0.6953125},"atlasBounds":{"left":211.5,"bottom":756.5,"right":256.5,"top":810.5}},{"unicode":98,"advance":0.60205078125,"planeBounds":{"left":-0.032958984375,"bottom":-0.1484375,"right":0.670166015625,"top":0.8984375},"atlasBounds":{"left":513.5,"bottom":956.5,"right":558.5,"top":1023.5}},{"unicode":99,"advance":0.60205078125,"planeBounds":{"left":-0.037109375,"bottom":-0.1484375,"right":0.650390625,"top":0.6953125},"atlasBounds":{"left":166.5,"bottom":756.5,"right":210.5,"top":810.5}},{"unicode":100,"advance":0.60205078125,"planeBounds":{"left":-0.067138671875,"bottom":-0.1484375,"right":0.635986328125,"top":0.8984375},"atlasBounds":{"left":559.5,"bottom":956.5,"right":604.5,"top":1023.5}},{"unicode":101,"advance":0.60205078125,"planeBounds":{"left":-0.065673828125,"bottom":-0.1484375,"right":0.668701171875,"top":0.6953125},"atlasBounds":{"left":75.5,"bottom":756.5,"right":122.5,"top":810.5}},{"unicode":102,"advance":0.60205078125,"planeBounds":{"left":-0.03662109375,"bottom":-0.1328125,"right":0.65087890625,"top":0.8984375},"atlasBounds":{"left":743.5,"bottom":957.5,"right":787.5,"top":1023.5}},{"unicode":103,"advance":0.60205078125,"planeBounds":{"left":-0.067138671875,"bottom":-0.3515625,"right":0.635986328125,"top":0.6953125},"atlasBounds":{"left":605.5,"bottom":956.5,"right":650.5,"top":1023.5}},{"unicode":104,"advance":0.60205078125,"planeBounds":{"left":-0.03173828125,"bottom":-0.1328125,"right":0.64013671875,"top":0.8984375},"atlasBounds":{"left":881.5,"bottom":957.5,"right":924.5,"top":1023.5}},{"unicode":105,"advance":0.60205078125,"planeBounds":{"left":-0.04150390625,"bottom":-0.1328125,"right":0.66162109375,"top":0.8984375},"atlasBounds":{"left":925.5,"bottom":957.5,"right":970.5,"top":1023.5}},{"unicode":106,"advance":0.60205078125,"planeBounds":{"left":-0.03662109375,"bottom":-0.3359375,"right":0.51025390625,"top":0.8984375},"atlasBounds":{"left":23.5,"bottom":944.5,"right":58.5,"top":1023.5}},{"unicode":107,"advance":0.60205078125,"planeBounds":{"left":-0.01611328125,"bottom":-0.1328125,"right":0.71826171875,"top":0.8984375},"atlasBounds":{"left":833.5,"bottom":957.5,"right":880.5,"top":1023.5}},{"unicode":108,"advance":0.60205078125,"planeBounds":{"left":-0.05224609375,"bottom":-0.1328125,"right":0.63525390625,"top":0.8984375},"atlasBounds":{"left":788.5,"bottom":957.5,"right":832.5,"top":1023.5}},{"unicode":109,"advance":0.60205078125,"planeBounds":{"left":-0.0791015625,"bottom":-0.1328125,"right":0.6865234375,"top":0.6953125},"atlasBounds":{"left":257.5,"bottom":757.5,"right":306.5,"top":810.5}},{"unicode":110,"advance":0.60205078125,"planeBounds":{"left":-0.03173828125,"bottom":-0.1328125,"right":0.64013671875,"top":0.6953125},"atlasBounds":{"left":351.5,"bottom":757.5,"right":394.5,"top":810.5}},{"unicode":111,"advance":0.60205078125,"planeBounds":{"left":-0.058349609375,"bottom":-0.1484375,"right":0.660400390625,"top":0.6953125},"atlasBounds":{"left":28.5,"bottom":756.5,"right":74.5,"top":810.5}},{"unicode":112,"advance":0.60205078125,"planeBounds":{"left":-0.03466796875,"bottom":-0.3359375,"right":0.66845703125,"top":0.6953125},"atlasBounds":{"left":697.5,"bottom":957.5,"right":742.5,"top":1023.5}},{"unicode":113,"advance":0.60205078125,"planeBounds":{"left":-0.060546875,"bottom":-0.3359375,"right":0.642578125,"top":0.6953125},"atlasBounds":{"left":651.5,"bottom":957.5,"right":696.5,"top":1023.5}},{"unicode":114,"advance":0.60205078125,"planeBounds":{"left":0.050048828125,"bottom":-0.1328125,"right":0.690673828125,"top":0.6953125},"atlasBounds":{"left":395.5,"bottom":757.5,"right":436.5,"top":810.5}},{"unicode":115,"advance":0.60205078125,"planeBounds":{"left":-0.024658203125,"bottom":-0.1484375,"right":0.631591796875,"top":0.6953125},"atlasBounds":{"left":123.5,"bottom":756.5,"right":165.5,"top":810.5}},{"unicode":116,"advance":0.60205078125,"planeBounds":{"left":-0.067626953125,"bottom":-0.1328125,"right":0.635498046875,"top":0.8359375},"atlasBounds":{"left":866.5,"bottom":813.5,"right":911.5,"top":875.5}},{"unicode":117,"advance":0.60205078125,"planeBounds":{"left":-0.03173828125,"bottom":-0.1484375,"right":0.64013671875,"top":0.6796875},"atlasBounds":{"left":307.5,"bottom":757.5,"right":350.5,"top":810.5}},{"unicode":118,"advance":0.60205078125,"planeBounds":{"left":-0.081787109375,"bottom":-0.1328125,"right":0.683837890625,"top":0.6796875},"atlasBounds":{"left":532.5,"bottom":758.5,"right":581.5,"top":810.5}},{"unicode":119,"advance":0.60205078125,"planeBounds":{"left":-0.128662109375,"bottom":-0.1328125,"right":0.730712890625,"top":0.6796875},"atlasBounds":{"left":966.5,"bottom":823.5,"right":1021.5,"top":875.5}},{"unicode":120,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":-0.1328125,"right":0.691650390625,"top":0.6796875},"atlasBounds":{"left":481.5,"bottom":758.5,"right":531.5,"top":810.5}},{"unicode":121,"advance":0.60205078125,"planeBounds":{"left":-0.075927734375,"bottom":-0.3359375,"right":0.689697265625,"top":0.6796875},"atlasBounds":{"left":54.5,"bottom":876.5,"right":103.5,"top":941.5}},{"unicode":122,"advance":0.60205078125,"planeBounds":{"left":-0.032470703125,"bottom":-0.1328125,"right":0.639404296875,"top":0.6796875},"atlasBounds":{"left":437.5,"bottom":758.5,"right":480.5,"top":810.5}},{"unicode":123,"advance":0.60205078125,"planeBounds":{"left":-0.019287109375,"bottom":-0.2890625,"right":0.621337890625,"top":0.8984375},"atlasBounds":{"left":101.5,"bottom":947.5,"right":142.5,"top":1023.5}},{"unicode":124,"advance":0.60205078125,"planeBounds":{"left":0.12890625,"bottom":-0.3671875,"right":0.47265625,"top":0.8984375},"atlasBounds":{"left":0.5,"bottom":942.5,"right":22.5,"top":1023.5}},{"unicode":125,"advance":0.60205078125,"planeBounds":{"left":-0.019287109375,"bottom":-0.2890625,"right":0.621337890625,"top":0.8984375},"atlasBounds":{"left":59.5,"bottom":947.5,"right":100.5,"top":1023.5}},{"unicode":126,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":0.1015625,"right":0.691650390625,"top":0.5078125},"atlasBounds":{"left":32.5,"bottom":724.5,"right":82.5,"top":750.5}}],"kerning":[]} diff --git a/resources/public/fonts/dejavu_sans_mono_atlas.png b/resources/public/fonts/dejavu_sans_mono_atlas.png new file mode 100644 index 0000000..72280d5 Binary files /dev/null and b/resources/public/fonts/dejavu_sans_mono_atlas.png differ diff --git a/resources/public/fonts/dejavu_sans_mono_slug_band.bin b/resources/public/fonts/dejavu_sans_mono_slug_band.bin new file mode 100644 index 0000000..9bf4f60 Binary files /dev/null and b/resources/public/fonts/dejavu_sans_mono_slug_band.bin differ diff --git a/resources/public/fonts/dejavu_sans_mono_slug_curve.bin b/resources/public/fonts/dejavu_sans_mono_slug_curve.bin new file mode 100644 index 0000000..6ebc532 Binary files /dev/null and b/resources/public/fonts/dejavu_sans_mono_slug_curve.bin differ diff --git a/resources/public/fonts/dejavu_sans_mono_slug_meta.json b/resources/public/fonts/dejavu_sans_mono_slug_meta.json new file mode 100644 index 0000000..e20843c --- /dev/null +++ b/resources/public/fonts/dejavu_sans_mono_slug_meta.json @@ -0,0 +1 @@ +{"version":1,"metrics":{"emSize":1,"lineHeight":1.1640625,"ascender":0.92822265625,"descender":-0.23583984375,"underlineY":-0.04150390625,"underlineThickness":0.0439453125},"curveTexture":{"width":4096,"height":1,"format":"rgba16float"},"bandTexture":{"width":4096,"height":2,"format":"rg16uint"},"glyphs":[{"unicode":32,"advance":0.60205078125,"planeBounds":null,"sampleBounds":null,"slug":{"glyphLoc":{"x":0,"y":0},"curveLoc":{"x":0,"y":0},"banding":{"scaleX":0.0,"scaleY":0.0,"offsetX":0.0,"offsetY":0.0},"bandMax":{"x":0,"y":0},"packedBandMeta":0}},{"unicode":33,"advance":0.60205078125,"planeBounds":{"left":0.121826171875,"bottom":-0.1328125,"right":0.481201171875,"top":0.8671875},"sampleBounds":{"left":0.25,"right":0.34375,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":2,"y":0},"curveLoc":{"x":0,"y":0},"banding":{"scaleX":10.666666666666666,"scaleY":2.723404255319149,"offsetX":-2.6666666666666665,"offsetY":-0.0},"bandMax":{"x":0,"y":1},"packedBandMeta":1}},{"unicode":34,"advance":0.60205078125,"planeBounds":{"left":0.035400390625,"bottom":0.3203125,"right":0.566650390625,"top":0.8671875},"sampleBounds":{"left":0.171875,"right":0.4375,"top":0.734375,"bottom":0.453125},"slug":{"glyphLoc":{"x":18,"y":0},"curveLoc":{"x":20,"y":0},"banding":{"scaleX":7.529411764705882,"scaleY":3.5555555555555554,"offsetX":-1.2941176470588236,"offsetY":-1.611111111111111},"bandMax":{"x":1,"y":0},"packedBandMeta":0}},{"unicode":35,"advance":0.60205078125,"planeBounds":{"left":-0.129150390625,"bottom":-0.1328125,"right":0.730224609375,"top":0.8515625},"sampleBounds":{"left":0.0,"right":0.59375,"top":0.71875,"bottom":0.0},"slug":{"glyphLoc":{"x":29,"y":0},"curveLoc":{"x":36,"y":0},"banding":{"scaleX":43.78947368421053,"scaleY":8.347826086956522,"offsetX":-0.0,"offsetY":-0.0},"bandMax":{"x":25,"y":5},"packedBandMeta":5}},{"unicode":36,"advance":0.60205078125,"planeBounds":{"left":-0.033203125,"bottom":-0.2734375,"right":0.669921875,"top":0.8984375},"sampleBounds":{"left":0.09375,"right":0.546875,"top":0.765625,"bottom":-0.140625},"slug":{"glyphLoc":{"x":181,"y":0},"curveLoc":{"x":100,"y":0},"banding":{"scaleX":30.896551724137932,"scaleY":18.75862068965517,"offsetX":-2.896551724137931,"offsetY":2.6379310344827585},"bandMax":{"x":13,"y":16},"packedBandMeta":16}},{"unicode":37,"advance":0.60205078125,"planeBounds":{"left":-0.113037109375,"bottom":-0.1328125,"right":0.715087890625,"top":0.8359375},"sampleBounds":{"left":0.015625,"right":0.59375,"top":0.703125,"bottom":0.0},"slug":{"glyphLoc":{"x":385,"y":0},"curveLoc":{"x":172,"y":0},"banding":{"scaleX":22.486486486486488,"scaleY":27.022222222222222,"offsetX":-0.35135135135135137,"offsetY":-0.0},"bandMax":{"x":12,"y":18},"packedBandMeta":18}},{"unicode":38,"advance":0.60205078125,"planeBounds":{"left":-0.10205078125,"bottom":-0.1484375,"right":0.72607421875,"top":0.8671875},"sampleBounds":{"left":0.03125,"right":0.59375,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":632,"y":0},"curveLoc":{"x":252,"y":0},"banding":{"scaleX":26.666666666666668,"scaleY":16.979591836734695,"offsetX":-0.8333333333333334,"offsetY":0.2653061224489796},"bandMax":{"x":14,"y":12},"packedBandMeta":12}},{"unicode":39,"advance":0.60205078125,"planeBounds":{"left":0.12841796875,"bottom":0.3203125,"right":0.47216796875,"top":0.8671875},"sampleBounds":{"left":0.265625,"right":0.34375,"top":0.734375,"bottom":0.453125},"slug":{"glyphLoc":{"x":841,"y":0},"curveLoc":{"x":332,"y":0},"banding":{"scaleX":12.8,"scaleY":3.5555555555555554,"offsetX":-3.4000000000000004,"offsetY":-1.611111111111111},"bandMax":{"x":0,"y":0},"packedBandMeta":0}},{"unicode":40,"advance":0.60205078125,"planeBounds":{"left":0.077880859375,"bottom":-0.2578125,"right":0.562255859375,"top":0.8984375},"sampleBounds":{"left":0.203125,"right":0.4375,"top":0.765625,"bottom":-0.125},"slug":{"glyphLoc":{"x":847,"y":0},"curveLoc":{"x":340,"y":0},"banding":{"scaleX":21.333333333333332,"scaleY":3.3684210526315788,"offsetX":-4.333333333333333,"offsetY":0.42105263157894735},"bandMax":{"x":4,"y":2},"packedBandMeta":2}},{"unicode":41,"advance":0.60205078125,"planeBounds":{"left":0.039794921875,"bottom":-0.2578125,"right":0.524169921875,"top":0.8984375},"sampleBounds":{"left":0.171875,"right":0.390625,"top":0.765625,"bottom":-0.125},"slug":{"glyphLoc":{"x":891,"y":0},"curveLoc":{"x":360,"y":0},"banding":{"scaleX":22.857142857142858,"scaleY":3.3684210526315788,"offsetX":-3.928571428571429,"offsetY":0.42105263157894735},"bandMax":{"x":4,"y":2},"packedBandMeta":2}},{"unicode":42,"advance":0.60205078125,"planeBounds":{"left":-0.050537109375,"bottom":0.1484375,"right":0.652587890625,"top":0.8671875},"sampleBounds":{"left":0.078125,"right":0.515625,"top":0.75,"bottom":0.28125},"slug":{"glyphLoc":{"x":933,"y":0},"curveLoc":{"x":380,"y":0},"banding":{"scaleX":9.142857142857142,"scaleY":12.8,"offsetX":-0.7142857142857142,"offsetY":-3.6},"bandMax":{"x":3,"y":5},"packedBandMeta":5}},{"unicode":43,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":-0.0703125,"right":0.691650390625,"top":0.7109375},"sampleBounds":{"left":0.046875,"right":0.5625,"top":0.578125,"bottom":0.0625},"slug":{"glyphLoc":{"x":1003,"y":0},"curveLoc":{"x":416,"y":0},"banding":{"scaleX":3.878787878787879,"scaleY":3.878787878787879,"offsetX":-0.18181818181818182,"offsetY":-0.24242424242424243},"bandMax":{"x":1,"y":1},"packedBandMeta":1}},{"unicode":44,"advance":0.60205078125,"planeBounds":{"left":0.071533203125,"bottom":-0.2734375,"right":0.493408203125,"top":0.2734375},"sampleBounds":{"left":0.203125,"right":0.375,"top":0.140625,"bottom":-0.140625},"slug":{"glyphLoc":{"x":1023,"y":0},"curveLoc":{"x":440,"y":0},"banding":{"scaleX":17.454545454545453,"scaleY":3.5555555555555554,"offsetX":-3.545454545454545,"offsetY":0.5},"bandMax":{"x":2,"y":0},"packedBandMeta":0}},{"unicode":45,"advance":0.60205078125,"planeBounds":{"left":0.043212890625,"bottom":0.1015625,"right":0.558837890625,"top":0.4453125},"sampleBounds":{"left":0.171875,"right":0.421875,"top":0.3125,"bottom":0.234375},"slug":{"glyphLoc":{"x":1039,"y":0},"curveLoc":{"x":452,"y":0},"banding":{"scaleX":4.0,"scaleY":12.8,"offsetX":-0.6875,"offsetY":-3.0},"bandMax":{"x":0,"y":0},"packedBandMeta":0}},{"unicode":46,"advance":0.60205078125,"planeBounds":{"left":0.11279296875,"bottom":-0.1328125,"right":0.48779296875,"top":0.2890625},"sampleBounds":{"left":0.234375,"right":0.359375,"top":0.15625,"bottom":0.0},"slug":{"glyphLoc":{"x":1045,"y":0},"curveLoc":{"x":460,"y":0},"banding":{"scaleX":8.0,"scaleY":6.4,"offsetX":-1.875,"offsetY":-0.0},"bandMax":{"x":0,"y":0},"packedBandMeta":0}},{"unicode":47,"advance":0.60205078125,"planeBounds":{"left":-0.078857421875,"bottom":-0.2265625,"right":0.655517578125,"top":0.8671875},"sampleBounds":{"left":0.046875,"right":0.53125,"top":0.734375,"bottom":-0.09375},"slug":{"glyphLoc":{"x":1051,"y":0},"curveLoc":{"x":468,"y":0},"banding":{"scaleX":4.129032258064516,"scaleY":1.2075471698113207,"offsetX":-0.1935483870967742,"offsetY":0.11320754716981132},"bandMax":{"x":1,"y":0},"packedBandMeta":0}},{"unicode":48,"advance":0.60205078125,"planeBounds":{"left":-0.066162109375,"bottom":-0.1484375,"right":0.668212890625,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":1062,"y":0},"curveLoc":{"x":476,"y":0},"banding":{"scaleX":14.933333333333334,"scaleY":14.36734693877551,"offsetX":-0.9333333333333333,"offsetY":0.22448979591836735},"bandMax":{"x":6,"y":10},"packedBandMeta":10}},{"unicode":49,"advance":0.60205078125,"planeBounds":{"left":-0.0087890625,"bottom":-0.1328125,"right":0.6630859375,"top":0.8671875},"sampleBounds":{"left":0.125,"right":0.53125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":1192,"y":0},"curveLoc":{"x":524,"y":0},"banding":{"scaleX":4.923076923076923,"scaleY":10.893617021276595,"offsetX":-0.6153846153846154,"offsetY":-0.0},"bandMax":{"x":1,"y":7},"packedBandMeta":7}},{"unicode":50,"advance":0.60205078125,"planeBounds":{"left":-0.055908203125,"bottom":-0.1328125,"right":0.647216796875,"top":0.8671875},"sampleBounds":{"left":0.078125,"right":0.515625,"top":0.75,"bottom":0.0},"slug":{"glyphLoc":{"x":1223,"y":0},"curveLoc":{"x":546,"y":0},"banding":{"scaleX":18.285714285714285,"scaleY":25.333333333333332,"offsetX":-1.4285714285714284,"offsetY":-0.0},"bandMax":{"x":7,"y":18},"packedBandMeta":18}},{"unicode":51,"advance":0.60205078125,"planeBounds":{"left":-0.0625,"bottom":-0.1484375,"right":0.65625,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":1358,"y":0},"curveLoc":{"x":588,"y":0},"banding":{"scaleX":14.933333333333334,"scaleY":13.061224489795919,"offsetX":-0.9333333333333333,"offsetY":0.20408163265306123},"bandMax":{"x":6,"y":9},"packedBandMeta":9}},{"unicode":52,"advance":0.60205078125,"planeBounds":{"left":-0.080810546875,"bottom":-0.1328125,"right":0.684814453125,"top":0.8671875},"sampleBounds":{"left":0.046875,"right":0.546875,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":1480,"y":0},"curveLoc":{"x":646,"y":0},"banding":{"scaleX":10.0,"scaleY":19.06382978723404,"offsetX":-0.46875,"offsetY":-0.0},"bandMax":{"x":4,"y":13},"packedBandMeta":13}},{"unicode":53,"advance":0.60205078125,"planeBounds":{"left":-0.0556640625,"bottom":-0.1484375,"right":0.6474609375,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.515625,"top":0.734375,"bottom":-0.015625},"slug":{"glyphLoc":{"x":1539,"y":0},"curveLoc":{"x":674,"y":0},"banding":{"scaleX":39.724137931034484,"scaleY":14.666666666666666,"offsetX":-2.4827586206896552,"offsetY":0.22916666666666666},"bandMax":{"x":17,"y":10},"packedBandMeta":10}},{"unicode":54,"advance":0.60205078125,"planeBounds":{"left":-0.066162109375,"bottom":-0.1484375,"right":0.668212890625,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":1691,"y":0},"curveLoc":{"x":718,"y":0},"banding":{"scaleX":14.933333333333334,"scaleY":13.061224489795919,"offsetX":-0.9333333333333333,"offsetY":0.20408163265306123},"bandMax":{"x":6,"y":9},"packedBandMeta":9}},{"unicode":55,"advance":0.60205078125,"planeBounds":{"left":-0.06201171875,"bottom":-0.1328125,"right":0.65673828125,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":1815,"y":0},"curveLoc":{"x":768,"y":0},"banding":{"scaleX":2.1333333333333333,"scaleY":1.3617021276595744,"offsetX":-0.13333333333333333,"offsetY":-0.0},"bandMax":{"x":0,"y":0},"packedBandMeta":0}},{"unicode":56,"advance":0.60205078125,"planeBounds":{"left":-0.066162109375,"bottom":-0.1484375,"right":0.668212890625,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":1826,"y":0},"curveLoc":{"x":782,"y":0},"banding":{"scaleX":10.666666666666666,"scaleY":11.755102040816327,"offsetX":-0.6666666666666666,"offsetY":0.1836734693877551},"bandMax":{"x":4,"y":8},"packedBandMeta":8}},{"unicode":57,"advance":0.60205078125,"planeBounds":{"left":-0.069091796875,"bottom":-0.1484375,"right":0.665283203125,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":1958,"y":0},"curveLoc":{"x":846,"y":0},"banding":{"scaleX":40.53333333333333,"scaleY":24.816326530612244,"offsetX":-2.533333333333333,"offsetY":0.3877551020408163},"bandMax":{"x":18,"y":18},"packedBandMeta":18}},{"unicode":58,"advance":0.60205078125,"planeBounds":{"left":0.11279296875,"bottom":-0.1328125,"right":0.48779296875,"top":0.6484375},"sampleBounds":{"left":0.234375,"right":0.359375,"top":0.515625,"bottom":0.0},"slug":{"glyphLoc":{"x":2171,"y":0},"curveLoc":{"x":896,"y":0},"banding":{"scaleX":8.0,"scaleY":3.878787878787879,"offsetX":-1.875,"offsetY":-0.0},"bandMax":{"x":0,"y":1},"packedBandMeta":1}},{"unicode":59,"advance":0.60205078125,"planeBounds":{"left":0.071533203125,"bottom":-0.2734375,"right":0.493408203125,"top":0.6484375},"sampleBounds":{"left":0.203125,"right":0.375,"top":0.515625,"bottom":-0.140625},"slug":{"glyphLoc":{"x":2182,"y":0},"curveLoc":{"x":912,"y":0},"banding":{"scaleX":17.454545454545453,"scaleY":3.0476190476190474,"offsetX":-3.545454545454545,"offsetY":0.42857142857142855},"bandMax":{"x":2,"y":1},"packedBandMeta":1}},{"unicode":60,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":-0.0703125,"right":0.691650390625,"top":0.6953125},"sampleBounds":{"left":0.046875,"right":0.5625,"top":0.5625,"bottom":0.0625},"slug":{"glyphLoc":{"x":2207,"y":0},"curveLoc":{"x":932,"y":0},"banding":{"scaleX":1.9393939393939394,"scaleY":14.0,"offsetX":-0.09090909090909091,"offsetY":-0.875},"bandMax":{"x":0,"y":6},"packedBandMeta":6}},{"unicode":61,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":0.0390625,"right":0.691650390625,"top":0.5859375},"sampleBounds":{"left":0.046875,"right":0.5625,"top":0.453125,"bottom":0.171875},"slug":{"glyphLoc":{"x":2238,"y":0},"curveLoc":{"x":946,"y":0},"banding":{"scaleX":1.9393939393939394,"scaleY":7.111111111111111,"offsetX":-0.09090909090909091,"offsetY":-1.222222222222222},"bandMax":{"x":0,"y":1},"packedBandMeta":1}},{"unicode":62,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":-0.0703125,"right":0.691650390625,"top":0.6953125},"sampleBounds":{"left":0.046875,"right":0.5625,"top":0.5625,"bottom":0.0625},"slug":{"glyphLoc":{"x":2249,"y":0},"curveLoc":{"x":962,"y":0},"banding":{"scaleX":1.9393939393939394,"scaleY":14.0,"offsetX":-0.09090909090909091,"offsetY":-0.875},"bandMax":{"x":0,"y":6},"packedBandMeta":6}},{"unicode":63,"advance":0.60205078125,"planeBounds":{"left":-0.0068359375,"bottom":-0.1328125,"right":0.6337890625,"top":0.8671875},"sampleBounds":{"left":0.125,"right":0.515625,"top":0.75,"bottom":0.0},"slug":{"glyphLoc":{"x":2280,"y":0},"curveLoc":{"x":976,"y":0},"banding":{"scaleX":46.08,"scaleY":25.333333333333332,"offsetX":-5.76,"offsetY":-0.0},"bandMax":{"x":17,"y":18},"packedBandMeta":18}},{"unicode":64,"advance":0.60205078125,"planeBounds":{"left":-0.112060546875,"bottom":-0.2890625,"right":0.700439453125,"top":0.8203125},"sampleBounds":{"left":0.015625,"right":0.578125,"top":0.6875,"bottom":-0.15625},"slug":{"glyphLoc":{"x":2450,"y":0},"curveLoc":{"x":1030,"y":0},"banding":{"scaleX":24.88888888888889,"scaleY":11.851851851851851,"offsetX":-0.3888888888888889,"offsetY":1.8518518518518516},"bandMax":{"x":13,"y":9},"packedBandMeta":9}},{"unicode":65,"advance":0.60205078125,"planeBounds":{"left":-0.113037109375,"bottom":-0.1328125,"right":0.715087890625,"top":0.8671875},"sampleBounds":{"left":0.015625,"right":0.578125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":2658,"y":0},"curveLoc":{"x":1104,"y":0},"banding":{"scaleX":12.444444444444445,"scaleY":4.085106382978723,"offsetX":-0.19444444444444445,"offsetY":-0.0},"bandMax":{"x":6,"y":2},"packedBandMeta":2}},{"unicode":66,"advance":0.60205078125,"planeBounds":{"left":-0.049072265625,"bottom":-0.1328125,"right":0.685302734375,"top":0.8671875},"sampleBounds":{"left":0.078125,"right":0.5625,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":2703,"y":0},"curveLoc":{"x":1126,"y":0},"banding":{"scaleX":8.258064516129032,"scaleY":12.25531914893617,"offsetX":-0.6451612903225806,"offsetY":-0.0},"bandMax":{"x":3,"y":8},"packedBandMeta":8}},{"unicode":67,"advance":0.60205078125,"planeBounds":{"left":-0.0634765625,"bottom":-0.1484375,"right":0.6552734375,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":2803,"y":0},"curveLoc":{"x":1176,"y":0},"banding":{"scaleX":8.533333333333333,"scaleY":13.061224489795919,"offsetX":-0.5333333333333333,"offsetY":0.20408163265306123},"bandMax":{"x":3,"y":9},"packedBandMeta":9}},{"unicode":68,"advance":0.60205078125,"planeBounds":{"left":-0.063720703125,"bottom":-0.1328125,"right":0.670654296875,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.546875,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":2881,"y":0},"curveLoc":{"x":1212,"y":0},"banding":{"scaleX":4.129032258064516,"scaleY":4.085106382978723,"offsetX":-0.25806451612903225,"offsetY":-0.0},"bandMax":{"x":1,"y":2},"packedBandMeta":2}},{"unicode":69,"advance":0.60205078125,"planeBounds":{"left":-0.034423828125,"bottom":-0.1328125,"right":0.668701171875,"top":0.8671875},"sampleBounds":{"left":0.09375,"right":0.53125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":2920,"y":0},"curveLoc":{"x":1240,"y":0},"banding":{"scaleX":2.2857142857142856,"scaleY":5.446808510638298,"offsetX":-0.21428571428571427,"offsetY":-0.0},"bandMax":{"x":0,"y":3},"packedBandMeta":3}},{"unicode":70,"advance":0.60205078125,"planeBounds":{"left":-0.015380859375,"bottom":-0.1328125,"right":0.672119140625,"top":0.8671875},"sampleBounds":{"left":0.109375,"right":0.546875,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":2943,"y":0},"curveLoc":{"x":1264,"y":0},"banding":{"scaleX":2.2857142857142856,"scaleY":5.446808510638298,"offsetX":-0.25,"offsetY":-0.0},"bandMax":{"x":0,"y":3},"packedBandMeta":3}},{"unicode":71,"advance":0.60205078125,"planeBounds":{"left":-0.08056640625,"bottom":-0.1484375,"right":0.66943359375,"top":0.8671875},"sampleBounds":{"left":0.046875,"right":0.546875,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":2964,"y":0},"curveLoc":{"x":1284,"y":0},"banding":{"scaleX":6.0,"scaleY":19.591836734693878,"offsetX":-0.28125,"offsetY":0.30612244897959184},"bandMax":{"x":2,"y":14},"packedBandMeta":14}},{"unicode":72,"advance":0.60205078125,"planeBounds":{"left":-0.058349609375,"bottom":-0.1328125,"right":0.660400390625,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3065,"y":0},"curveLoc":{"x":1328,"y":0},"banding":{"scaleX":4.266666666666667,"scaleY":2.723404255319149,"offsetX":-0.26666666666666666,"offsetY":-0.0},"bandMax":{"x":1,"y":1},"packedBandMeta":1}},{"unicode":73,"advance":0.60205078125,"planeBounds":{"left":-0.027587890625,"bottom":-0.1328125,"right":0.628662109375,"top":0.8671875},"sampleBounds":{"left":0.09375,"right":0.5,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3085,"y":0},"curveLoc":{"x":1352,"y":0},"banding":{"scaleX":4.923076923076923,"scaleY":2.723404255319149,"offsetX":-0.46153846153846156,"offsetY":-0.0},"bandMax":{"x":1,"y":1},"packedBandMeta":1}},{"unicode":74,"advance":0.60205078125,"planeBounds":{"left":-0.075927734375,"bottom":-0.1484375,"right":0.595947265625,"top":0.8671875},"sampleBounds":{"left":0.046875,"right":0.46875,"top":0.734375,"bottom":-0.015625},"slug":{"glyphLoc":{"x":3105,"y":0},"curveLoc":{"x":1376,"y":0},"banding":{"scaleX":7.111111111111111,"scaleY":18.666666666666668,"offsetX":-0.3333333333333333,"offsetY":0.2916666666666667},"bandMax":{"x":2,"y":13},"packedBandMeta":13}},{"unicode":75,"advance":0.60205078125,"planeBounds":{"left":-0.05810546875,"bottom":-0.1328125,"right":0.72314453125,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.59375,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3168,"y":0},"curveLoc":{"x":1404,"y":0},"banding":{"scaleX":16.941176470588236,"scaleY":2.723404255319149,"offsetX":-1.0588235294117647,"offsetY":-0.0},"bandMax":{"x":8,"y":1},"packedBandMeta":1}},{"unicode":76,"advance":0.60205078125,"planeBounds":{"left":-0.02099609375,"bottom":-0.1328125,"right":0.68212890625,"top":0.8671875},"sampleBounds":{"left":0.109375,"right":0.5625,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3218,"y":0},"curveLoc":{"x":1428,"y":0},"banding":{"scaleX":2.206896551724138,"scaleY":1.3617021276595744,"offsetX":-0.24137931034482757,"offsetY":-0.0},"bandMax":{"x":0,"y":0},"packedBandMeta":0}},{"unicode":77,"advance":0.60205078125,"planeBounds":{"left":-0.090087890625,"bottom":-0.1328125,"right":0.691162109375,"top":0.8671875},"sampleBounds":{"left":0.046875,"right":0.5625,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3226,"y":0},"curveLoc":{"x":1440,"y":0},"banding":{"scaleX":17.454545454545453,"scaleY":1.3617021276595744,"offsetX":-0.8181818181818181,"offsetY":-0.0},"bandMax":{"x":8,"y":0},"packedBandMeta":0}},{"unicode":78,"advance":0.60205078125,"planeBounds":{"left":-0.058349609375,"bottom":-0.1328125,"right":0.660400390625,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3269,"y":0},"curveLoc":{"x":1466,"y":0},"banding":{"scaleX":8.533333333333333,"scaleY":1.3617021276595744,"offsetX":-0.5333333333333333,"offsetY":-0.0},"bandMax":{"x":3,"y":0},"packedBandMeta":0}},{"unicode":79,"advance":0.60205078125,"planeBounds":{"left":-0.073974609375,"bottom":-0.1484375,"right":0.676025390625,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.546875,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":3292,"y":0},"curveLoc":{"x":1486,"y":0},"banding":{"scaleX":6.193548387096774,"scaleY":3.9183673469387754,"offsetX":-0.3870967741935484,"offsetY":0.061224489795918366},"bandMax":{"x":2,"y":2},"packedBandMeta":2}},{"unicode":80,"advance":0.60205078125,"planeBounds":{"left":-0.03271484375,"bottom":-0.1328125,"right":0.68603515625,"top":0.8671875},"sampleBounds":{"left":0.09375,"right":0.5625,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3346,"y":0},"curveLoc":{"x":1518,"y":0},"banding":{"scaleX":8.533333333333333,"scaleY":9.53191489361702,"offsetX":-0.8,"offsetY":-0.0},"bandMax":{"x":3,"y":6},"packedBandMeta":6}},{"unicode":81,"advance":0.60205078125,"planeBounds":{"left":-0.073974609375,"bottom":-0.2578125,"right":0.676025390625,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.546875,"top":0.75,"bottom":-0.125},"slug":{"glyphLoc":{"x":3409,"y":0},"curveLoc":{"x":1550,"y":0},"banding":{"scaleX":16.516129032258064,"scaleY":4.571428571428571,"offsetX":-1.032258064516129,"offsetY":0.5714285714285714},"bandMax":{"x":7,"y":3},"packedBandMeta":3}},{"unicode":82,"advance":0.60205078125,"planeBounds":{"left":-0.0625,"bottom":-0.1328125,"right":0.734375,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.609375,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3500,"y":0},"curveLoc":{"x":1592,"y":0},"banding":{"scaleX":23.771428571428572,"scaleY":12.25531914893617,"offsetX":-1.4857142857142858,"offsetY":-0.0},"bandMax":{"x":12,"y":8},"packedBandMeta":8}},{"unicode":83,"advance":0.60205078125,"planeBounds":{"left":-0.057373046875,"bottom":-0.1484375,"right":0.661376953125,"top":0.8671875},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.75,"bottom":-0.015625},"slug":{"glyphLoc":{"x":3621,"y":0},"curveLoc":{"x":1638,"y":0},"banding":{"scaleX":38.4,"scaleY":13.061224489795919,"offsetX":-2.4,"offsetY":0.20408163265306123},"bandMax":{"x":17,"y":9},"packedBandMeta":9}},{"unicode":84,"advance":0.60205078125,"planeBounds":{"left":-0.105224609375,"bottom":-0.1328125,"right":0.707275390625,"top":0.8671875},"sampleBounds":{"left":0.015625,"right":0.578125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3809,"y":0},"curveLoc":{"x":1694,"y":0},"banding":{"scaleX":3.5555555555555554,"scaleY":1.3617021276595744,"offsetX":-0.05555555555555555,"offsetY":-0.0},"bandMax":{"x":1,"y":0},"packedBandMeta":0}},{"unicode":85,"advance":0.60205078125,"planeBounds":{"left":-0.05859375,"bottom":-0.1484375,"right":0.66015625,"top":0.8671875},"sampleBounds":{"left":0.078125,"right":0.53125,"top":0.734375,"bottom":-0.015625},"slug":{"glyphLoc":{"x":3822,"y":0},"curveLoc":{"x":1710,"y":0},"banding":{"scaleX":19.862068965517242,"scaleY":18.666666666666668,"offsetX":-1.5517241379310345,"offsetY":0.2916666666666667},"bandMax":{"x":8,"y":13},"packedBandMeta":13}},{"unicode":86,"advance":0.60205078125,"planeBounds":{"left":-0.097412109375,"bottom":-0.1328125,"right":0.699462890625,"top":0.8671875},"sampleBounds":{"left":0.03125,"right":0.578125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3918,"y":0},"curveLoc":{"x":1754,"y":0},"banding":{"scaleX":12.8,"scaleY":1.3617021276595744,"offsetX":-0.4,"offsetY":-0.0},"bandMax":{"x":6,"y":0},"packedBandMeta":0}},{"unicode":87,"advance":0.60205078125,"planeBounds":{"left":-0.128662109375,"bottom":-0.1328125,"right":0.730712890625,"top":0.8671875},"sampleBounds":{"left":0.0,"right":0.609375,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3949,"y":0},"curveLoc":{"x":1768,"y":0},"banding":{"scaleX":13.128205128205128,"scaleY":1.3617021276595744,"offsetX":-0.0,"offsetY":-0.0},"bandMax":{"x":7,"y":0},"packedBandMeta":0}},{"unicode":88,"advance":0.60205078125,"planeBounds":{"left":-0.12109375,"bottom":-0.1328125,"right":0.72265625,"top":0.8671875},"sampleBounds":{"left":0.015625,"right":0.59375,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":3993,"y":0},"curveLoc":{"x":1794,"y":0},"banding":{"scaleX":6.918918918918919,"scaleY":9.53191489361702,"offsetX":-0.10810810810810811,"offsetY":-0.0},"bandMax":{"x":3,"y":6},"packedBandMeta":6}},{"unicode":89,"advance":0.60205078125,"planeBounds":{"left":-0.113037109375,"bottom":-0.1328125,"right":0.715087890625,"top":0.8671875},"sampleBounds":{"left":0.015625,"right":0.578125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":4038,"y":0},"curveLoc":{"x":1818,"y":0},"banding":{"scaleX":12.444444444444445,"scaleY":2.723404255319149,"offsetX":-0.19444444444444445,"offsetY":-0.0},"bandMax":{"x":6,"y":1},"packedBandMeta":1}},{"unicode":90,"advance":0.60205078125,"planeBounds":{"left":-0.051513671875,"bottom":-0.1328125,"right":0.698486328125,"top":0.8671875},"sampleBounds":{"left":0.078125,"right":0.578125,"top":0.734375,"bottom":0.0},"slug":{"glyphLoc":{"x":4074,"y":0},"curveLoc":{"x":1836,"y":0},"banding":{"scaleX":2.0,"scaleY":2.723404255319149,"offsetX":-0.15625,"offsetY":-0.0},"bandMax":{"x":0,"y":1},"packedBandMeta":1}},{"unicode":91,"advance":0.60205078125,"planeBounds":{"left":0.09521484375,"bottom":-0.2578125,"right":0.56396484375,"top":0.8984375},"sampleBounds":{"left":0.21875,"right":0.4375,"top":0.765625,"bottom":-0.125},"slug":{"glyphLoc":{"x":0,"y":1},"curveLoc":{"x":1856,"y":0},"banding":{"scaleX":4.571428571428571,"scaleY":2.245614035087719,"offsetX":-1.0,"offsetY":0.2807017543859649},"bandMax":{"x":0,"y":1},"packedBandMeta":1}},{"unicode":92,"advance":0.60205078125,"planeBounds":{"left":-0.078857421875,"bottom":-0.2265625,"right":0.655517578125,"top":0.8671875},"sampleBounds":{"left":0.046875,"right":0.53125,"top":0.734375,"bottom":-0.09375},"slug":{"glyphLoc":{"x":13,"y":1},"curveLoc":{"x":1872,"y":0},"banding":{"scaleX":4.129032258064516,"scaleY":1.2075471698113207,"offsetX":-0.1935483870967742,"offsetY":0.11320754716981132},"bandMax":{"x":1,"y":0},"packedBandMeta":0}},{"unicode":93,"advance":0.60205078125,"planeBounds":{"left":0.0380859375,"bottom":-0.2578125,"right":0.5068359375,"top":0.8984375},"sampleBounds":{"left":0.171875,"right":0.375,"top":0.765625,"bottom":-0.125},"slug":{"glyphLoc":{"x":24,"y":1},"curveLoc":{"x":1880,"y":0},"banding":{"scaleX":4.923076923076923,"scaleY":2.245614035087719,"offsetX":-0.8461538461538463,"offsetY":0.2807017543859649},"bandMax":{"x":0,"y":1},"packedBandMeta":1}},{"unicode":94,"advance":0.60205078125,"planeBounds":{"left":-0.097412109375,"bottom":0.3203125,"right":0.699462890625,"top":0.8671875},"sampleBounds":{"left":0.03125,"right":0.5625,"top":0.734375,"bottom":0.453125},"slug":{"glyphLoc":{"x":37,"y":1},"curveLoc":{"x":1896,"y":0},"banding":{"scaleX":7.529411764705882,"scaleY":3.5555555555555554,"offsetX":-0.23529411764705882,"offsetY":-1.611111111111111},"bandMax":{"x":3,"y":0},"packedBandMeta":0}},{"unicode":95,"advance":0.60205078125,"planeBounds":{"left":-0.128662109375,"bottom":-0.3671875,"right":0.730712890625,"top":-0.0703125},"sampleBounds":{"left":0.0,"right":0.609375,"top":-0.203125,"bottom":-0.234375},"slug":{"glyphLoc":{"x":60,"y":1},"curveLoc":{"x":1910,"y":0},"banding":{"scaleX":1.641025641025641,"scaleY":32.0,"offsetX":-0.0,"offsetY":7.5},"bandMax":{"x":0,"y":0},"packedBandMeta":0}},{"unicode":96,"advance":0.60205078125,"planeBounds":{"left":0.010986328125,"bottom":0.4765625,"right":0.495361328125,"top":0.9296875},"sampleBounds":{"left":0.140625,"right":0.375,"top":0.796875,"bottom":0.609375},"slug":{"glyphLoc":{"x":66,"y":1},"curveLoc":{"x":1918,"y":0},"banding":{"scaleX":8.533333333333333,"scaleY":5.333333333333333,"offsetX":-1.2,"offsetY":-3.25},"bandMax":{"x":1,"y":0},"packedBandMeta":0}},{"unicode":97,"advance":0.60205078125,"planeBounds":{"left":-0.060546875,"bottom":-0.1484375,"right":0.642578125,"top":0.6953125},"sampleBounds":{"left":0.0625,"right":0.515625,"top":0.5625,"bottom":-0.015625},"slug":{"glyphLoc":{"x":77,"y":1},"curveLoc":{"x":1926,"y":0},"banding":{"scaleX":37.51724137931034,"scaleY":17.2972972972973,"offsetX":-2.3448275862068964,"offsetY":0.2702702702702703},"bandMax":{"x":16,"y":9},"packedBandMeta":9}},{"unicode":98,"advance":0.60205078125,"planeBounds":{"left":-0.032958984375,"bottom":-0.1484375,"right":0.670166015625,"top":0.8984375},"sampleBounds":{"left":0.09375,"right":0.546875,"top":0.765625,"bottom":-0.015625},"slug":{"glyphLoc":{"x":264,"y":1},"curveLoc":{"x":1988,"y":0},"banding":{"scaleX":30.896551724137932,"scaleY":16.64,"offsetX":-2.896551724137931,"offsetY":0.26},"bandMax":{"x":13,"y":12},"packedBandMeta":12}},{"unicode":99,"advance":0.60205078125,"planeBounds":{"left":-0.037109375,"bottom":-0.1484375,"right":0.650390625,"top":0.6953125},"sampleBounds":{"left":0.09375,"right":0.515625,"top":0.5625,"bottom":-0.015625},"slug":{"glyphLoc":{"x":400,"y":1},"curveLoc":{"x":2030,"y":0},"banding":{"scaleX":9.481481481481481,"scaleY":17.2972972972973,"offsetX":-0.8888888888888888,"offsetY":0.2702702702702703},"bandMax":{"x":3,"y":9},"packedBandMeta":9}},{"unicode":100,"advance":0.60205078125,"planeBounds":{"left":-0.067138671875,"bottom":-0.1484375,"right":0.635986328125,"top":0.8984375},"sampleBounds":{"left":0.0625,"right":0.515625,"top":0.765625,"bottom":-0.015625},"slug":{"glyphLoc":{"x":480,"y":1},"curveLoc":{"x":2066,"y":0},"banding":{"scaleX":33.10344827586207,"scaleY":16.64,"offsetX":-2.0689655172413794,"offsetY":0.26},"bandMax":{"x":14,"y":12},"packedBandMeta":12}},{"unicode":101,"advance":0.60205078125,"planeBounds":{"left":-0.065673828125,"bottom":-0.1484375,"right":0.668701171875,"top":0.6953125},"sampleBounds":{"left":0.0625,"right":0.546875,"top":0.5625,"bottom":-0.015625},"slug":{"glyphLoc":{"x":619,"y":1},"curveLoc":{"x":2108,"y":0},"banding":{"scaleX":22.70967741935484,"scaleY":17.2972972972973,"offsetX":-1.4193548387096775,"offsetY":0.2702702702702703},"bandMax":{"x":10,"y":9},"packedBandMeta":9}},{"unicode":102,"advance":0.60205078125,"planeBounds":{"left":-0.03662109375,"bottom":-0.1328125,"right":0.65087890625,"top":0.8984375},"sampleBounds":{"left":0.09375,"right":0.515625,"top":0.765625,"bottom":0.0},"slug":{"glyphLoc":{"x":750,"y":1},"curveLoc":{"x":2150,"y":0},"banding":{"scaleX":28.444444444444443,"scaleY":14.36734693877551,"offsetX":-2.6666666666666665,"offsetY":-0.0},"bandMax":{"x":11,"y":10},"packedBandMeta":10}},{"unicode":103,"advance":0.60205078125,"planeBounds":{"left":-0.067138671875,"bottom":-0.3515625,"right":0.635986328125,"top":0.6953125},"sampleBounds":{"left":0.0625,"right":0.515625,"top":0.5625,"bottom":-0.21875},"slug":{"glyphLoc":{"x":827,"y":1},"curveLoc":{"x":2186,"y":0},"banding":{"scaleX":52.96551724137931,"scaleY":20.48,"offsetX":-3.310344827586207,"offsetY":4.48},"bandMax":{"x":23,"y":15},"packedBandMeta":15}},{"unicode":104,"advance":0.60205078125,"planeBounds":{"left":-0.03173828125,"bottom":-0.1328125,"right":0.64013671875,"top":0.8984375},"sampleBounds":{"left":0.09375,"right":0.515625,"top":0.765625,"bottom":0.0},"slug":{"glyphLoc":{"x":1045,"y":1},"curveLoc":{"x":2246,"y":0},"banding":{"scaleX":16.59259259259259,"scaleY":15.673469387755102,"offsetX":-1.5555555555555554,"offsetY":-0.0},"bandMax":{"x":6,"y":11},"packedBandMeta":11}},{"unicode":105,"advance":0.60205078125,"planeBounds":{"left":-0.04150390625,"bottom":-0.1328125,"right":0.66162109375,"top":0.8984375},"sampleBounds":{"left":0.09375,"right":0.53125,"top":0.765625,"bottom":0.0},"slug":{"glyphLoc":{"x":1122,"y":1},"curveLoc":{"x":2278,"y":0},"banding":{"scaleX":4.571428571428571,"scaleY":3.9183673469387754,"offsetX":-0.42857142857142855,"offsetY":-0.0},"bandMax":{"x":1,"y":2},"packedBandMeta":2}},{"unicode":106,"advance":0.60205078125,"planeBounds":{"left":-0.03662109375,"bottom":-0.3359375,"right":0.51025390625,"top":0.8984375},"sampleBounds":{"left":0.09375,"right":0.390625,"top":0.765625,"bottom":-0.203125},"slug":{"glyphLoc":{"x":1149,"y":1},"curveLoc":{"x":2306,"y":0},"banding":{"scaleX":16.842105263157894,"scaleY":11.35483870967742,"offsetX":-1.5789473684210527,"offsetY":2.306451612903226},"bandMax":{"x":4,"y":10},"packedBandMeta":10}},{"unicode":107,"advance":0.60205078125,"planeBounds":{"left":-0.01611328125,"bottom":-0.1328125,"right":0.71826171875,"top":0.8984375},"sampleBounds":{"left":0.109375,"right":0.59375,"top":0.765625,"bottom":0.0},"slug":{"glyphLoc":{"x":1211,"y":1},"curveLoc":{"x":2338,"y":0},"banding":{"scaleX":8.258064516129032,"scaleY":6.530612244897959,"offsetX":-0.9032258064516129,"offsetY":-0.0},"bandMax":{"x":3,"y":4},"packedBandMeta":4}},{"unicode":108,"advance":0.60205078125,"planeBounds":{"left":-0.05224609375,"bottom":-0.1328125,"right":0.63525390625,"top":0.8984375},"sampleBounds":{"left":0.078125,"right":0.5,"top":0.765625,"bottom":0.0},"slug":{"glyphLoc":{"x":1259,"y":1},"curveLoc":{"x":2362,"y":0},"banding":{"scaleX":28.444444444444443,"scaleY":10.448979591836734,"offsetX":-2.2222222222222223,"offsetY":-0.0},"bandMax":{"x":11,"y":7},"packedBandMeta":7}},{"unicode":109,"advance":0.60205078125,"planeBounds":{"left":-0.0791015625,"bottom":-0.1328125,"right":0.6865234375,"top":0.6953125},"sampleBounds":{"left":0.046875,"right":0.546875,"top":0.5625,"bottom":0.0},"slug":{"glyphLoc":{"x":1317,"y":1},"curveLoc":{"x":2386,"y":0},"banding":{"scaleX":30.0,"scaleY":19.555555555555557,"offsetX":-1.40625,"offsetY":-0.0},"bandMax":{"x":14,"y":10},"packedBandMeta":10}},{"unicode":110,"advance":0.60205078125,"planeBounds":{"left":-0.03173828125,"bottom":-0.1328125,"right":0.64013671875,"top":0.6953125},"sampleBounds":{"left":0.09375,"right":0.515625,"top":0.5625,"bottom":0.0},"slug":{"glyphLoc":{"x":1441,"y":1},"curveLoc":{"x":2440,"y":0},"banding":{"scaleX":16.59259259259259,"scaleY":26.666666666666668,"offsetX":-1.5555555555555554,"offsetY":-0.0},"bandMax":{"x":6,"y":14},"packedBandMeta":14}},{"unicode":111,"advance":0.60205078125,"planeBounds":{"left":-0.058349609375,"bottom":-0.1484375,"right":0.660400390625,"top":0.6953125},"sampleBounds":{"left":0.0625,"right":0.53125,"top":0.5625,"bottom":-0.015625},"slug":{"glyphLoc":{"x":1527,"y":1},"curveLoc":{"x":2472,"y":0},"banding":{"scaleX":6.4,"scaleY":5.1891891891891895,"offsetX":-0.4,"offsetY":0.08108108108108109},"bandMax":{"x":2,"y":2},"packedBandMeta":2}},{"unicode":112,"advance":0.60205078125,"planeBounds":{"left":-0.03466796875,"bottom":-0.3359375,"right":0.66845703125,"top":0.6953125},"sampleBounds":{"left":0.09375,"right":0.546875,"top":0.5625,"bottom":-0.203125},"slug":{"glyphLoc":{"x":1581,"y":1},"curveLoc":{"x":2504,"y":0},"banding":{"scaleX":30.896551724137932,"scaleY":14.36734693877551,"offsetX":-2.896551724137931,"offsetY":2.9183673469387754},"bandMax":{"x":13,"y":10},"packedBandMeta":10}},{"unicode":113,"advance":0.60205078125,"planeBounds":{"left":-0.060546875,"bottom":-0.3359375,"right":0.642578125,"top":0.6953125},"sampleBounds":{"left":0.0625,"right":0.515625,"top":0.5625,"bottom":-0.203125},"slug":{"glyphLoc":{"x":1715,"y":1},"curveLoc":{"x":2546,"y":0},"banding":{"scaleX":26.482758620689655,"scaleY":14.36734693877551,"offsetX":-1.6551724137931034,"offsetY":2.9183673469387754},"bandMax":{"x":11,"y":10},"packedBandMeta":10}},{"unicode":114,"advance":0.60205078125,"planeBounds":{"left":0.050048828125,"bottom":-0.1328125,"right":0.690673828125,"top":0.6953125},"sampleBounds":{"left":0.171875,"right":0.5625,"top":0.5625,"bottom":0.0},"slug":{"glyphLoc":{"x":1843,"y":1},"curveLoc":{"x":2588,"y":0},"banding":{"scaleX":10.24,"scaleY":16.0,"offsetX":-1.76,"offsetY":-0.0},"bandMax":{"x":3,"y":8},"packedBandMeta":8}},{"unicode":115,"advance":0.60205078125,"planeBounds":{"left":-0.024658203125,"bottom":-0.1484375,"right":0.631591796875,"top":0.6953125},"sampleBounds":{"left":0.109375,"right":0.5,"top":0.5625,"bottom":-0.015625},"slug":{"glyphLoc":{"x":1900,"y":1},"curveLoc":{"x":2616,"y":0},"banding":{"scaleX":48.64,"scaleY":13.837837837837839,"offsetX":-5.32,"offsetY":0.21621621621621623},"bandMax":{"x":18,"y":7},"packedBandMeta":7}},{"unicode":116,"advance":0.60205078125,"planeBounds":{"left":-0.067626953125,"bottom":-0.1328125,"right":0.635498046875,"top":0.8359375},"sampleBounds":{"left":0.0625,"right":0.5,"top":0.703125,"bottom":0.0},"slug":{"glyphLoc":{"x":2079,"y":1},"curveLoc":{"x":2672,"y":0},"banding":{"scaleX":20.571428571428573,"scaleY":11.377777777777778,"offsetX":-1.2857142857142858,"offsetY":-0.0},"bandMax":{"x":8,"y":7},"packedBandMeta":7}},{"unicode":117,"advance":0.60205078125,"planeBounds":{"left":-0.03173828125,"bottom":-0.1484375,"right":0.64013671875,"top":0.6796875},"sampleBounds":{"left":0.09375,"right":0.515625,"top":0.546875,"bottom":-0.015625},"slug":{"glyphLoc":{"x":2152,"y":1},"curveLoc":{"x":2708,"y":0},"banding":{"scaleX":16.59259259259259,"scaleY":28.444444444444443,"offsetX":-1.5555555555555554,"offsetY":0.4444444444444444},"bandMax":{"x":6,"y":15},"packedBandMeta":15}},{"unicode":118,"advance":0.60205078125,"planeBounds":{"left":-0.081787109375,"bottom":-0.1328125,"right":0.683837890625,"top":0.6796875},"sampleBounds":{"left":0.046875,"right":0.546875,"top":0.546875,"bottom":0.0},"slug":{"glyphLoc":{"x":2239,"y":1},"curveLoc":{"x":2740,"y":0},"banding":{"scaleX":14.0,"scaleY":1.8285714285714285,"offsetX":-0.65625,"offsetY":-0.0},"bandMax":{"x":6,"y":0},"packedBandMeta":0}},{"unicode":119,"advance":0.60205078125,"planeBounds":{"left":-0.128662109375,"bottom":-0.1328125,"right":0.730712890625,"top":0.6796875},"sampleBounds":{"left":0.0,"right":0.609375,"top":0.546875,"bottom":0.0},"slug":{"glyphLoc":{"x":2270,"y":1},"curveLoc":{"x":2754,"y":0},"banding":{"scaleX":13.128205128205128,"scaleY":1.8285714285714285,"offsetX":-0.0,"offsetY":-0.0},"bandMax":{"x":7,"y":0},"packedBandMeta":0}},{"unicode":120,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":-0.1328125,"right":0.691650390625,"top":0.6796875},"sampleBounds":{"left":0.03125,"right":0.5625,"top":0.546875,"bottom":0.0},"slug":{"glyphLoc":{"x":2314,"y":1},"curveLoc":{"x":2780,"y":0},"banding":{"scaleX":16.941176470588236,"scaleY":12.8,"offsetX":-0.5294117647058824,"offsetY":-0.0},"bandMax":{"x":8,"y":6},"packedBandMeta":6}},{"unicode":121,"advance":0.60205078125,"planeBounds":{"left":-0.075927734375,"bottom":-0.3359375,"right":0.689697265625,"top":0.6796875},"sampleBounds":{"left":0.046875,"right":0.5625,"top":0.546875,"bottom":-0.203125},"slug":{"glyphLoc":{"x":2382,"y":1},"curveLoc":{"x":2804,"y":0},"banding":{"scaleX":19.393939393939394,"scaleY":12.0,"offsetX":-0.9090909090909092,"offsetY":2.4375},"bandMax":{"x":9,"y":8},"packedBandMeta":8}},{"unicode":122,"advance":0.60205078125,"planeBounds":{"left":-0.032470703125,"bottom":-0.1328125,"right":0.639404296875,"top":0.6796875},"sampleBounds":{"left":0.09375,"right":0.515625,"top":0.546875,"bottom":0.0},"slug":{"glyphLoc":{"x":2466,"y":1},"curveLoc":{"x":2834,"y":0},"banding":{"scaleX":2.3703703703703702,"scaleY":3.657142857142857,"offsetX":-0.2222222222222222,"offsetY":-0.0},"bandMax":{"x":0,"y":1},"packedBandMeta":1}},{"unicode":123,"advance":0.60205078125,"planeBounds":{"left":-0.019287109375,"bottom":-0.2890625,"right":0.621337890625,"top":0.8984375},"sampleBounds":{"left":0.109375,"right":0.5,"top":0.765625,"bottom":-0.15625},"slug":{"glyphLoc":{"x":2483,"y":1},"curveLoc":{"x":2854,"y":0},"banding":{"scaleX":48.64,"scaleY":19.52542372881356,"offsetX":-5.32,"offsetY":3.050847457627119},"bandMax":{"x":18,"y":17},"packedBandMeta":17}},{"unicode":124,"advance":0.60205078125,"planeBounds":{"left":0.12890625,"bottom":-0.3671875,"right":0.47265625,"top":0.8984375},"sampleBounds":{"left":0.265625,"right":0.34375,"top":0.765625,"bottom":-0.234375},"slug":{"glyphLoc":{"x":2645,"y":1},"curveLoc":{"x":2912,"y":0},"banding":{"scaleX":12.8,"scaleY":1.0,"offsetX":-3.4000000000000004,"offsetY":0.234375},"bandMax":{"x":0,"y":0},"packedBandMeta":0}},{"unicode":125,"advance":0.60205078125,"planeBounds":{"left":-0.019287109375,"bottom":-0.2890625,"right":0.621337890625,"top":0.8984375},"sampleBounds":{"left":0.109375,"right":0.5,"top":0.765625,"bottom":-0.15625},"slug":{"glyphLoc":{"x":2651,"y":1},"curveLoc":{"x":2920,"y":0},"banding":{"scaleX":17.92,"scaleY":19.52542372881356,"offsetX":-1.9600000000000002,"offsetY":3.050847457627119},"bandMax":{"x":6,"y":17},"packedBandMeta":17}},{"unicode":126,"advance":0.60205078125,"planeBounds":{"left":-0.089599609375,"bottom":0.1015625,"right":0.691650390625,"top":0.5078125},"sampleBounds":{"left":0.046875,"right":0.5625,"top":0.375,"bottom":0.234375},"slug":{"glyphLoc":{"x":2775,"y":1},"curveLoc":{"x":2978,"y":0},"banding":{"scaleX":19.393939393939394,"scaleY":49.77777777777778,"offsetX":-0.9090909090909092,"offsetY":-11.666666666666666},"bandMax":{"x":9,"y":6},"packedBandMeta":6}}]} \ No newline at end of file diff --git a/resources/public/fonts/manifest.json b/resources/public/fonts/manifest.json new file mode 100644 index 0000000..c757997 --- /dev/null +++ b/resources/public/fonts/manifest.json @@ -0,0 +1,79 @@ +{ + "fonts": [ + { + "name": "Ubuntu Sans Mono", + "id": "ubuntu-sans-mono", + "atlas": "ubuntu_sans_mono_atlas.png", + "metrics": "ubuntu_sans_mono_atlas.json", + "charWidth": 0.56, + "defaults": { + "fontSize": 19, + "lineHeight": 1.2, + "pxRange": 8, + "sharpness": 0.0, + "snapToPixel": true, + "showDiagnostics": false + } + }, + { + "name": "Ubuntu Mono", + "id": "ubuntu-mono", + "atlas": "ubuntu_mono_atlas.png", + "metrics": "ubuntu_mono_atlas.json", + "charWidth": 0.56, + "defaults": { + "fontSize": 19, + "lineHeight": 1.2, + "pxRange": 8, + "sharpness": 0.0, + "snapToPixel": true, + "showDiagnostics": false + } + }, + { + "name": "DejaVu Sans Mono", + "id": "dejavu-sans-mono", + "atlas": "dejavu_sans_mono_atlas.png", + "metrics": "dejavu_sans_mono_atlas.json", + "preferredBackend": "msdf", + "slug": { + "meta": "dejavu_sans_mono_slug_meta.json", + "curve": "dejavu_sans_mono_slug_curve.bin", + "band": "dejavu_sans_mono_slug_band.bin" + }, + "charWidth": 0.60, + "default": true, + "defaults": { + "fontSize": 19, + "lineHeight": 1.2, + "pxRange": 16, + "sharpness": 0.05, + "snapToPixel": true, + "showDiagnostics": false + } + }, + { + "name": "Noto Sans Mono", + "id": "noto-sans-mono", + "atlas": "noto_sans_mono_atlas.png", + "metrics": "noto_sans_mono_atlas.json", + "charWidth": 0.60, + "defaults": { + "fontSize": 19, + "lineHeight": 1.2, + "pxRange": 8, + "sharpness": 0.0, + "snapToPixel": true, + "showDiagnostics": false + } + } + ], + "settings": { + "fontSize": {"min": 8, "max": 40, "default": 16}, + "lineHeight": {"min": 1.0, "max": 2.0, "default": 1.2}, + "pxRange": {"min": 4, "max": 12, "default": 8}, + "sharpness": {"min": -0.2, "max": 0.2, "default": 0.0}, + "snapToPixel": {"default": true}, + "showDiagnostics": {"default": false} + } +} diff --git a/resources/public/fonts/noto_sans_mono_atlas.json b/resources/public/fonts/noto_sans_mono_atlas.json new file mode 100644 index 0000000..faf2ae0 --- /dev/null +++ b/resources/public/fonts/noto_sans_mono_atlas.json @@ -0,0 +1 @@ +{"atlas":{"type":"msdf","distanceRange":8,"distanceRangeMiddle":0,"size":128,"width":1024,"height":1024,"yOrigin":"bottom"},"metrics":{"emSize":1,"lineHeight":1.3620000000000001,"ascender":1.069,"descender":-0.29299999999999998,"underlineY":-0.125,"underlineThickness":0.050000000000000003},"glyphs":[{"unicode":32,"advance":0.59999999999999998},{"unicode":33,"advance":0.59999999999999998,"planeBounds":{"left":0.20625000000000002,"bottom":-0.04296875,"right":0.39374999999999999,"top":0.74609375},"atlasBounds":{"left":970.5,"bottom":922.5,"right":994.5,"top":1023.5}},{"unicode":34,"advance":0.59999999999999998,"planeBounds":{"left":0.12812499999999999,"bottom":0.41796875,"right":0.47187499999999999,"top":0.74609375},"atlasBounds":{"left":979.5,"bottom":843.5,"right":1023.5,"top":885.5}},{"unicode":35,"advance":0.59999999999999998,"planeBounds":{"left":-0.012499999999999971,"bottom":-0.03515625,"right":0.61250000000000004,"top":0.74609375},"atlasBounds":{"left":697.5,"bottom":677.5,"right":777.5,"top":777.5}},{"unicode":36,"advance":0.59999999999999998,"planeBounds":{"left":0.046093750000000031,"bottom":-0.08984375,"right":0.55390625000000004,"top":0.79296875},"atlasBounds":{"left":402.5,"bottom":910.5,"right":467.5,"top":1023.5}},{"unicode":37,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.04296875,"right":0.63203125000000004,"top":0.75390625},"atlasBounds":{"left":141.5,"bottom":675.5,"right":226.5,"top":777.5}},{"unicode":38,"advance":0.59999999999999998,"planeBounds":{"left":0.0053125000000000186,"bottom":-0.04296875,"right":0.61468750000000005,"top":0.76171875},"atlasBounds":{"left":345.5,"bottom":782.5,"right":423.5,"top":885.5}},{"unicode":39,"advance":0.59999999999999998,"planeBounds":{"left":0.21746875000000002,"bottom":0.41796875,"right":0.38153124999999999,"top":0.74609375},"atlasBounds":{"left":995.5,"bottom":954.5,"right":1016.5,"top":996.5}},{"unicode":40,"advance":0.59999999999999998,"planeBounds":{"left":0.1665625,"bottom":-0.19140625,"right":0.4634375,"top":0.74609375},"atlasBounds":{"left":186.5,"bottom":903.5,"right":224.5,"top":1023.5}},{"unicode":41,"advance":0.59999999999999998,"planeBounds":{"left":0.1365625,"bottom":-0.19140625,"right":0.43343750000000003,"top":0.74609375},"atlasBounds":{"left":225.5,"bottom":903.5,"right":263.5,"top":1023.5}},{"unicode":42,"advance":0.59999999999999998,"planeBounds":{"left":0.055406249999999997,"bottom":0.30078125,"right":0.54759374999999999,"top":0.77734375},"atlasBounds":{"left":560.5,"bottom":410.5,"right":623.5,"top":471.5}},{"unicode":43,"advance":0.59999999999999998,"planeBounds":{"left":0.046093750000000031,"bottom":0.08203125,"right":0.55390625000000004,"top":0.62109375},"atlasBounds":{"left":421.5,"bottom":402.5,"right":486.5,"top":471.5}},{"unicode":44,"advance":0.59999999999999998,"planeBounds":{"left":0.17831250000000001,"bottom":-0.17578125,"right":0.41268749999999998,"top":0.16796875},"atlasBounds":{"left":975.5,"bottom":712.5,"right":1005.5,"top":756.5}},{"unicode":45,"advance":0.59999999999999998,"planeBounds":{"left":0.11640625,"bottom":0.19140625,"right":0.48359374999999999,"top":0.34765625},"atlasBounds":{"left":975.5,"bottom":757.5,"right":1022.5,"top":777.5}},{"unicode":46,"advance":0.59999999999999998,"planeBounds":{"left":0.20625000000000002,"bottom":-0.04296875,"right":0.39374999999999999,"top":0.16015625},"atlasBounds":{"left":995.5,"bottom":997.5,"right":1019.5,"top":1023.5}},{"unicode":47,"advance":0.59999999999999998,"planeBounds":{"left":0.085156250000000003,"bottom":-0.03515625,"right":0.51484375000000004,"top":0.74609375},"atlasBounds":{"left":820.5,"bottom":573.5,"right":875.5,"top":673.5}},{"unicode":48,"advance":0.59999999999999998,"planeBounds":{"left":0.026562499999999992,"bottom":-0.04296875,"right":0.57343750000000004,"top":0.76171875},"atlasBounds":{"left":424.5,"bottom":782.5,"right":494.5,"top":885.5}},{"unicode":49,"advance":0.59999999999999998,"planeBounds":{"left":0.062406249999999996,"bottom":-0.03515625,"right":0.55459375,"top":0.74609375},"atlasBounds":{"left":876.5,"bottom":573.5,"right":939.5,"top":673.5}},{"unicode":50,"advance":0.59999999999999998,"planeBounds":{"left":0.037968749999999989,"bottom":-0.03515625,"right":0.57703125,"top":0.76171875},"atlasBounds":{"left":227.5,"bottom":675.5,"right":296.5,"top":777.5}},{"unicode":51,"advance":0.59999999999999998,"planeBounds":{"left":0.041687499999999982,"bottom":-0.04296875,"right":0.55731249999999999,"top":0.76171875},"atlasBounds":{"left":495.5,"bottom":782.5,"right":561.5,"top":885.5}},{"unicode":52,"advance":0.59999999999999998,"planeBounds":{"left":0.018749999999999999,"bottom":-0.03515625,"right":0.58125000000000004,"top":0.75390625},"atlasBounds":{"left":297.5,"bottom":676.5,"right":369.5,"top":777.5}},{"unicode":53,"advance":0.59999999999999998,"planeBounds":{"left":0.04559375000000003,"bottom":-0.04296875,"right":0.55340624999999999,"top":0.74609375},"atlasBounds":{"left":370.5,"bottom":676.5,"right":435.5,"top":777.5}},{"unicode":54,"advance":0.59999999999999998,"planeBounds":{"left":0.026562499999999992,"bottom":-0.04296875,"right":0.57343750000000004,"top":0.76171875},"atlasBounds":{"left":562.5,"bottom":782.5,"right":632.5,"top":885.5}},{"unicode":55,"advance":0.59999999999999998,"planeBounds":{"left":0.026562499999999992,"bottom":-0.03515625,"right":0.57343750000000004,"top":0.74609375},"atlasBounds":{"left":155.5,"bottom":472.5,"right":225.5,"top":572.5}},{"unicode":56,"advance":0.59999999999999998,"planeBounds":{"left":0.033874999999999988,"bottom":-0.04296875,"right":0.56512499999999999,"top":0.76171875},"atlasBounds":{"left":633.5,"bottom":782.5,"right":701.5,"top":885.5}},{"unicode":57,"advance":0.59999999999999998,"planeBounds":{"left":0.026562499999999992,"bottom":-0.04296875,"right":0.57343750000000004,"top":0.76171875},"atlasBounds":{"left":702.5,"bottom":782.5,"right":772.5,"top":885.5}},{"unicode":58,"advance":0.59999999999999998,"planeBounds":{"left":0.20625000000000002,"bottom":-0.04296875,"right":0.39374999999999999,"top":0.58203125},"atlasBounds":{"left":469.5,"bottom":492.5,"right":493.5,"top":572.5}},{"unicode":59,"advance":0.59999999999999998,"planeBounds":{"left":0.17690625000000001,"bottom":-0.17578125,"right":0.41909374999999999,"top":0.58203125},"atlasBounds":{"left":294.5,"bottom":475.5,"right":325.5,"top":572.5}},{"unicode":60,"advance":0.59999999999999998,"planeBounds":{"left":0.057812499999999996,"bottom":0.08203125,"right":0.54218750000000004,"top":0.64453125},"atlasBounds":{"left":358.5,"bottom":399.5,"right":420.5,"top":471.5}},{"unicode":61,"advance":0.59999999999999998,"planeBounds":{"left":0.038281249999999989,"bottom":0.18359375,"right":0.56171875000000004,"top":0.51953125},"atlasBounds":{"left":624.5,"bottom":428.5,"right":691.5,"top":471.5}},{"unicode":62,"advance":0.59999999999999998,"planeBounds":{"left":0.057812499999999996,"bottom":0.08203125,"right":0.54218750000000004,"top":0.64453125},"atlasBounds":{"left":295.5,"bottom":399.5,"right":357.5,"top":471.5}},{"unicode":63,"advance":0.59999999999999998,"planeBounds":{"left":0.057812499999999996,"bottom":-0.04296875,"right":0.54218750000000004,"top":0.76171875},"atlasBounds":{"left":773.5,"bottom":782.5,"right":835.5,"top":885.5}},{"unicode":64,"advance":0.59999999999999998,"planeBounds":{"left":-0.016406249999999969,"bottom":-0.12109375,"right":0.61640625000000004,"top":0.74609375},"atlasBounds":{"left":468.5,"bottom":912.5,"right":549.5,"top":1023.5}},{"unicode":65,"advance":0.59999999999999998,"planeBounds":{"left":-0.012499999999999971,"bottom":-0.03515625,"right":0.61250000000000004,"top":0.75390625},"atlasBounds":{"left":504.5,"bottom":676.5,"right":584.5,"top":777.5}},{"unicode":66,"advance":0.59999999999999998,"planeBounds":{"left":0.062187499999999979,"bottom":-0.03515625,"right":0.57781250000000006,"top":0.74609375},"atlasBounds":{"left":615.5,"bottom":573.5,"right":681.5,"top":673.5}},{"unicode":67,"advance":0.59999999999999998,"planeBounds":{"left":0.034562499999999996,"bottom":-0.04296875,"right":0.58143750000000005,"top":0.76171875},"atlasBounds":{"left":836.5,"bottom":782.5,"right":906.5,"top":885.5}},{"unicode":68,"advance":0.59999999999999998,"planeBounds":{"left":0.035156249999999993,"bottom":-0.03515625,"right":0.58984375,"top":0.74609375},"atlasBounds":{"left":472.5,"bottom":573.5,"right":543.5,"top":673.5}},{"unicode":69,"advance":0.59999999999999998,"planeBounds":{"left":0.062812499999999993,"bottom":-0.03515625,"right":0.54718750000000005,"top":0.74609375},"atlasBounds":{"left":409.5,"bottom":573.5,"right":471.5,"top":673.5}},{"unicode":70,"advance":0.59999999999999998,"planeBounds":{"left":0.062812499999999993,"bottom":-0.03515625,"right":0.54718750000000005,"top":0.74609375},"atlasBounds":{"left":346.5,"bottom":573.5,"right":408.5,"top":673.5}},{"unicode":71,"advance":0.59999999999999998,"planeBounds":{"left":0.010156249999999993,"bottom":-0.04296875,"right":0.56484374999999998,"top":0.76171875},"atlasBounds":{"left":907.5,"bottom":782.5,"right":978.5,"top":885.5}},{"unicode":72,"advance":0.59999999999999998,"planeBounds":{"left":0.038281249999999989,"bottom":-0.03515625,"right":0.56171875000000004,"top":0.74609375},"atlasBounds":{"left":201.5,"bottom":573.5,"right":268.5,"top":673.5}},{"unicode":73,"advance":0.59999999999999998,"planeBounds":{"left":0.077343750000000003,"bottom":-0.03515625,"right":0.52265625000000004,"top":0.74609375},"atlasBounds":{"left":143.5,"bottom":573.5,"right":200.5,"top":673.5}},{"unicode":74,"advance":0.59999999999999998,"planeBounds":{"left":0.067656250000000001,"bottom":-0.04296875,"right":0.49734375000000003,"top":0.74609375},"atlasBounds":{"left":585.5,"bottom":676.5,"right":640.5,"top":777.5}},{"unicode":75,"advance":0.59999999999999998,"planeBounds":{"left":0.036562499999999991,"bottom":-0.03515625,"right":0.58343750000000005,"top":0.74609375},"atlasBounds":{"left":544.5,"bottom":573.5,"right":614.5,"top":673.5}},{"unicode":76,"advance":0.59999999999999998,"planeBounds":{"left":0.080625000000000016,"bottom":-0.03515625,"right":0.54937500000000006,"top":0.74609375},"atlasBounds":{"left":759.5,"bottom":573.5,"right":819.5,"top":673.5}},{"unicode":77,"advance":0.59999999999999998,"planeBounds":{"left":0.018749999999999999,"bottom":-0.03515625,"right":0.58125000000000004,"top":0.74609375},"atlasBounds":{"left":0.5,"bottom":573.5,"right":72.5,"top":673.5}},{"unicode":78,"advance":0.59999999999999998,"planeBounds":{"left":0.038281249999999989,"bottom":-0.03515625,"right":0.56171875000000004,"top":0.74609375},"atlasBounds":{"left":907.5,"bottom":677.5,"right":974.5,"top":777.5}},{"unicode":79,"advance":0.59999999999999998,"planeBounds":{"left":0.010937500000000005,"bottom":-0.04296875,"right":0.58906250000000004,"top":0.76171875},"atlasBounds":{"left":0.5,"bottom":674.5,"right":74.5,"top":777.5}},{"unicode":80,"advance":0.59999999999999998,"planeBounds":{"left":0.062187499999999979,"bottom":-0.03515625,"right":0.57781250000000006,"top":0.74609375},"atlasBounds":{"left":778.5,"bottom":677.5,"right":844.5,"top":777.5}},{"unicode":81,"advance":0.59999999999999998,"planeBounds":{"left":0.010937500000000005,"bottom":-0.21484375,"right":0.58906250000000004,"top":0.76171875},"atlasBounds":{"left":73.5,"bottom":898.5,"right":147.5,"top":1023.5}},{"unicode":82,"advance":0.59999999999999998,"planeBounds":{"left":0.06246874999999999,"bottom":-0.03515625,"right":0.60153124999999996,"top":0.74609375},"atlasBounds":{"left":73.5,"bottom":573.5,"right":142.5,"top":673.5}},{"unicode":83,"advance":0.59999999999999998,"planeBounds":{"left":0.05309375000000003,"bottom":-0.04296875,"right":0.56090625000000005,"top":0.76171875},"atlasBounds":{"left":75.5,"bottom":674.5,"right":140.5,"top":777.5}},{"unicode":84,"advance":0.59999999999999998,"planeBounds":{"left":0.010937500000000005,"bottom":-0.03515625,"right":0.58906250000000004,"top":0.74609375},"atlasBounds":{"left":80.5,"bottom":472.5,"right":154.5,"top":572.5}},{"unicode":85,"advance":0.59999999999999998,"planeBounds":{"left":0.038281249999999989,"bottom":-0.04296875,"right":0.56171875000000004,"top":0.74609375},"atlasBounds":{"left":436.5,"bottom":676.5,"right":503.5,"top":777.5}},{"unicode":86,"advance":0.59999999999999998,"planeBounds":{"left":-0.0085937499999999781,"bottom":-0.03515625,"right":0.60859375000000004,"top":0.74609375},"atlasBounds":{"left":0.5,"bottom":472.5,"right":79.5,"top":572.5}},{"unicode":87,"advance":0.59999999999999998,"planeBounds":{"left":-0.0085937499999999781,"bottom":-0.03515625,"right":0.60859375000000004,"top":0.74609375},"atlasBounds":{"left":940.5,"bottom":573.5,"right":1019.5,"top":673.5}},{"unicode":88,"advance":0.59999999999999998,"planeBounds":{"left":0.0031250000000000132,"bottom":-0.03515625,"right":0.59687500000000004,"top":0.74609375},"atlasBounds":{"left":269.5,"bottom":573.5,"right":345.5,"top":673.5}},{"unicode":89,"advance":0.59999999999999998,"planeBounds":{"left":0.0031250000000000132,"bottom":-0.03515625,"right":0.59687500000000004,"top":0.74609375},"atlasBounds":{"left":682.5,"bottom":573.5,"right":758.5,"top":673.5}},{"unicode":90,"advance":0.59999999999999998,"planeBounds":{"left":0.061718750000000017,"bottom":-0.03515625,"right":0.53828125000000004,"top":0.74609375},"atlasBounds":{"left":845.5,"bottom":677.5,"right":906.5,"top":777.5}},{"unicode":91,"advance":0.59999999999999998,"planeBounds":{"left":0.20796875000000004,"bottom":-0.19140625,"right":0.49703125000000004,"top":0.74609375},"atlasBounds":{"left":314.5,"bottom":903.5,"right":351.5,"top":1023.5}},{"unicode":92,"advance":0.59999999999999998,"planeBounds":{"left":0.085156250000000003,"bottom":-0.03515625,"right":0.51484375000000004,"top":0.74609375},"atlasBounds":{"left":641.5,"bottom":677.5,"right":696.5,"top":777.5}},{"unicode":93,"advance":0.59999999999999998,"planeBounds":{"left":0.10296875000000001,"bottom":-0.18359375,"right":0.39203125,"top":0.76171875},"atlasBounds":{"left":148.5,"bottom":902.5,"right":185.5,"top":1023.5}},{"unicode":94,"advance":0.59999999999999998,"planeBounds":{"left":0.018749999999999999,"bottom":0.23046875,"right":0.58125000000000004,"top":0.75390625},"atlasBounds":{"left":487.5,"bottom":404.5,"right":559.5,"top":471.5}},{"unicode":95,"advance":0.59999999999999998,"planeBounds":{"left":-0.032031250000000018,"bottom":-0.19140625,"right":0.63203125000000004,"top":-0.03515625},"atlasBounds":{"left":759.5,"bottom":451.5,"right":844.5,"top":471.5}},{"unicode":96,"advance":0.59999999999999998,"planeBounds":{"left":0.15668750000000001,"bottom":0.57421875,"right":0.42231250000000004,"top":0.80078125},"atlasBounds":{"left":979.5,"bottom":813.5,"right":1013.5,"top":842.5}},{"unicode":97,"advance":0.59999999999999998,"planeBounds":{"left":0.036093750000000029,"bottom":-0.04296875,"right":0.54390625000000004,"top":0.58203125},"atlasBounds":{"left":616.5,"bottom":492.5,"right":681.5,"top":572.5}},{"unicode":98,"advance":0.59999999999999998,"planeBounds":{"left":0.046874999999999986,"bottom":-0.04296875,"right":0.578125,"top":0.79296875},"atlasBounds":{"left":832.5,"bottom":916.5,"right":900.5,"top":1023.5}},{"unicode":99,"advance":0.59999999999999998,"planeBounds":{"left":0.052812499999999991,"bottom":-0.04296875,"right":0.53718750000000004,"top":0.58203125},"atlasBounds":{"left":553.5,"bottom":492.5,"right":615.5,"top":572.5}},{"unicode":100,"advance":0.59999999999999998,"planeBounds":{"left":0.021874999999999985,"bottom":-0.04296875,"right":0.55312499999999998,"top":0.79296875},"atlasBounds":{"left":901.5,"bottom":916.5,"right":969.5,"top":1023.5}},{"unicode":101,"advance":0.59999999999999998,"planeBounds":{"left":0.026562499999999992,"bottom":-0.04296875,"right":0.57343750000000004,"top":0.58203125},"atlasBounds":{"left":326.5,"bottom":492.5,"right":396.5,"top":572.5}},{"unicode":102,"advance":0.59999999999999998,"planeBounds":{"left":0.032843749999999998,"bottom":-0.03515625,"right":0.60315625000000006,"top":0.80078125},"atlasBounds":{"left":0.5,"bottom":778.5,"right":73.5,"top":885.5}},{"unicode":103,"advance":0.59999999999999998,"planeBounds":{"left":0.021874999999999985,"bottom":-0.27734375,"right":0.55312499999999998,"top":0.58203125},"atlasBounds":{"left":550.5,"bottom":913.5,"right":618.5,"top":1023.5}},{"unicode":104,"advance":0.59999999999999998,"planeBounds":{"left":0.048593750000000033,"bottom":-0.03515625,"right":0.55640624999999999,"top":0.79296875},"atlasBounds":{"left":211.5,"bottom":779.5,"right":276.5,"top":885.5}},{"unicode":105,"advance":0.59999999999999998,"planeBounds":{"left":0.048281249999999984,"bottom":-0.03515625,"right":0.57171875000000005,"top":0.79296875},"atlasBounds":{"left":143.5,"bottom":779.5,"right":210.5,"top":885.5}},{"unicode":106,"advance":0.59999999999999998,"planeBounds":{"left":0.025468750000000016,"bottom":-0.27734375,"right":0.43953124999999998,"top":0.79296875},"atlasBounds":{"left":0.5,"bottom":886.5,"right":53.5,"top":1023.5}},{"unicode":107,"advance":0.59999999999999998,"planeBounds":{"left":0.071874999999999981,"bottom":-0.03515625,"right":0.60312500000000002,"top":0.79296875},"atlasBounds":{"left":74.5,"bottom":779.5,"right":142.5,"top":885.5}},{"unicode":108,"advance":0.59999999999999998,"planeBounds":{"left":0.048281249999999984,"bottom":-0.03515625,"right":0.57171875000000005,"top":0.79296875},"atlasBounds":{"left":277.5,"bottom":779.5,"right":344.5,"top":885.5}},{"unicode":109,"advance":0.59999999999999998,"planeBounds":{"left":0.01534375,"bottom":-0.03515625,"right":0.58565624999999999,"top":0.58203125},"atlasBounds":{"left":682.5,"bottom":493.5,"right":755.5,"top":572.5}},{"unicode":110,"advance":0.59999999999999998,"planeBounds":{"left":0.048593750000000033,"bottom":-0.03515625,"right":0.55640624999999999,"top":0.58203125},"atlasBounds":{"left":756.5,"bottom":493.5,"right":821.5,"top":572.5}},{"unicode":111,"advance":0.59999999999999998,"planeBounds":{"left":0.022656249999999996,"bottom":-0.04296875,"right":0.57734375000000004,"top":0.58203125},"atlasBounds":{"left":397.5,"bottom":492.5,"right":468.5,"top":572.5}},{"unicode":112,"advance":0.59999999999999998,"planeBounds":{"left":0.046874999999999986,"bottom":-0.27734375,"right":0.578125,"top":0.58203125},"atlasBounds":{"left":619.5,"bottom":913.5,"right":687.5,"top":1023.5}},{"unicode":113,"advance":0.59999999999999998,"planeBounds":{"left":0.021874999999999985,"bottom":-0.27734375,"right":0.55312499999999998,"top":0.58203125},"atlasBounds":{"left":688.5,"bottom":913.5,"right":756.5,"top":1023.5}},{"unicode":114,"advance":0.59999999999999998,"planeBounds":{"left":0.027937500000000004,"bottom":-0.03515625,"right":0.60606250000000006,"top":0.58203125},"atlasBounds":{"left":888.5,"bottom":493.5,"right":962.5,"top":572.5}},{"unicode":115,"advance":0.59999999999999998,"planeBounds":{"left":0.073437500000000003,"bottom":-0.04296875,"right":0.52656250000000004,"top":0.58203125},"atlasBounds":{"left":494.5,"bottom":492.5,"right":552.5,"top":572.5}},{"unicode":116,"advance":0.59999999999999998,"planeBounds":{"left":0.024281249999999983,"bottom":-0.04296875,"right":0.54771875000000003,"top":0.71484375},"atlasBounds":{"left":226.5,"bottom":475.5,"right":293.5,"top":572.5}},{"unicode":117,"advance":0.59999999999999998,"planeBounds":{"left":0.043593750000000028,"bottom":-0.04296875,"right":0.55140624999999999,"top":0.57421875},"atlasBounds":{"left":822.5,"bottom":493.5,"right":887.5,"top":572.5}},{"unicode":118,"advance":0.59999999999999998,"planeBounds":{"left":0.010437500000000004,"bottom":-0.03515625,"right":0.58856249999999999,"top":0.57421875},"atlasBounds":{"left":75.5,"bottom":393.5,"right":149.5,"top":471.5}},{"unicode":119,"advance":0.59999999999999998,"planeBounds":{"left":-0.015906249999999969,"bottom":-0.03515625,"right":0.61690624999999999,"top":0.57421875},"atlasBounds":{"left":150.5,"bottom":393.5,"right":231.5,"top":471.5}},{"unicode":120,"advance":0.59999999999999998,"planeBounds":{"left":0.010437500000000004,"bottom":-0.03515625,"right":0.58856249999999999,"top":0.57421875},"atlasBounds":{"left":0.5,"bottom":393.5,"right":74.5,"top":471.5}},{"unicode":121,"advance":0.59999999999999998,"planeBounds":{"left":0.010437500000000004,"bottom":-0.27734375,"right":0.58856249999999999,"top":0.57421875},"atlasBounds":{"left":757.5,"bottom":914.5,"right":831.5,"top":1023.5}},{"unicode":122,"advance":0.59999999999999998,"planeBounds":{"left":0.057812499999999996,"bottom":-0.03515625,"right":0.54218750000000004,"top":0.57421875},"atlasBounds":{"left":232.5,"bottom":393.5,"right":294.5,"top":471.5}},{"unicode":123,"advance":0.59999999999999998,"planeBounds":{"left":0.10859375,"bottom":-0.19140625,"right":0.49140624999999999,"top":0.74609375},"atlasBounds":{"left":352.5,"bottom":903.5,"right":401.5,"top":1023.5}},{"unicode":124,"advance":0.59999999999999998,"planeBounds":{"left":0.23018750000000002,"bottom":-0.27734375,"right":0.37081249999999999,"top":0.79296875},"atlasBounds":{"left":54.5,"bottom":886.5,"right":72.5,"top":1023.5}},{"unicode":125,"advance":0.59999999999999998,"planeBounds":{"left":0.10859375,"bottom":-0.19140625,"right":0.49140624999999999,"top":0.74609375},"atlasBounds":{"left":264.5,"bottom":903.5,"right":313.5,"top":1023.5}},{"unicode":126,"advance":0.59999999999999998,"planeBounds":{"left":0.041187499999999981,"bottom":0.25390625,"right":0.55681250000000004,"top":0.49609375},"atlasBounds":{"left":692.5,"bottom":440.5,"right":758.5,"top":471.5}}],"kerning":[]} diff --git a/resources/public/fonts/noto_sans_mono_atlas.png b/resources/public/fonts/noto_sans_mono_atlas.png new file mode 100644 index 0000000..f125a16 Binary files /dev/null and b/resources/public/fonts/noto_sans_mono_atlas.png differ diff --git a/resources/public/fonts/ubuntu_mono_atlas.json b/resources/public/fonts/ubuntu_mono_atlas.json new file mode 100644 index 0000000..ca8fc35 --- /dev/null +++ b/resources/public/fonts/ubuntu_mono_atlas.json @@ -0,0 +1 @@ +{"atlas":{"type":"msdf","distanceRange":8,"distanceRangeMiddle":0,"size":128,"width":1024,"height":1024,"yOrigin":"bottom"},"metrics":{"emSize":1,"lineHeight":1.121,"ascender":0.93200000000000005,"descender":-0.189,"underlineY":-0.17699999999999999,"underlineThickness":0.079000000000000001},"glyphs":[{"unicode":32,"advance":0.56000000000000005},{"unicode":33,"advance":0.56000000000000005,"planeBounds":{"left":0.1759375,"bottom":-0.05078125,"right":0.37906250000000002,"top":0.73046875},"atlasBounds":{"left":979.5,"bottom":789.5,"right":1005.5,"top":889.5}},{"unicode":34,"advance":0.56000000000000005,"planeBounds":{"left":0.12325,"bottom":0.45703125,"right":0.43575000000000003,"top":0.79296875},"atlasBounds":{"left":983.5,"bottom":737.5,"right":1023.5,"top":780.5}},{"unicode":35,"advance":0.56000000000000005,"planeBounds":{"left":-0.0012500000000000026,"bottom":-0.03515625,"right":0.56125000000000003,"top":0.73046875},"atlasBounds":{"left":718.5,"bottom":682.5,"right":790.5,"top":780.5}},{"unicode":36,"advance":0.56000000000000005,"planeBounds":{"left":0.034406249999999999,"bottom":-0.14453125,"right":0.52659374999999997,"top":0.80859375},"atlasBounds":{"left":556.5,"bottom":901.5,"right":619.5,"top":1023.5}},{"unicode":37,"advance":0.56000000000000005,"planeBounds":{"left":-0.0090624999999999924,"bottom":-0.05078125,"right":0.56906250000000003,"top":0.74609375},"atlasBounds":{"left":192.5,"bottom":787.5,"right":266.5,"top":889.5}},{"unicode":38,"advance":0.56000000000000005,"planeBounds":{"left":0.0051562499999999959,"bottom":-0.04296875,"right":0.55984374999999997,"top":0.74609375},"atlasBounds":{"left":952.5,"bottom":922.5,"right":1023.5,"top":1023.5}},{"unicode":39,"advance":0.56000000000000005,"planeBounds":{"left":0.20478125,"bottom":0.43359375,"right":0.35321875000000003,"top":0.79296875},"atlasBounds":{"left":335.5,"bottom":435.5,"right":354.5,"top":481.5}},{"unicode":40,"advance":0.56000000000000005,"planeBounds":{"left":0.10271875000000001,"bottom":-0.22265625,"right":0.45428125000000003,"top":0.81640625},"atlasBounds":{"left":0.5,"bottom":890.5,"right":45.5,"top":1023.5}},{"unicode":41,"advance":0.56000000000000005,"planeBounds":{"left":0.10471875000000001,"bottom":-0.22265625,"right":0.45628125000000003,"top":0.81640625},"atlasBounds":{"left":46.5,"bottom":890.5,"right":91.5,"top":1023.5}},{"unicode":42,"advance":0.56000000000000005,"planeBounds":{"left":0.033406249999999992,"bottom":0.25390625,"right":0.52559374999999997,"top":0.73046875},"atlasBounds":{"left":204.5,"bottom":420.5,"right":267.5,"top":481.5}},{"unicode":43,"advance":0.56000000000000005,"planeBounds":{"left":0.019781249999999979,"bottom":0.01171875,"right":0.54321874999999997,"top":0.57421875},"atlasBounds":{"left":0.5,"bottom":409.5,"right":67.5,"top":481.5}},{"unicode":44,"advance":0.56000000000000005,"planeBounds":{"left":0.13996875,"bottom":-0.19140625,"right":0.42903125000000003,"top":0.18359375},"atlasBounds":{"left":980.5,"bottom":532.5,"right":1017.5,"top":580.5}},{"unicode":45,"advance":0.56000000000000005,"planeBounds":{"left":0.1315625,"bottom":0.21484375,"right":0.42843750000000003,"top":0.35546875},"atlasBounds":{"left":983.5,"bottom":718.5,"right":1021.5,"top":736.5}},{"unicode":46,"advance":0.56000000000000005,"planeBounds":{"left":0.171125,"bottom":-0.05078125,"right":0.38987500000000003,"top":0.18359375},"atlasBounds":{"left":423.5,"bottom":451.5,"right":451.5,"top":481.5}},{"unicode":47,"advance":0.56000000000000005,"planeBounds":{"left":0.049531250000000006,"bottom":-0.22265625,"right":0.51046875000000003,"top":0.81640625},"atlasBounds":{"left":227.5,"bottom":890.5,"right":286.5,"top":1023.5}},{"unicode":48,"advance":0.56000000000000005,"planeBounds":{"left":0.022187500000000034,"bottom":-0.05078125,"right":0.53781250000000003,"top":0.74609375},"atlasBounds":{"left":267.5,"bottom":787.5,"right":333.5,"top":889.5}},{"unicode":49,"advance":0.56000000000000005,"planeBounds":{"left":0.058031250000000006,"bottom":-0.03515625,"right":0.51896874999999998,"top":0.73046875},"atlasBounds":{"left":923.5,"bottom":682.5,"right":982.5,"top":780.5}},{"unicode":50,"advance":0.56000000000000005,"planeBounds":{"left":0.034406249999999992,"bottom":-0.03515625,"right":0.52659374999999997,"top":0.74609375},"atlasBounds":{"left":0.5,"bottom":680.5,"right":63.5,"top":780.5}},{"unicode":51,"advance":0.56000000000000005,"planeBounds":{"left":0.039812499999999994,"bottom":-0.05078125,"right":0.52418750000000003,"top":0.74609375},"atlasBounds":{"left":334.5,"bottom":787.5,"right":396.5,"top":889.5}},{"unicode":52,"advance":0.56000000000000005,"planeBounds":{"left":0.011468749999999989,"bottom":-0.03515625,"right":0.55053125000000003,"top":0.73046875},"atlasBounds":{"left":809.5,"bottom":581.5,"right":878.5,"top":679.5}},{"unicode":53,"advance":0.56000000000000005,"planeBounds":{"left":0.050718750000000014,"bottom":-0.05078125,"right":0.52728125000000003,"top":0.73046875},"atlasBounds":{"left":64.5,"bottom":680.5,"right":125.5,"top":780.5}},{"unicode":54,"advance":0.56000000000000005,"planeBounds":{"left":0.027593750000000028,"bottom":-0.05078125,"right":0.53540624999999997,"top":0.73046875},"atlasBounds":{"left":126.5,"bottom":680.5,"right":191.5,"top":780.5}},{"unicode":55,"advance":0.56000000000000005,"planeBounds":{"left":0.043500000000000004,"bottom":-0.03515625,"right":0.54349999999999998,"top":0.73046875},"atlasBounds":{"left":0.5,"bottom":482.5,"right":64.5,"top":580.5}},{"unicode":56,"advance":0.56000000000000005,"planeBounds":{"left":0.029500000000000002,"bottom":-0.05078125,"right":0.52949999999999997,"top":0.74609375},"atlasBounds":{"left":397.5,"bottom":787.5,"right":461.5,"top":889.5}},{"unicode":57,"advance":0.56000000000000005,"planeBounds":{"left":0.024593750000000029,"bottom":-0.03515625,"right":0.53240624999999997,"top":0.74609375},"atlasBounds":{"left":192.5,"bottom":680.5,"right":257.5,"top":780.5}},{"unicode":58,"advance":0.56000000000000005,"planeBounds":{"left":0.171125,"bottom":-0.05078125,"right":0.38987500000000003,"top":0.55078125},"atlasBounds":{"left":489.5,"bottom":503.5,"right":517.5,"top":580.5}},{"unicode":59,"advance":0.56000000000000005,"planeBounds":{"left":0.10796875000000002,"bottom":-0.19140625,"right":0.39703125,"top":0.55078125},"atlasBounds":{"left":65.5,"bottom":485.5,"right":102.5,"top":580.5}},{"unicode":60,"advance":0.56000000000000005,"planeBounds":{"left":0.022281249999999978,"bottom":0.03515625,"right":0.54571875000000003,"top":0.53515625},"atlasBounds":{"left":68.5,"bottom":417.5,"right":135.5,"top":481.5}},{"unicode":61,"advance":0.56000000000000005,"planeBounds":{"left":0.019781249999999979,"bottom":0.12109375,"right":0.54321874999999997,"top":0.47265625},"atlasBounds":{"left":355.5,"bottom":436.5,"right":422.5,"top":481.5}},{"unicode":62,"advance":0.56000000000000005,"planeBounds":{"left":0.023781249999999979,"bottom":0.03515625,"right":0.54721874999999998,"top":0.53515625},"atlasBounds":{"left":136.5,"bottom":417.5,"right":203.5,"top":481.5}},{"unicode":63,"advance":0.56000000000000005,"planeBounds":{"left":0.077875000000000014,"bottom":-0.05078125,"right":0.48412500000000003,"top":0.74609375},"atlasBounds":{"left":462.5,"bottom":787.5,"right":514.5,"top":889.5}},{"unicode":64,"advance":0.56000000000000005,"planeBounds":{"left":0.018968749999999986,"bottom":-0.18359375,"right":0.55803124999999998,"top":0.74609375},"atlasBounds":{"left":620.5,"bottom":904.5,"right":689.5,"top":1023.5}},{"unicode":65,"advance":0.56000000000000005,"planeBounds":{"left":-0.024687499999999984,"bottom":-0.03515625,"right":0.58468750000000003,"top":0.73046875},"atlasBounds":{"left":660.5,"bottom":581.5,"right":738.5,"top":679.5}},{"unicode":66,"advance":0.56000000000000005,"planeBounds":{"left":0.02559375000000003,"bottom":-0.04296875,"right":0.53340624999999997,"top":0.73828125},"atlasBounds":{"left":258.5,"bottom":680.5,"right":323.5,"top":780.5}},{"unicode":67,"advance":0.56000000000000005,"planeBounds":{"left":0.027687499999999979,"bottom":-0.05078125,"right":0.54331249999999998,"top":0.74609375},"atlasBounds":{"left":515.5,"bottom":787.5,"right":581.5,"top":889.5}},{"unicode":68,"advance":0.56000000000000005,"planeBounds":{"left":0.027687499999999979,"bottom":-0.04296875,"right":0.54331249999999998,"top":0.73828125},"atlasBounds":{"left":324.5,"bottom":680.5,"right":390.5,"top":780.5}},{"unicode":69,"advance":0.56000000000000005,"planeBounds":{"left":0.078625000000000014,"bottom":-0.03515625,"right":0.54737500000000006,"top":0.73046875},"atlasBounds":{"left":390.5,"bottom":581.5,"right":450.5,"top":679.5}},{"unicode":70,"advance":0.56000000000000005,"planeBounds":{"left":0.076437500000000005,"bottom":-0.03515625,"right":0.52956250000000005,"top":0.73046875},"atlasBounds":{"left":331.5,"bottom":581.5,"right":389.5,"top":679.5}},{"unicode":71,"advance":0.56000000000000005,"planeBounds":{"left":0.025687500000000033,"bottom":-0.05078125,"right":0.54131249999999997,"top":0.74609375},"atlasBounds":{"left":582.5,"bottom":787.5,"right":648.5,"top":889.5}},{"unicode":72,"advance":0.56000000000000005,"planeBounds":{"left":0.018281249999999982,"bottom":-0.03515625,"right":0.54171875000000003,"top":0.73046875},"atlasBounds":{"left":202.5,"bottom":581.5,"right":269.5,"top":679.5}},{"unicode":73,"advance":0.56000000000000005,"planeBounds":{"left":0.076875000000000013,"bottom":-0.03515625,"right":0.48312500000000003,"top":0.73046875},"atlasBounds":{"left":149.5,"bottom":581.5,"right":201.5,"top":679.5}},{"unicode":74,"advance":0.56000000000000005,"planeBounds":{"left":0.029625000000000012,"bottom":-0.05078125,"right":0.49837500000000001,"top":0.73046875},"atlasBounds":{"left":391.5,"bottom":680.5,"right":451.5,"top":780.5}},{"unicode":75,"advance":0.56000000000000005,"planeBounds":{"left":0.045468749999999988,"bottom":-0.03515625,"right":0.58453125000000006,"top":0.73046875},"atlasBounds":{"left":739.5,"bottom":581.5,"right":808.5,"top":679.5}},{"unicode":76,"advance":0.56000000000000005,"planeBounds":{"left":0.077625000000000013,"bottom":-0.03515625,"right":0.54637500000000006,"top":0.73046875},"atlasBounds":{"left":270.5,"bottom":581.5,"right":330.5,"top":679.5}},{"unicode":77,"advance":0.56000000000000005,"planeBounds":{"left":0.006562499999999992,"bottom":-0.03515625,"right":0.55343750000000003,"top":0.73046875},"atlasBounds":{"left":0.5,"bottom":581.5,"right":70.5,"top":679.5}},{"unicode":78,"advance":0.56000000000000005,"planeBounds":{"left":0.029999999999999995,"bottom":-0.03515625,"right":0.53000000000000003,"top":0.73046875},"atlasBounds":{"left":522.5,"bottom":581.5,"right":586.5,"top":679.5}},{"unicode":79,"advance":0.56000000000000005,"planeBounds":{"left":-0.00025000000000000179,"bottom":-0.05078125,"right":0.56225000000000003,"top":0.74609375},"atlasBounds":{"left":649.5,"bottom":787.5,"right":721.5,"top":889.5}},{"unicode":80,"advance":0.56000000000000005,"planeBounds":{"left":0.054312500000000014,"bottom":-0.03515625,"right":0.53868749999999999,"top":0.73828125},"atlasBounds":{"left":588.5,"bottom":681.5,"right":650.5,"top":780.5}},{"unicode":81,"advance":0.56000000000000005,"planeBounds":{"left":-0.00025000000000000179,"bottom":-0.21484375,"right":0.56225000000000003,"top":0.74609375},"atlasBounds":{"left":483.5,"bottom":900.5,"right":555.5,"top":1023.5}},{"unicode":82,"advance":0.56000000000000005,"planeBounds":{"left":0.028687500000000032,"bottom":-0.03515625,"right":0.54431249999999998,"top":0.73828125},"atlasBounds":{"left":651.5,"bottom":681.5,"right":717.5,"top":780.5}},{"unicode":83,"advance":0.56000000000000005,"planeBounds":{"left":0.033906249999999992,"bottom":-0.05078125,"right":0.52609375000000003,"top":0.74609375},"atlasBounds":{"left":722.5,"bottom":787.5,"right":785.5,"top":889.5}},{"unicode":84,"advance":0.56000000000000005,"planeBounds":{"left":0.014374999999999983,"bottom":-0.03515625,"right":0.54562500000000003,"top":0.73046875},"atlasBounds":{"left":854.5,"bottom":682.5,"right":922.5,"top":780.5}},{"unicode":85,"advance":0.56000000000000005,"planeBounds":{"left":0.022187500000000034,"bottom":-0.05078125,"right":0.53781250000000003,"top":0.73046875},"atlasBounds":{"left":452.5,"bottom":680.5,"right":518.5,"top":780.5}},{"unicode":86,"advance":0.56000000000000005,"planeBounds":{"left":-0.019781249999999986,"bottom":-0.03515625,"right":0.58178125000000003,"top":0.73046875},"atlasBounds":{"left":71.5,"bottom":581.5,"right":148.5,"top":679.5}},{"unicode":87,"advance":0.56000000000000005,"planeBounds":{"left":0.006562499999999992,"bottom":-0.03515625,"right":0.55343750000000003,"top":0.73046875},"atlasBounds":{"left":451.5,"bottom":581.5,"right":521.5,"top":679.5}},{"unicode":88,"advance":0.56000000000000005,"planeBounds":{"left":-0.0012500000000000026,"bottom":-0.03515625,"right":0.56125000000000003,"top":0.73046875},"atlasBounds":{"left":587.5,"bottom":581.5,"right":659.5,"top":679.5}},{"unicode":89,"advance":0.56000000000000005,"planeBounds":{"left":-0.019781249999999986,"bottom":-0.03515625,"right":0.58178125000000003,"top":0.73046875},"atlasBounds":{"left":946.5,"bottom":581.5,"right":1023.5,"top":679.5}},{"unicode":90,"advance":0.56000000000000005,"planeBounds":{"left":0.028687500000000032,"bottom":-0.03515625,"right":0.54431249999999998,"top":0.73046875},"atlasBounds":{"left":879.5,"bottom":581.5,"right":945.5,"top":679.5}},{"unicode":91,"advance":0.56000000000000005,"planeBounds":{"left":0.13734375000000001,"bottom":-0.22265625,"right":0.45765624999999999,"top":0.81640625},"atlasBounds":{"left":287.5,"bottom":890.5,"right":328.5,"top":1023.5}},{"unicode":92,"advance":0.56000000000000005,"planeBounds":{"left":0.052937500000000005,"bottom":-0.22265625,"right":0.50606249999999997,"top":0.81640625},"atlasBounds":{"left":329.5,"bottom":890.5,"right":387.5,"top":1023.5}},{"unicode":93,"advance":0.56000000000000005,"planeBounds":{"left":0.10234375,"bottom":-0.22265625,"right":0.42265625000000001,"top":0.81640625},"atlasBounds":{"left":388.5,"bottom":890.5,"right":429.5,"top":1023.5}},{"unicode":94,"advance":0.56000000000000005,"planeBounds":{"left":0.021187499999999981,"bottom":0.28515625,"right":0.53681250000000003,"top":0.73046875},"atlasBounds":{"left":268.5,"bottom":424.5,"right":334.5,"top":481.5}},{"unicode":95,"advance":0.56000000000000005,"planeBounds":{"left":-0.024687499999999984,"bottom":-0.22265625,"right":0.58468750000000003,"top":-0.08203125},"atlasBounds":{"left":521.5,"bottom":463.5,"right":599.5,"top":481.5}},{"unicode":96,"advance":0.56000000000000005,"planeBounds":{"left":0.15431249999999999,"bottom":0.54296875,"right":0.38868750000000002,"top":0.80859375},"atlasBounds":{"left":983.5,"bottom":683.5,"right":1013.5,"top":717.5}},{"unicode":97,"advance":0.56000000000000005,"planeBounds":{"left":0.037218750000000016,"bottom":-0.05078125,"right":0.51378124999999997,"top":0.56640625},"atlasBounds":{"left":169.5,"bottom":501.5,"right":230.5,"top":580.5}},{"unicode":98,"advance":0.56000000000000005,"planeBounds":{"left":0.051406250000000001,"bottom":-0.05078125,"right":0.54359374999999999,"top":0.80859375},"atlasBounds":{"left":690.5,"bottom":913.5,"right":753.5,"top":1023.5}},{"unicode":99,"advance":0.56000000000000005,"planeBounds":{"left":0.024593750000000029,"bottom":-0.05078125,"right":0.53240624999999997,"top":0.56640625},"atlasBounds":{"left":103.5,"bottom":501.5,"right":168.5,"top":580.5}},{"unicode":100,"advance":0.56000000000000005,"planeBounds":{"left":0.013999999999999997,"bottom":-0.05078125,"right":0.51400000000000001,"top":0.80859375},"atlasBounds":{"left":754.5,"bottom":913.5,"right":818.5,"top":1023.5}},{"unicode":101,"advance":0.56000000000000005,"planeBounds":{"left":0.015781249999999983,"bottom":-0.05078125,"right":0.53921874999999997,"top":0.56640625},"atlasBounds":{"left":292.5,"bottom":501.5,"right":359.5,"top":580.5}},{"unicode":102,"advance":0.56000000000000005,"planeBounds":{"left":0.046781249999999983,"bottom":-0.03515625,"right":0.57021875,"top":0.80859375},"atlasBounds":{"left":884.5,"bottom":915.5,"right":951.5,"top":1023.5}},{"unicode":103,"advance":0.56000000000000005,"planeBounds":{"left":0.011999999999999997,"bottom":-0.22265625,"right":0.51200000000000001,"top":0.56640625},"atlasBounds":{"left":786.5,"bottom":788.5,"right":850.5,"top":889.5}},{"unicode":104,"advance":0.56000000000000005,"planeBounds":{"left":0.051531250000000015,"bottom":-0.03515625,"right":0.51246875000000003,"top":0.80859375},"atlasBounds":{"left":0.5,"bottom":781.5,"right":59.5,"top":889.5}},{"unicode":105,"advance":0.56000000000000005,"planeBounds":{"left":0.031999999999999994,"bottom":-0.05078125,"right":0.53200000000000003,"top":0.77734375},"atlasBounds":{"left":127.5,"bottom":783.5,"right":191.5,"top":889.5}},{"unicode":106,"advance":0.56000000000000005,"planeBounds":{"left":0.053375000000000013,"bottom":-0.22265625,"right":0.45962500000000001,"top":0.77734375},"atlasBounds":{"left":430.5,"bottom":895.5,"right":482.5,"top":1023.5}},{"unicode":107,"advance":0.56000000000000005,"planeBounds":{"left":0.049687500000000037,"bottom":-0.03515625,"right":0.5653125,"top":0.80859375},"atlasBounds":{"left":60.5,"bottom":781.5,"right":126.5,"top":889.5}},{"unicode":108,"advance":0.56000000000000005,"planeBounds":{"left":0.031999999999999994,"bottom":-0.05078125,"right":0.53200000000000003,"top":0.80859375},"atlasBounds":{"left":819.5,"bottom":913.5,"right":883.5,"top":1023.5}},{"unicode":109,"advance":0.56000000000000005,"planeBounds":{"left":0.018374999999999985,"bottom":-0.03515625,"right":0.54962500000000003,"top":0.56640625},"atlasBounds":{"left":518.5,"bottom":503.5,"right":586.5,"top":580.5}},{"unicode":110,"advance":0.56000000000000005,"planeBounds":{"left":0.051531250000000015,"bottom":-0.03515625,"right":0.51246875000000003,"top":0.56640625},"atlasBounds":{"left":429.5,"bottom":503.5,"right":488.5,"top":580.5}},{"unicode":111,"advance":0.56000000000000005,"planeBounds":{"left":0.014374999999999983,"bottom":-0.04296875,"right":0.54562500000000003,"top":0.56640625},"atlasBounds":{"left":360.5,"bottom":502.5,"right":428.5,"top":580.5}},{"unicode":112,"advance":0.56000000000000005,"planeBounds":{"left":0.051406250000000001,"bottom":-0.22265625,"right":0.54359374999999999,"top":0.56640625},"atlasBounds":{"left":851.5,"bottom":788.5,"right":914.5,"top":889.5}},{"unicode":113,"advance":0.56000000000000005,"planeBounds":{"left":0.016406249999999997,"bottom":-0.22265625,"right":0.50859374999999996,"top":0.56640625},"atlasBounds":{"left":915.5,"bottom":788.5,"right":978.5,"top":889.5}},{"unicode":114,"advance":0.56000000000000005,"planeBounds":{"left":0.094062500000000007,"bottom":-0.03515625,"right":0.51593750000000005,"top":0.56640625},"atlasBounds":{"left":587.5,"bottom":503.5,"right":641.5,"top":580.5}},{"unicode":115,"advance":0.56000000000000005,"planeBounds":{"left":0.046125000000000013,"bottom":-0.05078125,"right":0.51487499999999997,"top":0.56640625},"atlasBounds":{"left":231.5,"bottom":501.5,"right":291.5,"top":580.5}},{"unicode":116,"advance":0.56000000000000005,"planeBounds":{"left":0.04631250000000002,"bottom":-0.05078125,"right":0.53068749999999998,"top":0.71484375},"atlasBounds":{"left":791.5,"bottom":682.5,"right":853.5,"top":780.5}},{"unicode":117,"advance":0.56000000000000005,"planeBounds":{"left":0.047531250000000018,"bottom":-0.04296875,"right":0.50846875000000002,"top":0.55078125},"atlasBounds":{"left":642.5,"bottom":504.5,"right":701.5,"top":580.5}},{"unicode":118,"advance":0.56000000000000005,"planeBounds":{"left":0.006562499999999992,"bottom":-0.03515625,"right":0.55343750000000003,"top":0.55078125},"atlasBounds":{"left":702.5,"bottom":505.5,"right":772.5,"top":580.5}},{"unicode":119,"advance":0.56000000000000005,"planeBounds":{"left":-0.0090624999999999942,"bottom":-0.03515625,"right":0.56906250000000003,"top":0.55078125},"atlasBounds":{"left":845.5,"bottom":505.5,"right":919.5,"top":580.5}},{"unicode":120,"advance":0.56000000000000005,"planeBounds":{"left":0.0031562499999999963,"bottom":-0.03515625,"right":0.55784374999999997,"top":0.55078125},"atlasBounds":{"left":773.5,"bottom":505.5,"right":844.5,"top":580.5}},{"unicode":121,"advance":0.56000000000000005,"planeBounds":{"left":0.012874999999999985,"bottom":-0.22265625,"right":0.54412499999999997,"top":0.55078125},"atlasBounds":{"left":519.5,"bottom":681.5,"right":587.5,"top":780.5}},{"unicode":122,"advance":0.56000000000000005,"planeBounds":{"left":0.049031250000000005,"bottom":-0.03515625,"right":0.50996874999999997,"top":0.55078125},"atlasBounds":{"left":920.5,"bottom":505.5,"right":979.5,"top":580.5}},{"unicode":123,"advance":0.56000000000000005,"planeBounds":{"left":0.061343750000000002,"bottom":-0.22265625,"right":0.50665625000000003,"top":0.81640625},"atlasBounds":{"left":169.5,"bottom":890.5,"right":226.5,"top":1023.5}},{"unicode":124,"advance":0.56000000000000005,"planeBounds":{"left":0.2101875,"bottom":-0.22265625,"right":0.35081250000000003,"top":0.81640625},"atlasBounds":{"left":150.5,"bottom":890.5,"right":168.5,"top":1023.5}},{"unicode":125,"advance":0.56000000000000005,"planeBounds":{"left":0.053343750000000002,"bottom":-0.22265625,"right":0.49865625000000002,"top":0.81640625},"atlasBounds":{"left":92.5,"bottom":890.5,"right":149.5,"top":1023.5}},{"unicode":126,"advance":0.56000000000000005,"planeBounds":{"left":0.012874999999999985,"bottom":0.18359375,"right":0.54412499999999997,"top":0.40234375},"atlasBounds":{"left":452.5,"bottom":453.5,"right":520.5,"top":481.5}}],"kerning":[]} diff --git a/resources/public/fonts/ubuntu_mono_atlas.png b/resources/public/fonts/ubuntu_mono_atlas.png new file mode 100644 index 0000000..f06ecfe Binary files /dev/null and b/resources/public/fonts/ubuntu_mono_atlas.png differ diff --git a/resources/public/fonts/ubuntu_sans_mono_atlas.json b/resources/public/fonts/ubuntu_sans_mono_atlas.json new file mode 100644 index 0000000..e3716b5 --- /dev/null +++ b/resources/public/fonts/ubuntu_sans_mono_atlas.json @@ -0,0 +1 @@ +{"atlas":{"type":"msdf","distanceRange":8,"distanceRangeMiddle":0,"size":128,"width":1024,"height":1024,"yOrigin":"bottom"},"metrics":{"emSize":1,"lineHeight":1.2,"ascender":0.94000000000000006,"descender":-0.26000000000000001,"underlineY":-0.17699999999999999,"underlineThickness":0.079000000000000001},"glyphs":[{"unicode":32,"advance":0.56000000000000005},{"unicode":33,"advance":0.56000000000000005,"planeBounds":{"left":0.1759375,"bottom":-0.05078125,"right":0.37906250000000002,"top":0.73046875},"atlasBounds":{"left":979.5,"bottom":789.5,"right":1005.5,"top":889.5}},{"unicode":34,"advance":0.56000000000000005,"planeBounds":{"left":0.12325,"bottom":0.45703125,"right":0.43575000000000003,"top":0.79296875},"atlasBounds":{"left":983.5,"bottom":737.5,"right":1023.5,"top":780.5}},{"unicode":35,"advance":0.56000000000000005,"planeBounds":{"left":-0.0012500000000000026,"bottom":-0.03515625,"right":0.56125000000000003,"top":0.73046875},"atlasBounds":{"left":718.5,"bottom":682.5,"right":790.5,"top":780.5}},{"unicode":36,"advance":0.56000000000000005,"planeBounds":{"left":0.034406249999999999,"bottom":-0.14453125,"right":0.52659374999999997,"top":0.80859375},"atlasBounds":{"left":556.5,"bottom":901.5,"right":619.5,"top":1023.5}},{"unicode":37,"advance":0.56000000000000005,"planeBounds":{"left":-0.0090624999999999924,"bottom":-0.05078125,"right":0.56906250000000003,"top":0.74609375},"atlasBounds":{"left":192.5,"bottom":787.5,"right":266.5,"top":889.5}},{"unicode":38,"advance":0.56000000000000005,"planeBounds":{"left":0.0051562499999999959,"bottom":-0.04296875,"right":0.55984374999999997,"top":0.74609375},"atlasBounds":{"left":952.5,"bottom":922.5,"right":1023.5,"top":1023.5}},{"unicode":39,"advance":0.56000000000000005,"planeBounds":{"left":0.20478125,"bottom":0.43359375,"right":0.35321875000000003,"top":0.79296875},"atlasBounds":{"left":335.5,"bottom":435.5,"right":354.5,"top":481.5}},{"unicode":40,"advance":0.56000000000000005,"planeBounds":{"left":0.10271875000000001,"bottom":-0.22265625,"right":0.45428125000000003,"top":0.81640625},"atlasBounds":{"left":0.5,"bottom":890.5,"right":45.5,"top":1023.5}},{"unicode":41,"advance":0.56000000000000005,"planeBounds":{"left":0.10471875000000001,"bottom":-0.22265625,"right":0.45628125000000003,"top":0.81640625},"atlasBounds":{"left":46.5,"bottom":890.5,"right":91.5,"top":1023.5}},{"unicode":42,"advance":0.56000000000000005,"planeBounds":{"left":0.033406249999999992,"bottom":0.25390625,"right":0.52559374999999997,"top":0.73046875},"atlasBounds":{"left":204.5,"bottom":420.5,"right":267.5,"top":481.5}},{"unicode":43,"advance":0.56000000000000005,"planeBounds":{"left":0.019781249999999979,"bottom":0.01171875,"right":0.54321874999999997,"top":0.57421875},"atlasBounds":{"left":0.5,"bottom":409.5,"right":67.5,"top":481.5}},{"unicode":44,"advance":0.56000000000000005,"planeBounds":{"left":0.13996875,"bottom":-0.19140625,"right":0.42903125000000003,"top":0.18359375},"atlasBounds":{"left":980.5,"bottom":532.5,"right":1017.5,"top":580.5}},{"unicode":45,"advance":0.56000000000000005,"planeBounds":{"left":0.1315625,"bottom":0.21484375,"right":0.42843750000000003,"top":0.35546875},"atlasBounds":{"left":983.5,"bottom":718.5,"right":1021.5,"top":736.5}},{"unicode":46,"advance":0.56000000000000005,"planeBounds":{"left":0.171125,"bottom":-0.05078125,"right":0.38987500000000003,"top":0.18359375},"atlasBounds":{"left":423.5,"bottom":451.5,"right":451.5,"top":481.5}},{"unicode":47,"advance":0.56000000000000005,"planeBounds":{"left":0.049531250000000006,"bottom":-0.22265625,"right":0.51046875000000003,"top":0.81640625},"atlasBounds":{"left":227.5,"bottom":890.5,"right":286.5,"top":1023.5}},{"unicode":48,"advance":0.56000000000000005,"planeBounds":{"left":0.022187500000000034,"bottom":-0.05078125,"right":0.53781250000000003,"top":0.74609375},"atlasBounds":{"left":267.5,"bottom":787.5,"right":333.5,"top":889.5}},{"unicode":49,"advance":0.56000000000000005,"planeBounds":{"left":0.058031250000000006,"bottom":-0.03515625,"right":0.51896874999999998,"top":0.73046875},"atlasBounds":{"left":923.5,"bottom":682.5,"right":982.5,"top":780.5}},{"unicode":50,"advance":0.56000000000000005,"planeBounds":{"left":0.034406249999999992,"bottom":-0.03515625,"right":0.52659374999999997,"top":0.74609375},"atlasBounds":{"left":0.5,"bottom":680.5,"right":63.5,"top":780.5}},{"unicode":51,"advance":0.56000000000000005,"planeBounds":{"left":0.039812499999999994,"bottom":-0.05078125,"right":0.52418750000000003,"top":0.74609375},"atlasBounds":{"left":334.5,"bottom":787.5,"right":396.5,"top":889.5}},{"unicode":52,"advance":0.56000000000000005,"planeBounds":{"left":0.011468749999999989,"bottom":-0.03515625,"right":0.55053125000000003,"top":0.73046875},"atlasBounds":{"left":809.5,"bottom":581.5,"right":878.5,"top":679.5}},{"unicode":53,"advance":0.56000000000000005,"planeBounds":{"left":0.050718750000000014,"bottom":-0.05078125,"right":0.52728125000000003,"top":0.73046875},"atlasBounds":{"left":64.5,"bottom":680.5,"right":125.5,"top":780.5}},{"unicode":54,"advance":0.56000000000000005,"planeBounds":{"left":0.027593750000000028,"bottom":-0.05078125,"right":0.53540624999999997,"top":0.73046875},"atlasBounds":{"left":126.5,"bottom":680.5,"right":191.5,"top":780.5}},{"unicode":55,"advance":0.56000000000000005,"planeBounds":{"left":0.043500000000000004,"bottom":-0.03515625,"right":0.54349999999999998,"top":0.73046875},"atlasBounds":{"left":0.5,"bottom":482.5,"right":64.5,"top":580.5}},{"unicode":56,"advance":0.56000000000000005,"planeBounds":{"left":0.029500000000000002,"bottom":-0.05078125,"right":0.52949999999999997,"top":0.74609375},"atlasBounds":{"left":397.5,"bottom":787.5,"right":461.5,"top":889.5}},{"unicode":57,"advance":0.56000000000000005,"planeBounds":{"left":0.024593750000000029,"bottom":-0.03515625,"right":0.53240624999999997,"top":0.74609375},"atlasBounds":{"left":192.5,"bottom":680.5,"right":257.5,"top":780.5}},{"unicode":58,"advance":0.56000000000000005,"planeBounds":{"left":0.171125,"bottom":-0.05078125,"right":0.38987500000000003,"top":0.55078125},"atlasBounds":{"left":489.5,"bottom":503.5,"right":517.5,"top":580.5}},{"unicode":59,"advance":0.56000000000000005,"planeBounds":{"left":0.10796875000000002,"bottom":-0.19140625,"right":0.39703125,"top":0.55078125},"atlasBounds":{"left":65.5,"bottom":485.5,"right":102.5,"top":580.5}},{"unicode":60,"advance":0.56000000000000005,"planeBounds":{"left":0.022281249999999978,"bottom":0.03515625,"right":0.54571875000000003,"top":0.53515625},"atlasBounds":{"left":68.5,"bottom":417.5,"right":135.5,"top":481.5}},{"unicode":61,"advance":0.56000000000000005,"planeBounds":{"left":0.019781249999999979,"bottom":0.12109375,"right":0.54321874999999997,"top":0.47265625},"atlasBounds":{"left":355.5,"bottom":436.5,"right":422.5,"top":481.5}},{"unicode":62,"advance":0.56000000000000005,"planeBounds":{"left":0.023781249999999979,"bottom":0.03515625,"right":0.54721874999999998,"top":0.53515625},"atlasBounds":{"left":136.5,"bottom":417.5,"right":203.5,"top":481.5}},{"unicode":63,"advance":0.56000000000000005,"planeBounds":{"left":0.077875000000000014,"bottom":-0.05078125,"right":0.48412500000000003,"top":0.74609375},"atlasBounds":{"left":462.5,"bottom":787.5,"right":514.5,"top":889.5}},{"unicode":64,"advance":0.56000000000000005,"planeBounds":{"left":0.018968749999999986,"bottom":-0.18359375,"right":0.55803124999999998,"top":0.74609375},"atlasBounds":{"left":620.5,"bottom":904.5,"right":689.5,"top":1023.5}},{"unicode":65,"advance":0.56000000000000005,"planeBounds":{"left":-0.024687499999999984,"bottom":-0.03515625,"right":0.58468750000000003,"top":0.73046875},"atlasBounds":{"left":660.5,"bottom":581.5,"right":738.5,"top":679.5}},{"unicode":66,"advance":0.56000000000000005,"planeBounds":{"left":0.02559375000000003,"bottom":-0.04296875,"right":0.53340624999999997,"top":0.73828125},"atlasBounds":{"left":258.5,"bottom":680.5,"right":323.5,"top":780.5}},{"unicode":67,"advance":0.56000000000000005,"planeBounds":{"left":0.027687499999999979,"bottom":-0.05078125,"right":0.54331249999999998,"top":0.74609375},"atlasBounds":{"left":515.5,"bottom":787.5,"right":581.5,"top":889.5}},{"unicode":68,"advance":0.56000000000000005,"planeBounds":{"left":0.027687499999999979,"bottom":-0.04296875,"right":0.54331249999999998,"top":0.73828125},"atlasBounds":{"left":324.5,"bottom":680.5,"right":390.5,"top":780.5}},{"unicode":69,"advance":0.56000000000000005,"planeBounds":{"left":0.078625000000000014,"bottom":-0.03515625,"right":0.54737500000000006,"top":0.73046875},"atlasBounds":{"left":390.5,"bottom":581.5,"right":450.5,"top":679.5}},{"unicode":70,"advance":0.56000000000000005,"planeBounds":{"left":0.076437500000000005,"bottom":-0.03515625,"right":0.52956250000000005,"top":0.73046875},"atlasBounds":{"left":331.5,"bottom":581.5,"right":389.5,"top":679.5}},{"unicode":71,"advance":0.56000000000000005,"planeBounds":{"left":0.025687500000000033,"bottom":-0.05078125,"right":0.54131249999999997,"top":0.74609375},"atlasBounds":{"left":582.5,"bottom":787.5,"right":648.5,"top":889.5}},{"unicode":72,"advance":0.56000000000000005,"planeBounds":{"left":0.018281249999999982,"bottom":-0.03515625,"right":0.54171875000000003,"top":0.73046875},"atlasBounds":{"left":202.5,"bottom":581.5,"right":269.5,"top":679.5}},{"unicode":73,"advance":0.56000000000000005,"planeBounds":{"left":0.076875000000000013,"bottom":-0.03515625,"right":0.48312500000000003,"top":0.73046875},"atlasBounds":{"left":149.5,"bottom":581.5,"right":201.5,"top":679.5}},{"unicode":74,"advance":0.56000000000000005,"planeBounds":{"left":0.029625000000000012,"bottom":-0.05078125,"right":0.49837500000000001,"top":0.73046875},"atlasBounds":{"left":391.5,"bottom":680.5,"right":451.5,"top":780.5}},{"unicode":75,"advance":0.56000000000000005,"planeBounds":{"left":0.045468749999999988,"bottom":-0.03515625,"right":0.58453125000000006,"top":0.73046875},"atlasBounds":{"left":739.5,"bottom":581.5,"right":808.5,"top":679.5}},{"unicode":76,"advance":0.56000000000000005,"planeBounds":{"left":0.077625000000000013,"bottom":-0.03515625,"right":0.54637500000000006,"top":0.73046875},"atlasBounds":{"left":270.5,"bottom":581.5,"right":330.5,"top":679.5}},{"unicode":77,"advance":0.56000000000000005,"planeBounds":{"left":0.006562499999999992,"bottom":-0.03515625,"right":0.55343750000000003,"top":0.73046875},"atlasBounds":{"left":0.5,"bottom":581.5,"right":70.5,"top":679.5}},{"unicode":78,"advance":0.56000000000000005,"planeBounds":{"left":0.029999999999999995,"bottom":-0.03515625,"right":0.53000000000000003,"top":0.73046875},"atlasBounds":{"left":522.5,"bottom":581.5,"right":586.5,"top":679.5}},{"unicode":79,"advance":0.56000000000000005,"planeBounds":{"left":-0.00025000000000000179,"bottom":-0.05078125,"right":0.56225000000000003,"top":0.74609375},"atlasBounds":{"left":649.5,"bottom":787.5,"right":721.5,"top":889.5}},{"unicode":80,"advance":0.56000000000000005,"planeBounds":{"left":0.054312500000000014,"bottom":-0.03515625,"right":0.53868749999999999,"top":0.73828125},"atlasBounds":{"left":588.5,"bottom":681.5,"right":650.5,"top":780.5}},{"unicode":81,"advance":0.56000000000000005,"planeBounds":{"left":-0.00025000000000000179,"bottom":-0.21484375,"right":0.56225000000000003,"top":0.74609375},"atlasBounds":{"left":483.5,"bottom":900.5,"right":555.5,"top":1023.5}},{"unicode":82,"advance":0.56000000000000005,"planeBounds":{"left":0.028687500000000032,"bottom":-0.03515625,"right":0.54431249999999998,"top":0.73828125},"atlasBounds":{"left":651.5,"bottom":681.5,"right":717.5,"top":780.5}},{"unicode":83,"advance":0.56000000000000005,"planeBounds":{"left":0.033906249999999992,"bottom":-0.05078125,"right":0.52609375000000003,"top":0.74609375},"atlasBounds":{"left":722.5,"bottom":787.5,"right":785.5,"top":889.5}},{"unicode":84,"advance":0.56000000000000005,"planeBounds":{"left":0.014374999999999983,"bottom":-0.03515625,"right":0.54562500000000003,"top":0.73046875},"atlasBounds":{"left":854.5,"bottom":682.5,"right":922.5,"top":780.5}},{"unicode":85,"advance":0.56000000000000005,"planeBounds":{"left":0.022187500000000034,"bottom":-0.05078125,"right":0.53781250000000003,"top":0.73046875},"atlasBounds":{"left":452.5,"bottom":680.5,"right":518.5,"top":780.5}},{"unicode":86,"advance":0.56000000000000005,"planeBounds":{"left":-0.019781249999999986,"bottom":-0.03515625,"right":0.58178125000000003,"top":0.73046875},"atlasBounds":{"left":71.5,"bottom":581.5,"right":148.5,"top":679.5}},{"unicode":87,"advance":0.56000000000000005,"planeBounds":{"left":0.006562499999999992,"bottom":-0.03515625,"right":0.55343750000000003,"top":0.73046875},"atlasBounds":{"left":451.5,"bottom":581.5,"right":521.5,"top":679.5}},{"unicode":88,"advance":0.56000000000000005,"planeBounds":{"left":-0.0012500000000000026,"bottom":-0.03515625,"right":0.56125000000000003,"top":0.73046875},"atlasBounds":{"left":587.5,"bottom":581.5,"right":659.5,"top":679.5}},{"unicode":89,"advance":0.56000000000000005,"planeBounds":{"left":-0.019781249999999986,"bottom":-0.03515625,"right":0.58178125000000003,"top":0.73046875},"atlasBounds":{"left":946.5,"bottom":581.5,"right":1023.5,"top":679.5}},{"unicode":90,"advance":0.56000000000000005,"planeBounds":{"left":0.028687500000000032,"bottom":-0.03515625,"right":0.54431249999999998,"top":0.73046875},"atlasBounds":{"left":879.5,"bottom":581.5,"right":945.5,"top":679.5}},{"unicode":91,"advance":0.56000000000000005,"planeBounds":{"left":0.13734375000000001,"bottom":-0.22265625,"right":0.45765624999999999,"top":0.81640625},"atlasBounds":{"left":287.5,"bottom":890.5,"right":328.5,"top":1023.5}},{"unicode":92,"advance":0.56000000000000005,"planeBounds":{"left":0.052937500000000005,"bottom":-0.22265625,"right":0.50606249999999997,"top":0.81640625},"atlasBounds":{"left":329.5,"bottom":890.5,"right":387.5,"top":1023.5}},{"unicode":93,"advance":0.56000000000000005,"planeBounds":{"left":0.10234375,"bottom":-0.22265625,"right":0.42265625000000001,"top":0.81640625},"atlasBounds":{"left":388.5,"bottom":890.5,"right":429.5,"top":1023.5}},{"unicode":94,"advance":0.56000000000000005,"planeBounds":{"left":0.021187499999999981,"bottom":0.28515625,"right":0.53681250000000003,"top":0.73046875},"atlasBounds":{"left":268.5,"bottom":424.5,"right":334.5,"top":481.5}},{"unicode":95,"advance":0.56000000000000005,"planeBounds":{"left":-0.024687499999999984,"bottom":-0.22265625,"right":0.58468750000000003,"top":-0.08203125},"atlasBounds":{"left":521.5,"bottom":463.5,"right":599.5,"top":481.5}},{"unicode":96,"advance":0.56000000000000005,"planeBounds":{"left":0.15431249999999999,"bottom":0.54296875,"right":0.38868750000000002,"top":0.80859375},"atlasBounds":{"left":983.5,"bottom":683.5,"right":1013.5,"top":717.5}},{"unicode":97,"advance":0.56000000000000005,"planeBounds":{"left":0.037218750000000016,"bottom":-0.05078125,"right":0.51378124999999997,"top":0.56640625},"atlasBounds":{"left":169.5,"bottom":501.5,"right":230.5,"top":580.5}},{"unicode":98,"advance":0.56000000000000005,"planeBounds":{"left":0.051406250000000001,"bottom":-0.05078125,"right":0.54359374999999999,"top":0.80859375},"atlasBounds":{"left":690.5,"bottom":913.5,"right":753.5,"top":1023.5}},{"unicode":99,"advance":0.56000000000000005,"planeBounds":{"left":0.024593750000000029,"bottom":-0.05078125,"right":0.53240624999999997,"top":0.56640625},"atlasBounds":{"left":103.5,"bottom":501.5,"right":168.5,"top":580.5}},{"unicode":100,"advance":0.56000000000000005,"planeBounds":{"left":0.013999999999999997,"bottom":-0.05078125,"right":0.51400000000000001,"top":0.80859375},"atlasBounds":{"left":754.5,"bottom":913.5,"right":818.5,"top":1023.5}},{"unicode":101,"advance":0.56000000000000005,"planeBounds":{"left":0.015781249999999983,"bottom":-0.05078125,"right":0.53921874999999997,"top":0.56640625},"atlasBounds":{"left":292.5,"bottom":501.5,"right":359.5,"top":580.5}},{"unicode":102,"advance":0.56000000000000005,"planeBounds":{"left":0.046781249999999983,"bottom":-0.03515625,"right":0.57021875,"top":0.80859375},"atlasBounds":{"left":884.5,"bottom":915.5,"right":951.5,"top":1023.5}},{"unicode":103,"advance":0.56000000000000005,"planeBounds":{"left":0.011999999999999997,"bottom":-0.22265625,"right":0.51200000000000001,"top":0.56640625},"atlasBounds":{"left":786.5,"bottom":788.5,"right":850.5,"top":889.5}},{"unicode":104,"advance":0.56000000000000005,"planeBounds":{"left":0.051531250000000015,"bottom":-0.03515625,"right":0.51246875000000003,"top":0.80859375},"atlasBounds":{"left":0.5,"bottom":781.5,"right":59.5,"top":889.5}},{"unicode":105,"advance":0.56000000000000005,"planeBounds":{"left":0.031999999999999994,"bottom":-0.05078125,"right":0.53200000000000003,"top":0.79296875},"atlasBounds":{"left":60.5,"bottom":781.5,"right":124.5,"top":889.5}},{"unicode":106,"advance":0.56000000000000005,"planeBounds":{"left":0.053375000000000013,"bottom":-0.22265625,"right":0.45962500000000001,"top":0.79296875},"atlasBounds":{"left":430.5,"bottom":893.5,"right":482.5,"top":1023.5}},{"unicode":107,"advance":0.56000000000000005,"planeBounds":{"left":0.049687500000000037,"bottom":-0.03515625,"right":0.5653125,"top":0.80859375},"atlasBounds":{"left":125.5,"bottom":781.5,"right":191.5,"top":889.5}},{"unicode":108,"advance":0.56000000000000005,"planeBounds":{"left":0.031999999999999994,"bottom":-0.05078125,"right":0.53200000000000003,"top":0.80859375},"atlasBounds":{"left":819.5,"bottom":913.5,"right":883.5,"top":1023.5}},{"unicode":109,"advance":0.56000000000000005,"planeBounds":{"left":0.018374999999999985,"bottom":-0.03515625,"right":0.54962500000000003,"top":0.56640625},"atlasBounds":{"left":518.5,"bottom":503.5,"right":586.5,"top":580.5}},{"unicode":110,"advance":0.56000000000000005,"planeBounds":{"left":0.051531250000000015,"bottom":-0.03515625,"right":0.51246875000000003,"top":0.56640625},"atlasBounds":{"left":429.5,"bottom":503.5,"right":488.5,"top":580.5}},{"unicode":111,"advance":0.56000000000000005,"planeBounds":{"left":0.014374999999999983,"bottom":-0.04296875,"right":0.54562500000000003,"top":0.56640625},"atlasBounds":{"left":360.5,"bottom":502.5,"right":428.5,"top":580.5}},{"unicode":112,"advance":0.56000000000000005,"planeBounds":{"left":0.051406250000000001,"bottom":-0.22265625,"right":0.54359374999999999,"top":0.56640625},"atlasBounds":{"left":851.5,"bottom":788.5,"right":914.5,"top":889.5}},{"unicode":113,"advance":0.56000000000000005,"planeBounds":{"left":0.016406249999999997,"bottom":-0.22265625,"right":0.50859374999999996,"top":0.56640625},"atlasBounds":{"left":915.5,"bottom":788.5,"right":978.5,"top":889.5}},{"unicode":114,"advance":0.56000000000000005,"planeBounds":{"left":0.094062500000000007,"bottom":-0.03515625,"right":0.51593750000000005,"top":0.56640625},"atlasBounds":{"left":587.5,"bottom":503.5,"right":641.5,"top":580.5}},{"unicode":115,"advance":0.56000000000000005,"planeBounds":{"left":0.046125000000000013,"bottom":-0.05078125,"right":0.51487499999999997,"top":0.56640625},"atlasBounds":{"left":231.5,"bottom":501.5,"right":291.5,"top":580.5}},{"unicode":116,"advance":0.56000000000000005,"planeBounds":{"left":0.04631250000000002,"bottom":-0.05078125,"right":0.53068749999999998,"top":0.71484375},"atlasBounds":{"left":791.5,"bottom":682.5,"right":853.5,"top":780.5}},{"unicode":117,"advance":0.56000000000000005,"planeBounds":{"left":0.047531250000000018,"bottom":-0.04296875,"right":0.50846875000000002,"top":0.55078125},"atlasBounds":{"left":642.5,"bottom":504.5,"right":701.5,"top":580.5}},{"unicode":118,"advance":0.56000000000000005,"planeBounds":{"left":0.006562499999999992,"bottom":-0.03515625,"right":0.55343750000000003,"top":0.55078125},"atlasBounds":{"left":702.5,"bottom":505.5,"right":772.5,"top":580.5}},{"unicode":119,"advance":0.56000000000000005,"planeBounds":{"left":-0.0090624999999999942,"bottom":-0.03515625,"right":0.56906250000000003,"top":0.55078125},"atlasBounds":{"left":845.5,"bottom":505.5,"right":919.5,"top":580.5}},{"unicode":120,"advance":0.56000000000000005,"planeBounds":{"left":0.0031562499999999963,"bottom":-0.03515625,"right":0.55784374999999997,"top":0.55078125},"atlasBounds":{"left":773.5,"bottom":505.5,"right":844.5,"top":580.5}},{"unicode":121,"advance":0.56000000000000005,"planeBounds":{"left":0.012874999999999985,"bottom":-0.22265625,"right":0.54412499999999997,"top":0.55078125},"atlasBounds":{"left":519.5,"bottom":681.5,"right":587.5,"top":780.5}},{"unicode":122,"advance":0.56000000000000005,"planeBounds":{"left":0.049031250000000005,"bottom":-0.03515625,"right":0.50996874999999997,"top":0.55078125},"atlasBounds":{"left":920.5,"bottom":505.5,"right":979.5,"top":580.5}},{"unicode":123,"advance":0.56000000000000005,"planeBounds":{"left":0.061343750000000002,"bottom":-0.22265625,"right":0.50665625000000003,"top":0.81640625},"atlasBounds":{"left":169.5,"bottom":890.5,"right":226.5,"top":1023.5}},{"unicode":124,"advance":0.56000000000000005,"planeBounds":{"left":0.2101875,"bottom":-0.22265625,"right":0.35081250000000003,"top":0.81640625},"atlasBounds":{"left":150.5,"bottom":890.5,"right":168.5,"top":1023.5}},{"unicode":125,"advance":0.56000000000000005,"planeBounds":{"left":0.053343750000000002,"bottom":-0.22265625,"right":0.49865625000000002,"top":0.81640625},"atlasBounds":{"left":92.5,"bottom":890.5,"right":149.5,"top":1023.5}},{"unicode":126,"advance":0.56000000000000005,"planeBounds":{"left":0.012874999999999985,"bottom":0.18359375,"right":0.54412499999999997,"top":0.40234375},"atlasBounds":{"left":452.5,"bottom":453.5,"right":520.5,"top":481.5}}],"kerning":[]} diff --git a/resources/public/fonts/ubuntu_sans_mono_atlas.png b/resources/public/fonts/ubuntu_sans_mono_atlas.png new file mode 100644 index 0000000..6636857 Binary files /dev/null and b/resources/public/fonts/ubuntu_sans_mono_atlas.png differ diff --git a/src-build/build.clj b/src-build/build.clj index fa04b5d..245b6ba 100644 --- a/src-build/build.clj +++ b/src-build/build.clj @@ -1,5 +1,6 @@ (ns build (:require + [build.slug-font :as slug-font] [clojure.tools.build.api :as b] [clojure.tools.logging :as log] [shadow.cljs.devtools.api :as shadow-api] @@ -60,6 +61,14 @@ :basis (b/create-basis {:project "deps.edn" :aliases aliases})}) (log/info jar-name))) +(defn build-slug-font + "Generate Slug assets for the default DejaVu Sans Mono font bundle. + Invoke with `clj -X:build build-slug-font`." + [_argmap] + (let [result (slug-font/write-font-assets! (slug-font/default-config))] + (log/info "Slug font assets generated:" (pr-str result)) + result)) + ;; clj -X:build:prod build-client ;; clj -X:build:prod uberjar :build/jar-name "app.jar" -;; java -cp app.jar clojure.main -m prod \ No newline at end of file +;; java -cp app.jar clojure.main -m prod diff --git a/src-dev/dev.cljc b/src-dev/dev.cljc index d501abf..041af07 100644 --- a/src-dev/dev.cljc +++ b/src-dev/dev.cljc @@ -19,17 +19,46 @@ :manifest-path ; contains Electric compiled program's version so client and server stays in sync "public/js/manifest.edn"}) + (defn- wrap-request-logging [handler] + (fn [ring-request] + (let [started-ns (System/nanoTime) + request-summary {:method (some-> (:request-method ring-request) name) + :uri (:uri ring-request) + :query (:query-string ring-request) + :websocket? (boolean (:websocket? ring-request))}] + (log/info "[DEV/REQ]" request-summary) + (try + (let [response (handler ring-request) + elapsed-ms (/ (double (- (System/nanoTime) started-ns)) 1000000.0)] + (log/info "[DEV/RESP]" + (assoc request-summary + :status (:status response) + :elapsed-ms (format "%.2f" elapsed-ms))) + response) + (catch Throwable t + (log/error t "[DEV/ERR]" request-summary) + (throw t)))))) + (defn -main [& args] - (log/info "Starting Electric compiler and server...") + (log/info "[DEV] Starting Electric compiler and server..." + {:args (vec args) + :config config}) (shadow-server/start!) + (log/info "[DEV] shadow-cljs server started") (shadow/watch :dev) + (log/info "[DEV] shadow-cljs watch started for build :dev") (comment (shadow-server/stop!)) (def server (jetty/start-server! - (fn [ring-request] - (e/boot-server {} app.electric-flow/main ring-request)) + (wrap-request-logging + (fn [ring-request] + (e/boot-server {} app.electric-flow/main ring-request))) config)) + (log/info "[DEV] Jetty server started" {:host (:host config) + :port (:port config) + :resources-path (:resources-path config) + :manifest-path (:manifest-path config)}) (comment (.stop server))))) @@ -45,4 +74,4 @@ (defn ^:dev/before-load stop! [] (when reactor (reactor)) ; stop the reactor - (set! reactor nil)))) \ No newline at end of file + (set! reactor nil)))) diff --git a/src/app/client/substrate/webgpu/buffer_pool.cljs b/src/app/client/substrate/webgpu/buffer_pool.cljs new file mode 100644 index 0000000..cc20d24 --- /dev/null +++ b/src/app/client/substrate/webgpu/buffer_pool.cljs @@ -0,0 +1,462 @@ +(ns app.client.substrate.webgpu.buffer-pool + "Slot-based GPU buffer pool for differential rendering. + Default: 28 floats (112 bytes) per rect. Configurable for other item types + (e.g. shadows: 20 floats, 80 bytes) via :floats-per-item and :pack-fn. + Supports per-slot updates via writeBuffer for O(1) partial writes, + and batch-update with diff for O(changed) bulk sync." + (:require [clojure.set] + [app.client.substrate.webgpu.gpu-budget :as gpu-budget])) + +(def floats-per-rect 28) +(def bytes-per-rect 112) ;; 28 × 4 + +(defn- make-buffer [^js device capacity bytes-per-item] + (.createBuffer device + (clj->js {:size (* capacity bytes-per-item) + :usage (bit-or js/GPUBufferUsage.VERTEX + js/GPUBufferUsage.COPY_DST + js/GPUBufferUsage.COPY_SRC)}))) + +(defn- pack-rect + "Pack a rect map into a Float32Array(28). Same layout as renderer/update-rects." + [rect-map] + (let [data (js/Float32Array. floats-per-rect) + {:keys [x y w h r g b a]} rect-map + cr (:corner-radii rect-map) + uniform-r (or (:radius rect-map) 0.0) + bw (:border-widths rect-map) + uniform-bw (or (:border-width rect-map) 0.0) + bc (or (:border-color rect-map) [0 0 0 0]) + gr (or (:gradient rect-map) [0 0 0 0]) + gc2 (or (:gradient-color2 rect-map) [0 0 0 0])] + ;; Slot 0: rect_geometry [x y w h] + (aset data 0 (or x 0)) (aset data 1 (or y 0)) + (aset data 2 (or w 0)) (aset data 3 (or h 0)) + ;; Slot 1: color [r g b a] + (aset data 4 (or r 0)) (aset data 5 (or g 0)) + (aset data 6 (or b 0)) (aset data 7 (or a 0)) + ;; Slot 2: corner_radii [tl tr br bl] + (if cr + (do (aset data 8 (nth cr 0)) (aset data 9 (nth cr 1)) + (aset data 10 (nth cr 2)) (aset data 11 (nth cr 3))) + (do (aset data 8 uniform-r) (aset data 9 uniform-r) + (aset data 10 uniform-r) (aset data 11 uniform-r))) + ;; Slot 3: border_widths [top right bottom left] + (if bw + (do (aset data 12 (nth bw 0)) (aset data 13 (nth bw 1)) + (aset data 14 (nth bw 2)) (aset data 15 (nth bw 3))) + (do (aset data 12 uniform-bw) (aset data 13 uniform-bw) + (aset data 14 uniform-bw) (aset data 15 uniform-bw))) + ;; Slot 4: border_color [r g b a] + (aset data 16 (nth bc 0)) (aset data 17 (nth bc 1)) + (aset data 18 (nth bc 2)) (aset data 19 (nth bc 3)) + ;; Slot 5: gradient [angle t_stop 0 0] + (aset data 20 (nth gr 0)) (aset data 21 (nth gr 1)) + (aset data 22 (nth gr 2)) (aset data 23 (nth gr 3)) + ;; Slot 6: gradient_color2 [r g b a] + (aset data 24 (nth gc2 0)) (aset data 25 (nth gc2 1)) + (aset data 26 (nth gc2 2)) (aset data 27 (nth gc2 3)) + data)) + +(defn pack-shadow + "Pack a shadow map into a Float32Array(20). Same GPU layout as renderer/update-shadows. + 20 floats = 80 bytes: expanded_rect, shadow_color, corner_radii, blur_params, inner_rect" + [shadow-map] + (let [data (js/Float32Array. 20) + {:keys [x y w h blur offset-x offset-y spread color radius corner-radii]} shadow-map + blur (or blur 8.0) + ox (or offset-x 0.0) + oy (or offset-y 0.0) + spread (or spread 0.0) + sc (or color [0 0 0 0.25]) + expand (* 3.0 blur) + ;; Expanded quad (captures Gaussian tail) + ex (- x expand (max ox 0)) + ey (- y expand (max oy 0)) + ew (+ w (* 2 expand) (Math/abs ox)) + eh (+ h (* 2 expand) (Math/abs oy)) + ;; Inner rect relative to expanded quad origin + ix (- x ex) + iy (- y ey) + cr corner-radii + ur (or radius 0.0)] + ;; Slot 0: expanded_rect + (aset data 0 ex) (aset data 1 ey) + (aset data 2 ew) (aset data 3 eh) + ;; Slot 1: shadow_color + (aset data 4 (nth sc 0)) (aset data 5 (nth sc 1)) + (aset data 6 (nth sc 2)) (aset data 7 (nth sc 3)) + ;; Slot 2: corner_radii + (if cr + (do (aset data 8 (nth cr 0)) (aset data 9 (nth cr 1)) + (aset data 10 (nth cr 2)) (aset data 11 (nth cr 3))) + (do (aset data 8 ur) (aset data 9 ur) + (aset data 10 ur) (aset data 11 ur))) + ;; Slot 3: blur_params [blur, offset_x, offset_y, spread] + (aset data 12 blur) (aset data 13 ox) + (aset data 14 oy) (aset data 15 spread) + ;; Slot 4: inner_rect (relative to expanded quad) + (aset data 16 ix) (aset data 17 iy) + (aset data 18 w) (aset data 19 h) + data)) + +(defn create-pool + "Create a slot-based GPU buffer pool. Returns an atom. + pipeline/bind-group are shared with the rendering system (same shader, same camera). + Optional :floats-per-item (default 28) and :pack-fn (default pack-rect) + allow the pool to manage different item types (rects, shadows, etc.)." + [device initial-capacity pipeline bind-group + & {:keys [floats-per-item pack-fn tracker label] + :or {floats-per-item floats-per-rect pack-fn pack-rect}}] + (let [bytes-per-item (* floats-per-item 4)] + (atom (let [buffer (make-buffer device initial-capacity bytes-per-item)] + (gpu-budget/register-buffer! tracker buffer (or label "pool/unnamed") + (* initial-capacity bytes-per-item) + :active-bytes 0) + {:device device + :buffer buffer + :capacity initial-capacity + :free-list () + :active-slots #{} + :high-water-mark 0 + :pipeline pipeline + :bind-group bind-group + :floats-per-item floats-per-item + :bytes-per-item bytes-per-item + :pack-fn pack-fn + :gpu-tracker tracker + :gpu-label (or label "pool/unnamed") + :generations (vec (repeat initial-capacity 0)) + :prev-rects nil})))) + +(defn- grow-pool! + "Double the pool's buffer capacity, copying existing data via command encoder." + [pool] + (let [{:keys [^js device ^js buffer capacity high-water-mark bytes-per-item gpu-tracker gpu-label]} @pool + new-capacity (* capacity 2) + new-buffer (make-buffer device new-capacity bytes-per-item) + copy-bytes (* high-water-mark bytes-per-item)] + (when (pos? copy-bytes) + (let [encoder (.createCommandEncoder device)] + (.copyBufferToBuffer ^js encoder buffer 0 new-buffer 0 copy-bytes) + (.submit (.-queue device) #js [(.finish ^js encoder)]))) + (gpu-budget/replace-buffer! gpu-tracker buffer new-buffer gpu-label + (* new-capacity bytes-per-item) + :active-bytes copy-bytes + :reason :pool-grow) + (.destroy buffer) + (swap! pool #(-> % + (assoc :buffer new-buffer :capacity new-capacity) + (update :generations into (repeat capacity 0)))))) + +(defn- ensure-capacity! [pool needed] + (while (> needed (:capacity @pool)) + (grow-pool! pool))) + +;; ============================================================================ +;; RAW PER-SLOT API (internal — used by diff engines. External callers use handles.) +;; ============================================================================ + +(defn allocate-slot! + "Allocate a slot from the pool. Returns slot index. + Takes from free-list, or advances high-water-mark (growing buffer if needed)." + [pool] + (let [{:keys [free-list high-water-mark]} @pool] + (if (seq free-list) + (let [slot (first free-list)] + (swap! pool #(-> % (update :free-list rest) (update :active-slots conj slot))) + slot) + (do + (ensure-capacity! pool (inc high-water-mark)) + (let [slot high-water-mark] + (swap! pool #(-> % (update :high-water-mark inc) (update :active-slots conj slot))) + slot))))) + +(defn free-slot! + "Free a slot, zeroing its GPU data to prevent ghost rendering." + [pool slot-index] + (let [{:keys [^js device ^js buffer floats-per-item bytes-per-item]} @pool + zeros (js/Float32Array. floats-per-item)] + (.writeBuffer (.-queue device) buffer (* slot-index bytes-per-item) zeros)) + (swap! pool #(-> % (update :free-list conj slot-index) + (update :active-slots disj slot-index) + (update-in [:generations slot-index] inc))) + nil) + +(defn update-slot! + "Write a single item to a specific slot. O(1) GPU write." + [pool slot-index item-map] + (let [{:keys [^js device ^js buffer bytes-per-item pack-fn]} @pool + data (pack-fn item-map)] + (.writeBuffer (.-queue device) buffer (* slot-index bytes-per-item) data)) + nil) + +;; ============================================================================ +;; KEYED DIFF API (Phase 5: differential rendering by identity) +;; ============================================================================ + +(defn keyed-diff-update-pool! + "Sync pool contents with a keyed rect list. Each rect must have an :id field. + Allocates new slots for new IDs, updates changed rects, frees removed IDs. + Returns {:added N :updated N :freed N :total-writes N} for diagnostics. + + This is the Missionary-side equivalent of what e/for-by would do: + - new ID → allocate-slot! + update-slot! + - same ID, changed rect → update-slot! + - removed ID → free-slot!" + [pool new-rects] + (let [new-rects (or new-rects []) + {:keys [id->slot prev-keyed-rects]} @pool + id->slot (or id->slot {}) + prev-keyed (or prev-keyed-rects {}) + new-keyed (into {} (map (fn [r] [(:id r) r])) new-rects) + new-ids (set (keys new-keyed)) + old-ids (set (keys prev-keyed)) + added-ids (clojure.set/difference new-ids old-ids) + removed-ids (clojure.set/difference old-ids new-ids) + kept-ids (clojure.set/intersection new-ids old-ids) + added (volatile! 0) + updated (volatile! 0) + freed (volatile! 0) + ;; Free removed slots + new-id->slot (reduce (fn [m id] + (when-let [slot (get m id)] + (free-slot! pool slot)) + (vswap! freed inc) + (dissoc m id)) + id->slot removed-ids) + ;; Allocate + write new slots + new-id->slot (reduce (fn [m id] + (let [slot (allocate-slot! pool) + rect (get new-keyed id)] + (update-slot! pool slot rect) + (vswap! added inc) + (assoc m id slot))) + new-id->slot added-ids) + ;; Update changed kept slots + new-id->slot (reduce (fn [m id] + (let [old-rect (get prev-keyed id) + new-rect (get new-keyed id)] + (when-not (= old-rect new-rect) + (when-let [slot (get m id)] + (update-slot! pool slot new-rect)) + (vswap! updated inc)) + m)) + new-id->slot kept-ids)] + (swap! pool assoc + :id->slot new-id->slot + :prev-keyed-rects new-keyed + :high-water-mark (max (:high-water-mark @pool) + (count (:active-slots @pool)))) + (gpu-budget/set-active-bytes! (:gpu-tracker @pool) (:buffer @pool) (* (count new-rects) (:bytes-per-item @pool))) + {:added @added :updated @updated :freed @freed + :total-writes (+ @added @updated @freed)})) + +;; ============================================================================ +;; ORDERED KEYED API (Phase 6A: identity-based diff with z-order preservation) +;; ============================================================================ + +(defn ordered-diff-update-pool! + "Sync pool with a rect list, maintaining input order in the buffer. + Slot i always holds the i-th input rect, preserving draw order (z-correctness). + Identity tracking via :id skips unchanged rects at unchanged positions. + Returns {:added :updated :freed :total-writes}." + [pool new-rects] + (let [new-rects (or new-rects []) + new-n (count new-rects) + {:keys [^js device ordered-ids prev-keyed-rects bytes-per-item pack-fn floats-per-item]} @pool + prev-ids (or ordered-ids []) + prev-n (count prev-ids) + prev-keyed (or prev-keyed-rects {}) + new-keyed (into {} (map (fn [r] [(:id r) r])) new-rects) + new-ids (mapv :id new-rects) + added (volatile! 0) + updated (volatile! 0) + freed (volatile! 0)] + (ensure-capacity! pool new-n) + (let [^js buf (:buffer @pool)] + ;; Write rects where identity shifted or content changed + (dotimes [i new-n] + (let [rect (nth new-rects i) + id (:id rect) + prev-id-at-pos (when (< i prev-n) (nth prev-ids i)) + prev-rect (get prev-keyed id)] + (when (or (nil? prev-rect) ;; new ID + (not= id prev-id-at-pos) ;; different ID at this slot + (not= rect prev-rect)) ;; same ID, content changed + (let [data (pack-fn rect)] + (.writeBuffer (.-queue device) buf (* i bytes-per-item) data) + (if (nil? prev-rect) + (vswap! added inc) + (vswap! updated inc)))))) + ;; Zero freed tail + (let [old-hwm (:high-water-mark @pool)] + (when (> old-hwm new-n) + (let [zero-count (- old-hwm new-n) + zeros (js/Float32Array. (* zero-count floats-per-item))] + (.writeBuffer (.-queue device) buf (* new-n bytes-per-item) zeros) + (vswap! freed + zero-count))))) + (swap! pool assoc + :ordered-ids new-ids + :prev-keyed-rects new-keyed + :high-water-mark new-n) + (gpu-budget/set-active-bytes! (:gpu-tracker @pool) (:buffer @pool) (* new-n bytes-per-item)) + {:added @added :updated @updated :freed @freed + :total-writes (+ @added @updated @freed)})) + +;; ============================================================================ +;; BATCH API (for Missionary-based diff: compare new vs previous rect list) +;; ============================================================================ + +(defn batch-update-pool! + "Sync pool contents with a new rect list. Compares each rect against the + previous list and only issues writeBuffer for changed rects. + Returns the number of GPU writes performed (for diagnostics)." + [pool new-rects] + (let [new-rects (or new-rects []) + new-n (count new-rects) + {:keys [^js device prev-rects bytes-per-item pack-fn floats-per-item]} @pool + prev-n (count (or prev-rects [])) + writes (volatile! 0)] + ;; Grow buffer if needed + (ensure-capacity! pool new-n) + ;; Re-read buffer after potential grow (grow replaces :buffer) + (let [^js buf (:buffer @pool)] + ;; Write changed items + (dotimes [i new-n] + (let [rect (nth new-rects i) + prev-rect (when (< i prev-n) (nth prev-rects i))] + (when-not (= rect prev-rect) + (let [data (pack-fn rect)] + (.writeBuffer (.-queue device) buf (* i bytes-per-item) data) + (vswap! writes inc))))) + ;; Zero freed slots (list shrank) + (when (> prev-n new-n) + (let [zero-count (- prev-n new-n) + zeros (js/Float32Array. (* zero-count floats-per-item))] + (.writeBuffer (.-queue device) buf (* new-n bytes-per-item) zeros) + (vswap! writes + zero-count)))) + ;; Update pool state + (swap! pool assoc + :prev-rects new-rects + :high-water-mark new-n) + (gpu-budget/set-active-bytes! (:gpu-tracker @pool) (:buffer @pool) (* new-n bytes-per-item)) + @writes)) + +(defn pool-draw-info + "Get draw parameters for the render pass. + Returns {:buffer :draw-count :pipeline :bind-group}." + [pool] + (let [{:keys [buffer high-water-mark pipeline bind-group]} @pool] + {:buffer buffer + :draw-count high-water-mark + :pipeline pipeline + :bind-group bind-group})) + +;; ============================================================================ +;; HANDLE-CHECKED API (Phase 6D: safe per-slot access for external callers) +;; Raw slot indices stay internal. External code holds handles {:slot :gen}. +;; ============================================================================ + +(defn allocate-handle! + "Allocate a slot and return a handle {:slot idx :gen g}. + If item-map is provided, writes it to the slot immediately." + ([pool] (allocate-handle! pool nil)) + ([pool item-map] + (let [slot (allocate-slot! pool) + gen (nth (:generations @pool) slot)] + (when item-map + (update-slot! pool slot item-map)) + {:slot slot :gen gen}))) + +(defn- validate-handle + "Check handle against pool state. Returns slot index if valid, nil if stale. + Degrades to warning on malformed input — never throws." + [pool handle] + (if-not (and (map? handle) (integer? (:slot handle)) (integer? (:gen handle))) + (do (js/console.warn "[POOL] Malformed handle:" (pr-str handle)) nil) + (let [{:keys [slot gen]} handle + {:keys [generations active-slots]} @pool] + (cond + (or (neg? slot) (>= slot (count generations))) + (do (js/console.warn "[POOL] Handle out of range: slot" slot "capacity" (count generations)) + nil) + + (not= gen (nth generations slot)) + (do (js/console.warn "[POOL] Stale handle: slot" slot "expected gen" (nth generations slot) "got" gen) + nil) + + (not (contains? active-slots slot)) + (do (js/console.warn "[POOL] Handle references inactive slot:" slot) + nil) + + :else slot)))) + +(defn update-handle! + "Write item data to a handle's slot. Returns handle if valid, nil if stale." + [pool handle item-map] + (when-let [slot (validate-handle pool handle)] + (update-slot! pool slot item-map) + handle)) + +(defn free-handle! + "Free a handle's slot. Returns true if successful, nil if stale." + [pool handle] + (when-let [slot (validate-handle pool handle)] + (free-slot! pool slot) + true)) + +;; ============================================================================ +;; MOUNT CALLBACKS (Phase 6D: bridge between Electric e/for-by and GPU pool) +;; ============================================================================ + +(defn- vec-index-of + "Find index of x in vector v by identity. Returns index or nil. + Uses identical? — handles must be the same object, not structural copies." + [v x] + (first (keep-indexed (fn [i h] (when (identical? h x) i)) v))) + +(defn gpu-mount + "Create Electric-compatible mount callbacks for a buffer pool. + Returns 5 callbacks matching Electric's incseq/mount contract. + Maintains internal ordering state for position-based child lookup. + Callbacks handle allocation/deallocation — callers pass item data in, + get handles out. nth-child returns the handle at position i. + + Usage with Electric: + (let [{:keys [append-child replace-child insert-before + remove-child nth-child]} (gpu-mount pool)] + (incseq/mount append-child replace-child insert-before + remove-child nth-child))" + [pool] + (let [!children (atom [])] + {:append-child + (fn [_element item] + (let [handle (allocate-handle! pool item)] + (swap! !children conj handle) + handle)) + + :replace-child + (fn [_element new-item old-handle] + (update-handle! pool old-handle new-item) + old-handle) + + :insert-before + (fn [_element item sibling] + (let [handle (allocate-handle! pool item)] + (swap! !children + (fn [v] + (let [idx (vec-index-of v sibling)] + (if (nil? idx) + (conj v handle) + (into (conj (subvec v 0 idx) handle) (subvec v idx)))))) + handle)) + + :remove-child + (fn [_element handle] + (free-handle! pool handle) + (swap! !children (fn [v] (filterv #(not (identical? % handle)) v)))) + + :nth-child + (fn [_element i] + (nth @!children i nil))})) diff --git a/src/app/client/substrate/webgpu/gpu_budget.cljs b/src/app/client/substrate/webgpu/gpu_budget.cljs new file mode 100644 index 0000000..5eeb837 --- /dev/null +++ b/src/app/client/substrate/webgpu/gpu_budget.cljs @@ -0,0 +1,334 @@ +(ns app.client.substrate.webgpu.gpu-budget + "Console-first GPU resource tracker for WebGPU buffers/textures. + Tracks reserved bytes by subsystem, plus optional active bytes where the + runtime knows how much of a reservation is currently populated." + (:require [clojure.string :as str])) + +(def ^:private max-events 200) +(def ^:private warn-threshold 0.8) + +(defn- now-ms [] + (.now js/Date)) + +(defn- clamp-bytes [n] + (max 0 (long (or n 0)))) + +(defn format-bytes [n] + (let [n (double (clamp-bytes n)) + kb 1024.0 + mb (* kb 1024.0) + gb (* mb 1024.0)] + (cond + (>= n gb) (str (.toFixed (/ n gb) 2) " GB") + (>= n mb) (str (.toFixed (/ n mb) 2) " MB") + (>= n kb) (str (.toFixed (/ n kb) 2) " KB") + :else (str (long n) " B")))) + +(defn- pct-str [ratio] + (when (number? ratio) + (str (.toFixed (* 100.0 ratio) 1) "%"))) + +(defn snapshot-adapter-limits [^js adapter] + (when adapter + (let [limits (.-limits adapter)] + {:max-buffer-size (some-> limits .-maxBufferSize) + :max-storage-buffer-binding-size (some-> limits .-maxStorageBufferBindingSize) + :max-texture-dimension-2d (some-> limits .-maxTextureDimension2D) + :max-texture-array-layers (some-> limits .-maxTextureArrayLayers)}))) + +(defn create-tracker [adapter-limits] + {:ids (js/WeakMap.) + :state (atom {:limits (or adapter-limits {}) + :resources {} + :events [] + :next-id 1 + :startup-logged? false})}) + +(defn- next-id! [tracker] + (let [!id (volatile! nil)] + (swap! (:state tracker) + (fn [state] + (vreset! !id (:next-id state)) + (update state :next-id inc))) + @!id)) + +(defn- lookup-id [tracker obj] + (when (and tracker obj) + (.get ^js (:ids tracker) obj))) + +(defn- lookup-resource [tracker obj] + (when-let [id (lookup-id tracker obj)] + (get-in @(:state tracker) [:resources id]))) + +(defn- push-event! [tracker event] + (when tracker + (swap! (:state tracker) + (fn [state] + (let [events (conj (:events state) (assoc event :ts (now-ms))) + trimmed (if (> (count events) max-events) + (subvec (vec events) (- (count events) max-events)) + (vec events))] + (assoc state :events trimmed)))))) + +(defn- buffer-limit-ratio [limits reserved-bytes] + (when-let [max-buffer-size (:max-buffer-size limits)] + (when (pos? max-buffer-size) + (/ reserved-bytes max-buffer-size)))) + +(defn- texture-limit-ratio [limits {:keys [width height depth-or-array-layers]}] + (let [max-dim (:max-texture-dimension-2d limits) + max-layers (:max-texture-array-layers limits) + ratios (cond-> [] + (and max-dim (pos? max-dim)) + (into [(/ (or width 0) max-dim) + (/ (or height 0) max-dim)]) + + (and max-layers (pos? max-layers) depth-or-array-layers) + (conj (/ depth-or-array-layers max-layers)))] + (when (seq ratios) + (apply max ratios)))) + +(defn- limit-note [limits {:keys [kind reserved-bytes details]}] + (case kind + :buffer + (some-> (buffer-limit-ratio limits reserved-bytes) pct-str (str "maxBuffer " )) + + :texture + (some-> (texture-limit-ratio limits details) pct-str (str "maxTextureDim " )) + + nil)) + +(defn- maybe-warn! [tracker resource] + (let [limits (:limits @(:state tracker)) + ratio (case (:kind resource) + :buffer (buffer-limit-ratio limits (:reserved-bytes resource)) + :texture (texture-limit-ratio limits (:details resource)) + nil)] + (when (and ratio (>= ratio warn-threshold)) + (js/console.warn + (str "[GPU-BUDGET] " (:label resource) " is at " + (pct-str ratio) " of the relevant WebGPU limit."))))) + +(defn- log-replacement! [tracker old-resource new-resource reason] + (let [limits (:limits @(:state tracker)) + note (limit-note limits new-resource) + parts (cond-> [(str "[GPU-BUDGET] " (:label new-resource) " " + (name (:kind new-resource)) " " + (or (some-> reason name) "replace") + ": " (format-bytes (:reserved-bytes old-resource)) + " -> " (format-bytes (:reserved-bytes new-resource)))] + (number? (:active-bytes new-resource)) + (conj (str "active " (format-bytes (:active-bytes new-resource)))) + note + (conj note))] + (js/console.log (str/join " | " parts)) + (maybe-warn! tracker new-resource))) + +(defn- resource-map [kind label reserved-bytes active-bytes details replacement-of] + (let [reserved-bytes (clamp-bytes reserved-bytes) + active-bytes (when (some? active-bytes) (clamp-bytes active-bytes))] + {:kind kind + :label label + :reserved-bytes reserved-bytes + :active-bytes active-bytes + :details details + :replacement-of replacement-of + :created-at (now-ms) + :updated-at (now-ms)})) + +(defn- install-resource! [tracker obj resource] + (let [id (next-id! tracker)] + (.set ^js (:ids tracker) obj id) + (swap! (:state tracker) assoc-in [:resources id] (assoc resource :id id)) + (assoc resource :id id))) + +(defn register-buffer! + [tracker obj label reserved-bytes & {:keys [active-bytes details]}] + (when (and tracker obj) + (let [resource (resource-map :buffer label reserved-bytes active-bytes details nil)] + (push-event! tracker {:type :create :kind :buffer :label label :bytes (clamp-bytes reserved-bytes)}) + (install-resource! tracker obj resource)))) + +(defn register-texture! + [tracker obj label & {:keys [width height depth-or-array-layers format active-bytes details]}] + (when (and tracker obj) + (let [details (merge {:width width + :height height + :depth-or-array-layers (or depth-or-array-layers 1) + :format format} + details) + bytes-per-pixel (case format + ("rgba8unorm" "bgra8unorm" "rgba8unorm-srgb" "bgra8unorm-srgb") 4 + "rgba16float" 8 + "rg16uint" 4 + 4) + reserved-bytes (* (max 1 (or width 1)) + (max 1 (or height 1)) + (max 1 (or depth-or-array-layers 1)) + bytes-per-pixel) + resource (resource-map :texture label reserved-bytes + (or active-bytes reserved-bytes) details nil)] + (push-event! tracker {:type :create :kind :texture :label label :bytes reserved-bytes}) + (install-resource! tracker obj resource)))) + +(defn replace-buffer! + [tracker old-obj new-obj label reserved-bytes & {:keys [active-bytes details reason]}] + (when (and tracker new-obj) + (let [old-resource (lookup-resource tracker old-obj) + new-resource (resource-map :buffer label reserved-bytes active-bytes details (:id old-resource))] + (when old-obj + (.delete ^js (:ids tracker) old-obj)) + (when old-resource + (swap! (:state tracker) update :resources dissoc (:id old-resource))) + (let [installed (install-resource! tracker new-obj new-resource)] + (push-event! tracker {:type :replace + :kind :buffer + :label label + :old-bytes (:reserved-bytes old-resource) + :new-bytes (:reserved-bytes installed) + :reason reason}) + (when old-resource + (log-replacement! tracker old-resource installed reason)) + installed)))) + +(defn replace-texture! + [tracker old-obj new-obj label & {:keys [width height depth-or-array-layers format active-bytes details reason]}] + (when (and tracker new-obj) + (let [old-resource (lookup-resource tracker old-obj) + details (merge {:width width + :height height + :depth-or-array-layers (or depth-or-array-layers 1) + :format format} + details) + bytes-per-pixel (case format + ("rgba8unorm" "bgra8unorm" "rgba8unorm-srgb" "bgra8unorm-srgb") 4 + "rgba16float" 8 + "rg16uint" 4 + 4) + reserved-bytes (* (max 1 (or width 1)) + (max 1 (or height 1)) + (max 1 (or depth-or-array-layers 1)) + bytes-per-pixel) + new-resource (resource-map :texture label reserved-bytes + (or active-bytes reserved-bytes) + details + (:id old-resource))] + (when old-obj + (.delete ^js (:ids tracker) old-obj)) + (when old-resource + (swap! (:state tracker) update :resources dissoc (:id old-resource))) + (let [installed (install-resource! tracker new-obj new-resource)] + (push-event! tracker {:type :replace + :kind :texture + :label label + :old-bytes (:reserved-bytes old-resource) + :new-bytes (:reserved-bytes installed) + :reason reason}) + (when old-resource + (log-replacement! tracker old-resource installed reason)) + installed)))) + +(defn destroy-resource! + [tracker obj & {:keys [reason]}] + (when-let [id (lookup-id tracker obj)] + (when-let [resource (get-in @(:state tracker) [:resources id])] + (.delete ^js (:ids tracker) obj) + (swap! (:state tracker) update :resources dissoc id) + (push-event! tracker {:type :destroy + :kind (:kind resource) + :label (:label resource) + :bytes (:reserved-bytes resource) + :reason reason}) + resource))) + +(defn set-active-bytes! + [tracker obj active-bytes] + (when-let [id (lookup-id tracker obj)] + (swap! (:state tracker) + (fn [state] + (if-let [resource (get-in state [:resources id])] + (assoc-in state [:resources id] + (assoc resource + :active-bytes (clamp-bytes active-bytes) + :updated-at (now-ms))) + state))))) + +(defn snapshot [tracker] + (let [{:keys [limits resources events]} (or (some-> tracker :state deref) {}) + entries (vals (or resources {})) + by-label (->> entries + (reduce (fn [acc {:keys [label kind reserved-bytes active-bytes]}] + (update acc label + (fn [entry] + (let [entry (or entry {:label label + :resource-count 0 + :reserved-bytes 0 + :active-bytes 0 + :kinds #{}})] + (-> entry + (update :resource-count inc) + (update :reserved-bytes + reserved-bytes) + (update :active-bytes + (or active-bytes 0)) + (update :kinds conj kind)))))) + {}) + vals + (sort-by :reserved-bytes >) + vec) + total-reserved-bytes (reduce + 0 (map :reserved-bytes entries)) + total-active-bytes (reduce + 0 (map #(or (:active-bytes %) 0) entries)) + buffer-reserved-bytes (reduce + 0 (map :reserved-bytes (filter #(= :buffer (:kind %)) entries))) + texture-reserved-bytes (reduce + 0 (map :reserved-bytes (filter #(= :texture (:kind %)) entries))) + largest-buffer (first (sort-by :reserved-bytes > (filter #(= :buffer (:kind %)) entries))) + largest-texture (first (sort-by :reserved-bytes > (filter #(= :texture (:kind %)) entries)))] + {:limits (or limits {}) + :resources (vec entries) + :events (or events []) + :by-label by-label + :total-reserved-bytes total-reserved-bytes + :total-active-bytes total-active-bytes + :buffer-reserved-bytes buffer-reserved-bytes + :texture-reserved-bytes texture-reserved-bytes + :largest-buffer largest-buffer + :largest-texture largest-texture})) + +(defn summary-line [tracker] + (when tracker + (let [{:keys [limits total-reserved-bytes buffer-reserved-bytes + texture-reserved-bytes largest-buffer]} (snapshot tracker) + max-buffer-note (when largest-buffer + (some-> (buffer-limit-ratio limits (:reserved-bytes largest-buffer)) + pct-str + (str "largest " (:label largest-buffer) " "))) + parts (cond-> [(str "gpu: " (format-bytes total-reserved-bytes)) + (str "buf " (format-bytes buffer-reserved-bytes)) + (str "tex " (format-bytes texture-reserved-bytes))] + max-buffer-note + (conj max-buffer-note))] + (str/join " | " parts)))) + +(defn log-startup-report! [tracker] + (when tracker + (let [!should-log? (volatile! false)] + (swap! (:state tracker) + (fn [state] + (if (:startup-logged? state) + state + (do (vreset! !should-log? true) + (assoc state :startup-logged? true))))) + (when @!should-log? + (let [{:keys [limits by-label total-reserved-bytes total-active-bytes]} (snapshot tracker) + rows (mapv (fn [{:keys [label resource-count reserved-bytes active-bytes kinds]}] + {:subsystem label + :resources resource-count + :kinds (->> kinds (map name) sort (str/join ", ")) + :reserved (format-bytes reserved-bytes) + :active (format-bytes active-bytes)}) + by-label)] + (js/console.groupCollapsed + (str "[GPU-BUDGET] Startup report | reserved " + (format-bytes total-reserved-bytes) + " | active " (format-bytes total-active-bytes))) + (js/console.log "[GPU-BUDGET] Adapter limits:" (clj->js limits)) + (when (seq rows) + (js/console.table (clj->js rows))) + (js/console.groupEnd)))))) diff --git a/src/app/client/substrate/webgpu/renderer.cljs b/src/app/client/substrate/webgpu/renderer.cljs new file mode 100644 index 0000000..b7772af --- /dev/null +++ b/src/app/client/substrate/webgpu/renderer.cljs @@ -0,0 +1,1673 @@ +(ns app.client.substrate.webgpu.renderer + (:require [app.client.substrate.webgpu.gpu-budget :as gpu-budget])) + +;; --- 1. SHADERS --- +;; Rich quads: 28 floats/rect, SDF-based rounded corners, borders, gradients +(def rect-vertex-shader " + struct Camera { pan: vec2, zoom: f32, padding: f32, screen_dimensions: vec2, }; + @group(0) @binding(0) var camera: Camera; + struct InstanceInput { + @location(0) rect_geometry: vec4, + @location(1) color: vec4, + @location(2) corner_radii: vec4, + @location(3) border_widths: vec4, + @location(4) border_color: vec4, + @location(5) gradient: vec4, + @location(6) gradient_color2: vec4, + }; + struct VertexOutput { + @builtin(position) position: vec4, + @location(0) color: vec4, + @location(1) local_pos: vec2, + @location(2) rect_size: vec2, + @location(3) corner_radii: vec4, + @location(4) border_widths: vec4, + @location(5) border_color: vec4, + @location(6) gradient: vec4, + @location(7) gradient_color2: vec4, + }; + + @vertex + fn main(@builtin(vertex_index) v_index: u32, instance: InstanceInput) -> VertexOutput { + var output: VertexOutput; + var pos = vec2(0.0, 0.0); + switch(v_index) { + case 0u: { pos = vec2(0.0, 0.0); } case 1u: { pos = vec2(1.0, 0.0); } + case 2u: { pos = vec2(0.0, 1.0); } case 3u: { pos = vec2(1.0, 0.0); } + case 4u: { pos = vec2(1.0, 1.0); } default: { pos = vec2(0.0, 1.0); } + } + let world_pos = vec2(instance.rect_geometry.x + (pos.x * instance.rect_geometry.z), + instance.rect_geometry.y + (pos.y * instance.rect_geometry.w)); + let panned = (world_pos * camera.zoom) + camera.pan; + let ndc = (panned / camera.screen_dimensions * 2.0) - vec2(1.0, 1.0); + output.position = vec4(ndc.x, -ndc.y, 0.0, 1.0); + output.color = instance.color; + // Pass local UV (0..size in pixels) and rect size for SDF evaluation + output.local_pos = pos * instance.rect_geometry.zw * camera.zoom; + output.rect_size = instance.rect_geometry.zw * camera.zoom; + output.corner_radii = instance.corner_radii; + output.border_widths = instance.border_widths; + output.border_color = instance.border_color; + output.gradient = instance.gradient; + output.gradient_color2 = instance.gradient_color2; + return output; + }") + +(def rect-fragment-shader " + // Inigo Quilez SDF rounded box with per-corner radii + fn sd_rounded_box(p: vec2, half_size: vec2, radii: vec4) -> f32 { + // radii: tl, tr, br, bl → select based on quadrant + var r: vec2; + if (p.x > 0.0) { + r = vec2(radii.y, radii.z); // tr, br + } else { + r = vec2(radii.x, radii.w); // tl, bl + } + if (p.y > 0.0) { + r.x = r.y; // bottom row + } + let q = abs(p) - half_size + vec2(r.x, r.x); + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0, 0.0))) - r.x; + } + + @fragment + fn main( + @location(0) color: vec4, + @location(1) local_pos: vec2, + @location(2) rect_size: vec2, + @location(3) corner_radii: vec4, + @location(4) border_widths: vec4, + @location(5) border_color: vec4, + @location(6) gradient: vec4, + @location(7) gradient_color2: vec4, + ) -> @location(0) vec4 { + let half_size = rect_size * 0.5; + // p in centered coordinates: (0,0) = center of rect + let p = local_pos - half_size; + + // Clamp radii so they don't exceed half the smallest dimension + let max_r = min(half_size.x, half_size.y); + let radii = min(corner_radii, vec4(max_r, max_r, max_r, max_r)); + + let dist = sd_rounded_box(p, half_size, radii); + + // Anti-aliased edge (1px smoothstep) + let aa = clamp(0.5 - dist, 0.0, 1.0); + + // --- Fill color (with optional gradient) --- + var fill = color; + let t_stop = gradient.y; + if (t_stop > 0.0) { + // Linear gradient: angle in radians, t_stop = blend position + let angle = gradient.x; + let cs = cos(angle); + let sn = sin(angle); + // Project centered UV onto gradient axis + let uv_norm = local_pos / rect_size; + let t = clamp(uv_norm.x * cs + uv_norm.y * sn, 0.0, 1.0); + fill = mix(color, gradient_color2, smoothstep(0.0, t_stop, t)); + } + + // --- Border --- + let has_border = (border_widths.x + border_widths.y + border_widths.z + border_widths.w) > 0.0; + if (has_border) { + // Use max border width for SDF shrink (uniform-ish approach) + let bw = max(max(border_widths.x, border_widths.y), max(border_widths.z, border_widths.w)); + let inner_half = half_size - vec2(bw, bw); + let inner_radii = max(radii - vec4(bw, bw, bw, bw), vec4(0.0, 0.0, 0.0, 0.0)); + let inner_dist = sd_rounded_box(p, inner_half, inner_radii); + let inner_aa = clamp(0.5 - inner_dist, 0.0, 1.0); + // Composite: border color in the ring, fill inside + let result = mix(border_color, fill, inner_aa); + return vec4(result.rgb, result.a * aa); + } + + return vec4(fill.rgb, fill.a * aa); + }") + +;; --- Shadow shaders --- +;; 20 floats/shadow (80 bytes): expanded_rect, shadow_color, corner_radii, blur_params, inner_rect +(def shadow-vertex-shader " + struct Camera { pan: vec2, zoom: f32, padding: f32, screen_dimensions: vec2, }; + @group(0) @binding(0) var camera: Camera; + struct InstanceInput { + @location(0) expanded_rect: vec4, + @location(1) shadow_color: vec4, + @location(2) corner_radii: vec4, + @location(3) blur_params: vec4, + @location(4) inner_rect: vec4, + }; + struct VertexOutput { + @builtin(position) position: vec4, + @location(0) shadow_color: vec4, + @location(1) local_pos: vec2, + @location(2) rect_size: vec2, + @location(3) corner_radii: vec4, + @location(4) blur_params: vec4, + @location(5) inner_rect: vec4, + }; + + @vertex + fn main(@builtin(vertex_index) v_index: u32, instance: InstanceInput) -> VertexOutput { + var output: VertexOutput; + var pos = vec2(0.0, 0.0); + switch(v_index) { + case 0u: { pos = vec2(0.0, 0.0); } case 1u: { pos = vec2(1.0, 0.0); } + case 2u: { pos = vec2(0.0, 1.0); } case 3u: { pos = vec2(1.0, 0.0); } + case 4u: { pos = vec2(1.0, 1.0); } default: { pos = vec2(0.0, 1.0); } + } + let world_pos = vec2(instance.expanded_rect.x + (pos.x * instance.expanded_rect.z), + instance.expanded_rect.y + (pos.y * instance.expanded_rect.w)); + let panned = (world_pos * camera.zoom) + camera.pan; + let ndc = (panned / camera.screen_dimensions * 2.0) - vec2(1.0, 1.0); + output.position = vec4(ndc.x, -ndc.y, 0.0, 1.0); + output.shadow_color = instance.shadow_color; + output.local_pos = pos * instance.expanded_rect.zw * camera.zoom; + output.rect_size = instance.expanded_rect.zw * camera.zoom; + output.corner_radii = instance.corner_radii; + output.blur_params = instance.blur_params; + // Scale inner_rect to match zoom-scaled local_pos + output.inner_rect = vec4( + instance.inner_rect.xy * camera.zoom, + instance.inner_rect.zw * camera.zoom); + return output; + }") + +(def shadow-fragment-shader " + // Approximate erf for Gaussian CDF shadow falloff + fn erf_approx(x: f32) -> f32 { + let a = abs(x); + // Abramowitz & Stegun approximation (max error ~1.5e-7) + let t = 1.0 / (1.0 + 0.3275911 * a); + let poly = t * (0.254829592 + t * (-0.284496736 + t * (1.421413741 + t * (-1.453152027 + t * 1.061405429)))); + let result = 1.0 - poly * exp(-a * a); + return select(-result, result, x >= 0.0); + } + + // SDF rounded box (same as rect shader) + fn sd_rounded_box(p: vec2, half_size: vec2, radii: vec4) -> f32 { + var r: vec2; + if (p.x > 0.0) { + r = vec2(radii.y, radii.z); + } else { + r = vec2(radii.x, radii.w); + } + if (p.y > 0.0) { + r.x = r.y; + } + let q = abs(p) - half_size + vec2(r.x, r.x); + return min(max(q.x, q.y), 0.0) + length(max(q, vec2(0.0, 0.0))) - r.x; + } + + @fragment + fn main( + @location(0) shadow_color: vec4, + @location(1) local_pos: vec2, + @location(2) rect_size: vec2, + @location(3) corner_radii: vec4, + @location(4) blur_params: vec4, + @location(5) inner_rect: vec4, + ) -> @location(0) vec4 { + let blur = blur_params.x; + let offset = blur_params.yz; + let spread = blur_params.w; + + // Inner rect center and half-size (in zoom-scaled pixels, relative to expanded quad) + let inner_center = (inner_rect.xy + inner_rect.zw * 0.5) + offset; + let inner_half = inner_rect.zw * 0.5 + vec2(spread, spread); + + let max_r = min(inner_half.x, inner_half.y); + let radii = min(corner_radii, vec4(max_r, max_r, max_r, max_r)); + + // Pixel position relative to inner rect center + let p = local_pos - inner_center; + let dist = sd_rounded_box(p, inner_half, radii); + + // Gaussian CDF falloff + let sigma = max(blur * 0.5, 0.001); + let alpha = 0.5 - 0.5 * erf_approx(dist / (sigma * 1.4142135)); + + return vec4(shadow_color.rgb, shadow_color.a * alpha); + }") + +(def text-vertex-shader " + struct Camera { pan: vec2, zoom: f32, padding: f32, screen_dimensions: vec2, }; + @group(0) @binding(2) var camera: Camera; + // Per-instance: rect (vec4), uv_bounds (vec4), color (vec4) = 12 floats + struct InstanceInput { @location(0) rect: vec4, @location(1) uv_bounds: vec4, @location(2) color: vec4, }; + struct VertexOutput { @builtin(position) position: vec4, @location(0) uv: vec2, @location(1) v_visual_size: f32, @location(2) color: vec4, }; + + @vertex + fn main(@builtin(vertex_index) v_index: u32, instance: InstanceInput) -> VertexOutput { + var output: VertexOutput; + var pos = vec2(0.0, 0.0); + switch(v_index) { + case 0u: { pos = vec2(0.0, 0.0); } case 1u: { pos = vec2(1.0, 0.0); } + case 2u: { pos = vec2(0.0, 1.0); } case 3u: { pos = vec2(1.0, 0.0); } + case 4u: { pos = vec2(1.0, 1.0); } default: { pos = vec2(0.0, 1.0); } + } + let world_pos = vec2(instance.rect.x + (pos.x * instance.rect.z), + instance.rect.y + (pos.y * instance.rect.w)); + let u = mix(instance.uv_bounds.x, instance.uv_bounds.z, pos.x); + let v = mix(instance.uv_bounds.y, instance.uv_bounds.w, pos.y); + let panned = (world_pos * camera.zoom) + camera.pan; + let ndc = (panned / camera.screen_dimensions * 2.0) - vec2(1.0, 1.0); + output.position = vec4(ndc.x, -ndc.y, 0.0, 1.0); + output.uv = vec2(u, v); + output.v_visual_size = max(instance.rect.z, instance.rect.w) * camera.zoom; + output.color = instance.color; + return output; + }") + +(def text-fragment-shader " + @group(0) @binding(0) var sampler0: sampler; + @group(0) @binding(1) var texture0: texture_2d; + // Sizing uniform: pxRange, atlasEmSize, sharpness (color now per-instance) + struct Sizing { pxRange: f32, atlasEmSize: f32, sharpness: f32, padding: f32, }; + @group(0) @binding(3) var params: Sizing; + fn median(a: f32, b: f32, c: f32) -> f32 { return max(min(a, b), min(max(a, b), c)); } + + @fragment + fn main(@location(0) uv: vec2, @location(1) visual_size: f32, @location(2) color: vec4) -> @location(0) vec4 { + let msd = textureSample(texture0, sampler0, uv).rgb; + let sd = median(msd.r, msd.g, msd.b); + let screenPxRange = max(params.pxRange * (visual_size / params.atlasEmSize), 1.0); + // sharpness: negative = sharper edges, positive = softer edges, 0 = standard MSDF + let dist = sd - 0.5 + params.sharpness; + let opacity = clamp(dist * screenPxRange + 0.5, 0.0, 1.0); + // Use per-instance color instead of uniform color + return vec4(color.rgb, opacity * color.a); + }") + +(def slug-vertex-shader " + struct Camera { pan: vec2, zoom: f32, padding: f32, screen_dimensions: vec2, }; + @group(0) @binding(2) var camera: Camera; + + struct InstanceInput { + @location(0) rect: vec4, + @location(1) sample_bounds: vec4, + @location(2) inv_jac: vec4, + @location(3) banding: vec4, + @location(4) glyph: vec4, + @location(5) color: vec4, + }; + + struct VertexOutput { + @builtin(position) position: vec4, + @location(0) texcoord: vec2, + @location(1) color: vec4, + @location(2) banding: vec4, + @location(3) glyph: vec4, + }; + + @vertex + fn main(@builtin(vertex_index) v_index: u32, instance: InstanceInput) -> VertexOutput { + var output: VertexOutput; + var pos = vec2(0.0, 0.0); + var sign = vec2(-1.0, -1.0); + + switch(v_index) { + case 0u: { pos = vec2(0.0, 0.0); sign = vec2(-1.0, -1.0); } + case 1u: { pos = vec2(1.0, 0.0); sign = vec2(1.0, -1.0); } + case 2u: { pos = vec2(0.0, 1.0); sign = vec2(-1.0, 1.0); } + case 3u: { pos = vec2(1.0, 0.0); sign = vec2(1.0, -1.0); } + case 4u: { pos = vec2(1.0, 1.0); sign = vec2(1.0, 1.0); } + default: { pos = vec2(0.0, 1.0); sign = vec2(-1.0, 1.0); } + } + + let world_pos = vec2(instance.rect.x + (pos.x * instance.rect.z), + instance.rect.y + (pos.y * instance.rect.w)); + let dilation = 0.5 / max(camera.zoom, 0.0001); + let delta = sign * dilation; + let panned = ((world_pos + delta) * camera.zoom) + camera.pan; + let ndc = (panned / camera.screen_dimensions * 2.0) - vec2(1.0, 1.0); + + let sample_x = mix(instance.sample_bounds.x, instance.sample_bounds.z, pos.x); + let sample_y = mix(instance.sample_bounds.y, instance.sample_bounds.w, pos.y); + let sample_delta = vec2(dot(delta, instance.inv_jac.xy), + dot(delta, instance.inv_jac.zw)); + + output.position = vec4(ndc.x, -ndc.y, 0.0, 1.0); + output.texcoord = vec2(sample_x, sample_y) + sample_delta; + output.color = instance.color; + output.banding = instance.banding; + output.glyph = instance.glyph; + return output; + }") + +(def slug-fragment-shader " + const kLogBandTextureWidth: u32 = 12u; + const kMinDerivative: f32 = 1.0 / 65536.0; + + @group(0) @binding(0) var curveTexture: texture_2d; + @group(0) @binding(1) var bandTexture: texture_2d; + + fn saturate(x: f32) -> f32 { + return clamp(x, 0.0, 1.0); + } + + fn calc_root_code(y1: f32, y2: f32, y3: f32) -> u32 { + let i1 = bitcast(y1) >> 31u; + let i2 = bitcast(y2) >> 30u; + let i3 = bitcast(y3) >> 29u; + var shift = (i2 & 2u) | (i1 & ~2u); + shift = (i3 & 4u) | (shift & ~4u); + return (0x2E74u >> shift) & 0x0101u; + } + + fn solve_horiz_poly(p12: vec4, p3: vec2) -> vec2 { + let a = p12.xy - p12.zw * 2.0 + p3; + let b = p12.xy - p12.zw; + let ra = 1.0 / a.y; + let rb = 0.5 / b.y; + let d = sqrt(max(b.y * b.y - a.y * p12.y, 0.0)); + var t1 = (b.y - d) * ra; + var t2 = (b.y + d) * ra; + if (abs(a.y) < kMinDerivative) { + t1 = p12.y * rb; + t2 = t1; + } + return vec2((a.x * t1 - b.x * 2.0) * t1 + p12.x, + (a.x * t2 - b.x * 2.0) * t2 + p12.x); + } + + fn solve_vert_poly(p12: vec4, p3: vec2) -> vec2 { + let a = p12.xy - p12.zw * 2.0 + p3; + let b = p12.xy - p12.zw; + let ra = 1.0 / a.x; + let rb = 0.5 / b.x; + let d = sqrt(max(b.x * b.x - a.x * p12.x, 0.0)); + var t1 = (b.x - d) * ra; + var t2 = (b.x + d) * ra; + if (abs(a.x) < kMinDerivative) { + t1 = p12.x * rb; + t2 = t1; + } + return vec2((a.y * t1 - b.y * 2.0) * t1 + p12.y, + (a.y * t2 - b.y * 2.0) * t2 + p12.y); + } + + fn calc_band_loc(glyph_loc: vec2, offset: u32) -> vec2 { + let width = 1i << i32(kLogBandTextureWidth); + var x = glyph_loc.x + i32(offset); + var y = glyph_loc.y + (x >> i32(kLogBandTextureWidth)); + x = x & (width - 1); + return vec2(x, y); + } + + fn calc_coverage(xcov: f32, ycov: f32, xwgt: f32, ywgt: f32) -> f32 { + let weighted = abs(xcov * xwgt + ycov * ywgt) / max(xwgt + ywgt, kMinDerivative); + let coverage = max(weighted, min(abs(xcov), abs(ycov))); + return saturate(coverage); + } + + fn slug_render(render_coord: vec2, band_transform: vec4, glyph: vec4) -> f32 { + let ems_per_pixel = max(fwidth(render_coord), vec2(kMinDerivative, kMinDerivative)); + let pixels_per_em = 1.0 / ems_per_pixel; + let glyph_loc = vec2(i32(glyph.x), i32(glyph.y)); + let band_max = vec2(i32(glyph.z), i32(glyph.w & 0xFFFFu)); + let band_index = clamp(vec2(floor(render_coord * band_transform.xy + band_transform.zw)), + vec2(0, 0), + band_max); + + var xcov = 0.0; + var xwgt = 0.0; + let hband_data = textureLoad(bandTexture, vec2(glyph_loc.x + band_index.y, glyph_loc.y), 0).xy; + let hband_loc = calc_band_loc(glyph_loc, hband_data.y); + for (var curve_index = 0i; curve_index < i32(hband_data.x); curve_index = curve_index + 1i) { + let curve_loc_data = textureLoad(bandTexture, vec2(hband_loc.x + curve_index, hband_loc.y), 0).xy; + let curve_loc = vec2(i32(curve_loc_data.x), i32(curve_loc_data.y)); + let p12 = textureLoad(curveTexture, curve_loc, 0) - vec4(render_coord, render_coord); + let p3 = textureLoad(curveTexture, vec2(curve_loc.x + 1, curve_loc.y), 0).xy - render_coord; + if (max(max(p12.x, p12.z), p3.x) * pixels_per_em.x < -0.5) { + break; + } + let code = calc_root_code(p12.y, p12.w, p3.y); + if (code != 0u) { + let roots = solve_horiz_poly(p12, p3) * pixels_per_em.x; + if ((code & 1u) != 0u) { + xcov = xcov + saturate(roots.x + 0.5); + xwgt = max(xwgt, saturate(1.0 - abs(roots.x) * 2.0)); + } + if (code > 1u) { + xcov = xcov - saturate(roots.y + 0.5); + xwgt = max(xwgt, saturate(1.0 - abs(roots.y) * 2.0)); + } + } + } + + var ycov = 0.0; + var ywgt = 0.0; + let vband_data = textureLoad(bandTexture, vec2(glyph_loc.x + band_max.y + 1 + band_index.x, glyph_loc.y), 0).xy; + let vband_loc = calc_band_loc(glyph_loc, vband_data.y); + for (var curve_index = 0i; curve_index < i32(vband_data.x); curve_index = curve_index + 1i) { + let curve_loc_data = textureLoad(bandTexture, vec2(vband_loc.x + curve_index, vband_loc.y), 0).xy; + let curve_loc = vec2(i32(curve_loc_data.x), i32(curve_loc_data.y)); + let p12 = textureLoad(curveTexture, curve_loc, 0) - vec4(render_coord, render_coord); + let p3 = textureLoad(curveTexture, vec2(curve_loc.x + 1, curve_loc.y), 0).xy - render_coord; + if (max(max(p12.y, p12.w), p3.y) * pixels_per_em.y < -0.5) { + break; + } + let code = calc_root_code(p12.x, p12.z, p3.x); + if (code != 0u) { + let roots = solve_vert_poly(p12, p3) * pixels_per_em.y; + if ((code & 1u) != 0u) { + ycov = ycov - saturate(roots.x + 0.5); + ywgt = max(ywgt, saturate(1.0 - abs(roots.x) * 2.0)); + } + if (code > 1u) { + ycov = ycov + saturate(roots.y + 0.5); + ywgt = max(ywgt, saturate(1.0 - abs(roots.y) * 2.0)); + } + } + } + + return calc_coverage(xcov, ycov, xwgt, ywgt); + } + + @fragment + fn main(@location(0) texcoord: vec2, + @location(1) color: vec4, + @location(2) banding: vec4, + @location(3) glyph: vec4) -> @location(0) vec4 { + let coverage = slug_render(texcoord, banding, glyph); + return vec4(color.rgb, coverage * color.a); + }") + +;; Calculate bracket highlight rectangles +(defn calculate-bracket-rects [bracket-match font-size start-x start-y line-h] + (when bracket-match + (let [char-w (* font-size 0.56) + {:keys [open close]} bracket-match + make-rect (fn [{:keys [line col]}] + {:x (+ start-x (* col char-w)) + :y (+ start-y (* line line-h)) + :w char-w + :h line-h + ;; Golden/yellow highlight for matching brackets + :r 0.8 :g 0.6 :b 0.2 :a 0.4})] + [(make-rect open) (make-rect close)]))) + +;; Updated hit-test: clamps column to actual line length +(defn hit-test [x y font-size start-x start-y line-h line-lengths] + (let [char-w (* font-size 0.56) + rel-x (- x start-x) + rel-y (- y start-y) + line-idx (max 0 (Math/floor (/ rel-y line-h))) + ;; Clamp line index to valid range + line-idx (min line-idx (max 0 (dec (count line-lengths)))) + ;; Get actual line length, default to 0 for empty lines + line-len (get line-lengths line-idx 0) + ;; Clamp column to [0, line-length] + col-idx (-> (/ rel-x char-w) + (Math/round) + (max 0) + (min line-len))] + {:line line-idx :col col-idx})) + +;; --- 2. INITIALIZATION --- + +(def rect-stride 112) ;; 28 floats × 4 bytes = 112 bytes per rect +(def msdf-text-instance-stride 48) +(def slug-text-instance-stride 96) + +(defn init-rect-system + [^js/GPUDevice device fformat camera-buffer + & {:keys [initial-capacity tracker label] + :or {initial-capacity 1000 + label "rect/shared-system"}}] + (let [v-module (.createShaderModule device (clj->js {:code rect-vertex-shader})) + f-module (.createShaderModule device (clj->js {:code rect-fragment-shader})) + buf-size (* initial-capacity rect-stride) + instance-buffer (.createBuffer device (clj->js {:size buf-size :usage (bit-or js/GPUBufferUsage.VERTEX js/GPUBufferUsage.COPY_DST)})) + _ (gpu-budget/register-buffer! tracker instance-buffer label buf-size :active-bytes 0) + bg-layout (.createBindGroupLayout device (clj->js {:entries [{:binding 0 :visibility (bit-or js/GPUShaderStage.VERTEX js/GPUShaderStage.FRAGMENT) :buffer {:type "uniform"}}]})) + pipeline-layout (.createPipelineLayout device (clj->js {:bindGroupLayouts [bg-layout]})) + pipeline (.createRenderPipeline device + (clj->js {:layout pipeline-layout + :vertex {:module v-module :entryPoint "main" + :buffers [{:arrayStride rect-stride :stepMode "instance" + :attributes [{:shaderLocation 0 :offset 0 :format "float32x4"} ;; rect_geometry + {:shaderLocation 1 :offset 16 :format "float32x4"} ;; color + {:shaderLocation 2 :offset 32 :format "float32x4"} ;; corner_radii + {:shaderLocation 3 :offset 48 :format "float32x4"} ;; border_widths + {:shaderLocation 4 :offset 64 :format "float32x4"} ;; border_color + {:shaderLocation 5 :offset 80 :format "float32x4"} ;; gradient + {:shaderLocation 6 :offset 96 :format "float32x4"}]}]} ;; gradient_color2 + :fragment {:module f-module :entryPoint "main" + :targets [{:format fformat :blend {:color {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"} + :alpha {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"}}}]} + :primitive {:topology "triangle-list"}})) + bind-group (.createBindGroup device (clj->js {:layout bg-layout :entries [{:binding 0 :resource {:buffer camera-buffer}}]}))] + {:pipeline pipeline + :bind-group bind-group + :instance-buffer instance-buffer + :capacity initial-capacity + :num-instances 0 + :gpu-tracker tracker + :gpu-label label})) + +(defn create-camera-buffer + [^js/GPUDevice device tracker] + (let [camera-buffer (.createBuffer device (clj->js {:size 24 + :usage (bit-or js/GPUBufferUsage.UNIFORM + js/GPUBufferUsage.COPY_DST)}))] + (gpu-budget/register-buffer! tracker camera-buffer "text/shared-camera" 24 :active-bytes 24) + camera-buffer)) + +(defn- create-instance-buffer [^js/GPUDevice device tracker label initial-capacity stride] + (let [size (* initial-capacity stride) + buffer (.createBuffer device (clj->js {:size size + :usage (bit-or js/GPUBufferUsage.VERTEX + js/GPUBufferUsage.COPY_DST)}))] + (gpu-budget/register-buffer! tracker buffer label size :active-bytes 0) + buffer)) + +(defn- create-msdf-bind-group [^js/GPUDevice device layout sampler texture-view camera-buffer sizes-buffer] + (.createBindGroup device + (clj->js {:layout layout + :entries [{:binding 0 :resource sampler} + {:binding 1 :resource texture-view} + {:binding 2 :resource {:buffer camera-buffer}} + {:binding 3 :resource {:buffer sizes-buffer}}]}))) + +(defn- create-slug-bind-group [^js/GPUDevice device layout curve-view band-view camera-buffer] + (.createBindGroup device + (clj->js {:layout layout + :entries [{:binding 0 :resource curve-view} + {:binding 1 :resource band-view} + {:binding 2 :resource {:buffer camera-buffer}}]}))) + +(defn- create-msdf-font-resources [^js/GPUDevice device tracker font-bitmap texture-label] + (let [texture (.createTexture device (clj->js {:size {:width (.-width font-bitmap) + :height (.-height font-bitmap) + :depthOrArrayLayers 1} + :format "rgba8unorm" + :usage (bit-or js/GPUTextureUsage.RENDER_ATTACHMENT + js/GPUTextureUsage.TEXTURE_BINDING + js/GPUTextureUsage.COPY_DST)})) + sampler (.createSampler device (clj->js {:minFilter "linear" + :magFilter "linear" + :mipmapFilter "linear"})) + _ (.copyExternalImageToTexture (.-queue device) + (clj->js {:source font-bitmap}) + (clj->js {:texture texture}) + (clj->js {:width (.-width font-bitmap) + :height (.-height font-bitmap)}))] + (gpu-budget/register-texture! tracker texture texture-label + :format "rgba8unorm" + :width (.-width font-bitmap) + :height (.-height font-bitmap)) + {:font-texture texture + :font-texture-view (.createView texture) + :font-sampler sampler + :font-texture-label texture-label + :font-resource-kind :msdf})) + +(defn- create-slug-texture [^js/GPUDevice device tracker label format width height bytes bytes-per-row] + (let [texture (.createTexture device (clj->js {:size {:width width + :height height + :depthOrArrayLayers 1} + :format format + :usage (bit-or js/GPUTextureUsage.TEXTURE_BINDING + js/GPUTextureUsage.COPY_DST)})) + data (js/Uint8Array. bytes)] + (.writeTexture (.-queue device) + (clj->js {:texture texture}) + data + (clj->js {:bytesPerRow bytes-per-row + :rowsPerImage height}) + (clj->js {:width width :height height :depthOrArrayLayers 1})) + (gpu-budget/register-texture! tracker texture label + :format format + :width width + :height height) + texture)) + +(defn- create-slug-font-resources [^js/GPUDevice device tracker slug-assets] + (let [curve-width (get-in slug-assets [:meta :curveTexture :width]) + curve-height (get-in slug-assets [:meta :curveTexture :height]) + band-width (get-in slug-assets [:meta :bandTexture :width]) + band-height (get-in slug-assets [:meta :bandTexture :height]) + curve-texture (create-slug-texture device tracker "text/slug-curve" + "rgba16float" + curve-width curve-height + (:curve-bytes slug-assets) + (* curve-width 8)) + band-texture (create-slug-texture device tracker "text/slug-band" + "rg16uint" + band-width band-height + (:band-bytes slug-assets) + (* band-width 4))] + (js/console.log "[RENDERER] Created Slug font resources" + {:curve-texture [curve-width curve-height] + :band-texture [band-width band-height]}) + {:curve-texture curve-texture + :curve-texture-view (.createView curve-texture) + :curve-texture-label "text/slug-curve" + :band-texture band-texture + :band-texture-view (.createView band-texture) + :band-texture-label "text/slug-band" + :font-resource-kind :slug})) + +(defn- destroy-msdf-font-resources! [text-sys] + (when-let [tracker (:gpu-tracker text-sys)] + (when-let [texture (:font-texture text-sys)] + (gpu-budget/destroy-resource! tracker texture :reason :text-font-destroy))) + (when-let [^js texture (:font-texture text-sys)] + (.destroy texture))) + +(defn- destroy-slug-font-resources! [text-sys] + (when-let [tracker (:gpu-tracker text-sys)] + (when-let [curve-texture (:curve-texture text-sys)] + (gpu-budget/destroy-resource! tracker curve-texture :reason :slug-curve-destroy)) + (when-let [band-texture (:band-texture text-sys)] + (gpu-budget/destroy-resource! tracker band-texture :reason :slug-band-destroy))) + (when-let [^js curve-texture (:curve-texture text-sys)] + (.destroy curve-texture)) + (when-let [^js band-texture (:band-texture text-sys)] + (.destroy band-texture))) + +(defn destroy-text-system! [text-sys] + (when-let [tracker (:gpu-tracker text-sys)] + (when-let [instance-buffer (:instance-buffer text-sys)] + (gpu-budget/destroy-resource! tracker instance-buffer :reason :text-instance-destroy)) + (when (and (:owns-sizing-buffer? text-sys) (:sizes-uniform-buffer text-sys)) + (gpu-budget/destroy-resource! tracker (:sizes-uniform-buffer text-sys) :reason :text-sizing-destroy))) + (when-let [^js instance-buffer (:instance-buffer text-sys)] + (.destroy instance-buffer)) + (when (and (:owns-sizing-buffer? text-sys) (:sizes-uniform-buffer text-sys)) + (.destroy ^js (:sizes-uniform-buffer text-sys))) + (when (:owns-font-resources? text-sys) + (case (:backend text-sys) + :msdf (destroy-msdf-font-resources! text-sys) + :slug (destroy-slug-font-resources! text-sys) + nil))) + +(defn- init-msdf-text-system + [^js/GPUDevice device fformat camera-buffer font-assets + & {:keys [initial-capacity tracker label] + :or {initial-capacity 10000 + label "text/content"}}] + (let [font-bitmap (:bitmap font-assets) + vertex-module (.createShaderModule device (clj->js {:code text-vertex-shader})) + fragment-module (.createShaderModule device (clj->js {:code text-fragment-shader})) + font-resources (create-msdf-font-resources device tracker font-bitmap "text/atlas") + instance-buffer (create-instance-buffer device tracker label initial-capacity msdf-text-instance-stride) + sizes-buffer (.createBuffer device (clj->js {:size 16 + :usage (bit-or js/GPUBufferUsage.UNIFORM + js/GPUBufferUsage.COPY_DST)})) + _ (gpu-budget/register-buffer! tracker sizes-buffer "text/shared-sizing" 16 :active-bytes 16) + bg-layout (.createBindGroupLayout device (clj->js {:entries [{:binding 0 :visibility js/GPUShaderStage.FRAGMENT :sampler {:type "filtering"}} + {:binding 1 :visibility js/GPUShaderStage.FRAGMENT :texture {:sampleType "float"}} + {:binding 2 :visibility js/GPUShaderStage.VERTEX :buffer {:type "uniform"}} + {:binding 3 :visibility js/GPUShaderStage.FRAGMENT :buffer {:type "uniform"}}]})) + pipeline-layout (.createPipelineLayout device (clj->js {:bindGroupLayouts [bg-layout]})) + pipeline (.createRenderPipeline device + (clj->js {:layout pipeline-layout + :vertex {:module vertex-module + :entryPoint "main" + :buffers [{:arrayStride msdf-text-instance-stride + :stepMode "instance" + :attributes [{:shaderLocation 0 :offset 0 :format "float32x4"} + {:shaderLocation 1 :offset 16 :format "float32x4"} + {:shaderLocation 2 :offset 32 :format "float32x4"}]}]} + :fragment {:module fragment-module + :entryPoint "main" + :targets [{:format fformat + :blend {:color {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"} + :alpha {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"}}}]} + :primitive {:topology "triangle-list"}})) + bind-group (create-msdf-bind-group device bg-layout (:font-sampler font-resources) (:font-texture-view font-resources) camera-buffer sizes-buffer)] + (js/console.log "[RENDERER] Init text system" + {:backend :msdf + :label label + :initial-capacity initial-capacity + :instance-stride msdf-text-instance-stride + :atlas-size [(.-width font-bitmap) (.-height font-bitmap)]}) + (merge font-resources + {:backend :msdf + :pipeline pipeline + :bind-group bind-group + :bind-group-layout bg-layout + :camera-uniform-buffer camera-buffer + :sizes-uniform-buffer sizes-buffer + :instance-buffer instance-buffer + :instance-stride msdf-text-instance-stride + :num-instances 0 + :gpu-tracker tracker + :gpu-label label + :owns-font-resources? true + :owns-sizing-buffer? true}))) + +(defn- init-slug-text-system + [^js/GPUDevice device fformat camera-buffer font-assets + & {:keys [initial-capacity tracker label] + :or {initial-capacity 10000 + label "text/content"}}] + (let [vertex-module (.createShaderModule device (clj->js {:code slug-vertex-shader})) + fragment-module (.createShaderModule device (clj->js {:code slug-fragment-shader})) + font-resources (create-slug-font-resources device tracker (:slug font-assets)) + instance-buffer (create-instance-buffer device tracker label initial-capacity slug-text-instance-stride) + bg-layout (.createBindGroupLayout device (clj->js {:entries [{:binding 0 :visibility js/GPUShaderStage.FRAGMENT :texture {:sampleType "unfilterable-float"}} + {:binding 1 :visibility js/GPUShaderStage.FRAGMENT :texture {:sampleType "uint"}} + {:binding 2 :visibility js/GPUShaderStage.VERTEX :buffer {:type "uniform"}}]})) + pipeline-layout (.createPipelineLayout device (clj->js {:bindGroupLayouts [bg-layout]})) + pipeline (.createRenderPipeline device + (clj->js {:layout pipeline-layout + :vertex {:module vertex-module + :entryPoint "main" + :buffers [{:arrayStride slug-text-instance-stride + :stepMode "instance" + :attributes [{:shaderLocation 0 :offset 0 :format "float32x4"} + {:shaderLocation 1 :offset 16 :format "float32x4"} + {:shaderLocation 2 :offset 32 :format "float32x4"} + {:shaderLocation 3 :offset 48 :format "float32x4"} + {:shaderLocation 4 :offset 64 :format "uint32x4"} + {:shaderLocation 5 :offset 80 :format "float32x4"}]}]} + :fragment {:module fragment-module + :entryPoint "main" + :targets [{:format fformat + :blend {:color {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"} + :alpha {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"}}}]} + :primitive {:topology "triangle-list"}})) + bind-group (create-slug-bind-group device bg-layout (:curve-texture-view font-resources) (:band-texture-view font-resources) camera-buffer)] + (js/console.log "[RENDERER] Init text system" + {:backend :slug + :label label + :initial-capacity initial-capacity + :instance-stride slug-text-instance-stride}) + (merge font-resources + {:backend :slug + :pipeline pipeline + :bind-group bind-group + :bind-group-layout bg-layout + :camera-uniform-buffer camera-buffer + :sizes-uniform-buffer nil + :instance-buffer instance-buffer + :instance-stride slug-text-instance-stride + :num-instances 0 + :gpu-tracker tracker + :gpu-label label + :owns-font-resources? true + :owns-sizing-buffer? false}))) + +(defn init-text-system + [^js/GPUDevice device fformat camera-buffer font-assets & {:as opts}] + (if (= :slug (:backend font-assets)) + (apply init-slug-text-system device fformat camera-buffer font-assets (mapcat identity opts)) + (apply init-msdf-text-system device fformat camera-buffer font-assets (mapcat identity opts)))) + +(defn update-font-assets [^js/GPUDevice device text-sys font-assets] + (js/console.log "[RENDERER] Update font assets" + {:current-backend (:backend text-sys) + :requested-backend (:backend font-assets) + :font-id (:id font-assets)}) + (if (not= (:backend text-sys) (:backend font-assets)) + (throw (ex-info "Text backend mismatch during font update." + {:current (:backend text-sys) + :requested (:backend font-assets)})) + (case (:backend text-sys) + :msdf + (let [tracker (:gpu-tracker text-sys) + old-texture (:font-texture text-sys) + font-resources (create-msdf-font-resources device tracker (:bitmap font-assets) (or (:font-texture-label text-sys) "text/atlas"))] + (when (and tracker old-texture (:owns-font-resources? text-sys)) + (gpu-budget/destroy-resource! tracker old-texture :reason :font-update)) + (when (and old-texture (:owns-font-resources? text-sys)) + (.destroy ^js old-texture)) + (merge text-sys + font-resources + {:bind-group (create-msdf-bind-group device + (:bind-group-layout text-sys) + (:font-sampler font-resources) + (:font-texture-view font-resources) + (:camera-uniform-buffer text-sys) + (:sizes-uniform-buffer text-sys)) + :owns-font-resources? true})) + + :slug + (let [tracker (:gpu-tracker text-sys) + old-curve (:curve-texture text-sys) + old-band (:band-texture text-sys) + font-resources (create-slug-font-resources device tracker (:slug font-assets))] + (when (and tracker old-curve (:owns-font-resources? text-sys)) + (gpu-budget/destroy-resource! tracker old-curve :reason :slug-curve-update)) + (when (and tracker old-band (:owns-font-resources? text-sys)) + (gpu-budget/destroy-resource! tracker old-band :reason :slug-band-update)) + (when (and old-curve (:owns-font-resources? text-sys)) + (.destroy ^js old-curve)) + (when (and old-band (:owns-font-resources? text-sys)) + (.destroy ^js old-band)) + (merge text-sys + font-resources + {:bind-group (create-slug-bind-group device + (:bind-group-layout text-sys) + (:curve-texture-view font-resources) + (:band-texture-view font-resources) + (:camera-uniform-buffer text-sys)) + :owns-font-resources? true}))))) + +(defn share-font-resources + "Point a secondary text system at a primary text system's shared font resources." + [target-state source-state] + (when (and (:owns-font-resources? target-state) + (not= (:backend target-state) (:backend source-state))) + (throw (ex-info "Cannot share font resources across different text backends." + {:source (:backend source-state) + :target (:backend target-state)}))) + (when (:owns-font-resources? target-state) + (case (:backend target-state) + :msdf (destroy-msdf-font-resources! target-state) + :slug (destroy-slug-font-resources! target-state) + nil)) + (-> target-state + (assoc :bind-group (:bind-group source-state) + :owns-font-resources? false) + (merge + (select-keys source-state + [:font-texture :font-texture-view :font-sampler :font-texture-label + :curve-texture :curve-texture-view :curve-texture-label + :band-texture :band-texture-view :band-texture-label + :font-resource-kind])))) + +(defn recreate-text-system + [^js/GPUDevice device fformat old-text-sys font-assets] + (let [capacity (max 1 (quot (.-size ^js (:instance-buffer old-text-sys)) + (:instance-stride old-text-sys))) + label (:gpu-label old-text-sys) + tracker (:gpu-tracker old-text-sys) + camera-buffer (:camera-uniform-buffer old-text-sys)] + (js/console.log "[RENDERER] Recreate text system" + {:old-backend (:backend old-text-sys) + :new-backend (:backend font-assets) + :label label + :capacity capacity + :font-id (:id font-assets)}) + (destroy-text-system! old-text-sys) + (init-text-system device fformat camera-buffer font-assets + :initial-capacity capacity + :tracker tracker + :label label))) + +(defn clone-text-system + "Create a lightweight text system clone sharing pipeline, bind-group, camera, + and font resources with the parent. Only the instance buffer is new." + [^js/GPUDevice device parent-text-sys initial-capacity] + (let [stride (:instance-stride parent-text-sys) + ib (create-instance-buffer device (:gpu-tracker parent-text-sys) "text/chrome" initial-capacity stride)] + (assoc parent-text-sys + :instance-buffer ib + :instance-stride stride + :num-instances 0 + :line-offsets nil + :gpu-label "text/chrome" + :owns-font-resources? false + :owns-sizing-buffer? false))) + +;; --- Shadow system --- +(def shadow-stride 80) ;; 20 floats × 4 bytes = 80 bytes per shadow + +(defn init-shadow-system + [^js/GPUDevice device fformat camera-buffer + & {:keys [initial-capacity tracker label] + :or {initial-capacity 256 + label "shadow/shared-system"}}] + (let [v-module (.createShaderModule device (clj->js {:code shadow-vertex-shader})) + f-module (.createShaderModule device (clj->js {:code shadow-fragment-shader})) + buf-size (* initial-capacity shadow-stride) + instance-buffer (.createBuffer device (clj->js {:size buf-size :usage (bit-or js/GPUBufferUsage.VERTEX js/GPUBufferUsage.COPY_DST)})) + _ (gpu-budget/register-buffer! tracker instance-buffer label buf-size :active-bytes 0) + bg-layout (.createBindGroupLayout device (clj->js {:entries [{:binding 0 :visibility (bit-or js/GPUShaderStage.VERTEX js/GPUShaderStage.FRAGMENT) :buffer {:type "uniform"}}]})) + pipeline-layout (.createPipelineLayout device (clj->js {:bindGroupLayouts [bg-layout]})) + pipeline (.createRenderPipeline device + (clj->js {:layout pipeline-layout + :vertex {:module v-module :entryPoint "main" + :buffers [{:arrayStride shadow-stride :stepMode "instance" + :attributes [{:shaderLocation 0 :offset 0 :format "float32x4"} ;; expanded_rect + {:shaderLocation 1 :offset 16 :format "float32x4"} ;; shadow_color + {:shaderLocation 2 :offset 32 :format "float32x4"} ;; corner_radii + {:shaderLocation 3 :offset 48 :format "float32x4"} ;; blur_params + {:shaderLocation 4 :offset 64 :format "float32x4"}]}]} ;; inner_rect + :fragment {:module f-module :entryPoint "main" + :targets [{:format fformat :blend {:color {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"} + :alpha {:srcFactor "src-alpha" :dstFactor "one-minus-src-alpha"}}}]} + :primitive {:topology "triangle-list"}})) + bind-group (.createBindGroup device (clj->js {:layout bg-layout :entries [{:binding 0 :resource {:buffer camera-buffer}}]}))] + {:pipeline pipeline + :bind-group bind-group + :instance-buffer instance-buffer + :capacity initial-capacity + :num-instances 0 + :gpu-tracker tracker + :gpu-label label})) + +(defn update-shadows [^js device shadow-system shadows] + (let [n (count shadows) + floats-per-shadow 20 + data (js/Float32Array. (* n floats-per-shadow)) + required-bytes (.-byteLength data) + current-buffer (:instance-buffer shadow-system) + current-size (.-size ^js current-buffer) + needs-resize? (> required-bytes current-size) + new-size (max required-bytes (* n shadow-stride)) + new-buffer (if needs-resize? + (.createBuffer device (clj->js {:size new-size + :usage (bit-or js/GPUBufferUsage.VERTEX + js/GPUBufferUsage.COPY_DST)})) + current-buffer)] + (when needs-resize? + (gpu-budget/replace-buffer! (:gpu-tracker shadow-system) current-buffer new-buffer (:gpu-label shadow-system) + new-size + :active-bytes (* n shadow-stride) + :reason :shadow-resize) + (.destroy ^js current-buffer)) + (loop [i 0 ss shadows] + (when (seq ss) + (let [s (first ss) + {:keys [x y w h blur offset-x offset-y spread color corner-radii radius]} s + blur (or blur 8.0) + ox (or offset-x 0.0) + oy (or offset-y 0.0) + spread (or spread 0.0) + sc (or color [0 0 0 0.25]) + expand (* 3.0 blur) + ;; Expanded quad (captures Gaussian tail) + ex (- x expand (max ox 0)) + ey (- y expand (max oy 0)) + ew (+ w (* 2 expand) (Math/abs ox)) + eh (+ h (* 2 expand) (Math/abs oy)) + ;; Inner rect relative to expanded quad origin + ix (- x ex) + iy (- y ey) + ;; Corner radii + cr corner-radii + ur (or radius 0.0) + base (* i floats-per-shadow)] + ;; Slot 0: expanded_rect + (aset data (+ base 0) ex) (aset data (+ base 1) ey) + (aset data (+ base 2) ew) (aset data (+ base 3) eh) + ;; Slot 1: shadow_color + (aset data (+ base 4) (nth sc 0)) (aset data (+ base 5) (nth sc 1)) + (aset data (+ base 6) (nth sc 2)) (aset data (+ base 7) (nth sc 3)) + ;; Slot 2: corner_radii + (if cr + (do (aset data (+ base 8) (nth cr 0)) + (aset data (+ base 9) (nth cr 1)) + (aset data (+ base 10) (nth cr 2)) + (aset data (+ base 11) (nth cr 3))) + (do (aset data (+ base 8) ur) (aset data (+ base 9) ur) + (aset data (+ base 10) ur) (aset data (+ base 11) ur))) + ;; Slot 3: blur_params [blur, offset_x, offset_y, spread] + (aset data (+ base 12) blur) (aset data (+ base 13) ox) + (aset data (+ base 14) oy) (aset data (+ base 15) spread) + ;; Slot 4: inner_rect (relative to expanded quad, in zoom-scaled space) + (aset data (+ base 16) ix) (aset data (+ base 17) iy) + (aset data (+ base 18) w) (aset data (+ base 19) h) + (recur (inc i) (next ss))))) + (when (pos? n) + (.writeBuffer (.-queue device) new-buffer 0 data)) + (gpu-budget/set-active-bytes! (:gpu-tracker shadow-system) new-buffer (* n shadow-stride)) + (assoc shadow-system :instance-buffer new-buffer :num-instances n))) + +;; --- Clear-quad system (Phase 6E: dirty-present) --- +(def clear-quad-shader " + @vertex + fn vs_main(@builtin(vertex_index) v: u32) -> @builtin(position) vec4 { + // Fullscreen triangle from vertex index — no vertex buffer needed + let x = f32(i32(v & 1u)) * 4.0 - 1.0; + let y = f32(i32(v >> 1u)) * 4.0 - 1.0; + return vec4(x, y, 0.0, 1.0); + } + @fragment + fn fs_main() -> @location(0) vec4 { + return vec4(0.0, 0.0, 0.0, 1.0); + }") + +(defn init-clear-quad [^js/GPUDevice device fformat] + (let [module (.createShaderModule device (clj->js {:code clear-quad-shader})) + layout (.createPipelineLayout device (clj->js {:bindGroupLayouts []})) + pipeline (.createRenderPipeline device + (clj->js {:layout layout + :vertex {:module module :entryPoint "vs_main"} + :fragment {:module module :entryPoint "fs_main" + :targets [{:format fformat + :writeMask 0xF}]} + :primitive {:topology "triangle-list"}}))] + {:pipeline pipeline})) + +;; --- Persistent render target (Phase 6E: survives swap chain double-buffering) --- + +(defn create-render-target + [^js device width height fformat & {:keys [tracker label previous] + :or {label "render-target/persistent"}}] + (let [safe-width (max 1 width) + safe-height (max 1 height) + tex (.createTexture device + (clj->js {:size {:width safe-width :height safe-height} + :format fformat + :usage (bit-or js/GPUTextureUsage.RENDER_ATTACHMENT + js/GPUTextureUsage.COPY_SRC)})) + old-texture (:texture previous)] + (js/console.log "[RENDERER] Create render target" + {:label label + :width safe-width + :height safe-height + :format fformat + :replacing? (boolean old-texture)}) + (if old-texture + (gpu-budget/replace-texture! tracker old-texture tex label + :format fformat + :width safe-width + :height safe-height + :reason :render-target-resize) + (gpu-budget/register-texture! tracker tex label + :format fformat + :width safe-width + :height safe-height)) + (when old-texture + (.destroy ^js old-texture)) + {:texture tex + :view (.createView tex) + :width safe-width + :height safe-height + :gpu-tracker tracker + :gpu-label label})) + +(defn destroy-render-target! [{:keys [^js texture gpu-tracker]}] + (when texture + (gpu-budget/destroy-resource! gpu-tracker texture :reason :render-target-destroy) + (.destroy texture))) + +(defn create-editor-state [{:keys [device format font-assets gpu-budget]}] + (let [camera-buffer (create-camera-buffer device gpu-budget) + text-sys (init-text-system device format camera-buffer font-assets + :initial-capacity 1000000 + :tracker gpu-budget + :label "text/content") + rect-sys (init-rect-system device format (:camera-uniform-buffer text-sys) + :initial-capacity 50000 + :tracker gpu-budget + :label "rect/shared-system") + shadow-sys (init-shadow-system device format (:camera-uniform-buffer text-sys) + :initial-capacity 256 + :tracker gpu-budget + :label "shadow/shared-system") + clear-quad (init-clear-quad device format) + + camera-floats (js/Float32Array. 6) + + pass-descriptor (clj->js {:colorAttachments [{:view nil + :clearValue {:r 0.0 :g 0.0 :b 0.0 :a 0.0} + :loadOp "clear" + :storeOp "store"}]})] + (js/console.log "[RENDERER] Create editor state" + {:format format + :font-id (:id font-assets) + :font-backend (:backend font-assets) + :camera-buffer-bytes 24}) + + {:text-sys text-sys + :rect-sys rect-sys + :shadow-sys shadow-sys + :clear-quad clear-quad + :format format + :camera-floats camera-floats + :pass-descriptor pass-descriptor})) + +;; --- 3. UPDATES (CPU -> GPU) --- +(defn- make-snapper [snap-step] + (when (and snap-step (pos? snap-step)) + (fn [v] (* (Math/round (/ v snap-step)) snap-step)))) + +(defn- token-color [{:keys [r g b a]}] + [(or r 1.0) (or g 1.0) (or b 1.0) (or a 1.0)]) + +(defn- advance-width [fsize char-width snap] + (let [v (* fsize char-width)] + (if snap (snap v) v))) + +(defn- glyph-map [glyphs] + (reduce (fn [acc glyph] (assoc acc (:unicode glyph) glyph)) {} glyphs)) + +(defn- font-line-height [font-assets] + (or (get-in font-assets [:atlas :metrics :lineHeight]) + (get-in font-assets [:slug :meta :metrics :lineHeight]) + 1.2)) + +(defn- shape-msdf-line + [texts global-fsize font-assets & {:keys [char-width snap-step] :or {char-width 0.56}}] + (let [atlas-w (or (get-in font-assets [:atlas :atlas :width]) 1) + atlas-h (or (get-in font-assets [:atlas :atlas :height]) 1) + line-h (font-line-height font-assets) + glyphs (glyph-map (get-in font-assets [:atlas :glyphs])) + res (atom [])] + (doseq [txt texts] + (let [{:keys [text x y]} txt + [cr cg cb ca] (token-color txt) + fsize (or (:size txt) global-fsize) + snap (make-snapper snap-step) + start-x (if snap (snap x) x) + start-y (if snap (snap y) y) + advance (advance-width fsize char-width snap) + !x (atom start-x) + !y (atom start-y)] + (doseq [ch (seq text)] + (let [code (.charCodeAt ch 0)] + (cond + (= ch \newline) (do (reset! !x start-x) (reset! !y (+ @!y (* fsize line-h)))) + (= ch \space) (swap! !x + advance) + :else + (when-let [g (get glyphs code)] + (let [pb (:planeBounds g) + ab (:atlasBounds g) + sl (+ @!x (* fsize (or (:left pb) 0))) + sr (+ @!x (* fsize (or (:right pb) 0))) + st (- @!y (* fsize (or (:top pb) 0))) + sb (- @!y (* fsize (or (:bottom pb) 0))) + ul (/ (:left ab) atlas-w) + ur (/ (:right ab) atlas-w) + vt (- 1.0 (/ (:top ab) atlas-h)) + vb (- 1.0 (/ (:bottom ab) atlas-h))] + (swap! !x + advance) + (swap! res conj {:rect [sl st (- sr sl) (- sb st)] + :uv [ul vt ur vb] + :color [cr cg cb ca]})))))))) + @res)) + +(defn- shape-slug-line + [texts global-fsize font-assets & {:keys [char-width snap-step] :or {char-width 0.56}}] + (let [line-h (font-line-height font-assets) + glyphs (glyph-map (get-in font-assets [:slug :meta :glyphs])) + res (atom [])] + (doseq [txt texts] + (let [{:keys [text x y]} txt + [cr cg cb ca] (token-color txt) + fsize (or (:size txt) global-fsize) + snap (make-snapper snap-step) + start-x (if snap (snap x) x) + start-y (if snap (snap y) y) + advance (advance-width fsize char-width snap) + inv-size (if (pos? fsize) (/ 1.0 fsize) 0.0) + !x (atom start-x) + !y (atom start-y)] + (doseq [ch (seq text)] + (let [code (.charCodeAt ch 0)] + (cond + (= ch \newline) (do (reset! !x start-x) (reset! !y (+ @!y (* fsize line-h)))) + (= ch \space) (swap! !x + advance) + :else + (when-let [g (get glyphs code)] + (let [sample-bounds (or (:sampleBounds g) (:planeBounds g)) + slug (:slug g) + left (or (:left sample-bounds) 0.0) + right (or (:right sample-bounds) 0.0) + top (or (:top sample-bounds) 0.0) + bottom (or (:bottom sample-bounds) 0.0) + world-left (+ @!x (* fsize left)) + world-right (+ @!x (* fsize right)) + world-top (- @!y (* fsize top)) + world-bottom (- @!y (* fsize bottom))] + (swap! !x + advance) + (swap! res conj {:rect [world-left world-top (- world-right world-left) (- world-bottom world-top)] + :sample-bounds [left top right bottom] + :inv-jac [inv-size 0.0 0.0 (- inv-size)] + :banding [(or (get-in slug [:banding :scaleX]) 0.0) + (or (get-in slug [:banding :scaleY]) 0.0) + (or (get-in slug [:banding :offsetX]) 0.0) + (or (get-in slug [:banding :offsetY]) 0.0)] + :glyph [(or (get-in slug [:glyphLoc :x]) 0) + (or (get-in slug [:glyphLoc :y]) 0) + (or (get-in slug [:bandMax :x]) 0) + (or (:packedBandMeta slug) 0)] + :color [cr cg cb ca]})))))))) + @res)) + +(defn shape-text [texts global-fsize font-assets & {:as opts}] + (if (= :slug (:backend font-assets)) + (apply shape-slug-line texts global-fsize font-assets (mapcat identity opts)) + (apply shape-msdf-line texts global-fsize font-assets (mapcat identity opts)))) + +(defn- line-offsets-for [lines] + (loop [remaining lines + current-idx 0 + offsets []] + (if (seq remaining) + (let [cnt (:count (first remaining))] + (recur (next remaining) (+ current-idx cnt) (conj offsets current-idx))) + (vec offsets)))) + +(defn- ensure-text-instance-buffer + [^js/GPUDevice device renderer-state required-size active-bytes] + (let [current-buffer (:instance-buffer renderer-state) + current-size (.-size ^js current-buffer) + needs-resize? (> required-size current-size) + new-buffer (if needs-resize? + (.createBuffer device (clj->js {:size required-size + :usage (bit-or js/GPUBufferUsage.VERTEX + js/GPUBufferUsage.COPY_DST)})) + current-buffer)] + (when needs-resize? + (gpu-budget/replace-buffer! (:gpu-tracker renderer-state) current-buffer new-buffer (:gpu-label renderer-state) + required-size + :active-bytes active-bytes + :reason :text-resize) + (.destroy ^js current-buffer)) + new-buffer)) + +(defn- pack-msdf-instances! [^js data shaped-lines] + (loop [lines shaped-lines + global-i 0] + (when (seq lines) + (let [instances (:instances (first lines))] + (loop [remaining instances + sub-i 0] + (when (seq remaining) + (let [{:keys [rect uv color]} (first remaining) + [x y w h] rect + [u-min v-min u-max v-max] uv + [cr cg cb ca] color + base (* (+ global-i sub-i) 12)] + (aset data (+ base 0) x) + (aset data (+ base 1) y) + (aset data (+ base 2) w) + (aset data (+ base 3) h) + (aset data (+ base 4) u-min) + (aset data (+ base 5) v-min) + (aset data (+ base 6) u-max) + (aset data (+ base 7) v-max) + (aset data (+ base 8) cr) + (aset data (+ base 9) cg) + (aset data (+ base 10) cb) + (aset data (+ base 11) ca) + (recur (next remaining) (inc sub-i))))) + (recur (next lines) (+ global-i (:count (first lines)))))))) + +(defn- pack-slug-instances! [^js float-view ^js uint-view shaped-lines] + (loop [lines shaped-lines + global-i 0] + (when (seq lines) + (let [instances (:instances (first lines))] + (loop [remaining instances + sub-i 0] + (when (seq remaining) + (let [{:keys [rect sample-bounds inv-jac banding glyph color]} (first remaining) + [x y w h] rect + [sl st sr sb] sample-bounds + [jx jy kx ky] inv-jac + [sx sy ox oy] banding + [gx gy gzx gwy] glyph + [cr cg cb ca] color + base (* (+ global-i sub-i) 24)] + (aset float-view (+ base 0) x) + (aset float-view (+ base 1) y) + (aset float-view (+ base 2) w) + (aset float-view (+ base 3) h) + (aset float-view (+ base 4) sl) + (aset float-view (+ base 5) st) + (aset float-view (+ base 6) sr) + (aset float-view (+ base 7) sb) + (aset float-view (+ base 8) jx) + (aset float-view (+ base 9) jy) + (aset float-view (+ base 10) kx) + (aset float-view (+ base 11) ky) + (aset float-view (+ base 12) sx) + (aset float-view (+ base 13) sy) + (aset float-view (+ base 14) ox) + (aset float-view (+ base 15) oy) + (aset uint-view (+ base 16) gx) + (aset uint-view (+ base 17) gy) + (aset uint-view (+ base 18) gzx) + (aset uint-view (+ base 19) gwy) + (aset float-view (+ base 20) cr) + (aset float-view (+ base 21) cg) + (aset float-view (+ base 22) cb) + (aset float-view (+ base 23) ca) + (recur (next remaining) (inc sub-i))))) + (recur (next lines) (+ global-i (:count (first lines)))))))) + +(defn update-text-data + [^js/GPUDevice device renderer-state texts font-assets font-size + & {:keys [px-range line-height-factor line-height sharpness char-width snap-step] + :or {px-range 8.0 line-height-factor 1.0 sharpness 0.0 char-width 0.56}}] + (when (not= (:backend renderer-state) (:backend font-assets)) + (throw (ex-info "Text backend mismatch during text upload." + {:renderer-backend (:backend renderer-state) + :font-backend (:backend font-assets)}))) + (let [line-h (or line-height (* font-size line-height-factor)) + shaped-lines (mapv (fn [tokens-in-line] + (let [instances (shape-text tokens-in-line font-size font-assets + :char-width char-width + :snap-step snap-step)] + {:instances instances + :count (count instances)})) + texts) + actual-instances (reduce + (map :count shaped-lines)) + buffer-instance-count (max actual-instances 1) + stride (:instance-stride renderer-state) + line-offsets (line-offsets-for shaped-lines) + active-bytes (* actual-instances stride)] + (case (:backend renderer-state) + :msdf + (let [data (js/Float32Array. (* buffer-instance-count 12)) + required-size (.-byteLength data) + new-buffer (ensure-text-instance-buffer device renderer-state required-size active-bytes) + atlas-em (or (get-in font-assets [:atlas :atlas :size]) 64.0)] + (pack-msdf-instances! data shaped-lines) + (.writeBuffer (.-queue device) new-buffer 0 data) + (when-let [sizes-buffer (:sizes-uniform-buffer renderer-state)] + (let [sizes (js/Float32Array. #js [(float px-range) (float atlas-em) (float sharpness) 0.0])] + (.writeBuffer (.-queue device) sizes-buffer 0 sizes))) + (gpu-budget/set-active-bytes! (:gpu-tracker renderer-state) new-buffer active-bytes) + (assoc renderer-state + :instance-buffer new-buffer + :num-instances actual-instances + :line-offsets line-offsets + :line-height line-h)) + + :slug + (let [raw-buffer (js/ArrayBuffer. (* buffer-instance-count stride)) + float-view (js/Float32Array. raw-buffer) + uint-view (js/Uint32Array. raw-buffer) + upload-view (js/Uint8Array. raw-buffer) + required-size (.-byteLength upload-view) + new-buffer (ensure-text-instance-buffer device renderer-state required-size active-bytes)] + (pack-slug-instances! float-view uint-view shaped-lines) + (.writeBuffer (.-queue device) new-buffer 0 upload-view) + (gpu-budget/set-active-bytes! (:gpu-tracker renderer-state) new-buffer active-bytes) + (assoc renderer-state + :instance-buffer new-buffer + :num-instances actual-instances + :line-offsets line-offsets + :line-height line-h))))) + + +(defn update-rects [^js device rect-system rects] + (let [n (count rects) + floats-per-rect 28 + data (js/Float32Array. (* n floats-per-rect)) + required-bytes (.-byteLength data) + current-buffer (:instance-buffer rect-system) + current-size (.-size ^js current-buffer) + needs-resize? (> required-bytes current-size) + new-size (max required-bytes (* n rect-stride)) + new-buffer (if needs-resize? + (.createBuffer device (clj->js {:size new-size + :usage (bit-or js/GPUBufferUsage.VERTEX + js/GPUBufferUsage.COPY_DST)})) + current-buffer)] + (when needs-resize? + (gpu-budget/replace-buffer! (:gpu-tracker rect-system) current-buffer new-buffer (:gpu-label rect-system) + new-size + :active-bytes (* n rect-stride) + :reason :rect-resize) + (.destroy ^js current-buffer)) + (loop [i 0 rs rects] + (when (seq rs) + (let [rect (first rs) + {:keys [x y w h r g b a]} rect + base (* i floats-per-rect) + ;; Corner radii: uniform :radius or per-corner :corner-radii [tl tr br bl] + cr (:corner-radii rect) + uniform-r (or (:radius rect) 0.0) + ;; Border widths: uniform :border-width or per-side :border-widths [t r b l] + bw (:border-widths rect) + uniform-bw (or (:border-width rect) 0.0) + ;; Border color + bc (or (:border-color rect) [0 0 0 0]) + ;; Gradient: [angle t-stop 0 0] + gr (or (:gradient rect) [0 0 0 0]) + ;; Gradient color 2 + gc2 (or (:gradient-color2 rect) [0 0 0 0])] + ;; Slot 0: rect_geometry [x y w h] + (aset data (+ base 0) x) (aset data (+ base 1) y) + (aset data (+ base 2) w) (aset data (+ base 3) h) + ;; Slot 1: color [r g b a] + (aset data (+ base 4) r) (aset data (+ base 5) g) + (aset data (+ base 6) b) (aset data (+ base 7) a) + ;; Slot 2: corner_radii [tl tr br bl] + (if cr + (do (aset data (+ base 8) (nth cr 0)) + (aset data (+ base 9) (nth cr 1)) + (aset data (+ base 10) (nth cr 2)) + (aset data (+ base 11) (nth cr 3))) + (do (aset data (+ base 8) uniform-r) + (aset data (+ base 9) uniform-r) + (aset data (+ base 10) uniform-r) + (aset data (+ base 11) uniform-r))) + ;; Slot 3: border_widths [top right bottom left] + (if bw + (do (aset data (+ base 12) (nth bw 0)) + (aset data (+ base 13) (nth bw 1)) + (aset data (+ base 14) (nth bw 2)) + (aset data (+ base 15) (nth bw 3))) + (do (aset data (+ base 12) uniform-bw) + (aset data (+ base 13) uniform-bw) + (aset data (+ base 14) uniform-bw) + (aset data (+ base 15) uniform-bw))) + ;; Slot 4: border_color [r g b a] + (aset data (+ base 16) (nth bc 0)) + (aset data (+ base 17) (nth bc 1)) + (aset data (+ base 18) (nth bc 2)) + (aset data (+ base 19) (nth bc 3)) + ;; Slot 5: gradient [angle t_stop 0 0] + (aset data (+ base 20) (nth gr 0)) + (aset data (+ base 21) (nth gr 1)) + (aset data (+ base 22) (nth gr 2)) + (aset data (+ base 23) (nth gr 3)) + ;; Slot 6: gradient_color2 [r g b a] + (aset data (+ base 24) (nth gc2 0)) + (aset data (+ base 25) (nth gc2 1)) + (aset data (+ base 26) (nth gc2 2)) + (aset data (+ base 27) (nth gc2 3)) + (recur (inc i) (next rs))))) + (when (pos? n) + (.writeBuffer (.-queue device) new-buffer 0 data)) + (gpu-budget/set-active-bytes! (:gpu-tracker rect-system) new-buffer (* n rect-stride)) + (assoc rect-system :instance-buffer new-buffer :num-instances n))) + + +(defn update-camera [^js device camera-buffer ^js floats pan-x pan-y zoom w h] + (aset floats 0 pan-x) + (aset floats 1 pan-y) + (aset floats 2 zoom) + (aset floats 3 0.0) + (aset floats 4 w) + (aset floats 5 h) + (.writeBuffer (.-queue device) camera-buffer 0 floats)) + + +(defn draw-frame! [^js device ^js context text-sys editor-pool-info cmd-rect-sys camera-floats _ignored_pass_descriptor pan-x pan-y w h + & {:keys [cmd-panel-visible cmd-panel-h chrome-text-sys chrome-base-line-count + settings-line-count settings-visible settings-rect-sys + diagnostics-visible diagnostics-line-index agent-visible + editor-shadow-pool-info sidebar-shadow-pool-info sidebar-pool-info + dirty-rect render-target clear-quad frame-idx] + :or {cmd-panel-visible false cmd-panel-h 40 chrome-text-sys nil chrome-base-line-count 0 + settings-line-count 0 settings-visible false + settings-rect-sys nil agent-visible false + editor-shadow-pool-info nil sidebar-shadow-pool-info nil sidebar-pool-info nil + dirty-rect nil render-target nil clear-quad nil frame-idx 0}}] + (update-camera device (:camera-uniform-buffer text-sys) camera-floats pan-x pan-y 1.0 w h) + (when (and chrome-text-sys + (not= (:camera-uniform-buffer chrome-text-sys) (:camera-uniform-buffer text-sys))) + (update-camera device (:camera-uniform-buffer chrome-text-sys) camera-floats pan-x pan-y 1.0 w h)) + + (let [encoder (.createCommandEncoder device) + swap-texture (.getCurrentTexture context) + swap-view (.createView swap-texture) + ;; Phase 6E: always render to persistent target (survives swap chain double-buffering) + ;; dirty-rect non-nil → loadOp "load" + scissor + clear-quad (partial redraw) + ;; dirty-rect nil → loadOp "clear" (first frame, resize, text/font change) + use-rt? (some? render-target) + target-view (if use-rt? (:view render-target) swap-view) + partial? (and use-rt? dirty-rect) + load-op (if partial? "load" "clear") + + pass-descriptor (clj->js + {:colorAttachments [{:view target-view + :clearValue {:r 0.0 :g 0.0 :b 0.0 :a 1.0} + :loadOp load-op + :storeOp "store"}]}) + + pass (.beginRenderPass encoder pass-descriptor)] + + (when (<= frame-idx 5) + (let [canvas (.-canvas context) + rect (when canvas (.getBoundingClientRect canvas))] + (js/console.log "[RENDER/PRESENT]" + (str "{\"frame\":" frame-idx + ",\"canvasWidth\":" (or (some-> canvas .-width) -1) + ",\"canvasHeight\":" (or (some-> canvas .-height) -1) + ",\"clientWidth\":" (or (some-> canvas .-clientWidth) -1) + ",\"clientHeight\":" (or (some-> canvas .-clientHeight) -1) + ",\"rectWidth\":" (or (some-> rect .-width) -1) + ",\"rectHeight\":" (or (some-> rect .-height) -1) + ",\"swapWidth\":" (or (some-> swap-texture .-width) -1) + ",\"swapHeight\":" (or (some-> swap-texture .-height) -1) + ",\"useRenderTarget\":" (if use-rt? "true" "false") + ",\"contentInstances\":" (:num-instances text-sys) + ",\"chromeInstances\":" (or (:num-instances chrome-text-sys) 0) + "}")))) + + ;; Phase 6E: scissor + clear-quad for partial redraw + (when (and partial? clear-quad) + (let [{:keys [x y]} dirty-rect + dw (:w dirty-rect) + dh (:h dirty-rect)] + (.setScissorRect pass (int x) (int y) (int (max 1 dw)) (int (max 1 dh))) + (.setPipeline pass (:pipeline clear-quad)) + (.draw pass 3))) + + ;; Draw shadows FIRST (behind everything) — per-source pools (Phase 6C) + (when (and editor-shadow-pool-info (> (:draw-count editor-shadow-pool-info) 0)) + (.setPipeline pass (:pipeline editor-shadow-pool-info)) + (.setBindGroup pass 0 (:bind-group editor-shadow-pool-info)) + (.setVertexBuffer pass 0 (:buffer editor-shadow-pool-info)) + (.draw pass 6 (:draw-count editor-shadow-pool-info))) + (when (and sidebar-shadow-pool-info (> (:draw-count sidebar-shadow-pool-info) 0)) + (.setPipeline pass (:pipeline sidebar-shadow-pool-info)) + (.setBindGroup pass 0 (:bind-group sidebar-shadow-pool-info)) + (.setVertexBuffer pass 0 (:buffer sidebar-shadow-pool-info)) + (.draw pass 6 (:draw-count sidebar-shadow-pool-info))) + + ;; Draw sidebar pool (behind editor content, uses differential buffer) + (when (and sidebar-pool-info (> (:draw-count sidebar-pool-info) 0)) + (.setPipeline pass (:pipeline sidebar-pool-info)) + (.setBindGroup pass 0 (:bind-group sidebar-pool-info)) + (.setVertexBuffer pass 0 (:buffer sidebar-pool-info)) + (.draw pass 6 (:draw-count sidebar-pool-info))) + + ;; Draw editor rects (differential pool — selection, brackets, fold indicators, caret, eval) + (when (and editor-pool-info (> (:draw-count editor-pool-info) 0)) + (.setPipeline pass (:pipeline editor-pool-info)) + (.setBindGroup pass 0 (:bind-group editor-pool-info)) + (.setVertexBuffer pass 0 (:buffer editor-pool-info)) + (.draw pass 6 (:draw-count editor-pool-info))) + + ;; Draw content text (editor + sidebar — all instances, viewport culled at data level) + (when (and text-sys (> (:num-instances text-sys) 0)) + (.setPipeline pass (:pipeline text-sys)) + (.setBindGroup pass 0 (:bind-group text-sys)) + (.setVertexBuffer pass 0 (:instance-buffer text-sys)) + (.draw pass 6 (:num-instances text-sys) 0 0)) + + (let [chrome-ready? (and chrome-text-sys (> (:num-instances chrome-text-sys) 0)) + chrome-offsets (:line-offsets chrome-text-sys) + chrome-lines (when chrome-offsets (count chrome-offsets)) + chrome-base chrome-base-line-count] + + ;; Agent output background (instance 0) — draw behind chrome text + (when (and agent-visible cmd-rect-sys (>= (:num-instances cmd-rect-sys) 1)) + (.setPipeline pass (:pipeline cmd-rect-sys)) + (.setBindGroup pass 0 (:bind-group cmd-rect-sys)) + (.setVertexBuffer pass 0 (:instance-buffer cmd-rect-sys)) + (.draw pass 6 1 0 0)) + + ;; Command panel background (instance 1) + (when (and cmd-panel-visible chrome-ready? + cmd-rect-sys (>= (:num-instances cmd-rect-sys) 2)) + (.setPipeline pass (:pipeline cmd-rect-sys)) + (.setBindGroup pass 0 (:bind-group cmd-rect-sys)) + (.setVertexBuffer pass 0 (:instance-buffer cmd-rect-sys)) + (.draw pass 6 1 0 1)) + + ;; Chrome base text (cmd + agent + status — before settings bg) + (when chrome-ready? + (let [base-end (if (and chrome-offsets (< chrome-base chrome-lines)) + (nth chrome-offsets chrome-base) + (:num-instances chrome-text-sys))] + (when (> base-end 0) + (.setPipeline pass (:pipeline chrome-text-sys)) + (.setBindGroup pass 0 (:bind-group chrome-text-sys)) + (.setVertexBuffer pass 0 (:instance-buffer chrome-text-sys)) + (.draw pass 6 base-end 0 0)))) + + ;; Caret (instance 2) + (when (and cmd-panel-visible chrome-ready? + cmd-rect-sys (>= (:num-instances cmd-rect-sys) 3)) + (.setPipeline pass (:pipeline cmd-rect-sys)) + (.setBindGroup pass 0 (:bind-group cmd-rect-sys)) + (.setVertexBuffer pass 0 (:instance-buffer cmd-rect-sys)) + (.draw pass 6 1 0 2)) + + ;; Status bar background (instance 3) — always visible + (when (and cmd-rect-sys (>= (:num-instances cmd-rect-sys) 4)) + (.setPipeline pass (:pipeline cmd-rect-sys)) + (.setBindGroup pass 0 (:bind-group cmd-rect-sys)) + (.setVertexBuffer pass 0 (:instance-buffer cmd-rect-sys)) + (.draw pass 6 1 0 3)) + + ;; Settings panel: draw on top of everything when visible + (when settings-visible + (when (and settings-rect-sys (> (:num-instances settings-rect-sys) 0)) + (.setPipeline pass (:pipeline settings-rect-sys)) + (.setBindGroup pass 0 (:bind-group settings-rect-sys)) + (.setVertexBuffer pass 0 (:instance-buffer settings-rect-sys)) + (.draw pass 6 (:num-instances settings-rect-sys))) + + ;; Settings text (from chrome buffer, after base chrome lines) + (when (and chrome-ready? (pos? settings-line-count) chrome-offsets) + (let [settings-start-line chrome-base + settings-end-line (+ settings-start-line settings-line-count) + settings-start-inst (if (< settings-start-line chrome-lines) + (nth chrome-offsets settings-start-line) + (:num-instances chrome-text-sys)) + settings-end-inst (if (< settings-end-line chrome-lines) + (nth chrome-offsets settings-end-line) + (:num-instances chrome-text-sys)) + settings-draw-count (- settings-end-inst settings-start-inst)] + (when (> settings-draw-count 0) + (.setPipeline pass (:pipeline chrome-text-sys)) + (.setBindGroup pass 0 (:bind-group chrome-text-sys)) + (.setVertexBuffer pass 0 (:instance-buffer chrome-text-sys)) + (.draw pass 6 settings-draw-count 0 settings-start-inst))))) + + ;; Diagnostics overlay + (when (and diagnostics-visible (not cmd-panel-visible) (not settings-visible) + diagnostics-line-index chrome-ready? chrome-offsets) + (when (< diagnostics-line-index chrome-lines) + (let [start-inst (nth chrome-offsets diagnostics-line-index) + next-line (inc diagnostics-line-index) + end-inst (if (< next-line chrome-lines) + (nth chrome-offsets next-line) + (:num-instances chrome-text-sys)) + draw-count (- end-inst start-inst)] + (when (> draw-count 0) + (.setPipeline pass (:pipeline chrome-text-sys)) + (.setBindGroup pass 0 (:bind-group chrome-text-sys)) + (.setVertexBuffer pass 0 (:instance-buffer chrome-text-sys)) + (.draw pass 6 draw-count 0 start-inst)))))) + + (.end pass) + + ;; Phase 6E: copy persistent render target → swap chain for presentation + (when use-rt? + (let [rt-tex (:texture render-target)] + (.copyTextureToTexture encoder + (clj->js {:texture rt-tex}) + (clj->js {:texture swap-texture}) + (clj->js {:width (:width render-target) + :height (:height render-target)})))) + + (.submit (.-queue device) #js [(.finish encoder)]))) diff --git a/src/app/client/workflows/dg_flow.cljs b/src/app/client/workflows/dg_flow.cljs new file mode 100644 index 0000000..c25871c --- /dev/null +++ b/src/app/client/workflows/dg_flow.cljs @@ -0,0 +1,1140 @@ +(ns app.client.workflows.dg-flow + "DG workflow: flow state machine, intake tree, run/review trees, ticket layout." + (:require [clojure.string :as str] + [cljs.reader :as reader] + [app.client.workspace.rect-tree :refer [rt-node wrap-line resolve-layout tree->rects tree->shadows tree->text-ops]] + [app.client.workspace.ui-primitives :as ui + :refer [dt typo-title typo-subtitle typo-body typo-caption + list-row-h list-group-header-h list-left-pane-pct + list-padding-x list-item-inset list-padding-top + list-checkbox-size list-divider-w list-group-gap list-footer-h + ui-card ui-badge ui-button ui-divider ui-panel + ui-panel-header ui-panel-content ui-panel-footer + ui-panel-group ui-list-item ui-checkbox ui-priority-dot + ui-scrollbar build-empty-state]] + [app.client.workspace.trail :refer [trail->chat-nodes]])) + +;; ============================================================================ +;; FLOW STATE MACHINE (V0 — "Prompts as API Calls") +;; ============================================================================ +;; Encodes the state graph from commission-consensus.md Section 4. +;; Pure functions — no atoms, no side effects. + +(def flow-transitions + "Directed graph of valid state transitions. + Keys are from-states, values are sets of reachable to-states. + NOTE: :idle is a pre-graph state (not in Section 4's locked graph). + Arrangement is an intake subphase (no :arrange node). + Run is triggered directly from :intake when batch has lanes." + {:idle #{:bootstrapping} + :bootstrapping #{:intake :bootstrapping} ;; retry on failure + :intake #{:run :bootstrapping} ;; run directly from intake + :run #{:review :intake} ;; back-edge: scope change + :review #{:rework :finalize :intake} ;; back-edge: re-scope + :rework #{:review :intake} ;; back-edge: re-scope + :finalize #{:intake}}) + +(defn valid-transition? + "Check if moving from `from` to `to` is allowed. + Human override: any state can jump to :intake." + [from to] + (or (contains? (get flow-transitions from) to) + (= to :intake))) + +(defn initial-flow-state + "Fresh flow state for a new session. + Starts in :idle — ticket list only appears after /bootstrap (/dg). + Object model: batch > lanes > runs > artifacts > decisions." + [] + {:node :idle + :tickets [] ;; all fetched tickets + :batch {:lanes [] ;; ordered vec of ticket indices (execution stack) + :id nil} ;; batch identifier + :active-lane-idx 0 ;; focused lane in the stack + :runs {} ;; {lane-idx -> [{:id :status :trail :artifacts}]} + :decisions {} ;; {lane-idx -> :approve/:rework/:defer/:finalize} + :session-id nil + :history [] + ;; Legacy compat — kept during transition, will be removed + :selected [] + :arrangement nil}) + +(defn set-selection + "Update flow-state with new selection, syncing batch lanes and clamping active-lane-idx." + [flow-state new-sel] + (let [n (count new-sel) + ai (or (:active-lane-idx flow-state) 0) + clamped-ai (if (pos? n) (min ai (dec n)) 0)] + (-> flow-state + (assoc :selected new-sel) + (assoc-in [:batch :lanes] new-sel) + (assoc :active-lane-idx clamped-ai)))) + +(defn transition-flow-state + "Attempt to transition flow state. Returns updated state or nil if invalid. + Appends previous node to :history for auditability." + [flow-state to-node & [extra-merge]] + (let [from (:node flow-state)] + (when (valid-transition? from to-node) + (cond-> (assoc flow-state + :node to-node + :history (conj (:history flow-state) from)) + extra-merge (merge extra-merge))))) + +(defn flow-canvas-active? + "True when the flow state machine is in a node that shows the master-detail + layout instead of the code editor. All active flow states use this view — + left pane is always the map, right pane is always the current artifact." + [flow-state] + (contains? #{:intake :run :review :rework :finalize} (:node flow-state))) + +(defn parse-dg-command + "Parse DG workflow commands. Returns nil when the command does not belong to + the DG workflow." + [trimmed] + (cond + (= trimmed "/bootstrap") + {:kind :flow-bootstrap} + + (= trimmed "/mock") + {:kind :flow-mock-bootstrap} + + (str/starts-with? trimmed "/select ") + (let [args (-> trimmed (subs (count "/select ")) str/trim (str/split #"\s+")) + indices (try (mapv #(dec (js/parseInt % 10)) args) + (catch :default _ nil))] + (if (and (seq indices) (every? #(and (number? %) (not (js/isNaN %))) indices)) + {:kind :flow-select :indices indices} + {:kind :error :message "Usage: /select 1 2 3 (1-based ticket numbers)"})) + + (= trimmed "/run") + {:kind :flow-run} + + (= trimmed "/review") + {:kind :flow-review} + + (str/starts-with? trimmed "/rework ") + {:kind :flow-rework :comment (-> trimmed (subs (count "/rework ")) str/trim)} + + (= trimmed "/rework") + {:kind :error :message "Usage: /rework "} + + (= trimmed "/finalize") + {:kind :flow-finalize} + + (= trimmed "/status") + {:kind :flow-status} + + (= trimmed "/reset") + {:kind :flow-reset} + + :else + nil)) + +(def mock-tickets + [{:id "DIS-101" :title "Fix SSO auth flow for enterprise users" + :status "In Progress" :assignee "Siddharth" :priority 1 + :description "Enterprise SSO login fails when SAML assertion contains multiple group claims. Need to handle array-valued attributes in the assertion parser."} + {:id "DIS-102" :title "Add batch export for reasoning trails" + :status "Todo" :assignee "Siddharth" :priority 2 + :description "Users want to export multiple reasoning trails as a single PDF or markdown bundle for offline review and sharing with stakeholders."} + {:id "DIS-103" :title "WebGPU text rendering perf regression" + :status "In Progress" :assignee "Siddharth" :priority 1 + :description "After adding MSDF font atlas, frame times spiked from 2ms to 8ms on large files. Suspect redundant texture uploads per frame."} + {:id "DIS-104" :title "Design review screen diff viewer" + :status "Backlog" :assignee "unassigned" :priority 3 + :description "Screen 4 needs a side-by-side diff viewer for code changes produced by agent runs. Should support syntax highlighting and inline comments."} + {:id "DIS-105" :title "Implement parallel lane arrangement" + :status "Todo" :assignee "unassigned" :priority 2 + :description "The arrange step currently only supports sequential chains. Add parallel lane layout so independent tickets can run concurrently."} + {:id "DIS-106" :title "Add keyboard navigation to ticket list" + :status "Backlog" :assignee "unassigned" :priority 4 + :description "j/k to move selection, space to toggle, enter to view detail. Vim-style navigation for the intake screen ticket list."} + {:id "DIS-107" :title "Streaming token counter in agent panel" + :status "Done" :assignee "Siddharth" :priority 3 + :description "Show a live token count in the agent output panel header during streaming. Helps users gauge cost and progress of long-running agent sessions."} + {:id "DIS-108" :title "Fix scroll clamping on window resize" + :status "In Progress" :assignee "Siddharth" :priority 2 + :description "When the browser window is resized smaller, scroll position can exceed content bounds. Need to re-clamp scroll-y in the resize handler."} + {:id "DIS-109" :title "Rama PState schema migration for trails" + :status "Todo" :assignee "unassigned" :priority 2 + :description "The reasoning trail PState needs a schema evolution to support the new structured tool-use events. Plan the migration path."} + {:id "DIS-110" :title "Release v0.2.0 milestone" + :status "Released" :assignee "Siddharth" :priority 1 + :description "Tag and release the v0.2.0 milestone including streaming agent output, rect tree UI, and the intake list view."}]) + +(defn handle-dg-command! + "Apply DG workflow side effects through the provided runtime env. + Returns true when the command was handled." + [parsed {:keys [!flow-state !scroll-y !agent-output show-flow-info! fire-flow-run! + enter-workflow! exit-workflow!]}] + (case (:kind parsed) + :flow-bootstrap + (do + (let [flow @!flow-state + next (or (transition-flow-state flow :bootstrapping) + (when (= (:node flow) :intake) + (transition-flow-state flow :bootstrapping)))] + (if next + (do + (reset! !flow-state next) + (when enter-workflow! (enter-workflow!)) + (show-flow-info! "Fetching tickets from Linear...") + (-> (js/fetch "/api/linear/issues?team=Engineering") + (.then (fn [resp] (.text resp))) + (.then (fn [text] + (let [data (reader/read-string text) + tickets (:tickets data)] + (js/console.log "[FLOW][BOOTSTRAP]" + (clj->js {:ok (:ok data) + :ticket-count (count tickets)})) + (if (and (:ok data) (seq tickets)) + (do + (swap! !flow-state assoc :node :intake :tickets tickets) + (reset! !scroll-y 0) + (show-flow-info! + (str "Bootstrap complete. " (count tickets) " tickets loaded."))) + (do + (swap! !flow-state assoc :node :bootstrapping) + (show-flow-info! + (str "Bootstrap failed: " (or (:error data) "no tickets found") + "\nUse /bootstrap to retry."))))))) + (.catch (fn [err] + (swap! !flow-state assoc :node :bootstrapping) + (show-flow-info! + (str "Bootstrap fetch error: " (.-message err) + "\nUse /bootstrap to retry.")))))) + (show-flow-info! + (str "Cannot bootstrap from state: " (name (:node flow)) + "\nUse /reset to return to idle.")))) + true) + + :flow-mock-bootstrap + (do + (let [flow @!flow-state + can-transition (or (= (:node flow) :idle) + (= (:node flow) :intake))] + (if can-transition + (do + (reset! !flow-state (assoc (initial-flow-state) + :node :intake + :tickets mock-tickets)) + (when enter-workflow! (enter-workflow!)) + (show-flow-info! + (str "Mock bootstrap complete. " (count mock-tickets) " tickets loaded."))) + (show-flow-info! + (str "Cannot bootstrap from state: " (name (:node flow)) + "\nUse /reset to return to idle.")))) + true) + + :flow-select + (do + (let [flow @!flow-state + indices (:indices parsed) + tickets (:tickets flow)] + (if (not= (:node flow) :intake) + (show-flow-info! + (str "Cannot select tickets in state: " (name (:node flow)) + "\nMust be in :intake state.")) + (let [invalid (filter #(or (neg? %) (>= % (count tickets))) indices)] + (if (seq invalid) + (show-flow-info! + (str "Invalid ticket numbers: " + (str/join ", " (map inc invalid)) + "\nValid range: 1-" (count tickets))) + (do + (swap! !flow-state set-selection (vec indices)) + (show-flow-info! + (str "Selected " (count indices) " ticket(s):\n" + (str/join "\n" (map (fn [i] + (let [t (nth tickets i)] + (str " " (inc i) ". " (:id t) " — " (:title t)))) + indices)) + "\n\nReorder in the stack, then /run to start."))))))) + true) + + :flow-run + (do + (let [flow @!flow-state + lanes (get-in flow [:batch :lanes]) + selected (:selected flow)] + (cond + ;; No lanes or selected tickets + (and (empty? lanes) (empty? selected)) + (show-flow-info! "No tickets selected. Select tickets first, then /run.") + + :else + (let [;; Use batch lanes if set, fall back to selected for compat + effective-lanes (if (seq lanes) lanes selected) + next (transition-flow-state flow :run)] + (if next + (do + (reset! !flow-state (assoc next + :batch (assoc (:batch next) :lanes effective-lanes))) + (reset! !scroll-y 0) + (fire-flow-run! :run-sequential + :on-done (fn [] + (swap! !flow-state assoc :node :review) + (show-flow-info! + (str "Run complete. Now in review state.\n\n" + "Use /rework to request changes,\n" + "or /finalize to wrap up."))))) + (show-flow-info! + (str "Cannot run from state: " (name (:node flow)) + "\nMust be in :intake state.")))))) + true) + + :flow-review + (do + (let [flow @!flow-state + msg (str "Current state: " (name (:node flow)) + (when (= (:node flow) :review) + "\n\nOptions:\n /rework — request changes\n /finalize — wrap up batch")) + has-trail? (seq (:trail @!agent-output))] + (if has-trail? + (js/console.log "[FLOW][REVIEW]" msg) + (show-flow-info! msg))) + true) + + :flow-rework + (do + (let [flow @!flow-state + next (transition-flow-state flow :rework {:rework-comment (:comment parsed)})] + (if next + (do + (reset! !flow-state next) + (fire-flow-run! :rework + :on-done (fn [] + (swap! !flow-state assoc :node :review) + (show-flow-info! + (str "Rework complete. Back in review.\n\n" + "Use /rework for more changes,\n" + "or /finalize to wrap up."))))) + (show-flow-info! + (str "Cannot rework from state: " (name (:node flow)) + "\nExpected :review. Current: " (name (:node flow)))))) + true) + + :flow-finalize + (do + (let [flow @!flow-state + next (transition-flow-state flow :finalize)] + (if (or next (= (:node flow) :review)) + (do + (reset! !flow-state (or next + (assoc flow :node :finalize + :history (conj (:history flow) (:node flow))))) + (fire-flow-run! :finalize + :on-done (fn [] + (swap! !flow-state assoc + :node :intake + :selected [] + :arrangement nil + :batch {:lanes [] :id nil} + :active-lane-idx 0 + :runs {} + :decisions {}) + (reset! !scroll-y 0) + (show-flow-info! + (str "Batch finalized. Returned to intake.\n\n" + "Tickets still loaded. Use /select to start a new batch,\n" + "or /bootstrap to refresh tickets."))))) + (show-flow-info! + (str "Cannot finalize from state: " (name (:node flow)) + "\nExpected :review. Current: " (name (:node flow)))))) + true) + + :flow-status + (do + (let [flow @!flow-state + msg (str "=== Flow State ===\n" + "Node: " (name (:node flow)) "\n" + "Session: " (or (:session-id flow) "none") "\n" + "Tickets: " (count (:tickets flow)) "\n" + "Selected: " (if (seq (:selected flow)) + (str/join ", " (map inc (:selected flow))) + "none") "\n" + "Arrangement: " (or (some-> (:arrangement flow) name) "none") "\n" + "History: [" (str/join " -> " (map name (:history flow))) "]") + has-trail? (seq (:trail @!agent-output))] + (if has-trail? + (js/console.log "[FLOW][STATUS]" msg) + (show-flow-info! msg))) + true) + + :flow-reset + (do + (reset! !flow-state (initial-flow-state)) + (when exit-workflow! (exit-workflow!)) + (show-flow-info! "Flow state reset to idle.\nUse /bootstrap to start fresh.") + true) + + false)) + +(def priority-colors + "Priority level \u2192 RGBA color for the priority dot." + {1 {:r 0.95 :g 0.30 :b 0.30 :a 1.0} ;; urgent - red + 2 {:r 0.95 :g 0.60 :b 0.25 :a 1.0} ;; high - orange + 3 {:r 0.90 :g 0.80 :b 0.30 :a 1.0} ;; medium - yellow + 4 {:r 0.45 :g 0.85 :b 0.45 :a 1.0} ;; low - green + 0 {:r 0.55 :g 0.55 :b 0.60 :a 0.8}}) ;; none - gray + +(def status-group-order + ["Ready to Merge" "Ready for Review" "In Progress" "Todo" "Backlog" "Done" "Released"]) + +(def status-icons + {"Ready to Merge" "\u25CF" ;; \u25CF + "Ready for Review" "\u25CB" ;; \u25CB + "In Progress" "\u25D0" ;; \u25D0 + "Todo" "\u25CB" ;; \u25CB + "Backlog" "\u25C7" ;; \u25C7 + "Done" "\u2713" ;; \u2713 + "Released" "\u2713"}) ;; \u2713 + +(def status-icon-colors + {"Ready to Merge" {:r 0.45 :g 0.85 :b 0.45 :a 1.0} ;; green + "Ready for Review" {:r 0.55 :g 0.75 :b 0.95 :a 1.0} ;; blue + "In Progress" {:r 0.95 :g 0.75 :b 0.30 :a 1.0} ;; amber + "Todo" {:r 0.65 :g 0.65 :b 0.70 :a 0.8} ;; gray + "Backlog" {:r 0.55 :g 0.55 :b 0.60 :a 0.6} ;; dim gray + "Done" {:r 0.45 :g 0.85 :b 0.45 :a 0.7} ;; green dim + "Released" {:r 0.45 :g 0.85 :b 0.45 :a 0.5}}) ;; green dimmer + +(defn group-tickets-by-status + "Group tickets into ordered sections by status. + Returns [{:status \"Ready to Merge\" :icon \"\u25CF\" :count N + :tickets [{:idx 0 :ticket {...}} ...]} ...] + :idx is the 0-based index into the original flat tickets vector." + [tickets] + (let [indexed (mapv (fn [i t] {:idx i :ticket t}) (range) tickets) + known-set (set status-group-order) + known-groups (->> status-group-order + (mapv (fn [status] + (let [group-tix (filterv #(= (:status (:ticket %)) status) indexed)] + (when (seq group-tix) + {:status status + :icon (get status-icons status "?") + :count (count group-tix) + :tickets group-tix})))) + (filterv some?)) + unknown-tix (filterv #(not (known-set (:status (:ticket %)))) indexed) + unknown-groups (when (seq unknown-tix) + [{:status "Other" :icon "?" :count (count unknown-tix) :tickets unknown-tix}])] + (into known-groups unknown-groups))) + +(defn ticket-list-layout + "Compute layout entries for the grouped ticket list in CONTENT SPACE (no scroll). + Returns flat vec of {:type :group-header|:ticket-row ...} with :y positions. + collapsed-groups is a set of status strings." + [grouped-tickets collapsed-groups selected-set] + (loop [groups (seq grouped-tickets) + y list-padding-top + result []] + (if-not groups + result + (let [{:keys [status icon tickets] grp-count :count} (first groups) + collapsed? (contains? collapsed-groups status) + header {:type :group-header + :status status :icon icon :count grp-count + :y y :collapsed? collapsed?} + next-y (+ y list-group-header-h) + rows (if collapsed? + [] + (mapv (fn [i {:keys [idx ticket]}] + {:type :ticket-row + :idx idx :ticket ticket + :y (+ next-y (* i list-row-h)) + :selected? (contains? selected-set idx)}) + (range) tickets)) + total-rows-h (if collapsed? 0 (* (count tickets) list-row-h))] + (recur (next groups) + (+ next-y total-rows-h) + (into (conj result header) rows)))))) + +(defn list-content-height + "Total content height for the grouped ticket list (for scroll clamping). + Accounts for group gaps and padding." + [grouped-tickets collapsed-groups] + (let [n-groups (count grouped-tickets) + gaps (* (max 0 (dec n-groups)) list-group-gap)] + (+ list-padding-top gaps 4 ;; 4 = panel-content top padding + (reduce (fn [h {:keys [status tickets]}] + (+ h list-group-header-h + (if (contains? collapsed-groups status) 0 (* (count tickets) list-row-h)))) + 0 + grouped-tickets)))) + +;; --- Drag state machine (IDLE → PENDING → DRAGGING → DROP/CLICK) ----------- + +(def drag-threshold-px 5) + +(defn drag-pending? + "True when drag state is :pending (mousedown happened, waiting for threshold)." + [drag-state] (= :pending (:phase drag-state))) + +(defn drag-active? + "True when drag state is :dragging (past threshold, item follows cursor)." + [drag-state] (= :dragging (:phase drag-state))) + +(defn drag-distance + "Euclidean distance from drag origin to point (px, py)." + [{:keys [origin]} px py] + (let [dx (- px (:x origin)) + dy (- py (:y origin))] + (Math/sqrt (+ (* dx dx) (* dy dy))))) + +;; ============================================================================ +;; INTAKE TREE BUILDER (rect tree for Screen 1) +;; ============================================================================ + +(defn build-right-detail + "Build the right pane content for the intake right panel. + Returns a vector of rt-node children. + 4 states: empty tickets, no selection, single selection, execution stack." + [tickets selected right-w viewport-h font-size char-advance active-lane-idx detail-scroll-y] + (let [right-inner-w (- right-w (* 2 list-padding-x)) + detail-max-chars (if (pos? char-advance) + (max 20 (int (/ right-inner-w char-advance))) + 60) + cy (/ viewport-h 2)] + (cond + ;; No tickets loaded — centered empty state + (empty? tickets) + [(build-empty-state :empty-tickets right-w viewport-h + {:icon "{}" + :headline "No tickets" + :description "Run /bootstrap to fetch tickets from Linear."})] + + ;; Nothing selected — centered empty state + (empty? selected) + [(build-empty-state :no-selection right-w viewport-h + {:icon "<>" + :headline "Select a ticket" + :description "Click a ticket from the list to view its details."})] + + ;; Single ticket selected — rich detail view + (= 1 (count selected)) + (let [idx (first selected) + tkt (nth tickets idx nil) + ttitle (or (:title tkt) "Untitled") + tstatus (or (:status tkt) "?") + tprio (or (:priority tkt) 0) + tassn (or (:assignee tkt) "unassigned") + tdesc (or (:description tkt) "") + card-x (:lg (:spacing dt)) + card-y 36 + card-w (- right-w (* 2 (:lg (:spacing dt)))) + card-h (max 180 (- viewport-h 72)) + content-w (- card-w (* 2 list-padding-x)) + title-size (max (+ font-size 6) (:size typo-title)) + meta-size (max font-size (:size typo-body)) + body-size (max (+ font-size 4) (:size typo-subtitle)) + hint-size (max (- body-size 4) (:size typo-caption)) + font-scale (if (pos? font-size) (/ char-advance font-size) 0.56) + body-char-advance (* body-size font-scale) + plabel (case tprio 1 "Urgent" 2 "High" 3 "Medium" 4 "Low" "None") + mline (str tstatus " | " plabel " | " tassn) + title-max-chars (if (pos? body-char-advance) + (max 20 (int (/ content-w body-char-advance))) + detail-max-chars) + tlines (vec (mapcat #(wrap-line % title-max-chars) + (str/split-lines (or ttitle "")))) + tlh (max 20 (+ title-size 6)) + hdr-y 28 + meta-y (+ hdr-y (* (count tlines) tlh) 4) + divider-y (+ meta-y 18) + section-y (+ divider-y 22) + footer-y (- card-h 18) + desc-y (+ section-y 18) + desc-max-h (max 84 (- footer-y desc-y 18)) + desc-nodes (if (seq tdesc) + (trail->chat-nodes [{:kind :reasoning :text tdesc}] + content-w body-size body-char-advance 0.0 #{}) + [(build-empty-state :ticket-md-empty content-w desc-max-h + {:icon "md" + :headline "No description" + :description "This issue does not include a Linear description yet."})]) + desc-content-h (reduce + 0 (map #(get-in % [:bounds :h] 0) desc-nodes)) + desc-gap 8 + desc-stack-h (+ desc-content-h (* desc-gap (max 0 (dec (count desc-nodes))))) + overflow? (> desc-stack-h desc-max-h) + max-scroll (max 0 (- desc-stack-h desc-max-h)) + clamped-scroll (min (max detail-scroll-y 0) max-scroll) + htext (if overflow? + "Wheel to scroll description | Select more or Enter to run" + "Select more or Enter to run") + title-ops (mapv (fn [i l] + {:text l :type :text + :from 0 :to (count l) + :x list-padding-x :y (+ hdr-y (* i tlh)) + :size title-size + :r 0.85 :g 0.85 :b 0.88 :a (:a typo-title)}) + (range) tlines) + static-ops [{:text mline :type :comment + :from 0 :to (count mline) + :x list-padding-x :y meta-y + :size meta-size + :r 0.55 :g 0.55 :b 0.60 :a (:a typo-caption)} + {:text "Description" :type :keyword + :from 0 :to 11 + :x list-padding-x :y section-y + :size hint-size + :r 0.58 :g 0.68 :b 0.90 :a 0.95} + {:text htext :type :comment + :from 0 :to (count htext) + :x list-padding-x :y footer-y + :size hint-size + :r 0.40 :g 0.40 :b 0.45 :a 0.5}] + all-ops (into title-ops static-ops)] + [(ui-card :detail-card + {:x card-x :y card-y :w card-w :h card-h} + :shadow :md + :children [(ui-divider :detail-sep + {:x (:md (:spacing dt)) :y divider-y + :w (- card-w (* 2 (:md (:spacing dt)))) :h 1}) + (rt-node :detail-md-clip :detail-md-clip + {:x list-padding-x :y desc-y :w content-w :h desc-max-h} + :clip? true + :children [(rt-node :detail-md-stack :detail-md-stack + {:x 0 :y (- clamped-scroll) :w content-w :h (max desc-max-h desc-stack-h)} + :layout {:direction :column :gap desc-gap} + :children desc-nodes)])]) + (rt-node :detail-content :text-block + {:x card-x :y card-y :w card-w :h card-h} + :text all-ops)]) + + ;; Multi-selection — execution stack (ordered batch view) + :else + (let [n (count selected) + hdr "EXECUTION ORDER" + badge (str "[" n "]") + row-h 36 + stack-top 52 + hint-y (+ stack-top (* n row-h) 20) + htxt "Enter: run | Shift+\u2191\u2193: reorder | Del: remove" + ;; Stack item text ops — numbered list with focus highlight + active-idx (or active-lane-idx 0) + stack-ops + (into + ;; Header + [{:text hdr :type :macro + :from 0 :to (count hdr) + :x list-padding-x :y 28 + :size (:size typo-caption) + :r 0.55 :g 0.60 :b 0.70 :a 1.0} + {:text badge :type :comment + :from 0 :to (count badge) + :x (+ list-padding-x (* (count hdr) char-advance) 8) :y 28 + :size (:size typo-caption) + :r 0.40 :g 0.55 :b 0.80 :a 1.0} + ;; Hint footer + {:text htxt :type :comment + :from 0 :to (count htxt) + :x list-padding-x :y hint-y + :size (:size typo-caption) + :r 0.40 :g 0.40 :b 0.45 :a 0.5}] + ;; Stack items + (mapcat + (fn [i si] + (let [tkt (nth tickets si nil) + t (or (:title tkt) "Untitled") + tid (or (:id tkt) "?") + label (str (inc i) ". " tid " — " t) + tr (if (> (count label) detail-max-chars) + (str (subs label 0 (- detail-max-chars 2)) "..") + label) + y (+ stack-top (* i row-h)) + prio (or (:priority tkt) 0) + pc (get priority-colors prio {:r 0.55 :g 0.55 :b 0.60 :a 0.8})] + [{:text tr :type :text + :from 0 :to (count tr) + :x (+ list-padding-x 12) :y (+ y 14) + :size (:size typo-body) + :r 0.80 :g 0.82 :b 0.88 :a (:a typo-body)}])) + (range) selected)) + ;; Stack item background rects with focus highlight on active lane + stack-rects + (mapv (fn [i si] + (let [y (+ stack-top (* i row-h)) + focused? (= i active-idx) + prio (or (:priority (nth tickets si nil)) 0) + pc (get priority-colors prio {:r 0.55 :g 0.55 :b 0.60 :a 0.8}) + bg (if focused? + {:r 0.15 :g 0.25 :b 0.40 :a 0.8} + {:r 0.16 :g 0.18 :b 0.22 :a 0.6})] + (rt-node (keyword (str "stack-item-" i)) :stack-item + {:x 8 :y y :w (- right-w 16) :h (- row-h 4)} + :style bg + :data {:lane-idx i :ticket-idx si} + :children [(rt-node (keyword (str "prio-dot-" i)) :decoration + {:x 4 :y 10 :w 6 :h 6} + :style {:r (:r pc) :g (:g pc) :b (:b pc) :a (:a pc)})]))) + (range) selected)] + (into stack-rects + [(rt-node :stack-text :text-block + {:x 0 :y 0 :w right-w :h viewport-h} + :text stack-ops)]))))) + +(defn build-intake-tree + "Build the Screen 1 intake scene graph. Returns an rt-node tree. + Walk with tree->rects for GPU rects, tree->text-ops for text. + All fixed elements (backgrounds, header, right pane) compensate for + scroll-y so the global camera can scroll content items. + drag-state: {:phase :idle|:pending|:dragging, :node ..., :current {:x :y}}" + [flow-state viewport-w viewport-h scroll-y detail-scroll-y hovered-row-idx collapsed-groups + font-size char-advance drag-state] + (let [tickets (:tickets flow-state) + selected (:selected flow-state) + selected-set (set selected) + grouped (group-tickets-by-status tickets) + left-w (int (* viewport-w list-left-pane-pct)) + right-x0 (+ left-w list-divider-w) + right-w (- viewport-w left-w list-divider-w) + sy scroll-y + list-font (max font-size (:sm (:font-sizes dt))) + group-font (max (- font-size 1) (:sm (:font-sizes dt))) + meta-font (max (- font-size 2) (:xs (:font-sizes dt))) + + ;; === DRAG STATE === + dragging? (drag-active? (or drag-state {:phase :idle})) + dragged-idx (when dragging? (:idx (:data (:node drag-state)))) + drag-cur (when dragging? (:current drag-state)) + ;; Is cursor over right pane? (drop zone highlight) + drag-over-right? (and dragging? drag-cur (>= (:x drag-cur) left-w)) + + ;; === FIXED CHROME (scroll-compensated, using design tokens) === + left-bg (rt-node :left-bg :bg + {:x 0 :y sy :w left-w :h viewport-h} + :style {:bg [0.0 0.0 0.0 1.0]}) + right-bg (rt-node :right-bg :bg + {:x right-x0 :y sy :w right-w :h viewport-h} + :style {:bg (if drag-over-right? + (:accent-muted (:colors dt)) + [0.0 0.0 0.0 1.0])}) + header-str (str "DISCOURSE-GRAPH " (count tickets) " active") + divider (rt-node :divider :chrome + {:x left-w :y sy :w list-divider-w :h viewport-h} + :style {:bg (:border (:colors dt))}) + + ;; === LEFT PANEL (composable components + layout engine) === + group-nodes + (vec (map-indexed + (fn [gi {:keys [status icon tickets] grp-count :count}] + (let [collapsed? (contains? collapsed-groups status) + ic (get status-icon-colors status {:r 0.6 :g 0.6 :b 0.6 :a 0.8}) + item-nodes + (mapv (fn [{:keys [idx ticket]}] + (let [sel? (contains? selected-set idx) + hov? (= idx hovered-row-idx) + ghost? (= idx dragged-idx) + prio (or (:priority ticket) 0) + leading (ui-checkbox (keyword (str "cb-" idx)) + :checked? sel?) + trailing (when (<= 1 prio 2) + (ui-priority-dot (keyword (str "pd-" idx)) + prio :parent-w left-w))] + (ui-list-item (keyword (str "t-" idx)) left-w + :title (or (:title ticket) "Untitled") + :leading leading + :trailing trailing + :selected? sel? + :hovered? hov? + :ghost? ghost? + :data {:idx idx} + :font-size list-font))) + tickets)] + (ui-panel-group (keyword (str "grp-" status)) left-w + :label (str status " " grp-count) + :status status + :icon icon + :icon-color ic + :font-size group-font + :collapsed? collapsed? + :first-group? (zero? gi) + :items item-nodes))) + grouped)) + + ;; Footer text + sel-count (count selected) + footer-txt (cond + (zero? (count tickets)) "/bootstrap to load" + (pos? sel-count) (str sel-count " selected") + :else "Click to select") + footer-hint (when (pos? sel-count) + "Enter to run | \u2191\u2193 reorder") + footer-fg (:fg-muted (:colors dt)) + footer-acc (:fg-subtle (:colors dt)) + content-h (- viewport-h list-padding-top list-footer-h) + + left-panel + (ui-panel :left-panel {:x 0 :y sy :w left-w :h viewport-h} + :style {:bg [0.0 0.0 0.0 1.0]} + :children + [(ui-panel-header :header left-w list-padding-top + :style {:bg [0.0 0.0 0.0 1.0]} + :text [{:text header-str :type :macro + :from 0 :to (count header-str) + :x list-padding-x + :y (+ (/ list-padding-top 2) (/ (:size typo-subtitle) 2.5)) + :size (:size typo-subtitle) + :r 0.75 :g 0.80 :b 0.95 :a (:a typo-subtitle)}]) + (ui-panel-content :linear-panel left-w content-h + :children group-nodes) + (ui-panel-footer :footer left-w list-footer-h + :style {:bg [0.0 0.0 0.0 1.0]} + :text (cond-> [{:text footer-txt :type :comment + :from 0 :to (count footer-txt) + :x list-padding-x :y 24 + :size (:xs (:font-sizes dt)) + :r (nth footer-fg 0) :g (nth footer-fg 1) + :b (nth footer-fg 2) :a (nth footer-fg 3)}] + footer-hint + (conj {:text footer-hint :type :comment + :from 0 :to (count footer-hint) + :x (+ list-padding-x + (* (count footer-txt) (* (:xs (:font-sizes dt)) 0.56)) + 12) + :y 24 + :size (:xs (:font-sizes dt)) + :r (nth footer-acc 0) :g (nth footer-acc 1) + :b (nth footer-acc 2) :a (nth footer-acc 3)})))]) + + ;; === RIGHT DETAIL PANE (delegated to build-right-detail) === + active-lane-idx (or (:active-lane-idx flow-state) 0) + right-children (build-right-detail tickets selected right-w viewport-h + font-size char-advance active-lane-idx detail-scroll-y) + right-detail (rt-node :right-detail :panel + {:x right-x0 :y sy :w right-w :h viewport-h} + :children (vec right-children)) + + ;; === DROP ZONE BORDER (visible when dragging over right pane) === + drop-accent (let [a (:accent (:colors dt))] (assoc a 3 0.7)) + drop-border (when drag-over-right? + [(rt-node :drop-top :chrome + {:x right-x0 :y sy :w right-w :h 2} + :style {:bg drop-accent}) + (rt-node :drop-bottom :chrome + {:x right-x0 :y (+ sy viewport-h -2) :w right-w :h 2} + :style {:bg drop-accent}) + (rt-node :drop-left :chrome + {:x right-x0 :y sy :w 2 :h viewport-h} + :style {:bg drop-accent}) + (rt-node :drop-right :chrome + {:x (+ right-x0 right-w -2) :y sy :w 2 :h viewport-h} + :style {:bg drop-accent})]) + + ;; === FLOATING TICKET (follows cursor during drag, renders on top) === + float-node + (when (and dragging? dragged-idx drag-cur) + (let [tkt (nth tickets dragged-idx nil)] + (when tkt + (let [ftitle (or (:title tkt) "Untitled") + ftrunc (if (> (count ftitle) 40) + (str (subs ftitle 0 38) "..") + ftitle) + fprio (or (:priority tkt) 0) + ;; Position in world space: cursor screen + scroll offset + fx (- (:x drag-cur) 20) + fy (+ (- (:y drag-cur) 12) sy) + fw (min 300 left-w)] + (rt-node :float-ticket :ticket-row + {:x fx :y fy :w fw :h list-row-h} + :style {:bg [0.18 0.22 0.35 0.95] + :radius (:md (:radii dt)) + :shadow {:blur 12 :offset-y 4 :color [0 0 0 0.4]}} + :text [{:text ftrunc :type :text + :from 0 :to (count ftrunc) + :x 10 :y 17 :size list-font + :r 0.90 :g 0.90 :b 0.95 :a 1.0} + {:text (str "P" fprio) :type :comment + :from 0 :to (+ 1 (count (str fprio))) + :x (- fw 36) :y 17 :size meta-font + :r (if (<= fprio 2) 0.95 0.55) + :g (if (<= fprio 2) 0.55 0.55) + :b (if (<= fprio 2) 0.30 0.60) + :a 0.9}]))))) + + base-children [left-bg right-bg left-panel divider right-detail]] + + ;; === ROOT === + (rt-node :intake-root :root + {:x 0 :y 0 :w viewport-w :h 100000} + :children (cond-> base-children + drop-border (into drop-border) + float-node (conj float-node))))) + +;; === Thin wrappers — drop-in replacements for the old compute fns === + +(defn offset-rects + "Shift all rect :x by dx. Used to push content right when sidebar visible." + [rects dx] + (if (zero? dx) + rects + (mapv #(update % :x + dx) rects))) + +(defn offset-shadows + "Shift all shadow :x by dx." + [shadows dx] + (if (zero? dx) + shadows + (mapv #(update % :x + dx) shadows))) + +(defn offset-text-ops + "Shift text ops :x by dx. Handles both nested [[op]] and flat [op] formats." + [ops dx] + (if (zero? dx) + ops + (mapv (fn [op] + (if (vector? op) + (mapv #(update % :x + dx) op) + (update op :x + dx))) + ops))) + +(defn compute-ticket-list-rects + "GPU rects + shadows for Screen 1 intake (delegates to rect tree). + Returns {:rects [...] :shadows [...]} for flow-canvas mode." + [flow-state viewport-w viewport-h scroll-y detail-scroll-y hovered-row-idx collapsed-groups drag-state + font-size char-advance] + (let [tree (resolve-layout + (build-intake-tree flow-state viewport-w viewport-h scroll-y detail-scroll-y + hovered-row-idx collapsed-groups font-size char-advance drag-state))] + {:rects (tree->rects tree) + :shadows (tree->shadows tree)})) + +(defn compute-ticket-list-text-ops + "Text ops for Screen 1 intake (delegates to rect tree)." + [flow-state viewport-w viewport-h font-size char-advance scroll-y detail-scroll-y + hovered-row-idx collapsed-groups drag-state] + (tree->text-ops + (resolve-layout + (build-intake-tree flow-state viewport-w viewport-h scroll-y detail-scroll-y + hovered-row-idx collapsed-groups font-size char-advance drag-state)))) + +;; ============================================================================ +;; PROMPT TEMPLATES (V0 Flow Actions) +;; ============================================================================ + +(defn flow-prompt + "Compose a prompt + optional system instruction for a flow action. + Returns {:prompt \"...\" :system-instruction nil}." + [action flow-state] + (case action + :bootstrap + {:prompt (str "List all active Linear tickets for the discourse-graph project. " + "Output ONLY a JSON array, no other text. Each object needs: " + "id, title, status, assignee, priority, description. " + "IMPORTANT: Copy the full description text verbatim from Linear — do NOT summarize or truncate it.") + :system-instruction nil} + + :run-sequential + (let [tickets (mapv #(nth (:tickets flow-state) %) (:selected flow-state)) + ticket-list (str/join "\n" (map-indexed + (fn [i t] + (str (inc i) ". " (:id t) " — " (:title t) + " [" (:status t) ", P" (:priority t) "]")) + tickets))] + {:prompt (str "Execute the following tickets SEQUENTIALLY. " + "Each ticket's output should feed into the next ticket's context.\n\n" + ticket-list "\n\n" + "For each ticket: create a worktree, implement the changes, " + "run tests, and report the result before moving to the next.") + :system-instruction nil}) + + :run-parallel + (let [tickets (mapv #(nth (:tickets flow-state) %) (:selected flow-state)) + ticket-list (str/join "\n" (map-indexed + (fn [i t] + (str (inc i) ". " (:id t) " — " (:title t) + " [" (:status t) ", P" (:priority t) "]")) + tickets))] + {:prompt (str "Execute the following tickets IN PARALLEL (independent worktrees). " + "Each ticket is independent — do not chain context between them.\n\n" + ticket-list "\n\n" + "For each ticket: create a separate worktree from main, " + "implement changes, run tests, and report results.") + :system-instruction nil}) + + :rework + {:prompt (str "Rework the previous implementation based on this feedback:\n\n" + (or (:rework-comment flow-state) "Please fix the issues found in review.") "\n\n" + "Apply fixes in the existing worktree(s) and re-run tests.") + :system-instruction nil} + + :finalize + {:prompt (str "Finalize the current batch of tickets. For each completed ticket:\n" + "1. Generate a PR description summarizing the changes\n" + "2. List any remaining TODOs or known issues\n" + "3. Confirm test status\n\n" + "Return a summary of all finalized work.") + :system-instruction nil} + + ;; Fallback — shouldn't happen if callers validate + {:prompt (str "Unknown flow action: " action) + :system-instruction nil})) + +;; ============================================================================ +;; RUN / REVIEW TREE BUILDER +;; ============================================================================ + +(def lane-status-colors + {:queued {:r 0.50 :g 0.50 :b 0.55 :a 0.8} + :running {:r 0.40 :g 0.70 :b 1.00 :a 1.0} + :done {:r 0.40 :g 0.85 :b 0.45 :a 1.0} + :failed {:r 0.95 :g 0.40 :b 0.40 :a 1.0}}) + +(def lane-status-labels + {:queued "QUEUED" :running "RUNNING" :done "DONE" :failed "FAILED"}) + +(defn build-run-tree + "Build the run/review scene graph. Same master-detail layout as intake. + Left pane: batch map with lane statuses. Right pane: trail stream. + mode: :run (live streaming) or :review (static, completed). + agent-output: the current agent output map with :trail, :status, etc. + shimmer-alpha: pulse for pending trail cards. + collapsed: set of collapsed trail block ids. + trail-scroll-y: scroll offset for the right pane trail content." + [flow-state viewport-w viewport-h scroll-y agent-output + font-size char-advance shimmer-alpha collapsed trail-scroll-y] + (let [tickets (:tickets flow-state) + selected (:selected flow-state) + mode (:node flow-state) + sy scroll-y ;; scroll compensation for fixed chrome + left-w (int (* viewport-w list-left-pane-pct)) + right-x0 (+ left-w list-divider-w) + right-w (- viewport-w left-w list-divider-w) + colors (:colors dt) + pad (:lg (:spacing dt)) + line-h (+ font-size 4) + row-h 36 + header-h 40 + + ;; Determine per-lane status (V0: all lanes share one run status) + a-status (:status agent-output) + global-lane-status (cond + (= a-status :running) :running + (= a-status :complete) :done + (= a-status :failed) :failed + :else :queued) + banner (case mode + :run "RUNNING" + :review "REVIEW" + :rework "REWORKING" + "BATCH") + banner-c (case mode + :run {:r 0.40 :g 0.70 :b 1.00 :a 1.0} + :review {:r 0.40 :g 0.85 :b 0.45 :a 1.0} + :rework {:r 0.95 :g 0.70 :b 0.25 :a 1.0} + {:r 0.70 :g 0.70 :b 0.75 :a 1.0}) + + ;; === LEFT PANE: batch map with lane statuses === + lane-nodes + (mapv (fn [i si] + (let [tkt (nth tickets si nil) + t (or (:title tkt) "Untitled") + tid (or (:id tkt) "?") + sc (get lane-status-colors global-lane-status) + sl (get lane-status-labels global-lane-status "?")] + (rt-node (keyword (str "lane-" i)) :lane-item + {:x 8 :y (+ header-h 8 (* i row-h)) + :w (- left-w 16) :h (- row-h 4)} + :style {:r 0.14 :g 0.15 :b 0.18 :a 0.8} + :text [{:text (str (inc i) ". " tid) :type :keyword + :from 0 :to (+ 3 (count tid)) + :x 8 :y 22 :size (:size typo-body) + :r 0.75 :g 0.78 :b 0.85 :a 1.0} + {:text sl :type :comment + :from 0 :to (count sl) + :x (- left-w 80) :y 22 :size (:size typo-caption) + :r (:r sc) :g (:g sc) :b (:b sc) :a (:a sc)}]))) + (range) selected) + + footer-txt (case mode + :review "/rework | /finalize" + :rework "Reworking..." + "") + + left-panel + (rt-node :run-left :panel + {:x 0 :y sy :w left-w :h viewport-h} + :style {:bg [0.0 0.0 0.0 1.0]} + :text [{:text banner :type :macro + :from 0 :to (count banner) + :x list-padding-x :y 28 + :size (:size typo-subtitle) + :r (:r banner-c) :g (:g banner-c) :b (:b banner-c) :a (:a banner-c)} + {:text (str (count selected) " tickets") + :type :comment + :from 0 :to (+ (count (str (count selected))) 8) + :x (+ list-padding-x (* (count banner) char-advance) 12) :y 28 + :size (:size typo-caption) + :r 0.50 :g 0.50 :b 0.55 :a 0.8} + {:text footer-txt :type :comment + :from 0 :to (count footer-txt) + :x list-padding-x :y (- viewport-h 16) + :size (:size typo-caption) + :r 0.45 :g 0.45 :b 0.50 :a 0.6}] + :children lane-nodes) + + ;; === RIGHT PANE: trail stream === + trail (:trail agent-output) + chat-w right-w + + trail-children + (if (seq trail) + (let [block-nodes (trail->chat-nodes trail chat-w font-size char-advance + (or shimmer-alpha 0.4) (or collapsed #{}))] + block-nodes) + [(build-empty-state :run-empty right-w (- viewport-h header-h) + {:icon (if (= mode :review) "OK" "...") + :headline (if (= mode :review) "Run complete" "Waiting for output") + :description (if (= mode :review) + "Review the trail below. /rework or /finalize." + "Agent is processing your batch...")})]) + + trail-content-h (reduce + 0 (map #(get-in % [:bounds :h] 0) trail-children)) + trail-sy (or trail-scroll-y 0) + max-trail-scroll (max 0 (- trail-content-h viewport-h)) + trail-scroll-offset (min trail-sy max-trail-scroll) + + right-panel + (rt-node :run-right :panel + {:x right-x0 :y sy :w right-w :h viewport-h} + :style {:bg [0.0 0.0 0.0 1.0]} + :children + [(rt-node :run-right-body :panel-content + {:x 0 :y 0 :w right-w :h viewport-h} + :clip? true + :children + [(rt-node :run-trail-scroll :scroll-container + {:x 0 :y (- 4 trail-scroll-offset) :w right-w :h (+ trail-content-h 8)} + :layout {:direction :column :gap 4 :padding [0 0 0 0]} + :children trail-children)])]) + + divider (rt-node :run-divider :chrome + {:x left-w :y sy :w list-divider-w :h viewport-h} + :style {:bg (:border colors)})] + + (rt-node :run-root :scene + {:x 0 :y 0 :w viewport-w :h viewport-h} + :children [left-panel divider right-panel]))) + +(defn compute-run-rects + "GPU rects + shadows for run/review mode." + [flow-state viewport-w viewport-h scroll-y agent-output + font-size char-advance shimmer-alpha collapsed trail-scroll-y] + (let [tree (resolve-layout + (build-run-tree flow-state viewport-w viewport-h scroll-y + agent-output font-size char-advance + shimmer-alpha collapsed trail-scroll-y))] + {:rects (tree->rects tree) + :shadows (tree->shadows tree)})) + +(defn compute-run-text-ops + "Text ops for run/review mode." + [flow-state viewport-w viewport-h scroll-y agent-output + font-size char-advance shimmer-alpha collapsed trail-scroll-y] + (tree->text-ops + (resolve-layout + (build-run-tree flow-state viewport-w viewport-h scroll-y + agent-output font-size char-advance + shimmer-alpha collapsed trail-scroll-y)))) diff --git a/src/app/client/workflows/jit.cljs b/src/app/client/workflows/jit.cljs new file mode 100644 index 0000000..a73c748 --- /dev/null +++ b/src/app/client/workflows/jit.cljs @@ -0,0 +1,228 @@ +(ns app.client.workflows.jit + "JIT workflow: command parsing plus hardcoded preview artifacts." + (:require [clojure.string :as str] + [app.client.workspace.rect-tree :refer [rt-node]] + [app.client.workspace.ui-primitives :refer [dt]])) + +(defn parse-jit-command + "Parse JIT workflow commands. Returns nil when the command does not belong + to JIT." + [trimmed] + (cond + (str/starts-with? trimmed "/extract ") + (let [args (-> trimmed (subs (count "/extract ")) str/trim)] + (if (str/blank? args) + {:kind :error :message "Usage: /extract [url] [selector]\n /extract https://site.com .card\n /extract .card\n /extract clear"} + (if (= args "clear") + {:kind :extract-component :clear? true} + (let [parts (str/split args #"\s+" 2) + first-part (first parts)] + (if (str/starts-with? first-part "http") + {:kind :extract-component :url first-part :selector (second parts)} + {:kind :extract-component :selector args}))))) + + (= trimmed "/extract") + {:kind :extract-component} + + (str/starts-with? trimmed "/hardcode ") + {:kind :hardcode :name (-> trimmed (subs (count "/hardcode ")) str/trim)} + + (= trimmed "/hardcode") + {:kind :hardcode :name "button"} + + :else + nil)) + +(defn hardcoded-preview-node + "Return a hardcoded rt-node demo for the current JIT prototype." + [name] + (let [sc-primary [0.898 0.898 0.898 1.0] + sc-primary-fg [0.09 0.09 0.09 1.0] + sc-secondary [0.149 0.149 0.149 1.0] + sc-secondary-fg [0.98 0.98 0.98 1.0] + sc-destructive [1.0 0.392 0.404 1.0] + sc-foreground [0.98 0.98 0.98 1.0] + sc-fg-muted [0.831 0.831 0.831 1.0] + sc-muted [0.149 0.149 0.149 1.0] + sc-border [1.0 1.0 1.0 0.15] + sc-bg [0.039 0.039 0.039 1.0] + sc-radius 10 + sc-h 32 + sc-font 14 + make-btn (fn [id label bg-color fg-color & {:keys [border-w border-c]}] + (let [char-w (* sc-font 0.56) + text-w (* (count label) char-w) + btn-w (+ text-w 20)] + (rt-node id :button + {:x 0 :y 0 :w btn-w :h sc-h} + :style (cond-> {:bg bg-color + :radius sc-radius} + border-w (assoc :border-width border-w) + border-c (assoc :border-color border-c)) + :text [{:text label + :type :text + :from 0 + :to (count label) + :x 10 + :y (+ sc-font 5) + :size sc-font + :r (nth fg-color 0) + :g (nth fg-color 1) + :b (nth fg-color 2) + :a (nth fg-color 3)}]))) + section-label (fn [id text] + (rt-node id :text + {:x 0 :y 0 :w 200 :h 18} + :text [{:text text + :type :keyword + :from 0 + :to (count text) + :x 0 + :y 13 + :size 12 + :r 0.55 + :g 0.55 + :b 0.60 + :a 1.0}]))] + (case name + "button" + (rt-node :hc-button-demo :rect + {:x 0 :y 0 :w 520 :h 500} + :style {:bg sc-bg} + :layout {:direction :column :padding [24 24 24 24] :gap 12} + :children + [(rt-node :hc-title :text + {:x 0 :y 0 :w 472 :h 24} + :text [{:text "shadcn/ui Button (v4) -- extracted from live page" + :type :keyword :from 0 :to 49 :x 0 :y 17 + :size 15 :r 0.98 :g 0.98 :b 0.98 :a 1.0}]) + (section-label :hc-s1 "Variants") + (rt-node :hc-variant-row :rect + {:x 0 :y 0 :w 472 :h sc-h} + :style {:bg [0 0 0 0]} + :layout {:direction :row :gap 10} + :children + [(make-btn :hc-default "Default" sc-primary sc-primary-fg) + (make-btn :hc-secondary "Secondary" sc-secondary sc-secondary-fg) + (make-btn :hc-destructive "Destructive" + (assoc sc-destructive 3 0.2) sc-destructive) + (make-btn :hc-outline "Outline" + [1.0 1.0 1.0 0.04] sc-fg-muted + :border-w 1 :border-c sc-border) + (make-btn :hc-ghost "Ghost" [0 0 0 0] sc-fg-muted) + (make-btn :hc-link "Link" [0 0 0 0] [0.35 0.55 0.95 1.0])]) + (section-label :hc-s2 "Hover states (simulated)") + (rt-node :hc-hover-row :rect + {:x 0 :y 0 :w 472 :h sc-h} + :style {:bg [0 0 0 0]} + :layout {:direction :row :gap 10} + :children + [(make-btn :hc-hov-default "Default" + (assoc sc-primary 3 0.8) sc-primary-fg) + (make-btn :hc-hov-secondary "Secondary" + (assoc sc-secondary 3 0.8) sc-secondary-fg) + (make-btn :hc-hov-destructive "Destructive" + (assoc sc-destructive 3 0.3) sc-destructive) + (make-btn :hc-hov-outline "Outline" + sc-muted sc-foreground + :border-w 1 :border-c sc-border) + (make-btn :hc-hov-ghost "Ghost" sc-muted sc-foreground) + (make-btn :hc-hov-link "Link" [0 0 0 0] [0.35 0.55 0.95 1.0])]) + (section-label :hc-s3 "Sizes") + (rt-node :hc-size-row :rect + {:x 0 :y 0 :w 472 :h 48} + :style {:bg [0 0 0 0]} + :layout {:direction :row :gap 10 :align :end} + :children + [(let [f 12 ch (* f 0.56) l "Extra Small" w (+ (* (count l) ch) 16)] + (rt-node :hc-sz-xs :button + {:x 0 :y 0 :w w :h 24} + :style {:bg sc-primary :radius 8} + :text [{:text l :type :text :from 0 :to (count l) + :x 8 :y 17 :size f + :r 0.09 :g 0.09 :b 0.09 :a 1.0}])) + (let [f 12 ch (* f 0.56) l "Small" w (+ (* (count l) ch) 20)] + (rt-node :hc-sz-sm :button + {:x 0 :y 0 :w w :h 28} + :style {:bg sc-primary :radius 8} + :text [{:text l :type :text :from 0 :to (count l) + :x 10 :y 19 :size f + :r 0.09 :g 0.09 :b 0.09 :a 1.0}])) + (make-btn :hc-sz-md "Default" sc-primary sc-primary-fg) + (let [f 14 ch (* f 0.56) l "Large" w (+ (* (count l) ch) 28)] + (rt-node :hc-sz-lg :button + {:x 0 :y 0 :w w :h 40} + :style {:bg sc-primary :radius 10} + :text [{:text l :type :text :from 0 :to (count l) + :x 14 :y 26 :size f + :r 0.09 :g 0.09 :b 0.09 :a 1.0}]))]) + (section-label :hc-s4 "Token mapping: shadcn v4 -> Softland dt") + (rt-node :hc-token-info :text + {:x 0 :y 0 :w 472 :h 100} + :text [{:text "primary [229,229,229] -> dt :fg" :type :text + :from 0 :to 30 :x 0 :y 14 + :size 11 :r 0.55 :g 0.55 :b 0.60 :a 1.0} + {:text "secondary [38,38,38] -> dt :bg-muted" :type :text + :from 0 :to 35 :x 0 :y 28 + :size 11 :r 0.55 :g 0.55 :b 0.60 :a 1.0} + {:text "destructive [255,100,103] -> dt :destructive" :type :text + :from 0 :to 44 :x 0 :y 42 + :size 11 :r 0.55 :g 0.55 :b 0.60 :a 1.0} + {:text "background [10,10,10] -> dt :bg" :type :text + :from 0 :to 30 :x 0 :y 56 + :size 11 :r 0.55 :g 0.55 :b 0.60 :a 1.0} + {:text "foreground [250,250,250] -> dt :fg" :type :text + :from 0 :to 33 :x 0 :y 70 + :size 11 :r 0.55 :g 0.55 :b 0.60 :a 1.0} + {:text "radius: 10px | border: white@15% | font: 14/500" :type :text + :from 0 :to 48 :x 0 :y 84 + :size 11 :r 0.55 :g 0.55 :b 0.60 :a 1.0}])]) + + (rt-node :hc-unknown :rect + {:x 0 :y 0 :w 300 :h 60} + :style {:bg (:bg-muted (:colors dt))} + :text [{:text (str "Unknown: " name) + :type :text + :from 0 + :to (+ 9 (count name)) + :x 16 + :y 36 + :size 14 + :r 0.90 + :g 0.30 + :b 0.30 + :a 1.0}])))) + +(defn handle-jit-command! + "Apply JIT command side effects through the provided runtime env. + Returns true when the command was handled." + [parsed {:keys [!extract-preview show-flow-info!]}] + (case (:kind parsed) + :extract-component + (do + (if (:clear? parsed) + (do + (reset! !extract-preview nil) + (show-flow-info! "Extract preview cleared.")) + (let [url (:url parsed) + selector (:selector parsed) + msg (cond + (and url selector) (str "Extract mode active.\nURL: " url "\nSelector: " selector + "\n\nWaiting for extracted data...\nUse /extract clear to exit.") + url (str "Extract mode active.\nURL: " url "\nSelector: auto-detect" + "\n\nWaiting for extracted data...\nUse /extract clear to exit.") + selector (str "Extract mode active.\nSelector: " selector + "\n\nWaiting for extracted data...\nUse /extract clear to exit.") + :else (str "Extract mode active.\nSelector: auto-detect" + "\n\nWaiting for extracted data...\nUse /extract clear to exit."))] + (reset! !extract-preview {:pending? true :url url :selector selector}) + (show-flow-info! msg))) + true) + + :hardcode + (do + (reset! !extract-preview {:rt-node (hardcoded-preview-node (:name parsed))}) + (show-flow-info! (str "Hardcoded: " (:name parsed))) + true) + + false)) diff --git a/src/app/client/workspace/agent.cljs b/src/app/client/workspace/agent.cljs new file mode 100644 index 0000000..f7d2328 --- /dev/null +++ b/src/app/client/workspace/agent.cljs @@ -0,0 +1,167 @@ +(ns app.client.workspace.agent + "Agent streaming: ticket parsing and SSE stream management." + (:require [clojure.string :as str] + [cljs.reader :as reader])) + +(def priority-label->num + {"urgent" 1 "high" 2 "medium" 3 "low" 4 "none" 0 + "1" 1 "2" 2 "3" 3 "4" 4 "0" 0}) + +(defn normalize-priority + "Coerce a priority value (number, string label, or string digit) to an int 0-4." + [p] + (cond + (number? p) (int p) + (string? p) (or (get priority-label->num (str/lower-case p)) 0) + :else 0)) + +(defn normalize-ticket + "Map a raw issue object (from Linear MCP or Claude JSON) to our ticket shape. + Handles both Linear native fields and pre-formatted fields." + [t] + {:id (or (:identifier t) (:id t) "UNKNOWN") + :title (or (:title t) "Untitled") + :status (or (get-in t [:state :name]) (:status t) "unknown") + :assignee (or (get-in t [:assignee :name]) (:assignee t) "unassigned") + :priority (normalize-priority (:priority t)) + :description (or (:description t) "")}) + +(defn parse-tickets-from-trail + "Extract tickets from trail :tool-result events (raw MCP responses). + Tries to parse each tool result as JSON containing an array of issues. + Returns [{:id :title :status ...}] or nil." + [trail] + (let [tool-results (->> trail + (filter #(= :tool-result (:kind %))) + (mapv :content)) + ;; Try each tool result — the Linear MCP response is usually a JSON array + tickets (some (fn [content] + (when (string? content) + (try + (let [parsed (js/JSON.parse content) + data (js->clj parsed :keywordize-keys true) + ;; Handle both direct array and {:issues [...]} wrapper + arr (cond + (vector? data) data + (vector? (:issues data)) (:issues data) + (vector? (:nodes data)) (:nodes data) + :else nil)] + (when (and (seq arr) (or (:title (first arr)) + (:identifier (first arr)))) + (mapv normalize-ticket arr))) + (catch :default _ nil)))) + tool-results)] + tickets)) + +(defn parse-tickets-from-output + "Fallback: extract a JSON ticket array from agent text output. + Tries ```json code block first, then bracket-matching. + Returns [{:id :title :status :assignee :priority}] or nil." + [output-text] + (when (and output-text (not (str/blank? output-text))) + (let [json-block-re #"(?s)```json\s*\n?(.*?)\n?\s*```" + match1 (re-find json-block-re output-text) + json-str (if match1 + (second match1) + (let [start (str/index-of output-text "[")] + (when start + (loop [i start depth 0 max-i (min (count output-text) (+ start 50000))] + (if (>= i max-i) + nil + (let [c (.charAt output-text i) + new-depth (cond (= c \[) (inc depth) + (= c \]) (dec depth) + :else depth)] + (if (zero? new-depth) + (subs output-text start (inc i)) + (recur (inc i) new-depth max-i))))))))] + (when json-str + (try + (let [parsed (js/JSON.parse json-str) + arr (js->clj parsed :keywordize-keys true)] + (when (vector? arr) + (mapv normalize-ticket arr))) + (catch :default e + (js/console.warn "[FLOW] Failed to parse tickets JSON:" (.-message e)) + nil)))))) + +(def ticket-json-schema + "JSON schema for Claude CLI --json-schema flag. Validates structured ticket output." + (js/JSON.stringify + (clj->js {:type "object" + :properties {:tickets {:type "array" + :items {:type "object" + :properties {:id {:type "string"} + :title {:type "string"} + :status {:type "string"} + :assignee {:type "string"} + :priority {:type "string"} + :description {:type "string"}} + :required ["title" "status"]}}} + :required ["tickets"]}))) + +(defn parse-structured-result + "Parse the structured result from Claude CLI --json-schema output. + Returns [{:id :title :status ...}] or nil." + [structured-result] + (when structured-result + (try + (let [parsed (if (string? structured-result) + (js/JSON.parse structured-result) + (clj->js structured-result)) + data (js->clj parsed :keywordize-keys true) + tickets (:tickets data)] + (when (seq tickets) + (mapv normalize-ticket tickets))) + (catch :default e + (js/console.warn "[FLOW] Failed to parse structured result:" (.-message e)) + nil)))) + +(defn stream-agent-run! + "Streaming fetch: POST to url, read SSE events via ReadableStream. + Calls (on-event edn-map) for each parsed SSE event. + Calls (on-error err) on failure. Returns nil." + [url body on-event on-error] + (-> (js/fetch url + (clj->js {:method "POST" + :headers {"Content-Type" "application/edn"} + :body (pr-str body)})) + (.then + (fn [resp] + (if-not (.-ok resp) + (on-error (js/Error. (str "HTTP " (.-status resp)))) + (let [rdr (.getReader (.-body resp)) + !buf (atom "")] + (letfn [(pump [] + (-> (.read rdr) + (.then + (fn [result] + (if (.-done result) + ;; Stream ended — flush any remaining buffer + (let [remaining @!buf] + (when (seq remaining) + (doseq [chunk (str/split remaining #"\n\n")] + (let [trimmed (str/trim chunk)] + (when (str/starts-with? trimmed "data: ") + (try + (on-event (reader/read-string (subs trimmed 6))) + (catch :default _ nil))))))) + ;; Got a chunk — decode + split on SSE boundary + (let [text (.decode (js/TextDecoder.) (.-value result)) + buf (swap! !buf str text) + parts (str/split buf #"\n\n" -1)] + ;; All parts except the last are complete events + (reset! !buf (peek parts)) + (doseq [part (pop parts)] + (let [trimmed (str/trim part)] + (when (str/starts-with? trimmed "data: ") + (try + (on-event (reader/read-string (subs trimmed 6))) + (catch :default _ nil))))) + (pump))))) + (.catch (fn [e] (on-error e)))))] + (pump)))))) + (.catch (fn [e] (on-error e)))) + nil) + + diff --git a/src/app/client/workspace/cmd_panel.cljs b/src/app/client/workspace/cmd_panel.cljs new file mode 100644 index 0000000..682805c --- /dev/null +++ b/src/app/client/workspace/cmd_panel.cljs @@ -0,0 +1,201 @@ +(ns app.client.workspace.cmd-panel + "Command panel: event handling, text parsing, and rect computation." + (:require [clojure.string :as str] + [missionary.core :as m] + [app.client.workspace.events :refer [maybe-snap]] + [app.client.workspace.rect-tree :refer [rt-node]] + [app.client.workspace.runtime.workspace-actions :as ws] + [app.client.workspace.text-input :as text-input] + [app.client.workspace.ui-primitives :refer [dt]] + [app.client.workspace.sidebar :refer [sidebar-w]] + [app.client.workspace.trail :refer [compute-agent-panel-h]])) + +(defn cmd-panel-apply-event + "Pure function: apply event to command panel, returns new panel state" + [panel event clipboard] + (let [input {:text (:text panel) :cursor (:cursor panel)}] + (case (:type event) + :char + (let [new-input (text-input/insert-char input (:char event) false)] + (assoc panel :text (:text new-input) :cursor (:cursor new-input))) + + :backspace + (let [new-input (text-input/delete-backward input false)] + (assoc panel :text (:text new-input) :cursor (:cursor new-input))) + + :delete + (let [new-input (text-input/delete-forward input false)] + (assoc panel :text (:text new-input) :cursor (:cursor new-input))) + + :enter + ;; Submit command - will be handled by caller + panel + + :left + (let [new-input (text-input/move-cursor input :left false nil)] + (assoc panel :cursor (:cursor new-input))) + + :right + (let [new-input (text-input/move-cursor input :right false nil)] + (assoc panel :cursor (:cursor new-input))) + + :home + (let [new-input (text-input/move-cursor input :home false nil)] + (assoc panel :cursor (:cursor new-input))) + + :end + (let [new-input (text-input/move-cursor input :end false nil)] + (assoc panel :cursor (:cursor new-input))) + + :word-left + (let [new-input (text-input/move-word input :left false nil)] + (assoc panel :cursor (:cursor new-input))) + + :word-right + (let [new-input (text-input/move-word input :right false nil)] + (assoc panel :cursor (:cursor new-input))) + + :paste + (if clipboard + (let [new-input (text-input/paste input clipboard false)] + (assoc panel :text (:text new-input) :cursor (:cursor new-input))) + panel) + + ;; Default: no change + panel))) + +(defn cmd-prompt-text + "Build the command panel prompt string for a given provider." + [provider] + (str "[" (-> (or provider :claude) name str/upper-case) "]> ")) + +(defn cmd-text-start-x + "Compute the x-pixel where user-typed text begins, after the prompt. + Must be used consistently by caret, text-ops, and mouse click handlers." + [provider font-size char-width dpr snap?] + (let [prompt (cmd-prompt-text provider) + char-advance (maybe-snap (* font-size char-width) dpr snap?) + prompt-x (maybe-snap 24 dpr snap?)] + (maybe-snap (+ prompt-x (* (count prompt) char-advance)) dpr snap?))) + +(defn parse-agent-command + "Parse command-panel text into a generic agent/runtime action. + Workflow-specific command vocabularies are delegated by runtime." + [cmd-text current-provider] + (let [trimmed (str/trim (or cmd-text ""))] + (cond + (str/blank? trimmed) + {:kind :noop} + + (str/starts-with? trimmed "/provider ") + (let [arg (-> trimmed + (subs (count "/provider ")) + str/trim + str/lower-case + keyword)] + (if (contains? #{:claude :codex :gemini} arg) + {:kind :set-provider :provider arg} + {:kind :error :message (str "Unknown provider: " arg)})) + + (str/starts-with? trimmed "/replay") + {:kind :replay} + + (str/starts-with? trimmed "/run ") + (let [argv (-> trimmed + (subs (count "/run ")) + str/trim + (str/split #"\s+") + vec) + first-bin (some-> (first argv) str/lower-case) + provider (case first-bin + "claude" :claude + "codex" :codex + "gemini" :gemini + current-provider)] + (if (seq argv) + {:kind :run :provider provider :argv argv :prompt (str/join " " argv)} + {:kind :error :message "Missing argv for /run"})) + + (str/starts-with? trimmed "/") + {:kind :workflow-command :command trimmed} + + :else + {:kind :run :provider current-provider :prompt trimmed}))) + +(defn rects tree->text-ops tree->shadows resolve-layout]] + [app.client.workspace.runtime.workspace-actions :as ws] + [app.client.workspace.ui-primitives :as ui :refer [dt]] + [app.client.workspace.sidebar :as sidebar :refer [sidebar-w]] + [app.client.workspace.trail :as trail :refer [trail->display-lines trail->chat-nodes agent-wrapped-line-count compute-agent-panel-h]] + [app.client.workspace.shell :refer [build-file-layout]] + [app.client.workspace.cmd-panel :refer [cmd-prompt-text cmd-text-start-x]] + [app.client.workspace.themes :as themes])) + +(defn (:provider agent-output) name str/upper-case) + prompt (:prompt agent-output) + result-output (or (:output agent-output) "") + status-color (case status + :complete {:r 0.55 :g 0.9 :b 0.55 :a 1.0} + :failed {:r 0.95 :g 0.45 :b 0.45 :a 1.0} + :timeout {:r 0.95 :g 0.75 :b 0.35 :a 1.0} + :running {:r 0.6 :g 0.8 :b 1.0 :a 1.0} + :submitting {:r 0.6 :g 0.8 :b 1.0 :a 1.0} + {:r 0.75 :g 0.75 :b 0.75 :a 1.0}) + header-text (cond + (nil? status) nil + (= status :running) (str "[" provider-name "] running: " prompt) + (= status :submitting) (str "[" provider-name "] submitting: " prompt) + (= status :failed) (str "[" provider-name "] failed: " prompt) + (= status :timeout) (str "[" provider-name "] timeout: " prompt) + (= status :complete) (str "[" provider-name "] complete: " prompt) + :else (str "[" provider-name "] " (name status) ": " prompt)) + output-lines (str/split-lines result-output) + agent-x-px (+ 24 sb-w) + available-w (- (:width viewport) agent-x-px 24) + max-chars (if (pos? char-advance) (max 1 (int (/ available-w char-advance))) 80) + trail (:trail agent-output) + display-entries (if (seq trail) (trail->display-lines trail) nil) + raw-lines (if display-entries + (cond-> [] + header-text (conj {:text header-text :color status-color}) + (and (= status :running) (empty? display-entries)) (conj {:text "..." :color status-color}) + (seq display-entries) (into display-entries)) + (let [flat-lines (cond-> [] + header-text (conj header-text) + (and (= status :running) (empty? output-lines)) (conj "...") + (seq output-lines) (into output-lines))] + (mapv (fn [l] {:text l :color status-color}) flat-lines))) + all-lines (into [] + (mapcat (fn [entry] + (let [nl-lines (str/split-lines (or (:text entry) "")) + wrapped (mapcat #(wrap-line % max-chars) nl-lines)] + (mapv (fn [wl] {:text wl :color (:color entry)}) wrapped)))) + raw-lines) + agent-panel-h (if file-workspace? 0 + (compute-agent-panel-h agent-output font-size (:height viewport) + (:width viewport) char-advance)) + agent-x (maybe-snap (+ 24 sb-w) dpr snap?) + agent-y0 (maybe-snap (+ scroll-y (- (:height viewport) cmd-panel-h status-bar-h agent-panel-h 12)) dpr snap?) + line-step (maybe-snap (* font-size 1.2) dpr snap?) + panel-top agent-y0 + panel-bottom (+ agent-y0 agent-panel-h) + agent-lines (into [] + (comp + (map (fn [idx] + (let [entry (nth all-lines idx) + y (+ agent-y0 8 font-size (* idx line-step) (- agent-scroll-y)) + c (:color entry)] + (when (and (>= y (+ panel-top 8)) (< y panel-bottom)) + [{:text (:text entry) :type :comment + :from 0 :to (count (:text entry)) + :x agent-x :y y :size font-size + :r (:r c) :g (:g c) :b (:b c) :a (:a c)}])))) + (filter some?)) + (range (count all-lines))) + status-y (maybe-snap (+ scroll-y (- (:height viewport) status-bar-h) 4 font-size) dpr snap?) + status-left-text (if flow-mode? + (let [node-name (some-> (:node flow-state) name str/upper-case) + n-tickets (count (:tickets flow-state)) + n-selected (count (:selected flow-state))] + (str node-name " | " n-tickets " tickets | " n-selected " selected")) + (let [sb-cursor (:cursor doc)] + (str "Ln " (inc (:line sb-cursor)) ", Col " (inc (:col sb-cursor))))) + file-name (or (:name current-file) + (get-in local-world [:selected-artifact :name]) + "untitled") + provider-upper (some-> provider name str/upper-case) + status-right-text (if provider-upper (str file-name " | " provider-upper) file-name) + status-right-w (* (count status-right-text) char-advance) + status-right-x (maybe-snap (- (:width viewport) status-right-w 16) dpr snap?)] + (vec (concat cmd-lines + (when-not file-workspace? agent-lines) + [{:text status-left-text :type :comment + :from 0 :to (count status-left-text) + :x (maybe-snap (+ 16 sb-w) dpr snap?) :y status-y :size font-size + :r 0.65 :g 0.65 :b 0.65 :a 0.9}] + [{:text status-right-text :type :comment + :from 0 :to (count status-right-text) + :x status-right-x :y status-y :size font-size + :r 0.65 :g 0.65 :b 0.65 :a 0.9}])))) + text-ops tree))) + + ;; MODE-SWITCH: use pre-computed flow text, or compute editor text + [editor-ops final-line-mapping line-num-ops] + (if (:rt-node extract-preview) + ;; Extract preview on right half + (let [half-w (/ content-vw 2) + preview-tree (when-let [rt (:rt-node extract-preview)] + (resolve-layout + (rt-node :extract-preview-root :rect + {:x half-w :y 0 :w half-w :h (:height viewport)} + :layout {:direction :column :padding [16 16 16 16] :gap 8} + :children [(rt-node :extract-label :text + {:x 0 :y 0 :w (- half-w 32) :h 24} + :text [{:text "Compiled Preview" :type :keyword + :from 0 :to 16 :x 0 :y 16 + :size 14 :r 0.55 :g 0.55 :b 0.60 :a 1.0}]) + (assoc-in rt [:bounds :w] (- half-w 32))])))] + [(if preview-tree (tree->text-ops preview-tree) []) + (vec (range (count lines))) + []]) + (if flow-mode? + ;; Flow canvas: use pre-computed ops from intake or run + [(if (ws/local-world-intake? local-world) intake-text run-text) + (vec (range (count lines))) + []] + + ;; Normal editor mode + (let [folded (or (:folded fold-state) #{}) + regions (or (:regions fold-state) []) + total-line-count (count lines) + large-file? (and (> total-line-count 500) (empty? folded)) + raw-start (max 0 (- (int (/ scroll-y line-h)) 5)) + raw-end (+ (int (/ (+ scroll-y (:height viewport)) line-h)) 5) + visible-start (min total-line-count raw-start) + visible-end (min total-line-count (max visible-start raw-end)) + visible-lines (subvec lines visible-start visible-end) + tokenized-visible (mapv tokenize-fn visible-lines)] + (if large-file? + (let [adjusted-y (+ layout-y (* visible-start line-h)) + result (layout-fn tokenized-visible editor-lx adjusted-y font-size + [] #{} char-advance line-h theme-id) + full-mapping (vec (range total-line-count)) + nums (mapv (fn [i] + (let [logical (+ visible-start i) + num-str (str (inc logical)) + num-w (* (count num-str) char-advance) + x (maybe-snap (- layout-x 8 num-w) dpr snap?) + y (+ adjusted-y font-size (* i line-h))] + [{:text num-str :type :line-number + :from 0 :to (count num-str) + :x x :y y :size font-size + :r 0.45 :g 0.45 :b 0.45 :a 0.4}])) + (range (count visible-lines)))] + [(:render-ops result) full-mapping nums]) + + (do (when (seq folded) + (js/console.log "[TEXT-OPS] folded:" (clj->js folded) + "total-lines:" total-line-count + "regions:" (count regions))) + (let [tokenized-all (into [] + (map-indexed + (fn [idx _] + (if (and (>= idx visible-start) (< idx visible-end)) + (nth tokenized-visible (- idx visible-start)) + []))) + lines) + result (layout-fn tokenized-all editor-lx layout-y font-size + regions folded char-advance line-h theme-id) + _ (when (seq folded) + (js/console.log "[TEXT-OPS] render-ops:" (count (:render-ops result)) + "mapping:" (count (:line-mapping result)) + "regions:" (count regions))) + mapping (:line-mapping result) + nums (mapv (fn [visual-idx] + (let [logical (get mapping visual-idx visual-idx) + num-str (str (inc logical)) + num-w (* (count num-str) char-advance) + x (maybe-snap (- layout-x 8 num-w) dpr snap?) + y (+ layout-y font-size (* visual-idx line-h))] + [{:text num-str :type :line-number + :from 0 :to (count num-str) + :x x :y y :size font-size + :r 0.45 + :g 0.45 :b 0.45 :a 0.4}])) + (range (count mapping)))] + [(filterv seq (:render-ops result)) mapping nums])))))) + + ;; When file is open, clip editor text to left 40% and add right-tree text ops + [editor-ops final-line-mapping line-num-ops] + (if (and file-workspace? (not (:rt-node extract-preview))) + (let [code-w (int (* content-vw (ws/pane-width-pct local-world :main 0.4))) + clip-left layout-x + header-h 36 + clip-top (+ scroll-y header-h) + clip-sub (fn [sub] + (let [x (or (:x sub) 0) + y (or (:y sub) 0) + fs (or (:size sub) font-size) + cw (if (== fs font-size) + char-advance + (maybe-snap (* fs char-width) dpr snap?)) + txt (or (:text sub) "") + text-end (+ x (* (count txt) cw))] + (when (and (< x code-w) (> text-end clip-left) (>= y clip-top)) + (let [skip (if (< x clip-left) (min (count txt) (int (Math/ceil (/ (- clip-left x) cw)))) 0) + adj-x (+ x (* skip cw)) + adj-txt (if (pos? skip) (subs txt skip) txt) + max-chars (if (pos? cw) + (max 0 (int (/ (- code-w adj-x) cw))) + 1000) + final-txt (if (> (count adj-txt) max-chars) + (subs adj-txt 0 max-chars) + adj-txt)] + (when (seq final-txt) + (assoc sub :text final-txt :x adj-x + :from skip :to (+ skip (count final-txt)))))))) + clip-op (fn [op] + (if (vector? op) + (let [clipped (into [] (keep clip-sub) op)] + (when (seq clipped) clipped)) + (clip-sub op))) + clipped (into [] (keep clip-op) editor-ops) + shimmer-alpha (if shimmer-phase 0.9 0.4) + file-layout-h (- (:height viewport) cmd-panel-h status-bar-h) + right-tree (resolve-layout + (build-file-layout content-vw file-layout-h + current-file agent-output font-size + shimmer-alpha trail-collapsed + :local-world local-world + :active-pane active-pane :char-advance char-advance + :chat-scroll-y (or chat-scroll-y 0) + :chat-input chat-input :focus focus)) + right-text-ops (tree->text-ops right-tree) + pinned-ops (mapv (fn [op] + (if (vector? op) + (mapv #(update % :y + scroll-y) op) + (update op :y + scroll-y))) + right-text-ops)] + (let [clip-ln (fn [op] + (if (vector? op) + (let [f (filterv #(>= (or (:y %) 0) clip-top) op)] + (when (seq f) f)) + (when (>= (or (:y op) 0) clip-top) op))) + clipped-ln (into [] (keep clip-ln) line-num-ops)] + [(into (vec clipped) pinned-ops) final-line-mapping clipped-ln])) + [editor-ops final-line-mapping line-num-ops]) + + ;; Bottom clip + bottom-clip-y (+ scroll-y (- (:height viewport) cmd-panel-h status-bar-h)) + clip-bottom (fn [ops] + (into [] + (keep (fn [op] + (if (vector? op) + (let [clipped (filterv #(< (or (:y %) 0) bottom-clip-y) op)] + (when (seq clipped) clipped)) + (when (< (or (:y op) 0) bottom-clip-y) op)))) + ops)) + + offset-editor-ops (offset-text-ops* (clip-bottom editor-ops) sb-w) + offset-line-num-ops (offset-text-ops* (clip-bottom line-num-ops) sb-w)] + ;; Content-only return (chrome is in separate rects tree->shadows]] + [app.client.workspace.runtime.workspace-actions :as ws] + [app.client.workspace.themes :as themes] + [app.client.workspace.text-input :as text-input] + [app.client.workspace.ui-primitives :refer [dt]] + [app.client.workspace.sidebar :as sidebar :refer [sidebar-w cmd-panel-h status-bar-h build-sidebar-tree derive-effective-sidebar]] + [app.client.workspace.shell :refer [build-file-layout]])) + +;; ============================================================================ +;; LAYER 5: COMPONENT UPDATE FLOWS +;; ============================================================================ + +(defn editor-apply-event + "Pure function: apply event to editor doc, returns new doc" + [doc event line-lengths clipboard] + (let [input {:lines (:lines doc) + :cursor (:cursor doc) + :selection (:selection doc) + :desired-col (:desired-col doc)}] + (case (:type event) + :char + (let [new-input (text-input/insert-char input (:char event) true)] + (merge doc new-input {:selection nil})) + + :backspace + (let [new-input (text-input/delete-backward input true)] + (merge doc new-input)) + + :delete + (let [new-input (text-input/delete-forward input true)] + (merge doc new-input)) + + :enter + (let [new-input (text-input/insert-char input "\n" true)] + (merge doc new-input {:selection nil})) + + :left + (let [new-input (text-input/move-cursor input :left true line-lengths)] + (merge doc new-input)) + + :right + (let [new-input (text-input/move-cursor input :right true line-lengths)] + (merge doc new-input)) + + :up + (let [new-input (text-input/move-cursor input :up true line-lengths)] + (merge doc new-input)) + + :down + (let [new-input (text-input/move-cursor input :down true line-lengths)] + (merge doc new-input)) + + :home + (let [new-input (text-input/move-cursor input :home true line-lengths)] + (merge doc new-input)) + + :end + (let [new-input (text-input/move-cursor input :end true line-lengths)] + (merge doc new-input)) + + :word-left + (let [new-input (text-input/move-word input :left true line-lengths)] + (merge doc new-input)) + + :word-right + (let [new-input (text-input/move-word input :right true line-lengths)] + (merge doc new-input)) + + :paste + (if clipboard + (let [new-input (text-input/paste input clipboard true)] + (merge doc new-input)) + doc) + + ;; Default: no change + doc))) + + +;; ============================================================================ +;; LAYER 6: GPU STATE DERIVED FLOWS +;; ============================================================================ + +(defn calculate-logical->visual + "Create reverse mapping from logical line to visual line" + [line-mapping] + (reduce-kv (fn [m visual-idx logical-idx] + (assoc m logical-idx visual-idx)) + {} + (vec line-mapping))) + +(defn build-line-mapping + "Build visual->logical mapping based on fold regions." + [lines regions folded] + (let [num-lines (count lines)] + (loop [logical-idx 0 mapping []] + (if (>= logical-idx num-lines) + mapping + (let [visible? (not (some (fn [{:keys [start-line end-line]}] + (and (contains? folded start-line) + (> logical-idx start-line) + (<= logical-idx end-line))) + regions))] + (if visible? + (recur (inc logical-idx) (conj mapping logical-idx)) + (recur (inc logical-idx) mapping))))))) + +(defn compute-fold-state + "Compute fold regions + line mapping once per doc/fold change. + Safe for large files because this only runs on document changes (cached in visual (calculate-logical->visual line-mapping)] + {:lines lines + :lengths lengths + :regions regions + :folded folded + :line-mapping line-mapping + :logical->visual logical->visual})) + +(defn visual]} fold-state + + ;; Helper to get visual y for logical line + logical->visual-y (fn [logical-line] + (when-let [visual-idx (get logical->visual logical-line)] + (+ layout-y (* visual-idx line-h)))) + + ;; Use the passed char advance (reactive based on active font) + char-w char-advance + ;; Gutter uses unscrolled x so fold indicators stay fixed + gutter-x (- (or gutter-lx layout-x) gutter-w) + + ;; Fold indicator rects + fold-rects (keep (fn [{:keys [start-line]}] + (when-let [visual-y (logical->visual-y start-line)] + (let [is-folded? (contains? folded start-line) + indicator-size 8 + x (+ gutter-x 2) + y (+ visual-y (/ (- line-h indicator-size) 2))] + {:id [:fold start-line] :z 1 + :x x :y y :w indicator-size :h indicator-size + :r (if is-folded? 0.3 0.7) + :g (if is-folded? 0.5 0.6) + :b (if is-folded? 0.9 0.3) + :a 0.8}))) + regions) + + ;; Bracket match rects (pre-computed, cached in visual-y line)] + {:id [:bracket btype] :z 2 + :x (+ layout-x (* col char-w)) + :y visual-y + :w char-w + :h line-h + :r 0.8 :g 0.6 :b 0.2 :a 0.4})) + [[:open (:open bracket-match)] [:close (:close bracket-match)]])) + + ;; Caret rect (only when editor is focused and no selection) + caret-rect (when (and cursor caret-visible (= focus :editor) (not selection)) + (when-let [visual-y (logical->visual-y (:line cursor))] + {:id :caret :z 4 + :x (+ layout-x (* (:col cursor) char-w)) + :y visual-y + :w 2 + :h line-h + :r 0.9 :g 0.9 :b 0.9 :a 1.0})) + + ;; Selection rects + selection-rects (when selection + (let [{:keys [start end]} selection + [s e] (if (or (> (:line start) (:line end)) + (and (= (:line start) (:line end)) + (> (:col start) (:col end)))) + [end start] + [start end])] + (keep (fn [logical-line] + (when-let [visual-y (logical->visual-y logical-line)] + (let [line-len (get lengths logical-line 0) + col-start (if (= logical-line (:line s)) (:col s) 0) + col-end (if (= logical-line (:line e)) (:col e) line-len) + width-chars (- col-end col-start) + x (+ layout-x (* col-start char-w)) + raw-w (* width-chars char-w) + ;; Clamp to editor pane boundary + clamped-w (min raw-w (max 0 (- viewport-w x)))] + (when (> clamped-w 0) + {:id [:selection logical-line] :z 3 + :x x :y visual-y + :w clamped-w :h line-h + :r 0.2 :g 0.4 :b 0.9 :a 0.5})))) + (range (:line s) (inc (:line e)))))) + + ;; Current-line highlight (subtle background on cursor's line) + current-line-rect (when (and cursor (= focus :editor) (not selection)) + (when-let [visual-y (logical->visual-y (:line cursor))] + {:id :current-line :z 0 + :x 0 :y visual-y :w viewport-w :h line-h + :r 1.0 :g 1.0 :b 1.0 :a 0.04})) + + ;; Eval result rect + eval-rect (when eval-result + (let [now (js/Date.now)] + (when (< now (:expires-at eval-result)) + (when-let [visual-y (logical->visual-y (:line eval-result))] + (let [line-len (get lengths (:line eval-result) 0) + result-x (+ layout-x (* (+ line-len 2) char-w)) + result-w (* (count (:text eval-result)) char-w)] + {:id :eval-result :z 5 + :x result-x + :y visual-y + :w (+ result-w 16) + :h line-h + :r (if (str/starts-with? (:text eval-result) "=>") 0.1 0.4) + :g (if (str/starts-with? (:text eval-result) "=>") 0.3 0.1) + :b 0.1 + :a 0.8})))))] + + (vec (concat (if current-line-rect [current-line-rect] []) + fold-rects + (or bracket-rects []) + (or selection-rects []) + (if caret-rect [caret-rect] []) + (if eval-rect [eval-rect] []))))) + +(defn :} so sidebar rects + can be routed to a differential buffer pool instead of the editor rect system. + Split into scoped sub-flows so each mode only watches its own atoms. + Caret blink no longer recomputes flow-canvas rects and vice versa." + [!editor-doc !eval-result !caret-visible !focus !settings !active-font !viewport + rects tree) + t3 (js/performance.now) + shadows (tree->shadows tree) + t4 (js/performance.now)] + (js/console.log "[SIDEBAR-FLOW] build:" (.toFixed (- t1 t0) 1) "ms | resolve:" (.toFixed (- t2 t1) 1) "ms | rects:" (.toFixed (- t3 t2) 1) "ms | shadows:" (.toFixed (- t4 t3) 1) "ms | TOTAL:" (.toFixed (- t4 t0) 1) "ms | rows:" (count (:children (first (:children (last (:children raw-tree))))))) + {:rects rects :shadows shadows}))))) + rects preview-tree) []) + :shadows (if preview-tree (tree->shadows preview-tree) [])}) + + ;; File open (3-pane) + (ws/local-world-file-workspace? local-world) + (let [code-w (int (* content-w (ws/pane-width-pct local-world :main 0.4))) + content-h (- (:height viewport) cmd-panel-h status-bar-h) + line-h (maybe-snap (* font-size (:line-height settings)) dpr snap?) + lx (maybe-snap (- layout-x (or scroll-x 0)) dpr snap?) + ly (maybe-snap layout-y dpr snap?) + ulx (maybe-snap layout-x dpr snap?) + editor-rects (compute-editor-rects doc fold-state bracket-match eval-result + caret-visible focus lx ly line-h gutter-w + char-advance code-w + :gutter-lx ulx) + shimmer-alpha (if shimmer-phase 0.9 0.4) + right-tree (resolve-layout + (build-file-layout content-w content-h current-file agent-output font-size + shimmer-alpha trail-collapsed + :local-world local-world + :active-pane active-pane :char-advance char-advance + :chat-scroll-y (or chat-scroll-y 0) + :chat-input chat-input :focus focus)) + right-rects (mapv #(update % :y + scroll-y) (tree->rects right-tree)) + right-shadows (mapv #(update % :y + scroll-y) (tree->shadows right-tree))] + {:rects (into (vec right-rects) editor-rects) + :shadows (vec right-shadows)}) + + ;; Plain editor + :else + (let [line-h (maybe-snap (* font-size (:line-height settings)) dpr snap?) + lx (maybe-snap (- layout-x (or scroll-x 0)) dpr snap?) + ulx (maybe-snap layout-x dpr snap?) + ly (maybe-snap layout-y dpr snap?)] + {:rects (compute-editor-rects doc fold-state bracket-match eval-result caret-visible focus + lx ly line-h gutter-w char-advance content-w + :gutter-lx ulx) + :shadows []})))) + semantic values. + Layer 2 (event sources) and Layer 4 (focus-based routing)." + (:require [missionary.core :as m])) + +;; ============================================================================ +;; LAYER 2: EVENT FLOWS (Produce Values, Don't Store) +;; ============================================================================ + +(defn >canvas-resize + "Flow that emits viewport dimensions when the canvas element resizes. + Uses ResizeObserver to detect size changes from flex layout, sidebar toggle, etc." + [canvas-node] + (->> (m/observe + (fn [!] + (let [emit! (fn [] + (let [raw-width (max 1 (.-clientWidth canvas-node)) + raw-height (max 1 (.-clientHeight canvas-node)) + win-width (max 1 (or (.-innerWidth js/window) raw-width)) + win-height (max 1 (or (.-innerHeight js/window) raw-height))] + (! {:width (max raw-width win-width) + :height (max raw-height win-height) + :dpr (or js/window.devicePixelRatio 1)})))] + (if (exists? js/ResizeObserver) + (let [obs (js/ResizeObserver. (fn [_entries] (emit!)))] + (.observe obs canvas-node) + (emit!) ;; Emit initial value + #(.disconnect obs)) + ;; Fallback: window resize (won't catch sidebar toggle) + (do (js/window.addEventListener "resize" emit!) + (emit!) + #(js/window.removeEventListener "resize" emit!)))))) + (m/relieve (fn [_old new] new)))) + +(defn >wheel [node] + "Flow that emits wheel delta values as {:dy N :dx N :x N :y N}." + (->> (m/observe + (fn [!] + (let [get-coords (fn [e] + (let [rect (.getBoundingClientRect node)] + {:x (- (.-clientX e) (.-left rect)) + :y (- (.-clientY e) (.-top rect))})) + handler (fn [e] + (let [{:keys [x y]} (get-coords e)] + (.preventDefault e) + (! {:dy (.-deltaY e) + :dx (.-deltaX e) + :shift? (.-shiftKey e) + :x x + :y y})))] + (.addEventListener node "wheel" handler #js {:passive false}) + #(.removeEventListener node "wheel" handler)))) + (m/relieve (fn [a b] {:dy (+ (:dy a) (:dy b)) + :dx (+ (:dx a) (:dx b)) + :shift? (:shift? b) + :x (:x b) + :y (:y b)})))) + +(defn >mouse [node] + "Flow that emits mouse events [:mousedown/:mouseup/:mousemove coords]" + (m/observe + (fn [!] + (let [get-coords (fn [e] + (let [rect (.getBoundingClientRect node)] + {:x (- (.-clientX e) (.-left rect)) + :y (- (.-clientY e) (.-top rect))})) + down-h (fn [e] (! [:mousedown (get-coords e)])) + up-h (fn [e] (! [:mouseup (get-coords e)])) + move-h (fn [e] (! [:mousemove (get-coords e)]))] + (.addEventListener node "mousedown" down-h) + (.addEventListener js/window "mouseup" up-h) + (.addEventListener js/window "mousemove" move-h) + (fn [] + (.removeEventListener node "mousedown" down-h) + (.removeEventListener js/window "mouseup" up-h) + (.removeEventListener js/window "mousemove" move-h)))))) + +(defn snap-to-dpr [v dpr] + (let [scale (or dpr 1)] + (/ (Math/round (* v scale)) scale))) + +(defn maybe-snap [v dpr snap?] + (if snap? (snap-to-dpr v dpr) v)) + +(defn parse-key-event + "Parse DOM keyboard event into semantic event map" + [e] + (let [key (.-key e) + ctrl? (or (.-ctrlKey e) (.-metaKey e)) + shift? (.-shiftKey e)] + (cond + ;; Global shortcuts (not affected by focus) + (and ctrl? (= key "k")) {:type :toggle-command-panel :global? true} + (and ctrl? (= key "g")) {:type :toggle-settings-panel :global? true} + (and ctrl? (= key "b")) {:type :toggle-file-viewer :global? true} + (and ctrl? (= key "s")) {:type :save :global? true} + ;; Pane focus shortcuts + (and ctrl? (= key "1")) {:type :focus-pane :pane :editor :global? true} + (and ctrl? (= key "2")) {:type :focus-pane :pane :chat :global? true} + (and ctrl? (= key "3")) {:type :focus-pane :pane :preview :global? true} + + ;; Escape - context dependent but handled globally + (= key "Escape") {:type :escape :global? true} + + ;; Editor-specific shortcuts + (and ctrl? (= key "Enter")) {:type :eval} + (and ctrl? (= key "z") (not shift?)) {:type :undo} + (and ctrl? (= key "z") shift?) {:type :redo} + (and ctrl? (= key "y")) {:type :redo} + (and ctrl? (= key "c")) {:type :copy} + (and ctrl? (= key "x")) {:type :cut} + ;; Ctrl+V: return nil so .preventDefault is NOT called — lets browser fire native paste event + + ;; Word navigation + (and ctrl? (= key "ArrowLeft")) {:type :word-left} + (and ctrl? (= key "ArrowRight")) {:type :word-right} + + ;; Navigation keys (shift? included for stack reorder) + (= key "ArrowLeft") {:type :left :shift? shift?} + (= key "ArrowRight") {:type :right :shift? shift?} + (= key "ArrowUp") {:type :up :shift? shift?} + (= key "ArrowDown") {:type :down :shift? shift?} + (= key "Home") {:type :home} + (= key "End") {:type :end} + + ;; Editing keys + (= key "Backspace") {:type :backspace} + (= key "Delete") {:type :delete} + (= key "Enter") {:type :enter} + + ;; Character input + (and (= 1 (count key)) (not ctrl?) (not (.-altKey e))) + {:type :char :char key} + + :else nil))) + +(defn >keyboard [node] + "Flow that emits parsed keyboard events" + (->> (m/observe + (fn [!] + (let [handler (fn [e] + (when-let [event (parse-key-event e)] + (.preventDefault e) + (! event)))] + (.addEventListener node "keydown" handler) + #(.removeEventListener node "keydown" handler)))) + (m/relieve (fn [_ x] x)))) + +(defn make-raf-flow + "Creates a fresh RAF flow - emits timestamps on each animation frame. + IMPORTANT: Must be called fresh for each subscription, not shared!" + [] + (m/observe + (fn [!] + (let [active? (volatile! true) + callback (fn loop [t] + (when @active? + (! t) + (js/requestAnimationFrame loop)))] + (js/requestAnimationFrame callback) + #(vreset! active? false))))) + +(defn make-blink-timer + "Creates a fresh blink timer flow - emits true/false every 530ms. + IMPORTANT: Must be called fresh for each subscription, not shared!" + [] + (m/ap + (loop [] + (m/amb true + (do (m/? (m/sleep 530)) + (m/amb false + (do (m/? (m/sleep 530)) + (recur)))))))) + +;; ============================================================================ +;; LAYER 4: FOCUS-BASED EVENT ROUTING +;; ============================================================================ +;; +;; IMPORTANT: Do NOT use m/ap with m/?< on m/watch here! +;; These flows filter discrete events - use m/eduction with deref instead. +;; This avoids cancellation when focus changes. + +(defn keyboard] + (->> >keyboard + (m/eduction (filter :global?)))) + +(defn keyboard !focus] + (->> >keyboard + (m/eduction (filter (fn [event] + (and (= @!focus :editor) + (not (:global? event)))))))) + +(defn keyboard !focus] + (->> >keyboard + (m/eduction (filter (fn [event] + (and (= @!focus :command-panel) + (not (:global? event)))))))) + +(defn keyboard !focus] + (->> >keyboard + (m/eduction (filter (fn [event] + (and (= @!focus :chat) + (not (:global? event)))))))) + +(defn keyboard !focus] + (->> >keyboard + (m/eduction (filter (fn [event] + (and (= @!focus :settings-panel) + (not (:global? event)))))))) diff --git a/src/app/client/workspace/rect_tree.cljs b/src/app/client/workspace/rect_tree.cljs new file mode 100644 index 0000000..d7ee8f4 --- /dev/null +++ b/src/app/client/workspace/rect_tree.cljs @@ -0,0 +1,394 @@ +(ns app.client.workspace.rect-tree + "Scene graph for nested UI. + Everything is a rect. The tree replaces scattered compute-*-rects fns with + one generic walk that produces flat GPU-compatible vectors." + (:require [clojure.string :as str])) + +(defn wrap-line + "Wrap a single string into lines of at most max-chars, breaking at word + boundaries (spaces). Falls back to hard char-split when a single word + exceeds max-chars." + [line max-chars] + (if (or (<= (count line) max-chars) (< max-chars 1)) + [line] + (let [words (str/split line #" ")] + (loop [ws words cur "" result []] + (if (empty? ws) + (if (seq cur) + (conj result cur) + result) + (let [w (first ws) + candidate (if (seq cur) (str cur " " w) w)] + (cond + ;; Fits on current line + (<= (count candidate) max-chars) + (recur (rest ws) candidate result) + ;; Current line has content — flush it, retry word on new line + (seq cur) + (recur ws "" (conj result cur)) + ;; Single word longer than max-chars — hard-split it + :else + (let [chunks (loop [rem w acc []] + (if (<= (count rem) max-chars) + (conj acc rem) + (recur (subs rem max-chars) + (conj acc (subs rem 0 max-chars)))))] + (recur (rest ws) + (peek chunks) + (into result (pop chunks))))))))))) + +(defn rt-node + "Create a rect tree node. Bounds are in parent-relative coordinates. + Children are rendered back-to-front (painter's order). + Optional :layout {:direction :column/:row :gap N :padding N :align :start/:center/:end :auto-height? bool} + enables automatic child positioning via resolve-layout." + [id type bounds & {:keys [style actions children text clip? data layout] + :or {clip? false}}] + {:id id + :type type + :bounds bounds + :style (or style {}) + :actions (or actions {}) + :children (vec (or children [])) + :text (or text []) + :clip? clip? + :data data + :layout layout}) + +;; --- Layout engine ---------------------------------------------------------- +;; Pure pre-pass: walks tree depth-first, computes child :x/:y from :layout +;; directives. Nodes without :layout pass through unchanged. + +(defn normalize-padding + "CSS-style padding shorthand: + number -> [n n n n] (uniform) + [vert horiz] -> [v h v h] (vertical, horizontal) + [t r b l] -> [t r b l] (clockwise from top)" + [p] + (cond + (number? p) [p p p p] + (nil? p) [0 0 0 0] + (and (vector? p) (= 2 (count p))) [(nth p 0) (nth p 1) (nth p 0) (nth p 1)] + (and (vector? p) (= 4 (count p))) p + :else [0 0 0 0])) + +(defn layout-children + "Position children inside a parent node according to its :layout directive. + Returns the node with children's :bounds :x/:y updated. + Children with (:data child :layout-skip?) pass through unchanged. + + Layout keys: + :direction :column (default) or :row + :gap px between children (default 0) + :padding number, [v h], or [t r b l] (default 0) + :align :start (default), :center, or :end — cross-axis alignment + :auto-height? if true, parent :h = content height + padding" + [node] + (let [layout (:layout node) + bounds (:bounds node) + parent-w (:w bounds 0) + parent-h (:h bounds 0)] + (if-not layout + node ;; no layout directive -> pass through + (let [{:keys [direction gap padding align auto-height?] + :or {direction :column gap 0 align :start}} layout + [pt pr pb pl] (normalize-padding padding) + children (:children node)] + (if (empty? children) + node + (let [;; Separate layout-managed children from skip children + positioned + (loop [cs children + cursor (if (= direction :column) pt pl) ;; start after top/left padding + result []] + (if (empty? cs) + result + (let [child (first cs)] + (if (get-in child [:data :layout-skip?]) + ;; Skip — preserve as-is + (recur (rest cs) cursor (conj result child)) + ;; Position this child + (let [cb (:bounds child) + cw (:w cb 0) + ch (:h cb 0) + ;; Cross-axis position + cross (case direction + :column + (case align + :center (+ pl (/ (- parent-w pl pr cw) 2)) + :end (- parent-w pr cw) + ;; :start + pl) + :row + (case align + :center (+ pt (/ (- parent-h pt pb ch) 2)) + :end (- parent-h pb ch) + ;; :start + pt)) + ;; Set x/y based on direction + new-bounds (if (= direction :column) + (assoc cb :x cross :y cursor) + (assoc cb :y cross :x cursor)) + new-child (assoc child :bounds new-bounds) + ;; Advance cursor along main axis + advance (if (= direction :column) ch cw) + next-cursor (+ cursor advance gap)] + (recur (rest cs) next-cursor (conj result new-child))))))) + ;; Auto-height: shrink-wrap parent to content + total-main (if auto-height? + (let [managed (filterv #(not (get-in % [:data :layout-skip?])) positioned) + last-child (peek managed)] + (when last-child + (let [lb (:bounds last-child)] + (+ (if (= direction :column) + (+ (:y lb 0) (:h lb 0) pb) + (+ (:x lb 0) (:w lb 0) pr)))))) + nil) + new-bounds (if total-main + (if (= direction :column) + (assoc bounds :h total-main) + (assoc bounds :w total-main)) + bounds)] + (assoc node :children positioned :bounds new-bounds))))))) + +(defn resolve-text-layout + "Auto-position text ops on a node that has :text-layout. + Text-layout map: {:line-height N :max-chars N :padding [t r b l] or N} + Text ops provide :text, :size, :r/:g/:b/:a, :type — but NOT :x/:y. + This fn computes :x/:y by wrapping text and stacking lines vertically. + Returns the node with :text updated (local coords)." + [node] + (let [tl (:text-layout node)] + (if-not tl + node + (let [{:keys [line-height max-chars padding]} tl + [pt _pr _pb pl] (normalize-padding padding) + text-specs (:text node)] + (if (empty? text-specs) + node + (let [ops (loop [specs text-specs + y pt + acc []] + (if (empty? specs) + acc + (let [spec (first specs) + txt (:text spec "") + size (:size spec 14) + ;; Split by newlines first, then wrap each line + raw-lines (str/split-lines txt) + lines (if max-chars + (vec (mapcat #(wrap-line % max-chars) raw-lines)) + raw-lines) + line-ops (mapv (fn [i line-text] + (assoc spec + :text line-text + :from 0 + :to (count line-text) + :x pl + :y (+ y (* i (or line-height size))))) + (range) lines) + next-y (+ y (* (count lines) (or line-height size)))] + (recur (rest specs) next-y (into acc line-ops)))))] + (assoc node :text ops))))))) + +(defn resolve-layout + "Recursive depth-first pre-pass: apply layout-children at each level, + resolve text-layout, then recurse into children. Returns a fully-positioned + tree ready for tree->rects / tree->text-ops / tree->shadows." + [node] + (let [laid-out (-> node layout-children resolve-text-layout) + children (:children laid-out)] + (if (empty? children) + laid-out + (assoc laid-out :children (mapv resolve-layout children))))) + +;; --- Tree walk: rects ------------------------------------------------------- + +(defn tree->rects + "Walk rect tree depth-first, emit flat vector of GPU rect maps. + Parent-relative coords are converted to absolute via parent-x/parent-y. + Clip-bounds is {:x :y :w :h} in absolute space (nil = no clipping). + Style keys: :bg, :radius, :corner-radii, :border-width, :border-widths, + :border-color, :gradient, :gradient-color2" + ([node] (tree->rects node 0 0 nil)) + ([node parent-x parent-y clip-bounds] + (let [{:keys [bounds style children clip?]} node + abs-x (+ parent-x (:x bounds 0)) + abs-y (+ parent-y (:y bounds 0)) + w (:w bounds 0) + h (:h bounds 0) + ;; If parent clips, check visibility + visible? (if clip-bounds + (let [cx (:x clip-bounds) cy (:y clip-bounds) + cw (:w clip-bounds) ch (:h clip-bounds)] + (and (< abs-x (+ cx cw)) + (< abs-y (+ cy ch)) + (> (+ abs-x w) cx) + (> (+ abs-y h) cy))) + true)] + (when visible? + (let [;; Background rect from style — now includes SDF properties + bg (when-let [c (:bg style)] + (cond-> {:x abs-x :y abs-y :w w :h h + :r (nth c 0) :g (nth c 1) :b (nth c 2) :a (nth c 3)} + ;; Carry node identity for keyed differential rendering (Phase 5) + (:id node) (assoc :id (:id node)) + (:radius style) (assoc :radius (:radius style)) + (:corner-radii style) (assoc :corner-radii (:corner-radii style)) + (:border-width style) (assoc :border-width (:border-width style)) + (:border-widths style) (assoc :border-widths (:border-widths style)) + (:border-color style) (assoc :border-color (:border-color style)) + (:gradient style) (assoc :gradient (:gradient style)) + (:gradient-color2 style)(assoc :gradient-color2 (:gradient-color2 style)))) + ;; This node's clip bounds for children (if clip? is set) + child-clip (if clip? + {:x abs-x :y abs-y :w w :h h} + clip-bounds) + ;; Recurse children (depth-first, painter's order) + child-rects (into [] (mapcat #(tree->rects % abs-x abs-y child-clip)) children)] + (cond-> [] + bg (conj bg) + true (into child-rects))))))) + +;; --- Tree walk: text ops ---------------------------------------------------- + +(defn tree->text-ops + "Walk rect tree depth-first, emit nested vector of text-op vectors. + Text ops on each node have :x/:y in node-local space; the walk + offsets them to absolute coordinates. Returns [[{op}] ...]." + ([node] (tree->text-ops node 0 0 nil)) + ([node parent-x parent-y clip-bounds] + (let [{:keys [bounds style children text clip?]} node + abs-x (+ parent-x (:x bounds 0)) + abs-y (+ parent-y (:y bounds 0)) + w (:w bounds 0) + h (:h bounds 0) + visible? (if clip-bounds + (let [cx (:x clip-bounds) cy (:y clip-bounds) + cw (:w clip-bounds) ch (:h clip-bounds)] + (and (< abs-x (+ cx cw)) + (< abs-y (+ cy ch)) + (> (+ abs-x w) cx) + (> (+ abs-y h) cy))) + true)] + (when visible? + (let [;; Clip bounds: right + vertical (top/bottom) for text op filtering + clip-right (when clip-bounds (+ (:x clip-bounds) (:w clip-bounds))) + clip-top (when clip-bounds (:y clip-bounds)) + clip-bottom (when clip-bounds (+ (:y clip-bounds) (:h clip-bounds))) + truncate-op (fn [op] + (if (and clip-right (:text op)) + (let [ox (:x op 0) + fs (:size op 14) + cw (* fs 0.56) + avail (- clip-right ox) + max-chars (if (pos? cw) (max 0 (int (/ avail cw))) 1000) + txt (:text op)] + (if (> (count txt) max-chars) + (assoc op :text (subs txt 0 max-chars) :to max-chars) + op)) + op)) + in-clip? (fn [shifted] + (and (or (nil? clip-right) (< (:x shifted) clip-right)) + (or (nil? clip-top) (>= (:y shifted) clip-top)) + (or (nil? clip-bottom) (< (:y shifted) clip-bottom)))) + ;; Offset this node's text ops to absolute space + clip truncation + own-ops (when (seq text) + (mapv (fn [op] + (if (vector? op) + ;; op is already a vec of text-op maps (nested format) + (into [] (keep (fn [sub] + (let [shifted (-> sub + (update :x + abs-x) + (update :y + abs-y))] + (when (in-clip? shifted) + (truncate-op shifted))))) + op) + ;; Single text-op map + (let [shifted (-> op + (update :x + abs-x) + (update :y + abs-y))] + (when (in-clip? shifted) + [(truncate-op shifted)])))) + text)) + child-clip (if clip? + {:x abs-x :y abs-y :w w :h h} + clip-bounds) + child-ops (into [] (mapcat #(tree->text-ops % abs-x abs-y child-clip)) children)] + (into (vec (filterv some? (or own-ops []))) child-ops)))))) + +;; --- Tree walk: shadows ----------------------------------------------------- + +(defn tree->shadows + "Walk rect tree depth-first, emit flat vector of shadow maps. + Only nodes with :shadow in style produce shadows. + Shadow map keys: :x :y :w :h :blur :offset-x :offset-y :spread :color :radius :corner-radii" + ([node] (tree->shadows node 0 0)) + ([node parent-x parent-y] + (let [{:keys [bounds style children]} node + abs-x (+ parent-x (:x bounds 0)) + abs-y (+ parent-y (:y bounds 0)) + w (:w bounds 0) + h (:h bounds 0) + shadow-spec (:shadow style) + own-shadow (when shadow-spec + (let [s shadow-spec] + {:x abs-x :y abs-y :w w :h h + :blur (or (:blur s) 8.0) + :offset-x (or (:offset-x s) 0.0) + :offset-y (or (:offset-y s) 0.0) + :spread (or (:spread s) 0.0) + :color (or (:color s) [0 0 0 0.25]) + :radius (:radius style) + :corner-radii (:corner-radii style)})) + child-shadows (into [] (mapcat #(tree->shadows % abs-x abs-y)) children)] + (cond-> [] + own-shadow (conj own-shadow) + true (into child-shadows))))) + +;; --- Hit testing ------------------------------------------------------------ + +(defn hit-test + "Find the deepest node containing point (px, py). + Returns a vector of nodes from root to deepest hit [root ... leaf], + or nil if the point misses the tree entirely. + The LAST element is the deepest (innermost) hit — the event target. + Earlier elements are ancestors — used for bubbling." + ([node px py] (hit-test node px py 0 0)) + ([node px py parent-x parent-y] + (let [{:keys [bounds children]} node + abs-x (+ parent-x (:x bounds 0)) + abs-y (+ parent-y (:y bounds 0)) + w (:w bounds 0) + h (:h bounds 0)] + (when (and (>= px abs-x) (< px (+ abs-x w)) + (>= py abs-y) (< py (+ abs-y h))) + ;; Point is inside this node — check children (reverse order = front-to-back) + (let [child-hit (some (fn [child] + (hit-test child px py abs-x abs-y)) + (rseq children))] + (if child-hit + (into [node] child-hit) + [node])))))) + +;; --- Event dispatch with bubbling ------------------------------------------- + +(defn dispatch-event + "Dispatch an event to the hit-test path (innermost -> outermost). + event-type is a keyword (:click, :scroll, etc.). + event is the event data map. + path is the hit-test result [root ... target]. + Walks from target to root (bubbling). First handler that returns + a non-nil value stops propagation. Returns {:handled? bool :result any}." + [path event-type event] + (when (seq path) + (loop [nodes (rseq path)] ;; target first, root last + (if-let [node (first nodes)] + (let [handler (get-in node [:actions event-type])] + (if (and handler (fn? handler)) + (let [result (handler node event)] + (if (some? result) + {:handled? true :result result :node node} + (recur (rest nodes)))) ;; nil = let it bubble + (recur (rest nodes)))) + {:handled? false})))) diff --git a/src/app/client/workspace/runtime.cljs b/src/app/client/workspace/runtime.cljs new file mode 100644 index 0000000..e975a0e --- /dev/null +++ b/src/app/client/workspace/runtime.cljs @@ -0,0 +1,394 @@ +(ns app.client.workspace.runtime + "Thin shell: build runtime context, wire modules, join the reactive loop. + All business logic lives in workspace/runtime/* modules." + (:require [clojure.string :as str] + [clojure.set :as set] + [missionary.core :as m] + [app.client.workspace.events :as events] + [app.client.workspace.sidebar :refer [cmd-panel-h status-bar-h]] + [app.client.workspace.runtime.state :as state] + [app.client.workspace.runtime.fonts :as fonts] + [app.client.workspace.runtime.sidebar-io :as sidebar-io :refer [emit-settings-update!]] + [app.client.workspace.runtime.workspace-actions :as ws] + [app.client.workspace.runtime.interop :as interop] + [app.client.workspace.runtime.agent-flow :as agent-flow] + [app.client.workspace.runtime.scroll :as scroll] + [app.client.workspace.runtime.mouse :as mouse] + [app.client.workspace.runtime.keyboard :as kbd] + [app.client.workspace.runtime.render :as render])) + +(defn start-loop! + "Start the reactive editor loop. + Public API — signature unchanged. Builds the rt context, wires modules, + returns a Missionary task that runs the render loop." + [node device ctx geometry initial-line-lengths initial-lines + tokenize-fn layout-fn find-bracket-fn detect-folds-fn + find-form-fn eval-form-fn font-assets & {:keys [font-manifest gpu-budget !sidebar-visible !file-load-request !preview-el !remote-sidebar-truth !remote-settings-truth !remote-agent-trail !remote-flow-session !remote-workspace-truth initial-file]}] + + (let [;; Phase 2: Build the rt context map + rt (state/make-runtime-state + {:node node :device device :ctx ctx :geometry geometry :font-assets font-assets + :initial-lines initial-lines :font-manifest font-manifest + :gpu-budget gpu-budget + :!sidebar-visible !sidebar-visible + :!file-load-request !file-load-request + :!preview-el !preview-el}) + + atoms (:atoms rt) + layout (:layout rt) + gpu (:gpu rt) + + deps {:tokenize-fn tokenize-fn :layout-fn layout-fn + :find-bracket-fn find-bracket-fn :detect-folds-fn detect-folds-fn + :find-form-fn find-form-fn :eval-form-fn eval-form-fn} + + ;; ── Install watches & side effects ────────────────────────── + _ (fonts/install-font-watch! atoms) + _ (interop/install-extract-preview-watch! atoms) + + ;; ── Sidebar I/O ───────────────────────────────────────────── + io (sidebar-io/make-sidebar-io atoms) + _ (sidebar-io/install-sidebar-watch! atoms (:fetch-home-dirs! io)) + _ (sidebar-io/seed-initial-file! atoms io initial-file) + + ;; ── Rama truth sync (Electric → local sidebar state) ────── + ;; Live reconciliation: Rama truth -> !sidebar-truth. + ;; When truth catches up, clear matching pending optimistic entries. + apply-sidebar-truth! + (fn [truth] + (when (map? truth) + (let [!st (:!sidebar-truth atoms) + !so (:!sidebar-overlay atoms) + !ui (:!sidebar-ui atoms) + !cf (:!current-file atoms) + old-file @!cf] + ;; 1. Update committed truth + (swap! !st (fn [s] + (cond-> s + (contains? truth :project) + (assoc :project (:project truth)) + (contains? truth :expanded-dirs) + (assoc :expanded-dirs (:expanded-dirs truth)) + (contains? truth :selected-file) + (assoc :selected-file (:selected-file truth))))) + + ;; Sync !selected-artifact + !current-file from truth + (when (contains? truth :selected-file) + (let [sf (:selected-file truth)] + (reset! !cf sf) + (reset! (:!selected-artifact atoms) + (when (:path sf) + {:kind :file :path (:path sf) :name (:name sf)})))) + + ;; 2. Clear matched optimistic overlay state + (swap! !so (fn [overlay] + (let [proj-matched? (if (contains? overlay :pending-project) + (= (:path (:project truth)) (:path (:pending-project overlay))) + false) + ;; clear dirs from pending if they are now in truth + new-pending-exp (set/difference (:pending-expanded-dirs overlay) (or (:expanded-dirs truth) #{})) + ;; collpased means it's NOT in truth + new-pending-col (set/intersection (:pending-collapsed-dirs overlay) (or (:expanded-dirs truth) #{})) + file-matched? (if (contains? overlay :pending-selected-file) + (= (:path (:selected-file truth)) (:path (:pending-selected-file overlay))) + false)] + (cond-> overlay + proj-matched? (dissoc :pending-project) + true (assoc :pending-expanded-dirs new-pending-exp) + true (assoc :pending-collapsed-dirs new-pending-col) + file-matched? (dissoc :pending-selected-file))))) + + ;; 3. Rehydrate dir-cache based on new truth + (when-let [proj (:project truth)] + (let [proj-path (:path proj) + dirs (or (:expanded-dirs truth) #{})] + ;; Fetch project root if not cached + (when (and proj-path + (not (contains? (:dir-cache @!ui) proj-path))) + ((:fetch-dir! io) proj-path)) + ;; Fetch each expanded dir if not cached + (doseq [d dirs] + (when-not (contains? (:dir-cache @!ui) d) + ((:fetch-dir! io) d))))) + + ;; 4. Rehydrate file content if selected file changed + (when-let [sf (:selected-file truth)] + (let [proj-path (some-> (:project truth) :path)] + (when (and (:path sf) proj-path + (not= (:path sf) (:path old-file))) + ((:fetch-file! io) (:path sf) proj-path))))))) + + ;; Watch remote truth continuously and reconcile against optimistic overlay + _ (when !remote-sidebar-truth + (add-watch !remote-sidebar-truth :truth-sync + (fn [_ _ _ new-truth] + (apply-sidebar-truth! new-truth))) + ;; Apply current truth initially + (apply-sidebar-truth! @!remote-sidebar-truth)) + + ;; ── Settings persistence (Rama round-trip) ───────────────── + ;; 1. On load: apply persisted settings from Rama → !settings + ;; 2. After load: watch local !settings changes → fire-and-forget to Rama + ;; Suppression flag prevents the initial load from triggering a pointless POST. + !settings-loaded (atom false) + + _ (when !remote-settings-truth + (let [persistent-keys #{:font-size :line-height :px-range :sharpness + :snap-to-pixel? :show-diagnostics? :font-id :theme-id} + truth @!remote-settings-truth] + (when (and (map? truth) (seq truth)) + (let [persistent-fields (select-keys truth persistent-keys)] + (when (seq persistent-fields) + (swap! (:!settings atoms) merge persistent-fields) + (js/console.log "[SETTINGS-TRUTH] Initial load applied:" (pr-str (keys persistent-fields))) + ;; Sync !active-font if font-id was restored from truth. + ;; Normally !active-font → !settings (via font watch), but on + ;; load we need the reverse direction to apply the saved font. + (when-let [saved-font-id (:font-id persistent-fields)] + (let [manifest @(:!font-manifest atoms) + fonts (or (:fonts manifest) []) + available-fonts (filterv #(not (false? (:available %))) fonts) + font-config (first (filter #(= (:id %) saved-font-id) available-fonts)) + font-idx (when font-config + (first (keep-indexed + (fn [i f] (when (= (:id f) saved-font-id) i)) + available-fonts)))] + (when font-config + ;; Reset !active-font — this triggers install-font-watch! which + ;; synchronously writes font defaults into !settings. + (reset! (:!active-font atoms) + {:id (:id font-config) + :char-width (or (:charWidth font-config) 0.56) + :name (:name font-config)}) + ;; Re-apply persisted settings to undo the font-watch default + ;; overwrite. The watch fires synchronously above, so this merge + ;; restores the user's saved slider values over the font's defaults. + (swap! (:!settings atoms) merge persistent-fields) + ;; Reconcile the settings panel's local cursor state so the + ;; highlighted row matches the restored font, not the boot default. + (when font-idx + (swap! (:!settings atoms) assoc :selected-index font-idx)))))))))) + + ;; Persist local settings changes to Rama via fire-and-forget HTTP. + ;; Only fires after initial truth has loaded (suppresses the no-op echo). + _ (let [persistent-keys #{:font-size :line-height :px-range :sharpness + :snap-to-pixel? :show-diagnostics? :font-id :theme-id}] + (add-watch (:!settings atoms) :settings-persist + (fn [_ _ old-val new-val] + (when @!settings-loaded + (let [old-p (select-keys old-val persistent-keys) + new-p (select-keys new-val persistent-keys)] + (when (not= old-p new-p) + (let [changed (into {} (filter (fn [[k v]] (not= v (get old-p k))) new-p))] + (when (seq changed) + (emit-settings-update! changed)))))))) + ;; Enable persistence after the initial truth has been applied + (reset! !settings-loaded true)) + + ;; ── Agent trail restore (Rama → local) ─────────────────────── + ;; On load: if Rama has a saved trail and !agent-output is empty, + ;; restore it so the last run's trail survives page reload. + _ (when !remote-agent-trail + (let [saved @!remote-agent-trail] + (when (and (map? saved) (:trail-data saved) (nil? @(:!agent-output atoms))) + (let [{:keys [run-id trail-data]} saved] + (reset! (:!agent-output atoms) + {:status (or (:status trail-data) :complete) + :provider (or (:provider trail-data) :claude) + :prompt (or (:prompt trail-data) "") + :output "" + :run-id run-id + :trail (or (:trail trail-data) []) + :tool-buf {} + :structured-result (:structured-result trail-data)}) + (js/console.log "[TRAIL-TRUTH] Restored trail for run:" run-id))))) + + ;; ── Flow session persistence (Rama round-trip) ─────────────── + ;; On load: restore flow state from Rama if !flow-state is at :idle. + ;; After load: watch !flow-state for FSM node transitions → persist. + _ (when !remote-flow-session + (let [saved @!remote-flow-session + persistent-keys #{:node :tickets :batch :active-lane-idx + :runs :decisions :session-id :history}] + ;; Restore on load (only if idle — don't overwrite an active session) + (when (and (map? saved) (seq saved) + (= :idle (:node @(:!flow-state atoms)))) + (let [restored (select-keys saved persistent-keys) + ;; Reconstruct :selected from persisted :batch :lanes. + ;; set-selection keeps these in sync, so on restore we + ;; reverse it — without this, the UI shows "no selection" + ;; even though the batch has lanes. + lanes (get-in restored [:batch :lanes]) + restored (if (seq lanes) + (assoc restored :selected (vec lanes)) + restored)] + (when (and (seq restored) (not= :idle (:node restored))) + (swap! (:!flow-state atoms) merge restored) + (js/console.log "[FLOW-TRUTH] Restored flow state:" (pr-str (:node restored)))))) + ;; Persist when any persistent field changes (not just :node). + ;; Covers batch/lane updates from set-selection, session-id + ;; from agent events, etc. + (add-watch (:!flow-state atoms) :flow-persist + (fn [_ _ old-val new-val] + (let [old-p (select-keys old-val persistent-keys) + new-p (select-keys new-val persistent-keys)] + (when (not= old-p new-p) + (sidebar-io/save-flow-state! new-p))))))) + + ;; Reset detail scroll when ticket selection changes — centralized + ;; so all paths (mouse, keyboard, /flow-select command) are covered. + _ (add-watch (:!flow-state atoms) :selection-scroll-reset + (fn [_ _ old-val new-val] + (when (not= (:selected old-val) (:selected new-val)) + (reset! (:!detail-scroll-y atoms) 0)))) + + ;; ── Workspace truth persistence (Phase 7) ──────────────────── + ;; Restore on load: selected-artifact, active-pane, sidebar-visible. + ;; Persist on change: debounced to avoid intermediate states. + ;; Do NOT persist derived state (!effective-local-world, :split, :panes). + !workspace-loaded (atom false) + !workspace-persist-timer (atom nil) + + _ (when !remote-workspace-truth + (let [saved @!remote-workspace-truth] + (when (and (map? saved) (seq saved)) + ;; Restore selected-artifact + sync !current-file + (let [art (:selected-artifact saved)] + (reset! (:!selected-artifact atoms) art) + (if (and art (= :file (:kind art))) + (do (reset! (:!current-file atoms) {:path (:path art) :name (:name art)}) + (when-let [project (or (:path (:project @(:!sidebar-truth atoms))) + (some-> (:path art) (str/split #"/") butlast seq (#(str/join "/" %))))] + ((:fetch-file! io) (:path art) project))) + ;; Not a file or nil — clear !current-file to prevent stale identity + (reset! (:!current-file atoms) nil))) + ;; Restore active-pane via semantic action (syncs !focus + !caret-visible) + (when-let [pane (:active-pane saved)] + (ws/set-active-pane! atoms pane)) + ;; Restore sidebar-visible + (when (contains? saved :sidebar-visible) + (when (:!sidebar-visible atoms) + (reset! (:!sidebar-visible atoms) (:sidebar-visible saved)))) + (js/console.log "[WORKSPACE-TRUTH] Restored:" (pr-str (keys saved)))))) + + ;; Debounced persist: wait 16ms after last atom change so sequential + ;; mutations from one action (e.g. clear-artifact! sets both + ;; !selected-artifact and !active-pane) settle before saving. + _ (let [persist-workspace! + (fn [] + (when-let [timer @!workspace-persist-timer] + (js/clearTimeout timer)) + (reset! !workspace-persist-timer + (js/setTimeout + (fn [] + (reset! !workspace-persist-timer nil) + (when @!workspace-loaded + (let [truth {:selected-artifact @(:!selected-artifact atoms) + :active-pane @(:!active-pane atoms) + :sidebar-visible (boolean (and (:!sidebar-visible atoms) + @(:!sidebar-visible atoms)))}] + (sidebar-io/save-workspace-truth! truth)))) + 16)))] + (add-watch (:!selected-artifact atoms) :workspace-persist (fn [_ _ _ _] (persist-workspace!))) + (add-watch (:!active-pane atoms) :workspace-persist (fn [_ _ _ _] (persist-workspace!))) + (when (:!sidebar-visible atoms) + (add-watch (:!sidebar-visible atoms) :workspace-persist (fn [_ _ _ _] (persist-workspace!)))) + (reset! !workspace-loaded true)) + + ;; ── Effective local world (reactive derivation) ────────────── + ;; One derived object that answers "what world is the user in?" + ;; Recomputed when any of its inputs change. + recompute-local-world! + (fn [] + (let [sidebar-truth @(:!sidebar-truth atoms) + sidebar-overlay @(:!sidebar-overlay atoms)] + (reset! (:!effective-local-world atoms) + (ws/derive-effective-local-world + {:selected-artifact @(:!selected-artifact atoms) + :active-pane @(:!active-pane atoms) + :sidebar-visible (and (:!sidebar-visible atoms) + @(:!sidebar-visible atoms)) + :flow-state @(:!flow-state atoms) + :agent-output @(:!agent-output atoms) + :project (or (:pending-project sidebar-overlay) + (:project sidebar-truth))})))) + + _ (recompute-local-world!) + _ (doseq [a [:!selected-artifact :!active-pane :!flow-state :!agent-output]] + (add-watch (get atoms a) :local-world (fn [_ _ _ _] (recompute-local-world!)))) + _ (when (:!sidebar-visible atoms) + (add-watch (:!sidebar-visible atoms) :local-world (fn [_ _ _ _] (recompute-local-world!)))) + _ (add-watch (:!sidebar-truth atoms) :local-world (fn [_ _ _ _] (recompute-local-world!))) + _ (add-watch (:!sidebar-overlay atoms) :local-world (fn [_ _ _ _] (recompute-local-world!))) + + ;; ── Agent API (needs io for trigger-dev-replay!) ──────────── + trigger-replay! (fn [] (interop/trigger-dev-replay! atoms)) + agent-api (agent-flow/make-agent-api atoms trigger-replay!) + _ (interop/install-window-globals! atoms (:show-flow-info! agent-api)) + + ;; ── Event flows (fresh per instance) ──────────────────────── + >blink-timer (events/make-blink-timer) + >shimmer-timer (events/make-blink-timer) + >resize (events/>canvas-resize node) + >wheel-events (events/>wheel node) + >mouse-events (events/>mouse node) + >keyboard-events (events/>keyboard js/window) + + ;; ── Focus-based routing ───────────────────────────────────── + keyboard-events) + keyboard-events (:!focus atoms)) + keyboard-events (:!focus atoms)) + keyboard-events (:!focus atoms)) + keyboard-events (:!focus atoms)) + + ;; ── DOM listeners (raw, not Missionary) ───────────────────── + _ (mouse/install-drag-select! atoms layout node) + _ (mouse/install-paste-handler! atoms)] + + ;; ═══════════════════════════════════════════════════════════════ + ;; JOIN: all consumers run concurrently + ;; ═══════════════════════════════════════════════════════════════ + (m/join vector + ;; Timers + (->> >blink-timer + (m/reduce (fn [_ v] (reset! (:!caret-visible atoms) v) nil) nil)) + (->> >shimmer-timer + (m/reduce (fn [_ v] (reset! (:!shimmer-phase atoms) v) nil) nil)) + + ;; Viewport resize + (->> >resize + (m/reduce + (fn [_ {:keys [width height dpr]}] + (let [safe-width (max 1 width) + safe-height (max 1 height) + safe-dpr (or dpr 1) + backing-width (Math/floor (* safe-width safe-dpr)) + backing-height (Math/floor (* safe-height safe-dpr))] + (reset! (:!viewport atoms) {:width safe-width :height safe-height :dpr safe-dpr}) + (set! (.-width node) backing-width) + (set! (.-height node) backing-height) + ;; No .configure() here — changing canvas.width/height is sufficient. + ;; WebGPU auto-creates new swap chain textures on the next .getCurrentTexture(). + ;; Calling .configure() would unconfigure the context, blanking the canvas + ;; until a new frame is presented — causing a black screen flash or worse. + (js/console.log "[RESIZE] Canvas backing resized" + (str "{\"width\":" safe-width + ",\"height\":" safe-height + ",\"dpr\":" safe-dpr + ",\"backingWidth\":" backing-width + ",\"backingHeight\":" backing-height "}"))) + nil) + nil)) + + ;; Consumers from modules + (scroll/scroll-consumer atoms >wheel-events) + (mouse/mouse-consumer atoms layout deps io >mouse-events) + (kbd/global-keys-consumer atoms ao + (update :output str (:text evt)) + (update :trail conj {:kind :reasoning :text (:text evt)})))) + (auto-scroll-agent!)) + + :tool-use-start + (swap! !agent-output + (fn [ao] + (-> ao + (assoc-in [:tool-buf (:tool-id evt)] + {:tool-name (:tool-name evt) :json "" :block-idx (:block-idx evt)}) + (update :trail conj {:kind :tool-call-start + :tool-name (:tool-name evt) + :tool-id (:tool-id evt) + :block-idx (:block-idx evt)})))) + + :thinking-delta + (do (swap! !agent-output + (fn [ao] + (-> ao + (update :output str (:text evt)) + (update :trail conj {:kind :thinking :text (:text evt)})))) + (auto-scroll-agent!)) + + :thinking-start + nil + + :tool-input-delta + (if-let [tid (:tool-id evt)] + (swap! !agent-output + (fn [ao] + (update-in ao [:tool-buf tid :json] str (:json-chunk evt)))) + (js/console.warn "[AGENT] :tool-input-delta missing :tool-id" (clj->js evt))) + + :tool-result + (swap! !agent-output + (fn [ao] + (update ao :trail conj {:kind :tool-result + :tool-id (:tool-id evt) + :content (:content evt)}))) + + :block-stop + (let [ao @!agent-output + matching-tool (some (fn [[tid buf]] + (when (= (:block-idx buf) (:block-idx evt)) + [tid buf])) + (:tool-buf ao))] + (when matching-tool + (let [[tid buf] matching-tool + parsed-input (try (js/JSON.parse (:json buf)) + (catch :default _ nil))] + (swap! !agent-output + (fn [ao] + (-> ao + (update :trail conj {:kind :tool-call + :tool-name (:tool-name buf) + :tool-id tid + :input (js->clj parsed-input :keywordize-keys true) + :block-idx (:block-idx evt)}) + (update :tool-buf dissoc tid))))))) + + (:done :run-done) + (do (swap! !agent-output (fn [ao] + (cond-> (assoc ao :status (or (:status evt) :complete)) + (:result evt) (assoc :structured-result (:result evt))))) + (js/console.log "[AGENT][DONE]" (clj->js {:run-id run-id + :status (:status evt) + :has-result (some? (:result evt))})) + ;; Persist completed trail to Rama (fire-and-forget) + (let [ao @!agent-output] + (when (and run-id (:trail ao)) + (save-agent-trail! run-id + {:status (or (:status evt) :complete) + :provider (:provider ao) + :prompt (:prompt ao) + :trail (:trail ao) + :structured-result (:structured-result ao)}))) + (when on-done-fn (on-done-fn))) + + (:start :run-start) + (do (js/console.log "[AGENT][STREAM-START]" (clj->js evt)) + (when-let [sid (:session-id evt)] + (swap! !flow-state assoc :session-id sid))) + + :init + (do (js/console.log "[AGENT][INIT]" (clj->js evt)) + (when-let [sid (:session-id evt)] + (swap! !flow-state assoc :session-id sid))) + + :result + (do (js/console.log "[AGENT][RESULT] session-id:" (:session-id evt)) + (when-let [sid (:session-id evt)] + (swap! !flow-state assoc :session-id sid))) + + :run-error + (do (swap! !agent-output assoc :status :failed) + (js/console.error "[AGENT][RUN-ERROR]" (clj->js evt)) + (when on-done-fn (on-done-fn))) + + (js/console.warn "[AGENT][UNKNOWN-EVENT]" (clj->js evt)))))) + + show-flow-info! + (fn [msg] + (reset! !agent-scroll-y 0) + (reset! !agent-output {:status :complete + :provider @!ai-provider + :prompt "flow" + :output msg + :run-id nil})) + + fire-flow-run! + (fn [prompt-action & {:keys [on-done json-schema max-budget-usd model append-system-prompt]}] + (let [flow @!flow-state + {:keys [prompt]} (flow-prompt prompt-action flow) + provider @!ai-provider + run-id (str (random-uuid)) + session-id (:session-id flow) + request-body (cond-> {:run-id run-id + :provider provider + :prompt prompt + :cwd flow-cwd + :allowed-tools flow-allowed-tools + :context {:timestamp (js/Date.now)}} + session-id (assoc :session-id session-id) + json-schema (assoc :json-schema json-schema) + max-budget-usd (assoc :max-budget-usd max-budget-usd) + model (assoc :model model) + append-system-prompt (assoc :append-system-prompt append-system-prompt))] + (js/console.log "[FLOW][FIRE]" (clj->js {:action prompt-action + :node (:node flow) + :run-id run-id + :session-id session-id})) + (reset! !agent-scroll-y 0) + (reset! !agent-output {:status :running + :provider provider + :prompt (str "[" (name prompt-action) "]") + :output "" + :run-id run-id + :trail [] + :tool-buf {}}) + (stream-agent-run! + "/api/agent/stream" + request-body + (make-event-handler run-id on-done) + (fn [err] + (js/console.error "[FLOW][STREAM-ERROR]" err) + (reset! !agent-output {:status :failed + :provider provider + :prompt (str "[" (name prompt-action) "]") + :output (str "Stream error: " (.-message err)) + :run-id run-id + :trail [] + :tool-buf {}}) + (when on-done (on-done)))))) + + submit-agent-run! + (fn [cmd-text] + (let [shell-parsed (parse-agent-command cmd-text @!ai-provider) + parsed (if (= :workflow-command (:kind shell-parsed)) + (or (dg/parse-dg-command (:command shell-parsed)) + (jit/parse-jit-command (:command shell-parsed)) + {:kind :error + :message (str "Unknown command: " (:command shell-parsed))}) + shell-parsed) + doc @!editor-doc + file-path (:path @!current-file) + scroll-y @!scroll-y + viewport @!viewport + cwd (or (:path (:pending-project @!sidebar-overlay)) + (:path (:project @!sidebar-truth)) + (some-> file-path (str/split #"/") butlast seq (str/join "/")) + ".") + context {:cursor (:cursor doc) + :selection (:selection doc) + :visible-range [scroll-y (+ scroll-y (:height viewport))] + :file-path file-path + :timestamp (js/Date.now)}] + (case (:kind parsed) + :noop nil + + :replay (trigger-dev-replay!) + + :set-provider + (do (reset! !ai-provider (:provider parsed)) + (reset! !agent-scroll-y 0) + (reset! !agent-output {:status :complete + :provider (:provider parsed) + :prompt "provider" + :output (str "Provider set to " (-> (:provider parsed) name str/upper-case)) + :run-id nil})) + + :error + (do (reset! !agent-scroll-y 0) + (reset! !agent-output {:status :failed + :provider @!ai-provider + :prompt cmd-text + :output (:message parsed) + :run-id nil})) + + :run + (let [provider (:provider parsed) + prompt (:prompt parsed) + argv (:argv parsed) + run-id (str (random-uuid)) + request-body (cond-> {:run-id run-id + :provider provider + :prompt prompt + :cwd cwd + :file file-path + :context context} + (seq argv) (assoc :argv argv) + (:session-id @!flow-state) (assoc :session-id (:session-id @!flow-state)))] + (js/console.log "[AGENT][CLIENT][SUBMIT]" + (clj->js {:run-id run-id + :provider provider + :prompt prompt})) + (reset! !agent-scroll-y 0) + (reset! !agent-output {:status :running + :provider provider + :prompt prompt + :output "" + :run-id run-id + :trail [] + :tool-buf {}}) + (stream-agent-run! + "/api/agent/stream" + request-body + (make-event-handler run-id nil) + (fn [err] + (js/console.error "[AGENT][CLIENT][STREAM-ERROR]" err) + (reset! !agent-output {:status :failed + :provider provider + :prompt prompt + :output (str "Stream error: " (.-message err)) + :run-id run-id + :trail [] + :tool-buf {}})))) + + :extract-component + (jit/handle-jit-command! parsed + {:!extract-preview !extract-preview + :show-flow-info! show-flow-info!}) + + :hardcode + (jit/handle-jit-command! parsed + {:!extract-preview !extract-preview + :show-flow-info! show-flow-info!}) + + (:flow-bootstrap :flow-mock-bootstrap :flow-select :flow-arrange :flow-run + :flow-review :flow-rework :flow-finalize :flow-status :flow-reset) + (dg/handle-dg-command! parsed + {:!flow-state !flow-state + :!scroll-y !scroll-y + :show-flow-info! show-flow-info! + :fire-flow-run! fire-flow-run! + :enter-workflow! #(ws/enter-workflow! atoms) + :exit-workflow! #(ws/exit-workflow! atoms)}))))] + + {:auto-scroll-agent! auto-scroll-agent! + :make-event-handler make-event-handler + :fire-flow-run! fire-flow-run! + :show-flow-info! show-flow-info! + :submit-agent-run! submit-agent-run!})) diff --git a/src/app/client/workspace/runtime/fonts.cljs b/src/app/client/workspace/runtime/fonts.cljs new file mode 100644 index 0000000..48fa0ff --- /dev/null +++ b/src/app/client/workspace/runtime/fonts.cljs @@ -0,0 +1,163 @@ +(ns app.client.workspace.runtime.fonts + "Font manifest helpers and runtime font asset loading." + (:require [app.client.workspace.settings-view :refer [font-defaults->settings]])) + +(def ^:private base-path "/fonts/") +(declare resolve-default-font-config) + +(defn load-font-manifest-async [] + "Load the font manifest from the fonts directory." + (-> (js/fetch (str base-path "manifest.json")) + (.then #(.json %)) + (.then #(js->clj % :keywordize-keys true)) + (.then (fn [manifest] + (js/console.log "[FONT] Manifest loaded" + {:font-count (count (:fonts manifest)) + :default-font (:id (resolve-default-font-config manifest)) + :settings-keys (keys (:settings manifest))}) + manifest)) + (.catch + (fn [e] + (js/console.error "[FONT] Manifest load failed, using fallback manifest" e) + {:fonts [{:name "Ubuntu Sans Mono" + :id "ubuntu-sans-mono" + :atlas "ubuntu_sans_mono_atlas.png" + :metrics "ubuntu_sans_mono_atlas.json" + :charWidth 0.56 + :default true + :defaults {:fontSize 19 + :lineHeight 1.2 + :pxRange 8 + :sharpness 0.0 + :snapToPixel true + :showDiagnostics false}}] + :settings {:fontSize {:default 19} + :lineHeight {:default 1.2} + :pxRange {:default 8} + :sharpness {:default 0.0} + :snapToPixel {:default true} + :showDiagnostics {:default false}}})))) + +(defn available-fonts [manifest] + (filterv #(not (false? (:available %))) (:fonts manifest))) + +(defn resolve-default-font-config [manifest] + (let [fonts (available-fonts manifest)] + (or (first (filter :default fonts)) + (first fonts) + {:name "DejaVu Sans Mono" + :id "dejavu-sans-mono" + :charWidth 0.56}))) + +(defn- fetch-json [url] + (-> (js/fetch url) + (.then #(.json %)) + (.then #(js->clj % :keywordize-keys true)))) + +(defn- fetch-bitmap [url] + (-> (js/fetch url) + (.then #(.blob %)) + (.then #(js/createImageBitmap %)))) + +(defn- fetch-bytes [url] + (-> (js/fetch url) + (.then #(.arrayBuffer %)))) + +(defn load-font-assets + "Load the runtime assets for a font config. Slug-enabled fonts still load + their MSDF bundle so the old path remains available as a fallback." + [font-config] + (let [msdf-promises (cond-> [] + (:atlas font-config) + (conj (fetch-bitmap (str base-path (:atlas font-config)))) + + (:metrics font-config) + (conj (fetch-json (str base-path (:metrics font-config))))) + slug-config (:slug font-config) + slug-promises (cond-> [] + (:meta slug-config) + (conj (fetch-json (str base-path (:meta slug-config)))) + + (:curve slug-config) + (conj (fetch-bytes (str base-path (:curve slug-config)))) + + (:band slug-config) + (conj (fetch-bytes (str base-path (:band slug-config)))))] + (-> (js/Promise.all (clj->js (concat msdf-promises slug-promises))) + (.then + (fn [assets] + (let [msdf-asset-count (count msdf-promises) + bitmap (when (:atlas font-config) (aget assets 0)) + atlas (when (:metrics font-config) + (aget assets (if (:atlas font-config) 1 0))) + slug-start msdf-asset-count + slug-meta (when (:meta slug-config) (aget assets slug-start)) + slug-curve (when (:curve slug-config) (aget assets (+ slug-start (if (:meta slug-config) 1 0)))) + slug-band (when (:band slug-config) + (aget assets (+ slug-start + (count (filter some? [(:meta slug-config) (:curve slug-config)])))) + ) + slug-ready? (and slug-meta slug-curve slug-band) + preferred-backend (keyword (or (:preferredBackend font-config) "msdf")) + active-backend (if (and (= preferred-backend :slug) slug-ready?) + :slug + :msdf)] + (js/console.log "[FONT] Asset resolution" + {:id (:id font-config) + :preferred-backend preferred-backend + :active-backend active-backend + :has-msdf? (boolean (and bitmap atlas)) + :has-slug-config? (boolean slug-config) + :slug-ready? (boolean slug-ready?)}) + {:id (:id font-config) + :name (:name font-config) + :backend active-backend + :bitmap bitmap + :atlas atlas + :msdf (when (and bitmap atlas) + {:bitmap bitmap :atlas atlas}) + :slug (when slug-ready? + {:meta slug-meta + :curve-bytes slug-curve + :band-bytes slug-band})})))))) + +(defn load-default-font-data-async [] + (-> (load-font-manifest-async) + (.then + (fn [manifest] + (let [font-config (resolve-default-font-config manifest)] + (js/console.log "[FONT] Loading default font" + {:id (:id font-config) + :name (:name font-config) + :preferred-backend (:preferredBackend font-config)}) + (-> (load-font-assets font-config) + (.then + (fn [font-assets] + (js/console.log "[FONT] Default font ready" + {:id (:id font-config) + :backend (:backend font-assets)}) + {:font-manifest manifest + :font-config font-config + :font-assets font-assets})))))))) + +(defn install-font-watch! + "Watch !active-font for id changes; apply defaults and async-load new font assets." + [{:keys [!active-font !font-manifest !font-assets !settings]}] + (add-watch !active-font :font-loader + (fn [_ _ old-val new-val] + (when (not= (:id old-val) (:id new-val)) + (let [manifest @!font-manifest + font-config (first (filter #(= (:id %) (:id new-val)) (:fonts manifest)))] + (js/console.log "[FONT] Loading font:" (:id new-val) font-config) + (when font-config + (swap! !settings assoc :font-id (:id font-config)) + (when-let [defaults (font-defaults->settings font-config)] + (swap! !settings merge defaults)) + (-> (load-font-assets font-config) + (.then + (fn [assets] + (js/console.log "[FONT] Loaded assets for:" (:id new-val) "backend=" (name (:backend assets))) + (reset! !font-assets assets))) + (.catch + (fn [err] + (js/console.error "[FONT] Failed to load:" err)))))))))) diff --git a/src/app/client/workspace/runtime/interop.cljs b/src/app/client/workspace/runtime/interop.cljs new file mode 100644 index 0000000..77c5f96 --- /dev/null +++ b/src/app/client/workspace/runtime/interop.cljs @@ -0,0 +1,156 @@ +(ns app.client.workspace.runtime.interop + "Dev interop: replay fixture, window globals for Claude-in-Chrome, extract preview overlay." + (:require [cljs.reader :as reader] + [app.client.workspace.trail :refer [compute-agent-panel-h agent-wrapped-line-count]])) + +(defn install-extract-preview-watch! + "Watch !extract-preview; show/hide DOM overlay + populate HTML." + [{:keys [!extract-preview !preview-el]}] + (add-watch !extract-preview :overlay + (fn [_ _ _ new-val] + (when-let [el (and !preview-el @!preview-el)] + (if new-val + (do (set! (.-display (.-style el)) "block") + (if-let [html (:html new-val)] + (set! (.-innerHTML el) + (str "
Source: " + (or (:source-url new-val) "extracted") "
" + "
" + html "
")) + (set! (.-innerHTML el) + "
No HTML preview available.
Run extractor to capture source HTML.
"))) + (do (set! (.-display (.-style el)) "none") + (set! (.-innerHTML el) ""))))))) + +(defn- auto-scroll-agent-impl! + "Scroll agent output to bottom. Used by replay." + [{:keys [!agent-output !viewport !settings !active-font !agent-scroll-y]}] + (let [ao @!agent-output + viewport @!viewport + settings @!settings + font-size (:font-size settings) + char-advance (* font-size (:char-width @!active-font)) + agent-h (compute-agent-panel-h ao font-size (:height viewport) + (:width viewport) char-advance) + line-step (* font-size 1.2) + max-chars (if (pos? char-advance) + (max 1 (int (/ (- (:width viewport) 48) char-advance))) + 80) + line-count (agent-wrapped-line-count ao max-chars) + total-h (* line-count line-step) + max-scroll (max 0 (- total-h (- agent-h 16)))] + (reset! !agent-scroll-y max-scroll))) + +(defn trigger-dev-replay! + "Fire the dev replay fixture — replays SSE events with staggered timeouts." + [atoms] + (let [{:keys [!agent-output !agent-scroll-y]} atoms] + (reset! !agent-scroll-y 0) + (reset! !agent-output {:status :running + :provider :claude + :prompt "Replay Fixture" + :output "" + :run-id "replay-dev" + :trail [] + :tool-buf {}}) + (-> (js/fetch "/api/dev/replay-fixture") + (.then (fn [resp] (.json resp))) + (.then (fn [json] + (let [data (js->clj json :keywordize-keys true)] + (if (:ok data) + (let [events (:events data)] + (doseq [[i evt] (map-indexed vector events)] + (js/setTimeout + (fn [] + (let [kind (:event evt)] + (case kind + :text-delta + (do (swap! !agent-output + (fn [ao] + (-> ao + (update :output str (:text evt)) + (update :trail conj {:kind :reasoning :text (:text evt)})))) + (auto-scroll-agent-impl! atoms)) + + :tool-use-start + (swap! !agent-output + (fn [ao] + (-> ao + (assoc-in [:tool-buf (:tool-id evt)] + {:tool-name (:tool-name evt) :json "" :block-idx (:block-idx evt)}) + (update :trail conj {:kind :tool-call-start + :tool-name (:tool-name evt) + :tool-id (:tool-id evt) + :block-idx (:block-idx evt)})))) + + :tool-input-delta + (if-let [tid (:tool-id evt)] + (swap! !agent-output + (fn [ao] + (update-in ao [:tool-buf tid :json] str (:json-chunk evt)))) + (js/console.warn "[DEV][REPLAY] :tool-input-delta missing :tool-id" (clj->js evt))) + + :tool-result + (swap! !agent-output + (fn [ao] + (update ao :trail conj {:kind :tool-result + :tool-id (:tool-id evt) + :content (:content evt)}))) + + :block-stop + (let [ao @!agent-output + matching-tool (some (fn [[tid buf]] + (when (= (:block-idx buf) (:block-idx evt)) + [tid buf])) + (:tool-buf ao))] + (when matching-tool + (let [[tid buf] matching-tool + parsed-input (try (js/JSON.parse (:json buf)) + (catch :default _ nil))] + (swap! !agent-output + (fn [ao] + (-> ao + (update :trail conj {:kind :tool-call + :tool-name (:tool-name buf) + :tool-id tid + :input (js->clj parsed-input :keywordize-keys true) + :block-idx (:block-idx evt)}) + (update :tool-buf dissoc tid))))))) + + (:result :run-done) + (swap! !agent-output assoc :status :complete) + + :run-error + (swap! !agent-output assoc :status :failed) + + (js/console.warn "[DEV][UNKNOWN-EVENT]" (clj->js evt))))) + (* i 50)))) + (js/console.error "[DEV] Replay failed:" (:error data)))))) + (.catch (fn [err] (js/console.error "[DEV] Replay fetch error:" err)))))) + +(defn install-window-globals! + "Register __softland_inject_preview and __softland_inject_rt_node on window." + [{:keys [!extract-preview]} show-flow-info!] + (set! (.-__softland_inject_preview js/window) + (fn [data-json] + (let [data (js->clj (.parse js/JSON data-json) :keywordize-keys true)] + (-> (js/fetch "/api/extract/compile" + (clj->js {:method "POST" + :headers {"Content-Type" "application/edn"} + :body (pr-str {:tree data :source-url (.-href js/location)})})) + (.then #(.text %)) + (.then (fn [text] + (let [result (reader/read-string text)] + (if (:ok result) + (do (reset! !extract-preview {:rt-node (:rt-node result) + :ir (:ir result) + :source-url (:source-url result)}) + (show-flow-info! (str "Component compiled.\nSource: " (:source-url result)))) + (show-flow-info! (str "Compile failed: " (:error result))))))) + (.catch (fn [err] + (show-flow-info! (str "Compile error: " (.-message err))))))))) + (set! (.-__softland_inject_rt_node js/window) + (fn [rt-node-json] + (let [rt-node (js->clj (.parse js/JSON rt-node-json) :keywordize-keys true)] + (reset! !extract-preview {:rt-node rt-node}) + (show-flow-info! "rt-node injected directly."))))) diff --git a/src/app/client/workspace/runtime/keyboard.cljs b/src/app/client/workspace/runtime/keyboard.cljs new file mode 100644 index 0000000..ba10610 --- /dev/null +++ b/src/app/client/workspace/runtime/keyboard.cljs @@ -0,0 +1,420 @@ +(ns app.client.workspace.runtime.keyboard + "Keyboard consumers: global, editor, command panel, chat, settings, file-load." + (:require [clojure.string :as str] + [missionary.core :as m] + [app.client.workspace.events :refer [maybe-snap]] + [app.client.workspace.sidebar :refer [cmd-panel-h status-bar-h]] + [app.client.workspace.text-input :as text-input] + [app.client.workspace.themes :as themes] + [app.client.workspace.cmd-panel :refer [cmd-panel-apply-event]] + [app.client.workspace.editor-compute :refer [editor-apply-event]] + [app.client.workspace.settings-view :refer [slider-specs font-defaults->settings]] + [app.client.workspace.runtime.state :refer [save-undo!]] + [app.client.workspace.runtime.sidebar-io :as sio] + [app.client.workspace.runtime.workspace-actions :as ws] + [app.client.workflows.dg-flow :refer [group-tickets-by-status set-selection]])) + +;; ───────────────────────────────────────────────── +;; GLOBAL KEYS +;; ───────────────────────────────────────────────── + +(defn global-keys-consumer + [{:keys [!cmd-panel !settings !focus !caret-visible !editor-doc !sidebar-visible + !current-file !effective-local-world !agent-output !active-pane !chat-input] + :as atoms} + > > (m/watch !file-load-request) + (m/eduction (filter some?)) + (m/reduce + (fn [_ request] + (let [{:keys [lines target-line]} request + target-line (or target-line 0)] + (when (seq lines) + (let [safe-line (min target-line (max 0 (dec (count lines))))] + (js/console.log "[FILE-LOAD] Loading file with" (count lines) "lines, target:" safe-line) + (reset! !editor-doc {:lines (vec lines) + :cursor {:line safe-line :col 0} + :selection nil + :desired-col 0}) + (let [font-size (:font-size @!settings) + line-h (* font-size (:line-height @!settings)) + target-y (* safe-line line-h)] + (reset! !scroll-y (max 0 (- target-y 100)))) + (reset! !scroll-x 0) + (reset! !undo-stack []) + (reset! !redo-stack []) + (reset! !folded-lines #{}) + (reset! !caret-visible true) + (when-not (:visible @!cmd-panel) (reset! !focus :editor)) + (reset! !file-load-request nil)))) + nil) + nil)) + (m/reduce (fn [_ _] nil) nil (m/seed [nil])))) + +;; ───────────────────────────────────────────────── +;; EDITOR KEYS +;; ───────────────────────────────────────────────── + +(defn editor-keys-consumer + [{:keys [!editor-doc !caret-visible !clipboard !undo-stack !redo-stack + !eval-result !scroll-y !viewport !settings !active-font !current-file]} + {:keys [layout-y]} + {:keys [find-form-fn eval-form-fn]} + > (+ caret-y line-h) (- viewport-bottom padding)) + (+ (- caret-y visible-h) line-h padding) + :else scroll-y) + new-scroll (maybe-snap new-scroll dpr snap?)] + (reset! !editor-doc new-doc) + (reset! !scroll-y new-scroll) + (reset! !caret-visible true)) + + :copy + (let [input {:lines (:lines doc) :cursor (:cursor doc) :selection (:selection doc)} + text (text-input/copy input true)] + (when text + (reset! !clipboard text) + (js/console.log "Copied:" text))) + + :cut + (let [input {:lines (:lines doc) :cursor (:cursor doc) :selection (:selection doc)} + result (text-input/cut input true)] + (when (:text result) + (save-undo! {:!undo-stack !undo-stack :!redo-stack !redo-stack} + (:lines doc) (:cursor doc)) + (reset! !clipboard (:text result)) + (let [new-doc (merge doc (:state result))] + (reset! !editor-doc new-doc) + (when-let [fp (:path @!current-file)] + (sio/save-editor-doc! fp {:lines (:lines new-doc)}))) + (js/console.log "Cut:" (:text result)))) + + :undo + (when-let [prev (peek @!undo-stack)] + (swap! !redo-stack conj {:lines (:lines doc) :cursor (:cursor doc)}) + (swap! !undo-stack pop) + (reset! !editor-doc (merge doc {:lines (:lines prev) + :cursor (:cursor prev) + :selection nil + :desired-col (:col (:cursor prev))})) + (reset! !caret-visible true) + (when-let [fp (:path @!current-file)] + (sio/save-editor-doc! fp {:lines (:lines prev)}))) + + :redo + (when-let [next-state (peek @!redo-stack)] + (swap! !undo-stack conj {:lines (:lines doc) :cursor (:cursor doc)}) + (swap! !redo-stack pop) + (reset! !editor-doc (merge doc {:lines (:lines next-state) + :cursor (:cursor next-state) + :selection nil + :desired-col (:col (:cursor next-state))})) + (reset! !caret-visible true) + (when-let [fp (:path @!current-file)] + (sio/save-editor-doc! fp {:lines (:lines next-state)}))) + + :eval + (when-let [pos (:cursor doc)] + (if-let [form-info (find-form-fn pos (:lines doc) lengths)] + (let [result-text (eval-form-fn (:form-str form-info))] + (js/console.log "SCI Eval:" (:form-str form-info) "=>" result-text) + (reset! !eval-result {:text result-text + :line (:end-line form-info) + :expires-at (+ (js/Date.now) 5000)})) + (do (js/console.log "SCI: No form at cursor") + (reset! !eval-result {:text "No form at cursor" + :line (:line pos) + :expires-at (+ (js/Date.now) 2000)})))) + nil))) + nil) + nil))) + +;; ───────────────────────────────────────────────── +;; COMMAND PANEL KEYS +;; ───────────────────────────────────────────────── + +(defn command-keys-consumer + [{:keys [!cmd-panel !flow-state !hovered-row-idx !focus !caret-visible + !clipboard !effective-local-world]} + submit-agent-run! + pos (fn [{:keys [x y]}] + (let [scroll-y @!scroll-y + sb-w (if (and !sidebar-visible @!sidebar-visible) sidebar-w 0) + local-x (- x sb-w) + dpr (:dpr @!viewport) + snap? (:snap-to-pixel? @!settings) + font-size (:font-size @!settings) + char-width (:char-width @!active-font) + line-h (maybe-snap (* font-size (:line-height @!settings)) dpr snap?) + char-w (maybe-snap (* font-size char-width) dpr snap?) + adj-y (+ y scroll-y) + lx (maybe-snap (- layout-x (or @!scroll-x 0)) dpr snap?) + ly (maybe-snap layout-y dpr snap?) + text-result @!text-geo + line-mapping (or (:line-mapping text-result) []) + lengths (mapv count (:lines @!editor-doc)) + visual-line (max 0 (Math/floor (/ (- adj-y ly) line-h))) + logical-line (get line-mapping visual-line + (min visual-line (dec (count lengths)))) + line-len (get lengths logical-line 0) + col (-> (/ (- local-x lx) char-w) + (Math/round) + (max 0) + (min line-len))] + {:line logical-line :col col}))] + (.addEventListener node "mousedown" + (fn [e] + (let [coords (get-coords e) + {:keys [x]} coords + sb-w (if (and !sidebar-visible @!sidebar-visible) sidebar-w 0) + local-x (- x sb-w) + local-world @!effective-local-world + content-w (- (:width @!viewport) sb-w) + code-w (int (* content-w (ws/pane-width-pct local-world :main 0.4))) + in-editor? (and (ws/local-world-file-workspace? local-world) (< local-x code-w)) + in-normal-editor? (and (not (ws/local-world-file-workspace? local-world)) + (not (ws/local-world-flow? local-world)))] + (when (or in-editor? in-normal-editor?) + (let [pos (mouse->pos coords)] + (reset! !drag-start pos) + (reset! !dragging? true) + (swap! !editor-doc assoc + :cursor pos :selection nil :desired-col (:col pos)) + (reset! !focus :editor) + (reset! !caret-visible true)))))) + (.addEventListener js/window "mousemove" + (fn [e] + (when @!dragging? + (let [pos (mouse->pos (get-coords e)) + start @!drag-start] + (when (and start (not= pos start)) + (swap! !editor-doc assoc + :selection {:start start :end pos})))))) + (.addEventListener js/window "mouseup" + (fn [_] + (reset! !dragging? false) + (reset! !drag-start nil))))) + +(def ^:private flow-cwd "/home/sid/projects/discourse-graph") + +;; ═══════════════════════════════════════════════════════════════════════ +;; Mousedown branch helpers +;; ═══════════════════════════════════════════════════════════════════════ + +(defn- handle-settings-click! + "Route click within the settings panel overlay." + [{:keys [!settings !font-manifest !active-font]} x y panel-x panel-y settings] + (let [left-w 220 + header-h 40 + content-y (+ panel-y header-h) + font-item-h 32 + slider-item-h 58 + slider-count (count (slider-specs settings)) + rel-x (- x panel-x) + rel-y (- y content-y)] + (cond + (< rel-y 0) nil + (< rel-x left-w) + (let [font-idx (int (/ rel-y font-item-h)) + fonts (or (:fonts @!font-manifest) + [{:name "DejaVu Sans Mono" :id "dejavu-sans-mono"}]) + available-fonts (filterv #(not (false? (:available %))) fonts)] + (when (< font-idx (count available-fonts)) + (swap! !settings assoc :selected-index font-idx :focus-section :fonts) + (let [selected-font (nth available-fonts font-idx)] + (reset! !active-font {:id (:id selected-font) + :char-width (or (:charWidth selected-font) 0.56) + :name (:name selected-font)})))) + :else + (let [slider-idx (int (/ rel-y slider-item-h))] + (when (< slider-idx slider-count) + (swap! !settings assoc :slider-index slider-idx :focus-section :sliders)))))) + +(defn- handle-sidebar-click! + "Route click within the sidebar file explorer. + Reads from the shared sidebar scene (cached by render flow) — same tree + that produced the current frame's rects. No redundant tree rebuild. + Optimistic local updates for instant UI, fire-and-forget POST to Rama. + Committed truth flows back via Electric subscription." + [{:keys [!sidebar-truth !sidebar-overlay !sidebar-ui !current-file !sidebar-scene !settings !active-font] + :as atoms} + {:keys [fetch-dir! fetch-file!]} + x y viewport scroll-y] + (let [t0 (js/performance.now) + tree @!sidebar-scene + path (when tree (hit-test tree x (+ y scroll-y))) + t1 (js/performance.now)] + (js/console.log "[SIDEBAR-CLICK] hit-test:" (.toFixed (- t1 t0) 2) "ms | hit:" (some? path)) + (when path + (some (fn [node] + (case (:type node) + :sidebar-entry + (let [d (:data node) + et (:entry-type d)] + (js/console.log "[SIDEBAR-CLICK] matched:" (str et) "| node-id:" (str (:id node))) + (case et + :back-btn + (do + (ws/clear-artifact! atoms) + (swap! !sidebar-overlay assoc + :pending-project {:path nil} + :pending-expanded-dirs #{} + :pending-collapsed-dirs #{} + :pending-selected-file {:path nil}) + (swap! !sidebar-ui assoc :dir-cache {} :scroll-y 0) + (emit-sidebar-action! :sidebar/project-back {} nil) + (js/console.log "[SIDEBAR-CLICK] back total:" (.toFixed (- (js/performance.now) t0) 2) "ms") + true) + :home-dir + (let [entry (:entry d)] + (ws/clear-artifact! atoms) + (swap! !sidebar-overlay assoc + :pending-project {:name (:name entry) :path (:path entry)} + :pending-expanded-dirs #{} + :pending-collapsed-dirs #{} + :pending-selected-file {:path nil}) + (swap! !sidebar-ui assoc :dir-cache {} :scroll-y 0) + (emit-sidebar-action! :sidebar/project-select + {:name (:name entry) :path (:path entry)} nil) + (fetch-dir! (:path entry)) + (js/console.log "[SIDEBAR-CLICK] home-dir total:" (.toFixed (- (js/performance.now) t0) 2) "ms") + true) + :dir + (let [entry (:entry d) dir-path (:path entry) + truth-exp (or (:expanded-dirs @!sidebar-truth) #{}) + overlay @!sidebar-overlay + eff-exp (clojure.set/difference + (clojure.set/union truth-exp (:pending-expanded-dirs overlay)) + (:pending-collapsed-dirs overlay)) + expanding? (not (contains? eff-exp dir-path))] + (if expanding? + (swap! !sidebar-overlay (fn [o] (-> o + (update :pending-expanded-dirs conj dir-path) + (update :pending-collapsed-dirs disj dir-path)))) + (swap! !sidebar-overlay (fn [o] (-> o + (update :pending-collapsed-dirs conj dir-path) + (update :pending-expanded-dirs disj dir-path))))) + (emit-sidebar-action! :sidebar/dir-toggle {:path dir-path} nil) + (when expanding? + (fetch-dir! dir-path)) + (js/console.log "[SIDEBAR-CLICK] dir-toggle total:" (.toFixed (- (js/performance.now) t0) 2) "ms") + true) + :file + (let [entry (:entry d) + project (or (:pending-project @!sidebar-overlay) (:project @!sidebar-truth))] + ;; Semantic selection — single entry point + (ws/select-artifact! atoms {:kind :file :path (:path entry) :name (:name entry)}) + ;; Sidebar overlay + Rama persistence (existing flow) + (swap! !sidebar-overlay assoc :pending-selected-file {:path (:path entry) :name (:name entry)}) + (emit-sidebar-action! :sidebar/file-select + {:path (:path entry) :name (:name entry)} nil) + (fetch-file! (:path entry) (:path project)) + (js/console.log "[SIDEBAR-CLICK] file-select total:" (.toFixed (- (js/performance.now) t0) 2) "ms") + true) + nil)) + nil)) + (rseq path))))) + +(defn- handle-cmd-click! + "Place cursor in the command panel." + [{:keys [!focus !cmd-panel !caret-visible !settings !viewport !active-font] :as atoms} + x sb-vis? cmd-panel] + (let [font-size (:font-size @!settings) + dpr (:dpr @!viewport) + snap? (:snap-to-pixel? @!settings) + char-width (:char-width @!active-font) + char-w (maybe-snap (* font-size char-width) dpr snap?) + sb-w (if sb-vis? sidebar-w 0) + text-x (+ (cmd-text-start-x @(get atoms :!ai-provider) font-size char-width dpr snap?) sb-w) + text (:text cmd-panel) + col (-> (/ (- x text-x) char-w) (Math/round) (max 0) (min (count text)))] + (reset! !focus :command-panel) + (swap! !cmd-panel assoc :cursor col) + (reset! !caret-visible true))) + +(defn- handle-editor-click! + "Place cursor or toggle fold in the editor gutter/body. Shared by file-layout and normal mode." + [{:keys [!viewport !settings !active-font !scroll-x !text-geo !editor-doc + !folded-lines !drag-start !caret-visible !focus]} + {:keys [detect-folds-fn]} + layout-x layout-y gutter-w x adj-y local-x] + (let [dpr (:dpr @!viewport) + snap? (:snap-to-pixel? @!settings) + font-size (:font-size @!settings) + char-width (:char-width @!active-font) + line-h (maybe-snap (* font-size (:line-height @!settings)) dpr snap?) + char-w (maybe-snap (* font-size char-width) dpr snap?) + elx (maybe-snap (- layout-x (or @!scroll-x 0)) dpr snap?) + ely (maybe-snap layout-y dpr snap?) + gutter-x (- elx gutter-w) + gutter-right (+ gutter-x gutter-w)] + (if (and (>= local-x gutter-x) (< local-x gutter-right)) + ;; Gutter click — toggle fold + (let [text-result @!text-geo + line-mapping (or (:line-mapping text-result) []) + visual-line (max 0 (Math/floor (/ (- adj-y ely) line-h))) + logical-line (get line-mapping visual-line visual-line) + regions (detect-folds-fn (:lines @!editor-doc) (mapv count (:lines @!editor-doc))) + fold-region (first (filter #(= (:start-line %) logical-line) (or regions [])))] + (when fold-region + (swap! !folded-lines + (fn [folded] + (if (contains? folded logical-line) + (disj folded logical-line) (conj folded logical-line))))) + (reset! !focus :editor)) + ;; Body click — place cursor + (let [text-result @!text-geo + line-mapping (or (:line-mapping text-result) []) + lengths (mapv count (:lines @!editor-doc)) + visual-line (max 0 (Math/floor (/ (- adj-y ely) line-h))) + logical-line (get line-mapping visual-line (min visual-line (dec (count lengths)))) + line-len (get lengths logical-line 0) + col (-> (/ (- local-x elx) char-w) (Math/round) (max 0) (min line-len)) + pos {:line logical-line :col col}] + (when-not @!drag-start + (swap! !editor-doc assoc :cursor pos :selection nil :desired-col col) + (reset! !caret-visible true) + (reset! !focus :editor)))))) + +(defn- handle-chat-click! + "Route click within the chat pane: nav links and tool collapse toggles." + [{:keys [!settings !active-font !shimmer-phase !current-file !effective-local-world !agent-output + !trail-collapsed !active-pane !chat-scroll-y !chat-input !focus + !editor-doc !scroll-y !sidebar-truth] + :as atoms} + {:keys [fetch-file!]} + rel-x y content-w viewport scroll-y] + (let [file-layout-h (- (:height viewport) cmd-panel-h status-bar-h)] + (when (< y (- file-layout-h 36)) + (let [font-size (:font-size @!settings) + char-advance (* font-size (:char-width @!active-font)) + shimmer-alpha (if @!shimmer-phase 0.9 0.4) + tree (resolve-layout + (build-file-layout content-w file-layout-h + @!current-file @!agent-output font-size + shimmer-alpha @!trail-collapsed + :local-world @!effective-local-world + :active-pane @!active-pane :char-advance char-advance + :chat-scroll-y (or @!chat-scroll-y 0) + :chat-input @!chat-input :focus @!focus)) + path (hit-test tree rel-x y)] + (when path + (let [handled-nav? + (some (fn [node] + (when-let [nav (:nav (:data node))] + (let [file-path (:file-path nav) + target-line (or (:line nav) 0) + current-path (:path @!current-file) + project (or (:path (:project @!sidebar-truth)) flow-cwd) + same-file? (or (= file-path current-path) + (and current-path + (str/ends-with? current-path file-path)))] + (if same-file? + (let [safe-line (min target-line + (max 0 (dec (count (:lines @!editor-doc)))))] + (swap! !editor-doc assoc + :cursor {:line safe-line :col 0} + :selection nil :desired-col 0) + (let [line-h (* font-size (:line-height @!settings))] + (reset! !scroll-y (max 0 (- (* safe-line line-h) 100))))) + (fetch-file! file-path project :target-line target-line)) + (ws/set-active-pane! atoms :editor) + true))) + (rseq path))] + (when-not handled-nav? + (some (fn [node] + (when (= :tool-header (:type node)) + (when-let [cid (:collapse-id (:data node))] + (swap! !trail-collapsed + (fn [s] (if (contains? s cid) (disj s cid) (conj s cid)))) + true))) + (rseq path))))))))) + +(defn- handle-flow-canvas-click! + "Route click within the DG flow canvas (group headers + ticket rows)." + [{:keys [!flow-state !collapsed-groups !hovered-row-idx !drag-state !settings !active-font]} + content-x y content-w viewport-h scroll-y] + (let [flow @!flow-state + font-size (:font-size @!settings) + char-advance (* font-size (:char-width @!active-font)) + tree (resolve-layout + (build-intake-tree flow content-w viewport-h + scroll-y nil @!hovered-row-idx @!collapsed-groups + font-size char-advance nil)) + path (hit-test tree content-x (+ y scroll-y))] + (when path + (some (fn [node] + (case (:type node) + :group-header + (let [status (:status (:data node))] + (swap! !collapsed-groups + (fn [cg] (if (contains? cg status) (disj cg status) (conj cg status)))) + true) + :ticket-row + (do (reset! !drag-state {:phase :pending :origin {:x content-x :y y} :node node}) + true) + nil)) + (rseq path))))) + +;; ═══════════════════════════════════════════════════════════════════════ +;; Top-level case handlers +;; ═══════════════════════════════════════════════════════════════════════ + +(defn- handle-mousedown! + "Route mousedown to the appropriate zone handler." + [{:keys [!viewport !scroll-y !settings !sidebar-visible !current-file !effective-local-world !cmd-panel + !flow-state !focus !caret-visible !active-pane] :as atoms} + {:keys [layout-x layout-y gutter-w] :as layout} + deps io x y] + (let [viewport @!viewport + scroll-y @!scroll-y + settings @!settings + sb-vis? (and !sidebar-visible @!sidebar-visible) + local-world @!effective-local-world + ;; Settings overlay + panel-w 600 panel-h 480 + panel-x (/ (- (:width viewport) panel-w) 2) + panel-y (+ scroll-y (/ (- (:height viewport) panel-h) 2)) + in-settings? (and (:visible settings) + (>= x panel-x) (< x (+ panel-x panel-w)) + (>= y panel-y) (< y (+ panel-y panel-h)))] + (cond + in-settings? + (handle-settings-click! atoms x y panel-x panel-y settings) + + (and sb-vis? (< x sidebar-w)) + (handle-sidebar-click! atoms io x y viewport scroll-y) + + (>= y (- (:height viewport) status-bar-h)) + nil ;; status bar — no-op + + :else + (let [cmd-panel @!cmd-panel + cmd-visible? (or (:visible cmd-panel) + (ws/local-world-file-workspace? local-world) + (ws/local-world-flow? local-world)) + cmd-panel-top (if cmd-visible? + (- (:height viewport) cmd-panel-h status-bar-h) + (:height viewport)) + in-cmd? (and cmd-visible? (>= y cmd-panel-top) + (< y (- (:height viewport) status-bar-h)))] + (if in-cmd? + (handle-cmd-click! atoms x sb-vis? cmd-panel) + ;; Editor area + (do + (when (:visible @!cmd-panel) + (swap! !cmd-panel assoc :visible false)) + (cond + (ws/local-world-flow? local-world) + (let [sb-w (if sb-vis? sidebar-w 0)] + (handle-flow-canvas-click! atoms + (- x sb-w) y + (- (:width viewport) sb-w) + (:height viewport) + scroll-y)) + + (ws/local-world-file-workspace? local-world) + (let [sb-w (if sb-vis? sidebar-w 0) + content-w (- (:width viewport) sb-w) + code-w (int (* content-w (ws/pane-width-pct local-world :main 0.4))) + chat-w (int (* content-w (ws/pane-width-pct local-world :right 0.55))) + rel-x (- x sb-w) + in-editor? (< rel-x code-w) + in-chat? (and (>= rel-x code-w) (< rel-x (+ code-w chat-w))) + clicked-pane (cond in-editor? :editor in-chat? :chat :else :preview)] + (ws/set-active-pane! atoms clicked-pane) + (when in-editor? + (handle-editor-click! atoms deps layout-x layout-y gutter-w + x (+ y scroll-y) (- x sb-w))) + (when in-chat? + (handle-chat-click! atoms io rel-x y content-w viewport scroll-y))) + + :else + (let [sb-w (if sb-vis? sidebar-w 0)] + (handle-editor-click! atoms deps layout-x layout-y gutter-w + x (+ y scroll-y) (- x sb-w)))))))))) + +(defn- handle-mousemove! + "Route mousemove: sidebar hover, drag state machine, flow canvas hover." + [{:keys [!mouse-x !mouse-y !sidebar-visible !sidebar-ui !sidebar-scene !settings !active-font + !current-file !effective-local-world !viewport !scroll-y !drag-state !flow-state + !hovered-row-idx !collapsed-groups]} + coords] + (reset! !mouse-x (:x coords)) + (reset! !mouse-y (:y coords)) + ;; Sidebar hover — writes to dedicated atom (:hover-id in !sidebar-ui) to avoid + ;; triggering expensive (drag-distance ds mx my) drag-threshold-px) + (reset! !drag-state {:phase :dragging :origin (:origin ds) + :node (:node ds) :current {:x mx :y my}})) + :dragging + (swap! !drag-state assoc :current {:x mx :y my}) + nil)) + ;; Flow canvas hover + (let [sb-vis? (and !sidebar-visible @!sidebar-visible) + sb-w (if sb-vis? sidebar-w 0) + in-sidebar? (and sb-vis? (< (:x coords) sidebar-w))] + (when (and (ws/local-world-intake? @!effective-local-world) + (not in-sidebar?) + (= :idle (:phase @!drag-state))) + (let [flow @!flow-state + content-x (- (:x coords) sb-w) + content-w (- (:width @!viewport) sb-w) + font-size (:font-size @!settings) + char-advance (* font-size (:char-width @!active-font)) + tree (resolve-layout + (build-intake-tree flow content-w (:height @!viewport) + @!scroll-y nil @!hovered-row-idx @!collapsed-groups + font-size char-advance nil)) + path (hit-test tree content-x (+ (:y coords) @!scroll-y)) + new-idx (when path + (some (fn [node] + (when (= :ticket-row (:type node)) + (:idx (:data node)))) + (rseq path)))] + (when (not= new-idx @!hovered-row-idx) + (reset! !hovered-row-idx new-idx)))))) + +(defn- handle-mouseup! + "Route mouseup: ticket selection or drag-to-select completion." + [{:keys [!drag-state !flow-state !viewport !sidebar-visible]}] + (let [ds @!drag-state] + (case (:phase ds) + :pending + (do (let [node (:node ds)] + (when (= :ticket-row (:type node)) + (let [idx (:idx (:data node)) + flow @!flow-state + selected (:selected flow) + already? (some #{idx} selected) + new-sel (if already? + (vec (remove #{idx} selected)) + (conj (vec selected) idx))] + (swap! !flow-state set-selection new-sel)))) + (reset! !drag-state {:phase :idle})) + :dragging + (let [sb-vis? (and !sidebar-visible @!sidebar-visible) + sb-w (if sb-vis? sidebar-w 0) + node (:node ds) + cur (:current ds) + content-w (- (:width @!viewport) sb-w) + left-w (int (* content-w list-left-pane-pct)) + cur-x (- (:x cur) sb-w)] + (when (and node cur (= :ticket-row (:type node))) + (if (>= cur-x left-w) + (let [idx (:idx (:data node)) + flow @!flow-state + selected (:selected flow) + already? (some #{idx} selected)] + (when-not already? + (swap! !flow-state set-selection (conj (vec selected) idx)))) + nil)) + (reset! !drag-state {:phase :idle})) + nil))) + +;; ═══════════════════════════════════════════════════════════════════════ +;; Missionary consumer +;; ═══════════════════════════════════════════════════════════════════════ + +(defn mouse-consumer + "Missionary consumer: route mouse events to named handlers." + [atoms layout deps io >mouse-events] + (->> >mouse-events + (m/reduce + (fn [_ [type coords]] + (case type + :mousedown (handle-mousedown! atoms layout deps io (:x coords) (:y coords)) + :mousemove (handle-mousemove! atoms coords) + :mouseup (handle-mouseup! atoms) + nil)) + nil))) diff --git a/src/app/client/workspace/runtime/render.cljs b/src/app/client/workspace/runtime/render.cljs new file mode 100644 index 0000000..bfaa5c0 --- /dev/null +++ b/src/app/client/workspace/runtime/render.cljs @@ -0,0 +1,567 @@ +(ns app.client.workspace.runtime.render + "Render consumer: derived flow assembly, world snapshot, GPU upload diffing, draw." + (:require [missionary.core :as m] + [app.client.substrate.webgpu.renderer :as editor] + [app.client.substrate.webgpu.buffer-pool :as pool] + [app.client.substrate.webgpu.gpu-budget :as gpu-budget] + [app.client.workspace.events :refer [maybe-snap]] + [app.client.workspace.runtime.workspace-actions :as ws] + [app.client.workspace.sidebar :refer [cmd-panel-h status-bar-h]] + [app.client.workspace.editor-compute :refer [js data)] + (js/console.log label payload) + (js/console.log (str label " JSON " (js/JSON.stringify payload))))) + +(defn render-consumer + "Missionary consumer: assemble derived flows, build world snapshot, diff-upload to GPU, draw on RAF." + [{:keys [!editor-doc !cmd-panel !ai-provider !agent-output !agent-scroll-y !scroll-y + !viewport !settings !active-font !current-file !effective-local-world !flow-state !collapsed-groups + !hovered-row-idx !drag-state !sidebar-truth !sidebar-overlay !sidebar-ui !sidebar-visible !sidebar-scene !extract-preview + !shimmer-phase !trail-collapsed !active-pane !scroll-x !chat-scroll-y !chat-input + !focus !run-scroll-y !detail-scroll-y !eval-result !caret-visible !folded-lines + !font-manifest !font-assets !text-geo !gpu-budget !cmd-rect-sys !settings-rect-sys + !sidebar-pool !editor-pool !editor-shadow-pool !sidebar-shadow-pool] + :as atoms} + {:keys [layout-x layout-y gutter-w]} + {:keys [device ctx geometry]} + {:keys [tokenize-fn layout-fn detect-folds-fn find-bracket-fn]}] + (let [;; Dirty-present RAF: only fires when world-snapshot changes (Phase 6E) + !request-frame (volatile! nil) + >dirty-raf + (m/observe + (fn [!] + (let [pending? (volatile! false) + raf-id (volatile! nil) + request! (fn [] + (when-not @pending? + (vreset! pending? true) + (vreset! raf-id + (js/requestAnimationFrame + (fn [t] + (vreset! pending? false) + (vreset! raf-id nil) + (! t))))))] + (vreset! !request-frame request!) + (request!) ;; ensure first frame renders + #(do (vreset! !request-frame nil) + (when-let [id @raf-id] + (js/cancelAnimationFrame id)))))) + + ;; Derived flows + [] + sidebar-dirty? + (conj {:x 0 :y 0 :w (max 1 sb-w) :h ph}) + (or editor-rects-dirty? editor-shadows-dirty?) + (conj {:x sb-w :y 0 :w (- pw sb-w) :h (- ph chrome-h)}) + cmd-dirty? + (conj {:x 0 :y (- ph chrome-h) :w pw :h chrome-h}) + settings-dirty? + (conj {:x 0 :y 0 :w pw :h ph}))] + (when (seq regions) + (let [x1 (apply min (map :x regions)) + y1 (apply min (map :y regions)) + x2 (apply max (map #(+ (:x %) (:w %)) regions)) + y2 (apply max (map #(+ (:y %) (:h %)) regions))] + {:x x1 :y y1 :w (- x2 x1) :h (- y2 y1)}))) + + ;; Nothing changed (shouldn't happen — world identity check should have caught this) + :else nil) + + frame-log? (or (<= frame-idx 8) + font-changed? + backend-changed? + (not content-same?) + (not chrome-same?) + (not settings-same?) + rt-resized?) + _ (when frame-log? + (render-debug! "[RENDER/FRAME]" + {:frame frame-idx + :font-id (:id font-assets) + :backend (:backend font-assets) + :viewport viewport + :content-lines (count (or content-ops [])) + :chrome-lines (count (or full-chrome-ops [])) + :content-instances (:num-instances new-content-geo) + :chrome-instances (:num-instances new-chrome-geo) + :editor-rects (count editor-rects) + :editor-shadows (count (or editor-shadows [])) + :sidebar-rects (count sidebar-rects) + :cmd-rects (count (or cmd-rects [])) + :settings-rects (count (or settings-rects [])) + :cmd-visible cmd-visible + :agent-visible agent-visible + :settings-visible settings-visible + :snap? snap? + :dirty-rect (or dirty-rect :full) + :rt-enabled? use-persistent-render-target?})) + _ (when (and (seq content-ops) + (zero? (:num-instances new-content-geo))) + (js/console.warn "[RENDER/WARN] content ops present but content instance buffer is empty" + {:frame frame-idx + :content-lines (count content-ops) + :font-id (:id font-assets) + :backend (:backend font-assets)})) + raf-t3 (js/performance.now)] ;; before draw + (try + (editor/draw-frame! device ctx + new-content-geo (pool/pool-draw-info !editor-pool) new-cmd-sys + (:camera-floats (:pipelines geometry)) + (:pass-descriptor (:pipelines geometry)) + 0 (- scroll-y) + (:width viewport) (:height viewport) + :frame-idx frame-idx + :cmd-panel-visible cmd-visible + :cmd-panel-h cmd-panel-h + :chrome-text-sys new-chrome-geo + :chrome-base-line-count chrome-base-count + :settings-line-count settings-line-count + :settings-visible settings-visible + :settings-rect-sys new-settings-sys + :diagnostics-visible show-diagnostics? + :diagnostics-line-index diagnostics-line-index + :agent-visible agent-visible + :editor-shadow-pool-info (pool/pool-draw-info !editor-shadow-pool) + :sidebar-shadow-pool-info (pool/pool-draw-info !sidebar-shadow-pool) + :sidebar-pool-info (pool/pool-draw-info !sidebar-pool) + :dirty-rect dirty-rect + :render-target render-target + :clear-quad (:clear-quad (:pipelines geometry))) + (catch :default err + (js/console.error "[RENDER/DRAW-FAIL]" + err + (clj->js {:frame frame-idx + :font-id (:id font-assets) + :backend (:backend font-assets) + :viewport viewport + :content-instances (:num-instances new-content-geo) + :chrome-instances (:num-instances new-chrome-geo) + :editor-rects (count editor-rects) + :sidebar-rects (count sidebar-rects) + :dirty-rect (or dirty-rect :full) + :rt-enabled? use-persistent-render-target?})) + (throw err))) + (let [raf-t4 (js/performance.now)] + (when (> (- raf-t4 raf-t0) 5) + (js/console.log "[RAF] prep:" (.toFixed (- raf-t1 raf-t0) 1) "ms | text-gpu:" (.toFixed (- raf-t2 raf-t1) 1) "ms | rects-gpu:" (.toFixed (- raf-t3 raf-t2) 1) "ms | draw:" (.toFixed (- raf-t4 raf-t3) 1) "ms | TOTAL:" (.toFixed (- raf-t4 raf-t0) 1) "ms | content-same?:" content-same? "chrome-same?:" chrome-same? + "dirty-rect:" (if dirty-rect "partial" "full"))))) + + {:content-text-geo new-content-geo + :chrome-text-geo new-chrome-geo + :cmd-rect-sys new-cmd-sys + :settings-rect-sys new-settings-sys + :render-target render-target + :prev-world world + :prev-content-ops content-ops + :prev-chrome-ops chrome-ops + :prev-sidebar-data sidebar-data + :prev-settings-text settings-text + :prev-settings-visible settings-visible + :prev-show-diagnostics show-diagnostics? + :prev-scroll-y scroll-y + :prev-editor-rects editor-rects + :prev-editor-shadows editor-shadows + :prev-cmd-rects cmd-rects + :prev-settings-rects settings-rects + :prev-font-size font-size + :prev-px-range px-range + :prev-line-height line-h + :prev-sharpness sharpness + :prev-char-width char-width + :prev-snap-step snap-step + :prev-font-id (:id font-assets) + :prev-font-backend (:backend font-assets) + :frame-idx frame-idx}))) + + (let [tracker @!gpu-budget + chrome-text-geo (editor/clone-text-system device (:text geometry) 2000) + render-target (when use-persistent-render-target? + (let [vp @!viewport + d (or (:dpr vp) 1)] + (editor/create-render-target device + (Math/floor (* (:width vp) d)) + (Math/floor (* (:height vp) d)) + (:format (:pipelines geometry)) + :tracker tracker)))] + (render-debug! "[RENDER/INIT]" + {:viewport @!viewport + :font-id (:id @!font-assets) + :backend (:backend @!font-assets) + :persistent-render-target? use-persistent-render-target? + :has-render-target? (boolean render-target)}) + (gpu-budget/log-startup-report! tracker) + {:content-text-geo (:text geometry) + :chrome-text-geo chrome-text-geo + :cmd-rect-sys @!cmd-rect-sys + :settings-rect-sys @!settings-rect-sys + :render-target render-target + :prev-world nil + :prev-content-ops nil + :prev-chrome-ops nil + :prev-sidebar-data nil + :prev-settings-text nil + :prev-settings-visible nil + :prev-show-diagnostics nil + :prev-scroll-y nil + :prev-editor-rects nil + :prev-editor-shadows nil + :prev-cmd-rects nil + :prev-settings-rects nil + :prev-font-size nil + :prev-px-range nil + :prev-line-height nil + :prev-sharpness nil + :prev-char-width nil + :prev-snap-step nil + :prev-font-id (:id @!font-assets) + :prev-font-backend (:backend @!font-assets) + :frame-idx 0}) + + (m/sample vector dirty-raf)))) diff --git a/src/app/client/workspace/runtime/scroll.cljs b/src/app/client/workspace/runtime/scroll.cljs new file mode 100644 index 0000000..c6674b9 --- /dev/null +++ b/src/app/client/workspace/runtime/scroll.cljs @@ -0,0 +1,120 @@ +(ns app.client.workspace.runtime.scroll + "Scroll consumer: wheel routing across sidebar, agent, chat, editor, flow canvas." + (:require [missionary.core :as m] + [app.client.workspace.events :refer [maybe-snap]] + [app.client.workspace.runtime.workspace-actions :as ws] + [app.client.workspace.sidebar :refer [sidebar-w sidebar-tab-h cmd-panel-h status-bar-h compute-sidebar-content-height derive-effective-sidebar]] + [app.client.workspace.trail :refer [compute-agent-panel-h agent-wrapped-line-count]] + [app.client.workspace.ui-primitives :refer [list-left-pane-pct list-divider-w]] + [app.client.workflows.dg-flow :refer [group-tickets-by-status list-content-height]])) + +(defn scroll-consumer + "Missionary consumer: route wheel events to the appropriate scroll target." + [{:keys [!scroll-y !scroll-x !viewport !settings !active-font !sidebar-visible + !mouse-x !mouse-y !sidebar-truth !sidebar-overlay !sidebar-ui !effective-local-world !agent-output + !agent-scroll-y !chat-scroll-y !detail-scroll-y !flow-state !collapsed-groups !editor-doc]} + >wheel-events] + (->> >wheel-events + (m/reduce + (fn [_ wheel-evt] + (let [delta (:dy wheel-evt 0) + dx (:dx wheel-evt 0) + shift? (:shift? wheel-evt) + viewport @!viewport + settings @!settings + dpr (:dpr viewport) + snap? (:snap-to-pixel? settings) + font-size (:font-size settings) + char-advance (* font-size (:char-width @!active-font)) + sb-vis? (and !sidebar-visible @!sidebar-visible) + mouse-x @!mouse-x + in-sidebar? (and sb-vis? (< mouse-x sidebar-w)) + local-world @!effective-local-world + file-workspace? (ws/local-world-file-workspace? local-world) + flow-active? (ws/local-world-flow? local-world) + agent-output @!agent-output + agent-h (if file-workspace? + 0 + (compute-agent-panel-h agent-output font-size + (:height viewport) (:width viewport) + char-advance)) + agent-y0 (- (:height viewport) cmd-panel-h status-bar-h agent-h 12) + agent-y1 (- (:height viewport) cmd-panel-h status-bar-h) + mouse-y @!mouse-y + in-agent? (and (not in-sidebar?) + (pos? agent-h) + (>= mouse-y agent-y0) + (< mouse-y agent-y1))] + (cond + ;; Sidebar file tree + in-sidebar? + (let [ss (derive-effective-sidebar @!sidebar-truth @!sidebar-overlay @!sidebar-ui) + content-h (compute-sidebar-content-height ss) + visible-h (- (:height viewport) sidebar-tab-h) + max-scroll (max 0 (- content-h visible-h))] + (swap! !sidebar-ui update :scroll-y + #(-> (+ (or % 0) delta) (max 0) (min max-scroll)))) + + ;; Agent panel + in-agent? + (let [line-step (* font-size 1.2) + max-chars (if (pos? char-advance) + (max 1 (int (/ (- (:width viewport) 48) char-advance))) + 80) + line-count (agent-wrapped-line-count agent-output max-chars) + total-h (* line-count line-step) + max-scroll (max 0 (- total-h (- agent-h 16)))] + (swap! !agent-scroll-y + #(-> (+ % delta) (max 0) (min max-scroll)))) + + ;; Flow canvas (intake / run) — must check BEFORE chat to avoid false match + flow-active? + (let [flow @!flow-state + sb-off (if sb-vis? sidebar-w 0) + content-w (- (:width viewport) sb-off) + left-w (int (* content-w list-left-pane-pct)) + right-x0 (+ sb-off left-w list-divider-w)] + (if (>= mouse-x right-x0) + ;; Right detail pane + (swap! !detail-scroll-y #(max 0 (+ % delta))) + ;; Left ticket list + (let [grouped (group-tickets-by-status (:tickets flow)) + content-h (list-content-height grouped @!collapsed-groups) + visible-h (- (:height viewport) cmd-panel-h status-bar-h agent-h 12) + max-scroll (max 0 (- content-h visible-h))] + (swap! !scroll-y #(-> (+ % delta) (max 0) (min max-scroll)))))) + + ;; Chat pane (3-pane mode) + (let [sb-off (if sb-vis? sidebar-w 0) + cw (- (:width viewport) sb-off) + code-w (int (* cw (ws/pane-width-pct local-world :main 0.4))) + chat-w (int (* cw (ws/pane-width-pct local-world :right 0.55))) + rel-mx (- mouse-x sb-off) + in-chat? (and file-workspace? (>= rel-mx code-w) (< rel-mx (+ code-w chat-w)))] + (and file-workspace? in-chat?)) + (swap! !chat-scroll-y #(max 0 (+ % delta))) + + ;; Editor (no file open, or mouse in code pane) + :else + (when (or (not file-workspace?) + (< (- mouse-x (if sb-vis? sidebar-w 0)) + (int (* (- (:width viewport) (if sb-vis? sidebar-w 0)) + (ws/pane-width-pct local-world :main 0.4))))) + (let [doc @!editor-doc + line-h (* font-size (:line-height settings)) + total-lines (count (:lines doc)) + chrome-h (+ cmd-panel-h status-bar-h) + overscroll (* 10 line-h) + visible-h (- (:height viewport) chrome-h) + max-scroll (max 0 (- (+ (* total-lines line-h) overscroll) visible-h))] + (swap! !scroll-y #(-> (+ % delta) (max 0) (min max-scroll) (maybe-snap dpr snap?)))) + (let [h-delta (if shift? delta dx) + sb-off (if sb-vis? sidebar-w 0) + cw (- (:width viewport) sb-off) + editor-right (if file-workspace? + (+ sb-off (int (* cw (ws/pane-width-pct local-world :main 0.4)))) + (+ sb-off cw))] + (when (and (not (zero? h-delta)) (< mouse-x editor-right)) + (swap! !scroll-x #(max 0 (+ (or % 0) h-delta)))))))) + nil) + ))) diff --git a/src/app/client/workspace/runtime/sidebar_io.cljs b/src/app/client/workspace/runtime/sidebar_io.cljs new file mode 100644 index 0000000..c1d67ea --- /dev/null +++ b/src/app/client/workspace/runtime/sidebar_io.cljs @@ -0,0 +1,230 @@ +(ns app.client.workspace.runtime.sidebar-io + "Sidebar I/O: HTTP fetch helpers, directory/file loading, sidebar visibility watch." + (:require [clojure.string :as str] + [cljs.reader :as reader])) + +(defn save-workspace-truth! + "Persist workspace truth to Rama via HTTP. Fire-and-forget." + [truth-data] + (when (seq truth-data) + (-> (js/fetch "/api/workspace/save-truth" + (clj->js {:method "POST" + :headers {"Content-Type" "application/edn"} + :body (pr-str {:data truth-data})})) + (.then (fn [resp] (.text resp))) + (.then (fn [_] (js/console.log "[WORKSPACE-HTTP] save-truth"))) + (.catch (fn [err] (js/console.error "[WORKSPACE] Save failed:" err)))))) + +(defn save-editor-doc! + "Persist editor document state to Rama. Logs full round-trip latency + for Phase 4B measurement. Fire-and-forget — the client does NOT wait + for the response to update the editor." + [file-path doc-state] + (when (and file-path doc-state) + (let [t0 (js/performance.now)] + (-> (js/fetch "/api/editor/save-doc" + (clj->js {:method "POST" + :headers {"Content-Type" "application/edn"} + :body (pr-str {:file-path file-path :doc-state doc-state})})) + (.then (fn [resp] (.text resp))) + (.then (fn [text] + (let [t1 (js/performance.now) + result (reader/read-string text) + server-ms (:latency-ms result) + total-ms (- t1 t0)] + (js/console.log "[EDITOR-RAMA] round-trip:" (.toFixed total-ms 1) "ms" + "| server:" server-ms "ms" + "| network:" (.toFixed (- total-ms (or server-ms 0)) 1) "ms")))) + (.catch (fn [err] (js/console.error "[EDITOR-RAMA] Save failed:" err))))))) + +(defn save-flow-state! + "Persist flow session FSM state to Rama via HTTP. Fire-and-forget." + [flow-data] + (when (seq flow-data) + (-> (js/fetch "/api/flow/save-state" + (clj->js {:method "POST" + :headers {"Content-Type" "application/edn"} + :body (pr-str {:data flow-data})})) + (.then (fn [resp] (.text resp))) + (.then (fn [_] (js/console.log "[FLOW-HTTP] save-state"))) + (.catch (fn [err] (js/console.error "[FLOW] Save failed:" err)))))) + +(defn save-agent-trail! + "Persist a completed agent trail to Rama via HTTP. Fire-and-forget." + [run-id trail-data] + (when (and run-id trail-data) + (let [t0 (js/performance.now)] + (-> (js/fetch "/api/agent/trail/save" + (clj->js {:method "POST" + :headers {"Content-Type" "application/edn"} + :body (pr-str {:run-id run-id :trail-data trail-data})})) + (.then (fn [resp] (.text resp))) + (.then (fn [_text] + (js/console.log "[TRAIL-HTTP] save round-trip:" + (.toFixed (- (js/performance.now) t0) 1) "ms"))) + (.catch (fn [err] (js/console.error "[TRAIL] Save failed:" err))))))) + +(defn emit-settings-update! + "Submit a settings update to Rama via HTTP. Fire-and-forget — truth + flows back via Electric subscription, not via HTTP response." + [settings-data] + (let [t0 (js/performance.now)] + (-> (js/fetch "/api/settings/update" + (clj->js {:method "POST" + :headers {"Content-Type" "application/edn"} + :body (pr-str {:data settings-data})})) + (.then (fn [resp] (.text resp))) + (.then (fn [_text] + (js/console.log "[SETTINGS-HTTP] update round-trip:" + (.toFixed (- (js/performance.now) t0) 1) "ms"))) + (.catch (fn [err] (js/console.error "[SETTINGS] Update failed:" err)))))) + +(defn emit-sidebar-action! + "Submit a sidebar action to Rama via HTTP. Calls callback with the new committed state." + [action-type data callback] + (let [t0 (js/performance.now)] + (-> (js/fetch "/api/sidebar/action" + (clj->js {:method "POST" + :headers {"Content-Type" "application/edn"} + :body (pr-str {:action-type action-type :data data})})) + (.then (fn [resp] (.text resp))) + (.then (fn [text] + (js/console.log "[SIDEBAR-HTTP]" (str action-type) "round-trip:" (.toFixed (- (js/performance.now) t0) 1) "ms") + (let [state (reader/read-string text)] + (when callback (callback state))))) + (.catch (fn [err] (js/console.error "[SIDEBAR] Rama action failed:" err)))))) + +(defn make-sidebar-io + "Create sidebar I/O closures. Returns {:fetch-edn! :post-edn! :fetch-home-dirs! :fetch-dir! :fetch-file!}." + [{:keys [!sidebar-ui !current-file !selected-artifact !scroll-x !file-load-request]}] + (let [fetch-edn! + (fn + ([url callback] + (-> (js/fetch url) + (.then (fn [resp] (.text resp))) + (.then (fn [text] (callback (reader/read-string text)))) + (.catch (fn [err] (js/console.error "[SIDEBAR] Fetch error:" err))))) + ([url callback err-callback] + (-> (js/fetch url) + (.then (fn [resp] (.text resp))) + (.then (fn [text] (callback (reader/read-string text)))) + (.catch (fn [err] (err-callback err)))))) + + post-edn! + (fn [url body callback] + (-> (js/fetch url + (clj->js {:method "POST" + :headers {"Content-Type" "application/edn"} + :body (pr-str body)})) + (.then (fn [resp] (.text resp))) + (.then (fn [text] (callback (reader/read-string text)))) + (.catch (fn [err] + (js/console.error "[AGENT][HTTP][POST-ERROR]" + (clj->js {:url url + :message (.-message err)}) + err)))) + nil) + + fetch-home-dirs! + (fn [] + (when (nil? (:home-dirs @!sidebar-ui)) + (fetch-edn! "/api/home-dirs" + (fn [dirs] + (swap! !sidebar-ui assoc :home-dirs dirs))))) + + fetch-dir! + (fn [path] + (let [ui @!sidebar-ui] + (when-not (or (contains? (:dir-cache ui) path) + (contains? (:in-flight-dirs ui) path)) + (swap! !sidebar-ui update :in-flight-dirs conj path) + (let [t0 (js/performance.now)] + (fetch-edn! (str "/api/list-dir?path=" (js/encodeURIComponent path)) + (fn [entries] + (js/console.log "[SIDEBAR-FETCH-DIR]" path ":" (.toFixed (- (js/performance.now) t0) 1) "ms |" (count entries) "entries") + (swap! !sidebar-ui (fn [s] + (-> s + (update :in-flight-dirs disj path) + (assoc-in [:dir-cache path] entries))))) + (fn [err] + (js/console.error "[SIDEBAR] Dir fetch error:" err) + (swap! !sidebar-ui update :in-flight-dirs disj path))))))) + + !latest-file-req (atom nil) + fetch-file! + (fn [path root-path & {:keys [target-line]}] + (reset! !latest-file-req path) + (let [ui @!sidebar-ui] + (when-not (contains? (:in-flight-files ui) path) + (swap! !sidebar-ui update :in-flight-files conj path) + (fetch-edn! (str "/api/read-file?path=" (js/encodeURIComponent path) + "&root=" (js/encodeURIComponent root-path)) + (fn [result] + (swap! !sidebar-ui update :in-flight-files disj path) + (if (:error result) + (do (js/console.error "[SIDEBAR] File read error:" (:error result)) + ;; Roll back selection — file can't be loaded. + ;; Clear local state AND committed sidebar truth so + ;; reconnect/reload doesn't retry the failed open. + (when !selected-artifact + (reset! !selected-artifact nil)) + (reset! !current-file nil) + (emit-sidebar-action! :sidebar/file-select {:path nil :name nil} nil)) + (when (= @!latest-file-req path) + (let [lines (str/split-lines (:content result))] + (reset! !current-file {:path path :name (last (str/split path #"/"))}) + (reset! !scroll-x 0) + (reset! !file-load-request + (cond-> {:lines lines} + target-line (assoc :target-line target-line))))))) + (fn [err] + (js/console.error "[SIDEBAR] File read error:" err) + (swap! !sidebar-ui update :in-flight-files disj path) + ;; Roll back selection on network error too + (when !selected-artifact + (reset! !selected-artifact nil)) + (reset! !current-file nil) + (emit-sidebar-action! :sidebar/file-select {:path nil :name nil} nil))))))] + {:fetch-edn! fetch-edn! + :post-edn! post-edn! + :fetch-home-dirs! fetch-home-dirs! + :fetch-dir! fetch-dir! + :fetch-file! fetch-file!})) + +(defn install-sidebar-watch! + "Watch !sidebar-visible; fetch home dirs when sidebar becomes visible." + [{:keys [!sidebar-visible]} fetch-home-dirs!] + (when !sidebar-visible + (when @!sidebar-visible + (fetch-home-dirs!)) + (add-watch !sidebar-visible :sidebar-fetch + (fn [_ _ old-vis new-vis] + (when (and new-vis (not old-vis)) + (fetch-home-dirs!)))))) + +(defn seed-initial-file! + "If initial-file provided, set selected-artifact + current-file and expand sidebar dirs." + [{:keys [!selected-artifact !current-file !sidebar-truth]} {:keys [fetch-dir!]} initial-file] + (when initial-file + (let [file-path (:path initial-file) + file-name (last (str/split file-path #"/")) + project-path (:project initial-file)] + (reset! !selected-artifact {:kind :file :path file-path :name file-name}) + (reset! !current-file {:path file-path :name file-name}) + (when project-path + (let [rel (subs file-path (count project-path)) + rel (if (str/starts-with? rel "/") (subs rel 1) rel) + parts (str/split rel #"/") + dir-parts (butlast parts) + dir-paths (loop [acc [] prefix project-path dirs dir-parts] + (if (empty? dirs) + acc + (let [next-path (str prefix "/" (first dirs))] + (recur (conj acc next-path) next-path (rest dirs)))))] + (swap! !sidebar-truth assoc + :project {:name (last (str/split project-path #"/")) + :path project-path} + :expanded-dirs (set dir-paths)) + (fetch-dir! project-path) + (doseq [dp dir-paths] + (fetch-dir! dp))))))) diff --git a/src/app/client/workspace/runtime/state.cljs b/src/app/client/workspace/runtime/state.cljs new file mode 100644 index 0000000..7d4f6f8 --- /dev/null +++ b/src/app/client/workspace/runtime/state.cljs @@ -0,0 +1,232 @@ +(ns app.client.workspace.runtime.state + "Runtime state: atom creation, layout constants, font defaults. + Returns the rt context map — the single shared contract for all runtime modules." + (:require [app.client.substrate.webgpu.renderer :as editor] + [app.client.substrate.webgpu.buffer-pool :as pool] + [app.client.substrate.webgpu.gpu-budget :as gpu-budget] + [app.client.workspace.settings-view :refer [manifest-defaults->settings font-defaults->settings]] + [app.client.workflows.dg-flow :refer [initial-flow-state]])) + +(def default-font-manifest + {:fonts [{:name "DejaVu Sans Mono" + :id "dejavu-sans-mono" + :charWidth 0.56 + :default true + :defaults {:fontSize 19 + :lineHeight 1.2 + :pxRange 8 + :sharpness 0.0 + :snapToPixel true + :showDiagnostics false}}] + :settings {:fontSize {:default 19} + :lineHeight {:default 1.2} + :pxRange {:default 8} + :sharpness {:default 0.0} + :snapToPixel {:default true} + :showDiagnostics {:default false}}}) + +(defn- initial-viewport [node] + (let [win-w (or (.-innerWidth js/window) 0) + win-h (or (.-innerHeight js/window) 0) + node-w (or (.-clientWidth node) 0) + node-h (or (.-clientHeight node) 0)] + {:width (max 1 node-w win-w) + :height (max 1 node-h win-h) + :dpr (or (.-devicePixelRatio js/window) 1)})) + +(defn make-runtime-state + "Create all runtime atoms and layout constants. Returns the rt context map. + External atoms (!sidebar-visible, !file-load-request, !preview-el) are threaded through." + [{:keys [node device ctx geometry font-assets initial-lines font-manifest + !sidebar-visible !file-load-request !preview-el gpu-budget]}] + (let [;; Layout constants + gutter-w 40 + layout-x (+ 50 gutter-w) + layout-y 100 + + ;; Font manifest processing + manifest (or font-manifest default-font-manifest) + fonts (or (:fonts manifest) []) + available-fonts (filterv #(not (false? (:available %))) fonts) + default-font (or (first (filter :default available-fonts)) + (first available-fonts) + {:id "dejavu-sans-mono" :name "DejaVu Sans Mono" :charWidth 0.56}) + default-font-idx (or (first (keep-indexed (fn [idx font] + (when (= (:id font) (:id default-font)) idx)) + available-fonts)) + 0) + manifest-settings (manifest-defaults->settings (:settings manifest)) + base-settings {:visible false + :font-id (:id default-font) + :selected-index default-font-idx + :slider-index 0 + :focus-section :fonts} + initial-settings (merge base-settings + manifest-settings + (font-defaults->settings default-font))] + + {:layout {:layout-x layout-x :layout-y layout-y :gutter-w gutter-w} + + :atoms + {;; Editor core + :!editor-doc (atom {:lines initial-lines + :cursor {:line 0 :col 0} + :selection nil + :desired-col 0}) + :!cmd-panel (atom {:text "" :cursor 0 :visible true}) + :!focus (atom :command-panel) + :!scroll-y (atom 0) + :!scroll-x (atom 0) + :!run-scroll-y (atom 0) + :!detail-scroll-y (atom 0) + :!viewport (atom (initial-viewport node)) + :!folded-lines (atom #{}) + :!caret-visible (atom true) + :!clipboard (atom nil) + :!undo-stack (atom []) + :!redo-stack (atom []) + :!eval-result (atom nil) + :!dragging? (atom false) + :!active-pane (atom :editor) + :!drag-start (atom nil) + + ;; Font / settings + :!settings (atom initial-settings) + :!font-manifest (atom manifest) + :!active-font (atom {:id (:id default-font) + :char-width (or (:charWidth default-font) 0.56) + :name (:name default-font)}) + :!font-assets (atom (or font-assets {:backend :msdf :atlas nil :bitmap nil :id "dejavu-sans-mono"})) + + ;; GPU state (terminals update these) + :!text-geo (atom (:text geometry)) + :!gpu-budget (atom gpu-budget) + :!cmd-rect-sys (atom (let [capacity 16 + size (* capacity editor/rect-stride) + ib (.createBuffer device + (clj->js {:size size + :usage (bit-or js/GPUBufferUsage.VERTEX + js/GPUBufferUsage.COPY_DST)})) + _ (gpu-budget/register-buffer! gpu-budget ib "runtime/cmd" size :active-bytes 0)] + {:pipeline (:pipeline (:rect geometry)) + :bind-group (:bind-group (:rect geometry)) + :instance-buffer ib + :num-instances 0 + :gpu-tracker gpu-budget + :gpu-label "runtime/cmd"})) + :!settings-rect-sys (atom (let [capacity 32 + size (* capacity editor/rect-stride) + ib (.createBuffer device + (clj->js {:size size + :usage (bit-or js/GPUBufferUsage.VERTEX + js/GPUBufferUsage.COPY_DST)})) + _ (gpu-budget/register-buffer! gpu-budget ib "runtime/settings" size :active-bytes 0)] + {:pipeline (:pipeline (:rect geometry)) + :bind-group (:bind-group (:rect geometry)) + :instance-buffer ib + :num-instances 0 + :gpu-tracker gpu-budget + :gpu-label "runtime/settings"})) + ;; Per-source shadow pools (Phase 6C: differential rendering, 20 floats/shadow) + ;; Separate pools prevent cross-source position shifts from causing full rewrites + :!editor-shadow-pool (pool/create-pool device 16 + (:pipeline (:shadow geometry)) + (:bind-group (:shadow geometry)) + :floats-per-item 20 + :pack-fn pool/pack-shadow + :tracker gpu-budget + :label "pool/editor-shadow") + :!sidebar-shadow-pool (pool/create-pool device 64 + (:pipeline (:shadow geometry)) + (:bind-group (:shadow geometry)) + :floats-per-item 20 + :pack-fn pool/pack-shadow + :tracker gpu-budget + :label "pool/sidebar-shadow") + + ;; Sidebar buffer pool (differential rendering) + :!sidebar-pool (pool/create-pool device 256 + (:pipeline (:rect geometry)) + (:bind-group (:rect geometry)) + :tracker gpu-budget + :label "pool/sidebar") + + ;; Editor rect pool (Phase 6A: differential rendering with stable identities) + :!editor-pool (pool/create-pool device 64 + (:pipeline (:rect geometry)) + (:bind-group (:rect geometry)) + :tracker gpu-budget + :label "pool/editor") + + ;; Effective local world — the single semantic root. + ;; Derived from truth+overlay+ui+artifacts. No independent write path. + ;; Updated reactively via watches in runtime.cljs. + :!effective-local-world (atom nil) + + ;; Artifact selection — the semantic intent ("the user chose this") + ;; :kind = :file | :trail | :workflow (extensible) + ;; :file → {:kind :file :path "..." :name "..."} + ;; nil = nothing selected (home screen) + :!selected-artifact (atom nil) + + ;; Sidebar / file state + ;; TRANSITIONAL: downstream I/O cache. Updated when selected-artifact + ;; is a :file and content finishes loading. Readers that check + ;; "is a file open?" still use this; they'll migrate to + ;; !selected-artifact in later phases. + :!current-file (atom {:path "/home/sid/projects/discourse-graph/apps/roam/src/index.ts" + :name "index.ts"}) + :!sidebar-truth (atom {:project nil + :expanded-dirs #{} + :selected-file nil}) + :!sidebar-overlay (atom {:pending-project nil + :pending-expanded-dirs #{} + :pending-collapsed-dirs #{} + :pending-selected-file nil}) + :!sidebar-ui (atom {:hover-id nil + :scroll-y 0 + :pointer-state :idle + :dir-cache {} + :home-dirs nil + :loading? false + :in-flight-dirs #{} + :in-flight-files #{}}) + ;; Shared sidebar scene — resolved tree cached by render flow, + ;; consumed by hit-testing. Single source, two consumers. + :!sidebar-scene (atom nil) + + ;; Agent / AI + :!ai-provider (atom :claude) + :!agent-output (atom nil) + :!agent-scroll-y (atom 0) + :!chat-scroll-y (atom 0) + :!chat-input (atom {:text "" :cursor 0}) + + ;; Mouse tracking + :!mouse-x (atom 0) + :!mouse-y (atom 0) + + ;; Workflow + :!flow-state (atom (initial-flow-state)) + :!collapsed-groups (atom #{}) + :!hovered-row-idx (atom nil) + :!drag-state (atom {:phase :idle}) + :!extract-preview (atom nil) + :!trail-collapsed (atom #{}) + :!shimmer-phase (atom false) + + ;; External atoms (passed through from caller) + :!sidebar-visible !sidebar-visible + :!file-load-request !file-load-request + :!preview-el !preview-el} + + :gpu {:device device :ctx ctx :geometry geometry :font-assets font-assets} + :node node})) + +(defn save-undo! + "Push current state to undo stack (max 100), clear redo stack." + [{:keys [!undo-stack !redo-stack]} lines cursor] + (swap! !undo-stack conj {:lines lines :cursor cursor}) + (when (> (count @!undo-stack) 100) + (swap! !undo-stack #(vec (drop 1 %)))) + (reset! !redo-stack [])) diff --git a/src/app/client/workspace/runtime/workspace_actions.cljs b/src/app/client/workspace/runtime/workspace_actions.cljs new file mode 100644 index 0000000..f7002d9 --- /dev/null +++ b/src/app/client/workspace/runtime/workspace_actions.cljs @@ -0,0 +1,243 @@ +(ns app.client.workspace.runtime.workspace-actions + "Semantic workspace actions — central dispatch for workspace-level transitions. + Replaces scattered atom mutations with named entry points. + + SEMANTIC (routed here): + select-artifact!, clear-artifact!, + set-active-pane!, toggle-sidebar!, hide-sidebar!, + enter-workflow!, exit-workflow! + EPHEMERAL (stays as direct mutation): + !focus, !caret-visible, !cmd-panel :visible, !settings :visible, + scrolls, hover, drag, mouse position") + +(defn set-active-pane! + "Set the primary visible pane. This is workspace truth — it determines + which pane the user is working in. Keyboard focus follows pane for + :editor and :chat; other panes don't auto-focus." + [{:keys [!active-pane !focus !caret-visible]} pane] + (reset! !active-pane pane) + (case pane + :editor (reset! !focus :editor) + :chat (do (reset! !focus :chat) + (reset! !caret-visible true)) + nil)) + +(defn toggle-sidebar! + "Toggle the sidebar file explorer." + [{:keys [!sidebar-visible]}] + (when !sidebar-visible + (swap! !sidebar-visible not))) + +(defn hide-sidebar! + "Hide the sidebar (e.g. on Escape when nothing else to dismiss)." + [{:keys [!sidebar-visible]}] + (when !sidebar-visible + (reset! !sidebar-visible false))) + +;; ── Artifact selection ────────────────────────────────────────── + +(defn select-artifact! + "Select an artifact. This is the single semantic entry point for + 'the user chose something.' Coordinates selection, downstream + I/O, and sidebar overlay updates. + + artifact-ref: {:kind :file :path '...' :name '...'} + {:kind :trail :run-id '...'} + {:kind :workflow :flow-id '...'} + + io: map with :fetch-file! and :fetch-dir! closures (from sidebar-io) + opts: optional {:emit-sidebar? true} to also fire sidebar Rama action" + [{:keys [!selected-artifact !current-file]} artifact-ref] + (reset! !selected-artifact artifact-ref) + ;; Keep !current-file in sync for the 40+ transitional readers. + ;; Only set for :file artifacts; clear for everything else. + (if (= :file (:kind artifact-ref)) + (reset! !current-file {:path (:path artifact-ref) :name (:name artifact-ref)}) + (reset! !current-file nil))) + +(defn clear-artifact! + "Clear the artifact selection (back to home / no file open). + Also resets active pane to :editor — chat and preview are meaningless + without an open artifact." + [{:keys [!selected-artifact !current-file !active-pane !focus]}] + (reset! !selected-artifact nil) + (reset! !current-file nil) + (reset! !active-pane :editor) + (reset! !focus :editor)) + +;; ── Workflow entry/exit ────────────────────────────────────────── + +(defn enter-workflow! + "Workspace-level consequences of entering a workflow. + DG command handlers own the FSM transition; this handles the + substrate side: clear file artifact (workflow takes full screen), + reset pane to editor, reset scroll." + [{:keys [!selected-artifact !current-file !active-pane !focus !scroll-y]}] + (reset! !selected-artifact nil) + (reset! !current-file nil) + (reset! !active-pane :editor) + (reset! !focus :editor) + (reset! !scroll-y 0)) + +(defn exit-workflow! + "Workspace-level consequences of exiting a workflow (back to idle). + DG command handlers call this after resetting !flow-state." + [{:keys [!active-pane !focus !scroll-y]}] + (reset! !active-pane :editor) + (reset! !focus :editor) + (reset! !scroll-y 0)) + +;; ── Editor event classification ────────────────────────────────── +;; +;; Phase 4A: classify editor events for the commitment boundary. +;; COMMITTED events change document content — candidates for Rama persistence. +;; EPHEMERAL events change cursor/selection — local-only, never persisted. +;; +;; Phase 4B will wire committed events through Rama and measure latency. +;; Until that measurement, the classification is the design contract. + +(def editor-committed-events + "Event types that change document content. These are the candidates + for Rama persistence (Phase 4B will measure whether direct committed + path is fast enough at keystroke rate)." + #{:char :backspace :delete :enter :paste :cut :undo :redo}) + +(def editor-ephemeral-events + "Event types that change cursor/selection/view only. Never persisted." + #{:left :right :up :down :home :end :word-left :word-right + :copy :eval}) + +(def editor-structural-events + "Events that change document structure (folding). Committed, but + persisted via !folded-lines (per-file), not !editor-doc. + Rama persistence deferred to Phase 7 (workspace-schema widening) + because folds need the artifact model for per-file scoping." + #{:fold :unfold}) + +(defn editor-event-committed? + "True if this editor event type is committed (content-changing or structural)." + [event-type] + (or (contains? editor-committed-events event-type) + (contains? editor-structural-events event-type))) + +;; ── Local world derivation ────────────────────────────────────── + +(defn derive-effective-local-world + "Derive the workspace's semantic root from current atom values. + Pure function — no side effects, no atom reads. + + :mode is the key layout-branching field: + :flow-intake — DG workflow intake screen + :flow-run — DG workflow run/review screen + :file-workspace — 3-pane file layout (editor + chat + preview) + :editor — standalone editor (no file open, no workflow) + + All consumers that currently branch on (flow-canvas-active?) and + (some? current-file) can instead read :mode from this object." + [{:keys [selected-artifact active-pane sidebar-visible + flow-state agent-output project]}] + (let [file-open? (and (some? selected-artifact) + (= :file (:kind selected-artifact))) + flow-active? (and (some? flow-state) + (contains? #{:bootstrapping :intake :select :arrange :run :review + :rework :finalize} + (:node flow-state))) + mode (cond + (and flow-active? (contains? #{:bootstrapping :intake} (:node flow-state))) :flow-intake + flow-active? :flow-run + file-open? :file-workspace + :else :editor)] + {:mode mode + :selected-artifact selected-artifact + :file-open? file-open? + :flow-active? flow-active? + :project project + :active-pane (or active-pane :editor) + :sidebar-visible (boolean sidebar-visible) + :flow-node (:node flow-state) + :flow-session-id (:session-id flow-state) + :agent-status (:status agent-output) + :agent-run-id (:run-id agent-output) + ;; Split — preserved co-presence. Describes what's held together, + ;; which artifact is primary, what's adjacent. + ;; :direction = :horizontal (panes side-by-side) or :single (one pane fills) + ;; :primary = :pane/id of the primary artifact pane + ;; :adjacent = vec of :pane/id that share the split + :split + (case mode + :file-workspace {:direction :horizontal + :primary :main + :adjacent [:right :preview]} + {:direction :single + :primary :main + :adjacent []}) + ;; Pane descriptors — semantic fills derived from mode + artifacts. + ;; Each pane: {:pane/id :role :artifact-ref :content} + ;; :role = what the pane is for (:primary-artifact, :trail, :preview, :command, :flow-canvas) + ;; :artifact-ref = which artifact it shows (nil = empty/placeholder) + ;; :content = keyword for non-artifact content (:cmd-panel, :intake-tree, etc.) + :panes + (case mode + :file-workspace + [{:pane/id :main :role :primary-artifact + :artifact-ref selected-artifact + :width-pct 0.4} + {:pane/id :right :role :trail + :artifact-ref (when (:run-id agent-output) + {:kind :trail :run-id (:run-id agent-output)}) + :width-pct 0.55} + {:pane/id :preview :role :preview + :artifact-ref nil + :width-pct 0.05}] + + :flow-intake + [{:pane/id :main :role :flow-canvas + :content :intake-tree + :width-pct 1.0}] + + :flow-run + [{:pane/id :main :role :flow-canvas + :content :run-detail + :width-pct 1.0}] + + :editor + [{:pane/id :main :role :primary-artifact + :artifact-ref nil + :width-pct 1.0}])})) + +(defn local-world-mode + "Read the effective mode from a local-world object, defaulting to :editor + during boot before the first derivation runs." + [local-world] + (or (:mode local-world) :editor)) + +(defn local-world-flow? + "True when the local world is showing a workflow surface." + [local-world] + (contains? #{:flow-intake :flow-run} (local-world-mode local-world))) + +(defn local-world-intake? + "True when the local world is showing the intake workflow surface." + [local-world] + (= :flow-intake (local-world-mode local-world))) + +(defn local-world-run? + "True when the local world is showing a run/review workflow surface." + [local-world] + (= :flow-run (local-world-mode local-world))) + +(defn local-world-file-workspace? + "True when the local world is showing the file workspace split." + [local-world] + (= :file-workspace (local-world-mode local-world))) + +(defn pane-descriptor + "Look up a semantic pane descriptor by stable :pane/id." + [local-world pane-id] + (some #(when (= pane-id (:pane/id %)) %) (:panes local-world))) + +(defn pane-width-pct + "Read a pane's semantic width percentage, falling back when the pane is absent." + [local-world pane-id fallback] + (or (:width-pct (pane-descriptor local-world pane-id)) + fallback)) diff --git a/src/app/client/workspace/settings_view.cljs b/src/app/client/workspace/settings_view.cljs new file mode 100644 index 0000000..4859711 --- /dev/null +++ b/src/app/client/workspace/settings_view.cljs @@ -0,0 +1,330 @@ +(ns app.client.workspace.settings-view + "Font/theme settings panel: defaults, sliders, rects, and text ops." + (:require [missionary.core :as m] + [app.client.workspace.events :refer [maybe-snap]] + [app.client.workspace.rect-tree :refer [rt-node]] + [app.client.workspace.themes :as themes] + [app.client.workspace.ui-primitives :refer [dt]] + [app.client.workspace.sidebar :refer [sidebar-w]])) + +(defn manifest-defaults->settings [manifest-settings] + (let [get-default (fn [k fallback] + (or (get-in manifest-settings [k :default]) fallback))] + {:font-size (get-default :fontSize 19) + :line-height (get-default :lineHeight 1.2) + :px-range (get-default :pxRange 8) + :sharpness (get-default :sharpness 0.0) + :snap-to-pixel? (get-default :snapToPixel true) + :show-diagnostics? (get-default :showDiagnostics false) + :theme-id (get-default :theme :gruvbox-dark)})) + +(defn compact-map [m] + (into {} (filter (comp some? val) m))) + +(defn font-defaults->settings [font] + (let [defaults (:defaults font)] + (when defaults + (compact-map + {:font-size (or (:fontSize defaults) (:font-size defaults)) + :line-height (or (:lineHeight defaults) (:line-height defaults)) + :px-range (or (:pxRange defaults) (:px-range defaults)) + :sharpness (or (:sharpness defaults) (:sharpness defaults)) + :snap-to-pixel? (or (:snapToPixel defaults) (:snap-to-pixel? defaults)) + :show-diagnostics? (or (:showDiagnostics defaults) (:show-diagnostics? defaults))})))) + +(defn slider-specs [settings] + [{:id :theme-id :label "Theme" :val (themes/theme-index (:theme-id settings)) :min 0 :max (dec (count themes/theme-list)) :discrete true} + {:id :font-size :label "Font Size" :val (:font-size settings) :min 8 :max 40} + {:id :line-height :label "Line Height" :val (:line-height settings) :min 1.0 :max 2.0} + ;; Some fonts/manifests legitimately default above 12; keep the slider domain wide enough + ;; so the thumb never escapes the track. + {:id :px-range :label "pxRange" :val (:px-range settings) :min 4 :max 32} + {:id :sharpness :label "Sharpness" :val (:sharpness settings) :min -0.2 :max 0.2} + {:id :snap-to-pixel? :label "Snap" :val (if (:snap-to-pixel? settings) 1 0) :min 0 :max 1} + {:id :show-diagnostics? :label "Diagnostics" :val (if (:show-diagnostics? settings) 1 0) :min 0 :max 1}]) + +(defn compute-settings-panel-rects + "Pure function: compute settings panel rectangles (background + font list + sliders)" + [settings focus viewport scroll-y font-manifest font-size] + (when (:visible settings) + (let [;; Panel dimensions - centered modal + panel-w 600 + panel-h 480 + panel-x (/ (- (:width viewport) panel-w) 2) + panel-y (+ scroll-y (/ (- (:height viewport) panel-h) 2)) + + ;; Colors (Modern Dark Theme) + bg-color {:r 0.12 :g 0.12 :b 0.14 :a 1.0} ;; Opaque modal background + border-color {:r 0.25 :g 0.25 :b 0.28 :a 1.0} + separator-color {:r 0.20 :g 0.20 :b 0.23 :a 1.0} + + item-hover {:r 0.18 :g 0.18 :b 0.22 :a 1.0} + item-selected {:r 0.22 :g 0.22 :b 0.26 :a 1.0} + item-active {:r 0.15 :g 0.25 :b 0.40 :a 0.8} ;; Blue-ish highlight for active focus + + slider-track {:r 0.20 :g 0.20 :b 0.24 :a 1.0} + slider-fill {:r 0.40 :g 0.60 :b 0.85 :a 1.0} ;; Accent Blue + slider-thumb {:r 0.90 :g 0.90 :b 0.95 :a 1.0} + + ;; State + current-focus (or (:focus-section settings) :fonts) ;; :fonts or :sliders + font-idx (or (:selected-index settings) 0) + slider-idx (or (:slider-index settings) 0) + + ;; Layout + left-w 220 + right-w (- panel-w left-w) + + ;; Left Pane (Fonts) + left-pane-x panel-x + left-pane-y panel-y + + ;; Right Pane (Sliders) + right-pane-x (+ panel-x left-w) + right-pane-y panel-y + + ;; Header + header-h 40 + content-y (+ panel-y header-h) + + ;; Font List + fonts (or (:fonts font-manifest) + [{:name "DejaVu Sans Mono" :id "dejavu-sans-mono"}]) + available-fonts (filter #(not (false? (:available %))) fonts) + font-item-h 32 + + font-rects (map-indexed + (fn [idx font] + (let [selected? (= idx font-idx) + focused? (= current-focus :fonts) + item-y (+ content-y 10 (* idx font-item-h)) + + bg (cond + (and selected? focused?) item-active + selected? item-selected + :else nil)] + (when bg + {:x (+ left-pane-x 8) + :y item-y + :w (- left-w 16) + :h (- font-item-h 4) + :r (:r bg) :g (:g bg) :b (:b bg) :a (:a bg)}))) + available-fonts) + + ;; Slider List + sliders (slider-specs settings) + + slider-item-h 64 + slider-rects (map-indexed + (fn [idx slider] + (let [selected? (= idx slider-idx) + focused? (= current-focus :sliders) + base-y (+ content-y 10 (* idx slider-item-h)) + + ;; Calculate ratio + range (- (:max slider) (:min slider)) + ratio (/ (- (:val slider) (:min slider)) range) + + ;; Background highlight + bg (cond + (and selected? focused?) item-active + selected? item-selected + :else nil) + + ;; Track geometry + track-x (+ right-pane-x 20) + track-y (+ base-y 34) + track-w (- right-w 40) + track-h 4 + fill-w (* track-w ratio)] + + (concat + ;; Item Background + (when bg + [{:x (+ right-pane-x 8) :y base-y :w (- right-w 16) :h (- slider-item-h 8) + :r (:r bg) :g (:g bg) :b (:b bg) :a (:a bg)}]) + + ;; Track Background + [{:x track-x :y track-y :w track-w :h track-h + :r (:r slider-track) :g (:g slider-track) :b (:b slider-track) :a (:a slider-track)}] + + ;; Filled Track + [{:x track-x :y track-y :w fill-w :h track-h + :r (:r slider-fill) :g (:g slider-fill) :b (:b slider-fill) :a (:a slider-fill)}] + + ;; Thumb/Knob + [{:x (+ track-x fill-w -3) :y (- track-y 5) :w 6 :h 14 + :r (:r slider-thumb) :g (:g slider-thumb) :b (:b slider-thumb) :a (:a slider-thumb)}]))) + sliders)] + + (vec + (concat + ;; Main Background + [{:x panel-x :y panel-y :w panel-w :h panel-h + :r (:r bg-color) :g (:g bg-color) :b (:b bg-color) :a (:a bg-color)}] + + ;; Header Separator + [{:x panel-x :y (+ panel-y header-h) :w panel-w :h 1 + :r (:r separator-color) :g (:g separator-color) :b (:b separator-color) :a (:a separator-color)}] + + ;; Vertical Separator + [{:x (+ panel-x left-w) :y (+ panel-y header-h) :w 1 :h (- panel-h header-h) + :r (:r separator-color) :g (:g separator-color) :b (:b separator-color) :a (:a separator-color)}] + + ;; Content + (filter some? font-rects) + (mapcat identity slider-rects)))))) + +(defn chat-nodes]])) + +(defn build-file-layout + "Build the 3-pane layout for any open file. + ┌──────────────┬──────────────┬──────────────┐ + │ CODE FILE │ CHAT SESSION │ PREVIEW │ + │ (live file) │ (Claude CLI) │ (placeholder)│ + └──────────────┴──────────────┴──────────────┘ + Returns a single rt-node tree spanning the full content area. + shimmer-alpha: 0.0-1.0 pulse for pending tool cards. + collapsed: #{keyword} set of collapsed block ids." + [w h current-file agent-output font-size shimmer-alpha collapsed + & {:keys [local-world active-pane char-advance chat-scroll-y chat-input focus] + :or {local-world nil active-pane :editor char-advance nil chat-scroll-y 0 + chat-input {:text "" :cursor 0} focus :editor}}] + (let [colors (:colors dt) + surfaces (:surfaces dt) + fg (:fg colors) + fg-dim (:fg-muted colors) + border (:border colors) + ;; Pane widths come from semantic pane descriptors when present. + code-w (int (* w (ws/pane-width-pct local-world :main 0.4))) + chat-w (int (* w (ws/pane-width-pct local-world :right 0.55))) + render-w (- w code-w chat-w) + ;; Header height — 36px (4px grid rhythm) + header-h 36 + ;; Text helpers — use typography hierarchy + fs (max font-size (:md (:font-sizes dt))) + fs-hdr (:size typo-subtitle) + hdr-alpha (:a typo-subtitle) + line-h (+ fs 4) + text-y (+ fs 6) + pad (:lg (:spacing dt)) + char-advance (or char-advance (* fs 0.56)) + ;; Surface colors for depth hierarchy — focused pane gets elevated, others sunken + elevated-bg (or (:elevated surfaces) (:bg-elevated colors)) + sunken-bg (or (:sunken surfaces) (:bg colors)) + hdr-bg elevated-bg + ;; Chat pane uses a warm dark bg (matching terminal #090200 feel) + chat-warm-bg [0.06 0.04 0.03 1.0] + ;; Per-pane background — focus tracked but same bg (no highlight shift) + code-bg (if (= active-pane :editor) sunken-bg sunken-bg) + chat-bg (if (= active-pane :chat) chat-warm-bg chat-warm-bg) + preview-bg (if (= active-pane :preview) sunken-bg sunken-bg) + ;; Header text — brighter for focused pane + text-primary (or (:text-primary surfaces) fg) + text-secondary (or (:text-secondary surfaces) fg-dim) + + ;; === CODE PANE (left) — real editor renders beneath, we just add header + border === + code-header-label (or (:name current-file) + (get-in local-world [:selected-artifact :name]) + "No file open") + + code-focused? (= active-pane :editor) + code-hdr-fg (if code-focused? text-primary text-secondary) + code-pane + (rt-node :file-code-pane :panel + {:x 0 :y 0 :w code-w :h h} + :style {:bg code-bg} + :children + (cond-> [(rt-node :file-code-hdr :header + {:x 0 :y 0 :w code-w :h header-h} + :style {:bg hdr-bg + :border-widths [0 0 1 0] + :border-color (:border-subtle colors)} + :text [{:text code-header-label :type :keyword + :from 0 :to (count code-header-label) + :x pad :y 24 :size fs-hdr + :r (nth code-hdr-fg 0) :g (nth code-hdr-fg 1) + :b (nth code-hdr-fg 2) :a hdr-alpha}])] + ;; Focus indicator: 2px bottom accent underline on focused pane header + code-focused? + (conj (rt-node :file-code-focus :accent-bar + {:x pad :y (- header-h 2) :w 40 :h 2} + :style {:bg (or (:accent surfaces) (:accent colors)) + :radius 1})))) + + ;; === CHAT PANE (center) — typed block rendering === + a-status (:status agent-output) + failed? (= :failed a-status) + complete? (= :complete a-status) + running? (or (= :running a-status) (= :submitting a-status)) + chat-status (cond + running? "Streaming..." + failed? "Failed" + complete? "Complete" + :else "Idle") + chat-header-str (str "Chat - " chat-status) + chat-input-h 36 ;; height of the chat input bar + chat-body-h (- h header-h chat-input-h) + trail (:trail agent-output) + ;; Build chat body children: either typed blocks from trail, or placeholder + chat-children + (if (seq trail) + ;; Status header + typed block nodes + (let [provider-name (some-> (:provider agent-output) name str/upper-case) + prompt-text (:prompt agent-output) + status-label (str "[" (or provider-name "AI") "] " (when a-status (name a-status)) ": " prompt-text) + status-c (case a-status + :complete {:r 0.55 :g 0.9 :b 0.55 :a 1.0} + :failed {:r 0.95 :g 0.45 :b 0.45 :a 1.0} + :running {:r 0.6 :g 0.8 :b 1.0 :a 1.0} + :submitting {:r 0.6 :g 0.8 :b 1.0 :a 1.0} + {:r 0.75 :g 0.75 :b 0.75 :a 1.0}) + status-node (rt-node :chat-status-hdr :reasoning-block + {:x 0 :y 0 :w chat-w :h (+ line-h 4)} + :text [{:text status-label :type :keyword + :from 0 :to (count status-label) + :x pad :y (+ fs 2) :size fs + :r (:r status-c) :g (:g status-c) + :b (:b status-c) :a (:a status-c)}]) + block-nodes (trail->chat-nodes trail chat-w fs char-advance + (or shimmer-alpha 0.4) (or collapsed #{}))] + (into [status-node] block-nodes)) + ;; No trail — centered empty state + (if (seq (or (:output agent-output) "")) + ;; Has flat output text — render it + (let [result-text (:output agent-output) + c {:r 0.55 :g 0.55 :b 0.60 :a 0.7} + chat-max-chars (max 20 (int (/ (- chat-w (* 2 pad)) char-advance))) + all-lines (vec (mapcat #(wrap-line % chat-max-chars) (str/split-lines result-text))) + text-ops (vec (map-indexed + (fn [i line] + {:text line :type :comment + :from 0 :to (count line) + :x pad :y (+ fs (* i line-h)) + :size fs :r (:r c) :g (:g c) :b (:b c) :a (:a c)}) + all-lines))] + [(rt-node :chat-placeholder :text-block + {:x 0 :y 0 :w chat-w :h (+ fs (* (count all-lines) line-h))} + :text text-ops)]) + ;; Empty — centered icon + headline + description + [(build-empty-state :chat-empty chat-w chat-body-h + {:icon "--" + :headline "No session" + :description "Type a prompt below to start a conversation."})])) + + ;; Chat scroll: use interactive scroll-y, clamped to content bounds + chat-content-h (reduce + 0 (map #(get-in % [:bounds :h] 0) chat-children)) + max-chat-scroll (max 0 (- chat-content-h chat-body-h)) + chat-scroll-offset (min chat-scroll-y max-chat-scroll) + + ;; Chat header status color + chat-hdr-accent (cond + running? (:accent colors) + complete? (:success colors) + failed? (:destructive colors) + :else nil) + + chat-focused? (= active-pane :chat) + chat-hdr-fg (if chat-focused? text-primary text-secondary) + + chat-pane + (rt-node :file-chat-pane :panel + {:x code-w :y 0 :w chat-w :h h} + :style {:bg chat-bg} + :children + (cond-> [(rt-node :file-chat-hdr :header + {:x 0 :y 0 :w chat-w :h header-h} + :style {:bg hdr-bg + :border-widths [0 0 1 0] + :border-color (:border-subtle colors)} + :text [{:text chat-header-str :type :keyword + :from 0 :to (count chat-header-str) + :x pad :y 24 :size fs-hdr + :r (nth chat-hdr-fg 0) :g (nth chat-hdr-fg 1) + :b (nth chat-hdr-fg 2) :a hdr-alpha}]) + (rt-node :file-chat-body :panel-content + {:x 0 :y header-h :w chat-w :h chat-body-h} + :clip? true + :children + [(rt-node :file-chat-scroll :scroll-container + {:x 0 :y (- 4 chat-scroll-offset) :w chat-w :h (+ chat-content-h 8)} + :layout {:direction :column :gap 4 :padding [0 0 0 0]} + :children chat-children)]) + ;; === CHAT INPUT BAR (bottom of chat pane) === + (let [input-y (- h chat-input-h) + ci-text (:text chat-input) + ci-cursor (:cursor chat-input) + chat-focused? (= focus :chat) + prompt-str "> " + prompt-len (count prompt-str) + display-text (str prompt-str ci-text) + placeholder? (and (empty? ci-text) (not chat-focused?)) + input-fg (if chat-focused? + {:r 0.85 :g 0.84 :b 0.83 :a 1.0} + {:r 0.50 :g 0.49 :b 0.48 :a 0.7}) + prompt-fg {:r 0.45 :g 0.70 :b 0.45 :a 0.9} + placeholder-fg {:r 0.45 :g 0.43 :b 0.41 :a 0.5} + ;; Input text ops + input-text-ops + (if placeholder? + [{:text "Type a message..." :type :comment + :from 0 :to 18 + :x (+ pad (* prompt-len char-advance)) :y (+ fs 10) :size fs + :r (:r placeholder-fg) :g (:g placeholder-fg) + :b (:b placeholder-fg) :a (:a placeholder-fg)} + {:text prompt-str :type :keyword + :from 0 :to prompt-len + :x pad :y (+ fs 10) :size fs + :r (:r prompt-fg) :g (:g prompt-fg) + :b (:b prompt-fg) :a (:a prompt-fg)}] + [{:text prompt-str :type :keyword + :from 0 :to prompt-len + :x pad :y (+ fs 10) :size fs + :r (:r prompt-fg) :g (:g prompt-fg) + :b (:b prompt-fg) :a (:a prompt-fg)} + {:text ci-text :type :keyword + :from 0 :to (count ci-text) + :x (+ pad (* prompt-len char-advance)) :y (+ fs 10) :size fs + :r (:r input-fg) :g (:g input-fg) + :b (:b input-fg) :a (:a input-fg)}]) + ;; Caret rect (only when focused) + caret-x (+ pad (* (+ prompt-len ci-cursor) char-advance)) + input-children + (if chat-focused? + [(rt-node :chat-input-caret :rect + {:x caret-x :y 8 :w 2 :h (+ fs 4)} + :style {:bg [0.85 0.84 0.83 1.0]})] + [])] + (rt-node :file-chat-input :panel + {:x 0 :y input-y :w chat-w :h chat-input-h} + :style {:bg [0.08 0.06 0.05 1.0] + :border-widths [1 0 0 0] + :border-color (:border-subtle colors)} + :text input-text-ops + :children input-children))] + ;; Status accent: bottom underline for focus, left bar for streaming status + chat-hdr-accent + (conj (rt-node :file-chat-status :accent-bar + {:x 0 :y 0 :w 2 :h header-h} + :style {:bg chat-hdr-accent})) + chat-focused? + (conj (rt-node :file-chat-focus :accent-bar + {:x pad :y (- header-h 2) :w 40 :h 2} + :style {:bg (or (:accent surfaces) (:accent colors)) + :radius 1})))) + + ;; === PREVIEW PANE (right) === + render-label "Preview" + render-msg "No preview" + + preview-focused? (= active-pane :preview) + preview-hdr-fg (if preview-focused? text-primary text-secondary) + + render-pane + (rt-node :file-render-pane :panel + {:x (+ code-w chat-w) :y 0 :w render-w :h h} + :style {:bg preview-bg} + :children + (cond-> [(rt-node :file-render-hdr :header + {:x 0 :y 0 :w render-w :h header-h} + :style {:bg hdr-bg + :border-widths [0 0 1 0] + :border-color (:border-subtle colors)} + :text [{:text render-label :type :keyword + :from 0 :to (count render-label) + :x pad :y 24 :size fs-hdr + :r (nth preview-hdr-fg 0) :g (nth preview-hdr-fg 1) + :b (nth preview-hdr-fg 2) :a hdr-alpha}]) + (rt-node :file-render-body :panel-content + {:x 0 :y header-h :w render-w :h (- h header-h)} + :children [(build-empty-state :preview-empty render-w (- h header-h) + {:icon "[]" + :headline "No preview" + :description "Preview will appear when a component is compiled."})])] + preview-focused? + (conj (rt-node :file-render-focus :accent-bar + {:x pad :y (- header-h 2) :w 40 :h 2} + :style {:bg (or (:accent surfaces) (:accent colors)) + :radius 1}))))] + + ;; Root: spans full content area — transparent so child pane bgs define depth + (rt-node :file-layout-root :panel + {:x 0 :y 0 :w w :h h} + :children [code-pane chat-pane render-pane]))) + diff --git a/src/app/client/workspace/sidebar.cljs b/src/app/client/workspace/sidebar.cljs new file mode 100644 index 0000000..4bd6eb3 --- /dev/null +++ b/src/app/client/workspace/sidebar.cljs @@ -0,0 +1,317 @@ +(ns app.client.workspace.sidebar + "File explorer sidebar: constants, tree flattening, and rect-tree builder." + (:require [clojure.string :as str] + [clojure.set :as set] + [app.client.workspace.rect-tree :refer [rt-node]] + [app.client.workspace.ui-primitives :as ui + :refer [dt typo-title typo-subtitle typo-body typo-caption]])) + +;; ============================================================================ +;; SIDEBAR CONSTANTS (WebGPU-native sidebar) +;; ============================================================================ + +(def sidebar-w 256) +(def sidebar-tab-h 36) +(def sidebar-row-h 32) +(def sidebar-back-h 48) +(def cmd-panel-h 40) +(def status-bar-h 24) +(def sidebar-breadcrumb-h 24) +(def sidebar-indent-px 14) +(def sidebar-padding-x 16) +(def sidebar-item-inset 8) +(def sidebar-font-size 13) + +(defn derive-effective-sidebar + "Derive the effective sidebar state from truth, overlay, and ui layers." + [truth overlay ui] + (let [project (or (:pending-project overlay) (:project truth)) + project (when (:path project) project) + expanded (set/difference + (set/union (or (:expanded-dirs truth) #{}) + (or (:pending-expanded-dirs overlay) #{})) + (or (:pending-collapsed-dirs overlay) #{})) + selected (or (:pending-selected-file overlay) (:selected-file truth)) + selected (when (:path selected) selected)] + {:project project + :expanded-dirs expanded + :selected-file selected + :dir-cache (:dir-cache ui) + :home-dirs (:home-dirs ui) + :scroll-y (:scroll-y ui) + :in-flight-dirs (:in-flight-dirs ui)})) + +(defn path->id + "Domain-identity keyword from a filesystem path. + Uses (keyword prefix path) — the prefix becomes the keyword namespace, + the raw path becomes the name. Collision-free, fully inspectable. + E.g. (path->id \"e\" \"/home/sid/foo.cljs\") → :e//home/sid/foo.cljs" + [prefix path] + (keyword prefix path)) + +(defn split-filename + "Split a filename into [stem extension] at the last dot. + Handles dotfiles (.gitignore → ['.gitignore' nil]), no-ext (Makefile → ['Makefile' nil]), + and truncated names ending in '..' (returned as-is, no split)." + [name] + (if (str/ends-with? name "..") + [name nil] ;; Truncated — don't split the '..' marker + (let [dot-idx (str/last-index-of name ".")] + (if (and dot-idx (pos? dot-idx)) + [(subs name 0 dot-idx) (subs name dot-idx)] + [name nil])))) + +(defn flatten-file-tree + "Recursively walk dir-cache tree and return a flat vector of row descriptors. + Each row: {:entry {:name :path :type} :depth N :expanded? bool :active? bool} + Dirs listed before files at each level, both sorted alphabetically." + [entries expanded-dirs selected-file cache depth] + (let [sorted (sort-by (fn [e] [(if (= (:type e) :dir) 0 1) + (str/lower-case (or (:name e) ""))]) + entries)] + (into [] + (mapcat + (fn [entry] + (let [is-dir? (= (:type entry) :dir) + is-exp? (and is-dir? (contains? expanded-dirs (:path entry))) + is-active? (and (not is-dir?) selected-file + (= (:path entry) (:path selected-file))) + row {:entry entry :depth depth :expanded? is-exp? :active? is-active?} + children (when (and is-dir? is-exp?) + (when-let [child-entries (get cache (:path entry))] + (flatten-file-tree child-entries expanded-dirs selected-file + cache (inc depth))))] + (if children + (into [row] children) + [row])))) + sorted))) + +(defn compute-sidebar-content-height + "Total content height in px for sidebar scroll clamping." + [sidebar-state] + (let [{:keys [project expanded-dirs dir-cache home-dirs selected-file]} sidebar-state] + (if (nil? project) + ;; Home dirs list + (* (count (or home-dirs [])) sidebar-row-h) + ;; File tree + (let [root-entries (get dir-cache (:path project) []) + flat (flatten-file-tree root-entries expanded-dirs selected-file dir-cache 0)] + (* (count flat) sidebar-row-h))))) + +;; ============================================================================ +;; SIDEBAR TREE BUILDER (rect tree for file sidebar) +;; ============================================================================ + +(defn build-sidebar-tree + "Build the file sidebar scene graph. Pure function, same pattern as build-intake-tree. + Returns a single rt-node tree. Walk with tree->rects for GPU rects, tree->text-ops for text. + The sidebar root is pinned to the viewport via scroll-y offset. + hover-id is a separate arg (not in sidebar-state) so hover doesn't trigger + expensive flows that watch sidebar-state." + [sidebar-state sidebar-visible? + viewport-h scroll-y font-size char-advance hover-id] + (when sidebar-visible? + (let [{:keys [project expanded-dirs dir-cache home-dirs selected-file]} sidebar-state + hovered-id hover-id + sidebar-scroll-y (or (:scroll-y sidebar-state) 0) + sb-w sidebar-w + sb-font font-size + sb-char-advance (* sb-font 0.56) + max-chars (max 8 (int (/ (- sb-w (* 2 sidebar-padding-x)) sb-char-advance))) + colors (:colors dt) + + ;; Right border line (1px separator between sidebar and content) + border-node (rt-node :sidebar-border :chrome + {:x (dec sb-w) :y 0 :w 1 :h viewport-h} + :style {:bg (:border colors)}) + + ;; Content fills full height (no tab bar) + content-top 0 + content-h viewport-h + + content-children + (cond + ;; No project selected — show home dirs + (nil? project) + (let [;; Header + explorer-label "EXPLORER" + ;; Overline typography for category labels + text-muted-c (or (:text-muted (:surfaces dt)) [0.36 0.42 0.50 1.0]) + header-node (rt-node :explorer-hdr :header + {:x 0 :y 0 :w sb-w :h sidebar-back-h} + :style {:bg (:bg-elevated colors) + :border-widths [0 0 1 0] + :border-color (:border-subtle colors)} + :text [{:text explorer-label :type :comment + :from 0 :to (count explorer-label) + :x sidebar-padding-x :y 28 + :size 11 + :r (nth text-muted-c 0) :g (nth text-muted-c 1) + :b (nth text-muted-c 2) :a (nth text-muted-c 3)}]) + ;; Dir list items + dir-items + (if (nil? home-dirs) + ;; Loading state + [(rt-node :home-loading :text-block + {:x 0 :y 0 :w sb-w :h sidebar-row-h} + :text [{:text "Loading..." :type :comment + :from 0 :to 10 + :x sidebar-padding-x :y 20 + :size sb-font + :r 0.40 :g 0.40 :b 0.45 :a 0.6}])] + ;; Render each home dir (semantic path-based IDs) + (mapv + (fn [i d] + (let [id-kw (path->id "hd" (:path d)) + name-str (str "▸ " (:name d)) + hovered? (= id-kw hovered-id) + text-y (+ (/ sidebar-row-h 2) (/ sb-font 2.5))] + (rt-node id-kw :sidebar-entry + {:x 0 :y 0 :w sb-w :h sidebar-row-h} + :data {:entry-type :home-dir :entry d :idx i} + :style (when hovered? + {:bg (:bg-hover colors) + :radius (:sm (:radii dt))}) + :children + (if hovered? + [(rt-node (keyword (namespace id-kw) (str (name id-kw) "_hl")) :highlight + {:x sidebar-item-inset :y 2 + :w (- sb-w (* 2 sidebar-item-inset)) :h (- sidebar-row-h 4)} + :style {:bg (:bg-hover colors) + :radius (:sm (:radii dt))})] + []) + :text [{:text name-str :type :text + :from 0 :to (count name-str) + :x sidebar-padding-x :y text-y + :size sb-font + :r 0.65 :g 0.65 :b 0.70 :a 1.0}]))) + (range) home-dirs))] + (into [header-node] dir-items)) + + ;; Files mode, project selected — file tree + :else + (let [;; Back button + back-label (str "← " (:name project)) + subtitle "Project Workspace" + back-node (rt-node :back-btn :sidebar-entry + {:x 0 :y 0 :w sb-w :h sidebar-back-h} + :data {:entry-type :back-btn} + :style {:bg (:bg-elevated colors) + :border-widths [0 0 1 0] + :border-color (:border-subtle colors)} + :text [{:text back-label :type :text + :from 0 :to (count back-label) + :x sidebar-padding-x :y 26 + :size (:size typo-subtitle) + :r 0.90 :g 0.90 :b 0.92 :a (:a typo-subtitle)} + {:text subtitle :type :comment + :from 0 :to (count subtitle) + :x (+ sidebar-padding-x 16) :y 40 + :size (:size typo-body) + :r 0.55 :g 0.55 :b 0.60 :a 0.7}]) + ;; Breadcrumb (when file is open) + breadcrumb-node + (when selected-file + (let [fname (:name selected-file)] + (rt-node :breadcrumb :text-block + {:x 0 :y 0 :w sb-w :h sidebar-breadcrumb-h} + :style {:bg [0.05 0.05 0.06 1.0] + :border-widths [0 0 1 0] + :border-color (:border-subtle colors)} + :text [{:text fname :type :comment + :from 0 :to (count fname) + :x sidebar-padding-x :y 17 + :size (:size typo-body) + :r 0.55 :g 0.55 :b 0.60 :a 0.8}]))) + ;; Flatten the file tree + root-entries (get dir-cache (:path project) []) + flat-rows (flatten-file-tree root-entries expanded-dirs selected-file dir-cache 0) + ;; Build file entry nodes + file-nodes + (mapv + (fn [i {:keys [entry depth expanded? active?]}] + (let [is-dir? (= (:type entry) :dir) + id-kw (path->id "e" (:path entry)) + id-ns (namespace id-kw) + id-nm (name id-kw) + hovered? (= id-kw hovered-id) + indent (* depth sidebar-indent-px) + chevron (cond + (not is-dir?) " " + expanded? "▾ " + :else "▸ ") + label (str chevron (:name entry)) + avail-chars (max 5 (- max-chars (int (/ indent sb-char-advance)))) + trunc (if (> (count label) avail-chars) + (str (subs label 0 (- avail-chars 2)) "..") + label) + fg-color (cond + active? [0.95 0.95 0.98 1.0] + :else (:fg colors)) + ;; Highlight background + inner-bg (cond + active? (:bg-selected colors) + hovered? (:bg-hover colors) + :else nil) + highlight (when inner-bg + (rt-node (keyword id-ns (str id-nm "_hl")) :highlight + {:x sidebar-item-inset :y 2 + :w (- sb-w (* 2 sidebar-item-inset)) :h (- sidebar-row-h 4)} + :style {:bg inner-bg + :radius (:sm (:radii dt))})) + ;; Active accent bar (2px, inner-left edge) + accent-bar (when active? + (rt-node (keyword id-ns (str id-nm "_acc")) :accent-bar + {:x (+ sidebar-item-inset 1) :y 6 + :w 2 :h (- sidebar-row-h 12)} + :style {:bg (:accent colors) + :radius (:sm (:radii dt))})) + ;; Indent guide lines (1px vertical per depth level) + guide-color (:border colors) + indent-guides + (when (pos? depth) + (mapv (fn [d] + (let [gx (+ sidebar-padding-x (* d sidebar-indent-px) (/ sidebar-indent-px 2))] + (rt-node (keyword id-ns (str id-nm "_g" d)) :indent-guide + {:x gx :y 0 :w 1 :h sidebar-row-h} + :style {:bg [(nth guide-color 0) (nth guide-color 1) + (nth guide-color 2) 0.10]}))) + (range depth))) + text-y (+ (/ sidebar-row-h 2) (/ sb-font 2.5))] + (rt-node id-kw :sidebar-entry + {:x 0 :y 0 :w sb-w :h sidebar-row-h} + :data {:entry-type (if is-dir? :dir :file) :entry entry :idx i + :expanded? expanded? :active? active? :depth depth} + :children (cond-> [] + highlight (conj highlight) + accent-bar (conj accent-bar) + indent-guides (into indent-guides)) + :text [{:text trunc :type :text + :from 0 :to (count trunc) + :x (+ sidebar-padding-x indent) :y text-y + :size sb-font + :r (nth fg-color 0) :g (nth fg-color 1) + :b (nth fg-color 2) :a (nth fg-color 3)}]))) + (range) flat-rows)] + (cond-> [back-node] + breadcrumb-node (conj breadcrumb-node) + true (into file-nodes)))) + + ;; Scrollable content area (clips children) + ;; Inner scroll container: offset by -scroll-y, layout positions children, + ;; outer clip-node hides overflow + scroll-inner (rt-node :sidebar-scroll-inner :container + {:x 0 :y (- sidebar-scroll-y) :w sb-w :h 99999} + :layout {:direction :column} + :children (vec content-children)) + content-node (rt-node :sidebar-content :panel-content + {:x 0 :y content-top :w sb-w :h content-h} + :clip? true + :children [scroll-inner])] + + ;; Root node: pinned to viewport via scroll-y + (rt-node :sidebar-root :panel + {:x 0 :y scroll-y :w sb-w :h viewport-h} + :style {:bg (or (:base (:surfaces dt)) (:bg colors))} + :children [border-node content-node])))) + diff --git a/src/app/client/workspace/text_input.cljs b/src/app/client/workspace/text_input.cljs new file mode 100644 index 0000000..e957071 --- /dev/null +++ b/src/app/client/workspace/text_input.cljs @@ -0,0 +1,580 @@ +(ns app.client.workspace.text-input + (:require [clojure.string :as str])) + +(defn- clamp [v min-v max-v] + (-> v (max min-v) (min max-v))) + +(defn- whitespace? [ch] + (boolean (re-matches #"\s" (str ch)))) + +(defn- word-char? [ch] + (boolean (re-matches #"\w" (str ch)))) + +(defn- normalize-single-selection [selection text-len] + (when (and selection (number? (:start selection)) (number? (:end selection))) + (let [start (clamp (int (:start selection)) 0 text-len) + end (clamp (int (:end selection)) 0 text-len) + [s e] (if (<= start end) [start end] [end start])] + (when (not= s e) + {:start s :end e})))) + +(defn- normalize-pos [lines pos] + (let [line-count (count lines) + line (clamp (int (or (:line pos) 0)) 0 (max 0 (dec line-count))) + line-text (get lines line "") + col (clamp (int (or (:col pos) 0)) 0 (count line-text))] + {:line line :col col})) + +(defn- pos<= [a b] + (or (< (:line a) (:line b)) + (and (= (:line a) (:line b)) + (<= (:col a) (:col b))))) + +(defn- normalize-multi-selection [selection lines] + (when (and selection (map? (:start selection)) (map? (:end selection))) + (let [start (normalize-pos lines (:start selection)) + end (normalize-pos lines (:end selection)) + [s e] (if (pos<= start end) [start end] [end start])] + (when (not (and (= (:line s) (:line e)) + (= (:col s) (:col e)))) + {:start s :end e})))) + +(defn- normalize-single [state] + (let [text (or (:text state) "") + len (count text) + cursor (clamp (int (or (:cursor state) 0)) 0 len) + selection (normalize-single-selection (:selection state) len)] + (assoc state :text text :cursor cursor :selection selection))) + +(defn- normalize-multi [state] + (let [lines (vec (or (:lines state) [""])) + lines (if (seq lines) lines [""]) + cursor (normalize-pos lines (or (:cursor state) {:line 0 :col 0})) + desired (or (:desired-col state) (:col cursor)) + selection (normalize-multi-selection (:selection state) lines)] + (assoc state + :lines lines + :cursor cursor + :desired-col desired + :selection selection))) + +(defn- resolve-line-lengths [lines line-lengths] + (if (and line-lengths (= (count line-lengths) (count lines))) + line-lengths + (mapv count lines))) + +(declare delete-selection) + +;; === Character Operations === + +(defn insert-char + "Insert character at cursor position. Works for single or multi-line." + [state ch multi-line?] + (if (nil? ch) + state + (let [ch-str (str ch)] + (if (zero? (count ch-str)) + state + (if multi-line? + (let [state (normalize-multi state) + state (if (:selection state) (delete-selection state true) state) + {:keys [lines cursor]} state + line-idx (:line cursor) + col (:col cursor) + current-line (get lines line-idx "") + before (subs current-line 0 col) + after (subs current-line col) + parts (str/split ch-str #"\n" -1) + part-count (count parts)] + (if (= part-count 1) + (let [new-line (str before (first parts) after) + new-lines (assoc lines line-idx new-line) + new-col (+ col (count (first parts)))] + (assoc state :lines new-lines + :cursor {:line line-idx :col new-col} + :desired-col new-col + :selection nil)) + (let [first-line (str before (first parts)) + last-line (str (last parts) after) + middle (subvec (vec parts) 1 (dec part-count)) + new-lines (vec (concat (subvec lines 0 line-idx) + [first-line] + middle + [last-line] + (subvec lines (inc line-idx)))) + new-line-idx (+ line-idx (dec part-count)) + new-col (count (last parts))] + (assoc state :lines new-lines + :cursor {:line new-line-idx :col new-col} + :desired-col new-col + :selection nil)))) + (let [state (normalize-single state) + state (if (:selection state) (delete-selection state false) state) + {:keys [text cursor]} state + before (subs text 0 cursor) + after (subs text cursor) + new-text (str before ch-str after) + new-cursor (+ cursor (count ch-str))] + (assoc state :text new-text :cursor new-cursor :selection nil))))))) + +(defn delete-backward + "Delete character before cursor (backspace)." + [state multi-line?] + (if multi-line? + (let [state (normalize-multi state)] + (if (:selection state) + (delete-selection state true) + (let [{:keys [lines cursor]} state + line-idx (:line cursor) + col (:col cursor) + current-line (get lines line-idx "")] + (cond + (> col 0) + (let [before (subs current-line 0 (dec col)) + after (subs current-line col) + new-line (str before after) + new-lines (assoc lines line-idx new-line) + new-col (dec col)] + (assoc state :lines new-lines + :cursor {:line line-idx :col new-col} + :desired-col new-col + :selection nil)) + + (> line-idx 0) + (let [prev-line (get lines (dec line-idx) "") + prev-len (count prev-line) + merged (str prev-line current-line) + new-lines (vec (concat (subvec lines 0 (dec line-idx)) + [merged] + (subvec lines (inc line-idx))))] + (assoc state :lines new-lines + :cursor {:line (dec line-idx) :col prev-len} + :desired-col prev-len + :selection nil)) + + :else state)))) + (let [state (normalize-single state)] + (if (:selection state) + (delete-selection state false) + (let [{:keys [text cursor]} state] + (if (> cursor 0) + (let [before (subs text 0 (dec cursor)) + after (subs text cursor) + new-text (str before after) + new-cursor (dec cursor)] + (assoc state :text new-text :cursor new-cursor :selection nil)) + state)))))) + +(defn delete-forward + "Delete character after cursor (delete key)." + [state multi-line?] + (if multi-line? + (let [state (normalize-multi state)] + (if (:selection state) + (delete-selection state true) + (let [{:keys [lines cursor]} state + line-idx (:line cursor) + col (:col cursor) + current-line (get lines line-idx "") + line-len (count current-line) + max-line (dec (count lines))] + (cond + (< col line-len) + (let [before (subs current-line 0 col) + after (subs current-line (inc col)) + new-line (str before after) + new-lines (assoc lines line-idx new-line)] + (assoc state :lines new-lines :selection nil)) + + (< line-idx max-line) + (let [next-line (get lines (inc line-idx) "") + merged (str current-line next-line) + new-lines (vec (concat (subvec lines 0 line-idx) + [merged] + (subvec lines (+ line-idx 2))))] + (assoc state :lines new-lines :selection nil)) + + :else state)))) + (let [state (normalize-single state)] + (if (:selection state) + (delete-selection state false) + (let [{:keys [text cursor]} state + text-len (count text)] + (if (< cursor text-len) + (let [before (subs text 0 cursor) + after (subs text (inc cursor)) + new-text (str before after)] + (assoc state :text new-text :selection nil)) + state)))))) + +;; === Navigation === + +(defn move-cursor + "Move cursor in direction (:left :right :up :down :home :end)." + [state direction multi-line? line-lengths] + (if multi-line? + (let [state (normalize-multi state) + {:keys [lines cursor]} state + line-lengths (resolve-line-lengths lines line-lengths) + line (:line cursor) + col (:col cursor) + desired (:desired-col state) + max-line (dec (count line-lengths)) + line-len (get line-lengths line 0) + [new-pos new-desired] + (case direction + :left + (let [np (if (> col 0) + {:line line :col (dec col)} + (if (> line 0) + (let [prev-len (get line-lengths (dec line) 0)] + {:line (dec line) :col prev-len}) + cursor))] + [np (:col np)]) + + :right + (let [np (if (< col line-len) + {:line line :col (inc col)} + (if (< line max-line) + {:line (inc line) :col 0} + cursor))] + [np (:col np)]) + + :up + (if (> line 0) + (let [prev-len (get line-lengths (dec line) 0)] + [{:line (dec line) :col (min desired prev-len)} desired]) + [cursor desired]) + + :down + (if (< line max-line) + (let [next-len (get line-lengths (inc line) 0)] + [{:line (inc line) :col (min desired next-len)} desired]) + [cursor desired]) + + :home + [{:line line :col 0} 0] + + :end + [{:line line :col line-len} line-len] + + [cursor desired])] + (assoc state :cursor new-pos :desired-col new-desired :selection nil)) + (let [state (normalize-single state) + {:keys [text cursor]} state + text-len (count text) + new-cursor (case direction + :left (max 0 (dec cursor)) + :right (min text-len (inc cursor)) + :home 0 + :end text-len + cursor)] + (assoc state :cursor new-cursor :selection nil)))) + +(defn move-word + "Move cursor by word (:left :right)." + [state direction multi-line? line-lengths] + (if multi-line? + (let [state (normalize-multi state) + {:keys [lines cursor]} state + line-lengths (resolve-line-lengths lines line-lengths) + line-idx (:line cursor) + col (:col cursor) + max-line (dec (count line-lengths)) + current-line (get lines line-idx "") + new-pos + (case direction + :left + (if (= col 0) + (if (> line-idx 0) + {:line (dec line-idx) :col (get line-lengths (dec line-idx) 0)} + cursor) + (let [before (subs current-line 0 col) + skip-ws (loop [i (dec (count before))] + (if (and (>= i 0) (whitespace? (nth before i))) + (recur (dec i)) + i)) + new-col (loop [i skip-ws] + (if (and (>= i 0) (word-char? (nth before i))) + (recur (dec i)) + (inc i)))] + {:line line-idx :col (max 0 new-col)})) + + :right + (let [line-len (count current-line)] + (if (= col line-len) + (if (< line-idx max-line) + {:line (inc line-idx) :col 0} + cursor) + (let [after (subs current-line col) + skip-word (loop [i 0] + (if (and (< i (count after)) (word-char? (nth after i))) + (recur (inc i)) + i)) + new-col (loop [i skip-word] + (if (and (< i (count after)) (whitespace? (nth after i))) + (recur (inc i)) + i))] + {:line line-idx :col (+ col new-col)}))) + cursor)] + (assoc state :cursor new-pos :desired-col (:col new-pos) :selection nil)) + (let [state (normalize-single state) + {:keys [text cursor]} state + text-len (count text) + new-cursor + (case direction + :left + (if (zero? cursor) + 0 + (let [before (subs text 0 cursor) + skip-ws (loop [i (dec (count before))] + (if (and (>= i 0) (whitespace? (nth before i))) + (recur (dec i)) + i)) + new-col (loop [i skip-ws] + (if (and (>= i 0) (word-char? (nth before i))) + (recur (dec i)) + (inc i)))] + (max 0 new-col))) + + :right + (if (= cursor text-len) + text-len + (let [after (subs text cursor) + skip-word (loop [i 0] + (if (and (< i (count after)) (word-char? (nth after i))) + (recur (inc i)) + i)) + new-col (loop [i skip-word] + (if (and (< i (count after)) (whitespace? (nth after i))) + (recur (inc i)) + i))] + (+ cursor new-col))) + cursor)] + (assoc state :cursor new-cursor :selection nil)))) + +;; === Selection === + +(defn select-all [state multi-line?] + (if multi-line? + (let [state (normalize-multi state) + lines (:lines state) + last-line (max 0 (dec (count lines))) + last-col (count (get lines last-line ""))] + (assoc state + :selection {:start {:line 0 :col 0} + :end {:line last-line :col last-col}} + :cursor {:line last-line :col last-col} + :desired-col last-col)) + (let [state (normalize-single state) + text (:text state) + text-len (count text)] + (assoc state :selection {:start 0 :end text-len} + :cursor text-len)))) + +(defn get-selected-text [state multi-line?] + (if multi-line? + (let [state (normalize-multi state) + {:keys [selection lines]} state] + (when selection + (let [{:keys [start end]} selection] + (if (= (:line start) (:line end)) + (subs (get lines (:line start) "") (:col start) (:col end)) + (let [first-line (subs (get lines (:line start) "") (:col start)) + middle (for [i (range (inc (:line start)) (:line end))] + (get lines i "")) + last-line (subs (get lines (:line end) "") 0 (:col end))] + (str/join "\n" (concat [first-line] middle [last-line]))))))) + (let [state (normalize-single state) + {:keys [selection text]} state] + (when selection + (subs text (:start selection) (:end selection)))))) + +(defn delete-selection + "Delete selected text, return new state with cursor at selection start." + [state multi-line?] + (if multi-line? + (let [state (normalize-multi state) + {:keys [selection lines]} state] + (if selection + (let [{:keys [start end]} selection + start-line (:line start) + end-line (:line end) + start-col (:col start) + end-col (:col end)] + (if (= start-line end-line) + (let [line (get lines start-line "") + before (subs line 0 start-col) + after (subs line end-col) + new-lines (assoc lines start-line (str before after))] + (assoc state :lines new-lines + :cursor {:line start-line :col start-col} + :desired-col start-col + :selection nil)) + (let [first-line (subs (get lines start-line "") 0 start-col) + last-line (subs (get lines end-line "") end-col) + merged (str first-line last-line) + new-lines (vec (concat (subvec lines 0 start-line) + [merged] + (subvec lines (inc end-line))))] + (assoc state :lines new-lines + :cursor {:line start-line :col start-col} + :desired-col start-col + :selection nil)))) + state)) + (let [state (normalize-single state) + {:keys [selection text]} state] + (if selection + (let [start (:start selection) + end (:end selection) + before (subs text 0 start) + after (subs text end) + new-text (str before after)] + (assoc state :text new-text :cursor start :selection nil)) + state)))) + +;; === Clipboard === + +(defn cut + "Cut selection. Returns {:state new-state :text cut-text}." + [state multi-line?] + (if multi-line? + (let [state (normalize-multi state) + {:keys [selection lines cursor]} state] + (if selection + (let [text (get-selected-text state true) + new-state (delete-selection state true)] + {:state new-state :text text}) + (let [line-idx (:line cursor) + line-text (get lines line-idx "") + line-text (str line-text "\n")] + (if (= 1 (count lines)) + {:state (assoc state :lines [""] + :cursor {:line 0 :col 0} + :desired-col 0 + :selection nil) + :text line-text} + (let [new-lines (vec (concat (subvec lines 0 line-idx) + (subvec lines (inc line-idx)))) + new-line-idx (min line-idx (dec (count new-lines)))] + {:state (assoc state :lines new-lines + :cursor {:line new-line-idx :col 0} + :desired-col 0 + :selection nil) + :text line-text}))))) + (let [state (normalize-single state)] + (if (:selection state) + (let [text (get-selected-text state false) + new-state (delete-selection state false)] + {:state new-state :text text}) + {:state state :text nil})))) + +(defn copy + "Copy selection. Returns selected text or current line for multi-line." + [state multi-line?] + (if multi-line? + (let [state (normalize-multi state) + {:keys [selection lines cursor]} state] + (if selection + (get-selected-text state true) + (let [line-text (get lines (:line cursor) "")] + (str line-text "\n")))) + (let [state (normalize-single state)] + (when (:selection state) + (get-selected-text state false))))) + +(defn paste [state text multi-line?] + (if (nil? text) + state + (let [text-str (str text)] + (if (zero? (count text-str)) + state + (if multi-line? + (let [state (normalize-multi state) + state (if (:selection state) (delete-selection state true) state) + {:keys [lines cursor]} state + line-idx (:line cursor) + col (:col cursor) + current-line (get lines line-idx "") + before (subs current-line 0 col) + after (subs current-line col) + parts (str/split text-str #"\n" -1) + part-count (count parts)] + (if (= part-count 1) + (let [new-line (str before (first parts) after) + new-lines (assoc lines line-idx new-line) + new-col (+ col (count (first parts)))] + (assoc state :lines new-lines + :cursor {:line line-idx :col new-col} + :desired-col new-col + :selection nil)) + (let [first-line (str before (first parts)) + last-line (str (last parts) after) + middle (subvec (vec parts) 1 (dec part-count)) + new-lines (vec (concat (subvec lines 0 line-idx) + [first-line] + middle + [last-line] + (subvec lines (inc line-idx)))) + new-line-idx (+ line-idx (dec part-count)) + new-col (count (last parts))] + (assoc state :lines new-lines + :cursor {:line new-line-idx :col new-col} + :desired-col new-col + :selection nil)))) + (let [state (normalize-single state) + state (if (:selection state) (delete-selection state false) state) + {:keys [text cursor]} state + before (subs text 0 cursor) + after (subs text cursor) + new-text (str before text-str after) + new-cursor (+ cursor (count text-str))] + (assoc state :text new-text :cursor new-cursor :selection nil))))))) + +;; === Rendering Helpers === + +(defn calculate-caret-rect + "Calculate caret rectangle for rendering." + [cursor font-size origin-x origin-y line-h visible?] + (when (and cursor visible?) + (let [{:keys [line col]} (if (map? cursor) cursor {:line 0 :col cursor}) + ;; Keep rendering helpers aligned with the runtime/editor 0.56 glyph advance. + char-w (* font-size 0.56) + x (+ origin-x (* col char-w)) + y (+ origin-y (* line line-h))] + {:x x :y y :w 2 :h line-h + :r 0.9 :g 0.9 :b 0.9 :a 1.0}))) + +(defn calculate-selection-rects + "Calculate selection highlight rectangles." + [selection font-size origin-x origin-y line-h line-lengths] + (when selection + (let [{:keys [start end]} selection + multi-line? (and (map? start) (contains? start :line)) + ;; Keep rendering helpers aligned with the runtime/editor 0.56 glyph advance. + char-w (* font-size 0.56) + r 0.2 g 0.4 b 0.9 a 0.5] + (if (not multi-line?) + (let [s (min start end) + e (max start end) + width-chars (- e s)] + (when (> width-chars 0) + [{:x (+ origin-x (* s char-w)) + :y origin-y + :w (* width-chars char-w) + :h line-h + :r r :g g :b b :a a}])) + (let [line-lengths (or line-lengths []) + [s e] (if (pos<= start end) [start end] [end start])] + (keep (fn [line-idx] + (let [line-len (get line-lengths line-idx 0) + col-start (if (= line-idx (:line s)) (:col s) 0) + col-end (if (= line-idx (:line e)) (:col e) line-len) + width-chars (- col-end col-start)] + (when (> width-chars 0) + {:x (+ origin-x (* col-start char-w)) + :y (+ origin-y (* line-idx line-h)) + :w (* width-chars char-w) + :h line-h + :r r :g g :b b :a a}))) + (range (:line s) (inc (:line e))))))))) diff --git a/src/app/client/workspace/themes.cljc b/src/app/client/workspace/themes.cljc new file mode 100644 index 0000000..88681f8 --- /dev/null +++ b/src/app/client/workspace/themes.cljc @@ -0,0 +1,117 @@ +(ns app.client.workspace.themes + "Syntax highlighting themes for the editor. + Extracted to avoid circular dependencies between electric-flow and loop.") + +;; ============================================================================ +;; SYNTAX HIGHLIGHTING THEMES +;; ============================================================================ +;; Each theme maps token types to RGBA colors (0.0-1.0 range for WebGPU) + +(def themes + {:gruvbox-dark + {:name "Gruvbox Dark" + :background {:r 0.06 :g 0.06 :b 0.05 :a 1.0} ;; #0f0f0d (near-black, matches nvim transparent) + :keyword {:r 0.996 :g 0.502 :b 0.098 :a 1.0} ;; #fe8019 orange + :macro {:r 0.556 :g 0.752 :b 0.486 :a 1.0} ;; #8ec07c aqua + :string {:r 0.722 :g 0.733 :b 0.149 :a 1.0} ;; #b8bb26 green + :comment {:r 0.573 :g 0.514 :b 0.455 :a 1.0} ;; #928374 gray (original gruvbox) + :delimiter {:r 0.659 :g 0.600 :b 0.518 :a 0.85} ;; #a89984 fg4 + :number {:r 0.827 :g 0.525 :b 0.608 :a 1.0} ;; #d3869b purple + :character {:r 0.827 :g 0.525 :b 0.608 :a 1.0} ;; #d3869b purple + :boolean {:r 0.827 :g 0.525 :b 0.608 :a 1.0} ;; #d3869b purple + :nil {:r 0.984 :g 0.286 :b 0.204 :a 1.0} ;; #fb4934 red + :text {:r 0.984 :g 0.945 :b 0.780 :a 1.0}} ;; #fbf1c7 fg (bright, gruvbox hard) + + :gruvbox-light + {:name "Gruvbox Light" + :background {:r 0.984 :g 0.945 :b 0.847 :a 1.0} ;; #fbf1c7 + :keyword {:r 0.839 :g 0.365 :b 0.055 :a 1.0} ;; #d65d0e orange + :macro {:r 0.408 :g 0.616 :b 0.416 :a 1.0} ;; #689d6a aqua + :string {:r 0.596 :g 0.592 :b 0.102 :a 1.0} ;; #98971a green + :comment {:r 0.573 :g 0.514 :b 0.455 :a 1.0} ;; #928374 gray + :delimiter {:r 0.404 :g 0.361 :b 0.325 :a 0.7} ;; #665c54 fg dim + :number {:r 0.694 :g 0.384 :b 0.525 :a 1.0} ;; #b16286 purple + :character {:r 0.694 :g 0.384 :b 0.525 :a 1.0} ;; #b16286 purple + :boolean {:r 0.694 :g 0.384 :b 0.525 :a 1.0} ;; #b16286 purple + :nil {:r 0.800 :g 0.141 :b 0.114 :a 1.0} ;; #cc241d red + :text {:r 0.235 :g 0.220 :b 0.212 :a 1.0}} ;; #3c3836 fg + + :rose-pine + {:name "Rosé Pine" + :background {:r 0.114 :g 0.106 :b 0.141 :a 1.0} ;; #191724 + :keyword {:r 0.922 :g 0.576 :b 0.545 :a 1.0} ;; #eb6f92 love + :macro {:r 0.769 :g 0.659 :b 0.890 :a 1.0} ;; #c4a7e7 iris + :string {:r 0.945 :g 0.820 :b 0.545 :a 1.0} ;; #f1d18b gold (adjusted) + :comment {:r 0.475 :g 0.455 :b 0.580 :a 1.0} ;; #797494 muted (brighter) + :delimiter {:r 0.576 :g 0.549 :b 0.659 :a 0.85} ;; #908caa subtle + :number {:r 0.922 :g 0.576 :b 0.545 :a 1.0} ;; #eb6f92 love + :character {:r 0.922 :g 0.820 :b 0.659 :a 1.0} ;; #ebbcba rose + :boolean {:r 0.769 :g 0.659 :b 0.890 :a 1.0} ;; #c4a7e7 iris + :nil {:r 0.922 :g 0.576 :b 0.545 :a 1.0} ;; #eb6f92 love + :text {:r 0.878 :g 0.851 :b 0.914 :a 1.0}} ;; #e0def4 text + + :kanagawa + {:name "Kanagawa" + :background {:r 0.102 :g 0.102 :b 0.137 :a 1.0} ;; #1a1a23 + :keyword {:r 0.886 :g 0.639 :b 0.545 :a 1.0} ;; #e2a38b surimiOrange + :macro {:r 0.498 :g 0.686 :b 0.702 :a 1.0} ;; #7fb4b3 waveAqua2 + :string {:r 0.596 :g 0.737 :b 0.545 :a 1.0} ;; #98bb6c springGreen + :comment {:r 0.510 :g 0.533 :b 0.580 :a 1.0} ;; #828894 fujiGray (brighter) + :delimiter {:r 0.565 :g 0.565 :b 0.659 :a 0.85} ;; #9090a8 dim + :number {:r 0.882 :g 0.557 :b 0.698 :a 1.0} ;; #e18eb2 sakuraPink + :character {:r 0.882 :g 0.557 :b 0.698 :a 1.0} ;; #e18eb2 sakuraPink + :boolean {:r 0.710 :g 0.580 :b 0.780 :a 1.0} ;; #b594c7 oniViolet + :nil {:r 0.886 :g 0.451 :b 0.451 :a 1.0} ;; #e27373 autumnRed + :text {:r 0.863 :g 0.855 :b 0.820 :a 1.0}} ;; #dcdad1 fujiWhite + + :cyberdream + {:name "Cyberdream" + :background {:r 0.063 :g 0.063 :b 0.094 :a 1.0} ;; #101018 + :keyword {:r 1.0 :g 0.380 :b 0.573 :a 1.0} ;; #ff6192 pink + :macro {:r 0.373 :g 0.843 :b 0.961 :a 1.0} ;; #5fd7f5 cyan + :string {:r 0.565 :g 0.933 :b 0.565 :a 1.0} ;; #90ee90 green + :comment {:r 0.480 :g 0.480 :b 0.545 :a 1.0} ;; #7a7a8b gray (brighter) + :delimiter {:r 0.600 :g 0.600 :b 0.700 :a 0.85} ;; #9999b3 dim + :number {:r 0.988 :g 0.722 :b 0.424 :a 1.0} ;; #fcb86c orange + :character {:r 0.988 :g 0.722 :b 0.424 :a 1.0} ;; #fcb86c orange + :boolean {:r 0.816 :g 0.529 :b 0.937 :a 1.0} ;; #d087ef purple + :nil {:r 1.0 :g 0.380 :b 0.573 :a 1.0} ;; #ff6192 pink + :text {:r 0.949 :g 0.949 :b 0.969 :a 1.0}} ;; #f2f2f7 white + + :classic-dark + {:name "Classic Dark" + :background {:r 0.12 :g 0.12 :b 0.14 :a 1.0} + :keyword {:r 0.8 :g 0.4 :b 0.8 :a 1.0} + :macro {:r 0.3 :g 0.6 :b 1.0 :a 1.0} + :string {:r 0.6 :g 0.8 :b 0.4 :a 1.0} + :comment {:r 0.55 :g 0.55 :b 0.55 :a 1.0} + :delimiter {:r 1.0 :g 1.0 :b 1.0 :a 0.75} + :number {:r 1.0 :g 0.6 :b 0.3 :a 1.0} + :character {:r 0.9 :g 0.7 :b 0.4 :a 1.0} + :boolean {:r 0.7 :g 0.3 :b 0.9 :a 1.0} + :nil {:r 0.8 :g 0.3 :b 0.3 :a 1.0} + :text {:r 0.9 :g 0.9 :b 0.9 :a 1.0}}}) + +;; Default theme (can be overridden by settings) +(def default-theme-id :gruvbox-dark) + +;; List of theme IDs for UI +(def theme-ids (keys themes)) + +;; Ordered list for cycling in UI +(def theme-list [:gruvbox-dark :gruvbox-light :rose-pine :kanagawa :cyberdream :classic-dark]) + +(defn get-theme [theme-id] + "Get a theme by ID, falling back to default" + (get themes theme-id (get themes default-theme-id))) + +(defn get-color + "Get color for a token type from the specified theme" + ([type] (get-color type default-theme-id)) + ([type theme-id] + (let [theme (get-theme theme-id)] + (get theme type (:text theme))))) + +(defn theme-index [theme-id] + "Get index of theme in theme-list for UI cycling" + (or (first (keep-indexed (fn [i t] (when (= t theme-id) i)) theme-list)) 0)) diff --git a/src/app/client/workspace/trail.cljs b/src/app/client/workspace/trail.cljs new file mode 100644 index 0000000..b48c599 --- /dev/null +++ b/src/app/client/workspace/trail.cljs @@ -0,0 +1,1337 @@ +(ns app.client.workspace.trail + "Markdown parsing, reasoning trail -> chat node building, agent panel sizing." + (:require [clojure.string :as str] + [app.client.workspace.rect-tree :refer [rt-node wrap-line]] + [app.client.workspace.ui-primitives :as ui :refer [dt typo-title typo-subtitle typo-body typo-caption]])) + +(def md-style-colors + "Colors for inline markdown styles in reasoning blocks. + Tuned for warm terminal-like feel on dark bg." + {:normal {:r 0.82 :g 0.82 :b 0.85 :a 1.0} + :bold {:r 0.94 :g 0.94 :b 0.96 :a 1.0} + :italic {:r 0.87 :g 0.87 :b 0.90 :a 0.98} + :strike {:r 0.58 :g 0.58 :b 0.62 :a 0.88} + :code {:r 0.00 :g 0.63 :b 0.89 :a 1.0} ;; terminal blue (color4 #00a0e4) + :link {:r 0.00 :g 0.63 :b 0.89 :a 0.85}}) ;; same blue, slightly dimmer + +(defn parse-md-inline-spans + "Parse inline markdown: **bold**, *emphasis*, ~~strike~~, `code`, [link](url). + Returns [{:text str :style :normal/:bold/:italic/:strike/:code/:link} ...]" + [line] + (let [len (count line)] + (loop [i 0 spans [] cur ""] + (if (>= i len) + (let [final (if (seq cur) (conj spans {:text cur :style :normal}) spans)] + (if (empty? final) [{:text "" :style :normal}] final)) + (let [ch (.charAt line i)] + (cond + ;; ***bold+italic*** -> treat as bold until font variants exist + (and (= ch \*) + (< (+ i 2) len) + (= (.charAt line (inc i)) \*) + (= (.charAt line (+ i 2)) \*)) + (let [end (str/index-of line "***" (+ i 3))] + (if (and end (> end (+ i 3))) + (recur (+ end 3) + (-> (if (seq cur) (conj spans {:text cur :style :normal}) spans) + (conj {:text (subs line (+ i 3) end) :style :bold})) + "") + (recur (+ i 3) spans (str cur "***")))) + ;; ~~strike~~ + (and (= ch \~) (< (inc i) len) (= (.charAt line (inc i)) \~)) + (let [end (str/index-of line "~~" (+ i 2))] + (if (and end (> end (+ i 2))) + (recur (+ end 2) + (-> (if (seq cur) (conj spans {:text cur :style :normal}) spans) + (conj {:text (subs line (+ i 2) end) :style :strike})) + "") + (recur (+ i 2) spans (str cur "~~")))) + ;; **bold** + (and (= ch \*) (< (inc i) len) (= (.charAt line (inc i)) \*)) + (let [end (str/index-of line "**" (+ i 2))] + (if (and end (> end (+ i 2))) + (recur (+ end 2) + (-> (if (seq cur) (conj spans {:text cur :style :normal}) spans) + (conj {:text (subs line (+ i 2) end) :style :bold})) + "") + (recur (+ i 2) spans (str cur "**")))) + ;; *emphasis* (single asterisk, not followed by another *) + (and (= ch \*) + (or (>= (inc i) len) (not= (.charAt line (inc i)) \*))) + (let [end (str/index-of line "*" (inc i))] + (if (and end (> end (inc i)) + ;; Ensure closing * is not part of ** + (or (>= (inc end) len) (not= (.charAt line (inc end)) \*))) + (recur (inc end) + (-> (if (seq cur) (conj spans {:text cur :style :normal}) spans) + (conj {:text (subs line (inc i) end) :style :italic})) + "") + (recur (inc i) spans (str cur "*")))) + ;; `code` + (= ch \`) + (let [end (str/index-of line "`" (inc i))] + (if (and end (> end (inc i))) + (recur (inc end) + (-> (if (seq cur) (conj spans {:text cur :style :normal}) spans) + (conj {:text (subs line (inc i) end) :style :code})) + "") + (recur (inc i) spans (str cur "`")))) + ;; [link](url) + (= ch \[) + (let [close-bracket (str/index-of line "](" i)] + (if close-bracket + (let [close-paren (str/index-of line ")" (+ close-bracket 2))] + (if close-paren + (recur (inc close-paren) + (-> (if (seq cur) (conj spans {:text cur :style :normal}) spans) + (conj {:text (subs line (inc i) close-bracket) :style :link})) + "") + (recur (inc i) spans (str cur "[")))) + (recur (inc i) spans (str cur "[")))) + ;; Normal character + :else + (recur (inc i) spans (str cur ch)))))))) + +(defn wrap-md-spans + "Word-wrap styled spans to fit max-chars per line. + Returns [[{:text str :style kw} ...] ...] — one vector of spans per visual line." + [spans max-chars] + (let [total-len (reduce + 0 (map (comp count :text) spans))] + (if (<= total-len max-chars) + [spans] + ;; Build flat [char style] vector, then greedy-wrap + (let [flat (vec (mapcat (fn [{:keys [text style]}] + (map #(vector % style) text)) + spans)) + n (count flat) + reconstitute (fn [chars] + (if (empty? chars) + [{:text "" :style :normal}] + (->> chars + (partition-by second) + (mapv (fn [g] {:text (apply str (map first g)) + :style (second (first g))})))))] + (loop [pos 0 lines []] + (if (>= pos n) + lines + (let [remaining (- n pos) + line-end (+ pos (min remaining max-chars))] + (if (<= remaining max-chars) + ;; Last line + (conj lines (reconstitute (subvec flat pos n))) + ;; Find last space in [pos, line-end) to break at word boundary + (let [break-at (loop [j (dec line-end)] + (cond + (<= j pos) -1 + (= (first (nth flat j)) \space) j + :else (recur (dec j))))] + (if (>= break-at 0) + (recur (inc break-at) + (conj lines (reconstitute (subvec flat pos break-at)))) + ;; No space found — hard break at max-chars + (recur line-end + (conj lines (reconstitute (subvec flat pos line-end)))))))))))))) + +(defn spans->text-ops + "Convert a single visual line of styled spans into positioned text-ops. + style-colors maps :normal/:bold/:code/:link to {:r :g :b :a}." + [spans x y font-size char-advance style-colors] + (loop [ss spans cx x ops []] + (if (empty? ss) + ops + (let [{:keys [text style]} (first ss) + c (get style-colors style (get style-colors :normal)) + op {:text text :type :comment + :from 0 :to (count text) + :x cx :y y + :size font-size + :r (:r c) :g (:g c) :b (:b c) :a (:a c)}] + (recur (rest ss) (+ cx (* (count text) char-advance)) (conj ops op)))))) + +(defn- decorative-line? + "True when line is a backtick-wrapped decorative border (contains ─ or ★)." + [trimmed] + (and (str/starts-with? trimmed "`") + (str/ends-with? trimmed "`") + (> (count trimmed) 2) + (re-find #"[\u2500\u2605]" trimmed))) + +(defn- decorative-inner + "Extract meaningful ASCII text from a decorative border line." + [trimmed] + (-> (subs trimmed 1 (dec (count trimmed))) + (str/replace #"[\u2500\u2605\u2014\u2022]" "") + str/trim)) + +(defn- md-table-separator-line? + "True for GFM separator rows like: + | --- | :---: | ---: | + --- | --- | ---" + [trimmed] + (let [parts (->> (str/split trimmed #"\|") + (map str/trim) + (filter seq) + (map #(str/replace % #"\s+" "")))] + (and (>= (count parts) 2) + (every? #(boolean (re-matches #":?-{2,}:?" %)) parts)))) + +(defn- md-table-header-line? + "Heuristic for a markdown table header row. + Accepts both leading-pipe and pipe-less GFM forms, but requires 2+ cells." + [trimmed] + (let [parts (->> (str/split trimmed #"\|") + (map str/trim) + (filter seq))] + (and (>= (count parts) 2) + (not (md-table-separator-line? trimmed))))) + +(defn- md-table-start? + "True when the current line and next line form a GFM table header + separator." + [line next-line] + (let [trimmed (some-> line str/trim) + next-trimmed (some-> next-line str/trim)] + (and (seq trimmed) + (seq next-trimmed) + (>= (count (re-seq #"\|" trimmed)) 1) + (md-table-header-line? trimmed) + (or (md-table-separator-line? next-trimmed) + (and (re-find #"\|" next-trimmed) + (re-find #"-{2,}" next-trimmed)))))) + +(defn- md-table-data-line? + "True for subsequent table rows after a header/separator pair." + [trimmed] + (let [parts (->> (str/split trimmed #"\|") + (map str/trim) + (filter seq))] + (and (>= (count parts) 2) + (not (md-table-separator-line? trimmed))))) + +(defn parse-md-blocks + "Parse markdown text into block-level elements. + States: :normal, :in-code, :in-callout. + Returns [{:type :header/:paragraph/:code-block/:list/:task-list/:blockquote/:callout ...}]" + [text] + (let [src-lines (str/split-lines text)] + (loop [ls src-lines state :normal blocks [] cur-para [] code-lang nil callout-label nil] + (if (empty? ls) + ;; Flush remaining + (cond + (= state :in-code) + (conj blocks {:type :code-block :lang code-lang :lines cur-para}) + (= state :in-callout) + (let [body-text (str/join "\n" cur-para) + body-blocks (when (seq body-text) (parse-md-blocks body-text))] + (conj blocks {:type :callout :label callout-label :body (or body-blocks [])})) + (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + :else blocks) + (let [line (first ls) + trimmed (str/trim line)] + (case state + :in-code + (if (str/starts-with? trimmed "```") + (recur (rest ls) :normal + (conj blocks {:type :code-block :lang code-lang :lines cur-para}) + [] nil nil) + (recur (rest ls) :in-code blocks (conj cur-para line) code-lang nil)) + + :in-callout + (if (and (decorative-line? trimmed) (empty? (decorative-inner trimmed))) + ;; Closing border — emit callout block with recursively-parsed body + (let [body-text (str/join "\n" cur-para) + body-blocks (when (seq body-text) (parse-md-blocks body-text))] + (recur (rest ls) :normal + (conj blocks {:type :callout :label callout-label :body (or body-blocks [])}) + [] nil nil)) + ;; Content inside callout — collect lines + (recur (rest ls) :in-callout blocks (conj cur-para line) nil callout-label)) + + ;; :normal state + (cond + ;; Code fence opening + (str/starts-with? trimmed "```") + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks) + lang (let [r (str/trim (subs trimmed 3))] (when (seq r) r))] + (recur (rest ls) :in-code blocks [] lang nil)) + ;; Decorative border line: backtick-wrapped ★/─ chars (Insight blocks) + (decorative-line? trimmed) + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks) + inner (decorative-inner trimmed)] + (if (seq inner) + ;; Has meaningful text (e.g., "Insight") — enter callout mode + (recur (rest ls) :in-callout blocks [] nil inner) + ;; Just decorative — horizontal rule + (recur (rest ls) :normal (conj blocks {:type :hr}) [] nil nil))) + ;; GFM table: header row + separator row, with or without outer pipes + (md-table-start? line (second ls)) + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks) + [remaining table-lines] + (loop [rem (drop 2 ls) tl [trimmed (str/trim (second ls))]] + (let [l (first rem) + t (when l (str/trim l))] + (if (and t (md-table-data-line? t)) + (recur (rest rem) (conj tl t)) + [rem tl])))] + (recur remaining :normal + (conj blocks {:type :table :lines table-lines}) [] nil nil)) + ;; Blockquote + (re-find #"^>\s?" trimmed) + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks) + [remaining quote-lines] + (loop [rem ls acc []] + (let [l (first rem) + t (when l (str/trim l))] + (if (and t (re-find #"^>\s?" t)) + (recur (rest rem) + (conj acc (str/replace-first t #"^>\s?" ""))) + [rem acc])))] + (recur remaining :normal + (conj blocks {:type :blockquote :lines quote-lines}) [] nil nil)) + ;; Header + (re-find #"^#{1,6}\s+" trimmed) + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks) + level (count (re-find #"^#+" trimmed)) + content (str/trim (subs trimmed (inc level)))] + (recur (rest ls) :normal + (conj blocks {:type :header :level level :content content}) + [] nil nil)) + ;; Horizontal rule: --- or *** or ___ or repeated ─ + (or (re-find #"^[-*_]{3,}\s*$" trimmed) + (re-find #"^\u2500{3,}" trimmed)) + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks)] + (recur (rest ls) :normal (conj blocks {:type :hr}) [] nil nil)) + ;; Task list item + (re-find #"^[-*]\s+\[( |x|X)\]\s+" trimmed) + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks) + [remaining items] + (loop [rem ls items []] + (let [l (first rem) + t (when l (str/trim l))] + (if (and t (re-find #"^[-*]\s+\[( |x|X)\]\s+" t)) + (let [[_ mark] (re-find #"^[-*]\s+\[( |x|X)\]" t) + content (str/trim (str/replace-first t #"^[-*]\s+\[( |x|X)\]\s+" ""))] + (recur (rest rem) + (conj items {:checked? (not= mark " ") + :content content}))) + [rem items])))] + (recur remaining :normal (conj blocks {:type :task-list :items items}) [] nil nil)) + ;; Bullet list item + (re-find #"^[-*]\s+" trimmed) + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks) + [remaining items] + (loop [rem ls items []] + (let [l (first rem) + t (when l (str/trim l))] + (if (and t (re-find #"^[-*]\s+" t)) + (recur (rest rem) (conj items {:content (str/trim (subs t 2))})) + [rem items])))] + (recur remaining :normal (conj blocks {:type :list :items items}) [] nil nil)) + ;; Numbered list item (1. 2. 3. etc.) + (re-find #"^\d+\.\s+" trimmed) + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks) + [remaining items] + (loop [rem ls items []] + (let [l (first rem) + t (when l (str/trim l))] + (if (and t (re-find #"^\d+\.\s+" t)) + (let [after-num (str/replace-first t #"^\d+\.\s+" "")] + (recur (rest rem) (conj items {:content after-num}))) + [rem items])))] + (recur remaining :normal (conj blocks {:type :numbered-list :items items}) [] nil nil)) + ;; Blank line — paragraph break + (empty? trimmed) + (let [blocks (if (seq cur-para) + (conj blocks {:type :paragraph :content (str/join " " cur-para)}) + blocks)] + (recur (rest ls) :normal blocks [] nil nil)) + ;; Regular text — accumulate into paragraph + :else + (recur (rest ls) :normal blocks (conj cur-para trimmed) nil nil)))))))) + +(defn trail-node-color + "Color for a trail node by kind. Returns {:r :g :b :a}." + [kind tool-name] + (case kind + :reasoning {:r 0.72 :g 0.71 :b 0.71 :a 1.0} + :thinking {:r 0.65 :g 0.65 :b 0.75 :a 0.6} ;; dimmed — internal reasoning + :tool-call (case tool-name + ("Read" "read") {:r 0.4 :g 0.85 :b 0.95 :a 1.0} ;; cyan + ("Edit" "edit") {:r 0.95 :g 0.85 :b 0.35 :a 1.0} ;; yellow + ("Write" "write") {:r 0.95 :g 0.85 :b 0.35 :a 1.0} ;; yellow + ("Grep" "grep") {:r 0.55 :g 0.9 :b 0.55 :a 1.0} ;; green + ("Glob" "glob") {:r 0.55 :g 0.9 :b 0.55 :a 1.0} ;; green + ("Bash" "bash") {:r 0.9 :g 0.65 :b 0.4 :a 1.0} ;; orange + ("Task" "task") {:r 0.75 :g 0.6 :b 0.95 :a 1.0} ;; purple + {:r 0.7 :g 0.7 :b 0.85 :a 1.0}) ;; default blue-gray + :tool-call-start {:r 0.6 :g 0.6 :b 0.7 :a 0.7} + :tool-result {:r 0.55 :g 0.55 :b 0.6 :a 0.7} ;; dim + {:r 0.75 :g 0.75 :b 0.75 :a 1.0})) + +(defn- tool-input-summary + "One-line summary of tool input for card header." + [tool-name input] + (cond + (and (string? (:file_path input)) (seq (:file_path input))) + (:file_path input) + (and (string? (:pattern input)) (seq (:pattern input))) + (str "\"" (:pattern input) "\"") + (and (string? (:command input)) (seq (:command input))) + (let [cmd (:command input)] + (if (> (count cmd) 60) (str (subs cmd 0 57) "...") cmd)) + :else "")) + +(defn- file-op-tool? + "True for tools that are read-only file operations (groupable)." + [tool-name] + (contains? #{"Read" "read" "Grep" "grep" "Glob" "glob"} tool-name)) + +(defn- group-trail-blocks + "Group consecutive trail nodes into logical blocks. + Merges consecutive :reasoning and :thinking nodes. + Groups :tool-call-start + :tool-call + :tool-result by tool-id. + Returns [{:block-type :nodes [...]}]." + [trail] + (reduce + (fn [acc node] + (let [kind (:kind node) + last-block (peek acc)] + (case kind + :reasoning + (if (and last-block (= :reasoning (:block-type last-block))) + (conj (pop acc) (update last-block :nodes conj node)) + (conj acc {:block-type :reasoning :nodes [node]})) + + :thinking + (if (and last-block (= :thinking (:block-type last-block))) + (conj (pop acc) (update last-block :nodes conj node)) + (conj acc {:block-type :thinking :nodes [node]})) + + :tool-call-start + (conj acc {:block-type :tool-card + :tool-id (:tool-id node) + :tool-name (:tool-name node) + :status :pending + :nodes [node]}) + + :tool-call + ;; Find matching tool-card block by tool-id, update it + (let [tid (:tool-id node) + idx (some (fn [i] + (when (and (= :tool-card (:block-type (nth acc i))) + (= tid (:tool-id (nth acc i)))) + i)) + (range (dec (count acc)) -1 -1))] + (if idx + (update (vec acc) idx + (fn [b] (-> b (assoc :status :complete :input (:input node)) + (update :nodes conj node)))) + ;; Orphan tool-call — make standalone card + (conj acc {:block-type :tool-card + :tool-id tid + :tool-name (:tool-name node) + :status :complete + :input (:input node) + :nodes [node]}))) + + :tool-result + (let [tid (:tool-id node) + idx (some (fn [i] + (when (and (= :tool-card (:block-type (nth acc i))) + (= tid (:tool-id (nth acc i)))) + i)) + (range (dec (count acc)) -1 -1))] + (if idx + (update (vec acc) idx + (fn [b] (-> b (assoc :result-content (:content node)) + (update :nodes conj node)))) + ;; Orphan result + (conj acc {:block-type :tool-card + :tool-id tid + :tool-name "?" + :status :complete + :result-content (:content node) + :nodes [node]}))) + + ;; Unknown kind — treat as reasoning + (conj acc {:block-type :reasoning :nodes [node]})))) + [] + trail)) + +(defn- group-consecutive-file-ops + "Collapse runs of 3+ consecutive file-op tool cards into group cards." + [blocks] + (loop [bs blocks result []] + (if (empty? bs) + result + (let [b (first bs)] + (if (and (= :tool-card (:block-type b)) + (file-op-tool? (:tool-name b))) + ;; Count consecutive file-op cards + (let [run (take-while #(and (= :tool-card (:block-type %)) + (file-op-tool? (:tool-name %))) + bs) + n (count run)] + (if (>= n 3) + (recur (drop n bs) + (conj result {:block-type :tool-group + :cards (vec run) + :count n})) + (recur (rest bs) (conj result b)))) + (recur (rest bs) (conj result b))))))) + +(defn trail->chat-nodes + "Convert trail to rt-node children for the chat pane. + Each logical block becomes an rt-node with typed visual treatment. + Returns [rt-node ...] — children for the chat body container." + [trail pane-w font-size char-advance shimmer-alpha collapsed] + (let [colors (:colors dt) + fg (:fg colors) + fg-dim (:fg-muted colors) + border (:border colors) + pad 12 + max-chars (max 20 (int (/ (- pane-w (* 2 pad)) char-advance))) + line-h (+ font-size 4) + card-h-header 28 + blocks (-> trail group-trail-blocks group-consecutive-file-ops)] + (vec + (map-indexed + (fn [bi block] + (case (:block-type block) + ;; --- Reasoning: markdown-formatted text --- + :reasoning + (let [merged-text (apply str (map :text (:nodes block))) + md-blocks (parse-md-blocks merged-text) + inner-w (- pane-w (* 2 pad)) + inner-max-chars (max 20 (int (/ inner-w char-advance))) + code-bg (get-in dt [:colors :bg-muted]) + code-pad 8 + code-max-chars (max 20 (int (/ (- inner-w (* 2 code-pad)) char-advance))) + block-gap 8 + ;; Build child nodes for each markdown block + children + (vec + (map-indexed + (fn [mi mb] + (case (:type mb) + :header + (let [level (or (:level mb) 2) + hdr-size (if (<= level 2) (:size typo-title) (:size typo-subtitle)) + hdr-color {:r 0.90 :g 0.89 :b 0.89 :a 1.0} + hdr-max (max 20 (int (/ inner-w (* hdr-size 0.56)))) + text (:content mb) + wrapped (wrap-line text hdr-max) + hdr-line-h (+ hdr-size 5) + text-h (* (count wrapped) hdr-line-h) + ;; Top margin + text + bottom accent + gap + top-margin (if (<= level 2) 10 6) + bottom-pad 6 + h (+ top-margin text-h bottom-pad) + text-ops (vec (map-indexed + (fn [i ln] + {:text ln :type :keyword + :from 0 :to (count ln) + :x pad :y (+ top-margin hdr-size (* i hdr-line-h)) + :size hdr-size + :r (:r hdr-color) :g (:g hdr-color) + :b (:b hdr-color) :a (:a hdr-color)}) + wrapped)) + ;; Subtle bottom border for h1/h2 + accent-line (when (<= level 2) + (rt-node (keyword (str "md-hdr-line-" bi "-" mi)) :hdr-accent + {:x pad :y (- h 2) :w (min (* (count (first wrapped)) (* hdr-size 0.56)) inner-w) :h 1} + :style {:bg (:border-subtle colors)}))] + (rt-node (keyword (str "md-hdr-" bi "-" mi)) :md-header + {:x 0 :y 0 :w pane-w :h h} + :text text-ops + :children (if accent-line [accent-line] []))) + + :paragraph + (let [spans (parse-md-inline-spans (:content mb)) + wrapped-lines (wrap-md-spans spans inner-max-chars) + all-ops (vec (apply concat + (map-indexed + (fn [li line-spans] + (spans->text-ops line-spans pad + (+ font-size (* li line-h)) + font-size char-advance md-style-colors)) + wrapped-lines))) + h (+ 4 (* (count wrapped-lines) line-h))] + (rt-node (keyword (str "md-para-" bi "-" mi)) :md-paragraph + {:x 0 :y 0 :w pane-w :h h} + :text all-ops)) + + :blockquote + (let [quote-lines (or (:lines mb) []) + quote-max (max 16 (int (/ (- inner-w 24) char-advance))) + quote-color (assoc md-style-colors :normal {:r 0.78 :g 0.79 :b 0.82 :a 0.95}) + line-data (mapv (fn [line] + (wrap-md-spans (parse-md-inline-spans line) quote-max)) + quote-lines) + all-ops (loop [remaining line-data li 0 ops []] + (if (empty? remaining) + ops + (let [wrapped (first remaining) + quote-ops (vec (apply concat + (map-indexed + (fn [wi line-spans] + (spans->text-ops line-spans (+ pad 12) + (+ font-size (* (+ li wi) line-h)) + font-size char-advance quote-color)) + wrapped)))] + (recur (rest remaining) + (+ li (max 1 (count wrapped))) + (into ops quote-ops))))) + total-lines (max 1 (reduce + 0 (map #(max 1 (count %)) line-data))) + h (+ 8 (* total-lines line-h))] + (rt-node (keyword (str "md-quote-" bi "-" mi)) :md-blockquote + {:x 0 :y 0 :w pane-w :h h} + :children [(rt-node (keyword (str "md-quote-bar-" bi "-" mi)) :quote-bar + {:x pad :y 2 :w 3 :h (- h 4)} + :style {:bg (:accent colors) :radius 2})] + :text all-ops)) + + :code-block + (let [code-color {:r 0.00 :g 0.63 :b 0.32 :a 1.0} + code-lines (:lines mb) + wrapped-lines (vec (mapcat #(wrap-line % code-max-chars) code-lines)) + text-ops (vec (map-indexed + (fn [i ln] + {:text ln :type :comment + :from 0 :to (count ln) + :x (+ pad code-pad) :y (+ code-pad font-size (* i line-h)) + :size font-size + :r (:r code-color) :g (:g code-color) + :b (:b code-color) :a (:a code-color)}) + wrapped-lines)) + body-h (+ (* 2 code-pad) (* (count wrapped-lines) line-h)) + total-h (+ body-h 4)] + (rt-node (keyword (str "md-code-" bi "-" mi)) :md-code-block + {:x 0 :y 0 :w pane-w :h total-h} + :children + [(rt-node (keyword (str "md-code-bg-" bi "-" mi)) :code-bg + {:x pad :y 0 :w inner-w :h body-h} + :style {:bg code-bg :radius 4}) + (rt-node (keyword (str "md-code-text-" bi "-" mi)) :code-text + {:x 0 :y 0 :w pane-w :h body-h} + :text text-ops)])) + + :list + (let [items (:items mb) + bullet-indent 2 + item-max-chars (max 10 (- inner-max-chars bullet-indent)) + item-data (mapv (fn [item] + (let [spans (parse-md-inline-spans (:content item)) + wrapped (wrap-md-spans spans item-max-chars)] + {:wrapped wrapped})) + items) + all-ops (loop [items-rem item-data li 0 ops []] + (if (empty? items-rem) + ops + (let [{:keys [wrapped]} (first items-rem) + item-ops + (vec (apply concat + (map-indexed + (fn [wi line-spans] + (let [bullet-ops (when (= wi 0) + [{:text "- " :type :comment + :from 0 :to 2 + :x pad :y (+ font-size (* (+ li wi) line-h)) + :size font-size + :r 0.55 :g 0.55 :b 0.6 :a 0.8}]) + span-ops (spans->text-ops + line-spans + (+ pad (* bullet-indent char-advance)) + (+ font-size (* (+ li wi) line-h)) + font-size char-advance md-style-colors)] + (into (vec (or bullet-ops [])) span-ops))) + wrapped)))] + (recur (rest items-rem) (+ li (count wrapped)) (into ops item-ops))))) + total-lines (reduce + 0 (map (comp count :wrapped) item-data)) + h (+ 4 (* total-lines line-h))] + (rt-node (keyword (str "md-list-" bi "-" mi)) :md-list + {:x 0 :y 0 :w pane-w :h h} + :text all-ops)) + + :task-list + (let [items (:items mb) + item-data (mapv (fn [item] + (let [prefix (if (:checked? item) "[x] " "[ ] ") + prefix-w (count prefix) + item-max (max 10 (- inner-max-chars prefix-w)) + spans (parse-md-inline-spans (:content item)) + wrapped (wrap-md-spans spans item-max)] + {:wrapped wrapped + :prefix prefix + :prefix-w prefix-w + :checked? (:checked? item)})) + items) + all-ops (loop [items-rem item-data li 0 ops []] + (if (empty? items-rem) + ops + (let [{:keys [wrapped prefix prefix-w checked?]} (first items-rem) + check-color (if checked? + {:r 0.42 :g 0.86 :b 0.58 :a 1.0} + {:r 0.52 :g 0.52 :b 0.58 :a 0.9}) + item-ops + (vec (apply concat + (map-indexed + (fn [wi line-spans] + (let [prefix-ops (when (= wi 0) + [{:text prefix :type :comment + :from 0 :to (count prefix) + :x pad :y (+ font-size (* (+ li wi) line-h)) + :size font-size + :r (:r check-color) :g (:g check-color) + :b (:b check-color) :a (:a check-color)}]) + span-ops (spans->text-ops + line-spans + (+ pad (* prefix-w char-advance)) + (+ font-size (* (+ li wi) line-h)) + font-size char-advance md-style-colors)] + (into (vec (or prefix-ops [])) span-ops))) + wrapped)))] + (recur (rest items-rem) (+ li (count wrapped)) (into ops item-ops))))) + total-lines (reduce + 0 (map (comp count :wrapped) item-data)) + h (+ 4 (* total-lines line-h))] + (rt-node (keyword (str "md-task-list-" bi "-" mi)) :md-task-list + {:x 0 :y 0 :w pane-w :h h} + :text all-ops)) + + :hr + (let [rule-h 8 + border-c (:border-subtle colors)] + (rt-node (keyword (str "md-hr-" bi "-" mi)) :md-hr + {:x 0 :y 0 :w pane-w :h rule-h} + :children + [(rt-node (keyword (str "md-hr-line-" bi "-" mi)) :hr-line + {:x pad :y 3 :w inner-w :h 1} + :style {:bg border-c})])) + + :numbered-list + (let [items (:items mb) + item-data (mapv (fn [idx item] + (let [prefix (str (inc idx) ". ") + prefix-w (count prefix) + item-max (max 10 (- inner-max-chars prefix-w)) + spans (parse-md-inline-spans (:content item)) + wrapped (wrap-md-spans spans item-max)] + {:wrapped wrapped :prefix prefix :prefix-w prefix-w})) + (range) items) + all-ops (loop [items-rem item-data li 0 ops []] + (if (empty? items-rem) + ops + (let [{:keys [wrapped prefix prefix-w]} (first items-rem) + item-ops + (vec (apply concat + (map-indexed + (fn [wi line-spans] + (let [num-ops (when (= wi 0) + [{:text prefix :type :comment + :from 0 :to (count prefix) + :x pad :y (+ font-size (* (+ li wi) line-h)) + :size font-size + :r 0.55 :g 0.55 :b 0.6 :a 0.8}]) + span-ops (spans->text-ops + line-spans + (+ pad (* prefix-w char-advance)) + (+ font-size (* (+ li wi) line-h)) + font-size char-advance md-style-colors)] + (into (vec (or num-ops [])) span-ops))) + wrapped)))] + (recur (rest items-rem) (+ li (count wrapped)) (into ops item-ops))))) + total-lines (reduce + 0 (map (comp count :wrapped) item-data)) + h (+ 4 (* total-lines line-h))] + (rt-node (keyword (str "md-nlist-" bi "-" mi)) :md-numbered-list + {:x 0 :y 0 :w pane-w :h h} + :text all-ops)) + + :callout + (let [label (:label mb) + body-blocks (:body mb) + accent-c (:accent colors) + label-c {:r 0.70 :g 0.80 :b 1.0 :a 1.0} + callout-pad (+ pad 10) + callout-w (- pane-w callout-pad pad) + callout-max (max 20 (int (/ callout-w char-advance))) + ;; Header node + hdr-h (+ font-size 6) + hdr-node (rt-node (keyword (str "md-co-hdr-" bi "-" mi)) :callout-hdr + {:x 0 :y 0 :w pane-w :h hdr-h} + :text [{:text label :type :keyword + :from 0 :to (count label) + :x callout-pad :y (+ font-size 2) + :size font-size + :r (:r label-c) :g (:g label-c) + :b (:b label-c) :a (:a label-c)}]) + ;; Body content nodes — reuse the same rendering logic + body-children + (vec (map-indexed + (fn [ci cb] + (case (:type cb) + :paragraph + (let [spans (parse-md-inline-spans (:content cb)) + wrapped-lines (wrap-md-spans spans callout-max) + ops (vec (apply concat + (map-indexed + (fn [li ls] + (spans->text-ops ls callout-pad + (+ font-size (* li line-h)) + font-size char-advance md-style-colors)) + wrapped-lines))) + h (+ 4 (* (count wrapped-lines) line-h))] + (rt-node (keyword (str "md-co-p-" bi "-" mi "-" ci)) :callout-para + {:x 0 :y 0 :w pane-w :h h} + :text ops)) + :list + (let [items (:items cb) + bullet-indent 2 + item-mc (max 10 (- callout-max bullet-indent)) + item-d (mapv (fn [item] + {:wrapped (wrap-md-spans (parse-md-inline-spans (:content item)) item-mc)}) + items) + ops (loop [ir item-d li 0 o []] + (if (empty? ir) o + (let [{:keys [wrapped]} (first ir) + io (vec (apply concat + (map-indexed + (fn [wi ls] + (let [bp (when (= wi 0) + [{:text "- " :type :comment :from 0 :to 2 + :x callout-pad :y (+ font-size (* (+ li wi) line-h)) + :size font-size :r 0.55 :g 0.55 :b 0.6 :a 0.8}]) + sp (spans->text-ops ls (+ callout-pad (* bullet-indent char-advance)) + (+ font-size (* (+ li wi) line-h)) + font-size char-advance md-style-colors)] + (into (vec (or bp [])) sp))) + wrapped)))] + (recur (rest ir) (+ li (count wrapped)) (into o io))))) + tl (reduce + 0 (map (comp count :wrapped) item-d)) + h (+ 4 (* tl line-h))] + (rt-node (keyword (str "md-co-l-" bi "-" mi "-" ci)) :callout-list + {:x 0 :y 0 :w pane-w :h h} + :text ops)) + :numbered-list + (let [items (:items cb) + item-d (mapv (fn [idx item] + (let [pfx (str (inc idx) ". ") + pw (count pfx)] + {:wrapped (wrap-md-spans (parse-md-inline-spans (:content item)) + (max 10 (- callout-max pw))) + :prefix pfx :prefix-w pw})) + (range) items) + ops (loop [ir item-d li 0 o []] + (if (empty? ir) o + (let [{:keys [wrapped prefix prefix-w]} (first ir) + io (vec (apply concat + (map-indexed + (fn [wi ls] + (let [np (when (= wi 0) + [{:text prefix :type :comment :from 0 :to (count prefix) + :x callout-pad :y (+ font-size (* (+ li wi) line-h)) + :size font-size :r 0.55 :g 0.55 :b 0.6 :a 0.8}]) + sp (spans->text-ops ls (+ callout-pad (* prefix-w char-advance)) + (+ font-size (* (+ li wi) line-h)) + font-size char-advance md-style-colors)] + (into (vec (or np [])) sp))) + wrapped)))] + (recur (rest ir) (+ li (count wrapped)) (into o io))))) + tl (reduce + 0 (map (comp count :wrapped) item-d)) + h (+ 4 (* tl line-h))] + (rt-node (keyword (str "md-co-n-" bi "-" mi "-" ci)) :callout-nlist + {:x 0 :y 0 :w pane-w :h h} + :text ops)) + ;; Other block types inside callout — render as paragraph + (let [content (or (:content cb) "") + spans (parse-md-inline-spans content) + wrapped-lines (wrap-md-spans spans callout-max) + ops (vec (apply concat + (map-indexed + (fn [li ls] + (spans->text-ops ls callout-pad + (+ font-size (* li line-h)) + font-size char-advance md-style-colors)) + wrapped-lines))) + h (+ 4 (* (count wrapped-lines) line-h))] + (rt-node (keyword (str "md-co-x-" bi "-" mi "-" ci)) :callout-misc + {:x 0 :y 0 :w pane-w :h h} + :text ops)))) + body-blocks)) + all-children (into [hdr-node] body-children) + body-gap 4 + content-h (+ (reduce + 0 (map #(get-in % [:bounds :h] 0) all-children)) + (* body-gap (max 0 (dec (count all-children))))) + total-h (+ content-h 4)] + (rt-node (keyword (str "md-callout-" bi "-" mi)) :md-callout + {:x 0 :y 0 :w pane-w :h total-h} + :layout {:direction :column :gap body-gap :padding [0 0 0 0]} + :children + (into [(rt-node (keyword (str "md-co-bar-" bi "-" mi)) :accent-bar + {:x pad :y 2 :w 3 :h (- total-h 4)} + :data {:layout-skip? true} + :style {:bg accent-c :radius 2})] + all-children))) + + :table + (let [table-lines (:lines mb) + ;; Filter separator rows (|---|---|) + is-separator? #(md-table-separator-line? (str/trim %)) + content-rows (filterv (complement is-separator?) table-lines) + ;; Parse each row: split by |, trim cells + parse-row (fn [row-str] + (->> (str/split row-str #"\|") + (map str/trim) + (filterv #(seq %)))) + raw-rows (mapv parse-row content-rows) + col-count (max 1 (reduce max 1 (map count raw-rows))) + rows (mapv (fn [row] + (vec (take col-count (concat row (repeat ""))))) + raw-rows) + header-row (first rows) + data-rows (vec (rest rows)) + table-pad 0 + table-inner-w inner-w + cell-pad-x 10 + cell-pad-y 8 + font-scale (if (pos? font-size) (/ char-advance font-size) 0.56) + header-font (max (+ font-size 1) (:size typo-body)) + header-char-advance (* header-font font-scale) + header-line-h (+ header-font 6) + col-w (/ table-inner-w col-count) + cell-max-chars (max 6 (int (/ (max 1 (- col-w (* 2 cell-pad-x))) char-advance))) + header-max-chars (max 6 (int (/ (max 1 (- col-w (* 2 cell-pad-x))) header-char-advance))) + rows-data + (mapv (fn [ri cells] + (let [is-header? (zero? ri) + wrapped-cells (mapv (fn [cell] + (let [spans (if is-header? + [{:text cell :style :bold}] + (parse-md-inline-spans cell))] + (wrap-md-spans spans (if is-header? header-max-chars cell-max-chars)))) + cells) + row-line-count (max 1 (reduce max 1 (map count wrapped-cells))) + row-font (if is-header? header-font font-size) + row-line-h (if is-header? header-line-h line-h) + row-char-advance (if is-header? header-char-advance char-advance)] + {:cells cells + :wrapped-cells wrapped-cells + :is-header? is-header? + :line-count row-line-count + :row-font row-font + :row-line-h row-line-h + :row-char-advance row-char-advance + :row-h (+ (* 2 cell-pad-y) (* row-line-count row-line-h))})) + (range) rows) + row-positions + (loop [remaining rows-data y 0 out []] + (if (empty? remaining) + out + (let [row (first remaining)] + (recur (rest remaining) + (+ y (:row-h row)) + (conj out (assoc row :y y)))))) + total-h (+ 4 (reduce + 0 (map :row-h rows-data))) + all-ops + (vec (mapcat + (fn [{:keys [wrapped-cells is-header? y row-font row-line-h row-char-advance]}] + (mapcat + (fn [ci wrapped] + (mapcat + (fn [wi line-spans] + (spans->text-ops line-spans + (+ pad (* ci col-w) cell-pad-x) + (+ 4 y cell-pad-y row-font (* wi row-line-h)) + row-font row-char-advance md-style-colors)) + (range) wrapped)) + (range) wrapped-cells)) + row-positions)) + table-lines-nodes + (vec + (concat + (mapcat + (fn [{:keys [y row-h is-header?]}] + (let [line-y (+ 4 y row-h -1)] + (cond-> [] + is-header? + (conj (rt-node (keyword (str "md-table-header-bg-" bi "-" mi "-" y)) :table-header-bg + {:x pad :y (+ 4 y) :w table-inner-w :h row-h} + :style {:bg (:bg-muted colors) + :radius 4})) + true + (conj (rt-node (keyword (str "md-table-row-line-" bi "-" mi "-" y)) :table-row-line + {:x pad :y line-y :w table-inner-w :h 1} + :style {:bg (:border-subtle colors)}))))) + row-positions) + (mapcat + (fn [ci] + (let [x (+ pad (* ci col-w))] + [(rt-node (keyword (str "md-table-col-line-" bi "-" mi "-" ci)) :table-col-line + {:x x :y 4 :w 1 :h (- total-h 5)} + :style {:bg (:border-subtle colors)})])) + (range 1 col-count))))] + (rt-node (keyword (str "md-table-" bi "-" mi)) :md-table + {:x 0 :y 0 :w pane-w :h total-h} + :children + [(rt-node (keyword (str "md-table-bg-" bi "-" mi)) :table-bg + {:x pad :y 4 :w inner-w :h (- total-h 4)} + :style {:bg (get-in dt [:colors :bg-elevated]) :radius 4 + :border-width 1 + :border-color (:border-subtle colors)}) + (rt-node (keyword (str "md-table-lines-" bi "-" mi)) :table-lines + {:x 0 :y 0 :w pane-w :h total-h} + :children table-lines-nodes) + (rt-node (keyword (str "md-table-text-" bi "-" mi)) :table-text + {:x 0 :y 0 :w pane-w :h total-h} + :text all-ops)])) + + ;; Fallback for unknown block types + (rt-node (keyword (str "md-unk-" bi "-" mi)) :md-unknown + {:x 0 :y 0 :w pane-w :h 0}))) + md-blocks)) + ;; Compute total height including gaps between blocks + total-h (+ (reduce + 0 (map #(get-in % [:bounds :h] 0) children)) + (* block-gap (max 0 (dec (count children)))))] + (rt-node (keyword (str "reasoning-" bi)) :reasoning-block + {:x 0 :y 0 :w pane-w :h total-h} + :layout {:direction :column :gap block-gap} + :children children)) + + ;; --- Thinking: dimmed block with left accent bar --- + :thinking + (let [merged-text (apply str (map :text (:nodes block))) + tid (str "thinking-" bi) + collapsed? (contains? collapsed (keyword tid)) + header-text "Thinking..." + c (trail-node-color :thinking nil) + accent-color [0.45 0.55 0.75 0.5] + header-op {:text header-text :type :comment + :from 0 :to (count header-text) + :x (+ pad 8) :y (+ font-size 0) + :size font-size + :r (:r c) :g (:g c) :b (:b c) :a (:a c)} + arrow-text (if collapsed? ">" "v") + arrow-op {:text arrow-text :type :comment + :from 0 :to 1 + :x (- pane-w pad 10) :y (+ font-size 0) + :size font-size + :r (:r c) :g (:g c) :b (:b c) :a 0.5}] + (if collapsed? + ;; Collapsed thinking — just header + (rt-node (keyword tid) :thinking-block + {:x 0 :y 0 :w pane-w :h (+ card-h-header 4)} + :data {:collapse-id (keyword tid)} + :children + [(rt-node (keyword (str tid "-accent")) :accent-bar + {:x 2 :y 2 :w 3 :h (- card-h-header 0)} + :style {:bg accent-color :radius 2}) + (rt-node (keyword (str tid "-hdr")) :tool-header + {:x 0 :y 0 :w pane-w :h card-h-header} + :data {:collapse-id (keyword tid)} + :text [header-op arrow-op])]) + ;; Expanded thinking — header + body + (let [lines (mapcat #(wrap-line % (- max-chars 2)) + (str/split-lines merged-text)) + body-ops (vec (map-indexed + (fn [i line] + {:text line :type :comment + :from 0 :to (count line) + :x (+ pad 8) :y (+ font-size (* i line-h)) + :size font-size + :r (:r c) :g (:g c) :b (:b c) :a (* (:a c) 0.8)}) + lines)) + body-h (+ 4 (* (count lines) line-h)) + total-h (+ card-h-header body-h 4)] + (rt-node (keyword tid) :thinking-block + {:x 0 :y 0 :w pane-w :h total-h} + :data {:collapse-id (keyword tid)} + :children + [(rt-node (keyword (str tid "-accent")) :accent-bar + {:x 2 :y 2 :w 3 :h (- total-h 4)} + :style {:bg accent-color :radius 2}) + (rt-node (keyword (str tid "-hdr")) :tool-header + {:x 0 :y 0 :w pane-w :h card-h-header} + :data {:collapse-id (keyword tid)} + :text [header-op arrow-op]) + (rt-node (keyword (str tid "-body")) :thinking-body + {:x 0 :y card-h-header :w pane-w :h body-h} + :text body-ops)])))) + + ;; --- Tool card: status dot + header + collapsible result --- + :tool-card + (let [tool-name (:tool-name block) + tid (or (:tool-id block) (str "tool-" bi)) + status (:status block) + pending? (= :pending status) + input (:input block) + ;; Navigation target: extract file path + line from tool input + nav-target (when input + (let [fp (or (:file_path input) (:path input))] + (when (and (string? fp) (seq fp)) + {:file-path fp + :line (or (:offset input) (:line input) 0)}))) + summary (if input (tool-input-summary tool-name input) + (some-> (:nodes block) first :tool-name (str "..."))) + header-label (str tool-name (when (seq summary) (str " " summary))) + header-label (if (> (count header-label) (- max-chars 6)) + (str (subs header-label 0 (- max-chars 9)) "...") + header-label) + collapsed? (contains? collapsed (keyword tid)) + c (trail-node-color :tool-call tool-name) + ;; Status dot: yellow = pending, green = complete + dot-color (if pending? + [0.95 0.85 0.25 (if pending? shimmer-alpha 1.0)] + [0.4 0.85 0.45 1.0]) + ;; Header text alpha pulses with shimmer when pending + text-alpha (if pending? shimmer-alpha (:a c)) + header-text-op {:text header-label :type :keyword + :from 0 :to (count header-label) + :x (+ pad 14) :y (+ font-size 2) + :size font-size + :r (:r c) :g (:g c) :b (:b c) :a text-alpha} + arrow-text (if collapsed? ">" "v") + arrow-op {:text arrow-text :type :comment + :from 0 :to 1 + :x (- pane-w pad 10) :y (+ font-size 2) + :size font-size + :r (nth fg-dim 0) :g (nth fg-dim 1) :b (nth fg-dim 2) :a 0.5} + ;; Result body + result-content (:result-content block) + has-body? (and result-content (not collapsed?)) + body-lines (when has-body? + (let [raw (if (string? result-content) result-content (pr-str result-content)) + all-lines (mapcat #(wrap-line % (- max-chars 2)) + (str/split-lines raw)) + ;; Truncate to 2 lines max + limited (take 2 all-lines)] + (vec limited))) + body-h (if has-body? (+ 4 (* (count body-lines) line-h)) 0) + total-h (+ card-h-header body-h 6) + card-bg (:bg-subtle colors)] + (rt-node (keyword (str "tc-" bi)) :tool-card-node + {:x 0 :y 0 :w pane-w :h total-h} + :style {:bg card-bg :radius 4 + :border-width 1 + :border-color (if nav-target (:border colors) (:border-subtle colors))} + :data (when nav-target {:nav nav-target}) + :children + (cond-> [(rt-node (keyword (str "dot-" bi)) :status-dot + {:x (+ pad 2) :y 10 :w 6 :h 6} + :style {:bg dot-color :radius 3}) + (rt-node (keyword (str "tch-" bi)) :tool-header + {:x 0 :y 0 :w pane-w :h card-h-header} + :data {:collapse-id (keyword tid)} + :text [header-text-op arrow-op])] + has-body? + (conj (rt-node (keyword (str "tcb-" bi)) :tool-body + {:x 0 :y card-h-header :w pane-w :h body-h} + :text (vec (map-indexed + (fn [i line] + (let [rc (trail-node-color :tool-result nil)] + {:text (str " " line) :type :comment + :from 0 :to (+ 2 (count line)) + :x pad :y (+ font-size (* i line-h)) + :size font-size + :r (:r rc) :g (:g rc) :b (:b rc) :a (:a rc)})) + body-lines))))))) + + ;; --- Tool group: collapsed run of 3+ file ops --- + :tool-group + (let [n (:count block) + cards (:cards block) + group-id (str "tg-" bi) + collapsed? (contains? collapsed (keyword group-id)) + header-label (str n " file operations") + c {:r 0.55 :g 0.75 :b 0.65 :a 1.0} + arrow-text (if collapsed? ">" "v") + header-text-op {:text header-label :type :keyword + :from 0 :to (count header-label) + :x (+ pad 4) :y (+ font-size 2) + :size font-size + :r (:r c) :g (:g c) :b (:b c) :a (:a c)} + arrow-op {:text arrow-text :type :comment + :from 0 :to 1 + :x (- pane-w pad 10) :y (+ font-size 2) + :size font-size + :r (:r c) :g (:g c) :b (:b c) :a 0.5}] + (if collapsed? + (rt-node (keyword group-id) :tool-group-node + {:x 0 :y 0 :w pane-w :h (+ card-h-header 6)} + :style {:bg (:bg-subtle colors) :radius 4 + :border-width 1 :border-color (:border-subtle colors)} + :children + [(rt-node (keyword (str group-id "-hdr")) :tool-header + {:x 0 :y 0 :w pane-w :h card-h-header} + :data {:collapse-id (keyword group-id)} + :text [header-text-op arrow-op])]) + ;; Expanded: show each card as a summary line + (let [item-lines + (vec (map-indexed + (fn [i card] + (let [tn (:tool-name card) + inp (:input card) + summary (if inp (tool-input-summary tn inp) "") + label (str ">> " tn " " summary) + label (if (> (count label) max-chars) + (str (subs label 0 (- max-chars 3)) "...") + label) + tc (trail-node-color :tool-call tn)] + {:text label :type :keyword + :from 0 :to (count label) + :x (+ pad 4) :y (+ font-size (* i line-h)) + :size font-size + :r (:r tc) :g (:g tc) :b (:b tc) :a (:a tc)})) + cards)) + body-h (+ 4 (* n line-h)) + total-h (+ card-h-header body-h 6)] + (rt-node (keyword group-id) :tool-group-node + {:x 0 :y 0 :w pane-w :h total-h} + :style {:bg (:bg-subtle colors) :radius 4 + :border-width 1 :border-color (:border-subtle colors)} + :children + [(rt-node (keyword (str group-id "-hdr")) :tool-header + {:x 0 :y 0 :w pane-w :h card-h-header} + :data {:collapse-id (keyword group-id)} + :text [header-text-op arrow-op]) + (rt-node (keyword (str group-id "-body")) :tool-group-body + {:x 0 :y card-h-header :w pane-w :h body-h} + :text item-lines)])))) + + ;; Fallback — render as reasoning + (let [text (pr-str block) + lines (wrap-line text max-chars) + c {:r 0.75 :g 0.75 :b 0.75 :a 1.0} + text-ops (vec (map-indexed + (fn [i line] + {:text line :type :comment + :from 0 :to (count line) + :x pad :y (+ font-size (* i line-h)) + :size font-size + :r (:r c) :g (:g c) :b (:b c) :a (:a c)}) + lines)) + h (+ 4 (* (count lines) line-h))] + (rt-node (keyword (str "unknown-" bi)) :reasoning-block + {:x 0 :y 0 :w pane-w :h h} + :text text-ops)))) + blocks)))) + +(defn trail->display-lines + "Convert trail nodes to [{:text :color}]. Merges consecutive reasoning nodes." + [trail] + (reduce + (fn [acc node] + (case (:kind node) + :reasoning + (let [last-entry (peek acc)] + (if (and last-entry (= :reasoning (:kind last-entry))) + ;; Merge with previous reasoning node + (conj (pop acc) (update last-entry :text str (:text node))) + (conj acc {:kind :reasoning :text (:text node) + :color (trail-node-color :reasoning nil)}))) + + :thinking + (let [last-entry (peek acc)] + (if (and last-entry (= :thinking (:kind last-entry))) + (conj (pop acc) (update last-entry :text str (:text node))) + (conj acc {:kind :thinking :text (:text node) + :color (trail-node-color :thinking nil)}))) + + :tool-call + (let [input-summary (let [inp (:input node)] + (cond + (and (string? (:file_path inp)) (seq (:file_path inp))) + (:file_path inp) + (and (string? (:pattern inp)) (seq (:pattern inp))) + (str "\"" (:pattern inp) "\"") + (and (string? (:command inp)) (seq (:command inp))) + (let [cmd (:command inp)] + (if (> (count cmd) 60) + (str (subs cmd 0 57) "...") + cmd)) + :else ""))] + (conj acc {:kind :tool-call + :text (str ">> " (:tool-name node) " " input-summary) + :color (trail-node-color :tool-call (:tool-name node))})) + + :tool-call-start + (conj acc {:kind :tool-call-start + :text (str "> " (:tool-name node) "...") + :color (trail-node-color :tool-call-start nil)}) + + :tool-result + (let [raw-content (or (:content node) "") + content (if (string? raw-content) raw-content (pr-str raw-content)) + short (if (> (count content) 120) + (str (subs content 0 117) "...") + content)] + (conj acc {:kind :tool-result + :text (str " <- " short) + :color (trail-node-color :tool-result nil)})) + + ;; Unknown kind — render as-is + (conj acc {:kind (:kind node) :text (pr-str node) + :color {:r 0.75 :g 0.75 :b 0.75 :a 1.0}}))) + [] + trail)) + +(defn agent-wrapped-line-count + "Count wrapped display lines for agent output. Trail-aware: uses structured trail + when available, falls back to flat :output text. Includes header line in count." + [agent-output max-chars] + (let [status (:status agent-output) + provider-name (some-> (:provider agent-output) name str/upper-case) + prompt (:prompt agent-output) + header-text (when status (str "[" provider-name "] " (name status) ": " prompt)) + trail (:trail agent-output) + display-entries (when (seq trail) (trail->display-lines trail)) + raw-lines (if display-entries + ;; Trail path: header + structured trail lines + (cond-> [] + header-text (conj {:text header-text}) + (and (= status :running) (empty? display-entries)) (conj {:text "..."}) + (seq display-entries) (into display-entries)) + ;; Flat text fallback + (let [output-lines (str/split-lines (or (:output agent-output) "")) + flat-lines (cond-> [] + header-text (conj header-text) + (and (= status :running) (empty? output-lines)) (conj "...") + (seq output-lines) (into output-lines))] + (mapv (fn [l] {:text l}) flat-lines)))] + (count (into [] (mapcat (fn [entry] + (let [nl-lines (str/split-lines (or (:text entry) ""))] + (mapcat #(wrap-line % max-chars) nl-lines)))) + raw-lines)))) + +(defn compute-agent-panel-h + "Pure: dynamic panel height from agent output content. + Returns 0 when no agent output, otherwise sizes to content capped at 50% viewport. + Wraps lines to viewport width for accurate height. Trail-aware." + [agent-output font-size viewport-height viewport-width char-advance] + (if-not (some? (:status agent-output)) + 0 + (let [agent-x 24 + right-pad 24 + available-w (- viewport-width agent-x right-pad) + max-chars (if (pos? char-advance) (max 1 (int (/ available-w char-advance))) 80) + line-count (agent-wrapped-line-count agent-output max-chars) + line-step (* font-size 1.2) + content-h (+ 16 (* line-count line-step)) + max-h (* viewport-height 0.5)] + (min content-h max-h)))) diff --git a/src/app/client/workspace/ui_primitives.cljs b/src/app/client/workspace/ui_primitives.cljs new file mode 100644 index 0000000..6468d04 --- /dev/null +++ b/src/app/client/workspace/ui_primitives.cljs @@ -0,0 +1,435 @@ +(ns app.client.workspace.ui-primitives + "Design tokens, typography, and reusable UI component builders." + (:require [clojure.string :as str] + [app.client.workspace.rect-tree :as rt :refer [rt-node wrap-line]] + [components.design-tokens :as design-tokens])) + +(def list-row-h 36) +(def list-group-header-h 28) +(def list-left-pane-pct 0.40) +(def list-padding-x 16) +(def list-item-inset 6) ;; horizontal inset for rounded hover bg +(def list-padding-top 44) +(def list-checkbox-size 16) +(def list-divider-w 1) +(def list-group-gap 12) ;; vertical space between groups +(def list-footer-h 40) ;; footer height + +(def priority-colors + "Priority level -> RGBA color used by generic list/ticket UI." + {1 {:r 0.95 :g 0.30 :b 0.30 :a 1.0} + 2 {:r 0.95 :g 0.60 :b 0.25 :a 1.0} + 3 {:r 0.90 :g 0.80 :b 0.30 :a 1.0} + 4 {:r 0.45 :g 0.85 :b 0.45 :a 1.0} + 0 {:r 0.55 :g 0.55 :b 0.60 :a 0.8}}) + +;; ============================================================================ +;; DESIGN TOKENS (Linear/shadcn-inspired dark theme) +;; ============================================================================ + +(def dt + "Design tokens — imported from shared components.design-tokens." + design-tokens/dt) + +;; --- Typography hierarchy ---------------------------------------------------- +;; Consistent type scale: reference these instead of ad-hoc font sizes/alphas. + +(def typo-title {:size (:xl (:font-sizes dt)) :a 1.0}) +(def typo-subtitle {:size (:lg (:font-sizes dt)) :a 0.9}) +(def typo-body {:size (:md (:font-sizes dt)) :a 0.85}) +(def typo-caption {:size (:sm (:font-sizes dt)) :a 0.6}) + +;; ============================================================================ +;; COMPONENT LIBRARY (pure fns → rt-node trees) +;; ============================================================================ + +(defn ui-card + "Card component: rounded rect with border, optional shadow. + Returns an rt-node with :shadow and :radius in style." + [id bounds & {:keys [children text shadow variant] + :or {shadow :md variant :default}}] + (let [bg (case variant + :elevated (:bg-elevated (:colors dt)) + :muted (:bg-muted (:colors dt)) + (:bg-subtle (:colors dt))) + border (:border (:colors dt)) + radius (:lg (:radii dt)) + shadow-spec (get (:shadows dt) shadow)] + (rt-node id :card bounds + :style (cond-> {:bg bg :radius radius + :border-width 1 :border-color border} + shadow-spec (assoc :shadow shadow-spec)) + :children (vec (or children [])) + :text (or text [])))) + +(defn ui-badge + "Small pill badge with tinted background. Returns an rt-node." + [id bounds label & {:keys [color text-color font-size] + :or {color (:accent-muted (:colors dt)) + text-color (:accent (:colors dt)) + font-size (:xs (:font-sizes dt))}}] + (rt-node id :badge bounds + :style {:bg color :radius (:full (:radii dt))} + :text [{:text label :type :keyword + :from 0 :to (count label) + :x 6 :y (- (:h bounds) 4) + :size font-size + :r (nth text-color 0) :g (nth text-color 1) + :b (nth text-color 2) :a (nth text-color 3)}])) + +(defn ui-button + "Button component: solid/outline/ghost variants. Returns an rt-node." + [id bounds label & {:keys [variant on-click font-size] + :or {variant :solid font-size (:sm (:font-sizes dt))}}] + (let [accent (:accent (:colors dt)) + styles (case variant + :solid {:bg accent :radius (:md (:radii dt))} + :outline {:bg [0 0 0 0] :radius (:md (:radii dt)) + :border-width 1 :border-color accent} + :ghost {:bg [0 0 0 0] :radius (:md (:radii dt))} + {:bg accent :radius (:md (:radii dt))}) + text-c (case variant + :solid [1 1 1 1] + :outline accent + :ghost (:fg-muted (:colors dt)) + [1 1 1 1])] + (rt-node id :button bounds + :style styles + :actions (if on-click {:click on-click} {}) + :text [{:text label :type :text + :from 0 :to (count label) + :x (:sm (:spacing dt)) :y (- (:h bounds) 5) + :size font-size + :r (nth text-c 0) :g (nth text-c 1) + :b (nth text-c 2) :a (nth text-c 3)}]))) + +(defn ui-divider + "Thin horizontal separator line. Returns an rt-node." + [id bounds & {:keys [color] :or {color (:border-subtle (:colors dt))}}] + (rt-node id :divider bounds :style {:bg color})) + +(defn ui-progress + "Progress bar (track + fill). Returns an rt-node with child fill rect." + [id bounds progress & {:keys [color track-color] + :or {color (:accent (:colors dt)) + track-color (:bg-muted (:colors dt))}}] + (let [fill-w (* (:w bounds) (min 1.0 (max 0.0 progress)))] + (rt-node id :progress bounds + :style {:bg track-color :radius (:sm (:radii dt))} + :children [(rt-node (keyword (str (name id) "-fill")) :progress-fill + {:x 0 :y 0 :w fill-w :h (:h bounds)} + :style {:bg color :radius (:sm (:radii dt))})]))) + +(defn ui-scrollbar + "Vertical scrollbar (track + thumb). Returns an rt-node." + [id bounds thumb-pct thumb-offset & {:keys [track-color thumb-color] + :or {track-color [0 0 0 0] + thumb-color [0.35 0.35 0.40 0.5]}}] + (let [track-h (:h bounds) + thumb-h (max 20 (* track-h thumb-pct)) + thumb-y (* (- track-h thumb-h) (min 1.0 (max 0.0 thumb-offset)))] + (rt-node id :scrollbar bounds + :style {:bg track-color} + :children [(rt-node (keyword (str (name id) "-thumb")) :scrollbar-thumb + {:x 1 :y thumb-y :w (- (:w bounds) 2) :h thumb-h} + :style {:bg thumb-color :radius (:full (:radii dt))})]))) + +(defn ui-tabs + "Tab bar with active indicator. Returns an rt-node. + tabs: [{:id :label}], active-id: keyword." + [id bounds tabs active-id & {:keys [font-size padding] + :or {font-size (:sm (:font-sizes dt)) padding 16}}] + (let [char-adv (* font-size 0.56) + {tab-nodes :nodes} + (reduce + (fn [{:keys [nodes tx]} tab] + (let [active? (= (:id tab) active-id) + label-str (:label tab) + text-w (* (count label-str) char-adv) + tab-w (+ text-w (* 2 padding))] + {:nodes + (conj nodes + (rt-node (:id tab) :tab + {:x tx :y 0 :w tab-w :h (:h bounds)} + :data {:tab-id (:id tab)} + :children (when active? + [(rt-node (keyword (str (name (:id tab)) "-ind")) :tab-indicator + {:x padding :y (- (:h bounds) 2) :w text-w :h 2} + :style {:bg [0.9 0.9 0.92 1.0]})]) + :text [{:text label-str :type (if active? :keyword :text) + :from 0 :to (count label-str) + :x padding :y (- (:h bounds) 14) + :size font-size + :r (if active? 0.9 0.55) + :g (if active? 0.9 0.55) + :b (if active? 0.92 0.60) + :a 1.0}])) + :tx (+ tx tab-w)})) + {:nodes [] :tx 0} + tabs)] + (rt-node id :tab-bar bounds + :style {:bg (:bg-elevated (:colors dt)) + :border-widths [0 0 1 0] + :border-color (:border (:colors dt))} + :children tab-nodes))) + +(defn ui-tooltip + "Small floating card with text, positioned at anchor. Returns an rt-node." + [id bounds label & {:keys [font-size] :or {font-size (:xs (:font-sizes dt))}}] + (let [fg (:fg (:colors dt))] + (ui-card id bounds + :shadow :sm + :variant :elevated + :text [{:text label :type :text + :from 0 :to (count label) + :x (:sm (:spacing dt)) :y (- (:h bounds) 5) + :size font-size + :r (nth fg 0) :g (nth fg 1) :b (nth fg 2) :a (nth fg 3)}]))) + +(defn build-empty-state + "Centered empty state with icon, headline, description. + Returns an rt-node positioned at center of w x h bounds. + icon: 2-char text symbol (e.g. \"--\", \"[]\", \"<>\") + headline: main message text + description: secondary guidance text" + [id w h {:keys [icon headline description]}] + (let [surfaces (:surfaces dt) + colors (:colors dt) + text-sec (or (:text-secondary surfaces) (:fg-muted colors)) + text-mut (or (:text-muted surfaces) (:fg-subtle colors)) + cx (/ w 2) + ;; Center vertically, offset upward slightly for visual balance + cy (- (/ h 2) 40) + icon-size (:size typo-title) + head-size (:size typo-subtitle) + desc-size (:size typo-body) + icon-w (* (count (or icon "")) icon-size 0.56) + head-w (* (count (or headline "")) head-size 0.56) + desc-lines (when description + (let [max-chars (max 20 (int (/ (* w 0.6) (* desc-size 0.56))))] + (wrap-line description max-chars)))] + (rt-node id :empty-state + {:x 0 :y 0 :w w :h h} + :text + (cond-> [] + ;; Icon + icon + (conj {:text icon :type :comment + :from 0 :to (count icon) + :x (- cx (/ icon-w 2)) :y cy + :size icon-size + :r (nth text-mut 0) :g (nth text-mut 1) + :b (nth text-mut 2) :a (nth text-mut 3 0.5)}) + ;; Headline + headline + (conj {:text headline :type :text + :from 0 :to (count headline) + :x (- cx (/ head-w 2)) :y (+ cy 32) + :size head-size + :r (nth text-sec 0) :g (nth text-sec 1) + :b (nth text-sec 2) :a (nth text-sec 3 0.7)}) + ;; Description lines (centered) + desc-lines + (into (map-indexed + (fn [i line] + (let [line-w (* (count line) desc-size 0.56)] + {:text line :type :comment + :from 0 :to (count line) + :x (- cx (/ line-w 2)) :y (+ cy 56 (* i 22)) + :size desc-size + :r (nth text-mut 0) :g (nth text-mut 1) + :b (nth text-mut 2) :a 0.7})) + desc-lines)))))) + +;; ============================================================================ +;; COMPOSABLE PANEL COMPONENTS (shadcn Sidebar pattern for WebGPU) +;; ============================================================================ +;; Pure fns returning rt-nodes with :layout directives. +;; Composition: ui-panel > ui-panel-header > ui-panel-content > ui-panel-group > ui-list-item +;; The layout engine (resolve-layout) handles all child positioning. + +(defn ui-panel + "Container panel — the outermost sidebar/panel frame. + Vertical column layout: stacks header/content/footer automatically. + variant: :default (bg), :elevated (bg-elevated), :subtle (bg-subtle)" + [id bounds & {:keys [children variant style] + :or {variant :default}}] + (let [bg (case variant + :elevated (:bg-elevated (:colors dt)) + :subtle (:bg-subtle (:colors dt)) + (:bg (:colors dt)))] + (rt-node id :panel bounds + :style (merge {:bg bg} style) + :layout {:direction :column} + :children (vec (or children []))))) + +(defn ui-panel-header + "Fixed-height header slot with integrated bottom border. + text: vector of text-op maps (with :x/:y in local coords)." + [id w h & {:keys [children text style]}] + (rt-node id :panel-header + {:x 0 :y 0 :w w :h h} + :style (merge {:bg (:bg-elevated (:colors dt)) + :border-widths [0 0 1 0] + :border-color (:border-subtle (:colors dt))} + style) + :children (vec (or children [])) + :text (or text []))) + +(defn ui-panel-content + "Scrollable content area — fills remaining height, clips children. + layout-opts override defaults: {:direction :column :gap list-group-gap}." + [id w h & {:keys [children layout-opts]}] + (rt-node id :panel-content + {:x 0 :y 0 :w w :h h} + :clip? true + :layout (merge {:direction :column :gap list-group-gap :padding [4 0 0 0]} layout-opts) + :children (vec (or children [])))) + +(defn ui-panel-footer + "Fixed-height footer slot with top border separator." + [id w h & {:keys [children text style]}] + (rt-node id :panel-footer + {:x 0 :y 0 :w w :h h} + :style (merge {:bg (:bg-elevated (:colors dt)) + :border-widths [1 0 0 0] + :border-color (:border-subtle (:colors dt))} style) + :children (vec (or children [])) + :text (or text []))) + +(defn ui-panel-group + "Collapsible labeled section — shadcn-style uppercase muted label + items. + items: vector of rt-nodes (typically ui-list-item results). + status: raw status string (for hit-test/collapse). label: display string. + Height is auto-computed: header-h + (collapsed? 0 : items)." + [id w & {:keys [label status icon icon-color collapsed? items font-size first-group?] + :or {font-size (:xs (:font-sizes dt)) + collapsed? false + first-group? false}}] + (let [header-h list-group-header-h + sc (:fg-section (:colors dt)) + ind (if collapsed? ">" "v") + upper-label (str/upper-case (or label "")) + label-str (str ind " " upper-label) + ;; Subtle top separator between groups (skip first) + sep-node (when-not first-group? + (rt-node (keyword (str (name id) "-sep")) :divider + {:x list-padding-x :y 0 :w (- w (* 2 list-padding-x)) :h 1} + :style {:bg (:border-subtle (:colors dt))})) + group-header (rt-node (keyword (str (name id) "-hdr")) :group-header + {:x 0 :y 0 :w w :h header-h} + :data {:status (or status label)} + :text [{:text label-str :type :comment + :from 0 :to (count label-str) + :x list-padding-x :y 19 + :size font-size + :r (nth sc 0) :g (nth sc 1) :b (nth sc 2) :a (nth sc 3)}]) + visible-items (if collapsed? [] (vec items)) + all-children (cond-> [] + sep-node (conj sep-node) + true (conj group-header) + true (into visible-items)) + sep-h (if sep-node 1 0) + total-h (+ sep-h header-h (if collapsed? 0 (* (count (or items [])) list-row-h)))] + (rt-node id :panel-group + {:x 0 :y 0 :w w :h total-h} + :layout {:direction :column} + :children all-children))) + +(defn ui-list-item + "Row with leading/title/trailing slots — shadcn SidebarMenuItem style. + Rounded inset hover/selected background with left accent bar on selected. + leading/trailing are child rt-nodes. + title: string — rendered as text between leading and trailing." + [id w & {:keys [title leading trailing selected? hovered? ghost? data + font-size title-x-offset] + :or {font-size (:sm (:font-sizes dt)) + title-x-offset (+ list-padding-x list-checkbox-size 16)}}] + (let [;; Outer row is transparent — inner child gets the rounded bg + ga (if ghost? 0.3 1.0) + ;; Inner rounded highlight rect (inset from edges) + inner-bg (cond + (and selected? ghost?) (assoc (:bg-selected (:colors dt)) 3 0.15) + selected? (:bg-selected (:colors dt)) + hovered? (:bg-hover (:colors dt)) + :else nil) + inset list-item-inset + inner-h (- list-row-h 4) ;; 2px top + 2px bottom breathing room + inner-w (- w (* 2 inset)) + highlight-node (when inner-bg + (rt-node (keyword (str (name id) "-hl")) :highlight + {:x inset :y 2 :w inner-w :h inner-h} + :style {:bg inner-bg + :radius (:lg (:radii dt))})) + ;; Left accent bar on selected items (shadcn active indicator) + accent-bar (when (and selected? (not ghost?)) + (rt-node (keyword (str (name id) "-acc")) :accent-bar + {:x (+ inset 1) :y 6 :w 3 :h (- list-row-h 12)} + :style {:bg (:accent (:colors dt)) + :radius (:sm (:radii dt))})) + ;; Text truncation + trunc-max (if (pos? font-size) + (max 8 (int (/ (- w title-x-offset 36) + (* font-size 0.56)))) + 30) + trunc (when title + (if (> (count title) trunc-max) + (str (subs title 0 (- trunc-max 2)) "..") + title)) + ;; Text vertically centered in row + text-y (+ (/ list-row-h 2) (/ font-size 2.5)) + children (cond-> [] + highlight-node (conj highlight-node) + accent-bar (conj accent-bar) + (and leading (not ghost?)) (conj leading) + (and trailing (not ghost?)) (conj trailing)) + ;; Selected text gets slightly brighter + fg (if selected? + [0.95 0.95 0.98 1.0] + (:fg (:colors dt))) + text-ops (cond-> [] + trunc + (conj {:text trunc :type :text + :from 0 :to (count trunc) + :x title-x-offset :y text-y + :size font-size + :r (nth fg 0) :g (nth fg 1) :b (nth fg 2) + :a (* (nth fg 3) ga)}))] + (rt-node id :ticket-row + {:x 0 :y 0 :w w :h list-row-h} + :data (merge {:selected? selected? :hovered? hovered? :ghost? ghost?} + data) + :children children + :text text-ops))) + +(defn ui-checkbox + "Simplified checkbox — single rect, no inner fill child. + Checked: accent bg + accent border. Unchecked: transparent + subtle border." + [id & {:keys [checked? size] + :or {size list-checkbox-size}}] + (let [cb-x (+ list-padding-x 4) + cb-y (/ (- list-row-h size) 2)] + (rt-node id :checkbox + {:x cb-x :y cb-y :w size :h size} + :style {:bg (if checked? + (:accent (:colors dt)) + [0.15 0.15 0.18 0.6]) + :radius (:sm (:radii dt)) + :border-width 1.5 + :border-color (if checked? + (:accent (:colors dt)) + [0.30 0.30 0.36 0.5])}))) + +(defn ui-priority-dot + "Circular color dot indicating priority level (1-4). + Uses priority-colors lookup. 8px dot, vertically centered." + [id priority & {:keys [parent-w] :or {parent-w 0}}] + (let [prio (or priority 0) + dot-size 8 + pc (get priority-colors prio {:r 0.55 :g 0.55 :b 0.60 :a 0.8})] + (rt-node id :priority-dot + {:x (if (pos? parent-w) (- parent-w 24) 0) + :y (/ (- list-row-h dot-size) 2) + :w dot-size :h dot-size} + :style {:bg [(:r pc) (:g pc) (:b pc) (:a pc)] + :radius (:full (:radii dt))}))) diff --git a/src/app/electric_flow.cljc b/src/app/electric_flow.cljc index 332b0da..e225c53 100644 --- a/src/app/electric_flow.cljc +++ b/src/app/electric_flow.cljc @@ -1,353 +1,598 @@ (ns app.electric-flow - (:require [hyperfiddle.electric3 :as e] - [missionary.core :as m] + (:require [clojure.string :as str] + [hyperfiddle.electric3 :as e] [hyperfiddle.electric-dom3 :as dom] - [hyperfiddle.electric-svg3] - [hyperfiddle.incseq.mount-impl :refer [mount]] - [hyperfiddle.kvs :as kvs] - [hyperfiddle.domlike :as dl] - [hyperfiddle.incseq :as i] - #?@(:cljs [[app.client.webgpu.core :as wcore :refer [render-rect render-text]] - [global-flow :refer [await-promise - mouse-down?> - debounce - !canvas - !font-bitmap - global-client-flow - !adapter - !global-atom - !device - !context - !atlas-data - !command-encoder - !format - !all-rects - !width - !height - !canvas-y - !visible-rects - !dpr - !old-visible-rects - !canvas-x - !zoom-factor - !offset]] - [app.client.webgpu.data :refer [!rects]]]))) - - -(hyperfiddle.rcf/enable!) - - -(e/declare canvas) -(e/declare squares) -(e/declare adapter) -(e/declare device) -(e/declare context) -(e/declare format) -(e/declare command-encoder) -(e/declare all-rects) -(e/declare width) -(e/declare height) -(e/declare canvas-y) -(e/declare canvas-x) -(e/declare offset) -(e/declare zoom-factor) -(e/declare rect-ids) -(e/declare visible-rects) -(e/declare old-visible-rects) -(e/declare data-spine) -(e/declare global-atom) -(e/declare font-bitmap) -(e/declare atlas-data) -(e/declare dpr) - - -(defn create-random-rects [rects ch cw] - (let [res (atom {})] - ;(println "RAND" @res rc ch cw) - (doseq [i rects] - (let [height (+ 20.0 (rand-int 60)) - width (+ 145.0 (rand-int 60)) - y (+ 0.1 (rand-int ch)) - x (+ 0.1 (rand-int cw))] - ;(js/console.log "xx" x y) - ;(println i 'Create-random-rects (keyword (str i)) [x y height width]) - (swap! res assoc (keyword (str i)) [x y height width]))) - (println "all RECTS" @res) - res)) - -#?(:cljs (defn format-float [inp] - (js/Number (.toFixed inp 3)))) - -#?(:cljs (defn clip-x [x w] (- (* 2 (/ x w)) 1))) -#?(:cljs (defn clip-y [y h] (- 1 (* 2 (/ y h))))) - -(e/defn Setup-webgpu [] - (e/client - (when (some? canvas) - (js/console.log canvas) - (let [context (.getContext canvas "webgpu" (clj->js {:alpha true})) - gpu js/navigator.gpu - adapter (e/Task (await-promise (.requestAdapter gpu (clj->js {:requiredFeatures ["validation"]})))) - device (e/Task (await-promise (.requestDevice adapter))) - cformat (.getPreferredCanvasFormat gpu) - config (clj->js {:format cformat - :device device})] - (.configure context config) - (reset! !adapter adapter) - (reset! !device device) - (reset! !context context) - (reset! !format cformat))))) - - - -(e/defn Mouse-down-cords [node] (e/input (mouse-down?> node))) - - -(e/defn Add-panning [] - (when-some [[start-x start-y] (Mouse-down-cords canvas)] - (let [[off-x off-y] (e/snapshot offset)] - (dom/On "mousemove" - (fn [e] - (.preventDefault e) - (let [end-x (.-clientX e) - end-y (.-clientY e) - new-pan-x (+ off-x (* dpr (- end-x start-x))) - new-pan-y (+ off-y (* dpr (- end-y start-y)))] - (reset! !offset [new-pan-x new-pan-y]) - [new-pan-x new-pan-y])) - "")))) - - - -(e/defn Add-wheel [] - (dom/On "wheel" - (fn [e] (.preventDefault e) - (let [delta (.-deltaY e) - rect (.getBoundingClientRect (.-target e)) - cursor-x (* dpr (- (.-clientX e) (.-left rect))) - cursor-y (* dpr (- (.-clientY e) (.-top rect))) - scale (if (< delta 0) 1.02 0.98) - new-zoom (* zoom-factor scale) - [off-x off-y] offset - pan-zoom (- 1 scale) - current-pan-x (* (- cursor-x off-x) pan-zoom) - current-pan-y (* (- cursor-y off-y) pan-zoom) - total-pan-x (+ off-x current-pan-x) - total-pan-y (+ off-y current-pan-y)] - (println "Wheel" total-pan-x total-pan-y new-zoom 1) - (reset! !offset [total-pan-x total-pan-y]) - (reset! !zoom-factor new-zoom))) - nil - {:passive false})) - - -(e/defn Render-with-webgpu [] - (let [[spend e] (e/Token offset) - dv (e/snapshot device) - con (e/snapshot context) - fmat (e/snapshot format)] - (when (and (some? atlas-data) - (some? device) - (some? font-bitmap) - (some? context) - (some? format)) - (when (some? spend) - (let [rects-data (flatten (into [] (vals all-rects))) - rects-ids (into [] (keys all-rects)) - [cx cy] offset - [off-x off-y] (spend (e/Task (m/sleep 25 offset))) - rx (e/amb cx off-x) - ry (e/amb cy off-y) - texts (reduce - (fn [acc [id data]] - (let [[x y dh dw] data - - left (clip-x - (+ (* (+ 7 x) zoom-factor) off-x) - width) - top (clip-y - (+ (* (+ 7 y) zoom-factor) off-y) - height)] - (conj acc {:x left - :y top - :text (str (name id))}))) - [] - all-rects) - zof (max 17 (* (/ 1 zoom-factor) 14))] - (render-rect - "zoom" - rects-data - dv - fmat - con - [width height rx ry zoom-factor] - rects-ids) - (render-text - dv - fmat - con - 16 - zof - atlas-data - font-bitmap - texts)))))) - - -(e/defn Tap-diffs - ([f! x] - (f! (e/input (e/pure x))) - x) - ([x] (Tap-diffs prn x))) - - - -(e/defn On-node-add [id] - (when-some [[x y h w] (id all-rects)] - ((fn [] - (let [gx (-> global-atom :cords first) - gy (-> global-atom :cords second) - [ox oy zf] offset - cgx (- (clip-x gx width) ox) - cgy (- (clip-y gy height) oy) - cl (clip-x x width) - cr (clip-x (+ x w) width) - ct (clip-y y height) - cb (clip-y (+ y h) height) - zff (or zf zoom-factor) - clicked? (and (<= cgx cr) (>= cgx cl) - (<= cgy ct) (>= cgy cb))] - (println - id - gx gy - zoom-factor - offset - ":R:" - [x y h w] - (format-float (+ ox (* zff cl))) - (format-float (+ oy (* zff ct))))))))) - -(e/defn Canvas-view [] - (e/client - (dom/canvas - (dom/props {:id "top-canvas" - :height height - :width width - :style {:height (str (/ height dpr) "px") - :width (str (/ width dpr) "px")}}) - (reset! !canvas dom/node) - (Render-with-webgpu) - - (when-some [down (Mouse-down-cords dom/node)] - (println "DOWN") - (reset! !global-atom {:cords down})) - #_(e/for-by identity [node (e/as-vec (e/input (e/join (i/items data-spine))))] - - (println node global-atom) - #_(On-node-add node)) - #_(println "NEW SPINE" - (count visible-rects) - (e/input (i/count data-spine)) - visible-rects - (e/as-vec (e/input (e/join (i/items data-spine))))) - (let [mount-items (mount - (fn [element child] (do - (data-spine - child - (fn [_ new] - (keyword (str new))) - child) - (.push element child) - element)) - (fn [element child previous] (do - (let [idx (.indexOf element previous)] - (when (>= idx 0) - (aset element idx child))) - element)) - (fn [element child sibling] (do - (let [idx (.indexOf element sibling)] - (if (>= idx 0) - (.splice element idx 0 child) - (.push element child))) - element)) - (fn [element child] (do - (data-spine - child - (fn [_ new] - (keyword (str new))) - nil) - (let [idx (.indexOf element child)] - (when (>= idx 0) - (.splice element idx 1))) - element)) - (fn [element i] (do - (aget element i)))) - - diff (e/input (e/pure (e/diff-by identity visible-rects)))] - - ((fn [] (when (some? diff) - (mount-items (object-array @!old-visible-rects) diff)))))))) - - - -#?(:cljs (defn load-bitmap-file [] - (println "Load bitmap file") - (-> (js/fetch "/font_atlas.png") - (.then #(.blob %)) - (.then #(js/createImageBitmap %)) - (.then (fn [img] - (reset! !font-bitmap img)))))) - + [app.client.workspace.themes :as themes] + [app.file-viewer :as fv] + #?@(:cljs [[app.client.substrate.webgpu.renderer :as editor] + [app.client.substrate.webgpu.gpu-budget :as gpu-budget] + [app.client.workspace.runtime.fonts :as runtime-fonts] + [app.client.workspace.runtime :as loop] + [global-flow :refer [await-promise]] + ["@lezer/lr" :as lr] + ["@nextjournal/lezer-clojure" :as clj-parser] + [sci.core :as sci]]))) + +(def source-code + #?(:clj (slurp "src/app/electric_flow.cljc") + :cljs nil)) + +(def initial-file-info + #?(:clj (let [project-dir (System/getProperty "user.dir") + rel-path "src/app/electric_flow.cljc"] + {:path (str project-dir "/" rel-path) + :project project-dir}) + :cljs nil)) + +#?(:clj (defn init-lezer-parser! [] nil)) +#?(:clj (defn find-matching-bracket [_ _ _] nil)) +#?(:clj (defn detect-fold-regions [_ _] [])) +#?(:clj (defn init-sci! [] nil)) +#?(:clj (defn sci-eval [_] {:error "SCI only available in browser"})) +#?(:clj (defn sci-eval-form [_] "SCI only available in browser")) +#?(:clj (defn find-form-at-cursor [_ _ _] nil)) + #?(:cljs - (defn read-json-file [] - (-> (js/fetch "/font_atlas.json") - (.then (fn [response] - (.json response))) - (.then (fn [data] - (reset! !atlas-data (js->clj data :keywordize-keys true))))))) + (do + (def lezer-parser (atom nil)) + (defn init-lezer-parser! [] + (when-not @lezer-parser + (reset! lezer-parser (.-parser clj-parser)))) -(e/defn main [ring-request] + ;; === SCI (Small Clojure Interpreter) === + + (def sci-ctx (atom nil)) + + (defn init-sci! [] + "Initialize SCI context with clojure.core and common namespaces" + (when-not @sci-ctx + (reset! sci-ctx + (sci/init {:namespaces {'user {}} + :classes {'js js/globalThis}})))) + + (defn sci-eval + "Evaluate a Clojure string using SCI. Returns {:result value} or {:error message}" + [code-str] + (try + (when-not @sci-ctx (init-sci!)) + {:result (sci/eval-string* @sci-ctx code-str)} + (catch :default e + {:error (.-message e)}))) + + (defn sci-eval-form + "Evaluate a single form string. Returns formatted result string." + [form-str] + (let [{:keys [result error]} (sci-eval form-str)] + (if error + (str "❌ " error) + (str "=> " (pr-str result))))) + + (def macro-symbols + #{"defn" "def" "defmacro" "defn-" "defonce" "defmulti" "defmethod" "defprotocol" "defrecord" "deftype" + "let" "fn" "if" "if-let" "if-some" "do" "ns" "when" "when-let" "when-some" "when-not" "when-first" + "cond" "condp" "case" "loop" "recur" "for" "doseq" "dotimes" "while" + "try" "catch" "finally" "throw" "assert" + "binding" "with-open" "with-local-vars" "with-redefs" + "require" "import" "use" "refer" "in-ns" + "->" "->>" "as->" "some->" "some->>" "cond->" "cond->>" + "and" "or" "not" + "lazy-seq" "delay" "future" "promise" + "e/defn" "e/client" "e/server" "e/fn" "dom/on"}) + + (defn classify-token [node-name text] + (cond + (= node-name "LineComment") :comment + (or (= node-name "String") (= node-name "RegExp")) :string + (= node-name "Character") :character + (= node-name "Keyword") :keyword + (= node-name "Number") :number + (or (= node-name "Boolean") (= node-name "BooleanLiteral")) :boolean + (= node-name "Nil") :nil + (contains? #{"(" ")" "[" "]" "{" "}"} text) :delimiter + (contains? macro-symbols text) :macro + :else :text)) + + (def container-node-types + #{"Program" "List" "Vector" "Map" "Set" "Meta" "Deref" "Quote" + "SyntaxQuote" "Unquote" "UnquoteSplice" "Anon" "Regex" + "VarQuote" "Discard" "NamespacedMap" "ReaderConditional"}) + + (defn extract-tokens-from-syntax-tree [tree text] + (let [tokens (atom [])] + (.. ^js tree + (iterate #js {:enter (fn [node] + (let [node-name (.-name ^js (.-type ^js node)) + from (.-from ^js node) + to (.-to ^js node) + node-text (.substring text from to)] + (when (and (not (contains? container-node-types node-name)) + (or (= node-name "String") + (= node-name "LineComment") + (not (re-find #"\s" node-text))) + (not (str/blank? node-text)) + (> (count node-text) 0)) + (swap! tokens conj {:text node-text + :type (classify-token node-name node-text) + :from from + :to to}))))})) + @tokens)) + + (defn tokenize-line [line-text] + (if (or (empty? line-text) (not @lezer-parser)) + [] + (let [tree (.parse ^js @lezer-parser line-text)] + (extract-tokens-from-syntax-tree tree line-text)))) + + ;; Bracket matching pairs + (def bracket-pairs + {"(" ")" ")" "(" + "[" "]" "]" "[" + "{" "}" "}" "{"}) + + (def open-brackets #{"(" "[" "{"}) + (def close-brackets #{")" "]" "}"}) + + (defn offset->line-col + "Convert absolute offset to {:line :col} given line-lengths" + [offset line-lengths] + (loop [remaining offset + line-idx 0] + (if (>= line-idx (count line-lengths)) + {:line (dec (count line-lengths)) :col (get line-lengths (dec (count line-lengths)) 0)} + (let [line-len (inc (get line-lengths line-idx 0))] ;; +1 for newline + (if (< remaining line-len) + {:line line-idx :col remaining} + (recur (- remaining line-len) (inc line-idx))))))) + + (defn line-col->offset + "Convert {:line :col} to absolute offset given line-lengths" + [{:keys [line col]} line-lengths] + (let [lines-before (subvec line-lengths 0 (min line (count line-lengths))) + offset-to-line (reduce + (map inc lines-before))] ;; +1 for each newline + (+ offset-to-line col))) + + (defn find-form-at-cursor + "Given cursor position and lines, find the outermost form containing cursor. + Returns {:form-str :start-line :end-line} or nil" + [cursor-pos lines line-lengths] + (when (and @lezer-parser cursor-pos (seq lines)) + (let [full-text (str/join "\n" lines) + cursor-offset (line-col->offset cursor-pos line-lengths) + tree (.parse ^js @lezer-parser full-text) + ;; Find the outermost List/Vector/Map containing cursor + ;; Lezer iterates parent-first, so first match is outermost + best-match (atom nil)] + (.. ^js tree + (iterate #js {:enter (fn [node] + (let [node-name (.-name ^js (.-type ^js node)) + from (.-from ^js node) + to (.-to ^js node)] + ;; Check if cursor is inside this node + (when (and (contains? #{"List" "Vector" "Map" "Set"} node-name) + (<= from cursor-offset) + (< cursor-offset to)) + ;; Keep only the first (outermost) match + (when (nil? @best-match) + (reset! best-match {:from from + :to to + :type node-name})))))})) + (when @best-match + (let [{:keys [from to]} @best-match + form-str (.substring full-text from to) + start-pos (offset->line-col from line-lengths) + end-pos (offset->line-col (dec to) line-lengths)] + {:form-str form-str + :start-line (:line start-pos) + :end-line (:line end-pos) + :from from + :to to}))))) + + (defn find-matching-bracket + "Given cursor position and document, find matching bracket if cursor is on one. + Returns {:open {:line :col} :close {:line :col}} or nil" + [cursor-pos lines line-lengths] + (when (and @lezer-parser cursor-pos (seq lines)) + (let [full-text (str/join "\n" lines) + cursor-offset (line-col->offset cursor-pos line-lengths) + ;; Check char at cursor and char before cursor + char-at (when (< cursor-offset (count full-text)) + (str (nth full-text cursor-offset))) + char-before (when (and (> cursor-offset 0) (<= cursor-offset (count full-text))) + (str (nth full-text (dec cursor-offset)))) + ;; Determine which bracket we're on + [bracket-char bracket-offset] + (cond + (get bracket-pairs char-at) [char-at cursor-offset] + (get bracket-pairs char-before) [char-before (dec cursor-offset)] + :else [nil nil])] + (when bracket-char + (let [tree (.parse ^js @lezer-parser full-text) + ;; Walk tree to find the bracket's container node + result (atom nil)] + ;; Iterate through tree to find matching container + (.. ^js tree + (iterate #js {:enter (fn [node] + (let [node-name (.-name ^js (.-type ^js node)) + from (.-from ^js node) + to (.-to ^js node)] + ;; Check if this is a container and bracket is at boundary + (when (and (contains? container-node-types node-name) + (or (= from bracket-offset) + (= (dec to) bracket-offset))) + (reset! result {:open (offset->line-col from line-lengths) + :close (offset->line-col (dec to) line-lengths)}))))})) + @result))))) + + (def foldable-node-types + #{"List" "Vector" "Map" "Set"}) + + (defn detect-fold-regions + "Detect all foldable regions in the document. + Returns [{:start-line :end-line :type} ...] for multi-line forms" + [lines line-lengths] + (when (and @lezer-parser (seq lines)) + (let [full-text (str/join "\n" lines) + tree (.parse ^js @lezer-parser full-text) + regions (atom [])] + (.. ^js tree + (iterate #js {:enter (fn [node] + (let [node-name (.-name ^js (.-type ^js node)) + from (.-from ^js node) + to (.-to ^js node)] + (when (contains? foldable-node-types node-name) + (let [start-pos (offset->line-col from line-lengths) + end-pos (offset->line-col (dec to) line-lengths)] + ;; Only foldable if spans multiple lines + (when (> (:line end-pos) (:line start-pos)) + (swap! regions conj {:start-line (:line start-pos) + :end-line (:line end-pos) + :type node-name}))))))})) + ;; Sort by start line, nested regions come after their parents + (sort-by :start-line @regions)))))) + +(def jvm-macro-symbols + #{"defn" "def" "defmacro" "defn-" "defonce" "defmulti" "defmethod" "defprotocol" "defrecord" "deftype" + "let" "fn" "if" "if-let" "if-some" "do" "ns" "when" "when-let" "when-some" "when-not" "when-first" + "cond" "condp" "case" "loop" "recur" "for" "doseq" "dotimes" "while" + "try" "catch" "finally" "throw" "assert" + "binding" "with-open" "with-local-vars" "with-redefs" + "require" "import" "use" "refer" "in-ns" + "->" "->>" "as->" "some->" "some->>" "cond->" "cond->>" + "and" "or" "not" + "lazy-seq" "delay" "future" "promise" + "e/defn" "e/client" "e/server" "e/fn" "dom/on"}) + +#?(:clj + (defn tokenize-line [line-text] + (if (empty? line-text) + [] + (let [pattern #";.*|:[^ \[\]\(\)\s]+|[\(\)\[\]\{\}]|\"[^\"]*\"|\s+|[^ ;:\[\]\(\)\{\}\"\s]+" + matches (re-seq pattern line-text)] + (loop [tokens [] + pos 0 + [m & rest-matches] matches] + (if (nil? m) + tokens + (let [match-start (.indexOf line-text m pos) + match-end (+ match-start (count m)) + token {:text m + :from match-start + :to match-end + :type (cond + (str/starts-with? m ";") :comment + (str/starts-with? m ":") :keyword + (str/starts-with? m "\"") :string + (str/blank? m) :whitespace + (contains? #{"(" ")" "[" "]" "{" "}"} m) :delimiter + (contains? jvm-macro-symbols m) :macro + :else :text)}] + (if (= :whitespace (:type token)) + (recur tokens match-end rest-matches) + (recur (conj tokens token) match-end rest-matches))))))))) + +;; ============================================================================ +;; SYNTAX HIGHLIGHTING (delegated to themes namespace) +;; ============================================================================ + +(def default-theme-id themes/default-theme-id) + +(defn get-color + "Get color for a token type from the specified theme" + ([type] (themes/get-color type)) + ([type theme-id] (themes/get-color type theme-id))) + +(defn line-visible? + "Check if a logical line should be visible given fold regions and folded state. + A line is hidden if it's inside a folded region (but not the first line of that region)." + [line-idx fold-regions folded-lines] + (not (some (fn [{:keys [start-line end-line]}] + (and (contains? folded-lines start-line) ;; This region is folded + (> line-idx start-line) ;; Line is after the fold start + (<= line-idx end-line))) ;; Line is within the fold + fold-regions))) + +(defn layout-tokens + "Layout tokens with optional folding support and theme. + Returns {:render-ops [...] :line-mapping [...]} where line-mapping maps visual->logical line." + ([lines-of-tokens start-x start-y font-size] + ;; No folding - all lines visible, default theme + (layout-tokens lines-of-tokens start-x start-y font-size [] #{} nil nil default-theme-id)) + ([lines-of-tokens start-x start-y font-size fold-regions folded-lines] + (layout-tokens lines-of-tokens start-x start-y font-size fold-regions folded-lines nil nil default-theme-id)) + ([lines-of-tokens start-x start-y font-size fold-regions folded-lines char-advance line-h] + (layout-tokens lines-of-tokens start-x start-y font-size fold-regions folded-lines char-advance line-h default-theme-id)) + ([lines-of-tokens start-x start-y font-size fold-regions folded-lines char-advance line-h theme-id] + (let [char-width (or char-advance (* font-size 0.56)) + line-h (or line-h (* font-size 1.2)) + active-theme-id (or theme-id default-theme-id)] + (loop [logical-idx 0 + visual-y (+ start-y font-size) + render-ops [] + line-mapping []] ;; Maps visual line index -> logical line index + (if (>= logical-idx (count lines-of-tokens)) + {:render-ops render-ops + :line-mapping line-mapping} + (let [tokens (nth lines-of-tokens logical-idx) + visible? (line-visible? logical-idx fold-regions folded-lines)] + (if visible? + ;; Render this line at current visual-y, using the active theme + (let [line-ops (mapv (fn [token] + (let [color (get-color (:type token) active-theme-id)] + (merge token color + {:x (+ start-x (* (or (:from token) 0) char-width)) + :y visual-y + :size font-size}))) + tokens)] + (recur (inc logical-idx) + (+ visual-y line-h) + (conj render-ops line-ops) + (conj line-mapping logical-idx))) + ;; Skip this line (it's folded) + (recur (inc logical-idx) + visual-y ;; Don't advance visual-y + render-ops + line-mapping)))))))) + +#?(:cljs + (do + (defonce !window-debug-hooks-installed? (atom false)) + + (defn- install-window-debug-hooks! [] + (when-not @!window-debug-hooks-installed? + (reset! !window-debug-hooks-installed? true) + (.addEventListener js/window "error" + (fn [event] + (js/console.error "[CLIENT/ERROR]" + {:message (.-message event) + :filename (.-filename event) + :lineno (.-lineno event) + :colno (.-colno event) + :error (.-error event)}))) + (.addEventListener js/window "unhandledrejection" + (fn [event] + (js/console.error "[CLIENT/UNHANDLED-REJECTION]" (.-reason event)))) + (js/console.log "[CLIENT] Installed global window debug hooks"))) + + (defn- install-webgpu-debug-hooks! [^js device] + (when (and device (not (true? (.-__softlandDebugHooksInstalled device)))) + (set! (.-__softlandDebugHooksInstalled device) true) + (.addEventListener device "uncapturederror" + (fn [event] + (js/console.error "[WEBGPU/UNCAUGHT-ERROR]" (.-error event)))) + (-> (.-lost device) + (.then (fn [info] + (js/console.error "[WEBGPU/DEVICE-LOST]" + {:message (.-message info) + :reason (.-reason info)}))) + (.catch (fn [err] + (js/console.error "[WEBGPU/DEVICE-LOST-HOOK-FAILED]" err)))) + (js/console.log "[WEBGPU] Installed device debug hooks"))))) + +(e/defn LoadWebGPU [] + (e/client + (let [_ (install-window-debug-hooks!) + gpu js/navigator.gpu + _ (when-not gpu + (js/console.error "[BOOT] navigator.gpu unavailable")) + adapter (e/Task (await-promise (.requestAdapter ^js gpu))) + device (e/Task (await-promise (.requestDevice ^js adapter))) + initial-font-data (e/Task (await-promise (runtime-fonts/load-default-font-data-async)))] + (when (and adapter device initial-font-data) + (let [format (.getPreferredCanvasFormat ^js gpu) + adapter-limits (gpu-budget/snapshot-adapter-limits adapter) + tracker (gpu-budget/create-tracker adapter-limits)] + (install-webgpu-debug-hooks! device) + (js/console.log "[BOOT] WebGPU ready" + {:format format + :font-id (get-in initial-font-data [:font-config :id]) + :font-backend (get-in initial-font-data [:font-assets :backend]) + :adapter-limits adapter-limits}) + (merge initial-font-data + {:device device + :format format + :adapter-limits adapter-limits + :gpu-budget tracker})))))) + +(e/defn Prepare-Geometry [device pipelines render-ops font-assets font-config] (e/client - (binding [dom/node js/document.body - canvas (e/watch !canvas) - canvas-x (e/watch !canvas-x) - canvas-y (e/watch !canvas-y) - height (e/watch !height) - width (e/watch !width) - device (e/watch !device) - format (e/watch !format) - context (e/watch !context) - all-rects (e/watch !all-rects) - offset (e/watch !offset) - zoom-factor (e/watch !zoom-factor) - visible-rects (e/watch !visible-rects) - old-visible-rects (e/watch !old-visible-rects) - data-spine (i/spine) - rect-ids (vec (range 1 30)) - global-atom (e/watch !global-atom) - font-bitmap (e/watch !font-bitmap) - atlas-data (e/watch !atlas-data) - dpr (e/watch !dpr)] - - (reset! !dpr (.-devicePixelRatio js/window)) - (reset! !width (* dpr (.-clientWidth dom/node))) - (reset! !height (* dpr (.-clientHeight dom/node))) - (reset! !canvas-x 0) - (reset! !canvas-y 0) - (reset! !offset [0 0]) - (reset! !zoom-factor 1) - (load-bitmap-file) - (read-json-file) - (Canvas-view) - (when-not (some nil? [canvas height width]) - (let [rnd (create-random-rects rect-ids height width)] - ;(println "RND" @rnd) - (reset! !all-rects @rnd) - (println "all-rects" all-rects) - (when (and (some? font-bitmap) (some? all-rects)) - (do - (println "total rncts" all-rects) - (println "success canvas" canvas all-rects) - (Setup-webgpu) - (Add-panning) - (Add-wheel)))))))) + (let [font-defaults (:defaults font-config) + font-size (or (:fontSize font-defaults) 19) + char-width (or (:charWidth font-config) 0.56) + px-range (or (:pxRange font-defaults) 8) + sharpness (or (:sharpness font-defaults) 0.0) + line-height-factor (or (:lineHeight font-defaults) 1.2) + dpr (or (.-devicePixelRatio js/window) 1) + snap-step (/ 1 dpr) + snap (fn [v] (* (Math/round (/ v snap-step)) snap-step)) + line-h (snap (* font-size line-height-factor))] + (js/console.log "[BOOT] Prepare geometry" + {:font-id (:id font-config) + :font-backend (:backend font-assets) + :render-line-count (count render-ops) + :font-size font-size + :char-width char-width + :px-range px-range + :line-height line-h + :dpr dpr}) + {:text (editor/update-text-data device (:text-sys pipelines) render-ops font-assets font-size + :px-range px-range + :line-height line-h + :char-width char-width + :snap-step snap-step + :sharpness sharpness) + :rect (editor/update-rects device (:rect-sys pipelines) []) + ;; Shadow pools now own runtime shadow uploads; bootstrap only needs the pipeline state. + :shadow (:shadow-sys pipelines) + :pipelines pipelines}))) + +;; ============================================================================ +;; SIDEBAR CONSTANTS (used by imperative DOM in loop.cljs) +;; ============================================================================ + +;; Sidebar constants removed — sidebar now rendered via WebGPU rect tree in loop.cljs + +(e/defn main [ring-request] + (e/server + (let [file-content source-code + file-info initial-file-info] + + (e/client + (binding [dom/node js/document.body] + (dom/style {:margin "0" :padding "0" + :width "100vw" :height "100vh" + :overflow "hidden" :background "#111" + :user-select "none"}) + + (init-lezer-parser!) + (init-sci!) + + (let [resources (LoadWebGPU) + ;; Rama truth atoms — Electric subscriptions populate these + !sidebar-truth (atom nil) + !settings-truth (atom nil) + !agent-trail-truth (atom nil) + !flow-session-truth (atom nil) + !workspace-truth (atom nil)] + ;; Reactive sync: Rama PState → Electric → client atom. + ;; Re-runs whenever the server-side PState changes. + (reset! !sidebar-truth (fv/WatchSidebarTruth)) + (reset! !settings-truth (fv/WatchUserSettings)) + (reset! !agent-trail-truth (fv/WatchAgentTrail)) + (reset! !flow-session-truth (fv/WatchFlowSession)) + (reset! !workspace-truth (fv/WatchWorkspaceTruth)) + ;; Sidebar visible: default true, but respect persisted workspace truth. + ;; Must be initialized AFTER workspace truth loads so install-sidebar-watch! + ;; sees the correct initial value and doesn't auto-show a hidden sidebar. + (let [!sidebar-visible (atom (get @!workspace-truth :sidebar-visible true)) + !file-load-request (atom nil)] + (when resources + (let [device (get resources :device) + format (get resources :format) + font-manifest (get resources :font-manifest) + font-config (get resources :font-config) + font-assets (get resources :font-assets) + pipelines (editor/create-editor-state resources)] + (js/console.log "[BOOT] Client resources ready" + {:font-id (:id font-config) + :font-backend (:backend font-assets) + :font-manifest-count (count (:fonts font-manifest)) + :format format}) + + (let [lines (str/split-lines file-content) + tokenized-lines (mapv tokenize-line lines) + gutter-w 40 + layout-x (+ 50 gutter-w) + font-defaults (:defaults font-config) + font-size (or (:fontSize font-defaults) 19) + dpr (or (.-devicePixelRatio js/window) 1) + snap-step (/ 1 dpr) + snap (fn [v] (* (Math/round (/ v snap-step)) snap-step)) + char-width (or (:charWidth font-config) 0.56) + char-advance (snap (* font-size char-width)) + line-h (snap (* font-size (or (:lineHeight font-defaults) 1.2))) + layout-result (layout-tokens tokenized-lines layout-x 100 font-size [] #{} char-advance line-h) + render-ops (:render-ops layout-result) + line-lengths (mapv count lines)] + + (let [geometry (Prepare-Geometry device pipelines render-ops font-assets font-config)] + + ;; Extract preview overlay (hidden by default, shown when extract mode active) + (let [preview-el-atom (atom nil)] + (dom/div + (dom/props {:id "extract-preview-overlay" + :style {:position "fixed" + :top "0" :left "0" + :width "50vw" :height "100vh" + :display "none" + :overflow "auto" + :background "#111" + :border-right "1px solid #333" + :z-index "10" + :padding "16px" + :box-sizing "border-box"}}) + (reset! preview-el-atom dom/node)) + + ;; Full-width canvas (sidebar rendered on GPU, no DOM element needed) + (dom/canvas + (dom/props {:id "webgpu-canvas" + :style {:width "100vw" + :height "100vh" + :display "block"}}) + (let [ctx (.getContext dom/node "webgpu" (clj->js {:alpha true}))] + (let [raw-client-width (max 1 (.-clientWidth dom/node)) + raw-client-height (max 1 (.-clientHeight dom/node)) + window-width (max 1 (or (.-innerWidth js/window) raw-client-width)) + window-height (max 1 (or (.-innerHeight js/window) raw-client-height)) + client-width (max raw-client-width window-width) + client-height (max raw-client-height window-height) + dpr (or (.-devicePixelRatio js/window) 1) + backing-width (Math/floor (* client-width dpr)) + backing-height (Math/floor (* client-height dpr))] + (set! (.-width dom/node) backing-width) + (set! (.-height dom/node) backing-height) + (js/console.log "[BOOT] Primed canvas backing size" + (str "{\"clientWidth\":" raw-client-width + ",\"clientHeight\":" raw-client-height + ",\"effectiveWidth\":" client-width + ",\"effectiveHeight\":" client-height + ",\"dpr\":" dpr + ",\"backingWidth\":" backing-width + ",\"backingHeight\":" backing-height "}"))) + (js/console.log "[BOOT] Configuring WebGPU canvas" + (str "{\"clientWidth\":" (.-clientWidth dom/node) + ",\"clientHeight\":" (.-clientHeight dom/node) + ",\"effectiveWidth\":" (max (max 1 (.-clientWidth dom/node)) + (max 1 (or (.-innerWidth js/window) + (.-clientWidth dom/node)))) + ",\"effectiveHeight\":" (max (max 1 (.-clientHeight dom/node)) + (max 1 (or (.-innerHeight js/window) + (.-clientHeight dom/node)))) + ",\"devicePixelRatio\":" (or (.-devicePixelRatio js/window) 1) + ",\"canvasWidth\":" (.-width dom/node) + ",\"canvasHeight\":" (.-height dom/node) + ",\"format\":\"" format "\"" + ",\"copyDst\":true}")) + (.configure ^js ctx + (clj->js {:device device + :format format + :alphaMode "premultiplied"})) + (js/console.log "[BOOT] Starting runtime loop" + {:initial-file (:path file-info) + :font-id (:id font-config) + :font-backend (:backend font-assets)}) + (e/Task (loop/start-loop! dom/node device ctx geometry line-lengths + lines tokenize-line layout-tokens + find-matching-bracket detect-fold-regions + find-form-at-cursor sci-eval-form font-assets + :font-manifest font-manifest + :gpu-budget (:gpu-budget resources) + :!sidebar-visible !sidebar-visible + :!file-load-request !file-load-request + :!preview-el preview-el-atom + :!remote-sidebar-truth !sidebar-truth + :!remote-settings-truth !settings-truth + :!remote-agent-trail !agent-trail-truth + :!remote-flow-session !flow-session-truth + :!remote-workspace-truth !workspace-truth + :initial-file file-info)))))))))))))))) diff --git a/src/app/file_viewer.cljc b/src/app/file_viewer.cljc new file mode 100644 index 0000000..916857c --- /dev/null +++ b/src/app/file_viewer.cljc @@ -0,0 +1,133 @@ +(ns app.file-viewer + "File explorer sidebar — server-side file I/O and Electric bridge functions. + + Server: list-home-dirs, list-directory, read-file-content + Electric: HomeDirs, DirContents, FileContent (e/defn wrappers)" + (:require [hyperfiddle.electric3 :as e] + #?(:clj [clojure.java.io :as io]) + #?(:clj [clojure.string :as str]) + #?(:clj [app.server.rama.util-fns :as util-fns]))) + +;; ============================================================================ +;; SERVER SIDE — File I/O (JVM only) +;; ============================================================================ + +#?(:clj + (do + (def hidden-dir-prefixes + "Directories to filter out from home listing" + #{"." "snap" "lost+found"}) + + (def ignored-names + "Names to filter from directory listings" + #{"node_modules" "target" ".git" ".cpcache" ".shadow-cljs" + "__pycache__" ".clj-kondo" ".lsp" ".nrepl-port"}) + + (defn list-home-dirs [] + "List top-level directories in user's home folder. + Returns [{:name \"projects\" :path \"/home/sid/projects\"} ...]" + (let [home (io/file (System/getProperty "user.home")) + dirs (->> (.listFiles home) + (filter #(.isDirectory %)) + (remove #(some (fn [prefix] (.startsWith (.getName %) prefix)) + hidden-dir-prefixes)) + (sort-by #(.toLowerCase (.getName %))))] + (mapv (fn [f] {:name (.getName f) + :path (.getAbsolutePath f)}) + dirs))) + + (defn list-directory [path] + "List one level of a directory. Dirs first, then files, both alphabetical. + Returns [{:name :type :path} ...]" + (let [dir (io/file path)] + (if (and (.exists dir) (.isDirectory dir)) + (let [children (->> (.listFiles dir) + (remove #(or (.startsWith (.getName %) ".") + (contains? ignored-names (.getName %)))) + (sort-by #(.toLowerCase (.getName %)))) + dirs (filter #(.isDirectory %) children) + files (remove #(.isDirectory %) children)] + (vec (concat + (mapv (fn [f] {:name (.getName f) :type :dir :path (.getAbsolutePath f)}) dirs) + (mapv (fn [f] {:name (.getName f) :type :file :path (.getAbsolutePath f)}) files)))) + []))) + + (defn path-under-root? [path root-path] + "Security check: ensure path is under root-path (no directory traversal)" + (let [canonical-path (.getCanonicalPath (io/file path)) + canonical-root (.getCanonicalPath (io/file root-path))] + (.startsWith canonical-path canonical-root))) + + (defn read-file-content [path root-path] + "Read file content safely. Validates path is under root-path. + Returns {:content \"...\" :path path :name \"file.clj\"} or error map." + (let [f (io/file path)] + (cond + (not (path-under-root? path root-path)) + {:error "Access denied: path outside project root"} + + (not (.exists f)) + {:error (str "File not found: " path)} + + (.isDirectory f) + {:error "Cannot read directory as file"} + + (> (.length f) (* 1024 1024)) ;; 1MB limit + {:error (str "File too large: " (quot (.length f) 1024) "KB")} + + :else + (try + {:content (slurp f) + :path path + :name (.getName f)} + (catch Exception e + {:error (str "Read error: " (.getMessage e))}))))))) + +#?(:cljs + (do + (defn list-home-dirs [] []) + (defn list-directory [_] []) + (defn read-file-content [_ _] {:error "Server only"}))) + +;; ============================================================================ +;; ELECTRIC BRIDGE FUNCTIONS +;; ============================================================================ + +(e/defn HomeDirs [] + (e/server (list-home-dirs))) + +(e/defn DirContents [path] + (e/server (list-directory path))) + +(e/defn FileContent [path root] + (e/server (read-file-content path root))) + +(e/defn WatchSidebarTruth + "Reactive bridge: Rama sidebar truth → Electric client. + Watches the server-side mirror atom (updated by emit-sidebar-event! + after each Rama write). foreign-proxy-async is broken in Rama 1.6.0 + test IPC, so this uses e/watch on the atom instead." + [] + (e/server (e/watch util-fns/!sidebar-truth-atom))) + +(e/defn WatchUserSettings + "Reactive bridge: Rama user settings → Electric client. + Same pattern as WatchSidebarTruth — server atom mirror + e/watch." + [] + (e/server (e/watch util-fns/!settings-truth-atom))) + +(e/defn WatchAgentTrail + "Reactive bridge: latest completed agent trail → Electric client. + Returns {:run-id \"...\" :trail-data {...}} or nil." + [] + (e/server (e/watch util-fns/!agent-trail-atom))) + +(e/defn WatchFlowSession + "Reactive bridge: DG workflow FSM state → Electric client." + [] + (e/server (e/watch util-fns/!flow-session-atom))) + +(e/defn WatchWorkspaceTruth + "Reactive bridge: workspace truth (selected artifact, active pane, sidebar) → Electric client." + [] + (e/server (e/watch util-fns/!workspace-truth-atom))) diff --git a/src/app/server/rama/core.clj b/src/app/server/rama/core.clj index 1d4b000..865b2ab 100644 --- a/src/app/server/rama/core.clj +++ b/src/app/server/rama/core.clj @@ -1,87 +1,71 @@ (ns app.server.rama.core (:use [com.rpl.rama] [com.rpl.rama.path] - [com.rpl.rama.ops] - [com.rpl.rama.aggs]) - (:require [app.server.env :refer [oai-key roam-api-key roam-graph-name]] - [app.server.rama.objects :refer [http-post-future query-roam-req task-global-client roam-client]]) + [com.rpl.rama.ops]) (:import (clojure.lang Keyword) - [app.server.rama.objects CljHttpTaskGlobal roam-task-global] [com.rpl.rama.helpers ModuleUniqueIdPState])) +(defn toggle-set + "Toggle membership of `item` in set `s`. Rama DSL cannot inline + let/if, so this must be a plain function called from the topology." + [s item] + (let [s (or s #{})] + (if (contains? s item) + (disj s item) + (conj s item)))) - +(defn now-ms + "Wrapper for System/currentTimeMillis — Rama's dataflow DSL cannot + inline Java static method calls, so we wrap it in a plain function." + ^long [] + (System/currentTimeMillis)) (defmodule node-events-module [setup topologies] (declare-depot setup *node-events-depot :random) (declare-depot setup *user-registration-depot (hash-by :username)) (declare-depot setup *user-graph-settings-depot (hash-by :user-id)) - (declare-object setup *http-client (CljHttpTaskGlobal.)) - (declare-object setup *roam-client (roam-task-global. - roam-api-key - roam-graph-name)) (let [n (stream-topology topologies "events-topology") id-gen (ModuleUniqueIdPState. "$$id")] - (declare-pstate n $$nodes-pstate {Keyword (map-schema - Keyword (fixed-keys-schema - {:id Keyword - :x (fixed-keys-schema - {:pos Double - :time Long}) - :y (fixed-keys-schema - {:pos Double - :time Long}) - :type-specific-data (map-schema Keyword Object) - :type String - :fill String}))} - #_{:global? true}) - (declare-pstate n $$dg-node-ids-pstate {Keyword (vector-schema String)}) - (declare-pstate n $$dg-pages-pstate {Keyword (map-schema String Object {:subindex? true})}) - (declare-pstate n $$dg-nodes-pstate {Keyword (map-schema String Object {:subindex? true})}) - (declare-pstate n $$dg-edges-pstate {Keyword (map-schema Keyword (vector-schema Object) #_{:subindex? true})}) - (declare-pstate n $$components-pstate {Keyword (map-schema Keyword Object)}) - (declare-pstate n $$node-ids-pstate {Keyword (vector-schema Keyword)}) - (declare-pstate n $$node-ids-inview-pstate {Keyword (vector-schema Keyword)}) (declare-pstate n $$event-id-pstate Long {:global? true :initial-value 0}) - (declare-pstate n $$user-registration-pstate {String ; username + (declare-pstate n $$agent-runs-pstate {String (map-schema Keyword Object)}) + (declare-pstate n $$cli-sessions-pstate + {String + {Keyword + (fixed-keys-schema + {:session-id String + :last-active Long})}}) + + ;; Sidebar committed truth — project, expanded dirs, selected file + ;; Global PState: single workspace, keyed by field name + (declare-pstate n $$sidebar-pstate {Keyword (map-schema Keyword Object)}) + + ;; User settings — font-size, line-height, theme-id, etc. + (declare-pstate n $$settings-pstate {Keyword (map-schema Keyword Object)}) + + ;; Agent trails — completed run trail data, keyed by run-id + (declare-pstate n $$agent-trails-pstate {String (map-schema Keyword Object)}) + + ;; Flow session — DG workflow FSM state, keyed by namespace + (declare-pstate n $$flow-session-pstate {Keyword (map-schema Keyword Object)}) + + ;; Editor state — document content keyed by file path + (declare-pstate n $$editor-state-pstate {String (map-schema Keyword Object)}) + + ;; Workspace truth — selected artifact, active pane, sidebar visible + (declare-pstate n $$workspace-truth-pstate {Keyword (map-schema Keyword Object)}) + + (declare-pstate n $$user-registration-pstate {String (fixed-keys-schema {:user-id Long :uuid String})}) - (declare-pstate n $$user-graph-settings-pstate {Long ;user-id + (declare-pstate n $$user-graph-settings-pstate {Long (map-schema Keyword (fixed-keys-schema {:ui-mode Keyword :viewbox (vector-schema Long)}))}) (.declarePState id-gen n) - (< *in-view-ids] - ;(println " QUERY topology start") - (|hash *graph-name) - (local-select> *path $$nodes-pstate :> *all-nodes) - ;(println "selected all nodes" *all-nodes) - (explode-map *all-nodes :> *nuid *ndata) - ;(println "NODE: " *ndata) - (local-select> [:x :pos] *ndata :> *nx) - (local-select> [:y :pos] *ndata :> *ny) - (identity (and> (>= *nx *cx) - (< *nx (+ *cx *ch)) - (>= *ny *cy) - (< *ny (+ *cy *ch))) :> *t?) - - ;(println "local select" *nuid *nx *ny *t? *cx *cy *ch *cw) - (< *t?) - ;(println "TRUE") - (identity *nuid :> *x-uid)) - - (|origin) - (+vec-agg *x-uid :> *in-view-ids)) - - - - (< *user-graph-settings-depot :> {:keys [*user-id *graph-name *settings-data *event-data]}) @@ -115,160 +99,108 @@ ;; Source from node-events-depot (source> *node-events-depot :> {:keys [*action-type *node-data *event-data]}) (local-select> (keypath :graph-name) *event-data :> *graph-name) - (local-select> (keypath :uid) *node-data :> *uid) (|hash *graph-name) (println "R: PROCESSING EVENT" *action-type) (< (= :llm-request *action-type)) - ;; request data attached to event data - (local-select> (keypath :request-data) *event-data :> *request-data) - (println "R: GOT LLM REQUEST: " *request-data) - ;; Send the data to post function - ;; which takes the data and posts it open ai - ;; givens response all at once - ;; have to figure out how to do streaming - (completable-future> - (http-post-future (task-global-client *http-client) (first *node-data) *event-data) - :> *response-body) - ;; find whats the current value at the given data path - #_(println "R: RESPONSE-->" *response-body) - #_(first *node-data :> *data-path) - #_(local-select> [*graph-name *data-path] $$nodes-pstate :> *cur-val) - #_(println "R: CURRENT VALUE LLM WILL REPLACE: " *cur-val) - ;; Update the response at the given path - #_(local-transform> - [*graph-name - *data-path (termval *response-body)] - $$nodes-pstate) - #_(println "R: UPDATED VALUE" (local-select> *data-path $$nodes-pstate)) - - - ;; ========Query roam======== - (case> (= :roam-query *action-type)) - (completable-future> - (query-roam-req - (roam-client *roam-client) - "[:find (pull ?e [*]) - :in $ ?uid - :where [?e :node/title ?uid]]" - "Testing") - :> *query-result) - (println "R: ** QUERY RESULT **" *query-result) - - - ;; ======== Add roam node ======== - (case> (= :add-dg-page-data *action-type)) - (local-transform> - [*graph-name - *uid - (termval *node-data)] - $$dg-pages-pstate) - - - ;; ======== add dg nodes ======== - (case> (= :add-dg-nodes *action-type)) - (assoc *node-data - :width 80 - :height 4 - :> *updated-node) - (identity (keyword *uid) :> *kuid) - (identity {:id *kuid - :x {:pos (+ 0.0009 (rand-int 400)) - :time 0} - :y {:pos (+ 0.00009 (rand-int 400)) - :time 0} - :type-specific-data *updated-node - :type "rect" - :fill "lightblue"} - :> *d) - (println "NODE DATA: " *d #_{:uid *uid}) - - (local-transform> - [*graph-name - *uid - (termval *node-data)] - $$dg-nodes-pstate) - (local-transform> - [*graph-name - (keypath *kuid) - (termval *d)] - $$nodes-pstate) - (local-transform> - [(keypath *graph-name) - AFTER-ELEM - (termval *kuid)] - $$node-ids-pstate) - (local-transform> - [*graph-name - AFTER-ELEM - (termval *uid)] - $$dg-node-ids-pstate) - - ;; ========Add dg edges======== - (case> (= :add-dg-edges *action-type)) - (local-select> [(keypath :edges) FIRST] *node-data :> *edges) - (local-select> [FIRST (keypath :uid)] *edges :> *suid) - (identity (keyword *suid) :> *ksuid) - (println "Ksuid ::" *ksuid) - (identity {:to (last *edges) - :relation (second *edges)} - :> *edge-map) - (println "EDGE::::" *edges) - (local-transform> - [*graph-name - *ksuid - AFTER-ELEM - (termval *edge-map)] - $$dg-edges-pstate) - - #_(local-transform> - [*graph-name AFTER-ELEM (termval *edges)] - $$dg-edges-pstate) - - - - ;; ========Add nodes======== - (case> (= :new-node *action-type)) - (println "R: ADDING NODE" *node-data) - (local-transform> - [*graph-name - (keypath (ffirst *node-data)) - (termval (val (first *node-data)))] - $$nodes-pstate) - (local-transform> - [(keypath *graph-name) - AFTER-ELEM - (termval (ffirst *node-data))] - $$node-ids-pstate) - - ;; ========Delete nodes======== - #_#_#_(case> (= :delete-node *action-type)) - (local-transform> - [*graph-name (keypath (first *node-data)) NONE>] - $$nodes-pstate) - (local-transform> - [*graph-name - [ALL (= % (first *node-data))] NONE>] - $$node-ids-pstate) - - ;; ========Update nodes======== - (case> (= :update-node *action-type)) - (println "----------------------------------------------------") - (println "R: UPDATING NODE" *node-data) - (local-transform> - [*graph-name (first *node-data) (termval (second *node-data))] - $$nodes-pstate) - (println "R: NODE UPDATED") - ;(clojure.pprint/pprint (local-select> ALL $$nodes-pstate)) - (println "----------------------------------------------------") - ;; ========update event id======== (case> (= :update-event-id *action-type)) (local-select> [] $$event-id-pstate :> *event-id) (local-transform> [(termval (inc *event-id))] $$event-id-pstate) + ;; ========agent run======== + (case> (= :agent-run *action-type)) + (local-select> (keypath :request-data) *event-data :> *request-data) + (local-select> (keypath :run-id) *event-data :> *run-id) + (local-select> (keypath :provider) *request-data :> *provider) + (local-select> (keypath :prompt) *request-data :> *prompt) + (now-ms :> *start-ms) + (local-transform> [(keypath *run-id) :status (termval :running)] $$agent-runs-pstate) + (local-transform> [(keypath *run-id) :provider (termval *provider)] $$agent-runs-pstate) + (local-transform> [(keypath *run-id) :prompt (termval *prompt)] $$agent-runs-pstate) + (local-transform> [(keypath *run-id) :request-data (termval *request-data)] $$agent-runs-pstate) + (local-transform> [(keypath *run-id) :started-at (termval *start-ms)] $$agent-runs-pstate) + (local-transform> [(keypath *run-id) :updated-at (termval *start-ms)] $$agent-runs-pstate) + + ;; ========update cli session (stores session-id for --resume)======== + (case> (= :update-cli-session *action-type)) + (local-select> (keypath :file-path) *event-data :> *file-path) + (local-select> (keypath :provider) *event-data :> *provider) + (local-select> (keypath :session-id) *event-data :> *session-id) + (now-ms :> *now) + (local-transform> + [(keypath *file-path) (keypath *provider) + (multi-path + [:session-id (termval *session-id)] + [:last-active (termval *now)])] + $$cli-sessions-pstate) + + ;; ========sidebar: toggle dir expand/collapse======== + (case> (= :sidebar/dir-toggle *action-type)) + (local-select> (keypath :path) *node-data :> *dir-path) + (local-select> [*graph-name (keypath :expanded-dirs)] $$sidebar-pstate :> *dirs) + (identity (toggle-set *dirs *dir-path) :> *new-dirs) + (local-transform> [*graph-name (keypath :expanded-dirs) (termval *new-dirs)] $$sidebar-pstate) + (println "R: SIDEBAR dir-toggle" *dir-path) + + ;; ========sidebar: select file======== + (case> (= :sidebar/file-select *action-type)) + (local-select> (keypath :path) *node-data :> *file-path) + (local-select> (keypath :name) *node-data :> *file-name) + (local-transform> [*graph-name (keypath :selected-file) + (termval {:path *file-path :name *file-name})] + $$sidebar-pstate) + (println "R: SIDEBAR file-select" *file-path) + + ;; ========sidebar: select project (home dir)======== + (case> (= :sidebar/project-select *action-type)) + (local-select> (keypath :name) *node-data :> *proj-name) + (local-select> (keypath :path) *node-data :> *proj-path) + (local-transform> [*graph-name (keypath :project) + (termval {:name *proj-name :path *proj-path})] + $$sidebar-pstate) + (local-transform> [*graph-name (keypath :expanded-dirs) (termval #{})] $$sidebar-pstate) + (local-transform> [*graph-name (keypath :selected-file) (termval nil)] $$sidebar-pstate) + (println "R: SIDEBAR project-select" *proj-name) + + ;; ========sidebar: go back to home dirs======== + (case> (= :sidebar/project-back *action-type)) + (local-transform> [*graph-name (keypath :project) (termval nil)] $$sidebar-pstate) + (local-transform> [*graph-name (keypath :expanded-dirs) (termval #{})] $$sidebar-pstate) + (local-transform> [*graph-name (keypath :selected-file) (termval nil)] $$sidebar-pstate) + (println "R: SIDEBAR project-back") + + ;; ========settings: update (merge partial settings map)======== + (case> (= :settings/update *action-type)) + (explode-map *node-data :> *setting-key *setting-val) + (local-transform> [*graph-name (keypath *setting-key) (termval *setting-val)] $$settings-pstate) + (println "R: SETTINGS update" *setting-key *setting-val) + + ;; ========workspace: save truth======== + (case> (= :workspace/save-truth *action-type)) + (explode-map *node-data :> *wkey *wval) + (local-transform> [*graph-name (keypath *wkey) (termval *wval)] $$workspace-truth-pstate) + (println "R: WORKSPACE save-truth") + + ;; ========editor: save document state======== + (case> (= :editor/save-doc *action-type)) + (local-select> (keypath :file-path) *node-data :> *file-path) + (local-select> (keypath :doc-state) *node-data :> *doc-state) + (explode-map *doc-state :> *dkey *dval) + (local-transform> [(keypath *file-path) (keypath *dkey) (termval *dval)] $$editor-state-pstate) + + ;; ========flow session: save FSM state======== + (case> (= :flow/save-state *action-type)) + (explode-map *node-data :> *fkey *fval) + (local-transform> [*graph-name (keypath *fkey) (termval *fval)] $$flow-session-pstate) + (println "R: FLOW save-state") + + ;; ========agent trail: save completed run======== + (case> (= :agent-trail/save-run *action-type)) + (local-select> (keypath :run-id) *node-data :> *trail-run-id) + (local-select> (keypath :trail-data) *node-data :> *trail-data) + (explode-map *trail-data :> *tkey *tval) + (local-transform> [(keypath *trail-run-id) (keypath *tkey) (termval *tval)] $$agent-trails-pstate) + (println "R: AGENT-TRAIL save-run" *trail-run-id) (default>) (println "FALSE" *action-type))))) - diff --git a/src/app/server/rama/objects.cljc b/src/app/server/rama/objects.cljc index 9541449..4acb63b 100644 --- a/src/app/server/rama/objects.cljc +++ b/src/app/server/rama/objects.cljc @@ -1,100 +1,58 @@ (ns app.server.rama.objects - (:use [com.rpl.rama] - [com.rpl.rama.path]) - (:require [clj-http.client :as http] - [app.server.env :refer [oai-key roam-api-key roam-graph-name]] - [com.roamresearch.sdk.backend :as b] - [cheshire.core :as json]) - (:import (clojure.lang Keyword) - [hyperfiddle.electric Failure Pending] - [com.rpl.rama.integration TaskGlobalObject] - [java.util.concurrent CompletableFuture] - [java.util.function Supplier] - [com.rpl.rama.helpers ModuleUniqueIdPState])) - - - -(defprotocol FetchTaskGlobalClient - (task-global-client [this])) - -(deftype CljHttpTaskGlobal [] - TaskGlobalObject - (prepareForTask [this task-id task-global-context]) - (close [this]) - - FetchTaskGlobalClient - (task-global-client [this] - {:http-get http/get - :http-post http/post})) - -(defn http-get-future [client url] - (future - (try - (:body ((:http-get client) url)) - (catch Exception e - (str "GET Error: " (.getMessage e)))))) - -(declare update-node) - -(defn http-post-future [client path event-data] - (CompletableFuture/supplyAsync - (reify Supplier - (get [this] - (try - (let [{:keys [request-data graph-name event-id create-time]} event-data - {:keys - [url - model - messages - temperature - max-tokens]} request-data - body (json/generate-string - {:model model - :messages messages - :temperature temperature - :max_tokens max-tokens}) - headers {"Content-Type" "application/json" - "Authorization" (str "Bearer " oai-key)} - _ (println "R: POST REQUEST DATA ") - response ((:http-post client) url {:headers headers - :body body - :content-type :json - :as :json - :throw-exceptions false}) - llm-reply (-> response :body :choices first :message :content str)] - (println "GOT RESPONSE" response) - - (update-node [path llm-reply] {:graph-name graph-name - :event-id event-id - :create-time create-time} true false)) - - (catch Exception e - (str "POST Error: " (.getMessage e)))))))) - -(defn query-roam-req [client query &args] - (CompletableFuture/supplyAsync - (reify Supplier - (get [this] - (try - (do - (println "trying to query" client query) - (b/q client query &args)) - (catch Exception e - (str "ROAM QUERY POST ERROR: " (.getMessage e)))))))) - - -(defprotocol fetch-roam-client - (roam-client [this])) - - -;; Define a task global to manage the Roam client -(deftype roam-task-global [token graph] - TaskGlobalObject - (prepareForTask [this task-id task-global-context] - (println "Preparing Roam client for task" task-id)) - (close [this] - (println "Closing Roam client")) - - fetch-roam-client - (roam-client [this] {:token token - :graph graph})) + (:require [clojure.string :as str] + [cheshire.core :as json])) + +(defn provider-default-argv + "Build argv when client does not send raw argv. + Session support is best-effort per provider. + Optional :output-format overrides Claude's default (\"json\"). + When streaming, pass :output-format \"stream-json\" :include-partials? true. + Optional :allowed-tools is a seq of tool patterns (e.g. [\"mcp__linear-server__*\"]). + Optional :json-schema is a JSON string for --json-schema (structured output). + Optional :max-budget-usd caps API spend (only works with -p/--print). + Optional :model overrides the default model. + Optional :append-system-prompt appends to the system prompt." + [provider prompt session-id & {:keys [output-format include-partials? allowed-tools + json-schema max-budget-usd model append-system-prompt]}] + (case provider + :claude (vec (concat ["claude"] + (when (seq session-id) ["--resume" session-id]) + ["-p" (or prompt "") "--output-format" (or output-format "json")] + (when include-partials? ["--include-partial-messages"]) + (mapcat (fn [t] ["--allowedTools" t]) allowed-tools) + (when json-schema ["--json-schema" json-schema]) + (when max-budget-usd ["--max-budget-usd" (str max-budget-usd)]) + (when model ["--model" model]) + (when append-system-prompt ["--append-system-prompt" append-system-prompt]))) + :codex (vec ["codex" "exec" (or prompt "")]) + :gemini (vec (concat ["gemini"] + (when (seq session-id) ["--resume" session-id]) + ["-p" (or prompt "") "--output-format" "text"])) + (vec ["echo" (str "Unknown provider: " provider)]))) + +(defn parse-claude-json-output + "Parse Claude's JSON output to extract session-id and text content. + Handles two formats: + 1. Object: {\"type\":\"result\", \"session_id\":\"...\", \"result\":\"...\"} + 2. Array: [{\"type\":\"system\",\"session_id\":\"...\",...}, ..., {\"type\":\"message\",\"role\":\"assistant\",\"content\":[{\"type\":\"text\",\"text\":\"...\"}]}]" + [raw-output] + (try + (let [parsed (json/parse-string raw-output true)] + (if (vector? parsed) + ;; Array format: extract session-id from system init, content from last assistant message + (let [init-msg (first (filter #(= (:type %) "system") parsed)) + assistant-msgs (filter #(and (= (:type %) "message") + (= (:role %) "assistant")) + parsed) + last-assistant (last assistant-msgs) + text-content (->> (:content last-assistant) + (filter #(= (:type %) "text")) + (map :text) + (str/join "\n"))] + {:session-id (:session_id init-msg) + :content (if (seq text-content) text-content raw-output)}) + ;; Object format: direct extraction + {:session-id (:session_id parsed) + :content (or (:result parsed) raw-output)})) + (catch Exception _ + {:session-id nil :content raw-output}))) diff --git a/src/app/server/rama/util_fns.cljc b/src/app/server/rama/util_fns.cljc index a0ff7d0..8a1ac4b 100644 --- a/src/app/server/rama/util_fns.cljc +++ b/src/app/server/rama/util_fns.cljc @@ -1,18 +1,10 @@ (ns app.server.rama.util-fns (:use [com.rpl.rama] [com.rpl.rama.path]) - (:require [com.rpl.rama.test :as rtest :refer [create-ipc launch-module! gen-hashing-index-keys]] - [app.server.file :refer [save-event deserialize-and-execute dg-edges-file-edn-edn dg-nodes-file-edn softland-edn dg-nodes-edn dg-edges-edn load-events dg-page-data-edn]] + (:require [com.rpl.rama.test :as rtest :refer [create-ipc launch-module!]] + [app.server.file :refer [save-event softland-edn]] [missionary.core :as m] - [app.server.rama.roam-ns :refer [roam-readers]] - [app.server.rama.core :refer [node-events-module]]) - (:import (clojure.lang Keyword) - (missionary Cancelled) - [hyperfiddle.electric Failure Pending] - [com.rpl.rama.integration TaskGlobalObject] - [java.util.concurrent CompletableFuture] - [java.util.function Supplier] - [com.rpl.rama.helpers ModuleUniqueIdPState])) + [app.server.rama.core :refer [node-events-module]])) @@ -27,27 +19,33 @@ (def ipc - (let [c (create-ipc)] - (println "--R--: Start ipc, launch module") + (let [c (create-ipc) + module-name (get-module-name node-events-module) + launch-opts {:tasks 4 :threads 2}] + (println "--R--: Start ipc, launch module" {:module module-name + :launch-opts launch-opts}) (reset! !rama-ipc c) - (launch-module! c node-events-module {:tasks 4 :threads 2}))) + (let [result (launch-module! c node-events-module launch-opts)] + (println "--R--: Rama IPC ready" {:module module-name}) + result))) -;; Define clj defs +;; Foreign handles (def event-depot (foreign-depot @!rama-ipc (get-module-name node-events-module) "*node-events-depot")) -(def nodes-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$nodes-pstate")) -(def node-ids-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$node-ids-pstate")) -(def dg-node-ids-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$dg-node-ids-pstate")) -(def dg-pages-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$dg-pages-pstate")) -(def dg-nodes-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$dg-nodes-pstate")) -(def dg-edges-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$dg-edges-pstate")) (def event-id-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$event-id-pstate")) +(def agent-runs-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$agent-runs-pstate")) +(def cli-sessions-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$cli-sessions-pstate")) (def user-registration-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$user-registration-pstate")) (def user-registration-depot (foreign-depot @!rama-ipc (get-module-name node-events-module) "*user-registration-depot")) (def user-graph-settings-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$user-graph-settings-pstate")) (def user-graph-settings-depot (foreign-depot @!rama-ipc (get-module-name node-events-module) "*user-graph-settings-depot")) -(def get-in-view-nodes-query (foreign-query @!rama-ipc (get-module-name node-events-module) "get-in-view-nodeids")) +(def sidebar-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$sidebar-pstate")) +(def settings-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$settings-pstate")) +(def agent-trails-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$agent-trails-pstate")) +(def flow-session-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$flow-session-pstate")) +(def editor-state-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$editor-state-pstate")) +(def workspace-truth-pstate (foreign-pstate @!rama-ipc (get-module-name node-events-module) "$$workspace-truth-pstate")) (defn update-event-id [] @@ -57,9 +55,6 @@ {}) :append-ack)) -(defn get-event-id [] - (first (foreign-select [] event-id-pstate))) - (defn get-user-id [username] (first (foreign-select [username] user-registration-pstate))) @@ -97,20 +92,18 @@ (update-event-id)))))) -(defn proxy-callback [f] - (fn [new-val diff old-val] - ;(println "R: nodes-pstate callback" new-val "::::" diff "::::" old-val) - (f new-val))) +(defn proxy-callback [emit] + (fn [new-val _diff _old-val] + (emit new-val) + nil)) (defn !subscribe [path pstate] - (println "---R---: SUBSCRIBE") (->> (m/observe (fn [!] - (println "SUBSCRIBE") - ;; check https://clojurians.slack.com/archives/CL85MBPEF/p1698064128506939?thread_ts=1698062851.851949&cid=CL85MBPEF - (! (Failure. (Pending.))) - ;; using subselect because foreign-procxy takes exactly one path + ;; emit current value immediately, then stream updates + (! (first (foreign-select path pstate))) + ;; using subselect because foreign-proxy takes exactly one path (let [proxy (foreign-proxy-async path pstate {:callback-fn (proxy-callback !)})] #(.close @proxy)))) @@ -118,147 +111,224 @@ (m/relieve {}))) -(defn add-new-node - ([node-map event-data] - (add-new-node node-map event-data false false softland-edn)) - ([node-map event-data save?] - (add-new-node node-map event-data save? false softland-edn)) - ([node-map event-data save? update?] - (add-new-node node-map event-data save? update? softland-edn)) - ([node-map event-data save? update? file-name] - (do - (foreign-append! event-depot (->node-events - :new-node - node-map - event-data) - :append-ack) - (when save? - (save-event - "add-new-node" - [node-map event-data] - file-name)) - (when (or update? - (some? (:event-id event-data))) - (update-event-id))))) - -(defn add-dg-page-data [data] - (foreign-append! event-depot (->node-events - :add-dg-page-data - data - {:graph-name :main}) - :append-ack)) - -(defn add-dg-nodes [data] - (foreign-append! - event-depot - (->node-events - :add-dg-nodes - data - {:graph-name :main}) - :append-ack)) - - - -(defn add-dg-edges [data] - (println "DATA ** " data) - - (foreign-append! - event-depot - (->node-events - :add-dg-edges - data - {:graph-name :main}) - :append-ack)) - -(defn update-node - ([node-map event-data] - (update-node node-map event-data false false)) - ([node-map event-data save? update?] - (do - (foreign-append! event-depot (->node-events - :update-node - node-map - event-data) - :append-ack) - (when save? - (save-event "update-node" [node-map event-data] softland-edn)) - (when (or update? - (some? (:event-id event-data))) - (update-event-id))))) - -(defn send-llm-request - [node-map event-data] - (println "SEND LLM REQUEST: " event-data) - (foreign-append! event-depot (->node-events - :llm-request - node-map - event-data) - :append-ack)) - - -(defn roam-query-request - [node-map event-data] - (println "SEND roam query REQUEST: " event-data) - (foreign-append! event-depot (->node-events - :roam-query - node-map - event-data) - :append-ack)) - -(defn get-path-data [path pstate] - (println "FOREIGN SELECT") - (foreign-select path pstate)) - -(defn get-query-top [x y h w gn path] - (foreign-invoke-query get-in-view-nodes-query x y h w gn path)) - - -(defn save-dg [node] - (let [{:keys [uid title id]} node - node-map {(keyword uid) {:y {:pos (+ 0.00000201 (rand-int 400)) - :time 0} - :fill "lightblue", - :type "dg-node-rect", - :id (keyword uid) - :x {:pos (+ 0.00000201 (rand-int 400)) - :time 0} - :type-specific-data {:db/id id - :width 20 - :height 20 - :text title}}} - event-data {:graph-name :main - :event-id (get-event-id) - :create-time 0}] - (save-event - "add-new-node" - [node-map event-data] - dg-nodes-file-edn))) - - - -(clojure.pprint/pprint roam-readers) -;(load-events softland-edn deserialize-and-execute false) ;; THIS IS A HACK: Will not work when we move away from ipc. -(println "------ ADDING DG PAGES------") -#_(load-events dg-page-data-edn add-dg-page-data) ;; THIS IS A HACK: Will not work when we move away from ipc. -(println "------ ADDING DG NODES------") -(load-events dg-nodes-edn add-dg-nodes) ;; THIS IS A HACK: Will not work when we move away from ipc. -(println "------ ADDING DG EDGES------") -(load-events dg-edges-edn add-dg-edges) ;; THIS IS A HACK: Will not work when we move away from ipc. - - -#_(load-events dg-nodes-file-edn deserialize-and-execute) - -#_{:nodes {"[[ISS]] - According to literature, look into how much integrin expression do WTC-11 cells have." - {:uid "kPsqOOAaS" - :title "[[ISS]] - According to literature, look into how much integrin expression do WTC-11 cells have." - :type :ISS}, - "[[HYP]] - Depending on the ECM substrate, we can vary the dynamics and relative abundance of the type of endocytosis in cells." - {:uid "y384RFx3P" - :title "[[HYP]] - Depending on the ECM substrate, we can vary the dynamics and relative abundance of the type of endocytosis in cells." - :type :HYP}} - :edges ([{:uid "kPsqOOAaS" - :title "[[ISS]] - According to literature, look into how much integrin expression do WTC-11 cells have."} - "InformedBy" - {:uid "y384RFx3P" - :title "[[HYP]] - Depending on the ECM substrate, we can vary the dynamics and relative abundance of the type of endocytosis in cells."}])} - +(defn get-cli-session + "Look up the saved CLI session for a file+provider pair (for --resume)." + [file-path provider] + (first (foreign-select [(keypath file-path) (keypath provider)] cli-sessions-pstate))) + +(defn update-cli-session + "Store/update the CLI session-id for a file+provider pair in Rama. + Called after a successful agent run to persist the session-id for --resume." + [file-path provider session-id] + (when (and (seq file-path) provider (seq session-id)) + (foreign-append! event-depot + (->node-events :update-cli-session + {} + {:graph-name :main + :file-path file-path + :provider provider + :session-id session-id}) + :append-ack))) + +(defn submit-agent-run + "Append an :agent-run event and return the run-id. + Auto-injects session-id from previous sessions for --resume support." + [request-data] + (let [run-id (or (:run-id request-data) + (str (java.util.UUID/randomUUID))) + ;; Auto-inject session-id for --resume if not provided + file-path (:file request-data) + provider (:provider request-data) + session (when (and file-path provider) + (get-cli-session file-path provider)) + request-data (cond-> (assoc request-data :run-id run-id) + (and session (not (:session-id request-data))) + (assoc :session-id (:session-id session))) + now-ms (System/currentTimeMillis)] + (foreign-append! event-depot + (->node-events :agent-run + {} + {:graph-name :main + :run-id run-id + :request-data request-data + :create-time now-ms}) + :append-ack) + run-id)) + + +(defn get-agent-run + [run-id] + (first (foreign-select [(keypath run-id)] agent-runs-pstate))) + + +;; ── Sidebar Rama helpers ────────────────────────────────────────── + +(defn get-sidebar-state + "Read the current sidebar committed truth from Rama." + [] + {:project (first (foreign-select [:sidebar (keypath :project)] sidebar-pstate)) + :expanded-dirs (or (first (foreign-select [:sidebar (keypath :expanded-dirs)] sidebar-pstate)) #{}) + :selected-file (first (foreign-select [:sidebar (keypath :selected-file)] sidebar-pstate))}) + +;; Server-side atom — the reactive source for Electric e/watch. +;; Updated after each Rama write by emit-sidebar-event!. +;; Initialized from Rama PState at boot (picks up persisted state). +;; +;; foreign-proxy-async is broken in Rama 1.6.0 test IPC: +;; - Root path [] -> RocksDBWrapper serialization failure +;; - Per-key paths -> WorpResolveTimeout / connection manager collapse +;; The atom mirror approach bypasses proxy entirely and is proven stable. +(defonce !sidebar-truth-atom + (atom (try (get-sidebar-state) + (catch Exception _ {:project nil :expanded-dirs #{} :selected-file nil})))) + +(defn emit-sidebar-event! + "Submit a sidebar action to Rama. Updates the server-side truth atom + (which Electric watches via e/watch) and returns the new state." + [action-type data] + (foreign-append! event-depot + (->node-events action-type + data + {:graph-name :sidebar}) + :append-ack) + (let [state (get-sidebar-state)] + (reset! !sidebar-truth-atom state) + state)) + +;; ── Settings Rama helpers ──────────────────────────────────────── + +(defn get-settings-state + "Read the current user settings from Rama." + [] + (or (first (foreign-select [:settings] settings-pstate)) {})) + +;; Server-side atom — reactive source for Electric e/watch. +;; Same pattern as sidebar: atom mirror bypasses broken foreign-proxy-async. +(defonce !settings-truth-atom + (atom (try (get-settings-state) + (catch Exception _ {})))) + +(defn emit-settings-event! + "Submit a settings update to Rama. Merges partial settings map. + Updates the server-side truth atom and returns the new state." + [settings-data] + (foreign-append! event-depot + (->node-events :settings/update + settings-data + {:graph-name :settings}) + :append-ack) + (let [state (get-settings-state)] + (reset! !settings-truth-atom state) + state)) + +;; ── Agent Trail Rama helpers ───────────────────────────────────── + +(defn get-agent-trail + "Read a single completed trail from Rama by run-id." + [run-id] + (first (foreign-select [(keypath run-id)] agent-trails-pstate))) + +(defn get-latest-trail-run-id + "Read the :latest-run-id marker from agent trails. + Stored as a special key so we know which run to restore on reload." + [] + (first (foreign-select [(keypath "__latest") (keypath :run-id)] agent-trails-pstate))) + +;; Server-side atom — holds the most recently completed trail for Electric. +;; On boot, tries to restore the latest trail from Rama. +(defonce !agent-trail-atom + (atom (try + (when-let [run-id (get-latest-trail-run-id)] + (when-let [trail-data (get-agent-trail run-id)] + {:run-id run-id :trail-data trail-data})) + (catch Exception _ nil)))) + +(defn save-agent-trail! + "Persist a completed agent trail to Rama. Updates the latest-run marker + and the server-side atom for Electric." + [run-id trail-data] + ;; Save the trail data + (foreign-append! event-depot + (->node-events :agent-trail/save-run + {:run-id run-id :trail-data trail-data} + {:graph-name :trails}) + :append-ack) + ;; Update the latest-run marker + (foreign-append! event-depot + (->node-events :agent-trail/save-run + {:run-id "__latest" + :trail-data {:run-id run-id}} + {:graph-name :trails}) + :append-ack) + (let [result {:run-id run-id :trail-data trail-data}] + (reset! !agent-trail-atom result) + result)) + +;; ── Workspace Truth Rama helpers ───────────────────────────────── + +(defn get-workspace-truth + "Read workspace truth from Rama." + [] + (or (first (foreign-select [:workspace] workspace-truth-pstate)) {})) + +(defonce !workspace-truth-atom + (atom (try (get-workspace-truth) + (catch Exception _ {})))) + +(defn emit-workspace-truth-event! + "Persist workspace truth fields to Rama." + [truth-data] + (foreign-append! event-depot + (->node-events :workspace/save-truth + truth-data + {:graph-name :workspace}) + :append-ack) + (let [state (get-workspace-truth)] + (reset! !workspace-truth-atom state) + state)) + +;; ── Editor State Rama helpers ──────────────────────────────────── + +(defn get-editor-doc + "Read editor document state for a file path from Rama." + [file-path] + (first (foreign-select [(keypath file-path)] editor-state-pstate))) + +(defonce !editor-doc-atom (atom nil)) + +(defn save-editor-doc! + "Persist editor document state to Rama. Returns the round-trip time in ms." + [file-path doc-state] + (let [t0 (System/currentTimeMillis)] + (foreign-append! event-depot + (->node-events :editor/save-doc + {:file-path file-path :doc-state doc-state} + {:graph-name :editor}) + :append-ack) + (let [t1 (System/currentTimeMillis) + latency (- t1 t0)] + (reset! !editor-doc-atom {:file-path file-path :doc-state doc-state :latency-ms latency}) + {:ok true :latency-ms latency}))) + +;; ── Flow Session Rama helpers ──────────────────────────────────── + +(defn get-flow-session-state + "Read the current flow session from Rama." + [] + (or (first (foreign-select [:flow] flow-session-pstate)) {})) + +(defonce !flow-session-atom + (atom (try (get-flow-session-state) + (catch Exception _ {})))) + +(defn emit-flow-session-event! + "Persist flow session FSM state to Rama. Merges provided fields." + [flow-data] + (foreign-append! event-depot + (->node-events :flow/save-state + flow-data + {:graph-name :flow}) + :append-ack) + (let [state (get-flow-session-state)] + (reset! !flow-session-atom state) + state)) diff --git a/src/app/server/review_pack.clj b/src/app/server/review_pack.clj new file mode 100644 index 0000000..a0496a5 --- /dev/null +++ b/src/app/server/review_pack.clj @@ -0,0 +1,457 @@ +(ns app.server.review-pack + "Review Pack v0 domain logic. + + Purpose: + - bounded review artifact for faster reviewer confidence + - strict evidence requirements for core claims + - commit-pinned anchors + publish snapshot hash for integrity" + (:require + [clojure.string :as str] + [clojure.walk :as walk])) + +(def ^:private section-keys + [:intent :scope :change-summary :decisions :evidence :risks-unknowns]) + +(def ^:private section-budgets + {:intent 1400 + :scope 2200 + :change-summary 5000 + :decisions 50 + :evidence 200 + :risks-unknowns 60}) + +(def ^:private decision-statuses + #{:accepted :rejected :parked}) + +(def ^:private anchor-kinds + #{:code :test :command :context :link}) + +(defonce !review-packs + (atom {})) + +(defn- now-ms [] + (System/currentTimeMillis)) + +(defn- fresh-id [] + (str (java.util.UUID/randomUUID))) + +(defn- non-empty-string? + [x] + (and (string? x) (not (str/blank? x)))) + +(defn- trim-str + [x] + (some-> x str str/trim)) + +(defn- keep-non-empty + [x] + (let [s (trim-str x)] + (when (non-empty-string? s) s))) + +(defn- normalize-span + [span] + (let [line-start (long (max 1 (or (:line-start span) 1))) + line-end (long (max line-start (or (:line-end span) line-start))) + col-start (long (max 1 (or (:col-start span) 1))) + col-end (long (max col-start (or (:col-end span) col-start)))] + {:line-start line-start + :line-end line-end + :col-start col-start + :col-end col-end})) + +(defn- normalize-changed-files + [changed-files] + (->> changed-files + (keep (fn [f] + (let [path (keep-non-empty (:path f)) + rationale (keep-non-empty (:rationale f)) + risk-tag (let [r (:risk-tag f)] + (cond + (keyword? r) r + (string? r) (keyword (str/lower-case (str/trim r))) + :else :normal))] + (when path + {:path path + :rationale rationale + :risk-tag risk-tag})))) + vec)) + +(defn- normalize-decisions + [decisions] + (->> decisions + (keep (fn [d] + (let [status (cond + (keyword? (:status d)) (:status d) + (string? (:status d)) (keyword (str/lower-case (str/trim (:status d)))) + :else :parked) + rationale (keep-non-empty (:rationale d)) + summary (keep-non-empty (:summary d))] + (when (or rationale summary) + {:id (or (keep-non-empty (:id d)) (fresh-id)) + :status (if (contains? decision-statuses status) status :parked) + :summary (or summary rationale) + :rationale (or rationale summary "") + :timestamp (long (or (:timestamp d) (now-ms)))})))) + vec)) + +(defn- normalize-evidence + [anchors] + (->> anchors + (keep (fn [a] + (let [kind (cond + (keyword? (:kind a)) (:kind a) + (string? (:kind a)) (keyword (str/lower-case (str/trim (:kind a)))) + :else :code) + file-path (keep-non-empty (:file-path a)) + commit (keep-non-empty (:commit a)) + snippet (or (keep-non-empty (:snippet a)) "") + claim-id (keep-non-empty (:claim-id a)) + confidence (double (max 0.0 (min 1.0 (or (:confidence a) 0.5))))] + (when (or file-path (= kind :link)) + {:id (or (keep-non-empty (:id a)) (fresh-id)) + :kind (if (contains? anchor-kinds kind) kind :code) + :claim-id claim-id + :file-path file-path + :commit commit + :span (normalize-span (or (:span a) {})) + :snippet snippet + :confidence confidence})))) + vec)) + +(defn- normalize-risks-unknowns + [items] + (->> items + (keep (fn [r] + (let [text (keep-non-empty (:text r)) + kind (cond + (keyword? (:kind r)) (:kind r) + (string? (:kind r)) (keyword (str/lower-case (str/trim (:kind r)))) + :else :risk) + severity (cond + (keyword? (:severity r)) (:severity r) + (string? (:severity r)) (keyword (str/lower-case (str/trim (:severity r)))) + :else :medium)] + (when text + {:id (or (keep-non-empty (:id r)) (fresh-id)) + :kind (if (#{:risk :unknown} kind) kind :risk) + :severity (if (#{:low :medium :high} severity) severity :medium) + :text text})))) + vec)) + +(defn- normalize-claims + [claims] + (->> claims + (keep (fn [c] + (let [text (keep-non-empty (:text c)) + evidence-ids (->> (:evidence-ids c) + (map keep-non-empty) + (filter some?) + vec) + core? (true? (:core? c))] + (when text + {:id (or (keep-non-empty (:id c)) (fresh-id)) + :text text + :core? core? + :evidence-ids evidence-ids})))) + vec)) + +(defn- normalize-sections + [sections] + {:intent (or (keep-non-empty (:intent sections)) "") + :scope (or (keep-non-empty (:scope sections)) "") + :change-summary (or (keep-non-empty (:change-summary sections)) "") + :decisions (normalize-decisions (:decisions sections)) + :evidence (normalize-evidence (:evidence sections)) + :risks-unknowns (normalize-risks-unknowns (:risks-unknowns sections))}) + +(defn- count-or-size + [v] + (if (string? v) + (count v) + (count (or v [])))) + +(defn- validate-section-budgets + [sections] + (->> section-budgets + (keep (fn [[k max-size]] + (let [actual (count-or-size (get sections k))] + (when (> actual max-size) + {:type :budget-exceeded + :section k + :max max-size + :actual actual})))) + vec)) + +(defn- validate-required-sections + [sections] + (let [missing (->> section-keys + (keep (fn [k] + (let [v (get sections k)] + (when (or (nil? v) + (and (string? v) (str/blank? v))) + k)))) + vec)] + (if (seq missing) + [{:type :missing-sections :sections missing}] + []))) + +(defn- validate-changed-files + [changed-files] + (->> changed-files + (keep (fn [f] + (cond + (not (non-empty-string? (:path f))) + {:type :invalid-changed-file :message "missing file path"} + + (not (non-empty-string? (:rationale f))) + {:type :invalid-changed-file + :file (:path f) + :message "missing rationale"} + + :else nil))) + vec)) + +(defn- validate-evidence-anchors + [anchors] + (->> anchors + (keep (fn [a] + (cond + (and (not= (:kind a) :link) (not (non-empty-string? (:file-path a)))) + {:type :invalid-evidence + :anchor-id (:id a) + :message "missing file-path"} + + (not (non-empty-string? (:commit a))) + {:type :invalid-evidence + :anchor-id (:id a) + :message "missing commit (anchors must be commit-pinned)"} + + :else nil))) + vec)) + +(defn- validate-core-claims + [claims evidence] + (let [evidence-ids (set (map :id evidence))] + (->> claims + (keep (fn [claim] + (when (:core? claim) + (let [ids (vec (:evidence-ids claim)) + missing (vec (remove evidence-ids ids))] + (cond + (empty? ids) + {:type :core-claim-missing-evidence + :claim-id (:id claim) + :message "core claim has no evidence anchors"} + + (seq missing) + {:type :core-claim-invalid-evidence + :claim-id (:id claim) + :missing-anchor-ids missing} + + :else nil))))) + vec))) + +(defn- validate-pack + [pack] + (let [sections (:sections pack) + claims (:claims pack) + evidence (get-in pack [:sections :evidence])] + (vec (concat + (validate-required-sections sections) + (validate-section-budgets sections) + (validate-changed-files (:changed-files pack)) + (validate-evidence-anchors evidence) + (validate-core-claims claims evidence))))) + +(defn- canonicalize + "Recursively convert map keys to sorted maps for deterministic hashing." + [x] + (walk/postwalk + (fn [v] + (if (map? v) + (into (sorted-map) v) + v)) + x)) + +(defn- sha256-hex + [s] + (let [digest (java.security.MessageDigest/getInstance "SHA-256")] + (.update digest (.getBytes (str s) "UTF-8")) + (format "%064x" (BigInteger. 1 (.digest digest))))) + +(defn- snapshot-hash + [pack] + (-> pack + (dissoc :feedback :updated-at :published-at) + canonicalize + pr-str + sha256-hex)) + +(defn create-review-pack! + "Create Review Pack v0 draft. + Returns {:ok true :pack ...} or {:ok false :errors [...]}." + [request] + (let [pack-id (or (keep-non-empty (:pack-id request)) (fresh-id)) + now (now-ms) + sections (normalize-sections (or (:sections request) {})) + claims (normalize-claims (:claims request)) + changed-files (normalize-changed-files (:changed-files request)) + pack {:pack-id pack-id + :issue-ref (keep-non-empty (:issue-ref request)) + :pr-ref (keep-non-empty (:pr-ref request)) + :branch (keep-non-empty (:branch request)) + :author (or (keep-non-empty (:author request)) "unknown") + :status :draft + :version 1 + :created-at now + :updated-at now + :published-at nil + :snapshot-sha nil + :constraints {:evidence-bar :strict-core-claims + :section-budgets section-budgets} + :changed-files changed-files + :claims claims + :sections sections + :feedback []} + errors (validate-pack pack)] + (if (seq errors) + {:ok false :errors errors} + (do + (swap! !review-packs assoc pack-id pack) + {:ok true + :pack-id pack-id + :pack pack})))) + +(defn get-review-pack + [pack-id] + (get @!review-packs pack-id)) + +(defn list-review-packs + [] + (->> @!review-packs + vals + (sort-by (juxt :updated-at :created-at) #(compare %2 %1)) + vec)) + +(defn publish-review-pack! + [pack-id request] + (if-let [pack (get-review-pack pack-id)] + (if (= :published (:status pack)) + {:ok false + :error :already-published + :message "Review pack already published"} + (let [merged (-> pack + (assoc :pr-ref (or (keep-non-empty (:pr-ref request)) (:pr-ref pack))) + (assoc :published-by (or (keep-non-empty (:published-by request)) + (keep-non-empty (:author request)) + "unknown")) + (assoc :updated-at (now-ms))) + errors (validate-pack merged)] + (if (seq errors) + {:ok false :errors errors} + (let [published-at (now-ms) + snapshot (snapshot-hash merged) + final-pack (assoc merged + :status :published + :published-at published-at + :updated-at published-at + :snapshot-sha snapshot)] + (swap! !review-packs assoc pack-id final-pack) + {:ok true + :pack-id pack-id + :snapshot-sha snapshot + :pack final-pack})))) + {:ok false + :error :not-found + :message "Review pack not found"})) + +(defn add-feedback! + [pack-id request] + (if-let [pack (get-review-pack pack-id)] + (let [comment (keep-non-empty (:comment request))] + (if-not comment + {:ok false + :error :invalid-feedback + :message "Feedback comment is required"} + (let [feedback {:id (fresh-id) + :section (or (keep-non-empty (:section request)) "general") + :by (or (keep-non-empty (:by request)) "reviewer") + :role (or (keep-non-empty (:role request)) "reviewer") + :comment comment + :created-at (now-ms)} + updated (-> pack + (update :feedback (fnil conj []) feedback) + (assoc :updated-at (now-ms)))] + (swap! !review-packs assoc pack-id updated) + {:ok true + :feedback feedback + :feedback-count (count (:feedback updated)) + :pack-id pack-id}))) + {:ok false + :error :not-found + :message "Review pack not found"})) + +(defn- decision-counts + [decisions] + (reduce (fn [m d] (update m (:status d) (fnil inc 0))) + {:accepted 0 :rejected 0 :parked 0} + decisions)) + +(defn- risk-counts + [items] + (reduce (fn [m r] (update m (:kind r) (fnil inc 0))) + {:risk 0 :unknown 0} + items)) + +(defn review-pack-summary + [pack-id {:keys [base-url]}] + (if-let [pack (get-review-pack pack-id)] + (let [sections (:sections pack) + decisions (:decisions sections) + evidence (:evidence sections) + risks (:risks-unknowns sections) + claims (:claims pack) + core-claims (count (filter :core? claims)) + changed-file-count (count (:changed-files pack)) + dc (decision-counts decisions) + rc (risk-counts risks) + link-path (str "/review-pack/" (:pack-id pack)) + pack-url (if (non-empty-string? base-url) + (str (str/replace base-url #"/$" "") link-path) + link-path) + compact-intent (let [s (or (:intent sections) "")] + (if (> (count s) 280) (str (subs s 0 280) "...") s)) + compact-change (let [s (or (:change-summary sections) "")] + (if (> (count s) 500) (str (subs s 0 500) "...") s)) + summary {:pack-id (:pack-id pack) + :status (:status pack) + :version (:version pack) + :issue-ref (:issue-ref pack) + :pr-ref (:pr-ref pack) + :snapshot-sha (:snapshot-sha pack) + :intent compact-intent + :change-summary compact-change + :changed-file-count changed-file-count + :core-claim-count core-claims + :evidence-anchor-count (count evidence) + :decision-counts dc + :risk-counts rc + :updated-at (:updated-at pack) + :pack-url pack-url} + pr-template (str + "### Review Pack\n" + "- Pack ID: `" (:pack-id pack) "`\n" + (when (:snapshot-sha pack) + (str "- Snapshot: `" (:snapshot-sha pack) "`\n")) + "- Intent: " compact-intent "\n" + "- Changed files: " changed-file-count "\n" + "- Decisions: accepted " (:accepted dc) ", rejected " (:rejected dc) ", parked " (:parked dc) "\n" + "- Evidence anchors: " (count evidence) " (core claims: " core-claims ")\n" + "- Risks: " (:risk rc) ", Unknowns: " (:unknown rc) "\n" + "- Full artifact: " pack-url "\n")] + {:ok true + :summary summary + :pr-comment-template pr-template}) + {:ok false + :error :not-found + :message "Review pack not found"})) diff --git a/src/app/server_jetty.clj b/src/app/server_jetty.clj index c49f7eb..5fa2369 100644 --- a/src/app/server_jetty.clj +++ b/src/app/server_jetty.clj @@ -6,14 +6,28 @@ [clojure.string :as str] [clojure.tools.logging :as log] [contrib.assert :refer [check]] + [app.file-viewer :as fv] + [app.server.review-pack :as review-pack] + [components.adapter :as adapter] + [components.compiler :as compiler] + [components.token-matcher :as token-matcher] + [components.design-tokens :as design-tokens] + [app.server.rama.util-fns :as util-fns] + [app.server.rama.objects :as rama-objects] + [app.server.env :as env] + [clj-http.client :as http] + [cheshire.core :as json] [hyperfiddle.electric-ring-adapter3 :as electric-ring] [ring.adapter.jetty :as ring] [ring.middleware.content-type :refer [wrap-content-type]] [ring.middleware.cookies :as cookies] [ring.middleware.params :refer [wrap-params]] [ring.middleware.resource :refer [wrap-resource]] + [ring.core.protocols :as ring-protocols] [ring.util.response :as res]) (:import + (java.io BufferedReader InputStreamReader OutputStreamWriter) + (java.util.concurrent TimeUnit) (org.eclipse.jetty.server.handler.gzip GzipHandler) (org.eclipse.jetty.websocket.server.config JettyWebSocketServletContainerInitializer JettyWebSocketServletContainerInitializer$Configurator))) @@ -73,12 +87,990 @@ information." (-> (res/not-found "Not found") (res/content-type "text/plain"))) +(defn json-response [data] + (-> (res/response (pr-str data)) + (res/content-type "application/edn"))) + +(defn parse-edn-body + [ring-req] + (let [body-str (some-> ring-req :body slurp str/trim)] + (if (str/blank? body-str) + {} + (edn/read-string body-str)))) + +;; ===================================================================== +;; Linear API (direct GraphQL — no LLM, no MCP, no CLI) +;; ===================================================================== + +(def linear-teams-query + "{ teams { nodes { id name } } }") + +(def linear-viewer-query + "{ viewer { id } }") + +(def linear-issues-query + "query($teamId: ID!, $viewerId: ID!) { + issues( + first: 100, + filter: { + team: { id: { eq: $teamId } } + assignee: { id: { eq: $viewerId } } + state: { type: { nin: [\"completed\", \"canceled\"] } } + } + ) { + nodes { + identifier + title + priority + description + state { name } + assignee { name } + } + } + }") + +(defn linear-api-key + [] + ;; Prefer a runtime-injected env var so local/dev secrets can stay out of the repo. + (or (some-> (System/getenv "LINEAR_API_KEY") str/trim not-empty) + env/linear-api-key)) + +(def linear-team-aliases + {"DIS" "Discourse Graphs Team"}) + +(defn linear-post + [api-key query variables] + (http/post "https://api.linear.app/graphql" + {:headers {"Authorization" api-key + "Content-Type" "application/json"} + :body (json/generate-string + {:query query + :variables variables}) + :as :json + :throw-exceptions false + :socket-timeout 10000 + :connection-timeout 5000})) + +(defn linear-errors->message + [resp fallback] + (let [errors (get-in resp [:body :errors])] + (if (seq errors) + (str fallback ": " + (str/join "; " (map #(or (:message %) (pr-str %)) errors))) + fallback))) + +(defn resolve-linear-team-id + [api-key team-ref] + (let [resp (linear-post api-key linear-teams-query nil) + teams (get-in resp [:body :data :teams :nodes]) + wanted (or (get linear-team-aliases team-ref) team-ref) + wanted-lc (some-> wanted str/lower-case) + match (some (fn [team] + (let [id (:id team) + name (:name team)] + (when (or (= id wanted) + (= (some-> name str/lower-case) wanted-lc)) + team))) + teams)] + (cond + (= 200 (:status resp)) (:id match) + :else nil))) + +(defn resolve-linear-viewer-id + [api-key] + (let [resp (linear-post api-key linear-viewer-query nil)] + (when (= 200 (:status resp)) + (get-in resp [:body :data :viewer :id])))) + +(defn fetch-linear-issues + "Fetch issues for a Linear team directly via GraphQL API. + Returns {:ok true :tickets [...]} or {:ok false :error ...}." + [team-ref] + (let [api-key (linear-api-key)] + (if (or (str/blank? api-key) (= api-key "YOUR_KEY_HERE")) + {:ok false :error "Linear API key not configured. Set LINEAR_API_KEY in the server environment."} + (try + (if-let [team-id (resolve-linear-team-id api-key team-ref)] + (if-let [viewer-id (resolve-linear-viewer-id api-key)] + (let [resp (linear-post api-key linear-issues-query {:teamId team-id + :viewerId viewer-id}) + nodes (get-in resp [:body :data :issues :nodes])] + (if (= 200 (:status resp)) + {:ok true + :tickets (mapv (fn [n] + {:id (:identifier n) + :title (:title n) + :status (get-in n [:state :name] "unknown") + :assignee (get-in n [:assignee :name] "unassigned") + :priority (:priority n 0) + :description (or (:description n) "")}) + nodes)} + {:ok false + :error (linear-errors->message resp "Linear issue query failed") + :raw-body (:body resp)})) + {:ok false :error "Could not resolve authenticated Linear user"}) + {:ok false + :error (str "Linear team not found for '" team-ref "'")}) + (catch Exception e + {:ok false :error (.getMessage e)}))))) + +(defonce !agent-runs (atom {})) + +(def default-agent-timeout-ms 120000) +(def min-agent-timeout-ms 1000) +(def max-agent-timeout-ms 900000) + +(defn normalize-timeout-ms + [timeout-ms] + (let [parsed (cond + (number? timeout-ms) (long timeout-ms) + (string? timeout-ms) (try (Long/parseLong (str/trim timeout-ms)) + (catch Exception _ default-agent-timeout-ms)) + :else default-agent-timeout-ms)] + (-> parsed + (max min-agent-timeout-ms) + (min max-agent-timeout-ms)))) + +(defn preview-str + [x] + (let [s (str (or x ""))] + (if (> (count s) 180) + (str (subs s 0 180) "...") + s))) + +(defn summarize-agent-request + [request-data] + {:run-id (:run-id request-data) + :provider (:provider request-data) + :file (:file request-data) + :cwd (:cwd request-data) + :timeout-ms (:timeout-ms request-data) + :argv (:argv request-data) + :prompt (preview-str (:prompt request-data)) + :keys (sort (keys request-data))}) + +(defn start-output-reader + [proc] + (let [output-promise (promise) + reader-thread (doto + (Thread. + (fn [] + (deliver output-promise + (try + (slurp (.getInputStream proc)) + (catch Exception e + (str "Failed reading process output: " (.getMessage e))))))) + (.setName (str "softland-agent-output-" (System/currentTimeMillis))) + (.setDaemon true))] + (.start reader-thread) + output-promise)) + +(defn destroy-process-tree! + [proc] + (try + (doseq [child-handle (iterator-seq (.iterator (.descendants (.toHandle proc))))] + (try + (.destroyForcibly child-handle) + (catch Exception _ nil))) + (catch Exception _ nil)) + (try + (.destroy proc) + (catch Exception _ nil)) + (when-not (.waitFor proc 2000 TimeUnit/MILLISECONDS) + (try + (.destroyForcibly proc) + (catch Exception _ nil))) + nil) + +(defn run-cli-process + [argv cwd timeout-ms] + (let [cmd (vec (map str argv)) + pb (ProcessBuilder. (into-array String cmd))] + (when (seq cwd) + (.directory pb (io/file cwd))) + (.redirectErrorStream pb true) + (let [proc (.start pb) + _ (try + ;; Always close stdin to avoid CLIs waiting indefinitely for input. + (.close (.getOutputStream proc)) + (catch Exception _ nil)) + output-promise (start-output-reader proc) + finished? (.waitFor proc timeout-ms TimeUnit/MILLISECONDS)] + (if finished? + {:argv cmd + :cwd cwd + :output (deref output-promise 2000 "") + :exit-code (.exitValue proc) + :timed-out? false + :timeout-ms timeout-ms} + (do + (destroy-process-tree! proc) + (try + (.close (.getInputStream proc)) + (catch Exception _ nil)) + (let [partial-output (deref output-promise 1000 "") + timeout-msg (str "\n[Softland] Process timed out after " timeout-ms " ms and was terminated.")] + {:argv cmd + :cwd cwd + :output (str partial-output timeout-msg) + :exit-code 124 + :timed-out? true + :timeout-ms timeout-ms})))))) + +(defn run-agent-request + [request-data] + (let [run-id (or (:run-id request-data) (str (java.util.UUID/randomUUID))) + provider (:provider request-data) + prompt (:prompt request-data) + file-path (:file request-data) + ;; Look up stored session-id from Rama for --resume + stored-session (when (and file-path provider) + (try (util-fns/get-cli-session file-path provider) + (catch Exception _ nil))) + session-id (or (:session-id request-data) + (:session-id stored-session)) + argv (or (:argv request-data) + (rama-objects/provider-default-argv provider prompt session-id)) + cwd (:cwd request-data) + timeout-ms (normalize-timeout-ms (:timeout-ms request-data)) + start-ms (System/currentTimeMillis)] + (log/info "[AGENT][SERVER][RUN-START]" + {:run-id run-id + :provider provider + :file file-path + :cwd cwd + :timeout-ms timeout-ms + :argv argv + :session-id session-id + :prompt (preview-str prompt)}) + (swap! !agent-runs assoc run-id {:run-id run-id + :status :running + :provider provider + :prompt prompt}) + (let [result (try + (run-cli-process argv cwd timeout-ms) + (catch Exception e + {:argv (vec (or argv [])) + :cwd cwd + :output (str "CLI Error: " (.getMessage e)) + :exit-code 1 + :timed-out? false + :timeout-ms timeout-ms})) + end-ms (System/currentTimeMillis) + ;; For Claude: parse JSON output to extract session-id and clean content + parsed (when (= provider :claude) + (try (rama-objects/parse-claude-json-output (:output result)) + (catch Exception _ nil))) + clean-output (if parsed (:content parsed) (:output result)) + new-session-id (when parsed (:session-id parsed)) + final-status (cond + (:timed-out? result) :timeout + (zero? (:exit-code result)) :complete + :else :failed) + response {:run-id run-id + :status final-status + :provider provider + :prompt prompt + :result (assoc result + :output clean-output + :duration-ms (- end-ms start-ms)) + :started-at start-ms + :ended-at end-ms}] + ;; Store session-id in Rama for next --resume + (when (and new-session-id file-path (= final-status :complete)) + (try + (util-fns/update-cli-session file-path provider new-session-id) + (catch Exception e + (log/warn "[AGENT][SERVER][SESSION-STORE-FAILED]" + {:file file-path :provider provider + :error (.getMessage e)})))) + (swap! !agent-runs assoc run-id response) + (log/info "[AGENT][SERVER][RUN-END]" + {:run-id run-id + :status final-status + :duration-ms (:duration-ms (:result response)) + :exit-code (get-in response [:result :exit-code]) + :timed-out? (get-in response [:result :timed-out?]) + :timeout-ms timeout-ms + :session-id new-session-id}) + response))) + +;;; ── Streaming agent execution (SSE over POST) ────────────────────────────── + +(defn stream-cli-process + "Spawn a CLI process and read its stdout line-by-line. + Calls (on-line line-str) for each line, (on-done info-map) when the process + exits or times out. Runs reader + waiter on daemon threads." + [argv cwd timeout-ms on-line on-done] + (let [cmd (vec (map str argv)) + pb (ProcessBuilder. (into-array String cmd))] + (when (seq cwd) + (.directory pb (io/file cwd))) + (.redirectErrorStream pb true) + (let [proc (.start pb) + _ (try (.close (.getOutputStream proc)) (catch Exception _ nil)) + start-ms (System/currentTimeMillis) + reader (BufferedReader. (InputStreamReader. (.getInputStream proc))) + ;; Reader thread: emit lines until EOF + read-thread + (doto (Thread. + (fn [] + (try + (loop [] + (when-let [line (.readLine reader)] + (try (on-line line) (catch Exception _ nil)) + (recur))) + (catch Exception e + (log/debug "[STREAM] reader exception" (.getMessage e))) + (finally + (try (.close reader) (catch Exception _ nil)))))) + (.setName (str "stream-reader-" (System/currentTimeMillis))) + (.setDaemon true)) + ;; Waiter thread: wait for exit or timeout, then invoke on-done + wait-thread + (doto (Thread. + (fn [] + (let [finished? (.waitFor proc timeout-ms TimeUnit/MILLISECONDS) + end-ms (System/currentTimeMillis)] + (if finished? + (do (.join read-thread 2000) ;; let reader drain + (on-done {:exit-code (.exitValue proc) + :timed-out? false + :duration-ms (- end-ms start-ms)})) + (do (destroy-process-tree! proc) + (try (.close (.getInputStream proc)) (catch Exception _ nil)) + (.join read-thread 1000) + (on-done {:exit-code 124 + :timed-out? true + :duration-ms (- end-ms start-ms)})))))) + (.setName (str "stream-waiter-" (System/currentTimeMillis))) + (.setDaemon true))] + (.start read-thread) + (.start wait-thread) + proc))) + +(defn parse-stream-json-line + "Parse a single NDJSON line from `claude --output-format stream-json --include-partial-messages`. + Event types: + system → {:session_id ...} + stream_event → wraps Anthropic API events (content_block_delta, etc.) + assistant → full message (ignored — we already got deltas) + result → {:session_id ... :total_cost_usd ...}" + [line] + (let [mk-event (fn [kind payload] + (assoc payload + :kind kind + :event kind + :ts (System/currentTimeMillis)))] + (try + (let [obj (json/parse-string line true)] + (case (:type obj) + ;; system line carries session-id; model as :run-start envelope. + "system" + (mk-event :run-start {:session-id (:session_id obj)}) + + "stream_event" + (let [inner (:event obj)] + (case (:type inner) + "content_block_delta" + (let [delta (:delta inner) + idx (:index inner)] + (cond + (= (:type delta) "text_delta") + (mk-event :text-delta {:text (:text delta)}) + + (= (:type delta) "input_json_delta") + (mk-event :tool-input-delta + {:block-idx idx + :json-chunk (:partial_json delta)}) + + (= (:type delta) "thinking_delta") + (mk-event :thinking-delta {:text (:thinking delta)}) + + (= (:type delta) "signature_delta") + nil ;; verification signature — not needed for display + + :else nil)) + + "content_block_start" + (let [block (:content_block inner) + idx (:index inner)] + (cond + (= (:type block) "tool_use") + (mk-event :tool-use-start + {:block-idx idx + :tool-id (:id block) + :tool-name (:name block)}) + + (= (:type block) "tool_result") + (mk-event :tool-result + {:block-idx idx + :tool-id (:tool_use_id block) + :content (:content block)}) + + (= (:type block) "thinking") + (mk-event :thinking-start {:block-idx idx}) + + :else nil)) + + "content_block_stop" + (mk-event :block-stop {:block-idx (:index inner)}) + + ;; message_start, message_delta, message_stop — skip + nil)) + + ;; assistant — full message; fallback if --include-partial-messages wasn't passed + "assistant" + (let [text (->> (get-in obj [:message :content]) + (filter #(= (:type %) "text")) + (map :text) + (str/join "\n"))] + (when (seq text) + (mk-event :text-delta {:text text}))) + + "result" + (mk-event :run-done + {:status :complete + :session-id (:session_id obj) + :cost-usd (:total_cost_usd obj) + :result (:result obj)}) + + ;; Unknown types — skip + nil)) + (catch Exception e + (mk-event :run-error + {:error :invalid-json-line + :message (.getMessage e) + :raw (preview-str line)}))))) + +(defn initial-stream-state [] + {:tool-ids #{} + :tool-by-block {} + :terminal-kind nil + :saw-run-start? false}) + +(defn apply-stream-invariants + "Validate and enrich one parsed stream event. + Returns [next-state maybe-emit-event]." + [state evt] + (let [mk-error (fn [payload] + [(assoc state :terminal-kind :run-error) + (assoc payload + :kind :run-error + :event :run-error + :ts (System/currentTimeMillis))])] + (cond + (:terminal-kind state) + [state nil] + + (or (nil? (:kind evt)) (nil? (:ts evt))) + (mk-error {:error :invalid-envelope + :details (dissoc evt :ts)}) + + (= :run-start (:kind evt)) + [(assoc state :saw-run-start? true) evt] + + (= :tool-use-start (:kind evt)) + (let [tool-id (:tool-id evt) + block-idx (:block-idx evt)] + (if (or (nil? tool-id) (str/blank? (str tool-id))) + (mk-error {:error :tool-use-missing-id + :block-idx block-idx}) + [(-> state + (update :tool-ids conj tool-id) + (assoc-in [:tool-by-block block-idx] tool-id)) + evt])) + + (= :tool-input-delta (:kind evt)) + (let [tool-id (or (:tool-id evt) + (get-in state [:tool-by-block (:block-idx evt)]))] + (if (contains? (:tool-ids state) tool-id) + [state (assoc evt :tool-id tool-id)] + (mk-error {:error :unknown-tool-reference + :source-kind :tool-input-delta + :tool-id tool-id + :block-idx (:block-idx evt)}))) + + (= :tool-result (:kind evt)) + (let [tool-id (:tool-id evt)] + (if (contains? (:tool-ids state) tool-id) + [state evt] + (mk-error {:error :unknown-tool-reference + :source-kind :tool-result + :tool-id tool-id + :block-idx (:block-idx evt)}))) + + (= :run-done (:kind evt)) + [(assoc state :terminal-kind :run-done) evt] + + (= :run-error (:kind evt)) + [(assoc state :terminal-kind :run-error) evt] + + :else + [state evt]))) + +(defn parse-stream-json-lines + "Parse + validate a sequence of raw NDJSON lines, returning normalized events." + [lines] + (let [{:keys [events state]} + (reduce (fn [{:keys [events state]} line] + (if-let [evt (parse-stream-json-line line)] + (let [[state* emit] (apply-stream-invariants state evt)] + {:events (cond-> events emit (conj emit)) + :state state*}) + {:events events :state state})) + {:events [] :state (initial-stream-state)} + lines)] + (if (:terminal-kind state) + events + (conj events + {:kind :run-error + :event :run-error + :ts (System/currentTimeMillis) + :error :missing-terminal-event})))) + +(defn write-event! + "Write one SSE event as `data: {edn}\\n\\n` and flush." + [^java.io.Writer writer evt] + (.write writer (str "data: " (pr-str evt) "\n\n")) + (.flush writer)) + +(defn run-agent-stream + "Streaming variant of run-agent-request. Returns a Ring response with + Content-Type text/event-stream. Each SSE event is an EDN map." + [request-data] + (let [run-id (or (:run-id request-data) (str (java.util.UUID/randomUUID))) + provider (:provider request-data) + prompt (:prompt request-data) + file-path (:file request-data) + stored-ses (when (and file-path provider) + (try (util-fns/get-cli-session file-path provider) + (catch Exception _ nil))) + session-id (or (:session-id request-data) + (:session-id stored-ses)) + allowed-tools (:allowed-tools request-data) + json-schema (:json-schema request-data) + max-budget-usd (:max-budget-usd request-data) + model (:model request-data) + append-sys-prompt (:append-system-prompt request-data) + argv (or (:argv request-data) + (rama-objects/provider-default-argv + provider prompt session-id + :output-format (if (= provider :claude) "stream-json" nil) + :include-partials? (= provider :claude) + :allowed-tools allowed-tools + :json-schema json-schema + :max-budget-usd max-budget-usd + :model model + :append-system-prompt append-sys-prompt)) + cwd (:cwd request-data) + timeout-ms (normalize-timeout-ms (:timeout-ms request-data)) + ;; Capture session-id from stream events + !session-id (atom session-id)] + {:status 200 + :headers {"Content-Type" "text/event-stream" + "Cache-Control" "no-cache" + "X-Accel-Buffering" "no" + "Connection" "keep-alive"} + :body + (reify ring-protocols/StreamableResponseBody + (write-body-to-stream [_ _response output-stream] + (let [writer (OutputStreamWriter. output-stream "UTF-8")] + (try + (let [!stream-state (atom (initial-stream-state))] + ;; Non-Claude providers have no stream-json envelope, so emit run-start upfront. + (when (not= provider :claude) + (let [[state* evt] + (apply-stream-invariants @!stream-state + {:kind :run-start + :event :run-start + :ts (System/currentTimeMillis) + :run-id run-id + :provider provider + :prompt prompt + :argv argv + :session-id session-id})] + (reset! !stream-state state*) + (when evt + (write-event! writer evt)))) + (let [done-promise (promise)] + (stream-cli-process + argv cwd timeout-ms + ;; on-line callback + (fn [line] + (if (= provider :claude) + (when-let [evt0 (parse-stream-json-line line)] + (when-let [sid (:session-id evt0)] + (reset! !session-id sid)) + (let [evt1 (if (= :run-start (:kind evt0)) + (merge {:run-id run-id + :provider provider + :prompt prompt + :argv argv} + evt0) + evt0) + [state* evt] (apply-stream-invariants @!stream-state evt1)] + (reset! !stream-state state*) + (when evt + (write-event! writer evt)))) + (let [[state* evt] + (apply-stream-invariants @!stream-state + {:kind :text-delta + :event :text-delta + :ts (System/currentTimeMillis) + :text (str line "\n")})] + (reset! !stream-state state*) + (when evt + (write-event! writer evt))))) + ;; on-done callback + (fn [{:keys [exit-code timed-out? duration-ms]}] + (let [status (cond timed-out? :timeout + (zero? exit-code) :complete + :else :failed) + terminal? (:terminal-kind @!stream-state)] + ;; Emit exactly one terminal event. + (when-not terminal? + (let [terminal-evt (if (= status :complete) + {:kind :run-done + :event :run-done + :ts (System/currentTimeMillis) + :status status + :exit-code exit-code + :duration-ms duration-ms} + {:kind :run-error + :event :run-error + :ts (System/currentTimeMillis) + :status status + :exit-code exit-code + :duration-ms duration-ms + :error (if timed-out? + :timeout + :process-failed)}) + [state* emit] (apply-stream-invariants @!stream-state terminal-evt)] + (reset! !stream-state state*) + (when emit + (write-event! writer emit)))) + ;; Persist session-id to Rama + (let [final-sid @!session-id] + (when (and final-sid file-path (= status :complete)) + (try + (util-fns/update-cli-session file-path provider final-sid) + (catch Exception e + (log/warn "[AGENT][STREAM][SESSION-STORE-FAILED]" + {:error (.getMessage e)}))))) + (deliver done-promise true)))) + ;; Block this thread until process completes — keeps stream open + @done-promise)) + (catch Exception e + (println "[STREAM][ERROR]" (.getMessage e))) + (finally + (try (.flush writer) (catch Exception _ nil)) + (try (.close writer) (catch Exception _ nil)))))))})) + +(defn wrap-file-api + "Handle /api/* routes for file explorer sidebar. + Returns EDN responses consumable by ClojureScript client." + [next-handler] + (fn [{:keys [uri query-params request-method] :as ring-req}] + (cond + ;; ===== Review Pack API ===== + (= uri "/api/review-pack/create") + (if (= request-method :post) + (try + (json-response (review-pack/create-review-pack! (parse-edn-body ring-req))) + (catch Exception e + (log/error e "[REVIEW-PACK][CREATE][ERROR]" {:uri uri}) + (json-response {:ok false + :error :server-error + :message (str "Failed creating review pack: " (.getMessage e))}))) + (json-response {:ok false :error :method-not-allowed :message "Method not allowed. Use POST."})) + + (= uri "/api/review-pack/list") + (if (= request-method :get) + (json-response {:ok true :review-packs (review-pack/list-review-packs)}) + (json-response {:ok false :error :method-not-allowed :message "Method not allowed. Use GET."})) + + (re-matches #"/api/review-pack/([^/]+)/publish" uri) + (let [[_ pack-id] (re-matches #"/api/review-pack/([^/]+)/publish" uri)] + (if (= request-method :post) + (try + (json-response (review-pack/publish-review-pack! pack-id (parse-edn-body ring-req))) + (catch Exception e + (log/error e "[REVIEW-PACK][PUBLISH][ERROR]" {:pack-id pack-id}) + (json-response {:ok false + :error :server-error + :message (str "Failed publishing review pack: " (.getMessage e))}))) + (json-response {:ok false :error :method-not-allowed :message "Method not allowed. Use POST."}))) + + (re-matches #"/api/review-pack/([^/]+)/summary" uri) + (let [[_ pack-id] (re-matches #"/api/review-pack/([^/]+)/summary" uri) + base-url (or (get query-params "base-url") "")] + (if (= request-method :get) + (json-response (review-pack/review-pack-summary pack-id {:base-url base-url})) + (json-response {:ok false :error :method-not-allowed :message "Method not allowed. Use GET."}))) + + (re-matches #"/api/review-pack/([^/]+)/feedback" uri) + (let [[_ pack-id] (re-matches #"/api/review-pack/([^/]+)/feedback" uri)] + (if (= request-method :post) + (try + (json-response (review-pack/add-feedback! pack-id (parse-edn-body ring-req))) + (catch Exception e + (log/error e "[REVIEW-PACK][FEEDBACK][ERROR]" {:pack-id pack-id}) + (json-response {:ok false + :error :server-error + :message (str "Failed adding feedback: " (.getMessage e))}))) + (json-response {:ok false :error :method-not-allowed :message "Method not allowed. Use POST."}))) + + (re-matches #"/api/review-pack/([^/]+)" uri) + (let [[_ pack-id] (re-matches #"/api/review-pack/([^/]+)" uri)] + (if (= request-method :get) + (if-let [pack (review-pack/get-review-pack pack-id)] + (json-response {:ok true :pack pack}) + (json-response {:ok false :error :not-found :message "Review pack not found"})) + (json-response {:ok false :error :method-not-allowed :message "Method not allowed. Use GET."}))) + + ;; ===== Component Library Registry ===== + (= uri "/api/components/registry") + (try + (let [base-dir (io/file "components") + registry-file (io/file base-dir "_registry.edn") + registry (when (.exists registry-file) + (edn/read-string (slurp registry-file))) + ;; Read _source.edn for each component to get current status + components (mapv (fn [{:keys [slug] :as comp}] + (let [source-file (io/file base-dir slug "_source.edn") + source (when (.exists source-file) + (edn/read-string (slurp source-file))) + ;; Check if .cljc file exists for shadcn + cljc-file (io/file base-dir slug "shadcn.cljc") + has-cljc? (.exists cljc-file)] + (assoc comp + :source source + :has-cljc? has-cljc?))) + (:components registry))] + (json-response {:ok true + :libraries (:libraries registry) + :components components})) + (catch Exception e + (log/error e "[COMPONENTS] registry read failed") + (json-response {:ok false :error (.getMessage e)}))) + + ;; Update component status (after conversion) + (= uri "/api/components/status") + (if (= request-method :post) + (try + (let [body (parse-edn-body ring-req) + slug (:slug body) + library (or (:library body) :shadcn-v4) + status (or (:status body) :ready) + source-file (io/file "components" slug "_source.edn")] + (if (.exists source-file) + (let [source (edn/read-string (slurp source-file)) + updated (-> source + (assoc-in [library :status] status) + (assoc-in [library :converted] (str (java.time.LocalDate/now))))] + (spit source-file (pr-str updated)) + (json-response {:ok true :slug slug :status status})) + (json-response {:ok false :error "Component not found"}))) + (catch Exception e + (log/error e "[COMPONENTS] status update failed") + (json-response {:ok false :error (.getMessage e)}))) + (json-response {:ok false :error "POST required"})) + + ;; ===== Linear API (direct, no LLM) ===== + (= uri "/api/linear/issues") + (let [team-key (or (get query-params "team") "DIS")] + (json-response (fetch-linear-issues team-key))) + + ;; ===== Design Converter API ===== + (= uri "/api/extract/compile") + (if (= request-method :post) + (try + (let [request-data (parse-edn-body ring-req) + extracted-tree (:tree request-data) + source-url (or (:source-url request-data) "") + dt design-tokens/dt + ;; Step 1: extracted JSON → Design IR + ir (adapter/extracted->ir extracted-tree {:source-url source-url}) + ;; Step 2: tokenize (snap to dt) + tokenized-ir (token-matcher/tokenize-ir ir dt) + ;; Step 3: compile to rt-node + rt-node (compiler/compile-ir tokenized-ir dt {:use-tokens? true})] + (json-response {:ok true + :ir tokenized-ir + :rt-node rt-node + :source-url source-url})) + (catch Exception e + (log/error e "[EXTRACT] compile failed") + (json-response {:ok false :error (.getMessage e)}))) + (json-response {:ok false :error "POST required"})) + + ;; ===== Sidebar Rama Actions ===== + (= uri "/api/sidebar/action") + (if (= request-method :post) + (try + (let [body (parse-edn-body ring-req) + action-type (:action-type body) + data (or (:data body) {})] + (json-response (util-fns/emit-sidebar-event! action-type data))) + (catch Exception e + (log/error e "[SIDEBAR][ACTION][ERROR]") + (json-response {:error (str "Sidebar action failed: " (.getMessage e))}))) + (json-response {:error "Method not allowed. Use POST."})) + + (= uri "/api/sidebar/state") + (json-response (util-fns/get-sidebar-state)) + + ;; ===== Settings Rama Actions ===== + (= uri "/api/settings/update") + (if (= request-method :post) + (try + (let [body (parse-edn-body ring-req) + settings-data (or (:data body) {})] + (json-response (util-fns/emit-settings-event! settings-data))) + (catch Exception e + (log/error e "[SETTINGS][UPDATE][ERROR]") + (json-response {:error (str "Settings update failed: " (.getMessage e))}))) + (json-response {:error "Method not allowed. Use POST."})) + + (= uri "/api/settings/state") + (json-response (util-fns/get-settings-state)) + + ;; ===== Agent Trail Persistence ===== + (= uri "/api/agent/trail/save") + (if (= request-method :post) + (try + (let [body (parse-edn-body ring-req) + run-id (:run-id body) + trail-data (:trail-data body)] + (if (and run-id trail-data) + (json-response (util-fns/save-agent-trail! run-id trail-data)) + (json-response {:error "Missing run-id or trail-data"}))) + (catch Exception e + (log/error e "[AGENT-TRAIL][SAVE][ERROR]") + (json-response {:error (str "Trail save failed: " (.getMessage e))}))) + (json-response {:error "Method not allowed. Use POST."})) + + ;; ===== Workspace Truth Persistence (Phase 7) ===== + (= uri "/api/workspace/save-truth") + (if (= request-method :post) + (try + (let [body (parse-edn-body ring-req) + truth-data (or (:data body) {})] + (json-response (util-fns/emit-workspace-truth-event! truth-data))) + (catch Exception e + (log/error e "[WORKSPACE][SAVE][ERROR]") + (json-response {:error (str "Workspace save failed: " (.getMessage e))}))) + (json-response {:error "Method not allowed. Use POST."})) + + ;; ===== Editor State Persistence (Phase 4B measurement) ===== + (= uri "/api/editor/save-doc") + (if (= request-method :post) + (try + (let [body (parse-edn-body ring-req) + file-path (:file-path body) + doc-state (:doc-state body)] + (if (and file-path doc-state) + (json-response (util-fns/save-editor-doc! file-path doc-state)) + (json-response {:error "Missing file-path or doc-state"}))) + (catch Exception e + (log/error e "[EDITOR][SAVE][ERROR]") + (json-response {:error (str "Editor save failed: " (.getMessage e))}))) + (json-response {:error "Method not allowed. Use POST."})) + + ;; ===== Flow Session Persistence ===== + (= uri "/api/flow/save-state") + (if (= request-method :post) + (try + (let [body (parse-edn-body ring-req) + flow-data (or (:data body) {})] + (json-response (util-fns/emit-flow-session-event! flow-data))) + (catch Exception e + (log/error e "[FLOW][SAVE][ERROR]") + (json-response {:error (str "Flow save failed: " (.getMessage e))}))) + (json-response {:error "Method not allowed. Use POST."})) + + ;; ===== Existing File/Agent API ===== + (= uri "/api/home-dirs") + (json-response (fv/list-home-dirs)) + + (= uri "/api/list-dir") + (let [path (get query-params "path")] + (if path + (json-response (fv/list-directory path)) + (json-response {:error "Missing path parameter"}))) + + (= uri "/api/read-file") + (let [path (get query-params "path") + root (get query-params "root")] + (if (and path root) + (json-response (fv/read-file-content path root)) + (json-response {:error "Missing path or root parameter"}))) + + (= uri "/api/agent/run") + (if (= request-method :post) + (try + (let [request-data (parse-edn-body ring-req)] + (log/info "[AGENT][SERVER][HTTP-IN]" + {:remote-addr (:remote-addr ring-req) + :method request-method + :uri uri + :request (summarize-agent-request request-data)}) + (json-response (run-agent-request request-data))) + (catch Exception e + (log/error e "[AGENT][SERVER][HTTP-ERROR]" + {:remote-addr (:remote-addr ring-req) + :method request-method + :uri uri}) + (json-response {:error (str "Failed: " (.getMessage e))}))) + (json-response {:error "Method not allowed. Use POST."})) + + (= uri "/api/agent/stream") + (if (= request-method :post) + (try + (let [request-data (parse-edn-body ring-req)] + (log/info "[AGENT][STREAM][HTTP-IN]" + {:remote-addr (:remote-addr ring-req) + :request (summarize-agent-request request-data)}) + (run-agent-stream request-data)) + (catch Exception e + (log/error e "[AGENT][STREAM][HTTP-ERROR]" + {:remote-addr (:remote-addr ring-req) :uri uri}) + (json-response {:error (str "Stream failed: " (.getMessage e))}))) + (json-response {:error "Method not allowed. Use POST."})) + + (= uri "/api/agent/run-status") + (let [run-id (get query-params "run-id")] + (if (str/blank? run-id) + (json-response {:error "Missing run-id parameter"}) + (if-let [run (get @!agent-runs run-id)] + (json-response run) + (json-response {:status :not-found :run-id run-id})))) + + (= uri "/api/dev/replay-fixture") + (try + (let [file (io/file "test/fixtures/claude-stream-sample.jsonl")] + (if (.exists file) + (let [lines (with-open [rdr (io/reader file)] + (doall (line-seq rdr))) + events (parse-stream-json-lines lines)] + (json-response {:ok true :events events})) + (json-response {:ok false :error "Fixture not found"}))) + (catch Exception e + (json-response {:ok false :error (.getMessage e)}))) + + :else + ;; Not an API route — pass through + (next-handler ring-req)))) + (defn http-middleware [config] ;; these compose as functions, so are applied bottom up (-> not-found-handler - (wrap-index-page config) ; 3. otherwise fallback to default page file - (wrap-resource (:resources-path config)) ; 2. serve static file from classpath - (wrap-content-type))) ; 1. detect content (e.g. for index.html) + (wrap-index-page config) ; 5. otherwise fallback to default page file + (wrap-resource (:resources-path config)) ; 4. serve static file from classpath + (wrap-content-type) ; 3. detect content (e.g. for index.html) + (wrap-file-api) ; 2. intercept /api/* routes for file explorer + (wrap-params))) ; 1. parse query params for API routes (defn middleware [config entrypoint] @@ -117,4 +1109,4 @@ information." (add-gzip-handler! server))} config))] (log/info "👉" (str "http://" host ":" (-> server (.getConnectors) first (.getPort)))) - server)) \ No newline at end of file + server)) diff --git a/src/components/adapter.cljc b/src/components/adapter.cljc new file mode 100644 index 0000000..260dbd6 --- /dev/null +++ b/src/components/adapter.cljc @@ -0,0 +1,199 @@ +(ns components.adapter + "Bridge between _extractor.js JSON output and Design IR format. + + Extractor output (JS-style camelCase): + {:tag \"div\" :bounds {:x :y :w :h} + :styles {:backgroundColor \"rgb(...)\" :borderTopWidth \"1px\" ...} + :textContent \"Hello\" :children [...]} + + Design IR (what the compiler expects): + {:tag \"div\" :role :container :bounds {:w :h} + :visual {:fill {:type :solid :value \"rgb(...)\"} :radius {:uniform 8} ...} + :typography {:content \"Hello\" :size 14 :color \"rgb(...)\" ...} + :layout {:display :flex :direction :row :gap 8 :padding [8 8 8 8]} + :children [...]}" + (:require [clojure.string :as str] + [components.css-parsers :as css])) + +;; --------------------------------------------------------------------------- +;; Role inference +;; --------------------------------------------------------------------------- + +(defn- infer-role + "Infer IR :role from tag name and context." + [tag has-text? has-children?] + (let [tag (when tag (str/lower-case tag))] + (cond + (#{"button" "a" "input" "select" "textarea"} tag) :interactive + (#{"img" "svg" "hr" "canvas"} tag) :decorative + has-text? :text + :else :container))) + +;; --------------------------------------------------------------------------- +;; Style helpers +;; --------------------------------------------------------------------------- + +(defn- transparent? [color-str] + (or (nil? color-str) + (= color-str "rgba(0, 0, 0, 0)") + (= color-str "transparent") + (str/blank? color-str))) + +(defn- zero-px? [s] + (or (nil? s) (= s "0px") (= s "0") (str/blank? s))) + +(defn- all-zero? [& strs] + (every? zero-px? strs)) + +;; --------------------------------------------------------------------------- +;; Core adapter +;; --------------------------------------------------------------------------- + +(defn extracted->ir + "Convert a single extracted node (from _extractor.js JSON) to Design IR. + Recursively converts children. + + opts: {:source-url string ;; for :source-meta + :library string} ;; e.g. \"shadcn\"" + ([node] (extracted->ir node {})) + ([node opts] + (when node + (let [styles (or (:styles node) {}) + tag (:tag node) + text (:textContent node) + children (:children node) + bounds (:bounds node) + + ;; --- Visual --- + bg (:backgroundColor styles) + fill (when-not (transparent? bg) + {:type :solid :value bg}) + + ;; Border radius + br-tl (css/parse-px (:borderTopLeftRadius styles)) + br-tr (css/parse-px (:borderTopRightRadius styles)) + br-br (css/parse-px (:borderBottomRightRadius styles)) + br-bl (css/parse-px (:borderBottomLeftRadius styles)) + radius (when (and br-tl br-tr br-br br-bl + (not (every? zero? [br-tl br-tr br-br br-bl]))) + (if (= br-tl br-tr br-br br-bl) + {:uniform br-tl} + {:corners [br-tl br-tr br-br br-bl]})) + + ;; Border + bw-t (css/parse-px (:borderTopWidth styles)) + bw-r (css/parse-px (:borderRightWidth styles)) + bw-b (css/parse-px (:borderBottomWidth styles)) + bw-l (css/parse-px (:borderLeftWidth styles)) + has-border? (and bw-t bw-r bw-b bw-l + (not (every? zero? [bw-t bw-r bw-b bw-l]))) + border-color (when has-border? + (css/parse-border-color + (:borderTopColor styles) + (:borderRightColor styles) + (:borderBottomColor styles) + (:borderLeftColor styles))) + border (when has-border? + (let [widths [bw-t bw-r bw-b bw-l]] + (cond-> (if (apply = widths) + {:width (first widths)} + {:widths widths}) + border-color (assoc :color border-color)))) + + ;; Box shadow + shadow-raw (css/parse-box-shadow (:boxShadow styles)) + shadow (when shadow-raw (first shadow-raw)) ;; take first shadow for IR (single) + + ;; Background gradient + gradient (css/parse-gradient (:backgroundImage styles)) + + ;; Opacity + opacity-str (:opacity styles) + opacity (when (and opacity-str (string? opacity-str)) + (let [o (css/parse-px opacity-str)] ;; parse-px handles bare numbers + (when (and o (< o 1.0)) o))) + + visual (let [v (cond-> {} + fill (assoc :fill fill) + radius (assoc :radius radius) + border (assoc :border border) + shadow (assoc :shadow shadow) + gradient (assoc :gradient gradient) + opacity (assoc :opacity opacity))] + (when (seq v) v)) + + ;; --- Typography --- + font-size (css/parse-px (:fontSize styles)) + font-weight (css/parse-font-weight (:fontWeight styles)) + text-color (:color styles) + font-family (:fontFamily styles) + + typography (when text + (cond-> {:content text} + font-size (assoc :size font-size) + font-weight (assoc :weight font-weight) + text-color (assoc :color text-color) + font-family (assoc :family font-family))) + + ;; --- Layout --- + display (css/parse-display (:display styles)) + direction (css/parse-direction (:flexDirection styles)) + align (css/parse-align (:alignItems styles)) + gap (css/parse-px (:gap styles)) + pad-t (css/parse-px (:paddingTop styles)) + pad-r (css/parse-px (:paddingRight styles)) + pad-b (css/parse-px (:paddingBottom styles)) + pad-l (css/parse-px (:paddingLeft styles)) + has-padding? (and pad-t pad-r pad-b pad-l + (not (every? zero? [pad-t pad-r pad-b pad-l]))) + padding (when has-padding? [pad-t pad-r pad-b pad-l]) + + layout (when (or (= display :flex) (= display :grid)) + (cond-> {:display :flex} + direction (assoc :direction direction) + align (assoc :align-items align) + gap (assoc :gap gap) + padding (assoc :padding padding))) + + ;; If not flex but has padding, still capture padding in layout + layout (if (and (nil? layout) padding) + {:display :block :padding padding} + layout) + + ;; --- Children --- + ir-children (when (seq children) + (mapv #(extracted->ir % opts) children)) + + ;; --- Role --- + role (infer-role tag (some? text) (seq children)) + + ;; --- Assemble --- + ir (cond-> {:tag (or tag "div") + :role role + :bounds {:w (or (:w bounds) 0) + :h (or (:h bounds) 0)}} + visual (assoc :visual visual) + typography (assoc :typography typography) + layout (assoc :layout layout) + ir-children (assoc :children (vec (remove nil? ir-children))) + (:source-url opts) (assoc :source-meta + (cond-> {:url (:source-url opts)} + (:library opts) (assoc :library (:library opts)) + tag (assoc :component tag))))] + ir)))) + +;; --------------------------------------------------------------------------- +;; Convenience: full pipeline in one call +;; --------------------------------------------------------------------------- + +(defn extract->rt-node + "Full pipeline: extractor JSON → Design IR → rt-node tree. + Requires compiler namespace and dt (design tokens). + + extracted-json: the parsed JSON from _extractor.js (:tree key) + compile-fn: components.compiler/compile-ir + dt: design tokens map from loop.cljs + opts: {:source-url :library :use-tokens?}" + [extracted-json compile-fn dt opts] + (let [ir (extracted->ir extracted-json (select-keys opts [:source-url :library]))] + (compile-fn ir dt (select-keys opts [:use-tokens? :id-prefix])))) diff --git a/src/components/compiler.cljc b/src/components/compiler.cljc new file mode 100644 index 0000000..7d08f34 --- /dev/null +++ b/src/components/compiler.cljc @@ -0,0 +1,352 @@ +(ns components.compiler + "Compile Design IR nodes → rt-node trees compatible with loop.cljs. + + The rt-node shape (loop.cljs:594-610): + {:id :type :bounds {:x :y :w :h} :style {} :actions {} :children [] :text [] :clip? :data :layout} + + Design IR shape (components._design_ir): + {:tag :role :bounds :visual :typography :layout :children :slots :states :source-meta} + + This compiler maps between the two, resolving token refs and CSS color strings + via the dt (design tokens) map." + (:require [clojure.string :as str] + [components.css-parsers :as css])) + +;; --------------------------------------------------------------------------- +;; Helpers +;; --------------------------------------------------------------------------- + +(defn- deep-merge + "Recursively merge b into a. b values win for non-map keys." + [a b] + (if (and (map? a) (map? b)) + (merge-with deep-merge a b) + b)) + +(defn- resolve-color + "Resolve a color value from IR to [r g b a]. + Handles: + - {:type :token :ref [:colors :bg]} → lookup in dt + - {:type :solid :value [r g b a]} → extract value + - {:type :solid :value \"#fff\"} → parse CSS string + - [r g b a] vector → pass through + - CSS string → parse" + [color-val dt use-tokens?] + (cond + (nil? color-val) + nil + + ;; Direct RGBA vector + (vector? color-val) + color-val + + ;; Token reference + (and (map? color-val) (= :token (:type color-val))) + (if use-tokens? + (get-in dt (:ref color-val)) + ;; Fallback: resolve token to literal + (or (get-in dt (:ref color-val)) + (:value color-val))) + + ;; Solid literal + (and (map? color-val) (= :solid (:type color-val))) + (let [v (:value color-val)] + (cond + (vector? v) v + (string? v) (css/parse-color v) + :else nil)) + + ;; Plain CSS string + (string? color-val) + (css/parse-color color-val) + + :else nil)) + +;; --------------------------------------------------------------------------- +;; Style compiler (IR :visual → rt-node :style) +;; --------------------------------------------------------------------------- + +(defn- compile-style + "Map IR :visual to rt-node :style map." + [visual dt use-tokens?] + (if (nil? visual) + {} + (let [;; Background fill + bg (resolve-color (:fill visual) dt use-tokens?) + + ;; Radius + radius-val (:radius visual) + radius (when radius-val + (cond + (:uniform radius-val) {:radius (:uniform radius-val)} + (:corners radius-val) {:corner-radii (:corners radius-val)} + (number? radius-val) {:radius radius-val} + :else nil)) + + ;; Border + border (:border visual) + border-style (when border + (cond-> {} + (:width border) + (assoc :border-width (:width border)) + + (:widths border) + (assoc :border-widths (:widths border)) + + (:color border) + (assoc :border-color + (resolve-color (:color border) dt use-tokens?)))) + + ;; Shadow: IR uses {:offset [x y]}, rt-node uses {:offset-x N :offset-y N} + shadow (:shadow visual) + shadow-style (when shadow + (let [color (resolve-color (:color shadow) dt use-tokens?) + offset (:offset shadow) + ox (if offset (nth offset 0) (or (:offset-x shadow) 0)) + oy (if offset (nth offset 1) (or (:offset-y shadow) 0))] + (cond-> {:offset-x ox :offset-y oy} + (:blur shadow) (assoc :blur (:blur shadow)) + (:spread shadow) (assoc :spread (:spread shadow)) + color (assoc :color color)))) + + ;; Gradient: extract angle + first/last stop colors for rt-node format + gradient (:gradient visual) + gradient-style (when (and gradient (= :linear (:type gradient)) (seq (:stops gradient))) + (let [stops (:stops gradient) + first-color (resolve-color (:value (first stops)) dt use-tokens?) + last-color (resolve-color (:value (last stops)) dt use-tokens?) + angle (or (:angle gradient) Math/PI) + ;; rt-node gradient format: [angle t_stop 0 0] + t-stop (or (:at (first (rest stops))) 0.5)] + (cond-> {} + true (assoc :gradient [angle t-stop 0 0]) + first-color (assoc :bg first-color) + last-color (assoc :gradient-color2 last-color)))) + + ;; Opacity: multiply into bg alpha + opacity (:opacity visual)] + + (cond-> {} + bg (assoc :bg (if (and opacity bg) + (assoc bg 3 (* (nth bg 3 1.0) opacity)) + bg)) + radius (merge radius) + border-style (merge border-style) + shadow-style (assoc :shadow shadow-style) + gradient-style (merge gradient-style))))) + +;; --------------------------------------------------------------------------- +;; Layout compiler (IR :layout → rt-node :layout) +;; --------------------------------------------------------------------------- + +(defn- compile-layout + "Map IR :layout to rt-node :layout map. Only for flex/grid layouts." + [ir-layout] + (when (and ir-layout + (contains? #{:flex :grid} (:display ir-layout))) + (let [direction (or (:direction ir-layout) :column) + gap (or (:gap ir-layout) 0) + padding (:padding ir-layout) + align (or (:align ir-layout) + (case (or (:align-items ir-layout) :start) + :flex-start :start + :start :start + :center :center + :flex-end :end + :end :end + :start)) + auto-height? (:auto-height? ir-layout)] + (cond-> {:direction direction + :gap gap + :align align} + padding (assoc :padding padding) + auto-height? (assoc :auto-height? true))))) + +;; --------------------------------------------------------------------------- +;; Text compiler (IR :typography → rt-node :text ops) +;; --------------------------------------------------------------------------- + +(defn- compile-text + "Map IR :typography to rt-node :text vector. + Returns a vector of text-op maps." + [typography dt use-tokens? bounds] + (when (and typography (:content typography)) + (let [content (:content typography) + size (or (:size typography) 14) + color (resolve-color (:color typography) dt use-tokens?) + [r g b a] (or color [0.9 0.9 0.92 1.0]) + ;; Position text relative to node bounds + ;; x starts at 0 (layout engine handles absolute positioning) + ;; y baseline at size (single-line default) + line-h (or (:line-height typography) (* size 1.4))] + [{:text content + :type :text + :from 0 + :to (count content) + :x 0 + :y line-h + :size size + :r r :g g :b b :a a}]))) + +;; --------------------------------------------------------------------------- +;; Type mapping (IR :tag + :role → rt-node :type) +;; --------------------------------------------------------------------------- + +(defn- compile-type + "Map IR :tag + :role to rt-node :type keyword." + [tag role] + (let [tag (when tag (str/lower-case tag))] + (cond + (= tag "button") :button + (= tag "input") :input + (= tag "a") :link + (= tag "img") :image + (= tag "svg") :icon + (= tag "span") :text + (= tag "p") :text + (= tag "h1") :text + (= tag "h2") :text + (= tag "h3") :text + (= tag "label") :text + (= role :text) :text + (= role :interactive) :interactive + (= role :decorative) :decorative + :else :rect))) + +;; --------------------------------------------------------------------------- +;; Main compiler +;; --------------------------------------------------------------------------- + +(defn- is-rt-node? + "Detect if input is already in rt-node format (not Design IR)." + [node] + (and (map? node) + (or (contains? node :style) + (contains? node :type)) + (not (contains? node :visual)) + (not (contains? node :tag)))) + +(defn compile-ir + "Compile a Design IR node → rt-node map. + + dt: design tokens map from loop.cljs + opts: {:use-tokens? bool ;; resolve token refs (default true) + :id-prefix string} ;; prefix for generated IDs + + Also handles direct rt-node input (from blueprints) — passes through unchanged." + ([node dt] (compile-ir node dt {})) + ([node dt opts] + (if (nil? node) + nil + ;; If it's already an rt-node, pass through + (if (is-rt-node? node) + (cond-> node + (:children node) + (update :children (fn [cs] (mapv #(compile-ir % dt opts) cs)))) + + ;; Compile from Design IR + (let [{:keys [use-tokens? id-prefix] + :or {use-tokens? true id-prefix ""}} opts + + ;; ID + ir-id (or (:id node) (str id-prefix (gensym "ir-"))) + id (if (keyword? ir-id) ir-id (keyword (str id-prefix ir-id))) + + ;; Type + node-type (compile-type (:tag node) (:role node)) + + ;; Bounds — always start at 0,0; layout engine positions + ir-bounds (:bounds node) + bounds {:x (or (:x ir-bounds) 0) + :y (or (:y ir-bounds) 0) + :w (or (:w ir-bounds) 0) + :h (or (:h ir-bounds) 0)} + + ;; Style from :visual + style (compile-style (:visual node) dt use-tokens?) + + ;; Layout from :layout + layout (compile-layout (:layout node)) + + ;; Text from :typography + text-ops (compile-text (:typography node) dt use-tokens? bounds) + + ;; Children — recursive compile + children (when (:children node) + (mapv #(compile-ir % dt opts) (:children node))) + + ;; Slots → placeholder children + slot-children (when (:slots node) + (mapv (fn [[slot-name slot-def]] + {:id (keyword (str (name id) "-slot-" (name slot-name))) + :type :slot + :bounds (or (:bounds slot-def) {:x 0 :y 0 :w 0 :h 0}) + :style {} + :actions {} + :children [] + :text [] + :clip? false + :data {:slot-name slot-name + :slot-role (:role slot-def) + :slot-description (:description slot-def)} + :layout nil}) + (:slots node))) + + all-children (vec (concat (or children []) (or slot-children [])))] + + ;; Assemble rt-node + {:id id + :type node-type + :bounds bounds + :style (or style {}) + :actions {} + :children all-children + :text (or text-ops []) + :clip? false + :data (cond-> {} + (:source-meta node) (assoc :source-meta (:source-meta node)) + (:tag node) (assoc :ir-tag (:tag node)) + (:role node) (assoc :ir-role (:role node))) + :layout layout}))))) + +;; --------------------------------------------------------------------------- +;; Component compiler (blueprint with states) +;; --------------------------------------------------------------------------- + +(defn compile-component + "Compile a blueprint (with :states) → map of state-name → rt-node. + + Handles two blueprint formats: + 1. Codex format: {:states [{:name :default :tree {rt-node-like-map}}]} + Each state has a complete :tree — compile each independently + 2. Design IR format: {:states {:hover {:visual ...}}} + States are override maps — deep-merge with :default, then compile + + Returns: {:default rt-node, :hover rt-node, ...}" + ([blueprint dt] (compile-component blueprint dt {})) + ([blueprint dt opts] + (cond + ;; Codex blueprint format: :states is a vector of {:name :tree} + (and (vector? (:states blueprint)) + (every? #(and (:name %) (:tree %)) (:states blueprint))) + (reduce (fn [acc state] + (assoc acc (:name state) + (compile-ir (:tree state) dt opts))) + {} + (:states blueprint)) + + ;; Design IR format: :states is a map of overrides + (map? (:states blueprint)) + (let [base-node (dissoc blueprint :states) + default-rt (compile-ir base-node dt opts)] + (reduce-kv + (fn [acc state-name overrides] + (let [merged (deep-merge base-node overrides) + state-rt (compile-ir merged dt opts)] + (assoc acc state-name state-rt))) + {:default default-rt} + (:states blueprint))) + + ;; No states — single default + :else + {:default (compile-ir blueprint dt opts)}))) diff --git a/src/components/css_parsers.cljc b/src/components/css_parsers.cljc new file mode 100644 index 0000000..5d3c9c7 --- /dev/null +++ b/src/components/css_parsers.cljc @@ -0,0 +1,436 @@ +(ns components.css-parsers + "Parse CSS property strings into Clojure data structures. + All colors return [r g b a] with 0-1 floats. + All lengths return numbers (px assumed)." + (:require [clojure.string :as str])) + +;; --------------------------------------------------------------------------- +;; Helpers +;; --------------------------------------------------------------------------- + +(defn- safe-parse-double [s] + (when (and s (string? s) (not (str/blank? s))) + (try + #?(:clj (Double/parseDouble (str/trim s)) + :cljs (let [n (js/parseFloat (str/trim s))] + (when-not (js/isNaN n) n))) + #?(:clj (catch Exception _ nil) + :cljs (catch :default _ nil))))) + +(defn- hex-byte + "Parse a 2-char hex string to 0-1 float." + [s] + (when (and s (= 2 (count s))) + (let [n #?(:clj (Integer/parseInt s 16) + :cljs (js/parseInt s 16))] + (/ n 255.0)))) + +(defn- clamp01 [x] (max 0.0 (min 1.0 (double x)))) + +;; --------------------------------------------------------------------------- +;; Named colors (minimal set — covers what getComputedStyle returns + common) +;; --------------------------------------------------------------------------- + +(def ^:private named-colors + {"transparent" [0 0 0 0] + "black" [0 0 0 1] + "white" [1 1 1 1] + "red" [1 0 0 1] + "green" [0 0.502 0 1] + "blue" [0 0 1 1] + "yellow" [1 1 0 1] + "cyan" [0 1 1 1] + "magenta" [1 0 1 1] + "gray" [0.502 0.502 0.502 1] + "grey" [0.502 0.502 0.502 1] + "orange" [1 0.647 0 1] + "purple" [0.502 0 0.502 1] + "pink" [1 0.753 0.796 1] + "none" [0 0 0 0] + "currentcolor" nil}) + +;; --------------------------------------------------------------------------- +;; HSL → RGB conversion +;; --------------------------------------------------------------------------- + +(defn- hue->rgb [p q t] + (let [t (cond (< t 0) (+ t 1) (> t 1) (- t 1) :else t)] + (cond + (< t (/ 1.0 6)) (+ p (* (- q p) 6.0 t)) + (< t 0.5) q + (< t (/ 2.0 3)) (+ p (* (- q p) (- (/ 2.0 3) t) 6.0)) + :else p))) + +(defn- hsl->rgb [h s l] + (let [h (/ (mod h 360) 360.0) + s (clamp01 (/ s 100.0)) + l (clamp01 (/ l 100.0))] + (if (zero? s) + [l l l] + (let [q (if (< l 0.5) (* l (+ 1.0 s)) (+ l s (- (* l s)))) + p (- (* 2.0 l) q)] + [(clamp01 (hue->rgb p q (+ h (/ 1.0 3)))) + (clamp01 (hue->rgb p q h)) + (clamp01 (hue->rgb p q (- h (/ 1.0 3))))])))) + +;; --------------------------------------------------------------------------- +;; parse-color — THE main color parser +;; --------------------------------------------------------------------------- + +(defn parse-color + "Parse a CSS color string → [r g b a] (0-1 floats) or nil." + [s] + (when (and s (string? s) (not (str/blank? s))) + (let [s (str/trim (str/lower-case s))] + (cond + ;; Named colors + (contains? named-colors s) + (get named-colors s) + + ;; Hex: #RGB, #RGBA, #RRGGBB, #RRGGBBAA + (str/starts-with? s "#") + (let [hex (subs s 1)] + (case (count hex) + 3 [(hex-byte (str (nth hex 0) (nth hex 0))) + (hex-byte (str (nth hex 1) (nth hex 1))) + (hex-byte (str (nth hex 2) (nth hex 2))) + 1.0] + 4 [(hex-byte (str (nth hex 0) (nth hex 0))) + (hex-byte (str (nth hex 1) (nth hex 1))) + (hex-byte (str (nth hex 2) (nth hex 2))) + (hex-byte (str (nth hex 3) (nth hex 3)))] + 6 [(hex-byte (subs hex 0 2)) + (hex-byte (subs hex 2 4)) + (hex-byte (subs hex 4 6)) + 1.0] + 8 [(hex-byte (subs hex 0 2)) + (hex-byte (subs hex 2 4)) + (hex-byte (subs hex 4 6)) + (hex-byte (subs hex 6 8))] + nil)) + + ;; CSS Color Level 4: rgb(R G B / A) or rgba(R G B / A) or rgb(R, G, B, A) + (or (str/starts-with? s "rgb(") (str/starts-with? s "rgba(")) + (let [inner (-> s + (str/replace #"^rgba?\(" "") + (str/replace #"\)$" "") + str/trim)] + (if (str/includes? inner "/") + ;; Space-separated with slash alpha: rgb(23 23 25 / 0.5) + (let [[color-part alpha-part] (str/split inner #"/") + parts (str/split (str/trim color-part) #"\s+") + alpha (str/trim alpha-part)] + (when (= 3 (count parts)) + (let [parse-comp (fn [v] + (if (str/ends-with? v "%") + (/ (safe-parse-double (subs v 0 (dec (count v)))) 100.0) + (/ (or (safe-parse-double v) 0) 255.0))) + a (if (str/ends-with? alpha "%") + (/ (safe-parse-double (subs alpha 0 (dec (count alpha)))) 100.0) + (safe-parse-double alpha))] + [(clamp01 (parse-comp (nth parts 0))) + (clamp01 (parse-comp (nth parts 1))) + (clamp01 (parse-comp (nth parts 2))) + (clamp01 (or a 1.0))]))) + ;; Comma-separated: rgb(255, 128, 0) or rgba(255, 128, 0, 0.5) + ;; Also handles space-separated without slash: rgb(255 128 0) + (let [parts (if (str/includes? inner ",") + (mapv str/trim (str/split inner #",")) + (str/split (str/trim inner) #"\s+")) + parse-comp (fn [v] + (if (str/ends-with? v "%") + (/ (safe-parse-double (subs v 0 (dec (count v)))) 100.0) + (/ (or (safe-parse-double v) 0) 255.0)))] + (case (count parts) + 3 [(clamp01 (parse-comp (nth parts 0))) + (clamp01 (parse-comp (nth parts 1))) + (clamp01 (parse-comp (nth parts 2))) + 1.0] + 4 (let [a-str (nth parts 3) + a (if (str/ends-with? a-str "%") + (/ (safe-parse-double (subs a-str 0 (dec (count a-str)))) 100.0) + (safe-parse-double a-str))] + [(clamp01 (parse-comp (nth parts 0))) + (clamp01 (parse-comp (nth parts 1))) + (clamp01 (parse-comp (nth parts 2))) + (clamp01 (or a 1.0))]) + nil)))) + + ;; HSL/HSLA + (or (str/starts-with? s "hsl(") (str/starts-with? s "hsla(")) + (let [inner (-> s + (str/replace #"^hsla?\(" "") + (str/replace #"\)$" "") + str/trim) + ;; Handle both comma and space/slash separators + [color-part alpha-part] (if (str/includes? inner "/") + (str/split inner #"/") + [inner nil]) + parts (if (str/includes? (str/trim color-part) ",") + (mapv str/trim (str/split (str/trim color-part) #",")) + (str/split (str/trim color-part) #"\s+")) + parse-num (fn [v] + (safe-parse-double (str/replace v #"[°%deg]" "")))] + (when (>= (count parts) 3) + (let [h (or (parse-num (nth parts 0)) 0) + s-val (or (parse-num (nth parts 1)) 0) + l (or (parse-num (nth parts 2)) 0) + [r g b] (hsl->rgb h s-val l) + a (cond + alpha-part + (let [at (str/trim alpha-part)] + (if (str/ends-with? at "%") + (/ (safe-parse-double (subs at 0 (dec (count at)))) 100.0) + (safe-parse-double at))) + (= 4 (count parts)) + (let [at (nth parts 3)] + (if (str/ends-with? at "%") + (/ (safe-parse-double (subs at 0 (dec (count at)))) 100.0) + (safe-parse-double at))) + :else 1.0)] + [r g b (clamp01 (or a 1.0))]))) + + :else nil)))) + +;; --------------------------------------------------------------------------- +;; Length / numeric parsers +;; --------------------------------------------------------------------------- + +(defn parse-px + "Parse a CSS length string → number (px). Handles px, rem (×16), em (×16), bare numbers." + [s] + (when (and s (string? s) (not (str/blank? s))) + (let [s (str/trim (str/lower-case s))] + (cond + (= s "0") 0 + (str/ends-with? s "px") (safe-parse-double (subs s 0 (- (count s) 2))) + (str/ends-with? s "rem") (when-let [n (safe-parse-double (subs s 0 (- (count s) 3)))] + (* n 16)) + (str/ends-with? s "em") (when-let [n (safe-parse-double (subs s 0 (- (count s) 2)))] + (* n 16)) + :else (safe-parse-double s))))) + +(defn parse-font-weight + "Parse CSS font-weight → number. 'normal'→400, 'bold'→700, numeric string." + [s] + (when (and s (string? s) (not (str/blank? s))) + (let [s (str/trim (str/lower-case s))] + (case s + "normal" 400 + "bold" 700 + "lighter" 300 + "bolder" 700 + (safe-parse-double s))))) + +;; --------------------------------------------------------------------------- +;; Box model parsers +;; --------------------------------------------------------------------------- + +(defn parse-padding + "Parse 4 CSS padding strings (top, right, bottom, left) → [t r b l] numbers." + [top right bottom left] + [(or (parse-px top) 0) + (or (parse-px right) 0) + (or (parse-px bottom) 0) + (or (parse-px left) 0)]) + +(defn parse-border-widths + "Parse 4 CSS border-width strings → [t r b l] numbers." + [top right bottom left] + [(or (parse-px top) 0) + (or (parse-px right) 0) + (or (parse-px bottom) 0) + (or (parse-px left) 0)]) + +(defn parse-border-color + "Parse 4 CSS border-color strings → first non-nil parsed [r g b a]." + [top right bottom left] + (some parse-color [top right bottom left])) + +(defn parse-border-radius + "Parse 4 CSS border-radius corner strings → {:uniform N} or {:corners [tl tr br bl]}." + [tl tr br bl] + (let [vals [(or (parse-px tl) 0) (or (parse-px tr) 0) (or (parse-px br) 0) (or (parse-px bl) 0)]] + (if (apply = vals) + {:uniform (first vals)} + {:corners vals}))) + +;; --------------------------------------------------------------------------- +;; Box shadow +;; --------------------------------------------------------------------------- + +(defn- split-outside-parens + "Split string on commas that are NOT inside parentheses." + [s] + (loop [i 0 depth 0 start 0 result []] + (if (>= i (count s)) + (conj result (subs s start)) + (let [c (nth s i)] + (cond + (= c \() (recur (inc i) (inc depth) start result) + (= c \)) (recur (inc i) (max 0 (dec depth)) start result) + (and (= c \,) (zero? depth)) + (recur (inc i) depth (inc i) (conj result (subs s start i))) + :else (recur (inc i) depth start result)))))) + +(defn- parse-single-shadow + "Parse a single CSS box-shadow value → shadow map or nil (for inset)." + [s] + (let [s (str/trim s)] + (when-not (str/blank? s) + ;; Skip inset shadows + (when-not (str/includes? s "inset") + ;; Strategy: extract color tokens (rgb/rgba/hsl/hsla/hex/named), rest are numbers + (let [;; Extract function-call colors first (e.g., rgba(...)) + color-fn-re #"(?:rgba?|hsla?)\([^)]+\)" + color-fn-match (re-find color-fn-re s) + remaining (if color-fn-match + (str/replace s color-fn-re "") + s) + ;; Tokenize remaining + tokens (filterv #(not (str/blank? %)) (str/split (str/trim remaining) #"\s+")) + ;; Separate color tokens from number tokens + color-token (when-not color-fn-match + (first (filter #(or (str/starts-with? % "#") + (contains? named-colors (str/lower-case %))) + tokens))) + num-tokens (filterv #(and (not (str/starts-with? % "#")) + (not (contains? named-colors (str/lower-case %)))) + tokens) + ;; Parse numeric values: offset-x offset-y [blur [spread]] + nums (mapv #(or (parse-px %) 0) num-tokens) + color-str (or color-fn-match color-token)] + (when (>= (count nums) 2) + (cond-> {:offset [(nth nums 0) (nth nums 1)] + :blur (or (get nums 2) 0) + :spread (or (get nums 3) 0)} + color-str (assoc :color color-str)))))))) + +(defn parse-box-shadow + "Parse CSS box-shadow string → vector of shadow maps matching IR format. + Each shadow: {:offset [x y] :blur N :spread N :color \"css-string\"} + Returns nil for 'none' or empty." + [s] + (when (and s (string? s) (not (str/blank? s))) + (let [s (str/trim s)] + (when (and (not= s "none") (not= s "")) + (let [shadows (->> (split-outside-parens s) + (keep parse-single-shadow) + vec)] + (when (seq shadows) + shadows)))))) + +;; --------------------------------------------------------------------------- +;; Gradient +;; --------------------------------------------------------------------------- + +(def ^:private direction-angles + {"to top" (* Math/PI 0) ;; 0deg + "to right" (* Math/PI 0.5) ;; 90deg + "to bottom" (* Math/PI 1) ;; 180deg + "to left" (* Math/PI 1.5) ;; 270deg + "to top right" (* Math/PI 0.25) + "to bottom right" (* Math/PI 0.75) + "to bottom left" (* Math/PI 1.25) + "to top left" (* Math/PI 1.75)}) + +(defn parse-gradient + "Parse CSS linear-gradient(...) → {:type :linear :angle N :stops [{:at N :value \"color\"}]} + Angle in radians. Returns nil for non-linear or unparseable." + [s] + (when (and s (string? s)) + (let [s (str/trim s)] + (when (str/starts-with? (str/lower-case s) "linear-gradient(") + (let [inner (-> s + (str/replace #"^[Ll]inear-gradient\(" "") + (str/replace #"\)$" "") + str/trim) + parts (split-outside-parens inner) + first-part (str/trim (first parts)) + ;; Check if first part is angle/direction + angle-deg (cond + (str/ends-with? first-part "deg") + (safe-parse-double (subs first-part 0 (- (count first-part) 3))) + + (str/ends-with? first-part "rad") + nil ;; handled separately below + + (contains? direction-angles (str/lower-case first-part)) + nil ;; handled separately below + + :else nil) + angle-rad (cond + angle-deg + (* angle-deg (/ Math/PI 180.0)) + + (str/ends-with? first-part "rad") + (safe-parse-double (subs first-part 0 (- (count first-part) 3))) + + (contains? direction-angles (str/lower-case first-part)) + (get direction-angles (str/lower-case first-part)) + + :else nil) + ;; If we consumed an angle, stops start at index 1; otherwise index 0 + has-angle? (some? angle-rad) + stop-parts (if has-angle? (rest parts) parts) + ;; Parse stops: "color position?" pairs + stops (vec (keep-indexed + (fn [i part] + (let [part (str/trim part) + ;; Try to extract a trailing percentage/px position + pos-match (re-find #"(\d+(?:\.\d+)?)\s*%\s*$" part) + at (if pos-match + (/ (safe-parse-double (second pos-match)) 100.0) + ;; Auto-distribute + (if (> (count stop-parts) 1) + (/ (double i) (dec (count stop-parts))) + 0.0)) + color-str (if pos-match + (str/trim (subs part 0 (- (count part) (count (first pos-match))))) + part)] + {:at (clamp01 at) :value color-str})) + stop-parts))] + {:type :linear + :angle (or angle-rad (* Math/PI 1)) ;; default 180deg = top-to-bottom + :stops stops}))))) + +;; --------------------------------------------------------------------------- +;; Layout property parsers +;; --------------------------------------------------------------------------- + +(defn parse-direction + "CSS flex-direction → :row or :column." + [s] + (when (and s (string? s)) + (case (str/trim (str/lower-case s)) + "row" :row + "row-reverse" :row + "column" :column + "column-reverse" :column + nil))) + +(defn parse-align + "CSS align-items → :start, :center, or :end." + [s] + (when (and s (string? s)) + (case (str/trim (str/lower-case s)) + "flex-start" :start + "start" :start + "center" :center + "flex-end" :end + "end" :end + "stretch" :start + "baseline" :start + nil))) + +(defn parse-display + "CSS display → :flex, :block, :inline, or :none." + [s] + (when (and s (string? s)) + (let [s (str/trim (str/lower-case s))] + (cond + (str/includes? s "flex") :flex + (str/includes? s "grid") :flex ;; treat grid as flex for our layout engine + (= s "none") :none + (str/includes? s "inline") :inline + :else :block)))) diff --git a/src/components/design_tokens.cljc b/src/components/design_tokens.cljc new file mode 100644 index 0000000..9121f54 --- /dev/null +++ b/src/components/design_tokens.cljc @@ -0,0 +1,40 @@ +(ns components.design-tokens + "Shared design tokens — single source of truth for colors, spacing, radii, shadows. + Used by both client (loop.cljs) and server (server_jetty.clj) pipelines.") + +(def dt + "Design tokens — Linear/shadcn-inspired dark theme." + {:colors {:bg [0.09 0.09 0.11 1.0] + :bg-subtle [0.11 0.11 0.13 1.0] + :bg-muted [0.14 0.14 0.17 1.0] + :bg-elevated [0.13 0.13 0.16 1.0] + :bg-hover [0.16 0.16 0.19 1.0] + :bg-selected [0.20 0.24 0.36 0.9] + :bg-active [0.15 0.15 0.18 1.0] + :border [0.22 0.22 0.28 1.0] + :border-subtle [0.18 0.18 0.22 0.6] + :fg [0.90 0.90 0.92 1.0] + :fg-muted [0.55 0.55 0.60 1.0] + :fg-subtle [0.40 0.40 0.45 0.8] + :fg-section [0.42 0.42 0.48 1.0] + :accent [0.35 0.55 0.95 1.0] + :accent-muted [0.25 0.38 0.65 0.3] + :destructive [0.90 0.30 0.30 1.0] + :success [0.30 0.80 0.50 1.0] + :warning [0.95 0.75 0.25 1.0]} + :spacing {:xs 4 :sm 8 :md 12 :lg 16 :xl 24 :xxl 32} + :radii {:sm 4 :md 6 :lg 8 :xl 12 :full 9999} + :shadows {:sm {:blur 4 :offset-y 1 :color [0 0 0 0.15]} + :md {:blur 8 :offset-y 2 :color [0 0 0 0.25]} + :lg {:blur 16 :offset-y 4 :color [0 0 0 0.35]}} + :font-sizes {:xs 10 :sm 12 :md 14 :lg 16 :xl 20} + ;; Surface elevation — depth over borders + :surfaces {:sunken [0.05 0.06 0.08 1.0] + :base [0.07 0.08 0.11 1.0] + :elevated [0.12 0.15 0.21 1.0] + :hover [0.13 0.17 0.22 1.0] + :active [0.18 0.23 0.31 1.0] + :text-primary [0.90 0.93 0.97 1.0] + :text-secondary [0.55 0.61 0.70 1.0] + :text-muted [0.36 0.42 0.50 1.0] + :accent [0.24 0.63 1.0 1.0]}}) diff --git a/src/components/token_matcher.cljc b/src/components/token_matcher.cljc new file mode 100644 index 0000000..7ddef41 --- /dev/null +++ b/src/components/token_matcher.cljc @@ -0,0 +1,228 @@ +(ns components.token-matcher + "Map literal CSS values → nearest design tokens (dt) from loop.cljs. + Produces token refs where matches are within threshold, preserving + literal values otherwise." + (:require [clojure.string :as str] + [components.css-parsers :as css])) + +;; --------------------------------------------------------------------------- +;; Distance functions +;; --------------------------------------------------------------------------- + +(defn color-distance + "Euclidean distance between two [r g b a] vectors. Alpha weighted 2x. + Returns 1.0 if only one is nil, 0.0 if both nil." + [a b] + (cond + (and (nil? a) (nil? b)) 0.0 + (or (nil? a) (nil? b)) 1.0 + :else + (let [dr (- (nth a 0) (nth b 0)) + dg (- (nth a 1) (nth b 1)) + db (- (nth a 2) (nth b 2)) + da (* 2.0 (- (nth a 3 1.0) (nth b 3 1.0)))] + (Math/sqrt (+ (* dr dr) (* dg dg) (* db db) (* da da)))))) + +(defn- numeric-distance [a b] (Math/abs (double (- a b)))) + +;; --------------------------------------------------------------------------- +;; Match functions — each returns {:type :token :ref [...]} or {:type :solid :value v} +;; --------------------------------------------------------------------------- + +(defn match-color + "Find the nearest dt color token for an [r g b a] vector. + dt-colors: flat map of keyword→[r g b a] (e.g., (:colors dt)). + Returns {:type :token :ref [:colors ]} if within threshold, + otherwise {:type :solid :value rgba-vec}." + [rgba dt-colors threshold] + (if (nil? rgba) + nil + (let [best (reduce-kv + (fn [acc k v] + (let [d (color-distance rgba v)] + (if (< d (:dist acc)) + {:dist d :key k} + acc))) + {:dist Double/MAX_VALUE :key nil} + dt-colors)] + (if (and (:key best) (<= (:dist best) threshold)) + {:type :token :ref [:colors (:key best)] :token-distance (:dist best)} + {:type :solid :value rgba})))) + +(defn match-radius + "Find nearest dt radius token for a number. + dt-radii: map of keyword→number (e.g., (:radii dt)). + Returns token keyword or literal number." + [n dt-radii] + (if (nil? n) + nil + (let [best (reduce-kv + (fn [acc k v] + (let [d (numeric-distance n v)] + (if (< d (:dist acc)) + {:dist d :key k} + acc))) + {:dist Double/MAX_VALUE :key nil} + dt-radii)] + (if (and (:key best) (<= (:dist best) 2.0)) + (:key best) + n)))) + +(defn match-shadow + "Find nearest dt shadow token for a shadow map. + Compares blur, offset-x, offset-y, and color distance. + Returns token keyword or literal shadow map." + [shadow-map dt-shadows] + (if (nil? shadow-map) + nil + (let [s-blur (or (:blur shadow-map) 0) + s-ox (or (get-in shadow-map [:offset 0]) + (:offset-x shadow-map) 0) + s-oy (or (get-in shadow-map [:offset 1]) + (:offset-y shadow-map) 0) + s-color (let [c (:color shadow-map)] + (cond + (vector? c) c + (string? c) (css/parse-color c) + :else nil)) + best (reduce-kv + (fn [acc k v] + (let [t-blur (or (:blur v) 0) + t-ox (or (:offset-x v) 0) + t-oy (or (:offset-y v) 0) + t-color (:color v) + d (+ (numeric-distance s-blur t-blur) + (numeric-distance s-ox t-ox) + (numeric-distance s-oy t-oy) + (* 10.0 (color-distance s-color t-color)))] + (if (< d (:dist acc)) + {:dist d :key k} + acc))) + {:dist Double/MAX_VALUE :key nil} + dt-shadows)] + (if (and (:key best) (<= (:dist best) 5.0)) + (:key best) + shadow-map)))) + +(defn match-font-size + "Find nearest dt font-size token. Returns keyword or number." + [n dt-font-sizes] + (if (nil? n) + nil + (let [best (reduce-kv + (fn [acc k v] + (let [d (numeric-distance n v)] + (if (< d (:dist acc)) + {:dist d :key k} + acc))) + {:dist Double/MAX_VALUE :key nil} + dt-font-sizes)] + (if (and (:key best) (<= (:dist best) 1.0)) + (:key best) + n)))) + +(defn match-spacing + "Find nearest dt spacing token. Returns keyword or number." + [n dt-spacing] + (if (nil? n) + nil + (let [best (reduce-kv + (fn [acc k v] + (let [d (numeric-distance n v)] + (if (< d (:dist acc)) + {:dist d :key k} + acc))) + {:dist Double/MAX_VALUE :key nil} + dt-spacing)] + (if (and (:key best) (<= (:dist best) 2.0)) + (:key best) + n)))) + +;; --------------------------------------------------------------------------- +;; Recursive tokenizer — walks IR tree, upgrades literals → token refs +;; --------------------------------------------------------------------------- + +(defn- resolve-color-val + "If color-val is a string, parse it. If vector, keep. Returns [r g b a] or nil." + [v] + (cond + (vector? v) v + (string? v) (css/parse-color v) + :else nil)) + +(defn- tokenize-fill [fill dt-colors threshold] + (if (and fill (= :solid (:type fill))) + (let [rgba (resolve-color-val (:value fill)) + matched (when rgba (match-color rgba dt-colors threshold))] + (or matched fill)) + fill)) + +(defn- tokenize-visual [visual dt threshold] + (if (nil? visual) + nil + (let [dt-colors (:colors dt)] + (cond-> visual + (:fill visual) + (update :fill tokenize-fill dt-colors threshold) + + (get-in visual [:border :color]) + (update-in [:border :color] + (fn [c] + (let [rgba (resolve-color-val c) + matched (when rgba (match-color rgba dt-colors threshold))] + (if (and matched (= :token (:type matched))) + matched + c)))) + + (get-in visual [:shadow :color]) + (update-in [:shadow :color] + (fn [c] + (let [rgba (resolve-color-val c) + matched (when rgba (match-color rgba dt-colors threshold))] + (if (and matched (= :token (:type matched))) + matched + c)))))))) + +(defn- tokenize-typography [typo dt threshold] + (if (nil? typo) + nil + (let [dt-colors (:colors dt)] + (cond-> typo + (:color typo) + (update :color + (fn [c] + (let [rgba (resolve-color-val c) + matched (when rgba (match-color rgba dt-colors threshold))] + (if (and matched (= :token (:type matched))) + matched + c)))))))) + +(defn tokenize-ir + "Recursively walk an IR node tree, replacing literal values with token refs + where they match dt tokens within threshold. + dt: the design tokens map from loop.cljs. + opts: {:color-threshold 0.08} (optional overrides)." + ([node dt] (tokenize-ir node dt {})) + ([node dt opts] + (let [threshold (or (:color-threshold opts) 0.08)] + (cond-> node + (:visual node) + (update :visual tokenize-visual dt threshold) + + (:typography node) + (update :typography tokenize-typography dt threshold) + + (:children node) + (update :children (fn [cs] (mapv #(tokenize-ir % dt opts) cs))) + + (:states node) + (update :states + (fn [states] + (reduce-kv + (fn [acc k v] + (assoc acc k + (cond-> v + (:visual v) (update :visual tokenize-visual dt threshold) + (:typography v) (update :typography tokenize-typography dt threshold)))) + {} + states))))))) diff --git a/src/global_flow.cljs b/src/global_flow.cljs index 795fe34..dfc2ede 100644 --- a/src/global_flow.cljs +++ b/src/global_flow.cljs @@ -38,6 +38,7 @@ (defonce !all-nodes-map (atom [])) (defonce !quad-tree (atom nil)) +(defonce !text-renderer (atom nil)) (defonce !canvas (atom nil)) (defonce !squares (atom nil)) @@ -85,3 +86,5 @@ #(v (fn [] (throw %)))) (m/absolve v))) + +