@ahrowe/ui 0.27.0 → 0.29.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/common/accordion/accordion.mjs +1 -1
- package/dist/esm/common/accordion/accordion.mjs.map +1 -1
- package/dist/esm/common/accordion/accordion.module.mjs +1 -1
- package/dist/esm/common/accordion/accordion.module.mjs.map +1 -1
- package/dist/esm/common/accordion/accordion.plain.module.mjs +1 -1
- package/dist/esm/common/accordion/accordion.plain.module.mjs.map +1 -1
- package/dist/esm/common/accordion/accordion.primary.module.mjs +1 -1
- package/dist/esm/common/accordion/accordion.primary.module.mjs.map +1 -1
- package/dist/esm/common/actionButtons/actionButtons.mjs.map +1 -1
- package/dist/esm/common/actionButtons/actionButtons.module.mjs +1 -1
- package/dist/esm/common/actionButtons/actionButtons.module.mjs.map +1 -1
- package/dist/esm/common/actionIcon/actionIcon.mjs.map +1 -1
- package/dist/esm/common/actionIcon/actionIcon.module.mjs +1 -1
- package/dist/esm/common/actionIcon/actionIcon.module.mjs.map +1 -1
- package/dist/esm/common/alert/alert.mjs.map +1 -1
- package/dist/esm/common/alert/alert.module.mjs +1 -1
- package/dist/esm/common/animatedIcon/animatedIcon.mjs.map +1 -1
- package/dist/esm/common/animatedIcon/animatedIcon.module.mjs +1 -1
- package/dist/esm/common/animatedIcon/animatedIcon.module.mjs.map +1 -1
- package/dist/esm/common/animatedLogo/animatedLogo.mjs.map +1 -1
- package/dist/esm/common/animatedLogo/animatedLogo.module.mjs +1 -1
- package/dist/esm/common/animatedText/animatedText.mjs +1 -1
- package/dist/esm/common/animatedText/animatedText.mjs.map +1 -1
- package/dist/esm/common/animatedText/animatedText.module.mjs +1 -1
- package/dist/esm/common/animatedText/animatedText.module.mjs.map +1 -1
- package/dist/esm/common/avatar/avatar.mjs.map +1 -1
- package/dist/esm/common/avatar/avatar.module.mjs +1 -1
- package/dist/esm/common/avatar/avatar.module.mjs.map +1 -1
- package/dist/esm/common/avatar/avatarGroup.mjs.map +1 -1
- package/dist/esm/common/badge/badge.mjs.map +1 -1
- package/dist/esm/common/badge/badge.module.mjs +1 -1
- package/dist/esm/common/badge/badge.module.mjs.map +1 -1
- package/dist/esm/common/breadcrumb/breadcrumb.mjs.map +1 -1
- package/dist/esm/common/breadcrumb/breadcrumb.module.mjs +1 -1
- package/dist/esm/common/button/button.mjs.map +1 -1
- package/dist/esm/common/button/button.module.mjs +1 -1
- package/dist/esm/common/button/button.module.mjs.map +1 -1
- package/dist/esm/common/buttonGroup/buttonGroup.mjs.map +1 -1
- package/dist/esm/common/buttonGroup/buttonGroup.module.mjs +1 -1
- package/dist/esm/common/buttonGroup/buttonGroup.module.mjs.map +1 -1
- package/dist/esm/common/card/Card.mjs.map +1 -1
- package/dist/esm/common/card/CardActions.mjs.map +1 -1
- package/dist/esm/common/card/CardHeader.mjs.map +1 -1
- package/dist/esm/common/card/card.module.mjs +1 -1
- package/dist/esm/common/card/card.module.mjs.map +1 -1
- package/dist/esm/common/carousel/carousel.mjs +1 -1
- package/dist/esm/common/carousel/carousel.mjs.map +1 -1
- package/dist/esm/common/carousel/carousel.module.mjs +1 -1
- package/dist/esm/common/carousel/carousel.module.mjs.map +1 -1
- package/dist/esm/common/checkbox/checkbox.mjs.map +1 -1
- package/dist/esm/common/checkbox/checkbox.module.mjs +1 -1
- package/dist/esm/common/checkbox/checkbox.module.mjs.map +1 -1
- package/dist/esm/common/chip/chip.mjs.map +1 -1
- package/dist/esm/common/chip/chip.module.mjs +1 -1
- package/dist/esm/common/chip/chip.module.mjs.map +1 -1
- package/dist/esm/common/colorPicker/colorPicker.mjs +1 -1
- package/dist/esm/common/colorPicker/colorPicker.mjs.map +1 -1
- package/dist/esm/common/colorPicker/colorPicker.module.mjs +1 -1
- package/dist/esm/common/colorPicker/colorPicker.module.mjs.map +1 -1
- package/dist/esm/common/configProvider/configProvider.mjs.map +1 -1
- package/dist/esm/common/configProvider/useComponentDefaults.mjs.map +1 -1
- package/dist/esm/common/confirmModal/confirmModal.mjs +1 -1
- package/dist/esm/common/confirmModal/confirmModal.mjs.map +1 -1
- package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.mjs.map +1 -1
- package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.module.mjs +1 -1
- package/dist/esm/common/datePicker/components/datePickerMonth/datePickerMonth.module.mjs.map +1 -1
- package/dist/esm/common/datePicker/dateFunctions.mjs.map +1 -1
- package/dist/esm/common/datePicker/datePicker.mjs +1 -1
- package/dist/esm/common/datePicker/datePicker.mjs.map +1 -1
- package/dist/esm/common/datePicker/datePicker.module.mjs +1 -1
- package/dist/esm/common/divider/divider.mjs.map +1 -1
- package/dist/esm/common/divider/divider.module.mjs +1 -1
- package/dist/esm/common/drawer/drawer.mjs.map +1 -1
- package/dist/esm/common/drawer/drawer.module.mjs +1 -1
- package/dist/esm/common/drawer/drawer.module.mjs.map +1 -1
- package/dist/esm/common/dropZone/dropZone.mjs +1 -1
- package/dist/esm/common/dropZone/dropZone.mjs.map +1 -1
- package/dist/esm/common/dropZone/dropZone.module.mjs +1 -1
- package/dist/esm/common/dropZone/dropZone.module.mjs.map +1 -1
- package/dist/esm/common/dropdown/dropdown.mjs +1 -1
- package/dist/esm/common/dropdown/dropdown.mjs.map +1 -1
- package/dist/esm/common/dropdown/dropdown.module.mjs +1 -1
- package/dist/esm/common/dropdown/dropdown.module.mjs.map +1 -1
- package/dist/esm/common/emptyState/emptyState.mjs.map +1 -1
- package/dist/esm/common/emptyState/emptyState.module.mjs +1 -1
- package/dist/esm/common/errorBoundary/errorBoundary.mjs.map +1 -1
- package/dist/esm/common/errorBoundary/errorBoundary.module.mjs +1 -1
- package/dist/esm/common/errorBoundary/errorBoundary.module.mjs.map +1 -1
- package/dist/esm/common/fab/components/fabBase/fabBase.mjs.map +1 -1
- package/dist/esm/common/fab/components/fabBase/fabBase.module.mjs +1 -1
- package/dist/esm/common/fab/fab.mjs.map +1 -1
- package/dist/esm/common/fab/fab.module.mjs +1 -1
- package/dist/esm/common/fab/fab.module.mjs.map +1 -1
- package/dist/esm/common/flip/flip.mjs +1 -1
- package/dist/esm/common/flip/flip.mjs.map +1 -1
- package/dist/esm/common/flip/flip.module.mjs +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.mjs +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.mjs.map +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.module.mjs +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.module.mjs.map +1 -1
- package/dist/esm/common/floatingMenu/floatingMenu.types.mjs.map +1 -1
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs +1 -1
- package/dist/esm/common/floatingMenu/useFloatingPosition.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.faces.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.geometry.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.graph.mjs +1 -1
- package/dist/esm/common/floorPlan/floorPlan.graph.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.hit.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.json.mjs +1 -1
- package/dist/esm/common/floorPlan/floorPlan.json.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.mesh.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.path.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.plan.mjs +1 -1
- package/dist/esm/common/floorPlan/floorPlan.plan.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.rooms.mjs +1 -1
- package/dist/esm/common/floorPlan/floorPlan.rooms.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.snap.mjs.map +1 -1
- package/dist/esm/common/floorPlan/floorPlan.triangulate.mjs +1 -1
- package/dist/esm/common/floorPlan/floorPlan.triangulate.mjs.map +1 -1
- package/dist/esm/common/floorPlan/legacySvg.mjs.map +1 -1
- package/dist/esm/common/hooks/useAnchorTracking.mjs +2 -0
- package/dist/esm/common/hooks/useAnchorTracking.mjs.map +1 -0
- package/dist/esm/common/hooks/useCoarsePointer.mjs +1 -1
- package/dist/esm/common/hooks/useFocusBoundary.mjs.map +1 -1
- package/dist/esm/common/hooks/useMediaQuery.mjs +1 -1
- package/dist/esm/common/hooks/useMediaQuery.mjs.map +1 -1
- package/dist/esm/common/hooks/useOverlay.mjs.map +1 -1
- package/dist/esm/common/hooks/useSheet.mjs +1 -1
- package/dist/esm/common/hooks/useSheet.mjs.map +1 -1
- package/dist/esm/common/hooks/useSwipeDismiss.mjs +1 -1
- package/dist/esm/common/hooks/useSwipeDismiss.mjs.map +1 -1
- package/dist/esm/common/iconPicker/iconPicker.mjs +1 -1
- package/dist/esm/common/iconPicker/iconPicker.mjs.map +1 -1
- package/dist/esm/common/iconPicker/iconPicker.module.mjs +1 -1
- package/dist/esm/common/idleManager/idleManager.mjs +1 -1
- package/dist/esm/common/idleManager/idleManager.mjs.map +1 -1
- package/dist/esm/common/infiniteBlock/infiniteBlock.mjs.map +1 -1
- package/dist/esm/common/infiniteBlock/infiniteBlock.module.mjs +1 -1
- package/dist/esm/common/input/input.mjs +1 -1
- package/dist/esm/common/input/input.mjs.map +1 -1
- package/dist/esm/common/input/input.module.mjs +1 -1
- package/dist/esm/common/input/input.module.mjs.map +1 -1
- package/dist/esm/common/input/input.types.mjs.map +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.mjs +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.mjs.map +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.module.mjs +1 -1
- package/dist/esm/common/inputDropdown/inputDropdown.module.mjs.map +1 -1
- package/dist/esm/common/interactableDiv/interactableDiv.mjs.map +1 -1
- package/dist/esm/common/interactableDiv/interactableDiv.module.mjs +1 -1
- package/dist/esm/common/kanbanBoard/KanbanBoard.mjs +1 -1
- package/dist/esm/common/kanbanBoard/KanbanBoard.mjs.map +1 -1
- package/dist/esm/common/kanbanBoard/KanbanColumn.mjs +1 -1
- package/dist/esm/common/kanbanBoard/KanbanColumn.mjs.map +1 -1
- package/dist/esm/common/kanbanBoard/KanbanItem.mjs.map +1 -1
- package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs +1 -1
- package/dist/esm/common/kanbanBoard/kanbanBoard.module.mjs.map +1 -1
- package/dist/esm/common/kanbanBoard/kanbanBoard.utils.mjs +1 -1
- package/dist/esm/common/kanbanBoard/kanbanBoard.utils.mjs.map +1 -1
- package/dist/esm/common/klipyPicker/components/gifPreview/gifPreview.mjs.map +1 -1
- package/dist/esm/common/klipyPicker/components/gifPreview/gifPreview.module.mjs +1 -1
- package/dist/esm/common/klipyPicker/components/gifPreview/gifPreview.module.mjs.map +1 -1
- package/dist/esm/common/klipyPicker/components/gifView/gifView.mjs +1 -1
- package/dist/esm/common/klipyPicker/components/gifView/gifView.mjs.map +1 -1
- package/dist/esm/common/klipyPicker/components/gifView/gifView.module.mjs +1 -1
- package/dist/esm/common/klipyPicker/components/gifView/gifView.module.mjs.map +1 -1
- package/dist/esm/common/klipyPicker/klipyPicker.mjs.map +1 -1
- package/dist/esm/common/klipyPicker/klipyPicker.module.mjs +1 -1
- package/dist/esm/common/loading/iconLoading/iconLoading.mjs.map +1 -1
- package/dist/esm/common/loading/iconLoading/iconLoading.module.mjs +1 -1
- package/dist/esm/common/loading/iconLoading/iconLoading.module.mjs.map +1 -1
- package/dist/esm/common/loading/spinnerLoading/spinnerLoading.mjs.map +1 -1
- package/dist/esm/common/loading/spinnerLoading/spinnerLoading.module.mjs +1 -1
- package/dist/esm/common/modal/modal.mjs.map +1 -1
- package/dist/esm/common/modal/modal.module.mjs +1 -1
- package/dist/esm/common/modal/modal.module.mjs.map +1 -1
- package/dist/esm/common/multiSelect/multiSelect.mjs +2 -0
- package/dist/esm/common/multiSelect/multiSelect.mjs.map +1 -0
- package/dist/esm/common/multiSelect/multiSelect.module.mjs +2 -0
- package/dist/esm/common/multiSelect/multiSelect.module.mjs.map +1 -0
- package/dist/esm/common/numberInput/numberInput.mjs +1 -1
- package/dist/esm/common/numberInput/numberInput.mjs.map +1 -1
- package/dist/esm/common/numberInput/numberInput.module.mjs +1 -1
- package/dist/esm/common/numberInput/numberInput.module.mjs.map +1 -1
- package/dist/esm/common/numberInput/useStepRepeat.mjs +1 -1
- package/dist/esm/common/numberInput/useStepRepeat.mjs.map +1 -1
- package/dist/esm/common/optionPicker/optionPicker.mjs +1 -1
- package/dist/esm/common/optionPicker/optionPicker.mjs.map +1 -1
- package/dist/esm/common/optionPicker/optionPicker.module.mjs +1 -1
- package/dist/esm/common/optionPicker/optionPicker.module.mjs.map +1 -1
- package/dist/esm/common/otpInput/otpInput.mjs.map +1 -1
- package/dist/esm/common/otpInput/otpInput.module.mjs +1 -1
- package/dist/esm/common/otpInput/otpInput.module.mjs.map +1 -1
- package/dist/esm/common/overscroll/overscroll.mjs +1 -1
- package/dist/esm/common/overscroll/overscroll.mjs.map +1 -1
- package/dist/esm/common/overscroll/overscroll.module.mjs +1 -1
- package/dist/esm/common/pagination/pagination.mjs +2 -0
- package/dist/esm/common/pagination/pagination.mjs.map +1 -0
- package/dist/esm/common/pagination/pagination.module.mjs +2 -0
- package/dist/esm/common/pagination/pagination.module.mjs.map +1 -0
- package/dist/esm/common/planCanvas/planCanvas.mjs +1 -1
- package/dist/esm/common/planCanvas/planCanvas.mjs.map +1 -1
- package/dist/esm/common/planCanvas/planCanvas.module.mjs +1 -1
- package/dist/esm/common/planCanvas/planCanvas.module.mjs.map +1 -1
- package/dist/esm/common/popover/popover.mjs +1 -1
- package/dist/esm/common/popover/popover.mjs.map +1 -1
- package/dist/esm/common/popover/popover.module.mjs +1 -1
- package/dist/esm/common/popover/popover.module.mjs.map +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs +1 -1
- package/dist/esm/common/popover/usePopoverPosition.mjs.map +1 -1
- package/dist/esm/common/progressBar/progressBar.mjs +1 -1
- package/dist/esm/common/progressBar/progressBar.mjs.map +1 -1
- package/dist/esm/common/progressBar/progressBar.module.mjs +1 -1
- package/dist/esm/common/progressBar/progressBar.module.mjs.map +1 -1
- package/dist/esm/common/radioGroup/radioGroup.mjs +1 -1
- package/dist/esm/common/radioGroup/radioGroup.mjs.map +1 -1
- package/dist/esm/common/radioGroup/radioGroup.module.mjs +1 -1
- package/dist/esm/common/radioGroup/radioGroup.module.mjs.map +1 -1
- package/dist/esm/common/rating/rating.mjs +1 -1
- package/dist/esm/common/rating/rating.mjs.map +1 -1
- package/dist/esm/common/rating/rating.module.mjs +1 -1
- package/dist/esm/common/revealLens/revealLens.mjs.map +1 -1
- package/dist/esm/common/revealLens/revealLens.module.mjs +1 -1
- package/dist/esm/common/revealLens/revealLens.module.mjs.map +1 -1
- package/dist/esm/common/ripple/ripple.mjs.map +1 -1
- package/dist/esm/common/ripple/ripple.module.mjs +1 -1
- package/dist/esm/common/ripple/ripple.module.mjs.map +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.mjs +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.mjs.map +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.module.mjs +1 -1
- package/dist/esm/common/roomDrawer/roomDrawer.module.mjs.map +1 -1
- package/dist/esm/common/roomDrawer/usePlanHistory.mjs.map +1 -1
- package/dist/esm/common/roomViewer/roomViewer.mjs +1 -1
- package/dist/esm/common/roomViewer/roomViewer.mjs.map +1 -1
- package/dist/esm/common/roomViewer/roomViewer.module.mjs +1 -1
- package/dist/esm/common/scrollbarProvider/scrollbarProvider.mjs.map +1 -1
- package/dist/esm/common/scrollbarProvider/scrollbarProvider.module.mjs +1 -1
- package/dist/esm/common/scrollbarProvider/scrollbarProvider.module.mjs.map +1 -1
- package/dist/esm/common/searchInput/searchInput.mjs.map +1 -1
- package/dist/esm/common/searchInput/searchInput.module.mjs +1 -1
- package/dist/esm/common/sectionHeader/sectionHeader.mjs.map +1 -1
- package/dist/esm/common/sectionHeader/sectionHeader.module.mjs +1 -1
- package/dist/esm/common/sectionHeader/sectionHeader.module.mjs.map +1 -1
- package/dist/esm/common/skeleton/skeleton.mjs.map +1 -1
- package/dist/esm/common/skeleton/skeleton.module.mjs +1 -1
- package/dist/esm/common/slider/slider.mjs +1 -1
- package/dist/esm/common/slider/slider.mjs.map +1 -1
- package/dist/esm/common/slider/slider.module.mjs +1 -1
- package/dist/esm/common/slider/slider.module.mjs.map +1 -1
- package/dist/esm/common/splitButton/splitButton.mjs.map +1 -1
- package/dist/esm/common/splitButton/splitButton.module.mjs +1 -1
- package/dist/esm/common/stepper/stepper.mjs.map +1 -1
- package/dist/esm/common/stepper/stepper.module.mjs +1 -1
- package/dist/esm/common/stepper/stepper.module.mjs.map +1 -1
- package/dist/esm/common/sticky/sticky.mjs.map +1 -1
- package/dist/esm/common/sticky/sticky.module.mjs +1 -1
- package/dist/esm/common/sticky/stickyStack.mjs.map +1 -1
- package/dist/esm/common/styles/sheet.module.mjs +1 -1
- package/dist/esm/common/styles/sheet.module.mjs.map +1 -1
- package/dist/esm/common/switch/switch.mjs.map +1 -1
- package/dist/esm/common/switch/switch.module.mjs +1 -1
- package/dist/esm/common/tabHeader/tabHeader.mjs.map +1 -1
- package/dist/esm/common/tabHeader/tabHeader.module.mjs +1 -1
- package/dist/esm/common/tabHeader/tabHeader.module.mjs.map +1 -1
- package/dist/esm/common/themeProvider/defaultTheme.mjs.map +1 -1
- package/dist/esm/common/themeProvider/theme.module.mjs +1 -1
- package/dist/esm/common/themeProvider/themeProvider.mjs +1 -1
- package/dist/esm/common/themeProvider/themeProvider.mjs.map +1 -1
- package/dist/esm/common/themeProvider/useTheme.mjs.map +1 -1
- package/dist/esm/common/tilt/tilt.mjs.map +1 -1
- package/dist/esm/common/tilt/tilt.module.mjs +1 -1
- package/dist/esm/common/tiltGroup/tiltGroup.mjs.map +1 -1
- package/dist/esm/common/timeInput/components/clockDial/clockDial.mjs +1 -1
- package/dist/esm/common/timeInput/components/clockDial/clockDial.mjs.map +1 -1
- package/dist/esm/common/timeInput/components/clockDial/clockDial.module.mjs +1 -1
- package/dist/esm/common/timeInput/components/clockDial/clockDial.module.mjs.map +1 -1
- package/dist/esm/common/timeInput/timeFunctions.mjs +1 -1
- package/dist/esm/common/timeInput/timeFunctions.mjs.map +1 -1
- package/dist/esm/common/timeInput/timeInput.mjs +1 -1
- package/dist/esm/common/timeInput/timeInput.mjs.map +1 -1
- package/dist/esm/common/timeInput/timeInput.module.mjs +1 -1
- package/dist/esm/common/timeInput/timeInput.module.mjs.map +1 -1
- package/dist/esm/common/timeline/timeline.mjs.map +1 -1
- package/dist/esm/common/timeline/timeline.module.mjs +1 -1
- package/dist/esm/common/timeline/timeline.module.mjs.map +1 -1
- package/dist/esm/common/timer/timer.chime.mjs.map +1 -1
- package/dist/esm/common/timer/timer.mjs +1 -1
- package/dist/esm/common/timer/timer.mjs.map +1 -1
- package/dist/esm/common/timer/timer.module.mjs +1 -1
- package/dist/esm/common/timer/timer.module.mjs.map +1 -1
- package/dist/esm/common/toast/toast.mjs.map +1 -1
- package/dist/esm/common/toast/toast.module.mjs +1 -1
- package/dist/esm/common/toast/toast.module.mjs.map +1 -1
- package/dist/esm/common/toast/toastProvider.mjs.map +1 -1
- package/dist/esm/common/tooltip/tooltip.mjs.map +1 -1
- package/dist/esm/common/tooltip/tooltip.module.mjs +1 -1
- package/dist/esm/common/tooltip/tooltip.module.mjs.map +1 -1
- package/dist/esm/common/tree/flattenTree.mjs +2 -0
- package/dist/esm/common/tree/flattenTree.mjs.map +1 -0
- package/dist/esm/common/tree/tree.mjs +2 -0
- package/dist/esm/common/tree/tree.mjs.map +1 -0
- package/dist/esm/common/tree/tree.module.mjs +2 -0
- package/dist/esm/common/tree/tree.module.mjs.map +1 -0
- package/dist/esm/common/utils/renderIcon.mjs +1 -1
- package/dist/esm/common/utils/renderIcon.mjs.map +1 -1
- package/dist/esm/common/virtualList/useColumns.mjs +1 -1
- package/dist/esm/common/virtualList/useColumns.mjs.map +1 -1
- package/dist/esm/common/virtualList/useRowDrag.mjs +1 -1
- package/dist/esm/common/virtualList/useRowDrag.mjs.map +1 -1
- package/dist/esm/common/virtualList/useSelection.mjs.map +1 -1
- package/dist/esm/common/virtualList/useVirtualWindow.mjs +1 -1
- package/dist/esm/common/virtualList/useVirtualWindow.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualList.mjs +1 -1
- package/dist/esm/common/virtualList/virtualList.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualList.module.mjs +1 -1
- package/dist/esm/common/virtualList/virtualList.module.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualList.utils.mjs.map +1 -1
- package/dist/esm/common/virtualList/virtualRow.mjs +1 -1
- package/dist/esm/common/virtualList/virtualRow.mjs.map +1 -1
- package/dist/esm/common/wizard/wizard.mjs +1 -1
- package/dist/esm/common/wizard/wizard.mjs.map +1 -1
- package/dist/esm/common/wizard/wizard.module.mjs +1 -1
- package/dist/esm/index.mjs +1 -1
- package/dist/esm/services/formValidation/FormValidatorGroupWithForm.mjs.map +1 -1
- package/dist/esm/services/formValidation/index.mjs +1 -1
- package/dist/esm/services/formValidation/useFormValidatorGroup.mjs.map +1 -1
- package/dist/esm/services/formValidation/validatableComponent.mjs.map +1 -1
- package/dist/esm/services/klipy.mjs.map +1 -1
- package/dist/esm/services/localization.mjs.map +1 -1
- package/dist/index.cjs +3 -3
- package/dist/index.cjs.map +1 -1
- package/dist/style.css +1 -1
- package/dist/types/{package/common → common}/actionButtons/actionButtons.d.ts +1 -1
- package/dist/types/{package/common → common}/animatedIcon/animatedIcon.d.ts +1 -1
- package/dist/types/common/card/Card.d.ts +4 -0
- package/dist/types/{package/common → common}/card/CardActions.d.ts +2 -1
- package/dist/types/common/card/CardContent.d.ts +4 -0
- package/dist/types/{package/common → common}/card/CardHeader.d.ts +2 -1
- package/dist/types/common/card/CardMedia.d.ts +4 -0
- package/dist/types/common/carousel/carousel.d.ts +4 -0
- package/dist/types/common/chip/chip.d.ts +4 -0
- package/dist/types/{package/common → common}/configProvider/configProvider.d.ts +1 -1
- package/dist/types/{package/common → common}/configProvider/configProvider.types.d.ts +6 -0
- package/dist/types/{package/common → common}/datePicker/components/datePickerMonth/datePickerMonth.d.ts +1 -1
- package/dist/types/{package/common → common}/datePicker/datePicker.d.ts +1 -1
- package/dist/types/{package/common → common}/drawer/drawer.d.ts +1 -1
- package/dist/types/{package/common → common}/dropZone/dropZone.d.ts +2 -1
- package/dist/types/common/dropdown/dropdown.d.ts +4 -0
- package/dist/types/{package/common → common}/fab/components/fabBase/fabBase.d.ts +1 -1
- package/dist/types/{package/common → common}/fab/fab.d.ts +1 -1
- package/dist/types/{package/common → common}/floatingMenu/floatingMenu.d.ts +1 -1
- package/dist/types/{package/common → common}/floatingMenu/useFloatingPosition.d.ts +5 -7
- package/dist/types/common/hooks/useAnchorTracking.d.ts +40 -0
- package/dist/types/{package/common → common}/hooks/useOverlay.d.ts +1 -1
- package/dist/types/{package/common → common}/iconPicker/iconPicker.d.ts +2 -1
- package/dist/types/{package/common → common}/idleManager/idleManager.d.ts +1 -1
- package/dist/types/{package/common → common}/infiniteBlock/infiniteBlock.d.ts +2 -1
- package/dist/types/common/input/input.d.ts +4 -0
- package/dist/types/{package/common → common}/inputDropdown/inputDropdown.d.ts +2 -1
- package/dist/types/{package/common → common}/inputDropdown/inputDropdown.types.d.ts +6 -0
- package/dist/types/{package/common → common}/kanbanBoard/KanbanBoard.d.ts +1 -1
- package/dist/types/{package/common → common}/kanbanBoard/KanbanColumn.d.ts +1 -1
- package/dist/types/{package/common → common}/kanbanBoard/KanbanItem.d.ts +1 -1
- package/dist/types/{package/common → common}/loading/iconLoading/iconLoading.d.ts +2 -1
- package/dist/types/common/modal/modal.d.ts +3 -0
- package/dist/types/common/multiSelect/index.d.ts +2 -0
- package/dist/types/common/multiSelect/multiSelect.d.ts +4 -0
- package/dist/types/common/multiSelect/multiSelect.types.d.ts +62 -0
- package/dist/types/{package/common → common}/overscroll/overscroll.d.ts +1 -1
- package/dist/types/common/pagination/index.d.ts +2 -0
- package/dist/types/common/pagination/pagination.d.ts +4 -0
- package/dist/types/common/pagination/pagination.types.d.ts +30 -0
- package/dist/types/common/popover/popover.d.ts +3 -0
- package/dist/types/{package/common → common}/popover/usePopoverPosition.d.ts +4 -2
- package/dist/types/{package/common → common}/scrollbarProvider/scrollbarProvider.d.ts +1 -1
- package/dist/types/{package/common → common}/sheetBackdrop/sheetBackdrop.d.ts +1 -1
- package/dist/types/common/slider/slider.d.ts +3 -0
- package/dist/types/{package/common → common}/slider/slider.types.d.ts +35 -10
- package/dist/types/{package/common → common}/splitButton/splitButton.d.ts +1 -1
- package/dist/types/{package/common → common}/stepper/stepper.d.ts +2 -1
- package/dist/types/{package/common → common}/sticky/sticky.d.ts +2 -1
- package/dist/types/{package/common → common}/tabHeader/tabHeader.d.ts +2 -1
- package/dist/types/common/textarea/textarea.d.ts +3 -0
- package/dist/types/{package/common → common}/themeProvider/themeProvider.d.ts +1 -1
- package/dist/types/{package/common → common}/timeInput/components/clockDial/clockDial.d.ts +2 -1
- package/dist/types/{package/common → common}/timeInput/timeInput.d.ts +2 -1
- package/dist/types/{package/common → common}/timeline/timeline.d.ts +2 -1
- package/dist/types/{package/common → common}/toast/index.d.ts +1 -1
- package/dist/types/{package/common → common}/toast/toast.d.ts +2 -1
- package/dist/types/{package/common → common}/toast/toastProvider.d.ts +2 -2
- package/dist/types/{package/common → common}/tooltip/tooltip.d.ts +2 -1
- package/dist/types/common/tree/flattenTree.d.ts +3 -0
- package/dist/types/common/tree/index.d.ts +3 -0
- package/dist/types/common/tree/tree.d.ts +4 -0
- package/dist/types/common/tree/tree.types.d.ts +62 -0
- package/dist/types/{package/common → common}/virtualList/virtualList.types.d.ts +8 -0
- package/dist/types/{package/common → common}/virtualList/virtualRow.d.ts +2 -2
- package/dist/types/{package/common → common}/wizard/wizard.d.ts +2 -1
- package/dist/types/{package/index.d.ts → index.d.ts} +6 -0
- package/dist/types/{package/services → services}/formValidation/index.d.ts +1 -1
- package/docs/CLAUDE.md +3 -0
- package/docs/InputDropdown.md +3 -0
- package/docs/MultiSelect.md +100 -0
- package/docs/Pagination.md +72 -0
- package/docs/Slider.md +34 -4
- package/docs/Tree.md +95 -0
- package/docs/VirtualList.md +1 -0
- package/package.json +74 -70
- package/dist/types/package/common/card/Card.d.ts +0 -3
- package/dist/types/package/common/card/CardContent.d.ts +0 -3
- package/dist/types/package/common/card/CardMedia.d.ts +0 -3
- package/dist/types/package/common/carousel/carousel.d.ts +0 -3
- package/dist/types/package/common/chip/chip.d.ts +0 -3
- package/dist/types/package/common/dropdown/dropdown.d.ts +0 -3
- package/dist/types/package/common/input/input.d.ts +0 -3
- package/dist/types/package/common/modal/modal.d.ts +0 -3
- package/dist/types/package/common/popover/popover.d.ts +0 -3
- package/dist/types/package/common/slider/slider.d.ts +0 -3
- package/dist/types/package/common/textarea/textarea.d.ts +0 -2
- /package/dist/types/{package/common → common}/accordion/accordion.d.ts +0 -0
- /package/dist/types/{package/common → common}/accordion/accordion.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/accordion/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/actionButtons/actionButtons.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/actionButtons/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/actionIcon/actionIcon.d.ts +0 -0
- /package/dist/types/{package/common → common}/actionIcon/actionIcon.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/actionIcon/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/alert/alert.d.ts +0 -0
- /package/dist/types/{package/common → common}/alert/alert.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/alert/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/animatedIcon/animatedIcon.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/animatedIcon/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/animatedLogo/animatedLogo.d.ts +0 -0
- /package/dist/types/{package/common → common}/animatedLogo/animatedLogo.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/animatedLogo/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/animatedText/animatedText.d.ts +0 -0
- /package/dist/types/{package/common → common}/animatedText/animatedText.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/animatedText/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/avatar/avatar.d.ts +0 -0
- /package/dist/types/{package/common → common}/avatar/avatar.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/avatar/avatarGroup.d.ts +0 -0
- /package/dist/types/{package/common → common}/avatar/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/badge/badge.d.ts +0 -0
- /package/dist/types/{package/common → common}/badge/badge.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/badge/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/bodyEnd/bodyEnd.d.ts +0 -0
- /package/dist/types/{package/common → common}/bodyEnd/bodyEnd.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/bodyEnd/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/breadcrumb/breadcrumb.d.ts +0 -0
- /package/dist/types/{package/common → common}/breadcrumb/breadcrumb.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/breadcrumb/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/button/button.d.ts +0 -0
- /package/dist/types/{package/common → common}/button/button.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/button/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/buttonGroup/buttonGroup.d.ts +0 -0
- /package/dist/types/{package/common → common}/buttonGroup/buttonGroup.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/buttonGroup/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/card/card.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/card/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/carousel/carousel.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/carousel/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/checkbox/checkbox.d.ts +0 -0
- /package/dist/types/{package/common → common}/checkbox/checkbox.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/checkbox/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/chip/chip.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/chip/chipContainer.d.ts +0 -0
- /package/dist/types/{package/common → common}/chip/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/colorPicker/colorPicker.d.ts +0 -0
- /package/dist/types/{package/common → common}/colorPicker/colorPicker.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/colorPicker/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/configProvider/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/configProvider/useComponentDefaults.d.ts +0 -0
- /package/dist/types/{package/common → common}/configProvider/usePresentationDefault.d.ts +0 -0
- /package/dist/types/{package/common → common}/confirmModal/confirmModal.d.ts +0 -0
- /package/dist/types/{package/common → common}/confirmModal/confirmModal.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/confirmModal/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/datePicker/components/datePickerMonth/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/datePicker/dateFunctions.d.ts +0 -0
- /package/dist/types/{package/common → common}/datePicker/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/divider/divider.d.ts +0 -0
- /package/dist/types/{package/common → common}/divider/divider.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/divider/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/drawer/drawer.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/drawer/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/dropZone/dropZone.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/dropZone/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/dropdown/dropdown.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/dropdown/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/emptyState/emptyState.d.ts +0 -0
- /package/dist/types/{package/common → common}/emptyState/emptyState.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/emptyState/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/errorBoundary/errorBoundary.d.ts +0 -0
- /package/dist/types/{package/common → common}/errorBoundary/errorBoundary.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/errorBoundary/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/fab/components/fabBase/fabBase.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/fab/components/fabBase/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/fab/fab.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/fab/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/flip/flip.d.ts +0 -0
- /package/dist/types/{package/common → common}/flip/flip.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/flip/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/floatingMenu/floatingMenu.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/floatingMenu/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.faces.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.fixtures.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.geometry.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.graph.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.hit.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.json.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.mesh.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.path.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.plan.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.rooms.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.snap.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.triangulate.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/floorPlan.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/floorPlan/legacySvg.d.ts +0 -0
- /package/dist/types/{package/common → common}/hooks/useCoarsePointer.d.ts +0 -0
- /package/dist/types/{package/common → common}/hooks/useFocusBoundary.d.ts +0 -0
- /package/dist/types/{package/common → common}/hooks/useMediaQuery.d.ts +0 -0
- /package/dist/types/{package/common → common}/hooks/useSheet.d.ts +0 -0
- /package/dist/types/{package/common → common}/hooks/useSwipeDismiss.d.ts +0 -0
- /package/dist/types/{package/common → common}/hooks/useVisualViewportSize.d.ts +0 -0
- /package/dist/types/{package/common → common}/iconPicker/iconPicker.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/iconPicker/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/idleManager/idleManager.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/idleManager/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/infiniteBlock/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/infiniteBlock/infiniteBlock.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/input/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/input/input.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/inputDropdown/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/interactableDiv/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/interactableDiv/interactableDiv.d.ts +0 -0
- /package/dist/types/{package/common → common}/kanbanBoard/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/kanbanBoard/kanbanBoard.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/kanbanBoard/kanbanBoard.utils.d.ts +0 -0
- /package/dist/types/{package/common → common}/klipyPicker/components/gifPreview/gifPreview.d.ts +0 -0
- /package/dist/types/{package/common → common}/klipyPicker/components/gifPreview/gifPreview.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/klipyPicker/components/gifPreview/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/klipyPicker/components/gifView/gifView.d.ts +0 -0
- /package/dist/types/{package/common → common}/klipyPicker/components/gifView/gifView.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/klipyPicker/components/gifView/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/klipyPicker/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/klipyPicker/klipyPicker.d.ts +0 -0
- /package/dist/types/{package/common → common}/klipyPicker/klipyPicker.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/loading/iconLoading/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/loading/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/loading/spinnerLoading/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/loading/spinnerLoading/spinnerLoading.d.ts +0 -0
- /package/dist/types/{package/common → common}/modal/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/modal/modal.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/numberInput/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/numberInput/numberInput.d.ts +0 -0
- /package/dist/types/{package/common → common}/numberInput/numberInput.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/numberInput/useStepRepeat.d.ts +0 -0
- /package/dist/types/{package/common → common}/optionPicker/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/optionPicker/optionPicker.d.ts +0 -0
- /package/dist/types/{package/common → common}/optionPicker/optionPicker.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/otpInput/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/otpInput/otpInput.d.ts +0 -0
- /package/dist/types/{package/common → common}/otpInput/otpInput.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/overscroll/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/overscroll/overscroll.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/planCanvas/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/planCanvas/planCanvas.d.ts +0 -0
- /package/dist/types/{package/common → common}/planCanvas/planCanvas.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/popover/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/popover/popover.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/progressBar/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/progressBar/progressBar.d.ts +0 -0
- /package/dist/types/{package/common → common}/progressBar/progressBar.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/radioGroup/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/radioGroup/radioGroup.d.ts +0 -0
- /package/dist/types/{package/common → common}/radioGroup/radioGroup.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/rating/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/rating/rating.d.ts +0 -0
- /package/dist/types/{package/common → common}/rating/rating.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/revealLens/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/revealLens/revealLens.d.ts +0 -0
- /package/dist/types/{package/common → common}/revealLens/revealLens.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/ripple/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/ripple/ripple.d.ts +0 -0
- /package/dist/types/{package/common → common}/ripple/ripple.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/roomDrawer/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/roomDrawer/roomDrawer.d.ts +0 -0
- /package/dist/types/{package/common → common}/roomDrawer/roomDrawer.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/roomDrawer/usePlanHistory.d.ts +0 -0
- /package/dist/types/{package/common → common}/roomViewer/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/roomViewer/roomViewer.d.ts +0 -0
- /package/dist/types/{package/common → common}/roomViewer/roomViewer.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/scrollbarProvider/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/searchInput/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/searchInput/searchInput.d.ts +0 -0
- /package/dist/types/{package/common → common}/searchInput/searchInput.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/sectionHeader/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/sectionHeader/sectionHeader.d.ts +0 -0
- /package/dist/types/{package/common → common}/sectionHeader/sectionHeader.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/sheetBackdrop/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/sheetBackdrop/sheetBackdrop.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/skeleton/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/skeleton/skeleton.d.ts +0 -0
- /package/dist/types/{package/common → common}/skeleton/skeleton.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/slider/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/splitButton/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/splitButton/splitButton.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/stepper/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/stepper/stepper.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/sticky/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/sticky/sticky.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/sticky/stickyStack.d.ts +0 -0
- /package/dist/types/{package/common → common}/switch/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/switch/switch.d.ts +0 -0
- /package/dist/types/{package/common → common}/switch/switch.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/tabHeader/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/tabHeader/tabHeader.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/textarea/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/textarea/textarea.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/themeProvider/defaultTheme.d.ts +0 -0
- /package/dist/types/{package/common → common}/themeProvider/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/themeProvider/theme.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/themeProvider/useTheme.d.ts +0 -0
- /package/dist/types/{package/common → common}/tilt/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/tilt/tilt.d.ts +0 -0
- /package/dist/types/{package/common → common}/tilt/tilt.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/tilt/tiltMath.d.ts +0 -0
- /package/dist/types/{package/common → common}/tiltGroup/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/tiltGroup/tiltGroup.d.ts +0 -0
- /package/dist/types/{package/common → common}/tiltGroup/tiltGroup.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/timeInput/components/clockDial/clockDial.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/timeInput/components/clockDial/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/timeInput/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/timeInput/timeFunctions.d.ts +0 -0
- /package/dist/types/{package/common → common}/timeInput/timeInput.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/timeline/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/timeline/timeline.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/timer/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/timer/timer.chime.d.ts +0 -0
- /package/dist/types/{package/common → common}/timer/timer.d.ts +0 -0
- /package/dist/types/{package/common → common}/timer/timer.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/toast/toast.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/toast/toastService.d.ts +0 -0
- /package/dist/types/{package/common → common}/tooltip/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/tooltip/tooltip.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/types/actions.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/types/baseProps.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/types/icon.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/types/slots.types.d.ts +0 -0
- /package/dist/types/{package/common → common}/utils/renderIcon.d.ts +0 -0
- /package/dist/types/{package/common → common}/utils/scrollAncestors.d.ts +0 -0
- /package/dist/types/{package/common → common}/utils/visualViewport.d.ts +0 -0
- /package/dist/types/{package/common → common}/virtualList/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/virtualList/useColumns.d.ts +0 -0
- /package/dist/types/{package/common → common}/virtualList/useRowDrag.d.ts +0 -0
- /package/dist/types/{package/common → common}/virtualList/useSelection.d.ts +0 -0
- /package/dist/types/{package/common → common}/virtualList/useVirtualWindow.d.ts +0 -0
- /package/dist/types/{package/common → common}/virtualList/virtualList.d.ts +0 -0
- /package/dist/types/{package/common → common}/virtualList/virtualList.utils.d.ts +0 -0
- /package/dist/types/{package/common → common}/wizard/index.d.ts +0 -0
- /package/dist/types/{package/common → common}/wizard/wizard.types.d.ts +0 -0
- /package/dist/types/{package/hooks → hooks}/useWindowSize.d.ts +0 -0
- /package/dist/types/{package/services → services}/formValidation/FormValidatorGroupWithForm.d.ts +0 -0
- /package/dist/types/{package/services → services}/formValidation/useFormValidatorGroup.d.ts +0 -0
- /package/dist/types/{package/services → services}/formValidation/validatableComponent.d.ts +0 -0
- /package/dist/types/{package/services → services}/klipy.d.ts +0 -0
- /package/dist/types/{package/services → services}/localization.d.ts +0 -0
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floorPlan.path.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.path.ts"],"sourcesContent":["/**\n * Turns walls into SVG path data for the two-pass thick-wall rendering.\n *\n * The trick: both passes stroke the SAME path data, the outline pass at `thickness` and\n * the fill pass at `thickness - 2 * outlineWidth`. The visible outline is not drawn at\n * all, it is what is left of the outline pass after the fill pass covers its middle.\n * That is why the two must never diverge, and why every outline path has to be drawn\n * before every fill path (one `<g>` each, never interleaved pairs).\n *\n * Three things do not fall out of that trick and are handled explicitly:\n *\n * 1. Nothing is ever drawn across a butt end, because there both passes terminate at\n * the same coordinate. Openings and free ends therefore need explicit cap lines.\n * 2. Two butt-capped walls meeting at an angle leave an uncovered notch at the outer\n * corner. Walls are emitted as CHAINS through degree-2 nodes so `stroke-linejoin`\n * miters the corner, and where a chain has to stop, `reflexCornerStubs` mitres the\n * one corner a junction cannot close by itself.\n * 3. Where two walls of different thickness meet end to end, the drawing steps from\n * thick fill straight to background with no edge, so that joint needs a cap too.\n */\n\nimport { nodeDegrees } from './floorPlan.faces';\nimport { dist } from './floorPlan.geometry';\nimport type { PlanFloor, PlanOpening, PlanPoint, PlanWall } from './floorPlan.types';\n\nexport interface WallPathGroup {\n /** Millimetres. The stroke width for the outline pass. */\n thickness: number;\n /**\n * Path data, to be handed VERBATIM to both passes. The outline pass strokes it at\n * `thickness`, the fill pass at `thickness - 2 * outlineWidth`.\n */\n d: string;\n}\n\nexport type WallCapReason = 'jamb' | 'freeEnd' | 'thicknessStep';\n\nexport interface WallCapLine {\n a: PlanPoint;\n b: PlanPoint;\n reason: WallCapReason;\n wallId: string;\n}\n\nexport interface WallPathResult {\n /** Render every group's outline first, then every group's fill. */\n groups: WallPathGroup[];\n /** Drawn last, in the outline colour, at the outline width. */\n caps: WallCapLine[];\n}\n\n\nexport interface BuildWallPathsOptions {\n /**\n * Build paths for only these walls, as if the rest were not drawn.\n *\n * Node degrees still come from the WHOLE floor, so chains break and corners are filled\n * exactly where they are for the real walls. That is what lets a subset be painted over\n * the top of the full drawing and land on it precisely: a highlight of a room's\n * boundary is the same geometry as the walls themselves, openings and all.\n */\n wallIds?: ReadonlySet<string>;\n /** Cap the free end of a wall that stops in mid-air. Default true. */\n capFreeEnds?: boolean;\n /** Cap the joint where two walls of different thickness meet. Default true. */\n capThicknessSteps?: boolean;\n}\n\nconst EPS = 1e-6;\n\nfunction lerpPoint(a: PlanPoint, b: PlanPoint, t: number): PlanPoint {\n return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };\n}\n\nfunction fmt(n: number): string {\n return Number.isInteger(n) ? String(n) : n.toFixed(3).replace(/\\.?0+$/, '');\n}\n\nfunction samePoint(a: PlanPoint, b: PlanPoint): boolean {\n return Math.abs(a.x - b.x) < 0.001 && Math.abs(a.y - b.y) < 0.001;\n}\n\n/**\n * The parts of a wall's centreline that are actually wall, as `[start, end]` offsets in\n * millimetres from node `a`.\n *\n * Overlapping openings merge into one gap; an opening reaching an end removes that span\n * entirely. An empty result means the opening spans the whole wall.\n */\nexport function wallCentrelineSpans(floor: PlanFloor, wall: PlanWall): Array<[number, number]> {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) return [];\n const len = dist(a, b);\n if (len < EPS) return [];\n\n const gaps: Array<[number, number]> = [];\n for (const opening of Object.values(floor.openings)) {\n if (opening.wallId !== wall.id) continue;\n const half = opening.width / 2;\n const start = Math.max(0, opening.offset - half);\n const end = Math.min(len, opening.offset + half);\n if (end > start) gaps.push([start, end]);\n }\n if (gaps.length === 0) return [[0, len]];\n\n gaps.sort((x, y) => x[0] - y[0]);\n const merged: Array<[number, number]> = [gaps[0]];\n for (let i = 1; i < gaps.length; i++) {\n const last = merged[merged.length - 1];\n if (gaps[i][0] <= last[1]) last[1] = Math.max(last[1], gaps[i][1]);\n else merged.push(gaps[i]);\n }\n\n const spans: Array<[number, number]> = [];\n let cursor = 0;\n for (const [start, end] of merged) {\n if (start - cursor > EPS) spans.push([cursor, start]);\n cursor = end;\n }\n if (len - cursor > EPS) spans.push([cursor, len]);\n return spans;\n}\n\n/** Everything a renderer needs to draw a door leaf, swing arc or window symbol. */\nexport function openingGeometry(\n floor: PlanFloor,\n opening: PlanOpening,\n): { centre: PlanPoint; dir: PlanPoint; normal: PlanPoint; p1: PlanPoint; p2: PlanPoint } | null {\n const wall = floor.walls[opening.wallId];\n if (!wall) return null;\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) return null;\n const len = dist(a, b);\n if (len < EPS) return null;\n\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const normal = { x: -dir.y, y: dir.x };\n const half = Math.min(opening.width / 2, len / 2);\n const offset = Math.min(Math.max(opening.offset, half), len - half);\n const centre = { x: a.x + dir.x * offset, y: a.y + dir.y * offset };\n\n return {\n centre,\n dir,\n normal,\n p1: { x: centre.x - dir.x * half, y: centre.y - dir.y * half },\n p2: { x: centre.x + dir.x * half, y: centre.y + dir.y * half },\n };\n}\n\n/** One directed step along a chain. */\ninterface Step {\n wall: PlanWall;\n from: string;\n to: string;\n}\n\n/**\n * Maximal runs of same-thickness walls joined through degree-2 nodes.\n *\n * A chain may only continue through a node whose TOTAL degree is 2 and whose two walls\n * share a thickness: a different stroke width means a different path, and a degree-3+\n * node is a junction rather than a corner.\n */\nfunction buildChains(floor: PlanFloor, walls: PlanWall[], degrees: Map<string, number>): Step[][] {\n const byId = new Map(walls.map((w) => [w.id, w]));\n const atNode = new Map<string, PlanWall[]>();\n for (const wall of walls) {\n for (const nodeId of [wall.a, wall.b]) {\n const list = atNode.get(nodeId);\n if (list) list.push(wall);\n else atNode.set(nodeId, [wall]);\n }\n }\n\n const continuesThrough = (nodeId: string): PlanWall[] | null => {\n if ((degrees.get(nodeId) ?? 0) !== 2) return null;\n const here = atNode.get(nodeId);\n // Both walls must be in this thickness group, or the chain has to break here.\n if (!here || here.length !== 2) return null;\n return here;\n };\n\n const used = new Set<string>();\n const chains: Step[][] = [];\n\n const other = (wall: PlanWall, nodeId: string): string => (wall.a === nodeId ? wall.b : wall.a);\n\n for (const wall of walls) {\n if (used.has(wall.id)) continue;\n\n // Walk backwards to the start of the run, unless it closes on itself.\n let startWall = wall;\n let startNode = wall.a;\n const seenBack = new Set<string>([wall.id]);\n for (;;) {\n const pair = continuesThrough(startNode);\n if (!pair) break;\n const prev = pair[0].id === startWall.id ? pair[1] : pair[0];\n if (seenBack.has(prev.id)) break; // closed loop\n seenBack.add(prev.id);\n startWall = prev;\n startNode = other(prev, startNode);\n }\n\n // Walk forwards, collecting directed steps.\n const steps: Step[] = [];\n let current = startWall;\n let from = startNode;\n for (;;) {\n if (used.has(current.id)) break;\n used.add(current.id);\n const to = other(current, from);\n steps.push({ wall: current, from, to });\n const pair = continuesThrough(to);\n if (!pair) break;\n const next = pair[0].id === current.id ? pair[1] : pair[0];\n if (used.has(next.id)) break;\n current = next;\n from = to;\n if (!byId.has(current.id)) break;\n }\n\n if (steps.length > 0) chains.push(steps);\n }\n\n return chains;\n}\n\n/**\n * The outer corner of the one REFLEX gap at a node, as a two-legged stub.\n *\n * A chain's `stroke-linejoin` mitres the corner wherever a wall bends, but a chain has\n * to stop at a junction, and at a thickness change, so something else has to close the\n * corner there. Most junction corners need nothing: where the angular gap between two\n * neighbouring walls is at most 180 degrees, the two bands reach across it and already\n * meet at the mitre point, because both start AT the node rather than short of it.\n *\n * A gap WIDER than 180 degrees is the exception, and it is the only one. There the two\n * faces bounding the gap diverge instead of crossing, so nothing spans the gap and what\n * is left between the two butt ends is an open wedge biting into the node. It shows up\n * whenever every wall at a node points into the same half-plane: a wall running east, a\n * wall running north, and a third splitting the two leaves a quarter-circle bitten out\n * of the south-west side. Because the gaps around a node sum to 360 degrees, at most\n * one can be reflex, so a node never needs more than one of these.\n *\n * The stub is two legs laid back along the two walls bounding that gap. Stroked at the\n * wall thickness it stays within their own bands, adding nothing anywhere else, and its\n * mitre join fills the wedge and bevels under the same `stroke-miterlimit` as every\n * other corner. Filling every gap this way instead of only the reflex one is what used\n * to fire spikes out of narrow junctions: those gaps had nothing to fill, so the join\n * ran outside the walls, the further the tighter the angle.\n */\nfunction reflexCornerStubs(\n floor: PlanFloor,\n walls: PlanWall[],\n degrees: Map<string, number>,\n): Array<{ thickness: number; d: string }> {\n interface Arm {\n wall: PlanWall;\n dir: PlanPoint;\n angle: number;\n /** Millimetres of wall between the node and the first opening, if any. */\n reach: number;\n }\n\n const armsAt = new Map<string, Arm[]>();\n const addArm = (nodeId: string, arm: Arm) => {\n const list = armsAt.get(nodeId);\n if (list) list.push(arm);\n else armsAt.set(nodeId, [arm]);\n };\n\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n const len = dist(a, b);\n if (len < EPS) continue;\n const spans = wallCentrelineSpans(floor, wall);\n if (spans.length === 0) continue;\n\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const first = spans[0];\n const last = spans[spans.length - 1];\n addArm(wall.a, {\n wall,\n dir,\n angle: Math.atan2(dir.y, dir.x),\n reach: first[0] < EPS ? first[1] : 0,\n });\n addArm(wall.b, {\n wall,\n dir: { x: -dir.x, y: -dir.y },\n angle: Math.atan2(-dir.y, -dir.x),\n reach: last[1] > len - EPS ? len - last[0] : 0,\n });\n }\n\n const stubs: Array<{ thickness: number; d: string }> = [];\n const TAU = Math.PI * 2;\n\n for (const [nodeId, arms] of armsAt) {\n if (arms.length < 2) continue;\n /*\n * A chain runs straight through a plain corner, and its own join closes it. The test\n * has to be the one `buildChains` uses, which reads the degree of the node in the\n * WHOLE floor: drawing a subset of the walls, two arms at a T junction look like a\n * corner but the chain still breaks there, and skipping the fill left a notch bitten\n * out of the join.\n */\n const chainRunsThrough =\n (degrees.get(nodeId) ?? 0) === 2 &&\n arms.length === 2 &&\n arms[0].wall.thickness === arms[1].wall.thickness;\n if (chainRunsThrough) continue;\n\n arms.sort((p, q) => p.angle - q.angle);\n\n let widest = 0;\n let widestGap = -1;\n for (let i = 0; i < arms.length; i++) {\n const next = arms[(i + 1) % arms.length];\n const gap = i === arms.length - 1 ? next.angle + TAU - arms[i].angle : next.angle - arms[i].angle;\n if (gap > widestGap) {\n widestGap = gap;\n widest = i;\n }\n }\n if (widestGap <= Math.PI + EPS) continue;\n\n const from = arms[widest];\n const to = arms[(widest + 1) % arms.length];\n // The thinner of the two, so the stub can never bulge out past the thinner wall.\n const thickness = Math.min(from.wall.thickness, to.wall.thickness);\n // Half a thickness of leg is enough to carry the join, and an opening close to the\n // node shortens it so the stub cannot paint across the jamb.\n const legFrom = Math.min(thickness / 2, from.reach);\n const legTo = Math.min(thickness / 2, to.reach);\n if (legFrom < EPS || legTo < EPS) continue;\n\n const node = floor.nodes[nodeId];\n stubs.push({\n thickness,\n d:\n `M ${fmt(node.x + from.dir.x * legFrom)},${fmt(node.y + from.dir.y * legFrom)}` +\n ` L ${fmt(node.x)},${fmt(node.y)}` +\n ` L ${fmt(node.x + to.dir.x * legTo)},${fmt(node.y + to.dir.y * legTo)}`,\n });\n }\n\n return stubs;\n}\n\n/**\n * Builds the stroke path data for every wall, grouped by thickness, plus the cap lines\n * the two-pass trick cannot produce on its own.\n */\nexport function buildWallPaths(\n floor: PlanFloor,\n options: BuildWallPathsOptions = {},\n): WallPathResult {\n const { wallIds, capFreeEnds = true, capThicknessSteps = true } = options;\n\n const walls = Object.values(floor.walls).filter((w) => {\n if (wallIds && !wallIds.has(w.id)) return false;\n const a = floor.nodes[w.a];\n const b = floor.nodes[w.b];\n return a && b && dist(a, b) > EPS && w.a !== w.b;\n });\n if (walls.length === 0) return { groups: [], caps: [] };\n\n const degrees = nodeDegrees(floor);\n const caps: WallCapLine[] = [];\n\n // ── Group by thickness: one stroke width per path. ────────────────────────────\n const byThickness = new Map<number, PlanWall[]>();\n for (const wall of walls) {\n const list = byThickness.get(wall.thickness);\n if (list) list.push(wall);\n else byThickness.set(wall.thickness, [wall]);\n }\n\n const subpathsByThickness = new Map<number, string[]>();\n const pushSubpath = (thickness: number, d: string) => {\n const list = subpathsByThickness.get(thickness);\n if (list) list.push(d);\n else subpathsByThickness.set(thickness, [d]);\n };\n\n for (const [thickness, groupWalls] of byThickness) {\n for (const chain of buildChains(floor, groupWalls, degrees)) {\n let current: PlanPoint[] = [];\n const flush = () => {\n if (current.length >= 2) {\n pushSubpath(\n thickness,\n current.map((p, i) => `${i === 0 ? 'M' : 'L'} ${fmt(p.x)},${fmt(p.y)}`).join(' '),\n );\n }\n current = [];\n };\n\n for (const step of chain) {\n const a = floor.nodes[step.from];\n const b = floor.nodes[step.to];\n const len = dist(a, b);\n if (len < EPS) continue;\n\n // Spans come back in the wall's own a->b direction; flip them when the chain\n // traverses the wall the other way.\n let spans = wallCentrelineSpans(floor, step.wall);\n if (step.from !== step.wall.a) {\n spans = spans.map(([s, e]) => [len - e, len - s] as [number, number]).reverse();\n }\n\n for (const [s, e] of spans) {\n const p1 = lerpPoint(a, b, s / len);\n const p2 = lerpPoint(a, b, e / len);\n if (current.length > 0 && samePoint(current[current.length - 1], p1)) {\n current.push(p2);\n } else {\n flush();\n current = [p1, p2];\n }\n }\n // An opening reaching the far end leaves nothing to continue from.\n if (spans.length === 0 || spans[spans.length - 1][1] < len - EPS) flush();\n }\n\n // A chain that returns to where it started closes properly, so the final corner\n // gets a miter join like every other corner rather than two butt caps.\n const first = chain[0];\n const last = chain[chain.length - 1];\n const closed = first.from === last.to;\n if (closed && current.length >= 3 && samePoint(current[0], current[current.length - 1])) {\n const ring = current.slice(0, -1);\n pushSubpath(\n thickness,\n `${ring.map((p, i) => `${i === 0 ? 'M' : 'L'} ${fmt(p.x)},${fmt(p.y)}`).join(' ')} Z`,\n );\n current = [];\n } else {\n flush();\n }\n }\n }\n\n for (const stub of reflexCornerStubs(floor, walls, degrees)) pushSubpath(stub.thickness, stub.d);\n\n // ── Caps. ─────────────────────────────────────────────────────────────────────\n const capAt = (wall: PlanWall, offset: number, reason: WallCapReason) => {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n const len = dist(a, b);\n if (len < EPS) return;\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const normal = { x: -dir.y, y: dir.x };\n const centre = { x: a.x + dir.x * offset, y: a.y + dir.y * offset };\n const half = wall.thickness / 2;\n caps.push({\n a: { x: centre.x - normal.x * half, y: centre.y - normal.y * half },\n b: { x: centre.x + normal.x * half, y: centre.y + normal.y * half },\n reason,\n wallId: wall.id,\n });\n };\n\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n const len = dist(a, b);\n for (const [s, e] of wallCentrelineSpans(floor, wall)) {\n if (s > EPS) capAt(wall, s, 'jamb');\n else if (capFreeEnds && (degrees.get(wall.a) ?? 0) === 1) capAt(wall, 0, 'freeEnd');\n\n if (e < len - EPS) capAt(wall, e, 'jamb');\n else if (capFreeEnds && (degrees.get(wall.b) ?? 0) === 1) capAt(wall, len, 'freeEnd');\n }\n }\n\n if (capThicknessSteps) {\n const atNode = new Map<string, PlanWall[]>();\n for (const wall of walls) {\n for (const nodeId of [wall.a, wall.b]) {\n const list = atNode.get(nodeId);\n if (list) list.push(wall);\n else atNode.set(nodeId, [wall]);\n }\n }\n for (const [nodeId, incident] of atNode) {\n if (incident.length !== 2) continue;\n const [w1, w2] = incident;\n if (w1.thickness === w2.thickness) continue;\n // Draw the thicker wall's full cross-section. It overdraws a line across the\n // thinner wall's interior, which is the usual CAD convention for a joint between\n // two wall constructions, and far better than leaving no edge at all.\n const thicker = w1.thickness >= w2.thickness ? w1 : w2;\n const offset = thicker.a === nodeId ? 0 : dist(floor.nodes[thicker.a], floor.nodes[thicker.b]);\n capAt(thicker, offset, 'thicknessStep');\n }\n }\n\n const groups: WallPathGroup[] = [...subpathsByThickness.entries()]\n .sort((x, y) => x[0] - y[0])\n .map(([thickness, subpaths]) => ({ thickness, d: subpaths.join(' ') }));\n\n return { groups, caps };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"floorPlan.path.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.path.ts"],"sourcesContent":["/**\n * Turns walls into SVG path data for the two-pass thick-wall rendering.\n *\n * The trick: both passes stroke the SAME path data, the outline pass at `thickness` and\n * the fill pass at `thickness - 2 * outlineWidth`. The visible outline is not drawn at\n * all, it is what is left of the outline pass after the fill pass covers its middle.\n * That is why the two must never diverge, and why every outline path has to be drawn\n * before every fill path (one `<g>` each, never interleaved pairs).\n *\n * Three things do not fall out of that trick and are handled explicitly:\n *\n * 1. Nothing is ever drawn across a butt end, because there both passes terminate at\n * the same coordinate. Openings and free ends therefore need explicit cap lines.\n * 2. Two butt-capped walls meeting at an angle leave an uncovered notch at the outer\n * corner. Walls are emitted as CHAINS through degree-2 nodes so `stroke-linejoin`\n * miters the corner, and where a chain has to stop, `reflexCornerStubs` mitres the\n * one corner a junction cannot close by itself.\n * 3. Where two walls of different thickness meet end to end, the drawing steps from\n * thick fill straight to background with no edge, so that joint needs a cap too.\n */\n\nimport { nodeDegrees } from './floorPlan.faces';\nimport { dist } from './floorPlan.geometry';\nimport type { PlanFloor, PlanOpening, PlanPoint, PlanWall } from './floorPlan.types';\n\nexport interface WallPathGroup {\n /** Millimetres. The stroke width for the outline pass. */\n thickness: number;\n /**\n * Path data, to be handed VERBATIM to both passes. The outline pass strokes it at\n * `thickness`, the fill pass at `thickness - 2 * outlineWidth`.\n */\n d: string;\n}\n\nexport type WallCapReason = 'jamb' | 'freeEnd' | 'thicknessStep';\n\nexport interface WallCapLine {\n a: PlanPoint;\n b: PlanPoint;\n reason: WallCapReason;\n wallId: string;\n}\n\nexport interface WallPathResult {\n /** Render every group's outline first, then every group's fill. */\n groups: WallPathGroup[];\n /** Drawn last, in the outline colour, at the outline width. */\n caps: WallCapLine[];\n}\n\nexport interface BuildWallPathsOptions {\n /**\n * Build paths for only these walls, as if the rest were not drawn.\n *\n * Node degrees still come from the WHOLE floor, so chains break and corners are filled\n * exactly where they are for the real walls. That is what lets a subset be painted over\n * the top of the full drawing and land on it precisely: a highlight of a room's\n * boundary is the same geometry as the walls themselves, openings and all.\n */\n wallIds?: ReadonlySet<string>;\n /** Cap the free end of a wall that stops in mid-air. Default true. */\n capFreeEnds?: boolean;\n /** Cap the joint where two walls of different thickness meet. Default true. */\n capThicknessSteps?: boolean;\n}\n\nconst EPS = 1e-6;\n\nfunction lerpPoint(a: PlanPoint, b: PlanPoint, t: number): PlanPoint {\n return { x: a.x + (b.x - a.x) * t, y: a.y + (b.y - a.y) * t };\n}\n\nfunction fmt(n: number): string {\n return Number.isInteger(n) ? String(n) : n.toFixed(3).replace(/\\.?0+$/, '');\n}\n\nfunction samePoint(a: PlanPoint, b: PlanPoint): boolean {\n return Math.abs(a.x - b.x) < 0.001 && Math.abs(a.y - b.y) < 0.001;\n}\n\n/**\n * The parts of a wall's centreline that are actually wall, as `[start, end]` offsets in\n * millimetres from node `a`.\n *\n * Overlapping openings merge into one gap; an opening reaching an end removes that span\n * entirely. An empty result means the opening spans the whole wall.\n */\nexport function wallCentrelineSpans(floor: PlanFloor, wall: PlanWall): Array<[number, number]> {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) return [];\n const len = dist(a, b);\n if (len < EPS) return [];\n\n const gaps: Array<[number, number]> = [];\n for (const opening of Object.values(floor.openings)) {\n if (opening.wallId !== wall.id) continue;\n const half = opening.width / 2;\n const start = Math.max(0, opening.offset - half);\n const end = Math.min(len, opening.offset + half);\n if (end > start) gaps.push([start, end]);\n }\n if (gaps.length === 0) return [[0, len]];\n\n gaps.sort((x, y) => x[0] - y[0]);\n const merged: Array<[number, number]> = [gaps[0]];\n for (let i = 1; i < gaps.length; i++) {\n const last = merged[merged.length - 1];\n if (gaps[i][0] <= last[1]) last[1] = Math.max(last[1], gaps[i][1]);\n else merged.push(gaps[i]);\n }\n\n const spans: Array<[number, number]> = [];\n let cursor = 0;\n for (const [start, end] of merged) {\n if (start - cursor > EPS) spans.push([cursor, start]);\n cursor = end;\n }\n if (len - cursor > EPS) spans.push([cursor, len]);\n return spans;\n}\n\n/** Everything a renderer needs to draw a door leaf, swing arc or window symbol. */\nexport function openingGeometry(\n floor: PlanFloor,\n opening: PlanOpening,\n): { centre: PlanPoint; dir: PlanPoint; normal: PlanPoint; p1: PlanPoint; p2: PlanPoint } | null {\n const wall = floor.walls[opening.wallId];\n if (!wall) return null;\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) return null;\n const len = dist(a, b);\n if (len < EPS) return null;\n\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const normal = { x: -dir.y, y: dir.x };\n const half = Math.min(opening.width / 2, len / 2);\n const offset = Math.min(Math.max(opening.offset, half), len - half);\n const centre = { x: a.x + dir.x * offset, y: a.y + dir.y * offset };\n\n return {\n centre,\n dir,\n normal,\n p1: { x: centre.x - dir.x * half, y: centre.y - dir.y * half },\n p2: { x: centre.x + dir.x * half, y: centre.y + dir.y * half },\n };\n}\n\n/** One directed step along a chain. */\ninterface Step {\n wall: PlanWall;\n from: string;\n to: string;\n}\n\n/**\n * Maximal runs of same-thickness walls joined through degree-2 nodes.\n *\n * A chain may only continue through a node whose TOTAL degree is 2 and whose two walls\n * share a thickness: a different stroke width means a different path, and a degree-3+\n * node is a junction rather than a corner.\n */\nfunction buildChains(floor: PlanFloor, walls: PlanWall[], degrees: Map<string, number>): Step[][] {\n const byId = new Map(walls.map((w) => [w.id, w]));\n const atNode = new Map<string, PlanWall[]>();\n for (const wall of walls) {\n for (const nodeId of [wall.a, wall.b]) {\n const list = atNode.get(nodeId);\n if (list) list.push(wall);\n else atNode.set(nodeId, [wall]);\n }\n }\n\n const continuesThrough = (nodeId: string): PlanWall[] | null => {\n if ((degrees.get(nodeId) ?? 0) !== 2) return null;\n const here = atNode.get(nodeId);\n // Both walls must be in this thickness group, or the chain has to break here.\n if (!here || here.length !== 2) return null;\n return here;\n };\n\n const used = new Set<string>();\n const chains: Step[][] = [];\n\n const other = (wall: PlanWall, nodeId: string): string => (wall.a === nodeId ? wall.b : wall.a);\n\n for (const wall of walls) {\n if (used.has(wall.id)) continue;\n\n // Walk backwards to the start of the run, unless it closes on itself.\n let startWall = wall;\n let startNode = wall.a;\n const seenBack = new Set<string>([wall.id]);\n for (;;) {\n const pair = continuesThrough(startNode);\n if (!pair) break;\n const prev = pair[0].id === startWall.id ? pair[1] : pair[0];\n if (seenBack.has(prev.id)) break; // closed loop\n seenBack.add(prev.id);\n startWall = prev;\n startNode = other(prev, startNode);\n }\n\n // Walk forwards, collecting directed steps.\n const steps: Step[] = [];\n let current = startWall;\n let from = startNode;\n for (;;) {\n if (used.has(current.id)) break;\n used.add(current.id);\n const to = other(current, from);\n steps.push({ wall: current, from, to });\n const pair = continuesThrough(to);\n if (!pair) break;\n const next = pair[0].id === current.id ? pair[1] : pair[0];\n if (used.has(next.id)) break;\n current = next;\n from = to;\n if (!byId.has(current.id)) break;\n }\n\n if (steps.length > 0) chains.push(steps);\n }\n\n return chains;\n}\n\n/**\n * The outer corner of the one REFLEX gap at a node, as a two-legged stub.\n *\n * A chain's `stroke-linejoin` mitres the corner wherever a wall bends, but a chain has\n * to stop at a junction, and at a thickness change, so something else has to close the\n * corner there. Most junction corners need nothing: where the angular gap between two\n * neighbouring walls is at most 180 degrees, the two bands reach across it and already\n * meet at the mitre point, because both start AT the node rather than short of it.\n *\n * A gap WIDER than 180 degrees is the exception, and it is the only one. There the two\n * faces bounding the gap diverge instead of crossing, so nothing spans the gap and what\n * is left between the two butt ends is an open wedge biting into the node. It shows up\n * whenever every wall at a node points into the same half-plane: a wall running east, a\n * wall running north, and a third splitting the two leaves a quarter-circle bitten out\n * of the south-west side. Because the gaps around a node sum to 360 degrees, at most\n * one can be reflex, so a node never needs more than one of these.\n *\n * The stub is two legs laid back along the two walls bounding that gap. Stroked at the\n * wall thickness it stays within their own bands, adding nothing anywhere else, and its\n * mitre join fills the wedge and bevels under the same `stroke-miterlimit` as every\n * other corner. Filling every gap this way instead of only the reflex one is what used\n * to fire spikes out of narrow junctions: those gaps had nothing to fill, so the join\n * ran outside the walls, the further the tighter the angle.\n */\nfunction reflexCornerStubs(\n floor: PlanFloor,\n walls: PlanWall[],\n degrees: Map<string, number>,\n): Array<{ thickness: number; d: string }> {\n interface Arm {\n wall: PlanWall;\n dir: PlanPoint;\n angle: number;\n /** Millimetres of wall between the node and the first opening, if any. */\n reach: number;\n }\n\n const armsAt = new Map<string, Arm[]>();\n const addArm = (nodeId: string, arm: Arm) => {\n const list = armsAt.get(nodeId);\n if (list) list.push(arm);\n else armsAt.set(nodeId, [arm]);\n };\n\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n const len = dist(a, b);\n if (len < EPS) continue;\n const spans = wallCentrelineSpans(floor, wall);\n if (spans.length === 0) continue;\n\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const first = spans[0];\n const last = spans[spans.length - 1];\n addArm(wall.a, {\n wall,\n dir,\n angle: Math.atan2(dir.y, dir.x),\n reach: first[0] < EPS ? first[1] : 0,\n });\n addArm(wall.b, {\n wall,\n dir: { x: -dir.x, y: -dir.y },\n angle: Math.atan2(-dir.y, -dir.x),\n reach: last[1] > len - EPS ? len - last[0] : 0,\n });\n }\n\n const stubs: Array<{ thickness: number; d: string }> = [];\n const TAU = Math.PI * 2;\n\n for (const [nodeId, arms] of armsAt) {\n if (arms.length < 2) continue;\n /*\n * A chain runs straight through a plain corner, and its own join closes it. The test\n * has to be the one `buildChains` uses, which reads the degree of the node in the\n * WHOLE floor: drawing a subset of the walls, two arms at a T junction look like a\n * corner but the chain still breaks there, and skipping the fill left a notch bitten\n * out of the join.\n */\n const chainRunsThrough =\n (degrees.get(nodeId) ?? 0) === 2 && arms.length === 2 && arms[0].wall.thickness === arms[1].wall.thickness;\n if (chainRunsThrough) continue;\n\n arms.sort((p, q) => p.angle - q.angle);\n\n let widest = 0;\n let widestGap = -1;\n for (let i = 0; i < arms.length; i++) {\n const next = arms[(i + 1) % arms.length];\n const gap = i === arms.length - 1 ? next.angle + TAU - arms[i].angle : next.angle - arms[i].angle;\n if (gap > widestGap) {\n widestGap = gap;\n widest = i;\n }\n }\n if (widestGap <= Math.PI + EPS) continue;\n\n const from = arms[widest];\n const to = arms[(widest + 1) % arms.length];\n // The thinner of the two, so the stub can never bulge out past the thinner wall.\n const thickness = Math.min(from.wall.thickness, to.wall.thickness);\n // Half a thickness of leg is enough to carry the join, and an opening close to the\n // node shortens it so the stub cannot paint across the jamb.\n const legFrom = Math.min(thickness / 2, from.reach);\n const legTo = Math.min(thickness / 2, to.reach);\n if (legFrom < EPS || legTo < EPS) continue;\n\n const node = floor.nodes[nodeId];\n stubs.push({\n thickness,\n d:\n `M ${fmt(node.x + from.dir.x * legFrom)},${fmt(node.y + from.dir.y * legFrom)}` +\n ` L ${fmt(node.x)},${fmt(node.y)}` +\n ` L ${fmt(node.x + to.dir.x * legTo)},${fmt(node.y + to.dir.y * legTo)}`,\n });\n }\n\n return stubs;\n}\n\n/**\n * Builds the stroke path data for every wall, grouped by thickness, plus the cap lines\n * the two-pass trick cannot produce on its own.\n */\nexport function buildWallPaths(floor: PlanFloor, options: BuildWallPathsOptions = {}): WallPathResult {\n const { wallIds, capFreeEnds = true, capThicknessSteps = true } = options;\n\n const walls = Object.values(floor.walls).filter((w) => {\n if (wallIds && !wallIds.has(w.id)) return false;\n const a = floor.nodes[w.a];\n const b = floor.nodes[w.b];\n return a && b && dist(a, b) > EPS && w.a !== w.b;\n });\n if (walls.length === 0) return { groups: [], caps: [] };\n\n const degrees = nodeDegrees(floor);\n const caps: WallCapLine[] = [];\n\n // ── Group by thickness: one stroke width per path. ────────────────────────────\n const byThickness = new Map<number, PlanWall[]>();\n for (const wall of walls) {\n const list = byThickness.get(wall.thickness);\n if (list) list.push(wall);\n else byThickness.set(wall.thickness, [wall]);\n }\n\n const subpathsByThickness = new Map<number, string[]>();\n const pushSubpath = (thickness: number, d: string) => {\n const list = subpathsByThickness.get(thickness);\n if (list) list.push(d);\n else subpathsByThickness.set(thickness, [d]);\n };\n\n for (const [thickness, groupWalls] of byThickness) {\n for (const chain of buildChains(floor, groupWalls, degrees)) {\n let current: PlanPoint[] = [];\n const flush = () => {\n if (current.length >= 2) {\n pushSubpath(thickness, current.map((p, i) => `${i === 0 ? 'M' : 'L'} ${fmt(p.x)},${fmt(p.y)}`).join(' '));\n }\n current = [];\n };\n\n for (const step of chain) {\n const a = floor.nodes[step.from];\n const b = floor.nodes[step.to];\n const len = dist(a, b);\n if (len < EPS) continue;\n\n // Spans come back in the wall's own a->b direction; flip them when the chain\n // traverses the wall the other way.\n let spans = wallCentrelineSpans(floor, step.wall);\n if (step.from !== step.wall.a) {\n spans = spans.map(([s, e]) => [len - e, len - s] as [number, number]).reverse();\n }\n\n for (const [s, e] of spans) {\n const p1 = lerpPoint(a, b, s / len);\n const p2 = lerpPoint(a, b, e / len);\n if (current.length > 0 && samePoint(current[current.length - 1], p1)) {\n current.push(p2);\n } else {\n flush();\n current = [p1, p2];\n }\n }\n // An opening reaching the far end leaves nothing to continue from.\n if (spans.length === 0 || spans[spans.length - 1][1] < len - EPS) flush();\n }\n\n // A chain that returns to where it started closes properly, so the final corner\n // gets a miter join like every other corner rather than two butt caps.\n const first = chain[0];\n const last = chain[chain.length - 1];\n const closed = first.from === last.to;\n if (closed && current.length >= 3 && samePoint(current[0], current[current.length - 1])) {\n const ring = current.slice(0, -1);\n pushSubpath(thickness, `${ring.map((p, i) => `${i === 0 ? 'M' : 'L'} ${fmt(p.x)},${fmt(p.y)}`).join(' ')} Z`);\n current = [];\n } else {\n flush();\n }\n }\n }\n\n for (const stub of reflexCornerStubs(floor, walls, degrees)) pushSubpath(stub.thickness, stub.d);\n\n // ── Caps. ─────────────────────────────────────────────────────────────────────\n const capAt = (wall: PlanWall, offset: number, reason: WallCapReason) => {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n const len = dist(a, b);\n if (len < EPS) return;\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const normal = { x: -dir.y, y: dir.x };\n const centre = { x: a.x + dir.x * offset, y: a.y + dir.y * offset };\n const half = wall.thickness / 2;\n caps.push({\n a: { x: centre.x - normal.x * half, y: centre.y - normal.y * half },\n b: { x: centre.x + normal.x * half, y: centre.y + normal.y * half },\n reason,\n wallId: wall.id,\n });\n };\n\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n const len = dist(a, b);\n for (const [s, e] of wallCentrelineSpans(floor, wall)) {\n if (s > EPS) capAt(wall, s, 'jamb');\n else if (capFreeEnds && (degrees.get(wall.a) ?? 0) === 1) capAt(wall, 0, 'freeEnd');\n\n if (e < len - EPS) capAt(wall, e, 'jamb');\n else if (capFreeEnds && (degrees.get(wall.b) ?? 0) === 1) capAt(wall, len, 'freeEnd');\n }\n }\n\n if (capThicknessSteps) {\n const atNode = new Map<string, PlanWall[]>();\n for (const wall of walls) {\n for (const nodeId of [wall.a, wall.b]) {\n const list = atNode.get(nodeId);\n if (list) list.push(wall);\n else atNode.set(nodeId, [wall]);\n }\n }\n for (const [nodeId, incident] of atNode) {\n if (incident.length !== 2) continue;\n const [w1, w2] = incident;\n if (w1.thickness === w2.thickness) continue;\n // Draw the thicker wall's full cross-section. It overdraws a line across the\n // thinner wall's interior, which is the usual CAD convention for a joint between\n // two wall constructions, and far better than leaving no edge at all.\n const thicker = w1.thickness >= w2.thickness ? w1 : w2;\n const offset = thicker.a === nodeId ? 0 : dist(floor.nodes[thicker.a], floor.nodes[thicker.b]);\n capAt(thicker, offset, 'thicknessStep');\n }\n }\n\n const groups: WallPathGroup[] = [...subpathsByThickness.entries()]\n .sort((x, y) => x[0] - y[0])\n .map(([thickness, subpaths]) => ({ thickness, d: subpaths.join(' ') }));\n\n return { groups, caps };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{PlanUnitSystem as e}from"./floorPlan.types.mjs";import{boundsOf as t}from"./floorPlan.geometry.mjs";import{newId as n}from"./floorPlan.graph.mjs";function r(e={}){return{id:e.floorId??n(`f`),label:e.floorLabel??`Ground floor`,elevation:0,height:e.height??2500,nodes:{},walls:{},openings:{},rooms:{}}}function i(t={}){return{id:t.id??n(`plan`),name:t.name,version:1,unitSystem:t.unitSystem??e.Metric,floors:[r(t)],revision:0}}function a(e,t){return t?e.floors.find(e=>e.id===t)??null:e.floors[0]??null}function o(e,t,n){let i=a(e,t);if(!i)return{plan:e,floor:r(),result:void 0,issues:[]};let o=n(i);return o.floor===i?{...o,plan:e}:{...o,plan:{...e,floors:e.floors.map(e=>e.id===i.id?o.floor:e),revision:e.revision+1}}}function s(e,t){let n=e.floors.some(e=>e.id===t.id);return{...e,floors:n?e.floors.map(e=>e.id===t.id?t:e):[...e.floors,t],revision:e.revision+1}}function c(e,t){return e.floors.findIndex(e=>e.id===t)}function l(e,t){let n=c(e,t);return n>0?e.floors[n-1]:null}function u(e,t={}){let i=e.floors[0]?.elevation??0,a=Math.max(0,Math.min(t.index??e.floors.length,e.floors.length)),o=e.floors[a-1],s={...r({height:t.height??o?.height??2500}),id:t.id??n(`f`),label:t.label??v(e),elevation:o?o.elevation+o.height:0},c=[...e.floors.slice(0,a),s,...e.floors.slice(a)];return{plan:{...e,floors:_(c,i),revision:e.revision+1},floor:s}}function d(e,t){if(e.floors.length<=1)return e;let n=e.floors[0].elevation,r=e.floors.filter(e=>e.id!==t);return r.length===e.floors.length?e:{...e,floors:_(r,n),revision:e.revision+1}}function f(e,t,r={}){let i=a(e,t);if(!i)return{plan:e,floor:null};let o=new Map(Object.keys(i.nodes).map(e=>[e,n(`n`)])),s=new Map(Object.keys(i.walls).map(e=>[e,n(`w`)])),l={...i,id:n(`f`),label:r.label??`${i.label} copy`,nodes:{},walls:{},openings:{},rooms:{}};for(let e of Object.values(i.nodes)){let t=o.get(e.id);l.nodes[t]={...e,id:t}}for(let e of Object.values(i.walls)){let t=s.get(e.id);l.walls[t]={...e,id:t,a:o.get(e.a),b:o.get(e.b)}}for(let e of Object.values(i.openings)){let t=s.get(e.wallId);if(!t)continue;let r=n(`o`);l.openings[r]={...e,id:r,wallId:t}}for(let e of Object.values(i.rooms)){let t=n(`r`);l.rooms[t]={...e,id:t,anchor:{...e.anchor},wallIds:e.wallIds.map(e=>s.get(e)).filter(e=>!!e)}}let u=e.floors[0].elevation,d=c(e,t)+1,f=[...e.floors.slice(0,d),l,...e.floors.slice(d)];return{plan:{...e,floors:_(f,u),revision:e.revision+1},floor:l}}var p=500;function m(e,t,n){let r=a(e,t),i=Math.max(500,Math.round(n));if(!r||r.height===i)return e;let o=e.floors[0].elevation,s=e.floors.map(e=>e.id===r.id?{...e,height:i}:e);return{...e,floors:_(s,o),revision:e.revision+1}}function h(e,t,n){let r=a(e,t);return!r||r.label===n?e:s(e,{...r,label:n})}function g(e,t,n){let r=c(e,t),i=Math.max(0,Math.min(n,e.floors.length-1));if(r<0||r===i)return e;let a=e.floors[0].elevation,o=[...e.floors],[s]=o.splice(r,1);return o.splice(i,0,s),{...e,floors:_(o,a),revision:e.revision+1}}function _(e,t){let n=t;return e.map(e=>{let t=e.elevation===n?e:{...e,elevation:n};return n+=e.height,t})}function v(e){let t=new Set(e.floors.map(e=>e.label));for(let n=e.floors.length+1;;n++){let e=`Floor ${n}`;if(!t.has(e))return e}}function y(e){let n=
|
|
1
|
+
import{PlanUnitSystem as e}from"./floorPlan.types.mjs";import{boundsOf as t}from"./floorPlan.geometry.mjs";import{newId as n}from"./floorPlan.graph.mjs";function r(e={}){return{id:e.floorId??n(`f`),label:e.floorLabel??`Ground floor`,elevation:0,height:e.height??2500,nodes:{},walls:{},openings:{},rooms:{}}}function i(t={}){return{id:t.id??n(`plan`),name:t.name,version:1,unitSystem:t.unitSystem??e.Metric,floors:[r(t)],revision:0}}function a(e,t){return t?e.floors.find(e=>e.id===t)??null:e.floors[0]??null}function o(e,t,n){let i=a(e,t);if(!i)return{plan:e,floor:r(),result:void 0,issues:[]};let o=n(i);return o.floor===i?{...o,plan:e}:{...o,plan:{...e,floors:e.floors.map(e=>e.id===i.id?o.floor:e),revision:e.revision+1}}}function s(e,t){let n=e.floors.some(e=>e.id===t.id);return{...e,floors:n?e.floors.map(e=>e.id===t.id?t:e):[...e.floors,t],revision:e.revision+1}}function c(e,t){return e.floors.findIndex(e=>e.id===t)}function l(e,t){let n=c(e,t);return n>0?e.floors[n-1]:null}function u(e,t={}){let i=e.floors[0]?.elevation??0,a=Math.max(0,Math.min(t.index??e.floors.length,e.floors.length)),o=e.floors[a-1],s={...r({height:t.height??o?.height??2500}),id:t.id??n(`f`),label:t.label??v(e),elevation:o?o.elevation+o.height:0},c=[...e.floors.slice(0,a),s,...e.floors.slice(a)];return{plan:{...e,floors:_(c,i),revision:e.revision+1},floor:s}}function d(e,t){if(e.floors.length<=1)return e;let n=e.floors[0].elevation,r=e.floors.filter(e=>e.id!==t);return r.length===e.floors.length?e:{...e,floors:_(r,n),revision:e.revision+1}}function f(e,t,r={}){let i=a(e,t);if(!i)return{plan:e,floor:null};let o=new Map(Object.keys(i.nodes).map(e=>[e,n(`n`)])),s=new Map(Object.keys(i.walls).map(e=>[e,n(`w`)])),l={...i,id:n(`f`),label:r.label??`${i.label} copy`,nodes:{},walls:{},openings:{},rooms:{}};for(let e of Object.values(i.nodes)){let t=o.get(e.id);l.nodes[t]={...e,id:t}}for(let e of Object.values(i.walls)){let t=s.get(e.id);l.walls[t]={...e,id:t,a:o.get(e.a),b:o.get(e.b)}}for(let e of Object.values(i.openings)){let t=s.get(e.wallId);if(!t)continue;let r=n(`o`);l.openings[r]={...e,id:r,wallId:t}}for(let e of Object.values(i.rooms)){let t=n(`r`);l.rooms[t]={...e,id:t,anchor:{...e.anchor},wallIds:e.wallIds.map(e=>s.get(e)).filter(e=>!!e)}}let u=e.floors[0].elevation,d=c(e,t)+1,f=[...e.floors.slice(0,d),l,...e.floors.slice(d)];return{plan:{...e,floors:_(f,u),revision:e.revision+1},floor:l}}var p=500;function m(e,t,n){let r=a(e,t),i=Math.max(500,Math.round(n));if(!r||r.height===i)return e;let o=e.floors[0].elevation,s=e.floors.map(e=>e.id===r.id?{...e,height:i}:e);return{...e,floors:_(s,o),revision:e.revision+1}}function h(e,t,n){let r=a(e,t);return!r||r.label===n?e:s(e,{...r,label:n})}function g(e,t,n){let r=c(e,t),i=Math.max(0,Math.min(n,e.floors.length-1));if(r<0||r===i)return e;let a=e.floors[0].elevation,o=[...e.floors],[s]=o.splice(r,1);return o.splice(i,0,s),{...e,floors:_(o,a),revision:e.revision+1}}function _(e,t){let n=t;return e.map(e=>{let t=e.elevation===n?e:{...e,elevation:n};return n+=e.height,t})}function v(e){let t=new Set(e.floors.map(e=>e.label));for(let n=e.floors.length+1;;n++){let e=`Floor ${n}`;if(!t.has(e))return e}}function y(e){let n=Object.values(e.nodes).map(e=>({x:e.x,y:e.y})),r=t(n);if(!r)return null;let i=0;for(let t of Object.values(e.walls))t.thickness>i&&(i=t.thickness);let a=i/2,o=r.minX-a,s=r.minY-a,c=r.maxX+a,l=r.maxY+a;return{minX:o,minY:s,maxX:c,maxY:l,width:c-o,height:l-s}}export{p as MIN_FLOOR_HEIGHT,u as addFloor,f as duplicateFloor,r as emptyFloor,i as emptyFloorPlan,l as floorBelow,y as floorBounds,c as floorIndex,a as getFloor,g as moveFloor,d as removeFloor,h as renameFloor,s as setFloor,m as setFloorHeight,o as updateFloor};
|
|
2
2
|
//# sourceMappingURL=floorPlan.plan.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floorPlan.plan.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.plan.ts"],"sourcesContent":["/**\n * Document-level helpers: creating a plan, addressing a floor within it, and the\n * revision counter that memoisation keys off.\n */\n\nimport { boundsOf } from './floorPlan.geometry';\nimport { newId } from './floorPlan.graph';\nimport { PlanUnitSystem } from './floorPlan.types';\nimport type { FloorPlan, PlanEdit, PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport interface EmptyFloorPlanOptions {\n id?: string;\n name?: string;\n floorId?: string;\n floorLabel?: string;\n unitSystem?: PlanUnitSystem;\n /** Default storey height in millimetres. */\n height?: number;\n}\n\nexport function emptyFloor(options: Omit<EmptyFloorPlanOptions, 'id' | 'name'> = {}): PlanFloor {\n return {\n id: options.floorId ?? newId('f'),\n label: options.floorLabel ?? 'Ground floor',\n elevation: 0,\n height: options.height ?? 2500,\n nodes: {},\n walls: {},\n openings: {},\n rooms: {},\n };\n}\n\nexport function emptyFloorPlan(options: EmptyFloorPlanOptions = {}): FloorPlan {\n return {\n id: options.id ?? newId('plan'),\n name: options.name,\n version: 1,\n unitSystem: options.unitSystem ?? PlanUnitSystem.Metric,\n floors: [emptyFloor(options)],\n revision: 0,\n };\n}\n\nexport function getFloor(plan: FloorPlan, floorId?: string): PlanFloor | null {\n if (!floorId) return plan.floors[0] ?? null;\n return plan.floors.find((f) => f.id === floorId) ?? null;\n}\n\n/**\n * Applies a floor-level edit to one floor of a plan, bumping `revision` so memoised\n * derivations know to recompute.\n *\n * The revision only moves when the edit actually returned a different floor, so a\n * gesture that changed nothing does not invalidate anything.\n */\nexport function updateFloor<T>(\n plan: FloorPlan,\n floorId: string | undefined,\n edit: (floor: PlanFloor) => PlanEdit<T>,\n): PlanEdit<T> & { plan: FloorPlan } {\n const floor = getFloor(plan, floorId);\n if (!floor) {\n return { plan, floor: emptyFloor(), result: undefined as T, issues: [] };\n }\n\n const applied = edit(floor);\n if (applied.floor === floor) {\n return { ...applied, plan };\n }\n\n return {\n ...applied,\n plan: {\n ...plan,\n floors: plan.floors.map((f) => (f.id === floor.id ? applied.floor : f)),\n revision: plan.revision + 1,\n },\n };\n}\n\n/** Replaces one floor outright, bumping the revision. */\nexport function setFloor(plan: FloorPlan, floor: PlanFloor): FloorPlan {\n const exists = plan.floors.some((f) => f.id === floor.id);\n return {\n ...plan,\n floors: exists ? plan.floors.map((f) => (f.id === floor.id ? floor : f)) : [...plan.floors, floor],\n revision: plan.revision + 1,\n };\n}\n\n// ─── Floors ────────────────────────────────────────────────────────────────────\n\n/**\n * Floors run bottom to top: index 0 is the lowest storey, and `elevation` rises with\n * the index. Nothing enforces that beyond these helpers, but `floorBelow` and the\n * drawer's underlay both read it, so an out-of-order `floors` array shows the wrong\n * storey underneath.\n */\nexport function floorIndex(plan: FloorPlan, floorId: string): number {\n return plan.floors.findIndex((f) => f.id === floorId);\n}\n\n/** The storey directly under this one, which is what an underlay traces. */\nexport function floorBelow(plan: FloorPlan, floorId: string): PlanFloor | null {\n const i = floorIndex(plan, floorId);\n return i > 0 ? plan.floors[i - 1] : null;\n}\n\nexport interface AddFloorOptions {\n label?: string;\n /** Millimetres. Defaults to the storey height of the floor it is added above. */\n height?: number;\n /** Where to put it. Defaults to directly above `aboveFloorId`, else on top. */\n index?: number;\n id?: string;\n}\n\n/**\n * Adds an empty storey and returns the plan with it, plus the floor itself.\n *\n * Its elevation is the top of the storey below, so a plan stays physically coherent\n * without the caller doing arithmetic. Everything above is pushed up by the new storey's\n * height, since inserting a floor into a building raises the ones over it.\n */\nexport function addFloor(\n plan: FloorPlan,\n options: AddFloorOptions = {},\n): { plan: FloorPlan; floor: PlanFloor } {\n const base = plan.floors[0]?.elevation ?? 0;\n const at = Math.max(0, Math.min(options.index ?? plan.floors.length, plan.floors.length));\n const below = plan.floors[at - 1];\n const height = options.height ?? below?.height ?? 2500;\n\n const floor: PlanFloor = {\n ...emptyFloor({ height }),\n id: options.id ?? newId('f'),\n label: options.label ?? nextFloorLabel(plan),\n elevation: below ? below.elevation + below.height : 0,\n };\n\n const floors = [...plan.floors.slice(0, at), floor, ...plan.floors.slice(at)];\n return { plan: { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 }, floor };\n}\n\n/**\n * Removes a storey, unless it is the last one: a plan with no floors has nowhere to\n * draw, and every consumer would need a special case for it.\n */\nexport function removeFloor(plan: FloorPlan, floorId: string): FloorPlan {\n if (plan.floors.length <= 1) return plan;\n const base = plan.floors[0].elevation;\n const floors = plan.floors.filter((f) => f.id !== floorId);\n if (floors.length === plan.floors.length) return plan;\n return { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 };\n}\n\n/**\n * Copies a storey, including its geometry, directly above itself.\n *\n * Every id is regenerated and every reference rewired to the copy, so the two storeys\n * share nothing: an edit to one must not reach through into the other. This is the usual\n * way to start an upper floor, since the walls below are mostly where the walls above go.\n */\nexport function duplicateFloor(\n plan: FloorPlan,\n floorId: string,\n options: { label?: string } = {},\n): { plan: FloorPlan; floor: PlanFloor | null } {\n const source = getFloor(plan, floorId);\n if (!source) return { plan, floor: null };\n\n const nodeIds = new Map(Object.keys(source.nodes).map((id) => [id, newId('n')]));\n const wallIds = new Map(Object.keys(source.walls).map((id) => [id, newId('w')]));\n\n const copy: PlanFloor = {\n ...source,\n id: newId('f'),\n label: options.label ?? `${source.label} copy`,\n nodes: {},\n walls: {},\n openings: {},\n rooms: {},\n };\n\n for (const node of Object.values(source.nodes)) {\n const id = nodeIds.get(node.id)!;\n copy.nodes[id] = { ...node, id };\n }\n for (const wall of Object.values(source.walls)) {\n const id = wallIds.get(wall.id)!;\n copy.walls[id] = { ...wall, id, a: nodeIds.get(wall.a)!, b: nodeIds.get(wall.b)! };\n }\n for (const opening of Object.values(source.openings)) {\n const wallId = wallIds.get(opening.wallId);\n if (!wallId) continue;\n const id = newId('o');\n copy.openings[id] = { ...opening, id, wallId };\n }\n for (const room of Object.values(source.rooms)) {\n const id = newId('r');\n copy.rooms[id] = {\n ...room,\n id,\n anchor: { ...room.anchor },\n // Room identity is matched on these, so they have to point at the copy's own walls\n // or the first derive would strand every room in it.\n wallIds: room.wallIds.map((w) => wallIds.get(w)).filter((w): w is string => !!w),\n };\n }\n\n const base = plan.floors[0].elevation;\n const at = floorIndex(plan, floorId) + 1;\n const floors = [...plan.floors.slice(0, at), copy, ...plan.floors.slice(at)];\n return { plan: { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 }, floor: copy };\n}\n\n/** Millimetres. Low enough for a crawl space, high enough to stop a zero-height storey. */\nexport const MIN_FLOOR_HEIGHT = 500;\n\n/**\n * Sets a storey's height, and drops or lifts everything above it to suit.\n *\n * This is why it is not a plain `setFloor`: a storey's height is what the next one sits\n * on, so changing it without re-stacking leaves the floors above either buried in it or\n * floating over it, which nothing in 2D would ever show.\n */\nexport function setFloorHeight(plan: FloorPlan, floorId: string, height: number): FloorPlan {\n const floor = getFloor(plan, floorId);\n const next = Math.max(MIN_FLOOR_HEIGHT, Math.round(height));\n if (!floor || floor.height === next) return plan;\n\n const base = plan.floors[0].elevation;\n const floors = plan.floors.map((f) => (f.id === floor.id ? { ...f, height: next } : f));\n return { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 };\n}\n\n/** Renames a storey. Its own field, since nothing else about a floor is text. */\nexport function renameFloor(plan: FloorPlan, floorId: string, label: string): FloorPlan {\n const floor = getFloor(plan, floorId);\n if (!floor || floor.label === label) return plan;\n return setFloor(plan, { ...floor, label });\n}\n\n/** Moves a storey up or down the stack, re-stacking the elevations behind it. */\nexport function moveFloor(plan: FloorPlan, floorId: string, toIndex: number): FloorPlan {\n const from = floorIndex(plan, floorId);\n const to = Math.max(0, Math.min(toIndex, plan.floors.length - 1));\n if (from < 0 || from === to) return plan;\n const base = plan.floors[0].elevation;\n const floors = [...plan.floors];\n const [moved] = floors.splice(from, 1);\n floors.splice(to, 0, moved);\n return { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 };\n}\n\n/**\n * Re-stacks elevations so each storey sits on the one below. Without this, adding or\n * removing a floor leaves a gap or an overlap in the building that only shows up in 3D.\n *\n * `base` is the building's own ground level and must come from the plan BEFORE the\n * edit: reading it off whichever floor ends up lowest moves the whole building whenever\n * two storeys of different heights are reordered.\n */\nfunction reElevate(floors: PlanFloor[], base: number): PlanFloor[] {\n let elevation = base;\n return floors.map((floor) => {\n const next = floor.elevation === elevation ? floor : { ...floor, elevation };\n elevation += floor.height;\n return next;\n });\n}\n\n/** `Floor 2`, `Floor 3`, … skipping any number already used. */\nfunction nextFloorLabel(plan: FloorPlan): string {\n const used = new Set(plan.floors.map((f) => f.label));\n for (let n = plan.floors.length + 1; ; n++) {\n const label = `Floor ${n}`;\n if (!used.has(label)) return label;\n }\n}\n\nexport interface PlanBounds {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n width: number;\n height: number;\n}\n\n/**\n * Bounding box of a floor's geometry, padded by half the thickest wall so the outer\n * face of every wall is inside it rather than its centreline.\n */\nexport function floorBounds(floor: PlanFloor): PlanBounds | null {\n const points: PlanPoint[] = Object.values(floor.nodes).map((n) => ({ x: n.x, y: n.y }));\n const raw = boundsOf(points);\n if (!raw) return null;\n\n let maxThickness = 0;\n for (const wall of Object.values(floor.walls)) {\n if (wall.thickness > maxThickness) maxThickness = wall.thickness;\n }\n const pad = maxThickness / 2;\n\n const minX = raw.minX - pad;\n const minY = raw.minY - pad;\n const maxX = raw.maxX + pad;\n const maxY = raw.maxY + pad;\n return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"floorPlan.plan.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.plan.ts"],"sourcesContent":["/**\n * Document-level helpers: creating a plan, addressing a floor within it, and the\n * revision counter that memoisation keys off.\n */\n\nimport { boundsOf } from './floorPlan.geometry';\nimport { newId } from './floorPlan.graph';\nimport { PlanUnitSystem } from './floorPlan.types';\nimport type { FloorPlan, PlanEdit, PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport interface EmptyFloorPlanOptions {\n id?: string;\n name?: string;\n floorId?: string;\n floorLabel?: string;\n unitSystem?: PlanUnitSystem;\n /** Default storey height in millimetres. */\n height?: number;\n}\n\nexport function emptyFloor(options: Omit<EmptyFloorPlanOptions, 'id' | 'name'> = {}): PlanFloor {\n return {\n id: options.floorId ?? newId('f'),\n label: options.floorLabel ?? 'Ground floor',\n elevation: 0,\n height: options.height ?? 2500,\n nodes: {},\n walls: {},\n openings: {},\n rooms: {},\n };\n}\n\nexport function emptyFloorPlan(options: EmptyFloorPlanOptions = {}): FloorPlan {\n return {\n id: options.id ?? newId('plan'),\n name: options.name,\n version: 1,\n unitSystem: options.unitSystem ?? PlanUnitSystem.Metric,\n floors: [emptyFloor(options)],\n revision: 0,\n };\n}\n\nexport function getFloor(plan: FloorPlan, floorId?: string): PlanFloor | null {\n if (!floorId) return plan.floors[0] ?? null;\n return plan.floors.find((f) => f.id === floorId) ?? null;\n}\n\n/**\n * Applies a floor-level edit to one floor of a plan, bumping `revision` so memoised\n * derivations know to recompute.\n *\n * The revision only moves when the edit actually returned a different floor, so a\n * gesture that changed nothing does not invalidate anything.\n */\nexport function updateFloor<T>(\n plan: FloorPlan,\n floorId: string | undefined,\n edit: (floor: PlanFloor) => PlanEdit<T>,\n): PlanEdit<T> & { plan: FloorPlan } {\n const floor = getFloor(plan, floorId);\n if (!floor) {\n return { plan, floor: emptyFloor(), result: undefined as T, issues: [] };\n }\n\n const applied = edit(floor);\n if (applied.floor === floor) {\n return { ...applied, plan };\n }\n\n return {\n ...applied,\n plan: {\n ...plan,\n floors: plan.floors.map((f) => (f.id === floor.id ? applied.floor : f)),\n revision: plan.revision + 1,\n },\n };\n}\n\n/** Replaces one floor outright, bumping the revision. */\nexport function setFloor(plan: FloorPlan, floor: PlanFloor): FloorPlan {\n const exists = plan.floors.some((f) => f.id === floor.id);\n return {\n ...plan,\n floors: exists ? plan.floors.map((f) => (f.id === floor.id ? floor : f)) : [...plan.floors, floor],\n revision: plan.revision + 1,\n };\n}\n\n// ─── Floors ────────────────────────────────────────────────────────────────────\n\n/**\n * Floors run bottom to top: index 0 is the lowest storey, and `elevation` rises with\n * the index. Nothing enforces that beyond these helpers, but `floorBelow` and the\n * drawer's underlay both read it, so an out-of-order `floors` array shows the wrong\n * storey underneath.\n */\nexport function floorIndex(plan: FloorPlan, floorId: string): number {\n return plan.floors.findIndex((f) => f.id === floorId);\n}\n\n/** The storey directly under this one, which is what an underlay traces. */\nexport function floorBelow(plan: FloorPlan, floorId: string): PlanFloor | null {\n const i = floorIndex(plan, floorId);\n return i > 0 ? plan.floors[i - 1] : null;\n}\n\nexport interface AddFloorOptions {\n label?: string;\n /** Millimetres. Defaults to the storey height of the floor it is added above. */\n height?: number;\n /** Where to put it. Defaults to directly above `aboveFloorId`, else on top. */\n index?: number;\n id?: string;\n}\n\n/**\n * Adds an empty storey and returns the plan with it, plus the floor itself.\n *\n * Its elevation is the top of the storey below, so a plan stays physically coherent\n * without the caller doing arithmetic. Everything above is pushed up by the new storey's\n * height, since inserting a floor into a building raises the ones over it.\n */\nexport function addFloor(plan: FloorPlan, options: AddFloorOptions = {}): { plan: FloorPlan; floor: PlanFloor } {\n const base = plan.floors[0]?.elevation ?? 0;\n const at = Math.max(0, Math.min(options.index ?? plan.floors.length, plan.floors.length));\n const below = plan.floors[at - 1];\n const height = options.height ?? below?.height ?? 2500;\n\n const floor: PlanFloor = {\n ...emptyFloor({ height }),\n id: options.id ?? newId('f'),\n label: options.label ?? nextFloorLabel(plan),\n elevation: below ? below.elevation + below.height : 0,\n };\n\n const floors = [...plan.floors.slice(0, at), floor, ...plan.floors.slice(at)];\n return { plan: { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 }, floor };\n}\n\n/**\n * Removes a storey, unless it is the last one: a plan with no floors has nowhere to\n * draw, and every consumer would need a special case for it.\n */\nexport function removeFloor(plan: FloorPlan, floorId: string): FloorPlan {\n if (plan.floors.length <= 1) return plan;\n const base = plan.floors[0].elevation;\n const floors = plan.floors.filter((f) => f.id !== floorId);\n if (floors.length === plan.floors.length) return plan;\n return { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 };\n}\n\n/**\n * Copies a storey, including its geometry, directly above itself.\n *\n * Every id is regenerated and every reference rewired to the copy, so the two storeys\n * share nothing: an edit to one must not reach through into the other. This is the usual\n * way to start an upper floor, since the walls below are mostly where the walls above go.\n */\nexport function duplicateFloor(\n plan: FloorPlan,\n floorId: string,\n options: { label?: string } = {},\n): { plan: FloorPlan; floor: PlanFloor | null } {\n const source = getFloor(plan, floorId);\n if (!source) return { plan, floor: null };\n\n const nodeIds = new Map(Object.keys(source.nodes).map((id) => [id, newId('n')]));\n const wallIds = new Map(Object.keys(source.walls).map((id) => [id, newId('w')]));\n\n const copy: PlanFloor = {\n ...source,\n id: newId('f'),\n label: options.label ?? `${source.label} copy`,\n nodes: {},\n walls: {},\n openings: {},\n rooms: {},\n };\n\n for (const node of Object.values(source.nodes)) {\n const id = nodeIds.get(node.id)!;\n copy.nodes[id] = { ...node, id };\n }\n for (const wall of Object.values(source.walls)) {\n const id = wallIds.get(wall.id)!;\n copy.walls[id] = { ...wall, id, a: nodeIds.get(wall.a)!, b: nodeIds.get(wall.b)! };\n }\n for (const opening of Object.values(source.openings)) {\n const wallId = wallIds.get(opening.wallId);\n if (!wallId) continue;\n const id = newId('o');\n copy.openings[id] = { ...opening, id, wallId };\n }\n for (const room of Object.values(source.rooms)) {\n const id = newId('r');\n copy.rooms[id] = {\n ...room,\n id,\n anchor: { ...room.anchor },\n // Room identity is matched on these, so they have to point at the copy's own walls\n // or the first derive would strand every room in it.\n wallIds: room.wallIds.map((w) => wallIds.get(w)).filter((w): w is string => !!w),\n };\n }\n\n const base = plan.floors[0].elevation;\n const at = floorIndex(plan, floorId) + 1;\n const floors = [...plan.floors.slice(0, at), copy, ...plan.floors.slice(at)];\n return { plan: { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 }, floor: copy };\n}\n\n/** Millimetres. Low enough for a crawl space, high enough to stop a zero-height storey. */\nexport const MIN_FLOOR_HEIGHT = 500;\n\n/**\n * Sets a storey's height, and drops or lifts everything above it to suit.\n *\n * This is why it is not a plain `setFloor`: a storey's height is what the next one sits\n * on, so changing it without re-stacking leaves the floors above either buried in it or\n * floating over it, which nothing in 2D would ever show.\n */\nexport function setFloorHeight(plan: FloorPlan, floorId: string, height: number): FloorPlan {\n const floor = getFloor(plan, floorId);\n const next = Math.max(MIN_FLOOR_HEIGHT, Math.round(height));\n if (!floor || floor.height === next) return plan;\n\n const base = plan.floors[0].elevation;\n const floors = plan.floors.map((f) => (f.id === floor.id ? { ...f, height: next } : f));\n return { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 };\n}\n\n/** Renames a storey. Its own field, since nothing else about a floor is text. */\nexport function renameFloor(plan: FloorPlan, floorId: string, label: string): FloorPlan {\n const floor = getFloor(plan, floorId);\n if (!floor || floor.label === label) return plan;\n return setFloor(plan, { ...floor, label });\n}\n\n/** Moves a storey up or down the stack, re-stacking the elevations behind it. */\nexport function moveFloor(plan: FloorPlan, floorId: string, toIndex: number): FloorPlan {\n const from = floorIndex(plan, floorId);\n const to = Math.max(0, Math.min(toIndex, plan.floors.length - 1));\n if (from < 0 || from === to) return plan;\n const base = plan.floors[0].elevation;\n const floors = [...plan.floors];\n const [moved] = floors.splice(from, 1);\n floors.splice(to, 0, moved);\n return { ...plan, floors: reElevate(floors, base), revision: plan.revision + 1 };\n}\n\n/**\n * Re-stacks elevations so each storey sits on the one below. Without this, adding or\n * removing a floor leaves a gap or an overlap in the building that only shows up in 3D.\n *\n * `base` is the building's own ground level and must come from the plan BEFORE the\n * edit: reading it off whichever floor ends up lowest moves the whole building whenever\n * two storeys of different heights are reordered.\n */\nfunction reElevate(floors: PlanFloor[], base: number): PlanFloor[] {\n let elevation = base;\n return floors.map((floor) => {\n const next = floor.elevation === elevation ? floor : { ...floor, elevation };\n elevation += floor.height;\n return next;\n });\n}\n\n/** `Floor 2`, `Floor 3`, … skipping any number already used. */\nfunction nextFloorLabel(plan: FloorPlan): string {\n const used = new Set(plan.floors.map((f) => f.label));\n for (let n = plan.floors.length + 1; ; n++) {\n const label = `Floor ${n}`;\n if (!used.has(label)) return label;\n }\n}\n\nexport interface PlanBounds {\n minX: number;\n minY: number;\n maxX: number;\n maxY: number;\n width: number;\n height: number;\n}\n\n/**\n * Bounding box of a floor's geometry, padded by half the thickest wall so the outer\n * face of every wall is inside it rather than its centreline.\n */\nexport function floorBounds(floor: PlanFloor): PlanBounds | null {\n const points: PlanPoint[] = Object.values(floor.nodes).map((n) => ({ x: n.x, y: n.y }));\n const raw = boundsOf(points);\n if (!raw) return null;\n\n let maxThickness = 0;\n for (const wall of Object.values(floor.walls)) {\n if (wall.thickness > maxThickness) maxThickness = wall.thickness;\n }\n const pad = maxThickness / 2;\n\n const minX = raw.minX - pad;\n const minY = raw.minY - pad;\n const maxX = raw.maxX + pad;\n const maxY = raw.maxY + pad;\n return { minX, minY, maxX, maxY, width: maxX - minX, height: maxY - minY };\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{PlanIssueKind as e}from"./floorPlan.types.mjs";import{pointInRings as t,representativePoint as n}from"./floorPlan.geometry.mjs";import{newId as r}from"./floorPlan.graph.mjs";import{deriveFaces as i}from"./floorPlan.faces.mjs";var a={boundary:2,anchor:1,area:.25};function
|
|
1
|
+
import{PlanIssueKind as e}from"./floorPlan.types.mjs";import{pointInRings as t,representativePoint as n}from"./floorPlan.geometry.mjs";import{newId as r}from"./floorPlan.graph.mjs";import{deriveFaces as i}from"./floorPlan.faces.mjs";var a={boundary:2,anchor:1,area:.25},o=.15;function s(e,t){if(e.size===0&&t.size===0)return 0;let n=0;for(let r of e)t.has(r)&&(n+=1);let r=e.size+t.size-n;return r===0?0:n/r}function c(e,n,r){let i=s(new Set(e.wallIds),r),o=+!!t(e.anchor,n.outer,n.holes),c=e.area>0&&n.area>0?Math.min(e.area,n.area)/Math.max(e.area,n.area):0;return a.boundary*i+a.anchor*o+a.area*c}function l(t){let r=i(t),a=[],o=Object.values(t.rooms),s=r.map(e=>new Set(e.wallIds)),l=[];for(let e=0;e<o.length;e++)for(let t=0;t<r.length;t++){let n=c(o[e],r[t],s[t]);n>.15&&l.push({roomIndex:e,faceIndex:t,score:n})}l.sort((e,t)=>{if(t.score!==e.score)return t.score-e.score;let n=o[e.roomIndex].seq-o[t.roomIndex].seq;return n===0?r[e.faceIndex].id<r[t.faceIndex].id?-1:1:n});let u=Array(o.length).fill(!1),d=Array(r.length).fill(-1);for(let e of l)u[e.roomIndex]||d[e.faceIndex]===-1&&(u[e.roomIndex]=!0,d[e.faceIndex]=e.roomIndex);let f={...t.rooms},p=r.map((e,t)=>{let r=n(e.outer,e.holes),i=d[t];if(i===-1)return{...e,roomId:null,meta:null,anchor:r};let a=o[i],s=a.anchorPinned?a.anchor:r,c={...a,anchor:s,wallIds:[...new Set(e.wallIds)],area:e.area};return f[a.id]=c,{...e,roomId:a.id,meta:c,anchor:s}});for(let t=0;t<o.length;t++)u[t]||a.push({kind:e.UnmatchedRoom,message:`"${o[t].name}" is not currently enclosed by any walls.`,roomIds:[o[t].id]});return{floor:{...t,rooms:f},rooms:p,issues:a}}var u=0;function d(e,t={}){let{rooms:n,floor:i}=l(e),a={...i.rooms},o=[],s=0,c=new Set(Object.values(a).map(e=>e.name));for(let e of n){if(e.roomId)continue;s+=1,u+=1;let n=t.nameFor?.(s,e);if(!n){let e=Object.keys(a).length+1;for(n=`Room ${e}`;c.has(n);)e+=1,n=`Room ${e}`}c.add(n);let i={id:r(`r`),name:n,anchor:e.anchor,anchorPinned:!1,seq:u,wallIds:[...new Set(e.wallIds)],area:e.area};a[i.id]=i,o.push(i)}return{floor:{...i,rooms:a},created:o}}function f(e){let{rooms:t}=l(e),n=new Set(t.map(e=>e.roomId).filter(Boolean)),r={},i=[];for(let t of Object.values(e.rooms))n.has(t.id)?r[t.id]=t:i.push(t.id);return{floor:{...e,rooms:r},removed:i}}function p(e,n){let r=null;for(let i of e)t(n,i.outer,i.holes)&&(!r||i.area<r.area)&&(r=i);return r}export{o as MATCH_THRESHOLD,a as MATCH_WEIGHTS,d as commitDerivation,l as derivePlan,f as pruneUnmatchedRooms,p as roomAtPoint};
|
|
2
2
|
//# sourceMappingURL=floorPlan.rooms.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floorPlan.rooms.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.rooms.ts"],"sourcesContent":["/**\n * Room identity: attaching stored metadata (name, colour) to derived faces.\n *\n * Identity is primarily the SET OF BOUNDARY WALL IDS, with the anchor point as a\n * secondary signal. That ordering is the important design decision here. Anchors alone\n * look sufficient but fail in three ways: a whole-plan drag leaves every absolute\n * anchor behind, a merge gives two anchors one face with no tie-break, and dragging a\n * node can move a boundary past its own anchor. Wall ids survive every operation that\n * only moves geometry, so reshaping and translating match perfectly without anyone\n * having to remember to transform anchors.\n */\n\nimport { deriveFaces } from './floorPlan.faces';\nimport { pointInRings, representativePoint } from './floorPlan.geometry';\nimport { newId } from './floorPlan.graph';\nimport { PlanIssueKind } from './floorPlan.types';\nimport type {\n DerivedRoom,\n PlanDerivation,\n PlanFace,\n PlanFloor,\n PlanIssue,\n PlanRoom,\n} from './floorPlan.types';\n\n/** Relative weights of the three identity signals. */\nexport const MATCH_WEIGHTS = {\n /** Wall ids survive every geometry-only edit, so they carry the most weight. */\n boundary: 2,\n /** Decides a split, where both children share half the old boundary. */\n anchor: 1,\n /** Breaks a merge towards the larger of the two rooms. */\n area: 0.25,\n} as const;\n\n/** Below this, a pair is not the same room at all. */\nexport const MATCH_THRESHOLD = 0.15;\n\nfunction jaccard(a: ReadonlySet<string>, b: ReadonlySet<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const id of a) if (b.has(id)) intersection += 1;\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\nfunction scorePair(room: PlanRoom, face: PlanFace, faceWalls: ReadonlySet<string>): number {\n const boundary = jaccard(new Set(room.wallIds), faceWalls);\n const inside = pointInRings(room.anchor, face.outer, face.holes) ? 1 : 0;\n const areaRatio =\n room.area > 0 && face.area > 0\n ? Math.min(room.area, face.area) / Math.max(room.area, face.area)\n : 0;\n return (\n MATCH_WEIGHTS.boundary * boundary +\n MATCH_WEIGHTS.anchor * inside +\n MATCH_WEIGHTS.area * areaRatio\n );\n}\n\n/**\n * Extracts faces and attaches stored room metadata to them.\n *\n * Pure: returns a new floor whose room match caches and anchors have been refreshed.\n * It never creates or deletes a `PlanRoom`, so it is safe to call on every render.\n * Minting rooms for newly enclosed faces is `commitDerivation`'s job, called once per\n * committed edit.\n */\nexport function derivePlan(floor: PlanFloor): PlanDerivation {\n const faces = deriveFaces(floor);\n const issues: PlanIssue[] = [];\n const rooms = Object.values(floor.rooms);\n\n const faceWallSets = faces.map((f) => new Set(f.wallIds));\n\n interface Pair {\n roomIndex: number;\n faceIndex: number;\n score: number;\n }\n\n const pairs: Pair[] = [];\n for (let r = 0; r < rooms.length; r++) {\n for (let f = 0; f < faces.length; f++) {\n const score = scorePair(rooms[r], faces[f], faceWallSets[f]);\n if (score > MATCH_THRESHOLD) pairs.push({ roomIndex: r, faceIndex: f, score });\n }\n }\n\n // Greedy one-to-one. The seq/id tie-breaks keep the result deterministic, which\n // matters because a merge is decided here: two rooms both score around 0.5 against\n // the merged face, and exactly one of them must win.\n pairs.sort((x, y) => {\n if (y.score !== x.score) return y.score - x.score;\n const seqDiff = rooms[x.roomIndex].seq - rooms[y.roomIndex].seq;\n if (seqDiff !== 0) return seqDiff;\n return faces[x.faceIndex].id < faces[y.faceIndex].id ? -1 : 1;\n });\n\n const roomTaken = new Array<boolean>(rooms.length).fill(false);\n const faceToRoom = new Array<number>(faces.length).fill(-1);\n for (const pair of pairs) {\n if (roomTaken[pair.roomIndex]) continue;\n if (faceToRoom[pair.faceIndex] !== -1) continue;\n roomTaken[pair.roomIndex] = true;\n faceToRoom[pair.faceIndex] = pair.roomIndex;\n }\n\n const nextRooms: Record<string, PlanRoom> = { ...floor.rooms };\n const derived: DerivedRoom[] = faces.map((face, f) => {\n const anchorFallback = representativePoint(face.outer, face.holes);\n const roomIndex = faceToRoom[f];\n if (roomIndex === -1) {\n return { ...face, roomId: null, meta: null, anchor: anchorFallback };\n }\n\n const room = rooms[roomIndex];\n const anchor = room.anchorPinned ? room.anchor : anchorFallback;\n const updated: PlanRoom = {\n ...room,\n anchor,\n // Refreshing the cache is what lets the next edit match against where the room is\n // now rather than where it was when it was created.\n wallIds: [...new Set(face.wallIds)],\n area: face.area,\n };\n nextRooms[room.id] = updated;\n return { ...face, roomId: room.id, meta: updated, anchor };\n });\n\n for (let r = 0; r < rooms.length; r++) {\n if (roomTaken[r]) continue;\n // Kept, never deleted. Re-adding a deleted wall re-splits the face and this room\n // matches its child again by anchor, so the name comes back by itself. Dropping\n // orphans is the one way this model loses data irrecoverably.\n issues.push({\n kind: PlanIssueKind.UnmatchedRoom,\n message: `\"${rooms[r].name}\" is not currently enclosed by any walls.`,\n roomIds: [rooms[r].id],\n });\n }\n\n return { floor: { ...floor, rooms: nextRooms }, rooms: derived, issues };\n}\n\nlet roomSeq = 0;\n\n/** Test-only: makes the room sequence counter reproducible. */\nexport function __resetRoomSeq(): void {\n roomSeq = 0;\n}\n\nexport interface CommitDerivationOptions {\n /** Names new rooms. Receives the 1-based index among rooms created in this call. */\n nameFor?: (index: number, face: DerivedRoom) => string;\n}\n\n/**\n * Mints a `PlanRoom` for every face that matched nothing.\n *\n * Separate from `derivePlan` so that derivation stays free of id minting and can run on\n * every render without inflating the document. Call it once per committed edit.\n */\nexport function commitDerivation(\n floor: PlanFloor,\n options: CommitDerivationOptions = {},\n): { floor: PlanFloor; created: PlanRoom[] } {\n const { rooms: derived, floor: refreshed } = derivePlan(floor);\n const nextRooms = { ...refreshed.rooms };\n const created: PlanRoom[] = [];\n\n let index = 0;\n const existingNames = new Set(Object.values(nextRooms).map((r) => r.name));\n\n for (const face of derived) {\n if (face.roomId) continue;\n index += 1;\n roomSeq += 1;\n\n let name = options.nameFor?.(index, face);\n if (!name) {\n let n = Object.keys(nextRooms).length + 1;\n name = `Room ${n}`;\n while (existingNames.has(name)) {\n n += 1;\n name = `Room ${n}`;\n }\n }\n existingNames.add(name);\n\n const room: PlanRoom = {\n id: newId('r'),\n name,\n anchor: face.anchor,\n anchorPinned: false,\n seq: roomSeq,\n wallIds: [...new Set(face.wallIds)],\n area: face.area,\n };\n nextRooms[room.id] = room;\n created.push(room);\n }\n\n return { floor: { ...refreshed, rooms: nextRooms }, created };\n}\n\n/** Removes room metadata that matched no face. Never called automatically. */\nexport function pruneUnmatchedRooms(floor: PlanFloor): { floor: PlanFloor; removed: string[] } {\n const { rooms: derived } = derivePlan(floor);\n const matched = new Set(derived.map((d) => d.roomId).filter(Boolean) as string[]);\n const nextRooms: Record<string, PlanRoom> = {};\n const removed: string[] = [];\n for (const room of Object.values(floor.rooms)) {\n if (matched.has(room.id)) nextRooms[room.id] = room;\n else removed.push(room.id);\n }\n return { floor: { ...floor, rooms: nextRooms }, removed };\n}\n\n/** Convenience: the derived room whose polygon contains `point`, if any. */\nexport function roomAtPoint(derived: DerivedRoom[], point: { x: number; y: number }): DerivedRoom | null {\n let best: DerivedRoom | null = null;\n for (const room of derived) {\n if (!pointInRings(point, room.outer, room.holes)) continue;\n // Smallest wins, so a room nested inside another is selectable.\n if (!best || room.area < best.area) best = room;\n }\n return best;\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"floorPlan.rooms.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.rooms.ts"],"sourcesContent":["/**\n * Room identity: attaching stored metadata (name, colour) to derived faces.\n *\n * Identity is primarily the SET OF BOUNDARY WALL IDS, with the anchor point as a\n * secondary signal. That ordering is the important design decision here. Anchors alone\n * look sufficient but fail in three ways: a whole-plan drag leaves every absolute\n * anchor behind, a merge gives two anchors one face with no tie-break, and dragging a\n * node can move a boundary past its own anchor. Wall ids survive every operation that\n * only moves geometry, so reshaping and translating match perfectly without anyone\n * having to remember to transform anchors.\n */\n\nimport { deriveFaces } from './floorPlan.faces';\nimport { pointInRings, representativePoint } from './floorPlan.geometry';\nimport { newId } from './floorPlan.graph';\nimport { PlanIssueKind } from './floorPlan.types';\nimport type { DerivedRoom, PlanDerivation, PlanFace, PlanFloor, PlanIssue, PlanRoom } from './floorPlan.types';\n\n/** Relative weights of the three identity signals. */\nexport const MATCH_WEIGHTS = {\n /** Wall ids survive every geometry-only edit, so they carry the most weight. */\n boundary: 2,\n /** Decides a split, where both children share half the old boundary. */\n anchor: 1,\n /** Breaks a merge towards the larger of the two rooms. */\n area: 0.25,\n} as const;\n\n/** Below this, a pair is not the same room at all. */\nexport const MATCH_THRESHOLD = 0.15;\n\nfunction jaccard(a: ReadonlySet<string>, b: ReadonlySet<string>): number {\n if (a.size === 0 && b.size === 0) return 0;\n let intersection = 0;\n for (const id of a) if (b.has(id)) intersection += 1;\n const union = a.size + b.size - intersection;\n return union === 0 ? 0 : intersection / union;\n}\n\nfunction scorePair(room: PlanRoom, face: PlanFace, faceWalls: ReadonlySet<string>): number {\n const boundary = jaccard(new Set(room.wallIds), faceWalls);\n const inside = pointInRings(room.anchor, face.outer, face.holes) ? 1 : 0;\n const areaRatio =\n room.area > 0 && face.area > 0 ? Math.min(room.area, face.area) / Math.max(room.area, face.area) : 0;\n return MATCH_WEIGHTS.boundary * boundary + MATCH_WEIGHTS.anchor * inside + MATCH_WEIGHTS.area * areaRatio;\n}\n\n/**\n * Extracts faces and attaches stored room metadata to them.\n *\n * Pure: returns a new floor whose room match caches and anchors have been refreshed.\n * It never creates or deletes a `PlanRoom`, so it is safe to call on every render.\n * Minting rooms for newly enclosed faces is `commitDerivation`'s job, called once per\n * committed edit.\n */\nexport function derivePlan(floor: PlanFloor): PlanDerivation {\n const faces = deriveFaces(floor);\n const issues: PlanIssue[] = [];\n const rooms = Object.values(floor.rooms);\n\n const faceWallSets = faces.map((f) => new Set(f.wallIds));\n\n interface Pair {\n roomIndex: number;\n faceIndex: number;\n score: number;\n }\n\n const pairs: Pair[] = [];\n for (let r = 0; r < rooms.length; r++) {\n for (let f = 0; f < faces.length; f++) {\n const score = scorePair(rooms[r], faces[f], faceWallSets[f]);\n if (score > MATCH_THRESHOLD) pairs.push({ roomIndex: r, faceIndex: f, score });\n }\n }\n\n // Greedy one-to-one. The seq/id tie-breaks keep the result deterministic, which\n // matters because a merge is decided here: two rooms both score around 0.5 against\n // the merged face, and exactly one of them must win.\n pairs.sort((x, y) => {\n if (y.score !== x.score) return y.score - x.score;\n const seqDiff = rooms[x.roomIndex].seq - rooms[y.roomIndex].seq;\n if (seqDiff !== 0) return seqDiff;\n return faces[x.faceIndex].id < faces[y.faceIndex].id ? -1 : 1;\n });\n\n const roomTaken = new Array<boolean>(rooms.length).fill(false);\n const faceToRoom = new Array<number>(faces.length).fill(-1);\n for (const pair of pairs) {\n if (roomTaken[pair.roomIndex]) continue;\n if (faceToRoom[pair.faceIndex] !== -1) continue;\n roomTaken[pair.roomIndex] = true;\n faceToRoom[pair.faceIndex] = pair.roomIndex;\n }\n\n const nextRooms: Record<string, PlanRoom> = { ...floor.rooms };\n const derived: DerivedRoom[] = faces.map((face, f) => {\n const anchorFallback = representativePoint(face.outer, face.holes);\n const roomIndex = faceToRoom[f];\n if (roomIndex === -1) {\n return { ...face, roomId: null, meta: null, anchor: anchorFallback };\n }\n\n const room = rooms[roomIndex];\n const anchor = room.anchorPinned ? room.anchor : anchorFallback;\n const updated: PlanRoom = {\n ...room,\n anchor,\n // Refreshing the cache is what lets the next edit match against where the room is\n // now rather than where it was when it was created.\n wallIds: [...new Set(face.wallIds)],\n area: face.area,\n };\n nextRooms[room.id] = updated;\n return { ...face, roomId: room.id, meta: updated, anchor };\n });\n\n for (let r = 0; r < rooms.length; r++) {\n if (roomTaken[r]) continue;\n // Kept, never deleted. Re-adding a deleted wall re-splits the face and this room\n // matches its child again by anchor, so the name comes back by itself. Dropping\n // orphans is the one way this model loses data irrecoverably.\n issues.push({\n kind: PlanIssueKind.UnmatchedRoom,\n message: `\"${rooms[r].name}\" is not currently enclosed by any walls.`,\n roomIds: [rooms[r].id],\n });\n }\n\n return { floor: { ...floor, rooms: nextRooms }, rooms: derived, issues };\n}\n\nlet roomSeq = 0;\n\n/** Test-only: makes the room sequence counter reproducible. */\nexport function __resetRoomSeq(): void {\n roomSeq = 0;\n}\n\nexport interface CommitDerivationOptions {\n /** Names new rooms. Receives the 1-based index among rooms created in this call. */\n nameFor?: (index: number, face: DerivedRoom) => string;\n}\n\n/**\n * Mints a `PlanRoom` for every face that matched nothing.\n *\n * Separate from `derivePlan` so that derivation stays free of id minting and can run on\n * every render without inflating the document. Call it once per committed edit.\n */\nexport function commitDerivation(\n floor: PlanFloor,\n options: CommitDerivationOptions = {},\n): { floor: PlanFloor; created: PlanRoom[] } {\n const { rooms: derived, floor: refreshed } = derivePlan(floor);\n const nextRooms = { ...refreshed.rooms };\n const created: PlanRoom[] = [];\n\n let index = 0;\n const existingNames = new Set(Object.values(nextRooms).map((r) => r.name));\n\n for (const face of derived) {\n if (face.roomId) continue;\n index += 1;\n roomSeq += 1;\n\n let name = options.nameFor?.(index, face);\n if (!name) {\n let n = Object.keys(nextRooms).length + 1;\n name = `Room ${n}`;\n while (existingNames.has(name)) {\n n += 1;\n name = `Room ${n}`;\n }\n }\n existingNames.add(name);\n\n const room: PlanRoom = {\n id: newId('r'),\n name,\n anchor: face.anchor,\n anchorPinned: false,\n seq: roomSeq,\n wallIds: [...new Set(face.wallIds)],\n area: face.area,\n };\n nextRooms[room.id] = room;\n created.push(room);\n }\n\n return { floor: { ...refreshed, rooms: nextRooms }, created };\n}\n\n/** Removes room metadata that matched no face. Never called automatically. */\nexport function pruneUnmatchedRooms(floor: PlanFloor): { floor: PlanFloor; removed: string[] } {\n const { rooms: derived } = derivePlan(floor);\n const matched = new Set(derived.map((d) => d.roomId).filter(Boolean) as string[]);\n const nextRooms: Record<string, PlanRoom> = {};\n const removed: string[] = [];\n for (const room of Object.values(floor.rooms)) {\n if (matched.has(room.id)) nextRooms[room.id] = room;\n else removed.push(room.id);\n }\n return { floor: { ...floor, rooms: nextRooms }, removed };\n}\n\n/** Convenience: the derived room whose polygon contains `point`, if any. */\nexport function roomAtPoint(derived: DerivedRoom[], point: { x: number; y: number }): DerivedRoom | null {\n let best: DerivedRoom | null = null;\n for (const room of derived) {\n if (!pointInRings(point, room.outer, room.holes)) continue;\n // Smallest wins, so a room nested inside another is selectable.\n if (!best || room.area < best.area) best = room;\n }\n return best;\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floorPlan.snap.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.snap.ts"],"sourcesContent":["/**\n * Snapping, and the screen-to-world conversion it depends on.\n *\n * Two things here are deliberately different from the previous implementation:\n *\n * 1. Conversion and snapping are separate. `clientToWorld` only converts; `resolveSnap`\n * is the only thing that decides. The old `clientToSVG` called `snapPt` internally,\n * which made grid snap unconditional and left no way to hold Alt to disable it.\n *\n * 2. Every tolerance is in SCREEN PIXELS, converted through the current zoom at the\n * point of use. A fixed world-unit tolerance (the old `WALL_TOLERANCE = 8`) means\n * snapping covers a smaller and smaller part of the screen as you zoom in, until it\n * stops working.\n */\n\nimport {\n dist,\n pointToSegment,\n projectOnSegment,\n segmentIntersection,\n} from './floorPlan.geometry';\nimport type { PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport enum SnapKind {\n None = 'none',\n Node = 'node',\n /** A point projected onto a wall's centreline. */\n Wall = 'wall',\n /** The crossing of two walls, or of a wall with the active angle ray. */\n Intersection = 'intersection',\n /** On the infinite line continuing an existing wall. */\n Extension = 'extension',\n /** Shares an x or y coordinate with an existing node. */\n Alignment = 'alignment',\n /** On a ray at a multiple of `angleStepDeg` from `origin`. */\n Angle = 'angle',\n Grid = 'grid',\n}\n\nexport interface SnapGuide {\n kind: SnapKind;\n a: PlanPoint;\n b: PlanPoint;\n /** Render as an unbounded construction line rather than a segment. */\n infinite?: boolean;\n}\n\nexport interface SnapOptions {\n /** Which snaps are active. Pass `[]` (Alt held) to disable snapping entirely. */\n enabled: SnapKind[];\n /** Millimetres. 0 disables grid snap. */\n gridSize: number;\n /** Degrees. 45 gives 0/45/90. 0 disables angle lock. */\n angleStepDeg: number;\n toleranceScreenPx: number;\n /** Nodes are the most valuable snap, so they get a little more room. */\n nodeToleranceScreenPx?: number;\n}\n\nexport const DEFAULT_SNAP_OPTIONS: SnapOptions = {\n enabled: [\n SnapKind.Node,\n SnapKind.Intersection,\n SnapKind.Wall,\n SnapKind.Angle,\n SnapKind.Extension,\n SnapKind.Alignment,\n SnapKind.Grid,\n ],\n gridSize: 100,\n angleStepDeg: 0,\n toleranceScreenPx: 10,\n nodeToleranceScreenPx: 12,\n};\n\nexport interface SnapContext {\n floor: PlanFloor;\n /** World units per screen pixel at the current zoom. See `screenScale`. */\n pxToWorld: number;\n /** The anchored point. Required for angle lock; also enables the length readout. */\n origin?: PlanPoint;\n /**\n * A direction in radians that angle lock measures from, in ADDITION to the world axes.\n *\n * Pass the direction of the wall being continued and the lock offers perpendicular and\n * parallel to that wall, not just to the world. Without it, angle lock is only useful\n * on a building that happens to be aligned to the page.\n */\n referenceAngle?: number;\n excludeWallIds?: ReadonlySet<string>;\n excludeNodeIds?: ReadonlySet<string>;\n options?: Partial<SnapOptions>;\n}\n\nexport interface SnapResult {\n point: PlanPoint;\n kind: SnapKind;\n nodeId?: string;\n wallId?: string;\n /** Parameter along `wallId`, for `SnapKind.Wall`. */\n t?: number;\n /** Everything an overlay needs to draw to explain the snap. */\n guides: SnapGuide[];\n /** Radians from `origin` to `point`, when an origin was given. */\n angle?: number;\n /** Distance from `origin` to `point`, when an origin was given. */\n distance?: number;\n /**\n * Set when angle lock produced the result, saying what it locked against: the world\n * axes, or the reference direction that was passed in.\n */\n angleRelativeTo?: 'world' | 'reference';\n}\n\n/** An infinite line as a point and a unit direction. */\ninterface Line {\n kind: SnapKind;\n point: PlanPoint;\n dir: PlanPoint;\n residual: number;\n /** Angle lock only: which basis the ray was measured from. */\n relativeTo?: 'world' | 'reference';\n}\n\n/** World units per screen pixel, from the element's current transform. */\nexport function screenScale(svg: SVGSVGElement): number {\n const ctm = svg.getScreenCTM();\n if (!ctm) return 1;\n // Measured rather than read off ctm.a, so a non-uniform or rotated transform still\n // gives a sensible scalar.\n const scale = Math.hypot(ctm.a, ctm.b);\n return scale === 0 ? 1 : 1 / scale;\n}\n\n/**\n * Client (screen) coordinates to world coordinates. Pure conversion, no snapping.\n *\n * Uses `getBoundingClientRect` arithmetic against an explicit viewport rather than\n * `getScreenCTM`, which returns `null` in jsdom and so makes anything built on it\n * untestable.\n */\nexport function clientToWorld(\n clientX: number,\n clientY: number,\n rect: { left: number; top: number },\n view: { x: number; y: number; zoom: number },\n): PlanPoint {\n return {\n x: view.x + (clientX - rect.left) / view.zoom,\n y: view.y + (clientY - rect.top) / view.zoom,\n };\n}\n\nfunction perpendicularDistanceToLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): number {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n return Math.abs(vx * line.dir.y - vy * line.dir.x);\n}\n\nfunction projectOnLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): PlanPoint {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n const along = vx * line.dir.x + vy * line.dir.y;\n return { x: line.point.x + line.dir.x * along, y: line.point.y + line.dir.y * along };\n}\n\nfunction intersectLines(l1: Line, l2: Line): PlanPoint | null {\n const denom = l1.dir.x * l2.dir.y - l1.dir.y * l2.dir.x;\n if (Math.abs(denom) < 1e-9) return null;\n const dx = l2.point.x - l1.point.x;\n const dy = l2.point.y - l1.point.y;\n const t = (dx * l2.dir.y - dy * l2.dir.x) / denom;\n return { x: l1.point.x + l1.dir.x * t, y: l1.point.y + l1.dir.y * t };\n}\n\nfunction angleBetween(l1: Line, l2: Line): number {\n const dot = Math.abs(l1.dir.x * l2.dir.x + l1.dir.y * l2.dir.y);\n return Math.acos(Math.min(1, dot));\n}\n\nfunction roundTo(value: number, step: number): number {\n return step > 0 ? Math.round(value / step) * step : value;\n}\n\n/**\n * Resolves a raw pointer position to the point the user meant.\n *\n * Two stages, because half of these snaps are constraints rather than points. A hard\n * snap (node, intersection, wall) fixes both degrees of freedom, so the first match\n * wins. A constraint (angle lock, extension, alignment) removes only one, so a flat\n * \"first match wins\" list would kick the point off the angle ray the moment an\n * alignment also matched. Two constraints intersect; one constraint is projected onto\n * and then slid ALONG to the nearest grid position, which is what lets angle lock and\n * grid coexist instead of fighting.\n */\nexport function resolveSnap(raw: PlanPoint, ctx: SnapContext): SnapResult {\n const options: SnapOptions = { ...DEFAULT_SNAP_OPTIONS, ...ctx.options };\n const enabled = new Set(options.enabled);\n\n /**\n * Measures the resolved point back against the origin, so every branch reports the\n * same angle and distance without each one having to compute them. Taking them from\n * the final point rather than from whichever branch produced it means they always\n * describe what the user actually gets.\n */\n const finish = (result: SnapResult): SnapResult => {\n if (!ctx.origin) return result;\n const dx = result.point.x - ctx.origin.x;\n const dy = result.point.y - ctx.origin.y;\n return { ...result, angle: Math.atan2(dy, dx), distance: Math.hypot(dx, dy) };\n };\n\n if (enabled.size === 0) return finish({ point: raw, kind: SnapKind.None, guides: [] });\n\n const tol = options.toleranceScreenPx * ctx.pxToWorld;\n const nodeTol = (options.nodeToleranceScreenPx ?? options.toleranceScreenPx) * ctx.pxToWorld;\n const { floor } = ctx;\n\n /**\n * Angle lock, when the caller has asked for it, is a HARD constraint rather than\n * another proximity snap: `angleStepDeg > 0` only happens because the user latched the\n * lock or held the modifier. Gating it by tolerance like the ambient snaps would mean\n * it silently stopped applying as soon as you dragged more than a few pixels off the\n * ray, which is the opposite of what a lock is for.\n */\n const lockedRay: Line | null = (() => {\n if (!enabled.has(SnapKind.Angle) || !ctx.origin || options.angleStepDeg <= 0) return null;\n const step = (options.angleStepDeg * Math.PI) / 180;\n const rawAngle = Math.atan2(raw.y - ctx.origin.y, raw.x - ctx.origin.x);\n\n // Measure from the world axes, and also from the reference direction when one was\n // given, so \"square to the wall I am continuing\" is available as well as \"square to\n // the page\". Whichever ray the pointer is closest to wins.\n const bases: Array<{ angle: number; relativeTo: 'world' | 'reference' }> = [\n { angle: 0, relativeTo: 'world' },\n ];\n if (ctx.referenceAngle !== undefined) {\n bases.push({ angle: ctx.referenceAngle, relativeTo: 'reference' });\n }\n\n let best: Line | null = null;\n for (const base of bases) {\n const snapped = base.angle + Math.round((rawAngle - base.angle) / step) * step;\n const dir = { x: Math.cos(snapped), y: Math.sin(snapped) };\n const line: Line = {\n kind: SnapKind.Angle,\n point: ctx.origin,\n dir,\n residual: 0,\n relativeTo: base.relativeTo,\n };\n line.residual = perpendicularDistanceToLine(raw, line);\n if (!best || line.residual < best.residual) best = line;\n }\n return best;\n })();\n\n /**\n * While the lock is on, a hard snap is only allowed to win if it sits ON the ray.\n * Otherwise \"hold the angle\" would be quietly overridden by any nearby corner, which\n * is exactly the case the lock exists to prevent.\n */\n const allowedByLock = (p: PlanPoint): boolean =>\n !lockedRay || perpendicularDistanceToLine(p, lockedRay) <= tol;\n\n // ── Stage A: hard snaps ──────────────────────────────────────────────────────\n\n if (enabled.has(SnapKind.Node)) {\n let best: { id: string; point: PlanPoint; d: number } | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const d = dist(raw, node);\n if (d <= nodeTol && allowedByLock(node) && (!best || d < best.d)) {\n best = { id: node.id, point: { x: node.x, y: node.y }, d };\n }\n }\n if (best) {\n return finish({ point: best.point, kind: SnapKind.Node, nodeId: best.id, guides: [] });\n }\n }\n\n const walls = Object.values(floor.walls).filter((w) => !ctx.excludeWallIds?.has(w.id));\n\n if (enabled.has(SnapKind.Intersection)) {\n let best: { point: PlanPoint; d: number } | null = null;\n for (let i = 0; i < walls.length; i++) {\n for (let j = i + 1; j < walls.length; j++) {\n const a = floor.nodes[walls[i].a];\n const b = floor.nodes[walls[i].b];\n const c = floor.nodes[walls[j].a];\n const d2 = floor.nodes[walls[j].b];\n if (!a || !b || !c || !d2) continue;\n const hit = segmentIntersection(a, b, c, d2);\n if (!hit.point) continue;\n const d = dist(raw, hit.point);\n if (d <= tol && allowedByLock(hit.point) && (!best || d < best.d)) {\n best = { point: hit.point, d };\n }\n }\n }\n if (best) return finish({ point: best.point, kind: SnapKind.Intersection, guides: [] });\n }\n\n if (enabled.has(SnapKind.Wall)) {\n let best: { wallId: string; point: PlanPoint; t: number; d: number } | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const r = pointToSegment(raw, a, b);\n if (r.dist <= tol && allowedByLock(r.point) && (!best || r.dist < best.d)) {\n // Project exactly: intent is allowed to be approximate, the model is not.\n best = { wallId: wall.id, point: projectOnSegment(raw, a, b), t: r.t, d: r.dist };\n }\n }\n if (best) {\n return finish({\n point: best.point,\n kind: SnapKind.Wall,\n wallId: best.wallId,\n t: best.t,\n guides: [],\n });\n }\n }\n\n // ── Stage B: constraints ─────────────────────────────────────────────────────\n\n const constraints: Line[] = [];\n\n if (enabled.has(SnapKind.Extension)) {\n let best: Line | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = dist(a, b);\n if (len < 1e-6) continue;\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const line: Line = { kind: SnapKind.Extension, point: { x: a.x, y: a.y }, dir, residual: 0 };\n line.residual = perpendicularDistanceToLine(raw, line);\n // Only beyond the segment itself; on the segment, the wall snap already applies.\n const onSegment = pointToSegment(raw, a, b).dist <= tol;\n if (line.residual <= tol && !onSegment && (!best || line.residual < best.residual)) {\n best = line;\n }\n }\n if (best) constraints.push(best);\n }\n\n if (enabled.has(SnapKind.Alignment)) {\n let bestV: Line | null = null;\n let bestH: Line | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const dx = Math.abs(raw.x - node.x);\n if (dx <= tol && (!bestV || dx < bestV.residual)) {\n bestV = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 0, y: 1 },\n residual: dx,\n };\n }\n const dy = Math.abs(raw.y - node.y);\n if (dy <= tol && (!bestH || dy < bestH.residual)) {\n bestH = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 1, y: 0 },\n residual: dy,\n };\n }\n }\n if (bestV) constraints.push(bestV);\n if (bestH) constraints.push(bestH);\n }\n\n const guideFor = (line: Line, through: PlanPoint): SnapGuide => ({\n kind: line.kind,\n a: { x: through.x - line.dir.x * 1e5, y: through.y - line.dir.y * 1e5 },\n b: { x: through.x + line.dir.x * 1e5, y: through.y + line.dir.y * 1e5 },\n infinite: true,\n });\n\n if (lockedRay) {\n // The ray is fixed; the only freedom left is where along it the point sits. Another\n // constraint that genuinely crosses it pins that down, which is the useful\n // \"45 degrees AND level with that corner\" case.\n const crossing = constraints\n .filter((c) => c.residual <= tol && angleBetween(lockedRay, c) > (15 * Math.PI) / 180)\n .sort((a, b) => a.residual - b.residual)[0];\n\n if (crossing) {\n const point = intersectLines(lockedRay, crossing);\n if (point && dist(point, raw) <= tol * 4) {\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point), guideFor(crossing, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n }\n\n let point = projectOnLine(raw, lockedRay);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the ray to a round length, rather than rounding both coordinates,\n // which would take the point straight back off it.\n const along =\n (point.x - lockedRay.point.x) * lockedRay.dir.x +\n (point.y - lockedRay.point.y) * lockedRay.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: lockedRay.point.x + lockedRay.dir.x * snappedAlong,\n y: lockedRay.point.y + lockedRay.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n\n if (constraints.length >= 2) {\n const sorted = [...constraints].sort((a, b) => a.residual - b.residual);\n const [l1, l2] = sorted;\n const point = intersectLines(l1, l2);\n // Two nearly parallel constraints intersect somewhere useless and far away.\n if (point && angleBetween(l1, l2) > (15 * Math.PI) / 180 && dist(point, raw) <= tol * 2) {\n return finish({\n point,\n kind: SnapKind.Intersection,\n guides: [guideFor(l1, point), guideFor(l2, point)],\n });\n }\n }\n\n if (constraints.length >= 1) {\n const line = [...constraints].sort((a, b) => a.residual - b.residual)[0];\n let point = projectOnLine(raw, line);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the constraint to a round distance, rather than rounding both\n // coordinates, which would take the point back off the line.\n const along =\n (point.x - line.point.x) * line.dir.x + (point.y - line.point.y) * line.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: line.point.x + line.dir.x * snappedAlong,\n y: line.point.y + line.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: line.kind,\n guides: [guideFor(line, point)],\n angleRelativeTo: line.relativeTo,\n });\n }\n\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n return finish({\n point: { x: roundTo(raw.x, options.gridSize), y: roundTo(raw.y, options.gridSize) },\n kind: SnapKind.Grid,\n guides: [],\n });\n }\n\n return finish({ point: raw, kind: SnapKind.None, guides: [] });\n}\n"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"floorPlan.snap.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.snap.ts"],"sourcesContent":["/**\n * Snapping, and the screen-to-world conversion it depends on.\n *\n * Two things here are deliberately different from the previous implementation:\n *\n * 1. Conversion and snapping are separate. `clientToWorld` only converts; `resolveSnap`\n * is the only thing that decides. The old `clientToSVG` called `snapPt` internally,\n * which made grid snap unconditional and left no way to hold Alt to disable it.\n *\n * 2. Every tolerance is in SCREEN PIXELS, converted through the current zoom at the\n * point of use. A fixed world-unit tolerance (the old `WALL_TOLERANCE = 8`) means\n * snapping covers a smaller and smaller part of the screen as you zoom in, until it\n * stops working.\n */\n\nimport { dist, pointToSegment, projectOnSegment, segmentIntersection } from './floorPlan.geometry';\nimport type { PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport enum SnapKind {\n None = 'none',\n Node = 'node',\n /** A point projected onto a wall's centreline. */\n Wall = 'wall',\n /** The crossing of two walls, or of a wall with the active angle ray. */\n Intersection = 'intersection',\n /** On the infinite line continuing an existing wall. */\n Extension = 'extension',\n /** Shares an x or y coordinate with an existing node. */\n Alignment = 'alignment',\n /** On a ray at a multiple of `angleStepDeg` from `origin`. */\n Angle = 'angle',\n Grid = 'grid',\n}\n\nexport interface SnapGuide {\n kind: SnapKind;\n a: PlanPoint;\n b: PlanPoint;\n /** Render as an unbounded construction line rather than a segment. */\n infinite?: boolean;\n}\n\nexport interface SnapOptions {\n /** Which snaps are active. Pass `[]` (Alt held) to disable snapping entirely. */\n enabled: SnapKind[];\n /** Millimetres. 0 disables grid snap. */\n gridSize: number;\n /** Degrees. 45 gives 0/45/90. 0 disables angle lock. */\n angleStepDeg: number;\n toleranceScreenPx: number;\n /** Nodes are the most valuable snap, so they get a little more room. */\n nodeToleranceScreenPx?: number;\n}\n\nexport const DEFAULT_SNAP_OPTIONS: SnapOptions = {\n enabled: [\n SnapKind.Node,\n SnapKind.Intersection,\n SnapKind.Wall,\n SnapKind.Angle,\n SnapKind.Extension,\n SnapKind.Alignment,\n SnapKind.Grid,\n ],\n gridSize: 100,\n angleStepDeg: 0,\n toleranceScreenPx: 10,\n nodeToleranceScreenPx: 12,\n};\n\nexport interface SnapContext {\n floor: PlanFloor;\n /** World units per screen pixel at the current zoom. See `screenScale`. */\n pxToWorld: number;\n /** The anchored point. Required for angle lock; also enables the length readout. */\n origin?: PlanPoint;\n /**\n * A direction in radians that angle lock measures from, in ADDITION to the world axes.\n *\n * Pass the direction of the wall being continued and the lock offers perpendicular and\n * parallel to that wall, not just to the world. Without it, angle lock is only useful\n * on a building that happens to be aligned to the page.\n */\n referenceAngle?: number;\n excludeWallIds?: ReadonlySet<string>;\n excludeNodeIds?: ReadonlySet<string>;\n options?: Partial<SnapOptions>;\n}\n\nexport interface SnapResult {\n point: PlanPoint;\n kind: SnapKind;\n nodeId?: string;\n wallId?: string;\n /** Parameter along `wallId`, for `SnapKind.Wall`. */\n t?: number;\n /** Everything an overlay needs to draw to explain the snap. */\n guides: SnapGuide[];\n /** Radians from `origin` to `point`, when an origin was given. */\n angle?: number;\n /** Distance from `origin` to `point`, when an origin was given. */\n distance?: number;\n /**\n * Set when angle lock produced the result, saying what it locked against: the world\n * axes, or the reference direction that was passed in.\n */\n angleRelativeTo?: 'world' | 'reference';\n}\n\n/** An infinite line as a point and a unit direction. */\ninterface Line {\n kind: SnapKind;\n point: PlanPoint;\n dir: PlanPoint;\n residual: number;\n /** Angle lock only: which basis the ray was measured from. */\n relativeTo?: 'world' | 'reference';\n}\n\n/** World units per screen pixel, from the element's current transform. */\nexport function screenScale(svg: SVGSVGElement): number {\n const ctm = svg.getScreenCTM();\n if (!ctm) return 1;\n // Measured rather than read off ctm.a, so a non-uniform or rotated transform still\n // gives a sensible scalar.\n const scale = Math.hypot(ctm.a, ctm.b);\n return scale === 0 ? 1 : 1 / scale;\n}\n\n/**\n * Client (screen) coordinates to world coordinates. Pure conversion, no snapping.\n *\n * Uses `getBoundingClientRect` arithmetic against an explicit viewport rather than\n * `getScreenCTM`, which returns `null` in jsdom and so makes anything built on it\n * untestable.\n */\nexport function clientToWorld(\n clientX: number,\n clientY: number,\n rect: { left: number; top: number },\n view: { x: number; y: number; zoom: number },\n): PlanPoint {\n return {\n x: view.x + (clientX - rect.left) / view.zoom,\n y: view.y + (clientY - rect.top) / view.zoom,\n };\n}\n\nfunction perpendicularDistanceToLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): number {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n return Math.abs(vx * line.dir.y - vy * line.dir.x);\n}\n\nfunction projectOnLine(p: PlanPoint, line: { point: PlanPoint; dir: PlanPoint }): PlanPoint {\n const vx = p.x - line.point.x;\n const vy = p.y - line.point.y;\n const along = vx * line.dir.x + vy * line.dir.y;\n return { x: line.point.x + line.dir.x * along, y: line.point.y + line.dir.y * along };\n}\n\nfunction intersectLines(l1: Line, l2: Line): PlanPoint | null {\n const denom = l1.dir.x * l2.dir.y - l1.dir.y * l2.dir.x;\n if (Math.abs(denom) < 1e-9) return null;\n const dx = l2.point.x - l1.point.x;\n const dy = l2.point.y - l1.point.y;\n const t = (dx * l2.dir.y - dy * l2.dir.x) / denom;\n return { x: l1.point.x + l1.dir.x * t, y: l1.point.y + l1.dir.y * t };\n}\n\nfunction angleBetween(l1: Line, l2: Line): number {\n const dot = Math.abs(l1.dir.x * l2.dir.x + l1.dir.y * l2.dir.y);\n return Math.acos(Math.min(1, dot));\n}\n\nfunction roundTo(value: number, step: number): number {\n return step > 0 ? Math.round(value / step) * step : value;\n}\n\n/**\n * Resolves a raw pointer position to the point the user meant.\n *\n * Two stages, because half of these snaps are constraints rather than points. A hard\n * snap (node, intersection, wall) fixes both degrees of freedom, so the first match\n * wins. A constraint (angle lock, extension, alignment) removes only one, so a flat\n * \"first match wins\" list would kick the point off the angle ray the moment an\n * alignment also matched. Two constraints intersect; one constraint is projected onto\n * and then slid ALONG to the nearest grid position, which is what lets angle lock and\n * grid coexist instead of fighting.\n */\nexport function resolveSnap(raw: PlanPoint, ctx: SnapContext): SnapResult {\n const options: SnapOptions = { ...DEFAULT_SNAP_OPTIONS, ...ctx.options };\n const enabled = new Set(options.enabled);\n\n /**\n * Measures the resolved point back against the origin, so every branch reports the\n * same angle and distance without each one having to compute them. Taking them from\n * the final point rather than from whichever branch produced it means they always\n * describe what the user actually gets.\n */\n const finish = (result: SnapResult): SnapResult => {\n if (!ctx.origin) return result;\n const dx = result.point.x - ctx.origin.x;\n const dy = result.point.y - ctx.origin.y;\n return { ...result, angle: Math.atan2(dy, dx), distance: Math.hypot(dx, dy) };\n };\n\n if (enabled.size === 0) return finish({ point: raw, kind: SnapKind.None, guides: [] });\n\n const tol = options.toleranceScreenPx * ctx.pxToWorld;\n const nodeTol = (options.nodeToleranceScreenPx ?? options.toleranceScreenPx) * ctx.pxToWorld;\n const { floor } = ctx;\n\n /**\n * Angle lock, when the caller has asked for it, is a HARD constraint rather than\n * another proximity snap: `angleStepDeg > 0` only happens because the user latched the\n * lock or held the modifier. Gating it by tolerance like the ambient snaps would mean\n * it silently stopped applying as soon as you dragged more than a few pixels off the\n * ray, which is the opposite of what a lock is for.\n */\n const lockedRay: Line | null = (() => {\n if (!enabled.has(SnapKind.Angle) || !ctx.origin || options.angleStepDeg <= 0) return null;\n const step = (options.angleStepDeg * Math.PI) / 180;\n const rawAngle = Math.atan2(raw.y - ctx.origin.y, raw.x - ctx.origin.x);\n\n // Measure from the world axes, and also from the reference direction when one was\n // given, so \"square to the wall I am continuing\" is available as well as \"square to\n // the page\". Whichever ray the pointer is closest to wins.\n const bases: Array<{ angle: number; relativeTo: 'world' | 'reference' }> = [{ angle: 0, relativeTo: 'world' }];\n if (ctx.referenceAngle !== undefined) {\n bases.push({ angle: ctx.referenceAngle, relativeTo: 'reference' });\n }\n\n let best: Line | null = null;\n for (const base of bases) {\n const snapped = base.angle + Math.round((rawAngle - base.angle) / step) * step;\n const dir = { x: Math.cos(snapped), y: Math.sin(snapped) };\n const line: Line = {\n kind: SnapKind.Angle,\n point: ctx.origin,\n dir,\n residual: 0,\n relativeTo: base.relativeTo,\n };\n line.residual = perpendicularDistanceToLine(raw, line);\n if (!best || line.residual < best.residual) best = line;\n }\n return best;\n })();\n\n /**\n * While the lock is on, a hard snap is only allowed to win if it sits ON the ray.\n * Otherwise \"hold the angle\" would be quietly overridden by any nearby corner, which\n * is exactly the case the lock exists to prevent.\n */\n const allowedByLock = (p: PlanPoint): boolean => !lockedRay || perpendicularDistanceToLine(p, lockedRay) <= tol;\n\n // ── Stage A: hard snaps ──────────────────────────────────────────────────────\n\n if (enabled.has(SnapKind.Node)) {\n let best: { id: string; point: PlanPoint; d: number } | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const d = dist(raw, node);\n if (d <= nodeTol && allowedByLock(node) && (!best || d < best.d)) {\n best = { id: node.id, point: { x: node.x, y: node.y }, d };\n }\n }\n if (best) {\n return finish({ point: best.point, kind: SnapKind.Node, nodeId: best.id, guides: [] });\n }\n }\n\n const walls = Object.values(floor.walls).filter((w) => !ctx.excludeWallIds?.has(w.id));\n\n if (enabled.has(SnapKind.Intersection)) {\n let best: { point: PlanPoint; d: number } | null = null;\n for (let i = 0; i < walls.length; i++) {\n for (let j = i + 1; j < walls.length; j++) {\n const a = floor.nodes[walls[i].a];\n const b = floor.nodes[walls[i].b];\n const c = floor.nodes[walls[j].a];\n const d2 = floor.nodes[walls[j].b];\n if (!a || !b || !c || !d2) continue;\n const hit = segmentIntersection(a, b, c, d2);\n if (!hit.point) continue;\n const d = dist(raw, hit.point);\n if (d <= tol && allowedByLock(hit.point) && (!best || d < best.d)) {\n best = { point: hit.point, d };\n }\n }\n }\n if (best) return finish({ point: best.point, kind: SnapKind.Intersection, guides: [] });\n }\n\n if (enabled.has(SnapKind.Wall)) {\n let best: { wallId: string; point: PlanPoint; t: number; d: number } | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const r = pointToSegment(raw, a, b);\n if (r.dist <= tol && allowedByLock(r.point) && (!best || r.dist < best.d)) {\n // Project exactly: intent is allowed to be approximate, the model is not.\n best = { wallId: wall.id, point: projectOnSegment(raw, a, b), t: r.t, d: r.dist };\n }\n }\n if (best) {\n return finish({\n point: best.point,\n kind: SnapKind.Wall,\n wallId: best.wallId,\n t: best.t,\n guides: [],\n });\n }\n }\n\n // ── Stage B: constraints ─────────────────────────────────────────────────────\n\n const constraints: Line[] = [];\n\n if (enabled.has(SnapKind.Extension)) {\n let best: Line | null = null;\n for (const wall of walls) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = dist(a, b);\n if (len < 1e-6) continue;\n const dir = { x: (b.x - a.x) / len, y: (b.y - a.y) / len };\n const line: Line = { kind: SnapKind.Extension, point: { x: a.x, y: a.y }, dir, residual: 0 };\n line.residual = perpendicularDistanceToLine(raw, line);\n // Only beyond the segment itself; on the segment, the wall snap already applies.\n const onSegment = pointToSegment(raw, a, b).dist <= tol;\n if (line.residual <= tol && !onSegment && (!best || line.residual < best.residual)) {\n best = line;\n }\n }\n if (best) constraints.push(best);\n }\n\n if (enabled.has(SnapKind.Alignment)) {\n let bestV: Line | null = null;\n let bestH: Line | null = null;\n for (const node of Object.values(floor.nodes)) {\n if (ctx.excludeNodeIds?.has(node.id)) continue;\n const dx = Math.abs(raw.x - node.x);\n if (dx <= tol && (!bestV || dx < bestV.residual)) {\n bestV = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 0, y: 1 },\n residual: dx,\n };\n }\n const dy = Math.abs(raw.y - node.y);\n if (dy <= tol && (!bestH || dy < bestH.residual)) {\n bestH = {\n kind: SnapKind.Alignment,\n point: { x: node.x, y: node.y },\n dir: { x: 1, y: 0 },\n residual: dy,\n };\n }\n }\n if (bestV) constraints.push(bestV);\n if (bestH) constraints.push(bestH);\n }\n\n const guideFor = (line: Line, through: PlanPoint): SnapGuide => ({\n kind: line.kind,\n a: { x: through.x - line.dir.x * 1e5, y: through.y - line.dir.y * 1e5 },\n b: { x: through.x + line.dir.x * 1e5, y: through.y + line.dir.y * 1e5 },\n infinite: true,\n });\n\n if (lockedRay) {\n // The ray is fixed; the only freedom left is where along it the point sits. Another\n // constraint that genuinely crosses it pins that down, which is the useful\n // \"45 degrees AND level with that corner\" case.\n const crossing = constraints\n .filter((c) => c.residual <= tol && angleBetween(lockedRay, c) > (15 * Math.PI) / 180)\n .sort((a, b) => a.residual - b.residual)[0];\n\n if (crossing) {\n const point = intersectLines(lockedRay, crossing);\n if (point && dist(point, raw) <= tol * 4) {\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point), guideFor(crossing, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n }\n\n let point = projectOnLine(raw, lockedRay);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the ray to a round length, rather than rounding both coordinates,\n // which would take the point straight back off it.\n const along = (point.x - lockedRay.point.x) * lockedRay.dir.x + (point.y - lockedRay.point.y) * lockedRay.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: lockedRay.point.x + lockedRay.dir.x * snappedAlong,\n y: lockedRay.point.y + lockedRay.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: SnapKind.Angle,\n guides: [guideFor(lockedRay, point)],\n angleRelativeTo: lockedRay.relativeTo,\n });\n }\n\n if (constraints.length >= 2) {\n const sorted = [...constraints].sort((a, b) => a.residual - b.residual);\n const [l1, l2] = sorted;\n const point = intersectLines(l1, l2);\n // Two nearly parallel constraints intersect somewhere useless and far away.\n if (point && angleBetween(l1, l2) > (15 * Math.PI) / 180 && dist(point, raw) <= tol * 2) {\n return finish({\n point,\n kind: SnapKind.Intersection,\n guides: [guideFor(l1, point), guideFor(l2, point)],\n });\n }\n }\n\n if (constraints.length >= 1) {\n const line = [...constraints].sort((a, b) => a.residual - b.residual)[0];\n let point = projectOnLine(raw, line);\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n // Slide along the constraint to a round distance, rather than rounding both\n // coordinates, which would take the point back off the line.\n const along = (point.x - line.point.x) * line.dir.x + (point.y - line.point.y) * line.dir.y;\n const snappedAlong = roundTo(along, options.gridSize);\n point = {\n x: line.point.x + line.dir.x * snappedAlong,\n y: line.point.y + line.dir.y * snappedAlong,\n };\n }\n return finish({\n point,\n kind: line.kind,\n guides: [guideFor(line, point)],\n angleRelativeTo: line.relativeTo,\n });\n }\n\n if (enabled.has(SnapKind.Grid) && options.gridSize > 0) {\n return finish({\n point: { x: roundTo(raw.x, options.gridSize), y: roundTo(raw.y, options.gridSize) },\n kind: SnapKind.Grid,\n guides: [],\n });\n }\n\n return finish({ point: raw, kind: SnapKind.None, guides: [] });\n}\n"],"mappings":"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"}
|
|
@@ -1,2 +1,2 @@
|
|
|
1
|
-
import{signedArea as e,stripSpurs as t}from"./floorPlan.geometry.mjs";var n=1e-9;function r(e,t,n){return(t.x-e.x)*(n.y-e.y)-(t.y-e.y)*(n.x-e.x)}function i(e,t){return Math.abs(e.x-t.x)<n&&Math.abs(e.y-t.y)<n}function a(e,t,i,a){let o=r(e,t,i),s=r(e,i,a),c=r(e,a,t);return!((o<-1e-9||s<-1e-9||c<-1e-9)&&(o>n||s>n||c>n))}function o(e,t){let n=e.slice(),r=t.filter(e=>e.length>=3).map(e=>({ring:e,index:e.reduce((t,n,r)=>n.x>e[t].x?r:t,0)})).sort((e,t)=>t.ring[t.index].x-e.ring[e.index].x);for(let e of r){let t=e.ring[e.index],r=-1,i=1/0;for(let a=0;a<n.length;a++){if(n[a].x<t.x)continue;let o=(n[a].x-t.x)**2+(n[a].y-t.y)**2;o<i&&s(t,n[a],n,e.ring)&&(i=o,r=a)}if(r<0)for(let e=0;e<n.length;e++){let a=(n[e].x-t.x)**2+(n[e].y-t.y)**2;a<i&&(i=a,r=e)}let a=[];for(let t=0;t<=e.ring.length;t++)a.push(e.ring[(e.index+t)%e.ring.length]);n=[...n.slice(0,r+1),...a,...n.slice(r)]}return n}function s(e,t,n,i){for(let a of[n,i])for(let n=0;n<a.length;n++){let i=a[n],o=a[(n+1)%a.length];if(i===e||o===e||i===t||o===t)continue;let s=r(e,t,i),c=r(e,t,o),l=r(i,o,e),u=r(i,o,t);if(s*c<0&&l*u<0)return!1}return!0}function c(s,c=[]){let l=t(s);if(l.length<3)return{vertices:[],indices:[]};let u=o(l,c.map(e=>t(e)).filter(e=>e.length>=3)),d=e(u)<0?u.slice().reverse():u.slice(),f=d.map((e,t)=>t),p=[],m=f.length*f.length;for(;f.length>3&&m
|
|
1
|
+
import{signedArea as e,stripSpurs as t}from"./floorPlan.geometry.mjs";var n=1e-9;function r(e,t,n){return(t.x-e.x)*(n.y-e.y)-(t.y-e.y)*(n.x-e.x)}function i(e,t){return Math.abs(e.x-t.x)<n&&Math.abs(e.y-t.y)<n}function a(e,t,i,a){let o=r(e,t,i),s=r(e,i,a),c=r(e,a,t);return!((o<-1e-9||s<-1e-9||c<-1e-9)&&(o>n||s>n||c>n))}function o(e,t){let n=e.slice(),r=t.filter(e=>e.length>=3).map(e=>({ring:e,index:e.reduce((t,n,r)=>n.x>e[t].x?r:t,0)})).sort((e,t)=>t.ring[t.index].x-e.ring[e.index].x);for(let e of r){let t=e.ring[e.index],r=-1,i=1/0;for(let a=0;a<n.length;a++){if(n[a].x<t.x)continue;let o=(n[a].x-t.x)**2+(n[a].y-t.y)**2;o<i&&s(t,n[a],n,e.ring)&&(i=o,r=a)}if(r<0)for(let e=0;e<n.length;e++){let a=(n[e].x-t.x)**2+(n[e].y-t.y)**2;a<i&&(i=a,r=e)}let a=[];for(let t=0;t<=e.ring.length;t++)a.push(e.ring[(e.index+t)%e.ring.length]);n=[...n.slice(0,r+1),...a,...n.slice(r)]}return n}function s(e,t,n,i){for(let a of[n,i])for(let n=0;n<a.length;n++){let i=a[n],o=a[(n+1)%a.length];if(i===e||o===e||i===t||o===t)continue;let s=r(e,t,i),c=r(e,t,o),l=r(i,o,e),u=r(i,o,t);if(s*c<0&&l*u<0)return!1}return!0}function c(s,c=[]){let l=t(s);if(l.length<3)return{vertices:[],indices:[]};let u=o(l,c.map(e=>t(e)).filter(e=>e.length>=3)),d=e(u)<0?u.slice().reverse():u.slice(),f=d.map((e,t)=>t),p=[],m=f.length*f.length;for(;f.length>3&&m-->0;){let e=!1;for(let t=0;t<f.length;t++){let o=f[(t+f.length-1)%f.length],s=f[t],c=f[(t+1)%f.length],l=d[o],u=d[s],m=d[c];if(r(l,u,m)<=n)continue;let h=!1;for(let e of f){if(e===o||e===s||e===c)continue;let t=d[e];if(!(i(t,l)||i(t,u)||i(t,m))&&a(t,l,u,m)){h=!0;break}}if(!h){p.push(o,s,c),f.splice(t,1),e=!0;break}}if(!e)break}return f.length===3&&p.push(f[0],f[1],f[2]),{vertices:d,indices:p}}export{c as triangulate};
|
|
2
2
|
//# sourceMappingURL=floorPlan.triangulate.mjs.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"floorPlan.triangulate.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.triangulate.ts"],"sourcesContent":["/**\n * Ear clipping for simple polygons, with holes bridged into the outer ring first.\n *\n * Written out rather than pulled in: a floor slab is the only thing in this package that\n * needs a triangulator, and a dependency is a poor trade for one file.\n *\n * The rooms this runs on come from face extraction, so they are already closed, already\n * consistently wound, and free of self-intersections. That is what makes plain ear\n * clipping enough: it is O(n²) and it cannot cope with a self-intersecting ring, and\n * neither limit matters for a room with a few dozen corners.\n */\n\nimport { signedArea, stripSpurs } from './floorPlan.geometry';\nimport type { PlanPoint } from './floorPlan.types';\n\nconst EPS = 1e-9;\n\nfunction area2(a: PlanPoint, b: PlanPoint, c: PlanPoint): number {\n return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);\n}\n\nfunction samePoint(a: PlanPoint, b: PlanPoint): boolean {\n return Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS;\n}\n\n/** Inclusive of the edges: a vertex lying on one would still leave a sliver behind. */\nfunction inTriangle(p: PlanPoint, a: PlanPoint, b: PlanPoint, c: PlanPoint): boolean {\n const d1 = area2(p, a, b);\n const d2 = area2(p, b, c);\n const d3 = area2(p, c, a);\n const neg = d1 < -EPS || d2 < -EPS || d3 < -EPS;\n const pos = d1 > EPS || d2 > EPS || d3 > EPS;\n return !(neg && pos);\n}\n\n/**\n * Joins each hole to the outer ring with a pair of coincident edges, leaving one ring\n * that ear clipping can take. The bridge runs from the hole's rightmost vertex to a\n * visible vertex of the outer ring, which is the standard construction: going rightwards\n * from the extreme point of a hole must leave it, so the first outer edge hit is visible\n * from it.\n */\nfunction bridgeHoles(outer: PlanPoint[], holes: PlanPoint[][]): PlanPoint[] {\n let ring = outer.slice();\n\n // Rightmost hole first: bridging it cannot cross a hole further right.\n const queue = holes\n .filter((h) => h.length >= 3)\n .map((h) => ({ ring: h, index: h.reduce((best, p, i) => (p.x > h[best].x ? i : best), 0) }))\n .sort((a, b) => b.ring[b.index].x - a.ring[a.index].x);\n\n for (const hole of queue) {\n const from = hole.ring[hole.index];\n\n // The outer vertex to bridge to: the one that is visible and, among those, closest.\n let best = -1;\n let bestDist = Infinity;\n for (let i = 0; i < ring.length; i++) {\n if (ring[i].x < from.x) continue; // must lie to the right, or the bridge doubles back\n const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n if (d < bestDist && visible(from, ring[i], ring, hole.ring)) {\n bestDist = d;\n best = i;\n }\n }\n // Nothing to the right was visible, so fall back to the nearest vertex anywhere. The\n // ring stays closed either way; at worst the slab gains a seam nobody can see.\n if (best < 0) {\n for (let i = 0; i < ring.length; i++) {\n const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n if (d < bestDist) {\n bestDist = d;\n best = i;\n }\n }\n }\n\n // Walk the hole from its bridge vertex, then come back along the bridge.\n const woven: PlanPoint[] = [];\n for (let k = 0; k <= hole.ring.length; k++) {\n woven.push(hole.ring[(hole.index + k) % hole.ring.length]);\n }\n ring = [...ring.slice(0, best + 1), ...woven, ...ring.slice(best)];\n }\n\n return ring;\n}\n\n/** Whether the segment from `a` to `b` crosses any edge of the rings it has to avoid. */\nfunction visible(a: PlanPoint, b: PlanPoint, ring: PlanPoint[], hole: PlanPoint[]): boolean {\n for (const poly of [ring, hole]) {\n for (let i = 0; i < poly.length; i++) {\n const p = poly[i];\n const q = poly[(i + 1) % poly.length];\n if (p === a || q === a || p === b || q === b) continue;\n const d1 = area2(a, b, p);\n const d2 = area2(a, b, q);\n const d3 = area2(p, q, a);\n const d4 = area2(p, q, b);\n if (d1 * d2 < 0 && d3 * d4 < 0) return false;\n }\n }\n return true;\n}\n\n/**\n * Triangulates a ring and its holes into index triples over the returned vertex list.\n *\n * The vertices come back as well as the indices because bridging a hole duplicates the\n * two vertices it joins, so the caller cannot simply index into what it passed in.\n */\nexport function triangulate(\n outer: readonly PlanPoint[],\n holes: readonly (readonly PlanPoint[])[] = [],\n): { vertices: PlanPoint[]; indices: number[] } {\n // A dangling wall inside a room puts the same vertex in the ring twice, which is not a\n // simple polygon and would stall ear clipping on a zero-area ear.\n const cleanOuter = stripSpurs(outer);\n if (cleanOuter.length < 3) return { vertices: [], indices: [] };\n\n const ring = bridgeHoles(\n cleanOuter,\n holes.map((h) => stripSpurs(h)).filter((h) => h.length >= 3),\n );\n\n // Ear clipping wants a counter-clockwise ring under the usual maths convention.\n const vertices = signedArea(ring) < 0 ? ring.slice().reverse() : ring.slice();\n\n const live = vertices.map((_, i) => i);\n const indices: number[] = [];\n let guard = live.length * live.length;\n\n while (live.length > 3 && guard-- > 0) {\n let clipped = false;\n for (let i = 0; i < live.length; i++) {\n const ai = live[(i + live.length - 1) % live.length];\n const bi = live[i];\n const ci = live[(i + 1) % live.length];\n const a = vertices[ai];\n const b = vertices[bi];\n const c = vertices[ci];\n if (area2(a, b, c) <= EPS) continue; // reflex or degenerate: not an ear\n\n let blocked = false;\n for (const vi of live) {\n if (vi === ai || vi === bi || vi === ci) continue;\n const v = vertices[vi];\n // Bridging a hole duplicates the two vertices it joins, so a candidate can sit\n // exactly on a corner of the ear. Counting that as \"inside\" blocks every ear in\n // the ring and the triangulation comes back empty.\n if (samePoint(v, a) || samePoint(v, b) || samePoint(v, c)) continue;\n if (inTriangle(v, a, b, c)) {\n blocked = true;\n break;\n }\n }\n if (blocked) continue;\n\n indices.push(ai, bi, ci);\n live.splice(i, 1);\n clipped = true;\n break;\n }\n // No ear found: the ring is degenerate in a way this cannot reduce further. Stopping\n // beats spinning, and the caller gets whatever was triangulated so far.\n if (!clipped) break;\n }\n if (live.length === 3) indices.push(live[0], live[1], live[2]);\n\n return { vertices, indices };\n}\n"],"mappings":"sEAeA,IAAM,EAAM,KAEZ,SAAS,EAAM,EAAc,EAAc,EAAsB,CAC/D,OAAQ,EAAE,EAAI,EAAE,IAAM,EAAE,EAAI,EAAE,IAAM,EAAE,EAAI,EAAE,IAAM,EAAE,EAAI,EAAE,EAC5D,CAEA,SAAS,EAAU,EAAc,EAAuB,CACtD,OAAO,KAAK,IAAI,EAAE,EAAI,EAAE,CAAC,EAAI,GAAO,KAAK,IAAI,EAAE,EAAI,EAAE,CAAC,EAAI,CAC5D,CAGA,SAAS,EAAW,EAAc,EAAc,EAAc,EAAuB,CACnF,IAAM,EAAK,EAAM,EAAG,EAAG,CAAC,EAClB,EAAK,EAAM,EAAG,EAAG,CAAC,EAClB,EAAK,EAAM,EAAG,EAAG,CAAC,EAGxB,MAAO,GAFK,EAAK,OAAQ,EAAK,OAAQ,EAAK,SAC/B,EAAK,GAAO,EAAK,GAAO,EAAK,GAE3C,CASA,SAAS,EAAY,EAAoB,EAAmC,CAC1E,IAAI,EAAO,EAAM,MAAM,EAGjB,EAAQ,EACX,OAAQ,GAAM,EAAE,QAAU,CAAC,
|
|
1
|
+
{"version":3,"file":"floorPlan.triangulate.mjs","names":[],"sources":["../../../../package/common/floorPlan/floorPlan.triangulate.ts"],"sourcesContent":["/**\n * Ear clipping for simple polygons, with holes bridged into the outer ring first.\n *\n * Written out rather than pulled in: a floor slab is the only thing in this package that\n * needs a triangulator, and a dependency is a poor trade for one file.\n *\n * The rooms this runs on come from face extraction, so they are already closed, already\n * consistently wound, and free of self-intersections. That is what makes plain ear\n * clipping enough: it is O(n²) and it cannot cope with a self-intersecting ring, and\n * neither limit matters for a room with a few dozen corners.\n */\n\nimport { signedArea, stripSpurs } from './floorPlan.geometry';\nimport type { PlanPoint } from './floorPlan.types';\n\nconst EPS = 1e-9;\n\nfunction area2(a: PlanPoint, b: PlanPoint, c: PlanPoint): number {\n return (b.x - a.x) * (c.y - a.y) - (b.y - a.y) * (c.x - a.x);\n}\n\nfunction samePoint(a: PlanPoint, b: PlanPoint): boolean {\n return Math.abs(a.x - b.x) < EPS && Math.abs(a.y - b.y) < EPS;\n}\n\n/** Inclusive of the edges: a vertex lying on one would still leave a sliver behind. */\nfunction inTriangle(p: PlanPoint, a: PlanPoint, b: PlanPoint, c: PlanPoint): boolean {\n const d1 = area2(p, a, b);\n const d2 = area2(p, b, c);\n const d3 = area2(p, c, a);\n const neg = d1 < -EPS || d2 < -EPS || d3 < -EPS;\n const pos = d1 > EPS || d2 > EPS || d3 > EPS;\n return !(neg && pos);\n}\n\n/**\n * Joins each hole to the outer ring with a pair of coincident edges, leaving one ring\n * that ear clipping can take. The bridge runs from the hole's rightmost vertex to a\n * visible vertex of the outer ring, which is the standard construction: going rightwards\n * from the extreme point of a hole must leave it, so the first outer edge hit is visible\n * from it.\n */\nfunction bridgeHoles(outer: PlanPoint[], holes: PlanPoint[][]): PlanPoint[] {\n let ring = outer.slice();\n\n // Rightmost hole first: bridging it cannot cross a hole further right.\n const queue = holes\n .filter((h) => h.length >= 3)\n .map((h) => ({ ring: h, index: h.reduce((best, p, i) => (p.x > h[best].x ? i : best), 0) }))\n .sort((a, b) => b.ring[b.index].x - a.ring[a.index].x);\n\n for (const hole of queue) {\n const from = hole.ring[hole.index];\n\n // The outer vertex to bridge to: the one that is visible and, among those, closest.\n let best = -1;\n let bestDist = Infinity;\n for (let i = 0; i < ring.length; i++) {\n if (ring[i].x < from.x) continue; // must lie to the right, or the bridge doubles back\n const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n if (d < bestDist && visible(from, ring[i], ring, hole.ring)) {\n bestDist = d;\n best = i;\n }\n }\n // Nothing to the right was visible, so fall back to the nearest vertex anywhere. The\n // ring stays closed either way; at worst the slab gains a seam nobody can see.\n if (best < 0) {\n for (let i = 0; i < ring.length; i++) {\n const d = (ring[i].x - from.x) ** 2 + (ring[i].y - from.y) ** 2;\n if (d < bestDist) {\n bestDist = d;\n best = i;\n }\n }\n }\n\n // Walk the hole from its bridge vertex, then come back along the bridge.\n const woven: PlanPoint[] = [];\n for (let k = 0; k <= hole.ring.length; k++) {\n woven.push(hole.ring[(hole.index + k) % hole.ring.length]);\n }\n ring = [...ring.slice(0, best + 1), ...woven, ...ring.slice(best)];\n }\n\n return ring;\n}\n\n/** Whether the segment from `a` to `b` crosses any edge of the rings it has to avoid. */\nfunction visible(a: PlanPoint, b: PlanPoint, ring: PlanPoint[], hole: PlanPoint[]): boolean {\n for (const poly of [ring, hole]) {\n for (let i = 0; i < poly.length; i++) {\n const p = poly[i];\n const q = poly[(i + 1) % poly.length];\n if (p === a || q === a || p === b || q === b) continue;\n const d1 = area2(a, b, p);\n const d2 = area2(a, b, q);\n const d3 = area2(p, q, a);\n const d4 = area2(p, q, b);\n if (d1 * d2 < 0 && d3 * d4 < 0) return false;\n }\n }\n return true;\n}\n\n/**\n * Triangulates a ring and its holes into index triples over the returned vertex list.\n *\n * The vertices come back as well as the indices because bridging a hole duplicates the\n * two vertices it joins, so the caller cannot simply index into what it passed in.\n */\nexport function triangulate(\n outer: readonly PlanPoint[],\n holes: readonly (readonly PlanPoint[])[] = [],\n): { vertices: PlanPoint[]; indices: number[] } {\n // A dangling wall inside a room puts the same vertex in the ring twice, which is not a\n // simple polygon and would stall ear clipping on a zero-area ear.\n const cleanOuter = stripSpurs(outer);\n if (cleanOuter.length < 3) return { vertices: [], indices: [] };\n\n const ring = bridgeHoles(\n cleanOuter,\n holes.map((h) => stripSpurs(h)).filter((h) => h.length >= 3),\n );\n\n // Ear clipping wants a counter-clockwise ring under the usual maths convention.\n const vertices = signedArea(ring) < 0 ? ring.slice().reverse() : ring.slice();\n\n const live = vertices.map((_, i) => i);\n const indices: number[] = [];\n let guard = live.length * live.length;\n\n while (live.length > 3 && guard-- > 0) {\n let clipped = false;\n for (let i = 0; i < live.length; i++) {\n const ai = live[(i + live.length - 1) % live.length];\n const bi = live[i];\n const ci = live[(i + 1) % live.length];\n const a = vertices[ai];\n const b = vertices[bi];\n const c = vertices[ci];\n if (area2(a, b, c) <= EPS) continue; // reflex or degenerate: not an ear\n\n let blocked = false;\n for (const vi of live) {\n if (vi === ai || vi === bi || vi === ci) continue;\n const v = vertices[vi];\n // Bridging a hole duplicates the two vertices it joins, so a candidate can sit\n // exactly on a corner of the ear. Counting that as \"inside\" blocks every ear in\n // the ring and the triangulation comes back empty.\n if (samePoint(v, a) || samePoint(v, b) || samePoint(v, c)) continue;\n if (inTriangle(v, a, b, c)) {\n blocked = true;\n break;\n }\n }\n if (blocked) continue;\n\n indices.push(ai, bi, ci);\n live.splice(i, 1);\n clipped = true;\n break;\n }\n // No ear found: the ring is degenerate in a way this cannot reduce further. Stopping\n // beats spinning, and the caller gets whatever was triangulated so far.\n if (!clipped) break;\n }\n if (live.length === 3) indices.push(live[0], live[1], live[2]);\n\n return { vertices, indices };\n}\n"],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"legacySvg.mjs","names":[],"sources":["../../../../package/common/floorPlan/legacySvg.ts"],"sourcesContent":["/**\n * Imports floor plans exported by the previous `RoomDrawer` into the current model.\n *\n * The old format stored each room as its own closed polygon, so a wall between two\n * rooms appeared twice, once per room. Feeding every polygon edge through `addWall` is\n * what converts that: coincident edges resolve to the same nodes and the second one is\n * recognised as already walled, so the pair collapses into the single shared wall the\n * new model needs.\n *\n * Import only. Nothing writes this format any more. See LEGACY_SVG_FORMAT.md in this\n * folder for the specification of what is accepted.\n */\n\nimport { DEFAULT_WALL_ATTRS, addOpening, addWall, scalePlan } from './floorPlan.graph';\nimport { commitDerivation, derivePlan } from './floorPlan.rooms';\nimport { emptyFloorPlan } from './floorPlan.plan';\nimport { OpeningKind, WallType } from './floorPlan.types';\nimport type { FloorPlan, PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport interface LegacyImportOptions {\n /**\n * Multiplies every imported coordinate, converting the old unitless canvas into the\n * module's millimetre invariant.\n *\n * There is no correct automatic value: the old format records no scale at all. The\n * default of 1 keeps the numbers as they were, which makes a typical 400x300 export\n * a 40cm plan. A 400-unit-wide export meant to be a 12m house wants `scale: 30`.\n */\n scale?: number;\n /** Wall thickness for imported walls, in millimetres after scaling. */\n thickness?: number;\n /** Tolerance for welding coincident endpoints, in millimetres after scaling. */\n weldTolerance?: number;\n label?: string;\n}\n\ninterface LegacyRoom {\n name: string;\n points: PlanPoint[];\n}\n\ninterface LegacyDoor {\n p1: PlanPoint;\n p2: PlanPoint;\n}\n\n/** Reads the absolute M/L commands the old exporter wrote. */\nfunction parseClosedPath(d: string): PlanPoint[] {\n const points: PlanPoint[] = [];\n for (const m of d.matchAll(/[ML]\\s*(-?[\\d.]+)[,\\s]+(-?[\\d.]+)/gi)) {\n points.push({ x: parseFloat(m[1]), y: parseFloat(m[2]) });\n }\n return points;\n}\n\n/**\n * Recovers a door from the old swing-arc path: `M p1 L p2 M p1 A w,w 0 0,1 end`.\n *\n * Position and width follow from p1 and p2. Anything that does not match this shape is\n * skipped and the wall is left solid, rather than guessed at.\n */\nfunction parseDoorPath(d: string): LegacyDoor | null {\n const match = d.match(\n /^\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*L\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*A\\s/i,\n );\n if (!match) return null;\n return {\n p1: { x: parseFloat(match[1]), y: parseFloat(match[2]) },\n p2: { x: parseFloat(match[3]), y: parseFloat(match[4]) },\n };\n}\n\nfunction parseLegacyDocument(svg: string): { rooms: LegacyRoom[]; doors: LegacyDoor[] } {\n const doc = new DOMParser().parseFromString(svg, 'image/svg+xml');\n const rooms: LegacyRoom[] = [];\n const doors: LegacyDoor[] = [];\n\n doc.querySelectorAll('[id^=\"room\"]').forEach((group) => {\n const background = group.querySelector('[id=\"background\"]');\n const d = background?.getAttribute('d');\n if (!d) return;\n const points = parseClosedPath(d);\n if (points.length < 3) return;\n rooms.push({ name: group.querySelector('text')?.textContent?.trim() ?? '', points });\n\n group.querySelectorAll('path').forEach((path) => {\n if (path === background) return;\n const door = parseDoorPath(path.getAttribute('d') ?? '');\n if (door) doors.push(door);\n });\n });\n\n return { rooms, doors };\n}\n\n/**\n * Parses a legacy exported SVG into a single-floor `FloorPlan`.\n *\n * Rooms keep their names. Doors are recovered where the old swing-arc path is\n * recognisable, as `OpeningKind.Door`. Windows and archways did not exist in that\n * format, so nothing is lost by not looking for them.\n */\nexport function planFromLegacySvg(svg: string, options: LegacyImportOptions = {}): FloorPlan {\n const { scale = 1, thickness = DEFAULT_WALL_ATTRS.thickness, weldTolerance, label } = options;\n const { rooms, doors } = parseLegacyDocument(svg);\n\n const plan = emptyFloorPlan({ floorLabel: label });\n let floor: PlanFloor = plan.floors[0];\n // Generous relative to the old canvas, where coordinates were snapped to a grid of 5.\n const tol = weldTolerance ?? Math.max(1, scale * 2);\n\n for (const room of rooms) {\n const points = room.points.map((p) => ({ x: p.x * scale, y: p.y * scale }));\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n // The welding step: an edge an adjacent room already drew resolves to the same\n // two nodes, and addWall declines to duplicate it.\n floor = addWall(floor, a, b, { thickness, type: WallType.Interior }, tol).floor;\n }\n }\n\n for (const door of doors) {\n const p1 = { x: door.p1.x * scale, y: door.p1.y * scale };\n const p2 = { x: door.p2.x * scale, y: door.p2.y * scale };\n const centre = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };\n const width = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n if (width <= 0) continue;\n\n // Find the wall the door sits on now that edges have been welded.\n let best: { wallId: string; offset: number; dist: number } | null = null;\n for (const wall of Object.values(floor.walls)) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = Math.hypot(b.x - a.x, b.y - a.y);\n if (len < 1e-6) continue;\n const t = ((centre.x - a.x) * (b.x - a.x) + (centre.y - a.y) * (b.y - a.y)) / (len * len);\n const clamped = Math.min(1, Math.max(0, t));\n const px = a.x + (b.x - a.x) * clamped;\n const py = a.y + (b.y - a.y) * clamped;\n const d = Math.hypot(centre.x - px, centre.y - py);\n if (d <= tol && (!best || d < best.dist)) {\n best = { wallId: wall.id, offset: clamped * len, dist: d };\n }\n }\n if (!best) continue;\n\n floor = addOpening(floor, {\n wallId: best.wallId,\n kind: OpeningKind.Door,\n offset: best.offset,\n width,\n sill: 0,\n head: 2100,\n }).floor;\n }\n\n // Name the derived faces from the polygons they came from, matching each face to the\n // legacy room whose own centroid falls inside it.\n const committed = commitDerivation(floor);\n floor = committed.floor;\n\n const derived = derivePlan(floor).rooms;\n for (const legacy of rooms) {\n if (!legacy.name) continue;\n const centroid = {\n x: (legacy.points.reduce((s, p) => s + p.x, 0) / legacy.points.length) * scale,\n y: (legacy.points.reduce((s, p) => s + p.y, 0) / legacy.points.length) * scale,\n };\n const match = derived.find(\n (face) =>\n face.roomId &&\n face.outer.length > 2 &&\n pointInsideRing(centroid, face.outer) &&\n floor.rooms[face.roomId]?.name.startsWith('Room '),\n );\n if (match?.roomId) {\n floor = {\n ...floor,\n rooms: { ...floor.rooms, [match.roomId]: { ...floor.rooms[match.roomId], name: legacy.name } },\n };\n }\n }\n\n return { ...plan, floors: [floor] };\n}\n\nfunction pointInsideRing(p: PlanPoint, ring: PlanPoint[]): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {\n const xi = ring[i].x;\n const yi = ring[i].y;\n const xj = ring[j].x;\n const yj = ring[j].y;\n if (yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi) inside = !inside;\n }\n return inside;\n}\n\nexport { scalePlan };\n"],"mappings":"yRA+CA,SAAS,EAAgB,EAAwB,CAC/C,IAAM,EAAsB,CAAC,EAC7B,IAAK,IAAM,KAAK,EAAE,SAAS,qCAAqC,EAC9D,EAAO,KAAK,CAAE,EAAG,WAAW,EAAE,EAAE,EAAG,EAAG,WAAW,EAAE,EAAE,CAAE,CAAC,EAE1D,OAAO,CACT,CAQA,SAAS,EAAc,EAA8B,CACnD,IAAM,EAAQ,EAAE,MACd,8FACF,EAEA,OADK,EACE,CACL,GAAI,CAAE,EAAG,WAAW,EAAM,EAAE,EAAG,EAAG,WAAW,EAAM,EAAE,CAAE,EACvD,GAAI,CAAE,EAAG,WAAW,EAAM,EAAE,EAAG,EAAG,WAAW,EAAM,EAAE,CAAE,CACzD,EAJmB,IAKrB,CAEA,SAAS,EAAoB,EAA2D,CACtF,IAAM,EAAM,IAAI,UAAU,EAAE,gBAAgB,EAAK,eAAe,EAC1D,EAAsB,CAAC,EACvB,EAAsB,CAAC,EAiB7B,OAfA,EAAI,iBAAiB,cAAc,EAAE,QAAS,GAAU,CACtD,IAAM,EAAa,EAAM,cAAc,mBAAmB,EACpD,EAAI,GAAY,aAAa,GAAG,EACtC,GAAI,CAAC,EAAG,OACR,IAAM,EAAS,EAAgB,CAAC,EAC5B,EAAO,OAAS,IACpB,EAAM,KAAK,CAAE,KAAM,EAAM,cAAc,MAAM,GAAG,aAAa,KAAK,GAAK,GAAI,QAAO,CAAC,EAEnF,EAAM,iBAAiB,MAAM,EAAE,QAAS,GAAS,CAC/C,GAAI,IAAS,EAAY,OACzB,IAAM,EAAO,EAAc,EAAK,aAAa,GAAG,GAAK,EAAE,EACnD,GAAM,EAAM,KAAK,CAAI,CAC3B,CAAC,EACH,CAAC,EAEM,CAAE,QAAO,OAAM,CACxB,CASA,SAAgB,EAAkB,EAAa,EAA+B,CAAC,EAAc,CAC3F,GAAM,CAAE,QAAQ,EAAG,YAAY,EAAmB,UAAW,gBAAe,SAAU,EAChF,CAAE,QAAO,SAAU,EAAoB,CAAG,EAE1C,EAAO,EAAe,CAAE,WAAY,CAAM,CAAC,EAC7C,EAAmB,EAAK,OAAO,GAE7B,EAAM,GAAiB,KAAK,IAAI,EAAG,EAAQ,CAAC,EAElD,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,EAAS,EAAK,OAAO,IAAK,IAAO,CAAE,EAAG,EAAE,EAAI,EAAO,EAAG,EAAE,EAAI,CAAM,EAAE,EAC1E,IAAK,IAAI,EAAI,EAAG,EAAI,EAAO,OAAQ,IAAK,CACtC,IAAM,EAAI,EAAO,GACX,EAAI,GAAQ,EAAI,GAAK,EAAO,QAGlC,EAAQ,EAAQ,EAAO,EAAG,EAAG,CAAE,YAAW,KAAM,EAAS,QAAS,EAAG,CAAG,EAAE,KAC5E,CACF,CAEA,IAAK,IAAM,KAAQ,EAAO,CACxB,IAAM,EAAK,CAAE,EAAG,EAAK,GAAG,EAAI,EAAO,EAAG,EAAK,GAAG,EAAI,CAAM,EAClD,EAAK,CAAE,EAAG,EAAK,GAAG,EAAI,EAAO,EAAG,EAAK,GAAG,EAAI,CAAM,EAClD,EAAS,CAAE,GAAI,EAAG,EAAI,EAAG,GAAK,EAAG,GAAI,EAAG,EAAI,EAAG,GAAK,CAAE,EACtD,EAAQ,KAAK,MAAM,EAAG,EAAI,EAAG,EAAG,EAAG,EAAI,EAAG,CAAC,EACjD,GAAI,GAAS,EAAG,SAGhB,IAAI,EAAgE,KACpE,IAAK,IAAM,KAAQ,OAAO,OAAO,EAAM,KAAK,EAAG,CAC7C,IAAM,EAAI,EAAM,MAAM,EAAK,GACrB,EAAI,EAAM,MAAM,EAAK,GAC3B,GAAI,CAAC,GAAK,CAAC,EAAG,SACd,IAAM,EAAM,KAAK,MAAM,EAAE,EAAI,EAAE,EAAG,EAAE,EAAI,EAAE,CAAC,EAC3C,GAAI,EAAM,KAAM,SAChB,IAAM,IAAM,EAAO,EAAI,EAAE,IAAM,EAAE,EAAI,EAAE,IAAM,EAAO,EAAI,EAAE,IAAM,EAAE,EAAI,EAAE,KAAO,EAAM,GAC/E,EAAU,KAAK,IAAI,EAAG,KAAK,IAAI,EAAG,CAAC,CAAC,EACpC,EAAK,EAAE,GAAK,EAAE,EAAI,EAAE,GAAK,EACzB,EAAK,EAAE,GAAK,EAAE,EAAI,EAAE,GAAK,EACzB,EAAI,KAAK,MAAM,EAAO,EAAI,EAAI,EAAO,EAAI,CAAE,EAC7C,GAAK,IAAQ,CAAC,GAAQ,EAAI,EAAK,QACjC,EAAO,CAAE,OAAQ,EAAK,GAAI,OAAQ,EAAU,EAAK,KAAM,CAAE,EAE7D,CACK,IAEL,EAAQ,EAAW,EAAO,CACxB,OAAQ,EAAK,OACb,KAAM,EAAY,KAClB,OAAQ,EAAK,OACb,QACA,KAAM,EACN,KAAM,IACR,CAAC,EAAE,MACL,CAKA,EADkB,EAAiB,CAC3B,EAAU,MAElB,IAAM,EAAU,EAAW,CAAK,EAAE,MAClC,IAAK,IAAM,KAAU,EAAO,CAC1B,GAAI,CAAC,EAAO,KAAM,SAClB,IAAM,EAAW,CACf,EAAI,EAAO,OAAO,QAAQ,EAAG,IAAM,EAAI,EAAE,EAAG,CAAC,EAAI,EAAO,OAAO,OAAU,EACzE,EAAI,EAAO,OAAO,QAAQ,EAAG,IAAM,EAAI,EAAE,EAAG,CAAC,EAAI,EAAO,OAAO,OAAU,CAC3E,EACM,EAAQ,EAAQ,KACnB,GACC,EAAK,QACL,EAAK,MAAM,OAAS,GACpB,EAAgB,EAAU,EAAK,KAAK,GACpC,EAAM,MAAM,EAAK,SAAS,KAAK,WAAW,OAAO,CACrD,EACI,GAAO,SACT,EAAQ,CACN,GAAG,EACH,MAAO,CAAE,GAAG,EAAM,OAAQ,EAAM,QAAS,CAAE,GAAG,EAAM,MAAM,EAAM,QAAS,KAAM,EAAO,IAAK,CAAE,CAC/F,EAEJ,CAEA,MAAO,CAAE,GAAG,EAAM,OAAQ,CAAC,CAAK,CAAE,CACpC,CAEA,SAAS,EAAgB,EAAc,EAA4B,CACjE,IAAI,EAAS,GACb,IAAK,IAAI,EAAI,EAAG,EAAI,EAAK,OAAS,EAAG,EAAI,EAAK,OAAQ,EAAI,IAAK,CAC7D,IAAM,EAAK,EAAK,GAAG,EACb,EAAK,EAAK,GAAG,EACb,EAAK,EAAK,GAAG,EACb,EAAK,EAAK,GAAG,EACf,EAAK,EAAE,GAAM,EAAK,EAAE,GAAK,EAAE,GAAM,EAAK,IAAO,EAAE,EAAI,IAAQ,EAAK,GAAM,IAAI,EAAS,CAAC,EAC1F,CACA,OAAO,CACT"}
|
|
1
|
+
{"version":3,"file":"legacySvg.mjs","names":[],"sources":["../../../../package/common/floorPlan/legacySvg.ts"],"sourcesContent":["/**\n * Imports floor plans exported by the previous `RoomDrawer` into the current model.\n *\n * The old format stored each room as its own closed polygon, so a wall between two\n * rooms appeared twice, once per room. Feeding every polygon edge through `addWall` is\n * what converts that: coincident edges resolve to the same nodes and the second one is\n * recognised as already walled, so the pair collapses into the single shared wall the\n * new model needs.\n *\n * Import only. Nothing writes this format any more. See LEGACY_SVG_FORMAT.md in this\n * folder for the specification of what is accepted.\n */\n\nimport { DEFAULT_WALL_ATTRS, addOpening, addWall, scalePlan } from './floorPlan.graph';\nimport { commitDerivation, derivePlan } from './floorPlan.rooms';\nimport { emptyFloorPlan } from './floorPlan.plan';\nimport { OpeningKind, WallType } from './floorPlan.types';\nimport type { FloorPlan, PlanFloor, PlanPoint } from './floorPlan.types';\n\nexport interface LegacyImportOptions {\n /**\n * Multiplies every imported coordinate, converting the old unitless canvas into the\n * module's millimetre invariant.\n *\n * There is no correct automatic value: the old format records no scale at all. The\n * default of 1 keeps the numbers as they were, which makes a typical 400x300 export\n * a 40cm plan. A 400-unit-wide export meant to be a 12m house wants `scale: 30`.\n */\n scale?: number;\n /** Wall thickness for imported walls, in millimetres after scaling. */\n thickness?: number;\n /** Tolerance for welding coincident endpoints, in millimetres after scaling. */\n weldTolerance?: number;\n label?: string;\n}\n\ninterface LegacyRoom {\n name: string;\n points: PlanPoint[];\n}\n\ninterface LegacyDoor {\n p1: PlanPoint;\n p2: PlanPoint;\n}\n\n/** Reads the absolute M/L commands the old exporter wrote. */\nfunction parseClosedPath(d: string): PlanPoint[] {\n const points: PlanPoint[] = [];\n for (const m of d.matchAll(/[ML]\\s*(-?[\\d.]+)[,\\s]+(-?[\\d.]+)/gi)) {\n points.push({ x: parseFloat(m[1]), y: parseFloat(m[2]) });\n }\n return points;\n}\n\n/**\n * Recovers a door from the old swing-arc path: `M p1 L p2 M p1 A w,w 0 0,1 end`.\n *\n * Position and width follow from p1 and p2. Anything that does not match this shape is\n * skipped and the wall is left solid, rather than guessed at.\n */\nfunction parseDoorPath(d: string): LegacyDoor | null {\n const match = d.match(/^\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*L\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*M\\s*(-?[\\d.]+),(-?[\\d.]+)\\s*A\\s/i);\n if (!match) return null;\n return {\n p1: { x: parseFloat(match[1]), y: parseFloat(match[2]) },\n p2: { x: parseFloat(match[3]), y: parseFloat(match[4]) },\n };\n}\n\nfunction parseLegacyDocument(svg: string): { rooms: LegacyRoom[]; doors: LegacyDoor[] } {\n const doc = new DOMParser().parseFromString(svg, 'image/svg+xml');\n const rooms: LegacyRoom[] = [];\n const doors: LegacyDoor[] = [];\n\n doc.querySelectorAll('[id^=\"room\"]').forEach((group) => {\n const background = group.querySelector('[id=\"background\"]');\n const d = background?.getAttribute('d');\n if (!d) return;\n const points = parseClosedPath(d);\n if (points.length < 3) return;\n rooms.push({ name: group.querySelector('text')?.textContent?.trim() ?? '', points });\n\n group.querySelectorAll('path').forEach((path) => {\n if (path === background) return;\n const door = parseDoorPath(path.getAttribute('d') ?? '');\n if (door) doors.push(door);\n });\n });\n\n return { rooms, doors };\n}\n\n/**\n * Parses a legacy exported SVG into a single-floor `FloorPlan`.\n *\n * Rooms keep their names. Doors are recovered where the old swing-arc path is\n * recognisable, as `OpeningKind.Door`. Windows and archways did not exist in that\n * format, so nothing is lost by not looking for them.\n */\nexport function planFromLegacySvg(svg: string, options: LegacyImportOptions = {}): FloorPlan {\n const { scale = 1, thickness = DEFAULT_WALL_ATTRS.thickness, weldTolerance, label } = options;\n const { rooms, doors } = parseLegacyDocument(svg);\n\n const plan = emptyFloorPlan({ floorLabel: label });\n let floor: PlanFloor = plan.floors[0];\n // Generous relative to the old canvas, where coordinates were snapped to a grid of 5.\n const tol = weldTolerance ?? Math.max(1, scale * 2);\n\n for (const room of rooms) {\n const points = room.points.map((p) => ({ x: p.x * scale, y: p.y * scale }));\n for (let i = 0; i < points.length; i++) {\n const a = points[i];\n const b = points[(i + 1) % points.length];\n // The welding step: an edge an adjacent room already drew resolves to the same\n // two nodes, and addWall declines to duplicate it.\n floor = addWall(floor, a, b, { thickness, type: WallType.Interior }, tol).floor;\n }\n }\n\n for (const door of doors) {\n const p1 = { x: door.p1.x * scale, y: door.p1.y * scale };\n const p2 = { x: door.p2.x * scale, y: door.p2.y * scale };\n const centre = { x: (p1.x + p2.x) / 2, y: (p1.y + p2.y) / 2 };\n const width = Math.hypot(p2.x - p1.x, p2.y - p1.y);\n if (width <= 0) continue;\n\n // Find the wall the door sits on now that edges have been welded.\n let best: { wallId: string; offset: number; dist: number } | null = null;\n for (const wall of Object.values(floor.walls)) {\n const a = floor.nodes[wall.a];\n const b = floor.nodes[wall.b];\n if (!a || !b) continue;\n const len = Math.hypot(b.x - a.x, b.y - a.y);\n if (len < 1e-6) continue;\n const t = ((centre.x - a.x) * (b.x - a.x) + (centre.y - a.y) * (b.y - a.y)) / (len * len);\n const clamped = Math.min(1, Math.max(0, t));\n const px = a.x + (b.x - a.x) * clamped;\n const py = a.y + (b.y - a.y) * clamped;\n const d = Math.hypot(centre.x - px, centre.y - py);\n if (d <= tol && (!best || d < best.dist)) {\n best = { wallId: wall.id, offset: clamped * len, dist: d };\n }\n }\n if (!best) continue;\n\n floor = addOpening(floor, {\n wallId: best.wallId,\n kind: OpeningKind.Door,\n offset: best.offset,\n width,\n sill: 0,\n head: 2100,\n }).floor;\n }\n\n // Name the derived faces from the polygons they came from, matching each face to the\n // legacy room whose own centroid falls inside it.\n const committed = commitDerivation(floor);\n floor = committed.floor;\n\n const derived = derivePlan(floor).rooms;\n for (const legacy of rooms) {\n if (!legacy.name) continue;\n const centroid = {\n x: (legacy.points.reduce((s, p) => s + p.x, 0) / legacy.points.length) * scale,\n y: (legacy.points.reduce((s, p) => s + p.y, 0) / legacy.points.length) * scale,\n };\n const match = derived.find(\n (face) =>\n face.roomId &&\n face.outer.length > 2 &&\n pointInsideRing(centroid, face.outer) &&\n floor.rooms[face.roomId]?.name.startsWith('Room '),\n );\n if (match?.roomId) {\n floor = {\n ...floor,\n rooms: { ...floor.rooms, [match.roomId]: { ...floor.rooms[match.roomId], name: legacy.name } },\n };\n }\n }\n\n return { ...plan, floors: [floor] };\n}\n\nfunction pointInsideRing(p: PlanPoint, ring: PlanPoint[]): boolean {\n let inside = false;\n for (let i = 0, j = ring.length - 1; i < ring.length; j = i++) {\n const xi = ring[i].x;\n const yi = ring[i].y;\n const xj = ring[j].x;\n const yj = ring[j].y;\n if (yi > p.y !== yj > p.y && p.x < ((xj - xi) * (p.y - yi)) / (yj - yi) + xi) inside = !inside;\n }\n return inside;\n}\n\nexport { scalePlan };\n"],"mappings":"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"}
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
import{getScrollableAncestors as e}from"../utils/scrollAncestors.mjs";import{useLayoutEffect as t,useRef as n}from"react";function r({isOpen:r,triggerRef:i,elementRefs:a,update:o,resizeRefs:s,teardown:c,deps:l=[]}){let u=n({elementRefs:a,update:o,resizeRefs:s,teardown:c});u.current={elementRefs:a,update:o,resizeRefs:s,teardown:c},t(()=>{if(!r)return;let t,n;function a(){let r=i.current,o=u.current.elementRefs;if(!r||o.some(e=>!e.current)){t=requestAnimationFrame(a);return}let s=e(r),c=new ResizeObserver(()=>d()),l=new Set;function d(){u.current.update(r,s),u.current.resizeRefs?.forEach(({current:e})=>{e&&!l.has(e)&&(l.add(e),c.observe(e))})}d();let f=[window,...s];f.forEach(e=>e.addEventListener(`scroll`,d,{passive:!0})),window.addEventListener(`resize`,d),window.visualViewport?.addEventListener(`resize`,d),window.visualViewport?.addEventListener(`scroll`,d),n=()=>{f.forEach(e=>e.removeEventListener(`scroll`,d)),window.removeEventListener(`resize`,d),window.visualViewport?.removeEventListener(`resize`,d),window.visualViewport?.removeEventListener(`scroll`,d),c.disconnect(),u.current.teardown?.()}}return a(),()=>{t!==void 0&&cancelAnimationFrame(t),n?.()}},[r,i,...l])}export{r as useAnchorTracking};
|
|
2
|
+
//# sourceMappingURL=useAnchorTracking.mjs.map
|