diff --git a/packages/editor/src/components/editor/floorplan-panel.test.ts b/packages/editor/src/components/editor/floorplan-panel.test.ts index 68b6a5b00f..9e05b54231 100644 --- a/packages/editor/src/components/editor/floorplan-panel.test.ts +++ b/packages/editor/src/components/editor/floorplan-panel.test.ts @@ -2,7 +2,11 @@ import { expect, test } from 'bun:test' import { GuideScaleReference } from '@pascal-app/core' import { z } from 'zod' import { createGuideScaleReference, formatReferenceScaleLabel } from '../../lib/reference-scale' -import { formatFloorplanNumber, parseReferenceScaleLength } from './floorplan-panel' +import { + formatFloorplanNumber, + getReferenceScaleInputError, + parseReferenceScaleLength, +} from './floorplan-panel' const Core106GuideScaleReference = z.object({ start: z.tuple([z.number(), z.number()]), @@ -104,6 +108,14 @@ test('oversized legacy reference labels bypass parsing within the input budget', test('reference-scale labels tolerate malformed persisted values', () => { expect(formatReferenceScaleLabel(runtimeScaleReference({ label: 42 }), 'en')).toBe('42') expect(formatReferenceScaleLabel(runtimeScaleReference({ label: null }), 'en')).toBe('') + expect( + formatReferenceScaleLabel(runtimeScaleReference({ label: { toString: null } }), 'en'), + ).toBe('') + expect( + formatReferenceScaleLabel(runtimeScaleReference({ label: Object.create(null) }), 'en'), + ).toBe('') + expect(formatReferenceScaleLabel(runtimeScaleReference({ label: Symbol('x') }), 'en')).toBe('') + expect(formatReferenceScaleLabel(runtimeScaleReference({ label: ['1 m'] }), 'en')).toBe('') expect( formatReferenceScaleLabel( runtimeScaleReference({ @@ -164,3 +176,45 @@ test('reference-scale schema caps persisted display lengths', () => { ).success, ).toBe(false) }) + +test('reference-scale calibration rejects lengths the schema would not accept', () => { + expect(getReferenceScaleInputError('1000000', 'meters', 'en')).toBeNull() + expect(getReferenceScaleInputError('2.75', 'feet', 'en')).toBeNull() + expect(getReferenceScaleInputError('1000001', 'meters', 'en')).toBe( + 'Length must be at most 1000000 m.', + ) + expect(getReferenceScaleInputError('1e9', 'centimeters', 'en')).toBe( + 'Length must be at most 1000000 cm.', + ) + expect(getReferenceScaleInputError('0', 'meters', 'en')).toBe('Length must be greater than 0.') + expect(getReferenceScaleInputError('', 'meters', 'en')).toBe('Enter the real length of the line.') +}) + +test('reference-scale creation drops display fields outside the schema range', () => { + const base = { + start: [0, 0] as [number, number], + end: [1, 0] as [number, number], + realLengthMeters: 2_000_000, + measuredLengthUnits: 1, + metersPerUnit: 2_000_000, + } + const oversized = createGuideScaleReference(base, 2_000_000, 'meters') + expect(oversized.displayLength).toBeUndefined() + expect(oversized.displayUnit).toBeUndefined() + expect(oversized.label).toBe('2000000 m') + expect(GuideScaleReference.safeParse(oversized).success).toBe(true) + + const atCap = createGuideScaleReference(base, 1_000_000, 'meters') + expect(atCap.displayLength).toBe(1_000_000) + expect(GuideScaleReference.safeParse(atCap).success).toBe(true) +}) + +test('legacy reference-scale labels accept whitespace runs between digit groups', () => { + const label = (value: string) => + formatReferenceScaleLabel(runtimeScaleReference({ label: value }), 'en') + expect(label('1 234,5 m')).toBe('1234.5 m') + expect(label('1 \u00a0234 cm')).toBe('1234 cm') + expect(label('12\u202f345\t\t678 ft')).toBe('12345678 ft') + expect(label('1 234 m')).toBe('1234 m') + expect(label('1 234 ')).toBe('1 234 ') +}) diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx index 2734b764c7..25be8c8af1 100644 --- a/packages/editor/src/components/editor/floorplan-panel.tsx +++ b/packages/editor/src/components/editor/floorplan-panel.tsx @@ -126,6 +126,8 @@ import { formatLinearMeasurement, linearUnitToMeters } from '../../lib/measureme import { createGuideScaleReference, formatReferenceScaleLabel, + isValidReferenceScaleDisplayLength, + MAX_REFERENCE_SCALE_DISPLAY_LENGTH, type ReferenceScaleUnit, } from '../../lib/reference-scale' import { snapRegisteredDraftPoint } from '../../lib/registered-draft-snap' @@ -2487,6 +2489,23 @@ export function parseReferenceScaleLength( ) } +export function getReferenceScaleInputError( + raw: string, + unit: ReferenceScaleUnit, + locale: string, +): string | null { + if (raw.trim() === '') return 'Enter the real length of the line.' + const displayLength = parseReferenceScaleLength(raw, unit, locale) + if (displayLength === null || Number.isNaN(displayLength)) { + return `Enter a length like 3.5, 180cm or 5'11".` + } + if (!(displayLength > 0)) return 'Length must be greater than 0.' + if (!isValidReferenceScaleDisplayLength(displayLength)) { + return `Length must be at most ${formatFloorplanNumber(MAX_REFERENCE_SCALE_DISPLAY_LENGTH, locale)} ${REFERENCE_SCALE_LINGO_UNIT[unit]}.` + } + return null +} + function referenceScaleLengthHint( raw: string, unit: ReferenceScaleUnit, @@ -7168,7 +7187,7 @@ export function FloorplanPanel({ } const displayLength = parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit, locale) - if (!(displayLength && displayLength > 0)) { + if (displayLength === null || !isValidReferenceScaleDisplayLength(displayLength)) { return } @@ -11218,7 +11237,7 @@ export function FloorplanPanel({ const pendingReferenceDisplayLength = parseReferenceScaleLength(referenceScaleValue, referenceScaleUnit, locale) ?? Number.NaN const pendingReferenceRealLengthMeters = - pendingReferenceScale && pendingReferenceDisplayLength > 0 + pendingReferenceScale && isValidReferenceScaleDisplayLength(pendingReferenceDisplayLength) ? convertReferenceLengthToMeters(pendingReferenceDisplayLength, referenceScaleUnit) : null const pendingReferenceMetersPerUnit = @@ -11229,14 +11248,11 @@ export function FloorplanPanel({ pendingReferenceScale && pendingReferenceRealLengthMeters ? pendingReferenceRealLengthMeters / pendingReferenceScale.measuredLengthUnits : null - const referenceScaleInputError = - referenceScaleValue.trim() === '' - ? 'Enter the real length of the line.' - : Number.isNaN(pendingReferenceDisplayLength) - ? `Enter a length like 3.5, 180cm or 5'11".` - : pendingReferenceDisplayLength > 0 - ? null - : 'Length must be greater than 0.' + const referenceScaleInputError = getReferenceScaleInputError( + referenceScaleValue, + referenceScaleUnit, + locale, + ) const referenceScaleHint = referenceScaleInputError ? null : referenceScaleLengthHint(referenceScaleValue, referenceScaleUnit, locale) @@ -11395,7 +11411,7 @@ export function FloorplanPanel({