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:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user