├── .github └── workflows │ ├── chromatic.yml │ └── deploy-docs.yml ├── .gitignore ├── .npmrc ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── docs ├── .gitignore ├── .vitepress │ ├── config.mts │ └── theme │ │ ├── CustomLayout.vue │ │ ├── index.ts │ │ └── tailwind.postcss ├── LICENSE ├── Sandbox.tsx ├── api-reference │ ├── assets │ │ └── api-camera-ready.png │ ├── index.md │ ├── marks │ │ ├── circle.md │ │ ├── ellipse.md │ │ ├── image.md │ │ ├── path.md │ │ ├── rect.md │ │ └── text.md │ ├── relations │ │ ├── align.md │ │ ├── arrow.md │ │ ├── background.md │ │ ├── distribute.md │ │ ├── group.md │ │ ├── line.md │ │ └── stack.md │ └── special │ │ ├── bluefish.md │ │ ├── layout.md │ │ ├── ref.md │ │ └── withBluefish.md ├── design-principles.md ├── examples │ ├── Examples.vue │ ├── ExamplesItem.vue │ ├── VPLink.vue │ └── index.md ├── index.md ├── learn │ ├── assets │ │ ├── bordered-mercury-label.png │ │ ├── circle-stack-with-background.png │ │ ├── circle-stack.png │ │ ├── hard-to-read-arrow.png │ │ ├── label-arrow.png │ │ ├── label-under-planet.png │ │ ├── mercury-label-outside-background.png │ │ ├── mercury-label.png │ │ └── overlaid-circles.png │ ├── get-started.md │ ├── tutorial-1-intro.md │ ├── tutorial-2-relations.md │ ├── tutorial-3-framework.md │ ├── tutorial-4-layouts.md │ └── what-is-bluefish.md ├── package.json ├── public │ ├── bluefish-logo.png │ ├── docs-teaser.png │ └── planets.png ├── sandboxUtil.ts ├── tailwind.config.js └── tsconfig.json ├── package.json ├── packages ├── bluefish-js │ ├── .gitignore │ ├── README.md │ ├── package.json │ ├── public │ │ ├── App.template.ts │ │ ├── index.css │ │ └── index.html │ ├── src │ │ └── index.ts │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vite.config.ts └── bluefish-solid │ ├── .eslintrc.cjs │ ├── .gitignore │ ├── .storybook │ ├── main.ts │ └── preview.ts │ ├── .vscode │ └── settings.json │ ├── CONTRIBUTING.md │ ├── LICENSE │ ├── README.md │ ├── examples │ ├── continuity.tsx │ ├── datasets │ │ └── cars.ts │ └── topology │ │ ├── image.tsx │ │ ├── neighborhood.tsx │ │ └── space.tsx │ ├── experiments │ ├── plot.ts │ ├── stratified.ts │ └── theoretical.ts │ ├── package.json │ ├── public │ ├── App.template.tsx │ ├── index.css │ ├── index.html │ └── index.tsx │ ├── src │ ├── align.tsx │ ├── arrow.tsx │ ├── background.tsx │ ├── blob.tsx │ ├── bluefish.tsx │ ├── chemistry │ │ ├── atom.tsx │ │ ├── bond.tsx │ │ └── molecule.tsx │ ├── circle.tsx │ ├── createName.tsx │ ├── distribute.tsx │ ├── ellipse.tsx │ ├── errorContext.ts │ ├── errors.ts │ ├── example-gallery │ │ ├── DFSCQ-log-figure.tsx │ │ ├── brownie.tsx │ │ ├── insertion-sort-hyperscript.ts │ │ ├── insertion-sort.tsx │ │ ├── ohm-parser.tsx │ │ ├── path.tsx │ │ ├── pulley.tsx │ │ ├── qc-text.tsx │ │ └── topology.tsx │ ├── gradient.tsx │ ├── graphLayered.tsx │ ├── group.tsx │ ├── image.tsx │ ├── index.ts │ ├── layout.tsx │ ├── layoutFunction.tsx │ ├── line.tsx │ ├── measure-text.tsx │ ├── path.tsx │ ├── performance-testing │ │ ├── README.md │ │ ├── insertionSort │ │ │ ├── InsertionSortTest.tsx │ │ │ ├── generate_insertion_sort.py │ │ │ └── insertionSortProps.ts │ │ ├── ohmParser │ │ │ ├── generate_ohm_expression.py │ │ │ ├── ohmParserTest.tsx │ │ │ └── ohmProps.ts │ │ └── pythonTutor │ │ │ ├── generate_python_tutor.py │ │ │ ├── pythonTutorProgram.py │ │ │ ├── pythonTutorProps.ts │ │ │ └── pythonTutorTest.tsx │ ├── plot │ │ ├── dot.tsx │ │ └── plot.tsx │ ├── python-tutor │ │ ├── elm-tuple.tsx │ │ ├── global-frame.tsx │ │ ├── heap-object.tsx │ │ ├── heap.tsx │ │ ├── python-tutor.tsx │ │ ├── stack-slot.tsx │ │ └── types.tsx │ ├── rect.tsx │ ├── ref.tsx │ ├── scenegraph.ts │ ├── stack.tsx │ ├── stackLayout.ts │ ├── stackh.tsx │ ├── stackv.tsx │ ├── stories │ │ ├── 4.stories.tsx │ │ ├── 41.stories.tsx │ │ ├── 44.stories.tsx │ │ ├── 50.stories.tsx │ │ ├── 6.stories.tsx │ │ ├── CarsPlot.stories.tsx │ │ ├── IdInference.stories.tsx │ │ ├── JetpackCompose.stories.tsx │ │ ├── Math3ma.stories.tsx │ │ ├── Molecule.stories.tsx │ │ ├── Planets.stories.tsx │ │ ├── PythonTutor.stories.tsx │ │ ├── README.md │ │ ├── SimpleTree.stories.tsx │ │ ├── ThreePointTopologies.stories.tsx │ │ ├── UISTExamples.stories.tsx │ │ ├── VennDiagram.stories.tsx │ │ └── components │ │ │ ├── Align.stories.tsx │ │ │ ├── Arrow.stories.tsx │ │ │ ├── Background.stories.tsx │ │ │ ├── Circle.stories.tsx │ │ │ ├── Distribute.stories.tsx │ │ │ ├── Ellipse.stories.tsx │ │ │ ├── GraphLayered.stories.tsx │ │ │ ├── Image.stories.tsx │ │ │ ├── Line.stories.tsx │ │ │ ├── Rect.stories.tsx │ │ │ ├── Stack.stories.tsx │ │ │ ├── StackH.stories.tsx │ │ │ └── StackV.stories.tsx │ ├── style.ts │ ├── text.tsx │ ├── text │ │ ├── README.md │ │ ├── getStringDims.ts │ │ ├── reduce-css-calc.d.ts │ │ ├── splitAtDelimiters.ts │ │ ├── textBBox.tsx │ │ ├── types.ts │ │ └── useText.tsx │ ├── util │ │ ├── bbox.ts │ │ ├── lca.ts │ │ └── maybe.ts │ └── withBluefish.tsx │ ├── tsconfig.build.json │ ├── tsconfig.json │ └── vite.config.ts ├── pnpm-lock.yaml ├── pnpm-workspace.yaml └── turbo.json /.github/workflows/chromatic.yml: -------------------------------------------------------------------------------- 1 | name: "Chromatic" 2 | 3 | on: 4 | push: 5 | paths: 6 | - "packages/**" 7 | branches: 8 | - main 9 | pull_request: 10 | paths: 11 | - "packages/**" 12 | types: [opened, synchronize, reopened] 13 | 14 | jobs: 15 | chromatic-deployment: 16 | runs-on: ubuntu-latest 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v3 20 | with: 21 | fetch-depth: 0 22 | - name: Setup pnpm 23 | uses: pnpm/action-setup@v4 24 | with: 25 | version: 8.15.6 26 | - name: Install dependencies 27 | run: pnpm install 28 | - name: Publish to Chromatic 29 | uses: chromaui/action@v1 30 | with: 31 | projectToken: chpt_8295300597e1f7e 32 | workingDir: packages/bluefish-solid 33 | exitZeroOnChanges: true 34 | -------------------------------------------------------------------------------- /.github/workflows/deploy-docs.yml: -------------------------------------------------------------------------------- 1 | # Sample workflow for building and deploying a VitePress site to GitHub Pages 2 | name: Deploy VitePress site to Pages 3 | 4 | on: 5 | # Runs on pushes targeting the `main` branch. Change this to `master` if you're 6 | # using the `master` branch as the default branch. 7 | push: 8 | paths: 9 | - "docs/**" 10 | branches: [main] 11 | 12 | # Allows you to run this workflow manually from the Actions tab 13 | workflow_dispatch: 14 | 15 | # Sets permissions of the GITHUB_TOKEN to allow deployment to GitHub Pages 16 | permissions: 17 | contents: read 18 | pages: write 19 | id-token: write 20 | 21 | # Allow only one concurrent deployment, skipping runs queued between the run in-progress and latest queued. 22 | # However, do NOT cancel in-progress runs as we want to allow these production deployments to complete. 23 | concurrency: 24 | group: pages 25 | cancel-in-progress: false 26 | 27 | jobs: 28 | # Build job 29 | build: 30 | runs-on: ubuntu-latest 31 | steps: 32 | - name: Checkout 33 | uses: actions/checkout@v4 34 | with: 35 | fetch-depth: 0 # Not needed if lastUpdated is not enabled 36 | - uses: pnpm/action-setup@v3 37 | - name: Setup Node 38 | uses: actions/setup-node@v4 39 | with: 40 | node-version: 20 41 | cache: pnpm # or pnpm / yarn 42 | - name: Setup Pages 43 | uses: actions/configure-pages@v4 44 | - name: Install dependencies 45 | run: pnpm install 46 | - name: Build with VitePress 47 | run: pnpm turbo docs:build 48 | - name: Upload artifact 49 | uses: actions/upload-pages-artifact@v3 50 | with: 51 | path: docs/.vitepress/dist 52 | 53 | # Deployment job 54 | deploy: 55 | environment: 56 | name: github-pages 57 | url: ${{ steps.deployment.outputs.page_url }} 58 | needs: build 59 | runs-on: ubuntu-latest 60 | name: Deploy 61 | steps: 62 | - name: Deploy to GitHub Pages 63 | id: deployment 64 | uses: actions/deploy-pages@v4 -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/articles/ignoring-files/ for more about ignoring files. 2 | 3 | # Dependencies 4 | node_modules 5 | .pnp 6 | .pnp.js 7 | 8 | # Local env files 9 | .env 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | # Testing 16 | coverage 17 | 18 | # Turbo 19 | .turbo 20 | 21 | # Vercel 22 | .vercel 23 | 24 | # Build Outputs 25 | .next/ 26 | out/ 27 | build 28 | dist 29 | 30 | 31 | # Debug 32 | npm-debug.log* 33 | yarn-debug.log* 34 | yarn-error.log* 35 | 36 | # Misc 37 | .DS_Store 38 | *.pem 39 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/.npmrc -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.workingDirectories": [ 3 | { 4 | "mode": "auto" 5 | } 6 | ] 7 | } 8 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Josh Pollock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Quickstart Dev Guide 2 | 3 | After cloning this repository, please follow these steps to get started on development with Bluefish. 4 | 5 | 1. Run the following command to globally install Turbo: 6 | 7 | ```sh 8 | npm install turbo --global 9 | ``` 10 | 11 | _For more information on how to do this, please reference the [Turbo docs](https://turbo.build/repo/docs/getting-started/installation#global-installation)_ 12 | 13 | 2. Run `pnpm install` from the root directory to install the necessary node modules. 14 | 3. Navigate to `packages/bluefish-solid/public` and duplicate the `App.template.tsx` file. Rename the copy to `App.tsx`. This `App.tsx` file serves as a playground for the `bluefish-solid` package. 15 | 4. Navigate to `packages/bluefish-js/public` and duplicate the `App.template.ts` file. Rename the copy to `App.ts`. This `App.ts` file serves as a playground for the `bluefish-js` package. 16 | 5. From the root of this repository, run `turbo build`. The `bluefish-js` package depends on the `bluefish-solid` package--so the latter must be built in order for the former to run properly. 17 | 6. Once the packages have been built, you can run `turbo dev`, and it will render in your browser whatever is in the `App.ts(x)` files. 18 | - If you just want to run one of the two packages, run `pnpm dev` from either the `bluefish-js` or `bluefish-solid` folders. 19 | -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | /node_modules 2 | /.vitepress/cache 3 | -------------------------------------------------------------------------------- /docs/.vitepress/config.mts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitepress"; 2 | import container from "markdown-it-container"; 3 | import { renderSandbox } from "vitepress-plugin-sandpack"; 4 | 5 | // https://vitepress.dev/reference/site-config 6 | export default defineConfig({ 7 | title: "Bluefish", 8 | description: "A diagramming library for the web", 9 | // base: "/bluefish-docs/", 10 | themeConfig: { 11 | externalLinkIcon: true, 12 | logo: "/bluefish-logo.png", 13 | // https://vitepress.dev/reference/default-theme-config 14 | nav: [ 15 | { text: "Home", link: "/" }, 16 | { text: "Get Started", link: "/learn/get-started" }, 17 | { text: "Learn", link: "/learn/tutorial-1-intro" }, 18 | { text: "Examples", link: "/examples" }, 19 | // { text: "API Reference", link: "/api-reference" }, 20 | { text: "Playground", link: "https://playground.solidjs.com/anonymous/ed046d3a-261f-4902-842c-b22220f25c1e" }, 21 | ], 22 | 23 | search: { 24 | provider: "local", 25 | }, 26 | 27 | sidebar: { 28 | "/learn": [ 29 | { text: "Get Started", link: "/learn/get-started" }, 30 | { text: "What Is Bluefish?", link: "/learn/what-is-bluefish" }, 31 | { 32 | text: "Tutorial", 33 | items: [ 34 | { text: "1. The Basics", link: "/learn/tutorial-1-intro" }, 35 | { text: "2. The Power of Relations", link: "/learn/tutorial-2-relations" }, 36 | // { text: "3. Data-Driven Diagrams", link: "/learn/tutorial-3-framework" }, 37 | ], 38 | }, 39 | ], 40 | "/api-reference": [ 41 | { 42 | text: "API Reference", 43 | items: [ 44 | { text: "Overview", link: "/api-reference/index" }, 45 | { 46 | text: "Marks", 47 | collapsed: true, 48 | items: [ 49 | { text: "Circle", link: "/api-reference/marks/circle" }, 50 | { text: "Ellipse", link: "/api-reference/marks/ellipse" }, 51 | { text: "Image", link: "/api-reference/marks/image" }, 52 | { text: "Path", link: "/api-reference/marks/path" }, 53 | { text: "Rect", link: "/api-reference/marks/rect" }, 54 | { text: "Text", link: "/api-reference/marks/text" }, 55 | ], 56 | }, 57 | { 58 | text: "Relations", 59 | collapsed: true, 60 | items: [ 61 | { text: "Align", link: "/api-reference/relations/align" }, 62 | { text: "Arrow", link: "/api-reference/relations/arrow" }, 63 | { text: "Background", link: "/api-reference/relations/background" }, 64 | { text: "Distribute", link: "/api-reference/relations/distribute" }, 65 | { text: "Group", link: "/api-reference/relations/group" }, 66 | { text: "Line", link: "/api-reference/relations/line" }, 67 | { text: "Stack", link: "/api-reference/relations/stack" }, 68 | ], 69 | }, 70 | { 71 | text: "Special", 72 | collapsed: true, 73 | items: [ 74 | { text: "Bluefish", link: "/api-reference/special/bluefish" }, 75 | { text: "Ref", link: "/api-reference/special/ref" }, 76 | { text: "withBluefish", link: "/api-reference/special/withBluefish" }, 77 | { text: "Layout", link: "/api-reference/special/layout" }, 78 | ], 79 | }, 80 | ], 81 | }, 82 | ], 83 | }, 84 | 85 | socialLinks: [ 86 | { icon: "github", link: "https://github.com/bluefishjs/bluefish" }, 87 | { icon: "discord", link: "https://discord.gg/RTKDYBBybB" }, 88 | ], 89 | }, 90 | markdown: { 91 | lineNumbers: true, 92 | config(md) { 93 | md 94 | // the second parameter is html tag name 95 | .use(container, "sandbox", { 96 | render(tokens, idx) { 97 | return renderSandbox(tokens, idx, "sandbox"); 98 | }, 99 | }); 100 | }, 101 | }, 102 | }); 103 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/index.ts: -------------------------------------------------------------------------------- 1 | import DefaultTheme from "vitepress/theme"; 2 | import { Sandbox } from "../../Sandbox"; 3 | // re-enable for some other styling options for the sandbox 4 | // import "vitepress-plugin-sandpack/dist/style.css"; 5 | import "./tailwind.postcss"; 6 | import { h } from "vue"; 7 | import CustomLayout from "./CustomLayout.vue"; 8 | 9 | const codeOptions = encodeURIComponent( 10 | JSON.stringify({ 11 | "App.tsx": `import { Component } from "solid-js"; 12 | 13 | const App: Component = () => { 14 | return

Ahoyyyyyyyyyyy!

