Before: ApplyTemplate dropped devices in a horizontal row at fixed
canvas coords with frame_id NULL — devices appeared anywhere and m
had no way to express "these belong together".
Now: each apply creates a frame named after the template (suffixed
"… 2/3/…" on name collision) and lays the devices out in a uniform
grid inside it. Grid is roughly square (cols = ceil(sqrt(N)), capped
at 4) with 30/50 px gaps and 32/48 px padding. Each device gets the
new frame's id and grid-cell coords.
Schema unchanged. ApplyTemplateResult.frames_added carries the new
frame so the frontend can refresh the canvas without a full snapshot
reload.
Tests:
- TestApplyTemplate_CreatesFrameAndPlacesDevicesInside — frame is
created with the template's name, every device has frame_id set,
every device sits inside the frame rect, no two devices share a
grid cell.
- TestApplyTemplate_FrameNameSuffixOnCollision — pre-existing
"Living Room" frame in the project ⇒ template's frame named
"Living Room 2".
- Existing tests unchanged.