diff --git a/assets/konva/app.js b/assets/konva/app.js index d61d948..52645a7 100644 --- a/assets/konva/app.js +++ b/assets/konva/app.js @@ -351,74 +351,109 @@ function _onRoomDragEnd(room, group) { // --------------------------------------------------------------------------- function renderSensors() { - sensorsLayer.destroyChildren(); + const activeIds = new Set( + sensors + .filter(s => s.room_id != null && s.floor_x != null) + .map(s => `sensor-${s.id}`) + ); + + // Remove groups for sensors that are now unplaced or gone. + sensorsLayer.find('Group').forEach(g => { + if (!activeIds.has(g.id())) g.destroy(); + }); + sensors.forEach(sensor => { if (sensor.room_id == null || sensor.floor_x == null) return; const room = rooms.find(r => r.id === sensor.room_id); if (!room) return; - const absX = (room.x + sensor.floor_x) * PPM; - const absY = (room.y + sensor.floor_y) * PPM; + const absX = (room.x + sensor.floor_x) * PPM; + const absY = (room.y + sensor.floor_y) * PPM; const label = sensor.name ?? sensor.sensor_id; - const group = new Konva.Group({ - id: `sensor-${sensor.id}`, - x: absX, y: absY, - draggable: mode === 'sensorMove', - }); + const existing = sensorsLayer.findOne(`#sensor-${sensor.id}`); + if (existing) { + // Update in-place — avoids the destroy-rebuild flash on drag end. + if (!existing.isDragging()) existing.position({ x: absX, y: absY }); + existing.draggable(mode === 'sensorMove'); + existing.findOne('Text')?.text(label); + return; + } - group.add(new Konva.Circle({ - radius: 8, - fill: '#1565C0', - stroke: '#ffffff', - strokeWidth: 2, - })); - - // Highlight ring — shown by highlightSensor(). - group.add(new Konva.Circle({ - name: 'highlight-ring', - radius: 14, - stroke: '#FDD835', - strokeWidth: 3, - visible: false, - listening: false, - })); - - group.add(new Konva.Text({ - text: label, - fontSize: 10, - fill: '#424242', - y: 11, - offsetX: 30, - width: 60, - align: 'center', - listening: false, - })); - - group.on('click tap', () => { - if (mode === 'view') { - notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) }); - } - }); - - let pressTimer = null; - group.on('mousedown touchstart', () => { - if (mode === 'view') return; - pressTimer = setTimeout(() => { - notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) }); - pressTimer = null; - }, 500); - }); - group.on('mouseup touchend touchcancel dragstart', () => { - clearTimeout(pressTimer); - pressTimer = null; - }); - group.on('dragend', () => _onSensorDragEnd(sensor, group, room)); - sensorsLayer.add(group); + _buildSensorGroup(sensor, absX, absY, label); }); sensorsLayer.batchDraw(); } +function _buildSensorGroup(sensor, absX, absY, label) { + const group = new Konva.Group({ + id: `sensor-${sensor.id}`, + x: absX, y: absY, + draggable: mode === 'sensorMove', + }); + + // Visible dot — hitFunc expands the touch target without changing appearance. + group.add(new Konva.Circle({ + radius: 8, + fill: '#1565C0', + stroke: '#ffffff', + strokeWidth: 2, + hitFunc(ctx, shape) { + ctx.beginPath(); + ctx.arc(0, 0, 20, 0, Math.PI * 2, true); + ctx.closePath(); + ctx.fillStrokeShape(shape); + }, + })); + + // Highlight ring — shown by highlightSensor(). + group.add(new Konva.Circle({ + name: 'highlight-ring', + radius: 14, + stroke: '#FDD835', + strokeWidth: 3, + visible: false, + listening: false, + })); + + group.add(new Konva.Text({ + text: label, + fontSize: 10, + fill: '#424242', + y: 11, + offsetX: 30, + width: 60, + align: 'center', + listening: false, + })); + + group.on('click tap', () => { + if (mode === 'view') { + notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) }); + } + }); + + let pressTimer = null; + group.on('mousedown touchstart', () => { + if (mode === 'view') return; + pressTimer = setTimeout(() => { + notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) }); + pressTimer = null; + }, 500); + }); + group.on('mouseup touchend touchcancel dragstart', () => { + clearTimeout(pressTimer); + pressTimer = null; + }); + // Look up the current sensor data at dragend so room_id is always fresh. + group.on('dragend', () => { + const s = sensors.find(ss => ss.id === sensor.id) ?? sensor; + const originalRoom = rooms.find(r => r.id === s.room_id) ?? rooms[0]; + _onSensorDragEnd(s, group, originalRoom); + }); + sensorsLayer.add(group); +} + function _onSensorDragEnd(sensor, group, originalRoom) { const newAbsX = group.x() / PPM; const newAbsY = group.y() / PPM; @@ -427,12 +462,19 @@ function _onSensorDragEnd(sensor, group, originalRoom) { newAbsY >= r.y && newAbsY <= r.y + r.height, ) ?? originalRoom; + // Optimistically update the local sensors array so any renderSensors() + // call that arrives before Flutter confirms the new position via loadSensors + // doesn't snap the group back to the old coordinates. + sensor.room_id = target.id; + sensor.floor_x = newAbsX - target.x; + sensor.floor_y = newAbsY - target.y; + notifyFlutter({ type: 'sensorMoved', id: String(sensor.id), roomId: target.id, - x: newAbsX - target.x, - y: newAbsY - target.y, + x: sensor.floor_x, + y: sensor.floor_y, }); }