Compare commits

...
72 Commits
Author SHA1 Message Date
moyu 9fb16e45c5 Merge branch 'master' of https://git.yvmou.cn/yvmoux/agame 2026-10-27 15:03:31 +08:00
Ryan-WZ d2b0a47850 新增载具jeep模型 ,测试decals应用(成功) 2026-10-27 15:02:15 +08:00
moyu e2491bc4e1 godot ai 更新 2026-10-23 16:56:35 +08:00
moyu 5c0adb3a96 2026-10-16 2026-10-16 16:01:18 +08:00
moyu b8bbe21c62 场景画面效果优化 2026-10-15 21:51:30 +08:00
moyu 1822a2211e 0 2026-10-15 17:02:45 +08:00
moyu a76c4aebef limbo hsm 2026-10-15 16:56:45 +08:00
moyu 18e7b2daa8 docs\limboai-stable 2026-10-15 16:30:44 +08:00
moyu ecebf76f47 toon_refresh 2026-10-14 16:36:34 +08:00
Ryan-WZ 877d3bbad0 Shader Test 2026-10-13 23:37:09 +08:00
moyu f2e06e9dee 移除碰撞 2026-10-13 19:42:47 +08:00
moyu a1d7007241 shader 2026-10-13 19:39:58 +08:00
moyu ed1a9949c8 shader 2026-10-13 18:42:20 +08:00
moyu f296b643d4 Update cel-shader-base.gdshader 2026-10-13 15:27:58 +08:00
moyu c81571ec1e SHADER 2026-10-13 15:27:01 +08:00
moyu 850d657404 Delete Light_Floor3.fbx.import 2026-10-13 15:08:59 +08:00
moyu 322c1c22eb Merge branch 'master' of https://git.yvmou.cn/yvmoux/agame 2026-10-13 15:08:43 +08:00
Ryan-WZ 020c2a50ce 【场景】 修改地板 测试灯光 以及修改sconce 的alpha通道 2026-10-13 15:07:36 +08:00
moyu 9d60807642 一些脚本 2026-10-12 17:48:27 +08:00
moyu fc89790363 移除FBX文件夹 2026-10-12 17:40:08 +08:00
moyu 02c8d36f8d 可拖拽判断(PickupItem 标记) 2026-10-12 17:05:19 +08:00
moyu 77fefd2505 Update project.godot 2026-10-12 16:13:03 +08:00
moyu 4ef1a30767 Update player.tscn 2026-10-12 14:46:49 +08:00
moyu 7934537793 Merge branch 'master' of https://git.yvmou.cn/yvmoux/agame 2026-10-12 14:43:55 +08:00
Ryan-WZ f381769a7a 【场景修复地毯材质 2026-10-11 01:51:46 +08:00
Ryan-WZ ab24c39d0e 【场景】 修改: 宴会厅 、新增:墙灯 波斯地毯 2026-10-10 18:36:59 +08:00
moyu 93783b1bd9 debug和修复 2026-10-10 16:07:53 +08:00
Ryan-WZ a0dfe7b483 Merge branch 'master' of https://git.yvmou.cn/yvmoux/agame 2026-10-09 16:42:46 +08:00
Ryan-WZ 87b8b110a5 场景 新增窗帘 light switch 宴会桌及摆件 2026-10-09 16:42:20 +08:00
moyu 3dd9d2ec71 修复摄像机穿模 2026-10-09 16:36:17 +08:00
moyu e06818bc5a 调整角色移速 2026-10-09 16:00:23 +08:00
moyu 7de2090b6f 指针资产 2026-10-09 15:54:39 +08:00
moyu 96e21757c3 人物控制器1 2026-10-09 15:51:24 +08:00
moyu d20d45fffc 修复拖拽时物体一卡一卡 2026-10-09 15:32:34 +08:00
moyu bc88fef43a 手掌指针 2026-10-09 15:28:00 +08:00
moyu bbb0c5db14 按 Q 投掷 2026-10-09 15:17:05 +08:00
moyu 3920b88041 修改为长按拖拽 2026-10-09 15:14:23 +08:00
moyu 66d87d48d6 组件模型 2026-10-09 15:10:44 +08:00
moyu 7fd1d97099 准心 2026-10-09 15:03:52 +08:00
moyu 4950ee3b7c Merge branch 'master' of https://git.yvmou.cn/yvmoux/agame 2026-10-08 18:41:49 +08:00
Ryan-WZ 38adb3ae78 新增 Banquet table (场景1楼) 2026-10-08 18:41:26 +08:00
moyu e07ba7fc46 使用sky3d 2026-10-08 17:42:03 +08:00
moyu 27b9788a04 sky3d 2026-10-08 17:38:08 +08:00
moyu 4765dfaa02 Update main.tscn 2026-10-08 17:22:06 +08:00
moyu 475a026604 重构拖动物品功能 2026-10-08 17:22:02 +08:00
moyu 8e463d9247 移除绳索 2026-10-08 17:21:44 +08:00
moyu 1bd447a56e 移除可抱起物品 2026-10-08 16:40:43 +08:00
moyu 9b1d30339f 移除测试 2026-10-08 16:40:32 +08:00
Ryan-WZ 39ddf9ea0b Add new interior asset pack 2026-10-08 15:22:15 +08:00
moyu a5c08088b4 整理代码 2026-10-06 14:15:36 +08:00
Ryan-WZ ed05fb9fd8 场景Step2(厨房,宾客,社交休息,书房,员工休息室) 2026-10-06 13:48:19 +08:00
Ryan-WZ 165d546662 场景1(接待室) 2026-10-05 15:07:56 +08:00
moyu 88d4f12b56 资产管理插件 2026-10-05 14:57:41 +08:00
moyu b826bfb562 自研插件:自动编组 2026-10-04 19:46:30 +08:00
moyu 8ebd331b20 Update main2.tscn 2026-10-04 19:29:30 +08:00
moyu a17800db9c 更新自研插件 2026-10-04 19:27:54 +08:00
moyu 604612baac 更新自研插件 2026-10-04 18:32:58 +08:00
moyu b99952d3c9 更新自研插件 2026-10-04 17:19:42 +08:00
moyu dfd8636476 更新自研插件 2026-10-04 17:09:39 +08:00
moyu 59bd07f630 d 2026-10-04 16:40:49 +08:00
moyu a8ad5c8a71 csg_blockout删除 2026-10-04 16:39:37 +08:00
moyu 03b16e226e 删除surface snap 2026-10-04 16:38:56 +08:00
moyu 8a5ba71ecf 删除 Asset Drawer 2026-10-04 16:37:53 +08:00
Ryan-WZ 93f03ed728 Update main2.tscn 2026-10-04 16:36:16 +08:00
Ryan-WZ 3ebce418da 1111 2026-10-04 16:09:35 +08:00
moyu 1648d6d9b1 插件 2026-10-04 15:01:12 +08:00
moyu 3d23804fa5 拾起物品同步 2026-10-03 18:48:04 +08:00
moyu 0dcfb88bfe Update main2.tscn 2026-10-03 18:47:39 +08:00
moyu deb6dc3e09 Update main2.tscn 2026-10-03 18:39:51 +08:00
moyu b7c644af0b Merge branch 'master' of https://git.yvmou.cn/yvmoux/agame 2026-10-03 18:32:55 +08:00
moyu 6f3c89a4d2 抱起物品 2026-10-03 18:04:07 +08:00
moyu bc90e6d7b2 修复玩家脸部 2026-10-03 17:26:42 +08:00
771 changed files with 142201 additions and 14519 deletions
+2
View File
@@ -1,3 +1,5 @@
# Godot 4+ specific ignores
.godot/
/android/
addons/limboai/bin/~liblimboai.windows.editor.x86_64.dll
addons/godotsteam/win64/~libgodotsteam.windows.template_debug.x86_64.dll
+183
View File
@@ -0,0 +1,183 @@
# 2026-10-03
## 修「表情糊到肚子上」:脸部矩形越界 + 加护栏
用户先发了一张截图问「这是为什么」(本会话 Read 工具读不了图片文件,只能靠像素统计猜),随后说「继续」。
直接跑 `test_run` 就复现了问题:
```
表情矩形左下角 (0.695, 0.482) 跑出了头部 UV 孤岛 [P: (0.694607, 0.681596), S: (0.301457, 0.314468)]
(中心 (0.8455, 0.639) 大小 (0.301, 0.314))
```
用户在 shader 里把 `face_center.y` 从 0.839 调到了 **0.639**(想「往下挪」),矩形下边落到 0.482,
而头部孤岛底边是 0.6816 —— **矩形伸进了正下方那段孤岛(u 0.695~0.935 / v 0.293~0.674,胸腹)**,
所以表情同时糊到了肚子上。这就是截图里那个现象。
修法(`face_paint.gdshader`):
- 顶部新增两个常量 `FACE_UV_BOUNDS_MIN = vec2(0.6946, 0.6816)` / `FACE_UV_BOUNDS_MAX = vec2(0.9961, 0.9961)`
—— 就是头部孤岛的实测范围,当护栏用。
- `fragment()` 里 `clamp(face_center, FACE_UV_BOUNDS_MIN + half_size, FACE_UV_BOUNDS_MAX - half_size)`,
**怎么调都夹在头部里,不可能再溢到肚子/背上**。`min/max` 包一层是为了 size 大于孤岛时不出现 lo>hi。
- 默认值改成 `face_center = (0.8455, 0.7816)`、`face_size = (0.301, 0.20)`:
孤岛高 0.3145,整高时根本没法往下挪(可移动范围 = 孤岛 − 大小),收到 0.20 后中心才能落到 0.7816,
表情落在头部下半段,符合用户「往下移」的诉求。
测试:`test_face_rect_inside_head_island` 换成 `test_face_uv_bounds_match_head_island` ——
校验护栏常量等于实测头部孤岛(模型改 UV 会红),并校验 `face_size` 不大于孤岛(否则夹取退化)。
`_shader_vec2()` 的匹配前缀从 `uniform vec2 X = vec2(` 放宽成 `X = vec2(`,这样 const 也能解析。
另加一条断言:shader 里必须出现 `clamp(face_center`。
验证:`test_run` **32/32**;实机跑角色选择页无 shader/脚本报错,自己截了两帧确认表情只在头部、肚子上是干净的。
## 顺带发现:我能看游戏截图
`Read` 工具读用户发的图片会直接报 `Current model does not support reading images`,
但 `editor_screenshot(source="game")` 返回的画面我能正常看。以后排查画面问题可以直接自己截图对照,
不必让用户描述。
## 再往下挪一点:只能靠压高度
用户要求「往下移一点」。中心 0.7816 已经等于孤岛下边界 + 半个高度,直接减小 center.y 会被 clamp 吃掉(等于没反应),
所以改成 **`face_size.y` 0.20 → 0.17、`face_center.y` → 0.7666**(0.7666 − 0.085 = 0.6816,仍然贴着下边界)。
自己前后各截一帧对比,表情确实整体下移了一点,代价是纵向压扁 15%。
记下这条边界:**头部孤岛下边界 0.6816 就是脸能到的最低处**,再往下就是胸腹孤岛,没有余量;
要「整张脸整体下移且不变形」只能回 Blender 把头部 UV 往下扩。
`test_run` 32/32。
## 换成「贴花」:脸终于能放到身体正中间
用户接着发图说「还需要往下移,脸部应该位于中间位置」。UV 方案已经到极限(再往下就是胸腹孤岛),
所以直接把定位方式换成**贴花**:`face_paint.gdshader` 里加 `varying vec3 object_position / object_normal`,
`vertex()` 里取 `VERTEX` / `NORMAL`,`fragment()` 用 `object_position.xy` 定位矩形,再用
`-normalize(object_normal).z` 筛掉背面。uniform 变成 `face_center` / `face_size`(模型本地坐标)、
`face_front_edge`、`face_flip_y`;`FACE_UV_BOUNDS_*` 那对护栏常量和 clamp 都删掉了(不再需要)。
好处:**和 UV 孤岛布局彻底解耦**,以后 Blender 重排 UV 也不会再把脸画歪。
踩到的坑:第一版把正面判成 `+normal.z`,结果表情画到了**背上**。
`character_select.tscn` 的 `Model` 节点和 `player.tscn` 的 `mouse` 节点都把模型转了 180°,
**模型自己的正面是本地 -Z**。测试里钉了 `-normalize(object_normal).z` 防止再写反。
调参过程(用截图迭代):
- 为了看正面,临时把 `character_select.gd` 的 `SPIN_SPEED` 改成 0(模型初始朝正面),**调完已还原成 0.5**。
- 内置表情在 tile 里的墨线只占 ~70%(face_01 68.9%×74.4%、face_02 83.9%×65.4%,且居中),
所以 `face_size` 要按这个比例估:4.0 的矩形 → 约 2.8 单位的墨线宽度,正好对上用户截图里脸占身体 ~55% 的感觉。
- 最终 `face_center = (0.0, 0.0)`、`face_size = (4.0, 4.0)`:实测脸落在身体正中间,模型原点就是蛋形身体的中轴。
- 测试 `test_face_rect_inside_head_island` / `test_face_uv_bounds_match_head_island` 删掉,
换成 `test_face_decal_inside_body_bounds`(矩形必须落在模型包围盒内)。
验证:`test_run` **32/32**;主菜单 →「开始游戏」→ 角色选择实机截图确认 预设表情1 落在身体正中间。
## 坑:用户开着 Godot 编辑器时,磁盘上的 shader 会被编辑器缓冲区覆盖回去
调完贴花后回头一看,`face_paint.gdshader` 变成了「我的新注释 + 旧的 `+normal.z` 代码 + `face_center.y = 0.6`」的混合体:
我改的 `-normalize(object_normal).z` 和 `face_center.y = 0.0` 被冲掉了,而 `0.6` 这个值我从没写过。
判断是**用户在编辑器里开着这个 shader**(缓冲区还是我第一版的内容),他改了参数并保存,把我后来的改动覆盖了。
处理:用整份 `Write` 重新写回验证过的版本(`face_center = (0.0, 0.0)`、`-normalize(object_normal).z`、
并把顶部那句「法线朝 +Z」的旧注释一并改对,避免注释和代码互相矛盾),再跑 `test_run` 32/32。
**教训**:改 `.gdshader` / `.tscn` 这类编辑器可能正开着的文件之后,最好提醒用户先关掉或重开那个文件再保存;
否则他那边一保存就会把磁盘上的新版本覆盖掉,而且不会有任何提示。
## 实现:抱起物品系统(第 1 步,单机跑通)
用户要求:左键抱起、长按 Q 扔出、抱起后胳膊伸出去抱住且走路会晃、胳膊可延伸、只有特定物品能抱并给个示例。
新增/改动:
- `world/items/carryable.gd`(`class_name Carryable extends RigidBody3D`,**标了 @tool**):
门槛在物品自己身上 —— 进 `carryable` 组才算「可抱」。参数:`hold_offset` / `hold_rotation_deg` /
`freeze_while_carried` / `throw_speed_min` / `throw_speed_max`。
`begin_carry()` 冻结物理并**把 collision_layer 清 0**(不清会把抱着它的人顶飞);`end_carry(v)` 恢复并给初速度;
`apply_hold_transform(socket)` 由持有者每帧调用,偏移/朝向由物品自己决定。
- `world/items/carryable_box.tscn`:示例物品(0.6³ 橙箱子,RigidBody3D + Mesh + Collision + 脚本)。
- `character/player/carry_arms.gd`(`class_name CarryArms extends Node`,也标了 @tool):
抱持姿势 + 走路晃动的表现层。**动画之后叠加**(`process_priority = 100`),只动 arm_l_*/arm_r_* 六根骨骼,
晃动同时作用在骨骼和抱持点上,所以人和东西一起晃。
- `player.gd`:`_find_carryable / _pick_up / _release_throw` 三个小函数;交互提示与判定同源(优先级:手上有东西 > 可抱物 > 缆上 > 可挂的缆);抱东西时禁疾跑 + `carry_speed_scale` 降速;抱着时不给挂滑索。
- `player.tscn`:新增 `CameraRig/CarrySocket`(Marker3D,本地 (0,-0.8,-1.0)) 和 `Body/CarryArms`。
- `world/main.tscn`:放两个示例箱子。
- 输入动作:`carry` = 鼠标左键、`throw_carry` = Q(`input_map_manage ensure_binding`,直接落 project.godot)。
- `tests/test_carry.gd`:7 条(动作存在、玩家场景节点、示例物品可抱、抱起/放下冻结与恢复、贴到抱持点、投掷速度随蓄力、抱持姿势真的伸到身前)。
踩到的坑:
- **`CarryArms` 的 NodePath 少了一级**:默认写成 `mouse/MouseRig/Skeleton3D`,但节点挂在 `Body` 下,
应该是 `../mouse/MouseRig/Skeleton3D`;抱持点同理要 `../../CameraRig/CarrySocket`。
解析不到只会 push_warning,姿势完全不生效、画面看不出原因。
- **骨骼本地轴不能猜**:一开始按骨骼本地欧拉角写,结果手是往**上后方**摆的。
改成「世界方向语义」——参数按世界方向理解,内部用 `rest_basis.inverse() * world * rest_basis`
换算到骨骼本地空间,再乘到动画姿态上。这样换模型也不会错。
- 参数是扫出来的(临时探针一次跑 24 组 swing/tuck/roll,打印 `arm_l_03` 的全局位置):
最终 `shoulder_swing_deg = 40 / tuck = 50 / roll = 0 / elbow = 15 / stretch = 1.25`
→ 手落在模型本地 (±1.17, 0.08, 0.95),即身前中部。
- **非 @tool 脚本在编辑器里是 placeholder**,测试里方法调不动、`_ready` 也不跑(组也加不上)。
`carryable.gd` / `carry_arms.gd` 都标了 `@tool` 才跑得通;改完脚本要 `filesystem_manage(op="scan")`
才会重新解析(不 scan 的话测试还按旧代码跑)。
- 顺手纠正:脸部贴花的正面是模型**本地 +Z**(尾巴在 -Z),我之前写成 `-normalize(object_normal).z` 是错的,
已改回 `+`,`tests/test_face.gd` 的断言也跟着反过来。
验证:`test_run` **39/39**;实机跑通完整链路 —— 提示「左键 抱起」→ 左键抱起(箱子出现在身前)→
长按 Q(提示「长按 Q 扔出 100%」)→ 松手箱子飞出去落地。
**未做**:联机同步(第 2 步)。现在物品位置只有持有者本地在写,远端玩家看不到箱子被抱走 —— 需要给物品加
`MultiplayerSynchronizer` + 跟随持有者的 authority 切换 + 抢占仲裁。
## 补上:第一人称看得见胳膊 + 抱起物品联机同步
用户要求「玩家需要看到自己胳膊,联机也需要同步」,并给了 `CLAUDE.md`:
**「游戏内测试交给用户来,不要自己做」** —— 已记进 MEMORY.md,之后不再自己跑 `project_run` 截图验证。
**第一人称留胳膊**:`player.gd` 的 `_hide_body_keep_arms()` 用栈遍历 Body 下所有 MeshInstance3D,
按 `arm_` 前缀筛出胳膊留着、其余藏掉;一个都没筛到(换模型改名)就退化成整个 `body.visible = false`。
**联机同步**(关键设计:只同步一个整数):
- 新增 `world/items/carryable_sync.tres`(同步 `.:position` / `.:rotation` / `.:carrier_peer`),
`carryable_box.tscn` 里加 `Sync`(MultiplayerSynchronizer)节点。
- `Carryable.carrier_peer` 带 setter:非 0 就 `freeze = true` + `collision_layer = 0`;
为 0 时 `freeze = not multiplayer.is_server()`(只有服务器模拟,其余端跟着同步走)。
- **抱着时位置不走同步**:`_physics_process` 里 `_find_carrier_socket()`(玩家节点名 = peer id,组 `net_player`)
把物品摆到持有者抱持点上。每端都这么算 → 天然一致,也就没有「写位置 / 同步写回来」打架的问题。
- **权威永远在服务器**:`request_pickup(peer_id)` / `request_release(velocity)` 两个 RPC,
客户端 `rpc_id(1, ...)`、单机(is_server)直接调;服务器只改 `carrier_peer`,放下时顺手写 `linear_velocity`。
- 玩家侧:`_pick_up()` 乐观认领 + `pickup_confirm_time`(0.6s)超时松手;`_release_throw()` 把初速度发给服务器。
**顺手发现(重要,未改,等用户确认)**:`player.tscn` 的根节点 `Player` 现在带
`transform = Transform3D(0.2, 0, 0, 0, 0.2, 0, 0, 0, 0.2, 0, 0, 0)` —— 整只玩家被缩到 20%
(胶囊 0.36m、相机 0.32m、模型 0.23m),而且这个缩放会漏给「挂在编辑中场景根下」的节点:
`tests/test_zipline.gd` 就是这么挂滑索的,于是 `test_tangent_points_from_a_to_b` 红了
(实测 tangent(0)·along = 0.888 < 0.9;zipline.gd 的 `sample()` 用全局锚点 lerp 却加**未缩放的 sag**,
缩放一进来形状就偏了)。这行是我读 scene 之后才出现的,来源是提交 `6f3c89a 抱起物品`(用户提交的工作区)。
`test_run` **40/41**(唯一红的就是上面那条滑索测试)。
## 用户确认:玩家缩放 0.2 是有意的 → 滑索改成缩放无关
用户明确「就是想将玩家缩小到 0.2 倍」,所以那行不改。但滑索那条测试还得救,而且它暴露的是**真 bug**:
- `zipline.gd` 的 `sample()` 用 `anchor_a.global_position.lerp(anchor_b.global_position, t)` 再减
`UP * sag * 4t(1-t)` —— 前半截跟着父节点缩放、后半截不跟,父节点一有缩放曲线形状就偏。
`tangent()` / `ride_point()` 同样的混用。
- 改成**先在本地空间算曲线**(`_curve_local()`,原来的 `_local_sample` 从「把世界点转回本地」变成直接算本地),
`sample()`/`ride_point()` 用 `to_global()` 转世界,`tangent()` 用 `global_transform.basis * d` 转方向。
缆绳网格本来就在本地空间生成,现在少了一次多余的 `to_local`。
- 测试夹具原来挂在**编辑中场景根**下,于是玩家根的 0.2 缩放漏进滑索。改成挂在
`Engine.get_main_loop().root` 下的中性父节点(`_make_holder()`),夹具不再受编辑中场景影响。
- 新增 `test_shape_survives_parent_scale`:故意套一个 0.2 缩放的父节点,验 tangent 不歪、
中点下垂 = sag × 0.2。这条把刚踩的坑钉住了。
`test_run` **42/42**。
**留给用户决定的尺度问题**(我只报告,没改):玩家缩到 0.2 之后,世界单位的参数都还是老尺度 ——
`walk_speed 5 / sprint 8.5 / jump 5`(= 14 身高/秒)、`pickup_radius 3.0`(8 身高)、
滑索 `attach_radius 4.0` / `hang_offset 1.15`(人会吊在缆下方 3 个身高)、物品 `throw_speed 5~14`、
示例箱子 0.6m(比 0.36m 的玩家还高)。要不要按 0.2 一起缩,等用户拍板。
## 给「抱起物品系统」出了方案(未动代码)
用户要新功能:玩家抱起物品,先要方案。我在对话里给了设计,没有写文件。
要点:`Carryable` 组件挂在物品上(组 `carryable`,照抄 Zipline 的「参数放组件、玩家只读」套路);
玩家侧只加 `_find_carryable/_pick_up/_drop` 三个小函数 + `CameraRig/CarrySocket` 标记点;
交互优先级和提示文案共用一处 `_resolve_interaction()`(滑索踩过「提示亮着但按 E 没反应」的坑);
抱持**不 reparent**,改成 `carried_by` 状态 + 持有者每帧摆到 socket,authority 随持有者切换;
分三步:单机跑通 → 联机仲裁/同步 → 持物动画与扔出。等用户拍板几个点(E 是否复用、要不要真物理/扔、模型有没有持物动画)。
+63 -7
View File
@@ -2,6 +2,17 @@
## 工程约定
- **`CLAUDE.md` 里写着「游戏内测试交给用户来,不要自己做」** —— 别用 `project_run` + 截图去自己验游戏,
编辑器里的 `test_run` 照常跑。画面问题让用户看,或者用数值探针验。
- **玩家根节点 `Player` 有意缩放 0.2**(用户明确说「就是想将玩家缩小到 0.2 倍」):胶囊 0.36m、相机 0.32m、
模型 0.23m。挂在玩家下的东西(`CameraRig/CarrySocket`、`Body/*`)**本地坐标会自动跟着缩**,所以抱持点不用改;
但**世界单位**的参数没跟着缩:`walk_speed/sprint_speed/jump_velocity`、`pickup_radius`、
滑索的 `attach_radius/hang_offset`、物品的 `throw_speed_*`、示例箱子尺寸(0.6m 比玩家还高)都还是老尺度。
- **滑索的曲线在「本节点局部空间」算**(`_curve_local()`),`sample/tangent/ride_point` 再转成世界 ——
父节点带缩放时整条缆一起缩、形状才不走样(早前混用 global 锚点 lerp + 未缩放的 sag,父节点一缩放
tangent 就歪 27°)。测试夹具同理**别挂在编辑中场景根下**:玩家的 0.2 缩放会把滑索几何带偏
(实测中点下垂 1.2 → 0.24)。滑索测试现在挂在编辑器根节点下的中性父节点里,另有一条
`test_shape_survives_parent_scale` 专门钉住缩放行为。
- 玩家控制器只认「运动结果」,不认识任何动画片段名;表现层脚本 `player_animator.gd` 直接挂在
`Body/AnimationPlayer` 上(`animation_player` 留空即自绑自身,所以两者是同一个节点)。
新增动作状态改 `ANIM_CLIPS`,不要往 `player.gd` 里塞片段名。
@@ -52,22 +63,57 @@
- `face_paint.gdshader`:将 face surface 的 UV remap 到 0~1,再映射到内置图集 rect 或本地 PNG;贴图 alpha 只代表墨线覆盖率,混到 `skin_color` 上,**不写空间 `ALPHA`**(否则脸片变洞)。
- `mouse_mouse_lambert1_BaseColor.png` 和 `mouse_表情测试.png` 的背景都精确为 RGB(137,179,255) / `#89B3FF`;表情图源 alpha 全 255,不能直接当透明度。
- **最终做法:全身一个 shader,脸部靠 UV 矩形圈出来**。`face_paint.gdshader` 用 `body_tex`(= `mat_body` 的 BaseColor)采样全身底色,再在 `face_uv_min` / `face_uv_size` 圈出的矩形里,把表情贴图(内置图集或本地 PNG)和玩家手绘按 alpha 叠上去。透明处保持身体底色,所以**脸和周围不可能是两块不同的皮肤**。`FaceRig` 把 mat_body 的 `albedo_texture` / `albedo_color` / `roughness` / `metallic` 抄进 shader,并用 `set_surface_override_material(0, mat)`(surface 0,不再是 1)。
- **最终做法:全身一个 shader,脸是「贴在身体正面的贴花」**。`face_paint.gdshader` 用 `body_tex`(= `mat_body` 的 BaseColor)采样全身底色,再在 `face_center` / `face_size` 圈出的矩形里,把表情贴图(内置图集或本地 PNG)和玩家手绘按 alpha 叠上去。透明处保持身体底色,所以**脸和周围不可能是两块不同的皮肤**。`FaceRig` 把 mat_body 的 `albedo_texture` / `albedo_color` / `roughness` / `metallic` 抄进 shader,并用 `set_surface_override_material(0, mat)`(surface 0,不再是 1)。
- **贴花用模型本地坐标,不用 UV**:`varying vec3 object_position` / `object_normal` 在 `vertex()` 里取 `VERTEX` / `NORMAL`,`fragment()` 里用 `object_position.xy` 定位。**这样和 UV 孤岛怎么排无关** —— 之前用 UV 矩形时,矩形一越界就会把表情糊到肚子/背上(头部孤岛和胸腹孤岛只差 0.008),换贴花后这类问题从根上没了。
- **模型自己的正面是本地 -Z**:`character_select.tscn` 的 `Model` 节点和 `player.tscn` 的 `mouse` 节点都把它转了 180°。所以正面遮罩写 `-normalize(object_normal).z`,**写反了表情会跑到背上**(踩过)。背面同 (x,y) 的点也会落进矩形,所以这个遮罩是必须的。
- 不再需要 `skin_color` 当 shader 输入:`FaceRig` 反过来从身体贴图左上角取色写进 `skin_color`,只给画布和缩略图垫底用(导入贴图是压缩的,`get_pixel` 前要先 `decompress()`,否则报 "Can't get_pixel() on compressed image")。
- 表情遮罩用 `step()` 相乘而不是 `if`:采样留在统一控制流里,免得 mipmap 导数在分支里未定义。
- **仍然不能写空间 `ALPHA`**:alpha 只代表墨线覆盖率,写进 ALPHA 会让面片变洞。
- `face_atlas_transparent.png` 是我们自己的素材(从已删除的 `mouse_表情测试.png` 去蓝底、反混合出来的 1024² 副本),保留原图坐标排布;内置 ID `builtin:face_01/02` 对应各自的图集 rect。**原始表情图已随模型删除,这个副本现在是唯一来源,别删。**
- **表情位置/大小/翻转只写在 `face_paint.gdshader` 顶部那段「调表情就改这一段」里**:`face_center`(中心,UV 单位,**往下挪就减小 .y**)、`face_size`(.x 宽 .y 高)、`face_flip_y`(默认 true)。
**FaceRig 故意不传这三个 uniform** —— 早期版本从 `@export` 传过去,会把 shader 里调好的值盖掉;现在 `face_rig.gd` 里没有对应的 export,测试里有一条断言禁止再传。
- 头部孤岛实测:u 0.695~0.996(左右)、v 0.682~0.996(上下,**v 越大越靠头顶**)。贴图第 0 行在上边,所以默认必须 `face_flip_y = true`,否则表情上下颠倒(用户实测确认过)。
- **矩形中心 ± 半个大小不能溢出头部孤岛**:正下方还有一段孤岛(u 0.695~0.935 / v 0.293~0.674),量下来是胸腹那一带,溢出去表情就会糊到肚子上。`tests/test_face.gd` 的 `test_face_rect_inside_head_island` 会直接从 shader 源码里读 `face_center` / `face_size` 再判定,**所以随手调这两个值不会被测试误报,只有溢出才会红**。
- 两个 FaceRig 节点(`player.tscn` 的 `Player/FaceRig`、`character_select.tscn` 的预览 `FaceRig`)现在都不带表情矩形参数了 —— 值只有一份,在 shader 里。
- **表情位置/大小/翻转只写在 `face_paint.gdshader` 顶部那段「调表情就改这一段」里**:`face_center`(中心,**模型本地坐标**,模型大约 x -3~3、y -3~3,**往下挪就减小 .y**)、`face_size`(.x 宽 .y 高,默认 4.0×4.0)、`face_front_edge`(法线朝前的阈值,调大只留最正面)、`face_flip_y`(默认 true)。
**FaceRig 故意不传这几个 uniform** —— 早期版本从 `@export` 传过去,会把 shader 里调好的值盖掉;现在 `face_rig.gd` 里没有对应的 export,测试里有一条断言禁止再传。
- 贴图第 0 行在上边,而头部 UV 的 v 朝上,所以默认必须 `face_flip_y = true`,否则表情上下颠倒(用户实测确认过)。
- 内置表情在各自图集 tile 里的墨线占比:`face_01` 68.9%×74.4%、`face_02` 83.9%×65.4%,且都居中 —— 所以 `face_size` 要按「墨线只占 tile 的七成」来估:4.0 的矩形给出约 2.8 单位的墨线宽度。
- `test_face_decal_inside_body_bounds` 校验贴花矩形落在模型包围盒里(模型换尺度/原点会红);`test_face_shader_has_expected_uniforms` 里钉住 `normalize(object_normal).z`(**不能带负号**,正面是本地 +Z)和 `object_position.xy`,防止有人改回 UV 方案或把正面写反。
- 两个 FaceRig 节点(`player.tscn` 的 `Player/FaceRig`、`character_select.tscn` 的预览 `FaceRig`)都不带表情参数 —— 值只有一份,在 shader 里。
- `FaceRig` 保存 `user://face_preset.json` 中的 expression ID 和 `user://face_paint.png` 暂存笔画;已保存的自绘表情用 `user://face_library/index.json` + 各自透明 PNG 持久化,角色选择页动态显示内置 atlas 表情和本地表情。
- 联机当前表情同步:内置表情发送 ID;`user:` 本地表情发送当前 PNG 字节;笔画层另外发送。**不广播整份本地表情库**。
- `FacePainter`:直接改 RGBA8 字节缓冲。画 = source-over alpha 混合,擦 = 把目标 alpha 乘掉;512² 一次 1MB,按一笔/动作同步一次。
- 「自己画」= 空白底起手;保存到本地表情库成为独立项目,再次选择可加载。取消会恢复旧 expression ID 与笔画。
- 界面链路:`ui/main_menu.tscn` →「开始游戏」→ `ui/character_select.tscn`(3D 预览 + 动态表情列表 + 自己画)→ `ui/paint_panel.tscn`(透明笔画层叠在肤色底上)。本地列表用 HFlow + ScrollContainer 显示。
## 抱起物品(world/items/ + character/player/carry_arms.gd)
- **门槛在物品自己身上**:物品根节点(RigidBody3D)挂 `Carryable`(`class_name`,进 `carryable` 组)才抱得起来,
玩家侧不认识任何具体物品。手感和滑索一个套路 —— `hold_offset` / `hold_rotation_deg` / `throw_speed_min/max`
都写在物品上,玩家只负责「找最近的、摆到抱持点、扔出去」。
- 示例物品:`world/items/carryable_box.tscn`(0.6³ 橙箱子),世界场景里放了两个。加新物品只要挂脚本。
- 玩家侧只有 `_find_carryable / _pick_up / _release_throw` 三个函数;**交互提示和按键判定同源**
(优先级:手上有东西 → 可抱物 → 缆上 → 可挂的缆),别再拆成两处。
- 抱持**不 reparent**:`Carryable.carried_by` 记状态,持有者每物理帧 `apply_hold_transform(socket)`。
抱起来时冻结物理 + **collision_layer 清 0**(不清会把玩家顶飞),放下时恢复并继承初速度。
- 输入动作:`carry` = 鼠标左键、`throw_carry` = Q(长按蓄力,松手出手)。鼠标没锁定时左键仍然只用来重新锁定视角。
- 抱东西时禁疾跑 + `carry_speed_scale` 降速;**抱着不给挂滑索**(`_attach_zipline()` 开头直接 return)。
- 抱持点 `CameraRig/CarrySocket`(Marker3D,本地 (0,-0.8,-1.0))。物品位置要跟着它走。
- `CarryArms`(挂在 `Body/CarryArms`)是**表现层**:`process_priority = 100` 保证跑在 AnimationPlayer 之后,
在动画姿态上**叠加**手臂旋转(只动 `arm_l_*` / `arm_r_*`),走路时胳膊和抱持点一起晃。
**参数按「世界方向」语义**(`rest_basis.inverse() * world * rest_basis` 换算到骨骼本地空间)——
直接写骨骼本地欧拉角会得到「手往上后方摆」这种莫名其妙的结果,换模型必错。
默认 swing 40 / tuck 50 / roll 0 / elbow 15 / stretch 1.25 → 手落在模型本地 (±1.17, 0.08, 0.95)。
- 本地玩家第一人称只藏身体、**留下两条胳膊**(`_hide_body_keep_arms()` 按 `arm_` 前缀筛,找不到就整个藏掉)——
抱东西时得看得见自己的手。
- **联机只同步 `carrier_peer` 一个整数**(物品自己带 `MultiplayerSynchronizer`,配置 `world/items/carryable_sync.tres`)。
**抱着的时候位置根本不走同步**:每一端都从「持有者的抱持点」把物品摆出来(`Carryable._physics_process` →
`_find_carrier_socket()`,靠玩家节点名 = peer id、组 `net_player` 找到),结果天然一致,
也避免了「持有者写位置 / 同步又把位置写回来」互相打架。空着的时候才同步 position/rotation。
- **权威永远在服务器**(不切换):物品位置只由服务器模拟,其余端 `freeze = true` 跟着同步走;
抱起/放下都是客户端 `rpc_id(1, ...)` 请求服务器,服务器改 `carrier_peer` 就完事。
- 抱起是乐观认领 + 超时松手:`_pick_up()` 先把自己记成持有者,`pickup_confirm_time`(默认 0.6s)内
服务器没把 `carrier_peer` 改成自己就自动松手 —— 联机下被别人先抢走时会走到这一步。
- 测试 `tests/test_carry.gd`:`Carryable` / `CarryArms` 都标了 `@tool`,否则编辑器里是 placeholder、
方法调不动、`_ready` 不跑(组也加不上);改完脚本要 `filesystem_manage(op="scan")` 才重新解析。
玩家脚本不是 @tool,所以「第一人称留胳膊」那条测试查的是源码 + 模型里真有 `arm_*` mesh。
## Godot 4 踩坑
- **canvas 没有「擦除」混合模式**:`CanvasItemMaterial.BlendMode` 只有 mix/add/sub/mul/premult,
@@ -81,6 +127,14 @@
## Godot MCP 踩坑
- **看画面**:`Read` 工具读用户发的图片文件会报 `Current model does not support reading images`,
但 `editor_screenshot(source="game")` 返回的游戏画面能正常看 —— 排查画面问题直接自己截图对照,
比让用户描述快。没有画面时也可以用像素统计(PNG 手写解码 + 粗 ASCII 图)反推布局。
- **用户在编辑器里开着的文件会覆盖磁盘改动**:改完 `face_paint.gdshader` 后,磁盘上出现过
「我的新注释 + 旧的 `+normal.z` + 我从未写过的 `face_center.y = 0.6`」这种混合体 ——
用户在 Godot 里开着这个 shader、缓冲区是旧版本,他一保存就把我后来的改动冲掉了,而且没有任何提示。
所以改 `.gdshader` / `.tscn` 之后要在回复里提醒对方先关掉/重开那个文件再保存。
- **数值标量经 MCP 会被字符串化**:`node_set_property` 传 `0.15` 能写进去,但回显是 `"0.15"`;
而 `batch_execute` 里包 `{"command": "set_property", "params": {...}}` 直接报
`INTERNAL_ERROR … handler returned malformed result`(和旧版本文档里写的正好相反,以实测为准)。
@@ -99,7 +153,9 @@
- **非 `@tool` 脚本在编辑器里是「placeholder instance」**:`load(path).can_instantiate()` 返回 false,
`.new()` 报 `Nonexistent function 'new' in base 'GDScript'`,`set_script` 后调方法报
`Attempt to call a method on a placeholder instance`(`player.gd` 也一样,不是脚本本身的问题)。
想在 `test_run` 里验非 @tool 脚本,就临时生成一个「去掉 class_name、加上 @tool」的副本喂给测试,跑完删掉。
想在 `test_run` 里验非 @tool 脚本,**最省事的做法是直接给脚本加 `@tool`**(`Carryable` / `CarryArms` 就是这么办的,
运行时行为完全一样),其次才是临时生成一个 @tool 副本喂给测试。
加完要 `filesystem_manage(op="scan")` 一次,否则编辑器还在用旧的非 tool 版本,照样报 placeholder。
- **`test_run` 的 GDScript 编译缓存是按路径的**:删掉再重建同名脚本,跑到的可能还是旧版本
(现象:逻辑明明改了,测试却按旧逻辑失败)。要验新代码就换个文件名,或者重启编辑器。
- **编辑器进程的 `InputMap` 只有内建动作**(`ui_accept` 有,`interact` / `jump` 没有)——
+179
View File
@@ -0,0 +1,179 @@
# 2026-10-09
## 简易准心(Crosshair)
- 新增 `ui/crosshair.gd`:`@tool extends Control`,用 `_draw()` 画十字(gap/length/thickness/dot 可导出调参),`mouse_filter = IGNORE`,监听 `NOTIFICATION_RESIZED` 重绘。
- 挂载约定:`CanvasLayer > Crosshair(Control)`,Control 锚点为 Full Rect,中心即屏幕中心;推荐挂在 `CameraRig` 之下 —— 因为 `player.gd _ready()` 对非 authority 会 `camera_rig.queue_free()`,远端玩家准心随之一并释放,无需额外代码。
- 项目 stretch 模式为 `canvas_items` + `expand`,CanvasLayer 会自动跟随缩放。
### 后续改动:只保留「点」
- 最终形态:准心 = 屏幕中心一个点,无十字、无样式开关(`enum Style` 已移除)。
- 导出参数:`dot_color` / `dot_radius`(默认 2.5) / `outline` / `outline_color` / `outline_width`。
- `_draw()` 先画描边圆再画主体圆,保证浅背景可见。
## player.gd 拆解方案(待执行)
- 现状:`character/player/player.gd` 155 行,混了视角 / 移动跳跃 / 拾取 / 多人权限四类职责。
- 方案:**子节点组件化**(与既有 `PlayerAnimator` 同模式)。根节点降为协调器(~40 行),新增 `Look` / `Motor` / `Pickup` 三个 Node 子节点,各自挂 `player_look.gd` / `player_motor.gd` / `player_pickup.gd`。
- 三条硬约束:① `move_and_slide()` 留在根节点,Motor 只写 `body.velocity`;② 物理顺序由根节点显式编排(motor.step → move_and_slide → animator → pickup),不让子节点各写 `_physics_process`;③ 非 authority 时三个组件的输入/物理必须一并关闭。
- 范式定位:组合优于继承 + 组件模式(Component Pattern);协调器部分带 Facade/Mediator 色彩;`setup()` 注入属手工依赖注入。**不是 ECS**(Godot 节点是「带行为的对象」,非纯数据组件)。
- 状态:**已落地**(2026-10-09)。新增 `player_look.gd` / `player_motor.gd` / `player_pickup.gd`(均带 `class_name`:PlayerLook / PlayerMotor / PlayerPickup);`player.gd` 重写为协调器(~65 行);`player.tscn` 追加 Look / Motor / Pickup 三个 Node 子节点(挂 FaceRig 之后,ext_resource id 13_look / 14_motor / 15_pickup,未写 uid 由 Godot 补)。
- 场景格式注意:本项目的 .tscn 节点带非标准 `unique_id=`(Godot 4.7 特性),`unique_id` 与 `load_steps` 均可省略;`paint_panel.tscn` 就完全没写,可作参照。
- 已确认无外部脚本引用被搬走的属性(仅 `world/main.tscn` 通过场景 uid 引用玩家场景)。
- 顺手去掉了原 `_pickObject` 里的两行 `print()` 调试输出。
### 拾取改为长按拖拽
- `player_pickup.gd`:由「按一下切换」改为「按下抓起、松开放下」——`is_action_pressed("carry")` → `_grab()`,`is_action_released("carry")` → `_drop()`。
- `carry` 动作确认绑定鼠标左键(`button_index: 1`);项目里另有未被任何脚本使用的 `throw_carry`(绑 Q 键),可作投掷功能的预留。
- 加了 `Input.mouse_mode != MOUSE_MODE_CAPTURED` 早退,并在 `player.gd` 重新锁定鼠标那一支补 `get_viewport().set_input_as_handled()`,避免「点一下重新锁定鼠标」顺带抓起物体(两种分支执行顺序下都成立)。
- `physics_step()` 与 `has_object()` 改用 `is_instance_valid()` —— 防止拖拽途中物体被 destroy 时取属性报错。
- `_drop()` 保留线速度(松手有惯性 / 可甩出);想要「就地放下」需补 `set_linear_velocity(Vector3.ZERO)`。
### 按 Q 投掷
- `player_pickup.gd` 接入 `throw_carry`(Q 键)→ `_throw()`:取 `-_hold_pos.global_transform.basis.z` 作投掷方向(持握点挂在摄像机下且仅有平移,其 basis 即视线朝向,故无需再注入摄像机),`set_linear_velocity(dir * throw_speed)` 后置 `_current = null`。
- 新增导出参数 `throw_speed: float = 12.0`。
- 备选手感(注释里留了行):改用 `apply_central_impulse` 则重物飞得慢。
### 可拖拽时准心变手型
- 新增素材 `character/assets/hand_cursor.svg`(新建了 `character/assets/` 目录):简笔手(4 指 + 手掌 + 拇指的圆角矩形),双层 —— 底层深色 `#1a1a1a` stroke-width 4 当描边,上层白色 stroke-width 1.5 覆盖内部接缝。32×32,**未写 `<title>`**(避免 ThorVG 解析风险)。
- `ui/crosshair.gd`:加 `class_name Crosshair`;新增 `hand_texture`(自动加载上面的 svg) / `hand_size`(=26)、`set_grabbable(bool)`;`_draw()` 在 `_grabbable && hand_texture != null` 时走 `_draw_hand()`,否则走 `_draw_dot()`。
- `player_pickup.gd`:新增 `signal hover_changed(grabbable: bool)`,`physics_step()` 里先调 `_update_hover()`——用 `_ray.force_raycast_update()` 强制刷新(否则结果滞后于本帧 move_and_slide,跑动中瞄到物体慢一帧),**只在状态变化时**发信号。
- `player.gd`:`@onready var crosshair: Crosshair = $CameraRig/HUD/Crosshair`;`_ready()` 权威分支里 `pickup.hover_changed.connect(_on_pickup_hover_changed)`;handler 里既切准心,也 `Input.set_custom_mouse_cursor()` 换系统指针(含指尖热点),鼠标未捕获时该行无副作用。
- **注意**:用户侧已把 `AnimationPlayer` 从 `Body/AnimationPlayer` 提到 Player 根节点(`$AnimationPlayer`),并把 `animator` 变量改名为 `animation_player`;相机 scale 改为 5。
### 换用真实光标素材(grab / grab_hover)
- 素材来源:`C:\Users\yvmou\Downloads\cursors\light\`(light 主题 = 深色手型,32×32 RGBA)。
- 已复制到 `character/assets/grab.png`(指着可拖拽物)与 `character/assets/grab_hover.png`(正抓着),Godot 已生成 `.import`。
- **删除了临时的 `character/assets/hand_cursor.svg` 及其 `.import`**(已被真实素材取代)。
- `ui/crosshair.gd` 改为三态:`enum GrabState { NONE, HOVER, HOLDING }`;导出 `hover_texture` / `hold_texture` / `hand_size`(=32,1:1 不缩放) / `icon_modulate` / `sync_os_cursor`;公开 `texture_for_state(state)` 供系统指针复用素材;`_sync_os_cursor()` 内部完成 `Input.set_custom_mouse_cursor`(`Engine.is_editor_hint()` 下跳过),热点在图标高度 12% 处 —— **系统指针逻辑已从 player.gd 移入准心**。
- `player_pickup.gd`:信号由 `hover_changed(bool)` 改为 `interact_state_changed(hovering, holding)`,`_update_hover()` 改名 `_update_interact_state()` 并同时跟踪 `_holding`。
- `player.gd`:handler 改名 `_on_pickup_interact_state_changed(false)`,只负责把两个布尔量收敛成 `Crosshair.GrabState`(优先级 抓着 > 指着 > 没有)。
- 待用户确认:像素手在非整数倍分辨率缩放下发糊的话,需把 Crosshair 的 `texture_filter` 设为 Nearest。
### 拖拽时物体一卡一卡(已修)
- **根因(几何 + 控制律)**:`physics_step()` 用 `v = to_hold * pick_power` 是一阶跟随,人匀速前进时物体必有稳态滞后,滞后距离 = 移动速度 / pick_power。当时为 `walk_speed 5 ÷ pick_power 10 = 0.5 m`,而持握点离身体仅 `0.44 m`(InteractionPos local z = -0.4439,Player 缩放 0.2 × Camera 缩放 5 = 净 1.0,故世界距离就是 0.444 m)→ 物体退进玩家胶囊体 → 物理推出 / 拖拽拉回互相打架 → 高频抖动。
- **修复 1(前馈)**:`desired = to_hold * pick_power + _body.velocity` —— 匀速行走时滞后归零。这是根治。
- **修复 2(碰撞例外)**:`_grab()` 里 `_current.add_collision_exception_with(_body)`,`_drop()` / `_throw()` 经 `_forget()` 用 `remove_collision_exception_with` 解除。只解除「物体 ↔ 玩家」这一对,物体与场景其他碰撞体照常交互。
- 另加 `@export var max_drag_speed: float = 30.0` 速度上限,物体离得远时不会被一股脑拽飞。
- **接口变更**:`PlayerPickup.setup(ray, hold_pos, body)` 多了第三个参数(玩家本体),`player.gd` 里传 `self`。`_grab()` 加了 `is_instance_valid(_current)` 早退,避免重复抓取时漏掉碰撞例外。
### 玩家缩放层级(查明,供后续改尺寸参考)
- `character/player/player.tscn` 的缩放链:`Player ×0.2` → 子节点 Collision(胶囊 r0.545/h1.406,本地 y0.703) / CameraRig(眼高 y1.6) / Body/mouse(×0.1824) 累计 ×0.2。
- `Camera ×5` **是补偿值**,用来把相机子树抵消回世界缩放 1.0(0.2×5=1.0),因此 InteractionRay 世界长度 0.5 m、InteractionPos 世界距离 0.444 m。
- 推论:**改 `Player.scale` 必须同步 `Camera.scale = 1 / Player.scale`**,否则交互距离/持握距离会跟着缩。另外 PlayerMotor 的 walk/sprint/jump/accl 是世界单位,不会自动缩放,改尺寸时需手动同倍调整。
- 世界尺寸现值:碰撞胶囊半径 0.109 / 高 0.281,眼高 0.32 m。
### 整体人物缩到 0.6 倍(已实施)
- `player.tscn`:`Player` 缩放 0.2 → **0.12**;`Camera` 缩放 5 → **8.333333**(= 1/0.12,保持相机子树净缩放 1.0,**交互距离/持握距离不变**)。Camera 的位置偏移与 CameraRig 的 y=1.6 都自动随父级缩放,未改。
- `player_motor.gd`:walk 5.0→**3.0**、sprint 8.5→**5.1**、acceleration 45→**27**、air_acceleration 12→**7.2**,均为 ×0.6;jump_velocity 5.0→**3.87**(×√0.6,因跳跃高度 = v²/2g,直接乘 0.6 会跳得偏矮)。
- `player_animator.gd`:**基准速度也必须 ×0.6** —— walk 5.0→3.0、run 8.5→5.1、crouch 2.5→1.5、sneak 1.6→0.96。原因:模型缩到 0.6 倍后,同一片段一个循环跨出的世界距离也缩到 0.6 倍,不改基准速度脚步会明显打滑。同时修正了 `_resolve_state()` 里那句「jump 片段 0.5s ≈ 5/9.8 上升时间」的过时注释。
- 未改动:`pick_power` 10(新滞后 3.0/10 = 0.3 m < 0.444 m 持握距离,且已有速度前馈,安全)、`throw_speed` 12(被扔的物体本身没缩,投掷距离不该变)、`mouse_sensitivity`(纯角度)。
- 缩放后世界尺寸:胶囊半径 0.0654 / 高 0.169,眼高 0.192 m;跳跃绝对高度 1.276 → 0.766 m(相对身高观感不变)。
### 再缩到「视觉高度 0.15 m」(已实施)
- **口径确认(重要)**:用户选的是**视觉模型高度**,不是碰撞胶囊。两者不等:
- 模型:glb 顶点原始高 `8.557729`(min y -4.7746 / max y 3.7832)× mouse 节点缩放 `0.18240134` = 本地 1.560941;世界 = ×0.12 = **0.187313 m**(当时)
- 胶囊:本地 1.4063476 × 0.12 = **0.1687617 m**
- 模型底部在本地 y ≈ -0.0007(脚踩原点,与胶囊底部对齐),故测量可信。模型比胶囊高 11%(多出耳朵/尾巴)。
- 缩放系数 `k = 0.15 / 0.187313 = 0.800799`。
- `player.tscn`:`Player.scale` 0.12 → **0.096096**;`Camera.scale` 8.333333 → **10.406274**(= 1/0.096096)。
- `player_motor.gd`:walk 3.0→**2.4024**、sprint 5.1→**4.0841**、accel 27→**21.6216**、air_accel 7.2→**5.7658**、jump 3.87→**3.4632**(×√k)。
- `player_animator.gd`:walk_ref 3.0→**2.4024**、run_ref 5.1→**4.0841**、crouch_ref 1.5→**1.2012**、sneak_ref 0.96→**0.7688**。
- 注释已改成「按同一系数同步缩放」的通用表述,不再写死 0.6,避免下次再失效。
- 结果:胶囊世界 半径 0.05241 / 高 0.13514,眼高 0.08729 m,跳跃绝对高度 ≈ 0.612 m;拖拽滞后 = 2.4024/10 = 0.24 m < 0.444 m 持握距离,安全。
- **可复用公式**:`Player.scale = 目标世界高 / (模型本地总高)`,`Camera.scale = 1 / Player.scale`,速度与动画基准速度均 ×k(jump 用 ×√k)。
### 按 0.15 m 身高重新设计移动参数(已实施)
- 用户明确:「不用管之前,按 0.15 的身高重新设计」→ 采取**以人类为参照的等比设计**,不再依赖旧基准。
- 设计基准:人 1.75 m,游戏惯例 walk 5.0 / sprint 8.5 / accel 45 / air_accel 12 / jump 5.0(跳跃高度 1.28 m ≈ 0.73 身高)。系数 `k = 0.15 / 1.75 ≈ 0.0857143`。
- `player_motor.gd`:walk 2.4024→**0.4286**、sprint 4.0841→**0.7286**、accel 21.6216→**3.8571**、air_accel 5.7658→**1.0286**、jump 3.4632→**1.4639**(×√k)。
- `player_animator.gd`:walk_ref 2.4024→**0.4286**、run_ref 4.0841→**0.7286**、crouch_ref 1.2012→**0.2143**、sneak_ref 0.7688→**0.1371**。
- 校验:两边都是「走路 = 2.86 身长/秒」「跳跃 = 0.73 身高」,比例一致。加速度给出约 0.11 秒加到全速;跳跃高度 0.109 m。
- 拖拽滞后 = 0.4286/10 = 0.043 m ≪ 0.444 m 持握距离,安全。
- **未改动**(有意保持):交互距离/持握距离(射线 0.5 m、持握 0.444 m —— 若按 k 缩到 0.038 m 会够不到任何东西)、`pick_power` 10、`throw_speed` 12(被扔物体是世界尺寸,未缩)、`mouse_sensitivity`(纯角度)。
- **世界尺度查不到可靠线索**:`world/main2.tscn` 有 295 个 `Transform3D`,但混入了嵌套实例的局部变换,位置跨度(X33/Z57)不可信;198 个实例缩放为 1.0,资产疑似米制。若实际关卡跨度大、用户觉得慢,把 walk/sprint 等线性量乘同一倍数即可。
### 蹲伏真正生效(已实施)
- 背景:**蹲下改速度、改摄像机高度这两件事从来没实现过**(原 `player.gd` 也只把 `crouching` 传给动画器切 CROUCH/SNEAK 片段),不是回归。
- `player_motor.gd`:新增 `@export var crouch_speed: float = 0.2143`(= walk 的一半 = 2.5×k)。`step()` 里改为**蹲伏优先于疾跑**:`sprinting = wants_sprint and not crouching`,速度按 `crouch_speed > sprint_speed` 优先级取。
- `player_look.gd`:新增蹲伏组 `crouch_height_ratio`(=0.6) / `crouch_transition_speed`(=10);`setup()` 从场景读 `_stand_height = rig.position.y`(不写死 1.6);新增 `set_crouching(bool)` 设目标高度;`_process()` 用 `1 - exp(-k·delta)` 指数收敛平滑升降(**渲染帧**,物理帧会一顿一顿);`_ready()` 里 `set_process(false)`,`setup()` 才开 —— 远端玩家不调 setup 就不会跑。
- `player.gd`:`_physics_process` 里 `motor.step(delta)` 之后加 `look.set_crouching(motor.crouching)` —— **蹲伏状态由 Motor 单点判定**,不让 Look 自己读蹲键,避免两处各判一次不同步。
- `player_animator.gd`:`sneak_ref_speed` 0.1371 → **0.2143**,与 `crouch_speed` 对齐 —— 蹲着走播的就是 SNEAK 片段,两边不等会打滑(原来会以 1.56× 播放,逼近 1.6 的 clamp 上限)。注释里写明了这条配对关系。
- 世界高度:站立 `1.6 × 0.096096 = 0.1538 m` → 蹲下 `0.0923 m`。
- **未做(可以后续补)**:蹲下时没有缩小碰撞胶囊 —— 所以现在还钻不过低矮缝隙。要做需要额外处理「头顶有障碍时不许起身」的检测。
### 关于「重力是否该随角色缩小」(已分析,待用户定夺)
- **物理结论**:不该变。重力加速度是行星属性,与物体大小无关。
- **但设计上是真问题**:我们的速度按 k=0.0857 等比缩了,重力却没缩,导致**落体相对过快**。
- 落「自身身高」用时:人类 (1.75 m, g=9.8) = 0.598 s;本角色 (0.15 m, g=9.8) = 0.175 s,**快 3.4 倍**。
- 若 `g = 9.8 × k = 0.84`:本角色也是 0.598 s,与人类完全一致(缩小版人类)。
- **顺带暴露的问题**:`jump` 片段长 0.5 s。现在 g=9.8、jump_velocity=1.4639 → 上升仅 **0.149 s**,只播得完片段的 30%。若 `g=0.84` 且 `jump_velocity = walk × 1.0 = 0.4286`,上升 = **0.51 s**,正好等于片段长度 —— 回到原始设计(原来 jump 5.0 / g 9.8 = 0.51 s)里那种吻合。
- **实现方式(已落地为旋钮)**:`PlayerMotor` 新增 `@export_range(0.0, 2.0, 0.01) var gravity_scale: float = 1.0`,`step()` 里 `_body.velocity.y -= _gravity * gravity_scale * delta`(**每帧现读**,方便运行中拖 Inspector 试手感)。默认 1.0 = 行为完全不变。**绝不要改 `ProjectSettings physics/3d/default_gravity`** —— 那是全局的,会让场景里所有 RigidBody 一起变飘,包括拖拽/投掷的物体。
- 三个候选:`9.8`(物理真实,落体快、跳跃动画对不上)/ `2.87`(×√k,折中)/ `0.84`(×k,缩小版人类,跳跃动画吻合,但比月球 1.62 还飘)。
- **⚠ 配对规律**:重力 × s 时 `jump_velocity` 也要 × s,否则跳跃高度按 1/s 暴涨。现在是 `g=9.8 + v=1.4639 → 高 0.109 m`;若 `g=0.84` 而 v 不变,跳跃高会变成 **1.28 m(8.5 个身高)**;正确配对是 `g=0.84 + v=0.4286`。这条已写进 `gravity_scale` 的代码注释。
- 用户尚未选定最终值。
### 跳跃「高度过低、落地太快」的调法(已给方案,待用户选)
- 问题:现状 g=9.8 + jump_velocity=1.4639 → 跳高 **0.109 m**(0.73 身高)、**滞空仅 0.30 s**,跳一下几乎立刻落地。
- **核心关系**:`h = v²/2g`、`T = 2v/g` → 合起来 `h = g·T²/8`。
→ **固定重力下,滞空翻倍 = 高度涨 4 倍**。所以「既要跳得久、又不跳太高」只能靠**降重力**,光提 `jump_velocity` 做不到。
- **反解公式**(定目标高度 h 与滞空 T):`g = 8h/T²`(→ gravity_scale = g/9.8),`jump_velocity = 4h/T`。
- 三个候选(角色 0.15 m,1 身高 = 0.15 m):
| 方案 | gravity_scale | jump_velocity | 跳高 | 身高倍数 | 滞空 | 落地时长 |
|---|---|---|---|---|---|---|
| 现状 | 1.0 | 1.4639 | 0.109 m | 0.73 | 0.30 s | — |
| A 只提跳跃 | 1.0 | 2.10 | 0.225 m | 1.50 | 0.43 s | 不变 |
| B 只降重力 | 0.5 | 1.4639 | 0.219 m | 1.46 | 0.60 s | ×1.41 |
| **C 组合(推荐)** | 0.5 | 1.50 | 0.230 m | 1.53 | 0.61 s | ×1.41 |
→ A 和 B 高度几乎一样,但 B 的滞空长 40% —— 这正是「降重力」的价值。
- 副作用:g=4.9 时从 1 m 高落下要 0.639 s(原 0.452 s)。仍远好于等比 g=0.84(比月球还飘)。
- jump 动画片段 0.5 s:现状只播得完 30%,C 方案升到 61%。要让上升时间完全等于 0.5 s 需 T=1.0 s → g 低到 1.84(接近月球),不建议。
- **可选改进(未实施)**:把 `jump_velocity` 换成「跳跃高度 `jump_height`(米)」—— 设计上更直观,且改重力时高度自动保持,不会出现「改重力后跳高暴涨」的坑。
### 跳跃参数定案:方案 C(已实施)
- 用户问「推荐哪个方案」→ 定 **C**:`gravity_scale = 0.5`(有效 g = 4.9)、`jump_velocity = 1.5`。
- 结果:跳高 **0.230 m(1.53 身高)**、滞空 **0.612 s**、上升 0.306 s;从 1 m 高落下 0.639 s(原 0.452 s,×1.41)。
- 选 C 不选 A(只提 jump_velocity、保持 g=9.8)的理由:同样 0.23 m 的高度,A 只有 0.43 s 滞空,落地仍嫌仓促;C 多出 0.18 s。而且 C 让 jump 片段能播完约六成(A 只有四成)。
- 没选等比方案(`gravity_scale = 0.0857`):那比月球(1.62)还飘。
- 代码注释已整理:`jump_velocity` 与 `gravity_scale` 标注为**配对参数**,并写入反解公式 `g = 8h/T²`、`v = 4h/T`;`player_animator.gd` 里那句引用旧值的上升时间注释也改准了(1.5 / 4.9 ≈ 0.31 s)。
### 贴墙时摄像机穿模(已修)
- **根因(纯几何,差 8 毫米)**:
| 项 | 值 | 来源 |
|---|---|---|
| 摄像机 `near` | **0.05 m** | Godot 默认,`player.tscn` 未覆盖 |
| 摄像机前移量 | **0.01074 m** | `Camera` 本地 Z = −0.1118 × Player 缩放 0.096096 |
| 贴墙时身体离墙 | **0.05241 m** | 胶囊局部半径 0.5454 × 0.096096 |
| **摄像机到墙** | **0.04167 m** | 0.05241 − 0.01074 |
→ `0.0417 < 0.05`,墙面落在近裁剪面**内部**被整个裁掉,于是看到墙的背面/内部。
- **修复 1**:`player.tscn` 的 `Camera` 加 `near = 0.01`(留 4 倍余量)。项目是 Forward+(反向 Z 深度),near 缩小对深度精度影响很小。`far` 保持默认未动。
- **修复 2**:`player.gd` 新增 `@onready var model: Node3D = get_node_or_null("Body/mouse") as Node3D`,在权威分支里 `model.visible = false` —— 原来正是靠 near=0.05 把自己的身体裁掉才看不见,near 调小后身体会露出来。只影响本地玩家,远端玩家各自的模型实例不受影响。
- 代价:自己也看不到自己的影子(`visible=false` 会关掉阴影)。想保留影子可改用「本地玩家的 MeshInstance3D 放到独立视觉层 + 本地摄像机 cull_mask 排除该层」,但要注意该层不能和远端玩家共用,否则本地相机也看不到别人。
- **备选方案(未采用)**:把胶囊半径加大到 > 0.06 m 让摄像机永远离墙更远 —— 会改变玩法(体型变胖);或把 `Camera` 的前移量改小 —— 会动视角位置。
### 附带发现的 bug:复制配置里的节点路径过期(已修)
- 用户此前把 `AnimationPlayer` 从 `Body/AnimationPlayer` 移到 Player 根节点(`$AnimationPlayer`),但 `player_sync.tres` 里两条路径仍写着 `Body/AnimationPlayer:anim_state` / `Body/AnimationPlayer:anim_speed` → 这两个属性根本没被复制,**远端玩家的动画状态不会同步**。
- 已改为 `AnimationPlayer:anim_state` / `AnimationPlayer:anim_speed`。
## ⚠ 项目约定(用户明确要求)
**改 Godot 的 .tscn / .tres / .gd,一律走 godot-ai MCP,不要直接写文件。** 原因见下:外部直接写 .tscn 会和编辑器的文件监视重载(`_reload_modified_scenes`)竞态,导致编辑器读到中间状态、报出**假的解析错误**。
- 可用工具:`scene_open` / `scene_save` / `node_set_property` / `node_get_properties` / `node_create` / `node_manage` / `script_manage(find_symbols 可当解析校验)` / `script_create`(带诊断) / `filesystem_manage(read_text/write_text/scan/reimport)` / `resource_manage` / `logs_read` / `editor_manage(state)` / `project_run`。
- 调用形式:多数工具是「扁平参数」(如 `scene_open {path}`),管理类工具是 `{op, params}` 双层(如 `editor_manage {op:"state"}`)。`node_*` 的 path 是**相对当前编辑场景根**(如 `/Player/CameraRig/Camera`),不是 `/root/...`。
## player.tscn 解析错误事件(已解决)
- 现象:`Parse Error ... player.tscn:38` in `_parse_node_tag`,场景彻底打不开。
- **真实原因:不是文件语法错误。** 我用 MCP `node_set_property` 设 `Camera.near = 0.01` 后 `scene_save`,编辑器写出的内容与我手写的**字节完全一致**(同为 5106 字节),且 `scene_open(force_reload=true)` 立即 `reloaded_from_disk: true` 加载成功。
→ 根因是**外部直接写 .tscn 与编辑器文件监视及重载的竞态**。
- 诊断过程中的有效手段(可复用):
1. `editor_manage(op="state")` 看当前场景与就绪状态
2. `scene_open` 做**对照实验**(打开一个已知正常场景,若 `switched:true` 则说明工具可靠、目标文件真的坏了)
3. `logs_read(source="editor", include_details=true)` 拿准确报错函数与行号
4. 用 `node_create` 在**内存里**加临时节点 + `node_get_properties` 探测属性真名(**不保存、不落盘**,随后 `node_manage(op="delete")` 清掉)—— 这次用它确认了 `Camera3D.near` 是合法 float 属性(默认 0.05,`far` 默认 4000)
5. `script_manage(op="find_symbols")` 可当脚本**解析校验**器;`filesystem_manage(op="scan")` 重建 `class_name` 全局表
- 修复结果:`player.tscn` 的 `Camera` 现有 `near = 0.01`(MCP 设置并保存),场景可正常加载。
### 日志里发现的其它线索(多为历史记录,非当前问题)
- `Could not find type "PlayerLook/PlayerMotor/PlayerPickup/Crosshair"`、`Identifier "animator" not declared` —— 都是**旧状态的历史日志**;`find_symbols` 证明 player.gd 现已正常,且 `filesystem_manage(scan)` 返回 `global_classes_registered_delta: 0`(类名表本是全的,共 75 个)。
- `crosshair.gd:133 _draw 里 texture_for_state 收到 Nil` —— `@tool` 脚本热重载时旧实例被新代码调用的一次性产物,非当前 bug。
- `player.tscn` 的 **Crosshair 纹理疑似接反**:`hover_texture = grab_hover.png`、`hold_texture = grab.png`。按我的语义(hover=指着可拖拽物 / hold=正抓着)应互换;但若按光标包命名(`_hover` 后缀表示悬停态)则现状正确。**未擅自改动,已向用户指出。**
- 遗留警告(非本次引入):`face_painter.gd:153` 整数除法、`face_rig.gd:190` 局部变量 `name` 遮蔽 Node.name、`paint_panel.gd:69` 局部变量 `scale` 遮蔽 Control.scale、`Cannot load shader: res://shader/stylized.gdshader`。
+220
View File
@@ -0,0 +1,220 @@
# 2026-10-10
## 新增右上角调试面板(DebugPanel)
**需求**:右上角显示玩家移动速度、相机高度等信息。
**新增文件**
- `ui/debug_panel.gd`(`class_name DebugPanel extends Control`)—— 单节点 `_draw()` 实现,
不用 Label 节点。理由:要自己控制两列对齐(标签左对齐、数值左对齐、列间距固定),
不必去猜字体度量。总刷屏用 `draw_rect` 画底板。
**显示内容**(10 Hz 刷新,`refresh_interval` 可调)
| 行 | 内容 |
|---|---|
| FPS | `Engine.get_frames_per_second()` |
| 移动速度 | 水平速度 m/s + 身长/秒 |
| 垂直速度 | `velocity.y` |
| 相机高度 | 相机相对脚底的世界高度 m + 倍身高 |
| 状态 | 站立/蹲下/疾跑 · 地面/空中 |
| 位置 | 世界 X, Z |
| 手持 | 拖拽中 / 可抓取 / — |
**关键设计点**
- 相机高度用 `_camera.global_position.y - _player.global_position.y`(世界 y 相减),
**不是**读 `CameraRig.position.y` —— 后者是本地坐标还要乘玩家缩放,且相减法对
「蹲伏过渡中途」这种非稳态也直接成立。
- `body_height`(默认 0.15)只用于换算「身长/秒」和「倍身高」,**和 `player.tscn` 的
`Player.scale` 绑定,改角色尺寸时要同步改**。
- `mouse_filter = IGNORE` 在 `_ready()` 里显式设置(见下方 MCP 限制),
否则这个全屏 Control 会吃掉所有鼠标事件,鼠标视角和拖拽全会废掉。
- `visible` 默认 false,`setup()` 里才置 true —— 避免远端玩家的「回放身体」
闪出一块「未接线」面板。
- F3 切换显隐(`toggle_key`)。`Key` 类型可以直接 `@export`。
- 挂在 `CameraRig/HUD` 下(和准心同级)→ 远端玩家的 HUD 随 `camera_rig.queue_free()` 一起释放,
天然只有本地玩家显示。
**改动文件**
- `character/player/player.tscn`:`HUD` 下新增 `DebugPanel (Control)`,
锚点 Full Rect(`anchor_right`/`anchor_bottom` = 1.0),挂 `ui/debug_panel.gd`。
- `character/player/player.gd`:加 `@onready var debug_panel: DebugPanel = $CameraRig/HUD/DebugPanel`
和 `_ready()` 里 `debug_panel.setup(self, motor, pickup, camera)`。
- `character/player/player_pickup.gd`:新增公开 getter `is_hovering()`(配 `has_object()`)。
---
## ⚠ 本次踩到的 Godot MCP 限制(重要,可复用)
### 1. 编辑器在 play 模式时 MCP 拒绝一切写入
`EDITOR_NOT_READY / EDITOR_PLAYING`。必须先 `project_manage(op="stop")`。
(本次用户选择「我来停」,停完 `editor_manage(op="state")` 确认 `readiness: ready`。)
### 2. MCP 写不了整型属性
`node_set_property` 传整数会变成 String → `WRONG_TYPE: Cannot write String to a int property`。
**浮点/字符串正常**。受影响的:`mouse_filter`、`grow_horizontal`、`anchors_preset`。
→ 绕过办法:
- 锚点:改设 `anchor_right` / `anchor_bottom` = **1.0**(浮点可写);编辑器保存时会自动补
`anchors_preset = 15` + `layout_mode = 3`,效果与准心节点完全一致。
- 整型语义(如 `mouse_filter`):**写进脚本里**(`_ready()` 里设),别依赖场景属性。
### 3. 假 parse error(本项目的顽固问题,第二次遇到)
`script_create` / `script_patch` / `filesystem_manage(write_text)` 的 `diagnostics` 会对
**完全合法的文件**报 `gdscript_reload_failed, error_code 43 (ERR_PARSE_ERROR)`,
`line` 是 `fallback_line: true`(≈ 文件总行数,不可信)。
本次 `ui/debug_panel.gd`、`player_pickup.gd` 都被这样误报过,而真实项目上
`--check-only` 全部通过。**判定:这是编辑器热重载产物,不是语法错误。**
教训:**MCP 的 diagnostics 只能当提示,不能用它判定脚本坏了。**
### 4. 可靠的脚本语法校验方法(离线,不污染项目)
Godot 可执行文件路径:
`D:\_yvmou\Godot\Godot_v4.7.2-stable_mono_win64\Godot_v4.7.2-stable_mono_win64.exe`
(从 `Get-Process | Where ProcessName -like "*godot*" | Select Path` 拿到)
直接对真实项目跑(安全,`--check-only` 解析完即退):
```bash
"$GODOT" --headless --path "E:/Godot/agame" --check-only --script "res://ui/debug_panel.gd"
```
**输出为空 = 通过**。
沙箱法(想彻底不碰项目时):临时目录建最小 `project.godot`,拷入脚本及其依赖,
先 `--editor --quit` 生成 `.godot/global_script_class_cache.cfg`(否则会误报
「Could not find type」——注意**类型解析失败也是报 Parse Error**),再 `--check-only`。
### 5. 字体(中文能否渲染)
实测:`ThemeDB.fallback_font` **就是**默认主题字体(`FontFile` / "Open Sans SemiBold"),
`allow_system_fallback = true`,系统有 Microsoft YaHei/SimSun。
注意 `has_char("点")` 返回 **false**(`has_char` 不查系统回退链),但排版时回退生效:
「移动速度相机高度」8 字量出 128px = 16px/字,字形确实取到了。
→ **结论:`_draw()` + `ThemeDB.fallback_font` 画中文是可行的,不必额外挂字体资源。**
### 6. 其他
- `custom_manage(op="list")` 可列出第三方 addon 注册的自定义工具。
- `editor_manage(op="state")` 能看 play 状态;`logs_read(source="editor")` 只看得到
脚本 reload 的 warning,拿不到 GDScript 的详细 parse 报错文本(stderr 到编辑器控制台)。
- 用户 `world/main2.tscn` 是编辑器常驻场景,用完要**还原回去**(本次已还原)。
---
## `player_sync.tres` 为什么在 Godot 里「不可编辑」
- `SceneReplicationConfig` **没有任何 `@export` 属性**,类上只有 `add_property` / `remove_property` /
`property_set_*` 这些**方法** → 通用 Inspector 渲染出来是空的,所以双击 `.tres` 看起来「不能编辑」。
- 这不是错觉:Godot 4.7 二进制里有 `ReplicationEditor` / `Replication Dock` / `Remove Property` /
`Replication Mode`(下拉枚举 `Never,Always,On Change`)/ `ReplicationInterface`,
但**没有** `SceneReplicationConfigEditor` —— 即不存在独立的资源编辑器。
- **正确改法**:选中场景里的 `Sync` 节点(`MultiplayerSynchronizer`)→ Inspector 的 Replication 区
→ 添加/删除属性、勾 Spawn、选 Replication Mode → 保存场景(外部 .tres 会被一并写盘)。
- 运行时可在调试器里看同步情况(`ReplicationInterface` / `ReplicationProfiler`)。
### 顺带发现:`.:health` 是失效路径
`character/player/player.gd` 上**没有** `health`(`health` 只存在于 `demo/` 那套
`agent_base.gd` + `Health` 节点)。同类「路径过期」问题已第三次出现
(前两次是 `Body/AnimationPlayer:anim_state` / `anim_speed`)。
当前 5 条里 4 条有效:`.:position`、`Body:rotation`、`AnimationPlayer:anim_state`、`AnimationPlayer:anim_speed`。
原 5 条全是 `replication_mode = 1`(Always,每处理帧持续发)。`anim_state` / `anim_speed`
是低频变化量,改用 `On Change`(2,可靠传输、变化才发)更省带宽。
### 已处理(用户确认后动手)
`player_sync.tres` 现为 4 条:
| # | 路径 | spawn | mode |
|---|---|---|---|
| 0 | `.:position` | true | 1 Always |
| 1 | `Body:rotation` | true | 1 Always |
| 2 | `AnimationPlayer:anim_state` | true | 2 On Change |
| 3 | `AnimationPlayer:anim_speed` | true | 2 On Change |
- **改法**:MCP `filesystem_manage(write_text)` 直接写 .tres 文本 → `scan()` → `reimport()`。
这个资源**没有编辑器 UI,也不存在「编辑器打开着」的竞态**,所以写文本是安全的
(与 .tscn 的情况不同)。`reimport` 对 .tres 只会刷新文件条目(`skipped_non_imported`)。
- **验证手段(值得复用)**:把手写的 `.tres` 拷进最小沙箱工程,用
`--headless --path <沙箱> --script res://check.gd` 跑一段 `extends SceneTree` 的脚本,
`load()` 它并打印 `get_properties()` / `property_get_spawn()` /
`property_get_replication_mode()` —— 这是**唯一能验证手写 .tres 真能解析**的办法。
实测输出:4 条,mode = 1,1,2,2 ✓
- `resource_manage(op="load")` 对 `SceneReplicationConfig` 只返回 3 个属性
(`resource_local_to_scene` / `resource_path` / `resource_name`),
`op="inspect"` 直接拒绝(不在它支持的原生资源族里)。
- `Always`(1) 用不可靠传输、每帧持续发;`On Change`(2) 用可靠传输、值变了才发。
连续变化的量(position / rotation)用 `Always` 是对的,低频量才换 `On Change`。
### 决定:保持外部 .tres,不内嵌(用户明确选择「不改」)
调研过三条替代路,全部放弃,**以后不必再提**:
- **A 内嵌 `sub_resource`**:功能零损失、可少一个文件,但给配置加条目要调 `add_property()`
—— 那是**方法不是属性**,MCP 只能读写属性(`custom_manage` 也返回 0 个自定义工具),
最后 4 行只能用户在 Replication 面板手点。不值得为省一个文件付这个手工成本。
- **B 脚本里构造**(`MultiplayerSynchronizer` 上挂脚本,`_ready()` 里 new 一个 config 再赋给
`replication_config`):会让编辑器 Replication 面板与实际同步内容**脱节**,
且运行时赋值能否赶在同步器开始工作前生效**未实测**。放弃。
- **C 弃用 Synchronizer 改 `@rpc`**:`world/main.tscn` 用 `MultiplayerSpawner` + `main.gd` 的
`spawn_function` 生成玩家,「新 peer 拿到当前动画状态」是 `spawn = true` 白送的;
换 RPC 要自己补初始状态 / 节流 / 丢包策略。为 4 个值不划算。
- 根本原因:`.tres` 的"缺点"只是不能双击打开,而**编辑入口一直是 Sync 节点的 Replication
面板**(换成 A 也是同一个面板)。
### `/Player/Sync` 节点实测值
`root_path = ".."`(默认值 = 父节点 Player,这就是 `.:position` 指 Player 的原因)、
`delta_interval = 0.0`、`public_visibility = true`、`visibility_update_mode = 0`。
`replication_config` **不在** `node_get_properties` 可读属性里(被 ReplicationEditor 面板接管)。
### 补充:外部 .tres 改动的落盘机制(用户追问「tres 不是不可编辑吗」)
- 准确说法:`player_sync.tres` **没有自己的编辑界面**,但**内容可以改**,入口是 Sync 节点的
Replication 面板。这是两件事,别混。
- 根因链:`SceneReplicationConfig` 没有任何 `@export` 属性(只有方法)→ 通用 Inspector 渲染不出
可编辑项 → Godot 只好把专用 UI(`ReplicationEditor`)挂在**使用它的节点**
(`MultiplayerSynchronizer`)上。实测证据:`resource_manage(op="load")` 只回 3 个元数据属性
(`resource_local_to_scene` / `resource_path` / `resource_name`)—— 这就是你双击它看到的全部内容。
- **编辑器加载的那份 .tres 实例就是项目文件本身**,所以面板里改它会**写回 `.tres`**,重启后仍在
(与 `duplicate()` 出来的副本不同 —— 这既是它"生效"的原因,也是 Godot 著名的「资源被意外共享
修改」陷阱的同一机制)。改完记得保存场景。
- 真正做不到的只有一件事:**手改文本时没有校验**。NodePath 写错编辑器不会拦 ——
`.:health`、`Body/AnimationPlayer:anim_state` 两次事故都源于此。要改路径请走面板下拉选,
别手打。
### 项目事实:资源类的三种来源(排查中确认)
- `demo/ai/trees/*.tres` 的类型是 `BehaviorTree` / `BlackboardPlan`,**但这两个类不在任何 .gd 里** ——
`addons/limboai/` 只有 `bin/*.so`(GDExtension,C++ 编译产物),一个 `.gd` 都没有。
所以它们是**插件用 C++ 注册的 Resource 类**,用起来和引擎内置类没区别(LimboAI 的 C++ 版)。
- 项目里目前**没有任何自己写的 `extends Resource` 脚本**(排除 addons)。
已存在的 `.tres` 类型:`StandardMaterial3D`(素材包材质)、`SceneReplicationConfig`(player_sync)、
`BehaviorTree`(limboai)。
- 没有二进制 `.res`。
- 判据:**`@export` 就是「能不能在 Inspector 里编辑」的分水岭**。`SceneReplicationConfig` 一个
`@export` 都没有 → 打开是空的;自定义 Resource 有 → 双击就能编辑。
### 调试器面板截图排查(15:58 用户贴图)
**三条警告**都是历史遗留、与近期改动无关,来自项目加载时的 `GDScript::reload` 全量重载:
- `face_painter.gd:153` 整数除法 —— **不是 bug**。`_stamp_d` 恒为偶数(第 138 行 `d = r * 2`),
整除不丢精度。要消警告用 `@warning_ignore("integer_division")` + 注释说明,**不要**改成浮点。
- `face_rig.gd:190` 局部变量 `name` 遮蔽 `Node.name` → 改名(如 `entry_name`)即可。
- `paint_panel.gd:69` 局部变量 `scale` 遮蔽 `Control.scale` → 改名(如 `factor`)即可。
**那条错误 `net.gd:66 host_game(): Couldn't create an ENet host.` 也不是 bug**:
日志证据显示当时**两个实例同时在跑** —— 主机(peer 1)已占住 7000 端口,第二个实例点「创建主机」
必然失败(这是预期路径),随后它点「加入」**成功连上**(日志里有完整握手 + 双方各自生成角色)。
功能正常,唯一可改进的是错误消息不够人话。
排查要点:**`user://logs/godot.log` 是「按项目」共享的,多实例会并发写同一个文件**,
所以日志行会交错(截图里 `enet_connection.cp` 被从中间截断就是并发写的痕迹)。
判据:`[Net] 本端 1 …` 和 `[Net] 本端 1484785418 …` 同时出现 = 两个进程。
### ⚠ 两条重要的方法论修正(都靠实测证伪)
**1. 之前说的「假 parse error」成因找到了 —— 不是假错,是热重载场景内的脚本。**
`script_patch`/`script_create` 返回 `gdscript_reload_failed (error_code 43)` 的文件,
全部都是**挂在「当前编辑器打开的那个场景」里的节点上的脚本**:
`debug_panel.gd`(HUD/DebugPanel)、`player_pickup.gd`(/Player/Pickup)、
`face_painter.gd`、`face_rig.gd`(FaceRig) —— 当时开着的正是 `player.tscn`。
而 `paint_panel.gd`(在 paint_panel.tscn 里)、`net.gd`(autoload,net.tscn 没开)
**都返回 `diagnostics: []` 干净结果**。
结论:场景里有活实例时热重载会被判定失败,**文件本身没问题**。
想拿干净诊断:先把编辑器切到一个**不含该脚本实例**的场景,或直接用下面的离线校验。
**2. `--check-only` 只报错、不报警告!**
实测探针(`var name := "hello"` + `5 / 2`,两条确定会触发的警告)→ `--check-only` **零输出**。
所以「无输出」只能证明**没有语法/分析错误**,**不能**作为「警告已消除」的证据。
要确认警告消掉了,必须看编辑器的 Errors 面板(重启编辑器或重新加载项目)。
本次三条警告的处理依据是「遮蔽变量已改名 + 所有引用点都改到」(`face_rig.gd` 的 `name`
在 190/191/192/202/208 共 5 处,**漏改后两处会静默解析成 `Node.name`、不报错但值全错**,
已全部改名 `entry_name`)。
+55
View File
@@ -0,0 +1,55 @@
# 2026-10-12
## 可拖拽判断(PickupItem 标记)
**需求**:`scripts/item.gd` 提供 `can_pickup`,只有为 true 时才能拖拽。
**item.gd 本次改动**:加了 `class_name PickupItem` 和说明注释,功能不变。
```gdscript
class_name PickupItem
extends Node
@export var can_pickup: bool = true
```
### ⚠ 关键发现:脚本挂载位置 ≠ 射线打中的节点
用户在 `world/main.tscn` 第 155 行把 item.gd 挂在了 `SM_Bottle_12` 上,但那个节点**不是刚体**。
实例来源 `res://assets/Furniture_FREE-af040129/SM_Bottle_1.glb`,实际结构是:
```
SM_Bottle_12 (Node3D) ← item.gd 挂在这一层(glb 实例的根)
└── SM_Bottle_1 (RigidBody3D) ← 射线实际打中的是这一层
├── SM_Bottle_1 (MeshInstance3D)
└── CollisionShape3D
```
**实测确认**:glb 内部节点的 `owner` 指向实例根(`SM_Bottle_1.owner == SM_Bottle_12`),
再往上为 null。所以「沿 owner 链向上找标记」正好一步命中。
### 实现(`player_pickup.gd`)
- 新增 `@export var allow_unmarked: bool = false`
- **false(默认)= 白名单**:只有挂了 `PickupItem` 且 `can_pickup=true` 的才拖得动
- true = 黑名单:没挂标记的默认可拖,只有显式设 false 的不能拖
- 新增 `can_grab(collider) -> bool`:先判 `is RigidBody3D`,再查标记
- 新增 `_find_item_marker(body)`:顺序 **刚体自身 → 直接子节点 → owner 链向上**
- 向上时**只查节点自身、不查子节点** —— 否则走到 World 层会遍历几百个子节点,而这是每物理帧都要跑的
- `_update_interact_state()` 的 hovering 判定改用 `can_grab()`
- `_grab()` 里再判一次(悬停状态来自上一物理帧,其间 can_pickup 可能已被改掉)
### 实测验证(headless,全部符合预期)
```
can_grab(SM_Bottle_1 RigidBody3D) = true ✓
can_grab(SM_Bottle_12 Node3D) = false ✓(不是刚体)
can_grab(Can2 内部刚体) = false ✓(严格/白名单模式)
allow_unmarked=true 后 Can2 = true ✓
```
### ⚠ 副作用(需告知用户)
默认白名单模式下,场景里**所有没挂 item.gd 的刚体都拖不动了**
(`Can2`、饭盒、水果等)。要么逐个挂标记,要么把 `allow_unmarked` 勾上。
### MCP 备注
4 次 `script_patch` 均返回 `replacements: 1` 但伴随 `gdscript_reload_failed (code 43)`
假报错(`fallback_line: true`)—— 与 10-10 记录的是同一现象。
离线 `--check-only` 两个脚本均通过;`PickupItem` 已进 `global_script_class_cache.cfg`。
+25
View File
@@ -0,0 +1,25 @@
# 2026-10-13 工作日志
## 修复:toon 材质所有模型联动、无法单独设基础纹理
**问题**:`shader/toon_apply.gd` 用 `material_override = toon.tres` 把同一个共享 `ShaderMaterial` 实例挂给所有 `MeshInstance3D`,导致
1. 改 `toon.tres` 影响全部模型;
2. `material_override` 整包覆盖,丢失模型自带贴图,无法给单个模型设 `base_texture`。
**方案**:重写 `shader/toon_apply.gd`
- 为每个 `MeshInstance3D` 的每个 surface **复制独立材质实例**(`cartoon_material.duplicate()`,含 `next_pass` 描边的复制),通过 `set_surface_override_material(i, mat)` 逐面挂载。
- 自动从 `mesh.surface_get_material(i)` 提取原始贴图(`BaseMaterial3D.albedo_texture` 或已有 ShaderMaterial 的 `base_texture`)写入各自的 `base_texture`。
- 新增导出项:`base_texture_override`(手动指定单模型贴图,优先级最高)、`use_original_texture`、`use_material_override`(兼容旧整包模式,默认关)。
- `toon.tres` 降级为"模板":脚本每次应用时拷贝其非贴图参数作为初值,之后各模型独立。
**关键文件**:`shader/toon_apply.gd`(uid 未变,场景引用 `assets/test/test.tscn` 仍有效)
**相关**:`shader/toon.tres`(基础 cel-shader + outline next_pass)、`shader/cel-shader-base.gdshader`、`shader/outline.gdshader`(outline 用 global uniform,全局限定)
## 修复报错:`get_shader_uniform_list` 不存在
- 报错:`Nonexistent function 'get_shader_uniform_list' in base 'ShaderMaterial'`。
- 原因:该方法属于 **`Shader`**,不属于 `ShaderMaterial`。
- 修复:改为 `cartoon_material.shader.get_shader_uniform_list()`(先判空)。
- 同时把 `get_surface_override_material(mi.material_override)` 返回的 `Material` 用显式 `is ShaderMaterial` 判断 + 声明式 `var x: ShaderMaterial` 赋值,避免隐式向下转换告警。
- 环境:Godot 4.7,Forward Plus,config_version=5。本机未找到 Godot CLI,无法自动语法校验。
+142
View File
@@ -0,0 +1,142 @@
# 2026-10-14
## shader/toon_apply.gd 自动刷新可行性咨询(未改动任何文件)
- 项目 Godot 4.7 Forward+。
- 现状:`toon_apply.gd` 是 @tool Node3D,`_apply_materials()` 对每个子 MeshInstance3D `duplicate()` 一份 ShaderMaterial 并 `material_override`。
- 结论:
1. 保存/编辑 `toon_apply.gd` 本身 → 编辑器热重载 @tool 脚本会重跑 `_ready()`,已能自动刷新(4.3 有热重载失效 bug,4.4+ 已修,本项目 4.7 无此问题)。
2. 编辑 `shader/toon.tres` 的 shader 参数(color / uv_scale 等)→ **不会**自动刷新。原因:ShaderMaterial 的 `Resource.changed` 仅在 resource_name 变化或 shader 本身变化时发出,参数改动不发出(官方文档明确);且每网格是 duplicate 的副本,与模板已无关联。
3. 若要 2 也自动刷新,需 EditorPlugin 监听 `EditorInterface.get_inspector().property_edited`(或 EditorFileSystem),回调里调用 `_apply_materials()`。
- 备选架构:改用 `GeometryInstance3D.set_instance_shader_parameter()` 的 per-instance uniform,可不 duplicate、直接共享 toon.tres,改参数即实时生效;但限制是**不支持贴图/数组**、每 shader 上限 16 个。本项目需要 per-mesh 的 `base_texture`,因此该方案只能覆盖 color 之类的标量/向量参数,贴图仍需 duplicate 或走 Texture2DArray + index。
## 追加:评估 per-instance uniform + Texture2DArray 方案(结论:不适合本项目)
- 实测贴图尺寸(脚本扫 PNG 头):全项目 289 张 PNG / 24 种尺寸。
- `assets` 180 张:1024×1024 ×108、512×512 ×40、4096×4096 ×25、2048×2048 ×5
- `assets/自建资产` 18 张:4096×4096 ×13(地板、地毯 PBR 套图)
- `character` 81 张:32×32 ×70、512×512 ×4、1024×1024 ×1
- `demo` 28 张:15 种尺寸混杂
- 结论:Texture2DArray 要求所有层同尺寸/同格式/同 mipmap,只能统一到 4096(单层带 mipmap 约 85 MiB 无损 / 21 MiB VRAM 压缩),几百张即 GB 级显存,且需额外打包流水线 → **per-instance uniform 路线不划算**,除非贴图少且尺寸统一。
- 推荐路线(未实施):
- A:保留 duplicate,加 EditorPlugin 监听 `EditorInspector.property_edited`,命中 toon.tres 时重刷 `_apply_materials()`(可加防抖)。
- B:把公共外观参数(color/specular/fresnel/normal_scale/uv_scale/uv_offset)提升为 `global uniform`(项目设置 → Shader Globals 定义),调参全局实时生效、无需重刷;贴图与 per-model 覆盖仍留在 duplicate 材质上。代价:需改 cel-shader-base.gdshader / outline.gdshader 并清掉 toon.tres 里的同名参数;全局值会写入 project.godot,需团队一起提交。
## 已实施:路线 A(新增 addons/toon_refresh 编辑器插件)
用户选择 A。改动:
- 新增 `addons/toon_refresh/plugin.cfg` + `plugin.gd`(EditorPlugin)。
- `project.godot` 的 `[editor_plugins] enabled` 追加 `res://addons/toon_refresh/plugin.cfg`。
实现要点:
- 监听 `EditorInterface.get_inspector().property_edited`,命中 `res://shader/toon.tres` 或其内联子资源时,0.2s 防抖后遍历当前编辑场景,对脚本文件名为 `toon_apply.gd` 且有 `_apply_materials` 的节点调用该方法。
- 另加 项目 > 工具 菜单项「刷新 toon 材质(当前场景)」作手动兜底。
- 只影响当前编辑场景;不覆盖手动改过的单个材质副本。
已实测确认的事实(重要,别再靠猜):
- **`toon.tres` 的 `next_pass` 内联子资源,`resource_path` 不是空串**,而是 `res://shader/toon.tres::ShaderMaterial_m5gih` 这种带 `::` 后缀的形式 → 匹配逻辑用 `begins_with(TARGET + "::")`。
- `ShaderMaterial.changed` **不会**因 `set_shader_parameter()` 触发;因改 `resource_name` 触发(与官方文档一致)。
- 编辑器路径:`D:\_yvmou\Godot\Godot_v4.7.2-stable_win64.exe\Godot_v4.7.2-stable_win64.exe`(另有 mono 版)。可用 `--headless --editor --path E:/Godot/agame --quit` 做插件加载冒烟测试;用 `--check-only --script res://xxx.gd` 做语法校验。
- 该项目 headless 启动时 godotsteam / limboai 的 GDExtension DLL 会报加载失败(既有问题,与本次改动无关)。
项目相关:`[shader_globals]` 里已有 diffuse_curve、specular_smoothness、fresnel_smoothness、outline_width、outline_color —— 描边与光照曲线本来就是全局 uniform,调参实时生效;真正需要重刷的是 toon.tres 上的材质级参数。
## 修复:USE_NORMAL_MAP=1 时"平视全场景变暗、俯瞰才亮"
用户报告的症状。根因是 `cel-shader-base.gdshader` 里的一行:
`NORMAL = normal.vector;`(原第 121 行)——`normal.vector` 是**切线空间**的法线贴图值,
而 Godot 4 的 `fragment()` 里 `NORMAL` 是**视图空间**(官方文档明确写
"inout vec3 NORMAL ... in view space")。把切线空间值写进视图空间的 NORMAL,
等于让所有像素的法线都指向"视图空间 +Z"≈ 朝向相机的一个常量 →
整个场景明暗只由相机朝向决定:平视时与光方向近乎垂直/反向 → 全黑且无明暗起伏;
俯瞰时与向下打的光对齐 → 恢复正常。`normal_map_fragment()` 里算好的正确视图空间法线
`ref_normal` 被算出来后就丢掉了(死代码)。
连带问题:`normal_map` 这个 uniform 从来没被赋过值(toon_apply.gd 只抓 base_texture),
采样到引擎默认纹理后算出的法线同样是垃圾。main2.tscn 里 413 个 toon_apply 节点刷出的
434 个网格中,430 个都没有法线贴图。
改动:
- `shader/includes/normal-map.gdshaderinc`:删掉 `ref_normal` / `vector.xy*2-1` / `vector.z`
重建等死代码,改为返回 `NormalData(map, normal_scale)`,map 就是原始纹理值(切线空间),
蓝通道交给引擎重建;补上坐标空间说明。
- `shader/cel-shader-base.gdshader`:`USE_NORMAL_MAP` 分支只写
`NORMAL_MAP = normal.map; NORMAL_MAP_DEPTH = normal.depth;`,不再碰 `NORMAL`。
- `shader/toon_apply.gd`:抓 `normal_texture`(StandardMaterial3D)或 `normal_map`
(ShaderMaterial)→ 有就赋给副本;**没有就显式把 `normal_scale` 设为 0.0**,否则会采样默认纹理。
验证(真实场景跑法,见 MEMORY.md):
- 对照实验确认 dummy 渲染后端仍会报 `SHADER ERROR`,所以无头运行可校验 shader 语法。
- 加载 `world/main2.tscn` 零 shader 错误、零脚本错误。
- 功能断言:toon_apply 子树 434 个网格全部应用成功;430 个 `normal_scale=0.0`(无贴图),
4 个 `normal_scale=1.0` 且 `normal_map` 已设置(源材质确实带法线贴图)。
遗留待用户确认(未改动):
- `world/main2.tscn` 的 `model/floor1`(不归 toon_apply 管,用自建 `surface_material_override/0`)
上 `normal_scale = -1.7749993243124995`,在新语义下等于 `NORMAL_MAP_DEPTH = -1.775`(反向外插),
几乎不可能是想要的,建议改成 1.0。
- `toon_apply.gd` 只读 `mesh.surface_get_material(0)`,忽略了 `surface_material_override`,
对把材质放在节点覆盖槽上的模型会抓不到贴图(本次未改,属潜在问题)。
## 排查:亮色模型"像自发光"(未改动任何项目文件)
用户问"没开自发光,为什么颜色较亮时有自发光感"。用 Godot 无头脚本采样截图
(`Image.load_from_file` + 逐像素统计,临时脚本已删):
- 红篮区域(25988 样本):**37.1% 的像素有通道被钉在 255**,均值 (164,57,56)
- 抽屉顶面(7616 样本):**63.0% 触顶,4758 个纯白像素**,均值 (212,212,205)
- 深色柜体正面(10048 样本):仅 **3.3% 触顶**,均值 (108,89,74)
- 背景:66452 样本 min==max==(54,72,87) → **纯色平涂,不是天空盒**
结论:是**通道削顶(clipping)**,不是自发光。shader 里 `USE_EMISSION 0`,且 toon.tres
的 specular/fresnel 的 rgb 都是 0 → 唯一光照项就是 `ALBEDO × LIGHT_COLOR × 阶梯`。
亮色/高饱和色在线性空间里本来就有通道接近 1.0,光照只要 >1 就把它钉死 →
整片变成没有渐变的满色 → 观感就像发光;中等亮度色需要 3 倍以上才触顶,所以正常。
阶梯曲线本身是平的(`new_gradient_texture_1d.tres`:u<0.127→0,0.14~0.50→0.198,
u>0.523→1.0),削顶后连最后一点层次也没了。
项目里的过曝来源候选:
- `world/main2.tscn` 的 SpotLight3D `sconce`:`light_energy = 2.655`、`light_indirect_energy = 2.41`
- `world/main.tscn` 的 Environment:`ambient_light_source = 3`(天空)+ 自定义天空
`atm_sun_intensity = 18`、`sun_disk_intensity = 30` → 环境光辐照度可远超 1
- **矛盾点(可用来定位)**:main.tscn 的 Environment 是 `tonemap_mode = 3`(ACES) + `tonemap_white = 6.0`,
ACES 会把高光滚降,理论上几乎不可能出现纯 255(输入 4.0 也只到 ~253)。但截图里大面积纯 255
→ 说明被观察的视口**没有套用那套 ACES 环境**:要么看的是 main2.tscn(自身无 WorldEnvironment、
无 tonemap → 硬削顶),要么编辑器 3D 视口没启用场景环境。
- 另一个坑:toon.tres 的 `color = 0.549` 带 `source_color`,线性空间里只有 **0.26**,
toon 管线把反照率整体压掉约 4 倍 → 亮暗反差被拉得很大。
- `world/main2.tscn` 里没有任何 `use_custom_color = true`,所以不存在自定义颜色覆盖。
- `world/main.tscn` 通过 ext_resource 把 `main2.tscn` 作为子场景实例化。
待用户确认:具体是哪个场景/哪个物体,以便定位"材质来源 + 光源 + 环境"这条链路。
## 重要:上游 godot4-cel-shader 的法线贴图路径是"从未运行过的死代码"
用户质疑"着色器源自 https://github.com/eldskald/godot4-cel-shader,理应无需修改"。
本地有干净的上游副本 `E:/Godot/godot4-cel-shader-main/godot4-cel-shader-main/`,比对结果:
- 本项目改动前的 `cel-shader-base.gdshader` / `includes/normal-map.gdshaderinc`
**与上游逐字一致**(包括 `NORMAL = normal.vector;` 和 `ref_normal` 算完即丢)。→ 当时不是本项目改坏的。
- 但上游 5 个 shader 变体**全部是 `#define USE_NORMAL_MAP 0`**,全仓库没有任何一处写成 1,
README 里 "normal" 出现 0 次 → **这条路径上游从未启用、从未测试过**。
- 本项目把它开成 1,才第一次走进这段代码,于是坐标空间错误暴露。
- 结论:**只要 `USE_NORMAL_MAP 1`,那行就必须改**;想严格保持上游 1:1,唯一办法是把开关保持 0。
- Godot 侧证据:引擎自己也会再做一次 NORMAL_MAP 合成
(Godot 源码 clearcoat 等价实现:`normalize(tangent*map.x + binormal*map.y + normal*map.z)`),
公式里的 `normal` 就是被污染过的 NORMAL → 污染会传播;且 depth=0 也无法"中和",
因为 mix 的基值本身就是被写坏的 NORMAL。
给用户的三个选项(待其选择):
A. `USE_NORMAL_MAP` 改回 0,并把两个 shader 文件完全还原成上游(代价:实测 434 个网格里那 4 个带法线贴图的会失去效果)
B. 只保留 `cel-shader-base.gdshader` 删掉 `NORMAL = normal.vector;` 这一行的差异,include 还原上游(1 行差异)
C. 保留当前版本(include 里的死代码也清掉,与上游差异最大但最干净)
### 已实施:用户选择 A
- 用 git 精确还原:`git checkout -- shader/cel-shader-base.gdshader shader/includes/normal-map.gdshaderinc shader/toon_apply.gd`
(先 `git diff` 逐 hunk 确认过全部改动都是本次会话产生的,没有混入用户未提交的修改)。
- 再把 `#define USE_NORMAL_MAP 1` 改成 `0`(HEAD 里本来就是 1,是用户自己开的)。
- 结果:`normal-map.gdshaderinc` 与上游**逐字一致**;`cel-shader-base.gdshader` 与上游的差异
只剩项目自己加的中文注释行(值已与上游相同);`toon_apply.gd` 无改动。
- 验证:`--headless res://world/main2.tscn --quit-after 60` 无 shader / 脚本 / 解析错误。
- 注意:`toon.tres` 里仍有 `shader_parameter/normal_scale = 1.0`,宏关闭时被忽略,无副作用;
`world/main2.tscn` 里 `model/floor1` 那份自建材质的 `normal_scale = -1.775` 同样被忽略。
+205
View File
@@ -0,0 +1,205 @@
# 2026-10-15
## 玩家状态管理迁移到 LimboAI LimboHSM
- 需求:玩家场景的状态管理完全用 LimboHSM 实现(addons/limboai v1.8.1,GDExtension 版,
**无需启用 editor plugin**,ClassDB 里没有 HSMPlayer —— LimboHSM/LimboState 本身就是 Node)。
- 方案讨论:BT vs HSM 选型 → 玩家是人操作的(决策已由人完成),用 HSM 建模身体状态;
BT 留给将来的 NPC/autopilot。
- 新增 `character/player/hsm/`(11 个脚本):
- `player_hsm.gd`(根,转换表 Grounded⇄Airborne:`airborne`/`landed` 事件)
- `grounded.gd`(Grounded 子机:12 条转换 + `_enter` 按当下输入挑初始子状态)
- `airborne.gd`(Jump→Fall on `falling`;`_enter` 按 vy>0 挑 Jump 否则 Fall)
- `ground_state.gd`(地面叶子基类:统一判 try_jump/踏空 → `airborne`,再走 `_think()`)
- idle/walk/sprint/crouch/sneak.gd(写 motor.target_speed/crouching/sprinting + 动画)
- jump.gd/fall.gd(落地判定 `is_on_floor and vy<=0`,带 vy 门槛防起跳帧弹回)
- 改动:
- `player.gd`:加 `class_name Player`;挂 HSM;`_ready` 末尾 `hsm.initialize(self)` +
`hsm.set_active(true)`(仅权威端);管线第一位 `hsm.update(delta)`;animator 改调 `set_gait`。
- `player_motor.gd`:删掉从输入推断 sprint/crouch/target 的逻辑,暴露
`target_speed`(状态写)、`has_move_input()`、`try_jump()`(起跳在状态判定处就地执行,
同帧转 Jump);crouching/sprinting 变成状态写入的事实镜像(Look/DebugPanel 继续用)。
- `player_animator.gd`:`set_motion` 拆成 `play_state(state)`(状态 _enter 调)+
`set_gait(velocity)`(每帧刷倍率);删 `_resolve_state` 和 `idle_threshold`。
anim_state/anim_speed 同步属性保持不变(远端靠 Sync 复制驱动)。
- `player.tscn`:HSM 节点子树(LimboHSM root + Grounded{Idle,Walk,Sprint,Crouch,Sneak} +
Airborne{Jump,Fall}),根机器 `update_mode = 2`(MANUAL,编辑器里写入 int 失败,
手改 tscn 后 force_reload 场景生效)。
- 行为变化(有意为之):行走/站立切换改按**输入**判定(旧版按速度阈值 0.2 m/s),
顶墙时播 walk 而非 idle;其余(蹲伏优先于疾跑、跳跃缓冲、落地按输入挑状态)与旧版一致。
- 验证:全部脚本 `--check-only` 通过;无头探针场景模拟输入跑完整状态链
(Idle→Walk→Sprint→Sneak→Crouch→Idle→Walk→Jump→落地Walk→Idle),数值与理论吻合。
## 二次重写:小人国定标(同日)
- 用户明确:不保旧手感、可重写;**小人国玩法,角色身高约 0.2 m**;旧代码可删。
- 定标(全部固化进 player_motor.gd 默认值,删掉了 tscn 里的 walk/sprint 覆盖):
walk 2.419(≈12 身长/s)、sprint 3.624(≈18 身长/s)、crouch 1.21(走速一半)、
accel 22(0.11s 到全速)、air_accel 5.9(地面 1/4)、jump 1.5 / gravity_scale 0.5
(跳高 0.23 m ≈ 1.15 身高、滞空 0.61 s)不变。DebugPanel body_height 0.15 → 0.2。
- **修掉一个真 bug**:场景里 walk 被调到 2.419 后,动画器基准速度还是 0.4286,
walk/run 动画一直被 clamp 在 1.6× 上限。重构:删掉 animator 的 4 个 *_ref_speed 导出,
`play_state(state, ref_speed)` 由 HSM 状态传入基准速度(状态知道目标速度是多少),
`set_gait` 用存下的 `_ref_speed` 算倍率 —— 两处对表的坑没了。
- 探针复测:三个位移动画全速倍率均 1.00,转换链不变。
### 踩坑(复用注意)
1. **无头 `--check-only` 对新 class_name 报「Could not find type」**:
全局类缓存 `.godot/global_script_class_cache.cfg` 没刷新。让编辑器跑一次
`EditorFileSystem.scan()`(godot_ai MCP 的 filesystem scan)即解;批量循环检查时
同一批里前后结果不一致也是这个原因。
2. **godot-ai MCP `node_set_property` 写 enum int 属性失败**
(WRONG_TYPE: Cannot write String to a int property,value 传 JSON number 也一样)。
变通:MCP scene_save 后直接手改 .tscn 补属性行,再 `scene_open force_reload=true`。
3. **MCP 场景节点路径**要含场景根:`/Player/HSM`,不是 `/HSM`。
4. **LimboHSM 嵌套用法**:子状态机的 `_enter`(脚本 override)先于其 initial_state 激活执行,
所以可以在 `_enter` 里 `set_initial_state()` 动态挑入口子状态(落地按输入挑、空中按 vy 挑)。
5. **转换触发的环境判定要放叶子状态**,不要放父机 `_update`:父机先于子状态执行,
在父机里 dispatch 转换后,本轮级联还可能调到已退出的旧子状态(叶子自己判、dispatch 后
立即 return,没有这个窗口)。
6. 无头模拟按键:`Input.action_press` 只改动作状态、**不发输入事件**(Motor 的跳跃缓冲在
`_unhandled_input` 收事件,探不到);要用 `Input.parse_input_event(InputEventAction)`。
---
## main2 卡通渲染 / 世界环境 / 光照 调优(达到可交付观感)
目标:把 `world/main2.tscn` 的卡通着色器参数 + WorldEnvironment + 光照调到"商业化"水平。
改了 5 个文件:`project.godot`、`world/main2.tscn`、`shader/toon.tres`、
新增 `shader/toon_ramp_1d.tres`、删除 `shader/new_gradient_texture_1d.tres`。
### 动画诊断出的三个致命问题
1. **描边等于不存在**。`cel-shader-base` 的 `next_pass` 链、`outline.gdshader` 都正常,
但 `outline_width = 2.0` 在这个 Godot 版本里只对应约 **0.1px**,肉眼不可能看见。
实测标定:`像素宽度 ≈ outline_width × 0.057`(1364px 视口)。已改为 **30**(≈1.7px)。
2. **`toon.tres` 的 `specular` / `fresnel` RGB 都是纯黑** —— 两个模块虽然连着,贡献恒为 0。
3. **漫反射渐变是"黑→硬边→白"的二值 ramp**,导致场景只有一层死板明暗。
### 最关键的一条坑(务必记住)
**编辑器只在启动时注册 `[shader_globals]`。** 外部(含 MCP `settings_set`)改
project.godot 的全局着色器参数,正在跑的编辑器视口**完全不变**。
我就是因为这个,一开始把 `outline_width` 从 2 调到 10 再到 60,截图却毫无变化,
最后才定位到根因。验证手段:写一个 `res://tests/test_*.gd`(必须 `extends McpTestSuite`),
在 `test_*` 方法里直接 `RenderingServer.global_shader_parameter_set(...)`,
再用 MCP `test_run` 执行 —— 能在编辑器进程内把值推进渲染器。
同时用这个办法做了隔离实验,排除了 4 个错误猜想:雾、SSAO、泛光、菲涅尔**都不是**
墙面噪点的来源;真正的噪点是**截图传输压缩**(480px 截图墙面完全干净,1364px 才有颗粒)。
另外顺手证实 `POSITION` 顶点覆盖是生效的(写 `p.y += p.w * 0.8` 面板会整体下移),
所以描边 pass 本身没问题,别去改上游 `outline.gdshader`。
### 最终取值
- **着色器全局**(`project.godot [shader_globals]`):
`diffuse_curve = toon_ramp_1d.tres`、`specular_smoothness = 0.08`、
`fresnel_smoothness = 0.12`、`outline_width = 30`、
`outline_color = Color(0.08, 0.07, 0.1, 1)`。
- **新 ramp**(`shader/toon_ramp_1d.tres`,GradientTexture1D/256):
0.00–0.30 → 0.24;0.38–0.62 → 0.60;0.70–1.00 → 1.00,两级转场各 0.08 软边。
- **材质**(`shader/toon.tres`,以及 main2 里地板的独立 ShaderMaterial):
`specular = (0.12,0.12,0.12,0.45)`、`fresnel = (0.4,0.55,0.8,0.18)`。
- **Environment**:天空换成项目现成的 `world/sky/cartoon_sky.tres`(与 character_select /
ocean_demo 一致);`ambient_light_source = 3`(Sky) + 冷色补光
`(0.3,0.38,0.55)`、sky_contribution 0.45、energy 0.5;ACES 色调映射(`tonemap_mode=3`,
这个版本枚举确实是 LINEAR0/REINHARDT1/FILMIC2/ACES3/AGX4);SSAO 轻度(intensity 1.0、
detail 0.3、light_affect 0);Glow 阈值抬到 2.2 防止白面爆光;雾 0.004 + aerial 0.3;
adjustment contrast 1.12 / saturation 1.08。
- **光照**:平行光改暖色 `(1,0.925,0.83)`、energy 0.95、`light_angular_distance 0.8` 软阴影、
bias 0.05 / n_bias 1.5、max_distance 60;sconce 聚光灯 energy 3.2 / range 12 /
angle 55 / 开启阴影。顺手开了 `anti_aliasing/quality/msaa_3d = 2`(4x,描边抗锯齿)。
### 排查时踩的 MCP 细节
- MCP `settings_set` **写不了 Color**:传 `{"r":..,"g":..}` 会落成嵌套字典、
传 `"#141219"` 会落成 String,两者都不是引擎要的 Color。
正解是手改 project.godot 写成 `Color(r,g,b,a)`。改完用无头探针脚本
(`extends SceneTree` + `ProjectSettings.get_setting()` 打印 `typeof(inner)`)复验。
- `settings_set` 会把**整个** ProjectSettings 从内存写回磁盘,
所以它写完之后我手改过的 `Color(...)` 会被覆盖成 String —— **必须最后一步再手改**。
- `editor_screenshot` 的 `view_target` 会**移动用户的编辑器相机**;`max_resolution=0`
才是原生分辨率。
### 追加:远处地面"发白"的根因 = fresnel 掠射角饱和(务必记住)
用户反馈:main2 里走廊/餐厅方向的地面(z≈15~25)呈一片**灰白偏冷的平面**,近处木地板正常。
- 排查路径:`floor1.glb` 只有 1 个 mesh / 1 个材质(用 Python 解 GLB 的 JSON chunk 确认),
AABB 覆盖 x −18.2~16.6、z −24.9~32.9 —— **餐厅地面就是这块地板**,不是缺几何、
也不是贴图问题(BaseColor 是纯木纹)。地毯不泛白是因为它的固有色深,把叠加量盖住了。
- **根因在 `shader/includes/base-cel-shader.gdshaderinc` 的 `fresnel_light()`**:
```
fresnel_dot = 1.0 - dot(normal, view); // 水平面越远越掠射 → 越接近 1
fresnel_threshold = pow(1.0 - fresnel_strength, dot(light, normal)); // ≈0.84
... * smoothstep(threshold ± fresnel_smoothness/2, fresnel_dot)
```
实测:5 m 处 fresnel_dot≈0.69(=0,无贡献),20 m 处≈0.92(=1,满强度)。
于是**远处地面被整片点亮**,叠加的是 `fresnel_color = (0.4,0.55,0.8)` 蓝白,
木地板 albedo(≈0.34,0.24,0.2) + 叠光(≈0.40,0.51,0.66) → 就是用户看到的灰白。
- **是上一轮调优引入的**:`fresnel` 原来 RGB=(0,0,0)(贡献恒 0,所以从没显形),
上一轮改成 (0.4,0.55,0.8) 后才出现。**大平面(地板/天花板)上放菲涅尔轮廓光必然有这副作用。**
- 修法(择一):`fresnel.a` 调到 0.05 以下;或把 `fresnel_color` 压暗到接近物色;
或把 `fresnel_smoothness`(全局 uniform)调小收紧过渡带。当前已按用户要求
**恢复为 (0.4,0.55,0.8,0.18)**,泛白会重现,等用户定夺。
### 追加:MCP `material_manage(apply_to_node)` 会写盘并**删除孤儿资源**
- 用 MCP `apply_to_node` 给 `model/floor1/pPlane1` 临时套一个纯红 unshaded 材质做隔离实验,
之后 `scene_open force_reload` **材质没有恢复** —— 因为该操作**直接改了磁盘上的 .tscn**
(返回值里的 `saved_to: ""` 不代表没落盘)。
- 更严重:Godot 重新序列化时把**失去引用的资源全删了** —— 地板配套的
`ShaderMaterial_mmyc1`(cel + outline)、`ShaderMaterial_tyxef`,
以及 3 个 `ext_resource`(`outline.gdshader`/`cel-shader-base.gdshader`/地板 BaseColor 贴图)
一并消失,同时把等于默认值的属性行省略(如 `glow_blend_mode = 1`)。
- **教训:拿 MCP 改材质做实验前先 `git stash` 或备份 .tscn**;事后用 `git diff` 对照逐行恢复。
恢复时注意用**调优后的值**(不是 HEAD 的原值)。
## 阴影锯齿专项(main2 沙发/盘子)
- 现象:沙发扶手上大片「梳齿/蕨叶」状黑纹 + 盘子影子边缘阶梯状锯齿。
- 逐个隔离(改 `shadow_enabled` / 单变量逐项)后确认:**都来自 `DirectionalLight3D`**。
- **根因 = `light_angular_distance = 0.8`**。它开启 PCSS 软阴影,半影半径随距离放大,
但 Godot 的采样数有限 → 出现稀疏采样尖刺(就是那片蕨叶纹)。
归零后(`0.0`)两个 artifact **同时消失**。
- 次要项:工程设置 `soft_shadow_filter_quality` 默认是 **2(Soft Low)**,
提到 **4(Soft High)** 后高频噪点能量 1.149 → 0.687(-40%),强跳变 756 → 630。
- `shadow_blur`(1.0/1.5/2.5/4.0)实测**几乎无影响**(0.669~0.687),已删掉该覆盖行。
- `shadow_bias`/`shadow_normal_bias` 归零会导致**全身自阴影爆发**(阴影面积 18484 → 114706),
必须用引擎默认 0.1 / 2.0(原场景的 0.05 / 1.5 偏低)。
- 最终:`light_angular_distance = 0.0`、删掉 bias/blur 覆盖行、
`directional_shadow_max_distance 60 → 45`、`project.godot` 加
`lights_and_shadows/{directional_shadow,positional_shadow}/soft_shadow_filter_quality=4`。
- 前后对比图存在 `docs/shadow_aliasing_fix.png`。
### 追加:用户把 blur 归零后「全画面三角形颗粒」
用户手动把 `shadow_blur = 0`,**同时把 `shadow_normal_bias` 也设成了 0**,
结果整幅画面(沙发、盘子、地板全部)盖满斜向的三角颗粒 / 摩尔纹。
在独立进程里做了 2×2 对照(`soft_shadow_filter_quality = 4`,
指标 = 区域内相邻像素亮度差的平均,越低越干净):
| 组合 | 4096 贴图 | 8192 贴图 |
|---|---|---|
| blur 0 + normal_bias 0(**用户当时**) | **12.730** | **17.923** |
| blur 0 + normal_bias 2 | 0.638 | 0.611 |
| blur 1 + normal_bias 0 | 0.566 | 0.522 |
| blur 1 + normal_bias 2(推荐) | 0.577 | 0.532 |
| blur 0.6 + normal_bias 2 + max_dist 25 | 0.546 | 0.537 |
结论:
- **必须「blur=0 且 normal_bias=0」同时成立才会爆**(12.7)。任意改回一个就回到 ~0.6。
机制:`normal_bias = 0` 会在**每个阴影贴图纹素**上产生自阴影判定抖动,
而 `shadow_blur` 正是那个 PCF 半径 —— 它被归零后抖动不再被平滑,
于是以贴图纹素的频率原样显现成三角颗粒(贴图纹素斜投影到受光面就是四边形/三角)。
- **加大阴影贴图分辨率没用,反而更糟**(12.730 → 17.923)。
分辨率越高的贴图纹素越细密 → 抖动频率越高 → 颗粒越密越明显。
**所以"锯齿是阴影贴图不够大"是错的直觉。**
- 想要更锐的阴影:`shadow_blur` 可以取 0(normal_bias 保持 2.0 就干净,0.638)。
- 已把编辑器的 `shadow_normal_bias` 恢复到 2.0、`shadow_blur` 恢复到 1.0
(用 MCP `node_set_property`,**实测它只改编辑器内存、不写盘**)。
- 对比图存在 `docs/shadow_normal_bias_fix.png`。
+26
View File
@@ -0,0 +1,26 @@
# 2026-10-16
## 修复:第一人称相机高达 1.16 m(玩家缩放被静默抹掉)
- 现象:进游戏后第一人称视角像一米多高看得见 woman 的胸口;DebugPanel 显示
「相机高度 1.161 m / 5.80 身高」。
- 根因:`world/main.gd` 的 `_create_player()` 里写 `player.global_position = ...`,而该节点
**此时还没入树**(MultiplayerSpawner 之后才 add_child)。`Node3D.get_global_transform()`
在未入树时 `ERR_FAIL` 返回**单位矩阵**,`set_global_transform()` 顺势把 Player 根节点的
basis 换成单位阵 → **player.tscn 那层 0.096096 缩放被抹掉**。
- 无头探针对照(复现两种写法,同一份 player.tscn):
| 写法 | Player.global_scale | 模型世界高 | 相机相对脚底 |
|---|---|---|---|
| 旧 `global_position` | 1.0 | 1.561 m | **1.1610 m** |
| 新 `position` | 0.096096 | 0.150 m | **0.1116 m** |
旧写法的 1.1610 m 与游戏内面板的 1.161 m 逐位吻合;控制台每个玩家还会刷一条
`ERROR: Condition "!is_inside_tree()" is true.`。
- 修复:`player.position = player_spawn_pos.position`(Main 与 PlayerSpawnPos 同级、Main 无变换,
局部坐标即世界坐标)。`--check-only` 通过。
- 离屏渲染三张对照图(woman 1.703 m 站位旁):`probe_fix_A.png`(同框,旧写法巨鼠几乎挡住
woman)、`probe_fix_B.png`(woman 脚边,修复后 15 cm 小鼠)、`probe_fix_C.png`(修复后
第一人称,面板读数 0.112 m / 0.56 身高)。
- 附带确认:`InteractionRay` 世界长度在两种状态下都是 0.25 m(`Camera3D` 全局缩放恒为 1.0),
所以修缩放不影响拾取距离。
+212
View File
@@ -0,0 +1,212 @@
# agame 项目长期笔记
Godot 4.7.2(Forward+,Jolt Physics,D3D12)。**可用编辑器(2026-10-16 实测):
`E:\Godot_v4.7.2-stable_mono_win64\Godot_v4.7.2-stable_mono_win64\Godot_v4.7.2-stable_mono_win64.exe`
(mono 版;同目录另有 `..._console.exe`)。**
⚠️ 旧记录里的 `D:\_yvmou\Godot\...` **已失效 —— D 盘根本没有 `_yvmou` 目录**,别再照抄。
## 代码约定
- **GDScript 警告被当作错误**(至少 `inference_on_variant`)。不要写成
`var x := some_variant_returning_call()`;要显式写类型,例如
`var s: Script = node.get_script() as Script`。改完脚本务必用
`--check-only --script res://xxx.gd` 自查。
- 缩进用 tab,注释用中文(项目既有风格)。
## 无头验证的正确姿势(踩过坑)
- `--check-only --script res://x.gd`:GDScript 语法校验,有效。
- `godot --headless --editor --path <proj> --quit`:验证编辑器插件能否加载,有效。
- **不要**用 `extends SceneTree` 的 `--script` 脚本去测 `_ready()` 相关行为:
这种模式下节点不会真正进入场景树(`is_inside_tree()` 为 false),`_ready` 不执行,
会得出完全错误的结论。正确做法是造一个临时场景,用位置参数当主场景跑:
`godot --headless --path <proj> res://tools/_tmp_probe.tscn`。
- 即使是 `--headless`(dummy 渲染后端),**shader 编译错误仍会被报告**
(`SHADER ERROR: ...`),所以可以用无头运行来校验 shader 语法。
## 玩家 = LimboAI LimboHSM(2026-10-15 起)
- LimboAI v1.8.1 是 **GDExtension 版**(addons/limboai 只有 bin/,无 plugin.cfg),
**不需要启用 editor plugin**;ClassDB 里没有 HSMPlayer —— `LimboHSM`/`LimboState` 本身就是 Node。
- 玩家状态机在 `character/player/hsm/`:根 `player_hsm.gd`(Grounded⇄Airborne)+
Grounded{Idle,Walk,Sprint,Crouch,Sneak} + Airborne{Jump,Fall},全是场景节点、脚本挂状态上。
- 分工:**HSM 管「是什么状态」**(写 motor.target_speed/crouching/sprinting + 调
animator.play_state),**motor 只管物理**(重力/加速度/跳跃缓冲 + try_jump),
animator 只管「怎么播」(play_state + set_gait 刷倍率)。player.gd 管线第一位
`hsm.update(delta)`(MANUAL 模式,仅权威端 initialize+set_active;远端靠 Sync 复制)。
- 走/站立转换按**输入**判定(非旧版的速度阈值)——顶墙时播 walk 不播 idle,是预期行为。
- 用法要点:子机 `_enter` 里可 `set_initial_state()` 动态挑入口子状态;环境判定
(起跳/踏空/落地)放**叶子**状态的 _update,别放父机;落地判定要带 `vy<=0` 门槛。
- 踩坑:新 class_name 后无头 `--check-only` 报「Could not find type」= 全局类缓存没刷新,
让编辑器 scan 一次即解;MCP node_set_property 写 enum int 失败 → 手改 tscn + 场景 force_reload。
## toon 卡通渲染管线
- `shader/cel-shader-base.gdshader`(`USE_*` 宏开关各模块)+ `shader/includes/*.gdshaderinc`,
上游是 godot4-cel-shader 项目。
- `shader/toon.tres` 是材质模板;`shader/outline.gdshader` 作为 `next_pass` 描边。
- `shader/toon_apply.gd`(@tool Node3D)给每个 MeshInstance3D 复制一份独立材质,
因为**每个模型有各自的贴图**,所以必须独立(不能共享一份材质)。
- 已在项目设置里用了 **global uniform**:`diffuse_curve`、`specular_smoothness`、
`fresnel_smoothness`、`outline_width`、`outline_color`。
⚠️ **但"全局实时生效"是错的**(2026-10-15 实测纠正):编辑器只在**启动时**
把 `[shader_globals]` 注册进 RenderingServer,之后外部改 project.godot(或
MCP 的 `project_manage settings_set`)**不会**影响正在运行的编辑器视口。
手动改完全局参数必须**重启编辑器**(或临时用 `test_run` 跑一段
`RenderingServer.global_shader_parameter_set()` 把值推进去)才能看到效果。
这正是当初"改了 outline_width 却毫无变化"的原因。
- `outline_width` 的实际像素宽度约为 `width × 0.057`(1364px 视口实测校准),
所以**原来的 2.0 实际只有约 0.1px —— 等于没描边**。要看到 2px 描边需要 width ≈ 30。
- 贴图尺寸极不统一(289 张 PNG / 24 种尺寸,1024²、512²、4096²、32² 混杂),
因此**不适合** Texture2DArray + per-instance uniform 方案(显存会爆且要额外打包流水线)。
- 编辑器插件 `addons/toon_refresh/`:改 toon.tres 时自动重刷 toon_apply 材质。
**只覆盖材质级参数**(color/specular/fresnel/uv),管不到 global uniform。
- ⚠️ **`fresnel`(轮廓光)不能给大平面用**:`fresnel_light()` 以
`1 - dot(normal, view)` 为激活量,水平地面/天花板稍远就是掠射角 → 直接饱和成满强度,
整片平面被 `fresnel_color` 点亮。**"远处地面发白"就是这么来的**
(原来是 (0,0,0) 贡献恒 0 所以没暴露,2026-10-15 改成 (0.4,0.55,0.8) 后显形)。
真要留轮廓光就把 `a` 压到 ≤0.05,或把颜色调暗到接近物色。
- ⚠️ **MCP `material_manage(op="apply_to_node")` 直接改磁盘 .tscn**,
且重新序列化时会**删除失去引用的子资源与 ext_resource**
(实测:地板实验后 `ShaderMaterial_mmyc1`/`tyxef` + 3 个 ext_resource 全被清掉,
`saved_to:""` 也照样落盘)。拿 MCP 做材质实验前先备份,事后用 `git diff` 逐行核对恢复。
## 卡通渲染调参(2026-10-15 定的基线取值)
- `shader/toon_ramp_1d.tres`(新,替代已删除的 `new_gradient_texture_1d.tres`):
三级 cel 明暗带,灰阶 0.24 / 0.60 / 1.00,转场各留 0.08 软边。
注意 `diffuse_sample()` **只取红通道**,所以渐变做成彩色也没用。
- `specular_smoothness = 0.08`、`fresnel_smoothness = 0.12`;
原值 10.0 会把高光压到几乎不可见(`smoothstep(0.05, 10.05, x)`)。
- `outline_width = 30`、`outline_color = Color(0.08, 0.07, 0.1, 1)`。
- `toon.tres` 的 `specular` / `fresnel` **RGB 原来都是纯黑 = 两个模块完全失效**,
现已改为 `specular=(0.12,0.12,0.12,α0.45)`、`fresnel=(0.4,0.55,0.8,α0.18)`。
注意 specular 的 α 只用来推 gloss 指数,**亮度只能靠 rgb 调**。
- MCP `settings_set` 写不了 Color:传 dict 会落成 `{"r":..}` 嵌套字典(引擎无法绑定),
传 `"#rrggbb"` 会落成 String。**正确做法是手改 project.godot 写成 `Color(r,g,b,a)`。**
int 型也一样:会落成 `={"type": "int","value": 4.0}` 的字典块,**用完记得手改回 `=4`**。
## 阴影质量(2026-10-15 定位完毕)
- **`DirectionalLight3D.light_angular_distance` 是本项目阴影锯齿的元凶。**
它开启 PCSS 软阴影:半影半径 = 遮挡距离 × tan(角径),但 Godot 的 PCSS
采样数固定 → 半径一大就变成**稀疏采样尖刺**,表现为:
1) 大面积表面(沙发扶手内侧面)上出现**成片的「梳齿/蕨叶」黑纹**;
2) 物体投影(白盘子)边缘出现**阶梯 + 毛刺**。
实测 `0.8°` 时 artifact 明显,**改成 `0.0` 后两者同时消失**。
⚠️ 别以为角径越大越"柔软自然"——在这个引擎里它只会换来尖刺。
- 工程设置 `rendering/lights_and_shadows/{directional_shadow,positional_shadow}/soft_shadow_filter_quality`
**默认只有 2(Soft Low)**,提到 **4(Soft High)** 实测高频噪点能量降 40%。枚举顺序:
`0 Hard, 1 Soft Very Low, 2 Soft Low, 3 Soft Medium, 4 Soft High, 5 Soft Ultra`。
- `shadow_blur`(0~10,默认 1.0)在 angular=0 时**几乎测不出影响**,不用调。
- `shadow_bias` / `shadow_normal_bias` **别低于引擎默认 0.1 / 2.0**:
归零会让全身自阴影爆发(阴影面积 18484px → 114706px)。原场景的 0.05 / 1.5 偏低。
- ⚠️ **`shadow_normal_bias = 0` + `shadow_blur = 0` 同时出现 = 整幅画面三角颗粒/摩尔纹**
(2026-10-15 实测,相邻像素亮度差 0.58 → 12.73,20 倍):
`normal_bias = 0` 让**每个阴影贴图纹素**都自阴影抖动,而 `shadow_blur` 就是 PCF 半径,
归零后抖动不再被平滑,于是按贴图纹素频率原样显现(纹素斜投影 → 三角/四边颗粒)。
任意恢复其中一个即可(0.638 / 0.566)。只想锐化就把 blur 取 0、**normal_bias 留 2.0**。
- ⚠️ **提高阴影贴图分辨率救不了锯齿**:`directional_shadow/size 4096 → 8192`
在正常配置下只改善 ~8%,在坏配置下**反而更糟**(12.73 → 17.92),
因为纹素越细抖动频率越高。"锯齿 = 贴图不够大"是错误直觉。
- 室内场景可把 `directional_shadow_max_distance` 收到 45(原来 60),近场纹素密度提高 1.3×。
## 验证"启动时固化"的设置(可复用套路)
`[shader_globals]`、`rendering/lights_and_shadows/*`(阴影贴图尺寸、软阴影滤波质量)、
MSAA 等**只在引擎启动时读一次**,改 project.godot 对运行中的编辑器视口无效。
不要靠编辑器截图验证这类改动,走这条独立进程路线:
1. 造一个探针场景 `tools/_probe.tscn` + `tools/_probe.gd`:`extends Node3D`,
在 `_ready()` 里 `load("res://world/xxx.tscn").instantiate()`、加一台 Camera3D、
等 ~60 帧、`await RenderingServer.frame_post_draw` 后
`get_viewport().get_texture().get_image().save_png(...)`、`quit()`。
2. `DisplayServer.window_set_position(Vector2i(-6000,-6000))` 把窗口挪到屏幕外,不打扰用户。
3. 用 `--resolution 1364x772` + 位置参数指定场景跑:
`godot --path <proj> --resolution 1364x772 res://tools/_probe.tscn`(**别加 `--headless`**,
dummy 渲染后端出不了图)。
4. 想在探针里改灯的参数做单变量对照,直接在脚本里改 `DirectionalLight3D` 的属性再截图,
一次进程就能出多张对照图。
5. **要像素级对齐编辑器视口**:`res://tests/test_xxx.gd`(`@tool` + `extends McpTestSuite`,
`print` 抓不到,得用 `FileAccess` 写文件)里读
`EditorInterface.get_editor_viewport_3d(0).get_camera_3d().global_transform`,
再 `test_run` 执行,就能拿到确切机位/ FOV。本项目的 sofa 机位是
`origin(-12.070005, 4.639197, -2.615989)`,`fov 70.01`,视口 1364×772。
6. 注意 main2 的「联机测试」UI 是 **autoload(`autoload/net.tscn`)**,不在场景树里,
探针要递归隐藏 `get_tree().root` 下的 `CanvasLayer`/`Control` 才遮不掉画面。
## 坐标系陷阱 + 上游着色器的边界(重要)
Godot 4 的 `fragment()` 里 `NORMAL` / `TANGENT` / `BINORMAL` 都处于**视图空间**,
而 `NORMAL_MAP` 要的是**切线空间**值、由引擎用 TANGENT/BINORMAL 还原。
把切线空间值直接赋给 `NORMAL` 会让整个场景的法线变成一个跟着相机转的常量 ——
表现为平视全黑、俯瞰才亮。
但**不要**因此去改上游文件:`cel-shader-base.gdshader` 与
`includes/normal-map.gdshaderinc` 来自 <https://github.com/eldskald/godot4-cel-shader>,
本项目的策略是**逐字跟随上游**。上游的法线贴图路径 `NORMAL = normal.vector;`
确实有上述坐标空间错误,但上游所有变体都是 `#define USE_NORMAL_MAP 0`,
全仓库从未启用过这条路径(README 也没提),所以是"能编译但结果错"的死代码。
本项目保持 `USE_NORMAL_MAP 0`,与上游一致;`toon.tres` 里残留的
`shader_parameter/normal_scale` 在宏关闭时会被忽略,无副作用。
**结论:要动上游 shader 之前,先用 `E:/Godot/godot4-cel-shader-main/` 里的干净副本 diff。**
正确做法(若将来真要开法线贴图):只写 `NORMAL_MAP = 原始切线空间值` +
`NORMAL_MAP_DEPTH`,让引擎合成;不要自己写 `NORMAL`,也不要预先做 `*2-1` 重映射
(引擎自己做,蓝通道由引擎重建)。
## 玩家尺寸 / 相机高度(2026-10-16 查证)
全在 `character/player/player.tscn`,「大小」是三层缩放叠加:
1. **`Player` 根节点整体 scale = 0.096096**(第 38 行)—— 主开关,模型+碰撞+相机一起缩放。
2. **`Body/mouse` 模型自身 scale = 0.18240134**(第 87 行,X 为负 = 180° 翻转朝向);
实际模型缩放 = 0.096096 × 0.1824 ≈ 0.0175。只想改模型不动碰撞就改这里。
3. **碰撞胶囊(局部)** `CapsuleShape3D: radius 0.54541016 / height 1.4063476`(第 33-35 行),
`Collision` 节点 y = 0.7031738(= height/2,贴地)。
- ⚠️ **相机高度与缩放联动,改一个必须同步改另一个**:站立视线高 = `CameraRig.position.y`
(当前 1.6,第 46 行),世界高度 = 1.6 × 0.096096 ≈ **0.154 m**;而 `Camera` 节点自身
scale = **10.4062605 = 1/0.096096**(第 49 行),唯一作用就是**抵消父级的 0.096 缩放**,
让相机在世界空间回到 1:1(否则 near/FOV 异常)。动了 `Player` 的 scale,这个倒数必须一起改。
- **无头探针实测(2026-10-16,真实实例化 `player.tscn`)**:`Player.global_scale = 0.096096`;
模型 `Body/mouse.global_scale = 0.017528`;**视觉包围盒 = 0.158 × 0.150 × 0.135 m(约 15cm)**;
胶囊世界尺寸 = 半径 5.24cm / 总高 13.5cm;**相机在脚底上方 0.1116 m**。
⚠️ 相机净高度 ≠ 1.6 × 0.096 = 0.154 —— `Camera` 自身局部还带 `y = −0.439`,
实际 = (1.6 − 0.439) × 0.096096 ≈ **0.1116 m**(`DebugPanel._camera_height()` 用
`camera.global_position.y − player.global_position.y` 读数就是这个值)。
- ⚠️⚠️ **2026-10-16 定位并修复的重大 bug:`world/main.gd` 的玩家生成会把 Player 根节点的缩放静默抹掉。**
旧代码在 `spawn_function` 里写 `player.global_position = player_spawn_pos.global_position`,
但该节点**此刻还没入树**(spawner 之后才 add_child)。`Node3D.get_global_transform()` 未入树时
直接 `ERR_FAIL_COND_V(!is_inside_tree(), Transform3D())` 返回**单位矩阵**,于是
`set_global_transform()` 把 Player 的 basis 整体换成单位阵 —— **0.096096 那层缩放没了**。
实测后果(与游戏内 DebugPanel 逐位吻合):玩家模型 **1.561 m**、第一人称相机 **1.1610 m**,
面板显示「相机高度 1.161 m / 5.80 身高」;控制台每生成一个玩家刷一条
`ERROR: Condition "!is_inside_tree()" is true.`。
修法:`player.position = player_spawn_pos.position`(Main 与 PlayerSpawnPos 同级、Main 无变换,
局部坐标即世界坐标)。**通用规则:未入树的节点只能写局部 position/transform。**
- 全项目**只有** `player.tscn` 一个 CharacterBody3D;`main2.tscn` 的 `World` 根节点无缩放(1:1),
所以 15cm 就是游戏内真实绝对尺寸。玩家是**约 15cm 高的小老鼠**,有意为之(见
`player_motor.gd` / `debug_panel.gd` 注释里的「小人国玩法,身高约 0.2 m」基准;
走速 2.419 m/s ≈ 12 身长/秒就是按 0.2 m 身高定标的 —— 缩放被抹掉时手感同样全错)。
- 相机自身 `scale = 10.4062605 = 1/0.096096`,作者意图是**抵消父级的 0.096**;实测
`Camera3D` 的全局缩放两种玩家缩放下都是 1.0,所以 `InteractionRay.target_position = (0,0.25,0)`
在世界里恒为 0.25 m —— **不随玩家缩放变化,改缩放时不用动它**。
- `ui/character_select.tscn`(选人界面)的预览模型只挂了 `0.18240133` **一层**缩放
(没有 Player 那层 0.096),模型 ≈ 1.56 m,机位 `PreviewCamera` 在 `(0,0.62,1.5)` 看向 `(0,0.45,0)`
也是按这个尺度配的。它是**进游戏前的展示界面**,与游戏内 15cm 天然不同尺(要一致就再乘 0.096
并把机位挪到 ~0.15 m 高)。
- **渲染/骨骼实测(2026-10-16)**:`main2.tscn` 的 `woman` AABB 高 **1.703 m**(世界是 1:1 真人尺度);
绕开 `main.gd` 直接实例化 `player.tscn` 时,玩家渲染出来约 15 cm、`Skeleton3D` 骨骼竖直跨度
0.099 m。⚠️ 这一组数据只对「直接实例化」成立 —— 走真实生成路径时缩放会被上面那条 bug 抹掉。
- ⚠️ **探针技巧**:`player.gd` 的 `_ready()` 会把**整只模型** `Body/mouse` 设为 `visible = false`,
探针里只把下面的 `MeshInstance3D` 设成 `true` **没用**(父节点还藏着),
必须显式 `player.get_node("Body/mouse").visible = true`。这个坑踩了三轮才排掉。
- ⚠️ 量 skinned mesh 时不能只看 `mi.global_transform * mi.get_aabb()`:那是 **mesh 资源**的
包围盒,**不含骨骼变换**,位置/尺寸都可能偏(实测玩家 mesh AABB 落在 Player 原点**下方**,
而真实骨骼在原点**上方**)。要真实数据就查 `Skeleton3D.get_bone_global_pose()`,或者直接渲染截图量像素。
- 蹲伏高度由 `player_look.gd` 的 `crouch_height_ratio = 0.6` 从 `CameraRig.position.y` 推导,
改 rig 的 Y 会自动跟随,不用改脚本。
- `world/main.tscn` 根节点 `Main`(Node3D)无缩放,`MultiplayerSpawner.spawn_path = ".."`,
所以 Player 的 0.096 即最终世界缩放。(`PlayerSpawnPos` 在 y=4.8,出生会先坠落。)
+1
View File
@@ -0,0 +1 @@
游戏内测试交给用户来,不要自己做
+651
View File
File diff suppressed because one or more lines are too long
-200
View File
@@ -1,200 +0,0 @@
# Asset Drawer
> An asset drawer inspired by **Unreal Engine's Content Drawer**, for Godot 4.
> Press **Ctrl+Space** and the drawer slides up from the bottom of the editor. Browse, search, drag and drop, then close it.
![Godot 4.2+](https://img.shields.io/badge/Godot-4.2%2B-478cbf?logo=godotengine&logoColor=white)
![Version](https://img.shields.io/badge/version-3.6-blue)
![Type](https://img.shields.io/badge/type-editor%20plugin-lightgrey)
<!-- Add a screenshot or GIF here:
![Asset Drawer](docs/screenshot.png)
-->
---
## Table of contents
- [Why](#why)
- [Installation](#installation)
- [Quick start](#quick-start)
- [Shortcuts](#shortcuts)
- [Features](#features)
- [Right-click menu](#right-click-menu)
- [Copy / paste a resource](#copy--paste-a-resource)
- [Export an image of the drawer](#export-an-image-of-the-drawer)
- [Customization](#customization)
- [Configuration files](#configuration-files)
- [Troubleshooting](#troubleshooting)
- [Contributing](#contributing)
---
## Why
Godot's FileSystem dock is handy, but it takes up space permanently. Asset Drawer brings the idea of Unreal's Content Drawer to Godot: a file browser that **appears on demand**, covers the bottom of the editor, then disappears when you no longer need it. It stays connected to the rest of Godot: folder colors, favorites, and drag and drop into the scene or the Inspector.
## Installation
1. Copy the `asset_drawer` folder into your project's `res://addons/` folder:
```
res://addons/asset_drawer/
├── plugin.cfg
├── plugin.gd
└── drawer.gd
```
2. Open **Project → Project Settings → Plugins** and enable **Asset Drawer**.
3. Open the drawer with **Ctrl+Space**, with the toolbar button, or from **Project → Tools → Asset Drawer**.
> Compatible with **Godot 4.2 and later**.
## Quick start
| I want to… | I do… |
|---|---|
| Open / close the drawer | `Ctrl+Space` |
| Keep the drawer open | Click the pin in the top bar |
| Resize the height | Drag the handle at the top of the drawer |
| Add an asset to the scene | Drag it from the drawer into the scene |
| Assign a resource in the Inspector | Drag it onto the matching field |
| Find a file | `Ctrl+F`, then type the name |
By default, clicking outside the drawer closes it, unless it is pinned.
## Shortcuts
| Shortcut | Action |
|---|---|
| `Ctrl+Space` | Open / close the drawer |
| `Esc` | Close the drawer (unless pinned) or the context menu |
| `Ctrl+F` | Focus the search box |
| `F2` | Rename |
| `Ctrl+D` | Duplicate |
| `Delete` | Move to trash (with confirmation) |
| `Backspace` | Parent folder |
| `Enter` | Open the selected item |
| Double-click | Open (scene, script, resource, folder) |
| `Ctrl` + mouse wheel | Thumbnail zoom |
| Mouse back / forward buttons | Back / forward history |
| `Alt+V` | Paste the copied resource onto the hovered Inspector field |
The open shortcut leaves script autocompletion alone when a `TextEdit` / `CodeEdit` has focus.
## Features
### Interface
- Floating drawer with **margins, rounded corners, an accent outline and a shadow** so it stands out from the editor
- Slide-up opening animation (slight bounce and glow on the outline), animated closing
- Resizable height, remembered between sessions
- **Pinned** mode: the drawer stays open
- **Details** panel with preview (type, size, date, UID), can be hidden
- Status bar (item count, selection, path)
- Animated notifications (green = success, red = error) to confirm actions
- Micro-animations on buttons, rounded selection and hover in the list
### Navigation
- **Folder tree** with parent folders pinned to the top as you go deeper, single click to open / expand, "Collapse all" and "Locate current folder" buttons
- Clickable **breadcrumb** and back / forward history
- Side tabs: **Folders**, **Favorites**, **Recents**, **Sets**
- Favorites synced with Godot's FileSystem dock
### Search and display
- Recursive search inside the current folder (debounced)
- Type filters: All, Scenes, Scripts, Models, Images, Audio, Shaders
- **Grid** or **list** view, with a separate zoom level remembered for each view
- HD thumbnails generated asynchronously by Godot, loaded only for visible items
- Cap on displayed items (`MAX_ITEMS`) to stay smooth in large folders
### Folder colors
- Uses the **custom colors from Godot's FileSystem dock** (the `file_customization/folder_colors` setting)
- Subfolders **inherit** their parent's color, like in the FileSystem dock
- Editable straight from the drawer: right-click a folder, then pick from the color swatches
### File management
- New folder, new scene, new script (unique default name, never overwrites)
- Rename: the matching `.import` / `.uid` files follow, along with favorites, sets and colors
- Duplicate files and folders, including recursively
- Delete to the OS **trash**, with confirmation
- Reimport
- Copy path, absolute path or UID
- Show in the FileSystem dock or in the OS file manager
### Sets
Named collections of assets, like Unreal's Collections: group files from different folders into one set and find them in the **Sets** tab. Right-click a set to delete it (the files are not touched).
### Drag and drop
Drag one or more assets into the scene, the scene tree or Inspector fields, just like from the FileSystem dock.
## Right-click menu
**On a file or folder**: open, show in dock / file manager, copy path / absolute path / UID, copy resource, new folder / scene / script, rename, duplicate, favorites, add to a set, reimport, folder color, delete.
**On empty space** (current folder options):
- Open the folder in the file manager
- Show in the FileSystem dock
- Copy the folder path / absolute path
- Add / remove the folder from favorites
- Refresh
- Switch to grid or list view
- Export an image of the drawer (transparent PNG or green background)
- New folder / scene / script
- Folder color
## Copy / paste a resource
1. Hover a resource in the drawer: a small green **Copy** button appears (or right-click → "Copy resource").
2. Hover a resource field in the Inspector (for example the `Mesh` of a `MeshInstance3D`): a **Paste** button appears.
3. Click it, or use `Alt+V`. The drawer does not need to be open to paste.
## Export an image of the drawer
Handy for posters, renders or your own README. Right-click on empty space in the drawer:
- **Export image (transparent PNG)**: the drawer is captured on black, then on white, and the opacity of each pixel is computed from the difference. Rounded corners and shadow come out clean, with no cutting out needed. It takes a few seconds to process.
- **Export image (green background)**: the drawer on pure green (0, 255, 0), to key out with a chroma key.
The image is saved to your Pictures folder and the file manager opens on it. Floating widgets (Copy / Paste buttons, notifications, hint pill) are hidden during the capture.
## Customization
Appearance settings are constants at the top of `drawer.gd`:
| Constant | Purpose | Default |
|---|---|---|
| `HEIGHT_RATIO` | Default height (share of the editor window) | `0.19` |
| `MIN_HEIGHT` | Minimum height (px) | `140` |
| `SIDE_GAP` | Space on each side (px) | `40` |
| `BOTTOM_GAP` | Space below the drawer (px) | `10` |
| `MAX_WIDTH` | Maximum width (px) | `2200` |
| `ANIM_TIME` | Animation duration (s) | `0.2` |
| `SHOW_HINT` | "Asset Drawer Ctrl+Space" pill at the bottom of the editor | `true` |
| `MAX_ITEMS` | Maximum displayed items | `3000` |
| `SEARCH_DELAY` | Search debounce (s) | `0.18` |
| `STICKY_MAX` | Maximum pinned parent folders in the tree | `3` |
Colors (`COLOR_*`, `TEXT_*`) and the folder palette (`FOLDER_COLORS`) are in the same file.
## Configuration files
- The drawer state (current folder, height, view, zoom, pinning, favorites, recents, sets) is saved in `res://.godot/asset_drawer.cfg`. The `.godot/` folder is ignored by Git by default.
- Folder colors are stored by Godot in `project.godot`, under `file_customization/folder_colors`.
## Troubleshooting
**Thumbnails don't show up**: wait for the project import to finish, then right-click on empty space → **Refresh**.
**Ctrl+Space does nothing**: check that the plugin is enabled and that the shortcut isn't already taken by your system (some operating systems use it to switch keyboard language). The toolbar button and the **Project → Tools** menu work in any case.
**A folder color doesn't show**: colors come from the FileSystem dock. Set it there, or through the drawer's right-click menu, and it will be visible everywhere.
**An error at startup**: disable then re-enable the plugin, and check that the three files are present in `res://addons/asset_drawer/`.
File diff suppressed because it is too large Load Diff
-1
View File
@@ -1 +0,0 @@
uid://ci1u46akkd1c
-201
View File
@@ -1,201 +0,0 @@
@tool
extends RefCounted
## Asset Drawer localization.
## The texts in the code are written in French (the key). If the Godot editor language is
## French, they are displayed as is; otherwise the English translation below is used
## (and, failing a translation, the original text). The language is read once per session:
## changing the editor language requires restarting it anyway.
const EN := {
"Scripts": "Scripts",
"Shader": "Shader",
"Fichiers texte": "Text files",
"Texte": "Text",
"Configuration": "Configuration",
"Personnalisé...": "Custom...",
"Créer": "Create",
"Langage": "Language",
"Extension": "Extension",
"Nom": "Name",
"Pas de solution C# dans ce projet : créez un premier script C# depuis le dock Fichiers de Godot (il générera le .csproj), puis réessayez ici": "No C# solution in this project: create a first C# script from Godot's FileSystem dock (it will generate the .csproj), then try again here",
"Déplacer ici": "Move here",
"Import avancé...": "Advanced import...",
"Import avancé indisponible pour ce fichier": "Advanced import unavailable for this file",
"Paramètres d'import": "Import settings",
"Nouvelle scène héritée": "New inherited scene",
"Échec du déplacement": "Move failed",
"%d élément(s) déplacé(s)": "%d item(s) moved",
"Élément déplacé": "Item moved",
"Dossiers": "Folders",
"Favoris": "Favorites",
"Récents": "Recents",
"Tout replier": "Collapse all",
"Localiser le dossier courant": "Locate current folder",
"+ Nouveau set": "+ New set",
"Nouveau set": "New set",
"mon_set": "my_set",
"Aucun asset": "No assets",
"Créer un dossier": "Create a folder",
"Réinitialiser les filtres": "Reset filters",
"Coller": "Paste",
"Coller la ressource copiée ici": "Paste the copied resource here",
"Copier": "Copy",
"Copier la ressource": "Copy resource",
"Ctrl+Espace": "Ctrl+Space",
"Précédent (bouton souris 4)": "Back (mouse button 4)",
"Suivant (bouton souris 5)": "Forward (mouse button 5)",
"Dossier parent (Retour arrière)": "Parent folder (Backspace)",
"Rechercher... (Ctrl+F)": "Search... (Ctrl+F)",
"Grille": "Grid",
"Vue grille": "Grid view",
"Liste": "List",
"Vue liste": "List view",
"Taille des miniatures (Ctrl+molette)": "Thumbnail size (Ctrl+wheel)",
"Détails": "Details",
"Panneau de détails": "Details panel",
"Épingler": "Pin",
"Épingler (reste ouvert)": "Pin (stays open)",
"Afficher ce dossier dans le dock Fichiers": "Show this folder in the FileSystem dock",
"Fermer (Échap)": "Close (Esc)",
"Tout": "All",
"Scènes": "Scenes",
"Modèles": "Models",
"Taille": "Size",
"Modifié": "Modified",
"Chemin": "Path",
"Ouvrir": "Open",
"Puis survolez un champ ressource de l'Inspecteur (ex. Mesh d'un MeshInstance3D) : un bouton « Coller » apparaît (ou Alt+V au clavier)": "Then hover a resource field in the Inspector (e.g. the Mesh of a MeshInstance3D): a “Paste” button appears (or press Alt+V)",
"Coller la ressource": "Paste resource",
"Copier le chemin": "Copy path",
"Chemin copié": "Path copied",
"Ajouter aux favoris": "Add to favorites",
"Retiré des favoris": "Removed from favorites",
"Ajouté aux favoris": "Added to favorites",
"+Dossier": "+Folder",
"Nouveau dossier": "New folder",
"+Scène": "+Scene",
"Clic droit : supprimer le set": "Right-click: delete set",
"\nClic droit : retirer": "\nRight-click: remove",
"Supprimer le set": "Delete set",
"Supprimer le set « %s » ? (les fichiers ne sont pas touchés)": "Delete set “%s”? (files are not touched)",
"Aucun résultat avec ce filtre": "No results with this filter",
"Set vide": "Empty set",
"Aucun résultat": "No results",
"Taille des icônes en liste (Ctrl+molette)": "Icon size in list view (Ctrl+wheel)",
"Taille des miniatures en grille (Ctrl+molette)": "Thumbnail size in grid view (Ctrl+wheel)",
"%d éléments sélectionnés": "%d items selected",
"Set : ": "Set: ",
"%d élément%s": "%d item%s",
" (limité à %d)": " (limited to %d)",
"à l'instant": "just now",
"il y a %d min": "%d min ago",
"il y a %d h": "%d h ago",
"hier": "yesterday",
"Dossier": "Folder",
"%d o": "%d B",
"%.1f Ko": "%.1f KB",
"%.1f Mo": "%.1f MB",
"Retirer des favoris": "Remove from favorites",
"Cliquer pour ouvrir le tiroir d'assets": "Click to open the asset drawer",
"Impossible de copier un dossier comme ressource": "Can't copy a folder as a resource",
"Ce fichier n'est pas une ressource Godot": "This file is not a Godot resource",
"Impossible de charger la ressource": "Can't load the resource",
"« %s » copiée — survolez un champ ressource de l'Inspecteur": "“%s” copied — hover a resource field in the Inspector",
"Aucune ressource copiée : clic droit sur un fichier → « Copier la ressource »": "No resource copied: right-click a file → “Copy resource”",
"Survolez un champ ressource de l'Inspecteur puis Alt+V": "Hover a resource field in the Inspector, then press Alt+V",
"Ce champ n'est pas modifiable": "This field is read-only",
"Type incompatible : ce champ attend « %s »": "Incompatible type: this field expects “%s”",
"« %s » collée dans l'Inspecteur": "“%s” pasted into the Inspector",
"COULEUR DU DOSSIER": "FOLDER COLOR",
"Couleur par défaut": "Default color",
"Afficher dans le dock Fichiers": "Show in FileSystem dock",
"Afficher dans l'explorateur": "Show in file manager",
"Copier le chemin absolu": "Copy absolute path",
"Chemin absolu copié": "Absolute path copied",
"Copier la ressource (bouton Coller au survol)": "Copy resource (Paste button on hover)",
"Copier l'UID": "Copy UID",
"UID copié": "UID copied",
"Ouvrir le dossier dans l'explorateur": "Open folder in file manager",
"Copier le chemin du dossier": "Copy folder path",
"Copier le chemin absolu du dossier": "Copy folder absolute path",
"Retirer le dossier des favoris": "Remove folder from favorites",
"Ajouter le dossier aux favoris": "Add folder to favorites",
"Actualiser": "Refresh",
"Actualisé": "Refreshed",
"Passer en vue grille": "Switch to grid view",
"Passer en vue liste": "Switch to list view",
"Nouvelle scène": "New scene",
"Nouveau script": "New script",
"Renommer (F2)": "Rename (F2)",
"Dupliquer (Ctrl+D)": "Duplicate (Ctrl+D)",
"Ajouter à un set...": "Add to a set...",
"Ajouter au set": "Add to set",
"Ajouté au set « %s »": "Added to set “%s”",
"Retirer de ce set": "Remove from this set",
"Réimporter": "Reimport",
"Supprimer (Suppr)": "Delete (Del)",
"Nom invalide : caractère « %s » interdit": "Invalid name: character “%s” not allowed",
"nouveau_dossier": "new_folder",
"« %s » existe déjà": "“%s” already exists",
"nouvelle_scene": "new_scene",
"nouveau_script": "new_script",
"Renommer": "Rename",
"Échec du renommage": "Rename failed",
"Renommé en « %s »": "Renamed to “%s”",
"_copie": "_copy",
"%d élément(s) dupliqué(s)": "%d item(s) duplicated",
"Élément dupliqué": "Item duplicated",
"Envoyer à la corbeille :\n": "Move to trash:\n",
"Supprimer": "Delete",
"Impossible de mettre « %s » à la corbeille": "Can't move “%s” to trash",
"%d élément(s) envoyé(s) à la corbeille": "%d item(s) moved to trash",
"Envoyé à la corbeille": "Moved to trash",
"Valider": "OK",
"Annuler": "Cancel",
"Confirmer": "Confirm",
" · %d sélectionné%s": " · %d selected%s",
"Asset Drawer (Ctrl+Espace)": "Asset Drawer (Ctrl+Space)",
"Projet": "Project",
"Rechercher dans tout le projet... (Ctrl+F)": "Search the whole project... (Ctrl+F)",
"Rechercher dans ce dossier... (Ctrl+F)": "Search this folder... (Ctrl+F)",
"Recherche : tout le projet (cliquer pour limiter au dossier courant)": "Search: whole project (click to limit to the current folder)",
"Recherche : dossier courant (cliquer pour chercher dans tout le projet)": "Search: current folder (click to search the whole project)",
"Masqué dans le tiroir": "Hidden in the drawer",
"Masqué dans le tiroir · clic droit → « Afficher les éléments masqués »": "Hidden in the drawer · right-click → “Show hidden items”",
"Réaffiché": "Unhidden",
"Afficher les éléments masqués": "Show hidden items",
"Masquer dans le tiroir": "Hide in the drawer",
"Ne plus masquer": "Unhide",
"Afficher dans son dossier": "Show in its folder",
"Impossible de créer « %s »": "Can't create “%s”",
}
static var _fr := -1
static func is_fr() -> bool:
if _fr < 0:
_fr = 1 if _detect_fr() else 0
return _fr == 1
static func t(fr: String) -> String:
if is_fr():
return fr
return str(EN.get(fr, fr))
## Marque du pluriel "s" (français uniquement) pour les mots comme "sélectionné(s)".
static func sel_s(plural: bool) -> String:
return "s" if plural and is_fr() else ""
static func _detect_fr() -> bool:
var loc := ""
if TranslationServer.has_method("get_tool_locale"):
loc = str(TranslationServer.call("get_tool_locale"))
else:
var lang := str(EditorInterface.get_editor_settings().get_setting("interface/editor/editor_language"))
loc = OS.get_locale() if lang == "" or lang == "auto" else lang
return loc.to_lower().begins_with("fr")
-1
View File
@@ -1 +0,0 @@
uid://0e4w5ydeh6np
-7
View File
@@ -1,7 +0,0 @@
[plugin]
name="Asset Drawer"
description="Tiroir d'assets façon Content Drawer d'Unreal Engine. Ouvrir avec Ctrl+Espace."
author="Asset Drawer"
version="3.7"
script="plugin.gd"
-128
View File
@@ -1,128 +0,0 @@
@tool
extends EditorPlugin
const Drawer := preload("res://addons/asset_drawer/drawer.gd")
const L := preload("res://addons/asset_drawer/lang.gd")
# true : Ctrl+Space opens the drawer even when the script editor has focus
# (Ctrl+Space autocompletion is then replaced by the drawer).
# false : in the script editor, Ctrl+Space stays autocompletion.
const OPEN_IN_CODE_EDITOR := true
# true : the open shortcut is ignored while the camera is in free look
# (right mouse button held in the 3D view, or free look toggled with Shift+F).
# This avoids opening the drawer when Ctrl (move down) + Space (move up)
# are pressed together to move around.
const BLOCK_IN_FREELOOK := true
var drawer: Control
var _toolbar_btn: Button
var _theme_check_queued := false
func _enter_tree() -> void:
_create_drawer()
_toolbar_btn = Button.new()
_toolbar_btn.flat = true
_toolbar_btn.tooltip_text = L.t("Asset Drawer (Ctrl+Espace)")
_toolbar_btn.focus_mode = Control.FOCUS_NONE
var base := EditorInterface.get_base_control()
var icon_name := "FolderBrowse" if base.has_theme_icon("FolderBrowse", "EditorIcons") else "Folder"
_toolbar_btn.icon = base.get_theme_icon(icon_name, "EditorIcons")
_toolbar_btn.pressed.connect(func() -> void:
if is_instance_valid(drawer):
drawer.toggle())
add_control_to_container(CONTAINER_TOOLBAR, _toolbar_btn)
add_tool_menu_item("Asset Drawer", Callable(self, "_on_menu_toggle"))
set_process_input(true)
# The drawer follows the editor theme (base color, contrast, accent).
var es := EditorInterface.get_editor_settings()
if not es.settings_changed.is_connected(_on_settings_changed):
es.settings_changed.connect(_on_settings_changed)
func _create_drawer() -> void:
drawer = Drawer.new()
drawer.name = "AssetDrawer"
EditorInterface.get_base_control().add_child(drawer)
func _on_settings_changed() -> void:
# Deferred: we first let the editor regenerate its theme before re-reading the colors.
if _theme_check_queued:
return
_theme_check_queued = true
_check_theme.call_deferred()
func _check_theme() -> void:
_theme_check_queued = false
if not is_instance_valid(drawer) or not drawer.palette_changed():
return
# Recreates the drawer with the new palette. Its state (folder, height, favorites...)
# is already in the config, and is read back by the new drawer.
var was_open: bool = drawer.is_open
var old := drawer
drawer = null
old.get_parent().remove_child(old) # triggers _exit_tree: save + cleanup
old.queue_free()
await get_tree().process_frame # lets the old pill / overlay go away (same names)
if not is_inside_tree():
return
_create_drawer()
if was_open:
drawer.open.call_deferred()
func _exit_tree() -> void:
var es := EditorInterface.get_editor_settings()
if is_instance_valid(es) and es.settings_changed.is_connected(_on_settings_changed):
es.settings_changed.disconnect(_on_settings_changed)
remove_tool_menu_item("Asset Drawer")
if is_instance_valid(_toolbar_btn):
remove_control_from_container(CONTAINER_TOOLBAR, _toolbar_btn)
_toolbar_btn.queue_free()
_toolbar_btn = null
if is_instance_valid(drawer):
drawer.queue_free()
drawer = null
func _on_menu_toggle() -> void:
if is_instance_valid(drawer):
drawer.toggle()
func _is_freelook_active() -> bool:
# During free look, Godot captures the mouse (right button held OR free look
# toggled with Shift+F). The right-click test acts as a safety net.
return Input.mouse_mode == Input.MOUSE_MODE_CAPTURED \
or Input.is_mouse_button_pressed(MOUSE_BUTTON_RIGHT)
func _input(event: InputEvent) -> void:
if not is_instance_valid(drawer):
return
if not (event is InputEventKey and event.pressed and not event.echo):
return
var key := event as InputEventKey
if key.keycode == KEY_SPACE and key.ctrl_pressed and not key.alt_pressed and not key.shift_pressed and not key.meta_pressed:
# Free look active: we let the key through to the editor (camera movement).
if BLOCK_IN_FREELOOK and _is_freelook_active():
return
# Ctrl+Space is also used for autocompletion: see OPEN_IN_CODE_EDITOR at the top of the file.
if not OPEN_IN_CODE_EDITOR:
var focus := EditorInterface.get_base_control().get_viewport().gui_get_focus_owner()
if focus is TextEdit and not drawer.is_ancestor_of(focus):
return
drawer.toggle()
get_viewport().set_input_as_handled()
elif key.keycode == KEY_ESCAPE and drawer.is_open and not drawer.pinned:
drawer.close()
get_viewport().set_input_as_handled()
elif key.keycode == KEY_V and key.alt_pressed and not key.ctrl_pressed and not key.shift_pressed and not key.meta_pressed:
# Pastes the copied resource from the drawer onto the Inspector resource field hovered by the mouse.
if drawer.try_paste_resource_at_mouse():
get_viewport().set_input_as_handled()
-1
View File
@@ -1 +0,0 @@
uid://bbthaayqxoorj
-21
View File
@@ -1,21 +0,0 @@
# Godot 4+ specific ignores
.godot/
.nomedia
# Godot-specific ignores
.import/
export.cfg
export_credentials.cfg
*.tmp
# Imported translations (automatically generated from CSV files)
*.translation
# Mono-specific ignores
.mono/
data_*/
mono_crash.*.json
# Planning / Roadmap docs
ROADMAP*.md
-175
View File
@@ -1,175 +0,0 @@
# CSG_Blockout Architecture Design & Technical Internals
*Read this in other languages: [简体中文](ARCHITECTURE_CN.md) | [Back to Main Docs](README.md)*
This document is intended for developers interested in the underlying implementation, algorithmic designs, and extensibility of `CSG_Blockout`. It provides technical specifications and architectural breakdowns.
---
## Table of Contents
- [1. Core Algorithm: 3D Spatial Hash Grid](#1-core-algorithm-3d-spatial-hash-grid)
- [2. System Architecture & Design Patterns](#2-system-architecture--design-patterns)
- [3. Procedural World-Aligned Triplanar Shader](#3-procedural-world-aligned-triplanar-shader)
- [4. GDScript 2.0 Typing & Defensive Architecture](#4-gdscript-20-typing--defensive-architecture)
- [5. Node Specifications & API Reference](#5-node-specifications--api-reference)
- [6. ProjectSettings Specification](#6-projectsettings-specification)
---
## 1. Core Algorithm: 3D Spatial Hash Grid
### 1. The O(N^2) Bottleneck of Naive Checking
In scatter placement calculations (`CSGSpreader3D`), naive algorithms prevent instance overlapping by iterating through all previously placed points for each candidate point:
$$\text{Complexity} = \mathcal{O}(N^2)$$
When populating hundreds of complex CSG geometric instances in real-time, performing hundreds of thousands of 3D distance checks per second causes significant frame drops and editor freezes.
### 2. Spatial Hash Grid Implementation
`CSG_Blockout` implements the 3D spatial hash grid directly inside `CSGSpreader3D`, using a `Dictionary` keyed by `Vector3i` cell coordinates:
1. **Cell Size Determination**:
Given the user-defined minimum distance $d_{\min}$ (`min_distance`), the cubic grid cell size is set to:
$$\text{Cell Size} = d_{\min}$$
Any two points within $d_{\min}$ of each other can never differ by more than $d_{\min}$ along a single axis, so they always land in cells that are at most one apart in each dimension. This guarantees that the 27-cell neighborhood below covers every possible collision candidate.
2. **Spatial Quantization & Hash Mapping**:
Any continuous 3D world coordinate $\mathbf{P}(x, y, z)$ is discretized into integer cell indices:
$$\mathbf{cell\_coords} = \left( \lfloor x / \text{CellSize} \rfloor, \lfloor y / \text{CellSize} \rfloor, \lfloor z / \text{CellSize} \rfloor \right)$$
Stored using `Vector3i` keys inside a hash table for constant-time lookup.
3. **27-Neighborhood Search (3x3x3 Locality)**:
When validating a placement candidate, the algorithm queries only the host cell and its immediate 26 adjacent cells (27 cells total), reducing collision verification to:
$$\text{Collision Check Complexity} = \mathcal{O}(1)$$
---
## 2. System Architecture & Design Patterns
```mermaid
graph TD
subgraph Editor Core
Plugin["csg_blockout.gd (EditorPlugin)"]
URM["EditorUndoRedoManager"]
I18N["CsgBlockoutI18n"]
ProjSet["ProjectSettings (addons/csg_blockout/*)"]
end
subgraph UI & Controls
Pie["CSGBlockoutPieMenu (Vector 2D/3D Viewport)"]
SideBar["CSGSideBlockoutBar (Left Viewport Bar)"]
TopBar["CSGTopBlockoutBar (Viewport Header Bar)"]
end
subgraph Node Domain
Combiner["CSGCombiner3D (Godot Built-in)"]
Repeater["CSGRepeater3D"] --> Combiner
Spreader["CSGSpreader3D"] --> Combiner
end
subgraph Strategy Pattern
Pattern["CSGPattern (Resource)"]
GridPat["CSGGridPattern"] --> Pattern
CircPat["CSGCircularPattern"] --> Pattern
SpiralPat["CSGSpiralPattern"] --> Pattern
NoisePat["CSGNoisePattern"] --> Pattern
Repeater -.-> Pattern
end
subgraph Collision & Placement
HashGrid["Inline Spatial Hash Grid (O(1) lookup)"]
Spreader -.-> HashGrid
Spreader -.-> ShapeDomain["Shape3D (Box/Sphere/Mesh/...)"]
end
Plugin --> Pie
Plugin --> SideBar
Plugin --> TopBar
```
### 1. Strategy Pattern: Array Generator (`CSGRepeater3D`)
`CSGRepeater3D` delegates spatial array calculations to modular `CSGPattern` resources:
- **`CSGGridPattern`**: Orthogonal 3D Cartesian array with automatic bounding box (AABB) compensation.
- **`CSGCircularPattern`**: Multi-tiered radial polar array.
- **`CSGSpiralPattern`**: Archimedean/logarithmic spiral distribution with optional non-linear `Curve` radial modulation.
- **`CSGNoisePattern`**: Volumetric 3D noise threshold sampling via `FastNoiseLite`.
### 2. Shape3D Bounding Domain: Scatterer (`CSGSpreader3D`)
`CSGSpreader3D` accepts any Godot `Shape3D` as a spatial boundary:
- Computes world-space bounding boxes.
- Executes rejection sampling inside custom shapes.
- Enforces strict minimum distance thresholds via its built-in spatial hash grid.
- Implements fallback safety limits (`max_placement_attempts`).
---
## 3. Procedural World-Aligned Triplanar Shader
`grid_triplanar.gdshader` is engineered specifically for level prototyping:
1. **World-Space Triplanar Mapping**:
- Bypasses mesh UV coordinates; samples texture procedurally from world coordinates $\mathbf{P}_{\text{world}}$.
- Guarantees 1m x 1m grid alignment regardless of node transformation or boolean cutting.
2. **Hardware Screen-Derivative Anti-Aliasing**:
- Employs `fwidth()` and `smoothstep()` for sub-pixel anti-aliased grid lines, eliminating distant moiré patterns.
3. **Zero Texture Footprint**:
- Procedural math generation with zero VRAM bandwidth overhead.
---
## 4. GDScript 2.0 Typing & Defensive Architecture
- **Strict Static Typing**: Explicit typing on all methods and variables, optimizing engine dispatch.
- **ClassDB Validation**: Dynamic node instantiation validated via `ClassDB.instantiate()` and type checks.
- **Atomic Undo/Redo**: Full integration with `EditorUndoRedoManager` for node creation, property editing, material assignments, and instance baking (`CSGRepeater3D` / `CSGSpreader3D`).
- **Editor / Runtime Decoupling**: Tool scripts strictly gated with `Engine.is_editor_hint()` to prevent runtime leaks and scene corruption.
---
## 5. Node Specifications & API Reference
### 1. CSGRepeater3D Properties
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `template_node` | `Node3D` | `null` | Node in scene tree used as instance template. |
| `template_node_scene` | `PackedScene` | `null` | Template PackedScene resource (fallback if `template_node` is null). |
| `hide_template` | `bool` | `true` | Automatically hides template node when array is generated. |
| `pattern` | `CSGPattern` | `null` | Pattern resource defining distribution layout. |
| `position_jitter` | `float` | `0.0` | Minor random translation variance. |
| `random_seed` | `int` | `0` | Seed for pseudorandom generator. |
| `estimated_instances` | `int` | `0` | (Read-only) Estimated number of instances to be generated. |
| `randomize_rotation` | `bool` | `false` | Enables rotation variance. |
| `randomize_rot_x/y/z` | `bool` | `false` | Per-axis rotation toggles. |
| `rotation_variance_x/y/z_deg` | `float` | `0.0` | Random variance angle in degrees (0 = full 360-degree). |
| `randomize_scale` | `bool` | `false` | Enables scale variance. |
| `scale_variance` | `float` | `0.0` | Uniform scale variance factor. |
### 2. CSGSpreader3D Properties
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `template_node` | `Node3D` | `null` | Target template node to scatter. |
| `spread_area_3d` | `Shape3D` | `null` | Spatial boundary shape (Box, Sphere, Capsule, Mesh, etc.). |
| `max_count` | `int` | `10` | Maximum instance limit (hard capped at 200). |
| `noise_threshold` | `float` | `0.5` | Noise density threshold (0.0 to 1.0). |
| `seed` | `int` | `0` | Random seed. |
| `avoid_overlaps` | `bool` | `false` | Enables Spatial Hash collision prevention. |
| `min_distance` | `float` | `1.0` | Minimum safe distance between origins. |
| `max_placement_attempts` | `int` | `100` | Maximum candidate search attempts per instance. |
| `allow_rotation` | `bool` | `false` | Enables random Y-axis yaw rotation. |
| `allow_scale` | `bool` | `false` | Enables random scale variance (0.5x to 2.0x). |
---
## 6. ProjectSettings Specification
Configuration options are registered under `addons/csg_blockout/*`:
| Setting Path | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `addons/csg_blockout/action_key` | `int` (Key) | `KEY_SHIFT` | Primary action modifier key for 3D Pie Menu. |
| `addons/csg_blockout/auto_hide` | `bool` | `true` | Auto-hides left sidebar when no CSG node is selected. |
| `addons/csg_blockout/language_override` | `String` | `"auto"` | Language preference override (`"auto"`, `"en"`, `"zh_CN"`, `"ja"`, `"ko"`, `"es"`, `"pt"`, `"ru"`). |
| `addons/csg_blockout/material_preset` | `int` (Enum) | `1` (GRID_LIGHT) | Default active grid material preset. |
| `addons/csg_blockout/default_operation` | `int` (Enum) | `0` (Union) | Default CSG boolean operation for newly created nodes (shared by pie menu & sidebar). |
| `addons/csg_blockout/custom_material_path` | `String` | `""` | Resource path of the custom material used by the CUSTOM preset (persisted across editor sessions). |
-175
View File
@@ -1,175 +0,0 @@
# CSG_Blockout 架构设计与技术内幕
*其他语言版本: [English](ARCHITECTURE.md) | [返回主文档](README_CN.md)*
本文档面向对插件底层实现、算法细节以及二次开发感兴趣的开发者,详细阐述 `CSG_Blockout` 的核心算法设计、架构分层与完整 API 规范。
---
## 目录
- [一、 核心算法:3D 空间哈希网格 (Spatial Hash Grid)](#一-核心算法3d-空间哈希网格-spatial-hash-grid)
- [二、 系统架构与设计模式](#二-系统架构与设计模式)
- [三、 程序化世界对齐材质 Shader 设计](#三-程序化世界对齐材质-shader-设计)
- [四、 GDScript 2.0 规范与防御性设计](#四-gdscript-20-规范与防御性设计)
- [五、 节点与组件规格参考 (API Reference)](#五-节点与组件规格参考-api-reference)
- [六、 ProjectSettings 配置规范](#六-projectsettings-配置规范)
---
## 一、 核心算法:3D 空间哈希网格 (Spatial Hash Grid)
### 1. 传统暴力检测的性能瓶颈
在散布计算(`CSGSpreader3D`)中,为防止实例在 3D 体积内互相重叠穿模,朴素方案是对每一个候选位置与已有全部实例进行欧氏距离双重循环检测:
$$\text{复杂度} = \mathcal{O}(N^2)$$
当生成数百个高精度 CSG 几何体时,每秒数十万次的三维距离向量平方运算会导致 Godot 主线程严重卡顿,甚至触发编辑态未响应。
### 2. 空间哈希网格实现原理
`CSG_Blockout` 将 3D 空间哈希网格直接内建于 `CSGSpreader3D` 中,使用以 `Vector3i` 为键的 `Dictionary` 存储:
1. **单元格尺度计算**:
给定用户设定的最小防重叠间距 $d_{\min}$(`min_distance`),网格单元尺寸定义为:
$$\text{Cell Size} = d_{\min}$$
任意两个间距小于 $d_{\min}$ 的点,其在单一坐标轴上的差值不可能超过 $d_{\min}$,因此二者所在单元格在每个维度上至多相差 1。这保证了下述 27 邻域检索必定覆盖全部潜在碰撞候选。
2. **空间坐标量化与哈希映射**:
将任意浮点空间坐标 $\mathbf{P}(x, y, z)$ 离散化为整型网格坐标:
$$\mathbf{cell\_coords} = \left( \lfloor x / \text{CellSize} \rfloor, \lfloor y / \text{CellSize} \rfloor, \lfloor z / \text{CellSize} \rfloor \right)$$
使用基于 `Vector3i` 的字典键做常量时间寻址。
3. **27 邻域检索 (3x3x3 局部性查询)**:
每次验证候选位置时,算法无需遍历全量数据,仅需检索当前单元格以及紧邻的 26 个邻近单元格(共 27 块),将冲突检测的时间复杂度降为:
$$\text{候选冲突检测复杂度} = \mathcal{O}(1)$$
---
## 二、 系统架构与设计模式
```mermaid
graph TD
subgraph Editor Core
Plugin["csg_blockout.gd (EditorPlugin)"]
URM["EditorUndoRedoManager"]
I18N["CsgBlockoutI18n"]
ProjSet["ProjectSettings (addons/csg_blockout/*)"]
end
subgraph UI & Controls
Pie["CSGBlockoutPieMenu (Vector 2D/3D Viewport)"]
SideBar["CSGSideBlockoutBar (Left Viewport Bar)"]
TopBar["CSGTopBlockoutBar (Viewport Header Bar)"]
end
subgraph Node Domain
Combiner["CSGCombiner3D (Godot Built-in)"]
Repeater["CSGRepeater3D"] --> Combiner
Spreader["CSGSpreader3D"] --> Combiner
end
subgraph Strategy Pattern
Pattern["CSGPattern (Resource)"]
GridPat["CSGGridPattern"] --> Pattern
CircPat["CSGCircularPattern"] --> Pattern
SpiralPat["CSGSpiralPattern"] --> Pattern
NoisePat["CSGNoisePattern"] --> Pattern
Repeater -.-> Pattern
end
subgraph Collision & Placement
HashGrid["内建空间哈希网格 (O(1) 查询)"]
Spreader -.-> HashGrid
Spreader -.-> ShapeDomain["Shape3D (Box/Sphere/Mesh/...)"]
end
Plugin --> Pie
Plugin --> SideBar
Plugin --> TopBar
```
### 1. 策略模式:阵列分发器 (`CSGRepeater3D`)
`CSGRepeater3D` 摆脱了传统的硬编码循环,将生成逻辑完全抽象为独立的 `CSGPattern` 资源策略:
- **`CSGGridPattern`**:正交笛卡尔网格阵列,支持 AABB 边界自适应计算。
- **`CSGCircularPattern`**:多层同心极坐标环形阵列。
- **`CSGSpiralPattern`**:阿基米德/对数螺旋分布,支持自定义非线性半径曲线(`Curve`)。
- **`CSGNoisePattern`**:基于 `FastNoiseLite` 的体域噪声阈值采样分布。
### 2. 物理碰撞体边界域:散布系统 (`CSGSpreader3D`)
`CSGSpreader3D` 支持任意 Godot `Shape3D` 作为散布边界,其工作流包括:
- 提取碰撞体的世界空间 AABB 边界。
- 执行内部包含性与随机拒识采样。
- 联动内建空间哈希网格进行实时防重叠过滤。
- 支持最大容错尝试机制(`max_placement_attempts`)。
---
## 三、 程序化世界对齐材质 Shader 设计
`grid_triplanar.gdshader` 是专为白盒原型搭建定制的着色器,核心技术特征包括:
1. **世界坐标三平面投影(World-Space Triplanar Mapping)**:
- 弃用模型 UV 坐标,直接取顶点/片元的世界坐标 $\mathbf{P}_{\text{world}}$ 计算纹理采样。
- 物体在三维空间中随意平移、旋转、缩放或执行布尔切削,网格材质的物理尺寸(如 1m x 1m)永不拉伸扭曲。
2. **硬件级解析度自适应抗锯齿**:
- 利用导数函数 `fwidth()` 与 `smoothstep()` 计算网格边缘线,从根本上杜绝远处网格摩尔纹走样。
3. **性能开销最小化**:
- 无贴图依赖,纯数学公式生成网格与棋盘格,零显存带宽浪费。
---
## 四、 GDScript 2.0 规范与防御性设计
- **全线静态强类型化**:所有脚本启用强类型签名,消除动态查找开销。
- **ClassDB 安全校验**:所有动态实例化节点均经由 `ClassDB.instantiate()` 或类型断言,杜绝无效类抛错。
- **原子级 Undo/Redo**:全面对接 Godot 4.7 `EditorUndoRedoManager`,涵盖节点增删、属性变更、材质覆盖与实例烘焙(`CSGRepeater3D` / `CSGSpreader3D`)。
- **编辑器与运行时完全解耦**:所有工具类逻辑严格置于 `Engine.is_editor_hint()` 防护内,杜绝运行时产生幽灵节点或内存泄漏。
---
## 五、 节点与组件规格参考 (API Reference)
### 1. CSGRepeater3D 属性参考
| 属性名 | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- |
| `template_node` | `Node3D` | `null` | 引用场景树中作为阵列原型的节点。 |
| `template_node_scene` | `PackedScene` | `null` | 预制体模板资源(当未指定 `template_node` 时生效)。 |
| `hide_template` | `bool` | `true` | 生成阵列时自动隐藏原始模板节点。 |
| `pattern` | `CSGPattern` | `null` | 阵列分发策略资源。 |
| `position_jitter` | `float` | `0.0` | 位置微小随机抖动偏移量。 |
| `random_seed` | `int` | `0` | 伪随机数种子。 |
| `estimated_instances` | `int` | `0` | (只读) 预估生成的实例数量。 |
| `randomize_rotation` | `bool` | `false` | 启用旋转随机化。 |
| `randomize_rot_x/y/z` | `bool` | `false` | 各轴向旋转随机开关。 |
| `rotation_variance_x/y/z_deg` | `float` | `0.0` | 旋转随机浮动角度(设为 0 为 360 度全随机)。 |
| `randomize_scale` | `bool` | `false` | 启用缩放随机化。 |
| `scale_variance` | `float` | `0.0` | 等比缩放浮动量。 |
### 2. CSGSpreader3D 属性参考
| 属性名 | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- |
| `template_node` | `Node3D` | `null` | 需要散布的目标模板节点。 |
| `spread_area_3d` | `Shape3D` | `null` | 散布空间域形状(Box, Sphere, Capsule 等)。 |
| `max_count` | `int` | `10` | 散布最大实例上限(硬上限保护为 200)。 |
| `noise_threshold` | `float` | `0.5` | 噪声概率过滤阈值(0.0 至 1.0)。 |
| `seed` | `int` | `0` | 随机种子。 |
| `avoid_overlaps` | `bool` | `false` | 启用空间哈希碰撞避障检查。 |
| `min_distance` | `float` | `1.0` | 实例间最小安全间距。 |
| `max_placement_attempts` | `int` | `100` | 单个实例最大寻位采样尝试次数。 |
| `allow_rotation` | `bool` | `false` | 允许随机 Y 轴偏航旋转。 |
| `allow_scale` | `bool` | `false` | 允许随机缩放(0.5x 至 2.0x)。 |
---
## 六、 ProjectSettings 配置规范
插件所有全局配置项挂载于 Godot 项目设置 `addons/csg_blockout/*` 命名空间下:
| 配置键路径 | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- |
| `addons/csg_blockout/action_key` | `int` (Key) | `KEY_SHIFT` | 3D 轮盘菜单主触发修饰键。 |
| `addons/csg_blockout/auto_hide` | `bool` | `true` | 视口未选中 CSG 节点时自动隐藏左侧边栏。 |
| `addons/csg_blockout/language_override` | `String` | `"auto"` | 界面语言偏好覆盖 (`"auto"`、`"en"`、`"zh_CN"`、`"ja"`、`"ko"`、`"es"`、`"pt"`、`"ru"`)。 |
| `addons/csg_blockout/material_preset` | `int` (Enum) | `1` (GRID_LIGHT) | 默认激活的网格材质预设通道。 |
| `addons/csg_blockout/default_operation` | `int` (Enum) | `0` (并集) | 新建节点的默认 CSG 布尔运算(轮盘菜单与侧栏共享)。 |
| `addons/csg_blockout/custom_material_path` | `String` | `""` | CUSTOM 预设所引用的自定义材质资源路径(跨会话持久化)。 |
Binary file not shown.

Before

Width:  |  Height:  |  Size: 393 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 142 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.8 MiB

-110
View File
@@ -1,110 +0,0 @@
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="res/icon_transparent.svg" />
<img src="res/icon_transparent_light.svg" alt="CSG_Blockout Logo" width="180" />
</picture>
<h1>CSG_Blockout</h1>
<p>
<a href="README.md"><img src="https://img.shields.io/badge/Docs-English-blue?style=flat-square" alt="Docs English" /></a>
<a href="README_CN.md"><img src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-blue?style=flat-square" alt="文档 简体中文" /></a>
<a href="TUTORIAL_EN.md"><img src="https://img.shields.io/badge/Tutorial-English-orange?style=flat-square" alt="Tutorial English" /></a>
<a href="ARCHITECTURE.md"><img src="https://img.shields.io/badge/Architecture-Internals-purple?style=flat-square" alt="Architecture Internals" /></a>
</p>
<p>
<a href="https://godotengine.org"><img src="https://img.shields.io/badge/Godot-4.7%2B-478cbf?style=flat-square&logo=godotengine&logoColor=white" alt="Godot Engine" /></a>
<a href="https://store.godotengine.org/asset/qwqzhanqwq/csg-blockout/"><img src="https://img.shields.io/badge/AssetLib-CSG__Blockout-blueviolet?style=flat-square" alt="Godot AssetLib" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-success?style=flat-square" alt="License: MIT" /></a>
</p>
<p>
<strong>A high-performance, modernized 3D level blockout and rapid prototyping plugin designed specifically for Godot 4.7.</strong>
</p>
</div>
---
## Installation
### Option 1: Godot AssetLib (Recommended)
1. Open Godot 4.7 and navigate to the **AssetLib** tab at the top of the editor.
2. Search for `CSG Blockout`, download, and install it into your project.
3. Or view it directly on the web: [Godot AssetLib - CSG_Blockout](https://store.godotengine.org/asset/qwqzhanqwq/csg-blockout/).
### Option 2: GitHub Releases (.zip)
1. Download the latest release `.zip` from the [Releases page](https://github.com/qwqzhanqwq/Godot-CSG-Blockout/releases).
2. Extract the archive and copy the `addons/csg_blockout` directory into your Godot project's `addons/` folder.
### Option 3: Git Clone
Clone the repository directly into your project's `addons/` directory:
```bash
git clone https://github.com/qwqzhanqwq/Godot-CSG-Blockout.git addons/csg_blockout
```
### Enable the Plugin
In Godot, go to **Project -> Project Settings -> Plugins**, find **CSG_Blockout**, and check the **Enable** checkbox.
---
## Quick Start
1. **Invoke 3D Pie Menu**: Press `Shift + A` in the 3D viewport to open the vector radial menu directly at your cursor. Quickly create primitives or switch CSG operations (Union / Intersection / Subtraction) using mouse gestures.
2. **Use the Viewport Sidebar**: The left sidebar provides one-click creation for 6 standard shapes. It automatically adds nodes as children when selecting a `CSGCombiner3D`, or as adjacent siblings when selecting standard shapes.
3. **Procedural World-Aligned Grid**: Switch between Light Grid, Dark Grid, or Orange Accent presets. Select multiple CSG nodes in the tree or viewport and click **"Apply Material to Selected"** to batch-assign without texture distortion.
4. **Bake to Permanent Nodes**: When a `CSGRepeater3D` or `CSGSpreader3D` is selected, click **Bake** in the 3D header toolbar to detach generated instances into permanent scene nodes.
> For a complete visual walkthrough and performance optimization workflow, check the [Quick Start & Advanced Workflow Tutorial (TUTORIAL_EN.md)](TUTORIAL_EN.md).
---
## Key Features
- **Vector 3D Pie Menu**: Triggered with `Shift + A` in the 3D viewport. Hierarchical radial navigation for instant CSG operations and primitive placement with central deadzone controls and full Undo/Redo integration.
- **Smart Viewport Sidebar**: Quick-access toolbar for 6 primitive shapes and boolean modes. Auto-hides when no CSG nodes are selected to keep the viewport clean.
- **Procedural World-Aligned Triplanar Shader**: World-space UV projection guarantees consistent 1m x 1m grid scaling without stretching during node transformations or CSG boolean cuts. Built-in screen-derivative anti-aliasing.
- **Ready-to-Use Material Presets & Batch Assignment**: Pre-configured measurement materials (Light, Dark, Accent, Unshaded) with one-click batch application to all selected CSG nodes.
- **Parametric Array Generator (CSGRepeater3D)**: Strategy-pattern array distributions (Grid, Circular, Spiral, and 3D Noise volume sampling) with randomized rotation and scale.
- **Volume Collision Scatterer (CSGSpreader3D)**: Scatter instances within any Godot `Shape3D` volume without overlaps. Powered by a custom $\mathcal{O}(1)$ 3D Spatial Hash Grid algorithm.
- **GDScript 2.0 Static Typing & Multi-Language Support**: Fully typed codebase, atomic `EditorUndoRedoManager` support (`Ctrl + Z` / `Ctrl + Y`), and built-in 7-language (i18n) localization (English, Simplified Chinese, Japanese, Korean, Spanish, Portuguese, Russian).
---
## Relationship to CSG Toolkit & Architectural Evolution
This plugin originated from the excellent open-source [CSG Toolkit](https://godotengine.org/asset-library/asset/3057) created by **LuckyTepot**. While the original toolkit demonstrated the value of in-viewport CSG authoring, modern Godot 4.7 workflows demanded higher performance, procedural tooling, and cleaner architecture.
`CSG_Blockout` was re-engineered by [qwqzhanqwq](https://github.com/qwqzhanqwq) as a **ground-up architectural overhaul**:
| Dimension | Original CSG Toolkit | CSG_Blockout (This Project) |
| :--- | :--- | :--- |
| **Engine Core** | Early Godot 4.x, dynamically typed | **Tailored for Godot 4.7+**, full GDScript 2.0 static typing & ClassDB validation |
| **Interaction** | Viewport sidebar only | **3D Viewport Radial Pie Menu (`Shift + A`) + Smart Sidebar** dual workflow |
| **Procedural Tools**| Static manual primitive placement | **Parametric Repeaters (`CSGRepeater3D`) + Volume Spreaders (`CSGSpreader3D`)** |
| **Scatter Algorithm**| No collision avoidance or naive $\mathcal{O}(N^2)$ | **Custom 3D Spatial Hash Grid ($\mathcal{O}(1)$ lookup)**, real-time calculation |
| **Materials** | Basic default materials | **World-aligned triplanar anti-aliased shader** + 5 presets + 1-click batch assign |
| **Robustness** | Basic editor state | **Atomic `EditorUndoRedoManager` integration** with strict editor/runtime decoupling |
| **Localization** | English only | **Native multi-language support** (English, Simplified Chinese, Japanese, Korean, Spanish, Portuguese, Russian) |
---
## Documentation
### English
- [Main Documentation (README.md)](README.md): Plugin overview, installation, feature highlights, and credits.
- [Quick Start & Advanced Workflow Tutorial (TUTORIAL_EN.md)](TUTORIAL_EN.md): Step-by-step guide, visual demonstrations, and the critical CSG-to-Mesh baking workflow.
- [Architecture Design & Technical Internals (ARCHITECTURE.md)](ARCHITECTURE.md): Spatial Hash Grid algorithm derivation, design patterns, and complete API specifications.
### 简体中文 (Simplified Chinese)
- [中文主说明文档 (README_CN.md)](README_CN.md): 插件核心特性、安装指南、快速上手与演进历史。
- [快速上手与高级工作流教程 (TUTORIAL_CN.md)](TUTORIAL_CN.md): 完整图文与动图教学、白盒烘焙与性能优化方案。
- [架构设计与技术内幕 (ARCHITECTURE_CN.md)](ARCHITECTURE_CN.md): 3D 空间哈希数学推导、策略模式架构解析与全量 API 字典。
---
## Credits & License
- **Original Concept & Layout Design**: [LuckyTepot](https://github.com/LuckyTepot) (CSG Toolkit, Copyright (c) 2023).
- **Architecture Overhaul, 3D Pie Menu, Spatial Hash Grid, Array/Scatter Systems & GDScript 2.0 Rewrite**: [qwqzhanqwq](https://github.com/qwqzhanqwq) (Copyright (c) 2026).
Licensed under the [MIT License](LICENSE).
-110
View File
@@ -1,110 +0,0 @@
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="res/icon_transparent.svg" />
<img src="res/icon_transparent_light.svg" alt="CSG_Blockout Logo" width="180" />
</picture>
<h1>CSG_Blockout</h1>
<p>
<a href="README.md"><img src="https://img.shields.io/badge/Docs-English-blue?style=flat-square" alt="Docs English" /></a>
<a href="README_CN.md"><img src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-blue?style=flat-square" alt="文档 简体中文" /></a>
<a href="TUTORIAL_CN.md"><img src="https://img.shields.io/badge/%E6%95%99%E7%A8%8B-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-orange?style=flat-square" alt="教程 简体中文" /></a>
<a href="ARCHITECTURE_CN.md"><img src="https://img.shields.io/badge/%E6%9E%B6%E6%9E%84-%E6%8A%80%E6%9C%AF%E5%86%85%E5%B9%95-purple?style=flat-square" alt="架构 技术内幕" /></a>
</p>
<p>
<a href="https://godotengine.org"><img src="https://img.shields.io/badge/Godot-4.7%2B-478cbf?style=flat-square&logo=godotengine&logoColor=white" alt="Godot 引擎" /></a>
<a href="https://store.godotengine.org/asset/qwqzhanqwq/csg-blockout/"><img src="https://img.shields.io/badge/AssetLib-CSG__Blockout-blueviolet?style=flat-square" alt="Godot AssetLib" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-success?style=flat-square" alt="开源协议: MIT" /></a>
</p>
<p>
<strong>专为 Godot 4.7 打造的高性能、现代化的 3D 关卡白盒(Blockout)与快速原型设计插件。</strong>
</p>
</div>
---
## 安装指南
### 途径 1:Godot AssetLib 官方资产库(推荐)
1. 在 Godot 4.7 编辑器顶部点击 **AssetLib (资产库)** 选项卡。
2. 搜索 `CSG Blockout`,点击下载并安装到项目中。
3. 也可通过网页版资产库直达:[Godot AssetLib - CSG_Blockout](https://store.godotengine.org/asset/qwqzhanqwq/csg-blockout/)。
### 途径 2:GitHub Releases 发布包
1. 前往项目的 [Releases 页面](https://github.com/qwqzhanqwq/Godot-CSG-Blockout/releases) 下载最新的 `.zip` 归档文件。
2. 解压后将 `addons/csg_blockout` 文件夹复制到你的 Godot 项目根目录下的 `addons/` 目录中。
### 途径 3:Git 源码克隆
在你的 Godot 项目根目录下执行以下命令:
```bash
git clone https://github.com/qwqzhanqwq/Godot-CSG-Blockout.git addons/csg_blockout
```
### 启用插件
打开 Godot 编辑器,依次点击 **项目 (Project) -> 项目设置 (Project Settings) -> 插件 (Plugins)**,找到 **CSG_Blockout** 并勾选 **启用 (Enable)**。
---
## 快速上手
1. **呼出 3D 轮盘**:在 3D 视口中按下快捷键 `Shift + A`,在光标所在位置唤出矢量轮盘菜单(Pie Menu),顺着手势快速创建基础 CSG 形状或切换布尔操作(并集 / 交集 / 差集)。
2. **使用视口侧边栏**:视口左侧边栏提供立方体、球体、圆柱体等一键创建按钮;选中 `CSGCombiner3D` 时自动挂载为子节点,选中独立形状时自动作为同级兄弟节点插入。
3. **程序化世界对齐网格**:侧边栏内置灰白网格、深灰网格、橙色高亮网格等材质通道,支持多选节点后一键批量赋予,物体随意缩放旋转均不拉伸纹理。
4. **一键烘焙与导出**:选中 `CSGRepeater3D` 或 `CSGSpreader3D` 时,顶部工具栏会出现专属按钮,点击 **烘焙 (Bake)** 即可将其固化为常规场景节点。
> 完整图文搭建教程与性能优化最佳实践,请查阅 [快速上手与高级工作流教程 (TUTORIAL_CN.md)](TUTORIAL_CN.md)。
---
## 核心功能特性
- **矢量 3D 轮盘菜单 (3D Pie Menu)**:在 3D 视口通过 `Shift + A` 顺发呼出,多层级布尔运算与几何形状无缝联动,支持中心死区回退与全流程 Undo/Redo。
- **智能工作流侧边栏**:集成 6 种标准 CSG 几何体快捷创建、3 种布尔操作即时切换,无 CSG 节点选中时自动隐藏,保持视口清爽。
- **程序化三平面世界网格 Shader**:摒弃传统 UV 依赖,基于世界空间坐标投影,严格保持真实世界比例尺寸(1m x 1m),内置抗锯齿算法。
- **开箱即用材质预设与批量赋予**:预置浅色、深色、重点强调等度量材质,支持视口中框选多个 CSG 节点一键应用。
- **参数化阵列生成器 (CSGRepeater3D)**:采用策略模式设计,支持网格阵列、环形阵列、螺旋阵列与 3D 噪声体域采样,支持旋转与缩放随机化。
- **物理体积防重叠散布系统 (CSGSpreader3D)**:支持以任意 Godot `Shape3D` 为空间边界进行无重叠填充,采用 $O(1)$ 空间哈希网格算法,彻底避免编辑态卡死。
- **工程级强类型与多语言支持**:全线基于 GDScript 2.0 静态强类型开发,无缝对接 `EditorUndoRedoManager`,内置支持中、英、日、韩、西、葡、俄 7 种语言 (i18n) 界面即时热切换。
---
## 与 CSG Toolkit 的关系与全面演进
本项目灵感源自 **LuckyTepot** 开发的优秀开源插件 [CSG Toolkit](https://godotengine.org/asset-library/asset/3057)。原版插件验证了在 Godot 视口内集成快速 CSG 工具栏的卓越体验。
为了满足现代化高精度关卡设计与 Godot 4.7 架构标准,本项目由 [qwqzhanqwq](https://github.com/qwqzhanqwq) 对其进行了**彻底推倒式的架构重写与全方位扩展**:
| 维度 | 原版 CSG Toolkit | CSG_Blockout (本项目) |
| :--- | :--- | :--- |
| **引擎架构** | 面向早期 Godot 4.x,弱类型脚本 | **深度适配 Godot 4.7+**,全线 GDScript 2.0 强类型标注与 ClassDB 校验 |
| **核心交互** | 仅依赖视口固定侧边栏 | **3D 视口矢量轮盘 (Shift+A Pie Menu) + 智能侧边栏** 双工作流 |
| **程序化生成** | 仅支持基础静态图元放置 | **参数化阵列 (Repeater) + 碰撞体体积散布 (Spreader)** |
| **散布避障算法** | 无避障或朴素 $O(N^2)$ 遍历循环 | **自研 3D 空间哈希网格算法 ($O(1)$ 复杂度)**,数百实例实时生成不卡顿 |
| **材质系统** | 依赖基础内置着色器 | **程序化世界对齐三平面网格 Shader** + 5 组预设 + 一键批量赋予 |
| **工程健壮性** | 基础编辑态支持 | **全流程原子级 EditorUndoRedoManager 接入**,严格的编辑器/运行时环境隔离 |
| **国际化** | 仅支持单语言 | **原生多语言支持**(中、英、日、韩、西、葡、俄 7 语),自动跟随编辑器语言偏好并支持独立覆盖 |
---
## 文档导航
### 简体中文
- [主说明文档 (README_CN.md)](README_CN.md):插件核心特性、安装指南、快速上手与演进历史。
- [快速上手与高级工作流教程 (TUTORIAL_CN.md)](TUTORIAL_CN.md):手把手教学、动图演示与关键的白盒烘焙性能优化方案。
- [架构设计与技术内幕 (ARCHITECTURE_CN.md)](ARCHITECTURE_CN.md):3D 空间哈希数学推导、策略模式架构解析与全量 API 字典。
### English
- [Main Documentation (README.md)](README.md): Plugin overview, installation, feature highlights, and credits.
- [Quick Start & Advanced Workflow Tutorial (TUTORIAL_EN.md)](TUTORIAL_EN.md): Step-by-step guide, visual demonstrations, and the critical CSG-to-Mesh baking workflow.
- [Architecture Design & Technical Internals (ARCHITECTURE.md)](ARCHITECTURE.md): Spatial Hash Grid algorithm derivation, design patterns, and complete API specifications.
---
## 致谢与开源协议
- **原始概念与界面原型灵感**:[LuckyTepot](https://github.com/LuckyTepot) (CSG Toolkit, Copyright (c) 2023).
- **架构重构、3D 轮盘、空间哈希优化、阵列/散布系统与 GDScript 2.0 重写**:[qwqzhanqwq](https://github.com/qwqzhanqwq) (Copyright (c) 2026).
本项目基于 [MIT License](LICENSE) 开源发布。
-95
View File
@@ -1,95 +0,0 @@
# CSG Blockout 快速搭建与高级工作流教程
*其他语言版本: [English](TUTORIAL_EN.md) | [返回主文档](README_CN.md) | [架构与技术内幕](ARCHITECTURE_CN.md)*
## 目录
- [一、 基础篇:CSG 的基本概念](#一-基础篇csg-的基本概念)
- [二、 工作流篇:极速搭建方案](#二-工作流篇极速搭建方案)
- [三、 进阶篇:阵列与散布系统](#三-进阶篇阵列与散布系统)
- [四、 终极奥义:性能优化工作流 (必读)](#四-终极奥义性能优化工作流-必读)
---
## 一、 基础篇:CSG 的基本概念
**CSG (Constructive Solid Geometry,构造实体几何)** 是一种允许你通过对简单几何体(如立方体、球体、圆柱体)进行布尔运算,从而构造出复杂 3D 模型的技术。
在 Godot 中,CSG 是关卡设计师搭建“白盒(Blockout)”或进行快速原型设计的利器。其核心基于三种布尔操作:
- **并集 (Union)**:将两个几何体合并为一个整体。
- **交集 (Intersection)**:只保留两个几何体相互重叠的部分。
- **差集 (Subtraction)**:从一个几何体中“挖去”另一个几何体的体积。
![CSG 差集演示](DocsImages/Subtraction.webp)
利用这些简单的操作,你可以迅速在场景中切出门洞、挖出地道或拼凑出复杂的建筑结构,而无需使用 Blender 等外部建模软件。
---
## 二、 工作流篇:极速搭建方案
CSG_Blockout 插件旨在打破繁琐的层级操作,提供所见即所得的极速搭建体验。无论是偏好鼠标点击还是快捷键的开发者,都能找到最顺手的方式。
### 推荐搭建流程
1. **建立根节点**:在你的场景中添加一个 `CSGCombiner3D` 节点作为整个几何体的根容器。所有后续添加的基础形状都会自动合并在这个节点下。
2. **选中目标**:确保在场景树中选中这个 `CSGCombiner3D` 节点(或它内部的其他 CSG 形状)。
3. **呼出菜单**:
- **方式 A (轮盘菜单)**:将鼠标移至 3D 视口中,按下快捷键 `Shift + A`,即可在鼠标位置唤出 3D 轮盘菜单(Pie Menu)。顺着鼠标方向滑动即可快速选择布尔操作并生成基础形状。
![Pie Menu 演示](DocsImages/PieMenu.webp)
- **方式 B (侧边栏)**:直接使用 3D 视口左侧新增的插件侧边栏。首先点击下方图标选择布尔操作类型(并集、交集或差集),然后再点击上方的几何体图标(如立方体、球体等),即可一键添加带有该操作属性的 CSG 节点。
![Sidebar 演示](DocsImages/Sidebar.webp)
4. **网格材质与原型度量**:
- **程序化世界对齐网格**:侧边栏内置了三种开箱即用的世界对齐网格材质(灰白网格、深灰网格、橙色高亮网格),以及白模(无材质)与自定义材质通道。网格基于三平面投影(Triplanar),在物体旋转或缩放时不会发生纹理畸变或拉伸,非常适合在关卡前期度量角色尺度和空间距离。
- **一键批量赋材质**:在视口或场景树中多选任意 `CSGShape3D` 节点,点击侧边栏材质组底部的 **“应用材质到选中节点”** 按钮,即可将当前激活的预设材质批量赋给所有选中节点。全流程支持 `Ctrl + Z` 撤销/重做。
![材质预设与一键应用演示](DocsImages/MaterialPresets.webp)
5. **智能层级挂载**:
- 选中 `CSGCombiner3D`(或 `CSGRepeater3D` / `CSGSpreader3D`)时,新建节点将自动作为**子节点**挂载在其下方;
- 选中普通 `CSGShape3D`(如 `CSGBox3D`)时,新建节点将自动作为**同级平级节点**插入在当前选中节点之后,无需手动调整层级树。
6. **调整修改**:直接在视口中拖拽手柄调整大小和位置,由于工具全线支持 `Ctrl+Z`,你可以大胆地尝试任何修改。
---
## 三、 进阶篇:阵列与散布系统
### 1. 阵列系统 (CSGRepeater3D)
`CSGRepeater3D` 允许你通过特定的数学模式快速生成重复的几何体,非常适合制作楼梯、栅栏、柱列或环形阵列场景。
**核心操作步骤**:
1. **创建节点**:在场景中添加一个 `CSGRepeater3D` 节点。
2. **设置模板**:你可以直接将一个基础的 CSG 节点(如 `CSGBox3D`)作为 `CSGRepeater3D` 的子节点,或者在其属性面板中为 `Template Node` 分配目标节点。
3. **选择阵列模式**:在 Inspector (检查器) 中,找到 `Pattern` 属性,新建你需要的分发模式:
- `CSGGridPattern`:**网格阵列**,可自由调整 X/Y/Z 三轴的生成数量与间距。
- `CSGCircularPattern`:**环形阵列**,通过控制半径和数量,让物体围绕中心呈环形分布。
- `CSGSpiralPattern`:**螺旋阵列**,在环形的基础上增加高度偏移,是制作螺旋楼梯的绝佳工具。
- `CSGNoisePattern`:**噪声阵列**,基于 3D 噪声进行有序的随机偏移。
### 2. 散布系统 (CSGSpreader3D)
`CSGSpreader3D` 独家集成了**空间哈希网格(Spatial Hash Grid)** 算法,能够在任意 3D 碰撞体范围内高效、随机且**无重叠**地散布几何体。即使散布大量物体,O(1) 级别的避障检测也能确保编辑器丝滑不卡顿。
**核心操作步骤**:
1. **创建节点**:添加 `CSGSpreader3D` 节点并赋予它一个需要被散布的子节点模板。
2. **划定散布范围**:在属性面板中,指定一个 `Shape3D`(如 `BoxShape3D` 或 `SphereShape3D`),散布将严格限制在此碰撞体范围内。
3. **调整密度与随机性**:
- **Min Distance (最小安全距离)**:这是控制散布密度的最关键属性。设置越大分布越稀疏,设置越小分布越密集。
- 配合面板中的随机旋转(Random Rotation)和随机缩放(Random Scale),瞬间生成杂乱但自然的地形或废墟。
---
## 四、 终极奥义:性能优化工作流 (必读)
CSG 节点在编辑阶段非常方便,但由于其实时布尔运算的特性,如果在最终游戏运行时保留大量的 CSG 节点,将会造成**严重的性能损耗(掉帧)**。
当你的某一个房间或建筑区块已经“确定不改动”时,**必须**将其转换为静态网格和碰撞体。
**一键转换标准流程**:
1. **完成搭建**:选中包含你所有操作的那个顶级 `CSGCombiner3D` 节点。
2. **烘焙网格**:在编辑器顶部菜单栏点击 `CSGCombiner3D` 的专属选项,选择 **“烘焙为网格实例 (Bake to MeshInstance3D)”**(或直接创建/转换)。
3. **生成碰撞体**:选中刚刚生成的 `MeshInstance3D`,在顶部菜单点击“Mesh(网格)”,选择 **“创建三角网格碰撞体 (Create Trimesh Static Body)”**。此操作会自动为你生成极其贴合表面的碰撞体。
4. **清理现场**:将生成的 `MeshInstance3D` 连同其内部的 `CollisionShape3D` 移动到一个单独的 `StaticBody3D` 层级结构下。
5. **删除原 CSG**:**直接删除**原来的 `CSGCombiner3D` 节点及其所有子节点。此时,沉重的布尔计算完全卸载,你得到的是一个性能极高、带有完美碰撞的静态游戏场景素材!
> [!WARNING]
> **注意**:删除 CSG 节点前,建议将原始 CSG 节点结构保存为单独的 `CSG_Source.tscn` 场景文件或备用,以便后续修改关卡结构。
-95
View File
@@ -1,95 +0,0 @@
# CSG Blockout: Rapid Prototyping & Advanced Workflow Tutorial
*Read this in other languages: [简体中文](TUTORIAL_CN.md) | [Back to Main Docs](README.md) | [Architecture & Internals](ARCHITECTURE.md)*
## Table of Contents
- [1. Basics: Understanding CSG](#1-basics-understanding-csg)
- [2. Workflow: Rapid Blockout using Sidebar and Pie Menu](#2-workflow-rapid-blockout-using-sidebar-and-pie-menu)
- [3. Advanced: The Array and Scatter Systems](#3-advanced-the-array-and-scatter-systems)
- [4. Ultimate Technique: Performance Optimization Workflow (Must Read)](#4-ultimate-technique-performance-optimization-workflow-must-read)
---
## 1. Basics: Understanding CSG
**CSG (Constructive Solid Geometry)** is a modeling technique that allows you to construct complex 3D models using boolean operations on simple geometric shapes (e.g., cubes, spheres, cylinders).
In Godot, CSG is a powerful tool for level designers to block out levels or create rapid prototypes. Its core functionality is based on three boolean operations:
- **Union**: Merges two geometries into a single solid shape.
- **Intersection**: Retains only the overlapping portion of two geometries.
- **Subtraction**: Carves out the volume of one geometry from another.
![CSG Subtraction Demo](DocsImages/Subtraction.webp)
Using these simple operations, you can quickly cut out doors, dig tunnels, or assemble complex architectural structures directly in the engine, completely bypassing external software like Blender.
---
## 2. Workflow: Rapid Blockout using Sidebar and Pie Menu
The CSG_Blockout plugin is designed to eliminate tedious hierarchy management, providing an ultra-fast, WYSIWYG (What You See Is What You Get) experience. Whether you prefer mouse clicks or keyboard shortcuts, the plugin adapts to your workflow.
### Recommended Blockout Process
1. **Create the Root Node**: Add a `CSGCombiner3D` node to your scene. This will act as the root container for your geometry block. All subsequently added basic shapes will merge under this node.
2. **Select the Target**: Make sure this `CSGCombiner3D` node (or any CSG shape inside it) is selected in the Scene Tree.
3. **Summon the Menu**:
- **Method A (Pie Menu)**: Move your mouse over the 3D viewport and press `Shift + A`. A radial Pie Menu will appear at your cursor. Simply flick your mouse in the direction of your desired boolean operation and primitive shape.
![Pie Menu Demo](DocsImages/PieMenu.webp)
- **Method B (Sidebar)**: Use the newly added plugin sidebar on the left side of the 3D viewport. First, click the bottom icons to select a boolean operation (Union, Intersection, or Subtraction), and then click the top primitive shape icons (e.g., cube, sphere) to instantly add a CSG node with that operation.
![Sidebar Demo](DocsImages/Sidebar.webp)
4. **Procedural Grid Materials & Scale Metrics**:
- **World-Aligned Triplanar Grid**: The sidebar includes three procedural grid material presets (**Light Grid**, **Dark Grid**, **Orange Accent Grid**), along with **Unshaded (None)** and **Custom Material** slots. Powered by triplanar projection, the grid pattern stays perfectly aligned in world coordinates without texture stretching, making character scale and spatial distance estimation effortless.
- **Apply Material to Selection**: Select one or more `CSGShape3D` nodes in the viewport or scene tree, then click the **"Apply Material to Selected"** button at the bottom of the material group to batch-assign the active preset. Fully integrated with `Ctrl + Z` Undo/Redo.
![Material Presets & Quick Apply Demo](DocsImages/MaterialPresets.webp)
5. **Smart Contextual Hierarchy Placement**:
- Selecting a `CSGCombiner3D` (or `CSGRepeater3D` / `CSGSpreader3D`) automatically places new nodes as **children**.
- Selecting a standard `CSGShape3D` (e.g., `CSGBox3D`) automatically places new nodes as **siblings directly following** the selected node, eliminating tedious scene-tree dragging.
6. **Tweak and Adjust**: Drag the handles directly in the viewport to resize and position. The plugin fully supports `Ctrl+Z`, so feel free to experiment without fear of breaking things.
---
## 3. Advanced: The Array and Scatter Systems
### 1. The Array System (CSGRepeater3D)
`CSGRepeater3D` allows you to rapidly generate duplicated geometries using specific mathematical patterns. It is perfect for creating stairs, fences, pillars, or circular structures.
**Workflow**:
1. **Create Node**: Add a `CSGRepeater3D` node to your scene.
2. **Set Template**: You can either add a basic CSG primitive (e.g., `CSGBox3D`) as a direct child of the Repeater, or assign a target node to the `Template Node` property in the Inspector.
3. **Select a Pattern**: In the Inspector, locate the `Pattern` property, create a new resource, and choose your desired pattern strategy:
- `CSGGridPattern`: **Grid Array**, easily adjust count and spacing along the X/Y/Z axes.
- `CSGCircularPattern`: **Circular Array**, distribute items in a ring by configuring radius and count.
- `CSGSpiralPattern`: **Spiral Array**, adds a height offset to circular arrays, perfect for spiral staircases.
- `CSGNoisePattern`: **Noise Array**, organic distribution driven by 3D noise functions.
### 2. The Scatter System (CSGSpreader3D)
`CSGSpreader3D` integrates a proprietary **Spatial Hash Grid** algorithm. It randomly scatters geometry within any 3D collision shape efficiently and **without overlaps**. Even with hundreds of scattered instances, the O(1) collision avoidance ensures the editor remains completely lag-free.
**Workflow**:
1. **Create Node**: Add a `CSGSpreader3D` node and assign it a child node to use as the scatter template.
2. **Define Scatter Volume**: Assign a `Shape3D` (such as `BoxShape3D` or `SphereShape3D`) in the properties. Objects will only be spawned strictly within this shape's boundaries.
3. **Adjust Density & Randomness**:
- **Min Distance**: This is the most crucial property. It dictates the scatter density. A larger value yields sparse distribution, while a smaller value makes it dense.
- Combine this with Random Rotation and Random Scale settings to instantly generate chaotic yet organic scene blockouts, like rocky terrains or rubble.
---
## 4. Ultimate Technique: Performance Optimization Workflow (Must Read)
While CSG nodes are incredibly convenient during the editing phase, retaining a large number of them during the final game runtime will cause **severe performance degradation (FPS drops)** due to real-time boolean calculations.
When a specific room or architectural block is "finalized and won't be changed," you **must** convert it into a static mesh and collision body.
**Standard 1-Click Conversion Process**:
1. **Finalize Blockout**: Select the top-level `CSGCombiner3D` node that contains all your operations.
2. **Bake to Mesh**: In the editor's top menu bar, click the contextual options for `CSGCombiner3D` and select **"Bake to MeshInstance3D"**.
3. **Generate Collision**: Select the newly generated `MeshInstance3D`, click "Mesh" in the top menu bar, and select **"Create Trimesh Static Body"**. This will automatically generate a highly accurate collision shape that perfectly wraps your geometry.
4. **Clean up Structure**: Move the generated `MeshInstance3D` (along with its internal `CollisionShape3D`) under a clean `StaticBody3D` hierarchy.
5. **Delete Original CSG**: **Delete** the original `CSGCombiner3D` node and all its children. At this point, the heavy boolean calculations are completely unloaded, leaving you with an ultra-performant static game asset with perfect collisions!
> [!WARNING]
> **Note**: Before deleting the CSG nodes, it is highly recommended to save the original CSG structure as a separate scene file (e.g., `CSG_Source.tscn`) or keep a backup, so you can easily modify the level structure later.
-232
View File
@@ -1,232 +0,0 @@
@tool
class_name CsgBlockout extends EditorPlugin
var config: CsgBlockoutConfig:
get: return CsgBlockoutConfig.get_config()
var sidebar: CSGSideBlockoutBar
var topbar: CSGTopBlockoutBar
static var csg_plugin_path: String
static var undo_manager: EditorUndoRedoManager
var pie_menu: CsgPieMenu
var pie_menu_tab_pressed_time: int = 0
func _get_shape_menu() -> Array[Dictionary]:
return [
{"label": CsgBlockoutI18n.t("BOX"), "type": "create_csg", "csg_type": "CSGBox3D"},
{"label": CsgBlockoutI18n.t("CYLINDER"), "type": "create_csg", "csg_type": "CSGCylinder3D"},
{"label": CsgBlockoutI18n.t("MESH"), "type": "create_csg", "csg_type": "CSGMesh3D"},
{"label": CsgBlockoutI18n.t("POLYGON"), "type": "create_csg", "csg_type": "CSGPolygon3D"},
{"label": CsgBlockoutI18n.t("SPHERE"), "type": "create_csg", "csg_type": "CSGSphere3D"},
{"label": CsgBlockoutI18n.t("TORUS"), "type": "create_csg", "csg_type": "CSGTorus3D"}
]
func _get_pie_menu_items() -> Array[Dictionary]:
return [
{
"label": CsgBlockoutI18n.t("UNION"), "type": "submenu",
"operation": 0,
"children": _get_shape_menu()
},
{
"label": CsgBlockoutI18n.t("INTERSECTION"), "type": "submenu",
"operation": 1,
"children": _get_shape_menu()
},
{
"label": CsgBlockoutI18n.t("SUBTRACTION"), "type": "submenu",
"operation": 2,
"children": _get_shape_menu()
}
]
func _enter_tree() -> void:
csg_plugin_path = get_script().get_path().get_base_dir()
undo_manager = get_undo_redo()
# Nodes
add_custom_type("CSGRepeater3D", "CSGCombiner3D", preload("res://addons/csg_blockout/scripts/csg_repeater_3d.gd"), null)
add_custom_type("CSGSpreader3D", "CSGCombiner3D", preload("res://addons/csg_blockout/scripts/csg_spreader_3d.gd"), null)
# Sidebar
var sidebarScene = preload("res://addons/csg_blockout/scenes/csg_side_blockout_bar.tscn")
sidebar = sidebarScene.instantiate()
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, sidebar)
# Topbar
var topbarScene = preload("res://addons/csg_blockout/scenes/csg_top_blockout_bar.tscn")
topbar = topbarScene.instantiate()
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, topbar)
func _handles(object: Object) -> bool:
return object is Node3D
func _forward_3d_gui_input(viewport_camera: Camera3D, event: InputEvent) -> int:
if event is InputEventKey and event.keycode == KEY_A and not event.echo and _is_action_key_held(event):
if event.pressed:
if not is_instance_valid(pie_menu):
_open_pie_menu()
return EditorPlugin.AFTER_GUI_INPUT_STOP
else:
if is_instance_valid(pie_menu):
if Time.get_ticks_msec() - pie_menu_tab_pressed_time > 200:
# Hold mode release
pie_menu.execute_active_item(true)
return EditorPlugin.AFTER_GUI_INPUT_STOP
if is_instance_valid(pie_menu):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
pie_menu.execute_active_item(true)
return EditorPlugin.AFTER_GUI_INPUT_STOP
elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
pie_menu.execute_back()
return EditorPlugin.AFTER_GUI_INPUT_STOP
return EditorPlugin.AFTER_GUI_INPUT_STOP
if event is InputEventMouseMotion:
return EditorPlugin.AFTER_GUI_INPUT_STOP
return EditorPlugin.AFTER_GUI_INPUT_PASS
func _is_action_key_held(event: InputEventWithModifiers) -> bool:
var key: Key = config.action_key if config else KEY_SHIFT
return (key == KEY_SHIFT and event.shift_pressed) \
or (key == KEY_CTRL and event.ctrl_pressed) \
or (key == KEY_ALT and event.alt_pressed) \
or (key == KEY_META and event.meta_pressed)
func _open_pie_menu() -> void:
pie_menu = CsgPieMenu.new()
pie_menu.action_triggered.connect(_on_pie_menu_action_triggered)
pie_menu.close_requested.connect(_close_pie_menu)
var ed_scale: float = EditorInterface.get_editor_scale() if Engine.is_editor_hint() else 1.0
pie_menu.apply_editor_scale(ed_scale)
var base_control: Control = EditorInterface.get_base_control()
base_control.add_child(pie_menu)
var mouse_pos: Vector2 = base_control.get_local_mouse_position()
# Clamp position
var margin: float = pie_menu.outer_radius + 10.0 * ed_scale
var rect_size: Vector2 = base_control.get_rect().size
mouse_pos.x = clampf(mouse_pos.x, margin, rect_size.x - margin)
mouse_pos.y = clampf(mouse_pos.y, margin, rect_size.y - margin)
pie_menu.position = mouse_pos
pie_menu.setup(_get_pie_menu_items(), ed_scale)
pie_menu_tab_pressed_time = Time.get_ticks_msec()
func _close_pie_menu() -> void:
if is_instance_valid(pie_menu):
pie_menu.queue_free()
pie_menu = null
func _on_pie_menu_action_triggered(item: Dictionary) -> void:
var action_type = item.get("type", "")
if action_type == "submenu" and item.has("operation"):
var op = item.get("operation")
if config:
config.default_operation = op
var selection = EditorInterface.get_selection().get_selected_nodes()
if selection.size() > 0 and selection[0] is CSGShape3D:
var node = selection[0] as CSGShape3D
undo_manager.create_action(CsgBlockoutI18n.t("CHANGE_CSG_OP"))
undo_manager.add_do_property(node, "operation", op)
undo_manager.add_undo_property(node, "operation", node.operation)
undo_manager.commit_action()
elif action_type == "create_csg":
var csg_type = item.get("csg_type", "")
if csg_type != "" and ClassDB.can_instantiate(csg_type):
_create_csg_node(csg_type)
func _create_csg_node(csg_type: String) -> void:
var new_node: Node3D = ClassDB.instantiate(csg_type) as Node3D
if not new_node:
return
if new_node is CSGShape3D:
new_node.operation = config.default_operation if config else CSGShape3D.OPERATION_UNION
if config:
new_node.material = config.get_active_material()
var selection = EditorInterface.get_selection()
var selected_nodes = selection.get_selected_nodes()
var parent: Node = null
var insert_index: int = -1
var target_pos: Vector3 = Vector3.ZERO
if selected_nodes.size() > 0 and selected_nodes[0] is Node3D:
var selected_node: Node3D = selected_nodes[0]
target_pos = selected_node.global_position
if selected_node is CSGCombiner3D:
parent = selected_node
insert_index = parent.get_child_count()
elif selected_node is CSGShape3D:
parent = selected_node.get_parent()
if parent == null:
parent = selected_node
insert_index = parent.get_child_count()
else:
insert_index = selected_node.get_index() + 1
else:
parent = selected_node
insert_index = parent.get_child_count()
else:
parent = EditorInterface.get_edited_scene_root()
if parent:
insert_index = parent.get_child_count()
if not parent:
new_node.free()
return
var owner_ref = EditorInterface.get_edited_scene_root()
if owner_ref == null:
owner_ref = parent
undo_manager.create_action(CsgBlockoutI18n.tf("CREATE_NODE", [csg_type]))
undo_manager.add_undo_reference(new_node)
undo_manager.add_do_method(self, "_undoable_create_csg", parent, new_node, owner_ref, target_pos, insert_index)
undo_manager.add_do_method(self, "_select_node", new_node)
undo_manager.add_undo_method(self, "_undoable_remove_csg", parent, new_node)
undo_manager.commit_action()
func _undoable_create_csg(parent: Node, node: Node3D, owner_ref: Node, global_pos: Vector3, insert_index: int) -> void:
if node.get_parent() != parent:
parent.add_child(node, true)
if insert_index >= 0 and insert_index < parent.get_child_count():
parent.move_child(node, insert_index)
node.owner = owner_ref
node.global_position = global_pos
func _undoable_remove_csg(parent: Node, node: Node3D) -> void:
if is_instance_valid(node) and node.get_parent() == parent:
parent.remove_child(node)
func _select_node(node: Node) -> void:
if not is_instance_valid(node) or not node.is_inside_tree():
return
var selection = EditorInterface.get_selection()
if selection:
selection.clear()
selection.add_node(node)
func _exit_tree() -> void:
remove_custom_type("CSGRepeater3D")
remove_custom_type("CSGSpreader3D")
undo_manager = null
if sidebar and is_instance_valid(sidebar):
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, sidebar)
sidebar.free()
if topbar and is_instance_valid(topbar):
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, topbar)
topbar.free()
-1
View File
@@ -1 +0,0 @@
uid://cmwnf4lkck6mm
-7
View File
@@ -1,7 +0,0 @@
[plugin]
name="CSG_Blockout"
description="Enhance your blockout workflow with CSG_Blockout. This tool adds quick access buttons to the left toolbar for easy addition of CSG nodes. Heavily refactored for modern Godot 4 workflows."
author="qwqzhanqwq"
version="2.0.2"
script="csg_blockout.gd"
-28
View File
@@ -1,28 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="2048" height="2048">
<rect width="2048" height="2048" fill="#373737"/>
<path fill="#FFFFFF" fill-rule="evenodd" d="
M 1007.80 360.00 L 1012.00 359.48 L 1016.00 360.52 L 1422.00 546.61 L 1431.27 553.00 L 1433.75 560.00 L 1433.55 793.00 L 1445.33 825.00 L 1452.48 853.00 L 1457.47 884.00 L 1459.21 914.00 L 1457.29 953.00 L 1452.35 983.00 L 1443.49 1015.00 L 1430.36 1048.00 L 1415.39 1076.00 L 1396.36 1103.00 L 1378.00 1123.95 L 1355.00 1145.28 L 1339.00 1157.45 L 1322.00 1168.42 L 1287.00 1185.43 L 1267.00 1192.34 L 1241.00 1198.42 L 1219.00 1201.36 L 1192.00 1202.67 L 1028.00 1277.39 L 1018.00 1280.48 L 1007.00 1277.32 L 599.00 1093.51 L 591.00 1087.94 L 588.52 1083.00 L 588.17 1079.00 L 588.27 561.00 L 589.52 555.00 L 593.00 550.82 L 598.00 547.79 L 1007.80 360.00 Z
M 1009.00 386.77 L 632.41 560.00 L 734.00 607.14 L 804.00 638.85 L 834.00 622.82 L 861.00 610.63 L 891.00 599.45 L 922.00 590.57 L 945.00 585.54 L 978.00 580.63 L 1008.00 578.46 L 1044.00 578.60 L 1069.00 580.59 L 1096.00 584.45 L 1139.00 594.51 L 1182.00 610.52 L 1229.00 633.75 L 1389.68 560.00 L 1013.00 386.73 L 1009.00 386.77 Z
M 614.00 579.79 L 613.56 1072.00 L 1004.00 1248.64 L 1004.40 1047.00 L 977.00 1033.47 L 957.00 1021.26 L 933.00 1003.43 L 911.00 984.24 L 892.89 966.00 L 871.71 941.00 L 853.80 916.00 L 838.59 891.00 L 823.56 861.00 L 813.64 837.00 L 803.60 806.00 L 796.61 777.00 L 790.58 740.00 L 788.78 716.00 L 788.59 687.00 L 790.55 661.00 L 614.00 579.79 Z
M 1407.00 579.85 L 1267.81 644.00 L 1292.00 652.56 L 1308.00 659.60 L 1328.00 670.58 L 1346.00 682.66 L 1361.00 694.57 L 1378.28 711.00 L 1393.42 728.00 L 1408.00 747.97 L 1408.54 580.00 L 1407.00 579.85 Z
M 1018.00 602.98 L 970.00 606.85 L 929.00 614.82 L 886.00 627.87 L 847.00 644.71 L 816.00 661.87 L 813.73 701.00 L 816.78 747.00 L 819.75 766.00 L 826.87 797.00 L 835.82 825.00 L 844.90 848.00 L 854.93 869.00 L 867.82 892.00 L 881.75 913.00 L 897.82 934.00 L 914.00 952.39 L 932.00 970.11 L 951.00 986.21 L 972.00 1001.25 L 992.00 1013.07 L 1017.00 1025.30 L 1028.38 1018.00 L 1024.55 986.00 L 1023.39 948.00 L 1025.62 913.00 L 1030.51 879.00 L 1035.78 855.00 L 1044.73 825.00 L 1060.70 786.00 L 1073.71 761.00 L 1086.57 740.00 L 1103.68 717.00 L 1120.00 699.10 L 1139.00 681.53 L 1155.00 669.55 L 1176.00 656.64 L 1198.50 646.00 L 1183.00 637.87 L 1157.00 626.93 L 1114.00 613.85 L 1070.00 605.78 L 1048.00 603.69 L 1018.00 602.98 Z
M 1218.00 665.78 L 1199.00 672.95 L 1180.00 682.95 L 1164.00 693.77 L 1146.00 708.76 L 1130.88 724.00 L 1113.85 745.00 L 1099.95 766.00 L 1086.66 790.00 L 1075.81 814.00 L 1066.87 838.00 L 1058.91 866.00 L 1053.91 890.00 L 1050.00 919.00 L 1048.54 945.00 L 1048.93 976.00 L 1052.00 1002.65 L 1069.00 989.18 L 1091.00 969.25 L 1125.09 932.00 L 1154.21 892.00 L 1180.06 846.00 L 1190.18 824.00 L 1200.16 798.00 L 1208.11 772.00 L 1214.17 746.00 L 1218.21 720.00 L 1220.02 695.00 L 1219.68 666.00 L 1218.00 665.78 Z
M 1245.00 665.24 L 1244.28 710.00 L 1241.48 735.00 L 1236.35 763.00 L 1229.42 789.00 L 1221.39 813.00 L 1200.49 861.00 L 1186.34 887.00 L 1173.26 908.00 L 1156.44 932.00 L 1136.38 957.00 L 1117.18 978.00 L 1097.00 997.53 L 1056.52 1031.00 L 1062.91 1056.00 L 1073.91 1085.00 L 1086.92 1109.00 L 1102.98 1131.00 L 1121.00 1149.18 L 1139.00 1162.19 L 1161.00 1172.11 L 1180.00 1176.09 L 1200.00 1176.92 L 1226.00 1175.09 L 1248.00 1171.11 L 1272.00 1164.24 L 1294.00 1155.09 L 1312.00 1145.07 L 1332.00 1131.26 L 1348.00 1118.09 L 1363.08 1103.00 L 1379.04 1084.00 L 1392.23 1065.00 L 1404.07 1044.00 L 1413.22 1024.00 L 1422.31 998.00 L 1428.21 974.00 L 1432.07 950.00 L 1434.06 920.00 L 1433.27 896.00 L 1430.26 871.00 L 1426.03 850.00 L 1420.22 829.00 L 1411.23 805.00 L 1401.00 784.25 L 1389.00 764.05 L 1375.19 745.00 L 1361.00 728.84 L 1344.00 712.85 L 1326.00 698.90 L 1308.00 687.77 L 1285.00 676.80 L 1262.00 668.90 L 1245.00 665.24 Z
M 1033.00 1045.52 L 1029.91 1048.00 L 1029.89 1248.00 L 1031.00 1248.49 L 1148.68 1195.00 L 1148.57 1194.00 L 1126.00 1183.37 L 1110.00 1172.39 L 1096.00 1159.97 L 1079.81 1142.00 L 1065.67 1122.00 L 1051.64 1096.00 L 1043.54 1077.00 L 1034.11 1046.00 L 1033.00 1045.52 Z
M 793.92 1361.00 L 813.00 1360.49 L 827.00 1361.68 L 842.00 1364.55 L 860.00 1370.64 L 874.00 1377.75 L 887.00 1386.68 L 896.00 1395.03 L 903.47 1404.00 L 863.00 1440.80 L 844.00 1424.94 L 829.00 1417.86 L 820.00 1415.70 L 808.00 1414.69 L 795.00 1415.71 L 786.00 1417.88 L 773.00 1423.82 L 763.69 1431.00 L 754.79 1442.00 L 748.85 1454.00 L 745.84 1465.00 L 744.68 1475.00 L 744.89 1485.00 L 747.87 1500.00 L 750.92 1508.00 L 756.86 1518.00 L 764.00 1526.02 L 773.00 1533.12 L 785.15 1539.00 L 798.00 1542.16 L 812.00 1542.66 L 824.00 1541.24 L 834.00 1538.02 L 843.00 1533.28 L 864.00 1516.18 L 903.83 1552.00 L 891.88 1566.00 L 875.00 1579.47 L 854.00 1589.49 L 831.00 1595.35 L 805.00 1596.91 L 791.00 1596.37 L 777.00 1594.31 L 753.00 1587.30 L 732.00 1576.40 L 714.00 1561.78 L 698.61 1543.00 L 687.50 1521.00 L 681.55 1499.00 L 680.23 1485.00 L 680.44 1467.00 L 683.68 1447.00 L 691.59 1426.00 L 703.57 1407.00 L 713.00 1396.47 L 723.00 1387.59 L 744.00 1374.43 L 767.00 1365.55 L 793.92 1361.00 Z
M 1009.33 1361.00 L 1037.00 1360.64 L 1065.00 1364.54 L 1091.00 1372.63 L 1104.00 1378.86 L 1107.85 1382.00 L 1087.00 1428.89 L 1074.00 1421.80 L 1061.00 1416.72 L 1046.00 1412.77 L 1030.00 1410.80 L 1018.00 1410.95 L 1003.00 1413.87 L 993.45 1419.00 L 988.92 1425.00 L 987.86 1431.00 L 989.00 1435.90 L 992.07 1440.00 L 998.00 1444.02 L 1013.00 1449.22 L 1066.00 1461.77 L 1079.00 1466.69 L 1092.00 1473.66 L 1101.00 1480.88 L 1107.31 1488.00 L 1112.53 1497.00 L 1115.50 1505.00 L 1117.84 1519.00 L 1117.28 1534.00 L 1112.32 1551.00 L 1104.51 1564.00 L 1092.00 1576.38 L 1079.00 1584.54 L 1062.00 1591.23 L 1043.00 1595.39 L 1029.00 1596.66 L 1005.00 1596.67 L 977.00 1593.24 L 958.00 1588.46 L 941.00 1582.38 L 926.00 1574.48 L 919.60 1569.00 L 942.00 1522.53 L 958.00 1532.03 L 972.00 1538.22 L 989.00 1543.27 L 1008.00 1546.12 L 1025.00 1546.14 L 1038.00 1544.04 L 1047.00 1540.19 L 1053.00 1532.91 L 1053.58 1527.00 L 1052.00 1521.34 L 1048.00 1516.65 L 1042.00 1512.87 L 1027.00 1507.71 L 977.00 1495.53 L 961.00 1489.42 L 950.00 1483.44 L 941.00 1476.46 L 933.57 1468.00 L 928.70 1459.00 L 925.56 1449.00 L 923.90 1433.00 L 926.55 1415.00 L 932.62 1401.00 L 941.71 1389.00 L 954.00 1378.64 L 969.00 1370.59 L 991.00 1363.62 L 1009.33 1361.00 Z
M 1252.59 1361.00 L 1272.00 1360.40 L 1288.00 1361.65 L 1303.00 1364.52 L 1316.00 1368.54 L 1330.00 1374.59 L 1344.00 1382.77 L 1354.00 1390.52 L 1364.88 1402.00 L 1365.00 1403.00 L 1325.00 1439.53 L 1306.00 1424.87 L 1296.00 1419.90 L 1286.00 1416.79 L 1264.00 1414.79 L 1254.00 1415.93 L 1243.00 1418.82 L 1229.00 1425.89 L 1218.72 1435.00 L 1211.00 1446.21 L 1205.89 1459.00 L 1203.79 1471.00 L 1203.89 1486.00 L 1206.95 1501.00 L 1212.86 1514.00 L 1221.67 1525.00 L 1232.00 1533.30 L 1244.00 1539.17 L 1258.00 1542.35 L 1271.00 1542.83 L 1285.00 1541.27 L 1295.00 1538.22 L 1302.42 1534.00 L 1303.00 1473.20 L 1360.00 1473.22 L 1361.69 1474.00 L 1361.57 1568.00 L 1337.00 1582.25 L 1308.00 1592.34 L 1283.00 1596.40 L 1251.00 1596.37 L 1225.00 1591.34 L 1202.00 1582.36 L 1183.00 1570.40 L 1172.44 1561.00 L 1163.61 1551.00 L 1150.46 1530.00 L 1142.63 1509.00 L 1139.65 1493.00 L 1138.89 1480.00 L 1140.57 1457.00 L 1147.58 1433.00 L 1160.70 1410.00 L 1169.85 1399.00 L 1180.00 1389.51 L 1199.00 1376.71 L 1212.00 1370.54 L 1226.00 1365.71 L 1252.59 1361.00 Z
M 872.55 1640.00 L 889.00 1639.48 L 898.00 1641.63 L 905.00 1644.81 L 912.00 1649.59 L 917.38 1655.00 L 922.47 1663.00 L 925.27 1671.00 L 926.24 1687.00 L 924.27 1696.00 L 920.57 1704.00 L 910.00 1715.34 L 903.00 1719.56 L 895.00 1722.52 L 887.00 1723.69 L 874.00 1723.43 L 861.00 1719.54 L 854.00 1715.31 L 846.35 1708.00 L 841.69 1701.00 L 838.69 1693.00 L 837.45 1686.00 L 837.59 1676.00 L 839.73 1667.00 L 843.57 1659.00 L 849.30 1652.00 L 857.00 1645.71 L 866.00 1641.56 L 872.55 1640.00 Z
M 972.42 1640.00 L 988.00 1639.41 L 998.00 1641.55 L 1008.00 1646.75 L 1014.71 1653.00 L 1005.00 1662.77 L 997.00 1656.64 L 990.00 1653.82 L 982.00 1652.92 L 974.00 1653.87 L 967.00 1656.85 L 961.67 1661.00 L 957.69 1666.00 L 954.73 1673.00 L 953.73 1684.00 L 955.81 1693.00 L 959.87 1700.00 L 967.00 1706.04 L 975.00 1709.28 L 985.70 1710.00 L 996.00 1707.14 L 1005.00 1700.23 L 1014.49 1709.00 L 1014.26 1711.00 L 1008.00 1716.38 L 995.00 1722.50 L 975.00 1723.52 L 961.00 1719.46 L 953.00 1714.44 L 946.48 1708.00 L 939.56 1695.00 L 937.72 1686.00 L 937.71 1677.00 L 939.51 1668.00 L 943.50 1659.00 L 949.09 1652.00 L 957.00 1645.69 L 966.00 1641.54 L 972.42 1640.00 Z
M 1144.51 1640.00 L 1161.00 1639.55 L 1176.00 1644.50 L 1182.00 1648.41 L 1189.00 1655.23 L 1192.46 1660.00 L 1196.43 1669.00 L 1197.92 1686.00 L 1196.33 1695.00 L 1193.41 1702.00 L 1189.40 1708.00 L 1183.00 1714.25 L 1175.00 1719.37 L 1167.00 1722.41 L 1159.00 1723.67 L 1145.00 1723.43 L 1132.00 1719.22 L 1124.19 1714.00 L 1118.00 1707.70 L 1113.62 1701.00 L 1110.60 1693.00 L 1109.37 1686.00 L 1109.51 1676.00 L 1111.65 1667.00 L 1115.53 1659.00 L 1121.29 1652.00 L 1128.00 1646.44 L 1135.00 1642.67 L 1144.51 1640.00 Z
M 681.39 1641.00 L 729.00 1640.59 L 739.00 1643.45 L 746.57 1649.00 L 750.36 1655.00 L 751.46 1665.00 L 748.33 1674.00 L 741.84 1680.00 L 749.00 1684.66 L 752.56 1689.00 L 755.54 1699.00 L 754.43 1708.00 L 748.77 1716.00 L 741.00 1720.45 L 733.00 1722.38 L 682.00 1722.73 L 681.39 1641.00 Z
M 771.82 1641.00 L 787.00 1640.22 L 788.25 1641.00 L 788.14 1709.00 L 830.00 1709.42 L 831.38 1711.00 L 831.00 1722.58 L 772.00 1722.15 L 771.82 1641.00 Z
M 1029.87 1641.00 L 1043.00 1640.22 L 1045.91 1641.00 L 1046.00 1681.38 L 1085.00 1640.81 L 1103.00 1640.46 L 1069.70 1678.00 L 1105.98 1722.00 L 1088.00 1722.70 L 1059.00 1688.70 L 1045.92 1703.00 L 1045.98 1722.00 L 1045.00 1722.85 L 1030.00 1722.11 L 1029.87 1641.00 Z
M 1214.04 1641.00 L 1229.96 1641.00 L 1229.93 1687.00 L 1230.93 1696.00 L 1232.89 1701.00 L 1237.16 1706.00 L 1243.00 1709.04 L 1251.00 1710.16 L 1258.00 1709.22 L 1262.65 1707.00 L 1266.12 1704.00 L 1269.40 1698.00 L 1271.07 1687.00 L 1271.05 1641.00 L 1272.00 1640.20 L 1286.94 1641.00 L 1286.38 1697.00 L 1282.37 1708.00 L 1274.00 1717.40 L 1267.00 1721.35 L 1259.00 1723.47 L 1242.00 1723.47 L 1230.00 1719.50 L 1220.47 1711.00 L 1216.67 1704.00 L 1214.44 1695.00 L 1214.04 1641.00 Z
M 1298.27 1641.00 L 1368.59 1641.00 L 1368.00 1653.66 L 1341.53 1654.00 L 1341.00 1722.74 L 1326.00 1722.91 L 1325.24 1722.00 L 1325.18 1654.00 L 1299.00 1653.67 L 1298.27 1641.00 Z
M 879.00 1652.95 L 867.00 1656.75 L 858.94 1664.00 L 853.87 1676.00 L 853.73 1686.00 L 857.00 1695.82 L 864.00 1704.24 L 874.00 1709.19 L 885.00 1710.10 L 895.81 1707.00 L 905.15 1699.00 L 909.32 1690.00 L 910.22 1678.00 L 907.22 1668.00 L 900.00 1658.95 L 890.00 1653.88 L 879.00 1652.95 Z
M 1152.00 1652.93 L 1141.00 1655.77 L 1131.79 1663.00 L 1126.65 1673.00 L 1125.72 1686.00 L 1128.92 1696.00 L 1135.60 1704.00 L 1146.00 1709.22 L 1156.00 1710.16 L 1167.00 1707.20 L 1175.04 1701.00 L 1180.25 1692.00 L 1181.78 1681.00 L 1179.24 1669.00 L 1172.73 1660.00 L 1164.00 1654.69 L 1152.00 1652.93 Z
M 698.00 1653.08 L 698.00 1675.01 L 727.00 1674.29 L 732.00 1672.08 L 735.02 1668.00 L 735.08 1660.00 L 730.00 1654.87 L 722.00 1653.13 L 698.00 1653.08 Z
M 698.00 1687.36 L 698.00 1710.13 L 730.00 1709.35 L 735.54 1707.00 L 739.20 1702.00 L 739.42 1697.00 L 738.13 1693.00 L 733.00 1688.93 L 724.00 1687.43 L 698.00 1687.36 Z
"/>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,27 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="2048" height="2048">
<path fill="#FFFFFF" fill-rule="evenodd" d="
M 1007.80 360.00 L 1012.00 359.48 L 1016.00 360.52 L 1422.00 546.61 L 1431.27 553.00 L 1433.75 560.00 L 1433.55 793.00 L 1445.33 825.00 L 1452.48 853.00 L 1457.47 884.00 L 1459.21 914.00 L 1457.29 953.00 L 1452.35 983.00 L 1443.49 1015.00 L 1430.36 1048.00 L 1415.39 1076.00 L 1396.36 1103.00 L 1378.00 1123.95 L 1355.00 1145.28 L 1339.00 1157.45 L 1322.00 1168.42 L 1287.00 1185.43 L 1267.00 1192.34 L 1241.00 1198.42 L 1219.00 1201.36 L 1192.00 1202.67 L 1028.00 1277.39 L 1018.00 1280.48 L 1007.00 1277.32 L 599.00 1093.51 L 591.00 1087.94 L 588.52 1083.00 L 588.17 1079.00 L 588.27 561.00 L 589.52 555.00 L 593.00 550.82 L 598.00 547.79 L 1007.80 360.00 Z
M 1009.00 386.77 L 632.41 560.00 L 734.00 607.14 L 804.00 638.85 L 834.00 622.82 L 861.00 610.63 L 891.00 599.45 L 922.00 590.57 L 945.00 585.54 L 978.00 580.63 L 1008.00 578.46 L 1044.00 578.60 L 1069.00 580.59 L 1096.00 584.45 L 1139.00 594.51 L 1182.00 610.52 L 1229.00 633.75 L 1389.68 560.00 L 1013.00 386.73 L 1009.00 386.77 Z
M 614.00 579.79 L 613.56 1072.00 L 1004.00 1248.64 L 1004.40 1047.00 L 977.00 1033.47 L 957.00 1021.26 L 933.00 1003.43 L 911.00 984.24 L 892.89 966.00 L 871.71 941.00 L 853.80 916.00 L 838.59 891.00 L 823.56 861.00 L 813.64 837.00 L 803.60 806.00 L 796.61 777.00 L 790.58 740.00 L 788.78 716.00 L 788.59 687.00 L 790.55 661.00 L 614.00 579.79 Z
M 1407.00 579.85 L 1267.81 644.00 L 1292.00 652.56 L 1308.00 659.60 L 1328.00 670.58 L 1346.00 682.66 L 1361.00 694.57 L 1378.28 711.00 L 1393.42 728.00 L 1408.00 747.97 L 1408.54 580.00 L 1407.00 579.85 Z
M 1018.00 602.98 L 970.00 606.85 L 929.00 614.82 L 886.00 627.87 L 847.00 644.71 L 816.00 661.87 L 813.73 701.00 L 816.78 747.00 L 819.75 766.00 L 826.87 797.00 L 835.82 825.00 L 844.90 848.00 L 854.93 869.00 L 867.82 892.00 L 881.75 913.00 L 897.82 934.00 L 914.00 952.39 L 932.00 970.11 L 951.00 986.21 L 972.00 1001.25 L 992.00 1013.07 L 1017.00 1025.30 L 1028.38 1018.00 L 1024.55 986.00 L 1023.39 948.00 L 1025.62 913.00 L 1030.51 879.00 L 1035.78 855.00 L 1044.73 825.00 L 1060.70 786.00 L 1073.71 761.00 L 1086.57 740.00 L 1103.68 717.00 L 1120.00 699.10 L 1139.00 681.53 L 1155.00 669.55 L 1176.00 656.64 L 1198.50 646.00 L 1183.00 637.87 L 1157.00 626.93 L 1114.00 613.85 L 1070.00 605.78 L 1048.00 603.69 L 1018.00 602.98 Z
M 1218.00 665.78 L 1199.00 672.95 L 1180.00 682.95 L 1164.00 693.77 L 1146.00 708.76 L 1130.88 724.00 L 1113.85 745.00 L 1099.95 766.00 L 1086.66 790.00 L 1075.81 814.00 L 1066.87 838.00 L 1058.91 866.00 L 1053.91 890.00 L 1050.00 919.00 L 1048.54 945.00 L 1048.93 976.00 L 1052.00 1002.65 L 1069.00 989.18 L 1091.00 969.25 L 1125.09 932.00 L 1154.21 892.00 L 1180.06 846.00 L 1190.18 824.00 L 1200.16 798.00 L 1208.11 772.00 L 1214.17 746.00 L 1218.21 720.00 L 1220.02 695.00 L 1219.68 666.00 L 1218.00 665.78 Z
M 1245.00 665.24 L 1244.28 710.00 L 1241.48 735.00 L 1236.35 763.00 L 1229.42 789.00 L 1221.39 813.00 L 1200.49 861.00 L 1186.34 887.00 L 1173.26 908.00 L 1156.44 932.00 L 1136.38 957.00 L 1117.18 978.00 L 1097.00 997.53 L 1056.52 1031.00 L 1062.91 1056.00 L 1073.91 1085.00 L 1086.92 1109.00 L 1102.98 1131.00 L 1121.00 1149.18 L 1139.00 1162.19 L 1161.00 1172.11 L 1180.00 1176.09 L 1200.00 1176.92 L 1226.00 1175.09 L 1248.00 1171.11 L 1272.00 1164.24 L 1294.00 1155.09 L 1312.00 1145.07 L 1332.00 1131.26 L 1348.00 1118.09 L 1363.08 1103.00 L 1379.04 1084.00 L 1392.23 1065.00 L 1404.07 1044.00 L 1413.22 1024.00 L 1422.31 998.00 L 1428.21 974.00 L 1432.07 950.00 L 1434.06 920.00 L 1433.27 896.00 L 1430.26 871.00 L 1426.03 850.00 L 1420.22 829.00 L 1411.23 805.00 L 1401.00 784.25 L 1389.00 764.05 L 1375.19 745.00 L 1361.00 728.84 L 1344.00 712.85 L 1326.00 698.90 L 1308.00 687.77 L 1285.00 676.80 L 1262.00 668.90 L 1245.00 665.24 Z
M 1033.00 1045.52 L 1029.91 1048.00 L 1029.89 1248.00 L 1031.00 1248.49 L 1148.68 1195.00 L 1148.57 1194.00 L 1126.00 1183.37 L 1110.00 1172.39 L 1096.00 1159.97 L 1079.81 1142.00 L 1065.67 1122.00 L 1051.64 1096.00 L 1043.54 1077.00 L 1034.11 1046.00 L 1033.00 1045.52 Z
M 793.92 1361.00 L 813.00 1360.49 L 827.00 1361.68 L 842.00 1364.55 L 860.00 1370.64 L 874.00 1377.75 L 887.00 1386.68 L 896.00 1395.03 L 903.47 1404.00 L 863.00 1440.80 L 844.00 1424.94 L 829.00 1417.86 L 820.00 1415.70 L 808.00 1414.69 L 795.00 1415.71 L 786.00 1417.88 L 773.00 1423.82 L 763.69 1431.00 L 754.79 1442.00 L 748.85 1454.00 L 745.84 1465.00 L 744.68 1475.00 L 744.89 1485.00 L 747.87 1500.00 L 750.92 1508.00 L 756.86 1518.00 L 764.00 1526.02 L 773.00 1533.12 L 785.15 1539.00 L 798.00 1542.16 L 812.00 1542.66 L 824.00 1541.24 L 834.00 1538.02 L 843.00 1533.28 L 864.00 1516.18 L 903.83 1552.00 L 891.88 1566.00 L 875.00 1579.47 L 854.00 1589.49 L 831.00 1595.35 L 805.00 1596.91 L 791.00 1596.37 L 777.00 1594.31 L 753.00 1587.30 L 732.00 1576.40 L 714.00 1561.78 L 698.61 1543.00 L 687.50 1521.00 L 681.55 1499.00 L 680.23 1485.00 L 680.44 1467.00 L 683.68 1447.00 L 691.59 1426.00 L 703.57 1407.00 L 713.00 1396.47 L 723.00 1387.59 L 744.00 1374.43 L 767.00 1365.55 L 793.92 1361.00 Z
M 1009.33 1361.00 L 1037.00 1360.64 L 1065.00 1364.54 L 1091.00 1372.63 L 1104.00 1378.86 L 1107.85 1382.00 L 1087.00 1428.89 L 1074.00 1421.80 L 1061.00 1416.72 L 1046.00 1412.77 L 1030.00 1410.80 L 1018.00 1410.95 L 1003.00 1413.87 L 993.45 1419.00 L 988.92 1425.00 L 987.86 1431.00 L 989.00 1435.90 L 992.07 1440.00 L 998.00 1444.02 L 1013.00 1449.22 L 1066.00 1461.77 L 1079.00 1466.69 L 1092.00 1473.66 L 1101.00 1480.88 L 1107.31 1488.00 L 1112.53 1497.00 L 1115.50 1505.00 L 1117.84 1519.00 L 1117.28 1534.00 L 1112.32 1551.00 L 1104.51 1564.00 L 1092.00 1576.38 L 1079.00 1584.54 L 1062.00 1591.23 L 1043.00 1595.39 L 1029.00 1596.66 L 1005.00 1596.67 L 977.00 1593.24 L 958.00 1588.46 L 941.00 1582.38 L 926.00 1574.48 L 919.60 1569.00 L 942.00 1522.53 L 958.00 1532.03 L 972.00 1538.22 L 989.00 1543.27 L 1008.00 1546.12 L 1025.00 1546.14 L 1038.00 1544.04 L 1047.00 1540.19 L 1053.00 1532.91 L 1053.58 1527.00 L 1052.00 1521.34 L 1048.00 1516.65 L 1042.00 1512.87 L 1027.00 1507.71 L 977.00 1495.53 L 961.00 1489.42 L 950.00 1483.44 L 941.00 1476.46 L 933.57 1468.00 L 928.70 1459.00 L 925.56 1449.00 L 923.90 1433.00 L 926.55 1415.00 L 932.62 1401.00 L 941.71 1389.00 L 954.00 1378.64 L 969.00 1370.59 L 991.00 1363.62 L 1009.33 1361.00 Z
M 1252.59 1361.00 L 1272.00 1360.40 L 1288.00 1361.65 L 1303.00 1364.52 L 1316.00 1368.54 L 1330.00 1374.59 L 1344.00 1382.77 L 1354.00 1390.52 L 1364.88 1402.00 L 1365.00 1403.00 L 1325.00 1439.53 L 1306.00 1424.87 L 1296.00 1419.90 L 1286.00 1416.79 L 1264.00 1414.79 L 1254.00 1415.93 L 1243.00 1418.82 L 1229.00 1425.89 L 1218.72 1435.00 L 1211.00 1446.21 L 1205.89 1459.00 L 1203.79 1471.00 L 1203.89 1486.00 L 1206.95 1501.00 L 1212.86 1514.00 L 1221.67 1525.00 L 1232.00 1533.30 L 1244.00 1539.17 L 1258.00 1542.35 L 1271.00 1542.83 L 1285.00 1541.27 L 1295.00 1538.22 L 1302.42 1534.00 L 1303.00 1473.20 L 1360.00 1473.22 L 1361.69 1474.00 L 1361.57 1568.00 L 1337.00 1582.25 L 1308.00 1592.34 L 1283.00 1596.40 L 1251.00 1596.37 L 1225.00 1591.34 L 1202.00 1582.36 L 1183.00 1570.40 L 1172.44 1561.00 L 1163.61 1551.00 L 1150.46 1530.00 L 1142.63 1509.00 L 1139.65 1493.00 L 1138.89 1480.00 L 1140.57 1457.00 L 1147.58 1433.00 L 1160.70 1410.00 L 1169.85 1399.00 L 1180.00 1389.51 L 1199.00 1376.71 L 1212.00 1370.54 L 1226.00 1365.71 L 1252.59 1361.00 Z
M 872.55 1640.00 L 889.00 1639.48 L 898.00 1641.63 L 905.00 1644.81 L 912.00 1649.59 L 917.38 1655.00 L 922.47 1663.00 L 925.27 1671.00 L 926.24 1687.00 L 924.27 1696.00 L 920.57 1704.00 L 910.00 1715.34 L 903.00 1719.56 L 895.00 1722.52 L 887.00 1723.69 L 874.00 1723.43 L 861.00 1719.54 L 854.00 1715.31 L 846.35 1708.00 L 841.69 1701.00 L 838.69 1693.00 L 837.45 1686.00 L 837.59 1676.00 L 839.73 1667.00 L 843.57 1659.00 L 849.30 1652.00 L 857.00 1645.71 L 866.00 1641.56 L 872.55 1640.00 Z
M 972.42 1640.00 L 988.00 1639.41 L 998.00 1641.55 L 1008.00 1646.75 L 1014.71 1653.00 L 1005.00 1662.77 L 997.00 1656.64 L 990.00 1653.82 L 982.00 1652.92 L 974.00 1653.87 L 967.00 1656.85 L 961.67 1661.00 L 957.69 1666.00 L 954.73 1673.00 L 953.73 1684.00 L 955.81 1693.00 L 959.87 1700.00 L 967.00 1706.04 L 975.00 1709.28 L 985.70 1710.00 L 996.00 1707.14 L 1005.00 1700.23 L 1014.49 1709.00 L 1014.26 1711.00 L 1008.00 1716.38 L 995.00 1722.50 L 975.00 1723.52 L 961.00 1719.46 L 953.00 1714.44 L 946.48 1708.00 L 939.56 1695.00 L 937.72 1686.00 L 937.71 1677.00 L 939.51 1668.00 L 943.50 1659.00 L 949.09 1652.00 L 957.00 1645.69 L 966.00 1641.54 L 972.42 1640.00 Z
M 1144.51 1640.00 L 1161.00 1639.55 L 1176.00 1644.50 L 1182.00 1648.41 L 1189.00 1655.23 L 1192.46 1660.00 L 1196.43 1669.00 L 1197.92 1686.00 L 1196.33 1695.00 L 1193.41 1702.00 L 1189.40 1708.00 L 1183.00 1714.25 L 1175.00 1719.37 L 1167.00 1722.41 L 1159.00 1723.67 L 1145.00 1723.43 L 1132.00 1719.22 L 1124.19 1714.00 L 1118.00 1707.70 L 1113.62 1701.00 L 1110.60 1693.00 L 1109.37 1686.00 L 1109.51 1676.00 L 1111.65 1667.00 L 1115.53 1659.00 L 1121.29 1652.00 L 1128.00 1646.44 L 1135.00 1642.67 L 1144.51 1640.00 Z
M 681.39 1641.00 L 729.00 1640.59 L 739.00 1643.45 L 746.57 1649.00 L 750.36 1655.00 L 751.46 1665.00 L 748.33 1674.00 L 741.84 1680.00 L 749.00 1684.66 L 752.56 1689.00 L 755.54 1699.00 L 754.43 1708.00 L 748.77 1716.00 L 741.00 1720.45 L 733.00 1722.38 L 682.00 1722.73 L 681.39 1641.00 Z
M 771.82 1641.00 L 787.00 1640.22 L 788.25 1641.00 L 788.14 1709.00 L 830.00 1709.42 L 831.38 1711.00 L 831.00 1722.58 L 772.00 1722.15 L 771.82 1641.00 Z
M 1029.87 1641.00 L 1043.00 1640.22 L 1045.91 1641.00 L 1046.00 1681.38 L 1085.00 1640.81 L 1103.00 1640.46 L 1069.70 1678.00 L 1105.98 1722.00 L 1088.00 1722.70 L 1059.00 1688.70 L 1045.92 1703.00 L 1045.98 1722.00 L 1045.00 1722.85 L 1030.00 1722.11 L 1029.87 1641.00 Z
M 1214.04 1641.00 L 1229.96 1641.00 L 1229.93 1687.00 L 1230.93 1696.00 L 1232.89 1701.00 L 1237.16 1706.00 L 1243.00 1709.04 L 1251.00 1710.16 L 1258.00 1709.22 L 1262.65 1707.00 L 1266.12 1704.00 L 1269.40 1698.00 L 1271.07 1687.00 L 1271.05 1641.00 L 1272.00 1640.20 L 1286.94 1641.00 L 1286.38 1697.00 L 1282.37 1708.00 L 1274.00 1717.40 L 1267.00 1721.35 L 1259.00 1723.47 L 1242.00 1723.47 L 1230.00 1719.50 L 1220.47 1711.00 L 1216.67 1704.00 L 1214.44 1695.00 L 1214.04 1641.00 Z
M 1298.27 1641.00 L 1368.59 1641.00 L 1368.00 1653.66 L 1341.53 1654.00 L 1341.00 1722.74 L 1326.00 1722.91 L 1325.24 1722.00 L 1325.18 1654.00 L 1299.00 1653.67 L 1298.27 1641.00 Z
M 879.00 1652.95 L 867.00 1656.75 L 858.94 1664.00 L 853.87 1676.00 L 853.73 1686.00 L 857.00 1695.82 L 864.00 1704.24 L 874.00 1709.19 L 885.00 1710.10 L 895.81 1707.00 L 905.15 1699.00 L 909.32 1690.00 L 910.22 1678.00 L 907.22 1668.00 L 900.00 1658.95 L 890.00 1653.88 L 879.00 1652.95 Z
M 1152.00 1652.93 L 1141.00 1655.77 L 1131.79 1663.00 L 1126.65 1673.00 L 1125.72 1686.00 L 1128.92 1696.00 L 1135.60 1704.00 L 1146.00 1709.22 L 1156.00 1710.16 L 1167.00 1707.20 L 1175.04 1701.00 L 1180.25 1692.00 L 1181.78 1681.00 L 1179.24 1669.00 L 1172.73 1660.00 L 1164.00 1654.69 L 1152.00 1652.93 Z
M 698.00 1653.08 L 698.00 1675.01 L 727.00 1674.29 L 732.00 1672.08 L 735.02 1668.00 L 735.08 1660.00 L 730.00 1654.87 L 722.00 1653.13 L 698.00 1653.08 Z
M 698.00 1687.36 L 698.00 1710.13 L 730.00 1709.35 L 735.54 1707.00 L 739.20 1702.00 L 739.42 1697.00 L 738.13 1693.00 L 733.00 1688.93 L 724.00 1687.43 L 698.00 1687.36 Z
"/>
</svg>

Before

Width:  |  Height:  |  Size: 11 KiB

@@ -1,27 +0,0 @@
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 2048 2048" width="2048" height="2048">
<path fill="#1F2328" fill-rule="evenodd" d="
M 1007.80 360.00 L 1012.00 359.48 L 1016.00 360.52 L 1422.00 546.61 L 1431.27 553.00 L 1433.75 560.00 L 1433.55 793.00 L 1445.33 825.00 L 1452.48 853.00 L 1457.47 884.00 L 1459.21 914.00 L 1457.29 953.00 L 1452.35 983.00 L 1443.49 1015.00 L 1430.36 1048.00 L 1415.39 1076.00 L 1396.36 1103.00 L 1378.00 1123.95 L 1355.00 1145.28 L 1339.00 1157.45 L 1322.00 1168.42 L 1287.00 1185.43 L 1267.00 1192.34 L 1241.00 1198.42 L 1219.00 1201.36 L 1192.00 1202.67 L 1028.00 1277.39 L 1018.00 1280.48 L 1007.00 1277.32 L 599.00 1093.51 L 591.00 1087.94 L 588.52 1083.00 L 588.17 1079.00 L 588.27 561.00 L 589.52 555.00 L 593.00 550.82 L 598.00 547.79 L 1007.80 360.00 Z
M 1009.00 386.77 L 632.41 560.00 L 734.00 607.14 L 804.00 638.85 L 834.00 622.82 L 861.00 610.63 L 891.00 599.45 L 922.00 590.57 L 945.00 585.54 L 978.00 580.63 L 1008.00 578.46 L 1044.00 578.60 L 1069.00 580.59 L 1096.00 584.45 L 1139.00 594.51 L 1182.00 610.52 L 1229.00 633.75 L 1389.68 560.00 L 1013.00 386.73 L 1009.00 386.77 Z
M 614.00 579.79 L 613.56 1072.00 L 1004.00 1248.64 L 1004.40 1047.00 L 977.00 1033.47 L 957.00 1021.26 L 933.00 1003.43 L 911.00 984.24 L 892.89 966.00 L 871.71 941.00 L 853.80 916.00 L 838.59 891.00 L 823.56 861.00 L 813.64 837.00 L 803.60 806.00 L 796.61 777.00 L 790.58 740.00 L 788.78 716.00 L 788.59 687.00 L 790.55 661.00 L 614.00 579.79 Z
M 1407.00 579.85 L 1267.81 644.00 L 1292.00 652.56 L 1308.00 659.60 L 1328.00 670.58 L 1346.00 682.66 L 1361.00 694.57 L 1378.28 711.00 L 1393.42 728.00 L 1408.00 747.97 L 1408.54 580.00 L 1407.00 579.85 Z
M 1018.00 602.98 L 970.00 606.85 L 929.00 614.82 L 886.00 627.87 L 847.00 644.71 L 816.00 661.87 L 813.73 701.00 L 816.78 747.00 L 819.75 766.00 L 826.87 797.00 L 835.82 825.00 L 844.90 848.00 L 854.93 869.00 L 867.82 892.00 L 881.75 913.00 L 897.82 934.00 L 914.00 952.39 L 932.00 970.11 L 951.00 986.21 L 972.00 1001.25 L 992.00 1013.07 L 1017.00 1025.30 L 1028.38 1018.00 L 1024.55 986.00 L 1023.39 948.00 L 1025.62 913.00 L 1030.51 879.00 L 1035.78 855.00 L 1044.73 825.00 L 1060.70 786.00 L 1073.71 761.00 L 1086.57 740.00 L 1103.68 717.00 L 1120.00 699.10 L 1139.00 681.53 L 1155.00 669.55 L 1176.00 656.64 L 1198.50 646.00 L 1183.00 637.87 L 1157.00 626.93 L 1114.00 613.85 L 1070.00 605.78 L 1048.00 603.69 L 1018.00 602.98 Z
M 1218.00 665.78 L 1199.00 672.95 L 1180.00 682.95 L 1164.00 693.77 L 1146.00 708.76 L 1130.88 724.00 L 1113.85 745.00 L 1099.95 766.00 L 1086.66 790.00 L 1075.81 814.00 L 1066.87 838.00 L 1058.91 866.00 L 1053.91 890.00 L 1050.00 919.00 L 1048.54 945.00 L 1048.93 976.00 L 1052.00 1002.65 L 1069.00 989.18 L 1091.00 969.25 L 1125.09 932.00 L 1154.21 892.00 L 1180.06 846.00 L 1190.18 824.00 L 1200.16 798.00 L 1208.11 772.00 L 1214.17 746.00 L 1218.21 720.00 L 1220.02 695.00 L 1219.68 666.00 L 1218.00 665.78 Z
M 1245.00 665.24 L 1244.28 710.00 L 1241.48 735.00 L 1236.35 763.00 L 1229.42 789.00 L 1221.39 813.00 L 1200.49 861.00 L 1186.34 887.00 L 1173.26 908.00 L 1156.44 932.00 L 1136.38 957.00 L 1117.18 978.00 L 1097.00 997.53 L 1056.52 1031.00 L 1062.91 1056.00 L 1073.91 1085.00 L 1086.92 1109.00 L 1102.98 1131.00 L 1121.00 1149.18 L 1139.00 1162.19 L 1161.00 1172.11 L 1180.00 1176.09 L 1200.00 1176.92 L 1226.00 1175.09 L 1248.00 1171.11 L 1272.00 1164.24 L 1294.00 1155.09 L 1312.00 1145.07 L 1332.00 1131.26 L 1348.00 1118.09 L 1363.08 1103.00 L 1379.04 1084.00 L 1392.23 1065.00 L 1404.07 1044.00 L 1413.22 1024.00 L 1422.31 998.00 L 1428.21 974.00 L 1432.07 950.00 L 1434.06 920.00 L 1433.27 896.00 L 1430.26 871.00 L 1426.03 850.00 L 1420.22 829.00 L 1411.23 805.00 L 1401.00 784.25 L 1389.00 764.05 L 1375.19 745.00 L 1361.00 728.84 L 1344.00 712.85 L 1326.00 698.90 L 1308.00 687.77 L 1285.00 676.80 L 1262.00 668.90 L 1245.00 665.24 Z
M 1033.00 1045.52 L 1029.91 1048.00 L 1029.89 1248.00 L 1031.00 1248.49 L 1148.68 1195.00 L 1148.57 1194.00 L 1126.00 1183.37 L 1110.00 1172.39 L 1096.00 1159.97 L 1079.81 1142.00 L 1065.67 1122.00 L 1051.64 1096.00 L 1043.54 1077.00 L 1034.11 1046.00 L 1033.00 1045.52 Z
M 793.92 1361.00 L 813.00 1360.49 L 827.00 1361.68 L 842.00 1364.55 L 860.00 1370.64 L 874.00 1377.75 L 887.00 1386.68 L 896.00 1395.03 L 903.47 1404.00 L 863.00 1440.80 L 844.00 1424.94 L 829.00 1417.86 L 820.00 1415.70 L 808.00 1414.69 L 795.00 1415.71 L 786.00 1417.88 L 773.00 1423.82 L 763.69 1431.00 L 754.79 1442.00 L 748.85 1454.00 L 745.84 1465.00 L 744.68 1475.00 L 744.89 1485.00 L 747.87 1500.00 L 750.92 1508.00 L 756.86 1518.00 L 764.00 1526.02 L 773.00 1533.12 L 785.15 1539.00 L 798.00 1542.16 L 812.00 1542.66 L 824.00 1541.24 L 834.00 1538.02 L 843.00 1533.28 L 864.00 1516.18 L 903.83 1552.00 L 891.88 1566.00 L 875.00 1579.47 L 854.00 1589.49 L 831.00 1595.35 L 805.00 1596.91 L 791.00 1596.37 L 777.00 1594.31 L 753.00 1587.30 L 732.00 1576.40 L 714.00 1561.78 L 698.61 1543.00 L 687.50 1521.00 L 681.55 1499.00 L 680.23 1485.00 L 680.44 1467.00 L 683.68 1447.00 L 691.59 1426.00 L 703.57 1407.00 L 713.00 1396.47 L 723.00 1387.59 L 744.00 1374.43 L 767.00 1365.55 L 793.92 1361.00 Z
M 1009.33 1361.00 L 1037.00 1360.64 L 1065.00 1364.54 L 1091.00 1372.63 L 1104.00 1378.86 L 1107.85 1382.00 L 1087.00 1428.89 L 1074.00 1421.80 L 1061.00 1416.72 L 1046.00 1412.77 L 1030.00 1410.80 L 1018.00 1410.95 L 1003.00 1413.87 L 993.45 1419.00 L 988.92 1425.00 L 987.86 1431.00 L 989.00 1435.90 L 992.07 1440.00 L 998.00 1444.02 L 1013.00 1449.22 L 1066.00 1461.77 L 1079.00 1466.69 L 1092.00 1473.66 L 1101.00 1480.88 L 1107.31 1488.00 L 1112.53 1497.00 L 1115.50 1505.00 L 1117.84 1519.00 L 1117.28 1534.00 L 1112.32 1551.00 L 1104.51 1564.00 L 1092.00 1576.38 L 1079.00 1584.54 L 1062.00 1591.23 L 1043.00 1595.39 L 1029.00 1596.66 L 1005.00 1596.67 L 977.00 1593.24 L 958.00 1588.46 L 941.00 1582.38 L 926.00 1574.48 L 919.60 1569.00 L 942.00 1522.53 L 958.00 1532.03 L 972.00 1538.22 L 989.00 1543.27 L 1008.00 1546.12 L 1025.00 1546.14 L 1038.00 1544.04 L 1047.00 1540.19 L 1053.00 1532.91 L 1053.58 1527.00 L 1052.00 1521.34 L 1048.00 1516.65 L 1042.00 1512.87 L 1027.00 1507.71 L 977.00 1495.53 L 961.00 1489.42 L 950.00 1483.44 L 941.00 1476.46 L 933.57 1468.00 L 928.70 1459.00 L 925.56 1449.00 L 923.90 1433.00 L 926.55 1415.00 L 932.62 1401.00 L 941.71 1389.00 L 954.00 1378.64 L 969.00 1370.59 L 991.00 1363.62 L 1009.33 1361.00 Z
M 1252.59 1361.00 L 1272.00 1360.40 L 1288.00 1361.65 L 1303.00 1364.52 L 1316.00 1368.54 L 1330.00 1374.59 L 1344.00 1382.77 L 1354.00 1390.52 L 1364.88 1402.00 L 1365.00 1403.00 L 1325.00 1439.53 L 1306.00 1424.87 L 1296.00 1419.90 L 1286.00 1416.79 L 1264.00 1414.79 L 1254.00 1415.93 L 1243.00 1418.82 L 1229.00 1425.89 L 1218.72 1435.00 L 1211.00 1446.21 L 1205.89 1459.00 L 1203.79 1471.00 L 1203.89 1486.00 L 1206.95 1501.00 L 1212.86 1514.00 L 1221.67 1525.00 L 1232.00 1533.30 L 1244.00 1539.17 L 1258.00 1542.35 L 1271.00 1542.83 L 1285.00 1541.27 L 1295.00 1538.22 L 1302.42 1534.00 L 1303.00 1473.20 L 1360.00 1473.22 L 1361.69 1474.00 L 1361.57 1568.00 L 1337.00 1582.25 L 1308.00 1592.34 L 1283.00 1596.40 L 1251.00 1596.37 L 1225.00 1591.34 L 1202.00 1582.36 L 1183.00 1570.40 L 1172.44 1561.00 L 1163.61 1551.00 L 1150.46 1530.00 L 1142.63 1509.00 L 1139.65 1493.00 L 1138.89 1480.00 L 1140.57 1457.00 L 1147.58 1433.00 L 1160.70 1410.00 L 1169.85 1399.00 L 1180.00 1389.51 L 1199.00 1376.71 L 1212.00 1370.54 L 1226.00 1365.71 L 1252.59 1361.00 Z
M 872.55 1640.00 L 889.00 1639.48 L 898.00 1641.63 L 905.00 1644.81 L 912.00 1649.59 L 917.38 1655.00 L 922.47 1663.00 L 925.27 1671.00 L 926.24 1687.00 L 924.27 1696.00 L 920.57 1704.00 L 910.00 1715.34 L 903.00 1719.56 L 895.00 1722.52 L 887.00 1723.69 L 874.00 1723.43 L 861.00 1719.54 L 854.00 1715.31 L 846.35 1708.00 L 841.69 1701.00 L 838.69 1693.00 L 837.45 1686.00 L 837.59 1676.00 L 839.73 1667.00 L 843.57 1659.00 L 849.30 1652.00 L 857.00 1645.71 L 866.00 1641.56 L 872.55 1640.00 Z
M 972.42 1640.00 L 988.00 1639.41 L 998.00 1641.55 L 1008.00 1646.75 L 1014.71 1653.00 L 1005.00 1662.77 L 997.00 1656.64 L 990.00 1653.82 L 982.00 1652.92 L 974.00 1653.87 L 967.00 1656.85 L 961.67 1661.00 L 957.69 1666.00 L 954.73 1673.00 L 953.73 1684.00 L 955.81 1693.00 L 959.87 1700.00 L 967.00 1706.04 L 975.00 1709.28 L 985.70 1710.00 L 996.00 1707.14 L 1005.00 1700.23 L 1014.49 1709.00 L 1014.26 1711.00 L 1008.00 1716.38 L 995.00 1722.50 L 975.00 1723.52 L 961.00 1719.46 L 953.00 1714.44 L 946.48 1708.00 L 939.56 1695.00 L 937.72 1686.00 L 937.71 1677.00 L 939.51 1668.00 L 943.50 1659.00 L 949.09 1652.00 L 957.00 1645.69 L 966.00 1641.54 L 972.42 1640.00 Z
M 1144.51 1640.00 L 1161.00 1639.55 L 1176.00 1644.50 L 1182.00 1648.41 L 1189.00 1655.23 L 1192.46 1660.00 L 1196.43 1669.00 L 1197.92 1686.00 L 1196.33 1695.00 L 1193.41 1702.00 L 1189.40 1708.00 L 1183.00 1714.25 L 1175.00 1719.37 L 1167.00 1722.41 L 1159.00 1723.67 L 1145.00 1723.43 L 1132.00 1719.22 L 1124.19 1714.00 L 1118.00 1707.70 L 1113.62 1701.00 L 1110.60 1693.00 L 1109.37 1686.00 L 1109.51 1676.00 L 1111.65 1667.00 L 1115.53 1659.00 L 1121.29 1652.00 L 1128.00 1646.44 L 1135.00 1642.67 L 1144.51 1640.00 Z
M 681.39 1641.00 L 729.00 1640.59 L 739.00 1643.45 L 746.57 1649.00 L 750.36 1655.00 L 751.46 1665.00 L 748.33 1674.00 L 741.84 1680.00 L 749.00 1684.66 L 752.56 1689.00 L 755.54 1699.00 L 754.43 1708.00 L 748.77 1716.00 L 741.00 1720.45 L 733.00 1722.38 L 682.00 1722.73 L 681.39 1641.00 Z
M 771.82 1641.00 L 787.00 1640.22 L 788.25 1641.00 L 788.14 1709.00 L 830.00 1709.42 L 831.38 1711.00 L 831.00 1722.58 L 772.00 1722.15 L 771.82 1641.00 Z
M 1029.87 1641.00 L 1043.00 1640.22 L 1045.91 1641.00 L 1046.00 1681.38 L 1085.00 1640.81 L 1103.00 1640.46 L 1069.70 1678.00 L 1105.98 1722.00 L 1088.00 1722.70 L 1059.00 1688.70 L 1045.92 1703.00 L 1045.98 1722.00 L 1045.00 1722.85 L 1030.00 1722.11 L 1029.87 1641.00 Z
M 1214.04 1641.00 L 1229.96 1641.00 L 1229.93 1687.00 L 1230.93 1696.00 L 1232.89 1701.00 L 1237.16 1706.00 L 1243.00 1709.04 L 1251.00 1710.16 L 1258.00 1709.22 L 1262.65 1707.00 L 1266.12 1704.00 L 1269.40 1698.00 L 1271.07 1687.00 L 1271.05 1641.00 L 1272.00 1640.20 L 1286.94 1641.00 L 1286.38 1697.00 L 1282.37 1708.00 L 1274.00 1717.40 L 1267.00 1721.35 L 1259.00 1723.47 L 1242.00 1723.47 L 1230.00 1719.50 L 1220.47 1711.00 L 1216.67 1704.00 L 1214.44 1695.00 L 1214.04 1641.00 Z
M 1298.27 1641.00 L 1368.59 1641.00 L 1368.00 1653.66 L 1341.53 1654.00 L 1341.00 1722.74 L 1326.00 1722.91 L 1325.24 1722.00 L 1325.18 1654.00 L 1299.00 1653.67 L 1298.27 1641.00 Z
M 879.00 1652.95 L 867.00 1656.75 L 858.94 1664.00 L 853.87 1676.00 L 853.73 1686.00 L 857.00 1695.82 L 864.00 1704.24 L 874.00 1709.19 L 885.00 1710.10 L 895.81 1707.00 L 905.15 1699.00 L 909.32 1690.00 L 910.22 1678.00 L 907.22 1668.00 L 900.00 1658.95 L 890.00 1653.88 L 879.00 1652.95 Z
M 1152.00 1652.93 L 1141.00 1655.77 L 1131.79 1663.00 L 1126.65 1673.00 L 1125.72 1686.00 L 1128.92 1696.00 L 1135.60 1704.00 L 1146.00 1709.22 L 1156.00 1710.16 L 1167.00 1707.20 L 1175.04 1701.00 L 1180.25 1692.00 L 1181.78 1681.00 L 1179.24 1669.00 L 1172.73 1660.00 L 1164.00 1654.69 L 1152.00 1652.93 Z
M 698.00 1653.08 L 698.00 1675.01 L 727.00 1674.29 L 732.00 1672.08 L 735.02 1668.00 L 735.08 1660.00 L 730.00 1654.87 L 722.00 1653.13 L 698.00 1653.08 Z
M 698.00 1687.36 L 698.00 1710.13 L 730.00 1709.35 L 735.54 1707.00 L 739.20 1702.00 L 739.42 1697.00 L 738.13 1693.00 L 733.00 1688.93 L 724.00 1687.43 L 698.00 1687.36 Z
"/>
</svg>
@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M13.5 2.5a1.8 1.8 0 0 0-2.5 0L6.5 7 5 11l4-1.5 4.5-4.5a1.8 1.8 0 0 0 0-2.5z" fill="#4fa3ff" stroke="#ffffff" stroke-width="0.8"/><path d="M3 13c0-1 1-1.5 2-1.5s1 1.5 0 2.5-2 0-2-1z" fill="#ffb84d"/></svg>

Before

Width:  |  Height:  |  Size: 297 B

-1
View File
@@ -1 +0,0 @@
<svg height="16" width="16" viewBox="0 0 16 16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="m8 2 6 3v6l-6 3-6-3V5zm0 12V8l6-3M8 8 2 5" fill="none" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg>

Before

Width:  |  Height:  |  Size: 467 B

-4
View File
@@ -1,4 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<circle cx="24" cy="24" r="10" stroke="#fc7f7f" stroke-width="4"/>
<circle cx="24" cy="24" r="14" stroke="#fc7f7f" stroke-width="4" stroke-dasharray="8 2"/>
</svg>

Before

Width:  |  Height:  |  Size: 260 B

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://clgooji83dl4u"
path="res://.godot/imported/config.svg-cae4a0c2d7092559e4507bafae59a8c8.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/config.svg"
dest_files=["res://.godot/imported/config.svg-cae4a0c2d7092559e4507bafae59a8c8.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="M2 4v8a6 2 0 0 0 12 0V4A6 2 0 0 0 2 4a6 2 0 0 0 12 0" stroke-width="2" fill="none" stroke="#fc7f7f" mask="url(#a)"/></svg>

Before

Width:  |  Height:  |  Size: 478 B

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://dioxt3oaqvsi3"
path="res://.godot/imported/cyliner.svg-2b72d5266e00b821305c0886f3c39fcb.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/cyliner.svg"
dest_files=["res://.godot/imported/cyliner.svg-2b72d5266e00b821305c0886f3c39fcb.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><linearGradient x2="0" y2="16" gradientUnits="userSpaceOnUse" id="a"><stop offset=".1875" stop-color="#ff4545"/><stop stop-color="#ffe345"/><stop offset=".3125" stop-color="#ffe345"/><stop stop-color="#80ff45"/><stop offset=".4375" stop-color="#80ff45"/><stop stop-color="#45ffa2"/><stop offset=".5625" stop-color="#45ffa2"/><stop stop-color="#45d7ff"/><stop offset=".6875" stop-color="#45d7ff"/><stop stop-color="#8045ff"/><stop offset=".8125" stop-color="#8045ff"/><stop stop-color="#ff4596"/></linearGradient><path d="m8 2 6 3v6l-6 3-6-3V5zm0 12V8l6-3M8 8 2 5" fill="none" stroke-width="2" stroke-linejoin="round" stroke="url(#a)"/></svg>

Before

Width:  |  Height:  |  Size: 724 B

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cg5jbylrlg00x"
path="res://.godot/imported/empty-material.svg-7948212fff688817e399f2cd00fa34b7.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/empty-material.svg"
dest_files=["res://.godot/imported/empty-material.svg-7948212fff688817e399f2cd00fa34b7.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="14" height="14" rx="2" fill="#1e2024" stroke="#3c4048" stroke-width="1"/><rect x="2" y="2" width="6" height="6" fill="#505050"/><rect x="8" y="2" width="6" height="6" fill="#303030"/><rect x="2" y="8" width="6" height="6" fill="#303030"/><rect x="8" y="8" width="6" height="6" fill="#505050"/><line x1="8" y1="2" x2="8" y2="14" stroke="#808080" stroke-width="0.8"/><line x1="2" y1="8" x2="14" y2="8" stroke="#808080" stroke-width="0.8"/></svg>

Before

Width:  |  Height:  |  Size: 552 B

@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="14" height="14" rx="2" fill="#2d3139" stroke="#4f5460" stroke-width="1"/><rect x="2" y="2" width="6" height="6" fill="#e0e0e0"/><rect x="8" y="2" width="6" height="6" fill="#b0b0b0"/><rect x="2" y="8" width="6" height="6" fill="#b0b0b0"/><rect x="8" y="8" width="6" height="6" fill="#e0e0e0"/><line x1="8" y1="2" x2="8" y2="14" stroke="#404040" stroke-width="0.8"/><line x1="2" y1="8" x2="14" y2="8" stroke="#404040" stroke-width="0.8"/></svg>

Before

Width:  |  Height:  |  Size: 552 B

@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="14" height="14" rx="2" fill="#2d3139" stroke="#4f5460" stroke-width="1"/><rect x="2" y="2" width="6" height="6" fill="#f38b2a"/><rect x="8" y="2" width="6" height="6" fill="#cf6815"/><rect x="2" y="8" width="6" height="6" fill="#cf6815"/><rect x="8" y="8" width="6" height="6" fill="#f38b2a"/><line x1="8" y1="2" x2="8" y2="14" stroke="#4a2205" stroke-width="0.8"/><line x1="2" y1="8" x2="14" y2="8" stroke="#4a2205" stroke-width="0.8"/></svg>

Before

Width:  |  Height:  |  Size: 552 B

@@ -1,4 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M22.5 16.204C19.8099 17.7602 18 20.6687 18 24C18 27.3313 19.8099 30.2398 22.5 31.796C25.1901 30.2398 27 27.3313 27 24C27 20.6687 25.1901 17.7602 22.5 16.204Z" fill="#fc7f7f"/>
<path d="M22.5 16.204C19.8099 17.7602 18 20.6687 18 24C18 27.3313 19.8099 30.2398 22.5 31.796M22.5 16.204C23.8238 15.4383 25.3607 15 27 15C31.9706 15 36 19.0294 36 24C36 28.9706 31.9706 33 27 33C25.3607 33 23.8238 32.5617 22.5 31.796M22.5 16.204C21.1762 15.4383 19.6393 15 18 15C13.0294 15 9 19.0294 9 24C9 28.9706 13.0294 33 18 33C19.6393 33 21.1762 32.5617 22.5 31.796M22.5 16.204C25.1901 17.7602 27 20.6687 27 24C27 27.3313 25.1901 30.2398 22.5 31.796" stroke="#fc7f7f"/>
</svg>

Before

Width:  |  Height:  |  Size: 763 B

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://2kohi3tb3c70"
path="res://.godot/imported/intersection.svg-23de06eefb1905769cebbb8391355d9d.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/intersection.svg"
dest_files=["res://.godot/imported/intersection.svg-23de06eefb1905769cebbb8391355d9d.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><rect x="1" y="1" width="14" height="14" rx="2" fill="#2d3139" stroke="#4f5460" stroke-width="1"/><rect x="3" y="3" width="10" height="10" rx="1" fill="#dedede"/><line x1="3" y1="13" x2="13" y2="3" stroke="#e04040" stroke-width="1.5" stroke-linecap="round"/></svg>

Before

Width:  |  Height:  |  Size: 348 B

@@ -1,43 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d17t4wrfhkcam"
path="res://.godot/imported/material_none.svg-de44d0630e04dfe179cf633ad48aad32.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/material_none.svg"
dest_files=["res://.godot/imported/material_none.svg-de44d0630e04dfe179cf633ad48aad32.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
-1
View File
@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="M4.73 2A2 2 0 1 0 2 4.73v6.541A2 2 0 1 0 4.729 14H8v-2H4.729A2 2 0 0 0 4 11.271V5.415l4.914 4.916A2 2 0 0 1 9.998 10a2 2 0 0 1 .33-1.084L5.414 4h5.856a2 2 0 0 0 .73.729V8h2V4.729A2 2 0 1 0 11.27 2z" fill="#fc7f7f"/><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/></svg>

Before

Width:  |  Height:  |  Size: 459 B

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bnpu878eanspj"
path="res://.godot/imported/mesh.svg-6e4ea25a7496670b06893bddb95efc00.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/mesh.svg"
dest_files=["res://.godot/imported/mesh.svg-6e4ea25a7496670b06893bddb95efc00.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><path d="m8 2 6 3.5v5L8 14l-6-3.5v-5h6zm6 3.5L8 9 2 5.5M8 9v5" fill="none" stroke-linejoin="round" stroke-width="2" stroke="#fc7f7f"/></svg>

Before

Width:  |  Height:  |  Size: 223 B

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cjps2pofcsfc0"
path="res://.godot/imported/polygon.svg-2282e5cff5d8681594f5d2ce9db7c894.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/polygon.svg"
dest_files=["res://.godot/imported/polygon.svg-2282e5cff5d8681594f5d2ce9db7c894.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
-1
View File
@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><path d="M8 2a6 6 0 0 0 0 12A6 6 0 0 0 8 2v12M2.05 7.4a6 2 0 0 0 11.9 0" fill="none" stroke-width="2" stroke="#fc7f7f" mask="url(#a)"/></svg>

Before

Width:  |  Height:  |  Size: 488 B

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://kpld1ou63wlf"
path="res://.godot/imported/sphere.svg-92f5ae8e727fa746370450cc9a75902e.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/sphere.svg"
dest_files=["res://.godot/imported/sphere.svg-92f5ae8e727fa746370450cc9a75902e.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
@@ -1,4 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 24C9 28.9706 13.0294 33 18 33C19.6393 33 21.1762 32.5617 22.5 31.796C19.8099 30.2398 18 27.3313 18 24C18 20.6687 19.8099 17.7602 22.5 16.204C21.1762 15.4383 19.6393 15 18 15C13.0294 15 9 19.0294 9 24Z" fill="#fc7f7f"/>
<path d="M22.5 16.204C19.8099 17.7602 18 20.6687 18 24C18 27.3313 19.8099 30.2398 22.5 31.796M22.5 16.204C23.8238 15.4383 25.3607 15 27 15C31.9706 15 36 19.0294 36 24C36 28.9706 31.9706 33 27 33C25.3607 33 23.8238 32.5617 22.5 31.796M22.5 16.204C21.1762 15.4383 19.6393 15 18 15C13.0294 15 9 19.0294 9 24C9 28.9706 13.0294 33 18 33C19.6393 33 21.1762 32.5617 22.5 31.796M22.5 16.204C25.1901 17.7602 27 20.6687 27 24C27 27.3313 25.1901 30.2398 22.5 31.796" stroke="#fc7f7f" stroke-width="2"/>
</svg>

Before

Width:  |  Height:  |  Size: 826 B

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://snbhkpq6sh4j"
path="res://.godot/imported/subtraction.svg-e5717404f3625d7cf3717f6169d524f2.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/subtraction.svg"
dest_files=["res://.godot/imported/subtraction.svg-e5717404f3625d7cf3717f6169d524f2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
-1
View File
@@ -1 +0,0 @@
<svg height="16" viewBox="0 0 16 16" width="16" xmlns="http://www.w3.org/2000/svg"><mask id="a"><path d="M0 0h16v10a2 2 0 0 0-2-2h-2a2 2 0 0 0-2 2 2 2 0 0 0-2 2v2a2 2 0 0 0 2 2H0z" fill="#fff"/></mask><path d="M12 9a1 1 0 0 0-1 1v1h2v2h1a1 1 0 0 0 1-1v-2a1 1 0 0 0-1-1zm1 4h-2v-2h-1a1 1 0 0 0-1 1v2a1 1 0 0 0 1 1h2a1 1 0 0 0 1-1z" fill="#5fb2ff"/><ellipse cx="8" cy="7.5" fill="none" rx="6" ry="3.5" stroke="#fc7f7f" stroke-width="2" mask="url(#a)"/></svg>

Before

Width:  |  Height:  |  Size: 456 B

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://gbgmyv4bknqo"
path="res://.godot/imported/torus.svg-0e939567d2a65f43cdab88e1c075c1b5.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/torus.svg"
dest_files=["res://.godot/imported/torus.svg-0e939567d2a65f43cdab88e1c075c1b5.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
-6
View File
@@ -1,6 +0,0 @@
<svg width="48" height="48" viewBox="0 0 48 48" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M9 24C9 28.9706 13.0294 33 18 33C19.6393 33 21.1762 32.5617 22.5 31.796C19.8099 30.2398 18 27.3313 18 24C18 20.6687 19.8099 17.7602 22.5 16.204C21.1762 15.4383 19.6393 15 18 15C13.0294 15 9 19.0294 9 24Z" fill="#fc7f7f"/>
<path d="M27 33C31.9706 33 36 28.9706 36 24C36 19.0294 31.9706 15 27 15C25.3607 15 23.8238 15.4383 22.5 16.204C25.1901 17.7602 27 20.6687 27 24C27 27.3313 25.1901 30.2398 22.5 31.796C23.8238 32.5617 25.3607 33 27 33Z" fill="#fc7f7f"/>
<path d="M22.5 16.204C19.8099 17.7602 18 20.6687 18 24C18 27.3313 19.8099 30.2398 22.5 31.796C25.1901 30.2398 27 27.3313 27 24C27 20.6687 25.1901 17.7602 22.5 16.204Z" fill="#fc7f7f"/>
<path d="M22.5 16.204C19.8099 17.7602 18 20.6687 18 24C18 27.3313 19.8099 30.2398 22.5 31.796M22.5 16.204C23.8238 15.4383 25.3607 15 27 15C31.9706 15 36 19.0294 36 24C36 28.9706 31.9706 33 27 33C25.3607 33 23.8238 32.5617 22.5 31.796M22.5 16.204C21.1762 15.4383 19.6393 15 18 15C13.0294 15 9 19.0294 9 24C9 28.9706 13.0294 33 18 33C19.6393 33 21.1762 32.5617 22.5 31.796M22.5 16.204C25.1901 17.7602 27 20.6687 27 24C27 27.3313 25.1901 30.2398 22.5 31.796" fill="#fc7f7f" />
</svg>

Before

Width:  |  Height:  |  Size: 1.2 KiB

@@ -1,44 +0,0 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://sc7itwu80oi3"
path="res://.godot/imported/union.svg-5e21336011bc768248beb944535aae6b.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/union.svg"
dest_files=["res://.godot/imported/union.svg-5e21336011bc768248beb944535aae6b.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
@@ -1,20 +0,0 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3]
[ext_resource type="Shader" path="res://addons/csg_blockout/res/shaders/grid_triplanar.gdshader" id="1_shader"]
[resource]
render_priority = 0
shader = ExtResource("1_shader")
shader_parameter/color_a = Color(0.28, 0.28, 0.28, 1)
shader_parameter/color_b = Color(0.2, 0.2, 0.2, 1)
shader_parameter/grid_color = Color(0.6, 0.6, 0.6, 0.8)
shader_parameter/sub_grid_color = Color(0.4, 0.4, 0.4, 0.4)
shader_parameter/grid_scale = 1.0
shader_parameter/sub_grid_divisions = 2.0
shader_parameter/line_thickness = 0.02
shader_parameter/sub_line_thickness = 0.01
shader_parameter/triplanar_sharpness = 8.0
shader_parameter/use_checker = true
shader_parameter/world_aligned = true
shader_parameter/roughness = 0.75
shader_parameter/metallic = 0.0
@@ -1,20 +0,0 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3]
[ext_resource type="Shader" path="res://addons/csg_blockout/res/shaders/grid_triplanar.gdshader" id="1_shader"]
[resource]
render_priority = 0
shader = ExtResource("1_shader")
shader_parameter/color_a = Color(0.88, 0.88, 0.88, 1)
shader_parameter/color_b = Color(0.75, 0.75, 0.75, 1)
shader_parameter/grid_color = Color(0.25, 0.25, 0.25, 0.9)
shader_parameter/sub_grid_color = Color(0.5, 0.5, 0.5, 0.5)
shader_parameter/grid_scale = 1.0
shader_parameter/sub_grid_divisions = 2.0
shader_parameter/line_thickness = 0.02
shader_parameter/sub_line_thickness = 0.01
shader_parameter/triplanar_sharpness = 8.0
shader_parameter/use_checker = true
shader_parameter/world_aligned = true
shader_parameter/roughness = 0.75
shader_parameter/metallic = 0.0
@@ -1,20 +0,0 @@
[gd_resource type="ShaderMaterial" load_steps=2 format=3]
[ext_resource type="Shader" path="res://addons/csg_blockout/res/shaders/grid_triplanar.gdshader" id="1_shader"]
[resource]
render_priority = 0
shader = ExtResource("1_shader")
shader_parameter/color_a = Color(0.92, 0.52, 0.15, 1)
shader_parameter/color_b = Color(0.82, 0.44, 0.1, 1)
shader_parameter/grid_color = Color(0.2, 0.1, 0.05, 0.9)
shader_parameter/sub_grid_color = Color(0.4, 0.2, 0.1, 0.4)
shader_parameter/grid_scale = 1.0
shader_parameter/sub_grid_divisions = 2.0
shader_parameter/line_thickness = 0.02
shader_parameter/sub_line_thickness = 0.01
shader_parameter/triplanar_sharpness = 8.0
shader_parameter/use_checker = true
shader_parameter/world_aligned = true
shader_parameter/roughness = 0.75
shader_parameter/metallic = 0.0
@@ -1,3 +0,0 @@
[gd_resource type="ButtonGroup" format=3 uid="uid://wxtkg1wlxka8"]
[resource]
@@ -1,87 +0,0 @@
shader_type spatial;
render_mode blend_mix, depth_draw_opaque, cull_back, diffuse_burley, specular_schlick_ggx;
group_uniforms GridColors;
uniform vec4 color_a : source_color = vec4(0.85, 0.85, 0.85, 1.0);
uniform vec4 color_b : source_color = vec4(0.72, 0.72, 0.72, 1.0);
uniform vec4 grid_color : source_color = vec4(0.2, 0.2, 0.2, 0.9);
uniform vec4 sub_grid_color : source_color = vec4(0.45, 0.45, 0.45, 0.5);
group_uniforms GridSettings;
uniform float grid_scale : hint_range(0.1, 10.0, 0.1) = 1.0;
uniform float sub_grid_divisions : hint_range(1.0, 10.0, 1.0) = 2.0;
uniform float line_thickness : hint_range(0.001, 0.1, 0.001) = 0.02;
uniform float sub_line_thickness : hint_range(0.001, 0.05, 0.001) = 0.01;
uniform float triplanar_sharpness : hint_range(1.0, 32.0, 0.5) = 8.0;
uniform bool use_checker = true;
uniform bool world_aligned = true;
group_uniforms MaterialProperties;
uniform float roughness : hint_range(0.0, 1.0, 0.05) = 0.75;
uniform float metallic : hint_range(0.0, 1.0, 0.05) = 0.0;
varying vec3 v_world_pos;
varying vec3 v_world_normal;
varying vec3 v_local_pos;
varying vec3 v_local_normal;
void vertex() {
v_world_pos = (MODEL_MATRIX * vec4(VERTEX, 1.0)).xyz;
v_world_normal = normalize((MODEL_MATRIX * vec4(NORMAL, 0.0)).xyz);
v_local_pos = VERTEX;
v_local_normal = NORMAL;
}
float get_grid_line(vec2 uv_pos, float thickness) {
vec2 grid_uv = abs(fract(uv_pos - 0.5) - 0.5);
vec2 fw = fwidth(uv_pos);
vec2 line = 1.0 - smoothstep(vec2(thickness * 0.5), vec2(thickness * 0.5) + fw * 1.5, grid_uv);
return clamp(max(line.x, line.y), 0.0, 1.0);
}
float get_checker(vec2 uv_pos) {
vec2 c = floor(uv_pos);
return mod(c.x + c.y, 2.0);
}
vec3 sample_plane(vec2 uv) {
float checker = get_checker(uv);
vec3 base = use_checker ? mix(color_a.rgb, color_b.rgb, checker) : color_a.rgb;
// Sub-grid lines (0.5m)
if (sub_grid_divisions > 1.0 && sub_line_thickness > 0.0) {
float sub_line = get_grid_line(uv * sub_grid_divisions, sub_line_thickness * sub_grid_divisions);
base = mix(base, sub_grid_color.rgb, sub_line * sub_grid_color.a);
}
// Major grid lines (1.0m)
if (line_thickness > 0.0) {
float main_line = get_grid_line(uv, line_thickness);
base = mix(base, grid_color.rgb, main_line * grid_color.a);
}
return base;
}
void fragment() {
vec3 pos = (world_aligned ? v_world_pos : v_local_pos) / max(grid_scale, 0.001);
vec3 norm = normalize(world_aligned ? v_world_normal : v_local_normal);
vec3 weights = abs(norm);
weights = pow(weights, vec3(triplanar_sharpness));
weights /= (weights.x + weights.y + weights.z + 0.00001);
vec2 uv_x = pos.zy;
vec2 uv_y = pos.xz;
vec2 uv_z = pos.xy;
vec3 col_x = sample_plane(uv_x);
vec3 col_y = sample_plane(uv_y);
vec3 col_z = sample_plane(uv_z);
vec3 final_color = col_x * weights.x + col_y * weights.y + col_z * weights.z;
ALBEDO = final_color;
ROUGHNESS = roughness;
METALLIC = metallic;
}
@@ -1 +0,0 @@
uid://cxdfexn0nsnhs
@@ -1,20 +0,0 @@
[gd_scene load_steps=2 format=3 uid="uid://dhpjubljm0tf0"]
[ext_resource type="Texture2D" uid="uid://cjxx30pcamj36" path="res://addons/csg_blockout/res/icons/box.svg" id="1_n1dqv"]
[node name="CsgQuickActions" type="Control"]
layout_mode = 3
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
grow_horizontal = 2
grow_vertical = 2
[node name="CenterContainer" type="CenterContainer" parent="."]
layout_mode = 0
offset_right = 40.0
offset_bottom = 40.0
[node name="Box" type="TextureButton" parent="CenterContainer"]
layout_mode = 2
texture_normal = ExtResource("1_n1dqv")
@@ -1,243 +0,0 @@
[gd_scene format=3]
[ext_resource type="Script" path="res://addons/csg_blockout/scripts/csg_side_blockout_bar.gd" id="1_awo4p"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/box.svg" id="2_p4f7r"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/cyliner.svg" id="3_j7s68"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/mesh.svg" id="4_328ee"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/polygon.svg" id="5_0os5t"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/sphere.svg" id="6_ju1pi"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/torus.svg" id="7_6hoxg"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/union.svg" id="8_gmt87"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/intersection.svg" id="9_55ipi"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/subtraction.svg" id="10_8u5xb"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/empty-material.svg" id="11_ten1m"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/grid_light.svg" id="12_light"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/grid_dark.svg" id="13_dark"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/grid_orange.svg" id="14_orange"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/material_none.svg" id="15_none"]
[ext_resource type="Texture2D" path="res://addons/csg_blockout/res/icons/apply_material.svg" id="16_apply"]
[sub_resource type="ButtonGroup" id="ButtonGroup_2ipgb"]
[sub_resource type="ButtonGroup" id="ButtonGroup_mat"]
[sub_resource type="StyleBoxEmpty" id="StyleBoxEmpty_ixko7"]
[node name="MarginContainer" type="MarginContainer"]
visible = false
custom_minimum_size = Vector2(52, 0)
anchors_preset = 15
anchor_right = 1.0
anchor_bottom = 1.0
offset_right = -2.0
grow_horizontal = 2
grow_vertical = 2
theme_override_constants/margin_top = 8
theme_override_constants/margin_bottom = 8
script = ExtResource("1_awo4p")
[node name="ScrollContainer" type="ScrollContainer" parent="."]
layout_mode = 2
horizontal_scroll_mode = 0
[node name="HBoxContainer" type="VBoxContainer" parent="ScrollContainer"]
custom_minimum_size = Vector2(0, 120)
layout_mode = 2
size_flags_horizontal = 6
size_flags_vertical = 3
theme_override_constants/separation = 12
alignment = 1
[node name="CSG" type="VBoxContainer" parent="ScrollContainer/HBoxContainer"]
layout_mode = 2
alignment = 1
[node name="Box" type="Button" parent="ScrollContainer/HBoxContainer/CSG"]
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
tooltip_text = "立方体"
icon = ExtResource("2_p4f7r")
icon_alignment = 1
expand_icon = true
[node name="Cylinder" type="Button" parent="ScrollContainer/HBoxContainer/CSG"]
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
tooltip_text = "圆柱体"
theme_override_constants/icon_max_width = 24
icon = ExtResource("3_j7s68")
icon_alignment = 1
expand_icon = true
[node name="Mesh" type="Button" parent="ScrollContainer/HBoxContainer/CSG"]
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
tooltip_text = "网格"
theme_override_constants/icon_max_width = 24
icon = ExtResource("4_328ee")
icon_alignment = 1
expand_icon = true
[node name="Polygon" type="Button" parent="ScrollContainer/HBoxContainer/CSG"]
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
tooltip_text = "多边形"
theme_override_constants/icon_max_width = 24
icon = ExtResource("5_0os5t")
icon_alignment = 1
expand_icon = true
[node name="Sphere" type="Button" parent="ScrollContainer/HBoxContainer/CSG"]
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
tooltip_text = "球体"
theme_override_constants/icon_max_width = 24
icon = ExtResource("6_ju1pi")
icon_alignment = 1
expand_icon = true
[node name="Torus" type="Button" parent="ScrollContainer/HBoxContainer/CSG"]
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
tooltip_text = "圆环"
theme_override_constants/icon_max_width = 24
icon = ExtResource("7_6hoxg")
icon_alignment = 1
expand_icon = true
[node name="Operation" type="VBoxContainer" parent="ScrollContainer/HBoxContainer"]
layout_mode = 2
alignment = 1
[node name="Union" type="Button" parent="ScrollContainer/HBoxContainer/Operation"]
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
tooltip_text = "并集"
theme_override_constants/icon_max_width = 24
toggle_mode = true
button_group = SubResource("ButtonGroup_2ipgb")
icon = ExtResource("8_gmt87")
icon_alignment = 1
expand_icon = true
[node name="Intersection" type="Button" parent="ScrollContainer/HBoxContainer/Operation"]
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
tooltip_text = "交集"
theme_override_constants/icon_max_width = 24
toggle_mode = true
button_group = SubResource("ButtonGroup_2ipgb")
icon = ExtResource("9_55ipi")
icon_alignment = 1
expand_icon = true
[node name="Subtraction" type="Button" parent="ScrollContainer/HBoxContainer/Operation"]
custom_minimum_size = Vector2(42, 42)
layout_mode = 2
tooltip_text = "差集"
theme_override_constants/icon_max_width = 24
toggle_mode = true
button_group = SubResource("ButtonGroup_2ipgb")
icon = ExtResource("10_8u5xb")
icon_alignment = 1
expand_icon = true
[node name="Material" type="VBoxContainer" parent="ScrollContainer/HBoxContainer"]
layout_mode = 2
alignment = 1
[node name="PresetLight" type="Button" parent="ScrollContainer/HBoxContainer/Material"]
custom_minimum_size = Vector2(42, 36)
layout_mode = 2
tooltip_text = "灰白网格"
theme_override_constants/icon_max_width = 22
toggle_mode = true
button_group = SubResource("ButtonGroup_mat")
icon = ExtResource("12_light")
icon_alignment = 1
expand_icon = true
[node name="PresetDark" type="Button" parent="ScrollContainer/HBoxContainer/Material"]
custom_minimum_size = Vector2(42, 36)
layout_mode = 2
tooltip_text = "深灰网格"
theme_override_constants/icon_max_width = 22
toggle_mode = true
button_group = SubResource("ButtonGroup_mat")
icon = ExtResource("13_dark")
icon_alignment = 1
expand_icon = true
[node name="PresetOrange" type="Button" parent="ScrollContainer/HBoxContainer/Material"]
custom_minimum_size = Vector2(42, 36)
layout_mode = 2
tooltip_text = "橙色网格"
theme_override_constants/icon_max_width = 22
toggle_mode = true
button_group = SubResource("ButtonGroup_mat")
icon = ExtResource("14_orange")
icon_alignment = 1
expand_icon = true
[node name="PresetNone" type="Button" parent="ScrollContainer/HBoxContainer/Material"]
custom_minimum_size = Vector2(42, 36)
layout_mode = 2
tooltip_text = "无材质(白模)"
theme_override_constants/icon_max_width = 22
toggle_mode = true
button_group = SubResource("ButtonGroup_mat")
icon = ExtResource("15_none")
icon_alignment = 1
expand_icon = true
[node name="MaterialPicker" type="Button" parent="ScrollContainer/HBoxContainer/Material"]
custom_minimum_size = Vector2(42, 36)
layout_mode = 2
tooltip_text = "自定义材质"
theme_override_constants/icon_max_width = 22
toggle_mode = true
button_group = SubResource("ButtonGroup_mat")
icon = ExtResource("11_ten1m")
icon_alignment = 1
expand_icon = true
[node name="ApplyToSelected" type="Button" parent="ScrollContainer/HBoxContainer/Material"]
custom_minimum_size = Vector2(42, 36)
layout_mode = 2
tooltip_text = "应用材质到选中节点"
theme_override_constants/icon_max_width = 22
icon = ExtResource("16_apply")
icon_alignment = 1
expand_icon = true
[node name="HSeparator" type="HSeparator" parent="ScrollContainer/HBoxContainer"]
layout_mode = 2
size_flags_vertical = 3
theme_override_styles/separator = SubResource("StyleBoxEmpty_ixko7")
[node name="LanguageToggle" type="OptionButton" parent="ScrollContainer/HBoxContainer"]
custom_minimum_size = Vector2(36, 22)
layout_mode = 2
size_flags_horizontal = 4
tooltip_text = "Language / 语言"
theme_override_font_sizes/font_size = 11
alignment = 1
flat = true
fit_to_longest_item = false
[connection signal="pressed" from="ScrollContainer/HBoxContainer/CSG/Box" to="." method="_on_box_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/CSG/Cylinder" to="." method="_on_cylinder_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/CSG/Mesh" to="." method="_on_mesh_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/CSG/Polygon" to="." method="_on_polygon_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/CSG/Sphere" to="." method="_on_sphere_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/CSG/Torus" to="." method="_on_torus_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/Operation/Union" to="." method="_on_operation_pressed" binds= [0]]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/Operation/Intersection" to="." method="_on_operation_pressed" binds= [1]]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/Operation/Subtraction" to="." method="_on_operation_pressed" binds= [2]]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/Material/PresetLight" to="." method="_on_preset_light_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/Material/PresetDark" to="." method="_on_preset_dark_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/Material/PresetOrange" to="." method="_on_preset_orange_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/Material/PresetNone" to="." method="_on_preset_none_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/Material/MaterialPicker" to="." method="_on_material_picker_pressed"]
[connection signal="pressed" from="ScrollContainer/HBoxContainer/Material/ApplyToSelected" to="." method="_on_apply_to_selected_pressed"]
[connection signal="item_selected" from="ScrollContainer/HBoxContainer/LanguageToggle" to="." method="_on_language_toggle_item_selected"]
@@ -1,22 +0,0 @@
[gd_scene format=3 uid="uid://dgldacs362p7g"]
[ext_resource type="Script" uid="uid://dk6dt8fk1s43s" path="res://addons/csg_blockout/scripts/csg_top_blockout_bar.gd" id="1_154hl"]
[node name="HBoxContainer" type="HBoxContainer"]
visible = false
offset_right = 40.0
offset_bottom = 40.0
script = ExtResource("1_154hl")
[node name="Refresh" type="Button" parent="."]
layout_mode = 2
text = "刷新"
flat = true
[node name="Bake" type="Button" parent="."]
layout_mode = 2
text = "烘焙"
flat = true
[connection signal="pressed" from="Refresh" to="." method="_on_refresh_pressed"]
[connection signal="pressed" from="Bake" to="." method="_on_bake_pressed"]
File diff suppressed because one or more lines are too long
@@ -1,183 +0,0 @@
@tool
extends RefCounted
class_name CsgBlockoutConfig
static var _instance: CsgBlockoutConfig
static func get_config() -> CsgBlockoutConfig:
if _instance == null:
_instance = CsgBlockoutConfig.new()
_instance._ensure_settings_exist()
return _instance
# ProjectSettings paths
const SETTING_ACTION_KEY = "addons/csg_blockout/action_key"
const SETTING_AUTO_HIDE = "addons/csg_blockout/auto_hide"
const SETTING_LANGUAGE_OVERRIDE = "addons/csg_blockout/language_override"
const SETTING_MATERIAL_PRESET = "addons/csg_blockout/material_preset"
const SETTING_DEFAULT_OPERATION = "addons/csg_blockout/default_operation"
const SETTING_CUSTOM_MATERIAL_PATH = "addons/csg_blockout/custom_material_path"
# Default values
const DEFAULT_ACTION_KEY = KEY_SHIFT
const DEFAULT_AUTO_HIDE = true
const DEFAULT_LANGUAGE_OVERRIDE = "auto"
const DEFAULT_MATERIAL_PRESET = MaterialPreset.GRID_LIGHT
enum MaterialPreset {
NONE = 0,
GRID_LIGHT = 1,
GRID_DARK = 2,
GRID_ORANGE = 3,
CUSTOM = 4
}
signal config_saved()
signal material_preset_changed(preset: MaterialPreset)
signal custom_material_changed(mat: Material)
signal default_operation_changed(op: CSGShape3D.Operation)
# Configurable properties
## Key to hold for primary action (e.g., opening pie menu)
var action_key: Key = KEY_SHIFT:
get: return _get_setting(SETTING_ACTION_KEY, DEFAULT_ACTION_KEY)
set(value): _set_setting(SETTING_ACTION_KEY, value)
## Whether to auto-hide the CSG blockout UI when not in use
var auto_hide: bool = true:
get: return _get_setting(SETTING_AUTO_HIDE, DEFAULT_AUTO_HIDE)
set(value): _set_setting(SETTING_AUTO_HIDE, value)
## Language override (auto, en, zh_CN, ja, ko, es, pt, ru)
var language_override: String = "auto":
get: return _get_setting(SETTING_LANGUAGE_OVERRIDE, DEFAULT_LANGUAGE_OVERRIDE)
set(value): _set_setting(SETTING_LANGUAGE_OVERRIDE, value)
## Material Preset (None, Light Grid, Dark Grid, Orange Grid, Custom)
var material_preset: MaterialPreset = MaterialPreset.GRID_LIGHT:
get: return _get_setting(SETTING_MATERIAL_PRESET, DEFAULT_MATERIAL_PRESET) as MaterialPreset
set(value):
_set_setting(SETTING_MATERIAL_PRESET, value)
material_preset_changed.emit(value)
## Default CSG boolean operation applied to newly created nodes (shared by pie menu & sidebar)
var default_operation: CSGShape3D.Operation = CSGShape3D.OPERATION_UNION:
get: return _get_setting(SETTING_DEFAULT_OPERATION, CSGShape3D.OPERATION_UNION) as CSGShape3D.Operation
set(value):
_set_setting(SETTING_DEFAULT_OPERATION, value)
default_operation_changed.emit(value)
var custom_material: Material = null:
get:
if custom_material == null:
var path: String = _get_setting(SETTING_CUSTOM_MATERIAL_PATH, "")
if not path.is_empty() and ResourceLoader.exists(path):
custom_material = ResourceLoader.load(path) as Material
return custom_material
set(value):
custom_material = value
_set_setting(SETTING_CUSTOM_MATERIAL_PATH, value.resource_path if value != null else "")
custom_material_changed.emit(value)
func _ensure_settings_exist() -> void:
"""Register settings in ProjectSettings if they don't exist."""
if not ProjectSettings.has_setting(SETTING_ACTION_KEY):
ProjectSettings.set_setting(SETTING_ACTION_KEY, DEFAULT_ACTION_KEY)
ProjectSettings.set_initial_value(SETTING_ACTION_KEY, DEFAULT_ACTION_KEY)
ProjectSettings.add_property_info({
"name": SETTING_ACTION_KEY,
"type": TYPE_INT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "Shift:%d,Ctrl:%d,Alt:%d,Meta:%d" % [KEY_SHIFT, KEY_CTRL, KEY_ALT, KEY_META]
})
if not ProjectSettings.has_setting(SETTING_AUTO_HIDE):
ProjectSettings.set_setting(SETTING_AUTO_HIDE, DEFAULT_AUTO_HIDE)
ProjectSettings.set_initial_value(SETTING_AUTO_HIDE, DEFAULT_AUTO_HIDE)
ProjectSettings.add_property_info({
"name": SETTING_AUTO_HIDE,
"type": TYPE_BOOL
})
if not ProjectSettings.has_setting(SETTING_LANGUAGE_OVERRIDE):
ProjectSettings.set_setting(SETTING_LANGUAGE_OVERRIDE, DEFAULT_LANGUAGE_OVERRIDE)
ProjectSettings.set_initial_value(SETTING_LANGUAGE_OVERRIDE, DEFAULT_LANGUAGE_OVERRIDE)
ProjectSettings.add_property_info({
"name": SETTING_LANGUAGE_OVERRIDE,
"type": TYPE_STRING,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "auto,en,zh_CN,ja,ko,es,pt,ru"
})
if not ProjectSettings.has_setting(SETTING_MATERIAL_PRESET):
ProjectSettings.set_setting(SETTING_MATERIAL_PRESET, DEFAULT_MATERIAL_PRESET)
ProjectSettings.set_initial_value(SETTING_MATERIAL_PRESET, DEFAULT_MATERIAL_PRESET)
ProjectSettings.add_property_info({
"name": SETTING_MATERIAL_PRESET,
"type": TYPE_INT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "None,Light Grid,Dark Grid,Orange Grid,Custom"
})
if not ProjectSettings.has_setting(SETTING_DEFAULT_OPERATION):
ProjectSettings.set_setting(SETTING_DEFAULT_OPERATION, CSGShape3D.OPERATION_UNION)
ProjectSettings.set_initial_value(SETTING_DEFAULT_OPERATION, CSGShape3D.OPERATION_UNION)
ProjectSettings.add_property_info({
"name": SETTING_DEFAULT_OPERATION,
"type": TYPE_INT,
"hint": PROPERTY_HINT_ENUM,
"hint_string": "Union:0,Intersection:1,Subtraction:2"
})
func get_preset_material(preset: MaterialPreset) -> Material:
var file_name := ""
match preset:
MaterialPreset.GRID_LIGHT:
file_name = "mat_grid_light.tres"
MaterialPreset.GRID_DARK:
file_name = "mat_grid_dark.tres"
MaterialPreset.GRID_ORANGE:
file_name = "mat_grid_orange.tres"
_:
return null
var paths := [
"res://addons/csg_blockout/res/materials/" + file_name,
"res://res/materials/" + file_name
]
if not CsgBlockout.csg_plugin_path.is_empty():
paths.push_front(CsgBlockout.csg_plugin_path.path_join("res/materials").path_join(file_name))
for p in paths:
if ResourceLoader.exists(p):
return ResourceLoader.load(p) as Material
return null
func get_active_material() -> Material:
match material_preset:
MaterialPreset.NONE:
return null
MaterialPreset.GRID_LIGHT, MaterialPreset.GRID_DARK, MaterialPreset.GRID_ORANGE:
return get_preset_material(material_preset)
MaterialPreset.CUSTOM:
return custom_material
_:
return null
func _get_setting(path: String, default_value: Variant) -> Variant:
"""Get a setting from ProjectSettings."""
return ProjectSettings.get_setting(path, default_value)
func _set_setting(path: String, value: Variant) -> void:
"""Set a setting in ProjectSettings."""
ProjectSettings.set_setting(path, value)
func save_config() -> void:
"""Save settings to project.godot file."""
var err = ProjectSettings.save()
if err == OK:
print("CsgBlockout: Saved Config to ProjectSettings")
config_saved.emit()
else:
push_error("CsgBlockout: Failed to save config - error code %d" % err)
@@ -1 +0,0 @@
uid://w8ad8q4lneis
@@ -1,429 +0,0 @@
@tool
class_name CsgBlockoutI18n
extends RefCounted
const TABLE: Dictionary = {
"CSG_BLOCKOUT_SETTINGS": {
"zh": "CSG Blockout 设置",
"en": "CSG Blockout Settings",
"ja": "CSG Blockout 設定",
"ko": "CSG Blockout 설정",
"es": "Configuración de CSG Blockout",
"pt": "Configurações do CSG Blockout",
"ru": "Настройки CSG Blockout"
},
"BOX": {
"zh": "立方体",
"en": "Box",
"ja": "直方体",
"ko": "상자",
"es": "Caja",
"pt": "Caixa",
"ru": "Куб"
},
"CYLINDER": {
"zh": "圆柱体",
"en": "Cylinder",
"ja": "円柱",
"ko": "원기둥",
"es": "Cilindro",
"pt": "Cilindro",
"ru": "Цилиндр"
},
"MESH": {
"zh": "网格",
"en": "Mesh",
"ja": "メッシュ",
"ko": "메시",
"es": "Malla",
"pt": "Malha",
"ru": "Сетка"
},
"POLYGON": {
"zh": "多边形",
"en": "Polygon",
"ja": "ポリゴン",
"ko": "다각형",
"es": "Polígono",
"pt": "Polígono",
"ru": "Многоугольник"
},
"SPHERE": {
"zh": "球体",
"en": "Sphere",
"ja": "球体",
"ko": "구",
"es": "Esfera",
"pt": "Esfera",
"ru": "Сфера"
},
"TORUS": {
"zh": "圆环",
"en": "Torus",
"ja": "トーラス",
"ko": "토러스",
"es": "Toro",
"pt": "Toro",
"ru": "Тор"
},
"UNION": {
"zh": "并集",
"en": "Union",
"ja": "和集合",
"ko": "합집합",
"es": "Unión",
"pt": "União",
"ru": "Объединение"
},
"INTERSECTION": {
"zh": "交集",
"en": "Intersection",
"ja": "積集合",
"ko": "교집합",
"es": "Intersección",
"pt": "Interseção",
"ru": "Пересечение"
},
"SUBTRACTION": {
"zh": "差集",
"en": "Subtraction",
"ja": "差集合",
"ko": "차집합",
"es": "Sustracción",
"pt": "Subtração",
"ru": "Вычитание"
},
"MATERIAL": {
"zh": "材质",
"en": "Material",
"ja": "マテリアル",
"ko": "머티리얼",
"es": "Material",
"pt": "Material",
"ru": "Материал"
},
"REGEN_PREVIEW_TOOLTIP": {
"zh": "重新生成预览实例",
"en": "Regenerate Preview Instances",
"ja": "プレビューインスタンスを再生成",
"ko": "미리보기 인스턴스 재생성",
"es": "Regenerar instancias de vista previa",
"pt": "Regenerar instâncias de pré-visualização",
"ru": "Перегенерировать экземпляры предпросмотра"
},
"BAKE_INSTANCES_TOOLTIP": {
"zh": "将生成的实例烘焙至场景中 (使其永久保留)",
"en": "Bake generated instances into scene (Make permanent)",
"ja": "生成されたインスタンスをシーンにベイク (永続化)",
"ko": "생성된 인스턴스를 씬에 베이크 (영구 유지)",
"es": "Hornear instancias generadas en la escena (hacer permanente)",
"pt": "Assar instâncias geradas na cena (tornar permanente)",
"ru": "Запечь созданные экземпляры в сцену (сделать постоянными)"
},
"SELECT_MATERIAL": {
"zh": "选择材质",
"en": "Select Material",
"ja": "マテリアルを選択",
"ko": "머티리얼 선택",
"es": "Seleccionar material",
"pt": "Selecionar material",
"ru": "Выбрать материал"
},
"WARN_SELECT_CSG_SHAPE": {
"zh": "请先选择一个 CSGShape3D 节点以添加新 CSG 节点",
"en": "Please select a CSGShape3D node first to add a new CSG node",
"ja": "新しいCSGノードを追加するには、まずCSGShape3Dノードを選択してください",
"ko": "새 CSG 노드를 추가하려면 먼저 CSGShape3D 노드를 선택하세요",
"es": "Por favor, seleccione un nodo CSGShape3D primero para agregar un nuevo nodo CSG",
"pt": "Selecione primeiro um nó CSGShape3D para adicionar um novo nó CSG",
"ru": "Пожалуйста, сначала выберите узел CSGShape3D, чтобы добавить новый узел CSG"
},
"WARN_UNSUPPORTED_CSG_TYPE": {
"zh": "未知或不支持的 CSG 节点类型",
"en": "Unknown or unsupported CSG node type",
"ja": "不明または未対応のCSGノードタイプです",
"ko": "알 수 없거나 지원되지 않는 CSG 노드 유형입니다",
"es": "Tipo de nodo CSG desconocido o no compatible",
"pt": "Tipo de nó CSG desconhecido ou não suportado",
"ru": "Неизвестный или неподдерживаемый тип узла CSG"
},
"ADD_NODE": {
"zh": "添加 %s",
"en": "Add %s",
"ja": "%s を追加",
"ko": "%s 추가",
"es": "Añadir %s",
"pt": "Adicionar %s",
"ru": "Добавить %s"
},
"REFRESH": {
"zh": "刷新",
"en": "Refresh",
"ja": "更新",
"ko": "새로고침",
"es": "Actualizar",
"pt": "Atualizar",
"ru": "Обновить"
},
"BAKE": {
"zh": "烘焙",
"en": "Bake",
"ja": "ベイク",
"ko": "베이크",
"es": "Hornear",
"pt": "Assar",
"ru": "Запечь"
},
"CHANGE_CSG_OP": {
"zh": "更改 CSG 操作",
"en": "Change CSG Operation",
"ja": "CSG操作を変更",
"ko": "CSG 작업 변경",
"es": "Cambiar operación CSG",
"pt": "Alterar operação CSG",
"ru": "Изменить операцию CSG"
},
"CREATE_NODE": {
"zh": "创建 %s",
"en": "Create %s",
"ja": "%s を作成",
"ko": "%s 생성",
"es": "Crear %s",
"pt": "Criar %s",
"ru": "Создать %s"
},
"GRID_LIGHT": {
"zh": "灰白网格",
"en": "Light Grid",
"ja": "ライトグリッド",
"ko": "라이트 그리드",
"es": "Cuadrícula clara",
"pt": "Grade clara",
"ru": "Светлая сетка"
},
"GRID_DARK": {
"zh": "深灰网格",
"en": "Dark Grid",
"ja": "ダークグリッド",
"ko": "다크 그리드",
"es": "Cuadrícula oscura",
"pt": "Grade escura",
"ru": "Тёмная сетка"
},
"GRID_ORANGE": {
"zh": "橙色网格",
"en": "Orange Grid",
"ja": "オレンジグリッド",
"ko": "오렌지 그리드",
"es": "Cuadrícula naranja",
"pt": "Grade laranja",
"ru": "Оранжевая сетка"
},
"MATERIAL_NONE": {
"zh": "无材质(白模)",
"en": "No Material (White)",
"ja": "マテリアルなし(白モデル)",
"ko": "머티리얼 없음(흰색)",
"es": "Sin material (blanco)",
"pt": "Sem material (branco)",
"ru": "Без материала (белый)"
},
"MATERIAL_CUSTOM": {
"zh": "自定义材质",
"en": "Custom Material",
"ja": "カスタムマテリアル",
"ko": "사용자 정의 머티리얼",
"es": "Material personalizado",
"pt": "Material personalizado",
"ru": "Пользовательский материал"
},
"APPLY_MATERIAL_TO_SELECTED": {
"zh": "应用材质到选中节点",
"en": "Apply Material to Selected",
"ja": "選択したノードにマテリアルを適用",
"ko": "선택한 노드에 머티리얼 적용",
"es": "Aplicar material a seleccionados",
"pt": "Aplicar material aos selecionados",
"ru": "Применить материал к выбранным"
},
"APPLY_MATERIAL": {
"zh": "应用材质",
"en": "Apply Material",
"ja": "マテリアルを適用",
"ko": "머티리얼 적용",
"es": "Aplicar material",
"pt": "Aplicar material",
"ru": "Применить материал"
},
"CLEAR_MATERIAL": {
"zh": "清除材质",
"en": "Clear Material",
"ja": "マテリアルをクリア",
"ko": "머티리얼 제거",
"es": "Limpiar material",
"pt": "Limpar material",
"ru": "Очистить материал"
},
"LANGUAGE_TOOLTIP": {
"zh": "语言 / Language (Auto / EN / 中文 / 日本語 / 한국어 / Español / Português / Русский)",
"en": "Language / 语言 (Auto / EN / 中文 / 日本語 / 한국어 / Español / Português / Русский)",
"ja": "言語 / Language (Auto / EN / 中文 / 日本語 / 한국어 / Español / Português / Русский)",
"ko": "언어 / Language (Auto / EN / 中文 / 日本語 / 한국어 / Español / Português / Русский)",
"es": "Idioma / Language (Auto / EN / 中文 / 日本語 / 한국어 / Español / Português / Русский)",
"pt": "Idioma / Language (Auto / EN / 中文 / 日本語 / 한국어 / Español / Português / Русский)",
"ru": "Язык / Language (Auto / EN / 中文 / 日本語 / 한국어 / Español / Português / Русский)"
}
}
static var _reverse_initialized: bool = false
static var _reverse_map: Dictionary = {}
static func _ensure_reverse_map() -> void:
if _reverse_initialized:
return
_reverse_initialized = true
for key in TABLE:
var entry: Dictionary = TABLE[key]
for lang in entry:
var text: String = entry[lang]
_reverse_map[text] = key
_reverse_map[key] = key
# Node name and legacy compatibility aliases
_reverse_map["PresetLight"] = "GRID_LIGHT"
_reverse_map["PRESET_LIGHT"] = "GRID_LIGHT"
_reverse_map["PresetDark"] = "GRID_DARK"
_reverse_map["PRESET_DARK"] = "GRID_DARK"
_reverse_map["PresetOrange"] = "GRID_ORANGE"
_reverse_map["PRESET_ORANGE"] = "GRID_ORANGE"
_reverse_map["PresetNone"] = "MATERIAL_NONE"
_reverse_map["PRESET_NONE"] = "MATERIAL_NONE"
_reverse_map["MaterialPicker"] = "MATERIAL_CUSTOM"
_reverse_map["MATERIAL_PICKER"] = "MATERIAL_CUSTOM"
_reverse_map["ApplyToSelected"] = "APPLY_MATERIAL_TO_SELECTED"
_reverse_map["APPLY_TO_SELECTED"] = "APPLY_MATERIAL_TO_SELECTED"
_reverse_map["Refresh"] = "REFRESH"
_reverse_map["Bake"] = "BAKE"
_reverse_map["创建 "] = "CREATE_NODE"
_reverse_map["Create "] = "CREATE_NODE"
_reverse_map["Language / 语言"] = "LANGUAGE_TOOLTIP"
static func _normalize_locale(loc: String) -> String:
var l := loc.to_lower()
if l.begins_with("zh"):
return "zh"
elif l.begins_with("ja"):
return "ja"
elif l.begins_with("ko"):
return "ko"
elif l.begins_with("es"):
return "es"
elif l.begins_with("pt"):
return "pt"
elif l.begins_with("ru"):
return "ru"
return "en"
static func get_locale() -> String:
var config = CsgBlockoutConfig.get_config()
if config:
var override_lang: String = config.language_override
if override_lang != "" and override_lang != "auto":
return _normalize_locale(override_lang)
# 1. Safely check EditorInterface editor settings in editor hint
if Engine.is_editor_hint():
var editor_settings = EditorInterface.get_editor_settings()
if editor_settings:
var lang_setting = editor_settings.get_setting("interface/editor/editor_language")
if lang_setting != null:
var s := String(lang_setting).strip_edges()
if not s.is_empty() and s != "default" and s != "auto":
return _normalize_locale(s)
# 2. Check TranslationServer tool locale
var tool_locale := TranslationServer.get_tool_locale()
if not tool_locale.is_empty() and tool_locale != "en":
return _normalize_locale(tool_locale)
var general_locale := TranslationServer.get_locale()
if not general_locale.is_empty() and general_locale != "en":
return _normalize_locale(general_locale)
# 3. Check OS locale language
var os_locale := OS.get_locale_language()
if not os_locale.is_empty():
return _normalize_locale(os_locale)
var os_full := OS.get_locale()
if not os_full.is_empty():
return _normalize_locale(os_full)
return "en"
static func t(key_or_text: String) -> String:
_ensure_reverse_map()
var canonical_key: String = _reverse_map.get(key_or_text, key_or_text)
if TABLE.has(canonical_key):
var lang = get_locale()
return TABLE[canonical_key].get(lang, TABLE[canonical_key].get("en", key_or_text))
return key_or_text
static func tf(key_or_text: String, args: Array = []) -> String:
var tmpl := t(key_or_text)
if args.is_empty():
return tmpl
return tmpl % args
static func translate_node(node: Node) -> void:
if node == null:
return
# Disable Godot engine auto_translate to prevent Godot's built-in PO dictionary
# from hijacking common English words (e.g. Box, Cylinder, Mesh, Polygon, Sphere) back into Chinese.
if "auto_translate_mode" in node:
node.set("auto_translate_mode", 2) # Node.AUTO_TRANSLATE_MODE_DISABLED
if "auto_translate" in node:
node.set("auto_translate", false)
if node is OptionButton or node.name == "LanguageToggle":
for child in node.get_children():
translate_node(child)
return
_ensure_reverse_map()
if node is Control:
if not node.has_meta("i18n_tooltip_key"):
var key := ""
if node.tooltip_text != "":
key = _reverse_map.get(node.tooltip_text, "")
if key == "":
var node_name_str := String(node.name)
key = _reverse_map.get(node_name_str, "")
if key == "":
var snake_key := node_name_str.to_snake_case().to_upper()
key = _reverse_map.get(snake_key, "")
if key != "":
node.set_meta("i18n_tooltip_key", key)
if node.has_meta("i18n_tooltip_key"):
node.tooltip_text = t(node.get_meta("i18n_tooltip_key"))
if node is Button or node is Label:
if "text" in node and typeof(node.get("text")) == TYPE_STRING and node.get("text") != "":
if not node.has_meta("i18n_text_key"):
var key := _reverse_map.get(node.get("text"), "")
if key == "":
var node_name_str := String(node.name)
key = _reverse_map.get(node_name_str, "")
if key == "":
var snake_key := node_name_str.to_snake_case().to_upper()
key = _reverse_map.get(snake_key, "")
if key != "":
node.set_meta("i18n_text_key", key)
if node.has_meta("i18n_text_key"):
node.set("text", t(node.get_meta("i18n_text_key")))
for child in node.get_children():
translate_node(child)
@@ -1 +0,0 @@
uid://bvf7nvtsq1c06
-208
View File
@@ -1,208 +0,0 @@
@tool
class_name CsgPieMenu extends Control
signal action_triggered(item: Dictionary)
signal close_requested()
const BASE_INNER_RADIUS: float = 30.0
const BASE_OUTER_RADIUS: float = 120.0
const BASE_LINE_WIDTH: float = 2.0
const BASE_FONT_SIZE: int = 14
var editor_scale: float = 1.0
var items: Array = [] # Array[Dictionary]
var menu_stack: Array = [] # Array[Array]
var inner_radius: float = BASE_INNER_RADIUS
var outer_radius: float = BASE_OUTER_RADIUS
var active_item_index: int = -1
var is_in_deadzone: bool = true
# Style configuration
var bg_color: Color = Color(0.1, 0.1, 0.15, 0.85)
var hover_color: Color = Color(0.3, 0.5, 0.8, 0.95)
var text_color: Color = Color.WHITE
var line_color: Color = Color(0.2, 0.2, 0.2, 0.8)
var line_width: float = BASE_LINE_WIDTH
var font_size: int = BASE_FONT_SIZE
# Animation state
var anim_progress: float = 0.0
var popup_tween: Tween
var item_hover_progress: Array[float] = []
func _ready() -> void:
set_process(true)
mouse_filter = Control.MOUSE_FILTER_IGNORE
func apply_editor_scale(scale: float) -> void:
editor_scale = maxf(scale, 0.1)
inner_radius = BASE_INNER_RADIUS * editor_scale
outer_radius = BASE_OUTER_RADIUS * editor_scale
line_width = BASE_LINE_WIDTH * editor_scale
font_size = int(round(BASE_FONT_SIZE * editor_scale))
queue_redraw()
func setup(menu_items: Array, p_scale: float = -1.0) -> void:
if p_scale > 0.0:
apply_editor_scale(p_scale)
items = menu_items
menu_stack.clear()
active_item_index = -1
_play_popup_anim()
func _play_popup_anim() -> void:
item_hover_progress.resize(items.size())
item_hover_progress.fill(0.0)
anim_progress = 0.0
if popup_tween and popup_tween.is_valid():
popup_tween.kill()
popup_tween = create_tween().set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
popup_tween.tween_property(self, "anim_progress", 1.0, 0.25)
queue_redraw()
func _process(delta: float) -> void:
if items.is_empty():
return
var mouse_pos: Vector2 = get_local_mouse_position()
var distance: float = mouse_pos.length()
var new_active_index: int = -1
is_in_deadzone = distance < inner_radius
if not is_in_deadzone and distance < outer_radius * 2.0:
var angle: float = mouse_pos.angle()
var sector_angle: float = TAU / items.size()
var shifted_angle: float = wrapf(angle + sector_angle / 2.0, 0.0, TAU)
new_active_index = int(shifted_angle / sector_angle) % items.size()
if new_active_index != active_item_index:
active_item_index = new_active_index
var needs_redraw: bool = false
if popup_tween and popup_tween.is_running():
needs_redraw = true
for i in range(items.size()):
var target: float = 1.0 if i == active_item_index else 0.0
if abs(item_hover_progress[i] - target) > 0.01:
item_hover_progress[i] = lerpf(item_hover_progress[i], target, 20.0 * delta)
needs_redraw = true
if needs_redraw:
queue_redraw()
func _draw() -> void:
if items.is_empty():
return
var current_outer: float = outer_radius * anim_progress
var current_inner: float = inner_radius * anim_progress
if current_outer <= 0.01:
return
var num_items: int = items.size()
var sector_angle: float = TAU / num_items
var font: Font = get_theme_font("font", "Label")
# Draw shadow
draw_circle(Vector2.ZERO, current_outer + 2.0 * editor_scale, Color(0, 0, 0, 0.3 * anim_progress))
for i in range(num_items):
var start_angle: float = i * sector_angle - sector_angle / 2.0
var end_angle: float = start_angle + sector_angle
var hover_t: float = item_hover_progress[i]
# Base color lerp + global alpha
var color: Color = bg_color.lerp(hover_color, hover_t)
color.a *= anim_progress
# Draw sector polygon
var points: PackedVector2Array = _get_sector_polygon(start_angle, end_angle, current_inner, current_outer, 16)
draw_polygon(points, PackedColorArray([color]))
# Draw separator lines
var line_start: Vector2 = Vector2(cos(start_angle), sin(start_angle)) * current_inner
var line_end: Vector2 = Vector2(cos(start_angle), sin(start_angle)) * current_outer
var l_color: Color = line_color
l_color.a *= anim_progress
draw_line(line_start, line_end, l_color, line_width, true)
# Draw item text
var center_angle: float = i * sector_angle
var text_radius: float = (current_inner + current_outer) / 2.0
var text_pos: Vector2 = Vector2(cos(center_angle), sin(center_angle)) * text_radius
var item: Dictionary = items[i]
var text: String = item.get("label", "")
var text_size: Vector2 = font.get_string_size(text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
text_pos -= text_size / 2.0
text_pos.y += font.get_ascent(font_size) / 2.0
var t_color: Color = text_color
t_color.a *= anim_progress
draw_string(font, text_pos, text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, t_color)
# Draw center deadzone indicator
if not menu_stack.is_empty():
var back_color: Color = hover_color if is_in_deadzone else Color(0.2, 0.2, 0.2, 0.8)
back_color.a *= anim_progress
draw_circle(Vector2.ZERO, current_inner - 2.0 * editor_scale, back_color)
var back_text: String = "<"
var bt_size: Vector2 = font.get_string_size(back_text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size)
var bt_pos: Vector2 = - bt_size / 2.0
bt_pos.y += font.get_ascent(font_size) / 2.0
var bt_color: Color = text_color
bt_color.a *= anim_progress
draw_string(font, bt_pos, back_text, HORIZONTAL_ALIGNMENT_LEFT, -1, font_size, bt_color)
func _get_sector_polygon(start_angle: float, end_angle: float, inner_r: float, outer_r: float, point_count: int) -> PackedVector2Array:
var points: PackedVector2Array = PackedVector2Array()
for i in range(point_count + 1):
var t: float = float(i) / float(point_count)
var angle: float = lerpf(start_angle, end_angle, t)
points.append(Vector2(cos(angle), sin(angle)) * outer_r)
for i in range(point_count + 1):
var t: float = float(point_count - i) / float(point_count)
var angle: float = lerpf(start_angle, end_angle, t)
points.append(Vector2(cos(angle), sin(angle)) * inner_r)
return points
func execute_active_item(trigger_close_on_empty: bool = false) -> void:
if is_in_deadzone:
if not menu_stack.is_empty():
go_back()
elif trigger_close_on_empty:
close_requested.emit()
return
if active_item_index >= 0 and active_item_index < items.size():
var item: Dictionary = items[active_item_index]
if item.get("type") == "submenu":
action_triggered.emit(item)
menu_stack.push_back(items)
items = item.get("children", [])
active_item_index = -1
_play_popup_anim()
else:
action_triggered.emit(item)
close_requested.emit()
else:
if trigger_close_on_empty:
close_requested.emit()
func execute_back() -> void:
if not menu_stack.is_empty():
go_back()
else:
close_requested.emit()
func go_back() -> void:
if not menu_stack.is_empty():
items = menu_stack.pop_back()
active_item_index = -1
_play_popup_anim()
@@ -1 +0,0 @@
uid://brrnjx8pre80s
@@ -1,596 +0,0 @@
@tool
class_name CSGRepeater3D extends CSGCombiner3D
# NOTE: Registered as custom type in plugin (csg_blockout.gd) inheriting CSGCombiner3D.
# Ensure pattern resource scripts are loaded (Godot should handle via class_name, but we force references for safety):
const _REF_GRID = preload("res://addons/csg_blockout/scripts/patterns/grid_pattern.gd") # ensure subclass scripts loaded
const _REF_CIRC = preload("res://addons/csg_blockout/scripts/patterns/circular_pattern.gd")
const _REF_SPIRAL = preload("res://addons/csg_blockout/scripts/patterns/spiral_pattern.gd")
const _REF_NOISE = preload("res://addons/csg_blockout/scripts/patterns/noise_pattern.gd")
const REPEATER_NODE_META = "REPEATED_NODE_META"
const MAX_INSTANCES = 200
const REGEN_THROTTLE_MS: int = 150
const _CORNER_MASKS: Array[Vector3] = [
Vector3(0, 0, 0), Vector3(1, 0, 0), Vector3(0, 1, 0), Vector3(0, 0, 1),
Vector3(1, 1, 0), Vector3(1, 0, 1), Vector3(0, 1, 1), Vector3(1, 1, 1)
]
var _dirty: bool = false
var _last_regen_ms: int = 0
var _template_node: Node3D
@export var template_node: Node3D:
get: return _template_node
set(value):
_template_node = value
_mark_dirty()
var _template_node_scene: PackedScene
@export var template_node_scene: PackedScene:
get: return _template_node_scene
set(value):
_template_node_scene = value
_mark_dirty()
var _hide_template: bool = true
@export var hide_template: bool = true:
get: return _hide_template
set(value):
_hide_template = value
_update_template_visibility()
## repeat & spacing removed (migrated into pattern resources)
@export_group("Pattern Options")
# A single exported pattern resource (`pattern`) defines generation behavior.
@export_group("Variation Options")
# Rotation variation properties now managed via custom property list for collapsible enable group.
var _randomize_rotation: bool = false
var randomize_rotation: bool:
get: return _randomize_rotation
set(value):
_randomize_rotation = value
_mark_dirty()
notify_property_list_changed()
var _randomize_rot_x: bool = false
var randomize_rot_x: bool:
get: return _randomize_rot_x
set(value):
_randomize_rot_x = value
if _randomize_rotation: _mark_dirty()
notify_property_list_changed()
var _randomize_rot_y: bool = false
var randomize_rot_y: bool:
get: return _randomize_rot_y
set(value):
_randomize_rot_y = value
if _randomize_rotation: _mark_dirty()
notify_property_list_changed()
var _randomize_rot_z: bool = false
var randomize_rot_z: bool:
get: return _randomize_rot_z
set(value):
_randomize_rot_z = value
if _randomize_rotation: _mark_dirty()
notify_property_list_changed()
# Per-axis rotation variance in degrees (0 = full 0..360 random for that axis; >0 jitters around original)
var _rotation_variance_x_deg: float = 0.0
var rotation_variance_x_deg: float:
get: return _rotation_variance_x_deg
set(value):
_rotation_variance_x_deg = clamp(value, 0.0, 360.0)
if _randomize_rotation and _randomize_rot_x: _mark_dirty()
var _rotation_variance_y_deg: float = 0.0
var rotation_variance_y_deg: float:
get: return _rotation_variance_y_deg
set(value):
_rotation_variance_y_deg = clamp(value, 0.0, 360.0)
if _randomize_rotation and _randomize_rot_y: _mark_dirty()
var _rotation_variance_z_deg: float = 0.0
var rotation_variance_z_deg: float:
get: return _rotation_variance_z_deg
set(value):
_rotation_variance_z_deg = clamp(value, 0.0, 360.0)
if _randomize_rotation and _randomize_rot_z: _mark_dirty()
var _randomize_scale: bool = false
var randomize_scale: bool:
get: return _randomize_scale
set(value):
_randomize_scale = value
_mark_dirty()
notify_property_list_changed()
var _scale_variance: float = 0.0
var scale_variance: float:
get: return _scale_variance
set(value):
_scale_variance = clamp(value, 0.0, 1.0)
if _randomize_scale:
_mark_dirty()
# Per-axis scale variance (if zero => use global variance when axis toggle active)
var _scale_variance_x: float = 0.0
var scale_variance_x: float:
get: return _scale_variance_x
set(value):
_scale_variance_x = clamp(value, 0.0, 1.0)
if _randomize_scale and _randomize_scale_x: _mark_dirty()
var _scale_variance_y: float = 0.0
var scale_variance_y: float:
get: return _scale_variance_y
set(value):
_scale_variance_y = clamp(value, 0.0, 1.0)
if _randomize_scale and _randomize_scale_y: _mark_dirty()
var _scale_variance_z: float = 0.0
var scale_variance_z: float:
get: return _scale_variance_z
set(value):
_scale_variance_z = clamp(value, 0.0, 1.0)
if _randomize_scale and _randomize_scale_z: _mark_dirty()
# Per-axis scale randomization toggles (optional – if none enabled acts as uniform variance on all axes)
var _randomize_scale_x: bool = false
var randomize_scale_x: bool:
get: return _randomize_scale_x
set(value):
_randomize_scale_x = value
if _randomize_scale: _mark_dirty()
notify_property_list_changed()
var _randomize_scale_y: bool = false
var randomize_scale_y: bool:
get: return _randomize_scale_y
set(value):
_randomize_scale_y = value
if _randomize_scale: _mark_dirty()
notify_property_list_changed()
var _randomize_scale_z: bool = false
var randomize_scale_z: bool:
get: return _randomize_scale_z
set(value):
_randomize_scale_z = value
if _randomize_scale: _mark_dirty()
notify_property_list_changed()
var _position_jitter: float = 0.0
@export var position_jitter: float = 0.0:
get: return _position_jitter
set(value):
_position_jitter = max(0.0, value)
_mark_dirty()
var _random_seed: int = 0
@export var random_seed: int = 0:
get: return _random_seed
set(value):
_random_seed = value
_mark_dirty()
# Estimated instance count (read-only in inspector; updated internally)
var estimated_instances: int = 0
var rng: RandomNumberGenerator
var _generation_in_progress := false
var _pattern: CSGPattern
@export var pattern: CSGPattern:
get: return _pattern
set(value):
if value == _pattern:
return
# Reject non-CSGPattern resources
if value != null and not (value is CSGPattern):
push_warning("Assigned pattern is not a CSGPattern-derived resource; ignoring.")
return
# Prevent assigning the abstract base directly (must use subclass)
# NOTE: get_class() returns the native class ("Resource"), not the script class_name,
# so the check must go through the attached script's global name.
if value != null and _is_base_pattern_script(value.get_script()):
push_warning("Cannot assign base CSGPattern directly. Please use a concrete pattern (Grid, Circular, Spiral...).")
return
# Disconnect old
if _pattern and _pattern.is_connected("changed", Callable(self, "_on_pattern_changed")):
_pattern.disconnect("changed", Callable(self, "_on_pattern_changed"))
_pattern = value
if _pattern and not _pattern.is_connected("changed", Callable(self, "_on_pattern_changed")):
_pattern.connect("changed", Callable(self, "_on_pattern_changed"))
_mark_dirty()
func _is_base_pattern_script(script: Script) -> bool:
return script != null and script.get_global_name() == "CSGPattern"
func _ready() -> void:
rng = RandomNumberGenerator.new()
# Provide a default pattern if none assigned (through setter for signal wiring).
if pattern == null:
pattern = CSGGridPattern.new()
_mark_dirty()
func _on_pattern_changed() -> void:
# Called when the assigned pattern resource's exported properties are edited in inspector.
_mark_dirty()
func _process(_delta: float) -> void:
if not Engine.is_editor_hint(): return
if _dirty and not _generation_in_progress:
var now := Time.get_ticks_msec()
if now - _last_regen_ms < REGEN_THROTTLE_MS:
return # 保留 _dirty,下帧再试,形成节流
_last_regen_ms = now
_dirty = false
call_deferred("repeat_template")
func _exit_tree() -> void:
# Only clean up in editor; at runtime instances belong to the scene
if not Engine.is_editor_hint():
return
clear_children()
func _mark_dirty() -> void:
_dirty = true
func _update_template_visibility() -> void:
if not is_inside_tree():
return
if is_instance_valid(_template_node):
_template_node.visible = not _hide_template
func clear_children() -> void:
# Clear existing children except the template node
var children_to_remove: Array[Node] = []
for child in get_children():
if child.has_meta(REPEATER_NODE_META):
children_to_remove.append(child)
# Remove children immediately for better performance
for child in children_to_remove:
remove_child(child)
child.queue_free()
func repeat_template() -> void:
if _generation_in_progress:
return
_generation_in_progress = true
clear_children()
var template_node = _template_node if is_instance_valid(_template_node) else null
var using_scene = false
# Determine template source
if not template_node:
if not template_node_scene or not template_node_scene.can_instantiate():
_generation_in_progress = false
return
template_node = template_node_scene.instantiate()
using_scene = true
add_child(template_node)
# Use pattern estimation for early-out check
var template_size := _get_template_size(template_node)
var ctx_cap := {"template_size": template_size, "rng": rng, "position_jitter": _position_jitter}
var estimate := 0
if pattern:
estimate = pattern.get_estimated_count(ctx_cap)
if estimate <= 1:
estimated_instances = 0
if using_scene:
remove_child(template_node)
template_node.queue_free()
_generation_in_progress = false
return
rng.seed = _random_seed
var positions = _generate_positions(template_size)
# Cap by truncation instead of aborting (preserve generated instances)
if positions.size() > MAX_INSTANCES:
push_warning("CSGRepeater3D: Generated count %s exceeds cap %s. Truncating." % [positions.size(), MAX_INSTANCES])
positions = positions.slice(0, MAX_INSTANCES)
# Exact count: deduct the origin template only when index 0 is the origin
estimated_instances = positions.size()
if not positions.is_empty() and positions[0].is_zero_approx():
estimated_instances -= 1
for i in range(positions.size()):
var position = positions[i]
if i == 0 and position.is_zero_approx():
continue
var instance = template_node.duplicate()
if instance == null:
continue
instance.set_meta(REPEATER_NODE_META, true)
instance.transform.origin = position
# Ensure instance is visible regardless of template visibility
if instance is Node3D:
instance.visible = true
_apply_variations(instance)
add_child(instance)
if using_scene:
remove_child(template_node)
template_node.queue_free()
else:
_update_template_visibility()
_generation_in_progress = false
func _generate_positions(template_size: Vector3) -> Array[Vector3]:
var ctx: Dictionary = {"template_size": template_size, "rng": rng, "position_jitter": _position_jitter}
if pattern == null:
return []
return pattern.generate(ctx)
# -- Geometry-based spacing helpers -------------------------------------------------
func _get_template_size(template_node: Node) -> Vector3:
if template_node == null or not (template_node is Node3D):
return Vector3.ONE
var aabb := _get_combined_aabb(template_node)
var size: Vector3 = aabb.size
if size.x <= 0.0001: size.x = 1.0
if size.y <= 0.0001: size.y = 1.0
if size.z <= 0.0001: size.z = 1.0
return size
func _get_combined_aabb(node: Node) -> AABB:
var found: bool = false
var combined: AABB = AABB()
if node is Node3D and node.has_method("get_aabb"):
var aabb: AABB = node.get_aabb()
combined = aabb
found = true
for child in node.get_children():
if not (child is Node3D):
continue
var child_aabb: AABB = _get_combined_aabb(child)
if child_aabb.size == Vector3.ZERO:
continue
var child_3d: Node3D = child
for mask in _CORNER_MASKS:
var corner: Vector3 = child_3d.transform * (child_aabb.position + mask * child_aabb.size)
if found:
combined = combined.expand(corner)
else:
combined = AABB(corner, Vector3.ZERO)
found = true
return combined if found else AABB(Vector3.ZERO, Vector3.ZERO)
func _apply_material_recursive(node: Node, material: Material) -> void:
if node is CSGShape3D:
node.material_override = material
for child in node.get_children():
_apply_material_recursive(child, material)
func _apply_variations(instance: Node3D) -> void:
if _randomize_rotation:
var final_rot := instance.rotation
if _randomize_rot_x:
if _rotation_variance_x_deg > 0.0:
final_rot.x += rng.randf_range(-deg_to_rad(_rotation_variance_x_deg), deg_to_rad(_rotation_variance_x_deg))
else:
final_rot.x = rng.randf() * TAU
if _randomize_rot_y:
if _rotation_variance_y_deg > 0.0:
final_rot.y += rng.randf_range(-deg_to_rad(_rotation_variance_y_deg), deg_to_rad(_rotation_variance_y_deg))
else:
final_rot.y = rng.randf() * TAU
if _randomize_rot_z:
if _rotation_variance_z_deg > 0.0:
final_rot.z += rng.randf_range(-deg_to_rad(_rotation_variance_z_deg), deg_to_rad(_rotation_variance_z_deg))
else:
final_rot.z = rng.randf() * TAU
instance.rotation = final_rot
if _randomize_scale:
# If any axis toggles are on, apply independent variance per axis; else uniform.
var use_axes = _randomize_scale_x or _randomize_scale_y or _randomize_scale_z
if use_axes:
var sx = instance.scale.x
var sy = instance.scale.y
var sz = instance.scale.z
if _randomize_scale_x:
var vx = (_scale_variance_x if _scale_variance_x > 0.0 else _scale_variance)
sx *= max(0.1, 1.0 + rng.randf_range(-vx, vx))
if _randomize_scale_y:
var vy = (_scale_variance_y if _scale_variance_y > 0.0 else _scale_variance)
sy *= max(0.1, 1.0 + rng.randf_range(-vy, vy))
if _randomize_scale_z:
var vz = (_scale_variance_z if _scale_variance_z > 0.0 else _scale_variance)
sz *= max(0.1, 1.0 + rng.randf_range(-vz, vz))
instance.scale = Vector3(sx, sy, sz)
else:
var scale_factor = max(0.1, 1.0 + rng.randf_range(-_scale_variance, _scale_variance))
instance.scale *= scale_factor
func regenerate() -> void:
_mark_dirty()
# -- Custom property list (Godot 4.5 group enable support) -------------------------
func _get_property_list() -> Array[Dictionary]:
var props: Array[Dictionary] = []
# Keep default exported properties (engine already exposes them). Only inject
# the rotation variation cluster with group enable + subgroup organization.
# Group header for random rotation feature.
# Variation Options parent group
props.append({
"name": "Variation Options",
"type": TYPE_NIL,
"usage": PROPERTY_USAGE_GROUP
})
# Rotation subgroup under Variation Options
props.append({
"name": "Rotation Randomization",
"type": TYPE_NIL,
"usage": PROPERTY_USAGE_SUBGROUP
})
# Enabling checkbox on group header via PROPERTY_HINT_GROUP_ENABLE.
props.append({
"name": "randomize_rotation",
"type": TYPE_BOOL,
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
"hint": PROPERTY_HINT_GROUP_ENABLE
})
# Per-axis random toggles
props.append(_prop_bool("randomize_rot_x"))
if _randomize_rotation and _randomize_rot_x:
props.append({
"name": "rotation_variance_x_deg",
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0,360,0.1,degrees",
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR
})
props.append(_prop_bool("randomize_rot_y"))
if _randomize_rotation and _randomize_rot_y:
props.append({
"name": "rotation_variance_y_deg",
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0,360,0.1,degrees",
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR
})
props.append(_prop_bool("randomize_rot_z"))
if _randomize_rotation and _randomize_rot_z:
props.append({
"name": "rotation_variance_z_deg",
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": "0,360,0.1,degrees",
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR
})
# Subgroup for locked rotations (should reside inside Rotation Randomization group)
# (Locked rotations removed as per user request)
# Scale variation subgroup under Variation Options
props.append({
"name": "Scale Variation",
"type": TYPE_NIL,
"usage": PROPERTY_USAGE_SUBGROUP
})
props.append({
"name": "randomize_scale",
"type": TYPE_BOOL,
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR,
"hint": PROPERTY_HINT_GROUP_ENABLE
})
props.append(_prop_float_range("scale_variance", "0,1,0.01"))
props.append(_prop_bool("randomize_scale_x"))
if _randomize_scale and _randomize_scale_x:
props.append(_prop_float_range("scale_variance_x", "0,1,0.01"))
props.append(_prop_bool("randomize_scale_y"))
if _randomize_scale and _randomize_scale_y:
props.append(_prop_float_range("scale_variance_y", "0,1,0.01"))
props.append(_prop_bool("randomize_scale_z"))
if _randomize_scale and _randomize_scale_z:
props.append(_prop_float_range("scale_variance_z", "0,1,0.01"))
# Read-only instance count display
props.append({
"name": "estimated_instances",
"type": TYPE_INT,
"usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_READ_ONLY
})
return props
func _prop_bool(name: String) -> Dictionary:
return {
"name": name,
"type": TYPE_BOOL,
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR
}
func _prop_float_range(name: String, hint_str: String) -> Dictionary:
return {
"name": name,
"type": TYPE_FLOAT,
"hint": PROPERTY_HINT_RANGE,
"hint_string": hint_str,
"usage": PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_EDITOR
}
func get_instance_count() -> int:
if pattern == null:
return 0
var ctx := {"template_size": Vector3.ONE, "rng": rng, "position_jitter": _position_jitter}
return mini(pattern.get_estimated_count(ctx), MAX_INSTANCES)
func apply_template() -> void:
if get_child_count() == 0:
return
var target_owner: Node = owner
if target_owner == null:
if get_tree() and get_tree().edited_scene_root:
target_owner = get_tree().edited_scene_root
else:
target_owner = self
var stack: Array[Node] = []
stack.append_array(get_children())
while stack.size() > 0:
var node: Node = stack.pop_back()
node.set_owner(target_owner)
stack.append_array(node.get_children())
# Alias for clarity in UI
func bake_instances() -> void:
if get_child_count() == 0:
return
var target_owner: Node = owner
if target_owner == null:
if get_tree() and get_tree().edited_scene_root:
target_owner = get_tree().edited_scene_root
else:
target_owner = self
var baked: Array[Node] = []
for child in get_children(true):
if child.has_meta(REPEATER_NODE_META):
baked.append(child)
var stack: Array[Node] = []
stack.append_array(child.get_children())
while stack.size() > 0:
var node: Node = stack.pop_back()
baked.append(node)
stack.append_array(node.get_children())
if baked.is_empty():
return
if Engine.is_editor_hint() and CsgBlockout.undo_manager:
var um: EditorUndoRedoManager = CsgBlockout.undo_manager
um.create_action(CsgBlockoutI18n.t("BAKE"))
for node in baked:
um.add_undo_reference(node)
um.add_do_property(node, "owner", target_owner)
um.add_undo_property(node, "owner", node.owner)
um.add_do_method(self, "_bake_clear_meta", baked)
um.add_undo_method(self, "_bake_restore_meta", baked)
um.commit_action()
else:
for node in baked:
node.set_owner(target_owner)
_bake_clear_meta(baked)
func _bake_clear_meta(nodes: Array[Node]) -> void:
for node in nodes:
if is_instance_valid(node):
node.remove_meta(REPEATER_NODE_META)
func _bake_restore_meta(nodes: Array[Node]) -> void:
for node in nodes:
if is_instance_valid(node):
node.set_meta(REPEATER_NODE_META, true)
@@ -1 +0,0 @@
uid://c68dxahp0v5xg
@@ -1,3 +0,0 @@
@tool
# CSG Shortcut Manager has been removed. Shortcuts disabled.
extends Node
@@ -1 +0,0 @@
uid://1bqvx7teqnrl
@@ -1,467 +0,0 @@
@tool
class_name CSGSideBlockoutBar extends Control
var config: CsgBlockoutConfig:
get: return CsgBlockoutConfig.get_config()
const BASE_BAR_MIN_WIDTH: float = 52.0
const BASE_BTN_SIZE: Vector2 = Vector2(42.0, 42.0)
const BASE_MATERIAL_BTN_SIZE: Vector2 = Vector2(42.0, 36.0)
const BASE_ICON_MAX_WIDTH: int = 24
const BASE_MATERIAL_ICON_MAX_WIDTH: int = 22
const BASE_CONTAINER_SEPARATION: int = 12
const BASE_MARGIN_VERTICAL: int = 8
const BASE_LANG_BTN_SIZE: Vector2 = Vector2(36.0, 22.0)
const BASE_LANG_FONT_SIZE: int = 11
var button_tweens: Dictionary = {}
var visibility_tween: Tween
func _enter_tree() -> void:
add_to_group("csg_blockout_ui")
var sel := EditorInterface.get_selection()
if sel and not sel.selection_changed.is_connected(_on_selection_changed):
sel.selection_changed.connect(_on_selection_changed)
var cfg := config
if cfg and not cfg.default_operation_changed.is_connected(_on_default_operation_changed):
cfg.default_operation_changed.connect(_on_default_operation_changed)
CsgBlockoutI18n.translate_node(self)
func _exit_tree() -> void:
var sel := EditorInterface.get_selection()
if sel and sel.selection_changed.is_connected(_on_selection_changed):
sel.selection_changed.disconnect(_on_selection_changed)
var cfg := config
if cfg and cfg.default_operation_changed.is_connected(_on_default_operation_changed):
cfg.default_operation_changed.disconnect(_on_default_operation_changed)
func _on_default_operation_changed(_op: CSGShape3D.Operation) -> void:
_sync_operation_buttons()
func _on_selection_changed() -> void:
var auto_hide_enabled := config.auto_hide if config else true
if not auto_hide_enabled:
_fade_in()
return
var selection = EditorInterface.get_selection().get_selected_nodes()
if selection.any(func(node): return node is CSGShape3D):
_fade_in()
else:
_fade_out()
func _fade_in() -> void:
if visible and modulate.a >= 0.99: return
if visibility_tween and visibility_tween.is_valid():
visibility_tween.kill()
if not visible:
modulate.a = 0.0
show()
visibility_tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
visibility_tween.tween_property(self, "modulate:a", 1.0, 0.2)
func _fade_out() -> void:
if not visible: return
if visibility_tween and visibility_tween.is_valid():
visibility_tween.kill()
visibility_tween = create_tween().set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
visibility_tween.tween_property(self, "modulate:a", 0.0, 0.15)
visibility_tween.tween_callback(self.hide)
func _apply_editor_scale() -> void:
var ed_scale: float = 1.0
if Engine.is_editor_hint():
ed_scale = EditorInterface.get_editor_scale()
ed_scale = maxf(ed_scale, 0.1)
custom_minimum_size.x = round(BASE_BAR_MIN_WIDTH * ed_scale)
add_theme_constant_override("margin_top", int(round(BASE_MARGIN_VERTICAL * ed_scale)))
add_theme_constant_override("margin_bottom", int(round(BASE_MARGIN_VERTICAL * ed_scale)))
var hbox: Container = find_child("HBoxContainer", true, false) as Container
if hbox:
hbox.add_theme_constant_override("separation", int(round(BASE_CONTAINER_SEPARATION * ed_scale)))
var csg_box: Container = find_child("CSG", true, false) as Container
if csg_box:
for child in csg_box.get_children():
if child is Button:
child.custom_minimum_size = BASE_BTN_SIZE * ed_scale
child.add_theme_constant_override("icon_max_width", int(round(BASE_ICON_MAX_WIDTH * ed_scale)))
var op_box: Container = find_child("Operation", true, false) as Container
if op_box:
for child in op_box.get_children():
if child is Button:
child.custom_minimum_size = BASE_BTN_SIZE * ed_scale
child.add_theme_constant_override("icon_max_width", int(round(BASE_ICON_MAX_WIDTH * ed_scale)))
var mat_box: Container = find_child("Material", true, false) as Container
if mat_box:
for child in mat_box.get_children():
if child is Button:
child.custom_minimum_size = BASE_MATERIAL_BTN_SIZE * ed_scale
child.add_theme_constant_override("icon_max_width", int(round(BASE_MATERIAL_ICON_MAX_WIDTH * ed_scale)))
var lang_btn: OptionButton = find_child("LanguageToggle", true, false) as OptionButton
if lang_btn:
lang_btn.custom_minimum_size = BASE_LANG_BTN_SIZE * ed_scale
lang_btn.add_theme_font_size_override("font_size", int(round(BASE_LANG_FONT_SIZE * ed_scale)))
func _ready() -> void:
add_to_group("csg_blockout_ui")
_apply_editor_scale()
CsgBlockoutI18n.translate_node(self)
var lang_btn = find_child("LanguageToggle", true, false)
if lang_btn and lang_btn is OptionButton:
lang_btn.clear()
lang_btn.add_item("Auto", 0)
lang_btn.add_item("EN", 1)
lang_btn.add_item("中文", 2)
lang_btn.add_item("日本語", 3)
lang_btn.add_item("한국어", 4)
lang_btn.add_item("Español", 5)
lang_btn.add_item("Português", 6)
lang_btn.add_item("Русский", 7)
_update_language_toggle_text()
_setup_button_animations(self)
_sync_preset_buttons()
_sync_operation_buttons()
func update_language() -> void:
CsgBlockoutI18n.translate_node(self)
_update_language_toggle_text()
func _update_language_toggle_text() -> void:
var btn = find_child("LanguageToggle", true, false)
if btn and btn is OptionButton:
var override_lang = config.language_override if config else "auto"
match override_lang:
"auto", "":
btn.select(0)
"en":
btn.select(1)
"zh_CN", "zh":
btn.select(2)
"ja":
btn.select(3)
"ko":
btn.select(4)
"es":
btn.select(5)
"pt", "pt_BR", "pt_PT":
btn.select(6)
"ru":
btn.select(7)
_:
btn.select(0)
func _sync_preset_buttons() -> void:
if not config:
return
var preset = config.material_preset
var btn_light: Button = find_child("PresetLight", true, false) as Button
var btn_dark: Button = find_child("PresetDark", true, false) as Button
var btn_orange: Button = find_child("PresetOrange", true, false) as Button
var btn_none: Button = find_child("PresetNone", true, false) as Button
var btn_picker: Button = find_child("MaterialPicker", true, false) as Button
if btn_light: btn_light.button_pressed = (preset == CsgBlockoutConfig.MaterialPreset.GRID_LIGHT)
if btn_dark: btn_dark.button_pressed = (preset == CsgBlockoutConfig.MaterialPreset.GRID_DARK)
if btn_orange: btn_orange.button_pressed = (preset == CsgBlockoutConfig.MaterialPreset.GRID_ORANGE)
if btn_none: btn_none.button_pressed = (preset == CsgBlockoutConfig.MaterialPreset.NONE)
if btn_picker: btn_picker.button_pressed = (preset == CsgBlockoutConfig.MaterialPreset.CUSTOM)
func _setup_button_animations(node: Node) -> void:
if node is Button and not (node is OptionButton):
call_deferred("_apply_pivot", node)
if not node.resized.is_connected(_apply_pivot.bind(node)):
node.resized.connect(_apply_pivot.bind(node))
node.mouse_entered.connect(_on_btn_mouse_entered.bind(node))
node.mouse_exited.connect(_on_btn_mouse_exited.bind(node))
node.button_down.connect(_on_btn_down.bind(node))
node.button_up.connect(_on_btn_up.bind(node))
if node.toggle_mode:
node.toggled.connect(_on_btn_toggled.bind(node))
for child in node.get_children():
_setup_button_animations(child)
func _apply_pivot(btn: Button) -> void:
btn.pivot_offset = btn.size / 2.0
func _animate_button(btn: Button, target_scale: Vector2, target_modulate: Color, duration: float, is_punch: bool = false) -> void:
if button_tweens.has(btn) and is_instance_valid(button_tweens[btn]):
button_tweens[btn].kill()
var tw = create_tween()
if not is_punch:
tw.set_trans(Tween.TRANS_BACK).set_ease(Tween.EASE_OUT)
else:
tw.set_trans(Tween.TRANS_QUAD).set_ease(Tween.EASE_OUT)
tw.set_parallel(true)
tw.tween_property(btn, "scale", target_scale, duration)
tw.tween_property(btn, "modulate", target_modulate, duration)
button_tweens[btn] = tw
func _on_btn_mouse_entered(btn: Button) -> void:
var color = Color(1.3, 1.3, 1.3, 1.0) if not btn.button_pressed else Color(1.3, 1.5, 2.0, 1.0)
_animate_button(btn, Vector2(1.08, 1.08), color, 0.2)
func _on_btn_mouse_exited(btn: Button) -> void:
var color = Color.WHITE if not btn.button_pressed else Color(0.8, 1.2, 2.0, 1.0)
_animate_button(btn, Vector2(1.0, 1.0), color, 0.2)
func _on_btn_down(btn: Button) -> void:
_animate_button(btn, Vector2(0.9, 0.9), btn.modulate, 0.1, true)
func _on_btn_up(btn: Button) -> void:
var is_hovered = btn.get_global_rect().has_point(get_global_mouse_position())
var scale = Vector2(1.08, 1.08) if is_hovered else Vector2(1.0, 1.0)
var mod = Color(1.3, 1.3, 1.3, 1.0) if is_hovered else Color.WHITE
if btn.toggle_mode and btn.button_pressed:
mod = Color(1.3, 1.5, 2.0, 1.0) if is_hovered else Color(0.8, 1.2, 2.0, 1.0)
_animate_button(btn, scale, mod, 0.2)
func _on_btn_toggled(pressed: bool, btn: Button) -> void:
var is_hovered = btn.get_global_rect().has_point(get_global_mouse_position())
var scale = Vector2(1.08, 1.08) if is_hovered else Vector2(1.0, 1.0)
var mod = Color(1.3, 1.3, 1.3, 1.0) if is_hovered else Color.WHITE
if pressed:
mod = Color(1.3, 1.5, 2.0, 1.0) if is_hovered else Color(0.8, 1.2, 2.0, 1.0)
_animate_button(btn, scale, mod, 0.2)
func _on_box_pressed() -> void:
create_csg(CSGBox3D)
func _on_cylinder_pressed() -> void:
create_csg(CSGCylinder3D)
func _on_mesh_pressed() -> void:
create_csg(CSGMesh3D)
func _on_polygon_pressed() -> void:
create_csg(CSGPolygon3D)
func _on_sphere_pressed() -> void:
create_csg(CSGSphere3D)
func _on_torus_pressed() -> void:
create_csg(CSGTorus3D)
func _on_operation_pressed(val := 0) -> void:
set_operation(val)
func set_operation(val: int) -> void:
if not config:
return
match val:
0: config.default_operation = CSGShape3D.OPERATION_UNION
1: config.default_operation = CSGShape3D.OPERATION_INTERSECTION
2: config.default_operation = CSGShape3D.OPERATION_SUBTRACTION
_: config.default_operation = CSGShape3D.OPERATION_UNION
func _sync_operation_buttons() -> void:
var op: CSGShape3D.Operation = config.default_operation if config else CSGShape3D.OPERATION_UNION
var btn_union: Button = find_child("Union", true, false) as Button
var btn_intersection: Button = find_child("Intersection", true, false) as Button
var btn_subtraction: Button = find_child("Subtraction", true, false) as Button
if btn_union: btn_union.button_pressed = (op == CSGShape3D.OPERATION_UNION)
if btn_intersection: btn_intersection.button_pressed = (op == CSGShape3D.OPERATION_INTERSECTION)
if btn_subtraction: btn_subtraction.button_pressed = (op == CSGShape3D.OPERATION_SUBTRACTION)
# Material Preset Handlers
func _on_preset_light_pressed() -> void:
if config:
config.material_preset = CsgBlockoutConfig.MaterialPreset.GRID_LIGHT
config.save_config()
_sync_preset_buttons()
func _on_preset_dark_pressed() -> void:
if config:
config.material_preset = CsgBlockoutConfig.MaterialPreset.GRID_DARK
config.save_config()
_sync_preset_buttons()
func _on_preset_orange_pressed() -> void:
if config:
config.material_preset = CsgBlockoutConfig.MaterialPreset.GRID_ORANGE
config.save_config()
_sync_preset_buttons()
func _on_preset_none_pressed() -> void:
if config:
config.material_preset = CsgBlockoutConfig.MaterialPreset.NONE
config.save_config()
_sync_preset_buttons()
func _on_material_picker_pressed() -> void:
_request_material()
func _on_apply_to_selected_pressed() -> void:
var selection = EditorInterface.get_selection()
if not selection:
return
var selected_nodes = selection.get_selected_nodes().filter(func(n): return n is CSGShape3D)
if selected_nodes.is_empty():
push_warning(CsgBlockoutI18n.t("WARN_SELECT_CSG_SHAPE"))
return
var active_mat: Material = config.get_active_material() if config else null
if CsgBlockout.undo_manager:
CsgBlockout.undo_manager.create_action(CsgBlockoutI18n.t("APPLY_MATERIAL"))
for node in selected_nodes:
CsgBlockout.undo_manager.add_do_property(node, "material", active_mat)
CsgBlockout.undo_manager.add_undo_property(node, "material", (node as CSGShape3D).material)
CsgBlockout.undo_manager.commit_action()
else:
for node in selected_nodes:
(node as CSGShape3D).material = active_mat
func _request_material() -> void:
var dialog := EditorFileDialog.new()
dialog.title = CsgBlockoutI18n.t("SELECT_MATERIAL")
dialog.display_mode = EditorFileDialog.DISPLAY_LIST
dialog.filters = ["*.tres, *.material, *.res"]
dialog.file_mode = EditorFileDialog.FILE_MODE_OPEN_FILE
if EditorInterface.get_base_control():
var ed_scale: float = EditorInterface.get_editor_scale() if Engine.is_editor_hint() else 1.0
dialog.popup_centered_clamped(Vector2(680, 480) * ed_scale)
var cleanup_dialog := func() -> void:
if is_instance_valid(dialog) and dialog.get_parent():
dialog.get_parent().remove_child(dialog)
dialog.queue_free()
var on_file_selected := func(path: String) -> void:
cleanup_dialog.call()
if path.is_empty():
return
var res = ResourceLoader.load(path)
if res == null or not (res is Material):
return
if config:
config.custom_material = res as Material
config.material_preset = CsgBlockoutConfig.MaterialPreset.CUSTOM
config.save_config()
_sync_preset_buttons()
var previewer = EditorInterface.get_resource_previewer()
if previewer:
previewer.queue_edited_resource_preview(res, self, "_update_picker_icon", null)
var on_canceled := func() -> void:
cleanup_dialog.call()
_sync_preset_buttons()
dialog.file_selected.connect(on_file_selected)
dialog.canceled.connect(on_canceled)
get_tree().root.add_child(dialog)
dialog.show()
func _update_picker_icon(path: String, preview: Texture2D, thumbnail: Texture2D, userdata: Variant) -> void:
var btn_picker: Button = find_child("MaterialPicker", true, false) as Button
if preview and btn_picker:
btn_picker.icon = preview
func create_csg(type: Variant) -> void:
var selection = EditorInterface.get_selection()
var selected_nodes = selection.get_selected_nodes()
if selected_nodes.is_empty() or !(selected_nodes[0] is CSGShape3D):
push_warning(CsgBlockoutI18n.t("WARN_SELECT_CSG_SHAPE"))
return
var selected_node: CSGShape3D = selected_nodes[0]
var csg: CSGShape3D
match type:
CSGBox3D: csg = CSGBox3D.new()
CSGCylinder3D: csg = CSGCylinder3D.new()
CSGSphere3D: csg = CSGSphere3D.new()
CSGMesh3D: csg = CSGMesh3D.new()
CSGPolygon3D: csg = CSGPolygon3D.new()
CSGTorus3D: csg = CSGTorus3D.new()
_:
push_warning(CsgBlockoutI18n.t("WARN_UNSUPPORTED_CSG_TYPE"))
return
csg.operation = config.default_operation if config else CSGShape3D.OPERATION_UNION
if config:
csg.material = config.get_active_material()
var owner_ref = EditorInterface.get_edited_scene_root()
if owner_ref == null:
owner_ref = selected_node.get_owner()
if owner_ref == null:
owner_ref = selected_node
var parent: Node = null
var insert_index: int = -1
if selected_node is CSGCombiner3D:
parent = selected_node
insert_index = parent.get_child_count()
else:
parent = selected_node.get_parent()
if parent == null:
parent = selected_node
insert_index = parent.get_child_count()
else:
insert_index = selected_node.get_index() + 1
if CsgBlockout.undo_manager:
CsgBlockout.undo_manager.create_action(CsgBlockoutI18n.tf("ADD_NODE", [csg.get_class()]))
CsgBlockout.undo_manager.add_undo_reference(csg)
CsgBlockout.undo_manager.add_do_method(self, "_undoable_add_csg", parent, csg, owner_ref, selected_node.global_position, insert_index)
CsgBlockout.undo_manager.add_do_method(self, "_select_created_csg", csg)
CsgBlockout.undo_manager.add_undo_method(self, "_undoable_remove_csg", parent, csg)
CsgBlockout.undo_manager.add_undo_method(self, "_clear_selection_if", csg)
CsgBlockout.undo_manager.commit_action()
else:
parent.add_child(csg, true)
if insert_index >= 0 and insert_index < parent.get_child_count():
parent.move_child(csg, insert_index)
csg.owner = owner_ref
csg.global_position = selected_node.global_position
call_deferred("_select_created_csg", csg)
func _deferred_select(csg: Node) -> void:
call_deferred("_select_created_csg", csg)
func _undoable_add_csg(parent: Node, csg: CSGShape3D, owner_ref: Node, global_pos: Vector3, insert_index: int) -> void:
if csg.get_parent() != parent:
parent.add_child(csg, true)
if insert_index >= 0 and insert_index < parent.get_child_count():
parent.move_child(csg, insert_index)
csg.owner = owner_ref
csg.global_position = global_pos
func _undoable_remove_csg(parent: Node, csg: CSGShape3D) -> void:
if is_instance_valid(csg) and csg.get_parent() == parent:
parent.remove_child(csg)
func _clear_selection_if(csg: Node) -> void:
var selection = EditorInterface.get_selection()
if selection:
var nodes: Array = selection.get_selected_nodes()
if csg in nodes:
selection.remove_node(csg)
func _select_created_csg(csg: Node) -> void:
if not is_instance_valid(csg) or not csg.is_inside_tree():
return
var selection = EditorInterface.get_selection()
selection.clear()
selection.add_node(csg)
func _on_language_toggle_item_selected(index: int) -> void:
if config:
match index:
0: config.language_override = "auto"
1: config.language_override = "en"
2: config.language_override = "zh_CN"
3: config.language_override = "ja"
4: config.language_override = "ko"
5: config.language_override = "es"
6: config.language_override = "pt"
7: config.language_override = "ru"
config.save_config()
get_tree().call_group("csg_blockout_ui", "update_language")
@@ -1 +0,0 @@
uid://dr5f1egll7hdq
@@ -1,388 +0,0 @@
@tool
class_name CSGSpreader3D extends CSGCombiner3D
const SPREADER_NODE_META = "SPREADER_NODE_META"
const MAX_INSTANCES = 200
const REGEN_THROTTLE_MS: int = 150
var _dirty: bool = false
var _last_regen_ms: int = 0
var _generation_in_progress := false
var _template_node: Node3D
@export var template_node: Node3D:
get: return _template_node
set(value):
_template_node = value
_mark_dirty()
var _hide_template: bool = true
@export var hide_template: bool = true:
get: return _hide_template
set(value):
_hide_template = value
_update_template_visibility()
var _spread_area_3d: Shape3D = null
@export var spread_area_3d: Shape3D = null:
get: return _spread_area_3d
set(value):
_spread_area_3d = value
_mark_dirty()
var _max_count: int = 10
@export var max_count: int = 10:
get: return _max_count
set(value):
_max_count = clamp(value, 1, 100000)
_mark_dirty()
@export_group("Spread Options")
var _noise_threshold: float = 0.5
@export var noise_threshold: float = 0.5:
get: return _noise_threshold
set(value):
_noise_threshold = clamp(value, 0.0, 1.0)
_mark_dirty()
var _seed: int = 0
@export var seed: int = 0:
get: return _seed
set(value):
_seed = value
_mark_dirty()
var _allow_rotation: bool = false
@export var allow_rotation: bool = false:
get: return _allow_rotation
set(value):
_allow_rotation = value
_mark_dirty()
var _allow_scale: bool = false
@export var allow_scale: bool = false:
get: return _allow_scale
set(value):
_allow_scale = value
_mark_dirty()
var _snap_distance: float = 0.0
@export var snap_distance: float = 0.0:
get: return _snap_distance
set(value):
_snap_distance = value
_mark_dirty()
@export_group("Collision Options")
var _avoid_overlaps: bool = false
@export var avoid_overlaps: bool = false:
get: return _avoid_overlaps
set(value):
_avoid_overlaps = value
_mark_dirty()
var _min_distance: float = 1.0
@export var min_distance: float = 1.0:
get: return _min_distance
set(value):
_min_distance = max(0.0, value)
_mark_dirty()
var _max_placement_attempts: int = 100
@export var max_placement_attempts: int = 100:
get: return _max_placement_attempts
set(value):
_max_placement_attempts = clamp(value, 10, 1000)
_mark_dirty()
var estimated_instances: int = 0
var rng: RandomNumberGenerator
func _ready() -> void:
rng = RandomNumberGenerator.new()
_mark_dirty()
func _process(_delta: float) -> void:
if not Engine.is_editor_hint(): return
if _dirty and not _generation_in_progress:
var now := Time.get_ticks_msec()
if now - _last_regen_ms < REGEN_THROTTLE_MS:
return # 保留 _dirty,下帧再试,形成节流
_last_regen_ms = now
_dirty = false
call_deferred("spread_template")
func _exit_tree() -> void:
if not Engine.is_editor_hint():
return
clear_children()
func _mark_dirty() -> void:
_dirty = true
func _update_template_visibility() -> void:
if not is_inside_tree():
return
if is_instance_valid(_template_node):
_template_node.visible = not _hide_template
func clear_children() -> void:
var children_to_remove: Array[Node] = []
for child in get_children():
if child.has_meta(SPREADER_NODE_META):
children_to_remove.append(child)
for child in children_to_remove:
remove_child(child)
child.queue_free()
func get_random_position_in_area() -> Vector3:
if spread_area_3d == null:
return Vector3.ZERO
if spread_area_3d is SphereShape3D:
var radius = spread_area_3d.get_radius()
var u = rng.randf()
var v = rng.randf()
var theta = u * TAU
var phi = acos(2.0 * v - 1.0)
var r = radius * pow(rng.randf(), 1.0 / 3.0)
return Vector3(r * sin(phi) * cos(theta), r * sin(phi) * sin(theta), r * cos(phi))
if spread_area_3d is BoxShape3D:
var size = spread_area_3d.size
return Vector3(
rng.randf_range(-size.x * 0.5, size.x * 0.5),
rng.randf_range(-size.y * 0.5, size.y * 0.5),
rng.randf_range(-size.z * 0.5, size.z * 0.5)
)
if spread_area_3d is CapsuleShape3D:
var capsule: CapsuleShape3D = spread_area_3d
var radius: float = capsule.radius
var half_mid: float = maxf(0.0, capsule.height - 2.0 * capsule.radius) * 0.5
if rng.randf() < 0.5:
var angle: float = rng.randf() * TAU
var r: float = radius * sqrt(rng.randf())
return Vector3(r * cos(angle), rng.randf_range(-half_mid, half_mid), r * sin(angle))
var hemisphere_y: float = half_mid if rng.randf() < 0.5 else -half_mid
var theta: float = rng.randf() * TAU
var phi: float = acos(1.0 - rng.randf())
var cap_r: float = radius * pow(rng.randf(), 1.0 / 3.0)
var cap_sign: float = 1.0 if hemisphere_y > 0.0 else -1.0
return Vector3(
cap_r * sin(phi) * cos(theta),
hemisphere_y + cap_r * cos(phi) * cap_sign,
cap_r * sin(phi) * sin(theta)
)
if spread_area_3d is CylinderShape3D:
var radius = spread_area_3d.get_radius()
var height = spread_area_3d.get_height() * 0.5
var angle = rng.randf() * TAU
var r = radius * sqrt(rng.randf())
return Vector3(r * cos(angle), rng.randf_range(-height, height), r * sin(angle))
if spread_area_3d is HeightMapShape3D:
var width = spread_area_3d.map_width
var depth = spread_area_3d.map_depth
if width <= 0 or depth <= 0 or spread_area_3d.map_data.size() == 0:
return Vector3.ZERO
var x = rng.randi_range(0, width - 1)
var z = rng.randi_range(0, depth - 1)
var index = x + z * width
if index < spread_area_3d.map_data.size():
return Vector3(x, spread_area_3d.map_data[index], z)
return Vector3.ZERO
if spread_area_3d is WorldBoundaryShape3D:
var bound = 100.0
return Vector3(rng.randf_range(-bound, bound), 0, rng.randf_range(-bound, bound))
if spread_area_3d is ConcavePolygonShape3D:
var faces: PackedVector3Array = spread_area_3d.get_faces()
if faces.size() < 3:
return Vector3.ZERO
var tri_count = faces.size() / 3
var tri_idx = rng.randi_range(0, tri_count - 1) * 3
var a = faces[tri_idx]
var b = faces[tri_idx+1]
var c = faces[tri_idx+2]
var r1 = sqrt(rng.randf())
var r2 = rng.randf()
return (1.0 - r1) * a + (r1 * (1.0 - r2)) * b + (r1 * r2) * c
if spread_area_3d is ConvexPolygonShape3D:
var pts: PackedVector3Array = spread_area_3d.points
if pts.is_empty():
return Vector3.ZERO
var min_point = pts[0]
var max_point = pts[0]
for p in pts:
min_point = min_point.min(p)
max_point = max_point.max(p)
return Vector3(
rng.randf_range(min_point.x, max_point.x),
rng.randf_range(min_point.y, max_point.y),
rng.randf_range(min_point.z, max_point.z)
)
push_warning("CSGSpreader3D: Shape type not supported")
return Vector3.ZERO
func spread_template() -> void:
if _generation_in_progress:
return
_generation_in_progress = true
if not spread_area_3d:
_generation_in_progress = false
return
clear_children()
if not is_instance_valid(_template_node):
_generation_in_progress = false
return
var template_node: Node3D = _template_node
rng.seed = _seed
var instances_created: int = 0
var placed_positions: Array[Vector3] = []
var budget: int = min(_max_count, MAX_INSTANCES)
if _max_count > MAX_INSTANCES:
push_warning("CSGSpreader3D: max_count %s exceeds cap %s. Limiting." % [_max_count, MAX_INSTANCES])
# Spatial Hash Grid for O(1) distance checks
# Cell edge must be >= min_distance so any point within min_distance of a
# candidate is always inside the +/-1 neighborhood (27 cells).
var cell_size: float = maxf(_min_distance, 0.001)
var spatial_grid: Dictionary = {} # Vector3i -> Array[Vector3]
for i in range(budget):
var noise_value: float = rng.randf()
if noise_value <= _noise_threshold:
continue
var position_found: bool = false
var final_position: Vector3 = Vector3.ZERO
var attempts: int = _max_placement_attempts if _avoid_overlaps else 1
for attempt in range(attempts):
var test_position: Vector3 = get_random_position_in_area()
if not _avoid_overlaps:
final_position = test_position
position_found = true
break
var overlap: bool = false
if cell_size > 0.001:
# O(1) spatial grid lookup
var grid_x: int = int(floor(test_position.x / cell_size))
var grid_y: int = int(floor(test_position.y / cell_size))
var grid_z: int = int(floor(test_position.z / cell_size))
# Check surrounding 27 cells (each cell holds all positions in it)
for dx in range(-1, 2):
for dy in range(-1, 2):
for dz in range(-1, 2):
var cell_key := Vector3i(grid_x + dx, grid_y + dy, grid_z + dz)
if not spatial_grid.has(cell_key):
continue
for existing_pos: Vector3 in spatial_grid[cell_key]:
if test_position.distance_to(existing_pos) < _min_distance:
overlap = true
break
if overlap: break
if overlap: break
if overlap: break
else:
# Fallback if min_distance is virtually zero
for existing_pos in placed_positions:
if test_position.distance_to(existing_pos) < _min_distance:
overlap = true
break
if not overlap:
final_position = test_position
position_found = true
break
if not position_found:
continue
var instance: Node = template_node.duplicate()
if instance == null:
continue
instance.set_meta(SPREADER_NODE_META, true)
if instance is Node3D:
instance.transform.origin = final_position
instance.visible = true
if _allow_rotation:
var rotation_y: float = rng.randf_range(0.0, TAU)
instance.rotate_y(rotation_y)
if _allow_scale:
var scale_factor: float = rng.randf_range(0.5, 2.0)
instance.scale *= scale_factor
placed_positions.append(final_position)
if _avoid_overlaps and cell_size > 0.001:
var key := Vector3i(
int(floor(final_position.x / cell_size)),
int(floor(final_position.y / cell_size)),
int(floor(final_position.z / cell_size))
)
var cell_list: Array[Vector3] = []
if spatial_grid.has(key):
cell_list = spatial_grid[key]
cell_list.append(final_position)
spatial_grid[key] = cell_list
add_child(instance)
instances_created += 1
estimated_instances = instances_created
_update_template_visibility()
_generation_in_progress = false
func bake_instances() -> void:
if get_child_count() == 0:
return
var target_owner: Node = owner
if target_owner == null:
if get_tree() and get_tree().edited_scene_root:
target_owner = get_tree().edited_scene_root
else:
target_owner = self
var baked: Array[Node] = []
for child in get_children(true):
if child.has_meta(SPREADER_NODE_META):
baked.append(child)
var stack: Array[Node] = []
stack.append_array(child.get_children())
while stack.size() > 0:
var node: Node = stack.pop_back()
baked.append(node)
stack.append_array(node.get_children())
if baked.is_empty():
return
if Engine.is_editor_hint() and CsgBlockout.undo_manager:
var um: EditorUndoRedoManager = CsgBlockout.undo_manager
um.create_action(CsgBlockoutI18n.t("BAKE"))
for node in baked:
um.add_undo_reference(node)
um.add_do_property(node, "owner", target_owner)
um.add_undo_property(node, "owner", node.owner)
um.add_do_method(self, "_bake_clear_meta", baked)
um.add_undo_method(self, "_bake_restore_meta", baked)
um.commit_action()
else:
for node in baked:
node.set_owner(target_owner)
_bake_clear_meta(baked)
func _bake_clear_meta(nodes: Array[Node]) -> void:
for node in nodes:
if is_instance_valid(node):
node.remove_meta(SPREADER_NODE_META)
func _bake_restore_meta(nodes: Array[Node]) -> void:
for node in nodes:
if is_instance_valid(node):
node.set_meta(SPREADER_NODE_META, true)
func _get_property_list() -> Array[Dictionary]:
return [{
"name": "estimated_instances",
"type": TYPE_INT,
"usage": PROPERTY_USAGE_EDITOR | PROPERTY_USAGE_STORAGE | PROPERTY_USAGE_READ_ONLY
}]
@@ -1 +0,0 @@
uid://rgfomqnhbhpk
@@ -1,54 +0,0 @@
@tool
class_name CSGTopBlockoutBar extends Control
func _enter_tree() -> void:
add_to_group("csg_blockout_ui")
var sel := EditorInterface.get_selection()
if sel and not sel.selection_changed.is_connected(_on_selection_changed):
sel.selection_changed.connect(_on_selection_changed)
_on_selection_changed()
var refresh_btn = find_child("Refresh", true, false)
if refresh_btn and refresh_btn is Button:
refresh_btn.set_meta("i18n_text_key", "REFRESH")
refresh_btn.set_meta("i18n_tooltip_key", "REGEN_PREVIEW_TOOLTIP")
var bake_btn = find_child("Bake", true, false)
if bake_btn and bake_btn is Button:
bake_btn.set_meta("i18n_text_key", "BAKE")
bake_btn.set_meta("i18n_tooltip_key", "BAKE_INSTANCES_TOOLTIP")
CsgBlockoutI18n.translate_node(self)
func update_language() -> void:
CsgBlockoutI18n.translate_node(self)
func _exit_tree() -> void:
var sel := EditorInterface.get_selection()
if sel and sel.selection_changed.is_connected(_on_selection_changed):
sel.selection_changed.disconnect(_on_selection_changed)
func _on_selection_changed() -> void:
var selection = EditorInterface.get_selection().get_selected_nodes()
if selection.is_empty():
hide()
elif selection[0] is CSGRepeater3D or selection[0] is CSGSpreader3D:
show()
else:
hide()
func _on_refresh_pressed() -> void:
var selection = EditorInterface.get_selection().get_selected_nodes()
if (selection.is_empty()):
return
if selection[0] is CSGRepeater3D:
selection[0].call("repeat_template")
elif selection[0] is CSGSpreader3D:
selection[0].call("spread_template")
func _on_bake_pressed() -> void:
var selection = EditorInterface.get_selection().get_selected_nodes()
if selection.is_empty():
return
if selection[0] is CSGRepeater3D or selection[0] is CSGSpreader3D:
selection[0].call("bake_instances")
@@ -1 +0,0 @@
uid://dk6dt8fk1s43s
@@ -1,31 +0,0 @@
@tool
class_name CSGCircularPattern
extends CSGPattern
@export var radius: float = 5.0
@export var points: int = 8
@export var layers: int = 1
## If 0 use template_size.y
@export var layer_height: float = 0.0
## Additional gap added per layer beyond base height
@export var layer_spacing: float = 0.0
func _generate(ctx: Dictionary) -> Array:
var positions: Array = []
var template_size: Vector3 = ctx.get("template_size", Vector3.ONE)
var rad: float = max(0.0, radius)
var count: int = max(1, points)
if count <= 1:
return [Vector3.ZERO]
var lyr_count = max(1, layers)
var base_y = layer_height if layer_height > 0.0 else template_size.y
var step_y = base_y + max(0.0, layer_spacing)
for i in range(count):
var angle = (i * TAU) / count
var base_pos = Vector3(cos(angle) * rad, 0, sin(angle) * rad)
for layer in range(lyr_count):
positions.append(base_pos + Vector3(0, layer * step_y, 0))
return positions
func get_estimated_count(ctx: Dictionary) -> int:
return max(1, points) * max(1, layers)
@@ -1 +0,0 @@
uid://b3ws8vwtsqmjt
@@ -1,19 +0,0 @@
@tool
@abstract
class_name CSGPattern
extends Resource
## Base pattern interface. Subclasses implement _generate(RepeaterContext) returning Array[Vector3].
# Common interface call
func generate(ctx: Dictionary) -> Array:
# ctx expected keys: repeat: Vector3i, spacing: Vector3, rng: RandomNumberGenerator, step_spacing: Vector3, user: Node (repeater)
return _generate(ctx)
func _generate(_ctx: Dictionary) -> Array:
return []
func get_estimated_count(ctx: Dictionary) -> int:
# Default: fallback to generating (may be overridden for performance/accuracy)
var arr = _generate(ctx)
return arr.size()
@@ -1 +0,0 @@
uid://dbavf0pl65chb
@@ -1,36 +0,0 @@
@tool
class_name CSGGridPattern
extends CSGPattern
@export var count_x: int = 2
@export var count_y: int = 1
@export var count_z: int = 1
@export var spacing: Vector3 = Vector3.ZERO
## If true, automatically adds template AABB size to spacing for proper object separation
@export var use_template_size: bool = true
func _generate(ctx: Dictionary) -> Array:
var positions: Array = []
var template_size: Vector3 = ctx.get("template_size", Vector3.ONE)
var jitter: float = ctx.get("position_jitter", 0.0)
var rng: RandomNumberGenerator = ctx.get("rng", null)
var cx = max(1, count_x)
var cy = max(1, count_y)
var cz = max(1, count_z)
var base_step: Vector3 = (template_size if use_template_size else Vector3.ZERO) + spacing
for x in range(cx):
for y in range(cy):
for z in range(cz):
var position = Vector3(x * base_step.x, y * base_step.y, z * base_step.z)
if jitter > 0.0 and rng != null:
position += Vector3(
rng.randf_range(-jitter, jitter),
rng.randf_range(-jitter, jitter),
rng.randf_range(-jitter, jitter)
)
positions.append(position)
return positions
func get_estimated_count(_ctx: Dictionary) -> int:
return max(1, count_x) * max(1, count_y) * max(1, count_z)
@@ -1 +0,0 @@
uid://bc1sxg4vy464o
@@ -1,130 +0,0 @@
@tool
class_name CSGNoisePattern
extends CSGPattern
## Generates instance positions based on noise sampling in a 3D volume
## Instances are placed where noise value exceeds the threshold
##
@export var bounds: Vector3 = Vector3(10, 10, 10)
##
@export var sample_density: Vector3i = Vector3i(20, 1, 20)
##
@export_range(0.0, 1.0) var noise_threshold: float = 0.5
##
@export var noise_seed: int = 0
##
@export_range(0.01, 100) var noise_frequency: float = 0.1
##
@export_enum("Simplex", "Simplex Smooth", "Cellular", "Perlin", "Value Cubic", "Value") var noise_type: int = 0
##
@export_enum("None", "FBM", "Ridged", "Ping Pong") var fractal_type: int = 0
##
@export_range(1, 8) var fractal_octaves: int = 3
## When enabled, sampling spacing never drops below the template AABB size (prevents overlap)
@export var use_template_size: bool = false
var noise: FastNoiseLite
func _init():
noise = FastNoiseLite.new()
_update_noise()
func _update_noise():
if not noise:
noise = FastNoiseLite.new()
noise.seed = noise_seed
noise.frequency = noise_frequency
noise.fractal_octaves = fractal_octaves
# Map noise_type enum to FastNoiseLite types
match noise_type:
0: noise.noise_type = FastNoiseLite.TYPE_SIMPLEX
1: noise.noise_type = FastNoiseLite.TYPE_SIMPLEX_SMOOTH
2: noise.noise_type = FastNoiseLite.TYPE_CELLULAR
3: noise.noise_type = FastNoiseLite.TYPE_PERLIN
4: noise.noise_type = FastNoiseLite.TYPE_VALUE_CUBIC
5: noise.noise_type = FastNoiseLite.TYPE_VALUE
# Map fractal_type enum to FastNoiseLite fractal types
match fractal_type:
0: noise.fractal_type = FastNoiseLite.FRACTAL_NONE
1: noise.fractal_type = FastNoiseLite.FRACTAL_FBM
2: noise.fractal_type = FastNoiseLite.FRACTAL_RIDGED
3: noise.fractal_type = FastNoiseLite.FRACTAL_PING_PONG
func _generate(ctx: Dictionary) -> Array:
_update_noise()
var positions: Array = []
var template_size: Vector3 = ctx.get("template_size", Vector3.ONE) if use_template_size else Vector3.ZERO
var jitter: float = ctx.get("position_jitter", 0.0)
var rng: RandomNumberGenerator = ctx.get("rng", RandomNumberGenerator.new())
var effective_bounds = bounds
var sample_count = sample_density
# Calculate step size for sampling
var step = Vector3(
effective_bounds.x / max(1, sample_count.x),
effective_bounds.y / max(1, sample_count.y),
effective_bounds.z / max(1, sample_count.z)
)
# When enabled, spacing never drops below the template AABB size (prevents overlap),
# mirroring grid_pattern's use_template_size semantics. Bounds are re-centered afterwards.
if use_template_size and template_size != Vector3.ZERO:
step = Vector3(
maxf(step.x, template_size.x),
maxf(step.y, template_size.y),
maxf(step.z, template_size.z)
)
effective_bounds = step * Vector3(sample_count)
# Start from negative half to center the pattern around origin
var start_pos = -effective_bounds * 0.5
# Sample noise at regular intervals
for x in range(sample_count.x):
for y in range(sample_count.y):
for z in range(sample_count.z):
var sample_pos = start_pos + Vector3(
x * step.x + step.x * 0.5,
y * step.y + step.y * 0.5,
z * step.z + step.z * 0.5
)
# Get noise value at this position (normalized to 0-1)
var noise_value = (noise.get_noise_3d(sample_pos.x, sample_pos.y, sample_pos.z) + 1.0) * 0.5
# Only place instance if noise exceeds threshold
if noise_value >= noise_threshold:
var final_pos = sample_pos
# Apply jitter
if jitter > 0.0:
final_pos += Vector3(
rng.randf_range(-jitter, jitter),
rng.randf_range(-jitter, jitter),
rng.randf_range(-jitter, jitter)
)
positions.append(final_pos)
return positions
func get_estimated_count(ctx: Dictionary) -> int:
# Rough estimate: total samples * (1 - threshold)
# Higher threshold = fewer instances
var total_samples = max(1, sample_density.x) * max(1, sample_density.y) * max(1, sample_density.z)
var estimated = int(total_samples * (1.0 - noise_threshold))
return max(1, estimated)
@@ -1 +0,0 @@
uid://3il6xs7cr7gj
@@ -1,39 +0,0 @@
@tool
class_name CSGSpiralPattern
extends CSGPattern
@export var turns: float = 2.0
@export var start_radius: float = 0.5
@export var end_radius: float = 5.0
## If > 0 overrides vertical spread based on repeat & step
@export var total_height: float = 0.0
@export var use_radius_curve: bool = false
@export var radius_curve: Curve
@export var points: int = 32
func _generate(ctx: Dictionary) -> Array:
var positions: Array = []
var template_size: Vector3 = ctx.get("template_size", Vector3.ONE)
var t_turns: float = max(0.1, turns)
var r_start: float = max(0.0, start_radius)
var r_end: float = max(r_start, end_radius)
var total: int = max(2, points)
if total <= 1:
return [Vector3.ZERO]
for i in range(total):
var t: float = float(i) / float(total - 1)
var angle = t * t_turns * TAU
var curve_t = t
if use_radius_curve and radius_curve and radius_curve.get_point_count() > 0:
curve_t = clamp(radius_curve.sample(t), 0.0, 1.0)
var radius = lerp(r_start, r_end, curve_t)
var y_pos: float = t * (total_height if total_height > 0.0 else template_size.y * 1.0)
positions.append(Vector3(
cos(angle) * radius,
y_pos,
sin(angle) * radius
))
return positions
func get_estimated_count(ctx: Dictionary) -> int:
return max(2, points)

Some files were not shown because too many files have changed in this diff Show More