feat: implement a more complete floor plan webview impl
This commit is contained in:
@@ -0,0 +1,599 @@
|
||||
/* globals: Konva, preact, preactHooks, htm */
|
||||
const { h, render } = preact;
|
||||
const { useState } = preactHooks;
|
||||
const html = htm.bind(h);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Constants & mutable state
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const PPM = 60; // pixels per meter
|
||||
|
||||
let rooms = []; // [{id, name, x, y, width, height}]
|
||||
let sensors = []; // [{id, sensor_id, name?, floor_x, floor_y, room_id}]
|
||||
let mode = 'view';
|
||||
|
||||
let _fittedOnce = false;
|
||||
let _dragging = false; // true while any room group is being dragged
|
||||
|
||||
// Room currently showing dimension overlay (follows during drag).
|
||||
let _dimRoom = null;
|
||||
let _dimGroup = null;
|
||||
|
||||
// Exposed so the Preact overlay can update its tooltip imperatively.
|
||||
let _setTooltip = (_v) => {};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Stage & layers
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const stage = new Konva.Stage({
|
||||
container: 'canvas-container',
|
||||
width: window.innerWidth,
|
||||
height: window.innerHeight,
|
||||
});
|
||||
stage.draggable(true);
|
||||
Konva.hitOnDragEnabled = true;
|
||||
|
||||
const roomsLayer = new Konva.Layer();
|
||||
const dimensionsLayer = new Konva.Layer();
|
||||
const sensorsLayer = new Konva.Layer();
|
||||
const tagsLayer = new Konva.Layer();
|
||||
const particlesLayer = new Konva.Layer();
|
||||
stage.add(roomsLayer, dimensionsLayer, sensorsLayer, tagsLayer, particlesLayer);
|
||||
|
||||
new ResizeObserver(() => {
|
||||
stage.width(window.innerWidth);
|
||||
stage.height(window.innerHeight);
|
||||
}).observe(document.body);
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Pinch-to-zoom & mouse wheel zoom
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
let _lastPinchDist = 0;
|
||||
|
||||
stage.on('touchmove', (e) => {
|
||||
e.evt.preventDefault();
|
||||
const touches = e.evt.touches;
|
||||
if (touches.length !== 2) return;
|
||||
stage.stopDrag();
|
||||
const dist = Math.hypot(
|
||||
touches[0].clientX - touches[1].clientX,
|
||||
touches[0].clientY - touches[1].clientY,
|
||||
);
|
||||
if (_lastPinchDist === 0) { _lastPinchDist = dist; return; }
|
||||
_applyZoom(
|
||||
{ x: (touches[0].clientX + touches[1].clientX) / 2,
|
||||
y: (touches[0].clientY + touches[1].clientY) / 2 },
|
||||
dist / _lastPinchDist,
|
||||
);
|
||||
_lastPinchDist = dist;
|
||||
});
|
||||
stage.on('touchend touchcancel', () => { _lastPinchDist = 0; });
|
||||
|
||||
stage.on('wheel', (e) => {
|
||||
e.evt.preventDefault();
|
||||
_applyZoom(stage.getPointerPosition(), e.evt.deltaY < 0 ? 1.06 : 1 / 1.06);
|
||||
});
|
||||
|
||||
function _applyZoom(center, factor) {
|
||||
const oldScale = stage.scaleX();
|
||||
const newScale = Math.max(0.1, Math.min(20, oldScale * factor));
|
||||
const anchorX = (center.x - stage.x()) / oldScale;
|
||||
const anchorY = (center.y - stage.y()) / oldScale;
|
||||
stage.scale({ x: newScale, y: newScale });
|
||||
stage.position({ x: center.x - anchorX * newScale, y: center.y - anchorY * newScale });
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Room rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderRooms() {
|
||||
if (_dragging) return; // never destroy groups while a drag is live
|
||||
roomsLayer.destroyChildren();
|
||||
rooms.forEach(_buildRoomGroup);
|
||||
roomsLayer.batchDraw();
|
||||
}
|
||||
|
||||
function _buildRoomGroup(room) {
|
||||
const rw = room.width * PPM;
|
||||
const rh = room.height * PPM;
|
||||
|
||||
const group = new Konva.Group({
|
||||
id: `room-${room.id}`,
|
||||
x: room.x * PPM,
|
||||
y: room.y * PPM,
|
||||
draggable: mode === 'edit',
|
||||
});
|
||||
|
||||
// Floor area
|
||||
group.add(new Konva.Rect({
|
||||
width: rw, height: rh,
|
||||
fill: '#ffffff',
|
||||
stroke: '#444444',
|
||||
strokeWidth: 1.5,
|
||||
}));
|
||||
|
||||
// Room label
|
||||
group.add(new Konva.Text({
|
||||
width: rw, height: rh,
|
||||
text: room.name,
|
||||
fontSize: 12,
|
||||
fill: 'rgba(0,0,0,0.45)',
|
||||
align: 'center',
|
||||
verticalAlign: 'middle',
|
||||
letterSpacing: 0.8,
|
||||
listening: false,
|
||||
}));
|
||||
|
||||
group.on('click tap', () => {
|
||||
if (mode === 'edit') {
|
||||
_dimRoom = room;
|
||||
_dimGroup = group;
|
||||
_showDimensions(room, group.x(), group.y());
|
||||
}
|
||||
});
|
||||
|
||||
group.on('dragstart', () => {
|
||||
_dragging = true;
|
||||
if (mode === 'edit') {
|
||||
_dimRoom = room;
|
||||
_dimGroup = group;
|
||||
_showDimensions(room, group.x(), group.y());
|
||||
}
|
||||
});
|
||||
|
||||
group.on('dragend', () => { _dragging = false; _onRoomDragEnd(room, group); });
|
||||
roomsLayer.add(group);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Room edge snapping during drag
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
const SNAP_THRESHOLD = 10; // screen pixels; scaled by stage zoom for layer coords
|
||||
|
||||
roomsLayer.on('dragmove', (e) => {
|
||||
if (mode !== 'edit') return;
|
||||
const node = e.target;
|
||||
if (!node.id().startsWith('room-')) return;
|
||||
const roomId = parseInt(node.id().replace('room-', ''), 10);
|
||||
const draggedRoom = rooms.find(r => r.id === roomId);
|
||||
if (!draggedRoom) return;
|
||||
|
||||
const threshold = SNAP_THRESHOLD / stage.scaleX();
|
||||
const x = node.x(), y = node.y();
|
||||
const w = draggedRoom.width * PPM, h = draggedRoom.height * PPM;
|
||||
|
||||
let bestX = null, bestY = null, dX = threshold, dY = threshold;
|
||||
|
||||
rooms.forEach(r => {
|
||||
if (r.id === draggedRoom.id) return;
|
||||
const rx = r.x * PPM, ry = r.y * PPM;
|
||||
const rw = r.width * PPM, rh = r.height * PPM;
|
||||
|
||||
// Four X snap candidates: left↔left, right↔right, right→left, left←right
|
||||
[[x, rx], [x + w, rx + rw], [x + w, rx], [x, rx + rw]].forEach(([a, b]) => {
|
||||
const d = Math.abs(a - b);
|
||||
if (d < dX) { dX = d; bestX = x + (b - a); }
|
||||
});
|
||||
|
||||
// Four Y snap candidates
|
||||
[[y, ry], [y + h, ry + rh], [y + h, ry], [y, ry + rh]].forEach(([a, b]) => {
|
||||
const d = Math.abs(a - b);
|
||||
if (d < dY) { dY = d; bestY = y + (b - a); }
|
||||
});
|
||||
});
|
||||
|
||||
if (bestX !== null) node.x(bestX);
|
||||
if (bestY !== null) node.y(bestY);
|
||||
|
||||
if (_dimRoom && _dimRoom.id === draggedRoom.id) {
|
||||
_showDimensions(_dimRoom, node.x(), node.y());
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Dimension overlay (tap-to-show in edit mode)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function _showDimensions(room, lx, ly) {
|
||||
dimensionsLayer.destroyChildren();
|
||||
const c = '#1565C0';
|
||||
const DIM = 26;
|
||||
const EXT = 5;
|
||||
const rx = lx;
|
||||
const ry = ly;
|
||||
const rw = room.width * PPM;
|
||||
const rh = room.height * PPM;
|
||||
|
||||
// Determine which outer sides are free from adjacent rooms (use stored positions).
|
||||
let widthBelow = true;
|
||||
let heightRight = true;
|
||||
rooms.forEach(r => {
|
||||
if (r.id === room.id) return;
|
||||
if (Math.abs(r.y * PPM - (ry + rh)) < DIM + EXT + 4) widthBelow = false;
|
||||
if (Math.abs(r.x * PPM - (rx + rw)) < DIM + EXT + 4) heightRight = false;
|
||||
});
|
||||
|
||||
const arrowCfg = {
|
||||
stroke: c, fill: c,
|
||||
strokeWidth: 1,
|
||||
pointerLength: 5, pointerWidth: 4,
|
||||
pointerAtBeginning: true,
|
||||
listening: false,
|
||||
};
|
||||
|
||||
// Width dimension
|
||||
if (widthBelow) {
|
||||
const wy = ry + rh + DIM;
|
||||
dimensionsLayer.add(new Konva.Arrow({ ...arrowCfg, points: [rx, wy, rx + rw, wy] }));
|
||||
dimensionsLayer.add(new Konva.Line({ points: [rx, ry + rh, rx, wy + EXT], stroke: c, strokeWidth: 0.8, listening: false }));
|
||||
dimensionsLayer.add(new Konva.Line({ points: [rx + rw, ry + rh, rx + rw, wy + EXT], stroke: c, strokeWidth: 0.8, listening: false }));
|
||||
dimensionsLayer.add(new Konva.Text({
|
||||
x: rx + rw / 2 - 20, y: wy - 13, width: 40,
|
||||
text: `${room.width.toFixed(1)} m`,
|
||||
fontSize: 9, fill: c, align: 'center', listening: false,
|
||||
}));
|
||||
} else {
|
||||
// Inside the room near the bottom edge
|
||||
const wy = ry + rh - 18;
|
||||
dimensionsLayer.add(new Konva.Arrow({ ...arrowCfg, points: [rx + 4, wy, rx + rw - 4, wy] }));
|
||||
dimensionsLayer.add(new Konva.Text({
|
||||
x: rx + rw / 2 - 20, y: wy - 12, width: 40,
|
||||
text: `${room.width.toFixed(1)} m`,
|
||||
fontSize: 9, fill: c, align: 'center', listening: false,
|
||||
}));
|
||||
}
|
||||
|
||||
// Height dimension
|
||||
if (heightRight) {
|
||||
const hx = rx + rw + DIM;
|
||||
dimensionsLayer.add(new Konva.Arrow({ ...arrowCfg, points: [hx, ry, hx, ry + rh] }));
|
||||
dimensionsLayer.add(new Konva.Line({ points: [rx + rw, ry, hx + EXT, ry], stroke: c, strokeWidth: 0.8, listening: false }));
|
||||
dimensionsLayer.add(new Konva.Line({ points: [rx + rw, ry + rh, hx + EXT, ry + rh], stroke: c, strokeWidth: 0.8, listening: false }));
|
||||
dimensionsLayer.add(new Konva.Text({
|
||||
x: hx + 4, y: ry + rh / 2 - 4,
|
||||
text: `${room.height.toFixed(1)} m`,
|
||||
fontSize: 9, fill: c, listening: false,
|
||||
}));
|
||||
} else {
|
||||
// Inside the room near the right edge
|
||||
const hx = rx + rw - 18;
|
||||
dimensionsLayer.add(new Konva.Arrow({ ...arrowCfg, points: [hx, ry + 4, hx, ry + rh - 4] }));
|
||||
dimensionsLayer.add(new Konva.Text({
|
||||
x: hx - 22, y: ry + rh / 2 - 4,
|
||||
text: `${room.height.toFixed(1)} m`,
|
||||
fontSize: 9, fill: c, listening: false,
|
||||
}));
|
||||
}
|
||||
|
||||
dimensionsLayer.batchDraw();
|
||||
}
|
||||
|
||||
function _clearDimensions() {
|
||||
_dimRoom = null;
|
||||
_dimGroup = null;
|
||||
dimensionsLayer.destroyChildren();
|
||||
dimensionsLayer.batchDraw();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Room drag end
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function _onRoomDragEnd(room, group) {
|
||||
room.x = group.x() / PPM;
|
||||
room.y = group.y() / PPM;
|
||||
|
||||
// Normalize: shift all rooms so the top-left of the bounding box is (0,0).
|
||||
const minX = Math.min(...rooms.map(r => r.x));
|
||||
const minY = Math.min(...rooms.map(r => r.y));
|
||||
if (minX !== 0 || minY !== 0) {
|
||||
rooms.forEach(r => { r.x -= minX; r.y -= minY; });
|
||||
rooms.forEach(r => {
|
||||
const g = roomsLayer.findOne(`#room-${r.id}`);
|
||||
if (g) { g.x(r.x * PPM); g.y(r.y * PPM); }
|
||||
});
|
||||
}
|
||||
|
||||
// Keep dimensions visible at the final position after normalization.
|
||||
if (_dimRoom && _dimRoom.id === room.id) {
|
||||
_showDimensions(room, group.x(), group.y());
|
||||
}
|
||||
|
||||
notifyFlutter({
|
||||
type: 'roomsUpdated',
|
||||
rooms,
|
||||
});
|
||||
roomsLayer.batchDraw();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Sensor rendering
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderSensors() {
|
||||
sensorsLayer.destroyChildren();
|
||||
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 label = sensor.name ?? sensor.sensor_id;
|
||||
|
||||
const group = new Konva.Group({
|
||||
id: `sensor-${sensor.id}`,
|
||||
x: absX, y: absY,
|
||||
draggable: mode === 'edit',
|
||||
});
|
||||
|
||||
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', () => _onSensorTap(sensor, group, label));
|
||||
group.on('dragend', () => _onSensorDragEnd(sensor, group, room));
|
||||
sensorsLayer.add(group);
|
||||
});
|
||||
sensorsLayer.batchDraw();
|
||||
}
|
||||
|
||||
function _onSensorTap(sensor, group, label) {
|
||||
if (mode === 'view') {
|
||||
const scale = stage.scaleX();
|
||||
_setTooltip({
|
||||
id: String(sensor.id),
|
||||
name: label,
|
||||
x: group.x() * scale + stage.x(),
|
||||
y: group.y() * scale + stage.y(),
|
||||
});
|
||||
} else {
|
||||
notifyFlutter({ type: 'sensorTapped', id: String(sensor.id) });
|
||||
}
|
||||
}
|
||||
|
||||
function _onSensorDragEnd(sensor, group, originalRoom) {
|
||||
const newAbsX = group.x() / PPM;
|
||||
const newAbsY = group.y() / PPM;
|
||||
const target = rooms.find(r =>
|
||||
newAbsX >= r.x && newAbsX <= r.x + r.width &&
|
||||
newAbsY >= r.y && newAbsY <= r.y + r.height,
|
||||
) ?? originalRoom;
|
||||
|
||||
notifyFlutter({
|
||||
type: 'sensorMoved',
|
||||
id: String(sensor.id),
|
||||
roomId: target.id,
|
||||
x: newAbsX - target.x,
|
||||
y: newAbsY - target.y,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tag rendering (chips grouped by room)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function renderTags(tags) {
|
||||
tagsLayer.destroyChildren();
|
||||
|
||||
const byRoom = {};
|
||||
tags.forEach(t => {
|
||||
if (t.roomId != null) (byRoom[t.roomId] ??= []).push(t);
|
||||
});
|
||||
|
||||
Object.entries(byRoom).forEach(([roomId, roomTags]) => {
|
||||
const room = rooms.find(r => r.id === Number(roomId));
|
||||
if (!room) return;
|
||||
|
||||
const chipW = 64, chipH = 22, chipGap = 4;
|
||||
const totalW = roomTags.length * chipW + (roomTags.length - 1) * chipGap;
|
||||
const cx = (room.x + room.width / 2) * PPM;
|
||||
const cy = (room.y + room.height / 2) * PPM;
|
||||
|
||||
roomTags.forEach((tag, i) => {
|
||||
const gx = cx - totalW / 2 + i * (chipW + chipGap);
|
||||
const g = new Konva.Group({ x: gx, y: cy - chipH / 2, listening: false });
|
||||
g.add(new Konva.Rect({ width: chipW, height: chipH, cornerRadius: 4, fill: tag.color || '#546E7A' }));
|
||||
g.add(new Konva.Text({
|
||||
width: chipW, height: chipH,
|
||||
text: tag.name,
|
||||
fontSize: 10, fill: '#ffffff',
|
||||
align: 'center', verticalAlign: 'middle',
|
||||
}));
|
||||
tagsLayer.add(g);
|
||||
});
|
||||
});
|
||||
tagsLayer.batchDraw();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Fit stage to show all rooms
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function _fitToRooms() {
|
||||
if (rooms.length === 0) return;
|
||||
const maxX = Math.max(...rooms.map(r => r.x + r.width));
|
||||
const maxY = Math.max(...rooms.map(r => r.y + r.height));
|
||||
const pad = 60; // px padding
|
||||
const scale = Math.min(
|
||||
(stage.width() - pad * 2) / (maxX * PPM),
|
||||
(stage.height() - pad * 2) / (maxY * PPM),
|
||||
2,
|
||||
);
|
||||
stage.scale({ x: scale, y: scale });
|
||||
stage.position({
|
||||
x: (stage.width() - maxX * PPM * scale) / 2,
|
||||
y: (stage.height() - maxY * PPM * scale) / 2,
|
||||
});
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Preact overlay — sensor tooltip
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function Tooltip({ id, name, x, y, onClose }) {
|
||||
return html`
|
||||
<div style=${{
|
||||
position: 'absolute',
|
||||
left: `${x + 14}px`,
|
||||
top: `${y - 48}px`,
|
||||
background: '#ffffff',
|
||||
borderRadius: '8px',
|
||||
padding: '8px 12px',
|
||||
boxShadow: '0 2px 12px rgba(0,0,0,0.18)',
|
||||
pointerEvents: 'auto',
|
||||
minWidth: '130px',
|
||||
fontFamily: 'Roboto, sans-serif',
|
||||
}}>
|
||||
<div style=${{ fontSize: '13px', fontWeight: '500', color: '#212121', marginBottom: '6px' }}>
|
||||
${name}
|
||||
</div>
|
||||
<div style=${{ display: 'flex', gap: '10px' }}>
|
||||
<button
|
||||
style=${{ fontSize: '12px', border: 'none', background: 'none', color: '#1565C0', cursor: 'pointer', padding: 0 }}
|
||||
onClick=${() => { notifyFlutter({ type: 'sensorTapped', id }); onClose(); }}>
|
||||
View details
|
||||
</button>
|
||||
<button
|
||||
style=${{ fontSize: '12px', border: 'none', background: 'none', color: '#9e9e9e', cursor: 'pointer', padding: 0 }}
|
||||
onClick=${onClose}>
|
||||
✕
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
`;
|
||||
}
|
||||
|
||||
function App() {
|
||||
const [tooltip, setTip] = useState(null);
|
||||
_setTooltip = setTip;
|
||||
return tooltip
|
||||
? html`<${Tooltip} ...${tooltip} onClose=${() => setTip(null)} />`
|
||||
: null;
|
||||
}
|
||||
|
||||
render(html`<${App} />`, document.getElementById('overlay'));
|
||||
|
||||
// Dismiss tooltip and dimensions when tapping the stage background.
|
||||
stage.on('click tap', (e) => {
|
||||
if (e.target === stage) {
|
||||
_setTooltip(null);
|
||||
_clearDimensions();
|
||||
}
|
||||
});
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Flutter → JS bridge
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
function notifyFlutter(msg) {
|
||||
if (window.FlutterBridge) FlutterBridge.postMessage(JSON.stringify(msg));
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Public API — called from Flutter via runJavaScript()
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
window.companion = {
|
||||
|
||||
loadFloorPlan(roomsData) {
|
||||
rooms = roomsData;
|
||||
renderRooms();
|
||||
renderSensors();
|
||||
if (!_fittedOnce && rooms.length > 0) {
|
||||
_fitToRooms();
|
||||
_fittedOnce = true;
|
||||
}
|
||||
_clearDimensions();
|
||||
},
|
||||
|
||||
loadSensors(sensorsData) {
|
||||
sensors = sensorsData;
|
||||
renderSensors();
|
||||
},
|
||||
|
||||
updateTags(tags) {
|
||||
renderTags(tags);
|
||||
},
|
||||
|
||||
updateCloud(particles) {
|
||||
particlesLayer.destroyChildren();
|
||||
particles.forEach(({ x, y, weight }) => {
|
||||
particlesLayer.add(new Konva.Circle({
|
||||
x: x * PPM, y: y * PPM,
|
||||
radius: 2,
|
||||
fill: `rgba(255,152,0,${Math.max(0, Math.min(1, weight))})`,
|
||||
listening: false,
|
||||
}));
|
||||
});
|
||||
particlesLayer.batchDraw();
|
||||
},
|
||||
|
||||
setMode(newMode) {
|
||||
mode = newMode;
|
||||
const edit = newMode === 'edit';
|
||||
roomsLayer.find('Group').forEach(g => g.draggable(edit));
|
||||
sensorsLayer.find('Group').forEach(g => g.draggable(edit));
|
||||
tagsLayer.visible(!edit);
|
||||
particlesLayer.visible(!edit);
|
||||
_clearDimensions();
|
||||
_setTooltip(null);
|
||||
stage.batchDraw();
|
||||
},
|
||||
|
||||
highlightSensor(id) {
|
||||
sensorsLayer.find('.highlight-ring').forEach(r => r.visible(false));
|
||||
if (id) {
|
||||
const group = sensorsLayer.findOne(`#sensor-${id}`);
|
||||
if (group) {
|
||||
group.findOne('.highlight-ring')?.visible(true);
|
||||
const s = stage.scaleX();
|
||||
stage.position({
|
||||
x: stage.width() / 2 - group.x() * s,
|
||||
y: stage.height() / 2 - group.y() * s,
|
||||
});
|
||||
}
|
||||
}
|
||||
sensorsLayer.batchDraw();
|
||||
},
|
||||
|
||||
addRoom() {
|
||||
const s = stage.scaleX();
|
||||
const cx = (stage.width() / 2 - stage.x()) / s / PPM;
|
||||
const cy = (stage.height() / 2 - stage.y()) / s / PPM;
|
||||
notifyFlutter({ type: 'roomAdded', x: cx, y: cy });
|
||||
},
|
||||
|
||||
fitToRooms() {
|
||||
_fitToRooms();
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user