Compare commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9fb16e45c5 | ||
|
|
d2b0a47850 | ||
|
|
e2491bc4e1 | ||
|
|
5c0adb3a96 | ||
|
|
b8bbe21c62 | ||
|
|
1822a2211e | ||
|
|
a76c4aebef | ||
|
|
18e7b2daa8 | ||
|
|
ecebf76f47 | ||
|
|
877d3bbad0 | ||
|
|
f2e06e9dee | ||
|
|
a1d7007241 | ||
|
|
ed1a9949c8 | ||
|
|
f296b643d4 | ||
|
|
c81571ec1e | ||
|
|
850d657404 | ||
|
|
322c1c22eb | ||
|
|
020c2a50ce | ||
|
|
9d60807642 | ||
|
|
fc89790363 | ||
|
|
02c8d36f8d | ||
|
|
77fefd2505 | ||
|
|
4ef1a30767 | ||
|
|
7934537793 | ||
|
|
f381769a7a | ||
|
|
ab24c39d0e | ||
|
|
93783b1bd9 | ||
|
|
a0dfe7b483 | ||
|
|
87b8b110a5 | ||
|
|
3dd9d2ec71 | ||
|
|
e06818bc5a | ||
|
|
7de2090b6f | ||
|
|
96e21757c3 | ||
|
|
d20d45fffc | ||
|
|
bc88fef43a | ||
|
|
bbb0c5db14 | ||
|
|
3920b88041 | ||
|
|
66d87d48d6 | ||
|
|
7fd1d97099 | ||
|
|
4950ee3b7c | ||
|
|
38adb3ae78 | ||
|
|
e07ba7fc46 | ||
|
|
27b9788a04 | ||
|
|
4765dfaa02 | ||
|
|
475a026604 | ||
|
|
8e463d9247 | ||
|
|
1bd447a56e | ||
|
|
9b1d30339f | ||
|
|
39ddf9ea0b | ||
|
|
a5c08088b4 | ||
|
|
ed05fb9fd8 | ||
|
|
165d546662 | ||
|
|
88d4f12b56 | ||
|
|
b826bfb562 | ||
|
|
8ebd331b20 | ||
|
|
a17800db9c | ||
|
|
604612baac | ||
|
|
b99952d3c9 | ||
|
|
dfd8636476 | ||
|
|
59bd07f630 | ||
|
|
a8ad5c8a71 | ||
|
|
03b16e226e | ||
|
|
8a5ba71ecf | ||
|
|
93f03ed728 | ||
|
|
3ebce418da | ||
|
|
1648d6d9b1 |
@@ -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`。
|
||||
@@ -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`)。
|
||||
@@ -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`。
|
||||
@@ -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,无法自动语法校验。
|
||||
|
||||
@@ -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` 同样被忽略。
|
||||
@@ -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`。
|
||||
|
||||
@@ -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),
|
||||
所以修缩放不影响拾取距离。
|
||||
@@ -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,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.
|
||||
|
||||

|
||||

|
||||

|
||||
|
||||
<!-- Add a screenshot or GIF here:
|
||||

|
||||
-->
|
||||
|
||||
---
|
||||
|
||||
## 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/`.
|
||||
@@ -1 +0,0 @@
|
||||
uid://ci1u46akkd1c
|
||||
@@ -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 +0,0 @@
|
||||
uid://0e4w5ydeh6np
|
||||
@@ -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"
|
||||
@@ -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 +0,0 @@
|
||||
uid://bbthaayqxoorj
|
||||
@@ -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
|
||||
|
||||
@@ -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). |
|
||||
@@ -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 预设所引用的自定义材质资源路径(跨会话持久化)。 |
|
||||
|
Before Width: | Height: | Size: 393 KiB |
|
Before Width: | Height: | Size: 142 KiB |
|
Before Width: | Height: | Size: 1.1 MiB |
|
Before Width: | Height: | Size: 1.8 MiB |
@@ -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).
|
||||
@@ -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) 开源发布。
|
||||
@@ -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)**:从一个几何体中“挖去”另一个几何体的体积。
|
||||
|
||||