15 | }; 16 | 17 | export default App;`, 18 | }) 19 | ); 20 | 21 | export default { 22 | ...DefaultTheme, 23 | enhanceApp(ctx) { 24 | DefaultTheme.enhanceApp(ctx); 25 | +ctx.app.component("Sandbox", Sandbox); 26 | }, 27 | Layout: CustomLayout, 28 | }; 29 | -------------------------------------------------------------------------------- /docs/.vitepress/theme/tailwind.postcss: -------------------------------------------------------------------------------- 1 | @tailwind base; 2 | 3 | @tailwind components; 4 | 5 | @tailwind utilities; 6 | -------------------------------------------------------------------------------- /docs/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2024 Josh Pollock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /docs/Sandbox.tsx: -------------------------------------------------------------------------------- 1 | import { h, defineComponent, ref, watch, onBeforeMount } from "vue"; 2 | // import { Sandbox as DefaultSandbox, sandboxProps } from "vitepress-plugin-sandpack"; 3 | import { Sandpack, SandpackProvider, SandpackCodeEditor, SandpackPreview, type SandpackFiles } from "sandpack-vue3"; 4 | import { getSandpackFiles } from "./sandboxUtil"; 5 | 6 | export const Sandbox = defineComponent({ 7 | name: "Sandbox", 8 | props: { 9 | codeOptions: { 10 | type: String, 11 | required: true, 12 | }, 13 | }, 14 | setup(props, { slots }) { 15 | const files = ref({}); 16 | 17 | const resolveFiles = async () => { 18 | files.value = await getSandpackFiles(props, slots); 19 | }; 20 | 21 | watch(props, resolveFiles); 22 | 23 | onBeforeMount(resolveFiles); 24 | 25 | return () => { 26 | return ( 27 | 35 |
36 | 37 | 38 |
39 |
40 | ); 41 | }; 42 | }, 43 | }); 44 | -------------------------------------------------------------------------------- /docs/api-reference/assets/api-camera-ready.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/api-reference/assets/api-camera-ready.png -------------------------------------------------------------------------------- /docs/api-reference/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | :::warning 6 | 7 | This page is under construction. 8 | 9 | ::: 10 | 11 | Bluefish has three kinds of primitives: marks, relations, and special. 12 | 13 | ![API Reference](/api-reference/assets/api-camera-ready.png) 14 | 15 | # Marks 16 | 17 | A **mark** is a basic visual element. Bluefish's marks are mostly thin wrappers around SVG 18 | primitives. However, unlike in SVG, in Bluefish a mark's position and size are often omitted, 19 | because they are determined by relations instead. 20 | 21 | - [`Circle`](/api-reference/marks/circle) 22 | - [`Ellipse`](/api-reference/marks/ellipse) 23 | - [`Image`](/api-reference/marks/image) 24 | - [`Path`](/api-reference/marks/path) 25 | - [`Rect`](/api-reference/marks/rect) 26 | - [`Text`](/api-reference/marks/text) 27 | 28 | # Relations 29 | 30 | A **relation** is a visual arrangement of elements. Bluefish's relations are inspired by Gestalt 31 | relations including uniform density, alignment, common region, and connectedness. 32 | 33 | - [`Align`](/api-reference/relations/align) 34 | - [`Arrow`](/api-reference/relations/arrow) 35 | - [`Background`](/api-reference/relations/background) 36 | - [`Distribute`](/api-reference/relations/distribute) 37 | - [`Group`](/api-reference/relations/group) 38 | - [`Line`](/api-reference/relations/line) 39 | - [`Stack`](/api-reference/relations/stack) 40 | 41 | # Special 42 | 43 | Bluefish provides additional primitives for overlapping relations, composing new marks and 44 | relations, and creating new mark and relation primitives. 45 | 46 | - [`Bluefish`](/api-reference/special/bluefish) 47 | - [`Ref`](/api-reference/special/ref) 48 | - [`withBluefish`](/api-reference/special/withBluefish) 49 | - [`Layout`](/api-reference/special/layout) 50 | 51 | 72 | -------------------------------------------------------------------------------- /docs/api-reference/marks/circle.md: -------------------------------------------------------------------------------- 1 | # Circle 2 | -------------------------------------------------------------------------------- /docs/api-reference/marks/ellipse.md: -------------------------------------------------------------------------------- 1 | # Ellipse 2 | -------------------------------------------------------------------------------- /docs/api-reference/marks/image.md: -------------------------------------------------------------------------------- 1 | # Image 2 | -------------------------------------------------------------------------------- /docs/api-reference/marks/path.md: -------------------------------------------------------------------------------- 1 | # Path 2 | -------------------------------------------------------------------------------- /docs/api-reference/marks/rect.md: -------------------------------------------------------------------------------- 1 | # Rect 2 | -------------------------------------------------------------------------------- /docs/api-reference/marks/text.md: -------------------------------------------------------------------------------- 1 | # Text 2 | -------------------------------------------------------------------------------- /docs/api-reference/relations/align.md: -------------------------------------------------------------------------------- 1 | # Align 2 | -------------------------------------------------------------------------------- /docs/api-reference/relations/arrow.md: -------------------------------------------------------------------------------- 1 | # Arrow 2 | -------------------------------------------------------------------------------- /docs/api-reference/relations/background.md: -------------------------------------------------------------------------------- 1 | # Background 2 | -------------------------------------------------------------------------------- /docs/api-reference/relations/distribute.md: -------------------------------------------------------------------------------- 1 | # Distribute 2 | -------------------------------------------------------------------------------- /docs/api-reference/relations/group.md: -------------------------------------------------------------------------------- 1 | # Group 2 | -------------------------------------------------------------------------------- /docs/api-reference/relations/line.md: -------------------------------------------------------------------------------- 1 | # Line 2 | -------------------------------------------------------------------------------- /docs/api-reference/relations/stack.md: -------------------------------------------------------------------------------- 1 | # Stack 2 | -------------------------------------------------------------------------------- /docs/api-reference/special/bluefish.md: -------------------------------------------------------------------------------- 1 | # Bluefish 2 | -------------------------------------------------------------------------------- /docs/api-reference/special/layout.md: -------------------------------------------------------------------------------- 1 | # Layout 2 | -------------------------------------------------------------------------------- /docs/api-reference/special/ref.md: -------------------------------------------------------------------------------- 1 | # Ref 2 | 3 | ## createName 4 | -------------------------------------------------------------------------------- /docs/api-reference/special/withBluefish.md: -------------------------------------------------------------------------------- 1 | # withBluefish 2 | -------------------------------------------------------------------------------- /docs/design-principles.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | # Bluefish Design Principles 6 | -------------------------------------------------------------------------------- /docs/examples/Examples.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 34 | 35 | 74 | -------------------------------------------------------------------------------- /docs/examples/VPLink.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 31 | -------------------------------------------------------------------------------- /docs/examples/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 57 | 58 | Take a look at our examples! They're all based on real diagrams. 59 | 60 | - [Baking Recipe](https://playground.solidjs.com/anonymous/8f733a7c-2ed8-4f5f-b98e-f17eb2cf2b13) 61 | - [Pulleys](https://playground.solidjs.com/anonymous/9bdf7c8f-260c-4e6d-bc96-67383a5c6647) 62 | - [Quantum Circuit Equivalence](https://playground.solidjs.com/anonymous/57f3baf1-6af4-4126-9159-d050b88523c3) 63 | 64 | 65 | 66 | 74 | -------------------------------------------------------------------------------- /docs/index.md: -------------------------------------------------------------------------------- 1 | --- 2 | # https://vitepress.dev/reference/default-theme-home-page 3 | layout: home 4 | 5 | hero: 6 | name: "Bluefish" 7 | text: "The missing diagramming framework" 8 | tagline: "Because your diagrams are worth it" 9 | # image: "/planets.png" 10 | image: "/bluefish-logo.png" 11 | # image: https://github.com/bluefishjs/bluefish/assets/21694516/063a0056-3386-430d-a18b-cfbecf500c0b 12 | # image: /docs-teaser.png 13 | actions: 14 | - theme: brand 15 | text: Get started 16 | link: /learn/get-started 17 | - theme: alt 18 | text: What is Bluefish? 19 | link: /learn/what-is-bluefish 20 | # - theme: alt 21 | # text: Examples 22 | # link: /examples/index.md 23 | features: 24 | - title: "Compose with Relations" 25 | details: Build complex diagrams from simpler building blocks like alignment, spacing, and arrows 26 | - title: "Build Reactive Diagrams" 27 | details: Created interactive and animated diagrams with popular reactive UI primitives 28 | - title: "Use Powerful Custom Layouts" 29 | details: Bluefish provides graph and arrow layouts out of the box, with the ability to add your own 30 | --- 31 | 32 | 33 | 34 | ![Bluefish diagrams](/docs-teaser.png) 35 | 36 | 95 | -------------------------------------------------------------------------------- /docs/learn/assets/bordered-mercury-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/learn/assets/bordered-mercury-label.png -------------------------------------------------------------------------------- /docs/learn/assets/circle-stack-with-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/learn/assets/circle-stack-with-background.png -------------------------------------------------------------------------------- /docs/learn/assets/circle-stack.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/learn/assets/circle-stack.png -------------------------------------------------------------------------------- /docs/learn/assets/hard-to-read-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/learn/assets/hard-to-read-arrow.png -------------------------------------------------------------------------------- /docs/learn/assets/label-arrow.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/learn/assets/label-arrow.png -------------------------------------------------------------------------------- /docs/learn/assets/label-under-planet.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/learn/assets/label-under-planet.png -------------------------------------------------------------------------------- /docs/learn/assets/mercury-label-outside-background.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/learn/assets/mercury-label-outside-background.png -------------------------------------------------------------------------------- /docs/learn/assets/mercury-label.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/learn/assets/mercury-label.png -------------------------------------------------------------------------------- /docs/learn/assets/overlaid-circles.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/learn/assets/overlaid-circles.png -------------------------------------------------------------------------------- /docs/learn/get-started.md: -------------------------------------------------------------------------------- 1 | # Get Started 2 | 3 | ## Install 4 | 5 | Bluefish comes in two flavors: `bluefish-js` and `bluefish-solid`. 6 | 7 | `bluefish-js` is a pure-JS/TS library that works in any JavaScript environment. 8 | **Use this unless you are using SolidJS.** 9 | 10 | ::: code-group 11 | 12 | ```bash:no-line-numbers [yarn] 13 | yarn add bluefish-js 14 | ``` 15 | 16 | ```bash:no-line-numbers [npm] 17 | npm install bluefish-js 18 | ``` 19 | 20 | ```bash:no-line-numbers [pnpm] 21 | pnpm install bluefish-js 22 | ``` 23 | 24 | ```bash:no-line-numbers [bun] 25 | bun add bluefish-js 26 | ``` 27 | 28 | ::: 29 | 30 | `bluefish-solid` offers Bluefish as a SolidJS component library using JSX syntax. 31 | 32 | ::: code-group 33 | 34 | ```bash:no-line-numbers [yarn] 35 | yarn add bluefish-solid 36 | ``` 37 | 38 | ```bash:no-line-numbers [npm] 39 | npm install bluefish-solid 40 | ``` 41 | 42 | ```bash:no-line-numbers [pnpm] 43 | pnpm install bluefish-solid 44 | ``` 45 | 46 | ```bash:no-line-numbers [bun] 47 | bun add bluefish-solid 48 | ``` 49 | 50 | ::: 51 | 52 | ### Starter REPLs 53 | 54 | We have REPLs to get you started in some common environments and frameworks: 55 | 56 | - [Vanilla JS](https://playground.solidjs.com/anonymous/d19113c2-dab6-4867-9d2b-4c14040757b9) 57 | - [Observable](https://observablehq.com/@joshpoll/bluefish-in-observable) 58 | - [Svelte](https://svelte.dev/repl/1fa5bf8713ac4fc2a991560e50564932?version=4.2.1) 59 | - [SolidJS](https://playground.solidjs.com/anonymous/b5ec4207-c725-4bce-9bc8-18e639067514) 60 | -------------------------------------------------------------------------------- /docs/learn/tutorial-4-layouts.md: -------------------------------------------------------------------------------- 1 | # Tutorial Part 4: Custom Layouts 2 | 3 | The final part of this tutorial is about making custom layouts. When you need to go beyond 4 | Bluefish's standard library, you can lean our custom layout API. 5 | 6 | ## The `Layout` component 7 | 8 | ## Anatomy of a relation 9 | 10 | - marks and relations are the same thing 11 | - everything has a bounding box 12 | - everything has a layout 13 | - everything has a paint 14 | 15 | ## Writing a custom layout 16 | 17 | ## The bounding box model -------------------------------------------------------------------------------- /docs/learn/what-is-bluefish.md: -------------------------------------------------------------------------------- 1 | --- 2 | outline: deep 3 | --- 4 | 5 | 72 | 73 | # What Is Bluefish? 74 | 75 | Bluefish is a diagramming framework for the web. It relaxes the UI component model popularized by 76 | React. The main primitive of Bluefish is the _relation_. Just as components are the 77 | building blocks of user interfaces, relations are the building blocks of diagrams. Unlike 78 | components, relations can share children with other relations, and they don't need to fully specify 79 | their childrens' layouts. 80 | 81 | Bluefish is based on work from the [MIT Visualization Group](https://vis.csail.mit.edu/) and the [Software 82 | is based on work from the [MIT Visualization Group](https://vis.csail.mit.edu/) and the [Software Design Group](https://sdg.csail.mit.edu/). You can read more 83 | about the theory, design, and implementation of Bluefish in our [research 84 | paper](https://arxiv.org/abs/2307.00146). 85 | 86 | # Team 87 | 88 | 89 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "private": true, 4 | "scripts": { 5 | "docs:dev": "vitepress dev .", 6 | "docs:build": "vitepress build .", 7 | "docs:preview": "vitepress preview ." 8 | }, 9 | "dependencies": { 10 | "@vue/babel-plugin-jsx": "^1.2.2", 11 | "@vue/babel-preset-jsx": "^1.4.0", 12 | "@wdns/vue-code-block": "^2.3.3", 13 | "autoprefixer": "^10.4.19", 14 | "markdown-it-container": "^4.0.0", 15 | "postcss": "^8.4.38", 16 | "sandpack-vue3": "^3.1.11", 17 | "tailwindcss": "^3.4.4", 18 | "vitepress": "^1.2.3", 19 | "vitepress-plugin-sandpack": "^1.1.4", 20 | "vue": "^3.4.27" 21 | }, 22 | "devDependencies": { 23 | "@vue/tsconfig": "^0.5.1" 24 | }, 25 | "postcss": { 26 | "plugins": { 27 | "tailwindcss": {}, 28 | "autoprefixer": {} 29 | } 30 | } 31 | } -------------------------------------------------------------------------------- /docs/public/bluefish-logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/public/bluefish-logo.png -------------------------------------------------------------------------------- /docs/public/docs-teaser.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/public/docs-teaser.png -------------------------------------------------------------------------------- /docs/public/planets.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bluefishjs/bluefish/516ec19ed9e3226ec5d14b9dc3fb16c2e6307447/docs/public/planets.png -------------------------------------------------------------------------------- /docs/sandboxUtil.ts: -------------------------------------------------------------------------------- 1 | // https://github.com/jerrywu001/vitepress-plugin-sandpack/blob/main/src/components/SandpackUtil.ts 2 | import type { Slots, VNode } from "vue"; 3 | import { renderToString } from "vue/server-renderer"; 4 | import type { SandpackFiles } from "sandpack-vue3"; 5 | 6 | const defaultFilePath = "/index.ts"; 7 | 8 | const getFileAttributes = (info: string) => { 9 | let path: string | undefined; 10 | const opts = (info || "").split(" "); 11 | const hidden = opts.includes("[hidden]"); 12 | const readOnly = opts.includes("[readonly]") || opts.includes("[readOnly]"); 13 | const active = opts.includes("[active]"); 14 | const filename = opts.find((v) => v.includes(".")); 15 | if (filename) { 16 | path = filename; 17 | path = path === "App.vue" ? defaultFilePath : path; 18 | path = path.startsWith("/") ? path : `/${path}`; 19 | } 20 | return { hidden, active, readOnly, path }; 21 | }; 22 | 23 | export const getSandpackFiles = async (props: { codeOptions: string }, slot: Slots) => { 24 | const items = {} as SandpackFiles; 25 | const content = (slot.default ? slot.default() : []) as VNode[]; 26 | let codeItems = content.filter((v) => v.type === "div") || []; 27 | let i = 0; 28 | if (Array.isArray(codeItems)) { 29 | if (!codeItems.length) { 30 | // @ts-ignore 31 | const ctx = content ? content[0]?.ctx : {}; 32 | if (ctx && ctx?.slots) { 33 | codeItems = ctx.slots?.default ? ctx.slots.default() : []; 34 | } 35 | } 36 | for await (const v of codeItems) { 37 | let code = ""; 38 | let div: HTMLDivElement | null = document.createElement("div"); 39 | const children = (v.children || []) as VNode[]; 40 | const { active, hidden, readOnly, path } = getFileAttributes( 41 | JSON.parse(decodeURIComponent(props.codeOptions /* as string */))[i] 42 | ); 43 | const filename = path || defaultFilePath; 44 | const pre = children.find((c) => c.type === "pre"); 45 | v.children = pre ? [pre] : []; 46 | const html = await renderToString(v); 47 | div.insertAdjacentHTML("beforeend", html); 48 | code = div.innerText; 49 | div = null; 50 | items[filename] = { 51 | code, 52 | active, 53 | hidden, 54 | readOnly, 55 | }; 56 | i++; 57 | } 58 | } 59 | return items; 60 | }; 61 | -------------------------------------------------------------------------------- /docs/tailwind.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | content: { 3 | enabled: process.env.NODE_ENV === "production", 4 | content: ["./**/*.{ js,vue,ts,md}"], 5 | options: { 6 | safelist: ["html", "body"], 7 | }, 8 | }, 9 | }; 10 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.json", 3 | "compilerOptions": { 4 | "jsx": "preserve", 5 | "jsxFactory": "h", 6 | "jsxFragmentFactory": "Fragment" 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluefish-monorepo", 3 | "private": true, 4 | "scripts": { 5 | "build": "turbo build", 6 | "dev": "turbo dev", 7 | "lint": "turbo lint", 8 | "format": "prettier --write \"**/*.{ts,tsx,md}\"" 9 | }, 10 | "devDependencies": { 11 | "prettier": "^3.2.5", 12 | "turbo": "^2.1.2", 13 | "typescript": "^5.4.5" 14 | }, 15 | "packageManager": "pnpm@8.15.6", 16 | "engines": { 17 | "node": ">=18" 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/bluefish-js/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | public/App.ts 4 | -------------------------------------------------------------------------------- /packages/bluefish-js/README.md: -------------------------------------------------------------------------------- 1 | # Bluefish JS 2 | 3 |
4 | 5 | Bluefish 6 | 7 |

Create beautiful, customizable diagrams in JavaScript.

8 |
9 | 10 | ## Installation 11 | 12 | ```bash 13 | npm install bluefish-js 14 | ``` -------------------------------------------------------------------------------- /packages/bluefish-js/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluefish-js", 3 | "version": "0.0.37", 4 | "description": "A JS diagramming library", 5 | "type": "module", 6 | "files": [ 7 | "dist" 8 | ], 9 | "main": "./dist/index.umd.cjs", 10 | "module": "./dist/index.js", 11 | "types": "./dist/index.d.ts", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/index.js", 15 | "require": "./dist/index.umd.cjs" 16 | } 17 | }, 18 | "scripts": { 19 | "build": "vite build", 20 | "dev": "vite", 21 | "serve": "vite preview", 22 | "publish": "pnpm publish" 23 | }, 24 | "license": "MIT", 25 | "devDependencies": { 26 | "solid-devtools": "^0.29.2", 27 | "typescript": "^5.3.3", 28 | "vite": "^5.0.11", 29 | "vite-plugin-dts": "^4.2.2", 30 | "vite-plugin-solid": "^2.8.2" 31 | }, 32 | "dependencies": { 33 | "bluefish-solid": "workspace:*", 34 | "solid-js": "^1.9.3" 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /packages/bluefish-js/public/App.template.ts: -------------------------------------------------------------------------------- 1 | import { Text, render } from "../src"; 2 | 3 | function App() { 4 | return Text( 5 | "Duplicate this file and name it `App.ts` to use the dev playground!" 6 | ); 7 | } 8 | 9 | render(App, document.getElementById("root")!); 10 | -------------------------------------------------------------------------------- /packages/bluefish-js/public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/bluefish-js/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Bluefish-JS 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/bluefish-js/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["node_modules", "dist", "examples", "experiments", "public", "src/stories", "vite.config.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/bluefish-js/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "dist"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "jsx": "preserve", 11 | "jsxImportSource": "solid-js", 12 | "types": ["vite/client"], 13 | "emitDeclarationOnly": true, 14 | "isolatedModules": true, 15 | "declaration": true, 16 | "declarationDir": "dist", 17 | "declarationMap": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/bluefish-js/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import dts from "vite-plugin-dts"; 4 | 5 | export default defineConfig(({ command }) => ({ 6 | publicDir: command === "serve" ? "public" : false, 7 | server: { 8 | port: 3000, 9 | open: "/public/index.html", 10 | }, 11 | build: { 12 | emptyOutDir: false, 13 | target: "esnext", 14 | lib: { 15 | entry: resolve(__dirname, "src/index.ts"), 16 | formats: ["es", "cjs", "umd"], 17 | name: "bluefish", 18 | fileName: "index", 19 | }, 20 | }, 21 | plugins: [dts({ tsconfigPath: "./tsconfig.build.json" })], 22 | })); 23 | -------------------------------------------------------------------------------- /packages/bluefish-solid/.eslintrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { 3 | browser: true, 4 | es2021: true, 5 | }, 6 | extends: [ 7 | "eslint:recommended", 8 | "plugin:@typescript-eslint/recommended", 9 | "plugin:solid/typescript", 10 | "plugin:storybook/recommended" 11 | ], 12 | overrides: [ 13 | { 14 | env: { 15 | node: true, 16 | }, 17 | files: [".eslintrc.{js,cjs}"], 18 | parserOptions: { 19 | sourceType: "script", 20 | }, 21 | }, 22 | ], 23 | parser: "@typescript-eslint/parser", 24 | parserOptions: { 25 | ecmaVersion: "latest", 26 | sourceType: "module", 27 | }, 28 | plugins: ["@typescript-eslint", "solid"], 29 | rules: { 30 | "no-debugger": "off", 31 | "@typescript-eslint/no-unused-vars": [ 32 | "warn", 33 | { 34 | varsIgnorePattern: "^_", 35 | argsIgnorePattern: "^_", 36 | }, 37 | ], 38 | "@typescript-eslint/no-explicit-any": "warn", 39 | "prefer-const": "warn", 40 | "no-constant-condition": ["error", { checkLoops: false }], 41 | }, 42 | }; 43 | -------------------------------------------------------------------------------- /packages/bluefish-solid/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | /public/App.tsx 4 | */.DS_Store 5 | build-storybook.log 6 | chromatic.log 7 | -------------------------------------------------------------------------------- /packages/bluefish-solid/.storybook/main.ts: -------------------------------------------------------------------------------- 1 | import { dirname, join } from "path"; 2 | import type { StorybookConfig } from "storybook-solidjs-vite"; 3 | 4 | const config: StorybookConfig = { 5 | stories: ["../src/**/*.mdx", "../src/**/*.stories.@(js|jsx|mjs|ts|tsx)"], 6 | addons: [ 7 | getAbsolutePath("@storybook/addon-links"), 8 | getAbsolutePath("@storybook/addon-essentials"), 9 | getAbsolutePath("@storybook/addon-interactions"), 10 | getAbsolutePath("@storybook/addon-docs"), 11 | getAbsolutePath("@chromatic-com/storybook") 12 | ], 13 | framework: { 14 | name: getAbsolutePath("storybook-solidjs-vite"), 15 | options: {}, 16 | }, 17 | docs: {}, 18 | }; 19 | export default config; 20 | 21 | function getAbsolutePath(value: string): any { 22 | return dirname(require.resolve(join(value, "package.json"))); 23 | } 24 | -------------------------------------------------------------------------------- /packages/bluefish-solid/.storybook/preview.ts: -------------------------------------------------------------------------------- 1 | const preview: Preview = { 2 | parameters: { 3 | actions: { argTypesRegex: "^on[A-Z].*" }, 4 | controls: { 5 | matchers: { 6 | color: /(background|color)$/i, 7 | date: /Date$/, 8 | }, 9 | }, 10 | }, 11 | }; 12 | 13 | export default preview; 14 | -------------------------------------------------------------------------------- /packages/bluefish-solid/.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.printWidth": 80, 3 | "typescript.tsdk": "./node_modules/typescript/lib" 4 | } 5 | -------------------------------------------------------------------------------- /packages/bluefish-solid/CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | # Contributing to Bluefish 2 | 3 | Pull requests are welcome. For major changes, please open an issue first to discuss what you would like to change. 4 | 5 | ## Setup 6 | 7 | 1. Fork and clone the repo 8 | 2. Run `pnpm install` to install dependencies. (Ensure you have `pnpm` first) 9 | 3. Create a branch for your PR with `git checkout -b your-branch-name` 10 | 4. To keep the `main` branch of your fork pointing to this repo, run 11 | 12 | ``` 13 | git remote add upstream https://github.com/bluefishjs/rockfish.git 14 | git fetch upstream 15 | git branch --set-upstream-to=upstream/main main 16 | ``` 17 | 18 | 5. If you want to experiment with changes locally, you can duplicate `App.template.tsx` and rename 19 | it to `App.tsx`. This file is ignored by git, so you can make changes to it without affecting the 20 | repo. 21 | 6. If you are adding a bug fix or feature, add a test or example, respectively, to `stories/` and verify that it works with `pnpm storybook`. 22 | 23 | ## Running 24 | 25 | The code can be run with `pnpm start`. This should open a web browser that serves `App.tsx`, typically on `localhost:3000/public/index.html`. 26 | You can use `App.tsx` as a scratchpad for experiments. 27 | 28 | To check examples, run `pnpm storybook`. This will launch storybook, typically on `localhost:6006`. 29 | Changes to examples must be verified by a contributer before a PR can be accepted. 30 | 31 | ## Design Principles and Goals 32 | 33 | Minor bug fixes and features may be accepted readily. For major changes to be accepted into Bluefish, they should adhere to Bluefish's design principles. 34 | 35 | ### End-User Goals 36 | 37 | - **Local Reasoning:** Users should be able to understand a Bluefish program by reasoning locally about individual components rather than globally across the entire program. 38 | - **Gradual Specification:** It should be possible to make small changes to a Bluefish program and get meaningful outputs at each step. 39 | 40 | ### System Interface Goals 41 | 42 | - **Simple Core:** The core of the system should have simple interface. This includes the layout engine and declarative references. Individual components may have complex sets of parameters. 43 | - **Extensibile:** As much as possible no component should be ``privileged'' by the core system. New APIs should be accessible to both standard library and user-defined elements. 44 | 45 | ### System Design Principles 46 | 47 | - **Composition:** To achieve simplicity, aim to design simple pieces that compose in complex ways. 48 | - **Abstraction:** To achieve extensibility, aim to design pieces that can be abstracted into functions without changing their behavior. 49 | 50 | ## Acknowledgements 51 | 52 | This contributing doc is based on [Vega-Lite's](https://github.com/vega/vega-lite/blob/main/CONTRIBUTING.md) and [Excalidraw's](https://docs.excalidraw.com/docs/introduction/contributing). 53 | -------------------------------------------------------------------------------- /packages/bluefish-solid/LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Joshua M Pollock 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /packages/bluefish-solid/README.md: -------------------------------------------------------------------------------- 1 |
2 | 3 | Bluefish 4 | 5 |

