Skip to content

3D viewer ​

A 3D view of the same plan and markers, built with three.js. It's for viewing only: plans are edited in the 2D editor.

Here is the sample hotel in the exploded view. Drag to orbit, right-drag to pan, scroll to zoom, and click a floor to select it.

Loading the live demo…

Installation ​

three.js is an optional dependency. Install it only if you use the 3D viewer:

bash
npm install three

The 3D viewer has its own import path, so apps that don't import it never load three.js:

ts
import { FloorPlanViewer3D } from '@jbaysolutions/floorplan-vue/3d'
import '@jbaysolutions/floorplan-vue/style.css' // same stylesheet as the 2D components
tsx
import { FloorplanViewer3D } from '@jbaysolutions/floorplan-react/3d'
js
import '@jbaysolutions/floorplan-elements/3d' // defines <floorplan-viewer-3d>

To keep three.js out of your main bundle until it's needed, load the 3D view lazily:

ts
import { defineAsyncComponent } from 'vue'

const Plan3D = defineAsyncComponent(() => import('./Plan3D.vue')) // Plan3D.vue imports FloorPlanViewer3D
tsx
import { Suspense, lazy } from 'react'

const FloorplanViewer3D = lazy(() => import('@jbaysolutions/floorplan-react/3d').then((m) => ({ default: m.FloorplanViewer3D })))

<Suspense fallback={null}>
  <FloorplanViewer3D plan={plan} markers={markers} mode="stacked" style={{ height: 500 }} />
</Suspense>
js
// define <floorplan-viewer-3d> only when it's needed
await import('@jbaysolutions/floorplan-elements/3d')

Usage ​

vue
<div style="height: 600px">
  <FloorPlanViewer3D :plan="plan" :markers="markers" theme="light" mode="stacked" @plan-click="onClick" />
</div>
tsx
<FloorplanViewer3D plan={plan} markers={markers} theme="light" mode="stacked" onPlanClick={onClick} style={{ height: 600 }} />
html
<floorplan-viewer-3d theme="light" mode="stacked" style="height: 600px"></floorplan-viewer-3d>

<script type="module">
  import '@jbaysolutions/floorplan-elements/3d'

  const viewer3d = document.querySelector('floorplan-viewer-3d')
  viewer3d.plan = plan
  viewer3d.markers = markers
  viewer3d.addEventListener('plan-click', (e) => onClick(e.detail))
</script>

plan-click sends the same payload as the 2D viewer ({ x, y, room, floor }, in cm). A marker placed from a 3D click lands in the same spot in 2D, and the other way round.

What is rendered ​

Plan elementIn 3D
WallsEach floor's height minus the slab, with darker tops.
FloorsStacked at their real heights, with a concrete slab under each upper floor. See Multi-floor buildings.
DoorsOpenings 2.10 m high with the leaf standing open; sliding doors are glass panels.
WindowsOpenings from 0.90 m to 2.10 m with glass.
RoomsFloors with their finish: wood, tile, stone, carpet, concrete or grass.
FurnitureLow-poly models for every item in the furniture catalogue, and for registered add-on packs (registerPack3D).
PoolsSunk pools cut into the ground with tiled walls, a sloping floor and see-through water; raised pools stand on the floor, optionally with glass sides. See Outdoor areas.
PlantsBuilt leaf by leaf (leafy plant, monstera, palm, olive tree, ornamental grass). Each one is slightly different, and they scale to the size you give them.
MarkersThe same markers as in 2D, as pins that fade when a wall hides them.

Props ​

PropTypeDefaultDescription
planFloorPlan | null—The plan to show.
floor (v-model:floor)string | nullground floorThe selected floor.
mode (v-model:mode)'floor' | 'stacked' | 'exploded''stacked'Multi-floor view: one floor, the whole building, or floors pulled apart.
showFloorSwitcherboolean | 'auto''auto'Floor buttons (top right) for multi-floor plans.
markersMarker[][]See Markers.
theme'light' | 'dark' | 'blueprint' | 'paper' | 'mist' | 'ink' | 'graphite''light'Blueprint renders a see-through wireframe; the minimal themes a white (or graphite) model.
floorColors'natural' | 'room''natural'Realistic finish colours, or each room's colour. The minimal themes always use flat floors.
showFurniturebooleantrueFurniture layer. Stairs and elevators are part of the building and always show (FIXTURE_KINDS).
showRoomLabelsbooleantrueRoom name and area labels.
showControlsbooleantrue3D / Top, cutaway, auto-rotate and fit buttons.
showCoordinatesbooleantrueLive cursor coordinates.
markerSizenumber34Default marker size in px.
markerVariantMarkerVariant'pin'Default marker style.
alwaysShowLabelsbooleanfalseShow marker labels without hovering.
selectedMarkerIdstring | number | nullnullHighlights one marker.
highlightRoomsstring[][]Room ids whose floors glow.
hoverRoomsbooleantrueGlow the floor under the cursor.
autoRotatebooleanfalseSlowly orbit the model (showroom mode).
initialView'perspective' | 'top''perspective'Starting camera.
fitScalenumber1Multiplies the framing distance of fit() and view changes. Use e.g. 1.3 when overlays cover part of the view.
precisionnumber1Decimal places in emitted coordinates.
cursorstring'crosshair'CSS cursor.

