├── .editorconfig ├── .gitignore ├── README.md ├── angular.json ├── browserslist ├── e2e ├── protractor.conf.js ├── src │ ├── app.e2e-spec.ts │ └── app.po.ts └── tsconfig.e2e.json ├── package-lock.json ├── package.json ├── src ├── app │ ├── app.component.html │ ├── app.component.scss │ ├── app.component.ts │ ├── app.module.ts │ ├── app.service.ts │ ├── components │ │ ├── chairs-layout │ │ │ ├── chairs-layout.component.html │ │ │ ├── chairs-layout.component.scss │ │ │ └── chairs-layout.component.ts │ │ ├── preview-furniture │ │ │ ├── preview-furniture.component.html │ │ │ ├── preview-furniture.component.scss │ │ │ └── preview-furniture.component.ts │ │ └── view │ │ │ ├── view.component.html │ │ │ ├── view.component.scss │ │ │ └── view.component.ts │ ├── helpers.ts │ ├── models │ │ ├── furnishings.ts │ │ └── index.ts │ └── shared │ │ ├── components │ │ ├── index.ts │ │ └── zoom │ │ │ ├── zoom.component.html │ │ │ ├── zoom.component.scss │ │ │ └── zoom.component.ts │ │ ├── helpers.ts │ │ ├── models │ │ ├── furnishings.ts │ │ └── index.ts │ │ ├── modules │ │ ├── design.module.ts │ │ ├── index.ts │ │ └── material.module.ts │ │ └── shared.module.ts ├── assets │ ├── .gitkeep │ └── fonts │ │ ├── glyphicons-halflings-regular.eot │ │ ├── glyphicons-halflings-regular.svg │ │ ├── glyphicons-halflings-regular.ttf │ │ └── glyphicons-halflings-regular.woff ├── environments │ ├── environment.prod.ts │ └── environment.ts ├── favicon.ico ├── index.html ├── karma.conf.js ├── main.ts ├── polyfills.ts ├── styles.scss ├── styles │ ├── _customize.scss │ ├── _theme.scss │ └── main.scss ├── test.ts ├── tsconfig.app.json ├── tsconfig.spec.json └── tslint.json ├── tsconfig.json └── tslint.json /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see https://editorconfig.org 2 | root = true 3 | 4 | [*] 5 | charset = utf-8 6 | indent_style = space 7 | indent_size = 2 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true 10 | 11 | [*.md] 12 | max_line_length = off 13 | trim_trailing_whitespace = false 14 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # See http://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # compiled output 4 | /tmp 5 | /out-tsc 6 | 7 | # dependencies 8 | /node_modules 9 | 10 | # profiling files 11 | chrome-profiler-events.json 12 | speed-measure-plugin.json 13 | 14 | # IDEs and editors 15 | /.idea 16 | .project 17 | .classpath 18 | .c9/ 19 | *.launch 20 | .settings/ 21 | *.sublime-workspace 22 | 23 | # IDE - VSCode 24 | .vscode/* 25 | !.vscode/settings.json 26 | !.vscode/tasks.json 27 | !.vscode/launch.json 28 | !.vscode/extensions.json 29 | 30 | # misc 31 | /.sass-cache 32 | /connect.lock 33 | /coverage 34 | /libpeerconnection.log 35 | npm-debug.log 36 | yarn-error.log 37 | testem.log 38 | /typings 39 | 40 | # System Files 41 | .DS_Store 42 | Thumbs.db 43 | .angular -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Angular App for drawing a floorplan. 2 | 3 | - Built with Fabric.js 4 | - Angular 13 5 | -------------------------------------------------------------------------------- /angular.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "./node_modules/@angular/cli/lib/config/schema.json", 3 | "version": 1, 4 | "newProjectRoot": "projects", 5 | "projects": { 6 | "room-layout": { 7 | "root": "", 8 | "sourceRoot": "src", 9 | "projectType": "application", 10 | "prefix": "app", 11 | "schematics": { 12 | "@schematics/angular:component": { 13 | "style": "scss" 14 | } 15 | }, 16 | "architect": { 17 | "build": { 18 | "builder": "@angular-devkit/build-angular:browser", 19 | "options": { 20 | "outputPath": "dist", 21 | "index": "src/index.html", 22 | "main": "src/main.ts", 23 | "polyfills": "src/polyfills.ts", 24 | "tsConfig": "src/tsconfig.app.json", 25 | "assets": [ 26 | "src/favicon.ico", 27 | "src/assets" 28 | ], 29 | "styles": [ 30 | "src/styles.scss" 31 | ], 32 | "scripts": [], 33 | "vendorChunk": true, 34 | "extractLicenses": false, 35 | "buildOptimizer": false, 36 | "sourceMap": true, 37 | "optimization": false, 38 | "namedChunks": true 39 | }, 40 | "configurations": { 41 | "production": { 42 | "fileReplacements": [ 43 | { 44 | "replace": "src/environments/environment.ts", 45 | "with": "src/environments/environment.prod.ts" 46 | } 47 | ], 48 | "optimization": true, 49 | "outputHashing": "all", 50 | "sourceMap": false, 51 | "extractCss": true, 52 | "namedChunks": false, 53 | "extractLicenses": true, 54 | "vendorChunk": false, 55 | "buildOptimizer": true, 56 | "budgets": [ 57 | { 58 | "type": "initial", 59 | "maximumWarning": "2mb", 60 | "maximumError": "5mb" 61 | }, 62 | { 63 | "type": "anyComponentStyle", 64 | "maximumWarning": "6kb" 65 | } 66 | ] 67 | } 68 | }, 69 | "defaultConfiguration": "" 70 | }, 71 | "serve": { 72 | "builder": "@angular-devkit/build-angular:dev-server", 73 | "options": { 74 | "browserTarget": "room-layout:build" 75 | }, 76 | "configurations": { 77 | "production": { 78 | "browserTarget": "room-layout:build:production" 79 | } 80 | } 81 | }, 82 | "extract-i18n": { 83 | "builder": "@angular-devkit/build-angular:extract-i18n", 84 | "options": { 85 | "browserTarget": "room-layout:build" 86 | } 87 | }, 88 | "test": { 89 | "builder": "@angular-devkit/build-angular:karma", 90 | "options": { 91 | "main": "src/test.ts", 92 | "polyfills": "src/polyfills.ts", 93 | "tsConfig": "src/tsconfig.spec.json", 94 | "karmaConfig": "src/karma.conf.js", 95 | "styles": [ 96 | "./node_modules/@angular/material/prebuilt-themes/pink-bluegrey.css", 97 | "src/styles.scss" 98 | ], 99 | "scripts": [], 100 | "assets": [ 101 | "src/favicon.ico", 102 | "src/assets" 103 | ] 104 | } 105 | }, 106 | "lint": { 107 | "builder": "@angular-devkit/build-angular:tslint", 108 | "options": { 109 | "tsConfig": [ 110 | "src/tsconfig.app.json", 111 | "src/tsconfig.spec.json" 112 | ], 113 | "exclude": [ 114 | "**/node_modules/**" 115 | ] 116 | } 117 | } 118 | } 119 | }, 120 | "room-layout-e2e": { 121 | "root": "e2e/", 122 | "projectType": "application", 123 | "prefix": "", 124 | "architect": { 125 | "e2e": { 126 | "builder": "@angular-devkit/build-angular:protractor", 127 | "options": { 128 | "protractorConfig": "e2e/protractor.conf.js", 129 | "devServerTarget": "room-layout:serve" 130 | }, 131 | "configurations": { 132 | "production": { 133 | "devServerTarget": "room-layout:serve:production" 134 | } 135 | } 136 | }, 137 | "lint": { 138 | "builder": "@angular-devkit/build-angular:tslint", 139 | "options": { 140 | "tsConfig": "e2e/tsconfig.e2e.json", 141 | "exclude": [ 142 | "**/node_modules/**" 143 | ] 144 | } 145 | } 146 | } 147 | } 148 | }, 149 | "defaultProject": "room-layout" 150 | } -------------------------------------------------------------------------------- /browserslist: -------------------------------------------------------------------------------- 1 | # This file is currently used by autoprefixer to adjust CSS to support the below specified browsers 2 | # For additional information regarding the format and rule options, please see: 3 | # https://github.com/browserslist/browserslist#queries 4 | # 5 | # For IE 9-11 support, please remove 'not' from the last line of the file and adjust as needed 6 | 7 | > 0.5% 8 | last 2 versions 9 | Firefox ESR 10 | not dead 11 | not IE 9-11 -------------------------------------------------------------------------------- /e2e/protractor.conf.js: -------------------------------------------------------------------------------- 1 | // Protractor configuration file, see link for more information 2 | // https://github.com/angular/protractor/blob/master/lib/config.ts 3 | 4 | const { SpecReporter } = require('jasmine-spec-reporter'); 5 | 6 | exports.config = { 7 | allScriptsTimeout: 11000, 8 | specs: [ 9 | './src/**/*.e2e-spec.ts' 10 | ], 11 | capabilities: { 12 | 'browserName': 'chrome' 13 | }, 14 | directConnect: true, 15 | baseUrl: 'http://localhost:4200/', 16 | framework: 'jasmine', 17 | jasmineNodeOpts: { 18 | showColors: true, 19 | defaultTimeoutInterval: 30000, 20 | print: function() {} 21 | }, 22 | onPrepare() { 23 | require('ts-node').register({ 24 | project: require('path').join(__dirname, './tsconfig.e2e.json') 25 | }); 26 | jasmine.getEnv().addReporter(new SpecReporter({ spec: { displayStacktrace: true } })); 27 | } 28 | }; -------------------------------------------------------------------------------- /e2e/src/app.e2e-spec.ts: -------------------------------------------------------------------------------- 1 | import { AppPage } from './app.po'; 2 | 3 | describe('workspace-project App', () => { 4 | let page: AppPage; 5 | 6 | beforeEach(() => { 7 | page = new AppPage(); 8 | }); 9 | 10 | it('should display welcome message', () => { 11 | page.navigateTo(); 12 | expect(page.getTitleText()).toEqual('Welcome to room-layout!'); 13 | }); 14 | }); 15 | -------------------------------------------------------------------------------- /e2e/src/app.po.ts: -------------------------------------------------------------------------------- 1 | import { browser, by, element } from 'protractor'; 2 | 3 | export class AppPage { 4 | navigateTo() { 5 | return browser.get('/'); 6 | } 7 | 8 | getTitleText() { 9 | return element(by.css('app-root h1')).getText(); 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /e2e/tsconfig.e2e.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "module": "commonjs", 6 | "target": "es5", 7 | "types": [ 8 | "jasmine", 9 | "jasminewd2", 10 | "node" 11 | ] 12 | } 13 | } -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "room-layout", 3 | "version": "0.0.0", 4 | "scripts": { 5 | "ng": "ng", 6 | "start": "ng serve", 7 | "build": "ng build", 8 | "stage": "ng build --configuration production --base-href 'https://eliteluxury.github.io/room-layout/'", 9 | "test": "ng test", 10 | "lint": "ng lint --fix", 11 | "e2e": "ng e2e" 12 | }, 13 | "private": true, 14 | "dependencies": { 15 | "@angular/animations": "~13.3.4", 16 | "@angular/cdk": "~13.3.4", 17 | "@angular/common": "~13.3.4", 18 | "@angular/compiler": "~13.3.4", 19 | "@angular/core": "~13.3.4", 20 | "@angular/flex-layout": "^13.0.0-beta.38", 21 | "@angular/forms": "~13.3.4", 22 | "@angular/localize": "^13.3.4", 23 | "@angular/material": "^13.3.4", 24 | "@angular/platform-browser": "~13.3.4", 25 | "@angular/platform-browser-dynamic": "~13.3.4", 26 | "@angular/router": "~13.3.4", 27 | "@fortawesome/angular-fontawesome": "^0.10.2", 28 | "@fortawesome/fontawesome-svg-core": "^6.1.0", 29 | "@fortawesome/free-brands-svg-icons": "^6.1.0", 30 | "@fortawesome/free-regular-svg-icons": "^6.1.0", 31 | "@fortawesome/free-solid-svg-icons": "^6.1.0", 32 | "canvas": "^2.9.1", 33 | "core-js": "^3.22.2", 34 | "fabric": "^5.2.1", 35 | "file-saver": "^2.0.5", 36 | "rxjs": "~6.5.4", 37 | "tslib": "^2.2.0", 38 | "uuid": "^8.3.2", 39 | "zone.js": "~0.11.4" 40 | }, 41 | "devDependencies": { 42 | "@angular-devkit/build-angular": "~13.3.3", 43 | "@angular/cli": "~13.3.3", 44 | "@angular/compiler-cli": "^13.3.4", 45 | "@angular/language-service": "~13.3.4", 46 | "@types/fabric": "^4.5.7", 47 | "@types/file-saver": "^2.0.5", 48 | "@types/jasmine": "~2.8.8", 49 | "@types/jasminewd2": "~2.0.3", 50 | "@types/node": "^17.0.26", 51 | "@types/uuid": "^8.3.4", 52 | "jasmine-core": "~2.99.1", 53 | "jasmine-spec-reporter": "~4.2.1", 54 | "karma": "^6.3.19", 55 | "karma-chrome-launcher": "~2.2.0", 56 | "karma-coverage-istanbul-reporter": "~2.0.1", 57 | "karma-jasmine": "~1.1.2", 58 | "karma-jasmine-html-reporter": "^0.2.2", 59 | "protractor": "~7.0.0", 60 | "ts-node": "~7.0.0", 61 | "tslint": "~6.1.3", 62 | "typescript": "~4.6.3" 63 | } 64 | } 65 | -------------------------------------------------------------------------------- /src/app/app.component.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 |

Room Layout

