Skip to content

Commit 5a82730

Browse files
committed
feat: Add door feature to FloorPlanEditor with related room connections
- Updated DrawnFeature type to include 'door' and relatedRoomIds. - Modified fetchFloorFeatures to load door features and handle geometry. - Enhanced EditorCanvas to render door markers and icons. - Updated EditorSidebar to display features and manage door connections. - Added door properties management in PropertyPanel. - Introduced new button in EditorToolbar for placing doors. - Improved CalibrationModal with copy URL functionality. - Enhanced styles for better UI/UX.
1 parent 58e7aa0 commit 5a82730

12 files changed

Lines changed: 1065 additions & 245 deletions

File tree

apps/admin/src/pages/Calibration.tsx

Lines changed: 595 additions & 144 deletions
Large diffs are not rendered by default.

apps/admin/src/pages/FloorPlanEditor.tsx

Lines changed: 64 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -21,12 +21,13 @@ type FeatureType = 'room' | 'office' | 'hallway' | 'bathroom' | 'stairs' | 'elev
2121

2222
interface DrawnFeature {
2323
id: string;
24-
type: 'polygon';
24+
type: 'polygon' | 'door';
2525
featureType: FeatureType;
26-
points: number[]; // [x1, y1, x2, y2, ...]
26+
points: number[]; // [x1, y1, x2, y2, ...] for polygon or [x, y] for door
2727
name: string;
2828
accessible: boolean;
2929
calibrated: boolean;
30+
relatedRoomIds?: string[]; // For doors: which rooms they connect to
3031
}
3132

