载入中...
搜索中...
未找到
建筑放置模块

建筑放置 — building

面向策略 / RTS / 经营模拟(ETS)的建筑放置框架:数据驱动定义、格子占用世界、鬼影预览、 可插拔校验与吸附规则。设计见 建筑放置系统设计。

可运行示例:examples/building(make building)。

离散楼层

PlacementWorld 把 level 作为 cell/edge 权威地址的一部分。使用 setActiveLevel(level) 选择当前编辑层,使用 setFloorHeight(height) 设置楼层间沿网格 平面法向的距离。原有放置与占用 API 操作活动层;getOccupantAtLevel、 getAnyOccupantAtLevel、getEdgeOccupantAtLevel 可在不切换活动层时显式查询。不同层可占用 同一格或同一边,同层仍会冲突,edge 拓扑也只在本层连接。BuildingFX 使用 level * floorHeight + elevation 投影最终高度。

BuildingFX 可用 setLevelVisibilityMode(world, mode) 控制楼层显示:all 显示全部, active 仅显示活动层,active_and_below 显示活动层及以下。切换 world.setActiveLevel(...) 后调用 sync(world) 即会原地刷新现有视觉的可见性。

结构定义可设置 structuralRole(例如 floor、wall、ceiling、roof、stair)。 当 supportMode 为 cell_below 时,定义的每个占地格都必须在下一层找到支撑实例;可用 supportTag 进一步限定支撑类型。缺少支撑时预览和放置返回稳定原因 support_missing。 普通拆除或移动仍被上层实例依赖的支撑体会被拒绝,原因为 support_in_use;脚本可用 getBuildingSupportCount/getBuildingSupportAt/getBuildingDependentCount 查询关系,或调用 removeBuildingCascade 按依赖优先顺序拆除整个结构子树。C++ 应使用返回 owning receipt 的 removeBuildingCascadeResult。定义热重载或外部恢复后,应调用 rebuildStructuralLinksResult 原子验证并重建派生链接。

入口

building <- eve.Building();
building.registerBuildingsFromJson(jsonText);
local world = building.newWorld(64, 64, 32.0); // 宽、高、格子像素/世界单位
local ghost = building.newGhost();

最小示例

