Skip to content

Commit 271dee0

Browse files
floorplan WIP 1
1 parent 843a93d commit 271dee0

File tree

1 file changed

+264
-1
lines changed

1 file changed

+264
-1
lines changed

src/ios/CDVRoomPlan.swift

Lines changed: 264 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import Foundation
99
import UIKit
1010
import RoomPlan
1111
import ARKit
12+
import SpriteKit
13+
import simd
1214

1315
@objc(CDVRoomPlan)
1416
class CDVRoomPlan: CDVPlugin, RoomCaptureSessionDelegate, RoomCaptureViewDelegate, UIDocumentPickerDelegate {
@@ -125,7 +127,19 @@ class CDVRoomPlan: CDVPlugin, RoomCaptureSessionDelegate, RoomCaptureViewDelegat
125127
try jsonData.write(to: jsonFile)
126128
try self.processedResult?.export(to: modelFile, exportOptions: .parametric)
127129
if (self.processedResult != nil) && isCapturedRoomNil(capturedRoom: self.processedResult!) {
128-
let result = ["model": modelFile.absoluteString, "json": jsonFile.absoluteString, "message": "Scanning completed successfully"]
130+
// Generate 2D floor plan
131+
let floorPlanImagePath = generate2DFloorPlan(capturedRoom: self.processedResult!, outputDirectory: documentsDirectory, uuid: uuid)
132+
133+
var result: [String: Any] = [
134+
"model": modelFile.absoluteString,
135+
"json": jsonFile.absoluteString,
136+
"message": "Scanning completed successfully"
137+
]
138+
139+
if let floorPlanPath = floorPlanImagePath {
140+
result["floorPlan"] = floorPlanPath.absoluteString
141+
}
142+
129143
let pluginResult = CDVPluginResult(status: CDVCommandStatus_OK, messageAs: result)
130144
pluginResult?.keepCallback = true
131145
self.commandDelegate.send(pluginResult, callbackId: self.command.callbackId)
@@ -143,6 +157,228 @@ class CDVRoomPlan: CDVPlugin, RoomCaptureSessionDelegate, RoomCaptureViewDelegat
143157
}
144158
}
145159

