diff --git a/addons/csg_blockout/.gitignore b/addons/csg_blockout/.gitignore new file mode 100644 index 0000000..de511fe --- /dev/null +++ b/addons/csg_blockout/.gitignore @@ -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 + diff --git a/addons/csg_blockout/ARCHITECTURE.md b/addons/csg_blockout/ARCHITECTURE.md new file mode 100644 index 0000000..3777b2b --- /dev/null +++ b/addons/csg_blockout/ARCHITECTURE.md @@ -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). | diff --git a/addons/csg_blockout/ARCHITECTURE_CN.md b/addons/csg_blockout/ARCHITECTURE_CN.md new file mode 100644 index 0000000..2e59644 --- /dev/null +++ b/addons/csg_blockout/ARCHITECTURE_CN.md @@ -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 预设所引用的自定义材质资源路径(跨会话持久化)。 | diff --git a/addons/csg_blockout/DocsImages/MaterialPresets.webp b/addons/csg_blockout/DocsImages/MaterialPresets.webp new file mode 100644 index 0000000..a4292e4 Binary files /dev/null and b/addons/csg_blockout/DocsImages/MaterialPresets.webp differ diff --git a/addons/csg_blockout/DocsImages/MaterialPresets.webp.import b/addons/csg_blockout/DocsImages/MaterialPresets.webp.import new file mode 100644 index 0000000..74fc663 --- /dev/null +++ b/addons/csg_blockout/DocsImages/MaterialPresets.webp.import @@ -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 diff --git a/addons/csg_blockout/DocsImages/PieMenu.webp b/addons/csg_blockout/DocsImages/PieMenu.webp new file mode 100644 index 0000000..39cb21b Binary files /dev/null and b/addons/csg_blockout/DocsImages/PieMenu.webp differ diff --git a/addons/csg_blockout/DocsImages/PieMenu.webp.import b/addons/csg_blockout/DocsImages/PieMenu.webp.import new file mode 100644 index 0000000..f8dcd2a --- /dev/null +++ b/addons/csg_blockout/DocsImages/PieMenu.webp.import @@ -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 diff --git a/addons/csg_blockout/DocsImages/Sidebar.webp b/addons/csg_blockout/DocsImages/Sidebar.webp new file mode 100644 index 0000000..73d933f Binary files /dev/null and b/addons/csg_blockout/DocsImages/Sidebar.webp differ diff --git a/addons/csg_blockout/DocsImages/Sidebar.webp.import b/addons/csg_blockout/DocsImages/Sidebar.webp.import new file mode 100644 index 0000000..0940c84 --- /dev/null +++ b/addons/csg_blockout/DocsImages/Sidebar.webp.import @@ -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 diff --git a/addons/csg_blockout/DocsImages/Subtraction.webp b/addons/csg_blockout/DocsImages/Subtraction.webp new file mode 100644 index 0000000..8ba256d Binary files /dev/null and b/addons/csg_blockout/DocsImages/Subtraction.webp differ diff --git a/addons/csg_blockout/DocsImages/Subtraction.webp.import b/addons/csg_blockout/DocsImages/Subtraction.webp.import new file mode 100644 index 0000000..ae10fa1 --- /dev/null +++ b/addons/csg_blockout/DocsImages/Subtraction.webp.import @@ -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 diff --git a/addons/csg_blockout/LICENSE b/addons/csg_blockout/LICENSE new file mode 100644 index 0000000..6ecc29f --- /dev/null +++ b/addons/csg_blockout/LICENSE @@ -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. diff --git a/addons/csg_blockout/README.md b/addons/csg_blockout/README.md new file mode 100644 index 0000000..db33960 --- /dev/null +++ b/addons/csg_blockout/README.md @@ -0,0 +1,110 @@ +
+ + + CSG_Blockout Logo + +

CSG_Blockout

+ +

+ Docs English + 文档 简体中文 + Tutorial English + Architecture Internals +

+ +

+ Godot Engine + Godot AssetLib + License: MIT +

+ +

+ A high-performance, modernized 3D level blockout and rapid prototyping plugin designed specifically for Godot 4.7. +

+
+ +--- + +## 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). diff --git a/addons/csg_blockout/README_CN.md b/addons/csg_blockout/README_CN.md new file mode 100644 index 0000000..e444377 --- /dev/null +++ b/addons/csg_blockout/README_CN.md @@ -0,0 +1,110 @@ +
+ + + CSG_Blockout Logo + +

CSG_Blockout

+ +

+ Docs English + 文档 简体中文 + 教程 简体中文 + 架构 技术内幕 +

+ +

+ Godot 引擎 + Godot AssetLib + 开源协议: MIT +

+ +

+ 专为 Godot 4.7 打造的高性能、现代化的 3D 关卡白盒(Blockout)与快速原型设计插件。 +

