feat(world-sketcher): 增加高度标注和连通关系,AI分析改为读JSON

- sketch.html: 双击命名弹窗增加高度(m)输入和连通多选
- sketch.html: 新增defaultHeightForType()按画笔类型设默认高度
- sketch.html: 连通关系双向同步,JSON输出增加height_m/connections
- sketch.html: restoreScene恢复新字段,保存/加载流程不受影响
- SKILL.md: 步骤3从vision分析PNG改为读取JSON结构化数据
- SKILL.md: 布局解读文档模板增加高度和连通列
- README.md: 画笔图例增加默认高度,更新JSON示例和AI流程说明

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
2026-06-10 00:06:05 +08:00
parent ca023844c9
commit bb34641bfe
3 changed files with 150 additions and 42 deletions
+86 -4
View File
@@ -85,6 +85,14 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se
#name-dialog button { padding: 5px 14px; border-radius: 3px; border: 1px solid #555; cursor: pointer; font-size: 12px; }
#name-dialog .btn-ok { background: var(--accent); color: #fff; border-color: var(--accent); }
#name-dialog .btn-cancel { background: #4a4a4a; color: #ddd; }
#name-height-row { display: flex; align-items: center; gap: 6px; margin-bottom: 8px; font-size: 12px; }
#name-height-row input { width: 60px; background: #3a3a3a; border: 1px solid #555; color: #eee; padding: 4px 6px; border-radius: 3px; text-align: right; font-size: 12px; }
#name-connections-row { margin-bottom: 8px; font-size: 12px; }
#name-connections-row > label { display: block; margin-bottom: 4px; color: var(--text-dim); }
#name-connections-list { max-height: 100px; overflow-y: auto; background: #3a3a3a; border-radius: 3px; padding: 4px; }
#name-connections-list label { display: flex; align-items: center; gap: 6px; padding: 2px 4px; cursor: pointer; border-radius: 2px; }
#name-connections-list label:hover { background: #4a4a4a; }
#name-connections-list input { accent-color: var(--accent); }
</style>
</head>
<body>
@@ -165,6 +173,15 @@ html, body { height: 100%; overflow: hidden; font-family: "Microsoft YaHei", "Se
<h4>命名元素</h4>
<input id="name-input" type="text" placeholder="输入名称(如"家"、"小卖部")">
<label><input type="checkbox" id="name-enterable"> 可进入(有下级详图)</label>
<div id="name-height-row" style="display:none;">
<label style="color:var(--text-dim);">高度</label>
<input id="name-height" type="number" min="0" step="0.5" value="3">
<span style="color:var(--text-dim);">m</span>
</div>
<div id="name-connections-row" style="display:none;">
<label>连通到</label>
<div id="name-connections-list"></div>
</div>
<div class="btn-row">
<button class="btn-cancel" id="name-cancel">取消</button>
<button class="btn-ok" id="name-ok">确定</button>
@@ -642,6 +659,11 @@ const App = {
return best;
},
/* 默认高度(米),按画笔类型 */
defaultHeightForType(type) {
return { building: 3, blocked: 3, zone: 3, greenery: 2, road: 0, water: 0 }[type] || 0;
},
showNameModal(px, py) {
const pixel = this.dc.getContext("2d").getImageData(px, py, 1, 1).data;
if (pixel[3] < 50) return;
@@ -651,10 +673,46 @@ const App = {
const modal = document.getElementById("name-modal");
const inp = document.getElementById("name-input");
const chk = document.getElementById("name-enterable");
const heightRow = document.getElementById("name-height-row");
const heightInp = document.getElementById("name-height");
const connRow = document.getElementById("name-connections-row");
const connList = document.getElementById("name-connections-list");
inp.value = existing ? existing.name : "";
chk.checked = existing ? !!existing.hasDetail : false;
// L2 层不显示"可进入"复选框(L3 未实现)
chk.parentElement.style.display = this.currentLevel === "L1" ? "" : "none";
// 高度输入:building / blocked / zone 类型显示
const showHeight = ["building", "blocked", "zone"].includes(brushType.id);
heightRow.style.display = showHeight ? "flex" : "none";
if (showHeight) {
heightInp.value = existing && existing.height_m != null ? existing.height_m : this.defaultHeightForType(brushType.id);
}
// 连通多选:列出同层级其他已命名元素
const d = this.levelData[this.currentLevel];
const others = d.elements.filter(e => e !== existing && e.name);
if (others.length > 0) {
connRow.style.display = "";
connList.innerHTML = "";
const existingConns = existing ? (existing.connections || []) : [];
others.forEach(o => {
const lbl = document.createElement("label");
const cb = document.createElement("input");
cb.type = "checkbox"; cb.value = o.id;
cb.checked = existingConns.includes(o.id);
const sw = document.createElement("span");
sw.className = "el-swatch"; sw.style.background = o.color; sw.style.width = "12px"; sw.style.height = "12px";
const txt = document.createElement("span"); txt.textContent = o.name;
lbl.append(cb, sw, txt);
connList.appendChild(lbl);
});
} else {
connRow.style.display = "none";
connList.innerHTML = "";
}
modal.style.display = "block"; inp.focus();
const ok = document.getElementById("name-ok"), cancel = document.getElementById("name-cancel");
@@ -665,19 +723,39 @@ const App = {
nOk.addEventListener("click", () => {
const name = inp.value.trim(); if (!name) { cleanup(); return; }
const hasDetail = chk.checked;
if (existing) { existing.name = name; existing.hasDetail = hasDetail; }
else {
const el = { id: "el_"+Date.now(), type: brushType.id, name, color: brushType.hex, px, py, hasDetail, detailRef: null };
const height_m = showHeight ? parseFloat(heightInp.value) || 0 : null;
const connIds = Array.from(connList.querySelectorAll("input:checked")).map(cb => cb.value);
if (existing) {
existing.name = name; existing.hasDetail = hasDetail;
existing.height_m = height_m;
existing.connections = connIds;
} else {
const el = {
id: "el_"+Date.now(), type: brushType.id, name, color: brushType.hex, px, py,
hasDetail, detailRef: null,
height_m, connections: connIds,
};
this.levelData[this.currentLevel].elements.push(el);
if (hasDetail && this.currentLevel === "L1") {
const l2Id = "L2-" + el.id;
// L2 尺寸延迟到 enterElement 时计算
this.initLevel(l2Id, 80, 60, 800, 600);
this.levelData[l2Id].parentLevel = "L1";
this.levelData[l2Id].parentElement = el.id;
el.detailRef = l2Id;
}
}
// 双向连接:同步到被连通的元素
connIds.forEach(cId => {
const target = d.elements.find(e => e.id === cId);
if (target) {
if (!target.connections) target.connections = [];
const elId = existing ? existing.id : this.levelData[this.currentLevel].elements[this.levelData[this.currentLevel].elements.length - 1].id;
if (!target.connections.includes(elId)) target.connections.push(elId);
}
});
this.updateElementsList(); this.drawElementLabels(); cleanup();
});
nCancel.addEventListener("click", cleanup);
@@ -815,6 +893,8 @@ const App = {
world_x_m: parseFloat((el.px * sx).toFixed(1)),
world_y_m: parseFloat(((d.canvasH - el.py) * sy).toFixed(1)),
has_detail: !!el.hasDetail, detail_ref: el.detailRef || null,
height_m: el.height_m != null ? el.height_m : null,
connections: el.connections || [],
})),
regions,
image_file: imageFile,
@@ -909,6 +989,8 @@ const App = {
elements: (lvl.elements || []).map(el => ({
id: el.id, type: el.type, name: el.name, color: el.color,
px: el.px, py: el.py, hasDetail: el.has_detail, detailRef: el.detail_ref,
height_m: el.height_m != null ? el.height_m : null,
connections: el.connections || [],
})),
imageData: null, l1BaseData: null,
parentLevel: lvl.parent_level || null, parentElement: lvl.parent_element || null,