├── .DS_Store ├── .github ├── PULL_REQUEST_TEMPLATE.md └── workflows │ └── release_notes.yaml ├── .gitignore ├── LICENSE.md ├── developer.md ├── docs ├── .gitignore ├── README.md ├── docs │ ├── intro.md │ └── tutorial-create-elements │ │ ├── _category_.json │ │ ├── create-custom-elements.md │ │ ├── create-door.md │ │ ├── create-double-window.md │ │ ├── create-wall.md │ │ └── create-window.md ├── docusaurus.config.ts ├── package-lock.json ├── package.json ├── sidebars.ts ├── src │ ├── components │ │ └── HomepageFeatures │ │ │ ├── index.tsx │ │ │ └── styles.module.css │ ├── css │ │ └── custom.css │ └── pages │ │ ├── index.module.css │ │ ├── index.tsx │ │ └── markdown-page.md ├── static │ ├── .nojekyll │ └── img │ │ ├── docusaurus-social-card.jpg │ │ ├── docusaurus.png │ │ ├── favicon.ico │ │ ├── logo.svg │ │ ├── undraw_docusaurus_mountain.svg │ │ ├── undraw_docusaurus_react.svg │ │ └── undraw_docusaurus_tree.svg └── tsconfig.json ├── example.html ├── examples ├── drawings │ └── paper.html ├── dxf-export │ └── index.html ├── elements │ ├── baseDoor.html │ ├── baseWall.html │ ├── baseWindow.html │ ├── board.html │ ├── doubleWindow.html │ ├── spaceContainer.html │ └── window.html ├── export-ifc.html ├── glyph.html ├── initialState.og ├── jsonGeneratedPlans.html ├── object-Generated-Plans │ ├── alternate.json │ ├── graph.json │ ├── impleniaGraph.json │ ├── impleniaJsonToFloorplans.html │ └── jsonGeneratedPlans.html ├── shape-builder │ └── index.html └── wall.html ├── index.html ├── open-interface ├── index.ts └── oi-dropdown.ts ├── package-lock.json ├── package.json ├── public ├── door2050.json ├── tiles2050.json ├── typescript.svg ├── wallCrossTexture.jpg ├── wallDotTexture.jpg └── window2050.json ├── readme.md ├── release.md ├── rollup.config.js ├── src ├── .DS_Store ├── base-type.ts ├── constant.ts ├── custom-elements │ └── custom.md ├── drawing │ ├── description-block.ts │ ├── index.ts │ ├── logo-info-block.ts │ ├── paper-frame.ts │ ├── row-info-block.ts │ ├── side-profile.ts │ ├── toon-profile.ts │ └── top-profile.ts ├── elements │ ├── base-door.ts │ ├── base-element.ts │ ├── base-elements.md │ ├── base-spaces.ts │ ├── base-wall.ts │ ├── base-window.ts │ ├── board.ts │ ├── double-window.ts │ └── elements.md ├── helpers │ ├── OpenGridHelper.ts │ └── OpenOutliner.ts ├── index.ts ├── parser │ ├── IGraph.ts │ └── ImpleniaConverter.ts ├── service │ ├── plancamera.ts │ ├── plangrid.ts │ ├── selector.ts │ └── three.ts ├── shape-builder │ ├── circle-builder.ts │ ├── polygon-builder.ts │ ├── polyline-builder.ts │ └── shape-builder.md ├── shape │ ├── circle-shape.ts │ ├── polygon-shape.ts │ └── polyline-shape.ts └── utils │ ├── event.ts │ └── map-helper.ts ├── tsconfig.json └── vite.config.js /.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/.DS_Store -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | 2 | 3 | ### Description/Context/What Does Your PR Do 4 | 5 | 6 | 7 | ### Steps To Run/Review for Other Devs 8 | 9 | 10 | 11 | ### Production Log Message 12 | 13 | [ ] - 14 | 15 | -------------------------------------------------------------------------------- /.github/workflows/release_notes.yaml: -------------------------------------------------------------------------------- 1 | name: Release Notes 2 | 3 | on: 4 | pull_request: 5 | types: 6 | - closed 7 | 8 | jobs: 9 | release_notes: 10 | if: github.event.pull_request.merged == true && github.event.pull_request.base.ref == 'main' 11 | runs-on: ubuntu-latest 12 | 13 | steps: 14 | - name: Checkout PR Content 15 | uses: actions/checkout@v4 16 | 17 | - name: Capture PR Title, Date and User 18 | run: | 19 | PR_TITLE="${{ github.event.pull_request.title }}" 20 | PR_USER="${{ github.event.pull_request.user.login }}" 21 | PR_DATE=${{ github.event.pull_request.merged_at }} 22 | 23 | echo "pr_title=$PR_TITLE" >> $GITHUB_ENV 24 | echo "pr_user=$PR_USER" >> $GITHUB_ENV 25 | 26 | PR_DATE=$(date -d "$PR_DATE" '+%Y-%m-%d') 27 | echo "pr_date=$PR_DATE" >> $GITHUB_ENV 28 | 29 | # Check if PR Title is empty 30 | if [[ -z "$PR_TITLE" ]]; then 31 | echo "No PR title found." 32 | exit 0 33 | fi 34 | 35 | # Check if PR Title contains feat: and extract the message and trigger the release 36 | if [[ "$PR_TITLE" == *"feat:"* ]]; then 37 | TYPE="feature" 38 | MESSAGE=$(echo "$PR_TITLE" | grep -oP "(?<=feat: ).*") 39 | if [[ -n "$MESSAGE" ]]; then 40 | echo "message=$MESSAGE" >> $GITHUB_ENV 41 | echo "type=$TYPE" >> $GITHUB_ENV 42 | else 43 | echo "No valid feature message found after feat: " 44 | exit 0 45 | fi 46 | else 47 | echo "No feat: found in PR title." 48 | exit 0 49 | fi 50 | 51 | - name: Push To Notion 52 | env: 53 | NOTION_API_KEY: ${{ secrets.NOTION_API_KEY }} 54 | run: | 55 | echo "Message: ${{ env.message }}" 56 | 57 | curl -X POST https://api.notion.com/v1/pages \ 58 | -H 'Authorization: Bearer '"$NOTION_API_KEY"'' \ 59 | -H "Content-Type: application/json" \ 60 | -H "Notion-Version: 2022-06-28" \ 61 | --data '{ 62 | "parent": { "database_id": "15053776c9c18005be88d76140c9a7b5" }, 63 | "properties": { 64 | "Title": { 65 | "rich_text": [ 66 | { 67 | "text": { 68 | "content": "${{ env.message }}" 69 | } 70 | } 71 | ] 72 | }, 73 | "Release Date": { 74 | "date": { 75 | "start": "${{ env.pr_date }}" 76 | } 77 | }, 78 | "Type": { 79 | "rich_text": [ 80 | { 81 | "text": { 82 | "content": "${{ env.type }}" 83 | } 84 | } 85 | ] 86 | }, 87 | "Released By": { 88 | "rich_text": [ 89 | { 90 | "text": { 91 | "content": "${{ env.pr_user }}" 92 | } 93 | } 94 | ] 95 | }, 96 | "Name": { 97 | "title": [ 98 | { 99 | "text": { 100 | "content": "${{ env.message }}" 101 | } 102 | } 103 | ] 104 | } 105 | } 106 | }' 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | kernel/ 3 | .vscode/ 4 | dist/ 5 | examples-dist/ 6 | .DS_Store -------------------------------------------------------------------------------- /LICENSE.md: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2025 OpenPlans 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining 6 | a copy of this software and associated documentation files (the 7 | "Software"), to deal in the Software without restriction, including 8 | without limitation the rights to use, copy, modify, merge, publish, 9 | distribute, sublicense, and/or sell copies of the Software, and to 10 | permit persons to whom the Software is furnished to do so, subject to 11 | the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be 14 | included in all copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, 17 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF 18 | MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND 19 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE 20 | LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION 21 | OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION 22 | WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE. -------------------------------------------------------------------------------- /developer.md: -------------------------------------------------------------------------------- 1 | Openplans uses OpenGeometry Kernel for creating and managing geometry data. 2 | More information about OpenGeometry Kernel can be found [here](https://docs.opengeometry.io/opengeometry/intro) 3 | 4 | ## Developer Guide 5 | 6 | Clone the Openplans repository 7 | ```bash 8 | git clone https://github.com/OpenGeometry-io/OpenPlans 9 | ``` 10 | 11 | ### Setting up the development environment 12 | 13 | OpenGeometry Kernel is needed to run OpenPlans locally as it provides the Kernel Code, we are planning to use the OpenGeometry Kernel as a dependancy. In the meantime, you can set up the OpenGeometry Kernel locally by following these steps. 14 | 15 | ### Prerequisites 16 | 1. Clone the OpenGeometry repository 17 | ```bash 18 | git clone https://github.com/OpenGeometry-io/OpenGeometry 19 | ``` 20 | 21 | 2. Install the required dependencies 22 | ```bash 23 | cd OpenGeometry 24 | npm install 25 | ``` 26 | 27 | 3. Build the OpenGeometry Kernel locally and link it to the Openplans project 28 | ```bash 29 | npm run build-local 30 | ``` 31 | This command will copy the build files inside the `openplans/kernel` directory. 32 | You should now have the OpenGeometry Kernel set up locally, you can verify this by checking the `openplans/kernel` directory for the build files. 33 | 34 | ### Running OpenPlans locally 35 | 1. Navigate to the OpenPlans directory 36 | 37 | 2. Install the required dependencies 38 | ```bash 39 | npm install 40 | ``` 41 | 42 | 3. Start the development server 43 | ```bash 44 | npm run dev 45 | ``` 46 | 47 | This will start the development server and you should be able to access OpenPlans at `http://localhost:5555`. 48 | 49 | - Note: If you make changes to the OpenGeometry Kernel, you will need to rebuild it. 50 | - We use vite as the build tool, so you can use the vite commands to build and run the project. 51 | - Server port can be changed by modifying the `vite.config.js` file in the root directory of the OpenPlans project. -------------------------------------------------------------------------------- /docs/.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | /node_modules 3 | 4 | # Production 5 | /build 6 | 7 | # Generated files 8 | .docusaurus 9 | .cache-loader 10 | 11 | # Misc 12 | .DS_Store 13 | .env.local 14 | .env.development.local 15 | .env.test.local 16 | .env.production.local 17 | 18 | npm-debug.log* 19 | yarn-debug.log* 20 | yarn-error.log* 21 | 22 | 23 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /docs/README.md: -------------------------------------------------------------------------------- 1 | # Website 2 | 3 | This website is built using [Docusaurus](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 | Using SSH: 30 | 31 | ``` 32 | $ USE_SSH=true yarn deploy 33 | ``` 34 | 35 | Not using SSH: 36 | 37 | ``` 38 | $ GIT_USER= yarn deploy 39 | ``` 40 | 41 | 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. 42 | -------------------------------------------------------------------------------- /docs/docs/intro.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | --- 4 | 5 | # OpenPlans 6 | 7 | A robust floorplanning sdk being developed using **[OpenGeometry Kernel](https://github.com/OpenGeometry-io/OpenGeometry)**. You can use OpenPlans as a library and import it into your web project. OpenPlans uses Three.js for rendering on web. 8 | 9 | More information on **[Three.js can be found here](https://threejs.org)**. 10 | 11 | ## Introduction 12 | 13 | OpenPlans currently comes as a npm package, we have bundled all the basic elemenets and are working on more and more elements which can be directly imported into your project and rendered as geometries. 14 | 15 | OpenPlans generates the needful Buffer Geometry for Three.js engine, you can read more about **[BufferGeometry here](https://threejs.org/docs/?q=buffer#api/en/core/BufferGeometry)**. 16 | We are working on devising a way so that you can extend these elements to create your own. 17 | 18 | ### Purpose 19 | 20 | We feel the current tools lacks creative freedom an Architect has, we want to shift the power into the user hands. We want to help Architects create beautiful and elegant buildings without worrying about the software limitations. 21 | 22 | We are building Open Source alternatives to tools that an Building Designer uses day in day out. 23 | 24 | ### What you'll need 25 | 26 | - Basic understanding about JS/TS 27 | - Ability to Think Creatively 28 | - Willigness to Embrace Change 29 | 30 | ## Create Simple Wall 31 | 32 | We will start with the most basic steps and quickly go through the process of how to add a Wall and edit it. 33 | Before going forward, make sure you have node installed, if you don't have it, you can find the setup instructions [here](https://nodejs.org/en/download) 34 | 35 | ### Create a new Directory 36 | ```bash 37 | mkdir OpenPlans-Wall 38 | cd OpenPlans-Wall 39 | ``` 40 | 41 | ### Setup a npm project 42 | ```bash 43 | npm init -y 44 | ``` 45 | Accept the default conditions 46 | 47 | ### Create Project Files 48 | - Create `index.html` - This will contain skeleton for your app 49 | - Create `style.css` - This will contain various styles 50 | 51 | Your directory would like this after you have the files added 52 | ```bash 53 | |-index.html 54 | |-node_modules/ 55 | |-package.json 56 | |-styles.css 57 | ``` 58 | 59 | ### Creating HTML Skeleton 60 | Add the below code into your `index.html` file 61 | ```html 62 | 63 | 64 | 65 | OpenPlans 66 | 67 | 68 | 69 | 70 | 71 |
72 | 73 | 74 | 75 | ``` 76 | 77 | ### Adding Basic Styling 78 | Add the below code to your `style.css` file 79 | ```css 80 | body { 81 | margin: 0; 82 | overflow: hidden; 83 | } 84 | 85 | #app { 86 | width: 100%; 87 | height: 100vh; 88 | } 89 | ``` 90 | 91 | ### Setup OpenPlans 92 | ```js 93 | async function init() { 94 | 95 | const container = document.getElementById('app'); 96 | const openPlans = new OpenPlans(container); 97 | 98 | } 99 | ``` 100 | 101 | ### Adding OpenGeometry 102 | 103 | OpenGeometry is a Kernel which makes it easier to perform graphical operations, so we need to setup the engine before we can start with OpenPlans. 104 | It's a single API call. 105 | ```js 106 | async function init() { 107 | // .. 108 | // Rest of the code 109 | 110 | await openPlans.setupOpenGeometry(); 111 | } 112 | ``` 113 | 114 | ### Add Simple Wall 115 | ```js 116 | async function init() { 117 | // .. 118 | // Rest of the code 119 | 120 | const basicWall = openPlans.wall(); 121 | } 122 | ``` 123 | 124 | ### Edit Wall Position 125 | ```js 126 | async function init() { 127 | // .. 128 | // Rest of the code 129 | 130 | basicWall.position.set(2, 0, 0); 131 | } 132 | ``` 133 | 134 | Once you add all the code, your script tag would like this 135 | ```js 136 | 153 | ``` 154 | 155 | ### Live Action 156 | Let's see the code in action! 157 | 158 | ```html live 159 | function Wall (props) { 160 | const [date, setDate] = useState(new Date()); 161 | useEffect(() => { 162 | const timerID = setInterval(() => tick(), 1000); 163 | 164 | return function cleanup() { 165 | clearInterval(timerID); 166 | }; 167 | }); 168 | 169 | function tick() { 170 | setDate(new Date()); 171 | } 172 | 173 | return ( 174 |
175 |

It is {date.toLocaleTimeString()}.