In React the props have the same names (autoRotate, floorColors="room"). On the <floorplan-viewer-3d> element they are properties with the same names (viewer3d.autoRotate = true); simple values also work as kebab-case attributes (mode="exploded" auto-rotate). Objects and arrays (plan, markers, highlightRooms) must be set as properties.

Events ​

EventPayloadDescription
plan-click{ x, y, room, floor, originalEvent }Click on a floor, wall or object (not fired after a drag). Clicking another floor selects it.
marker-click(marker, event)A marker was clicked.
room-hoverRoom | nullThe pointer entered or left a room.
update:floor / update:modestring / modeThe user switched floor / view mode.

WARNING

Markers can't be dragged in 3D. Use the 2D viewer for that.

The same events in React and on the element:

VueReactHTML element (e.detail)
@plan-clickonPlanClick(e)plan-click: the event object { x, y, room, floor, originalEvent }
@marker-clickonMarkerClick(marker, originalEvent)marker-click: { marker, originalEvent }
@room-hoveronRoomHover(room)room-hover: { room }
v-model:floor (update:floor)floor + onFloorChange(floorId)floor property + floor-change: { floor }
v-model:mode (update:mode)mode + onModeChange(mode)mode property + mode-change: { mode }
vue
<FloorPlanViewer3D :plan="plan" v-model:floor="floorId" v-model:mode="mode" @marker-click="(m) => (selected = m.id)" />
tsx
<FloorplanViewer3D
  plan={plan}
  floor={floorId}
  onFloorChange={setFloorId}
  mode={mode}
  onModeChange={setMode}
  onMarkerClick={(m) => setSelected(m.id)}
  style={{ height: 600 }}
/>
js
viewer3d.addEventListener('floor-change', (e) => (floorId = e.detail.floor))
viewer3d.addEventListener('mode-change', (e) => (mode = e.detail.mode))
viewer3d.addEventListener('marker-click', (e) => (selected = e.detail.marker.id))

Methods ​

In Vue, call methods through a template ref. In React, the ref is the <floorplan-viewer-3d> element itself (useRef<FloorplanViewer3DElement>(null)). On the HTML element, call them on the element.

vue
<script setup lang="ts">
import { ref } from 'vue'
import { FloorPlanViewer3D } from '@jbaysolutions/floorplan-vue/3d'

const viewer3d = ref<InstanceType<typeof FloorPlanViewer3D>>()
</script>

<template>
  <FloorPlanViewer3D ref="viewer3d" :plan="plan" />
  <button @click="viewer3d?.setMode('exploded')">Explode</button>
</template>
tsx
const viewer3d = useRef<FloorplanViewer3DElement>(null)

<FloorplanViewer3D ref={viewer3d} plan={plan} style={{ height: 600 }} />
<button onClick={() => viewer3d.current?.setMode('exploded')}>Explode</button>
js
const viewer3d = document.querySelector('floorplan-viewer-3d')
viewer3d.setMode('exploded')
const png = viewer3d.snapshot()
MethodDescription
fit()Frame the whole plan.
setView('perspective' | 'top')Animate to the 3D or the straight-down view.
setFloor(id) / setMode(mode)Select a floor / switch between floor, building and exploded views.
setCutaway(on)Animate a section cut at about 1.1 m through walls and furniture, so you can see into every room.
focus(x, y, distance = 7)Fly the camera to a plan point (distance in metres). In React and on the element: focusPoint(x, y, distance?).
focusMarker(id, distance?)Fly to a marker.
focusRoom(id)Frame a room.
snapshot()Render the current view and return a PNG data: URL (labels and markers are not included).
planToScreen(x, y, floorId?)Screen position (px, relative to the viewer) of a plan point, plus visible.
roomAt({ x, y })The room containing a plan point.

Custom markers ​

Custom marker templates work as in the 2D viewer (see Markers): the marker slot in Vue, renderMarker in React and markerRenderer on the element. In 3D the state is { selected, occluded }, where occluded is true while the marker is behind a wall or slab from the current camera.

Controls ​

  • Mouse: left-drag orbits, right-drag pans, scroll zooms.
  • Touch: one finger orbits, two fingers pan and zoom.

Customising 3D colours ​

3D colours come from THEMES_3D rather than CSS. Change them before the viewer mounts:

ts
import { THEMES_3D } from '@jbaysolutions/floorplan-vue/3d' // or floorplan-react/3d, floorplan-elements/3d

THEMES_3D.light.colors.wall = '#fffaf0' // warm white walls
THEMES_3D.light.colors.cap = '#0f766e'  // teal wall tops

Every built-in theme has an entry: light, dark, blueprint, paper, mist, ink and graphite. The minimal themes also use flatFloors (one indoor and one outdoor floor colour) and mono (how add-on pack and pool colours are muted). See Themes for the 2D side.

Performance notes ​

  • The scene is only redrawn when something changes, so an idle 3D view costs almost nothing.
  • Changing the plan, theme, floor or mode rebuilds the scene. Building mode draws the floors above the selected one as light wireframes, which keeps tall buildings fast.
  • A plan with a few hundred rooms and furniture items runs comfortably on integrated graphics.

JBay Floorplan is free for open source and companies under €1M yearly revenue. Licensing and pricing