3233
export default function FloorPlanEditor() {
@@ -85,26 +86,36 @@ export default function FloorPlanEditor() {
8586
return;
8687
}
8788

88-
fetchFloorFeatures(selectedFloor.id, ['space'])
89+
fetchFloorFeatures(selectedFloor.id, ['space', 'door'])
8990
.then((collection: any) => {
9091
const loadedFeatures: DrawnFeature[] = collection.features.map((feature: any) => {
9192
const props = feature.properties as any;
9293
let points: number[] = [];
94+
let featureType = (props.featureType || props.category || 'room') as FeatureType;
95+
let type: 'polygon' | 'door' = 'polygon';
9396

94-
if (feature.geometry.type === 'Polygon' && feature.geometry.coordinates.length > 0) {
97+
// Handle door features (Point geometry) - loaded globally
98+
if (feature.geometry.type === 'Point') {
99+
points = [feature.geometry.coordinates[0], feature.geometry.coordinates[1]];
100+
featureType = 'door';
101+
type = 'door';
102+
}
103+
// Handle polygon features (rooms)
104+
else if (feature.geometry.type === 'Polygon' && feature.geometry.coordinates.length > 0) {
95105
const ring = feature.geometry.coordinates[0];
96106
const coords = ring.slice(0, -1);
97107
points = coords.flatMap((coord: any) => [coord[0], coord[1]]);
98108
}
99109

100110
return {
101111
id: props.id || crypto.randomUUID(),
102-
type: 'polygon' as const,
103-
featureType: (props.featureType || props.category || 'room') as FeatureType,
112+
type,
113+
featureType,
104114
points,
105115
name: props.name || props.roomName || '',
106116
accessible: props.accessible || false,
107117
calibrated: props.calibrated || false,
118+
relatedRoomIds: props.relatedRoomIds || [], // Doors can reference rooms
108119
};
109120
});
110121
setFeatures(loadedFeatures);
@@ -120,12 +131,31 @@ export default function FloorPlanEditor() {
120131
const featureCollection = {
121132
type: 'FeatureCollection' as const,
122133
features: features.map((feature) => {
123-
// Convert points array to polygon coordinates
134+
// Handle door features (Point geometry)
135+
if (feature.type === 'door') {
136+
return {
137+
type: 'Feature' as const,
138+
geometry: {
139+
type: 'Point' as const,
140+
coordinates: [feature.points[0], feature.points[1]] as [number, number],
141+
},
142+
properties: {
143+
id: feature.id,
144+
layer: 'door' as const,
145+
name: feature.name,
146+
featureType: 'door' as const,
147+
accessible: feature.accessible,
148+
calibrated: feature.calibrated,
149+
relatedRoomIds: feature.relatedRoomIds || [], // Reference to connected rooms
150+
},
151+
};
152+
}
153+
154+
// Handle polygon features
124155
const coordinates: [number, number][] = [];
125156
for (let i = 0; i < feature.points.length; i += 2) {
126-
coordinates.push([feature.points[i], feature.points[i + 1]]);
157+
coordinates.push([feature.points[i], feature.points[i + 1]] as [number, number]);
127158
}
128-
// Close the polygon by adding first point at the end
129159
if (coordinates.length > 0) {
130160
coordinates.push(coordinates[0]);
131161
}
@@ -146,7 +176,7 @@ export default function FloorPlanEditor() {
146176
},
147177
};
148178
}),
149-
};
179+
} as any;
150180

151181
saveFloorFeatures(selectedFloor.id, featureCollection)
152182
.then(() => console.log('Features auto-saved'))
@@ -222,6 +252,23 @@ export default function FloorPlanEditor() {
222252
if (drawMode === 'polygon') {
223253
setCurrentPoints([...currentPoints, x, y]);
224254
setIsDrawing(true);
255+
} else if (drawMode === 'door') {
256+
// Place a door at the clicked location
257+
const newDoor: DrawnFeature = {
258+
id: crypto.randomUUID(),
259+
type: 'door',
260+
featureType: 'door',
261+
points: [x, y],
262+
name: `Door ${features.filter(f => f.featureType === 'door').length + 1}`,
263+
accessible: false,
264+
calibrated: false,
265+
};
266+
267+
setFeatures([...features, newDoor]);
268+
setDrawMode('select');
269+
setSelectedFeatureId(newDoor.id);
270+
setFeatureName(newDoor.name);
271+
setFeatureType('door');
225272
}
226273
};
227274

@@ -308,6 +355,13 @@ export default function FloorPlanEditor() {
308355
const handleVertexDrag = (featureId: string, vertexIndex: number, x: number, y: number) => {
309356
setFeatures(features.map(f => {
310357
if (f.id !== featureId) return f;
358+
359+
// Handle door features (single point)
360+
if (f.type === 'door') {
361+
return { ...f, points: [x, y] };
362+
}
363+
364+
// Handle polygon features
311365
const newPoints = [...f.points];
312366
newPoints[vertexIndex * 2] = x;
313367
newPoints[vertexIndex * 2 + 1] = y;

apps/admin/src/pages/FloorPlanEditor/CalibrationModal.tsx

Lines changed: 64 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,15 @@ const CalibrationModal: React.FC<CalibrationModalProps> = ({
1616
}) => {
1717
if (!isOpen) return null;
1818

19+
const handleOpenCalibration = () => {
20+
window.open(calibrationUrl, '_blank');
21+
};
22+
23+
const handleCopyUrl = () => {
24+
navigator.clipboard.writeText(calibrationUrl);
25+
alert('Calibration URL copied to clipboard!');
26+
};
27+
1928
return (
2029
<div className="modal-overlay">
2130
<div className="modal-content" style={{ maxWidth: '500px', textAlign: 'center' }}>
@@ -25,23 +34,65 @@ const CalibrationModal: React.FC<CalibrationModalProps> = ({
2534
Walk to each corner of the room and capture the GPS coordinates.
2635
</p>
2736

28-
<div style={{ background: 'white', padding: '20px', display: 'inline-block', borderRadius: '10px', marginBottom: '20px' }}>
29-
<QRCode value={calibrationUrl} size={200} />
37+
<div style={{ background: '#f8f9fa', padding: '20px', display: 'inline-block', borderRadius: '10px', marginBottom: '20px', border: '2px solid #e9ecef' }}>
38+
<QRCode value={calibrationUrl} size={200} level="H" includeMargin={true} />
3039
</div>
3140

32-
<div className="input-group" style={{ marginBottom: '20px' }}>
33-
<input
34-
type="text"
35-
readOnly
36-
value={calibrationUrl}
37-
onClick={(e) => e.currentTarget.select()}
38-
style={{ textAlign: 'center', width: '100%' }}
39-
/>
41+
<div style={{ marginBottom: '20px' }}>
42+
<p style={{ fontSize: '0.85rem', color: '#7f8c8d', marginBottom: '10px' }}>Or use this link:</p>
43+
<div className="input-group" style={{ marginBottom: '10px' }}>
44+
<input
45+
type="text"
46+
readOnly
47+
value={calibrationUrl}
48+
onClick={(e) => e.currentTarget.select()}
49+
style={{ textAlign: 'center', flex: 1 }}
50+
/>
51+
<button
52+
onClick={handleCopyUrl}
53+
style={{
54+
padding: '0.75rem 1rem',
55+
backgroundColor: '#95a5a6',
56+
color: 'white',
57+
border: 'none',
58+
borderRadius: '4px',
59+
cursor: 'pointer'
60+
}}
61+
>
62+
Copy
63+
</button>
64+
</div>
4065
</div>
4166

42-
<div className="flex justify-end">
43-
<button className="btn btn-primary" onClick={onClose}>
44-
Done
67+
<div style={{ display: 'flex', gap: '10px', justifyContent: 'center' }}>
68+
<button
69+
onClick={handleOpenCalibration}
70+
style={{
71+
flex: 1,
72+
padding: '0.75rem 1.5rem',
73+
backgroundColor: '#3498db',
74+
color: 'white',
75+
border: 'none',
76+
borderRadius: '6px',
77+
cursor: 'pointer',
78+
fontWeight: 'bold'
79+
}}
80+
>
81+
Open Calibration
82+
</button>
83+
<button
84+
onClick={onClose}
85+
style={{
86+
flex: 1,
87+
padding: '0.75rem 1.5rem',
88+
backgroundColor: '#95a5a6',
89+
color: 'white',
90+
border: 'none',
91+
borderRadius: '6px',
92+
cursor: 'pointer'
93+
}}
94+
>
95+
Close
4596
</button>
4697
</div>
4798
</div>

apps/admin/src/pages/FloorPlanEditor/EditorCanvas.tsx

Lines changed: 59 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
1-
import { Stage, Layer, Image as KonvaImage, Line, Circle, Group } from 'react-konva';
1+
import { Stage, Layer, Image as KonvaImage, Line, Circle, Group, Rect, Text } from 'react-konva';
22

33
type DrawMode = 'select' | 'polygon' | 'door';
44

@@ -56,6 +56,63 @@ const EditorCanvas: React.FC<EditorCanvasProps> = ({
5656
{/* Drawn Features */}
5757
{features.map((feature) => {
5858
const isSelected = feature.id === selectedFeatureId;
59+
60+
// Handle door features (single point)
61+
if (feature.type === 'door') {
62+
const [x, y] = feature.points;
63+
return (
64+
<Group
65+
key={feature.id}
66+
onClick={(e) => {
67+
if (drawMode === 'select') {
68+
e.cancelBubble = true;
69+
onFeatureClick(feature.id);
70+
}
71+
}}
72+
>
73+
{/* Door marker */}
74+
<Rect
75+
x={x - 8 / scale}
76+
y={y - 8 / scale}
77+
width={16 / scale}
78+
height={16 / scale}
79+
fill={isSelected ? '#dc2626' : '#ef4444'}
80+
stroke="white"
81+
strokeWidth={2 / scale}
82+
cornerRadius={3 / scale}
83+
/>
84+
85+
{/* Door icon/label */}
86+
<Text
87+
x={x - 4 / scale}
88+
y={y - 4 / scale}
89+
text="🚪"
90+
fontSize={12 / scale}
91+
width={8 / scale}
92+
height={8 / scale}
93+
/>
94+
95+
{/* Draggable handle when selected */}
96+
{isSelected && drawMode === 'select' && (
97+
<Circle
98+
x={x}
99+
y={y}
100+
radius={6 / scale}
101+
fill="#fff"
102+
stroke="#dc2626"
103+
strokeWidth={2 / scale}
104+
draggable
105+
onDragMove={(e) => {
106+
const node = e.target;
107+
onVertexDrag(feature.id, 0, node.x(), node.y());
108+
}}
109+
/>
110+
)}
111+
</Group>
112+
);
113+
}
114+
115+
// Handle polygon features
59116
return (
60117
<Group
61118
key={feature.id}
@@ -102,7 +159,7 @@ const EditorCanvas: React.FC<EditorCanvasProps> = ({
102159
})}
103160

104161
{/* Current Drawing */}
105-
{currentPoints.length > 0 && (
162+
{currentPoints.length > 0 && drawMode !== 'door' && (
106163
<>
107164
<Line
108165
points={currentPoints}

0 commit comments

Comments
 (0)