+
+ +--- + +## 安装指南 + +### 途径 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) 开源发布。 diff --git a/addons/csg_blockout/TUTORIAL_CN.md b/addons/csg_blockout/TUTORIAL_CN.md new file mode 100644 index 0000000..dd61f45 --- /dev/null +++ b/addons/csg_blockout/TUTORIAL_CN.md @@ -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` 场景文件或备用,以便后续修改关卡结构。 diff --git a/addons/csg_blockout/TUTORIAL_EN.md b/addons/csg_blockout/TUTORIAL_EN.md new file mode 100644 index 0000000..4e0dfef --- /dev/null +++ b/addons/csg_blockout/TUTORIAL_EN.md @@ -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. diff --git a/addons/csg_blockout/csg_blockout.gd b/addons/csg_blockout/csg_blockout.gd new file mode 100644 index 0000000..2e25821 --- /dev/null +++ b/addons/csg_blockout/csg_blockout.gd @@ -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() diff --git a/addons/csg_blockout/csg_blockout.gd.uid b/addons/csg_blockout/csg_blockout.gd.uid new file mode 100644 index 0000000..5048361 --- /dev/null +++ b/addons/csg_blockout/csg_blockout.gd.uid @@ -0,0 +1 @@ +uid://cmwnf4lkck6mm diff --git a/addons/csg_blockout/plugin.cfg b/addons/csg_blockout/plugin.cfg new file mode 100644 index 0000000..70c961e --- /dev/null +++ b/addons/csg_blockout/plugin.cfg @@ -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" diff --git a/addons/csg_blockout/res/icon.svg b/addons/csg_blockout/res/icon.svg new file mode 100644 index 0000000..7587239 --- /dev/null +++ b/addons/csg_blockout/res/icon.svg @@ -0,0 +1,28 @@ + + + + diff --git a/addons/csg_blockout/res/icon.svg.import b/addons/csg_blockout/res/icon.svg.import new file mode 100644 index 0000000..d90efda --- /dev/null +++ b/addons/csg_blockout/res/icon.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icon_transparent.svg b/addons/csg_blockout/res/icon_transparent.svg new file mode 100644 index 0000000..38f13cb --- /dev/null +++ b/addons/csg_blockout/res/icon_transparent.svg @@ -0,0 +1,27 @@ + + + diff --git a/addons/csg_blockout/res/icon_transparent.svg.import b/addons/csg_blockout/res/icon_transparent.svg.import new file mode 100644 index 0000000..1b82c6e --- /dev/null +++ b/addons/csg_blockout/res/icon_transparent.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icon_transparent_light.svg b/addons/csg_blockout/res/icon_transparent_light.svg new file mode 100644 index 0000000..e618139 --- /dev/null +++ b/addons/csg_blockout/res/icon_transparent_light.svg @@ -0,0 +1,27 @@ + + + diff --git a/addons/csg_blockout/res/icon_transparent_light.svg.import b/addons/csg_blockout/res/icon_transparent_light.svg.import new file mode 100644 index 0000000..15fd96d --- /dev/null +++ b/addons/csg_blockout/res/icon_transparent_light.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/apply_material.svg b/addons/csg_blockout/res/icons/apply_material.svg new file mode 100644 index 0000000..8b8e92e --- /dev/null +++ b/addons/csg_blockout/res/icons/apply_material.svg @@ -0,0 +1 @@ + diff --git a/addons/csg_blockout/res/icons/apply_material.svg.import b/addons/csg_blockout/res/icons/apply_material.svg.import new file mode 100644 index 0000000..d09f371 --- /dev/null +++ b/addons/csg_blockout/res/icons/apply_material.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/box.svg b/addons/csg_blockout/res/icons/box.svg new file mode 100644 index 0000000..d86e36b --- /dev/null +++ b/addons/csg_blockout/res/icons/box.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/csg_blockout/res/icons/box.svg.import b/addons/csg_blockout/res/icons/box.svg.import new file mode 100644 index 0000000..ec902ee --- /dev/null +++ b/addons/csg_blockout/res/icons/box.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/config.svg b/addons/csg_blockout/res/icons/config.svg new file mode 100644 index 0000000..0b78a88 --- /dev/null +++ b/addons/csg_blockout/res/icons/config.svg @@ -0,0 +1,4 @@ + + + + diff --git a/addons/csg_blockout/res/icons/config.svg.import b/addons/csg_blockout/res/icons/config.svg.import new file mode 100644 index 0000000..c9fc449 --- /dev/null +++ b/addons/csg_blockout/res/icons/config.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/cyliner.svg b/addons/csg_blockout/res/icons/cyliner.svg new file mode 100644 index 0000000..d25e39a --- /dev/null +++ b/addons/csg_blockout/res/icons/cyliner.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/csg_blockout/res/icons/cyliner.svg.import b/addons/csg_blockout/res/icons/cyliner.svg.import new file mode 100644 index 0000000..fc41847 --- /dev/null +++ b/addons/csg_blockout/res/icons/cyliner.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/empty-material.svg b/addons/csg_blockout/res/icons/empty-material.svg new file mode 100644 index 0000000..0cac311 --- /dev/null +++ b/addons/csg_blockout/res/icons/empty-material.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/csg_blockout/res/icons/empty-material.svg.import b/addons/csg_blockout/res/icons/empty-material.svg.import new file mode 100644 index 0000000..aa12094 --- /dev/null +++ b/addons/csg_blockout/res/icons/empty-material.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/grid_dark.svg b/addons/csg_blockout/res/icons/grid_dark.svg new file mode 100644 index 0000000..9988199 --- /dev/null +++ b/addons/csg_blockout/res/icons/grid_dark.svg @@ -0,0 +1 @@ + diff --git a/addons/csg_blockout/res/icons/grid_dark.svg.import b/addons/csg_blockout/res/icons/grid_dark.svg.import new file mode 100644 index 0000000..608ef97 --- /dev/null +++ b/addons/csg_blockout/res/icons/grid_dark.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/grid_light.svg b/addons/csg_blockout/res/icons/grid_light.svg new file mode 100644 index 0000000..07346c8 --- /dev/null +++ b/addons/csg_blockout/res/icons/grid_light.svg @@ -0,0 +1 @@ + diff --git a/addons/csg_blockout/res/icons/grid_light.svg.import b/addons/csg_blockout/res/icons/grid_light.svg.import new file mode 100644 index 0000000..8e84360 --- /dev/null +++ b/addons/csg_blockout/res/icons/grid_light.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/grid_orange.svg b/addons/csg_blockout/res/icons/grid_orange.svg new file mode 100644 index 0000000..83076a4 --- /dev/null +++ b/addons/csg_blockout/res/icons/grid_orange.svg @@ -0,0 +1 @@ + diff --git a/addons/csg_blockout/res/icons/grid_orange.svg.import b/addons/csg_blockout/res/icons/grid_orange.svg.import new file mode 100644 index 0000000..8699447 --- /dev/null +++ b/addons/csg_blockout/res/icons/grid_orange.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/intersection.svg b/addons/csg_blockout/res/icons/intersection.svg new file mode 100644 index 0000000..6021360 --- /dev/null +++ b/addons/csg_blockout/res/icons/intersection.svg @@ -0,0 +1,4 @@ + + + + diff --git a/addons/csg_blockout/res/icons/intersection.svg.import b/addons/csg_blockout/res/icons/intersection.svg.import new file mode 100644 index 0000000..8bbe109 --- /dev/null +++ b/addons/csg_blockout/res/icons/intersection.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/material_none.svg b/addons/csg_blockout/res/icons/material_none.svg new file mode 100644 index 0000000..be94de4 --- /dev/null +++ b/addons/csg_blockout/res/icons/material_none.svg @@ -0,0 +1 @@ + diff --git a/addons/csg_blockout/res/icons/material_none.svg.import b/addons/csg_blockout/res/icons/material_none.svg.import new file mode 100644 index 0000000..9a334b1 --- /dev/null +++ b/addons/csg_blockout/res/icons/material_none.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/mesh.svg b/addons/csg_blockout/res/icons/mesh.svg new file mode 100644 index 0000000..8f65ccc --- /dev/null +++ b/addons/csg_blockout/res/icons/mesh.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/csg_blockout/res/icons/mesh.svg.import b/addons/csg_blockout/res/icons/mesh.svg.import new file mode 100644 index 0000000..fb33057 --- /dev/null +++ b/addons/csg_blockout/res/icons/mesh.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/polygon.svg b/addons/csg_blockout/res/icons/polygon.svg new file mode 100644 index 0000000..4fa9d96 --- /dev/null +++ b/addons/csg_blockout/res/icons/polygon.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/csg_blockout/res/icons/polygon.svg.import b/addons/csg_blockout/res/icons/polygon.svg.import new file mode 100644 index 0000000..5df512c --- /dev/null +++ b/addons/csg_blockout/res/icons/polygon.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/sphere.svg b/addons/csg_blockout/res/icons/sphere.svg new file mode 100644 index 0000000..a51db6e --- /dev/null +++ b/addons/csg_blockout/res/icons/sphere.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/csg_blockout/res/icons/sphere.svg.import b/addons/csg_blockout/res/icons/sphere.svg.import new file mode 100644 index 0000000..2504e11 --- /dev/null +++ b/addons/csg_blockout/res/icons/sphere.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/subtraction.svg b/addons/csg_blockout/res/icons/subtraction.svg new file mode 100644 index 0000000..a99d3cf --- /dev/null +++ b/addons/csg_blockout/res/icons/subtraction.svg @@ -0,0 +1,4 @@ + + + + diff --git a/addons/csg_blockout/res/icons/subtraction.svg.import b/addons/csg_blockout/res/icons/subtraction.svg.import new file mode 100644 index 0000000..459df71 --- /dev/null +++ b/addons/csg_blockout/res/icons/subtraction.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/torus.svg b/addons/csg_blockout/res/icons/torus.svg new file mode 100644 index 0000000..39661b4 --- /dev/null +++ b/addons/csg_blockout/res/icons/torus.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/addons/csg_blockout/res/icons/torus.svg.import b/addons/csg_blockout/res/icons/torus.svg.import new file mode 100644 index 0000000..61ee0a7 --- /dev/null +++ b/addons/csg_blockout/res/icons/torus.svg.import @@ -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 diff --git a/addons/csg_blockout/res/icons/union.svg b/addons/csg_blockout/res/icons/union.svg new file mode 100644 index 0000000..ed72562 --- /dev/null +++ b/addons/csg_blockout/res/icons/union.svg @@ -0,0 +1,6 @@ + + + + + + diff --git a/addons/csg_blockout/res/icons/union.svg.import b/addons/csg_blockout/res/icons/union.svg.import new file mode 100644 index 0000000..abb6a2b --- /dev/null +++ b/addons/csg_blockout/res/icons/union.svg.import @@ -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 diff --git a/addons/csg_blockout/res/materials/mat_grid_dark.tres b/addons/csg_blockout/res/materials/mat_grid_dark.tres new file mode 100644 index 0000000..a0215f5 --- /dev/null +++ b/addons/csg_blockout/res/materials/mat_grid_dark.tres @@ -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 diff --git a/addons/csg_blockout/res/materials/mat_grid_light.tres b/addons/csg_blockout/res/materials/mat_grid_light.tres new file mode 100644 index 0000000..e6c49d9 --- /dev/null +++ b/addons/csg_blockout/res/materials/mat_grid_light.tres @@ -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 diff --git a/addons/csg_blockout/res/materials/mat_grid_orange.tres b/addons/csg_blockout/res/materials/mat_grid_orange.tres new file mode 100644 index 0000000..c743fca --- /dev/null +++ b/addons/csg_blockout/res/materials/mat_grid_orange.tres @@ -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 diff --git a/addons/csg_blockout/res/operation_group.tres b/addons/csg_blockout/res/operation_group.tres new file mode 100644 index 0000000..3f03eea --- /dev/null +++ b/addons/csg_blockout/res/operation_group.tres @@ -0,0 +1,3 @@ +[gd_resource type="ButtonGroup" format=3 uid="uid://wxtkg1wlxka8"] + +[resource] diff --git a/addons/csg_blockout/res/shaders/grid_triplanar.gdshader b/addons/csg_blockout/res/shaders/grid_triplanar.gdshader new file mode 100644 index 0000000..f07d8b7 --- /dev/null +++ b/addons/csg_blockout/res/shaders/grid_triplanar.gdshader @@ -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; +} diff --git a/addons/csg_blockout/res/shaders/grid_triplanar.gdshader.uid b/addons/csg_blockout/res/shaders/grid_triplanar.gdshader.uid new file mode 100644 index 0000000..5b6821c --- /dev/null +++ b/addons/csg_blockout/res/shaders/grid_triplanar.gdshader.uid @@ -0,0 +1 @@ +uid://cxdfexn0nsnhs diff --git a/addons/csg_blockout/scenes/csg_quick_actions.tscn b/addons/csg_blockout/scenes/csg_quick_actions.tscn new file mode 100644 index 0000000..3c9044b --- /dev/null +++ b/addons/csg_blockout/scenes/csg_quick_actions.tscn @@ -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") diff --git a/addons/csg_blockout/scenes/csg_side_blockout_bar.tscn b/addons/csg_blockout/scenes/csg_side_blockout_bar.tscn new file mode 100644 index 0000000..e0bdd75 --- /dev/null +++ b/addons/csg_blockout/scenes/csg_side_blockout_bar.tscn @@ -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"] diff --git a/addons/csg_blockout/scenes/csg_top_blockout_bar.tscn b/addons/csg_blockout/scenes/csg_top_blockout_bar.tscn new file mode 100644 index 0000000..2bb7fde --- /dev/null +++ b/addons/csg_blockout/scenes/csg_top_blockout_bar.tscn @@ -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"] diff --git a/addons/csg_blockout/scenes/demo.tscn b/addons/csg_blockout/scenes/demo.tscn new file mode 100644 index 0000000..56e5d74 --- /dev/null +++ b/addons/csg_blockout/scenes/demo.tscn @@ -0,0 +1,58 @@ +[gd_scene format=4 uid="uid://b6adrnqobv5fi"] + +[sub_resource type="ArrayMesh" id="ArrayMesh_4qltd"] +_surfaces = [{ +"aabb": AABB(-2.5, -2.5, -2.5, 5, 4.498816, 5), +"attribute_data": PackedByteArray("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"), +"format": 34359738391, +"primitive": 3, +"uv_scale": Vector4(0, 0, 0, 0), +"vertex_count": 822, +"vertex_data": PackedByteArray("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AAAAAP9/AAD//wAA/38AAAAAAAD/fwAA//8AAP9/AAAAAAAAAAD/f/9//n8AAP9//3/+fwAA/3//f/5//////wAA/z//////AAD/P/////8AAP8/AAD/f/9//n8AAP9//3/+fwAA/3//f/5/AAD/f/9//n8AAP9//3/+fwAA/3//f/5//////wAA/z//////AAD/P/////8AAP8//////wAA/z//////AAD/P/////8AAP8//////wAA/z//////AAD/P/////8AAP8//////wAA/z//////AAD/P/////8AAP8/AAD/f/9//n8AAP9//3/+fwAA/3//f/5/AAD/f/9//n8AAP9//3/+fwAA/3//f/5/AAD/f/9//n8AAP9//3/+fwAA/3//f/5/AAD/f/9//n8AAP9//3/+fwAA/3//f/5//////wAA/z//////AAD/P/////8AAP8//38AAAAAAAD/fwAA//8AAP9/AAD//wAA/38AAP//AAD/fwAA//8AAP9/AAD//wAA/////wAA/z//////AAD/P/////8AAP8//////wAA/z//////AAD/P/////8AAP8//38AAP//AAD/fwAA//8AAP9/AAAAAAAA/38AAP//AAD/fwAA//8AAP9/AAD//wAA/38AAP//AAD/fwAA//8AAP9/AAD//wAAAAD/f/9//n8AAP9//3/+fwAA/3//f/5//38AAP//AAD/fwAA//8AAP9/AAD//wAA/3//fwAA/z//f/9/AAD/P/9//38AAP8//3//////AAD/f/////8AAP9//////wAAAAD/f/9//n8AAP9//3/+fwAA/3//f/5//3//////AAD/f/////8AAP9//////wAA/3//////AAD/f/////8AAP9//////wAA/3//////AAD/f/////8AAP9//////wAA/3//////AAD/f/////8AAP9//////wAA/3///wAAAAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f/////8AAP9//////wAA/3///wAAAAD/f/////8AAP9///8AAAAA/3///wAAAAD/f///AAAAAP9//////wAA/3//////AAD/f/////8AAP9//////wAA/3///wAAAAD/f///AAAAAP9///8AAAAA/3//////AAD/f///AAAAAP9//////wAA/3//////AAD/f/////8AAP9//////wAA/3//////AAD/f/////8AAP9//////wAA/3//////AAD/f/////8AAP9///8AAAAA/3//////AAD/f///AAAAAP9//////wAA/3//////AAD/f/////8AAP9//////wAA/3///wAAAAD/f///AAAAAP9//////wAA/3//////AAD/f///AAAAAP9///8AAAAA/3//////AAD/f/////8AAP9//////wAA/3///wAAAAD/f/////8AAP9//////wAA/3///wAAAAD/f/////8AAP9///8AAAAA/3//////AAD/f/////8AAP9//////wAA/3//////AAD/f/////8AAP9///8AAAAA/3//////AAD/f///AAAAAP9///8AAAAA/38AAP//AAD/fwAA//8AAP9/AAD//wAA/38AAP//AAD/fwAA//8AAP9/AAD//wAA/////wAA/z//////AAD/P/////8AAP8//////wAA/z//////AAD/P/////8AAP8/////f/9//n////9//3/+f////3//f/5//38AAP//AAD/fwAA//8AAP9/AAD//wAA/3//fwAA/z//f/9/AAD/P/9//38AAP8/////f/9//n////9//3/+f////3//f/5//////wAA/z//////AAD/P/////8AAP8//3///wAAAAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f///AAAAAP9//////wAA/3//////AAD/f///AAAAAP9//////wAA/3//////AAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f/////8AAP9///8AAAAA/3///wAAAAD/f/////8AAP9//////wAA/3///wAAAAD/f///AAAAAP9//////wAA/3//////AAD/f/////8AAP9//////wAA/3//////AAD/f///AAAAAP9///8AAAAA/3//////AAD/f///AAAAAP9///8AAAAA/3//////AAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f///AAAAAP9///8AAAAA/3///wAAAAD/f///AAAAAP9///8AAAAAAaDAx176CmKkuWS+y/jnAyC5q9BBDKt9ILmr0EEMq32kuWS+y/jnA4DIvNaMBYIcAaDAx2T68p0l0dmu/v8AgCunGqVH+l/eILmr0NgLnYLT4JK9RAGxkyXR2a7+/wCA0+CSvUQBsZPA4hnUfg/znCXR2a7+/wCAAaDAx2T68p0guavQ2AudgiXR2a7+/wCAAaDAx176CmIBhnzOSvgKRT6ffbdx+rYgPp99t3H6tiABhnzOSvgKRQl4ZL7/8FI8AaDAx176CmI+n323cfq2IKS5ZL7L+OcDAaDAx2T68p0rpxqlR/pf3gGGfM5e+O26snUTq8XtoMMBhnzOXvjtuiunGqVH+l/eILmr0EEMq32AyLzWjAWCHD64weegBcRg0+CSvUQBsZPj2kHH9gnJ5wjpb6dKBcv1COlvp0oFy/Xj2kHH9gnJ5/fyYJZSBLn6ILmr0NgLnYLj2kHH9gnJ59Pgkr1EAbGTILmr0NgLnYI+uMHnmgU/n+PaQcf2CcnnPrjB56AFxGCAyLzWjAWCHJrBCfhkB4A4PrjB56AFxGCawQn4ZAeAOHxO/fkaCtZDPrjB55oFP5/q1LH1QgccyePaQcf2CcnnPrjB55oFP598Tv35IwouvOrUsfVCBxzJAYZ8zkr4CkXCZ8DHqs8qOwl4ZL7/8FI8CXhkvv/wUjzCZ8DHqs8qO71WfbdKvlY1snUTq8XtoMPCZ8DHqs/UxAGGfM5e+O26snUTq8XtoMNHTBqlubabycJnwMeqz9TEvVZ9txMLCuvCZ8DHZRKL7H5UfM4mCjCPvVZ9txMLCut+VHzOJgowj2Q+Wcb+GHGHwmfAx2UScxNHTBqlmQmNF35UfM4dCtRwflR8zh0K1HBHTBqlmQmNFxQrYbV2Ga5/msEJ+GQHgDh+N7/gzT4jPXxO/fkaCtZDfje/4M0+Iz3AR/3fCkTWQnxO/fkaCtZDfE79+SMKLrzAR/3fGkQyvRsl09jAPNvCfE79+SMKLrwbJdPYwDzbwurUsfVCBxzJZD5Zxm5mDEbAR/3fCkTWQn43v+DNPiM9ZD5Zxm5mDEZ+VHzOr2fxScBH/d8KRNZCFCthtSxsbrobJdPYwDzbwsBH/d8aRDK9flR8zq9nDbYUK2G1LGxuusBH/d8aRDK91cj/f///lBs+n323G/snIYaP/3///zw4/3//////QO3/f///////3/9////+//+//3///3yl/z//f////v//P/9/////v/8///+GjwAAwwekuWS+pvlcBNXI/3///5Qb/3///////9//f///////v/9////+//+///+GjwAAwwf//9XIAABqJIDIvNbjBGYc/3////+//z//f////v//P/9///////8////VyAAAaiQAAHfwhw//P5rBCfhYBnY4ho//f///PDgJeGS+7fDTPNVI/3/VyP8//3///////7//f////7//v/9////+//+//3////+//7//f///fKX/v/9////+//+//3////7//z//f/////++Ev9///////8f1Uj/fwAAlNu9Vn23FgsL64cP/38AADv4hw//f4aP/z9kPlnGa2YNRgAAKbfVSP8//3////7//z//f///////H/9///////8/AAB38IcP/z8AACm31Uj/P343v+CwPY09ho//f///PDg+n323G/snIQl4ZL7t8NM81cj/f///lBukuWS+pvlcBD6ffbcb+ych//+GjwAAwweAyLzW4wRmHKS5ZL6m+VwE///VyAAAaiSawQn4WAZ2OIDIvNbjBGYc1Uj/f9XI/z8JeGS+7fDTPL1WfbdKvlY1hw//fwAAO/i9Vn23FgsL62Q+WcbyGGiHAAB38IcP/z9+N7/gsD2NPZrBCfhYBnY4AAApt9VI/z9kPlnGa2YNRn43v+CwPY09mZmJ3/Jk/z8Cl03e7WQqQouaE+PTZs09i5oT43aXSkT/f///DJv/P0uiQOoMm808i5oT49NmzT0Cl03e7WQqQv9////yZP8/ApdN3gdFSzyNjLLdyEHPQv9/////P/8/S6JA6ie8z0L/f////7//P7ChUPWdvUs8/3///wwb/z+NjLLdcCHNPP9/8uTOHkFD/3///wAAhk3/f/LkggaYMHFzst1kBrhQ/3///wAA/19xc7LdnwUbH/xoTd5pB4Ni/3////Lk/z9GsfT7fuI4PLChUPWX4CpCsKFQ9ZfgKkJGsfT7fuI4PPK1mPTy5P8//3//////hk1/RIvs2/zIV0ax9Psf8RlQ/3///wAAeXL8aE3eVQSJDRdQ+dG0/XB//3///////19GQ8nb4/yDaH9Ei+zb/MhX/3//////eXIXUPnRtP1wf0ZDydvj/INo/GhN3lUEiQ2FUMzDAACGDRdQ+dG0/XB/f0SL7Nv8yFdGQ8nb4/yDaD4mx99p/0hX8rWY9PLk/z9GsfT7fuI4PDzSne7y5P8/RrH0+37iODxl0fD+y+sOOjzSne7y5P8/RrH0+x/xGVB/RIvs2/zIV2XR8P668sRJf0SL7Nv8yFc+Jsffaf9IV2XR8P668sRJhVDMwwAAhg2ZLqevNf9lfxdQ+dG0/XB/F1D50bT9cH8NJpvHav9aaEZDydvj/INoRkPJ2+P8g2gNJpvHav9aaD4mx99p/0hXhVDMwwAAhg0wNmiiAACGDZkup681/2V/F1D50bT9cH+ZLqevNf9lfw0mm8dq/1poPNKd7vLk/z9l0fD+y+sOOv//DBvy5P8/ZdHw/svrDjoAAH4K//8/Rf//DBvy5P8/ZdHw/rryxEk+Jsffaf9IVwAAfgr//8A6AAB+Cv//wDo+Jsffaf9IV8QEZs+z/60m//8MG/Lk/z8AAH4K//8/RbrmbAjQ73BC//8MG/Lk/z+65mwI0O9wQqrpVBby5P8/AAB+Cv//wDr9FCon//9sV7rmbAi07vE6AAB+Cv//wDrEBGbPs/+tJv0UKif//2xXMDZoogAAhg1kDDGGYwA8A5kup681/2V/mS6nrzX/ZX+GA35R1//saQ0mm8dq/1pomS6nrzX/ZX9kDDGGYwA8A4YDflHX/+xpDSabx2r/WmjEBGbPs/+tJj4mx99p/0hXDSabx2r/WmiGA35R1//sacQEZs+z/60mhgN+Udf/7GkMG/Jk///+f/0U10P//5JohgN+Udf/7Gn9FCon//9sV8QEZs+z/60mhgN+Udf/7Gn9FNdD//+SaP0UKif//2xXMDZoogAAhg2xIZN3AAB5cmQMMYZjADwDsSGTdwAAeXIMG/Jk///+f2QMMYZjADwDhgN+Udf/7GlkDDGGYwA8Awwb8mT///5/sSGTdwAAeXKpLKppAAB5cgwb8mT///5/7QtGV3yl//+B2gAAfKX//wEGfE7hdSr8AQZ8TuF1KvyB2gAAfKX//9kIqfNqgYX67QtGV3yl//+CMf15Z3mP+n1FWZR8pf//7QtGV3yl//8BBnxO4XUq/IIx/XlneY/6fUVZlHyl//+CMf15Z3mP+j6JoYZVeLP/fUVZlHyl//8+iaGGVXiz//9/fKWCWv//AQZ8TuF1Kvw+OP1fmm1Z44Ix/XlneY/6gjH9eWd5j/r/f/8//3//3z6JoYZVeLP/AQZ8TuF1Kvw9GD64qonU5z44/V+abVnjAQZ8TuF1KvzZCKnzaoGF+j0YPriqidTngjH9eWd5j/o+OP1fmm1Z4/9//z//f//fPRg+uKqJ1OfZCKnzaoGF+v+/////f//fPjj9X6kmKnv/fwAAAAD+f/9//z//v/5/PRg+uBjZzHb/v////7/+f4ErgrFWHvB6Pjj9X6kmKns9GD64GNnMdoErfE6u5w12PRg+uBjZzHaBK4KxVh7weoErfE6u5w12Pjj9X6kmKnuBK3xOrucNdv9/AAAAAP5/gSuCsVYe8Hr/v////7/+f/9//////wAAWhR8xXylAIAAAD5g/38AgLkoEnSCWgCAAAA+YP9/AIBtOv9//38AgLkoEnSCWgCAWhR8xXylAICiBr/2VXhLgAAAPmD/fwCAWhR8xXylAICB2v//fKUAgKIGv/ZVeEuAAAA+YP9/AICiBr/2VXhLgD0YwEcYexmguSgSdIJaAIBtOv9//38AgP9/glp8pQCAbTr/f/9/AIBUjNiIgYGChf9/glp8pQCAPRjARxh7GaD/vwAA/3//n4ErfE4citSwAAA+YP9/AIA9GMBHGHsZoD44AaBgegqiPRjARxh7GaCiBr/2VXhLgP+/AAD/f/+fAAA+YP9/AIA+OAGgYHoKom06/3//fwCAbTr/f/9/AIA+OAGgYHoKov9//7//f/+fPRjARxh7GaCBK3xOHIrUsIErgrEbbNezgSt8ThyK1LD/vwAA/3//n/9/AAD/f/+/PRjARxh7GaCBK4KxG2zXsz44AaBgegqigSuCsRts17P/f////3//vz44AaBgegqiPjgBoGB6CqL/f////3//v/9//7//f/+fbTr/f/9/AID/f/+//3//n1SM2IiBgYKF2Qip84GBggWB2gAAgloAAP//bLr/fwAAgdoAAIJaAAAR9EZXgloAAP//bLr/fwAAPomhhlV4s/+BulmUglr///9/fKWCWv//PomhhlV4s/894P9//3///4G6WZSCWv//EfRGV4Ja//+BulmUglr//z3g/3//f///EfRGV4Ja//894P9//3//////bLr/f///PomhhlV4s//Ax8JnqonU5z3g/3//f///2Qip84GBggX//2y6/38AAP+/////f/8f//9suv9///894P9//3////3fPriabVnjPomhhlV4s///f/8//3//38DHwmeqidTnPeD/f/9////Ax8JnqonU5/3fPriabVnj//9suv9/AAD93z64mm2lHP+/////f/8f/3//P/+//n98zn5UVh7wesDHwmcY2cx2/7////+/AID93z64qSbUhP9/////////wMfCZxjZzHZ8zoCrrucNdv3fPripJip7/3//P/+//n//fwAAAAD+f3zOflRWHvB6wMfCZxjZzHZ8zn5UVh7wenzOgKuu5w12/d8+uKkm1IR8zoCrrufxif9/////////gdr//3ylAIDA4hnUDq0dgqIGv/ZVeEuAoga/9lV4S4DA4hnUDq0dgv//pa3/fwCA0+CSva63yIcI6W+n1rHAhv//pa3/fwCA0+CSva63yIf//6Wt/38AgMDiGdQOrR2CCOlvp9axwIb38mCWVKufg///pa3/fwCA//+lrf9/AID38mCWVKufg4bUuXqcd1mDoga/9lV4S4D//6Wt/38AgP+/AAD/f/+f//+lrf9/AICG1Ll6nHdZg/3fwEcYe1ii/3+CWnyl/n9UjNiIaoGFeobUuXqzd5t8/3+CWnyl/n+G1Ll6s3ebfMDHhm98pf5/9/JgllSrn4PAx4ZvfKUAgIbUuXqcd1mD/78AAP9//5/938BHGHtYov9/AAD/f/+///+lrf9/AID938BHGHtYov+/AAD/f/+fVIzYiGqBhXrAxzyYZXo/X4bUuXqzd5t8htS5epx3WYPAxzyYWHrLoP3fwEcYe1iiVIzYiGqBhXr/f/+//3//X8DHPJhlej9f/d/ARxh7WKJ8zn5UG2zXs/9/AAD/f/+//d/ARxh7WKLAxzyYWHrLoHzOflQbbNezfM5+VBts17PAxzyYWHrLoHzOgKscitSw/3////9//z98zoCrHIoqT/9//7//f/9ffM6AqxyKKk/AxzyYZXo/X/9//7//f/9fd/D/f///wwfVyP9///+UGym3/3///2okKbf/f///aiSGj/9///88OHhw/3938P8/d/D/f///wwf//4aPAADDB9XI/3///5Qbho//f///PDgpt/9///9qJNXI/3///5Qb1Uj/f9XI/z94cP9/d/D/P4aP/3///zw4eHD/f3fw/z/VSP9/1cj/Pyk3/38pt/8/KTf/fwAAauSHD/9/AAA7+AAAho///zv4KTf/fwAAauTVSP9/AACU24cP/38AADv4//8ptwAAlBv//4aPAADDB3fw/3///8MH//8ptwAAlBv//3fwAAA8OP//1cgAAGok//938AAAPDgAANXIKTf/PwAAd/CHD/8///+GjwAAwwf//ym3AACUG///1cgAAGok//938AAAPDgAAHfwhw//P///1cgAAGokAACGj3hw/z8AACm31Uj/PwAA1cgpN/8/AADVyCk3/z8AACm31Uj/PwAAd/CHD/8/AAApt9VI/z8AAIaPeHD/P4cP/3+Gj/8/") +}] + +[sub_resource type="ConcavePolygonShape3D" id="ConcavePolygonShape3D_ghy1h"] +data = PackedVector3Array(-2.5, -2.5, -2.5, -2.2008, -2.5, 0, -2.0029, -2.5, -0.47779998, -2.5, -2.5, -2.5, -2.0029, -2.5, -0.47779998, -1.525, -2.5, -0.67579997, -2.5, -2.5, -2.5, -2.5, -2.5, 2.5, -2.2008, -2.5, 0, -2.5, -2.5, -2.5, -2.5, -0.65279996, -2.1371, -2.5, -2.5, 2.5, -2.5, -2.5, -2.5, -2.3869, -0.7298, -2.5, -2.5, -0.69949996, -2.5, -2.5, -2.5, -2.5, -2.5, -0.69949996, -2.5, -2.5, -0.7025, -2.4582999, -2.5, -2.5, -2.5, -2.5, -0.7025, -2.4582999, -2.5, -0.69479996, -2.3511999, -2.5, -2.5, -2.5, -2.3387, -0.7392, -2.5, -2.3869, -0.7298, -2.5, -2.5, -2.5, -2.5, -2.3146, -0.7409, -2.5, -2.3387, -0.7392, -2.5, -2.5, -2.5, -2.5, -2.2905, -0.7392, -2.5, -2.3146, -0.7409, -2.5, -2.5, -2.5, -2.5, -2.2423, -0.7298, -2.5, -2.2905, -0.7392, -2.5, -2.5, -2.5, -2.5, -2.5, -0.69479996, -2.3511999, -2.5, -0.65279996, -2.1371, -2.5, -0.65279996, -2.1371, -2.5, -0.5973, -1.9302, -2.5, -2.5, 2.5, -2.5, -0.5973, -1.9302, -2.5, -0.1744, -1.5073, -2.5, -2.5, 2.5, -2.5, -2.5, 2.5, -2.5, -0.1744, -1.5073, -2.5, 0.4033, -1.3525, -2.5, -2.5, -2.5, 2.5, -2.5, -2.5, -2.2423, -0.7298, -2.5, -2.5, -2.5, -2.5, -1.525, -2.5, -0.67579997, 2.5, -2.5, -2.5, 2.5, -2.5, -2.5, -1.525, -2.5, -0.67579997, -1.0472, -2.5, -0.47779998, -2.2423, -0.7298, -2.5, 2.5, -2.5, -2.5, -1.7480999, -0.5973, -2.5, -1.7480999, -0.5973, -2.5, 2.5, -2.5, -2.5, -1.3251, -0.1744, -2.5, -2.5, -2.5, 2.5, -2.0029, -2.5, 0.47779998, -2.2008, -2.5, 0, -2.5, -2.5, 2.5, -1.525, -2.5, 0.67579997, -2.0029, -2.5, 0.47779998, -2.5, -2.5, 2.5, -1.0472, -2.5, 0.47779998, -1.525, -2.5, 0.67579997, -2.5, -2.5, 2.5, -2.5, 0.4033, -1.3525, -2.5, 0.5, 2.5, -2.5, -2.5, 2.5, 2.5, -2.5, -2.5, -1.0472, -2.5, 0.47779998, -2.5, -2.5, 2.5, -2.5, 0.5, 2.5, 2.5, 0.5, 2.5, -2.3146, 0.5, -1.3287, -1.525, 0.5, -0.67579997, -2.0029, 0.5, -0.47779998, -2.5, 0.4033, -1.3525, -2.5, 0.5, -1.3784, -2.5, 0.5, 2.5, -2.5, 0.5, -1.3784, -2.3146, 0.5, -1.3287, -2.5, 0.5, 2.5, -2.3146, 0.5, -1.3287, -2.0029, 0.5, -0.47779998, -2.2008, 0.5, 0, -2.3146, 0.5, -1.3287, -1.7498, 0.5, -1.48, -1.525, 0.5, -0.67579997, -1.7498, 0.5, -1.48, -1.0472, 0.5, -0.47779998, -1.525, 0.5, -0.67579997, -1.3363999, 0.5, -1.8935, -1.1851, 0.5, -2.4582999, 2.5, 0.5, -2.5, -1.3363999, 0.5, -1.8935, 0.0046, 0.5, -1.3929, -0.2034, 0.5, -1.2728, -1.3363999, 0.5, -1.8935, -0.2034, 0.5, -1.2728, -0.2034, 0.5, -1.0327, -1.3363999, 0.5, -1.8935, 0.4973, 0.5, 0.21949999, -1.7498, 0.5, -1.48, -1.7498, 0.5, -1.48, 0.0046, 0.5, 0.93979996, -1.0472, 0.5, -0.47779998, -1.3363999, 0.5, -1.8935, -0.2034, 0.5, -1.0327, 0.4973, 0.5, 0.21949999, -1.7498, 0.5, -1.48, 0.4973, 0.5, 0.21949999, 0.1655, 0.5, 1.0205, -2.3146, 0.5, -1.3287, -2.2008, 0.5, 0, -2.5, 0.5, 2.5, -2.5, 0.5, 2.5, -2.2008, 0.5, 0, -2.0029, 0.5, 0.47779998, -2.5, 0.5, 2.5, -2.0029, 0.5, 0.47779998, -1.525, 0.5, 0.67579997, -2.5, 0.5, 2.5, -1.525, 0.5, 0.67579997, -0.2034, 0.5, 1.3, 0.0046, 0.5, 0.93979996, -0.84919995, 0.5, 0, -1.0472, 0.5, -0.47779998, -0.2034, 0.5, 1.0598, -1.0472, 0.5, 0.47779998, -0.84919995, 0.5, 0, -0.2034, 0.5, 1.3, -1.525, 0.5, 0.67579997, -1.0472, 0.5, 0.47779998, -1.7498, 0.5, -1.48, 0.1655, 0.5, 1.0205, 0.0046, 0.5, 0.93979996, -0.2034, 0.5, 1.0598, -0.84919995, 0.5, 0, 0.0046, 0.5, 0.93979996, -0.2034, 0.5, 1.0598, -0.2034, 0.5, 1.3, -1.0472, 0.5, 0.47779998, -2.5, 0.5, 2.5, -0.2034, 0.5, 1.3, 0.0046, 0.5, 1.4201, -2.5, 0.5, 2.5, 0.0046, 0.5, 1.4201, 0.4973, 0.5, 1.8215, -2.5, 0.5, 2.5, 0.4973, 0.5, 1.8215, 1.2982, 0.5, 2.1533, 2.5, -2.5, -2.5, -1.0472, -2.5, -0.47779998, -0.84919995, -2.5, 0, 2.5, -2.5, -2.5, -0.84919995, -2.5, 0, -1.0472, -2.5, 0.47779998, -1.3251, -0.1744, -2.5, 2.5, -2.5, -2.5, -1.1703, 0.4033, -2.5, -1.1703, 0.4033, -2.5, 2.5, -2.5, -2.5, 2.5, 0.5, -2.5, 2.5, -2.5, -2.5, 2.5, 0.5, 2.5, 2.5, 0.5, -2.5, -2.5, -2.5, 2.5, 2.5, -2.5, 2.5, 2.5, -2.5, -2.5, -2.5, -2.5, 2.5, 2.5, 0.5, 2.5, 2.5, -2.5, 2.5, 2.5, -2.5, -2.5, 2.5, -2.5, 2.5, 2.5, 0.5, 2.5, -1.1703, 0.4033, -2.5, 2.5, 0.5, -2.5, -1.1962999, 0.5, -2.5, -1.1962999, 0.5, -2.5, 2.5, 0.5, -2.5, -1.1851, 0.5, -2.4582999, -1.3363999, 0.5, -1.8935, 2.5, 0.5, -2.5, 0.0046, 0.5, -1.3929, 0.0046, 0.5, -1.3929, 2.5, 0.5, -2.5, 0.21249999, 0.5, -1.2728, 0.21249999, 0.5, -1.2728, 2.5, 0.5, -2.5, 0.21249999, 0.5, -1.0327, -0.2034, 0.5, -1.0327, 0.0046, 0.5, -0.9126, 0.4973, 0.5, 0.21949999, 0.21249999, 0.5, -1.0327, 0.4973, 0.5, 0.21949999, 0.0046, 0.5, -0.9126, 0.21249999, 0.5, -1.0327, 2.5, 0.5, -2.5, 0.4973, 0.5, 0.21949999, 2.5, 0.5, -2.5, 1.2982, 0.5, -0.1122, 0.4973, 0.5, 0.21949999, 2.5, 0.5, -2.5, 2.0992, 0.5, 0.21949999, 1.2982, 0.5, -0.1122, 2.5, 0.5, -2.5, 2.431, 0.5, 1.0205, 2.0992, 0.5, 0.21949999, 0.0046, 0.5, 0.93979996, 0.1655, 0.5, 1.0205, 0.1721, 0.5, 1.0365, 0.21249999, 0.5, 1.1341, 0.4973, 0.5, 1.8215, 0.21249999, 0.5, 1.3, 0.0046, 0.5, 1.4201, 0.21249999, 0.5, 1.3, 0.4973, 0.5, 1.8215, -2.5, 0.5, 2.5, 1.2982, 0.5, 2.1533, 2.5, 0.5, 2.5, 2.5, 0.5, -2.5, 2.5, 0.5, 2.5, 2.431, 0.5, 1.0205, 2.431, 0.5, 1.0205, 2.5, 0.5, 2.5, 2.0992, 0.5, 1.8215, 1.2982, 0.5, 2.1533, 2.0992, 0.5, 1.8215, 2.5, 0.5, 2.5, 0.60539997, 0.235, 0.3277, 0.6161, 0.235, 1.0191, 0.31849998, 0.235, 1.0205, 0.31849998, 0.235, 1.0205, 0.6161, 0.235, 1.0191, 0.81589997, 0.235, 1.5014, 0.60539997, 0.235, 0.3277, 0.1655, 0.5, 1.0205, 0.4973, 0.5, 0.21949999, 0.31849998, 0.235, 1.0205, 0.21249999, 0.4242, 1.0284, 0.1655, 0.5, 1.0205, 0.21249999, 0.4242, 1.0284, 0.1721, 0.5, 1.0365, 0.1655, 0.5, 1.0205, 0.60539997, 0.235, 0.3277, 0.31849998, 0.235, 1.0205, 0.1655, 0.5, 1.0205, 0.60539997, 0.235, 0.3277, 1.2982, 0.235, 0.0407, 0.81589997, 0.235, 0.53679997, 0.81589997, 0.235, 0.53679997, 1.2982, 0.235, 0.0407, 1.2982, 0.235, 0.33699998, 0.60539997, 0.235, 0.3277, 0.81589997, 0.235, 0.53679997, 0.6161, 0.235, 1.0191, 0.60539997, 0.235, 0.3277, 0.4973, 0.5, 0.21949999, 1.2982, 0.235, 0.0407, 1.2982, 0.5, -0.1122, 1.2982, 0.235, 0.0407, 0.4973, 0.5, 0.21949999, 0.31849998, 0.235, 1.0205, 0.81589997, 0.235, 1.5014, 0.60539997, 0.235, 1.7133, 0.21249999, 0.4242, 1.0284, 0.4973, 0.5, 1.8215, 0.21249999, 0.45369998, 1.0695, 0.21249999, 0.45369998, 1.0695, 0.4973, 0.5, 1.8215, 0.21249999, 0.5, 1.1341, 0.31849998, 0.235, 1.0205, 0.4973, 0.5, 1.8215, 0.21249999, 0.4242, 1.0284, 0.31849998, 0.235, 1.0205, 0.60539997, 0.235, 1.7133, 0.4973, 0.5, 1.8215, 0.60539997, 0.235, 1.7133, 0.81589997, 0.235, 1.5014, 1.2982, 0.235, 1.7012, 0.60539997, 0.235, 1.7133, 1.2982, 0.235, 1.7012, 1.2982, 0.235, 2.0003, 0.60539997, 0.235, 1.7133, 1.2982, 0.5, 2.1533, 0.4973, 0.5, 1.8215, 0.60539997, 0.235, 1.7133, 1.2982, 0.235, 2.0003, 1.2982, 0.5, 2.1533, 1.2982, 0.235, 0.0407, 1.9911, 0.235, 0.3277, 1.2982, 0.235, 0.33699998, 1.2982, 0.235, 0.33699998, 1.9911, 0.235, 0.3277, 1.7806, 0.235, 0.53679997, 1.2982, 0.5, -0.1122, 1.9911, 0.235, 0.3277, 1.2982, 0.235, 0.0407, 1.2982, 0.5, -0.1122, 2.0992, 0.5, 0.21949999, 1.9911, 0.235, 0.3277, 1.7806, 0.235, 0.53679997, 1.9911, 0.235, 0.3277, 2.2779999, 0.235, 1.0205, 1.7806, 0.235, 0.53679997, 2.2779999, 0.235, 1.0205, 1.9804, 0.235, 1.0191, 1.9911, 0.235, 0.3277, 2.0992, 0.5, 0.21949999, 2.2779999, 0.235, 1.0205, 2.2779999, 0.235, 1.0205, 2.0992, 0.5, 0.21949999, 2.431, 0.5, 1.0205, 1.2982, 0.235, 1.7012, 1.7806, 0.235, 1.5014, 1.2982, 0.235, 2.0003, 1.7806, 0.235, 1.5014, 1.9911, 0.235, 1.7133, 1.2982, 0.235, 2.0003, 1.2982, 0.235, 2.0003, 1.9911, 0.235, 1.7133, 2.0992, 0.5, 1.8215, 1.2982, 0.235, 2.0003, 2.0992, 0.5, 1.8215, 1.2982, 0.5, 2.1533, 1.9804, 0.235, 1.0191, 1.9911, 0.235, 1.7133, 1.7806, 0.235, 1.5014, 1.9804, 0.235, 1.0191, 2.2779999, 0.235, 1.0205, 1.9911, 0.235, 1.7133, 2.431, 0.5, 1.0205, 2.0992, 0.5, 1.8215, 1.9911, 0.235, 1.7133, 2.2779999, 0.235, 1.0205, 2.431, 0.5, 1.0205, 1.9911, 0.235, 1.7133, 0.81589997, -0.6508, 0.53679997, 0.81589997, 0.235, 0.53679997, 1.2982, -0.6508, 0.33699998, 0.6161, -0.6508, 1.0191, 0.81589997, -0.6508, 0.53679997, 1.2982, -0.6508, 1.0191, 0.6161, -0.6508, 1.0191, 1.2982, -0.6508, 1.0191, 0.81589997, -0.6508, 1.5014, 0.6161, -0.6508, 1.0191, 0.6161, 0.235, 1.0191, 0.81589997, -0.6508, 0.53679997, 0.81589997, -0.6508, 0.53679997, 1.2982, -0.6508, 0.33699998, 1.2982, -0.6508, 1.0191, 0.6161, -0.6508, 1.0191, 0.81589997, -0.6508, 1.5014, 0.81589997, 0.235, 1.5014, 0.81589997, -0.6508, 1.5014, 1.2982, -0.6508, 1.0191, 1.2982, -0.6508, 1.7012, 0.81589997, -0.6508, 1.5014, 1.2982, -0.6508, 1.7012, 1.2982, 0.235, 1.7012, 1.2982, -0.6508, 0.33699998, 1.2982, 0.235, 0.33699998, 1.7806, -0.6508, 0.53679997, 1.2982, -0.6508, 0.33699998, 1.7806, -0.6508, 0.53679997, 1.2982, -0.6508, 1.0191, 1.7806, -0.6508, 0.53679997, 1.9804, -0.6508, 1.0191, 1.2982, -0.6508, 1.0191, 1.2982, -0.6508, 1.0191, 1.9804, -0.6508, 1.0191, 1.7806, -0.6508, 1.5014, 1.7806, -0.6508, 0.53679997, 1.7806, 0.235, 0.53679997, 1.9804, -0.6508, 1.0191, 1.9804, -0.6508, 1.0191, 1.9804, 0.235, 1.0191, 1.7806, -0.6508, 1.5014, 1.2982, -0.6508, 1.0191, 1.7806, -0.6508, 1.5014, 1.2982, -0.6508, 1.7012, 1.2982, -0.6508, 1.7012, 1.7806, -0.6508, 1.5014, 1.7806, 0.235, 1.5014, 1.2982, -0.6508, 0.33699998, 0.81589997, 0.235, 0.53679997, 1.2982, 0.235, 0.33699998, 0.81589997, -0.6508, 0.53679997, 0.6161, 0.235, 1.0191, 0.81589997, 0.235, 0.53679997, 0.6161, -0.6508, 1.0191, 0.81589997, 0.235, 1.5014, 0.6161, 0.235, 1.0191, 0.81589997, -0.6508, 1.5014, 1.2982, 0.235, 1.7012, 0.81589997, 0.235, 1.5014, 1.7806, -0.6508, 0.53679997, 1.2982, 0.235, 0.33699998, 1.7806, 0.235, 0.53679997, 1.9804, -0.6508, 1.0191, 1.7806, 0.235, 0.53679997, 1.9804, 0.235, 1.0191, 1.2982, -0.6508, 1.7012, 1.7806, 0.235, 1.5014, 1.2982, 0.235, 1.7012, 1.7806, -0.6508, 1.5014, 1.9804, 0.235, 1.0191, 1.7806, 0.235, 1.5014, -2.5, -0.69949996, -2.5, -2.3869, -0.7298, -2.5, -2.5, -0.7025, -2.4582999, -2.5, -0.7025, -2.4582999, -2.3146, -0.7521, -2.4582999, -2.5, -0.69479996, -2.3511999, -2.5, -0.7025, -2.4582999, -2.3869, -0.7298, -2.5, -2.3146, -0.7521, -2.4582999, -2.3869, -0.7298, -2.5, -2.3387, -0.7392, -2.5, -2.3146, -0.7521, -2.4582999, -2.5, -0.69479996, -2.3511999, -2.3146, -0.7521, -2.4582999, -2.5, -0.65279996, -2.1371, -2.3146, -0.7521, -2.4582999, -2.3387, -0.7392, -2.5, -2.3146, -0.7409, -2.5, -2.3146, -0.7521, -2.4582999, -2.3146, -0.7409, -2.5, -2.2905, -0.7392, -2.5, -2.3146, -0.7521, -2.4582999, -2.2905, -0.7392, -2.5, -2.2423, -0.7298, -2.5, -2.3146, -0.7521, -2.4582999, -2.3146, -0.5973, -1.8805, -2.5, -0.65279996, -2.1371, -2.5, -0.65279996, -2.1371, -2.3146, -0.5973, -1.8805, -2.5, -0.5973, -1.9302, -2.3146, -0.7521, -2.4582999, -2.0256999, -0.5973, -1.9578999, -2.3146, -0.5973, -1.8805, -2.3146, -0.7521, -2.4582999, -2.2423, -0.7298, -2.5, -1.7369, -0.5973, -2.4582999, -2.3146, -0.7521, -2.4582999, -1.8143, -0.5973, -2.1694, -2.0256999, -0.5973, -1.9578999, -2.3146, -0.7521, -2.4582999, -1.7369, -0.5973, -2.4582999, -1.8143, -0.5973, -2.1694, -2.2423, -0.7298, -2.5, -1.7480999, -0.5973, -2.5, -1.7369, -0.5973, -2.4582999, -2.0256999, -0.5973, -1.9578999, -1.8143, -0.5973, -2.1694, -1.8143, -0.1744, -1.5917, -2.5, -0.5973, -1.9302, -2.3146, -0.5973, -1.8805, -2.5, -0.1744, -1.5073, -2.3146, -0.5973, -1.8805, -2.3146, -0.1744, -1.4576, -2.5, -0.1744, -1.5073, -2.3146, -0.5973, -1.8805, -2.0256999, -0.5973, -1.9578999, -2.3146, -0.1744, -1.4576, -2.0256999, -0.5973, -1.9578999, -1.8143, -0.1744, -1.5917, -2.3146, -0.1744, -1.4576, -1.7480999, -0.5973, -2.5, -1.314, -0.1744, -2.4582999, -1.7369, -0.5973, -2.4582999, -1.7369, -0.5973, -2.4582999, -1.448, -0.1744, -1.9578999, -1.8143, -0.5973, -2.1694, -1.8143, -0.5973, -2.1694, -1.448, -0.1744, -1.9578999, -1.8143, -0.1744, -1.5917, -1.7480999, -0.5973, -2.5, -1.3251, -0.1744, -2.5, -1.314, -0.1744, -2.4582999, -1.7369, -0.5973, -2.4582999, -1.314, -0.1744, -2.4582999, -1.448, -0.1744, -1.9578999, -2.5, -0.1744, -1.5073, -2.3146, -0.1744, -1.4576, -2.5, 0.4033, -1.3525, -2.3146, -0.1744, -1.4576, -2.3146, 0.4033, -1.3027999, -2.5, 0.4033, -1.3525, -2.3146, -0.1744, -1.4576, -1.8143, -0.1744, -1.5917, -2.3146, 0.4033, -1.3027999, -2.3146, 0.4033, -1.3027999, -1.8143, -0.1744, -1.5917, -1.7369, 0.4033, -1.4576, -2.5, 0.4033, -1.3525, -2.3146, 0.4033, -1.3027999, -2.3146, 0.5, -1.3287, -2.5, 0.4033, -1.3525, -2.3146, 0.5, -1.3287, -2.5, 0.5, -1.3784, -2.3146, 0.4033, -1.3027999, -1.7498, 0.5, -1.48, -2.3146, 0.5, -1.3287, -2.3146, 0.4033, -1.3027999, -1.7369, 0.4033, -1.4576, -1.7498, 0.5, -1.48, -1.3251, -0.1744, -2.5, -1.1592, 0.4033, -2.4582999, -1.314, -0.1744, -2.4582999, -1.314, -0.1744, -2.4582999, -1.314, 0.4033, -1.8805, -1.448, -0.1744, -1.9578999, -1.314, -0.1744, -2.4582999, -1.1592, 0.4033, -2.4582999, -1.314, 0.4033, -1.8805, -1.448, -0.1744, -1.9578999, -1.7369, 0.4033, -1.4576, -1.8143, -0.1744, -1.5917, -1.448, -0.1744, -1.9578999, -1.314, 0.4033, -1.8805, -1.7369, 0.4033, -1.4576, -1.314, 0.4033, -1.8805, -1.1851, 0.5, -2.4582999, -1.3363999, 0.5, -1.8935, -1.314, 0.4033, -1.8805, -1.7498, 0.5, -1.48, -1.7369, 0.4033, -1.4576, -1.314, 0.4033, -1.8805, -1.3363999, 0.5, -1.8935, -1.7498, 0.5, -1.48, -1.3251, -0.1744, -2.5, -1.1703, 0.4033, -2.5, -1.1592, 0.4033, -2.4582999, -1.1703, 0.4033, -2.5, -1.1851, 0.5, -2.4582999, -1.1592, 0.4033, -2.4582999, -1.314, 0.4033, -1.8805, -1.1592, 0.4033, -2.4582999, -1.1851, 0.5, -2.4582999, -1.1703, 0.4033, -2.5, -1.1962999, 0.5, -2.5, -1.1851, 0.5, -2.4582999, -0.2034, 0.5, -1.2728, 0.0046, 0.5, -1.3929, -0.2034, 0.5653, -1.2998999, -0.2034, 0.5653, -1.2998999, 0.0046, 0.5, -1.3929, 0.0046, 0.5653, -1.4199, -0.2034, 0.5, -1.2728, -0.2034, 0.5653, -1.0597, -0.2034, 0.5, -1.0327, -0.2034, 0.5, -1.2728, -0.2034, 0.5653, -1.2998999, -0.2034, 0.5653, -1.0597, -0.2034, 0.5, -1.0327, -0.2034, 0.5653, -1.0597, 0.0046, 0.5653, -0.9396, -0.2034, 0.5, -1.0327, 0.0046, 0.5653, -0.9396, 0.0046, 0.5, -0.9126, -0.2034, 0.5653, -1.2998999, -0.2034, 1.3241999, -0.74539995, -0.2034, 0.5653, -1.0597, -0.2034, 0.5653, -1.0597, 0.0046, 1.2393, -0.6604, 0.0046, 0.5653, -0.9396, -0.2034, 0.5653, -1.2998999, -0.2034, 1.494, -0.9152, -0.2034, 1.3241999, -0.74539995, -0.2034, 0.5653, -1.2998999, 0.0046, 0.5653, -1.4199, -0.2034, 1.494, -0.9152, -0.2034, 0.5653, -1.0597, -0.2034, 1.3241999, -0.74539995, 0.0046, 1.2393, -0.6604, -0.2034, 1.494, -0.9152, 0.0046, 0.5653, -1.4199, 0.0046, 1.5789, -1.0001, -0.2034, 1.3241999, -0.74539995, 0.0046, 1.5185, 0.013599999, 0.0046, 1.2393, -0.6604, -0.2034, 1.494, -0.9152, 0.0046, 1.5789, -1.0001, -0.2034, 1.8786999, 0.013599999, -0.2034, 1.3241999, -0.74539995, -0.2034, 1.494, -0.9152, -0.2034, 1.6386, 0.013599999, -0.2034, 1.494, -0.9152, -0.2034, 1.8786999, 0.013599999, -0.2034, 1.6386, 0.013599999, -0.2034, 1.3241999, -0.74539995, -0.2034, 1.6386, 0.013599999, 0.0046, 1.5185, 0.013599999, -0.2034, 1.8786999, 0.013599999, 0.0046, 1.5789, -1.0001, 0.0046, 1.9987999, 0.013599999, -0.2034, 0.5, 1.0598, -0.2034, 0.5653, 1.0869, -0.2034, 0.5, 1.3, -0.2034, 0.5653, 1.0869, -0.2034, 0.5653, 1.327, -0.2034, 0.5, 1.3, -0.2034, 0.5, 1.0598, 0.0046, 0.5653, 0.9668, -0.2034, 0.5653, 1.0869, -0.2034, 0.5, 1.0598, 0.0046, 0.5, 0.93979996, 0.0046, 0.5653, 0.9668, -0.2034, 0.5653, 1.0869, 0.0046, 0.5653, 0.9668, -0.2034, 1.3241999, 0.7725, -0.2034, 0.5, 1.3, -0.2034, 0.5653, 1.327, 0.0046, 0.5, 1.4201, -0.2034, 0.5653, 1.327, 0.0046, 0.5653, 1.4470999, 0.0046, 0.5, 1.4201, -0.2034, 1.3241999, 0.7725, 0.0046, 1.2393, 0.68759996, -0.2034, 1.6386, 0.013599999, -0.2034, 0.5653, 1.0869, -0.2034, 1.3241999, 0.7725, -0.2034, 1.494, 0.94229996, -0.2034, 1.3241999, 0.7725, 0.0046, 0.5653, 0.9668, 0.0046, 1.2393, 0.68759996, -0.2034, 0.5653, 1.0869, -0.2034, 1.494, 0.94229996, -0.2034, 0.5653, 1.327, -0.2034, 0.5653, 1.327, -0.2034, 1.494, 0.94229996, 0.0046, 1.5789, 1.0272, -0.2034, 1.3241999, 0.7725, -0.2034, 1.6386, 0.013599999, -0.2034, 1.8786999, 0.013599999, -0.2034, 1.6386, 0.013599999, 0.0046, 1.2393, 0.68759996, 0.0046, 1.5185, 0.013599999, -0.2034, 1.3241999, 0.7725, -0.2034, 1.8786999, 0.013599999, -0.2034, 1.494, 0.94229996, -0.2034, 1.8786999, 0.013599999, 0.0046, 1.9987999, 0.013599999, -0.2034, 1.494, 0.94229996, -0.2034, 1.494, 0.94229996, 0.0046, 1.9987999, 0.013599999, 0.0046, 1.5789, 1.0272, -0.2034, 0.5653, 1.327, 0.0046, 1.5789, 1.0272, 0.0046, 0.5653, 1.4470999, 0.0046, 0.5653, -1.4199, 0.0046, 0.5, -1.3929, 0.21249999, 0.5653, -1.2998999, 0.0046, 0.5, -1.3929, 0.21249999, 0.5, -1.2728, 0.21249999, 0.5653, -1.2998999, 0.0046, 0.5653, -0.9396, 0.21249999, 0.5, -1.0327, 0.0046, 0.5, -0.9126, 0.0046, 0.5653, -0.9396, 0.21249999, 0.5653, -1.0597, 0.21249999, 0.5, -1.0327, 0.21249999, 0.5, -1.2728, 0.21249999, 0.5, -1.0327, 0.21249999, 0.5653, -1.0597, 0.21249999, 0.5, -1.2728, 0.21249999, 0.5653, -1.0597, 0.21249999, 0.5653, -1.2998999, 0.0046, 0.5653, -0.9396, 0.21249999, 1.3241999, -0.74539995, 0.21249999, 0.5653, -1.0597, 0.0046, 0.5653, -1.4199, 0.21249999, 0.5653, -1.2998999, 0.0046, 1.5789, -1.0001, 0.21249999, 0.5653, -1.2998999, 0.21249999, 0.5653, -1.0597, 0.21249999, 1.494, -0.9152, 0.0046, 0.5653, -0.9396, 0.0046, 1.2393, -0.6604, 0.21249999, 1.3241999, -0.74539995, 0.21249999, 0.5653, -1.0597, 0.21249999, 1.3241999, -0.74539995, 0.21249999, 1.494, -0.9152, 0.21249999, 0.5653, -1.2998999, 0.21249999, 1.494, -0.9152, 0.0046, 1.5789, -1.0001, 0.0046, 1.2393, -0.6604, 0.21249999, 1.6386, 0.013599999, 0.21249999, 1.3241999, -0.74539995, 0.0046, 1.5789, -1.0001, 0.21249999, 1.494, -0.9152, 0.0046, 1.9987999, 0.013599999, 0.21249999, 1.3241999, -0.74539995, 0.21249999, 1.8786999, 0.013599999, 0.21249999, 1.494, -0.9152, 0.0046, 1.2393, -0.6604, 0.0046, 1.5185, 0.013599999, 0.21249999, 1.6386, 0.013599999, 0.21249999, 1.3241999, -0.74539995, 0.21249999, 1.6386, 0.013599999, 0.21249999, 1.8786999, 0.013599999, 0.21249999, 1.494, -0.9152, 0.21249999, 1.8786999, 0.013599999, 0.0046, 1.9987999, 0.013599999, 0.0046, 0.5, 0.93979996, 0.1721, 0.5, 1.0365, 0.0046, 0.5653, 0.9668, 0.0046, 0.5653, 0.9668, 0.1721, 0.5, 1.0365, 0.21249999, 0.5653, 1.0869, 0.21249999, 0.4242, 1.0284, 0.21249999, 0.45369998, 1.0695, 0.21249999, 0.5653, 1.0869, 0.21249999, 0.4242, 1.0284, 0.21249999, 0.5653, 1.0869, 0.1721, 0.5, 1.0365, 0.21249999, 0.45369998, 1.0695, 0.21249999, 0.5, 1.1341, 0.21249999, 0.5653, 1.0869, 0.21249999, 0.5653, 1.0869, 0.21249999, 0.5, 1.1341, 0.21249999, 0.5653, 1.327, 0.0046, 0.5653, 0.9668, 0.21249999, 0.5653, 1.0869, 0.0046, 1.2393, 0.68759996, 0.21249999, 0.5653, 1.0869, 0.21249999, 0.5653, 1.327, 0.21249999, 1.3241999, 0.7725, 0.0046, 0.5, 1.4201, 0.0046, 0.5653, 1.4470999, 0.21249999, 0.5653, 1.327, 0.0046, 0.5, 1.4201, 0.21249999, 0.5653, 1.327, 0.21249999, 0.5, 1.3, 0.21249999, 0.5, 1.1341, 0.21249999, 0.5, 1.3, 0.21249999, 0.5653, 1.327, 0.0046, 1.2393, 0.68759996, 0.21249999, 1.3241999, 0.7725, 0.0046, 1.5185, 0.013599999, 0.21249999, 0.5653, 1.0869, 0.21249999, 1.3241999, 0.7725, 0.0046, 1.2393, 0.68759996, 0.0046, 0.5653, 1.4470999, 0.21249999, 1.494, 0.94229996, 0.21249999, 0.5653, 1.327, 0.21249999, 0.5653, 1.327, 0.21249999, 1.494, 0.94229996, 0.21249999, 1.3241999, 0.7725, 0.0046, 0.5653, 1.4470999, 0.0046, 1.5789, 1.0272, 0.21249999, 1.494, 0.94229996, 0.21249999, 1.3241999, 0.7725, 0.21249999, 1.6386, 0.013599999, 0.0046, 1.5185, 0.013599999, 0.21249999, 1.3241999, 0.7725, 0.21249999, 1.494, 0.94229996, 0.21249999, 1.6386, 0.013599999, 0.21249999, 1.6386, 0.013599999, 0.21249999, 1.494, 0.94229996, 0.21249999, 1.8786999, 0.013599999, 0.0046, 1.9987999, 0.013599999, 0.21249999, 1.8786999, 0.013599999, 0.0046, 1.5789, 1.0272, 0.21249999, 1.8786999, 0.013599999, 0.21249999, 1.494, 0.94229996, 0.0046, 1.5789, 1.0272, -2.2008, -2.5, 0, -2.0029, 0.5, -0.47779998, -2.0029, -2.5, -0.47779998, -2.0029, -2.5, -0.47779998, -1.525, 0.5, -0.67579997, -1.525, -2.5, -0.67579997, -2.2008, -2.5, 0, -2.2008, 0.5, 0, -2.0029, 0.5, -0.47779998, -1.525, 0.5, -0.67579997, -2.0029, -2.5, -0.47779998, -2.0029, 0.5, -0.47779998, -1.0472, 0.5, -0.47779998, -1.525, -2.5, -0.67579997, -1.525, 0.5, -0.67579997, -1.525, -2.5, -0.67579997, -1.0472, 0.5, -0.47779998, -1.0472, -2.5, -0.47779998, -1.0472, -2.5, -0.47779998, -0.84919995, 0.5, 0, -0.84919995, -2.5, 0, -1.0472, -2.5, -0.47779998, -1.0472, 0.5, -0.47779998, -0.84919995, 0.5, 0, -2.0029, -2.5, 0.47779998, -2.2008, 0.5, 0, -2.2008, -2.5, 0, -2.0029, -2.5, 0.47779998, -1.525, -2.5, 0.67579997, -2.0029, 0.5, 0.47779998, -1.525, -2.5, 0.67579997, -1.0472, -2.5, 0.47779998, -1.525, 0.5, 0.67579997, -2.2008, 0.5, 0, -2.0029, -2.5, 0.47779998, -2.0029, 0.5, 0.47779998, -1.525, -2.5, 0.67579997, -1.525, 0.5, 0.67579997, -2.0029, 0.5, 0.47779998, -0.84919995, -2.5, 0, -1.0472, 0.5, 0.47779998, -1.0472, -2.5, 0.47779998, -1.0472, -2.5, 0.47779998, -1.0472, 0.5, 0.47779998, -1.525, 0.5, 0.67579997, -1.0472, 0.5, 0.47779998, -0.84919995, -2.5, 0, -0.84919995, 0.5, 0) + +[node name="Demo" type="Node3D" unique_id=127198201] + +[node name="CSGCombiner3D" type="CSGCombiner3D" parent="." unique_id=627954764] + +[node name="CSGBox3D" type="CSGBox3D" parent="CSGCombiner3D" unique_id=1684500091] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, -1, 0) +size = Vector3(5, 3, 5) + +[node name="CSGTorus3D" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=1718120774] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.298244, 0.51203084, 1.0205324) +operation = 2 +outer_radius = 1.1397109 + +[node name="CSGCylinder3D" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=46812473] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 1.298244, 0.3492006, 1.0190988) +operation = 2 +radius = 0.6821289 + +[node name="CSGSphere3D" type="CSGSphere3D" parent="CSGCombiner3D" unique_id=2113214510] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -2.3146038, 0.40331006, -2.4582562) +operation = 2 +radius = 1.1554451 + +[node name="CSGTorus3D2" type="CSGTorus3D" parent="CSGCombiner3D" unique_id=1178859882] +transform = Transform3D(-4.371139e-08, -1, 0, 1, -4.371139e-08, 0, 0, 0, 1, 0.0045518624, 0.56529653, 0.0135884285) +inner_radius = 0.9532316 +outer_radius = 1.4335196 + +[node name="CSGCylinder3D2" type="CSGCylinder3D" parent="CSGCombiner3D" unique_id=1781335518] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, -1.5250039, -1.8120117, 0) +operation = 2 +radius = 0.67578125 +height = 6.604492 + +[node name="StaticBody3D" type="StaticBody3D" parent="." unique_id=411249296] +transform = Transform3D(1, 0, 0, 0, 1, 0, 0, 0, 1, 0, 0, -7.181916) + +[node name="CSGBakedMeshInstance3D" type="MeshInstance3D" parent="StaticBody3D" unique_id=1990421645] +mesh = SubResource("ArrayMesh_4qltd") + +[node name="CollisionShape3D" type="CollisionShape3D" parent="StaticBody3D" unique_id=163985259] +shape = SubResource("ConcavePolygonShape3D_ghy1h") diff --git a/addons/csg_blockout/scripts/csg_blockout_config.gd b/addons/csg_blockout/scripts/csg_blockout_config.gd new file mode 100644 index 0000000..e1e02c9 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_blockout_config.gd @@ -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) + diff --git a/addons/csg_blockout/scripts/csg_blockout_config.gd.uid b/addons/csg_blockout/scripts/csg_blockout_config.gd.uid new file mode 100644 index 0000000..5fdd12b --- /dev/null +++ b/addons/csg_blockout/scripts/csg_blockout_config.gd.uid @@ -0,0 +1 @@ +uid://w8ad8q4lneis diff --git a/addons/csg_blockout/scripts/csg_blockout_i18n.gd b/addons/csg_blockout/scripts/csg_blockout_i18n.gd new file mode 100644 index 0000000..2132bfb --- /dev/null +++ b/addons/csg_blockout/scripts/csg_blockout_i18n.gd @@ -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) diff --git a/addons/csg_blockout/scripts/csg_blockout_i18n.gd.uid b/addons/csg_blockout/scripts/csg_blockout_i18n.gd.uid new file mode 100644 index 0000000..9ae5f56 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_blockout_i18n.gd.uid @@ -0,0 +1 @@ +uid://bvf7nvtsq1c06 diff --git a/addons/csg_blockout/scripts/csg_pie_menu.gd b/addons/csg_blockout/scripts/csg_pie_menu.gd new file mode 100644 index 0000000..7692f34 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_pie_menu.gd @@ -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() diff --git a/addons/csg_blockout/scripts/csg_pie_menu.gd.uid b/addons/csg_blockout/scripts/csg_pie_menu.gd.uid new file mode 100644 index 0000000..d99e0f7 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_pie_menu.gd.uid @@ -0,0 +1 @@ +uid://brrnjx8pre80s diff --git a/addons/csg_blockout/scripts/csg_repeater_3d.gd b/addons/csg_blockout/scripts/csg_repeater_3d.gd new file mode 100644 index 0000000..df534ff --- /dev/null +++ b/addons/csg_blockout/scripts/csg_repeater_3d.gd @@ -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) diff --git a/addons/csg_blockout/scripts/csg_repeater_3d.gd.uid b/addons/csg_blockout/scripts/csg_repeater_3d.gd.uid new file mode 100644 index 0000000..fee0dfc --- /dev/null +++ b/addons/csg_blockout/scripts/csg_repeater_3d.gd.uid @@ -0,0 +1 @@ +uid://c68dxahp0v5xg diff --git a/addons/csg_blockout/scripts/csg_shortcut_manager.gd b/addons/csg_blockout/scripts/csg_shortcut_manager.gd new file mode 100644 index 0000000..3916bd2 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_shortcut_manager.gd @@ -0,0 +1,3 @@ +@tool +# CSG Shortcut Manager has been removed. Shortcuts disabled. +extends Node diff --git a/addons/csg_blockout/scripts/csg_shortcut_manager.gd.uid b/addons/csg_blockout/scripts/csg_shortcut_manager.gd.uid new file mode 100644 index 0000000..eb12c06 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_shortcut_manager.gd.uid @@ -0,0 +1 @@ +uid://1bqvx7teqnrl diff --git a/addons/csg_blockout/scripts/csg_side_blockout_bar.gd b/addons/csg_blockout/scripts/csg_side_blockout_bar.gd new file mode 100644 index 0000000..46f3a49 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_side_blockout_bar.gd @@ -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") diff --git a/addons/csg_blockout/scripts/csg_side_blockout_bar.gd.uid b/addons/csg_blockout/scripts/csg_side_blockout_bar.gd.uid new file mode 100644 index 0000000..00d6fc8 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_side_blockout_bar.gd.uid @@ -0,0 +1 @@ +uid://dr5f1egll7hdq diff --git a/addons/csg_blockout/scripts/csg_spreader_3d.gd b/addons/csg_blockout/scripts/csg_spreader_3d.gd new file mode 100644 index 0000000..a7dd46b --- /dev/null +++ b/addons/csg_blockout/scripts/csg_spreader_3d.gd @@ -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 + }] diff --git a/addons/csg_blockout/scripts/csg_spreader_3d.gd.uid b/addons/csg_blockout/scripts/csg_spreader_3d.gd.uid new file mode 100644 index 0000000..b82587f --- /dev/null +++ b/addons/csg_blockout/scripts/csg_spreader_3d.gd.uid @@ -0,0 +1 @@ +uid://rgfomqnhbhpk diff --git a/addons/csg_blockout/scripts/csg_top_blockout_bar.gd b/addons/csg_blockout/scripts/csg_top_blockout_bar.gd new file mode 100644 index 0000000..e390387 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_top_blockout_bar.gd @@ -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") diff --git a/addons/csg_blockout/scripts/csg_top_blockout_bar.gd.uid b/addons/csg_blockout/scripts/csg_top_blockout_bar.gd.uid new file mode 100644 index 0000000..6ef2111 --- /dev/null +++ b/addons/csg_blockout/scripts/csg_top_blockout_bar.gd.uid @@ -0,0 +1 @@ +uid://dk6dt8fk1s43s diff --git a/addons/csg_blockout/scripts/patterns/circular_pattern.gd b/addons/csg_blockout/scripts/patterns/circular_pattern.gd new file mode 100644 index 0000000..3a3012b --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/circular_pattern.gd @@ -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) diff --git a/addons/csg_blockout/scripts/patterns/circular_pattern.gd.uid b/addons/csg_blockout/scripts/patterns/circular_pattern.gd.uid new file mode 100644 index 0000000..971810e --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/circular_pattern.gd.uid @@ -0,0 +1 @@ +uid://b3ws8vwtsqmjt diff --git a/addons/csg_blockout/scripts/patterns/csg_pattern.gd b/addons/csg_blockout/scripts/patterns/csg_pattern.gd new file mode 100644 index 0000000..ce85aa9 --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/csg_pattern.gd @@ -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() diff --git a/addons/csg_blockout/scripts/patterns/csg_pattern.gd.uid b/addons/csg_blockout/scripts/patterns/csg_pattern.gd.uid new file mode 100644 index 0000000..d6486e3 --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/csg_pattern.gd.uid @@ -0,0 +1 @@ +uid://dbavf0pl65chb diff --git a/addons/csg_blockout/scripts/patterns/grid_pattern.gd b/addons/csg_blockout/scripts/patterns/grid_pattern.gd new file mode 100644 index 0000000..a1ad3fb --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/grid_pattern.gd @@ -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) diff --git a/addons/csg_blockout/scripts/patterns/grid_pattern.gd.uid b/addons/csg_blockout/scripts/patterns/grid_pattern.gd.uid new file mode 100644 index 0000000..ebbd89f --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/grid_pattern.gd.uid @@ -0,0 +1 @@ +uid://bc1sxg4vy464o diff --git a/addons/csg_blockout/scripts/patterns/noise_pattern.gd b/addons/csg_blockout/scripts/patterns/noise_pattern.gd new file mode 100644 index 0000000..1e34f46 --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/noise_pattern.gd @@ -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) diff --git a/addons/csg_blockout/scripts/patterns/noise_pattern.gd.uid b/addons/csg_blockout/scripts/patterns/noise_pattern.gd.uid new file mode 100644 index 0000000..c8ca953 --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/noise_pattern.gd.uid @@ -0,0 +1 @@ +uid://3il6xs7cr7gj diff --git a/addons/csg_blockout/scripts/patterns/spiral_pattern.gd b/addons/csg_blockout/scripts/patterns/spiral_pattern.gd new file mode 100644 index 0000000..a0d1600 --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/spiral_pattern.gd @@ -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) diff --git a/addons/csg_blockout/scripts/patterns/spiral_pattern.gd.uid b/addons/csg_blockout/scripts/patterns/spiral_pattern.gd.uid new file mode 100644 index 0000000..3c62d79 --- /dev/null +++ b/addons/csg_blockout/scripts/patterns/spiral_pattern.gd.uid @@ -0,0 +1 @@ +uid://belgcjd0ys212