Compare commits

...

2 Commits

Author SHA1 Message Date
wy bc300f4ff9 Improve map editor image previews 2026-06-10 10:17:25 +08:00
wy 7d0f115fcb Integrate mossy cavern stage scene 2026-06-10 02:21:44 +08:00
16 changed files with 176 additions and 43 deletions
+28 -28
View File
@@ -12,47 +12,47 @@ collision 1760 500 300 28
collision 2180 455 260 28
collision 2650 515 360 28
tileset graybox_ground assets/stage/stage_01/tiles/graybox_ground.png 10 10 0.62 0.68 0.73 1.0
tileset graybox_platform assets/stage/stage_01/tiles/graybox_platform.png 10 14 0.62 0.68 0.73 1.0
layer background_far BackgroundFar 0.25
rect 0 0 3200 720 0.08 0.10 0.14 1.0
rect 360 120 420 220 0.13 0.16 0.21 1.0
rect 1380 90 560 260 0.12 0.15 0.20 1.0
rect 2440 140 520 210 0.13 0.16 0.21 1.0
rect 0 0 3200 720 0.04 0.06 0.05 1.0
rect_sprite_src -140 80 980 620 0.34 0.54 0.36 0.38 assets/stage/stage_01/background/mossy_background_decoration.png 0 0 1030 4096
rect_sprite_src 860 35 940 620 0.30 0.50 0.34 0.34 assets/stage/stage_01/background/mossy_background_decoration.png 1180 0 900 3500
rect_sprite_src 2020 100 980 560 0.31 0.52 0.34 0.34 assets/stage/stage_01/background/mossy_background_decoration.png 2680 170 1060 1520
rect_sprite_src 2660 410 1060 360 0.26 0.44 0.30 0.30 assets/stage/stage_01/background/mossy_background_decoration.png 2460 2920 1180 880
endlayer
layer background_mid BackgroundMid 0.55
rect 80 360 280 240 0.18 0.22 0.27 1.0
rect 1020 310 340 290 0.18 0.22 0.27 1.0
rect 2020 330 360 270 0.18 0.22 0.27 1.0
rect 2860 280 260 320 0.18 0.22 0.27 1.0
rect_sprite_src -180 390 560 260 0.45 0.70 0.45 0.42 assets/stage/stage_01/background/mossy_hills.png 50 580 420 420
rect_sprite_src 520 335 760 300 0.46 0.72 0.46 0.36 assets/stage/stage_01/background/mossy_hills.png 720 40 1160 500
rect_sprite_src 1390 360 720 270 0.42 0.68 0.43 0.36 assets/stage/stage_01/background/mossy_hills.png 760 650 1120 720
rect_sprite_src 2310 330 760 310 0.40 0.66 0.42 0.36 assets/stage/stage_01/background/mossy_hills.png 645 650 1280 840
rect_sprite_src 2860 400 560 240 0.43 0.68 0.44 0.34 assets/stage/stage_01/background/mossy_hills.png 1460 1520 520 420
endlayer
layer level_visual LevelVisual 1.0
tile_rect graybox_ground 0 600 320 8 0 0.62 0.68 0.73
tile_rect graybox_platform 230 500 22 2 0 0.62 0.68 0.73
tile_rect graybox_platform 560 430 26 2 0 0.62 0.68 0.73
tile_rect graybox_platform 930 510 26 2 0 0.62 0.68 0.73
tile_rect graybox_platform 1250 420 23 2 0 0.62 0.68 0.73
tile_rect graybox_platform 1760 500 30 2 0 0.62 0.68 0.73
tile_rect graybox_platform 2180 455 26 2 0 0.62 0.68 0.73
tile_rect graybox_platform 2650 515 36 2 0 0.62 0.68 0.73
rect_sprite_src -70 520 1680 270 1.0 1.0 1.0 1.0 assets/stage/stage_01/tiles/mossy_floating_platforms.png 420 1120 1560 490
rect_sprite_src 1520 520 1680 270 1.0 1.0 1.0 1.0 assets/stage/stage_01/tiles/mossy_floating_platforms.png 420 1120 1560 490
rect_sprite_src 230 460 240 100 1.0 1.0 1.0 1.0 assets/stage/stage_01/tiles/mossy_floating_platforms.png 500 40 1050 420
rect_sprite_src 540 390 300 105 1.0 1.0 1.0 1.0 assets/stage/stage_01/tiles/mossy_floating_platforms.png 500 540 1060 430
rect_sprite_src 910 470 310 105 1.0 1.0 1.0 1.0 assets/stage/stage_01/tiles/mossy_floating_platforms.png 500 40 1050 420
rect_sprite_src 1230 380 270 105 1.0 1.0 1.0 1.0 assets/stage/stage_01/tiles/mossy_floating_platforms.png 500 540 1060 430
rect_sprite_src 1740 460 340 105 1.0 1.0 1.0 1.0 assets/stage/stage_01/tiles/mossy_floating_platforms.png 500 40 1050 420
rect_sprite_src 2160 415 300 105 1.0 1.0 1.0 1.0 assets/stage/stage_01/tiles/mossy_floating_platforms.png 500 540 1060 430
rect_sprite_src 2625 475 400 120 1.0 1.0 1.0 1.0 assets/stage/stage_01/tiles/mossy_floating_platforms.png 420 1120 1560 490
endlayer
layer props_back PropsBack 1.0
rect 118 392 88 208 0.26 0.30 0.34 1.0
rect 780 250 72 180 0.26 0.30 0.34 1.0
rect 1440 300 80 300 0.26 0.30 0.34 1.0
rect 1880 360 110 240 0.26 0.30 0.34 1.0
rect 2520 390 92 210 0.26 0.30 0.34 1.0
rect 3040 315 82 285 0.26 0.30 0.34 1.0
rect_sprite_src 110 120 150 500 0.78 0.94 0.72 0.72 assets/stage/stage_01/props/mossy_hanging_plants.png 70 0 360 1240
rect_sprite_src 780 80 130 430 0.78 0.94 0.72 0.62 assets/stage/stage_01/props/mossy_hanging_plants.png 550 0 330 1180
rect_sprite_src 1430 115 150 520 0.78 0.94 0.72 0.68 assets/stage/stage_01/props/mossy_hanging_plants.png 990 0 360 1300
rect_sprite_src 1880 155 190 420 0.78 0.94 0.72 0.62 assets/stage/stage_01/props/mossy_hanging_plants.png 1340 0 470 1040
rect_sprite_src 2510 160 170 450 0.78 0.94 0.72 0.64 assets/stage/stage_01/props/mossy_hanging_plants.png 540 1260 420 1040
rect_sprite_src 3030 110 160 500 0.78 0.94 0.72 0.66 assets/stage/stage_01/props/mossy_hanging_plants.png 1020 1320 420 1120
endlayer
layer props_front PropsFront 1.0
rect 520 580 220 34 0.14 0.17 0.18 0.90
rect 1510 570 280 42 0.14 0.17 0.18 0.90
rect 2580 572 260 38 0.14 0.17 0.18 0.90
rect_sprite_src 510 560 260 80 0.85 1.0 0.82 0.78 assets/stage/stage_01/tiles/mossy_floating_platforms.png 0 540 430 430
rect_sprite_src 1490 545 330 95 0.85 1.0 0.82 0.78 assets/stage/stage_01/tiles/mossy_floating_platforms.png 0 540 430 430
rect_sprite_src 2560 550 330 90 0.85 1.0 0.82 0.78 assets/stage/stage_01/tiles/mossy_floating_platforms.png 0 540 430 430
endlayer
battle_zone zone_01_intro 520 260 560 340 0 0 1940 720
+7 -4
View File
@@ -6,12 +6,14 @@ This directory is reserved for the first playable side-scrolling ACT stage.
| Slot | Expected file | Notes | Source / License |
| --- | --- | --- | --- |
| Far background | `background/background_far.png` | Large or horizontally tileable image. | TBD |
| Mid background | `background/background_mid.png` | Parallax midground image or prop sheet. | TBD |
| Tileset | `tiles/stage_01_tileset.png` | Ground, platform, wall, edge tiles. | TBD |
| Far background | `background/mossy_background_decoration.png` | Mossy cave silhouettes used by `BackgroundFar` layer. | Local asset pack: `X:\NS_unknown_game\asset\Mossy Cavern`; license TBD |
| Mid background | `background/mossy_hills.png` | Mossy hill silhouettes used by `BackgroundMid` layer. | Local asset pack: `X:\NS_unknown_game\asset\Mossy Cavern`; license TBD |
| Tileset | `tiles/mossy_tileset.png` | Full Mossy tileset atlas, reserved for later tile-id based editing. | Local asset pack: `X:\NS_unknown_game\asset\Mossy Cavern`; license TBD |
| Floating platforms | `tiles/mossy_floating_platforms.png` | Current `LevelVisual` ground and platform sprites in `stage_01.map`. | Local asset pack: `X:\NS_unknown_game\asset\Mossy Cavern`; license TBD |
| Graybox ground tiles | `tiles/graybox_ground.png` | Optional temporary tileset referenced by `stage_01.map`; fallback color is used if missing. | TBD |
| Graybox platform tiles | `tiles/graybox_platform.png` | Optional temporary platform tileset referenced by `stage_01.map`; fallback color is used if missing. | TBD |
| Props sheet | `props/stage_01_props.png` | Crates, signs, lamps, blockers, exits. | TBD |
| Props sheet | `props/mossy_hanging_plants.png` | Current hanging plant props used by `PropsBack` layer. | Local asset pack: `X:\NS_unknown_game\asset\Mossy Cavern`; license TBD |
| Decoration / hazards sheet | `props/mossy_decorations_hazards.png` | Reserved for later foreground props and hazards; not used for gameplay yet. | Local asset pack: `X:\NS_unknown_game\asset\Mossy Cavern`; license TBD |
| Hit spark | `effects/hit_spark.png` | Small combat impact effect. | TBD |
| Dust | `effects/dust.png` | Run, jump, and landing dust. | TBD |
| Music / ambience | `../../music/stage_01.ogg` | Optional loopable stage audio cue. | TBD |
@@ -23,3 +25,4 @@ This directory is reserved for the first playable side-scrolling ACT stage.
- Prefer PNG for visual assets.
- Record the original download URL, author, license, and any attribution text before using an asset.
- Keep collision data separate from visual tiles; visual alignment should stay within 4 pixels of collision.
- Current Mossy Cavern integration only covers map scene visuals. Character animations, slimes, plant animations, hazards, and combat use are intentionally deferred.
Binary file not shown.

