Compare commits

...
8 Commits
Author SHA1 Message Date
moyu 44b7bf1011 Update project.godot 2026-10-01 07:37:34 +08:00
moyu bd8c1c9a21 ai 插件demo 2026-10-01 07:37:29 +08:00
moyu e2a833536f 安装 ai插件 2026-10-01 07:36:55 +08:00
moyu cd73302181 导入模型资产 2026-10-01 06:07:14 +08:00
moyu eef5a9ce07 更新godot ai 到最新版本 2026-10-01 05:50:19 +08:00
moyu 260a118b5d csg插件 2026-10-01 05:36:12 +08:00
moyu a1308cf8fd 滑索 2026-09-24 16:35:28 +08:00
moyu 2f662c6c6f 更新godot ai插件到最新版本 2026-09-24 15:33:39 +08:00
901 changed files with 37651 additions and 49 deletions
+26
View File
@@ -0,0 +1,26 @@
# 2026-09-24
## 滑索(Zipline)组件 —— 新增
需求:Godot 4 第三人称滑索,要求全程用 Godot MCP 搭建。
新增 / 改动:
- `world/zipline/zipline.gd` + `zipline.tscn`:两个 Marker3D 锚点 + 抛物线下垂曲线。
缆绳网格在 `_ready()` 用 SurfaceTool 现造,子节点不给 owner → 不会被写进 .tscn。
对外接口:`sample / tangent / ride_point / closest_point`;手感参数(速度、刹车、吸附半径)都放在这一侧。
- `character/player/player.gd`:新增 `_find_zipline / _attach_zipline / _detach_zipline / _ride_zipline`。
缆上直接写 `global_position`,不走 `move_and_slide()`;相机在缆上缓慢对齐到行进方向。
- `character/player/player.tscn`:新增 `InteractHint`(Label3D),沿用 HealthLabel 的「节点配置」做法。
- `project.godot`:新增 `interact` 输入动作(E)。
- `world/main.tscn`:演示关卡 —— LaunchRamp(26.57° 斜引道)+ LaunchDeck(顶面 y=6.0)+ Zipline 实例。
- `tests/test_zipline.gd`:13 项回归测试,钉住几何契约(端点、下垂量、切线方向、下滑方向、悬挂偏移)。
验证:全量测试 20/20 通过(180 条断言);实跑三次,按 E 后玩家从 `(0,0,0)` 滑到 `(0,0,18.8)` 缆末端。
关键取舍:
- `sag` 定义成「中点下垂深度」而不是比例 —— 调参所见即所得,不用解悬链线方程。
- 出口固定为**较低**的那个锚点(滑索只能往下滑),两端等高时退回 A→B。
- 模型里没有滑索动画片段,所以缆上复用下落姿态:`animator.set_motion(velocity, false, false)`,
没有为此在 `PlayerAnimator` 里加新状态。
+24
View File
@@ -0,0 +1,24 @@
# 项目长期记忆
## 工程约定
- 玩家控制器只认「运动结果」,不认识任何动画片段名;表现层在 `Body/PlayerAnimator`。
新增动作状态改 `player_animator.gd` 的 `ANIM_CLIPS`,不要往 `player.gd` 里塞片段名。
- 组件的可调参数放在组件自己身上(如 Zipline 的速度 / 刹车 / 吸附半径),控制器只读不定义。
- 头顶 UI(血条、交互提示)是 `player.tscn` 里的 Label3D 节点,不在代码里动态创建。
- 测试用 `McpTestSuite`(`addons/godot_ai/testing`),脚本放 `res://tests/`。
## Godot MCP 踩坑
- **数值标量会被字符串化**:直接调 `node_set_property` 传 `1` / `false` 会报
`WRONG_TYPE: Cannot write String to a int/bool property`。
绕法:改用 `batch_execute`,把 value 放进每条 command 的 `params` 里(嵌套数字能正常保型)。
字符串、Vector / Color 对象不受影响。
- **@tool 脚本现造的节点不会写进 .tscn**:`add_child` 后不给 `owner`,
`PackedScene::pack()` 就会跳过它。缆绳网格因此可以纯由锚点推导,不存在「文件和锚点对不上」的脏数据。
- **新 `class_name` 要跑一次 `filesystem_manage(op="scan")`** 才进全局类表,
否则别的脚本把它当类型引用会解析不到。
- `game_manage(op="suspend")` 期间**输入事件不送达**,`next_frame` 也拦不住物理帧;
想抓「运行中途某一瞬间」的画面不可靠,只能靠日志 / 属性断言验证。
- 测试跑在编辑器进程里,`@tool` 节点必须真的 `add_child` 进编辑中场景才会跑 `_ready()`;
用完在 `teardown()` 摘掉,节点名以 `_McpTest` 开头可被测试运行器兜底清理。
+21
View File
@@ -0,0 +1,21 @@
# Godot 4+ specific ignores
.godot/
.nomedia
# Godot-specific ignores
.import/
export.cfg
export_credentials.cfg
*.tmp
# Imported translations (automatically generated from CSV files)
*.translation
# Mono-specific ignores
.mono/
data_*/
mono_crash.*.json
# Planning / Roadmap docs
ROADMAP*.md
+175
View File
@@ -0,0 +1,175 @@
# CSG_Blockout Architecture Design & Technical Internals
*Read this in other languages: [简体中文](ARCHITECTURE_CN.md) | [Back to Main Docs](README.md)*
This document is intended for developers interested in the underlying implementation, algorithmic designs, and extensibility of `CSG_Blockout`. It provides technical specifications and architectural breakdowns.
---
## Table of Contents
- [1. Core Algorithm: 3D Spatial Hash Grid](#1-core-algorithm-3d-spatial-hash-grid)
- [2. System Architecture & Design Patterns](#2-system-architecture--design-patterns)
- [3. Procedural World-Aligned Triplanar Shader](#3-procedural-world-aligned-triplanar-shader)
- [4. GDScript 2.0 Typing & Defensive Architecture](#4-gdscript-20-typing--defensive-architecture)
- [5. Node Specifications & API Reference](#5-node-specifications--api-reference)
- [6. ProjectSettings Specification](#6-projectsettings-specification)
---
## 1. Core Algorithm: 3D Spatial Hash Grid
### 1. The O(N^2) Bottleneck of Naive Checking
In scatter placement calculations (`CSGSpreader3D`), naive algorithms prevent instance overlapping by iterating through all previously placed points for each candidate point:
$$\text{Complexity} = \mathcal{O}(N^2)$$
When populating hundreds of complex CSG geometric instances in real-time, performing hundreds of thousands of 3D distance checks per second causes significant frame drops and editor freezes.
### 2. Spatial Hash Grid Implementation
`CSG_Blockout` implements the 3D spatial hash grid directly inside `CSGSpreader3D`, using a `Dictionary` keyed by `Vector3i` cell coordinates:
1. **Cell Size Determination**:
Given the user-defined minimum distance $d_{\min}$ (`min_distance`), the cubic grid cell size is set to:
$$\text{Cell Size} = d_{\min}$$
Any two points within $d_{\min}$ of each other can never differ by more than $d_{\min}$ along a single axis, so they always land in cells that are at most one apart in each dimension. This guarantees that the 27-cell neighborhood below covers every possible collision candidate.
2. **Spatial Quantization & Hash Mapping**:
Any continuous 3D world coordinate $\mathbf{P}(x, y, z)$ is discretized into integer cell indices:
$$\mathbf{cell\_coords} = \left( \lfloor x / \text{CellSize} \rfloor, \lfloor y / \text{CellSize} \rfloor, \lfloor z / \text{CellSize} \rfloor \right)$$
Stored using `Vector3i` keys inside a hash table for constant-time lookup.
3. **27-Neighborhood Search (3x3x3 Locality)**:
When validating a placement candidate, the algorithm queries only the host cell and its immediate 26 adjacent cells (27 cells total), reducing collision verification to:
$$\text{Collision Check Complexity} = \mathcal{O}(1)$$
---
## 2. System Architecture & Design Patterns
```mermaid
graph TD
subgraph Editor Core
Plugin["csg_blockout.gd (EditorPlugin)"]
URM["EditorUndoRedoManager"]
I18N["CsgBlockoutI18n"]
ProjSet["ProjectSettings (addons/csg_blockout/*)"]
end
subgraph UI & Controls
Pie["CSGBlockoutPieMenu (Vector 2D/3D Viewport)"]
SideBar["CSGSideBlockoutBar (Left Viewport Bar)"]
TopBar["CSGTopBlockoutBar (Viewport Header Bar)"]
end
subgraph Node Domain
Combiner["CSGCombiner3D (Godot Built-in)"]
Repeater["CSGRepeater3D"] --> Combiner
Spreader["CSGSpreader3D"] --> Combiner
end
subgraph Strategy Pattern
Pattern["CSGPattern (Resource)"]
GridPat["CSGGridPattern"] --> Pattern
CircPat["CSGCircularPattern"] --> Pattern
SpiralPat["CSGSpiralPattern"] --> Pattern
NoisePat["CSGNoisePattern"] --> Pattern
Repeater -.-> Pattern
end
subgraph Collision & Placement
HashGrid["Inline Spatial Hash Grid (O(1) lookup)"]
Spreader -.-> HashGrid
Spreader -.-> ShapeDomain["Shape3D (Box/Sphere/Mesh/...)"]
end
Plugin --> Pie
Plugin --> SideBar
Plugin --> TopBar
```
### 1. Strategy Pattern: Array Generator (`CSGRepeater3D`)
`CSGRepeater3D` delegates spatial array calculations to modular `CSGPattern` resources:
- **`CSGGridPattern`**: Orthogonal 3D Cartesian array with automatic bounding box (AABB) compensation.
- **`CSGCircularPattern`**: Multi-tiered radial polar array.
- **`CSGSpiralPattern`**: Archimedean/logarithmic spiral distribution with optional non-linear `Curve` radial modulation.
- **`CSGNoisePattern`**: Volumetric 3D noise threshold sampling via `FastNoiseLite`.
### 2. Shape3D Bounding Domain: Scatterer (`CSGSpreader3D`)
`CSGSpreader3D` accepts any Godot `Shape3D` as a spatial boundary:
- Computes world-space bounding boxes.
- Executes rejection sampling inside custom shapes.
- Enforces strict minimum distance thresholds via its built-in spatial hash grid.
- Implements fallback safety limits (`max_placement_attempts`).
---
## 3. Procedural World-Aligned Triplanar Shader
`grid_triplanar.gdshader` is engineered specifically for level prototyping:
1. **World-Space Triplanar Mapping**:
- Bypasses mesh UV coordinates; samples texture procedurally from world coordinates $\mathbf{P}_{\text{world}}$.
- Guarantees 1m x 1m grid alignment regardless of node transformation or boolean cutting.
2. **Hardware Screen-Derivative Anti-Aliasing**:
- Employs `fwidth()` and `smoothstep()` for sub-pixel anti-aliased grid lines, eliminating distant moiré patterns.
3. **Zero Texture Footprint**:
- Procedural math generation with zero VRAM bandwidth overhead.
---
## 4. GDScript 2.0 Typing & Defensive Architecture
- **Strict Static Typing**: Explicit typing on all methods and variables, optimizing engine dispatch.
- **ClassDB Validation**: Dynamic node instantiation validated via `ClassDB.instantiate()` and type checks.
- **Atomic Undo/Redo**: Full integration with `EditorUndoRedoManager` for node creation, property editing, material assignments, and instance baking (`CSGRepeater3D` / `CSGSpreader3D`).
- **Editor / Runtime Decoupling**: Tool scripts strictly gated with `Engine.is_editor_hint()` to prevent runtime leaks and scene corruption.
---
## 5. Node Specifications & API Reference
### 1. CSGRepeater3D Properties
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `template_node` | `Node3D` | `null` | Node in scene tree used as instance template. |
| `template_node_scene` | `PackedScene` | `null` | Template PackedScene resource (fallback if `template_node` is null). |
| `hide_template` | `bool` | `true` | Automatically hides template node when array is generated. |
| `pattern` | `CSGPattern` | `null` | Pattern resource defining distribution layout. |
| `position_jitter` | `float` | `0.0` | Minor random translation variance. |
| `random_seed` | `int` | `0` | Seed for pseudorandom generator. |
| `estimated_instances` | `int` | `0` | (Read-only) Estimated number of instances to be generated. |
| `randomize_rotation` | `bool` | `false` | Enables rotation variance. |
| `randomize_rot_x/y/z` | `bool` | `false` | Per-axis rotation toggles. |
| `rotation_variance_x/y/z_deg` | `float` | `0.0` | Random variance angle in degrees (0 = full 360-degree). |
| `randomize_scale` | `bool` | `false` | Enables scale variance. |
| `scale_variance` | `float` | `0.0` | Uniform scale variance factor. |
### 2. CSGSpreader3D Properties
| Property | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `template_node` | `Node3D` | `null` | Target template node to scatter. |
| `spread_area_3d` | `Shape3D` | `null` | Spatial boundary shape (Box, Sphere, Capsule, Mesh, etc.). |
| `max_count` | `int` | `10` | Maximum instance limit (hard capped at 200). |
| `noise_threshold` | `float` | `0.5` | Noise density threshold (0.0 to 1.0). |
| `seed` | `int` | `0` | Random seed. |
| `avoid_overlaps` | `bool` | `false` | Enables Spatial Hash collision prevention. |
| `min_distance` | `float` | `1.0` | Minimum safe distance between origins. |
| `max_placement_attempts` | `int` | `100` | Maximum candidate search attempts per instance. |
| `allow_rotation` | `bool` | `false` | Enables random Y-axis yaw rotation. |
| `allow_scale` | `bool` | `false` | Enables random scale variance (0.5x to 2.0x). |
---
## 6. ProjectSettings Specification
Configuration options are registered under `addons/csg_blockout/*`:
| Setting Path | Type | Default | Description |
| :--- | :--- | :--- | :--- |
| `addons/csg_blockout/action_key` | `int` (Key) | `KEY_SHIFT` | Primary action modifier key for 3D Pie Menu. |
| `addons/csg_blockout/auto_hide` | `bool` | `true` | Auto-hides left sidebar when no CSG node is selected. |
| `addons/csg_blockout/language_override` | `String` | `"auto"` | Language preference override (`"auto"`, `"en"`, `"zh_CN"`, `"ja"`, `"ko"`, `"es"`, `"pt"`, `"ru"`). |
| `addons/csg_blockout/material_preset` | `int` (Enum) | `1` (GRID_LIGHT) | Default active grid material preset. |
| `addons/csg_blockout/default_operation` | `int` (Enum) | `0` (Union) | Default CSG boolean operation for newly created nodes (shared by pie menu & sidebar). |
| `addons/csg_blockout/custom_material_path` | `String` | `""` | Resource path of the custom material used by the CUSTOM preset (persisted across editor sessions). |
+175
View File
@@ -0,0 +1,175 @@
# CSG_Blockout 架构设计与技术内幕
*其他语言版本: [English](ARCHITECTURE.md) | [返回主文档](README_CN.md)*
本文档面向对插件底层实现、算法细节以及二次开发感兴趣的开发者,详细阐述 `CSG_Blockout` 的核心算法设计、架构分层与完整 API 规范。
---
## 目录
- [一、 核心算法:3D 空间哈希网格 (Spatial Hash Grid)](#一-核心算法3d-空间哈希网格-spatial-hash-grid)
- [二、 系统架构与设计模式](#二-系统架构与设计模式)
- [三、 程序化世界对齐材质 Shader 设计](#三-程序化世界对齐材质-shader-设计)
- [四、 GDScript 2.0 规范与防御性设计](#四-gdscript-20-规范与防御性设计)
- [五、 节点与组件规格参考 (API Reference)](#五-节点与组件规格参考-api-reference)
- [六、 ProjectSettings 配置规范](#六-projectsettings-配置规范)
---
## 一、 核心算法:3D 空间哈希网格 (Spatial Hash Grid)
### 1. 传统暴力检测的性能瓶颈
在散布计算(`CSGSpreader3D`)中,为防止实例在 3D 体积内互相重叠穿模,朴素方案是对每一个候选位置与已有全部实例进行欧氏距离双重循环检测:
$$\text{复杂度} = \mathcal{O}(N^2)$$
当生成数百个高精度 CSG 几何体时,每秒数十万次的三维距离向量平方运算会导致 Godot 主线程严重卡顿,甚至触发编辑态未响应。
### 2. 空间哈希网格实现原理
`CSG_Blockout` 将 3D 空间哈希网格直接内建于 `CSGSpreader3D` 中,使用以 `Vector3i` 为键的 `Dictionary` 存储:
1. **单元格尺度计算**:
给定用户设定的最小防重叠间距 $d_{\min}$(`min_distance`),网格单元尺寸定义为:
$$\text{Cell Size} = d_{\min}$$
任意两个间距小于 $d_{\min}$ 的点,其在单一坐标轴上的差值不可能超过 $d_{\min}$,因此二者所在单元格在每个维度上至多相差 1。这保证了下述 27 邻域检索必定覆盖全部潜在碰撞候选。
2. **空间坐标量化与哈希映射**:
将任意浮点空间坐标 $\mathbf{P}(x, y, z)$ 离散化为整型网格坐标:
$$\mathbf{cell\_coords} = \left( \lfloor x / \text{CellSize} \rfloor, \lfloor y / \text{CellSize} \rfloor, \lfloor z / \text{CellSize} \rfloor \right)$$
使用基于 `Vector3i` 的字典键做常量时间寻址。
3. **27 邻域检索 (3x3x3 局部性查询)**:
每次验证候选位置时,算法无需遍历全量数据,仅需检索当前单元格以及紧邻的 26 个邻近单元格(共 27 块),将冲突检测的时间复杂度降为:
$$\text{候选冲突检测复杂度} = \mathcal{O}(1)$$
---
## 二、 系统架构与设计模式
```mermaid
graph TD
subgraph Editor Core
Plugin["csg_blockout.gd (EditorPlugin)"]
URM["EditorUndoRedoManager"]
I18N["CsgBlockoutI18n"]
ProjSet["ProjectSettings (addons/csg_blockout/*)"]
end
subgraph UI & Controls
Pie["CSGBlockoutPieMenu (Vector 2D/3D Viewport)"]
SideBar["CSGSideBlockoutBar (Left Viewport Bar)"]
TopBar["CSGTopBlockoutBar (Viewport Header Bar)"]
end
subgraph Node Domain
Combiner["CSGCombiner3D (Godot Built-in)"]
Repeater["CSGRepeater3D"] --> Combiner
Spreader["CSGSpreader3D"] --> Combiner
end
subgraph Strategy Pattern
Pattern["CSGPattern (Resource)"]
GridPat["CSGGridPattern"] --> Pattern
CircPat["CSGCircularPattern"] --> Pattern
SpiralPat["CSGSpiralPattern"] --> Pattern
NoisePat["CSGNoisePattern"] --> Pattern
Repeater -.-> Pattern
end
subgraph Collision & Placement
HashGrid["内建空间哈希网格 (O(1) 查询)"]
Spreader -.-> HashGrid
Spreader -.-> ShapeDomain["Shape3D (Box/Sphere/Mesh/...)"]
end
Plugin --> Pie
Plugin --> SideBar
Plugin --> TopBar
```
### 1. 策略模式:阵列分发器 (`CSGRepeater3D`)
`CSGRepeater3D` 摆脱了传统的硬编码循环,将生成逻辑完全抽象为独立的 `CSGPattern` 资源策略:
- **`CSGGridPattern`**:正交笛卡尔网格阵列,支持 AABB 边界自适应计算。
- **`CSGCircularPattern`**:多层同心极坐标环形阵列。
- **`CSGSpiralPattern`**:阿基米德/对数螺旋分布,支持自定义非线性半径曲线(`Curve`)。
- **`CSGNoisePattern`**:基于 `FastNoiseLite` 的体域噪声阈值采样分布。
### 2. 物理碰撞体边界域:散布系统 (`CSGSpreader3D`)
`CSGSpreader3D` 支持任意 Godot `Shape3D` 作为散布边界,其工作流包括:
- 提取碰撞体的世界空间 AABB 边界。
- 执行内部包含性与随机拒识采样。
- 联动内建空间哈希网格进行实时防重叠过滤。
- 支持最大容错尝试机制(`max_placement_attempts`)。
---
## 三、 程序化世界对齐材质 Shader 设计
`grid_triplanar.gdshader` 是专为白盒原型搭建定制的着色器,核心技术特征包括:
1. **世界坐标三平面投影(World-Space Triplanar Mapping)**:
- 弃用模型 UV 坐标,直接取顶点/片元的世界坐标 $\mathbf{P}_{\text{world}}$ 计算纹理采样。
- 物体在三维空间中随意平移、旋转、缩放或执行布尔切削,网格材质的物理尺寸(如 1m x 1m)永不拉伸扭曲。
2. **硬件级解析度自适应抗锯齿**:
- 利用导数函数 `fwidth()` 与 `smoothstep()` 计算网格边缘线,从根本上杜绝远处网格摩尔纹走样。
3. **性能开销最小化**:
- 无贴图依赖,纯数学公式生成网格与棋盘格,零显存带宽浪费。
---
## 四、 GDScript 2.0 规范与防御性设计
- **全线静态强类型化**:所有脚本启用强类型签名,消除动态查找开销。
- **ClassDB 安全校验**:所有动态实例化节点均经由 `ClassDB.instantiate()` 或类型断言,杜绝无效类抛错。
- **原子级 Undo/Redo**:全面对接 Godot 4.7 `EditorUndoRedoManager`,涵盖节点增删、属性变更、材质覆盖与实例烘焙(`CSGRepeater3D` / `CSGSpreader3D`)。
- **编辑器与运行时完全解耦**:所有工具类逻辑严格置于 `Engine.is_editor_hint()` 防护内,杜绝运行时产生幽灵节点或内存泄漏。
---
## 五、 节点与组件规格参考 (API Reference)
### 1. CSGRepeater3D 属性参考
| 属性名 | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- |
| `template_node` | `Node3D` | `null` | 引用场景树中作为阵列原型的节点。 |
| `template_node_scene` | `PackedScene` | `null` | 预制体模板资源(当未指定 `template_node` 时生效)。 |
| `hide_template` | `bool` | `true` | 生成阵列时自动隐藏原始模板节点。 |
| `pattern` | `CSGPattern` | `null` | 阵列分发策略资源。 |
| `position_jitter` | `float` | `0.0` | 位置微小随机抖动偏移量。 |
| `random_seed` | `int` | `0` | 伪随机数种子。 |
| `estimated_instances` | `int` | `0` | (只读) 预估生成的实例数量。 |
| `randomize_rotation` | `bool` | `false` | 启用旋转随机化。 |
| `randomize_rot_x/y/z` | `bool` | `false` | 各轴向旋转随机开关。 |
| `rotation_variance_x/y/z_deg` | `float` | `0.0` | 旋转随机浮动角度(设为 0 为 360 度全随机)。 |
| `randomize_scale` | `bool` | `false` | 启用缩放随机化。 |
| `scale_variance` | `float` | `0.0` | 等比缩放浮动量。 |
### 2. CSGSpreader3D 属性参考
| 属性名 | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- |
| `template_node` | `Node3D` | `null` | 需要散布的目标模板节点。 |
| `spread_area_3d` | `Shape3D` | `null` | 散布空间域形状(Box, Sphere, Capsule 等)。 |
| `max_count` | `int` | `10` | 散布最大实例上限(硬上限保护为 200)。 |
| `noise_threshold` | `float` | `0.5` | 噪声概率过滤阈值(0.0 至 1.0)。 |
| `seed` | `int` | `0` | 随机种子。 |
| `avoid_overlaps` | `bool` | `false` | 启用空间哈希碰撞避障检查。 |
| `min_distance` | `float` | `1.0` | 实例间最小安全间距。 |
| `max_placement_attempts` | `int` | `100` | 单个实例最大寻位采样尝试次数。 |
| `allow_rotation` | `bool` | `false` | 允许随机 Y 轴偏航旋转。 |
| `allow_scale` | `bool` | `false` | 允许随机缩放(0.5x 至 2.0x)。 |
---
## 六、 ProjectSettings 配置规范
插件所有全局配置项挂载于 Godot 项目设置 `addons/csg_blockout/*` 命名空间下:
| 配置键路径 | 类型 | 默认值 | 说明 |
| :--- | :--- | :--- | :--- |
| `addons/csg_blockout/action_key` | `int` (Key) | `KEY_SHIFT` | 3D 轮盘菜单主触发修饰键。 |
| `addons/csg_blockout/auto_hide` | `bool` | `true` | 视口未选中 CSG 节点时自动隐藏左侧边栏。 |
| `addons/csg_blockout/language_override` | `String` | `"auto"` | 界面语言偏好覆盖 (`"auto"`、`"en"`、`"zh_CN"`、`"ja"`、`"ko"`、`"es"`、`"pt"`、`"ru"`)。 |
| `addons/csg_blockout/material_preset` | `int` (Enum) | `1` (GRID_LIGHT) | 默认激活的网格材质预设通道。 |
| `addons/csg_blockout/default_operation` | `int` (Enum) | `0` (并集) | 新建节点的默认 CSG 布尔运算(轮盘菜单与侧栏共享)。 |
| `addons/csg_blockout/custom_material_path` | `String` | `""` | CUSTOM 预设所引用的自定义材质资源路径(跨会话持久化)。 |
Binary file not shown.

After

Width:  |  Height:  |  Size: 393 KiB

@@ -0,0 +1,36 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://5qx1d3dn67k5"
valid=false
[deps]
source_file="res://addons/csg_blockout/DocsImages/MaterialPresets.webp"
[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
Binary file not shown.

After

Width:  |  Height:  |  Size: 142 KiB

@@ -0,0 +1,36 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://vne3q1357fot"
valid=false
[deps]
source_file="res://addons/csg_blockout/DocsImages/PieMenu.webp"
[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
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

@@ -0,0 +1,36 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bcg4dbln751fd"
valid=false
[deps]
source_file="res://addons/csg_blockout/DocsImages/Sidebar.webp"
[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
Binary file not shown.

After

Width:  |  Height:  |  Size: 1.8 MiB

@@ -0,0 +1,36 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://c01xs0wjxnlfd"
valid=false
[deps]
source_file="res://addons/csg_blockout/DocsImages/Subtraction.webp"
[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
+22
View File
@@ -0,0 +1,22 @@
MIT License
Copyright (c) 2026 qwqzhanqwq
Copyright (c) 2023 LuckyTepot
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in all
copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
SOFTWARE.
+110
View File
@@ -0,0 +1,110 @@
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="res/icon_transparent.svg" />
<img src="res/icon_transparent_light.svg" alt="CSG_Blockout Logo" width="180" />
</picture>
<h1>CSG_Blockout</h1>
<p>
<a href="README.md"><img src="https://img.shields.io/badge/Docs-English-blue?style=flat-square" alt="Docs English" /></a>
<a href="README_CN.md"><img src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-blue?style=flat-square" alt="文档 简体中文" /></a>
<a href="TUTORIAL_EN.md"><img src="https://img.shields.io/badge/Tutorial-English-orange?style=flat-square" alt="Tutorial English" /></a>
<a href="ARCHITECTURE.md"><img src="https://img.shields.io/badge/Architecture-Internals-purple?style=flat-square" alt="Architecture Internals" /></a>
</p>
<p>
<a href="https://godotengine.org"><img src="https://img.shields.io/badge/Godot-4.7%2B-478cbf?style=flat-square&logo=godotengine&logoColor=white" alt="Godot Engine" /></a>
<a href="https://store.godotengine.org/asset/qwqzhanqwq/csg-blockout/"><img src="https://img.shields.io/badge/AssetLib-CSG__Blockout-blueviolet?style=flat-square" alt="Godot AssetLib" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-success?style=flat-square" alt="License: MIT" /></a>
</p>
<p>
<strong>A high-performance, modernized 3D level blockout and rapid prototyping plugin designed specifically for Godot 4.7.</strong>
</p>
</div>
---
## Installation
### Option 1: Godot AssetLib (Recommended)
1. Open Godot 4.7 and navigate to the **AssetLib** tab at the top of the editor.
2. Search for `CSG Blockout`, download, and install it into your project.
3. Or view it directly on the web: [Godot AssetLib - CSG_Blockout](https://store.godotengine.org/asset/qwqzhanqwq/csg-blockout/).
### Option 2: GitHub Releases (.zip)
1. Download the latest release `.zip` from the [Releases page](https://github.com/qwqzhanqwq/Godot-CSG-Blockout/releases).
2. Extract the archive and copy the `addons/csg_blockout` directory into your Godot project's `addons/` folder.
### Option 3: Git Clone
Clone the repository directly into your project's `addons/` directory:
```bash
git clone https://github.com/qwqzhanqwq/Godot-CSG-Blockout.git addons/csg_blockout
```
### Enable the Plugin
In Godot, go to **Project -> Project Settings -> Plugins**, find **CSG_Blockout**, and check the **Enable** checkbox.
---
## Quick Start
1. **Invoke 3D Pie Menu**: Press `Shift + A` in the 3D viewport to open the vector radial menu directly at your cursor. Quickly create primitives or switch CSG operations (Union / Intersection / Subtraction) using mouse gestures.
2. **Use the Viewport Sidebar**: The left sidebar provides one-click creation for 6 standard shapes. It automatically adds nodes as children when selecting a `CSGCombiner3D`, or as adjacent siblings when selecting standard shapes.
3. **Procedural World-Aligned Grid**: Switch between Light Grid, Dark Grid, or Orange Accent presets. Select multiple CSG nodes in the tree or viewport and click **"Apply Material to Selected"** to batch-assign without texture distortion.
4. **Bake to Permanent Nodes**: When a `CSGRepeater3D` or `CSGSpreader3D` is selected, click **Bake** in the 3D header toolbar to detach generated instances into permanent scene nodes.
> For a complete visual walkthrough and performance optimization workflow, check the [Quick Start & Advanced Workflow Tutorial (TUTORIAL_EN.md)](TUTORIAL_EN.md).
---
## Key Features
- **Vector 3D Pie Menu**: Triggered with `Shift + A` in the 3D viewport. Hierarchical radial navigation for instant CSG operations and primitive placement with central deadzone controls and full Undo/Redo integration.
- **Smart Viewport Sidebar**: Quick-access toolbar for 6 primitive shapes and boolean modes. Auto-hides when no CSG nodes are selected to keep the viewport clean.
- **Procedural World-Aligned Triplanar Shader**: World-space UV projection guarantees consistent 1m x 1m grid scaling without stretching during node transformations or CSG boolean cuts. Built-in screen-derivative anti-aliasing.
- **Ready-to-Use Material Presets & Batch Assignment**: Pre-configured measurement materials (Light, Dark, Accent, Unshaded) with one-click batch application to all selected CSG nodes.
- **Parametric Array Generator (CSGRepeater3D)**: Strategy-pattern array distributions (Grid, Circular, Spiral, and 3D Noise volume sampling) with randomized rotation and scale.
- **Volume Collision Scatterer (CSGSpreader3D)**: Scatter instances within any Godot `Shape3D` volume without overlaps. Powered by a custom $\mathcal{O}(1)$ 3D Spatial Hash Grid algorithm.
- **GDScript 2.0 Static Typing & Multi-Language Support**: Fully typed codebase, atomic `EditorUndoRedoManager` support (`Ctrl + Z` / `Ctrl + Y`), and built-in 7-language (i18n) localization (English, Simplified Chinese, Japanese, Korean, Spanish, Portuguese, Russian).
---
## Relationship to CSG Toolkit & Architectural Evolution
This plugin originated from the excellent open-source [CSG Toolkit](https://godotengine.org/asset-library/asset/3057) created by **LuckyTepot**. While the original toolkit demonstrated the value of in-viewport CSG authoring, modern Godot 4.7 workflows demanded higher performance, procedural tooling, and cleaner architecture.
`CSG_Blockout` was re-engineered by [qwqzhanqwq](https://github.com/qwqzhanqwq) as a **ground-up architectural overhaul**:
| Dimension | Original CSG Toolkit | CSG_Blockout (This Project) |
| :--- | :--- | :--- |
| **Engine Core** | Early Godot 4.x, dynamically typed | **Tailored for Godot 4.7+**, full GDScript 2.0 static typing & ClassDB validation |
| **Interaction** | Viewport sidebar only | **3D Viewport Radial Pie Menu (`Shift + A`) + Smart Sidebar** dual workflow |
| **Procedural Tools**| Static manual primitive placement | **Parametric Repeaters (`CSGRepeater3D`) + Volume Spreaders (`CSGSpreader3D`)** |
| **Scatter Algorithm**| No collision avoidance or naive $\mathcal{O}(N^2)$ | **Custom 3D Spatial Hash Grid ($\mathcal{O}(1)$ lookup)**, real-time calculation |
| **Materials** | Basic default materials | **World-aligned triplanar anti-aliased shader** + 5 presets + 1-click batch assign |
| **Robustness** | Basic editor state | **Atomic `EditorUndoRedoManager` integration** with strict editor/runtime decoupling |
| **Localization** | English only | **Native multi-language support** (English, Simplified Chinese, Japanese, Korean, Spanish, Portuguese, Russian) |
---
## Documentation
### English
- [Main Documentation (README.md)](README.md): Plugin overview, installation, feature highlights, and credits.
- [Quick Start & Advanced Workflow Tutorial (TUTORIAL_EN.md)](TUTORIAL_EN.md): Step-by-step guide, visual demonstrations, and the critical CSG-to-Mesh baking workflow.
- [Architecture Design & Technical Internals (ARCHITECTURE.md)](ARCHITECTURE.md): Spatial Hash Grid algorithm derivation, design patterns, and complete API specifications.
### 简体中文 (Simplified Chinese)
- [中文主说明文档 (README_CN.md)](README_CN.md): 插件核心特性、安装指南、快速上手与演进历史。
- [快速上手与高级工作流教程 (TUTORIAL_CN.md)](TUTORIAL_CN.md): 完整图文与动图教学、白盒烘焙与性能优化方案。
- [架构设计与技术内幕 (ARCHITECTURE_CN.md)](ARCHITECTURE_CN.md): 3D 空间哈希数学推导、策略模式架构解析与全量 API 字典。
---
## Credits & License
- **Original Concept & Layout Design**: [LuckyTepot](https://github.com/LuckyTepot) (CSG Toolkit, Copyright (c) 2023).
- **Architecture Overhaul, 3D Pie Menu, Spatial Hash Grid, Array/Scatter Systems & GDScript 2.0 Rewrite**: [qwqzhanqwq](https://github.com/qwqzhanqwq) (Copyright (c) 2026).
Licensed under the [MIT License](LICENSE).
+110
View File
@@ -0,0 +1,110 @@
<div align="center">
<picture>
<source media="(prefers-color-scheme: dark)" srcset="res/icon_transparent.svg" />
<img src="res/icon_transparent_light.svg" alt="CSG_Blockout Logo" width="180" />
</picture>
<h1>CSG_Blockout</h1>
<p>
<a href="README.md"><img src="https://img.shields.io/badge/Docs-English-blue?style=flat-square" alt="Docs English" /></a>
<a href="README_CN.md"><img src="https://img.shields.io/badge/%E6%96%87%E6%A1%A3-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-blue?style=flat-square" alt="文档 简体中文" /></a>
<a href="TUTORIAL_CN.md"><img src="https://img.shields.io/badge/%E6%95%99%E7%A8%8B-%E7%AE%80%E4%BD%93%E4%B8%AD%E6%96%87-orange?style=flat-square" alt="教程 简体中文" /></a>
<a href="ARCHITECTURE_CN.md"><img src="https://img.shields.io/badge/%E6%9E%B6%E6%9E%84-%E6%8A%80%E6%9C%AF%E5%86%85%E5%B9%95-purple?style=flat-square" alt="架构 技术内幕" /></a>
</p>
<p>
<a href="https://godotengine.org"><img src="https://img.shields.io/badge/Godot-4.7%2B-478cbf?style=flat-square&logo=godotengine&logoColor=white" alt="Godot 引擎" /></a>
<a href="https://store.godotengine.org/asset/qwqzhanqwq/csg-blockout/"><img src="https://img.shields.io/badge/AssetLib-CSG__Blockout-blueviolet?style=flat-square" alt="Godot AssetLib" /></a>
<a href="LICENSE"><img src="https://img.shields.io/badge/License-MIT-success?style=flat-square" alt="开源协议: MIT" /></a>
</p>
<p>
<strong>专为 Godot 4.7 打造的高性能、现代化的 3D 关卡白盒(Blockout)与快速原型设计插件。</strong>
</p>
</div>
---
## 安装指南
### 途径 1:Godot AssetLib 官方资产库(推荐)
1. 在 Godot 4.7 编辑器顶部点击 **AssetLib (资产库)** 选项卡。
2. 搜索 `CSG Blockout`,点击下载并安装到项目中。
3. 也可通过网页版资产库直达:[Godot AssetLib - CSG_Blockout](https://store.godotengine.org/asset/qwqzhanqwq/csg-blockout/)。
### 途径 2:GitHub Releases 发布包
1. 前往项目的 [Releases 页面](https://github.com/qwqzhanqwq/Godot-CSG-Blockout/releases) 下载最新的 `.zip` 归档文件。
2. 解压后将 `addons/csg_blockout` 文件夹复制到你的 Godot 项目根目录下的 `addons/` 目录中。
### 途径 3:Git 源码克隆
在你的 Godot 项目根目录下执行以下命令:
```bash
git clone https://github.com/qwqzhanqwq/Godot-CSG-Blockout.git addons/csg_blockout
```
### 启用插件
打开 Godot 编辑器,依次点击 **项目 (Project) -> 项目设置 (Project Settings) -> 插件 (Plugins)**,找到 **CSG_Blockout** 并勾选 **启用 (Enable)**。
---
## 快速上手
1. **呼出 3D 轮盘**:在 3D 视口中按下快捷键 `Shift + A`,在光标所在位置唤出矢量轮盘菜单(Pie Menu),顺着手势快速创建基础 CSG 形状或切换布尔操作(并集 / 交集 / 差集)。
2. **使用视口侧边栏**:视口左侧边栏提供立方体、球体、圆柱体等一键创建按钮;选中 `CSGCombiner3D` 时自动挂载为子节点,选中独立形状时自动作为同级兄弟节点插入。
3. **程序化世界对齐网格**:侧边栏内置灰白网格、深灰网格、橙色高亮网格等材质通道,支持多选节点后一键批量赋予,物体随意缩放旋转均不拉伸纹理。
4. **一键烘焙与导出**:选中 `CSGRepeater3D` 或 `CSGSpreader3D` 时,顶部工具栏会出现专属按钮,点击 **烘焙 (Bake)** 即可将其固化为常规场景节点。
> 完整图文搭建教程与性能优化最佳实践,请查阅 [快速上手与高级工作流教程 (TUTORIAL_CN.md)](TUTORIAL_CN.md)。
---
## 核心功能特性
- **矢量 3D 轮盘菜单 (3D Pie Menu)**:在 3D 视口通过 `Shift + A` 顺发呼出,多层级布尔运算与几何形状无缝联动,支持中心死区回退与全流程 Undo/Redo。
- **智能工作流侧边栏**:集成 6 种标准 CSG 几何体快捷创建、3 种布尔操作即时切换,无 CSG 节点选中时自动隐藏,保持视口清爽。
- **程序化三平面世界网格 Shader**:摒弃传统 UV 依赖,基于世界空间坐标投影,严格保持真实世界比例尺寸(1m x 1m),内置抗锯齿算法。
- **开箱即用材质预设与批量赋予**:预置浅色、深色、重点强调等度量材质,支持视口中框选多个 CSG 节点一键应用。
- **参数化阵列生成器 (CSGRepeater3D)**:采用策略模式设计,支持网格阵列、环形阵列、螺旋阵列与 3D 噪声体域采样,支持旋转与缩放随机化。
- **物理体积防重叠散布系统 (CSGSpreader3D)**:支持以任意 Godot `Shape3D` 为空间边界进行无重叠填充,采用 $O(1)$ 空间哈希网格算法,彻底避免编辑态卡死。
- **工程级强类型与多语言支持**:全线基于 GDScript 2.0 静态强类型开发,无缝对接 `EditorUndoRedoManager`,内置支持中、英、日、韩、西、葡、俄 7 种语言 (i18n) 界面即时热切换。
---
## 与 CSG Toolkit 的关系与全面演进
本项目灵感源自 **LuckyTepot** 开发的优秀开源插件 [CSG Toolkit](https://godotengine.org/asset-library/asset/3057)。原版插件验证了在 Godot 视口内集成快速 CSG 工具栏的卓越体验。
为了满足现代化高精度关卡设计与 Godot 4.7 架构标准,本项目由 [qwqzhanqwq](https://github.com/qwqzhanqwq) 对其进行了**彻底推倒式的架构重写与全方位扩展**:
| 维度 | 原版 CSG Toolkit | CSG_Blockout (本项目) |
| :--- | :--- | :--- |
| **引擎架构** | 面向早期 Godot 4.x,弱类型脚本 | **深度适配 Godot 4.7+**,全线 GDScript 2.0 强类型标注与 ClassDB 校验 |
| **核心交互** | 仅依赖视口固定侧边栏 | **3D 视口矢量轮盘 (Shift+A Pie Menu) + 智能侧边栏** 双工作流 |
| **程序化生成** | 仅支持基础静态图元放置 | **参数化阵列 (Repeater) + 碰撞体体积散布 (Spreader)** |
| **散布避障算法** | 无避障或朴素 $O(N^2)$ 遍历循环 | **自研 3D 空间哈希网格算法 ($O(1)$ 复杂度)**,数百实例实时生成不卡顿 |
| **材质系统** | 依赖基础内置着色器 | **程序化世界对齐三平面网格 Shader** + 5 组预设 + 一键批量赋予 |
| **工程健壮性** | 基础编辑态支持 | **全流程原子级 EditorUndoRedoManager 接入**,严格的编辑器/运行时环境隔离 |
| **国际化** | 仅支持单语言 | **原生多语言支持**(中、英、日、韩、西、葡、俄 7 语),自动跟随编辑器语言偏好并支持独立覆盖 |
---
## 文档导航
### 简体中文
- [主说明文档 (README_CN.md)](README_CN.md):插件核心特性、安装指南、快速上手与演进历史。
- [快速上手与高级工作流教程 (TUTORIAL_CN.md)](TUTORIAL_CN.md):手把手教学、动图演示与关键的白盒烘焙性能优化方案。
- [架构设计与技术内幕 (ARCHITECTURE_CN.md)](ARCHITECTURE_CN.md):3D 空间哈希数学推导、策略模式架构解析与全量 API 字典。
### English
- [Main Documentation (README.md)](README.md): Plugin overview, installation, feature highlights, and credits.
- [Quick Start & Advanced Workflow Tutorial (TUTORIAL_EN.md)](TUTORIAL_EN.md): Step-by-step guide, visual demonstrations, and the critical CSG-to-Mesh baking workflow.
- [Architecture Design & Technical Internals (ARCHITECTURE.md)](ARCHITECTURE.md): Spatial Hash Grid algorithm derivation, design patterns, and complete API specifications.
---
## 致谢与开源协议
- **原始概念与界面原型灵感**:[LuckyTepot](https://github.com/LuckyTepot) (CSG Toolkit, Copyright (c) 2023).
- **架构重构、3D 轮盘、空间哈希优化、阵列/散布系统与 GDScript 2.0 重写**:[qwqzhanqwq](https://github.com/qwqzhanqwq) (Copyright (c) 2026).
本项目基于 [MIT License](LICENSE) 开源发布。
+95
View File
@@ -0,0 +1,95 @@
# CSG Blockout 快速搭建与高级工作流教程
*其他语言版本: [English](TUTORIAL_EN.md) | [返回主文档](README_CN.md) | [架构与技术内幕](ARCHITECTURE_CN.md)*
## 目录
- [一、 基础篇:CSG 的基本概念](#一-基础篇csg-的基本概念)
- [二、 工作流篇:极速搭建方案](#二-工作流篇极速搭建方案)
- [三、 进阶篇:阵列与散布系统](#三-进阶篇阵列与散布系统)
- [四、 终极奥义:性能优化工作流 (必读)](#四-终极奥义性能优化工作流-必读)
---
## 一、 基础篇:CSG 的基本概念
**CSG (Constructive Solid Geometry,构造实体几何)** 是一种允许你通过对简单几何体(如立方体、球体、圆柱体)进行布尔运算,从而构造出复杂 3D 模型的技术。
在 Godot 中,CSG 是关卡设计师搭建“白盒(Blockout)”或进行快速原型设计的利器。其核心基于三种布尔操作:
- **并集 (Union)**:将两个几何体合并为一个整体。
- **交集 (Intersection)**:只保留两个几何体相互重叠的部分。
- **差集 (Subtraction)**:从一个几何体中“挖去”另一个几何体的体积。
![CSG 差集演示](DocsImages/Subtraction.webp)
利用这些简单的操作,你可以迅速在场景中切出门洞、挖出地道或拼凑出复杂的建筑结构,而无需使用 Blender 等外部建模软件。
---
## 二、 工作流篇:极速搭建方案
CSG_Blockout 插件旨在打破繁琐的层级操作,提供所见即所得的极速搭建体验。无论是偏好鼠标点击还是快捷键的开发者,都能找到最顺手的方式。
### 推荐搭建流程
1. **建立根节点**:在你的场景中添加一个 `CSGCombiner3D` 节点作为整个几何体的根容器。所有后续添加的基础形状都会自动合并在这个节点下。
2. **选中目标**:确保在场景树中选中这个 `CSGCombiner3D` 节点(或它内部的其他 CSG 形状)。
3. **呼出菜单**:
- **方式 A (轮盘菜单)**:将鼠标移至 3D 视口中,按下快捷键 `Shift + A`,即可在鼠标位置唤出 3D 轮盘菜单(Pie Menu)。顺着鼠标方向滑动即可快速选择布尔操作并生成基础形状。
![Pie Menu 演示](DocsImages/PieMenu.webp)
- **方式 B (侧边栏)**:直接使用 3D 视口左侧新增的插件侧边栏。首先点击下方图标选择布尔操作类型(并集、交集或差集),然后再点击上方的几何体图标(如立方体、球体等),即可一键添加带有该操作属性的 CSG 节点。
![Sidebar 演示](DocsImages/Sidebar.webp)
4. **网格材质与原型度量**:
- **程序化世界对齐网格**:侧边栏内置了三种开箱即用的世界对齐网格材质(灰白网格、深灰网格、橙色高亮网格),以及白模(无材质)与自定义材质通道。网格基于三平面投影(Triplanar),在物体旋转或缩放时不会发生纹理畸变或拉伸,非常适合在关卡前期度量角色尺度和空间距离。
- **一键批量赋材质**:在视口或场景树中多选任意 `CSGShape3D` 节点,点击侧边栏材质组底部的 **“应用材质到选中节点”** 按钮,即可将当前激活的预设材质批量赋给所有选中节点。全流程支持 `Ctrl + Z` 撤销/重做。
![材质预设与一键应用演示](DocsImages/MaterialPresets.webp)
5. **智能层级挂载**:
- 选中 `CSGCombiner3D`(或 `CSGRepeater3D` / `CSGSpreader3D`)时,新建节点将自动作为**子节点**挂载在其下方;
- 选中普通 `CSGShape3D`(如 `CSGBox3D`)时,新建节点将自动作为**同级平级节点**插入在当前选中节点之后,无需手动调整层级树。
6. **调整修改**:直接在视口中拖拽手柄调整大小和位置,由于工具全线支持 `Ctrl+Z`,你可以大胆地尝试任何修改。
---
## 三、 进阶篇:阵列与散布系统
### 1. 阵列系统 (CSGRepeater3D)
`CSGRepeater3D` 允许你通过特定的数学模式快速生成重复的几何体,非常适合制作楼梯、栅栏、柱列或环形阵列场景。
**核心操作步骤**:
1. **创建节点**:在场景中添加一个 `CSGRepeater3D` 节点。
2. **设置模板**:你可以直接将一个基础的 CSG 节点(如 `CSGBox3D`)作为 `CSGRepeater3D` 的子节点,或者在其属性面板中为 `Template Node` 分配目标节点。
3. **选择阵列模式**:在 Inspector (检查器) 中,找到 `Pattern` 属性,新建你需要的分发模式:
- `CSGGridPattern`:**网格阵列**,可自由调整 X/Y/Z 三轴的生成数量与间距。
- `CSGCircularPattern`:**环形阵列**,通过控制半径和数量,让物体围绕中心呈环形分布。
- `CSGSpiralPattern`:**螺旋阵列**,在环形的基础上增加高度偏移,是制作螺旋楼梯的绝佳工具。
- `CSGNoisePattern`:**噪声阵列**,基于 3D 噪声进行有序的随机偏移。
### 2. 散布系统 (CSGSpreader3D)
`CSGSpreader3D` 独家集成了**空间哈希网格(Spatial Hash Grid)** 算法,能够在任意 3D 碰撞体范围内高效、随机且**无重叠**地散布几何体。即使散布大量物体,O(1) 级别的避障检测也能确保编辑器丝滑不卡顿。
**核心操作步骤**:
1. **创建节点**:添加 `CSGSpreader3D` 节点并赋予它一个需要被散布的子节点模板。
2. **划定散布范围**:在属性面板中,指定一个 `Shape3D`(如 `BoxShape3D` 或 `SphereShape3D`),散布将严格限制在此碰撞体范围内。
3. **调整密度与随机性**:
- **Min Distance (最小安全距离)**:这是控制散布密度的最关键属性。设置越大分布越稀疏,设置越小分布越密集。
- 配合面板中的随机旋转(Random Rotation)和随机缩放(Random Scale),瞬间生成杂乱但自然的地形或废墟。
---
## 四、 终极奥义:性能优化工作流 (必读)
CSG 节点在编辑阶段非常方便,但由于其实时布尔运算的特性,如果在最终游戏运行时保留大量的 CSG 节点,将会造成**严重的性能损耗(掉帧)**。
当你的某一个房间或建筑区块已经“确定不改动”时,**必须**将其转换为静态网格和碰撞体。
**一键转换标准流程**:
1. **完成搭建**:选中包含你所有操作的那个顶级 `CSGCombiner3D` 节点。
2. **烘焙网格**:在编辑器顶部菜单栏点击 `CSGCombiner3D` 的专属选项,选择 **“烘焙为网格实例 (Bake to MeshInstance3D)”**(或直接创建/转换)。
3. **生成碰撞体**:选中刚刚生成的 `MeshInstance3D`,在顶部菜单点击“Mesh(网格)”,选择 **“创建三角网格碰撞体 (Create Trimesh Static Body)”**。此操作会自动为你生成极其贴合表面的碰撞体。
4. **清理现场**:将生成的 `MeshInstance3D` 连同其内部的 `CollisionShape3D` 移动到一个单独的 `StaticBody3D` 层级结构下。
5. **删除原 CSG**:**直接删除**原来的 `CSGCombiner3D` 节点及其所有子节点。此时,沉重的布尔计算完全卸载,你得到的是一个性能极高、带有完美碰撞的静态游戏场景素材!
> [!WARNING]
> **注意**:删除 CSG 节点前,建议将原始 CSG 节点结构保存为单独的 `CSG_Source.tscn` 场景文件或备用,以便后续修改关卡结构。
+95
View File
@@ -0,0 +1,95 @@
# CSG Blockout: Rapid Prototyping & Advanced Workflow Tutorial
*Read this in other languages: [简体中文](TUTORIAL_CN.md) | [Back to Main Docs](README.md) | [Architecture & Internals](ARCHITECTURE.md)*
## Table of Contents
- [1. Basics: Understanding CSG](#1-basics-understanding-csg)
- [2. Workflow: Rapid Blockout using Sidebar and Pie Menu](#2-workflow-rapid-blockout-using-sidebar-and-pie-menu)
- [3. Advanced: The Array and Scatter Systems](#3-advanced-the-array-and-scatter-systems)
- [4. Ultimate Technique: Performance Optimization Workflow (Must Read)](#4-ultimate-technique-performance-optimization-workflow-must-read)
---
## 1. Basics: Understanding CSG
**CSG (Constructive Solid Geometry)** is a modeling technique that allows you to construct complex 3D models using boolean operations on simple geometric shapes (e.g., cubes, spheres, cylinders).
In Godot, CSG is a powerful tool for level designers to block out levels or create rapid prototypes. Its core functionality is based on three boolean operations:
- **Union**: Merges two geometries into a single solid shape.
- **Intersection**: Retains only the overlapping portion of two geometries.
- **Subtraction**: Carves out the volume of one geometry from another.
![CSG Subtraction Demo](DocsImages/Subtraction.webp)
Using these simple operations, you can quickly cut out doors, dig tunnels, or assemble complex architectural structures directly in the engine, completely bypassing external software like Blender.
---
## 2. Workflow: Rapid Blockout using Sidebar and Pie Menu
The CSG_Blockout plugin is designed to eliminate tedious hierarchy management, providing an ultra-fast, WYSIWYG (What You See Is What You Get) experience. Whether you prefer mouse clicks or keyboard shortcuts, the plugin adapts to your workflow.
### Recommended Blockout Process
1. **Create the Root Node**: Add a `CSGCombiner3D` node to your scene. This will act as the root container for your geometry block. All subsequently added basic shapes will merge under this node.
2. **Select the Target**: Make sure this `CSGCombiner3D` node (or any CSG shape inside it) is selected in the Scene Tree.
3. **Summon the Menu**:
- **Method A (Pie Menu)**: Move your mouse over the 3D viewport and press `Shift + A`. A radial Pie Menu will appear at your cursor. Simply flick your mouse in the direction of your desired boolean operation and primitive shape.
![Pie Menu Demo](DocsImages/PieMenu.webp)
- **Method B (Sidebar)**: Use the newly added plugin sidebar on the left side of the 3D viewport. First, click the bottom icons to select a boolean operation (Union, Intersection, or Subtraction), and then click the top primitive shape icons (e.g., cube, sphere) to instantly add a CSG node with that operation.
![Sidebar Demo](DocsImages/Sidebar.webp)
4. **Procedural Grid Materials & Scale Metrics**:
- **World-Aligned Triplanar Grid**: The sidebar includes three procedural grid material presets (**Light Grid**, **Dark Grid**, **Orange Accent Grid**), along with **Unshaded (None)** and **Custom Material** slots. Powered by triplanar projection, the grid pattern stays perfectly aligned in world coordinates without texture stretching, making character scale and spatial distance estimation effortless.
- **Apply Material to Selection**: Select one or more `CSGShape3D` nodes in the viewport or scene tree, then click the **"Apply Material to Selected"** button at the bottom of the material group to batch-assign the active preset. Fully integrated with `Ctrl + Z` Undo/Redo.
![Material Presets & Quick Apply Demo](DocsImages/MaterialPresets.webp)
5. **Smart Contextual Hierarchy Placement**:
- Selecting a `CSGCombiner3D` (or `CSGRepeater3D` / `CSGSpreader3D`) automatically places new nodes as **children**.
- Selecting a standard `CSGShape3D` (e.g., `CSGBox3D`) automatically places new nodes as **siblings directly following** the selected node, eliminating tedious scene-tree dragging.
6. **Tweak and Adjust**: Drag the handles directly in the viewport to resize and position. The plugin fully supports `Ctrl+Z`, so feel free to experiment without fear of breaking things.
---
## 3. Advanced: The Array and Scatter Systems
### 1. The Array System (CSGRepeater3D)
`CSGRepeater3D` allows you to rapidly generate duplicated geometries using specific mathematical patterns. It is perfect for creating stairs, fences, pillars, or circular structures.
**Workflow**:
1. **Create Node**: Add a `CSGRepeater3D` node to your scene.
2. **Set Template**: You can either add a basic CSG primitive (e.g., `CSGBox3D`) as a direct child of the Repeater, or assign a target node to the `Template Node` property in the Inspector.
3. **Select a Pattern**: In the Inspector, locate the `Pattern` property, create a new resource, and choose your desired pattern strategy:
- `CSGGridPattern`: **Grid Array**, easily adjust count and spacing along the X/Y/Z axes.
- `CSGCircularPattern`: **Circular Array**, distribute items in a ring by configuring radius and count.
- `CSGSpiralPattern`: **Spiral Array**, adds a height offset to circular arrays, perfect for spiral staircases.
- `CSGNoisePattern`: **Noise Array**, organic distribution driven by 3D noise functions.
### 2. The Scatter System (CSGSpreader3D)
`CSGSpreader3D` integrates a proprietary **Spatial Hash Grid** algorithm. It randomly scatters geometry within any 3D collision shape efficiently and **without overlaps**. Even with hundreds of scattered instances, the O(1) collision avoidance ensures the editor remains completely lag-free.
**Workflow**:
1. **Create Node**: Add a `CSGSpreader3D` node and assign it a child node to use as the scatter template.
2. **Define Scatter Volume**: Assign a `Shape3D` (such as `BoxShape3D` or `SphereShape3D`) in the properties. Objects will only be spawned strictly within this shape's boundaries.
3. **Adjust Density & Randomness**:
- **Min Distance**: This is the most crucial property. It dictates the scatter density. A larger value yields sparse distribution, while a smaller value makes it dense.
- Combine this with Random Rotation and Random Scale settings to instantly generate chaotic yet organic scene blockouts, like rocky terrains or rubble.
---
## 4. Ultimate Technique: Performance Optimization Workflow (Must Read)
While CSG nodes are incredibly convenient during the editing phase, retaining a large number of them during the final game runtime will cause **severe performance degradation (FPS drops)** due to real-time boolean calculations.
When a specific room or architectural block is "finalized and won't be changed," you **must** convert it into a static mesh and collision body.
**Standard 1-Click Conversion Process**:
1. **Finalize Blockout**: Select the top-level `CSGCombiner3D` node that contains all your operations.
2. **Bake to Mesh**: In the editor's top menu bar, click the contextual options for `CSGCombiner3D` and select **"Bake to MeshInstance3D"**.
3. **Generate Collision**: Select the newly generated `MeshInstance3D`, click "Mesh" in the top menu bar, and select **"Create Trimesh Static Body"**. This will automatically generate a highly accurate collision shape that perfectly wraps your geometry.
4. **Clean up Structure**: Move the generated `MeshInstance3D` (along with its internal `CollisionShape3D`) under a clean `StaticBody3D` hierarchy.
5. **Delete Original CSG**: **Delete** the original `CSGCombiner3D` node and all its children. At this point, the heavy boolean calculations are completely unloaded, leaving you with an ultra-performant static game asset with perfect collisions!
> [!WARNING]
> **Note**: Before deleting the CSG nodes, it is highly recommended to save the original CSG structure as a separate scene file (e.g., `CSG_Source.tscn`) or keep a backup, so you can easily modify the level structure later.
+232
View File
@@ -0,0 +1,232 @@
@tool
class_name CsgBlockout extends EditorPlugin
var config: CsgBlockoutConfig:
get: return CsgBlockoutConfig.get_config()
var sidebar: CSGSideBlockoutBar
var topbar: CSGTopBlockoutBar
static var csg_plugin_path: String
static var undo_manager: EditorUndoRedoManager
var pie_menu: CsgPieMenu
var pie_menu_tab_pressed_time: int = 0
func _get_shape_menu() -> Array[Dictionary]:
return [
{"label": CsgBlockoutI18n.t("BOX"), "type": "create_csg", "csg_type": "CSGBox3D"},
{"label": CsgBlockoutI18n.t("CYLINDER"), "type": "create_csg", "csg_type": "CSGCylinder3D"},
{"label": CsgBlockoutI18n.t("MESH"), "type": "create_csg", "csg_type": "CSGMesh3D"},
{"label": CsgBlockoutI18n.t("POLYGON"), "type": "create_csg", "csg_type": "CSGPolygon3D"},
{"label": CsgBlockoutI18n.t("SPHERE"), "type": "create_csg", "csg_type": "CSGSphere3D"},
{"label": CsgBlockoutI18n.t("TORUS"), "type": "create_csg", "csg_type": "CSGTorus3D"}
]
func _get_pie_menu_items() -> Array[Dictionary]:
return [
{
"label": CsgBlockoutI18n.t("UNION"), "type": "submenu",
"operation": 0,
"children": _get_shape_menu()
},
{
"label": CsgBlockoutI18n.t("INTERSECTION"), "type": "submenu",
"operation": 1,
"children": _get_shape_menu()
},
{
"label": CsgBlockoutI18n.t("SUBTRACTION"), "type": "submenu",
"operation": 2,
"children": _get_shape_menu()
}
]
func _enter_tree() -> void:
csg_plugin_path = get_script().get_path().get_base_dir()
undo_manager = get_undo_redo()
# Nodes
add_custom_type("CSGRepeater3D", "CSGCombiner3D", preload("res://addons/csg_blockout/scripts/csg_repeater_3d.gd"), null)
add_custom_type("CSGSpreader3D", "CSGCombiner3D", preload("res://addons/csg_blockout/scripts/csg_spreader_3d.gd"), null)
# Sidebar
var sidebarScene = preload("res://addons/csg_blockout/scenes/csg_side_blockout_bar.tscn")
sidebar = sidebarScene.instantiate()
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, sidebar)
# Topbar
var topbarScene = preload("res://addons/csg_blockout/scenes/csg_top_blockout_bar.tscn")
topbar = topbarScene.instantiate()
add_control_to_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, topbar)
func _handles(object: Object) -> bool:
return object is Node3D
func _forward_3d_gui_input(viewport_camera: Camera3D, event: InputEvent) -> int:
if event is InputEventKey and event.keycode == KEY_A and not event.echo and _is_action_key_held(event):
if event.pressed:
if not is_instance_valid(pie_menu):
_open_pie_menu()
return EditorPlugin.AFTER_GUI_INPUT_STOP
else:
if is_instance_valid(pie_menu):
if Time.get_ticks_msec() - pie_menu_tab_pressed_time > 200:
# Hold mode release
pie_menu.execute_active_item(true)
return EditorPlugin.AFTER_GUI_INPUT_STOP
if is_instance_valid(pie_menu):
if event is InputEventMouseButton:
if event.button_index == MOUSE_BUTTON_LEFT and event.pressed:
pie_menu.execute_active_item(true)
return EditorPlugin.AFTER_GUI_INPUT_STOP
elif event.button_index == MOUSE_BUTTON_RIGHT and event.pressed:
pie_menu.execute_back()
return EditorPlugin.AFTER_GUI_INPUT_STOP
return EditorPlugin.AFTER_GUI_INPUT_STOP
if event is InputEventMouseMotion:
return EditorPlugin.AFTER_GUI_INPUT_STOP
return EditorPlugin.AFTER_GUI_INPUT_PASS
func _is_action_key_held(event: InputEventWithModifiers) -> bool:
var key: Key = config.action_key if config else KEY_SHIFT
return (key == KEY_SHIFT and event.shift_pressed) \
or (key == KEY_CTRL and event.ctrl_pressed) \
or (key == KEY_ALT and event.alt_pressed) \
or (key == KEY_META and event.meta_pressed)
func _open_pie_menu() -> void:
pie_menu = CsgPieMenu.new()
pie_menu.action_triggered.connect(_on_pie_menu_action_triggered)
pie_menu.close_requested.connect(_close_pie_menu)
var ed_scale: float = EditorInterface.get_editor_scale() if Engine.is_editor_hint() else 1.0
pie_menu.apply_editor_scale(ed_scale)
var base_control: Control = EditorInterface.get_base_control()
base_control.add_child(pie_menu)
var mouse_pos: Vector2 = base_control.get_local_mouse_position()
# Clamp position
var margin: float = pie_menu.outer_radius + 10.0 * ed_scale
var rect_size: Vector2 = base_control.get_rect().size
mouse_pos.x = clampf(mouse_pos.x, margin, rect_size.x - margin)
mouse_pos.y = clampf(mouse_pos.y, margin, rect_size.y - margin)
pie_menu.position = mouse_pos
pie_menu.setup(_get_pie_menu_items(), ed_scale)
pie_menu_tab_pressed_time = Time.get_ticks_msec()
func _close_pie_menu() -> void:
if is_instance_valid(pie_menu):
pie_menu.queue_free()
pie_menu = null
func _on_pie_menu_action_triggered(item: Dictionary) -> void:
var action_type = item.get("type", "")
if action_type == "submenu" and item.has("operation"):
var op = item.get("operation")
if config:
config.default_operation = op
var selection = EditorInterface.get_selection().get_selected_nodes()
if selection.size() > 0 and selection[0] is CSGShape3D:
var node = selection[0] as CSGShape3D
undo_manager.create_action(CsgBlockoutI18n.t("CHANGE_CSG_OP"))
undo_manager.add_do_property(node, "operation", op)
undo_manager.add_undo_property(node, "operation", node.operation)
undo_manager.commit_action()
elif action_type == "create_csg":
var csg_type = item.get("csg_type", "")
if csg_type != "" and ClassDB.can_instantiate(csg_type):
_create_csg_node(csg_type)
func _create_csg_node(csg_type: String) -> void:
var new_node: Node3D = ClassDB.instantiate(csg_type) as Node3D
if not new_node:
return
if new_node is CSGShape3D:
new_node.operation = config.default_operation if config else CSGShape3D.OPERATION_UNION
if config:
new_node.material = config.get_active_material()
var selection = EditorInterface.get_selection()
var selected_nodes = selection.get_selected_nodes()
var parent: Node = null
var insert_index: int = -1
var target_pos: Vector3 = Vector3.ZERO
if selected_nodes.size() > 0 and selected_nodes[0] is Node3D:
var selected_node: Node3D = selected_nodes[0]
target_pos = selected_node.global_position
if selected_node is CSGCombiner3D:
parent = selected_node
insert_index = parent.get_child_count()
elif selected_node is CSGShape3D:
parent = selected_node.get_parent()
if parent == null:
parent = selected_node
insert_index = parent.get_child_count()
else:
insert_index = selected_node.get_index() + 1
else:
parent = selected_node
insert_index = parent.get_child_count()
else:
parent = EditorInterface.get_edited_scene_root()
if parent:
insert_index = parent.get_child_count()
if not parent:
new_node.free()
return
var owner_ref = EditorInterface.get_edited_scene_root()
if owner_ref == null:
owner_ref = parent
undo_manager.create_action(CsgBlockoutI18n.tf("CREATE_NODE", [csg_type]))
undo_manager.add_undo_reference(new_node)
undo_manager.add_do_method(self, "_undoable_create_csg", parent, new_node, owner_ref, target_pos, insert_index)
undo_manager.add_do_method(self, "_select_node", new_node)
undo_manager.add_undo_method(self, "_undoable_remove_csg", parent, new_node)
undo_manager.commit_action()
func _undoable_create_csg(parent: Node, node: Node3D, owner_ref: Node, global_pos: Vector3, insert_index: int) -> void:
if node.get_parent() != parent:
parent.add_child(node, true)
if insert_index >= 0 and insert_index < parent.get_child_count():
parent.move_child(node, insert_index)
node.owner = owner_ref
node.global_position = global_pos
func _undoable_remove_csg(parent: Node, node: Node3D) -> void:
if is_instance_valid(node) and node.get_parent() == parent:
parent.remove_child(node)
func _select_node(node: Node) -> void:
if not is_instance_valid(node) or not node.is_inside_tree():
return
var selection = EditorInterface.get_selection()
if selection:
selection.clear()
selection.add_node(node)
func _exit_tree() -> void:
remove_custom_type("CSGRepeater3D")
remove_custom_type("CSGSpreader3D")
undo_manager = null
if sidebar and is_instance_valid(sidebar):
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_SIDE_LEFT, sidebar)
sidebar.free()
if topbar and is_instance_valid(topbar):
remove_control_from_container(EditorPlugin.CONTAINER_SPATIAL_EDITOR_MENU, topbar)
topbar.free()
+1
View File
@@ -0,0 +1 @@
uid://cmwnf4lkck6mm
+7
View File
@@ -0,0 +1,7 @@
[plugin]
name="CSG_Blockout"
description="Enhance your blockout workflow with CSG_Blockout. This tool adds quick access buttons to the left toolbar for easy addition of CSG nodes. Heavily refactored for modern Godot 4 workflows."
author="qwqzhanqwq"
version="2.0.2"
script="csg_blockout.gd"
+28
View File
@@ -0,0 +1,28 @@
<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>

After

Width:  |  Height:  |  Size: 11 KiB

+43
View File
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cgv4ig5q8u5pg"
path="res://.godot/imported/icon.svg-703cc03c5ab36ae38c8a719f60f9a439.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icon.svg"
dest_files=["res://.godot/imported/icon.svg-703cc03c5ab36ae38c8a719f60f9a439.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
@@ -0,0 +1,27 @@
<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>

After

Width:  |  Height:  |  Size: 11 KiB

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cwp4dvut8fjrr"
path="res://.godot/imported/icon_transparent.svg-6944df47fbb3a6414b7e98a67550bc34.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icon_transparent.svg"
dest_files=["res://.godot/imported/icon_transparent.svg-6944df47fbb3a6414b7e98a67550bc34.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
@@ -0,0 +1,27 @@
<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>
@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://hrewmraubhxp"
path="res://.godot/imported/icon_transparent_light.svg-005ac4a8f9cf9023b8cc7301364f16eb.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icon_transparent_light.svg"
dest_files=["res://.godot/imported/icon_transparent_light.svg-005ac4a8f9cf9023b8cc7301364f16eb.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
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 297 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://bf2b0ma6rari8"
path="res://.godot/imported/apply_material.svg-0af461409ae979f6e6e12a3a8ad3993c.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/apply_material.svg"
dest_files=["res://.godot/imported/apply_material.svg-0af461409ae979f6e6e12a3a8ad3993c.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 467 B

@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cjxx30pcamj36"
path="res://.godot/imported/box.svg-7067ac48679a06d2db551db6227fbac7.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/box.svg"
dest_files=["res://.godot/imported/box.svg-7067ac48679a06d2db551db6227fbac7.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
+4
View File
@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 260 B

@@ -0,0 +1,44 @@
[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
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 478 B

@@ -0,0 +1,44 @@
[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
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 724 B

@@ -0,0 +1,44 @@
[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
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 552 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://ccq4rgvepggjv"
path="res://.godot/imported/grid_dark.svg-78b47a06bed738dca2c7b4f971df90b6.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/grid_dark.svg"
dest_files=["res://.godot/imported/grid_dark.svg-78b47a06bed738dca2c7b4f971df90b6.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
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 552 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://di683jr1esdfo"
path="res://.godot/imported/grid_light.svg-2e57e76125a9f88f1e3d2725024d0705.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/grid_light.svg"
dest_files=["res://.godot/imported/grid_light.svg-2e57e76125a9f88f1e3d2725024d0705.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
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 552 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://kjqx76tn78ot"
path="res://.godot/imported/grid_orange.svg-bf95244de8e41e5ec92a6f927e2ac92a.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/grid_orange.svg"
dest_files=["res://.godot/imported/grid_orange.svg-bf95244de8e41e5ec92a6f927e2ac92a.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
@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 763 B

@@ -0,0 +1,44 @@
[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
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 348 B

@@ -0,0 +1,43 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://d17t4wrfhkcam"
path="res://.godot/imported/material_none.svg-de44d0630e04dfe179cf633ad48aad32.ctex"
metadata={
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/material_none.svg"
dest_files=["res://.godot/imported/material_none.svg-de44d0630e04dfe179cf633ad48aad32.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=false
editor/convert_colors_with_editor_theme=false
+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 459 B

@@ -0,0 +1,44 @@
[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
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 223 B

@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://cjps2pofcsfc0"
path="res://.godot/imported/polygon.svg-2282e5cff5d8681594f5d2ce9db7c894.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/polygon.svg"
dest_files=["res://.godot/imported/polygon.svg-2282e5cff5d8681594f5d2ce9db7c894.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 488 B

@@ -0,0 +1,44 @@
[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
@@ -0,0 +1,4 @@
<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>

After

Width:  |  Height:  |  Size: 826 B

@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://snbhkpq6sh4j"
path="res://.godot/imported/subtraction.svg-e5717404f3625d7cf3717f6169d524f2.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/subtraction.svg"
dest_files=["res://.godot/imported/subtraction.svg-e5717404f3625d7cf3717f6169d524f2.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=false
+1
View File
@@ -0,0 +1 @@
<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>

After

Width:  |  Height:  |  Size: 456 B

@@ -0,0 +1,44 @@
[remap]
importer="texture"
type="CompressedTexture2D"
uid="uid://gbgmyv4bknqo"
path="res://.godot/imported/torus.svg-0e939567d2a65f43cdab88e1c075c1b5.ctex"
metadata={
"has_editor_variant": true,
"vram_texture": false
}
[deps]
source_file="res://addons/csg_blockout/res/icons/torus.svg"
dest_files=["res://.godot/imported/torus.svg-0e939567d2a65f43cdab88e1c075c1b5.ctex"]
[params]
compress/mode=0
compress/high_quality=false
compress/lossy_quality=0.7
compress/uastc_level=0
compress/rdo_quality_loss=0.0
compress/hdr_compression=1
compress/normal_map=0
compress/channel_pack=0
mipmaps/generate=false
mipmaps/limit=-1
roughness/mode=0
roughness/src_normal=""
process/channel_remap/red=0
process/channel_remap/green=1
process/channel_remap/blue=2
process/channel_remap/alpha=3
process/fix_alpha_border=true
process/premult_alpha=false
process/normal_map_invert_y=false
process/hdr_as_srgb=false
process/hdr_clamp_exposure=false
process/size_limit=0
detect_3d/compress_to=1
svg/scale=1.0
editor/scale_with_editor_scale=true
editor/convert_colors_with_editor_theme=true
+6
View File
@@ -0,0 +1,6 @@
<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>

After

Width:  |  Height:  |  Size: 1.2 KiB

@@ -0,0 +1,44 @@
[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
@@ -0,0 +1,20 @@
[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
@@ -0,0 +1,20 @@
[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
@@ -0,0 +1,20 @@
[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
@@ -0,0 +1,3 @@
[gd_resource type="ButtonGroup" format=3 uid="uid://wxtkg1wlxka8"]
[resource]
@@ -0,0 +1,87 @@
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;
}
@@ -0,0 +1 @@
uid://cxdfexn0nsnhs
@@ -0,0 +1,20 @@
[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")
@@ -0,0 +1,243 @@
[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"]
@@ -0,0 +1,22 @@
[gd_scene format=3 uid="uid://dgldacs362p7g"]
[ext_resource type="Script" uid="uid://dk6dt8fk1s43s" path="res://addons/csg_blockout/scripts/csg_top_blockout_bar.gd" id="1_154hl"]
[node name="HBoxContainer" type="HBoxContainer"]
visible = false
offset_right = 40.0
offset_bottom = 40.0
script = ExtResource("1_154hl")
[node name="Refresh" type="Button" parent="."]
layout_mode = 2
text = "刷新"
flat = true
[node name="Bake" type="Button" parent="."]
layout_mode = 2
text = "烘焙"
flat = true
[connection signal="pressed" from="Refresh" to="." method="_on_refresh_pressed"]
[connection signal="pressed" from="Bake" to="." method="_on_bake_pressed"]
File diff suppressed because one or more lines are too long
@@ -0,0 +1,183 @@
@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)
@@ -0,0 +1 @@
uid://w8ad8q4lneis
@@ -0,0 +1,429 @@
@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)
@@ -0,0 +1 @@
uid://bvf7nvtsq1c06
+208
View File
@@ -0,0 +1,208 @@
@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()
@@ -0,0 +1 @@
uid://brrnjx8pre80s
@@ -0,0 +1,596 @@
@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)
@@ -0,0 +1 @@
uid://c68dxahp0v5xg
@@ -0,0 +1,3 @@
@tool
# CSG Shortcut Manager has been removed. Shortcuts disabled.
extends Node
@@ -0,0 +1 @@
uid://1bqvx7teqnrl
@@ -0,0 +1,467 @@
@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")
@@ -0,0 +1 @@
uid://dr5f1egll7hdq
@@ -0,0 +1,388 @@
@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
}]
@@ -0,0 +1 @@
uid://rgfomqnhbhpk
@@ -0,0 +1,54 @@
@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")
@@ -0,0 +1 @@
uid://dk6dt8fk1s43s
@@ -0,0 +1,31 @@
@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)
@@ -0,0 +1 @@
uid://b3ws8vwtsqmjt
@@ -0,0 +1,19 @@
@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()
@@ -0,0 +1 @@
uid://dbavf0pl65chb
@@ -0,0 +1,36 @@
@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)
@@ -0,0 +1 @@
uid://bc1sxg4vy464o
@@ -0,0 +1,130 @@
@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)
@@ -0,0 +1 @@
uid://3il6xs7cr7gj
@@ -0,0 +1,39 @@
@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)
@@ -0,0 +1 @@
uid://belgcjd0ys212
+7 -7
View File
@@ -1676,7 +1676,7 @@ static func _is_symlink(path: String) -> bool:
return dir.is_link(path)
static func get_server_command() -> Array[String]:
static func get_server_command(trace: Callable = Callable()) -> Array[String]:
## `mode_override() == "user"` skips the dev_venv tier even when a nearby
## .venv exists — the override then becomes an actual workaround for
## the "user venv misidentified as dev checkout" bug, not just a
@@ -1687,7 +1687,7 @@ static func get_server_command() -> Array[String]:
print("MCP | using dev venv: %s" % venv_python)
return [venv_python, "-m", "godot_ai"]
var uvx := find_uvx()
var uvx := find_uvx(trace)
if not uvx.is_empty():
var version := get_plugin_version()
## PEP 440 local build tags (e.g. 3.0.2+local.1) are not on PyPI.
@@ -1715,7 +1715,7 @@ static func get_server_command() -> Array[String]:
cmd.append_array(["--from", "godot-ai==%s" % pypi_version, "godot-ai"])
return cmd
var system_cmd := _find_system_install()
var system_cmd := _find_system_install(trace)
if not system_cmd.is_empty():
print("MCP | using system install: %s" % system_cmd)
return [system_cmd]
@@ -1746,8 +1746,8 @@ static func get_server_launch_mode() -> String:
const PREWARM_TIMEOUT_MS := 180000
static func find_uvx() -> String:
return CliFinder.find(_uvx_cli_names())
static func find_uvx(trace: Callable = Callable()) -> String:
return CliFinder.find(_uvx_cli_names(), trace)
## Pure argv builder for the pre-warm spawn, split out so tests can pin the
@@ -2085,11 +2085,11 @@ static func find_worktree_src_dir(start_dir: String) -> String:
## directly: the finder adds the well-known-install-dirs and login-shell
## PATH tiers plus its per-exe cache, and this drops the private
## `_pick_best_path` cross-class reach (#711).
static func _find_system_install() -> String:
static func _find_system_install(trace: Callable = Callable()) -> String:
## Built with append, not a ternary of untyped literals — assigning a
## ternary's Array to Array[String] is a runtime error on newer Godot
## builds (same idiom as _uvx_cli_names above).
var names: Array[String] = ["godot-ai"]
if OS.get_name() == "Windows":
names.push_front("godot-ai.exe")
return CliFinder.find(names)
return CliFinder.find(names, trace)
+44 -5
View File
@@ -29,9 +29,9 @@ const _LOOKUP_TIMEOUT_MS := 3000
## Find any of the supplied exe names; returns the first hit.
## On Windows pass the .exe variant in `exe_names` if relevant.
static func find(exe_names: Array[String]) -> String:
static func find(exe_names: Array[String], trace: Callable = Callable()) -> String:
for name in exe_names:
var hit := _find_one(name)
var hit := _find_one(name, trace)
if not hit.is_empty():
return hit
return ""
@@ -49,19 +49,21 @@ static func invalidate(exe_name: String = "") -> void:
_mutex.unlock()
static func _find_one(exe_name: String) -> String:
static func _find_one(exe_name: String, trace: Callable = Callable()) -> String:
var started := Time.get_ticks_msec()
_mutex.lock()
var already_searched: bool = _searched.get(exe_name, false)
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)
return cached
# `_resolve()` does FileAccess + bounded subprocess lookup (forks
# `bash -lc` / `which`), which can take 100ms-1s. Holding the mutex across that
# would let a concurrent `invalidate()` on the main thread freeze the
# editor for the duration of the subprocess — which defeats the whole
# point of running CLI lookup off the main thread.
var hit := _resolve(exe_name)
var hit := _resolve(exe_name, trace)
_mutex.lock()
_cache[exe_name] = hit
_searched[exe_name] = true
@@ -69,14 +71,17 @@ static func _find_one(exe_name: String) -> String:
return hit
static func _resolve(exe_name: String) -> String:
static func _resolve(exe_name: String, trace: Callable = Callable()) -> String:
var started := Time.get_ticks_msec()
var is_windows := OS.get_name() == "Windows"
# 1. Well-known locations
for dir in _well_known_dirs():
var full := dir.path_join(exe_name)
if FileAccess.file_exists(full):
_trace_lookup(trace, "well_known", "resolved", started)
return full
_trace_lookup(trace, "well_known", "no_match", started)
# 2. Login shell lookup (Unix only)
if not is_windows:
@@ -87,24 +92,58 @@ static func _resolve(exe_name: String) -> String:
if shell.is_empty():
shell = "/bin/bash"
var stripped := exe_name.trim_suffix(".exe")
started = Time.get_ticks_msec()
var login_result := McpCliExec.run(shell, ["-lc", "command -v %s" % stripped], _LOOKUP_TIMEOUT_MS, false)
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)
return login_found
_trace_lookup(trace, "login_shell", _lookup_failure(login_result, false), started, login_result)
# 3. which / where with inherited PATH
var lookup := "where" if is_windows else "which"
started = Time.get_ticks_msec()
var result := McpCliExec.run(lookup, [exe_name], _LOOKUP_TIMEOUT_MS, false)
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)
return found
_trace_lookup(trace, "inherited_path", _lookup_failure(result, true), started, result)
return ""
static func _lookup_failure(result: Dictionary, absence_exit: bool) -> String:
for flag in ["termination_failed", "cancelled", "timed_out", "spawn_failed"]:
if bool(result.get(flag, false)):
return flag
var code := int(result.get("exit_code", -1))
if code == 0:
return "empty_output" if str(result.get("stdout", "")).strip_edges().is_empty() else "unusable_output"
return "not_found" if absence_exit and code == 1 else "nonzero_exit"
static func _trace_lookup(
trace: Callable, tier: String, status: String, started: int,
result: Dictionary = {}, cache_hit := false,
) -> void:
if not trace.is_valid():
return
trace.call({
"tier": tier, "status": status,
"elapsed_ms": clampi(Time.get_ticks_msec() - started, 0, 2147483647),
"cache_hit": cache_hit,
"exit_code": clampi(int(result.get("exit_code", -1)), -2147483648, 2147483647),
"timed_out": bool(result.get("timed_out", false)),
"spawn_failed": bool(result.get("spawn_failed", false)),
"cancelled": bool(result.get("cancelled", false)),
"termination_failed": bool(result.get("termination_failed", false)),
})
## 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
+1 -1
View File
@@ -3,5 +3,5 @@
name="Godot AI"
description="MCP server and AI tools for Godot"
author="Godot AI"
version="4.2.1"
version="4.2.3"
script="plugin.gd"
+7 -1
View File
@@ -1355,6 +1355,11 @@ func _startup_trace_call_end(name: String, started_ms: int) -> void:
)
func _startup_trace_lookup(record: Dictionary) -> void:
if _startup_trace_enabled:
print("MCP startup trace | lookup=%s" % JSON.stringify(record))
func _startup_trace_finish(path: String) -> void:
if not _startup_trace_enabled:
return
@@ -1392,7 +1397,8 @@ func _capture_lifecycle_plan() -> Dictionary:
_startup_trace_call_end("worktree_source", worktree_started)
var expected_version := ClientConfigurator.get_plugin_version()
var command_started := _startup_trace_call_begin("server_command")
var server_command := ClientConfigurator.get_server_command()
var lookup_trace := _startup_trace_lookup if _startup_trace_enabled else Callable()
var server_command := ClientConfigurator.get_server_command(lookup_trace)
_startup_trace_call_end("server_command", command_started)
var pid_file := ProjectSettings.globalize_path(PortResolver.SERVER_PID_FILE)
var startup_report := ProjectSettings.globalize_path(PortResolver.SERVER_STARTUP_REPORT)
+160
View File
@@ -0,0 +1,160 @@
@tool
extends RefCounted
## Linux process and listener evidence from the editor's own PID/network
## namespace. Never run host commands across a Flatpak/Steam sandbox boundary.
## proc files report length zero: read bounded chunks, not get_as_text().
static func read_text(path: String, limit := 1024 * 1024) -> Dictionary:
var file := FileAccess.open(path, FileAccess.READ)
if file == null:
return {"known": false, "text": ""}
var bytes := PackedByteArray()
while bytes.size() <= limit:
var chunk := file.get_buffer(mini(4096, limit + 1 - bytes.size()))
if chunk.is_empty():
break
bytes.append_array(chunk)
if file.eof_reached():
break
var error := file.get_error()
file.close()
if bytes.size() > limit or error not in [OK, ERR_FILE_EOF]:
return {"known": false, "text": ""}
for index in range(bytes.size()):
if bytes[index] == 0:
bytes[index] = 32
return {"known": true, "text": bytes.get_string_from_utf8()}
static func parse_stat(raw: String, expected_pid: int) -> Dictionary:
## comm may contain spaces, parentheses and newlines. The final ')' is
## the delimiter; field 22 (starttime) is index 19 after comm.
var opening := raw.find(" (")
var closing := raw.rfind(") ")
if opening < 1 or closing <= opening:
return {}
var pid_text := raw.substr(0, opening)
var fields := raw.substr(closing + 2).strip_edges().split(" ", false)
if not pid_text.is_valid_int() or int(pid_text) != expected_pid or fields.size() < 20:
return {}
if fields[0] not in ["R", "S", "D", "T", "t", "Z", "X", "x", "K", "W", "P", "I"]:
return {}
if not fields[1].is_valid_int() or int(fields[1]) < 0 or not fields[19].is_valid_int() or int(fields[19]) < 0:
return {}
return {"state": fields[0], "parent_pid": int(fields[1]), "start": fields[19]}
static func process_stat(pid: int, root := "/proc") -> Dictionary:
var read := read_text(root.path_join("%d/stat" % pid), 8192)
return parse_stat(read.text, pid) if read.known else {}
static func is_alive(row: Dictionary) -> bool:
return not row.is_empty() and row.state not in ["Z", "X", "x"]
static func commandline(pid: int, root := "/proc") -> String:
var read := read_text(root.path_join("%d/cmdline" % pid))
return str(read.text).strip_edges() if read.known else ""
static func process_snapshot(pid: int, root := "/proc") -> Dictionary:
if DirAccess.open(root) == null:
return {"capture_error": true}
var snapshot := {}
var current := pid
for _depth in range(16):
if current <= 1:
break
if snapshot.has(current):
return {"capture_error": true}
var first := process_stat(current, root)
if first.is_empty():
## A vanished process differs from an unreadable proc mount.
return snapshot if not DirAccess.dir_exists_absolute(root.path_join(str(current))) else {"capture_error": true}
if not is_alive(first):
return snapshot
var read := read_text(root.path_join("%d/cmdline" % current))
var final := process_stat(current, root)
if not read.known or not is_alive(final) or first.start != final.start or first.parent_pid != final.parent_pid:
return {"capture_error": true}
var command := str(read.text).strip_edges()
snapshot[current] = {"pid": current, "parent_pid": first.parent_pid,
"identity": "linux:" + str(first.start) + "|" + command, "commandline": command}
current = int(first.parent_pid)
return snapshot
static func parse_tcp(raw: String) -> Dictionary:
var listeners := {}
var lines := raw.strip_edges().split("\n", false)
if lines.is_empty() or not lines[0].contains("local_address") or not lines[0].contains("inode"):
return {"known": false, "listeners": {}}
for line in lines.slice(1):
var fields := line.replace("\t", " ").strip_edges().split(" ", false)
if fields.size() < 10 or not fields[1].contains(":"):
return {"known": false, "listeners": {}}
if fields[3] != "0A":
continue
var port_hex := fields[1].get_slice(":", 1)
if port_hex.length() != 4 or not port_hex.is_valid_hex_number() or not fields[9].is_valid_int() or int(fields[9]) <= 0:
return {"known": false, "listeners": {}}
var port := port_hex.hex_to_int()
if not listeners.has(port):
listeners[port] = []
listeners[port].append(fields[9])
return {"known": true, "listeners": listeners}
static func listener_snapshot(root := "/proc") -> Dictionary:
var listeners := {}
for name in ["tcp", "tcp6"]:
var path := root.path_join("net/" + name)
## IPv6 may be disabled at kernel build time; IPv4 must be readable.
if name == "tcp6" and not FileAccess.file_exists(path):
continue
var read := read_text(path, 8 * 1024 * 1024)
var parsed := parse_tcp(read.text) if read.known else {"known": false}
if not parsed.known:
return {"known": false, "listeners": {}}
for port in parsed.listeners:
if not listeners.has(port):
listeners[port] = []
listeners[port].append_array(parsed.listeners[port])
return {"known": true, "listeners": listeners}
static func listener_pids(port: int, snapshot: Dictionary, root := "/proc") -> Array[int]:
var result: Array[int] = []
if not snapshot.get("known", false) or not snapshot.listeners.has(port):
return result
var wanted: Array = snapshot.listeners[port]
var proc := DirAccess.open(root)
if proc == null:
return result
for name in proc.get_directories():
if not name.is_valid_int() or int(name) <= 1:
continue
var descriptors := DirAccess.open(root.path_join(name).path_join("fd"))
if descriptors == null:
continue
## fd entries are symlinks; their socket targets need not exist as files.
descriptors.list_dir_begin()
var descriptor := descriptors.get_next()
while not descriptor.is_empty():
var target := descriptors.read_link(descriptor)
if target.begins_with("socket:[") and target.ends_with("]") and wanted.has(target.substr(8, target.length() - 9)):
result.append(int(name))
break
descriptor = descriptors.get_next()
descriptors.list_dir_end()
return result
static func available() -> bool:
return (
is_alive(process_stat(OS.get_process_id()))
and DirAccess.open("/proc/self/fd") != null
and listener_snapshot().known
)

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