|
||||
|
||||
利用这些简单的操作,你可以迅速在场景中切出门洞、挖出地道或拼凑出复杂的建筑结构,而无需使用 Blender 等外部建模软件。
|
||||
|
||||
---
|
||||
|
||||
## 二、 工作流篇:极速搭建方案
|
||||
|
||||
CSG_Blockout 插件旨在打破繁琐的层级操作,提供所见即所得的极速搭建体验。无论是偏好鼠标点击还是快捷键的开发者,都能找到最顺手的方式。
|
||||
|
||||
### 推荐搭建流程
|
||||
1. **建立根节点**:在你的场景中添加一个 `CSGCombiner3D` 节点作为整个几何体的根容器。所有后续添加的基础形状都会自动合并在这个节点下。
|
||||
2. **选中目标**:确保在场景树中选中这个 `CSGCombiner3D` 节点(或它内部的其他 CSG 形状)。
|
||||
3. **呼出菜单**:
|
||||
- **方式 A (轮盘菜单)**:将鼠标移至 3D 视口中,按下快捷键 `Shift + A`,即可在鼠标位置唤出 3D 轮盘菜单(Pie Menu)。顺着鼠标方向滑动即可快速选择布尔操作并生成基础形状。
|
||||
|
||||

|
||||
|
||||
- **方式 B (侧边栏)**:直接使用 3D 视口左侧新增的插件侧边栏。首先点击下方图标选择布尔操作类型(并集、交集或差集),然后再点击上方的几何体图标(如立方体、球体等),即可一键添加带有该操作属性的 CSG 节点。
|
||||
|
||||

|
||||
4. **网格材质与原型度量**:
|
||||
- **程序化世界对齐网格**:侧边栏内置了三种开箱即用的世界对齐网格材质(灰白网格、深灰网格、橙色高亮网格),以及白模(无材质)与自定义材质通道。网格基于三平面投影(Triplanar),在物体旋转或缩放时不会发生纹理畸变或拉伸,非常适合在关卡前期度量角色尺度和空间距离。
|
||||
- **一键批量赋材质**:在视口或场景树中多选任意 `CSGShape3D` 节点,点击侧边栏材质组底部的 **“应用材质到选中节点”** 按钮,即可将当前激活的预设材质批量赋给所有选中节点。全流程支持 `Ctrl + Z` 撤销/重做。
|
||||
|
||||

