# HART Estate Widget

The package is designed to present 2D and 3D floor plans generated by the AI service [getfloorplan.com](https://getfloorplan.com).

# Table of Contents
- [Quick Start](#quick-start)
  - [Example vite.config.ts](#vite)
  - [Example index.html](#html)
  - [Example index.js](#javascript)
  - [Example index.sass](#sass)
- [Docs](#docs)
  - [Parameters](#parameters)
  - [REST API Object](#widget-object-from-the-rest-api)
- [Versioning](#versioning)
- [Copyright and License](#copyright-and-license)

# Quick Start

As a result, you will receive a website that can display various floor plans.

![node: ^18.0.0](https://img.shields.io/badge/nodeJS-^18.0.0-blue)

- Download NodeJS 18+
- Create a new project
```shell
npm init
```
- Download the required packages
```shell
npm install -S hart-estate-widget@4.0.0  # GFP Widget package
npm install -S -D vite                   # Vite bundler
npm install -S -D sass-embedded          # Sass style compiler
```
- Example structure of project
```
.
├── src
│   ├── index.html
│   ├── index.js
│   ├── index.sass
│   └── logo.png
├── package.json
└── package-lock.json
```
- Copy-paste sample assets from below.
- Run with `rm -rf dist && npx vite`
- Open browser at:
http://localhost:1234/?id=73b833c3-072a-4ac2-9f5d-7f7ac3d1fc9c

> The query `id` parameter accepts the UUID4 received from [getfloorplan.com](https://getfloorplan.com)
> You can use these UUID4 for test purposes:
> - Scandy style: `228ba1dd-64d3-4d33-bcd7-b4c670bed40e`
> - Boho style: `f9032373-bb2c-416e-b0ab-20b8fd24d482`
> - England style: `b21871a2-2d5b-4beb-817b-ed750eebab9a`
> - Neutral style: `e8553134-0457-488c-8d3e-611b0e2be4d4`
> - Modern style: `73b833c3-072a-4ac2-9f5d-7f7ac3d1fc9c`

## VITE
Insert the example into a file `vite.config.ts`

```ts
import { defineConfig } from 'vite';

export default defineConfig({
  root: 'src',
  build: { outDir: '../dist' },
  server: { port: 1234, open: true },
});
```

## HTML
Insert the example into a file `src/index.html`

```html
<!DOCTYPE html>
<html lang="en">
  <head>
    <meta charset="utf-8">
    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=0" />
    <title>HART Estate Widget</title>

    <script type="module" src="./index.js"></script>
    <link rel="icon" href="./logo.png">
  </head>

  <body>
    <div id="widget"></div>
  </body>
</html>
```

## JavaScript
Insert the example into a file `src/index.js`

```js
import { Api } from 'hart-estate-widget/build/api.js';
import { usePreloader } from 'hart-estate-widget/build/usePreloader.js';

const searchParams = new URLSearchParams(document.location.search);
const planId = searchParams.get('id');
const crmPlanId = searchParams.get('crmPlanId');

const baseUrl = 'https://backend.estate.hart-digital.com';

const loadData = async () => {
  const {loadCrmWidgetData, loadWidgetData} = new Api(baseUrl);
  return crmPlanId ? loadCrmWidgetData(crmPlanId) : loadWidgetData(planId);
};
const loadCreateWidget = async () => (await import('hart-estate-widget/build/createWidget.js')).createWidget;
const loadFovPlugin = async () => (await import('hart-estate-widget/build/plugins/FovPlugin.js')).FovPlugin;
const loadLogoUrl = async () => (await import('./logo.png')).default;
const loadStyle = async () => (await import('./index.sass')).default;
const loadDocument = async () => new Promise((resolve) => (window.onload = resolve));

const queue = [
  loadData(),
  loadCreateWidget(),
  loadFovPlugin(),
  loadLogoUrl(),
  loadStyle(),
  loadDocument(),
];

const cleanupPreloader = usePreloader();

Promise.all(queue).then(([data, createWidget, FovPlugin, logoUrl]) => {
  const options = {
    baseUrl,
    logoUrl,
    logoLinkUrl: 'https://getfloorplan.com',
  };

  const plugins = [
    new FovPlugin(2),
  ];

  createWidget('#widget', data, options, plugins).then(cleanupPreloader);
});
```

## SASS
Insert the example into a file `src/index.sass`

```sass
*, *:before, *:after
  -webkit-font-smoothing: antialiased
  -moz-osx-font-smoothing: grayscale
  font-family: 'Proxima Nova', sans-serif
  text-decoration: none
  font-weight: 400
  color: #fff
  outline: none
  padding: 0
  margin: 0
  box-sizing: border-box
  -webkit-box-sizing: border-box

html, body
  width: 100%
  height: 100%

#widget
  width: 100%
  height: 100%
  overflow: hidden
```

# Docs
## Parameters:
Here you can see a list of accessible options and examples of usage. There are accessible values for each option below in the block "Types of Elements".

```json
{
  // Widget API base URL
  "baseUrl": "https://backend.estate.hart-digital.com",

  // Widget locale can be one of
  // "en" - English language
  // "ru" - Russian language
  // "de" - German language
  // "es" - Spanish language
  // "ja" - Japanese language
  // "nl" - Dutch language
  // "bg" - Bulgarian language
  // "he" - Hebrew language
  "locale": "en",

  // Path/link to the logo
  "logoUrl": "https://getfloorplan.com/img/logo.58f6cd11.svg",
  // Link opened when logo is clicked
  "logoLinkUrl": "https://getfloorplan.com",
  // Widget color settings in CSS color formal like: "rgba(255, 255, 255, 0)", "#ABCDEF", etc...
  // "main" - main color of buttons, elements
  // "mainText" - text color for buttons, elements contrasting with the main color
  "colors": {
    "main": "#FFA900",
    "mainText": "#413E3E"
  },

  // First opened tab
  "primaryTab": "panorama",
  // Available widget tabs array with order:
  // "vr" - VR button
  // "panorama" - Panorama 360 tab
  // "rotation" - Isometric plan rotation tab
  // "plan" - Original plan tab
  // "carousel" - 2d tab with middle cuts, plan and topView
  "tabs": ["plan", "rotation", "panorama"],

  // Controls in bottom bar, can be:
  // "ruler" - Ruler enable/disable button (panorama tab only)
  // "scale" - Scale x1, x2, x0.5 button (panorama tab only)
  // "autoRotate" - Auto rotate enable/disable button (panorama tab only)
  // "minusScale" - Scale minus button (panorama tab only)
  // "plusScale" - Scale plus button (panorama tab only)
  // "furnished" - Furnished / bareshell button (panorama tab only)
  // "prevPanorama" - Prev panorama button (panorama tab only)
  // "nextPanorama" - Next panorama button (panorama tab only)
  // "2d" - 2D tab button
  // "3d" - 3D tab button
  // "360" - 360 tab button
  // "isometry" - Isometry rotation arrows (rotation tab only)
  // "floors" - Floors select with reverse order inside application frame
  // "carousel" - carousel tab button with plan, top view, middle cuts
  "controls": ["ruler", "scale", "autoRotate"],

  // Enable/disable modal of instruction in 3D tour
  "isInstructionsVisible": true,
  // Enable/disable modal of instruction in ruler
  "isRulerInstructionVisible": true,
  // Show the ruler's instructions only once per session
  "isRulerInstructionShownOnce": false,
  // Enable/disable auto rotation in 3D tour
  "isAutoRotate": false,
  // Enable/disable device gyroscope for AR
  "isGyroscopeEnabled": true,
  // Show/hide fullscreen button
  "isFullscreenButtonVisible": true,
  // Show/hide current room type top label
  "isRoomLabelVisible": false,
  // Show next scale text or current
  // true: next (current: 0.5, shows: 1x)
  // false: current (current: 0.5, shows: 0.5x)
  "isShowNextScaleText": false,
  // Enable InteractiveRotationTab
  // true: InteractiveRotationTab will show on interactive_top_view capability
  // false: always use default RotationTab
  "isInteractiveTabAvailable": true,

  // Use multifloor map instead of default or topView
  "isMultifloorMap": false,
  // Automatic crop transparent part of images by rasterizer (may lag a bit)
  "isNeedCropMultifloorMap": true,
  // Precision of multifloor map crop
  "multiFloorMapCropPrecision": 100,

  // Move panorama control buttons to the application context
  "isAbsolutePanoramaControls": false,
  // Show prev/next room type labels on prev/next panorama buttons
  "isShowLabelsOnPrevNext": true,
  // Render links as div elements in UI layer
  "isOverrideLinks": false,
  // Enable room indexer button in rotation tab
  "isRoomIndexerVisible": false,
  // Enable room indexer button to rotate middle cuts
  "isRoomIndexerRotate": false,
  // Direction of rotation of rooms
  "indexerButtonDirection": false,
  // Show top view on rotation tab
  "isShowTopViewOnRotationTab": true,
  // Show left and right arrows in room scroll container
  "isInteractiveScrollArrowsVisible": true,
  // Show the "hide furniture" button with a separate switch
  "isHiddenFurnitureSwitchVisible": true,

  // Apply scale offset in 2D/3D
  "isTransformScaleWithOffset": true,
  // Apply transform offset in 2D/3D
  "isTransformEnabled": true,
  // Apply scale offset from screen center instead of cursor
  "isTransformAlongScreenCenter": false,
  // Resolve zero angle in middle cuts to rotate from top view more angle-seamlessly
  "isNeedMiddleCutZeroAngle": false,
  // Show VR button (on VR device connected)
  "isVRVisible": true,

  // Rotate the panorama using the left mouse button in any mode
  "isPanoramaLeftMouseRotationOnly": false,

  // Apply a fixed scale that is specified in the scales parameter
  "isFixedScale": false,

  // Inability to scale less than a given value (0.5x / 1x / 2x)
  "minScaleLimit": "0.5x",
  // Inability to scale more than a given value (0.5x / 1x / 2x)
  "maxScaleLimit": "2x",

  // "original" - use original_plan_img
  // "miniplan" - use miniplan_img
  "2DTabBehavior": "miniplan",

  // Base camera persective FOV
  "cameraFov": 75,
  // Custom camera fovs by base FOV Scale
  "cameraFovs": [
    { "text": "1x", "value": 1 },
    { "text": "2x", "value": 0.5 },
    { "text": "4x", "value": 0.25 },
    { "text": "0.5x", "value": 1.35 },
  ],
  // Legacy camera fovs by scale texts (available: x1, x2, x05)
  "scales": ["x1", "x2", "x05"],
  // The size of the links on different scales
  "linkScales": { "x1": 0.1, "x2": 0.1, "x05": 0.1 },
  // Link pulse speed
  "linkPulseSpeed": 0.75,
  // Link pulse amplitude
  "linkPulseAmplitude": 0.08,
  // Link types include regex that should pulse
  "linkPulseTypesIncludeRegex": ".*",
  // Link types exclude regex that should pulse
  "linkPulseTypesExcludeRegex": "door",
  // Link icon include regex that should pulse
  "linkPulseIconsIncludeRegex": "\\w+",
  // Link icon exclude regex that should pulse
  "linkPulseIconsExcludeRegex": "door",
  // Use feet by default
  "isFeetMetric": true,

  // Panorama fade time in seconds
  "panoramaFadeTime": 0.5,

  // Panorama miniplan type, use "svg" or "original"
  "panoramaMiniplanType": "svg",

  // Overrides for interactive opacity
  "interactiveHoverOpacity": 0.65,
  "interactiveActiveOpacity": 0.8,

  // Camera sensitivity
  "cameraHorizontalSensitivity": 1,
  "cameraVerticalSensitivity": 1,
  "mobileCameraHorizontalSensitivity": 2,
  "mobileCameraVerticalSensitivity": 2,

  // Panorama tab config
  "panoramaTabConfig": {
    // Panorama link config
    "linkConfig": {
      // Base sprite size in meters
      "baseSize": 0.1,
      // Sprite scale on hover
      "hoverScale": 1.5,
      // Sprite scale change speed (0 = instant)
      "scaleSpeed": 15,
      // Overlay base width in pixels
      "overlayWidth": 64,
      // Overlay base height in pixels
      "overlayHeight": 64
    },
    // Panorama config
    "panoramaConfig": {
      // Warp offset size
      "warpSize": 0,
      // Second panorama warp offset size
      "secondaryWarpSize": 0,
      // Normalize warp direction to 0..1
      "isNormalizedWarp": true,
      // Force camera rotation on every panorama
      "forceCameraRotation": false
    }
  },

  // Rotation tab config
  "rotationTabConfig": {
    // Index template, example: "{index}/{count}"
    "indexTemplate": "{index}/{count}",

    // Max area error delta to show area instead of size
    "maxAreaErrorDelta": 0.15,

    // Use comma decimal separator in meters
    "isMetersUseComma": false,
    // Meters area fixed precision, example: 1 -> 0.2 ; 2 -> 0.25 ...
    "metersAreaPrecision": 1,
    // Meters size fixed precision, example: 1 -> 0.2 ; 2 -> 0.25 ...
    "metersSizePrecision": 1,
  },

  // Ruler info settings
  "rulerInfoSettings": {
    "fontSize": "20px",
    "fontWeight": "Bold",
    "lineHeight": "1",
    "fontFamily": "Roboto Flex",
    "textAlign": "center",
    "borderRadius": 55,
    "scale": 1.5,
    "width": 256,
    "height": 128,
    "textFillColor": "#0008",
    "textHoverColor": "#000f",
    "fillColor": "#fff8",
    "hoverColor": "#fffa",
    "feetsPositionY": 100,
    "feetsPrecision": 2,
    "useFeetsComma": true,
    "metersPositionY": 50,
    "metersPrecision": 2
  },

  // MultiFloorMap settings
  "multiFloorMap": {
    // Available dimensions: px, vw, vh, vmax, vmin
    // Subtractive margin suffix (applies to vw, vh, vmax and vmin): <dimension>m = <dimension> - m * 2 (margin)
    // Subtractive: vwm, vhm, vmaxm and vminm
    // Example: 512px, 37vh, 10vminm (vmin - 10 * 2), 10vmax

    // Default multifloor map mode with 1-2 floors
    "defaultMode": {
      // Desktop size width/height
      "desktopSize": {
        "width": "37vh",
        "height": "37vh",
      },
      // Extended desktop size width/height
      "extendedDesktopSize": {
        "width": "66vh",
        "height": "66vh",
      },
      // Tablet size width/height
      "tabletSize": {
        "width": "372px",
        "height": "372px",
      },
      // Mobile size width/height
      "mobileSize": {
        "width": "10vminm",
        "height": "10vminm",
      },
      // MultiFloorMap padding
      "padding": 10,
      // MultiFloorMap gap
      "gap": 4,
      // MultiFloorMap maxHeight multiplier
      "maxHeightMultiplier": 0.6,
      // Calculate relative size between images
      "isNeedRelativeSize": false
    },

    // Multifloor map mode with 3 or more floors
    "manyMode": {
      // ...
    }
  },

  // Stairs settings
  "stairs": {
    // Forcing to search camera if upperCameraId or lowerCameraId was not presented
    "forceCameraSearch": false
  },

  // Fallback config
  "assetHostConfig": {
    "version": 1,
    "strategy": {
      "order": ["origin", "group:same", "group:other"],
    },
    "failurePolicy": {
      "http": [403, 404, 410, 429, 451, 500, 502, 503, 504],
      "network": true,
      "cors": true,
      "contentMismatch": true,
    },
    "keyExtraction": {
      "patterns": [
        { "re": "^https?://[^/]+/app_storage_production/public/(.*)$" },
        { "re": "^https?://[^/]+/storage/(.*)$" },
      ],
      "fallback": "pathname-after-first-segment",
    },
    "routes": {
      "cdn-origin.url": {
        "template": "https://cdn-origin.url/{key}",
        "probe": "range",
        "cors": true,
      },
      "cdn2-origin.url": {
        "template": "https://cdn-origin.url/{key}",
        "probe": "head",
        "cors": true,
      },
      // ...
    },
    "groups": {
      "int": [
        "cdn-origin.url",
        // ...
      ],
      "ru": [
        "cdn2-origin.url",
        // ...
      ],
    },
  },

  // Lazy load config
  "lazyLoad": {
    // Is lazy load enabled
    "enabled": true,

    // Strategies (select only one of presented)
    // Nearest cameras on the same floor
    "strategy": {
      "type": "nearest",
      // Preload cameras count
      "count": 3
    },

    // Preload all style on the same floor/camera
    "strategy": {
      "type": "style",
      // Search for another camera with the same position if no same ID
      "usePositionFallback": true
    }
  },

  // Widget external integrations
  "integrations": {
    // https://docs.sentry.io/platforms/javascript/configuration/options/
    "sentry": {
      "dsn": "https://1234567890abcdef@sentry.com/12345"
    }
  },

  // Query parameters for override, only on custom
  "query": {
    "primaryCameraPointId": "CameraPoint123456",
    "primaryFloorNumber": 1,
    "primaryVariantId": "<plan_id>",
    "primaryStyleName": "scandy",
    "primaryFov": 75,
    "primaryTab": "panorama"
  },

  // Custom link icons
  "linkIcons": {
    "door": "https://site.com/door.png",
    "doorHover": "https://site.com/door-hover.png",
    "spot": "https://site.com/spot.png",
    "spotHover": "https://site.com/spot-hover.png",
    "stairUp": "https://site.com/stairUp.png",
    "stairUpHover": "https://site.com/stairUp-hover.png",
    "stairDown": "https://site.com/stairDown.png",
    "stairDownHover": "https://site.com/stairDown-hover.png",
  },

  // Custom UI icons
  "uiIcons": {
    "tabs.2D": "https://site.com/2D.png",
    "tabs.3D": "https://site.com/3D.png",
    "tabs.360": "https://site.com/360.png",
    "tabs.roomIndexer": "https://site.com/roomIndexer.png",
    "room.kidroom": "https://site.com/kidroom.png",
    "room.balcony": "https://site.com/balcony.png",
    "room.gym": "https://site.com/gym.png",
    "room.bathroom": "https://site.com/bathroom.png",
    "room.bedroom": "https://site.com/bedroom.png",
    "room.garage": "https://site.com/garage.png",
    "room.hallway": "https://site.com/hallway.png",
    "room.kitchen": "https://site.com/kitchen.png",
    "room.office": "https://site.com/office.png",
    "room.living": "https://site.com/living.png",
    "room.storage": "https://site.com/storage.png",
    "room.terrace": "https://site.com/terrace.png",
    "room.toilet": "https://site.com/toilet.png",
    "float.close": "https://site.com/close.png",
    "float.extend": "https://site.com/extend.png",
    "float.multiMap": "https://site.com/multiMap.png",
    "float.shrink": "https://site.com/shrink.png",
    "float.fullscreen": "https://site.com/fullscreen.png",
    "instructions.move": "https://site.com/move.png",
    "instructions.research": "https://site.com/research.png",
    "instructions.createPoints": "https://site.com/createPoints.png",
    "instructions.deletePoints": "https://site.com/deletePoints.png",
    "controls.rulerOn": "https://site.com/rulerOn.png",
    "controls.rulerOff": "https://site.com/rulerOff.png",
    "controls.prevPanorama": "https://site.com/prevPanorama.png",
    "controls.nextPanorama": "https://site.com/nextPanorama.png",
    "controls.minusScale": "https://site.com/minusScale.png",
    "controls.plusScale": "https://site.com/plusScale.png",
    "controls.autoRotateOn": "https://site.com/autoRotateOn.png",
    "controls.autoRotateOff": "https://site.com/autoRotateOff.png",
    "controls.arrowLeft": "https://site.com/arrowLeft.png",

    // Locale-related icon template: <locale>:<key>
    "ru:tabs.2D": "https://site.ru/2d.svg",
    "ru:tabs.3D": "https://site.ru/3d.svg"
  },

  // Overrides locale keys with custom text or translation
  "localeOverrides": {
    "rotate-plan": "Rotate plan",
    "research-plan": "Research plan",
    "create-points": "Create a point",
    "delete-points": "Remove point",
    "instructions-hint-text": "",
    "ok": "Ok",
    "ruler-ok": "Ok",

    "furnished": "Furnished",
    "bareshell": "Bareshell",

    "feets": "Feets",
    "meters": "Meters",

    "metersAreaTemplate": "$0 m²",
    "metersSizeTemplate": "$0 x $1 m",
    "feetAreaTemplate": "$0",
    "feetSizeTemplate": "$0 x $1",
    "metersFormat": "$0 m",
    "feetsFormat": "$0 ft",

    "made-by-template": "$0 $1",
    "made-by-prefix": "made by",
    "made-by-link": "https://getfloorplan.com/",
    "made-by-text": "getfloorplan.com",

    "style": "Style",

    "floor": "$0 floor",
    "floorNumberEndings": {
      "1": "$0st",
      "2": "$0nd",
      "3": "$0d",
      "rest": "$0th"
    },

    "roomTypes": {
      "KitchenLiving": "KitchLiv",
      // ...
    },
    "fullRoomTypes": {
      "KitchenLiving": "Kitchen Living",
      // ...
    },

    "debugPanel": {
      "header": "Debug Panel",
      "minimizeMaximize": "Minimize/Maximize",
      "planInformation": "Plan information",
      "planID": "Plan ID",
      "serviceID": "Service ID",
      "totalArea": "Total Area (floor)",
      "totalAreaTemplate": "$0 m²",
      "floors": "Floors",
      "rooms": "Rooms (floor / total)",
      "qualityProcessing": "Quality & Processing",
      "rtxQuality": "RTX Quality (panorama)",
      "rtxQualityTopView": "RTX Quality (top view)",
      "processingServer": "Processing Server",
      "primaryVariant": "Primary Variant",
      "variantID": "Variant ID",
      "styleVariants": "Style Variants",
      "navigation": "Navigation",
      "unknown": "Unknown",
      "qualityNames": {
        "-1": "No RTX Preview",
        "0": "Preview",
        "1": "Low",
        "2": "Medium",
        "3": "High",
        "4": "Ultra High",
        "5": "Epic",
        "6": "Unlit"
      },
      "valueNames": {
        "classic": "Classic",
        "proactive": "Proactive",
        "portals": "Portals",
        "mask": "Mask",

        "true": "Enabled",
        "false": "Disabled"
      },
      "capabilityNames": {
        "camera_navigation": "Camera Navigation",
        "floor_navigation": "Floor Navigation",
        "has_panorama": "Has Panorama",
        "hidden_furniture": "Hidden Furniture",
        "interactive_top_view": "Interactive Top View",
        "ruler": "Ruler",
        "top_view_as_miniplan": "Top View Miniplan"
      }
    }
  }
}
```

## Widget object from the REST API

JSON object returned from backend

```js
export default {
  primary_variant: "<id>", // primary variant to open
  variants: [              // all plan variants
    {
      variant_info: {
        style_name: "<name>",          // the name of the variant style
        is_renovation: true,           // is there a renovation in the plan
      },
      plan_id: "<uuid>",               // variant service plan uuid
      json: "<url>",                   // json url for additional plan data
      assets_path: "<path>",           // plan assets base url
      capabilities: { /*...*/ },       // plan capabilities
      rendering_settings: { /*...*/ }, // plan rendering settings

      primary_floor: 0,                // primary plan floor index
      floors: [                        // plan floors
        {
          original_plan_img: "<url>",           // absolute path to original plan image
          miniplan_img: "<url>",                // absolute path to miniplan image

          top_view: {
            image_template: "<template>",       // template for top view decoding
            room_ids_template: "<template>",    // template for top view mask decoding
            items: ["<filename>"],              // top view image names
          },
          isometric_view: {
            image_template: "<template>",       // template for isometric images decoding
            room_ids_template: "<template>",    // template for isometric images masks decoding
            items: ["<filename>"],              // isometric images names
          },
          panorama_view: {
            template: "<template>",             // template for panorama image decoding
            scene_depth_template: "<template>", // template for panorama depth image decoding
            primary_camera_point_id: "<id>",    // primary plan camera id
          },

          location: { X: 0, Y: 0, Z: 0 },        // floor location
          height: 280,                           // floor height

          vertices: [ /*...*/ ],                 // plan vertices data
          rooms: [ /*...*/ ],                    // plan rooms data
          camera_points: [ /*...*/ ],            // plan camera points data
          walls: [ /*...*/ ],                    // plan walls data
          doors: [ /*...*/ ],                    // plan doors data
          stairs: [ /*...*/ ],                   // plan stairs data
          portals: [ /*...*/ ],                  // plan portals data
          apertures: [ /*...*/ ],                // plan apertures data
          decors: [ /*...*/ ],                   // plan decors data

          object_pairs: [],                      // color pairs for furniture data
          room_pairs: [],                        // color pairs for room data
          plan_meta: { /*...*/ },                // plan metadata
        },
      ],
    },
  ],
};
```

# Versioning
For the latest stable version refer to the latest up-to-date version in [Quick Start](#quick-start)

This project is maintained under the [Semantic Versioning guidelines](https://semver.org).

However, some versions are being developed for specific clients. **We do not recommend using them**, as changes in these versions are not documented and may affect your functionality.

# Copyright and license
The project code is licensed under the [GPLv3 license](https://www.gnu.org/licenses/gpl-3.0-standalone.html).

Project code and documentation copyright [hart-digital.com](https://hart-digital.com).
All renders of floor plans copyright [getfloorplan.com](https://getfloorplan.com).
