mirror of
https://github.com/Matysh/houseplan-card
synced 2026-07-31 08:28:31 +00:00
feat v1.18.0: live segment ruler while drawing rooms + per-space scale
- Draw mode shows the current segment length near the cursor (metres or feet+inches per the HA unit system). New pure helpers segmentCm/formatLength (unit-tested). - New per-space 'Scale (grid cell size)' field (space.cell_cm, default 5 cm) so each plan has its own real-world dimensions. i18n en/ru, .measurelabel style. +3 tests (61).
This commit is contained in:
@@ -11,7 +11,7 @@ PLANS_DIR = "houseplan/plans" # relative to the HA configuration directory
|
|||||||
FILES_URL = "/houseplan_files/files"
|
FILES_URL = "/houseplan_files/files"
|
||||||
FILES_DIR = "houseplan/files"
|
FILES_DIR = "houseplan/files"
|
||||||
CONF_ADMIN_ONLY = "admin_only"
|
CONF_ADMIN_ONLY = "admin_only"
|
||||||
VERSION = "1.17.2"
|
VERSION = "1.18.0"
|
||||||
|
|
||||||
DEFAULT_CONFIG: dict = {
|
DEFAULT_CONFIG: dict = {
|
||||||
"spaces": [],
|
"spaces": [],
|
||||||
|
|||||||
File diff suppressed because one or more lines are too long
@@ -16,5 +16,5 @@
|
|||||||
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
"issue_tracker": "https://github.com/Matysh/houseplan-card/issues",
|
||||||
"requirements": [],
|
"requirements": [],
|
||||||
"single_config_entry": true,
|
"single_config_entry": true,
|
||||||
"version": "1.17.2"
|
"version": "1.18.0"
|
||||||
}
|
}
|
||||||
|
|||||||
Vendored
+26
-4
File diff suppressed because one or more lines are too long
@@ -1,5 +1,16 @@
|
|||||||
# Changelog
|
# Changelog
|
||||||
|
|
||||||
|
## v1.18.0 — 2026-07-14 (live measurements while drawing rooms + per-space scale)
|
||||||
|
- **Ruler while drawing.** In room-markup "draw" mode, a badge follows the cursor showing the
|
||||||
|
length of the current segment (last placed vertex → cursor). Units come from the HA unit
|
||||||
|
system: metric → metres ("1.25 m"), imperial → feet+inches ("4′ 1″").
|
||||||
|
- **Per-space scale.** New "Scale (grid cell size)" field in the space dialog — cm represented
|
||||||
|
by one grid cell (default **5 cm**, i.e. 240 cells ≈ 12 m). Stored as `space.cell_cm`; each
|
||||||
|
plan can have its own real-world size.
|
||||||
|
- Pure helpers `segmentCm` / `formatLength` in logic.ts (unit-tested); `_fmtLen` +
|
||||||
|
`_renderMeasureLabel` in the card; `.measurelabel` style; i18n `space.scale_label`/`scale_unit`.
|
||||||
|
(+3 tests: 58 → 61 frontend.)
|
||||||
|
|
||||||
## v1.17.2 — 2026-07-11 (humidity badge: gate on the sensor, not the icon)
|
## v1.17.2 — 2026-07-11 (humidity badge: gate on the sensor, not the icon)
|
||||||
- Fix on top of v1.17.1: the humidity `%` badge is now shown whenever the marker's primary
|
- Fix on top of v1.17.1: the humidity `%` badge is now shown whenever the marker's primary
|
||||||
entity is a humidity sensor (`device_class: humidity`), regardless of the resolved icon.
|
entity is a humidity sensor (`device_class: humidity`), regardless of the resolved icon.
|
||||||
|
|||||||
+2
-2
@@ -13,14 +13,14 @@
|
|||||||
|
|
||||||
| Item | State |
|
| Item | State |
|
||||||
|---|---|
|
|---|---|
|
||||||
| Version | **v1.17.2** everywhere (manifest, const.py, package.json, CARD_VERSION) |
|
| Version | **v1.18.0** everywhere (manifest, const.py, package.json, CARD_VERSION) |
|
||||||
| GitHub | https://github.com/Matysh/houseplan-card — branch `main`, releases v1.9.3…v1.11.2 |
|
| GitHub | https://github.com/Matysh/houseplan-card — branch `main`, releases v1.9.3…v1.11.2 |
|
||||||
| CI | `.github/workflows/validate.yml` (hacs + hassfest + frontend + backend) — **fully green** since v1.11.1; `release.yml` auto-attaches the card bundle (needs `permissions: contents: write`, fixed) |
|
| CI | `.github/workflows/validate.yml` (hacs + hassfest + frontend + backend) — **fully green** since v1.11.1; `release.yml` auto-attaches the card bundle (needs `permissions: contents: write`, fixed) |
|
||||||
| HACS | Works as custom repository (id 1290210112 on the home instance). **Inclusion PR: https://github.com/hacs/default/pull/9004** (queue ≈2 months as of 2026-07). Lesson: #8995 was auto-closed by hacs-bot — the PR body MUST be their exact template with every checkbox ticked and all 3 links (release, HACS action run, hassfest run); a custom body gets closed without discussion |
|
| HACS | Works as custom repository (id 1290210112 on the home instance). **Inclusion PR: https://github.com/hacs/default/pull/9004** (queue ≈2 months as of 2026-07). Lesson: #8995 was auto-closed by hacs-bot — the PR body MUST be their exact template with every checkbox ticked and all 3 links (release, HACS action run, hassfest run); a custom body gets closed without discussion |
|
||||||
| Brands | Ships **inside the integration**: `custom_components/houseplan/brand/{icon,icon@2x,logo,logo@2x}.png` (HA ≥2026.3 local-brands mechanism). home-assistant/brands PR #10700 was auto-closed — that repo no longer accepts custom integrations |
|
| Brands | Ships **inside the integration**: `custom_components/houseplan/brand/{icon,icon@2x,logo,logo@2x}.png` (HA ≥2026.3 local-brands mechanism). home-assistant/brands PR #10700 was auto-closed — that repo no longer accepts custom integrations |
|
||||||
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed v1.11.2, installed *via HACS* (custom repo) — updates flow through HACS now |
|
| Home instance | ha.jbstudio.pro (SSH port 323, key `ha_jb`), deployed v1.11.2, installed *via HACS* (custom repo) — updates flow through HACS now |
|
||||||
| Localization | UI en/ru (`src/i18n.ts`), auto by `hass.locale` + `language` card option; codebase and docs are English-first (`README.ru.md` is the Russian copy) |
|
| Localization | UI en/ru (`src/i18n.ts`), auto by `hass.locale` + `language` card option; codebase and docs are English-first (`README.ru.md` is the Russian copy) |
|
||||||
| Tests | 58 frontend (node:test, incl. a 12-test buildDevices suite on a fake hass) + 10 pure backend (anywhere) + 12 HA-harness backend (CI only, py3.13; skipped locally — sandbox has py3.10) |
|
| Tests | 61 frontend (node:test, incl. a 12-test buildDevices suite on a fake hass) + 10 pure backend (anywhere) + 12 HA-harness backend (CI only, py3.13; skipped locally — sandbox has py3.10) |
|
||||||
|
|
||||||
## Recent milestones (details in CHANGELOG.md)
|
## Recent milestones (details in CHANGELOG.md)
|
||||||
|
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
{
|
{
|
||||||
"name": "houseplan-card",
|
"name": "houseplan-card",
|
||||||
"version": "1.17.2",
|
"version": "1.18.0",
|
||||||
"description": "Interactive house plan Lovelace card for Home Assistant",
|
"description": "Interactive house plan Lovelace card for Home Assistant",
|
||||||
"license": "MIT",
|
"license": "MIT",
|
||||||
"type": "module",
|
"type": "module",
|
||||||
|
|||||||
+41
-1
@@ -13,6 +13,7 @@ import {
|
|||||||
} from './rules';
|
} from './rules';
|
||||||
import {
|
import {
|
||||||
lqiColor, snapToGrid, segKey as segKeyOf, samePoint, pointInPolygon, markerIdForBinding,
|
lqiColor, snapToGrid, segKey as segKeyOf, samePoint, pointInPolygon, markerIdForBinding,
|
||||||
|
segmentCm, formatLength,
|
||||||
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
averageLqi, fitView, declump, safeUrl, resolveTapAction, floorsOf, type FloorInfo,
|
||||||
spaceDisplayOf, roomFillColor, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY,
|
spaceDisplayOf, roomFillColor, DEFAULT_ROOM_COLOR, DEFAULT_ROOM_OPACITY,
|
||||||
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
|
DEFAULT_TEMP_MIN, DEFAULT_TEMP_MAX, type SpaceDisplay,
|
||||||
@@ -26,7 +27,7 @@ import './space-card';
|
|||||||
import { cardStyles } from './styles';
|
import { cardStyles } from './styles';
|
||||||
import { langOf, t, type I18nKey } from './i18n';
|
import { langOf, t, type I18nKey } from './i18n';
|
||||||
|
|
||||||
const CARD_VERSION = '1.17.2';
|
const CARD_VERSION = '1.18.0';
|
||||||
const LS_KEY = 'houseplan_card_layout_v1';
|
const LS_KEY = 'houseplan_card_layout_v1';
|
||||||
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
const LS_CFG = 'houseplan_card_cfg_v1'; // cache of the server config+layout for instant rendering
|
||||||
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
const LS_ZOOM = 'houseplan_card_zoom_v1';
|
||||||
@@ -132,6 +133,7 @@ class HouseplanCard extends LitElement {
|
|||||||
fillMode: 'none' | 'lqi' | 'light' | 'temp';
|
fillMode: 'none' | 'lqi' | 'light' | 'temp';
|
||||||
tempMin: number;
|
tempMin: number;
|
||||||
tempMax: number;
|
tempMax: number;
|
||||||
|
cellCm: number; // real-world cm represented by one grid cell
|
||||||
busy: boolean;
|
busy: boolean;
|
||||||
} | null = null;
|
} | null = null;
|
||||||
private _keyHandler = (e: KeyboardEvent) => this._onKey(e);
|
private _keyHandler = (e: KeyboardEvent) => this._onKey(e);
|
||||||
@@ -944,6 +946,18 @@ class HouseplanCard extends LitElement {
|
|||||||
return NORM_W / GRID_N;
|
return NORM_W / GRID_N;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** cm represented by one grid cell for the current space (default 5). */
|
||||||
|
private get _cellCm(): number {
|
||||||
|
const v = Number(this._curSpaceCfg?.cell_cm);
|
||||||
|
return Number.isFinite(v) && v > 0 ? v : 5;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Human-readable length of a segment (render units) using the HA unit system. */
|
||||||
|
private _fmtLen(a: number[], b: number[]): string {
|
||||||
|
const cm = segmentCm(a, b, this._gridPitch, this._cellCm);
|
||||||
|
return formatLength(cm, this.hass?.config?.unit_system?.length === 'mi');
|
||||||
|
}
|
||||||
|
|
||||||
private get _curSpaceCfg(): any {
|
private get _curSpaceCfg(): any {
|
||||||
return this._serverCfg?.spaces.find((s: any) => s.id === this._space);
|
return this._serverCfg?.spaces.find((s: any) => s.id === this._space);
|
||||||
}
|
}
|
||||||
@@ -1509,6 +1523,7 @@ class HouseplanCard extends LitElement {
|
|||||||
showBorders: disp.showBorders, showNames: disp.showNames,
|
showBorders: disp.showBorders, showNames: disp.showNames,
|
||||||
roomColor: disp.color, roomOpacity: disp.opacity, fillMode: disp.fill,
|
roomColor: disp.color, roomOpacity: disp.opacity, fillMode: disp.fill,
|
||||||
tempMin: disp.tempMin, tempMax: disp.tempMax,
|
tempMin: disp.tempMin, tempMax: disp.tempMax,
|
||||||
|
cellCm: Number(sp.cell_cm) > 0 ? Number(sp.cell_cm) : 5,
|
||||||
busy: false,
|
busy: false,
|
||||||
};
|
};
|
||||||
} else {
|
} else {
|
||||||
@@ -1518,6 +1533,7 @@ class HouseplanCard extends LitElement {
|
|||||||
showBorders: false, showNames: false,
|
showBorders: false, showNames: false,
|
||||||
roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'none',
|
roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'none',
|
||||||
tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX,
|
tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX,
|
||||||
|
cellCm: 5,
|
||||||
busy: false,
|
busy: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -1602,6 +1618,7 @@ class HouseplanCard extends LitElement {
|
|||||||
temp_min: Number.isFinite(d.tempMin) ? Math.min(d.tempMin, d.tempMax) : DEFAULT_TEMP_MIN,
|
temp_min: Number.isFinite(d.tempMin) ? Math.min(d.tempMin, d.tempMax) : DEFAULT_TEMP_MIN,
|
||||||
temp_max: Number.isFinite(d.tempMax) ? Math.max(d.tempMin, d.tempMax) : DEFAULT_TEMP_MAX,
|
temp_max: Number.isFinite(d.tempMax) ? Math.max(d.tempMin, d.tempMax) : DEFAULT_TEMP_MAX,
|
||||||
};
|
};
|
||||||
|
sp.cell_cm = Number.isFinite(d.cellCm) && d.cellCm > 0 ? d.cellCm : 5;
|
||||||
await this._saveConfigNow();
|
await this._saveConfigNow();
|
||||||
this._spaceDialog = null;
|
this._spaceDialog = null;
|
||||||
if (d.mode === 'create') this._space = sp.id;
|
if (d.mode === 'create') this._space = sp.id;
|
||||||
@@ -1690,6 +1707,7 @@ class HouseplanCard extends LitElement {
|
|||||||
showBorders: false, showNames: false,
|
showBorders: false, showNames: false,
|
||||||
roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'none',
|
roomColor: DEFAULT_ROOM_COLOR, roomOpacity: DEFAULT_ROOM_OPACITY, fillMode: 'none',
|
||||||
tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX,
|
tempMin: DEFAULT_TEMP_MIN, tempMax: DEFAULT_TEMP_MAX,
|
||||||
|
cellCm: 5,
|
||||||
busy: false,
|
busy: false,
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
@@ -2007,6 +2025,9 @@ class HouseplanCard extends LitElement {
|
|||||||
${disp.showNames && !this._markup
|
${disp.showNames && !this._markup
|
||||||
? space.rooms.map((r) => this._renderRoomLabel(r, space, view, disp))
|
? space.rooms.map((r) => this._renderRoomLabel(r, space, view, disp))
|
||||||
: nothing}
|
: nothing}
|
||||||
|
${this._markup && this._tool === 'draw' && this._path.length && this._cursorPt && !this._contourClosed
|
||||||
|
? this._renderMeasureLabel(view)
|
||||||
|
: nothing}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
${this._zoom > 1
|
${this._zoom > 1
|
||||||
@@ -2128,6 +2149,15 @@ class HouseplanCard extends LitElement {
|
|||||||
>${r.name}</div>`;
|
>${r.name}</div>`;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Length badge that follows the cursor while drawing the current segment. */
|
||||||
|
private _renderMeasureLabel(view: { x: number; y: number; w: number; h: number }): TemplateResult {
|
||||||
|
const a = this._path[this._path.length - 1];
|
||||||
|
const b = this._cursorPt!;
|
||||||
|
const left = ((b[0] - view.x) / view.w) * 100;
|
||||||
|
const top = ((b[1] - view.y) / view.h) * 100;
|
||||||
|
return html`<div class="measurelabel" style="left:${left}%;top:${top}%">${this._fmtLen(a, b)}</div>`;
|
||||||
|
}
|
||||||
|
|
||||||
private _roomCenter(r: RoomCfg): number[] {
|
private _roomCenter(r: RoomCfg): number[] {
|
||||||
if (r.poly) {
|
if (r.poly) {
|
||||||
const n = r.poly.length;
|
const n = r.poly.length;
|
||||||
@@ -2402,6 +2432,16 @@ class HouseplanCard extends LitElement {
|
|||||||
</select>`
|
</select>`
|
||||||
: nothing}
|
: nothing}
|
||||||
|
|
||||||
|
<label>${this._t('space.scale_label')}</label>
|
||||||
|
<div class="colorrow">
|
||||||
|
<input class="namein tempin" type="number" min="0.1" step="0.1" .value=${String(d.cellCm)}
|
||||||
|
@input=${(e: Event) => {
|
||||||
|
const n = parseFloat((e.target as HTMLInputElement).value);
|
||||||
|
this._spaceDialog = { ...d, cellCm: Number.isFinite(n) && n > 0 ? n : d.cellCm };
|
||||||
|
}} />
|
||||||
|
<span class="opl">${this._t('space.scale_unit')}</span>
|
||||||
|
</div>
|
||||||
|
|
||||||
<label class="dispsection">${this._t('space.display_section')}</label>
|
<label class="dispsection">${this._t('space.display_section')}</label>
|
||||||
<label class="srcrow">
|
<label class="srcrow">
|
||||||
<input type="checkbox" .checked=${d.showBorders}
|
<input type="checkbox" .checked=${d.showBorders}
|
||||||
|
|||||||
@@ -152,6 +152,8 @@
|
|||||||
"import.progress": "Floor {i} of {n}",
|
"import.progress": "Floor {i} of {n}",
|
||||||
"import.done": "Spaces created. Outline the rooms: click grid dots and close the contour.",
|
"import.done": "Spaces created. Outline the rooms: click grid dots and close the contour.",
|
||||||
"btn.skip": "Skip",
|
"btn.skip": "Skip",
|
||||||
|
"space.scale_label": "Scale (grid cell size)",
|
||||||
|
"space.scale_unit": "cm per cell",
|
||||||
"space.display_section": "Display",
|
"space.display_section": "Display",
|
||||||
"space.show_borders": "Always show room borders",
|
"space.show_borders": "Always show room borders",
|
||||||
"space.show_names": "Show room names (drag to move)",
|
"space.show_names": "Show room names (drag to move)",
|
||||||
|
|||||||
@@ -152,6 +152,8 @@
|
|||||||
"import.progress": "Этаж {i} из {n}",
|
"import.progress": "Этаж {i} из {n}",
|
||||||
"import.done": "Пространства созданы. Обведите комнаты: кликайте по точкам сетки и замкните контур.",
|
"import.done": "Пространства созданы. Обведите комнаты: кликайте по точкам сетки и замкните контур.",
|
||||||
"btn.skip": "Пропустить",
|
"btn.skip": "Пропустить",
|
||||||
|
"space.scale_label": "Масштаб (размер клетки сетки)",
|
||||||
|
"space.scale_unit": "см на клетку",
|
||||||
"space.display_section": "Отображение",
|
"space.display_section": "Отображение",
|
||||||
"space.show_borders": "Всегда отображать границы комнат",
|
"space.show_borders": "Всегда отображать границы комнат",
|
||||||
"space.show_names": "Отображать названия комнат (перетаскиваются)",
|
"space.show_names": "Отображать названия комнат (перетаскиваются)",
|
||||||
|
|||||||
@@ -13,6 +13,24 @@ export function snapToGrid(v: number, pitch: number): number {
|
|||||||
return Math.round(v / pitch) * pitch;
|
return Math.round(v / pitch) * pitch;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/** Real-world length (cm) of a segment given the grid pitch (render units per cell) and cm per cell. */
|
||||||
|
export function segmentCm(a: number[], b: number[], gridPitch: number, cellCm: number): number {
|
||||||
|
const cells = Math.hypot(b[0] - a[0], b[1] - a[1]) / gridPitch;
|
||||||
|
return cells * cellCm;
|
||||||
|
}
|
||||||
|
|
||||||
|
/** Format a length (cm) for display: metric metres ("1.25 m") or imperial feet+inches ("4′ 1″"). */
|
||||||
|
export function formatLength(cm: number, imperial: boolean): string {
|
||||||
|
if (imperial) {
|
||||||
|
const totalIn = cm / 2.54;
|
||||||
|
let ft = Math.floor(totalIn / 12);
|
||||||
|
let inch = Math.round(totalIn - ft * 12);
|
||||||
|
if (inch === 12) { ft += 1; inch = 0; }
|
||||||
|
return `${ft}′ ${inch}″`;
|
||||||
|
}
|
||||||
|
return `${(cm / 100).toFixed(2)} m`;
|
||||||
|
}
|
||||||
|
|
||||||
/** Canonical key of a segment (independent of direction). */
|
/** Canonical key of a segment (independent of direction). */
|
||||||
export function segKey(a: number[], b: number[]): string {
|
export function segKey(a: number[], b: number[]): string {
|
||||||
const [p, q] = a[0] < b[0] || (a[0] === b[0] && a[1] <= b[1]) ? [a, b] : [b, a];
|
const [p, q] = a[0] < b[0] || (a[0] === b[0] && a[1] <= b[1]) ? [a, b] : [b, a];
|
||||||
|
|||||||
@@ -236,6 +236,20 @@ export const cardStyles = css`
|
|||||||
z-index: 1;
|
z-index: 1;
|
||||||
}
|
}
|
||||||
.roomlabel:active { cursor: grabbing; }
|
.roomlabel:active { cursor: grabbing; }
|
||||||
|
.measurelabel {
|
||||||
|
position: absolute;
|
||||||
|
transform: translate(12px, -150%);
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 600;
|
||||||
|
padding: 1px 6px;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(0, 0, 0, 0.72);
|
||||||
|
color: #fff;
|
||||||
|
white-space: nowrap;
|
||||||
|
pointer-events: none;
|
||||||
|
user-select: none;
|
||||||
|
z-index: 3;
|
||||||
|
}
|
||||||
.rlabel {
|
.rlabel {
|
||||||
fill: var(--hp-muted);
|
fill: var(--hp-muted);
|
||||||
font-size: 15px;
|
font-size: 15px;
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ import assert from 'node:assert/strict';
|
|||||||
import {
|
import {
|
||||||
lqiColor, snapToGrid, segKey, samePoint, pointInPolygon, markerIdForBinding, averageLqi,
|
lqiColor, snapToGrid, segKey, samePoint, pointInPolygon, markerIdForBinding, averageLqi,
|
||||||
fitView, declump, safeUrl, resolveTapAction, floorsOf, subst, spaceDisplayOf, roomFillColor,
|
fitView, declump, safeUrl, resolveTapAction, floorsOf, subst, spaceDisplayOf, roomFillColor,
|
||||||
|
segmentCm, formatLength,
|
||||||
} from '../test-build/logic.js';
|
} from '../test-build/logic.js';
|
||||||
import {
|
import {
|
||||||
iconFor, compileIconRules, isValidPattern, iconFromDeviceClasses,
|
iconFor, compileIconRules, isValidPattern, iconFromDeviceClasses,
|
||||||
@@ -258,3 +259,20 @@ test('spaceDisplayOf: temp bounds default to 20..25 and accept overrides', () =>
|
|||||||
assert.equal(o.tempMin, 18.5);
|
assert.equal(o.tempMin, 18.5);
|
||||||
assert.equal(o.tempMax, 23);
|
assert.equal(o.tempMax, 23);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
test('segmentCm: cells scaled by cm-per-cell', () => {
|
||||||
|
assert.equal(segmentCm([0, 0], [30, 40], 10, 5), 25); // 50 units / pitch 10 = 5 cells * 5cm
|
||||||
|
assert.ok(Math.abs(segmentCm([0, 0], [240, 0], 1000 / 240, 5) - 288) < 1e-9);
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatLength: metric metres with 2 decimals', () => {
|
||||||
|
assert.equal(formatLength(25, false), '0.25 m');
|
||||||
|
assert.equal(formatLength(125, false), '1.25 m');
|
||||||
|
assert.equal(formatLength(0, false), '0.00 m');
|
||||||
|
});
|
||||||
|
|
||||||
|
test('formatLength: imperial feet + inches, with inch rollover', () => {
|
||||||
|
assert.equal(formatLength(124.46, true), '4′ 1″');
|
||||||
|
assert.equal(formatLength(30.48, true), '1′ 0″');
|
||||||
|
assert.equal(formatLength(29.464, true), '1′ 0″');
|
||||||
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user