|
||||
|
||||
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` 场景文件或备用,以便后续修改关卡结构。
|
||||
@@ -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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
|
||||

|
||||
|
||||
- **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.
|
||||
|
||||

|
||||
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.
|
||||
|
||||

|
||||
|
||||
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.
|
||||
@@ -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 +0,0 @@
|
||||
uid://cmwnf4lkck6mm
|
||||
@@ -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"
|
||||
@@ -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 +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 |
@@ -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 +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 +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 +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
|
||||
@@ -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"]
|
||||
@@ -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
|
||||
@@ -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)
|
||||
@@ -1 +0,0 @@
|
||||
uid://belgcjd0ys212
|
||||
@@ -613,6 +613,12 @@ static func configure(id: String, url: String = "", launch_context: Dictionary =
|
||||
if not client.automatic_config_edits:
|
||||
return _manual_edit_result(client, "configure")
|
||||
var path_error := _config_path_resolution_error(client)
|
||||
if path_error.is_empty():
|
||||
path_error = _config_write_error(client, "configure")
|
||||
if path_error.is_empty():
|
||||
path_error = _config_create_error(client)
|
||||
if path_error.is_empty():
|
||||
path_error = _credentials_error(client)
|
||||
if not path_error.is_empty():
|
||||
return {"status": "error", "message": path_error}
|
||||
## Capture `url` once so a port flip in EditorSettings between write and
|
||||
@@ -857,6 +863,8 @@ static func remove(id: String, url: String = "", launch_context: Dictionary = {}
|
||||
if not client.automatic_config_edits:
|
||||
return _manual_edit_result(client, "remove")
|
||||
var path_error := _config_path_resolution_error(client)
|
||||
if path_error.is_empty():
|
||||
path_error = _config_write_error(client, "remove")
|
||||
if not path_error.is_empty():
|
||||
return {"status": "error", "message": path_error}
|
||||
if url.is_empty():
|
||||
@@ -897,6 +905,49 @@ static func _config_path_resolution_error(client: Client) -> String:
|
||||
return str(client.resolved_config_path_details().get("error", ""))
|
||||
|
||||
|
||||
## Why Configure must not create this client's file although status may still
|
||||
## read the path: `create_error` in `McpClient`'s candidate rules. Kept apart
|
||||
## from the function above, whose signature `script/local-self-update-smoke`
|
||||
## patches by exact match, in release trees as well as this one.
|
||||
static func _config_create_error(client: Client) -> String:
|
||||
if client.config_type == "cli":
|
||||
return ""
|
||||
return str(client.resolved_config_path_details().get("create_error", ""))
|
||||
|
||||
|
||||
## Why neither Configure nor Remove may touch this client's file: this editor's
|
||||
## Flatpak sandbox does not share it with the host (`McpClient.config_write_error`,
|
||||
## or the CLI strategy's own rule for a client that writes through its CLI, where
|
||||
## Configure and Remove differ). Every strategy refuses on it too; checking here
|
||||
## puts the refusal ahead of launcher discovery and the lock.
|
||||
static func _config_write_error(client: Client, action: String) -> String:
|
||||
if client.config_type == "cli":
|
||||
if action == "configure":
|
||||
return CliStrategy.configure_write_error(client)
|
||||
return CliStrategy.remove_write_error(client)
|
||||
return client.config_write_error()
|
||||
|
||||
|
||||
## Why Configure must not write an attach entry although the client's file is
|
||||
## writable: this editor's Flatpak sandbox keeps Godot AI's credentials where
|
||||
## the bridge that entry launches does not look. A missing entry is better
|
||||
## than one known to be broken. Remove is not held to it.
|
||||
static func _credentials_error(client: Client) -> String:
|
||||
if client.command_shape == Client.CommandShape.NONE:
|
||||
return ""
|
||||
if not McpPathTemplate.flatpak_hides_credentials():
|
||||
return ""
|
||||
var app_id := McpPathTemplate.flatpak_app_id()
|
||||
return (
|
||||
"Godot runs in a Flatpak sandbox that keeps Godot AI's credentials to "
|
||||
+ "itself, so the bridge %s starts could not connect and nothing was "
|
||||
+ "changed. Run `flatpak override --user "
|
||||
+ "--filesystem=xdg-config/godot-ai:create %s`, restart Godot, and try "
|
||||
+ "again. docs/steam-capability-directory.md covers the other ways to "
|
||||
+ "share them."
|
||||
) % [client.display_name, app_id if not app_id.is_empty() else "<Godot's Flatpak ID>"]
|
||||
|
||||
|
||||
# --- Strategy dispatch + verify (testable seam) --------------------------
|
||||
|
||||
static func _manual_edit_result(client: Client, action: String) -> Dictionary:
|
||||
@@ -1240,6 +1291,16 @@ static func manual_command(id: String) -> String:
|
||||
)
|
||||
if cmd.is_empty():
|
||||
return cmd
|
||||
## The path above is the one outside Flatpak. When this sandboxed editor
|
||||
## cannot see where a Flatpak build keeps the same file, that may be the
|
||||
## wrong one for this user, and this text is all a refused Configure (or a
|
||||
## manual-only client) leaves them to go on.
|
||||
var hidden_paths: Variant = path_resolution.get("hidden_paths", null)
|
||||
if hidden_paths is PackedStringArray and not hidden_paths.is_empty():
|
||||
cmd += (
|
||||
"\n\nA Flatpak build keeps this file at %s instead. Godot's own Flatpak "
|
||||
+ "sandbox cannot see it, so edit that one if it is the build you use."
|
||||
) % " or ".join(hidden_paths)
|
||||
## #507: when the allow-host opt-in names a non-loopback range, also
|
||||
## surface the LAN URL so the user can copy-paste the right address into
|
||||
## a remote agent. Informational only — configure/remove still WRITE the
|
||||
|
||||
@@ -79,10 +79,21 @@ var path_template: Dictionary = {}
|
||||
## a fallback path that may become invisible after copy-on-write. When
|
||||
## that private leaf is new, Configure seeds it from the first later
|
||||
## existing candidate so read-through content is not shadowed.
|
||||
## 2. If no file or wildcard package match exists, the first non-wildcard
|
||||
## template is the deterministic create target.
|
||||
## 2. If no file or wildcard package match exists, the create target is the
|
||||
## non-wildcard template whose own directories already exist, because the
|
||||
## client has run from there (`_evident_create_path` gives the order),
|
||||
## and otherwise the first non-wildcard template. This is what sends a
|
||||
## Flatpak-only IDE's entry to `~/.var/app/<id>/config/...` instead of a
|
||||
## `~/.config` it never reads.
|
||||
## 3. Multiple matches within any wildcard group are ambiguous and fail
|
||||
## closed instead of choosing an arbitrary package.
|
||||
## 4. A template inside another Flatpak app's `~/.var/app/<id>` is never a
|
||||
## create target while this editor's own Flatpak sandbox hides that
|
||||
## directory (`McpPathTemplate.hidden_flatpak_app_dir`). When rule 2 then
|
||||
## finds no existing directory among the rest, the resolution carries
|
||||
## `create_error` and the unseen `hidden_paths`: status still reads the
|
||||
## first template, and Configure refuses rather than create a file the
|
||||
## client may never read.
|
||||
##
|
||||
## Exact-file and config-home environment overrides still have higher
|
||||
## priority. When this map has no entry for the current platform,
|
||||
@@ -331,6 +342,57 @@ func resolved_config_path_details() -> Dictionary:
|
||||
return {"path": "", "error": unresolved_config_path_error(display_name, path)}
|
||||
|
||||
|
||||
## Why Configure and Remove must leave this client's resolved file alone; ""
|
||||
## when they may write it. See `unshared_flatpak_config_error`.
|
||||
func config_write_error() -> String:
|
||||
var path := resolved_config_path()
|
||||
return "" if path.is_empty() else unshared_flatpak_config_error(display_name, path)
|
||||
|
||||
|
||||
## Why Configure and Remove must leave the file at `path` alone when this
|
||||
## editor runs in a Flatpak sandbox that does not share it read-write with the
|
||||
## host; "" when it does, and outside Flatpak. A file written there is read
|
||||
## back and verified by this editor and never seen by the client.
|
||||
##
|
||||
## Every strategy asks this before it writes, ahead of `create_error`: where a
|
||||
## Flatpak build of the client keeps its settings is beside the point until
|
||||
## the sandbox can write the ordinary location at all. Status does not ask.
|
||||
## It keeps reading the path, and under a read-only grant that is the
|
||||
## client's real file.
|
||||
static func unshared_flatpak_config_error(client_name: String, path: String) -> String:
|
||||
var block := McpPathTemplate.flatpak_write_block(path)
|
||||
if block.is_empty():
|
||||
return ""
|
||||
var refused := (
|
||||
"Godot runs in a Flatpak sandbox that cannot write %s where %s reads "
|
||||
+ "it, so nothing was changed."
|
||||
) % [path, client_name]
|
||||
var by_hand := "or edit that file by hand (docs/steam-capability-directory.md)."
|
||||
if block.has("unmounted"):
|
||||
return refused + (
|
||||
" Flatpak was asked to share %s but mounted nothing there, which "
|
||||
+ "is what it does when the directory does not exist as Godot "
|
||||
+ "starts. Create it, restart Godot, and try again, %s"
|
||||
) % [block["unmounted"], by_hand]
|
||||
if block.has("hidden"):
|
||||
return refused + (
|
||||
" Flatpak hides %s from this sandbox, as a --nofilesystem rule "
|
||||
+ "does. Remove that rule, restart Godot, and try again, %s"
|
||||
) % [block["hidden"], by_hand]
|
||||
if block.has("read_only"):
|
||||
return refused + (
|
||||
" %s is mounted read-only. Make it writable, restart Godot, and "
|
||||
+ "try again, %s"
|
||||
) % [block["read_only"], by_hand]
|
||||
var app_id := McpPathTemplate.flatpak_app_id()
|
||||
return refused + (
|
||||
" Run `flatpak override --user --filesystem=%s %s`, restart Godot, "
|
||||
+ "and try again, or edit that file by hand: "
|
||||
+ "docs/steam-capability-directory.md covers a sandbox without that "
|
||||
+ "grant."
|
||||
) % [block["needs"], app_id if not app_id.is_empty() else "<Godot's Flatpak ID>"]
|
||||
|
||||
|
||||
## Shared wording for a path template `McpPathTemplate.expand` could not fully
|
||||
## resolve. Used by the guard above and by the merge-tier loader in
|
||||
## `_json_strategy.gd`, which resolves its own templates and never passes
|
||||
@@ -404,7 +466,11 @@ func _resolve_ordered_config_path_candidates(templates: Variant) -> Dictionary:
|
||||
var ordered_templates: Array = []
|
||||
for template_variant in templates:
|
||||
ordered_templates.append(str(template_variant))
|
||||
var fallback_create_path := ""
|
||||
## Candidates whose file does not exist yet and that Configure could create.
|
||||
var create_templates := PackedStringArray()
|
||||
var create_paths := PackedStringArray()
|
||||
var hidden_app_dirs := PackedStringArray()
|
||||
var hidden_paths := PackedStringArray()
|
||||
for index in range(ordered_templates.size()):
|
||||
var template := str(ordered_templates[index])
|
||||
var expanded_template := McpPathTemplate.expand(template)
|
||||
@@ -444,10 +510,113 @@ func _resolve_ordered_config_path_candidates(templates: Variant) -> Dictionary:
|
||||
return {"path": "", "error": seed["error"]}
|
||||
_clear_config_path_warning()
|
||||
return {"path": path, "error": "", "seed_path": seed.get("path", "")}
|
||||
if fallback_create_path.is_empty():
|
||||
fallback_create_path = path
|
||||
# Creating below a directory the sandbox hides would land on its
|
||||
# private tmpfs and vanish with the editor, so a hidden candidate can
|
||||
# be neither evidence nor a create target.
|
||||
var hidden_app_dir := McpPathTemplate.hidden_flatpak_app_dir(path)
|
||||
if not hidden_app_dir.is_empty():
|
||||
hidden_paths.append(path)
|
||||
if not hidden_app_dirs.has(hidden_app_dir):
|
||||
hidden_app_dirs.append(hidden_app_dir)
|
||||
continue
|
||||
create_templates.append(template)
|
||||
create_paths.append(path)
|
||||
_clear_config_path_warning()
|
||||
var evident_create_path := _evident_create_path(create_templates, create_paths)
|
||||
if not evident_create_path.is_empty():
|
||||
return {"path": evident_create_path, "error": ""}
|
||||
var fallback_create_path := "" if create_paths.is_empty() else create_paths[0]
|
||||
if hidden_app_dirs.is_empty():
|
||||
return {"path": fallback_create_path, "error": ""}
|
||||
var hidden_error := hidden_flatpak_config_error(
|
||||
display_name, fallback_create_path.get_base_dir(), hidden_app_dirs
|
||||
)
|
||||
if fallback_create_path.is_empty():
|
||||
return {"path": "", "error": hidden_error}
|
||||
return {
|
||||
"path": fallback_create_path,
|
||||
"error": "",
|
||||
"create_error": hidden_error,
|
||||
"hidden_paths": hidden_paths,
|
||||
}
|
||||
|
||||
|
||||
## The location the client has demonstrably run from, among candidates with no
|
||||
## config file yet; "" when nothing says. Candidates are compared level by
|
||||
## level, from the directory that would hold the file upward, and the first
|
||||
## level at which one of those directories exists decides, in descriptor order.
|
||||
## A client nested in its host app's settings (a VS Code extension) therefore
|
||||
## follows the host app when the extension's own directory exists nowhere,
|
||||
## instead of creating a second, unread copy of the host's settings tree.
|
||||
##
|
||||
## Only directories the templates name below their shared root are compared,
|
||||
## and only as many levels as the shortest template has. Those belong to the
|
||||
## client; a container such as ~/.config exists whether or not the client does.
|
||||
static func _evident_create_path(templates: PackedStringArray, paths: PackedStringArray) -> String:
|
||||
if paths.is_empty():
|
||||
return ""
|
||||
var shared := _shared_root_length(templates)
|
||||
var levels := -1
|
||||
for template in templates:
|
||||
var own_directories := template.split("/").size() - 1 - shared
|
||||
levels = own_directories if levels < 0 else mini(levels, own_directories)
|
||||
var directories := PackedStringArray()
|
||||
for path in paths:
|
||||
directories.append(path.get_base_dir())
|
||||
for _level in range(maxi(levels, 0)):
|
||||
for index in range(directories.size()):
|
||||
if DirAccess.dir_exists_absolute(directories[index]):
|
||||
return paths[index]
|
||||
for index in range(directories.size()):
|
||||
directories[index] = directories[index].get_base_dir()
|
||||
return ""
|
||||
|
||||
|
||||
## How many leading path elements every template shares, counting the root
|
||||
## element (`$XDG_CONFIG_HOME`, `~`, a drive) and never the file name. At least
|
||||
## one: the root is never the client's own directory.
|
||||
static func _shared_root_length(templates: PackedStringArray) -> int:
|
||||
if templates.size() < 2:
|
||||
return 1
|
||||
var first := templates[0].split("/")
|
||||
var shared := first.size() - 1
|
||||
for index in range(1, templates.size()):
|
||||
var other := templates[index].split("/")
|
||||
var limit := mini(shared, other.size() - 1)
|
||||
var same := 0
|
||||
while same < limit and first[same] == other[same]:
|
||||
same += 1
|
||||
shared = same
|
||||
return maxi(shared, 1)
|
||||
|
||||
|
||||
## Why Configure will not create a client's settings file from inside a
|
||||
## Flatpak editor: nothing shows the client runs from the ordinary location,
|
||||
## and the location a Flatpak build of it uses cannot be inspected. Names the
|
||||
## one grant that lets this editor see it.
|
||||
static func hidden_flatpak_config_error(
|
||||
client_name: String, settings_dir: String, hidden_app_dirs: PackedStringArray
|
||||
) -> String:
|
||||
var app_id := McpPathTemplate.flatpak_app_id()
|
||||
var grants := PackedStringArray()
|
||||
for app_dir in hidden_app_dirs:
|
||||
grants.append("--filesystem=%s" % app_dir)
|
||||
var missing := (
|
||||
"%s does not exist" % settings_dir
|
||||
if not settings_dir.is_empty()
|
||||
else "it has no settings location outside Flatpak"
|
||||
)
|
||||
return (
|
||||
"Could not find where %s keeps its settings: %s, and a Flatpak build "
|
||||
+ "keeps them under %s, which Godot's own Flatpak sandbox cannot see. "
|
||||
+ "If you use the Flatpak build, run `flatpak override --user %s %s`, "
|
||||
+ "restart Godot, and configure again. If it is installed outside "
|
||||
+ "Flatpak, start it once so it creates its settings, then configure "
|
||||
+ "again."
|
||||
) % [
|
||||
client_name, missing, ", ".join(hidden_app_dirs),
|
||||
" ".join(grants), app_id if not app_id.is_empty() else "<Godot's Flatpak ID>",
|
||||
]
|
||||
|
||||
|
||||
## Find a readable seed for a newly-created authoritative wildcard target.
|
||||
|
||||
@@ -25,6 +25,8 @@ static var _cache: Dictionary = {} # exe_name -> resolved path (or "")
|
||||
static var _searched: Dictionary = {}
|
||||
|
||||
const _LOOKUP_TIMEOUT_MS := 3000
|
||||
const _TRACE_PATH_LIMIT := 16
|
||||
const _TRACE_PATH_MAX_CHARS := 512
|
||||
|
||||
|
||||
## Find any of the supplied exe names; returns the first hit.
|
||||
@@ -56,7 +58,15 @@ static func _find_one(exe_name: String, trace: Callable = Callable()) -> String:
|
||||
var cached: String = _cache.get(exe_name, "")
|
||||
_mutex.unlock()
|
||||
if already_searched:
|
||||
_trace_lookup(trace, "cache", "resolved" if not cached.is_empty() else "cached_miss", started, {}, true)
|
||||
_trace_lookup(
|
||||
trace,
|
||||
"cache",
|
||||
"resolved" if not cached.is_empty() else "cached_miss",
|
||||
started,
|
||||
{},
|
||||
true,
|
||||
cached,
|
||||
)
|
||||
return cached
|
||||
# `_resolve()` does FileAccess + bounded subprocess lookup (forks
|
||||
# `bash -lc` / `which`), which can take 100ms-1s. Holding the mutex across that
|
||||
@@ -76,12 +86,14 @@ static func _resolve(exe_name: String, trace: Callable = Callable()) -> String:
|
||||
var is_windows := OS.get_name() == "Windows"
|
||||
|
||||
# 1. Well-known locations
|
||||
var well_known_paths := PackedStringArray()
|
||||
for dir in _well_known_dirs():
|
||||
var full := dir.path_join(exe_name)
|
||||
well_known_paths.append(full)
|
||||
if FileAccess.file_exists(full):
|
||||
_trace_lookup(trace, "well_known", "resolved", started)
|
||||
_trace_lookup(trace, "well_known", "resolved", started, {}, false, full, well_known_paths)
|
||||
return full
|
||||
_trace_lookup(trace, "well_known", "no_match", started)
|
||||
_trace_lookup(trace, "well_known", "no_match", started, {}, false, "", well_known_paths)
|
||||
|
||||
# 2. Login shell lookup (Unix only)
|
||||
if not is_windows:
|
||||
@@ -97,22 +109,50 @@ static func _resolve(exe_name: String, trace: Callable = Callable()) -> String:
|
||||
if int(login_result.get("exit_code", -1)) == 0:
|
||||
var login_found: String = str(login_result.get("stdout", "")).strip_edges()
|
||||
if not login_found.is_empty() and FileAccess.file_exists(login_found):
|
||||
_trace_lookup(trace, "login_shell", "resolved", started, login_result)
|
||||
_trace_lookup(
|
||||
trace,
|
||||
"login_shell",
|
||||
"resolved",
|
||||
started,
|
||||
login_result,
|
||||
false,
|
||||
login_found,
|
||||
PackedStringArray([login_found]),
|
||||
)
|
||||
return login_found
|
||||
_trace_lookup(trace, "login_shell", _lookup_failure(login_result, false), started, login_result)
|
||||
|
||||
# 3. which / where with inherited PATH
|
||||
return _resolve_inherited_path(exe_name, is_windows, trace)
|
||||
|
||||
|
||||
## Resolve from the inherited PATH through a single lookup command. The
|
||||
## optional runner keeps the Windows/where failure modes deterministic in unit
|
||||
## tests without changing the production invocation.
|
||||
static func _resolve_inherited_path(
|
||||
exe_name: String,
|
||||
is_windows: bool,
|
||||
trace: Callable = Callable(),
|
||||
lookup_runner: Callable = Callable(),
|
||||
) -> String:
|
||||
var lookup := "where" if is_windows else "which"
|
||||
started = Time.get_ticks_msec()
|
||||
var result := McpCliExec.run(lookup, [exe_name], _LOOKUP_TIMEOUT_MS, false)
|
||||
var started := Time.get_ticks_msec()
|
||||
var result: Dictionary = {}
|
||||
if lookup_runner.is_valid():
|
||||
var injected: Variant = lookup_runner.call(lookup, exe_name)
|
||||
if injected is Dictionary:
|
||||
result = injected
|
||||
else:
|
||||
result = McpCliExec.run(lookup, [exe_name], _LOOKUP_TIMEOUT_MS, false)
|
||||
var lines := PackedStringArray(str(result.get("stdout", "")).split("\n"))
|
||||
if int(result.get("exit_code", -1)) == 0:
|
||||
var output := str(result.get("stdout", ""))
|
||||
var lines := PackedStringArray(output.split("\n"))
|
||||
var found := _pick_best_path(lines) if is_windows else lines[0].strip_edges()
|
||||
if not found.is_empty():
|
||||
_trace_lookup(trace, "inherited_path", "resolved", started, result)
|
||||
_trace_lookup(trace, "inherited_path", "resolved", started, result, false, found, lines)
|
||||
return found
|
||||
_trace_lookup(trace, "inherited_path", _lookup_failure(result, true), started, result)
|
||||
_trace_lookup(
|
||||
trace, "inherited_path", _lookup_failure(result, true), started, result, false, "", lines
|
||||
)
|
||||
return ""
|
||||
|
||||
|
||||
@@ -128,7 +168,8 @@ static func _lookup_failure(result: Dictionary, absence_exit: bool) -> String:
|
||||
|
||||
static func _trace_lookup(
|
||||
trace: Callable, tier: String, status: String, started: int,
|
||||
result: Dictionary = {}, cache_hit := false,
|
||||
result: Dictionary = {}, cache_hit := false, resolved_path := "",
|
||||
candidate_paths: PackedStringArray = PackedStringArray(),
|
||||
) -> void:
|
||||
if not trace.is_valid():
|
||||
return
|
||||
@@ -141,9 +182,44 @@ static func _trace_lookup(
|
||||
"spawn_failed": bool(result.get("spawn_failed", false)),
|
||||
"cancelled": bool(result.get("cancelled", false)),
|
||||
"termination_failed": bool(result.get("termination_failed", false)),
|
||||
"resolved_path": _bounded_trace_path(resolved_path),
|
||||
"candidate_paths": _bounded_trace_paths(candidate_paths),
|
||||
})
|
||||
|
||||
|
||||
## Keep lookup diagnostics useful without retaining arbitrary command output or
|
||||
## environment values. Only absolute executable paths cross this boundary.
|
||||
static func _bounded_trace_paths(paths: PackedStringArray) -> Array[String]:
|
||||
var bounded: Array[String] = []
|
||||
for raw in paths:
|
||||
var path := _bounded_trace_path(raw)
|
||||
if path.is_empty() or bounded.has(path):
|
||||
continue
|
||||
bounded.append(path)
|
||||
if bounded.size() >= _TRACE_PATH_LIMIT:
|
||||
break
|
||||
return bounded
|
||||
|
||||
|
||||
static func _bounded_trace_path(raw: String) -> String:
|
||||
var path := raw.strip_edges()
|
||||
if not _is_traceable_path(path):
|
||||
return ""
|
||||
return path.left(_TRACE_PATH_MAX_CHARS)
|
||||
|
||||
|
||||
static func _is_traceable_path(path: String) -> bool:
|
||||
if path.is_empty() or path.contains("\n") or path.contains("\r"):
|
||||
return false
|
||||
if path.begins_with("/") or path.begins_with("\\\\"):
|
||||
return true
|
||||
return (
|
||||
path.length() >= 3
|
||||
and path.substr(1, 1) == ":"
|
||||
and (path.substr(2, 1) == "\\" or path.substr(2, 1) == "/")
|
||||
)
|
||||
|
||||
|
||||
## Executable extensions Windows' CreateProcessW can launch from a path
|
||||
## (after the cmd.exe wrap in `_cli_exec.gd`). Order is preference: `.exe`
|
||||
## is a native PE binary; `.cmd` / `.bat` go through the shell; `.com` is
|
||||
|
||||