diff --git a/packages/editor/src/components/editor/first-person-controls.tsx b/packages/editor/src/components/editor/first-person-controls.tsx
index 35ce42d6..b146d533 100644
--- a/packages/editor/src/components/editor/first-person-controls.tsx
+++ b/packages/editor/src/components/editor/first-person-controls.tsx
@@ -166,7 +166,10 @@ export const FirstPersonControls = () => {
node.doorType === 'folding' ||
node.doorType === 'pocket' ||
node.doorType === 'barn' ||
- node.doorType === 'sliding'
+ node.doorType === 'sliding' ||
+ node.doorType === 'garage-sectional' ||
+ node.doorType === 'garage-rollup' ||
+ node.doorType === 'garage-tiltup'
) {
const currentOpenAmount = node.operationState ?? 0
updateDoorOpenState(doorId, {
diff --git a/packages/editor/src/components/editor/floorplan-panel.tsx b/packages/editor/src/components/editor/floorplan-panel.tsx
index df6f4a1e..0cb75168 100644
--- a/packages/editor/src/components/editor/floorplan-panel.tsx
+++ b/packages/editor/src/components/editor/floorplan-panel.tsx
@@ -4820,6 +4820,23 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
}
const slidingFixedPoints = slidingPanelPoints(slidingFixedCenter, doorCubeSize * 0.34)
const slidingActivePoints = slidingPanelPoints(slidingActiveCenter, doorCubeSize * 0.68)
+ const isGarageSectionalDoor = opening.doorType === 'garage-sectional'
+ const isGarageRollupDoor = opening.doorType === 'garage-rollup'
+ const isGarageTiltupDoor = opening.doorType === 'garage-tiltup'
+ const garagePanelCount = Math.max(3, Math.min(12, opening.garagePanelCount ?? 4))
+ const garagePanelLines = Array.from({ length: garagePanelCount - 1 }, (_, index) => {
+ const t = (index + 1) / garagePanelCount
+ return {
+ start: {
+ x: svgP1.x + (svgP2.x - svgP1.x) * t,
+ y: svgP1.y + (svgP2.y - svgP1.y) * t,
+ },
+ end: {
+ x: svgP1.x + (svgP2.x - svgP1.x) * t + px * swingSign * doorCubeSize * 0.78,
+ y: svgP1.y + (svgP2.y - svgP1.y) * t + py * swingSign * doorCubeSize * 0.78,
+ },
+ }
+ })
const isDoubleSwingDoor = opening.doorType === 'double' || opening.doorType === 'french'
const doubleLeafPlans = isDoubleSwingDoor
? (
@@ -5215,6 +5232,66 @@ const FloorplanGeometryLayer = memo(function FloorplanGeometryLayer({
vectorEffect="non-scaling-stroke"
/>
>
+ ) : isGarageSectionalDoor || isGarageRollupDoor || isGarageTiltupDoor ? (
+ <>
+
+
+ {isGarageRollupDoor ? (
+
+ ) : isGarageTiltupDoor ? (
+
+ ) : (
+ garagePanelLines.map((line, index) => (
+
+ ))
+ )}
+ >
) : isDoubleSwingDoor ? (
<>
{doubleLeafPlans.map((leaf) =>
diff --git a/packages/editor/src/components/ui/panels/door-panel.tsx b/packages/editor/src/components/ui/panels/door-panel.tsx
index f0b5c480..77b009d7 100755
--- a/packages/editor/src/components/ui/panels/door-panel.tsx
+++ b/packages/editor/src/components/ui/panels/door-panel.tsx
@@ -37,6 +37,16 @@ const doorTypeOptions = [
available: boolean
}[]
+const garageDoorTypeOptions = [
+ { label: 'Sectional', value: 'garage-sectional', available: true },
+ { label: 'Roll-up', value: 'garage-rollup', available: true },
+ { label: 'Tilt-up', value: 'garage-tiltup', available: true },
+] satisfies {
+ label: string
+ value: DoorNode['doorType']
+ available: boolean
+}[]
+
const frenchDoorSegments: DoorNode['segments'] = [
{
type: 'glass',
@@ -328,7 +338,13 @@ export function DoorPanel() {
const doorType = node.doorType ?? 'hinged'
const isSwingDoor = doorType === 'hinged' || doorType === 'double' || doorType === 'french'
const isSlidingDoor = doorType === 'pocket' || doorType === 'barn' || doorType === 'sliding'
+ const isGarageDoor = node.doorCategory === 'garage' || doorType.startsWith('garage-')
+ const isSectionalGarageDoor = doorType === 'garage-sectional'
+ const isRollupGarageDoor = doorType === 'garage-rollup'
+ const isTiltupGarageDoor = doorType === 'garage-tiltup'
+ const typeMode = isOpening ? 'opening' : isGarageDoor ? 'garage' : 'door'
const supportsHandleSide = isSwingDoor
+ const maxDoorWidth = isGarageDoor ? 6 : 3
const setOpeningTopRadius = (index: number, value: number, commit = false) => {
const next = [...openingTopRadii] as [number, number]
@@ -427,11 +443,60 @@ export function DoorPanel() {
}
}
+ if (nextDoorType === 'garage-sectional') {
+ return {
+ doorCategory: 'garage',
+ doorType: nextDoorType,
+ leafCount: 1,
+ ...dimensionUpdates,
+ handle: false,
+ threshold: false,
+ trackStyle: 'overhead',
+ operationState: 0,
+ garagePanelCount: Math.max(3, Math.min(8, node.garagePanelCount ?? 4)),
+ contentPadding: [0.04, 0.04],
+ segments: foldingDoorSegments,
+ }
+ }
+
+ if (nextDoorType === 'garage-rollup') {
+ return {
+ doorCategory: 'garage',
+ doorType: nextDoorType,
+ leafCount: 1,
+ ...dimensionUpdates,
+ handle: false,
+ threshold: false,
+ trackStyle: 'overhead',
+ operationState: 0,
+ garagePanelCount: 4,
+ contentPadding: [0.04, 0.04],
+ segments: foldingDoorSegments,
+ }
+ }
+
+ if (nextDoorType === 'garage-tiltup') {
+ return {
+ doorCategory: 'garage',
+ doorType: nextDoorType,
+ leafCount: 1,
+ ...dimensionUpdates,
+ handle: false,
+ threshold: false,
+ trackStyle: 'overhead',
+ operationState: 0,
+ garagePanelCount: 4,
+ contentPadding: [0.04, 0.04],
+ segments: foldingDoorSegments,
+ }
+ }
+
return {
doorCategory: 'interior',
doorType: nextDoorType,
leafCount: 1,
...dimensionUpdates,
+ threshold: true,
}
}
@@ -478,19 +543,28 @@ export function DoorPanel() {
archHeight,
openingRevealRadius,
}
- : { openingKind: v },
+ : v === 'garage'
+ ? {
+ openingKind: 'door',
+ ...getDoorTypeUpdates(isGarageDoor ? doorType : 'garage-sectional'),
+ }
+ : {
+ openingKind: 'door',
+ ...(isGarageDoor ? getDoorTypeUpdates('hinged') : {}),
+ },
)
}
options={[
{ label: 'Door', value: 'door' },
{ label: 'Opening', value: 'opening' },
+ { label: 'Garage', value: 'garage' },
]}
- value={node.openingKind}
+ value={typeMode}
/>
{!isOpening && (
- {doorTypeOptions.map((option) => {
+ {(isGarageDoor ? garageDoorTypeOptions : doorTypeOptions).map((option) => {
const isSelected = doorType === option.value
return (