160+
func generate2DFloorPlan(capturedRoom: CapturedRoom, outputDirectory: URL, uuid: String) -> URL? {
161+
guard #available(iOS 17.0, *) else {
162+
return nil
163+
}
164+
165+
// Create SpriteKit scene for 2D floor plan
166+
let sceneSize = CGSize(width: 2048, height: 2048)
167+
let scene = SKScene(size: sceneSize)
168+
scene.backgroundColor = .white
169+
170+
// Calculate bounds to center the floor plan
171+
var minX: Float = Float.greatestFiniteMagnitude
172+
var maxX: Float = -Float.greatestFiniteMagnitude
173+
var minZ: Float = Float.greatestFiniteMagnitude
174+
var maxZ: Float = -Float.greatestFiniteMagnitude
175+
176+
// Collect all points to determine bounds
177+
var allPoints: [(x: Float, z: Float)] = []
178+
179+
// Extract all surfaces to calculate bounds
180+
for wall in capturedRoom.walls {
181+
let transform = wall.transform
182+
let position = transform.position
183+
let dimensions = wall.dimensions
184+
let halfLength = dimensions.x / 2.0
185+
let forward = simd_float3(transform.columns.0.x, transform.columns.0.y, transform.columns.0.z)
186+
let wallStart = position - forward * halfLength
187+
let wallEnd = position + forward * halfLength
188+
allPoints.append((x: wallStart.x, z: wallStart.z))
189+
allPoints.append((x: wallEnd.x, z: wallEnd.z))
190+
}
191+
192+
for door in capturedRoom.doors {
193+
let transform = door.transform
194+
let position = transform.position
195+
allPoints.append((x: position.x, z: position.z))
196+
}
197+
198+
for window in capturedRoom.windows {
199+
let transform = window.transform
200+
let position = transform.position
201+
allPoints.append((x: position.x, z: position.z))
202+
}
203+
204+
for obj in capturedRoom.objects {
205+
let transform = obj.transform
206+
let position = transform.position
207+
allPoints.append((x: position.x, z: position.z))
208+
}
209+
210+
// Calculate bounds
211+
for point in allPoints {
212+
minX = min(minX, point.x)
213+
maxX = max(maxX, point.x)
214+
minZ = min(minZ, point.z)
215+
maxZ = max(maxZ, point.z)
216+
}
217+
218+
if allPoints.isEmpty {
219+
return nil
220+
}
221+
222+
// Calculate scale and offset to fit in the scene
223+
let width = maxX - minX
224+
let height = maxZ - minZ
225+
let maxDimension = max(width, height)
226+
let scale: Float = maxDimension > 0 ? 1800.0 / maxDimension : 1.0
227+
let offsetX = (minX + maxX) / 2.0
228+
let offsetZ = (minZ + maxZ) / 2.0
229+
230+
// Helper function to convert 3D coordinates to 2D scene coordinates
231+
func convertToScene(x: Float, z: Float) -> CGPoint {
232+
let sceneX = CGFloat((x - offsetX) * scale) + sceneSize.width / 2
233+
let sceneY = CGFloat((z - offsetZ) * scale) + sceneSize.height / 2
234+
return CGPoint(x: sceneX, y: sceneY)
235+
}
236+
237+
// Draw walls using SpriteKit
238+
for wall in capturedRoom.walls {
239+
let transform = wall.transform
240+
let position = transform.position
241+
let dimensions = wall.dimensions
242+
let eulerAngles = transform.eulerAngles
243+
244+
let halfLength = dimensions.x / 2.0
245+
let forward = simd_float3(transform.columns.0.x, transform.columns.0.y, transform.columns.0.z)
246+
247+
let wallStart = position - forward * halfLength
248+
let wallEnd = position + forward * halfLength
249+
250+
let startPoint = convertToScene(x: wallStart.x, z: wallStart.z)
251+
let endPoint = convertToScene(x: wallEnd.x, z: wallEnd.z)
252+
253+
// Create path for wall
254+
let path = CGMutablePath()
255+
path.move(to: startPoint)
256+
path.addLine(to: endPoint)
257+
258+
// Create SKShapeNode for wall
259+
let wallNode = SKShapeNode(path: path)
260+
wallNode.strokeColor = .black
261+
wallNode.lineWidth = max(2, CGFloat(dimensions.y * scale * 0.1))
262+
wallNode.lineCap = .round
263+
scene.addChild(wallNode)
264+
}
265+
266+
// Draw doors using SpriteKit
267+
for door in capturedRoom.doors {
268+
let transform = door.transform
269+
let position = transform.position
270+
let dimensions = door.dimensions
271+
let eulerAngles = transform.eulerAngles
272+
273+
let scenePoint = convertToScene(x: position.x, z: position.z)
274+
let width = CGFloat(dimensions.x * scale)
275+
let depth = CGFloat(dimensions.z * scale)
276+
277+
// Create rectangle path for door
278+
let doorRect = CGRect(
279+
x: scenePoint.x - width / 2,
280+
y: scenePoint.y - depth / 2,
281+
width: width,
282+
height: depth
283+
)
284+
let path = CGPath(rect: doorRect, transform: nil)
285+
286+
// Create SKShapeNode for door
287+
let doorNode = SKShapeNode(path: path)
288+
doorNode.fillColor = .lightGray
289+
doorNode.strokeColor = .blue
290+
doorNode.lineWidth = 2
291+
doorNode.zRotation = CGFloat(eulerAngles.y) // Rotate based on Y-axis rotation
292+
scene.addChild(doorNode)
293+
}
294+
295+
// Draw windows using SpriteKit
296+
for window in capturedRoom.windows {
297+
let transform = window.transform
298+
let position = transform.position
299+
let dimensions = window.dimensions
300+
let eulerAngles = transform.eulerAngles
301+
302+
let scenePoint = convertToScene(x: position.x, z: position.z)
303+
let width = CGFloat(dimensions.x * scale)
304+
let depth = CGFloat(dimensions.z * scale)
305+
306+
// Create rectangle path for window
307+
let windowRect = CGRect(
308+
x: scenePoint.x - width / 2,
309+
y: scenePoint.y - depth / 2,
310+
width: width,
311+
height: depth
312+
)
313+
let path = CGPath(rect: windowRect, transform: nil)
314+
315+
// Create SKShapeNode for window
316+
let windowNode = SKShapeNode(path: path)
317+
windowNode.fillColor = .white
318+
windowNode.strokeColor = .cyan
319+
windowNode.lineWidth = 2
320+
windowNode.zRotation = CGFloat(eulerAngles.y)
321+
scene.addChild(windowNode)
322+
}
323+
324+
// Draw objects using SpriteKit
325+
for obj in capturedRoom.objects {
326+
let transform = obj.transform
327+
let position = transform.position
328+
let dimensions = obj.dimensions
329+
let eulerAngles = transform.eulerAngles
330+
331+
let scenePoint = convertToScene(x: position.x, z: position.z)
332+
let width = CGFloat(dimensions.x * scale)
333+
let depth = CGFloat(dimensions.z * scale)
334+
335+
// Create rectangle path for object
336+
let objectRect = CGRect(
337+
x: scenePoint.x - width / 2,
338+
y: scenePoint.y - depth / 2,
339+
width: width,
340+
height: depth
341+
)
342+
let path = CGPath(rect: objectRect, transform: nil)
343+
344+
// Create SKShapeNode for object
345+
let objectNode = SKShapeNode(path: path)
346+
objectNode.fillColor = .lightGray
347+
objectNode.strokeColor = .brown
348+
objectNode.lineWidth = 2
349+
objectNode.zRotation = CGFloat(eulerAngles.y)
350+
scene.addChild(objectNode)
351+
}
352+
353+
// Render the SpriteKit scene to an image
354+
let imageFile = outputDirectory.appendingPathComponent(uuid + "_floorplan.png")
355+
356+
// Create SKView to render the scene
357+
let skView = SKView(frame: CGRect(origin: .zero, size: sceneSize))
358+
skView.presentScene(scene)
359+
360+
// Get texture from the scene
361+
guard let texture = skView.texture(from: scene) else {
362+
return nil
363+
}
364+
365+
// Convert texture to UIImage
366+
let cgImage = texture.cgImage()
367+
let uiImage = UIImage(cgImage: cgImage)
368+
369+
// Save the image
370+
guard let imageData = uiImage.pngData() else {
371+
return nil
372+
}
373+
374+
do {
375+
try imageData.write(to: imageFile)
376+
return imageFile
377+
} catch {
378+
return nil
379+
}
380+
}
381+
146382
private func addButtons() {
147383
cancelButton = createButton(title: "Cancel", backgroundColor: UIColor(hex: "#D65745"))
148384
doneButton = createButton(title: "Done", backgroundColor: UIColor(hex: "#00A885"))
@@ -194,6 +430,33 @@ class CDVRoomPlan: CDVPlugin, RoomCaptureSessionDelegate, RoomCaptureViewDelegat
194430
}
195431
}
196432