5 |
6 |
7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | Rooms 15 | 16 | 17 | 18 | 19 | 20 | 21 | {{room.title}} 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | Doors 32 | 33 | 34 | 35 |
36 |
37 |
38 | 39 |
{{door.title}}
40 |
41 |
42 |
43 |
44 | 45 | 46 | 47 | 48 | Windows 49 | 50 | 51 | 52 |
53 |
54 |
55 | 56 |
{{window.title}}
57 |
58 |
59 |
60 |
61 | 62 | 63 | 64 | 65 | Tables 66 | 67 | 68 | 69 | 70 | Default Chair 71 | 72 | {{chair.title}} 73 | 74 | 75 |
76 |
77 |
78 | 79 |
{{table.title}}
80 |
81 |
82 |
83 |
84 | 85 | 86 | 87 | 88 | Chairs 89 | 90 | 91 | 92 |
93 |
94 |
95 | 96 |
{{chair.title}}
97 |
98 |
99 |
100 |
101 | 102 | 103 | 104 | 105 | Miscellaneous 106 | 107 | 108 | 109 |
110 |
111 |
112 | 113 |
{{m.title}}
114 |
115 |
116 |
117 |
118 | 119 | 120 | 121 | 122 | Text 123 | 124 | 125 | 126 |
127 | 128 | 129 | 130 | 131 | 132 | 133 |
134 | 135 | Horizontal 136 | Vertical 137 | 138 |
139 |
140 | 141 |
142 |
143 |
144 | 145 | 146 | 147 | 148 | Advanced 149 | 150 | 151 | 152 |
153 | 154 |
155 |
156 | 157 |
158 | 159 | 160 |
161 |
162 |
163 | 164 | 165 | 166 | 167 |
168 | 169 | 173 | 177 | 181 | 185 | 189 | 193 | 197 | 201 | 202 | 204 | 205 | 206 | 210 | 214 | 216 | 217 |
218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | 230 | 231 |
232 |
233 | 234 | 235 | 236 | 237 | 238 | 239 | 240 | 241 | 242 | 243 | 244 | 245 | 246 | 247 | 248 | 249 | 250 | 251 | 252 | 253 | 254 | 259 | 260 | 261 |
TypeNameLeftTopRotationWidthHeight
{{selected.name.split(':')[0] | titlecase}}{{selected.name.split(':')[1]}}{{selected.left}}{{selected.top}}{{selected.angle}}'{{selected.width}}{{selected.height}} 255 | 256 | {{selected._objects.length - 1}} Chairs 257 | 258 |
262 |
263 |
264 | -------------------------------------------------------------------------------- /src/app/app.component.scss: -------------------------------------------------------------------------------- 1 | mat-drawer { 2 | width: 350px; 3 | } 4 | 5 | mat-drawer-container { 6 | height: calc(100% - 64px) 7 | } 8 | 9 | .preview-layout { 10 | display: flex; 11 | flex-wrap: wrap; 12 | justify-content: space-between; 13 | padding: 8px; 14 | 15 | .preview-item { 16 | padding: 8px; 17 | cursor: pointer; 18 | 19 | &:hover { 20 | background: white; 21 | box-shadow: 22 | 0 3px 1px -2px rgba(0, 0, 0, .2), 23 | 0 2px 2px 0 rgba(0, 0, 0, .14), 24 | 0 1px 5px 0 rgba(0, 0, 0, .12); 25 | } 26 | } 27 | 28 | .preview-title { 29 | margin-top: 8px; 30 | text-align: center 31 | } 32 | } 33 | 34 | .status-bar { 35 | border-top: 1px solid rgba(0, 0, 0, .12); 36 | background: #ececec; 37 | min-height: 79px; 38 | width: 100%; 39 | 40 | .status-bar-item { 41 | td { 42 | padding: 10px; 43 | border-bottom: 1px solid rgba(0, 0, 0, .12); 44 | } 45 | 46 | span { 47 | margin-right: 15px; 48 | } 49 | } 50 | } 51 | 52 | .new-text { 53 | mat-radio-group { 54 | padding-left: 12px; 55 | 56 | mat-radio-button { 57 | margin-right: 16px; 58 | } 59 | } 60 | } 61 | 62 | .export-btns { 63 | padding: 24px; 64 | 65 | button { 66 | width: 100%; 67 | &:first-of-type { 68 | margin-bottom: 24px; 69 | } 70 | } 71 | } 72 | 73 | mat-toolbar-row{ 74 | justify-content: space-between 75 | } 76 | -------------------------------------------------------------------------------- /src/app/app.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, FormControl } from '@angular/forms'; 3 | import { MatDialog } from '@angular/material/dialog'; 4 | import { library } from '@fortawesome/fontawesome-svg-core'; 5 | import { 6 | faReply, 7 | faShare, 8 | faClone, 9 | faTrash, 10 | faUndo, 11 | faRedo, 12 | faObjectGroup, 13 | faObjectUngroup, 14 | faPlus, 15 | faMinus 16 | } from '@fortawesome/free-solid-svg-icons'; 17 | 18 | import { FURNISHINGS } from './models/furnishings'; 19 | import { AppService } from './app.service'; 20 | import { ChairsLayoutComponent } from './components/chairs-layout/chairs-layout.component'; 21 | 22 | library.add(faReply, faShare, faClone, faTrash, faUndo, faRedo, faObjectGroup, faObjectUngroup, faMinus, faPlus); 23 | 24 | @Component({ 25 | selector: 'app-root', 26 | templateUrl: './app.component.html', 27 | styleUrls: ['./app.component.scss'] 28 | }) 29 | export class AppComponent implements OnInit { 30 | title = 'room-layout'; 31 | 32 | init = false; 33 | furnishings = FURNISHINGS; 34 | defaultChairIndex = 0; 35 | 36 | textForm: FormGroup; 37 | 38 | previewItem = null; 39 | previewType = null; 40 | 41 | // icons 42 | faReply = faReply; 43 | faShare = faShare; 44 | faClone = faClone; 45 | faTrash = faTrash; 46 | faUndo = faUndo; 47 | faRedo = faRedo; 48 | faObjectGroup = faObjectGroup; 49 | faObjectUngroup = faObjectUngroup; 50 | faPlus = faPlus; 51 | faMinus = faMinus; 52 | 53 | constructor(public app: AppService, private dialog: MatDialog) { } 54 | 55 | ngOnInit() { 56 | const defaultChair = FURNISHINGS.chairs[0]; 57 | setTimeout(() => { 58 | this.app.defaultChair.next(defaultChair); 59 | this.init = true; 60 | }, 100); 61 | this.initTextForm(); 62 | } 63 | 64 | insert(object: any, type: string) { 65 | if (this.app.roomEdit) { return; } 66 | this.app.insertObject.next({ type, object }); 67 | } 68 | 69 | defaultChairChanged(index: number) { 70 | this.defaultChairIndex = index; 71 | this.app.defaultChair.next(FURNISHINGS.chairs[index]); 72 | } 73 | 74 | initTextForm() { 75 | this.textForm = new FormGroup({ 76 | text: new FormControl('New Text'), 77 | font_size: new FormControl(16), 78 | direction: new FormControl('HORIZONTAL') 79 | }); 80 | } 81 | 82 | insertNewText() { 83 | this.insert({ ...this.textForm.value, name: 'TEXT:Text' }, 'TEXT'); 84 | } 85 | 86 | layoutChairs() { 87 | const ref = this.dialog.open(ChairsLayoutComponent); 88 | ref.afterClosed().subscribe(res => { 89 | if (!res) { 90 | return; 91 | } 92 | this.insert(res, 'LAYOUT'); 93 | }); 94 | } 95 | 96 | download(format: string) { 97 | this.app.performOperation.next(format); 98 | } 99 | 100 | onZoom(value) { 101 | this.app.zoom = value; 102 | this.app.performOperation.next('ZOOM'); 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/app/app.module.ts: -------------------------------------------------------------------------------- 1 | import { BrowserModule } from '@angular/platform-browser'; 2 | import { NgModule } from '@angular/core'; 3 | import { FormsModule, ReactiveFormsModule } from '@angular/forms'; 4 | import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; 5 | 6 | import { SharedModule } from './shared/shared.module'; 7 | 8 | import { AppComponent } from './app.component'; 9 | import { ViewComponent } from './components/view/view.component'; 10 | import { PreviewFurnitureComponent } from './components/preview-furniture/preview-furniture.component'; 11 | import { ChairsLayoutComponent } from './components/chairs-layout/chairs-layout.component'; 12 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 13 | 14 | @NgModule({ 15 | declarations: [ 16 | AppComponent, 17 | ViewComponent, 18 | PreviewFurnitureComponent, 19 | ChairsLayoutComponent 20 | ], 21 | imports: [ 22 | BrowserModule, 23 | BrowserAnimationsModule, 24 | SharedModule, 25 | FormsModule, 26 | ReactiveFormsModule, 27 | FontAwesomeModule 28 | ], 29 | providers: [], 30 | bootstrap: [AppComponent], 31 | entryComponents: [ChairsLayoutComponent] 32 | }) 33 | export class AppModule { } 34 | -------------------------------------------------------------------------------- /src/app/app.service.ts: -------------------------------------------------------------------------------- 1 | import { Injectable } from '@angular/core'; 2 | import { Subject, BehaviorSubject } from 'rxjs'; 3 | 4 | @Injectable({ 5 | providedIn: 'root' 6 | }) 7 | export class AppService { 8 | 9 | roomEdit = false; 10 | 11 | states = []; 12 | redoStates = []; 13 | 14 | roomEditOperate = 'CORNER'; 15 | roomEditStates = []; 16 | roomEditRedoStates = []; 17 | 18 | selections: any[] = []; 19 | copied: any; 20 | 21 | ungroupable = false; 22 | 23 | insertObject: Subject = new Subject(); 24 | defaultChair: Subject = new Subject(); 25 | performOperation: Subject = new Subject(); 26 | roomEdition: Subject = new Subject(); 27 | saveState = new Subject(); 28 | zoom = 100; 29 | 30 | constructor() { 31 | this.saveState.subscribe(res => { 32 | if (this.roomEdit) { 33 | this.roomEditStates.push(res); 34 | this.roomEditRedoStates = []; 35 | return; 36 | } 37 | this.states.push(res); 38 | this.redoStates = []; 39 | }); 40 | } 41 | 42 | editRoom() { 43 | this.roomEdit = true; 44 | this.roomEdition.next(true); 45 | } 46 | 47 | endEditRoom() { 48 | this.roomEdit = false; 49 | this.roomEdition.next(false); 50 | } 51 | 52 | undo() { 53 | if ((this.states.length === 1 && !this.roomEdit) || (this.roomEditStates.length === 1 && this.roomEdit)) { 54 | return; 55 | } 56 | this.performOperation.next('UNDO'); 57 | } 58 | 59 | redo() { 60 | if ((this.redoStates.length === 0 && !this.roomEdit) || (this.roomEditRedoStates.length === 0 && this.roomEdit)) { 61 | return; 62 | } 63 | this.performOperation.next('REDO'); 64 | } 65 | 66 | clone() { 67 | this.copy(true); 68 | } 69 | 70 | copy(doClone = false) { 71 | this.performOperation.next('COPY'); 72 | if (doClone) { 73 | setTimeout(() => this.paste(), 100); 74 | } 75 | } 76 | 77 | paste() { 78 | this.performOperation.next('PASTE'); 79 | } 80 | 81 | delete() { 82 | if (!this.selections.length) { 83 | return; 84 | } 85 | this.performOperation.next('DELETE'); 86 | } 87 | 88 | rotateAntiClockWise() { 89 | this.performOperation.next('ROTATE_ANTI'); 90 | } 91 | 92 | rotateClockWise() { 93 | this.performOperation.next('ROTATE'); 94 | } 95 | 96 | group() { 97 | this.performOperation.next('GROUP'); 98 | } 99 | 100 | ungroup() { 101 | this.performOperation.next('UNGROUP'); 102 | } 103 | 104 | placeInCenter(direction) { 105 | this.performOperation.next(direction); 106 | } 107 | 108 | arrange(side) { 109 | this.performOperation.next(side); 110 | } 111 | 112 | zoomIn() { 113 | if (this.zoom >= 150) { 114 | return; 115 | } 116 | this.zoom += 10; 117 | this.performOperation.next('ZOOM'); 118 | } 119 | 120 | zoomOut() { 121 | if (this.zoom <= 20) { 122 | return; 123 | } 124 | this.zoom -= 10; 125 | this.performOperation.next('ZOOM'); 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/app/components/chairs-layout/chairs-layout.component.html: -------------------------------------------------------------------------------- 1 |
2 |
3 |
4 | 6 | Normal 7 | Curved 8 | 9 | 10 |
11 |
12 |
13 | 14 | Select Chair 15 | 16 | {{chair.title}} 17 | 18 | 19 |
20 |
21 |
22 | 23 | 24 | Between 1 to 5 25 | 26 |
27 |
28 | 29 | 30 | Between 50 to 500 31 | 32 |
33 |
34 | 35 | 36 | Between 10 to 360 37 | 38 |
39 |
40 | 41 | 43 | Between 10 to 50 44 | 45 |
46 |
47 |
48 |

Number of chairs in rows

49 | 50 |
51 | 52 | 53 | 54 |
55 | 56 |
57 | 58 |
59 |
60 | 61 | Select Chair 62 | 63 | {{chair.title}} 64 | 65 | 66 |
67 |
68 |
69 | 70 | Sections 71 | 72 | {{opt}} 73 | 74 | 75 |
76 |
77 | 78 | 79 | Between 1 to 50 80 | 81 |
82 |
83 | 84 | 85 | 86 |
87 |
88 | 89 | 91 | Between 0 to 6 92 | 93 |
94 |
95 | 96 | 97 | 98 |
99 |
100 |
101 |

Spacing between sections

102 | 103 |
104 | 105 | 106 | 107 |
108 | 109 |
110 | 111 |
112 |
113 | 114 |
115 |
116 | 117 | 118 |
119 |
120 | -------------------------------------------------------------------------------- /src/app/components/chairs-layout/chairs-layout.component.scss: -------------------------------------------------------------------------------- 1 | .layout-chairs { 2 | // min-width: 700px; 3 | 4 | .layout-type { 5 | padding: 24px 0; 6 | mat-radio-button { 7 | margin-right: 24px; 8 | } 9 | } 10 | 11 | label { 12 | display: block; 13 | margin-bottom: 5px; 14 | font-size: 16px; 15 | } 16 | 17 | span { 18 | display: block; 19 | font-size: 12px; 20 | color: #555; 21 | } 22 | 23 | // input { 24 | // width: 80px; 25 | // padding: 6px 2px; 26 | // } 27 | 28 | .layout-option { 29 | margin-bottom: 1rem; 30 | } 31 | 32 | canvas { 33 | border: 1px solid #ececec; 34 | border-radius: 3px; 35 | } 36 | 37 | p { 38 | margin-bottom: 0; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/app/components/chairs-layout/chairs-layout.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit } from '@angular/core'; 2 | import { FormGroup, FormControl, FormArray } from '@angular/forms'; 3 | import { fabric } from 'fabric'; 4 | import { MatDialogRef } from '@angular/material/dialog'; 5 | 6 | import { FURNISHINGS } from '../../models/furnishings'; 7 | import { createShape, RL_FILL, RL_STROKE } from '../../helpers'; 8 | 9 | const WIDTH = 1100, HEIGHT = 400; 10 | 11 | @Component({ 12 | selector: 'app-chairs-layout', 13 | templateUrl: './chairs-layout.component.html', 14 | styleUrls: ['./chairs-layout.component.scss'] 15 | }) 16 | export class ChairsLayoutComponent implements OnInit { 17 | 18 | layout: fabric.Group; 19 | layoutOption = 'NORMAL'; 20 | rectBlock: FormGroup; 21 | curvedBlock: FormGroup; 22 | view: fabric.Canvas; 23 | chairs = []; 24 | sps: FormArray; // Spacing between sections 25 | zoom = 100; 26 | 27 | constructor(private dialogRef: MatDialogRef) { } 28 | 29 | ngOnInit() { 30 | this.chairs = FURNISHINGS.chairs; 31 | 32 | this.rectBlock = new FormGroup({ 33 | chair: new FormControl(0), 34 | rows: new FormControl(1), 35 | sections: new FormControl(1), 36 | chairs: new FormControl(12), 37 | spacing_chair: new FormControl(0), 38 | spacing_row: new FormControl(22), 39 | spacing_sections: new FormArray([1, 2, 3, 4].map(() => new FormControl(5))) 40 | }); 41 | 42 | const array = []; 43 | for (let i = 0; i < 20; i++) { 44 | array.push(i); 45 | } 46 | 47 | this.curvedBlock = new FormGroup({ 48 | chair: new FormControl(0), 49 | radius: new FormControl(200), 50 | angle: new FormControl(180), 51 | rows: new FormControl(1), 52 | spacing_row: new FormControl(40), 53 | chairs: new FormArray(new Array(10).fill(new FormControl(10))), 54 | }); 55 | 56 | this.view = new fabric.Canvas('layout_chairs'); 57 | this.view.setWidth(WIDTH); 58 | this.view.setHeight(HEIGHT); 59 | 60 | this.rectBlock.valueChanges.subscribe(() => this.changeLayout()); 61 | this.curvedBlock.valueChanges.subscribe(() => this.changeLayout()); 62 | this.changeLayout(); 63 | } 64 | 65 | layoutOptionChanged(value: 'CURVED' | 'NORMAL') { 66 | this.layoutOption = value; 67 | this.changeLayout(); 68 | } 69 | 70 | 71 | changeLayout() { 72 | const chrs = []; 73 | 74 | if (this.layoutOption === 'CURVED') { 75 | const { radius, angle, rows, chair, spacing_row, chairs } = this.curvedBlock.value; 76 | const start = -(angle / 2); 77 | for (let r = 0; r < rows; r++) { 78 | const N = chairs[r], A = angle / N; 79 | const rad = radius + r * spacing_row; 80 | for (let i = 0; i <= N; i += 1) { 81 | const ca = start + i * A; 82 | const chr = createShape(this.chairs[chair], RL_STROKE, RL_FILL); 83 | chr.angle = ca; 84 | const x = Math.sin(this.toRadians(ca)) * rad; 85 | const y = Math.cos(this.toRadians(ca)) * rad; 86 | chr.left = x; 87 | chr.top = -y; 88 | chr.angle += 180; 89 | chrs.push(chr); 90 | } 91 | } 92 | } else { 93 | const { rows, sections, chairs, spacing_chair, spacing_row, chair } = this.rectBlock.value; 94 | const total = rows * chairs; 95 | const cps = Math.floor(chairs / sections); // Chairs per section 96 | let x = 0, y = 0; 97 | 98 | for (let i = 1; i <= total; i++) { 99 | const chr = createShape(this.chairs[chair], RL_STROKE, RL_FILL); 100 | chr.left = x, chr.top = y; 101 | 102 | if (i % chairs === 0) { 103 | y += (spacing_row + chr.height); 104 | x = 0; 105 | } else { 106 | x += (chr.width + spacing_chair); 107 | const s = Math.floor(i % chairs / cps); 108 | if (i % chairs % cps === 0 && s + 1 <= this.sections) { 109 | x += this.rectBlock.value.spacing_sections[s - 1]; 110 | } 111 | } 112 | chrs.push(chr); 113 | } 114 | } 115 | this.view.clear(); 116 | this.layout = new fabric.Group(chrs, { 117 | originX: 'center', 118 | originY: 'center', 119 | left: WIDTH / 2, 120 | top: HEIGHT / 2, 121 | selectable: false, 122 | name: 'BLOCK:Chairs', 123 | hasControls: false, 124 | }); 125 | this.layout.scale(this.zoom / 100); 126 | this.view.add(this.layout); 127 | this.view.renderAll(); 128 | } 129 | 130 | onZoom(value: number) { 131 | this.zoom = value; 132 | this.layout.scale(value / 100); 133 | this.view.renderAll(); 134 | } 135 | 136 | get spacing_sections() { 137 | const c = this.rectBlock.get('spacing_sections') as FormArray; 138 | return c.controls; 139 | } 140 | 141 | get sections() { 142 | return this.rectBlock.value.sections; 143 | } 144 | 145 | get curved_chairs() { 146 | const c = this.curvedBlock.get('chairs') as FormArray; 147 | return c.controls; 148 | } 149 | 150 | get curved_rows() { 151 | return this.curvedBlock.value.rows; 152 | } 153 | 154 | create() { 155 | this.layout.selectable = true; 156 | this.layout.scale(1); 157 | this.dialogRef.close(this.layout); 158 | } 159 | 160 | cancel() { 161 | this.dialogRef.close(); 162 | } 163 | 164 | toRadians(angle: number) { 165 | return angle * (Math.PI / 180); 166 | } 167 | 168 | toDegrees(radian: number) { 169 | return radian * (180 / Math.PI); 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /src/app/components/preview-furniture/preview-furniture.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
4 | -------------------------------------------------------------------------------- /src/app/components/preview-furniture/preview-furniture.component.scss: -------------------------------------------------------------------------------- 1 | canvas { 2 | border: 1px solid #ececec; 3 | } -------------------------------------------------------------------------------- /src/app/components/preview-furniture/preview-furniture.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, AfterViewInit } from '@angular/core'; 2 | import { fabric } from 'fabric'; 3 | import * as uuid from 'uuid'; 4 | 5 | import { RL_PREVIEW_HEIGHT, RL_PREVIEW_WIDTH, createFurniture } from '../../helpers'; 6 | import { AppService } from '../../app.service'; 7 | 8 | let RL_DEFAULT_CHAIR = null; 9 | 10 | @Component({ 11 | selector: 'app-preview-furniture', 12 | templateUrl: './preview-furniture.component.html', 13 | styleUrls: ['./preview-furniture.component.scss'] 14 | }) 15 | export class PreviewFurnitureComponent implements OnInit, AfterViewInit { 16 | 17 | id: any; 18 | canvas: fabric.Canvas; 19 | 20 | @Input() 21 | type: string; 22 | 23 | @Input() 24 | furniture: any; 25 | 26 | constructor(public app: AppService) { } 27 | 28 | ngOnInit() { 29 | this.id = uuid.v4(); 30 | this.app.defaultChair.subscribe(res => { 31 | this.canvas.clear(); 32 | RL_DEFAULT_CHAIR = res; 33 | const type = this.type, object = this.furniture; 34 | this.handleObjectInsertion({type, object}); 35 | this.canvas.renderAll(); 36 | }); 37 | } 38 | 39 | ngAfterViewInit() { 40 | const canvas = new fabric.Canvas(this.id); 41 | canvas.setWidth(RL_PREVIEW_WIDTH); 42 | canvas.setHeight(RL_PREVIEW_HEIGHT); 43 | this.canvas = canvas; 44 | } 45 | 46 | handleObjectInsertion({ type, object }) { 47 | const group = createFurniture(type, object, RL_DEFAULT_CHAIR); 48 | 49 | group.left = RL_PREVIEW_WIDTH / 2; 50 | group.top = RL_PREVIEW_HEIGHT / 2; 51 | group.selectable = false; 52 | group.hoverCursor = 'pointer'; 53 | 54 | this.canvas.add(group); 55 | } 56 | } 57 | -------------------------------------------------------------------------------- /src/app/components/view/view.component.html: -------------------------------------------------------------------------------- 1 |
2 | 3 |
-------------------------------------------------------------------------------- /src/app/components/view/view.component.scss: -------------------------------------------------------------------------------- 1 | .main-container { 2 | padding: 24px; 3 | overflow: auto; 4 | height: calc(100% - 199px); 5 | } -------------------------------------------------------------------------------- /src/app/components/view/view.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, AfterViewInit } from '@angular/core'; 2 | import { formatDate } from '@angular/common'; 3 | import { fabric } from 'fabric'; 4 | import { saveAs } from 'file-saver'; 5 | 6 | import { AppService } from '../../app.service'; 7 | import * as _ from '../../helpers'; 8 | 9 | const { 10 | RL_VIEW_WIDTH, 11 | RL_VIEW_HEIGHT, 12 | RL_FOOT, 13 | RL_AISLEGAP, 14 | RL_ROOM_OUTER_SPACING, 15 | RL_ROOM_INNER_SPACING, 16 | RL_ROOM_STROKE, 17 | RL_CORNER_FILL, 18 | RL_UNGROUPABLES, 19 | RL_CREDIT_TEXT, 20 | RL_CREDIT_TEXT_PARAMS 21 | } = _; 22 | 23 | const { Line, Point } = fabric; 24 | const 25 | HORIZONTAL = 'HORIZONTAL', 26 | VERTICAL = 'VERTICAL', 27 | OFFSET = RL_ROOM_INNER_SPACING / 2; 28 | 29 | const Left = (wall) => wall.x1 < wall.x2 ? wall.x1 : wall.x2; 30 | const Top = (wall) => wall.y1 < wall.y2 ? wall.y1 : wall.y2; 31 | const Right = (wall) => wall.x1 > wall.x2 ? wall.x1 : wall.x2; 32 | const Bottom = (wall) => wall.y1 > wall.y2 ? wall.y1 : wall.y2; 33 | 34 | 35 | @Component({ 36 | selector: 'app-view', 37 | templateUrl: './view.component.html', 38 | styleUrls: ['./view.component.scss'], 39 | host: { 40 | '(document:keydown)': 'onKeyDown($event)', 41 | '(document:keyup)': 'onKeyUp($event)' 42 | } 43 | }) 44 | export class ViewComponent implements OnInit, AfterViewInit { 45 | 46 | view: fabric.Canvas; 47 | room: fabric.Group; 48 | roomLayer: fabric.Group | fabric.Rect; 49 | corners = []; 50 | walls: fabric.Line[] = []; 51 | lastObjectDefinition = null; 52 | lastObject = null; 53 | 54 | CTRL_KEY_DOWN = false; 55 | MOVE_WALL_ID = -1; 56 | ROOM_SIZE = { width: 960, height: 480 }; 57 | DEFAULT_CHAIR = null; 58 | REMOVE_DW = false; 59 | 60 | constructor(public app: AppService) { } 61 | 62 | 63 | 64 | ngOnInit() { 65 | this.app.roomEdition.subscribe(doEdit => { 66 | this.corners.forEach(c => this.setCornerStyle(c)); 67 | this.drawRoom(); 68 | if (doEdit) { this.editRoom(); } else { this.cancelRoomEdition(); } 69 | }); 70 | 71 | this.app.insertObject.subscribe(res => { 72 | this.handleObjectInsertion(res); 73 | this.saveState(); 74 | }); 75 | 76 | this.app.defaultChair.subscribe(res => this.DEFAULT_CHAIR = res); 77 | 78 | this.app.performOperation.subscribe(operation => { 79 | switch (operation) { 80 | 81 | case 'UNDO': 82 | this.undo(); 83 | break; 84 | 85 | case 'REDO': 86 | this.redo(); 87 | break; 88 | 89 | case 'COPY': 90 | this.copy(); 91 | break; 92 | 93 | case 'PASTE': 94 | this.paste(); 95 | break; 96 | 97 | case 'DELETE': 98 | this.delete(); 99 | break; 100 | 101 | case 'ROTATE': 102 | this.rotate(); 103 | break; 104 | 105 | case 'ROTATE_ANTI': 106 | this.rotate(false); 107 | break; 108 | 109 | case 'GROUP': 110 | this.group(); 111 | break; 112 | 113 | case 'UNGROUP': 114 | this.ungroup(); 115 | break; 116 | 117 | case 'HORIZONTAL': 118 | case 'VERTICAL': 119 | this.placeInCenter(operation); 120 | break; 121 | 122 | case 'ROOM_OPERATION': 123 | this.drawRoom(); 124 | break; 125 | 126 | case 'PNG': 127 | case 'SVG': 128 | this.saveAs(operation); 129 | break; 130 | 131 | case 'ZOOM': 132 | this.setZoom(); 133 | break; 134 | 135 | case 'LEFT': 136 | case 'CENTER': 137 | case 'RIGHT': 138 | case 'TOP': 139 | case 'MIDDLE': 140 | case 'BOTTOM': 141 | this.arrange(operation); 142 | break; 143 | } 144 | }); 145 | } 146 | 147 | 148 | 149 | ngAfterViewInit() { 150 | /** Initialize canvas */ 151 | this.setCanvasView(); 152 | /** Add room */ 153 | this.setRoom(this.ROOM_SIZE); 154 | this.saveState(); 155 | } 156 | 157 | 158 | 159 | get room_origin() { 160 | return RL_ROOM_OUTER_SPACING + RL_ROOM_INNER_SPACING; 161 | } 162 | 163 | 164 | 165 | onKeyDown(event: KeyboardEvent) { 166 | const code = event.key || event.keyCode; 167 | // Ctrl Key is down 168 | if (event.ctrlKey) { 169 | this.CTRL_KEY_DOWN = true; 170 | // Ctrl + Shift + Z 171 | if (event.shiftKey && code === 90) 172 | this.app.redo(); 173 | else if (code === 90) 174 | this.app.undo(); 175 | else if (code === 67) 176 | this.app.copy(); 177 | else if (code === 86) 178 | this.paste(); 179 | else if (code === 37) 180 | this.rotate(); 181 | else if (code === 39) 182 | this.rotate(false); 183 | else if (code === 71) 184 | this.group(); 185 | } 186 | else if (code === 46) 187 | this.delete(); 188 | else if (code === 37) 189 | this.move('LEFT'); 190 | else if (code === 38) 191 | this.move('UP'); 192 | else if (code === 39) 193 | this.move('RIGHT'); 194 | else if (code === 40) 195 | this.move('DOWN'); 196 | } 197 | 198 | 199 | 200 | onKeyUp(event: KeyboardEvent) { 201 | if (event.key === 'Control') { 202 | this.CTRL_KEY_DOWN = false; 203 | } 204 | } 205 | 206 | 207 | onScroll(event) { } 208 | 209 | 210 | setGroupableState() { 211 | if (this.app.selections.length > 1) { 212 | this.app.ungroupable = false; 213 | return; 214 | } 215 | 216 | const obj = this.view.getActiveObject(); 217 | const type = obj.name ? obj.name.split(':')[0] : ''; 218 | 219 | if (RL_UNGROUPABLES.indexOf(type) > -1) { 220 | this.app.ungroupable = false; 221 | } else { 222 | this.app.ungroupable = true; 223 | } 224 | } 225 | 226 | 227 | onSelected() { 228 | const active = this.view.getActiveObject(); 229 | active.lockScalingX = true, active.lockScalingY = true; 230 | if (!active.name) { 231 | active.name = 'GROUP'; 232 | } 233 | this.app.selections = this.view.getActiveObjects(); 234 | this.setGroupableState(); 235 | } 236 | 237 | 238 | /********************************************************************************************************** 239 | * init the canvas view & bind events 240 | * ------------------------------------------------------------------------------------------------------- 241 | */ 242 | setCanvasView() { 243 | const canvas = new fabric.Canvas('main'); 244 | canvas.setWidth(RL_VIEW_WIDTH * RL_FOOT); 245 | canvas.setHeight(RL_VIEW_HEIGHT * RL_FOOT); 246 | this.view = canvas; 247 | 248 | const cornersOfWall = (obj: fabric.Line) => { 249 | const id = Number(obj.name.split(':')[1]); 250 | const v1Id = id; 251 | const v1 = this.corners[v1Id]; 252 | const v2Id = (id + 1) % this.walls.length; 253 | const v2 = this.corners[v2Id]; 254 | return { v1, v1Id, v2, v2Id }; 255 | }; 256 | 257 | this.view.on('selection:created', (e: fabric.IEvent) => { 258 | if (this.app.roomEdit) { 259 | return; 260 | } 261 | this.onSelected(); 262 | }); 263 | 264 | this.view.on('selection:updated', (e: fabric.IEvent) => { 265 | if (this.app.roomEdit) { 266 | return; 267 | } 268 | this.onSelected(); 269 | }); 270 | 271 | this.view.on('selection:cleared', (e: fabric.IEvent) => { 272 | if (this.app.roomEdit) { 273 | return; 274 | } 275 | this.app.selections = []; 276 | this.app.ungroupable = false; 277 | }); 278 | 279 | this.view.on('object:moved', () => { 280 | if (this.MOVE_WALL_ID !== -1) { 281 | this.MOVE_WALL_ID = -1; 282 | } 283 | this.saveState(); 284 | }); 285 | 286 | this.view.on('object:rotated', () => this.saveState()); 287 | 288 | this.view.on('mouse:down:before', (e: fabric.IEvent) => { 289 | const obj = e.target; 290 | 291 | if (this.app.roomEdit && obj && obj.name.indexOf('WALL') > -1 && obj instanceof Line) { 292 | let { v1, v2, v1Id, v2Id } = cornersOfWall(obj); 293 | const v0Id = (v1Id === 0) ? this.corners.length - 1 : v1Id - 1; 294 | const v3Id = (v2Id === this.corners.length - 1) ? 0 : v2Id + 1; 295 | const v0 = this.corners[v0Id]; 296 | const v3 = this.corners[v3Id]; 297 | 298 | this.MOVE_WALL_ID = v1Id; 299 | 300 | if ((v0.top === v1.top && v1.top === v2.top) || (v0.left === v1.left && v1.left === v2.left)) { 301 | this.corners.splice(v1Id, 0, this.drawCorner(new Point(v1.left, v1.top))); 302 | this.MOVE_WALL_ID = v1Id + 1; 303 | v2Id += 1; 304 | } 305 | 306 | if ((v1.top === v2.top && v2.top === v3.top) || (v1.left === v2.left && v2.left === v3.left)) { 307 | this.corners.splice(v2Id + 1, 0, this.drawCorner(new Point(v2.left, v2.top))); 308 | } 309 | 310 | this.drawRoom(); 311 | this.saveState(); 312 | } 313 | }); 314 | 315 | this.view.on('object:moving', (e: fabric.IEvent) => { 316 | if (this.MOVE_WALL_ID !== -1) { 317 | const p = e['pointer']; 318 | const v1 = this.corners[this.MOVE_WALL_ID]; 319 | const v2 = this.corners[(this.MOVE_WALL_ID + 1) % this.corners.length]; 320 | const direction = v1.left === v2.left ? 'HORIZONTAL' : 'VERTICAL'; 321 | 322 | if (p.y < RL_ROOM_OUTER_SPACING) { p.y = RL_ROOM_OUTER_SPACING; } 323 | if (p.x < RL_ROOM_OUTER_SPACING) { p.x = RL_ROOM_OUTER_SPACING; } 324 | 325 | if (direction === 'VERTICAL') { 326 | v1.top = v2.top = p.y; 327 | } else { 328 | v1.left = v2.left = p.x; 329 | } 330 | 331 | this.drawRoom(); 332 | } 333 | 334 | const obj = e.target; 335 | const point = e['pointer']; 336 | 337 | if (obj && this.isDW(obj) && obj instanceof fabric.Group) { 338 | let wall, distance = 999; 339 | const dist2 = (v, w) => (v.x - w.x) * (v.x - w.x) + (v.y - w.y) * (v.y - w.y); 340 | const point_to_line = (p, v, w) => Math.sqrt(distToSegmentSquared(p, v, w)); 341 | const distToSegmentSquared = (p, v, w) => { 342 | const l2 = dist2(v, w); 343 | 344 | if (l2 == 0) 345 | return dist2(p, v); 346 | 347 | const t = ((p.x - v.x) * (w.x - v.x) + (p.y - v.y) * (w.y - v.y)) / l2; 348 | 349 | if (t < 0) 350 | return dist2(p, v); 351 | 352 | if (t > 1) 353 | return dist2(p, w); 354 | 355 | return dist2(p, { x: v.x + t * (w.x - v.x), y: v.y + t * (w.y - v.y) }); 356 | }; 357 | 358 | this.walls.forEach(w => { 359 | const d = point_to_line(point, { x: w.x1, y: w.y1 }, { x: w.x2, y: w.y2 }); 360 | if (d < distance) { 361 | distance = d, wall = w; 362 | } 363 | }); 364 | 365 | if (distance > 20) { 366 | this.REMOVE_DW = true; 367 | } else { 368 | this.REMOVE_DW = false; 369 | const direction = this.directionOfWall(wall); 370 | 371 | if (direction === HORIZONTAL) { 372 | this.locateDW(obj, wall, point.x, Top(wall)); 373 | } else { 374 | this.locateDW(obj, wall, Left(wall), point.y); 375 | } 376 | } 377 | } 378 | }); 379 | 380 | this.view.on('mouse:up', (e: fabric.IEvent) => { 381 | const obj = e.target; 382 | 383 | if (this.REMOVE_DW) { 384 | this.view.remove(obj); 385 | this.REMOVE_DW = false; 386 | } 387 | }); 388 | 389 | this.view.on('mouse:dblclick', (e: fabric.IEvent) => { 390 | const obj = e.target; 391 | 392 | if (this.app.roomEdit && this.app.roomEditOperate === 'CORNER' && obj && obj.name.indexOf('WALL') > -1 && obj instanceof Line) { 393 | const p = e['pointer']; 394 | const { v1, v1Id, v2, v2Id } = cornersOfWall(obj); 395 | const ind = v1Id < v2Id ? v1Id : v2Id; 396 | 397 | if (v1.left === v2.left) { 398 | p.x = v1.left; 399 | } else if (v1.top === v2.top) { 400 | p.y = v1.top; 401 | } 402 | 403 | const newCorner = this.drawCorner(new Point(p.x, p.y)); 404 | 405 | if (Math.abs(v1Id - v2Id) != 1) { 406 | this.corners.push(newCorner); 407 | } else { 408 | this.corners.splice(ind + 1, 0, newCorner); 409 | } 410 | 411 | this.drawRoom(); 412 | this.saveState(); 413 | } 414 | }); 415 | } 416 | 417 | 418 | 419 | 420 | /********************************************************************************************************** 421 | * draw Rooms defined in Model 422 | * ------------------------------------------------------------------------------------------------------- 423 | */ 424 | setRoom({ width, height }) { 425 | if (this.walls.length) { 426 | this.view.remove(...this.walls); 427 | this.view.renderAll(); 428 | } 429 | 430 | const LT = new Point(RL_ROOM_OUTER_SPACING, RL_ROOM_OUTER_SPACING); 431 | const RT = new Point(LT.x + width, LT.y); 432 | const LB = new Point(LT.x, LT.y + height); 433 | const RB = new Point(RT.x, LB.y); 434 | 435 | this.corners = [LT, RT, RB, LB].map(p => this.drawCorner(p)); 436 | this.drawRoom(); 437 | } 438 | 439 | 440 | /********************************************************************************************************** 441 | * set corner according to current edition status 442 | * ------------------------------------------------------------------------------------------------------- 443 | */ 444 | setCornerStyle(c: fabric.Rect) { 445 | c.moveCursor = this.view.freeDrawingCursor; 446 | c.hoverCursor = this.view.freeDrawingCursor; 447 | c.selectable = false; 448 | c.evented = false; 449 | c.width = c.height = (RL_ROOM_INNER_SPACING / (this.app.roomEdit ? 1.5 : 2)) * 2; 450 | c.set('fill', this.app.roomEdit ? RL_CORNER_FILL : RL_ROOM_STROKE); 451 | } 452 | 453 | 454 | 455 | /********************************************************************************************************** 456 | * draw corner 457 | * ------------------------------------------------------------------------------------------------------- 458 | */ 459 | drawCorner(p: fabric.Point) { 460 | const c = new fabric.Rect({ 461 | left: p.x, 462 | top: p.y, 463 | strokeWidth: 0, 464 | hasControls: false, 465 | originX: 'center', 466 | originY: 'center', 467 | name: 'CORNER' 468 | }); 469 | this.setCornerStyle(c); 470 | return c; 471 | } 472 | 473 | 474 | /********************************************************************************************************** 475 | * draw room 476 | * ------------------------------------------------------------------------------------------------------- 477 | */ 478 | drawRoom() { 479 | 480 | const exists = this.view.getObjects().filter(obj => obj.name.indexOf('WALL') > -1 || obj.name === 'CORNER'); 481 | this.view.remove(...exists); 482 | 483 | this.view.add(...this.corners); 484 | 485 | const wall = (coords: number[], index: number) => new Line(coords, { 486 | stroke: RL_ROOM_STROKE, 487 | strokeWidth: RL_ROOM_INNER_SPACING, 488 | name: `WALL:${index}`, 489 | originX: 'center', 490 | originY: 'center', 491 | hoverCursor: this.app.roomEdit ? this.view.moveCursor : this.view.defaultCursor, 492 | hasControls: false, 493 | hasBorders: false, 494 | selectable: this.app.roomEdit, 495 | evented: this.app.roomEdit, 496 | cornerStyle: 'rect' 497 | }); 498 | 499 | let LT = new Point(9999, 9999), RB = new Point(0, 0); 500 | 501 | this.walls = this.corners.map((corner, i) => { 502 | const start = corner; 503 | const end = (i === this.corners.length - 1) ? this.corners[0] : this.corners[i + 1]; 504 | 505 | if (corner.top < LT.x && corner.left < LT.y) 506 | LT = new Point(corner.left, corner.top); 507 | 508 | if (corner.top > RB.y && corner.left > RB.y) 509 | RB = new Point(corner.left, corner.top); 510 | 511 | const w = wall([start.left, start.top, end.left, end.top], i); 512 | return w; 513 | }); 514 | 515 | this.view.add(...this.walls); 516 | this.walls.forEach(w => w.sendToBack()); 517 | this.ROOM_SIZE = { width: RB.x - LT.x, height: RB.y - LT.y }; 518 | } 519 | 520 | 521 | locateDW(dw: fabric.Group, wall: fabric.Line, x: number, y: number) { 522 | const dWall = this.directionOfWall(wall); 523 | const dDW = dw.angle % 180 === 0 ? HORIZONTAL : VERTICAL; 524 | 525 | if (dWall != dDW) { 526 | dw.angle = (dw.angle + 90) % 360; 527 | } 528 | 529 | dw.top = y, dw.left = x; 530 | const center = dw.getCenterPoint(); 531 | 532 | if (dWall === HORIZONTAL) 533 | center.y < dw.top ? dw.top += OFFSET : dw.top -= OFFSET; 534 | else 535 | center.x < dw.left ? dw.left += OFFSET : dw.left -= OFFSET; 536 | 537 | return dw; 538 | } 539 | 540 | setDWOrigin(dw: fabric.Group) { 541 | if (!dw.flipX && !dw.flipY) 542 | dw.originX = 'left', dw.originY = 'top'; 543 | else if (dw.flipX && !dw.flipY) 544 | dw.originX = 'right', dw.originY = 'top'; 545 | else if (!dw.flipX && dw.flipY) 546 | dw.originX = 'left', dw.originY = 'bottom'; 547 | else if (dw.flipX && dw.flipY) 548 | dw.originX = 'right', dw.originY = 'bottom'; 549 | return dw; 550 | } 551 | 552 | 553 | 554 | /**********************************************************************************************************/ 555 | 556 | editRoom() { 557 | this.view.getObjects().forEach(r => { 558 | if (r.name.indexOf('WALL') !== -1) { 559 | r.selectable = true; 560 | r.evented = true; 561 | } else { 562 | r.selectable = false; 563 | r.evented = false; 564 | } 565 | }); 566 | 567 | if (this.app.roomEditStates.length === 0) 568 | this.saveState(); 569 | } 570 | 571 | cancelRoomEdition() { 572 | this.view.getObjects().forEach(r => { 573 | if (r.name.indexOf('WALL') !== -1 || r.name.indexOf('CORNER') !== -1) { 574 | r.selectable = false; 575 | r.evented = false; 576 | } else { 577 | r.selectable = true; 578 | r.evented = true; 579 | } 580 | }); 581 | } 582 | 583 | handleObjectInsertion({ type, object }) { 584 | 585 | if (type === 'ROOM') { 586 | this.setRoom(object); 587 | return; 588 | } 589 | 590 | const group = _.createFurniture(type, object, this.DEFAULT_CHAIR); 591 | 592 | if (type === 'DOOR' || type === 'WINDOW') { 593 | group.originX = 'center'; 594 | group.originY = 'top'; 595 | 596 | 597 | const dws = this.filterObjects(['DOOR', 'WINDOW']); 598 | const dw = dws.length ? dws[dws.length - 1] : null; 599 | 600 | let wall, x, y; 601 | if (!dw) { 602 | wall = this.walls[0]; 603 | x = Left(wall) + RL_AISLEGAP; 604 | y = Top(wall); 605 | } else { 606 | const od = dw.angle % 180 === 0 ? HORIZONTAL : VERTICAL; 607 | 608 | let placeOnNextWall = false; 609 | wall = this.wallOfDW(dw); 610 | 611 | if (od === HORIZONTAL) { 612 | x = dw.left + dw.width + RL_AISLEGAP; 613 | y = Top(wall); 614 | if (x + group.width > Right(wall)) { 615 | placeOnNextWall = true; 616 | } 617 | } else { 618 | y = dw.top + dw.width + RL_AISLEGAP; 619 | x = Left(wall); 620 | if (y + group.width > Bottom(wall)) { 621 | placeOnNextWall = true; 622 | } 623 | } 624 | 625 | if (placeOnNextWall) { 626 | wall = this.walls[(Number(wall.name.split(':')[1]) + 1) % this.walls.length]; 627 | const nd = this.directionOfWall(wall); 628 | 629 | if (nd === HORIZONTAL) { 630 | x = Left(wall) + RL_AISLEGAP, y = Top(wall); 631 | } else { 632 | x = Left(wall), y = Top(wall) + RL_AISLEGAP; 633 | } 634 | } 635 | } 636 | 637 | this.locateDW(group, wall, x, y); 638 | 639 | group.hasBorders = false; 640 | this.view.add(group); 641 | 642 | return; 643 | } 644 | 645 | // retrieve spacing from object, use rlAisleGap if not specified 646 | const newLR = object.lrSpacing || RL_AISLEGAP; 647 | const newTB = object.tbSpacing || RL_AISLEGAP; 648 | 649 | // object groups use center as origin, so add half width and height of their reported 650 | // width and size; note that this will not account for chairs around tables, which is 651 | // intentional; they go in the specified gaps 652 | group.left = newLR + (group.width / 2) + this.room_origin; 653 | group.top = newTB + (group.height / 2) + this.room_origin; 654 | 655 | if (this.lastObject) { 656 | // retrieve spacing from object, use rlAisleGap if not specified 657 | const lastLR = this.lastObjectDefinition.lrSpacing || RL_AISLEGAP; 658 | const lastTB = this.lastObjectDefinition.tbSpacing || RL_AISLEGAP; 659 | 660 | // calculate maximum gap required by last and this object 661 | // Note: this isn't smart enough to get new row gap right when 662 | // object above had a much bigger gap, etc. We aren't fitting yet. 663 | const useLR = Math.max(newLR, lastLR), useTB = Math.max(newTB, lastTB); 664 | 665 | // using left/top vocab, though all objects are now centered 666 | const lastWidth = this.lastObjectDefinition.width || 100; 667 | const lastHeight = this.lastObjectDefinition.height || 40; 668 | 669 | let newLeft = this.lastObject.left + lastWidth + useLR; 670 | let newTop = this.lastObject.top; 671 | 672 | // make sure we fit left to right, including our required right spacing 673 | if (newLeft + group.width + newLR > this.ROOM_SIZE.width) { 674 | newLeft = newLR + (group.width / 2); 675 | newTop += lastHeight + useTB; 676 | } 677 | 678 | group.left = newLeft; 679 | group.top = newTop; 680 | 681 | if ((group.left - group.width / 2) < this.room_origin) { group.left += this.room_origin; } 682 | if ((group.top - group.height / 2) < this.room_origin) { group.top += this.room_origin; } 683 | } 684 | 685 | this.view.add(group); 686 | this.view.setActiveObject(group); 687 | 688 | this.lastObject = group; 689 | this.lastObjectDefinition = object; 690 | } 691 | 692 | 693 | /** Save current state */ 694 | saveState() { 695 | const state = this.view.toDatalessJSON(['name', 'hasControls', 'selectable', 'hasBorders', 'evented', 'hoverCursor', 'moveCursor']); 696 | this.app.saveState.next(JSON.stringify(state)); 697 | } 698 | 699 | 700 | undo() { 701 | let current = null; 702 | 703 | if (this.app.roomEdit) { 704 | const state = this.app.roomEditStates.pop(); 705 | this.app.roomEditRedoStates.push(state); 706 | current = this.app.roomEditStates[this.app.roomEditStates.length - 1]; 707 | } else { 708 | const state = this.app.states.pop(); 709 | this.app.redoStates.push(state); 710 | current = this.app.states[this.app.states.length - 1]; 711 | } 712 | 713 | this.view.clear(); 714 | this.view.loadFromJSON(current, () => { 715 | this.view.renderAll(); 716 | this.corners = this.view.getObjects().filter(obj => obj.name === 'CORNER'); 717 | this.drawRoom(); 718 | }); 719 | } 720 | 721 | 722 | /** Redo operation */ 723 | redo() { 724 | let current = null; 725 | 726 | if (this.app.roomEdit) { 727 | current = this.app.roomEditRedoStates.pop(); 728 | this.app.roomEditStates.push(current); 729 | } else { 730 | current = this.app.redoStates.pop(); 731 | this.app.states.push(current); 732 | } 733 | 734 | this.view.clear(); 735 | this.view.loadFromJSON(current, () => { 736 | this.view.renderAll(); 737 | this.corners = this.view.getObjects().filter(obj => obj.name === 'CORNER'); 738 | this.drawRoom(); 739 | }); 740 | } 741 | 742 | 743 | /** Copy operation */ 744 | copy() { 745 | if (this.app.roomEdit) { 746 | return; 747 | } 748 | const active = this.view.getActiveObject(); 749 | if (!active) { 750 | return; 751 | } 752 | active.clone(cloned => this.app.copied = cloned, ['name', 'hasControls']); 753 | } 754 | 755 | /** Paste operation */ 756 | paste() { 757 | if (!this.app.copied || this.app.roomEdit) { 758 | return; 759 | } 760 | this.app.copied.clone((cloned) => { 761 | this.view.discardActiveObject(); 762 | cloned.set({ 763 | left: cloned.left + RL_AISLEGAP, 764 | top: cloned.top + RL_AISLEGAP 765 | }); 766 | if (cloned.type === 'activeSelection') { 767 | cloned.canvas = this.view; 768 | cloned.forEachObject(obj => this.view.add(obj)); 769 | cloned.setCoords(); 770 | } else { 771 | this.view.add(cloned); 772 | } 773 | this.app.copied.top += RL_AISLEGAP; 774 | this.app.copied.left += RL_AISLEGAP; 775 | this.view.setActiveObject(cloned); 776 | this.view.requestRenderAll(); 777 | this.saveState(); 778 | }, ['name', 'hasControls']); 779 | } 780 | 781 | /** Delete operation */ 782 | delete() { 783 | if (this.app.roomEdit) { 784 | return; 785 | } 786 | this.app.selections.forEach(selection => this.view.remove(selection)); 787 | this.view.discardActiveObject(); 788 | this.view.requestRenderAll(); 789 | this.saveState(); 790 | } 791 | 792 | /** Rotate Operation */ 793 | rotate(clockwise = true) { 794 | if (this.app.roomEdit) { 795 | return; 796 | } 797 | 798 | let angle = this.CTRL_KEY_DOWN ? 90 : 15; 799 | const obj = this.view.getActiveObject(); 800 | 801 | if (!obj) { return; } 802 | 803 | if ((obj.originX !== 'center' || obj.originY !== 'center') && obj.centeredRotation) { 804 | obj.originX = 'center'; 805 | obj.originY = 'center'; 806 | obj.left += obj.width / 2; 807 | obj.top += obj.height / 2; 808 | } 809 | 810 | if (this.isDW(obj)) { 811 | angle = obj.angle + (clockwise ? 180 : -180); 812 | } else { 813 | angle = obj.angle + (clockwise ? angle : -angle); 814 | } 815 | 816 | if (angle > 360) { angle -= 360; } else if (angle < 0) { angle += 360; } 817 | 818 | obj.angle = angle; 819 | this.view.requestRenderAll(); 820 | } 821 | 822 | /** Group */ 823 | group() { 824 | if (this.app.roomEdit) { 825 | return; 826 | } 827 | 828 | const active = this.view.getActiveObject(); 829 | if (!(this.app.selections.length > 1 && active instanceof fabric.ActiveSelection)) { 830 | return; 831 | } 832 | 833 | active.toGroup(); 834 | active.lockScalingX = true, active.lockScalingY = true; 835 | this.onSelected(); 836 | this.view.renderAll(); 837 | this.saveState(); 838 | } 839 | 840 | ungroup() { 841 | if (this.app.roomEdit) { 842 | return; 843 | } 844 | 845 | const active = this.view.getActiveObject(); 846 | if (!(active && active instanceof fabric.Group)) { 847 | return; 848 | } 849 | 850 | active.toActiveSelection(); 851 | active.lockScalingX = true, active.lockScalingY = true; 852 | this.onSelected(); 853 | this.view.renderAll(); 854 | this.saveState(); 855 | } 856 | 857 | move(direction, increament = 6) { 858 | if (this.app.roomEdit) { 859 | return; 860 | } 861 | 862 | const active = this.view.getActiveObject(); 863 | if (!active) { 864 | return; 865 | } 866 | switch (direction) { 867 | case 'LEFT': 868 | active.left -= increament; 869 | break; 870 | case 'UP': 871 | active.top -= increament; 872 | break; 873 | case 'RIGHT': 874 | active.left += increament; 875 | break; 876 | case 'DOWN': 877 | active.top += increament; 878 | break; 879 | } 880 | this.view.requestRenderAll(); 881 | this.saveState(); 882 | } 883 | 884 | setZoom() { 885 | this.view.setZoom(this.app.zoom / 100); 886 | this.view.renderAll(); 887 | } 888 | 889 | placeInCenter(direction) { 890 | const active = this.view.getActiveObject(); 891 | 892 | if (!active) { 893 | return; 894 | } 895 | 896 | if (direction === 'HORIZONTAL') { 897 | active.left = this.ROOM_SIZE.width / 2 - (active.originX === 'center' ? 0 : active.width / 2); 898 | } else { 899 | active.top = this.ROOM_SIZE.height / 2 - (active.originX === 'center' ? 0 : active.height / 2); 900 | } 901 | 902 | active.setCoords(); 903 | this.view.requestRenderAll(); 904 | this.saveState(); 905 | } 906 | 907 | arrange(action: string) { 908 | const rect = this.getBoundingRect(this.app.selections); 909 | action = action.toLowerCase(); 910 | this.app.selections.forEach(s => { 911 | if (action === 'left' || action === 'right' || action === 'center') { 912 | s.left = rect[action]; 913 | } else { 914 | s.top = rect[action]; 915 | } 916 | }); 917 | this.view.renderAll(); 918 | this.saveState(); 919 | } 920 | 921 | filterObjects(names: string[]) { 922 | return this.view.getObjects().filter(obj => names.some(n => obj.name.indexOf(n) > -1)); 923 | } 924 | 925 | 926 | wallOfDW(dw: fabric.Group | fabric.Object) { 927 | const d = dw.angle % 180 === 0 ? HORIZONTAL : VERTICAL; 928 | return this.walls.find(w => Math.abs(d === HORIZONTAL ? w.top - dw.top : w.left - dw.left) === OFFSET); 929 | } 930 | 931 | directionOfWall(wall: fabric.Line) { 932 | if (wall.x1 === wall.x2) { 933 | return VERTICAL; 934 | } else { 935 | return HORIZONTAL; 936 | } 937 | } 938 | 939 | isDW(object) { 940 | return object.name.indexOf('DOOR') > -1 || object.name.indexOf('WINDOW') > -1; 941 | } 942 | 943 | getBoundingRect(objects: any[]) { 944 | let top = 9999, left = 9999, right = 0, bottom = 0; 945 | objects.forEach(obj => { 946 | if (obj.left < top) { 947 | top = obj.top; 948 | } 949 | if (obj.left < left) { 950 | left = obj.left; 951 | } 952 | if (obj.top > bottom) { 953 | bottom = obj.top; 954 | } 955 | if (obj.left > right) { 956 | right = obj.left; 957 | } 958 | }); 959 | 960 | const center = (left + right) / 2; 961 | const middle = (top + bottom) / 2; 962 | 963 | return { left, top, right, bottom, center, middle }; 964 | } 965 | 966 | saveAs(format: string) { 967 | 968 | const { right, bottom } = this.getBoundingRect(this.corners); 969 | const width = this.view.getWidth(); 970 | const height = this.view.getHeight(); 971 | 972 | this.view.setWidth(right + RL_ROOM_OUTER_SPACING); 973 | this.view.setHeight(bottom + RL_ROOM_OUTER_SPACING + 12); 974 | this.view.setBackgroundColor('white', () => { }); 975 | 976 | const credit = new fabric.Text(RL_CREDIT_TEXT, 977 | { 978 | ...RL_CREDIT_TEXT_PARAMS, 979 | left: RL_ROOM_OUTER_SPACING, 980 | top: bottom + RL_ROOM_OUTER_SPACING - RL_CREDIT_TEXT_PARAMS.fontSize 981 | } 982 | ); 983 | this.view.add(credit); 984 | this.view.discardActiveObject(); 985 | this.view.renderAll(); 986 | 987 | const restore = () => { 988 | this.view.remove(credit); 989 | this.view.setBackgroundColor('transparent', () => { }); 990 | this.view.setWidth(width); 991 | this.view.setHeight(height); 992 | this.view.renderAll(); 993 | }; 994 | 995 | if (format === 'PNG') { 996 | const canvas: any = document.getElementById('main'); 997 | canvas.toBlob((blob: Blob) => { 998 | saveAs(blob, `room_layout_${formatDate(new Date(), 'yyyy-MM-dd-hh-mm-ss', 'en')}.png`); 999 | restore(); 1000 | }); 1001 | } else if (format === 'SVG') { 1002 | const svg = this.view.toSVG(); 1003 | const blob = new Blob([svg], { type: 'image/svg+xml;charset=utf-8' }); 1004 | saveAs(blob, `room_layout_${formatDate(new Date(), 'yyyy-MM-dd-hh-mm-ss', 'en')}.svg`); 1005 | restore(); 1006 | } 1007 | } 1008 | 1009 | } 1010 | -------------------------------------------------------------------------------- /src/app/helpers.ts: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | const { Group, Rect, Line, Circle, Ellipse, Path, Polygon, Polyline, Triangle } = fabric; 3 | 4 | const 5 | RL_FILL = '#FFF', 6 | RL_STROKE = '#000', 7 | RL_PREVIEW_WIDTH = 140, 8 | RL_PREVIEW_HEIGHT = 120, 9 | RL_CHAIR_STROKE = '#999', 10 | RL_CHAIR_FILL = '#FFF', 11 | RL_CHAIR_TUCK = 6, 12 | RL_VIEW_WIDTH = 120, 13 | RL_VIEW_HEIGHT = 56, 14 | RL_FOOT = 12, 15 | RL_AISLEGAP = 12 * 3, 16 | RL_ROOM_OUTER_SPACING = 48, 17 | RL_ROOM_INNER_SPACING = 4, 18 | RL_ROOM_STROKE = '#000', 19 | RL_CORNER_FILL = '#88f', 20 | RL_UNGROUPABLES = ['CHAIR', 'MISCELLANEOUS', 'DOOR'], 21 | RL_CREDIT_TEXT = 'Created By https://github.com/ilhccc', 22 | RL_CREDIT_TEXT_PARAMS = { fontSize: 12, fontFamily: 'Arial', fill: '#999', left: 12 }; 23 | 24 | 25 | const createText = (properties) => { 26 | let { text } = properties; 27 | if (properties.direction === 'VERTICAL') { 28 | const chars = []; 29 | for (const char of text) { 30 | chars.push(char); 31 | } 32 | text = chars.join('\n'); 33 | } 34 | 35 | return new fabric.IText(text, { 36 | fontSize: properties.font_size, 37 | lineHeight: 0.8, 38 | name: properties.name, 39 | hasControls: false 40 | }); 41 | }; 42 | 43 | 44 | /** Create Basic Shape */ 45 | const createBasicShape = (part: any, stroke: string = '#aaaaaa', fill: string = 'white') => { 46 | if (part.definition.fill == null) 47 | part.definition.fill = fill; 48 | 49 | if (part.definition.stroke == null) 50 | part.definition.stroke = stroke; 51 | else if (part.definition.stroke == 'chair') 52 | part.definition.stroke = RL_CHAIR_STROKE; 53 | 54 | let fObj; 55 | 56 | switch (part.type) { 57 | case 'circle': 58 | fObj = new Circle(part.definition); 59 | break; 60 | case 'ellipse': 61 | fObj = new Ellipse(part.definition); 62 | break; 63 | case 'line': 64 | fObj = new Line(part.line, part.definition); 65 | break; 66 | case 'path': 67 | fObj = new Path(part.path, part.definition); 68 | break; 69 | case 'polygon': 70 | fObj = new Polygon(part.definition); 71 | break; 72 | case 'polyline': 73 | fObj = new Polyline(part.definition); 74 | break; 75 | case 'rect': 76 | fObj = new Rect(part.definition); 77 | break; 78 | case 'triangle': 79 | fObj = new Triangle(part.definition); 80 | break; 81 | } 82 | 83 | return (fObj); 84 | }; 85 | 86 | 87 | const createFurniture = (type: string, object, chair = {}) => { 88 | if (type === 'TABLE') { 89 | return createTable(object, chair); 90 | } else if (type === 'TEXT') { 91 | return createText(object); 92 | } else if (type === 'LAYOUT') { 93 | return object; 94 | } else { 95 | return createShape(object, RL_STROKE, RL_FILL, type); 96 | } 97 | }; 98 | 99 | /** Adding Chairs */ 100 | const createShape = (object: any, stroke = RL_CHAIR_STROKE, fill = RL_CHAIR_FILL, type: string = 'CHAIR'): fabric.Group => { 101 | const parts = object.parts.map(obj => createBasicShape(obj, stroke, fill)); 102 | const group = new Group(parts, { 103 | name: `${type}:${object.title}`, 104 | hasControls: false, 105 | originX: 'center', 106 | originY: 'center' 107 | }); 108 | 109 | return group; 110 | }; 111 | 112 | 113 | // All Create[Name]Object() functions should return a group 114 | 115 | const createTable = (def: any, RL_DEFAULT_CHAIR: any, type: string = 'TABLE') => { 116 | // tables with chairs have the chairs full-height around the table 117 | 118 | const components = []; 119 | let index = 0; 120 | 121 | // Note that we're using the provided width and height for table placement 122 | // Issues may arise if rendered shape is larger/smaller, since it's positioned from center point 123 | const chairWidth = RL_DEFAULT_CHAIR.width; 124 | const chairHeight = RL_DEFAULT_CHAIR.height; 125 | const tableLeft = def.leftChairs > 0 ? (chairHeight - RL_CHAIR_TUCK) : 0; 126 | const tableTop = (chairHeight - RL_CHAIR_TUCK); 127 | 128 | if (def.shape == 'circle') { 129 | 130 | const origin_x = def.width / 2 + chairHeight - RL_CHAIR_TUCK; 131 | const origin_y = def.width / 2 + chairHeight - RL_CHAIR_TUCK; 132 | const x2 = origin_x; 133 | const y2 = 0 + chairHeight / 2; 134 | 135 | const rotation_origin = new fabric.Point(origin_x, origin_y); 136 | 137 | const tableRadius = def.width / 2; 138 | const radius = def.width / 2 + chairHeight; // outer radius of whole shape unit 139 | let angle = 0; 140 | const angleIncrement = 360 / (def.chairs > 0 ? def.chairs : 1); 141 | 142 | for (let x = 0; x < def.chairs; ++x) { 143 | // Note that width and height are the same for circle tables 144 | // width of whole area when done 145 | const width = def.width + chairHeight - (RL_CHAIR_TUCK * 2); 146 | 147 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 148 | 149 | const angle_radians = fabric.util.degreesToRadians(angle); 150 | const end = fabric.util.rotatePoint(new fabric.Point(x2, y2), rotation_origin, angle_radians); 151 | components[index].left = end.x; 152 | components[index].top = end.y; 153 | components[index].angle = (angle + 180 > 360) ? (angle - 180) : (angle + 180); 154 | index++; 155 | angle += angleIncrement; 156 | } 157 | 158 | const tableCircle = { 159 | left: origin_x, 160 | top: origin_y, 161 | radius: tableRadius, 162 | fill: RL_FILL, 163 | stroke: RL_STROKE, 164 | originX: 'center', 165 | originY: 'center', 166 | name: 'DESK' 167 | }; 168 | components[index] = new fabric.Circle(tableCircle); 169 | 170 | } else if (def.shape == 'rect') { 171 | const tableRect = { 172 | width: def.width, 173 | height: def.height, 174 | fill: RL_FILL, 175 | stroke: RL_STROKE, 176 | name: 'DESK' 177 | }; 178 | 179 | // calculate gap between chairs, with extra for gap to end of table 180 | let gap = 0, firstOffset = 0, leftOffset = 0, topOffset = 0; 181 | 182 | // top chair row 183 | // Note that chairs 'look up' by default, so the bottom row isn't rotated 184 | // and the top row is. 185 | gap = (def.width - (def.topChairs * chairWidth)) / (def.topChairs + 1); 186 | firstOffset = gap + tableLeft; 187 | leftOffset = firstOffset; 188 | topOffset = 0; 189 | 190 | for (let x = 0; x < def.topChairs; x++) { 191 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 192 | components[index].angle = -180; 193 | components[index].left = leftOffset + chairWidth / 2; 194 | components[index].top = topOffset + chairHeight / 2; 195 | index++; 196 | 197 | leftOffset += (chairWidth + gap); 198 | } 199 | 200 | // bottom chair row 201 | gap = (def.width - (def.bottomChairs * chairWidth)) / (def.bottomChairs + 1); 202 | firstOffset = gap + tableLeft; 203 | leftOffset = firstOffset; 204 | topOffset = tableRect.height + chairHeight - (RL_CHAIR_TUCK * 2); 205 | 206 | for (let x = 0; x < def.bottomChairs; x++) { 207 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 208 | components[index].left = leftOffset + chairWidth / 2; 209 | components[index].top = topOffset + chairWidth / 2; 210 | ++index; 211 | 212 | leftOffset += (chairWidth + gap); 213 | } 214 | 215 | // left chair row 216 | gap = (def.height - (def.leftChairs * chairWidth)) / (def.leftChairs + 1); 217 | leftOffset = chairWidth / 2; 218 | topOffset = tableTop + gap + chairWidth / 2; // top of table plus first gap, then to center 219 | 220 | for (let x = 0; x < def.leftChairs; x++) { 221 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 222 | components[index].angle = 90; 223 | components[index].left = leftOffset; 224 | components[index].top = topOffset; 225 | ++index; 226 | 227 | topOffset += (chairWidth + gap); 228 | } 229 | 230 | // right chair row 231 | gap = (def.height - (def.rightChairs * chairWidth)) / (def.rightChairs + 1); 232 | leftOffset = tableRect.width + chairWidth / 2; 233 | topOffset = tableTop + gap + chairWidth / 2; // top of table plus first gap, then to center 234 | 235 | for (let x = 0; x < def.rightChairs; x++) { 236 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 237 | components[index].angle = -90; 238 | components[index].left = leftOffset + chairHeight - (RL_CHAIR_TUCK * 2); 239 | components[index].top = topOffset; 240 | ++index; 241 | 242 | topOffset += (chairWidth + gap); 243 | } 244 | 245 | // add table on top of chairs 246 | components[index] = new fabric.Rect(tableRect); 247 | components[index].left = tableLeft; 248 | components[index].top = tableTop; 249 | } 250 | 251 | const tableGroup = new fabric.Group(components, { 252 | left: 0, 253 | top: 0, 254 | hasControls: false, 255 | // set origin for all groups to center 256 | originX: 'center', 257 | originY: 'center', 258 | name: `${type}:${def.title}` 259 | }); 260 | 261 | return tableGroup; 262 | }; 263 | 264 | export { 265 | createBasicShape, 266 | createTable, 267 | createShape, 268 | createText, 269 | createFurniture, 270 | 271 | RL_FILL, 272 | RL_STROKE, 273 | RL_CHAIR_STROKE, 274 | RL_CHAIR_FILL, 275 | RL_CHAIR_TUCK, 276 | RL_PREVIEW_HEIGHT, 277 | RL_PREVIEW_WIDTH, 278 | RL_VIEW_WIDTH, 279 | RL_VIEW_HEIGHT, 280 | RL_FOOT, 281 | RL_AISLEGAP, 282 | RL_ROOM_OUTER_SPACING, 283 | RL_ROOM_INNER_SPACING, 284 | RL_ROOM_STROKE, 285 | RL_CORNER_FILL, 286 | RL_UNGROUPABLES, 287 | RL_CREDIT_TEXT, 288 | RL_CREDIT_TEXT_PARAMS 289 | }; 290 | -------------------------------------------------------------------------------- /src/app/models/furnishings.ts: -------------------------------------------------------------------------------- 1 | import { RL_ROOM_INNER_SPACING as WT } from '../helpers'; // WT = Wall Thickness 2 | 3 | const FURNISHINGS = { 4 | 'title': 'Faithlife Room Layout Furniture Library', 5 | 'rooms': [ 6 | { 7 | 'title': '13\' x 17\' Small Conference Room', 8 | 'width': 156, 9 | 'height': 204 10 | }, 11 | { 12 | 'title': '15\' x 26\' Medium Conference Room', 13 | 'width': 180, 14 | 'height': 312 15 | }, 16 | { 17 | 'title': '18\' x 21\' Medium Conference Room', 18 | 'width': 216, 19 | 'height': 252 20 | }, 21 | { 22 | 'title': '20\' x 10\'', 23 | 'width': 240, 24 | 'height': 120 25 | }, 26 | { 27 | 'title': '16\' x 12\'', 28 | 'width': 192, 29 | 'height': 144 30 | }, 31 | { 32 | 'title': 'Gym (Regulation)', 33 | 'width': 1320, 34 | 'height': 720 35 | }, 36 | { 37 | 'title': 'Gym (High School)', 38 | 'width': 1008, 39 | 'height': 600 40 | }, 41 | { 42 | 'title': '40\' x 20\'', 43 | 'width': 480, 44 | 'height': 240 45 | } 46 | ], 47 | 'tables': [ 48 | { 49 | 'title': '54" Round Folding', 50 | 'width': 54, 51 | 'height': 54, 52 | 'lrSpacing': 54, 53 | 'tbSpacing': 54, 54 | 'shape': 'circle', 55 | 'chairs': 6 56 | }, 57 | { 58 | 'title': '60" Round Folding', 59 | 'width': 60, 60 | 'height': 60, 61 | 'lrSpacing': 60, 62 | 'tbSpacing': 60, 63 | 'shape': 'circle', 64 | 'chairs': 8 65 | }, 66 | { 67 | 'title': '72" Round Folding', 68 | 'width': 72, 69 | 'height': 72, 70 | 'lrSpacing': 72, 71 | 'tbSpacing': 72, 72 | 'shape': 'circle', 73 | 'chairs': 8 74 | }, 75 | { 76 | 'title': '6\' x 30" Folding', 77 | 'width': 72, 78 | 'height': 30, 79 | 'lrSpacing': 24, 80 | 'tbSpacing': 60, 81 | 'shape': 'rect', 82 | 'topChairs': 3, 83 | 'bottomChairs': 3, 84 | 'leftChairs': 0, 85 | 'rightChairs': 0 86 | }, 87 | { 88 | 'title': '8\' x 30" Folding', 89 | 'width': 96, 90 | 'height': 30, 91 | 'lrSpacing': 24, 92 | 'tbSpacing': 60, 93 | 'shape': 'rect', 94 | 'topChairs': 4, 95 | 'bottomChairs': 4, 96 | 'leftChairs': 0, 97 | 'rightChairs': 0 98 | }, 99 | { 100 | 'title': '8\' x 40" Family', 101 | 'width': 96, 102 | 'height': 40, 103 | 'lrSpacing': 60, 104 | 'tbSpacing': 60, 105 | 'shape': 'rect', 106 | 'topChairs': 4, 107 | 'bottomChairs': 3, 108 | 'leftChairs': 1, 109 | 'rightChairs': 1 110 | }, 111 | { 112 | 'title': '8\' x 18" Classroom', 113 | 'width': 96, 114 | 'height': 18, 115 | 'lrSpacing': 24, 116 | 'tbSpacing': 36, 117 | 'shape': 'rect', 118 | 'topChairs': 0, 119 | 'bottomChairs': 4, 120 | 'leftChairs': 0, 121 | 'rightChairs': 0 122 | }, 123 | { 124 | 'title': '6\' x 18" Classroom', 125 | 'width': 72, 126 | 'height': 18, 127 | 'lrSpacing': 24, 128 | 'tbSpacing': 36, 129 | 'shape': 'rect', 130 | 'topChairs': 0, 131 | 'bottomChairs': 3, 132 | 'leftChairs': 0, 133 | 'rightChairs': 0 134 | } 135 | ], 136 | 'chairs': [ 137 | { 138 | 'title': 'Generic', 139 | 'width': 18, 140 | 'height': 20, 141 | 'lrSpacing': 2, 142 | 'tbSpacing': 12, 143 | 'parts': [ 144 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 18, height: 20 } }, 145 | { 'type': 'rect', 'definition': { left: 0, top: 18, width: 18, height: 2 } } 146 | ] 147 | }, 148 | { 149 | 'title': '14" Children\'s', 150 | 'width': 14, 151 | 'height': 14, 152 | 'lrSpacing': 2, 153 | 'tbSpacing': 12, 154 | 'parts': [ 155 | { 'type': 'circle', 'definition': { originX: 'center', originY: 'center', radius: 7 } }, 156 | { 'type': 'circle', 'definition': { originX: 'center', originY: 'center', radius: 4 } } 157 | ] 158 | }, 159 | { 160 | 'title': '18" Folding', 161 | 'width': 18, 162 | 'height': 18, 163 | 'lrSpacing': 2, 164 | 'tbSpacing': 12, 165 | 'parts': [ 166 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 18, height: 18 } }, 167 | { 'type': 'rect', 'definition': { left: 0, top: 16, width: 18, height: 2 } } 168 | ] 169 | }, 170 | { 171 | 'title': '18" Stacking', 172 | 'width': 18.375, 173 | 'height': 23.25, 174 | 'lrSpacing': 2, 175 | 'tbSpacing': 12.75, 176 | 'parts': [ 177 | { 'type': 'rect', 'definition': { width: 18.375, height: 23.25 } }, 178 | { 'type': 'rect', 'definition': { width: 18.375, height: 4, top: 19.25 } }, 179 | { 'type': 'rect', 'definition': { width: 18.375, height: 2, top: 21.25 } } 180 | ] 181 | }, 182 | { 183 | 'title': '20" Pew Stacker', 184 | 'source': 'http://sanctuaryseating.com/church-chairs/impressions-series/model-7027/', 185 | 'width': 20.25, 186 | 'height': 26.3, 187 | 'lrSpacing': 1, 188 | 'tbSpacing': 12, 189 | 'parts': [ 190 | { 'type': 'rect', 'definition': { width: 20.25, height: 26.3 } }, 191 | { 'type': 'rect', 'definition': { width: 20.25, height: 8, top: 18.3 } }, 192 | { 'type': 'rect', 'definition': { width: 20.25, height: 6, top: 20.3 } } 193 | ] 194 | }, 195 | { 196 | 'title': '22" Pew Stacker', 197 | 'source': 'http://sanctuaryseating.com/church-chairs/impressions-series/model-7227/', 198 | 'width': 22, 199 | 'height': 26.3, 200 | 'lrSpacing': 1, 201 | 'tbSpacing': 12, 202 | 'parts': [ 203 | { 'type': 'rect', 'definition': { width: 22, height: 26.3 } }, 204 | { 'type': 'rect', 'definition': { width: 22, height: 8, top: 18.3 } }, 205 | { 'type': 'rect', 'definition': { width: 22, height: 6, top: 20.3 } } 206 | ] 207 | }, 208 | { 209 | 'title': '22" Square', 210 | 'width': 22, 211 | 'height': 22, 212 | 'lrSpacing': 2, 213 | 'tbSpacing': 12, 214 | 'parts': [ 215 | { 'type': 'rect', 'definition': { width: 22, height: 22 } }, 216 | { 'type': 'rect', 'definition': { width: 22, height: 6, top: 16 } } 217 | ] 218 | } 219 | ], 220 | 'miscellaneous': [ 221 | { 222 | 'title': 'Rectangle', 223 | 'width': 36, 224 | 'height': 12, 225 | 'flexible': true, 226 | 'parts': [ 227 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 36, height: 12 } } 228 | ] 229 | }, 230 | { 231 | 'title': '5\' x 23" Upright Piano', 232 | 'width': 60, 233 | 'height': 23, 234 | 'parts': [ 235 | { 'type': 'rect', 'definition': { left: 15, top: 16, width: 30, height: 14, stroke: 'chair' } }, // bench 236 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 60, height: 23 } }, // base 237 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 6, height: 23 } }, // side pillar 238 | { 'type': 'rect', 'definition': { left: 54, top: 0, width: 6, height: 23 } }, // side pillar 239 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 60, height: 13 } } // top 240 | ] 241 | }, 242 | { 243 | 'title': '6\' Grand Piano', 244 | 'width': 58, 245 | 'height': 84, 246 | 'parts': [ 247 | { 'type': 'rect', 'definition': { left: 11, top: 77, width: 36, height: 14, stroke: 'chair' } }, // bench 248 | { 'type': 'path', 'path': 'M 0,84 L 0,36 C 0,2 42,2 42,32 S 58,50 58,72 L 58,84 z', 'definition': {} }, // outline 249 | { 'type': 'rect', 'definition': { left: 0, top: 74, width: 58, height: 10 } }, // keyboard area 250 | { 'type': 'rect', 'definition': { left: 0, top: 74, width: 6, height: 10 } }, // side pillar 251 | { 'type': 'rect', 'definition': { left: 52, top: 74, width: 6, height: 10 } } // side pillar 252 | ] 253 | }, 254 | { 255 | 'title': '7\' Grand Piano', 256 | 'width': 62, 257 | 'height': 84, 258 | 'parts': [ 259 | // { "type": "rect", "definition": { left: 13, top: 77, width: 36, height: 14, stroke: "chair" } }, // bench 260 | { 'type': 'path', 'path': 'M 0,84 L 0,24 C 0,-10 46,-10 46,26 S 62,50 62,72 L 62,84 z', 'definition': {} }, // outline 261 | { 'type': 'rect', 'definition': { left: 0, top: 74, width: 62, height: 10 } }, // keyboard area 262 | // { "type": "rect", "definition": { left: 0, top: 74, width: 6, height: 10 } }, // side pillar 263 | // { "type": "rect", "definition": { left: 56, top: 74, width: 6, height: 10 } } // side pillar 264 | ] 265 | }, 266 | { 267 | 'title': '8\' Grand Piano', 268 | 'width': 62, 269 | 'height': 94, 270 | 'parts': [ 271 | { 'type': 'rect', 'definition': { left: 13, top: 87, width: 36, height: 14, stroke: 'chair' } }, // bench 272 | { 'type': 'path', 'path': 'M 0,94 L 0,24 C 0,-10 46,-10 46,28 S 62,62 62,78 L 62,94 z', 'definition': {} }, // outline 273 | { 'type': 'rect', 'definition': { left: 0, top: 84, width: 62, height: 10 } }, // keyboard area 274 | { 'type': 'rect', 'definition': { left: 0, top: 84, width: 6, height: 10 } }, // side pillar 275 | { 'type': 'rect', 'definition': { left: 56, top: 84, width: 6, height: 10 } } // side pillar 276 | ] 277 | }, 278 | { 279 | 'title': 'Awana Game Square', // note: calculation from Excel spreadsheet 280 | 'width': 200, 281 | 'height': 200, 282 | 'parts': [ 283 | { 'type': 'circle', 'definition': { left: 240, top: 240, strokeWidth: 2, stroke: '#aaaaaa', originX: 'center', originY: 'center', radius: 180 } }, // game circle 284 | 285 | // blue 286 | { 'type': 'path', 'path': 'M329.8,82.32L340.41,71.71', 'definition': { strokeWidth: 2, stroke: '#aaaaaa' } }, // circle starting line 287 | { 'type': 'path', 'path': 'M480,0L480,480M480,0L240,240M299.4,180.6L299.4,299.4M278.19,193.33L286.67,201.81M273.94,138.18L341.82,206.06M312.13,159.39L320.61,167.87M320.61,150.91L329.09,159.39M329.1,142.42L337.58,150.9M337.58,133.94L346.06,142.42M346.07,125.45L354.55,133.93', 'definition': { stroke: 'blue', strokeWidth: 2 } }, 288 | 289 | // green 290 | { 'type': 'path', 'path': 'M408.29,340.41L397.68,329.8', 'definition': { strokeWidth: 2, stroke: '#aaaaaa' } }, // circle starting line 291 | { 'type': 'path', 'path': 'M0,480L480,480M240,240L480,480M180.6,299.4L299.4,299.4M278.19,286.67L286.67,278.19M273.94,341.82L341.82,273.94M312.13,320.61L320.61,312.13M320.61,329.09L329.09,320.61M329.1,337.58L337.58,329.1M337.58,346.06L346.06,337.58M346.07,354.55L354.55,346.07', 'definition': { stroke: 'green', strokeWidth: 2 } }, 292 | 293 | // yellow 294 | { 'type': 'path', 'path': 'M150.2,397.68L139.59,408.29', 'definition': { strokeWidth: 2, stroke: '#aaaaaa' } }, // circle starting line 295 | { 'type': 'path', 'path': 'M0,480L0,0M0,480L240,240M180.6,299.4L180.6,180.6M201.81,286.67L193.33,278.19M206.06,341.82L138.18,273.94M167.87,320.61L159.39,312.13M159.39,329.09L150.91,320.61M150.9,337.58L142.42,329.1M142.42,346.06L133.94,337.58M133.93,354.55L125.45,346.07', 'definition': { stroke: 'yellow', strokeWidth: 2 } }, 296 | 297 | // draw red last because it's stronger and on top, and it'll capture corners this way 298 | // red 299 | { 'type': 'path', 'path': 'M82.32,150.2L71.71,139.59', 'definition': { strokeWidth: 2, stroke: '#aaaaaa' } }, // circle starting line 300 | { 'type': 'path', 'path': 'M0,0L480,0M0,0L240,240M180.6,180.6L299.4,180.6M193.33,201.81L201.81,193.33M138.18,206.06L206.06,138.18M159.39,167.87L167.87,159.39M150.91,159.39L159.39,150.91M142.42,150.9L150.9,142.42M133.94,142.42L142.42,133.94M125.45,133.93L133.93,125.45', 'definition': { stroke: 'red', strokeWidth: 2 } } 301 | 302 | // { "type": "line", "line": [ 0,0,100,0 ], "definition": { stroke: "#ffff00", strokeWidth: "2" } }, 303 | // { "type": "line", "line": [ 0,0,100,100 ], "definition": { stroke: "#00ffff", strokeWidth: "2" } } 304 | ] 305 | } 306 | ], 307 | doors: [ 308 | { 309 | title: 'Narrow Door (28" wide)', 310 | parts: [ 311 | { type: 'rect', definition: { left: 0, width: 28, top: 0, height: WT, fill: 'white', strokeWidth: 0, originX: 'left', originY: 'top' } }, 312 | { type: 'line', line: [0, 0, 0, `${WT + 28}`], definition: { stroke: 'black', strokeWidth: 1 } }, 313 | { type: 'path', path: `M 0 ${WT + 28} Q 28, ${WT + 28}, 28, ${WT}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 314 | ] 315 | }, { 316 | title: 'Normal Door (32" wide)', 317 | parts: [ 318 | { type: 'rect', definition: { left: 0, width: 32, top: 0, height: WT, fill: 'white', strokeWidth: 0, originX: 'left', originY: 'top' } }, 319 | { type: 'line', line: [0, 0, 0, `${WT + 32}`], definition: { stroke: 'black', strokeWidth: 1 } }, 320 | { type: 'path', path: `M 0 ${WT + 32} Q 32, ${WT + 32}, 32, ${WT}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 321 | ] 322 | }, { 323 | title: 'Wide Door (36" wide)', 324 | parts: [ 325 | { type: 'rect', definition: { left: 0, width: 36, top: 0, height: WT, fill: 'white', strokeWidth: 0, originX: 'left', originY: 'top' } }, 326 | { type: 'line', line: [0, 0, 0, `${WT + 36}`], definition: { stroke: 'black', strokeWidth: 1 } }, 327 | { type: 'path', path: `M 0 ${WT + 36} Q 36, ${WT + 36}, 36, ${WT}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 328 | ] 329 | }, { 330 | title: 'Double Doors (64" wide)', 331 | parts: [ 332 | { type: 'rect', definition: { left: 0, width: 64, top: 0, height: WT, fill: 'white', strokeWidth: 0, originX: 'left', originY: 'top' } }, 333 | { type: 'line', line: [0, 0, 0, `${WT + 32}`], definition: { stroke: 'black', strokeWidth: 1 } }, 334 | { type: 'path', path: `M 0 ${WT + 32} Q 32, ${WT + 32}, 32, ${WT}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 335 | { type: 'line', line: [64, 0, 64, `${WT + 32}`], definition: { stroke: 'black', strokeWidth: 1 } }, 336 | { type: 'path', path: `M 32 ${WT} Q 32, ${WT + 32}, 64, ${WT + 32}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 337 | ] 338 | } 339 | ], 340 | windows: [ 341 | { 342 | title: '2’ Window (24” wide)', 343 | parts: [ 344 | { type: 'rect', definition: { left: 0, width: 24, top: 0, height: WT - 1, fill: 'white', strokeWidth: 1, originX: 'left', originY: 'top' } }, 345 | ] 346 | }, 347 | { 348 | title: '3’ Window (36” wide)', 349 | parts: [ 350 | { type: 'rect', definition: { left: 0, width: 36, top: 0, height: WT - 1, fill: 'white', strokeWidth: 1, originX: 'left', originY: 'top' } }, 351 | ] 352 | }, 353 | { 354 | title: '4’ Window (48” wide)', 355 | parts: [ 356 | { type: 'rect', definition: { left: 0, width: 48, top: 0, height: WT - 1, fill: 'white', strokeWidth: 1, originX: 'left', originY: 'top' } }, 357 | ] 358 | }, 359 | { 360 | title: '6’ Window (72” wide)', 361 | parts: [ 362 | { type: 'rect', definition: { left: 0, width: 72, top: 0, height: WT - 1, fill: 'white', strokeWidth: 1, originX: 'left', originY: 'top' } }, 363 | ] 364 | } 365 | ] 366 | }; 367 | 368 | export { FURNISHINGS }; 369 | -------------------------------------------------------------------------------- /src/app/models/index.ts: -------------------------------------------------------------------------------- 1 | export interface Furnish { 2 | title: string; 3 | width: number; 4 | height: number; 5 | lrSpacing: number; 6 | tbSpacing: number; 7 | } 8 | 9 | export interface Part { 10 | type: 'Canvas' | 'Group' | 'Rect' | 'Line' | 'Circle' | 'Ellipse' | 'Path' | 'Polygon' | 'Polyline' | 'Triangle'; 11 | definition?: { 12 | left?: number; 13 | top?: number; 14 | width?: number; 15 | height?: number; 16 | originX?: string; 17 | originY?: string; 18 | radius?: number; 19 | strokeWidth?: number; 20 | stroke?: string; 21 | fill?: string; 22 | }; 23 | path?: string; 24 | line?: number[]; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/app/shared/components/index.ts: -------------------------------------------------------------------------------- 1 | export { ZoomComponent } from "./zoom/zoom.component"; 2 | -------------------------------------------------------------------------------- /src/app/shared/components/zoom/zoom.component.html: -------------------------------------------------------------------------------- 1 |
2 | 5 | {{ zoom }}% 6 | 9 |
10 | -------------------------------------------------------------------------------- /src/app/shared/components/zoom/zoom.component.scss: -------------------------------------------------------------------------------- 1 | .zoom-widget { 2 | border: 1px solid #ddd; 3 | background: white; 4 | border-radius: 8px; 5 | fa-icon { 6 | font-size: 9px; 7 | } 8 | button { 9 | line-height: 30px; 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/app/shared/components/zoom/zoom.component.ts: -------------------------------------------------------------------------------- 1 | import { Component, OnInit, Input, Output, EventEmitter } from '@angular/core'; 2 | import { faMinus, faPlus } from '@fortawesome/free-solid-svg-icons'; 3 | 4 | @Component({ 5 | selector: 'app-zoom', 6 | templateUrl: './zoom.component.html', 7 | styleUrls: ['./zoom.component.scss'] 8 | }) 9 | export class ZoomComponent implements OnInit { 10 | 11 | @Input() 12 | zoom = 100; 13 | 14 | @Output() 15 | zoomChange = new EventEmitter(); 16 | 17 | // icons 18 | faMinus = faMinus; 19 | faPlus = faPlus; 20 | 21 | constructor() { } 22 | 23 | ngOnInit() { 24 | } 25 | 26 | zoomIn() { 27 | if (this.zoom >= 150) { 28 | return; 29 | } 30 | this.zoom += 10; 31 | this.zoomChange.emit(this.zoom); 32 | } 33 | 34 | zoomOut() { 35 | if (this.zoom <= 20) { 36 | return; 37 | } 38 | this.zoom -= 10; 39 | this.zoomChange.emit(this.zoom); 40 | } 41 | 42 | } 43 | -------------------------------------------------------------------------------- /src/app/shared/helpers.ts: -------------------------------------------------------------------------------- 1 | import { fabric } from 'fabric'; 2 | const { Group, Rect, Line, Circle, Ellipse, Path, Polygon, Polyline, Triangle, Point } = fabric; 3 | 4 | const 5 | RL_FILL = '#FFF', 6 | RL_STROKE = '#000', 7 | RL_PREVIEW_WIDTH = 140, 8 | RL_PREVIEW_HEIGHT = 120, 9 | RL_CHAIR_STROKE = '#999', 10 | RL_CHAIR_FILL = '#FFF', 11 | RL_CHAIR_TUCK = 6, 12 | RL_VIEW_WIDTH = 120, 13 | RL_VIEW_HEIGHT = 56, 14 | RL_FOOT = 12, 15 | RL_AISLEGAP = 12 * 3, 16 | RL_ROOM_OUTER_SPACING = 48, 17 | RL_ROOM_INNER_SPACING = 4, 18 | RL_ROOM_STROKE = '#000', 19 | RL_CORNER_FILL = '#88f', 20 | RL_UNGROUPABLES = ['CHAIR', 'MISCELLANEOUS', 'DOOR'], 21 | RL_CREDIT_TEXT = 'Created by https://github.com/ilhccc', 22 | RL_CREDIT_TEXT_PARAMS = { fontSize: 12, fontFamily: 'Arial', fill: '#999', left: 12 }; 23 | 24 | // All Create[Name]Object() functions should return a group 25 | 26 | const createTable = (def: any, RL_DEFAULT_CHAIR: any, type: string = 'TABLE') => { 27 | // tables with chairs have the chairs full-height around the table 28 | 29 | const components = []; 30 | let index = 0; 31 | 32 | // Note that we're using the provided width and height for table placement 33 | // Issues may arise if rendered shape is larger/smaller, since it's positioned from center point 34 | const chairWidth = RL_DEFAULT_CHAIR.width; 35 | const chairHeight = RL_DEFAULT_CHAIR.height; 36 | const tableLeft = def.leftChairs > 0 ? (chairHeight - RL_CHAIR_TUCK) : 0; 37 | const tableTop = (chairHeight - RL_CHAIR_TUCK); 38 | 39 | if (def.shape == 'circle') { 40 | 41 | const origin_x = def.width / 2 + chairHeight - RL_CHAIR_TUCK; 42 | const origin_y = def.width / 2 + chairHeight - RL_CHAIR_TUCK; 43 | const x2 = origin_x; 44 | const y2 = 0 + chairHeight / 2; 45 | 46 | const rotation_origin = new fabric.Point(origin_x, origin_y); 47 | 48 | const tableRadius = def.width / 2; 49 | const radius = def.width / 2 + chairHeight; // outer radius of whole shape unit 50 | let angle = 0; 51 | const angleIncrement = 360 / (def.chairs > 0 ? def.chairs : 1); 52 | 53 | for (let x = 0; x < def.chairs; ++x) { 54 | // Note that width and height are the same for circle tables 55 | // width of whole area when done 56 | const width = def.width + chairHeight - (RL_CHAIR_TUCK * 2); 57 | 58 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 59 | 60 | const angle_radians = fabric.util.degreesToRadians(angle); 61 | const end = fabric.util.rotatePoint(new fabric.Point(x2, y2), rotation_origin, angle_radians); 62 | components[index].left = end.x; 63 | components[index].top = end.y; 64 | components[index].angle = (angle + 180 > 360) ? (angle - 180) : (angle + 180); 65 | index++; 66 | angle += angleIncrement; 67 | } 68 | 69 | const tableCircle = { 70 | left: origin_x, 71 | top: origin_y, 72 | radius: tableRadius, 73 | fill: RL_FILL, 74 | stroke: RL_STROKE, 75 | originX: 'center', 76 | originY: 'center', 77 | name: 'DESK' 78 | }; 79 | components[index] = new fabric.Circle(tableCircle); 80 | 81 | } else if (def.shape == 'rect') { 82 | const tableRect = { 83 | width: def.width, 84 | height: def.height, 85 | fill: RL_FILL, 86 | stroke: RL_STROKE, 87 | name: 'DESK' 88 | }; 89 | 90 | // calculate gap between chairs, with extra for gap to end of table 91 | let gap = 0, firstOffset = 0, leftOffset = 0, topOffset = 0; 92 | 93 | // top chair row 94 | // Note that chairs 'look up' by default, so the bottom row isn't rotated 95 | // and the top row is. 96 | gap = (def.width - (def.topChairs * chairWidth)) / (def.topChairs + 1); 97 | firstOffset = gap + tableLeft; 98 | leftOffset = firstOffset; 99 | topOffset = 0; 100 | 101 | for (let x = 0; x < def.topChairs; x++) { 102 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 103 | components[index].angle = -180; 104 | components[index].left = leftOffset + chairWidth / 2; 105 | components[index].top = topOffset + chairHeight / 2; 106 | index++; 107 | 108 | leftOffset += (chairWidth + gap); 109 | } 110 | 111 | // bottom chair row 112 | gap = (def.width - (def.bottomChairs * chairWidth)) / (def.bottomChairs + 1); 113 | firstOffset = gap + tableLeft; 114 | leftOffset = firstOffset; 115 | topOffset = tableRect.height + chairHeight - (RL_CHAIR_TUCK * 2); 116 | 117 | for (let x = 0; x < def.bottomChairs; x++) { 118 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 119 | components[index].left = leftOffset + chairWidth / 2; 120 | components[index].top = topOffset + chairWidth / 2; 121 | ++index; 122 | 123 | leftOffset += (chairWidth + gap); 124 | } 125 | 126 | // left chair row 127 | gap = (def.height - (def.leftChairs * chairWidth)) / (def.leftChairs + 1); 128 | leftOffset = chairWidth / 2; 129 | topOffset = tableTop + gap + chairWidth / 2; // top of table plus first gap, then to center 130 | 131 | for (let x = 0; x < def.leftChairs; x++) { 132 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 133 | components[index].angle = 90; 134 | components[index].left = leftOffset; 135 | components[index].top = topOffset; 136 | ++index; 137 | 138 | topOffset += (chairWidth + gap); 139 | } 140 | 141 | // right chair row 142 | gap = (def.height - (def.rightChairs * chairWidth)) / (def.rightChairs + 1); 143 | leftOffset = tableRect.width + chairWidth / 2; 144 | topOffset = tableTop + gap + chairWidth / 2; // top of table plus first gap, then to center 145 | 146 | for (let x = 0; x < def.rightChairs; x++) { 147 | components[index] = createShape(RL_DEFAULT_CHAIR, RL_CHAIR_STROKE, RL_CHAIR_FILL); 148 | components[index].angle = -90; 149 | components[index].left = leftOffset + chairHeight - (RL_CHAIR_TUCK * 2); 150 | components[index].top = topOffset; 151 | ++index; 152 | 153 | topOffset += (chairWidth + gap); 154 | } 155 | 156 | // add table on top of chairs 157 | components[index] = new fabric.Rect(tableRect); 158 | components[index].left = tableLeft; 159 | components[index].top = tableTop; 160 | } 161 | 162 | const tableGroup = new fabric.Group(components, { 163 | left: 0, 164 | top: 0, 165 | hasControls: false, 166 | // set origin for all groups to center 167 | originX: 'center', 168 | originY: 'center', 169 | name: `${type}:${def.title}` 170 | }); 171 | 172 | return tableGroup; 173 | }; 174 | 175 | const createText = (properties) => { 176 | let { text } = properties; 177 | if (properties.direction === 'VERTICAL') { 178 | const chars = []; 179 | for (const char of text) { 180 | chars.push(char); 181 | } 182 | text = chars.join('\n'); 183 | } 184 | 185 | return new fabric.IText(text, { 186 | fontSize: properties.font_size, 187 | lineHeight: 0.8, 188 | name: properties.name, 189 | hasControls: false 190 | }); 191 | }; 192 | 193 | 194 | /** Create Basic Shape */ 195 | const createBasicShape = (part: any, stroke: string = '#aaaaaa', fill: string = 'white') => { 196 | if (part.definition.fill == null) { 197 | part.definition.fill = fill; 198 | } if (part.definition.stroke == null) { 199 | part.definition.stroke = stroke; 200 | } else if (part.definition.stroke === 'chair') { 201 | part.definition.stroke = RL_CHAIR_STROKE; 202 | } 203 | 204 | let fObj; 205 | 206 | switch (part.type) { 207 | case 'circle': 208 | fObj = new Circle(part.definition); 209 | break; 210 | case 'ellipse': 211 | fObj = new Ellipse(part.definition); 212 | break; 213 | case 'line': 214 | fObj = new Line(part.line, part.definition); 215 | break; 216 | case 'path': 217 | fObj = new Path(part.path, part.definition); 218 | break; 219 | case 'polygon': 220 | fObj = new Polygon(part.definition); 221 | break; 222 | case 'polyline': 223 | fObj = new Polyline(part.definition); 224 | break; 225 | case 'rect': 226 | fObj = new Rect(part.definition); 227 | break; 228 | case 'triangle': 229 | fObj = new Triangle(part.definition); 230 | break; 231 | } 232 | 233 | return (fObj); 234 | }; 235 | 236 | 237 | /** Adding Chairs */ 238 | const createShape = (object: any, stroke = RL_CHAIR_STROKE, fill = RL_CHAIR_FILL, type: string = 'CHAIR'): fabric.Group => { 239 | const parts = object.parts.map(obj => createBasicShape(obj, stroke, fill)); 240 | const group = new Group(parts, { 241 | name: `${type}:${object.title}`, 242 | hasControls: false, 243 | originX: 'center', 244 | originY: 'center' 245 | }); 246 | 247 | return group; 248 | }; 249 | 250 | const createFurniture = (type: string, object, chair = {}) => { 251 | if (type === 'TABLE') { 252 | return createTable(object, chair); 253 | } else if (type === 'TEXT') { 254 | return createText(object); 255 | } else if (type === 'LAYOUT') { 256 | return object; 257 | } else { 258 | return createShape(object, RL_STROKE, RL_FILL, type); 259 | } 260 | }; 261 | 262 | export { 263 | createBasicShape, 264 | createTable, 265 | createShape, 266 | createText, 267 | createFurniture, 268 | 269 | RL_FILL, 270 | RL_STROKE, 271 | RL_CHAIR_STROKE, 272 | RL_CHAIR_FILL, 273 | RL_CHAIR_TUCK, 274 | RL_PREVIEW_HEIGHT, 275 | RL_PREVIEW_WIDTH, 276 | RL_VIEW_WIDTH, 277 | RL_VIEW_HEIGHT, 278 | RL_FOOT, 279 | RL_AISLEGAP, 280 | RL_ROOM_OUTER_SPACING, 281 | RL_ROOM_INNER_SPACING, 282 | RL_ROOM_STROKE, 283 | RL_CORNER_FILL, 284 | RL_UNGROUPABLES, 285 | RL_CREDIT_TEXT, 286 | RL_CREDIT_TEXT_PARAMS 287 | }; 288 | -------------------------------------------------------------------------------- /src/app/shared/models/furnishings.ts: -------------------------------------------------------------------------------- 1 | import { RL_ROOM_INNER_SPACING as WT } from '../helpers'; // WT = Wall Thickness 2 | 3 | const FURNISHINGS = { 4 | // 'title': 'Faithlife Room Layout Furniture Library', 5 | 'rooms': [ 6 | { 7 | 'title': '13\' x 17\' Small Conference Room', 8 | 'width': 156, 9 | 'height': 204 10 | }, 11 | { 12 | 'title': '15\' x 26\' Medium Conference Room', 13 | 'width': 180, 14 | 'height': 312 15 | }, 16 | { 17 | 'title': '18\' x 21\' Medium Conference Room', 18 | 'width': 216, 19 | 'height': 252 20 | }, 21 | { 22 | 'title': '20\' x 10\'', 23 | 'width': 240, 24 | 'height': 120 25 | }, 26 | { 27 | 'title': '16\' x 12\'', 28 | 'width': 192, 29 | 'height': 144 30 | }, 31 | { 32 | 'title': 'Gym (Regulation)', 33 | 'width': 1320, 34 | 'height': 720 35 | }, 36 | { 37 | 'title': 'Gym (High School)', 38 | 'width': 1008, 39 | 'height': 600 40 | }, 41 | { 42 | 'title': '40\' x 20\'', 43 | 'width': 480, 44 | 'height': 240 45 | } 46 | ], 47 | 'tables': [ 48 | { 49 | 'title': '54" Round Folding', 50 | 'width': 54, 51 | 'height': 54, 52 | 'lrSpacing': 54, 53 | 'tbSpacing': 54, 54 | 'shape': 'circle', 55 | 'chairs': 6 56 | }, 57 | { 58 | 'title': '60" Round Folding', 59 | 'width': 60, 60 | 'height': 60, 61 | 'lrSpacing': 60, 62 | 'tbSpacing': 60, 63 | 'shape': 'circle', 64 | 'chairs': 8 65 | }, 66 | { 67 | 'title': '72" Round Folding', 68 | 'width': 72, 69 | 'height': 72, 70 | 'lrSpacing': 72, 71 | 'tbSpacing': 72, 72 | 'shape': 'circle', 73 | 'chairs': 8 74 | }, 75 | { 76 | 'title': '6\' x 30" Folding', 77 | 'width': 72, 78 | 'height': 30, 79 | 'lrSpacing': 24, 80 | 'tbSpacing': 60, 81 | 'shape': 'rect', 82 | 'topChairs': 3, 83 | 'bottomChairs': 3, 84 | 'leftChairs': 0, 85 | 'rightChairs': 0 86 | }, 87 | { 88 | 'title': '8\' x 30" Folding', 89 | 'width': 96, 90 | 'height': 30, 91 | 'lrSpacing': 24, 92 | 'tbSpacing': 60, 93 | 'shape': 'rect', 94 | 'topChairs': 4, 95 | 'bottomChairs': 4, 96 | 'leftChairs': 0, 97 | 'rightChairs': 0 98 | }, 99 | { 100 | 'title': '8\' x 40" Family', 101 | 'width': 96, 102 | 'height': 40, 103 | 'lrSpacing': 60, 104 | 'tbSpacing': 60, 105 | 'shape': 'rect', 106 | 'topChairs': 4, 107 | 'bottomChairs': 3, 108 | 'leftChairs': 1, 109 | 'rightChairs': 1 110 | }, 111 | { 112 | 'title': '8\' x 18" Classroom', 113 | 'width': 96, 114 | 'height': 18, 115 | 'lrSpacing': 24, 116 | 'tbSpacing': 36, 117 | 'shape': 'rect', 118 | 'topChairs': 0, 119 | 'bottomChairs': 4, 120 | 'leftChairs': 0, 121 | 'rightChairs': 0 122 | }, 123 | { 124 | 'title': '6\' x 18" Classroom', 125 | 'width': 72, 126 | 'height': 18, 127 | 'lrSpacing': 24, 128 | 'tbSpacing': 36, 129 | 'shape': 'rect', 130 | 'topChairs': 0, 131 | 'bottomChairs': 3, 132 | 'leftChairs': 0, 133 | 'rightChairs': 0 134 | } 135 | ], 136 | 'chairs': [ 137 | { 138 | 'title': 'Generic', 139 | 'width': 18, 140 | 'height': 20, 141 | 'lrSpacing': 2, 142 | 'tbSpacing': 12, 143 | 'parts': [ 144 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 18, height: 20 } }, 145 | { 'type': 'rect', 'definition': { left: 0, top: 18, width: 18, height: 2 } } 146 | ] 147 | }, 148 | { 149 | 'title': '14" Children\'s', 150 | 'width': 14, 151 | 'height': 14, 152 | 'lrSpacing': 2, 153 | 'tbSpacing': 12, 154 | 'parts': [ 155 | { 'type': 'circle', 'definition': { originX: 'center', originY: 'center', radius: 7 } }, 156 | { 'type': 'circle', 'definition': { originX: 'center', originY: 'center', radius: 4 } } 157 | ] 158 | }, 159 | { 160 | 'title': '18" Folding', 161 | 'width': 18, 162 | 'height': 18, 163 | 'lrSpacing': 2, 164 | 'tbSpacing': 12, 165 | 'parts': [ 166 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 18, height: 18 } }, 167 | { 'type': 'rect', 'definition': { left: 0, top: 16, width: 18, height: 2 } } 168 | ] 169 | }, 170 | { 171 | 'title': '18" Stacking', 172 | 'width': 18.375, 173 | 'height': 23.25, 174 | 'lrSpacing': 2, 175 | 'tbSpacing': 12.75, 176 | 'parts': [ 177 | { 'type': 'rect', 'definition': { width: 18.375, height: 23.25 } }, 178 | { 'type': 'rect', 'definition': { width: 18.375, height: 4, top: 19.25 } }, 179 | { 'type': 'rect', 'definition': { width: 18.375, height: 2, top: 21.25 } } 180 | ] 181 | }, 182 | { 183 | 'title': '20" Pew Stacker', 184 | 'source': 'http://sanctuaryseating.com/church-chairs/impressions-series/model-7027/', 185 | 'width': 20.25, 186 | 'height': 26.3, 187 | 'lrSpacing': 1, 188 | 'tbSpacing': 12, 189 | 'parts': [ 190 | { 'type': 'rect', 'definition': { width: 20.25, height: 26.3 } }, 191 | { 'type': 'rect', 'definition': { width: 20.25, height: 8, top: 18.3 } }, 192 | { 'type': 'rect', 'definition': { width: 20.25, height: 6, top: 20.3 } } 193 | ] 194 | }, 195 | { 196 | 'title': '22" Pew Stacker', 197 | 'source': 'http://sanctuaryseating.com/church-chairs/impressions-series/model-7227/', 198 | 'width': 22, 199 | 'height': 26.3, 200 | 'lrSpacing': 1, 201 | 'tbSpacing': 12, 202 | 'parts': [ 203 | { 'type': 'rect', 'definition': { width: 22, height: 26.3 } }, 204 | { 'type': 'rect', 'definition': { width: 22, height: 8, top: 18.3 } }, 205 | { 'type': 'rect', 'definition': { width: 22, height: 6, top: 20.3 } } 206 | ] 207 | }, 208 | { 209 | 'title': '22" Square', 210 | 'width': 22, 211 | 'height': 22, 212 | 'lrSpacing': 2, 213 | 'tbSpacing': 12, 214 | 'parts': [ 215 | { 'type': 'rect', 'definition': { width: 22, height: 22 } }, 216 | { 'type': 'rect', 'definition': { width: 22, height: 6, top: 16 } } 217 | ] 218 | } 219 | ], 220 | 'miscellaneous': [ 221 | { 222 | 'title': 'Rectangle', 223 | 'width': 36, 224 | 'height': 12, 225 | 'flexible': true, 226 | 'parts': [ 227 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 36, height: 12 } } 228 | ] 229 | }, 230 | { 231 | 'title': '5\' x 23" Upright Piano', 232 | 'width': 60, 233 | 'height': 23, 234 | 'parts': [ 235 | { 'type': 'rect', 'definition': { left: 15, top: 16, width: 30, height: 14, stroke: 'chair' } }, // bench 236 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 60, height: 23 } }, // base 237 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 6, height: 23 } }, // side pillar 238 | { 'type': 'rect', 'definition': { left: 54, top: 0, width: 6, height: 23 } }, // side pillar 239 | { 'type': 'rect', 'definition': { left: 0, top: 0, width: 60, height: 13 } } // top 240 | ] 241 | }, 242 | { 243 | 'title': '6\' Grand Piano', 244 | 'width': 58, 245 | 'height': 84, 246 | 'parts': [ 247 | { 'type': 'rect', 'definition': { left: 11, top: 77, width: 36, height: 14, stroke: 'chair' } }, // bench 248 | { 'type': 'path', 'path': 'M 0,84 L 0,36 C 0,2 42,2 42,32 S 58,50 58,72 L 58,84 z', 'definition': {} }, // outline 249 | { 'type': 'rect', 'definition': { left: 0, top: 74, width: 58, height: 10 } }, // keyboard area 250 | { 'type': 'rect', 'definition': { left: 0, top: 74, width: 6, height: 10 } }, // side pillar 251 | { 'type': 'rect', 'definition': { left: 52, top: 74, width: 6, height: 10 } } // side pillar 252 | ] 253 | }, 254 | { 255 | 'title': '7\' Grand Piano', 256 | 'width': 62, 257 | 'height': 84, 258 | 'parts': [ 259 | // { "type": "rect", "definition": { left: 13, top: 77, width: 36, height: 14, stroke: "chair" } }, // bench 260 | { 'type': 'path', 'path': 'M 0,84 L 0,24 C 0,-10 46,-10 46,26 S 62,50 62,72 L 62,84 z', 'definition': {} }, // outline 261 | { 'type': 'rect', 'definition': { left: 0, top: 74, width: 62, height: 10 } }, // keyboard area 262 | // { "type": "rect", "definition": { left: 0, top: 74, width: 6, height: 10 } }, // side pillar 263 | // { "type": "rect", "definition": { left: 56, top: 74, width: 6, height: 10 } } // side pillar 264 | ] 265 | }, 266 | { 267 | 'title': '8\' Grand Piano', 268 | 'width': 62, 269 | 'height': 94, 270 | 'parts': [ 271 | { 'type': 'rect', 'definition': { left: 13, top: 87, width: 36, height: 14, stroke: 'chair' } }, // bench 272 | { 'type': 'path', 'path': 'M 0,94 L 0,24 C 0,-10 46,-10 46,28 S 62,62 62,78 L 62,94 z', 'definition': {} }, // outline 273 | { 'type': 'rect', 'definition': { left: 0, top: 84, width: 62, height: 10 } }, // keyboard area 274 | { 'type': 'rect', 'definition': { left: 0, top: 84, width: 6, height: 10 } }, // side pillar 275 | { 'type': 'rect', 'definition': { left: 56, top: 84, width: 6, height: 10 } } // side pillar 276 | ] 277 | }, 278 | { 279 | 'title': 'Awana Game Square', // note: calculation from Excel spreadsheet 280 | 'width': 200, 281 | 'height': 200, 282 | 'parts': [ 283 | { 'type': 'circle', 'definition': { left: 240, top: 240, strokeWidth: 2, stroke: '#aaaaaa', originX: 'center', originY: 'center', radius: 180 } }, // game circle 284 | 285 | // blue 286 | { 'type': 'path', 'path': 'M329.8,82.32L340.41,71.71', 'definition': { strokeWidth: 2, stroke: '#aaaaaa' } }, // circle starting line 287 | { 'type': 'path', 'path': 'M480,0L480,480M480,0L240,240M299.4,180.6L299.4,299.4M278.19,193.33L286.67,201.81M273.94,138.18L341.82,206.06M312.13,159.39L320.61,167.87M320.61,150.91L329.09,159.39M329.1,142.42L337.58,150.9M337.58,133.94L346.06,142.42M346.07,125.45L354.55,133.93', 'definition': { stroke: 'blue', strokeWidth: 2 } }, 288 | 289 | // green 290 | { 'type': 'path', 'path': 'M408.29,340.41L397.68,329.8', 'definition': { strokeWidth: 2, stroke: '#aaaaaa' } }, // circle starting line 291 | { 'type': 'path', 'path': 'M0,480L480,480M240,240L480,480M180.6,299.4L299.4,299.4M278.19,286.67L286.67,278.19M273.94,341.82L341.82,273.94M312.13,320.61L320.61,312.13M320.61,329.09L329.09,320.61M329.1,337.58L337.58,329.1M337.58,346.06L346.06,337.58M346.07,354.55L354.55,346.07', 'definition': { stroke: 'green', strokeWidth: 2 } }, 292 | 293 | // yellow 294 | { 'type': 'path', 'path': 'M150.2,397.68L139.59,408.29', 'definition': { strokeWidth: 2, stroke: '#aaaaaa' } }, // circle starting line 295 | { 'type': 'path', 'path': 'M0,480L0,0M0,480L240,240M180.6,299.4L180.6,180.6M201.81,286.67L193.33,278.19M206.06,341.82L138.18,273.94M167.87,320.61L159.39,312.13M159.39,329.09L150.91,320.61M150.9,337.58L142.42,329.1M142.42,346.06L133.94,337.58M133.93,354.55L125.45,346.07', 'definition': { stroke: 'yellow', strokeWidth: 2 } }, 296 | 297 | // draw red last because it's stronger and on top, and it'll capture corners this way 298 | // red 299 | { 'type': 'path', 'path': 'M82.32,150.2L71.71,139.59', 'definition': { strokeWidth: 2, stroke: '#aaaaaa' } }, // circle starting line 300 | { 'type': 'path', 'path': 'M0,0L480,0M0,0L240,240M180.6,180.6L299.4,180.6M193.33,201.81L201.81,193.33M138.18,206.06L206.06,138.18M159.39,167.87L167.87,159.39M150.91,159.39L159.39,150.91M142.42,150.9L150.9,142.42M133.94,142.42L142.42,133.94M125.45,133.93L133.93,125.45', 'definition': { stroke: 'red', strokeWidth: 2 } } 301 | 302 | // { "type": "line", "line": [ 0,0,100,0 ], "definition": { stroke: "#ffff00", strokeWidth: "2" } }, 303 | // { "type": "line", "line": [ 0,0,100,100 ], "definition": { stroke: "#00ffff", strokeWidth: "2" } } 304 | ] 305 | } 306 | ], 307 | 'doors': [ 308 | { 309 | title: 'Narrow Door (28" wide)', 310 | parts: [ 311 | { type: 'rect', definition: { left: 0, width: 28, top: 0, height: WT, fill: 'white', strokeWidth: 0, originX: 'left', originY: 'top' } }, 312 | { type: 'line', line: [0, 0, 0, `${WT + 28}`], definition: { stroke: 'black', strokeWidth: 1 } }, 313 | { type: 'path', path: `M 0 ${WT + 28} Q 28, ${WT + 28}, 28, ${WT}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 314 | ] 315 | }, { 316 | title: 'Normal Door (32" wide)', 317 | parts: [ 318 | { type: 'rect', definition: { left: 0, width: 32, top: 0, height: WT, fill: 'white', strokeWidth: 0, originX: 'left', originY: 'top' } }, 319 | { type: 'line', line: [0, 0, 0, `${WT + 32}`], definition: { stroke: 'black', strokeWidth: 1 } }, 320 | { type: 'path', path: `M 0 ${WT + 32} Q 32, ${WT + 32}, 32, ${WT}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 321 | ] 322 | }, { 323 | title: 'Wide Door (36" wide)', 324 | parts: [ 325 | { type: 'rect', definition: { left: 0, width: 36, top: 0, height: WT, fill: 'white', strokeWidth: 0, originX: 'left', originY: 'top' } }, 326 | { type: 'line', line: [0, 0, 0, `${WT + 36}`], definition: { stroke: 'black', strokeWidth: 1 } }, 327 | { type: 'path', path: `M 0 ${WT + 36} Q 36, ${WT + 36}, 36, ${WT}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 328 | ] 329 | }, { 330 | title: 'Double Doors (64" wide)', 331 | parts: [ 332 | { type: 'rect', definition: { left: 0, width: 64, top: 0, height: WT, fill: 'white', strokeWidth: 0, originX: 'left', originY: 'top' } }, 333 | { type: 'line', line: [0, 0, 0, `${WT + 32}`], definition: { stroke: 'black', strokeWidth: 1 } }, 334 | { type: 'path', path: `M 0 ${WT + 32} Q 32, ${WT + 32}, 32, ${WT}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 335 | { type: 'line', line: [64, 0, 64, `${WT + 32}`], definition: { stroke: 'black', strokeWidth: 1 } }, 336 | { type: 'path', path: `M 32 ${WT} Q 32, ${WT + 32}, 64, ${WT + 32}`, definition: { stroke: '#ddd', strokeWidth: 1, fill: 'transparent' } }, 337 | ] 338 | } 339 | ], 340 | 'windows': [ 341 | { 342 | title: '2’ Window (24” wide)', 343 | parts: [ 344 | { type: 'rect', definition: { left: 0, width: 24, top: 0, height: WT - 1, fill: 'white', strokeWidth: 1, originX: 'left', originY: 'top' } }, 345 | ] 346 | }, 347 | { 348 | title: '3’ Window (36” wide)', 349 | parts: [ 350 | { type: 'rect', definition: { left: 0, width: 36, top: 0, height: WT - 1, fill: 'white', strokeWidth: 1, originX: 'left', originY: 'top' } }, 351 | ] 352 | }, 353 | { 354 | title: '4’ Window (48” wide)', 355 | parts: [ 356 | { type: 'rect', definition: { left: 0, width: 48, top: 0, height: WT - 1, fill: 'white', strokeWidth: 1, originX: 'left', originY: 'top' } }, 357 | ] 358 | }, 359 | { 360 | title: '6’ Window (72” wide)', 361 | parts: [ 362 | { type: 'rect', definition: { left: 0, width: 72, top: 0, height: WT - 1, fill: 'white', strokeWidth: 1, originX: 'left', originY: 'top' } }, 363 | ] 364 | } 365 | ] 366 | }; 367 | 368 | export { FURNISHINGS }; 369 | -------------------------------------------------------------------------------- /src/app/shared/models/index.ts: -------------------------------------------------------------------------------- 1 | export interface Furnish { 2 | title: string; 3 | width: number; 4 | height: number; 5 | lrSpacing: number; 6 | tbSpacing: number; 7 | } 8 | 9 | export interface Part { 10 | type: 'Canvas' | 'Group' | 'Rect' | 'Line' | 'Circle' | 'Ellipse' | 'Path' | 'Polygon' | 'Polyline' | 'Triangle'; 11 | definition?: { 12 | left?: number; 13 | top?: number; 14 | width?: number; 15 | height?: number; 16 | originX?: string; 17 | originY?: string; 18 | radius?: number; 19 | strokeWidth?: number; 20 | stroke?: string; 21 | fill?: string; 22 | }; 23 | path?: string; 24 | line?: number[]; 25 | } 26 | 27 | -------------------------------------------------------------------------------- /src/app/shared/modules/design.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 3 | import { FlexLayoutModule } from '@angular/flex-layout'; 4 | 5 | @NgModule({ 6 | imports: [ 7 | FontAwesomeModule, 8 | FlexLayoutModule 9 | ], 10 | exports: [ 11 | FontAwesomeModule, 12 | FlexLayoutModule 13 | ] 14 | }) 15 | export class DesignModule { } 16 | -------------------------------------------------------------------------------- /src/app/shared/modules/index.ts: -------------------------------------------------------------------------------- 1 | export { DesignModule } from "./design.module"; 2 | export { MaterialModule } from './material.module'; 3 | -------------------------------------------------------------------------------- /src/app/shared/modules/material.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | import { MatButtonModule } from '@angular/material/button'; 4 | import { MatButtonToggleModule } from '@angular/material/button-toggle'; 5 | import { MatDialogModule } from '@angular/material/dialog'; 6 | import { MatDividerModule } from '@angular/material/divider'; 7 | import { MatExpansionModule } from '@angular/material/expansion'; 8 | import { MatFormFieldModule } from '@angular/material/form-field'; 9 | import { MatIconModule } from '@angular/material/icon'; 10 | import { MatInputModule } from '@angular/material/input'; 11 | import { MatListModule } from '@angular/material/list'; 12 | import { MatMenuModule } from '@angular/material/menu'; 13 | import { MatRadioModule } from '@angular/material/radio'; 14 | import { MatSelectModule } from '@angular/material/select'; 15 | import { MatSidenavModule } from '@angular/material/sidenav'; 16 | import { MatToolbarModule } from '@angular/material/toolbar'; 17 | import { MatTooltipModule } from '@angular/material/tooltip'; 18 | 19 | @NgModule({ 20 | imports: [ 21 | MatButtonModule, 22 | MatButtonToggleModule, 23 | MatDialogModule, 24 | MatDividerModule, 25 | MatExpansionModule, 26 | MatFormFieldModule, 27 | MatIconModule, 28 | MatInputModule, 29 | MatListModule, 30 | MatMenuModule, 31 | MatRadioModule, 32 | MatSelectModule, 33 | MatSidenavModule, 34 | MatToolbarModule, 35 | MatTooltipModule, 36 | ], 37 | exports: [ 38 | MatButtonModule, 39 | MatButtonToggleModule, 40 | MatDialogModule, 41 | MatDividerModule, 42 | MatExpansionModule, 43 | MatFormFieldModule, 44 | MatIconModule, 45 | MatInputModule, 46 | MatListModule, 47 | MatMenuModule, 48 | MatRadioModule, 49 | MatSelectModule, 50 | MatSidenavModule, 51 | MatToolbarModule, 52 | MatTooltipModule, 53 | ] 54 | }) 55 | export class MaterialModule { } 56 | -------------------------------------------------------------------------------- /src/app/shared/shared.module.ts: -------------------------------------------------------------------------------- 1 | import { NgModule } from '@angular/core'; 2 | 3 | // Modules 4 | import { MaterialModule, DesignModule } from './modules'; 5 | 6 | // Components 7 | import { ZoomComponent } from './components'; 8 | import { FontAwesomeModule } from '@fortawesome/angular-fontawesome'; 9 | 10 | @NgModule({ 11 | imports: [ 12 | MaterialModule, 13 | DesignModule, 14 | FontAwesomeModule 15 | ], 16 | exports: [ 17 | MaterialModule, 18 | DesignModule, 19 | ZoomComponent 20 | ], 21 | providers: [], 22 | declarations: [ZoomComponent] 23 | }) 24 | export class SharedModule { } 25 | -------------------------------------------------------------------------------- /src/assets/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andsilver/floorplan-angular/486752c15b5fbcdca57fc70b0d601f3ae8b3285b/src/assets/.gitkeep -------------------------------------------------------------------------------- /src/assets/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andsilver/floorplan-angular/486752c15b5fbcdca57fc70b0d601f3ae8b3285b/src/assets/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/assets/fonts/glyphicons-halflings-regular.svg: -------------------------------------------------------------------------------- 1 | 2 | 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 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | 70 | 71 | 72 | 73 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | 81 | 82 | 83 | 84 | 85 | 86 | 87 | 88 | 89 | 90 | 91 | 92 | 93 | 94 | 95 | 96 | 97 | 98 | 99 | 100 | 101 | 102 | 103 | 104 | 105 | 106 | 107 | 108 | 109 | 110 | 111 | 112 | 113 | 114 | 115 | 116 | 117 | 118 | 119 | 120 | 121 | 122 | 123 | 124 | 125 | 126 | 127 | 128 | 129 | 130 | 131 | 132 | 133 | 134 | 135 | 136 | 137 | 138 | 139 | 140 | 141 | 142 | 143 | 144 | 145 | 146 | 147 | 148 | 149 | 150 | 151 | 152 | 153 | 154 | 155 | 156 | 157 | 158 | 159 | 160 | 161 | 162 | 163 | 164 | 165 | 166 | 167 | 168 | 169 | 170 | 171 | 172 | 173 | 174 | 175 | 176 | 177 | 178 | 179 | 180 | 181 | 182 | 183 | 184 | 185 | 186 | 187 | 188 | 189 | 190 | 191 | 192 | 193 | 194 | 195 | 196 | 197 | 198 | 199 | 200 | 201 | 202 | 203 | 204 | 205 | 206 | 207 | 208 | 209 | 210 | 211 | 212 | 213 | 214 | 215 | 216 | 217 | 218 | 219 | 220 | 221 | 222 | 223 | 224 | 225 | 226 | 227 | 228 | 229 | -------------------------------------------------------------------------------- /src/assets/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andsilver/floorplan-angular/486752c15b5fbcdca57fc70b0d601f3ae8b3285b/src/assets/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/assets/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andsilver/floorplan-angular/486752c15b5fbcdca57fc70b0d601f3ae8b3285b/src/assets/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/environments/environment.prod.ts: -------------------------------------------------------------------------------- 1 | export const environment = { 2 | production: true 3 | }; 4 | -------------------------------------------------------------------------------- /src/environments/environment.ts: -------------------------------------------------------------------------------- 1 | // This file can be replaced during build by using the `fileReplacements` array. 2 | // `ng build --prod` replaces `environment.ts` with `environment.prod.ts`. 3 | // The list of file replacements can be found in `angular.json`. 4 | 5 | export const environment = { 6 | production: false 7 | }; 8 | 9 | /* 10 | * For easier debugging in development mode, you can import the following file 11 | * to ignore zone related error stack frames such as `zone.run`, `zoneDelegate.invokeTask`. 12 | * 13 | * This import should be commented out in production mode because it will have a negative impact 14 | * on performance if an error is thrown. 15 | */ 16 | // import 'zone.js/plugins/zone-error'; // Included with Angular CLI. 17 | -------------------------------------------------------------------------------- /src/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/andsilver/floorplan-angular/486752c15b5fbcdca57fc70b0d601f3ae8b3285b/src/favicon.ico -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | RoomLayout 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | -------------------------------------------------------------------------------- /src/karma.conf.js: -------------------------------------------------------------------------------- 1 | // Karma configuration file, see link for more information 2 | // https://karma-runner.github.io/1.0/config/configuration-file.html 3 | 4 | module.exports = function (config) { 5 | config.set({ 6 | basePath: '', 7 | frameworks: ['jasmine', '@angular-devkit/build-angular'], 8 | plugins: [ 9 | require('karma-jasmine'), 10 | require('karma-chrome-launcher'), 11 | require('karma-jasmine-html-reporter'), 12 | require('karma-coverage-istanbul-reporter'), 13 | require('@angular-devkit/build-angular/plugins/karma') 14 | ], 15 | client: { 16 | clearContext: false // leave Jasmine Spec Runner output visible in browser 17 | }, 18 | coverageIstanbulReporter: { 19 | dir: require('path').join(__dirname, '../coverage'), 20 | reports: ['html', 'lcovonly', 'text-summary'], 21 | fixWebpackSourcePaths: true 22 | }, 23 | reporters: ['progress', 'kjhtml'], 24 | port: 9876, 25 | colors: true, 26 | logLevel: config.LOG_INFO, 27 | autoWatch: true, 28 | browsers: ['Chrome'], 29 | singleRun: false 30 | }); 31 | }; -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | 2 | import { enableProdMode } from '@angular/core'; 3 | import { platformBrowserDynamic } from '@angular/platform-browser-dynamic'; 4 | 5 | import { AppModule } from './app/app.module'; 6 | import { environment } from './environments/environment'; 7 | 8 | if (environment.production) { 9 | enableProdMode(); 10 | } 11 | 12 | platformBrowserDynamic().bootstrapModule(AppModule) 13 | .catch(err => console.error(err)); 14 | -------------------------------------------------------------------------------- /src/polyfills.ts: -------------------------------------------------------------------------------- 1 | /*************************************************************************************************** 2 | * Load `$localize` onto the global scope - used if i18n tags appear in Angular templates. 3 | */ 4 | import '@angular/localize/init'; 5 | /** 6 | * This file includes polyfills needed by Angular and is loaded before the app. 7 | * You can add your own extra polyfills to this file. 8 | * 9 | * This file is divided into 2 sections: 10 | * 1. Browser polyfills. These are applied before loading ZoneJS and are sorted by browsers. 11 | * 2. Application imports. Files imported after ZoneJS that should be loaded before your main 12 | * file. 13 | * 14 | * The current setup is for so-called "evergreen" browsers; the last versions of browsers that 15 | * automatically update themselves. This includes Safari >= 10, Chrome >= 55 (including Opera), 16 | * Edge >= 13 on the desktop, and iOS 10 and Chrome on mobile. 17 | * 18 | * Learn more in https://angular.io/guide/browser-support 19 | */ 20 | 21 | /*************************************************************************************************** 22 | * BROWSER POLYFILLS 23 | */ 24 | 25 | /** IE9, IE10 and IE11 requires all of the following polyfills. **/ 26 | // import 'core-js/es6/symbol'; 27 | // import 'core-js/es6/object'; 28 | // import 'core-js/es6/function'; 29 | // import 'core-js/es6/parse-int'; 30 | // import 'core-js/es6/parse-float'; 31 | // import 'core-js/es6/number'; 32 | // import 'core-js/es6/math'; 33 | // import 'core-js/es6/string'; 34 | // import 'core-js/es6/date'; 35 | // import 'core-js/es6/array'; 36 | // import 'core-js/es6/regexp'; 37 | // import 'core-js/es6/map'; 38 | // import 'core-js/es6/weak-map'; 39 | // import 'core-js/es6/set'; 40 | 41 | /** 42 | * If the application will be indexed by Google Search, the following is required. 43 | * Googlebot uses a renderer based on Chrome 41. 44 | * https://developers.google.com/search/docs/guides/rendering 45 | **/ 46 | // import 'core-js/es6/array'; 47 | 48 | /** IE10 and IE11 requires the following for NgClass support on SVG elements */ 49 | // import 'classlist.js'; // Run `npm install --save classlist.js`. 50 | 51 | /** IE10 and IE11 requires the following for the Reflect API. */ 52 | // import 'core-js/es6/reflect'; 53 | 54 | /** 55 | * Web Animations `@angular/platform-browser/animations` 56 | * Only required if AnimationBuilder is used within the application and using IE/Edge or Safari. 57 | * Standard animation support in Angular DOES NOT require any polyfills (as of Angular 6.0). 58 | **/ 59 | // import 'web-animations-js'; // Run `npm install --save web-animations-js`. 60 | 61 | /** 62 | * By default, zone.js will patch all possible macroTask and DomEvents 63 | * user can disable parts of macroTask/DomEvents patch by setting following flags 64 | * because those flags need to be set before `zone.js` being loaded, and webpack 65 | * will put import in the top of bundle, so user need to create a separate file 66 | * in this directory (for example: zone-flags.ts), and put the following flags 67 | * into that file, and then add the following code before importing zone.js. 68 | * import './zone-flags.ts'; 69 | * 70 | * The flags allowed in zone-flags.ts are listed here. 71 | * 72 | * The following flags will work for all browsers. 73 | * 74 | * (window as any).__Zone_disable_requestAnimationFrame = true; // disable patch requestAnimationFrame 75 | * (window as any).__Zone_disable_on_property = true; // disable patch onProperty such as onclick 76 | * (window as any).__zone_symbol__BLACK_LISTED_EVENTS = ['scroll', 'mousemove']; // disable patch specified eventNames 77 | * 78 | * in IE/Edge developer tools, the addEventListener will also be wrapped by zone.js 79 | * with the following flag, it will bypass `zone.js` patch for IE/Edge 80 | * 81 | * (window as any).__Zone_enable_cross_context_check = true; 82 | * 83 | */ 84 | 85 | /*************************************************************************************************** 86 | * Zone JS is required by default for Angular itself. 87 | */ 88 | import 'zone.js'; // Included with Angular CLI. 89 | 90 | 91 | /*************************************************************************************************** 92 | * APPLICATION IMPORTS 93 | */ 94 | -------------------------------------------------------------------------------- /src/styles.scss: -------------------------------------------------------------------------------- 1 | /* You can add global styles to this file, and also import other style files */ 2 | @import '@angular/material/prebuilt-themes/deeppurple-amber.css'; 3 | @import './styles/main'; 4 | -------------------------------------------------------------------------------- /src/styles/_customize.scss: -------------------------------------------------------------------------------- 1 | @import "./theme"; 2 | 3 | html, 4 | body { 5 | height: 100%; 6 | } 7 | 8 | body { 9 | margin: 0; 10 | font-family: Roboto, "Helvetica Neue", sans-serif; 11 | color: #292b2c; 12 | } 13 | 14 | .rl-object-options { 15 | .mat-expansion-panel-body { 16 | padding: 0 !important; 17 | } 18 | 19 | .mat-list-item:hover { 20 | background: #ececec; 21 | cursor: pointer; 22 | } 23 | 24 | mat-form-field { 25 | padding-left: 12px; 26 | padding-top: 20px; 27 | width: 90%; 28 | } 29 | } 30 | 31 | .main-container { 32 | .canvas-container { 33 | width: 100% !important; 34 | height: 100% !important; 35 | } 36 | } 37 | 38 | mat-button-toggle { 39 | &:focus, 40 | &:active { 41 | outline: none; 42 | border: none; 43 | } 44 | } 45 | 46 | .mat-button-toggle-appearance-standard .mat-button-toggle-label-content { 47 | font-size: 12px; 48 | font-weight: normal; 49 | line-height: none; 50 | } 51 | 52 | .mat-form-field-appearance-outline.mat-form-field-can-float 53 | .mat-input-server:focus 54 | + .mat-form-field-label-wrapper 55 | .mat-form-field-label, 56 | .mat-form-field-appearance-outline.mat-form-field-can-float.mat-form-field-should-float 57 | .mat-form-field-label { 58 | transform: translateY(-1.19375em) scale(0.75) !important; 59 | } 60 | 61 | .mat-form-field-appearance-outline .mat-form-field-label { 62 | top: 1.34375em; 63 | } 64 | 65 | .mat-form-field-appearance-outline .mat-form-field-infix { 66 | padding: 0.5em 0; 67 | } 68 | 69 | .mat-form-field-subscript-wrapper { 70 | margin-top: 0.16667em; 71 | } 72 | -------------------------------------------------------------------------------- /src/styles/_theme.scss: -------------------------------------------------------------------------------- 1 | @use '@angular/material' as mat; 2 | @include mat.core(); 3 | 4 | $primary-pane: ( 5 | 50: #e4e8f2, 6 | 100: #bcc6de, 7 | 200: #8fa0c8, 8 | 300: #6279b2, 9 | 400: #415da2, 10 | 500: #1f4091, 11 | 600: #1b3a89, 12 | 700: #17327e, 13 | 800: #122a74, 14 | 900: #0a1c62, 15 | A100: #95a6ff, 16 | A200: #627cff, 17 | A400: #2f51ff, 18 | A700: #153cff, 19 | contrast: ( 20 | 50: #000000, 21 | 100: #000000, 22 | 200: #000000, 23 | 300: #ffffff, 24 | 400: #ffffff, 25 | 500: #ffffff, 26 | 600: #ffffff, 27 | 700: #ffffff, 28 | 800: #ffffff, 29 | 900: #ffffff, 30 | A100: #000000, 31 | A200: #000000, 32 | A400: #ffffff, 33 | A700: #ffffff 34 | ) 35 | ); 36 | 37 | $secondary-pane: ( 38 | 50: #edf3e8, 39 | 100: #d1e1c5, 40 | 200: #b2ce9f, 41 | 300: #93ba78, 42 | 400: #7cab5b, 43 | 500: #659c3e, 44 | 600: #5d9438, 45 | 700: #538a30, 46 | 800: #498028, 47 | 900: #376e1b, 48 | A100: #c5ffaa, 49 | A200: #a1ff77, 50 | A400: #7eff44, 51 | A700: #6dff2a, 52 | contrast: ( 53 | 50: #000000, 54 | 100: #000000, 55 | 200: #000000, 56 | 300: #000000, 57 | 400: #000000, 58 | 500: #ffffff, 59 | 600: #ffffff, 60 | 700: #ffffff, 61 | 800: #ffffff, 62 | 900: #ffffff, 63 | A100: #000000, 64 | A200: #000000, 65 | A400: #000000, 66 | A700: #000000 67 | ) 68 | ); 69 | 70 | $primary: mat.define-palette(mat.$cyan-palette); 71 | $accent: mat.define-palette(mat.$red-palette); 72 | $warn: mat.define-palette(mat.$red-palette); 73 | 74 | $theme: mat.define-light-theme($primary, $accent, $warn); 75 | 76 | @include mat.all-component-themes($theme); 77 | -------------------------------------------------------------------------------- /src/styles/main.scss: -------------------------------------------------------------------------------- 1 | @import './customize'; 2 | @import './theme'; 3 | 4 | -------------------------------------------------------------------------------- /src/test.ts: -------------------------------------------------------------------------------- 1 | // This file is required by karma.conf.js and loads recursively all the .spec and framework files 2 | 3 | import 'zone.js/testing'; 4 | import { getTestBed } from '@angular/core/testing'; 5 | import { 6 | BrowserDynamicTestingModule, 7 | platformBrowserDynamicTesting 8 | } from '@angular/platform-browser-dynamic/testing'; 9 | 10 | declare const require: any; 11 | 12 | // First, initialize the Angular testing environment. 13 | getTestBed().initTestEnvironment( 14 | BrowserDynamicTestingModule, 15 | platformBrowserDynamicTesting() 16 | ); 17 | // Then we find all the tests. 18 | const context = require.context('./', true, /\.spec\.ts$/); 19 | // And load the modules. 20 | context.keys().map(context); 21 | -------------------------------------------------------------------------------- /src/tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/app", 5 | "types": ["node"], 6 | }, 7 | "files": [ 8 | "main.ts", 9 | "polyfills.ts" 10 | ], 11 | "include": [ 12 | "src/**/*.d.ts" 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /src/tsconfig.spec.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tsconfig.json", 3 | "compilerOptions": { 4 | "outDir": "../out-tsc/spec", 5 | "types": [ 6 | "jasmine", 7 | "node" 8 | ] 9 | }, 10 | "files": [ 11 | "test.ts", 12 | "polyfills.ts" 13 | ], 14 | "include": [ 15 | "**/*.spec.ts", 16 | "**/*.d.ts" 17 | ] 18 | } 19 | -------------------------------------------------------------------------------- /src/tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "../tslint.json", 3 | "rules": { 4 | "directive-selector": [ 5 | true, 6 | "attribute", 7 | "app", 8 | "camelCase" 9 | ], 10 | "component-selector": [ 11 | true, 12 | "element", 13 | "app", 14 | "kebab-case" 15 | ] 16 | } 17 | } 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "baseUrl": "./", 5 | "downlevelIteration": true, 6 | "outDir": "./dist/out-tsc", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "experimentalDecorators": true, 12 | "importHelpers": true, 13 | "target": "es2015", 14 | "typeRoots": [ 15 | "node_modules/@types" 16 | ], 17 | "lib": [ 18 | "es2018", 19 | "dom" 20 | ] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /tslint.json: -------------------------------------------------------------------------------- 1 | { 2 | "rulesDirectory": [ 3 | "codelyzer" 4 | ], 5 | "rules": { 6 | "arrow-return-shorthand": true, 7 | "callable-types": true, 8 | "class-name": true, 9 | "comment-format": [ 10 | true, 11 | "check-space" 12 | ], 13 | "curly": true, 14 | "deprecation": { 15 | "severity": "warn" 16 | }, 17 | "eofline": true, 18 | "forin": true, 19 | "import-blacklist": [ 20 | true, 21 | "rxjs/Rx" 22 | ], 23 | "import-spacing": true, 24 | "indent": [ 25 | true, 26 | "spaces" 27 | ], 28 | "interface-over-type-literal": true, 29 | "label-position": true, 30 | "max-line-length": [ 31 | true, 32 | 140 33 | ], 34 | "member-access": false, 35 | "member-ordering": [ 36 | true, 37 | { 38 | "order": [ 39 | "static-field", 40 | "instance-field", 41 | "static-method", 42 | "instance-method" 43 | ] 44 | } 45 | ], 46 | "no-arg": true, 47 | "no-bitwise": true, 48 | "no-console": [ 49 | true, 50 | "debug", 51 | "info", 52 | "time", 53 | "timeEnd", 54 | "trace" 55 | ], 56 | "no-construct": true, 57 | "no-debugger": true, 58 | "no-duplicate-super": true, 59 | "no-empty": false, 60 | "no-empty-interface": true, 61 | "no-eval": true, 62 | "no-inferrable-types": [ 63 | true, 64 | "ignore-params" 65 | ], 66 | "no-misused-new": true, 67 | "no-non-null-assertion": true, 68 | "no-redundant-jsdoc": true, 69 | "no-shadowed-variable": true, 70 | "no-string-literal": false, 71 | "no-string-throw": true, 72 | "no-switch-case-fall-through": true, 73 | "no-trailing-whitespace": true, 74 | "no-unnecessary-initializer": true, 75 | "no-unused-expression": true, 76 | "no-use-before-declare": true, 77 | "no-var-keyword": true, 78 | "object-literal-sort-keys": false, 79 | "one-line": [ 80 | true, 81 | "check-open-brace", 82 | "check-catch", 83 | "check-else", 84 | "check-whitespace" 85 | ], 86 | "prefer-const": true, 87 | "quotemark": [ 88 | true, 89 | "single" 90 | ], 91 | "radix": true, 92 | "semicolon": [ 93 | true, 94 | "always" 95 | ], 96 | "triple-equals": [ 97 | true, 98 | "allow-null-check" 99 | ], 100 | "typedef-whitespace": [ 101 | true, 102 | { 103 | "call-signature": "nospace", 104 | "index-signature": "nospace", 105 | "parameter": "nospace", 106 | "property-declaration": "nospace", 107 | "variable-declaration": "nospace" 108 | } 109 | ], 110 | "unified-signatures": true, 111 | "variable-name": false, 112 | "whitespace": [ 113 | true, 114 | "check-branch", 115 | "check-decl", 116 | "check-operator", 117 | "check-separator", 118 | "check-type" 119 | ], 120 | "no-output-on-prefix": true, 121 | "no-inputs-metadata-property": true, 122 | "no-outputs-metadata-property": true, 123 | "no-host-metadata-property": true, 124 | "no-input-rename": true, 125 | "no-output-rename": true, 126 | "use-lifecycle-interface": true, 127 | "use-pipe-transform-interface": true, 128 | "component-class-suffix": true, 129 | "directive-class-suffix": true 130 | } 131 | } 132 | --------------------------------------------------------------------------------