Compare commits

..

3 Commits

Author SHA1 Message Date
f30d7c0117 Merge pull request #158 from SyncrowIOT:bugfix/duplicate-space
Bugfix/duplicate-space
2025-04-27 11:13:07 +04:00
976d6e385a duplicated space 2025-04-27 11:12:03 +04:00
ff07e7509d fixed the issue in aligning child space 2025-04-26 16:04:41 +04:00
6 changed files with 180 additions and 176 deletions

View File

@ -24,10 +24,11 @@ class FlushMountedPresenceSensorChangeValueEvent
extends FlushMountedPresenceSensorEvent { extends FlushMountedPresenceSensorEvent {
final int value; final int value;
final String code; final String code;
final bool isBatchControl;
const FlushMountedPresenceSensorChangeValueEvent({ const FlushMountedPresenceSensorChangeValueEvent({
required this.value, required this.value,
required this.code, required this.code,
this.isBatchControl = false,
}); });
@override @override

View File

@ -75,7 +75,7 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.nearDetection / 100).clamp(0.0, double.infinity), value: (model.nearDetection / 100).toDouble(),
title: 'Nearest Detect Dist:', title: 'Nearest Detect Dist:',
description: 'm', description: 'm',
minValue: 0.0, minValue: 0.0,
@ -92,7 +92,7 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.farDetection / 100).clamp(0.0, double.infinity), value: (model.farDetection / 100).toDouble(),
title: 'Max Detect Dist:', title: 'Max Detect Dist:',
description: 'm', description: 'm',
minValue: 0.0, minValue: 0.0,
@ -109,7 +109,7 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: model.sensiReduce.toDouble(), value: model.presenceDelay.toDouble(),
title: 'Trigger Level:', title: 'Trigger Level:',
minValue: 0, minValue: 0,
maxValue: 3, maxValue: 3,
@ -117,7 +117,7 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add( action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorBatchControlEvent( FlushMountedPresenceSensorBatchControlEvent(
deviceIds: devicesIds, deviceIds: devicesIds,
code: FlushMountedPresenceSensorModel.codeSensiReduce, code: FlushMountedPresenceSensorModel.codePresenceDelay,
value: value, value: value,
), ),
), ),
@ -137,21 +137,19 @@ class FlushMountedPresenceSensorBatchControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.presenceDelay / 10).toDouble(), value: (model.sensiReduce.toDouble()),
title: 'Target Confirm Time:', title: 'Target Confirm Time:',
description: 's', description: 's',
minValue: 0.0, minValue: 0,
maxValue: 0.5, maxValue: 3,
steps: 0.1, steps: 1,
valuesPercision: 1, action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
action: (double value) => FlushMountedPresenceSensorBatchControlEvent(
context.read<FlushMountedPresenceSensorBloc>().add( deviceIds: devicesIds,
FlushMountedPresenceSensorBatchControlEvent( code: FlushMountedPresenceSensorModel.codeSensiReduce,
deviceIds: devicesIds, value: value,
code: FlushMountedPresenceSensorModel.codePresenceDelay, ),
value: (value * 10).toInt(), ),
),
),
), ),
PresenceUpdateData( PresenceUpdateData(
value: ((model.noneDelay / 10).toDouble()), value: ((model.noneDelay / 10).toDouble()),

View File

@ -15,7 +15,7 @@ import 'package:syncrow_web/utils/helpers/responsice_layout_helper/responsive_la
class FlushMountedPresenceSensorControlView extends StatelessWidget class FlushMountedPresenceSensorControlView extends StatelessWidget
with HelperResponsiveLayout { with HelperResponsiveLayout {
const FlushMountedPresenceSensorControlView({required this.device, super.key}); const FlushMountedPresenceSensorControlView({super.key, required this.device});
final AllDevicesModel device; final AllDevicesModel device;
@ -113,7 +113,7 @@ class FlushMountedPresenceSensorControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.nearDetection / 100).clamp(0.0, double.infinity), value: (model.nearDetection / 100).toDouble(),
title: 'Nearest Detect Dist:', title: 'Nearest Detect Dist:',
description: 'm', description: 'm',
minValue: 0.0, minValue: 0.0,
@ -129,7 +129,7 @@ class FlushMountedPresenceSensorControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.farDetection / 100).clamp(0.0, double.infinity), value: (model.farDetection / 100).toDouble(),
title: 'Max Detect Dist:', title: 'Max Detect Dist:',
description: 'm', description: 'm',
minValue: 0.0, minValue: 0.0,
@ -145,20 +145,20 @@ class FlushMountedPresenceSensorControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: model.sensiReduce.toDouble(), value: (model.presenceDelay.toDouble()),
title: 'Trigger Level:', title: 'Trigger Level:',
minValue: 0, minValue: 0,
maxValue: 3, maxValue: 3,
steps: 1, steps: 1,
action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add( action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
FlushMountedPresenceSensorChangeValueEvent( FlushMountedPresenceSensorChangeValueEvent(
code: FlushMountedPresenceSensorModel.codeSensiReduce, code: FlushMountedPresenceSensorModel.codePresenceDelay,
value: value, value: value,
), ),
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: model.occurDistReduce.toDouble(), value: (model.occurDistReduce.toDouble()),
title: 'Indent Level:', title: 'Indent Level:',
minValue: 0, minValue: 0,
maxValue: 3, maxValue: 3,
@ -171,23 +171,21 @@ class FlushMountedPresenceSensorControlView extends StatelessWidget
), ),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.presenceDelay / 10).toDouble(), value: (model.sensiReduce.toDouble()),
valuesPercision: 1,
title: 'Target Confirm Time:', title: 'Target Confirm Time:',
description: 's', description: 's',
minValue: 0.0, minValue: 0,
maxValue: 0.5, maxValue: 3,
steps: 0.1, steps: 1,
action: (double value) => action: (int value) => context.read<FlushMountedPresenceSensorBloc>().add(
context.read<FlushMountedPresenceSensorBloc>().add( FlushMountedPresenceSensorChangeValueEvent(
FlushMountedPresenceSensorChangeValueEvent( code: FlushMountedPresenceSensorModel.codeSensiReduce,
code: FlushMountedPresenceSensorModel.codePresenceDelay, value: value,
value: (value * 10).toInt(), ),
), ),
),
), ),
PresenceUpdateData( PresenceUpdateData(
value: (model.noneDelay / 10).toDouble(), value: ((model.noneDelay / 10).toDouble()),
description: 's', description: 's',
title: 'Disappe Delay:', title: 'Disappe Delay:',
minValue: 20, minValue: 20,

View File

@ -1,4 +1,6 @@
// Flutter imports // Flutter imports
import 'dart:math';
import 'package:flutter/material.dart'; import 'package:flutter/material.dart';
import 'package:flutter_bloc/flutter_bloc.dart'; import 'package:flutter_bloc/flutter_bloc.dart';
@ -336,6 +338,7 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
} }
spaces.add(newSpace); spaces.add(newSpace);
_updateNodePosition(newSpace, newSpace.position); _updateNodePosition(newSpace, newSpace.position);
realignTree();
}); });
}, },
); );
@ -450,7 +453,6 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
void _saveSpaces() { void _saveSpaces() {
if (widget.selectedCommunity == null) { if (widget.selectedCommunity == null) {
debugPrint("No community selected for saving spaces.");
return; return;
} }
@ -530,35 +532,83 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
} }
Offset getBalancedChildPosition(SpaceModel parent) { Offset getBalancedChildPosition(SpaceModel parent) {
int totalSiblings = parent.children.length + 1; const double nodeWidth = 200;
double totalWidth = (totalSiblings - 1) * 250; // Horizontal spacing const double verticalGap = 180;
double startX = parent.position.dx - (totalWidth / 2);
Offset position = Offset(startX + (parent.children.length * 250), parent.position.dy + 180); if (parent.children.isEmpty) {
// First child → exactly center
return Offset(parent.position.dx, parent.position.dy + verticalGap);
} else {
// More children → arrange them spaced horizontally
double totalWidth = (parent.children.length) * (nodeWidth + 60);
double startX = parent.position.dx - (totalWidth / 2);
// Check for overlaps & adjust double childX = startX + (parent.children.length * (nodeWidth + 60));
while (spaces.any((s) => (s.position - position).distance < 250)) { return Offset(childX, parent.position.dy + verticalGap);
position = Offset(position.dx + 250, position.dy);
} }
return position;
} }
void realignTree() { void realignTree() {
void updatePositions(SpaceModel node, double x, double y) { const double nodeWidth = 200;
node.position = Offset(x, y); const double nodeHeight = 100;
const double horizontalGap = 60;
const double verticalGap = 180;
const double rootGap = 400; // extra space between different roots
int numChildren = node.children.length; double canvasRightEdge = 1000;
double childStartX = x - ((numChildren - 1) * 250) / 2; double canvasBottomEdge = 1000;
for (int i = 0; i < numChildren; i++) { double calculateSubtreeWidth(SpaceModel node) {
updatePositions(node.children[i], childStartX + (i * 250), y + 180); if (node.children.isEmpty) return nodeWidth;
double totalWidth = 0;
for (var child in node.children) {
totalWidth += calculateSubtreeWidth(child) + horizontalGap;
}
return totalWidth - horizontalGap;
}
void layoutSubtree(SpaceModel node, double startX, double y) {
double subtreeWidth = calculateSubtreeWidth(node);
double centerX = startX + subtreeWidth / 2 - nodeWidth / 2;
node.position = Offset(centerX, y);
canvasRightEdge = max(canvasRightEdge, centerX + nodeWidth);
canvasBottomEdge = max(canvasBottomEdge, y + nodeHeight);
if (node.children.length == 1) {
final child = node.children.first;
layoutSubtree(child, centerX, y + verticalGap);
} else {
double childX = startX;
for (var child in node.children) {
double childWidth = calculateSubtreeWidth(child);
layoutSubtree(child, childX, y + verticalGap);
childX += childWidth + horizontalGap;
}
} }
} }
if (spaces.isNotEmpty) { // ⚡ New: layout each root separately
updatePositions(spaces.first, spaces.first.position.dx, spaces.first.position.dy); final List<SpaceModel> roots = spaces
.where((s) =>
s.parent == null &&
s.status != SpaceStatus.deleted &&
s.status != SpaceStatus.parentDeleted)
.toList();
double currentX = 100; // start some margin from left
double currentY = 100; // top margin
for (var root in roots) {
layoutSubtree(root, currentX, currentY);
double rootWidth = calculateSubtreeWidth(root);
currentX += rootWidth + rootGap;
} }
setState(() {
canvasWidth = canvasRightEdge + 400;
canvasHeight = canvasBottomEdge + 400;
});
} }
void _onDuplicate(BuildContext parentContext) { void _onDuplicate(BuildContext parentContext) {
@ -642,63 +692,19 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
} }
void _duplicateSpace(SpaceModel space) { void _duplicateSpace(SpaceModel space) {
final Map<SpaceModel, SpaceModel> originalToDuplicate = {}; final double horizontalGap = 250.0;
double horizontalGap = 250.0; // Increased spacing final double verticalGap = 180.0;
double verticalGap = 180.0; // Adjusted for better visualization final double nodeWidth = 200;
final double nodeHeight = 100;
final double breathingSpace = 300.0; // extra gap after original tree
print("🟢 Duplicating: ${space.name}"); /// Helper to recursively duplicate a node and its children
/// **Find a new position ensuring no overlap**
Offset getBalancedChildPosition(SpaceModel parent) {
int totalSiblings = parent.children.length + 1;
double totalWidth = (totalSiblings - 1) * horizontalGap;
double startX = parent.position.dx - (totalWidth / 2);
Offset position = Offset(
startX + (parent.children.length * horizontalGap), parent.position.dy + verticalGap);
// **Check for overlaps & adjust**
while (spaces.any((s) => (s.position - position).distance < horizontalGap)) {
position = Offset(position.dx + horizontalGap, position.dy);
}
print("🔹 New position for ${parent.name}: (${position.dx}, ${position.dy})");
return position;
}
/// **Realign the entire tree after duplication**
void realignTree() {
void updatePositions(SpaceModel node, double x, double y) {
node.position = Offset(x, y);
print("✅ Adjusted ${node.name} to (${x}, ${y})");
int numChildren = node.children.length;
double childStartX = x - ((numChildren - 1) * horizontalGap) / 2;
for (int i = 0; i < numChildren; i++) {
updatePositions(node.children[i], childStartX + (i * horizontalGap), y + verticalGap);
}
}
if (spaces.isNotEmpty) {
print("🔄 Realigning tree...");
updatePositions(spaces.first, spaces.first.position.dx, spaces.first.position.dy);
}
}
/// **Recursive duplication logic**
SpaceModel duplicateRecursive(SpaceModel original, SpaceModel? duplicatedParent) { SpaceModel duplicateRecursive(SpaceModel original, SpaceModel? duplicatedParent) {
Offset newPosition = duplicatedParent == null
? Offset(original.position.dx + horizontalGap, original.position.dy)
: getBalancedChildPosition(duplicatedParent);
final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces); final duplicatedName = SpaceHelper.generateUniqueSpaceName(original.name, spaces);
print(
"🟡 Duplicating ${original.name}${duplicatedName} at (${newPosition.dx}, ${newPosition.dy})");
final duplicated = SpaceModel( final duplicated = SpaceModel(
name: duplicatedName, name: duplicatedName,
icon: original.icon, icon: original.icon,
position: newPosition, position: Offset.zero,
isPrivate: original.isPrivate, isPrivate: original.isPrivate,
children: [], children: [],
status: SpaceStatus.newSpace, status: SpaceStatus.newSpace,
@ -708,28 +714,20 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
tags: original.tags, tags: original.tags,
); );
setState(() { spaces.add(duplicated);
spaces.add(duplicated);
_updateNodePosition(duplicated, duplicated.position);
if (duplicatedParent != null) { if (duplicatedParent != null) {
final newConnection = Connection( final newConnection = Connection(
startSpace: duplicatedParent, startSpace: duplicatedParent,
endSpace: duplicated, endSpace: duplicated,
direction: "down", direction: "down",
); );
connections.add(newConnection); connections.add(newConnection);
duplicated.incomingConnection = newConnection; duplicated.incomingConnection = newConnection;
duplicatedParent.addOutgoingConnection(newConnection); duplicatedParent.addOutgoingConnection(newConnection);
duplicatedParent.children.add(duplicated); duplicatedParent.children.add(duplicated);
print("🔗 Created connection: ${duplicatedParent.name}${duplicated.name}"); }
}
// **Recalculate the whole tree to avoid overlaps**
realignTree();
});
// Recursively duplicate children
for (var child in original.children) { for (var child in original.children) {
duplicateRecursive(child, duplicated); duplicateRecursive(child, duplicated);
} }
@ -737,21 +735,49 @@ class _CommunityStructureAreaState extends State<CommunityStructureArea> {
return duplicated; return duplicated;
} }
/// **Handle root duplication** /// Layout a subtree rooted at node
if (space.parent == null) { void layoutSubtree(SpaceModel node, double startX, double startY) {
print("🟠 Duplicating root node: ${space.name}"); double calculateSubtreeWidth(SpaceModel n) {
SpaceModel duplicatedRoot = duplicateRecursive(space, null); if (n.children.isEmpty) return nodeWidth;
double width = 0;
for (var child in n.children) {
width += calculateSubtreeWidth(child) + horizontalGap;
}
return width - horizontalGap;
}
setState(() { void assignPositions(SpaceModel n, double x, double y) {
spaces.add(duplicatedRoot); double subtreeWidth = calculateSubtreeWidth(n);
realignTree(); double centerX = x + subtreeWidth / 2 - nodeWidth / 2;
}); n.position = Offset(centerX, y);
print("✅ Root duplication successful: ${duplicatedRoot.name}"); if (n.children.length == 1) {
} else { assignPositions(n.children.first, centerX, y + verticalGap);
duplicateRecursive(space, space.parent); } else {
double childX = x;
for (var child in n.children) {
double childWidth = calculateSubtreeWidth(child);
assignPositions(child, childX, y + verticalGap);
childX += childWidth + horizontalGap;
}
}
}
double totalSubtreeWidth = calculateSubtreeWidth(node);
assignPositions(node, startX, startY);
} }
print("🟢 Finished duplication process for: ${space.name}"); /// Actual duplication process
setState(() {
if (space.parent == null) {
// Duplicating a ROOT node
SpaceModel duplicatedRoot = duplicateRecursive(space, null);
realignTree();
} else {
// Duplicating a CHILD node inside its parent
SpaceModel duplicated = duplicateRecursive(space, space.parent);
realignTree();
}
});
} }
} }

