├── .eslintrc.json ├── .github ├── FUNDING.yml └── workflows │ └── publish-studio.yml ├── .gitignore ├── .nvmrc ├── LICENSE ├── README.md ├── lerna.json ├── package.json ├── packages ├── replicad-app-example │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── index.html │ ├── package.json │ ├── src │ │ ├── App.jsx │ │ ├── ReplicadMesh.jsx │ │ ├── ThreeContext.jsx │ │ ├── cad.js │ │ ├── index.jsx │ │ └── worker.js │ ├── vite.config.js │ └── watch.json ├── replicad-docs │ ├── .gitignore │ ├── .nvmrc │ ├── README.md │ ├── babel.config.js │ ├── docs │ │ ├── advanced-topics │ │ │ ├── _category_.json │ │ │ └── typescript.md │ │ ├── examples │ │ │ ├── _category_.json │ │ │ ├── cadquery-gear.mdx │ │ │ ├── code-cad-birdhouse.mdx │ │ │ ├── design-watering-can.mdx │ │ │ ├── gridfinity.md │ │ │ ├── occt-bottle.mdx │ │ │ ├── projections.mdx │ │ │ ├── simple-vase.mdx │ │ │ └── wavy-vase.mdx │ │ ├── intro.md │ │ ├── other-ressources.md │ │ ├── recipes │ │ │ ├── _category_.json │ │ │ ├── fuse-all.md │ │ │ ├── polar-array.md │ │ │ ├── sweeped-profile-box.md │ │ │ └── why-recipes.md │ │ ├── tutorial-making-a-watering-can │ │ │ ├── _category_.json │ │ │ ├── combining-the-shapes.md │ │ │ ├── creating-the-shapes.md │ │ │ ├── drawing-the-body.md │ │ │ ├── intro.mdx │ │ │ └── using-planes-for-spiller.md │ │ ├── tutorial-overview │ │ │ ├── _category_.json │ │ │ ├── adding-depth.md │ │ │ ├── combinations.md │ │ │ ├── drawing.md │ │ │ ├── finders.md │ │ │ ├── modifications.md │ │ │ ├── planes-and-sketches.md │ │ │ ├── sharing-models.md │ │ │ ├── transformations.md │ │ │ └── using-the-workbench.md │ │ └── use-as-a-library.md │ ├── docusaurus.config.js │ ├── examples │ │ ├── birdhouse.js │ │ ├── bottle.js │ │ ├── cadquery-cycloidal-gear.js │ │ ├── gridfinity.js │ │ ├── helpers.js │ │ ├── projections.js │ │ ├── simpleVase.js │ │ ├── watering-can.js │ │ └── wavyVase.js │ ├── package.json │ ├── sidebars.js │ ├── src │ │ ├── components │ │ │ ├── HomepageFeatures.js │ │ │ └── HomepageFeatures.module.css │ │ ├── css │ │ │ └── custom.css │ │ ├── pages │ │ │ ├── index.js │ │ │ ├── index.module.css │ │ │ └── markdown-page.md │ │ └── theme │ │ │ └── CodeBlock │ │ │ ├── Container │ │ │ ├── index.js │ │ │ └── styles.module.css │ │ │ ├── Content │ │ │ ├── Element.js │ │ │ ├── String.js │ │ │ └── styles.module.css │ │ │ ├── CopyButton │ │ │ ├── index.js │ │ │ └── styles.module.css │ │ │ ├── Line │ │ │ ├── index.js │ │ │ └── styles.module.css │ │ │ ├── WordWrapButton │ │ │ ├── index.js │ │ │ └── styles.module.css │ │ │ ├── WorkbenchButton │ │ │ ├── index.js │ │ │ └── styles.module.css │ │ │ └── index.js │ └── static │ │ ├── .nojekyll │ │ ├── fonts │ │ ├── HKGrotesk-Bold.woff2 │ │ ├── HKGrotesk-Light.woff2 │ │ ├── HKGrotesk-LightItalic.woff2 │ │ └── HKGrotesk-Regular.woff2 │ │ └── img │ │ ├── browser.svg │ │ ├── favicon.ico │ │ ├── finger.svg │ │ ├── gear.svg │ │ ├── replicad.png │ │ ├── tools.svg │ │ └── tutorial │ │ ├── adding-depth-1.png │ │ ├── adding-depth-2.png │ │ ├── adding-depth-3.png │ │ ├── combinations-1.png │ │ ├── combinations-2.png │ │ ├── combinations-3.png │ │ ├── drawing-1.png │ │ ├── finders-1.png │ │ ├── first-model.png │ │ ├── planes-1.png │ │ ├── planes-2.png │ │ ├── sketching-1.png │ │ ├── transformations-1.png │ │ ├── typescript-1.png │ │ └── workbench.png ├── replicad-opencascadejs │ ├── LICENSE │ ├── README.md │ ├── __tests__ │ │ └── replicad-opencascadejs.test.js │ ├── build-config │ │ ├── custom_build_single.yml │ │ └── custom_build_with_exceptions.yml │ ├── build-source │ │ ├── custom_build_single.yml │ │ ├── custom_build_with_exceptions.yml │ │ └── defaults.yml │ ├── package.json │ └── src │ │ ├── replicad_single.d.ts │ │ ├── replicad_single.js │ │ ├── replicad_single.wasm │ │ ├── replicad_with_exceptions.d.ts │ │ ├── replicad_with_exceptions.js │ │ └── replicad_with_exceptions.wasm ├── replicad-threejs-helper │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── __tests__ │ │ └── replicad-threejs-helper.test.js │ ├── lib │ │ └── replicad-threejs-helper.ts │ ├── package.json │ ├── rollup.config.js │ └── tsconfig.json ├── replicad │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── __tests__ │ │ ├── diffSVGToSnapshot.ts │ │ ├── drawing │ │ │ ├── __snapshots__ │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--build-from-triangles-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--cut-such-that-a-hole-becomes-the-outside-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--cut-two-rectangles-with-corners-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--cuts-one-rectangle-from-the-other-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--fuse-two-rectangles-with-corners-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--fuses-two-circles-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--fuses-two-rectangles-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--handles-a-complex-example-of-merging-shapes-with-11-copies-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--handles-a-complex-example-of-merging-shapes-with-13-copies-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--handles-a-complex-example-of-merging-shapes-with-5-copies-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--handles-the-case-when-a-compound-is-created-from-fusion-1-snap.svg │ │ │ │ ├── booleans.test.ts-__tests__-drawing-booleans.test.ts--intersects-two-rectangles-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--1-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--10-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--25-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--50-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--75-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset-1-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset-10-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset-25-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset-50-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset--1-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset--10-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset--2-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset--5-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset-1-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset-20-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-vase,-with-offset--1-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-vase,-with-offset--5-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-vase,-with-offset-1-1-snap.svg │ │ │ │ ├── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-vase,-with-offset-10-1-snap.svg │ │ │ │ └── offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-vase,-with-offset-5-1-snap.svg │ │ │ ├── booleans.test.ts │ │ │ └── offsets.test.ts │ │ ├── setup.ts │ │ └── toMatchSVGSnapshot.ts │ ├── package.json │ ├── src │ │ ├── Sketcher.ts │ │ ├── Sketcher2d.ts │ │ ├── addThickness.ts │ │ ├── blueprints │ │ │ ├── Blueprint.ts │ │ │ ├── Blueprints.ts │ │ │ ├── CompoundBlueprint.ts │ │ │ ├── approximations.ts │ │ │ ├── boolean2D.ts │ │ │ ├── booleanOperations.ts │ │ │ ├── cannedBlueprints.ts │ │ │ ├── customCorners.ts │ │ │ ├── index.ts │ │ │ ├── lib.ts │ │ │ ├── offset.ts │ │ │ └── svg.ts │ │ ├── constants.ts │ │ ├── curves.ts │ │ ├── definitionMaps.ts │ │ ├── draw.ts │ │ ├── export │ │ │ └── assemblyExporter.ts │ │ ├── finders │ │ │ ├── cornerFinder.ts │ │ │ ├── definitions.ts │ │ │ ├── edgeFinder.ts │ │ │ ├── faceFinder.ts │ │ │ ├── generic3dfinder.ts │ │ │ └── index.ts │ │ ├── geom.ts │ │ ├── geomHelpers.ts │ │ ├── importers.ts │ │ ├── index.ts │ │ ├── lib2d │ │ │ ├── BoundingBox2d.ts │ │ │ ├── Curve2D.ts │ │ │ ├── approximations.ts │ │ │ ├── customCorners.ts │ │ │ ├── definitions.ts │ │ │ ├── index.ts │ │ │ ├── intersections.ts │ │ │ ├── makeCurves.ts │ │ │ ├── ocWrapper.ts │ │ │ ├── offset.ts │ │ │ ├── stitching.ts │ │ │ ├── svgPath.ts │ │ │ ├── utils.ts │ │ │ └── vectorOperations.ts │ │ ├── measureShape.ts │ │ ├── oclib.ts │ │ ├── projection │ │ │ ├── ProjectionCamera.ts │ │ │ ├── index.ts │ │ │ └── makeProjectedEdges.ts │ │ ├── register.ts │ │ ├── shapeHelpers.ts │ │ ├── shapes.ts │ │ ├── shortcuts.ts │ │ ├── sketcherlib.ts │ │ ├── sketches │ │ │ ├── CompoundSketch.ts │ │ │ ├── Sketch.ts │ │ │ ├── Sketches.ts │ │ │ ├── cannedSketches.ts │ │ │ ├── index.ts │ │ │ └── lib.ts │ │ ├── text.ts │ │ └── utils │ │ │ ├── ProgressRange.ts │ │ │ ├── precisionRound.ts │ │ │ ├── range.ts │ │ │ ├── round2.ts │ │ │ ├── round5.ts │ │ │ ├── uuid.ts │ │ │ └── zip.ts │ ├── tsconfig.json │ ├── vite.config.js │ └── vite.config.js.timestamp-1692005769277.mjs └── studio │ ├── .gitignore │ ├── LICENSE │ ├── README.md │ ├── __tests__ │ └── studio.test.js │ ├── index.html │ ├── lib │ └── studio.js │ ├── package.json │ ├── public │ ├── favicon.ico │ ├── fonts │ │ ├── HKGrotesk-Bold.woff2 │ │ ├── HKGrotesk-Light.woff2 │ │ ├── HKGrotesk-LightItalic.woff2 │ │ ├── HKGrotesk-Regular.ttf │ │ └── HKGrotesk-Regular.woff2 │ ├── icon-192x192.png │ ├── icon-256x256.png │ ├── icon-384x384.png │ ├── icon-512x512.png │ ├── manifest.webmanifest │ ├── robots.txt │ └── textures │ │ └── matcap-1.png │ ├── src │ ├── App.jsx │ ├── GlobalStyles.jsx │ ├── LinkWidget.jsx │ ├── ReloadPrompt.jsx │ ├── Welcome.jsx │ ├── builder.worker.js │ ├── components-3d │ │ ├── ClipPlane.jsx │ │ ├── Controls.jsx │ │ ├── DefaultGeometry.jsx │ │ ├── FloatingLabel.jsx │ │ ├── InfiniteGrid.jsx │ │ ├── Positionner.jsx │ │ ├── ShapeGeometry.jsx │ │ ├── Stage.jsx │ │ └── replicadMesh.jsx │ ├── components │ │ ├── Button.jsx │ │ ├── ButtonMenu.jsx │ │ ├── Dialog.jsx │ │ ├── ErrorBoundary.jsx │ │ ├── FloatingInfo.jsx │ │ ├── LinkEditor.jsx │ │ ├── LoadingScreen.jsx │ │ ├── StandardUI.jsx │ │ ├── ToolUI.jsx │ │ └── Toolbar.jsx │ ├── icons │ │ ├── Clipping.jsx │ │ ├── Configure.jsx │ │ ├── Download.jsx │ │ ├── Focus.jsx │ │ ├── Fullscreen.jsx │ │ ├── Loading.jsx │ │ ├── NewWindow.jsx │ │ ├── Reload.jsx │ │ └── Share.jsx │ ├── index.jsx │ ├── initOCSingle.js │ ├── initOCWithExceptions.js │ ├── utils │ │ ├── StudioHelper.js │ │ ├── builderAPI.js │ │ ├── diskFileAccess.js │ │ ├── downloadCode.js │ │ ├── dumpCode.js │ │ ├── loadCode.js │ │ ├── normalizeColor.ts │ │ ├── renderOutput.ts │ │ ├── saveShape.js │ │ ├── useDarkMode.js │ │ └── zip.js │ ├── viewers │ │ ├── Canvas.jsx │ │ ├── EditorViewer.jsx │ │ ├── Material.jsx │ │ ├── NicePresentationViewer.jsx │ │ ├── PresentationViewer.jsx │ │ └── SVGViewer.jsx │ ├── visualiser │ │ ├── Editor.jsx │ │ └── editor │ │ │ ├── ClippingParams.jsx │ │ │ ├── DownloadDialog.jsx │ │ │ ├── HighlighedInfo.jsx │ │ │ ├── ParamsEditor.jsx │ │ │ ├── code-state.js │ │ │ ├── codeInit.js │ │ │ ├── selected-info.js │ │ │ ├── state.js │ │ │ ├── ui-state.js │ │ │ └── useEditorStore.jsx │ ├── vm.js │ └── workbench │ │ ├── Autoload.jsx │ │ ├── EditorPane.jsx │ │ ├── VisualizerPane.jsx │ │ ├── Workbench.jsx │ │ ├── loadMonaco.js │ │ └── panes.jsx │ └── vite.config.js ├── pnpm-lock.yaml └── pnpm-workspace.yaml /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "env": { 3 | "browser": true, 4 | "commonjs": true, 5 | "es6": true 6 | }, 7 | "extends": [ 8 | "eslint:recommended", 9 | "plugin:@typescript-eslint/eslint-recommended", 10 | "plugin:@typescript-eslint/recommended" 11 | ], 12 | "parser": "@typescript-eslint/parser", 13 | "rules": { 14 | "react/prop-types": "off", 15 | "@typescript-eslint/no-explicit-any": "off" 16 | }, 17 | "parserOptions": { 18 | "ecmaVersion": 2018, 19 | "sourceType": "module", 20 | "ecmaFeatures": { 21 | "jsx": true 22 | } 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /.github/FUNDING.yml: -------------------------------------------------------------------------------- 1 | # These are supported funding model platforms 2 | 3 | github: sgenoud 4 | -------------------------------------------------------------------------------- /.github/workflows/publish-studio.yml: -------------------------------------------------------------------------------- 1 | name: "Publish pages" 2 | 3 | on: 4 | push: 5 | branches: [main] 6 | pull_request: 7 | types: [opened, synchronize] 8 | 9 | jobs: 10 | publish-studio: 11 | runs-on: ubuntu-latest 12 | permissions: 13 | contents: read 14 | deployments: write 15 | 16 | steps: 17 | - name: Checkout 18 | uses: actions/checkout@v3 19 | 20 | - name: Install Node.js 21 | uses: actions/setup-node@v3 22 | with: 23 | node-version: 20 24 | 25 | - uses: pnpm/action-setup@v2 26 | name: Install pnpm 27 | with: 28 | version: 8 29 | run_install: false 30 | 31 | - name: Get pnpm store directory 32 | shell: bash 33 | run: | 34 | echo "STORE_PATH=$(pnpm store path --silent)" >> $GITHUB_ENV 35 | 36 | - uses: actions/cache@v3 37 | name: Setup pnpm cache 38 | with: 39 | path: ${{ env.STORE_PATH }} 40 | key: ${{ runner.os }}-pnpm-store-${{ hashFiles('**/pnpm-lock.yaml') }} 41 | restore-keys: | 42 | ${{ runner.os }}-pnpm-store- 43 | 44 | - name: Install dependencies 45 | run: pnpm install --no-frozen-lockfile 46 | 47 | - name: Build packages 48 | run: pnpm prepare-packages 49 | 50 | - name: Build the studio 51 | run: cd packages/studio && pnpm build 52 | 53 | - name: Build the example app 54 | run: cd packages/replicad-app-example && pnpm build 55 | 56 | - name: Publish the studio 57 | uses: cloudflare/pages-action@v1 58 | with: 59 | apiToken: ${{ secrets.CLOUDFLARE_PAGES_ACCESS }} 60 | accountId: d0e0387dd16b671a301d1ce6e7b73609 61 | projectName: replicad-studio 62 | directory: packages/studio/dist 63 | wranglerVersion: '3' 64 | 65 | - name: Publish the example app 66 | uses: cloudflare/pages-action@v1 67 | with: 68 | apiToken: ${{ secrets.CLOUDFLARE_PAGES_ACCESS }} 69 | accountId: d0e0387dd16b671a301d1ce6e7b73609 70 | projectName: replicad-example-app 71 | directory: packages/replicad-app-example/build 72 | wranglerVersion: '3' 73 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | .DS_Store 4 | -------------------------------------------------------------------------------- /.nvmrc: -------------------------------------------------------------------------------- 1 | v20.12.1 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 QuaroTech Sàrl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # `replicad` 2 | 3 | The library to build browser based 3D models with code. 4 | 5 | As an abstraction over opencascade, it gives developers the power to integrate 6 | it in their web application. 7 | 8 | So there are two ways you might be interested in this library: 9 | 10 | - how do I use it to build a 3D model? 11 | - how do I integrate replicad in my web application? 12 | 13 | For more information, [check the documentation](https://replicad.xyz) 14 | 15 | ## A simple example 16 | 17 | [![ab4e3c12f4257659](https://user-images.githubusercontent.com/263325/142650619-7f737a89-d0fa-4c2b-adcb-202b612056b9.jpg)](https://studio.replicad.xyz/share/https%3A%2F%2Fraw.githubusercontent.com%2Fsgenoud%2Freplicad%2Fmain%2Fpackages%2Freplicad-docs%2Fexamples%2FsimpleVase.js) 18 | -------------------------------------------------------------------------------- /lerna.json: -------------------------------------------------------------------------------- 1 | { 2 | "npmClient": "pnpm", 3 | "version": "0.19.0", 4 | "packages": ["packages/*"], 5 | "$schema": "node_modules/lerna/schemas/lerna-schema.json" 6 | } 7 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "root", 3 | "private": true, 4 | "type": "module", 5 | "workspaces": [ 6 | "packages/*" 7 | ], 8 | "license": "MIT", 9 | "scripts": { 10 | "prepare-packages": "lerna exec pnpm build --no-private --no-bail", 11 | "publish-packages": "lerna publish" 12 | }, 13 | "devDependencies": { 14 | "eslint": "^8.1.0", 15 | "lerna": "7.1.5", 16 | "prettier": "^2.4.1" 17 | }, 18 | "packageManager": "pnpm@9.13.2+sha512.88c9c3864450350e65a33587ab801acf946d7c814ed1134da4a924f6df5a2120fd36b46aab68f7cd1d413149112d53c7db3a4136624cfd00ff1846a0c6cef48a" 19 | } 20 | -------------------------------------------------------------------------------- /packages/replicad-app-example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build 3 | -------------------------------------------------------------------------------- /packages/replicad-app-example/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 QuaroTech Sàrl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/replicad-app-example/README.md: -------------------------------------------------------------------------------- 1 | # A sample app built with replicad! 2 | 3 | This module is here to show how to integrate replicad in a React app with 4 | react-three-fiber. 5 | 6 | You can visit this page at 7 | 8 | This is a basic integration that aims to show how the different pieces can 9 | stick together: 10 | 11 | - The replicad code can be just a javascript function 12 | - It should live, with the opencascade loading in a webworker (ideally with 13 | functions exposed via comlink). 14 | - Your main app can be just a react app (with react-three-fiber for instance) 15 | - The `replicad-three-helper` helps you synchronise the data out of a meshed 16 | replicad shape to 17 | 18 | This sample app uses vitejs, as a create react app setup does not play nice with 19 | webassembly and webworkers. This allowed me to keep the code simple and nice. 20 | 21 | ## How to run locally 22 | 23 | I do not advice to just clone the multirepo and then run from packages (it 24 | needs some magic with the multi repo logic, this is not great). 25 | 26 | What I would advice you do to is copy the sample app out of the multirepo and 27 | then install and run. So something in that spirit: 28 | 29 | ```sh 30 | git clone git@github.com:sgenoud/replicad.git 31 | cp -r replicad/packages/replicad-app-example . 32 | cd replicad-app-example 33 | npm install 34 | npm start 35 | ``` 36 | -------------------------------------------------------------------------------- /packages/replicad-app-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Replicad sample app 5 | 6 | 7 | 8 | 9 | 10 | 11 |
12 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /packages/replicad-app-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replicad-app-example", 3 | "version": "0.19.0", 4 | "description": "A simple React app based on replicad.", 5 | "private": true, 6 | "license": "MIT", 7 | "scripts": { 8 | "start": "vite", 9 | "build": "vite build", 10 | "serve": "vite preview" 11 | }, 12 | "dependencies": { 13 | "@react-three/drei": "9.92.7", 14 | "@react-three/fiber": "8.13.6", 15 | "comlink": "^4.3.1", 16 | "file-saver": "^2.0.5", 17 | "react": "18.2.0", 18 | "react-dom": "18.2.0", 19 | "replicad": "^0.19.0", 20 | "replicad-opencascadejs": "^0.19.0", 21 | "replicad-threejs-helper": "^0.19.0", 22 | "three": "^0.155.0" 23 | }, 24 | "devDependencies": { 25 | "@vitejs/plugin-react": "3.0.1", 26 | "vite": "4.0.4" 27 | }, 28 | "browserslist": [ 29 | ">0.2%", 30 | "not dead" 31 | ] 32 | } 33 | -------------------------------------------------------------------------------- /packages/replicad-app-example/src/ReplicadMesh.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useLayoutEffect, useEffect } from "react"; 2 | import { useThree } from "@react-three/fiber"; 3 | import { BufferGeometry } from "three"; 4 | import { 5 | syncFaces, 6 | syncLines, 7 | syncLinesFromFaces, 8 | } from "replicad-threejs-helper"; 9 | 10 | export default React.memo(function ShapeMeshes({ faces, edges }) { 11 | const { invalidate } = useThree(); 12 | 13 | const body = useRef(new BufferGeometry()); 14 | const lines = useRef(new BufferGeometry()); 15 | 16 | useLayoutEffect(() => { 17 | // We use the three helpers to synchronise the buffer geometry with the 18 | // new data from the parameters 19 | if (faces) syncFaces(body.current, faces); 20 | 21 | if (edges) syncLines(lines.current, edges); 22 | else if (faces) syncLinesFromFaces(lines.current, body.current); 23 | 24 | // We have configured the canvas to only refresh when there is a change, 25 | // the invalidate function is here to tell it to recompute 26 | invalidate(); 27 | }, [faces, edges, invalidate]); 28 | 29 | useEffect( 30 | () => () => { 31 | body.current.dispose(); 32 | lines.current.dispose(); 33 | invalidate(); 34 | }, 35 | [invalidate] 36 | ); 37 | 38 | return ( 39 | 40 | 41 | {/* the offsets are here to avoid z fighting between the mesh and the lines */} 42 | 48 | 49 | 50 | 51 | 52 | 53 | ); 54 | }); 55 | -------------------------------------------------------------------------------- /packages/replicad-app-example/src/ThreeContext.jsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { Canvas } from "@react-three/fiber"; 3 | import { OrbitControls } from "@react-three/drei"; 4 | import * as THREE from "three"; 5 | 6 | // We change the default orientation - threejs tends to use Y are the height, 7 | // while replicad uses Z. This is mostly a representation default. 8 | THREE.Object3D.DEFAULT_UP.set(0, 0, 1); 9 | 10 | // This is the basics to render a nice looking model user react-three-fiber 11 | // 12 | // The camera is positioned for the model we present (that cannot change size. 13 | // You might need to adjust this with something like the autoadjust from the 14 | // `Stage` component of `drei` 15 | // 16 | // Depending on your needs I would advice not using a light and relying on 17 | // a matcap material instead of the meshStandardMaterial used here. 18 | export default function ThreeContext({ children, ...props }) { 19 | const dpr = Math.min(window.devicePixelRatio, 2); 20 | 21 | return ( 22 | 23 | 34 | 35 | 36 | 37 | {children} 38 | 39 | 40 | ); 41 | } 42 | -------------------------------------------------------------------------------- /packages/replicad-app-example/src/cad.js: -------------------------------------------------------------------------------- 1 | import { drawRoundedRectangle } from "replicad"; 2 | 3 | // The replicad code! Not much there! 4 | export function drawBox(thickness) { 5 | return drawRoundedRectangle(30, 50) 6 | .sketchOnPlane() 7 | .extrude(20) 8 | .shell(thickness, (f) => f.inPlane("XY", 20)); 9 | } 10 | -------------------------------------------------------------------------------- /packages/replicad-app-example/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import ReactDOM from "react-dom"; 3 | import App from "./App.jsx"; 4 | 5 | // This is here to compensate for a bug in vite 6 | import "replicad-opencascadejs/src/replicad_single.wasm?url"; 7 | 8 | ReactDOM.render( 9 | 10 | 11 | , 12 | document.getElementById("root") 13 | ); 14 | 15 | // Hot Module Replacement (HMR) - Remove this snippet to remove HMR. 16 | // Learn more: https://vitejs.dev/guide/api-hmr.html 17 | if (import.meta.hot) { 18 | import.meta.hot.accept(); 19 | } 20 | -------------------------------------------------------------------------------- /packages/replicad-app-example/src/worker.js: -------------------------------------------------------------------------------- 1 | import opencascade from "replicad-opencascadejs/src/replicad_single.js"; 2 | import opencascadeWasm from "replicad-opencascadejs/src/replicad_single.wasm?url"; 3 | import { setOC } from "replicad"; 4 | import { expose } from "comlink"; 5 | 6 | // We import our model as a simple function 7 | import { drawBox } from "./cad"; 8 | 9 | // This is the logic to load the web assembly code into replicad 10 | let loaded = false; 11 | const init = async () => { 12 | if (loaded) return Promise.resolve(true); 13 | 14 | const OC = await opencascade({ 15 | locateFile: () => opencascadeWasm, 16 | }); 17 | 18 | loaded = true; 19 | setOC(OC); 20 | 21 | return true; 22 | }; 23 | const started = init(); 24 | 25 | function createBlob(thickness) { 26 | // note that you might want to do some caching for more complex models 27 | return started.then(() => { 28 | return drawBox(thickness).blobSTL(); 29 | }); 30 | } 31 | 32 | function createMesh(thickness) { 33 | return started.then(() => { 34 | const box = drawBox(thickness); 35 | // This is how you get the data structure that the replica-three-helper 36 | // can synchronise with three BufferGeometry 37 | return { 38 | faces: box.mesh(), 39 | edges: box.meshEdges(), 40 | }; 41 | }); 42 | } 43 | 44 | // comlink is great to expose your functions within the worker as a simple API 45 | // to your app. 46 | expose({ createBlob, createMesh }); 47 | -------------------------------------------------------------------------------- /packages/replicad-app-example/vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vite"; 2 | import reactPlugin from "@vitejs/plugin-react"; 3 | 4 | // https://vitejs.dev/config/ 5 | export default defineConfig({ 6 | plugins: [reactPlugin()], 7 | build: { 8 | outDir: "build", 9 | }, 10 | server: { 11 | port: 4444, 12 | }, 13 | }); 14 | -------------------------------------------------------------------------------- /packages/replicad-app-example/watch.json: -------------------------------------------------------------------------------- 1 | { 2 | "install": { 3 | "include": [ 4 | "^package\\.json$" 5 | ], 6 | "exclude": [] 7 | }, 8 | "restart": { 9 | "include": [ 10 | "^\\.env$", 11 | "^backend/" 12 | ], 13 | "exclude": [ 14 | "src/" 15 | ] 16 | }, 17 | "noSavedEvents": true 18 | } 19 | -------------------------------------------------------------------------------- /packages/replicad-docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Autogenerated API 8 | /docs/api 9 | 10 | # Generated files 11 | .docusaurus 12 | .cache-loader 13 | 14 | # Misc 15 | .DS_Store 16 | .env.local 17 | .env.development.local 18 | .env.test.local 19 | .env.production.local 20 | 21 | npm-debug.log* 22 | yarn-debug.log* 23 | yarn-error.log* 24 | -------------------------------------------------------------------------------- /packages/replicad-docs/.nvmrc: -------------------------------------------------------------------------------- 1 | v16.18.0 2 | -------------------------------------------------------------------------------- /packages/replicad-docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus 2](https://docusaurus.io/), a modern static website generator. 4 | 5 | ### Installation 6 | 7 | ``` 8 | $ yarn 9 | ``` 10 | 11 | ### Local Development 12 | 13 | ``` 14 | $ yarn start 15 | ``` 16 | 17 | This command starts a local development server and opens up a browser window. Most changes are reflected live without having to restart the server. 18 | 19 | ### Build 20 | 21 | ``` 22 | $ yarn build 23 | ``` 24 | 25 | This command generates static content into the `build` directory and can be served using any static contents hosting service. 26 | 27 | ### Deployment 28 | 29 | ``` 30 | $ GIT_USER= USE_SSH=true yarn deploy 31 | ``` 32 | 33 | If you are using GitHub pages for hosting, this command is a convenient way to build the website and push to the `gh-pages` branch. 34 | -------------------------------------------------------------------------------- /packages/replicad-docs/babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | presets: [require.resolve('@docusaurus/core/lib/babel/preset')], 3 | }; 4 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/advanced-topics/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Advanced Topics", 3 | "position": 3.5 4 | } 5 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/advanced-topics/typescript.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | title: Typescript autocompletion 4 | --- 5 | 6 | replicad is build with Typescript. The main advantage to the user is that it 7 | offers nice autocompletion and inline documentation. 8 | 9 | How can one benefit from these while using the visualiser? 10 | 11 | Initialise a node package somewhere on your disk 12 | 13 | ```bash 14 | mkdir my-replicad-model 15 | cd my-replicad-model/ 16 | yarn init 17 | yarn add typescript replicad 18 | ``` 19 | 20 | You can then open your favourite editor (or Visual Studio Code if you do not 21 | have a favourite yet) in this folder. 22 | 23 | Create a `myModel.js` file, and add the following boilerplate: 24 | 25 | ```js 26 | const defaultParams = {}; 27 | 28 | /** @typedef { typeof import("replicad") } replicadLib */ 29 | /** @type {function(replicadLib, typeof defaultParams): any} */ 30 | function main({ Sketcher }, {}) { 31 | return new Sketcher(); // Build your model here 32 | } 33 | ``` 34 | 35 | If your editor is configured correctly for typescript it should autocomplete 36 | now. 37 | 38 | ![a working editor](/img/tutorial/typescript-1.png) 39 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/examples/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Examples", 3 | "position": 5 4 | } 5 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/examples/cadquery-gear.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | title: The CadQuery cycloidal gear 4 | --- 5 | 6 | import CodeBlock from '@theme/CodeBlock'; 7 | import {iframePath} from '../../examples/helpers.js'; 8 | import MyComponentSource from '!!raw-loader!../../examples/cadquery-cycloidal-gear.js'; 9 | 10 | As in CadQuery replicad supports sketching parametric functions, we can 11 | implement [this 12 | gear](https://cadquery.readthedocs.io/en/latest/examples.html#cycloidal-gear) 13 | as well. 14 | 15 | We can have a look at the rendered shape. 16 | 17 | 18 | 19 | And here is what the code looks like. 20 | 21 | {MyComponentSource} 22 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/examples/code-cad-birdhouse.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | title: Code CAD Birdhouse 4 | --- 5 | 6 | import CodeBlock from '@theme/CodeBlock'; 7 | import {iframePath} from '../../examples/helpers.js'; 8 | import MyComponentSource from '!!raw-loader!../../examples/birdhouse.js'; 9 | 10 | This is the birdhouse used to [demonstrate many different code cad tools](https://github.com/Irev-Dev/curated-code-cad). 11 | 12 | We can have a look at the rendered shape. 13 | 14 | 15 | 16 | And here is what the code looks like. 17 | 18 | {MyComponentSource} 19 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/examples/design-watering-can.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 7 3 | title: Watering can 4 | --- 5 | 6 | import CodeBlock from '@theme/CodeBlock'; 7 | import {iframePath} from '../../examples/helpers.js'; 8 | import MyComponentSource from '!!raw-loader!../../examples/watering-can.js'; 9 | 10 | Note that this model is inspired by [Robert Bronwasser's](https://www.robertbronwasser.com/project/spring) watering can. The original implementation [comes from our community](https://github.com/sgenoud/replicad/discussions/35). You can see step by step explanation on 11 | [how to build it here](/docs/tutorial-making-a-watering-can/intro) 12 | 13 | 14 | 15 | And here is what the code looks like. 16 | 17 | {MyComponentSource} 18 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/examples/gridfinity.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | title: Gridfinity boxes generator 4 | --- 5 | 6 | import CodeBlock from '@theme/CodeBlock'; 7 | import {iframePath} from '../../examples/helpers.js'; 8 | import MyComponentSource from '!!raw-loader!../../examples/gridfinity.js'; 9 | 10 | From the [gridfinity system](https://www.youtube.com/watch?v=ra_9zU-mnl8). This 11 | is a fairly complex build (that uses blueprints and sweeps). 12 | 13 | 14 | 15 | And here is what the code looks like. 16 | 17 | {MyComponentSource} 18 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/examples/occt-bottle.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | title: Opencascade bottle 4 | --- 5 | 6 | import CodeBlock from '@theme/CodeBlock'; 7 | import {iframePath} from '../../examples/helpers.js'; 8 | import MyComponentSource from '!!raw-loader!../../examples/bottle.js'; 9 | 10 | If you have dabbled in opencascade you have read [the bottle tutorial](https://dev.opencascade.org/doc/overview/html/occt__tutorial.html). So what does it look like with replicad? 11 | 12 | We can have a look at the rendered shape. 13 | 14 | 15 | 16 | And here is what the code looks like. 17 | 18 | {MyComponentSource} 19 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/examples/projections.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 8 3 | title: Orthographic Projections 4 | --- 5 | 6 | import CodeBlock from "@theme/CodeBlock"; 7 | import MyComponentSource from "!!raw-loader!../../examples/projections.js"; 8 | 9 | Historically technical drawing consisted of a lot of orthographic projections. 10 | In some cases it still is a great method of communication for 3D models. 11 | 12 | Replicad support projections into a drawing (and therefore a projection as 13 | an SVG). This code follows the [first angle 14 | convention](https://en.wikipedia.org/wiki/Multiview_orthographic_projection#First-angle_projection). 15 | 16 | And here is what the code looks like. 17 | 18 | 19 | {MyComponentSource} 20 | 21 | 22 | ## Custom perspectives 23 | 24 | You can also have nice looking perspectives on your shapes. 25 | 26 | ```js withWorkbench 27 | const { drawProjection, ProjectionCamera, draw } = replicad; 28 | 29 | const prettyProjection = (shape) => { 30 | const bbox = shape.boundingBox; 31 | const center = bbox.center; 32 | const corner = [ 33 | bbox.center[0] + bbox.width, 34 | bbox.center[1] - bbox.height, 35 | bbox.center[2] + bbox.depth, 36 | ]; 37 | const camera = new ProjectionCamera(corner).lookAt(center); 38 | const { visible, hidden } = drawProjection(shape, camera); 39 | 40 | return [ 41 | { shape: hidden, strokeType: "dots", name: "Hidden Lines" }, 42 | { shape: visible, name: "Visible Lines" }, 43 | ]; 44 | }; 45 | 46 | const main = () => { 47 | const shape = draw() 48 | .vLine(-10) 49 | .hLine(-5) 50 | .vLine(15) 51 | .customCorner(2) 52 | .hLine(15) 53 | .vLine(-5) 54 | .close() 55 | .sketchOnPlane() 56 | .extrude(10) 57 | .chamfer(5, (e) => e.inPlane("XY", 10).containsPoint([10, 1, 10])); 58 | 59 | return prettyProjection(shape); 60 | }; 61 | ``` 62 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/examples/simple-vase.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | title: A simple vase 4 | --- 5 | 6 | import CodeBlock from '@theme/CodeBlock'; 7 | import {iframePath} from '../../examples/helpers.js'; 8 | import MyComponentSource from '!!raw-loader!../../examples/simpleVase.js'; 9 | 10 | This is a simple vase that shows off the smoothSpline API. 11 | 12 | We can have a look at the rendered shape. 13 | 14 | 15 | 16 | And here is what the code looks like. 17 | 18 | {MyComponentSource} 19 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/examples/wavy-vase.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | title: Wavy vases 4 | --- 5 | 6 | import CodeBlock from '@theme/CodeBlock'; 7 | import {iframePath} from '../../examples/helpers.js'; 8 | import MyComponentSource from '!!raw-loader!../../examples/wavyVase.js'; 9 | 10 | A more complex vase generator that uses twisted extrusion. 11 | 12 | We can have a look at the rendered shape. 13 | 14 | 15 | 16 | And here is what the code looks like. 17 | 18 | {MyComponentSource} 19 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # Introduction 6 | 7 | replicad is a library to build browser based 3D models with code. 8 | 9 | In the fashion of code cad, it allows you to build 3D models with code. 10 | 11 | But, unlike most CAD offerings, replicad is a library first. As an abstraction 12 | over opencascade, it gives developers the power to integrate it in their web 13 | application. 14 | 15 | So there are two ways you might be interested in this library: 16 | 17 | - how do I use it to build a 3D model? 18 | - how do I integrate replicad in my web application? 19 | 20 | ## Building a 3D model 21 | 22 | As of now, there is only a simple online workbench to play with the replicad 23 | API and draw a 3D model. 24 | 25 | You can use [the workbench](https://studio.replicad.xyz/workbench) to follow [the 26 | tutorial](/docs/tutorial-overview/using-the-workbench) 27 | 28 | ## Integrating replicad in your web application 29 | 30 | If you want to use replicad to build your own application, an editor, 31 | a generative 3D design, you can have a look at the [sample app](https://sample-app.replicad.xyz) and read the [documentation that is related to it](/docs/use-as-a-library) 32 | 33 | ## Another ressource, the replicad manual 34 | 35 | A member of the community has written a manual for replicad. This is 36 | a very in depth resource that will help you build models with replicad. You can 37 | find it [here](https://github.com/raydeleu/ReplicadManual/wiki) 38 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/other-ressources.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # Other ressources 6 | 7 | ## [Questions and community][forum] 8 | 9 | The community lives on [github 10 | forums][forum]. Feel free to ask or 11 | show off with your designs there. 12 | 13 | [forum]: https://github.com/sgenoud/replicad/discussions 14 | 15 | ## The [replicad manual][manual] 16 | 17 | A member of the community has written a manual for replicad. This is 18 | a very in depth resource that will help you build models with replicad. You can find it [here][manual]. 19 | 20 | [manual]: https://github.com/raydeleu/ReplicadManual/wiki 21 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/recipes/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Recipes", 3 | "position": 4 4 | } 5 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/recipes/fuse-all.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 10 3 | title: Fuse All 4 | --- 5 | 6 | You might find yourself in a situation where you have an array of shapes (2D or 7 | 3D) and you just want to fuse (or intersect) them all together. The following 8 | snippet just does this 9 | 10 | ```js 11 | const fuseAll = (shapes) => { 12 | let result = shapes[0]; 13 | shapes.slice(1).forEach((shape) => { 14 | result = result.fuse(shape); 15 | }); 16 | return result; 17 | }; 18 | ``` 19 | 20 | Let's show an example (also using polar copies). 21 | 22 | ```js withWorkbench 23 | const { drawCircle } = replicad; 24 | 25 | const polarCopies = (shape, count, radius) => { 26 | const base = shape.translate(0, radius); 27 | const angle = 360 / count; 28 | 29 | const copies = []; 30 | for (let i = 0; i < count; i++) { 31 | copies.push(base.clone().rotate(i * angle)); 32 | } 33 | return copies; 34 | }; 35 | 36 | const fuseAll = (shapes) => { 37 | let result = shapes[0]; 38 | shapes.slice(1).forEach((shape) => { 39 | result = result.fuse(shape); 40 | }); 41 | return result; 42 | }; 43 | 44 | function main() { 45 | return fuseAll(polarCopies(drawCircle(5), 5, 7)); 46 | } 47 | ``` 48 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/recipes/polar-array.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | title: Polar array 4 | --- 5 | 6 | Sometimes you want to copy a shape with a circular pattern. This is fairly easy 7 | to do with a little bit of javascript. 8 | 9 | ```js 10 | const polarCopies = (shape, count, radius) => { 11 | const base = shape.translate(0, radius); 12 | const angle = 360 / count; 13 | 14 | const copies = []; 15 | for (let i = 0; i < count; i++) { 16 | copies.push(base.clone().rotate(i * angle)); 17 | } 18 | return copies; 19 | }; 20 | ``` 21 | 22 | For the optimal use, take into account that we assume 23 | 24 | - that your original shape is centered at the origin 25 | - that you want to rotate around the origin 26 | - that you want to go all around the circle 27 | 28 | Let's show an example 29 | 30 | ```js withWorkbench 31 | const { drawCircle } = replicad; 32 | 33 | const polarCopies = (shape, count, radius) => { 34 | const base = shape.translate(0, radius); 35 | const angle = 360 / count; 36 | 37 | const copies = []; 38 | for (let i = 0; i < count; i++) { 39 | copies.push(base.clone().rotate(i * angle)); 40 | } 41 | return copies; 42 | }; 43 | 44 | function main() { 45 | return polarCopies(drawCircle(5), 5, 12); 46 | } 47 | ``` 48 | 49 | Note that this code works for both 2D and 3D cases. In the case of 3D, it will 50 | do the copies in the `XY` plane. 51 | 52 | ```js withWorkbench 53 | const { drawCircle } = replicad; 54 | 55 | const polarCopies = (shape, count, radius) => { 56 | const base = shape.translate(0, radius); 57 | const angle = 360 / count; 58 | 59 | const copies = []; 60 | for (let i = 0; i < count; i++) { 61 | copies.push(base.clone().rotate(i * angle)); 62 | } 63 | return copies; 64 | }; 65 | 66 | function main() { 67 | return polarCopies(drawCircle(5).sketchOnPlane().extrude(2), 5, 12); 68 | } 69 | ``` 70 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/recipes/why-recipes.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | title: Why recipes? 4 | --- 5 | 6 | There are some operations using replicad (or any CAD tool) that are at the same 7 | time very common - but have many different configurations and edge cases. 8 | 9 | In tools with a UI, this translates to very complex configuration boxes with 10 | multiple options that do not work well together most of the time. 11 | 12 | In a code CAD tool like replicad, this would translate to a function with a very 13 | complex signature. 14 | 15 | In some cases - the basic logic behind this functionality is fairly simple 16 | – but the different tweaks possible add the complexity. 17 | 18 | Instead of offering these as a standard library with a complex interface, 19 | I propose a recipe book of code that can be easily copied and tweaked to your 20 | particular needs. 21 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/tutorial-making-a-watering-can/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Making a watering can", 3 | "position": 3 4 | } 5 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/tutorial-making-a-watering-can/intro.mdx: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | title: Introduction 4 | --- 5 | 6 | import { iframePath } from "../../examples/helpers.js"; 7 | 8 | Do you like to follow steps to learn? This tutorial is for you - it allows you 9 | to build this plunge watering can by using the replicad APIs. 10 | 11 | 12 | 13 | Note that this model is inspired by [Robert 14 | Bronwasser's](https://www.robertbronwasser.com/project/spring/) watering can. The 15 | original implementation [comes from our 16 | community](https://github.com/sgenoud/replicad/discussions/35). 17 | 18 | If you want to follow along you can click on the workbench icon (next to copy!) 19 | in the code examples! 20 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/tutorial-overview/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Overview", 3 | "position": 2 4 | } 5 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/tutorial-overview/combinations.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | title: Combinations 4 | --- 5 | 6 | It is now time to introduce a way to combine shapes together, the main 7 | operations of constructive geometry, also known as the boolean operations. 8 | 9 | We will play with two shapes, a box and a cylinder. 10 | 11 | ## Pasting shapes together 12 | 13 | This is what we call the fuse operation: 14 | 15 | ```js withWorkbench 16 | const { drawRoundedRectangle, drawCircle } = replicad; 17 | const main = () => { 18 | const cylinder = drawCircle(20).sketchOnPlane().extrude(50); 19 | const box = drawRoundedRectangle(60, 90).sketchOnPlane().extrude(25); 20 | 21 | return box.fuse(cylinder); 22 | }; 23 | ``` 24 | 25 | ![the box and cylinder fused](/img/tutorial/combinations-1.png) 26 | 27 | ## Cutting one shape with another 28 | 29 | This is what we call the cut operation: 30 | 31 | ```js withWorkbench 32 | const { drawRoundedRectangle, drawCircle } = replicad; 33 | const main = () => { 34 | const cylinder = drawCircle(20).sketchOnPlane().extrude(50); 35 | const box = drawRoundedRectangle(60, 90).sketchOnPlane().extrude(25); 36 | 37 | return box.cut(cylinder); 38 | }; 39 | ``` 40 | 41 | ![the cylinder cut into the box](/img/tutorial/combinations-2.png) 42 | 43 | ## Intersecting two shapes 44 | 45 | For the intersection we will intersect the cylinder with itself. This creates 46 | a fun shape: 47 | 48 | ```js withWorkbench 49 | const { drawRoundedRectangle, drawCircle } = replicad; 50 | const main = () => { 51 | const cylinder = drawCircle(20).sketchOnPlane().extrude(50); 52 | const sideCylinder = cylinder.clone().rotate(90, [0, 0, 20], [1, 0, 0]); 53 | 54 | return sideCylinder.intersect(cylinder); 55 | }; 56 | ``` 57 | 58 | ![the cylinder intersecting itself](/img/tutorial/combinations-3.png) 59 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/tutorial-overview/sharing-models.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 8 3 | title: Sharing models 4 | --- 5 | 6 | Once you have created your models you might want to share them without having 7 | to build a full web app yourself. 8 | 9 | You can use the [replicad share application](https://studio.replicad.xyz/share) 10 | for this. 11 | 12 | First, you need to make your code available online easily. For instance, [create 13 | a gist](https://gist.github.com/) with your code, and the the url for the 14 | **raw** file (click on the `raw` button). 15 | 16 | Paste this link in the [share application](https://studio.replicad.xyz/share) 17 | input form. The link generated can then be shared with anyone. 18 | 19 | You can also include it in your blog with an iframe 20 | 21 | ```html 22 | 26 | ``` 27 | 28 | ## Parametric models 29 | 30 | You can easily define some parameters to configure your model. In your program, 31 | in addition to the `main` function, add a `defaultParams` object. These 32 | parameters will be passed to the main function as the second argument: 33 | 34 | ```js 35 | const defaultParams = { 36 | height: 85.0, 37 | width: 120.0, 38 | thickness: 2.0, 39 | holeDia: 50.0, 40 | hookHeight: 10.0, 41 | }; 42 | 43 | function main( 44 | { Sketcher, FaceFinder, EdgeFinder, sketchCircle }, 45 | { width, height, thickness, holeDia, hookHeight } 46 | ) { 47 | //... 48 | } 49 | ``` 50 | 51 | When sharing with the share application, a simple form will be offered to the 52 | user to change the parameters and customise the shape. 53 | 54 | If you play with it, share it with me 55 | ([@stevegenoud@toot.cafe](https://toot.cafe/@stevegenoud) or 56 | [@stevegenoud](https://twitter.com/stevegenoud))! 57 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/tutorial-overview/transformations.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | title: Transformations 4 | --- 5 | 6 | Now that we have a 3D shape it is time to move it around. Note that usually you 7 | will transform a shape in order to align it with another shape, for instance, or 8 | to have different versions of a same basic shape. 9 | 10 | For this part of the tutorial we will create a weird, non symmetrical shape: 11 | 12 | ```js withWorkbench 13 | const { draw } = replicad; 14 | const main = () => { 15 | const shape = draw() 16 | .movePointerTo([50, 50]) 17 | .hLine(-120) 18 | .vSagittaArc(-80, -20) 19 | .sagittaArc(100, 20, 60) 20 | .close() 21 | .sketchOnPlane() 22 | .extrude(100, { extrusionProfile: { profile: "linear", endFactor: 0.5 } }); 23 | 24 | return shape; 25 | }; 26 | ``` 27 | 28 | ![A weird shape to transform](/img/tutorial/transformations-1.png) 29 | 30 | ## Let's move stuff around 31 | 32 | This is fairly straightforward. We have a shape, we translate it on the 33 | axes (or on a vector) 34 | 35 | ```js 36 | return shape.translateZ(20); 37 | ``` 38 | 39 | You can see the 2cm between the base of the shape and the grid 40 | 41 | ## Let's rotate this thing 42 | 43 | ```js 44 | return shape.rotate(45, [0, 0, 0], [1, 0, 0]); 45 | ``` 46 | 47 | The shape is rotated 45 degrees around an axis going through the origin and in the 48 | X direction. Try to move these points around to see what is going on. 49 | 50 | ## Finally mirroring 51 | 52 | ```js 53 | return shape.mirror("XZ"); 54 | ``` 55 | 56 | The mirror image of the shape! 57 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/tutorial-overview/using-the-workbench.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | title: The workbench 4 | --- 5 | 6 | So let's use [the workbench](https://studio.replicad.xyz/workbench)! 7 | 8 | ## A first example 9 | 10 | Let's draw using a basic replicad script. Do not worry about the details for 11 | now. 12 | 13 | Open [the workbench](https://studio.replicad.xyz/workbench) in a new tab 14 | and copy this: 15 | 16 | ```js withWorkbench 17 | const { drawEllipse } = replicad; 18 | const main = () => { 19 | return drawEllipse(20, 30).sketchOnPlane().extrude(50).fillet(2); 20 | }; 21 | ``` 22 | 23 | You should see something like that: 24 | 25 | ![Your first 3D model](/img/tutorial/first-model.png) 26 | 27 | Congratulations, you have built your first model with replicad! 28 | 29 | :::tip 30 | 31 |
33 | 34 |
35 | 36 | You can click on the `Open in workbench` button in most code samples to see (and 37 | edit them) within the workbench. 38 | 39 |
40 | 41 |
42 | The workbench button 43 |
44 |
45 | 46 | ::: 47 | 48 | ## Direct links 49 | 50 | You can even open a model directly in the workbench if you click on the `Open in workbench` button next to the copy button! 51 | 52 | ## Working with local files 53 | 54 | If you prefer to use your editor of choice it is also possible. 55 | 56 | Create a file (`model1.js` for instance) somewhere on your disk, and then you 57 | can point the workbench to that file using the reload menu (left of the menu bar 58 | of the editor). 59 | 60 | Unfortunately, in order to have all the file reloading abilities you will need 61 | to use Chrome (or Edge). The load from disk button does not appear in Firefox 62 | and Safari. 63 | -------------------------------------------------------------------------------- /packages/replicad-docs/docs/use-as-a-library.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 6 3 | --- 4 | 5 | # replicad as a library 6 | 7 | At its core, replicad is just a library. You can then create your own viewer, 8 | editor, configurator on top of it. 9 | 10 | In order to show what can be done in the most simple way, you can find a sample 11 | app here: [https://sample-app.replicad.xyz](https://sample-app.replicad.xyz). 12 | 13 | ## Display of the model 14 | 15 | With replicad you can easily export an STL (or STEP) file to be opened in 16 | another application. Nevertheless displaying a model in your page tends to be 17 | nicer. 18 | 19 | For this you will need to use a 3D library. For instance, replicad has 20 | [helpers](https://www.npmjs.com/package/replicad-threejs-helper) to integrate 21 | with [threejs](https://threejs.org/). 22 | 23 | ## opencascade.js and webassembly 24 | 25 | Most of the complexity in using replicad as a library is that it depends on 26 | a webassembly module, 27 | [opencascadejs](https://github.com/donalffons/opencascade.js), and the tooling 28 | around WASM is not always easy to use. 29 | 30 | Additionally, you should load the webassembly code from opencascadejs (or the 31 | [replicad custom build](https://www.npmjs.com/package/replicad-opencascadejs)) 32 | in a webworker. The model computation can take some time and the parallelism of 33 | a worker will allow you to offer a reactive interface during the computation. 34 | 35 | ### Injecting opencascadejs 36 | 37 | The important bit you need to do to have replicad work is that you need to 38 | inject an instance of opencascadejs at initialisation. 39 | 40 | You can have a look at the initialisation in [the sample 41 | app](https://github.com/sgenoud/replicad/blob/main/packages/replicad-app-example/src/worker.js#L11): 42 | 43 | ```js 44 | let loaded = false; 45 | const init = async () => { 46 | if (loaded) return Promise.resolve(true); 47 | 48 | const OC = await opencascade({ 49 | locateFile: () => opencascadeWasm, 50 | }); 51 | 52 | loaded = true; 53 | setOC(OC); 54 | 55 | return true; 56 | }; 57 | const started = init(); 58 | ``` 59 | 60 | In addition to the [opencascadejs 61 | boilerplate](https://github.com/donalffons/opencascade.js), we use the `setOC` 62 | function. This will inject the instance of the opencascade library into 63 | replicad. 64 | 65 | Once this is done, replicad will work. 66 | -------------------------------------------------------------------------------- /packages/replicad-docs/examples/birdhouse.js: -------------------------------------------------------------------------------- 1 | const defaultParams = { 2 | height: 85.0, 3 | width: 120.0, 4 | thickness: 2.0, 5 | holeDia: 50.0, 6 | hookHeight: 10.0, 7 | }; 8 | 9 | const { drawCircle, draw, makePlane } = replicad; 10 | 11 | function main( 12 | r, 13 | { width: inputWidth, height, thickness, holeDia, hookHeight } 14 | ) { 15 | const length = inputWidth; 16 | const width = inputWidth * 0.9; 17 | 18 | const tobleroneShape = draw([-width / 2, 0]) 19 | .lineTo([0, height]) 20 | .lineTo([width / 2, 0]) 21 | .close() 22 | .sketchOnPlane("XZ", -length / 2) 23 | .extrude(length) 24 | .shell(thickness, (f) => f.parallelTo("XZ")) 25 | .fillet(thickness / 2, (e) => 26 | e 27 | .inDirection("Y") 28 | .either([(f) => f.inPlane("XY"), (f) => f.inPlane("XY", height)]) 29 | ); 30 | 31 | const hole = drawCircle(holeDia / 2) 32 | .sketchOnPlane(makePlane("YZ").translate([-length / 2, 0, height / 3])) 33 | .extrude(length); 34 | 35 | const base = tobleroneShape.cut(hole); 36 | const body = base.clone().fuse(base.rotate(90)); 37 | 38 | const hookWidth = length / 2; 39 | const hook = draw([0, hookHeight / 2]) 40 | .smoothSplineTo([hookHeight / 2, 0], -45) 41 | .lineTo([hookWidth / 2, 0]) 42 | .line(-hookWidth / 4, hookHeight / 2) 43 | .smoothSplineTo([0, hookHeight], { 44 | endTangent: 180, 45 | endFactor: 0.6, 46 | }) 47 | .closeWithMirror() 48 | .sketchOnPlane("XZ") 49 | .extrude(thickness) 50 | .translate([0, thickness / 2, height - thickness / 2]); 51 | 52 | return body.fuse(hook); 53 | } 54 | -------------------------------------------------------------------------------- /packages/replicad-docs/examples/bottle.js: -------------------------------------------------------------------------------- 1 | const defaultParams = { 2 | width: 50, 3 | height: 70, 4 | thickness: 30, 5 | }; 6 | 7 | const { draw, makeCylinder, makeOffset, FaceFinder } = replicad; 8 | 9 | const main = ( 10 | r, 11 | { width: myWidth, height: myHeight, thickness: myThickness } 12 | ) => { 13 | let shape = draw([-myWidth / 2, 0]) 14 | .vLine(-myThickness / 4) 15 | .threePointsArc(myWidth, 0, myWidth / 2, -myThickness / 4) 16 | .vLine(myThickness / 4) 17 | .closeWithMirror() 18 | .sketchOnPlane() 19 | .extrude(myHeight) 20 | .fillet(myThickness / 12); 21 | 22 | const myNeckRadius = myThickness / 4; 23 | const myNeckHeight = myHeight / 10; 24 | const neck = makeCylinder( 25 | myNeckRadius, 26 | myNeckHeight, 27 | [0, 0, myHeight], 28 | [0, 0, 1] 29 | ); 30 | 31 | shape = shape.fuse(neck); 32 | 33 | shape = shape.shell(myThickness / 50, (f) => 34 | f.inPlane("XY", [0, 0, myHeight + myNeckHeight]) 35 | ); 36 | 37 | const neckFace = new FaceFinder() 38 | .containsPoint([0, myNeckRadius, myHeight]) 39 | .ofSurfaceType("CYLINDRE") 40 | .find(shape.clone(), { unique: true }); 41 | 42 | const bottomThreadFace = makeOffset(neckFace, -0.01 * myNeckRadius).faces[0]; 43 | const baseThreadSketch = draw([0.75, 0.25]) 44 | .halfEllipse(2, 0.5, 0.1) 45 | .close() 46 | .sketchOnFace(bottomThreadFace, "bounds"); 47 | 48 | const topThreadFace = makeOffset(neckFace, 0.05 * myNeckRadius).faces[0]; 49 | const topThreadSketch = draw([0.75, 0.25]) 50 | .halfEllipse(2, 0.5, 0.05) 51 | .close() 52 | .sketchOnFace(topThreadFace, "bounds"); 53 | 54 | const thread = baseThreadSketch.loftWith(topThreadSketch); 55 | 56 | return shape.fuse(thread); 57 | }; 58 | -------------------------------------------------------------------------------- /packages/replicad-docs/examples/cadquery-cycloidal-gear.js: -------------------------------------------------------------------------------- 1 | const { drawCircle, drawParametricFunction } = replicad; 2 | 3 | const hypocycloid = (t, r1, r2) => { 4 | return [ 5 | (r1 - r2) * Math.cos(t) + r2 * Math.cos((r1 / r2) * t - t), 6 | (r1 - r2) * Math.sin(t) + r2 * Math.sin(-((r1 / r2) * t - t)), 7 | ]; 8 | }; 9 | 10 | const epicycloid = (t, r1, r2) => { 11 | return [ 12 | (r1 + r2) * Math.cos(t) - r2 * Math.cos((r1 / r2) * t + t), 13 | (r1 + r2) * Math.sin(t) - r2 * Math.sin((r1 / r2) * t + t), 14 | ]; 15 | }; 16 | 17 | const gear = (t, r1 = 4, r2 = 1) => { 18 | if ((-1) ** (1 + Math.floor((t / 2 / Math.PI) * (r1 / r2))) < 0) 19 | return epicycloid(t, r1, r2); 20 | else return hypocycloid(t, r1, r2); 21 | }; 22 | 23 | const defaultParams = { 24 | height: 15, 25 | }; 26 | 27 | const main = (r, { height }) => { 28 | const base = drawParametricFunction((t) => gear(2 * Math.PI * t, 6, 1)) 29 | .sketchOnPlane() 30 | .extrude(height, { twistAngle: 90 }); 31 | 32 | const hole = drawCircle(2).sketchOnPlane().extrude(height); 33 | 34 | return base.cut(hole); 35 | }; 36 | -------------------------------------------------------------------------------- /packages/replicad-docs/examples/helpers.js: -------------------------------------------------------------------------------- 1 | export const BASE_PATH = 2 | "https://raw.githubusercontent.com/sgenoud/replicad/main/packages/replicad-docs/examples/"; 3 | 4 | export const iframePath = (filename) => { 5 | return `https://studio.replicad.xyz/share/${encodeURIComponent( 6 | BASE_PATH + filename 7 | )}`; 8 | }; 9 | -------------------------------------------------------------------------------- /packages/replicad-docs/examples/projections.js: -------------------------------------------------------------------------------- 1 | const { drawProjection, draw } = replicad; 2 | 3 | /* This follow the "first angle projection" convention 4 | * https://en.wikipedia.org/wiki/Multiview_orthographic_projection#First-angle_projection 5 | */ 6 | const descriptiveGeom = (shape) => { 7 | return [ 8 | { shape, name: "Shape to project" }, 9 | { shape: drawProjection(shape, "front").visible, name: "Front" }, 10 | { shape: drawProjection(shape, "back").visible, name: "Back" }, 11 | { shape: drawProjection(shape, "top").visible, name: "Top" }, 12 | { shape: drawProjection(shape, "bottom").visible, name: "Bottom" }, 13 | { shape: drawProjection(shape, "left").visible, name: "Left" }, 14 | { shape: drawProjection(shape, "right").visible, name: "Right" }, 15 | ]; 16 | }; 17 | 18 | const main = () => { 19 | // This shape looks different from every angle 20 | const shape = draw() 21 | .vLine(-10) 22 | .hLine(-5) 23 | .vLine(15) 24 | .customCorner(2) 25 | .hLine(15) 26 | .vLine(-5) 27 | .close() 28 | .sketchOnPlane() 29 | .extrude(10) 30 | .chamfer(5, (e) => e.inPlane("XY", 10).containsPoint([10, 1, 10])); 31 | 32 | return descriptiveGeom(shape); 33 | }; 34 | -------------------------------------------------------------------------------- /packages/replicad-docs/examples/simpleVase.js: -------------------------------------------------------------------------------- 1 | const { draw } = replicad; 2 | 3 | const defaultParams = { 4 | height: 100, 5 | baseWidth: 20, 6 | wallThickness: 5, 7 | lowerCircleRadius: 1.5, 8 | lowerCirclePosition: 0.25, 9 | higherCircleRadius: 0.75, 10 | higherCirclePosition: 0.75, 11 | topRadius: 0.9, 12 | topFillet: true, 13 | bottomHeavy: true, 14 | }; 15 | 16 | const main = ( 17 | r, 18 | { 19 | height, 20 | baseWidth, 21 | wallThickness, 22 | lowerCirclePosition, 23 | lowerCircleRadius, 24 | higherCircleRadius, 25 | higherCirclePosition, 26 | topRadius, 27 | topFillet, 28 | bottomHeavy, 29 | } 30 | ) => { 31 | const splinesConfig = [ 32 | { position: lowerCirclePosition, radius: lowerCircleRadius }, 33 | { 34 | position: higherCirclePosition, 35 | radius: higherCircleRadius, 36 | startFactor: bottomHeavy ? 3 : 1, 37 | }, 38 | { position: 1, radius: topRadius, startFactor: bottomHeavy ? 3 : 1 }, 39 | ]; 40 | 41 | const sketchVaseProfile = draw().hLine(baseWidth); 42 | 43 | splinesConfig.forEach(({ position, radius, startFactor, endFactor }) => { 44 | sketchVaseProfile.smoothSplineTo([baseWidth * radius, height * position], { 45 | endTangent: [0, 1], 46 | startFactor, 47 | endFactor, 48 | }); 49 | }); 50 | 51 | let vase = sketchVaseProfile 52 | .lineTo([0, height]) 53 | .close() 54 | .sketchOnPlane("XZ") 55 | .revolve(); 56 | 57 | if (wallThickness) { 58 | vase = vase.shell(wallThickness, (f) => f.containsPoint([0, 0, height])); 59 | } 60 | 61 | if (topFillet) { 62 | vase = vase.fillet(wallThickness / 3, (e) => e.inPlane("XY", height)); 63 | } 64 | 65 | return vase; 66 | }; 67 | -------------------------------------------------------------------------------- /packages/replicad-docs/examples/watering-can.js: -------------------------------------------------------------------------------- 1 | const { makePlane, makeCylinder, draw, drawCircle } = replicad; 2 | 3 | const defaultParams = {}; 4 | 5 | const main = () => { 6 | // Building the body 7 | const profile = draw() 8 | .hLine(20) 9 | .line(10, 5) 10 | .vLine(3) 11 | .lineTo([8, 100]) 12 | .hLine(-8) 13 | .close(); 14 | 15 | const body = profile.sketchOnPlane("XZ").revolve([0, 0, 1]); 16 | 17 | // Building the filler 18 | const topPlane = makePlane().pivot(-20, "Y").translate([-35, 0, 135]); 19 | const topCircle = drawCircle(12).sketchOnPlane(topPlane); 20 | 21 | const middleCircle = drawCircle(8).sketchOnPlane("XY", 100); 22 | 23 | const bottomPlane = makePlane().pivot(20, "Y").translateZ(80); 24 | const bottomCircle = drawCircle(9).sketchOnPlane(bottomPlane); 25 | 26 | const filler = topCircle.loftWith([middleCircle, bottomCircle], { 27 | ruled: false, 28 | }); 29 | 30 | // Building the spout 31 | const spout = makeCylinder(5, 70) 32 | .translateZ(100) 33 | .rotate(45, [0, 0, 100], [0, 1, 0]); 34 | 35 | let wateringCan = body 36 | .fuse(filler) 37 | .fillet(30, (e) => e.inPlane("XY", 100)) 38 | .fuse(spout) 39 | .fillet(10, (e) => e.inBox([20, 20, 100], [-20, -20, 120])); 40 | 41 | const spoutOpening = [ 42 | Math.cos((45 * Math.PI) / 180) * 70, 43 | 0, 44 | 100 + Math.sin((45 * Math.PI) / 180) * 70, 45 | ]; 46 | 47 | wateringCan = wateringCan.shell(-1, (face) => 48 | face.either([ 49 | (f) => f.containsPoint(spoutOpening), 50 | (f) => f.inPlane(topPlane), 51 | ]) 52 | ); 53 | 54 | return { 55 | shape: wateringCan, 56 | name: "Watering Can", 57 | }; 58 | }; 59 | -------------------------------------------------------------------------------- /packages/replicad-docs/examples/wavyVase.js: -------------------------------------------------------------------------------- 1 | const { drawCircle, drawPolysides, polysideInnerRadius } = replicad; 2 | 3 | const defaultParams = { 4 | height: 150, 5 | radius: 40, 6 | sidesCount: 12, 7 | sideRadius: -2, 8 | sideTwist: 6, 9 | endFactor: 1.5, 10 | topFillet: 0, 11 | bottomFillet: 5, 12 | 13 | holeMode: 1, 14 | wallThickness: 2, 15 | }; 16 | 17 | const main = ( 18 | r, 19 | { 20 | height, 21 | radius, 22 | sidesCount, 23 | sideRadius, 24 | sideTwist, 25 | endFactor, 26 | topFillet, 27 | bottomFillet, 28 | holeMode, 29 | wallThickness, 30 | } 31 | ) => { 32 | const extrusionProfile = endFactor 33 | ? { profile: "s-curve", endFactor } 34 | : undefined; 35 | const twistAngle = (360 / sidesCount) * sideTwist; 36 | 37 | let shape = drawPolysides(radius, sidesCount, -sideRadius) 38 | .sketchOnPlane() 39 | .extrude(height, { 40 | twistAngle, 41 | extrusionProfile, 42 | }); 43 | 44 | if (bottomFillet) { 45 | shape = shape.fillet(bottomFillet, (e) => e.inPlane("XY")); 46 | } 47 | 48 | if (holeMode === 1 || holeMode === 2) { 49 | const holeHeight = height - wallThickness; 50 | 51 | let hole; 52 | if (holeMode === 1) { 53 | const insideRadius = 54 | polysideInnerRadius(radius, sidesCount, sideRadius) - wallThickness; 55 | 56 | hole = drawCircle(insideRadius).sketchOnPlane().extrude(holeHeight, { 57 | extrusionProfile, 58 | }); 59 | 60 | shape = shape.cut( 61 | hole 62 | .fillet( 63 | Math.max(wallThickness / 3, bottomFillet - wallThickness), 64 | (e) => e.inPlane("XY") 65 | ) 66 | .translate([0, 0, wallThickness]) 67 | ); 68 | } else if (holeMode === 2) { 69 | shape = shape.shell(wallThickness, (f) => f.inPlane("XY", height)); 70 | } 71 | } 72 | 73 | if (topFillet) { 74 | shape = shape.fillet(topFillet, (e) => e.inPlane("XY", height)); 75 | } 76 | return shape; 77 | }; 78 | -------------------------------------------------------------------------------- /packages/replicad-docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replicad-docs", 3 | "version": "0.19.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "build-typedoc": "docusaurus generate-typedoc", 8 | "start": "TYPEDOC_WATCH=true docusaurus start --port 3333", 9 | "build": "TYPEDOC_WATCH=false docusaurus build", 10 | "swizzle": "docusaurus swizzle", 11 | "deploy": "docusaurus deploy", 12 | "clear": "docusaurus clear", 13 | "serve": "docusaurus serve", 14 | "write-translations": "docusaurus write-translations", 15 | "write-heading-ids": "docusaurus write-heading-ids" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "^3.7.0", 19 | "@docusaurus/preset-classic": "^3.7.0", 20 | "@docusaurus/theme-classic": "^3.7.0", 21 | "@docusaurus/theme-common": "^3.7.0", 22 | "@docusaurus/types": "^3.7.0", 23 | "@mdx-js/react": "^3.1.0", 24 | "@svgr/webpack": "^5.5.0", 25 | "clsx": "^2.1.1", 26 | "copy-text-to-clipboard": "^3.2.0", 27 | "file-loader": "^6.2.0", 28 | "jszip": "^3.10.1", 29 | "prism-react-renderer": "^2.4.1", 30 | "raw-loader": "^4.0.2", 31 | "react": "^19.0.1", 32 | "react-dom": "^19.0.1", 33 | "url-loader": "^4.1.1" 34 | }, 35 | "browserslist": { 36 | "production": [ 37 | ">0.5%", 38 | "not dead", 39 | "not op_mini all" 40 | ], 41 | "development": [ 42 | "last 1 chrome version", 43 | "last 1 firefox version", 44 | "last 1 safari version" 45 | ] 46 | }, 47 | "devDependencies": { 48 | "docusaurus-plugin-typedoc": "^1.3.0", 49 | "typedoc": "^0.28.2", 50 | "typedoc-plugin-markdown": "^4.6.2", 51 | "typescript": "^5.1.4" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /packages/replicad-docs/sidebars.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Creating a sidebar enables you to: 3 | - create an ordered group of docs 4 | - render a sidebar for each doc of that group 5 | - provide next/previous navigation 6 | 7 | The sidebars can be generated from the filesystem, or explicitly defined here. 8 | 9 | Create as many sidebars as you want. 10 | */ 11 | 12 | // @ts-check 13 | 14 | /** @type {import('@docusaurus/plugin-content-docs').SidebarsConfig} */ 15 | const sidebars = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | 18 | typedocSidebar: [ 19 | "intro", 20 | { 21 | "Getting Started": [ 22 | { type: "autogenerated", dirName: "tutorial-overview" }, 23 | ], 24 | }, 25 | { 26 | "Making a watering can": [ 27 | { type: "autogenerated", dirName: "tutorial-making-a-watering-can" }, 28 | ], 29 | }, 30 | { Recipes: [{ type: "autogenerated", dirName: "recipes" }] }, 31 | { Examples: [{ type: "autogenerated", dirName: "examples" }] }, 32 | "use-as-a-library", 33 | "other-ressources", 34 | { 35 | type: "category", 36 | label: "Replicad API", 37 | link: { 38 | type: "doc", 39 | id: "api/index", 40 | }, 41 | items: require("./docs/api/typedoc-sidebar.cjs"), 42 | }, 43 | ], 44 | }; 45 | 46 | module.exports = sidebars; 47 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/components/HomepageFeatures.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import styles from "./HomepageFeatures.module.css"; 4 | 5 | const FeatureList = [ 6 | { 7 | title: "A simple API", 8 | Svg: require("../../static/img/tools.svg").default, 9 | description: ( 10 | <> 11 | You can easily sketch, give shape and then modify your shape with a 12 | streamlined API inspired by{" "} 13 | cadquery and{" "} 14 | cascade studio. 15 | 16 | ), 17 | }, 18 | { 19 | title: "In the modern javascript ecosystem", 20 | Svg: require("../../static/img/browser.svg").default, 21 | description: ( 22 | <> 23 | replicad is just javascript, you have the benefit of all the libraries 24 | you love, the tooling you are used to and it runs in all modern 25 | browsers. 26 | 27 | ), 28 | }, 29 | { 30 | title: "Powered by Opencascade", 31 | Svg: require("../../static/img/gear.svg").default, 32 | description: ( 33 | <> 34 | You get all the nice feature of a battle tested kernel: STEP export, 35 | easy fillets and chamfers,... 36 | 37 | ), 38 | }, 39 | ]; 40 | 41 | function Feature({ Svg, title, description }) { 42 | return ( 43 |
44 |
45 | 46 |
47 |
48 |

{title}

49 |

{description}

50 |
51 |
52 | ); 53 | } 54 | 55 | export default function HomepageFeatures() { 56 | return ( 57 |
58 |
59 |
60 | {FeatureList.map((props, idx) => ( 61 | 62 | ))} 63 |
64 |
65 |
66 | ); 67 | } 68 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/components/HomepageFeatures.module.css: -------------------------------------------------------------------------------- 1 | .features { 2 | display: flex; 3 | align-items: center; 4 | padding: 2rem 0; 5 | width: 100%; 6 | } 7 | 8 | .featureSvg { 9 | height: 200px; 10 | width: 200px; 11 | } 12 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/css/custom.css: -------------------------------------------------------------------------------- 1 | /** 2 | * Any CSS included here will be global. The classic template 3 | * bundles Infima by default. Infima is a CSS framework designed to 4 | * work well for content-centric websites. 5 | */ 6 | 7 | /* You can override the default Infima variables here. */ 8 | 9 | @font-face { 10 | font-family: "HKGrotesk"; 11 | font-weight: 500; 12 | src: url("/fonts/HKGrotesk-Regular.woff2") format("woff2"); 13 | } 14 | 15 | @font-face { 16 | font-family: "HKGrotesk"; 17 | font-weight: 300; 18 | src: url("/fonts/HKGrotesk-Light.woff2") format("woff2"); 19 | } 20 | 21 | @font-face { 22 | font-family: "HKGrotesk"; 23 | font-weight: bold; 24 | src: url("/fonts/HKGrotesk-Bold.woff2") format("woff2"); 25 | } 26 | 27 | :root { 28 | 29 | --ifm-font-family-base: HKGrotesk, system-ui, -apple-system, BlinkMacSystemFont, 30 | 'Segoe UI', Helvetica, Arial, sans-serif, 'Apple Color Emoji', 31 | 'Segoe UI Emoji', 'Segoe UI Symbol'; 32 | font-weight: 300; 33 | 34 | --ifm-color-primary: #5a8296; 35 | --ifm-color-primary-dark: #517587; 36 | --ifm-color-primary-darker: #4d6e80; 37 | --ifm-color-primary-darkest: #3f5b69; 38 | --ifm-color-primary-light: #658ea3; 39 | --ifm-color-primary-lighter: #6d94a7; 40 | --ifm-color-primary-lightest: #83a4b5; 41 | } 42 | 43 | .docusaurus-highlight-code-line { 44 | background-color: rgba(0, 0, 0, 0.1); 45 | display: block; 46 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 47 | padding: 0 var(--ifm-pre-padding); 48 | } 49 | 50 | html[data-theme='dark'] .docusaurus-highlight-code-line { 51 | background-color: rgba(0, 0, 0, 0.3); 52 | } 53 | 54 | iframe { 55 | box-sizing: border-box; 56 | width: 100%; 57 | height: 500px; 58 | border-style: none; 59 | } 60 | 61 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/pages/index.js: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import clsx from "clsx"; 3 | import Layout from "@theme/Layout"; 4 | import Link from "@docusaurus/Link"; 5 | import useDocusaurusContext from "@docusaurus/useDocusaurusContext"; 6 | import styles from "./index.module.css"; 7 | import HomepageFeatures from "../components/HomepageFeatures"; 8 | 9 | function HomepageHeader() { 10 | const { siteConfig } = useDocusaurusContext(); 11 | return ( 12 |
13 |
14 |

{siteConfig.title}

15 |

{siteConfig.tagline}

16 |
17 | 21 | Documentation 22 | 23 | 27 | Examples 28 | 29 |
30 |
31 |
32 | ); 33 | } 34 | 35 | export default function Home() { 36 | const { siteConfig } = useDocusaurusContext(); 37 | return ( 38 | 39 | 40 |
41 | 42 |
43 |
44 | ); 45 | } 46 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/pages/index.module.css: -------------------------------------------------------------------------------- 1 | /** 2 | * CSS files with the .module.css suffix will be treated as CSS modules 3 | * and scoped locally. 4 | */ 5 | 6 | .heroBanner { 7 | padding: 4rem 0; 8 | text-align: center; 9 | position: relative; 10 | overflow: hidden; 11 | } 12 | 13 | @media screen and (max-width: 966px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | 25 | .buttons > :not(:last-child) { 26 | margin-right: 1em; 27 | } 28 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/pages/markdown-page.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: Markdown page example 3 | --- 4 | 5 | # Markdown page example 6 | 7 | You don't need React to write simple standalone pages. 8 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/theme/CodeBlock/Container/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import {ThemeClassNames, usePrismTheme} from '@docusaurus/theme-common'; 4 | import {getPrismCssVariables} from '@docusaurus/theme-common/internal'; 5 | import styles from './styles.module.css'; 6 | export default function CodeBlockContainer({as: As, ...props}) { 7 | const prismTheme = usePrismTheme(); 8 | const prismCssVariables = getPrismCssVariables(prismTheme); 9 | return ( 10 | 20 | ); 21 | } 22 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/theme/CodeBlock/Container/styles.module.css: -------------------------------------------------------------------------------- 1 | .codeBlockContainer { 2 | background: var(--prism-background-color); 3 | color: var(--prism-color); 4 | margin-bottom: var(--ifm-leading); 5 | box-shadow: var(--ifm-global-shadow-lw); 6 | border-radius: var(--ifm-code-border-radius); 7 | } 8 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/theme/CodeBlock/Content/Element.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import Container from '@theme/CodeBlock/Container'; 4 | import styles from './styles.module.css'; 5 | //
 tags in markdown map to CodeBlocks. They may contain JSX children. When
 6 | // the children is not a simple string, we just return a styled block without
 7 | // actually highlighting.
 8 | export default function CodeBlockJSX({children, className}) {
 9 |   return (
10 |     
14 |       {children}
15 |     
16 |   );
17 | }
18 | 


--------------------------------------------------------------------------------
/packages/replicad-docs/src/theme/CodeBlock/Content/styles.module.css:
--------------------------------------------------------------------------------
 1 | .codeBlockContent {
 2 |   position: relative;
 3 |   /* rtl:ignore */
 4 |   direction: ltr;
 5 |   border-radius: inherit;
 6 | }
 7 | 
 8 | .codeBlockTitle {
 9 |   border-bottom: 1px solid var(--ifm-color-emphasis-300);
10 |   font-size: var(--ifm-code-font-size);
11 |   font-weight: 500;
12 |   padding: 0.75rem var(--ifm-pre-padding);
13 |   border-top-left-radius: inherit;
14 |   border-top-right-radius: inherit;
15 | }
16 | 
17 | .codeBlock {
18 |   --ifm-pre-background: var(--prism-background-color);
19 |   margin: 0;
20 |   padding: 0;
21 | }
22 | 
23 | .codeBlockTitle + .codeBlockContent .codeBlock {
24 |   border-top-left-radius: 0;
25 |   border-top-right-radius: 0;
26 | }
27 | 
28 | .codeBlockStandalone {
29 |   padding: 0;
30 | }
31 | 
32 | .codeBlockLines {
33 |   font: inherit;
34 |   /* rtl:ignore */
35 |   float: left;
36 |   min-width: 100%;
37 |   padding: var(--ifm-pre-padding);
38 | }
39 | 
40 | .codeBlockLinesWithNumbering {
41 |   display: table;
42 |   padding: var(--ifm-pre-padding) 0;
43 | }
44 | 
45 | @media print {
46 |   .codeBlockLines {
47 |     white-space: pre-wrap;
48 |   }
49 | }
50 | 
51 | .buttonGroup {
52 |   display: flex;
53 |   column-gap: 0.2rem;
54 |   position: absolute;
55 |   /* rtl:ignore */
56 |   right: calc(var(--ifm-pre-padding) / 2);
57 |   top: calc(var(--ifm-pre-padding) / 2);
58 | }
59 | 
60 | .buttonGroup button {
61 |   display: flex;
62 |   align-items: center;
63 |   background: var(--prism-background-color);
64 |   color: var(--prism-color);
65 |   border: 1px solid var(--ifm-color-emphasis-300);
66 |   border-radius: var(--ifm-global-radius);
67 |   padding: 0.4rem;
68 |   line-height: 0;
69 |   transition: opacity var(--ifm-transition-fast) ease-in-out;
70 |   opacity: 0;
71 | }
72 | 
73 | .buttonGroup button:focus-visible,
74 | .buttonGroup button:hover {
75 |   opacity: 1 !important;
76 | }
77 | 
78 | :global(.theme-code-block:hover) .buttonGroup button {
79 |   opacity: 0.4;
80 | }
81 | 


--------------------------------------------------------------------------------
/packages/replicad-docs/src/theme/CodeBlock/CopyButton/index.js:
--------------------------------------------------------------------------------
 1 | import React, {useCallback, useState, useRef, useEffect} from 'react';
 2 | import clsx from 'clsx';
 3 | import copy from 'copy-text-to-clipboard';
 4 | import {translate} from '@docusaurus/Translate';
 5 | import IconCopy from '@theme/Icon/Copy';
 6 | import IconSuccess from '@theme/Icon/Success';
 7 | import styles from './styles.module.css';
 8 | export default function CopyButton({code, className}) {
 9 |   const [isCopied, setIsCopied] = useState(false);
10 |   const copyTimeout = useRef(undefined);
11 |   const handleCopyCode = useCallback(() => {
12 |     copy(code);
13 |     setIsCopied(true);
14 |     copyTimeout.current = window.setTimeout(() => {
15 |       setIsCopied(false);
16 |     }, 1000);
17 |   }, [code]);
18 |   useEffect(() => () => window.clearTimeout(copyTimeout.current), []);
19 |   return (
20 |     
52 |   );
53 | }
54 | 


--------------------------------------------------------------------------------
/packages/replicad-docs/src/theme/CodeBlock/CopyButton/styles.module.css:
--------------------------------------------------------------------------------
 1 | :global(.theme-code-block:hover) .copyButtonCopied {
 2 |   opacity: 1 !important;
 3 | }
 4 | 
 5 | .copyButtonIcons {
 6 |   position: relative;
 7 |   width: 1.125rem;
 8 |   height: 1.125rem;
 9 | }
10 | 
11 | .copyButtonIcon,
12 | .copyButtonSuccessIcon {
13 |   position: absolute;
14 |   top: 0;
15 |   left: 0;
16 |   fill: currentColor;
17 |   opacity: inherit;
18 |   width: inherit;
19 |   height: inherit;
20 |   transition: all var(--ifm-transition-fast) ease;
21 | }
22 | 
23 | .copyButtonSuccessIcon {
24 |   top: 50%;
25 |   left: 50%;
26 |   transform: translate(-50%, -50%) scale(0.33);
27 |   opacity: 0;
28 |   color: #00d600;
29 | }
30 | 
31 | .copyButtonCopied .copyButtonIcon {
32 |   transform: scale(0.33);
33 |   opacity: 0;
34 | }
35 | 
36 | .copyButtonCopied .copyButtonSuccessIcon {
37 |   transform: translate(-50%, -50%) scale(1);
38 |   opacity: 1;
39 |   transition-delay: 0.075s;
40 | }
41 | 


--------------------------------------------------------------------------------
/packages/replicad-docs/src/theme/CodeBlock/Line/index.js:
--------------------------------------------------------------------------------
 1 | import React from 'react';
 2 | import clsx from 'clsx';
 3 | import styles from './styles.module.css';
 4 | export default function CodeBlockLine({
 5 |   line,
 6 |   classNames,
 7 |   showLineNumbers,
 8 |   getLineProps,
 9 |   getTokenProps,
10 | }) {
11 |   if (line.length === 1 && line[0].content === '\n') {
12 |     line[0].content = '';
13 |   }
14 |   const lineProps = getLineProps({
15 |     line,
16 |     className: clsx(classNames, showLineNumbers && styles.codeLine),
17 |   });
18 |   const lineTokens = line.map((token, key) => (
19 |     
20 |   ));
21 |   return (
22 |     
23 |       {showLineNumbers ? (
24 |         <>
25 |           
26 |           {lineTokens}
27 |         
28 |       ) : (
29 |         lineTokens
30 |       )}
31 |       
32 |
33 | ); 34 | } 35 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/theme/CodeBlock/Line/styles.module.css: -------------------------------------------------------------------------------- 1 | /* Intentionally has zero specificity, so that to be able to override 2 | the background in custom CSS file due bug https://github.com/facebook/docusaurus/issues/3678 */ 3 | :where(:root) { 4 | --docusaurus-highlighted-code-line-bg: rgb(72 77 91); 5 | } 6 | 7 | :where([data-theme='dark']) { 8 | --docusaurus-highlighted-code-line-bg: rgb(100 100 100); 9 | } 10 | 11 | :global(.theme-code-block-highlighted-line) { 12 | background-color: var(--docusaurus-highlighted-code-line-bg); 13 | display: block; 14 | margin: 0 calc(-1 * var(--ifm-pre-padding)); 15 | padding: 0 var(--ifm-pre-padding); 16 | } 17 | 18 | .codeLine { 19 | display: table-row; 20 | counter-increment: line-count; 21 | } 22 | 23 | .codeLineNumber { 24 | display: table-cell; 25 | text-align: right; 26 | width: 1%; 27 | position: sticky; 28 | left: 0; 29 | padding: 0 var(--ifm-pre-padding); 30 | background: var(--ifm-pre-background); 31 | overflow-wrap: normal; 32 | } 33 | 34 | .codeLineNumber::before { 35 | content: counter(line-count); 36 | opacity: 0.4; 37 | } 38 | 39 | :global(.theme-code-block-highlighted-line) .codeLineNumber::before { 40 | opacity: 0.8; 41 | } 42 | 43 | .codeLineContent { 44 | padding-right: var(--ifm-pre-padding); 45 | } 46 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/theme/CodeBlock/WordWrapButton/index.js: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import clsx from 'clsx'; 3 | import {translate} from '@docusaurus/Translate'; 4 | import IconWordWrap from '@theme/Icon/WordWrap'; 5 | import styles from './styles.module.css'; 6 | export default function WordWrapButton({className, onClick, isEnabled}) { 7 | const title = translate({ 8 | id: 'theme.CodeBlock.wordWrapToggle', 9 | message: 'Toggle word wrap', 10 | description: 11 | 'The title attribute for toggle word wrapping button of code block lines', 12 | }); 13 | return ( 14 | 26 | ); 27 | } 28 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/theme/CodeBlock/WordWrapButton/styles.module.css: -------------------------------------------------------------------------------- 1 | .wordWrapButtonIcon { 2 | width: 1.2rem; 3 | height: 1.2rem; 4 | } 5 | 6 | .wordWrapButtonEnabled .wordWrapButtonIcon { 7 | color: var(--ifm-color-primary); 8 | } 9 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/theme/CodeBlock/WorkbenchButton/styles.module.css: -------------------------------------------------------------------------------- 1 | :global(.theme-code-block:hover) .copyButtonCopied { 2 | opacity: 1 !important; 3 | } 4 | 5 | .copyButtonIcons { 6 | position: relative; 7 | width: 1.125rem; 8 | height: 1.125rem; 9 | } 10 | 11 | .copyButtonIcon, 12 | .copyButtonSuccessIcon { 13 | position: absolute; 14 | top: 0; 15 | left: 0; 16 | fill: currentColor; 17 | opacity: inherit; 18 | width: inherit; 19 | height: inherit; 20 | transition: all 0.15s ease; 21 | } 22 | 23 | .copyButtonSuccessIcon { 24 | top: 50%; 25 | left: 50%; 26 | transform: translate(-50%, -50%) scale(0.33); 27 | opacity: 0; 28 | color: #00d600; 29 | } 30 | 31 | .copyButtonCopied .copyButtonIcon { 32 | transform: scale(0.33); 33 | opacity: 0; 34 | } 35 | 36 | .copyButtonCopied .copyButtonSuccessIcon { 37 | transform: translate(-50%, -50%) scale(1); 38 | opacity: 1; 39 | transition-delay: 0.075s; 40 | } 41 | -------------------------------------------------------------------------------- /packages/replicad-docs/src/theme/CodeBlock/index.js: -------------------------------------------------------------------------------- 1 | import React, {isValidElement} from 'react'; 2 | import useIsBrowser from '@docusaurus/useIsBrowser'; 3 | import ElementContent from '@theme/CodeBlock/Content/Element'; 4 | import StringContent from '@theme/CodeBlock/Content/String'; 5 | /** 6 | * Best attempt to make the children a plain string so it is copyable. If there 7 | * are react elements, we will not be able to copy the content, and it will 8 | * return `children` as-is; otherwise, it concatenates the string children 9 | * together. 10 | */ 11 | function maybeStringifyChildren(children) { 12 | if (React.Children.toArray(children).some((el) => isValidElement(el))) { 13 | return children; 14 | } 15 | // The children is now guaranteed to be one/more plain strings 16 | return Array.isArray(children) ? children.join('') : children; 17 | } 18 | export default function CodeBlock({children: rawChildren, ...props}) { 19 | // The Prism theme on SSR is always the default theme but the site theme can 20 | // be in a different mode. React hydration doesn't update DOM styles that come 21 | // from SSR. Hence force a re-render after mounting to apply the current 22 | // relevant styles. 23 | const isBrowser = useIsBrowser(); 24 | const children = maybeStringifyChildren(rawChildren); 25 | const CodeBlockComp = 26 | typeof children === 'string' ? StringContent : ElementContent; 27 | return ( 28 | 29 | {children} 30 | 31 | ); 32 | } 33 | -------------------------------------------------------------------------------- /packages/replicad-docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/.nojekyll -------------------------------------------------------------------------------- /packages/replicad-docs/static/fonts/HKGrotesk-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/fonts/HKGrotesk-Bold.woff2 -------------------------------------------------------------------------------- /packages/replicad-docs/static/fonts/HKGrotesk-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/fonts/HKGrotesk-Light.woff2 -------------------------------------------------------------------------------- /packages/replicad-docs/static/fonts/HKGrotesk-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/fonts/HKGrotesk-LightItalic.woff2 -------------------------------------------------------------------------------- /packages/replicad-docs/static/fonts/HKGrotesk-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/fonts/HKGrotesk-Regular.woff2 -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/favicon.ico -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/finger.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/replicad.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/replicad.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/adding-depth-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/adding-depth-1.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/adding-depth-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/adding-depth-2.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/adding-depth-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/adding-depth-3.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/combinations-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/combinations-1.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/combinations-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/combinations-2.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/combinations-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/combinations-3.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/drawing-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/drawing-1.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/finders-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/finders-1.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/first-model.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/first-model.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/planes-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/planes-1.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/planes-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/planes-2.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/sketching-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/sketching-1.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/transformations-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/transformations-1.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/typescript-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/typescript-1.png -------------------------------------------------------------------------------- /packages/replicad-docs/static/img/tutorial/workbench.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-docs/static/img/tutorial/workbench.png -------------------------------------------------------------------------------- /packages/replicad-opencascadejs/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 QuaroTech Sàrl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/replicad-opencascadejs/README.md: -------------------------------------------------------------------------------- 1 | # `replicad-opencascadejs` 2 | 3 | An opencascadejs build containing only the APIs necessary to run replicad. You 4 | will need to have docker installed, as well as ytt (in order to generate the 5 | configuration files). 6 | 7 | ## Usage 8 | 9 | ``` 10 | pnpm buildWasm 11 | ``` 12 | 13 | You need to have a docker instance running and have run 14 | `docker pull donalffons/opencascade.js` 15 | -------------------------------------------------------------------------------- /packages/replicad-opencascadejs/__tests__/replicad-opencascadejs.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const replicadOpencascadejs = require('..'); 4 | 5 | describe('replicad-opencascadejs', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/replicad-opencascadejs/build-source/custom_build_single.yml: -------------------------------------------------------------------------------- 1 | #@ load("@ytt:data", "data") 2 | 3 | #@ data.values.buildFlags.extend(["-sDISABLE_EXCEPTION_CATCHING=1"]) 4 | 5 | mainBuild: 6 | name: replicad_single.js 7 | bindings: #@ data.values.bindings 8 | emccFlags: #@ data.values.buildFlags 9 | -------------------------------------------------------------------------------- /packages/replicad-opencascadejs/build-source/custom_build_with_exceptions.yml: -------------------------------------------------------------------------------- 1 | #@ load("@ytt:data", "data") 2 | 3 | #@ data.values.bindings.extend([{"symbol": "OCJS"}, {"symbol": "Standard_Failure"}]) 4 | 5 | mainBuild: 6 | name: replicad_with_exceptions.js 7 | bindings: #@ data.values.bindings 8 | emccFlags: #@ data.values.buildFlags 9 | 10 | additionalCppCode: | 11 | typedef Handle(IMeshTools_Context) Handle_IMeshTools_Context; 12 | class OCJS { 13 | public: 14 | static Standard_Failure* getStandard_FailureData(intptr_t exceptionPtr) { 15 | return reinterpret_cast(exceptionPtr); 16 | } 17 | }; 18 | 19 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /packages/replicad-opencascadejs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replicad-opencascadejs", 3 | "version": "0.19.0", 4 | "description": "OpencascadeJS custom build for replicad", 5 | "author": "Steve Genoud ", 6 | "homepage": "", 7 | "license": "MIT", 8 | "main": "src/replicad_single.js", 9 | "directories": { 10 | "lib": "src", 11 | "test": "__tests__" 12 | }, 13 | "files": [ 14 | "src" 15 | ], 16 | "scripts": { 17 | "build": "echo 'Please build manually'", 18 | "buildWasm": "pnpm run generateConfig && pnpm run buildSingle && pnpm run buildWithExceptions", 19 | "updateDocker": "docker pull donalffons/opencascade.js", 20 | "generateConfig": "ytt -f build-source/ --output-files build-config", 21 | "buildSingle": "cd build-config; docker run -it --rm -v $(pwd):/src -u $(id -u):$(id -g) donalffons/opencascade.js custom_build_single.yml && mv replicad_single* ../src; cd -", 22 | "buildWithExceptions": "cd build-config; docker run -it --rm -v $(pwd):/src -u $(id -u):$(id -g) donalffons/opencascade.js custom_build_with_exceptions.yml && mv replicad_with_exceptions* ../src; cd -" 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /packages/replicad-opencascadejs/src/replicad_single.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-opencascadejs/src/replicad_single.wasm -------------------------------------------------------------------------------- /packages/replicad-opencascadejs/src/replicad_with_exceptions.wasm: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad-opencascadejs/src/replicad_with_exceptions.wasm -------------------------------------------------------------------------------- /packages/replicad-threejs-helper/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/replicad-threejs-helper/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 QuaroTech Sàrl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/replicad-threejs-helper/__tests__/replicad-threejs-helper.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const replicadThreejsHelper = require('..'); 4 | 5 | describe('replicad-threejs-helper', () => { 6 | it('needs tests'); 7 | }); 8 | -------------------------------------------------------------------------------- /packages/replicad-threejs-helper/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replicad-threejs-helper", 3 | "version": "0.19.0", 4 | "description": "Helper to use threejs to render replicad models", 5 | "keywords": [ 6 | "cad", 7 | "threejs", 8 | "replicad" 9 | ], 10 | "author": "Steve Genoud ", 11 | "homepage": "https://replicad.xyz", 12 | "license": "MIT", 13 | "main": "dist/umd/replicad-threejs-helper.js", 14 | "module": "dist/es/replicad-threejs-helper.js", 15 | "type": "module", 16 | "directories": { 17 | "lib": "lib", 18 | "test": "__tests__" 19 | }, 20 | "repository": { 21 | "type": "git", 22 | "url": "git+https://github.com/sgenoud/replicad.git" 23 | }, 24 | "bugs": { 25 | "url": "https://github.com/sgenoud/replicad/issues" 26 | }, 27 | "peerDependencies": { 28 | "three": ">=0.155.0" 29 | }, 30 | "files": [ 31 | "dist" 32 | ], 33 | "scripts": { 34 | "build": "rollup -c rollup.config.js", 35 | "start": "rollup -w -c rollup.config.js", 36 | "build-doc": "typedoc --exclude \"../replicad-opencascadejs/*\" --out docs --theme minimal src", 37 | "build-docss": "typedoc --help", 38 | "test": "echo \"Error: run tests from root\" && exit 1" 39 | }, 40 | "devDependencies": { 41 | "@rollup/plugin-commonjs": "^25.0.1", 42 | "@rollup/plugin-node-resolve": "^15.0.0", 43 | "@rollup/plugin-terser": "^0.4.3", 44 | "@types/three": "^0.155.0", 45 | "@typescript-eslint/eslint-plugin": "^5.2.0", 46 | "@typescript-eslint/parser": "^5.4.0", 47 | "eslint": "^8.1.0", 48 | "prettier": "^2.4.1", 49 | "rollup": "3.10.1", 50 | "rollup-plugin-sourcemaps": "^0.6.3", 51 | "rollup-plugin-ts": "3.1.1", 52 | "typescript": "^4.9.5" 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /packages/replicad-threejs-helper/rollup.config.js: -------------------------------------------------------------------------------- 1 | import { nodeResolve } from "@rollup/plugin-node-resolve"; 2 | import commonjs from "@rollup/plugin-commonjs"; 3 | import typescript from "rollup-plugin-ts"; 4 | 5 | export default { 6 | input: `lib/replicad-threejs-helper.ts`, 7 | output: [ 8 | { 9 | file: "dist/umd/replicad-threejs-helper.js", 10 | name: "replicad", 11 | format: "umd", 12 | sourcemap: true, 13 | }, 14 | { 15 | file: "dist/cjs/replicad-threejs-helper.js", 16 | name: "replicad", 17 | format: "cjs", 18 | sourcemap: true, 19 | }, 20 | { 21 | file: "dist/es/replicad-threejs-helper.js", 22 | format: "es", 23 | sourcemap: true, 24 | }, 25 | ], 26 | watch: { 27 | include: "lib/**", 28 | }, 29 | plugins: [typescript(), commonjs(), nodeResolve()], 30 | external: ["three"], 31 | }; 32 | -------------------------------------------------------------------------------- /packages/replicad-threejs-helper/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es6", 5 | "module":"es2015", 6 | "strict": true, 7 | "sourceMap": true, 8 | "skipLibCheck": true, 9 | "declaration": true, 10 | "allowSyntheticDefaultImports": true 11 | }, 12 | "include": [ 13 | "lib" 14 | ], 15 | "typedocOptions": { 16 | "watch": true, 17 | "readme": "none", 18 | "excludeProtected": true 19 | } 20 | } 21 | 22 | -------------------------------------------------------------------------------- /packages/replicad/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | yarn-error.log 3 | dist 4 | -------------------------------------------------------------------------------- /packages/replicad/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 QuaroTech Sàrl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/diffSVGToSnapshot.ts: -------------------------------------------------------------------------------- 1 | import * as fs from "fs"; 2 | import * as path from "path"; 3 | 4 | const isFailure = ({ 5 | pass, 6 | updateSnapshot, 7 | }: { 8 | pass: boolean; 9 | updateSnapshot: boolean; 10 | }) => !pass && !updateSnapshot; 11 | const shouldUpdate = ({ 12 | pass, 13 | updateSnapshot, 14 | }: { 15 | pass: boolean; 16 | updateSnapshot: boolean; 17 | }) => !pass && updateSnapshot; 18 | 19 | export function diffSVGToSnapshot(options: { 20 | receivedSVG: string; 21 | snapshotIdentifier: string; 22 | snapshotsDir: string; 23 | updateSnapshot: boolean; 24 | }) { 25 | const { 26 | receivedSVG, 27 | snapshotIdentifier, 28 | snapshotsDir, 29 | updateSnapshot = false, 30 | } = options; 31 | 32 | let result: { 33 | pass: boolean; 34 | added?: boolean; 35 | updated?: boolean; 36 | expected?: string; 37 | } = { 38 | pass: false, 39 | }; 40 | 41 | const baselineSnapshotPath = path.join( 42 | snapshotsDir, 43 | `${snapshotIdentifier}-snap.svg` 44 | ); 45 | 46 | if (!fs.existsSync(baselineSnapshotPath)) { 47 | fs.mkdirSync(snapshotsDir, { recursive: true }); 48 | fs.writeFileSync(baselineSnapshotPath, receivedSVG, "utf-8"); 49 | result = { added: true, pass: false }; 50 | } else { 51 | const expectedSVG = fs.readFileSync(baselineSnapshotPath, "utf8"); 52 | 53 | const pass = expectedSVG === receivedSVG; 54 | 55 | if (isFailure({ pass, updateSnapshot })) { 56 | result = { 57 | pass: false, 58 | expected: expectedSVG, 59 | }; 60 | } else if (shouldUpdate({ pass, updateSnapshot })) { 61 | fs.mkdirSync(snapshotsDir, { recursive: true }); 62 | fs.writeFileSync(baselineSnapshotPath, receivedSVG, "utf-8"); 63 | result = { updated: true, pass: false }; 64 | } else { 65 | result = { 66 | pass, 67 | }; 68 | } 69 | } 70 | return result; 71 | } 72 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/booleans.test.ts-__tests__-drawing-booleans.test.ts--cut-such-that-a-hole-becomes-the-outside-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/booleans.test.ts-__tests__-drawing-booleans.test.ts--cut-two-rectangles-with-corners-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/booleans.test.ts-__tests__-drawing-booleans.test.ts--cuts-one-rectangle-from-the-other-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/booleans.test.ts-__tests__-drawing-booleans.test.ts--fuse-two-rectangles-with-corners-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/booleans.test.ts-__tests__-drawing-booleans.test.ts--fuses-two-circles-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/booleans.test.ts-__tests__-drawing-booleans.test.ts--fuses-two-rectangles-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/booleans.test.ts-__tests__-drawing-booleans.test.ts--handles-the-case-when-a-compound-is-created-from-fusion-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/booleans.test.ts-__tests__-drawing-booleans.test.ts--intersects-two-rectangles-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--1-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--10-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--25-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--50-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset--75-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset-1-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset-10-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset-25-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-1,-with-offset-50-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset--1-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset--10-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset--2-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset--5-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset-1-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-complex-shape-2,-with-offset-20-1-snap.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-vase,-with-offset--5-1-snap.svg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/replicad/__tests__/drawing/__snapshots__/offsets.test.ts-__tests__-drawing-offsets.test.ts--offset-vase,-with-offset--5-1-snap.svg -------------------------------------------------------------------------------- /packages/replicad/__tests__/setup.ts: -------------------------------------------------------------------------------- 1 | import { join } from "path"; 2 | import { expect, beforeAll } from "vitest"; 3 | import opencascade from "replicad-opencascadejs/src/replicad_single.js"; 4 | import { setOC } from "../src/index"; 5 | import toMatchSVGSnapshot from "./toMatchSVGSnapshot"; 6 | 7 | declare global { 8 | namespace jest { 9 | interface Matchers { 10 | toMatchSVGSnapshot(): R; 11 | } 12 | } 13 | } 14 | 15 | beforeAll(async function () { 16 | if (globalThis.replicadInit) return; 17 | expect.extend({ toMatchSVGSnapshot }); 18 | 19 | const opencascadeWasm = join( 20 | __dirname, 21 | "../../replicad-opencascadejs/src/replicad_single.wasm" 22 | ); 23 | // @ts-expect-error bad ocjs typings 24 | const OC = await opencascade({ 25 | locateFile: () => opencascadeWasm, 26 | }); 27 | 28 | setOC(OC); 29 | globalThis.replicadInit = true; 30 | }); 31 | -------------------------------------------------------------------------------- /packages/replicad/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "replicad", 3 | "version": "0.19.0", 4 | "description": "The library to build browser based 3D models with code", 5 | "keywords": [ 6 | "cad", 7 | "opencascadejs", 8 | "brep", 9 | "STEP", 10 | "STL" 11 | ], 12 | "author": "Steve Genoud ", 13 | "homepage": "https://replicad.xyz", 14 | "license": "MIT", 15 | "type": "module", 16 | "main": "./dist/replicad.cjs", 17 | "module": "./dist/replicad.js", 18 | "types": "./dist/replicad.d.ts", 19 | "exports": { 20 | ".": { 21 | "import": "./dist/replicad.js", 22 | "require": "./dist/replicad.umd.cjs" 23 | } 24 | }, 25 | "directories": { 26 | "src": "src", 27 | "test": "__tests__" 28 | }, 29 | "files": [ 30 | "dist" 31 | ], 32 | "scripts": { 33 | "build": "vite build", 34 | "start": "NO_TYPES=true vite build --watch", 35 | "build-doc": "typedoc --exclude \"../replicad-opencascadejs/*\" --out docs --theme default src", 36 | "build-docss": "typedoc --help", 37 | "test": "vitest" 38 | }, 39 | "devDependencies": { 40 | "@rollup/plugin-commonjs": "24.0.1", 41 | "@rollup/plugin-json": "6.0.0", 42 | "@rollup/plugin-node-resolve": "15.0.1", 43 | "@rollup/plugin-terser": "^0.4.3", 44 | "@swc/core": "^1.3.27", 45 | "@swc/helpers": "^0.4.14", 46 | "@types/flatbush": "^3.3.0", 47 | "@typescript-eslint/eslint-plugin": "^5.2.0", 48 | "@typescript-eslint/parser": "^5.4.0", 49 | "chalk": "^5.2.0", 50 | "eslint": "^8.1.0", 51 | "jest-image-snapshot": "^6.1.0", 52 | "jest-matcher-utils": "^29.4.1", 53 | "jest-svg-snapshot": "^0.1.0", 54 | "prettier": "^2.4.1", 55 | "replicad-opencascadejs": "^0.19.0", 56 | "rollup": "3.28.0", 57 | "rollup-plugin-ts": "3.4.4", 58 | "typedoc": "^0.28.2", 59 | "typescript": "5.1.6", 60 | "vite": "^4.0.4", 61 | "vite-plugin-dts": "^3.5.2", 62 | "vitest": "^0.28.3" 63 | }, 64 | "dependencies": { 65 | "@types/opentype.js": "1.3.4", 66 | "flatbush": "^4.0.0", 67 | "opentype.js": "1.3.4" 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /packages/replicad/src/blueprints/approximations.ts: -------------------------------------------------------------------------------- 1 | import { 2 | approximateAsSvgCompatibleCurve, 3 | ApproximationOptions, 4 | } from "../lib2d"; 5 | import Blueprint from "./Blueprint"; 6 | import Blueprints from "./Blueprints"; 7 | import { Shape2D } from "./boolean2D"; 8 | import CompoundBlueprint from "./CompoundBlueprint"; 9 | 10 | export function approximateForSVG( 11 | bp: T, 12 | options: ApproximationOptions 13 | ): T { 14 | if (bp instanceof Blueprint) { 15 | return new Blueprint( 16 | approximateAsSvgCompatibleCurve(bp.curves, options) 17 | ) as T; 18 | } else if (bp instanceof CompoundBlueprint) { 19 | return new CompoundBlueprint( 20 | bp.blueprints.map((b) => approximateForSVG(b, options)) 21 | ) as T; 22 | } else if (bp instanceof Blueprints) { 23 | return new Blueprints( 24 | bp.blueprints.map((b) => approximateForSVG(b, options)) 25 | ) as T; 26 | } 27 | return bp; 28 | } 29 | -------------------------------------------------------------------------------- /packages/replicad/src/blueprints/cannedBlueprints.ts: -------------------------------------------------------------------------------- 1 | import Blueprint from "./Blueprint"; 2 | import { BlueprintSketcher } from "../Sketcher2d"; 3 | 4 | export const polysidesBlueprint = ( 5 | radius: number, 6 | sidesCount: number, 7 | sagitta = 0 8 | ): Blueprint => { 9 | const points = [...Array(sidesCount).keys()].map((i) => { 10 | const theta = -((Math.PI * 2) / sidesCount) * i; 11 | return [radius * Math.sin(theta), radius * Math.cos(theta)]; 12 | }); 13 | 14 | // We start with the last point to make sure the shape is complete 15 | const blueprint = new BlueprintSketcher().movePointerTo([ 16 | points[points.length - 1][0], 17 | points[points.length - 1][1], 18 | ]); 19 | 20 | if (sagitta) { 21 | points.forEach(([x, y]) => blueprint.sagittaArcTo([x, y], sagitta)); 22 | } else { 23 | points.forEach(([x, y]) => blueprint.lineTo([x, y])); 24 | } 25 | 26 | return blueprint.done(); 27 | }; 28 | 29 | export const roundedRectangleBlueprint = ( 30 | width: number, 31 | height: number, 32 | r: number | { rx?: number; ry?: number } = 0 33 | ) => { 34 | const { rx: inputRx = 0, ry: inputRy = 0 } = 35 | typeof r === "number" ? { ry: r, rx: r } : r; 36 | 37 | let rx = Math.min(inputRx, width / 2); 38 | let ry = Math.min(inputRy, height / 2); 39 | 40 | const withRadius = rx && ry; 41 | if (!withRadius) { 42 | rx = 0; 43 | ry = 0; 44 | } 45 | const symmetricRadius = rx === ry; 46 | 47 | const sk = new BlueprintSketcher([ 48 | Math.min(0, -(width / 2 - rx)), 49 | -height / 2, 50 | ]); 51 | 52 | const addFillet = (xDist: number, yDist: number) => { 53 | if (withRadius) { 54 | if (symmetricRadius) sk.tangentArc(xDist, yDist); 55 | else sk.ellipse(xDist, yDist, rx, ry, 0, false, true); 56 | } 57 | }; 58 | 59 | if (rx < width / 2) { 60 | sk.hLine(width - 2 * rx); 61 | } 62 | addFillet(rx, ry); 63 | if (ry < height / 2) { 64 | sk.vLine(height - 2 * ry); 65 | } 66 | addFillet(-rx, ry); 67 | if (rx < width / 2) { 68 | sk.hLine(-(width - 2 * rx)); 69 | } 70 | addFillet(-rx, -ry); 71 | if (ry < height / 2) { 72 | sk.vLine(-(height - 2 * ry)); 73 | } 74 | addFillet(rx, -ry); 75 | return sk.close(); 76 | }; 77 | -------------------------------------------------------------------------------- /packages/replicad/src/blueprints/index.ts: -------------------------------------------------------------------------------- 1 | import Blueprint from "./Blueprint"; 2 | import CompoundBlueprint from "./CompoundBlueprint"; 3 | import Blueprints from "./Blueprints"; 4 | import { organiseBlueprints, DrawingInterface } from "./lib"; 5 | import { ScaleMode } from "../curves"; 6 | 7 | export { Blueprint, CompoundBlueprint, Blueprints, organiseBlueprints }; 8 | 9 | export type { DrawingInterface, ScaleMode }; 10 | 11 | export * from "./cannedBlueprints"; 12 | export * from "./booleanOperations"; 13 | export * from "./boolean2D"; 14 | -------------------------------------------------------------------------------- /packages/replicad/src/blueprints/svg.ts: -------------------------------------------------------------------------------- 1 | import { BoundingBox2d } from "../lib2d"; 2 | 3 | export const viewbox = (bbox: BoundingBox2d, margin = 1) => { 4 | const minX = bbox.bounds[0][0] - margin; 5 | const minY = -bbox.bounds[1][1] - margin; 6 | 7 | return `${minX} ${minY} ${bbox.width + 2 * margin} ${ 8 | bbox.height + 2 * margin 9 | }`; 10 | }; 11 | 12 | export const asSVG = (body: string, boundingBox: BoundingBox2d, margin = 1) => { 13 | const vbox = viewbox(boundingBox, margin); 14 | return ` 15 | ${body} 16 | `; 17 | }; 18 | -------------------------------------------------------------------------------- /packages/replicad/src/constants.ts: -------------------------------------------------------------------------------- 1 | export const HASH_CODE_MAX = 2147483647; 2 | export const DEG2RAD = Math.PI / 180; 3 | export const RAD2DEG = 180 / Math.PI; 4 | -------------------------------------------------------------------------------- /packages/replicad/src/definitionMaps.ts: -------------------------------------------------------------------------------- 1 | import { getOC } from "./oclib.js"; 2 | 3 | export type CurveType = 4 | | "LINE" 5 | | "CIRCLE" 6 | | "ELLIPSE" 7 | | "HYPERBOLA" 8 | | "PARABOLA" 9 | | "BEZIER_CURVE" 10 | | "BSPLINE_CURVE" 11 | | "OFFSET_CURVE" 12 | | "OTHER_CURVE"; 13 | 14 | let CURVE_TYPES_MAP: Map | null = null; 15 | 16 | const getCurveTypesMap = (refresh?: boolean): Map => { 17 | if (CURVE_TYPES_MAP && !refresh) return CURVE_TYPES_MAP; 18 | 19 | const oc = getOC(); 20 | const ga = oc.GeomAbs_CurveType; 21 | 22 | CURVE_TYPES_MAP = new Map([ 23 | [ga.GeomAbs_Line, "LINE"], 24 | [ga.GeomAbs_Circle, "CIRCLE"], 25 | [ga.GeomAbs_Ellipse, "ELLIPSE"], 26 | [ga.GeomAbs_Hyperbola, "HYPERBOLA"], 27 | [ga.GeomAbs_Parabola, "PARABOLA"], 28 | [ga.GeomAbs_BezierCurve, "BEZIER_CURVE"], 29 | [ga.GeomAbs_BSplineCurve, "BSPLINE_CURVE"], 30 | [ga.GeomAbs_OffsetCurve, "OFFSET_CURVE"], 31 | [ga.GeomAbs_OtherCurve, "OTHER_CURVE"], 32 | ]); 33 | return CURVE_TYPES_MAP; 34 | }; 35 | 36 | export const findCurveType = (type: any): CurveType => { 37 | let shapeType = getCurveTypesMap().get(type); 38 | if (!shapeType) shapeType = getCurveTypesMap(true).get(type); 39 | if (!shapeType) throw new Error("unknown type"); 40 | return shapeType; 41 | }; 42 | -------------------------------------------------------------------------------- /packages/replicad/src/finders/index.ts: -------------------------------------------------------------------------------- 1 | import { Finder, FilterFcn } from "./definitions"; 2 | 3 | export type { FilterFcn }; 4 | 5 | /** 6 | * Combine a set of finder filters (defined with radius) to pass as a filter 7 | * function. 8 | * 9 | * @param filters - An array of objects containing a filter and its radius. 10 | * @returns A tuple containing a filter function and a cleanup function. 11 | * 12 | * @category Finders 13 | */ 14 | export const combineFinderFilters = ( 15 | filters: { filter: Finder; radius: R }[] 16 | ): [(v: Type) => R | null, () => void] => { 17 | const filter = (element: Type) => { 18 | for (const { filter, radius } of filters) { 19 | if (filter.shouldKeep(element)) return radius; 20 | } 21 | return null; 22 | }; 23 | 24 | return [filter, () => filters.forEach((f) => f.filter.delete())]; 25 | }; 26 | 27 | export * from "./edgeFinder"; 28 | export * from "./faceFinder"; 29 | export * from "./cornerFinder"; 30 | -------------------------------------------------------------------------------- /packages/replicad/src/importers.ts: -------------------------------------------------------------------------------- 1 | import { cast } from "./shapes"; 2 | import { getOC } from "./oclib"; 3 | import { localGC } from "./register"; 4 | 5 | const uniqueId = () => 6 | Date.now().toString(36) + Math.random().toString(36).substring(2); 7 | 8 | /** 9 | * Creates a new shapes from a STEP file (as a Blob or a File). 10 | * 11 | * @category Import 12 | */ 13 | export async function importSTEP(STLBlob: Blob) { 14 | const oc = getOC(); 15 | const [r, gc] = localGC(); 16 | 17 | const fileName = uniqueId(); 18 | const bufferView = new Uint8Array(await STLBlob.arrayBuffer()); 19 | oc.FS.writeFile(`/${fileName}`, bufferView); 20 | 21 | const reader = r(new oc.STEPControl_Reader_1()); 22 | if (reader.ReadFile(fileName)) { 23 | oc.FS.unlink("/" + fileName); 24 | reader.TransferRoots(r(new oc.Message_ProgressRange_1())); 25 | const stepShape = r(reader.OneShape()); 26 | 27 | const shape = cast(stepShape); 28 | gc(); 29 | return shape; 30 | } else { 31 | oc.FS.unlink("/" + fileName); 32 | gc(); 33 | throw new Error("Failed to load STEP file"); 34 | } 35 | } 36 | 37 | /** Creates a new shapes from a STL file (as a Blob or a File). 38 | * 39 | * This process can be relatively long depending on how much tesselation has 40 | * been done to your STL. 41 | * 42 | * This function tries to clean a bit the triangulation of faces, but can fail 43 | * in bad ways. 44 | * 45 | * @category Import 46 | */ 47 | export async function importSTL(STLBlob: Blob) { 48 | const oc = getOC(); 49 | const [r, gc] = localGC(); 50 | 51 | const fileName = uniqueId(); 52 | const bufferView = new Uint8Array(await STLBlob.arrayBuffer()); 53 | oc.FS.writeFile(`/${fileName}`, bufferView); 54 | 55 | const reader = r(new oc.StlAPI_Reader()); 56 | const readShape = r(new oc.TopoDS_Shell()); 57 | 58 | if (reader.Read(readShape, fileName)) { 59 | oc.FS.unlink("/" + fileName); 60 | 61 | const shapeUpgrader = r( 62 | new oc.ShapeUpgrade_UnifySameDomain_2(readShape, true, true, false) 63 | ); 64 | shapeUpgrader.Build(); 65 | const upgradedShape = r(shapeUpgrader.Shape()); 66 | 67 | const solidSTL = r(new oc.BRepBuilderAPI_MakeSolid_1()); 68 | solidSTL.Add(oc.TopoDS.Shell_1(upgradedShape)); 69 | const asSolid = r(solidSTL.Solid()); 70 | 71 | const shape = cast(asSolid); 72 | gc(); 73 | return shape; 74 | } else { 75 | oc.FS.unlink("/" + fileName); 76 | gc(); 77 | throw new Error("Failed to load STL file"); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /packages/replicad/src/index.ts: -------------------------------------------------------------------------------- 1 | import Sketcher from "./Sketcher"; 2 | import FaceSketcher, { BaseSketcher2d, BlueprintSketcher } from "./Sketcher2d"; 3 | 4 | import { Point2D, BoundingBox2d, Curve2D, axis2d } from "./lib2d"; 5 | import { GenericSketcher, SplineConfig } from "./sketcherlib"; 6 | 7 | export { 8 | axis2d, 9 | Sketcher, 10 | BaseSketcher2d, 11 | FaceSketcher, 12 | BlueprintSketcher, 13 | BoundingBox2d, 14 | Curve2D, 15 | }; 16 | 17 | export type { GenericSketcher, SplineConfig, Point2D }; 18 | 19 | export * from "./constants"; 20 | export * from "./oclib"; 21 | export * from "./register"; 22 | export * from "./geom"; 23 | export * from "./geomHelpers"; 24 | export * from "./shapes"; 25 | export * from "./shapeHelpers"; 26 | export * from "./measureShape"; 27 | export * from "./finders"; 28 | export * from "./shortcuts.js"; 29 | export * from "./addThickness"; 30 | export * from "./blueprints"; 31 | export * from "./sketches"; 32 | export * from "./text"; 33 | export * from "./importers"; 34 | export * from "./draw"; 35 | export * from "./projection"; 36 | export * from "./export/assemblyExporter"; 37 | -------------------------------------------------------------------------------- /packages/replicad/src/lib2d/BoundingBox2d.ts: -------------------------------------------------------------------------------- 1 | import { WrappingObj, GCWithScope } from "../register.js"; 2 | import { getOC } from "../oclib.js"; 3 | import { Bnd_Box2d } from "replicad-opencascadejs"; 4 | 5 | import { Point2D } from "./definitions.js"; 6 | import { reprPnt } from "./utils.js"; 7 | import { pnt } from "./ocWrapper.js"; 8 | 9 | export class BoundingBox2d extends WrappingObj { 10 | constructor(wrapped?: Bnd_Box2d) { 11 | const oc = getOC(); 12 | let boundBox = wrapped; 13 | if (!boundBox) { 14 | boundBox = new oc.Bnd_Box2d(); 15 | } 16 | super(boundBox); 17 | } 18 | 19 | get repr(): string { 20 | const [min, max] = this.bounds; 21 | return `${reprPnt(min)} - ${reprPnt(max)}`; 22 | } 23 | 24 | get bounds(): [Point2D, Point2D] { 25 | const xMin = { current: 0 }; 26 | const yMin = { current: 0 }; 27 | const xMax = { current: 0 }; 28 | const yMax = { current: 0 }; 29 | 30 | // @ts-expect-error missing type in oc 31 | this.wrapped.Get(xMin, yMin, xMax, yMax); 32 | return [ 33 | [xMin.current, yMin.current], 34 | [xMax.current, yMax.current], 35 | ]; 36 | } 37 | 38 | get center(): Point2D { 39 | const [[xmin, ymin], [xmax, ymax]] = this.bounds; 40 | return [xmin + (xmax - xmin) / 2, ymin + (ymax - ymin) / 2]; 41 | } 42 | 43 | get width(): number { 44 | const [[xmin], [xmax]] = this.bounds; 45 | return Math.abs(xmax - xmin); 46 | } 47 | 48 | get height(): number { 49 | const [[, ymin], [, ymax]] = this.bounds; 50 | return Math.abs(ymax - ymin); 51 | } 52 | 53 | outsidePoint(paddingPercent = 1): Point2D { 54 | const [min, max] = this.bounds; 55 | const width = max[0] - min[0]; 56 | const height = max[1] - min[1]; 57 | 58 | return [ 59 | max[0] + (width / 100) * paddingPercent, 60 | max[1] + (height / 100) * paddingPercent * 0.9, 61 | ]; 62 | } 63 | 64 | add(other: BoundingBox2d) { 65 | this.wrapped.Add_1(other.wrapped); 66 | } 67 | 68 | isOut(other: BoundingBox2d): boolean { 69 | return this.wrapped.IsOut_4(other.wrapped); 70 | } 71 | 72 | containsPoint(other: Point2D): boolean { 73 | const r = GCWithScope(); 74 | const point = r(pnt(other)); 75 | return !this.wrapped.IsOut_1(point); 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/replicad/src/lib2d/definitions.ts: -------------------------------------------------------------------------------- 1 | export type Point2D = [number, number]; 2 | 3 | export function isPoint2D(point: unknown): point is Point2D { 4 | return Array.isArray(point) && point.length === 2; 5 | } 6 | -------------------------------------------------------------------------------- /packages/replicad/src/lib2d/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./definitions.js"; 2 | export * from "./approximations.js"; 3 | export * from "./BoundingBox2d.js"; 4 | export * from "./ocWrapper.js"; 5 | export * from "./vectorOperations.js"; 6 | export * from "./intersections.js"; 7 | export * from "./Curve2D"; 8 | export * from "./customCorners"; 9 | export * from "./makeCurves"; 10 | export * from "./utils"; 11 | export * from "./offset"; 12 | export * from "./stitching"; 13 | export * from "./svgPath"; 14 | -------------------------------------------------------------------------------- /packages/replicad/src/lib2d/ocWrapper.ts: -------------------------------------------------------------------------------- 1 | import { gp_Ax2d, gp_Dir2d, gp_Pnt2d, gp_Vec2d } from "replicad-opencascadejs"; 2 | import { getOC } from "../oclib"; 3 | import { localGC } from "../register"; 4 | import { Point2D } from "./definitions"; 5 | 6 | export const pnt = ([x, y]: Point2D): gp_Pnt2d => { 7 | const oc = getOC(); 8 | return new oc.gp_Pnt2d_3(x, y); 9 | }; 10 | 11 | export const direction2d = ([x, y]: Point2D): gp_Dir2d => { 12 | const oc = getOC(); 13 | return new oc.gp_Dir2d_4(x, y); 14 | }; 15 | 16 | export const vec = ([x, y]: Point2D): gp_Vec2d => { 17 | const oc = getOC(); 18 | return new oc.gp_Vec2d_4(x, y); 19 | }; 20 | 21 | export const axis2d = (point: Point2D, direction: Point2D): gp_Ax2d => { 22 | const oc = getOC(); 23 | const [r, gc] = localGC(); 24 | const axis = new oc.gp_Ax2d_2(r(pnt(point)), r(direction2d(direction))); 25 | gc(); 26 | return axis; 27 | }; 28 | -------------------------------------------------------------------------------- /packages/replicad/src/lib2d/stitching.ts: -------------------------------------------------------------------------------- 1 | import Flatbush from "flatbush"; 2 | import { Curve2D } from "./Curve2D"; 3 | 4 | export const stitchCurves = ( 5 | curves: Curve2D[], 6 | precision = 1e-7 7 | ): Curve2D[][] => { 8 | // We create a spacial index of the startpoints 9 | const startPoints = new Flatbush(curves.length); 10 | curves.forEach((c) => { 11 | const [x, y] = c.firstPoint; 12 | startPoints.add(x - precision, y - precision, x + precision, y + precision); 13 | }); 14 | startPoints.finish(); 15 | 16 | const stitchedCurves: Curve2D[][] = []; 17 | const visited = new Set(); 18 | 19 | curves.forEach((curve, index) => { 20 | if (visited.has(index)) return; 21 | 22 | const connectedCurves: Curve2D[] = [curve]; 23 | let currentIndex = index; 24 | 25 | visited.add(index); 26 | 27 | // Once we have started a connected curve segment, we look for the next 28 | 29 | let maxLoops = curves.length; 30 | // eslint-disable-next-line no-constant-condition 31 | while (true) { 32 | if (maxLoops-- < 0) { 33 | throw new Error("Infinite loop detected"); 34 | } 35 | 36 | const lastPoint = connectedCurves[connectedCurves.length - 1].lastPoint; 37 | 38 | const [x, y] = lastPoint; 39 | const neighbors = startPoints.search( 40 | x - precision, 41 | y - precision, 42 | x + precision, 43 | y + precision 44 | ); 45 | 46 | const indexDistance = (otherIndex: number) => 47 | Math.abs((currentIndex - otherIndex) % curves.length); 48 | const potentialNextCurves = neighbors 49 | .filter((neighborIndex) => !visited.has(neighborIndex)) 50 | .map((neighborIndex): [Curve2D, number, number] => [ 51 | curves[neighborIndex], 52 | neighborIndex, 53 | indexDistance(neighborIndex), 54 | ]) 55 | .sort(([, , a], [, , b]) => indexDistance(a) - indexDistance(b)); 56 | 57 | if (potentialNextCurves.length === 0) { 58 | // No more curves to connect we should have wrapped 59 | stitchedCurves.push(connectedCurves); 60 | break; 61 | } 62 | 63 | const [nextCurve, nextCurveIndex] = potentialNextCurves[0]; 64 | 65 | connectedCurves.push(nextCurve); 66 | visited.add(nextCurveIndex); 67 | currentIndex = nextCurveIndex; 68 | } 69 | }); 70 | 71 | return stitchedCurves; 72 | }; 73 | -------------------------------------------------------------------------------- /packages/replicad/src/lib2d/utils.ts: -------------------------------------------------------------------------------- 1 | import round2 from "../utils/round2"; 2 | import { Point2D } from "./definitions"; 3 | 4 | export const reprPnt = ([x, y]: Point2D): string => { 5 | return `(${round2(x)},${round2(y)})`; 6 | }; 7 | 8 | const asFixed = (p: number, precision = 1e-9): string => { 9 | let num = p; 10 | if (Math.abs(p) < precision) num = 0; 11 | return num.toFixed(-Math.log10(precision)); 12 | }; 13 | export const removeDuplicatePoints = ( 14 | points: Point2D[], 15 | precision = 1e-9 16 | ): Point2D[] => { 17 | return Array.from( 18 | new Map( 19 | points.map(([p0, p1]) => [ 20 | `[${asFixed(p0, precision)},${asFixed(p1, precision)}]`, 21 | [p0, p1] as Point2D, 22 | ]) 23 | ).values() 24 | ); 25 | }; 26 | -------------------------------------------------------------------------------- /packages/replicad/src/oclib.ts: -------------------------------------------------------------------------------- 1 | import { OpenCascadeInstance } from "replicad-opencascadejs"; 2 | 3 | const OC: { library: OpenCascadeInstance | null } = { 4 | library: null, 5 | }; 6 | 7 | export const setOC = (oc: OpenCascadeInstance): void => { 8 | OC.library = oc; 9 | }; 10 | 11 | export const getOC = (): OpenCascadeInstance => { 12 | if (!OC.library) throw new Error("oppencascade has not been loaded"); 13 | return OC.library; 14 | }; 15 | -------------------------------------------------------------------------------- /packages/replicad/src/projection/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./ProjectionCamera"; 2 | export * from "./makeProjectedEdges"; 3 | -------------------------------------------------------------------------------- /packages/replicad/src/projection/makeProjectedEdges.ts: -------------------------------------------------------------------------------- 1 | import { cast } from "../shapes"; 2 | import { getOC } from "../oclib"; 3 | import { GCWithScope } from "../register"; 4 | 5 | import type { Edge, AnyShape } from "../shapes"; 6 | import type { ProjectionCamera } from "./ProjectionCamera"; 7 | import type { TopoDS_Shape } from "replicad-opencascadejs"; 8 | 9 | const getEdges = (shape: TopoDS_Shape) => { 10 | if (shape.IsNull()) return []; 11 | return cast(shape).edges; 12 | }; 13 | 14 | export function makeProjectedEdges( 15 | shape: AnyShape, 16 | camera: ProjectionCamera, 17 | withHiddenLines = true 18 | ): { visible: Edge[]; hidden: Edge[] } { 19 | const oc = getOC(); 20 | const r = GCWithScope(); 21 | 22 | const hiddenLineRemoval = r(new oc.HLRBRep_Algo_1()); 23 | hiddenLineRemoval.Add_2(shape.wrapped, 0); 24 | 25 | const projector = r(new oc.HLRAlgo_Projector_2(camera.wrapped)); 26 | hiddenLineRemoval.Projector_1(projector); 27 | 28 | hiddenLineRemoval.Update(); 29 | hiddenLineRemoval.Hide_1(); 30 | 31 | const hlrShapes = new oc.HLRBRep_HLRToShape( 32 | new oc.Handle_HLRBRep_Algo_2(hiddenLineRemoval) 33 | ); 34 | 35 | const visible = [ 36 | ...getEdges(hlrShapes.VCompound_1()), 37 | ...getEdges(hlrShapes.Rg1LineVCompound_1()), 38 | ...getEdges(hlrShapes.OutLineVCompound_1()), 39 | ]; 40 | 41 | visible.forEach((e) => oc.BRepLib.BuildCurves3d_2(e.wrapped)); 42 | 43 | const hidden = withHiddenLines 44 | ? [ 45 | ...getEdges(hlrShapes.HCompound_1()), 46 | ...getEdges(hlrShapes.Rg1LineHCompound_1()), 47 | ...getEdges(hlrShapes.OutLineHCompound_1()), 48 | ] 49 | : []; 50 | 51 | hidden.forEach((e) => oc.BRepLib.BuildCurves3d_2(e.wrapped)); 52 | 53 | return { visible, hidden }; 54 | } 55 | -------------------------------------------------------------------------------- /packages/replicad/src/shortcuts.ts: -------------------------------------------------------------------------------- 1 | import { Shape3D } from "./shapes"; 2 | import Sketcher from "./Sketcher"; 3 | 4 | export const makeBaseBox = ( 5 | xLength: number, 6 | yLength: number, 7 | zLength: number 8 | ): Shape3D => { 9 | return new Sketcher() 10 | .movePointerTo([-xLength / 2, yLength / 2]) 11 | .hLine(xLength) 12 | .vLine(-yLength) 13 | .hLine(-xLength) 14 | .close() 15 | .extrude(zLength); 16 | }; 17 | -------------------------------------------------------------------------------- /packages/replicad/src/sketches/Sketches.ts: -------------------------------------------------------------------------------- 1 | import { Point } from "../geom.js"; 2 | import { compoundShapes } from "../shapeHelpers.js"; 3 | import { ExtrusionProfile } from "../addThickness.js"; 4 | import { AnyShape } from "../shapes.js"; 5 | 6 | import CompoundSketch from "./CompoundSketch"; 7 | import Sketch from "./Sketch"; 8 | 9 | export default class Sketches { 10 | sketches: Array; 11 | 12 | constructor(sketches: Array) { 13 | this.sketches = sketches; 14 | } 15 | 16 | wires(): AnyShape { 17 | const wires = this.sketches.map((s) => 18 | s instanceof Sketch ? s.wire : s.wires 19 | ); 20 | return compoundShapes(wires); 21 | } 22 | 23 | faces(): AnyShape { 24 | const faces = this.sketches.map((s) => s.face()); 25 | return compoundShapes(faces); 26 | } 27 | 28 | /** Extrudes the sketch to a certain distance.(along the default direction 29 | * and origin of the sketch). 30 | * 31 | * You can define another extrusion direction or origin, 32 | * 33 | * It is also possible to twist extrude with an angle (in degrees), or to 34 | * give a profile to the extrusion (the endFactor will scale the face, and 35 | * the profile will define how the scale is applied (either linarly or with 36 | * a s-shape). 37 | */ 38 | extrude( 39 | extrusionDistance: number, 40 | extrusionConfig: { 41 | extrusionDirection?: Point; 42 | extrusionProfile?: ExtrusionProfile; 43 | twistAngle?: number; 44 | origin?: Point; 45 | } = {} 46 | ): AnyShape { 47 | const extruded = this.sketches.map((s) => 48 | s.extrude(extrusionDistance, extrusionConfig) 49 | ); 50 | 51 | return compoundShapes(extruded); 52 | } 53 | 54 | /** 55 | * Revolves the drawing on an axis (defined by its direction and an origin 56 | * (defaults to the sketch origin) 57 | */ 58 | revolve(revolutionAxis?: Point, config?: { origin?: Point }): AnyShape { 59 | return compoundShapes( 60 | this.sketches.map((s) => s.revolve(revolutionAxis, config)) 61 | ); 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /packages/replicad/src/sketches/index.ts: -------------------------------------------------------------------------------- 1 | import Sketch from "./Sketch"; 2 | import { SketchInterface } from "./lib"; 3 | import CompoundSketch from "./CompoundSketch"; 4 | import Sketches from "./Sketches"; 5 | 6 | export * from "./cannedSketches"; 7 | export { Sketch, CompoundSketch, Sketches }; 8 | export type { SketchInterface }; 9 | -------------------------------------------------------------------------------- /packages/replicad/src/sketches/lib.ts: -------------------------------------------------------------------------------- 1 | import { Face, Shape3D } from "../shapes"; 2 | import { Point } from "../geom"; 3 | 4 | import { ExtrusionProfile, LoftConfig } from "../addThickness.js"; 5 | 6 | export interface SketchInterface { 7 | /** 8 | * Transforms the lines into a face. The lines should be closed. 9 | */ 10 | face(): Face; 11 | 12 | /** 13 | * Revolves the drawing on an axis (defined by its direction and an origin 14 | * (defaults to the sketch origin) 15 | */ 16 | revolve(revolutionAxis?: Point, config?: { origin?: Point }): Shape3D; 17 | 18 | /** 19 | * Extrudes the sketch to a certain distance.(along the default direction 20 | * and origin of the sketch). 21 | * 22 | * You can define another extrusion direction or origin, 23 | * 24 | * It is also possible to twist extrude with an angle (in degrees), or to 25 | * give a profile to the extrusion (the endFactor will scale the face, and 26 | * the profile will define how the scale is applied (either linarly or with 27 | * a s-shape). 28 | */ 29 | extrude( 30 | extrusionDistance: number, 31 | extrusionConfig?: { 32 | extrusionDirection?: Point; 33 | extrusionProfile?: ExtrusionProfile; 34 | twistAngle?: number; 35 | origin?: Point; 36 | } 37 | ): Shape3D; 38 | 39 | /** 40 | * Loft between this sketch and another sketch (or an array of them) 41 | * 42 | * You can also define a `startPoint` for the loft (that will be placed 43 | * before this sketch) and an `endPoint` after the last one. 44 | * 45 | * You can also define if you want the loft to result in a ruled surface. 46 | * 47 | * Note that all sketches will be deleted by this operation 48 | */ 49 | loftWith( 50 | otherSketches: this | this[], 51 | loftConfig: LoftConfig, 52 | returnShell?: boolean 53 | ): Shape3D; 54 | } 55 | -------------------------------------------------------------------------------- /packages/replicad/src/utils/ProgressRange.ts: -------------------------------------------------------------------------------- 1 | import type { Message_ProgressRange } from "replicad-opencascadejs"; 2 | import { WrappingObj } from "../register"; 3 | import { getOC } from "../oclib"; 4 | 5 | export class ProgressRange extends WrappingObj { 6 | constructor() { 7 | const oc = getOC(); 8 | super(new oc.Message_ProgressRange_1()); 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /packages/replicad/src/utils/precisionRound.ts: -------------------------------------------------------------------------------- 1 | // from https://stackoverflow.com/a/49729715 2 | export default function precisionRound(number: number, precision: number) { 3 | const factor = Math.pow(10, precision); 4 | const n = precision < 0 ? number : 0.01 / factor + number; 5 | return Math.round(n * factor) / factor; 6 | } 7 | -------------------------------------------------------------------------------- /packages/replicad/src/utils/range.ts: -------------------------------------------------------------------------------- 1 | export default function range(len: number): number[] { 2 | return Array.from(Array(len).keys()); 3 | } 4 | -------------------------------------------------------------------------------- /packages/replicad/src/utils/round2.ts: -------------------------------------------------------------------------------- 1 | export default function round2(v: number): number { 2 | return Math.round(v * 100) / 100; 3 | } 4 | -------------------------------------------------------------------------------- /packages/replicad/src/utils/round5.ts: -------------------------------------------------------------------------------- 1 | export default function round5(v: number): number { 2 | return Math.round(v * 100000) / 100000; 3 | } 4 | -------------------------------------------------------------------------------- /packages/replicad/src/utils/uuid.ts: -------------------------------------------------------------------------------- 1 | export function uuidv() { 2 | // @ts-expect-error copied code 3 | return ([1e7] + -1e3 + -4e3 + -8e3 + -1e11).replace(/[018]/g, c => 4 | ( 5 | c ^ 6 | (crypto.getRandomValues(new Uint8Array(1))[0] & (15 >> (c / 4))) 7 | ).toString(16) 8 | ); 9 | } 10 | -------------------------------------------------------------------------------- /packages/replicad/src/utils/zip.ts: -------------------------------------------------------------------------------- 1 | import range from "./range"; 2 | 3 | export default function zip( 4 | arrays: T 5 | ): { [K in keyof T]: T[K] extends (infer V)[] ? V : never }[] { 6 | const minLength = Math.min(...arrays.map((arr) => arr.length)); 7 | // @ts-expect-error This is too much for ts 8 | return range(minLength).map((i) => arrays.map((arr) => arr[i])); 9 | } 10 | -------------------------------------------------------------------------------- /packages/replicad/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "moduleResolution": "node", 4 | "target": "es6", 5 | "module": "es2015", 6 | "strict": true, 7 | "lib": ["es2022", "dom"], 8 | "preserveSymlinks": true, 9 | "sourceMap": true, 10 | "skipLibCheck": true, 11 | "declaration": true, 12 | "allowSyntheticDefaultImports": true 13 | }, 14 | "include": ["src"], 15 | "typedocOptions": { 16 | "watch": true, 17 | "readme": "none", 18 | "useCodeBlocks": true, 19 | "categoryOrder": ["Drawing", "Import", "Finders", "Solids", "*"], 20 | "kindSortOrder": [ 21 | "Function", 22 | "Reference", 23 | "Project", 24 | "Module", 25 | "Namespace", 26 | "Enum", 27 | "EnumMember", 28 | "Class", 29 | "Interface", 30 | "TypeAlias", 31 | "Constructor", 32 | "Property", 33 | "Variable", 34 | "Accessor", 35 | "Method", 36 | "ObjectLiteral", 37 | "Parameter", 38 | "TypeParameter", 39 | "TypeLiteral", 40 | "CallSignature", 41 | "ConstructorSignature", 42 | "IndexSignature", 43 | "GetSignature", 44 | "SetSignature" 45 | ], 46 | "excludeProtected": true 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /packages/replicad/vite.config.js: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { defineConfig } from "vite"; 3 | import dts from "vite-plugin-dts"; 4 | 5 | export default defineConfig({ 6 | build: { 7 | lib: { 8 | entry: resolve(__dirname, "src/index.ts"), 9 | name: "replicad", 10 | fileName: "replicad", 11 | formats: ["es", "umd", "cjs"], 12 | }, 13 | sourcemap: true, 14 | minify: false, 15 | }, 16 | plugins: [ 17 | process.env.NO_TYPES?.toLowerCase() === "true" 18 | ? null 19 | : dts({ 20 | rollupTypes: true, 21 | }), 22 | ].filter((a) => !!a), 23 | test: { 24 | setupFiles: ["./__tests__/setup.ts"], 25 | }, 26 | }); 27 | -------------------------------------------------------------------------------- /packages/studio/.gitignore: -------------------------------------------------------------------------------- 1 | dist 2 | -------------------------------------------------------------------------------- /packages/studio/LICENSE: -------------------------------------------------------------------------------- 1 | Copyright 2023 QuaroTech Sàrl 2 | 3 | Permission is hereby granted, free of charge, to any person obtaining a copy of this software and associated documentation files (the “Software”), to deal in the Software without restriction, including without limitation the rights to use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons to whom the Software is furnished to do so, subject to the following conditions: 4 | 5 | The above copyright notice and this permission notice shall be included in all copies or substantial portions of the Software. 6 | 7 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. 8 | -------------------------------------------------------------------------------- /packages/studio/README.md: -------------------------------------------------------------------------------- 1 | ## replicad studio 2 | 3 | This repo contains the code of the [replicad studio 4 | website](studio.replicad.xyz). 5 | 6 | You can run it locall by cloning the repo and running (you can use another 7 | package manager as well): 8 | 9 | ```bash 10 | cp -r replicad/packages/studio my-studio 11 | cd my-studio 12 | npm install 13 | ``` 14 | 15 | You can then run it locally (for development purposes) with: 16 | 17 | ``` 18 | # in the my-studio directory 19 | npm run start 20 | ``` 21 | 22 | You can also build it: 23 | 24 | ```bash 25 | # in the my-studio directory 26 | npm run build 27 | ``` 28 | 29 | The assets will be in the `dist` directory. You can serve them locally with: 30 | 31 | ```bash 32 | npm run serve 33 | ``` 34 | 35 | But I would advise you to use a proper web server if you want to expose it to 36 | the web (this is a pure static website). 37 | -------------------------------------------------------------------------------- /packages/studio/__tests__/studio.test.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | const studio = require('..'); 4 | const assert = require('assert').strict; 5 | 6 | assert.strictEqual(studio(), 'Hello from studio'); 7 | console.info('studio tests passed'); 8 | -------------------------------------------------------------------------------- /packages/studio/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Replicad Studio 5 | 9 | 10 | 11 | 12 | 13 | 14 | 15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /packages/studio/lib/studio.js: -------------------------------------------------------------------------------- 1 | 'use strict'; 2 | 3 | module.exports = studio; 4 | 5 | function studio() { 6 | return 'Hello from studio'; 7 | } 8 | -------------------------------------------------------------------------------- /packages/studio/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "studio", 3 | "version": "0.19.0", 4 | "description": "The replicad studio", 5 | "author": "Steve Genoud ", 6 | "homepage": "https://studio.replicad.xyz", 7 | "license": "MIT", 8 | "main": "lib/studio.js", 9 | "type": "module", 10 | "private": true, 11 | "directories": { 12 | "lib": "lib", 13 | "test": "__tests__" 14 | }, 15 | "files": [ 16 | "lib" 17 | ], 18 | "repository": { 19 | "type": "git", 20 | "url": "git+https://github.com/sgenoud/replicad.git" 21 | }, 22 | "bugs": { 23 | "url": "https://github.com/sgenoud/replicad/issues" 24 | }, 25 | "scripts": { 26 | "lint": "eslint", 27 | "build": "vite build", 28 | "serve": "vite preview", 29 | "start": "vite" 30 | }, 31 | "dependencies": { 32 | "@devbookhq/splitter": "^1.4.2", 33 | "@monaco-editor/react": "^4.6.0", 34 | "@react-three/drei": "9.120.4", 35 | "@react-three/fiber": "8.17.10", 36 | "axios": "^0.24.0", 37 | "browser-fs-access": "^0.20.5", 38 | "comlink": "^4.4.2", 39 | "debounce": "^1.2.1", 40 | "idb-keyval": "^6.2.1", 41 | "jszip": "^3.10.1", 42 | "leva": "^0.9.35", 43 | "mobx": "^6.13.5", 44 | "mobx-react": "^9.2.0", 45 | "mobx-state-tree": "^5.4.2", 46 | "monaco-editor": "^0.51.0", 47 | "parse-css-color": "^0.2.1", 48 | "polished": "^4.3.1", 49 | "react": "^18.3.1", 50 | "react-dom": "^18.3.1", 51 | "react-portal": "^4.2.2", 52 | "react-router-dom": "^5.3.4", 53 | "react-use-rect": "^2.0.6", 54 | "replicad": "workspace:^", 55 | "replicad-opencascadejs": "workspace:^", 56 | "replicad-threejs-helper": "workspace:^", 57 | "styled-components": "^5.3.11", 58 | "three": "^0.155.0", 59 | "vite-plugin-pwa": "^0.16.7" 60 | }, 61 | "devDependencies": { 62 | "@types/react-dom": "^18.3.5", 63 | "@types/styled-components": "5.1.9", 64 | "@types/three": "^0.155.1", 65 | "@typescript-eslint/eslint-plugin": "^6.21.0", 66 | "@typescript-eslint/parser": "^6.21.0", 67 | "@vitejs/plugin-react": "^4.3.4", 68 | "babel-plugin-styled-components": "1.13.3", 69 | "binaryen": "^101.0.0", 70 | "eslint": "^8.57.1", 71 | "eslint-plugin-react": "^7.37.2", 72 | "eslint-plugin-react-hooks": "^4.6.2", 73 | "prettier": "^2.8.8", 74 | "vite": "^4.5.5", 75 | "workbox-window": "^7.3.0" 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /packages/studio/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/favicon.ico -------------------------------------------------------------------------------- /packages/studio/public/fonts/HKGrotesk-Bold.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/fonts/HKGrotesk-Bold.woff2 -------------------------------------------------------------------------------- /packages/studio/public/fonts/HKGrotesk-Light.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/fonts/HKGrotesk-Light.woff2 -------------------------------------------------------------------------------- /packages/studio/public/fonts/HKGrotesk-LightItalic.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/fonts/HKGrotesk-LightItalic.woff2 -------------------------------------------------------------------------------- /packages/studio/public/fonts/HKGrotesk-Regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/fonts/HKGrotesk-Regular.ttf -------------------------------------------------------------------------------- /packages/studio/public/fonts/HKGrotesk-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/fonts/HKGrotesk-Regular.woff2 -------------------------------------------------------------------------------- /packages/studio/public/icon-192x192.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/icon-192x192.png -------------------------------------------------------------------------------- /packages/studio/public/icon-256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/icon-256x256.png -------------------------------------------------------------------------------- /packages/studio/public/icon-384x384.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/icon-384x384.png -------------------------------------------------------------------------------- /packages/studio/public/icon-512x512.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/icon-512x512.png -------------------------------------------------------------------------------- /packages/studio/public/manifest.webmanifest: -------------------------------------------------------------------------------- 1 | { 2 | "short_name": "Replicad Studio", 3 | "name": "The sandbox for your replicad projects", 4 | "icons": [ 5 | { 6 | "src": "favicon.ico", 7 | "sizes": "64x64 32x32 24x24 16x16", 8 | "type": "image/x-icon" 9 | }, 10 | { 11 | "src": "icon-192x192.png", 12 | "sizes": "192x192", 13 | "type": "image/png" 14 | }, 15 | { 16 | "src": "icon-256x256.png", 17 | "sizes": "256x256", 18 | "type": "image/png" 19 | }, 20 | { 21 | "src": "icon-384x384.png", 22 | "sizes": "384x384", 23 | "type": "image/png" 24 | }, 25 | { 26 | "src": "icon-512x512.png", 27 | "sizes": "512x512", 28 | "type": "image/png" 29 | } 30 | ], 31 | "start_url": ".", 32 | "display": "standalone", 33 | "theme_color": "rgb(90, 130, 150)", 34 | "background_color": "#ffffff" 35 | } 36 | -------------------------------------------------------------------------------- /packages/studio/public/robots.txt: -------------------------------------------------------------------------------- 1 | # https://www.robotstxt.org/robotstxt.html 2 | User-agent: * 3 | Disallow: 4 | -------------------------------------------------------------------------------- /packages/studio/public/textures/matcap-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/sgenoud/replicad/bf2c9bfa07b69599e810a6b8e33f25139df1d497/packages/studio/public/textures/matcap-1.png -------------------------------------------------------------------------------- /packages/studio/src/App.jsx: -------------------------------------------------------------------------------- 1 | import "replicad-opencascadejs/src/replicad_single.wasm?url"; 2 | 3 | import React from "react"; 4 | import { Switch, Route, Redirect } from "react-router-dom"; 5 | 6 | import Welcome from "./Welcome.jsx"; 7 | import Editor from "./visualiser/Editor.jsx"; 8 | import LinkWidget, { MakeLink } from "./LinkWidget.jsx"; 9 | 10 | import ReloadPrompt from "./ReloadPrompt.jsx"; 11 | import LoadingScreen from "./components/LoadingScreen.jsx"; 12 | 13 | const Workbench = React.lazy(() => import("./workbench/Workbench.jsx")); 14 | 15 | export default function App() { 16 | return ( 17 | <> 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | }> 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | ); 50 | } 51 | -------------------------------------------------------------------------------- /packages/studio/src/GlobalStyles.jsx: -------------------------------------------------------------------------------- 1 | import { createGlobalStyle } from "styled-components"; 2 | 3 | import { normalize } from "polished"; 4 | 5 | export default createGlobalStyle` 6 | ${normalize()} 7 | 8 | @font-face { 9 | font-family: "HKGrotesk"; 10 | font-weight: 500; 11 | src: url("/fonts/HKGrotesk-Regular.woff2") format("woff2"); 12 | } 13 | 14 | @font-face { 15 | font-family: "HKGrotesk"; 16 | font-weight: 300; 17 | src: url("/fonts/HKGrotesk-Light.woff2") format("woff2"); 18 | } 19 | 20 | @font-face { 21 | font-family: "HKGrotesk"; 22 | font-weight: bold; 23 | src: url("/fonts/HKGrotesk-Bold.woff2") format("woff2"); 24 | } 25 | 26 | 27 | body { 28 | font-family: HKGrotesk, sans-serif; 29 | font-weight: 300; 30 | overflow-x: hidden; 31 | } 32 | 33 | * { 34 | box-sizing: border-box; 35 | } 36 | 37 | html, body { 38 | box-sizing: border-box; 39 | background-color: white; 40 | color: var(--text-color); 41 | fill: var(--text-color); 42 | scroll-behavior: smooth; 43 | } 44 | 45 | html, body, #root { 46 | min-height: 100%; 47 | width: 100%; 48 | display: flex; 49 | } 50 | 51 | :root { 52 | --text-color: #444; 53 | --bg-color-secondary: white; 54 | 55 | --color-primary: rgb(90, 130, 150); 56 | --color-primary-light: rgb(170, 190, 200); 57 | --color-primary-dark: rgb(60, 90, 110); 58 | --color-secondary: #d49991; 59 | --color-secondary-light: #f2e0de; 60 | --color-secondary-dark: #c0695d; 61 | --bg-color: #f2f3f4; 62 | 63 | --color-header-primary: rgb(90, 130, 150); 64 | --color-header-secondary: rgb(60, 90, 110); 65 | 66 | --color-lines: #ccc; 67 | } 68 | 69 | @media (prefers-color-scheme: dark) { 70 | :root { 71 | --color-primary: rgb(122, 176, 204); 72 | --color-primary-light: rgb(60, 90, 110); 73 | --color-primary-dark: rgb(170, 190, 200); 74 | --bg-color: #1e1e1e; 75 | --bg-color-secondary: #2e2e2e; 76 | --text-color: #f2f2f2; 77 | 78 | --color-lines: #333; 79 | } 80 | } 81 | 82 | 83 | a { 84 | color: var(--color-primary); 85 | } 86 | `; 87 | -------------------------------------------------------------------------------- /packages/studio/src/ReloadPrompt.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | import { Button } from "./components/Button"; 5 | 6 | import { useRegisterSW } from "virtual:pwa-register/react"; 7 | 8 | const ButtonsList = styled.div` 9 | display: flex; 10 | justify-content: flex-end; 11 | 12 | & > :not(:last-child) { 13 | margin-right: 1em; 14 | } 15 | `; 16 | 17 | const Toast = styled.div` 18 | position: fixed; 19 | right: 0; 20 | top: 0; 21 | margin-top: 4em; 22 | padding: 12px; 23 | border: 1px solid var(--color-primary); 24 | border-right-style: none; 25 | border-radius: 4px 0 0 4px; 26 | z-index: 1; 27 | text-align: left; 28 | box-shadow: 3px 4px 5px 0 var(--bg-color); 29 | background-color: var(--bg-color-secondary); 30 | 31 | & > :not(:last-child) { 32 | margin-bottom: 8px; 33 | } 34 | `; 35 | 36 | function ReloadPrompt() { 37 | const { 38 | needRefresh: [needRefresh, setNeedRefresh], 39 | updateServiceWorker, 40 | } = useRegisterSW({ 41 | onRegistered(r) { 42 | r && 43 | setInterval(() => { 44 | r.update(); 45 | }, 24 * 3600000); 46 | //}, 20000 /* 0s for testing purposes */); 47 | }, 48 | onRegisterError(error) { 49 | console.log("SW registration error", error); 50 | }, 51 | }); 52 | 53 | const close = () => { 54 | setNeedRefresh(false); 55 | }; 56 | 57 | if (!needRefresh) return null; 58 | 59 | return ( 60 | 61 |
62 | The site has been updated, click reload to refresh. 63 |
64 | 65 | {needRefresh && ( 66 | 74 | )} 75 | 76 | 77 |
78 | ); 79 | } 80 | 81 | export default ReloadPrompt; 82 | -------------------------------------------------------------------------------- /packages/studio/src/components-3d/Controls.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef } from "react"; 2 | import { OrbitControls, GizmoHelper, GizmoViewport } from "@react-three/drei"; 3 | import Stage from "./Stage.jsx"; 4 | 5 | const Controls = React.memo( 6 | React.forwardRef(function Controls( 7 | { hideGizmo, enableDamping }, 8 | controlsRef 9 | ) { 10 | return ( 11 | <> 12 | 17 | {!hideGizmo && ( 18 | 19 | 20 | 21 | )} 22 | 23 | ); 24 | }) 25 | ); 26 | 27 | export default React.memo(function Scene({ 28 | hideGizmo, 29 | center, 30 | enableDamping = true, 31 | children, 32 | }) { 33 | const controlsRef = useRef(); 34 | 35 | return ( 36 | <> 37 | 42 | 43 | {children} 44 | 45 | 46 | ); 47 | }); 48 | -------------------------------------------------------------------------------- /packages/studio/src/components-3d/DefaultGeometry.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useLayoutEffect } from "react"; 2 | import { Sphere, MeshDistortMaterial } from "@react-three/drei"; 3 | import { useThree } from "@react-three/fiber"; 4 | 5 | export default function DefaultGeometry() { 6 | const camera = useThree((state) => state.camera); 7 | const set = useThree((state) => state.set); 8 | const frameloop = useThree((state) => state.frameloop); 9 | const originalFrameloop = useRef(frameloop); 10 | 11 | useLayoutEffect(() => { 12 | if (originalFrameloop.current !== "demand") return; 13 | set({ frameloop: "always" }); 14 | return () => set({ frameloop: "demand" }); 15 | }, [set]); 16 | 17 | return ( 18 | 19 | 20 | 21 | 22 | 23 | 30 | 31 | 32 | ); 33 | } 34 | -------------------------------------------------------------------------------- /packages/studio/src/components/ButtonMenu.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | import { InfoTopLeft } from "./FloatingInfo.jsx"; 3 | import { Button } from "./Button.jsx"; 4 | 5 | export const InfoMenu = styled(InfoTopLeft)` 6 | & > * { 7 | flex-shrink: 0; 8 | } 9 | & > :not(:first-child) { 10 | margin-top: 0.3em; 11 | } 12 | 13 | opacity: ${(props) => (props.hide ? 0 : 1)}; 14 | transition: opacity 0.5s ease-in-out; 15 | 16 | :hover { 17 | opacity: 1; 18 | } 19 | 20 | @media (max-width: 400px) { 21 | margin-top: 40px; 22 | } 23 | `; 24 | export const ContextButton = styled(Button)` 25 | font-size: 1.5em; 26 | position: relative; 27 | margin: auto; 28 | `; 29 | -------------------------------------------------------------------------------- /packages/studio/src/components/ErrorBoundary.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default class ErrorBoundary extends React.Component { 4 | constructor(props) { 5 | super(props); 6 | this.state = { hasError: false }; 7 | } 8 | 9 | static getDerivedStateFromError(error) { 10 | // Update state so the next render will show the fallback UI. 11 | return { hasError: true }; 12 | } 13 | 14 | componentDidCatch(error, errorInfo) { 15 | // You can also log the error to an error reporting service 16 | console.error(error, errorInfo); 17 | } 18 | 19 | render() { 20 | if (this.state.hasError) { 21 | // You can render any custom fallback UI 22 | return

Something went wrong.

; 23 | } 24 | 25 | return this.props.children; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /packages/studio/src/components/FloatingInfo.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export const InfoTopRight = styled.div` 4 | display: flex; 5 | flex-direction: column; 6 | position: absolute; 7 | top: 3.5em; 8 | right: 2em; 9 | max-width: 300px; 10 | padding: 8px; 11 | border-radius: 10px; 12 | max-height: calc(100% - 5em); 13 | overflow-y: auto; 14 | 15 | ${(props) => 16 | props.noBg 17 | ? "" 18 | : `background-color: var(--bg-color); border: 1px solid var(--color-primary-light);`} 19 | `; 20 | 21 | export const InfoBottomLeft = styled(InfoTopRight)` 22 | top: auto; 23 | right: auto; 24 | bottom: 2em; 25 | left: 2em; 26 | `; 27 | 28 | export const InfoBottomRight = styled(InfoTopRight)` 29 | top: auto; 30 | bottom: 2em; 31 | `; 32 | 33 | export const InfoTopLeft = styled(InfoTopRight)` 34 | right: auto; 35 | left: 2em; 36 | `; 37 | -------------------------------------------------------------------------------- /packages/studio/src/components/LoadingScreen.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | const StyledCanvas = styled.div` 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | width: 100%; 9 | height: 100%; 10 | background-color: #f2f3f4; 11 | color: var(--color-primary-light); 12 | `; 13 | 14 | const LoadingAnimation = ({ size = "1em" }) => ( 15 | 21 | 30 | 40 | 49 | 50 | 51 | 60 | 70 | 71 | 72 | ); 73 | 74 | export default function LoadingScreen() { 75 | return ( 76 | 77 | 78 | 79 | ); 80 | } 81 | -------------------------------------------------------------------------------- /packages/studio/src/components/ToolUI.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import styled from "styled-components"; 3 | 4 | export const ParamsSection = styled.div` 5 | background-color: var(--bg-color); 6 | font-family: ui-monospace, SFMono-Regular, Menlo, "Roboto Mono", monospace; 7 | font-size: 11px; 8 | 9 | padding-top: 10px; 10 | padding-bottom: 10px; 11 | 12 | row-gap: 10px; 13 | `; 14 | 15 | export const LabelledBlockWrapper = styled.div` 16 | display: flex; 17 | flex-direction: column; 18 | padding-left: 10px; 19 | padding-right: 10px; 20 | 21 | & > label { 22 | display: flex; 23 | height: 24px; 24 | align-items: center; 25 | color: var(--color-primary); 26 | } 27 | `; 28 | 29 | export const LabelledBlock = ({ label, labelFor, children }) => { 30 | return ( 31 | 32 | 33 | {children} 34 | 35 | ); 36 | }; 37 | -------------------------------------------------------------------------------- /packages/studio/src/components/Toolbar.jsx: -------------------------------------------------------------------------------- 1 | import styled from "styled-components"; 2 | 3 | export default styled.div` 4 | position: absolute; 5 | left: 50%; 6 | transform: translateX(-50%); 7 | background-color: white; 8 | 9 | color: var(--color-primary); 10 | border: lightgrey solid 1px; 11 | border-top: none; 12 | background-color: #fff; 13 | border-radius: 0 0 0.2em 0.2em; 14 | top: 0; 15 | 16 | padding: 0.4em; 17 | font-size: 1.4em; 18 | `; 19 | -------------------------------------------------------------------------------- /packages/studio/src/icons/Clipping.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Clipping({ size = "1em" }) { 4 | return ( 5 | 11 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/studio/src/icons/Configure.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function Configure({ size = "1em" }) { 3 | return ( 4 | 10 | 11 | 12 | ); 13 | } 14 | -------------------------------------------------------------------------------- /packages/studio/src/icons/Download.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Download({ size = "1em", text }) { 4 | return ( 5 | 12 | 13 | {text && 14 | {text} 15 | } 16 | 17 | ); 18 | }; 19 | -------------------------------------------------------------------------------- /packages/studio/src/icons/Focus.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Focus({ size = "1em" }) { 4 | return ( 5 | 6 | 12 | 13 | ); 14 | } 15 | -------------------------------------------------------------------------------- /packages/studio/src/icons/Fullscreen.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default ({ size = "1em" }) => { 4 | return ( 5 | 11 | 12 | 13 | ); 14 | }; 15 | -------------------------------------------------------------------------------- /packages/studio/src/icons/Loading.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | export default function LoadingAnimation({ size = "1em" }) { 3 | return ( 4 | 10 | 19 | 29 | 30 | 31 | 41 | 42 | 43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /packages/studio/src/icons/NewWindow.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default ({ size = "1em" }) => { 4 | return ( 5 | 6 | 10 | 11 | ); 12 | }; 13 | -------------------------------------------------------------------------------- /packages/studio/src/icons/Reload.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Reload({ size = "1em" }) { 4 | return ( 5 | 10 | 14 | 15 | ); 16 | } 17 | -------------------------------------------------------------------------------- /packages/studio/src/icons/Share.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | export default function Download({ size = "1em" }) { 4 | return ( 5 | 12 | 13 | 14 | ); 15 | } 16 | -------------------------------------------------------------------------------- /packages/studio/src/index.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { createRoot } from "react-dom/client"; 3 | import * as THREE from "three"; 4 | 5 | import App from "./App.jsx"; 6 | import GlobalStyle from "./GlobalStyles.jsx"; 7 | 8 | import "replicad-opencascadejs/src/replicad_single.wasm?url"; 9 | import "replicad-opencascadejs/src/replicad_with_exceptions.wasm?url"; 10 | 11 | import { BrowserRouter } from "react-router-dom"; 12 | 13 | THREE.Object3D.DEFAULT_UP.set(0, 0, 1); 14 | 15 | const root = createRoot(document.getElementById("root")); 16 | 17 | root.render( 18 | <> 19 | 20 | 21 | 22 | 23 | 24 | ); 25 | -------------------------------------------------------------------------------- /packages/studio/src/initOCSingle.js: -------------------------------------------------------------------------------- 1 | import opencascade from "replicad-opencascadejs/src/replicad_single.js"; 2 | import opencascadeWasm from "replicad-opencascadejs/src/replicad_single.wasm?url"; 3 | 4 | export default async () => { 5 | const OC = await opencascade({ 6 | locateFile: () => opencascadeWasm, 7 | }); 8 | 9 | return OC; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/studio/src/initOCWithExceptions.js: -------------------------------------------------------------------------------- 1 | import opencascade from "replicad-opencascadejs/src/replicad_with_exceptions.js"; 2 | import opencascadeWasm from "replicad-opencascadejs/src/replicad_with_exceptions.wasm?url"; 3 | 4 | export default async () => { 5 | const OC = await opencascade({ 6 | locateFile: () => opencascadeWasm, 7 | }); 8 | 9 | return OC; 10 | }; 11 | -------------------------------------------------------------------------------- /packages/studio/src/utils/StudioHelper.js: -------------------------------------------------------------------------------- 1 | const shapeOrSketch = (shape) => { 2 | if (!(shape instanceof replicad.Sketch)) return shape; 3 | if (shape.wire.isClosed) return shape.face(); 4 | return shape.wire; 5 | }; 6 | 7 | export class StudioHelper { 8 | constructor() { 9 | this._shapes = []; 10 | this._faceFinder = null; 11 | this._edgeFinder = null; 12 | } 13 | 14 | debug(shape) { 15 | this._shapes.push(shape); 16 | return shape; 17 | } 18 | 19 | d(shape) { 20 | return this.debug(shape); 21 | } 22 | 23 | highlightFace(faceFinder) { 24 | this._faceFinder = faceFinder; 25 | return faceFinder; 26 | } 27 | 28 | hf(faceFinder) { 29 | return this.highlightFace(faceFinder); 30 | } 31 | 32 | highlightEdge(edgeFinder) { 33 | this._edgeFinder = edgeFinder; 34 | return edgeFinder; 35 | } 36 | 37 | he(edgeFinder) { 38 | return this.highlightEdge(edgeFinder); 39 | } 40 | 41 | apply(config) { 42 | const conf = config.concat( 43 | this._shapes.map((s, i) => ({ 44 | shape: shapeOrSketch(s), 45 | name: `Debug ${i}`, 46 | })) 47 | ); 48 | conf.forEach((shape) => { 49 | if (this._edgeFinder && !shape.highlightEdge) { 50 | shape.highlightEdge = this._edgeFinder; 51 | } 52 | if (this._faceFinder && !shape.highlightFace) { 53 | shape.highlightFace = this._faceFinder; 54 | } 55 | }); 56 | return conf; 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /packages/studio/src/utils/builderAPI.js: -------------------------------------------------------------------------------- 1 | // I keep this around for debugging purpose (the reloading is faster when 2 | // working on files in the worker.) 3 | // 4 | //import api from "../builder.worker"; /* 5 | import normalizeColor from "./normalizeColor"; 6 | 7 | import { wrap } from "comlink"; 8 | import CadWorker from "../builder.worker?worker"; 9 | const api = wrap(new CadWorker()); 10 | /**/ 11 | 12 | export default api; 13 | -------------------------------------------------------------------------------- /packages/studio/src/utils/diskFileAccess.js: -------------------------------------------------------------------------------- 1 | import { fileOpen } from "browser-fs-access"; 2 | import { get, set } from "idb-keyval"; 3 | 4 | const HANDLE_ID = "file-handle"; 5 | 6 | export const requestFile = async () => { 7 | const blob = await fileOpen({ id: "source" }); 8 | if (!blob?.handle) return; 9 | 10 | set(HANDLE_ID, blob.handle); 11 | return blob.handle; 12 | }; 13 | 14 | export const loadFile = async () => { 15 | const handle = await get(HANDLE_ID); 16 | if (!handle) return; 17 | 18 | if ((await handle.queryPermission({ mode: "read" })) === "granted") { 19 | return handle; 20 | } 21 | if ((await handle.requestPermission({ mode: "read" })) === "granted") { 22 | return handle; 23 | } 24 | }; 25 | 26 | export const clearFileSave = async () => { 27 | set(HANDLE_ID, null); 28 | }; 29 | 30 | export const getSavedHandleName = async () => { 31 | const handle = await get(HANDLE_ID); 32 | if (!handle) return; 33 | return handle.name; 34 | }; 35 | -------------------------------------------------------------------------------- /packages/studio/src/utils/downloadCode.js: -------------------------------------------------------------------------------- 1 | import { fileSave } from "browser-fs-access"; 2 | import builderAPI from "./builderAPI"; 3 | 4 | export default async (code, fileName) => { 5 | fileName = 6 | (await builderAPI.extractDefaultNameFromCode(code)) || 7 | fileName || 8 | "replicad-script"; 9 | return fileSave( 10 | new Blob([code], { 11 | type: "application/javascript", 12 | }), 13 | { 14 | id: "save-js", 15 | fileName: `${fileName}.js`, 16 | description: "JS replicad script of the current geometry", 17 | extensions: [".js"], 18 | } 19 | ); 20 | }; 21 | -------------------------------------------------------------------------------- /packages/studio/src/utils/dumpCode.js: -------------------------------------------------------------------------------- 1 | import JSZip from "jszip"; 2 | 3 | export async function dumpCode(rawCode) { 4 | const zip = new JSZip(); 5 | zip.file("code.js", rawCode); 6 | const content = await zip.generateAsync({ 7 | type: "base64", 8 | compression: "DEFLATE", 9 | compressionOptions: { 10 | level: 6, 11 | }, 12 | }); 13 | return encodeURIComponent(content); 14 | } 15 | -------------------------------------------------------------------------------- /packages/studio/src/utils/loadCode.js: -------------------------------------------------------------------------------- 1 | import JSZip from 'jszip' 2 | 3 | export default async (rawCode) => { 4 | const content = decodeURIComponent(rawCode); 5 | const zip = await new JSZip().loadAsync(content, { base64: true }); 6 | return await zip?.file("code.js")?.async("string"); 7 | }; 8 | -------------------------------------------------------------------------------- /packages/studio/src/utils/normalizeColor.ts: -------------------------------------------------------------------------------- 1 | import parse from "parse-css-color"; 2 | 3 | const rgbToHex = (color: [number, number, number]) => { 4 | const [r, g, b] = color; 5 | 6 | return ( 7 | "#" + 8 | [r, g, b] 9 | .map((x) => { 10 | const hex = x.toString(16); 11 | return hex.length === 1 ? "0" + hex : hex; 12 | }) 13 | .join("") 14 | ); 15 | }; 16 | 17 | export default function normalizeColor(color: string) { 18 | const parsed = parse(color); 19 | 20 | if (!parsed || parsed.type.startsWith("hsl")) 21 | return { color: "#fff", alpha: 1 }; 22 | return { color: rgbToHex(parsed.values as [number, number, number]), alpha: parsed.alpha }; 23 | } 24 | -------------------------------------------------------------------------------- /packages/studio/src/utils/saveShape.js: -------------------------------------------------------------------------------- 1 | import builderAPI from "./builderAPI"; 2 | import { fileSave } from "browser-fs-access"; 3 | import JSZip from "jszip/dist/jszip"; 4 | 5 | const EXTS = new Map([ 6 | ["stl-binary", "stl"], 7 | ["step-assembly", "step"], 8 | ]); 9 | const mapExt = (ext) => { 10 | if (EXTS.has(ext)) return EXTS.get(ext); 11 | return ext; 12 | }; 13 | 14 | export default async function saveShapes(shapeId, fileType = "stl", code) { 15 | const defaultName = 16 | code && (await builderAPI.extractDefaultNameFromCode(code)); 17 | const shapes = await builderAPI.exportShape(fileType, shapeId); 18 | if (shapes.length === 1) { 19 | const { blob, name } = shapes[0]; 20 | const ext = mapExt(fileType); 21 | 22 | await fileSave(blob, { 23 | fileName: `${defaultName || name || "shape"}.${ext}`, 24 | extensions: [`.${ext}`], 25 | description: `Save ${defaultName || name || "shape"} as ${fileType}`, 26 | }); 27 | return; 28 | } 29 | 30 | const zip = new JSZip(); 31 | shapes.forEach((shape, i) => { 32 | zip.file(`${shape.name || `shape-${i}`}.${mapExt(fileType)}`, shape.blob); 33 | }); 34 | const zipBlob = await zip.generateAsync({ type: "blob" }); 35 | await fileSave(zipBlob, { 36 | id: "exports", 37 | description: "Save zip", 38 | fileName: `${defaultName || "shapes"}.zip`, 39 | extensions: [".zip"], 40 | }); 41 | } 42 | -------------------------------------------------------------------------------- /packages/studio/src/utils/useDarkMode.js: -------------------------------------------------------------------------------- 1 | import { useState, useEffect } from "react"; 2 | 3 | export function useDarkMode() { 4 | const [isDarkMode, setIsDarkMode] = useState(false); 5 | 6 | useEffect(() => { 7 | // Create the media query inside useEffect to ensure browser environment 8 | const mediaQuery = window?.matchMedia("(prefers-color-scheme: dark)"); 9 | 10 | if (!mediaQuery) { 11 | return; 12 | } 13 | 14 | // Define the change handler 15 | const handleChange = (event) => { 16 | setIsDarkMode(event.matches); 17 | }; 18 | 19 | // Add the listener 20 | mediaQuery.addEventListener("change", handleChange); 21 | 22 | // Set initial value 23 | setIsDarkMode(mediaQuery.matches); 24 | 25 | // Clean up 26 | return () => { 27 | mediaQuery.removeEventListener("change", handleChange); 28 | }; 29 | }, []); 30 | 31 | return isDarkMode; 32 | } 33 | -------------------------------------------------------------------------------- /packages/studio/src/utils/zip.js: -------------------------------------------------------------------------------- 1 | export default function zip(arrays) { 2 | return arrays[0].map((_, i) => arrays.map((array) => array[i])); 3 | } 4 | -------------------------------------------------------------------------------- /packages/studio/src/viewers/Canvas.jsx: -------------------------------------------------------------------------------- 1 | import React, { Suspense } from "react"; 2 | import { Canvas as ThreeCanvas } from "@react-three/fiber"; 3 | import styled from "styled-components"; 4 | import LoadingScreen from "../components/LoadingScreen.jsx"; 5 | 6 | const StyledCanvas = styled(ThreeCanvas)` 7 | width: 100%; 8 | height: 100%; 9 | background-color: var(--bg-color); 10 | `; 11 | 12 | export default function Canvas({ children, ...props }) { 13 | const dpr = Math.min(window.devicePixelRatio, 2); 14 | 15 | return ( 16 | }> 17 | 18 | {children} 19 | 20 | 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /packages/studio/src/viewers/Material.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | import { useTexture } from "@react-three/drei"; 3 | 4 | export default function Material(props) { 5 | const [matcap1] = useTexture(["/textures/matcap-1.png"]); 6 | return ; 7 | } 8 | -------------------------------------------------------------------------------- /packages/studio/src/viewers/NicePresentationViewer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Canvas from "./Canvas.jsx"; 4 | 5 | import Controls from "../components-3d/Controls.jsx"; 6 | import { ShapeGeometries } from "../components-3d/ShapeGeometry.jsx"; 7 | import DefaultGeometry from "../components-3d/DefaultGeometry.jsx"; 8 | 9 | const PrettyMaterial = ({ ...props }) => { 10 | return ; 11 | }; 12 | 13 | export default React.memo(function NicePresentationViewer({ 14 | shapes, 15 | orthographicCamera, 16 | disableAutoPosition, 17 | }) { 18 | return ( 19 | 20 | 21 | 26 | 27 | 28 | {shapes !== "error" && shapes.length && ( 29 | 36 | )} 37 | {shapes === "error" && } 38 | 39 | 40 | ); 41 | }); 42 | -------------------------------------------------------------------------------- /packages/studio/src/viewers/PresentationViewer.jsx: -------------------------------------------------------------------------------- 1 | import React from "react"; 2 | 3 | import Canvas from "./Canvas.jsx"; 4 | import Material from "./Material.jsx"; 5 | 6 | import Controls from "../components-3d/Controls.jsx"; 7 | import { ShapeGeometries } from "../components-3d/ShapeGeometry.jsx"; 8 | import DefaultGeometry from "../components-3d/DefaultGeometry.jsx"; 9 | import InfiniteGrid from "../components-3d/InfiniteGrid.jsx"; 10 | 11 | export default React.memo(function PresentationViewer({ 12 | shapes, 13 | disableAutoPosition = false, 14 | disableDamping = false, 15 | hideGrid = false, 16 | orthographicCamera = false, 17 | }) { 18 | const geometryReady = shapes && shapes.length && shapes[0].name; 19 | 20 | return ( 21 | 22 | {!hideGrid && } 23 | 24 | {shapes !== "error" && shapes.length && ( 25 | 31 | )} 32 | {shapes === "error" && } 33 | 34 | 35 | ); 36 | }); 37 | -------------------------------------------------------------------------------- /packages/studio/src/visualiser/editor/ParamsEditor.jsx: -------------------------------------------------------------------------------- 1 | import React, { useRef, useEffect, useMemo } from "react"; 2 | import { observer } from "mobx-react"; 3 | import { useControls, levaStore, Leva } from "leva"; 4 | 5 | export default observer(function ParamsEditor({ 6 | defaultParams, 7 | hidden, 8 | onRun, 9 | }) { 10 | const runFcn = useRef(onRun); 11 | useEffect(() => { 12 | runFcn.current = onRun; 13 | }, [onRun]); 14 | 15 | const paramsConfig = useMemo(() => { 16 | return { 17 | _run: { 18 | type: "BUTTON", 19 | onClick: (get) => 20 | runFcn.current( 21 | Object.fromEntries( 22 | levaStore 23 | .getVisiblePaths() 24 | .filter((f) => f !== "_run") 25 | .map((f) => [f, get(f)]) 26 | ) 27 | ), 28 | settings: { disabled: false }, 29 | label: "Apply params", 30 | }, 31 | ...defaultParams, 32 | }; 33 | }, [defaultParams]); 34 | 35 | useControls(() => paramsConfig, [defaultParams]); 36 | 37 | useEffect( 38 | () => () => { 39 | levaStore.dispose(); 40 | }, 41 | [] 42 | ); 43 | 44 | return ( 45 |