require("../../_virtual/_rolldown/runtime.js");
const require_objectSpread2 = require("../../_virtual/_@oxc-project_runtime@0.122.0/helpers/objectSpread2.js");
const require_topologyLocale = require("./topologyLocale.js");
const require_topologyFloorPlanConstants = require("./topologyFloorPlanConstants.js");
let _pisell_materials = require("@pisell/materials");
//#region src/plus/pisellTopology/topologyDeviceListView.ts
/** 自动布局格点尺寸（内容坐标 px）；与 TopologyDeviceCard 紧凑布局对齐 */
const NODE_WIDTH = 132;
const NODE_HEIGHT = 72;
/** 同层水平间距：留得宽，避免多条连线挤在节点之间、标签互相重叠 */
const NODE_GAP = 56;
/** 同 cluster 内层与层之间的垂直距离（略大于节点高，给总线留空即可） */
const LAYER_GAP_Y = 56;
/** cluster 与 cluster（包括孤立点网格）之间的间距 */
const CLUSTER_GAP = 56;
/** 单行排布最大宽度，超过就换行（单位 px，按内容坐标） */
const CLUSTER_ROW_MAX_WIDTH = 1400;
const PADDING = 40;
/** 分区（选区虚线框）内部边距：设备布局块到虚线框四边的留白 */
const ZONE_INNER_PADDING = 32;
/** 分区顶部额外留白：给选区标题（骑在上边框）让位，避免压住首行设备 */
const ZONE_TITLE_TOP = 20;
/** 分区与分区之间的间距（横向 / 换行后纵向）；偏小，让分区排布更紧凑 */
const GROUP_GAP = 32;
const DEVICE_CATEGORY_ID = "devices";
/**
* 静态 `type_code` 枚举：拓扑图所有可识别的设备类型。
* 接口返回里若出现未列入的 `type_code`，会落到 `<prefix>unknown` 兜底 kind。
*/
const TOPOLOGY_DEVICE_TYPE_CODES = [
	"pos",
	"receipt_printer",
	"label_printer",
	"switch",
	"info_display",
	"terminal",
	"chenlei",
	"test1"
];
/** type_code → 默认中文 label（label 主要用于 elementKinds palette / 调试，非业务展示） */
const TYPE_CODE_LABEL = {
	pos: "POS",
	receipt_printer: "小票打印机",
	label_printer: "标签打印机",
	switch: "交换机",
	info_display: "显示屏",
	terminal: "终端",
	chenlei: "chenlei",
	test1: "test1"
};
function typeCodeToKindValue(code) {
	return `${require_topologyFloorPlanConstants.TOPOLOGY_DEVICE_KIND_PREFIX}${code}`;
}
/** 拓扑设备所有 kind value（含 unknown 兜底） */
const TOPOLOGY_DEVICE_KIND_VALUES = [...TOPOLOGY_DEVICE_TYPE_CODES.map(typeCodeToKindValue), `${require_topologyFloorPlanConstants.TOPOLOGY_DEVICE_KIND_PREFIX}unknown`];
/** 从 HTTP 封装响应中提取 `data` 数组（兼容 `{data: [...]}` / 直接数组） */
function extractRawDeviceList(response) {
	if (Array.isArray(response)) return response;
	if (response && typeof response === "object") {
		const arr = response.data;
		if (Array.isArray(arr)) return arr;
	}
	return [];
}
/** 从 HTTP 封装响应中提取设备连接 `data` 数组 */
function extractRawDeviceConnectionList(response) {
	if (Array.isArray(response)) return response;
	if (response && typeof response === "object") {
		const arr = response.data;
		if (Array.isArray(arr)) return arr;
	}
	return [];
}
/** 多语言名称取值：优先 zh-CN → zh-HK → en → original → 兜底 */
function pickLangText(source, fallback) {
	if (typeof source === "string") return source.trim() || fallback;
	if (!source || typeof source !== "object") return fallback;
	const candidates = [
		source["zh-CN"],
		source["zh-HK"],
		source.en,
		source.original
	];
	for (const v of candidates) if (typeof v === "string" && v.trim()) return v.trim();
	return fallback;
}
function deviceDisplayName(device) {
	var _ref, _ref2, _pickLangText, _device$model;
	return (_ref = (_ref2 = (_pickLangText = pickLangText(device.name, void 0)) !== null && _pickLangText !== void 0 ? _pickLangText : pickLangText((_device$model = device.model) === null || _device$model === void 0 ? void 0 : _device$model.name, void 0)) !== null && _ref2 !== void 0 ? _ref2 : device.number) !== null && _ref !== void 0 ? _ref : `#${device.id}`;
}
/**
* 设备 IP：与 devicePlanning 详情「网络 → 本地 IP」同源（`data.network.local_ip`）。
*/
function deviceIpAddress(device) {
	const data = device.data;
	if (data && typeof data === "object") {
		const network = data.network;
		if (network && typeof network === "object" && !Array.isArray(network)) {
			const lip = network.local_ip;
			if (typeof lip === "string" && lip.trim()) return lip.trim();
		}
	}
}
function deviceStatus(device) {
	switch (device.status) {
		case "online": return "online";
		case "offline": return "offline";
		case "warning": return "warning";
		default: return "unknown";
	}
}
/** 设备数组 → `dataSources.devices`（PisellFloorMapLayout 需要的扁平 record 形式） */
function buildTopologyDeviceDataSources(rawDevices) {
	const records = rawDevices.map((device) => {
		var _ref3, _pickLangText2, _device$model2, _device$number;
		return {
			id: String(device.id),
			name: deviceDisplayName(device),
			model: (_ref3 = (_pickLangText2 = pickLangText((_device$model2 = device.model) === null || _device$model2 === void 0 ? void 0 : _device$model2.name, void 0)) !== null && _pickLangText2 !== void 0 ? _pickLangText2 : device.model_code) !== null && _ref3 !== void 0 ? _ref3 : void 0,
			sn: (_device$number = device.number) !== null && _device$number !== void 0 ? _device$number : void 0,
			status: deviceStatus(device),
			ip: deviceIpAddress(device)
		};
	});
	return { [require_topologyFloorPlanConstants.TOPOLOGY_DEVICES_DATA_SOURCE_KEY]: records };
}
/**
* 基于固定 `TOPOLOGY_DEVICE_TYPE_CODES` 枚举生成图元种类，**不依赖**接口响应。
* 接口若返回新的 type_code，运行时会落到 `unknown` 兜底 kind（见 `buildTopologyConnectionAwareSceneElements`）。
*/
function buildTopologyDeviceElementKinds(displayLocale = "en") {
	const typeCodeToKind = /* @__PURE__ */ new Map();
	const kinds = TOPOLOGY_DEVICE_TYPE_CODES.map((code) => {
		var _TYPE_CODE_LABEL$code;
		const value = typeCodeToKindValue(code);
		typeCodeToKind.set(code, value);
		return {
			value,
			label: (_TYPE_CODE_LABEL$code = TYPE_CODE_LABEL[code]) !== null && _TYPE_CODE_LABEL$code !== void 0 ? _TYPE_CODE_LABEL$code : code,
			categoryId: DEVICE_CATEGORY_ID,
			dataBindingPolicy: "optional",
			allowedDataSourceKeys: [require_topologyFloorPlanConstants.TOPOLOGY_DEVICES_DATA_SOURCE_KEY],
			showInPalette: false
		};
	});
	/** unknown 兜底 kind，确保接口出现新 type_code 时也能渲染 */
	kinds.push({
		value: `${require_topologyFloorPlanConstants.TOPOLOGY_DEVICE_KIND_PREFIX}unknown`,
		label: "Unknown",
		categoryId: DEVICE_CATEGORY_ID,
		dataBindingPolicy: "optional",
		allowedDataSourceKeys: [require_topologyFloorPlanConstants.TOPOLOGY_DEVICES_DATA_SOURCE_KEY],
		showInPalette: false
	});
	/**
	* 选区（分区）kind：workarea → 虚线框。自动排列时由布局算法生成，
	* 不绑定数据源、不出现在 palette（用户无需手动拖拽）。
	*/
	kinds.push({
		value: _pisell_materials.FLOOR_MAP_SELECTION_ZONE_ELEMENT_KIND,
		label: require_topologyLocale.getTopologyCategoryAreaLabel(displayLocale),
		categoryId: DEVICE_CATEGORY_ID,
		dataBindingPolicy: "none",
		showInPalette: false
	});
	return {
		categories: [{
			id: DEVICE_CATEGORY_ID,
			label: require_topologyLocale.getTopologyCategoryDevicesLabel(displayLocale),
			order: 0
		}],
		kinds,
		typeCodeToKind
	};
}
/**
* 把设备 + 连接 → 无向邻接表（去除自环 / 端点缺失的边）
*/
function buildAdjacency(rawDevices, rawConnections) {
	const adj = /* @__PURE__ */ new Map();
	for (const d of rawDevices) adj.set(String(d.id), /* @__PURE__ */ new Set());
	for (const c of rawConnections) {
		const s = String(c.source_device_id);
		const t = String(c.target_device_id);
		if (s === t) continue;
		if (!adj.has(s) || !adj.has(t)) continue;
		adj.get(s).add(t);
		adj.get(t).add(s);
	}
	return adj;
}
/** 邻接表 → 连通分量数组（每个分量是 deviceId 列表，保持原数组顺序） */
function findConnectedComponents(rawDevices, adj) {
	const visited = /* @__PURE__ */ new Set();
	const components = [];
	for (const d of rawDevices) {
		const id = String(d.id);
		if (visited.has(id)) continue;
		const queue = [id];
		const comp = [];
		while (queue.length) {
			var _adj$get;
			const cur = queue.shift();
			if (visited.has(cur)) continue;
			visited.add(cur);
			comp.push(cur);
			for (const n of (_adj$get = adj.get(cur)) !== null && _adj$get !== void 0 ? _adj$get : []) if (!visited.has(n)) queue.push(n);
		}
		components.push(comp);
	}
	return components;
}
/**
* 设备层级语义（按 type_kind 严格分 3 行，从上到下展示）：
*   smart   →  顶层（POS / 终端 / 显示屏 等"发起方"智能设备）
*   dumb    →  中层（小票/标签打印机 / 交换机 等附属哑设备）
*   integrated → 底层（一体机 / 集成型设备）
*/
const DEVICE_LAYER_ORDER = [
	"smart",
	"dumb",
	"integrated"
];
function classifyDeviceLayer(device) {
	if (device.type_kind === "dumb") return "dumb";
	if (device.type_kind === "integrated") return "integrated";
	return "smart";
}
/**
* 蛇形居中：度数从大到小排序后，按"中间→右→左→右→左..."的顺序铺开，
* 让连接最多的设备稳定居中。仅在「最顶层」使用，下层用 barycenter 对齐顶层。
*/
function snakeCenterByDegree(ids, adj) {
	if (ids.length <= 1) return [...ids];
	const sorted = [...ids].sort((a, b) => {
		var _adj$get$size, _adj$get2, _adj$get$size2, _adj$get3;
		const da = (_adj$get$size = (_adj$get2 = adj.get(a)) === null || _adj$get2 === void 0 ? void 0 : _adj$get2.size) !== null && _adj$get$size !== void 0 ? _adj$get$size : 0;
		const db = (_adj$get$size2 = (_adj$get3 = adj.get(b)) === null || _adj$get3 === void 0 ? void 0 : _adj$get3.size) !== null && _adj$get$size2 !== void 0 ? _adj$get$size2 : 0;
		if (db !== da) return db - da;
		return ids.indexOf(a) - ids.indexOf(b);
	});
	const result = [];
	sorted.forEach((id, idx) => {
		if (idx % 2 === 0) result.push(id);
		else result.unshift(id);
	});
	return result;
}
/**
* 节点连到 reference 层的平均索引；无连接者落到最后；多对多取均值。
*/
function avgConnectedIdx(id, adj, referenceIdx, fallbackLen) {
	const neighbors = adj.get(id);
	if (!neighbors || neighbors.size === 0) return fallbackLen;
	let sum = 0;
	let cnt = 0;
	for (const n of neighbors) {
		const idx = referenceIdx.get(n);
		if (idx == null) continue;
		sum += idx;
		cnt += 1;
	}
	return cnt === 0 ? fallbackLen : sum / cnt;
}
/**
* 把 target 层按其连接的 reference 层平均索引排序（in-place）。
* 让 dumb / integrated 节点自动落到"它连接的 POS 的正下方"，连线大多平行不交叉。
*/
function sortLayerByReference(target, reference, adj) {
	if (target.length <= 1 || reference.length === 0) return;
	const refIdx = new Map(reference.map((id, idx) => [id, idx]));
	target.sort((a, b) => avgConnectedIdx(a, adj, refIdx, target.length) - avgConnectedIdx(b, adj, refIdx, target.length));
}
/**
* 单 cluster 多层布局：按 `DEVICE_LAYER_ORDER` 从上到下分行，空层不占垂直空间；
* 同层 barycenter 重排，多对多连线交叉显著减少。
*/
function layoutClusterByKind(comp, adj, deviceById) {
	if (comp.length === 1) return {
		positions: new Map([[comp[0], {
			x: 0,
			y: 0
		}]]),
		width: NODE_WIDTH,
		height: NODE_HEIGHT,
		size: 1
	};
	const byLayer = {
		smart: [],
		dumb: [],
		integrated: []
	};
	for (const id of comp) {
		const device = deviceById.get(id);
		if (!device) continue;
		byLayer[classifyDeviceLayer(device)].push(id);
	}
	/**
	* 顶层（smart）按"度数最大居中"蛇形排——重要 POS 放中间。
	* 下层（dumb / integrated）按 barycenter 跟随上一层位置排——
	* 让附属设备自动落在它对应主设备的正下方，连线尽量平行减少交叉。
	*/
	const layers = DEVICE_LAYER_ORDER.map((k) => byLayer[k]);
	if (layers[0].length > 0) layers[0] = snakeCenterByDegree(layers[0], adj);
	for (let i = 1; i < layers.length; i++) sortLayerByReference(layers[i], layers[i - 1], adj);
	const nonEmpty = layers.filter((l) => l.length > 0);
	const widths = nonEmpty.map((row) => row.length * NODE_WIDTH + (row.length - 1) * NODE_GAP);
	const width = Math.max(...widths, NODE_WIDTH);
	const height = nonEmpty.length * NODE_HEIGHT + (nonEmpty.length - 1) * LAYER_GAP_Y;
	const positions = /* @__PURE__ */ new Map();
	let y = 0;
	for (const row of layers) {
		if (row.length === 0) continue;
		const startX = (width - (row.length * NODE_WIDTH + (row.length - 1) * NODE_GAP)) / 2;
		row.forEach((id, idx) => {
			positions.set(id, {
				x: startX + idx * (NODE_WIDTH + NODE_GAP),
				y
			});
		});
		y += NODE_HEIGHT + LAYER_GAP_Y;
	}
	return {
		positions,
		width,
		height,
		size: comp.length
	};
}
/**
* 横向打包：超过 `rowMaxWidth` 换行；返回每个块的全局偏移。
* cluster / 选区分区共用同一套「行优先、尽量塞满一行再换行」的紧凑规则，
* `gap` 控制块间距（默认 cluster 间距，分区可传更小的间距）。
*/
function packClusters(clusters, gap = CLUSTER_GAP, rowMaxWidth = CLUSTER_ROW_MAX_WIDTH) {
	let rowX = 0;
	let rowY = 0;
	let rowMaxH = 0;
	let totalRight = 0;
	let totalBottom = 0;
	const placements = [];
	for (const cluster of clusters) {
		if (rowX > 0 && rowX + cluster.width > rowMaxWidth) {
			rowY += rowMaxH + gap;
			rowX = 0;
			rowMaxH = 0;
		}
		placements.push({
			cluster,
			offsetX: rowX,
			offsetY: rowY
		});
		rowX += cluster.width + gap;
		rowMaxH = Math.max(rowMaxH, cluster.height);
		totalRight = Math.max(totalRight, rowX - gap);
		totalBottom = Math.max(totalBottom, rowY + cluster.height);
	}
	return {
		placements,
		totalRight,
		totalBottom
	};
}
/** 把 deviceId 列表按 type_kind 分到 3 个桶，保持原顺序 */
function splitByLayer(ids, deviceById) {
	const out = {
		smart: [],
		dumb: [],
		integrated: []
	};
	for (const id of ids) {
		const device = deviceById.get(id);
		if (!device) continue;
		out[classifyDeviceLayer(device)].push(id);
	}
	return out;
}
/**
* 孤立设备区域：智能设备一行（上）、附属设备一行（下），数量过多时按
* `cols = ceil(sqrt(N))` 换成多行网格。
*/
function placeIsolatedRow(ids, startX, startY, positions) {
	if (ids.length === 0) return {
		width: 0,
		height: 0
	};
	const cols = Math.max(1, Math.min(ids.length, Math.ceil(Math.sqrt(ids.length * 1.5))));
	const rows = Math.ceil(ids.length / cols);
	ids.forEach((id, idx) => {
		const col = idx % cols;
		const row = Math.floor(idx / cols);
		positions.set(id, {
			x: startX + col * (NODE_WIDTH + NODE_GAP),
			y: startY + row * (NODE_HEIGHT + NODE_GAP)
		});
	});
	return {
		width: cols * NODE_WIDTH + (cols - 1) * NODE_GAP,
		height: rows * NODE_HEIGHT + (rows - 1) * NODE_GAP
	};
}
/**
* 单台设备 → sceneElement（绝对坐标）。统一 instanceId / kind / dataBinding 生成逻辑，
* 供整体布局与分区布局复用。
*/
function buildDeviceSceneElement(device, x, y, typeCodeToKind) {
	return {
		instanceId: `topology-device-${device.id}`,
		canvasId: require_topologyFloorPlanConstants.TOPOLOGY_MAIN_CANVAS_ID,
		x: Math.round(x),
		y: Math.round(y),
		width: NODE_WIDTH,
		height: NODE_HEIGHT,
		elementKind: resolveDeviceKind(device, typeCodeToKind),
		name: deviceDisplayName(device),
		dataBinding: {
			dataSourceKey: require_topologyFloorPlanConstants.TOPOLOGY_DEVICES_DATA_SOURCE_KEY,
			recordId: String(device.id)
		}
	};
}
/**
* 连接优先布局的「纯几何」内核：给定设备 + 连接，算出每台设备相对 (0,0) 的
* 位置以及整块包围盒尺寸。**不**生成 sceneElement、不加全局 PADDING，方便上层
* 既能整体布局，也能把单个分区当成一个「块」再做二次摆放。
*
* 规则：
* 1. 用连接关系把设备分成连通分量（多对多关系交由 edges 渲染）。
* 2. 每个有连接的分量按 `type_kind` 分层：smart 在上、dumb 居中、integrated 在下；
*    同层 barycenter 重排，减少连线交叉。
* 3. 多个 cluster 横向并排（超出最大行宽换行）。
* 4. 孤立设备放在已布局区域**下方**，按 layer 成行、过多则网格化。
*/
function computeDeviceBlockLayout(devices, connections) {
	const positions = /* @__PURE__ */ new Map();
	if (devices.length === 0) return {
		positions,
		width: 0,
		height: 0
	};
	const deviceById = new Map(devices.map((d) => [String(d.id), d]));
	const adj = buildAdjacency(devices, connections);
	const components = findConnectedComponents(devices, adj);
	const connectedComps = components.filter((c) => c.length > 1);
	const isolatedIds = components.filter((c) => c.length === 1).map((c) => c[0]);
	const { placements, totalBottom: clusterTotalBottom } = packClusters(connectedComps.map((c) => layoutClusterByKind(c, adj, deviceById)).sort((a, b) => b.size - a.size));
	let maxRight = 0;
	let maxBottom = 0;
	for (const p of placements) for (const [id, pos] of p.cluster.positions) {
		const x = p.offsetX + pos.x;
		const y = p.offsetY + pos.y;
		positions.set(id, {
			x,
			y
		});
		maxRight = Math.max(maxRight, x + NODE_WIDTH);
		maxBottom = Math.max(maxBottom, y + NODE_HEIGHT);
	}
	/** 孤立设备：按 layer 依次成行，整体放在已布局区域下方 */
	if (isolatedIds.length > 0) {
		const isoByLayer = splitByLayer(isolatedIds, deviceById);
		let isoY = clusterTotalBottom > 0 ? clusterTotalBottom + CLUSTER_GAP : 0;
		for (const layer of DEVICE_LAYER_ORDER) {
			const row = isoByLayer[layer];
			if (row.length === 0) continue;
			const rect = placeIsolatedRow(row, 0, isoY, positions);
			maxRight = Math.max(maxRight, rect.width);
			maxBottom = Math.max(maxBottom, isoY + rect.height);
			isoY += rect.height + LAYER_GAP_Y;
		}
	}
	return {
		positions,
		width: maxRight,
		height: maxBottom
	};
}
/**
* 连接优先布局：把所有设备当成一整块做 connection-aware 布局并生成 sceneElement。
* 不区分分区，整体居左上、留 PADDING 边距。
*/
function buildTopologyConnectionAwareSceneElements(rawDevices, rawConnections, typeCodeToKind) {
	if (rawDevices.length === 0) return { sceneElements: [] };
	const { positions } = computeDeviceBlockLayout(rawDevices, rawConnections);
	return { sceneElements: rawDevices.map((device) => {
		var _positions$get;
		const pos = (_positions$get = positions.get(String(device.id))) !== null && _positions$get !== void 0 ? _positions$get : {
			x: 0,
			y: 0
		};
		return buildDeviceSceneElement(device, PADDING + pos.x, PADDING + pos.y, typeCodeToKind);
	}) };
}
/** 设备的分区 key：以 `workarea.id` 优先，其次 `workarea_id`；0 / 空视为无分区(null) */
function getDeviceWorkareaKey(device) {
	var _device$workarea$id, _device$workarea;
	const raw = (_device$workarea$id = (_device$workarea = device.workarea) === null || _device$workarea === void 0 ? void 0 : _device$workarea.id) !== null && _device$workarea$id !== void 0 ? _device$workarea$id : device.workarea_id;
	if (raw == null) return null;
	const key = String(raw).trim();
	if (!key || key === "0") return null;
	return key;
}
/**
* 按 `workarea` 把设备分组：每个 workarea 一组（保留接口 `sort` 排序），
* `workarea_id=0` / 无 workarea 的设备汇到末尾「无分区」桶。
*/
function groupDevicesByWorkarea(devices) {
	const map = /* @__PURE__ */ new Map();
	const ungrouped = [];
	for (const device of devices) {
		const key = getDeviceWorkareaKey(device);
		if (key == null) {
			ungrouped.push(device);
			continue;
		}
		let group = map.get(key);
		if (!group) {
			var _pickLangText3, _device$workarea2, _device$workarea3;
			group = {
				workareaId: key,
				name: (_pickLangText3 = pickLangText((_device$workarea2 = device.workarea) === null || _device$workarea2 === void 0 ? void 0 : _device$workarea2.name, void 0)) !== null && _pickLangText3 !== void 0 ? _pickLangText3 : `Workarea ${key}`,
				sort: typeof ((_device$workarea3 = device.workarea) === null || _device$workarea3 === void 0 ? void 0 : _device$workarea3.sort) === "number" ? device.workarea.sort : 0,
				devices: []
			};
			map.set(key, group);
		}
		group.devices.push(device);
	}
	const groups = [...map.values()].sort((a, b) => a.sort - b.sort || a.name.localeCompare(b.name));
	if (ungrouped.length > 0) groups.push({
		workareaId: null,
		name: "",
		sort: Number.MAX_SAFE_INTEGER,
		devices: ungrouped
	});
	return groups;
}
/**
* 分区（workarea）感知布局：
* - 按 `workarea` 把设备分组；同组设备聚成一个**选区虚线框**（取 workarea 名）。
* - 每个分区**内部**单独跑 connection-aware 布局，连线逻辑只用「两端都在本分区」的连接，
*   保证同分区设备贴在一起、连线不跨区乱穿。
* - 各分区块按行打包（超出最大行宽换行），分区之间留 `GROUP_GAP`。
* - 「无分区」设备同样独立成块，但不画虚线框。
*
* 跨分区的连接仍由 {@link buildTopologyEdgesFromConnections} 统一画成边（连线本身不丢）。
*/
function buildTopologyWorkareaGroupedSceneElements(rawDevices, rawConnections, typeCodeToKind) {
	if (rawDevices.length === 0) return { sceneElements: [] };
	/** 1. 先各分区内部布局，算出每个分区块的外框尺寸（含虚线框内边距 / 标题留白） */
	const blocks = groupDevicesByWorkarea(rawDevices).map((group) => {
		const hasZone = group.workareaId != null;
		const idSet = new Set(group.devices.map((d) => String(d.id)));
		/** 同分区连线逻辑：只取两端都落在本分区的连接做内部布局 */
		const intraConnections = rawConnections.filter((c) => idSet.has(String(c.source_device_id)) && idSet.has(String(c.target_device_id)));
		const block = computeDeviceBlockLayout(group.devices, intraConnections);
		const innerPad = hasZone ? ZONE_INNER_PADDING : 0;
		const titleTop = hasZone ? ZONE_TITLE_TOP : 0;
		return {
			group,
			hasZone,
			block,
			innerPad,
			titleTop,
			outerW: block.width + innerPad * 2,
			outerH: block.height + innerPad * 2 + titleTop
		};
	});
	/**
	* 2. 用与「all」整体布局相同的 packClusters 行优先紧凑打包来摆放各分区块，
	*    分区间距取较小的 GROUP_GAP，尽量减少空白。
	*/
	const packed = packClusters(blocks.map((b) => ({
		positions: b.block.positions,
		width: b.outerW,
		height: b.outerH,
		size: b.group.devices.length
	})), GROUP_GAP);
	const sceneElements = [];
	packed.placements.forEach(({ offsetX, offsetY }, i) => {
		const b = blocks[i];
		const originX = PADDING + offsetX;
		const originY = PADDING + offsetY;
		if (b.hasZone) sceneElements.push({
			instanceId: `topology-workarea-${b.group.workareaId}`,
			canvasId: require_topologyFloorPlanConstants.TOPOLOGY_MAIN_CANVAS_ID,
			x: Math.round(originX),
			y: Math.round(originY),
			width: Math.round(b.outerW),
			height: Math.round(b.outerH),
			elementKind: _pisell_materials.FLOOR_MAP_SELECTION_ZONE_ELEMENT_KIND,
			name: b.group.name,
			zIndex: -50
		});
		const deviceOriginX = originX + b.innerPad;
		const deviceOriginY = originY + b.innerPad + b.titleTop;
		for (const device of b.group.devices) {
			var _b$block$positions$ge;
			const pos = (_b$block$positions$ge = b.block.positions.get(String(device.id))) !== null && _b$block$positions$ge !== void 0 ? _b$block$positions$ge : {
				x: 0,
				y: 0
			};
			sceneElements.push(buildDeviceSceneElement(device, deviceOriginX + pos.x, deviceOriginY + pos.y, typeCodeToKind));
		}
	});
	return { sceneElements };
}
/** sceneElement.instanceId 命名约定：`topology-device-${deviceId}` */
function sceneInstanceIdFromDeviceId(deviceId) {
	return `topology-device-${deviceId}`;
}
const SCENE_INSTANCE_ID_PREFIX = "topology-device-";
/** instanceId → deviceId（仅当符合 sceneInstanceIdFromDeviceId 约定） */
function deviceIdFromSceneInstanceId(instanceId) {
	if (!instanceId.startsWith(SCENE_INSTANCE_ID_PREFIX)) return null;
	const rest = instanceId.slice(16);
	return rest.length > 0 ? rest : null;
}
/** type_code → kind value，未识别走 unknown */
function resolveDeviceKind(device, typeCodeToKind) {
	var _device$type_code, _ref4;
	const code = ((_device$type_code = device.type_code) !== null && _device$type_code !== void 0 ? _device$type_code : "").trim();
	return (_ref4 = code && typeCodeToKind.get(code)) !== null && _ref4 !== void 0 ? _ref4 : `${require_topologyFloorPlanConstants.TOPOLOGY_DEVICE_KIND_PREFIX}unknown`;
}
/**
* 连接类型 → 视觉编码（描边色 + 虚线）。
* 用线型/颜色区分 connection_type，避免在线上画文字 pill 互相重叠。
*
* - ethernet：默认色，实线（继续走 status 色板）
* - usb：紫色实线
* - bluetooth：蓝色虚线
* - wifi：青色虚线
* - unknown / 未识别：默认色，由 status 色板决定
*/
const CONNECTION_TYPE_STYLE = {
	ethernet: {},
	lan: {},
	wired: {},
	usb: { color: "#8b5cf6" },
	bluetooth: {
		color: "#3b82f6",
		dashed: true
	},
	bt: {
		color: "#3b82f6",
		dashed: true
	},
	wifi: {
		color: "#06b6d4",
		dashed: true
	},
	"wi-fi": {
		color: "#06b6d4",
		dashed: true
	},
	wireless: {
		color: "#06b6d4",
		dashed: true
	}
};
function getConnectionTypeStyle(connectionType) {
	var _CONNECTION_TYPE_STYL;
	if (!connectionType) return {};
	return (_CONNECTION_TYPE_STYL = CONNECTION_TYPE_STYLE[connectionType.trim().toLowerCase()]) !== null && _CONNECTION_TYPE_STYL !== void 0 ? _CONNECTION_TYPE_STYL : {};
}
function connectionStatusToEdgeStatus(status) {
	switch (status) {
		case "connected":
		case "online": return "online";
		case "disconnected":
		case "offline": return "offline";
		case "warning": return "warning";
		default: return "unknown";
	}
}
/**
* connection 数组 → `FloorMapEdge[]`。
* - 仅保留两端 device 都在当前 device 数组里的连接（避免悬空 edge）。
* - 锚点强制走 top/bottom：source 所在层在 target 上方时 source.bottom→target.top；
*   反之 source.top→target.bottom；同层时也用 top↔bottom，避免出现左右锚点连线。
*/
function buildTopologyEdgesFromConnections(rawConnections, rawDevices) {
	const deviceById = new Map(rawDevices.map((d) => [String(d.id), d]));
	const layerOrder = (id) => {
		const device = deviceById.get(id);
		if (!device) return 0;
		return DEVICE_LAYER_ORDER.indexOf(classifyDeviceLayer(device));
	};
	return rawConnections.filter((c) => {
		const sId = String(c.source_device_id);
		const tId = String(c.target_device_id);
		return deviceById.has(sId) && deviceById.has(tId) && sId !== tId;
	}).map((c) => {
		var _c$config, _c$config$connection_, _c$config2;
		const sId = String(c.source_device_id);
		const tId = String(c.target_device_id);
		const sLayer = layerOrder(sId);
		const tLayer = layerOrder(tId);
		let sourceAnchor;
		let targetAnchor;
		if (sLayer < tLayer) {
			sourceAnchor = "bottom";
			targetAnchor = "top";
		} else if (sLayer > tLayer) {
			sourceAnchor = "top";
			targetAnchor = "bottom";
		} else {
			/** 同层：约定 source 从 bottom 出、target 从 top 入，避免左右锚点 */
			sourceAnchor = "bottom";
			targetAnchor = "top";
		}
		const typeStyle = getConnectionTypeStyle((_c$config = c.config) === null || _c$config === void 0 ? void 0 : _c$config.connection_type);
		return require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({
			id: `topology-connection-${c.id}`,
			sourceId: sceneInstanceIdFromDeviceId(sId),
			targetId: sceneInstanceIdFromDeviceId(tId),
			sourceAnchor,
			targetAnchor,
			status: connectionStatusToEdgeStatus(c.status)
		}, typeStyle.color ? { color: typeStyle.color } : {}), typeStyle.dashed ? { dashed: true } : {}), {}, { extensions: { connectionType: (_c$config$connection_ = (_c$config2 = c.config) === null || _c$config2 === void 0 ? void 0 : _c$config2.connection_type) !== null && _c$config$connection_ !== void 0 ? _c$config$connection_ : null } });
	});
}
function topologyMainCanvasConfig(displayLocale = "en") {
	return [{
		id: require_topologyFloorPlanConstants.TOPOLOGY_MAIN_CANVAS_ID,
		name: require_topologyLocale.getTopologyCanvasDevicesName(displayLocale),
		order: 0,
		enabled: true,
		mapLayer: {
			showGrid: false,
			backgroundColor: "#ffffff"
		}
	}];
}
/**
* 把「新增设备」追加到已有布局下方的「新增区」。
*
* 复用 connection-aware 整体布局：先按连线分簇成纵向多层，再把孤立设备
* 按 DEVICE_LAYER_ORDER 网格化在簇下方。最后把整块结果平移到已存在元素
* 的最下沿，避免和已保存/ghost/free 节点重叠。
*/
function layoutNewcomerSceneElements(added, rawConnections, keptScene, ghostScene, typeCodeToKind) {
	if (added.length === 0) return [];
	let baseY = PADDING;
	const allExisting = [...keptScene, ...ghostScene];
	for (const el of allExisting) {
		var _el$y, _el$height;
		const bottom = ((_el$y = el.y) !== null && _el$y !== void 0 ? _el$y : 0) + ((_el$height = el.height) !== null && _el$height !== void 0 ? _el$height : NODE_HEIGHT);
		if (bottom > baseY) baseY = bottom;
	}
	if (allExisting.length > 0) baseY += CLUSTER_GAP * 2;
	const { sceneElements } = buildTopologyConnectionAwareSceneElements(added, rawConnections, typeCodeToKind);
	const dy = baseY - PADDING;
	return dy === 0 ? sceneElements : sceneElements.map((el) => {
		var _el$y2;
		return require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, el), {}, { y: ((_el$y2 = el.y) !== null && _el$y2 !== void 0 ? _el$y2 : 0) + dy });
	});
}
/**
* 接入 floor-plan 的核心：把「已保存布局」「设备列表」「连接列表」对账成最终 view config。
*
* 规则：
* - 设备同时在 API + 已保存里：保留位置/尺寸，用 API 数据刷新 elementKind / name
* - 设备只在 API（新设备）：放到「新增区」auto-layout
* - 设备只在已保存（接口已下线，ghost）：保留 sceneElement，由 UI 侧渲染下线样式
* - 边：按当前 connections 接口重建；保留 savedEdges 的 routing snapshot（按 id 配对）
*
* 当 `savedSceneElements` 为空时退化为整体 auto-layout，等价于 `buildTopologyFloorMapConfigFromDevices`。
*/
function reconcileTopologyView(params) {
	var _params$displayLocale;
	const { rawDevices, rawConnections, savedSceneElements, savedEdges } = params;
	const displayLocale = (_params$displayLocale = params.displayLocale) !== null && _params$displayLocale !== void 0 ? _params$displayLocale : "en";
	const { categories, kinds, typeCodeToKind } = buildTopologyDeviceElementKinds(displayLocale);
	const dataSources = buildTopologyDeviceDataSources(rawDevices);
	/** 已保存里没有任何 sceneElement：直接按分区整体 auto-layout，等价首次 / 自动排列 */
	if (!savedSceneElements || savedSceneElements.length === 0) {
		const { sceneElements } = buildTopologyWorkareaGroupedSceneElements(rawDevices, rawConnections, typeCodeToKind);
		const edges = buildTopologyEdgesFromConnections(rawConnections, rawDevices);
		return {
			config: {
				canvases: topologyMainCanvasConfig(displayLocale),
				canvasUi: {
					canCreate: false,
					canEdit: false
				},
				elementKindCategories: categories,
				elementKinds: kinds,
				sceneElements,
				edges
			},
			dataSources,
			addedDeviceIds: rawDevices.map((d) => String(d.id)),
			ghostInstanceIds: [],
			ghostDeviceIds: [],
			keptDeviceIds: []
		};
	}
	/**
	* 1. 把 saved sceneElements 拆成两组：
	*    - boundById：可解析出 deviceId 的（来自设备同步），参与三态对账
	*    - freeScene：用户在编辑模式手动添加、与设备接口无关（如自定义网络节点），原样透传
	*/
	const boundById = /* @__PURE__ */ new Map();
	const freeScene = [];
	for (const el of savedSceneElements) {
		var _el$dataBinding;
		const fromBinding = (_el$dataBinding = el.dataBinding) === null || _el$dataBinding === void 0 ? void 0 : _el$dataBinding.recordId;
		const id = typeof fromBinding === "string" && fromBinding.trim() ? fromBinding : deviceIdFromSceneInstanceId(el.instanceId);
		if (id) boundById.set(id, el);
		else freeScene.push(el);
	}
	const deviceById = new Map(rawDevices.map((d) => [String(d.id), d]));
	/** 2. 三态分桶 */
	const keptScene = [];
	const newDevices = [];
	const keptDeviceIds = [];
	for (const device of rawDevices) {
		const id = String(device.id);
		const saved = boundById.get(id);
		if (saved) {
			keptScene.push(require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, saved), {}, {
				elementKind: resolveDeviceKind(device, typeCodeToKind),
				name: deviceDisplayName(device),
				dataBinding: {
					dataSourceKey: require_topologyFloorPlanConstants.TOPOLOGY_DEVICES_DATA_SOURCE_KEY,
					recordId: id
				}
			}));
			keptDeviceIds.push(id);
		} else newDevices.push(device);
	}
	const ghostScene = [];
	const ghostInstanceIds = [];
	const ghostDeviceIds = [];
	for (const [id, saved] of boundById) {
		if (deviceById.has(id)) continue;
		/** 保持原有 sceneElement，让用户能在编辑态删除；渲染层根据 device 是否存在自行降级 */
		ghostScene.push(saved);
		ghostInstanceIds.push(saved.instanceId);
		ghostDeviceIds.push(id);
	}
	/** 3. 新设备布局：在已布局 + ghost + free 包围盒下方追加 */
	const newScene = layoutNewcomerSceneElements(newDevices, rawConnections, [...keptScene, ...freeScene], ghostScene, typeCodeToKind);
	const sceneElements = [
		...freeScene,
		...keptScene,
		...newScene,
		...ghostScene
	];
	/** 4. 边：按当前连接接口重建；保留 savedEdges 的 routing snapshot */
	const savedEdgeById = new Map((savedEdges !== null && savedEdges !== void 0 ? savedEdges : []).map((e) => [e.id, e]));
	const edges = buildTopologyEdgesFromConnections(rawConnections, rawDevices).map((e) => {
		const saved = savedEdgeById.get(e.id);
		return (saved === null || saved === void 0 ? void 0 : saved.routing) ? require_objectSpread2._objectSpread2(require_objectSpread2._objectSpread2({}, e), {}, { routing: saved.routing }) : e;
	});
	return {
		config: {
			canvases: topologyMainCanvasConfig(displayLocale),
			canvasUi: {
				canCreate: false,
				canEdit: false
			},
			elementKindCategories: categories,
			elementKinds: kinds,
			sceneElements,
			edges
		},
		dataSources,
		addedDeviceIds: newDevices.map((d) => String(d.id)),
		ghostInstanceIds,
		ghostDeviceIds,
		keptDeviceIds
	};
}
//#endregion
exports.TOPOLOGY_DEVICE_KIND_VALUES = TOPOLOGY_DEVICE_KIND_VALUES;
exports.extractRawDeviceConnectionList = extractRawDeviceConnectionList;
exports.extractRawDeviceList = extractRawDeviceList;
exports.reconcileTopologyView = reconcileTopologyView;