176 |
177 | ); 178 | } 179 | ``` -------------------------------------------------------------------------------- /docs/docs/tutorial-create-elements/_category_.json: -------------------------------------------------------------------------------- 1 | { 2 | "label": "Create Elements", 3 | "position": 2, 4 | "collapsed": false, 5 | "collapsible": false, 6 | "link": { 7 | "type": "generated-index", 8 | "description": "Tutorials for creating basic 2D Elements using the OpenPlans Engine" 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /docs/docs/tutorial-create-elements/create-custom-elements.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 5 3 | --- 4 | 5 | # Create Custom Elements 6 | 7 | OpenPlans wants to push the boundaries, the Elements you see can be extended and customised the way you want them without much learning curve. 8 | 9 | More information coming soon! 10 | -------------------------------------------------------------------------------- /docs/docs/tutorial-create-elements/create-door.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 2 3 | --- 4 | 5 | # Create Base Door -------------------------------------------------------------------------------- /docs/docs/tutorial-create-elements/create-double-window.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 4 3 | --- 4 | 5 | # Create Base Double Window -------------------------------------------------------------------------------- /docs/docs/tutorial-create-elements/create-wall.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 1 3 | sidebar_label: 🧱 Create Wall 4 | --- 5 | 6 | # 🧱 Create Base Wall 7 | 8 | ```jsx title="src/pages/my-react-page.js" 9 | import React from 'react'; 10 | import Layout from '@theme/Layout'; 11 | 12 | export default function MyReactPage() { 13 | return ( 14 | 15 |

My React page

16 |

This is a React page

17 |
18 | ); 19 | } 20 | ``` 21 | 22 | A new page is now available at [http://localhost:3000/my-react-page](http://localhost:3000/my-react-page). 23 | 24 | ## Create your first Markdown Page 25 | 26 | Create a file at `src/pages/my-markdown-page.md`: 27 | 28 | ```mdx title="src/pages/my-markdown-page.md" 29 | # My Markdown page 30 | 31 | This is a Markdown page 32 | ``` 33 | 34 | A new page is now available at [http://localhost:3000/my-markdown-page](http://localhost:3000/my-markdown-page). 35 | -------------------------------------------------------------------------------- /docs/docs/tutorial-create-elements/create-window.md: -------------------------------------------------------------------------------- 1 | --- 2 | sidebar_position: 3 3 | --- 4 | 5 | # Create Base Window -------------------------------------------------------------------------------- /docs/docusaurus.config.ts: -------------------------------------------------------------------------------- 1 | import {themes as prismThemes} from 'prism-react-renderer'; 2 | import type {Config} from '@docusaurus/types'; 3 | import type * as Preset from '@docusaurus/preset-classic'; 4 | 5 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 6 | 7 | const config: Config = { 8 | title: 'OpenPlans', 9 | tagline: '2D CAD on the web', 10 | favicon: 'img/favicon.ico', 11 | 12 | // Set the production url of your site here 13 | url: 'https://your-docusaurus-site.example.com', 14 | // Set the // pathname under which your site is served 15 | // For GitHub pages deployment, it is often '//' 16 | baseUrl: '/', 17 | 18 | // GitHub pages deployment config. 19 | // If you aren't using GitHub pages, you don't need these. 20 | organizationName: 'opengeometry', // Usually your GitHub org/user name. 21 | projectName: 'openplans', // Usually your repo name. 22 | 23 | onBrokenLinks: 'throw', 24 | onBrokenMarkdownLinks: 'warn', 25 | 26 | // Even if you don't use internationalization, you can use this field to set 27 | // useful metadata like html lang. For example, if your site is Chinese, you 28 | // may want to replace "en" with "zh-Hans". 29 | i18n: { 30 | defaultLocale: 'en', 31 | locales: ['en'], 32 | }, 33 | 34 | themes: ['@docusaurus/theme-live-codeblock'], 35 | presets: [ 36 | [ 37 | 'classic', 38 | { 39 | docs: { 40 | sidebarPath: './sidebars.ts', 41 | // Please change this to your repo. 42 | // Remove this to remove the "edit this page" links. 43 | editUrl: 44 | 'https://github.com/facebook/docusaurus/tree/main/packages/create-docusaurus/templates/shared/', 45 | }, 46 | theme: { 47 | customCss: './src/css/custom.css', 48 | }, 49 | } satisfies Preset.Options, 50 | ], 51 | ], 52 | 53 | themeConfig: { 54 | // Replace with your project's social card 55 | image: 'img/docusaurus-social-card.jpg', 56 | navbar: { 57 | title: 'OpenPlans', 58 | logo: { 59 | alt: 'OpenPlans Logo', 60 | src: 'img/logo.svg', 61 | }, 62 | items: [ 63 | { 64 | type: 'docSidebar', 65 | sidebarId: 'tutorialSidebar', 66 | position: 'left', 67 | label: 'Documentation', 68 | }, 69 | { 70 | href: 'https://github.com/opengeometry-io/openplans', 71 | label: 'GitHub', 72 | position: 'right', 73 | }, 74 | ], 75 | }, 76 | footer: { 77 | style: 'dark', 78 | links: [ 79 | { 80 | title: 'Docs', 81 | items: [ 82 | { 83 | label: 'Example', 84 | to: '/docs/example', 85 | }, 86 | ], 87 | }, 88 | { 89 | title: 'Community', 90 | items: [ 91 | { 92 | label: 'Discord', 93 | href: 'https://discordapp.com/invite/opengeometry', 94 | }, 95 | { 96 | label: 'X', 97 | href: 'https://x.com/opengeometry', 98 | }, 99 | ], 100 | }, 101 | { 102 | title: 'More', 103 | items: [ 104 | { 105 | label: 'GitHub', 106 | href: 'https://github.com/opengeometry-io/openplans', 107 | }, 108 | ], 109 | }, 110 | ], 111 | copyright: `Copyright © ${new Date().getFullYear()} OpenPlans, OpenGeometry.`, 112 | }, 113 | prism: { 114 | theme: prismThemes.github, 115 | darkTheme: prismThemes.dracula, 116 | }, 117 | } satisfies Preset.ThemeConfig, 118 | }; 119 | 120 | export default config; 121 | -------------------------------------------------------------------------------- /docs/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "docs", 3 | "version": "0.0.0", 4 | "private": true, 5 | "scripts": { 6 | "docusaurus": "docusaurus", 7 | "start": "docusaurus start", 8 | "build": "docusaurus build", 9 | "swizzle": "docusaurus swizzle", 10 | "deploy": "docusaurus deploy", 11 | "clear": "docusaurus clear", 12 | "serve": "docusaurus serve", 13 | "write-translations": "docusaurus write-translations", 14 | "write-heading-ids": "docusaurus write-heading-ids", 15 | "typecheck": "tsc" 16 | }, 17 | "dependencies": { 18 | "@docusaurus/core": "3.7.0", 19 | "@docusaurus/preset-classic": "3.7.0", 20 | "@docusaurus/theme-live-codeblock": "^3.7.0", 21 | "@mdx-js/react": "^3.0.0", 22 | "clsx": "^2.0.0", 23 | "prism-react-renderer": "^2.3.0", 24 | "react": "^19.0.0", 25 | "react-dom": "^19.0.0" 26 | }, 27 | "devDependencies": { 28 | "@docusaurus/module-type-aliases": "3.7.0", 29 | "@docusaurus/tsconfig": "3.7.0", 30 | "@docusaurus/types": "3.7.0", 31 | "typescript": "~5.6.2" 32 | }, 33 | "browserslist": { 34 | "production": [ 35 | ">0.5%", 36 | "not dead", 37 | "not op_mini all" 38 | ], 39 | "development": [ 40 | "last 3 chrome version", 41 | "last 3 firefox version", 42 | "last 5 safari version" 43 | ] 44 | }, 45 | "engines": { 46 | "node": ">=18.0" 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /docs/sidebars.ts: -------------------------------------------------------------------------------- 1 | import type {SidebarsConfig} from '@docusaurus/plugin-content-docs'; 2 | 3 | // This runs in Node.js - Don't use client-side code here (browser APIs, JSX...) 4 | 5 | /** 6 | * Creating a sidebar enables you to: 7 | - create an ordered group of docs 8 | - render a sidebar for each doc of that group 9 | - provide next/previous navigation 10 | 11 | The sidebars can be generated from the filesystem, or explicitly defined here. 12 | 13 | Create as many sidebars as you want. 14 | */ 15 | const sidebars: SidebarsConfig = { 16 | // By default, Docusaurus generates a sidebar from the docs folder structure 17 | tutorialSidebar: [{type: 'autogenerated', dirName: '.'}], 18 | 19 | // But you can create a sidebar manually 20 | /* 21 | tutorialSidebar: [ 22 | 'intro', 23 | 'hello', 24 | { 25 | type: 'category', 26 | label: 'Tutorial', 27 | items: ['tutorial-basics/create-a-document'], 28 | }, 29 | ], 30 | */ 31 | }; 32 | 33 | export default sidebars; 34 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/index.tsx: -------------------------------------------------------------------------------- 1 | import type {ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Heading from '@theme/Heading'; 4 | import styles from './styles.module.css'; 5 | 6 | type FeatureItem = { 7 | title: string; 8 | Svg: React.ComponentType>; 9 | description: ReactNode; 10 | }; 11 | 12 | const FeatureList: FeatureItem[] = [ 13 | { 14 | title: 'Easy to Use', 15 | Svg: require('@site/static/img/undraw_docusaurus_mountain.svg').default, 16 | description: ( 17 | <> 18 | Docusaurus was designed from the ground up to be easily installed and 19 | used to get your website up and running quickly. 20 | 21 | ), 22 | }, 23 | { 24 | title: 'Focus on What Matters', 25 | Svg: require('@site/static/img/undraw_docusaurus_tree.svg').default, 26 | description: ( 27 | <> 28 | Docusaurus lets you focus on your docs, and we'll do the chores. Go 29 | ahead and move your docs into the docs directory. 30 | 31 | ), 32 | }, 33 | { 34 | title: 'Powered by React', 35 | Svg: require('@site/static/img/undraw_docusaurus_react.svg').default, 36 | description: ( 37 | <> 38 | Extend or customize your website layout by reusing React. Docusaurus can 39 | be extended while reusing the same header and footer. 40 | 41 | ), 42 | }, 43 | ]; 44 | 45 | function Feature({title, Svg, description}: FeatureItem) { 46 | return ( 47 |
48 |
49 | 50 |
51 |
52 | {title} 53 |

{description}