building <- eve.Building();
building.registerBuildingsFromJson(@"
[
{\"id\":\"house\",\"displayName\":\"House\",\"footprintW\":2,\"footprintH\":2,
\"tags\":[\"house\"],\"cost\":{\"wood\":20}},
{\"id\":\"road\",\"footprintW\":1,\"footprintH\":1,\"tags\":[\"road\"]}
]
");
world <- building.newWorld(32, 32, 32.0);
world.setId("town");
world.fillTerrain(1);
ghost <- building.newGhost();
ghost.setBuildingId("house");
ghost.setFromWorld(world, mouseX, mouseY); // 按定义 snapMode 吸附
if (ghost.validate(world)) {
local id = world.placeGhost(ghost);
print("placed " + id + "\n");
} else {
print("cannot place: " + ghost.getReason() + "\n");
}

任务导向示例

1. 沿道路摆摊(邻接标签)

定义里设 "requireAdjacentTag":"road",先铺 road,再放摊位;失败原因为 adjacency_tag。

2. 码头只能建在水域

地形语义由 world.setTerrain(x,y,semantic) 写入;定义 "requireTerrain":[2]。 陆地格会得到 terrain_mismatch。

3. 自定义“金币不足”校验(C++)

PlacementSystem::registerValidateRule("needGold",
[](const PlacementWorld&, const PlacementQuery&, std::string* reason) {
if (playerGold < 10) { if (reason) *reason = "not_enough_gold"; return false; }
return true;
});

建筑定义 "validateRule":"needGold";脚本侧只需选用该规则名。

API 快查

Building(模块)

方法 说明
registerBuildingsFromJson(json) 批量注册定义,返回成功数
clearBuildingDefinitions() 清空定义表
getBuildingDefinitionCount() / hasBuildingDefinition(id) 查询
getBuildingDisplayName / Category / FootprintW / FootprintH 定义字段
getBuildingSnapMode / RotationMode / ValidateRule 策略名
buildingHasTag / getBuildingExtra / getBuildingCost 标签与扩展
newWorld(w,h,cellSize) / newGhost() 工厂
hasValidateRule / hasSnapRule 内置+已注册规则
clearChangeEvents / getChangeEvent* 变更事件 poll

PlacementWorld

方法 说明
setOrigin / setCellSize / worldToCell* / cellToWorld* 坐标
getId / getWidth / getHeight / destroy 世界身份、格网尺寸与释放
fillTerrain / setTerrain / getTerrain 地形语义
canPlace / canPlaceReason / placeAt / placeAtWorld / placeGhost 放置
removeBuilding / moveBuilding / clearBuildings 拆除与移动
getOccupant / isCellEmpty / getBuilding* 查询

Ghost

方法 说明
setBuildingId / setCell / setFromWorld / setRotationDeg / rotateBy 姿态
validate(world) / isValid / getReason 校验结果
destroy 释放鬼影

PlacementSession

方法 说明
startPlacement / updateFromSurface / updateFromWorld / setMode / execute 会话放置流
isActive / destroy 会话是否进行中、释放

生命周期注意

  • 将 PlacementWorld / Ghost 保存在全局或实体组件中;不要每帧 newWorld。
  • 变更事件不会自动清空;批量操作后调用 clearChangeEvents 或按帧 poll。
  • 自定义 validate/snap/hook 只能在 C++ 注册;脚本通过字符串策略名选用。
  • 模块**不**扣资源、不画鬼影;cost 仅作数据提示,由游戏在 hook 或脚本中处理。

多样式网格 / 3D / Tilemap(2026-08-18 起)

坐标换算统一走 grid 模块(纯数学),PlacementWorld 缺省为正交 2D,旧脚本零改动。

网格布局

world.setGridLayout("isometric"); // rectangle | hexagon | isometric | staggered | isometric-z-as-y
world.setGridPlane("xz"); // xy(默认)| xz(3D:第二轴 -> 世界 Z)
world.setCellGap(2.0, 2.0);
world.setStagger("y", "odd");
world.setHexSideLength(14.0);

绑定 Tilemap(复用投影 + GID 地形)

world.bindTileLayer(layer); // 同时复用 layer 的 orientation/stagger/hex
world.setTerrainGid(2, 2); // GID 2 -> 地形语义 2(水域)
world.setTerrainGidMapJson("{\"1\":1,\"2\":2}"); // 或整表

绑定后 getTerrain 从 GID 懒解析;setTerrain 手动覆盖优先。

3D 放置(XZ 平面 + 表面接口)

world.setGridPlane("xz");
ghost.setFromSurface(world, "plane", hitX, hitZ); // 内置平面表面;高度 setPlaneSurfaceHeight
ghost.setFromWorld3D(world, worldX, worldY, worldZ); // 直接喂真实世界坐标
world.placeAtWorld3D("house", x, y, z, rot);

物理射线由游戏在 C++ 侧实现 Provider;引擎内置 plane、不可变规则高度场 HeightfieldSurface,以及不可变静态三角网格 StaticMeshSurface。

新实现应优先使用 PlacementSystem::registerSurfaceProvider。Provider 返回受检查的 Result<PlacementHit>;命中除世界坐标外还包含归一化法线、正交切线框架、稳定 surfaceId、surfaceRevision、primitive id 与表面标签。registerSurface 保留为 兼容适配层,只能用 bool 表达“命中/未命中”。非法坐标或零法线会被结构化拒绝。

C++ 高度场使用行主序样本,支持独立原点、XY 间距、高度缩放/偏移及稳定版本:

HeightfieldSurface::Config config;
config.width = width;
config.height = height;
config.spacingX = cellWidth;
config.spacingY = cellDepth;
config.surfaceId = "terrain.chunk.0";
config.surfaceRevision = terrainRevision;
auto surface = HeightfieldSurface::create(config, std::move(rowMajorHeights));
if (surface.ok()) {
auto registered = PlacementSystem::registerHeightfieldSurface(
"terrain", std::move(surface).takeValue());
// registered 必须检查;unregisterSurface("terrain") 释放 registry 所持引用。
}
std::uint32_t height
std::uint32_t width

采样范围包含最外圈顶点,范围外返回 NotFound。内部使用双线性高度;先以中心差分 (边界为单边差分)得到顶点梯度,再双线性插值梯度,因此共享片边界上的法线和切线 连续。XZ 网格把高度映射到 world Y,XY 网格映射到 world Z。对象构造完成后不可变,可 并发采样;注册、替换和注销仍限定调用线程且不得与 采样并发。修改地形时应构造新对象、递增 surfaceRevision 后原子替换注册项。

静态网格使用拥有所有权的世界空间 XYZ 顶点、三角形索引和可选逐顶点法线,在创建时 构建确定性的 median-split BVH:

StaticMeshSurface::Config config;
config.surfaceId = "terrain.mesh.0";
config.surfaceRevision = meshRevision;
config.hitSelection = StaticMeshSurface::HitSelection::Highest;
auto surface = StaticMeshSurface::create(
config, std::move(packedWorldPositions), std::move(triangleIndices),
std::move(optionalPackedWorldNormals));
if (surface.ok()) {
auto registered = PlacementSystem::registerStaticMeshSurface(
"terrain-mesh", std::move(surface).takeValue());
// registered 必须检查。
}

查询仍采用网格平面坐标而非相机射线:XZ 网格沿世界 Y 投影,XY 网格沿世界 Z 投影; 与投影方向平行、在平面上退化的三角面不会命中。重叠层可确定性选择 Highest、 Lowest 或离 referenceHeight 最近的面,相同高度/距离以源三角形序号决胜;该序号写入 primitiveId。法线优先重心插值逐顶点法线,否则使用几何法线,并可统一朝向网格 up。 对象构造后不可变且可并发采样,注册表持有快照至替换或注销。动态物理体、相机射线和 任意方向墙面拾取仍应使用自定义 registerSurfaceProvider。

通过表面放置后,Ghost 与 PlacedBuilding 会保留表面身份、revision 和局部法线。 脚本可用 ghost.getSurfaceId/getSurfaceRevision/getSurfaceNormal*,以及 world.getBuildingSurfaceId/getBuildingSurfaceRevision/getBuildingSurfaceNormal* 查询。 这些值是放置时快照;provider revision 变化后的已放置实例自动重投影尚未提供,当前 不会静默移动已经放置的建筑。

多格建筑通过 sampleSurfacePatch 覆盖旋转后的实际占地掩码。定义可配置:

{
"id": "hill-house",
"footprintW": 2,
"footprintH": 2,
"maxSurfaceSlopeDegrees": 25.0,
"maxSurfaceHeightDelta": 0.4
}

默认规则会以 surface_slope 或 surface_height_delta 拒绝超过限制的 Ghost;跨 surfaceId 或 revision 的 footprint 会在采样阶段返回 Conflict。旋转带表面附着的 Ghost 后,下一次 validate 会自动按新 footprint 重采样。3D buildingfx 使用表面 tangent/normal/bitangent 对齐建筑、Ghost 与占地光标,并保留 free snap 的精确锚点。

通道(同格叠放)

定义 "channel":"floor" / "channel":"furniture",同一格允许跨通道建筑并存; getOccupantInChannel(channel, x, y) / isCellEmptyInChannel 按通道查询。

边对象(墙、围栏、门)

边对象使用独立于格子 footprint 的占用域,因此墙不会错误占用相邻房间的地板格:

{
"id": "stone-wall",
"placementKind": "edge",
"connectionGroup": "stone-wall-network",
"channel": "walls",
"renderMode": "3d",
"visual3d": {"height":"3.0", "thickness":"0.25"}
}
session.startPlacement(world, "stone-wall");
session.updateEdge(world, cellX, cellY, "north"); // north | east | south | west
if (session.isValid()) session.execute();

north/south 和 east/west 会规范化为唯一物理边,例如 (x,y,north) 与 (x,y-1,south) 指向同一占用。world.getEdgeConnectionMask(instanceId) 返回同一 connectionGroup 的六位邻接信息:沿边起点/终点延伸,以及两个端点各自的两条垂直 分支。BuildingFX 使用边中点、长度和 thickness 生成视觉。

连续墙线通过会话原子提交:

local status = session.executeEdgeLine(startVertexX, startVertexY, endVertexX, endVertexY);
for (local i = 0; i < session.getLastPlacedCount(); ++i)
print(session.getLastPlacedId(i) + "\n");

端点必须形成非空水平线或垂直线。系统会先验证全部单位边;任意一段越界或被占用时, 不会创建实例、写占用或发送部分事件。成功后每段仍拥有独立 instance id。

带转角的正交路径也可以作为一次手势提交:

session.beginEdgePath(startVertexX, startVertexY);
session.appendEdgePathVertex(turnVertexX, turnVertexY);
session.appendEdgePathVertex(endVertexX, endVertexY);
local status = session.executeEdgePath();

每个相邻顶点必须形成非空水平或垂直段,同一路径不能重复经过同一物理边。系统在写入前 预检整条路径,失败时不会留下半条墙;成功 id 按路径方向排序,BuildingFX 会根据现有边 拓扑自动选择转角视觉。building_editing::BuildingTarget::makeEdgePath 将同一路径编码为一个 可撤销/重做的原子批次。

三次 Bezier 曲线使用四个逻辑网格控制点和固定细分数:

session.beginEdgeCurve(1.0, 1.0);
session.appendEdgeCurveControlPoint(1.0, 6.0);
session.appendEdgeCurveControlPoint(7.0, 6.0);
session.appendEdgeCurveControlPoint(7.0, 1.0);
local status = session.executeEdgeCurve(24);

细分数范围为 2–4096。采样点以固定公式舍入到逻辑顶点,再以确定性 Manhattan 步进连接, 同样走整条路径的重复边、边界和占用预检。相同控制点与细分数会得到相同单位边序列,适合 重放、撤销和联网命令。曲线若自交并重复经过同一物理边会整体拒绝。当前 BuildingFX 将结果 显示为拓扑正确的单位边组合。成功提交还会在 PlacementWorld 创建一个权威曲线组,保存 强类型组 id、四个控制点、细分数和有序成员 id;clone/swap 会完整保留该状态。移动、替换或 删除任一成员会原子解散整个组并清除其余成员链接,防止旧控制点继续驱动视觉;普通单位边 仍然保留。单实例 restore 不会凭不完整快照重建曲线组;building_editing schema v8 使用 组级 codec 同时保存组身份、控制点、细分数和有序实例快照,撤销删除整组,重做在独立候选 世界中按原 ID 全量预检后原子恢复。任一成员冲突都会拒绝整个恢复且不改变目标世界。

编辑器可在每次控制点拖动时调用 previewEdgeCubicBezier 获得带 world revision 的不可变 草稿,内容包括四个控制点、确定性采样顶点、规范边序列和结构化诊断。edgeCubicBezierGizmo 把相同草稿投影为 UI 中立的四个球形手柄、虚线控制多边形和红/绿采样路径;视口只负责绘制 与拖拽,不拥有曲线事实。编辑已有曲线时传入任一成员 id,预览会在候选世界中临时排除原组。 拖拽结束后使用 makeUpdateEdgeCubicBezier 生成一个 building.edge-curve.replace.v1 原子事务: 组 id 保持稳定,新旧边集合整体替换,撤销恢复完整旧控制点和成员集合。

首次创建贴地曲线使用 makeEdgeCubicBezierOnSurface;它先生成同一离散边候选,再完整采样 命名 surface provider,最后把边实例和表面帧编码进同一个 building.edge-curve.set.v1 操作。 previewEdgeCubicBezier 与 edgeCubicBezierGizmo 的可选 surfaceName 参数沿用相同采样规则。 成功 draft 携带 provider、surface identity/revision 和拥有型世界坐标/法线样本;gizmo 的控制柄、 控制多边形和解析曲线路径会投射到表面。采样失败时 draft 返回 Rejected 和结构化诊断,路径使用 红色回退投影,且不会产生可提交操作。

BuildingEdgeCurveDragSession 把视口射线适配为上述 draft/事务协议:按最近命中的球形控制柄 开始拖动,在相机朝向的拖动平面上投影后转换回连续逻辑网格坐标,每次移动都重新生成 surface-aware preview 与 gizmo。鼠标释放时仅为最后一个有效 draft 生成 building.edge-curve.replace.v1;视口仍通过编辑 authority 预检和提交该操作。会话记录开始时的 target revision,拖动期间若世界被其他编辑修改,则返回 Conflict、取消手势且不覆盖并发修改。 输入层不被保留,target 由调用方拥有并必须活得比拖拽会话更久。

building_editor::BuildingEdgeCurveTool 已把该协议接入通用 EditorSession。宿主实现 IBuildingViewportAdapter,同步提供屏幕指针到世界射线以及世界点到 overlay 坐标的投影;工具 在 Pointer Down 命中控制柄后请求捕获,Move 只更新拥有型 preview,Pointer Up 才通过借用的 IEditAuthority 预检并提交一个操作。Cancel、未命中、投影失败和 revision conflict 均不改变 世界。工具把四个控制柄与控制线/曲线路径输出到 IEditorOverlay,并保存最后一次结构化诊断和 TransactionReceipt。提交后会采用 receipt 中的新曲线成员 id 和最新控制点,因此同一选择可 继续拖动;更换 viewport 或 authority 会先取消未提交手势,避免跨宿主提交。

自定义表面曲线使用 placeEdgeCubicBezierOnSurface。系统先完成整条离散边预检,再沿解析 Bezier 等距参数采样 subdivisions+1 个表面帧;所有命中必须具有相同 surfaceId 和 surfaceRevision,否则不写入任何边。成功后曲线组拥有 provider 名、表面身份/修订以及每个 样本的世界坐标和法线,因此渲染、快照、撤销和重放不依赖提供者继续存在。编辑控制点时会 重新调用原 provider,只有新样本全部连续有效时才替换旧组。

运行时脚本可在同一个 PlacementSession 中完成对应提交,无需绕过会话状态:

session.beginEdgeCurve(1.0, 1.0);
session.appendEdgeCurveControlPoint(1.0, 6.0);
session.appendEdgeCurveControlPoint(7.0, 6.0);
session.appendEdgeCurveControlPoint(7.0, 1.0);
local status = session.executeEdgeCurveOnSurface(24, "terrain.main");

该入口与平面曲线共享控制点、原子占用预检和 lastPlacedIds;仅在完整表面采样与边提交均 成功后清空控制点。失败会保留控制点,调用方可修改 provider、细分数或控制点后再次预览。

BuildingFX 会把连接掩码归类为 isolated、end、straight、corner、tee、 cross。视觉表可用 variant.<类型>.<字段> 覆盖基础值,例如:

"visual3d": {
"mesh":"models/wall.glb#main",
"height":"3", "thickness":"0.25", "colorR":"0.6",
"variant.end.mesh":"models/wall-end.glb#main",
"variant.corner.mesh":"models/wall-corner.glb#main",
"variant.corner.mask.34.mesh":"models/wall-corner-34.glb#main",
"variant.end.rotationDeg":"15",
"variant.corner.mirrorZ":"true",
"variant.corner.thickness":"0.4",
"variant.tee.colorR":"0.9"
}

字段解析顺序是精确 mask、拓扑类型、基础字段;因此特殊连接形态可以使用独立资源,未提供 精确资源时仍能回退到类型或基础资源。宿主 C++ 通过 setMeshResolver 把稳定资源 id 映射到 provider-owned graphics::Mesh*。邻接墙增删后下一次 BuildingFx.sync 会重新分类并刷新资源、 旋转、镜像、尺寸和颜色。getVisualVariant/Resource/FallbackReason 可用于调试;fallback 原因 为 primitive_default、resolver_unavailable 或 resource_unresolved。

角点对象(柱、连接节点)

placementKind:"corner" 使用网格顶点而不是格子或边。顶点坐标范围包含最外侧边界, 例如 8×8 世界的合法范围为 (0..8, 0..8):

local id = world.placeCorner("wood-post", 4, 3);
local occupant = world.getCornerOccupant("posts", 4, 3);

corner 与 cell/edge 独立占用,同一坐标可同时存在地板、墙和柱。跨层柱可设置 supportMode:"corner_below",并通过 supportTag 限定下层柱类型。C++ 应优先调用返回 结构化结果和 owning snapshot 的 placeCornerResult。Ghost 使用 setCorner(world,x,y), PlacementSession 使用 updateCorner(world,x,y);放置、查询、移动、拆除、替换和结构级联均 保持 corner 域。building_editing v3 快照保存独立 cornerX/cornerY,撤销与重做不会把柱子 错误恢复成 cell 对象;旧 v1/v2 操作仍可读取。

BuildingFX 在顶点锚点生成 corner 视觉。3D 可用 visual3d.width/depth/height,2D 可用 visual2d.width/height;未配置宽深时使用 size,再回退到较短格边的 20。Ghost 和占地 光标使用同一尺寸规则。

自由对象

placementKind:"free" 保存未吸附的世界平面坐标,适用于家具、植被与装饰物。定义通过 freeRadiusCells 声明圆形碰撞 footprint(单位为较短格边,默认 0.25)。同时提供正数 freeFootprintWidthCells 与 freeFootprintHeightCells 时改用随自由旋转姿态变化的 OBB; 宽度沿网格局部 X、长度沿网格局部 Y。freeFootprintVertices 可进一步提供以锚点为原点的 凸多边形局部 x/y 坐标对(单位仍为格);它的优先级高于 OBB。数组必须包含 3–64 个顶点、 保持一致绕序且不能为凹多边形,否则注册时清除该多边形并回退 OBB/圆形。仅同楼层、同 channel 的 free 对象彼此阻挡,cell/edge/corner 占用保持独立:

[
{"id":"tree","placementKind":"free","snapMode":"free","rotationMode":"free",
"channel":"decor","freeRadiusCells":0.3,"renderMode":"3d"},
{"id":"bench","placementKind":"free","snapMode":"free","rotationMode":"free",
"channel":"decor","freeFootprintWidthCells":2.0,"freeFootprintHeightCells":0.5},
{"id":"diamond-table","placementKind":"free","snapMode":"free","rotationMode":"free",
"channel":"decor","freeFootprintVertices":[-1,0, 0,-0.4, 1,0, 0,0.4]}
]
session.startPlacement(world, "tree");
session.updateFree(world, 12.25, 17.75, 0.5);
local id = session.execute();

placeFree/canPlaceFree/getFreeOccupant 提供直接脚本访问;碰撞返回稳定原因 free_overlap。圆形、OBB、凸多边形之间的任意组合均使用实际旋转姿态检测;点选也使用 同一权威 footprint。移动保持精确小数坐标、elevation 与自由旋转。BuildingFX 的宽深未 显式配置时回退为圆形直径或多边形局部包围尺寸。building_editing v6 快照保存 freeRadius、freeHalfWidth、freeHalfHeight 与 freeFootprintVertices,并通过 makeMoveFree 构造不经过格子吸附的可逆移动。setFromSurface 对 free 定义保留精确命中、 surface identity/revision、法线、切线和采样统计,并在提交前应用坡度与高度差限制; BuildingFX 使用同一表面框架对齐视觉。free 对象也可通过表面 Ghost 在不同自定义表面间 原子移动;成功时位置与完整表面附件进入同一收据/事件,限制校验失败则不改变实例。 OBB/凸多边形的表面 Patch 会采样中心和全部旋转顶点,跨 surface identity/revision 会拒绝 提交。provider revision 主动变化后的已放置实例重采样,以及凹多边形/多轮廓 footprint 留待后续增强。

移动与替换也通过同一个会话原子提交。先把鬼影更新到目标姿态,再执行移动;替换使用 会话当前选择的建筑定义,并保持原 instance id、属性、驻军和表面附件:

session.startPlacement(world, "wood-chair");
session.updateFromWorld(world, targetX, targetY);
if (session.executeMove(existingId) == 0)
print("move committed\n");
session.startPlacement(world, "stone-chair");
if (session.executeReplace(existingId) == 0)
print("replace rejected\n");

executeMove 和 executeReplace 返回命名状态(Committed 为 0,Rejected 为 1), 成功后 getLastEditedId() 返回受影响实例。移动支持 cell、edge 与 corner 鬼影;替换要求新旧定义 属于同一 placement domain。冲突、越界、未知定义或跨 domain 替换均在写入前拒绝,不发送 部分事件。C++ 应优先使用 moveBuildingResult、moveEdgeResult 和 replaceBuildingResult 获取结构化诊断及 before/after receipt。

矩形与圆形笔刷先预览、再原子提交:

session.startPlacement(world, "floor");
session.previewRectangle(2, 3, 6, 7); // 或 previewBrush(cx, cy, radius)
fx.updateAreaPreview(world, session);
fx.setGridVisible(world, true); // drawGrid2D/drawGrid3D 显示绿/红热图
if (session.executeRectangle(2, 3, 6, 7) == 0) {
for (local i = 0; i < session.getLastPlacedCount(); ++i)
print(session.getLastPlacedId(i) + "\n");
}

预览按行主序返回每个锚点,getAreaPreviewAccepted/Reason 可显示具体冲突。圆形笔刷使用 包含边界的欧氏格距。区域内任一锚点非法或候选 footprint 互相覆盖时,执行会整体拒绝; 不会产生部分建筑。BuildingFX 的 2D/3D 热图直接复制会话预检结果,绿色表示可提交,红色 表示占用、越界、地形或区域内部冲突。

C++ 编辑工具应通过 building_editing::BuildingPlacementTarget::makeRectangle 或 makeBrush 生成一个 DomainOperation,再交给 LocalWorldAuthority。整个矩形或笔刷只有一个 revision、 一个 transaction receipt 和一个撤销记录;撤销会删除该手势创建的全部精确 instance id。 如果提交前出现新的占用冲突,staging candidate 会整体丢弃,权威世界与 Building 事件队列不变。

需要由同一个工具栏切换多种绘制方式时,使用统一 Placement Pattern 协议。当前内置 edge_line、edge_path、edge_cubic_bezier、rectangle_fill、rectangle_outline 和 circle_brush 六种模式;它们共享 owning request、非变异 preview、结构化失败和一次性原子 placement receipt。旧的 executeEdgeLine、executeEdgePath、executeEdgeCurve、矩形和 刷子入口仍保留,但现在都投影到相同权威执行路径。

session.startPlacement(world, "floor");
session.beginPattern("rectangle_outline");
session.appendPatternPoint(2, 2);
session.appendPatternPoint(8, 6);
if (session.previewPattern() == 0) {
fx.updateAreaPreview(world, session);
session.executePattern();
}

区域 preview 可继续通过 BuildingFX 热图显示;通用 UI 可读取 getPatternPreviewCount/X/Y/Axis/Accepted/Reason,无需理解每种模式的内部结果类型。矩形边框 按行主序生成唯一周长锚点,单行或单列不会重复。离散边和区域坐标必须是有限整数;Bezier 控制点保持连续逻辑坐标。只有 Bezier 当前接受 setPatternSurface,其他模式传入 surface 会以 结构化 InvalidArgument 拒绝。

视觉双形态与渲染桥(eve.BuildingFx)

fx <- eve.BuildingFx();
fx.attach(world);
fx.sync(world); // 按定义 renderMode/visual2d/visual3d 生成/同步/销毁视觉
fx.updateGhost(world, ghost); // 每帧:鬼影 + 占地光标(绿/红)
fx.setGridVisible(world, true);
fx.drawGrid2D(world, gfx); // 2D 网格叠加(渲染循环中调用)
fx.drawGrid3D(world, gfx, 0.01); // 3D 平面线框

放置会话(eve.Building.newSession)

session <- building.newSession();
session.startPlacement(world, "house");
session.updateFromSurface(world, "plane", hitX, hitZ); // 或 updateFromWorld / updateFromWorld3D
session.setMode("remove");
session.execute(); // place 放置 / remove 拆除

新增脚本 API 索引

以下名称是上述多层、表面、边/角/free 域、区域工具和统一 Pattern 协议的脚本绑定索引:

  • 世界查询与校验:canPlaceCorner、canPlaceCornerReason、canPlaceEdge、 canPlaceEdgeReason、canPlaceFreeReason、canRemoveBuildingReason、isCornerEmpty、 isEdgeEmpty、getCornerOccupantAtLevel、getFreeOccupantAtLevel。
  • 层与实例元数据:getActiveLevel、getFloorHeight、getBuildingLevel、 getBuildingPlacementKind、getBuildingConnectionGroup、getEdgeAxis、 getEdgeVariant、getEdgePathVertexCount、getEdgeCurveControlPointCount。
  • 已提交表面帧:getBuildingSurfaceNormalX、getBuildingSurfaceNormalY、 getBuildingSurfaceNormalZ。
  • Ghost 域与表面 patch:setEdge、setFree、getPlacementKind、 getSurfaceSampleCount、getSurfaceMaxSlopeDegrees、getSurfaceHeightDelta、 getSurfaceNormalX、getSurfaceNormalY、getSurfaceNormalZ、getSurfaceTangentX、 getSurfaceTangentY、getSurfaceTangentZ。
  • 区域工具:executeBrush、getAreaPreviewCount、getAreaPreviewCellX、 getAreaPreviewCellY、getAreaPreviewReason。
  • Pattern 配置与预览:getPatternKind、getPatternPointCount、setPatternRadius、 setPatternSubdivisions、getPatternPreviewX、getPatternPreviewY、 getPatternPreviewAxis、getPatternPreviewAccepted、getPatternPreviewReason。