After

Width:  |  Height:  |  Size: 4.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.5 MiB

+9 -2
View File
@@ -72,11 +72,18 @@ endbattle_zone
- `tile_index` 当前表示 tileset 第一行的横向 tile 序号。
- `r g b` 是该批 tile 的灰盒 fallback 色,alpha 固定为 `1.0`
当前 `stage_01.map``level_visual` 地面和平台从长色块替换为 `tile_rect`,碰撞仍由 `collision` 独立定义。
当前 `stage_01.map`接入 Mossy Cavern 场景图集:
- `BackgroundFar` 使用 `mossy_background_decoration.png` 做远景洞穴轮廓。
- `BackgroundMid` 使用 `mossy_hills.png` 做中景苔藓轮廓。
- `LevelVisual` 使用 `mossy_floating_platforms.png` 做地面和悬空平台。
- `PropsBack` / `PropsFront` 使用苔藓悬挂植物和平台边缘装饰。
本轮只接入地图场景视觉,碰撞仍由 `collision` 独立定义;角色、怪物、植物动画和 hazard 玩法暂不接入。
## 下一步
- 下一阶段开始正式做地图场景,优先建设简易 `.map` 编辑器,详见 `map_editor.md`
- 继续用地图编辑器调整 Mossy 场景视觉块和碰撞对齐
-`tile_rect` 扩展成可表达 tile row/column 或 tile id。
- 增加空 layer、battle zone 无 spawn、tile texture 不存在等更严格错误日志。
- 保留 `whitebox_level.cpp` 作为 fallback,不再把主地图写死在 C++。
+9
View File
@@ -104,6 +104,8 @@ http://127.0.0.1:8787/tools/map_editor/
- 支持选择图层或图层内对象后,对对应图层上移/下移;预览和导出都按当前 `.map` 图层顺序处理。
- 通过“导入图形”创建 `rect_sprite`,并自动追加一个同坐标、同尺寸的 `collision`
- 导入图形在当前浏览器会话内支持图片预览;图形移动和缩放时,同步更新绑定碰撞块。
- 从仓库根目录启动静态服务时,`.map``assets/...` 贴图路径会自动映射到 `game/assets/...` 并加载项目内已有图片预览。
- `rect_sprite_src` 图形支持在属性面板编辑源图裁切区域。
- 重新打开 `.map` 时,如果 `rect_sprite` 与某个 `collision` 坐标尺寸一致,编辑器会自动恢复二者的编辑绑定。
- 导出兼容 `StageMapLoader``.map` 文本。
@@ -129,6 +131,13 @@ assets/stage/stage_01/props/<图片文件名>
实际运行游戏前,需要把对应图片素材放到该目录。编辑器内的图形-碰撞绑定是工具层能力,导出的 `.map` 仍保持运行时兼容格式,不额外写入私有字段。
项目内已有素材预览规则:
- `assets/...` 会映射到仓库内 `game/assets/...`
- `game/assets/...` 会直接映射到仓库同名路径。
- 如果文件存在,编辑器重新打开 `.map` 后也会自动预览。
- 如果文件不存在,编辑器使用颜色块 fallback。
## 首版验收
完成时应达到:
+15 -1
View File
@@ -34,6 +34,20 @@ UI
- 支持 `.map` tile entry 绘制。
- 支持 tileset 图片缺失时按 tile fallback 色绘制。
- 资源缺失时先检查 `Asset::exists()`,再回退色块,不触发纹理加载错误日志。
- `LevelVisual` 里的贴图矩形不再叠加灰盒顶边辅助线;只有纯色占位块会显示该辅助线。
## 当前素材接入
第一关已经通过 `rect_sprite_src` 接入 Mossy Cavern 场景图集:
- `background/mossy_background_decoration.png`
- `background/mossy_hills.png`
- `tiles/mossy_floating_platforms.png`
- `props/mossy_hanging_plants.png`
- `props/mossy_decorations_hazards.png`
- `tiles/mossy_tileset.png`
由于当前 `tile_rect` 只能取 tileset 第一行,正式 Mossy 场景暂时优先使用 `rect_sprite_src` 从大图集裁切。`mossy_tileset.png` 已落位,等后续扩展 tile row/column 或 tile id 后再用于精细铺砖。
## 约束
@@ -44,5 +58,5 @@ UI
## 下一步
- 给 tile entry 增加 tile id 或二维 source index。
- 把正式 tileset 素材接入 `stage_01.map`
- 用地图编辑器继续调整 Mossy 场景块、前后景装饰和碰撞对齐
- 如果引擎提供统一 Texture cache,再迁移当前局部贴图缓存。
+11
View File
@@ -319,3 +319,14 @@ game/assets/stage/stage_01/
3. 把灰盒 `StageLayer` 的色块替换成 sprite/tile 条目。
4. 对齐视觉块和碰撞块,误差控制在 4 像素以内。
5. 更新素材来源和授权记录。
### Mossy Cavern 场景接入
当前第一关已经先接入 `X:\NS_unknown_game\asset\Mossy Cavern` 的场景图集,范围只包含地图视觉:
1. 已完成:把 Mossy 背景、平台、悬挂植物和装饰图集复制到 `game/assets/stage/stage_01/`,并统一为小写下划线路径。
2. 已完成:`stage_01.map``BackgroundFar``BackgroundMid``LevelVisual``PropsBack``PropsFront` 已改用 `rect_sprite_src` 裁切 Mossy 图集。
3. 已完成:碰撞、玩家出生点、camera bounds、battle zone 和 spawn point 保持原数据,不把视觉块当碰撞使用。
4. 已完成:贴图 `LevelVisual` 不再叠加灰盒辅助顶线,避免平台素材显示异常。
暂不接入 `BlueWizard Animations``Plant Animations``Slimes` 和 hazard 玩法。下一步围绕地图编辑器继续微调场景块位置和碰撞对齐。
+1 -1
View File
@@ -107,7 +107,7 @@ void StageRenderer::drawLayer(const LevelDefinition& level,
rect.bounds.top() + parallaxOffset.y,
rect.bounds.width(), rect.bounds.height());
drawStageRect(rect, bounds);
if (layer.role == StageLayerRole::LevelVisual) {
if (layer.role == StageLayerRole::LevelVisual && !rect.source.hasTexture()) {
drawRect(frostbite2D::Rect(bounds.left(), bounds.top(), bounds.width(),
4.0f),
frostbite2D::Color(0.91f, 0.95f, 0.98f, 1.0f));
+6 -1
View File
@@ -133,6 +133,10 @@ assets/stage/stage_01/props/<图片文件名>
静态网页不能自动复制图片到项目目录。实际运行游戏前,需要手动把对应图片素材放到该路径。
如果从仓库根目录启动静态服务,编辑器会把 `.map` 里的 `assets/...` 贴图路径映射到 `game/assets/...` 并自动加载预览。也就是说,项目内已经存在的 Mossy Cavern 等 PNG 会在重新打开地图后直接显示;只有文件没有放到对应目录时才会回退为颜色块。
`rect_sprite_src` 图形会在属性面板显示 `源 X``源 Y``源 W``源 H`,可以直接调整大图集的裁切区域。普通 `rect_sprite` 仍按整张图绘制,不会因为打开属性面板而自动变成 `rect_sprite_src`
### 调整图层顺序
1. 在对象列表中选择一个图层,或选择该图层下的图形/铺砖区。
@@ -205,7 +209,8 @@ spawn
- 不能直接覆盖本地源文件,只能下载 `.map`
- 导入图片的真实文件仍需手动放入项目资源目录。
- 已导入图片的浏览器预览只在当前会话有效;重新打开 `.map` 后,如果没有重新选择图片,会显示 fallback 色块
- `.map` 中存在于 `game/assets/...``assets/...` 贴图路径会自动预览
- 如果导入图片后没有把真实文件放到 `game/assets/...` 对应路径,重新打开 `.map` 后会显示 fallback 色块。
- 暂未实现重做。
- 暂未实现多选和批量移动。
- 暂未实现 tile palette 和 tileset 图片预览。
+6 -2
View File
@@ -33,7 +33,9 @@ http://127.0.0.1:8787/tools/map_editor/
- 通过“编辑目标”过滤画布选择对象,默认“智能选择”把碰撞块当作上层覆盖;同位置对象第一次点选碰撞块,再点一次切到下层铺砖区或图形。
- 选中矩形对象后,拖动右下角蓝色手柄可以调整宽高。
- 通过“导入图形”选择图片,自动生成一个 `rect_sprite` 图形对象和一个同尺寸 `collision`
- 导入的图形会在浏览器内临时预览,拖动或缩放图形时,同步移动或缩放绑定的碰撞块
- 从仓库根目录启动静态服务时,`.map``assets/...` 贴图路径会自动映射到 `game/assets/...` 并显示预览
- 导入的图形会在浏览器内预览,拖动或缩放图形时,同步移动或缩放绑定的碰撞块。
- 已有 `rect_sprite_src` 图形可以在属性面板调整源图裁切区域。
- 编辑选中对象的数值和文本属性。
- 新增/删除图块集、图层、碰撞块、铺砖区、战斗区和出生点。
- 支持撤回最近编辑操作,也可以使用 `Ctrl+Z`
@@ -71,6 +73,7 @@ http://127.0.0.1:8787/tools/map_editor/
4. 拖动图形本体可以移动图形和绑定碰撞块。
5. 拖动右下角蓝色手柄可以调整图形和绑定碰撞块尺寸。
6. 也可以在右侧属性面板修改 `X``Y``W``H`、颜色和贴图路径。
7. 如果图形来自 `rect_sprite_src`,属性面板还会显示 `源 X``源 Y``源 W``源 H`,用于调整图集裁切区域。
静态网页不能自动把图片复制进项目目录。导入后 `.map` 默认写入:
@@ -89,6 +92,7 @@ assets/stage/stage_01/props/<图片文件名>
## 当前限制
- 浏览器安全限制不允许静态网页直接覆盖本地源文件。当前需要先下载 `.map`,再手动替换项目里的地图文件;部署到服务器后可以增加服务端保存接口。
- 已导入图形的预览只在当前浏览器会话内有效;重新打开 `.map` 后,如果浏览器没有重新选择图片,图形会用颜色块显示
- 如果 `.map` 里的贴图路径指向 `assets/...` 且文件存在于 `game/assets/...`,重新打开后会自动预览图片
- 如果导入图片后没有把真实文件放入 `game/assets/...` 对应路径,重新打开后会回退为颜色块。
- 暂未实现图块集图片预览,铺砖区当前使用 fallback 颜色显示。
- 暂未实现重做和多选。
+84 -4
View File
@@ -7,6 +7,8 @@ const state = {
gridSize: 10,
hitFilter: "all",
imagePreviews: new Map(),
imageLoading: new Set(),
imageFailures: new Set(),
history: [],
dragging: null,
};
@@ -121,6 +123,7 @@ async function importGraphicAsset(event) {
const image = await loadImage(previewUrl);
const texturePath = defaultGraphicTexturePath(file.name);
state.imagePreviews.set(texturePath, image);
state.imageFailures.delete(texturePath);
pushUndo("导入图形");
addGraphicSprite(texturePath, image.naturalWidth || image.width || 64, image.naturalHeight || image.height || 64);
setStatus(`已导入图形 ${file.name},并自动创建同尺寸碰撞块。请把素材放到 ${texturePath}`);
@@ -140,11 +143,58 @@ function loadImage(url) {
});
}
function preloadMapImages(map) {
for (const layer of map.layers) {
for (const rect of layer.rects) {
if (rect.texture) queueImagePreview(rect.texture);
}
}
}
async function queueImagePreview(texturePath) {
if (!texturePath ||
state.imagePreviews.has(texturePath) ||
state.imageLoading.has(texturePath) ||
state.imageFailures.has(texturePath)) {
return;
}
const url = previewUrlForTexture(texturePath);
if (!url) {
state.imageFailures.add(texturePath);
return;
}
state.imageLoading.add(texturePath);
try {
const image = await loadImage(url);
state.imagePreviews.set(texturePath, image);
state.imageFailures.delete(texturePath);
refresh();
} catch (error) {
state.imageFailures.add(texturePath);
setStatus(`贴图预览加载失败:${texturePath}`);
} finally {
state.imageLoading.delete(texturePath);
}
}
function previewUrlForTexture(texturePath) {
const clean = String(texturePath || "").replace(/\\/g, "/").replace(/^\/+/, "");
if (!clean) return "";
if (/^(blob:|data:|https?:\/\/)/i.test(clean)) return clean;
if (clean.startsWith("assets/")) return `../../game/${clean}`;
if (clean.startsWith("game/assets/")) return `../../${clean}`;
return clean;
}
function loadMapText(text, status) {
try {
state.map = parseMap(text);
state.selected = null;
state.history = [];
state.imageFailures.clear();
preloadMapImages(state.map);
fitWorldToCanvas();
setStatus(status);
refresh();
@@ -489,6 +539,7 @@ function drawLayerRect(ctx, rect) {
ctx.strokeRect(rect.bounds.x, rect.bounds.y, rect.bounds.width, rect.bounds.height);
return;
}
if (rect.texture) queueImagePreview(rect.texture);
drawFilledRect(ctx, rect.bounds, rect.color, 0.55, "#7d8794");
}
@@ -1108,7 +1159,7 @@ function rectFields(rect) {
}
function layerRectFields(item) {
return [
const fields = [
numberField("X", () => item.bounds.x, (v) => {
const next = Number.isFinite(v) ? v : item.bounds.x;
moveLinkedCollision(item, next - item.bounds.x, 0);
@@ -1129,11 +1180,27 @@ function layerRectFields(item) {
}),
textField("贴图路径", () => item.texture || "", (v) => {
const next = String(v || "").trim();
if (next) item.texture = next;
else delete item.texture;
if (next) {
item.texture = next;
state.imageFailures.delete(next);
queueImagePreview(next);
} else {
delete item.texture;
}
}),
...colorFields(item.color, true),
];
if (item.texture && item.source) {
fields.push(
numberField("源 X", () => sourceRectFor(item).x, (v) => { sourceRectFor(item).x = Math.max(0, v); }),
numberField("源 Y", () => sourceRectFor(item).y, (v) => { sourceRectFor(item).y = Math.max(0, v); }),
numberField("源 W", () => sourceRectFor(item).width, (v) => { sourceRectFor(item).width = Math.max(1, v); }),
numberField("源 H", () => sourceRectFor(item).height, (v) => { sourceRectFor(item).height = Math.max(1, v); })
);
}
fields.push(...colorFields(item.color, true));
return fields;
}
function pointFields(point) {
@@ -1161,6 +1228,19 @@ function textField(label, get, set, options = null) {
return { label, type: "text", options, get, set };
}
function sourceRectFor(item) {
if (!item.source) {
const image = item.texture ? state.imagePreviews.get(item.texture) : null;
item.source = {
x: 0,
y: 0,
width: image ? (image.naturalWidth || image.width || Math.max(1, item.bounds.width)) : Math.max(1, item.bounds.width),
height: image ? (image.naturalHeight || image.height || Math.max(1, item.bounds.height)) : Math.max(1, item.bounds.height),
};
}
return item.source;
}
function ensureLevelVisualLayer() {
if (!state.map.layers.some((layer) => layer.role === "LevelVisual")) {
state.map.layers.push({ id: "level_visual", role: "LevelVisual", parallax: 1, rects: [], tileRects: [] });