54 |
55 |
56 | ); 57 | } 58 | 59 | export default function HomepageFeatures(): ReactNode { 60 | return ( 61 |
62 |
63 |
64 | {FeatureList.map((props, idx) => ( 65 | 66 | ))} 67 |
68 |
69 |
70 | ); 71 | } 72 | -------------------------------------------------------------------------------- /docs/src/components/HomepageFeatures/styles.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 | -------------------------------------------------------------------------------- /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 | :root { 9 | --ifm-color-primary: #3d3d3d; 10 | --ifm-color-primary-dark: #373737; 11 | --ifm-color-primary-darker: #343434; 12 | --ifm-color-primary-darkest: #2b2b2b; 13 | --ifm-color-primary-light: #434343; 14 | --ifm-color-primary-lighter: #464646; 15 | --ifm-color-primary-lightest: #4f4f4f; 16 | --ifm-code-font-size: 95%; 17 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.1); 18 | } 19 | 20 | /* For readability concerns, you should choose a lighter palette in dark mode. */ 21 | [data-theme='dark'] { 22 | --ifm-color-primary: #fff7e5; 23 | --ifm-color-primary-dark: #fff7e5; 24 | --ifm-color-primary-darker: #fff7e5; 25 | --ifm-color-primary-darkest: #fff7e5; 26 | --ifm-color-primary-light: #fff7e5; 27 | --ifm-color-primary-lighter: #fff7e5; 28 | --ifm-color-primary-lightest: #fff7e5; 29 | --docusaurus-highlighted-code-line-bg: rgba(0, 0, 0, 0.3); 30 | } 31 | -------------------------------------------------------------------------------- /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: 996px) { 14 | .heroBanner { 15 | padding: 2rem; 16 | } 17 | } 18 | 19 | .buttons { 20 | display: flex; 21 | align-items: center; 22 | justify-content: center; 23 | } 24 | -------------------------------------------------------------------------------- /docs/src/pages/index.tsx: -------------------------------------------------------------------------------- 1 | import type {ReactNode} from 'react'; 2 | import clsx from 'clsx'; 3 | import Link from '@docusaurus/Link'; 4 | import useDocusaurusContext from '@docusaurus/useDocusaurusContext'; 5 | import Layout from '@theme/Layout'; 6 | import HomepageFeatures from '@site/src/components/HomepageFeatures'; 7 | import Heading from '@theme/Heading'; 8 | 9 | import styles from './index.module.css'; 10 | 11 | function HomepageHeader() { 12 | const {siteConfig} = useDocusaurusContext(); 13 | return ( 14 |
15 |
16 | 17 | {siteConfig.title} 18 | 19 |

{siteConfig.tagline}

20 |
21 | 24 | Take Me To Tutorials 25 | 26 |
27 |
28 |
29 | ); 30 | } 31 | 32 | export default function Home(): ReactNode { 33 | const {siteConfig} = useDocusaurusContext(); 34 | return ( 35 | 38 | 39 |
40 | 41 |
42 |
43 | ); 44 | } 45 | -------------------------------------------------------------------------------- /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 | -------------------------------------------------------------------------------- /docs/static/.nojekyll: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/docs/static/.nojekyll -------------------------------------------------------------------------------- /docs/static/img/docusaurus-social-card.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/docs/static/img/docusaurus-social-card.jpg -------------------------------------------------------------------------------- /docs/static/img/docusaurus.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/docs/static/img/docusaurus.png -------------------------------------------------------------------------------- /docs/static/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/docs/static/img/favicon.ico -------------------------------------------------------------------------------- /docs/static/img/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /docs/static/img/undraw_docusaurus_tree.svg: -------------------------------------------------------------------------------- 1 | 2 | Focus on What Matters 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 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 | -------------------------------------------------------------------------------- /docs/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | // This file is not used in compilation. It is here just for a nice editor experience. 3 | "extends": "@docusaurus/tsconfig", 4 | "compilerOptions": { 5 | "baseUrl": "." 6 | }, 7 | "exclude": [".docusaurus", "build"] 8 | } 9 | -------------------------------------------------------------------------------- /example.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Hello AEC 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 | 92 | 93 | -------------------------------------------------------------------------------- /examples/drawings/paper.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenPlan Paper Frame 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 | 155 | 156 | -------------------------------------------------------------------------------- /examples/dxf-export/index.html: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /examples/elements/baseDoor.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenPlan Door 6 | 7 | 8 | 9 | 10 | 21 | 22 | 23 | 24 |
25 |
26 | 155 | 156 | 157 | -------------------------------------------------------------------------------- /examples/elements/baseWall.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | OpenPlans Simple Wall 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 30 | 31 | 32 | 33 |
34 |
35 | 164 | 165 | 166 | -------------------------------------------------------------------------------- /examples/elements/baseWindow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | OpenPlan | Base Single Window 6 | 7 | 8 | 9 | 10 | 21 | 22 | 23 | 24 |
25 |
26 | 147 | 148 | 149 | -------------------------------------------------------------------------------- /examples/elements/board.html: -------------------------------------------------------------------------------- 1 | 9 | 10 | 11 | 12 | 13 | OpenPlan Board 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 33 | 34 | 35 | 36 |
37 |
38 | 176 | 177 | 178 | -------------------------------------------------------------------------------- /examples/elements/doubleWindow.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenPlan Double Window 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 | 72 | 73 | -------------------------------------------------------------------------------- /examples/elements/spaceContainer.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenPlan Spaces 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 | 72 | 73 | -------------------------------------------------------------------------------- /examples/elements/window.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenPlan Window 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 | 72 | 73 | -------------------------------------------------------------------------------- /examples/export-ifc.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenPlans Wall 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 | 109 | 110 | -------------------------------------------------------------------------------- /examples/glyph.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenPlan Glyph 5 | 6 | 7 | 8 | 9 | 20 | 21 | 22 |
23 |
24 | 135 | 136 | -------------------------------------------------------------------------------- /examples/initialState.og: -------------------------------------------------------------------------------- 1 | { 2 | { 3 | "id": "123", 4 | "type": "camera", 5 | "position": { 6 | "x": 3, 7 | "y": 6, 8 | "z": 2 9 | }, 10 | "target": { 11 | "x": 0, 12 | "y": 0, 13 | "z": 0 14 | }, 15 | "createdBy": "tejas" 16 | }, 17 | { 18 | "id: "43432", 19 | "type: "chair", 20 | "group": "kitchen", 21 | "groupId": "34234234", 22 | "mesh": "https://elements.openplans.io/userId/meshId", 23 | "position": { 24 | "x": 3, 25 | "y": 6, 26 | "z": 2 27 | }, 28 | "floorName: "Third Floor" 29 | "floorId": "56786534" 30 | } 31 | } -------------------------------------------------------------------------------- /examples/jsonGeneratedPlans.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenPlan JSON 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 | 51 | 52 | -------------------------------------------------------------------------------- /examples/object-Generated-Plans/alternate.json: -------------------------------------------------------------------------------- 1 | { 2 | "building_id": "BLDG_001", 3 | "building_name": "Apartment Building 1", 4 | "floors": [ 5 | { 6 | "OG_ID": "FLOOR_001", 7 | "OG_TYPE": "OG_FLOOR", 8 | "OG_DATA": ["ROOM_001", "ROOM_002", "ROOM_003", "ROOM_004", "ROOM_005", "ROOM_006", "ROOM_007", "ROOM_008"] 9 | } 10 | ], 11 | "rooms": [ 12 | { 13 | "OG_ID": "ROOM_001", 14 | "OG_TYPE": "OG_ROOM", 15 | "type": "Living Room", 16 | "OG_DATA": [ 17 | { 18 | "OG_ID": "WALL_004", 19 | "OG_TYPE": "OG_WALL" 20 | }, 21 | { 22 | "OG_ID": "WALL_005", 23 | "OG_TYPE": "OG_WALL" 24 | }, 25 | { 26 | "OG_ID": "WALL_006", 27 | "OG_TYPE": "OG_WALL" 28 | }, 29 | { 30 | "OG_ID": "WALL_007", 31 | "OG_TYPE": "OG_WALL" 32 | }, 33 | { 34 | "OG_ID": "WALL_008", 35 | "OG_TYPE": "OG_WALL" 36 | }, 37 | { 38 | "OG_ID": "WALL_009", 39 | "OG_TYPE": "OG_WALL" 40 | } 41 | ], 42 | "coordinates": [ 43 | [18.9, 0, 7.96], 44 | [18.9, 0, 3.45], 45 | [28.18, 0, 3.45], 46 | [28.18, 0, 7.88], 47 | [23.54, 0, 7.88], 48 | [21.84, 0, 7.96] 49 | ], 50 | "connections": ["ROOM_002", "ROOM_006"] 51 | }, 52 | { 53 | "OG_ID": "ROOM_002", 54 | "OG_TYPE": "OG_ROOM", 55 | "type": "Bedroom", 56 | "OG_DATA": [ 57 | { 58 | "OG_ID": "WALL_010", 59 | "OG_TYPE": "OG_WALL" 60 | }, 61 | { 62 | "OG_ID": "WALL_011", 63 | "OG_TYPE": "OG_WALL" 64 | }, 65 | { 66 | "OG_ID": "WALL_012", 67 | "OG_TYPE": "OG_WALL" 68 | }, 69 | { 70 | "OG_ID": "WALL_013", 71 | "OG_TYPE": "OG_WALL" 72 | } 73 | ], 74 | "coordinates": [ 75 | [23.54, 0, 7.88], 76 | [23.54, 0, 10.77], 77 | [28.27, 0, 10.77], 78 | [28.18, 0, 7.88] 79 | ], 80 | "connections": ["ROOM_003", "ROOM_006"] 81 | }, 82 | { 83 | "OG_ID": "ROOM_003", 84 | "OG_TYPE": "OG_ROOM", 85 | "type": "Bedroom", 86 | "OG_DATA": [ 87 | { 88 | "OG_ID": "WALL_014", 89 | "OG_TYPE": "OG_WALL" 90 | }, 91 | { 92 | "OG_ID": "WALL_015", 93 | "OG_TYPE": "OG_WALL" 94 | }, 95 | { 96 | "OG_ID": "WALL_016", 97 | "OG_TYPE": "OG_WALL" 98 | }, 99 | { 100 | "OG_ID": "WALL_017", 101 | "OG_TYPE": "OG_WALL" 102 | } 103 | ], 104 | "coordinates": [ 105 | [23.54, 0, 10.77], 106 | [23.54, 0, 13.58], 107 | [28.27, 0, 13.58], 108 | [28.27, 0, 10.77] 109 | ], 110 | "connections": ["ROOM_002", "ROOM_004", "ROOM_006"] 111 | }, 112 | { 113 | "OG_ID": "ROOM_004", 114 | "OG_TYPE": "OG_ROOM", 115 | "type": "Bedroom", 116 | "OG_DATA": [ 117 | { 118 | "OG_ID": "WALL_018", 119 | "OG_TYPE": "OG_WALL" 120 | }, 121 | { 122 | "OG_ID": "WALL_019", 123 | "OG_TYPE": "OG_WALL" 124 | }, 125 | { 126 | "OG_ID": "WALL_020", 127 | "OG_TYPE": "OG_WALL" 128 | }, 129 | { 130 | "OG_ID": "WALL_021", 131 | "OG_TYPE": "OG_WALL" 132 | } 133 | ], 134 | "coordinates": [ 135 | [23.54, 0, 13.58], 136 | [23.54, 0, 16.56], 137 | [28.27, 0, 16.56], 138 | [28.27, 0, 13.58] 139 | ], 140 | "connections": ["ROOM_003", "ROOM_005", "ROOM_006"] 141 | }, 142 | { 143 | "OG_ID": "ROOM_005", 144 | "OG_TYPE": "OG_ROOM", 145 | "type": "Bathroom", 146 | "OG_DATA": [ 147 | { 148 | "OG_ID": "WALL_023", 149 | "OG_TYPE": "OG_WALL" 150 | }, 151 | { 152 | "OG_ID": "WALL_024", 153 | "OG_TYPE": "OG_WALL" 154 | }, 155 | { 156 | "OG_ID": "WALL_025", 157 | "OG_TYPE": "OG_WALL" 158 | }, 159 | { 160 | "OG_ID": "WALL_026", 161 | "OG_TYPE": "OG_WALL" 162 | } 163 | ], 164 | "coordinates": [ 165 | [23.54, 0, 14.56], 166 | [21.84, 0, 14.56], 167 | [21.84, 0, 16.47], 168 | [23.54, 0, 16.56] 169 | ], 170 | "connections": ["ROOM_004", "ROOM_006"] 171 | }, 172 | { 173 | "OG_ID": "ROOM_006", 174 | "OG_TYPE": "OG_ROOM", 175 | "type": "Corridor", 176 | "OG_DATA": [ 177 | { 178 | "OG_ID": "WALL_027", 179 | "OG_TYPE": "OG_WALL" 180 | }, 181 | { 182 | "OG_ID": "WALL_028", 183 | "OG_TYPE": "OG_WALL" 184 | }, 185 | { 186 | "OG_ID": "WALL_029", 187 | "OG_TYPE": "OG_WALL" 188 | }, 189 | { 190 | "OG_ID": "WALL_030", 191 | "OG_TYPE": "OG_WALL" 192 | }, 193 | { 194 | "OG_ID": "WALL_031", 195 | "OG_TYPE": "OG_WALL" 196 | }, 197 | { 198 | "OG_ID": "WALL_032", 199 | "OG_TYPE": "OG_WALL" 200 | }, 201 | { 202 | "OG_ID": "WALL_033", 203 | "OG_TYPE": "OG_WALL" 204 | } 205 | ], 206 | "coordinates": [ 207 | [21.84, 0, 10.0], 208 | [21.84, 0, 14.56], 209 | [23.54, 0, 14.56], 210 | [23.54, 0, 13.58], 211 | [23.54, 0, 10.77], 212 | [23.54, 0, 7.88], 213 | [21.84, 0, 7.96] 214 | ], 215 | "connections": ["ROOM_001", "ROOM_002", "ROOM_003", "ROOM_004", "ROOM_005", "ROOM_007"] 216 | }, 217 | { 218 | "OG_ID": "ROOM_007", 219 | "OG_TYPE": "OG_ROOM", 220 | "type": "Bathroom", 221 | "OG_DATA": [ 222 | { 223 | "OG_ID": "WALL_001", 224 | "OG_TYPE": "OG_WALL" 225 | }, 226 | { 227 | "OG_ID": "WALL_002", 228 | "OG_TYPE": "OG_WALL" 229 | }, 230 | { 231 | "OG_ID": "WALL_003", 232 | "OG_TYPE": "OG_WALL" 233 | } 234 | ], 235 | "coordinates": [ 236 | [18.9, 0, 9.96], 237 | [18.9, 0, 7.96], 238 | [21.84, 0, 7.96], 239 | [21.84, 0, 10.0] 240 | ], 241 | "connections": ["ROOM_006"] 242 | }, 243 | { 244 | "OG_ID": "ROOM_008", 245 | "OG_TYPE": "OG_ROOM", 246 | "type": "Core", 247 | "OG_DATA": [ 248 | { 249 | "OG_ID": "WALL_034", 250 | "OG_TYPE": "OG_WALL" 251 | }, 252 | { 253 | "OG_ID": "WALL_035", 254 | "OG_TYPE": "OG_WALL" 255 | }, 256 | { 257 | "OG_ID": "WALL_036", 258 | "OG_TYPE": "OG_WALL" 259 | }, 260 | { 261 | "OG_ID": "WALL_037", 262 | "OG_TYPE": "OG_WALL" 263 | }, 264 | { 265 | "OG_ID": "WALL_038", 266 | "OG_TYPE": "OG_WALL" 267 | }, 268 | { 269 | "OG_ID": "WALL_039", 270 | "OG_TYPE": "OG_WALL" 271 | } 272 | ], 273 | "coordinates": [ 274 | [18.9, 0, 9.96], 275 | [18.9, 0, 18.98], 276 | [21.88, 0, 18.98], 277 | [21.84, 0, 16.47], 278 | [21.84, 0, 14.56], 279 | [21.84, 0, 10.0] 280 | ], 281 | "connections": [] 282 | } 283 | ], 284 | "walls": [ 285 | { 286 | "OG_ID": "WALL_001", 287 | "OG_TYPE": "OG_WALL", 288 | "type": "internal", 289 | "thickness": 0.2, 290 | "start": [18.9, 0, 9.96], 291 | "end": [18.9, 0, 7.96] 292 | }, 293 | { 294 | "OG_ID": "WALL_002", 295 | "OG_TYPE": "OG_WALL", 296 | "type": "internal", 297 | "thickness": 0.2, 298 | "start": [18.9, 0, 7.96], 299 | "end": [21.84, 0, 7.96] 300 | }, 301 | { 302 | "OG_ID": "WALL_003", 303 | "OG_TYPE": "OG_WALL", 304 | "type": "internal", 305 | "thickness": 0.2, 306 | "start": [21.84, 0, 7.96], 307 | "end": [21.84, 0, 10.0] 308 | }, 309 | { 310 | "OG_ID": "WALL_004", 311 | "OG_TYPE": "OG_WALL", 312 | "type": "internal", 313 | "thickness": 0.2, 314 | "start": [18.9, 0, 7.96], 315 | "end": [18.9, 0, 3.45] 316 | }, 317 | { 318 | "OG_ID": "WALL_005", 319 | "OG_TYPE": "OG_WALL", 320 | "type": "internal", 321 | "thickness": 0.2, 322 | "start": [18.9, 0, 3.45], 323 | "end": [28.18, 0, 3.45] 324 | }, 325 | { 326 | "OG_ID": "WALL_006", 327 | "OG_TYPE": "OG_WALL", 328 | "type": "internal", 329 | "thickness": 0.2, 330 | "start": [28.18, 0, 3.45], 331 | "end": [28.18, 0, 7.88] 332 | } 333 | ], 334 | "windows": [ 335 | { 336 | "OG_ID": "WINDOW_001", 337 | "OG_TYPE": "OG_WINDOW", 338 | "type": "internal", 339 | "thickness": 0.2, 340 | "start": [28.18, 0, 5.45], 341 | "end": [28.18, 0, 6.45] 342 | } 343 | ], 344 | "doors": [ 345 | { 346 | "OG_ID": "DOOR_001", 347 | "OG_TYPE": "OG_DOOR", 348 | "type": "internal", 349 | "thickness": 0.2, 350 | "hingeThickness": 0.1, 351 | "start": [21.84, 0, 7.96], 352 | "end": [22.84, 0, 7.96] 353 | } 354 | ] 355 | } -------------------------------------------------------------------------------- /examples/object-Generated-Plans/impleniaJsonToFloorplans.html: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/examples/object-Generated-Plans/impleniaJsonToFloorplans.html -------------------------------------------------------------------------------- /examples/object-Generated-Plans/jsonGeneratedPlans.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenPlan JSON 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 | 60 | 61 | -------------------------------------------------------------------------------- /examples/shape-builder/index.html: -------------------------------------------------------------------------------- 1 | 5 | 6 | 7 | 8 | 9 | OpenPlans Shapes 10 | 11 | 12 | 13 | 14 | 15 | 16 | 18 | 30 | 31 | 32 | 33 |
34 |
35 | 181 | 182 | 183 | -------------------------------------------------------------------------------- /examples/wall.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | OpenPlans Wall 5 | 6 | 7 | 8 | 9 | 19 | 20 | 21 |
22 |
23 | 133 | 134 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | OpenPlans Examples 7 | 8 | 9 | 11 | 57 | 58 | 59 |

OpenPlans Examples

60 | 61 |
62 | 63 |
64 |

Shape Builder

65 |
66 |

Shapes

67 | View Example 68 |
69 |
70 | 71 |
72 |

Elements

73 |
74 |

Base Door

75 | View Example 76 |
77 |
78 |

Base Window

79 | View Example 80 |
81 |
82 |

Simple Window

83 | View Example 84 |
85 |
86 |

Double Window

87 | View Example 88 |
89 |
90 |

Base Wall

91 | View Example 92 |
93 |
94 |

JSON Generated Plans

95 | View Example 96 |
97 |
98 |

Paper Frame

99 | View Example 100 |
101 |
102 |

Drawing Board

103 | View Example 104 |
105 |
106 | 107 |
108 | 109 | -------------------------------------------------------------------------------- /open-interface/index.ts: -------------------------------------------------------------------------------- 1 | export * from './oi-dropdown' -------------------------------------------------------------------------------- /open-interface/oi-dropdown.ts: -------------------------------------------------------------------------------- 1 | export class OIDropdown extends HTMLElement { 2 | private shadow: ShadowRoot; 3 | trigger: any; 4 | dropdown: any; 5 | 6 | constructor() { 7 | super(); 8 | this.shadow = this.attachShadow({ mode: "open" }); 9 | } 10 | 11 | connectedCallback() { 12 | this.render(); 13 | console.log(this); 14 | // Create Base Elements 15 | const dropdownTrigger = document.createElement('span'); 16 | dropdownTrigger.innerHTML = 'Select'; 17 | 18 | // Get elements 19 | const options = this.getElementsByTagName('oi-option'); 20 | if (options.length === 0) { 21 | throw new Error('Dropdown must have at least one oi-option element'); 22 | } 23 | 24 | // Create Dropdown 25 | const dropdown = document.createElement('div'); 26 | dropdown.classList.add('dropdown'); 27 | dropdown.appendChild(dropdownTrigger); 28 | 29 | const dropdownOptions = document.createElement('div'); 30 | dropdownOptions.classList.add('dropdown-options'); 31 | dropdown.appendChild(dropdownOptions); 32 | 33 | this.shadowRoot?.appendChild(dropdown); 34 | 35 | // Add options to dropdown 36 | for (let option of options) { 37 | const optionElement = document.createElement('span'); 38 | optionElement.innerHTML = option.innerHTML; 39 | optionElement.setAttribute('name', option.getAttribute('name') || option.innerHTML); 40 | optionElement.addEventListener('click', this.onDropdownSelect.bind(this)); 41 | dropdownOptions.appendChild(optionElement); 42 | } 43 | 44 | this.dropdown = dropdown; 45 | 46 | // Add event listeners 47 | dropdownTrigger.addEventListener('click', this.toggleDropdown.bind(this)); 48 | } 49 | 50 | onDropdownSelect(event) { 51 | const customEvent = new CustomEvent('onselect', { 52 | detail: { 53 | name: event.target.getAttribute('name'), 54 | value: event.target.innerHTML 55 | } 56 | }); 57 | console.log(customEvent); 58 | 59 | // Dispatch the event from the host element 60 | this.dispatchEvent(customEvent); 61 | 62 | // Close the dropdown after selection 63 | this.dropdown.classList.remove('active'); 64 | } 65 | 66 | toggleDropdown(event) { 67 | event.stopPropagation(); 68 | this.dropdown.classList.toggle('active'); 69 | } 70 | 71 | render() { 72 | this.shadow.innerHTML = ` 73 | 112 | `; 113 | 114 | console.log('OIDropdown connectedCallback'); 115 | } 116 | } 117 | 118 | customElements.define("oi-dropdown", OIDropdown); 119 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "@opengeometry/openplans", 3 | "version": "0.0.10", 4 | "author": "Vishwajeet Mane", 5 | "description": "Create 2D Drawings with OpenPlans", 6 | "main": "./index.js", 7 | "type": "module", 8 | "types": "./types/", 9 | "scripts": { 10 | "test": "echo \"Error: no test specified\" && exit 1", 11 | "dev": "vite", 12 | "clean": "rimraf dist", 13 | "build-examples": "vite build", 14 | "build": "npm run clean && rollup -c", 15 | "cpy-pkg-json": "cp package.json dist/package.json && cp README.md dist/README.md", 16 | "pre-prepare": "npm run build && npm run cpy-pkg-json", 17 | "release": "npm run pre-prepare && cd dist && npm publish" 18 | }, 19 | "keywords": [ 20 | "openplans", 21 | "opengeometry", 22 | "architecture", 23 | "design", 24 | "cad", 25 | "geometry" 26 | ], 27 | "license": "MIT", 28 | "dependencies": { 29 | "@opengeometry/openglyph": "^0.0.6" 30 | }, 31 | "peerDependencies": { 32 | "three": "^0.168.0", 33 | "@types/three": "^0.168.0", 34 | "camera-controls": "^2.9.0", 35 | "lil-gui": "^0.19.2" 36 | }, 37 | "devDependencies": { 38 | "@rollup/plugin-commonjs": "^28.0.2", 39 | "@rollup/plugin-node-resolve": "^16.0.0", 40 | "@rollup/plugin-terser": "^0.4.4", 41 | "@rollup/plugin-typescript": "^12.1.2", 42 | "@types/dat.gui": "^0.7.7", 43 | "@types/three": "^0.173.0", 44 | "rimraf": "^6.0.1", 45 | "rollup": "^4.34.8", 46 | "stats.js": "^0.17.0", 47 | "tslib": "^2.8.1", 48 | "vite": "^5.4.7" 49 | }, 50 | "publishConfig": { 51 | "directory": "dist" 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /public/typescript.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /public/wallCrossTexture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/public/wallCrossTexture.jpg -------------------------------------------------------------------------------- /public/wallDotTexture.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/public/wallDotTexture.jpg -------------------------------------------------------------------------------- /readme.md: -------------------------------------------------------------------------------- 1 | ![OpenPlans Version](https://img.shields.io/github/package-json/v/opengeometry-io/openplans?style=for-the-badge&color=4460FF) 2 | 3 | ## 🚧 Under Heavy Development 🚧 4 | 5 | **This project is currently a work in progress!** 6 | Expect frequent updates, breaking changes. 7 | 8 | ### A Full Suit of Elements to create Floorplans and 2D Designs of Models 9 | 10 | OpenPlans can be used in Two Ways 11 | #### 1. Element Creation 12 | - Create Base Families 13 | - Create Editor Tools 14 | 15 | #### 2. Headless Floorplan Generation 16 | 17 | #### Feature List 18 | - Triangulation 19 | - Shapes - Rectangles, Circles, Polygons 20 | - Extrusion 21 | - Offset 22 | - Boolean Operations (In Progress) 23 | 24 | #### Developer Guide 25 | You can find the developer guide [here](https://github.com/OpenGeometry-io/OpenPlans/blob/main/developer.md) 26 | 27 | - Docusaurus is being used for documentation 28 | - `cd docs` 29 | - `npm run start` to make any changes 30 | -------------------------------------------------------------------------------- /release.md: -------------------------------------------------------------------------------- 1 | OpenPlans is bundled and released as a Node.js package. 2 | The following steps outline the process for releasing a new version of the package: 3 | 4 | ### Release Process 5 | - Build package using `npm run build` 6 | - Copy `package.json` from root to `dist` 7 | - Navigate to dist and run `npm publish` -------------------------------------------------------------------------------- /rollup.config.js: -------------------------------------------------------------------------------- 1 | import resolve from '@rollup/plugin-node-resolve'; 2 | import commonjs from '@rollup/plugin-commonjs'; 3 | import typescript from '@rollup/plugin-typescript'; 4 | import { defineConfig } from 'rollup'; 5 | 6 | export default defineConfig({ 7 | input: 'src/index.ts', 8 | output: [ 9 | { 10 | dir: 'dist/', 11 | format: 'esm', 12 | sourcemap: true, 13 | entryFileNames: '[name].js' 14 | } 15 | ], 16 | external: ['three'], 17 | plugins: [ 18 | resolve(), 19 | commonjs(), 20 | typescript({ 21 | tsconfig: './tsconfig.json', // Use the tsconfig file for TypeScript options 22 | declaration: true, // Enable type declaration files 23 | declarationDir: 'dist/types', // Place declaration files in dist/types 24 | outDir: 'dist', // Place all JS files in dist 25 | rootDir: 'src', // Ensure TypeScript files are compiled from src 26 | }) 27 | ] 28 | }); 29 | -------------------------------------------------------------------------------- /src/.DS_Store: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/src/.DS_Store -------------------------------------------------------------------------------- /src/base-type.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | 3 | export interface ITheme { 4 | background: string 5 | color: string 6 | gridColor: number 7 | } 8 | 9 | export type activeTheme = 'darkBlue' | 'light' | 'dark' 10 | 11 | export interface ICanvasTheme { 12 | darkBlue: ITheme, 13 | light: ITheme, 14 | dark: ITheme, 15 | } 16 | 17 | export type IBaseWallType = 'exterior' | 'interior' | 'partition' | 'curtain'; 18 | export type IBaseWallMaterial = 'concrete' | 'brick' | 'wood' | 'glass' | 'metal' | 'other'; 19 | 20 | export interface IBaseWall { 21 | id?: string; 22 | labelName: string; 23 | type: 'wall'; 24 | dimensions: { 25 | start: { 26 | x: number; 27 | y: number; 28 | z: number; 29 | }; 30 | end: { 31 | x: number; 32 | y: number; 33 | z: number; 34 | }; 35 | width: number; 36 | }; 37 | color: number; 38 | wallType: IBaseWallType; 39 | wallHeight: number; 40 | wallThickness: number; 41 | wallMaterial: IBaseWallMaterial; 42 | coordinates: Array<[number, number, number]>; 43 | } 44 | 45 | export interface OPDoor { 46 | id: number; 47 | position: { 48 | x: number; 49 | y: number; 50 | z: number; 51 | }, 52 | anchor: { 53 | start: { 54 | x: number; 55 | y: number; 56 | z: number; 57 | }, 58 | end: { 59 | x: number; 60 | y: number; 61 | z: number; 62 | } 63 | }, 64 | thickness: number; 65 | halfThickness: number; 66 | hingeColor: number; 67 | hingeThickness: number; 68 | doorColor: number; 69 | } 70 | 71 | export interface OPSpace { 72 | id: number; 73 | position: { 74 | x: number; 75 | y: number; 76 | z: number; 77 | }, 78 | color: number; 79 | type: 'internal' | 'external'; 80 | coordinates: Array<[number, number, number]>; 81 | labelName: string; 82 | } 83 | 84 | export interface OPWallMesh { 85 | shadowMesh: THREE.Mesh; 86 | cosmeticMesh: THREE.Group; 87 | } -------------------------------------------------------------------------------- /src/constant.ts: -------------------------------------------------------------------------------- 1 | export const OPENPLANS_VERSION = '0.0.9'; -------------------------------------------------------------------------------- /src/custom-elements/custom.md: -------------------------------------------------------------------------------- 1 | ### Extending Base Entities 2 | 3 | - Provide a way to extend the base entities and create own geometry -------------------------------------------------------------------------------- /src/drawing/description-block.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/src/drawing/description-block.ts -------------------------------------------------------------------------------- /src/drawing/index.ts: -------------------------------------------------------------------------------- 1 | export * from './paper-frame'; 2 | export * from './row-info-block'; -------------------------------------------------------------------------------- /src/drawing/logo-info-block.ts: -------------------------------------------------------------------------------- 1 | import { Glyphs } from '@opengeometry/openglyph'; 2 | import * as THREE from 'three'; 3 | import { Polygon, Vector3D } from '../../kernel/dist'; 4 | 5 | export type BlockRowTypes = 'image' | 'text' | 'logo' | 'qrCode'; 6 | 7 | export interface LogoInfoBlockOptions { 8 | type: BlockRowTypes; 9 | id: string; 10 | name: string; 11 | width: number; 12 | height: number; 13 | url?: string; 14 | description?: string; 15 | borderColor?: number; 16 | } 17 | 18 | export class LogoInfoBlock extends Polygon { 19 | rowOptions: LogoInfoBlockOptions; 20 | name: string; 21 | 22 | set borderColor(color: number) { 23 | this.outlineColor = color; 24 | } 25 | 26 | // TODO: Add a method to set the background color, but we need a Polygon as well if we have to do that 27 | // set backgroundColor(color: string) 28 | 29 | constructor(options: LogoInfoBlockOptions) { 30 | super(); 31 | this.rowOptions = options; 32 | 33 | // calculate points based on width and height 34 | const points = [ 35 | new Vector3D(-options.width / 2, 0, -options.height / 2), 36 | new Vector3D(options.width / 2, 0, -options.height / 2), 37 | new Vector3D(options.width / 2, 0, options.height / 2), 38 | new Vector3D(-options.width / 2, 0, options.height / 2), 39 | ]; 40 | this.addVertices(points); 41 | this.outline = true; 42 | 43 | this.name = `rowInfoBlock` + this.ogid; 44 | 45 | // Material for the block 46 | const material = new THREE.MeshBasicMaterial({ 47 | color: 0xffffff, 48 | }); 49 | this.material = material; 50 | 51 | this.blockConfig(); 52 | this.addBlockData(options); 53 | } 54 | 55 | blockConfig() { 56 | const { width, height, borderColor } = this.rowOptions; 57 | this.borderColor = borderColor || 0x000000; 58 | } 59 | 60 | async addBlockData(logoOptions: LogoInfoBlockOptions) { 61 | // const { width, height } = textOptions; 62 | 63 | if (!logoOptions.url) { 64 | throw new Error('No image URL provided for logo block'); 65 | } 66 | 67 | const textureLoader = new THREE.TextureLoader(); 68 | 69 | textureLoader.load( 70 | logoOptions.url as string, 71 | (texture) => { 72 | texture.needsUpdate = true; 73 | const { width, height } = texture.image; 74 | const aspectRatio = width / height; 75 | 76 | const material = new THREE.MeshBasicMaterial({ 77 | map: texture, 78 | side: THREE.DoubleSide, 79 | color: 0xffffff, 80 | }); 81 | 82 | const dimensions = { 83 | width: logoOptions.width, 84 | height: logoOptions.height, 85 | } 86 | 87 | const absoluteHeight = logoOptions.height - 0.002; 88 | const absoluteWidth = absoluteHeight * aspectRatio; // Correct width calculation 89 | 90 | const planeGeometry = new THREE.PlaneGeometry(absoluteWidth, absoluteHeight); 91 | const mesh = new THREE.Mesh(planeGeometry, material); 92 | mesh.name = 'LogoInfoBlock'; 93 | mesh.position.set(0, 0.001, 0); 94 | mesh.rotateX( -Math.PI / 2 ); 95 | this.add(mesh); 96 | } 97 | ); 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/drawing/paper-frame.ts: -------------------------------------------------------------------------------- 1 | import { Polygon, Vector3D } from "../../kernel/dist"; 2 | import * as THREE from 'three'; 3 | 4 | export type PaperFormat = 'A4' | 'A3' | 'A2' | 'Custom'; 5 | export type PaperOrientation = 'portrait' | 'landscape'; 6 | export type SubNodeOptions = 'OuterBorder' | 'InnerBorder'; 7 | 8 | // Paper size based on PaperFormat 9 | export const paperSizes: Record = { 10 | A4: { width: 21.0, height: 29.7 }, 11 | A3: { width: 29.7, height: 42.0 }, 12 | A2: { width: 42.0, height: 59.4 }, 13 | Custom: { width: 0, height: 0 }, // Custom size will be set later 14 | }; 15 | 16 | export interface PaperFrameOptions { 17 | name: string; 18 | format: PaperFormat; 19 | orientation: PaperOrientation; 20 | margin: number; 21 | backgroundColor: string; 22 | borderColor: string; 23 | borderWidth: number; 24 | paperSize: { width: number; height: number }; 25 | } 26 | 27 | export class PaperFrame extends Polygon { 28 | ogType = 'paperFrame'; 29 | 30 | private options: PaperFrameOptions; 31 | private subNodes: Map = new Map(); 32 | 33 | private readonly Y_OFFSET = 0.0010; // Offset to avoid z-fighting 34 | 35 | constructor() { 36 | super(); 37 | this.options = { 38 | name: 'PaperFrame', 39 | format: 'A4', 40 | orientation: 'portrait', 41 | margin: 10, 42 | backgroundColor: '#ffffff', 43 | borderColor: '#000000', 44 | borderWidth: 1, 45 | paperSize: paperSizes['A4'], 46 | }; 47 | 48 | this.setupGeometry(); 49 | this.setupMaterial(); 50 | 51 | // Cosmetic properties 52 | this.createOuterBorder(); 53 | this.createInnerBorder(); 54 | } 55 | 56 | set paperName(name: string) { 57 | this.options.name = name; 58 | } 59 | 60 | set format(format: PaperFormat) { 61 | this.options.format = format; 62 | this.options.paperSize = paperSizes[format]; 63 | 64 | this.updateGeometry(); 65 | } 66 | 67 | set orientation(orientation: PaperOrientation) { 68 | this.options.orientation = orientation; 69 | this.updateGeometry(); 70 | } 71 | 72 | set margin(margin: number) { 73 | this.options.margin = margin; 74 | 75 | this.remove(this.subNodes.get('InnerBorder')!); 76 | this.createInnerBorder(); 77 | } 78 | 79 | get margin() { 80 | return this.options.margin; 81 | } 82 | 83 | set backgroundColor(color: string) { 84 | this.options.backgroundColor = color; 85 | } 86 | 87 | set borderColor(color: string) { 88 | this.options.borderColor = color; 89 | } 90 | 91 | set borderWidth(width: number) { 92 | this.options.borderWidth = width; 93 | } 94 | 95 | get paperSize() { 96 | return this.options.paperSize; 97 | } 98 | 99 | set paperSize(size: { width: number; height: number }) { 100 | if (this.format !== 'Custom') { 101 | throw new Error('Cannot set paper size for non-custom formats'); 102 | } 103 | this.options.paperSize = size; 104 | 105 | this.updateGeometry(); 106 | } 107 | 108 | private setupGeometry() { 109 | const { width, height } = this.options.paperSize; 110 | 111 | const isPortrait = this.options.orientation === 'portrait'; 112 | const absoluteWidth = isPortrait ? width : height; 113 | const absoluteHeight = isPortrait ? height : width; 114 | 115 | const vertices = [ 116 | new Vector3D(-absoluteWidth / 2, -absoluteHeight / 2, 0), // Bottom left 117 | new Vector3D(absoluteWidth / 2, -absoluteHeight / 2, 0), // Bottom right 118 | new Vector3D(absoluteWidth / 2, absoluteHeight / 2, 0), // Top right 119 | new Vector3D(-absoluteWidth / 2, absoluteHeight / 2, 0), // Top left 120 | ]; 121 | this.addVertices(vertices); 122 | 123 | this.rotation.x = -Math.PI / 2; // Rotate to face the camera 124 | this.position.y = -0.01; 125 | // Create The Polygon Page Done here 126 | } 127 | 128 | private setupMaterial() { 129 | // Setup the material for the paper frame 130 | const material = new THREE.MeshBasicMaterial({ 131 | color: this.options.backgroundColor, 132 | // side: THREE.DoubleSide, 133 | }); 134 | this.material = material; 135 | } 136 | 137 | private createOuterBorder() { 138 | const borderMaterial = new THREE.LineBasicMaterial({ 139 | color: '#000000', 140 | linewidth: 1, 141 | }); 142 | const borderGeometry = new THREE.EdgesGeometry(this.geometry); 143 | const borderMesh = new THREE.LineSegments(borderGeometry, borderMaterial); 144 | this.add(borderMesh); 145 | this.subNodes.set('OuterBorder', borderMesh); 146 | } 147 | 148 | private createInnerBorder() { 149 | const { width, height } = this.options.paperSize; 150 | const margin = this.options.margin / 10; 151 | 152 | const isPortrait = this.options.orientation === 'portrait'; 153 | const absoluteWidth = isPortrait ? width : height; 154 | const absoluteHeight = isPortrait ? height : width; 155 | 156 | const innerVertices = [ 157 | new THREE.Vector3(-absoluteWidth / 2 + margin, -absoluteHeight / 2 + margin, 0), // Top left 158 | new THREE.Vector3(absoluteWidth / 2 - margin, -absoluteHeight / 2 + margin, 0), // Top right 159 | 160 | new THREE.Vector3(absoluteWidth / 2 - margin, absoluteHeight / 2 - margin, 0), // Bottom right 161 | new THREE.Vector3(-absoluteWidth / 2 + margin, absoluteHeight / 2 - margin, 0), // Bottom left 162 | 163 | new THREE.Vector3(-absoluteWidth / 2 + margin, -absoluteHeight / 2 + margin, 0), // Top left 164 | new THREE.Vector3(-absoluteWidth / 2 + margin, absoluteHeight / 2 - margin, 0), // Bottom left 165 | 166 | new THREE.Vector3(absoluteWidth / 2 - margin, -absoluteHeight / 2 + margin, 0), // Top right 167 | new THREE.Vector3(absoluteWidth / 2 - margin, absoluteHeight / 2 - margin, 0), // Bottom right 168 | ]; 169 | 170 | const innerBorderMaterial = new THREE.LineBasicMaterial({ 171 | color: '#000000', 172 | linewidth: 1, 173 | }); 174 | const innerBorderGeometry = new THREE.BufferGeometry().setFromPoints(innerVertices); 175 | const innerBorderMesh = new THREE.LineSegments(innerBorderGeometry, innerBorderMaterial); 176 | innerBorderMesh.position.set(0, 0, this.Y_OFFSET); 177 | this.add(innerBorderMesh); 178 | this.subNodes.set('InnerBorder', innerBorderMesh); 179 | } 180 | 181 | private updateGeometry() { 182 | this.resetVertices(); 183 | 184 | // Clear previous geometry and borders 185 | this.remove(this.subNodes.get('InnerBorder')!); 186 | this.remove(this.subNodes.get('OuterBorder')!); 187 | 188 | this.setupGeometry(); 189 | this.setupMaterial(); 190 | this.createOuterBorder(); 191 | this.createInnerBorder(); 192 | } 193 | 194 | /** 195 | * 196 | * @param infoBlock InfoBlock 197 | * @description Adds an InfoBlock to the paper frame. The InfoBlock is positioned based on its placement property. 198 | */ 199 | // async addBlock(infoBlock: InfoBlock) { 200 | // const infoBlockMaterial = new THREE.LineBasicMaterial({ 201 | // color: infoBlock.options.borderColor, 202 | // linewidth: 1, 203 | // }); 204 | // const { width, height } = infoBlock.options; 205 | // const infoBlockGeometry = new THREE.BufferGeometry(); 206 | // const blockVertices = [ 207 | // new THREE.Vector3(-width / 2, -height / 2, this.Y_OFFSET), // Bottom left 208 | // new THREE.Vector3(width / 2, -height / 2, this.Y_OFFSET), // Bottom right 209 | // new THREE.Vector3(width / 2, height / 2, this.Y_OFFSET), // Top right 210 | // new THREE.Vector3(-width / 2, height / 2, this.Y_OFFSET), // Top left 211 | // new THREE.Vector3(-width / 2, -height / 2, this.Y_OFFSET), // Bottom left 212 | // new THREE.Vector3(-width / 2, height / 2, this.Y_OFFSET), // Top left 213 | // new THREE.Vector3(width / 2, -height / 2, this.Y_OFFSET), // Bottom right 214 | // new THREE.Vector3(width / 2, height / 2, this.Y_OFFSET), // Top right 215 | // ]; 216 | // infoBlockGeometry.setFromPoints(blockVertices); 217 | // const infoBlockMesh = new THREE.LineSegments(infoBlockGeometry, infoBlockMaterial); 218 | 219 | // this.add(infoBlockMesh); 220 | 221 | // this.blocks.push(infoBlock); 222 | // infoBlockMesh.name = infoBlock.options.id; 223 | // this.subNodes.set(infoBlock.options.id, infoBlockMesh); 224 | 225 | // // Set position based on the block layout 226 | // const placement = infoBlock.options.placement; 227 | // switch (placement) { 228 | // case 'topRight': 229 | // infoBlockMesh.position.set(this.options.paperSize.width / 2 - infoBlock.options.width / 2 - this.margin / 10, this.options.paperSize.height / 2 - infoBlock.options.height / 2 - this.margin / 10, this.Y_OFFSET); 230 | // break; 231 | // case 'topLeft': 232 | // infoBlockMesh.position.set(-this.options.paperSize.width / 2 + infoBlock.options.width / 2 + this.margin / 10, this.options.paperSize.height / 2 - infoBlock.options.height / 2 - this.margin / 10, this.Y_OFFSET); 233 | // break; 234 | // case 'bottomRight': 235 | // infoBlockMesh.position.set(this.options.paperSize.width / 2 - infoBlock.options.width / 2 - this.margin / 10, -this.options.paperSize.height / 2 + infoBlock.options.height / 2 + this.margin / 10, this.Y_OFFSET); 236 | // break; 237 | // case 'bottomLeft': 238 | // infoBlockMesh.position.set(-this.options.paperSize.width / 2 + infoBlock.options.width / 2 + this.margin / 10, -this.options.paperSize.height / 2 + infoBlock.options.height / 2 + this.margin / 10, this.Y_OFFSET); 239 | // break; 240 | // default: 241 | // throw new Error('Invalid block placement'); 242 | // } 243 | 244 | // // Add Blocks based on the layout 245 | // if (!infoBlock.layoutOptions.layout) { 246 | // console.log('No layout set for the block'); 247 | // return; 248 | // } 249 | // const layout = infoBlock.layoutOptions.layout; 250 | // const layoutBlocks: Record = infoBlock.layoutOptions.blocks; 251 | 252 | // const layoutArray = layout.split('\n'); 253 | 254 | // for (let i = 1; i < layoutArray.length - 1; i++) { 255 | // const lBlockName = layoutArray[i].trim(); 256 | // const lBlock = layoutBlocks[lBlockName]; 257 | 258 | // for (const block of await lBlock.getBlockData()) { 259 | // let absoluteHeightInBlock = infoBlockMesh.position.y + infoBlock.options.height / 2 - block.userData.dimension.height / 2; 260 | // if (i > 1) { 261 | // absoluteHeightInBlock = infoBlockMesh.position.y + infoBlock.options.height / 2 - block.userData.dimension.height / 2 - (i - 1) * block.userData.dimension.height; 262 | // } 263 | 264 | // block.position.set( 265 | // infoBlockMesh.position.x - infoBlock.options.width / 2 + block.userData.dimension.width / 2, 266 | // absoluteHeightInBlock, 267 | // this.Y_OFFSET); 268 | 269 | // this.add(block); 270 | // this.subNodes.set(block.name, block); 271 | // } 272 | // } 273 | // } 274 | 275 | // updateBlocks() { 276 | // for (const block of this.blocks) { 277 | // const blockMesh = this.subNodes.get(block.options.id); 278 | 279 | // this.remove(blockMesh!); 280 | 281 | // this.subNodes.delete(block.options.id); 282 | // } 283 | 284 | // console.log(this.subNodes); 285 | 286 | // for (const block of this.blocks) { 287 | // console.log(block); 288 | // } 289 | // } 290 | 291 | removeBlock(blockId: string) { 292 | const blockMesh = this.subNodes.get(blockId); 293 | console.log('BlockMesh:', blockMesh); 294 | if (blockMesh) { 295 | this.remove(blockMesh); 296 | this.subNodes.delete(blockId); 297 | } 298 | // remove logo and other blocks 299 | } 300 | } 301 | -------------------------------------------------------------------------------- /src/drawing/row-info-block.ts: -------------------------------------------------------------------------------- 1 | import { Glyphs } from '@opengeometry/openglyph'; 2 | import * as THREE from 'three'; 3 | import { Rectangle, Vector3D } from '../../kernel/dist'; 4 | 5 | export type BlockRowTypes = 'image' | 'text' | 'logo' | 'qrCode'; 6 | 7 | export interface RowInfoBlockOptions { 8 | type: BlockRowTypes; 9 | id: string; 10 | name: string; 11 | width: number; 12 | height: number; 13 | backgroundColor?: string; 14 | borderColor?: number; 15 | image?: string; 16 | title?: string; 17 | description?: string; 18 | fontSize?: number; 19 | fontColor?: number; 20 | } 21 | 22 | export class RowInfoBlock extends Rectangle { 23 | rowOptions: RowInfoBlockOptions; 24 | name: string; 25 | 26 | set borderColor(color: number) { 27 | this.color = color; 28 | } 29 | 30 | set description(text: string) { 31 | this.rowOptions.description = text; 32 | this.clearBlockData(); 33 | this.addBlockData(this.rowOptions); 34 | } 35 | 36 | set title(text: string) { 37 | this.rowOptions.title = text; 38 | } 39 | 40 | // TODO: Add a method to set the background color, but we need a Polygon as well if we have to do that 41 | // set backgroundColor(color: string) 42 | 43 | constructor(options: RowInfoBlockOptions) { 44 | super({ 45 | width: options.width, 46 | breadth: options.height, 47 | center: new Vector3D(0, 0, 0) 48 | }); 49 | this.rowOptions = options; 50 | this.name = `rowInfoBlock` + this.ogid; 51 | 52 | this.blockConfig(); 53 | this.addBlockData(options); 54 | } 55 | 56 | blockConfig() { 57 | const { width, height, backgroundColor, borderColor } = this.rowOptions; 58 | this.color = borderColor || 0x000000; 59 | } 60 | 61 | clearBlockData() { 62 | while (this.children.length > 0) { 63 | const child = this.children[0]; 64 | this.remove(child); 65 | if (child instanceof THREE.Mesh) { 66 | child.geometry.dispose(); 67 | if (child.material instanceof THREE.Material) { 68 | child.material.dispose(); 69 | } 70 | child.geometry = null; 71 | child.material = null; 72 | } 73 | } 74 | this.children = []; 75 | } 76 | 77 | async addBlockData(textOptions: RowInfoBlockOptions) { 78 | const { width, height } = textOptions; 79 | 80 | if (textOptions.title) { 81 | const textMesh = Glyphs.addGlyph( 82 | textOptions.title, 83 | textOptions.fontSize || 0.5, 84 | textOptions.fontColor || 0x000000, 85 | false 86 | ); 87 | 88 | // Top Left Corner 89 | if (textOptions.fontSize) { 90 | const box = new THREE.Box3().setFromObject(textMesh); 91 | const center = new THREE.Vector3(); 92 | box.getCenter(center); 93 | const textWidth = box.max.x - box.min.x; 94 | const textHeight = box.max.z - box.min.z; 95 | textMesh.position.set( 96 | -width / 2 + textWidth / 2, 97 | center.y, 98 | -height / 2 + textHeight / 2 99 | ); 100 | } 101 | this.add(textMesh); 102 | } 103 | 104 | if (textOptions.description) { 105 | const textMesh = Glyphs.addGlyph( 106 | textOptions.description, 107 | textOptions.fontSize || 0.5, 108 | textOptions.fontColor || 0x000000, 109 | false 110 | ); 111 | 112 | // Bottom Left Corner 113 | if (textOptions.fontSize) { 114 | const box = new THREE.Box3().setFromObject(textMesh); 115 | const center = new THREE.Vector3(); 116 | box.getCenter(center); 117 | const textWidth = box.max.x - box.min.x; 118 | const textHeight = box.max.z - box.min.z; 119 | textMesh.position.set( 120 | -width / 2 + textWidth / 2, 121 | center.y, 122 | height / 2 - textHeight / 2 123 | ); 124 | } 125 | this.add(textMesh); 126 | } 127 | } 128 | } 129 | -------------------------------------------------------------------------------- /src/drawing/side-profile.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/src/drawing/side-profile.ts -------------------------------------------------------------------------------- /src/drawing/toon-profile.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/src/drawing/toon-profile.ts -------------------------------------------------------------------------------- /src/drawing/top-profile.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/src/drawing/top-profile.ts -------------------------------------------------------------------------------- /src/elements/base-element.ts: -------------------------------------------------------------------------------- 1 | import { Vector3 } from "three"; 2 | import { BasePoly } from "../../kernel/dist"; 3 | import * as THREE from 'three'; 4 | 5 | /** 6 | * Purpose of Geometry Set is to provide a set of properties that can be used to create a geometry 7 | * This can be stored in a database and used to recreate the geometry 8 | * Anchors should only be adjusted by the Kernel and not by the user 9 | */ 10 | export interface BaseGeometrySet { 11 | type: string; 12 | id: string; 13 | position: Vector3; 14 | length: number; 15 | material: string; 16 | anchor?: { 17 | start: Vector3; 18 | end: Vector3; 19 | }; 20 | } 21 | 22 | // TODO: BasePoly is provided from OpenGeometry, do more research if something more robust class can be created for Base Elements 23 | export abstract class BaseElement extends BasePoly { 24 | abstract ogType: string; 25 | 26 | abstract visibleMesh: THREE.Group; 27 | 28 | public isEditing: boolean = false; 29 | public isHovered: boolean = false; 30 | public isSelected: boolean = false; 31 | public isLocked: boolean = false; 32 | public isHidden: boolean = false; 33 | public isDeleted: boolean = false; 34 | 35 | abstract geometrySet: BaseGeometrySet; 36 | 37 | /** 38 | * Creating the Element Geometry based on the geometry set 39 | */ 40 | abstract setupGeometry(): void; 41 | abstract setupEvents(): void; 42 | 43 | /** 44 | * To be used for updating main geometry and shadow geometry 45 | */ 46 | abstract updateGeometry(): void; 47 | 48 | /** 49 | * Check if the geometry set is valid, if a property is set optional it will be ignored 50 | * @param geometrySet 51 | * @returns 52 | */ 53 | verifyGeometrySet(geometrySet: BaseGeometrySet): boolean { 54 | type RequiredKeys = { [K in keyof T]-?: undefined extends T[K] ? never : K }[keyof T]; 55 | const requiredKeys: RequiredKeys[] = ["type", "id", "position", "length", "material"]; 56 | return requiredKeys.every(key => geometrySet[key] !== undefined && geometrySet[key] !== null); 57 | } 58 | 59 | /** 60 | * Adjust the length of the geometry set based on the anchor points 61 | * @param geometrySet 62 | */ 63 | adjustAnchors(length: number): void { 64 | if (length < 0.1) { 65 | length = 0.1; 66 | throw new Error('Length cannot be less than 1'); 67 | } 68 | 69 | this.geometrySet.length = length; 70 | const startAnchor = length / 2; 71 | const endAnchor = length / 2; 72 | this.geometrySet.anchor = { 73 | start: new Vector3(-startAnchor, 0, 0), 74 | end: new Vector3(endAnchor, 0, 0) 75 | }; 76 | } 77 | } -------------------------------------------------------------------------------- /src/elements/base-elements.md: -------------------------------------------------------------------------------- 1 | #### Base Elements 2 | 3 | - Each Base Element is a Threejs Mesh. That way it becomes easier for us to manipulate a parent and everything inside that element has a local reference to that object. 4 | - Each Element also has a shadow mesh, which represents the OpenGeometry Mesh, I am yet to find what's the best way to represent the Mesh but as of now we can computationally afford creation of two meshes. 5 | - All The Meshes must have double side rendering on, I am yet to figure out how to keep rendering order consistent across all the elements 6 | 7 | #### Rigid Elements and Soft Elements 8 | 9 | Rigid 10 | - Some Elements Like Door have predefined parts e.g. Hinges, Nobs. 11 | - The elements with predefined parts shall be called Rigid Elements and they can be extended to create their own different types 12 | - Every Rigid Body Element can be created as soft element 13 | - Rigid Body Element saves the time and effort for customisations 14 | 15 | Soft 16 | - Soft Elements are created by combing different geometry 17 | - They can be grouped to form a new element altogether 18 | - To create a soft element, users would have to use perform geometry operations on their own and then group it to publish 19 | -------------------------------------------------------------------------------- /src/elements/base-spaces.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Space is somewhat equivalent to a room in a building 3 | * It can be bounded by walls on all sides 4 | * It can also be a free space without walls 5 | */ 6 | import { Polygon, Vector3D } from "../../kernel/dist"; 7 | import { Pencil } from "../../kernel/dist/src/pencil"; 8 | import * as THREE from 'three'; 9 | import { GlyphNode, Glyphs } from "@opengeometry/openglyph"; 10 | import { OPSpace } from "./base-types"; 11 | import { Event } from "../utils/event"; 12 | 13 | interface SpaceContainerMesh { 14 | id: number; 15 | mainMesh: THREE.Mesh; 16 | labelMesh: GlyphNode; 17 | } 18 | 19 | export class BaseSpace extends Polygon { 20 | public ogType = 'space'; 21 | 22 | mesh: Polygon | null = null; 23 | private spaceSetMesh: SpaceContainerMesh = {} as SpaceContainerMesh; 24 | private spaceSet: OPSpace; 25 | 26 | isEditing = false; 27 | 28 | isHovered = false; 29 | isHighlighted = false; 30 | isLocked = false; 31 | 32 | onSpaceSelected = new Event(); 33 | 34 | get labelName() { 35 | const label = this.spaceSetMesh.labelMesh; 36 | return label.text; 37 | } 38 | 39 | set labelName(name: string) { 40 | const label = this.spaceSetMesh.labelMesh; 41 | Glyphs.updateGlyphText(label.uuid, name); 42 | } 43 | 44 | constructor(private pencil: Pencil, initialSpaceSet?: OPSpace) { 45 | super(); 46 | this.name = `space`+this.ogid; 47 | 48 | if (initialSpaceSet) { 49 | this.spaceSet = initialSpaceSet; 50 | } else { 51 | this.spaceSet = { 52 | id: 0, 53 | position: { 54 | x: 0, 55 | y: 0, 56 | z: 0 57 | }, 58 | color: 0xff0000, 59 | type: 'internal', 60 | coordinates: [ 61 | [-10, 0, -10], 62 | [10, 0, -10], 63 | [10, 0, 10], 64 | [-10, 0, 10] 65 | ], 66 | labelName: 'Space' 67 | }; 68 | } 69 | 70 | this.setGeometry(); 71 | this.setupEvents(); 72 | } 73 | 74 | private setupEvents() { 75 | this.pencil.onElementHover.add((mesh) => { 76 | if (mesh.name === this.name) { 77 | this.isHovered = true; 78 | // const material = this.material as THREE.MeshBasicMaterial; 79 | // material.color.setHex(0x00ff00); 80 | } else { 81 | this.isHovered = false; 82 | const material = this.material as THREE.MeshBasicMaterial; 83 | 84 | if (this.isEditing) { 85 | material.color.setHex(0xffffff); 86 | } else { 87 | material.color.setHex(this.spaceSet.color); 88 | } 89 | } 90 | }); 91 | 92 | this.pencil.onElementSelected.add((mesh) => { 93 | // if (mesh.name === this.name) { 94 | // this.isEditing = true; 95 | // const material = this.material as THREE.MeshBasicMaterial; 96 | // material.color.setHex(0xff00ff); 97 | 98 | // this.onSpaceSelected.trigger(this.name); 99 | // } 100 | }); 101 | } 102 | 103 | private setGeometry() { 104 | if (!this.spaceSetMesh) return; 105 | 106 | const { coordinates, color } = this.spaceSet; 107 | const { x, y, z } = this.spaceSet.position; 108 | 109 | const spaceGeoemtry = new THREE.BufferGeometry(); 110 | 111 | for (let i = 0; i < coordinates.length; i++) { 112 | const coord = coordinates[i]; 113 | const x = coord[0]; 114 | const y = coord[1]; 115 | const z = coord[2]; 116 | const vector = new Vector3D(x, y, z); 117 | this.addVertex(vector); 118 | } 119 | 120 | const randomColor = Math.floor(Math.random() * 16777215); 121 | this.spaceSet.color = randomColor; 122 | const spaceMaterial = new THREE.MeshBasicMaterial({ color: randomColor, side: THREE.DoubleSide, transparent: true, opacity: 0.5 }); 123 | const spaceMesh = new THREE.Mesh(spaceGeoemtry, spaceMaterial); 124 | spaceMesh.position.set(x, y, z); 125 | this.material = spaceMaterial; 126 | 127 | // this.spaceSetMesh.mainMesh = spaceMesh; 128 | // this.add(spaceMesh); 129 | 130 | const label = Glyphs.addGlyph(this.spaceSet.labelName, 8, 0x000000, false); 131 | this.spaceSetMesh.labelMesh = label; 132 | 133 | // // get center of space mesh 134 | const center = new THREE.Vector3(); 135 | this.geometry.computeBoundingBox(); 136 | if (!this.geometry.boundingBox) return; 137 | this.geometry.boundingBox.getCenter(center); 138 | label.position.set(center.x + x, center.y + 0.01, center.z + z); // add small offset to y axis to avoid z-fighting 139 | this.add(label); 140 | 141 | this.pencil.pencilMeshes.push(this); 142 | } 143 | 144 | get area() { 145 | const spaceDim = { 146 | area: 0, 147 | perimeter: 0 148 | }; 149 | 150 | const position = this.geometry.getAttribute('position'); 151 | // calculate area 152 | for (let i = 0; i < position.array.length; i+=9) { 153 | const triangle = new THREE.Triangle( 154 | new THREE.Vector3(position.array[i], position.array[i+1], position.array[i+2]), 155 | new THREE.Vector3(position.array[i+3], position.array[i+4], position.array[i+5]), 156 | new THREE.Vector3(position.array[i+6], position.array[i+7], position.array[i+8]) 157 | ); 158 | spaceDim.area += triangle.getArea(); 159 | } 160 | 161 | return spaceDim.area; 162 | } 163 | } -------------------------------------------------------------------------------- /src/elements/board.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Vector3D } from '../../kernel/dist'; 3 | import { CSS2DObject } from 'three/examples/jsm/renderers/CSS2DRenderer.js'; 4 | import { PolygonShape } from '../shape/polygon-shape'; 5 | 6 | export interface OPBoard { 7 | id?: string; 8 | center: { 9 | x: number; 10 | y: number; 11 | z: number; 12 | }; 13 | color: number; 14 | type: 'board'; 15 | coordinates: Array<[number, number, number]>; 16 | labelName: string; 17 | dimensions: { 18 | start: { 19 | x: number; 20 | y: number; 21 | z: number; 22 | }, 23 | end: { 24 | x: number; 25 | y: number; 26 | z: number; 27 | }, 28 | width: number; 29 | height: number; 30 | }, 31 | // A Board can have multiple layers which are used to organize elements on the board 32 | // For example, a board can have a layer for walls, a layer for furniture, etc. 33 | layers?: string[]; 34 | } 35 | 36 | // TODO: Use Polygon Shape instead of Polygon Mesh, which will provide us with editing capabilities 37 | export class Board extends PolygonShape { 38 | ogType = 'board'; 39 | 40 | // Properties that cannot be set externally should be just private, can be accessed at runtime 41 | subNodes: Map = new Map(); 42 | private labelDivMesh: CSS2DObject | null = null; 43 | 44 | // Properties that can be set externally start with an #, provides tight encapsulation and prevents accidental access 45 | _selected = false; 46 | 47 | propertySet: OPBoard = { 48 | center: { 49 | x: 0, 50 | y: 0, 51 | z: 0, 52 | }, 53 | color: 0xcccccc, 54 | type: 'board', 55 | /* 56 | Anti-clockwise coordinates of the board, starting from top left corner. 57 | Ends in top right corner. 58 | The coordinates are in the XY plane, so Z is always 0. 59 | */ 60 | coordinates: [ 61 | [0, 0, 0], 62 | [0, 0, 0], 63 | [0, 0, 0], 64 | [0, 0, 0] 65 | ], 66 | labelName: 'Drawing Board', 67 | dimensions: { 68 | start: { 69 | x: 0, 70 | y: 0, 71 | z: 0 72 | }, 73 | end: { 74 | x: 10, 75 | y: -10, 76 | z: 0, 77 | }, 78 | width: 20, 79 | height: 20 80 | } 81 | }; 82 | 83 | set selected(value: boolean) { 84 | if (value) { 85 | this.outlineColor = 0x4460FF; 86 | } 87 | else { 88 | this.outlineColor = 0x000000; 89 | } 90 | this._selected = value; 91 | } 92 | 93 | get selected() { 94 | return this._selected; 95 | } 96 | 97 | set width(value: number) { 98 | this.propertySet.dimensions.width = value; 99 | this.calculateCoordinatesByConfig(); 100 | } 101 | 102 | get width() { 103 | return this.propertySet.dimensions.width; 104 | } 105 | 106 | set height(value: number) { 107 | this.propertySet.dimensions.height = value; 108 | this.calculateCoordinatesByConfig(); 109 | } 110 | 111 | get height() { 112 | return this.propertySet.dimensions.height; 113 | } 114 | 115 | set start(value: { x: number; y: number; z: number }) { 116 | this.propertySet.dimensions.start.x = value.x; 117 | this.propertySet.dimensions.start.y = value.y; 118 | 119 | this.calculateCoordinatesByConfig(); 120 | } 121 | 122 | set labelName(value: string) { 123 | this.propertySet.labelName = value; 124 | } 125 | 126 | get labelName() { 127 | return this.propertySet.labelName; 128 | } 129 | 130 | set color(value: number) { 131 | const material = new THREE.MeshBasicMaterial({ 132 | color: value, 133 | }); 134 | this.material = material; 135 | } 136 | 137 | get color() { 138 | return (this.material as THREE.MeshBasicMaterial).color.getHex(); 139 | } 140 | 141 | constructor(boardConfig?: OPBoard) { 142 | super(); 143 | 144 | if (boardConfig) { 145 | this.setOPConfig(boardConfig); 146 | } else { 147 | this.propertySet.id = this.ogid; 148 | } 149 | 150 | this.calculateCoordinatesByConfig(); 151 | 152 | // If we create XZ plane, the polygon has normals facing downwards, so trick as of now is to create XY plane 153 | // and then rotate it to face upwards 154 | this.rotateX(-Math.PI / 2); 155 | this.createLabelDivMesh(); 156 | } 157 | 158 | private calculateCoordinatesByConfig() { 159 | const start = this.propertySet.dimensions.start; 160 | // start.y = -start.y; // find out if we need to use this, this is how figma works 161 | 162 | const width = this.propertySet.dimensions.width; 163 | const height = this.propertySet.dimensions.height; 164 | 165 | this.propertySet.coordinates[0][0] = start.x; 166 | this.propertySet.coordinates[0][1] = start.y; 167 | this.propertySet.coordinates[1][0] = start.x; 168 | this.propertySet.coordinates[1][1] = start.y - height; 169 | this.propertySet.coordinates[2][0] = start.x + width; 170 | this.propertySet.coordinates[2][1] = start.y - height; 171 | this.propertySet.coordinates[3][0] = start.x + width; 172 | this.propertySet.coordinates[3][1] = start.y; 173 | 174 | // For reference only, not used in calculations 175 | // These two properties should not influence the coordinates, they are just for reference 176 | this.propertySet.center.x = start.x + width / 2; 177 | this.propertySet.center.y = start.y - height / 2; 178 | this.propertySet.center.z = start.z; 179 | this.propertySet.dimensions.end.x = start.x + width; 180 | this.propertySet.dimensions.end.y = start.y + height; 181 | 182 | this.setOPGeometry(); 183 | } 184 | 185 | setOPConfig(propertySet: OPBoard) { 186 | this.propertySet = propertySet; 187 | } 188 | 189 | getOPConfig(): OPBoard { 190 | return this.propertySet; 191 | } 192 | 193 | setOPGeometry() { 194 | this.resetVertices(); 195 | this.outline = false; 196 | 197 | const points = [ 198 | new Vector3D(this.propertySet.coordinates[0][0], this.propertySet.coordinates[0][1], 0), 199 | new Vector3D(this.propertySet.coordinates[1][0], this.propertySet.coordinates[1][1], 0), 200 | new Vector3D(this.propertySet.coordinates[2][0], this.propertySet.coordinates[2][1], 0), 201 | new Vector3D(this.propertySet.coordinates[3][0], this.propertySet.coordinates[3][1], 0), 202 | ]; 203 | this.addVertices(points); 204 | 205 | // this.getBrepData(); 206 | this.setOPMaterial(); 207 | this.outline = true; 208 | 209 | this.labelDivMesh?.position.set( 210 | this.propertySet.dimensions.start.x, 211 | this.propertySet.dimensions.start.y, 212 | this.propertySet.dimensions.start.z 213 | ); 214 | } 215 | 216 | dispose() { 217 | this.geometry.dispose(); 218 | this.labelDivMesh?.element.remove(); 219 | this.labelDivMesh = null; 220 | this.clear(); 221 | this.subNodes.clear(); 222 | this.removeFromParent(); 223 | } 224 | 225 | setOPMaterial() { 226 | const material = new THREE.MeshBasicMaterial({ 227 | color: 0xffffff, 228 | }); 229 | this.material = material; 230 | } 231 | 232 | private createLabelDivMesh() { 233 | const labelDiv = document.createElement('div'); 234 | labelDiv.textContent = this.propertySet.labelName; 235 | labelDiv.style.fontSize = '12px'; 236 | 237 | this.labelDivMesh = new CSS2DObject(labelDiv); 238 | this.add(this.labelDivMesh); 239 | 240 | setTimeout(() => { 241 | this.setLabelPosition(); 242 | }, 100); 243 | } 244 | 245 | private setLabelPosition() { 246 | const labelDiv = this.labelDivMesh?.element; 247 | if (!labelDiv) return; 248 | 249 | const width = labelDiv.clientWidth; 250 | const newWidth = width + width + 10; 251 | 252 | labelDiv.style.width = `${newWidth}px`; 253 | labelDiv.style.textAlign = 'right'; 254 | 255 | const height = labelDiv.clientHeight; 256 | const newHeight = height + height + 10; 257 | 258 | labelDiv.style.height = `${newHeight}px`; 259 | 260 | this.labelDivMesh?.position.set( 261 | this.propertySet.dimensions.start.x, 262 | this.propertySet.dimensions.start.y, 263 | 0 264 | ); 265 | } 266 | } -------------------------------------------------------------------------------- /src/elements/elements.md: -------------------------------------------------------------------------------- 1 | Elements extends one major Shape and has one or more ShapeBuilder. 2 | Major shape is the base shape and commands the element's geometry. 3 | 4 | # Element 5 | List of Elements 6 | - Wall 7 | - Door 8 | - Window 9 | - Column 10 | - Slab 11 | - Stairs 12 | - Beam 13 | 14 | # Calculated Properties vs User Defined Properties 15 | - For some elements, the properties are calculated based on the geometry. 16 | E.g. When a door is created Door is created, coordinates are calculated based on the start and end points of the wall. 17 | Here, **coordinates** are calculated properties. 18 | 19 | E.g. When a wall is created start and end points are calculated based on coordinates of the wall. 20 | Here, **start and end points** are calculated properties. 21 | -------------------------------------------------------------------------------- /src/helpers/OpenGridHelper.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Author: Vishwajeet Mane 3 | * References 4 | * 1. https://github.com/Fyrestar/THREE.InfiniteGridHelper/blob/master/InfiniteGridHelper.js 5 | * 2. https://dev.to/javiersalcedopuyo/simple-infinite-grid-shader-5fah 6 | */ 7 | 8 | /** 9 | * Notes 10 | * 1. Area is the size of the grid 11 | * 2. Visible Area is the portion of the grid that is visible with the raidal blur 12 | */ 13 | 14 | let vAxes = "xy"; 15 | 16 | import * as THREE from 'three'; 17 | 18 | function generateGridPoints(axes: string, size: number) { 19 | const gridPoints = []; 20 | gridPoints.push(1, 0, 1); 21 | gridPoints.push(-1, 0, 1); 22 | gridPoints.push(-1, 0, -1); 23 | gridPoints.push(1, 0, -1); 24 | gridPoints.push(1, 0, 1); 25 | 26 | const offsets = []; 27 | const area = size; 28 | const areaNeg = -area; 29 | for (let i = areaNeg/2; i < area/2; i++) { 30 | for (let j = areaNeg/2; j < area/2; j++) { 31 | offsets.push(i, 0, j); 32 | } 33 | } 34 | 35 | return { 36 | gridPoints: new Float32Array(gridPoints), 37 | offset: new Float32Array(offsets) 38 | }; 39 | } 40 | 41 | class Grid { 42 | constructor(axes="xzy", color = new THREE.Vector3(0, 0, 0), size = 50, visibleArea = 25, polka = false) { 43 | const axesArray = axes.substring(0, 2); 44 | vAxes = axesArray; 45 | const shader = Shader; 46 | 47 | shader.uniforms.lineColor.value = color; 48 | shader.uniforms.visibleArea.value = visibleArea; 49 | shader.uniforms.polka.value = polka; 50 | 51 | const gridMaterial = new THREE.RawShaderMaterial({ 52 | name: shader.name, 53 | uniforms: shader.uniforms, 54 | vertexShader: vertexShaderFunc(), 55 | fragmentShader: fragmentShaderFun(), 56 | side: THREE.DoubleSide, 57 | forceSinglePass: true, 58 | transparent: true 59 | }); 60 | 61 | const gridData = generateGridPoints(axes, size); 62 | const gridPosition = gridData.gridPoints; 63 | const offsets = gridData.offset; 64 | 65 | const gridInstancedGeometry = new THREE.InstancedBufferGeometry(); 66 | gridInstancedGeometry.instanceCount = offsets.length / 3; 67 | gridInstancedGeometry.setAttribute('position', new THREE.Float32BufferAttribute(gridPosition, 3)); 68 | gridInstancedGeometry.setAttribute('offset', new THREE.InstancedBufferAttribute(new Float32Array(offsets), 3)); 69 | 70 | if (polka) { 71 | gridMaterial.uniforms.polkaTexture = { value: new THREE.TextureLoader().load('https://threejs.org/examples/textures/sprites/disc.png') }; 72 | const grid = new THREE.Points(gridInstancedGeometry, gridMaterial); 73 | grid.frustumCulled = false; 74 | return grid; 75 | } 76 | else { 77 | const grid = new THREE.Line(gridInstancedGeometry, gridMaterial); 78 | grid.frustumCulled = false; 79 | return grid; 80 | } 81 | } 82 | } 83 | 84 | 85 | 86 | 87 | function fragmentShaderFun() { 88 | return ` 89 | precision highp float; 90 | uniform vec3 lineColor; 91 | 92 | float near = 0.1; 93 | uniform float visibleArea; 94 | 95 | varying vec3 vPosition; 96 | uniform vec3 cameraPosition; 97 | 98 | uniform bool polka; 99 | uniform sampler2D polkaTexture; 100 | 101 | void main() { 102 | float dist = distance(vPosition, cameraPosition); 103 | float alpha = 1.0 - smoothstep(near, visibleArea, dist); 104 | 105 | if (polka) { 106 | gl_FragColor = vec4( lineColor, alpha ); 107 | gl_FragColor = gl_FragColor * texture2D( polkaTexture, gl_PointCoord ); 108 | } 109 | else { 110 | gl_FragColor = vec4(lineColor, alpha); 111 | } 112 | } 113 | `; 114 | } 115 | 116 | function vertexShaderFunc() { 117 | return ` 118 | precision highp float; 119 | attribute vec3 offset; 120 | attribute vec3 position; 121 | 122 | varying vec3 vPosition; 123 | 124 | uniform mat4 modelViewMatrix; 125 | uniform mat4 projectionMatrix; 126 | uniform vec3 cameraPosition; 127 | 128 | uniform float visibleArea; 129 | uniform vec3 lineColor; 130 | 131 | vec3 worldPosition; 132 | uniform bool polka; 133 | 134 | void main() { 135 | worldPosition = position + offset; 136 | worldPosition.${vAxes} += cameraPosition.${vAxes} - mod(cameraPosition.${vAxes}, 1.0); 137 | vPosition = worldPosition; 138 | 139 | if (polka) { 140 | gl_PointSize = 2.0; 141 | } 142 | gl_Position = projectionMatrix * modelViewMatrix * vec4(vPosition, 1.0); 143 | } 144 | `; 145 | } 146 | 147 | const Shader = { 148 | name: 'OpenGridHelper', 149 | uniforms: { 150 | 'lineColor': { value: new THREE.Vector3(0, 0, 0) }, 151 | 'visibleArea': { value: 25 }, 152 | 'polka': { value: false } 153 | } 154 | } 155 | 156 | export { Grid, Shader } -------------------------------------------------------------------------------- /src/helpers/OpenOutliner.ts: -------------------------------------------------------------------------------- 1 | // https://threejs.org/docs/#api/en/renderers/webgl/WebGLProgram 2 | 3 | import * as THREE from 'three'; 4 | 5 | function vertexShader () { 6 | return ` 7 | precision highp float; 8 | 9 | attribute vec3 position; 10 | attribute vec3 color; 11 | 12 | uniform mat4 modelViewMatrix; 13 | uniform mat4 projectionMatrix; 14 | 15 | varying vec3 vColor; 16 | 17 | void main() { 18 | vColor = color; 19 | gl_Position = projectionMatrix * modelViewMatrix * vec4(position, 1.0); 20 | } 21 | `; 22 | } 23 | 24 | function fragmentShader () { 25 | return ` 26 | precision highp float; 27 | varying vec3 vColor; 28 | 29 | void main() { 30 | float threshold = 0.1; // Sensitivity threshold 31 | 32 | // Approximate the edge detection by checking the color differences 33 | // In a real scenario, you would sample neighboring pixels 34 | // Here, we'll just use a fake neighboring color to illustrate: 35 | 36 | vec3 neighborColor = vec3(0.0, 0.0, 0.0); // Replace with actual neighboring color if possible 37 | 38 | // Calculate color difference 39 | float edgeStrength = length(vColor - neighborColor); 40 | 41 | // Output white for edges, black otherwise 42 | if (edgeStrength > threshold) { 43 | gl_FragColor = vec4(1.0, 1.0, 1.0, 1.0); // White for edges 44 | } else { 45 | gl_FragColor = vec4(0.0, 0.0, 0.0, 1.0); // Black otherwise 46 | } 47 | } 48 | `; 49 | } 50 | 51 | 52 | 53 | 54 | const shader = { 55 | name: 'OpenOutliner', 56 | uniforms: { 57 | thickness: { value: 0.1 }, 58 | color: { value: new THREE.Color(0x000000) }, 59 | screenResolution: { value: new THREE.Vector2(window.innerWidth, window.innerHeight) }, 60 | } 61 | } 62 | 63 | export { 64 | vertexShader, 65 | fragmentShader, 66 | shader 67 | }; -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import { OpenGeometry } from '../kernel/dist'; 2 | import { Pencil, PencilMode } from '../kernel/dist/src/pencil'; 3 | import { BaseSpace } from './elements/base-spaces'; 4 | 5 | import { DoubleWindow } from './elements/double-window'; 6 | import { GlyphNode, Glyphs } from '@opengeometry/openglyph'; 7 | import { BuildingData } from './parser/IGraph'; 8 | import convertToOGFormat from './parser/ImpleniaConverter'; 9 | import { PlanCamera } from './service/plancamera'; 10 | import { OpenThree } from './service/three'; 11 | import * as THREE from 'three'; 12 | import { Event } from './utils/event'; 13 | 14 | // Shape Builders 15 | import { IPolylineBuilder, PolylineBuilder } from './shape-builder/polyline-builder'; 16 | import { IPolygonBuilder, PolygonBuilder } from './shape-builder/polygon-builder'; 17 | 18 | // Elements 19 | import { PaperFrame } from './drawing'; 20 | import { LogoInfoBlock, LogoInfoBlockOptions } from './drawing/logo-info-block'; 21 | import { RowInfoBlock, RowInfoBlockOptions } from './drawing/row-info-block'; 22 | import { Board, OPBoard } from './elements/board'; 23 | import { BaseWall } from './elements/base-wall'; 24 | import { IBaseWall } from './base-type'; 25 | import { OPDoor, BaseDoor } from './elements/base-door'; 26 | import { OPWindow, BaseWindow } from './elements/base-window'; 27 | 28 | export class OpenPlans { 29 | private container: HTMLElement 30 | private openThree: OpenThree 31 | static sOThree: OpenThree; 32 | 33 | private pencil: Pencil | undefined 34 | private planCamera: PlanCamera 35 | 36 | private og: OpenGeometry | undefined 37 | private ogElements: any[] = [] 38 | 39 | private onRender: Event = new Event(); 40 | 41 | constructor(container: HTMLElement) { 42 | this.callback = this.callback.bind(this) 43 | 44 | this.container = container 45 | this.openThree = new OpenThree(container, this.callback) 46 | OpenPlans.sOThree = this.openThree; 47 | 48 | this.planCamera = this.openThree.planCamera 49 | 50 | this.openThree.planCamera.controls.addEventListener("update", () => { 51 | Glyphs.updateManager(this.openThree.threeCamera) 52 | }) 53 | } 54 | 55 | callback() { 56 | if (this.og?.labelRenderer) { 57 | this.og.update(this.openThree.scene, this.openThree.threeCamera) 58 | } 59 | 60 | for (const element of this.ogElements) { 61 | if ( 62 | element.ogType === 'polyline' || 63 | element.ogType === 'polygon' || 64 | element.ogType === 'baseWall' || 65 | element.ogType === 'baseDoor' || 66 | element.ogType === 'baseWindow' 67 | ) { 68 | element.calulateAnchorEdges(true); 69 | } 70 | } 71 | 72 | this.onRender.trigger(); 73 | } 74 | 75 | async setupOpenGeometry(wasmURL: string) { 76 | this.og = new OpenGeometry(this.container, this.openThree.scene, this.openThree.threeCamera) 77 | await this.og.setup(wasmURL) 78 | this.pencil = this.og.pencil 79 | 80 | await Glyphs.loadFaces('Source_Code_Pro_Regular'); 81 | Glyphs.scene = this.openThree.scene 82 | Glyphs.camera = this.openThree.threeCamera 83 | 84 | this.pencil?.onCursorDown.add((coords) => { 85 | console.log('Cursor Down', coords) 86 | }); 87 | } 88 | 89 | disposeElement(ogid: string) { 90 | const element = this.ogElements.find((el) => el.ogid === ogid); 91 | if (element) { 92 | element.dispose(); 93 | this.openThree.scene.remove(element); 94 | this.ogElements.splice(this.ogElements.indexOf(element), 1); 95 | } else { 96 | console.warn(`Element with ogid ${ogid} not found`); 97 | } 98 | } 99 | 100 | baseWall(config: IBaseWall): BaseWall { 101 | if (!this.pencil) { 102 | throw new Error('Pencil not initialized') 103 | } 104 | const wall = new BaseWall(config); 105 | wall.pencil = this.pencil; 106 | this.openThree.scene.add(wall) 107 | this.ogElements.push(wall) 108 | return wall 109 | } 110 | 111 | baseSingleWindow(config: OPWindow): BaseWindow { 112 | if (!this.pencil) { 113 | throw new Error('Pencil not initialized') 114 | } 115 | const window = new BaseWindow(config); 116 | window.pencil = this.pencil; 117 | this.openThree.scene.add(window) 118 | this.ogElements.push(window) 119 | return window 120 | } 121 | 122 | baseDoor(config: OPDoor): BaseDoor { 123 | if (!this.pencil) { 124 | throw new Error('Pencil not initialized') 125 | } 126 | const door = new BaseDoor(config); 127 | door.pencil = this.pencil; 128 | this.openThree.scene.add(door) 129 | this.ogElements.push(door) 130 | return door 131 | } 132 | 133 | space(): BaseSpace { 134 | if (!this.pencil) { 135 | throw new Error('Pencil not initialized') 136 | } 137 | const space = new BaseSpace(this.pencil) 138 | this.openThree.scene.add(space) 139 | this.ogElements.push(space) 140 | return space 141 | } 142 | 143 | doubleWindow(): DoubleWindow { 144 | if (!this.pencil) { 145 | throw new Error('Pencil not initialized') 146 | } 147 | const window = new DoubleWindow(this.pencil) 148 | this.openThree.scene.add(window) 149 | this.ogElements.push(window) 150 | return window 151 | } 152 | 153 | board(boardConfig?:OPBoard): Board { 154 | if (!this.pencil) { 155 | throw new Error('Pencil not initialized') 156 | } 157 | const board = new Board(boardConfig) 158 | this.openThree.scene.add(board) 159 | this.ogElements.push(board) 160 | return board 161 | } 162 | 163 | /***** Shape Builders *****/ 164 | 165 | /** 166 | * Create Polyline using Interactive Builder 167 | * @param polyLineConfig 168 | * @returns 169 | */ 170 | polylineBuilder(polyLineConfig?: IPolylineBuilder): PolylineBuilder { 171 | if (!this.pencil) { 172 | throw new Error('Pencil not initialized') 173 | } 174 | const polylineBuilder = new PolylineBuilder(polyLineConfig) 175 | polylineBuilder.pencil = this.pencil; 176 | this.openThree.scene.add(polylineBuilder) 177 | this.ogElements.push(polylineBuilder) 178 | return polylineBuilder 179 | } 180 | 181 | polygonBuilder(polygonConfig?: IPolygonBuilder): PolygonBuilder { 182 | if (!this.pencil) { 183 | throw new Error('Pencil not initialized') 184 | } 185 | const polygonBuilder = new PolygonBuilder(polygonConfig) 186 | polygonBuilder.pencil = this.pencil; 187 | this.openThree.scene.add(polygonBuilder) 188 | this.ogElements.push(polygonBuilder) 189 | return polygonBuilder 190 | } 191 | 192 | /***** Utilities *****/ 193 | 194 | getEntitiesByType(type: string) { 195 | return this.ogElements.filter((el) => el.ogType === type) 196 | } 197 | 198 | fit(element: string) { 199 | if (!element) return 200 | const entities = this.getEntitiesByType(element) 201 | if (entities.length === 0) return 202 | this.planCamera.fitToElement(entities) 203 | } 204 | 205 | glyph(text: string, size: number, color: number, staticZoom: boolean = true) { 206 | const glyph = Glyphs.addGlyph(text, size, color, staticZoom) 207 | return glyph 208 | } 209 | 210 | getGlyph(id: string): GlyphNode { 211 | const glyph = Glyphs.getGlyph(id) 212 | if (!glyph) throw new Error('Glyph not found') 213 | return glyph 214 | } 215 | 216 | selectGlyph(id: string) { 217 | if (!id) throw new Error('ID not provided') 218 | Glyphs.selectGlyph(id) 219 | } 220 | 221 | rotateGlyph(id: string, angle: number) { 222 | if (!id) throw new Error('ID not provided') 223 | Glyphs.rotateGlyph(id, angle) 224 | } 225 | 226 | get glyphNodes() { 227 | return Glyphs.glyphNodes 228 | } 229 | 230 | clearGlyphSelection() { 231 | Glyphs.clearSelection() 232 | } 233 | 234 | updateGlyphText(id: string, text: string) { 235 | Glyphs.updateGlyphText(id, text) 236 | } 237 | 238 | convertImpleniaToOGFormat(sourceJson: any) { 239 | const ogJSON = convertToOGFormat(sourceJson); 240 | // this.generateGeometry(ogJSON); 241 | } 242 | 243 | public startEditingSpaces() { 244 | const spaces = this.getEntitiesByType('space'); 245 | for (let i = 0; i < spaces.length; i++) { 246 | // change material to white 247 | const baseMaterial = new THREE.MeshBasicMaterial({ color: 0xffffff, side: THREE.DoubleSide, transparent: true, opacity: 0.5 }); 248 | spaces[i].material = baseMaterial; 249 | 250 | spaces[i].isEditing = true; 251 | } 252 | } 253 | 254 | public stopEditingSpaces() { 255 | const spaces = this.getEntitiesByType('space'); 256 | for (let i = 0; i < spaces.length; i++) { 257 | spaces[i].isEditing = false; 258 | spaces[i].material = new THREE.MeshBasicMaterial({ color: spaces[i].spaceSet.color, side: THREE.DoubleSide, transparent: true, opacity: 0.5 }); 259 | } 260 | } 261 | 262 | public fitToSpace(spaceId: string) { 263 | const space = this.getEntitiesByType('space').find((s) => s.name === spaceId); 264 | if (!space) return; 265 | this.planCamera.fitToElement([space]); 266 | } 267 | 268 | public fitToAllSpaces() { 269 | const spaces = this.getEntitiesByType('space'); 270 | this.planCamera.fitToElement(spaces); 271 | } 272 | 273 | public getSpaceData(spaceId: string) { 274 | const space = this.getEntitiesByType('space').find((s) => s.name === spaceId); 275 | if (!space) return; 276 | return space.spaceSet; 277 | } 278 | 279 | public getSpaceArea(spaceId: string) { 280 | const space = this.getEntitiesByType('space').find((s) => s.name === spaceId); 281 | if (!space) return; 282 | const spaceArea = space.area; 283 | return spaceArea; 284 | } 285 | 286 | public getElementArea(elementId: string) { 287 | const element = this.ogElements.find((el) => el.id === elementId); 288 | if (!element) return; 289 | const elementArea = element.area; 290 | return elementArea; 291 | } 292 | 293 | /** 294 | * Paper Creation and Frames 295 | */ 296 | paperFrame() { 297 | // if (!this.pencil) { 298 | // throw new Error('Pencil not initialized') 299 | // } 300 | const paperFrame = new PaperFrame() 301 | this.openThree.scene.add(paperFrame) 302 | this.ogElements.push(paperFrame) 303 | return paperFrame 304 | } 305 | 306 | logoInfoBlock(options:LogoInfoBlockOptions) { 307 | const logoBlock = new LogoInfoBlock(options); 308 | this.openThree.scene.add(logoBlock); 309 | return logoBlock 310 | } 311 | 312 | rowInfoBlock(options: RowInfoBlockOptions) { 313 | const rowInfoBlock = new RowInfoBlock(options); 314 | this.openThree.scene.add(rowInfoBlock); 315 | return rowInfoBlock; 316 | } 317 | 318 | static toScreenPosition(pos: THREE.Vector3): { x: number; y: number } { 319 | const vector = pos.clone().project(OpenPlans.sOThree.threeCamera); 320 | 321 | const halfWidth = OpenPlans.sOThree.renderer.domElement.clientWidth / 2; 322 | const halfHeight = OpenPlans.sOThree.renderer.domElement.clientHeight / 2; 323 | 324 | return { 325 | x: vector.x * halfWidth + halfWidth, 326 | y: -vector.y * halfHeight + halfHeight 327 | }; 328 | } 329 | } 330 | -------------------------------------------------------------------------------- /src/parser/IGraph.ts: -------------------------------------------------------------------------------- 1 | type Coordinates = [number, number, number]; 2 | 3 | type OGType = 'OG_FLOOR' | 'OG_ROOM' | 'OG_WALL' | 'OG_WINDOW' | 'OG_DOOR'; 4 | 5 | interface BaseOGObject { 6 | OG_ID: string; 7 | OG_TYPE: OGType; 8 | } 9 | 10 | interface Floor extends BaseOGObject { 11 | OG_TYPE: 'OG_FLOOR'; 12 | OG_DATA: string[]; // Array of room IDs 13 | } 14 | 15 | // New interface for room components 16 | interface RoomComponent extends BaseOGObject { 17 | OG_ID: string; 18 | OG_TYPE: 'OG_WALL' | 'OG_WINDOW' | 'OG_DOOR'; 19 | } 20 | 21 | interface Room extends BaseOGObject { 22 | OG_TYPE: 'OG_ROOM'; 23 | type: 'Bedroom' | 'Living Room' | 'Kitchen'; 24 | OG_DATA: RoomComponent[]; // Array of wall, window, and door objects 25 | coordinates: [Coordinates, Coordinates, Coordinates, Coordinates]; 26 | connections: string[]; // Array of connected room IDs 27 | USER_DATA: string; 28 | } 29 | 30 | interface Wall extends BaseOGObject { 31 | OG_TYPE: 'OG_WALL'; 32 | USER_DATA: string; 33 | type: 'internal'; 34 | thickness: number; 35 | start: Coordinates; 36 | end: Coordinates; 37 | } 38 | 39 | interface Window extends BaseOGObject { 40 | OG_TYPE: 'OG_WINDOW'; 41 | type: 'internal'; 42 | thickness: number; 43 | start: Coordinates; 44 | end: Coordinates; 45 | } 46 | 47 | interface Door extends BaseOGObject { 48 | OG_TYPE: 'OG_DOOR'; 49 | type: 'internal'; 50 | thickness: number; 51 | hingeThickness: number; 52 | start: Coordinates; 53 | end: Coordinates; 54 | } 55 | 56 | interface BuildingData { 57 | building_id: string; 58 | building_name: string; 59 | floors: Floor[]; 60 | rooms: Room[]; 61 | walls: Wall[]; 62 | windows: Window[]; 63 | doors: Door[]; 64 | } 65 | 66 | export type { 67 | BuildingData, 68 | Floor, 69 | Room, 70 | Wall, 71 | Window, 72 | Door, 73 | RoomComponent, 74 | Coordinates, 75 | OGType, 76 | BaseOGObject 77 | }; -------------------------------------------------------------------------------- /src/parser/ImpleniaConverter.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * As for today, I'm creating a reader in the format shared by Implenai. 3 | * But I need to identify the format of the graph/JSON that I will be using. Maybe GraphML. 4 | * If there is a standard format, I will use it. If not, better I will create one. 5 | */ 6 | 7 | import type { 8 | BuildingData, 9 | Floor, 10 | Room, 11 | Wall, 12 | Window, 13 | Door, 14 | RoomComponent, 15 | Coordinates 16 | } from './IGraph'; 17 | 18 | // Types for the source JSON 19 | interface SourcePanel { 20 | panel_type: string; 21 | start_point: number[]; 22 | end_point: number[]; 23 | height: number; 24 | thickness: number; 25 | room?: string; 26 | apartment?: string; 27 | } 28 | 29 | interface SourceSpace { 30 | room_type: string; 31 | apartment?: string; 32 | coordinates: { 33 | x: number; 34 | y: number; 35 | z: number; 36 | }[]; 37 | } 38 | 39 | interface SourceJSON { 40 | panels: { 41 | attributes: Record; 42 | items: Record; 43 | max_key: number; 44 | }; 45 | spaces: Record; 46 | } 47 | 48 | // Helper function to convert coordinates 49 | function convertCoordinates(point: number[]): Coordinates { 50 | return [point[0], point[2], point[1]]; 51 | } 52 | 53 | // Helper function to check if two sets of coordinates share points 54 | function doComponentsShare(start1: Coordinates, end1: Coordinates, start2: Coordinates, end2: Coordinates): boolean { 55 | const pointsMatch = (p1: Coordinates, p2: Coordinates) => 56 | Math.abs(p1[0] - p2[0]) < 0.01 && 57 | Math.abs(p1[1] - p2[1]) < 0.01 && 58 | Math.abs(p1[2] - p2[2]) < 0.01; 59 | 60 | return pointsMatch(start1, start2) || 61 | pointsMatch(start1, end2) || 62 | pointsMatch(end1, start2) || 63 | pointsMatch(end1, end2); 64 | } 65 | 66 | function convertToOGFormat(sourceJson: SourceJSON): BuildingData { 67 | const processedElements = new Set(); 68 | const rooms: Room[] = []; 69 | const walls: Wall[] = []; 70 | const windows: Window[] = []; 71 | const doors: Door[] = []; 72 | const componentsByRoom = new Map>(); 73 | 74 | // First pass: Create all components (walls, windows, doors) 75 | Object.entries(sourceJson.panels.items).forEach(([panelId, panel]) => { 76 | const id = (parseInt(panelId) + 1).toString().padStart(3, '0'); 77 | const start = convertCoordinates(panel.start_point); 78 | const end = convertCoordinates(panel.end_point); 79 | 80 | // Determine component type based on panel_type 81 | if (panel.panel_type.includes('WINDOW')) { 82 | const window: Window = { 83 | OG_ID: `WINDOW_${id}`, 84 | OG_TYPE: 'OG_WINDOW', 85 | type: 'internal', 86 | thickness: panel.thickness, 87 | start, 88 | end 89 | }; 90 | windows.push(window); 91 | 92 | if (panel.room) { 93 | if (!componentsByRoom.has(panel.room)) { 94 | componentsByRoom.set(panel.room, new Set()); 95 | } 96 | componentsByRoom.get(panel.room)!.add(window.OG_ID); 97 | } 98 | } else if (panel.panel_type.includes('DOOR')) { 99 | const door: Door = { 100 | OG_ID: `DOOR_${id}`, 101 | OG_TYPE: 'OG_DOOR', 102 | type: 'internal', 103 | thickness: panel.thickness, 104 | hingeThickness: panel.thickness * 0.5, // Assuming hinge thickness is half of door thickness 105 | start, 106 | end 107 | }; 108 | doors.push(door); 109 | 110 | if (panel.room) { 111 | if (!componentsByRoom.has(panel.room)) { 112 | componentsByRoom.set(panel.room, new Set()); 113 | } 114 | componentsByRoom.get(panel.room)!.add(door.OG_ID); 115 | } 116 | } else { 117 | const wall: Wall = { 118 | OG_ID: `WALL_${id}`, 119 | OG_TYPE: 'OG_WALL', 120 | USER_DATA: JSON.stringify(panel.panel_type), 121 | type: 'internal', 122 | thickness: panel.thickness, 123 | start, 124 | end 125 | }; 126 | walls.push(wall); 127 | 128 | if (panel.room) { 129 | if (!componentsByRoom.has(panel.room)) { 130 | componentsByRoom.set(panel.room, new Set()); 131 | } 132 | componentsByRoom.get(panel.room)!.add(wall.OG_ID); 133 | } 134 | } 135 | }); 136 | 137 | // Second pass: Create rooms 138 | Object.entries(sourceJson.spaces).forEach(([spaceId, space]) => { 139 | const roomId = `ROOM_${(parseInt(spaceId) + 1).toString().padStart(3, '0')}`; 140 | 141 | // Convert coordinates 142 | const spaceCoords = space.coordinates.map(coord => 143 | [coord.x, coord.z, coord.y] as Coordinates 144 | ); 145 | 146 | // Ensure we have exactly 4 coordinates 147 | // while (spaceCoords.length < 4) { 148 | spaceCoords.push([...spaceCoords[spaceCoords.length - 1]]); 149 | // } 150 | // if (spaceCoords.length > 4) { 151 | // spaceCoords.length = 4; 152 | // } 153 | 154 | // Get components for this room 155 | const roomComponents: RoomComponent[] = []; 156 | const roomComponentIds = componentsByRoom.get(space.room_type) || new Set(); 157 | 158 | roomComponentIds.forEach(componentId => { 159 | if (componentId.startsWith('WALL_')) { 160 | if (processedElements.has(componentId)) return; 161 | 162 | const wall = walls.find(w => w.OG_ID === componentId); 163 | if (wall) roomComponents.push({ OG_ID: wall.OG_ID, OG_TYPE: 'OG_WALL' }); 164 | 165 | processedElements.add(componentId); 166 | } else if (componentId.startsWith('WINDOW_')) { 167 | const window = windows.find(w => w.OG_ID === componentId); 168 | if (window) roomComponents.push({ OG_ID: window.OG_ID, OG_TYPE: 'OG_WINDOW' }); 169 | } else if (componentId.startsWith('DOOR_')) { 170 | const door = doors.find(d => d.OG_ID === componentId); 171 | if (door) roomComponents.push({ OG_ID: door.OG_ID, OG_TYPE: 'OG_DOOR' }); 172 | } 173 | }); 174 | 175 | // Create room 176 | const room: Room = { 177 | OG_ID: roomId, 178 | OG_TYPE: 'OG_ROOM', 179 | type: space.room_type.charAt(0).toUpperCase() + space.room_type.slice(1) as Room['type'], 180 | OG_DATA: roomComponents, 181 | coordinates: spaceCoords as [Coordinates, Coordinates, Coordinates, Coordinates], 182 | connections: [], 183 | USER_DATA: JSON.stringify(space.room_type) 184 | }; 185 | 186 | rooms.push(room); 187 | }); 188 | 189 | // Final pass: Create room connections 190 | const processedConnections = new Set(); 191 | 192 | rooms.forEach((room1) => { 193 | rooms.forEach((room2) => { 194 | if (room1.OG_ID === room2.OG_ID) return; 195 | 196 | const connectionKey = [room1.OG_ID, room2.OG_ID].sort().join('-'); 197 | if (processedConnections.has(connectionKey)) return; 198 | 199 | // Check if rooms share any components 200 | const hasSharedComponent = room1.OG_DATA.some(comp1 => { 201 | const component1 = [...walls, ...windows, ...doors].find(c => c.OG_ID === comp1.OG_ID); 202 | if (!component1) return false; 203 | 204 | return room2.OG_DATA.some(comp2 => { 205 | const component2 = [...walls, ...windows, ...doors].find(c => c.OG_ID === comp2.OG_ID); 206 | if (!component2) return false; 207 | 208 | return doComponentsShare(component1.start, component1.end, component2.start, component2.end); 209 | }); 210 | }); 211 | 212 | if (hasSharedComponent) { 213 | room1.connections.push(room2.OG_ID); 214 | room2.connections.push(room1.OG_ID); 215 | } 216 | 217 | processedConnections.add(connectionKey); 218 | }); 219 | }); 220 | 221 | // Create floor 222 | const floor: Floor = { 223 | OG_ID: "FLOOR_001", 224 | OG_TYPE: "OG_FLOOR", 225 | OG_DATA: rooms.map(room => room.OG_ID) 226 | }; 227 | 228 | return { 229 | building_id: "BLDG_001", 230 | building_name: "Apartment Building 1", 231 | floors: [floor], 232 | rooms, 233 | walls, 234 | windows, 235 | doors 236 | }; 237 | } 238 | 239 | export default convertToOGFormat; -------------------------------------------------------------------------------- /src/service/plancamera.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import CameraControls from 'camera-controls' 3 | 4 | export class PlanCamera { 5 | private camera: THREE.PerspectiveCamera | THREE.OrthographicCamera 6 | controls: CameraControls 7 | clock: THREE.Clock = new THREE.Clock() 8 | 9 | constructor(camera: THREE.PerspectiveCamera | THREE.OrthographicCamera, container: HTMLElement) { 10 | this.camera = camera; 11 | // camera from top 12 | this.camera.position.set(0, 20, 0); 13 | // Container Events are not being sent to the shadow dom 14 | this.controls = new CameraControls(camera, container); 15 | this.controls.mouseButtons.left = CameraControls.ACTION.NONE; 16 | this.controls.touches.one = CameraControls.ACTION.NONE; 17 | this.controls.dollyToCursor = true; 18 | this.controls.minDistance = 1.5; 19 | 20 | this.setupCamera(); 21 | } 22 | 23 | setupCamera() { 24 | // this.controls.moveTo( 3, 5, 2, true ) 25 | } 26 | 27 | orthoCamera() { 28 | 29 | } 30 | 31 | perspectiveCamera() { 32 | 33 | } 34 | 35 | isometricCamera() { 36 | 37 | } 38 | 39 | fitToElement(meshes: THREE.Mesh[]) { 40 | const box = new THREE.Box3(); 41 | for (const mesh of meshes) { 42 | box.expandByObject(mesh); 43 | } 44 | box.expandByScalar(2); 45 | this.controls.fitToSphere(box.getBoundingSphere(new THREE.Sphere()), true); 46 | } 47 | 48 | update() { 49 | const delta = this.clock.getDelta(); 50 | this.controls.update(delta); 51 | } 52 | } 53 | 54 | -------------------------------------------------------------------------------- /src/service/plangrid.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Infinity Grid Creator 3 | */ 4 | import * as THREE from 'three' 5 | import { activeTheme, ICanvasTheme } from '../base-type' 6 | 7 | export class PlanGrid { 8 | private grid: THREE.GridHelper 9 | private scene: THREE.Scene 10 | private theme: ICanvasTheme 11 | 12 | constructor(scene: THREE.Scene, theme: ICanvasTheme, activeTheme: activeTheme) { 13 | this.scene = scene 14 | this.theme = theme 15 | this.grid = new THREE.GridHelper(100, 100, theme[activeTheme].color, theme[activeTheme].color) 16 | this.scene.add(this.grid) 17 | } 18 | 19 | toggleGrid() { 20 | this.grid.visible = !this.grid.visible 21 | } 22 | 23 | applyTheme(activeTheme: activeTheme) { 24 | const lineMaterial = new THREE.LineBasicMaterial({ color: this.theme[activeTheme].color }) 25 | this.grid.material = lineMaterial 26 | this.grid.material.color.set(this.theme[activeTheme].color) 27 | } 28 | } -------------------------------------------------------------------------------- /src/service/selector.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/src/service/selector.ts -------------------------------------------------------------------------------- /src/service/three.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three' 2 | import { CSS2DRenderer } from 'three/examples/jsm/renderers/CSS2DRenderer.js' 3 | import { PlanCamera } from './plancamera' 4 | import CameraControls from 'camera-controls' 5 | import { activeTheme, ICanvasTheme } from '../base-type' 6 | import * as OpenGrid from '../helpers/OpenGridHelper.ts' 7 | 8 | export class OpenThree { 9 | scene: THREE.Scene 10 | renderer: THREE.WebGLRenderer 11 | labelRenderer: CSS2DRenderer | undefined 12 | planCamera: PlanCamera 13 | threeCamera: THREE.PerspectiveCamera 14 | container: HTMLElement 15 | theme!: ICanvasTheme 16 | activeTheme: activeTheme = 'light' 17 | // planGrid: PlanGrid 18 | 19 | constructor(container: HTMLElement, private callback: any) { 20 | CameraControls.install({THREE: THREE}) 21 | this.generateTheme() 22 | 23 | this.container = container 24 | this.scene = new THREE.Scene() 25 | this.renderer = new THREE.WebGLRenderer({ 26 | antialias: true, 27 | logarithmicDepthBuffer: true 28 | }) 29 | 30 | this.threeCamera = new THREE.PerspectiveCamera(75, this.container.clientWidth / this.container.clientHeight, 1, 100) 31 | this.planCamera = new PlanCamera(this.threeCamera, container) 32 | 33 | this.setup() 34 | } 35 | 36 | // accept a theme with type 37 | generateTheme() { 38 | this.theme = { 39 | darkBlue: { 40 | background: '#003ca0', 41 | color: '#fff', 42 | gridColor: 0xffffff 43 | }, 44 | light: { 45 | background: '#ebdbcc', 46 | color: '#003ca0', 47 | gridColor: 0x003ca0 48 | }, 49 | dark: { 50 | background: '#242b2f', 51 | color: '#fff', 52 | gridColor: 0xffffff 53 | } 54 | } 55 | } 56 | 57 | toggleTheme(name: activeTheme) { 58 | if (!this.theme[name]) { 59 | return 60 | } 61 | this.activeTheme = name 62 | this.scene.background = new THREE.Color(this.theme[this.activeTheme].background) 63 | // this.planGrid.applyTheme(this.activeTheme) 64 | const gridColor = this.hexToRgb(this.theme[this.activeTheme].gridColor) 65 | OpenGrid.Shader.uniforms.lineColor.value = gridColor 66 | } 67 | 68 | async setup() { 69 | // this.scene.background = new THREE.Color(0xff00ff) 70 | this.renderer.setSize(this.container.clientWidth, this.container.clientHeight) 71 | this.renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2)) 72 | this.container.appendChild(this.renderer.domElement) 73 | 74 | const ambientLight = new THREE.AmbientLight(0xffffff, 0.5) 75 | this.scene.add(ambientLight) 76 | 77 | const directionalLight = new THREE.DirectionalLight(0xffffff, 0.5) 78 | this.scene.add(directionalLight) 79 | 80 | this.scene.background = new THREE.Color(this.theme[this.activeTheme].background) 81 | 82 | window.addEventListener('resize', () => { 83 | this.renderer.setSize(this.container.clientWidth, this.container.clientHeight) 84 | this.threeCamera.aspect = this.container.clientWidth / this.container.clientHeight 85 | this.threeCamera.updateProjectionMatrix() 86 | }) 87 | this.animate() 88 | 89 | // Utils like Grid, Lights and Etc 90 | const gridColor = this.hexToRgb(this.theme[this.activeTheme].gridColor) 91 | // const openGrid = new OpenGrid.Grid("xzy", gridColor, 50, 25, true) 92 | 93 | const openGrid = new THREE.GridHelper(100, 100); 94 | // @ts-ignore 95 | this.scene.add(openGrid) 96 | } 97 | 98 | hexToRgb(hex: number) { 99 | const color = new THREE.Color(hex) 100 | return new THREE.Vector3(color.r, color.g, color.b) 101 | } 102 | 103 | animate() { 104 | this.renderer.render(this.scene, this.threeCamera) 105 | this.planCamera.update() 106 | // console.log('OpenThree animate') 107 | this.callback() 108 | 109 | requestAnimationFrame(() => this.animate()) 110 | } 111 | 112 | // addGUI() { 113 | // const gui = new GUI(); 114 | // const wallFolder = gui.addFolder('Wall'); 115 | // const wallControls = { 116 | // 'thickness': 0.25, 117 | // 'color': '#00ff00', 118 | // }; 119 | // const walls = this.getEntitiesByType('wall'); 120 | // console.log(walls); 121 | // walls.forEach((wall: BaseWall) => { 122 | // const subWall = wallFolder.addFolder(wall.name); 123 | // subWall.add(wallControls, 'thickness', 0.1, 1).name('Thickness').onChange((value) => { 124 | // wall.halfThickness = value / 2; 125 | // }); 126 | // }); 127 | 128 | // const doorFolder = gui.addFolder('Door'); 129 | // const doorControls = { 130 | // 'rotation': 1, 131 | // 'quadrant': 1, 132 | // }; 133 | // const doors = this.getEntitiesByType('door'); 134 | // doors.forEach((door: BaseDoor) => { 135 | // const subDoor = doorFolder.addFolder(door.name); 136 | // subDoor.add(doorControls, 'rotation', 1, 2).name('Rotation').onChange((value) => { 137 | // door.doorRotation = value; 138 | // }); 139 | // subDoor.add(doorControls, 'quadrant', [1, 2, 3, 4]).name('Quadrant').onChange((value) => { 140 | // door.doorQudrant = value; 141 | // }); 142 | // }); 143 | 144 | // const pencil = gui.addFolder('Pencil'); 145 | // const pencilControls = { 146 | // 'mode': "cursor", 147 | // }; 148 | // pencil.add(pencilControls, 'mode', ["select", "cursor"]).name('Mode').onChange((value) => { 149 | // if (!this.openGeometry?.pencil) return; 150 | // this.openGeometry.pencil.mode = value as PencilMode; 151 | // }); 152 | // } 153 | } -------------------------------------------------------------------------------- /src/shape-builder/circle-builder.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/src/shape-builder/circle-builder.ts -------------------------------------------------------------------------------- /src/shape-builder/shape-builder.md: -------------------------------------------------------------------------------- 1 | ShapeBuilder is a combination of Shape/Mesh and Geometry Data along with Editing Capabilities. 2 | Rule - A ShapeBuilder should always have a Editing Capability. 3 | 4 | # ShapeBuilder 5 | 6 | - PolylineBuilder in XZ plane 7 | - PolygonBuilder in XZ plane 8 | - RectangleBuilder in XZ plane 9 | - CircleBuilder in XZ plane 10 | - PolyHedronBuilder (This is a 3D ShapeBuilder)(Won't be implemented here, just reference) -------------------------------------------------------------------------------- /src/shape/circle-shape.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/OpenGeometry-io/OpenPlans/af7d0a4693bc1e1ed62b704aa3fe33cb07fb7bb3/src/shape/circle-shape.ts -------------------------------------------------------------------------------- /src/shape/polygon-shape.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { Polygon } from '../../kernel/dist'; 3 | 4 | /** 5 | * If any element start moves, cast a ray and check if it interesects with the board. 6 | * Add the element to the board if it does. 7 | * If the element is moved outside the board, remove it from the board. 8 | */ 9 | 10 | export abstract class PolygonShape extends Polygon{ 11 | abstract ogType: string; 12 | abstract subNodes: Map; 13 | abstract _selected: boolean; 14 | 15 | abstract propertySet: Record; 16 | 17 | abstract setOPConfig(config: Record): void; 18 | abstract getOPConfig(): Record; 19 | 20 | abstract setOPGeometry() : void; 21 | abstract setOPMaterial() : void; 22 | } -------------------------------------------------------------------------------- /src/shape/polyline-shape.ts: -------------------------------------------------------------------------------- 1 | import * as THREE from 'three'; 2 | import { PolyLine } from '../../kernel/dist'; 3 | 4 | /** 5 | * If any element start moves, cast a ray and check if it interesects with the board. 6 | * Add the element to the board if it does. 7 | * If the element is moved outside the board, remove it from the board. 8 | */ 9 | 10 | export abstract class PolyLineShape extends PolyLine{ 11 | abstract ogType: string; 12 | abstract subNodes: Map; 13 | abstract _selected: boolean; 14 | 15 | abstract propertySet: Record; 16 | 17 | abstract setOPConfig(config: Record): void; 18 | abstract getOPConfig(): Record; 19 | 20 | abstract setOPGeometry() : void; 21 | abstract setOPMaterial() : void; 22 | } -------------------------------------------------------------------------------- /src/utils/event.ts: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Simple event handler by [Jason Kleban](https://gist.github.com/JasonKleban/50cee44960c225ac1993c922563aa540). Keep in mind that if you want to remove it later, you might want to declare the callback as an object. If you want to maintain the reference to `this`, you will need to declare the callback as an arrow function. 4 | */ 5 | export class Event { 6 | /** 7 | * Add a callback to this event instance. 8 | * @param handler - the callback to be added to this event. 9 | */ 10 | add(handler: T extends void ? { (): void } : { (data: T): void }): void { 11 | this.handlers.push(handler); 12 | } 13 | 14 | /** 15 | * Removes a callback from this event instance. 16 | * @param handler - the callback to be removed from this event. 17 | */ 18 | remove(handler: T extends void ? { (): void } : { (data: T): void }): void { 19 | this.handlers = this.handlers.filter((h) => h !== handler); 20 | } 21 | 22 | /** Triggers all the callbacks assigned to this event. */ 23 | trigger = (data?: T) => { 24 | const handlers = this.handlers.slice(0); 25 | for (const handler of handlers) { 26 | handler(data as any); 27 | } 28 | }; 29 | 30 | /** Gets rid of all the suscribed events. */ 31 | reset() { 32 | this.handlers.length = 0; 33 | } 34 | 35 | private handlers: (T extends void ? { (): void } : { (data: T): void })[] = 36 | []; 37 | } 38 | -------------------------------------------------------------------------------- /src/utils/map-helper.ts: -------------------------------------------------------------------------------- 1 | export function getKeyByValue(map: Map, targetValue: any): string | undefined { 2 | for (const [key, value] of map) { 3 | if (value === targetValue) { 4 | return key; 5 | } 6 | } 7 | return undefined; 8 | } 9 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "lib": ["ES2020", "DOM", "DOM.Iterable"], 7 | "skipLibCheck": true, 8 | 9 | /* Bundler mode */ 10 | "moduleResolution": "bundler", 11 | "allowImportingTsExtensions": true, 12 | "emitDeclarationOnly": true, 13 | "isolatedModules": true, 14 | "moduleDetection": "force", 15 | 16 | /* Output settings */ 17 | "declaration": true, 18 | "declarationDir": "dist/types", 19 | "outDir": "dist", 20 | 21 | /* Linting */ 22 | "strict": true, 23 | "noUnusedLocals": true, 24 | "noUnusedParameters": true, 25 | "noFallthroughCasesInSwitch": true, 26 | 27 | "types": ["vite/client"] 28 | }, 29 | "include": ["src"] 30 | } 31 | -------------------------------------------------------------------------------- /vite.config.js: -------------------------------------------------------------------------------- 1 | import { defineConfig } from 'vite'; 2 | import { resolve } from 'path'; 3 | 4 | export default defineConfig({ 5 | base: './', 6 | build: { 7 | outDir: 'examples-dist', 8 | rollupOptions: { 9 | input: { 10 | main: 'index.html', 11 | door: resolve(__dirname, 'examples/door.html'), 12 | wall: resolve(__dirname, 'examples/wall.html'), 13 | spaceContainer: resolve(__dirname, 'examples/spaceContainer.html'), 14 | paper: resolve(__dirname, 'examples/drawings/paper.html'), 15 | }, 16 | output: { 17 | entryFileNames: 'assets/js/[name]-[hash].js', // JS files inside assets/js 18 | chunkFileNames: 'assets/chunks/[name]-[hash].js', // Chunked JS files 19 | assetFileNames: 'assets/static/[name]-[hash][extname]', // Organize static files 20 | }, 21 | }, 22 | }, 23 | server: { 24 | port: 5555 25 | } 26 | }); --------------------------------------------------------------------------------