433+
// Extension for simd_float4x4 to extract position and euler angles (as per article reference)
434+
extension simd_float4x4 {
435+
var position: simd_float3 {
436+
return simd_float3(self.columns.3.x, self.columns.3.y, self.columns.3.z)
437+
}
438+
439+
var eulerAngles: simd_float3 {
440+
// Extract rotation angles from the transform matrix
441+
let sy = sqrt(self.columns.0.x * self.columns.0.x + self.columns.1.x * self.columns.1.x)
442+
let singular = sy < 1e-6
443+
444+
var x: Float, y: Float, z: Float
445+
446+
if !singular {
447+
x = atan2(self.columns.2.y, self.columns.2.z)
448+
y = atan2(-self.columns.2.x, sy)
449+
z = atan2(self.columns.1.x, self.columns.0.x)
450+
} else {
451+
x = atan2(-self.columns.1.z, self.columns.1.y)
452+
y = atan2(-self.columns.2.x, sy)
453+
z = 0
454+
}
455+
456+
return simd_float3(x, y, z)
457+
}
458+
}
459+
197460
extension UIColor {
198461
convenience init(hex: String, alpha: CGFloat = 1.0) {
199462
let hexString = hex.trimmingCharacters(in: CharacterSet.alphanumerics.inverted)

0 commit comments

Comments
 (0)