Create beautiful, customizable visualizations in SolidJS.

6 |
7 | 8 | ## Installation Guide 9 | 10 | To start, make sure you have `pnpm` installed. Installation instructions can be found at `https://pnpm.io/installation`. 11 | 12 | After you have installed `pnpm`, open a new terminal window and navigate to project directory. Run the following command to install all necessary packages and dependencies: 13 | 14 | `pnpm install` 15 | 16 | ## Available Scripts 17 | 18 | You can run the following commands inside the project directory: 19 | 20 | ### `pnpm start` 21 | 22 | Runs the app in the development mode.
23 | Open [http://localhost:3000](http://localhost:3000) to view it in the browser. 24 | 25 | The page will reload if you make edits.
26 | 27 | ### `pnpm storybook` 28 | 29 | Runs storybook 30 | 31 | ## Making an `App.tsx` File 32 | 33 | When you run `pnpm start`, it builds and renders the contents of `App.tsx`. You should create an `App.tsx` file in `/public`. This can be done by duplicating the `App.template.tsx` file in the `/public` folder and renaming it to `App.tsx`. 34 | -------------------------------------------------------------------------------- /packages/bluefish-solid/examples/continuity.tsx: -------------------------------------------------------------------------------- 1 | // https://en.wikipedia.org/wiki/File:Continuity_topology.svg 2 | /* 3 | A function f : X -> Y 4 | is continuous at a point x in X 5 | 6 | iff 7 | 8 | for every neighborhood V of f(x) in Y 9 | there is a neighborhood U of x in X 10 | s.t. f(U) is a subset of V 11 | */ 12 | 13 | /* 14 | - Draw X 15 | - Draw Y 16 | - Draw x 17 | - Draw f(x) 18 | - Draw V 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | */ 40 | -------------------------------------------------------------------------------- /packages/bluefish-solid/examples/topology/image.tsx: -------------------------------------------------------------------------------- 1 | import { ParentProps } from "solid-js"; 2 | import { Id, withBluefish } from "../../src"; 3 | import Background from "../../src/background"; 4 | 5 | export type ImageProps = ParentProps<{ 6 | name: Id; 7 | }>; 8 | 9 | export const Image = withBluefish((props: ImageProps) => { 10 | return ( 11 | 12 | {props.children} 13 | 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/bluefish-solid/examples/topology/neighborhood.tsx: -------------------------------------------------------------------------------- 1 | import { ParentProps } from "solid-js"; 2 | import { Id, withBluefish } from "../../src"; 3 | import Background from "../../src/background"; 4 | 5 | export type NeighborhoodProps = ParentProps<{ 6 | name: Id; 7 | }>; 8 | 9 | export const Neighborhood = withBluefish((props: NeighborhoodProps) => { 10 | return ( 11 | 12 | {props.children} 13 | 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/bluefish-solid/examples/topology/space.tsx: -------------------------------------------------------------------------------- 1 | import { ParentProps } from "solid-js"; 2 | import { Id, withBluefish } from "../../src"; 3 | import Background from "../../src/background"; 4 | 5 | export type SpaceProps = ParentProps<{ 6 | name: Id; 7 | }>; 8 | 9 | export const Space = withBluefish((props: SpaceProps) => { 10 | return ( 11 | 12 | {props.children} 13 | 14 | ); 15 | }); 16 | -------------------------------------------------------------------------------- /packages/bluefish-solid/experiments/plot.ts: -------------------------------------------------------------------------------- 1 | const alphabet: any = null; 2 | 3 | // Observable Plot 4 | Plot({ 5 | x: { percent: true }, 6 | marks: [ 7 | BarX(alphabet, StackX({ x: "frequency", fillOpacity: 0.3, inset: 0.5 })), 8 | TextX(alphabet, StackX({ x: "frequency", text: "letter", inset: 0.5 })), 9 | ], 10 | }); 11 | 12 | // Bluefish Plot StackX(BarX) 13 | Plot( 14 | { data: alphabet, x: { percent: true } }, 15 | Align("center", [ 16 | StackX({ spacing: 0.5 }, BarX({ x: "frequency", fillOpacity: 0.3 })), 17 | TextX({ text: "letter" }), 18 | ]) 19 | ); 20 | 21 | // Bluefish Plot BarX(StackX) 22 | Plot( 23 | { data: alphabet, x: { percent: true } }, 24 | Align("center", [ 25 | BarX({ 26 | x: "frequency", 27 | fillOpacity: 0.3, 28 | layout: StackX({ spacing: 0.5 }), 29 | }), 30 | TextX({ text: "letter" }), 31 | ]) 32 | ); 33 | 34 | function Plot(...arg0: any) { 35 | throw new Error("Function not implemented."); 36 | } 37 | 38 | function Align(...arg0: any[]): any { 39 | throw new Error("Function not implemented."); 40 | } 41 | 42 | function StackX(...BarX: any): any { 43 | throw new Error("Function not implemented."); 44 | } 45 | 46 | function BarX(...args: any): any { 47 | throw new Error("Function not implemented."); 48 | } 49 | 50 | function TextX(...args: any): any { 51 | throw new Error("Function not implemented."); 52 | } 53 | -------------------------------------------------------------------------------- /packages/bluefish-solid/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "bluefish-solid", 3 | "version": "0.0.37", 4 | "description": "A SolidJS diagramming library", 5 | "type": "module", 6 | "files": [ 7 | "dist" 8 | ], 9 | "main": "./dist/index.umd.cjs", 10 | "module": "./dist/index.js", 11 | "types": "./dist/index.d.ts", 12 | "exports": { 13 | ".": { 14 | "import": "./dist/index.js", 15 | "require": "./dist/index.umd.cjs" 16 | } 17 | }, 18 | "scripts": { 19 | "start": "vite", 20 | "dev": "vite", 21 | "build": "vite build", 22 | "publish": "pnpm publish", 23 | "tsc": "npx tsc --skipLibCheck -p ./tsconfig.build.json", 24 | "serve": "vite preview", 25 | "storybook": "storybook dev -p 6006", 26 | "storybook-docs": "storybook dev --docs", 27 | "build-storybook": "storybook build", 28 | "test-storybook": "test-storybook", 29 | "chromatic": "pnpm dlx chromatic --project-token=chpt_8295300597e1f7e" 30 | }, 31 | "license": "MIT", 32 | "devDependencies": { 33 | "@chromatic-com/storybook": "^3.2.2", 34 | "@solid-devtools/logger": "^0.8.3", 35 | "@storybook/addon-docs": "^8.5.0-alpha.18", 36 | "@storybook/addon-essentials": "^8.5.0-alpha.18", 37 | "@storybook/addon-interactions": "^8.5.0-alpha.18", 38 | "@storybook/addon-links": "^8.5.0-alpha.18", 39 | "@storybook/blocks": "^8.5.0-alpha.18", 40 | "@storybook/docs-tools": "^8.5.0-alpha.18", 41 | "@storybook/test": "^8.5.0-alpha.18", 42 | "@storybook/test-runner": "^0.20.1", 43 | "@types/dagre": "^0.7.51", 44 | "@types/node": "^20.3.3", 45 | "@typescript-eslint/eslint-plugin": "^6.7.5", 46 | "@typescript-eslint/parser": "^6.7.5", 47 | "chromatic": "^7.6.0", 48 | "eslint": "^8.51.0", 49 | "eslint-plugin-solid": "^0.13.0", 50 | "eslint-plugin-storybook": "^0.11.1", 51 | "ohm-js": "^17.1.0", 52 | "playwright": "^1.38.1", 53 | "solid-devtools": "^0.30.1", 54 | "solid-js": "^1.9.3", 55 | "storybook": "^8.5.0-alpha.18", 56 | "storybook-solidjs": "^1.0.0-beta.6", 57 | "storybook-solidjs-vite": "^1.0.0-beta.6", 58 | "typescript": "^5.3.3", 59 | "vite": "^4.3.9", 60 | "vite-plugin-dts": "^4.2.2", 61 | "vite-plugin-solid": "^2.7.0" 62 | }, 63 | "dependencies": { 64 | "@types/d3-scale": "^4.0.5", 65 | "@types/lodash": "^4.14.195", 66 | "d3-scale": "^4.0.2", 67 | "dagre": "^0.8.5", 68 | "lodash": "^4.17.21", 69 | "paper": "^0.12.17", 70 | "perfect-arrows": "^0.3.7", 71 | "reduce-css-calc": "^2.1.8", 72 | "smiles-drawer": "^2.1.7", 73 | "solid-toast": "^0.5.0", 74 | "tailwindcss": "^3.4.3" 75 | }, 76 | "peerDependencies": { 77 | "solid-js": "^1.9.3" 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/bluefish-solid/public/App.template.tsx: -------------------------------------------------------------------------------- 1 | import { type Component } from "solid-js"; 2 | import { Bluefish, Text } from "../src"; 3 | 4 | const App: Component = () => { 5 | return ( 6 | <> 7 | 8 | 9 | Duplicate this file and name it `App.tsx` to use the dev playground! 10 | 11 | 12 | 13 | ); 14 | }; 15 | 16 | export default App; 17 | -------------------------------------------------------------------------------- /packages/bluefish-solid/public/index.css: -------------------------------------------------------------------------------- 1 | body { 2 | margin: 0; 3 | font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', 'Roboto', 'Oxygen', 4 | 'Ubuntu', 'Cantarell', 'Fira Sans', 'Droid Sans', 'Helvetica Neue', 5 | sans-serif; 6 | -webkit-font-smoothing: antialiased; 7 | -moz-osx-font-smoothing: grayscale; 8 | } 9 | 10 | code { 11 | font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New', 12 | monospace; 13 | } 14 | -------------------------------------------------------------------------------- /packages/bluefish-solid/public/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | Solid App 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /packages/bluefish-solid/public/index.tsx: -------------------------------------------------------------------------------- 1 | /* @refresh reload */ 2 | import { render } from "solid-js/web"; 3 | 4 | import App from "./App"; 5 | 6 | const root = document.getElementById("root"); 7 | 8 | if (import.meta.env.DEV && !(root instanceof HTMLElement)) { 9 | throw new Error( 10 | "Root element not found. Did you forget to add it to your index.html? Or maybe the id attribute got misspelled?" 11 | ); 12 | } 13 | 14 | render(() => , root!); 15 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/chemistry/atom.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from "solid-js/jsx-runtime"; 2 | import { Layout } from "../layout"; 3 | import { BBox, Id, Transform } from "../scenegraph"; 4 | import { splitProps } from "solid-js"; 5 | import withBluefish from "../withBluefish"; 6 | 7 | type maxBondTypes = { 8 | [key: string]: number; 9 | }; 10 | 11 | const maxBonds: maxBondTypes = { 12 | H: 1, 13 | C: 4, 14 | N: 3, 15 | O: 2, 16 | P: 3, 17 | S: 2, 18 | B: 3, 19 | F: 1, 20 | I: 1, 21 | Cl: 1, 22 | Br: 1, 23 | }; 24 | 25 | type elementNameTypes = { 26 | [key: string]: string; 27 | }; 28 | 29 | const elementName: elementNameTypes = { 30 | H: "Hydrogen", 31 | C: "Carbon", 32 | N: "Nitrogen", 33 | O: "Oxygen", 34 | P: "Phosphorus", 35 | S: "Sulfur", 36 | B: "Boron", 37 | F: "Flourine", 38 | I: "Iodine", 39 | Cl: "Chlorine", 40 | Br: "Bromine", 41 | }; 42 | 43 | export type AtomProps = JSX.CircleSVGAttributes & { 44 | content: string; 45 | name: Id; 46 | isTerminal: boolean; 47 | bondCount: number; 48 | ariaHidden: boolean; 49 | r: number; 50 | cx?: number; 51 | cy?: number; 52 | }; 53 | 54 | export const Atom = withBluefish((props: AtomProps) => { 55 | const numHydrogens = () => maxBonds[props.content] - props.bondCount; 56 | const hydrogenString = () => "H".repeat(numHydrogens()); 57 | const atomContent = () => 58 | props.content === "C" ? "" : props.content + hydrogenString(); 59 | const layout = () => { 60 | return { 61 | bbox: { 62 | left: -props.r, 63 | top: -props.r, 64 | width: props.r * 2, 65 | height: props.r * 2, 66 | }, 67 | transform: { 68 | translate: { 69 | x: props.cx, 70 | y: props.cy, 71 | }, 72 | }, 73 | }; 74 | }; 75 | 76 | const paint = (paintProps: { bbox: BBox; transform: Transform }) => { 77 | const [_, rest] = splitProps(props, ["name", "cx", "cy", "r"]); 78 | 79 | const r = () => (paintProps.bbox.width ?? 0) / 2; 80 | 81 | return ( 82 | <> 83 | {/* if atom content length is greater than 1, then render a rectangle instead */} 84 | {atomContent().length > 1 ? ( 85 | 100 | ) : ( 101 | 116 | )} 117 | 132 | {atomContent()} 133 | 134 | 135 | ); 136 | }; 137 | 138 | return ; 139 | }); 140 | 141 | export default Atom; 142 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/circle.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from "solid-js/jsx-runtime"; 2 | import { Layout } from "./layout"; 3 | import { BBox, Id, Transform } from "./scenegraph"; 4 | import { splitProps } from "solid-js"; 5 | import withBluefish from "./withBluefish"; 6 | 7 | export type CircleProps = JSX.CircleSVGAttributes & { 8 | name: Id; 9 | cx?: number; 10 | cy?: number; 11 | r: number; 12 | }; 13 | 14 | export const Circle = withBluefish( 15 | (props: CircleProps) => { 16 | const layout = () => { 17 | return { 18 | bbox: { 19 | left: -props.r, 20 | top: -props.r, 21 | width: props.r * 2, 22 | height: props.r * 2, 23 | }, 24 | transform: { 25 | translate: { 26 | x: props.cx, 27 | y: props.cy, 28 | }, 29 | }, 30 | }; 31 | }; 32 | 33 | const paint = (paintProps: { bbox: BBox; transform: Transform }) => { 34 | const [_, rest] = splitProps(props, ["name", "cx", "cy", "r"]); 35 | 36 | const r = () => (paintProps.bbox.width ?? 0) / 2; 37 | 38 | return ( 39 | 53 | ); 54 | }; 55 | 56 | return ; 57 | }, 58 | { displayName: "Circle" } 59 | ); 60 | 61 | export default Circle; 62 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/createName.tsx: -------------------------------------------------------------------------------- 1 | import { Accessor, createContext, createUniqueId, useContext } from "solid-js"; 2 | import { SetStoreFunction, Store } from "solid-js/store"; 3 | import { Id } from "./scenegraph"; 4 | 5 | export type Name = string; 6 | 7 | /* 8 | TODO: This is wrong!!! 9 | A name should either point to an id of another name or else point to an id of a layout node. 10 | 11 | draw this out. it's too hard to see 12 | */ 13 | 14 | export type Scope = { 15 | [key: Id]: { 16 | layoutNode: Id | undefined; 17 | parent: Id; // TODO: not sure if this is used. might be useful for debugging/error reporting, though 18 | children: { [key: Name]: Id }; 19 | }; 20 | }; 21 | 22 | // the scope context is a map from uid to a set of names that map to other uids 23 | export const ScopeContext = createContext< 24 | [get: Store, set: SetStoreFunction] 25 | >([{}, () => {}]); 26 | export const ParentScopeIdContext = createContext>(() => ""); 27 | 28 | export const createName = (name: string) => { 29 | const genId = `${name}(${createUniqueId()})`; 30 | 31 | const [scope, setScope] = useContext(ScopeContext); 32 | const parentId = useContext(ParentScopeIdContext); 33 | 34 | // TODO: check if it exists before adding 35 | 36 | // setScope(name, genId); 37 | setScope(genId, { 38 | parent: parentId(), 39 | layoutNode: undefined, 40 | children: {}, 41 | }); // inside withBluefish, we first check if we have a props.name as input, which will then become the parent context. otherwise we'll generate a new id for the parent context 42 | setScope(parentId(), "children", name, genId); 43 | 44 | return genId; 45 | }; 46 | 47 | export const resolveName = ( 48 | layoutNode: Id, 49 | options?: { default?: boolean } 50 | ): Name | undefined => { 51 | options = { 52 | default: true, 53 | ...options, 54 | }; 55 | const [scope] = useContext(ScopeContext); 56 | const name = Object.keys(scope).find( 57 | (name) => scope[name].layoutNode === layoutNode 58 | ); 59 | 60 | return name ?? (options?.default ? layoutNode : undefined); 61 | }; 62 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/ellipse.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from "solid-js/jsx-runtime"; 2 | import { Layout } from "./layout"; 3 | import { BBox, Id, Transform } from "./scenegraph"; 4 | import { splitProps } from "solid-js"; 5 | import withBluefish from "./withBluefish"; 6 | 7 | export type EllipseProps = JSX.EllipseSVGAttributes & { 8 | name: Id; 9 | cx?: number; 10 | cy?: number; 11 | rx: number; 12 | ry: number, 13 | }; 14 | 15 | export const Ellipse = withBluefish( 16 | (props: EllipseProps) => { 17 | const layout = () => { 18 | return { 19 | bbox: { 20 | left: -props.rx, 21 | top: -props.ry, 22 | width: props.rx * 2, 23 | height: props.ry * 2, 24 | }, 25 | transform: { 26 | translate: { 27 | x: props.cx, 28 | y: props.cy, 29 | }, 30 | }, 31 | }; 32 | }; 33 | 34 | const paint = (paintProps: { bbox: BBox; transform: Transform }) => { 35 | const [_, rest] = splitProps(props, ["name", "cx", "cy", "rx", "ry"]); 36 | 37 | const rx = () => (paintProps.bbox.width ?? 0) / 2; 38 | const ry = () => (paintProps.bbox.height ?? 0)/2; 39 | 40 | return ( 41 | 56 | ); 57 | }; 58 | 59 | return ; 60 | }, 61 | { displayName: "Ellipse" } 62 | ); 63 | 64 | export default Ellipse; 65 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/errorContext.ts: -------------------------------------------------------------------------------- 1 | import { createContext, useContext } from "solid-js"; 2 | import { BluefishError } from "./errors"; 3 | import { createStore } from "solid-js/store"; 4 | 5 | export type ErrorContextType = { 6 | errors: BluefishError[]; 7 | addError: (error: BluefishError) => void; 8 | }; 9 | 10 | export const createErrorContext = ( 11 | onError: (error: BluefishError) => void 12 | ): ErrorContextType => { 13 | const [errors, setErrors] = createStore([]); 14 | const addError = (error: BluefishError) => { 15 | setErrors((prev) => [...prev, error]); 16 | onError(error); 17 | }; 18 | return { errors, addError }; 19 | }; 20 | 21 | export const ErrorContext = createContext(null); 22 | 23 | export const useError = () => { 24 | const context = useContext(ErrorContext); 25 | 26 | // if (context === null) { 27 | // throw new Error("useError must be used within an ErrorContext.Provider"); 28 | // } 29 | 30 | // return context.addError; 31 | return (error: BluefishError) => { 32 | console.warn(error); 33 | }; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/example-gallery/path.tsx: -------------------------------------------------------------------------------- 1 | // Path Component extending base Bluefish. Should eventually be merged into Bluefish 2 | import { 3 | JSX, 4 | ParentProps, 5 | Show, 6 | createEffect, 7 | mergeProps, 8 | splitProps, 9 | untrack, 10 | } from "solid-js"; 11 | import withBluefish from "../withBluefish"; 12 | import { PaperScope } from "paper"; 13 | import Layout from "../layout"; 14 | // TODO: should be exported by Bluefish 15 | export const maybeSub = (a, b) => 16 | a !== undefined && b !== undefined ? a - b : undefined; 17 | 18 | // TODO: add support for points as well as d? 19 | // export type PathProps = JSX.PathSVGAttributes & { 20 | // name: Id; 21 | // d: string; 22 | // x?: number; 23 | // y?: number; 24 | // position?: "absolute" | "relative"; 25 | // }; 26 | 27 | export const Path = withBluefish((props) => { 28 | props = mergeProps( 29 | { 30 | "stroke-width": 3, 31 | stroke: "black", 32 | position: "relative", 33 | fill: "none", 34 | }, 35 | props 36 | ); 37 | 38 | const canvas = document.createElement("canvas"); 39 | const paperScope = new PaperScope(); 40 | paperScope.setup(canvas); 41 | 42 | const layout = (childIds) => { 43 | childIds = Array.from(childIds); 44 | 45 | if (props.name.endsWith("DEBUG")) { 46 | debugger; 47 | } 48 | 49 | const path = new paperScope.Path(props.d); 50 | const bounds = path.bounds; 51 | 52 | return { 53 | transform: { 54 | translate: { 55 | x: props.position === "absolute" ? 0 : maybeSub(props.x, bounds.left), 56 | y: props.position === "absolute" ? 0 : maybeSub(props.y, bounds.top), 57 | }, 58 | }, 59 | bbox: { 60 | left: bounds.left, 61 | top: bounds.top, 62 | width: bounds.width, 63 | height: bounds.height, 64 | }, 65 | customData: { 66 | path: path, 67 | }, 68 | }; 69 | }; 70 | 71 | const paint = (paintProps) => { 72 | const [_, rest] = splitProps(props, ["name", "x", "y", "d", "position"]); 73 | 74 | return ( 75 | {paintProps.children}} 78 | > 79 | 84 | 85 | {paintProps.children} 86 | 87 | 88 | ); 89 | }; 90 | 91 | return ( 92 | 93 | {props.children} 94 | 95 | ); 96 | }); 97 | 98 | export default Path; 99 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/example-gallery/qc-text.tsx: -------------------------------------------------------------------------------- 1 | import { Path } from "./path.jsx"; 2 | import { Line } from "../line.jsx"; 3 | import Background from "../background.jsx"; 4 | import Bluefish from "../bluefish.jsx"; 5 | import Circle from "../circle.jsx"; 6 | import Group from "../group.jsx"; 7 | import Rect from "../rect.jsx"; 8 | import Ref from "../ref.jsx"; 9 | import { StackH } from "../stackh.jsx"; 10 | import { StackV } from "../stackv.jsx"; 11 | import withBluefish from "../withBluefish.jsx"; 12 | import Align from "../align.jsx"; 13 | import Text from "../text.jsx"; 14 | 15 | const Wire = withBluefish((props) => ( 16 | 17 | 18 | 19 | 20 | {props.children} 21 | 22 | 23 | )); 24 | 25 | const WireSymbol = withBluefish((props) => ( 26 | 27 | 28 | {props.children} 29 | 30 | )); 31 | 32 | const EmptySpot = withBluefish(() => ); 33 | 34 | const BoxedSymbol = withBluefish((props) => { 35 | return ( 36 | ( 38 | 45 | )} 46 | > 47 | 56 | {props.children} 57 | 58 | 59 | ); 60 | }); 61 | 62 | const OPlus = withBluefish(() => ( 63 | 64 | 65 | 71 | 72 | 73 | 74 | 75 | )); 76 | 77 | const ControlDot = withBluefish(() => ); 78 | 79 | export const QCText = () => { 80 | return ( 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | Z 91 | 92 | 93 | 94 | ≡ 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | H 105 | 106 | 107 | 108 | H 109 | 110 | 111 | This is a controlled-NOT. 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | } 123 | > 124 | 125 | 126 | } 128 | > 129 | 130 | 131 | 132 | ); 133 | }; 134 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/gradient.tsx: -------------------------------------------------------------------------------- 1 | import { For } from "solid-js"; 2 | import withBluefish from "./withBluefish"; 3 | import Layout from "./layout"; 4 | import { BBox, Id, Transform } from "./scenegraph"; 5 | 6 | type ColorOffset = { offset: number; color: string }; 7 | 8 | type GradientProps = { 9 | name: Id; 10 | id: string; 11 | colorOffsets: ColorOffset[]; 12 | x1?: number; 13 | x2?: number; 14 | y1?: number; 15 | y2?: number; 16 | }; 17 | 18 | export const Gradient = withBluefish( 19 | (props: GradientProps) => { 20 | const layout = () => { 21 | return { 22 | bbox: { 23 | centerX: 0, 24 | centerY: 0, 25 | width: 0, 26 | height: 0, 27 | }, 28 | transform: { 29 | translate: {}, 30 | }, 31 | }; 32 | }; 33 | 34 | const paint = (_paintProps: { bbox: BBox; transform: Transform }) => { 35 | return ( 36 | 37 | 44 | 45 | {(colorOffset) => ( 46 | 50 | )} 51 | 52 | 53 | 54 | ); 55 | }; 56 | 57 | return ; 58 | }, 59 | { displayName: "Gradient" } 60 | ); 61 | 62 | export default Gradient; 63 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/group.tsx: -------------------------------------------------------------------------------- 1 | import { JSX, ParentProps } from "solid-js"; 2 | import { BBox, ChildNode, Id, Transform, useScenegraph } from "./scenegraph"; 3 | import Layout from "./layout"; 4 | import { 5 | maxOfMaybes, 6 | maybeAdd, 7 | maybeMax, 8 | maybeMin, 9 | maybeSub, 10 | minOfMaybes, 11 | } from "./util/maybe"; 12 | import { startsWith } from "lodash"; 13 | import withBluefish from "./withBluefish"; 14 | 15 | export type GroupProps = ParentProps<{ 16 | name: Id; 17 | x?: number; 18 | y?: number; 19 | rels?: () => JSX.Element; 20 | }>; 21 | 22 | export const Group = withBluefish( 23 | (props: GroupProps) => { 24 | // NOTE: unlike other layout functions. this one determines its bbox by *skipping* undefined 25 | // values. 26 | // this is to ensure that if some child of the group doesn't know all its dimensions, then we 27 | // ignore that. 28 | // COMBAK: I'm not sure this is the correct behavior in general... 29 | const layout = (childNodes: ChildNode[]) => { 30 | if (props.name.endsWith("DEBUG")) { 31 | debugger; 32 | } 33 | 34 | for (const childNode of childNodes) { 35 | if (!childNode.owned.left) { 36 | childNode.bbox.left = 0; 37 | } 38 | 39 | if (!childNode.owned.top) { 40 | childNode.bbox.top = 0; 41 | } 42 | } 43 | 44 | const bboxes = { 45 | left: childNodes.map((childNode) => childNode.bbox.left), 46 | top: childNodes.map((childNode) => childNode.bbox.top), 47 | width: childNodes.map((childNode) => childNode.bbox.width), 48 | height: childNodes.map((childNode) => childNode.bbox.height), 49 | }; 50 | 51 | const left = minOfMaybes(bboxes.left) ?? 0; 52 | 53 | const right = maxOfMaybes( 54 | bboxes.left.map((left, i) => maybeAdd(left, bboxes.width[i])) 55 | ); 56 | 57 | const top = minOfMaybes(bboxes.top) ?? 0; 58 | 59 | const bottom = maxOfMaybes( 60 | bboxes.top.map((top, i) => maybeAdd(top, bboxes.height[i])) 61 | ); 62 | 63 | const width = maybeSub(right, left); 64 | const height = maybeSub(bottom, top); 65 | 66 | return { 67 | transform: { 68 | translate: { 69 | x: maybeSub(props.x, left), 70 | y: maybeSub(props.y, top), 71 | }, 72 | }, 73 | bbox: { left, top, width, height }, 74 | }; 75 | }; 76 | 77 | const paint = (paintProps: { 78 | bbox: BBox; 79 | transform: Transform; 80 | children: JSX.Element; 81 | }) => { 82 | return ( 83 | 88 | {paintProps.children} 89 | 90 | ); 91 | }; 92 | 93 | return ( 94 | 95 | {props.children} 96 | {props.rels?.() ?? null} 97 | 98 | ); 99 | }, 100 | { displayName: "Group" } 101 | ); 102 | 103 | export default Group; 104 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/image.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from "solid-js/jsx-runtime"; 2 | import { Layout } from "./layout"; 3 | import { BBox, Id, Transform } from "./scenegraph"; 4 | import { splitProps } from "solid-js"; 5 | import withBluefish from "./withBluefish"; 6 | 7 | export type ImageProps = JSX.ImageSVGAttributes & { 8 | name: Id; 9 | x?: number; 10 | y?: number; 11 | width?: number; 12 | height?: number; 13 | }; 14 | 15 | export const Image = withBluefish( 16 | (props: ImageProps) => { 17 | const layout = () => { 18 | return { 19 | bbox: { 20 | left: 0, 21 | top: 0, 22 | width: props.width, 23 | height: props.height, 24 | }, 25 | transform: { 26 | translate: { 27 | x: props.x, 28 | y: props.y, 29 | }, 30 | }, 31 | }; 32 | }; 33 | 34 | const paint = (paintProps: { bbox: BBox; transform: Transform }) => { 35 | const [_, rest] = splitProps(props, [ 36 | "name", 37 | "x", 38 | "y", 39 | "width", 40 | "height", 41 | ]); 42 | 43 | return ( 44 | 56 | ); 57 | }; 58 | 59 | return ; 60 | }, 61 | { displayName: "Image" } 62 | ); 63 | 64 | export default Image; 65 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/index.ts: -------------------------------------------------------------------------------- 1 | export { Align } from "./align"; 2 | export type { AlignProps, Alignment1D, Alignment2D, AlignmentHorizontal, AlignmentVertical } from "./align"; 3 | 4 | export { Arrow } from "./arrow"; 5 | export type { ArrowProps } from "./arrow"; 6 | 7 | export { Background } from "./background"; 8 | export type { BackgroundProps } from "./background"; 9 | 10 | export { Blob } from "./blob"; 11 | export type { BlobProps } from "./blob"; 12 | 13 | export { Bluefish } from "./bluefish"; 14 | export type { BluefishProps } from "./bluefish"; 15 | 16 | export { Circle } from "./circle"; 17 | export type { CircleProps } from "./circle"; 18 | 19 | export { StackV } from "./stackv"; 20 | export type { StackVProps } from "./stackv"; 21 | 22 | export { Distribute } from "./distribute"; 23 | export type { DistributeProps } from "./distribute"; 24 | 25 | export { Group } from "./group"; 26 | export type { GroupProps } from "./group"; 27 | 28 | export { Image } from "./image"; 29 | export type { ImageProps } from "./image"; 30 | 31 | export { Layout } from "./layout"; 32 | export type { LayoutProps } from "./layout"; 33 | 34 | export { LayoutFunction } from "./layoutFunction"; 35 | export type { LayoutFunctionProps } from "./layoutFunction"; 36 | 37 | export { Line } from "./line"; 38 | export type { LineProps } from "./line"; 39 | 40 | export { Rect } from "./rect"; 41 | export type { RectProps } from "./rect"; 42 | 43 | export { Ref } from "./ref"; 44 | export type { RefProps } from "./ref"; 45 | 46 | export { StackH } from "./stackh"; 47 | export type { StackHProps } from "./stackh"; 48 | 49 | export { Text } from "./text"; 50 | export type { TextProps } from "./text/types"; 51 | 52 | export { Path } from "./path"; 53 | export type { PathProps } from "./path"; 54 | 55 | export { withBluefish } from "./withBluefish"; 56 | export type { WithBluefishProps } from "./withBluefish"; 57 | 58 | export { createScenegraph, useScenegraph, UNSAFE_useScenegraph, ScenegraphContext } from "./scenegraph"; 59 | export type { 60 | Id, 61 | BBox, 62 | BBoxOwners, 63 | Transform, 64 | TransformOwners, 65 | ScenegraphNode, 66 | Scenegraph, 67 | ScenegraphContextType, 68 | LayoutFn, 69 | } from "./scenegraph"; 70 | export { createName, ScopeContext, ParentScopeIdContext } from "./createName"; 71 | export type { Name, Scope } from "./createName"; 72 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/layout.tsx: -------------------------------------------------------------------------------- 1 | import { Dynamic } from "solid-js/web"; 2 | import { 3 | For, 4 | JSX, 5 | ParentProps, 6 | createRenderEffect, 7 | createSignal, 8 | on, 9 | onCleanup, 10 | useContext, 11 | } from "solid-js"; 12 | import { 13 | BBox, 14 | Transform, 15 | UNSAFE_useScenegraph, 16 | LayoutFn, 17 | Id, 18 | ScenegraphElement, 19 | resolveScenegraphElements, 20 | UNSAFE_asNode, 21 | } from "./scenegraph"; 22 | import { IdContext } from "./withBluefish"; 23 | import { ScopeContext } from "./createName"; 24 | import { useError } from "./errorContext"; 25 | import { LayoutUIDContext } from "./bluefish"; 26 | import { createStore, produce } from "solid-js/store"; 27 | 28 | export type LayoutProps = ParentProps<{ 29 | name: Id; 30 | bbox?: BBox; 31 | layout: LayoutFn; 32 | paint: (props: { 33 | bbox: BBox; 34 | transform: Transform; 35 | children: JSX.Element; 36 | customData?: any; 37 | }) => JSX.Element; 38 | }>; 39 | 40 | export const Layout = (props: LayoutProps) => { 41 | const [_scope, setScope] = useContext(ScopeContext); 42 | const error = useError(); 43 | const layoutUID = useContext(LayoutUIDContext); 44 | const [childLayouts, setChildLayouts] = createSignal< 45 | ((parentId: Id | null) => void)[] 46 | >([]); 47 | 48 | const [scenegraphInfo, setScenegraphInfo] = createStore({ 49 | bbox: {}, 50 | transform: { translate: {} }, 51 | customData: {}, 52 | }); 53 | 54 | const { scenegraph, createNode, mergeBBoxAndTransform, createChildRepr } = 55 | UNSAFE_useScenegraph(); 56 | 57 | // evaluate the child props before running the effect so that children's layout functions are 58 | // called before the parent's layout function 59 | // h/t Erik Demaine 60 | const jsx = ( 61 | undefined}> 62 | {(() => { 63 | const childNodes = resolveScenegraphElements(props.children); 64 | 65 | setChildLayouts(() => { 66 | return childNodes.map((child) => { 67 | return child.layout; 68 | }); 69 | }); 70 | 71 | return ( 72 | 73 | {(child) => child.jsx} 74 | 75 | ); 76 | })()} 77 | 78 | ); 79 | 80 | onCleanup(() => { 81 | // when the Layout node is destroyed, we need to clear any relevant scopes 82 | setScope( 83 | produce((scope) => { 84 | // filter out scopes that have this id as their layoutNode 85 | for (const key of Object.keys(scope) as Array) { 86 | if (scope[key].layoutNode === props.name) { 87 | delete scope[key]; 88 | } 89 | } 90 | }) 91 | ); 92 | }); 93 | 94 | createRenderEffect( 95 | on( 96 | () => layoutUID(), 97 | () => { 98 | const node = UNSAFE_asNode( 99 | scenegraph[props.name] ?? { 100 | type: "node", 101 | bbox: {}, 102 | transform: { translate: {} }, 103 | children: [], 104 | customData: {}, 105 | } 106 | ); 107 | setScenegraphInfo({ 108 | bbox: node.bbox ?? {}, 109 | transform: { 110 | translate: { 111 | x: node.transform?.translate?.x ?? 0, 112 | y: node.transform?.translate?.y ?? 0, 113 | }, 114 | }, 115 | customData: node.customData, 116 | }); 117 | } 118 | ) 119 | ); 120 | 121 | const layout = (parentId: Id | null) => { 122 | createNode(props.name, parentId); 123 | 124 | for (const childLayout of childLayouts()) { 125 | childLayout(props.name); 126 | } 127 | 128 | const node = UNSAFE_asNode(scenegraph[props.name]); 129 | 130 | const { bbox, transform, customData } = props.layout( 131 | (node.children ?? []).map((childId: Id) => 132 | createChildRepr(props.name, childId) 133 | ) 134 | ); 135 | 136 | mergeBBoxAndTransform(props.name, props.name, bbox, transform); 137 | node.customData = customData; 138 | }; 139 | 140 | return { 141 | jsx, 142 | layout, 143 | } satisfies ScenegraphElement as unknown as JSX.Element; 144 | }; 145 | 146 | export default Layout; 147 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/layoutFunction.tsx: -------------------------------------------------------------------------------- 1 | import { JSX, ParentProps, Show, createEffect, mergeProps, untrack } from "solid-js"; 2 | 3 | import _, { get, startsWith } from "lodash"; 4 | import Layout from "./layout"; 5 | import withBluefish from "./withBluefish"; 6 | import { Id, ChildNode, BBox } from "./scenegraph"; 7 | 8 | export const maybeSub = (a: number, b: number) => (a !== undefined && b !== undefined ? a - b : undefined); 9 | const maybeMin = (a: number, b: number) => (a !== undefined && b !== undefined ? Math.min(a, b) : undefined); 10 | const maybeMax = (a: number, b: number) => (a !== undefined && b !== undefined ? Math.max(a, b) : undefined); 11 | 12 | export type LayoutFunctionProps = ParentProps<{ 13 | f: (fromBBox: BBox, toBBox: BBox) => BBox; 14 | x?: number; 15 | y?: number; 16 | }>; 17 | 18 | export const LayoutFunction = withBluefish( 19 | (props: LayoutFunctionProps) => { 20 | props = mergeProps({}, props); 21 | 22 | const layout = (childNodes: ChildNode[]) => { 23 | const fromBBox = childNodes[0].bbox; 24 | const toBBox = childNodes[1].bbox; 25 | 26 | for (const [key, value] of Object.entries(props.f(fromBBox, toBBox))) { 27 | toBBox[key] = value; 28 | } 29 | 30 | return { 31 | transform: { 32 | translate: { 33 | x: props.x, 34 | y: props.y, 35 | }, 36 | }, 37 | bbox: toBBox, 38 | }; 39 | }; 40 | 41 | const paint = (paintProps) => { 42 | return {paintProps.children}; 43 | }; 44 | 45 | return ( 46 | 47 | {props.children} 48 | 49 | ); 50 | }, 51 | { displayName: "LayoutFunction" } 52 | ); 53 | 54 | export default LayoutFunction; 55 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/measure-text.tsx: -------------------------------------------------------------------------------- 1 | export type TextMeasurement = { 2 | left: number; 3 | right: number; 4 | width: number; 5 | fontTop: number; 6 | fontBottom: number; 7 | fontHeight: number; 8 | // position of text's alphabetic baseline assuming top is the origin 9 | baseline: number; 10 | fontDescent: number; 11 | actualDescent: number; 12 | }; 13 | 14 | export function measureText(text: string, font: string): TextMeasurement { 15 | measureText.context.textBaseline = "alphabetic"; 16 | measureText.context.font = font; 17 | const measurements = measureText.context.measureText(text); 18 | return { 19 | left: measurements.actualBoundingBoxLeft, 20 | right: measurements.actualBoundingBoxRight, 21 | fontTop: measurements.fontBoundingBoxAscent, 22 | fontBottom: measurements.fontBoundingBoxDescent, 23 | width: 24 | Math.abs(measurements.actualBoundingBoxLeft) + 25 | Math.abs(measurements.actualBoundingBoxRight), 26 | fontHeight: 27 | Math.abs(measurements.fontBoundingBoxAscent) + 28 | Math.abs(measurements.fontBoundingBoxDescent), 29 | baseline: Math.abs(measurements.fontBoundingBoxAscent), 30 | fontDescent: Math.abs(measurements.fontBoundingBoxDescent), 31 | actualDescent: Math.abs(measurements.actualBoundingBoxDescent), 32 | }; 33 | } 34 | // static variable 35 | export namespace measureText { 36 | export const element = document.createElement("canvas"); 37 | // puts canvas on screen. useful for debugging measurements 38 | /* element.width = 1000; 39 | element.height = 1000; 40 | document.body.appendChild(element); */ 41 | export const context = element.getContext("2d")!; 42 | } 43 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/path.tsx: -------------------------------------------------------------------------------- 1 | import { mergeProps, splitProps } from "solid-js"; 2 | import Layout from "./layout"; 3 | import { BBox, Id, Transform } from "./scenegraph"; 4 | import { maybeSub } from "./util/maybe"; 5 | import { PaperScope } from "paper/dist/paper-core"; 6 | import withBluefish from "./withBluefish"; 7 | import { JSX } from "solid-js/jsx-runtime"; 8 | 9 | export type PathProps = JSX.PathSVGAttributes & { 10 | name: Id; 11 | d: string; 12 | x?: number; 13 | y?: number; 14 | position?: "absolute" | "relative"; 15 | }; 16 | 17 | export const Path = withBluefish((rawProps: PathProps) => { 18 | const props = mergeProps( 19 | { 20 | "stroke-width": 3, 21 | stroke: "black", 22 | position: "relative", 23 | fill: "none", 24 | d: "", 25 | }, 26 | rawProps 27 | ); 28 | 29 | const canvas = document.createElement("canvas"); 30 | const paperScope = new PaperScope(); 31 | paperScope.setup(canvas); 32 | 33 | const layout = () => { 34 | const path = new paperScope.Path(props.d); 35 | const bounds = path.bounds; 36 | 37 | return { 38 | transform: { 39 | translate: { 40 | x: props.position === "absolute" ? 0 : maybeSub(props.x, bounds.left), 41 | y: props.position === "absolute" ? 0 : maybeSub(props.y, bounds.top), 42 | }, 43 | }, 44 | bbox: { 45 | left: bounds.left, 46 | top: bounds.top, 47 | width: bounds.width, 48 | height: bounds.height, 49 | }, 50 | customData: { 51 | path, 52 | }, 53 | }; 54 | }; 55 | 56 | const paint = (paintProps: { 57 | bbox: BBox; 58 | transform: Transform; 59 | customData?: { path: { pathData: string } }; 60 | }) => { 61 | const [_, rest] = splitProps(props, ["name", "x", "y", "d", "position"]); 62 | 63 | return ( 64 | 69 | 70 | 71 | ); 72 | }; 73 | 74 | return ; 75 | }); 76 | 77 | export default Path; 78 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/README.md: -------------------------------------------------------------------------------- 1 | # Performance Testing 2 | 3 | Contains scripts and files for generating test data and testing code speed. 4 | 5 | ## File Structure 6 | 7 | For three diagrams (Python Tutor, Ohm Parser, and Insertion Sort), we measured how render time of the diagram scaled with the size/number of sub-elements in the diagram. Performance testing scripts for these 3 diagrams are in separate folders. 8 | 9 | Inside each folder is an associated Python file, prefixed with `generate_`, which is used to generate a `.ts` file containing the requisite props for the diagram (with `Props` suffixed). The code for generating each diagram is in the `tsx` file in the folder, and the diagram component has been configured to take the props generated by the Python file as input. 10 | 11 | ## Running 12 | 13 | In order to performance test the diagrams, there are a few steps to take: 14 | 15 | 1. Create an `App.tsx` file in `/public`. This can be done by duplicating the `App.template.tsx` file in the `/public` folder and renaming it to `App.tsx`. 16 | 17 | 2. Replace the following block of code in `App.tsx` 18 | 19 | ```js 20 | 21 | Duplicate this file and name it `App.tsx` to use the dev playground! 22 | 23 | ``` 24 | 25 | with the name of the component that you are trying to test. The names of the components are as follows: 26 | 27 | - Insertion Sort: `` 28 | - Ohm Parser: `` 29 | - Python Tutor: `` 30 | 31 | 3. Generate a props file for the diagram you are trying to test. This is done by downloading and running `python [the relevant script].py`. For example, for the Insertion Sort diagram, you would run download and run `python generate_insertion_sort.py`. Each script also has toggleable size parameters controlling the complexity of the rendered diagram. 32 | 33 | 4. Replace the `.ts` props file in the repository with the `.ts` props file generated by the python script. 34 | 35 | 5. [Optional] To find the number of nodes in the scenegraph of the diagram that you're generating, you should go into the relevant `.tsx` file and change the `debug={false}` parameter in to `debug={true}`. This will cause Bluefish to print out the total length of the scenegraph. After you find this number, to get more accurate performance results, make sure to change it back to `debug={false}` 36 | 37 | 6. Run `pnpm dev` to start a localhost server. Navigate to `http://localhost:3000/public/index.html` to see the rendered diagram. You should then be able to use browser tools or other means to measure the time it takes Bluefish to render the diagram. 38 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/insertionSort/InsertionSortTest.tsx: -------------------------------------------------------------------------------- 1 | import Bluefish from "../../bluefish"; 2 | import { InsertionSort } from "../../example-gallery/insertion-sort"; 3 | import { sortProps } from "./insertionSortProps"; 4 | 5 | export const InsertionSortTest = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/insertionSort/generate_insertion_sort.py: -------------------------------------------------------------------------------- 1 | import random 2 | 3 | def generate_random_array_and_write_to_ts(size): 4 | # Generate a random array of integers between 0 and 99 inclusive 5 | random_array = [random.randint(0, 99) for _ in range(size)] 6 | 7 | # Write the array to a TypeScript file 8 | with open("insertionSortProps.ts", "w") as file: 9 | file.write(f"export const sortProps = {random_array};\n") 10 | 11 | 12 | # -------------- PARAMETERS -------------- 13 | num_elements = 10 # number of elements in array to generate 14 | # ---------------------------------------- 15 | 16 | generate_random_array_and_write_to_ts(num_elements) 17 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/insertionSort/insertionSortProps.ts: -------------------------------------------------------------------------------- 1 | export const sortProps = [22, 32, 9, 47, 33, 67, 0, 57, 23, 21]; 2 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/ohmParser/generate_ohm_expression.py: -------------------------------------------------------------------------------- 1 | # randomly generate an arithmetic expression with * and + operators (as well as parantheses) that can be used for testing the ohm parser 2 | import random 3 | 4 | def generate_string_expression(num_constants, max_open_parens, open_parens_thresh, close_parens_thresh): 5 | expression = "" 6 | open_parens = 0 7 | for i in range(num_constants): 8 | if open_parens < max_open_parens: 9 | if(random.random() > open_parens_thresh): 10 | # create another open parenthesis 11 | expression += "(" 12 | open_parens += 1 13 | 14 | expression += str(random.randint(0, 10)) 15 | 16 | if open_parens > 0: 17 | if(random.random() > close_parens_thresh): 18 | # remove an open parenthesis 19 | expression += ")" 20 | open_parens -= 1 21 | 22 | if i != num_constants - 1: 23 | expression += "+" if random.random() > 1 else "*" # randomly add an operator 24 | 25 | for i in range(open_parens): 26 | expression += ")" 27 | 28 | return expression 29 | 30 | # -------------- PARAMETERS -------------- 31 | num_constants = 10 # number of constants in expression 32 | max_open_parens = 5 # maximum number of open parentheses at a time 33 | open_parens_thresh = 0.66 # probability that a opening parenthesis will appear before a constant in the expression 34 | close_parens_thresh = 0.66 # probability that a closing parenthesis will appear after a constant in the expression (given there are open parentheses) 35 | # ---------------------------------------- 36 | 37 | expression = generate_string_expression(num_constants, max_open_parens, open_parens_thresh, close_parens_thresh) 38 | print("Expression Length:", len(expression)) 39 | # Write the generated expression to a TypeScript file 40 | with open("ohm_expression.ts", "w") as ts_file: 41 | ts_file.write(f"export const expression = \"{expression}\";") 42 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/ohmParser/ohmParserTest.tsx: -------------------------------------------------------------------------------- 1 | import Bluefish from "../../bluefish"; 2 | import { OhmParser } from "../../example-gallery/ohm-parser"; 3 | import { expression } from "./ohmProps"; 4 | 5 | export const OhmParserTest = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/ohmParser/ohmProps.ts: -------------------------------------------------------------------------------- 1 | export const expression = "8*3*(7)*1*(8)*0*2*(2*3*(3))"; -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/pythonTutor/pythonTutorProgram.py: -------------------------------------------------------------------------------- 1 | var0 = [['20', '98', ['84'], '28'], '22'] 2 | var1 = ['7'] 3 | var2 = ['24', var0, '81'] 4 | var3 = ['76', '64', var0] 5 | var4 = [var2, '79'] -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/pythonTutor/pythonTutorProps.ts: -------------------------------------------------------------------------------- 1 | export const pythonProps = { 2 | "stack": [ 3 | { 4 | "variable": "var0", 5 | "value": { 6 | "type": "pointer", 7 | "value": 0 8 | } 9 | }, 10 | { 11 | "variable": "var1", 12 | "value": { 13 | "type": "pointer", 14 | "value": 3 15 | } 16 | }, 17 | { 18 | "variable": "var2", 19 | "value": { 20 | "type": "pointer", 21 | "value": 4 22 | } 23 | }, 24 | { 25 | "variable": "var3", 26 | "value": { 27 | "type": "pointer", 28 | "value": 5 29 | } 30 | }, 31 | { 32 | "variable": "var4", 33 | "value": { 34 | "type": "pointer", 35 | "value": 6 36 | } 37 | } 38 | ], 39 | "heap": [ 40 | { 41 | "type": "tuple", 42 | "values": [ 43 | { 44 | "type": "pointer", 45 | "value": 1 46 | }, 47 | "22" 48 | ] 49 | }, 50 | { 51 | "type": "tuple", 52 | "values": [ 53 | "20", 54 | "98", 55 | { 56 | "type": "pointer", 57 | "value": 2 58 | }, 59 | "28" 60 | ] 61 | }, 62 | { 63 | "type": "tuple", 64 | "values": [ 65 | "84" 66 | ] 67 | }, 68 | { 69 | "type": "tuple", 70 | "values": [ 71 | "7" 72 | ] 73 | }, 74 | { 75 | "type": "tuple", 76 | "values": [ 77 | "24", 78 | { 79 | "type": "pointer", 80 | "value": 0 81 | }, 82 | "81" 83 | ] 84 | }, 85 | { 86 | "type": "tuple", 87 | "values": [ 88 | "76", 89 | "64", 90 | { 91 | "type": "pointer", 92 | "value": 0 93 | } 94 | ] 95 | }, 96 | { 97 | "type": "tuple", 98 | "values": [ 99 | { 100 | "type": "pointer", 101 | "value": 4 102 | }, 103 | "79" 104 | ] 105 | } 106 | ], 107 | "heapArrangement": [ 108 | [ 109 | 0, 110 | 1, 111 | 2 112 | ], 113 | [ 114 | 3 115 | ], 116 | [ 117 | 4 118 | ], 119 | [ 120 | 5 121 | ], 122 | [ 123 | 6 124 | ] 125 | ] 126 | }; -------------------------------------------------------------------------------- /packages/bluefish-solid/src/performance-testing/pythonTutor/pythonTutorTest.tsx: -------------------------------------------------------------------------------- 1 | import Bluefish from "../../bluefish"; 2 | import { PythonTutor } from "../../python-tutor/python-tutor"; 3 | import { pythonProps } from "./pythonTutorProps"; 4 | 5 | export const PythonTutorTest = () => { 6 | return ( 7 | 8 | 9 | 10 | ); 11 | }; 12 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/plot/dot.tsx: -------------------------------------------------------------------------------- 1 | import { For, ParentProps } from "solid-js"; 2 | import { JSX } from "solid-js/jsx-runtime"; 3 | import withBluefish from "../withBluefish"; 4 | import Circle from "../circle"; 5 | import { usePlotContext } from "./plot"; 6 | import Group from "../group"; 7 | 8 | export type DotProps = ParentProps< 9 | Omit< 10 | JSX.CircleSVGAttributes, 11 | "cx" | "cy" | "fill" | "width" | "height" | "label" 12 | > & { 13 | x: keyof T; 14 | y: keyof T; 15 | color?: keyof T; 16 | stroke?: keyof T; 17 | label?: 18 | | keyof T 19 | | { 20 | field: keyof T; 21 | avoid: Symbol[]; 22 | }; 23 | data?: T[]; 24 | } 25 | >; 26 | 27 | export const Dot = withBluefish((props: DotProps) => { 28 | const plotContext = usePlotContext(); 29 | 30 | // const resolvedX = () => plotContext.data.map((datum: any) => datum[props.x]); 31 | // const resolvedY = () => plotContext.data.map((datum: any) => datum[props.y]); 32 | // const resolvedColor = () => 33 | // plotContext.data.map((datum: any) => datum[props.color]); 34 | 35 | // const mappedX = () => resolvedX().map(plotContext.scales.x()); 36 | // const mappedY = () => resolvedY().map(plotContext.scales.y()); 37 | // const mappedColor = () => resolvedColor()?.map(plotContext.scales.color()); 38 | 39 | return ( 40 | 41 | 42 | {(datum) => { 43 | return ( 44 | 50 | ); 51 | }} 52 | 53 | 54 | ); 55 | }); 56 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/plot/plot.tsx: -------------------------------------------------------------------------------- 1 | import { ParentProps, createContext, splitProps, useContext } from "solid-js"; 2 | import { Id } from "../scenegraph"; 3 | import withBluefish from "../withBluefish"; 4 | import Group from "../group"; 5 | 6 | export type Scale = any; 7 | 8 | export type PlotProps = ParentProps<{ 9 | name: Id; 10 | width?: number; 11 | height?: number; 12 | x?: Scale; 13 | y?: Scale; 14 | color?: Scale; 15 | data?: any; 16 | }>; 17 | 18 | export type PlotContextValue = { 19 | data?: any; 20 | scales: { [key in string /* Scale */]: any }; 21 | }; 22 | 23 | const PlotContext = createContext(); 24 | 25 | export const usePlotContext = () => { 26 | const context = useContext(PlotContext); 27 | 28 | if (context === undefined) { 29 | throw new Error( 30 | "This component must be used within a containing Plot component." 31 | ); 32 | } 33 | 34 | return context; 35 | }; 36 | 37 | export const Plot = withBluefish((props: PlotProps) => { 38 | return ( 39 | 57 | {props.children} 58 | 59 | ); 60 | }); 61 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/python-tutor/elm-tuple.tsx: -------------------------------------------------------------------------------- 1 | import { createUniqueId } from "solid-js"; 2 | import { Align } from "../align"; 3 | import { Group } from "../group"; 4 | import { Rect } from "../rect"; 5 | import { Ref } from "../ref"; 6 | import { Id } from "../scenegraph"; 7 | import { Text } from "../text"; 8 | import { Value } from "./types"; 9 | import { createName } from "../createName"; 10 | import withBluefish from "../withBluefish"; 11 | 12 | export type ElmTupleProps = { 13 | name?: Id; 14 | tupleIndex: string; 15 | tupleData: { type: string; value: Value }; 16 | }; 17 | 18 | export const ElmTuple = withBluefish((props: ElmTupleProps) => { 19 | const fontFamily = "verdana, arial, helvetica, sans-serif"; 20 | 21 | const boxName = createName("box"); 22 | const labelName = createName("label"); 23 | const valName = createName("val"); 24 | 25 | return ( 26 | 27 | 34 | 40 | {props.tupleIndex} 41 | 42 | {props.tupleData.type === "string" ? ( 43 | 49 | {props.tupleData.value as string} 50 | 51 | ) : ( 52 | 53 | {""} 54 | 55 | )} 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | ); 66 | }); 67 | 68 | export default ElmTuple; 69 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/python-tutor/global-frame.tsx: -------------------------------------------------------------------------------- 1 | import { For, createUniqueId } from "solid-js"; 2 | import { Id } from "../scenegraph"; 3 | import Rect from "../rect"; 4 | import Group from "../group"; 5 | import Align from "../align"; 6 | import Ref from "../ref"; 7 | import { StackSlot } from "./stack-slot"; 8 | import Distribute from "../distribute"; 9 | import Text from "../text"; 10 | import { Value } from "./types"; 11 | import { createName } from "../createName"; 12 | import { StackV } from "../stackv"; 13 | import withBluefish from "../withBluefish"; 14 | 15 | export type GlobalFrameProps = { 16 | name?: Id; 17 | variables: { variable: string; value: Value }[]; 18 | }; 19 | 20 | export const GlobalFrame = withBluefish((props: GlobalFrameProps) => { 21 | const frameName = createName("frame"); 22 | const frameBorderName = createName("frameBorder"); 23 | const labelName = createName("label"); 24 | const frameVariablesName = createName("frameVariables"); 25 | const stackSlotNames = props.variables.map((_, i) => 26 | createName(`stackSlot-${i}`) 27 | ); 28 | 29 | // Font declaration 30 | const fontFamily = "Andale mono, monospace"; 31 | 32 | return ( 33 | ( 35 | <> 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | )} 50 | > 51 | {/* Global Frame and relevant text */} 52 | 53 | 54 | {/* TODO: there is a bug where the text is showing up lower than I expect it to... */} 55 | 61 | Global Frame 62 | 63 | 64 | 65 | {(variable, i) => ( 66 | 71 | )} 72 | 73 | 74 | 75 | ); 76 | }); 77 | 78 | export default GlobalFrame; 79 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/python-tutor/heap-object.tsx: -------------------------------------------------------------------------------- 1 | import Group from "../group"; 2 | import Text from "../text"; 3 | import { Id } from "../scenegraph"; 4 | import Distribute from "../distribute"; 5 | import Ref from "../ref"; 6 | import Align from "../align"; 7 | import { For, createUniqueId } from "solid-js"; 8 | import ElmTuple from "./elm-tuple"; 9 | import withBluefish from "../withBluefish"; 10 | import { Value } from "./types"; 11 | import { createName } from "../createName"; 12 | import { StackH } from "../stackh"; 13 | import { StackV } from "../stackv"; 14 | 15 | export type ObjectProps = { 16 | name: Id; 17 | objectType: string; 18 | objectValues: { 19 | type: string; 20 | value: Value; 21 | }[]; 22 | }; 23 | 24 | export const HeapObject = withBluefish((props: ObjectProps) => { 25 | const fontFamily = "verdana, arial, helvetica, sans-serif"; 26 | 27 | const objectTypeName = createName("objectType"); 28 | const objectRefName = createName("objectRef"); 29 | 30 | const elmNames = props.objectValues.map((_, i) => createName(`elm-${i}`)); 31 | 32 | return ( 33 | 34 | 40 | {props.objectType} 41 | 42 | 43 | 44 | {(elementData, index) => ( 45 | 50 | )} 51 | 52 | 53 | 54 | ); 55 | }); 56 | 57 | export default HeapObject; 58 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/python-tutor/heap.tsx: -------------------------------------------------------------------------------- 1 | import { For } from "solid-js"; 2 | import { Align } from "../align"; 3 | import { Distribute } from "../distribute"; 4 | import { Group } from "../group"; 5 | import { Rect } from "../rect"; 6 | import { Arrow } from "../arrow"; 7 | import { Ref } from "../ref"; 8 | import { Id } from "../scenegraph"; 9 | import withBluefish from "../withBluefish"; 10 | import { HeapObject } from "./heap-object"; 11 | import { Address, HeapObject as HeapObjectType, formatValue } from "./types"; 12 | import { StackH } from "../stackh"; 13 | import { StackV } from "../stackv"; 14 | import { createName } from "../createName"; 15 | 16 | export type HeapProps = { 17 | name: Id; 18 | heap: HeapObjectType[]; 19 | heapArrangement: (Address | null)[][]; 20 | }; 21 | 22 | export const Heap = withBluefish((props: HeapProps) => { 23 | const addressNames = props.heap.map((_, i) => createName(`address-${i}`)); 24 | 25 | return ( 26 | 27 | 28 | 29 | {(row, index) => ( 30 | 31 | 32 | {(address) => 33 | address === null ? ( 34 | 40 | ) : ( 41 | ({ 45 | type: typeof value === "string" ? "string" : "pointer", 46 | value: formatValue(value), 47 | }))} 48 | /> 49 | ) 50 | } 51 | 52 | 53 | )} 54 | 55 | 56 | 57 | {/* Add arrows between heap objects */} 58 | 59 | {(heapObject, address) => ( 60 | 61 | {(elmTupleValue, elmTupleIndex) => { 62 | // TODO: probably should just box every value to make this simpler 63 | if ( 64 | typeof elmTupleValue === "object" && 65 | "type" in elmTupleValue && 66 | elmTupleValue.type === "pointer" 67 | ) { 68 | return ( 69 | 76 | 83 | 86 | 87 | ); 88 | } 89 | }} 90 | 91 | )} 92 | 93 | 94 | ); 95 | }); 96 | 97 | export default Heap; 98 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/python-tutor/python-tutor.tsx: -------------------------------------------------------------------------------- 1 | import { For } from "solid-js"; 2 | import Arrow from "../arrow"; 3 | import { createName } from "../createName"; 4 | import Group from "../group"; 5 | import Ref from "../ref"; 6 | import { StackH } from "../stackh"; 7 | import withBluefish, { WithBluefishProps } from "../withBluefish"; 8 | import GlobalFrame from "./global-frame"; 9 | import Heap from "./heap"; 10 | import { StackSlot, HeapObject, Address } from "./types"; 11 | 12 | type PythonTutorProps = WithBluefishProps<{ 13 | stack: StackSlot[]; 14 | heap: HeapObject[]; 15 | heapArrangement: (Address | null)[][]; 16 | debug?: boolean; 17 | }>; 18 | 19 | export const PythonTutor = withBluefish((props: PythonTutorProps) => { 20 | const globalFrameName = createName("globalFrame"); 21 | const heapName = createName("heap"); 22 | 23 | // Maps object number to the ID of the corresponding heap object 24 | // This will help generate Arrows between objects 25 | const objectIdToComponentId = new Map(); 26 | props.heapArrangement.forEach((row, rowIndex) => { 27 | row.forEach((obj, colIndex) => { 28 | if (obj !== null) { 29 | objectIdToComponentId.set(obj, `row${rowIndex}_col${colIndex}`); 30 | } 31 | }); 32 | }); 33 | 34 | return ( 35 | ( 37 | <> 38 | {/* Make arrows from stack slots to heap objects */} 39 | 40 | {(stackSlot, stackSlotIndex) => 41 | typeof stackSlot.value === "object" && 42 | "type" in stackSlot.value && 43 | stackSlot.value.type === "pointer" ? ( 44 | 52 | 59 | 66 | 67 | ) : null 68 | } 69 | 70 | 71 | )} 72 | > 73 | 74 | 75 | 80 | 81 | 82 | ); 83 | }); 84 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/python-tutor/stack-slot.tsx: -------------------------------------------------------------------------------- 1 | import Group from "../group"; 2 | import Rect from "../rect"; 3 | import Align from "../align"; 4 | import Distribute from "../distribute"; 5 | import Ref from "../ref"; 6 | import Text from "../text"; 7 | import { createUniqueId } from "solid-js"; 8 | import { Id } from "../scenegraph"; 9 | import { Pointer } from "./types"; 10 | import withBluefish from "../withBluefish"; 11 | import { createName } from "../createName"; 12 | 13 | export type StackSlotProps = { 14 | name?: Id; 15 | variable: string; 16 | value: string | Pointer; 17 | }; 18 | 19 | export const StackSlot = withBluefish((props: StackSlotProps) => { 20 | const fontFamily = "verdana, arial, helvetica, sans-serif"; 21 | 22 | const boxName = createName("box"); 23 | const nameName = createName("name"); 24 | const valueName = createName("value"); 25 | 26 | return ( 27 | 28 | 29 | 30 | {props.variable} 31 | 32 | {/* TODO: if we only align or distribute on a single dimension, then their bounding boxes should be null or something along the unconstrained dimension... */} 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | {typeof props.value === "string" ? ( 51 | 52 | 53 | {props.value} 54 | 55 | 56 | 57 | ) : ( 58 | 59 | 60 | {""} 61 | 62 | 63 | 64 | )} 65 | 66 | ); 67 | }); 68 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/python-tutor/types.tsx: -------------------------------------------------------------------------------- 1 | export type Address = number; 2 | 3 | export type Pointer = { type: "pointer"; value: Address }; 4 | 5 | export const pointer = (value: Address): Pointer => ({ 6 | type: "pointer", 7 | value, 8 | }); 9 | 10 | export type Value = string | number | Pointer; 11 | 12 | export type StackSlot = { 13 | variable: string; 14 | value: Value; 15 | }; 16 | 17 | export const formatValue = (value: Value): string | number => { 18 | let formattedValue: string | number; 19 | if (typeof value === "string" || typeof value === "number") { 20 | formattedValue = `${value}`; 21 | } else { 22 | formattedValue = value.value; 23 | } 24 | return formattedValue; 25 | }; 26 | 27 | export const stackSlot = (variable: string, value: Value): StackSlot => { 28 | return { 29 | variable, 30 | value: value, 31 | }; 32 | }; 33 | 34 | export type Tuple = { type: "tuple"; values: Value[] }; 35 | 36 | export const tuple = (values: Value[]): Tuple => ({ type: "tuple", values }); 37 | 38 | export type HeapObject = Tuple; 39 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/rect.tsx: -------------------------------------------------------------------------------- 1 | import { JSX } from "solid-js/jsx-runtime"; 2 | import { Layout } from "./layout"; 3 | import { BBox, Id, Transform } from "./scenegraph"; 4 | import { splitProps } from "solid-js"; 5 | import withBluefish from "./withBluefish"; 6 | 7 | export type RectProps = JSX.RectSVGAttributes & { 8 | name: Id; 9 | x?: number; 10 | y?: number; 11 | width?: number; 12 | height?: number; 13 | }; 14 | 15 | export const Rect = withBluefish( 16 | (props: RectProps) => { 17 | const layout = () => { 18 | return { 19 | bbox: { 20 | left: 0, 21 | top: 0, 22 | width: props.width, 23 | height: props.height, 24 | }, 25 | transform: { 26 | translate: { 27 | x: props.x, 28 | y: props.y, 29 | }, 30 | }, 31 | }; 32 | }; 33 | 34 | const paint = (paintProps: { bbox: BBox; transform: Transform }) => { 35 | const [_, rest] = splitProps(props, [ 36 | "name", 37 | "x", 38 | "y", 39 | "width", 40 | "height", 41 | ]); 42 | 43 | return ( 44 | 56 | ); 57 | }; 58 | 59 | return ; 60 | }, 61 | { displayName: "Rect" } 62 | ); 63 | 64 | export default Rect; 65 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/ref.tsx: -------------------------------------------------------------------------------- 1 | import { onCleanup, useContext } from "solid-js"; 2 | import type { JSX } from "solid-js"; 3 | import { Id, UNSAFE_useScenegraph, ScenegraphElement } from "./scenegraph"; 4 | import withBluefish from "./withBluefish"; 5 | import { Name, Scope, ScopeContext } from "./createName"; 6 | import { useError } from "./errorContext"; 7 | import { produce } from "solid-js/store"; 8 | 9 | export type Selection = Id | [Id, ...Name[]]; 10 | export type NormalizedSelection = [Id, ...Name[]]; 11 | 12 | export type RefProps = { 13 | name: Id; 14 | select: Selection; 15 | }; 16 | 17 | export const normalizeSelection = (select: Selection): NormalizedSelection => { 18 | if (Array.isArray(select)) { 19 | return select; 20 | } else { 21 | return [select]; 22 | } 23 | }; 24 | 25 | // TODO: probably scopeIds and layoutIds should have different types... 26 | export const resolveSelection = ( 27 | scope: Scope, 28 | select: NormalizedSelection 29 | ): Id => { 30 | const [id, ...names] = select; 31 | 32 | let currId = id; 33 | 34 | if (!(id in scope)) { 35 | throw new Error( 36 | `Could not find ${id}. Available names: ${Object.keys(scope).join(", ")}` 37 | ); 38 | } 39 | 40 | for (const name of names) { 41 | const child = scope[currId].children[name]; 42 | if (child === undefined) { 43 | console.log(JSON.parse(JSON.stringify(scope))); 44 | throw new Error( 45 | `Could not find ${name} in ${currId}. Available names: ${Object.keys( 46 | scope[currId].children 47 | ).join(", ")}` 48 | ); 49 | } 50 | currId = child; 51 | } 52 | 53 | const layoutId = scope[currId].layoutNode; 54 | 55 | if (layoutId === undefined) { 56 | console.log(JSON.parse(JSON.stringify(scope))); 57 | throw new Error( 58 | `Could not find layout node for ${currId}. Available names: ${Object.keys( 59 | scope[currId].children 60 | ).join(", ")}` 61 | ); 62 | } 63 | 64 | return layoutId; 65 | }; 66 | 67 | export const Ref = withBluefish( 68 | (props: RefProps) => { 69 | const error = useError(); 70 | const [scope, setScope] = useContext(ScopeContext); 71 | const { createRef } = UNSAFE_useScenegraph(); 72 | 73 | const normalizedSelection = () => normalizeSelection(props.select); 74 | 75 | onCleanup(() => { 76 | // when the Ref node is destroyed, we need to clear any relevant scopes 77 | setScope( 78 | produce((scope) => { 79 | // filter out scopes that have this id as their layoutNode 80 | for (const key of Object.keys(scope) as Array) { 81 | if (scope[key].layoutNode === props.name) { 82 | delete scope[key]; 83 | } 84 | } 85 | }) 86 | ); 87 | }); 88 | 89 | // return <>; 90 | return { 91 | jsx: <>, 92 | layout: (parentId: Id | null) => { 93 | if (parentId === null) { 94 | throw new Error("Ref must be a child of a Layout"); 95 | } 96 | 97 | createRef( 98 | props.name, 99 | resolveSelection(scope, normalizedSelection()), 100 | parentId 101 | ); 102 | }, 103 | } satisfies ScenegraphElement as unknown as JSX.Element; 104 | }, 105 | { displayName: "Ref" } 106 | ); 107 | 108 | export default Ref; 109 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stack.tsx: -------------------------------------------------------------------------------- 1 | import { withBluefish } from "./withBluefish"; 2 | import { ParentProps, JSX, mergeProps, createMemo } from "solid-js"; 3 | import { StackArgs, stackLayout } from "./stackLayout"; 4 | import { BBox, Transform, ChildNode } from "./scenegraph"; 5 | import Layout from "./layout"; 6 | 7 | export type StackProps = ParentProps; 8 | 9 | export const Stack = withBluefish( 10 | (props: StackProps) => { 11 | // if both total and spacing are undefined, default spacing to 10 12 | const spacing = createMemo(() => { 13 | if (props.total === undefined && props.spacing === undefined) return 10; 14 | return props.spacing; 15 | }); 16 | 17 | const alignment = createMemo(() => { 18 | if (props.direction === "vertical") return "centerX"; 19 | if (props.direction === "horizontal") return "centerY"; 20 | return undefined; 21 | }); 22 | 23 | props = mergeProps( 24 | { 25 | get spacing() { 26 | return spacing(); 27 | }, 28 | get alignment() { 29 | return alignment(); 30 | }, 31 | }, 32 | props 33 | ); 34 | 35 | const layout = (childNodes: ChildNode[]) => { 36 | return stackLayout(props)(childNodes); 37 | }; 38 | 39 | const paint = (paintProps: { 40 | bbox: BBox; 41 | transform: Transform; 42 | children: JSX.Element; 43 | }) => { 44 | return ( 45 | 50 | {paintProps.children} 51 | 52 | ); 53 | }; 54 | 55 | return ( 56 | 57 | {props.children} 58 | 59 | ); 60 | }, 61 | { displayName: "Stack" } 62 | ); 63 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stackh.tsx: -------------------------------------------------------------------------------- 1 | import { ParentProps, JSX, mergeProps } from "solid-js"; 2 | import { StackArgs, stackLayout } from "./stackLayout"; 3 | import withBluefish from "./withBluefish"; 4 | import Layout from "./layout"; 5 | import { BBox, Transform, ChildNode } from "./scenegraph"; 6 | import { AlignmentVertical } from "./align"; 7 | import { Stack } from "./stack"; 8 | 9 | export type StackHProps = ParentProps< 10 | Omit & { alignment?: AlignmentVertical } 11 | >; 12 | 13 | export const StackH = withBluefish( 14 | (props: StackHProps) => { 15 | const stackProps = mergeProps( 16 | { 17 | direction: "horizontal" as const, 18 | alignment: "centerY" as const, 19 | }, 20 | props 21 | ); 22 | 23 | return ; 24 | }, 25 | { displayName: "StackH" } 26 | ); 27 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stackv.tsx: -------------------------------------------------------------------------------- 1 | import { ParentProps, JSX, mergeProps } from "solid-js"; 2 | import { StackArgs, stackLayout } from "./stackLayout"; 3 | import withBluefish from "./withBluefish"; 4 | import Layout from "./layout"; 5 | import { BBox, Transform, ChildNode } from "./scenegraph"; 6 | import { AlignmentHorizontal } from "./align"; 7 | import { Stack } from "./stack"; 8 | 9 | export type StackVProps = ParentProps< 10 | Omit & { alignment?: AlignmentHorizontal } 11 | >; 12 | 13 | export const StackV = withBluefish( 14 | (props: StackVProps) => { 15 | const stackProps = mergeProps( 16 | { 17 | direction: "vertical" as const, 18 | alignment: "centerX" as const, 19 | }, 20 | props 21 | ); 22 | 23 | return ; 24 | }, 25 | { displayName: "StackV" } 26 | ); 27 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/4.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from "storybook-solidjs"; 2 | import Bluefish from "../bluefish"; 3 | import Group from "../group"; 4 | import Rect from "../rect"; 5 | import Distribute from "../distribute"; 6 | import Align from "../align"; 7 | import Ref from "../ref"; 8 | 9 | const meta: Meta = { 10 | title: "Regression/#4", 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const App: Story = { 17 | name: "#4", 18 | render: () => { 19 | return ( 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | ); 34 | }, 35 | }; 36 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/41.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from "storybook-solidjs"; 2 | import Bluefish from "../bluefish"; 3 | import Group from "../group"; 4 | import Rect from "../rect"; 5 | import Distribute from "../distribute"; 6 | import Align from "../align"; 7 | import Ref from "../ref"; 8 | import { createSignal } from "solid-js"; 9 | import Background from "../background"; 10 | import { StackH } from "../stackh"; 11 | import { Text } from "../text"; 12 | import Circle from "../circle"; 13 | import Arrow from "../arrow"; 14 | import { within, userEvent } from "@storybook/test"; 15 | 16 | const meta: Meta = { 17 | title: "Regression/#41", 18 | }; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | export const App: Story = { 24 | name: "#41", 25 | play: async ({ canvasElement }) => { 26 | const canvas = within(canvasElement); 27 | 28 | await userEvent.click(canvas.getByRole("button")); 29 | await userEvent.click(canvas.getByRole("button")); 30 | await userEvent.click(canvas.getByRole("button")); 31 | await userEvent.click(canvas.getByRole("button")); 32 | await userEvent.click(canvas.getByRole("button")); 33 | }, 34 | render: () => { 35 | const [planet, setPlanet] = createSignal("mercury"); 36 | 37 | const handler = () => { 38 | if (planet() == "mercury") { 39 | setPlanet("venus"); 40 | } else if (planet() == "venus") { 41 | setPlanet("earth"); 42 | } else if (planet() == "earth") { 43 | setPlanet("mars"); 44 | } else { 45 | setPlanet("mercury"); 46 | } 47 | }; 48 | 49 | return ( 50 | <> 51 | 52 |
Current planet: {planet()}
53 |
54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | {planet()} 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 |
77 | 78 | ); 79 | }, 80 | }; 81 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/44.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from "storybook-solidjs"; 2 | import Bluefish from "../bluefish"; 3 | import Group from "../group"; 4 | import Rect from "../rect"; 5 | import Distribute from "../distribute"; 6 | import Align from "../align"; 7 | import Ref from "../ref"; 8 | import { createSignal } from "solid-js"; 9 | import Background from "../background"; 10 | import { StackH } from "../stackh"; 11 | import { Text } from "../text"; 12 | import Circle from "../circle"; 13 | import Arrow from "../arrow"; 14 | 15 | const meta: Meta = { 16 | title: "Regression/#44", 17 | }; 18 | 19 | export default meta; 20 | type Story = StoryObj; 21 | 22 | export const App: Story = { 23 | name: "#44", 24 | render: () => { 25 | return ( 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | }, 45 | }; 46 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/50.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from "storybook-solidjs"; 2 | import Bluefish from "../bluefish"; 3 | import Group from "../group"; 4 | import Rect from "../rect"; 5 | import Distribute from "../distribute"; 6 | import Align from "../align"; 7 | import Ref from "../ref"; 8 | import { For, createSignal } from "solid-js"; 9 | import Background from "../background"; 10 | import { StackH } from "../stackh"; 11 | import { Text } from "../text"; 12 | import Circle from "../circle"; 13 | import Arrow from "../arrow"; 14 | import { within, userEvent } from "@storybook/test"; 15 | 16 | const meta: Meta = { 17 | title: "Regression/#50", 18 | }; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | export const App: Story = { 24 | name: "#50", 25 | play: async ({ canvasElement }) => { 26 | const canvas = within(canvasElement); 27 | 28 | await userEvent.click(canvas.getByRole("button")); 29 | await userEvent.click(canvas.getByRole("button")); 30 | await userEvent.click(canvas.getByRole("button")); 31 | await userEvent.click(canvas.getByRole("button")); 32 | await userEvent.click(canvas.getByRole("button")); 33 | }, 34 | render: () => { 35 | const [count, setCount] = createSignal(0); 36 | 37 | return ( 38 | <> 39 | 42 |
43 | 44 | 45 | 46 | {(variable) => ( 47 | 48 | )} 49 | 50 | 51 | 52 |
53 | 54 | ); 55 | }, 56 | }; 57 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/6.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from "storybook-solidjs"; 2 | import Bluefish from "../bluefish"; 3 | import Group from "../group"; 4 | import Rect from "../rect"; 5 | import Distribute from "../distribute"; 6 | import Text from "../text"; 7 | import Align from "../align"; 8 | import Ref from "../ref"; 9 | 10 | const meta: Meta = { 11 | title: "Regression/#6", 12 | }; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | export const A: Story = { 18 | name: "#6/a", 19 | render: () => { 20 | return ( 21 | 22 | 23 | 24 | one 25 | two 26 | 27 | three 28 | 29 | 30 | ); 31 | }, 32 | }; 33 | 34 | export const B: Story = { 35 | name: "#6/b", 36 | render: () => { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/CarsPlot.stories.tsx: -------------------------------------------------------------------------------- 1 | import { scaleLinear } from "d3-scale"; 2 | import { nonNullData } from "../../examples/datasets/cars"; 3 | import { Bluefish, withBluefish } from ".."; 4 | import { Dot } from "../plot/dot"; 5 | import { Plot } from "../plot/plot"; 6 | import { Meta, StoryObj } from "storybook-solidjs"; 7 | 8 | const meta: Meta = { 9 | title: "Example/Plot/Cars", 10 | }; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const Cars: Story = { 16 | render: () => { 17 | return ( 18 | 19 | 22 | scaleLinear( 23 | [0, Math.max(...nonNullData.map((d) => +d.Horsepower))!], 24 | [0, 1000], 25 | ) 26 | } 27 | y={(dims: any) => 28 | scaleLinear( 29 | [0, Math.max(...nonNullData.map((d) => +d.Miles_per_Gallon))!], 30 | [500, 0], 31 | ) 32 | } 33 | color={() => () => "cornflowerblue"} 34 | > 35 | 36 | 37 | 38 | ); 39 | }, 40 | }; 41 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/IdInference.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from "storybook-solidjs"; 2 | import Bluefish from "../bluefish"; 3 | import Group from "../group"; 4 | import Rect from "../rect"; 5 | import Ref from "../ref"; 6 | import withBluefish from "../withBluefish"; 7 | import { StackH } from "../stackh"; 8 | import { StackV } from "../stackv"; 9 | 10 | const meta: Meta = { 11 | title: "Feat/Id Inference", 12 | }; 13 | 14 | export default meta; 15 | type Story = StoryObj; 16 | 17 | const CustomComponent = withBluefish(() => { 18 | return ; 19 | }); 20 | 21 | export const App: Story = { 22 | name: "Id Inference", 23 | render: () => { 24 | return ( 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | ); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/Math3ma.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import Align from "../align"; 3 | import Arrow from "../arrow"; 4 | import Background from "../background"; 5 | import Bluefish from "../bluefish"; 6 | import Circle from "../circle"; 7 | import Distribute from "../distribute"; 8 | import Group from "../group"; 9 | import Ref from "../ref"; 10 | import Text from "../text"; 11 | import Rect from "../rect"; 12 | import { Blob } from "../blob"; 13 | import { PaperScope, Path, Point, Size } from "paper/dist/paper-core"; 14 | import { StackV } from "../stackv"; 15 | import { StackH } from "../stackh"; 16 | 17 | const meta: Meta = { 18 | title: "Example/Math3ma", 19 | }; 20 | 21 | export default meta; 22 | type Story = StoryObj; 23 | 24 | const canvas = document.createElement("canvas"); 25 | const paperScope = new PaperScope(); 26 | paperScope.setup(canvas); 27 | const dims = { 28 | x: 50, 29 | y: 25, 30 | width: 200, 31 | height: 100, 32 | }; 33 | const myPath = new Path.Rectangle(new Point(dims.x, dims.y), new Size(dims.width, dims.height)); 34 | // const myPath = new Path(); 35 | // myPath.add(new Point(50, 75)); 36 | // myPath.add(new Point(50, 25)); 37 | // myPath.add(new Point(150, 25)); 38 | // myPath.add(new Point(150, 75)); 39 | myPath.insert(4, new Point(dims.x + dims.width / 2, dims.y + dims.height - (dims.height * 5) / 50)); 40 | 41 | const dims2 = { 42 | x: 50, 43 | y: 50, 44 | width: 100, 45 | height: 50, 46 | }; 47 | const myPath2 = new Path.Rectangle(new Point(dims2.x, dims2.y), new Size(dims2.width, dims2.height)); 48 | myPath2.insert(2, new Point(dims2.x + dims2.width / 2, dims2.y + (dims2.height * 5) / 50)); 49 | myPath2.insert(5, new Point(dims2.x + dims2.width / 2, dims2.y + dims2.height - (dims2.height * 5) / 50)); 50 | 51 | export const Math3ma: Story = { 52 | name: "Math3ma", 53 | render: () => { 54 | return ( 55 | 56 | } 62 | > 63 | 64 | } 66 | > 67 | Borel sets 68 | 69 | 70 | x 71 | 72 | 73 | 74 | 75 | 76 | 77 | {"f^{-1}(N) lives here!"} 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | ); 86 | }, 87 | }; 88 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/Molecule.stories.tsx: -------------------------------------------------------------------------------- 1 | // Integrates with SmilesDrawer: https://pubs.acs.org/doi/10.1021/acs.jcim.7b00425 2 | // SmilesDrawer GitHub Repository: https://github.com/reymond-group/smilesDrawer 3 | // SmilesDrawer Interactive Playground: https://smilesdrawer.surge.sh/ 4 | 5 | import type { Meta, StoryObj } from "storybook-solidjs"; 6 | import { Bluefish } from "../bluefish"; 7 | import { Molecule } from "../chemistry/molecule"; 8 | 9 | const meta: Meta = { 10 | title: "Example/Molecule", 11 | }; 12 | 13 | export default meta; 14 | type Story = StoryObj; 15 | 16 | export const AspirinMolecule: Story = { 17 | args: { 18 | id: "aspirin", 19 | chemicalFormula: "CC(OC1=C(C(=O)O)C=CC=C1)=O", 20 | ariaLabel: "Aspirin Molecule", 21 | }, 22 | render: (props) => ( 23 | 24 | 29 | 30 | ), 31 | }; 32 | 33 | export const NicotineMolecule: Story = { 34 | args: { 35 | id: "nicotine", 36 | chemicalFormula: "CN1CCCC1C2=CN=CC=C2", 37 | ariaLabel: "Nicotine Molecule", 38 | }, 39 | render: (props) => ( 40 | 41 | 46 | 47 | ), 48 | }; 49 | 50 | export const SucroseMolecule: Story = { 51 | args: { 52 | id: "sucrose", 53 | chemicalFormula: "C(C1C(C(C(C(O1)OC2(C(C(C(O2)CO)O)O)CO)O)O)O)O", 54 | ariaLabel: "Sucrose Molecule", 55 | }, 56 | render: (props) => ( 57 | 58 | 63 | 64 | ), 65 | }; 66 | 67 | export const PenicillinMolecule: Story = { 68 | args: { 69 | id: "Penicillin", 70 | chemicalFormula: "CC1(C(N2C(S1)C(C2=O)NC(=O)CC3=CC=CC=C3)C(=O)O)C", 71 | ariaLabel: "Penicillin Molecule", 72 | }, 73 | render: (props) => ( 74 | 75 | 80 | 81 | ), 82 | }; 83 | 84 | export const DiphenylEtherMolecule: Story = { 85 | args: { 86 | id: "diphenylether", 87 | chemicalFormula: "C1=CC=C(C=C1)OC2=CC=CC=C2", 88 | ariaLabel: "Diphenyl ether Molecule", 89 | }, 90 | render: (props) => ( 91 | 92 | 97 | 98 | ), 99 | }; 100 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/PythonTutor.stories.tsx: -------------------------------------------------------------------------------- 1 | // Examples created based on Python Tutor: https://pythontutor.com/ 2 | 3 | import type { Meta, StoryObj } from "storybook-solidjs"; 4 | import { Bluefish } from "../bluefish"; 5 | import { Address, HeapObject, StackSlot, pointer } from "../python-tutor/types"; 6 | import { PythonTutor } from "../python-tutor/python-tutor"; 7 | 8 | const meta: Meta = { 9 | title: "Example/PythonTutor", 10 | }; 11 | 12 | export default meta; 13 | type Story = StoryObj; 14 | 15 | export const PythonTutorExample: Story = { 16 | args: { 17 | stack: [ 18 | { variable: "c", value: pointer(0) }, 19 | { variable: "d", value: pointer(1) }, 20 | { variable: "x", value: "5" }, 21 | ], 22 | heap: [ 23 | { 24 | type: "tuple", 25 | values: ["1", pointer(1), pointer(2)], 26 | }, 27 | { type: "tuple", values: ["1", "4"] }, 28 | { type: "tuple", values: ["3", "10"] }, 29 | ], 30 | heapArrangement: [ 31 | [0, null, null], 32 | [null, 1, 2], 33 | ], 34 | }, 35 | render: (props) => ( 36 | 37 | 44 | 45 | ), 46 | }; 47 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/README.md: -------------------------------------------------------------------------------- 1 | # Adding a test 2 | 3 | Make a new file called `.stories.tsx`. 4 | 5 | ```tsx 6 | const meta: Meta = { 7 | title: "Example//", 8 | }; 9 | 10 | export default meta; 11 | type Story = StoryObj; 12 | 13 | export const Cars: Story = { 14 | render: () => { 15 | return 16 | } 17 | } 18 | ``` 19 | 20 | These examples are checked for differences on every commit. 21 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/SimpleTree.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Bluefish } from "../bluefish"; 3 | import { For, createUniqueId, mergeProps } from "solid-js"; 4 | import withBluefish, { WithBluefishProps } from "../withBluefish"; 5 | import { Group } from "../group"; 6 | import { Rect } from "../rect"; 7 | import { Ref } from "../ref"; 8 | import { Text } from "../text"; 9 | import { StackV } from "../stackv"; 10 | import { StackH } from "../stackh"; 11 | import { Arrow } from "../arrow"; 12 | import { Align } from "../align"; 13 | import { createName } from "../createName"; 14 | 15 | const meta: Meta = { 16 | title: "Example/SimpleTree", 17 | }; 18 | 19 | export default meta; 20 | type Story = StoryObj; 21 | 22 | // Component to render Tree node 23 | type NodeProps = WithBluefishProps<{ 24 | value: string; 25 | }>; 26 | 27 | const Node = withBluefish((props: NodeProps) => { 28 | props = mergeProps({ value: "?" }, props); 29 | const valueName = createName("value"); 30 | const outlineName = createName("outline"); 31 | 32 | return ( 33 | 34 | 43 | 44 | {props.value} 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | ); 56 | }); 57 | 58 | // Component to render Tree 59 | type TreeData = { 60 | value?: string; 61 | subtrees?: TreeData[]; 62 | }; 63 | 64 | type TreeProps = WithBluefishProps; 65 | 66 | const Tree = withBluefish((props: TreeProps) => { 67 | // merge props.data with default 68 | props = mergeProps( 69 | { 70 | subtrees: [] as TreeData[], 71 | nodeId: createUniqueId(), 72 | value: "?", 73 | }, 74 | props, 75 | ); 76 | 77 | // merge props with default 78 | props = mergeProps({ id: createUniqueId() }, props); 79 | 80 | const nodeName = createName("node"); 81 | 82 | // TODO: could probably get even fancier with this by turning objects into nested names somehow 83 | const subtreeNames = (props.subtrees ?? []).map((_, i) => 84 | createName(`subtree${i}`), 85 | ); 86 | 87 | return ( 88 | 89 | 90 | 91 | {props.subtrees?.length ? ( 92 | <> 93 | 94 | 95 | 96 | 97 | {(child, i) => } 98 | 99 | 100 | 101 | 102 | {(child, i) => ( 103 | 104 | 105 | 106 | 107 | )} 108 | 109 | 110 | ) : null} 111 | 112 | ); 113 | }); 114 | 115 | export const TwoLevelTree: Story = { 116 | args: { 117 | value: "A", 118 | subtrees: [{ value: "B" }, { value: "C" }], 119 | }, 120 | render: (props) => ( 121 | 122 | 123 | 124 | ), 125 | }; 126 | 127 | export const ThreeLevelTree: Story = { 128 | args: { 129 | value: "A", 130 | subtrees: [ 131 | { 132 | value: "B", 133 | subtrees: [{ value: "D" }, { value: "E" }], 134 | }, 135 | { 136 | value: "C", 137 | subtrees: [{ value: "F" }, { value: "G" }], 138 | }, 139 | ], 140 | }, 141 | render: (props) => ( 142 | 143 | 144 | 145 | ), 146 | }; 147 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/VennDiagram.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Bluefish } from "../bluefish"; 3 | import withBluefish, { WithBluefishProps } from "../withBluefish"; 4 | import Group from "../group"; 5 | import Text from "../text"; 6 | import { StackH } from "../stackh"; 7 | import Background from "../background"; 8 | import Circle from "../circle"; 9 | import Ref from "../ref"; 10 | 11 | const meta: Meta = { 12 | title: "Example/VennDiagram", 13 | }; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | // Adding props here to allow for additions later 19 | type VennDiagramProps = WithBluefishProps<{}>; 20 | 21 | const VennDiagram = withBluefish((props: VennDiagramProps) => { 22 | return ( 23 | ( 25 | <> 26 | }> 27 | 28 | 29 | 30 | }> 31 | 32 | 33 | 34 | 35 | )} 36 | > 37 | 38 | 39 | 40 | 41 | 42 | 43 | ); 44 | }); 45 | 46 | export const VennDiagramExample: Story = { 47 | args: {}, 48 | render: (props) => ( 49 | 50 | 51 | 52 | ), 53 | }; 54 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Align.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Align } from "../../align"; 3 | import { Bluefish } from "../../bluefish"; 4 | import { Rect } from "../../rect"; 5 | import Group from "../../group"; 6 | import Distribute from "../../distribute"; 7 | import Ref from "../../ref"; 8 | import { StackH } from "../../stackh"; 9 | import Background from "../../background"; 10 | 11 | /** 12 | * Bluefish's `Align` component contains many different options for aligning its children components. Taking in any number of components as children, 13 | * the alignments can be split into: 14 | * - Alignments that just set Y positions: `top`, `centerY`, `bottom`, 15 | * - Alignments that just set X positions: `left`, `centerX`, `right`, 16 | * - and Alignments that set both X and Y positions: `center`, `topLeft`, `topCenter`, `topRight`, `centerLeft`, `centerRight`, `bottomLeft`, `bottomMiddle`, `bottomRight` 17 | */ 18 | const meta: Meta = { 19 | title: "Components/Align", 20 | component: Align, 21 | tags: ["autodocs"], 22 | argTypes: { 23 | alignment1: { 24 | control: "radio", 25 | options: ["top", "centerY", "bottom"], 26 | }, 27 | alignment2: { 28 | control: "radio", 29 | options: ["left", "centerX", "right"], 30 | }, 31 | alignment3: { 32 | control: "radio", 33 | options: [ 34 | "center", 35 | "topLeft", 36 | "topCenter", 37 | "topRight", 38 | "centerLeft", 39 | "centerRight", 40 | "bottomLeft", 41 | "bottomMiddle", 42 | "bottomRight", 43 | ], 44 | }, 45 | }, 46 | }; 47 | 48 | export default meta; 49 | type Story = StoryObj; 50 | 51 | export const AlignComponent: Story = { 52 | args: { 53 | alignment1: "centerY", 54 | alignment2: "right", 55 | alignment3: "center", 56 | }, 57 | render: (props) => { 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | ); 94 | }, 95 | }; 96 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Arrow.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Arrow } from "../../arrow"; 3 | import { Bluefish } from "../../bluefish"; 4 | import { Rect } from "../../rect"; 5 | /** 6 | * Bluefish's `Arrow` component creates an arrow between two components. It uses [perfect-arrows'](https://github.com/steveruizok/perfect-arrows#options) parameters 7 | * in order to do so. 8 | */ 9 | const meta: Meta = { 10 | title: "Components/Arrow", 11 | component: Arrow, 12 | tags: ["autodocs"], 13 | argTypes: { 14 | stretch: { 15 | control: { 16 | step: 0.1, 17 | }, 18 | }, 19 | bow: { 20 | control: { 21 | step: 0.1, 22 | }, 23 | }, 24 | }, 25 | }; 26 | 27 | export default meta; 28 | type Story = StoryObj; 29 | 30 | export const ArrowComponent: Story = { 31 | args: { 32 | padStart: 5, 33 | padEnd: 10, 34 | bow: 0, 35 | flip: false, 36 | stretch: 0.1, 37 | }, 38 | render: (props) => { 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | ); 47 | }, 48 | }; 49 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Background.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Background } from "../../background"; 3 | import { Bluefish } from "../../bluefish"; 4 | import { Text } from "../../text"; 5 | import { StackH } from "../../stackh"; 6 | import Rect from "../../rect"; 7 | 8 | /** 9 | * Bluefish's `Background` component creates a background around a set of children elements. The background element's sizing will be inferred from that 10 | * of the children components. 11 | * 12 | * It takes in the following params: 13 | * - `background`: What the background should be. Should be of type `(() => JSX.Element) | undefined`. Defaults to a rectangle 14 | * with no fill and black borders 15 | * - `padding`: How much padding should be placed around the children elements 16 | */ 17 | const meta: Meta = { 18 | title: "Components/Background", 19 | component: Background, 20 | tags: ["autodocs"], 21 | }; 22 | 23 | export default meta; 24 | type Story = StoryObj; 25 | 26 | export const BackgroundComponent: Story = { 27 | args: { 28 | padding: 20, 29 | }, 30 | render: (props) => { 31 | return ( 32 | 33 | { 36 | return ; 37 | }} 38 | > 39 | I have a background! 40 | 41 | 42 | ); 43 | }, 44 | }; 45 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Circle.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from "storybook-solidjs"; 2 | import Bluefish from "../../bluefish"; 3 | import Group from "../../group"; 4 | import Circle from "../../circle"; 5 | 6 | const meta: Meta = { 7 | title: "Components/Circle", 8 | component: Circle, 9 | tags: ["autodocs"], 10 | argTypes: { 11 | fill: { control: "color" }, 12 | stroke: { control: "color" }, 13 | }, 14 | }; 15 | 16 | export default meta; 17 | type Story = StoryObj; 18 | 19 | /** 20 | * Creates a circle. Takes [SVG's Circle Element's Attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/circle#attributes) 21 | * as parameters. 22 | */ 23 | export const CircleComponent: Story = { 24 | args: { 25 | r: 15, 26 | cx: 20, 27 | cy: 20, 28 | fill: "red", 29 | "stroke-width": 3, 30 | stroke: "black", 31 | }, 32 | render: (props) => { 33 | return ( 34 | 35 | 36 | 37 | ); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Distribute.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Distribute } from "../../distribute"; 3 | import { Bluefish } from "../../bluefish"; 4 | import Rect from "../../rect"; 5 | 6 | /** 7 | * Bluefish's `Distribute` component distributes its children across one axis. Takes parameters: 8 | * - `direction`: either `horizontal` or `vertical`, determines which axis to distribute its children 9 | * - `spacing`: determines the amount of space to put between each child 10 | * 11 | * Each child of a `Distribute` should be positioned in its other axis e.g. with an `Align` or 12 | * another `Distribute`. 13 | */ 14 | const meta: Meta = { 15 | title: "Components/Distribute", 16 | component: Distribute, 17 | tags: ["autodocs"], 18 | }; 19 | 20 | export default meta; 21 | type Story = StoryObj; 22 | 23 | /** 24 | * Using `direction` = `horizontal` 25 | */ 26 | export const HorizontalDistribution: Story = { 27 | args: { 28 | spacing: 20, 29 | }, 30 | render: (props) => { 31 | return ( 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | ); 40 | }, 41 | }; 42 | 43 | /** 44 | * Using `direction` = `vertical` 45 | */ 46 | export const VerticalDistribution: Story = { 47 | args: { 48 | spacing: 20, 49 | }, 50 | render: (props) => { 51 | return ( 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | ); 60 | }, 61 | }; 62 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Ellipse.stories.tsx: -------------------------------------------------------------------------------- 1 | import { Meta, StoryObj } from "storybook-solidjs"; 2 | import Bluefish from "../../bluefish"; 3 | import Ellipse from "../../ellipse"; 4 | 5 | const meta: Meta = { 6 | title: "Components/Ellipse", 7 | component: Ellipse, 8 | tags: ["autodocs"], 9 | argTypes: { 10 | fill: { control: "color" }, 11 | stroke: { control: "color" }, 12 | }, 13 | }; 14 | 15 | export default meta; 16 | type Story = StoryObj; 17 | 18 | /** 19 | * Creates a Ellipse. Takes [SVG's Ellipse Element Attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/ellipse#attributes) 20 | * as parameters. 21 | */ 22 | export const EllipseComponent: Story = { 23 | args: { 24 | rx: 15, 25 | ry: 20, 26 | cx: 20, 27 | cy: 20, 28 | fill: "red", 29 | "stroke-width": 3, 30 | stroke: "black", 31 | }, 32 | render: (props) => { 33 | return ( 34 | 35 | 36 | 37 | ); 38 | }, 39 | }; 40 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/GraphLayered.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Rect } from "../../rect"; 3 | import { Bluefish } from "../../bluefish"; 4 | import Arrow from "../../arrow"; 5 | import Ref from "../../ref"; 6 | import { GraphLayered, Node, Edge } from "../../graphLayered"; 7 | 8 | /** 9 | * Creates a layered graph using [dagre](https://github.com/dagrejs/dagre). The direction of the 10 | * graph can be specified as `left-right`, `right-left`, `top-bottom`, or `bottom-top`. This 11 | * component is best used for directed acyclic graphs (DAGs). 12 | * 13 | * Edges are specified using the `edges` prop. The names used in this prop are read off the children. 14 | */ 15 | const meta: Meta = { 16 | title: "Components/GraphLayered", 17 | component: GraphLayered, 18 | tags: ["autodocs"], 19 | argTypes: { 20 | direction: { 21 | control: "radio", 22 | options: ["left-right", "right-left", "top-bottom", "bottom-top"], 23 | }, 24 | }, 25 | }; 26 | 27 | export default meta; 28 | type Story = StoryObj; 29 | 30 | export const FirstStory: Story = { 31 | args: { 32 | direction: "left-right", 33 | }, 34 | render: (props) => ( 35 | 36 | 37 | 38 | 45 | 46 | 47 | 54 | 55 | 56 | 63 | 64 | 65 | 72 | 73 | 74 | 81 | 82 | 83 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | ), 119 | }; 120 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Image.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Image } from "../../image"; 3 | import { Bluefish } from "../../bluefish"; 4 | 5 | /** 6 | * Creates an image. Takes [SVGImageElement attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/image#attributes) 7 | * as parameters. Currently the image's width and height must be specified either by the component 8 | * or through a `Ref` component. They cannot be inferred from the intrinsic dimensions of the image. 9 | */ 10 | const meta: Meta = { 11 | title: "Components/Image", 12 | component: Image, 13 | tags: ["autodocs"], 14 | argTypes: {}, 15 | }; 16 | 17 | export default meta; 18 | type Story = StoryObj; 19 | 20 | export const FirstStory: Story = { 21 | args: { 22 | width: 100, 23 | height: 100, 24 | href: "https://bluefishjs.org/bluefish-logo.png", 25 | }, 26 | render: (props) => ( 27 | 28 | 29 | 30 | ), 31 | }; 32 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Line.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Line } from "../../line"; 3 | import { Bluefish } from "../../bluefish"; 4 | import { Rect } from "../../rect"; 5 | import { Ref } from "../../ref"; 6 | /** 7 | * Bluefish's `Line` relation creates a line between two components. The first child component of the `Line` is the **source**, 8 | * which dictates the start point of the line. The second child component of the `Line` is the **target**, which dictates the end point of the line. 9 | */ 10 | const meta: Meta = { 11 | title: "Components/Line", 12 | component: Line, 13 | tags: ["autodocs"], 14 | argTypes: { 15 | "stroke-dasharray": { 16 | description: 17 | "The specification of dashes and gaps for the line. See [here](https://developer.mozilla.org/en-US/docs/Web/SVG/Attribute/stroke-dasharray) for more details.", 18 | }, 19 | "stroke-width": { 20 | description: "The width of the line.", 21 | control: { 22 | type: "number", 23 | step: 1, 24 | }, 25 | }, 26 | stroke: { 27 | description: "The color of the line.", 28 | control: { 29 | type: "text", 30 | }, 31 | }, 32 | 33 | source: { 34 | description: 35 | "Either an array of two numbers, `[a, b]`, between 0 and 1, or `undefined`. If an array is specified, the start point of the line's x-coordinate \ 36 | is a linear interpolation between the left and right edges of the source box. For example, if `a` is 0.25, then the \ 37 | x-coordinate is a quarter of the way from the left edge. Similarly, its y-coordinate is a linear interpolation between \ 38 | the top and bottom edges of the source box. For example, if `b` is 0.25, then the y-coordinate is a quarter of the way from the top edge.\ 39 | If `undefined`, the start point of the line is inferred.", 40 | control: { 41 | type: "array" || "undefined", 42 | }, 43 | }, 44 | target: { 45 | description: 46 | "Either an array of two numbers, `[a, b]`, between 0 and 1, or `undefined`. If an array is specified, the end point of the line's x-coordinate \ 47 | is a linear interpolation between the left and right edges of the source box. For example, if `a` is 0.25, then the \ 48 | x-coordinate is a quarter of the way from the left edge. Similarly, its y-coordinate is a linear interpolation between \ 49 | the top and bottom edges of the source box. For example, if `b` is 0.25, then the y-coordinate is a quarter of the way from the top edge.\ 50 | If `undefined`, the end point of the line is inferred.", 51 | control: { 52 | type: "array" || "undefined", 53 | }, 54 | }, 55 | }, 56 | }; 57 | 58 | export default meta; 59 | type Story = StoryObj; 60 | 61 | export const LineComponent: Story = { 62 | args: { 63 | "stroke-width": 3, 64 | "stroke-dasharray": "0", 65 | stroke: "black", 66 | source: [1, 0.5], 67 | target: [0, 0.5], 68 | }, 69 | render: (props) => { 70 | return ( 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | ); 80 | }, 81 | }; 82 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Rect.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Rect } from "../../rect"; 3 | import { Bluefish } from "../../bluefish"; 4 | 5 | /** 6 | * Creates a rectangle. Takes [SVGRectElement attributes](https://developer.mozilla.org/en-US/docs/Web/SVG/Element/rect#attributes) 7 | * as parameters. 8 | */ 9 | const meta: Meta = { 10 | title: "Components/Rect", 11 | component: Rect, 12 | tags: ["autodocs"], 13 | argTypes: { 14 | fill: { control: "color" }, 15 | }, 16 | }; 17 | 18 | export default meta; 19 | type Story = StoryObj; 20 | 21 | export const FirstStory: Story = { 22 | args: { 23 | width: 300, 24 | height: 200, 25 | x: 30, 26 | y: 30, 27 | fill: "red", 28 | }, 29 | render: (props) => ( 30 | 31 | 32 | 33 | ), 34 | }; 35 | 36 | export const SecondStory: Story = { 37 | args: { 38 | width: 100, 39 | height: 100, 40 | x: 30, 41 | y: 30, 42 | fill: "steelblue", 43 | }, 44 | render: (props) => ( 45 | 46 | 47 | 48 | ), 49 | }; 50 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/Stack.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { Stack } from "../../stack"; 3 | import { Bluefish } from "../../bluefish"; 4 | import Rect from "../../rect"; 5 | 6 | /** 7 | * `Stack` combines `StackH` and `StackV` into a single component. It takes parameters: 8 | * - `direction`: either `horizontal` or `vertical`, determines which axis to stack its children 9 | * - `alignment`: determines how to align the children in the other axis. This should be a 1D align 10 | * like in `Align`. 11 | * - `spacing`: determines the amount of space to put between each child 12 | * - `total`: determines the total size of the stack in the stacking axis 13 | * 14 | * Spacing/Total Behavior: 15 | * - If only `spacing` is specified, each child will be placed `spacing` apart. 16 | * - If only `total` is specified, this spacing will be divided evenly among the children. 17 | * - If both `spacing` and `total` are specified, the children will be resized to fit the total 18 | * size with `spacing` between them. 19 | */ 20 | const meta: Meta = { 21 | title: "Components/Stack", 22 | component: Stack, 23 | tags: ["autodocs"], 24 | argTypes: { 25 | alignment: { control: "radio", options: ["left", "centerX", "right"] }, 26 | }, 27 | }; 28 | 29 | export default meta; 30 | type Story = StoryObj; 31 | 32 | export const Horizontal: Story = { 33 | args: { 34 | spacing: 20, 35 | alignment: "centerY", 36 | direction: "horizontal", 37 | }, 38 | render: (props) => { 39 | return ( 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | ); 48 | }, 49 | }; 50 | 51 | export const Vertical: Story = { 52 | args: { 53 | spacing: 20, 54 | alignment: "centerX", 55 | direction: "vertical", 56 | }, 57 | render: (props) => { 58 | return ( 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | ); 67 | }, 68 | }; 69 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/StackH.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { StackH } from "../../stackh"; 3 | import { Bluefish } from "../../bluefish"; 4 | import Rect from "../../rect"; 5 | 6 | /** 7 | * `StackH` stacks its children horizontally. It takes parameters: 8 | * - `alignment`: determines how to align the children vertically. This should be a 1D vertical 9 | * alignment: `top`, `centerY`, or `bottom`. 10 | * - `spacing`: determines the amount of space to put between each child 11 | * - `total`: determines the total size of the stack in the stacking axis 12 | * 13 | * Spacing/Total Behavior: 14 | * - If only `spacing` is specified, each child will be placed `spacing` apart. 15 | * - If only `total` is specified, this spacing will be divided evenly among the children. 16 | * - If both `spacing` and `total` are specified, the children will be resized to fit the total 17 | * size with `spacing` between them. 18 | */ 19 | const meta: Meta = { 20 | title: "Components/StackH", 21 | component: StackH, 22 | tags: ["autodocs"], 23 | argTypes: { 24 | alignment: { control: "radio", options: ["top", "centerY", "bottom"] }, 25 | }, 26 | }; 27 | 28 | export default meta; 29 | type Story = StoryObj; 30 | 31 | export const DefinedSpacing: Story = { 32 | args: { 33 | spacing: 20, 34 | alignment: "centerY", 35 | }, 36 | render: (props) => { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }, 47 | }; 48 | 49 | export const TotalSpacing: Story = { 50 | args: { 51 | total: 110, 52 | alignment: "centerY", 53 | }, 54 | render: (props) => { 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | }, 65 | }; 66 | 67 | export const TotalAndSpacing: Story = { 68 | args: { 69 | spacing: 20, 70 | total: 500, 71 | alignment: "centerY", 72 | }, 73 | render: (props) => { 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/stories/components/StackV.stories.tsx: -------------------------------------------------------------------------------- 1 | import type { Meta, StoryObj } from "storybook-solidjs"; 2 | import { StackV } from "../../stackv"; 3 | import { Bluefish } from "../../bluefish"; 4 | import Rect from "../../rect"; 5 | 6 | /** 7 | * `StackV` stacks its children vertically. It takes parameters: 8 | * - `alignment`: determines how to align the children horizontally. This should be a 1D horizontal 9 | * alignment: `left`, `centerX`, or `right`. 10 | * - `spacing`: determines the amount of space to put between each child 11 | * - `total`: determines the total size of the stack in the stacking axis 12 | * 13 | * Spacing/Total Behavior: 14 | * - If only `spacing` is specified, each child will be placed `spacing` apart. 15 | * - If only `total` is specified, this spacing will be divided evenly among the children. 16 | * - If both `spacing` and `total` are specified, the children will be resized to fit the total 17 | * size with `spacing` between them. 18 | */ 19 | const meta: Meta = { 20 | title: "Components/StackV", 21 | component: StackV, 22 | tags: ["autodocs"], 23 | argTypes: { 24 | alignment: { control: "radio", options: ["left", "centerX", "right"] }, 25 | }, 26 | }; 27 | 28 | export default meta; 29 | type Story = StoryObj; 30 | 31 | export const DefinedSpacing: Story = { 32 | args: { 33 | spacing: 20, 34 | alignment: "centerX", 35 | }, 36 | render: (props) => { 37 | return ( 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | ); 46 | }, 47 | }; 48 | 49 | export const TotalSpacing: Story = { 50 | args: { 51 | total: 110, 52 | alignment: "centerX", 53 | }, 54 | render: (props) => { 55 | return ( 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | ); 64 | }, 65 | }; 66 | 67 | export const TotalAndSpacing: Story = { 68 | args: { 69 | spacing: 20, 70 | total: 100, 71 | alignment: "centerX", 72 | }, 73 | render: (props) => { 74 | return ( 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | ); 83 | }, 84 | }; 85 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/text.tsx: -------------------------------------------------------------------------------- 1 | import { For, Ref, mergeProps, splitProps } from "solid-js"; 2 | import { TextProps } from "./text/types"; 3 | import useText from "./text/useText"; 4 | import { BBox, Transform } from "./scenegraph"; 5 | import Layout from "./layout"; 6 | import computeBoundingBox from "./text/textBBox"; 7 | import withBluefish from "./withBluefish"; 8 | import splitAtDelimiters from "./text/splitAtDelimiters"; 9 | 10 | const SVG_STYLE = { overflow: "visible" }; 11 | 12 | export const Text = withBluefish( 13 | (props: TextProps) => { 14 | props = mergeProps( 15 | { 16 | // dx: 0, 17 | // dy: 0, 18 | "text-anchor": "start" as const, 19 | "vertical-anchor": "end" as const, 20 | "line-height": "1em", 21 | "cap-height": "0.71em", // Magic number from d3 22 | "font-family": "Alegreya Sans, sans-serif", 23 | "font-weight": 700, 24 | "font-size": "14", 25 | x: 0, 26 | y: 0, 27 | delimiters: [ 28 | { left: "$$", right: "$$", display: true }, 29 | { left: "\\(", right: "\\)", display: false }, 30 | { left: "$", right: "$", display: false }, 31 | { left: "\\[", right: "\\]", display: true }, 32 | ], 33 | }, 34 | props 35 | ); 36 | 37 | const [_, textProps] = splitProps(props, [ 38 | "name", 39 | "dx", 40 | "dy", 41 | "innerRef", 42 | "innerTextRef", 43 | "vertical-anchor", 44 | "angle", 45 | "line-height", 46 | "scaleToFit", 47 | "cap-height", 48 | "width", 49 | "delimiters", 50 | ]); 51 | 52 | // const textAndMathRegions = () => 53 | // splitAtDelimiters( 54 | // props.children !== undefined ? `${props.children}` : "", 55 | // props.delimiters! 56 | // ); 57 | 58 | const { wordsByLines, startDy, transform } = useText(props); 59 | 60 | const mergedProps = mergeProps(props, { 61 | get wordsByLines() { 62 | return wordsByLines(); 63 | }, 64 | get startDy() { 65 | return startDy(); 66 | }, 67 | get transform() { 68 | return transform(); 69 | }, 70 | }); 71 | 72 | const layout = () => { 73 | const bbox = computeBoundingBox(mergedProps); 74 | 75 | return { 76 | bbox: { 77 | left: bbox.x, 78 | top: bbox.y, 79 | width: bbox.width, 80 | height: bbox.height, 81 | }, 82 | transform: { 83 | translate: { 84 | x: props.dx !== undefined ? parseFloat(`${props.dx}`) : undefined, 85 | y: props.dy !== undefined ? parseFloat(`${props.dy}`) : undefined, 86 | }, 87 | }, 88 | }; 89 | }; 90 | 91 | const paint = (paintProps: { bbox: BBox; transform: Transform }) => { 92 | return ( 93 | 98 | {wordsByLines().length > 0 ? ( 99 | 104 | 105 | {(line, index) => ( 106 | 111 | {line.words.join(" ")} 112 | 113 | )} 114 | 115 | 116 | ) : null} 117 | 118 | ); 119 | }; 120 | 121 | return ; 122 | }, 123 | { displayName: "Text" } 124 | ); 125 | 126 | export default Text; 127 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/text/README.md: -------------------------------------------------------------------------------- 1 | This text component is based very closely on visx's text component. 2 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/text/getStringDims.ts: -------------------------------------------------------------------------------- 1 | import memoize from "lodash/memoize"; 2 | import { TextProps } from "./types"; 3 | 4 | const MEASUREMENT_ELEMENT_ID = "__react_svg_text_measurement_id"; 5 | 6 | function getStringDims(str: string, style?: TextProps) { 7 | try { 8 | // Calculate length of each word to be used to determine number of words per line 9 | let textEl = document.getElementById( 10 | MEASUREMENT_ELEMENT_ID 11 | ) as SVGTextElement | null; 12 | if (!textEl) { 13 | const svg = document.createElementNS("http://www.w3.org/2000/svg", "svg"); 14 | svg.setAttribute("aria-hidden", "true"); 15 | svg.style.width = "0"; 16 | svg.style.height = "0"; 17 | svg.style.position = "absolute"; 18 | svg.style.top = "-100%"; 19 | svg.style.left = "-100%"; 20 | svg.style.padding = "0"; 21 | svg.style.margin = "0"; 22 | textEl = document.createElementNS("http://www.w3.org/2000/svg", "text"); 23 | textEl.setAttribute("id", MEASUREMENT_ELEMENT_ID); 24 | svg.appendChild(textEl); 25 | document.body.appendChild(svg); 26 | } 27 | 28 | Object.assign(textEl.style, style); 29 | textEl.textContent = str; 30 | const width = textEl.getComputedTextLength(); 31 | 32 | return { 33 | width, 34 | height: style !== undefined ? parseFloat(`${style["font-size"]}`) : 0, 35 | }; 36 | } catch (e) { 37 | console.error(e); 38 | return null; 39 | } 40 | } 41 | 42 | export default memoize(getStringDims, (str, style) => { 43 | if (!style) return `${str}_`; 44 | const { children, name, ...rest } = style; 45 | return `${str}_${JSON.stringify(rest)}`; 46 | }); 47 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/text/reduce-css-calc.d.ts: -------------------------------------------------------------------------------- 1 | declare module "reduce-css-calc" { 2 | export default function (_: any): string; 3 | } 4 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/text/splitAtDelimiters.ts: -------------------------------------------------------------------------------- 1 | /* Adapted from https://github.com/harunurhan/react-latex-next/blob/master/src/splitAtDelimiters.ts, 2 | which was in turn adapted from /contrib/auto-render/splitAtDelimiters.js at github.com/Khan/KaTeX */ 3 | 4 | export type MathJaxData = { 5 | data: string; 6 | type: string; 7 | rawData?: string; 8 | display?: boolean; 9 | }; 10 | 11 | export type Delimiter = { 12 | right: string; 13 | left: string; 14 | display: boolean; 15 | }; 16 | 17 | function findEndOfMath( 18 | delimiterValue: string, 19 | text: string, 20 | startIndex: number 21 | ): number { 22 | let index = startIndex; 23 | let braceLevel = 0; 24 | 25 | const delimLength = delimiterValue.length; 26 | 27 | while (index < text.length) { 28 | const character = text[index]; 29 | 30 | if ( 31 | braceLevel <= 0 && 32 | text.slice(index, index + delimLength) === delimiterValue 33 | ) { 34 | return index; 35 | } else if (character === "\\") { 36 | index++; 37 | } else if (character === "{") { 38 | braceLevel++; 39 | } else if (character === "}") { 40 | braceLevel--; 41 | } 42 | 43 | index++; 44 | } 45 | 46 | return -1; 47 | } 48 | 49 | function escapeRegex(text: string): string { 50 | return text.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); 51 | } 52 | 53 | const amsRegex = /^\\begin{/; 54 | 55 | export default function splitAtDelimiters( 56 | text: string, 57 | delimiters: Delimiter[] 58 | ): MathJaxData[] { 59 | let index; 60 | const data = []; 61 | 62 | const regexLeft = new RegExp( 63 | "(" + delimiters.map((x) => escapeRegex(x.left)).join("|") + ")" 64 | ); 65 | 66 | while (true) { 67 | index = text.search(regexLeft); 68 | if (index === -1) { 69 | break; 70 | } 71 | if (index > 0) { 72 | data.push({ 73 | type: "text", 74 | data: text.slice(0, index), 75 | }); 76 | text = text.slice(index); // now text starts with delimiter 77 | } 78 | // ... so this always succeeds: 79 | const i = delimiters.findIndex((delim) => text.startsWith(delim.left)); 80 | index = findEndOfMath(delimiters[i].right, text, delimiters[i].left.length); 81 | if (index === -1) { 82 | break; 83 | } 84 | const rawData = text.slice(0, index + delimiters[i].right.length); 85 | const math = amsRegex.test(rawData) 86 | ? rawData 87 | : text.slice(delimiters[i].left.length, index); 88 | data.push({ 89 | type: "math", 90 | data: math, 91 | rawData, 92 | display: delimiters[i].display, 93 | }); 94 | text = text.slice(index + delimiters[i].right.length); 95 | } 96 | 97 | if (text !== "") { 98 | data.push({ 99 | type: "text", 100 | data: text, 101 | }); 102 | } 103 | 104 | return data; 105 | } 106 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/text/textBBox.tsx: -------------------------------------------------------------------------------- 1 | import { createEffect } from "solid-js"; 2 | import { TextProps, WordsWithDims } from "./types"; 3 | import useText from "./useText"; 4 | 5 | type FnProps = TextProps & { 6 | wordsByLines: WordsWithDims[]; 7 | startDy: string; 8 | transform: string; 9 | }; 10 | 11 | function computeBoundingBox(props: FnProps): { 12 | x: number; 13 | y: number; 14 | width: number; 15 | height: number; 16 | } { 17 | // Compute width. This depends on the props.width, actual text width, and the scaleToFit prop. 18 | let width = () => { 19 | let width = props.width || 0; 20 | if (props.wordsByLines.length > 0) { 21 | const maxWidthLine = Math.max( 22 | ...props.wordsByLines.map((line) => line.width || 0) 23 | ); 24 | if (props.scaleToFit) { 25 | width = Math.min(maxWidthLine, props.width || maxWidthLine); 26 | } else { 27 | width = maxWidthLine; 28 | } 29 | } 30 | 31 | // Adjust x and y for any transformations. 32 | // Note: This implementation only considers scaling transformations. 33 | // If other transformations are added (like skewing), this needs to be updated. 34 | if (props.transform) { 35 | const scaleMatch = () => 36 | /matrix\(([^,]+),[^,]+,[^,]+,[^,]+,[^,]+,[^,]+\)/.exec(props.transform); 37 | if (scaleMatch()) { 38 | const scale = () => parseFloat(scaleMatch()![1]); 39 | width *= scale(); 40 | // If height needs to be scaled similarly, add height *= scale; 41 | } 42 | } 43 | 44 | return width; 45 | }; 46 | 47 | // height is the sum of each line's height, plus the space between each line. 48 | const height = () => 49 | props.wordsByLines.reduce( 50 | (height, line, index) => height + (line.height || 0), 51 | 0 52 | ); 53 | 54 | let x = () => (props.dx !== undefined ? parseFloat(`${props.dx}`) : 0); 55 | let y = () => { 56 | let y = props.dy !== undefined ? parseFloat(`${props.dy}`) : 0; 57 | // Adjust x and y based on vertical-anchor prop. 58 | switch (props["vertical-anchor"]) { 59 | case "start": 60 | y += parseFloat(props.startDy); 61 | break; 62 | case "middle": 63 | y += height() / 2; 64 | break; 65 | case "end": 66 | y -= height(); 67 | break; 68 | default: 69 | break; 70 | } 71 | return y; 72 | }; 73 | 74 | return { 75 | get x() { 76 | return x(); 77 | }, 78 | get y() { 79 | return y(); 80 | }, 81 | get width() { 82 | return width(); 83 | }, 84 | get height() { 85 | return height(); 86 | }, 87 | }; 88 | } 89 | 90 | export default computeBoundingBox; 91 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/text/types.ts: -------------------------------------------------------------------------------- 1 | import { Ref } from "solid-js"; 2 | import { JSX } from "solid-js/jsx-runtime"; 3 | import { Id } from "../scenegraph"; 4 | import { Delimiter } from "./splitAtDelimiters"; 5 | 6 | type SVGTSpanProps = JSX.TSpanSVGAttributes; 7 | type SVGTextProps = JSX.TextSVGAttributes; 8 | 9 | type OwnProps = { 10 | /** className to apply to the SVGText element. */ 11 | className?: string; 12 | /** Whether to scale the fontSize to accommodate the specified width. */ 13 | scaleToFit?: boolean | "shrink-only"; 14 | /** Rotational angle of the text. */ 15 | angle?: number; 16 | /** Horizontal text anchor. */ 17 | // "text-anchor"?: "start" | "middle" | "end" | "inherit"; 18 | /** Vertical text anchor. */ 19 | "vertical-anchor"?: "start" | "middle" | "end"; 20 | /** Styles to be applied to the text (and used in computation of its size). */ 21 | // style?: JSX.CSSProperties; 22 | /** Ref passed to the Text SVG element. */ 23 | innerRef?: Ref; 24 | /** Ref passed to the Text text element */ 25 | innerTextRef?: Ref; 26 | /** x position of the text. */ 27 | x?: string | number; 28 | /** y position of the text. */ 29 | y?: string | number; 30 | /** dx offset of the text. */ 31 | dx?: string | number; 32 | /** dy offset of the text. */ 33 | dy?: string | number; 34 | /** Desired "line height" of the text, implemented as y offsets. */ 35 | "line-height"?: SVGTSpanProps["dy"]; 36 | /** Cap height of the text. */ 37 | "cap-height"?: string | number | undefined; 38 | /** Font size of text. */ 39 | // "font-size"?: string; 40 | /** Font family of text. */ 41 | // "font-family"?: string; 42 | /** Fill color of text. */ 43 | // fill?: string; 44 | /** Maximum width to occupy (approximate as words are not split). */ 45 | width?: number; 46 | /** String (or number coercible to one) to be styled and positioned. */ 47 | children?: string | number; 48 | 49 | delimiters?: Delimiter[]; 50 | }; 51 | 52 | export type TextProps = OwnProps & 53 | Omit & { name: Id }; 54 | 55 | export type compareFunction = (prev: T | undefined, next: T) => boolean; 56 | 57 | export interface WordsWithDims { 58 | words: string[]; 59 | width?: number; 60 | height?: number; 61 | } 62 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/util/lca.ts: -------------------------------------------------------------------------------- 1 | export type Tree = { [key: string]: { parent: string | null } }; 2 | 3 | export const getAncestorChain = (scenegraph: Tree, id: string): string[] => { 4 | const chain = []; 5 | let node = scenegraph[id]; 6 | if (node === undefined) { 7 | throw new Error(`node ${id} doesn't exist`); 8 | } 9 | while (node.parent !== null) { 10 | chain.push(node.parent); 11 | node = scenegraph[node.parent]; 12 | } 13 | return chain.reverse(); 14 | }; 15 | 16 | export const getLCAChain = ( 17 | scenegraph: Tree, 18 | id1: string, 19 | id2: string 20 | ): string[] => { 21 | const chain1 = getAncestorChain(scenegraph, id1); 22 | const chain2 = getAncestorChain(scenegraph, id2); 23 | 24 | const lcaChain = []; 25 | for (let i = 0; i < Math.min(chain1.length, chain2.length); i++) { 26 | if (chain1[i] === chain2[i]) { 27 | lcaChain.push(chain1[i]); 28 | } else { 29 | break; 30 | } 31 | } 32 | 33 | return lcaChain; 34 | }; 35 | 36 | // like getLCAChain, but returns the suffixes of the chains instead 37 | export const getLCAChainSuffixes = ( 38 | scenegraph: Tree, 39 | id1: string, 40 | id2: string 41 | ): [string[], string[]] => { 42 | const chain1 = getAncestorChain(scenegraph, id1); 43 | const chain2 = getAncestorChain(scenegraph, id2); 44 | 45 | const lcaChain = []; 46 | for (let i = 0; i < Math.min(chain1.length, chain2.length); i++) { 47 | if (chain1[i] === chain2[i]) { 48 | lcaChain.push(chain1[i]); 49 | } else { 50 | break; 51 | } 52 | } 53 | 54 | return [chain1.slice(lcaChain.length), chain2.slice(lcaChain.length)]; 55 | }; 56 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/util/maybe.ts: -------------------------------------------------------------------------------- 1 | /* 2 | Monadic functions for dealing with undefined values. 3 | Saves us a lot of boilerplate with undefined checks that make the code 4 | harder to read. 5 | */ 6 | 7 | // TODO: merge these two functions once I'm confident they're the same 8 | export const maybeAdd = (a: number | undefined, b: number | undefined) => 9 | a !== undefined && b !== undefined ? a + b : undefined; 10 | 11 | export const maybeAddAll = (...xs: (number | undefined)[]) => { 12 | if (xs.every((x) => x !== undefined)) { 13 | let sum = 0; 14 | for (const x of xs) { 15 | sum += x!; 16 | } 17 | return sum; 18 | } else { 19 | return undefined; 20 | } 21 | }; 22 | 23 | export const maybeSub = (a: number | undefined, b: number | undefined) => 24 | a !== undefined && b !== undefined ? a - b : undefined; 25 | 26 | export const maybeMul = (a: number | undefined, b: number | undefined) => 27 | a !== undefined && b !== undefined ? a * b : undefined; 28 | 29 | export const maybeDiv = (a: number | undefined, b: number | undefined) => 30 | a !== undefined && b !== undefined ? a / b : undefined; 31 | 32 | export const maybeMax = (xs: (number | undefined)[]) => 33 | xs.every((x) => x !== undefined) ? Math.max(...(xs as number[])) : undefined; 34 | 35 | // only returns undefined if all values are undefined, otherwise skips undefined values 36 | export const maxOfMaybes = (xs: (number | undefined)[]) => 37 | xs.every((x) => x === undefined) 38 | ? undefined 39 | : Math.max(...(xs.filter((x) => x !== undefined) as number[])); 40 | 41 | export const maybeMin = (xs: (number | undefined)[]) => 42 | xs.every((x) => x !== undefined) ? Math.min(...(xs as number[])) : undefined; 43 | 44 | export const minOfMaybes = (xs: (number | undefined)[]) => 45 | xs.every((x) => x === undefined) 46 | ? undefined 47 | : Math.min(...(xs.filter((x) => x !== undefined) as number[])); 48 | 49 | export const maybe = (x: T | undefined, f: (x: T) => U): U | undefined => 50 | x !== undefined ? f(x) : undefined; 51 | -------------------------------------------------------------------------------- /packages/bluefish-solid/src/withBluefish.tsx: -------------------------------------------------------------------------------- 1 | import { Accessor, Component, createContext, createSignal, createUniqueId, useContext } from "solid-js"; 2 | import type { JSX } from "solid-js"; 3 | import { Id, ScenegraphElement, resolveScenegraphElements } from "./scenegraph"; 4 | import { ParentScopeIdContext, ScopeContext } from "./createName"; 5 | import { Dynamic } from "solid-js/web"; 6 | 7 | export type WithBluefishProps = T & { 8 | name?: Id; 9 | }; 10 | 11 | export const IdContext = createContext>(() => undefined); 12 | 13 | export function withBluefish( 14 | WrappedComponent: Component>, 15 | options?: { displayName?: string } 16 | ) { 17 | return (props: Omit & { name?: Id }) => { 18 | // scenegraph id 19 | const contextId = useContext(IdContext); 20 | const parentScopeId = useContext(ParentScopeIdContext); 21 | const displayNamePrefix = options?.displayName !== undefined ? `${options?.displayName}(` : ""; 22 | const displayNameSuffix = options?.displayName !== undefined ? ")" : ""; 23 | const genId = `${displayNamePrefix}${createUniqueId()}${displayNameSuffix}`; 24 | const genScopeId = `${displayNamePrefix}${createUniqueId()}${displayNameSuffix}`; 25 | // const id = () => props.name ?? contextId() ?? genId; 26 | const id = () => contextId() ?? genId; 27 | const scopeId = () => props.name ?? genScopeId; 28 | 29 | // component scope id 30 | const [scope, setScope] = useContext(ScopeContext); 31 | const [layout, setLayout] = createSignal<(parentId: Id | null) => void>(() => {}); 32 | 33 | // TODO: might have to initialize the scope in the store if the scope id was auto-generated 34 | 35 | if (scope[scopeId()] === undefined) { 36 | setScope(scopeId(), { 37 | parent: parentScopeId(), 38 | layoutNode: undefined, 39 | children: {}, 40 | }); 41 | } 42 | setScope(scopeId(), "layoutNode", id()); 43 | 44 | const jsx = ( 45 | 46 | 47 | {(() => { 48 | const layoutNode = resolveScenegraphElements( 49 | )} name={id()} /> 50 | ); 51 | 52 | setLayout(() => layoutNode[0].layout); 53 | 54 | return layoutNode[0].jsx; 55 | })()} 56 | 57 | 58 | ); 59 | 60 | return { 61 | jsx, 62 | layout: (parentId) => layout()(parentId), 63 | } satisfies ScenegraphElement as unknown as JSX.Element; 64 | }; 65 | } 66 | 67 | export default withBluefish; 68 | -------------------------------------------------------------------------------- /packages/bluefish-solid/tsconfig.build.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig", 3 | "exclude": ["node_modules", "dist", "examples", "experiments", "public", "src/stories", "vite.config.ts"] 4 | } 5 | -------------------------------------------------------------------------------- /packages/bluefish-solid/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "exclude": ["node_modules", "dist"], 3 | "compilerOptions": { 4 | "strict": true, 5 | "target": "ESNext", 6 | "module": "ESNext", 7 | "moduleResolution": "node", 8 | "allowSyntheticDefaultImports": true, 9 | "esModuleInterop": true, 10 | "jsx": "preserve", 11 | "jsxImportSource": "solid-js", 12 | "types": ["vite/client"], 13 | "emitDeclarationOnly": true, 14 | "isolatedModules": true, 15 | "declaration": true, 16 | "declarationDir": "dist", 17 | "declarationMap": true 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /packages/bluefish-solid/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import { resolve } from "path"; 3 | import solidPlugin from "vite-plugin-solid"; 4 | import dts from "vite-plugin-dts"; 5 | 6 | export default defineConfig(({ command }) => ({ 7 | publicDir: command === "serve" ? "public" : false, 8 | plugins: [ 9 | solidPlugin(), 10 | dts({ tsconfigPath: "./tsconfig.build.json" }), 11 | ], 12 | server: { 13 | port: 3000, 14 | open: "/public/index.html", 15 | }, 16 | build: { 17 | lib: { 18 | entry: resolve(__dirname, "src/index.ts"), 19 | formats: ["es", "cjs", "umd"], 20 | name: "bluefish", 21 | fileName: "index", 22 | }, 23 | emptyOutDir: false, 24 | target: "esnext", 25 | rollupOptions: { 26 | external: ["solid-js", "solid-js/web", "solid-js/store"], 27 | output: { 28 | globals: { 29 | "solid-js": "solidJs", 30 | "solid-js/web": "web", 31 | "solid-js/store": "store", 32 | }, 33 | }, 34 | }, 35 | }, 36 | })); 37 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - "docs" 3 | - "packages/*" 4 | -------------------------------------------------------------------------------- /turbo.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://turbo.build/schema.json", 3 | "ui": "tui", 4 | "tasks": { 5 | "build": { 6 | "dependsOn": ["^build"], 7 | "inputs": ["$TURBO_DEFAULT$", ".env*"], 8 | "outputs": [".next/**", "!.next/cache/**", "dist/**"] 9 | }, 10 | "publish": { 11 | "dependsOn": ["build"] 12 | }, 13 | "docs:build": { 14 | "dependsOn": ["^docs:build"], 15 | "inputs": ["$TURBO_DEFAULT$"], 16 | "outputs": [".vitepress/dist/**"] 17 | }, 18 | "lint": { 19 | "dependsOn": ["^lint"] 20 | }, 21 | "dev": { 22 | "cache": false, 23 | "persistent": true 24 | }, 25 | "docs:dev": { 26 | "cache": false, 27 | "persistent": true 28 | }, 29 | "storybook": { 30 | "cache": false, 31 | "persistent": true 32 | }, 33 | "storybook-docs": { 34 | "cache": false, 35 | "persistent": true 36 | }, 37 | "build-storybook": { 38 | "dependsOn": ["^build-storybook"], 39 | "inputs": ["$TURBO_DEFAULT$"], 40 | "outputs": ["storybook-static/**"] 41 | }, 42 | "test-storybook": { 43 | "dependsOn": ["build-storybook"] 44 | }, 45 | "chromatic": { 46 | "dependsOn": ["build-storybook"], 47 | "inputs": ["$TURBO_DEFAULT$"] 48 | } 49 | } 50 | } 51 | --------------------------------------------------------------------------------