View File

@ -46,14 +46,14 @@ final class DebouncedBatchControlDevicesService
final BatchControlDevicesService decoratee; final BatchControlDevicesService decoratee;
final Duration debounceDuration; final Duration debounceDuration;
DebouncedBatchControlDevicesService({
required this.decoratee,
this.debounceDuration = const Duration(milliseconds: 1500),
});
final _pendingRequests = <(List<String> uuids, String code, Object value)>[]; final _pendingRequests = <(List<String> uuids, String code, Object value)>[];
var _isProcessing = false; var _isProcessing = false;
DebouncedBatchControlDevicesService({
required this.decoratee,
this.debounceDuration = const Duration(milliseconds: 800),
});
@override @override
Future<bool> batchControlDevices({ Future<bool> batchControlDevices({
required List<String> uuids, required List<String> uuids,
@ -68,26 +68,16 @@ final class DebouncedBatchControlDevicesService
await Future.delayed(debounceDuration); await Future.delayed(debounceDuration);
final groupedRequests = final lastRequest = _pendingRequests.last;
<String, (List<String> uuids, String code, Object value)>{};
for (final request in _pendingRequests) {
final (_, requestCode, requestValue) = request;
groupedRequests[requestCode] = request;
}
_pendingRequests.clear(); _pendingRequests.clear();
try { try {
var allSuccessful = true; final (lastRequestUuids, lastRequestCode, lastRequestValue) = lastRequest;
for (final request in groupedRequests.values) { return decoratee.batchControlDevices(
final (lastRequestUuids, lastRequestCode, lastRequestValue) = request; uuids: lastRequestUuids,
final success = await decoratee.batchControlDevices( code: lastRequestCode,
uuids: lastRequestUuids, value: lastRequestValue,
code: lastRequestCode, );
value: lastRequestValue,
);
if (!success) allSuccessful = false;
}
return allSuccessful;
} finally { } finally {
_isProcessing = false; _isProcessing = false;
} }

View File

@ -40,7 +40,7 @@ final class DebouncedControlDeviceService implements ControlDeviceService {
DebouncedControlDeviceService({ DebouncedControlDeviceService({
required this.decoratee, required this.decoratee,
this.debounceDuration = const Duration(milliseconds: 1500), this.debounceDuration = const Duration(milliseconds: 800),
}); });
final _pendingRequests = <(String deviceUuid, Status status)>[]; final _pendingRequests = <(String deviceUuid, Status status)>[];
@ -59,24 +59,15 @@ final class DebouncedControlDeviceService implements ControlDeviceService {
await Future.delayed(debounceDuration); await Future.delayed(debounceDuration);
final groupedRequests = <String, (String deviceUuid, Status status)>{}; final lastRequest = _pendingRequests.last;
for (final request in _pendingRequests) {
final (_, requestStatus) = request;
groupedRequests[requestStatus.code] = request;
}
_pendingRequests.clear(); _pendingRequests.clear();
try { try {
var allSuccessful = true; final (lastRequestDeviceUuid, lastRequestStatus) = lastRequest;
for (final request in groupedRequests.values) { return decoratee.controlDevice(
final (lastRequestDeviceUuid, lastRequestStatus) = request; deviceUuid: lastRequestDeviceUuid,
final success = await decoratee.controlDevice( status: lastRequestStatus,
deviceUuid: lastRequestDeviceUuid, );
status: lastRequestStatus,
);
if (!success) allSuccessful = false;
}
return allSuccessful;
} finally { } finally {
_isProcessing = false; _isProcessing = false;
} }