diff --git a/.github/actions/build-docs/action.yml b/.github/actions/build-docs/action.yml index a31bece..2981ccf 100644 --- a/.github/actions/build-docs/action.yml +++ b/.github/actions/build-docs/action.yml @@ -1,20 +1,20 @@ name: "Build Documentation" -description: "Build Astro/Starlight documentation" +description: "Execute the tutorial notebooks and build the Astro/Starlight documentation" inputs: python-version: description: "Python version to use" required: false default: "3.12" - build-tutorials: - description: "Whether to generate tutorial markdown/assets" + execute-notebooks: + description: "Whether to execute the tutorial notebooks (into astro_docs/.cache/notebooks)" required: false default: "true" - tutorial-build-args: - description: "Extra arguments passed to build_tutorials.py" + notebook-directory: + description: "Execute only the notebooks in this directory of tutorials/ (a CI shard)" required: false default: "" build-site: - description: "Whether to build the Astro site" + description: "Whether to convert the executed notebooks into pages and build the Astro site" required: false default: "true" runs: @@ -25,6 +25,11 @@ runs: with: python-version: ${{ inputs.python-version }} + - name: Set up Node + uses: actions/setup-node@v4 + with: + node-version: 22 + - name: Check if pdflatex is available shell: bash run: | @@ -36,11 +41,32 @@ runs: run: | python -m pip install --upgrade pip python -m pip install ".[docs]" + python -m ipykernel install --user --name python3 + echo "DOCS_PYTHON=$(command -v python)" >> "$GITHUB_ENV" + + - name: Install the npm packages + shell: bash + working-directory: astro_docs + run: npm ci + + - name: Execute the tutorial notebooks + if: ${{ inputs.execute-notebooks == 'true' }} + shell: bash + working-directory: astro_docs + run: | + if [ -n "${{ inputs.notebook-directory }}" ]; then + npx starlight-notebooks execute --source "../tutorials/${{ inputs.notebook-directory }}" --section tutorials + else + npx starlight-notebooks execute + fi - - name: Build tutorials (Quarto + Jupyter → Markdown) - if: ${{ inputs.build-tutorials == 'true' }} + - name: Convert the notebooks into pages + if: ${{ inputs.build-site == 'true' }} shell: bash - run: python astro_docs/scripts/build_tutorials.py ${{ inputs.tutorial-build-args }} + working-directory: astro_docs + run: | + npx starlight-notebooks convert --strict + npx starlight-check-mdx - name: Build and upload site if: ${{ inputs.build-site == 'true' }} @@ -48,8 +74,10 @@ runs: with: path: astro_docs + # The links in the pages carry the /tikzfigure base path, so they cannot all resolve in a + # site served from /; the link check already ran on the real build above. - name: Rebuild without base path for local artifact inspection if: ${{ inputs.build-site == 'true' }} shell: bash working-directory: astro_docs - run: npx astro build --base / + run: DOCS_VALIDATE_LINKS=false npx astro build --base / diff --git a/.github/workflows/test_docs.yml b/.github/workflows/test_docs.yml index bfdc1ee..920748c 100644 --- a/.github/workflows/test_docs.yml +++ b/.github/workflows/test_docs.yml @@ -32,21 +32,20 @@ jobs: - name: Checkout uses: actions/checkout@v4 - - name: Build tutorial directory + - name: Execute the notebooks of one tutorial directory uses: ./.github/actions/build-docs with: python-version: "3.12" build-site: "false" - tutorial-build-args: --serial --directory ${{ matrix.tutorial-dir }} + notebook-directory: ${{ matrix.tutorial-dir }} - name: Upload tutorial directory artifact uses: actions/upload-artifact@v4 if: always() with: name: tutorials-dir-${{ matrix.tutorial-dir }} - path: | - astro_docs/src/content/docs/tutorials/ - astro_docs/public/tutorials/ + path: astro_docs/.cache/notebooks/ + include-hidden-files: true retention-days: 7 test-docs: @@ -66,14 +65,14 @@ jobs: uses: actions/download-artifact@v4 with: pattern: tutorials-dir-* - path: astro_docs + path: astro_docs/.cache/notebooks merge-multiple: true - name: Build documentation uses: ./.github/actions/build-docs with: python-version: "3.12" - build-tutorials: "false" + execute-notebooks: "false" - name: Upload docs artifact uses: actions/upload-artifact@v4 diff --git a/astro_docs/.gitignore b/astro_docs/.gitignore index 5cc5d2c..c48a788 100644 --- a/astro_docs/.gitignore +++ b/astro_docs/.gitignore @@ -3,7 +3,8 @@ dist/ # generated types .astro/ -# generated by astro_docs/scripts/build_tutorials.py +# generated by starlight-notebooks +.cache/ src/content/docs/tutorials/ public/tutorials/ diff --git a/astro_docs/astro.config.mjs b/astro_docs/astro.config.mjs index f9b195e..33ee217 100644 --- a/astro_docs/astro.config.mjs +++ b/astro_docs/astro.config.mjs @@ -1,84 +1,108 @@ // @ts-check -import { defineConfig } from "astro/config"; -import starlight from "@astrojs/starlight"; -import remarkMath from "remark-math"; -import rehypeKatex from "rehype-katex"; +// The shared max-models docs setup (@max-models/starlight-theme). The site lives in astro_docs/ +// rather than docs/, hence the editLink override. The tutorial pages are converted from the +// notebooks in tutorials/ by starlight-notebooks, the API page by scripts/generate_api_docs.py. +import { defineDocs } from '@max-models/starlight-theme'; -// https://astro.build/config -export default defineConfig({ - site: "https://max-models.github.io/tikzfigure", - base: "/tikzfigure", - markdown: { - remarkPlugins: [remarkMath], - rehypePlugins: [rehypeKatex], - }, - integrations: [ - starlight({ - title: "tikzfigure", - logo: { src: "./src/assets/logo.png", replacesTitle: true }, - customCss: ["katex/dist/katex.min.css"], - social: [ - { - icon: "github", - label: "GitHub", - href: "https://github.com/max-models/tikzfigure", - }, - ], - sidebar: [ - { label: "Home", slug: "" }, - { label: "Installation", slug: "installation" }, - { - label: "Fundamentals", - items: [ - { label: "Getting Started", slug: "tutorials/tutorial_01_getting_started" }, - { label: "Styling", slug: "tutorials/tutorial_02_styling" }, - { label: "Saving and Exporting", slug: "tutorials/tutorial_16_saving_and_exporting" }, - ], - }, - { - label: "Drawing", - items: [ - { label: "Paths", slug: "tutorials/tutorial_03_paths" }, - { label: "TikZ Primitives", slug: "tutorials/tutorial_04_primitives" }, - { label: "Layers", slug: "tutorials/tutorial_05_layers" }, - { label: "Loops", slug: "tutorials/tutorial_06_loops" }, - { label: "Coordinates and Midpoints", slug: "tutorials/tutorial_07_coordinates_and_midpoints" }, - ], - }, - { - label: "2D Plots", - items: [ - { label: "Basics", slug: "tutorials/tutorial_11_2d_plots_basics" }, - { label: "Styling", slug: "tutorials/tutorial_12_2d_plots_styling" }, - { label: "Functions", slug: "tutorials/tutorial_13_2d_plots_functions" }, - { label: "Subfigures and Grids", slug: "tutorials/tutorial_14_subfigures" }, - { label: "Mixed Subfigures", slug: "tutorials/tutorial_15_mixed_subfigures" }, - ], - }, - { - label: "Advanced", - items: [ - { label: "3D Figures", slug: "tutorials/tutorial_08_3d_figures" }, - { label: "Raw TikZ", slug: "tutorials/tutorial_09_raw_tikz" }, - { label: "Math Expressions", slug: "tutorials/tutorial_10_math_expressions" }, - { label: "Tokamak X-Point", slug: "tutorials/tutorial_25_parametric_tikz_plots" }, - { label: "Figure Configuration", slug: "tutorials/tutorial_17_figure_configuration" }, - { label: "Shading and Patterns", slug: "tutorials/tutorial_18_shading_and_patterns" }, - { label: "Opacity", slug: "tutorials/tutorial_19_opacity_and_transparency" }, - { label: "Decorations", slug: "tutorials/tutorial_20_decorations" }, - { label: "Advanced Styling", slug: "tutorials/tutorial_21_advanced_styling" }, - { label: "Gantt Charts", slug: "tutorials/tutorial_26_gantt" }, - ], - }, - { - label: "Showcases", - items: [ - { label: "Calendar", slug: "tutorials/tutorial_22_calendar" }, - ], - }, - { label: "API Reference", slug: "api" }, - { label: "Interactive Editor", slug: "editor" }, - ], - }), - ], +export default defineDocs({ + repo: 'tikzfigure', + title: 'tikzfigure', + description: 'A Python library to generate readable TikZ figures.', + nav: [ + { label: 'Installation', link: '/installation/' }, + { label: 'Tutorials', link: '/tutorials/tutorial-01-getting-started/', section: '/tutorials/' }, + { label: 'API Reference', link: '/api/' }, + { label: 'Interactive Editor', link: '/editor/' }, + ], + sidebar: [ + { label: 'Home', slug: '' }, + { label: 'Installation', slug: 'installation' }, + { + label: 'Fundamentals', + items: [ + { label: 'Getting Started', slug: 'tutorials/tutorial-01-getting-started' }, + { label: 'Styling', slug: 'tutorials/tutorial-02-styling' }, + { label: 'Saving and Exporting', slug: 'tutorials/tutorial-16-saving-and-exporting' }, + ], + }, + { + label: 'Drawing', + items: [ + { label: 'Paths', slug: 'tutorials/tutorial-03-paths' }, + { label: 'TikZ Primitives', slug: 'tutorials/tutorial-04-primitives' }, + { label: 'Layers', slug: 'tutorials/tutorial-05-layers' }, + { label: 'Loops', slug: 'tutorials/tutorial-06-loops' }, + { label: 'Coordinates and Midpoints', slug: 'tutorials/tutorial-07-coordinates-and-midpoints' }, + ], + }, + { + label: '2D Plots', + items: [ + { label: 'Basics', slug: 'tutorials/tutorial-11-2d-plots-basics' }, + { label: 'Styling', slug: 'tutorials/tutorial-12-2d-plots-styling' }, + { label: 'Functions', slug: 'tutorials/tutorial-13-2d-plots-functions' }, + { label: 'Subfigures and Grids', slug: 'tutorials/tutorial-14-subfigures' }, + { label: 'Mixed Subfigures', slug: 'tutorials/tutorial-15-mixed-subfigures' }, + ], + }, + { + label: 'Advanced', + items: [ + { label: '3D Figures', slug: 'tutorials/tutorial-08-3d-figures' }, + { label: 'Raw TikZ', slug: 'tutorials/tutorial-09-raw-tikz' }, + { label: 'Math Expressions', slug: 'tutorials/tutorial-10-math-expressions' }, + { label: 'Tokamak X-Point', slug: 'tutorials/tutorial-25-parametric-tikz-plots' }, + { label: 'Figure Configuration', slug: 'tutorials/tutorial-17-figure-configuration' }, + { label: 'Shading and Patterns', slug: 'tutorials/tutorial-18-shading-and-patterns' }, + { label: 'Opacity', slug: 'tutorials/tutorial-19-opacity-and-transparency' }, + { label: 'Decorations', slug: 'tutorials/tutorial-20-decorations' }, + { label: 'Advanced Styling', slug: 'tutorials/tutorial-21-advanced-styling' }, + { label: 'Gantt Charts', slug: 'tutorials/tutorial-26-gantt' }, + ], + }, + { + label: 'Showcases', + items: [{ label: 'Calendar', slug: 'tutorials/tutorial-22-calendar' }], + }, + { label: 'API Reference', slug: 'api' }, + { label: 'Interactive Editor', slug: 'editor' }, + ], + astro: { + // the pages had the notebooks' file names before (Quarto) + redirects: { + '/tutorials/tutorial_01_getting_started/': '/tutorials/tutorial-01-getting-started/', + '/tutorials/tutorial_02_styling/': '/tutorials/tutorial-02-styling/', + '/tutorials/tutorial_03_paths/': '/tutorials/tutorial-03-paths/', + '/tutorials/tutorial_04_primitives/': '/tutorials/tutorial-04-primitives/', + '/tutorials/tutorial_05_layers/': '/tutorials/tutorial-05-layers/', + '/tutorials/tutorial_06_loops/': '/tutorials/tutorial-06-loops/', + '/tutorials/tutorial_07_coordinates_and_midpoints/': '/tutorials/tutorial-07-coordinates-and-midpoints/', + '/tutorials/tutorial_08_3d_figures/': '/tutorials/tutorial-08-3d-figures/', + '/tutorials/tutorial_09_raw_tikz/': '/tutorials/tutorial-09-raw-tikz/', + '/tutorials/tutorial_10_math_expressions/': '/tutorials/tutorial-10-math-expressions/', + '/tutorials/tutorial_11_2d_plots_basics/': '/tutorials/tutorial-11-2d-plots-basics/', + '/tutorials/tutorial_11_jupyter_magic/': '/tutorials/tutorial-11-jupyter-magic/', + '/tutorials/tutorial_12_2d_plots_styling/': '/tutorials/tutorial-12-2d-plots-styling/', + '/tutorials/tutorial_13_2d_plots_functions/': '/tutorials/tutorial-13-2d-plots-functions/', + '/tutorials/tutorial_14_subfigures/': '/tutorials/tutorial-14-subfigures/', + '/tutorials/tutorial_15_mixed_subfigures/': '/tutorials/tutorial-15-mixed-subfigures/', + '/tutorials/tutorial_16_images_legends_and_ticks/': '/tutorials/tutorial-16-images-legends-and-ticks/', + '/tutorials/tutorial_16_saving_and_exporting/': '/tutorials/tutorial-16-saving-and-exporting/', + '/tutorials/tutorial_17_figure_configuration/': '/tutorials/tutorial-17-figure-configuration/', + '/tutorials/tutorial_18_shading_and_patterns/': '/tutorials/tutorial-18-shading-and-patterns/', + '/tutorials/tutorial_19_opacity_and_transparency/': '/tutorials/tutorial-19-opacity-and-transparency/', + '/tutorials/tutorial_20_decorations/': '/tutorials/tutorial-20-decorations/', + '/tutorials/tutorial_21_advanced_styling/': '/tutorials/tutorial-21-advanced-styling/', + '/tutorials/tutorial_22_calendar/': '/tutorials/tutorial-22-calendar/', + '/tutorials/tutorial_23_karl_students/': '/tutorials/tutorial-23-karl-students/', + '/tutorials/tutorial_24_spy_library/': '/tutorials/tutorial-24-spy-library/', + '/tutorials/tutorial_25_parametric_tikz_plots/': '/tutorials/tutorial-25-parametric-tikz-plots/', + '/tutorials/tutorial_26_gantt/': '/tutorials/tutorial-26-gantt/', + '/tutorials/tutorial_27_parsing_tikz/': '/tutorials/tutorial-27-parsing-tikz/', + }, + }, + starlight: { + logo: { src: './src/assets/logo.png', replacesTitle: true }, + editLink: { baseUrl: 'https://github.com/max-models/tikzfigure/edit/devel/astro_docs/' }, + }, }); diff --git a/astro_docs/package-lock.json b/astro_docs/package-lock.json index 8109c4f..1dd903e 100644 --- a/astro_docs/package-lock.json +++ b/astro_docs/package-lock.json @@ -1,298 +1,49 @@ { - "name": "peaceful-plasma", + "name": "tikzfigure-docs", "version": "0.0.1", "lockfileVersion": 3, "requires": true, "packages": { "": { - "name": "peaceful-plasma", + "name": "tikzfigure-docs", "version": "0.0.1", "dependencies": { - "@astrojs/starlight": "^0.38.2", - "astro": "^6.0.1", - "rehype-katex": "^7.0.1", - "remark-math": "^6.0.0", - "sharp": "^0.34.2" - } - }, - "node_modules/@astrojs/compiler": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/@astrojs/compiler/-/compiler-3.0.1.tgz", - "integrity": "sha512-z97oYbdebO5aoWzuJ/8q5hLK232+17KcLZ7cJ8BCWk6+qNzVxn/gftC0KzMBUTD8WAaBkPpNSQK6PXLnNrZ0CA==", - "license": "MIT" - }, - "node_modules/@astrojs/internal-helpers": { - "version": "0.8.0", - "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.8.0.tgz", - "integrity": "sha512-J56GrhEiV+4dmrGLPNOl2pZjpHXAndWVyiVDYGDuw6MWKpBSEMLdFxHzeM/6sqaknw9M+HFfHZAcvi3OfT3D/w==", - "license": "MIT", - "dependencies": { - "picomatch": "^4.0.3" - } - }, - "node_modules/@astrojs/markdown-remark": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.1.0.tgz", - "integrity": "sha512-P+HnCsu2js3BoTc8kFmu+E9gOcFeMdPris75g+Zl4sY8+bBRbSQV6xzcBDbZ27eE7yBGEGQoqjpChx+KJYIPYQ==", - "license": "MIT", - "dependencies": { - "@astrojs/internal-helpers": "0.8.0", - "@astrojs/prism": "4.0.1", - "github-slugger": "^2.0.0", - "hast-util-from-html": "^2.0.3", - "hast-util-to-text": "^4.0.2", - "js-yaml": "^4.1.1", - "mdast-util-definitions": "^6.0.0", - "rehype-raw": "^7.0.0", - "rehype-stringify": "^10.0.1", - "remark-gfm": "^4.0.1", - "remark-parse": "^11.0.0", - "remark-rehype": "^11.1.2", - "remark-smartypants": "^3.0.2", - "retext-smartypants": "^6.2.0", - "shiki": "^4.0.0", - "smol-toml": "^1.6.0", - "unified": "^11.0.5", - "unist-util-remove-position": "^5.0.0", - "unist-util-visit": "^5.1.0", - "unist-util-visit-parents": "^6.0.2", - "vfile": "^6.0.3" - } - }, - "node_modules/@astrojs/mdx": { - "version": "5.0.3", - "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-5.0.3.tgz", - "integrity": "sha512-zv/OlM5sZZvyjHqJjR3FjJvoCgbxdqj3t4jO/gSEUNcck3BjdtMgNQw8UgPfAGe4yySdG4vjZ3OC5wUxhu7ckg==", - "license": "MIT", - "dependencies": { - "@astrojs/markdown-remark": "7.1.0", - "@mdx-js/mdx": "^3.1.1", - "acorn": "^8.16.0", - "es-module-lexer": "^2.0.0", - "estree-util-visit": "^2.0.0", - "hast-util-to-html": "^9.0.5", - "piccolore": "^0.1.3", - "rehype-raw": "^7.0.0", - "remark-gfm": "^4.0.1", - "remark-smartypants": "^3.0.2", - "source-map": "^0.7.6", - "unist-util-visit": "^5.1.0", - "vfile": "^6.0.3" - }, - "engines": { - "node": ">=22.12.0" - }, - "peerDependencies": { - "astro": "^6.0.0" - } - }, - "node_modules/@astrojs/prism": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.1.tgz", - "integrity": "sha512-nksZQVjlferuWzhPsBpQ1JE5XuKAf1id1/9Hj4a9KG4+ofrlzxUUwX4YGQF/SuDiuiGKEnzopGOt38F3AnVWsQ==", - "license": "MIT", - "dependencies": { - "prismjs": "^1.30.0" + "@astrojs/starlight": "^0.42.6", + "@max-models/starlight-theme": "github:max-models/starlight-theme#347851725c6014b379c3d90ffea1d6a51b7d937e", + "astro": "^7.3.8", + "sharp": "^0.35.3" }, "engines": { "node": ">=22.12.0" } }, - "node_modules/@astrojs/sitemap": { - "version": "3.7.2", - "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.2.tgz", - "integrity": "sha512-PqkzkcZTb5ICiyIR8VoKbIAP/laNRXi5tw616N1Ckk+40oNB8Can1AzVV56lrbC5GKSZFCyJYUVYqVivMisvpA==", + "node_modules/@astrojs/compiler-binding": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding/-/compiler-binding-0.5.1.tgz", + "integrity": "sha512-oczyolLUl9mNrtuBuNjuvn5thpHrFLXAHAWGTRg0JDHQ8cGVsVeFekOu2uSoQGgainJIi+A3/9m0DyYwz3Nlnw==", "license": "MIT", - "dependencies": { - "sitemap": "^9.0.0", - "stream-replace-string": "^2.0.0", - "zod": "^4.3.6" - } - }, - "node_modules/@astrojs/starlight": { - "version": "0.38.2", - "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.38.2.tgz", - "integrity": "sha512-7AsrvG4EsXUmJT5uqiXJN4oZqKaY0wc/Ip7C6/zGnShHRVoTAA4jxeYIZ3wqbqA6zv4cnp9qk31vB2m2dUcmfg==", - "license": "MIT", - "dependencies": { - "@astrojs/markdown-remark": "^7.0.0", - "@astrojs/mdx": "^5.0.0", - "@astrojs/sitemap": "^3.7.1", - "@pagefind/default-ui": "^1.3.0", - "@types/hast": "^3.0.4", - "@types/js-yaml": "^4.0.9", - "@types/mdast": "^4.0.4", - "astro-expressive-code": "^0.41.6", - "bcp-47": "^2.1.0", - "hast-util-from-html": "^2.0.1", - "hast-util-select": "^6.0.2", - "hast-util-to-string": "^3.0.0", - "hastscript": "^9.0.0", - "i18next": "^23.11.5", - "js-yaml": "^4.1.0", - "klona": "^2.0.6", - "magic-string": "^0.30.17", - "mdast-util-directive": "^3.0.0", - "mdast-util-to-markdown": "^2.1.0", - "mdast-util-to-string": "^4.0.0", - "pagefind": "^1.3.0", - "rehype": "^13.0.1", - "rehype-format": "^5.0.0", - "remark-directive": "^3.0.0", - "ultrahtml": "^1.6.0", - "unified": "^11.0.5", - "unist-util-visit": "^5.0.0", - "vfile": "^6.0.2" - }, - "peerDependencies": { - "astro": "^6.0.0" - } - }, - "node_modules/@astrojs/telemetry": { - "version": "3.3.0", - "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.0.tgz", - "integrity": "sha512-UFBgfeldP06qu6khs/yY+q1cDAaArM2/7AEIqQ9Cuvf7B1hNLq0xDrZkct+QoIGyjq56y8IaE2I3CTvG99mlhQ==", - "license": "MIT", - "dependencies": { - "ci-info": "^4.2.0", - "debug": "^4.4.0", - "dlv": "^1.1.3", - "dset": "^3.1.4", - "is-docker": "^3.0.0", - "is-wsl": "^3.1.0", - "which-pm-runs": "^1.1.0" - }, "engines": { - "node": "18.20.8 || ^20.3.0 || >=22.0.0" - } - }, - "node_modules/@babel/helper-string-parser": { - "version": "7.27.1", - "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", - "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/helper-validator-identifier": { - "version": "7.28.5", - "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", - "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/parser": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.2.tgz", - "integrity": "sha512-4GgRzy/+fsBa72/RZVJmGKPmZu9Byn8o4MoLpmNe1m8ZfYnz5emHLQz3U4gLud6Zwl0RZIcgiLD7Uq7ySFuDLA==", - "license": "MIT", - "dependencies": { - "@babel/types": "^7.29.0" - }, - "bin": { - "parser": "bin/babel-parser.js" - }, - "engines": { - "node": ">=6.0.0" - } - }, - "node_modules/@babel/runtime": { - "version": "7.29.2", - "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.29.2.tgz", - "integrity": "sha512-JiDShH45zKHWyGe4ZNVRrCjBz8Nh9TMmZG1kh4QTK8hCBTWBi8Da+i7s1fJw7/lYpM4ccepSNfqzZ/QvABBi5g==", - "license": "MIT", - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@babel/types": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", - "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", - "license": "MIT", - "dependencies": { - "@babel/helper-string-parser": "^7.27.1", - "@babel/helper-validator-identifier": "^7.28.5" - }, - "engines": { - "node": ">=6.9.0" - } - }, - "node_modules/@capsizecss/unpack": { - "version": "4.0.0", - "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.0.tgz", - "integrity": "sha512-VERIM64vtTP1C4mxQ5thVT9fK0apjPFobqybMtA1UdUujWka24ERHbRHFGmpbbhp73MhV+KSsHQH9C6uOTdEQA==", - "license": "MIT", - "dependencies": { - "fontkitten": "^1.0.0" + "node": "^20.19.0 || >=22.12.0" }, - "engines": { - "node": ">=18" - } - }, - "node_modules/@clack/core": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.1.0.tgz", - "integrity": "sha512-SVcm4Dqm2ukn64/8Gub2wnlA5nS2iWJyCkdNHcvNHPIeBTGojpdJ+9cZKwLfmqy7irD4N5qLteSilJlE0WLAtA==", - "license": "MIT", - "dependencies": { - "sisteransi": "^1.0.5" - } - }, - "node_modules/@clack/prompts": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.1.0.tgz", - "integrity": "sha512-pkqbPGtohJAvm4Dphs2M8xE29ggupihHdy1x84HNojZuMtFsHiUlRvqD24tM2+XmI+61LlfNceM3Wr7U5QES5g==", - "license": "MIT", - "dependencies": { - "@clack/core": "1.1.0", - "sisteransi": "^1.0.5" - } - }, - "node_modules/@ctrl/tinycolor": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.0.tgz", - "integrity": "sha512-kzyuwOAQnXJNLS9PSyrk0CWk35nWJW/zl/6KvnTBMFK65gm7U1/Z5BqjxeapjZCIhQcM/DsrEmcbRwDyXyXK4A==", - "license": "MIT", - "engines": { - "node": ">=14" - } - }, - "node_modules/@emnapi/runtime": { - "version": "1.9.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.9.1.tgz", - "integrity": "sha512-VYi5+ZVLhpgK4hQ0TAjiQiZ6ol0oe4mBx7mVv7IflsiEp0OWoVsp/+f9Vc1hOhE0TtkORVrI1GvzyreqpgWtkA==", - "license": "MIT", - "optional": true, - "dependencies": { - "tslib": "^2.4.0" - } - }, - "node_modules/@esbuild/aix-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.27.4.tgz", - "integrity": "sha512-cQPwL2mp2nSmHHJlCyoXgHGhbEPMrEEU5xhkcy3Hs/O7nGZqEpZ2sUtLaL9MORLtDfRvVl2/3PAuEkYZH0Ty8Q==", - "cpu": [ - "ppc64" - ], - "license": "MIT", - "optional": true, - "os": [ - "aix" - ], - "engines": { - "node": ">=18" - } - }, - "node_modules/@esbuild/android-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.27.4.tgz", - "integrity": "sha512-X9bUgvxiC8CHAGKYufLIHGXPJWnr0OCdR0anD2e21vdvgCI8lIfqFbnoeOz7lBjdrAGUhqLZLcQo6MLhTO2DKQ==", + "optionalDependencies": { + "@astrojs/compiler-binding-android-arm64": "0.5.1", + "@astrojs/compiler-binding-darwin-arm64": "0.5.1", + "@astrojs/compiler-binding-darwin-x64": "0.5.1", + "@astrojs/compiler-binding-linux-arm64-gnu": "0.5.1", + "@astrojs/compiler-binding-linux-arm64-musl": "0.5.1", + "@astrojs/compiler-binding-linux-x64-gnu": "0.5.1", + "@astrojs/compiler-binding-linux-x64-musl": "0.5.1", + "@astrojs/compiler-binding-wasm32-wasi": "0.5.1", + "@astrojs/compiler-binding-win32-arm64-msvc": "0.5.1", + "@astrojs/compiler-binding-win32-x64-msvc": "0.5.1" + } + }, + "node_modules/@astrojs/compiler-binding-android-arm64": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-android-arm64/-/compiler-binding-android-arm64-0.5.1.tgz", + "integrity": "sha512-nMb2nMIJtcWLNH+fV75Enmi+jp4tIOYOfsv4XtpgdOWkqc9P4uSSdK5u1xb340AzrXo5V18SxLl/nc3vcqtamQ==", "cpu": [ - "arm" + "arm64" ], "license": "MIT", "optional": true, @@ -300,381 +51,521 @@ "android" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/android-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.27.4.tgz", - "integrity": "sha512-gdLscB7v75wRfu7QSm/zg6Rx29VLdy9eTr2t44sfTW7CxwAtQghZ4ZnqHk3/ogz7xao0QAgrkradbBzcqFPasw==", + "node_modules/@astrojs/compiler-binding-darwin-arm64": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-arm64/-/compiler-binding-darwin-arm64-0.5.1.tgz", + "integrity": "sha512-5k4+nZ/WRt+dcVow4z7rBuI7iv9A58h/uQ91YJe1PrTivbcCkq3u2Ynx/AZJwII4CsZawBCeGpojkFjutZBhBQ==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/android-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.27.4.tgz", - "integrity": "sha512-PzPFnBNVF292sfpfhiyiXCGSn9HZg5BcAz+ivBuSsl6Rk4ga1oEXAamhOXRFyMcjwr2DVtm40G65N3GLeH1Lvw==", + "node_modules/@astrojs/compiler-binding-darwin-x64": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-darwin-x64/-/compiler-binding-darwin-x64-0.5.1.tgz", + "integrity": "sha512-0Buw+YST9Ff2EqQyhRMQujuFq/dijLw3zXWhsvI0dJ2pGrT38iVoxTEca2eBX/PfhlwkVWSRJEKO5gMqKHyeHg==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "android" + "darwin" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/darwin-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.27.4.tgz", - "integrity": "sha512-b7xaGIwdJlht8ZFCvMkpDN6uiSmnxxK56N2GDTMYPr2/gzvfdQN8rTfBsvVKmIVY/X7EM+/hJKEIbbHs9oA4tQ==", + "node_modules/@astrojs/compiler-binding-linux-arm64-gnu": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-gnu/-/compiler-binding-linux-arm64-gnu-0.5.1.tgz", + "integrity": "sha512-x5LRpyCoMKuu3INr18cn42YhwSOaRpp9b5Ny3eJJ/sWVeejC16Zeshu1JlvlSmEZACLeWxU5JM0RX6T6nI+YyA==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/darwin-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.27.4.tgz", - "integrity": "sha512-sR+OiKLwd15nmCdqpXMnuJ9W2kpy0KigzqScqHI3Hqwr7IXxBp3Yva+yJwoqh7rE8V77tdoheRYataNKL4QrPw==", + "node_modules/@astrojs/compiler-binding-linux-arm64-musl": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-arm64-musl/-/compiler-binding-linux-arm64-musl-0.5.1.tgz", + "integrity": "sha512-mL7tQEuDk3rGfaKmrO63nw6WBy1TPVioQGmVyjcAr+C+a/k3SVMYBytNSMqH9mZPT/Swj30JaZ2N842Z1NnOEQ==", "cpu": [ - "x64" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "darwin" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/freebsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.27.4.tgz", - "integrity": "sha512-jnfpKe+p79tCnm4GVav68A7tUFeKQwQyLgESwEAUzyxk/TJr4QdGog9sqWNcUbr/bZt/O/HXouspuQDd9JxFSw==", + "node_modules/@astrojs/compiler-binding-linux-x64-gnu": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-gnu/-/compiler-binding-linux-x64-gnu-0.5.1.tgz", + "integrity": "sha512-uLGc9jtxwiDe4cQyhNPQim4ECiB/YzbWlXssfLAv8r374kArfTwBHjwR2+j+XVyF71yCx5mF9YNRclRqdNBgeg==", "cpu": [ - "arm64" + "x64" ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/freebsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.27.4.tgz", - "integrity": "sha512-2kb4ceA/CpfUrIcTUl1wrP/9ad9Atrp5J94Lq69w7UwOMolPIGrfLSvAKJp0RTvkPPyn6CIWrNy13kyLikZRZQ==", + "node_modules/@astrojs/compiler-binding-linux-x64-musl": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-linux-x64-musl/-/compiler-binding-linux-x64-musl-0.5.1.tgz", + "integrity": "sha512-G0cywinaQUc8v1MrytvYKUX7svTDMmYtOCZfxjwKPbJi59zIqM+Ll0rWgO0GQWLZH3XoPYo19ZwlP3Xt9ep9YA==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.27.4.tgz", - "integrity": "sha512-aBYgcIxX/wd5n2ys0yESGeYMGF+pv6g0DhZr3G1ZG4jMfruU9Tl1i2Z+Wnj9/KjGz1lTLCcorqE2viePZqj4Eg==", + "node_modules/@astrojs/compiler-binding-wasm32-wasi": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-wasm32-wasi/-/compiler-binding-wasm32-wasi-0.5.1.tgz", + "integrity": "sha512-yMhLL8asmLydoAdnEZxAw6eLfNDs3YJl6jqn0pjPt6zrb2U5qIDLnii4/twThpIFODKkQ6YDpBRTOz/vSfZCxw==", "cpu": [ - "arm" + "wasm32" ], "license": "MIT", "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@emnapi/core": "1.11.3", + "@emnapi/runtime": "1.11.3", + "@napi-rs/wasm-runtime": "^1.2.4" + }, "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.27.4.tgz", - "integrity": "sha512-7nQOttdzVGth1iz57kxg9uCz57dxQLHWxopL6mYuYthohPKEK0vU0C3O21CcBK6KDlkYVcnDXY099HcCDXd9dA==", + "node_modules/@astrojs/compiler-binding-win32-arm64-msvc": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-arm64-msvc/-/compiler-binding-win32-arm64-msvc-0.5.1.tgz", + "integrity": "sha512-aClIG6poFt3Wf/bZykrEAX2nAnKvBHW6RXiKUa69QjBtWH0/FOlOAjnFcxRNlhej2AMWNYFx0o+LGjwMcH8P7Q==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.27.4.tgz", - "integrity": "sha512-oPtixtAIzgvzYcKBQM/qZ3R+9TEUd1aNJQu0HhGyqtx6oS7qTpvjheIWBbes4+qu1bNlo2V4cbkISr8q6gRBFA==", + "node_modules/@astrojs/compiler-binding-win32-x64-msvc": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-binding-win32-x64-msvc/-/compiler-binding-win32-x64-msvc-0.5.1.tgz", + "integrity": "sha512-/E6uBCagY70YpWqssBC+/FuniNSloeQPRgj7l3LDWiqjxIyTLnIgX1sTt18FU7jrigetpSnC9cHAM6b6IxSgPw==", "cpu": [ - "ia32" + "x64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ], "engines": { - "node": ">=18" + "node": "^20.19.0 || >=22.12.0" } }, - "node_modules/@esbuild/linux-loong64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.27.4.tgz", - "integrity": "sha512-8mL/vh8qeCoRcFH2nM8wm5uJP+ZcVYGGayMavi8GmRJjuI3g1v6Z7Ni0JJKAJW+m0EtUuARb6Lmp4hMjzCBWzA==", - "cpu": [ - "loong64" - ], + "node_modules/@astrojs/compiler-rs": { + "version": "0.5.1", + "resolved": "https://registry.npmjs.org/@astrojs/compiler-rs/-/compiler-rs-0.5.1.tgz", + "integrity": "sha512-1k1acnX7DepIiea9U9/Y+d1hdAFfylu1ZQQkkYgRSwBOBAsXInolyZuh/2tax+83vu/A+hOYP6P3+265RvMTvw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@astrojs/compiler-binding": "0.5.1" + }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, - "node_modules/@esbuild/linux-mips64el": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.27.4.tgz", - "integrity": "sha512-1RdrWFFiiLIW7LQq9Q2NES+HiD4NyT8Itj9AUeCl0IVCA459WnPhREKgwrpaIfTOe+/2rdntisegiPWn/r/aAw==", - "cpu": [ - "mips64el" - ], + "node_modules/@astrojs/internal-helpers": { + "version": "0.12.0", + "resolved": "https://registry.npmjs.org/@astrojs/internal-helpers/-/internal-helpers-0.12.0.tgz", + "integrity": "sha512-pKPuwCDtO0L7zgmt1JGd9X4Aes1+0SX3IEBSqBNPHsScYdPMlfmed3Y7DTbXyF7E+8ylnb563o/0OzGQPOFehw==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "@types/hast": "^3.0.4", + "@types/mdast": "^4.0.4", + "picomatch": "^4.0.4", + "retext-smartypants": "^6.2.0", + "shiki": "^4.0.2", + "smol-toml": "^1.8.0", + "unified": "^11.0.5", + "yaml": "^2.9.1" + } + }, + "node_modules/@astrojs/markdown-remark": { + "version": "7.3.2", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-remark/-/markdown-remark-7.3.2.tgz", + "integrity": "sha512-ubszw8SVEI4OHR5Y+0jTbkmzG8xatrW47iAkQNqH7wUM1bky7DOnr51FGgoUdXt/fmKpUl7sRRX9talmU64Y+g==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.12.0", + "@astrojs/prism": "4.0.2", + "@mdx-js/mdx": "^3.1.1", + "acorn": "^8.16.0", + "estree-util-visit": "^2.0.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "hast-util-to-html": "^9.0.5", + "hast-util-to-text": "^4.0.2", + "mdast-util-definitions": "^6.0.0", + "rehype-raw": "^7.0.0", + "rehype-stringify": "^10.0.1", + "remark-gfm": "^4.0.1", + "remark-parse": "^11.0.0", + "remark-rehype": "^11.1.2", + "remark-smartypants": "^3.0.2", + "source-map": "^0.7.6", + "unified": "^11.0.5", + "unist-util-remove-position": "^5.0.0", + "unist-util-visit": "^5.1.0", + "unist-util-visit-parents": "^6.0.2", + "vfile": "^6.0.3" + } + }, + "node_modules/@astrojs/markdown-satteri": { + "version": "0.4.3", + "resolved": "https://registry.npmjs.org/@astrojs/markdown-satteri/-/markdown-satteri-0.4.3.tgz", + "integrity": "sha512-PFbEHPnwUlLvpXd7RXrrKM+j12akF4bG+DSuNuoN+3eiwjOwp4HaPi+QQdbkBV7QEYXmzyKca50OJsmGxhNIUA==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.12.0", + "@astrojs/prism": "4.0.2", + "github-slugger": "^2.0.0", + "satteri": "^0.10.3" + } + }, + "node_modules/@astrojs/mdx": { + "version": "8.0.3", + "resolved": "https://registry.npmjs.org/@astrojs/mdx/-/mdx-8.0.3.tgz", + "integrity": "sha512-SmwnVrGZSohDG0B9LXiAPbR6fe9TOumli9Vw/9zo815xGTeB1T3Fqt6HEClmBsVwIKFg/ccos4TgjZAedvg7zA==", + "license": "MIT", + "dependencies": { + "@astrojs/internal-helpers": "0.12.0", + "@astrojs/markdown-satteri": "0.4.3", + "es-module-lexer": "^3.0.2" + }, "engines": { - "node": ">=18" + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0", + "@astrojs/markdown-satteri": "^0.4.0", + "astro": "^7.2.10" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } } }, - "node_modules/@esbuild/linux-ppc64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.27.4.tgz", - "integrity": "sha512-tLCwNG47l3sd9lpfyx9LAGEGItCUeRCWeAx6x2Jmbav65nAwoPXfewtAdtbtit/pJFLUWOhpv0FpS6GQAmPrHA==", - "cpu": [ - "ppc64" - ], + "node_modules/@astrojs/prism": { + "version": "4.0.2", + "resolved": "https://registry.npmjs.org/@astrojs/prism/-/prism-4.0.2.tgz", + "integrity": "sha512-KTivpmnz6lDsC6o9H4+DNm2SrE/GHzw8cNAvEJwAvUT+eoaEnn/4NtbDNfRRaxaJHdp15gf+tfHAWiXR4wB3BA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "prismjs": "^1.30.0" + }, "engines": { - "node": ">=18" + "node": ">=22.12.0" } }, - "node_modules/@esbuild/linux-riscv64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.27.4.tgz", - "integrity": "sha512-BnASypppbUWyqjd1KIpU4AUBiIhVr6YlHx/cnPgqEkNoVOhHg+YiSVxM1RLfiy4t9cAulbRGTNCKOcqHrEQLIw==", - "cpu": [ - "riscv64" - ], + "node_modules/@astrojs/sitemap": { + "version": "3.7.4", + "resolved": "https://registry.npmjs.org/@astrojs/sitemap/-/sitemap-3.7.4.tgz", + "integrity": "sha512-LbKNC24bdUWcQf/pThB6qLlSqHojxGjZDURIzFocY8rlWnAn2t74nnhnK6S5x0NHriHoAduLEpVjRykmeGiVvA==", "license": "MIT", - "optional": true, - "os": [ - "linux" - ], + "dependencies": { + "sitemap": "^9.0.0", + "zod": "^4.3.6" + } + }, + "node_modules/@astrojs/starlight": { + "version": "0.42.6", + "resolved": "https://registry.npmjs.org/@astrojs/starlight/-/starlight-0.42.6.tgz", + "integrity": "sha512-Cfs7GdgzgNRCauDxQVsFUVL1thrGHK/rQdbv/SURv/xhGBXx+j5ypOHW9ENPraQxbIzUC8KAnJV7L9mvEVRuyQ==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-satteri": "^0.4.0", + "@astrojs/mdx": "^8.0.0", + "@astrojs/sitemap": "^3.7.3", + "@pagefind/default-ui": "^1.3.0", + "@types/hast": "^3.0.5", + "@types/js-yaml": "^4.0.9", + "@types/mdast": "^4.0.4", + "astro-expressive-code": "^0.44.0", + "bcp-47": "^2.1.0", + "hast-util-format": "^1.1.0", + "hast-util-select": "^6.0.4", + "hast-util-to-html": "^9.0.5", + "hast-util-to-string": "^3.0.1", + "hastscript": "^9.0.1", + "i18next": "^26.0.7", + "js-yaml": "^4.1.1", + "klona": "^2.0.6", + "magic-string": "^1.2.3", + "mdast-util-directive": "^3.1.0", + "mdast-util-to-markdown": "^2.1.2", + "mdast-util-to-string": "^4.0.0", + "pagefind": "^1.5.2", + "remark-directive": "^4.0.0", + "satteri": "^0.10.3", + "ultrahtml": "^1.6.0", + "unified": "^11.0.5", + "unist-util-visit": "^5.1.0", + "vfile": "^6.0.3" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0", + "astro": "^7.2.10" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } + } + }, + "node_modules/@astrojs/telemetry": { + "version": "3.3.3", + "resolved": "https://registry.npmjs.org/@astrojs/telemetry/-/telemetry-3.3.3.tgz", + "integrity": "sha512-C1TLn5sPJr0x4vk56piHWKbnqlEB8BKyte5Y45V02U+D7BGO5eMqZDH5aPjnkXQWJggvmsTXxH03QMZ9NgWLzQ==", + "license": "MIT", + "dependencies": { + "ci-info": "^4.4.0", + "dset": "^3.1.4", + "is-docker": "^4.0.0", + "package-manager-detector": "^1.6.0" + }, "engines": { - "node": ">=18" + "node": "18.20.8 || ^20.3.0 || >=22.0.0" } }, - "node_modules/@esbuild/linux-s390x": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.27.4.tgz", - "integrity": "sha512-+eUqgb/Z7vxVLezG8bVB9SfBie89gMueS+I0xYh2tJdw3vqA/0ImZJ2ROeWwVJN59ihBeZ7Tu92dF/5dy5FttA==", + "node_modules/@babel/helper-string-parser": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.29.7.tgz", + "integrity": "sha512-Pb5ijPrZ89GDH8223L4UP8i6QApWxs04RbPQJTeWDV0/keR2E36MeKnyr6LYmUUvqRRI+Iv87SuF1W6ErINzYw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.29.7", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.29.7.tgz", + "integrity": "sha512-qehxGkRj55h/ff8EMaJ+cYhyaKlHIxqYDn682wQD7RNp9UujOQsHog2uS0r2vzr4pW+sXf90NeeayjcNaX3fFg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.9", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.9.tgz", + "integrity": "sha512-CjXrNHTnvqBVqHgdBysY3vk2T8tpJHb5/RMeHJBTyVa9xgugCB0CJTx/3oO8RV2QRQP391RWpB7D6hLjm8V9uA==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.8" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.8", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.8.tgz", + "integrity": "sha512-Vj1jF3cPfxg7OAfoI7QnVKLoILlm2JF9pnVHrX8qx7AHMiYWT+NDAA7jChlNgRS4WTLc/fD1lXLmPixluj+3Gg==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.29.7", + "@babel/helper-validator-identifier": "^7.29.7" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@bruits/satteri-darwin-arm64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-arm64/-/satteri-darwin-arm64-0.10.5.tgz", + "integrity": "sha512-27KTVl4TJkVahMy/ohyA7qd4938G5UNneFUz/PsScYfpIhj0IVAS23mpcJXdPF44sa6nva198lmV/cKIb2YPyA==", "cpu": [ - "s390x" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "darwin" + ] }, - "node_modules/@esbuild/linux-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.27.4.tgz", - "integrity": "sha512-S5qOXrKV8BQEzJPVxAwnryi2+Iq5pB40gTEIT69BQONqR7JH1EPIcQ/Uiv9mCnn05jff9umq/5nqzxlqTOg9NA==", + "node_modules/@bruits/satteri-darwin-x64": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-darwin-x64/-/satteri-darwin-x64-0.10.5.tgz", + "integrity": "sha512-IjnLe3nKspq6qaeqGgjT7MT8VrTV74yWRlaag7ZdNsI8TDAYZ0iPxMCo+9KQZHUk5EyVB+reBI/PFWL5KuFw9Q==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "linux" - ], - "engines": { - "node": ">=18" - } + "darwin" + ] }, - "node_modules/@esbuild/netbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.27.4.tgz", - "integrity": "sha512-xHT8X4sb0GS8qTqiwzHqpY00C95DPAq7nAwX35Ie/s+LO9830hrMd3oX0ZMKLvy7vsonee73x0lmcdOVXFzd6Q==", + "node_modules/@bruits/satteri-linux-arm64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-gnu/-/satteri-linux-arm64-gnu-0.10.5.tgz", + "integrity": "sha512-glkYXZCJywjP13v67eAyAMSJdF+ncvEbYvgi/wOtffL9tQ27lr/zsyzUfgs+ovjJ9d8JNQKiXeiArJcX8PJL9w==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/netbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.27.4.tgz", - "integrity": "sha512-RugOvOdXfdyi5Tyv40kgQnI0byv66BFgAqjdgtAKqHoZTbTF2QqfQrFwa7cHEORJf6X2ht+l9ABLMP0dnKYsgg==", + "node_modules/@bruits/satteri-linux-arm64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-arm64-musl/-/satteri-linux-arm64-musl-0.10.5.tgz", + "integrity": "sha512-yWdgG1g17Nh2QyGVlFUxGRa3FEFwiMcpZEyMNWkbM3deC94cmVc+/i9OuyFpdKuWo3GkgoCtYVOoxk1uCnCZIA==", "cpu": [ - "x64" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "netbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/openbsd-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.27.4.tgz", - "integrity": "sha512-2MyL3IAaTX+1/qP0O1SwskwcwCoOI4kV2IBX1xYnDDqthmq5ArrW94qSIKCAuRraMgPOmG0RDTA74mzYNQA9ow==", + "node_modules/@bruits/satteri-linux-x64-gnu": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-gnu/-/satteri-linux-x64-gnu-0.10.5.tgz", + "integrity": "sha512-FVaLoPT1fBgGl0J+AYebyyXJYBachGl8Oyyrf1lye4RTqCB4S0Gwkj1uM9RJyThUOvx5VUmAT1CnNh1SFHA+kw==", "cpu": [ - "arm64" + "x64" ], "license": "MIT", "optional": true, "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/openbsd-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.27.4.tgz", - "integrity": "sha512-u8fg/jQ5aQDfsnIV6+KwLOf1CmJnfu1ShpwqdwC0uA7ZPwFws55Ngc12vBdeUdnuWoQYx/SOQLGDcdlfXhYmXQ==", + "node_modules/@bruits/satteri-linux-x64-musl": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-linux-x64-musl/-/satteri-linux-x64-musl-0.10.5.tgz", + "integrity": "sha512-EHpVAx2bqW3GINHTKkljtxVfQmVDGWIuwOYOP5YghTj+0PkBa2o8oKPRtQ9Kbsr1Fye8jtUcDjhwj2jMNugZKg==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "openbsd" - ], - "engines": { - "node": ">=18" - } + "linux" + ] }, - "node_modules/@esbuild/openharmony-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/openharmony-arm64/-/openharmony-arm64-0.27.4.tgz", - "integrity": "sha512-JkTZrl6VbyO8lDQO3yv26nNr2RM2yZzNrNHEsj9bm6dOwwu9OYN28CjzZkH57bh4w0I2F7IodpQvUAEd1mbWXg==", + "node_modules/@bruits/satteri-wasm32-wasi": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-wasm32-wasi/-/satteri-wasm32-wasi-0.10.5.tgz", + "integrity": "sha512-ypz8c/Zmipxp4IoeDa228Gstv6TLzVmNs3yC6wKCoNSOjx1iwpgzu87Y3hTkXFdwChVGU85qeUDuOIarGUZQLw==", "cpu": [ - "arm64" + "wasm32" ], "license": "MIT", "optional": true, - "os": [ - "openharmony" - ], + "dependencies": { + "@emnapi/core": "1.11.1", + "@emnapi/runtime": "1.11.1", + "@napi-rs/wasm-runtime": "^1.2.3" + }, "engines": { - "node": ">=18" + "node": ">=14.0.0" } }, - "node_modules/@esbuild/sunos-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.27.4.tgz", - "integrity": "sha512-/gOzgaewZJfeJTlsWhvUEmUG4tWEY2Spp5M20INYRg2ZKl9QPO3QEEgPeRtLjEWSW8FilRNacPOg8R1uaYkA6g==", - "cpu": [ - "x64" - ], + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/core": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", + "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", "license": "MIT", "optional": true, - "os": [ - "sunos" - ], - "engines": { - "node": ">=18" + "dependencies": { + "@emnapi/wasi-threads": "1.2.2", + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-arm64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.27.4.tgz", - "integrity": "sha512-Z9SExBg2y32smoDQdf1HRwHRt6vAHLXcxD2uGgO/v2jK7Y718Ix4ndsbNMU/+1Qiem9OiOdaqitioZwxivhXYg==", - "cpu": [ - "arm64" - ], + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/runtime": { + "version": "1.11.1", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", + "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", "license": "MIT", "optional": true, - "os": [ - "win32" - ], - "engines": { - "node": ">=18" + "dependencies": { + "tslib": "^2.4.0" + } + }, + "node_modules/@bruits/satteri-wasm32-wasi/node_modules/@emnapi/wasi-threads": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", + "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" } }, - "node_modules/@esbuild/win32-ia32": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.27.4.tgz", - "integrity": "sha512-DAyGLS0Jz5G5iixEbMHi5KdiApqHBWMGzTtMiJ72ZOLhbu/bzxgAe8Ue8CTS3n3HbIUHQz/L51yMdGMeoxXNJw==", + "node_modules/@bruits/satteri-win32-arm64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-arm64-msvc/-/satteri-win32-arm64-msvc-0.10.5.tgz", + "integrity": "sha512-siTV88nb0LRqNpkL2gXboqCwVdq95sLtzMHS1/3eONV2gLbB3NAK46wmSMvCO/yquBvI2lvaFIfd8P12ecsxBw==", "cpu": [ - "ia32" + "arm64" ], "license": "MIT", "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@esbuild/win32-x64": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.27.4.tgz", - "integrity": "sha512-+knoa0BDoeXgkNvvV1vvbZX4+hizelrkwmGJBdT17t8FNPwG2lKemmuMZlmaNQ3ws3DKKCxpb4zRZEIp3UxFCg==", + "node_modules/@bruits/satteri-win32-x64-msvc": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/@bruits/satteri-win32-x64-msvc/-/satteri-win32-x64-msvc-0.10.5.tgz", + "integrity": "sha512-C3IfPvfvMXmlzBxaMPKFS1XiuV9pu2mC7YqkPk7PSvTgPZ8gbdASIpHpztDLvTTQjqZ0z1Ol8tK5X+V6XXC0wQ==", "cpu": [ "x64" ], @@ -682,131 +573,131 @@ "optional": true, "os": [ "win32" - ], - "engines": { - "node": ">=18" - } + ] }, - "node_modules/@expressive-code/core": { - "version": "0.41.7", - "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.41.7.tgz", - "integrity": "sha512-ck92uZYZ9Wba2zxkiZLsZGi9N54pMSAVdrI9uW3Oo9AtLglD5RmrdTwbYPCT2S/jC36JGB2i+pnQtBm/Ib2+dg==", + "node_modules/@capsizecss/unpack": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/@capsizecss/unpack/-/unpack-4.0.1.tgz", + "integrity": "sha512-CuNiSqg7+e1cO/GjffyMOm5Tt2jUF9CWHHnvQ/UkqvtkGfHdgwEC0wpmq7fkN3gxwpRnrAN0WzO3vREKmNolMQ==", "license": "MIT", "dependencies": { - "@ctrl/tinycolor": "^4.0.4", - "hast-util-select": "^6.0.2", - "hast-util-to-html": "^9.0.1", - "hast-util-to-text": "^4.0.1", - "hastscript": "^9.0.0", - "postcss": "^8.4.38", - "postcss-nested": "^6.0.1", - "unist-util-visit": "^5.0.0", - "unist-util-visit-parents": "^6.0.1" + "fontkitten": "^1.0.3" + }, + "engines": { + "node": ">=18" } }, - "node_modules/@expressive-code/plugin-frames": { - "version": "0.41.7", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.41.7.tgz", - "integrity": "sha512-diKtxjQw/979cTglRFaMCY/sR6hWF0kSMg8jsKLXaZBSfGS0I/Hoe7Qds3vVEgeoW+GHHQzMcwvgx/MOIXhrTA==", + "node_modules/@clack/core": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@clack/core/-/core-1.5.1.tgz", + "integrity": "sha512-iHTrHA8MtVuLl2TfZySmcKv1qO2PoyC9Z7pfSDozEuV5vtY3/wcOPKJXlqJ5Oq2Cx5DDGQGAMVx6HZfRRoVEbQ==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.41.7" + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" } }, - "node_modules/@expressive-code/plugin-shiki": { - "version": "0.41.7", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.41.7.tgz", - "integrity": "sha512-DL605bLrUOgqTdZ0Ot5MlTaWzppRkzzqzeGEu7ODnHF39IkEBbFdsC7pbl3LbUQ1DFtnfx6rD54k/cdofbW6KQ==", + "node_modules/@clack/prompts": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/@clack/prompts/-/prompts-1.8.1.tgz", + "integrity": "sha512-dlT1m5e/0yUL0kRNcQn7yGLVThkgbB0Ga/1AmfDDC/8ik6AIiSf2QLQO2zPYvefsHP0aFgxO93cVLCCfDp7kzQ==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.41.7", - "shiki": "^3.2.2" + "@clack/core": "1.5.1", + "fast-string-width": "^3.0.2", + "fast-wrap-ansi": "^0.2.0", + "sisteransi": "^1.0.5" + }, + "engines": { + "node": ">= 20.12.0" } }, - "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/core": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-3.23.0.tgz", - "integrity": "sha512-NSWQz0riNb67xthdm5br6lAkvpDJRTgB36fxlo37ZzM2yq0PQFFzbd8psqC2XMPgCzo1fW6cVi18+ArJ44wqgA==", + "node_modules/@ctrl/tinycolor": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/@ctrl/tinycolor/-/tinycolor-4.2.1.tgz", + "integrity": "sha512-Vh5uy9Y1JBMId6J1f68TMXnZrTCvNdo9Fu2k0d1YnJngR+sM5kWTKl441c/3TFs+cG0NWOjRWgQ/iaWhbYx+7A==", "license": "MIT", - "dependencies": { - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", - "hast-util-to-html": "^9.0.5" + "engines": { + "node": ">=14" } }, - "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/engine-javascript": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-3.23.0.tgz", - "integrity": "sha512-aHt9eiGFobmWR5uqJUViySI1bHMqrAgamWE1TYSUoftkAeCCAiGawPMwM+VCadylQtF4V3VNOZ5LmfItH5f3yA==", + "node_modules/@emnapi/core": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", "license": "MIT", + "optional": true, "dependencies": { - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" + "@emnapi/wasi-threads": "1.2.3", + "tslib": "^2.4.0" } }, - "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/engine-oniguruma": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-3.23.0.tgz", - "integrity": "sha512-1nWINwKXxKKLqPibT5f4pAFLej9oZzQTsby8942OTlsJzOBZ0MWKiwzMsd+jhzu8YPCHAswGnnN1YtQfirL35g==", + "node_modules/@emnapi/runtime": { + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "license": "MIT", + "optional": true, "dependencies": { - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2" + "tslib": "^2.4.0" } }, - "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/langs": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-3.23.0.tgz", - "integrity": "sha512-2Ep4W3Re5aB1/62RSYQInK9mM3HsLeB91cHqznAJMuylqjzNVAVCMnNWRHFtcNHXsoNRayP9z1qj4Sq3nMqYXg==", + "node_modules/@emnapi/wasi-threads": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "license": "MIT", + "optional": true, "dependencies": { - "@shikijs/types": "3.23.0" + "tslib": "^2.4.0" } }, - "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/themes": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-3.23.0.tgz", - "integrity": "sha512-5qySYa1ZgAT18HR/ypENL9cUSGOeI2x+4IvYJu4JgVJdizn6kG4ia5Q1jDEOi7gTbN4RbuYtmHh0W3eccOrjMA==", + "node_modules/@expressive-code/core": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/core/-/core-0.44.2.tgz", + "integrity": "sha512-Yw5KonQCD1HoOO2P89y4wifqS9qrJcGVY0LkVMUsVZjhi++bhz4WwQ37inenKFNOEVo50P7cjoVsCdaVnUbq8w==", "license": "MIT", "dependencies": { - "@shikijs/types": "3.23.0" + "@ctrl/tinycolor": "^4.0.4", + "hast-util-select": "^6.0.2", + "hast-util-to-html": "^9.0.1", + "hast-util-to-text": "^4.0.1", + "hastscript": "^9.0.0", + "postcss": "^8.4.38", + "postcss-nested": "^6.0.1", + "unist-util-visit": "^5.0.0", + "unist-util-visit-parents": "^6.0.1" } }, - "node_modules/@expressive-code/plugin-shiki/node_modules/@shikijs/types": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-3.23.0.tgz", - "integrity": "sha512-3JZ5HXOZfYjsYSk0yPwBrkupyYSLpAE26Qc0HLghhZNGTZg/SKxXIIgoxOpmmeQP0RRSDJTk1/vPfw9tbw+jSQ==", + "node_modules/@expressive-code/plugin-frames": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-frames/-/plugin-frames-0.44.2.tgz", + "integrity": "sha512-gVLm6TQMNIRWIaM7o+mCtxkGNjLZY5/2faSTLoXeo9wv/Pkj/0QAtZyXPVPrRsPARqiipATNFmfvQFLJudxPHQ==", "license": "MIT", "dependencies": { - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@expressive-code/core": "^0.44.2" } }, - "node_modules/@expressive-code/plugin-shiki/node_modules/shiki": { - "version": "3.23.0", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-3.23.0.tgz", - "integrity": "sha512-55Dj73uq9ZXL5zyeRPzHQsK7Nbyt6Y10k5s7OjuFZGMhpp4r/rsLBH0o/0fstIzX1Lep9VxefWljK/SKCzygIA==", + "node_modules/@expressive-code/plugin-shiki": { + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-shiki/-/plugin-shiki-0.44.2.tgz", + "integrity": "sha512-ebWIvCDzXvjyD8rH6T6DfytYJw1GoArLR4nusIAE4VxXrCQf3d+owytiv010KU/dUg+SRLZskDn0N+HaFXeuSg==", "license": "MIT", "dependencies": { - "@shikijs/core": "3.23.0", - "@shikijs/engine-javascript": "3.23.0", - "@shikijs/engine-oniguruma": "3.23.0", - "@shikijs/langs": "3.23.0", - "@shikijs/themes": "3.23.0", - "@shikijs/types": "3.23.0", - "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@expressive-code/core": "^0.44.2", + "shiki": "^4.0.2" } }, "node_modules/@expressive-code/plugin-text-markers": { - "version": "0.41.7", - "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.41.7.tgz", - "integrity": "sha512-Ewpwuc5t6eFdZmWlFyeuy3e1PTQC0jFvw2Q+2bpcWXbOZhPLsT7+h8lsSIJxb5mS7wZko7cKyQ2RLYDyK6Fpmw==", + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/@expressive-code/plugin-text-markers/-/plugin-text-markers-0.44.2.tgz", + "integrity": "sha512-SNI7EgUSiADqbqUzzMbj1ZeC/r2pa5eg3KyFOGAd3a7EsIA4qzjXQLp+ODWQtHVDKFCdGs4J3ovSJUqvKIK96g==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.41.7" + "@expressive-code/core": "^0.44.2" } }, "node_modules/@img/colour": { @@ -819,9 +710,9 @@ } }, "node_modules/@img/sharp-darwin-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.34.5.tgz", - "integrity": "sha512-imtQ3WMJXbMY4fxb/Ndp6HBTNVtWCUI0WdobyheGf5+ad6xX8VIDO8u2xE4qc/fr08CKG/7dDseFtn6M6g/r3w==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-arm64/-/sharp-darwin-arm64-0.35.5.tgz", + "integrity": "sha512-QRUlFQ0WxvdWyqqG/WtI3iupfD5rBzmCHXSdPsY91sAtVtTo7Q4cb6zOccZ3gqEqkr0f1As1ehLqmEpDsRf+lg==", "cpu": [ "arm64" ], @@ -831,19 +722,19 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-arm64": "1.2.4" + "@img/sharp-libvips-darwin-arm64": "1.3.4" } }, "node_modules/@img/sharp-darwin-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.34.5.tgz", - "integrity": "sha512-YNEFAF/4KQ/PeW0N+r+aVVsoIY0/qxxikF2SWdp+NRkmMB7y9LBZAVqQ4yhGCm/H3H270OSykqmQMKLBhBJDEw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-darwin-x64/-/sharp-darwin-x64-0.35.5.tgz", + "integrity": "sha512-+BR255RhDlpygUpOc/Jdt1nT6DQ3XG/ERo5wbcdOf5Q320dKtPCKPLR1LJs9VGXRaMa8l1uUa0tkCNOXiAxZUw==", "cpu": [ "x64" ], @@ -853,19 +744,38 @@ "darwin" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-darwin-x64": "1.2.4" + "@img/sharp-libvips-darwin-x64": "1.3.4" + } + }, + "node_modules/@img/sharp-freebsd-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-freebsd-wasm32/-/sharp-freebsd-wasm32-0.35.5.tgz", + "integrity": "sha512-Y/z91nEZ4uIBX5X3nfTovjU9lHNKFYbL2lpHCLVNmXQK03VIZvXBBt0KxbPGp2SdGSF+2mQU4e+hQaWOt86iAw==", + "license": "Apache-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "dependencies": { + "@img/sharp-wasm32": "0.35.5" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-libvips-darwin-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.2.4.tgz", - "integrity": "sha512-zqjjo7RatFfFoP0MkQ51jfuFZBnVE2pRiaydKJ1G/rHZvnsrHAOcQALIi9sA5co5xenQdTugCvtb1cuf78Vf4g==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-arm64/-/sharp-libvips-darwin-arm64-1.3.4.tgz", + "integrity": "sha512-5R89nBYiRdUlSWJxPhO+GVtaXzXSxKnRu/xqMn3KTA3L9EB9Oy/P+Nn2f2vlhPuUdy/Zusb2DarbyTpGCfEDuw==", "cpu": [ "arm64" ], @@ -879,9 +789,9 @@ } }, "node_modules/@img/sharp-libvips-darwin-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.2.4.tgz", - "integrity": "sha512-1IOd5xfVhlGwX+zXv2N93k0yMONvUlANylbJw1eTah8K/Jtpi15KC+WSiaX/nBmbm2HxRM1gZ0nSdjSsrZbGKg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-darwin-x64/-/sharp-libvips-darwin-x64-1.3.4.tgz", + "integrity": "sha512-iR2OKH80yi0U+dUplyh3/xdpFvps6YkCwsXenIJxqxR1v9o+xtKTGbS9H7cps+2Vxjc8B1j96p75NmTGjIhtpQ==", "cpu": [ "x64" ], @@ -895,9 +805,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.2.4.tgz", - "integrity": "sha512-bFI7xcKFELdiNCVov8e44Ia4u2byA+l3XtsAj+Q8tfCwO6BQ8iDojYdvoPMqsKDkuoOo+X6HZA0s0q11ANMQ8A==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm/-/sharp-libvips-linux-arm-1.3.4.tgz", + "integrity": "sha512-LmRtTsOHuvM2+wlO2Db37dx5MiZhB0FvSunciw48YjdOkZz9KAiRbm8ujeMOA1INqmei5NapFxYEK1D1ZSidmw==", "cpu": [ "arm" ], @@ -911,9 +821,9 @@ } }, "node_modules/@img/sharp-libvips-linux-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.2.4.tgz", - "integrity": "sha512-excjX8DfsIcJ10x1Kzr4RcWe1edC9PquDRRPx3YVCvQv+U5p7Yin2s32ftzikXojb1PIFc/9Mt28/y+iRklkrw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-arm64/-/sharp-libvips-linux-arm64-1.3.4.tgz", + "integrity": "sha512-Y3dgX/6lE2QhQb+Gxy0WZxfg9MEm/JBjamZpS2IklP7xIQoKN4hzAm7KcMVGtaVDt3neE9OKBC7vAfonA/Lr1A==", "cpu": [ "arm64" ], @@ -927,9 +837,9 @@ } }, "node_modules/@img/sharp-libvips-linux-ppc64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.2.4.tgz", - "integrity": "sha512-FMuvGijLDYG6lW+b/UvyilUWu5Ayu+3r2d1S8notiGCIyYU/76eig1UfMmkZ7vwgOrzKzlQbFSuQfgm7GYUPpA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-ppc64/-/sharp-libvips-linux-ppc64-1.3.4.tgz", + "integrity": "sha512-Le6boB8Tai0Nis+gIxIpKx68UDVVIqdR8Tin5Yf1z2LJJQLDJvCDRqRu+jC2qCoD+eIomonmOwB4smBRxfVpYQ==", "cpu": [ "ppc64" ], @@ -943,9 +853,9 @@ } }, "node_modules/@img/sharp-libvips-linux-riscv64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.2.4.tgz", - "integrity": "sha512-oVDbcR4zUC0ce82teubSm+x6ETixtKZBh/qbREIOcI3cULzDyb18Sr/Wcyx7NRQeQzOiHTNbZFF1UwPS2scyGA==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-riscv64/-/sharp-libvips-linux-riscv64-1.3.4.tgz", + "integrity": "sha512-aHkkIEHPRdQEegJN20MLmGtxYD9R2wQr3Cwpddnu5+YKMt6Uzax7S9h5gpZTo8wyrGuZSlfQ63OevL5mTyOC7Q==", "cpu": [ "riscv64" ], @@ -959,9 +869,9 @@ } }, "node_modules/@img/sharp-libvips-linux-s390x": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.2.4.tgz", - "integrity": "sha512-qmp9VrzgPgMoGZyPvrQHqk02uyjA0/QrTO26Tqk6l4ZV0MPWIW6LTkqOIov+J1yEu7MbFQaDpwdwJKhbJvuRxQ==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-s390x/-/sharp-libvips-linux-s390x-1.3.4.tgz", + "integrity": "sha512-ra/mB6MikESDUO7Yg+Mi95bFBb9GsObURuhnOv3OqknjGe9sZrG8tCe9q0xSIGrtLgvgw0gKnFWcK4blSgQOuQ==", "cpu": [ "s390x" ], @@ -975,9 +885,9 @@ } }, "node_modules/@img/sharp-libvips-linux-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.2.4.tgz", - "integrity": "sha512-tJxiiLsmHc9Ax1bz3oaOYBURTXGIRDODBqhveVHonrHJ9/+k89qbLl0bcJns+e4t4rvaNBxaEZsFtSfAdquPrw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linux-x64/-/sharp-libvips-linux-x64-1.3.4.tgz", + "integrity": "sha512-GJ//SSXbnwSDes02umB3nDJLFcQzw8a18V8fyhqr6tV515tOEMdImjjxj1AoafMRz56F3PHgftnj1QEKSU1zkw==", "cpu": [ "x64" ], @@ -991,9 +901,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-arm64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.2.4.tgz", - "integrity": "sha512-FVQHuwx1IIuNow9QAbYUzJ+En8KcVm9Lk5+uGUQJHaZmMECZmOlix9HnH7n1TRkXMS0pGxIJokIVB9SuqZGGXw==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-arm64/-/sharp-libvips-linuxmusl-arm64-1.3.4.tgz", + "integrity": "sha512-hvulFwtjUcagsis6BBxHwGFwWoNZjgYmULGVrZcyfNbjA8hKILbRxGg15/7w5HDyXHXUos/j6baAWqnCyQ2DWA==", "cpu": [ "arm64" ], @@ -1007,9 +917,9 @@ } }, "node_modules/@img/sharp-libvips-linuxmusl-x64": { - "version": "1.2.4", - "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.2.4.tgz", - "integrity": "sha512-+LpyBk7L44ZIXwz/VYfglaX/okxezESc6UxDSoyo2Ks6Jxc4Y7sGjpgU9s4PMgqgjj1gZCylTieNamqA1MF7Dg==", + "version": "1.3.4", + "resolved": "https://registry.npmjs.org/@img/sharp-libvips-linuxmusl-x64/-/sharp-libvips-linuxmusl-x64-1.3.4.tgz", + "integrity": "sha512-6zXKeE/p39I1AmA3cJG35eyBGNqNddLnUXjhwBnsGjFPWqf5VKkDBEqaEkPDoTEtkxwi2vv8Tcr2mDyP4So7Fg==", "cpu": [ "x64" ], @@ -1023,9 +933,9 @@ } }, "node_modules/@img/sharp-linux-arm": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.34.5.tgz", - "integrity": "sha512-9dLqsvwtg1uuXBGZKsxem9595+ujv0sJ6Vi8wcTANSFpwV/GONat5eCkzQo/1O6zRIkh0m/8+5BjrRr7jDUSZw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm/-/sharp-linux-arm-0.35.5.tgz", + "integrity": "sha512-LEaXK2WdXVK5ykcw0buWyPMsmLLL2vpHLD6yrNSW+JGEL3BZPA4tpKN6iaMc4AxTTAoaX/sU1rOL51lcIz48ZQ==", "cpu": [ "arm" ], @@ -1035,19 +945,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm": "1.2.4" + "@img/sharp-libvips-linux-arm": "1.3.4" } }, "node_modules/@img/sharp-linux-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.34.5.tgz", - "integrity": "sha512-bKQzaJRY/bkPOXyKx5EVup7qkaojECG6NLYswgktOZjaXecSAeCWiZwwiFf3/Y+O1HrauiE3FVsGxFg8c24rZg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-arm64/-/sharp-linux-arm64-0.35.5.tgz", + "integrity": "sha512-LYVx5JTsOM2CBzmxreh+nl64/3H6Xb09iSLknqH47z2T2DFFxDeFLP5y4dJwe6H7uGQlHPyEEtIqyo3DYsRwdQ==", "cpu": [ "arm64" ], @@ -1057,19 +967,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-arm64": "1.2.4" + "@img/sharp-libvips-linux-arm64": "1.3.4" } }, "node_modules/@img/sharp-linux-ppc64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.34.5.tgz", - "integrity": "sha512-7zznwNaqW6YtsfrGGDA6BRkISKAAE1Jo0QdpNYXNMHu2+0dTrPflTLNkpc8l7MUP5M16ZJcUvysVWWrMefZquA==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-ppc64/-/sharp-linux-ppc64-0.35.5.tgz", + "integrity": "sha512-QVxAAq8evVRI9ia2vqgwrmWucn5Dfv+JdWzj75pD8omHLPSP7f8p20O8jxzjCcuCEQEOtYOZUmX1hkiZ0kdevA==", "cpu": [ "ppc64" ], @@ -1079,19 +989,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-ppc64": "1.2.4" + "@img/sharp-libvips-linux-ppc64": "1.3.4" } }, "node_modules/@img/sharp-linux-riscv64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.34.5.tgz", - "integrity": "sha512-51gJuLPTKa7piYPaVs8GmByo7/U7/7TZOq+cnXJIHZKavIRHAP77e3N2HEl3dgiqdD/w0yUfiJnII77PuDDFdw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-riscv64/-/sharp-linux-riscv64-0.35.5.tgz", + "integrity": "sha512-LtdreXguaavKODPIfzJ4kffx7UNt1omwtK0rch4EBbbSTXPnxWmYSayXdLJw0fJzQ97kHt1gL/yh4tvU+nCyRQ==", "cpu": [ "riscv64" ], @@ -1101,19 +1011,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-riscv64": "1.2.4" + "@img/sharp-libvips-linux-riscv64": "1.3.4" } }, "node_modules/@img/sharp-linux-s390x": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.34.5.tgz", - "integrity": "sha512-nQtCk0PdKfho3eC5MrbQoigJ2gd1CgddUMkabUj+rBevs8tZ2cULOx46E7oyX+04WGfABgIwmMC0VqieTiR4jg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-s390x/-/sharp-linux-s390x-0.35.5.tgz", + "integrity": "sha512-UZasTOFiYzotTsGOCu42BfUzP6Tu6Do/947iRm1RsLKvlllxwGcn4RN27LibGWceix4Y+Pmw3jsnTcCQIgWjqA==", "cpu": [ "s390x" ], @@ -1123,19 +1033,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-s390x": "1.2.4" + "@img/sharp-libvips-linux-s390x": "1.3.4" } }, "node_modules/@img/sharp-linux-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.34.5.tgz", - "integrity": "sha512-MEzd8HPKxVxVenwAa+JRPwEC7QFjoPWuS5NZnBt6B3pu7EG2Ge0id1oLHZpPJdn3OQK+BQDiw9zStiHBTJQQQQ==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linux-x64/-/sharp-linux-x64-0.35.5.tgz", + "integrity": "sha512-SxFtLTeJInhAA9Q836kux2vZNeOBQEx658qvbboZScr0wIARym3IcGmW7KpVD5sbVg0Ojy+udFQdayYIZyoNog==", "cpu": [ "x64" ], @@ -1145,19 +1055,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linux-x64": "1.2.4" + "@img/sharp-libvips-linux-x64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.34.5.tgz", - "integrity": "sha512-fprJR6GtRsMt6Kyfq44IsChVZeGN97gTD331weR1ex1c1rypDEABN6Tm2xa1wE6lYb5DdEnk03NZPqA7Id21yg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-arm64/-/sharp-linuxmusl-arm64-0.35.5.tgz", + "integrity": "sha512-9HbMclmI1zlNkFRs3z9/eBtDjfD0sGlrX1z6b1qwmiFY5ElDLh4BC0LPBdVp7z1DXFiKlIcznf+ZlsuZzLxQqg==", "cpu": [ "arm64" ], @@ -1167,19 +1077,19 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4" + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4" } }, "node_modules/@img/sharp-linuxmusl-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.34.5.tgz", - "integrity": "sha512-Jg8wNT1MUzIvhBFxViqrEhWDGzqymo3sV7z7ZsaWbZNDLXRJZoRGrjulp60YYtV4wfY8VIKcWidjojlLcWrd8Q==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-linuxmusl-x64/-/sharp-linuxmusl-x64-0.35.5.tgz", + "integrity": "sha512-4KOphqB035HrVdqLZfCgMzzERrQkkzOwRhl4OAkRO1YCldbaFjySXMaK534Mo0V+LndnlJk+sbUyLeU0ULyD1A==", "cpu": [ "x64" ], @@ -1189,38 +1099,54 @@ "linux" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-libvips-linuxmusl-x64": "1.2.4" + "@img/sharp-libvips-linuxmusl-x64": "1.3.4" } }, "node_modules/@img/sharp-wasm32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.34.5.tgz", - "integrity": "sha512-OdWTEiVkY2PHwqkbBI8frFxQQFekHaSSkUIJkwzclWZe64O1X4UlUjqqqLaPbUpMOQk6FBu/HtlGXNblIs0huw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-wasm32/-/sharp-wasm32-0.35.5.tgz", + "integrity": "sha512-Ptsga1su4tQx+LLF1ECS9U6nz5kmrXKo6XVbtR48Ke3ZRxxgaWBu7IDtEe1quo8hiupwm6WFqxVlXaSf7IINGQ==", + "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "optional": true, + "dependencies": { + "@emnapi/runtime": "^1.11.3" + }, + "engines": { + "node": ">=20.9.0" + }, + "funding": { + "url": "https://opencollective.com/libvips" + } + }, + "node_modules/@img/sharp-webcontainers-wasm32": { + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-webcontainers-wasm32/-/sharp-webcontainers-wasm32-0.35.5.tgz", + "integrity": "sha512-hfhF/FmoQyTUkA0bIKFOtw536BQSeBMe6BF6QyWlrPxT754+TFLaZ7sKKTfvvM0yJgKgaYTwnFCIZ/GuDw5SUA==", "cpu": [ "wasm32" ], - "license": "Apache-2.0 AND LGPL-3.0-or-later AND MIT", + "license": "Apache-2.0", "optional": true, "dependencies": { - "@emnapi/runtime": "^1.7.0" + "@img/sharp-wasm32": "0.35.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-arm64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.34.5.tgz", - "integrity": "sha512-WQ3AgWCWYSb2yt+IG8mnC6Jdk9Whs7O0gxphblsLvdhSpSTtmu69ZG1Gkb6NuvxsNACwiPV6cNSZNzt0KPsw7g==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-arm64/-/sharp-win32-arm64-0.35.5.tgz", + "integrity": "sha512-X4t7g+7ZA5DKblCBEXGjUqqemj4vczING/5viFwAL8h4N3qYeyjwdCvRLHi4EdOUI+2Z7UFlp1VM+p/AuEtm6Q==", "cpu": [ "arm64" ], @@ -1230,16 +1156,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-ia32": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.34.5.tgz", - "integrity": "sha512-FV9m/7NmeCmSHDD5j4+4pNI8Cp3aW+JvLoXcTUo0IqyjSfAZJ8dIUmijx1qaJsIiU+Hosw6xM5KijAWRJCSgNg==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-ia32/-/sharp-win32-ia32-0.35.5.tgz", + "integrity": "sha512-5Zm82LoBc43nhwNybZlG7Y1KO//Zhsn306fQl29ZOuStHLGTo3BWL83q3cznX0poxSAMuYL1On/BHBxkBeKr6A==", "cpu": [ "ia32" ], @@ -1249,16 +1175,16 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": "^20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@img/sharp-win32-x64": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.34.5.tgz", - "integrity": "sha512-+29YMsqY2/9eFEiW93eqWnuLcWcufowXewwSNIT6UwZdUUCrM3oFjMWH/Z6/TMmb4hlFenmfAVbpWeup2jryCw==", + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/@img/sharp-win32-x64/-/sharp-win32-x64-0.35.5.tgz", + "integrity": "sha512-x76eH0vEiHlcMQu8Y8IenntaACtddpT6W0wmXtWrnKcnKI7ME5DdgqhAD6SEWOEl1v2zDvkZDhFA9KnURwpfqg==", "cpu": [ "x64" ], @@ -1268,18 +1194,47 @@ "win32" ], "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" } }, "node_modules/@jridgewell/sourcemap-codec": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", - "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.6.0.tgz", + "integrity": "sha512-T7jf+5zgsZHwNJ4lvQ7/aezbyk0nNX+zJVWpmHA7VYsEx7a7qr5Rg5IbtJFqkgze5Y2sruq1RUY8Q837Od7iFw==", "license": "MIT" }, + "node_modules/@max-models/starlight-theme": { + "version": "0.2.0", + "resolved": "git+ssh://git@github.com/max-models/starlight-theme.git#347851725c6014b379c3d90ffea1d6a51b7d937e", + "integrity": "sha512-h8ahajOXli2LbiPyfv1Ze5rbAbKS9WP9HHM4sKDBPHCQ8+nGvHfCcs9bWawRC4zRv23jMajk3EuSCo1XiThvXw==", + "license": "MIT", + "workspaces": [ + "example/docs" + ], + "dependencies": { + "@astrojs/markdown-remark": "^7.3.2", + "@mdx-js/mdx": "^3.1.1", + "katex": "^0.19.0", + "rehype-katex": "^7.0.1", + "remark-math": "^6.0.0", + "starlight-links-validator": "^0.27.0", + "starlight-pydocs": "0.3.0" + }, + "bin": { + "starlight-check-mdx": "bin/check-mdx.mjs", + "starlight-notebooks": "bin/notebooks.mjs" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.42.6", + "astro": ">=7.3.8" + } + }, "node_modules/@mdx-js/mdx": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@mdx-js/mdx/-/mdx-3.1.1.tgz", @@ -1317,156 +1272,61 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/@oslojs/encoding": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", - "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", - "license": "MIT" - }, - "node_modules/@pagefind/darwin-arm64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.4.0.tgz", - "integrity": "sha512-2vMqkbv3lbx1Awea90gTaBsvpzgRs7MuSgKDxW0m9oV1GPZCZbZBJg/qL83GIUEN2BFlY46dtUZi54pwH+/pTQ==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@pagefind/darwin-x64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.4.0.tgz", - "integrity": "sha512-e7JPIS6L9/cJfow+/IAqknsGqEPjJnVXGjpGm25bnq+NPdoD3c/7fAwr1OXkG4Ocjx6ZGSCijXEV4ryMcH2E3A==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "darwin" - ] - }, - "node_modules/@pagefind/default-ui": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.4.0.tgz", - "integrity": "sha512-wie82VWn3cnGEdIjh4YwNESyS1G6vRHwL6cNjy9CFgNnWW/PGRjsLq300xjVH5sfPFK3iK36UxvIBymtQIEiSQ==", - "license": "MIT" - }, - "node_modules/@pagefind/freebsd-x64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.4.0.tgz", - "integrity": "sha512-WcJVypXSZ+9HpiqZjFXMUobfFfZZ6NzIYtkhQ9eOhZrQpeY5uQFqNWLCk7w9RkMUwBv1HAMDW3YJQl/8OqsV0Q==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "freebsd" - ] - }, - "node_modules/@pagefind/linux-arm64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.4.0.tgz", - "integrity": "sha512-PIt8dkqt4W06KGmQjONw7EZbhDF+uXI7i0XtRLN1vjCUxM9vGPdtJc2mUyVPevjomrGz5M86M8bqTr6cgDp1Uw==", - "cpu": [ - "arm64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@pagefind/linux-x64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.4.0.tgz", - "integrity": "sha512-z4oddcWwQ0UHrTHR8psLnVlz6USGJ/eOlDPTDYZ4cI8TK8PgwRUPQZp9D2iJPNIPcS6Qx/E4TebjuGJOyK8Mmg==", - "cpu": [ - "x64" - ], - "license": "MIT", - "optional": true, - "os": [ - "linux" - ] - }, - "node_modules/@pagefind/windows-x64": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.4.0.tgz", - "integrity": "sha512-NkT+YAdgS2FPCn8mIA9bQhiBs+xmniMGq1LFPDhcFn0+2yIUEiIG06t7bsZlhdjknEQRTSdT7YitP6fC5qwP0g==", - "cpu": [ - "x64" - ], + "node_modules/@napi-rs/wasm-runtime": { + "version": "1.2.5", + "resolved": "https://registry.npmjs.org/@napi-rs/wasm-runtime/-/wasm-runtime-1.2.5.tgz", + "integrity": "sha512-HshSvXzmBxNzqejd3k/KemgQ3hdREUYFRRrgxOZ+gPljDIsO2SDZrVZMd4Z/HsxudQbE62OVyVFE8z9BCurfjA==", "license": "MIT", "optional": true, - "os": [ - "win32" - ] - }, - "node_modules/@rollup/pluginutils": { - "version": "5.3.0", - "resolved": "https://registry.npmjs.org/@rollup/pluginutils/-/pluginutils-5.3.0.tgz", - "integrity": "sha512-5EdhGZtnu3V88ces7s53hhfK5KSASnJZv8Lulpc04cWO3REESroJXg73DFsOmgbU2BhwV0E20bu2IDZb3VKW4Q==", - "license": "MIT", "dependencies": { - "@types/estree": "^1.0.0", - "estree-walker": "^2.0.2", - "picomatch": "^4.0.2" + "@tybys/wasm-util": "^0.10.3" }, "engines": { - "node": ">=14.0.0" + "node": "^20.19.0 || ^22.13.0 || >=23.5.0" }, - "peerDependencies": { - "rollup": "^1.20.0||^2.0.0||^3.0.0||^4.0.0" + "funding": { + "type": "github", + "url": "https://github.com/sponsors/Brooooooklyn" }, - "peerDependenciesMeta": { - "rollup": { - "optional": true - } + "peerDependencies": { + "@emnapi/core": "^1.7.1 || ^2.0.0-alpha.4", + "@emnapi/runtime": "^1.7.1 || ^2.0.0-alpha.4" } }, - "node_modules/@rollup/pluginutils/node_modules/estree-walker": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/estree-walker/-/estree-walker-2.0.2.tgz", - "integrity": "sha512-Rfkk/Mp/DL7JVje3u18FxFujQlTNR2q6QfMSMB7AvCBx91NGj/ba3kCfza0f6dVDbw7YlRf/nDrn7pQrCCyQ/w==", + "node_modules/@oslojs/encoding": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@oslojs/encoding/-/encoding-1.1.0.tgz", + "integrity": "sha512-70wQhgYmndg4GCPxPPxPGevRKqTIJ2Nh4OkiMWmDAVYsTQ+Ta7Sq+rPevXyXGdzr30/qZBnyOalCszoMxlyldQ==", "license": "MIT" }, - "node_modules/@rollup/rollup-android-arm-eabi": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm-eabi/-/rollup-android-arm-eabi-4.60.0.tgz", - "integrity": "sha512-WOhNW9K8bR3kf4zLxbfg6Pxu2ybOUbB2AjMDHSQx86LIF4rH4Ft7vmMwNt0loO0eonglSNy4cpD3MKXXKQu0/A==", - "cpu": [ - "arm" - ], + "node_modules/@oxc-project/types": { + "version": "0.153.0", + "resolved": "https://registry.npmjs.org/@oxc-project/types/-/types-0.153.0.tgz", + "integrity": "sha512-jSNrRIyvPVLL5E1z/tS5zLAprosGz/FuI1PWwEOz59j3E4TTTzSpqKb/RDGqJhhW0QHYISGXxq582wGx6T11Tw==", "license": "MIT", - "optional": true, - "os": [ - "android" - ] + "funding": { + "url": "https://github.com/sponsors/oxc-project" + } }, - "node_modules/@rollup/rollup-android-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-android-arm64/-/rollup-android-arm64-4.60.0.tgz", - "integrity": "sha512-u6JHLll5QKRvjciE78bQXDmqRqNs5M/3GVqZeMwvmjaNODJih/WIrJlFVEihvV0MiYFmd+ZyPr9wxOVbPAG2Iw==", + "node_modules/@pagefind/darwin-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-arm64/-/darwin-arm64-1.5.2.tgz", + "integrity": "sha512-MXpI+7HsAdPkvJ0gk9xj9g541BCqBZOBbdwj9g6lB5LCj6kSV6nqDSjzcAJwvOsfu0fjwvC8hQU+ecfhp+MpiQ==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "android" + "darwin" ] }, - "node_modules/@rollup/rollup-darwin-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-arm64/-/rollup-darwin-arm64-4.60.0.tgz", - "integrity": "sha512-qEF7CsKKzSRc20Ciu2Zw1wRrBz4g56F7r/vRwY430UPp/nt1x21Q/fpJ9N5l47WWvJlkNCPJz3QRVw008fi7yA==", + "node_modules/@pagefind/darwin-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/darwin-x64/-/darwin-x64-1.5.2.tgz", + "integrity": "sha512-IojxFWMEJe0RQ7PQ3KXQsPIImNsbpPYpoZ+QUDrL8fAl/O27IX+LVLs74/UzEZy5uA2LD8Nz1AiwKr72vrkZQw==", "cpu": [ - "arm64" + "x64" ], "license": "MIT", "optional": true, @@ -1474,205 +1334,241 @@ "darwin" ] }, - "node_modules/@rollup/rollup-darwin-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-darwin-x64/-/rollup-darwin-x64-4.60.0.tgz", - "integrity": "sha512-WADYozJ4QCnXCH4wPB+3FuGmDPoFseVCUrANmA5LWwGmC6FL14BWC7pcq+FstOZv3baGX65tZ378uT6WG8ynTw==", + "node_modules/@pagefind/default-ui": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/default-ui/-/default-ui-1.5.2.tgz", + "integrity": "sha512-pm1LMnQg8N2B3n2TnjKlhaFihpz6zTiA4HiGQ6/slKO/+8K9CAU5kcjdSSPgpuk1PMuuN4hxLipUIifnrkl3Sg==", + "license": "MIT" + }, + "node_modules/@pagefind/freebsd-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/freebsd-x64/-/freebsd-x64-1.5.2.tgz", + "integrity": "sha512-7EVzo9+0w+2cbe671BtMj10UlNo83I+HrLVLfRxO731svHRJKUfJ/mo05gU14pe9PCfpKNQT8FS3Xc/oDN6pOA==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "darwin" + "freebsd" ] }, - "node_modules/@rollup/rollup-freebsd-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-arm64/-/rollup-freebsd-arm64-4.60.0.tgz", - "integrity": "sha512-6b8wGHJlDrGeSE3aH5mGNHBjA0TTkxdoNHik5EkvPHCt351XnigA4pS7Wsj/Eo9Y8RBU6f35cjN9SYmCFBtzxw==", + "node_modules/@pagefind/linux-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-arm64/-/linux-arm64-1.5.2.tgz", + "integrity": "sha512-Ovt9+K35sqzn8H3ZMXGwls4TD/wMJuvRtShHIsmUQREmaxjrDEX7gHckRCrwYJ4XE1H1p6HkLz3wukrAnsfXQw==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ] }, - "node_modules/@rollup/rollup-freebsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-freebsd-x64/-/rollup-freebsd-x64-4.60.0.tgz", - "integrity": "sha512-h25Ga0t4jaylMB8M/JKAyrvvfxGRjnPQIR8lnCayyzEjEOx2EJIlIiMbhpWxDRKGKF8jbNH01NnN663dH638mA==", + "node_modules/@pagefind/linux-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/linux-x64/-/linux-x64-1.5.2.tgz", + "integrity": "sha512-V+tFqHKXhQKq/WqPBD67AFy7scn1/aZID00ws4fSDd+1daSi5UHR9VVlRrOUYKxn3VuFQYRD7lYXdZK1WED1YA==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "freebsd" + "linux" ] }, - "node_modules/@rollup/rollup-linux-arm-gnueabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-gnueabihf/-/rollup-linux-arm-gnueabihf-4.60.0.tgz", - "integrity": "sha512-RzeBwv0B3qtVBWtcuABtSuCzToo2IEAIQrcyB/b2zMvBWVbjo8bZDjACUpnaafaxhTw2W+imQbP2BD1usasK4g==", + "node_modules/@pagefind/windows-arm64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-arm64/-/windows-arm64-1.5.2.tgz", + "integrity": "sha512-hN9Nh90fNW61nNRCW9ZyQrAj/mD0eRvmJ8NlTUzkbuW8kIzGJUi3cxjFkEcMZ5h/8FsKWD/VcouZl4yo1F7B6g==", "cpu": [ - "arm" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@rollup/rollup-linux-arm-musleabihf": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm-musleabihf/-/rollup-linux-arm-musleabihf-4.60.0.tgz", - "integrity": "sha512-Sf7zusNI2CIU1HLzuu9Tc5YGAHEZs5Lu7N1ssJG4Tkw6e0MEsN7NdjUDDfGNHy2IU+ENyWT+L2obgWiguWibWQ==", + "node_modules/@pagefind/windows-x64": { + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/@pagefind/windows-x64/-/windows-x64-1.5.2.tgz", + "integrity": "sha512-Fa2Iyw7kaDRzGMfNYNUXNW2zbL5FQVDgSOcbDHdzBrDEdpqOqg8TcZ68F22ol6NJ9IGzvUdmeyZypLW5dyhqsg==", "cpu": [ - "arm" + "x64" ], "license": "MIT", "optional": true, "os": [ - "linux" + "win32" ] }, - "node_modules/@rollup/rollup-linux-arm64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-gnu/-/rollup-linux-arm64-gnu-4.60.0.tgz", - "integrity": "sha512-DX2x7CMcrJzsE91q7/O02IJQ5/aLkVtYFryqCjduJhUfGKG6yJV8hxaw8pZa93lLEpPTP/ohdN4wFz7yp/ry9A==", + "node_modules/@rolldown/binding-android-arm-eabi": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm-eabi/-/binding-android-arm-eabi-1.2.13.tgz", + "integrity": "sha512-Vu/NICrgdRvUOeaJZIyB2FP63s6fO/4ud5l8dXmxDx9yDyn+mN0y+gsvsVviSli+T361Aqo0h46EuMoobrwawA==", "cpu": [ - "arm64" + "arm" ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-arm64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-arm64-musl/-/rollup-linux-arm64-musl-4.60.0.tgz", - "integrity": "sha512-09EL+yFVbJZlhcQfShpswwRZ0Rg+z/CsSELFCnPt3iK+iqwGsI4zht3secj5vLEs957QvFFXnzAT0FFPIxSrkQ==", + "node_modules/@rolldown/binding-android-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-android-arm64/-/binding-android-arm64-1.2.13.tgz", + "integrity": "sha512-6LEoIrGy5vvCd2boNnv2soPSUZj/PRvhp8R/t9KlX+mIZxThIE/ZqxrGXxmuNX0FuvQFvLdZr7S4HrYpYBVhEQ==", "cpu": [ "arm64" ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "android" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-gnu/-/rollup-linux-loong64-gnu-4.60.0.tgz", - "integrity": "sha512-i9IcCMPr3EXm8EQg5jnja0Zyc1iFxJjZWlb4wr7U2Wx/GrddOuEafxRdMPRYVaXjgbhvqalp6np07hN1w9kAKw==", + "node_modules/@rolldown/binding-darwin-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-arm64/-/binding-darwin-arm64-1.2.13.tgz", + "integrity": "sha512-CaJSRzlQ6Q+zkDMVA9OkVxBjVoPRCDEE3v0FSJ9kIQBgRjFApA3Si4ijhbNVUji7ZJt+PfO0e46qy4nPInHUPA==", "cpu": [ - "loong64" + "arm64" ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-loong64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-loong64-musl/-/rollup-linux-loong64-musl-4.60.0.tgz", - "integrity": "sha512-DGzdJK9kyJ+B78MCkWeGnpXJ91tK/iKA6HwHxF4TAlPIY7GXEvMe8hBFRgdrR9Ly4qebR/7gfUs9y2IoaVEyog==", + "node_modules/@rolldown/binding-darwin-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-darwin-x64/-/binding-darwin-x64-1.2.13.tgz", + "integrity": "sha512-e4r340I5hxSzJkD3YJBxgzq/rJ3wwxwdLLb2FPt1Zpyem20nrQPT11DDEEkazwxaHumH8wpglsp/Ozgj/wT6mw==", "cpu": [ - "loong64" + "x64" ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "darwin" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-gnu/-/rollup-linux-ppc64-gnu-4.60.0.tgz", - "integrity": "sha512-RwpnLsqC8qbS8z1H1AxBA1H6qknR4YpPR9w2XX0vo2Sz10miu57PkNcnHVaZkbqyw/kUWfKMI73jhmfi9BRMUQ==", + "node_modules/@rolldown/binding-freebsd-x64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-freebsd-x64/-/binding-freebsd-x64-1.2.13.tgz", + "integrity": "sha512-hoBWDZ4/K/mJKVMbsgUd1jfpA2DiOrJ54mpegObU9TBHBBrtX4FLIXJJQRDPZ/2Dn91GvPob+Z2hwKJzf4E5SQ==", "cpu": [ - "ppc64" + "x64" ], "license": "MIT", "optional": true, "os": [ - "linux" - ] + "freebsd" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-ppc64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-ppc64-musl/-/rollup-linux-ppc64-musl-4.60.0.tgz", - "integrity": "sha512-Z8pPf54Ly3aqtdWC3G4rFigZgNvd+qJlOE52fmko3KST9SoGfAdSRCwyoyG05q1HrrAblLbk1/PSIV+80/pxLg==", + "node_modules/@rolldown/binding-linux-arm-gnueabihf": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm-gnueabihf/-/binding-linux-arm-gnueabihf-1.2.13.tgz", + "integrity": "sha512-oF9fZu3vTWnTiVAkDFxU9PJLt1nh20K9RRpuHsR7ZnE2L7XookBbDBVLojCVSLGbvtSlxeUNnjAj6DbNsKyzbg==", "cpu": [ - "ppc64" + "arm" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-gnu/-/rollup-linux-riscv64-gnu-4.60.0.tgz", - "integrity": "sha512-3a3qQustp3COCGvnP4SvrMHnPQ9d1vzCakQVRTliaz8cIp/wULGjiGpbcqrkv0WrHTEp8bQD/B3HBjzujVWLOA==", + "node_modules/@rolldown/binding-linux-arm64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-gnu/-/binding-linux-arm64-gnu-1.2.13.tgz", + "integrity": "sha512-DTpb/+jMiqWMPgphn4ksW1zW+XLJ6dJJ9FMvnIk9I291LhCctMPhMYJrP5t9p6C9qa3KXadDdUEZHBaPh3kNxQ==", "cpu": [ - "riscv64" + "arm64" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-riscv64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-riscv64-musl/-/rollup-linux-riscv64-musl-4.60.0.tgz", - "integrity": "sha512-pjZDsVH/1VsghMJ2/kAaxt6dL0psT6ZexQVrijczOf+PeP2BUqTHYejk3l6TlPRydggINOeNRhvpLa0AYpCWSQ==", + "node_modules/@rolldown/binding-linux-arm64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-arm64-musl/-/binding-linux-arm64-musl-1.2.13.tgz", + "integrity": "sha512-xU+Kvv9N2A3SY35zFh1FWWNjxbadE/BKeqnkXe7vScU4GZsw757BlVmDLITZEq8DPpk6m7uoDYkK7P+x/uU5Ow==", "cpu": [ - "riscv64" + "arm64" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-s390x-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-s390x-gnu/-/rollup-linux-s390x-gnu-4.60.0.tgz", - "integrity": "sha512-3ObQs0BhvPgiUVZrN7gqCSvmFuMWvWvsjG5ayJ3Lraqv+2KhOsp+pUbigqbeWqueGIsnn+09HBw27rJ+gYK4VQ==", + "node_modules/@rolldown/binding-linux-ppc64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-ppc64-gnu/-/binding-linux-ppc64-gnu-1.2.13.tgz", + "integrity": "sha512-XMk/hnhh25oQ756Lclb1Q11h4ll0xC0Esa5w4Vzp+mAE2S3AGJgeM0SonrIgY8fHhjarb6hcoTuwoFG0VVP2ig==", "cpu": [ - "s390x" + "ppc64" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-gnu/-/rollup-linux-x64-gnu-4.60.0.tgz", - "integrity": "sha512-EtylprDtQPdS5rXvAayrNDYoJhIz1/vzN2fEubo3yLE7tfAw+948dO0g4M0vkTVFhKojnF+n6C8bDNe+gDRdTg==", + "node_modules/@rolldown/binding-linux-s390x-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-s390x-gnu/-/binding-linux-s390x-gnu-1.2.13.tgz", + "integrity": "sha512-U1BoTHYmlywE2n3HUWjzZ+8EQsKr+OwpxYuDWaubx42tUL8QSL7nOqRz/NVHanibbFP3M1Rxuprz/wNcbMKjqw==", "cpu": [ - "x64" + "s390x" ], "license": "MIT", "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-linux-x64-musl": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-linux-x64-musl/-/rollup-linux-x64-musl-4.60.0.tgz", - "integrity": "sha512-k09oiRCi/bHU9UVFqD17r3eJR9bn03TyKraCrlz5ULFJGdJGi7VOmm9jl44vOJvRJ6P7WuBi/s2A97LxxHGIdw==", + "node_modules/@rolldown/binding-linux-x64-gnu": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-gnu/-/binding-linux-x64-gnu-1.2.13.tgz", + "integrity": "sha512-J4cm80Pc6wEwV5AZ42o62sIQgDk+Y7R2s646jIwuvO+4V46QB58N4vVuMPWCBaNgursXLpmUWJV//WQAkPvHBQ==", "cpu": [ "x64" ], @@ -1680,25 +1576,31 @@ "optional": true, "os": [ "linux" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openbsd-x64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openbsd-x64/-/rollup-openbsd-x64-4.60.0.tgz", - "integrity": "sha512-1o/0/pIhozoSaDJoDcec+IVLbnRtQmHwPV730+AOD29lHEEo4F5BEUB24H0OBdhbBBDwIOSuf7vgg0Ywxdfiiw==", + "node_modules/@rolldown/binding-linux-x64-musl": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-linux-x64-musl/-/binding-linux-x64-musl-1.2.13.tgz", + "integrity": "sha512-W0euZS40U4V6JkmEnVCIX9mcsL6cbXQ4X8c2w+MMOIjFg68TXUW+u3RT+2e1t1pv9mg/EP3pDtK3POXdQ7Rxyw==", "cpu": [ "x64" ], "license": "MIT", "optional": true, "os": [ - "openbsd" - ] + "linux" + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-openharmony-arm64": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-openharmony-arm64/-/rollup-openharmony-arm64-4.60.0.tgz", - "integrity": "sha512-pESDkos/PDzYwtyzB5p/UoNU/8fJo68vcXM9ZW2V0kjYayj1KaaUfi1NmTUTUpMn4UhU4gTuK8gIaFO4UGuMbA==", + "node_modules/@rolldown/binding-openharmony-arm64": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-openharmony-arm64/-/binding-openharmony-arm64-1.2.13.tgz", + "integrity": "sha512-do/AF/4n9MRshia3a5aXoy8xM2c6mgfT6D7H/dY6my5z+hfCIR4ab7mk4IRj++nsEQpKy39kd0DQnDq+r692tw==", "cpu": [ "arm64" ], @@ -1706,12 +1608,15 @@ "optional": true, "os": [ "openharmony" - ] + ], + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-arm64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-arm64-msvc/-/rollup-win32-arm64-msvc-4.60.0.tgz", - "integrity": "sha512-hj1wFStD7B1YBeYmvY+lWXZ7ey73YGPcViMShYikqKT1GtstIKQAtfUI6yrzPjAy/O7pO0VLXGmUVWXQMaYgTQ==", + "node_modules/@rolldown/binding-win32-arm64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-arm64-msvc/-/binding-win32-arm64-msvc-1.2.13.tgz", + "integrity": "sha512-6FtUf2cJZobutJdxnGYX8z0vtbgqU0b2v8vfBhkXb9wp2MppMFtkunE+IaOntyi01rkIQoPW2GHABtfzsC0BRQ==", "cpu": [ "arm64" ], @@ -1719,25 +1624,15 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-ia32-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-ia32-msvc/-/rollup-win32-ia32-msvc-4.60.0.tgz", - "integrity": "sha512-SyaIPFoxmUPlNDq5EHkTbiKzmSEmq/gOYFI/3HHJ8iS/v1mbugVa7dXUzcJGQfoytp9DJFLhHH4U3/eTy2Bq4w==", - "cpu": [ - "ia32" ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } }, - "node_modules/@rollup/rollup-win32-x64-gnu": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-gnu/-/rollup-win32-x64-gnu-4.60.0.tgz", - "integrity": "sha512-RdcryEfzZr+lAr5kRm2ucN9aVlCCa2QNq4hXelZxb8GG0NJSazq44Z3PCCc8wISRuCVnGs0lQJVX5Vp6fKA+IA==", + "node_modules/@rolldown/binding-win32-x64-msvc": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/@rolldown/binding-win32-x64-msvc/-/binding-win32-x64-msvc-1.2.13.tgz", + "integrity": "sha512-UKhFaZTeTcbKrb3iU82m17jVcPEAoqDkCN5MBqgPyZSwp/cQmib21crTS0DdH7T1B7wZRWEI89YQAOJMet5yhg==", "cpu": [ "x64" ], @@ -1745,31 +1640,27 @@ "optional": true, "os": [ "win32" - ] - }, - "node_modules/@rollup/rollup-win32-x64-msvc": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/@rollup/rollup-win32-x64-msvc/-/rollup-win32-x64-msvc-4.60.0.tgz", - "integrity": "sha512-PrsWNQ8BuE00O3Xsx3ALh2Df8fAj9+cvvX9AIA6o4KpATR98c9mud4XtDWVvsEuyia5U4tVSTKygawyJkjm60w==", - "cpu": [ - "x64" ], - "license": "MIT", - "optional": true, - "os": [ - "win32" - ] + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, + "node_modules/@rolldown/pluginutils": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/@rolldown/pluginutils/-/pluginutils-1.0.1.tgz", + "integrity": "sha512-2j9bGt5Jh8hj+vPtgzPtl72j0yRxHAyumoo6TNfAjsLB04UtpSvPbPcDcBMxz7n+9CYB0c1GxQFxYRg2jimqGw==", + "license": "MIT" }, "node_modules/@shikijs/core": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.0.2.tgz", - "integrity": "sha512-hxT0YF4ExEqB8G/qFdtJvpmHXBYJ2lWW7qTHDarVkIudPFE6iCIrqdgWxGn5s+ppkGXI0aEGlibI0PAyzP3zlw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/core/-/core-4.5.0.tgz", + "integrity": "sha512-6CIgwq+bt2JZ3OSjb/nBkkwEf/CfpHcbam+Mg5Pllco7HAGRQ2/EFhZrrJtbyNTgKwknfVS+llsXGygiDGwJWw==", "license": "MIT", "dependencies": { - "@shikijs/primitive": "4.0.2", - "@shikijs/types": "4.0.2", + "@shikijs/primitive": "4.5.0", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4", + "@types/hast": "^3.0.5", "hast-util-to-html": "^9.0.5" }, "engines": { @@ -1777,26 +1668,26 @@ } }, "node_modules/@shikijs/engine-javascript": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.0.2.tgz", - "integrity": "sha512-7PW0Nm49DcoUIQEXlJhNNBHyoGMjalRETTCcjMqEaMoJRLljy1Bi/EGV3/qLBgLKQejdspiiYuHGQW6dX94Nag==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-javascript/-/engine-javascript-4.5.0.tgz", + "integrity": "sha512-Ed6UdEj84LyYkwnxM5CvSMWAD+id9ExTYri0/+x41U9zrgNifPSS9lZm0JqBP0VfYI8yr8tY5nbNq9+p9R0kFA==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2", - "oniguruma-to-es": "^4.3.4" + "oniguruma-to-es": "^4.3.6" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/engine-oniguruma": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.0.2.tgz", - "integrity": "sha512-UpCB9Y2sUKlS9z8juFSKz7ZtysmeXCgnRF0dlhXBkmQnek7lAToPte8DkxmEYGNTMii72zU/lyXiCB6StuZeJg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/engine-oniguruma/-/engine-oniguruma-4.5.0.tgz", + "integrity": "sha512-dKVmbJecVdB9recl8a/CU4jdpNXyjJQrIcesIkpS70s5HS02ohpnCSpMUbotPnEUzbAcLQ+S4hwZYbkBBiTcJw==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2" }, "engines": { @@ -1804,51 +1695,51 @@ } }, "node_modules/@shikijs/langs": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.0.2.tgz", - "integrity": "sha512-KaXby5dvoeuZzN0rYQiPMjFoUrz4hgwIE+D6Du9owcHcl6/g16/yT5BQxSW5cGt2MZBz6Hl0YuRqf12omRfUUg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/langs/-/langs-4.5.0.tgz", + "integrity": "sha512-wwBTqwduXSz0X4v9Qa2EWeFaYJEw1clCncWjoRNQRbVDu3SPRTRWucnouYapKj3b8Q+sk/41Vbc4B5c8nuYzLA==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2" + "@shikijs/types": "4.5.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/primitive": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.0.2.tgz", - "integrity": "sha512-M6UMPrSa3fN5ayeJwFVl9qWofl273wtK1VG8ySDZ1mQBfhCpdd8nEx7nPZ/tk7k+TYcpqBZzj/AnwxT9lO+HJw==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/primitive/-/primitive-4.5.0.tgz", + "integrity": "sha512-UM7ShlA8oz0OWxLLO8BmO9Ip6fVqdkt8yF0EH7nOCqHfrx1eEAzF0ZUVK7L+I8NEgeu1PEWSRlxaiPPeCb4iDg==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/themes": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.0.2.tgz", - "integrity": "sha512-mjCafwt8lJJaVSsQvNVrJumbnnj1RI8jbUKrPKgE6E3OvQKxnuRoBaYC51H4IGHePsGN/QtALglWBU7DoKDFnA==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/themes/-/themes-4.5.0.tgz", + "integrity": "sha512-zGb87H3UBbpuI4yoh5UguB1XbU+2BDU3kflM7UfZ+Cy2hWo9lBtqjSVK20gmLY6ukYl040hAypGXMgud9vul8Q==", "license": "MIT", "dependencies": { - "@shikijs/types": "4.0.2" + "@shikijs/types": "4.5.0" }, "engines": { "node": ">=20" } }, "node_modules/@shikijs/types": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.0.2.tgz", - "integrity": "sha512-qzbeRooUTPnLE+sHD/Z8DStmaDgnbbc/pMrU203950aRqjX/6AFHeDYT+j00y2lPdz0ywJKx7o/7qnqTivtlXg==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/@shikijs/types/-/types-4.5.0.tgz", + "integrity": "sha512-drtPrtD8N0DENyD2cyt6Ajy+IWsiCLgMxODwLTpvH1UsN6a9jf90tNTPjqwHvA+0Lzu9e0txk8DjfqH1p9UJRw==", "license": "MIT", "dependencies": { "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" }, "engines": { "node": ">=20" @@ -1860,6 +1751,16 @@ "integrity": "sha512-83yeghZ2xxin3Nj8z1NMd/NCuca+gsYXswywDy5bHvwlWL8tpTQmzGeUuHd9FC3E/SBEMvzJRwWEOz5gGes9Qg==", "license": "MIT" }, + "node_modules/@tybys/wasm-util": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/@tybys/wasm-util/-/wasm-util-0.10.4.tgz", + "integrity": "sha512-W3c4gRigFS0T/Ma4qIYF3GDAc5AQdHb1yL5znJT1Zv1YaD9Kitx656wBjvr19qbiosmZT8lWDM5BEMynUqX65A==", + "license": "MIT", + "optional": true, + "dependencies": { + "tslib": "^2.4.0" + } + }, "node_modules/@types/debug": { "version": "4.1.13", "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.13.tgz", @@ -1870,9 +1771,9 @@ } }, "node_modules/@types/estree": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", - "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "version": "1.0.9", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.9.tgz", + "integrity": "sha512-GhdPgy1el4/ImP05X05Uw4cw2/M93BCUmnEvWZNStlCzEKME4Fkk+YpoA5OiHNQmoS7Cafb8Xa3Pya8m1Qrzeg==", "license": "MIT" }, "node_modules/@types/estree-jsx": { @@ -1885,9 +1786,9 @@ } }, "node_modules/@types/hast": { - "version": "3.0.4", - "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.4.tgz", - "integrity": "sha512-WPs+bbQw5aCj+x6laNGWLH3wviHtoCv/P3+otBhbOhJgG8qtpdAMlTCxLtsTWA7LH1Oh/bFCHsBn0TPS5m30EQ==", + "version": "3.0.5", + "resolved": "https://registry.npmjs.org/@types/hast/-/hast-3.0.5.tgz", + "integrity": "sha512-rp/ezSWaD1m44dPKICGhiskI13nVr7qTloFwDa/IYkhhf5nzwP+zIQcIJh3WIFSBOy/H1PzB40jPjMDksN4F+g==", "license": "MIT", "dependencies": { "@types/unist": "*" @@ -1915,9 +1816,9 @@ } }, "node_modules/@types/mdx": { - "version": "2.0.13", - "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.13.tgz", - "integrity": "sha512-+OWZQfAYyio6YkJb3HLxDrvnx6SWWDbC0zVPfBRzUk0/nqoDyf6dNxQi3eArPe8rJ473nobTMQ/8Zk+LxJ+Yuw==", + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/@types/mdx/-/mdx-2.0.14.tgz", + "integrity": "sha512-T48PeuJtvLosNTPVhfnIp3i/n3a4g4Bad7YCq5k64D4u7NwDrAotikQ+5+sjtUvBmxCMlbo3dVL+C2dP0rWHzg==", "license": "MIT" }, "node_modules/@types/ms": { @@ -1936,12 +1837,12 @@ } }, "node_modules/@types/node": { - "version": "24.12.0", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.12.0.tgz", - "integrity": "sha512-GYDxsZi3ChgmckRT9HPU0WEhKLP08ev/Yfcq2AstjrDASOYCSXeyjDsHg4v5t4jOj7cyDX3vmprafKlWIG9MXQ==", + "version": "24.19.2", + "resolved": "https://registry.npmjs.org/@types/node/-/node-24.19.2.tgz", + "integrity": "sha512-93d49wCWJkCyN87MxLpNN6KXPCwFw7bVUg79mGUV1OS40JGWwVCr8n61DXCSqz7CV6/VfPkb7xec7uP2M+QHWQ==", "license": "MIT", "dependencies": { - "undici-types": "~7.16.0" + "undici-types": ">=7.24.0 <7.24.7" } }, "node_modules/@types/sax": { @@ -1960,15 +1861,15 @@ "license": "MIT" }, "node_modules/@ungap/structured-clone": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.3.0.tgz", - "integrity": "sha512-WmoN8qaIAo7WTYWbAZuG8PYEhn5fkz7dZrqTBZ7dtt//lL2Gwms1IcnQ5yHqjDfX8Ft5j4YzDM23f87zBfDe9g==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/@ungap/structured-clone/-/structured-clone-1.4.0.tgz", + "integrity": "sha512-1mEZtMKPM09vDmQt5y7YvmN2+DFTP7Tg0EWXdic8/C6VRnpb33e4ghisCIE3WZjsE2N8mf+QV1Zqh7ZFYLWInQ==", "license": "ISC" }, "node_modules/acorn": { - "version": "8.16.0", - "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", - "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.19.0.tgz", + "integrity": "sha512-oJlA3XiRm7Cyk6qFD2Jn8ak9B7jSy0qy00ADO3+8dpT0LSjFihQYv4C02LFCSYJV3Q37xYwwRy5m+IpIiUzqWw==", "license": "MIT", "bin": { "acorn": "bin/acorn" @@ -1986,6 +1887,33 @@ "acorn": "^6.0.0 || ^7.0.0 || ^8.0.0" } }, + "node_modules/am-i-vibing": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/am-i-vibing/-/am-i-vibing-0.5.0.tgz", + "integrity": "sha512-KLcsDhqf1XRrJmJr+9I/C0EX2K+sGrAGZFgkmd84c8TJO9zJBu+/kFInjazu1KfJWFhWDKdRWkpBzEs7s35DVA==", + "license": "MIT", + "dependencies": { + "process-ancestry": "^0.1.0" + }, + "bin": { + "am-i-vibing": "dist/cli.mjs" + } + }, + "node_modules/ansi-escapes": { + "version": "7.3.0", + "resolved": "https://registry.npmjs.org/ansi-escapes/-/ansi-escapes-7.3.0.tgz", + "integrity": "sha512-BvU8nYgGQBxcmMuEeUEmNTvrMVjJNSH7RgW24vXexN4Ven6qCvy4TntnvlnwnMLTVlcRQQdbRY8NKnaIoeWDNg==", + "license": "MIT", + "dependencies": { + "environment": "^1.0.0" + }, + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/anymatch": { "version": "3.1.3", "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", @@ -2052,66 +1980,63 @@ } }, "node_modules/astro": { - "version": "6.1.1", - "resolved": "https://registry.npmjs.org/astro/-/astro-6.1.1.tgz", - "integrity": "sha512-vq8sHpu1JsY1fWAunn+tdKNbVDmLQNiVdyuGsVT2csgITdFGXXVAyEXFWc1DzkMN0ehElPeiHnqItyQOJK+GqA==", + "version": "7.3.8", + "resolved": "https://registry.npmjs.org/astro/-/astro-7.3.8.tgz", + "integrity": "sha512-LHzDHSaV+YzZiR3EFndDYTLLeNeuMtP1qX1FLcCNUUHOCqTTIWPczy2UV3zpA5VqvJe5cWZolqM9Z8JFqR2i9w==", "license": "MIT", "dependencies": { - "@astrojs/compiler": "^3.0.1", - "@astrojs/internal-helpers": "0.8.0", - "@astrojs/markdown-remark": "7.1.0", - "@astrojs/telemetry": "3.3.0", - "@capsizecss/unpack": "^4.0.0", - "@clack/prompts": "^1.1.0", + "@astrojs/compiler-rs": "^0.5.1", + "@astrojs/internal-helpers": "0.12.0", + "@astrojs/markdown-satteri": "0.4.3", + "@astrojs/telemetry": "3.3.3", + "@capsizecss/unpack": "^4.0.1", + "@clack/prompts": "^1.8.1", "@oslojs/encoding": "^1.1.0", - "@rollup/pluginutils": "^5.3.0", + "am-i-vibing": "^0.5.0", "aria-query": "^5.3.2", "axobject-query": "^4.1.0", "ci-info": "^4.4.0", "clsx": "^2.1.1", "common-ancestor-path": "^2.0.0", - "cookie": "^1.1.1", - "devalue": "^5.6.3", - "diff": "^8.0.3", - "dlv": "^1.1.3", + "cookie": "^2.0.1", + "devalue": "^5.9.4", + "diff": "^9.0.0", "dset": "^3.1.4", - "es-module-lexer": "^2.0.0", - "esbuild": "^0.27.3", + "es-module-lexer": "^3.0.2", + "find-proc": "0.2.0", "flattie": "^1.1.1", "fontace": "~0.4.1", + "get-tsconfig": "5.0.0-beta.6", "github-slugger": "^2.0.0", "html-escaper": "3.0.3", "http-cache-semantics": "^4.2.0", - "js-yaml": "^4.1.1", - "magic-string": "^0.30.21", - "magicast": "^0.5.2", + "jsonc-parser": "^3.3.1", + "magic-string": "^1.4.2", + "magicast": "^0.5.5", "mrmime": "^2.0.1", - "neotraverse": "^0.6.18", - "obug": "^2.1.1", - "p-limit": "^7.3.0", - "p-queue": "^9.1.0", - "package-manager-detector": "^1.6.0", + "neotraverse": "^1.0.1", + "obug": "^3.0.0", + "p-limit": "^7.3.3", + "p-queue": "^9.3.3", + "package-manager-detector": "^1.8.0", "piccolore": "^0.1.3", - "picomatch": "^4.0.3", - "rehype": "^13.0.2", - "semver": "^7.7.4", - "shiki": "^4.0.2", - "smol-toml": "^1.6.0", - "svgo": "^4.0.1", - "tinyclip": "^0.1.12", - "tinyexec": "^1.0.4", - "tinyglobby": "^0.2.15", - "tsconfck": "^3.1.6", - "ultrahtml": "^1.6.0", - "unifont": "~0.7.4", - "unist-util-visit": "^5.1.0", - "unstorage": "^1.17.4", - "vfile": "^6.0.3", - "vite": "^7.3.1", - "vitefu": "^1.1.2", + "picomatch": "^4.0.7", + "rolldown": "^1.2.11", + "shiki": "^4.4.3", + "smol-toml": "^1.9.0", + "svgo": "^4.1.0", + "tinyclip": "^1.0.3", + "tinyexec": "^1.3.1", + "tinyglobby": "^0.2.17", + "ultrahtml": "^1.7.0", + "unifont": "~0.7.5", + "unstorage": "^1.17.5", + "verkit": "^0.5.0", + "vite": "^8.3.1", + "vitefu": "^1.1.3", "xxhash-wasm": "^1.1.0", "yargs-parser": "^22.0.0", - "zod": "^4.3.6" + "zod": "^4.6.5" }, "bin": { "astro": "bin/astro.mjs" @@ -2126,19 +2051,28 @@ "url": "https://opencollective.com/astrodotbuild" }, "optionalDependencies": { - "sharp": "^0.34.0" + "sharp": "^0.35.4" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.3.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + } } }, "node_modules/astro-expressive-code": { - "version": "0.41.7", - "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.41.7.tgz", - "integrity": "sha512-hUpogGc6DdAd+I7pPXsctyYPRBJDK7Q7d06s4cyP0Vz3OcbziP3FNzN0jZci1BpCvLn9675DvS7B9ctKKX64JQ==", + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/astro-expressive-code/-/astro-expressive-code-0.44.2.tgz", + "integrity": "sha512-avop7nZcRwC7quvIxVguhwEZ9Tp6IoTzKUCLwP30Fb7+er+pWQgcysP01dcuzRIJo81kLGg30q/YAI2Yb1Sdzg==", "license": "MIT", "dependencies": { - "rehype-expressive-code": "^0.41.7" + "rehype-expressive-code": "^0.44.2", + "url-extras": "^0.1.0" }, "peerDependencies": { - "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta" + "astro": "^4.0.0-beta || ^5.0.0-beta || ^3.3.0 || ^6.0.0-beta || ^7.0.0" } }, "node_modules/axobject-query": { @@ -2161,9 +2095,9 @@ } }, "node_modules/bcp-47": { - "version": "2.1.0", - "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.0.tgz", - "integrity": "sha512-9IIS3UPrvIa1Ej+lVDdDwO7zLehjqsaByECw0bu2RRGP73jALm6FYbzI5gWbgHLvNdkvfXB5YrSbocZdOS0c0w==", + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/bcp-47/-/bcp-47-2.1.1.tgz", + "integrity": "sha512-KLw+H/gd2p4zly1X7Yh/qziuyae5/w/QFnvTng9eZL5fvszL7Whl3MBoWF8yxL7ksUjBfOD+OxkytiqbBpG+Fw==", "license": "MIT", "dependencies": { "is-alphabetical": "^2.0.0", @@ -2301,12 +2235,12 @@ } }, "node_modules/commander": { - "version": "11.1.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", - "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "version": "15.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-15.0.0.tgz", + "integrity": "sha512-z67u4ZhzCL/Tydu1lJARtEZYWbWaN7oYLHbsuzocr6y4N6WZAagG3RQ4FW61V1/0+jImpj293XfrcYnd1qxtPg==", "license": "MIT", "engines": { - "node": ">=16" + "node": ">=22.12.0" } }, "node_modules/common-ancestor-path": { @@ -2319,12 +2253,12 @@ } }, "node_modules/cookie": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/cookie/-/cookie-1.1.1.tgz", - "integrity": "sha512-ei8Aos7ja0weRpFzJnEA9UHJ/7XQmqglbRwnf2ATjcB9Wq874VKH9kfjjirM6UhU2/E5fFYadylyhFldcqSidQ==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-2.0.1.tgz", + "integrity": "sha512-yuToqVvRrj6pfDXREyQAAv8SkAEk/8GS3jQRTiUMm66TVtBYmqQeoEjL2Lmq8Rpo6271vH76InTChTitEAm65w==", "license": "MIT", "engines": { - "node": ">=18" + "node": ">=22" }, "funding": { "type": "opencollective", @@ -2332,9 +2266,9 @@ } }, "node_modules/cookie-es": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.2.tgz", - "integrity": "sha512-+W7VmiVINB+ywl1HGXJXmrqkOhpKrIiVZV6tQuV54ZyQC7MMuBt81Vc336GMLoHBq5hV/F9eXgt5Mnx0Rha5Fg==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/cookie-es/-/cookie-es-1.2.3.tgz", + "integrity": "sha512-lXVyvUvrNXblMqzIRrxHb57UUVmqsSWlxqt3XIjCkUP0wDAf6uicO6KMbEgYrMNtEvWgWHwe42CKxPu9MYAnWw==", "license": "MIT" }, "node_modules/crossws": { @@ -2347,16 +2281,16 @@ } }, "node_modules/css-select": { - "version": "5.2.2", - "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.2.2.tgz", - "integrity": "sha512-TizTzUddG/xYLA3NXodFM0fSbNizXjOKhqiQQwvhlspadZokn1KDy0NZFS0wuEubIYAV5/c1/lAr0TaaFXEXzw==", + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-6.0.0.tgz", + "integrity": "sha512-rZZVSLle8v0+EY8QAkDWrKhpgt6SA5OtHsgBnsj6ZaLb5dmDVOWUDtQitd9ydxxvEjhewNudS6eTVU7uOyzvXw==", "license": "BSD-2-Clause", "dependencies": { "boolbase": "^1.0.0", - "css-what": "^6.1.0", - "domhandler": "^5.0.2", - "domutils": "^3.0.1", - "nth-check": "^2.0.1" + "css-what": "^7.0.0", + "domhandler": "^5.0.3", + "domutils": "^3.2.2", + "nth-check": "^2.1.1" }, "funding": { "url": "https://github.com/sponsors/fb55" @@ -2392,9 +2326,9 @@ } }, "node_modules/css-what": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.2.2.tgz", - "integrity": "sha512-u/O3vwbptzhMs3L1fQE82ZSLHQQfto5gyZzwteVIEyeaY5Fc7R4dapF/BvRoSYFeqfBk4m0V1Vafq5Pjv25wvA==", + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-7.0.0.tgz", + "integrity": "sha512-wD5oz5xibMOPHzy13CyGmogB3phdvcDaB5t0W/Nr5Z2O/agcB8YwOz6e2Lsp10pNDzBoDO9nVa3RGs/2BttpHQ==", "license": "BSD-2-Clause", "engines": { "node": ">= 6" @@ -2479,9 +2413,9 @@ } }, "node_modules/defu": { - "version": "6.1.4", - "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.4.tgz", - "integrity": "sha512-mEQCMmwJu317oSz8CwdIOdwf3xMif1ttiM8LTufzc3g6kR+9Pe236twL8j3IYT1F7GfRgGcW6MWxzZjLIkuHIg==", + "version": "6.1.7", + "resolved": "https://registry.npmjs.org/defu/-/defu-6.1.7.tgz", + "integrity": "sha512-7z22QmUWiQ/2d0KkdYmANbRUVABpZ9SNYyH5vx6PZ+nE5bcC0l7uFvEfHlyld/HcGBFTL536ClDt3DEcSlEJAQ==", "license": "MIT" }, "node_modules/dequal": { @@ -2509,9 +2443,9 @@ } }, "node_modules/devalue": { - "version": "5.6.4", - "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.6.4.tgz", - "integrity": "sha512-Gp6rDldRsFh/7XuouDbxMH3Mx8GMCcgzIb1pDTvNyn8pZGQ22u+Wa+lGV9dQCltFQ7uVw0MhRyb8XDskNFOReA==", + "version": "5.9.4", + "resolved": "https://registry.npmjs.org/devalue/-/devalue-5.9.4.tgz", + "integrity": "sha512-sPAT4pztbu6586/hrhOnMKS17IJrvg12mXiSPSS3W5qDeN2RGgvZ0diZCm31dBbnevfVmujNO3IM2wrS4Y2Rhg==", "license": "MIT" }, "node_modules/devlop": { @@ -2528,9 +2462,9 @@ } }, "node_modules/diff": { - "version": "8.0.4", - "resolved": "https://registry.npmjs.org/diff/-/diff-8.0.4.tgz", - "integrity": "sha512-DPi0FmjiSU5EvQV0++GFDOJ9ASQUVFh5kD+OzOnYdi7n3Wpm9hWWGfB/O2blfHcMVTL5WkQXSnRiK9makhrcnw==", + "version": "9.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-9.0.0.tgz", + "integrity": "sha512-svtcdpS8CgJyqAjEQIXdb3OjhFVVYjzGAPO8WGCmRbrml64SPw/jJD4GoE98aR7r25A0XcgrK3F02yw9R/vhQw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.3.1" @@ -2549,12 +2483,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/dlv": { - "version": "1.1.3", - "resolved": "https://registry.npmjs.org/dlv/-/dlv-1.1.3.tgz", - "integrity": "sha512-+HlytyjlPKnIG8XuRG8WvmBP8xs8P71y+SKKS6ZXWoEgLuePxtDoUEiH7WkdePWrQ5JBpE6aoVqfZfJUQkjXwA==", - "license": "MIT" - }, "node_modules/dom-serializer": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", @@ -2643,10 +2571,22 @@ "url": "https://github.com/fb55/entities?sponsor=1" } }, + "node_modules/environment": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/environment/-/environment-1.1.0.tgz", + "integrity": "sha512-xUtoPkMggbz0MPyPiIWr1Kp4aeWJjDZ6SMvURhimjdZgsRuDplF5/s9hcgGhyXMhs+6vpnuoiZ2kFiu3FMnS8Q==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/es-module-lexer": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", - "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-3.0.3.tgz", + "integrity": "sha512-yWGe1O/j+almnV4hOYrmZo8u58JdnmXEMneRQAYkWtqSiT86yoMIEKF4rpqEDj52tbYl3xceBD3DQIBwcsC9Pw==", "license": "MIT" }, "node_modules/esast-util-from-estree": { @@ -2681,47 +2621,6 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/esbuild": { - "version": "0.27.4", - "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.27.4.tgz", - "integrity": "sha512-Rq4vbHnYkK5fws5NF7MYTU68FPRE1ajX7heQ/8QXXWqNgqqJ/GkmmyxIzUnf2Sr/bakf8l54716CcMGHYhMrrQ==", - "hasInstallScript": true, - "license": "MIT", - "bin": { - "esbuild": "bin/esbuild" - }, - "engines": { - "node": ">=18" - }, - "optionalDependencies": { - "@esbuild/aix-ppc64": "0.27.4", - "@esbuild/android-arm": "0.27.4", - "@esbuild/android-arm64": "0.27.4", - "@esbuild/android-x64": "0.27.4", - "@esbuild/darwin-arm64": "0.27.4", - "@esbuild/darwin-x64": "0.27.4", - "@esbuild/freebsd-arm64": "0.27.4", - "@esbuild/freebsd-x64": "0.27.4", - "@esbuild/linux-arm": "0.27.4", - "@esbuild/linux-arm64": "0.27.4", - "@esbuild/linux-ia32": "0.27.4", - "@esbuild/linux-loong64": "0.27.4", - "@esbuild/linux-mips64el": "0.27.4", - "@esbuild/linux-ppc64": "0.27.4", - "@esbuild/linux-riscv64": "0.27.4", - "@esbuild/linux-s390x": "0.27.4", - "@esbuild/linux-x64": "0.27.4", - "@esbuild/netbsd-arm64": "0.27.4", - "@esbuild/netbsd-x64": "0.27.4", - "@esbuild/openbsd-arm64": "0.27.4", - "@esbuild/openbsd-x64": "0.27.4", - "@esbuild/openharmony-arm64": "0.27.4", - "@esbuild/sunos-x64": "0.27.4", - "@esbuild/win32-arm64": "0.27.4", - "@esbuild/win32-ia32": "0.27.4", - "@esbuild/win32-x64": "0.27.4" - } - }, "node_modules/escape-string-regexp": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-5.0.0.tgz", @@ -2774,9 +2673,9 @@ } }, "node_modules/estree-util-scope": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.0.tgz", - "integrity": "sha512-2CAASclonf+JFWBNJPndcOpA8EMJwa0Q8LUFJEKqXLW6+qBvbFZuF5gItbQOs/umBUkjviCSDCbBwU2cXbmrhQ==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/estree-util-scope/-/estree-util-scope-1.0.1.tgz", + "integrity": "sha512-B0np3dcdxqILX5e9nEi5/Fr4K7gL4oYFVPV1zRa2e9wRCbQoZZNWOZFYyoInvXUPJXBXjss+QXlWLJChDEHDkA==", "license": "MIT", "dependencies": { "@types/estree": "^1.0.0", @@ -2832,15 +2731,15 @@ "license": "MIT" }, "node_modules/expressive-code": { - "version": "0.41.7", - "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.41.7.tgz", - "integrity": "sha512-2wZjC8OQ3TaVEMcBtYY4Va3lo6J+Ai9jf3d4dbhURMJcU4Pbqe6EcHe424MIZI0VHUA1bR6xdpoHYi3yxokWqA==", + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/expressive-code/-/expressive-code-0.44.2.tgz", + "integrity": "sha512-MLNghkvhyFgVW4oCD5DxBuYnnTXIf0PygzGmfT6OZeOOYWh/D4sH1j931biElkA5RG/J9wY/iuSMGAPF5vzHOg==", "license": "MIT", "dependencies": { - "@expressive-code/core": "^0.41.7", - "@expressive-code/plugin-frames": "^0.41.7", - "@expressive-code/plugin-shiki": "^0.41.7", - "@expressive-code/plugin-text-markers": "^0.41.7" + "@expressive-code/core": "^0.44.2", + "@expressive-code/plugin-frames": "^0.44.2", + "@expressive-code/plugin-shiki": "^0.44.2", + "@expressive-code/plugin-text-markers": "^0.44.2" } }, "node_modules/extend": { @@ -2849,6 +2748,30 @@ "integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==", "license": "MIT" }, + "node_modules/fast-string-truncated-width": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/fast-string-truncated-width/-/fast-string-truncated-width-3.0.3.tgz", + "integrity": "sha512-0jjjIEL6+0jag3l2XWWizO64/aZVtpiGE3t0Zgqxv0DPuxiMjvB3M24fCyhZUO4KomJQPj3LTSUnDP3GpdwC0g==", + "license": "MIT" + }, + "node_modules/fast-string-width": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/fast-string-width/-/fast-string-width-3.0.2.tgz", + "integrity": "sha512-gX8LrtNEI5hq8DVUfRQMbr5lpaS4nMIWV+7XEbXk2b8kiQIizgnlr12B4dA3ZEx3308ze0O4Q1R+cHts8kyUJg==", + "license": "MIT", + "dependencies": { + "fast-string-truncated-width": "^3.0.2" + } + }, + "node_modules/fast-wrap-ansi": { + "version": "0.2.2", + "resolved": "https://registry.npmjs.org/fast-wrap-ansi/-/fast-wrap-ansi-0.2.2.tgz", + "integrity": "sha512-7F2Fl+TjRSenLqlU3UjSH0iyqopqoZIu7eZVpEirP2g1GtWa2G/ecEmBdgz31+Mxr+ELclgg6sokpSFIQiZ02Q==", + "license": "MIT", + "dependencies": { + "fast-string-width": "^3.0.2" + } + }, "node_modules/fdir": { "version": "6.5.0", "resolved": "https://registry.npmjs.org/fdir/-/fdir-6.5.0.tgz", @@ -2866,6 +2789,15 @@ } } }, + "node_modules/find-proc": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/find-proc/-/find-proc-0.2.0.tgz", + "integrity": "sha512-a6h2nTrgB3g5Wrn4au9Ux4kMk7IJtyF6oA8uGn+mk3weLZssUtc1cp6meHIzzuW2o1srrBAxzkmXLbqjRkgB6w==", + "license": "MIT", + "engines": { + "node": "^20.19.0 || >=22.12.0" + } + }, "node_modules/flattie": { "version": "1.1.1", "resolved": "https://registry.npmjs.org/flattie/-/flattie-1.1.1.tgz", @@ -2910,6 +2842,21 @@ "node": "^8.16.0 || ^10.6.0 || >=11.0.0" } }, + "node_modules/get-tsconfig": { + "version": "5.0.0-beta.6", + "resolved": "https://registry.npmjs.org/get-tsconfig/-/get-tsconfig-5.0.0-beta.6.tgz", + "integrity": "sha512-X6fBC0pmImC70gvX2zm56go9hx0MyoGVdG0tUCkg/D+Xnh5TJsOZ7iDbOdI3PvmtrDxnu1YdDufpK2QJX1Meqw==", + "license": "MIT", + "dependencies": { + "resolve-pkg-maps": "^1.0.0" + }, + "engines": { + "node": ">=20.20.0" + }, + "funding": { + "url": "https://github.com/privatenumber/get-tsconfig?sponsor=1" + } + }, "node_modules/github-slugger": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/github-slugger/-/github-slugger-2.0.0.tgz", @@ -2917,14 +2864,14 @@ "license": "ISC" }, "node_modules/h3": { - "version": "1.15.10", - "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.10.tgz", - "integrity": "sha512-YzJeWSkDZxAhvmp8dexjRK5hxziRO7I9m0N53WhvYL5NiWfkUkzssVzY9jvGu0HBoLFW6+duYmNSn6MaZBCCtg==", + "version": "1.15.11", + "resolved": "https://registry.npmjs.org/h3/-/h3-1.15.11.tgz", + "integrity": "sha512-L3THSe2MPeBwgIZVSH5zLdBBU90TOxarvhK9d04IDY2AmVS8j2Jz2LIWtwsGOU3lu2I5jCN7FNvVfY2+XyF+mg==", "license": "MIT", "dependencies": { - "cookie-es": "^1.2.2", + "cookie-es": "^1.2.3", "crossws": "^0.3.5", - "defu": "^6.1.4", + "defu": "^6.1.6", "destr": "^2.0.5", "iron-webcrypto": "^1.2.1", "node-mock-http": "^1.0.4", @@ -2933,6 +2880,18 @@ "uncrypto": "^0.1.3" } }, + "node_modules/has-flag": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-5.0.1.tgz", + "integrity": "sha512-CsNUt5x9LUdx6hnk/E2SZLsDyvfqANZSUq4+D3D8RzDJ2M+HDTIkF60ibS1vHaK55vzgiZw1bEPFG9yH7l33wA==", + "license": "MIT", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/hast-util-embedded": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/hast-util-embedded/-/hast-util-embedded-3.0.0.tgz", @@ -3356,32 +3315,37 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", "license": "BSD-2-Clause" }, "node_modules/i18next": { - "version": "23.16.8", - "resolved": "https://registry.npmjs.org/i18next/-/i18next-23.16.8.tgz", - "integrity": "sha512-06r/TitrM88Mg5FdUXAKL96dJMzgqLE5dv3ryBAra4KCwD9mJ4ndOTS95ZuymIGoE+2hzfdaMak2X11/es7ZWg==", + "version": "26.4.4", + "resolved": "https://registry.npmjs.org/i18next/-/i18next-26.4.4.tgz", + "integrity": "sha512-O+rqmQUi54OwAgnZ4SAwO6aRrbXZt36VZXWt6zvq0FpC3vJbH46oQRs6u4Uwm1vf8ZI7UqOXQiJhQVTor7vRiQ==", "funding": [ { "type": "individual", - "url": "https://locize.com" + "url": "https://www.locize.com/i18next" }, { "type": "individual", - "url": "https://locize.com/i18next.html" + "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" }, { "type": "individual", - "url": "https://www.i18next.com/how-to/faq#i18next-is-awesome.-how-can-i-support-the-project" + "url": "https://www.locize.com" } ], "license": "MIT", - "dependencies": { - "@babel/runtime": "^7.23.2" + "peerDependencies": { + "typescript": "^5 || ^6 || ^7" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } } }, "node_modules/inline-style-parser": { @@ -3399,6 +3363,18 @@ "url": "https://github.com/sponsors/brc-dd" } }, + "node_modules/is-absolute-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/is-absolute-url/-/is-absolute-url-5.0.0.tgz", + "integrity": "sha512-sdJyNpBnQHuVnBunfzjAecOhZr2+A30ywfFvu3EnxtKLUWfwGgyWUmqHbGZiU6vTfHpCPm5GvLe4BAvlU9n8VQ==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/is-alphabetical": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/is-alphabetical/-/is-alphabetical-2.0.1.tgz", @@ -3434,15 +3410,15 @@ } }, "node_modules/is-docker": { - "version": "3.0.0", - "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-3.0.0.tgz", - "integrity": "sha512-eljcgEDlEns/7AXFosB5K/2nCM4P7FQPkGc/DWLy5rmFEWvZayGrik1d9/QIY5nJ4f9YsVvBkA6kJpHn9rISdQ==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/is-docker/-/is-docker-4.0.0.tgz", + "integrity": "sha512-LHE+wROyG/Y/0ZnbktRCoTix2c1RhgWaZraMZ8o1Q7zCh0VSrICJQO5oqIIISrcSBtrXv0o233w1IYwsWCjTzA==", "license": "MIT", "bin": { "is-docker": "cli.js" }, "engines": { - "node": "^12.20.0 || ^14.13.1 || >=16.0.0" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -3458,24 +3434,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/is-inside-container": { - "version": "1.0.0", - "resolved": "https://registry.npmjs.org/is-inside-container/-/is-inside-container-1.0.0.tgz", - "integrity": "sha512-KIYLCCJghfHZxqjYBE7rEy0OBuTd5xCHS7tHVgvCLkx7StIoaxwNW3hCALgEUjFfeRk+MG/Qxmp/vtETEF3tRA==", - "license": "MIT", - "dependencies": { - "is-docker": "^3.0.0" - }, - "bin": { - "is-inside-container": "cli.js" - }, - "engines": { - "node": ">=14.16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/is-plain-obj": { "version": "4.1.0", "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-4.1.0.tgz", @@ -3488,25 +3446,20 @@ "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/is-wsl": { - "version": "3.1.1", - "resolved": "https://registry.npmjs.org/is-wsl/-/is-wsl-3.1.1.tgz", - "integrity": "sha512-e6rvdUCiQCAuumZslxRJWR/Doq4VpPR82kqclvcS0efgt430SlGIk05vdCN58+VrzgtIcfNODjozVielycD4Sw==", - "license": "MIT", - "dependencies": { - "is-inside-container": "^1.0.0" - }, - "engines": { - "node": ">=16" - }, - "funding": { - "url": "https://github.com/sponsors/sindresorhus" - } - }, "node_modules/js-yaml": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", - "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/puzrin" + }, + { + "type": "github", + "url": "https://github.com/sponsors/nodeca" + } + ], "license": "MIT", "dependencies": { "argparse": "^2.0.1" @@ -3515,31 +3468,28 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsonc-parser": { + "version": "3.3.1", + "resolved": "https://registry.npmjs.org/jsonc-parser/-/jsonc-parser-3.3.1.tgz", + "integrity": "sha512-HUgH65KyejrUFPvHFPbqOY0rsFip3Bo5wb4ngvdi1EpCYWUQDC5V+Y7mZws+DLkr4M//zQJoanu1SP+87Dv1oQ==", + "license": "MIT" + }, "node_modules/katex": { - "version": "0.16.44", - "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.44.tgz", - "integrity": "sha512-EkxoDTk8ufHqHlf9QxGwcxeLkWRR3iOuYfRpfORgYfqc8s13bgb+YtRY59NK5ZpRaCwq1kqA6a5lpX8C/eLphQ==", + "version": "0.19.0", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.19.0.tgz", + "integrity": "sha512-v6Tznz3zJ7u3niRCoDTsumM2+HA2XXcCu+WAacCeHD2z3p9A9Ks987o5FzfTGBN0e8A0vjEgIDvLbICrXpdw/Q==", "funding": [ "https://opencollective.com/katex", "https://github.com/sponsors/katex" ], "license": "MIT", "dependencies": { - "commander": "^8.3.0" + "commander": "^15.0.0" }, "bin": { "katex": "cli.js" } }, - "node_modules/katex/node_modules/commander": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", - "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", - "license": "MIT", - "engines": { - "node": ">= 12" - } - }, "node_modules/klona": { "version": "2.0.6", "resolved": "https://registry.npmjs.org/klona/-/klona-2.0.6.tgz", @@ -3549,6 +3499,255 @@ "node": ">= 8" } }, + "node_modules/lightningcss": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss/-/lightningcss-1.33.0.tgz", + "integrity": "sha512-WkUDrojuJs0xkgGf2udWxa3yGBRxPtxUkB79i6aCZLRgc7PM8fZe9TosfPDcvEpQZbuFASnHYmRLBLUbmLOIIA==", + "license": "MPL-2.0", + "dependencies": { + "detect-libc": "^2.0.3" + }, + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + }, + "optionalDependencies": { + "lightningcss-android-arm64": "1.33.0", + "lightningcss-darwin-arm64": "1.33.0", + "lightningcss-darwin-x64": "1.33.0", + "lightningcss-freebsd-x64": "1.33.0", + "lightningcss-linux-arm-gnueabihf": "1.33.0", + "lightningcss-linux-arm64-gnu": "1.33.0", + "lightningcss-linux-arm64-musl": "1.33.0", + "lightningcss-linux-x64-gnu": "1.33.0", + "lightningcss-linux-x64-musl": "1.33.0", + "lightningcss-win32-arm64-msvc": "1.33.0", + "lightningcss-win32-x64-msvc": "1.33.0" + } + }, + "node_modules/lightningcss-android-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-android-arm64/-/lightningcss-android-arm64-1.33.0.tgz", + "integrity": "sha512-gEpRTalKdosp4Bb8qWtc2iOgE5SeIHlpS1up9bFq2wAyYhl1UdTObYiHe98zEM9SQvSoqQZ1IQD0JNpg3Ml5pg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "android" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-arm64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-arm64/-/lightningcss-darwin-arm64-1.33.0.tgz", + "integrity": "sha512-Sciaz8eenNTKn9b3t7+xr0ipTp9YxKQY4npwQ3mrRuL0BAVHBLyZxofhaKBAVtzmtRZ/zTyo0/to4B1uWG/Djg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-darwin-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-darwin-x64/-/lightningcss-darwin-x64-1.33.0.tgz", + "integrity": "sha512-Z5UPAxzrjlWNNyGy6i65cJzzvgJ5D3T6wMvs+gWpY9d7qRhANrxqAp6LhxIgZhWEw18RfJTGcRxjuLIBr+m8XQ==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-freebsd-x64": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-freebsd-x64/-/lightningcss-freebsd-x64-1.33.0.tgz", + "integrity": "sha512-QQM/Ti/hQajJwCY+RiWuCZ9sdtI/XQk7nDK5vC8kkdwixezOlDgvDx7+RT+QjK6FcFT4MpsuoBnHIo/O3StRRg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "freebsd" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm-gnueabihf": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm-gnueabihf/-/lightningcss-linux-arm-gnueabihf-1.33.0.tgz", + "integrity": "sha512-N7FVBe6iS24MlM6R/4RBTxGhQheZGs7tiQ9U32UtF75NzP5Q7xWPRqLBCKxlRQRk3rY1jCIPLzx7WzOhuUIRLQ==", + "cpu": [ + "arm" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-gnu/-/lightningcss-linux-arm64-gnu-1.33.0.tgz", + "integrity": "sha512-j2v/itmy4HlNxlc6voKXYgBqNi0Ng2LShg4z7GufpEgs05P+2suBVyi9I6YHq5uoVFx9ETin3eCEhLVyXGQnKg==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-arm64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-arm64-musl/-/lightningcss-linux-arm64-musl-1.33.0.tgz", + "integrity": "sha512-yiO5ROMuYQgXbC60yjZU5CYSFZGKXL0HFATXt9mHJn1+zW55oCtMI9NfcVhYLMFDL7gV7oBPon/EmMMGg2OvtQ==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-gnu": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-gnu/-/lightningcss-linux-x64-gnu-1.33.0.tgz", + "integrity": "sha512-ar+Ju7LmcN0Jo4FpL4hpFybwNG9/3A/Br5KW2n2jyODg3MEZXaDYADdemoNS+BDNfMgKvylJLj4S5tyRActuAg==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-linux-x64-musl": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-linux-x64-musl/-/lightningcss-linux-x64-musl-1.33.0.tgz", + "integrity": "sha512-RYiYbkokw0trfKqqzfF55lginwEPrD3OJDfTuJzFs1MK6iFnDenaz1fqLLtX4ITG3OktJQXOeTaw1awrBAlZPw==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-arm64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-arm64-msvc/-/lightningcss-win32-arm64-msvc-1.33.0.tgz", + "integrity": "sha512-1K+MPfLSFVpphzpdbfkhlWk6wBrTObBzS2T6db10PNOZgR9GoVsAWzwNyuhUYYbTp23j+4RrncfujZ4uAzXvwA==", + "cpu": [ + "arm64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, + "node_modules/lightningcss-win32-x64-msvc": { + "version": "1.33.0", + "resolved": "https://registry.npmjs.org/lightningcss-win32-x64-msvc/-/lightningcss-win32-x64-msvc-1.33.0.tgz", + "integrity": "sha512-OlEICDx/Xl0FqSp4bry8zFnCvGpig3Gl4gCquvYwHuqJKEC1+n9NgDniFvqHGmMv1ZkqDJrDqKKSykTDX+ehuA==", + "cpu": [ + "x64" + ], + "license": "MPL-2.0", + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 12.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/parcel" + } + }, "node_modules/longest-streak": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/longest-streak/-/longest-streak-3.1.0.tgz", @@ -3560,31 +3759,31 @@ } }, "node_modules/lru-cache": { - "version": "11.2.7", - "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.2.7.tgz", - "integrity": "sha512-aY/R+aEsRelme17KGQa/1ZSIpLpNYYrhcrepKTZgE+W3WM16YMCaPwOHLHsmopZHELU0Ojin1lPVxKR0MihncA==", + "version": "11.5.3", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.5.3.tgz", + "integrity": "sha512-U4N8FgzmWxc8k1VH8Kr6lQg18U7Fjvby6wXHVRX/ZZ7IwWbRMgrRbP0Wrb5q5NVinryp4SQampHKdvtecItxUg==", "license": "BlueOak-1.0.0", "engines": { "node": "20 || >=22" } }, "node_modules/magic-string": { - "version": "0.30.21", - "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-0.30.21.tgz", - "integrity": "sha512-vd2F4YUyEXKGcLHoq+TEyCjxueSeHnFxyyjNp80yg0XV4vUhnDer/lvvlqM/arB5bXQN5K2/3oinyCRyx8T2CQ==", + "version": "1.4.3", + "resolved": "https://registry.npmjs.org/magic-string/-/magic-string-1.4.3.tgz", + "integrity": "sha512-z12OxmaPGE0F4xlpGdjuAUckipLpQlTAuAmyGhNoD7ODpYUzvDfvtxUbjM/jwznyP178oju/KDdyi220wNJ0RQ==", "license": "MIT", "dependencies": { - "@jridgewell/sourcemap-codec": "^1.5.5" + "@jridgewell/sourcemap-codec": "^1.6.0" } }, "node_modules/magicast": { - "version": "0.5.2", - "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.2.tgz", - "integrity": "sha512-E3ZJh4J3S9KfwdjZhe2afj6R9lGIN5Pher1pF39UGrXRqq/VDaGVIGN13BjHd2u8B61hArAGOnso7nBOouW3TQ==", + "version": "0.5.5", + "resolved": "https://registry.npmjs.org/magicast/-/magicast-0.5.5.tgz", + "integrity": "sha512-UicdXN8zQ3JHlxVq+28afMXPr1z7WNY6+7EJnzTdQWkTAlMLF5fNCCKxJHBQwGaNGR11581EiQmQzx73+MvszA==", "license": "MIT", "dependencies": { - "@babel/parser": "^7.29.0", - "@babel/types": "^7.29.0", + "@babel/parser": "^7.29.7", + "@babel/types": "^7.29.7", "source-map-js": "^1.2.1" } }, @@ -3626,9 +3825,9 @@ } }, "node_modules/mdast-util-directive": { - "version": "3.1.0", - "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.0.tgz", - "integrity": "sha512-I3fNFt+DHmpWCYAT7quoM6lHf9wuqtI+oCOfvILnoicNIqjh5E3dEJWiXuYME2gNe8vl1iMQwyUHa7bgFmak6Q==", + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/mdast-util-directive/-/mdast-util-directive-3.1.1.tgz", + "integrity": "sha512-Gqpa3MHorXYEZ0VfbroyvBlVKUfz371V6mIERqOYMcb0W1D3LwmFq1ZW4O2TDrbn+4ZmGLwMWeFRJgYZ+bc5fQ==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3637,6 +3836,7 @@ "devlop": "^1.0.0", "mdast-util-from-markdown": "^2.0.0", "mdast-util-to-markdown": "^2.0.0", + "micromark-util-character": "^2.0.0", "parse-entities": "^4.0.0", "stringify-entities": "^4.0.0", "unist-util-visit-parents": "^6.0.0" @@ -3647,9 +3847,9 @@ } }, "node_modules/mdast-util-find-and-replace": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.2.tgz", - "integrity": "sha512-Tmd1Vg/m3Xz43afeNxDIhWRtFZgM2VLyaf4vSTYwudTyeuTneoL3qtWMA5jeLyz/O1vDJmmV4QuScFCA2tBPwg==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/mdast-util-find-and-replace/-/mdast-util-find-and-replace-3.0.3.tgz", + "integrity": "sha512-xPgpDNl0/OXHsI7TlaIs22lWnH3KlpvZdXG1ET/m/YT2Hhdkx8lJV4kLphE6l9simyM4KPDFBwas08BgEob6Jw==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3663,9 +3863,9 @@ } }, "node_modules/mdast-util-from-markdown": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.0.3.tgz", - "integrity": "sha512-W4mAWTvSlKvf8L6J+VN9yLSqQ9AOAAvHuoDAmPkz4dHf553m5gVj2ejadHJhoJmcmxEnOv6Pa8XJhpxE93kb8Q==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mdast-util-from-markdown/-/mdast-util-from-markdown-2.1.0.tgz", + "integrity": "sha512-KRLgmPaP2H1ul2bpoJkOS+9pAcDAAj9Nu1mqLnh8NLeJG5tlvj42TH80PIGBpROWlRXLk0lDrWNuZCFy/tsB4A==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3740,9 +3940,9 @@ } }, "node_modules/mdast-util-gfm-strikethrough": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.0.tgz", - "integrity": "sha512-mKKb915TF+OC5ptj5bJ7WFRPdYtuHv0yTRxK2tJvi+BDqbkiG7h7u/9SI89nRAYcmap2xHQL9D+QG/6wSrTtXg==", + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/mdast-util-gfm-strikethrough/-/mdast-util-gfm-strikethrough-2.0.1.tgz", + "integrity": "sha512-OuJHqvr455pwu2OaOrir7dbzqs4jlWKOtlu9L6GjcIjjQwNyw02VntQsr/Ek6/A13vgnXfUhBekWRUP1OkNivw==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3919,9 +4119,9 @@ } }, "node_modules/mdast-util-to-markdown": { - "version": "2.1.2", - "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.1.2.tgz", - "integrity": "sha512-xj68wMTvGXVOKonmog6LwyJKrYXZPvlwabaryTjLh9LuvovB/KAH+kvi8Gjj+7rJjsFi23nkUxRQv1KqSroMqA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/mdast-util-to-markdown/-/mdast-util-to-markdown-2.2.0.tgz", + "integrity": "sha512-2Jn/ADrHFFyyniBMi0bncTbJqU78K3/TEYvzvwrWWzD9MYPUa/Gm4r3nOeKFQg/PDM5BsbDYdc8ackW4NTY6NA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", @@ -3929,8 +4129,10 @@ "longest-streak": "^3.0.0", "mdast-util-phrasing": "^4.0.0", "mdast-util-to-string": "^4.0.0", + "micromark-util-character": "^2.0.0", "micromark-util-classify-character": "^2.0.0", "micromark-util-decode-string": "^2.0.0", + "micromark-util-html-tag-name": "^2.0.0", "unist-util-visit": "^5.0.0", "zwitch": "^2.0.0" }, @@ -3959,9 +4161,9 @@ "license": "CC0-1.0" }, "node_modules/micromark": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.2.tgz", - "integrity": "sha512-zpe98Q6kvavpCr1NPVSCMebCKfD7CA2NqZ+rykeNhONIJBpc1tFKt9hucLGwha3jNTNI8lHpctWJWoimVF4PfA==", + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/micromark/-/micromark-4.0.3.tgz", + "integrity": "sha512-oGYfQzHSG5dOMovQcJ3fyTmZlWAWpi0XA0sJwJs+i6OT88o1+Jtw/8z0CmdowSLxGhhFK89rQ/oXph/wN02PNw==", "funding": [ { "type": "GitHub Sponsors", @@ -3984,6 +4186,7 @@ "micromark-util-chunked": "^2.0.0", "micromark-util-combine-extensions": "^2.0.0", "micromark-util-decode-numeric-character-reference": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-encode": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", @@ -3994,9 +4197,9 @@ } }, "node_modules/micromark-core-commonmark": { - "version": "2.0.3", - "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.3.tgz", - "integrity": "sha512-RDBrHEMSxVFLg6xvnXmb1Ayr2WzLAWjeSATAoxwKYJV94TeNavgoIdA0a9ytzDSVzBy2YKFK+emCPOEibLeCrg==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/micromark-core-commonmark/-/micromark-core-commonmark-2.0.4.tgz", + "integrity": "sha512-wxEeE8v8XVvOrxn1TZj74qYhAtTQsSqufHVS3uNVyT1MupXxhyaHk8/9xDNPh9BmL77QNDgWTkZ3RsYHB2ry7w==", "funding": [ { "type": "GitHub Sponsors", @@ -4013,24 +4216,25 @@ "devlop": "^1.0.0", "micromark-factory-destination": "^2.0.0", "micromark-factory-label": "^2.0.0", - "micromark-factory-space": "^2.0.0", + "micromark-factory-space": "^2.1.0", "micromark-factory-title": "^2.0.0", "micromark-factory-whitespace": "^2.0.0", "micromark-util-character": "^2.0.0", "micromark-util-chunked": "^2.0.0", "micromark-util-classify-character": "^2.0.0", + "micromark-util-edit-map": "^1.0.0", "micromark-util-html-tag-name": "^2.0.0", "micromark-util-normalize-identifier": "^2.0.0", "micromark-util-resolve-all": "^2.0.0", "micromark-util-subtokenize": "^2.0.0", "micromark-util-symbol": "^2.0.0", - "micromark-util-types": "^2.0.0" + "micromark-util-types": "^2.0.3" } }, "node_modules/micromark-extension-directive": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-3.0.2.tgz", - "integrity": "sha512-wjcXHgk+PPdmvR58Le9d7zQYWy+vKEU9Se44p2CrCDPiLr2FMyiT4Fyb5UFKFC66wGB3kPlgD7q3TnoqPS7SZA==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/micromark-extension-directive/-/micromark-extension-directive-4.0.0.tgz", + "integrity": "sha512-/C2nqVmXXmiseSSuCdItCMho7ybwwop6RrrRPk0KbOHW21JKoCldC+8rFOaundDoRBUWBnJJcxeA/Kvi34WQXg==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -4121,9 +4325,9 @@ } }, "node_modules/micromark-extension-gfm-table": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.1.tgz", - "integrity": "sha512-t2OU/dXXioARrC6yWfJ4hqB7rct14e8f7m0cbI5hUmDyyIlwv5vEtooptH8INkbLzOatzKuVbQmAYcbWoyz6Dg==", + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/micromark-extension-gfm-table/-/micromark-extension-gfm-table-2.1.2.tgz", + "integrity": "sha512-pRzm4kDTu0MjlmBkxmS9yYhw60nncfcEwu9NNdPFSQEFXS95ZKyIIyTSHu/o3ReBUrLKYEq+7YaXCRn/bPB4MA==", "license": "MIT", "dependencies": { "devlop": "^1.0.0", @@ -4186,6 +4390,31 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/micromark-extension-math/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/micromark-extension-math/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], + "license": "MIT", + "dependencies": { + "commander": "^8.3.0" + }, + "bin": { + "katex": "cli.js" + } + }, "node_modules/micromark-extension-mdx-expression": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/micromark-extension-mdx-expression/-/micromark-extension-mdx-expression-3.0.1.tgz", @@ -4359,9 +4588,9 @@ } }, "node_modules/micromark-factory-space": { - "version": "2.0.1", - "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.0.1.tgz", - "integrity": "sha512-zRkxjtBxxLd2Sc0d+fbnEunsTj46SWXgXciZmHq0kDYGnck/ZSGj9/wULTV95uoeYiK5hRXP2mJ98Uo4cq/LQg==", + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/micromark-factory-space/-/micromark-factory-space-2.1.0.tgz", + "integrity": "sha512-fS8hnLIjnjvdQIj39Geug8wWsR0HrYZ43KShKxNfwT7t2LOHo/LbWZEzEaSOjwtjdCsjoE6syHhonEzW0zv0+Q==", "funding": [ { "type": "GitHub Sponsors", @@ -4543,6 +4772,25 @@ "micromark-util-symbol": "^2.0.0" } }, + "node_modules/micromark-util-edit-map": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/micromark-util-edit-map/-/micromark-util-edit-map-1.0.0.tgz", + "integrity": "sha512-Pa2ljlsEL6sVwFaYeyrOLSYbQt73JvGbPOYFq+9AElXiSnfI5Q4465RRZ1sHv7lDjDOnGRDDaqPXqClqWK/q1Q==", + "funding": [ + { + "type": "GitHub Sponsors", + "url": "https://github.com/sponsors/unifiedjs" + }, + { + "type": "OpenCollective", + "url": "https://opencollective.com/unified" + } + ], + "license": "MIT", + "dependencies": { + "micromark-util-types": "^2.0.0" + } + }, "node_modules/micromark-util-encode": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/micromark-util-encode/-/micromark-util-encode-2.0.1.tgz", @@ -4698,9 +4946,9 @@ "license": "MIT" }, "node_modules/micromark-util-types": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.2.tgz", - "integrity": "sha512-Yw0ECSpJoViF1qTU4DC6NwtC4aWGt1EkzaQB8KPPyCRR8z9TWeV0HbEFGTO+ZY1wB22zmxnJqhPyTpOVCpeHTA==", + "version": "2.0.3", + "resolved": "https://registry.npmjs.org/micromark-util-types/-/micromark-util-types-2.0.3.tgz", + "integrity": "sha512-oxB2Ik03hI0gv+VNn9tnh1t1YEe9MDPptViAEgfdf3YQHsn0pzGTgCdlSCJXcwhqm8phaEuM7zeEu3QQzVBrPg==", "funding": [ { "type": "GitHub Sponsors", @@ -4729,9 +4977,9 @@ "license": "MIT" }, "node_modules/nanoid": { - "version": "3.3.11", - "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", - "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "version": "3.3.20", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.20.tgz", + "integrity": "sha512-uKdg2G3GNCKQn9byYOpxbGqrT2fGO5KRt5J/8b3pok8rT6qxGWF6hxMyJiEYtAf+FVyYuD9hRaDqX5uPFYJ4ZQ==", "funding": [ { "type": "github", @@ -4747,9 +4995,9 @@ } }, "node_modules/neotraverse": { - "version": "0.6.18", - "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-0.6.18.tgz", - "integrity": "sha512-Z4SmBUweYa09+o6pG+eASabEpP6QkQ70yHj351pQoEXIs8uHbaU2DWVmzBANKgflPa47A50PtB2+NgRpQvr7vA==", + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/neotraverse/-/neotraverse-1.0.1.tgz", + "integrity": "sha512-WmmLty1YWwJl9yZi77v2dVIV6X2kuYV8YYBI/G3LWGKdGHmHUvL1z7FW0iDvEvGAwNEoc5x1tOOOyDnf5jJw/w==", "license": "MIT", "engines": { "node": ">= 10" @@ -4775,9 +5023,9 @@ "license": "MIT" }, "node_modules/node-mock-http": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.4.tgz", - "integrity": "sha512-8DY+kFsDkNXy1sJglUfuODx1/opAGJGyrTuFqEoN90oRc2Vk0ZbD4K2qmKXBBEhZQzdKHIVfEJpDU8Ak2NJEvQ==", + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/node-mock-http/-/node-mock-http-1.0.5.tgz", + "integrity": "sha512-KQyt/wLjG3TAc7DOUhpqWzgd4ERxR80JOlTK5VE5R1S12IaPVN5qkj4klBce9HPG1Njuup4Sb5bljaT34lIyjw==", "license": "MIT" }, "node_modules/normalize-path": { @@ -4802,14 +5050,17 @@ } }, "node_modules/obug": { - "version": "2.1.1", - "resolved": "https://registry.npmjs.org/obug/-/obug-2.1.1.tgz", - "integrity": "sha512-uTqF9MuPraAQ+IsnPf366RG4cP9RtUi7MLO1N3KEc+wb0a6yKpeL0lmk2IB1jY5KHPAlTc6T/JRdC/YqxHNwkQ==", + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/obug/-/obug-3.0.0.tgz", + "integrity": "sha512-5vvB5+W7ePv+p3uqxi+RcW1XAzLW0/hxt3/4X4Lc4qHudzOhmBiBwOY6DRob4WnanAEGvNcLjF+KNOufrUoEQw==", "funding": [ "https://github.com/sponsors/sxzz", "https://opencollective.com/debug" ], - "license": "MIT" + "license": "MIT", + "engines": { + "node": ">=12.20.0" + } }, "node_modules/ofetch": { "version": "1.5.1", @@ -4823,32 +5074,32 @@ } }, "node_modules/ohash": { - "version": "2.0.11", - "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.11.tgz", - "integrity": "sha512-RdR9FQrFwNBNXAr4GixM8YaRZRJ5PUWbKYbE5eOsrwAjJW0q2REGcf79oYPsLyskQCZG1PLN+S/K1V00joZAoQ==", + "version": "2.0.12", + "resolved": "https://registry.npmjs.org/ohash/-/ohash-2.0.12.tgz", + "integrity": "sha512-65S/5gk9YSsaRjcyf7Nfa6h/d3E8/1gslpXfI4W7Dxn/oap8IKRuNT5VXkLQ1YFKIEg4apRY4Pj6aiwFzrDdmw==", "license": "MIT" }, "node_modules/oniguruma-parser": { - "version": "0.12.1", - "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.1.tgz", - "integrity": "sha512-8Unqkvk1RYc6yq2WBYRj4hdnsAxVze8i7iPfQr8e4uSP3tRv0rpZcbGUDvxfQQcdwHt/e9PrMvGCsa8OqG9X3w==", + "version": "0.12.2", + "resolved": "https://registry.npmjs.org/oniguruma-parser/-/oniguruma-parser-0.12.2.tgz", + "integrity": "sha512-6HVa5oIrgMC6aA6WF6XyyqbhRPJrKR02L20+2+zpDtO5QAzGHAUGw5TKQvwi5vctNnRHkJYmjAhRVQF2EKdTQw==", "license": "MIT" }, "node_modules/oniguruma-to-es": { - "version": "4.3.5", - "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.5.tgz", - "integrity": "sha512-Zjygswjpsewa0NLTsiizVuMQZbp0MDyM6lIt66OxsF21npUDlzpHi1Mgb/qhQdkb+dWFTzJmFbEWdvZgRho8eQ==", + "version": "4.3.6", + "resolved": "https://registry.npmjs.org/oniguruma-to-es/-/oniguruma-to-es-4.3.6.tgz", + "integrity": "sha512-csuQ9x3Yr0cEIs/Zgx/OEt9iBw9vqIunAPQkx19R/fiMq2oGVTgcMqO/V3Ybqefr1TBvosI6jU539ksaBULJyA==", "license": "MIT", "dependencies": { - "oniguruma-parser": "^0.12.1", + "oniguruma-parser": "^0.12.2", "regex": "^6.1.0", "regex-recursion": "^6.0.2" } }, "node_modules/p-limit": { - "version": "7.3.0", - "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.0.tgz", - "integrity": "sha512-7cIXg/Z0M5WZRblrsOla88S4wAK+zOQQWeBYfV3qJuJXMr+LnbYjaadrFaS0JILfEDPVqHyKnZ1Z/1d6J9VVUw==", + "version": "7.3.3", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-7.3.3.tgz", + "integrity": "sha512-SKgVvDU5TNIxQ4r30U75BNLC6a+6GOEjngrk/ysM5+Zu3oJuk1SC5ApzIzY/7PjCEjoVPMWJ0zho/4QUiEB7TQ==", "license": "MIT", "dependencies": { "yocto-queue": "^1.2.1" @@ -4861,12 +5112,12 @@ } }, "node_modules/p-queue": { - "version": "9.1.0", - "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.1.0.tgz", - "integrity": "sha512-O/ZPaXuQV29uSLbxWBGGZO1mCQXV2BLIwUr59JUU9SoH76mnYvtms7aafH/isNSNGwuEfP6W/4xD0/TJXxrizw==", + "version": "9.3.5", + "resolved": "https://registry.npmjs.org/p-queue/-/p-queue-9.3.5.tgz", + "integrity": "sha512-Lki/zhXFVmDhesBUn/yHRLW9Bte7mE7xoFHzTR8sIomV7a2z+LxYAPkGvdfidNiu4fKSfjtawixxSHcZci5QcQ==", "license": "MIT", "dependencies": { - "eventemitter3": "^5.0.1", + "eventemitter3": "^5.0.4", "p-timeout": "^7.0.0" }, "engines": { @@ -4877,9 +5128,9 @@ } }, "node_modules/p-timeout": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.1.tgz", - "integrity": "sha512-AxTM2wDGORHGEkPCt8yqxOTMgpfbEHqF51f/5fJCmwFC3C/zNcGT63SymH2ttOAaiIws2zVg4+izQCjrakcwHg==", + "version": "7.0.2", + "resolved": "https://registry.npmjs.org/p-timeout/-/p-timeout-7.0.2.tgz", + "integrity": "sha512-prbX4Z3YszrFNgH+MW5Zoeq3baXrMtP/MQnFeET90UB/GtGcGDQ5Usg9OCy6ETjTTntOw1SL2z9fMPUppN3Guw==", "license": "MIT", "engines": { "node": ">=20" @@ -4889,26 +5140,27 @@ } }, "node_modules/package-manager-detector": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.6.0.tgz", - "integrity": "sha512-61A5ThoTiDG/C8s8UMZwSorAGwMJ0ERVGj2OjoW5pAalsNOg15+iQiPzrLJ4jhZ1HJzmC2PIHT2oEiH3R5fzNA==", + "version": "1.9.0", + "resolved": "https://registry.npmjs.org/package-manager-detector/-/package-manager-detector-1.9.0.tgz", + "integrity": "sha512-zAMfbta9nPA8AKkA4WdmNAT8R8s6Dk0/xwPsqNnRidMFcLo1YkLuPDTgs2f64XdzS8vbLNnoJ+uUIkgbz7oRHA==", "license": "MIT" }, "node_modules/pagefind": { - "version": "1.4.0", - "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.4.0.tgz", - "integrity": "sha512-z2kY1mQlL4J8q5EIsQkLzQjilovKzfNVhX8De6oyE6uHpfFtyBaqUpcl/XzJC/4fjD8vBDyh1zolimIcVrCn9g==", + "version": "1.5.2", + "resolved": "https://registry.npmjs.org/pagefind/-/pagefind-1.5.2.tgz", + "integrity": "sha512-XTUaK0hXMCu2jszWE584JGQT7y284TmMV9l/HX3rnG5uo3rHI/uHU56XTyyyPFjeWEBxECbAi0CaFDJOONtG0Q==", "license": "MIT", "bin": { "pagefind": "lib/runner/bin.cjs" }, "optionalDependencies": { - "@pagefind/darwin-arm64": "1.4.0", - "@pagefind/darwin-x64": "1.4.0", - "@pagefind/freebsd-x64": "1.4.0", - "@pagefind/linux-arm64": "1.4.0", - "@pagefind/linux-x64": "1.4.0", - "@pagefind/windows-x64": "1.4.0" + "@pagefind/darwin-arm64": "1.5.2", + "@pagefind/darwin-x64": "1.5.2", + "@pagefind/freebsd-x64": "1.5.2", + "@pagefind/linux-arm64": "1.5.2", + "@pagefind/linux-x64": "1.5.2", + "@pagefind/windows-arm64": "1.5.2", + "@pagefind/windows-x64": "1.5.2" } }, "node_modules/parse-entities": { @@ -4979,9 +5231,9 @@ "license": "ISC" }, "node_modules/picomatch": { - "version": "4.0.4", - "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.4.tgz", - "integrity": "sha512-QP88BAKvMam/3NxH6vj2o21R6MjxZUAd6nlwAS/pnGvN9IVLocLHxGYIzFhg6fUQ+5th6P4dv4eW9jX3DSIj7A==", + "version": "4.0.7", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-4.0.7.tgz", + "integrity": "sha512-qcJu88Q2IWqJsDD529JKMdwGm/dvInW4HvQnRwiH9JtihJvzGOscDtHE3x1pBKeUOTysQ8kVmLnJ2kJu7yhcGA==", "license": "MIT", "engines": { "node": ">=12" @@ -4991,9 +5243,9 @@ } }, "node_modules/postcss": { - "version": "8.5.8", - "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", - "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "version": "8.5.30", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.30.tgz", + "integrity": "sha512-zkd+e9S1AQHhmaBKye9zlFHWjOwVgqXl1q3ECYy8561K7qxR2cREf/STLskulBFd78k6AOFeA+ff1TtEzk9bAA==", "funding": [ { "type": "opencollective", @@ -5010,9 +5262,9 @@ ], "license": "MIT", "dependencies": { - "nanoid": "^3.3.11", + "nanoid": "^3.3.20", "picocolors": "^1.1.1", - "source-map-js": "^1.2.1" + "source-map-js": "^1.2.2" }, "engines": { "node": "^10 || ^12 || >=14" @@ -5044,9 +5296,9 @@ } }, "node_modules/postcss-selector-parser": { - "version": "6.1.2", - "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.2.tgz", - "integrity": "sha512-Q8qQfPiZ+THO/3ZrOrO0cJJKfpYCagtMUkXbnEfmgUjwXg6z/WBeOyS9APBBPCTSiDV+s4SwQGu8yFsiMRIudg==", + "version": "6.1.4", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.1.4.tgz", + "integrity": "sha512-bIoJLOmjCO1S9XdY/DcnR5hJxvrDir1PbGChrzXG3vw0/FOliy/fA3dmdhQ441kah4gKv+TwckGzex6wNS5cnQ==", "license": "MIT", "dependencies": { "cssesc": "^3.0.0", @@ -5065,10 +5317,19 @@ "node": ">=6" } }, + "node_modules/process-ancestry": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/process-ancestry/-/process-ancestry-0.1.0.tgz", + "integrity": "sha512-tGqJW/UnclpYASFcM6Xh8D8l/BMtaQ9+CSG0vlJSJTcdMM4lDRv4c6H0Pdcsfted+bVczdYSfk2fdukg2gQkZg==", + "license": "MIT", + "engines": { + "node": ">=18.0.0" + } + }, "node_modules/property-information": { - "version": "7.1.0", - "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.1.0.tgz", - "integrity": "sha512-TwEZ+X+yCJmYfL7TPUOcvBZ4QfoT5YenQiJuX//0th53DE6w0xxLEtfK3iyryQFddXuvkIk51EEgrJQ0WJkOmQ==", + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/property-information/-/property-information-7.2.0.tgz", + "integrity": "sha512-IAtzIB6sUiWaJYrX9smp3V46pBGbBeLFRGdh25kg1334VcBlD8HzhPeNIWQH9zhGmo2itIe25EHt9dQP7G5hmg==", "license": "MIT", "funding": { "type": "github", @@ -5082,9 +5343,9 @@ "license": "MIT" }, "node_modules/readdirp": { - "version": "5.0.0", - "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.0.0.tgz", - "integrity": "sha512-9u/XQ1pvrQtYyMpZe7DXKv2p5CNvyVwzUB6uhLAnQwHMSgKMBR62lc7AHljaeteeHXn11XTAaLLUVZYVZyuRBQ==", + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-5.1.1.tgz", + "integrity": "sha512-Kko+Y5XQ6fM+Ce3dq3m9YGxnacYZYl9cA1wZjaF3Vbry2L3i1qVg8+CAgNPsXRArPMUMCaOR7oa9Nqntc43JKA==", "license": "MIT", "engines": { "node": ">= 20.19.0" @@ -5185,43 +5446,13 @@ "integrity": "sha512-8VhliFJAWRaUiVvREIiW2NXXTmHs4vMNnSzuJVhscgmGav3g9VDxLrQndI3dZZVVdp0ZO/5v0xmX516/7M9cng==", "license": "MIT" }, - "node_modules/rehype": { - "version": "13.0.2", - "resolved": "https://registry.npmjs.org/rehype/-/rehype-13.0.2.tgz", - "integrity": "sha512-j31mdaRFrwFRUIlxGeuPXXKWQxet52RBQRvCmzl5eCefn/KGbomK5GMHNMsOJf55fgo3qw5tST5neDuarDYR2A==", - "license": "MIT", - "dependencies": { - "@types/hast": "^3.0.0", - "rehype-parse": "^9.0.0", - "rehype-stringify": "^10.0.0", - "unified": "^11.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" - } - }, "node_modules/rehype-expressive-code": { - "version": "0.41.7", - "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.41.7.tgz", - "integrity": "sha512-25f8ZMSF1d9CMscX7Cft0TSQIqdwjce2gDOvQ+d/w0FovsMwrSt3ODP4P3Z7wO1jsIJ4eYyaDRnIR/27bd/EMQ==", - "license": "MIT", - "dependencies": { - "expressive-code": "^0.41.7" - } - }, - "node_modules/rehype-format": { - "version": "5.0.1", - "resolved": "https://registry.npmjs.org/rehype-format/-/rehype-format-5.0.1.tgz", - "integrity": "sha512-zvmVru9uB0josBVpr946OR8ui7nJEdzZobwLOOqHb/OOD88W0Vk2SqLwoVOj0fM6IPCCO6TaV9CvQvJMWwukFQ==", + "version": "0.44.2", + "resolved": "https://registry.npmjs.org/rehype-expressive-code/-/rehype-expressive-code-0.44.2.tgz", + "integrity": "sha512-jvjQpsfWKBquDrEl5YQA3CJybRRIuDOYsui1BJTs5oKDUK8pPkJSSxY8FjxNlIFIdtgrTjC1zuahuY4Cc1j9SQ==", "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-format": "^1.0.0" - }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "expressive-code": "^0.44.2" } }, "node_modules/rehype-katex": { @@ -5243,19 +5474,29 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/rehype-parse": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/rehype-parse/-/rehype-parse-9.0.1.tgz", - "integrity": "sha512-ksCzCD0Fgfh7trPDxr2rSylbwq9iYDkSn8TCDmEJ49ljEUBxDVCzCHv7QNzZOfODanX4+bWQ4WZqLCRWYLfhag==", + "node_modules/rehype-katex/node_modules/commander": { + "version": "8.3.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.3.0.tgz", + "integrity": "sha512-OkTL9umf+He2DZkUq8f8J9of7yL6RJKI24dVITBmNfZBmri9zYZQrKkuXiKhyfPSu8tUhnVBB1iKXevvnlR4Ww==", + "license": "MIT", + "engines": { + "node": ">= 12" + } + }, + "node_modules/rehype-katex/node_modules/katex": { + "version": "0.16.47", + "resolved": "https://registry.npmjs.org/katex/-/katex-0.16.47.tgz", + "integrity": "sha512-Eeo8Ys1doU1z+x8AZsPpQu+p/QcZBI5PeOo7QGQdy2x2m0MU/hYagBbGOmXwr5KVbEfVuWv9LpnQWeehogurjg==", + "funding": [ + "https://opencollective.com/katex", + "https://github.com/sponsors/katex" + ], "license": "MIT", "dependencies": { - "@types/hast": "^3.0.0", - "hast-util-from-html": "^2.0.0", - "unified": "^11.0.0" + "commander": "^8.3.0" }, - "funding": { - "type": "opencollective", - "url": "https://opencollective.com/unified" + "bin": { + "katex": "cli.js" } }, "node_modules/rehype-raw": { @@ -5304,14 +5545,14 @@ } }, "node_modules/remark-directive": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-3.0.1.tgz", - "integrity": "sha512-gwglrEQEZcZYgVyG1tQuA+h58EZfq5CSULw7J90AFuCTyib1thgHPoqQ+h9iFvU6R+vnZ5oNFQR5QKgGpk741A==", + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/remark-directive/-/remark-directive-4.0.0.tgz", + "integrity": "sha512-7sxn4RfF1o3izevPV1DheyGDD6X4c9hrGpfdUpm7uC++dqrnJxIZVkk7CoKqcLm0VUMAuOol7Mno3m6g8cfMuA==", "license": "MIT", "dependencies": { "@types/mdast": "^4.0.0", "mdast-util-directive": "^3.0.0", - "micromark-extension-directive": "^3.0.0", + "micromark-extension-directive": "^4.0.0", "unified": "^11.0.0" }, "funding": { @@ -5401,9 +5642,9 @@ } }, "node_modules/remark-smartypants": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.2.tgz", - "integrity": "sha512-ILTWeOriIluwEvPjv67v7Blgrcx+LZOkAUVtKI3putuhlZm84FnqDORNXPPm+HY3NdZOMhyDwZ1E+eZB/Df5dA==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/remark-smartypants/-/remark-smartypants-3.0.3.tgz", + "integrity": "sha512-gCaK+ndZ0hYezlqFegHFCVh2CQemsi0Npdh1qVM9bxlUFknjkbP6VmojWhddOCrbK0PbbacmYLWfTULRiT1eWA==", "license": "MIT", "dependencies": { "retext": "^9.0.0", @@ -5430,6 +5671,15 @@ "url": "https://opencollective.com/unified" } }, + "node_modules/resolve-pkg-maps": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/resolve-pkg-maps/-/resolve-pkg-maps-1.0.0.tgz", + "integrity": "sha512-seS2Tj26TBVOC2NIc2rOe2y2ZO7efxITtLZcGSOnHHNOQ7CkiUBfw0Iw2ck6xkIhPwLhKNLS8BO+hEpngQlqzw==", + "license": "MIT", + "funding": { + "url": "https://github.com/privatenumber/resolve-pkg-maps?sponsor=1" + } + }, "node_modules/retext": { "version": "9.0.0", "resolved": "https://registry.npmjs.org/retext/-/retext-9.0.0.tgz", @@ -5491,63 +5741,75 @@ "url": "https://opencollective.com/unified" } }, - "node_modules/rollup": { - "version": "4.60.0", - "resolved": "https://registry.npmjs.org/rollup/-/rollup-4.60.0.tgz", - "integrity": "sha512-yqjxruMGBQJ2gG4HtjZtAfXArHomazDHoFwFFmZZl0r7Pdo7qCIXKqKHZc8yeoMgzJJ+pO6pEEHa+V7uzWlrAQ==", + "node_modules/rolldown": { + "version": "1.2.13", + "resolved": "https://registry.npmjs.org/rolldown/-/rolldown-1.2.13.tgz", + "integrity": "sha512-/5apOkwWxtACcQtZ5WlrAsKS4JwXVThQPJZ3RqIruRY9UG9M8e0VlXN068xum/JRKA4aIUoJ13ghe/kGCvidiQ==", "license": "MIT", "dependencies": { - "@types/estree": "1.0.8" + "@oxc-project/types": "=0.153.0", + "@rolldown/pluginutils": "^1.0.0" }, "bin": { - "rollup": "dist/bin/rollup" + "rolldown": "bin/cli.mjs" }, "engines": { - "node": ">=18.0.0", - "npm": ">=8.0.0" + "node": "^20.19.0 || >=22.12.0" + }, + "optionalDependencies": { + "@rolldown/binding-android-arm-eabi": "1.2.13", + "@rolldown/binding-android-arm64": "1.2.13", + "@rolldown/binding-darwin-arm64": "1.2.13", + "@rolldown/binding-darwin-x64": "1.2.13", + "@rolldown/binding-freebsd-x64": "1.2.13", + "@rolldown/binding-linux-arm-gnueabihf": "1.2.13", + "@rolldown/binding-linux-arm64-gnu": "1.2.13", + "@rolldown/binding-linux-arm64-musl": "1.2.13", + "@rolldown/binding-linux-ppc64-gnu": "1.2.13", + "@rolldown/binding-linux-s390x-gnu": "1.2.13", + "@rolldown/binding-linux-x64-gnu": "1.2.13", + "@rolldown/binding-linux-x64-musl": "1.2.13", + "@rolldown/binding-openharmony-arm64": "1.2.13", + "@rolldown/binding-win32-arm64-msvc": "1.2.13", + "@rolldown/binding-win32-x64-msvc": "1.2.13" + } + }, + "node_modules/satteri": { + "version": "0.10.5", + "resolved": "https://registry.npmjs.org/satteri/-/satteri-0.10.5.tgz", + "integrity": "sha512-Ao1LKpAEa9Wdg0otgbVKViZHEq9ebdXe4DMrp3s9vQAU0HNIuHnFEuMuOcm0ZIXyV0Yzxj91NvhLpvXZJO/5ZQ==", + "license": "MIT", + "dependencies": { + "@types/estree-jsx": "^1.0.5", + "@types/hast": "^3.0.5", + "@types/mdast": "^4.0.4", + "@types/unist": "^3.0.3" }, "optionalDependencies": { - "@rollup/rollup-android-arm-eabi": "4.60.0", - "@rollup/rollup-android-arm64": "4.60.0", - "@rollup/rollup-darwin-arm64": "4.60.0", - "@rollup/rollup-darwin-x64": "4.60.0", - "@rollup/rollup-freebsd-arm64": "4.60.0", - "@rollup/rollup-freebsd-x64": "4.60.0", - "@rollup/rollup-linux-arm-gnueabihf": "4.60.0", - "@rollup/rollup-linux-arm-musleabihf": "4.60.0", - "@rollup/rollup-linux-arm64-gnu": "4.60.0", - "@rollup/rollup-linux-arm64-musl": "4.60.0", - "@rollup/rollup-linux-loong64-gnu": "4.60.0", - "@rollup/rollup-linux-loong64-musl": "4.60.0", - "@rollup/rollup-linux-ppc64-gnu": "4.60.0", - "@rollup/rollup-linux-ppc64-musl": "4.60.0", - "@rollup/rollup-linux-riscv64-gnu": "4.60.0", - "@rollup/rollup-linux-riscv64-musl": "4.60.0", - "@rollup/rollup-linux-s390x-gnu": "4.60.0", - "@rollup/rollup-linux-x64-gnu": "4.60.0", - "@rollup/rollup-linux-x64-musl": "4.60.0", - "@rollup/rollup-openbsd-x64": "4.60.0", - "@rollup/rollup-openharmony-arm64": "4.60.0", - "@rollup/rollup-win32-arm64-msvc": "4.60.0", - "@rollup/rollup-win32-ia32-msvc": "4.60.0", - "@rollup/rollup-win32-x64-gnu": "4.60.0", - "@rollup/rollup-win32-x64-msvc": "4.60.0", - "fsevents": "~2.3.2" + "@bruits/satteri-darwin-arm64": "0.10.5", + "@bruits/satteri-darwin-x64": "0.10.5", + "@bruits/satteri-linux-arm64-gnu": "0.10.5", + "@bruits/satteri-linux-arm64-musl": "0.10.5", + "@bruits/satteri-linux-x64-gnu": "0.10.5", + "@bruits/satteri-linux-x64-musl": "0.10.5", + "@bruits/satteri-wasm32-wasi": "0.10.5", + "@bruits/satteri-win32-arm64-msvc": "0.10.5", + "@bruits/satteri-win32-x64-msvc": "0.10.5" } }, "node_modules/sax": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.0.tgz", - "integrity": "sha512-6R3J5M4AcbtLUdZmRv2SygeVaM7IhrLXu9BmnOGmmACak8fiUtOsYNWUS4uK7upbmHIBbLBeFeI//477BKLBzA==", + "version": "1.6.1", + "resolved": "https://registry.npmjs.org/sax/-/sax-1.6.1.tgz", + "integrity": "sha512-42tBVwLWnaQvW5zc4HbZrTuWccECCZfBi92FDuwtqxasH+JbPB3/FOKb1m222K42R4WxuxzzMsTswfzgtSu64Q==", "license": "BlueOak-1.0.0", "engines": { "node": ">=11.0.0" } }, "node_modules/semver": { - "version": "7.7.4", - "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", - "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "version": "7.8.5", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.8.5.tgz", + "integrity": "sha512-Y7/KDsb8LjooZpwaqGyulO6DQlksgCncchHGk+sZIY4SBvUocMBEFH5Ur1fI4dV+Jvl0w6cjvucaIi40puRioA==", "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -5557,63 +5819,68 @@ } }, "node_modules/sharp": { - "version": "0.34.5", - "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.34.5.tgz", - "integrity": "sha512-Ou9I5Ft9WNcCbXrU9cMgPBcCK8LiwLqcbywW3t4oDV37n1pzpuNLsYiAV8eODnjbtQlSDwZ2cUEeQz4E54Hltg==", - "hasInstallScript": true, + "version": "0.35.5", + "resolved": "https://registry.npmjs.org/sharp/-/sharp-0.35.5.tgz", + "integrity": "sha512-Ywn4OnzGukp7CDMrp08RQ50YKmuwG47brZgIVPTvBaaAfQlRlygrRqSrxdCiL9M+LlzLBiJ68IR1QqvzHyjC7g==", "license": "Apache-2.0", "dependencies": { - "@img/colour": "^1.0.0", + "@img/colour": "^1.1.0", "detect-libc": "^2.1.2", - "semver": "^7.7.3" + "semver": "^7.8.5" }, "engines": { - "node": "^18.17.0 || ^20.3.0 || >=21.0.0" + "node": ">=20.9.0" }, "funding": { "url": "https://opencollective.com/libvips" }, "optionalDependencies": { - "@img/sharp-darwin-arm64": "0.34.5", - "@img/sharp-darwin-x64": "0.34.5", - "@img/sharp-libvips-darwin-arm64": "1.2.4", - "@img/sharp-libvips-darwin-x64": "1.2.4", - "@img/sharp-libvips-linux-arm": "1.2.4", - "@img/sharp-libvips-linux-arm64": "1.2.4", - "@img/sharp-libvips-linux-ppc64": "1.2.4", - "@img/sharp-libvips-linux-riscv64": "1.2.4", - "@img/sharp-libvips-linux-s390x": "1.2.4", - "@img/sharp-libvips-linux-x64": "1.2.4", - "@img/sharp-libvips-linuxmusl-arm64": "1.2.4", - "@img/sharp-libvips-linuxmusl-x64": "1.2.4", - "@img/sharp-linux-arm": "0.34.5", - "@img/sharp-linux-arm64": "0.34.5", - "@img/sharp-linux-ppc64": "0.34.5", - "@img/sharp-linux-riscv64": "0.34.5", - "@img/sharp-linux-s390x": "0.34.5", - "@img/sharp-linux-x64": "0.34.5", - "@img/sharp-linuxmusl-arm64": "0.34.5", - "@img/sharp-linuxmusl-x64": "0.34.5", - "@img/sharp-wasm32": "0.34.5", - "@img/sharp-win32-arm64": "0.34.5", - "@img/sharp-win32-ia32": "0.34.5", - "@img/sharp-win32-x64": "0.34.5" + "@img/sharp-darwin-arm64": "0.35.5", + "@img/sharp-darwin-x64": "0.35.5", + "@img/sharp-freebsd-wasm32": "0.35.5", + "@img/sharp-libvips-darwin-arm64": "1.3.4", + "@img/sharp-libvips-darwin-x64": "1.3.4", + "@img/sharp-libvips-linux-arm": "1.3.4", + "@img/sharp-libvips-linux-arm64": "1.3.4", + "@img/sharp-libvips-linux-ppc64": "1.3.4", + "@img/sharp-libvips-linux-riscv64": "1.3.4", + "@img/sharp-libvips-linux-s390x": "1.3.4", + "@img/sharp-libvips-linux-x64": "1.3.4", + "@img/sharp-libvips-linuxmusl-arm64": "1.3.4", + "@img/sharp-libvips-linuxmusl-x64": "1.3.4", + "@img/sharp-linux-arm": "0.35.5", + "@img/sharp-linux-arm64": "0.35.5", + "@img/sharp-linux-ppc64": "0.35.5", + "@img/sharp-linux-riscv64": "0.35.5", + "@img/sharp-linux-s390x": "0.35.5", + "@img/sharp-linux-x64": "0.35.5", + "@img/sharp-linuxmusl-arm64": "0.35.5", + "@img/sharp-linuxmusl-x64": "0.35.5", + "@img/sharp-webcontainers-wasm32": "0.35.5", + "@img/sharp-win32-arm64": "0.35.5", + "@img/sharp-win32-ia32": "0.35.5", + "@img/sharp-win32-x64": "0.35.5" + }, + "peerDependenciesMeta": { + "@types/node": { + "optional": true + } } }, "node_modules/shiki": { - "version": "4.0.2", - "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.0.2.tgz", - "integrity": "sha512-eAVKTMedR5ckPo4xne/PjYQYrU3qx78gtJZ+sHlXEg5IHhhoQhMfZVzetTYuaJS0L2Ef3AcCRzCHV8T0WI6nIQ==", + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/shiki/-/shiki-4.5.0.tgz", + "integrity": "sha512-OcRW72ngQvc+MSVWBRIovC/BTW589iUE/Vxav7VeM4RcY9MUjqB+IR6ILG1k9nuW6LiG/nct9uma4/MBtnZbmQ==", "license": "MIT", "dependencies": { - "@shikijs/core": "4.0.2", - "@shikijs/engine-javascript": "4.0.2", - "@shikijs/engine-oniguruma": "4.0.2", - "@shikijs/langs": "4.0.2", - "@shikijs/themes": "4.0.2", - "@shikijs/types": "4.0.2", + "@shikijs/core": "4.5.0", + "@shikijs/engine-javascript": "4.5.0", + "@shikijs/engine-oniguruma": "4.5.0", + "@shikijs/langs": "4.5.0", + "@shikijs/themes": "4.5.0", + "@shikijs/types": "4.5.0", "@shikijs/vscode-textmate": "^10.0.2", - "@types/hast": "^3.0.4" + "@types/hast": "^3.0.5" }, "engines": { "node": ">=20" @@ -5645,9 +5912,9 @@ } }, "node_modules/smol-toml": { - "version": "1.6.1", - "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.6.1.tgz", - "integrity": "sha512-dWUG8F5sIIARXih1DTaQAX4SsiTXhInKf1buxdY9DIg4ZYPZK5nGM1VRIYmEbDbsHt7USo99xSLFu5Q1IqTmsg==", + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/smol-toml/-/smol-toml-1.9.1.tgz", + "integrity": "sha512-j7WyPdf0CJoutMzMoPLF/bVXHtKZIT9Rb4eGeEbWw8X2t5oirjPgpL+OpaENZrILYaXqYwnVH+qgUwVbl+pUJQ==", "license": "BSD-3-Clause", "engines": { "node": ">= 18" @@ -5666,9 +5933,9 @@ } }, "node_modules/source-map-js": { - "version": "1.2.1", - "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", - "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.2.tgz", + "integrity": "sha512-KGj/8Y43x35aZVDtt+J4mK1hoLGHULMYfSkODJNQjNDC3oW1PqPoxMwo0pLUsWM/UEGzON/NxeHywEfNXNP3Vw==", "license": "BSD-3-Clause", "engines": { "node": ">=0.10.0" @@ -5684,11 +5951,53 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/stream-replace-string": { - "version": "2.0.0", - "resolved": "https://registry.npmjs.org/stream-replace-string/-/stream-replace-string-2.0.0.tgz", - "integrity": "sha512-TlnjJ1C0QrmxRNrON00JvaFFlNh5TTG00APw23j74ET7gkQpTASi6/L2fuiav8pzK715HXtUeClpBTw2NPSn6w==", - "license": "MIT" + "node_modules/starlight-links-validator": { + "version": "0.27.0", + "resolved": "https://registry.npmjs.org/starlight-links-validator/-/starlight-links-validator-0.27.0.tgz", + "integrity": "sha512-UvPl8L9hfWR+yWbLOEtljLmpJCGcM6AVseVy5IO9dmqdOjW4R+Y8FaYyVA5axCC8RvxM5xU7M1WTerpwU8rYbw==", + "license": "MIT", + "dependencies": { + "@astrojs/markdown-satteri": "^0.4.0", + "github-slugger": "^2.0.0", + "hast-util-from-html": "^2.0.3", + "is-absolute-url": "^5.0.0", + "mdast-util-mdx-jsx": "^3.2.0", + "mdast-util-to-hast": "^13.2.1", + "picomatch": "^4.0.3", + "satteri": "^0.10.3", + "terminal-link": "^5.0.0", + "unist-util-visit": "^5.1.0", + "yaml": "^2.8.3" + }, + "peerDependencies": { + "@astrojs/starlight": ">=0.42.0", + "astro": ">=7.2.10" + } + }, + "node_modules/starlight-pydocs": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/starlight-pydocs/-/starlight-pydocs-0.3.0.tgz", + "integrity": "sha512-2RVc8fhDvc9ECBt91W3G1OLXUqj0wNF4FCdk7qGj0v0zP6ngl+0jeH9C0cNbFDG/x1/E6r+VZyrAYciCDB8pcA==", + "license": "MIT", + "dependencies": { + "shiki": "^4.0.2" + }, + "engines": { + "node": ">=22.12.0" + }, + "peerDependencies": { + "@astrojs/markdown-remark": "^7.2.0", + "@astrojs/starlight": ">=0.41.0", + "astro": ">=7.0.0" + }, + "peerDependenciesMeta": { + "@astrojs/markdown-remark": { + "optional": true + }, + "@astrojs/starlight": { + "optional": true + } + } }, "node_modules/stringify-entities": { "version": "4.0.4", @@ -5722,19 +6031,47 @@ "inline-style-parser": "0.2.7" } }, + "node_modules/supports-color": { + "version": "10.2.2", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-10.2.2.tgz", + "integrity": "sha512-SS+jx45GF1QjgEXQx4NJZV9ImqmO2NPz5FNsIHrsDjh2YsHnawpan7SNQ1o8NuhrbHZy9AZhIoCUiCeaW/C80g==", + "license": "MIT", + "engines": { + "node": ">=18" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/supports-hyperlinks": { + "version": "4.6.0", + "resolved": "https://registry.npmjs.org/supports-hyperlinks/-/supports-hyperlinks-4.6.0.tgz", + "integrity": "sha512-FL/q/YePS4tFFTNiBOR+zPk2ZrKheqNtmTJ/9OUZ4e1wKHJofDbGQSmSsxS0XNnabSkqexryCaXGvEKz1vf9UQ==", + "license": "MIT", + "dependencies": { + "has-flag": "^5.0.1", + "supports-color": "^10.2.2" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/chalk/supports-hyperlinks?sponsor=1" + } + }, "node_modules/svgo": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.0.1.tgz", - "integrity": "sha512-XDpWUOPC6FEibaLzjfe0ucaV0YrOjYotGJO1WpF0Zd+n6ZGEQUsSugaoLq9QkEZtAfQIxT42UChcssDVPP3+/w==", + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-4.1.0.tgz", + "integrity": "sha512-bkxnTg1kSU0guhIBmibA6UUhrQmPVA1XsQLN+ylCd+UWzbnLkySOcXpyk1mrl05f+pcaCx2eHb+sp6BgMZWX+Q==", "license": "MIT", "dependencies": { "commander": "^11.1.0", - "css-select": "^5.1.0", + "css-select": "^6.0.0", "css-tree": "^3.0.1", - "css-what": "^6.1.0", + "css-what": "^7.0.0", "csso": "^5.0.5", "picocolors": "^1.1.1", - "sax": "^1.5.0" + "sax": "1.6.1" }, "bin": { "svgo": "bin/svgo.js" @@ -5747,6 +6084,31 @@ "url": "https://opencollective.com/svgo" } }, + "node_modules/svgo/node_modules/commander": { + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-11.1.0.tgz", + "integrity": "sha512-yPVavfyCcRhmorC7rWlkHn15b4wDVgVmBA7kV4QVBsF7kv/9TKJAbAXVTxvTnwP8HHKjRCJDClKbciiYS7p0DQ==", + "license": "MIT", + "engines": { + "node": ">=16" + } + }, + "node_modules/terminal-link": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/terminal-link/-/terminal-link-5.0.0.tgz", + "integrity": "sha512-qFAy10MTMwjzjU8U16YS4YoZD+NQLHzLssFMNqgravjbvIPNiqkGFR4yjhJfmY9R5OFU7+yHxc6y+uGHkKwLRA==", + "license": "MIT", + "dependencies": { + "ansi-escapes": "^7.0.0", + "supports-hyperlinks": "^4.1.0" + }, + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/tiny-inflate": { "version": "1.0.3", "resolved": "https://registry.npmjs.org/tiny-inflate/-/tiny-inflate-1.0.3.tgz", @@ -5754,31 +6116,31 @@ "license": "MIT" }, "node_modules/tinyclip": { - "version": "0.1.12", - "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-0.1.12.tgz", - "integrity": "sha512-Ae3OVUqifDw0wBriIBS7yVaW44Dp6eSHQcyq4Igc7eN2TJH/2YsicswaW+J/OuMvhpDPOKEgpAZCjkb4hpoyeA==", + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/tinyclip/-/tinyclip-1.0.3.tgz", + "integrity": "sha512-nTv2raKFMzGG6JJp0lpNDsRPdfFE6UHlu2rVEjXo8vkx1ZS3LW2CVXqIaLu8xZlu+6tbFs9V58Qc/Jmk/Ii7wA==", "license": "MIT", "engines": { "node": "^16.14.0 || >= 17.3.0" } }, "node_modules/tinyexec": { - "version": "1.0.4", - "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.0.4.tgz", - "integrity": "sha512-u9r3uZC0bdpGOXtlxUIdwf9pkmvhqJdrVCH9fapQtgy/OeTTMZ1nqH7agtvEfmGui6e1XxjcdrlxvxJvc3sMqw==", + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/tinyexec/-/tinyexec-1.3.1.tgz", + "integrity": "sha512-GCvB3aoys96IuDFBMcTB46JOR6mdMtAToqwiW8JlWhsoh1mhHi/xn9ss/Dg7N555GiJyEt2qzoG/NHCwM6h1EA==", "license": "MIT", "engines": { "node": ">=18" } }, "node_modules/tinyglobby": { - "version": "0.2.15", - "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.15.tgz", - "integrity": "sha512-j2Zq4NyQYG5XMST4cbs02Ak8iJUdxRM0XI5QyxXuZOzKOINmWurp3smXu3y5wDcJrptwpSjgXHzIQxR0omXljQ==", + "version": "0.2.17", + "resolved": "https://registry.npmjs.org/tinyglobby/-/tinyglobby-0.2.17.tgz", + "integrity": "sha512-wXR/dYpcqKmfWpEdZjiKJOwCNFndD0DMnrW/cYjVGttEkBfVgcLFHoNrlj47mjOVic9yyNu65alsgF4NQyTa2g==", "license": "MIT", "dependencies": { "fdir": "^6.5.0", - "picomatch": "^4.0.3" + "picomatch": "^4.0.4" }, "engines": { "node": ">=12.0.0" @@ -5807,26 +6169,6 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/tsconfck": { - "version": "3.1.6", - "resolved": "https://registry.npmjs.org/tsconfck/-/tsconfck-3.1.6.tgz", - "integrity": "sha512-ks6Vjr/jEw0P1gmOVwutM3B7fWxoWBL2KRDb1JfqGVawBmO5UsvmWOQFGHBPl5yxYz4eERr19E6L7NMv+Fej4w==", - "license": "MIT", - "bin": { - "tsconfck": "bin/tsconfck.js" - }, - "engines": { - "node": "^18 || >=20" - }, - "peerDependencies": { - "typescript": "^5.0.0" - }, - "peerDependenciesMeta": { - "typescript": { - "optional": true - } - } - }, "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -5835,15 +6177,15 @@ "optional": true }, "node_modules/ufo": { - "version": "1.6.3", - "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.3.tgz", - "integrity": "sha512-yDJTmhydvl5lJzBmy/hyOAA0d+aqCBuwl818haVdYCRrWV84o7YyeVm4QlVHStqNrrJSTb6jKuFAVqAFsr+K3Q==", + "version": "1.6.4", + "resolved": "https://registry.npmjs.org/ufo/-/ufo-1.6.4.tgz", + "integrity": "sha512-JFNbkD1Svwe0KvGi8GOeLcP4kAWQ609twvCdcHxq1oSL8svv39ZuSvajcD8B+5D0eL4+s1Is2D/O6KN3qcTeRA==", "license": "MIT" }, "node_modules/ultrahtml": { - "version": "1.6.0", - "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.6.0.tgz", - "integrity": "sha512-R9fBn90VTJrqqLDwyMph+HGne8eqY1iPfYhPzZrvKpIfwkWZbcYlfpsb8B9dTvBfpy1/hqAD7Wi8EKfP9e8zdw==", + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/ultrahtml/-/ultrahtml-1.7.0.tgz", + "integrity": "sha512-2xRd0VHoAQE4M+vF/DvFFB7pUV0ZxTW1TLi7lHQWnF/Sb5TPeEUV/l+hxcNnGO00ZXGnR0voCMmYRKQf+rvJ2g==", "license": "MIT" }, "node_modules/uncrypto": { @@ -5852,10 +6194,19 @@ "integrity": "sha512-Ql87qFHB3s/De2ClA9e0gsnS6zXG27SkTiSJwjCc9MebbfapQfuPzumMIUMi38ezPZVNFcHI9sUIepeQfw8J8Q==", "license": "MIT" }, + "node_modules/undici": { + "version": "8.11.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-8.11.2.tgz", + "integrity": "sha512-u4UB2/IrKdU6lFxumHmmo1a3fCQO5tzQllRorfoRS63txhrB7xTpSn1PftwC4qEHkOaqP95fCWW4lJzwErwzhQ==", + "license": "MIT", + "engines": { + "node": ">=22.19.0" + } + }, "node_modules/undici-types": { - "version": "7.16.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.16.0.tgz", - "integrity": "sha512-Zz+aZWSj8LE6zoxD+xrjh4VfkIG8Ya6LvYkZqtUQGJPZjYl53ypCaUwWqo7eI0x66KBGeRo+mlBEkMSeSZ38Nw==", + "version": "7.24.6", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.24.6.tgz", + "integrity": "sha512-WRNW+sJgj5OBN4/0JpHFqtqzhpbnV0GuB+OozA9gCL7a993SmU+1JBZCzLNxYsbMfIeDL+lTsphD5jN5N+n0zg==", "license": "MIT" }, "node_modules/unified": { @@ -5878,14 +6229,14 @@ } }, "node_modules/unifont": { - "version": "0.7.4", - "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.4.tgz", - "integrity": "sha512-oHeis4/xl42HUIeHuNZRGEvxj5AaIKR+bHPNegRq5LV1gdc3jundpONbjglKpihmJf+dswygdMJn3eftGIMemg==", + "version": "0.7.5", + "resolved": "https://registry.npmjs.org/unifont/-/unifont-0.7.5.tgz", + "integrity": "sha512-ULe/Cs+ZIsq+dcFofNkhqielCrUJnb5mr+Yc4EBM2VlL+6OZR6+cjtI2mT1bJvRBrVncqHAbLURxmPLcCXzWMg==", "license": "MIT", "dependencies": { "css-tree": "^3.1.0", - "ofetch": "^1.5.1", - "ohash": "^2.0.11" + "ohash": "^2.0.11", + "undici": "^8.0.0" } }, "node_modules/unist-util-find-after": { @@ -6120,12 +6471,36 @@ } } }, + "node_modules/url-extras": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/url-extras/-/url-extras-0.1.0.tgz", + "integrity": "sha512-8tzwTeXFPuX/5PHuCDQE5Dd9Ts4rwoq2t9aIT+HS4iAVpmj5l4Ao7Q+BuuFjvWRqrLswBhQDk8O96ZicgCqQqw==", + "license": "MIT", + "engines": { + "node": ">=20" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "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==", "license": "MIT" }, + "node_modules/verkit": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/verkit/-/verkit-0.5.0.tgz", + "integrity": "sha512-u4J1hr6Y+C/hit34GwL3eIcUf3wngT8TlYUjK7tk0jxydKLAq3sYvifjdTDA6U3KCKRcQCQDGf/srjxfZF3l7g==", + "license": "MIT", + "engines": { + "node": ">=18.12.0" + }, + "funding": { + "url": "https://github.com/sponsors/sxzz" + } + }, "node_modules/vfile": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/vfile/-/vfile-6.0.3.tgz", @@ -6169,17 +6544,16 @@ } }, "node_modules/vite": { - "version": "7.3.1", - "resolved": "https://registry.npmjs.org/vite/-/vite-7.3.1.tgz", - "integrity": "sha512-w+N7Hifpc3gRjZ63vYBXA56dvvRlNWRczTdmCBBa+CotUzAPf5b7YMdMR/8CQoeYE5LX3W4wj6RYTgonm1b9DA==", + "version": "8.3.4", + "resolved": "https://registry.npmjs.org/vite/-/vite-8.3.4.tgz", + "integrity": "sha512-IftLE8phJhdNRIGNEV4JCCvfNScoZQVfcXaN8TQropWGJtOHcu+iAB9pHcuGvmEb/kddw0LBe/jS8ztdC6xdiQ==", "license": "MIT", "dependencies": { - "esbuild": "^0.27.0", - "fdir": "^6.5.0", - "picomatch": "^4.0.3", - "postcss": "^8.5.6", - "rollup": "^4.43.0", - "tinyglobby": "^0.2.15" + "lightningcss": "^1.33.0", + "picomatch": "^4.0.7", + "postcss": "^8.5.29", + "rolldown": "~1.2.12", + "tinyglobby": "^0.2.17" }, "bin": { "vite": "bin/vite.js" @@ -6195,9 +6569,10 @@ }, "peerDependencies": { "@types/node": "^20.19.0 || >=22.12.0", + "@vitejs/devtools": "^0.7.1", + "esbuild": "^0.27.0 || ^0.28.0", "jiti": ">=1.21.0", "less": "^4.0.0", - "lightningcss": "^1.21.0", "sass": "^1.70.0", "sass-embedded": "^1.70.0", "stylus": ">=0.54.8", @@ -6210,13 +6585,16 @@ "@types/node": { "optional": true }, - "jiti": { + "@vitejs/devtools": { "optional": true }, - "less": { + "esbuild": { + "optional": true + }, + "jiti": { "optional": true }, - "lightningcss": { + "less": { "optional": true }, "sass": { @@ -6243,9 +6621,9 @@ } }, "node_modules/vitefu": { - "version": "1.1.2", - "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.2.tgz", - "integrity": "sha512-zpKATdUbzbsycPFBN71nS2uzBUQiVnFoOrr2rvqv34S1lcAgMKKkjWleLGeiJlZ8lwCXvtWaRn7R3ZC16SYRuw==", + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/vitefu/-/vitefu-1.1.3.tgz", + "integrity": "sha512-ub4okH7Z5KLjb6hDyjqrGXqWtWvoYdU3IGm/NorpgHncKoLTCfRIbvlhBm7r0YstIaQRYlp4yEbFqDcKSzXSSg==", "license": "MIT", "workspaces": [ "tests/deps/*", @@ -6253,7 +6631,7 @@ "tests/projects/workspace/packages/*" ], "peerDependencies": { - "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0-beta.0" + "vite": "^3.0.0 || ^4.0.0 || ^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0" }, "peerDependenciesMeta": { "vite": { @@ -6271,21 +6649,27 @@ "url": "https://github.com/sponsors/wooorm" } }, - "node_modules/which-pm-runs": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/which-pm-runs/-/which-pm-runs-1.1.0.tgz", - "integrity": "sha512-n1brCuqClxfFfq/Rb0ICg9giSZqCS+pLtccdag6C2HyufBrh3fBOiy9nb6ggRMvWOVH5GrdJskj5iGTZNxd7SA==", - "license": "MIT", - "engines": { - "node": ">=4" - } - }, "node_modules/xxhash-wasm": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/xxhash-wasm/-/xxhash-wasm-1.1.0.tgz", "integrity": "sha512-147y/6YNh+tlp6nd/2pWq38i9h6mz/EuQ6njIrmW8D1BS5nCqs0P6DG+m6zTGnNz5I+uhZ0SHxBs9BsPrwcKDA==", "license": "MIT" }, + "node_modules/yaml": { + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.9.1.tgz", + "integrity": "sha512-3NxN8+78OdzbT7C/WjGsyfPAtJaN3FNDsWxv7Y7mcDsT/oOmgW8BpyQQFFBnvZE3j9Y2Sdz1ULFLezL7Eb2yFw==", + "license": "ISC", + "bin": { + "yaml": "bin.mjs" + }, + "engines": { + "node": ">= 14.6" + }, + "funding": { + "url": "https://github.com/sponsors/eemeli" + } + }, "node_modules/yargs-parser": { "version": "22.0.0", "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-22.0.0.tgz", @@ -6308,9 +6692,9 @@ } }, "node_modules/zod": { - "version": "4.3.6", - "resolved": "https://registry.npmjs.org/zod/-/zod-4.3.6.tgz", - "integrity": "sha512-rftlrkhHZOcjDwkGlnUtZZkvaPHCsDATp4pGpuOOMDaTdDDXF91wuVDJoWoPsKX/3YPQ5fHuF3STjcYyKr+Qhg==", + "version": "4.6.7", + "resolved": "https://registry.npmjs.org/zod/-/zod-4.6.7.tgz", + "integrity": "sha512-g8W2D2kU6a8untc053fX1uVBKzM7yqAxW/3zwk83+6RyF2+SKOuV+c0wBDhUwzV1PaGapmZH+rmcUzPf+3CGwg==", "license": "MIT", "funding": { "url": "https://github.com/sponsors/colinhacks" diff --git a/astro_docs/package.json b/astro_docs/package.json index 314b16f..3df88b7 100644 --- a/astro_docs/package.json +++ b/astro_docs/package.json @@ -1,23 +1,28 @@ { - "name": "peaceful-plasma", + "name": "tikzfigure-docs", + "private": true, "type": "module", "version": "0.0.1", + "engines": { + "node": ">=22.12.0" + }, "scripts": { "docs:api": "python scripts/generate_api_docs.py", - "predev": "python scripts/generate_api_docs.py", + "predev": "python scripts/generate_api_docs.py && starlight-notebooks convert", "dev": "astro dev", - "prestart": "python scripts/generate_api_docs.py", + "prestart": "python scripts/generate_api_docs.py && starlight-notebooks convert", "start": "astro dev", "prebuild": "python scripts/generate_api_docs.py", "build": "astro build", "preview": "astro preview", + "check:mdx": "starlight-check-mdx", + "notebooks": "starlight-notebooks", "astro": "astro" }, "dependencies": { - "@astrojs/starlight": "^0.38.2", - "astro": "^6.0.1", - "rehype-katex": "^7.0.1", - "remark-math": "^6.0.0", - "sharp": "^0.34.2" + "@astrojs/starlight": "^0.42.6", + "@max-models/starlight-theme": "github:max-models/starlight-theme#347851725c6014b379c3d90ffea1d6a51b7d937e", + "astro": "^7.3.8", + "sharp": "^0.35.3" } } diff --git a/astro_docs/scripts/build_tutorials.py b/astro_docs/scripts/build_tutorials.py deleted file mode 100644 index 6c04995..0000000 --- a/astro_docs/scripts/build_tutorials.py +++ /dev/null @@ -1,372 +0,0 @@ -#!/usr/bin/env python3 -"""Convert .qmd tutorials and .ipynb notebooks to Starlight-compatible markdown. - -Run from anywhere: - python astro_docs/scripts/build_tutorials.py -""" - -import argparse -import os -import re -import shutil -import subprocess -import tempfile -from multiprocessing import Pool -from pathlib import Path - -ASTRO_ROOT = Path(__file__).resolve().parent.parent -TUTORIALS_SRC = ASTRO_ROOT.parent / "tutorials" -CONTENT_DST = ASTRO_ROOT / "src" / "content" / "docs" / "tutorials" -PUBLIC_DST = ASTRO_ROOT / "public" / "tutorials" -BASE_PATH = "/tikzfigure" -# This is the tab for python code, do not use TAB from tikzfigure.core.constants -# since that is used for the tikz code and could change in the future. -TAB = " " - - -def iter_tutorial_sources() -> list[Path]: - """Return all tutorial sources in a stable order for sharding/building.""" - qmds = sorted( - path - for path in TUTORIALS_SRC.rglob("*.qmd") - if "includes" not in path.relative_to(TUTORIALS_SRC).parts - ) - nbs = sorted( - nb - for nb in TUTORIALS_SRC.rglob("*.ipynb") - if ".ipynb_checkpoints" not in nb.parts - ) - return sorted([*qmds, *nbs], key=lambda path: path.name) - - -def select_tutorial_shard( - tutorials: list[Path], shard_count: int | None, shard_index: int | None -) -> list[Path]: - """Return the subset of tutorials assigned to a shard.""" - if shard_count is None and shard_index is None: - return tutorials - if shard_count is None or shard_index is None: - raise ValueError("shard_count and shard_index must be provided together.") - if shard_count < 1: - raise ValueError("shard_count must be at least 1.") - if not 0 <= shard_index < shard_count: - raise ValueError( - f"shard_index must be between 0 and {shard_count - 1}, got {shard_index}." - ) - return [ - tutorial - for position, tutorial in enumerate(tutorials) - if position % shard_count == shard_index - ] - - -def extract_title(content: str, fallback: str) -> str: - """Extract title from YAML frontmatter or first # heading.""" - fm_match = re.match(r"^---\n(.*?)\n---", content, re.DOTALL) - if fm_match: - title_match = re.search( - r'^title:\s*["\']?(.*?)["\']?\s*$', fm_match.group(1), re.MULTILINE - ) - if title_match: - return title_match.group(1).strip() - heading_match = re.search(r"^# (.+)$", content, re.MULTILINE) - if heading_match: - return heading_match.group(1).strip() - return fallback - - -def strip_frontmatter(content: str) -> str: - """Remove YAML frontmatter block and the leading # heading (Starlight renders the title from frontmatter).""" - content = re.sub(r"^---\n.*?\n---\n?", "", content, flags=re.DOTALL).lstrip() - content = re.sub(r"^#[^\n]+\n+", "", content) - return content - - -def fix_image_paths(content: str, tutorial_name: str) -> str: - """Rewrite relative image paths to absolute /tutorials//... paths. - - Quarto emits paths like ``_files/figure-commonmark/foo.png``, but - copy_assets() copies the *contents* of ``_files/`` directly into - ``public/tutorials//``, so the ``_files/`` prefix must be - stripped before building the final URL. - - Handles both markdown ``![alt](path)`` syntax and HTML ```` - tags, since Quarto emits the latter for figures with width attributes. - """ - - files_prefix = f"{tutorial_name}_files/" - - def rewrite(path: str) -> str: - if path.startswith(("http://", "https://", "/")): - return path - path = path.removeprefix(files_prefix) - return f"{BASE_PATH}/tutorials/{tutorial_name}/{path}" - # return f"./{path}" - - def replace_md(m: re.Match[str]) -> str: - alt, path = m.group(1), m.group(2) - return f"![{alt}]({rewrite(path)})" - - def replace_img(m: re.Match[str]) -> str: - return m.group(0).replace(m.group(1), rewrite(m.group(1))) - - content = re.sub(r"!\[([^\]]*)\]\(([^)]+)\)", replace_md, content) - content = re.sub(r']*\bsrc="([^"]+)"', replace_img, content) - return content - - -def fix_tikzcode_callouts(content: str) -> str: - """Convert [[CALLOUT-*]] and [[TAB]] markers to JSX Astro components. - - Transforms: - [[CALLOUT-NOTE START]] ... [[CALLOUT-NOTE END]] - → ... - - ## heading markers within callouts become TabItem components - Multiple ## sections create a wrapper - """ - # Wrap tab groups in a collapsible section so code panes are hidden by default. - content = content.replace( - "`TABS_START`", "
\nShow Tikz code\n\n" - ) - content = content.replace("`TABS_END`", "\n
") - - # Replace `TAB_START` and `TAB_END` with and - # Get the label from the `TAB_START` marker, e.g. `TAB_START label="Section"` - tab_start_pattern = r"`TAB_START\s+label=\"([^\"]+)\"`" - content = re.sub(tab_start_pattern, r'', content) - content = content.replace("`TAB_END`", "") - - return content - - -def fence_indented_output_blocks(content: str) -> str: - """Convert top-level 4-space-indented blocks into fenced text code blocks.""" - lines = content.splitlines() - out: list[str] = [] - i = 0 - in_fence = False - - while i < len(lines): - line = lines[i] - - # Track whether we're inside a fenced code block - if line.startswith("```"): - in_fence = not in_fence - out.append(line) - i += 1 - continue - - prev_blank = i == 0 or lines[i - 1].strip() == "" - - # Only process indented blocks if we're NOT in a fence - if line.startswith(TAB) and prev_blank and not in_fence: - block: list[str] = [] - j = i - while j < len(lines): - current = lines[j] - if current.startswith(TAB): - block.append(current[len(TAB) :]) - j += 1 - continue - # Keep blank lines inside the block if followed by another - # indented line, so one output becomes one fenced block. - if ( - current.strip() == "" - and j + 1 < len(lines) - and lines[j + 1].startswith(TAB) - ): - block.append("") - j += 1 - continue - break - - out.append("```tex") - out.extend(block) - out.append("```") - i = j - continue - - out.append(line) - i += 1 - - rewritten = "\n".join(out) - if content.endswith("\n"): - rewritten += "\n" - return rewritten - - -def write_content(title: str, body: str, out_path: Path) -> None: - """Write MDX content with component imports if needed.""" - imports = "" - if " None: - if not src_files_dir.exists(): - return - dst = PUBLIC_DST / tutorial_name - if dst.exists(): - shutil.rmtree(dst) - shutil.copytree(src_files_dir, dst) - - -def process_qmd(qmd_path: Path) -> None: - name = qmd_path.stem - print(f"Rendering {qmd_path.relative_to(TUTORIALS_SRC)} ...") - with tempfile.TemporaryDirectory() as tmp: - tmp_dir = Path(tmp) - # Copy the .qmd into its own isolated directory so concurrent Quarto - # instances don't collide on project-level temp files in the source dir. - work_dir = tmp_dir / "src" - work_dir.mkdir() - work_qmd = work_dir / qmd_path.name - shutil.copy2(qmd_path, work_qmd) - # Copy includes directory if it exists - includes_src = TUTORIALS_SRC / "includes" - includes_dst = work_dir / "includes" - if includes_src.exists() and includes_src.is_dir(): - shutil.copytree(includes_src, includes_dst, dirs_exist_ok=True) - out_dir = tmp_dir / "out" - out_dir.mkdir() - env = {**os.environ, "TMPDIR": tmp} - subprocess.run( - [ - "quarto", - "render", - str(work_qmd), - "--to", - "gfm", - "--output-dir", - str(out_dir), - ], - check=True, - env=env, - ) - md_file = out_dir / f"{name}.md" - if not md_file.exists(): - print(f" Warning: expected {md_file.name}, skipping") - return - - content = md_file.read_text(encoding="utf-8") - title = extract_title(content, name) - body = fix_image_paths(strip_frontmatter(content), name) - body = fix_tikzcode_callouts(body) - body = fence_indented_output_blocks(body) - copy_assets(out_dir / f"{name}_files", name) - - write_content(title, body, CONTENT_DST / f"{name}.mdx") - - -def process_notebook(nb_path: Path) -> None: - name = nb_path.stem - print(f"Converting {nb_path.relative_to(TUTORIALS_SRC)} ...") - with tempfile.TemporaryDirectory() as tmp: - tmp_dir = Path(tmp) - subprocess.run( - [ - "jupyter", - "nbconvert", - "--to", - "markdown", - "--execute", - str(nb_path), - "--output-dir", - str(tmp_dir), - ], - check=True, - ) - md_file = tmp_dir / f"{name}.md" - if not md_file.exists(): - print(f" Warning: expected {md_file.name}, skipping") - return - - content = md_file.read_text(encoding="utf-8") - title = extract_title(content, name) - body = fix_image_paths(strip_frontmatter(content), name) - body = fence_indented_output_blocks(body) - copy_assets(tmp_dir / f"{name}_files", name) - - write_content(title, body, CONTENT_DST / f"{name}.mdx") - - -def main() -> None: - parser = argparse.ArgumentParser(description=__doc__) - parser.add_argument( - "--serial", action="store_true", help="Process tutorials one at a time" - ) - parser.add_argument( - "--name", - type=str, - help="Process a specific tutorial by name (without extension)", - ) - parser.add_argument( - "--directory", - type=str, - help="Process tutorials from one top-level tutorials subdirectory.", - ) - parser.add_argument( - "--shard-count", - type=int, - help="Split tutorials into this many deterministic shards.", - ) - parser.add_argument( - "--shard-index", - type=int, - help="Process only one zero-based shard from --shard-count.", - ) - args = parser.parse_args() - - CONTENT_DST.mkdir(parents=True, exist_ok=True) - PUBLIC_DST.mkdir(parents=True, exist_ok=True) - - tutorials = iter_tutorial_sources() - - if args.name: - tutorials = [tutorial for tutorial in tutorials if args.name in tutorial.stem] - if args.directory: - tutorials = [ - tutorial - for tutorial in tutorials - if tutorial.relative_to(TUTORIALS_SRC).parts[0] == args.directory - ] - - tutorials = select_tutorial_shard(tutorials, args.shard_count, args.shard_index) - - qmds = [tutorial for tutorial in tutorials if tutorial.suffix == ".qmd"] - nbs = [tutorial for tutorial in tutorials if tutorial.suffix == ".ipynb"] - - if args.shard_count is not None and args.shard_index is not None: - print( - f"Processing shard {args.shard_index + 1}/{args.shard_count} " - f"({len(tutorials)} tutorial(s))." - ) - elif args.directory: - print( - f"Processing {len(tutorials)} tutorial(s) in directory {args.directory!r}." - ) - elif args.name: - print(f"Processing {len(tutorials)} tutorial(s) matching {args.name!r}.") - else: - print(f"Processing {len(tutorials)} tutorial(s).") - - if args.serial: - for qmd in qmds: - process_qmd(qmd) - for nb in nbs: - process_notebook(nb) - else: - with Pool() as pool: - pool.map(process_qmd, qmds) - pool.map(process_notebook, nbs) - - print("Done.") - - -if __name__ == "__main__": - main() diff --git a/astro_docs/scripts/generate_api_docs.py b/astro_docs/scripts/generate_api_docs.py index 5580865..fee3823 100644 --- a/astro_docs/scripts/generate_api_docs.py +++ b/astro_docs/scripts/generate_api_docs.py @@ -124,6 +124,7 @@ def build_api_page() -> str: --- title: API Reference description: Generated reference for the current tikzfigure Python API. + editUrl: false --- > This page is generated from the current Python docstrings by diff --git a/astro_docs/src/components/Header.astro b/astro_docs/src/components/Header.astro deleted file mode 100644 index 4b21a98..0000000 --- a/astro_docs/src/components/Header.astro +++ /dev/null @@ -1,112 +0,0 @@ ---- -// Custom Header — extends Starlight's default with a secondary tab nav. -import config from 'virtual:starlight/user-config'; -import LanguageSelect from 'virtual:starlight/components/LanguageSelect'; -import Search from 'virtual:starlight/components/Search'; -import SiteTitle from 'virtual:starlight/components/SiteTitle'; -import SocialIcons from 'virtual:starlight/components/SocialIcons'; -import ThemeSelect from 'virtual:starlight/components/ThemeSelect'; - -const shouldRenderSearch = - config.pagefind || - config.components.Search !== '@astrojs/starlight/components/Search.astro'; - -const tabs = [ - { label: 'Home', href: '/tikzfigure/' }, - { label: 'Installation', href: '/tikzfigure/installation/' }, - { label: 'Tutorials', href: '/tikzfigure/tutorials/tutorial_01_getting_started/' }, - { label: 'API Reference',href: '/tikzfigure/api/' }, -]; - -const path = Astro.url.pathname; - -function isActive(href: string) { - if (href === '/tikzfigure/' || href === '/tikzfigure') { - return path === '/tikzfigure/' || path === '/tikzfigure'; - } - return path.startsWith(href.replace(/\/$/, '')); -} ---- - - -
- -
-
- {shouldRenderSearch && } -
-
- - - -
- - - - - diff --git a/astro_docs/src/content.config.ts b/astro_docs/src/content.config.ts index 7fbcf2c..08161a2 100644 --- a/astro_docs/src/content.config.ts +++ b/astro_docs/src/content.config.ts @@ -1,7 +1 @@ -import { defineCollection } from "astro:content"; -import { docsLoader } from "@astrojs/starlight/loaders"; -import { docsSchema } from "@astrojs/starlight/schema"; - -export const collections = { - docs: defineCollection({ loader: docsLoader(), schema: docsSchema() }), -}; +export { collections } from '@max-models/starlight-theme/content'; diff --git a/astro_docs/src/content/docs/api.md b/astro_docs/src/content/docs/api.md index ecb2d23..19f869e 100644 --- a/astro_docs/src/content/docs/api.md +++ b/astro_docs/src/content/docs/api.md @@ -1,6 +1,7 @@ --- title: API Reference description: Generated reference for the current tikzfigure Python API. +editUrl: false --- > This page is generated from the current Python docstrings by @@ -8,7 +9,8 @@ description: Generated reference for the current tikzfigure Python API. > `src/tikzfigure/`, not this file. The sections below follow the current public API surface exported from -`tikzfigure`, plus the underlying modules that define the main public classes. +`tikzfigure`, plus the underlying modules that define the main public +classes. ## Figure API @@ -27,31 +29,38 @@ class TikzFigure(FigureParsingMixin, FigurePathMixin, FigureLayoutMixin, Main entry point for building TikZ figures programmatically. -Holds layers, variables, colors, and all TikZ primitives. Call `generate_tikz` -to obtain the LaTeX source, `compile_pdf` to compile it, and `savefig` / `show` -to export or display the result. - -Compilation: By default, `compile_pdf`, `savefig`, and `show` use pdflatex if -available on your system. If pdflatex is unavailable, tikzfigure automatically -falls back to the latex-on-http web API for compilation. You can explicitly -request web-based compilation by passing `use_web_compilation=True` to these -methods. This allows figures to be compiled and rendered without requiring a -local LaTeX installation. You can also force this behavior process-wide by -setting the `TIKZFIGURE_USE_WEB_COMPILATION=1` environment variable. See +Holds layers, variables, colors, and all TikZ primitives. Call +`generate_tikz` to obtain the LaTeX source, `compile_pdf` +to compile it, and `savefig` / `show` to export or display +the result. + +Compilation: `compile_pdf`, `savefig`, and `show` +compile with a local pdflatex. If pdflatex is missing or the document +fails to build, they raise a `RuntimeError` describing what went +wrong. + +Without a local LaTeX installation, tikzfigure can compile through the +latex-on-http web API instead. That uploads the LaTeX source of the figure +to a third-party server, so it is opt-in and never happens on its own: +pass ``use_web_compilation=True`` to those methods, or set +``TIKZFIGURE_USE_WEB_COMPILATION=1`` to enable it process-wide. Either way +a `UserWarning` is emitted when a figure is uploaded. See https://github.com/max-models/tikzfigure for more details. **Attributes**: - `layers` - The `LayerCollection` holding all drawing layers. - `variables` - List of `Variable` objects defined in this figure. -- `colors` - List of `(name, Color)` pairs defined with `colorlet`. -- `ndim` - Number of spatial dimensions (`2` or `3`). -- `document_setup` - Custom LaTeX preamble inserted before `\begin{document}`. +- `colors` - List of ``(name, Color)`` pairs defined with + `colorlet`. +- `ndim` - Number of spatial dimensions (``2`` or ``3``). +- `document_setup` - Custom LaTeX preamble inserted before + ``\begin{document}``. - `extra_packages` - Extra LaTeX packages to include in the preamble. - + -#### TikzFigure.\_\_init\_\_ +##### TikzFigure.\_\_init\_\_ ```python def __init__(ndim: int = 2, @@ -72,30 +81,76 @@ Initialize a TikzFigure. **Arguments**: -- `ndim` - Number of spatial dimensions. Use `2` for standard figures and `3` - for pgfplots 3-D axis figures. Defaults to `2`. -- `label` - LaTeX `\label` for the surrounding `figure` environment. When set, - the figure is wrapped in a `figure` environment. -- `grid` - If `True`, draw a background grid. Defaults to `False`. -- `tikz_code` - Existing TikZ code to parse and import. When provided, nodes and - paths are reconstructed from the source. Defaults to `None`. -- `extra_packages` - Additional LaTeX packages to include in the standalone - preamble (e.g. `["amsmath"]`). -- `document_setup` - Raw LaTeX to insert in the preamble after the standard - packages. +- `ndim` - Number of spatial dimensions. Use ``2`` for standard + figures and ``3`` for pgfplots 3-D axis figures. + Defaults to ``2``. +- `label` - LaTeX ``\label`` for the surrounding ``figure`` + environment. When set, the figure is wrapped in a + ``figure`` environment. +- `grid` - If ``True``, draw a background grid. Defaults to + ``False``. +- `tikz_code` - Existing TikZ code to parse and import. When + provided, nodes and paths are reconstructed from the + source. Defaults to ``None``. +- `extra_packages` - Additional LaTeX packages to include in the + standalone preamble (e.g. ``["amsmath"]``). +- `document_setup` - Raw LaTeX to insert in the preamble after + the standard packages. - `figure_setup` - TikZ options string placed inside the opening - `\begin{tikzpicture}[…]` bracket. -- `figsize` - `(width, height)` in centimetres used as a hint for display - backends. Defaults to `(10, 6)`. -- `description` - Long description stored for documentation purposes (not - emitted in TikZ output). -- `show_axes` - For 3-D figures, if `True` render the pgfplots axis lines and - labels. Defaults to `False`. + ``\begin{tikzpicture}[…]`` bracket. +- `figsize` - ``(width, height)`` in centimetres used as a hint + for display backends. Defaults to ``(10, 6)``. +- `description` - Long description stored for documentation + purposes (not emitted in TikZ output). +- `show_axes` - For 3-D figures, if ``True`` render the pgfplots + axis lines and labels. Defaults to ``False``. - `rows` - Number of rows in subfigure grid. Must be specified with cols. Defaults to None (no grid). - `cols` - Number of columns in subfigure grid. Must be specified with rows. Defaults to None (no grid). + + +##### TikzFigure.to\_python + +```python +def to_python(name: str = "fig", + header: bool = True, + verify: bool = True) -> str +``` + +Return Python code that rebuilds this figure. + +Useful together with `from_tikz_code` to turn existing TikZ +source into a tikzfigure script. + +**Examples**: + + >>> fig = TikzFigure() + >>> _ = fig.add_node(0, 0, label="a", content="A") + >>> print(fig.to_python(header=False), end="") + fig = TikzFigure() + fig.add_node(0, 0, label='a', content='A') + + +**Arguments**: + +- `name` - Variable name for the figure in the generated code. +- `header` - Include the ``import`` lines. +- `verify` - Run the generated code and check it produces the same + TikZ output as this figure. + + +**Returns**: + + Python source code. + + +**Raises**: + +- `CodegenError` - If the figure uses features that cannot be + expressed as Python calls yet (pgfplots axes or subfigures). + ##### TikzFigure.to\_dict @@ -108,8 +163,8 @@ Serialize this figure to a plain dictionary. **Returns**: -A dictionary capturing all figure settings, layers, variables, and colors, -suitable for round-tripping via `from_dict`. + A dictionary capturing all figure settings, layers, variables, + and colors, suitable for round-tripping via `from_dict`. @@ -126,9 +181,10 @@ Reconstruct a TikzFigure from a dictionary. - `d` - Dictionary as produced by `to_dict`. + **Returns**: -A fully restored `TikzFigure` instance. + A fully restored `TikzFigure` instance. @@ -165,9 +221,9 @@ def configure_spy_scope(*, Append figure-wide defaults for subsequent spy commands. -This updates `figure_setup` with a top-level spy scope configuration, which is -useful when several `fig.spy(...)` calls should share the same magnification, -lens, node styling, or connection-path settings. +This updates ``figure_setup`` with a top-level spy scope configuration, +which is useful when several ``fig.spy(...)`` calls should share the +same magnification, lens, node styling, or connection-path settings. @@ -194,11 +250,11 @@ def add_spy(on: Any, **kwargs: Any) -> Spy ``` -Add a top-level `\spy` command to the figure. +Add a top-level ``\spy`` command to the figure. -If the figure does not yet have any spy-scope configuration, this method enables -a bare `spy scope` automatically so the generated command works without extra -setup. +If the figure does not yet have any spy-scope configuration, this +method enables a bare ``spy scope`` automatically so the generated +command works without extra setup. @@ -227,9 +283,9 @@ def add_spy_scope(layer: int = 0, Create a nested scope with local spy defaults. -Use this when spy behavior should apply only to a region of the figure, or when -you want nested spy configurations such as an outer `outlines` scope with an -inner `overlays` scope. +Use this when spy behavior should apply only to a region of the figure, +or when you want nested spy configurations such as an outer +``outlines`` scope with an inner ``overlays`` scope. @@ -241,28 +297,28 @@ def add_copy(node: Node, verbose: bool = False, **overrides: Any) -> Node Copy a node into this figure, applying optional constructor-style overrides. -If the source node has an explicit label and no `label=...` override is -provided, the copied node keeps that label and a warning is logged. Nodes whose -label was auto-generated by a figure are treated as unlabeled here and receive a -fresh `node` label from this figure. +If the source node has an explicit label and no ``label=...`` override is +provided, the copied node keeps that label and a warning is logged. Nodes +whose label was auto-generated by a figure are treated as unlabeled here +and receive a fresh ``node`` label from this figure. ##### TikzFigure.add ```python -def add(items: list | tuple | Node, - layer: int = 0, - verbose: bool = False) -> None +def add(items: Any, layer: int = 0, verbose: bool = False) -> None ``` Add one or more pre-built items to the figure. **Arguments**: -- `items` - A single `Node` or a list/tuple of items to add. -- `layer` - Target layer index. Defaults to `0`. -- `verbose` - If `True`, print a debug message for each insertion. +- `items` - A single TikZ object (`Node`, `TikzPath`, + `tikzfigure.core.circle.Circle`, ...) or a + list/tuple of them. +- `layer` - Target layer index. Defaults to ``0``. +- `verbose` - If ``True``, print a debug message for each insertion. @@ -272,17 +328,18 @@ Add one or more pre-built items to the figure. def colorlet(name: str, color_spec: ColorInput, layer: int = 0) -> Color ``` -Define a named color with `\colorlet`. +Define a named color with ``\colorlet``. **Arguments**: - `name` - The name to assign to the color in LaTeX. -- `color_spec` - A TikZ color specification (e.g. `"blue!20"`). -- `layer` - Layer index (currently unused). Defaults to `0`. +- `color_spec` - A TikZ color specification (e.g. ``"blue!20"``). +- `layer` - Layer index (currently unused). Defaults to ``0``. + **Returns**: -The `Color` object that was registered. + The `Color` object that was registered. @@ -296,7 +353,8 @@ Add a LaTeX package to the standalone preamble. **Examples**: -> > > fig = TikzFigure() fig.add_package("amsmath") + >>> fig = TikzFigure() + >>> fig.add_package("amsmath") @@ -312,24 +370,33 @@ Define or update a named TikZ style at figure scope. **Examples**: -> > > fig = TikzFigure() axes = fig.add_style("axes") fig.add_style("important -> > > line", options=["very thick"]) fig.add_style("information text", -> > > fill="red!10", inner_sep="1ex") fig.draw([(0, 0), (1, 0)], options=[axes]) + >>> fig = TikzFigure() + >>> axes = fig.add_style("axes") + >>> fig.add_style("important line", options=["very thick"]) + >>> fig.add_style("information text", fill="red!10", inner_sep="1ex") + >>> fig.draw([(0, 0), (1, 0)], options=[axes]) ##### TikzFigure.usetikzlibrary ```python -def usetikzlibrary(*libraries: str) -> None +def usetikzlibrary( + *libraries: "str | TikzLibrary | type[TikzLibrary]") -> None ``` Register TikZ libraries for standalone output and compilation. +Accepts plain library name strings, or a `TikzLibrary` +subclass/instance for libraries with dedicated Python support (e.g. +`tikzfigure.core.matrix.MatrixLibrary`, +`tikzfigure.core.spy.SpyLibrary`). + **Examples**: -> > > fig = TikzFigure() fig.usetikzlibrary("calc", "intersections") -> > > fig.usetikzlibrary("positioning,quotes") + >>> fig = TikzFigure() + >>> fig.usetikzlibrary("calc", "intersections") + >>> fig.usetikzlibrary("positioning,quotes") @@ -337,7 +404,6 @@ Register TikZ libraries for standalone output and compilation. ```python def add_node(x: (float - | int | str | tuple[float | int | str, float | int | str] | tuple[float | int | str, float | int | str, @@ -345,8 +411,8 @@ def add_node(x: (float | Node | TikzCoordinate | None) = None, - y: float | int | str | None = None, - z: float | int | str | None = None, + y: float | str | None = None, + z: float | str | None = None, label: str | None = None, content: str = "", layer: int = 0, @@ -432,27 +498,31 @@ Add a node to the TikZ figure. **Arguments**: -- `x` - X-coordinate, a `(x, y)` / `(x, y, z)` tuple, or a `TikzCoordinate`. Use - `None` for relatively positioned nodes. -- `y` - Y-coordinate. Use `None` when `x` already provides the full position. -- `z` - Z-coordinate for 3-D figures. Use `None` when `x` already provides the +- `x` - X-coordinate, a ``(x, y)`` / ``(x, y, z)`` tuple, or a + `TikzCoordinate`. Use ``None`` for relatively + positioned nodes. +- `y` - Y-coordinate. Use ``None`` when ``x`` already provides the full position. -- `label` - Internal TikZ name. Auto-assigned when `None`. +- `z` - Z-coordinate for 3-D figures. Use ``None`` when ``x`` already + provides the full position. +- `label` - Internal TikZ name. Auto-assigned when ``None``. - `content` - Text or LaTeX content displayed inside the node. -- `layer` - Target layer index. Defaults to `0`. +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `options` - Flag-style TikZ options (e.g. `["draw", "thick"]`). -- `verbose` - If `True`, print a debug message. -- `shape` - Node shape (e.g. `"circle"`, `"rectangle"`, `"diamond"`). -- `color` - Text color (e.g. `"red"`, `"blue!50"`). +- `options` - Flag-style TikZ options + (e.g. ``["draw", "thick"]``). +- `verbose` - If ``True``, print a debug message. +- `shape` - Node shape + (e.g. ``"circle"``, ``"rectangle"``, ``"diamond"``). +- `color` - Text color (e.g. ``"red"``, ``"blue!50"``). - `fill` - Fill color. -- `draw` - Border/stroke color. Use `"none"` to hide the border. +- `draw` - Border/stroke color. Use ``"none"`` to hide the border. - `text` - Alias for text color. - `opacity` - Overall opacity (0–1). - `fill_opacity` - Fill opacity (0–1). - `draw_opacity` - Border opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `minimum_width` - Minimum node width (e.g. `"2cm"`). +- `minimum_width` - Minimum node width (e.g. ``"2cm"``). - `minimum_height` - Minimum node height. - `minimum_size` - Minimum size in both dimensions. - `inner_sep` - Inner separation between content and border. @@ -461,14 +531,15 @@ Add a node to the TikZ figure. - `outer_sep` - Outer separation between border and anchors. - `outer_xsep` - Horizontal outer separation. - `outer_ysep` - Vertical outer separation. -- `line_width` - Border line width (e.g. `"1pt"`). -- `font` - Font specification (e.g. `"\\tiny"`). +- `line_width` - Border line width (e.g. ``"1pt"``). +- `font` - Font specification (e.g. ``"\\tiny"``). - `text_width` - Text width for automatic line wrapping. - `text_height` - Explicit text height. - `text_depth` - Explicit text depth. -- `align` - Text alignment (`"left"`, `"center"`, `"right"`). -- `anchor` - Anchor point (e.g. `"center"`, `"north"`, `"south west"`). -- `above` - Place above, with optional offset (e.g. `"5pt"`). +- `align` - Text alignment (``"left"``, ``"center"``, ``"right"``). +- `anchor` - Anchor point + (e.g. ``"center"``, ``"north"``, ``"south west"``). +- `above` - Place above, with optional offset (e.g. ``"5pt"``). - `below` - Place below. - `left` - Place left. - `right` - Place right. @@ -480,23 +551,25 @@ Add a node to the TikZ figure. - `below_of` - Place below a named node. - `left_of` - Place left of a named node. - `right_of` - Place right of a named node. -- `node_distance` - Distance for relative positioning (e.g. `"1cm"`). +- `node_distance` - Distance for relative positioning + (e.g. ``"1cm"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). - `yshift` - Vertical shift. - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `xslant` - Horizontal slant factor. - `yslant` - Vertical slant factor. -- `rounded_corners` - Corner rounding radius (e.g. `"3pt"`). +- `rounded_corners` - Corner rounding radius (e.g. ``"3pt"``). - `double` - Inner color for double borders. - `double_distance` - Distance between double borders. -- `dash_pattern` - Custom dash pattern (e.g. `"on 2pt off 3pt"`). +- `dash_pattern` - Custom dash pattern (e.g. ``"on 2pt off 3pt"``). - `dash_phase` - Dash pattern starting offset. -- `pattern` - Fill pattern (e.g. `"north east lines"`). +- `pattern` - Fill pattern (e.g. ``"north east lines"``). - `pattern_color` - Color for the fill pattern. -- `shading` - Shading type (`"axis"`, `"radial"`, or `"ball"`). +- `shading` - Shading type + (``"axis"``, ``"radial"``, or ``"ball"``). - `shading_angle` - Angle for axis shading. - `left_color` - Left color for axis shading. - `right_color` - Right color for axis shading. @@ -505,8 +578,9 @@ Add a node to the TikZ figure. - `inner_color` - Inner color for radial shading. - `outer_color` - Outer color for radial shading. - `ball_color` - Ball color for ball shading. -- `regular_polygon_sides` - Number of sides for `shape="regular polygon"`. -- `star_points` - Number of points for `shape="star"`. +- `regular_polygon_sides` - Number of sides for + ``shape="regular polygon"``. +- `star_points` - Number of points for ``shape="star"``. - `star_point_ratio` - Inner-to-outer radius ratio for stars. - `star_point_height` - Height of star points. - `shadow_xshift` - Horizontal shadow shift. @@ -514,13 +588,14 @@ Add a node to the TikZ figure. - `shadow_color` - Shadow color. - `shadow_opacity` - Shadow opacity (0–1). - `shadow_scale` - Shadow scale factor. -- `pin` - Pin label specification (e.g. `"above:text"`). -- `**kwargs` - Additional TikZ options. Underscores in keys become spaces (e.g. - `signal_from` → `signal from`). +- `pin` - Pin label specification (e.g. ``"above:text"``). +- `**kwargs` - Additional TikZ options. Underscores in keys become + spaces (e.g. ``signal_from`` → ``signal from``). + **Returns**: -The `Node` object that was added. + The `Node` object that was added. @@ -541,14 +616,17 @@ Add a node at the midpoint between two existing nodes. - `node1` - First node object or its label string. - `node2` - Second node object or its label string. -- `label` - Label for the new midpoint node. Auto-assigned when `None`. -- `layer` - Target layer index. Defaults to `0`. +- `label` - Label for the new midpoint node. Auto-assigned when + ``None``. +- `layer` - Target layer index. Defaults to ``0``. - `content` - Text content for the midpoint node. - `**kwargs` - Additional options forwarded to `add_node`. + **Returns**: -The new `Node` placed at the computed midpoint. + The new `Node` placed at the computed midpoint. + **Raises**: @@ -561,105 +639,106 @@ The new `Node` placed at the computed midpoint. ```python def add_coordinate(label: str, x: (float - | int | str | tuple[float | int | str, float | int | str] | tuple[float | int | str, float | int | str, float | int | str] | TikzCoordinate | None) = None, - y: float | int | str | None = None, - z: float | int | str | None = None, + y: float | str | None = None, + z: float | str | None = None, at: str | None = None, layer: int = 0, comment: str | None = None, verbose: bool = False) -> Coordinate ``` -Add a named coordinate point using `\coordinate`. +Add a named coordinate point using ``\coordinate``. A coordinate is an invisible named point — lighter than a -`tikzfigure.core.node.Node` because it has no box, content, or visible styling. -Use it to define reusable anchor points, midpoints, or expression-based -positions that you can reference in paths and other commands. +`tikzfigure.core.node.Node` because it has no box, content, +or visible styling. Use it to define reusable anchor points, +midpoints, or expression-based positions that you can reference in +paths and other commands. -Either provide explicit coordinates, or a raw TikZ coordinate expression via -`at`. +Either provide explicit coordinates, or a raw TikZ coordinate +expression via ``at``. Examples:: ## Fixed point - -fig.add_coordinate("origin", (0, 0)) fig.draw(["origin", "A"]) +fig.add_coordinate("origin", (0, 0)) +fig.draw(["origin", "A"]) ## Midpoint using calc (add "calc" to extra_packages) - -fig.add_coordinate("mid", at="$(A)!0.5!(B)$") fig.draw(["A", "mid", "B"]) +fig.add_coordinate("mid", at="$(A)!0.5!(B)$") +fig.draw(["A", "mid", "B"]) ## At a node anchor - fig.add_coordinate("atip", at="A.north") **Arguments**: -- `label` - TikZ name for this coordinate. Used when referencing it in path - lists (e.g. `["A", "mid", "B"]`). -- `x` - X-coordinate value, a `(x, y)` / `(x, y, z)` tuple, or a - `TikzCoordinate`. Must be provided together with `y` when passed as a scalar. - Mutually exclusive with `at`. -- `y` - Y-coordinate value when passing explicit scalar coordinates. Mutually - exclusive with `at`. -- `z` - Z-coordinate value when passing explicit scalar coordinates. Mutually - exclusive with `at`. -- `at` - Raw TikZ coordinate expression. Examples: - - - `"$(A)!0.5!(B)$"` — midpoint via `calc` library - - `"A.north"` — at a node anchor - - `"30:2cm"` — polar coordinates - - Parentheses are added automatically if absent. Mutually exclusive with - `x`/`y`. -- `layer` - Target layer index. Defaults to `0`. +- `label` - TikZ name for this coordinate. Used when referencing + it in path lists (e.g. ``["A", "mid", "B"]``). +- `x` - X-coordinate value, a ``(x, y)`` / ``(x, y, z)`` tuple, or a + `TikzCoordinate`. Must be provided together with ``y`` + when passed as a scalar. Mutually exclusive with ``at``. +- `y` - Y-coordinate value when passing explicit scalar coordinates. + Mutually exclusive with ``at``. +- `z` - Z-coordinate value when passing explicit scalar coordinates. + Mutually exclusive with ``at``. +- `at` - Raw TikZ coordinate expression. Examples: + + * ``"$(A)!0.5!(B)$"`` — midpoint via ``calc`` library + * ``"A.north"`` — at a node anchor + * ``"30:2cm"`` — polar coordinates + + Parentheses are added automatically if absent. + Mutually exclusive with ``x``/``y``. +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message after insertion. +- `verbose` - If ``True``, print a debug message after insertion. + **Returns**: -The newly created `Coordinate` object. + The newly created `Coordinate` object. -### TikzFigure.add\_variable +##### TikzFigure.add\_variable ```python def add_variable(label: str, - value: int | float | str, + value: float | str, layer: int | None = 0, comment: str | None = None, verbose: bool = False) -> Variable ``` -Add a pgfmath variable (`\pgfmathsetmacro`) to the figure. +Add a pgfmath variable (``\pgfmathsetmacro``) to the figure. -Variables are emitted at the top of the `tikzpicture` environment so they can be -referenced anywhere in the figure. +Variables are emitted at the top of the ``tikzpicture`` environment +so they can be referenced anywhere in the figure. **Arguments**: - `label` - Variable name (without the leading backslash). -- `value` - Numeric value or PGF math expression string to assign (e.g. `5`, - `"sqrt(2)"`, `"sin(60)"`). -- `layer` - Layer index (currently unused). Defaults to `0`. +- `value` - Numeric value or PGF math expression string to assign + (e.g. ``5``, ``"sqrt(2)"``, ``"sin(60)"``). +- `layer` - Layer index (currently unused). Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. - `verbose` - Unused; reserved for future debug output. + **Returns**: -The `Variable` object that was added. + The `Variable` object that was added. -#### TikzFigure.declare\_function +##### TikzFigure.declare\_function ```python def declare_function(name: str, @@ -671,6 +750,10 @@ def declare_function(name: str, Declare a reusable PGF math function for this figure. +The returned `DeclaredFunction` is callable, so you can keep a +Python reference and later build expressions with ``f(x)`` instead of +spelling the function name again as a string. + ##### TikzFigure.add\_plot @@ -693,6 +776,41 @@ def add_plot(x: Any, Add a plain TikZ expression plot outside a pgfplots axis. + + +##### TikzFigure.add\_parametric\_grid + +```python +def add_parametric_grid(x: Any, + y: Any, + *, + u_variable: str, + v_variable: str, + u_values: list[Any] | tuple[Any, ...] | range, + v_values: list[Any] | tuple[Any, ...] | range, + u_domain: tuple[Any, Any] | str = (0, 1), + v_domain: tuple[Any, Any] | str = (0, 1), + samples: int | None = None, + smooth: bool = False, + layer: int = 0, + comment: str | None = None, + verbose: bool = False, + options: OptionInput | None = None, + **kwargs: Any) -> tuple[Loop, Loop] +``` + +Draw both coordinate families of a parametric quadrilateral grid. + +``x`` and ``y`` are expressions in ``u_variable`` and +``v_variable``. The first family fixes each value in ``u_values`` +and samples ``v_variable``; the second fixes each value in +``v_values`` and samples ``u_variable``. This is useful for mapped +meshes, finite-element patches, and other curvilinear grids. + +The variables should normally be created with `Var`, for +example ``x = map_x(Var("u"), Var("v"))``. The returned loops allow +callers to inspect or further customize the generated TikZ. + ##### TikzFigure.add\_raw @@ -706,12 +824,36 @@ Insert a verbatim TikZ code block into the figure. **Arguments**: - `tikz_code` - Raw TikZ source to include verbatim. -- `layer` - Target layer index. Defaults to `0`. -- `verbose` - If `True`, print a debug message after insertion. +- `layer` - Target layer index. Defaults to ``0``. +- `verbose` - If ``True``, print a debug message after insertion. + **Returns**: -The `RawTikz` object that was added. + The `RawTikz` object that was added. + + + +##### TikzFigure.add\_gantt\_chart + +```python +def add_gantt_chart(start: int | str, + end: int | str, + rows: list[dict[str, Any]] + | tuple[dict[str, Any], ...] = (), + layer: int = 0, + comment: str | None = None, + options: OptionInput | None = None, + verbose: bool = False, + **kwargs: Any) -> GanttChart +``` + +Add a chart rendered by LaTeX's ``pgfgantt`` package. + +Each row is a dictionary with ``type`` set to ``title``, +``titlelist``, ``group``, ``bar``, ``milestone``, ``link``, or +``raw``. For example: ``{"type": "bar", "content": "Build", +"start": 1, "end": 3}``. @@ -753,15 +895,15 @@ Add an arc to the figure. **Arguments**: -- `start` - Starting coordinate as an `(x, y)` / `(x, y, z)` tuple or +- `start` - Starting coordinate as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. - `start_angle` - Start angle in degrees. - `end_angle` - End angle in degrees. -- `radius` - Radius of the arc (e.g. `"3mm"`, `0.5`). -- `layer` - Target layer index. Defaults to `0`. +- `radius` - Radius of the arc (e.g. ``"3mm"``, ``0.5``). +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `color` - Line color (e.g. `"red"`, `"blue!50"`). +- `verbose` - If ``True``, print a debug message. +- `color` - Line color (e.g. ``"red"``, ``"blue!50"``). - `fill` - Fill color for the enclosed area. - `draw` - Stroke color when different from *color*. - `text` - Text color. @@ -769,24 +911,27 @@ Add an arc to the figure. - `draw_opacity` - Stroke opacity (0–1). - `fill_opacity` - Fill opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `line_width` - Line width (e.g. `"1pt"`). -- `line_cap` - Line cap style: `"butt"`, `"rect"`, or `"round"`. -- `line_join` - Line join style: `"miter"`, `"bevel"`, or `"round"`. +- `line_width` - Line width (e.g. ``"1pt"``). +- `line_cap` - Line cap style: ``"butt"``, ``"rect"``, or + ``"round"``. +- `line_join` - Line join style: ``"miter"``, ``"bevel"``, or + ``"round"``. - `miter_limit` - Miter limit factor for miter joins. -- `dash_pattern` - Custom dash pattern (e.g. `"on 2pt off 3pt"`). -- `dash_phase` - Dash pattern starting offset (e.g. `"2pt"`). -- `arrows` - Arrow specification (e.g. `"->"`, `"<->"`, `"*-*"`). +- `dash_pattern` - Custom dash pattern (e.g. ``"on 2pt off 3pt"``). +- `dash_phase` - Dash pattern starting offset (e.g. ``"2pt"``). +- `arrows` - Arrow specification (e.g. ``"->"``, ``"<->"``, ``"*-*"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). -- `yshift` - Vertical shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). +- `yshift` - Vertical shift (e.g. ``"1cm"``). - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `**kwargs` - Catch-all for unlisted TikZ options. + **Returns**: -The `Arc` object that was added. + The `Arc` object that was added. @@ -825,13 +970,13 @@ Add a circle to the figure. **Arguments**: -- `center` - Center coordinate as an `(x, y)` / `(x, y, z)` tuple or +- `center` - Center coordinate as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. -- `radius` - Radius of the circle (e.g. `"5mm"`, `1.0`). -- `layer` - Target layer index. Defaults to `0`. +- `radius` - Radius of the circle (e.g. ``"5mm"``, ``1.0``). +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `color` - Line color (e.g. `"red"`, `"blue!50"`). +- `verbose` - If ``True``, print a debug message. +- `color` - Line color (e.g. ``"red"``, ``"blue!50"``). - `fill` - Fill color for the circle. - `draw` - Stroke color when different from *color*. - `text` - Text color. @@ -839,23 +984,26 @@ Add a circle to the figure. - `draw_opacity` - Stroke opacity (0–1). - `fill_opacity` - Fill opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `line_width` - Line width (e.g. `"1pt"`). -- `line_cap` - Line cap style: `"butt"`, `"rect"`, or `"round"`. -- `line_join` - Line join style: `"miter"`, `"bevel"`, or `"round"`. +- `line_width` - Line width (e.g. ``"1pt"``). +- `line_cap` - Line cap style: ``"butt"``, ``"rect"``, or + ``"round"``. +- `line_join` - Line join style: ``"miter"``, ``"bevel"``, or + ``"round"``. - `miter_limit` - Miter limit factor for miter joins. -- `dash_pattern` - Custom dash pattern (e.g. `"on 2pt off 3pt"`). -- `dash_phase` - Dash pattern starting offset (e.g. `"2pt"`). +- `dash_pattern` - Custom dash pattern (e.g. ``"on 2pt off 3pt"``). +- `dash_phase` - Dash pattern starting offset (e.g. ``"2pt"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). -- `yshift` - Vertical shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). +- `yshift` - Vertical shift (e.g. ``"1cm"``). - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `**kwargs` - Catch-all for unlisted TikZ options. + **Returns**: -The `Circle` object that was added. + The `Circle` object that was added. @@ -895,14 +1043,14 @@ Add a rectangle to the figure. **Arguments**: -- `corner1` - First corner as an `(x, y)` / `(x, y, z)` tuple or +- `corner1` - First corner as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. -- `corner2` - Opposite corner as an `(x, y)` / `(x, y, z)` tuple or +- `corner2` - Opposite corner as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. -- `layer` - Target layer index. Defaults to `0`. +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `color` - Line color (e.g. `"red"`, `"blue!50"`). +- `verbose` - If ``True``, print a debug message. +- `color` - Line color (e.g. ``"red"``, ``"blue!50"``). - `fill` - Fill color for the rectangle. - `draw` - Stroke color when different from *color*. - `text` - Text color. @@ -910,24 +1058,149 @@ Add a rectangle to the figure. - `draw_opacity` - Stroke opacity (0–1). - `fill_opacity` - Fill opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `line_width` - Line width (e.g. `"1pt"`). -- `line_cap` - Line cap style: `"butt"`, `"rect"`, or `"round"`. -- `line_join` - Line join style: `"miter"`, `"bevel"`, or `"round"`. +- `line_width` - Line width (e.g. ``"1pt"``). +- `line_cap` - Line cap style: ``"butt"``, ``"rect"``, or ``"round"``. +- `line_join` - Line join style: ``"miter"``, ``"bevel"``, or ``"round"``. - `miter_limit` - Miter limit factor for miter joins. -- `dash_pattern` - Custom dash pattern (e.g. `"on 2pt off 3pt"`). -- `dash_phase` - Dash pattern starting offset (e.g. `"2pt"`). -- `rounded_corners` - Corner rounding radius (e.g. `"5pt"`). +- `dash_pattern` - Custom dash pattern (e.g. ``"on 2pt off 3pt"``). +- `dash_phase` - Dash pattern starting offset (e.g. ``"2pt"``). +- `rounded_corners` - Corner rounding radius (e.g. ``"5pt"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). -- `yshift` - Vertical shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). +- `yshift` - Vertical shift (e.g. ``"1cm"``). - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `**kwargs` - Catch-all for unlisted TikZ options. + +**Returns**: + + The `Rectangle` object that was added. + + + +##### TikzFigure.add\_matrix + +```python +def add_matrix(rows: list[list[Any]], + x: (float + | str + | tuple[float | int | str, float | int | str] + | tuple[float | int | str, float | int | str, + float | int | str] + | TikzCoordinate + | None) = None, + y: float | str | None = None, + z: float | str | None = None, + label: str | None = None, + layer: int = 0, + comment: str | None = None, + options: OptionInput | None = None, + row_sep: str | None = None, + column_sep: str | None = None, + cell_style: str | list[str] | None = None, + anchor: _Anchor = None, + verbose: bool = False, + **kwargs: Any) -> Matrix +``` + +Add a matrix of nodes (``\matrix``) to the TikZ figure. + +A first-class alternative to hand-writing raw ``\matrix`` TikZ, +with row/column separation, per-cell styling, and easy cell +addressing via `Matrix.cell`. + +Examples:: + +m = fig.add_matrix( +[["A", "B"], ["C", "D"]], +label="m", +row_sep="5pt", +column_sep="1cm", +cell_style="draw, minimum size=8mm, anchor=center", +) +fig.add_coordinate("c11", at=m.cell(1, 1)) +fig.add_coordinate("c22", at=m.cell(2, 2)) +fig.draw(["c11", "c22"]) + +**Arguments**: + +- `rows` - Grid of cells, given row by row. Each cell is either a + plain string (node content), a dict of +- ```{"content"` - str, "options": ..., **node_kwargs}`` for + per-cell styling, or ``None`` for an empty cell. +- `x` - X-coordinate, a ``(x, y)`` / ``(x, y, z)`` tuple, or a + `TikzCoordinate` giving the matrix's anchor + position. Use ``None`` to let TikZ place it at the origin. +- `y` - Y-coordinate. Use ``None`` when ``x`` already provides the + full position. +- `z` - Z-coordinate for 3-D figures. +- `label` - Internal TikZ name. Auto-assigned when ``None``. + Required (explicitly or via auto-assignment) to address + individual cells with `Matrix.cell`. +- `layer` - Target layer index. Defaults to ``0``. +- `comment` - Optional comment prepended in the TikZ output. +- `options` - Flag-style TikZ options for the matrix node itself. +- `row_sep` - Row separation (e.g. ``"5pt"``). +- `column_sep` - Column separation (e.g. ``"1cm"``). +- `cell_style` - Shared TikZ style(s) applied to every cell node, + rendered as ``nodes={}``. +- `anchor` - Anchor point for the whole matrix node. +- `verbose` - If ``True``, print a debug message. +- `**kwargs` - Additional matrix-level TikZ options. + + +**Returns**: + + The `Matrix` object that was added. + + + +##### TikzFigure.add\_fit + +```python +def add_fit(targets: list[Any], + content: str = "", + label: str | None = None, + layer: int = 0, + comment: str | None = None, + options: OptionInput | None = None, + verbose: bool = False, + **kwargs: Any) -> Fit +``` + +Wrap existing nodes/coordinates in a bounding node (``fit`` library). + +A first-class alternative to hand-writing raw ``fit=(...)...`` node +options. + +Examples:: + +a = fig.add_node((0, 0), content="A") +b = fig.add_node((2, 1), content="B") +fig.add_fit([a, b], options=["draw", "dashed", "rounded corners"]) + +**Arguments**: + +- `targets` - Nodes/coordinates to fit around, as a list of + `Node`/`Coordinate` objects or label strings + (optionally with a ``.anchor`` suffix, e.g. ``"a.north"``). + At least one is required. +- `content` - Text or LaTeX content displayed inside the fit node. + Usually left empty. +- `label` - Internal TikZ name. Auto-assigned when ``None``. +- `layer` - Target layer index. Defaults to ``0``. +- `comment` - Optional comment prepended in the TikZ output. +- `options` - Flag-style TikZ options (e.g. ``["draw", "dashed"]``). +- `verbose` - If ``True``, print a debug message. +- `**kwargs` - Additional TikZ options for the fit node (e.g. + ``inner_sep="5pt"``). + + **Returns**: -The `Rectangle` object that was added. + The `Fit` object that was added. @@ -967,14 +1240,14 @@ Add an ellipse to the figure. **Arguments**: -- `center` - Center coordinate as an `(x, y)` / `(x, y, z)` tuple or +- `center` - Center coordinate as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. -- `x_radius` - Horizontal radius (e.g. `"2cm"`, `1.5`). -- `y_radius` - Vertical radius (e.g. `"1cm"`, `0.75`). -- `layer` - Target layer index. Defaults to `0`. +- `x_radius` - Horizontal radius (e.g. ``"2cm"``, ``1.5``). +- `y_radius` - Vertical radius (e.g. ``"1cm"``, ``0.75``). +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `color` - Line color (e.g. `"red"`, `"blue!50"`). +- `verbose` - If ``True``, print a debug message. +- `color` - Line color (e.g. ``"red"``, ``"blue!50"``). - `fill` - Fill color for the ellipse. - `draw` - Stroke color when different from *color*. - `text` - Text color. @@ -982,23 +1255,24 @@ Add an ellipse to the figure. - `draw_opacity` - Stroke opacity (0–1). - `fill_opacity` - Fill opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `line_width` - Line width (e.g. `"1pt"`). -- `line_cap` - Line cap style: `"butt"`, `"rect"`, or `"round"`. -- `line_join` - Line join style: `"miter"`, `"bevel"`, or `"round"`. +- `line_width` - Line width (e.g. ``"1pt"``). +- `line_cap` - Line cap style: ``"butt"``, ``"rect"``, or ``"round"``. +- `line_join` - Line join style: ``"miter"``, ``"bevel"``, or ``"round"``. - `miter_limit` - Miter limit factor for miter joins. -- `dash_pattern` - Custom dash pattern (e.g. `"on 2pt off 3pt"`). -- `dash_phase` - Dash pattern starting offset (e.g. `"2pt"`). +- `dash_pattern` - Custom dash pattern (e.g. ``"on 2pt off 3pt"``). +- `dash_phase` - Dash pattern starting offset (e.g. ``"2pt"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). -- `yshift` - Vertical shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). +- `yshift` - Vertical shift (e.g. ``"1cm"``). - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `**kwargs` - Catch-all for unlisted TikZ options. + **Returns**: -The `Ellipse` object that was added. + The `Ellipse` object that was added. @@ -1032,34 +1306,36 @@ Add a grid to the figure. **Arguments**: -- `corner1` - First corner as an `(x, y)` / `(x, y, z)` tuple or +- `corner1` - First corner as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. -- `corner2` - Opposite corner as an `(x, y)` / `(x, y, z)` tuple or +- `corner2` - Opposite corner as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. -- `step` - Grid step size (e.g. `"1cm"` for uniform, or `"0.5cm,1cm"` for - different x/y steps). Defaults to `None` (TikZ default of 1cm). -- `layer` - Target layer index. Defaults to `0`. +- `step` - Grid step size (e.g. ``"1cm"`` for uniform, or + ``"0.5cm,1cm"`` for different x/y steps). Defaults to ``None`` + (TikZ default of 1cm). +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `color` - Line color (e.g. `"red"`, `"gray!30"`). +- `verbose` - If ``True``, print a debug message. +- `color` - Line color (e.g. ``"red"``, ``"gray!30"``). - `opacity` - Overall opacity (0–1). - `draw_opacity` - Stroke opacity (0–1). -- `line_width` - Line width (e.g. `"0.5pt"`). -- `line_cap` - Line cap style: `"butt"`, `"rect"`, or `"round"`. -- `line_join` - Line join style: `"miter"`, `"bevel"`, or `"round"`. -- `dash_pattern` - Custom dash pattern (e.g. `"on 1pt off 1pt"`). -- `dash_phase` - Dash pattern starting offset (e.g. `"1pt"`). +- `line_width` - Line width (e.g. ``"0.5pt"``). +- `line_cap` - Line cap style: ``"butt"``, ``"rect"``, or ``"round"``. +- `line_join` - Line join style: ``"miter"``, ``"bevel"``, or ``"round"``. +- `dash_pattern` - Custom dash pattern (e.g. ``"on 1pt off 1pt"``). +- `dash_phase` - Dash pattern starting offset (e.g. ``"1pt"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). -- `yshift` - Vertical shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). +- `yshift` - Vertical shift (e.g. ``"1cm"``). - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `**kwargs` - Catch-all for unlisted TikZ options. + **Returns**: -The `Grid` object that was added. + The `Grid` object that was added. @@ -1100,16 +1376,16 @@ Add a parabola to the figure. **Arguments**: -- `start` - Starting coordinate as an `(x, y)` / `(x, y, z)` tuple or +- `start` - Starting coordinate as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. -- `end` - Ending coordinate as an `(x, y)` / `(x, y, z)` tuple or +- `end` - Ending coordinate as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. -- `bend` - Bend point as an `(x, y)` / `(x, y, z)` tuple or `TikzCoordinate`. If - None, uses TikZ default. -- `layer` - Target layer index. Defaults to `0`. +- `bend` - Bend point as an ``(x, y)`` / ``(x, y, z)`` tuple or + `TikzCoordinate`. If None, uses TikZ default. +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `color` - Line color (e.g. `"red"`, `"blue!50"`). +- `verbose` - If ``True``, print a debug message. +- `color` - Line color (e.g. ``"red"``, ``"blue!50"``). - `fill` - Fill color for the parabola. - `draw` - Stroke color when different from *color*. - `text` - Text color. @@ -1117,24 +1393,25 @@ Add a parabola to the figure. - `draw_opacity` - Stroke opacity (0–1). - `fill_opacity` - Fill opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `line_width` - Line width (e.g. `"1pt"`). -- `line_cap` - Line cap style: `"butt"`, `"rect"`, or `"round"`. -- `line_join` - Line join style: `"miter"`, `"bevel"`, or `"round"`. +- `line_width` - Line width (e.g. ``"1pt"``). +- `line_cap` - Line cap style: ``"butt"``, ``"rect"``, or ``"round"``. +- `line_join` - Line join style: ``"miter"``, ``"bevel"``, or ``"round"``. - `miter_limit` - Miter limit factor for miter joins. -- `dash_pattern` - Custom dash pattern (e.g. `"on 2pt off 3pt"`). -- `dash_phase` - Dash pattern starting offset (e.g. `"2pt"`). -- `arrows` - Arrow specification (e.g. `"->"`, `"<->"`, `"*-*"`). +- `dash_pattern` - Custom dash pattern (e.g. ``"on 2pt off 3pt"``). +- `dash_phase` - Dash pattern starting offset (e.g. ``"2pt"``). +- `arrows` - Arrow specification (e.g. ``"->"``, ``"<->"``, ``"*-*"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). -- `yshift` - Vertical shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). +- `yshift` - Vertical shift (e.g. ``"1cm"``). - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `**kwargs` - Catch-all for unlisted TikZ options. + **Returns**: -The `Parabola` object that was added. + The `Parabola` object that was added. @@ -1171,36 +1448,38 @@ Add a line segment to the figure. **Arguments**: -- `start` - Starting point as an `(x, y)` / `(x, y, z)` tuple or +- `start` - Starting point as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. -- `end` - Ending point as an `(x, y)` / `(x, y, z)` tuple or `TikzCoordinate`. -- `layer` - Target layer index. Defaults to `0`. +- `end` - Ending point as an ``(x, y)`` / ``(x, y, z)`` tuple or + `TikzCoordinate`. +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `options` - Flag-style TikZ options without values (e.g. - `["dashed", "thick"]`). -- `color` - Line color (e.g. `"red"`, `"blue!50"`). +- `verbose` - If ``True``, print a debug message. +- `options` - Flag-style TikZ options without values + (e.g. ``["dashed", "thick"]``). +- `color` - Line color (e.g. ``"red"``, ``"blue!50"``). - `text` - Text color. - `opacity` - Overall opacity (0–1). - `draw_opacity` - Stroke opacity (0–1). -- `line_width` - Line width (e.g. `"1pt"`). -- `line_cap` - Line cap style: `"butt"`, `"rect"`, or `"round"`. -- `line_join` - Line join style: `"miter"`, `"bevel"`, or `"round"`. +- `line_width` - Line width (e.g. ``"1pt"``). +- `line_cap` - Line cap style: ``"butt"``, ``"rect"``, or ``"round"``. +- `line_join` - Line join style: ``"miter"``, ``"bevel"``, or ``"round"``. - `miter_limit` - Miter limit factor for miter joins. -- `dash_pattern` - Custom dash pattern (e.g. `"on 2pt off 3pt"`). -- `dash_phase` - Dash pattern starting offset (e.g. `"2pt"`). -- `arrows` - Arrow specification (e.g. `"->"`, `"<->"`, `"*-*"`). +- `dash_pattern` - Custom dash pattern (e.g. ``"on 2pt off 3pt"``). +- `dash_phase` - Dash pattern starting offset (e.g. ``"2pt"``). +- `arrows` - Arrow specification (e.g. ``"->"``, ``"<->"``, ``"*-*"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). -- `yshift` - Vertical shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). +- `yshift` - Vertical shift (e.g. ``"1cm"``). - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `**kwargs` - Catch-all for unlisted TikZ options. + **Returns**: -The `Line` object that was added. + The `Line` object that was added. @@ -1241,15 +1520,15 @@ Add a regular polygon to the figure. **Arguments**: -- `center` - Center coordinate as an `(x, y)` / `(x, y, z)` tuple or +- `center` - Center coordinate as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. - `radius` - Distance from center to vertices. - `sides` - Number of sides (must be >= 3). -- `rotation` - Rotation angle in degrees. Defaults to `0`. -- `layer` - Target layer index. Defaults to `0`. +- `rotation` - Rotation angle in degrees. Defaults to ``0``. +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `color` - Line color (e.g. `"red"`, `"blue!50"`). +- `verbose` - If ``True``, print a debug message. +- `color` - Line color (e.g. ``"red"``, ``"blue!50"``). - `fill` - Fill color for the polygon. - `draw` - Stroke color when different from *color*. - `text` - Text color. @@ -1257,23 +1536,24 @@ Add a regular polygon to the figure. - `draw_opacity` - Stroke opacity (0–1). - `fill_opacity` - Fill opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `line_width` - Line width (e.g. `"1pt"`). -- `line_cap` - Line cap style: `"butt"`, `"rect"`, or `"round"`. -- `line_join` - Line join style: `"miter"`, `"bevel"`, or `"round"`. +- `line_width` - Line width (e.g. ``"1pt"``). +- `line_cap` - Line cap style: ``"butt"``, ``"rect"``, or ``"round"``. +- `line_join` - Line join style: ``"miter"``, ``"bevel"``, or ``"round"``. - `miter_limit` - Miter limit factor for miter joins. -- `dash_pattern` - Custom dash pattern (e.g. `"on 2pt off 3pt"`). -- `dash_phase` - Dash pattern starting offset (e.g. `"2pt"`). +- `dash_pattern` - Custom dash pattern (e.g. ``"on 2pt off 3pt"``). +- `dash_phase` - Dash pattern starting offset (e.g. ``"2pt"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). -- `yshift` - Vertical shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). +- `yshift` - Vertical shift (e.g. ``"1cm"``). - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `**kwargs` - Catch-all for unlisted TikZ options. + **Returns**: -The `Polygon` object that was added. + The `Polygon` object that was added. @@ -1302,14 +1582,14 @@ Add an equilateral triangle to the figure. **Arguments**: -- `center` - Center coordinate as an `(x, y)` / `(x, y, z)` tuple or +- `center` - Center coordinate as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. - `radius` - Distance from center to vertices. -- `rotation` - Rotation angle in degrees. Defaults to `0`. -- `layer` - Target layer index. Defaults to `0`. +- `rotation` - Rotation angle in degrees. Defaults to ``0``. +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `color` - Line color (e.g. `"red"`, `"blue!50"`). +- `verbose` - If ``True``, print a debug message. +- `color` - Line color (e.g. ``"red"``, ``"blue!50"``). - `fill` - Fill color for the triangle. - `draw` - Stroke color when different from *color*. - `text` - Text color. @@ -1317,12 +1597,13 @@ Add an equilateral triangle to the figure. - `draw_opacity` - Stroke opacity (0–1). - `fill_opacity` - Fill opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `line_width` - Line width (e.g. `"1pt"`). +- `line_width` - Line width (e.g. ``"1pt"``). - `**kwargs` - Catch-all for unlisted TikZ options. + **Returns**: -The `Triangle` object that was added. + The `Triangle` object that was added. @@ -1351,14 +1632,14 @@ Add a square to the figure. **Arguments**: -- `center` - Center coordinate as an `(x, y)` / `(x, y, z)` tuple or +- `center` - Center coordinate as an ``(x, y)`` / ``(x, y, z)`` tuple or `TikzCoordinate`. - `radius` - Distance from center to vertices (corner-to-center distance). -- `rotation` - Rotation angle in degrees. Defaults to `45` (axis-aligned). -- `layer` - Target layer index. Defaults to `0`. +- `rotation` - Rotation angle in degrees. Defaults to ``45`` (axis-aligned). +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. -- `color` - Line color (e.g. `"red"`, `"blue!50"`). +- `verbose` - If ``True``, print a debug message. +- `color` - Line color (e.g. ``"red"``, ``"blue!50"``). - `fill` - Fill color for the square. - `draw` - Stroke color when different from *color*. - `text` - Text color. @@ -1366,12 +1647,13 @@ Add a square to the figure. - `draw_opacity` - Stroke opacity (0–1). - `fill_opacity` - Fill opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `line_width` - Line width (e.g. `"1pt"`). +- `line_width` - Line width (e.g. ``"1pt"``). - `**kwargs` - Catch-all for unlisted TikZ options. + **Returns**: -The `Square` object that was added. + The `Square` object that was added. @@ -1388,22 +1670,23 @@ def plot3d(x: list[float], **kwargs: Any) -> Plot3D ``` -Add a 3-D data plot with `\addplot3`. +Add a 3-D data plot with ``\addplot3``. **Arguments**: - `x` - List of x-coordinate values. - `y` - List of y-coordinate values. - `z` - List of z-coordinate values. -- `layer` - Target layer index. Defaults to `0`. +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `center` - If `True`, connect through `.center` anchors. -- `verbose` - If `True`, print a debug message. -- `**kwargs` - Additional pgfplots options (e.g. `mark="*"`). +- `center` - If ``True``, connect through ``.center`` anchors. +- `verbose` - If ``True``, print a debug message. +- `**kwargs` - Additional pgfplots options (e.g. ``mark="*"``). + **Returns**: -The `Plot3D` object that was added. + The `Plot3D` object that was added. @@ -1414,9 +1697,11 @@ def axis2d(xlabel: str = "", ylabel: str = "", xlim: tuple[float, float] | None = None, ylim: tuple[float, float] | None = None, + xlog: bool = False, + ylog: bool = False, grid: bool | str = True, - width: str | int | float | None = None, - height: str | int | float | None = None, + width: str | float | None = None, + height: str | float | None = None, layer: int = 0, comment: str | None = None, **kwargs: Any) -> Axis2D @@ -1430,19 +1715,22 @@ Create and register a 2D axis. - `ylabel` - Label for y-axis. Defaults to "". - `xlim` - (min, max) tuple for x-axis limits, or None for auto. - `ylim` - (min, max) tuple for y-axis limits, or None for auto. -- `grid` - Enable grid lines. Pass `True` / `False` for the usual pgfplots - values or a string such as `"major"`. -- `width` - Width of the axis as a string (e.g., "8cm"), number in cm, or None - for auto. Defaults to None. -- `height` - Height of the axis as a string (e.g., "6cm"), number in cm, or None - for auto. Defaults to None. +- `xlog` - Whether to use logarithmic scaling on the x-axis. +- `ylog` - Whether to use logarithmic scaling on the y-axis. +- `grid` - Enable grid lines. Pass ``True`` / ``False`` for the usual + pgfplots values or a string such as ``"major"``. +- `width` - Width of the axis as a string (e.g., "8cm"), number in cm, + or None for auto. Defaults to None. +- `height` - Height of the axis as a string (e.g., "6cm"), number in cm, + or None for auto. Defaults to None. - `layer` - Layer index (for metadata; axes render after all layers). - `comment` - Optional comment prepended in TikZ output. - `**kwargs` - Additional pgfplots axis options. + **Returns**: -The newly created Axis2D object. + The newly created Axis2D object. @@ -1456,21 +1744,22 @@ def add_loop(variable: str, verbose: bool = False) -> Loop ``` -Add a `\foreach` loop to the figure. +Add a ``\foreach`` loop to the figure. **Arguments**: -- `variable` - Loop variable name (without the leading backslash), e.g. `"i"` - produces `\foreach \i in {...}`. +- `variable` - Loop variable name (without the leading backslash), + e.g. ``"i"`` produces ``\foreach \i in {...}``. - `values` - Sequence of values to iterate over. -- `layer` - Target layer index. Defaults to `0`. +- `layer` - Target layer index. Defaults to ``0``. - `comment` - Optional comment prepended in the TikZ output. -- `verbose` - If `True`, print a debug message. +- `verbose` - If ``True``, print a debug message. + **Returns**: -The `Loop` context manager. Add nodes and paths inside a `with` block or by -calling methods on the returned object. + The `Loop` context manager. Add nodes and paths inside a + ``with`` block or by calling methods on the returned object. @@ -1484,7 +1773,7 @@ def add_scope(layer: int = 0, **kwargs: Any) -> Scope ``` -Add a TikZ `scope` block with local options to the figure. +Add a TikZ ``scope`` block with local options to the figure. @@ -1495,7 +1784,7 @@ Add a TikZ `scope` block with local options to the figure. def document_setup() -> str | None ``` -Raw LaTeX inserted in the preamble before `\begin{document}`. +Raw LaTeX inserted in the preamble before ``\begin{document}``. @@ -1528,7 +1817,7 @@ Named TikZ styles defined at figure scope. def tikz_libraries() -> list[str] ``` -TikZ libraries loaded in standalone output via `\usetikzlibrary`. +TikZ libraries loaded in standalone output via ``\usetikzlibrary``. @@ -1539,7 +1828,7 @@ TikZ libraries loaded in standalone output via `\usetikzlibrary`. def ndim() -> int ``` -Number of spatial dimensions (`2` or `3`). +Number of spatial dimensions (``2`` or ``3``). @@ -1561,7 +1850,7 @@ The `LayerCollection` holding all drawing layers. def colors() -> list[tuple[str, Color]] ``` -List of `(name, Color)` pairs defined with `colorlet`. +List of ``(name, Color)`` pairs defined with `colorlet`. @@ -1607,7 +1896,7 @@ def subfigure_axes() -> list[tuple[Axis2D, float]] List of subfigure axes with widths. - + ##### TikzFigure.\_\_repr\_\_ @@ -1617,7 +1906,7 @@ def __repr__() -> str Return the generated TikZ source as the object representation. - + ##### TikzFigure.\_\_str\_\_ @@ -1643,27 +1932,29 @@ class Node(TikzObject) A TikZ node with an optional position, content, and styling options. -Accepts the same TikZ options as `TikzFigure.add_node`. Refer to that method for -the full parameter reference. +Accepts the same TikZ options as `TikzFigure.add_node`. Refer to +that method for the full parameter reference. **Attributes**: -- `x` - X-coordinate of the node, or `None` for relatively positioned nodes. -- `y` - Y-coordinate of the node, or `None` for relatively positioned nodes. -- `z` - Z-coordinate for 3-D figures, or `None` for 2-D figures. -- `ndim` - Number of spatial dimensions (`2` or `3`). +- `x` - X-coordinate of the node, or ``None`` for relatively positioned + nodes. +- `y` - Y-coordinate of the node, or ``None`` for relatively positioned + nodes. +- `z` - Z-coordinate for 3-D figures, or ``None`` for 2-D figures. +- `ndim` - Number of spatial dimensions (``2`` or ``3``). - `content` - Text or LaTeX content displayed inside the node. - + -#### Node.\_\_init\_\_ +##### Node.\_\_init\_\_ ```python -def __init__(x: CoordinateValue - | CoordinateTuple2D - | CoordinateTuple3D - | TikzCoordinate - | None = None, +def __init__(x: (CoordinateValue + | CoordinateTuple2D + | CoordinateTuple3D + | TikzCoordinate + | None) = None, y: CoordinateValue | None = None, z: CoordinateValue | None = None, label: str = "", @@ -1748,22 +2039,24 @@ def __init__(x: CoordinateValue Initialize a Node. -Accepts the same TikZ options as `TikzFigure.add_node`. Refer to that method for -the full parameter reference. +Accepts the same TikZ options as `TikzFigure.add_node`. +Refer to that method for the full parameter reference. **Arguments**: -- `x` - X-coordinate, a `(x, y)` / `(x, y, z)` tuple, or a `TikzCoordinate`. Use - `None` for relatively positioned nodes. -- `y` - Y-coordinate. Use `None` when `x` already provides the full position. -- `z` - Z-coordinate for 3-D figures. Use `None` when `x` already provides the +- `x` - X-coordinate, a ``(x, y)`` / ``(x, y, z)`` tuple, or a + `TikzCoordinate`. Use ``None`` for relatively + positioned nodes. +- `y` - Y-coordinate. Use ``None`` when ``x`` already provides the full position. -- `label` - Internal TikZ name for this node. Defaults to `""`. +- `z` - Z-coordinate for 3-D figures. Use ``None`` when ``x`` already + provides the full position. +- `label` - Internal TikZ name for this node. Defaults to ``""``. - `content` - Text or LaTeX content displayed inside the node. - `comment` - Optional comment prepended in the TikZ output. -- `layer` - Layer index. Defaults to `0`. -- `options` - Flag-style TikZ options (e.g. `["draw", "thick"]`). -- `shape` - Node shape (e.g. `"circle"`, `"rectangle"`). +- `layer` - Layer index. Defaults to ``0``. +- `options` - Flag-style TikZ options (e.g. ``["draw", "thick"]``). +- `shape` - Node shape (e.g. ``"circle"``, ``"rectangle"``). - `color` - Text color. - `fill` - Fill color. - `draw` - Border/stroke color. @@ -1772,7 +2065,7 @@ the full parameter reference. - `fill_opacity` - Fill opacity (0–1). - `draw_opacity` - Border opacity (0–1). - `text_opacity` - Text opacity (0–1). -- `minimum_width` - Minimum node width (e.g. `"2cm"`). +- `minimum_width` - Minimum node width (e.g. ``"2cm"``). - `minimum_height` - Minimum node height. - `minimum_size` - Minimum size in both dimensions. - `inner_sep` - Inner separation between content and border. @@ -1781,14 +2074,14 @@ the full parameter reference. - `outer_sep` - Outer separation between border and anchors. - `outer_xsep` - Horizontal outer separation. - `outer_ysep` - Vertical outer separation. -- `line_width` - Border line width (e.g. `"1pt"`). -- `font` - Font specification (e.g. `"\\tiny"`). +- `line_width` - Border line width (e.g. ``"1pt"``). +- `font` - Font specification (e.g. ``"\\tiny"``). - `text_width` - Text width for automatic line wrapping. - `text_height` - Explicit text height. - `text_depth` - Explicit text depth. -- `align` - Text alignment (`"left"`, `"center"`, `"right"`). -- `anchor` - Anchor point (e.g. `"center"`, `"north"`). -- `above` - Place above, with optional offset (e.g. `"5pt"`). +- `align` - Text alignment (``"left"``, ``"center"``, ``"right"``). +- `anchor` - Anchor point (e.g. ``"center"``, ``"north"``). +- `above` - Place above, with optional offset (e.g. ``"5pt"``). - `below` - Place below. - `left` - Place left. - `right` - Place right. @@ -1800,23 +2093,23 @@ the full parameter reference. - `below_of` - Place below a named node. - `left_of` - Place left of a named node. - `right_of` - Place right of a named node. -- `node_distance` - Distance for relative positioning (e.g. `"1cm"`). +- `node_distance` - Distance for relative positioning (e.g. ``"1cm"``). - `rotate` - Rotation angle in degrees. -- `xshift` - Horizontal shift (e.g. `"1cm"`). +- `xshift` - Horizontal shift (e.g. ``"1cm"``). - `yshift` - Vertical shift. - `scale` - Uniform scaling factor. - `xscale` - Horizontal scaling factor. - `yscale` - Vertical scaling factor. - `xslant` - Horizontal slant factor. - `yslant` - Vertical slant factor. -- `rounded_corners` - Corner rounding radius (e.g. `"3pt"`). +- `rounded_corners` - Corner rounding radius (e.g. ``"3pt"``). - `double` - Inner color for double borders. - `double_distance` - Distance between double borders. -- `dash_pattern` - Custom dash pattern (e.g. `"on 2pt off 3pt"`). +- `dash_pattern` - Custom dash pattern (e.g. ``"on 2pt off 3pt"``). - `dash_phase` - Dash pattern starting offset. -- `pattern` - Fill pattern (e.g. `"north east lines"`). +- `pattern` - Fill pattern (e.g. ``"north east lines"``). - `pattern_color` - Color for the fill pattern. -- `shading` - Shading type (`"axis"`, `"radial"`, `"ball"`). +- `shading` - Shading type (``"axis"``, ``"radial"``, ``"ball"``). - `shading_angle` - Angle for axis shading. - `left_color` - Left color for axis shading. - `right_color` - Right color for axis shading. @@ -1825,8 +2118,8 @@ the full parameter reference. - `inner_color` - Inner color for radial shading. - `outer_color` - Outer color for radial shading. - `ball_color` - Ball color for ball shading. -- `regular_polygon_sides` - Number of sides for `shape="regular polygon"`. -- `star_points` - Number of points for `shape="star"`. +- `regular_polygon_sides` - Number of sides for ``shape="regular polygon"``. +- `star_points` - Number of points for ``shape="star"``. - `star_point_ratio` - Inner-to-outer radius ratio for stars. - `star_point_height` - Height of star points. - `shadow_xshift` - Horizontal shadow shift. @@ -1834,9 +2127,9 @@ the full parameter reference. - `shadow_color` - Shadow color. - `shadow_opacity` - Shadow opacity (0–1). - `shadow_scale` - Shadow scale factor. -- `pin` - Pin label specification (e.g. `"above:text"`). -- `**kwargs` - Additional TikZ options not listed above. Underscores in keys - become spaces in the output. +- `pin` - Pin label specification (e.g. ``"above:text"``). +- `**kwargs` - Additional TikZ options not listed above. Underscores + in keys become spaces in the output. @@ -1847,7 +2140,7 @@ the full parameter reference. def x() -> CoordinateValue | None ``` -X-coordinate, or `None` for relatively positioned nodes. +X-coordinate, or ``None`` for relatively positioned nodes. @@ -1858,7 +2151,7 @@ X-coordinate, or `None` for relatively positioned nodes. def y() -> CoordinateValue | None ``` -Y-coordinate, or `None` for relatively positioned nodes. +Y-coordinate, or ``None`` for relatively positioned nodes. @@ -1869,7 +2162,7 @@ Y-coordinate, or `None` for relatively positioned nodes. def z() -> CoordinateValue | None ``` -Z-coordinate for 3-D figures, or `None` for 2-D figures. +Z-coordinate for 3-D figures, or ``None`` for 2-D figures. @@ -1880,7 +2173,7 @@ Z-coordinate for 3-D figures, or `None` for 2-D figures. def coordinate() -> TikzCoordinate | None ``` -TikzCoordinate for this node, or `None` for relatively positioned nodes. +TikzCoordinate for this node, or ``None`` for relatively positioned nodes. @@ -1902,7 +2195,7 @@ Alias for `coordinate` for geometric operations. def ndim() -> int ``` -Number of spatial dimensions (`2` or `3`). +Number of spatial dimensions (``2`` or ``3``). @@ -1986,12 +2279,13 @@ def to(target: "Node | Coordinate", **kwargs: Any) -> "NodePathBuilder" ``` -Create a path builder for a segment from this node to `target`. +Create a path builder for a segment from this node to ``target``. -The returned builder always starts with `self` and records the segment options -for the edge from `self` to `target`. Chain additional `.to(...)` or `.arc(...)` -calls on the builder to add more path segments. Only `Node` and `Coordinate` -targets are accepted by `.to(...)`. +The returned builder always starts with ``self`` and records the segment +options for the edge from ``self`` to ``target``. Chain additional +``.to(...)`` or ``.arc(...)`` calls on the builder to add more path +segments. Only `Node` and `Coordinate` targets are +accepted by ``.to(...)``. @@ -2001,12 +2295,12 @@ targets are accepted by `.to(...)`. def to_tikz(output_unit: str | None = None) -> str ``` -Generate the TikZ `\node` command for this node. +Generate the TikZ ``\node`` command for this node. **Returns**: -A TikZ `\node` command string ending with a newline, optionally preceded by a -comment line. + A TikZ ``\node`` command string ending with a newline, optionally + preceded by a comment line. @@ -2020,7 +2314,8 @@ Serialize this node to a plain dictionary. **Returns**: -A dictionary with `type`, `x`, `y`, `z`, `content`, and all base-class keys. + A dictionary with ``type``, ``x``, ``y``, ``z``, ``content``, + and all base-class keys. @@ -2037,385 +2332,1034 @@ Reconstruct a Node from a dictionary. - `d` - Dictionary as produced by `to_dict`. + **Returns**: -A new `Node` instance. + A new `Node` instance. -## Coordinates and vectors +## Implicit figure API ```python -from tikzfigure import TikzCoordinate, TikzVector +import tikzfigure as tf ``` - - -### Coordinate +Module-level drawing API backed by an implicit "current figure". -```python -class Coordinate(TikzObject) -``` +This mirrors the way `matplotlib.pyplot` works: instead of creating a +`tikzfigure.core.figure.TikzFigure` yourself, you call the drawing +functions directly on the module and they are forwarded to the current +figure, which is created on demand:: -A named TikZ coordinate declared with `\coordinate`. + import tikzfigure as tf -Unlike a `tikzfigure.core.node.Node`, a coordinate has no visible output — it -simply registers a named point in the figure that can be referenced in paths, -calc expressions, or anywhere TikZ accepts a node label. + tf.draw([(0, 0), (1, 1)]) + tf.show() -Common uses: +This is a convenience layer for quick scripts and notebooks. For anything +larger, prefer the explicit object-oriented API:: -- Defining fixed reference points:: + fig = tf.TikzFigure() + fig.draw([(0, 0), (1, 1)]) + fig.show() -fig.add_coordinate("origin", x=0, y=0) fig.draw(["origin", "A"]) +Every function here simply calls the identically named method on +`gcf`, so see `tikzfigure.core.figure.TikzFigure` for the +full argument documentation. -- Placing a point at the midpoint between two nodes (requires the `calc` TikZ - library):: + -fig.add_coordinate("mid", at="$(A)!0.5!(B)$") fig.draw(["A", "mid", "B"]) +##### gcf -- Naming a point at a node anchor:: +```python +def gcf() -> TikzFigure +``` -fig.add_coordinate("atip", at="A.north") +Return the current figure, creating an empty one if needed. -**Attributes**: +**Returns**: -- `label` - The TikZ name for this coordinate. -- `x` - X value when using absolute positioning, or `None`. -- `y` - Y value when using absolute positioning, or `None`. -- `at` - Raw TikZ coordinate expression when using expression positioning, or - `None`. + The `tikzfigure.core.figure.TikzFigure` that the + module-level drawing functions operate on. - + -#### Coordinate.\_\_init\_\_ +##### scf ```python -def __init__(label: str, - x: CoordinateValue - | CoordinateTuple2D - | CoordinateTuple3D - | "TikzCoordinate" - | None = None, - y: CoordinateValue | None = None, - z: CoordinateValue | None = None, - at: str | None = None, - layer: int = 0, - comment: str | None = None) -> None +def scf(fig: TikzFigure) -> TikzFigure ``` -Initialize a Coordinate. - -Provide either explicit coordinates, or a raw TikZ coordinate expression via -`at`. +Make *fig* the current figure. **Arguments**: -- `label` - TikZ name for this coordinate, used when referencing it in path - lists and other commands. -- `x` - X-coordinate value, a `(x, y)` / `(x, y, z)` tuple, or a - `TikzCoordinate`. Must be provided together with `y` when passed as a scalar. - Mutually exclusive with `at`. -- `y` - Y-coordinate value when passing explicit scalar coordinates. Mutually - exclusive with `at`. -- `z` - Z-coordinate value when passing explicit scalar coordinates. Mutually - exclusive with `at`. -- `at` - Raw TikZ coordinate expression. Examples: +- `fig` - The figure the module-level functions should draw on. - - `"$(A)!0.5!(B)$"` — midpoint via `calc` library - - `"A.north"` — at a node anchor - - `"30:2cm"` — polar coordinates - If the string does not begin with `(`, parentheses are added automatically. - Mutually exclusive with `x`/`y`. -- `layer` - Layer index this coordinate belongs to. Defaults to `0`. -- `comment` - Optional comment prepended in the TikZ output. +**Returns**: + + The figure that was passed in. + **Raises**: -- `ValueError` - If both `at` and explicit coordinates are supplied, or if - neither is supplied. +- `TypeError` - If *fig* is not a `TikzFigure`. - + -##### Coordinate.x +##### figure ```python -@property -def x() -> CoordinateValue | None +def figure(*args: Any, **kwargs: Any) -> TikzFigure ``` -X-coordinate value, or `None` if using an `at` expression. +Create a new figure, make it current, and return it. - +Accepts the same arguments as `tikzfigure.core.figure.TikzFigure`. -##### Coordinate.y + + +##### clf ```python -@property -def y() -> CoordinateValue | None +def clf(*args: Any, **kwargs: Any) -> TikzFigure ``` -Y-coordinate value, or `None` if using an `at` expression. +Replace the current figure with a fresh, empty one. - +Accepts the same arguments as `tikzfigure.core.figure.TikzFigure`. -##### Coordinate.z + + +##### close ```python -@property -def z() -> CoordinateValue | None +def close() -> None ``` -Z-coordinate value, or `None` for 2-D / `at` coordinates. +Discard the current figure. - +The next call to `gcf` (or to any drawing function) creates a +new empty figure. -##### Coordinate.to + + +##### from\_tikz\_code ```python -def to(target: "Node | Coordinate", - options: "OptionInput | None" = None, - **kwargs: Any) -> "NodePathBuilder" +def from_tikz_code(*args: Any, **kwargs: Any) -> TikzFigure ``` -Create a path builder for a segment from this coordinate to `target`. +Parse TikZ source into a new current figure. -The returned builder always starts with `self` and records the segment options -for the edge from `self` to `target`. Chain additional `.to(...)` or `.arc(...)` -calls on the builder to add more path segments. Only `tikzfigure.core.node.Node` -and `Coordinate` targets are accepted by `.to(...)`. +See `TikzFigure.from_tikz_code`. The parsed figure becomes the +current one, so subsequent module-level calls draw on top of it. - + -##### Coordinate.ndim +##### from\_dict ```python -@property -def ndim() -> int +def from_dict(*args: Any, **kwargs: Any) -> TikzFigure ``` -Number of spatial dimensions (`2` or `3`). +Rebuild a figure from a dict and make it current. - +See `TikzFigure.from_dict`. -##### Coordinate.at + + +##### copy ```python -@property -def at() -> str | None +def copy(*args: Any, **kwargs: Any) -> TikzFigure ``` -Raw TikZ coordinate expression, or `None` if using x/y. +Copy the current figure. See `TikzFigure.copy`. - +The copy is *not* made current; pass it to `scf` if you want that. -##### Coordinate.to\_tikz + + +##### to\_dict ```python -def to_tikz(output_unit: str | None = None) -> str +def to_dict(*args: Any, **kwargs: Any) -> dict[str, Any] ``` -Generate the TikZ `\coordinate` declaration. - -**Returns**: - -A `\coordinate (label) at (...);` string ending with a newline, optionally -preceded by a comment line. +Serialize the current figure to a dict. See `TikzFigure.to_dict`. - + -##### Coordinate.to\_dict +##### to\_python ```python -def to_dict() -> dict[str, Any] +def to_python(*args: Any, **kwargs: Any) -> str ``` -Serialize this coordinate to a plain dictionary. +Emit Python that rebuilds the current figure. See `TikzFigure.to_python`. -**Returns**: + -A dictionary with `type`, `label`, `x`, `y`, `at`, `layer`, and `comment` keys. +##### generate\_subfigures - +Combine several figures side by side. See `TikzFigure.generate_subfigures`. +This one is a static helper: it takes the figures explicitly rather than +operating on the current figure. -##### Coordinate.from\_dict + + +##### draw ```python -@classmethod -def from_dict(cls, d: dict[str, Any]) -> "Coordinate" +def draw(*args: Any, **kwargs: Any) -> TikzPath ``` -Reconstruct a Coordinate from a dictionary. +Draw a path on the current figure. See `TikzFigure.draw`. -**Arguments**: + -- `d` - Dictionary as produced by `to_dict`. +##### filldraw -**Returns**: +```python +def filldraw(*args: Any, **kwargs: Any) -> TikzPath +``` -A new `Coordinate` instance. +Fill and draw a path on the current figure. See `TikzFigure.filldraw`. - + -### TikzCoordinate +##### fill ```python -class TikzCoordinate(TikzObject) +def fill(*args: Any, **kwargs: Any) -> TikzPath ``` -A bare coordinate point (no drawn node) in a TikZ figure. +Fill a path on the current figure. See `TikzFigure.fill`. -Used to define path waypoints without producing visible nodes. + -**Attributes**: +##### clip -- `x` - X-coordinate value (numeric or PGF expression string). -- `y` - Y-coordinate value (numeric or PGF expression string). -- `z` - Z-coordinate value, or `None` for 2-D figures. -- `ndim` - Number of dimensions (`2` or `3`). -- `coordinate` - Coordinate as a tuple ready for TikZ output. +```python +def clip(*args: Any, **kwargs: Any) -> TikzPath +``` - +Clip against a path on the current figure. See `TikzFigure.clip`. -#### TikzCoordinate.\_\_init\_\_ + + +##### path ```python -def __init__(x: CoordinateValue | CoordinateTuple2D | CoordinateTuple3D - | "TikzCoordinate", - y: CoordinateValue | None = None, - z: CoordinateValue | None = None, - layer: int = 0) -> None +def path(*args: Any, **kwargs: Any) -> TikzPath ``` -Initialize a TikzCoordinate. - -**Arguments**: - -- `x` - X-coordinate value, a `(x, y)` / `(x, y, z)` tuple, or an existing - `TikzCoordinate`. -- `y` - Y-coordinate value when passing explicit scalar coordinates. -- `z` - Z-coordinate value when passing explicit scalar coordinates. When - provided, the coordinate is treated as 3-D. Defaults to `None` (2-D). -- `layer` - Layer index this coordinate belongs to. Defaults to `0`. +Add a plain path to the current figure. See `TikzFigure.path`. - + -##### TikzCoordinate.x +##### add ```python -@property -def x() -> CoordinateValue +def add(*args: Any, **kwargs: Any) -> None ``` -X-coordinate value (numeric or PGF expression string). +Add existing items to the current figure. See `TikzFigure.add`. - + -##### TikzCoordinate.y +##### add\_copy ```python -@property -def y() -> CoordinateValue +def add_copy(*args: Any, **kwargs: Any) -> Node ``` -Y-coordinate value (numeric or PGF expression string). +Add a modified copy of a node. See `TikzFigure.add_copy`. - + -##### TikzCoordinate.z +##### add\_node ```python -@property -def z() -> CoordinateValue | None +def add_node(*args: Any, **kwargs: Any) -> Node ``` -Z-coordinate value (numeric or PGF expression string), or `None` for 2-D -coordinates. +Add a node to the current figure. See `TikzFigure.add_node`. - + -##### TikzCoordinate.coordinate +##### add\_coordinate ```python -@property -def coordinate() -> tuple[CoordinateValue, ...] +def add_coordinate(*args: Any, **kwargs: Any) -> Coordinate ``` -Coordinate as a tuple, either `(x, y)` or `(x, y, z)`. +Add a coordinate to the current figure. See `TikzFigure.add_coordinate`. - + -##### TikzCoordinate.ndim +##### midpoint ```python -@property -def ndim() -> int +def midpoint(*args: Any, **kwargs: Any) -> Node ``` -Number of spatial dimensions (`2` or `3`). +Add a midpoint node to the current figure. See `TikzFigure.midpoint`. - + -##### TikzCoordinate.to\_vector +##### arc ```python -def to_vector() -> TikzVector +def arc(*args: Any, **kwargs: Any) -> Arc ``` -Interpret this point as a vector from the origin. +Add an arc to the current figure. See `TikzFigure.arc`. - + -##### TikzCoordinate.vector\_to +##### circle ```python -def vector_to(other: TikzCoordinate) -> TikzVector +def circle(*args: Any, **kwargs: Any) -> Circle ``` -Return the vector from this point to another point. +Add a circle to the current figure. See `TikzFigure.circle`. - + -##### TikzCoordinate.distance\_to +##### ellipse ```python -def distance_to(other: TikzCoordinate) -> float +def ellipse(*args: Any, **kwargs: Any) -> Ellipse ``` -Return the Euclidean distance to another point. +Add an ellipse to the current figure. See `TikzFigure.ellipse`. - + -##### TikzCoordinate.translate +##### grid ```python -def translate(vector: TikzVector) -> TikzCoordinate +def grid(*args: Any, **kwargs: Any) -> Grid ``` -Return the translated point. +Add a grid to the current figure. See `TikzFigure.grid`. - + -##### TikzCoordinate.to\_tikz +##### line ```python -def to_tikz(output_unit: str | None = None) -> str +def line(*args: Any, **kwargs: Any) -> Line ``` -Render this coordinate as a TikZ position expression. +Add a line to the current figure. See `TikzFigure.line`. - + -##### TikzCoordinate.to\_dict +##### parabola ```python -def to_dict() -> dict[str, Any] +def parabola(*args: Any, **kwargs: Any) -> Parabola +``` + +Add a parabola to the current figure. See `TikzFigure.parabola`. + + + +##### polygon + +```python +def polygon(*args: Any, **kwargs: Any) -> Polygon +``` + +Add a polygon to the current figure. See `TikzFigure.polygon`. + + + +##### rectangle + +```python +def rectangle(*args: Any, **kwargs: Any) -> Rectangle +``` + +Add a rectangle to the current figure. See `TikzFigure.rectangle`. + + + +##### square + +```python +def square(*args: Any, **kwargs: Any) -> Square +``` + +Add a square to the current figure. See `TikzFigure.square`. + + + +##### triangle + +```python +def triangle(*args: Any, **kwargs: Any) -> Triangle +``` + +Add a triangle to the current figure. See `TikzFigure.triangle`. + + + +##### add\_plot + +```python +def add_plot(*args: Any, **kwargs: Any) -> TikzPlot +``` + +Add a plot to the current figure. See `TikzFigure.add_plot`. + + + +##### add\_parametric\_grid + +```python +def add_parametric_grid(*args: Any, **kwargs: Any) -> tuple[Loop, Loop] +``` + +Add a parametric grid. See `TikzFigure.add_parametric_grid`. + + + +##### axis2d + +```python +def axis2d(*args: Any, **kwargs: Any) -> Axis2D +``` + +Add a pgfplots axis to the current figure. See `TikzFigure.axis2d`. + + + +##### plot3d + +```python +def plot3d(*args: Any, **kwargs: Any) -> Plot3D +``` + +Add a 3-D plot to the current figure. See `TikzFigure.plot3d`. + + + +##### add\_gantt\_chart + +```python +def add_gantt_chart(*args: Any, **kwargs: Any) -> GanttChart +``` + +Add a Gantt chart to the current figure. See `TikzFigure.add_gantt_chart`. + + + +##### add\_loop + +```python +def add_loop(*args: Any, **kwargs: Any) -> Loop +``` + +Add a ``\foreach`` loop to the current figure. See `TikzFigure.add_loop`. + + + +##### add\_scope + +```python +def add_scope(*args: Any, **kwargs: Any) -> Scope +``` + +Add a scope to the current figure. See `TikzFigure.add_scope`. + + + +##### add\_matrix + +```python +def add_matrix(*args: Any, **kwargs: Any) -> Matrix +``` + +Add a matrix to the current figure. See `TikzFigure.add_matrix`. + + + +##### add\_fit + +```python +def add_fit(*args: Any, **kwargs: Any) -> Fit +``` + +Add a fit node to the current figure. See `TikzFigure.add_fit`. + + + +##### add\_spy + +```python +def add_spy(*args: Any, **kwargs: Any) -> Spy +``` + +Add a spy to the current figure. See `TikzFigure.add_spy`. + + + +##### add\_spy\_scope + +```python +def add_spy_scope(*args: Any, **kwargs: Any) -> Scope +``` + +Add a spy scope to the current figure. See `TikzFigure.add_spy_scope`. + + + +##### configure\_spy\_scope + +```python +def configure_spy_scope(*args: Any, **kwargs: Any) -> None +``` + +Configure the figure-level spy scope. See `TikzFigure.configure_spy_scope`. + + + +##### add\_raw + +```python +def add_raw(*args: Any, **kwargs: Any) -> RawTikz +``` + +Add raw TikZ code to the current figure. See `TikzFigure.add_raw`. + + + +##### usetikzlibrary + +```python +def usetikzlibrary(*args: Any, **kwargs: Any) -> None +``` + +Load TikZ libraries in the current figure. See `TikzFigure.usetikzlibrary`. + + + +##### add\_package + +```python +def add_package(*args: Any, **kwargs: Any) -> None +``` + +Add a LaTeX package to the current figure. See `TikzFigure.add_package`. + + + +##### add\_style + +```python +def add_style(*args: Any, **kwargs: Any) -> TikzStyle +``` + +Define a named style on the current figure. See `TikzFigure.add_style`. + + + +##### colorlet + +```python +def colorlet(*args: Any, **kwargs: Any) -> Color +``` + +Define a color on the current figure. See `TikzFigure.colorlet`. + + + +##### add\_variable + +```python +def add_variable(*args: Any, **kwargs: Any) -> Variable +``` + +Define a variable on the current figure. See `TikzFigure.add_variable`. + + + +##### declare\_function + +```python +def declare_function(*args: Any, **kwargs: Any) -> DeclaredFunction +``` + +Declare a function on the current figure. See `TikzFigure.declare_function`. + + + +##### generate\_tikz + +```python +def generate_tikz(*args: Any, **kwargs: Any) -> str +``` + +Return the TikZ code of the current figure. See `TikzFigure.generate_tikz`. + + + +##### generate\_standalone + +```python +def generate_standalone(*args: Any, **kwargs: Any) -> str +``` + +Return a standalone document. See `TikzFigure.generate_standalone`. + + + +##### files + +```python +def files() -> dict[str, bytes] +``` + +The image files of the current figure. See `TikzFigure.files`. + + + +##### compile\_pdf + +```python +def compile_pdf(*args: Any, **kwargs: Any) -> None +``` + +Compile the current figure to PDF. See `TikzFigure.compile_pdf`. + + + +##### savefig + +```python +def savefig(*args: Any, **kwargs: Any) -> None +``` + +Save the current figure to a file. See `TikzFigure.savefig`. + + + +##### show + +```python +def show(*args: Any, **kwargs: Any) -> None +``` + +Display the current figure. See `TikzFigure.show`. + + + +##### add\_subfigure + +```python +def add_subfigure(*args: Any, **kwargs: Any) -> TikzFigure +``` + +Add a subfigure to the current figure. See `TikzFigure.add_subfigure`. + + + +##### subfigure\_axis + +```python +def subfigure_axis(*args: Any, **kwargs: Any) -> Axis2D +``` + +Add a subfigure axis. See `TikzFigure.subfigure_axis`. + +## Coordinates and vectors + +```python +from tikzfigure import TikzCoordinate, TikzVector +``` + + + +### Coordinate + +```python +class Coordinate(TikzObject) +``` + +A named TikZ coordinate declared with ``\coordinate``. + +Unlike a `tikzfigure.core.node.Node`, a coordinate has no +visible output — it simply registers a named point in the figure that +can be referenced in paths, calc expressions, or anywhere TikZ accepts a +node label. + +Common uses: + +* Defining fixed reference points:: + +fig.add_coordinate("origin", x=0, y=0) +fig.draw(["origin", "A"]) + +* Placing a point at the midpoint between two nodes (requires the +``calc`` TikZ library):: + +fig.add_coordinate("mid", at="$(A)!0.5!(B)$") +fig.draw(["A", "mid", "B"]) + +* Naming a point at a node anchor:: + +fig.add_coordinate("atip", at="A.north") + +**Attributes**: + +- `label` - The TikZ name for this coordinate. +- `x` - X value when using absolute positioning, or ``None``. +- `y` - Y value when using absolute positioning, or ``None``. +- `at` - Raw TikZ coordinate expression when using expression + positioning, or ``None``. + + + +##### Coordinate.\_\_init\_\_ + +```python +def __init__(label: str, + x: (CoordinateValue + | CoordinateTuple2D + | CoordinateTuple3D + | TikzCoordinate + | None) = None, + y: CoordinateValue | None = None, + z: CoordinateValue | None = None, + at: str | None = None, + layer: int = 0, + comment: str | None = None) -> None +``` + +Initialize a Coordinate. + +Provide either explicit coordinates, or a raw TikZ coordinate +expression via ``at``. + +**Arguments**: + +- `label` - TikZ name for this coordinate, used when referencing it + in path lists and other commands. +- `x` - X-coordinate value, a ``(x, y)`` / ``(x, y, z)`` tuple, or a + `TikzCoordinate`. Must be provided together with ``y`` + when passed as a scalar. Mutually exclusive with ``at``. +- `y` - Y-coordinate value when passing explicit scalar coordinates. + Mutually exclusive with ``at``. +- `z` - Z-coordinate value when passing explicit scalar coordinates. + Mutually exclusive with ``at``. +- `at` - Raw TikZ coordinate expression. Examples: + + * ``"$(A)!0.5!(B)$"`` — midpoint via ``calc`` library + * ``"A.north"`` — at a node anchor + * ``"30:2cm"`` — polar coordinates + + If the string does not begin with ``(``, parentheses are + added automatically. Mutually exclusive with ``x``/``y``. +- `layer` - Layer index this coordinate belongs to. Defaults to ``0``. +- `comment` - Optional comment prepended in the TikZ output. + + +**Raises**: + +- `ValueError` - If both ``at`` and explicit coordinates are supplied, + or if neither is supplied. + + + +##### Coordinate.x + +```python +@property +def x() -> CoordinateValue | None +``` + +X-coordinate value, or ``None`` if using an ``at`` expression. + + + +##### Coordinate.y + +```python +@property +def y() -> CoordinateValue | None +``` + +Y-coordinate value, or ``None`` if using an ``at`` expression. + + + +##### Coordinate.z + +```python +@property +def z() -> CoordinateValue | None +``` + +Z-coordinate value, or ``None`` for 2-D / ``at`` coordinates. + + + +##### Coordinate.to + +```python +def to(target: Node | Coordinate, + options: OptionInput | None = None, + **kwargs: Any) -> NodePathBuilder +``` + +Create a path builder for a segment from this coordinate to ``target``. + +The returned builder always starts with ``self`` and records the segment +options for the edge from ``self`` to ``target``. Chain additional +``.to(...)`` or ``.arc(...)`` calls on the builder to add more path +segments. Only `tikzfigure.core.node.Node` and +`Coordinate` targets are accepted by ``.to(...)``. + + + +##### Coordinate.ndim + +```python +@property +def ndim() -> int +``` + +Number of spatial dimensions (``2`` or ``3``). + + + +##### Coordinate.at + +```python +@property +def at() -> str | None +``` + +Raw TikZ coordinate expression, or ``None`` if using x/y. + + + +##### Coordinate.to\_tikz + +```python +def to_tikz(output_unit: str | None = None) -> str +``` + +Generate the TikZ ``\coordinate`` declaration. + +**Returns**: + + A ``\coordinate (label) at (...);`` string ending with a + newline, optionally preceded by a comment line. + + + +##### Coordinate.to\_dict + +```python +def to_dict() -> dict[str, Any] +``` + +Serialize this coordinate to a plain dictionary. + +**Returns**: + + A dictionary with ``type``, ``label``, ``x``, ``y``, ``at``, + ``layer``, and ``comment`` keys. + + + +##### Coordinate.from\_dict + +```python +@classmethod +def from_dict(cls, d: dict[str, Any]) -> Coordinate +``` + +Reconstruct a Coordinate from a dictionary. + +**Arguments**: + +- `d` - Dictionary as produced by `to_dict`. + + +**Returns**: + + A new `Coordinate` instance. + + + +### TikzCoordinate + +```python +class TikzCoordinate(TikzObject) +``` + +A bare coordinate point (no drawn node) in a TikZ figure. + +Used to define path waypoints without producing visible nodes. + +**Attributes**: + +- `x` - X-coordinate value (numeric or PGF expression string). +- `y` - Y-coordinate value (numeric or PGF expression string). +- `z` - Z-coordinate value, or ``None`` for 2-D figures. +- `ndim` - Number of dimensions (``2`` or ``3``). +- `coordinate` - Coordinate as a tuple ready for TikZ output. + + + +##### TikzCoordinate.\_\_init\_\_ + +```python +def __init__(x: CoordinateValue | CoordinateTuple2D | CoordinateTuple3D + | TikzCoordinate, + y: CoordinateValue | None = None, + z: CoordinateValue | None = None, + layer: int = 0) -> None +``` + +Initialize a TikzCoordinate. + +**Arguments**: + +- `x` - X-coordinate value, a ``(x, y)`` / ``(x, y, z)`` tuple, or an + existing `TikzCoordinate`. +- `y` - Y-coordinate value when passing explicit scalar coordinates. +- `z` - Z-coordinate value when passing explicit scalar coordinates. + When provided, the coordinate is treated as 3-D. Defaults to + ``None`` (2-D). +- `layer` - Layer index this coordinate belongs to. Defaults to ``0``. + + + +##### TikzCoordinate.x + +```python +@property +def x() -> CoordinateValue +``` + +X-coordinate value (numeric or PGF expression string). + + + +##### TikzCoordinate.y + +```python +@property +def y() -> CoordinateValue +``` + +Y-coordinate value (numeric or PGF expression string). + + + +##### TikzCoordinate.z + +```python +@property +def z() -> CoordinateValue | None +``` + +Z-coordinate value (numeric or PGF expression string), or ``None`` for 2-D coordinates. + + + +##### TikzCoordinate.coordinate + +```python +@property +def coordinate() -> tuple[CoordinateValue, ...] +``` + +Coordinate as a tuple, either ``(x, y)`` or ``(x, y, z)``. + + + +##### TikzCoordinate.ndim + +```python +@property +def ndim() -> int +``` + +Number of spatial dimensions (``2`` or ``3``). + + + +##### TikzCoordinate.to\_vector + +```python +def to_vector() -> TikzVector +``` + +Interpret this point as a vector from the origin. + + + +##### TikzCoordinate.vector\_to + +```python +def vector_to(other: TikzCoordinate) -> TikzVector +``` + +Return the vector from this point to another point. + + + +##### TikzCoordinate.distance\_to + +```python +def distance_to(other: TikzCoordinate) -> float +``` + +Return the Euclidean distance to another point. + + + +##### TikzCoordinate.translate + +```python +def translate(vector: TikzVector) -> TikzCoordinate +``` + +Return the translated point. + + + +##### TikzCoordinate.to\_tikz + +```python +def to_tikz(output_unit: str | None = None) -> str +``` + +Render this coordinate as a TikZ position expression. + + + +##### TikzCoordinate.to\_dict + +```python +def to_dict() -> dict[str, Any] ``` Serialize this coordinate to a plain dictionary. **Returns**: -A dictionary with `type`, `x`, `y`, `z`, and `layer` keys. + A dictionary with ``type``, ``x``, ``y``, ``z``, and ``layer`` + keys. @@ -2423,7 +3367,7 @@ A dictionary with `type`, `x`, `y`, `z`, and `layer` keys. ```python @classmethod -def from_dict(cls, d: dict[str, Any]) -> "TikzCoordinate" +def from_dict(cls, d: dict[str, Any]) -> TikzCoordinate ``` Reconstruct a TikzCoordinate from a dictionary. @@ -2432,9 +3376,10 @@ Reconstruct a TikzCoordinate from a dictionary. - `d` - Dictionary as produced by `to_dict`. + **Returns**: -A new `TikzCoordinate` instance. + A new `TikzCoordinate` instance. @@ -2448,7 +3393,7 @@ A geometric vector used for linear algebra operations. -#### TikzVector.dot +##### TikzVector.dot ```python def dot(other: TikzVector) -> float @@ -2502,7 +3447,7 @@ Serialize this vector to a plain dictionary. ```python @classmethod -def from_dict(cls, d: dict[str, Any]) -> "TikzVector" +def from_dict(cls, d: dict[str, Any]) -> TikzVector ``` Reconstruct a TikzVector from a dictionary. @@ -2527,21 +3472,21 @@ An xcolor-compatible TikZ color expression. -#### TikzColor.mix +##### TikzColor.mix ```python -def mix(other: str | "TikzColor" | None = None, - percent: float | int = 0.5) -> "TikzColor" +def mix(other: str | TikzColor | None = None, + percent: float = 0.5) -> TikzColor ``` -Build an xcolor mix like `red!50` or `red!50!blue`. +Build an xcolor mix like ``red!50`` or ``red!50!blue``. ##### TikzColor.complement ```python -def complement() -> "TikzColor" +def complement() -> TikzColor ``` Return the xcolor complementary-expression form. @@ -2574,10 +3519,10 @@ A TikZ dimension value with a unit, e.g. 2.5cm or 10pt. -#### TikzDimension.to +##### TikzDimension.to ```python -def to(target_unit: str) -> "TikzDimension" +def to(target_unit: str) -> TikzDimension ``` Convert to another TikZ unit. @@ -2593,21 +3538,11 @@ from tikzfigure import shapes ### TikzShape ```python -class TikzShape() +class TikzShape(TikzToken) ``` A reusable TikZ node-shape specification. - - -#### TikzShape.to\_tikz - -```python -def to_tikz() -> str -``` - -Return the raw TikZ shape specification. - ##### shape @@ -2629,21 +3564,11 @@ from tikzfigure import styles ### TikzStyle ```python -class TikzStyle() +class TikzStyle(TikzToken) ``` A reusable TikZ style token or option fragment. - - -#### TikzStyle.to\_tikz - -```python -def to_tikz() -> str -``` - -Return the raw TikZ style fragment. - ##### style @@ -2659,31 +3584,31 @@ Create a custom raw TikZ style token. ##### line\_width ```python -def line_width(value: str | int | float | TikzDimension) -> TikzStyle +def line_width(value: str | float | TikzDimension) -> TikzStyle ``` -Build a `line width=...` option fragment. +Build a ``line width=...`` option fragment. ##### rounded\_corners ```python -def rounded_corners(radius: str | int | float | TikzDimension) -> TikzStyle +def rounded_corners(radius: str | float | TikzDimension) -> TikzStyle ``` -Build a `rounded corners=...` option fragment. +Build a ``rounded corners=...`` option fragment. ##### dash\_pattern ```python -def dash_pattern(on: str | int | float | TikzDimension, - off: str | int | float | TikzDimension) -> TikzStyle +def dash_pattern(on: str | float | TikzDimension, + off: str | float | TikzDimension) -> TikzStyle ``` -Build a simple `dash pattern=on ... off ...` option fragment. +Build a simple ``dash pattern=on ... off ...`` option fragment. @@ -2693,7 +3618,7 @@ Build a simple `dash pattern=on ... off ...` option fragment. def bend_left(angle: float | None = None) -> TikzStyle ``` -Build a `bend left` option fragment. +Build a ``bend left`` option fragment. @@ -2703,7 +3628,7 @@ Build a `bend left` option fragment. def bend_right(angle: float | None = None) -> TikzStyle ``` -Build a `bend right` option fragment. +Build a ``bend right`` option fragment. ## Arrows @@ -2716,21 +3641,11 @@ from tikzfigure import arrows ### TikzArrow ```python -class TikzArrow() +class TikzArrow(TikzToken) ``` A reusable TikZ arrow-tip specification. - - -#### TikzArrow.to\_tikz - -```python -def to_tikz() -> str -``` - -Return the raw TikZ arrow specification. - ##### tip @@ -2752,21 +3667,11 @@ from tikzfigure import patterns ### TikzPattern ```python -class TikzPattern() +class TikzPattern(TikzToken) ``` A reusable TikZ fill-pattern specification. - - -#### TikzPattern.to\_tikz - -```python -def to_tikz() -> str -``` - -Return the raw TikZ pattern specification. - ##### pattern @@ -2788,21 +3693,11 @@ from tikzfigure import decorations ### TikzDecoration ```python -class TikzDecoration() +class TikzDecoration(TikzToken) ``` A reusable TikZ path-decoration specification. - - -#### TikzDecoration.to\_tikz - -```python -def to_tikz() -> str -``` - -Return the raw TikZ decoration specification. - ##### decoration @@ -2824,21 +3719,11 @@ from tikzfigure import marks ### TikzMark ```python -class TikzMark() +class TikzMark(TikzToken) ``` A reusable pgfplots plot-mark specification. - - -#### TikzMark.to\_tikz - -```python -def to_tikz() -> str -``` - -Return the raw pgfplots mark specification. - ##### mark @@ -2857,7 +3742,7 @@ from tikzfigure import options -### normalize\_options +##### normalize\_options ```python def normalize_options(options: OptionInput | None) -> list[OptionValue] diff --git a/astro_docs/src/content/docs/index.mdx b/astro_docs/src/content/docs/index.mdx index ab48e1e..6bdd303 100644 --- a/astro_docs/src/content/docs/index.mdx +++ b/astro_docs/src/content/docs/index.mdx @@ -13,7 +13,7 @@ hero: variant: minimal --- -[Check out the first tutorial →](/tikzfigure/tutorials/tutorial_01_getting_started/) +[Check out the first tutorial →](/tikzfigure/tutorials/tutorial-01-getting-started/) --- diff --git a/astro_docs/src/styles/custom.css b/astro_docs/src/styles/custom.css deleted file mode 100644 index 073fdaa..0000000 --- a/astro_docs/src/styles/custom.css +++ /dev/null @@ -1,4 +0,0 @@ -/* Expand the nav height to include the secondary tab bar (2.25rem) */ -:root { - --sl-nav-height: 5.75rem; -} diff --git a/pyproject.toml b/pyproject.toml index a6aec16..6185cb3 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -5,7 +5,7 @@ requires = [ "setuptools", "wheel" ] [project] name = "tikzfigure" -version = "0.4.0" +version = "0.5.0" description = "Python interface to generate (readable) Tikz figures." readme = "README.md" keywords = [ "python" ] diff --git a/src/tikzfigure/__init__.py b/src/tikzfigure/__init__.py index e5e40d7..86a5e09 100644 --- a/src/tikzfigure/__init__.py +++ b/src/tikzfigure/__init__.py @@ -15,7 +15,7 @@ styles, units, ) -from .pyplot import * # noqa: F403 +from .pyplot import * from .pyplot import __all__ as _pyplot_all __all__ = [ diff --git a/src/tikzfigure/core/loop.py b/src/tikzfigure/core/loop.py index 343a12e..7494b6c 100644 --- a/src/tikzfigure/core/loop.py +++ b/src/tikzfigure/core/loop.py @@ -1,6 +1,6 @@ import types -from collections.abc import Sequence -from typing import Any, Callable +from collections.abc import Callable, Sequence +from typing import Any from tikzfigure.core.base import TikzObject from tikzfigure.core.coordinate import Coordinate diff --git a/src/tikzfigure/core/matrix.py b/src/tikzfigure/core/matrix.py index 6d36db0..87b70ec 100644 --- a/src/tikzfigure/core/matrix.py +++ b/src/tikzfigure/core/matrix.py @@ -12,6 +12,7 @@ from tikzfigure.options import OptionInput, normalize_options MatrixCell = str | dict[str, Any] | None +CellReference = tuple[int, int] | str class MatrixLibrary(TikzLibrary): @@ -48,6 +49,19 @@ class Matrix(TikzObject): fig.add_coordinate("c22", at=m.cell(2, 2)) fig.draw(["c11", "c22"]) + :meth:`arrow` connects cells directly, with an optional label, which + makes a matrix of math nodes a commutative diagram:: + + m = fig.add_matrix( + [["V^0", "V^1"], ["H^1", "L^2"]], + label="d", + options="matrix of math nodes", + column_sep="1.5cm", + row_sep="1cm", + ) + m.arrow((1, 1), (1, 2), label=r"$\\nabla$") + m.arrow((2, 1), (1, 1), label=r"$\\pi^0$", label_position="left") + Attributes: rows: The grid of cells, as a list of rows of :data:`MatrixCell`. row_sep: Row separation TikZ value, or ``None``. @@ -119,6 +133,7 @@ def __init__( self._row_sep = row_sep self._column_sep = column_sep self._cell_style = cell_style + self._arrows: list[dict[str, Any]] = [] tikz_kwargs = dict(kwargs) if row_sep is not None: @@ -229,6 +244,89 @@ def cell(self, row: int, col: int) -> str: ) return MatrixLibrary.cell_name(self.label, row, col) + @property + def arrows(self) -> list[dict[str, Any]]: + """The arrows between cells added with :meth:`arrow`, in order.""" + return [dict(arrow) for arrow in self._arrows] + + def arrow( + self, + source: CellReference, + target: CellReference, + label: str | None = None, + label_position: str = "above", + label_options: OptionInput | None = None, + arrows: str = "->", + options: OptionInput | None = None, + **kwargs: Any, + ) -> "Matrix": + """Draw an arrow between two cells, e.g. a map of a commutative diagram. + + The arrow runs from the border of the source cell to the border of + the target cell (``\\draw[->] (m-1-1) to node[above] {f} (m-1-2);``), + and is drawn after the matrix. + + Args: + source: The start cell, as a 1-indexed ``(row, col)`` tuple of + this matrix, or any TikZ node name (e.g. a cell of another + matrix, from its :meth:`cell`, or ``"m-1-1.east"``). + target: The end cell, given like ``source``. + label: Content of a node placed midway along the arrow (TikZ + text, so math needs ``$...$``). Default: no label. + label_position: Where the label sits relative to the arrow + (``"above"``, ``"below"``, ``"left"``, ``"right"``, + ``"above left"``, ...). Defaults to ``"above"``. + label_options: Further TikZ options of the label node, e.g. + ``"font=\\scriptsize"`` or ``["sloped", "pos=0.3"]``. + arrows: The arrow specification. Defaults to ``"->"``. + options: Further flag-style TikZ options of the arrow, e.g. + ``"dashed"`` or ``"bend left=20"``. + **kwargs: Further TikZ options of the arrow; underscores in keys + become spaces (e.g. ``color="blue"``, ``line_width="1pt"``). + + Returns: + This matrix, so arrows can be chained. + + Raises: + ValueError: If a ``(row, col)`` cell is out of range, or this + matrix has no label. + """ + for end in (source, target): + if not isinstance(end, str): + self.cell(*end) # validates the label and the range + self._arrows.append( + { + "source": source if isinstance(source, str) else tuple(source), + "target": target if isinstance(target, str) else tuple(target), + "label": label, + "label_position": label_position, + "label_options": [str(o) for o in normalize_options(label_options)], + "arrows": arrows, + "options": [str(o) for o in normalize_options(options)], + "kwargs": dict(kwargs), + } + ) + return self + + def _reference(self, end: CellReference) -> str: + """The TikZ node name of an arrow end.""" + return end if isinstance(end, str) else self.cell(*end) + + def _arrow_to_tikz(self, arrow: dict[str, Any]) -> str: + """Render one arrow added with :meth:`arrow` as a ``\\draw`` command.""" + draw_options = [arrow["arrows"], *arrow["options"]] + draw_options += [ + f"{k.replace('_', ' ')}={v}" for k, v in arrow["kwargs"].items() + ] + node = "" + if arrow["label"] is not None: + node_options = ", ".join([arrow["label_position"], *arrow["label_options"]]) + node = f" node[{node_options}] {{{arrow['label']}}}" + return ( + f"\\draw[{', '.join(draw_options)}] ({self._reference(arrow['source'])}) to" + f"{node} ({self._reference(arrow['target'])});\n" + ) + @staticmethod def _cell_to_tikz(cell: MatrixCell) -> str: """Render a single cell to its TikZ representation.""" @@ -275,6 +373,7 @@ def to_tikz(self, output_unit: str | None = None) -> str: body = "\n ".join(row_strings) matrix_string = f"\\matrix{options_str}{label_str}{at_str} {{\n {body}\n}};\n" + matrix_string += "".join(self._arrow_to_tikz(arrow) for arrow in self._arrows) matrix_string = self.add_comment(matrix_string) return matrix_string @@ -297,6 +396,18 @@ def to_dict(self) -> dict[str, Any]: "row_sep": self._row_sep, "column_sep": self._column_sep, "cell_style": self._cell_style, + "arrows": [ + { + **arrow, + "source": list(arrow["source"]) + if isinstance(arrow["source"], tuple) + else arrow["source"], + "target": list(arrow["target"]) + if isinstance(arrow["target"], tuple) + else arrow["target"], + } + for arrow in self._arrows + ], } ) serialized = serialize_tikz_value(d) @@ -335,7 +446,7 @@ def from_dict(cls, d: dict[str, Any]) -> "Matrix": ) marker = f"nodes={{{style_str}}}" options = [o for o in options if o != marker] - return cls( + matrix = cls( rows=restored.get("rows", []), x=restored.get("x"), y=restored.get("y"), @@ -349,3 +460,19 @@ def from_dict(cls, d: dict[str, Any]) -> "Matrix": cell_style=restored.get("cell_style"), **kwargs, ) + for arrow in restored.get("arrows") or []: + source, target = ( + end if isinstance(end, str) else (int(end[0]), int(end[1])) + for end in (arrow["source"], arrow["target"]) + ) + matrix.arrow( + source, + target, + label=arrow.get("label"), + label_position=arrow.get("label_position", "above"), + label_options=arrow.get("label_options") or None, + arrows=arrow.get("arrows", "->"), + options=arrow.get("options") or None, + **(arrow.get("kwargs") or {}), + ) + return matrix diff --git a/src/tikzfigure/core/style_options.py b/src/tikzfigure/core/style_options.py index 9e11227..fb1a2d8 100644 --- a/src/tikzfigure/core/style_options.py +++ b/src/tikzfigure/core/style_options.py @@ -118,11 +118,7 @@ def collect_style( Returns: The style options to pass on to a :class:`~tikzfigure.core.base.TikzObject`. """ - style = { - name: scope[name] - for name in STYLE_FIELDS - if scope.get(name) is not None # noqa: SIM118 -- Mapping, not a dict - } + style = {name: scope[name] for name in STYLE_FIELDS if scope.get(name) is not None} if extra: style.update(extra) return style diff --git a/src/tikzfigure/core/web_compiler.py b/src/tikzfigure/core/web_compiler.py index 5c793fb..19b4251 100644 --- a/src/tikzfigure/core/web_compiler.py +++ b/src/tikzfigure/core/web_compiler.py @@ -4,14 +4,13 @@ import warnings from pathlib import Path -from typing import Union LATEX_ON_HTTP_URL = "https://latex.ytotech.com/builds/sync" def compile_with_latex_on_http( latex_content: str, - output_path: Union[Path, str], + output_path: Path | str, timeout: int = 30, verbose: bool = False, ) -> None: diff --git a/src/tikzfigure/parser/builder.py b/src/tikzfigure/parser/builder.py index 971df52..66906ab 100644 --- a/src/tikzfigure/parser/builder.py +++ b/src/tikzfigure/parser/builder.py @@ -14,8 +14,9 @@ import re from collections import Counter +from collections.abc import Callable from dataclasses import dataclass, field -from typing import TYPE_CHECKING, Any, Callable +from typing import TYPE_CHECKING, Any from tikzfigure.core.circle import Circle from tikzfigure.core.coordinate import Coordinate, TikzCoordinate diff --git a/src/tikzfigure/tests/test_matrix.py b/src/tikzfigure/tests/test_matrix.py index bae53f9..54e8436 100644 --- a/src/tikzfigure/tests/test_matrix.py +++ b/src/tikzfigure/tests/test_matrix.py @@ -105,3 +105,63 @@ def test_figure_add_matrix_second_auto_label_increments_counter(): fig.add_matrix([["A"]]) second = fig.add_matrix([["B"]]) assert second.label == "node1" + + +def test_matrix_arrows_connect_cells_with_labels(): + matrix = Matrix( + rows=[["V^0", "V^1"], ["H^1", "L^2"]], + label="d", + options="matrix of math nodes", + ) + matrix.arrow((1, 1), (1, 2), label=r"$\nabla$").arrow( + (2, 1), + (1, 1), + label=r"$\pi^0$", + label_position="left", + label_options="font=\\scriptsize", + options="dashed", + color="blue", + ) + tikz = matrix.to_tikz() + assert tikz.index("\\matrix") < tikz.index("\\draw") + assert "\\draw[->] (d-1-1) to node[above] {$\\nabla$} (d-1-2);" in tikz + assert ( + "\\draw[->, dashed, color=blue] (d-2-1) to " + "node[left, font=\\scriptsize] {$\\pi^0$} (d-1-1);" + ) in tikz + assert len(matrix.arrows) == 2 + + +def test_matrix_arrow_without_label_and_to_named_nodes(): + first = Matrix(rows=[["A", "B"]], label="m") + second = Matrix(rows=[["C"]], label="n") + first.arrow((1, 2), second.cell(1, 1), arrows="<-").arrow("m-1-1.east", (1, 2)) + tikz = first.to_tikz() + assert "\\draw[<-] (m-1-2) to (n-1-1);" in tikz + assert "\\draw[->] (m-1-1.east) to (m-1-2);" in tikz + + +def test_matrix_arrow_validates_cells(): + with pytest.raises(ValueError): + Matrix(rows=[["A", "B"]], label="m").arrow((1, 1), (2, 1)) + with pytest.raises(ValueError): + Matrix(rows=[["A", "B"]]).arrow((1, 1), (1, 2)) + + +def test_matrix_arrows_round_trip(): + matrix = Matrix(rows=[["A", "B"], ["C", "D"]], label="m") + matrix.arrow((1, 1), (1, 2), label="f", label_options=["sloped"], bend_left=20) + matrix.arrow((2, 1), "m-1-1", options="dashed") + restored = Matrix.from_dict(matrix.to_dict()) + assert restored.to_dict() == matrix.to_dict() + assert restored.to_tikz() == matrix.to_tikz() + assert "bend left=20" in restored.to_tikz() + + +def test_figure_matrix_arrows_render_in_the_document(): + fig = TikzFigure() + matrix = fig.add_matrix( + [["A", "B"]], label="m", options="matrix of math nodes", column_sep="1cm" + ) + matrix.arrow((1, 1), (1, 2), label="$f$") + assert "\\draw[->] (m-1-1) to node[above] {$f$} (m-1-2);" in fig.generate_tikz() diff --git a/tikz_catalogue/README.md b/tikz_catalogue/README.md index d4e7bc7..dc4d411 100644 --- a/tikz_catalogue/README.md +++ b/tikz_catalogue/README.md @@ -29,9 +29,15 @@ python -m tikzfigure.codegen tikz_catalogue/19_commutative_diagram.tex ```python fig = TikzFigure() -fig.add_node(0, 2, label='A', content='$A$') -fig.add_node(2.5, 2, label='B', content='$B$') -fig.draw(['A', 'B'], segment_options=[{'connector': '--', 'node': {'content': '$f$', 'options': ['above']}}], options=['->']) +fig.add_node(0, 2, label="A", content="$A$") +fig.add_node(2.5, 2, label="B", content="$B$") +fig.draw( + ["A", "B"], + segment_options=[ + {"connector": "--", "node": {"content": "$f$", "options": ["above"]}} + ], + options=["->"], +) ``` `figure.to_python()` runs the generated code and compares its TikZ output with diff --git a/tutorials/.gitignore b/tutorials/.gitignore index ad29309..87620ac 100644 --- a/tutorials/.gitignore +++ b/tutorials/.gitignore @@ -1,2 +1 @@ -/.quarto/ -**/*.quarto_ipynb +.ipynb_checkpoints/ diff --git a/tutorials/advanced/tutorial_09_raw_tikz.ipynb b/tutorials/advanced/tutorial_09_raw_tikz.ipynb new file mode 100644 index 0000000..d47dd1f --- /dev/null +++ b/tutorials/advanced/tutorial_09_raw_tikz.ipynb @@ -0,0 +1,593 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Raw TikZ\n", + "\n", + "tikzfigure covers the most common TikZ operations through its Python API, but\n", + "TikZ is a vast language. `fig.raw(string)` injects an arbitrary LaTeX/TikZ\n", + "snippet directly into the generated code - giving you a full escape hatch.\n", + "\n", + "A key pattern: create nodes with the Python API (to get auto-generated labels),\n", + "then reference them in raw TikZ commands via `{node.label}`.\n", + "\n", + "This tutorial covers: - basic raw insertion - flowcharts with decision diamonds\n", + "- mathematical annotations and axis drawing - network graphs with curved edges -\n", + "PDF animations - timeline visualizations" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "from tikzfigure import TikzFigure" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Basic raw insertion\n", + "\n", + "Pass any TikZ string to `raw`. Node labels created by `node` can be interpolated\n", + "into the string." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "n1 = fig.node(\n", + " (0, 0),\n", + " shape=\"circle\",\n", + " color=\"white\",\n", + " fill=\"blue\",\n", + " content=\"Hello!\",\n", + ")\n", + "n2 = fig.node(\n", + " (5, 0),\n", + " shape=\"circle\",\n", + " color=\"white\",\n", + " fill=\"red\",\n", + " content=\"Hi!\",\n", + ")\n", + "\n", + "# A raw comment\n", + "fig.raw(\"% --- raw TikZ below ---\")\n", + "\n", + "# A plain text node at a fixed coordinate\n", + "fig.raw(\n", + " \"\\\\node at (2.5, 1.2) {raw TikZ node};\",\n", + ")\n", + "\n", + "# A dashed arrow referencing the Python-created node labels\n", + "fig.raw(\n", + " f\"\\\\draw[->, thick, dashed] ({n2.label}) -- ({n1.label});\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Flowchart\n", + "\n", + "Flowcharts need `diamond` decision nodes and labelled arrows - both easy with\n", + "raw TikZ." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "fig.usetikzlibrary(\"shapes.geometric\")\n", + "\n", + "start = fig.node(\n", + " (2, 4),\n", + " shape=\"ellipse\",\n", + " fill=\"green!30\",\n", + " content=\"Start\",\n", + ")\n", + "process = fig.node(\n", + " (2, 2),\n", + " shape=\"rectangle\",\n", + " fill=\"blue!20\",\n", + " content=\"Process\",\n", + ")\n", + "decision = fig.node(\n", + " (2, 0),\n", + " shape=\"diamond\",\n", + " fill=\"orange!30\",\n", + " content=\"Decision?\",\n", + ")\n", + "yes_node = fig.node(\n", + " (0, -2),\n", + " shape=\"rectangle\",\n", + " fill=\"red!20\",\n", + " content=\"Action A\",\n", + ")\n", + "no_node = fig.node(\n", + " (4, -2),\n", + " shape=\"rectangle\",\n", + " fill=\"green!20\",\n", + " content=\"Action B\",\n", + ")\n", + "end = fig.node(\n", + " (2, -4),\n", + " shape=\"ellipse\",\n", + " fill=\"red!30\",\n", + " content=\"End\",\n", + ")\n", + "\n", + "fig.raw(\n", + " f\"\\\\draw[->, thick] ({start.label}) -- ({process.label});\",\n", + ")\n", + "fig.raw(\n", + " f\"\\\\draw[->, thick] ({process.label}) -- ({decision.label});\",\n", + ")\n", + "fig.raw(\n", + " f\"\\\\draw[->, thick] ({decision.label}) -- node[left] {{Yes}} ({yes_node.label});\",\n", + ")\n", + "fig.raw(\n", + " f\"\\\\draw[->, thick] ({decision.label}) -- node[right] {{No}} ({no_node.label});\",\n", + ")\n", + "fig.raw(\n", + " f\"\\\\draw[->, thick] ({yes_node.label}) -- ({end.label});\",\n", + ")\n", + "fig.raw(\n", + " f\"\\\\draw[->, thick] ({no_node.label}) -- ({end.label});\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Mathematical annotations and coordinate axes\n", + "\n", + "Raw TikZ handles axis arrows, tick marks, parabolas, and LaTeX math (`$...$`)\n", + "naturally." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "p1 = fig.node((0, 0), content=\"\")\n", + "p2 = fig.node((3, 2), content=\"\")\n", + "p3 = fig.node((6, 0), content=\"\")\n", + "\n", + "fig.raw(\n", + " f\"\\\\draw[thick, blue] ({p1.label}) parabola bend ({p2.label}) ({p3.label});\",\n", + ")\n", + "fig.raw(\n", + " f\"\\\\draw[->, dashed, red] ({p1.label}) -- ({p2.label}) node[midway, left=40pt] {{$\\\\Delta y$}};\",\n", + ")\n", + "fig.raw(\n", + " f\"\\\\draw[->, dashed, green!50!black] ({p1.label}) -- ({p3.label}) node[midway, below=10pt] {{$\\\\Delta x$}};\",\n", + ")\n", + "fig.raw(\n", + " \"\\\\node at (3, 3) {$y = ax^2 + bx + c$};\",\n", + ")\n", + "\n", + "fig.raw(\n", + " \"\\\\draw[->, thick] (-1, 0) -- (7, 0) node[right] {$x$};\",\n", + ")\n", + "fig.raw(\n", + " \"\\\\draw[->, thick] (0, -1) -- (0, 3.5) node[above] {$y$};\",\n", + ")\n", + "\n", + "for i in range(1, 7):\n", + " fig.raw(\n", + " f\"\\\\draw ({i}, -0.1) -- ({i}, 0.1) node[below=3pt] {{\\\\small {i}}};\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Network graph with curved edges\n", + "\n", + "Nodes placed in a ring with `bend left` edges and some cross-connections." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "n = 6\n", + "R = 3\n", + "nodes = []\n", + "for i in range(n):\n", + " angle = 360 / n * i\n", + " x = R * math.cos(math.radians(angle))\n", + " y = R * math.sin(math.radians(angle))\n", + " nodes.append(\n", + " fig.node(\n", + " x,\n", + " y,\n", + " shape=\"circle\",\n", + " fill=\"cyan!40\",\n", + " content=f\"N{i}\",\n", + " ),\n", + " )\n", + "\n", + "# Ring\n", + "for i in range(n):\n", + " j = (i + 1) % n\n", + " fig.raw(\n", + " f\"\\\\draw[->, thick, bend left=15] ({nodes[i].label}) edge ({nodes[j].label});\",\n", + " )\n", + "\n", + "# Cross-connections\n", + "fig.raw(\n", + " f\"\\\\draw[->, red, thick, dashed, bend right=30] ({nodes[0].label}) edge ({nodes[3].label});\",\n", + ")\n", + "fig.raw(\n", + " f\"\\\\draw[->, blue, thick, dotted, bend left=30] ({nodes[1].label}) edge ({nodes[4].label});\",\n", + ")\n", + "fig.raw(\n", + " f\"\\\\draw[->, green!60!black, thick, bend right=30] ({nodes[2].label}) edge ({nodes[5].label});\",\n", + ")\n", + "\n", + "fig.raw(\n", + " \"\\\\node[font=\\\\Large\\\\bfseries] at (0, 4.5) {Network Graph};\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## PDF animations (`animations` library)\n", + "\n", + "The `animations` library drives per-attribute animations (color changes,\n", + "motion along a path, ...) that get embedded in the generated PDF and play\n", + "back in a compatible viewer (e.g. Adobe Reader). tikzfigure has no dedicated\n", + "object model for it yet, so register the library with `usetikzlibrary(...)`\n", + "and write the `animate={:=\"\"}` directive with\n", + "`raw(...)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "fig.usetikzlibrary(\"animations\")\n", + "\n", + "# Animate a node's fill color.\n", + "traffic_light = fig.node(\n", + " (0, 0),\n", + " shape=\"circle\",\n", + " fill=\"orange\",\n", + " minimum_size=\"1cm\",\n", + ")\n", + "fig.raw(\n", + " f'\\\\tikzset{{animate={{{traffic_light.label}:fill=\"red\"}}}}',\n", + ")\n", + "\n", + "# Animate a node moving along a path (a translation over 2 seconds).\n", + "ball = fig.node(\n", + " (0, -2.5),\n", + " shape=\"circle\",\n", + " fill=\"blue!60\",\n", + " minimum_size=\"0.8cm\",\n", + ")\n", + "fig.raw(\n", + " f'\\\\tikzset{{animate={{{ball.label}:shift=\"{{(4,0)}}\", begin=0s, end=2s}}}}',\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## Timeline\n", + "\n", + "A horizontal timeline with event markers, year labels, and bracket annotations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "events = [\n", + " (0, \"2020\", \"Project Start\", \"blue!30\"),\n", + " (2, \"2021\", \"Alpha Release\", \"green!30\"),\n", + " (4, \"2022\", \"Beta Release\", \"yellow!40\"),\n", + " (6, \"2023\", \"v1.0\", \"orange!40\"),\n", + " (8, \"2024\", \"v2.0\", \"red!30\"),\n", + "]\n", + "\n", + "fig.raw(\n", + " \"\\\\draw[->, ultra thick, gray] (-0.5, 0) -- (8.5, 0) node[right] {Time};\",\n", + ")\n", + "\n", + "for x, year, label, color in events:\n", + " event_node = fig.node(\n", + " (x, 1.2),\n", + " shape=\"rectangle\",\n", + " fill=color,\n", + " content=label,\n", + " options=[\"rounded corners=3pt\"],\n", + " )\n", + " fig.raw(\n", + " f\"\\\\draw[thick] ({x}, 0) -- ({x}, 0.7);\",\n", + " )\n", + " fig.raw(f\"\\\\fill ({x}, 0) circle (0.1);\")\n", + " fig.raw(\n", + " f\"\\\\node[below] at ({x}, -0.3) {{\\\\textbf{{{year}}}}};\",\n", + " )\n", + " fig.raw(\n", + " f\"\\\\draw[->, dashed] ({x}, 0.7) -- ({event_node.label});\",\n", + " )\n", + "\n", + "fig.raw(\n", + " \"\\\\node[font=\\\\Large\\\\bfseries] at (4, 2.5) {Project Timeline};\",\n", + ")\n", + "fig.raw(\n", + " \"\\\\draw[<->, red, thick] (0, -1) -- node[below] {4 years} (8, -1);\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_09_raw_tikz.qmd b/tutorials/advanced/tutorial_09_raw_tikz.qmd deleted file mode 100644 index 03d4dff..0000000 --- a/tutorials/advanced/tutorial_09_raw_tikz.qmd +++ /dev/null @@ -1,315 +0,0 @@ ---- -title: "Raw TikZ" -jupyter: python3 ---- - -tikzfigure covers the most common TikZ operations through its Python API, but -TikZ is a vast language. `fig.raw(string)` injects an arbitrary LaTeX/TikZ -snippet directly into the generated code - giving you a full escape hatch. - -A key pattern: create nodes with the Python API (to get auto-generated labels), -then reference them in raw TikZ commands via `{node.label}`. - -This tutorial covers: - basic raw insertion - flowcharts with decision diamonds -- mathematical annotations and axis drawing - network graphs with curved edges - -PDF animations - timeline visualizations - -```{python} -import math - -from tikzfigure import TikzFigure -``` - -## Basic raw insertion - -Pass any TikZ string to `raw`. Node labels created by `node` can be interpolated -into the string. - -```{python} -fig = TikzFigure() - -n1 = fig.node( - (0, 0), - shape="circle", - color="white", - fill="blue", - content="Hello!", -) -n2 = fig.node( - (5, 0), - shape="circle", - color="white", - fill="red", - content="Hi!", -) - -# A raw comment -fig.raw("% --- raw TikZ below ---") - -# A plain text node at a fixed coordinate -fig.raw( - "\\node at (2.5, 1.2) {raw TikZ node};", -) - -# A dashed arrow referencing the Python-created node labels -fig.raw( - f"\\draw[->, thick, dashed] ({n2.label}) -- ({n1.label});", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Flowchart - -Flowcharts need `diamond` decision nodes and labelled arrows - both easy with -raw TikZ. - -```{python} -fig = TikzFigure() -fig.usetikzlibrary("shapes.geometric") - -start = fig.node( - (2, 4), - shape="ellipse", - fill="green!30", - content="Start", -) -process = fig.node( - (2, 2), - shape="rectangle", - fill="blue!20", - content="Process", -) -decision = fig.node( - (2, 0), - shape="diamond", - fill="orange!30", - content="Decision?", -) -yes_node = fig.node( - (0, -2), - shape="rectangle", - fill="red!20", - content="Action A", -) -no_node = fig.node( - (4, -2), - shape="rectangle", - fill="green!20", - content="Action B", -) -end = fig.node( - (2, -4), - shape="ellipse", - fill="red!30", - content="End", -) - -fig.raw( - f"\\draw[->, thick] ({start.label}) -- ({process.label});", -) -fig.raw( - f"\\draw[->, thick] ({process.label}) -- ({decision.label});", -) -fig.raw( - f"\\draw[->, thick] ({decision.label}) -- node[left] {{Yes}} ({yes_node.label});", -) -fig.raw( - f"\\draw[->, thick] ({decision.label}) -- node[right] {{No}} ({no_node.label});", -) -fig.raw( - f"\\draw[->, thick] ({yes_node.label}) -- ({end.label});", -) -fig.raw( - f"\\draw[->, thick] ({no_node.label}) -- ({end.label});", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Mathematical annotations and coordinate axes - -Raw TikZ handles axis arrows, tick marks, parabolas, and LaTeX math (`$...$`) -naturally. - -```{python} -fig = TikzFigure() - -p1 = fig.node((0, 0), content="") -p2 = fig.node((3, 2), content="") -p3 = fig.node((6, 0), content="") - -fig.raw( - f"\\draw[thick, blue] ({p1.label}) parabola bend ({p2.label}) ({p3.label});", -) -fig.raw( - f"\\draw[->, dashed, red] ({p1.label}) -- ({p2.label}) node[midway, left=40pt] {{$\\Delta y$}};", -) -fig.raw( - f"\\draw[->, dashed, green!50!black] ({p1.label}) -- ({p3.label}) node[midway, below=10pt] {{$\\Delta x$}};", -) -fig.raw( - "\\node at (3, 3) {$y = ax^2 + bx + c$};", -) - -fig.raw( - "\\draw[->, thick] (-1, 0) -- (7, 0) node[right] {$x$};", -) -fig.raw( - "\\draw[->, thick] (0, -1) -- (0, 3.5) node[above] {$y$};", -) - -for i in range(1, 7): - fig.raw( - f"\\draw ({i}, -0.1) -- ({i}, 0.1) node[below=3pt] {{\\small {i}}};", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Network graph with curved edges - -Nodes placed in a ring with `bend left` edges and some cross-connections. - -```{python} -fig = TikzFigure() - -n = 6 -R = 3 -nodes = [] -for i in range(n): - angle = 360 / n * i - x = R * math.cos(math.radians(angle)) - y = R * math.sin(math.radians(angle)) - nodes.append( - fig.node( - x, - y, - shape="circle", - fill="cyan!40", - content=f"N{i}", - ), - ) - -# Ring -for i in range(n): - j = (i + 1) % n - fig.raw( - f"\\draw[->, thick, bend left=15] ({nodes[i].label}) edge ({nodes[j].label});", - ) - -# Cross-connections -fig.raw( - f"\\draw[->, red, thick, dashed, bend right=30] ({nodes[0].label}) edge ({nodes[3].label});", -) -fig.raw( - f"\\draw[->, blue, thick, dotted, bend left=30] ({nodes[1].label}) edge ({nodes[4].label});", -) -fig.raw( - f"\\draw[->, green!60!black, thick, bend right=30] ({nodes[2].label}) edge ({nodes[5].label});", -) - -fig.raw( - "\\node[font=\\Large\\bfseries] at (0, 4.5) {Network Graph};", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## PDF animations (`animations` library) - -The `animations` library drives per-attribute animations (color changes, -motion along a path, ...) that get embedded in the generated PDF and play -back in a compatible viewer (e.g. Adobe Reader). tikzfigure has no dedicated -object model for it yet, so register the library with `usetikzlibrary(...)` -and write the `animate={:=""}` directive with -`raw(...)`. - -```{python} -fig = TikzFigure() -fig.usetikzlibrary("animations") - -# Animate a node's fill color. -traffic_light = fig.node( - (0, 0), - shape="circle", - fill="orange", - minimum_size="1cm", -) -fig.raw( - f'\\tikzset{{animate={{{traffic_light.label}:fill="red"}}}}', -) - -# Animate a node moving along a path (a translation over 2 seconds). -ball = fig.node( - (0, -2.5), - shape="circle", - fill="blue!60", - minimum_size="0.8cm", -) -fig.raw( - f'\\tikzset{{animate={{{ball.label}:shift="{{(4,0)}}", begin=0s, end=2s}}}}', -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Timeline - -A horizontal timeline with event markers, year labels, and bracket annotations. - -```{python} -fig = TikzFigure() - -events = [ - (0, "2020", "Project Start", "blue!30"), - (2, "2021", "Alpha Release", "green!30"), - (4, "2022", "Beta Release", "yellow!40"), - (6, "2023", "v1.0", "orange!40"), - (8, "2024", "v2.0", "red!30"), -] - -fig.raw( - "\\draw[->, ultra thick, gray] (-0.5, 0) -- (8.5, 0) node[right] {Time};", -) - -for x, year, label, color in events: - event_node = fig.node( - (x, 1.2), - shape="rectangle", - fill=color, - content=label, - options=["rounded corners=3pt"], - ) - fig.raw( - f"\\draw[thick] ({x}, 0) -- ({x}, 0.7);", - ) - fig.raw(f"\\fill ({x}, 0) circle (0.1);") - fig.raw( - f"\\node[below] at ({x}, -0.3) {{\\textbf{{{year}}}}};", - ) - fig.raw( - f"\\draw[->, dashed] ({x}, 0.7) -- ({event_node.label});", - ) - -fig.raw( - "\\node[font=\\Large\\bfseries] at (4, 2.5) {Project Timeline};", -) -fig.raw( - "\\draw[<->, red, thick] (0, -1) -- node[below] {4 years} (8, -1);", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} diff --git a/tutorials/advanced/tutorial_10_math_expressions.ipynb b/tutorials/advanced/tutorial_10_math_expressions.ipynb new file mode 100644 index 0000000..0252208 --- /dev/null +++ b/tutorials/advanced/tutorial_10_math_expressions.ipynb @@ -0,0 +1,705 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Mathematical Expressions\n", + "\n", + "**tikzfigure** includes a `math` module that lets you build PGF mathematical\n", + "expressions in Python. This is useful for creating parametric or symbolic\n", + "geometries without manually writing TikZ syntax.\n", + "\n", + "Instead of hardcoding coordinates like `(1.5, 2.0)`, you can write\n", + "`(sin(60), cos(60))` or `(Var(\"radius\") * 2, Var(\"angle\"))` — all as Python\n", + "code, with full composability and operator support.\n", + "\n", + "This tutorial covers:\n", + "\n", + "- Building expressions with functions and operators\n", + "- Using expressions as node coordinates\n", + "- Using expressions in variables\n", + "- Composing complex expressions\n", + "- Available PGF functions and constants" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure, arrows\n", + "from tikzfigure.math import Var, cos, max, pi, sin, sqrt" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Your first expression\n", + "\n", + "Any PGF math function is available as a Python function. Calling it returns an\n", + "`Expr` object that knows how to render itself into valid PGF syntax." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "# Basic expressions\n", + "expr1 = sin(60)\n", + "expr2 = cos(45)\n", + "expr3 = sqrt(2)\n", + "\n", + "print(f\"sin(60) → {expr1}\")\n", + "print(f\"cos(45) → {expr2}\")\n", + "print(f\"sqrt(2) → {expr3}\")" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "When you pass an `Expr` to a TikZ method, it's automatically converted to a\n", + "string via `__str__`.\n", + "\n", + "## Using expressions as node coordinates\n", + "\n", + "Any coordinate parameter (`x`, `y`, or in a tuple) accepts expressions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(figsize=(10, 8))\n", + "\n", + "# Place nodes at trigonometric coordinates\n", + "fig.node(\n", + " (sin(0), cos(0)),\n", + " label=\"P0\",\n", + " content=\"(sin 0°, cos 0°)\",\n", + " fill=\"red!30\",\n", + " shape=\"circle\",\n", + ")\n", + "fig.node(\n", + " (sin(45), cos(45)),\n", + " label=\"P45\",\n", + " content=\"(sin 45°, cos 45°)\",\n", + " fill=\"blue!30\",\n", + " shape=\"circle\",\n", + ")\n", + "fig.node(\n", + " (sin(90), cos(90)),\n", + " label=\"P90\",\n", + " content=\"(sin 90°, cos 90°)\",\n", + " fill=\"green!30\",\n", + " shape=\"circle\",\n", + ")\n", + "fig.node(\n", + " (sin(180), cos(180)),\n", + " label=\"P180\",\n", + " content=\"(sin 180°, cos 180°)\",\n", + " fill=\"yellow!30\",\n", + " shape=\"circle\",\n", + ")\n", + "\n", + "# Draw a path using expressions\n", + "fig.draw(\n", + " [\n", + " (sin(0), cos(0)),\n", + " (sin(45), cos(45)),\n", + " (sin(90), cos(90)),\n", + " (sin(180), cos(180)),\n", + " ],\n", + " arrows=arrows.forward,\n", + " color=\"gray\",\n", + " line_width=1.5,\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Operators and composition\n", + "\n", + "Expressions support Python's arithmetic operators (`+`, `-`, `*`, `/`) and\n", + "exponentiation (`**`). The operators are translated to valid PGF syntax:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "# Build complex expressions using operators\n", + "expr_sum = sin(60) + cos(60)\n", + "expr_product = sin(60) * 2\n", + "expr_power = sin(45) ** 2 # Note: ** becomes ^ in PGF\n", + "expr_composed = (sin(45) ** 2 + cos(45) ** 2) / Var(\"scale\")\n", + "\n", + "print(f\"sin(60) + cos(60) = {expr_sum}\")\n", + "print(f\"sin(60) * 2 = {expr_product}\")\n", + "print(f\"sin(45) ** 2 = {expr_power}\")\n", + "print(\n", + " f\"(sin(45)^2 + cos(45)^2) / scale = {expr_composed}\",\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Variable references with `Var()`\n", + "\n", + "Use `Var(name)` to reference a PGF variable by name. This is useful when you\n", + "define variables with `variable()` and want to use them in expressions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Define a variable\n", + "fig.variable(\"radius\", 3.5)\n", + "\n", + "# Use it in node coordinates\n", + "node_a = fig.node(\n", + " (Var(\"radius\"), 0),\n", + " label=\"A\",\n", + " content=\"(r, 0)\",\n", + " fill=\"blue!30\",\n", + " shape=\"circle\",\n", + ")\n", + "node_b = fig.node(\n", + " (0, Var(\"radius\")),\n", + " label=\"B\",\n", + " content=\"(0, r)\",\n", + " fill=\"red!30\",\n", + " shape=\"circle\",\n", + ")\n", + "\n", + "# Draw using the variable\n", + "fig.draw(\n", + " [(Var(\"radius\"), 0), (0, Var(\"radius\"))],\n", + " arrows=arrows.forward,\n", + " color=\"gray\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Expressions in variables\n", + "\n", + "You can pass expressions directly to `variable()`. This is powerful for defining\n", + "derived or computed values at compile time:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Define base variables (coordinates of a point)\n", + "fig.variable(\"x\", 3)\n", + "fig.variable(\"y\", 4)\n", + "\n", + "# Compute distance from origin and angle\n", + "fig.variable(\n", + " \"distance\",\n", + " sqrt(Var(\"x\") ** 2 + Var(\"y\") ** 2),\n", + ")\n", + "fig.variable(\"angle\", \"atan2(\\\\y, \\\\x)\") # PGF math: angle in radians\n", + "fig.variable(\"scaled\", Var(\"distance\") * 2)\n", + "\n", + "# Place nodes at (x, y), (distance, 0), and (scaled, 0)\n", + "fig.node(\n", + " (Var(\"x\"), Var(\"y\")),\n", + " label=\"xy_point\",\n", + " content=\"(x, y)\",\n", + " fill=\"blue!20\",\n", + " shape=\"circle\",\n", + ")\n", + "fig.node(\n", + " (Var(\"distance\"), 0),\n", + " label=\"distance\",\n", + " content=\"distance\",\n", + " fill=\"green!30\",\n", + " shape=\"circle\",\n", + ")\n", + "fig.node(\n", + " (Var(\"scaled\"), 0),\n", + " label=\"scaled\",\n", + " content=\"scaled\",\n", + " fill=\"purple!30\",\n", + " shape=\"circle\",\n", + ")\n", + "\n", + "# Draw a line from origin to (x, y)\n", + "fig.draw(\n", + " [(0, 0), (Var(\"x\"), Var(\"y\"))],\n", + " arrows=arrows.forward,\n", + " color=\"gray\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "Note: the generated TikZ emits `\\pgfmathsetmacro` commands with the expression\n", + "as-is. PGF evaluates these at compile time.\n", + "\n", + "## Mathematical functions\n", + "\n", + "The `math` module includes 60+ PGF functions:\n", + "\n", + "**Trigonometric:** `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sec`,\n", + "`cosec`, `cot`, `sinh`, `cosh`, `tanh`\n", + "\n", + "**Rounding:** `abs`, `ceil`, `floor`, `round` (⚠️ shadows Python built-ins)\n", + "\n", + "**Power & logarithm:** `sqrt`, `pow`, `exp`, `ln`, `log2`, `log10`\n", + "\n", + "**Min/max:** `min`, `max` (⚠️ shadows Python built-ins)\n", + "\n", + "**Arithmetic:** `neg`, `mod`, `div`, `frac`, `gcd`, `factorial`\n", + "\n", + "**Logic:** `and_`, `or_`, `not_`, `iseven`, `isodd`, `isprime`\n", + "\n", + "**Comparison:** `equal`, `greater`, `less`, `notequal`, `notgreater`, `notless`\n", + "\n", + "**Geometry:** `width`, `height`, `depth`, `veclen`\n", + "\n", + "**Other:** `deg`, `rad`, `int_`, `real`, `scalar`, `bin_`, `oct_`, `hex_`,\n", + "`ifthenelse`" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure.math import (\n", + " cos,\n", + " deg,\n", + " factorial, # Arithmetic\n", + " iseven, # Logic\n", + " ln,\n", + " sin,\n", + " sqrt,\n", + ")\n", + "\n", + "# Examples\n", + "print(f\"sqrt(2) = {sqrt(2)}\")\n", + "print(f\"max(3, 5) = {max(3, 5)}\")\n", + "print(f\"ln(e) = {ln('e')}\")\n", + "print(f\"deg(pi) = {deg('pi')}\")\n", + "print(f\"factorial(5) = {factorial(5)}\")\n", + "print(f\"iseven(4) = {iseven(4)}\")" + ] + }, + { + "cell_type": "markdown", + "id": "20", + "metadata": {}, + "source": [ + "## Constants\n", + "\n", + "Pre-defined constants are available as expressions:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure.math import e, false, rnd, true\n", + "\n", + "print(f\"pi = {pi}\")\n", + "print(f\"e = {e}\")\n", + "print(f\"rnd = {rnd}\") # Random [0, 1)\n", + "print(f\"true = {true}\")\n", + "print(f\"false = {false}\")" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## A practical example: plotting a circle\n", + "\n", + "Here's a complete example using expressions to parametrically place points\n", + "around a circle:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure.math import Var, cos, pi, sin\n", + "\n", + "fig = TikzFigure(figsize=(8, 8))\n", + "\n", + "# Define the radius as a variable\n", + "fig.variable(\"R\", 3)\n", + "\n", + "# Place 12 points around a circle at 30° intervals\n", + "num_points = 12\n", + "for i in range(num_points):\n", + " angle = i * 360 / num_points\n", + " x = Var(\"R\") * cos(angle)\n", + " y = Var(\"R\") * sin(angle)\n", + " fig.node(\n", + " (x, y),\n", + " label=f\"P{i}\",\n", + " content=str(i),\n", + " shape=\"circle\",\n", + " fill=\"blue!40\",\n", + " minimum_size=\"0.5cm\",\n", + " )\n", + "\n", + "# Draw the circle by connecting consecutive points\n", + "for i in range(num_points):\n", + " angle1 = i * 360 / num_points\n", + " angle2 = ((i + 1) % num_points) * 360 / num_points\n", + "\n", + " p1 = (Var(\"R\") * cos(angle1), Var(\"R\") * sin(angle1))\n", + " p2 = (Var(\"R\") * cos(angle2), Var(\"R\") * sin(angle2))\n", + "\n", + " fig.draw(\n", + " [p1, p2],\n", + " options=[\"very thin\"],\n", + " color=\"gray\",\n", + " )\n", + "\n", + "# Add a central node\n", + "fig.node(\n", + " (0, 0),\n", + " label=\"center\",\n", + " content=\"C\",\n", + " shape=\"circle\",\n", + " fill=\"red!40\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "The expressions are evaluated by PGF at compile time, not Python, so the\n", + "generated LaTeX is clean and readable — not a long list of hardcoded\n", + "coordinates.\n", + "\n", + "## Advanced: combining loops with expressions\n", + "\n", + "For parametric figures like grids or circles, you can combine TikZ `\\foreach`\n", + "loops (from [tutorial 6](https://max-models.github.io/tikzfigure/tutorials/tutorial-06-loops/)) with math expressions for even\n", + "more compact code:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure.math import Var, cos, sin\n", + "\n", + "fig = TikzFigure(figsize=(8, 8))\n", + "\n", + "# Define radius\n", + "fig.variable(\"R\", 2.5)\n", + "\n", + "# Use TikZ \\foreach loop with math expressions\n", + "with fig.loop(\n", + " \"i\",\n", + " range(8),\n", + " comment=\"Circle with 8 points\",\n", + ") as loop:\n", + " # Position: (R * cos(i*45°), R * sin(i*45°))\n", + " x = Var(\"R\") * cos(r\"\\i * 45\")\n", + " y = Var(\"R\") * sin(r\"\\i * 45\")\n", + "\n", + " loop.node(\n", + " (x, y),\n", + " label=r\"P\\i\",\n", + " content=r\"\\i\",\n", + " shape=\"circle\",\n", + " fill=\"blue!40\",\n", + " minimum_size=\"0.6cm\",\n", + " )\n", + "\n", + "# Add center\n", + "fig.node(\n", + " (0, 0),\n", + " label=\"O\",\n", + " content=\"O\",\n", + " shape=\"circle\",\n", + " fill=\"red!30\",\n", + ")\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "Notice how the loop variable `\\i` is embedded in the math expressions. TikZ\n", + "evaluates `\\i * 45` at each iteration, then passes it to `cos()` and `sin()`.\n", + "\n", + "## Next step: parametric TikZ plots\n", + "\n", + "If you want to go further and use declared PGF functions together with plain\n", + "TikZ `plot(...)` commands, continue with [Tokamak X-Point with Parametric TikZ\n", + "Plots](https://max-models.github.io/tikzfigure/tutorials/tutorial-25-parametric-tikz-plots/). That tutorial starts with a\n", + "single simple sampled curve and builds up step by step to a stylized tokamak\n", + "separatrix / X-point drawing.\n", + "\n", + "## Warning: shadowing Python built-ins\n", + "\n", + "Some PGF functions have names that match Python built-ins:\n", + "\n", + "- `abs`, `min`, `max`, `round` from `tikzfigure.math` shadow Python's built-ins\n", + "\n", + "This is intentional and fine when using targeted imports:\n", + "\n", + "```python\n", + "from tikzfigure.math import sin, cos # No shadowing\n", + "from tikzfigure.math import abs, max # Shadows Python built-ins in this scope\n", + "```\n", + "\n", + "To avoid confusion, avoid `from tikzfigure.math import *` (which would shadow\n", + "globally).\n", + "\n", + "## Next steps\n", + "\n", + "- Combine expressions with [loops](https://max-models.github.io/tikzfigure/tutorials/tutorial-06-loops/) for parametric grids\n", + "- Use expressions in [styling and\n", + " transformations](https://max-models.github.io/tikzfigure/tutorials/tutorial-21-advanced-styling/)\n", + "- See the [PGF manual](https://ctan.org/pkg/pgf) for the full list of functions\n", + " and their behavior" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_10_math_expressions.qmd b/tutorials/advanced/tutorial_10_math_expressions.qmd deleted file mode 100644 index bc2196c..0000000 --- a/tutorials/advanced/tutorial_10_math_expressions.qmd +++ /dev/null @@ -1,421 +0,0 @@ ---- -title: "Mathematical Expressions" -jupyter: python3 ---- - -**tikzfigure** includes a `math` module that lets you build PGF mathematical -expressions in Python. This is useful for creating parametric or symbolic -geometries without manually writing TikZ syntax. - -Instead of hardcoding coordinates like `(1.5, 2.0)`, you can write -`(sin(60), cos(60))` or `(Var("radius") * 2, Var("angle"))` — all as Python -code, with full composability and operator support. - -This tutorial covers: - -- Building expressions with functions and operators -- Using expressions as node coordinates -- Using expressions in variables -- Composing complex expressions -- Available PGF functions and constants - -```{python} -from tikzfigure import TikzFigure, arrows -from tikzfigure.math import sin, cos, sqrt, Var, pi, max, min -``` - -## Your first expression - -Any PGF math function is available as a Python function. Calling it returns an -`Expr` object that knows how to render itself into valid PGF syntax. - -```{python} -# Basic expressions -expr1 = sin(60) -expr2 = cos(45) -expr3 = sqrt(2) - -print(f"sin(60) → {expr1}") -print(f"cos(45) → {expr2}") -print(f"sqrt(2) → {expr3}") -``` - -When you pass an `Expr` to a TikZ method, it's automatically converted to a -string via `__str__`. - -## Using expressions as node coordinates - -Any coordinate parameter (`x`, `y`, or in a tuple) accepts expressions: - -```{python} -fig = TikzFigure(figsize=(10, 8)) - -# Place nodes at trigonometric coordinates -fig.node( - (sin(0), cos(0)), - label="P0", - content="(sin 0°, cos 0°)", - fill="red!30", - shape="circle", -) -fig.node( - (sin(45), cos(45)), - label="P45", - content="(sin 45°, cos 45°)", - fill="blue!30", - shape="circle", -) -fig.node( - (sin(90), cos(90)), - label="P90", - content="(sin 90°, cos 90°)", - fill="green!30", - shape="circle", -) -fig.node( - (sin(180), cos(180)), - label="P180", - content="(sin 180°, cos 180°)", - fill="yellow!30", - shape="circle", -) - -# Draw a path using expressions -fig.draw( - [ - (sin(0), cos(0)), - (sin(45), cos(45)), - (sin(90), cos(90)), - (sin(180), cos(180)), - ], - arrows=arrows.forward, - color="gray", - line_width=1.5, -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Operators and composition - -Expressions support Python's arithmetic operators (`+`, `-`, `*`, `/`) and -exponentiation (`**`). The operators are translated to valid PGF syntax: - -```{python} -# Build complex expressions using operators -expr_sum = sin(60) + cos(60) -expr_product = sin(60) * 2 -expr_power = sin(45) ** 2 # Note: ** becomes ^ in PGF -expr_composed = (sin(45) ** 2 + cos(45) ** 2) / Var("scale") - -print(f"sin(60) + cos(60) = {expr_sum}") -print(f"sin(60) * 2 = {expr_product}") -print(f"sin(45) ** 2 = {expr_power}") -print( - f"(sin(45)^2 + cos(45)^2) / scale = {expr_composed}", -) -``` - -## Variable references with `Var()` - -Use `Var(name)` to reference a PGF variable by name. This is useful when you -define variables with `variable()` and want to use them in expressions: - -```{python} -fig = TikzFigure() - -# Define a variable -fig.variable("radius", 3.5) - -# Use it in node coordinates -node_a = fig.node( - (Var("radius"), 0), - label="A", - content="(r, 0)", - fill="blue!30", - shape="circle", -) -node_b = fig.node( - (0, Var("radius")), - label="B", - content="(0, r)", - fill="red!30", - shape="circle", -) - -# Draw using the variable -fig.draw( - [(Var("radius"), 0), (0, Var("radius"))], - arrows=arrows.forward, - color="gray", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Expressions in variables - -You can pass expressions directly to `variable()`. This is powerful for defining -derived or computed values at compile time: - -```{python} -fig = TikzFigure() - -# Define base variables (coordinates of a point) -fig.variable("x", 3) -fig.variable("y", 4) - -# Compute distance from origin and angle -fig.variable( - "distance", - sqrt(Var("x") ** 2 + Var("y") ** 2), -) -fig.variable("angle", "atan2(\\y, \\x)") # PGF math: angle in radians -fig.variable("scaled", Var("distance") * 2) - -# Place nodes at (x, y), (distance, 0), and (scaled, 0) -fig.node( - (Var("x"), Var("y")), - label="xy_point", - content="(x, y)", - fill="blue!20", - shape="circle", -) -fig.node( - (Var("distance"), 0), - label="distance", - content="distance", - fill="green!30", - shape="circle", -) -fig.node( - (Var("scaled"), 0), - label="scaled", - content="scaled", - fill="purple!30", - shape="circle", -) - -# Draw a line from origin to (x, y) -fig.draw( - [(0, 0), (Var("x"), Var("y"))], - arrows=arrows.forward, - color="gray", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -Note: the generated TikZ emits `\pgfmathsetmacro` commands with the expression -as-is. PGF evaluates these at compile time. - -## Mathematical functions - -The `math` module includes 60+ PGF functions: - -**Trigonometric:** `sin`, `cos`, `tan`, `asin`, `acos`, `atan`, `atan2`, `sec`, -`cosec`, `cot`, `sinh`, `cosh`, `tanh` - -**Rounding:** `abs`, `ceil`, `floor`, `round` (⚠️ shadows Python built-ins) - -**Power & logarithm:** `sqrt`, `pow`, `exp`, `ln`, `log2`, `log10` - -**Min/max:** `min`, `max` (⚠️ shadows Python built-ins) - -**Arithmetic:** `neg`, `mod`, `div`, `frac`, `gcd`, `factorial` - -**Logic:** `and_`, `or_`, `not_`, `iseven`, `isodd`, `isprime` - -**Comparison:** `equal`, `greater`, `less`, `notequal`, `notgreater`, `notless` - -**Geometry:** `width`, `height`, `depth`, `veclen` - -**Other:** `deg`, `rad`, `int_`, `real`, `scalar`, `bin_`, `oct_`, `hex_`, -`ifthenelse` - -```{python} -from tikzfigure.math import ( - sin, - cos, - sqrt, - max, - min, # Common ones - ln, - log10, # Logarithms - deg, - rad, # Angle conversion - gcd, - factorial, # Arithmetic - iseven, - isodd, # Logic -) - -# Examples -print(f"sqrt(2) = {sqrt(2)}") -print(f"max(3, 5) = {max(3, 5)}") -print(f"ln(e) = {ln('e')}") -print(f"deg(pi) = {deg('pi')}") -print(f"factorial(5) = {factorial(5)}") -print(f"iseven(4) = {iseven(4)}") -``` - -## Constants - -Pre-defined constants are available as expressions: - -```{python} -from tikzfigure.math import pi, e, rnd, rand, true, false - -print(f"pi = {pi}") -print(f"e = {e}") -print(f"rnd = {rnd}") # Random [0, 1) -print(f"true = {true}") -print(f"false = {false}") -``` - -## A practical example: plotting a circle - -Here's a complete example using expressions to parametrically place points -around a circle: - -```{python} -from tikzfigure.math import sin, cos, pi, Var - -fig = TikzFigure(figsize=(8, 8)) - -# Define the radius as a variable -fig.variable("R", 3) - -# Place 12 points around a circle at 30° intervals -num_points = 12 -for i in range(num_points): - angle = i * 360 / num_points - x = Var("R") * cos(angle) - y = Var("R") * sin(angle) - fig.node( - (x, y), - label=f"P{i}", - content=str(i), - shape="circle", - fill="blue!40", - minimum_size="0.5cm", - ) - -# Draw the circle by connecting consecutive points -for i in range(num_points): - angle1 = i * 360 / num_points - angle2 = ((i + 1) % num_points) * 360 / num_points - - p1 = (Var("R") * cos(angle1), Var("R") * sin(angle1)) - p2 = (Var("R") * cos(angle2), Var("R") * sin(angle2)) - - fig.draw( - [p1, p2], - options=["very thin"], - color="gray", - ) - -# Add a central node -fig.node( - (0, 0), - label="center", - content="C", - shape="circle", - fill="red!40", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -The expressions are evaluated by PGF at compile time, not Python, so the -generated LaTeX is clean and readable — not a long list of hardcoded -coordinates. - -## Advanced: combining loops with expressions - -For parametric figures like grids or circles, you can combine TikZ `\foreach` -loops (from [tutorial 6](tutorial_06_loops.qmd)) with math expressions for even -more compact code: - -```{python} -from tikzfigure.math import sin, cos, Var - -fig = TikzFigure(figsize=(8, 8)) - -# Define radius -fig.variable("R", 2.5) - -# Use TikZ \foreach loop with math expressions -with fig.loop( - "i", - range(8), - comment="Circle with 8 points", -) as loop: - # Position: (R * cos(i*45°), R * sin(i*45°)) - x = Var("R") * cos(r"\i * 45") - y = Var("R") * sin(r"\i * 45") - - loop.node( - (x, y), - label=r"P\i", - content=r"\i", - shape="circle", - fill="blue!40", - minimum_size="0.6cm", - ) - -# Add center -fig.node( - (0, 0), - label="O", - content="O", - shape="circle", - fill="red!30", -) -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -Notice how the loop variable `\i` is embedded in the math expressions. TikZ -evaluates `\i * 45` at each iteration, then passes it to `cos()` and `sin()`. - -## Next step: parametric TikZ plots - -If you want to go further and use declared PGF functions together with plain -TikZ `plot(...)` commands, continue with [Tokamak X-Point with Parametric TikZ -Plots](tutorial_25_parametric_tikz_plots.qmd). That tutorial starts with a -single simple sampled curve and builds up step by step to a stylized tokamak -separatrix / X-point drawing. - -## Warning: shadowing Python built-ins - -Some PGF functions have names that match Python built-ins: - -- `abs`, `min`, `max`, `round` from `tikzfigure.math` shadow Python's built-ins - -This is intentional and fine when using targeted imports: - -```python -from tikzfigure.math import sin, cos # No shadowing -from tikzfigure.math import abs, max # Shadows Python built-ins in this scope -``` - -To avoid confusion, avoid `from tikzfigure.math import *` (which would shadow -globally). - -## Next steps - -- Combine expressions with [loops](tutorial_06_loops.qmd) for parametric grids -- Use expressions in [styling and - transformations](tutorial_21_advanced_styling.qmd) -- See the [PGF manual](https://ctan.org/pkg/pgf) for the full list of functions - and their behavior diff --git a/tutorials/advanced/tutorial_17_figure_configuration.ipynb b/tutorials/advanced/tutorial_17_figure_configuration.ipynb new file mode 100644 index 0000000..b856a94 --- /dev/null +++ b/tutorials/advanced/tutorial_17_figure_configuration.ipynb @@ -0,0 +1,946 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Figure Configuration\n", + "\n", + "`TikzFigure` accepts several constructor parameters that control how the figure\n", + "is set up in LaTeX. This tutorial covers:\n", + "\n", + "- `figure_setup`: global TikZ options applied to the entire `tikzpicture`\n", + " environment\n", + "- `add_style()`: named TikZ styles at figure scope\n", + "- `scope()`: local option blocks inside the figure\n", + "- `grid`: debug grid overlay\n", + "- `document_setup`: raw LaTeX preamble additions (for `\\usetikzlibrary` etc.)\n", + "- `add_package()`: additional LaTeX packages\n", + "- `generate_standalone()`: inspecting the full compilable document\n", + "- `label`: wrapping the figure in a `\\begin{figure}` environment\n", + "- `figsize`: display-size hint for Jupyter" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure, arrows, styles" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## `figure_setup`: global TikZ options\n", + "\n", + "The `figure_setup` string is placed inside `\\begin{tikzpicture}[HERE]`. Anything\n", + "you would normally write in the `tikzpicture` options block goes here.\n", + "\n", + "A particularly useful technique is `every node/.append style={...}` — it applies\n", + "a style to every node in the figure without repeating the options on each\n", + "individual `node` call." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " figure_setup=\"every node/.append style={draw, fill=blue!10, minimum size=1.2cm}\",\n", + ")\n", + "\n", + "A = fig.node((0, 0), content=\"A\")\n", + "B = fig.node((2, 0), content=\"B\")\n", + "C = fig.node((4, 0), content=\"C\")\n", + "D = fig.node((1, 2), content=\"D\")\n", + "E = fig.node((3, 2), content=\"E\")\n", + "\n", + "fig.draw([A, B, C], arrows=arrows.forward)\n", + "fig.draw([D, E], arrows=arrows.forward)\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "Every node gets a border and blue tint automatically — no per-node `draw=` or\n", + "`fill=` needed." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "# A more elaborate example: uniform rounded rectangles in a flow diagram\n", + "fig = TikzFigure(\n", + " figure_setup=(\n", + " \"every node/.append style={\"\n", + " \"draw=gray!70, fill=gray!10, \"\n", + " \"minimum width=2cm, minimum height=0.8cm, \"\n", + " \"rounded corners=4pt, font=\\\\small\"\n", + " \"}\"\n", + " ),\n", + ")\n", + "\n", + "nodes = [\"Input\", \"Process\", \"Output\"]\n", + "for i, label in enumerate(nodes):\n", + " fig.node(i * 3, 0, content=label)\n", + "\n", + "for i in range(len(nodes) - 1):\n", + " fig.draw(\n", + " [(i * 3 + 1.0, 0), (i * 3 + 2.0, 0)],\n", + " arrows=arrows.forward,\n", + " color=\"gray!70\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## `add_style()`: named TikZ styles at figure scope\n", + "\n", + "Use `add_style()` when you want reusable named styles inside the\n", + "`tikzpicture[...]` options block. This is the first-class equivalent of writing\n", + "entries such as `important line/.style={very thick}` by hand.\n", + "\n", + "`add_style()` returns a reusable style token, so you can immediately apply the\n", + "new style in later drawing calls." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "axes = fig.add_style(\"axes\")\n", + "important_line = fig.add_style(\"important line\", options=[\"very thick\"])\n", + "information_text = fig.add_style(\n", + " \"information text\",\n", + " options=[\"rounded corners\"],\n", + " fill=\"red!10\",\n", + " inner_sep=\"1ex\",\n", + ")\n", + "\n", + "fig.draw(\n", + " [(-1, 0), (2, 0)],\n", + " options=[axes, arrows.forward],\n", + " color=\"gray!70\",\n", + ")\n", + "fig.draw(\n", + " [(0, -1), (0, 2)],\n", + " options=[axes, arrows.forward],\n", + " color=\"gray!70\",\n", + ")\n", + "fig.draw(\n", + " [(0, 0), (1.2, 1.4)],\n", + " options=[important_line],\n", + " color=\"blue!70\",\n", + ")\n", + "fig.node(\n", + " (2.3, 1.1),\n", + " content=\"Named styles\\nstay reusable\",\n", + " options=[information_text],\n", + " text_width=\"3.2cm\",\n", + " anchor=\"west\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## `scope()`: local option blocks inside the figure\n", + "\n", + "Use `scope()` when a style or transformation should only apply to part of the\n", + "figure. It wraps nested drawing commands in `\\begin{scope}[...] ... \\end{scope}`\n", + "with local options.\n", + "\n", + "This is useful for small grouped coordinate systems, shifted sub-drawings, or\n", + "temporary style changes that should not affect the rest of the figure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "axes = fig.add_style(\"axes\")\n", + "\n", + "with fig.scope(options=[axes], xshift=\"0.5cm\", yshift=\"0.3cm\") as local:\n", + " local.draw([(-1, 0), (2, 0)], options=[arrows.forward], color=\"gray!70\")\n", + " local.draw([(0, -1), (0, 2)], options=[arrows.forward], color=\"gray!70\")\n", + "\n", + " A = local.node(\n", + " (0.8, 1.1),\n", + " label=\"A\",\n", + " content=\"A\",\n", + " shape=\"circle\",\n", + " fill=\"blue!20\",\n", + " )\n", + " B = local.node(\n", + " (1.6, 0.4),\n", + " label=\"B\",\n", + " content=\"B\",\n", + " shape=\"circle\",\n", + " fill=\"orange!20\",\n", + " )\n", + " local.draw(\n", + " [A, B],\n", + " options=[styles.thick],\n", + " color=\"purple!70\",\n", + " )\n", + "\n", + "fig.node(\n", + " (3.2, 1.2),\n", + " content=\"Outside the scope\",\n", + " draw=\"none\",\n", + " anchor=\"west\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## `grid`: debug grid overlay\n", + "\n", + "Pass `grid=True` to draw a background grid over the figure. This is a handy\n", + "positioning aid when you are still working out coordinates." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(grid=True)\n", + "\n", + "n1 = fig.node(\n", + " (0, 0),\n", + " shape=\"circle\",\n", + " fill=\"red!40\",\n", + " content=\"A\",\n", + " minimum_size=\"0.8cm\",\n", + ")\n", + "n2 = fig.node(\n", + " (3, 2),\n", + " shape=\"circle\",\n", + " fill=\"blue!40\",\n", + " content=\"B\",\n", + " minimum_size=\"0.8cm\",\n", + ")\n", + "n3 = fig.node(\n", + " (5, 0),\n", + " shape=\"circle\",\n", + " fill=\"green!40\",\n", + " content=\"C\",\n", + " minimum_size=\"0.8cm\",\n", + ")\n", + "fig.draw([n1, n2], arrows=arrows.forward)\n", + "fig.draw([n2, n3], arrows=arrows.forward)\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "The grid makes it easy to read off exact coordinates and spot misalignments\n", + "before removing `grid=True` for the final figure.\n", + "\n", + "## `document_setup`: raw LaTeX preamble additions\n", + "\n", + "`document_setup` is a raw LaTeX string inserted into the standalone document\n", + "preamble. It is the correct place for `\\usetikzlibrary{...}` calls and any other\n", + "preamble additions that the figure needs.\n", + "\n", + "A common use is loading the `positioning` library, which enables relative\n", + "placement keywords such as `above_of`, `below_of`, `left_of`, `right_of`, and\n", + "the `node_distance` parameter." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{positioning}\",\n", + ")\n", + "\n", + "root = fig.node(\n", + " (0, 0),\n", + " label=\"root\",\n", + " content=\"Root\",\n", + " shape=\"rectangle\",\n", + " fill=\"purple!30\",\n", + " minimum_size=\"1cm\",\n", + ")\n", + "\n", + "# Relative positioning: omit x/y so no 'at (...)' is emitted\n", + "fig.node(\n", + " label=\"above_node\",\n", + " content=\"Above\",\n", + " shape=\"rectangle\",\n", + " fill=\"cyan!30\",\n", + " minimum_size=\"1cm\",\n", + " above_of=\"root\",\n", + " node_distance=\"1.8cm\",\n", + ")\n", + "\n", + "fig.node(\n", + " label=\"below_node\",\n", + " content=\"Below\",\n", + " shape=\"rectangle\",\n", + " fill=\"cyan!30\",\n", + " minimum_size=\"1cm\",\n", + " below_of=\"root\",\n", + " node_distance=\"1.8cm\",\n", + ")\n", + "\n", + "fig.node(\n", + " label=\"left_node\",\n", + " content=\"Left\",\n", + " shape=\"rectangle\",\n", + " fill=\"orange!30\",\n", + " minimum_size=\"1cm\",\n", + " left_of=\"root\",\n", + " node_distance=\"2.5cm\",\n", + ")\n", + "\n", + "fig.node(\n", + " label=\"right_node\",\n", + " content=\"Right\",\n", + " shape=\"rectangle\",\n", + " fill=\"orange!30\",\n", + " minimum_size=\"1cm\",\n", + " right_of=\"root\",\n", + " node_distance=\"2.5cm\",\n", + ")\n", + "\n", + "for label in [\"above_node\", \"below_node\", \"left_node\", \"right_node\"]:\n", + " fig.draw(\n", + " [\"root\", label],\n", + " arrows=arrows.forward,\n", + " color=\"gray\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "## `add_package()`: additional LaTeX packages\n", + "\n", + "Call `add_package()` for each package you want in the standalone preamble. Each\n", + "package name becomes a `\\usepackage{name}` line in the standalone preamble. This\n", + "is useful when your figure content uses macros from packages like `amsmath` or\n", + "`xcolor`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "fig.add_package(\"amsmath\")\n", + "fig.add_package(\"xcolor\")\n", + "\n", + "# amsmath lets us use \\dfrac inside node content\n", + "fig.node(\n", + " (0, 2),\n", + " content=r\"$\\dfrac{a}{b} + \\sqrt{c}$\",\n", + " shape=\"rectangle\",\n", + " fill=\"yellow!20\",\n", + " draw=\"orange\",\n", + " minimum_width=\"3cm\",\n", + " minimum_height=\"1.2cm\",\n", + ")\n", + "\n", + "fig.node(\n", + " (0, 0),\n", + " content=r\"$\\sum_{k=1}^{n} k = \\dfrac{n(n+1)}{2}$\",\n", + " shape=\"rectangle\",\n", + " fill=\"green!20\",\n", + " draw=\"teal\",\n", + " minimum_width=\"4cm\",\n", + " minimum_height=\"1.2cm\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "## `generate_standalone()`: inspecting the full document\n", + "\n", + "`generate_standalone()` returns a complete compilable LaTeX document including\n", + "the preamble. This shows exactly how `document_setup`, `add_package()`, and\n", + "`usetikzlibrary()` are woven into the output." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "fig.add_package(\"amsmath\")\n", + "fig.usetikzlibrary(\"positioning\")\n", + "\n", + "fig.node(\n", + " (0, 0),\n", + " content=\"Hello\",\n", + " shape=\"circle\",\n", + " fill=\"blue!30\",\n", + ")\n", + "\n", + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "## `label`: wrapping in a figure environment\n", + "\n", + "When `label` is set, tikzfigure wraps the `tikzpicture` in a\n", + "`\\begin{figure}...\\end{figure}` environment and adds `\\label{...}` for\n", + "cross-referencing." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " label=\"fig:simple-graph\",\n", + ")\n", + "\n", + "A = fig.node(\n", + " (0, 1),\n", + " label=\"A\",\n", + " content=\"A\",\n", + " shape=\"circle\",\n", + " fill=\"cyan!40\",\n", + ")\n", + "B = fig.node(\n", + " (3, 1),\n", + " label=\"B\",\n", + " content=\"B\",\n", + " shape=\"circle\",\n", + " fill=\"cyan!40\",\n", + ")\n", + "C = fig.node(\n", + " (1.5, -1),\n", + " label=\"C\",\n", + " content=\"C\",\n", + " shape=\"circle\",\n", + " fill=\"cyan!40\",\n", + ")\n", + "\n", + "fig.draw([A, B], arrows=arrows.forward)\n", + "fig.draw([A, C], arrows=arrows.forward)\n", + "fig.draw([B, C], arrows=arrows.forward)\n", + "\n", + "print(fig.generate_tikz())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "Notice that `generate_tikz()` now includes the `\\begin{figure}` wrapper.\n", + "\n", + "## `figsize`: Jupyter display-size hint\n", + "\n", + "`figsize=(width, height)` controls how large the compiled image appears in\n", + "Jupyter. It is purely a display hint — it does not affect the LaTeX output or\n", + "the saved file." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(figsize=(4, 2))\n", + "\n", + "fig.node(\n", + " (0, 0),\n", + " shape=\"circle\",\n", + " fill=\"red!40\",\n", + " content=\"Small\",\n", + " minimum_size=\"1.5cm\",\n", + ")\n", + "fig.node(\n", + " (3, 0),\n", + " shape=\"circle\",\n", + " fill=\"blue!40\",\n", + " content=\"display\",\n", + " minimum_size=\"1.5cm\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "42", + "metadata": {}, + "outputs": [], + "source": [ + "# Same figure, larger display size\n", + "fig = TikzFigure(figsize=(8, 4))\n", + "\n", + "fig.node(\n", + " (0, 0),\n", + " shape=\"circle\",\n", + " fill=\"red!40\",\n", + " content=\"Large\",\n", + " minimum_size=\"1.5cm\",\n", + ")\n", + "fig.node(\n", + " (3, 0),\n", + " shape=\"circle\",\n", + " fill=\"blue!40\",\n", + " content=\"display\",\n", + " minimum_size=\"1.5cm\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "45", + "metadata": {}, + "source": [ + "The LaTeX code for both figures is identical — only the rendered size in the\n", + "notebook differs." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_17_figure_configuration.qmd b/tutorials/advanced/tutorial_17_figure_configuration.qmd deleted file mode 100644 index cecf7af..0000000 --- a/tutorials/advanced/tutorial_17_figure_configuration.qmd +++ /dev/null @@ -1,458 +0,0 @@ ---- -title: "Figure Configuration" -jupyter: python3 ---- - -`TikzFigure` accepts several constructor parameters that control how the figure -is set up in LaTeX. This tutorial covers: - -- `figure_setup`: global TikZ options applied to the entire `tikzpicture` - environment -- `add_style()`: named TikZ styles at figure scope -- `scope()`: local option blocks inside the figure -- `grid`: debug grid overlay -- `document_setup`: raw LaTeX preamble additions (for `\usetikzlibrary` etc.) -- `add_package()`: additional LaTeX packages -- `generate_standalone()`: inspecting the full compilable document -- `label`: wrapping the figure in a `\begin{figure}` environment -- `figsize`: display-size hint for Jupyter - -```{python} -from tikzfigure import TikzFigure, arrows, styles -``` - -## `figure_setup`: global TikZ options - -The `figure_setup` string is placed inside `\begin{tikzpicture}[HERE]`. Anything -you would normally write in the `tikzpicture` options block goes here. - -A particularly useful technique is `every node/.append style={...}` — it applies -a style to every node in the figure without repeating the options on each -individual `node` call. - -```{python} -fig = TikzFigure( - figure_setup="every node/.append style={draw, fill=blue!10, minimum size=1.2cm}", -) - -A = fig.node((0, 0), content="A") -B = fig.node((2, 0), content="B") -C = fig.node((4, 0), content="C") -D = fig.node((1, 2), content="D") -E = fig.node((3, 2), content="E") - -fig.draw([A, B, C], arrows=arrows.forward) -fig.draw([D, E], arrows=arrows.forward) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -Every node gets a border and blue tint automatically — no per-node `draw=` or -`fill=` needed. - -```{python} -# A more elaborate example: uniform rounded rectangles in a flow diagram -fig = TikzFigure( - figure_setup=( - "every node/.append style={" - "draw=gray!70, fill=gray!10, " - "minimum width=2cm, minimum height=0.8cm, " - "rounded corners=4pt, font=\\small" - "}" - ), -) - -nodes = ["Input", "Process", "Output"] -for i, label in enumerate(nodes): - fig.node(i * 3, 0, content=label) - -for i in range(len(nodes) - 1): - fig.draw( - [(i * 3 + 1.0, 0), (i * 3 + 2.0, 0)], - arrows=arrows.forward, - color="gray!70", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `add_style()`: named TikZ styles at figure scope - -Use `add_style()` when you want reusable named styles inside the -`tikzpicture[...]` options block. This is the first-class equivalent of writing -entries such as `important line/.style={very thick}` by hand. - -`add_style()` returns a reusable style token, so you can immediately apply the -new style in later drawing calls. - -```{python} -fig = TikzFigure() - -axes = fig.add_style("axes") -important_line = fig.add_style("important line", options=["very thick"]) -information_text = fig.add_style( - "information text", - options=["rounded corners"], - fill="red!10", - inner_sep="1ex", -) - -fig.draw( - [(-1, 0), (2, 0)], - options=[axes, arrows.forward], - color="gray!70", -) -fig.draw( - [(0, -1), (0, 2)], - options=[axes, arrows.forward], - color="gray!70", -) -fig.draw( - [(0, 0), (1.2, 1.4)], - options=[important_line], - color="blue!70", -) -fig.node( - (2.3, 1.1), - content="Named styles\nstay reusable", - options=[information_text], - text_width="3.2cm", - anchor="west", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `scope()`: local option blocks inside the figure - -Use `scope()` when a style or transformation should only apply to part of the -figure. It wraps nested drawing commands in `\begin{scope}[...] ... \end{scope}` -with local options. - -This is useful for small grouped coordinate systems, shifted sub-drawings, or -temporary style changes that should not affect the rest of the figure. - -```{python} -fig = TikzFigure() - -axes = fig.add_style("axes") - -with fig.scope(options=[axes], xshift="0.5cm", yshift="0.3cm") as local: - local.draw([(-1, 0), (2, 0)], options=[arrows.forward], color="gray!70") - local.draw([(0, -1), (0, 2)], options=[arrows.forward], color="gray!70") - - A = local.node( - (0.8, 1.1), - label="A", - content="A", - shape="circle", - fill="blue!20", - ) - B = local.node( - (1.6, 0.4), - label="B", - content="B", - shape="circle", - fill="orange!20", - ) - local.draw( - [A, B], - options=[styles.thick], - color="purple!70", - ) - -fig.node( - (3.2, 1.2), - content="Outside the scope", - draw="none", - anchor="west", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `grid`: debug grid overlay - -Pass `grid=True` to draw a background grid over the figure. This is a handy -positioning aid when you are still working out coordinates. - -```{python} -fig = TikzFigure(grid=True) - -n1 = fig.node( - (0, 0), - shape="circle", - fill="red!40", - content="A", - minimum_size="0.8cm", -) -n2 = fig.node( - (3, 2), - shape="circle", - fill="blue!40", - content="B", - minimum_size="0.8cm", -) -n3 = fig.node( - (5, 0), - shape="circle", - fill="green!40", - content="C", - minimum_size="0.8cm", -) -fig.draw([n1, n2], arrows=arrows.forward) -fig.draw([n2, n3], arrows=arrows.forward) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -The grid makes it easy to read off exact coordinates and spot misalignments -before removing `grid=True` for the final figure. - -## `document_setup`: raw LaTeX preamble additions - -`document_setup` is a raw LaTeX string inserted into the standalone document -preamble. It is the correct place for `\usetikzlibrary{...}` calls and any other -preamble additions that the figure needs. - -A common use is loading the `positioning` library, which enables relative -placement keywords such as `above_of`, `below_of`, `left_of`, `right_of`, and -the `node_distance` parameter. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{positioning}", -) - -root = fig.node( - (0, 0), - label="root", - content="Root", - shape="rectangle", - fill="purple!30", - minimum_size="1cm", -) - -# Relative positioning: omit x/y so no 'at (...)' is emitted -fig.node( - label="above_node", - content="Above", - shape="rectangle", - fill="cyan!30", - minimum_size="1cm", - above_of="root", - node_distance="1.8cm", -) - -fig.node( - label="below_node", - content="Below", - shape="rectangle", - fill="cyan!30", - minimum_size="1cm", - below_of="root", - node_distance="1.8cm", -) - -fig.node( - label="left_node", - content="Left", - shape="rectangle", - fill="orange!30", - minimum_size="1cm", - left_of="root", - node_distance="2.5cm", -) - -fig.node( - label="right_node", - content="Right", - shape="rectangle", - fill="orange!30", - minimum_size="1cm", - right_of="root", - node_distance="2.5cm", -) - -for label in ["above_node", "below_node", "left_node", "right_node"]: - fig.draw( - ["root", label], - arrows=arrows.forward, - color="gray", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `add_package()`: additional LaTeX packages - -Call `add_package()` for each package you want in the standalone preamble. Each -package name becomes a `\usepackage{name}` line in the standalone preamble. This -is useful when your figure content uses macros from packages like `amsmath` or -`xcolor`. - -```{python} -fig = TikzFigure() -fig.add_package("amsmath") -fig.add_package("xcolor") - -# amsmath lets us use \dfrac inside node content -fig.node( - (0, 2), - content=r"$\dfrac{a}{b} + \sqrt{c}$", - shape="rectangle", - fill="yellow!20", - draw="orange", - minimum_width="3cm", - minimum_height="1.2cm", -) - -fig.node( - (0, 0), - content=r"$\sum_{k=1}^{n} k = \dfrac{n(n+1)}{2}$", - shape="rectangle", - fill="green!20", - draw="teal", - minimum_width="4cm", - minimum_height="1.2cm", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `generate_standalone()`: inspecting the full document - -`generate_standalone()` returns a complete compilable LaTeX document including -the preamble. This shows exactly how `document_setup`, `add_package()`, and -`usetikzlibrary()` are woven into the output. - -```{python} -fig = TikzFigure() -fig.add_package("amsmath") -fig.usetikzlibrary("positioning") - -fig.node( - (0, 0), - content="Hello", - shape="circle", - fill="blue!30", -) - -print(fig.generate_standalone()) -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `label`: wrapping in a figure environment - -When `label` is set, tikzfigure wraps the `tikzpicture` in a -`\begin{figure}...\end{figure}` environment and adds `\label{...}` for -cross-referencing. - -```{python} -fig = TikzFigure( - label="fig:simple-graph", -) - -A = fig.node( - (0, 1), - label="A", - content="A", - shape="circle", - fill="cyan!40", -) -B = fig.node( - (3, 1), - label="B", - content="B", - shape="circle", - fill="cyan!40", -) -C = fig.node( - (1.5, -1), - label="C", - content="C", - shape="circle", - fill="cyan!40", -) - -fig.draw([A, B], arrows=arrows.forward) -fig.draw([A, C], arrows=arrows.forward) -fig.draw([B, C], arrows=arrows.forward) - -print(fig.generate_tikz()) -``` - -{{< include includes/tikz_code_callout.qmd >}} - -Notice that `generate_tikz()` now includes the `\begin{figure}` wrapper. - -## `figsize`: Jupyter display-size hint - -`figsize=(width, height)` controls how large the compiled image appears in -Jupyter. It is purely a display hint — it does not affect the LaTeX output or -the saved file. - -```{python} -fig = TikzFigure(figsize=(4, 2)) - -fig.node( - (0, 0), - shape="circle", - fill="red!40", - content="Small", - minimum_size="1.5cm", -) -fig.node( - (3, 0), - shape="circle", - fill="blue!40", - content="display", - minimum_size="1.5cm", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -```{python} -# Same figure, larger display size -fig = TikzFigure(figsize=(8, 4)) - -fig.node( - (0, 0), - shape="circle", - fill="red!40", - content="Large", - minimum_size="1.5cm", -) -fig.node( - (3, 0), - shape="circle", - fill="blue!40", - content="display", - minimum_size="1.5cm", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -The LaTeX code for both figures is identical — only the rendered size in the -notebook differs. diff --git a/tutorials/advanced/tutorial_18_shading_and_patterns.ipynb b/tutorials/advanced/tutorial_18_shading_and_patterns.ipynb new file mode 100644 index 0000000..16bf6a5 --- /dev/null +++ b/tutorials/advanced/tutorial_18_shading_and_patterns.ipynb @@ -0,0 +1,581 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Shading and Patterns\n", + "\n", + "TikZ supports rich fill styles beyond solid colors: smooth color gradients\n", + "(shadings) and repeating geometric fill patterns. This tutorial covers:\n", + "\n", + "- axis shading on nodes: horizontal and vertical gradients\n", + "- radial shading: glowing circles\n", + "- ball shading: three-dimensional-looking spheres\n", + "- axis shading on closed paths\n", + "- fill patterns: hatching, dots, crosshatch, checkerboard\n", + "- combining shadings and patterns in one figure\n", + "\n", + "All shading examples require `\\usetikzlibrary{shadings}` and all pattern\n", + "examples require `\\usetikzlibrary{patterns}`, both passed via `document_setup`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure, patterns, shapes" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Axis shading: horizontal and vertical gradients\n", + "\n", + "`shading=\"axis\"` produces a linear gradient. Use `left_color`/`right_color` for\n", + "a horizontal gradient, or `top_color`/`bottom_color` for a vertical one." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{shadings}\",\n", + ")\n", + "\n", + "# Horizontal gradients\n", + "pairs = [\n", + " (\"red\", \"yellow\"),\n", + " (\"blue\", \"cyan\"),\n", + " (\"purple\", \"orange\"),\n", + " (\"green!60!black\", \"lime\"),\n", + "]\n", + "\n", + "for i, (lc, rc) in enumerate(pairs):\n", + " fig.node(\n", + " (i * 3, 1.5),\n", + " shape=shapes.rectangle,\n", + " shading=\"axis\",\n", + " left_color=lc,\n", + " right_color=rc,\n", + " minimum_width=\"2.2cm\",\n", + " minimum_height=\"1.2cm\",\n", + " draw=\"gray!50\",\n", + " )\n", + " fig.node(\n", + " i * 3,\n", + " 0.5,\n", + " content=f\"{lc} → {rc}\",\n", + " draw=\"none\",\n", + " font=r\"\\tiny\",\n", + " anchor=\"center\",\n", + " )\n", + "\n", + "# Vertical gradients\n", + "vert_pairs = [\n", + " (\"blue!80\", \"white\"),\n", + " (\"orange\", \"yellow!50\"),\n", + " (\"teal\", \"green!30\"),\n", + "]\n", + "\n", + "for i, (tc, bc) in enumerate(vert_pairs):\n", + " fig.node(\n", + " (i * 3, -1),\n", + " shape=shapes.rectangle,\n", + " shading=\"axis\",\n", + " top_color=tc,\n", + " bottom_color=bc,\n", + " minimum_width=\"2.2cm\",\n", + " minimum_height=\"1.2cm\",\n", + " draw=\"gray!50\",\n", + " )\n", + " fig.node(\n", + " i * 3,\n", + " -2,\n", + " content=f\"{tc} → {bc}\",\n", + " draw=\"none\",\n", + " font=r\"\\tiny\",\n", + " anchor=\"center\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Radial shading: glowing circles\n", + "\n", + "`shading=\"radial\"` paints from `inner_color` at the center outward to\n", + "`outer_color`. This gives a convincing glow effect." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{shadings}\",\n", + ")\n", + "\n", + "glows = [\n", + " (\"white\", \"red\"),\n", + " (\"white\", \"blue\"),\n", + " (\"yellow\", \"orange\"),\n", + " (\"cyan!20\", \"teal\"),\n", + " (\"white\", \"purple\"),\n", + "]\n", + "\n", + "for i, (ic, oc) in enumerate(glows):\n", + " fig.node(\n", + " (i * 2.5, 0),\n", + " shape=shapes.circle,\n", + " shading=\"radial\",\n", + " inner_color=ic,\n", + " outer_color=oc,\n", + " minimum_size=\"1.8cm\",\n", + " draw=\"none\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Ball shading: three-dimensional spheres\n", + "\n", + "`shading=\"ball\"` with `ball_color` produces a highlight-shaded sphere that looks\n", + "three-dimensional." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{shadings}\",\n", + ")\n", + "\n", + "colors = [\"red\", \"blue\", \"green!60!black\", \"orange\", \"purple\", \"teal\"]\n", + "\n", + "for i, color in enumerate(colors):\n", + " fig.node(\n", + " (i * 2.2, 0),\n", + " shape=\"circle\",\n", + " shading=\"ball\",\n", + " ball_color=color,\n", + " minimum_size=\"1.6cm\",\n", + " draw=\"none\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Axis shading on a closed path\n", + "\n", + "Shading is not limited to nodes — you can apply it to any filled closed path\n", + "using `fig.draw(pts, cycle=True, shading=\"axis\", ...)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{shadings}\",\n", + ")\n", + "\n", + "# A gradient-filled pentagon\n", + "N = 5\n", + "angles = [math.pi / 2 + 2 * math.pi * k / N for k in range(N)]\n", + "pentagon = [(2 * math.cos(a), 2 * math.sin(a)) for a in angles]\n", + "\n", + "fig.draw(\n", + " pentagon,\n", + " cycle=True,\n", + " shading=\"axis\",\n", + " left_color=\"blue!60\",\n", + " right_color=\"cyan!30\",\n", + " draw=\"blue!80\",\n", + " line_width=1.5,\n", + ")\n", + "\n", + "# A gradient-filled star (six-point crossing)\n", + "star = [(0, 0), (1, 2.5), (2, 0), (-0.5, 1.5), (2.5, 1.5), (0, 0)]\n", + "shifted = [(x + 5, y - 1) for x, y in star]\n", + "fig.draw(\n", + " shifted,\n", + " cycle=True,\n", + " shading=\"axis\",\n", + " left_color=\"orange\",\n", + " right_color=\"yellow!60\",\n", + " draw=\"orange!80\",\n", + " line_width=1.5,\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## Fill patterns on nodes\n", + "\n", + "Patterns are repeating geometric fills. Common patterns include\n", + "`\"north east lines\"`, `\"dots\"`, `\"crosshatch\"`, `\"grid\"`, and `\"checkerboard\"`.\n", + "Use `pattern_color` to set the pattern ink color." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{patterns}\",\n", + ")\n", + "\n", + "patterns_used = [\n", + " (patterns.north_east_lines, \"blue!60\"),\n", + " (patterns.north_west_lines, \"red!60\"),\n", + " (patterns.dots, \"black\"),\n", + " (patterns.crosshatch, \"teal\"),\n", + " (patterns.grid, \"orange!80\"),\n", + " (patterns.checkerboard, \"purple!60\"),\n", + "]\n", + "\n", + "for i, (pat, pc) in enumerate(patterns_used):\n", + " col = i % 3\n", + " row = i // 3\n", + " fig.node(\n", + " (col * 3.5, row * (-2.5)),\n", + " shape=shapes.rectangle,\n", + " pattern=pat,\n", + " pattern_color=pc,\n", + " minimum_width=\"2.5cm\",\n", + " minimum_height=\"1.8cm\",\n", + " draw=\"gray!70\",\n", + " fill=\"white\",\n", + " )\n", + " fig.node(\n", + " col * 3.5,\n", + " row * (-2.5) - 1.2,\n", + " content=f\"\\\\texttt{{{pat}}}\",\n", + " draw=\"none\",\n", + " font=r\"\\tiny\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## Combining shadings and patterns in one figure\n", + "\n", + "Here a shaded background rectangle sits on a lower layer, while patterned nodes\n", + "sit on top, demonstrating how shadings and patterns can coexist in one figure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=(\n", + " r\"\\usetikzlibrary{shadings}\"\n", + " \"\\n\"\n", + " r\"\\usetikzlibrary{patterns}\"\n", + " ),\n", + ")\n", + "\n", + "# Shaded background panel\n", + "fig.node(\n", + " (3, 1),\n", + " shape=shapes.rectangle,\n", + " shading=\"axis\",\n", + " top_color=\"blue!20\",\n", + " bottom_color=\"white\",\n", + " minimum_width=\"8cm\",\n", + " minimum_height=\"4cm\",\n", + " draw=\"blue!30\",\n", + ")\n", + "\n", + "# Ball-shaded decorative sphere\n", + "fig.node(\n", + " (0.5, 2),\n", + " shape=shapes.circle,\n", + " shading=\"ball\",\n", + " ball_color=\"cyan\",\n", + " minimum_size=\"1.2cm\",\n", + " draw=\"none\",\n", + ")\n", + "\n", + "# Patterned foreground nodes\n", + "for i, (pat, fc) in enumerate(\n", + " [\n", + " (patterns.north_east_lines, \"red!40\"),\n", + " (patterns.dots, \"green!40\"),\n", + " (patterns.crosshatch, \"orange!40\"),\n", + " ],\n", + "):\n", + " fig.node(\n", + " (2 + i * 2, 1),\n", + " shape=shapes.rectangle,\n", + " pattern=pat,\n", + " pattern_color=fc,\n", + " fill=\"white\",\n", + " draw=\"gray!60\",\n", + " minimum_width=\"1.4cm\",\n", + " minimum_height=\"1.4cm\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_18_shading_and_patterns.qmd b/tutorials/advanced/tutorial_18_shading_and_patterns.qmd deleted file mode 100644 index 4893262..0000000 --- a/tutorials/advanced/tutorial_18_shading_and_patterns.qmd +++ /dev/null @@ -1,303 +0,0 @@ ---- -title: "Shading and Patterns" -jupyter: python3 ---- - -TikZ supports rich fill styles beyond solid colors: smooth color gradients -(shadings) and repeating geometric fill patterns. This tutorial covers: - -- axis shading on nodes: horizontal and vertical gradients -- radial shading: glowing circles -- ball shading: three-dimensional-looking spheres -- axis shading on closed paths -- fill patterns: hatching, dots, crosshatch, checkerboard -- combining shadings and patterns in one figure - -All shading examples require `\usetikzlibrary{shadings}` and all pattern -examples require `\usetikzlibrary{patterns}`, both passed via `document_setup`. - -```{python} -from tikzfigure import TikzFigure, patterns, shapes -``` - -## Axis shading: horizontal and vertical gradients - -`shading="axis"` produces a linear gradient. Use `left_color`/`right_color` for -a horizontal gradient, or `top_color`/`bottom_color` for a vertical one. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{shadings}", -) - -# Horizontal gradients -pairs = [ - ("red", "yellow"), - ("blue", "cyan"), - ("purple", "orange"), - ("green!60!black", "lime"), -] - -for i, (lc, rc) in enumerate(pairs): - fig.node( - (i * 3, 1.5), - shape=shapes.rectangle, - shading="axis", - left_color=lc, - right_color=rc, - minimum_width="2.2cm", - minimum_height="1.2cm", - draw="gray!50", - ) - fig.node( - i * 3, - 0.5, - content=f"{lc} → {rc}", - draw="none", - font=r"\tiny", - anchor="center", - ) - -# Vertical gradients -vert_pairs = [ - ("blue!80", "white"), - ("orange", "yellow!50"), - ("teal", "green!30"), -] - -for i, (tc, bc) in enumerate(vert_pairs): - fig.node( - (i * 3, -1), - shape=shapes.rectangle, - shading="axis", - top_color=tc, - bottom_color=bc, - minimum_width="2.2cm", - minimum_height="1.2cm", - draw="gray!50", - ) - fig.node( - i * 3, - -2, - content=f"{tc} → {bc}", - draw="none", - font=r"\tiny", - anchor="center", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Radial shading: glowing circles - -`shading="radial"` paints from `inner_color` at the center outward to -`outer_color`. This gives a convincing glow effect. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{shadings}", -) - -glows = [ - ("white", "red"), - ("white", "blue"), - ("yellow", "orange"), - ("cyan!20", "teal"), - ("white", "purple"), -] - -for i, (ic, oc) in enumerate(glows): - fig.node( - (i * 2.5, 0), - shape=shapes.circle, - shading="radial", - inner_color=ic, - outer_color=oc, - minimum_size="1.8cm", - draw="none", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Ball shading: three-dimensional spheres - -`shading="ball"` with `ball_color` produces a highlight-shaded sphere that looks -three-dimensional. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{shadings}", -) - -colors = ["red", "blue", "green!60!black", "orange", "purple", "teal"] - -for i, color in enumerate(colors): - fig.node( - (i * 2.2, 0), - shape="circle", - shading="ball", - ball_color=color, - minimum_size="1.6cm", - draw="none", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Axis shading on a closed path - -Shading is not limited to nodes — you can apply it to any filled closed path -using `fig.draw(pts, cycle=True, shading="axis", ...)`. - -```{python} -import math - -fig = TikzFigure( - document_setup=r"\usetikzlibrary{shadings}", -) - -# A gradient-filled pentagon -N = 5 -angles = [math.pi / 2 + 2 * math.pi * k / N for k in range(N)] -pentagon = [(2 * math.cos(a), 2 * math.sin(a)) for a in angles] - -fig.draw( - pentagon, - cycle=True, - shading="axis", - left_color="blue!60", - right_color="cyan!30", - draw="blue!80", - line_width=1.5, -) - -# A gradient-filled star (six-point crossing) -star = [(0, 0), (1, 2.5), (2, 0), (-0.5, 1.5), (2.5, 1.5), (0, 0)] -shifted = [(x + 5, y - 1) for x, y in star] -fig.draw( - shifted, - cycle=True, - shading="axis", - left_color="orange", - right_color="yellow!60", - draw="orange!80", - line_width=1.5, -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Fill patterns on nodes - -Patterns are repeating geometric fills. Common patterns include -`"north east lines"`, `"dots"`, `"crosshatch"`, `"grid"`, and `"checkerboard"`. -Use `pattern_color` to set the pattern ink color. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{patterns}", -) - -patterns_used = [ - (patterns.north_east_lines, "blue!60"), - (patterns.north_west_lines, "red!60"), - (patterns.dots, "black"), - (patterns.crosshatch, "teal"), - (patterns.grid, "orange!80"), - (patterns.checkerboard, "purple!60"), -] - -for i, (pat, pc) in enumerate(patterns_used): - col = i % 3 - row = i // 3 - fig.node( - (col * 3.5, row * (-2.5)), - shape=shapes.rectangle, - pattern=pat, - pattern_color=pc, - minimum_width="2.5cm", - minimum_height="1.8cm", - draw="gray!70", - fill="white", - ) - fig.node( - col * 3.5, - row * (-2.5) - 1.2, - content=f"\\texttt{{{pat}}}", - draw="none", - font=r"\tiny", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Combining shadings and patterns in one figure - -Here a shaded background rectangle sits on a lower layer, while patterned nodes -sit on top, demonstrating how shadings and patterns can coexist in one figure. - -```{python} -fig = TikzFigure( - document_setup=( - r"\usetikzlibrary{shadings}" - "\n" - r"\usetikzlibrary{patterns}" - ), -) - -# Shaded background panel -fig.node( - (3, 1), - shape=shapes.rectangle, - shading="axis", - top_color="blue!20", - bottom_color="white", - minimum_width="8cm", - minimum_height="4cm", - draw="blue!30", -) - -# Ball-shaded decorative sphere -fig.node( - (0.5, 2), - shape=shapes.circle, - shading="ball", - ball_color="cyan", - minimum_size="1.2cm", - draw="none", -) - -# Patterned foreground nodes -for i, (pat, fc) in enumerate( - [ - (patterns.north_east_lines, "red!40"), - (patterns.dots, "green!40"), - (patterns.crosshatch, "orange!40"), - ], -): - fig.node( - (2 + i * 2, 1), - shape=shapes.rectangle, - pattern=pat, - pattern_color=fc, - fill="white", - draw="gray!60", - minimum_width="1.4cm", - minimum_height="1.4cm", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} diff --git a/tutorials/advanced/tutorial_19_opacity_and_transparency.ipynb b/tutorials/advanced/tutorial_19_opacity_and_transparency.ipynb new file mode 100644 index 0000000..e49b631 --- /dev/null +++ b/tutorials/advanced/tutorial_19_opacity_and_transparency.ipynb @@ -0,0 +1,547 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Opacity and Transparency\n", + "\n", + "tikzfigure provides fine-grained control over transparency at every level: whole\n", + "nodes, just the fill, just the stroke, paths, and the final PNG background. This\n", + "tutorial covers:\n", + "\n", + "- `opacity` on nodes\n", + "- `fill_opacity` vs `draw_opacity` independently\n", + "- `opacity` and `fill_opacity` on paths\n", + "- transparent vs white backgrounds with `show()` and `savefig()`\n", + "- a practical Venn diagram" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## `opacity` on nodes\n", + "\n", + "`opacity` controls the overall transparency of a node (both fill and stroke\n", + "together). A value of `1.0` is fully opaque; `0.0` is invisible.\n", + "\n", + "The circles below overlap so you can see how lower opacity lets underlying\n", + "content show through." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "opacities = [1.0, 0.8, 0.6, 0.4, 0.2]\n", + "\n", + "for i, op in enumerate(opacities):\n", + " fig.node(\n", + " (i * 1.5, 0),\n", + " shape=\"circle\",\n", + " fill=\"blue!60\",\n", + " draw=\"blue!80\",\n", + " minimum_size=\"2.5cm\",\n", + " opacity=op,\n", + " )\n", + " fig.node(\n", + " (i * 1.5, -1.8),\n", + " content=f\"opacity={op}\",\n", + " draw=\"none\",\n", + " font=r\"\\tiny\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## `fill_opacity` vs `draw_opacity`\n", + "\n", + "`fill_opacity` and `draw_opacity` let you control the fill and stroke\n", + "independently. Here every node has an opaque border but a translucent fill — a\n", + "common styling choice for overlapping regions." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "colors = [\"red\", \"blue\", \"green!60!black\", \"orange\"]\n", + "\n", + "for i, color in enumerate(colors):\n", + " fig.node(\n", + " (i * 2, 0),\n", + " shape=\"circle\",\n", + " fill=color,\n", + " draw=color,\n", + " minimum_size=\"2cm\",\n", + " fill_opacity=0.3,\n", + " draw_opacity=1.0,\n", + " line_width=2,\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## `opacity` on paths\n", + "\n", + "Paths support the same `opacity` parameter. Below, three filled triangles\n", + "overlap with decreasing opacity." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "triangles = [\n", + " ([(0, 0), (3, 0), (1.5, 2.5)], \"red\", 0.9),\n", + " ([(1, 0), (4, 0), (2.5, 2.5)], \"blue\", 0.7),\n", + " ([(2, 0), (5, 0), (3.5, 2.5)], \"green!60!black\", 0.5),\n", + "]\n", + "\n", + "for pts, color, op in triangles:\n", + " fig.draw(\n", + " pts,\n", + " cycle=True,\n", + " fill=color,\n", + " draw=color,\n", + " opacity=op,\n", + " line_width=1.5,\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## `fill_opacity` on path fills\n", + "\n", + "Use `fill_opacity` on a path to make the fill semi-transparent while keeping the\n", + "outline fully visible." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "import math\n", + "\n", + "# A set of overlapping circles rendered as paths\n", + "for i in range(4):\n", + " angle = i * math.pi / 2\n", + " cx = 2 + math.cos(angle) * 1.2\n", + " cy = 2 + math.sin(angle) * 1.2\n", + " theta = [2 * math.pi * k / 40 for k in range(40)]\n", + " pts = [(cx + math.cos(t), cy + math.sin(t)) for t in theta]\n", + " colors = [\"red\", \"blue\", \"green!70!black\", \"orange\"]\n", + " fig.draw(\n", + " pts,\n", + " cycle=True,\n", + " fill=colors[i],\n", + " draw=colors[i],\n", + " fill_opacity=0.35,\n", + " draw_opacity=1.0,\n", + " line_width=2,\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## Transparent vs white backgrounds\n", + "\n", + "By default `show()` and `savefig()` produce a **transparent** PNG background\n", + "(`transparent=True`). This is ideal for embedding the figure in slides or\n", + "documents where the background color matters.\n", + "\n", + "Passing `transparent=False` gives a **white** background — useful when the\n", + "rendering environment does not support transparency. Note that setting\n", + "`transparent=False` does not affect PDF output, which is always vector and\n", + "relies on the viewer for background rendering. It also does not affect the Tikz\n", + "code itself, which does not include any background rectangle by default." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "fig.node(\n", + " (0, 0),\n", + " shape=\"circle\",\n", + " fill=\"orange!60\",\n", + " content=\"Hello!\",\n", + " minimum_size=\"2cm\",\n", + ")\n", + "fig.node(\n", + " (3, 0),\n", + " shape=\"rectangle\",\n", + " fill=\"teal!40\",\n", + " content=\"World!\",\n", + " minimum_width=\"2.5cm\",\n", + " minimum_height=\"1.2cm\",\n", + ")\n", + "\n", + "# Default: transparent PNG background (default transparent=True)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "# White background\n", + "fig.show(transparent=False)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "# Saving examples (commented out — uncomment to run)\n", + "# fig.savefig(\"figure_transparent.png\") # transparent background (default)\n", + "# fig.savefig(\"figure_white.png\", transparent=False) # white background" + ] + }, + { + "cell_type": "markdown", + "id": "24", + "metadata": {}, + "source": [ + "The `transparent` flag only affects PNG output. PDF output is always vector and\n", + "the background transparency is determined by the viewer.\n", + "\n", + "## Practical example: Venn diagram\n", + "\n", + "A classic Venn diagram uses `fill_opacity` so the overlapping region inherits\n", + "both colors visually." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "fig = TikzFigure()\n", + "\n", + "R = 1.5 # circle radius\n", + "d = 1.2 # distance between centers\n", + "\n", + "# Left circle\n", + "theta = [2 * math.pi * k / 80 for k in range(80)]\n", + "left = [(-d / 2 + R * math.cos(t), R * math.sin(t)) for t in theta]\n", + "right = [(d / 2 + R * math.cos(t), R * math.sin(t)) for t in theta]\n", + "\n", + "fig.draw(\n", + " left,\n", + " cycle=True,\n", + " fill=\"blue\",\n", + " draw=\"blue!80\",\n", + " fill_opacity=0.4,\n", + " line_width=2,\n", + ")\n", + "fig.draw(\n", + " right,\n", + " cycle=True,\n", + " fill=\"red\",\n", + " draw=\"red!80\",\n", + " fill_opacity=0.4,\n", + " line_width=2,\n", + ")\n", + "\n", + "# Labels\n", + "fig.node(\n", + " -d / 2 - 0.6,\n", + " 0,\n", + " content=\"Set A\",\n", + " draw=\"none\",\n", + " font=r\"\\bfseries\",\n", + " color=\"blue!80\",\n", + ")\n", + "fig.node(\n", + " d / 2 + 0.6,\n", + " 0,\n", + " content=\"Set B\",\n", + " draw=\"none\",\n", + " font=r\"\\bfseries\",\n", + " color=\"red!80\",\n", + ")\n", + "fig.node(\n", + " (0, 0),\n", + " content=r\"$A \\cap B$\",\n", + " draw=\"none\",\n", + " font=r\"\\small\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "26", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_19_opacity_and_transparency.qmd b/tutorials/advanced/tutorial_19_opacity_and_transparency.qmd deleted file mode 100644 index 4419d8b..0000000 --- a/tutorials/advanced/tutorial_19_opacity_and_transparency.qmd +++ /dev/null @@ -1,257 +0,0 @@ ---- -title: "Opacity and Transparency" -jupyter: python3 ---- - -tikzfigure provides fine-grained control over transparency at every level: whole -nodes, just the fill, just the stroke, paths, and the final PNG background. This -tutorial covers: - -- `opacity` on nodes -- `fill_opacity` vs `draw_opacity` independently -- `opacity` and `fill_opacity` on paths -- transparent vs white backgrounds with `show()` and `savefig()` -- a practical Venn diagram - -```{python} -from tikzfigure import TikzFigure -``` - -## `opacity` on nodes - -`opacity` controls the overall transparency of a node (both fill and stroke -together). A value of `1.0` is fully opaque; `0.0` is invisible. - -The circles below overlap so you can see how lower opacity lets underlying -content show through. - -```{python} -fig = TikzFigure() - -opacities = [1.0, 0.8, 0.6, 0.4, 0.2] - -for i, op in enumerate(opacities): - fig.node( - (i * 1.5, 0), - shape="circle", - fill="blue!60", - draw="blue!80", - minimum_size="2.5cm", - opacity=op, - ) - fig.node( - (i * 1.5, -1.8), - content=f"opacity={op}", - draw="none", - font=r"\tiny", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `fill_opacity` vs `draw_opacity` - -`fill_opacity` and `draw_opacity` let you control the fill and stroke -independently. Here every node has an opaque border but a translucent fill — a -common styling choice for overlapping regions. - -```{python} -fig = TikzFigure() - -colors = ["red", "blue", "green!60!black", "orange"] - -for i, color in enumerate(colors): - fig.node( - (i * 2, 0), - shape="circle", - fill=color, - draw=color, - minimum_size="2cm", - fill_opacity=0.3, - draw_opacity=1.0, - line_width=2, - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `opacity` on paths - -Paths support the same `opacity` parameter. Below, three filled triangles -overlap with decreasing opacity. - -```{python} -fig = TikzFigure() - -triangles = [ - ([(0, 0), (3, 0), (1.5, 2.5)], "red", 0.9), - ([(1, 0), (4, 0), (2.5, 2.5)], "blue", 0.7), - ([(2, 0), (5, 0), (3.5, 2.5)], "green!60!black", 0.5), -] - -for pts, color, op in triangles: - fig.draw( - pts, - cycle=True, - fill=color, - draw=color, - opacity=op, - line_width=1.5, - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `fill_opacity` on path fills - -Use `fill_opacity` on a path to make the fill semi-transparent while keeping the -outline fully visible. - -```{python} -fig = TikzFigure() - -import math - -# A set of overlapping circles rendered as paths -for i in range(4): - angle = i * math.pi / 2 - cx = 2 + math.cos(angle) * 1.2 - cy = 2 + math.sin(angle) * 1.2 - theta = [2 * math.pi * k / 40 for k in range(40)] - pts = [(cx + math.cos(t), cy + math.sin(t)) for t in theta] - colors = ["red", "blue", "green!70!black", "orange"] - fig.draw( - pts, - cycle=True, - fill=colors[i], - draw=colors[i], - fill_opacity=0.35, - draw_opacity=1.0, - line_width=2, - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Transparent vs white backgrounds - -By default `show()` and `savefig()` produce a **transparent** PNG background -(`transparent=True`). This is ideal for embedding the figure in slides or -documents where the background color matters. - -Passing `transparent=False` gives a **white** background — useful when the -rendering environment does not support transparency. Note that setting -`transparent=False` does not affect PDF output, which is always vector and -relies on the viewer for background rendering. It also does not affect the Tikz -code itself, which does not include any background rectangle by default. - -```{python} -fig = TikzFigure() - -fig.node( - (0, 0), - shape="circle", - fill="orange!60", - content="Hello!", - minimum_size="2cm", -) -fig.node( - (3, 0), - shape="rectangle", - fill="teal!40", - content="World!", - minimum_width="2.5cm", - minimum_height="1.2cm", -) - -# Default: transparent PNG background (default transparent=True) -fig.show() -``` - -```{python} -# White background -fig.show(transparent=False) -``` - -{{< include includes/tikz_code_callout.qmd >}} - -```{python} -# Saving examples (commented out — uncomment to run) -# fig.savefig("figure_transparent.png") # transparent background (default) -# fig.savefig("figure_white.png", transparent=False) # white background -``` - -The `transparent` flag only affects PNG output. PDF output is always vector and -the background transparency is determined by the viewer. - -## Practical example: Venn diagram - -A classic Venn diagram uses `fill_opacity` so the overlapping region inherits -both colors visually. - -```{python} -import math - -fig = TikzFigure() - -R = 1.5 # circle radius -d = 1.2 # distance between centers - -# Left circle -theta = [2 * math.pi * k / 80 for k in range(80)] -left = [(-d / 2 + R * math.cos(t), R * math.sin(t)) for t in theta] -right = [(d / 2 + R * math.cos(t), R * math.sin(t)) for t in theta] - -fig.draw( - left, - cycle=True, - fill="blue", - draw="blue!80", - fill_opacity=0.4, - line_width=2, -) -fig.draw( - right, - cycle=True, - fill="red", - draw="red!80", - fill_opacity=0.4, - line_width=2, -) - -# Labels -fig.node( - -d / 2 - 0.6, - 0, - content="Set A", - draw="none", - font=r"\bfseries", - color="blue!80", -) -fig.node( - d / 2 + 0.6, - 0, - content="Set B", - draw="none", - font=r"\bfseries", - color="red!80", -) -fig.node( - (0, 0), - content=r"$A \cap B$", - draw="none", - font=r"\small", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} diff --git a/tutorials/advanced/tutorial_20_decorations.ipynb b/tutorials/advanced/tutorial_20_decorations.ipynb new file mode 100644 index 0000000..13537a6 --- /dev/null +++ b/tutorials/advanced/tutorial_20_decorations.ipynb @@ -0,0 +1,626 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Decorations\n", + "\n", + "TikZ decorations transform the appearance of paths: a straight line can become a\n", + "zigzag, a snake wave, a coil spring, or a brace bracket. This tutorial covers\n", + "the decorations available through `tikzfigure`'s `decoration` parameter:\n", + "\n", + "- `\"zigzag\"`, `\"snake\"`, `\"coil\"`, `\"bumps\"` — path morphing decorations\n", + "- side-by-side gallery of all morphing decorations\n", + "- `\"brace\"` — bracket annotations (requires a second library)\n", + "- combining decorations with colors and line widths\n", + "\n", + "**Important:** You must always pass `options=[\"decorate\"]` alongside\n", + "`decoration=` to actually activate the decoration. The `decoration=` kwarg sets\n", + "*which* decoration to use; `decorate` is the flag that switches it on.\n", + "\n", + "All morphing decoration examples require\n", + "`document_setup=r\"\\usetikzlibrary{decorations.pathmorphing}\"`. The brace\n", + "decoration additionally requires `\\usetikzlibrary{decorations.pathreplacing}`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure, decorations, shapes, styles" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## `decoration=\"zigzag\"`\n", + "\n", + "A zigzag path oscillates sharply between two extremes along the path direction." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{decorations.pathmorphing}\",\n", + ")\n", + "\n", + "fig.draw(\n", + " [(0, 0), (6, 0)],\n", + " options=[styles.decorate],\n", + " decoration=decorations.zigzag,\n", + " line_width=2,\n", + " color=\"red\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## `decoration=\"snake\"`\n", + "\n", + "A snake decoration produces a smooth sinusoidal wave — like a zigzag but with\n", + "rounded peaks." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{decorations.pathmorphing}\",\n", + ")\n", + "\n", + "fig.draw(\n", + " [(0, 0), (6, 0)],\n", + " options=[styles.decorate],\n", + " decoration=decorations.snake,\n", + " line_width=2,\n", + " color=\"blue\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## `decoration=\"coil\"`\n", + "\n", + "A coil decoration draws a helical spring along the path. It works well for\n", + "physics diagrams." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{decorations.pathmorphing}\",\n", + ")\n", + "\n", + "fig.draw(\n", + " [(0, 0), (6, 0)],\n", + " options=[styles.decorate],\n", + " decoration=decorations.coil,\n", + " line_width=1.5,\n", + " color=\"teal\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## `decoration=\"bumps\"`\n", + "\n", + "Bumps produce smooth humps along the path — similar to a snake but with the\n", + "oscillation only on one side." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{decorations.pathmorphing}\",\n", + ")\n", + "\n", + "fig.draw(\n", + " [(0, 0), (6, 0)],\n", + " options=[styles.decorate],\n", + " decoration=decorations.bumps,\n", + " line_width=2,\n", + " color=\"orange\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## Side-by-side gallery of morphing decorations\n", + "\n", + "Showing all four morphing decorations together, each with a label, makes it easy\n", + "to choose the right one for your diagram." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{decorations.pathmorphing}\",\n", + ")\n", + "\n", + "decoration_specs = [\n", + " (decorations.zigzag, \"red\"),\n", + " (decorations.snake, \"blue\"),\n", + " (decorations.coil, \"teal\"),\n", + " (decorations.bumps, \"orange\"),\n", + "]\n", + "\n", + "for i, (dec, color) in enumerate(decoration_specs):\n", + " y = -i * 1.4\n", + " fig.draw(\n", + " [(0, y), (5, y)],\n", + " options=[styles.decorate],\n", + " decoration=dec,\n", + " line_width=2,\n", + " color=color,\n", + " )\n", + " fig.node(\n", + " 5.3,\n", + " y,\n", + " content=f\"\\\\texttt{{{dec}}}\",\n", + " draw=\"none\",\n", + " anchor=\"west\",\n", + " font=r\"\\small\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## `decoration=\"brace\"`: bracket annotations\n", + "\n", + "The `\"brace\"` decoration draws a curly bracket spanning two points. It requires\n", + "the `decorations.pathreplacing` library.\n", + "\n", + "Use a brace to annotate the width or height of a region in your diagram. Draw\n", + "the brace from right to left (or bottom to top) so the bracket opens toward the\n", + "region." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=(\n", + " r\"\\usetikzlibrary{decorations.pathmorphing}\"\n", + " \"\\n\"\n", + " r\"\\usetikzlibrary{decorations.pathreplacing}\"\n", + " ),\n", + ")\n", + "\n", + "# The region being annotated\n", + "fig.node(\n", + " (2, 1),\n", + " shape=shapes.rectangle,\n", + " fill=\"blue!15\",\n", + " draw=\"blue!50\",\n", + " minimum_width=\"4cm\",\n", + " minimum_height=\"1.5cm\",\n", + " content=\"annotated region\",\n", + ")\n", + "\n", + "# Horizontal brace below the rectangle (right-to-left so the brace opens upward)\n", + "fig.draw(\n", + " [(4, 0.1), (0, 0.1)],\n", + " options=[styles.decorate],\n", + " decoration=decorations.brace,\n", + " line_width=1.5,\n", + " color=\"blue!70\",\n", + ")\n", + "fig.node(\n", + " (2, -0.45),\n", + " content=\"4 cm wide\",\n", + " draw=\"none\",\n", + " color=\"blue!70\",\n", + " font=r\"\\small\",\n", + ")\n", + "\n", + "# Vertical brace on the right side (bottom-to-top so the brace opens rightward)\n", + "fig.draw(\n", + " [(4.15, 0.25), (4.15, 1.75)],\n", + " options=[styles.decorate],\n", + " decoration=decorations.brace,\n", + " line_width=1.5,\n", + " color=\"red!70\",\n", + ")\n", + "fig.node(\n", + " (5.0, 1.0),\n", + " content=\"1.5 cm\",\n", + " draw=\"none\",\n", + " color=\"red!70\",\n", + " font=r\"\\small\",\n", + " anchor=\"west\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "## Combining decorations with colors and line widths\n", + "\n", + "Decorations become more expressive when combined with thick lines and vivid\n", + "colors. Here is a physics-style figure with springs, a snake rope, and a bumpy\n", + "road." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=(\n", + " r\"\\usetikzlibrary{decorations.pathmorphing}\"\n", + " \"\\n\"\n", + " r\"\\usetikzlibrary{decorations.pathreplacing}\"\n", + " ),\n", + ")\n", + "\n", + "# Ground line (zigzag = rough terrain)\n", + "fig.draw(\n", + " [(0, 0), (8, 0)],\n", + " options=[\"decorate\"],\n", + " decoration=\"zigzag\",\n", + " line_width=2,\n", + " color=\"brown!70\",\n", + ")\n", + "\n", + "# Springs connecting ground to ceiling\n", + "fig.draw(\n", + " [(1, 0), (1, 3)],\n", + " options=[\"decorate\"],\n", + " decoration=\"coil\",\n", + " line_width=2,\n", + " color=\"gray!70\",\n", + ")\n", + "fig.draw(\n", + " [(7, 0), (7, 3)],\n", + " options=[\"decorate\"],\n", + " decoration=\"coil\",\n", + " line_width=2,\n", + " color=\"gray!70\",\n", + ")\n", + "\n", + "# Snake rope across the top\n", + "fig.draw(\n", + " [(1, 3), (7, 3)],\n", + " options=[\"decorate\"],\n", + " decoration=\"snake\",\n", + " line_width=2.5,\n", + " color=\"orange!80\",\n", + ")\n", + "\n", + "# Bumpy road in the middle\n", + "fig.draw(\n", + " [(2, 1.5), (6, 1.5)],\n", + " options=[\"decorate\"],\n", + " decoration=\"bumps\",\n", + " line_width=2,\n", + " color=\"teal\",\n", + ")\n", + "\n", + "# Anchor nodes\n", + "for x in [1, 7]:\n", + " fig.node(\n", + " x,\n", + " 3,\n", + " shape=\"circle\",\n", + " fill=\"gray!50\",\n", + " draw=\"gray\",\n", + " minimum_size=\"0.3cm\",\n", + " )\n", + " fig.node(\n", + " x,\n", + " 0,\n", + " shape=\"rectangle\",\n", + " fill=\"brown!40\",\n", + " draw=\"brown\",\n", + " minimum_width=\"0.4cm\",\n", + " minimum_height=\"0.2cm\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_20_decorations.qmd b/tutorials/advanced/tutorial_20_decorations.qmd deleted file mode 100644 index f932ef6..0000000 --- a/tutorials/advanced/tutorial_20_decorations.qmd +++ /dev/null @@ -1,306 +0,0 @@ ---- -title: "Decorations" -jupyter: python3 ---- - -TikZ decorations transform the appearance of paths: a straight line can become a -zigzag, a snake wave, a coil spring, or a brace bracket. This tutorial covers -the decorations available through `tikzfigure`'s `decoration` parameter: - -- `"zigzag"`, `"snake"`, `"coil"`, `"bumps"` — path morphing decorations -- side-by-side gallery of all morphing decorations -- `"brace"` — bracket annotations (requires a second library) -- combining decorations with colors and line widths - -**Important:** You must always pass `options=["decorate"]` alongside -`decoration=` to actually activate the decoration. The `decoration=` kwarg sets -*which* decoration to use; `decorate` is the flag that switches it on. - -All morphing decoration examples require -`document_setup=r"\usetikzlibrary{decorations.pathmorphing}"`. The brace -decoration additionally requires `\usetikzlibrary{decorations.pathreplacing}`. - -```{python} -from tikzfigure import TikzFigure, decorations, shapes, styles -``` - -## `decoration="zigzag"` - -A zigzag path oscillates sharply between two extremes along the path direction. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{decorations.pathmorphing}", -) - -fig.draw( - [(0, 0), (6, 0)], - options=[styles.decorate], - decoration=decorations.zigzag, - line_width=2, - color="red", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `decoration="snake"` - -A snake decoration produces a smooth sinusoidal wave — like a zigzag but with -rounded peaks. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{decorations.pathmorphing}", -) - -fig.draw( - [(0, 0), (6, 0)], - options=[styles.decorate], - decoration=decorations.snake, - line_width=2, - color="blue", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `decoration="coil"` - -A coil decoration draws a helical spring along the path. It works well for -physics diagrams. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{decorations.pathmorphing}", -) - -fig.draw( - [(0, 0), (6, 0)], - options=[styles.decorate], - decoration=decorations.coil, - line_width=1.5, - color="teal", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `decoration="bumps"` - -Bumps produce smooth humps along the path — similar to a snake but with the -oscillation only on one side. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{decorations.pathmorphing}", -) - -fig.draw( - [(0, 0), (6, 0)], - options=[styles.decorate], - decoration=decorations.bumps, - line_width=2, - color="orange", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Side-by-side gallery of morphing decorations - -Showing all four morphing decorations together, each with a label, makes it easy -to choose the right one for your diagram. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{decorations.pathmorphing}", -) - -decoration_specs = [ - (decorations.zigzag, "red"), - (decorations.snake, "blue"), - (decorations.coil, "teal"), - (decorations.bumps, "orange"), -] - -for i, (dec, color) in enumerate(decoration_specs): - y = -i * 1.4 - fig.draw( - [(0, y), (5, y)], - options=[styles.decorate], - decoration=dec, - line_width=2, - color=color, - ) - fig.node( - 5.3, - y, - content=f"\\texttt{{{dec}}}", - draw="none", - anchor="west", - font=r"\small", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `decoration="brace"`: bracket annotations - -The `"brace"` decoration draws a curly bracket spanning two points. It requires -the `decorations.pathreplacing` library. - -Use a brace to annotate the width or height of a region in your diagram. Draw -the brace from right to left (or bottom to top) so the bracket opens toward the -region. - -```{python} -fig = TikzFigure( - document_setup=( - r"\usetikzlibrary{decorations.pathmorphing}" - "\n" - r"\usetikzlibrary{decorations.pathreplacing}" - ), -) - -# The region being annotated -fig.node( - (2, 1), - shape=shapes.rectangle, - fill="blue!15", - draw="blue!50", - minimum_width="4cm", - minimum_height="1.5cm", - content="annotated region", -) - -# Horizontal brace below the rectangle (right-to-left so the brace opens upward) -fig.draw( - [(4, 0.1), (0, 0.1)], - options=[styles.decorate], - decoration=decorations.brace, - line_width=1.5, - color="blue!70", -) -fig.node( - (2, -0.45), - content="4 cm wide", - draw="none", - color="blue!70", - font=r"\small", -) - -# Vertical brace on the right side (bottom-to-top so the brace opens rightward) -fig.draw( - [(4.15, 0.25), (4.15, 1.75)], - options=[styles.decorate], - decoration=decorations.brace, - line_width=1.5, - color="red!70", -) -fig.node( - (5.0, 1.0), - content="1.5 cm", - draw="none", - color="red!70", - font=r"\small", - anchor="west", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Combining decorations with colors and line widths - -Decorations become more expressive when combined with thick lines and vivid -colors. Here is a physics-style figure with springs, a snake rope, and a bumpy -road. - -```{python} -fig = TikzFigure( - document_setup=( - r"\usetikzlibrary{decorations.pathmorphing}" - "\n" - r"\usetikzlibrary{decorations.pathreplacing}" - ), -) - -# Ground line (zigzag = rough terrain) -fig.draw( - [(0, 0), (8, 0)], - options=["decorate"], - decoration="zigzag", - line_width=2, - color="brown!70", -) - -# Springs connecting ground to ceiling -fig.draw( - [(1, 0), (1, 3)], - options=["decorate"], - decoration="coil", - line_width=2, - color="gray!70", -) -fig.draw( - [(7, 0), (7, 3)], - options=["decorate"], - decoration="coil", - line_width=2, - color="gray!70", -) - -# Snake rope across the top -fig.draw( - [(1, 3), (7, 3)], - options=["decorate"], - decoration="snake", - line_width=2.5, - color="orange!80", -) - -# Bumpy road in the middle -fig.draw( - [(2, 1.5), (6, 1.5)], - options=["decorate"], - decoration="bumps", - line_width=2, - color="teal", -) - -# Anchor nodes -for x in [1, 7]: - fig.node( - x, - 3, - shape="circle", - fill="gray!50", - draw="gray", - minimum_size="0.3cm", - ) - fig.node( - x, - 0, - shape="rectangle", - fill="brown!40", - draw="brown", - minimum_width="0.4cm", - minimum_height="0.2cm", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} diff --git a/tutorials/advanced/tutorial_21_advanced_styling.ipynb b/tutorials/advanced/tutorial_21_advanced_styling.ipynb new file mode 100644 index 0000000..a1dca30 --- /dev/null +++ b/tutorials/advanced/tutorial_21_advanced_styling.ipynb @@ -0,0 +1,694 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Advanced Styling\n", + "\n", + "This tutorial covers advanced visual styling options that go beyond basic colors\n", + "and line widths:\n", + "\n", + "- `double` and `double_distance` for double-bordered nodes and paths\n", + "- `dash_pattern` and `dash_phase` for custom dash sequences\n", + "- `line_cap` styles: butt, rect, round\n", + "- `line_join` styles: miter, bevel, round\n", + "- shadow effects: `shadow_xshift`, `shadow_yshift`, and the `drop shadow` style\n", + "- `pin` annotations: attaching small labels to nodes\n", + "- special shapes: star, regular polygon, cylinder" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure, arrows" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## `double` and `double_distance`: double borders\n", + "\n", + "The `double` parameter draws a second parallel stroke, creating a double-border\n", + "effect. `double_distance` controls the gap between the two strokes. Setting\n", + "`double=\"white\"` with a colored outer stroke gives a classic railway-line look." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Double-bordered node with white gap (railway style)\n", + "fig.node(\n", + " (0, 0),\n", + " shape=\"rectangle\",\n", + " draw=\"black\",\n", + " fill=\"gray!10\",\n", + " double=\"white\",\n", + " double_distance=\"3pt\",\n", + " minimum_width=\"2.5cm\",\n", + " minimum_height=\"1cm\",\n", + " content=\"double=white\",\n", + " line_width=1.5,\n", + ")\n", + "\n", + "# Double-bordered with black inner line\n", + "fig.node(\n", + " (4, 0),\n", + " shape=\"rectangle\",\n", + " draw=\"blue!70\",\n", + " fill=\"blue!10\",\n", + " double=\"blue!70\",\n", + " double_distance=\"2pt\",\n", + " minimum_width=\"2.5cm\",\n", + " minimum_height=\"1cm\",\n", + " content=\"double=blue\",\n", + " line_width=1,\n", + ")\n", + "\n", + "# Double on a circle\n", + "fig.node(\n", + " (8, 0),\n", + " shape=\"circle\",\n", + " draw=\"red!70\",\n", + " fill=\"red!10\",\n", + " double=\"red!70\",\n", + " double_distance=\"2pt\",\n", + " minimum_size=\"1.5cm\",\n", + " content=\"circle\",\n", + " line_width=1,\n", + ")\n", + "\n", + "# Double on a path (railway track)\n", + "fig.draw(\n", + " [(0, -1.8), (8, -1.8)],\n", + " double=\"white\",\n", + " double_distance=\"4pt\",\n", + " line_width=2,\n", + " color=\"black\",\n", + ")\n", + "fig.node(\n", + " (4, -2.3),\n", + " content=\"railway track (double path)\",\n", + " draw=\"none\",\n", + " font=r\"\\small\\itshape\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## `dash_pattern` and `dash_phase`: custom dash sequences\n", + "\n", + "`dash_pattern` accepts a TikZ dash specification string such as\n", + "`\"on 5pt off 3pt\"`. You can combine multiple on/off segments for complex\n", + "patterns. `dash_phase` shifts the starting position within the pattern." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "patterns = [\n", + " (\"on 3pt off 2pt\", 0, \"on 3 off 2\"),\n", + " (\"on 1pt off 1pt on 5pt off 1pt\", 0, \"on 1 off 1 on 5 off 1\"),\n", + " (\"on 6pt off 2pt on 1pt off 2pt\", 0, \"on 6 off 2 on 1 off 2\"),\n", + " (\"on 2pt off 2pt\", 3, \"on 2 off 2, phase=3pt\"),\n", + "]\n", + "\n", + "for i, (dp, phase, label) in enumerate(patterns):\n", + " y = -i * 1.3\n", + " fig.draw(\n", + " [(0, y), (6, y)],\n", + " dash_pattern=dp,\n", + " dash_phase=f\"{phase}pt\" if phase else None,\n", + " line_width=2,\n", + " color=\"black\",\n", + " )\n", + " fig.node(\n", + " 6.2,\n", + " y,\n", + " content=f\"\\\\texttt{{{label}}}\",\n", + " draw=\"none\",\n", + " anchor=\"west\",\n", + " font=r\"\\tiny\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## `line_cap`: end-cap styles\n", + "\n", + "`line_cap` controls how the ends of thick open paths are rendered. `\"butt\"` cuts\n", + "flat at the endpoint, `\"rect\"` extends a half-linewidth rectangle past the\n", + "endpoint, and `\"round\"` caps with a semicircle." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "caps = [\"butt\", \"rect\", \"round\"]\n", + "\n", + "for i, cap in enumerate(caps):\n", + " y = -i * 1.5\n", + " fig.draw(\n", + " [(0.5, y), (4.5, y)],\n", + " line_cap=cap,\n", + " line_width=12,\n", + " color=\"blue!60\",\n", + " )\n", + " # Thin reference line to show exact endpoint\n", + " fig.draw(\n", + " [(0.5, y - 0.5), (0.5, y + 0.5)],\n", + " line_width=0.5,\n", + " color=\"red\",\n", + " )\n", + " fig.draw(\n", + " [(4.5, y - 0.5), (4.5, y + 0.5)],\n", + " line_width=0.5,\n", + " color=\"red\",\n", + " )\n", + " fig.node(\n", + " 5.2,\n", + " y,\n", + " content=f\"\\\\texttt{{{cap}}}\",\n", + " draw=\"none\",\n", + " anchor=\"west\",\n", + " font=r\"\\small\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## `line_join`: corner join styles\n", + "\n", + "`line_join` controls how two path segments meet at a corner. `\"miter\"` extends\n", + "to a sharp point, `\"bevel\"` cuts the corner flat, and `\"round\"` rounds it." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "joins = [\"miter\", \"bevel\", \"round\"]\n", + "\n", + "for i, join in enumerate(joins):\n", + " x0 = i * 4\n", + " zigzag = [(x0 + 0, 0), (x0 + 1, 2), (x0 + 2, 0), (x0 + 3, 2)]\n", + " fig.draw(\n", + " zigzag,\n", + " line_join=join,\n", + " line_width=8,\n", + " color=\"teal!70\",\n", + " )\n", + " fig.node(\n", + " x0 + 1.5,\n", + " -0.6,\n", + " content=f\"\\\\texttt{{{join}}}\",\n", + " draw=\"none\",\n", + " font=r\"\\small\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## Shadow effects\n", + "\n", + "The `shadows` library adds drop shadows to nodes. Control the shadow offset with\n", + "`shadow_xshift` / `shadow_yshift`. The shadow color is a parameter of the\n", + "`drop shadow` style itself (`drop shadow={fill=}`), not a standalone\n", + "TikZ key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{shadows}\",\n", + ")\n", + "\n", + "shadow_specs = [\n", + " (\"gray!60\", \"3pt\", \"-3pt\", \"gray shadow\"),\n", + " (\"blue!40\", \"4pt\", \"-2pt\", \"blue shadow\"),\n", + " (\"red!50\", \"2pt\", \"-4pt\", \"red shadow\"),\n", + " (\"black!30\", \"5pt\", \"-5pt\", \"deep shadow\"),\n", + "]\n", + "\n", + "for i, (sc, sx, sy, label) in enumerate(shadow_specs):\n", + " fig.node(\n", + " (i * 3, 0),\n", + " shape=\"rectangle\",\n", + " fill=\"white\",\n", + " draw=\"gray!70\",\n", + " minimum_width=\"2cm\",\n", + " minimum_height=\"1cm\",\n", + " content=label,\n", + " options=[f\"drop shadow={{fill={sc}}}\"], # activate the drop shadow style\n", + " shadow_xshift=sx,\n", + " shadow_yshift=sy,\n", + " font=r\"\\small\",\n", + " rounded_corners=\"3pt\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## `pin` annotations\n", + "\n", + "The `pin` option attaches a small labeled pin to a node. The value is a TikZ pin\n", + "specification string: `\"above:text\"`, `\"right:text\"`, `\"below left:text\"`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Three nodes with pins in different directions\n", + "nA = fig.node(\n", + " (0, 0),\n", + " label=\"A\",\n", + " shape=\"circle\",\n", + " fill=\"orange!40\",\n", + " minimum_size=\"1cm\",\n", + " content=\"A\",\n", + " pin=\"above:measured here\",\n", + ")\n", + "\n", + "nB = fig.node(\n", + " (4, 0),\n", + " label=\"B\",\n", + " shape=\"circle\",\n", + " fill=\"teal!40\",\n", + " minimum_size=\"1cm\",\n", + " content=\"B\",\n", + " pin=\"right:peak value\",\n", + ")\n", + "\n", + "nC = fig.node(\n", + " (2, 3),\n", + " label=\"C\",\n", + " shape=\"circle\",\n", + " fill=\"purple!40\",\n", + " minimum_size=\"1cm\",\n", + " content=\"C\",\n", + " pin=\"below left:origin\",\n", + ")\n", + "\n", + "fig.draw([nA, nB], arrows=arrows.forward)\n", + "fig.draw([nA, nC], arrows=arrows.forward)\n", + "fig.draw([nB, nC], arrows=arrows.forward)\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "## Special shapes: star, regular polygon, cylinder\n", + "\n", + "The `shapes.geometric` library provides additional node shapes beyond the\n", + "built-in `circle`, `rectangle`, and `ellipse`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(\n", + " document_setup=r\"\\usetikzlibrary{shapes.geometric}\",\n", + ")\n", + "\n", + "# Five-pointed star\n", + "fig.node(\n", + " (0, 0),\n", + " shape=\"star\",\n", + " star_points=5,\n", + " star_point_ratio=0.45,\n", + " fill=\"yellow!70\",\n", + " draw=\"orange\",\n", + " minimum_size=\"2cm\",\n", + " line_width=1.5,\n", + " content=\"star\",\n", + " font=r\"\\small\",\n", + ")\n", + "\n", + "# Six-pointed star\n", + "fig.node(\n", + " (3, 0),\n", + " shape=\"star\",\n", + " star_points=6,\n", + " star_point_ratio=0.5,\n", + " fill=\"cyan!50\",\n", + " draw=\"teal\",\n", + " minimum_size=\"2cm\",\n", + " line_width=1.5,\n", + " content=\"6-pt\",\n", + " font=r\"\\small\",\n", + ")\n", + "\n", + "# Regular hexagon\n", + "fig.node(\n", + " (6, 0),\n", + " shape=\"regular polygon\",\n", + " regular_polygon_sides=6,\n", + " fill=\"green!40\",\n", + " draw=\"green!70!black\",\n", + " minimum_size=\"2cm\",\n", + " line_width=1.5,\n", + " content=\"hex\",\n", + " font=r\"\\small\",\n", + ")\n", + "\n", + "# Regular triangle (3 sides)\n", + "fig.node(\n", + " (9, 0),\n", + " shape=\"regular polygon\",\n", + " regular_polygon_sides=3,\n", + " fill=\"red!30\",\n", + " draw=\"red!70\",\n", + " minimum_size=\"2cm\",\n", + " line_width=1.5,\n", + " content=\"tri\",\n", + " font=r\"\\small\",\n", + ")\n", + "\n", + "# Cylinder\n", + "fig.node(\n", + " (12, 0),\n", + " shape=\"cylinder\",\n", + " fill=\"purple!30\",\n", + " draw=\"purple!70\",\n", + " minimum_width=\"1.4cm\",\n", + " minimum_height=\"2cm\",\n", + " line_width=1.5,\n", + " content=\"DB\",\n", + " font=r\"\\small\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_21_advanced_styling.qmd b/tutorials/advanced/tutorial_21_advanced_styling.qmd deleted file mode 100644 index 55a11c5..0000000 --- a/tutorials/advanced/tutorial_21_advanced_styling.qmd +++ /dev/null @@ -1,374 +0,0 @@ ---- -title: "Advanced Styling" -jupyter: python3 ---- - -This tutorial covers advanced visual styling options that go beyond basic colors -and line widths: - -- `double` and `double_distance` for double-bordered nodes and paths -- `dash_pattern` and `dash_phase` for custom dash sequences -- `line_cap` styles: butt, rect, round -- `line_join` styles: miter, bevel, round -- shadow effects: `shadow_xshift`, `shadow_yshift`, and the `drop shadow` style -- `pin` annotations: attaching small labels to nodes -- special shapes: star, regular polygon, cylinder - -```{python} -from tikzfigure import TikzFigure, arrows -``` - -## `double` and `double_distance`: double borders - -The `double` parameter draws a second parallel stroke, creating a double-border -effect. `double_distance` controls the gap between the two strokes. Setting -`double="white"` with a colored outer stroke gives a classic railway-line look. - -```{python} -fig = TikzFigure() - -# Double-bordered node with white gap (railway style) -fig.node( - (0, 0), - shape="rectangle", - draw="black", - fill="gray!10", - double="white", - double_distance="3pt", - minimum_width="2.5cm", - minimum_height="1cm", - content="double=white", - line_width=1.5, -) - -# Double-bordered with black inner line -fig.node( - (4, 0), - shape="rectangle", - draw="blue!70", - fill="blue!10", - double="blue!70", - double_distance="2pt", - minimum_width="2.5cm", - minimum_height="1cm", - content="double=blue", - line_width=1, -) - -# Double on a circle -fig.node( - (8, 0), - shape="circle", - draw="red!70", - fill="red!10", - double="red!70", - double_distance="2pt", - minimum_size="1.5cm", - content="circle", - line_width=1, -) - -# Double on a path (railway track) -fig.draw( - [(0, -1.8), (8, -1.8)], - double="white", - double_distance="4pt", - line_width=2, - color="black", -) -fig.node( - (4, -2.3), - content="railway track (double path)", - draw="none", - font=r"\small\itshape", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `dash_pattern` and `dash_phase`: custom dash sequences - -`dash_pattern` accepts a TikZ dash specification string such as -`"on 5pt off 3pt"`. You can combine multiple on/off segments for complex -patterns. `dash_phase` shifts the starting position within the pattern. - -```{python} -fig = TikzFigure() - -patterns = [ - ("on 3pt off 2pt", 0, "on 3 off 2"), - ("on 1pt off 1pt on 5pt off 1pt", 0, "on 1 off 1 on 5 off 1"), - ("on 6pt off 2pt on 1pt off 2pt", 0, "on 6 off 2 on 1 off 2"), - ("on 2pt off 2pt", 3, "on 2 off 2, phase=3pt"), -] - -for i, (dp, phase, label) in enumerate(patterns): - y = -i * 1.3 - fig.draw( - [(0, y), (6, y)], - dash_pattern=dp, - dash_phase=f"{phase}pt" if phase else None, - line_width=2, - color="black", - ) - fig.node( - 6.2, - y, - content=f"\\texttt{{{label}}}", - draw="none", - anchor="west", - font=r"\tiny", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `line_cap`: end-cap styles - -`line_cap` controls how the ends of thick open paths are rendered. `"butt"` cuts -flat at the endpoint, `"rect"` extends a half-linewidth rectangle past the -endpoint, and `"round"` caps with a semicircle. - -```{python} -fig = TikzFigure() - -caps = ["butt", "rect", "round"] - -for i, cap in enumerate(caps): - y = -i * 1.5 - fig.draw( - [(0.5, y), (4.5, y)], - line_cap=cap, - line_width=12, - color="blue!60", - ) - # Thin reference line to show exact endpoint - fig.draw( - [(0.5, y - 0.5), (0.5, y + 0.5)], - line_width=0.5, - color="red", - ) - fig.draw( - [(4.5, y - 0.5), (4.5, y + 0.5)], - line_width=0.5, - color="red", - ) - fig.node( - 5.2, - y, - content=f"\\texttt{{{cap}}}", - draw="none", - anchor="west", - font=r"\small", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `line_join`: corner join styles - -`line_join` controls how two path segments meet at a corner. `"miter"` extends -to a sharp point, `"bevel"` cuts the corner flat, and `"round"` rounds it. - -```{python} -fig = TikzFigure() - -joins = ["miter", "bevel", "round"] - -for i, join in enumerate(joins): - x0 = i * 4 - zigzag = [(x0 + 0, 0), (x0 + 1, 2), (x0 + 2, 0), (x0 + 3, 2)] - fig.draw( - zigzag, - line_join=join, - line_width=8, - color="teal!70", - ) - fig.node( - x0 + 1.5, - -0.6, - content=f"\\texttt{{{join}}}", - draw="none", - font=r"\small", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Shadow effects - -The `shadows` library adds drop shadows to nodes. Control the shadow offset with -`shadow_xshift` / `shadow_yshift`. The shadow color is a parameter of the -`drop shadow` style itself (`drop shadow={fill=}`), not a standalone -TikZ key. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{shadows}", -) - -shadow_specs = [ - ("gray!60", "3pt", "-3pt", "gray shadow"), - ("blue!40", "4pt", "-2pt", "blue shadow"), - ("red!50", "2pt", "-4pt", "red shadow"), - ("black!30", "5pt", "-5pt", "deep shadow"), -] - -for i, (sc, sx, sy, label) in enumerate(shadow_specs): - fig.node( - (i * 3, 0), - shape="rectangle", - fill="white", - draw="gray!70", - minimum_width="2cm", - minimum_height="1cm", - content=label, - options=[f"drop shadow={{fill={sc}}}"], # activate the drop shadow style - shadow_xshift=sx, - shadow_yshift=sy, - font=r"\small", - rounded_corners="3pt", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## `pin` annotations - -The `pin` option attaches a small labeled pin to a node. The value is a TikZ pin -specification string: `"above:text"`, `"right:text"`, `"below left:text"`, etc. - -```{python} -fig = TikzFigure() - -# Three nodes with pins in different directions -nA = fig.node( - (0, 0), - label="A", - shape="circle", - fill="orange!40", - minimum_size="1cm", - content="A", - pin="above:measured here", -) - -nB = fig.node( - (4, 0), - label="B", - shape="circle", - fill="teal!40", - minimum_size="1cm", - content="B", - pin="right:peak value", -) - -nC = fig.node( - (2, 3), - label="C", - shape="circle", - fill="purple!40", - minimum_size="1cm", - content="C", - pin="below left:origin", -) - -fig.draw([nA, nB], arrows=arrows.forward) -fig.draw([nA, nC], arrows=arrows.forward) -fig.draw([nB, nC], arrows=arrows.forward) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Special shapes: star, regular polygon, cylinder - -The `shapes.geometric` library provides additional node shapes beyond the -built-in `circle`, `rectangle`, and `ellipse`. - -```{python} -fig = TikzFigure( - document_setup=r"\usetikzlibrary{shapes.geometric}", -) - -# Five-pointed star -fig.node( - (0, 0), - shape="star", - star_points=5, - star_point_ratio=0.45, - fill="yellow!70", - draw="orange", - minimum_size="2cm", - line_width=1.5, - content="star", - font=r"\small", -) - -# Six-pointed star -fig.node( - (3, 0), - shape="star", - star_points=6, - star_point_ratio=0.5, - fill="cyan!50", - draw="teal", - minimum_size="2cm", - line_width=1.5, - content="6-pt", - font=r"\small", -) - -# Regular hexagon -fig.node( - (6, 0), - shape="regular polygon", - regular_polygon_sides=6, - fill="green!40", - draw="green!70!black", - minimum_size="2cm", - line_width=1.5, - content="hex", - font=r"\small", -) - -# Regular triangle (3 sides) -fig.node( - (9, 0), - shape="regular polygon", - regular_polygon_sides=3, - fill="red!30", - draw="red!70", - minimum_size="2cm", - line_width=1.5, - content="tri", - font=r"\small", -) - -# Cylinder -fig.node( - (12, 0), - shape="cylinder", - fill="purple!30", - draw="purple!70", - minimum_width="1.4cm", - minimum_height="2cm", - line_width=1.5, - content="DB", - font=r"\small", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} diff --git a/tutorials/advanced/tutorial_24_spy_library.ipynb b/tutorials/advanced/tutorial_24_spy_library.ipynb new file mode 100644 index 0000000..35d04ae --- /dev/null +++ b/tutorials/advanced/tutorial_24_spy_library.ipynb @@ -0,0 +1,571 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Spy Library\n", + "\n", + "This tutorial introduces the dedicated spy API for inset zoom views and\n", + "magnified regions.\n", + "\n", + "The examples now start simple and then build up:\n", + "\n", + "1. one basic spy inset on a small drawing\n", + "2. custom spy-in node shapes and sizes\n", + "3. figure-wide defaults with `configure_spy_scope(...)`\n", + "4. the classic pgfplots spy example from the TikZ manual\n", + "5. local spy defaults with `spy_scope(...)`\n", + "6. nested spy scopes with a custom connection path" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure, colors, shapes" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## A minimal spy example\n", + "\n", + "The smallest useful pattern is just a drawing plus one spy inset. `fig.spy(...)`\n", + "automatically enables a basic `spy scope` on the figure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "fig.draw([(-0.3, 0), (2.8, 0)], color=colors.black, line_width=\"0.8pt\")\n", + "fig.draw([(0, -0.8), (0, 1.2)], color=colors.lightgray)\n", + "fig.draw([(1.2, -0.6), (1.2, 0.9)], color=colors.lightgray)\n", + "fig.draw([(0, 0), (2.2, 0.8)], color=colors.blue, line_width=\"1.1pt\")\n", + "\n", + "fig.spy(\n", + " on=(1.2, 0.45),\n", + " at=(4.0, 0.9),\n", + " magnification=3,\n", + " size=\"1.3cm\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "This already gives you the core spy behavior:\n", + "\n", + "- `on=...` picks the center of the region to magnify\n", + "- `at=...` places the inset node\n", + "- `magnification=` controls zoom\n", + "- `size=` controls the inset size\n", + "\n", + "## Customizing the spy-in node\n", + "\n", + "You can style the spy-in node just like a normal TikZ node. Here we use a small\n", + "zig-zag path and highlight two different details." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "ridge = [(-0.2, 0), (0.8, 0.9), (1.5, 0.3), (2.3, 1.1), (3.0, 0.5)]\n", + "fig.draw(ridge, color=colors.black, line_width=\"1pt\")\n", + "\n", + "for point in ridge:\n", + " fig.node(\n", + " point,\n", + " shape=\"circle\",\n", + " minimum_size=\"4pt\",\n", + " inner_sep=\"0pt\",\n", + " fill=colors.orange.mix(percent=20),\n", + " draw=colors.black,\n", + " )\n", + "\n", + "fig.spy(\n", + " on=(0.8, 0.9),\n", + " at=(4.4, 1.1),\n", + " options=[colors.red],\n", + " magnification=4,\n", + " size=\"1.35cm\",\n", + " connect_spies=True,\n", + " node_options=[shapes.circle],\n", + " node_style={\"draw\": colors.red, \"line_width\": \"0.8pt\", \"fill\": colors.white},\n", + ")\n", + "\n", + "fig.spy(\n", + " on=(2.3, 1.1),\n", + " at=(4.3, -0.2),\n", + " options=[colors.blue],\n", + " magnification=5,\n", + " width=\"1.8cm\",\n", + " height=\"1.0cm\",\n", + " node_label=\"peak detail\",\n", + " node_options=[shapes.rectangle],\n", + " node_style={\n", + " \"draw\": colors.blue,\n", + " \"rounded_corners\": \"2pt\",\n", + " \"fill\": colors.white,\n", + " },\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "This adds a few new pieces:\n", + "\n", + "- `connect_spies=True` draws the default connector\n", + "- `node_options=[...]` controls flag-style node options like shapes\n", + "- `node_style={...}` controls keyword-style node options like `draw=...`\n", + "- `node_label=\"peak detail\"` names the spy-in node\n", + "- `width=` and `height=` let you use non-square spy nodes\n", + "\n", + "## Figure-wide defaults with `configure_spy_scope(...)`\n", + "\n", + "If several spies in the same figure should share defaults, configure the\n", + "tikzpicture-level spy scope once and then use `fig.spy(...)` repeatedly.\n", + "\n", + "This simple city-block map uses the same inset style for multiple locations." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "fig.configure_spy_scope(\n", + " mode=\"outlines\",\n", + " options=[shapes.rectangle],\n", + " magnification=4,\n", + " width=\"1.9cm\",\n", + " height=\"1.0cm\",\n", + " connect_spies=True,\n", + " every_spy_in_node_style={\n", + " \"draw\": colors.green.mix(colors.black, percent=35),\n", + " \"fill\": colors.white,\n", + " \"rounded_corners\": \"2pt\",\n", + " },\n", + " every_spy_on_node_style={\n", + " \"draw\": colors.green.mix(colors.black, percent=15),\n", + " \"fill\": colors.green,\n", + " \"fill_opacity\": 0.08,\n", + " },\n", + ")\n", + "\n", + "fig.draw(\n", + " [(-0.2, -0.2), (4.8, -0.2), (4.8, 3.2), (-0.2, 3.2)],\n", + " cycle=True,\n", + " fill=colors.gray.mix(percent=12),\n", + " draw=\"none\",\n", + ")\n", + "\n", + "for x in [1.4, 3.2]:\n", + " fig.draw([(x, -0.2), (x, 3.2)], color=colors.white, line_width=\"10pt\")\n", + "for y in [1.0, 2.2]:\n", + " fig.draw([(-0.2, y), (4.8, y)], color=colors.white, line_width=\"10pt\")\n", + "\n", + "blocks = [\n", + " ((0.15, 0.15), (1.05, 0.75), colors.orange.mix(percent=18), \"cafe\"),\n", + " ((1.75, 0.15), (2.85, 0.75), colors.blue.mix(percent=14), \"library\"),\n", + " ((3.55, 0.15), (4.45, 0.75), colors.red.mix(percent=12), \"gallery\"),\n", + " ((0.15, 1.35), (1.05, 2.95), colors.green.mix(percent=16), \"park\"),\n", + " ((1.75, 1.35), (2.85, 2.95), colors.yellow.mix(percent=16), \"market\"),\n", + " ((3.55, 1.35), (4.45, 2.95), colors.purple.mix(percent=10), \"museum\"),\n", + "]\n", + "\n", + "for lower_left, upper_right, fill_color, label in blocks:\n", + " x0, y0 = lower_left\n", + " x1, y1 = upper_right\n", + " fig.draw(\n", + " [(x0, y0), (x1, y0), (x1, y1), (x0, y1)],\n", + " cycle=True,\n", + " rounded_corners=\"2pt\",\n", + " fill=fill_color,\n", + " draw=fill_color.mix(colors.black, percent=20),\n", + " )\n", + " fig.node(((x0 + x1) / 2, (y0 + y1) / 2), content=label, draw=\"none\", font=r\"\\small\")\n", + "\n", + "fig.spy(on=(0.55, 2.1), at=(6.4, 2.45), options=[colors.green])\n", + "fig.spy(on=(2.3, 2.15), at=(6.4, 0.8), options=[colors.orange])\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "This is a good pattern when you want:\n", + "\n", + "- the same magnification for all spies\n", + "- the same inset shape and border styling\n", + "- the same spy-on styling\n", + "\n", + "## Reproducing the classic pgfplots spy example\n", + "\n", + "Here is the classic pgfplots example from the TikZ manual, reproduced using only\n", + "the Python API." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "fig.configure_spy_scope(\n", + " mode=\"outlines\",\n", + " options=[shapes.rectangle],\n", + " magnification=3,\n", + " connect_spies=True,\n", + ")\n", + "\n", + "ax = fig.axis2d(\n", + " xlim=(-5, 5),\n", + " grid=\"major\",\n", + " options=[\"no markers\"],\n", + " domain=\"-5:5\",\n", + " enlargelimits=\"false\",\n", + ")\n", + "\n", + "ax.add_plot(func=\"x^2\", color=colors.red)\n", + "ax.add_plot(func=\"x^3\", color=colors.blue)\n", + "\n", + "spypoint = ax.coordinate(\"spypoint\", at=\"axis cs:0,0\")\n", + "spyviewer = ax.coordinate(\"spyviewer\", at=\"axis cs:0.5,-90\")\n", + "ax.spy(\n", + " on=spypoint,\n", + " at=spyviewer,\n", + " width=\"6cm\",\n", + " height=\"1cm\",\n", + " node_style={\"fill\": colors.white},\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "This matches the original idea closely:\n", + "\n", + "- `mode=\"outlines\"` maps to `spy using outlines={...}`\n", + "- `options=[shapes.rectangle]` makes the inset rectangular\n", + "- `ax.coordinate(...)` and `ax.spy(...)` keep everything inside the `axis`\n", + " environment\n", + "\n", + "## Local spy defaults with `spy_scope(...)`\n", + "\n", + "Use `spy_scope(...)` when spy settings should only apply to part of the figure.\n", + "This mirrors the TikZ manual more closely than a figure-wide spy scope.\n", + "\n", + "Here the spy scope is used on a small transit-map sketch, and one spy zooms in\n", + "directly on a labeled `Node` object." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "def add_station(scope, position, size=\"3pt\", fill=colors.white, label=None):\n", + " return scope.node(\n", + " position,\n", + " label=label,\n", + " shape=\"circle\",\n", + " minimum_size=size,\n", + " inner_sep=\"0pt\",\n", + " fill=fill,\n", + " draw=colors.black,\n", + " line_width=\"0.3pt\",\n", + " )\n", + "\n", + "\n", + "fig = TikzFigure()\n", + "\n", + "with fig.spy_scope(\n", + " mode=\"outlines\",\n", + " options=[shapes.circle],\n", + " magnification=4,\n", + " size=\"1.5cm\",\n", + " connect_spies=True,\n", + " every_spy_in_node_options=[shapes.circle],\n", + " every_spy_in_node_style={\n", + " \"draw\": colors.orange.mix(colors.black, percent=28),\n", + " \"line_width\": \"0.9pt\",\n", + " \"fill\": colors.white,\n", + " },\n", + " every_spy_on_node_options=[shapes.circle],\n", + " every_spy_on_node_style={\n", + " \"draw\": colors.orange.mix(colors.black, percent=18),\n", + " \"fill\": colors.orange,\n", + " \"fill_opacity\": 0.08,\n", + " },\n", + ") as metro:\n", + " red_line = [(-0.1, -0.5), (0.9, 0.1), (1.9, 0.7), (3.0, 1.2), (4.0, 1.55)]\n", + " blue_line = [(0.1, 1.35), (1.1, 0.95), (2.0, 0.65), (3.1, 0.1), (4.1, -0.4)]\n", + "\n", + " for line_points, line_color in [(red_line, colors.red), (blue_line, colors.blue)]:\n", + " metro.draw(line_points, color=line_color, line_width=\"2.6pt\")\n", + " for point in line_points:\n", + " add_station(metro, point)\n", + "\n", + " central = add_station(\n", + " metro,\n", + " (2.0, 0.65),\n", + " size=\"5pt\",\n", + " fill=colors.yellow.mix(percent=18),\n", + " label=\"central_hub\",\n", + " )\n", + " metro.node((2.0, 0.2), content=\"Central\", draw=\"none\", font=r\"\\small\\bfseries\")\n", + "\n", + " metro.spy(\n", + " on=central,\n", + " at=(5.7, 1.2),\n", + " options=[colors.orange],\n", + " node_label=\"central interchange\",\n", + " )\n", + " metro.spy(\n", + " on=(3.95, 1.5),\n", + " at=(5.7, -0.45),\n", + " options=[colors.blue],\n", + " width=\"1.8cm\",\n", + " height=\"1.0cm\",\n", + " node_options=[shapes.rectangle],\n", + " node_style={\n", + " \"draw\": colors.blue,\n", + " \"rounded_corners\": \"2pt\",\n", + " \"fill\": colors.white,\n", + " },\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "The `outlines` preset gives a nice default look:\n", + "\n", + "- the spy-in node is drawn prominently\n", + "- the spy-on node is outlined more lightly\n", + "- both are easy to recolor and reshape\n", + "- `on=` can target a `Node` directly, not just a coordinate tuple" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_24_spy_library.qmd b/tutorials/advanced/tutorial_24_spy_library.qmd deleted file mode 100644 index 5075e2c..0000000 --- a/tutorials/advanced/tutorial_24_spy_library.qmd +++ /dev/null @@ -1,331 +0,0 @@ ---- -title: "Spy Library" -jupyter: python3 ---- - -This tutorial introduces the dedicated spy API for inset zoom views and -magnified regions. - -The examples now start simple and then build up: - -1. one basic spy inset on a small drawing -2. custom spy-in node shapes and sizes -3. figure-wide defaults with `configure_spy_scope(...)` -4. the classic pgfplots spy example from the TikZ manual -5. local spy defaults with `spy_scope(...)` -6. nested spy scopes with a custom connection path - -```{python} -import math - -from tikzfigure import TikzFigure, colors, shapes -``` - -## A minimal spy example - -The smallest useful pattern is just a drawing plus one spy inset. `fig.spy(...)` -automatically enables a basic `spy scope` on the figure. - -```{python} -fig = TikzFigure() - -fig.draw([(-0.3, 0), (2.8, 0)], color=colors.black, line_width="0.8pt") -fig.draw([(0, -0.8), (0, 1.2)], color=colors.lightgray) -fig.draw([(1.2, -0.6), (1.2, 0.9)], color=colors.lightgray) -fig.draw([(0, 0), (2.2, 0.8)], color=colors.blue, line_width="1.1pt") - -fig.spy( - on=(1.2, 0.45), - at=(4.0, 0.9), - magnification=3, - size="1.3cm", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -This already gives you the core spy behavior: - -- `on=...` picks the center of the region to magnify -- `at=...` places the inset node -- `magnification=` controls zoom -- `size=` controls the inset size - -## Customizing the spy-in node - -You can style the spy-in node just like a normal TikZ node. Here we use a small -zig-zag path and highlight two different details. - -```{python} -fig = TikzFigure() - -ridge = [(-0.2, 0), (0.8, 0.9), (1.5, 0.3), (2.3, 1.1), (3.0, 0.5)] -fig.draw(ridge, color=colors.black, line_width="1pt") - -for point in ridge: - fig.node( - point, - shape="circle", - minimum_size="4pt", - inner_sep="0pt", - fill=colors.orange.mix(percent=20), - draw=colors.black, - ) - -fig.spy( - on=(0.8, 0.9), - at=(4.4, 1.1), - options=[colors.red], - magnification=4, - size="1.35cm", - connect_spies=True, - node_options=[shapes.circle], - node_style={"draw": colors.red, "line_width": "0.8pt", "fill": colors.white}, -) - -fig.spy( - on=(2.3, 1.1), - at=(4.3, -0.2), - options=[colors.blue], - magnification=5, - width="1.8cm", - height="1.0cm", - node_label="peak detail", - node_options=[shapes.rectangle], - node_style={ - "draw": colors.blue, - "rounded_corners": "2pt", - "fill": colors.white, - }, -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -This adds a few new pieces: - -- `connect_spies=True` draws the default connector -- `node_options=[...]` controls flag-style node options like shapes -- `node_style={...}` controls keyword-style node options like `draw=...` -- `node_label="peak detail"` names the spy-in node -- `width=` and `height=` let you use non-square spy nodes - -## Figure-wide defaults with `configure_spy_scope(...)` - -If several spies in the same figure should share defaults, configure the -tikzpicture-level spy scope once and then use `fig.spy(...)` repeatedly. - -This simple city-block map uses the same inset style for multiple locations. - -```{python} -fig = TikzFigure() - -fig.configure_spy_scope( - mode="outlines", - options=[shapes.rectangle], - magnification=4, - width="1.9cm", - height="1.0cm", - connect_spies=True, - every_spy_in_node_style={ - "draw": colors.green.mix(colors.black, percent=35), - "fill": colors.white, - "rounded_corners": "2pt", - }, - every_spy_on_node_style={ - "draw": colors.green.mix(colors.black, percent=15), - "fill": colors.green, - "fill_opacity": 0.08, - }, -) - -fig.draw( - [(-0.2, -0.2), (4.8, -0.2), (4.8, 3.2), (-0.2, 3.2)], - cycle=True, - fill=colors.gray.mix(percent=12), - draw="none", -) - -for x in [1.4, 3.2]: - fig.draw([(x, -0.2), (x, 3.2)], color=colors.white, line_width="10pt") -for y in [1.0, 2.2]: - fig.draw([(-0.2, y), (4.8, y)], color=colors.white, line_width="10pt") - -blocks = [ - ((0.15, 0.15), (1.05, 0.75), colors.orange.mix(percent=18), "cafe"), - ((1.75, 0.15), (2.85, 0.75), colors.blue.mix(percent=14), "library"), - ((3.55, 0.15), (4.45, 0.75), colors.red.mix(percent=12), "gallery"), - ((0.15, 1.35), (1.05, 2.95), colors.green.mix(percent=16), "park"), - ((1.75, 1.35), (2.85, 2.95), colors.yellow.mix(percent=16), "market"), - ((3.55, 1.35), (4.45, 2.95), colors.purple.mix(percent=10), "museum"), -] - -for lower_left, upper_right, fill_color, label in blocks: - x0, y0 = lower_left - x1, y1 = upper_right - fig.draw( - [(x0, y0), (x1, y0), (x1, y1), (x0, y1)], - cycle=True, - rounded_corners="2pt", - fill=fill_color, - draw=fill_color.mix(colors.black, percent=20), - ) - fig.node(((x0 + x1) / 2, (y0 + y1) / 2), content=label, draw="none", font=r"\small") - -fig.spy(on=(0.55, 2.1), at=(6.4, 2.45), options=[colors.green]) -fig.spy(on=(2.3, 2.15), at=(6.4, 0.8), options=[colors.orange]) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -This is a good pattern when you want: - -- the same magnification for all spies -- the same inset shape and border styling -- the same spy-on styling - -## Reproducing the classic pgfplots spy example - -Here is the classic pgfplots example from the TikZ manual, reproduced using only -the Python API. - -```{python} -fig = TikzFigure() - -fig.configure_spy_scope( - mode="outlines", - options=[shapes.rectangle], - magnification=3, - connect_spies=True, -) - -ax = fig.axis2d( - xlim=(-5, 5), - grid="major", - options=["no markers"], - domain="-5:5", - enlargelimits="false", -) - -ax.add_plot(func="x^2", color=colors.red) -ax.add_plot(func="x^3", color=colors.blue) - -spypoint = ax.coordinate("spypoint", at="axis cs:0,0") -spyviewer = ax.coordinate("spyviewer", at="axis cs:0.5,-90") -ax.spy( - on=spypoint, - at=spyviewer, - width="6cm", - height="1cm", - node_style={"fill": colors.white}, -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -This matches the original idea closely: - -- `mode="outlines"` maps to `spy using outlines={...}` -- `options=[shapes.rectangle]` makes the inset rectangular -- `ax.coordinate(...)` and `ax.spy(...)` keep everything inside the `axis` - environment - -## Local spy defaults with `spy_scope(...)` - -Use `spy_scope(...)` when spy settings should only apply to part of the figure. -This mirrors the TikZ manual more closely than a figure-wide spy scope. - -Here the spy scope is used on a small transit-map sketch, and one spy zooms in -directly on a labeled `Node` object. - -```{python} -def add_station(scope, position, size="3pt", fill=colors.white, label=None): - return scope.node( - position, - label=label, - shape="circle", - minimum_size=size, - inner_sep="0pt", - fill=fill, - draw=colors.black, - line_width="0.3pt", - ) - - -fig = TikzFigure() - -with fig.spy_scope( - mode="outlines", - options=[shapes.circle], - magnification=4, - size="1.5cm", - connect_spies=True, - every_spy_in_node_options=[shapes.circle], - every_spy_in_node_style={ - "draw": colors.orange.mix(colors.black, percent=28), - "line_width": "0.9pt", - "fill": colors.white, - }, - every_spy_on_node_options=[shapes.circle], - every_spy_on_node_style={ - "draw": colors.orange.mix(colors.black, percent=18), - "fill": colors.orange, - "fill_opacity": 0.08, - }, -) as metro: - red_line = [(-0.1, -0.5), (0.9, 0.1), (1.9, 0.7), (3.0, 1.2), (4.0, 1.55)] - blue_line = [(0.1, 1.35), (1.1, 0.95), (2.0, 0.65), (3.1, 0.1), (4.1, -0.4)] - - for line_points, line_color in [(red_line, colors.red), (blue_line, colors.blue)]: - metro.draw(line_points, color=line_color, line_width="2.6pt") - for point in line_points: - add_station(metro, point) - - central = add_station( - metro, - (2.0, 0.65), - size="5pt", - fill=colors.yellow.mix(percent=18), - label="central_hub", - ) - metro.node((2.0, 0.2), content="Central", draw="none", font=r"\small\bfseries") - - metro.spy( - on=central, - at=(5.7, 1.2), - options=[colors.orange], - node_label="central interchange", - ) - metro.spy( - on=(3.95, 1.5), - at=(5.7, -0.45), - options=[colors.blue], - width="1.8cm", - height="1.0cm", - node_options=[shapes.rectangle], - node_style={ - "draw": colors.blue, - "rounded_corners": "2pt", - "fill": colors.white, - }, - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -The `outlines` preset gives a nice default look: - -- the spy-in node is drawn prominently -- the spy-on node is outlined more lightly -- both are easy to recolor and reshape -- `on=` can target a `Node` directly, not just a coordinate tuple diff --git a/tutorials/advanced/tutorial_25_parametric_tikz_plots.ipynb b/tutorials/advanced/tutorial_25_parametric_tikz_plots.ipynb new file mode 100644 index 0000000..81c5512 --- /dev/null +++ b/tutorials/advanced/tutorial_25_parametric_tikz_plots.ipynb @@ -0,0 +1,791 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Tokamak X-Point with Parametric TikZ Plots\n", + "\n", + "This tutorial builds on [Mathematical\n", + "Expressions](https://max-models.github.io/tikzfigure/tutorials/tutorial-10-math-expressions/) and focuses on the plain TikZ\n", + "plotting API by building up to a stylized **tokamak X-point / separatrix**\n", + "drawing in a poloidal cross-section.\n", + "\n", + "The goal is not to derive a full equilibrium solver inside TikZ. Instead, the\n", + "point is to show how declared PGF functions, parametric expressions, and looped\n", + "sampling can be combined to produce the kind of contour structure you see in an\n", + "X-point divertor configuration.\n", + "\n", + "Along the way we will use:\n", + "\n", + "- `fig.plot(...)` for a single sampled plot\n", + "- `loop.plot(...)` for families of curves\n", + "- callable objects returned by `fig.declare_function(...)`\n", + "- `func(name, ...)` when you still want the string-based interface\n", + "\n", + "These plots are regular TikZ `\\draw ... plot(...)` commands, not pgfplots axis\n", + "plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure, colors\n", + "from tikzfigure.math import Var, cos, exp, ln, max, sin, sqrt" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## A first sampled curve\n", + "\n", + "The simplest case is a regular function graph. Here `fig.plot(...)` samples a\n", + "single expression over a domain. This is the basic building block behind all of\n", + "the more complicated flux-surface-like contours later in the tutorial." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(figsize=(8, 4))\n", + "\n", + "x = Var(\"x\")\n", + "\n", + "fig.draw([(-2.4, 0), (2.4, 0)], color=\"gray\")\n", + "fig.draw([(0, -0.8), (0, 0.8)], color=\"gray\")\n", + "fig.plot(\n", + " sin(90 * x) / 2,\n", + " variable=\"x\",\n", + " domain=(-2, 2),\n", + " samples=120,\n", + " smooth=True,\n", + " options=[colors.blue],\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "When only one expression is passed, TikZ uses the sampling variable as the\n", + "x-coordinate and the expression as the y-coordinate.\n", + "\n", + "## Declaring a custom shaping function\n", + "\n", + "You can declare helper functions once and reuse them in later plots. For this\n", + "tutorial we will use a hyperbolic-tangent-like shaping function that will later\n", + "help us bend contours into an X-point geometry." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(figsize=(8, 4))\n", + "\n", + "th = fig.declare_function(\n", + " \"th\",\n", + " \"x\",\n", + " (1 - exp(-2 * Var(\"x\"))) / (1 + exp(-2 * Var(\"x\"))),\n", + ")\n", + "\n", + "x = Var(\"x\")\n", + "\n", + "fig.draw([(-3.4, 0), (3.4, 0)], color=\"gray\")\n", + "fig.draw([(0, -1.1), (0, 1.1)], color=\"gray\")\n", + "fig.plot(\n", + " th(x),\n", + " variable=\"x\",\n", + " domain=(-3, 3),\n", + " samples=150,\n", + " smooth=True,\n", + " options=[colors.Azure4],\n", + " line_width=\"0.8pt\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "The object returned by `fig.declare_function(...)` is callable, so the most\n", + "natural way to use it is `th(x)`. The older `func(\"th\", x)` style still works\n", + "when you want it.\n", + "\n", + "## Your first closed flux-surface-like curve\n", + "\n", + "If you pass both `x` and `y` expressions, the plot becomes parametric. This is\n", + "the key step from ordinary function graphs to closed magnetic-surface-like\n", + "contours." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(figsize=(7, 7))\n", + "\n", + "fig.variable(\"scale\", 1.25)\n", + "fig.variable(\"radius\", 1.6)\n", + "arcth = fig.declare_function(\n", + " \"arcth\",\n", + " \"x\",\n", + " 0.5 * ln((1 + Var(\"x\")) / (1 - Var(\"x\"))),\n", + ")\n", + "th = fig.declare_function(\n", + " \"th\",\n", + " \"x\",\n", + " (1 - exp(-2 * Var(\"x\"))) / (1 + exp(-2 * Var(\"x\"))),\n", + ")\n", + "\n", + "theta = Var(\"theta\")\n", + "radius = Var(\"radius\")\n", + "\n", + "curve_x = Var(\"scale\") * arcth(th(radius) * cos(theta))\n", + "curve_y = Var(\"scale\") * arcth(th(radius) * sin(theta))\n", + "\n", + "fig.plot(\n", + " curve_x,\n", + " curve_y,\n", + " variable=\"theta\",\n", + " domain=(0, 360),\n", + " samples=140,\n", + " smooth=True,\n", + " options=[colors.Azure4],\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "Now the sampling variable is `\\theta`, and both coordinates are computed from\n", + "it. You can think of `\\theta` here as a poloidal-angle-like parameter tracing a\n", + "single closed contour.\n", + "\n", + "## Building a family of nested flux surfaces\n", + "\n", + "The same parametric expression becomes much more interesting when combined with\n", + "`fig.loop(...)`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(figsize=(8, 8))\n", + "\n", + "fig.variable(\"scale\", 1.25)\n", + "arcth = fig.declare_function(\n", + " \"arcth\",\n", + " \"x\",\n", + " 0.5 * ln((1 + Var(\"x\")) / (1 - Var(\"x\"))),\n", + ")\n", + "th = fig.declare_function(\n", + " \"th\",\n", + " \"x\",\n", + " (1 - exp(-2 * Var(\"x\"))) / (1 + exp(-2 * Var(\"x\"))),\n", + ")\n", + "\n", + "theta = Var(\"theta\")\n", + "\n", + "with fig.loop(\"i\", range(21)) as i:\n", + " radius = 0.2 * i\n", + " curve_x = Var(\"scale\") * arcth(th(radius) * cos(theta))\n", + " curve_y = Var(\"scale\") * arcth(th(radius) * sin(theta))\n", + " fig.plot(\n", + " curve_x,\n", + " curve_y,\n", + " variable=\"theta\",\n", + " domain=(0, 360),\n", + " samples=100,\n", + " smooth=True,\n", + " options=[colors.Azure4],\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "Each loop iteration gives TikZ a different integer value of `\\i`. Because the\n", + "context variable `i` now behaves like that TikZ variable directly, you can\n", + "derive the physical radius inside the block with `radius = 0.2 * i` and then use\n", + "the ordinary `fig.plot(...)` API inside the loop body. That keeps the loop\n", + "compact while still producing a whole family of nested contours. In the tokamak\n", + "picture, these read naturally as flux surfaces moving outward from the core.\n", + "\n", + "## Adding the crossing family near the X-point\n", + "\n", + "Next, add the second family by keeping `\\theta` fixed and varying `\\r`. Once\n", + "both families are drawn, the contour structure starts to look like the crossing\n", + "geometry around an X-point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(figsize=(8, 8))\n", + "\n", + "fig.variable(\"scale\", 1.25)\n", + "arcth = fig.declare_function(\n", + " \"arcth\",\n", + " \"x\",\n", + " 0.5 * ln((1 + Var(\"x\")) / (1 - Var(\"x\"))),\n", + ")\n", + "th = fig.declare_function(\n", + " \"th\",\n", + " \"x\",\n", + " (1 - exp(-2 * Var(\"x\"))) / (1 + exp(-2 * Var(\"x\"))),\n", + ")\n", + "\n", + "theta = Var(\"theta\")\n", + "\n", + "with fig.loop(\"i\", range(21)) as i:\n", + " radius = 0.2 * i\n", + " curve_x_loop = Var(\"scale\") * arcth(th(radius) * cos(theta))\n", + " curve_y_loop = Var(\"scale\") * arcth(th(radius) * sin(theta))\n", + " fig.plot(\n", + " curve_x_loop,\n", + " curve_y_loop,\n", + " variable=\"theta\",\n", + " domain=(0, 360),\n", + " samples=100,\n", + " smooth=True,\n", + " options=[colors.Azure4],\n", + " )\n", + "\n", + "with fig.loop(\"theta\", range(0, 361, 9)) as theta:\n", + " r = Var(\"r\")\n", + " curve_x_cross = Var(\"scale\") * arcth(th(r) * cos(theta))\n", + " curve_y_cross = Var(\"scale\") * arcth(th(r) * sin(theta))\n", + " fig.plot(\n", + " curve_x_cross,\n", + " curve_y_cross,\n", + " variable=\"r\",\n", + " domain=(0, 4),\n", + " samples=15,\n", + " smooth=True,\n", + " options=[colors.Azure4],\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## The full X-point / separatrix sketch\n", + "\n", + "Now add a second pass with thicker red guide curves to highlight a few selected\n", + "surfaces and the separatrix-like branches." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure(figsize=(9, 9))\n", + "\n", + "fig.variable(\"scale\", 1.25)\n", + "arcth = fig.declare_function(\n", + " \"arcth\",\n", + " \"x\",\n", + " 0.5 * ln((1 + Var(\"x\")) / (1 - Var(\"x\"))),\n", + ")\n", + "th = fig.declare_function(\n", + " \"th\",\n", + " \"x\",\n", + " (1 - exp(-2 * Var(\"x\"))) / (1 + exp(-2 * Var(\"x\"))),\n", + ")\n", + "\n", + "theta = Var(\"theta\")\n", + "\n", + "fine_radii = range(21)\n", + "major_radii = range(5, 21, 5)\n", + "fine_angles = range(0, 361, 9)\n", + "major_angles = range(0, 360, 30)\n", + "\n", + "with fig.loop(\"i\", fine_radii) as i:\n", + " radius = 0.2 * i\n", + " curve_x_loop = Var(\"scale\") * arcth(th(radius) * cos(theta))\n", + " curve_y_loop = Var(\"scale\") * arcth(th(radius) * sin(theta))\n", + " fig.plot(\n", + " curve_x_loop,\n", + " curve_y_loop,\n", + " variable=\"theta\",\n", + " domain=(0, 360),\n", + " samples=100,\n", + " smooth=True,\n", + " options=[colors.Azure4],\n", + " )\n", + "\n", + "with fig.loop(\"theta\", fine_angles) as theta:\n", + " r = Var(\"r\")\n", + " curve_x_cross = Var(\"scale\") * arcth(th(r) * cos(theta))\n", + " curve_y_cross = Var(\"scale\") * arcth(th(r) * sin(theta))\n", + " fig.plot(\n", + " curve_x_cross,\n", + " curve_y_cross,\n", + " variable=\"r\",\n", + " domain=(0, 4),\n", + " samples=15,\n", + " smooth=True,\n", + " options=[colors.Azure4],\n", + " )\n", + "\n", + "with fig.loop(\"i\", major_radii) as i:\n", + " radius = 0.2 * i\n", + " curve_x_loop = Var(\"scale\") * arcth(th(radius) * cos(theta))\n", + " curve_y_loop = Var(\"scale\") * arcth(th(radius) * sin(theta))\n", + " fig.plot(\n", + " curve_x_loop,\n", + " curve_y_loop,\n", + " variable=\"theta\",\n", + " domain=(0, 360),\n", + " samples=120,\n", + " smooth=True,\n", + " options=[colors.Red3],\n", + " line_width=\"0.9pt\",\n", + " )\n", + "\n", + "with fig.loop(\"theta\", major_angles) as theta:\n", + " r = Var(\"r\")\n", + " curve_x_cross = Var(\"scale\") * arcth(th(r) * cos(theta))\n", + " curve_y_cross = Var(\"scale\") * arcth(th(r) * sin(theta))\n", + " fig.plot(\n", + " curve_x_cross,\n", + " curve_y_cross,\n", + " variable=\"r\",\n", + " domain=(0, 4),\n", + " samples=25,\n", + " smooth=True,\n", + " options=[colors.Red3],\n", + " line_width=\"0.9pt\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "This final figure uses the exact same ingredients as the earlier steps: declared\n", + "functions, composable expressions, sampled plots, and loops. The result is a\n", + "stylized tokamak X-point drawing with a separatrix-like structure, built\n", + "entirely from plain TikZ plots.\n", + "\n", + "## A flux-surface-aligned curvilinear mesh\n", + "\n", + "The same API can also draw a mesh aligned to an X-point map. The next example\n", + "adds the patch layout with native `filldraw` and `grid` calls, then uses\n", + "`parametric_grid(...)` to draw both coordinate families of the mapped mesh." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "def add_eight_patch_background(fig, radius=6, color=\"Azure4\"):\n", + " patches = [\n", + " [(0, 0), (radius, 0), (radius, radius), (0, radius)],\n", + " [(0, 0), (-radius, 0), (-radius, radius), (0, radius)],\n", + " [(0, 0), (radius, 0), (radius, -radius), (0, -radius)],\n", + " [(0, 0), (-radius, 0), (-radius, -radius), (0, -radius)],\n", + " [(radius, 0), (2 * radius, 0), (2 * radius, radius), (radius, radius)],\n", + " [(-2 * radius, 0), (-radius, 0), (-radius, radius), (-2 * radius, radius)],\n", + " [(0, radius), (radius, radius), (radius, 2 * radius), (0, 2 * radius)],\n", + " [(0, -2 * radius), (radius, -2 * radius), (radius, -radius), (0, -radius)],\n", + " ]\n", + " for patch in patches:\n", + " fig.filldraw(\n", + " patch,\n", + " cycle=True,\n", + " fill=color,\n", + " fill_opacity=0.06,\n", + " draw=color,\n", + " draw_opacity=0.35,\n", + " line_width=\"0.4pt\",\n", + " )\n", + "\n", + "\n", + "fig = TikzFigure(figsize=(8, 8))\n", + "add_eight_patch_background(fig)\n", + "fig.grid((-6, -6), (6, 6), step=\"1\", color=colors.Azure4, opacity=0.25)" + ] + }, + { + "cell_type": "markdown", + "id": "28", + "metadata": {}, + "source": [ + "The square-root map and the smooth blending function are declared once as PGF\n", + "functions. `Var` keeps the expressions symbolic until TikZ samples the plots." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": {}, + "outputs": [], + "source": [ + "arg_eta = Var(\"argEta\")\n", + "arg_xi = Var(\"argXi\")\n", + "x_mesh = Var(\"sampleParam\")\n", + "x_mesh_two = Var(\"sampleParamTwo\")\n", + "u_max = 6\n", + "v_max = 6\n", + "\n", + "map_x = fig.function(\n", + " \"map_x\", (\"x\", \"y\"), sqrt(max(0, sqrt(Var(\"x\") ** 2 + Var(\"y\") ** 2) - Var(\"x\")))\n", + ")\n", + "map_y = fig.function(\n", + " \"map_y\", (\"x\", \"y\"), sqrt(max(0, sqrt(Var(\"x\") ** 2 + Var(\"y\") ** 2) + Var(\"x\")))\n", + ")\n", + "mapped_x = lambda a, b: sqrt(max(0, sqrt(a**2 + b**2) - a))\n", + "mapped_y = lambda a, b: sqrt(max(0, sqrt(a**2 + b**2) + a))\n", + "fig.function(\"smoothstep\", \"x\", r\"6*\\x^5 - 15*\\x^4 + 10*\\x^3\")\n", + "fig.function(\n", + " \"blend\",\n", + " \"x\",\n", + " r\"ifthenelse(\\x < 0, 0, ifthenelse(\\x > 1, 1, smoothstep(\\x)))\",\n", + ")\n", + "fig.function(\"q\", \"x\", r\"blend(2*(\\x - 0.4))\")" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "`parametric_grid` emits one loop for fixed parameter values and one for the\n", + "other parameter family. The outer sign loops reuse the first-quadrant map in\n", + "all four sectors; the second call draws its coordinate-swapped counterpart." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "u_values = [i / 6 for i in range(7)]\n", + "v_values = [i / 6 for i in range(7)]\n", + "arg_eta_two = Var(\"argEtaTwo\")\n", + "\n", + "fig.parametric_grid(\n", + " mapped_x(arg_eta, x_mesh),\n", + " mapped_y(arg_eta, x_mesh),\n", + " u_variable=\"argEta\",\n", + " v_variable=\"sampleParam\",\n", + " u_values=u_values,\n", + " v_values=v_values,\n", + " samples=18,\n", + " smooth=True,\n", + " options=[colors.Azure4],\n", + " line_width=\"0.7pt\",\n", + ")\n", + "fig.parametric_grid(\n", + " mapped_y(arg_eta_two, x_mesh_two),\n", + " mapped_x(arg_eta_two, x_mesh_two),\n", + " u_variable=\"argEtaTwo\",\n", + " v_variable=\"sampleParamTwo\",\n", + " u_values=u_values,\n", + " v_values=v_values,\n", + " samples=18,\n", + " smooth=True,\n", + " options=[colors.Azure4],\n", + " line_width=\"0.7pt\",\n", + ")\n", + "\n", + "fig.node((-2, 0.5), content=r\"$\\times$\", text=colors.black)\n", + "fig.node((-2, 0.0), content=r\"$\\bullet$\", text=colors.Orange1)\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_25_parametric_tikz_plots.qmd b/tutorials/advanced/tutorial_25_parametric_tikz_plots.qmd deleted file mode 100644 index 3799d0d..0000000 --- a/tutorials/advanced/tutorial_25_parametric_tikz_plots.qmd +++ /dev/null @@ -1,436 +0,0 @@ ---- -title: "Tokamak X-Point with Parametric TikZ Plots" -jupyter: python3 ---- - -This tutorial builds on [Mathematical -Expressions](tutorial_10_math_expressions.qmd) and focuses on the plain TikZ -plotting API by building up to a stylized **tokamak X-point / separatrix** -drawing in a poloidal cross-section. - -The goal is not to derive a full equilibrium solver inside TikZ. Instead, the -point is to show how declared PGF functions, parametric expressions, and looped -sampling can be combined to produce the kind of contour structure you see in an -X-point divertor configuration. - -Along the way we will use: - -- `fig.plot(...)` for a single sampled plot -- `loop.plot(...)` for families of curves -- callable objects returned by `fig.declare_function(...)` -- `func(name, ...)` when you still want the string-based interface - -These plots are regular TikZ `\draw ... plot(...)` commands, not pgfplots axis -plots. - -```{python} -from tikzfigure import TikzFigure, colors -from tikzfigure.math import Var, cos, exp, func, ln, max, sin, sqrt -``` - -## A first sampled curve - -The simplest case is a regular function graph. Here `fig.plot(...)` samples a -single expression over a domain. This is the basic building block behind all of -the more complicated flux-surface-like contours later in the tutorial. - -```{python} -fig = TikzFigure(figsize=(8, 4)) - -x = Var("x") - -fig.draw([(-2.4, 0), (2.4, 0)], color="gray") -fig.draw([(0, -0.8), (0, 0.8)], color="gray") -fig.plot( - sin(90 * x) / 2, - variable="x", - domain=(-2, 2), - samples=120, - smooth=True, - options=[colors.blue], -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -When only one expression is passed, TikZ uses the sampling variable as the -x-coordinate and the expression as the y-coordinate. - -## Declaring a custom shaping function - -You can declare helper functions once and reuse them in later plots. For this -tutorial we will use a hyperbolic-tangent-like shaping function that will later -help us bend contours into an X-point geometry. - -```{python} -fig = TikzFigure(figsize=(8, 4)) - -th = fig.declare_function( - "th", - "x", - (1 - exp(-2 * Var("x"))) / (1 + exp(-2 * Var("x"))), -) - -x = Var("x") - -fig.draw([(-3.4, 0), (3.4, 0)], color="gray") -fig.draw([(0, -1.1), (0, 1.1)], color="gray") -fig.plot( - th(x), - variable="x", - domain=(-3, 3), - samples=150, - smooth=True, - options=[colors.Azure4], - line_width="0.8pt", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -The object returned by `fig.declare_function(...)` is callable, so the most -natural way to use it is `th(x)`. The older `func("th", x)` style still works -when you want it. - -## Your first closed flux-surface-like curve - -If you pass both `x` and `y` expressions, the plot becomes parametric. This is -the key step from ordinary function graphs to closed magnetic-surface-like -contours. - -```{python} -fig = TikzFigure(figsize=(7, 7)) - -fig.variable("scale", 1.25) -fig.variable("radius", 1.6) -arcth = fig.declare_function( - "arcth", - "x", - 0.5 * ln((1 + Var("x")) / (1 - Var("x"))), -) -th = fig.declare_function( - "th", - "x", - (1 - exp(-2 * Var("x"))) / (1 + exp(-2 * Var("x"))), -) - -theta = Var("theta") -radius = Var("radius") - -curve_x = Var("scale") * arcth(th(radius) * cos(theta)) -curve_y = Var("scale") * arcth(th(radius) * sin(theta)) - -fig.plot( - curve_x, - curve_y, - variable="theta", - domain=(0, 360), - samples=140, - smooth=True, - options=[colors.Azure4], -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -Now the sampling variable is `\theta`, and both coordinates are computed from -it. You can think of `\theta` here as a poloidal-angle-like parameter tracing a -single closed contour. - -## Building a family of nested flux surfaces - -The same parametric expression becomes much more interesting when combined with -`fig.loop(...)`. - -```{python} -fig = TikzFigure(figsize=(8, 8)) - -fig.variable("scale", 1.25) -arcth = fig.declare_function( - "arcth", - "x", - 0.5 * ln((1 + Var("x")) / (1 - Var("x"))), -) -th = fig.declare_function( - "th", - "x", - (1 - exp(-2 * Var("x"))) / (1 + exp(-2 * Var("x"))), -) - -theta = Var("theta") - -with fig.loop("i", range(21)) as i: - radius = 0.2 * i - curve_x = Var("scale") * arcth(th(radius) * cos(theta)) - curve_y = Var("scale") * arcth(th(radius) * sin(theta)) - fig.plot( - curve_x, - curve_y, - variable="theta", - domain=(0, 360), - samples=100, - smooth=True, - options=[colors.Azure4], - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -Each loop iteration gives TikZ a different integer value of `\i`. Because the -context variable `i` now behaves like that TikZ variable directly, you can -derive the physical radius inside the block with `radius = 0.2 * i` and then use -the ordinary `fig.plot(...)` API inside the loop body. That keeps the loop -compact while still producing a whole family of nested contours. In the tokamak -picture, these read naturally as flux surfaces moving outward from the core. - -## Adding the crossing family near the X-point - -Next, add the second family by keeping `\theta` fixed and varying `\r`. Once -both families are drawn, the contour structure starts to look like the crossing -geometry around an X-point. - -```{python} -fig = TikzFigure(figsize=(8, 8)) - -fig.variable("scale", 1.25) -arcth = fig.declare_function( - "arcth", - "x", - 0.5 * ln((1 + Var("x")) / (1 - Var("x"))), -) -th = fig.declare_function( - "th", - "x", - (1 - exp(-2 * Var("x"))) / (1 + exp(-2 * Var("x"))), -) - -theta = Var("theta") - -with fig.loop("i", range(21)) as i: - radius = 0.2 * i - curve_x_loop = Var("scale") * arcth(th(radius) * cos(theta)) - curve_y_loop = Var("scale") * arcth(th(radius) * sin(theta)) - fig.plot( - curve_x_loop, - curve_y_loop, - variable="theta", - domain=(0, 360), - samples=100, - smooth=True, - options=[colors.Azure4], - ) - -with fig.loop("theta", range(0, 361, 9)) as theta: - r = Var("r") - curve_x_cross = Var("scale") * arcth(th(r) * cos(theta)) - curve_y_cross = Var("scale") * arcth(th(r) * sin(theta)) - fig.plot( - curve_x_cross, - curve_y_cross, - variable="r", - domain=(0, 4), - samples=15, - smooth=True, - options=[colors.Azure4], - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## The full X-point / separatrix sketch - -Now add a second pass with thicker red guide curves to highlight a few selected -surfaces and the separatrix-like branches. - -```{python} -fig = TikzFigure(figsize=(9, 9)) - -fig.variable("scale", 1.25) -arcth = fig.declare_function( - "arcth", - "x", - 0.5 * ln((1 + Var("x")) / (1 - Var("x"))), -) -th = fig.declare_function( - "th", - "x", - (1 - exp(-2 * Var("x"))) / (1 + exp(-2 * Var("x"))), -) - -theta = Var("theta") - -fine_radii = range(21) -major_radii = range(5, 21, 5) -fine_angles = range(0, 361, 9) -major_angles = range(0, 360, 30) - -with fig.loop("i", fine_radii) as i: - radius = 0.2 * i - curve_x_loop = Var("scale") * arcth(th(radius) * cos(theta)) - curve_y_loop = Var("scale") * arcth(th(radius) * sin(theta)) - fig.plot( - curve_x_loop, - curve_y_loop, - variable="theta", - domain=(0, 360), - samples=100, - smooth=True, - options=[colors.Azure4], - ) - -with fig.loop("theta", fine_angles) as theta: - r = Var("r") - curve_x_cross = Var("scale") * arcth(th(r) * cos(theta)) - curve_y_cross = Var("scale") * arcth(th(r) * sin(theta)) - fig.plot( - curve_x_cross, - curve_y_cross, - variable="r", - domain=(0, 4), - samples=15, - smooth=True, - options=[colors.Azure4], - ) - -with fig.loop("i", major_radii) as i: - radius = 0.2 * i - curve_x_loop = Var("scale") * arcth(th(radius) * cos(theta)) - curve_y_loop = Var("scale") * arcth(th(radius) * sin(theta)) - fig.plot( - curve_x_loop, - curve_y_loop, - variable="theta", - domain=(0, 360), - samples=120, - smooth=True, - options=[colors.Red3], - line_width="0.9pt", - ) - -with fig.loop("theta", major_angles) as theta: - r = Var("r") - curve_x_cross = Var("scale") * arcth(th(r) * cos(theta)) - curve_y_cross = Var("scale") * arcth(th(r) * sin(theta)) - fig.plot( - curve_x_cross, - curve_y_cross, - variable="r", - domain=(0, 4), - samples=25, - smooth=True, - options=[colors.Red3], - line_width="0.9pt", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -This final figure uses the exact same ingredients as the earlier steps: declared -functions, composable expressions, sampled plots, and loops. The result is a -stylized tokamak X-point drawing with a separatrix-like structure, built -entirely from plain TikZ plots. - -## A flux-surface-aligned curvilinear mesh - -The same API can also draw a mesh aligned to an X-point map. The next example -adds the patch layout with native `filldraw` and `grid` calls, then uses -`parametric_grid(...)` to draw both coordinate families of the mapped mesh. - -```{python} -def add_eight_patch_background(fig, radius=6, color="Azure4"): - patches = [ - [(0, 0), (radius, 0), (radius, radius), (0, radius)], - [(0, 0), (-radius, 0), (-radius, radius), (0, radius)], - [(0, 0), (radius, 0), (radius, -radius), (0, -radius)], - [(0, 0), (-radius, 0), (-radius, -radius), (0, -radius)], - [(radius, 0), (2 * radius, 0), (2 * radius, radius), (radius, radius)], - [(-2 * radius, 0), (-radius, 0), (-radius, radius), (-2 * radius, radius)], - [(0, radius), (radius, radius), (radius, 2 * radius), (0, 2 * radius)], - [(0, -2 * radius), (radius, -2 * radius), (radius, -radius), (0, -radius)], - ] - for patch in patches: - fig.filldraw( - patch, - cycle=True, - fill=color, - fill_opacity=0.06, - draw=color, - draw_opacity=0.35, - line_width="0.4pt", - ) - - -fig = TikzFigure(figsize=(8, 8)) -add_eight_patch_background(fig) -fig.grid((-6, -6), (6, 6), step="1", color=colors.Azure4, opacity=0.25) -``` - -The square-root map and the smooth blending function are declared once as PGF -functions. `Var` keeps the expressions symbolic until TikZ samples the plots. - -```{python} -arg_eta = Var("argEta") -arg_xi = Var("argXi") -x_mesh = Var("sampleParam") -x_mesh_two = Var("sampleParamTwo") -u_max = 6 -v_max = 6 - -map_x = fig.function( - "map_x", ("x", "y"), sqrt(max(0, sqrt(Var("x")**2 + Var("y")**2) - Var("x"))) -) -map_y = fig.function( - "map_y", ("x", "y"), sqrt(max(0, sqrt(Var("x")**2 + Var("y")**2) + Var("x"))) -) -mapped_x = lambda a, b: sqrt(max(0, sqrt(a**2 + b**2) - a)) -mapped_y = lambda a, b: sqrt(max(0, sqrt(a**2 + b**2) + a)) -fig.function("smoothstep", "x", r"6*\x^5 - 15*\x^4 + 10*\x^3") -fig.function( - "blend", "x", - r"ifthenelse(\x < 0, 0, ifthenelse(\x > 1, 1, smoothstep(\x)))", -) -fig.function("q", "x", r"blend(2*(\x - 0.4))") -``` - -`parametric_grid` emits one loop for fixed parameter values and one for the -other parameter family. The outer sign loops reuse the first-quadrant map in -all four sectors; the second call draws its coordinate-swapped counterpart. - -```{python} -u_values = [i / 6 for i in range(7)] -v_values = [i / 6 for i in range(7)] -arg_eta_two = Var("argEtaTwo") - -fig.parametric_grid( - mapped_x(arg_eta, x_mesh), mapped_y(arg_eta, x_mesh), - u_variable="argEta", v_variable="sampleParam", - u_values=u_values, v_values=v_values, - samples=18, smooth=True, options=[colors.Azure4], - line_width="0.7pt", -) -fig.parametric_grid( - mapped_y(arg_eta_two, x_mesh_two), mapped_x(arg_eta_two, x_mesh_two), - u_variable="argEtaTwo", v_variable="sampleParamTwo", - u_values=u_values, v_values=v_values, - samples=18, smooth=True, options=[colors.Azure4], - line_width="0.7pt", -) - -fig.node((-2, 0.5), content=r"$\times$", text=colors.black) -fig.node((-2, 0.0), content=r"$\bullet$", text=colors.Orange1) -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} diff --git a/tutorials/advanced/tutorial_26_gantt.ipynb b/tutorials/advanced/tutorial_26_gantt.ipynb new file mode 100644 index 0000000..00028f3 --- /dev/null +++ b/tutorials/advanced/tutorial_26_gantt.ipynb @@ -0,0 +1,265 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Gantt Charts\n", + "\n", + "Gantt charts are useful for showing a project plan, a publication timeline, or\n", + "any other schedule organized along a numeric time axis. tikzfigure provides a\n", + "small structured wrapper around LaTeX's [`pgfgantt`](https://ctan.org/pkg/pgfgantt)\n", + "package.\n", + "\n", + "The package is added automatically when you create a chart, so the same figure\n", + "can be exported with `generate_standalone()` or compiled with `show()`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## A simple project plan\n", + "\n", + "Pass the first and last time slot to `gantt()` and describe rows with small\n", + "dictionaries. The supported row types are `title`, `titlelist`, `group`,\n", + "`bar`, `milestone`, `link`, and `raw`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "fig.gantt(\n", + " 1,\n", + " 12,\n", + " options=[\"hgrid\", \"vgrid\"],\n", + " rows=[\n", + " {\"type\": \"titlelist\", \"content\": \"1,2,3,4,5,6,7,8,9,10,11,12\"},\n", + " {\"type\": \"group\", \"content\": \"Discovery\", \"start\": 1, \"end\": 3},\n", + " {\"type\": \"bar\", \"content\": \"Research\", \"start\": 1, \"end\": 2},\n", + " {\"type\": \"bar\", \"content\": \"Requirements\", \"start\": 2, \"end\": 3},\n", + " {\"type\": \"group\", \"content\": \"Delivery\", \"start\": 4, \"end\": 12},\n", + " {\"type\": \"bar\", \"content\": \"Implementation\", \"start\": 4, \"end\": 8},\n", + " {\"type\": \"bar\", \"content\": \"Testing\", \"start\": 8, \"end\": 10},\n", + " {\"type\": \"milestone\", \"content\": \"Release\", \"at\": 12},\n", + " ],\n", + ")\n", + "\n", + "print(fig.generate_tikz())" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "Call `fig.show()` in a notebook to compile and display the chart, or save it\n", + "with `fig.savefig(\"project-plan.pdf\")`.\n", + "\n", + "## Dependencies between tasks\n", + "\n", + "Give bars and milestones a `name` and connect them with `link` rows. The names\n", + "are passed to pgfgantt and are used by `ganttlink`." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "chart = fig.add_gantt_chart(\n", + " 1,\n", + " 8,\n", + " options=[\"hgrid\", \"vgrid\"],\n", + ")\n", + "\n", + "chart.add_row(\n", + " \"titlelist\",\n", + " content=\"Jan,Feb,Mar,Apr,May,Jun,Jul,Aug\",\n", + " width=1,\n", + " # Text labels must not be run through pgfmath's default numeric\n", + " # evaluation, which pgfgantt otherwise applies to titlelist entries.\n", + " options=[r\"title list options={var=\\x}\"],\n", + ")\n", + "chart.add_row(\n", + " \"bar\",\n", + " content=\"Design\",\n", + " start=1,\n", + " end=3,\n", + " name=\"design\",\n", + " options=[\"progress=100\"],\n", + ")\n", + "chart.add_row(\n", + " \"bar\",\n", + " content=\"Build\",\n", + " start=3,\n", + " end=6,\n", + " name=\"build\",\n", + " options=[\"progress=60\"],\n", + ")\n", + "chart.add_row(\"milestone\", content=\"Launch\", at=8, name=\"launch\")\n", + "chart.add_row(\"link\", source=\"design\", target=\"build\")\n", + "chart.add_row(\"link\", source=\"build\", target=\"launch\")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "`add_gantt_chart()` is the explicit spelling; `gantt()` and `add_gantt()` are\n", + "short aliases. The returned `GanttChart` can be extended incrementally with\n", + "`add_row()`, which is convenient when rows come from application data.\n", + "\n", + "## A multi-phase project timeline\n", + "\n", + "This example mirrors a common plotting API: task names, starts, and durations\n", + "are kept in parallel Python lists. Convert each phase into Gantt rows and use\n", + "per-bar options to give the phases distinct colors." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "phase1_tasks = [\"Requirements\", \"Architecture\", \"UI Design\"]\n", + "phase1_starts = [0, 3, 6]\n", + "phase1_durations = [3, 3, 4]\n", + "\n", + "phase2_tasks = [\"Backend Dev\", \"Frontend Dev\", \"Integration\"]\n", + "phase2_starts = [10, 10, 20]\n", + "phase2_durations = [10, 10, 5]\n", + "\n", + "phase3_tasks = [\"Unit Tests\", \"Integration Tests\", \"UAT\"]\n", + "phase3_starts = [25, 28, 32]\n", + "phase3_durations = [3, 4, 3]\n", + "\n", + "\n", + "def phase_rows(title, tasks, starts, durations, fill, draw):\n", + " rows = [\n", + " {\n", + " \"type\": \"group\",\n", + " \"content\": title,\n", + " \"start\": min(starts),\n", + " \"end\": max(start + duration for start, duration in zip(starts, durations)),\n", + " }\n", + " ]\n", + " rows.extend(\n", + " {\n", + " \"type\": \"bar\",\n", + " \"content\": task,\n", + " \"start\": start,\n", + " \"end\": start + duration,\n", + " # pgfgantt bars are styled via the `bar` style itself, not raw\n", + " # `fill=`/`draw=` keys (those live outside the /pgfgantt family).\n", + " \"options\": [f\"bar/.append style={{fill={fill}, draw={draw}}}\"],\n", + " }\n", + " for task, start, duration in zip(tasks, starts, durations)\n", + " )\n", + " return rows\n", + "\n", + "\n", + "rows = [{\"type\": \"titlelist\", \"content\": \",\".join(str(i) for i in range(36))}]\n", + "rows += phase_rows(\n", + " \"Planning\", phase1_tasks, phase1_starts, phase1_durations, \"LightBlue\", \"blue\"\n", + ")\n", + "rows += phase_rows(\n", + " \"Development\", phase2_tasks, phase2_starts, phase2_durations, \"LightGreen\", \"green\"\n", + ")\n", + "rows += phase_rows(\n", + " \"Testing\", phase3_tasks, phase3_starts, phase3_durations, \"LightYellow\", \"orange\"\n", + ")\n", + "\n", + "fig = TikzFigure()\n", + "fig.gantt(0, 35, rows=rows, options=[\"hgrid\", \"vgrid\", \"bar height=.6\"])\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "8", + "metadata": {}, + "source": [ + "## Team resource allocation\n", + "\n", + "The same row-building pattern works when each row represents a person rather\n", + "than a project phase." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": {}, + "outputs": [], + "source": [ + "team_members = [\"Alice\", \"Bob\", \"Charlie\", \"Diana\", \"Eve\"]\n", + "task_starts = [0, 5, 3, 8, 10]\n", + "task_durations = [8, 6, 10, 7, 5]\n", + "\n", + "team_rows = [{\"type\": \"titlelist\", \"content\": \",\".join(str(i) for i in range(19))}]\n", + "team_rows.extend(\n", + " {\n", + " \"type\": \"bar\",\n", + " \"content\": member,\n", + " \"start\": start,\n", + " \"end\": start + duration,\n", + " \"options\": [\"bar/.append style={fill=Coral, draw=DarkRed, fill opacity=.7}\"],\n", + " }\n", + " for member, start, duration in zip(team_members, task_starts, task_durations)\n", + ")\n", + "\n", + "fig = TikzFigure()\n", + "fig.gantt(0, 18, rows=team_rows, options=[\"hgrid\", \"vgrid\"])\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "Rows are ordinary dictionaries, so they can be generated from CSV files,\n", + "database records, or any other application data. Use a `raw` row when a\n", + "pgfgantt command is not covered by the structured row types." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_26_gantt.qmd b/tutorials/advanced/tutorial_26_gantt.qmd deleted file mode 100644 index 7a10dec..0000000 --- a/tutorials/advanced/tutorial_26_gantt.qmd +++ /dev/null @@ -1,180 +0,0 @@ ---- -title: "Gantt Charts" -jupyter: python3 ---- - -Gantt charts are useful for showing a project plan, a publication timeline, or -any other schedule organized along a numeric time axis. tikzfigure provides a -small structured wrapper around LaTeX's [`pgfgantt`](https://ctan.org/pkg/pgfgantt) -package. - -The package is added automatically when you create a chart, so the same figure -can be exported with `generate_standalone()` or compiled with `show()`. - -```{python} -from tikzfigure import TikzFigure -``` - -## A simple project plan - -Pass the first and last time slot to `gantt()` and describe rows with small -dictionaries. The supported row types are `title`, `titlelist`, `group`, -`bar`, `milestone`, `link`, and `raw`. - -```{python} -fig = TikzFigure() -fig.gantt( - 1, - 12, - options=["hgrid", "vgrid"], - rows=[ - {"type": "titlelist", "content": "1,2,3,4,5,6,7,8,9,10,11,12"}, - {"type": "group", "content": "Discovery", "start": 1, "end": 3}, - {"type": "bar", "content": "Research", "start": 1, "end": 2}, - {"type": "bar", "content": "Requirements", "start": 2, "end": 3}, - {"type": "group", "content": "Delivery", "start": 4, "end": 12}, - {"type": "bar", "content": "Implementation", "start": 4, "end": 8}, - {"type": "bar", "content": "Testing", "start": 8, "end": 10}, - {"type": "milestone", "content": "Release", "at": 12}, - ], -) - -print(fig.generate_tikz()) -``` - -Call `fig.show()` in a notebook to compile and display the chart, or save it -with `fig.savefig("project-plan.pdf")`. - -## Dependencies between tasks - -Give bars and milestones a `name` and connect them with `link` rows. The names -are passed to pgfgantt and are used by `ganttlink`. - -```{python} -fig = TikzFigure() -chart = fig.add_gantt_chart( - 1, - 8, - options=["hgrid", "vgrid"], -) - -chart.add_row( - "titlelist", - content="Jan,Feb,Mar,Apr,May,Jun,Jul,Aug", - width=1, - # Text labels must not be run through pgfmath's default numeric - # evaluation, which pgfgantt otherwise applies to titlelist entries. - options=[r"title list options={var=\x}"], -) -chart.add_row( - "bar", - content="Design", - start=1, - end=3, - name="design", - options=["progress=100"], -) -chart.add_row( - "bar", - content="Build", - start=3, - end=6, - name="build", - options=["progress=60"], -) -chart.add_row("milestone", content="Launch", at=8, name="launch") -chart.add_row("link", source="design", target="build") -chart.add_row("link", source="build", target="launch") - -fig.show() -``` - -`add_gantt_chart()` is the explicit spelling; `gantt()` and `add_gantt()` are -short aliases. The returned `GanttChart` can be extended incrementally with -`add_row()`, which is convenient when rows come from application data. - -## A multi-phase project timeline - -This example mirrors a common plotting API: task names, starts, and durations -are kept in parallel Python lists. Convert each phase into Gantt rows and use -per-bar options to give the phases distinct colors. - -```{python} -phase1_tasks = ["Requirements", "Architecture", "UI Design"] -phase1_starts = [0, 3, 6] -phase1_durations = [3, 3, 4] - -phase2_tasks = ["Backend Dev", "Frontend Dev", "Integration"] -phase2_starts = [10, 10, 20] -phase2_durations = [10, 10, 5] - -phase3_tasks = ["Unit Tests", "Integration Tests", "UAT"] -phase3_starts = [25, 28, 32] -phase3_durations = [3, 4, 3] - - -def phase_rows(title, tasks, starts, durations, fill, draw): - rows = [{"type": "group", "content": title, "start": min(starts), "end": max( - start + duration for start, duration in zip(starts, durations) - )}] - rows.extend( - { - "type": "bar", - "content": task, - "start": start, - "end": start + duration, - # pgfgantt bars are styled via the `bar` style itself, not raw - # `fill=`/`draw=` keys (those live outside the /pgfgantt family). - "options": [f"bar/.append style={{fill={fill}, draw={draw}}}"], - } - for task, start, duration in zip(tasks, starts, durations) - ) - return rows - - -rows = [{"type": "titlelist", "content": ",".join(str(i) for i in range(36))}] -rows += phase_rows( - "Planning", phase1_tasks, phase1_starts, phase1_durations, "LightBlue", "blue" -) -rows += phase_rows( - "Development", phase2_tasks, phase2_starts, phase2_durations, "LightGreen", "green" -) -rows += phase_rows( - "Testing", phase3_tasks, phase3_starts, phase3_durations, "LightYellow", "orange" -) - -fig = TikzFigure() -fig.gantt(0, 35, rows=rows, options=["hgrid", "vgrid", "bar height=.6"]) -fig.show() -``` - -## Team resource allocation - -The same row-building pattern works when each row represents a person rather -than a project phase. - -```{python} -team_members = ["Alice", "Bob", "Charlie", "Diana", "Eve"] -task_starts = [0, 5, 3, 8, 10] -task_durations = [8, 6, 10, 7, 5] - -team_rows = [{"type": "titlelist", "content": ",".join(str(i) for i in range(19))}] -team_rows.extend( - { - "type": "bar", - "content": member, - "start": start, - "end": start + duration, - "options": ["bar/.append style={fill=Coral, draw=DarkRed, fill opacity=.7}"], - } - for member, start, duration in zip(team_members, task_starts, task_durations) -) - -fig = TikzFigure() -fig.gantt(0, 18, rows=team_rows, options=["hgrid", "vgrid"]) -fig.show() -``` - -Rows are ordinary dictionaries, so they can be generated from CSV files, -database records, or any other application data. Use a `raw` row when a -pgfgantt command is not covered by the structured row types. diff --git a/tutorials/advanced/tutorial_27_parsing_tikz.ipynb b/tutorials/advanced/tutorial_27_parsing_tikz.ipynb new file mode 100644 index 0000000..63d4bdd --- /dev/null +++ b/tutorials/advanced/tutorial_27_parsing_tikz.ipynb @@ -0,0 +1,356 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Parsing TikZ\n", + "\n", + "Most tutorials start from Python and produce TikZ. This one goes the other way:\n", + "`TikzFigure.from_tikz_code` reads existing TikZ source and rebuilds it as\n", + "tikzfigure objects, so you can edit a figure you already have - or a figure\n", + "someone sent you - with the Python API.\n", + "\n", + "This tutorial covers: - importing a `tikzpicture` and editing it - inspecting\n", + "what the parser understood - the raw-TikZ fallback for constructs tikzfigure\n", + "does not model - strict mode and parse errors - turning TikZ into Python code" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "from tikzfigure import TikzFigure\n", + "from tikzfigure.parser import TikzParseError, parse_tikz" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Importing an existing figure\n", + "\n", + "Pass any `tikzpicture` to `from_tikz_code`. Nodes, paths, coordinates, scopes,\n", + "loops, layers and definitions become real objects." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "source = r\"\"\"\n", + "\\begin{tikzpicture}[scale=1.2, box/.style={draw, rounded corners, fill=blue!15}]\n", + " \\node[box] (input) at (0, 0) {Input};\n", + " \\node[box] (model) at (3, 0) {Model};\n", + " \\node[box] (output) at (6, 0) {Output};\n", + " \\draw[->, thick] (input) -- node[above] {$x$} (model);\n", + " \\draw[->, thick] (model) -- node[above] {$y$} (output);\n", + "\\end{tikzpicture}\n", + "\"\"\"\n", + "\n", + "fig = TikzFigure.from_tikz_code(source)\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4", + "metadata": {}, + "source": [ + "The figure is a normal `TikzFigure`, so you can look up the nodes it contains\n", + "and keep building on it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": {}, + "outputs": [], + "source": [ + "model = fig.layers.get_node(\"model\")\n", + "print(f\"{model.label!r} at ({model.x}, {model.y}) with options {model.options}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "6", + "metadata": {}, + "outputs": [], + "source": [ + "loss = fig.add_node(3, -2, label=\"loss\", content=\"Loss\", options=[\"box\"], fill=\"red!20\")\n", + "fig.draw([\"output\", \"loss\"], options=[\"->\", \"dashed\"], color=\"red\")\n", + "fig.draw([\"loss\", \"model\"], options=[\"->\", \"dashed\"], color=\"red\")\n", + "fig.show()" + ] + }, + { + "cell_type": "markdown", + "id": "7", + "metadata": {}, + "source": [ + "## Seeing what the parser understood\n", + "\n", + "`parse_tikz` returns the figure together with one diagnostic per statement, so\n", + "you can check what became a tikzfigure object and what did not." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": {}, + "outputs": [], + "source": [ + "result = parse_tikz(source)\n", + "\n", + "print(f\"coverage: {result.coverage:.0%}\")\n", + "for diagnostic in result.diagnostics:\n", + " print(diagnostic)" + ] + }, + { + "cell_type": "markdown", + "id": "9", + "metadata": {}, + "source": [ + "## Raw TikZ fallback\n", + "\n", + "TikZ is much larger than tikzfigure's API. Anything the library cannot\n", + "represent is kept verbatim as raw TikZ instead of being dropped, so the figure\n", + "still renders exactly as before. Here `edge` and `plot` have no Python\n", + "equivalent yet:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "10", + "metadata": {}, + "outputs": [], + "source": [ + "mixed = r\"\"\"\n", + "\\begin{tikzpicture}\n", + " \\node[draw, circle] (a) at (0, 0) {a};\n", + " \\node[draw, circle] (b) at (3, 0) {b};\n", + " \\draw (a) edge[bend left, ->] (b);\n", + " \\draw[domain=0:3, smooth, blue] plot (\\x, {0.3*\\x*\\x - 1});\n", + "\\end{tikzpicture}\n", + "\"\"\"\n", + "\n", + "result = parse_tikz(mixed)\n", + "print(result.summary())" + ] + }, + { + "cell_type": "markdown", + "id": "11", + "metadata": {}, + "source": [ + "`summary()` lists the statements that stayed raw and why. The figure still\n", + "draws the curve and the bent arrow:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": {}, + "outputs": [], + "source": [ + "result.figure.show()" + ] + }, + { + "cell_type": "markdown", + "id": "13", + "metadata": {}, + "source": [ + "Because those statements survive untouched, you can keep editing the parts that\n", + "*were* understood:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "14", + "metadata": {}, + "outputs": [], + "source": [ + "b = result.figure.layers.get_node(\"b\")\n", + "b.kwargs[\"fill\"] = \"green!30\"\n", + "result.figure.show()" + ] + }, + { + "cell_type": "markdown", + "id": "15", + "metadata": {}, + "source": [ + "## Strict mode\n", + "\n", + "When you want to know that a figure is fully represented as objects - for\n", + "instance before rewriting it programmatically - pass `strict=True` and handle\n", + "the error:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": {}, + "outputs": [], + "source": [ + "try:\n", + " TikzFigure.from_tikz_code(mixed, strict=True)\n", + "except TikzParseError as error:\n", + " print(error)" + ] + }, + { + "cell_type": "markdown", + "id": "17", + "metadata": {}, + "source": [ + "Malformed input is reported with a line number rather than failing silently:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "18", + "metadata": {}, + "outputs": [], + "source": [ + "broken = \"\\\\begin{tikzpicture}\\n \\\\node at (0, 0) {unclosed;\\n\\\\end{tikzpicture}\"\n", + "\n", + "try:\n", + " TikzFigure.from_tikz_code(broken)\n", + "except TikzParseError as error:\n", + " print(error)" + ] + }, + { + "cell_type": "markdown", + "id": "19", + "metadata": {}, + "source": [ + "## From TikZ to Python code\n", + "\n", + "`to_python()` writes the Python that rebuilds a figure. Combined with the\n", + "parser, it converts existing TikZ into a tikzfigure script:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": {}, + "outputs": [], + "source": [ + "print(TikzFigure.from_tikz_code(source).to_python())" + ] + }, + { + "cell_type": "markdown", + "id": "21", + "metadata": {}, + "source": [ + "The generated code is executed and compared against the original figure before\n", + "it is returned, so it is verified rather than merely plausible. The same thing\n", + "is available on the command line:\n", + "\n", + "```bash\n", + "python -m tikzfigure.codegen figure.tikz -o figure.py\n", + "```\n", + "\n", + "## What survives a round trip\n", + "\n", + "A statement is only turned into an object when that object renders back to\n", + "equivalent TikZ, which means importing a figure never quietly changes it:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "22", + "metadata": {}, + "outputs": [], + "source": [ + "imported = TikzFigure.from_tikz_code(source)\n", + "reparsed = TikzFigure.from_tikz_code(imported.generate_tikz())\n", + "\n", + "print(imported.generate_tikz() == reparsed.generate_tikz())" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "25", + "metadata": {}, + "source": [ + "The `tikz_catalogue/` directory in the repository collects TikZ documents used\n", + "to measure this. Run\n", + "\n", + "```bash\n", + "python -m tikzfigure.parser.catalogue tikz_catalogue --details\n", + "```\n", + "\n", + "for a report of how much of each example maps onto tikzfigure objects, and\n", + "which TikZ constructs still fall back to raw output." + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/advanced/tutorial_27_parsing_tikz.qmd b/tutorials/advanced/tutorial_27_parsing_tikz.qmd deleted file mode 100644 index f179ab1..0000000 --- a/tutorials/advanced/tutorial_27_parsing_tikz.qmd +++ /dev/null @@ -1,168 +0,0 @@ ---- -title: "Parsing TikZ" -jupyter: python3 ---- - -Most tutorials start from Python and produce TikZ. This one goes the other way: -`TikzFigure.from_tikz_code` reads existing TikZ source and rebuilds it as -tikzfigure objects, so you can edit a figure you already have - or a figure -someone sent you - with the Python API. - -This tutorial covers: - importing a `tikzpicture` and editing it - inspecting -what the parser understood - the raw-TikZ fallback for constructs tikzfigure -does not model - strict mode and parse errors - turning TikZ into Python code - -```{python} -from tikzfigure import TikzFigure -from tikzfigure.parser import TikzParseError, parse_tikz -``` - -## Importing an existing figure - -Pass any `tikzpicture` to `from_tikz_code`. Nodes, paths, coordinates, scopes, -loops, layers and definitions become real objects. - -```{python} -source = r""" -\begin{tikzpicture}[scale=1.2, box/.style={draw, rounded corners, fill=blue!15}] - \node[box] (input) at (0, 0) {Input}; - \node[box] (model) at (3, 0) {Model}; - \node[box] (output) at (6, 0) {Output}; - \draw[->, thick] (input) -- node[above] {$x$} (model); - \draw[->, thick] (model) -- node[above] {$y$} (output); -\end{tikzpicture} -""" - -fig = TikzFigure.from_tikz_code(source) -fig.show() -``` - -The figure is a normal `TikzFigure`, so you can look up the nodes it contains -and keep building on it: - -```{python} -model = fig.layers.get_node("model") -print(f"{model.label!r} at ({model.x}, {model.y}) with options {model.options}") -``` - -```{python} -loss = fig.add_node(3, -2, label="loss", content="Loss", options=["box"], fill="red!20") -fig.draw(["output", "loss"], options=["->", "dashed"], color="red") -fig.draw(["loss", "model"], options=["->", "dashed"], color="red") -fig.show() -``` - -## Seeing what the parser understood - -`parse_tikz` returns the figure together with one diagnostic per statement, so -you can check what became a tikzfigure object and what did not. - -```{python} -result = parse_tikz(source) - -print(f"coverage: {result.coverage:.0%}") -for diagnostic in result.diagnostics: - print(diagnostic) -``` - -## Raw TikZ fallback - -TikZ is much larger than tikzfigure's API. Anything the library cannot -represent is kept verbatim as raw TikZ instead of being dropped, so the figure -still renders exactly as before. Here `edge` and `plot` have no Python -equivalent yet: - -```{python} -mixed = r""" -\begin{tikzpicture} - \node[draw, circle] (a) at (0, 0) {a}; - \node[draw, circle] (b) at (3, 0) {b}; - \draw (a) edge[bend left, ->] (b); - \draw[domain=0:3, smooth, blue] plot (\x, {0.3*\x*\x - 1}); -\end{tikzpicture} -""" - -result = parse_tikz(mixed) -print(result.summary()) -``` - -`summary()` lists the statements that stayed raw and why. The figure still -draws the curve and the bent arrow: - -```{python} -result.figure.show() -``` - -Because those statements survive untouched, you can keep editing the parts that -*were* understood: - -```{python} -b = result.figure.layers.get_node("b") -b.kwargs["fill"] = "green!30" -result.figure.show() -``` - -## Strict mode - -When you want to know that a figure is fully represented as objects - for -instance before rewriting it programmatically - pass `strict=True` and handle -the error: - -```{python} -try: - TikzFigure.from_tikz_code(mixed, strict=True) -except TikzParseError as error: - print(error) -``` - -Malformed input is reported with a line number rather than failing silently: - -```{python} -broken = "\\begin{tikzpicture}\n \\node at (0, 0) {unclosed;\n\\end{tikzpicture}" - -try: - TikzFigure.from_tikz_code(broken) -except TikzParseError as error: - print(error) -``` - -## From TikZ to Python code - -`to_python()` writes the Python that rebuilds a figure. Combined with the -parser, it converts existing TikZ into a tikzfigure script: - -```{python} -print(TikzFigure.from_tikz_code(source).to_python()) -``` - -The generated code is executed and compared against the original figure before -it is returned, so it is verified rather than merely plausible. The same thing -is available on the command line: - -```bash -python -m tikzfigure.codegen figure.tikz -o figure.py -``` - -## What survives a round trip - -A statement is only turned into an object when that object renders back to -equivalent TikZ, which means importing a figure never quietly changes it: - -```{python} -imported = TikzFigure.from_tikz_code(source) -reparsed = TikzFigure.from_tikz_code(imported.generate_tikz()) - -print(imported.generate_tikz() == reparsed.generate_tikz()) -``` - -{{< include includes/tikz_code_callout.qmd >}} - -The `tikz_catalogue/` directory in the repository collects TikZ documents used -to measure this. Run - -```bash -python -m tikzfigure.parser.catalogue tikz_catalogue --details -``` - -for a report of how much of each example maps onto tikzfigure objects, and -which TikZ constructs still fall back to raw output. diff --git a/tutorials/drawing/tutorial_03_paths.ipynb b/tutorials/drawing/tutorial_03_paths.ipynb new file mode 100644 index 0000000..a0bf879 --- /dev/null +++ b/tutorials/drawing/tutorial_03_paths.ipynb @@ -0,0 +1,1750 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# Paths and Shapes\n", + "\n", + "Beyond simple node-to-node connections, tikzfigure lets you draw arbitrary paths\n", + "from lists of `(x, y)` coordinates.\n", + "\n", + "- drawing open and closed coordinate paths\n", + "- `filldraw` for filled closed shapes\n", + "- rounded corners\n", + "- per-segment styling with `segment_options`\n", + "- node paths and `.center` anchors for correct fills\n", + "- custom shapes (heart, star)\n", + "- parametric curves with NumPy (spiral, sun)\n", + "- `fill` for stroke-free fills and even-odd rule\n", + "- `clip` for clipping regions\n", + "- `path` for invisible named paths and mid-path labels" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import numpy as np\n", + "\n", + "from tikzfigure import TikzFigure, arrows, styles, units" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Open and closed coordinate paths\n", + "\n", + "Pass a list of `[x, y]` pairs (or `(x, y)` tuples) to `fig.draw()`. Set\n", + "`cycle=True` to close the path back to the starting point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Open path - a zigzag\n", + "zigzag = [[0, 0], [1, 1], [2, 0], [3, 1], [4, 0]]\n", + "fig.draw(zigzag, line_width=2, color=\"blue\")\n", + "\n", + "# Closed path - a triangle\n", + "triangle = [[6, 0], [8, 0], [7, 1.5]]\n", + "fig.draw(\n", + " triangle,\n", + " cycle=True,\n", + " line_width=2,\n", + " color=\"red\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Filled shapes with `filldraw`\n", + "\n", + "`fig.filldraw()` is shorthand for a closed path that is both drawn (outline) and\n", + "filled. You can also use `fig.draw(..., fill=\"color\")` for the same effect." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Pentagon via filldraw\n", + "angles = np.linspace(np.pi / 2, np.pi / 2 + 2 * np.pi, 6)[:-1]\n", + "pentagon = list(\n", + " zip(\n", + " np.cos(angles) * 2 + 2,\n", + " np.sin(angles) * 2 + 2,\n", + " ),\n", + ")\n", + "fig.filldraw(\n", + " pentagon,\n", + " cycle=True,\n", + " line_width=2,\n", + " color=\"purple\",\n", + " fill=\"purple!20\",\n", + ")\n", + "\n", + "# Same via draw(..., fill=...)\n", + "hexagon_angles = np.linspace(0, 2 * np.pi, 7)[:-1]\n", + "hexagon = list(\n", + " zip(\n", + " np.cos(hexagon_angles) * 2 + 7,\n", + " np.sin(hexagon_angles) * 2 + 2,\n", + " ),\n", + ")\n", + "fig.draw(\n", + " hexagon,\n", + " cycle=True,\n", + " line_width=2,\n", + " color=\"teal\",\n", + " fill=\"teal!20\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Rounded corners\n", + "\n", + "The `rounded_corners` parameter (in pt) smooths the corners of any path." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Same square, with increasing corner radius\n", + "for i, radius in enumerate([0, 5, 15, 30]):\n", + " x0 = i * 3.5\n", + " square = [[x0, 0], [x0 + 2, 0], [x0 + 2, 2], [x0, 2]]\n", + " fig.draw(\n", + " square,\n", + " cycle=True,\n", + " line_width=2,\n", + " color=\"black\",\n", + " fill=\"orange!20\",\n", + " rounded_corners=f\"{radius}pt\",\n", + " )\n", + " fig.node(\n", + " x0 + 1,\n", + " -0.5,\n", + " content=f\"{radius}pt\",\n", + " draw=\"none\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Per-segment styling\n", + "\n", + "`segment_options` accepts a list of per-connector option dicts, one for each\n", + "`to` step in the path. Each dict uses keyword-style TikZ options (underscores\n", + "become spaces) plus an optional `\"options\"` key for flag-style items like\n", + "`styles.dashed` or `arrows.forward`.\n", + "\n", + "Here four segments are given different colors and styles in a single `draw`\n", + "call:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "waypoints = [[0, 0], [2, 2], [4, 0], [6, 2], [8, 0]]\n", + "\n", + "fig.draw(\n", + " waypoints,\n", + " line_width=2,\n", + " segment_options=[\n", + " {\"color\": \"red\"},\n", + " {\"color\": \"blue\", \"options\": [styles.dashed], \"line_width\": 1.5},\n", + " {\"color\": \"green!60!black\", \"line_width\": 3},\n", + " {\"color\": \"purple\", \"options\": [styles.dotted], \"line_width\": 2},\n", + " ],\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "Per-segment bending works the same way. Here three nodes are connected with\n", + "arrows where each segment curves in a different direction:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "specs = [(\"src\", 0, 1, \"teal\"), (\"mid\", 4, 3, \"orange\"), (\"dst\", 8, 1, \"purple\")]\n", + "for label, x, y, color in specs:\n", + " fig.node(\n", + " (x, y),\n", + " label=label,\n", + " shape=\"circle\",\n", + " fill=f\"{color}!25\",\n", + " draw=f\"{color}!70\",\n", + " minimum_size=1.0 * units.cm,\n", + " content=label.upper(),\n", + " )\n", + "\n", + "fig.draw(\n", + " [\"src.center\", \"mid.center\", \"dst.center\"],\n", + " arrows=arrows.forward,\n", + " options=[styles.thick],\n", + " segment_options=[\n", + " {\"bend_left\": 30}, # src → mid curves upward-left\n", + " {\"bend_right\": 25}, # mid → dst curves downward-right\n", + " ],\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## Fluent node chaining\n", + "\n", + "If you're connecting named nodes, `.to(...)` is a concise alternative to\n", + "building `segment_options` by hand. Options passed to `.to(...)` affect only\n", + "that connector." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "A = fig.node(\n", + " (0, 1),\n", + " label=\"A\",\n", + " shape=\"circle\",\n", + " fill=\"teal!25\",\n", + " draw=\"teal!70\",\n", + " minimum_size=1.0 * units.cm,\n", + " content=\"A\",\n", + ")\n", + "B = fig.node(\n", + " (4, 3),\n", + " label=\"B\",\n", + " shape=\"circle\",\n", + " fill=\"orange!25\",\n", + " draw=\"orange!70\",\n", + " minimum_size=\"1cm\",\n", + " content=\"B\",\n", + ")\n", + "C = fig.node(\n", + " (8, 1),\n", + " label=\"C\",\n", + " shape=\"circle\",\n", + " fill=\"purple!25\",\n", + " draw=\"purple!70\",\n", + " minimum_size=\"1cm\",\n", + " content=\"C\",\n", + ")\n", + "\n", + "fig.draw(\n", + " A.to(B).to(C, options=styles.bend_left()),\n", + " arrows=arrows.forward,\n", + " options=[styles.thick],\n", + " color=\"teal!70\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "Arc segments fit into the same fluent API. Unlike `.to(...)`, `.arc(...)` does\n", + "not consume another waypoint — it appends a TikZ `arc[...]` operation at the\n", + "current point, and you can keep chaining afterward:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "A = fig.node(\n", + " (0, 0),\n", + " label=\"A\",\n", + " shape=\"circle\",\n", + " fill=\"teal!25\",\n", + " draw=\"teal!70\",\n", + " minimum_size=\"1cm\",\n", + " content=\"A\",\n", + ")\n", + "B = fig.node(\n", + " (4, 0),\n", + " label=\"B\",\n", + " shape=\"circle\",\n", + " fill=\"orange!25\",\n", + " draw=\"orange!70\",\n", + " minimum_size=\"1cm\",\n", + " content=\"B\",\n", + ")\n", + "C = fig.node(\n", + " (7, 1.5),\n", + " label=\"C\",\n", + " shape=\"circle\",\n", + " fill=\"purple!25\",\n", + " draw=\"purple!70\",\n", + " minimum_size=\"1cm\",\n", + " content=\"C\",\n", + ")\n", + "\n", + "fig.draw(\n", + " A.to(B)\n", + " .arc(start_angle=-90, end_angle=90, radius=\"1.5cm\")\n", + " .to(C, options=styles.bend_right()),\n", + " arrows=arrows.forward,\n", + " options=[styles.thick],\n", + " color=\"blue!70\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "Use `\"out\"` and `\"in\"` keys for explicit departure and arrival angles (in\n", + "degrees). This gives precise control over how each segment enters and leaves its\n", + "endpoints — useful when `bend_left`/`bend_right` doesn't give you the exact\n", + "shape you need:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "for label, x, y, color in [\n", + " (\"A\", 0, 2, \"blue\"),\n", + " (\"B\", 5, 4, \"orange\"),\n", + " (\"C\", 10, 2, \"purple\"),\n", + "]:\n", + " fig.node(\n", + " (x, y),\n", + " label=label,\n", + " shape=\"circle\",\n", + " fill=f\"{color}!20\",\n", + " draw=f\"{color}!70\",\n", + " minimum_size=\"1.2cm\",\n", + " content=f\"${label}$\",\n", + " )\n", + "\n", + "fig.draw(\n", + " [\"A.center\", \"B.center\", \"C.center\"],\n", + " arrows=arrows.forward,\n", + " options=[styles.thick],\n", + " segment_options=[\n", + " {\"out\": 45, \"in\": 195}, # A→B: leave at 45°, arrive at 195°\n", + " {\"out\": -15, \"in\": 135}, # B→C: leave at -15°, arrive at 135°\n", + " ],\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "`looseness` scales how far the control handles extend, making the same bend\n", + "either tighter (`looseness < 1`) or more exaggerated (`looseness > 1`). Here the\n", + "two segments of the same symmetric path use opposite looseness values:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "for label, x, y in [(\"A\", 0, 2), (\"B\", 5, 2), (\"C\", 10, 2)]:\n", + " fig.node(\n", + " (x, y),\n", + " label=label,\n", + " shape=\"circle\",\n", + " fill=\"gray!15\",\n", + " draw=\"gray!60\",\n", + " minimum_size=\"1cm\",\n", + " content=f\"${label}$\",\n", + " )\n", + "\n", + "fig.draw(\n", + " [\"A.center\", \"B.center\", \"C.center\"],\n", + " arrows=arrows.forward,\n", + " options=[styles.thick],\n", + " segment_options=[\n", + " {\"bend_left\": 50, \"looseness\": 2.5}, # A→B: wide exaggerated arc\n", + " {\"bend_right\": 50, \"looseness\": 0.5}, # B→C: tight compressed arc\n", + " ],\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "All segment options can be combined freely. Here each segment gets its own\n", + "color, line style, and curve geometry in a single `draw` call:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "for label, x, y, color in [\n", + " (\"A\", 0, 0, \"teal\"),\n", + " (\"B\", 4, 3, \"red\"),\n", + " (\"C\", 8, 0, \"blue\"),\n", + "]:\n", + " fig.node(\n", + " (x, y),\n", + " label=label,\n", + " shape=\"circle\",\n", + " fill=f\"{color}!20\",\n", + " draw=f\"{color}!60\",\n", + " minimum_size=\"1.2cm\",\n", + " content=f\"${label}$\",\n", + " )\n", + "\n", + "fig.draw(\n", + " [\"A.center\", \"B.center\", \"C.center\"],\n", + " segment_options=[\n", + " { # A→B\n", + " \"options\": [arrows.forward],\n", + " \"color\": \"teal!80\",\n", + " \"line_width\": 2,\n", + " \"out\": 60,\n", + " \"in\": 240,\n", + " },\n", + " { # B→C\n", + " \"options\": [arrows.forward, styles.dashed],\n", + " \"color\": \"red!70\",\n", + " \"line_width\": 1.5,\n", + " \"out\": -60,\n", + " \"in\": 120,\n", + " },\n", + " ],\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "## Node paths and `.center` anchors\n", + "\n", + "When you fill a closed path that goes through named nodes, TikZ routes to each\n", + "node's *border* by default. For circular nodes this means the filled area has\n", + "indented corners — it doesn't look like a triangle.\n", + "\n", + "Appending `.center` to a node label in the path list routes the segment through\n", + "the node's geometric center instead. The path then bounds a proper filled\n", + "polygon." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "node_specs = [\n", + " (\"top\", 3, 4, \"blue\", r\"$v_1$\"),\n", + " (\"bl\", 0, 0, \"blue\", r\"$v_2$\"),\n", + " (\"br\", 6, 0, \"blue\", r\"$v_3$\"),\n", + "]\n", + "for label, x, y, color, text in node_specs:\n", + " fig.node(\n", + " (x, y),\n", + " label=label,\n", + " shape=\"circle\",\n", + " draw=f\"{color}!60\",\n", + " fill=\"white\",\n", + " minimum_size=\"1.4cm\",\n", + " content=text,\n", + " )\n", + "\n", + "# Border anchors: the fill \"leaks\" under the node circles\n", + "fig.draw(\n", + " [\"top\", \"bl\", \"br\"],\n", + " cycle=True,\n", + " fill=\"red!25\",\n", + " draw=\"red!60\",\n", + " line_width=1,\n", + " fill_opacity=0.6,\n", + ")\n", + "\n", + "# .center anchors: fills the exact triangle spanned by node centres\n", + "fig.draw(\n", + " [\"top.center\", \"bl.center\", \"br.center\"],\n", + " cycle=True,\n", + " fill=\"blue!30\",\n", + " draw=\"blue!70\",\n", + " line_width=1.5,\n", + " fill_opacity=0.7,\n", + ")\n", + "\n", + "# Labels outside the shapes\n", + "fig.node(\n", + " (7.5, 2),\n", + " content=r\"\\textcolor{red!60}{border anchors}\",\n", + " draw=\"none\",\n", + " anchor=\"west\",\n", + " font=r\"\\small\",\n", + ")\n", + "fig.node(\n", + " (7.5, 1),\n", + " content=r\"\\textcolor{blue!70}{.center anchors}\",\n", + " draw=\"none\",\n", + " anchor=\"west\",\n", + " font=r\"\\small\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "You can mix anchors: only the nodes that need a specific anchor require the dot\n", + "suffix. Any TikZ anchor string works — `\"north\"`, `\"south east\"`, `\"west\"`, etc." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "fig.node(\n", + " (0, 0),\n", + " label=\"A\",\n", + " shape=\"rectangle\",\n", + " draw=\"black\",\n", + " fill=\"gray!15\",\n", + " minimum_width=\"2cm\",\n", + " minimum_height=\"1cm\",\n", + " content=\"A\",\n", + ")\n", + "fig.node(\n", + " (5, 2),\n", + " label=\"B\",\n", + " shape=\"rectangle\",\n", + " draw=\"black\",\n", + " fill=\"gray!15\",\n", + " minimum_width=\"2cm\",\n", + " minimum_height=\"1cm\",\n", + " content=\"B\",\n", + ")\n", + "fig.node(\n", + " (10, 0),\n", + " label=\"C\",\n", + " shape=\"rectangle\",\n", + " draw=\"black\",\n", + " fill=\"gray!15\",\n", + " minimum_width=\"2cm\",\n", + " minimum_height=\"1cm\",\n", + " content=\"C\",\n", + ")\n", + "\n", + "# Connect from the east side of A, enter B from the west, leave from east, arrive at C.west\n", + "fig.draw(\n", + " [\"A.east\", \"B.west\"],\n", + " arrows=arrows.forward,\n", + " options=[styles.thick],\n", + " color=\"teal\",\n", + " segment_options=[{\"bend_left\": 15}],\n", + ")\n", + "fig.draw(\n", + " [\"B.east\", \"C.west\"],\n", + " arrows=arrows.forward,\n", + " options=[styles.thick],\n", + " color=\"orange\",\n", + " segment_options=[{\"bend_right\": 15}],\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "## Heart shape\n", + "\n", + "A heart with circular lobes drawn by `fig.arc()` and a pointed bottom built from\n", + "a chained path." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "51", + "metadata": {}, + "outputs": [], + "source": [ + "width, height = 1.75, 2.0\n", + "\n", + "fig = TikzFigure()\n", + "\n", + "fig.colorlet(\"lightred\", \"red!40!white\")\n", + "\n", + "A = fig.node(-width, height)\n", + "B = fig.node((0, 0))\n", + "C = fig.node(width, height)\n", + "D = fig.node(0, height)\n", + "\n", + "fig.draw(\n", + " A.to(B, options=[\"out=-90, in=135\"])\n", + " .to(C, options=[\"out=45, in=-90\"])\n", + " .to(D, options=[\"in=80, out=100\"])\n", + " .to(A, options=[\"in=80, out=100\"]),\n", + " color=\"red\",\n", + " line_width=4,\n", + " cycle=True,\n", + " center=True,\n", + " fill=\"lightred\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "52", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "53", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "54", + "metadata": {}, + "source": [ + "## Star shape\n", + "\n", + "A five-pointed star drawn from six points (the last one closes back to the\n", + "first)." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "55", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "star = [[0, 0], [1, 2.5], [2, 0], [-0.5, 1.5], [2.5, 1.5], [0, 0]]\n", + "\n", + "fig.draw(\n", + " star,\n", + " line_width=3,\n", + " cycle=True,\n", + " color=\"Gold\",\n", + " fill=\"yellow!50!white\",\n", + ")\n", + "\n", + "fig.show()\n", + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "56", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "57", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "58", + "metadata": {}, + "source": [ + "## Parametric curves with NumPy\n", + "\n", + "Any iterable of `(x, y)` pairs works as a path, so NumPy arrays plug in\n", + "naturally for smooth parametric curves." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "59", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Spiral\n", + "N = 300\n", + "theta = np.linspace(0, 8 * np.pi, N)\n", + "r = np.linspace(0.1, 3, N)\n", + "spiral = list(\n", + " zip(r * np.cos(theta) + 4, r * np.sin(theta) + 4),\n", + ")\n", + "fig.draw(spiral, line_width=2, color=\"teal\")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "60", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "61", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "62", + "metadata": {}, + "source": [ + "## Sun: circle core + radial rays\n", + "\n", + "A filled circle for the core, plus short line segments for rays at equal angles." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "63", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "cx, cy, R = 4, 4, 1.0 # center and core radius\n", + "\n", + "# Core\n", + "theta = np.linspace(0, 2 * np.pi, 60)\n", + "core = list(\n", + " zip(\n", + " cx + R * np.cos(theta),\n", + " cy + R * np.sin(theta),\n", + " ),\n", + ")\n", + "fig.filldraw(\n", + " core,\n", + " cycle=True,\n", + " line_width=2,\n", + " color=\"orange\",\n", + " fill=\"yellow!60\",\n", + ")\n", + "\n", + "# Rays\n", + "for angle in np.linspace(\n", + " 0,\n", + " 2 * np.pi,\n", + " 12,\n", + " endpoint=False,\n", + "):\n", + " x0 = cx + R * np.cos(angle)\n", + " y0 = cy + R * np.sin(angle)\n", + " x1 = cx + (R + 0.7) * np.cos(angle)\n", + " y1 = cy + (R + 0.7) * np.sin(angle)\n", + " fig.draw(\n", + " [(x0, y0), (x1, y1)],\n", + " line_width=2,\n", + " color=\"orange\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "64", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "65", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "66", + "metadata": {}, + "source": [ + "## Fill without stroke\n", + "\n", + "`fig.fill()` fills a closed path without drawing any outline — the equivalent of\n", + "`\\fill[...]` in TikZ. Use it when you want pure flat colour with no border." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "67", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Three overlapping filled triangles — no outlines at all\n", + "triangles = [\n", + " ([[1, 0], [4, 0], [2.5, 2.5]], \"red!60\"),\n", + " ([[2, 0], [5, 0], [3.5, 2.5]], \"blue!50\"),\n", + " ([[3, 0], [6, 0], [4.5, 2.5]], \"green!60\"),\n", + "]\n", + "for pts, color in triangles:\n", + " fig.fill(\n", + " pts,\n", + " cycle=True,\n", + " fill=color,\n", + " fill_opacity=0.6,\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "68", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "69", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "70", + "metadata": {}, + "source": [ + "The `even_odd_rule` flag corrects the fill for self-intersecting paths. Without\n", + "it, TikZ fills the interior using the non-zero winding rule, which fills the\n", + "overlapping centre region twice — visually it looks like a solid shape. With\n", + "`even_odd_rule=True` the centre hole is punched out correctly." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "71", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# A five-pointed star is self-intersecting\n", + "angles = np.linspace(np.pi / 2, np.pi / 2 + 4 * np.pi, 6)[:-1]\n", + "star_pts = list(\n", + " zip(\n", + " np.cos(angles) * 2 + 2,\n", + " np.sin(angles) * 2 + 2,\n", + " ),\n", + ")\n", + "\n", + "# Default winding rule: centre is filled solid\n", + "fig.fill(\n", + " star_pts,\n", + " cycle=True,\n", + " fill=\"orange!80\",\n", + " fill_opacity=0.9,\n", + ")\n", + "fig.node(\n", + " (2, -0.3),\n", + " content=\"winding rule (default)\",\n", + " draw=\"none\",\n", + " font=r\"\\small\",\n", + " anchor=\"center\",\n", + ")\n", + "\n", + "# Even-odd rule: centre is hollow\n", + "angles2 = np.linspace(np.pi / 2, np.pi / 2 + 4 * np.pi, 6)[:-1]\n", + "star_pts2 = list(\n", + " zip(\n", + " np.cos(angles2) * 2 + 8,\n", + " np.sin(angles2) * 2 + 2,\n", + " ),\n", + ")\n", + "fig.fill(\n", + " star_pts2,\n", + " cycle=True,\n", + " fill=\"teal!70\",\n", + " fill_opacity=0.9,\n", + " even_odd_rule=True,\n", + ")\n", + "fig.node(\n", + " (8, -0.3),\n", + " content=\"even odd rule\",\n", + " draw=\"none\",\n", + " font=r\"\\small\",\n", + " anchor=\"center\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "72", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "73", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "74", + "metadata": {}, + "source": [ + "## Clip\n", + "\n", + "`fig.clip()` installs a clipping path — all subsequent drawing is masked to the\n", + "given boundary. Wrap the clip together with the clipped content inside a\n", + "`\\begin{scope}...\\end{scope}` via `fig.raw()` so the clip doesn't leak into the\n", + "rest of the figure." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "75", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Draw a full circle for reference (unclipped)\n", + "fig.draw(\n", + " list(\n", + " zip(\n", + " np.cos(np.linspace(0, 2 * np.pi, 60)) * 2 + 2,\n", + " np.sin(np.linspace(0, 2 * np.pi, 60)) * 2 + 2,\n", + " ),\n", + " ),\n", + " cycle=True,\n", + " color=\"gray!40\",\n", + " line_width=1,\n", + ")\n", + "\n", + "# Open a scope, clip to a rectangle, then draw a dense grid inside\n", + "fig.raw(r\"\\begin{scope}\")\n", + "clip_rect = [[0.5, 0.5], [3.5, 0.5], [3.5, 3.5], [0.5, 3.5]]\n", + "fig.clip(clip_rect, cycle=True)\n", + "\n", + "for i in np.linspace(0, 4, 20):\n", + " fig.draw(\n", + " [(0, i), (4, i)],\n", + " color=\"blue!40\",\n", + " line_width=0.4,\n", + " )\n", + " fig.draw(\n", + " [(i, 0), (i, 4)],\n", + " color=\"blue!40\",\n", + " line_width=0.4,\n", + " )\n", + "\n", + "fig.raw(r\"\\end{scope}\")\n", + "\n", + "# Anything drawn after the scope is unclipped again\n", + "fig.node(\n", + " (2, -0.5),\n", + " content=\"clipped grid inside circle\",\n", + " draw=\"none\",\n", + " font=r\"\\small\",\n", + " anchor=\"center\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "76", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "77", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "78", + "metadata": {}, + "source": [ + "## Invisible path\n", + "\n", + "`fig.path()` traces a path without drawing or filling anything. It has two\n", + "common uses:\n", + "\n", + "**Named paths** — register a path under a name so the `intersections` TikZ\n", + "library can compute where two paths cross.\n", + "\n", + "**Mid-path labels** — place a text node at a specific position along an\n", + "invisible connector using `segment_options` with a `\"node\"` key." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "79", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Three nodes\n", + "for label, x, y, color in [\n", + " (\"A\", 0, 0, \"teal\"),\n", + " (\"B\", 5, 3, \"orange\"),\n", + " (\"C\", 10, 0, \"purple\"),\n", + "]:\n", + " fig.node(\n", + " (x, y),\n", + " label=label,\n", + " shape=\"circle\",\n", + " fill=f\"{color}!20\",\n", + " draw=f\"{color}!70\",\n", + " minimum_size=\"1.2cm\",\n", + " content=f\"${label}$\",\n", + " )\n", + "\n", + "# Visible arrows\n", + "fig.draw(\n", + " [\"A.center\", \"B.center\"],\n", + " arrows=arrows.forward,\n", + " options=[styles.thick],\n", + " color=\"teal!70\",\n", + " segment_options=[{\"bend_left\": 20}],\n", + ")\n", + "fig.draw(\n", + " [\"B.center\", \"C.center\"],\n", + " arrows=arrows.forward,\n", + " options=[styles.thick],\n", + " color=\"orange!80\",\n", + " segment_options=[{\"bend_right\": 20}],\n", + ")\n", + "\n", + "# Invisible paths with mid-path labels\n", + "fig.path(\n", + " [\"A.center\", \"B.center\"],\n", + " segment_options=[\n", + " {\n", + " \"node\": {\n", + " \"content\": r\"$e_1$\",\n", + " \"above\": True,\n", + " \"sloped\": True,\n", + " \"font\": r\"\\small\",\n", + " },\n", + " \"bend_left\": 20,\n", + " },\n", + " ],\n", + ")\n", + "fig.path(\n", + " [\"B.center\", \"C.center\"],\n", + " segment_options=[\n", + " {\n", + " \"node\": {\n", + " \"content\": r\"$e_2$\",\n", + " \"above\": True,\n", + " \"sloped\": True,\n", + " \"font\": r\"\\small\",\n", + " },\n", + " \"bend_right\": 20,\n", + " },\n", + " ],\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "80", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "81", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/tutorials/drawing/tutorial_03_paths.qmd b/tutorials/drawing/tutorial_03_paths.qmd deleted file mode 100644 index 379d13f..0000000 --- a/tutorials/drawing/tutorial_03_paths.qmd +++ /dev/null @@ -1,884 +0,0 @@ ---- -title: "Paths and Shapes" -jupyter: python3 ---- - -Beyond simple node-to-node connections, tikzfigure lets you draw arbitrary paths -from lists of `(x, y)` coordinates. - -- drawing open and closed coordinate paths -- `filldraw` for filled closed shapes -- rounded corners -- per-segment styling with `segment_options` -- node paths and `.center` anchors for correct fills -- custom shapes (heart, star) -- parametric curves with NumPy (spiral, sun) -- `fill` for stroke-free fills and even-odd rule -- `clip` for clipping regions -- `path` for invisible named paths and mid-path labels - -```{python} -import numpy as np - -from tikzfigure import TikzFigure, arrows, styles, units -``` - -## Open and closed coordinate paths - -Pass a list of `[x, y]` pairs (or `(x, y)` tuples) to `fig.draw()`. Set -`cycle=True` to close the path back to the starting point. - -```{python} -fig = TikzFigure() - -# Open path - a zigzag -zigzag = [[0, 0], [1, 1], [2, 0], [3, 1], [4, 0]] -fig.draw(zigzag, line_width=2, color="blue") - -# Closed path - a triangle -triangle = [[6, 0], [8, 0], [7, 1.5]] -fig.draw( - triangle, - cycle=True, - line_width=2, - color="red", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Filled shapes with `filldraw` - -`fig.filldraw()` is shorthand for a closed path that is both drawn (outline) and -filled. You can also use `fig.draw(..., fill="color")` for the same effect. - -```{python} -fig = TikzFigure() - -# Pentagon via filldraw -angles = np.linspace(np.pi / 2, np.pi / 2 + 2 * np.pi, 6)[:-1] -pentagon = list( - zip( - np.cos(angles) * 2 + 2, - np.sin(angles) * 2 + 2, - ), -) -fig.filldraw( - pentagon, - cycle=True, - line_width=2, - color="purple", - fill="purple!20", -) - -# Same via draw(..., fill=...) -hexagon_angles = np.linspace(0, 2 * np.pi, 7)[:-1] -hexagon = list( - zip( - np.cos(hexagon_angles) * 2 + 7, - np.sin(hexagon_angles) * 2 + 2, - ), -) -fig.draw( - hexagon, - cycle=True, - line_width=2, - color="teal", - fill="teal!20", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Rounded corners - -The `rounded_corners` parameter (in pt) smooths the corners of any path. - -```{python} -fig = TikzFigure() - -# Same square, with increasing corner radius -for i, radius in enumerate([0, 5, 15, 30]): - x0 = i * 3.5 - square = [[x0, 0], [x0 + 2, 0], [x0 + 2, 2], [x0, 2]] - fig.draw( - square, - cycle=True, - line_width=2, - color="black", - fill="orange!20", - rounded_corners=f"{radius}pt", - ) - fig.node( - x0 + 1, - -0.5, - content=f"{radius}pt", - draw="none", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Per-segment styling - -`segment_options` accepts a list of per-connector option dicts, one for each -`to` step in the path. Each dict uses keyword-style TikZ options (underscores -become spaces) plus an optional `"options"` key for flag-style items like -`styles.dashed` or `arrows.forward`. - -Here four segments are given different colors and styles in a single `draw` -call: - -```{python} -fig = TikzFigure() - -waypoints = [[0, 0], [2, 2], [4, 0], [6, 2], [8, 0]] - -fig.draw( - waypoints, - line_width=2, - segment_options=[ - {"color": "red"}, - {"color": "blue", "options": [styles.dashed], "line_width": 1.5}, - {"color": "green!60!black", "line_width": 3}, - {"color": "purple", "options": [styles.dotted], "line_width": 2}, - ], -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -Per-segment bending works the same way. Here three nodes are connected with -arrows where each segment curves in a different direction: - -```{python} -fig = TikzFigure() - -specs = [("src", 0, 1, "teal"), ("mid", 4, 3, "orange"), ("dst", 8, 1, "purple")] -for label, x, y, color in specs: - fig.node( - (x, y), - label=label, - shape="circle", - fill=f"{color}!25", - draw=f"{color}!70", - minimum_size=1.0 * units.cm, - content=label.upper(), - ) - -fig.draw( - ["src.center", "mid.center", "dst.center"], - arrows=arrows.forward, - options=[styles.thick], - segment_options=[ - {"bend_left": 30}, # src → mid curves upward-left - {"bend_right": 25}, # mid → dst curves downward-right - ], -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Fluent node chaining - -If you're connecting named nodes, `.to(...)` is a concise alternative to -building `segment_options` by hand. Options passed to `.to(...)` affect only -that connector. - -```{python} -fig = TikzFigure() - -A = fig.node( - (0, 1), - label="A", - shape="circle", - fill="teal!25", - draw="teal!70", - minimum_size=1.0 * units.cm, - content="A", -) -B = fig.node( - (4, 3), - label="B", - shape="circle", - fill="orange!25", - draw="orange!70", - minimum_size="1cm", - content="B", -) -C = fig.node( - (8, 1), - label="C", - shape="circle", - fill="purple!25", - draw="purple!70", - minimum_size="1cm", - content="C", -) - -fig.draw( - A.to(B).to(C, options=styles.bend_left()), - arrows=arrows.forward, - options=[styles.thick], - color="teal!70", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -Arc segments fit into the same fluent API. Unlike `.to(...)`, `.arc(...)` does -not consume another waypoint — it appends a TikZ `arc[...]` operation at the -current point, and you can keep chaining afterward: - -```{python} -fig = TikzFigure() - -A = fig.node( - (0, 0), - label="A", - shape="circle", - fill="teal!25", - draw="teal!70", - minimum_size="1cm", - content="A", -) -B = fig.node( - (4, 0), - label="B", - shape="circle", - fill="orange!25", - draw="orange!70", - minimum_size="1cm", - content="B", -) -C = fig.node( - (7, 1.5), - label="C", - shape="circle", - fill="purple!25", - draw="purple!70", - minimum_size="1cm", - content="C", -) - -fig.draw( - A.to(B) - .arc(start_angle=-90, end_angle=90, radius="1.5cm") - .to(C, options=styles.bend_right()), - arrows=arrows.forward, - options=[styles.thick], - color="blue!70", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -Use `"out"` and `"in"` keys for explicit departure and arrival angles (in -degrees). This gives precise control over how each segment enters and leaves its -endpoints — useful when `bend_left`/`bend_right` doesn't give you the exact -shape you need: - -```{python} -fig = TikzFigure() - -for label, x, y, color in [ - ("A", 0, 2, "blue"), - ("B", 5, 4, "orange"), - ("C", 10, 2, "purple"), -]: - fig.node( - (x, y), - label=label, - shape="circle", - fill=f"{color}!20", - draw=f"{color}!70", - minimum_size="1.2cm", - content=f"${label}$", - ) - -fig.draw( - ["A.center", "B.center", "C.center"], - arrows=arrows.forward, - options=[styles.thick], - segment_options=[ - {"out": 45, "in": 195}, # A→B: leave at 45°, arrive at 195° - {"out": -15, "in": 135}, # B→C: leave at -15°, arrive at 135° - ], -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -`looseness` scales how far the control handles extend, making the same bend -either tighter (`looseness < 1`) or more exaggerated (`looseness > 1`). Here the -two segments of the same symmetric path use opposite looseness values: - -```{python} -fig = TikzFigure() - -for label, x, y in [("A", 0, 2), ("B", 5, 2), ("C", 10, 2)]: - fig.node( - (x, y), - label=label, - shape="circle", - fill="gray!15", - draw="gray!60", - minimum_size="1cm", - content=f"${label}$", - ) - -fig.draw( - ["A.center", "B.center", "C.center"], - arrows=arrows.forward, - options=[styles.thick], - segment_options=[ - {"bend_left": 50, "looseness": 2.5}, # A→B: wide exaggerated arc - {"bend_right": 50, "looseness": 0.5}, # B→C: tight compressed arc - ], -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -All segment options can be combined freely. Here each segment gets its own -color, line style, and curve geometry in a single `draw` call: - -```{python} -fig = TikzFigure() - -for label, x, y, color in [ - ("A", 0, 0, "teal"), - ("B", 4, 3, "red"), - ("C", 8, 0, "blue"), -]: - fig.node( - (x, y), - label=label, - shape="circle", - fill=f"{color}!20", - draw=f"{color}!60", - minimum_size="1.2cm", - content=f"${label}$", - ) - -fig.draw( - ["A.center", "B.center", "C.center"], - segment_options=[ - { # A→B - "options": [arrows.forward], - "color": "teal!80", - "line_width": 2, - "out": 60, - "in": 240, - }, - { # B→C - "options": [arrows.forward, styles.dashed], - "color": "red!70", - "line_width": 1.5, - "out": -60, - "in": 120, - }, - ], -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Node paths and `.center` anchors - -When you fill a closed path that goes through named nodes, TikZ routes to each -node's *border* by default. For circular nodes this means the filled area has -indented corners — it doesn't look like a triangle. - -Appending `.center` to a node label in the path list routes the segment through -the node's geometric center instead. The path then bounds a proper filled -polygon. - -```{python} -fig = TikzFigure() - -node_specs = [ - ("top", 3, 4, "blue", r"$v_1$"), - ("bl", 0, 0, "blue", r"$v_2$"), - ("br", 6, 0, "blue", r"$v_3$"), -] -for label, x, y, color, text in node_specs: - fig.node( - (x, y), - label=label, - shape="circle", - draw=f"{color}!60", - fill="white", - minimum_size="1.4cm", - content=text, - ) - -# Border anchors: the fill "leaks" under the node circles -fig.draw( - ["top", "bl", "br"], - cycle=True, - fill="red!25", - draw="red!60", - line_width=1, - fill_opacity=0.6, -) - -# .center anchors: fills the exact triangle spanned by node centres -fig.draw( - ["top.center", "bl.center", "br.center"], - cycle=True, - fill="blue!30", - draw="blue!70", - line_width=1.5, - fill_opacity=0.7, -) - -# Labels outside the shapes -fig.node( - (7.5, 2), - content=r"\textcolor{red!60}{border anchors}", - draw="none", - anchor="west", - font=r"\small", -) -fig.node( - (7.5, 1), - content=r"\textcolor{blue!70}{.center anchors}", - draw="none", - anchor="west", - font=r"\small", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -You can mix anchors: only the nodes that need a specific anchor require the dot -suffix. Any TikZ anchor string works — `"north"`, `"south east"`, `"west"`, etc. - -```{python} -fig = TikzFigure() - -fig.node( - (0, 0), - label="A", - shape="rectangle", - draw="black", - fill="gray!15", - minimum_width="2cm", - minimum_height="1cm", - content="A", -) -fig.node( - (5, 2), - label="B", - shape="rectangle", - draw="black", - fill="gray!15", - minimum_width="2cm", - minimum_height="1cm", - content="B", -) -fig.node( - (10, 0), - label="C", - shape="rectangle", - draw="black", - fill="gray!15", - minimum_width="2cm", - minimum_height="1cm", - content="C", -) - -# Connect from the east side of A, enter B from the west, leave from east, arrive at C.west -fig.draw( - ["A.east", "B.west"], - arrows=arrows.forward, - options=[styles.thick], - color="teal", - segment_options=[{"bend_left": 15}], -) -fig.draw( - ["B.east", "C.west"], - arrows=arrows.forward, - options=[styles.thick], - color="orange", - segment_options=[{"bend_right": 15}], -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Heart shape - -A heart with circular lobes drawn by `fig.arc()` and a pointed bottom built from -a chained path. - -```{python} -width, height = 1.75, 2.0 - -fig = TikzFigure() - -fig.colorlet("lightred", "red!40!white") - -A = fig.node(-width, height) -B = fig.node((0, 0)) -C = fig.node(width, height) -D = fig.node(0, height) - -fig.draw( - A.to(B, options=["out=-90, in=135"]) - .to(C, options=["out=45, in=-90"]) - .to(D, options=["in=80, out=100"]) - .to(A, options=["in=80, out=100"]), - color="red", - line_width=4, - cycle=True, - center=True, - fill="lightred", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Star shape - -A five-pointed star drawn from six points (the last one closes back to the -first). - -```{python} -fig = TikzFigure() - -star = [[0, 0], [1, 2.5], [2, 0], [-0.5, 1.5], [2.5, 1.5], [0, 0]] - -fig.draw( - star, - line_width=3, - cycle=True, - color="Gold", - fill="yellow!50!white", -) - -fig.show() -print(fig) -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Parametric curves with NumPy - -Any iterable of `(x, y)` pairs works as a path, so NumPy arrays plug in -naturally for smooth parametric curves. - -```{python} -fig = TikzFigure() - -# Spiral -N = 300 -theta = np.linspace(0, 8 * np.pi, N) -r = np.linspace(0.1, 3, N) -spiral = list( - zip(r * np.cos(theta) + 4, r * np.sin(theta) + 4), -) -fig.draw(spiral, line_width=2, color="teal") - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Sun: circle core + radial rays - -A filled circle for the core, plus short line segments for rays at equal angles. - -```{python} -fig = TikzFigure() - -cx, cy, R = 4, 4, 1.0 # center and core radius - -# Core -theta = np.linspace(0, 2 * np.pi, 60) -core = list( - zip( - cx + R * np.cos(theta), - cy + R * np.sin(theta), - ), -) -fig.filldraw( - core, - cycle=True, - line_width=2, - color="orange", - fill="yellow!60", -) - -# Rays -for angle in np.linspace( - 0, - 2 * np.pi, - 12, - endpoint=False, -): - x0 = cx + R * np.cos(angle) - y0 = cy + R * np.sin(angle) - x1 = cx + (R + 0.7) * np.cos(angle) - y1 = cy + (R + 0.7) * np.sin(angle) - fig.draw( - [(x0, y0), (x1, y1)], - line_width=2, - color="orange", - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Fill without stroke - -`fig.fill()` fills a closed path without drawing any outline — the equivalent of -`\fill[...]` in TikZ. Use it when you want pure flat colour with no border. - -```{python} -fig = TikzFigure() - -# Three overlapping filled triangles — no outlines at all -triangles = [ - ([[1, 0], [4, 0], [2.5, 2.5]], "red!60"), - ([[2, 0], [5, 0], [3.5, 2.5]], "blue!50"), - ([[3, 0], [6, 0], [4.5, 2.5]], "green!60"), -] -for pts, color in triangles: - fig.fill( - pts, - cycle=True, - fill=color, - fill_opacity=0.6, - ) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -The `even_odd_rule` flag corrects the fill for self-intersecting paths. Without -it, TikZ fills the interior using the non-zero winding rule, which fills the -overlapping centre region twice — visually it looks like a solid shape. With -`even_odd_rule=True` the centre hole is punched out correctly. - -```{python} -fig = TikzFigure() - -# A five-pointed star is self-intersecting -angles = np.linspace(np.pi / 2, np.pi / 2 + 4 * np.pi, 6)[:-1] -star_pts = list( - zip( - np.cos(angles) * 2 + 2, - np.sin(angles) * 2 + 2, - ), -) - -# Default winding rule: centre is filled solid -fig.fill( - star_pts, - cycle=True, - fill="orange!80", - fill_opacity=0.9, -) -fig.node( - (2, -0.3), - content="winding rule (default)", - draw="none", - font=r"\small", - anchor="center", -) - -# Even-odd rule: centre is hollow -angles2 = np.linspace(np.pi / 2, np.pi / 2 + 4 * np.pi, 6)[:-1] -star_pts2 = list( - zip( - np.cos(angles2) * 2 + 8, - np.sin(angles2) * 2 + 2, - ), -) -fig.fill( - star_pts2, - cycle=True, - fill="teal!70", - fill_opacity=0.9, - even_odd_rule=True, -) -fig.node( - (8, -0.3), - content="even odd rule", - draw="none", - font=r"\small", - anchor="center", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Clip - -`fig.clip()` installs a clipping path — all subsequent drawing is masked to the -given boundary. Wrap the clip together with the clipped content inside a -`\begin{scope}...\end{scope}` via `fig.raw()` so the clip doesn't leak into the -rest of the figure. - -```{python} -fig = TikzFigure() - -# Draw a full circle for reference (unclipped) -fig.draw( - list( - zip( - np.cos(np.linspace(0, 2 * np.pi, 60)) * 2 + 2, - np.sin(np.linspace(0, 2 * np.pi, 60)) * 2 + 2, - ), - ), - cycle=True, - color="gray!40", - line_width=1, -) - -# Open a scope, clip to a rectangle, then draw a dense grid inside -fig.raw(r"\begin{scope}") -clip_rect = [[0.5, 0.5], [3.5, 0.5], [3.5, 3.5], [0.5, 3.5]] -fig.clip(clip_rect, cycle=True) - -for i in np.linspace(0, 4, 20): - fig.draw( - [(0, i), (4, i)], - color="blue!40", - line_width=0.4, - ) - fig.draw( - [(i, 0), (i, 4)], - color="blue!40", - line_width=0.4, - ) - -fig.raw(r"\end{scope}") - -# Anything drawn after the scope is unclipped again -fig.node( - (2, -0.5), - content="clipped grid inside circle", - draw="none", - font=r"\small", - anchor="center", -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} - -## Invisible path - -`fig.path()` traces a path without drawing or filling anything. It has two -common uses: - -**Named paths** — register a path under a name so the `intersections` TikZ -library can compute where two paths cross. - -**Mid-path labels** — place a text node at a specific position along an -invisible connector using `segment_options` with a `"node"` key. - -```{python} -fig = TikzFigure() - -# Three nodes -for label, x, y, color in [ - ("A", 0, 0, "teal"), - ("B", 5, 3, "orange"), - ("C", 10, 0, "purple"), -]: - fig.node( - (x, y), - label=label, - shape="circle", - fill=f"{color}!20", - draw=f"{color}!70", - minimum_size="1.2cm", - content=f"${label}$", - ) - -# Visible arrows -fig.draw( - ["A.center", "B.center"], - arrows=arrows.forward, - options=[styles.thick], - color="teal!70", - segment_options=[{"bend_left": 20}], -) -fig.draw( - ["B.center", "C.center"], - arrows=arrows.forward, - options=[styles.thick], - color="orange!80", - segment_options=[{"bend_right": 20}], -) - -# Invisible paths with mid-path labels -fig.path( - ["A.center", "B.center"], - segment_options=[ - { - "node": { - "content": r"$e_1$", - "above": True, - "sloped": True, - "font": r"\small", - }, - "bend_left": 20, - }, - ], -) -fig.path( - ["B.center", "C.center"], - segment_options=[ - { - "node": { - "content": r"$e_2$", - "above": True, - "sloped": True, - "font": r"\small", - }, - "bend_right": 20, - }, - ], -) - -fig.show() -``` - -{{< include includes/tikz_code_callout.qmd >}} diff --git a/tutorials/drawing/tutorial_04_primitives.ipynb b/tutorials/drawing/tutorial_04_primitives.ipynb new file mode 100644 index 0000000..9955d78 --- /dev/null +++ b/tutorials/drawing/tutorial_04_primitives.ipynb @@ -0,0 +1,1393 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "0", + "metadata": {}, + "source": [ + "# TikZ Primitives\n", + "\n", + "tikzfigure wraps common TikZ drawing primitives so you don't need raw LaTeX for\n", + "standard geometric shapes.\n", + "\n", + "- arcs and circles\n", + "- rectangles and ellipses\n", + "- grids and parabolas\n", + "- lines and intersections\n", + "- regular polygons (triangle, square, pentagon, ...)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "1", + "metadata": {}, + "outputs": [], + "source": [ + "import math\n", + "\n", + "from tikzfigure import TikzFigure, arrows, styles" + ] + }, + { + "cell_type": "markdown", + "id": "2", + "metadata": {}, + "source": [ + "## Arcs\n", + "\n", + "An arc is defined by a starting point, start and end angles (in degrees), and a\n", + "radius." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "3", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Quarter circle arc\n", + "fig.arc(\n", + " start=(0, 0),\n", + " start_angle=0,\n", + " end_angle=90,\n", + " radius=\"2cm\",\n", + " color=\"red\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Three-quarter circle arc\n", + "fig.arc(\n", + " start=(4, 0),\n", + " start_angle=90,\n", + " end_angle=360,\n", + " radius=\"1.5cm\",\n", + " color=\"blue\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Arc with arrow\n", + "fig.arc(\n", + " start=(8, 0),\n", + " start_angle=0,\n", + " end_angle=180,\n", + " radius=\"2cm\",\n", + " arrows=arrows.forward,\n", + " color=\"green\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "4", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "5", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "6", + "metadata": {}, + "source": [ + "## Circles\n", + "\n", + "A circle is defined by a center point and a radius." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Unfilled circle\n", + "fig.circle(\n", + " center=(0, 0),\n", + " radius=\"1cm\",\n", + " color=\"blue\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Filled circle\n", + "fig.circle(\n", + " center=(3, 0),\n", + " radius=\"1cm\",\n", + " fill=\"yellow\",\n", + " color=\"orange\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Dashed circle\n", + "fig.circle(\n", + " center=(6, 0),\n", + " radius=\"1cm\",\n", + " color=\"purple\",\n", + " dash_pattern=\"on 2pt off 2pt\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "8", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "9", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "10", + "metadata": {}, + "source": [ + "## Clock face\n", + "\n", + "A practical example: a clock face using a circle and line segments for hour\n", + "markers." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "11", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Outer circle\n", + "fig.circle(\n", + " center=(0, 0),\n", + " radius=\"3cm\",\n", + " color=\"black\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Hour markers (12 arcs around the circle)\n", + "for hour in range(12):\n", + " angle = ((hour + 3) * 30) % 360 # 30 degrees apart\n", + " rad = math.radians(angle)\n", + "\n", + " # Small line segment from inner to outer\n", + " x_inner = 2.7 * math.cos(rad)\n", + " y_inner = 2.7 * math.sin(rad)\n", + " fig.draw(\n", + " [(x_inner, y_inner), (x_inner * 1.1, y_inner * 1.1)],\n", + " line_width=\"1.5pt\",\n", + " color=\"black\",\n", + " )\n", + "\n", + " # Hour numbers\n", + " x_text = 2.4 * math.cos(rad)\n", + " y_text = 2.4 * math.sin(rad)\n", + " fig.node(\n", + " x_text,\n", + " y_text,\n", + " content=str(hour if hour > 0 else 12),\n", + " draw=\"none\",\n", + " )\n", + "\n", + "# Center dot\n", + "fig.circle(\n", + " center=(0, 0),\n", + " radius=\"0.15cm\",\n", + " fill=\"black\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "12", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "13", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "14", + "metadata": {}, + "source": [ + "## Rectangles\n", + "\n", + "Rectangles are defined by two opposite corners." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "15", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Unfilled rectangle\n", + "fig.rectangle(\n", + " (0, 0),\n", + " (2, 1),\n", + " color=\"blue\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Filled rectangle\n", + "fig.rectangle(\n", + " (3, 0),\n", + " (5, 1),\n", + " fill=\"yellow\",\n", + " color=\"orange\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Rectangle with rounded corners\n", + "fig.rectangle(\n", + " (6, 0),\n", + " (8, 1),\n", + " fill=\"green!30\",\n", + " color=\"green\",\n", + " rounded_corners=\"5pt\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "16", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "17", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "18", + "metadata": {}, + "source": [ + "## Ellipses\n", + "\n", + "Ellipses are defined by a center point and two radii." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "19", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Circle (equal radii)\n", + "fig.ellipse(\n", + " (0, 0),\n", + " x_radius=\"1cm\",\n", + " y_radius=\"1cm\",\n", + " color=\"red\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Horizontal ellipse\n", + "fig.ellipse(\n", + " (4, 0),\n", + " x_radius=\"1.5cm\",\n", + " y_radius=\"0.75cm\",\n", + " color=\"blue\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Filled ellipse\n", + "fig.ellipse(\n", + " (8, 0),\n", + " x_radius=\"1.2cm\",\n", + " y_radius=\"0.8cm\",\n", + " fill=\"purple!40\",\n", + " color=\"purple\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "20", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "21", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "22", + "metadata": {}, + "source": [ + "## Grids\n", + "\n", + "A grid spans between two opposite corners with a specified step size." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "23", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Uniform grid\n", + "fig.grid(\n", + " (0, 0),\n", + " (3, 3),\n", + " step=\"0.5cm\",\n", + " color=\"gray!30\",\n", + ")\n", + "\n", + "# Grid with different x and y steps\n", + "fig.grid(\n", + " (4, 0),\n", + " (7, 3),\n", + " xstep=\"0.5cm\",\n", + " ystep=\"1cm\",\n", + " color=\"blue!20\",\n", + ")\n", + "\n", + "# Grid with larger step\n", + "fig.grid(\n", + " (8, 0),\n", + " (10, 3),\n", + " step=\"1cm\",\n", + " color=\"red!20\",\n", + " line_width=\"1pt\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "24", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "25", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "26", + "metadata": {}, + "source": [ + "## Parabolas\n", + "\n", + "Parabolas connect two points, optionally with a bend (control) point." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "27", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Simple parabola\n", + "fig.parabola(\n", + " (0, 0),\n", + " (3, 0),\n", + " color=\"red\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Parabola with bend point (creates a specific curve shape)\n", + "fig.parabola(\n", + " (5, 0),\n", + " (8, 0),\n", + " bend=(6.5, 2),\n", + " color=\"blue\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Parabola with arrow\n", + "fig.parabola(\n", + " (0, 3),\n", + " (3, 4),\n", + " color=\"green\",\n", + " arrows=arrows.forward,\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "28", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "29", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "30", + "metadata": {}, + "source": [ + "## Lines and intersections\n", + "\n", + "Lines connect two points and support arrows and all standard styling." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "31", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Simple line\n", + "fig.line(\n", + " (0, 0),\n", + " (2, 0),\n", + " color=\"red\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Line with arrow\n", + "fig.line(\n", + " (3, 0),\n", + " (5, 2),\n", + " color=\"blue\",\n", + " arrows=arrows.forward,\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Dashed line\n", + "fig.line(\n", + " (6, 0),\n", + " (8, 1),\n", + " color=\"green\",\n", + " options=[styles.dashed],\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "32", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "33", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "34", + "metadata": {}, + "source": [ + "Find where two lines intersect using the `intersection()` method:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "35", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "\n", + "# Create two lines\n", + "line1 = fig.line(\n", + " (0, 0),\n", + " (4, 4),\n", + " color=\"red\",\n", + " line_width=\"2pt\",\n", + ")\n", + "line2 = fig.line(\n", + " (0, 4),\n", + " (4, 0),\n", + " color=\"blue\",\n", + " line_width=\"2pt\",\n", + ")\n", + "\n", + "# Find intersection\n", + "intersection = line1.intersection(line2)\n", + "print(f\"Lines intersect at: {intersection}\")\n", + "\n", + "# Draw intersection point\n", + "if intersection:\n", + " fig.node(\n", + " intersection[0],\n", + " intersection[1],\n", + " shape=\"circle\",\n", + " fill=\"black\",\n", + " minimum_size=\"0.15cm\",\n", + " draw=\"none\",\n", + " )\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "36", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "37", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "38", + "metadata": {}, + "source": [ + "## Regular polygons\n", + "\n", + "Draw regular polygons (triangle, square, pentagon, etc.) with specified center\n", + "and radius." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "39", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Triangle (3 sides)\n", + "fig.triangle(\n", + " (0, 1),\n", + " radius=0.8,\n", + " color=\"red\",\n", + " fill=\"red!20\",\n", + ")\n", + "\n", + "# Square (4 sides)\n", + "fig.square(\n", + " (3, 1),\n", + " radius=0.8,\n", + " color=\"blue\",\n", + " fill=\"blue!20\",\n", + ")\n", + "\n", + "# Pentagon (5 sides)\n", + "fig.polygon(\n", + " (6, 1),\n", + " radius=0.8,\n", + " sides=5,\n", + " color=\"green\",\n", + " fill=\"green!20\",\n", + ")\n", + "\n", + "# Hexagon (6 sides)\n", + "fig.polygon(\n", + " (9, 1),\n", + " radius=0.8,\n", + " sides=6,\n", + " color=\"orange\",\n", + " fill=\"orange!20\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "40", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "41", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "42", + "metadata": {}, + "source": [ + "## Rotated polygons\n", + "\n", + "Polygons can be rotated by specifying a rotation angle in degrees." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "43", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "# Squares with different rotations\n", + "rotations = [0, 22.5, 45]\n", + "for i, rot in enumerate(rotations):\n", + " fig.square(\n", + " (i * 3, 1),\n", + " radius=0.8,\n", + " rotation=rot,\n", + " color=\"purple\",\n", + " fill=\"purple!30\",\n", + " )\n", + " fig.node(\n", + " i * 3,\n", + " -0.3,\n", + " content=f\"{rot}\\\\textdegree\",\n", + " draw=\"none\",\n", + " )\n", + "\n", + "# Triangles pointing in different directions\n", + "fig.triangle(\n", + " (0, 3.5),\n", + " radius=0.8,\n", + " rotation=0,\n", + " color=\"blue\",\n", + " fill=\"blue!20\",\n", + ")\n", + "fig.triangle(\n", + " (3, 3.5),\n", + " radius=0.8,\n", + " rotation=120,\n", + " color=\"blue\",\n", + " fill=\"blue!20\",\n", + ")\n", + "fig.triangle(\n", + " (6, 3.5),\n", + " radius=0.8,\n", + " rotation=240,\n", + " color=\"blue\",\n", + " fill=\"blue!20\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "44", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "45", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "46", + "metadata": {}, + "source": [ + "## Matrix of nodes\n", + "\n", + "`add_matrix()` lays out a grid of nodes using TikZ's `matrix` library, so you\n", + "don't need to hand-write raw `\\matrix` code. Cells are given row by row, and\n", + "per-cell styling is set with a `{\"content\": ..., **options}` dict. Use\n", + "`row_sep`, `column_sep`, and `cell_style` to control spacing and the shared\n", + "look of every cell." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "47", + "metadata": {}, + "outputs": [], + "source": [ + "fig = TikzFigure()\n", + "\n", + "fig.add_matrix(\n", + " [\n", + " [\"A\", \"B\", \"C\"],\n", + " [\"D\", {\"content\": \"E\", \"fill\": \"yellow!60\"}, \"F\"],\n", + " ],\n", + " label=\"grid\",\n", + " options=\"matrix of nodes\",\n", + " row_sep=\"5pt\",\n", + " column_sep=\"5pt\",\n", + " cell_style=\"draw, minimum size=1cm, anchor=center\",\n", + ")\n", + "\n", + "fig.show()" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "48", + "metadata": { + "fold_summary": "Show the TikZ code", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig)" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "49", + "metadata": { + "fold_summary": "Show the standalone LaTeX document", + "tags": [ + "hide-input", + "fold-output" + ] + }, + "outputs": [], + "source": [ + "print(fig.generate_standalone())" + ] + }, + { + "cell_type": "markdown", + "id": "50", + "metadata": {}, + "source": [ + "Individual cells are addressable with `Matrix.cell(row, col)`, which returns\n", + "the TikZ node name for that cell (`\"