Map-Builder-Export (t_82d2a8dd): Modular-Builder v0.2.0 vendored (addons/modular_builder_en, self-contained) + ScopedVR-Bibliothek-Export im Bauen-Modus. scopedvr_export.gd (Kern, statisch testbar): slugify (Umlaute aufgeloest, fuehrende Ziffern/Unterstriche weg, max. 48 Zeichen), normalisiere (Fusspunkt y=0, XZ zentriert, Material bleibt), trimesh_vertices (Dreiecks-Suppe, Doppel-Dreieck-Dedup + 1-mm-Quantisierung — Achtung: ARRAY_INDEX ist NULL bei nicht-indizierten Oberflaechen, direkter Cast bricht die Funktion ab), glb_bytes (GLTFDocument, Texturen eingebettet, null-Mesh → leere Bytes). scopedvr_export_ui.gd: Name-Dialog → Pipeline (mesh --budget 20000 → registry trimesh --vertices → client → editor) → Thumbnail aus dem ECHTEN Mesh (await, 128 px). editor_actions: „Struktur exportieren" im Dock. project.godot: Plugin enabled. gen_thumbs: Trimesh-Objekte mit vorhandenem Thumbnail ueberspringen. tests/test_builder_export.gd 21/0. --import ohne Parse-/Script-Error (Klassen-Kollision der Root-Duplikate beseitigt — nur modular_builder_en bleibt); Editor-Suite 354 Checks gruen.
19
addons/modular_builder_en/CHANGELOG.md
Normal file
|
|
@ -0,0 +1,19 @@
|
|||
# Changelog
|
||||
|
||||
## 0.2.0
|
||||
|
||||
- Add gable walls, gable/shed/pyramid roofs, stairs, fences, and parapets.
|
||||
- Add plain pillars between vertices or vertically from one vertex with `Alt+click`.
|
||||
- Add manual vertex creation, movement, removal, merging, and simplified marker display.
|
||||
- Add wall/gable/pillar centering controls and editable asymmetric dimensions.
|
||||
- Export a finished structure as one textured mesh in a new scene or as a copy beside the editable structure.
|
||||
- Include the demo scene and its example textures.
|
||||
- Split the editor and geometry code into focused scripts.
|
||||
|
||||
## 0.1.0 — Initial release
|
||||
|
||||
- Create square and triangular foundations, walls, and square or triangular floors directly in the Godot editor.
|
||||
- Snap pieces together using visible vertices and directional foundation placement controls.
|
||||
- Split walls at intermediate foundation vertices for separate texturing.
|
||||
- Add windows, doors, and hatches as openings or separate editable objects.
|
||||
- Generate general concave collision and save reusable building modules.
|
||||
125
addons/modular_builder_en/LICENSE
Normal file
|
|
@ -0,0 +1,125 @@
|
|||
CC0 1.0 Universal
|
||||
|
||||
Copyright (c) 2026 Michel Savadim
|
||||
|
||||
This CC0 dedication applies only to the original Modular Builder source code,
|
||||
icons, and documentation in this directory authored by Michel Savadim. It does
|
||||
not apply to Godot Engine, user-created project content, trademarks, patents,
|
||||
or third-party materials distributed separately.
|
||||
|
||||
CREATIVE COMMONS CORPORATION IS NOT A LAW FIRM AND DOES NOT PROVIDE
|
||||
LEGAL SERVICES. DISTRIBUTION OF THIS DOCUMENT DOES NOT CREATE AN
|
||||
ATTORNEY-CLIENT RELATIONSHIP. CREATIVE COMMONS PROVIDES THIS
|
||||
INFORMATION ON AN "AS-IS" BASIS. CREATIVE COMMONS MAKES NO WARRANTIES
|
||||
REGARDING THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS
|
||||
PROVIDED HEREUNDER, AND DISCLAIMS LIABILITY FOR DAMAGES RESULTING FROM
|
||||
THE USE OF THIS DOCUMENT OR THE INFORMATION OR WORKS PROVIDED
|
||||
HEREUNDER.
|
||||
|
||||
Statement of Purpose
|
||||
|
||||
The laws of most jurisdictions throughout the world automatically confer
|
||||
exclusive Copyright and Related Rights (defined below) upon the creator
|
||||
and subsequent owner(s) (each and all, an "owner") of an original work of
|
||||
authorship and/or a database (each, a "Work").
|
||||
|
||||
Certain owners wish to permanently relinquish those rights to a Work for
|
||||
the purpose of contributing to a commons of creative, cultural and
|
||||
scientific works ("Commons") that the public can reliably and without fear
|
||||
of later claims of infringement build upon, modify, incorporate in other
|
||||
works, reuse and redistribute as freely as possible in any form whatsoever
|
||||
and for any purposes, including without limitation commercial purposes.
|
||||
These owners may contribute to the Commons to promote the ideal of a free
|
||||
culture and the further production of creative, cultural and scientific
|
||||
works, or to gain reputation or greater distribution for their Work in
|
||||
part through the use and efforts of others.
|
||||
|
||||
For these and/or other purposes and motivations, and without any
|
||||
expectation of additional consideration or compensation, the person
|
||||
associating CC0 with a Work (the "Affirmer"), to the extent that he or she
|
||||
is an owner of Copyright and Related Rights in the Work, voluntarily
|
||||
elects to apply CC0 to the Work and publicly distribute the Work under its
|
||||
terms, with knowledge of his or her Copyright and Related Rights in the
|
||||
Work and the meaning and intended legal effect of CC0 on those rights.
|
||||
|
||||
1. Copyright and Related Rights. A Work made available under CC0 may be
|
||||
protected by copyright and related or neighboring rights ("Copyright and
|
||||
Related Rights"). Copyright and Related Rights include, but are not
|
||||
limited to, the following:
|
||||
i. the right to reproduce, adapt, distribute, perform, display,
|
||||
communicate, and translate a Work;
|
||||
ii. moral rights retained by the original author(s) and/or performer(s);
|
||||
iii. publicity and privacy rights pertaining to a person's image or
|
||||
likeness depicted in a Work;
|
||||
iv. rights protecting against unfair competition in regards to a Work,
|
||||
subject to the limitations in paragraph 4(a), below;
|
||||
v. rights protecting the extraction, dissemination, use and reuse of data
|
||||
in a Work;
|
||||
vi. database rights (such as those arising under Directive 96/9/EC of the
|
||||
European Parliament and of the Council of 11 March 1996 on the legal
|
||||
protection of databases, and under any national implementation
|
||||
thereof, including any amended or successor version of such
|
||||
directive); and
|
||||
vii. other similar, equivalent or corresponding rights throughout the
|
||||
world based on applicable law or treaty, and any national
|
||||
implementations thereof.
|
||||
|
||||
2. Waiver. To the greatest extent permitted by, but not in contravention
|
||||
of, applicable law, Affirmer hereby overtly, fully, permanently,
|
||||
irrevocably and unconditionally waives, abandons, and surrenders all of
|
||||
Affirmer's Copyright and Related Rights and associated claims and causes
|
||||
of action, whether now known or unknown (including existing as well as
|
||||
future claims and causes of action), in the Work (i) in all territories
|
||||
worldwide, (ii) for the maximum duration provided by applicable law or
|
||||
treaty (including future time extensions), (iii) in any current or future
|
||||
medium and for any number of copies, and (iv) for any purpose whatsoever,
|
||||
including without limitation commercial, advertising or promotional
|
||||
purposes (the "Waiver"). Affirmer makes the Waiver for the benefit of each
|
||||
member of the public at large and to the detriment of Affirmer's heirs and
|
||||
successors, fully intending that such Waiver shall not be subject to
|
||||
revocation, rescission, cancellation, termination, or any other legal or
|
||||
equitable action to disrupt the quiet enjoyment of the Work by the public
|
||||
as contemplated by Affirmer's express Statement of Purpose.
|
||||
|
||||
3. Public License Fallback. Should any part of the Waiver for any reason
|
||||
be judged legally invalid or ineffective under applicable law, then the
|
||||
Waiver shall be preserved to the maximum extent permitted taking into
|
||||
account Affirmer's express Statement of Purpose. In addition, to the
|
||||
extent the Waiver is so judged Affirmer hereby grants to each affected
|
||||
person a royalty-free, non transferable, non sublicensable, non exclusive,
|
||||
irrevocable and unconditional license to exercise Affirmer's Copyright and
|
||||
Related Rights in the Work (i) in all territories worldwide, (ii) for the
|
||||
maximum duration provided by applicable law or treaty (including future
|
||||
time extensions), (iii) in any current or future medium and for any number
|
||||
of copies, and (iv) for any purpose whatsoever, including without
|
||||
limitation commercial, advertising or promotional purposes (the
|
||||
"License"). The License shall be deemed effective as of the date CC0 was
|
||||
applied by Affirmer to the Work. Should any part of the License for any
|
||||
reason be judged legally invalid or ineffective under applicable law, such
|
||||
partial invalidity or ineffectiveness shall not invalidate the remainder
|
||||
of the License, and in such case Affirmer hereby affirms that he or she
|
||||
will not (i) exercise any of his or her remaining Copyright and Related
|
||||
Rights in the Work or (ii) assert any associated claims and causes of
|
||||
action with respect to the Work, in either case contrary to Affirmer's
|
||||
express Statement of Purpose.
|
||||
|
||||
4. Limitations and Disclaimers.
|
||||
a. No trademark or patent rights held by Affirmer are waived, abandoned,
|
||||
surrendered, licensed or otherwise affected by this document.
|
||||
b. Affirmer offers the Work as-is and makes no representations or
|
||||
warranties of any kind concerning the Work, express, implied,
|
||||
statutory or otherwise, including without limitation warranties of
|
||||
title, merchantability, fitness for a particular purpose, non
|
||||
infringement, or the absence of latent or other defects, accuracy, or
|
||||
the present or absence of errors, whether or not discoverable, all to
|
||||
the greatest extent permissible under applicable law.
|
||||
c. Affirmer disclaims responsibility for clearing rights of other persons
|
||||
that may apply to the Work or any use thereof, including without
|
||||
limitation any person's Copyright and Related Rights in the Work.
|
||||
Further, Affirmer disclaims responsibility for obtaining any necessary
|
||||
consents, permissions or other rights required for any use of the Work.
|
||||
d. Affirmer understands and acknowledges that Creative Commons is not a
|
||||
party to this document and has no duty or obligation with respect to
|
||||
this CC0 or use of the Work.
|
||||
|
||||
https://creativecommons.org/publicdomain/zero/1.0/legalcode
|
||||
37
addons/modular_builder_en/README.md
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
# Modular Builder 0.2.0
|
||||
|
||||
Build modular structures directly in the Godot 4.7 editor. The panel appears at the bottom of the 3D editor. Dimensions are shown in meters; one Godot 3D unit equals one meter.
|
||||
|
||||
## Install and try the demo
|
||||
|
||||
Open this repository as a Godot project to explore `addons/modular_builder_en/demo/Demo.tscn`. After Godot finishes importing the assets, enable **Modular Builder (English)** in **Project > Project Settings > Plugins**. The demo and its five textures are contained in `addons/modular_builder_en/demo/`.
|
||||
|
||||
To use the plugin in an existing project, extract the ZIP at the project root so it creates only `addons/modular_builder_en/`, then enable **Modular Builder (English)** in **Project > Project Settings > Plugins**. Open a 3D scene to use the editor panel. The demo remains inside the addon folder and can be removed if you do not need it.
|
||||
|
||||
## Build
|
||||
|
||||
Click **Add ModularBuilderPiece** to create a structure container and its first foundation. Green tools choose square or triangular foundations; a green `+` on a free edge attaches another foundation. Red tools place walls and triangular gables between vertices. Yellow tools place square floors using opposite diagonal vertices or triangular floors using three vertices. Wall segments crossing several foundations are split at intermediate foundation vertices so each segment can be textured separately.
|
||||
|
||||
Blue markers show connection vertices; selected points turn yellow. Enable **Only show points on selected pieces** to reduce clutter, or **Simplify vertices** to show points at the middle of a piece's thickness. The separate vertex tools merge markers and create, move, or remove manual connection points. Manual vertices belong to their selected piece or detail and do not deform its mesh.
|
||||
|
||||
The blue roof and stairs tools create gable, shed, and pyramid roofs and stairs. Gable roofs use two gable peaks or diagonal top corners of opposite walls. Shed roofs use a low point and a diagonally opposite high point; pyramid roofs use two diagonal points at the same height. The Inspector lets you adjust roof width and rise, stair width, and step count. Walls can be placed on other walls or floors. **Center walls** and **Center** for gables align them with supporting walls.
|
||||
|
||||
Pink detail tools place windows, doors, hatches, pillars, fences, and parapets. Windows, doors, and hatches can be openings alone or openings with separately editable and textured objects. Pillars connect two vertices; `Alt+click` one vertex to create a vertical pillar from it. The **Center** option aligns a pillar with the supporting piece. Fences have two rails; parapets have one thicker rail. Their height and intermediate post count are editable.
|
||||
|
||||
Building pieces start with **Symmetric dimensions** enabled. Turn it off to edit width, height, and depth independently. **Thickness** continues to control the relevant dimension for walls, floors, and foundations. Select a piece in the Scene tree to edit its material and dimensions, or use the rotation button to rotate it by 90 degrees. The active creation tool remains selected until you click it again or choose another tool. `Esc` clears the current points; `Ctrl+Z` undoes placements and vertex edits.
|
||||
|
||||
## Collision, modules, and export
|
||||
|
||||
Pieces have no collision by default. Select the `ModularBuilderPiece` container and use **Add general collision** to generate or update one concave static collision shape for the structure. Connection markers and internal contact faces are excluded.
|
||||
|
||||
Save a selected structure as a reusable `.tscn` module, or insert a saved module into the open scene. For a finished structure, **Export** combines visible meshes into one `MeshInstance3D`, preserving material surfaces and UVs. Choose **New scene** for an independent `.tscn`, or **Copy in this scene** to add a sibling of the editable `ModularBuilderPiece`. The original remains editable.
|
||||
|
||||
## Files and license
|
||||
|
||||
The English plugin lives in `addons/modular_builder_en/`. Editor code is split into UI, scene actions, detail placement, vertex editing, and viewport interaction. Geometry generation, mesh baking, roof/pillar placement, and vertex simplification have separate scripts.
|
||||
|
||||
The original plugin code, icons, and documentation are dedicated to the public domain under [CC0 1.0](LICENSE). The bundled demo textures are example project assets; the CC0 dedication in this repository's license does not automatically cover them. Check their provenance and permission before redistributing the textures separately.
|
||||
|
||||
## AI usage disclosure
|
||||
|
||||
Generative AI (OpenAI ChatGPT/Codex) assisted with GDScript implementation and revisions, debugging, translation from Portuguese to English, and documentation. Michel Savadim defined the requirements and reviewed and tested the project.
|
||||
6
addons/modular_builder_en/icons/add_building.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="m5 17 15-8 15 8-15 8L5 17Z"/><path d="M5 17v16l15 8 15-8V17M20 25v16"/>
|
||||
<path d="M38 13v16m-8-8h16"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
43
addons/modular_builder_en/icons/add_building.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dkuvxakhpo11m"
|
||||
path="res://.godot/imported/add_building.svg-c540099545c836d05aaa84aabb9d42a4.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/add_building.svg"
|
||||
dest_files=["res://.godot/imported/add_building.svg-c540099545c836d05aaa84aabb9d42a4.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
|
||||
3
addons/modular_builder_en/icons/cancel.svg
Normal file
|
|
@ -0,0 +1,3 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<path d="m12 12 24 24m0-24L12 36" fill="none" stroke="#fff" stroke-width="4" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 195 B |
43
addons/modular_builder_en/icons/cancel.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bdfxl3d88c45p"
|
||||
path="res://.godot/imported/cancel.svg-47b87f3e857516f74642fe118e831480.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/cancel.svg"
|
||||
dest_files=["res://.godot/imported/cancel.svg-47b87f3e857516f74642fe118e831480.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
|
||||
6
addons/modular_builder_en/icons/collision.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="m5 16 14-8 14 8-14 8L5 16Z"/><path d="M5 16v17l14 8 14-8V16M19 24v17"/>
|
||||
<path d="M39 23v16m-8-8h16"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 315 B |
43
addons/modular_builder_en/icons/collision.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://blcmsiuegfhy"
|
||||
path="res://.godot/imported/collision.svg-e4cf0320ea5b34c663eb051e13179fb1.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/collision.svg"
|
||||
dest_files=["res://.godot/imported/collision.svg-e4cf0320ea5b34c663eb051e13179fb1.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
|
||||
4
addons/modular_builder_en/icons/create_vertex.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<circle cx="32" cy="32" r="19" fill="#9b7bd1" stroke="#222" stroke-width="4"/>
|
||||
<path d="M32 20v24M20 32h24" stroke="#fff" stroke-width="6" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 259 B |
43
addons/modular_builder_en/icons/create_vertex.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d4iv2qc3e6k8f"
|
||||
path="res://.godot/imported/create_vertex.svg-b23af865d7a800b0e078b26b2e99a9f7.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/create_vertex.svg"
|
||||
dest_files=["res://.godot/imported/create_vertex.svg-b23af865d7a800b0e078b26b2e99a9f7.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/modular_builder_en/icons/door.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"><path d="M12 54V10h40v44"/><path d="M19 54V17l26-4v41"/></g><circle cx="39" cy="35" r="2.8" fill="#fff"/></svg>
|
||||
|
After Width: | Height: | Size: 265 B |
43
addons/modular_builder_en/icons/door.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://shrvgd57wqmy"
|
||||
path="res://.godot/imported/door.svg-abe4c0258238339b0ce092292a14a613.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/door.svg"
|
||||
dest_files=["res://.godot/imported/door.svg-abe4c0258238339b0ce092292a14a613.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
|
||||
5
addons/modular_builder_en/icons/fence.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M9 24h46M9 42h46" fill="none" stroke="#eee" stroke-width="6" stroke-linecap="round"/>
|
||||
<path d="M15 13v39M32 13v39M49 13v39" fill="none" stroke="#eee" stroke-width="7" stroke-linecap="round"/>
|
||||
<path d="M15 13v39M32 13v39M49 13v39" fill="none" stroke="#222" stroke-width="2" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 404 B |
43
addons/modular_builder_en/icons/fence.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bmvaiklprw3ob"
|
||||
path="res://.godot/imported/fence.svg-4eb777531b4c56b60ad89e9e327e738c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/fence.svg"
|
||||
dest_files=["res://.godot/imported/fence.svg-4eb777531b4c56b60ad89e9e327e738c.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
|
||||
6
addons/modular_builder_en/icons/floor_square.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="m8 18 16-9 16 9-16 9-16-9Z"/>
|
||||
<path d="M8 18v5l16 9 16-9v-5M24 27v5" opacity=".82"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 298 B |
43
addons/modular_builder_en/icons/floor_square.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://b11j8aumv12dr"
|
||||
path="res://.godot/imported/floor_square.svg-929ec0d7e4700eccd8871b9e7bdc53c8.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/floor_square.svg"
|
||||
dest_files=["res://.godot/imported/floor_square.svg-929ec0d7e4700eccd8871b9e7bdc53c8.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
|
||||
6
addons/modular_builder_en/icons/floor_triangle.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="m24 9 18 29H6L24 9Z"/>
|
||||
<path d="M6 38v3l18 4 18-4v-3M24 38v7" opacity=".82"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 291 B |
43
addons/modular_builder_en/icons/floor_triangle.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://blm8vvelfa0ov"
|
||||
path="res://.godot/imported/floor_triangle.svg-dccf4703c638fbb44617d82da92f981f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/floor_triangle.svg"
|
||||
dest_files=["res://.godot/imported/floor_triangle.svg-dccf4703c638fbb44617d82da92f981f.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
|
||||
6
addons/modular_builder_en/icons/foundation_square.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M8 16 24 8l16 8-16 8-16-8Z"/>
|
||||
<path d="M8 16v17l16 8 16-8V16M24 24v17"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 286 B |
43
addons/modular_builder_en/icons/foundation_square.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bdg64bvathtkp"
|
||||
path="res://.godot/imported/foundation_square.svg-dc8be81ca55b382cac6eed9723c4ea7c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/foundation_square.svg"
|
||||
dest_files=["res://.godot/imported/foundation_square.svg-dc8be81ca55b382cac6eed9723c4ea7c.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
|
||||
6
addons/modular_builder_en/icons/foundation_triangle.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M24 8 42 38H6L24 8Z"/>
|
||||
<path d="m24 16 10 17H14l10-17Z" opacity=".72"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 285 B |
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c7sts0wrdier2"
|
||||
path="res://.godot/imported/foundation_triangle.svg-c2747d1fa5be8206a98f02f2c53db66f.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/foundation_triangle.svg"
|
||||
dest_files=["res://.godot/imported/foundation_triangle.svg-c2747d1fa5be8206a98f02f2c53db66f.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
|
||||
4
addons/modular_builder_en/icons/gable_wall.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<path d="M8 54V29L32 9l24 20v25H8Z" fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"/>
|
||||
<path d="M8 29h48M32 9v45" fill="none" stroke="#fff" stroke-width="3" opacity=".7"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 262 B |
43
addons/modular_builder_en/icons/gable_wall.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://420a0ay7eep0"
|
||||
path="res://.godot/imported/gable_wall.svg-c2d60f6ba4255837e31c628621486d99.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/gable_wall.svg"
|
||||
dest_files=["res://.godot/imported/gable_wall.svg-c2d60f6ba4255837e31c628621486d99.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
|
||||
4
addons/modular_builder_en/icons/gable_wall_right.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M11 51V13l42 38z" fill="#eeeeee" stroke="#222222" stroke-width="5" stroke-linejoin="round"/>
|
||||
<circle cx="11" cy="51" r="4" fill="#c62828"/><circle cx="11" cy="13" r="4" fill="#c62828"/><circle cx="53" cy="51" r="4" fill="#c62828"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 336 B |
43
addons/modular_builder_en/icons/gable_wall_right.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cbap2sd7o8fwg"
|
||||
path="res://.godot/imported/gable_wall_right.svg-ad644706ef5497d44f89d17f68ec357c.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/gable_wall_right.svg"
|
||||
dest_files=["res://.godot/imported/gable_wall_right.svg-ad644706ef5497d44f89d17f68ec357c.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/modular_builder_en/icons/hatch.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"><path d="M8 20 32 9l24 11-24 12z"/><path d="M8 20v26l24 11 24-11V20M32 32v25"/><path d="m32 32 13-6v14l-13 6z"/></g></svg>
|
||||
|
After Width: | Height: | Size: 276 B |
43
addons/modular_builder_en/icons/hatch.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bhqb8ky2bus1f"
|
||||
path="res://.godot/imported/hatch.svg-0413a819fca720a8bc1e64dc6ad76826.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/hatch.svg"
|
||||
dest_files=["res://.godot/imported/hatch.svg-0413a819fca720a8bc1e64dc6ad76826.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/modular_builder_en/icons/load_module.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round" d="M10 8h38l8 8v40H10zM19 8v17h25V8M19 56V35h26v21"/><path fill="none" stroke="#fff" stroke-width="4" d="M32 51V40m-6 5 6-6 6 6"/></svg>
|
||||
|
After Width: | Height: | Size: 293 B |
43
addons/modular_builder_en/icons/load_module.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://yesw2vj8yaah"
|
||||
path="res://.godot/imported/load_module.svg-63e2b3e2ac8c24bc991fcf4e75ace01b.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/load_module.svg"
|
||||
dest_files=["res://.godot/imported/load_module.svg-63e2b3e2ac8c24bc991fcf4e75ace01b.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
|
||||
4
addons/modular_builder_en/icons/merge_vertices.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M15 15L32 32 49 15M32 32v17" fill="none" stroke="#eeeeee" stroke-width="6" stroke-linecap="round" stroke-linejoin="round"/>
|
||||
<circle cx="15" cy="15" r="7" fill="#9b7bd1" stroke="#222" stroke-width="3"/><circle cx="49" cy="15" r="7" fill="#9b7bd1" stroke="#222" stroke-width="3"/><circle cx="32" cy="49" r="7" fill="#9b7bd1" stroke="#222" stroke-width="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 460 B |
43
addons/modular_builder_en/icons/merge_vertices.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://1amy53ump51l"
|
||||
path="res://.godot/imported/merge_vertices.svg-5ece4f02ab55d6738ab8836b942201e1.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/merge_vertices.svg"
|
||||
dest_files=["res://.godot/imported/merge_vertices.svg-5ece4f02ab55d6738ab8836b942201e1.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
|
||||
4
addons/modular_builder_en/icons/move_vertex.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M32 6l-8 9h5v12h6V15h5zM58 32l-9-8v5H37v6h12v5zM32 58l8-9h-5V37h-6v12h-5zM6 32l9 8v-5h12v-6H15v-5z" fill="#eee" stroke="#222" stroke-width="2" stroke-linejoin="round"/>
|
||||
<circle cx="32" cy="32" r="8" fill="#9b7bd1" stroke="#222" stroke-width="3"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 351 B |
43
addons/modular_builder_en/icons/move_vertex.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://cfh7b0kxdwl6i"
|
||||
path="res://.godot/imported/move_vertex.svg-ceb416f7c4a6671d49b72b5d7058c829.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/move_vertex.svg"
|
||||
dest_files=["res://.godot/imported/move_vertex.svg-ceb416f7c4a6671d49b72b5d7058c829.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
|
||||
6
addons/modular_builder_en/icons/parapet.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<g fill="none" stroke="#ffffff" stroke-linecap="round" stroke-linejoin="round" stroke-width="6">
|
||||
<path d="M12 50V14M52 50V14M12 25H52"/>
|
||||
<path d="M8 50H16M48 50H56"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 275 B |
43
addons/modular_builder_en/icons/parapet.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dl55t56w8agod"
|
||||
path="res://.godot/imported/parapet.svg-ed0a5acaa465eefbbe527d0d92ac41e6.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/parapet.svg"
|
||||
dest_files=["res://.godot/imported/parapet.svg-ed0a5acaa465eefbbe527d0d92ac41e6.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
|
||||
6
addons/modular_builder_en/icons/pillar.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round">
|
||||
<path d="M15 8h18v32H15z"/>
|
||||
<path d="M12 8h18l3 3M12 8v32l3 0M12 40h18l3-3"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
43
addons/modular_builder_en/icons/pillar.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bsvap0ukvvtxl"
|
||||
path="res://.godot/imported/pillar.svg-0acb51ea7c090f4767757e19389c7697.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/pillar.svg"
|
||||
dest_files=["res://.godot/imported/pillar.svg-0acb51ea7c090f4767757e19389c7697.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
|
||||
4
addons/modular_builder_en/icons/remove_vertex.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<circle cx="32" cy="32" r="21" fill="#9b7bd1" stroke="#222" stroke-width="4"/>
|
||||
<path d="M20 32h24" stroke="#fff" stroke-width="7" stroke-linecap="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 250 B |
43
addons/modular_builder_en/icons/remove_vertex.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://d3smkmuij3tiu"
|
||||
path="res://.godot/imported/remove_vertex.svg-a2650c4a5b88e73778470ad394dd8d24.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/remove_vertex.svg"
|
||||
dest_files=["res://.godot/imported/remove_vertex.svg-a2650c4a5b88e73778470ad394dd8d24.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
|
||||
4
addons/modular_builder_en/icons/roof_gable.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<path d="M5 30 32 8l27 22-5 6L32 19 10 36l-5-6Z" fill="#fff"/>
|
||||
<path d="M14 35v17h36V35L32 21 14 35Z" fill="none" stroke="#fff" stroke-width="4" stroke-linejoin="round" opacity=".75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 257 B |
43
addons/modular_builder_en/icons/roof_gable.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://7mg7tjvygg82"
|
||||
path="res://.godot/imported/roof_gable.svg-19c7e85e7be7616ba9781f8a0b3ca1c3.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/roof_gable.svg"
|
||||
dest_files=["res://.godot/imported/roof_gable.svg-19c7e85e7be7616ba9781f8a0b3ca1c3.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
|
||||
4
addons/modular_builder_en/icons/roof_pyramid.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M8 45L32 13 56 45 32 53z" fill="#4d83ae" stroke="#222" stroke-width="4" stroke-linejoin="round"/>
|
||||
<path d="M32 13v40M8 45l24 8 24-8" fill="none" stroke="#eee" stroke-width="3" stroke-linejoin="round"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 306 B |
43
addons/modular_builder_en/icons/roof_pyramid.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://55uqhamplshs"
|
||||
path="res://.godot/imported/roof_pyramid.svg-9e1f692744fe6843f12da4ad40b1e7fd.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/roof_pyramid.svg"
|
||||
dest_files=["res://.godot/imported/roof_pyramid.svg-9e1f692744fe6843f12da4ad40b1e7fd.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
|
||||
4
addons/modular_builder_en/icons/roof_shed.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
||||
<path d="m8 22 48-12v9L8 32v-10Z" fill="#fff"/>
|
||||
<path d="M13 34v18h38V24L13 34Z" fill="none" stroke="#fff" stroke-width="4" stroke-linejoin="round" opacity=".75"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 236 B |
43
addons/modular_builder_en/icons/roof_shed.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dl0xf605xhwdl"
|
||||
path="res://.godot/imported/roof_shed.svg-6a66b1c82d6d6e005d37c43a5b9009df.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/roof_shed.svg"
|
||||
dest_files=["res://.godot/imported/roof_shed.svg-6a66b1c82d6d6e005d37c43a5b9009df.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
|
||||
6
addons/modular_builder_en/icons/rotate.svg
Normal file
|
|
@ -0,0 +1,6 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="3" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M37 20A15 15 0 0 0 11 14L7 19"/><path d="M7 10v9h9"/>
|
||||
<path d="M11 28a15 15 0 0 0 26 6l4-5"/><path d="M41 38v-9h-9"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 329 B |
43
addons/modular_builder_en/icons/rotate.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://dhi55x4bo0dmp"
|
||||
path="res://.godot/imported/rotate.svg-2f2c1519ec865048747f49f7cbeed3c8.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/rotate.svg"
|
||||
dest_files=["res://.godot/imported/rotate.svg-2f2c1519ec865048747f49f7cbeed3c8.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/modular_builder_en/icons/save_module.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><path fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round" d="M10 8h38l8 8v40H10zM19 8v17h25V8M19 56V35h26v21"/><path fill="none" stroke="#fff" stroke-width="4" d="M32 40v11m-6-6 6 6 6-6"/></svg>
|
||||
|
After Width: | Height: | Size: 293 B |
43
addons/modular_builder_en/icons/save_module.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://di37e5sjuktph"
|
||||
path="res://.godot/imported/save_module.svg-de34a0b11c2e62ba100617a23de20ad2.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/save_module.svg"
|
||||
dest_files=["res://.godot/imported/save_module.svg-de34a0b11c2e62ba100617a23de20ad2.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
|
||||
4
addons/modular_builder_en/icons/stairs.svg
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64">
|
||||
<path d="M8 52h12V40h12V28h12V16h12" fill="none" stroke="#eee" stroke-width="7" stroke-linejoin="round" stroke-linecap="square"/>
|
||||
<path d="M8 52h12V40h12V28h12V16h12" fill="none" stroke="#222" stroke-width="2" stroke-linejoin="round" stroke-linecap="square"/>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 355 B |
43
addons/modular_builder_en/icons/stairs.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://bl85bimtdaffm"
|
||||
path="res://.godot/imported/stairs.svg-8a83477aca2bfaf1ecd92ddcb3d7ad52.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/stairs.svg"
|
||||
dest_files=["res://.godot/imported/stairs.svg-8a83477aca2bfaf1ecd92ddcb3d7ad52.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
|
||||
5
addons/modular_builder_en/icons/wall.svg
Normal file
|
|
@ -0,0 +1,5 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="48" height="48" viewBox="0 0 48 48">
|
||||
<g fill="none" stroke="#fff" stroke-width="2.8" stroke-linejoin="round" stroke-linecap="round">
|
||||
<path d="M8 10h32v28H8zM8 20h32M8 29h32M19 10v10m12 0v9M19 29v9"/>
|
||||
</g>
|
||||
</svg>
|
||||
|
After Width: | Height: | Size: 267 B |
43
addons/modular_builder_en/icons/wall.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://c61wb4mnf0wls"
|
||||
path="res://.godot/imported/wall.svg-25deeb903c389412115a2f457cdde263.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/wall.svg"
|
||||
dest_files=["res://.godot/imported/wall.svg-25deeb903c389412115a2f457cdde263.ctex"]
|
||||
|
||||
[params]
|
||||
|
||||
compress/mode=0
|
||||
compress/high_quality=false
|
||||
compress/lossy_quality=0.7
|
||||
compress/uastc_level=0
|
||||
compress/rdo_quality_loss=0.0
|
||||
compress/hdr_compression=1
|
||||
compress/normal_map=0
|
||||
compress/channel_pack=0
|
||||
mipmaps/generate=false
|
||||
mipmaps/limit=-1
|
||||
roughness/mode=0
|
||||
roughness/src_normal=""
|
||||
process/channel_remap/red=0
|
||||
process/channel_remap/green=1
|
||||
process/channel_remap/blue=2
|
||||
process/channel_remap/alpha=3
|
||||
process/fix_alpha_border=true
|
||||
process/premult_alpha=false
|
||||
process/normal_map_invert_y=false
|
||||
process/hdr_as_srgb=false
|
||||
process/hdr_clamp_exposure=false
|
||||
process/size_limit=0
|
||||
detect_3d/compress_to=1
|
||||
svg/scale=1.0
|
||||
editor/scale_with_editor_scale=false
|
||||
editor/convert_colors_with_editor_theme=false
|
||||
1
addons/modular_builder_en/icons/window.svg
Normal file
|
|
@ -0,0 +1 @@
|
|||
<svg xmlns="http://www.w3.org/2000/svg" width="64" height="64" viewBox="0 0 64 64"><g fill="none" stroke="#fff" stroke-width="5" stroke-linejoin="round"><path d="M10 12h44v40H10z"/><path d="M32 12v40M10 32h44"/></g></svg>
|
||||
|
After Width: | Height: | Size: 222 B |
43
addons/modular_builder_en/icons/window.svg.import
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
[remap]
|
||||
|
||||
importer="texture"
|
||||
type="CompressedTexture2D"
|
||||
uid="uid://np7qscb7nf3s"
|
||||
path="res://.godot/imported/window.svg-ac8dc7e4787d27b79bd94a1bd7b1f8f8.ctex"
|
||||
metadata={
|
||||
"vram_texture": false
|
||||
}
|
||||
|
||||
[deps]
|
||||
|
||||
source_file="res://addons/modular_builder_en/icons/window.svg"
|
||||
dest_files=["res://.godot/imported/window.svg-ac8dc7e4787d27b79bd94a1bd7b1f8f8.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
|
||||
614
addons/modular_builder_en/modular_builder_editor_actions.gd
Normal file
|
|
@ -0,0 +1,614 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder_en/modular_builder_editor_ui.gd"
|
||||
|
||||
## ScopedVR-Bibliothek-Export (Karte t_82d2a8dd): dritter Export-Modus
|
||||
## „In ScopedVR-Bibliothek" — gebackenes Mesh + Trimesh-Kollision durch die
|
||||
## Objekt-Pipeline (tools/objekt im Match-Server-Repo), Name-Dialog inklusive.
|
||||
const SCOPEDVR_EXPORT_UI := preload("res://addons/modular_builder_en/scopedvr_export_ui.gd")
|
||||
var _scopedvr_export: RefCounted = null
|
||||
|
||||
func _add_piece() -> void:
|
||||
var root := get_editor_interface().get_edited_scene_root()
|
||||
if root == null or not root is Node3D:
|
||||
status_label.text = "Open a 3D scene before adding a piece."
|
||||
return
|
||||
var piece := Node3D.new()
|
||||
piece.name = "ModularBuilderPiece"
|
||||
piece.set_script(PIECE_SCRIPT)
|
||||
piece.set("piece_type", ModularBuilderPiece.PieceType.BUILDING)
|
||||
var module_size_units := _meters_to_godot_units(size_spin.value)
|
||||
piece.set("unit_size", module_size_units)
|
||||
var foundation := Node3D.new()
|
||||
foundation.name = "Foundation"
|
||||
foundation.set_script(PIECE_SCRIPT)
|
||||
var chosen_type := _foundation_type_selected
|
||||
if chosen_type == 1:
|
||||
foundation.set("piece_type", ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE)
|
||||
else:
|
||||
foundation.set("piece_type", ModularBuilderPiece.PieceType.FOUNDATION_SQUARE)
|
||||
foundation.set("unit_size", module_size_units)
|
||||
foundation.set("centered_origin", true)
|
||||
foundation.position.y = foundation.thickness * 0.5
|
||||
piece.add_child(foundation, true)
|
||||
foundation.owner = root
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add ModularBuilderPiece")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", root)
|
||||
undo.add_do_property(foundation, "owner", root)
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.add_do_method(get_editor_interface().get_selection(), "clear")
|
||||
undo.add_do_method(get_editor_interface().get_selection(), "add_node", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = foundation
|
||||
_active_button_key = TOOL_FOUNDATION_TRIANGLE if _foundation_type_selected == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE else TOOL_FOUNDATION_SQUARE
|
||||
_set_tool(TOOL_FOUNDATION)
|
||||
|
||||
func _meters_to_godot_units(meters: float) -> float:
|
||||
return meters * METERS_TO_GODOT_UNITS
|
||||
|
||||
func _show_save_module_dialog() -> void:
|
||||
var building := _get_selected_piece()
|
||||
if building == null:
|
||||
status_label.text = "Select a ModularBuilderPiece structure to save as a module."
|
||||
return
|
||||
_save_module_dialog.current_file = "%s.tscn" % building.name
|
||||
_save_module_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _show_load_module_dialog() -> void:
|
||||
_load_module_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _on_export_pressed() -> void:
|
||||
var building := _get_selected_piece()
|
||||
if building == null:
|
||||
status_label.text = "Select a ModularBuilderPiece to export."
|
||||
return
|
||||
var baked_mesh := _bake_building_mesh(building)
|
||||
if baked_mesh == null or baked_mesh.get_surface_count() == 0:
|
||||
status_label.text = "No visible meshes were found to export."
|
||||
return
|
||||
if _export_mode_option.get_selected_id() == EXPORT_LIBRARY:
|
||||
_export_to_library(building, baked_mesh)
|
||||
return
|
||||
if _export_mode_option.get_selected_id() == EXPORT_SCENE_COPY:
|
||||
_add_baked_copy(building, baked_mesh)
|
||||
return
|
||||
_pending_export_mesh = baked_mesh
|
||||
_pending_export_name = "%s_Baked" % building.name
|
||||
_pending_export_basis = building.global_transform.basis
|
||||
_export_scene_dialog.current_file = "%s.tscn" % _pending_export_name
|
||||
_export_scene_dialog.popup_centered_ratio(0.7)
|
||||
|
||||
func _bake_building_mesh(building: ModularBuilderPiece) -> ArrayMesh:
|
||||
return MESH_BAKER.new().bake(building)
|
||||
|
||||
## Export-Modus EXPORT_LIBRARY (Karte t_82d2a8dd): Name-Dialog öffnen, danach
|
||||
## läuft die Objekt-Pipeline (scopedvr_export_ui.gd); der Status landet im
|
||||
## Dock-Statusfeld. Die Instanz bleibt gespeichert, damit der Dialog nicht
|
||||
## vom Garbage Collector gefressen wird, während er offen ist.
|
||||
func _export_to_library(building: ModularBuilderPiece, baked_mesh: ArrayMesh) -> void:
|
||||
_scopedvr_export = SCOPEDVR_EXPORT_UI.new()
|
||||
_scopedvr_export.starten(self, building, baked_mesh, func(text: String) -> void:
|
||||
status_label.text = str(text))
|
||||
|
||||
func _add_baked_copy(building: ModularBuilderPiece, mesh: ArrayMesh) -> void:
|
||||
var edited_scene_root := get_editor_interface().get_edited_scene_root()
|
||||
if edited_scene_root == null or building.get_parent() == null:
|
||||
status_label.text = "Open a scene before adding the exported copy."
|
||||
return
|
||||
var sibling_parent := building.get_parent()
|
||||
var baked_root := _make_baked_scene_root(building.name + "_Baked", mesh)
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Export structure as a mesh copy")
|
||||
undo.add_do_method(self, "_attach_baked_copy", sibling_parent, edited_scene_root, baked_root, building.global_transform)
|
||||
undo.add_undo_method(self, "_detach_baked_copy", sibling_parent, baked_root)
|
||||
undo.commit_action()
|
||||
status_label.text = "Copy exported as one mesh with %d material surface(s). Ctrl+Z removes the copy." % mesh.get_surface_count()
|
||||
|
||||
func _attach_baked_copy(sibling_parent: Node, edited_scene_root: Node, baked_root: Node3D, target_transform: Transform3D) -> void:
|
||||
if baked_root.get_parent() != sibling_parent:
|
||||
if baked_root.get_parent() != null:
|
||||
baked_root.get_parent().remove_child(baked_root)
|
||||
sibling_parent.add_child(baked_root, true)
|
||||
baked_root.global_transform = target_transform
|
||||
baked_root.owner = edited_scene_root
|
||||
_set_export_owner_recursive(baked_root, edited_scene_root)
|
||||
var editor_selection := get_editor_interface().get_selection()
|
||||
editor_selection.clear()
|
||||
editor_selection.add_node(baked_root)
|
||||
|
||||
func _detach_baked_copy(sibling_parent: Node, baked_root: Node3D) -> void:
|
||||
if baked_root.get_parent() == sibling_parent:
|
||||
sibling_parent.remove_child(baked_root)
|
||||
|
||||
func _make_baked_scene_root(root_name: String, mesh: ArrayMesh) -> Node3D:
|
||||
var baked_root := Node3D.new()
|
||||
baked_root.name = root_name
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = "BakedMesh"
|
||||
mesh_instance.mesh = mesh
|
||||
baked_root.add_child(mesh_instance)
|
||||
return baked_root
|
||||
|
||||
func _export_baked_scene_to_path(path: String) -> void:
|
||||
if _pending_export_mesh == null:
|
||||
status_label.text = "The exported mesh is no longer available. Bake it again."
|
||||
return
|
||||
var baked_root := _make_baked_scene_root(_pending_export_name, _pending_export_mesh)
|
||||
baked_root.transform = Transform3D(_pending_export_basis, Vector3.ZERO)
|
||||
_set_export_owner_recursive(baked_root, baked_root)
|
||||
var packed := PackedScene.new()
|
||||
var pack_error := packed.pack(baked_root)
|
||||
baked_root.free()
|
||||
if pack_error != OK:
|
||||
status_label.text = "Could not pack the exported mesh (%s)." % error_string(pack_error)
|
||||
return
|
||||
var save_error := ResourceSaver.save(packed, path)
|
||||
if save_error != OK:
|
||||
status_label.text = "Could not save the exported scene (%s)." % error_string(save_error)
|
||||
return
|
||||
get_editor_interface().get_resource_filesystem().scan()
|
||||
status_label.text = "Exported scene: %s (%d material surface(s))." % [path, _pending_export_mesh.get_surface_count()]
|
||||
_pending_export_mesh = null
|
||||
|
||||
func _set_export_owner_recursive(node: Node, scene_owner: Node) -> void:
|
||||
for child in node.get_children():
|
||||
child.owner = scene_owner
|
||||
_set_export_owner_recursive(child, scene_owner)
|
||||
|
||||
func _save_module_to_path(path: String) -> void:
|
||||
var building := _get_selected_piece()
|
||||
if building == null:
|
||||
status_label.text = "Select a ModularBuilderPiece structure to save as a module."
|
||||
return
|
||||
var descendants: Array[Node] = []
|
||||
_collect_module_nodes(building, descendants)
|
||||
var original_owners: Dictionary = {}
|
||||
for node in descendants:
|
||||
original_owners[node] = node.owner
|
||||
node.owner = building
|
||||
var packed := PackedScene.new()
|
||||
var pack_error := packed.pack(building)
|
||||
for node in descendants:
|
||||
node.owner = original_owners[node]
|
||||
if pack_error != OK:
|
||||
status_label.text = "Could not pack the structure as a scene (%s)." % error_string(pack_error)
|
||||
return
|
||||
var save_error := ResourceSaver.save(packed, path)
|
||||
if save_error != OK:
|
||||
status_label.text = "Could not save the module (%s)." % error_string(save_error)
|
||||
return
|
||||
status_label.text = "Module saved: %s" % path
|
||||
|
||||
func _collect_module_nodes(node: Node, result: Array[Node]) -> void:
|
||||
for child in node.get_children():
|
||||
if child.name == "ModularBuilderMarkers" or child.has_meta("modular_builder_preview"):
|
||||
continue
|
||||
result.append(child)
|
||||
_collect_module_nodes(child, result)
|
||||
|
||||
func _load_module_from_path(path: String) -> void:
|
||||
var packed := ResourceLoader.load(path) as PackedScene
|
||||
if packed == null:
|
||||
status_label.text = "The selected file is not a valid Godot scene."
|
||||
return
|
||||
var instance := packed.instantiate()
|
||||
if not instance is Node3D:
|
||||
instance.free()
|
||||
status_label.text = "The module scene must have a Node3D root."
|
||||
return
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
if scene_root == null:
|
||||
instance.free()
|
||||
status_label.text = "Open a scene before inserting a module."
|
||||
return
|
||||
var anchor := _get_selected_piece()
|
||||
if anchor != null:
|
||||
var target_world := anchor.global_position + anchor.global_transform.basis.x * maxf(anchor.unit_size, 1.0)
|
||||
instance.position = scene_root.to_local(target_world)
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Insert module %s" % instance.name)
|
||||
undo.add_do_method(self, "_attach_module", scene_root, instance)
|
||||
undo.add_undo_method(self, "_detach_module", scene_root, instance)
|
||||
undo.commit_action()
|
||||
status_label.text = "Module inserted. Move the piece to the desired location."
|
||||
|
||||
func _attach_module(scene_root: Node, instance: Node) -> void:
|
||||
if instance.get_parent() != scene_root:
|
||||
scene_root.add_child(instance, true)
|
||||
instance.owner = scene_root
|
||||
var editor_selection := get_editor_interface().get_selection()
|
||||
editor_selection.clear()
|
||||
editor_selection.add_node(instance)
|
||||
|
||||
func _detach_module(scene_root: Node, instance: Node) -> void:
|
||||
if instance.get_parent() == scene_root:
|
||||
scene_root.remove_child(instance)
|
||||
|
||||
func _rotate_selected_piece() -> void:
|
||||
var selected_piece := _raw_selected_piece()
|
||||
var target := selected_piece
|
||||
if selected_piece == null:
|
||||
target = _last_created_piece if is_instance_valid(_last_created_piece) and _last_created_piece.is_inside_tree() else null
|
||||
elif selected_piece.piece_type == ModularBuilderPiece.PieceType.BUILDING:
|
||||
target = null
|
||||
if is_instance_valid(_last_created_piece) and _last_created_piece.is_inside_tree() and _is_descendant_of(_last_created_piece, selected_piece):
|
||||
target = _last_created_piece
|
||||
if target == null:
|
||||
status_label.text = "Select or create a piece to rotate."
|
||||
return
|
||||
var old_rotation := target.rotation_degrees
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Rotate piece 90 degrees")
|
||||
undo.add_do_property(target, "rotation_degrees", old_rotation + Vector3(0.0, 90.0, 0.0))
|
||||
undo.add_undo_property(target, "rotation_degrees", old_rotation)
|
||||
undo.commit_action()
|
||||
_last_created_piece = target
|
||||
status_label.text = "%s rotated by 90°." % target.name
|
||||
update_overlays()
|
||||
|
||||
func _is_descendant_of(node: Node, ancestor: Node) -> bool:
|
||||
var current := node.get_parent()
|
||||
while current != null:
|
||||
if current == ancestor:
|
||||
return true
|
||||
current = current.get_parent()
|
||||
return false
|
||||
|
||||
func _raw_selected_piece() -> ModularBuilderPiece:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.size() == 1 and selected[0] is ModularBuilderPiece:
|
||||
return selected[0] as ModularBuilderPiece
|
||||
return null
|
||||
|
||||
func _add_general_collision() -> void:
|
||||
var piece := _get_selected_piece()
|
||||
if piece == null:
|
||||
status_label.text = "Select a structure to generate general collision."
|
||||
return
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
var faces := PackedVector3Array()
|
||||
var structure_pieces: Array[ModularBuilderPiece] = []
|
||||
_collect_structure_pieces(piece, structure_pieces)
|
||||
_collect_scene_pieces(scene_root, structure_pieces)
|
||||
_collect_collision_faces(piece, piece, faces, {}, structure_pieces)
|
||||
if faces.is_empty():
|
||||
status_label.text = "The selected structure has no geometry for collision."
|
||||
return
|
||||
var concave := ConcavePolygonShape3D.new()
|
||||
concave.set_faces(faces)
|
||||
var body := piece.find_child("GeneralCollision", true, false) as StaticBody3D
|
||||
var undo := get_undo_redo()
|
||||
if body != null:
|
||||
var shape_node := body.get_node_or_null("Shape") as CollisionShape3D
|
||||
if shape_node == null:
|
||||
shape_node = CollisionShape3D.new()
|
||||
shape_node.name = "Shape"
|
||||
shape_node.shape = concave
|
||||
undo.create_action("Add shape to general collision")
|
||||
undo.add_do_method(body, "add_child", shape_node, true)
|
||||
undo.add_do_property(shape_node, "owner", scene_root)
|
||||
undo.add_undo_method(body, "remove_child", shape_node)
|
||||
else:
|
||||
var old_shape: Shape3D = shape_node.shape
|
||||
undo.create_action("Update general collision")
|
||||
undo.add_do_property(shape_node, "shape", concave)
|
||||
undo.add_undo_property(shape_node, "shape", old_shape)
|
||||
else:
|
||||
body = StaticBody3D.new()
|
||||
body.name = "GeneralCollision"
|
||||
var shape_node := CollisionShape3D.new()
|
||||
shape_node.name = "Shape"
|
||||
shape_node.shape = concave
|
||||
body.add_child(shape_node)
|
||||
undo.create_action("Add general collision")
|
||||
undo.add_do_method(piece, "add_child", body, true)
|
||||
undo.add_do_property(body, "owner", scene_root)
|
||||
undo.add_do_property(shape_node, "owner", scene_root)
|
||||
undo.add_undo_method(piece, "remove_child", body)
|
||||
undo.commit_action()
|
||||
status_label.text = "General collision added or updated. Snap markers are excluded."
|
||||
|
||||
func _collect_structure_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece:
|
||||
pieces.append(node)
|
||||
for child in node.get_children():
|
||||
if child.name == "ModularBuilderMarkers" or child.name == "GeneralCollision":
|
||||
continue
|
||||
_collect_structure_pieces(child, pieces)
|
||||
|
||||
func _collect_scene_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and not pieces.has(node):
|
||||
pieces.append(node)
|
||||
for child in node.get_children():
|
||||
if child.name == "ModularBuilderMarkers" or child.name == "GeneralCollision":
|
||||
continue
|
||||
_collect_scene_pieces(child, pieces)
|
||||
|
||||
func _collect_collision_faces(node: Node, piece_root: ModularBuilderPiece, faces: PackedVector3Array, seen_faces: Dictionary, structure_pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is MeshInstance3D and node.name == "Geometry" and node.mesh != null:
|
||||
var relative_transform: Transform3D = piece_root.global_transform.affine_inverse() * node.global_transform
|
||||
var mesh_faces: PackedVector3Array = node.mesh.get_faces()
|
||||
var owner_piece := node.get_parent() as ModularBuilderPiece
|
||||
for i in range(0, mesh_faces.size(), 3):
|
||||
if i + 2 >= mesh_faces.size():
|
||||
break
|
||||
var local_a: Vector3 = mesh_faces[i]
|
||||
var local_b: Vector3 = mesh_faces[i + 1]
|
||||
var local_c: Vector3 = mesh_faces[i + 2]
|
||||
if _is_connection_face(owner_piece, local_a, local_b, local_c, structure_pieces):
|
||||
continue
|
||||
# Procedural meshes have both sides. Deduplication may retain
|
||||
# either orientation; normalize top faces beforehand to
|
||||
# keep custom foundations navigable.
|
||||
if _is_piece_top_face(owner_piece, local_a, local_b, local_c):
|
||||
if (local_b - local_a).cross(local_c - local_a).y < 0.0:
|
||||
var swap := local_b
|
||||
local_b = local_c
|
||||
local_c = swap
|
||||
var a: Vector3 = relative_transform * local_a
|
||||
var b: Vector3 = relative_transform * local_b
|
||||
var c: Vector3 = relative_transform * local_c
|
||||
var key_parts := [_collision_vertex_key(a), _collision_vertex_key(b), _collision_vertex_key(c)]
|
||||
key_parts.sort()
|
||||
var face_key := "%s|%s|%s" % key_parts
|
||||
if seen_faces.has(face_key):
|
||||
continue
|
||||
seen_faces[face_key] = true
|
||||
faces.append_array(PackedVector3Array([a, b, c]))
|
||||
for child in node.get_children():
|
||||
if child.name == "ModularBuilderMarkers" or child.name == "GeneralCollision":
|
||||
continue
|
||||
_collect_collision_faces(child, piece_root, faces, seen_faces, structure_pieces)
|
||||
|
||||
func _is_piece_top_face(piece: ModularBuilderPiece, a: Vector3, b: Vector3, c: Vector3) -> bool:
|
||||
if piece == null:
|
||||
return false
|
||||
if piece.piece_type != ModularBuilderPiece.PieceType.FLOOR and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
return false
|
||||
if absf(a.y - b.y) > 0.002 or absf(a.y - c.y) > 0.002:
|
||||
return false
|
||||
var top_y := piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR and piece.custom_points.size() >= 3:
|
||||
top_y = piece.custom_points[0].y
|
||||
return absf(a.y - top_y) < 0.002 and absf(b.y - top_y) < 0.002 and absf(c.y - top_y) < 0.002
|
||||
|
||||
func _is_connection_face(piece: ModularBuilderPiece, a: Vector3, b: Vector3, c: Vector3, structure_pieces: Array[ModularBuilderPiece]) -> bool:
|
||||
if piece == null:
|
||||
return false
|
||||
if (piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE) and _foundation_edge_is_shared(piece, a, b, c, structure_pieces):
|
||||
return true
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR and piece.custom_points.size() >= 3:
|
||||
var bottom_point := piece.custom_points[0] - Vector3.UP * piece.thickness
|
||||
return _triangle_on_plane(a, b, c, bottom_point, Vector3.UP)
|
||||
if piece.piece_type != ModularBuilderPiece.PieceType.WALL or piece.custom_points.size() < 2:
|
||||
return false
|
||||
var start: Vector3 = piece.custom_points[0]
|
||||
var finish: Vector3 = piece.custom_points[1]
|
||||
var direction := (finish - start).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * piece.thickness * 0.5
|
||||
var base_normal := (finish - start).cross(side).normalized()
|
||||
if _triangle_on_plane(a, b, c, start, base_normal):
|
||||
return true
|
||||
if _wall_has_floor_contact(piece, structure_pieces):
|
||||
if _triangle_on_plane(a, b, c, start + Vector3.UP * piece.unit_size, base_normal):
|
||||
return true
|
||||
for other in structure_pieces:
|
||||
if other == piece or other.piece_type != ModularBuilderPiece.PieceType.WALL or other.custom_points.size() < 2:
|
||||
continue
|
||||
for endpoint in piece.custom_points:
|
||||
var endpoint_world := piece.to_global(endpoint)
|
||||
for other_endpoint in other.custom_points:
|
||||
if endpoint_world.distance_to(other.to_global(other_endpoint)) < 0.02:
|
||||
var local_endpoint := piece.to_local(endpoint_world)
|
||||
if _triangle_on_plane(a, b, c, local_endpoint, direction):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _wall_has_floor_contact(wall: ModularBuilderPiece, structure_pieces: Array[ModularBuilderPiece]) -> bool:
|
||||
var endpoints: PackedVector3Array = wall.custom_points
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
var top := endpoint + Vector3.UP * wall.unit_size
|
||||
for wall_corner in [wall.to_global(top + side), wall.to_global(top - side)]:
|
||||
for other in structure_pieces:
|
||||
if other.piece_type != ModularBuilderPiece.PieceType.FLOOR or other.custom_points.size() < 3:
|
||||
continue
|
||||
for floor_point in other.custom_points:
|
||||
if wall_corner.distance_to(other.to_global(floor_point)) < 0.02:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _foundation_edge_is_shared(piece: ModularBuilderPiece, a: Vector3, b: Vector3, c: Vector3, structure_pieces: Array[ModularBuilderPiece]) -> bool:
|
||||
var polygon := _foundation_polygon(piece)
|
||||
for other in structure_pieces:
|
||||
if other == piece or (other.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and other.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE):
|
||||
continue
|
||||
var other_polygon := _foundation_polygon(other)
|
||||
for i in range(polygon.size()):
|
||||
var p0: Vector2 = polygon[i]
|
||||
var p1: Vector2 = polygon[(i + 1) % polygon.size()]
|
||||
for j in range(other_polygon.size()):
|
||||
var q0: Vector2 = other_polygon[j]
|
||||
var q1: Vector2 = other_polygon[(j + 1) % other_polygon.size()]
|
||||
var other_base_y := -other.thickness * 0.5 if other.centered_origin else 0.0
|
||||
var q0_local := piece.to_local(other.to_global(Vector3(q0.x, other_base_y, q0.y)))
|
||||
var q1_local := piece.to_local(other.to_global(Vector3(q1.x, other_base_y, q1.y)))
|
||||
var shared := _segments_overlap(p0, p1, Vector2(q0_local.x, q0_local.z), Vector2(q1_local.x, q1_local.z))
|
||||
var piece_base_y := -piece.thickness * 0.5 if piece.centered_origin else 0.0
|
||||
if shared and absf(q0_local.y - piece_base_y) < 0.02 and absf(q1_local.y - piece_base_y) < 0.02 and _triangle_on_edge(a, b, c, p0, p1):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _triangle_on_edge(a: Vector3, b: Vector3, c: Vector3, edge_a: Vector2, edge_b: Vector2) -> bool:
|
||||
return _point_on_edge(Vector2(a.x, a.z), edge_a, edge_b) and _point_on_edge(Vector2(b.x, b.z), edge_a, edge_b) and _point_on_edge(Vector2(c.x, c.z), edge_a, edge_b)
|
||||
|
||||
func _point_on_edge(point: Vector2, edge_a: Vector2, edge_b: Vector2) -> bool:
|
||||
var edge := edge_b - edge_a
|
||||
var length_squared := edge.length_squared()
|
||||
if length_squared < 0.000001:
|
||||
return false
|
||||
var t := (point - edge_a).dot(edge) / length_squared
|
||||
var closest := edge_a + edge * clampf(t, 0.0, 1.0)
|
||||
return t >= -0.002 and t <= 1.002 and point.distance_to(closest) < 0.002
|
||||
|
||||
func _segments_overlap(a: Vector2, b: Vector2, c: Vector2, d: Vector2) -> bool:
|
||||
var direction := b - a
|
||||
var length := direction.length()
|
||||
if length < 0.0001:
|
||||
return false
|
||||
var unit := direction / length
|
||||
if absf(unit.x * (c - a).y - unit.y * (c - a).x) > 0.02 or absf(unit.x * (d - a).y - unit.y * (d - a).x) > 0.02:
|
||||
return false
|
||||
var c_proj := (c - a).dot(unit)
|
||||
var d_proj := (d - a).dot(unit)
|
||||
var overlap := minf(length, maxf(c_proj, d_proj)) - maxf(0.0, minf(c_proj, d_proj))
|
||||
return overlap > 0.02
|
||||
|
||||
func _triangle_on_plane(a: Vector3, b: Vector3, c: Vector3, plane_point: Vector3, plane_normal: Vector3) -> bool:
|
||||
if plane_normal.length_squared() < 0.000001:
|
||||
return false
|
||||
var normal := plane_normal.normalized()
|
||||
return absf((a - plane_point).dot(normal)) < 0.002 and absf((b - plane_point).dot(normal)) < 0.002 and absf((c - plane_point).dot(normal)) < 0.002
|
||||
|
||||
func _collision_vertex_key(point: Vector3) -> String:
|
||||
return "%.4f,%.4f,%.4f" % [point.x, point.y, point.z]
|
||||
|
||||
func _set_tool(tool: String) -> void:
|
||||
active_tool = tool
|
||||
_detail_options_row.visible = _is_detail_tool(tool)
|
||||
_rise_options_row.visible = tool == TOOL_WALL or tool == TOOL_GABLE_WALL or tool == TOOL_GABLE_WALL_RIGHT or tool == TOOL_ROOF_GABLE or tool == TOOL_ROOF_PYRAMID
|
||||
_linear_options_row.visible = tool == TOOL_STAIRS or tool == TOOL_FENCE
|
||||
_stair_count_label.visible = tool == TOOL_STAIRS
|
||||
stair_count_spin.visible = tool == TOOL_STAIRS
|
||||
_stair_width_label.visible = tool == TOOL_STAIRS
|
||||
stair_width_spin.visible = tool == TOOL_STAIRS
|
||||
_fence_divisions_label.visible = tool == TOOL_FENCE
|
||||
fence_divisions_spin.visible = tool == TOOL_FENCE
|
||||
_gable_centralize_check.visible = tool == TOOL_WALL or tool == TOOL_GABLE_WALL or tool == TOOL_GABLE_WALL_RIGHT
|
||||
_rise_label.text = "Peak rise:"
|
||||
if not _is_detail_tool(tool):
|
||||
call("_clear_detail_preview")
|
||||
if tool == TOOL_NONE:
|
||||
_active_button_key = ""
|
||||
pending_points.clear()
|
||||
for button_tool in _tool_buttons:
|
||||
_tool_buttons[button_tool].set_pressed_no_signal(button_tool == _active_button_key)
|
||||
if tool == TOOL_NONE:
|
||||
status_label.text = "Tool deactivated."
|
||||
elif _get_selected_piece() == null:
|
||||
status_label.text = "Select the corresponding piece in the Scene tree."
|
||||
elif tool == TOOL_FOUNDATION:
|
||||
status_label.text = "Click a + button beside any foundation. Click the tool again to deactivate it."
|
||||
elif tool == TOOL_WINDOW or tool == TOOL_DOOR:
|
||||
status_label.text = "Click a wall face to insert the detail. Edit its mode and dimensions in the Inspector."
|
||||
elif tool == TOOL_HATCH:
|
||||
status_label.text = "Click a foundation or floor face to insert the hatch. Edit it in the Inspector."
|
||||
elif tool == TOOL_GABLE_WALL:
|
||||
status_label.text = "Click the two base vertices of the gable. Adjust peak height below."
|
||||
elif tool == TOOL_GABLE_WALL_RIGHT:
|
||||
status_label.text = "Click the base vertices; the second point sets the high side of the right triangular gable."
|
||||
elif tool == TOOL_WALL:
|
||||
status_label.text = "Click two vertices on foundations, floors, or wall tops. Enable Center walls to align the endpoints with supporting walls."
|
||||
elif tool == TOOL_ROOF_GABLE:
|
||||
status_label.text = "Click the peaks of two gables or diagonal corners atop opposite walls."
|
||||
elif tool == TOOL_ROOF_SHED:
|
||||
status_label.text = "Click a low point, then a diagonally opposite high point. The roof finds the other corners; adjust width in the Inspector."
|
||||
elif tool == TOOL_ROOF_PYRAMID:
|
||||
status_label.text = "Click two diagonal vertices at the same height. Adjust pyramid peak height below or in the Inspector."
|
||||
elif tool == TOOL_STAIRS:
|
||||
status_label.text = "Click the low point, then the high point to create stairs. Adjust steps and width below."
|
||||
elif tool == TOOL_FENCE or tool == TOOL_PARAPET:
|
||||
status_label.text = "Click two vertices to set the path. Adjust height and intermediate posts below."
|
||||
elif tool == TOOL_MERGE_VERTICES:
|
||||
status_label.text = "Click two vertices to merge their markers. Ctrl+Z undoes the merge."
|
||||
elif tool == TOOL_CREATE_VERTEX:
|
||||
status_label.text = "Click a surface to create a connection point linked to the selected piece or detail."
|
||||
elif tool == TOOL_MOVE_VERTEX:
|
||||
status_label.text = "Click a blue manual point, then the surface where you want to move it."
|
||||
elif tool == TOOL_REMOVE_VERTEX:
|
||||
status_label.text = "Click a blue manual point to remove it."
|
||||
elif tool == TOOL_PILLAR:
|
||||
status_label.text = "Click two vertices to connect them. Alt+click one vertex to create a vertical pillar with the panel length."
|
||||
else:
|
||||
status_label.text = "Click the blue structure vertices. Click the tool again to deactivate it."
|
||||
call("_sync_vertex_markers")
|
||||
update_overlays()
|
||||
|
||||
func _get_selected_piece() -> ModularBuilderPiece:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.is_empty():
|
||||
return null
|
||||
var common_root: ModularBuilderPiece
|
||||
for selected_node in selected:
|
||||
var selected_piece := selected_node as ModularBuilderPiece
|
||||
if selected_piece == null and selected_node is ModularBuilderDetail:
|
||||
var parent := selected_node.get_parent()
|
||||
while parent != null and not parent is ModularBuilderPiece:
|
||||
parent = parent.get_parent()
|
||||
selected_piece = parent as ModularBuilderPiece
|
||||
if selected_piece == null:
|
||||
return null
|
||||
var piece_root := selected_piece
|
||||
var ancestor := selected_piece.get_parent()
|
||||
while ancestor != null:
|
||||
if ancestor is ModularBuilderPiece and ancestor.piece_type == ModularBuilderPiece.PieceType.BUILDING:
|
||||
piece_root = ancestor
|
||||
break
|
||||
ancestor = ancestor.get_parent()
|
||||
if common_root == null:
|
||||
common_root = piece_root
|
||||
elif common_root != piece_root:
|
||||
return null
|
||||
return common_root
|
||||
|
||||
func _on_editor_selection_changed() -> void:
|
||||
if _redirecting_selection:
|
||||
return
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
var redirected_selection: Array[Node] = []
|
||||
var needs_redirect := false
|
||||
for selected_node in selected:
|
||||
var target_node: Node = selected_node
|
||||
if selected_node is MeshInstance3D:
|
||||
var ancestor := selected_node.get_parent()
|
||||
while ancestor != null and not (ancestor is ModularBuilderPiece or ancestor is ModularBuilderDetail):
|
||||
ancestor = ancestor.get_parent()
|
||||
if ancestor != null:
|
||||
target_node = ancestor
|
||||
needs_redirect = true
|
||||
if not redirected_selection.has(target_node):
|
||||
redirected_selection.append(target_node)
|
||||
if needs_redirect:
|
||||
_redirecting_selection = true
|
||||
var editor_selection := get_editor_interface().get_selection()
|
||||
editor_selection.clear()
|
||||
for target_node in redirected_selection:
|
||||
editor_selection.add_node(target_node)
|
||||
_redirecting_selection = false
|
||||
if active_tool != TOOL_NONE:
|
||||
pending_points.clear()
|
||||
_sync_pillar_centralize_control()
|
||||
call("_sync_vertex_markers")
|
||||
update_overlays()
|
||||
|
||||
func _on_marker_scope_toggled(_enabled: bool) -> void:
|
||||
call("_sync_vertex_markers")
|
||||
update_overlays()
|
||||
|
||||
func _on_simplify_vertices_toggled(_enabled: bool) -> void:
|
||||
pending_points.clear()
|
||||
call("_sync_vertex_markers")
|
||||
update_overlays()
|
||||
|
||||
func _foundation_polygon(root: ModularBuilderPiece) -> Array[Vector2]:
|
||||
if root.custom_points.size() >= 3:
|
||||
var custom: Array[Vector2] = []
|
||||
for point in root.custom_points:
|
||||
custom.append(Vector2(point.x, point.z))
|
||||
return custom
|
||||
var half := root.unit_size * 0.5
|
||||
if root.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
return [Vector2(-half, -half), Vector2(half, -half), Vector2(-half, half)]
|
||||
return [Vector2(-half, -half), Vector2(half, -half), Vector2(half, half), Vector2(-half, half)]
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://bem5uer4jw8ik
|
||||
430
addons/modular_builder_en/modular_builder_editor_details.gd
Normal file
|
|
@ -0,0 +1,430 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder_en/modular_builder_editor_actions.gd"
|
||||
|
||||
const PILLAR_SUPPORT = preload("res://addons/modular_builder_en/modular_pillar_support.gd")
|
||||
const VERTEX_SIMPLIFIER = preload("res://addons/modular_builder_en/modular_vertex_simplifier.gd")
|
||||
|
||||
func _create_detail_from_click(camera: Camera3D, screen_position: Vector2) -> bool:
|
||||
var placement := _find_detail_placement(camera, screen_position)
|
||||
if placement.is_empty():
|
||||
return false
|
||||
var detail := _new_detail(placement, false)
|
||||
var best_piece := placement.host as ModularBuilderPiece
|
||||
if best_piece == null:
|
||||
return false
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add %s" % detail.name)
|
||||
undo.add_do_method(self, "_attach_detail", best_piece, detail, get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(self, "_detach_detail", best_piece, detail)
|
||||
undo.commit_action()
|
||||
get_editor_interface().get_selection().clear()
|
||||
get_editor_interface().get_selection().add_node(detail)
|
||||
_set_tool(TOOL_NONE)
|
||||
status_label.text = "%s created. Edit style, materials, and dimensions in the Inspector." % detail.name
|
||||
return true
|
||||
|
||||
func _create_pillar_from_vertices(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> void:
|
||||
var placement := _find_pillar_placement(root, first, second)
|
||||
if placement.is_empty():
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two different structure vertices to connect with the pillar."
|
||||
return
|
||||
_commit_pillar_placement(placement, "Plain pillar created between vertices. Edit its section and material in the Inspector.")
|
||||
|
||||
func _create_vertical_pillar_from_vertex(root: ModularBuilderPiece, anchor: Vector3) -> void:
|
||||
pending_points.clear()
|
||||
var length := maxf(0.2, detail_height_spin.value * METERS_TO_GODOT_UNITS)
|
||||
var direction := _vertical_pillar_direction(root, anchor)
|
||||
var placement := _find_pillar_placement(root, anchor, anchor + direction * length)
|
||||
if placement.is_empty():
|
||||
status_label.text = "Could not place a vertical pillar at this vertex."
|
||||
return
|
||||
_commit_pillar_placement(placement, "Vertical pillar created from the vertex. Edit its length in the Inspector.")
|
||||
|
||||
func _commit_pillar_placement(placement: Dictionary, message: String) -> void:
|
||||
var detail := _new_detail(placement, false)
|
||||
var host := placement.host as ModularBuilderPiece
|
||||
detail.set("height", float(placement.length))
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add plain pillar")
|
||||
undo.add_do_method(self, "_attach_detail", host, detail, get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(self, "_detach_detail", host, detail)
|
||||
undo.commit_action()
|
||||
get_editor_interface().get_selection().clear()
|
||||
get_editor_interface().get_selection().add_node(detail)
|
||||
_set_tool(TOOL_NONE)
|
||||
status_label.text = message
|
||||
|
||||
func _vertical_pillar_direction(root: ModularBuilderPiece, anchor: Vector3) -> Vector3:
|
||||
var selected_pillar := _get_selected_pillar_detail()
|
||||
if selected_pillar != null:
|
||||
var pillar_local := selected_pillar.to_local(anchor)
|
||||
if absf(pillar_local.x) <= selected_pillar.width * 0.5 + 0.05 and absf(pillar_local.z) <= selected_pillar.width * 0.5 + 0.05 and absf(absf(pillar_local.y) - selected_pillar.height * 0.5) <= 0.05:
|
||||
return PILLAR_SUPPORT.vertical_direction_for_pillar(selected_pillar, anchor)
|
||||
var source := _get_pillar_vertex_source(root, anchor)
|
||||
if source != null:
|
||||
return PILLAR_SUPPORT.vertical_direction_for_piece(source, anchor)
|
||||
var pillar := _find_pillar_at_vertex(root, anchor)
|
||||
return PILLAR_SUPPORT.vertical_direction_for_pillar(pillar, anchor) if pillar != null else Vector3.UP
|
||||
|
||||
func _find_pillar_at_vertex(node: Node, anchor: Vector3) -> ModularBuilderDetail:
|
||||
if node is ModularBuilderDetail and node.detail_type == ModularBuilderDetail.DetailType.PILLAR:
|
||||
var detail := node as ModularBuilderDetail
|
||||
var local := detail.to_local(anchor)
|
||||
var half := detail.width * 0.5
|
||||
var at_corner := absf(absf(local.x) - half) <= 0.05 and absf(absf(local.z) - half) <= 0.05
|
||||
var at_center := _simplify_vertices_check != null and _simplify_vertices_check.button_pressed and absf(local.x) <= 0.05 and absf(local.z) <= 0.05
|
||||
if (at_corner or at_center) and absf(absf(local.y) - detail.height * 0.5) <= 0.05:
|
||||
return detail
|
||||
for child in node.get_children():
|
||||
var pillar := _find_pillar_at_vertex(child, anchor)
|
||||
if pillar != null:
|
||||
return pillar
|
||||
return null
|
||||
|
||||
func _create_fence_decoration(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> void:
|
||||
if first.distance_to(second) < 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two vertices far enough apart."
|
||||
return
|
||||
var is_parapet := active_tool == TOOL_PARAPET
|
||||
var detail := Node3D.new()
|
||||
detail.set_script(DETAIL_SCRIPT)
|
||||
detail.name = "Parapet" if is_parapet else "Fence"
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.PARAPET if is_parapet else ModularBuilderDetail.DetailType.FENCE)
|
||||
detail.set("detail_mode", ModularBuilderDetail.DetailMode.SEPARATE_OBJECT)
|
||||
detail.set("path_points", PackedVector3Array([root.to_local(first), root.to_local(second)]))
|
||||
detail.set("fence_height", fence_height_spin.value * METERS_TO_GODOT_UNITS)
|
||||
detail.set("fence_divisions", int(fence_divisions_spin.value))
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add parapet" if is_parapet else "Add decorative fence")
|
||||
undo.add_do_method(self, "_attach_detail", root, detail, get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(self, "_detach_detail", root, detail)
|
||||
undo.commit_action()
|
||||
get_editor_interface().get_selection().clear()
|
||||
get_editor_interface().get_selection().add_node(detail)
|
||||
_set_tool(TOOL_NONE)
|
||||
pending_points.clear()
|
||||
status_label.text = "%s created with %d intermediate post(s)." % [detail.name, detail.fence_divisions]
|
||||
|
||||
func _find_pillar_placement(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> Dictionary:
|
||||
if first.distance_to(second) < 0.02:
|
||||
return {}
|
||||
var local_first := root.to_local(first)
|
||||
var local_second := root.to_local(second)
|
||||
var segment := local_second - local_first
|
||||
var segment_length := segment.length()
|
||||
if segment_length < 0.02:
|
||||
return {}
|
||||
var y_axis := segment / segment_length
|
||||
var reference_axis := Vector3.FORWARD if absf(y_axis.dot(Vector3.UP)) > 0.99 else Vector3.UP
|
||||
var x_axis := reference_axis.cross(y_axis).normalized()
|
||||
var z_axis := x_axis.cross(y_axis).normalized()
|
||||
var basis := Basis(x_axis, y_axis, z_axis).orthonormalized()
|
||||
var source := _get_pillar_vertex_source(root, first)
|
||||
var alignment_basis := Basis.IDENTITY
|
||||
if source != null:
|
||||
alignment_basis = (root.global_transform.basis.inverse() * source.global_transform.basis).orthonormalized()
|
||||
return {
|
||||
"host": root,
|
||||
"position": (local_first + local_second) * 0.5,
|
||||
"rotation": Vector3.ZERO,
|
||||
"basis": basis,
|
||||
"direction": segment,
|
||||
"alignment_basis": alignment_basis,
|
||||
"length": segment_length
|
||||
}
|
||||
|
||||
func _get_pillar_vertex_source(root: ModularBuilderPiece, point: Vector3) -> ModularBuilderPiece:
|
||||
var best_piece: ModularBuilderPiece
|
||||
var best_distance := 0.05
|
||||
var pieces: Array[ModularBuilderPiece] = []
|
||||
call("_collect_all_modular_pieces", root, pieces)
|
||||
for piece in pieces:
|
||||
if piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE and piece.piece_type != ModularBuilderPiece.PieceType.FLOOR and piece.piece_type != ModularBuilderPiece.PieceType.WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
continue
|
||||
var vertices: Array[Vector3] = []
|
||||
call("_collect_piece_geometry_vertices", piece, vertices)
|
||||
for vertex in vertices:
|
||||
var distance := vertex.distance_to(point)
|
||||
if _simplify_vertices_check != null and _simplify_vertices_check.button_pressed:
|
||||
distance = minf(distance, VERTEX_SIMPLIFIER.piece_vertex(piece, vertex).distance_to(point))
|
||||
if distance < best_distance:
|
||||
best_distance = distance
|
||||
best_piece = piece
|
||||
return best_piece
|
||||
|
||||
func _point_inside_surface_or_edge(piece: ModularBuilderPiece, point: Vector2, edge_tolerance: float) -> bool:
|
||||
if _point_inside_surface(piece, point):
|
||||
return true
|
||||
var polygon := _foundation_polygon(piece) if piece.piece_type != ModularBuilderPiece.PieceType.FLOOR else _piece_polygon_xz(piece)
|
||||
for i in range(polygon.size()):
|
||||
var a: Vector2 = polygon[i]
|
||||
var b: Vector2 = polygon[(i + 1) % polygon.size()]
|
||||
var edge := b - a
|
||||
var t := clampf((point - a).dot(edge) / maxf(edge.length_squared(), 0.000001), 0.0, 1.0)
|
||||
if point.distance_to(a + edge * t) <= edge_tolerance + 0.01:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _find_detail_placement(camera: Camera3D, screen_position: Vector2) -> Dictionary:
|
||||
var root := _get_selected_piece()
|
||||
if root == null or not _is_detail_tool(active_tool):
|
||||
return {}
|
||||
var ray_origin := camera.project_ray_origin(screen_position)
|
||||
var ray_direction := camera.project_ray_normal(screen_position)
|
||||
var candidates: Array[ModularBuilderPiece] = []
|
||||
if active_tool == TOOL_HATCH or active_tool == TOOL_PILLAR:
|
||||
_collect_surface_pieces(root, candidates)
|
||||
else:
|
||||
_collect_wall_pieces(root, candidates)
|
||||
var best_distance := INF
|
||||
var best_piece: ModularBuilderPiece
|
||||
var best_local := Vector3.ZERO
|
||||
var best_surface_y := 0.0
|
||||
for candidate in candidates:
|
||||
if not is_instance_valid(candidate) or candidate.piece_type == ModularBuilderPiece.PieceType.BUILDING:
|
||||
continue
|
||||
var local_origin := candidate.to_local(ray_origin)
|
||||
var local_direction := candidate.to_local(ray_origin + ray_direction) - local_origin
|
||||
if active_tool != TOOL_HATCH and candidate.custom_points.size() < 2:
|
||||
continue
|
||||
var uses_surface := active_tool == TOOL_HATCH or active_tool == TOOL_PILLAR
|
||||
var plane_normal := Vector3.UP if uses_surface else Vector3(-candidate.custom_points[1].z + candidate.custom_points[0].z, 0.0, candidate.custom_points[1].x - candidate.custom_points[0].x).normalized()
|
||||
if uses_surface and candidate.custom_points.size() >= 3:
|
||||
plane_normal = Vector3.UP
|
||||
if absf(local_direction.dot(plane_normal)) < 0.0001:
|
||||
continue
|
||||
var plane_y := 0.0
|
||||
var plane_point := Vector3.ZERO
|
||||
if uses_surface:
|
||||
plane_y = candidate.thickness * 0.5 if candidate.centered_origin else candidate.thickness
|
||||
plane_point.y = plane_y
|
||||
var distance := (plane_point - local_origin).dot(plane_normal) / local_direction.dot(plane_normal)
|
||||
if distance < 0.0 or distance >= best_distance:
|
||||
continue
|
||||
var hit := local_origin + local_direction * distance
|
||||
if uses_surface:
|
||||
if candidate.piece_type != ModularBuilderPiece.PieceType.FLOOR and candidate.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and candidate.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
continue
|
||||
if not _point_inside_surface(candidate, Vector2(hit.x, hit.z)):
|
||||
continue
|
||||
else:
|
||||
if candidate.piece_type != ModularBuilderPiece.PieceType.WALL or candidate.custom_points.size() < 2:
|
||||
continue
|
||||
var a: Vector3 = candidate.custom_points[0]
|
||||
var b: Vector3 = candidate.custom_points[1]
|
||||
var wall_dir := (b - a).normalized()
|
||||
var along := (hit - a).dot(wall_dir)
|
||||
var vertical := hit.y - a.y
|
||||
if along < 0.0 or along > a.distance_to(b) or vertical < 0.0 or vertical > candidate.unit_size:
|
||||
continue
|
||||
best_distance = distance
|
||||
best_piece = candidate
|
||||
best_local = hit
|
||||
best_surface_y = plane_y
|
||||
if best_piece == null:
|
||||
return {}
|
||||
# best_local uses the selected piece’s local coordinates. The child detail
|
||||
# must use the same space, especially for attached foundations.
|
||||
var host_local := best_local
|
||||
var detail_position: Vector3
|
||||
var detail_rotation := Vector3.ZERO
|
||||
if active_tool == TOOL_HATCH:
|
||||
detail_position = Vector3(host_local.x, 0.0, host_local.z)
|
||||
elif active_tool == TOOL_PILLAR:
|
||||
detail_position = Vector3(host_local.x, best_surface_y, host_local.z)
|
||||
else:
|
||||
var wall_a: Vector3 = best_piece.custom_points[0]
|
||||
var wall_b: Vector3 = best_piece.custom_points[1]
|
||||
var direction := (wall_b - wall_a).normalized()
|
||||
var n := Vector3(-direction.z, 0.0, direction.x)
|
||||
detail_rotation.y = atan2(n.x, n.z)
|
||||
var opening_center_y := wall_a.y + detail_height_spin.value * 0.5 + detail_height_offset_spin.value
|
||||
detail_position = Vector3((wall_a.x + wall_b.x) * 0.5, opening_center_y, (wall_a.z + wall_b.z) * 0.5)
|
||||
return {"host": best_piece, "position": detail_position, "rotation": detail_rotation}
|
||||
|
||||
func _new_detail(placement: Dictionary, preview: bool) -> ModularBuilderDetail:
|
||||
var detail := Node3D.new()
|
||||
detail.set_script(DETAIL_SCRIPT)
|
||||
if active_tool == TOOL_WINDOW:
|
||||
detail.name = "Window"
|
||||
elif active_tool == TOOL_DOOR:
|
||||
detail.name = "Door"
|
||||
elif active_tool == TOOL_HATCH:
|
||||
detail.name = "Hatch"
|
||||
elif active_tool == TOOL_PILLAR:
|
||||
detail.name = "Pillar"
|
||||
elif active_tool == TOOL_FENCE:
|
||||
detail.name = "Fence"
|
||||
else:
|
||||
detail.name = "Parapet"
|
||||
if active_tool == TOOL_WINDOW:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.WINDOW)
|
||||
elif active_tool == TOOL_DOOR:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.DOOR)
|
||||
elif active_tool == TOOL_HATCH:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.HATCH)
|
||||
elif active_tool == TOOL_FENCE:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.FENCE)
|
||||
elif active_tool == TOOL_PARAPET:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.PARAPET)
|
||||
else:
|
||||
detail.set("detail_type", ModularBuilderDetail.DetailType.PILLAR)
|
||||
detail.set("width", detail_width_spin.value * METERS_TO_GODOT_UNITS)
|
||||
detail.set("height", float(placement.length) if active_tool == TOOL_PILLAR and placement.has("length") else detail_height_spin.value * METERS_TO_GODOT_UNITS)
|
||||
detail.set("detail_mode", ModularBuilderDetail.DetailMode.SEPARATE_OBJECT if preview or active_tool == TOOL_PILLAR else detail_mode_option.get_selected_id())
|
||||
detail.set("surface_material", _preview_material if preview else null)
|
||||
if preview:
|
||||
var host := placement.host as ModularBuilderPiece
|
||||
var preview_depth := host.thickness if host != null else 0.08
|
||||
if active_tool == TOOL_WINDOW or active_tool == TOOL_DOOR:
|
||||
preview_depth *= 1.25
|
||||
detail.set("follow_host_thickness", false)
|
||||
detail.set("frame_depth", preview_depth)
|
||||
if active_tool == TOOL_WINDOW:
|
||||
detail.set("window_style", _selected_window_style)
|
||||
elif active_tool == TOOL_DOOR:
|
||||
detail.set("door_style", _selected_door_style)
|
||||
if preview:
|
||||
detail.name = "ModularBuilderPreview"
|
||||
detail.set_meta("modular_builder_preview", true)
|
||||
var detail_basis: Basis = placement.basis if placement.has("basis") else Basis.from_euler(placement.rotation)
|
||||
detail.transform = Transform3D(detail_basis, placement.position)
|
||||
if active_tool == TOOL_PILLAR:
|
||||
detail.set("pillar_direction", placement.direction)
|
||||
detail.set("pillar_alignment_basis", placement.alignment_basis)
|
||||
detail.set("center_align", _pillar_centralize_check.button_pressed)
|
||||
if active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET:
|
||||
detail.set("fence_height", fence_height_spin.value * METERS_TO_GODOT_UNITS)
|
||||
detail.set("fence_divisions", int(fence_divisions_spin.value))
|
||||
return detail as ModularBuilderDetail
|
||||
|
||||
func _update_detail_preview() -> void:
|
||||
if not _is_detail_tool(active_tool) or active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET or _preview_camera == null:
|
||||
return
|
||||
var placement: Dictionary
|
||||
if active_tool == TOOL_PILLAR:
|
||||
var root := _get_selected_piece()
|
||||
if root == null or pending_points.is_empty():
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.visible = false
|
||||
return
|
||||
var second_vertex := _nearest_vertex(_preview_camera, _preview_screen_position, call("_get_tool_vertices", root))
|
||||
if second_vertex == null:
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.visible = false
|
||||
return
|
||||
placement = _find_pillar_placement(root, pending_points[0], second_vertex)
|
||||
else:
|
||||
placement = _find_detail_placement(_preview_camera, _preview_screen_position)
|
||||
if placement.is_empty():
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.visible = false
|
||||
return
|
||||
if not is_instance_valid(_detail_preview) or _detail_preview.detail_type != _detail_type_for_tool(active_tool):
|
||||
_clear_detail_preview()
|
||||
_detail_preview = _new_detail(placement, true)
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
if scene_root == null:
|
||||
_detail_preview = null
|
||||
return
|
||||
var preview_host := placement.host as ModularBuilderPiece
|
||||
if active_tool == TOOL_PILLAR and preview_host != null:
|
||||
preview_host.add_child(_detail_preview, true)
|
||||
else:
|
||||
scene_root.add_child(_detail_preview, true)
|
||||
_detail_preview.owner = scene_root
|
||||
_detail_preview.visible = true
|
||||
if not is_equal_approx(_detail_preview.width, detail_width_spin.value):
|
||||
_detail_preview.width = detail_width_spin.value
|
||||
var preview_height := float(placement.length) if active_tool == TOOL_PILLAR and placement.has("length") else detail_height_spin.value
|
||||
if not is_equal_approx(_detail_preview.height, preview_height):
|
||||
_detail_preview.height = preview_height
|
||||
var host := placement.host as ModularBuilderPiece
|
||||
if host != null:
|
||||
var preview_basis: Basis = placement.basis if placement.has("basis") else Basis.from_euler(placement.rotation)
|
||||
if active_tool == TOOL_PILLAR:
|
||||
_detail_preview.transform = Transform3D(preview_basis, placement.position)
|
||||
_detail_preview.pillar_direction = placement.direction
|
||||
_detail_preview.pillar_alignment_basis = placement.alignment_basis
|
||||
_detail_preview.center_align = _pillar_centralize_check.button_pressed
|
||||
else:
|
||||
_detail_preview.global_transform = host.global_transform * Transform3D(preview_basis, placement.position)
|
||||
|
||||
func _detail_type_for_tool(tool: String) -> int:
|
||||
if tool == TOOL_DOOR:
|
||||
return ModularBuilderDetail.DetailType.DOOR
|
||||
if tool == TOOL_HATCH:
|
||||
return ModularBuilderDetail.DetailType.HATCH
|
||||
if tool == TOOL_PILLAR:
|
||||
return ModularBuilderDetail.DetailType.PILLAR
|
||||
if tool == TOOL_FENCE:
|
||||
return ModularBuilderDetail.DetailType.FENCE
|
||||
if tool == TOOL_PARAPET:
|
||||
return ModularBuilderDetail.DetailType.PARAPET
|
||||
return ModularBuilderDetail.DetailType.WINDOW
|
||||
|
||||
func _refresh_detail_preview(_value: float = 0.0) -> void:
|
||||
if _preview_camera != null:
|
||||
_update_detail_preview()
|
||||
|
||||
func _clear_detail_preview() -> void:
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.queue_free()
|
||||
_detail_preview = null
|
||||
|
||||
func _attach_detail(host: ModularBuilderPiece, detail: Node, scene_root: Node) -> void:
|
||||
if detail.get_parent() != host:
|
||||
host.add_child(detail, true)
|
||||
detail.owner = scene_root
|
||||
host._rebuild()
|
||||
|
||||
func _detach_detail(host: ModularBuilderPiece, detail: Node) -> void:
|
||||
if detail.get_parent() == host:
|
||||
host.remove_child(detail)
|
||||
host._rebuild()
|
||||
|
||||
func _collect_surface_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and (node.piece_type == ModularBuilderPiece.PieceType.FLOOR or node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE):
|
||||
pieces.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_surface_pieces(child, pieces)
|
||||
|
||||
func _collect_wall_pieces(node: Node, pieces: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and node.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
pieces.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_wall_pieces(child, pieces)
|
||||
|
||||
func _point_inside_surface(piece: ModularBuilderPiece, point: Vector2) -> bool:
|
||||
var polygon := _foundation_polygon(piece) if piece.piece_type != ModularBuilderPiece.PieceType.FLOOR else _piece_polygon_xz(piece)
|
||||
if polygon.size() < 3:
|
||||
return false
|
||||
var inside := false
|
||||
var j := polygon.size() - 1
|
||||
for i in range(polygon.size()):
|
||||
var a: Vector2 = polygon[i]
|
||||
var b: Vector2 = polygon[j]
|
||||
if (a.y > point.y) != (b.y > point.y) and point.x < (b.x - a.x) * (point.y - a.y) / (b.y - a.y) + a.x:
|
||||
inside = not inside
|
||||
j = i
|
||||
return inside
|
||||
|
||||
func _piece_polygon_xz(piece: ModularBuilderPiece) -> Array[Vector2]:
|
||||
var polygon: Array[Vector2] = []
|
||||
for point in piece.custom_points:
|
||||
polygon.append(Vector2(point.x, point.z))
|
||||
return polygon
|
||||
|
||||
func _nearest_vertex(camera: Camera3D, screen_position: Vector2, vertices: Array[Vector3]) -> Variant:
|
||||
var nearest: Variant = null
|
||||
var nearest_distance := MARKER_HIT_RADIUS
|
||||
for world_point in vertices:
|
||||
if camera.is_position_behind(world_point):
|
||||
continue
|
||||
var distance := camera.unproject_position(world_point).distance_to(screen_position)
|
||||
if distance < nearest_distance:
|
||||
nearest_distance = distance
|
||||
nearest = world_point
|
||||
return nearest
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dd8jw301grvq8
|
||||
544
addons/modular_builder_en/modular_builder_editor_ui.gd
Normal file
|
|
@ -0,0 +1,544 @@
|
|||
@tool
|
||||
extends EditorPlugin
|
||||
|
||||
const PIECE_SCRIPT = preload("res://addons/modular_builder_en/modular_piece.gd")
|
||||
const DETAIL_SCRIPT = preload("res://addons/modular_builder_en/modular_building_detail.gd")
|
||||
const MESH_BAKER = preload("res://addons/modular_builder_en/modular_mesh_baker.gd")
|
||||
const TOOL_NONE := ""
|
||||
const TOOL_FOUNDATION := "foundation"
|
||||
const TOOL_FOUNDATION_SQUARE := "foundation_square"
|
||||
const TOOL_FOUNDATION_TRIANGLE := "foundation_triangle"
|
||||
const TOOL_WALL := "wall"
|
||||
const TOOL_GABLE_WALL := "gable_wall"
|
||||
const TOOL_GABLE_WALL_RIGHT := "gable_wall_right"
|
||||
const TOOL_FLOOR_SQUARE := "floor_square"
|
||||
const TOOL_FLOOR_TRIANGLE := "floor_triangle"
|
||||
const TOOL_ROOF_GABLE := "roof_gable"
|
||||
const TOOL_ROOF_SHED := "roof_shed"
|
||||
const TOOL_ROOF_PYRAMID := "roof_pyramid"
|
||||
const TOOL_STAIRS := "stairs"
|
||||
const TOOL_FENCE := "fence"
|
||||
const TOOL_PARAPET := "parapet"
|
||||
const TOOL_MERGE_VERTICES := "merge_vertices"
|
||||
const TOOL_CREATE_VERTEX := "create_vertex"
|
||||
const TOOL_MOVE_VERTEX := "move_vertex"
|
||||
const TOOL_REMOVE_VERTEX := "remove_vertex"
|
||||
const MERGED_VERTEX_GROUPS_META := &"modular_builder_merged_vertex_groups"
|
||||
const CUSTOM_VERTICES_META := &"modular_builder_custom_vertices"
|
||||
const TOOL_WINDOW := "detail_window"
|
||||
const TOOL_DOOR := "detail_door"
|
||||
const TOOL_HATCH := "detail_hatch"
|
||||
const TOOL_PILLAR := "detail_pillar"
|
||||
const EXPORT_NEW_SCENE := 0
|
||||
const EXPORT_SCENE_COPY := 1
|
||||
const EXPORT_LIBRARY := 2
|
||||
const METERS_TO_GODOT_UNITS := 1.0
|
||||
const DOOR_DEFAULT_SCALE := 0.8
|
||||
const DOOR_DEFAULT_WIDTH_METERS := 0.87 * DOOR_DEFAULT_SCALE
|
||||
const DOOR_DEFAULT_HEIGHT_METERS := 2.15 * DOOR_DEFAULT_SCALE
|
||||
const MARKER_RADIUS := 8.0
|
||||
const MARKER_HIT_RADIUS := 18.0
|
||||
|
||||
var dock: VBoxContainer
|
||||
var _creation_row: HFlowContainer
|
||||
var _export_row: HFlowContainer
|
||||
var _tool_row: HFlowContainer
|
||||
var _vertex_edit_row: HFlowContainer
|
||||
var _marker_scope_check: CheckBox
|
||||
var _simplify_vertices_check: CheckBox
|
||||
var _rise_options_row: HFlowContainer
|
||||
var _linear_options_row: HFlowContainer
|
||||
var _stair_count_label: Label
|
||||
var _stair_width_label: Label
|
||||
var _fence_divisions_label: Label
|
||||
var _detail_options_row: HFlowContainer
|
||||
var _foundation_type_selected := ModularBuilderPiece.PieceType.FOUNDATION_SQUARE
|
||||
var size_spin: SpinBox
|
||||
var rise_spin: SpinBox
|
||||
var stair_count_spin: SpinBox
|
||||
var stair_width_spin: SpinBox
|
||||
var fence_divisions_spin: SpinBox
|
||||
var fence_height_spin: SpinBox
|
||||
var _fence_height_label: Label
|
||||
var _rise_label: Label
|
||||
var _gable_centralize_check: CheckBox
|
||||
var _gable_centralize_default := false
|
||||
var detail_width_spin: SpinBox
|
||||
var detail_height_spin: SpinBox
|
||||
var detail_height_offset_spin: SpinBox
|
||||
var _detail_height_offset_label: Label
|
||||
var _detail_width_label: Label
|
||||
var _detail_height_label: Label
|
||||
var detail_mode_option: OptionButton
|
||||
var detail_style_option: OptionButton
|
||||
var status_label: Label
|
||||
var active_tool := TOOL_NONE
|
||||
var pending_points: Array[Vector3] = []
|
||||
var _redirecting_selection := false
|
||||
var _marker_container: Node3D
|
||||
var _marker_root: ModularBuilderPiece
|
||||
var _marker_point_nodes: Array[Label3D] = []
|
||||
var _side_button_nodes: Array[Label3D] = []
|
||||
var _side_material: StandardMaterial3D
|
||||
var _tool_button_group: ButtonGroup
|
||||
var _tool_buttons: Dictionary = {}
|
||||
var _pillar_centralize_check: CheckBox
|
||||
var _pillar_centralize_default := false
|
||||
var _active_button_key := ""
|
||||
var _last_created_piece: ModularBuilderPiece
|
||||
var _detail_preview: ModularBuilderDetail
|
||||
var _preview_material: StandardMaterial3D
|
||||
var _preview_camera: Camera3D
|
||||
var _preview_screen_position := Vector2.ZERO
|
||||
var _save_module_dialog: FileDialog
|
||||
var _load_module_dialog: FileDialog
|
||||
var _export_scene_dialog: FileDialog
|
||||
var _export_mode_option: OptionButton
|
||||
var _pending_export_mesh: ArrayMesh
|
||||
var _pending_export_name := "BakedBuilding"
|
||||
var _pending_export_basis := Basis.IDENTITY
|
||||
var _selected_window_style := ModularBuilderDetail.WindowStyle.SINGLE_PANE
|
||||
var _selected_door_style := ModularBuilderDetail.DoorStyle.FLAT
|
||||
var _editor_sync_elapsed := 0.0
|
||||
|
||||
func _ready() -> void:
|
||||
set_process(true)
|
||||
_side_material = StandardMaterial3D.new()
|
||||
_side_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_side_material.albedo_color = Color(0.18, 0.95, 0.52)
|
||||
_side_material.no_depth_test = true
|
||||
_side_material.cull_mode = BaseMaterial3D.CULL_DISABLED
|
||||
|
||||
func _process(delta: float) -> void:
|
||||
_editor_sync_elapsed += delta
|
||||
if _editor_sync_elapsed < 1.0 / 15.0:
|
||||
return
|
||||
_editor_sync_elapsed = 0.0
|
||||
call("_sync_vertex_markers")
|
||||
_sync_pillar_centralize_control()
|
||||
_sync_gable_centralize_control()
|
||||
|
||||
func _enter_tree() -> void:
|
||||
dock = VBoxContainer.new()
|
||||
dock.name = "Modular Builder"
|
||||
dock.custom_minimum_size = Vector2(0, 110)
|
||||
_creation_row = HFlowContainer.new()
|
||||
dock.add_child(_creation_row)
|
||||
_creation_row.add_child(_label("Size:"))
|
||||
size_spin = SpinBox.new()
|
||||
size_spin.min_value = 0.5
|
||||
size_spin.max_value = 8.0
|
||||
size_spin.step = 0.001
|
||||
size_spin.value = 2.0
|
||||
size_spin.suffix = " m"
|
||||
size_spin.custom_minimum_size = Vector2(100, 0)
|
||||
_creation_row.add_child(size_spin)
|
||||
var create_button := _make_icon_button("res://addons/modular_builder_en/icons/add_building.svg", "Add ModularBuilderPiece", Color(0.22, 0.30, 0.38))
|
||||
create_button.pressed.connect(Callable(self, "_add_piece"))
|
||||
_creation_row.add_child(create_button)
|
||||
var rotate_button := _make_icon_button("res://addons/modular_builder_en/icons/rotate.svg", "Rotate selected piece 90°", Color(0.22, 0.30, 0.38))
|
||||
rotate_button.pressed.connect(Callable(self, "_rotate_selected_piece"))
|
||||
_creation_row.add_child(rotate_button)
|
||||
var collision_button := _make_icon_button("res://addons/modular_builder_en/icons/collision.svg", "Add general collision to selected structure", Color(0.22, 0.30, 0.38))
|
||||
collision_button.pressed.connect(Callable(self, "_add_general_collision"))
|
||||
_creation_row.add_child(collision_button)
|
||||
var save_module_button := _make_icon_button("res://addons/modular_builder_en/icons/save_module.svg", "Save selected structure as a .tscn module", Color(0.22, 0.30, 0.38))
|
||||
save_module_button.pressed.connect(Callable(self, "_show_save_module_dialog"))
|
||||
_creation_row.add_child(save_module_button)
|
||||
var load_module_button := _make_icon_button("res://addons/modular_builder_en/icons/load_module.svg", "Insert saved module into the scene", Color(0.22, 0.30, 0.38))
|
||||
load_module_button.pressed.connect(Callable(self, "_show_load_module_dialog"))
|
||||
_creation_row.add_child(load_module_button)
|
||||
_export_row = HFlowContainer.new()
|
||||
dock.add_child(_export_row)
|
||||
_export_row.add_child(_label("Export structure as mesh:") )
|
||||
_export_mode_option = OptionButton.new()
|
||||
_export_mode_option.add_item("New scene", EXPORT_NEW_SCENE)
|
||||
_export_mode_option.add_item("Copy in this scene", EXPORT_SCENE_COPY)
|
||||
_export_mode_option.add_item("In ScopedVR-Bibliothek (Objekt-Pipeline)", EXPORT_LIBRARY)
|
||||
_export_mode_option.tooltip_text = "Choose whether to save the combined mesh as a new scene, add a copy to the current scene, or export it into the ScopedVR object library (svmap-ready with trimesh collision)."
|
||||
_export_row.add_child(_export_mode_option)
|
||||
var export_button := Button.new()
|
||||
export_button.text = "Export"
|
||||
export_button.tooltip_text = "Generate one static mesh with materials and UVs from the selected structure."
|
||||
export_button.pressed.connect(Callable(self, "_on_export_pressed"))
|
||||
_export_row.add_child(export_button)
|
||||
_tool_row = HFlowContainer.new()
|
||||
dock.add_child(_tool_row)
|
||||
_tool_button_group = ButtonGroup.new()
|
||||
_tool_button_group.allow_unpress = true
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/foundation_square.svg", "Square foundation", TOOL_FOUNDATION_SQUARE, Color(0.08, 0.48, 0.22))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/foundation_triangle.svg", "Triangular foundation", TOOL_FOUNDATION_TRIANGLE, Color(0.08, 0.48, 0.22))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/wall.svg", "Wall", TOOL_WALL, Color(0.65, 0.12, 0.12))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/gable_wall.svg", "Gable wall", TOOL_GABLE_WALL, Color(0.65, 0.12, 0.12))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/gable_wall_right.svg", "Right triangular gable wall", TOOL_GABLE_WALL_RIGHT, Color(0.65, 0.12, 0.12))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/floor_square.svg", "Square floor", TOOL_FLOOR_SQUARE, Color(0.68, 0.48, 0.06))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/floor_triangle.svg", "Triangular floor", TOOL_FLOOR_TRIANGLE, Color(0.68, 0.48, 0.06))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/roof_gable.svg", "Gable roof", TOOL_ROOF_GABLE, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/roof_shed.svg", "Shed roof", TOOL_ROOF_SHED, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/roof_pyramid.svg", "Pyramid roof", TOOL_ROOF_PYRAMID, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/stairs.svg", "Stairs", TOOL_STAIRS, Color(0.10, 0.38, 0.62))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/fence.svg", "Decorative fence", TOOL_FENCE, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/parapet.svg", "Parapet: single rail and posts", TOOL_PARAPET, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/window.svg", "Detail: window", TOOL_WINDOW, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/door.svg", "Detail: door", TOOL_DOOR, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/hatch.svg", "Detail: hatch", TOOL_HATCH, Color(0.88, 0.22, 0.55))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/pillar.svg", "Plain pillar: two vertices or Alt+click for a vertical pillar", TOOL_PILLAR, Color(0.88, 0.22, 0.55))
|
||||
var cancel_button := _make_icon_button("res://addons/modular_builder_en/icons/cancel.svg", "Deactivate tool", Color(0.34, 0.20, 0.20))
|
||||
cancel_button.pressed.connect(Callable(self, "_set_tool").bind(TOOL_NONE))
|
||||
_tool_row.add_child(cancel_button)
|
||||
_vertex_edit_row = HFlowContainer.new()
|
||||
dock.add_child(_vertex_edit_row)
|
||||
_vertex_edit_row.add_child(_label("Vertices:"))
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/merge_vertices.svg", "Merge vertices to reduce markers", TOOL_MERGE_VERTICES, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/create_vertex.svg", "Create a connection vertex on the clicked surface", TOOL_CREATE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/move_vertex.svg", "Move a manual connection vertex", TOOL_MOVE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
_add_tool_button("res://addons/modular_builder_en/icons/remove_vertex.svg", "Remove a manual connection vertex", TOOL_REMOVE_VERTEX, Color(0.38, 0.32, 0.52), _vertex_edit_row)
|
||||
var marker_options_row := HFlowContainer.new()
|
||||
dock.add_child(marker_options_row)
|
||||
_marker_scope_check = CheckBox.new()
|
||||
_marker_scope_check.text = "Only show points on selected pieces"
|
||||
_marker_scope_check.tooltip_text = "Limit markers to pieces selected in the Scene tree. Selecting the structure container shows all its points."
|
||||
_marker_scope_check.toggled.connect(Callable(self, "_on_marker_scope_toggled"))
|
||||
marker_options_row.add_child(_marker_scope_check)
|
||||
_simplify_vertices_check = CheckBox.new()
|
||||
_simplify_vertices_check.text = "Simplify vertices"
|
||||
_simplify_vertices_check.tooltip_text = "Merge front and back points at the middle of walls and pillars; floors and foundations show their top outline. Vertex editing tools show the exact points."
|
||||
_simplify_vertices_check.toggled.connect(Callable(self, "_on_simplify_vertices_toggled"))
|
||||
marker_options_row.add_child(_simplify_vertices_check)
|
||||
_rise_options_row = HFlowContainer.new()
|
||||
dock.add_child(_rise_options_row)
|
||||
_rise_label = _label("Peak rise:")
|
||||
_rise_options_row.add_child(_rise_label)
|
||||
rise_spin = _detail_spin(1.0, 0.1, 8.0, 0.001)
|
||||
_rise_options_row.add_child(rise_spin)
|
||||
_linear_options_row = HFlowContainer.new()
|
||||
dock.add_child(_linear_options_row)
|
||||
_stair_count_label = _label("Steps:")
|
||||
_linear_options_row.add_child(_stair_count_label)
|
||||
stair_count_spin = SpinBox.new()
|
||||
stair_count_spin.min_value = 1
|
||||
stair_count_spin.max_value = 100
|
||||
stair_count_spin.step = 1
|
||||
stair_count_spin.value = 10
|
||||
stair_count_spin.custom_minimum_size = Vector2(72, 0)
|
||||
_linear_options_row.add_child(stair_count_spin)
|
||||
_stair_width_label = _label("Width:")
|
||||
_linear_options_row.add_child(_stair_width_label)
|
||||
stair_width_spin = _detail_spin(1.0, 0.01, 8.0, 0.001)
|
||||
_linear_options_row.add_child(stair_width_spin)
|
||||
_fence_divisions_label = _label("Intermediate posts:")
|
||||
_linear_options_row.add_child(_fence_divisions_label)
|
||||
fence_divisions_spin = SpinBox.new()
|
||||
fence_divisions_spin.min_value = 0
|
||||
fence_divisions_spin.max_value = 100
|
||||
fence_divisions_spin.step = 1
|
||||
fence_divisions_spin.value = 1
|
||||
fence_divisions_spin.custom_minimum_size = Vector2(72, 0)
|
||||
_linear_options_row.add_child(fence_divisions_spin)
|
||||
_fence_height_label = _label("Height:")
|
||||
_linear_options_row.add_child(_fence_height_label)
|
||||
fence_height_spin = _detail_spin(0.7, 0.1, 8.0, 0.001)
|
||||
_linear_options_row.add_child(fence_height_spin)
|
||||
_linear_options_row.visible = false
|
||||
_gable_centralize_check = CheckBox.new()
|
||||
_gable_centralize_check.text = "Center"
|
||||
_gable_centralize_check.tooltip_text = "Align the gable with the center of the supporting wall thickness."
|
||||
_gable_centralize_check.visible = false
|
||||
_gable_centralize_check.toggled.connect(_on_gable_centralize_toggled)
|
||||
_rise_options_row.add_child(_gable_centralize_check)
|
||||
_rise_options_row.visible = false
|
||||
_detail_options_row = HFlowContainer.new()
|
||||
dock.add_child(_detail_options_row)
|
||||
_detail_width_label = _label("Width:")
|
||||
_detail_options_row.add_child(_detail_width_label)
|
||||
detail_width_spin = _detail_spin(1.0, 0.01, 5.0, 0.001)
|
||||
_detail_options_row.add_child(detail_width_spin)
|
||||
_detail_height_label = _label("Height:")
|
||||
_detail_options_row.add_child(_detail_height_label)
|
||||
detail_height_spin = _detail_spin(1.0, 0.2, 5.0, 0.001)
|
||||
_detail_options_row.add_child(detail_height_spin)
|
||||
_pillar_centralize_check = CheckBox.new()
|
||||
_pillar_centralize_check.text = "Center"
|
||||
_pillar_centralize_check.tooltip_text = "Align the pillar with the nearest axis of the first vertex’s piece while keeping it centered between the points."
|
||||
_pillar_centralize_check.visible = false
|
||||
_pillar_centralize_check.toggled.connect(_on_pillar_centralize_toggled)
|
||||
_detail_options_row.add_child(_pillar_centralize_check)
|
||||
_detail_height_offset_label = _label("Starting height:")
|
||||
_detail_options_row.add_child(_detail_height_offset_label)
|
||||
detail_height_offset_spin = _detail_spin(0.0, -2.0, 2.0, 0.001)
|
||||
detail_height_offset_spin.suffix = " m"
|
||||
_detail_options_row.add_child(detail_height_offset_spin)
|
||||
detail_mode_option = OptionButton.new()
|
||||
detail_mode_option.add_item("Opening", ModularBuilderDetail.DetailMode.OPENING)
|
||||
detail_mode_option.add_item("Opening + object", ModularBuilderDetail.DetailMode.SEPARATE_OBJECT)
|
||||
detail_mode_option.select(0)
|
||||
detail_mode_option.tooltip_text = "Choose an opening alone or an opening with a separately textured object."
|
||||
_detail_options_row.add_child(detail_mode_option)
|
||||
detail_style_option = OptionButton.new()
|
||||
detail_style_option.tooltip_text = "Window or door style."
|
||||
detail_style_option.item_selected.connect(_on_detail_style_selected)
|
||||
_detail_options_row.add_child(detail_style_option)
|
||||
detail_width_spin.value_changed.connect(Callable(self, "_refresh_detail_preview"))
|
||||
detail_height_spin.value_changed.connect(Callable(self, "_refresh_detail_preview"))
|
||||
detail_height_offset_spin.value_changed.connect(Callable(self, "_refresh_detail_preview"))
|
||||
detail_mode_option.item_selected.connect(_on_detail_mode_selected)
|
||||
_detail_options_row.visible = false
|
||||
_preview_material = StandardMaterial3D.new()
|
||||
_preview_material.shading_mode = BaseMaterial3D.SHADING_MODE_UNSHADED
|
||||
_preview_material.transparency = BaseMaterial3D.TRANSPARENCY_ALPHA
|
||||
_preview_material.albedo_color = Color(1.0, 0.35, 0.72, 0.48)
|
||||
_save_module_dialog = FileDialog.new()
|
||||
_save_module_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
|
||||
_save_module_dialog.access = FileDialog.ACCESS_RESOURCES
|
||||
_save_module_dialog.add_filter("*.tscn", "Godot Scene")
|
||||
_save_module_dialog.file_selected.connect(Callable(self, "_save_module_to_path"))
|
||||
add_child(_save_module_dialog)
|
||||
_load_module_dialog = FileDialog.new()
|
||||
_load_module_dialog.file_mode = FileDialog.FILE_MODE_OPEN_FILE
|
||||
_load_module_dialog.access = FileDialog.ACCESS_RESOURCES
|
||||
_load_module_dialog.add_filter("*.tscn", "Godot Scene")
|
||||
_load_module_dialog.file_selected.connect(Callable(self, "_load_module_from_path"))
|
||||
add_child(_load_module_dialog)
|
||||
_export_scene_dialog = FileDialog.new()
|
||||
_export_scene_dialog.file_mode = FileDialog.FILE_MODE_SAVE_FILE
|
||||
_export_scene_dialog.access = FileDialog.ACCESS_RESOURCES
|
||||
_export_scene_dialog.add_filter("*.tscn", "Godot Scene")
|
||||
_export_scene_dialog.file_selected.connect(Callable(self, "_export_baked_scene_to_path"))
|
||||
add_child(_export_scene_dialog)
|
||||
status_label = _label("Select a tool to show the vertices.")
|
||||
status_label.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
dock.add_child(status_label)
|
||||
add_control_to_bottom_panel(dock, "Modular Builder")
|
||||
set_input_event_forwarding_always_enabled()
|
||||
get_editor_interface().get_selection().selection_changed.connect(Callable(self, "_on_editor_selection_changed"))
|
||||
var inspector := get_editor_interface().get_inspector()
|
||||
if not inspector.property_edited.is_connected(_on_inspector_property_edited):
|
||||
inspector.property_edited.connect(_on_inspector_property_edited)
|
||||
|
||||
func _exit_tree() -> void:
|
||||
set_process(false)
|
||||
var editor_selection := get_editor_interface().get_selection()
|
||||
if editor_selection.selection_changed.is_connected(Callable(self, "_on_editor_selection_changed")):
|
||||
editor_selection.selection_changed.disconnect(Callable(self, "_on_editor_selection_changed"))
|
||||
var inspector := get_editor_interface().get_inspector()
|
||||
if inspector.property_edited.is_connected(_on_inspector_property_edited):
|
||||
inspector.property_edited.disconnect(_on_inspector_property_edited)
|
||||
if dock:
|
||||
remove_control_from_bottom_panel(dock)
|
||||
dock.queue_free()
|
||||
call("_clear_marker_container")
|
||||
call("_clear_detail_preview")
|
||||
update_overlays()
|
||||
|
||||
func _handles(object: Object) -> bool:
|
||||
return object is ModularBuilderPiece
|
||||
|
||||
func _label(text: String) -> Label:
|
||||
var label := Label.new()
|
||||
label.text = text
|
||||
return label
|
||||
|
||||
func _detail_spin(value: float, minimum: float, maximum: float, step: float) -> SpinBox:
|
||||
var spin := SpinBox.new()
|
||||
spin.min_value = minimum
|
||||
spin.max_value = maximum
|
||||
spin.step = step
|
||||
spin.value = value
|
||||
spin.suffix = " m"
|
||||
spin.custom_minimum_size = Vector2(84, 0)
|
||||
return spin
|
||||
|
||||
func _make_icon_button(icon_path: String, tooltip: String, color: Color) -> Button:
|
||||
var button := Button.new()
|
||||
button.icon = load(icon_path) as Texture2D
|
||||
button.tooltip_text = tooltip
|
||||
button.custom_minimum_size = Vector2(48, 48)
|
||||
button.icon_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
button.expand_icon = true
|
||||
button.add_theme_constant_override("icon_max_width", 30)
|
||||
var normal := StyleBoxFlat.new()
|
||||
normal.bg_color = color
|
||||
normal.set_corner_radius_all(4)
|
||||
normal.set_border_width_all(1)
|
||||
normal.border_color = Color(0.02, 0.02, 0.02)
|
||||
button.add_theme_stylebox_override("normal", normal)
|
||||
var hover := normal.duplicate() as StyleBoxFlat
|
||||
hover.bg_color = color.lightened(0.16)
|
||||
button.add_theme_stylebox_override("hover", hover)
|
||||
var pressed := normal.duplicate() as StyleBoxFlat
|
||||
pressed.bg_color = color.lightened(0.28)
|
||||
pressed.border_width_bottom = 3
|
||||
button.add_theme_stylebox_override("pressed", pressed)
|
||||
return button
|
||||
|
||||
func _add_tool_button(icon_path: String, tooltip: String, tool: String, color: Color, target_row: Container = null) -> void:
|
||||
var button := _make_icon_button(icon_path, tooltip, color)
|
||||
button.toggle_mode = true
|
||||
button.button_group = _tool_button_group
|
||||
button.toggled.connect(_on_tool_button_toggled.bind(tool))
|
||||
(_tool_row if target_row == null else target_row).add_child(button)
|
||||
_tool_buttons[tool] = button
|
||||
|
||||
func _on_tool_button_toggled(pressed: bool, tool: String) -> void:
|
||||
var is_foundation := tool == TOOL_FOUNDATION_SQUARE or tool == TOOL_FOUNDATION_TRIANGLE
|
||||
if pressed:
|
||||
_active_button_key = tool
|
||||
if is_foundation:
|
||||
_foundation_type_selected = ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE if tool == TOOL_FOUNDATION_TRIANGLE else ModularBuilderPiece.PieceType.FOUNDATION_SQUARE
|
||||
call("_set_tool", TOOL_FOUNDATION)
|
||||
else:
|
||||
call("_set_tool", tool)
|
||||
_update_detail_options(tool)
|
||||
else:
|
||||
call_deferred("_finish_tool_button_toggle_off", tool)
|
||||
|
||||
func _finish_tool_button_toggle_off(tool: String) -> void:
|
||||
var button := _tool_buttons.get(tool) as Button
|
||||
if button != null and not button.button_pressed and _active_button_key == tool:
|
||||
call("_set_tool", TOOL_NONE)
|
||||
|
||||
func _update_detail_options(tool: String) -> void:
|
||||
var is_detail := _is_detail_tool(tool)
|
||||
var is_fence := tool == TOOL_FENCE or tool == TOOL_PARAPET
|
||||
_detail_options_row.visible = is_detail
|
||||
detail_width_spin.visible = is_detail and not is_fence
|
||||
_detail_width_label.visible = is_detail and not is_fence
|
||||
detail_height_spin.visible = is_detail and not is_fence
|
||||
_detail_height_label.visible = is_detail and not is_fence
|
||||
detail_height_offset_spin.visible = tool != TOOL_HATCH and tool != TOOL_PILLAR and not is_fence
|
||||
_detail_height_offset_label.visible = detail_height_offset_spin.visible
|
||||
detail_mode_option.visible = tool != TOOL_PILLAR and not is_fence
|
||||
_detail_width_label.text = "Side:" if tool == TOOL_PILLAR else "Width:"
|
||||
_detail_height_label.text = "Length:" if tool == TOOL_PILLAR else "Height:"
|
||||
_linear_options_row.visible = tool == TOOL_STAIRS or is_fence
|
||||
_stair_count_label.visible = tool == TOOL_STAIRS
|
||||
stair_count_spin.visible = tool == TOOL_STAIRS
|
||||
_stair_width_label.visible = tool == TOOL_STAIRS
|
||||
stair_width_spin.visible = tool == TOOL_STAIRS
|
||||
_fence_divisions_label.visible = is_fence
|
||||
fence_divisions_spin.visible = is_fence
|
||||
_fence_height_label.visible = is_fence
|
||||
fence_height_spin.visible = is_fence
|
||||
if not is_detail:
|
||||
return
|
||||
var width := 1.0
|
||||
var height := 1.0
|
||||
var bottom_offset := 0.5
|
||||
if tool == TOOL_PILLAR:
|
||||
width = 0.2
|
||||
height = 2.0
|
||||
bottom_offset = 0.0
|
||||
elif tool == TOOL_DOOR:
|
||||
width = DOOR_DEFAULT_WIDTH_METERS
|
||||
height = DOOR_DEFAULT_HEIGHT_METERS
|
||||
bottom_offset = 0.0
|
||||
elif tool == TOOL_HATCH:
|
||||
bottom_offset = 0.0
|
||||
detail_width_spin.set_value_no_signal(width)
|
||||
detail_height_spin.set_value_no_signal(height)
|
||||
detail_height_offset_spin.set_value_no_signal(bottom_offset)
|
||||
detail_style_option.clear()
|
||||
detail_style_option.visible = tool == TOOL_WINDOW or tool == TOOL_DOOR
|
||||
if tool == TOOL_WINDOW:
|
||||
detail_style_option.add_item("Single pane", ModularBuilderDetail.WindowStyle.SINGLE_PANE)
|
||||
detail_style_option.add_item("Cross pane", ModularBuilderDetail.WindowStyle.CROSS_PANE)
|
||||
detail_style_option.select(_selected_window_style)
|
||||
elif tool == TOOL_DOOR:
|
||||
detail_style_option.add_item("Flat", ModularBuilderDetail.DoorStyle.FLAT)
|
||||
detail_style_option.add_item("Paneled", ModularBuilderDetail.DoorStyle.PANELED)
|
||||
detail_style_option.select(_selected_door_style)
|
||||
call("_refresh_detail_preview")
|
||||
|
||||
func _on_detail_style_selected(index: int) -> void:
|
||||
if active_tool == TOOL_WINDOW:
|
||||
_selected_window_style = index
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.window_style = index
|
||||
elif active_tool == TOOL_DOOR:
|
||||
_selected_door_style = index
|
||||
if is_instance_valid(_detail_preview):
|
||||
_detail_preview.door_style = index
|
||||
call("_refresh_detail_preview")
|
||||
|
||||
func _on_detail_mode_selected(_index: int) -> void:
|
||||
call("_refresh_detail_preview")
|
||||
|
||||
func _on_pillar_centralize_toggled(enabled: bool) -> void:
|
||||
_pillar_centralize_default = enabled
|
||||
var selected_pillar := _get_selected_pillar_detail()
|
||||
if selected_pillar != null:
|
||||
selected_pillar.center_align = enabled
|
||||
if is_instance_valid(_detail_preview) and _detail_preview.detail_type == ModularBuilderDetail.DetailType.PILLAR:
|
||||
_detail_preview.center_align = enabled
|
||||
call("_refresh_detail_preview")
|
||||
|
||||
func _on_gable_centralize_toggled(enabled: bool) -> void:
|
||||
_gable_centralize_default = enabled
|
||||
var selected := _get_selected_gable_wall()
|
||||
if selected != null:
|
||||
selected.center_align = enabled
|
||||
|
||||
func _sync_gable_centralize_control() -> void:
|
||||
if _gable_centralize_check == null:
|
||||
return
|
||||
var selected := _get_selected_gable_wall()
|
||||
var show_option := active_tool == TOOL_WALL or active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT or selected != null
|
||||
var show_rise := active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT or active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_PYRAMID
|
||||
_rise_options_row.visible = show_rise or selected != null
|
||||
_rise_label.visible = show_rise
|
||||
rise_spin.visible = show_rise
|
||||
_gable_centralize_check.visible = show_option
|
||||
_gable_centralize_check.text = "Center walls" if active_tool == TOOL_WALL or (selected != null and selected.piece_type == ModularBuilderPiece.PieceType.WALL) else "Center"
|
||||
_gable_centralize_check.tooltip_text = "Align wall endpoints with the centerlines of supporting walls." if active_tool == TOOL_WALL or (selected != null and selected.piece_type == ModularBuilderPiece.PieceType.WALL) else "Align the gable with the center of the supporting wall thickness."
|
||||
if selected != null:
|
||||
_gable_centralize_default = selected.center_align
|
||||
_gable_centralize_check.set_pressed_no_signal(selected.center_align)
|
||||
else:
|
||||
_gable_centralize_check.set_pressed_no_signal(_gable_centralize_default)
|
||||
|
||||
func _get_selected_gable_wall() -> ModularBuilderPiece:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.size() == 1 and selected[0] is ModularBuilderPiece and (selected[0].piece_type == ModularBuilderPiece.PieceType.WALL or selected[0].piece_type == ModularBuilderPiece.PieceType.GABLE_WALL or selected[0].piece_type == ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT):
|
||||
return selected[0] as ModularBuilderPiece
|
||||
return null
|
||||
|
||||
func _sync_pillar_centralize_control() -> void:
|
||||
if _pillar_centralize_check == null:
|
||||
return
|
||||
var selected_pillar := _get_selected_pillar_detail()
|
||||
var show_pillar_options := active_tool == TOOL_PILLAR or selected_pillar != null
|
||||
_detail_options_row.visible = _is_detail_tool(active_tool) or show_pillar_options
|
||||
_pillar_centralize_check.visible = show_pillar_options
|
||||
if selected_pillar != null and active_tool != TOOL_PILLAR and not _is_detail_tool(active_tool):
|
||||
_detail_width_label.visible = false
|
||||
detail_width_spin.visible = false
|
||||
_detail_height_label.visible = false
|
||||
detail_height_spin.visible = false
|
||||
_detail_height_offset_label.visible = false
|
||||
detail_height_offset_spin.visible = false
|
||||
detail_mode_option.visible = false
|
||||
detail_style_option.visible = false
|
||||
elif active_tool == TOOL_PILLAR:
|
||||
_detail_width_label.visible = true
|
||||
detail_width_spin.visible = true
|
||||
_detail_height_label.visible = true
|
||||
detail_height_spin.visible = true
|
||||
if selected_pillar != null:
|
||||
_pillar_centralize_default = selected_pillar.center_align
|
||||
_pillar_centralize_check.set_pressed_no_signal(selected_pillar.center_align)
|
||||
else:
|
||||
_pillar_centralize_check.set_pressed_no_signal(_pillar_centralize_default)
|
||||
|
||||
func _get_selected_pillar_detail() -> ModularBuilderDetail:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.size() != 1 or not selected[0] is ModularBuilderDetail:
|
||||
return null
|
||||
var detail := selected[0] as ModularBuilderDetail
|
||||
return detail if detail.detail_type == ModularBuilderDetail.DetailType.PILLAR else null
|
||||
|
||||
func _on_inspector_property_edited(property: String) -> void:
|
||||
if property == "center_align":
|
||||
_sync_pillar_centralize_control()
|
||||
|
||||
func _is_detail_tool(tool: String) -> bool:
|
||||
return tool == TOOL_WINDOW or tool == TOOL_DOOR or tool == TOOL_HATCH or tool == TOOL_PILLAR or tool == TOOL_FENCE or tool == TOOL_PARAPET
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://duvk2kap6rjef
|
||||
727
addons/modular_builder_en/modular_builder_editor_vertices.gd
Normal file
|
|
@ -0,0 +1,727 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder_en/modular_builder_editor_details.gd"
|
||||
|
||||
const MESH_PICKER = preload("res://addons/modular_builder_en/modular_mesh_picker.gd")
|
||||
|
||||
func _sync_vertex_markers() -> void:
|
||||
var root := _get_selected_piece()
|
||||
var vertex_points: Array[Vector3] = []
|
||||
var side_points: Array[Dictionary] = []
|
||||
if root != null:
|
||||
if active_tool == TOOL_WALL or active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT or active_tool == TOOL_FLOOR_SQUARE or active_tool == TOOL_FLOOR_TRIANGLE or active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_SHED or active_tool == TOOL_ROOF_PYRAMID or active_tool == TOOL_STAIRS or active_tool == TOOL_FENCE or active_tool == TOOL_PILLAR or active_tool == TOOL_MERGE_VERTICES or active_tool == TOOL_CREATE_VERTEX or active_tool == TOOL_MOVE_VERTEX or active_tool == TOOL_REMOVE_VERTEX:
|
||||
vertex_points = _get_tool_vertices(root)
|
||||
elif active_tool == TOOL_FOUNDATION:
|
||||
side_points = _get_side_button_points(root)
|
||||
if root != _marker_root or (root == null and _marker_container != null) or vertex_points.size() != _marker_point_nodes.size() or side_points.size() != _side_button_nodes.size():
|
||||
_clear_marker_container()
|
||||
_marker_root = root
|
||||
if root == null:
|
||||
return
|
||||
_marker_container = Node3D.new()
|
||||
_marker_container.name = "ModularBuilderMarkers"
|
||||
root.add_child(_marker_container)
|
||||
for _point in vertex_points:
|
||||
var marker := Label3D.new()
|
||||
marker.text = "●"
|
||||
marker.font_size = 56
|
||||
marker.pixel_size = 0.004
|
||||
marker.horizontal_alignment = HORIZONTAL_ALIGNMENT_CENTER
|
||||
marker.vertical_alignment = VERTICAL_ALIGNMENT_CENTER
|
||||
marker.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
marker.no_depth_test = true
|
||||
marker.modulate = Color(0.05, 0.58, 1.0)
|
||||
marker.outline_size = 8
|
||||
marker.outline_modulate = Color.BLACK
|
||||
_marker_container.add_child(marker)
|
||||
_marker_point_nodes.append(marker)
|
||||
for _button in side_points:
|
||||
var label := Label3D.new()
|
||||
label.text = "+"
|
||||
label.font_size = 56
|
||||
label.outline_size = 8
|
||||
label.outline_modulate = Color.BLACK
|
||||
label.pixel_size = 0.01
|
||||
label.billboard = BaseMaterial3D.BILLBOARD_ENABLED
|
||||
label.no_depth_test = true
|
||||
label.modulate = Color(0.2, 1.0, 0.55)
|
||||
_marker_container.add_child(label)
|
||||
_side_button_nodes.append(label)
|
||||
for i in range(vertex_points.size()):
|
||||
_marker_point_nodes[i].position = root.to_local(vertex_points[i])
|
||||
var is_pending := false
|
||||
for pending_point in pending_points:
|
||||
if pending_point.distance_to(vertex_points[i]) < 0.025:
|
||||
is_pending = true
|
||||
break
|
||||
_marker_point_nodes[i].modulate = Color(1.0, 0.88, 0.08) if is_pending else Color(0.05, 0.58, 1.0)
|
||||
for i in range(side_points.size()):
|
||||
_side_button_nodes[i].position = root.to_local(side_points[i].position)
|
||||
if _marker_container:
|
||||
_marker_container.visible = active_tool != TOOL_NONE or not side_points.is_empty()
|
||||
|
||||
func _clear_marker_container() -> void:
|
||||
if is_instance_valid(_marker_container):
|
||||
_marker_container.queue_free()
|
||||
_marker_container = null
|
||||
_marker_root = null
|
||||
_marker_point_nodes.clear()
|
||||
_side_button_nodes.clear()
|
||||
|
||||
func _get_side_button_points(root: ModularBuilderPiece) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
for foundation in _get_foundations(root):
|
||||
var top_y := foundation.thickness * 0.5 if foundation.centered_origin else foundation.thickness
|
||||
var y := top_y + 0.22
|
||||
var points := _foundation_polygon(foundation)
|
||||
var centroid := Vector2.ZERO
|
||||
for point in points:
|
||||
centroid += point
|
||||
centroid /= points.size()
|
||||
for i in range(points.size()):
|
||||
var a: Vector2 = points[i]
|
||||
var b: Vector2 = points[(i + 1) % points.size()]
|
||||
if _foundation_edge_is_occupied(root, foundation, a, b):
|
||||
continue
|
||||
var midpoint := (a + b) * 0.5
|
||||
var outward := (midpoint - centroid).normalized()
|
||||
var edge_length := a.distance_to(b)
|
||||
var neighbor_polygon: Array[Vector2] = []
|
||||
if _foundation_type_selected == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE:
|
||||
var square_side := minf(foundation.unit_size, edge_length)
|
||||
var edge_direction := (b - a).normalized()
|
||||
var square_a := midpoint - edge_direction * square_side * 0.5
|
||||
var square_b := midpoint + edge_direction * square_side * 0.5
|
||||
neighbor_polygon = [square_a, square_b, square_b + outward * square_side, square_a + outward * square_side]
|
||||
elif points.size() == 3:
|
||||
for source_point in points:
|
||||
neighbor_polygon.append(_reflect_point_across_edge(source_point, a, b))
|
||||
else:
|
||||
neighbor_polygon = [a, b, a + outward * edge_length]
|
||||
var custom_points := PackedVector3Array()
|
||||
for neighbor_point in neighbor_polygon:
|
||||
var base_y := -foundation.thickness * 0.5 if foundation.centered_origin else 0.0
|
||||
var foundation_local := Vector3(neighbor_point.x, base_y, neighbor_point.y)
|
||||
custom_points.append(root.to_local(foundation.to_global(foundation_local)))
|
||||
result.append({
|
||||
"position": foundation.to_global(Vector3(midpoint.x + outward.x * 0.55, y, midpoint.y + outward.y * 0.55)),
|
||||
"custom_points": custom_points,
|
||||
"foundation": foundation
|
||||
})
|
||||
return result
|
||||
|
||||
func _foundation_edge_is_occupied(root: ModularBuilderPiece, foundation: ModularBuilderPiece, edge_a: Vector2, edge_b: Vector2) -> bool:
|
||||
var foundation_base_y := -foundation.thickness * 0.5 if foundation.centered_origin else 0.0
|
||||
var edge_a_3d := root.to_local(foundation.to_global(Vector3(edge_a.x, foundation_base_y, edge_a.y)))
|
||||
var edge_b_3d := root.to_local(foundation.to_global(Vector3(edge_b.x, foundation_base_y, edge_b.y)))
|
||||
var target_a := Vector2(edge_a_3d.x, edge_a_3d.z)
|
||||
var target_b := Vector2(edge_b_3d.x, edge_b_3d.z)
|
||||
for other in _get_foundations(root):
|
||||
if other == foundation:
|
||||
continue
|
||||
var other_polygon := _foundation_polygon(other)
|
||||
for i in range(other_polygon.size()):
|
||||
var a := other_polygon[i]
|
||||
var b := other_polygon[(i + 1) % other_polygon.size()]
|
||||
var other_base_y := -other.thickness * 0.5 if other.centered_origin else 0.0
|
||||
var a_in_root := root.to_local(other.to_global(Vector3(a.x, other_base_y, a.y)))
|
||||
var b_in_root := root.to_local(other.to_global(Vector3(b.x, other_base_y, b.y)))
|
||||
var other_a := Vector2(a_in_root.x, a_in_root.z)
|
||||
var other_b := Vector2(b_in_root.x, b_in_root.z)
|
||||
if absf(a_in_root.y - edge_a_3d.y) < 0.02 and absf(b_in_root.y - edge_b_3d.y) < 0.02 and _segments_overlap(target_a, target_b, other_a, other_b):
|
||||
return true
|
||||
return false
|
||||
|
||||
func _get_foundations(root: ModularBuilderPiece) -> Array[ModularBuilderPiece]:
|
||||
var result: Array[ModularBuilderPiece] = []
|
||||
if root.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or root.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
result.append(root)
|
||||
for child in root.get_children():
|
||||
_collect_foundations(child, result)
|
||||
return result
|
||||
|
||||
func _collect_foundations(node: Node, result: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and (node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or node.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE):
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_foundations(child, result)
|
||||
|
||||
func _reflect_point_across_edge(point: Vector2, a: Vector2, b: Vector2) -> Vector2:
|
||||
var edge := b - a
|
||||
var projection := a + edge * ((point - a).dot(edge) / edge.length_squared())
|
||||
return projection * 2.0 - point
|
||||
|
||||
func _nearest_side_button(camera: Camera3D, screen_position: Vector2) -> int:
|
||||
var root := _get_selected_piece()
|
||||
if root == null:
|
||||
return -1
|
||||
var buttons := _get_side_button_points(root)
|
||||
var closest := MARKER_HIT_RADIUS + 5.0
|
||||
var result := -1
|
||||
for i in range(buttons.size()):
|
||||
var point: Vector3 = buttons[i].position
|
||||
if camera.is_position_behind(point):
|
||||
continue
|
||||
var distance := camera.unproject_position(point).distance_to(screen_position)
|
||||
if distance < closest:
|
||||
closest = distance
|
||||
result = i
|
||||
return result
|
||||
|
||||
func _add_foundation_at_side(root: ModularBuilderPiece, side: int) -> void:
|
||||
var buttons := _get_side_button_points(root)
|
||||
if side < 0 or side >= buttons.size():
|
||||
return
|
||||
var scene_root := get_editor_interface().get_edited_scene_root() as Node3D
|
||||
var piece := Node3D.new()
|
||||
piece.name = "Foundation"
|
||||
piece.set_script(PIECE_SCRIPT)
|
||||
var source_foundation: ModularBuilderPiece = buttons[side].foundation
|
||||
piece.set("unit_size", source_foundation.unit_size)
|
||||
piece.set("thickness", source_foundation.thickness)
|
||||
piece.set("centered_origin", true)
|
||||
piece.set("piece_type", _foundation_type_selected)
|
||||
var points_in_root: PackedVector3Array = buttons[side].custom_points
|
||||
var pivot := _packed_points_bounds_center(points_in_root)
|
||||
pivot.y += source_foundation.thickness * 0.5
|
||||
var custom_points := PackedVector3Array()
|
||||
for point in points_in_root:
|
||||
custom_points.append(point - pivot)
|
||||
piece.position = pivot
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Add foundation to selected side")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "custom_points", custom_points)
|
||||
undo.add_do_property(piece, "owner", scene_root)
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
status_label.text = "Foundation added to structure."
|
||||
|
||||
func _packed_points_bounds_center(points: PackedVector3Array) -> Vector3:
|
||||
if points.is_empty():
|
||||
return Vector3.ZERO
|
||||
var minimum := points[0]
|
||||
var maximum := points[0]
|
||||
for point in points:
|
||||
minimum = minimum.min(point)
|
||||
maximum = maximum.max(point)
|
||||
return (minimum + maximum) * 0.5
|
||||
|
||||
func _get_tool_vertices(root: ModularBuilderPiece) -> Array[Vector3]:
|
||||
var result: Array[Vector3] = []
|
||||
if active_tool == TOOL_MERGE_VERTICES:
|
||||
_collect_all_connection_vertices(root, result)
|
||||
elif active_tool == TOOL_CREATE_VERTEX or active_tool == TOOL_MOVE_VERTEX or active_tool == TOOL_REMOVE_VERTEX:
|
||||
_collect_all_connection_vertices(root, result)
|
||||
elif active_tool == TOOL_STAIRS:
|
||||
_collect_stair_vertices(root, result)
|
||||
elif active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET:
|
||||
_collect_fence_vertices(root, result)
|
||||
elif active_tool == TOOL_PILLAR:
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for piece in surfaces:
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.BUILDING:
|
||||
continue
|
||||
var polygon := _piece_polygon_xz(piece) if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR else _foundation_polygon(piece)
|
||||
var top_y := piece.custom_points[0].y if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR and not piece.custom_points.is_empty() else (piece.thickness * 0.5 if piece.centered_origin else piece.thickness)
|
||||
var bottom_y := top_y - piece.thickness
|
||||
for point in polygon:
|
||||
_add_unique_vertex(result, piece.to_global(Vector3(point.x, top_y, point.y)))
|
||||
_add_unique_vertex(result, piece.to_global(Vector3(point.x, bottom_y, point.y)))
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for offset in [Vector3.ZERO, Vector3.UP * wall.unit_size]:
|
||||
_add_unique_vertex(result, wall.to_global(endpoint + offset + side))
|
||||
_add_unique_vertex(result, wall.to_global(endpoint + offset - side))
|
||||
_add_gable_wall_vertices(root, result)
|
||||
elif active_tool == TOOL_WALL:
|
||||
for foundation in _get_foundations(root):
|
||||
var top_y := foundation.thickness * 0.5 if foundation.centered_origin else foundation.thickness
|
||||
for point in _foundation_polygon(foundation):
|
||||
_add_unique_vertex(result, foundation.to_global(Vector3(point.x, top_y, point.y)))
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for surface in surfaces:
|
||||
if surface.piece_type != ModularBuilderPiece.PieceType.FLOOR:
|
||||
continue
|
||||
for point in surface.custom_points:
|
||||
_add_unique_vertex(result, surface.to_global(point))
|
||||
var existing_walls: Array[Node] = []
|
||||
_collect_walls(root, existing_walls)
|
||||
for wall_node in existing_walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
var top := endpoint + Vector3.UP * wall.unit_size
|
||||
_add_unique_vertex(result, wall.to_global(top + side))
|
||||
_add_unique_vertex(result, wall.to_global(top - side))
|
||||
_add_gable_wall_vertices(root, result)
|
||||
else:
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * float(wall.get("thickness")) * 0.5
|
||||
for endpoint in endpoints:
|
||||
if active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_SHED:
|
||||
_add_unique_vertex(result, wall.to_global(endpoint + side))
|
||||
_add_unique_vertex(result, wall.to_global(endpoint - side))
|
||||
var top := endpoint + Vector3.UP * float(wall.get("unit_size"))
|
||||
_add_unique_vertex(result, wall.to_global(top + side))
|
||||
_add_unique_vertex(result, wall.to_global(top - side))
|
||||
_add_gable_wall_vertices(root, result)
|
||||
_collect_detail_connection_vertices(root, result)
|
||||
for manual_vertex in _get_manual_vertices(root):
|
||||
_add_unique_vertex(result, manual_vertex)
|
||||
_collapse_merged_vertex_groups(root, result)
|
||||
if _marker_scope_check != null and _marker_scope_check.button_pressed:
|
||||
var scope_pieces := _get_marker_scope_pieces(root)
|
||||
var scope_vertices: Array[Vector3] = []
|
||||
for piece in scope_pieces:
|
||||
_collect_piece_geometry_vertices(piece, scope_vertices)
|
||||
for manual_vertex in _get_manual_vertices(piece):
|
||||
_add_unique_vertex(scope_vertices, manual_vertex)
|
||||
for selected_node in get_editor_interface().get_selection().get_selected_nodes():
|
||||
if selected_node is ModularBuilderDetail:
|
||||
_collect_detail_connection_vertices(selected_node, scope_vertices)
|
||||
_collapse_merged_vertex_groups(root, scope_vertices)
|
||||
var filtered: Array[Vector3] = []
|
||||
for vertex in result:
|
||||
for scope_vertex in scope_vertices:
|
||||
if vertex.distance_to(scope_vertex) < 0.025:
|
||||
_add_unique_vertex(filtered, vertex)
|
||||
break
|
||||
result = filtered
|
||||
if _simplify_vertices_check != null and _simplify_vertices_check.button_pressed and active_tool != TOOL_MERGE_VERTICES and active_tool != TOOL_CREATE_VERTEX and active_tool != TOOL_MOVE_VERTEX and active_tool != TOOL_REMOVE_VERTEX:
|
||||
result = _simplify_tool_vertices(root, result)
|
||||
return result
|
||||
|
||||
func _simplify_tool_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> Array[Vector3]:
|
||||
var replacements := {}
|
||||
var pieces: Array[ModularBuilderPiece] = []
|
||||
_collect_all_modular_pieces(root, pieces)
|
||||
for piece in pieces:
|
||||
if piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_SQUARE and piece.piece_type != ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE and piece.piece_type != ModularBuilderPiece.PieceType.FLOOR and piece.piece_type != ModularBuilderPiece.PieceType.WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL and piece.piece_type != ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
continue
|
||||
var piece_vertices: Array[Vector3] = []
|
||||
_collect_piece_geometry_vertices(piece, piece_vertices)
|
||||
for original in piece_vertices:
|
||||
var key := _vertex_position_key(original)
|
||||
if not replacements.has(key):
|
||||
replacements[key] = VERTEX_SIMPLIFIER.piece_vertex(piece, original)
|
||||
_add_pillar_simplification_candidates(root, replacements)
|
||||
var result: Array[Vector3] = []
|
||||
for original in vertices:
|
||||
_add_unique_vertex(result, replacements.get(_vertex_position_key(original), original))
|
||||
return result
|
||||
|
||||
func _add_pillar_simplification_candidates(node: Node, replacements: Dictionary) -> void:
|
||||
if node is ModularBuilderDetail:
|
||||
var detail := node as ModularBuilderDetail
|
||||
if detail.detail_type == ModularBuilderDetail.DetailType.PILLAR:
|
||||
var half := detail.width * 0.5
|
||||
for x in [-half, half]:
|
||||
for y in [-detail.height * 0.5, detail.height * 0.5]:
|
||||
for z in [-half, half]:
|
||||
var original := detail.to_global(Vector3(x, y, z))
|
||||
var key := _vertex_position_key(original)
|
||||
if not replacements.has(key):
|
||||
replacements[key] = VERTEX_SIMPLIFIER.pillar_vertex(detail, original)
|
||||
for child in node.get_children():
|
||||
_add_pillar_simplification_candidates(child, replacements)
|
||||
|
||||
func _vertex_position_key(point: Vector3) -> Vector3i:
|
||||
return Vector3i(roundi(point.x * 1000.0), roundi(point.y * 1000.0), roundi(point.z * 1000.0))
|
||||
|
||||
func _collect_stair_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for surface in surfaces:
|
||||
var polygon := _piece_polygon_xz(surface) if surface.piece_type == ModularBuilderPiece.PieceType.FLOOR else _foundation_polygon(surface)
|
||||
var y := surface.thickness * 0.5 if surface.centered_origin else surface.thickness
|
||||
for point in polygon:
|
||||
_add_unique_vertex(vertices, surface.to_global(Vector3(point.x, y, point.y)))
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
var top := endpoint + Vector3.UP * wall.unit_size
|
||||
_add_unique_vertex(vertices, wall.to_global(top + side))
|
||||
_add_unique_vertex(vertices, wall.to_global(top - side))
|
||||
_add_gable_wall_vertices(root, vertices)
|
||||
|
||||
func _collect_fence_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for surface in surfaces:
|
||||
var polygon := _piece_polygon_xz(surface) if surface.piece_type == ModularBuilderPiece.PieceType.FLOOR else _foundation_polygon(surface)
|
||||
var top_y := surface.thickness * 0.5 if surface.centered_origin else surface.thickness
|
||||
for point in polygon:
|
||||
_add_unique_vertex(vertices, surface.to_global(Vector3(point.x, top_y, point.y)))
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for height in [0.0, wall.unit_size]:
|
||||
_add_unique_vertex(vertices, wall.to_global(endpoint + Vector3.UP * height + side))
|
||||
_add_unique_vertex(vertices, wall.to_global(endpoint + Vector3.UP * height - side))
|
||||
_add_gable_wall_vertices(root, vertices)
|
||||
|
||||
func _get_marker_scope_pieces(root: ModularBuilderPiece) -> Array[ModularBuilderPiece]:
|
||||
var result: Array[ModularBuilderPiece] = []
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
for selected_node in selected:
|
||||
var piece := selected_node as ModularBuilderPiece
|
||||
if piece == null and selected_node is ModularBuilderDetail:
|
||||
var parent := selected_node.get_parent()
|
||||
while parent != null and not parent is ModularBuilderPiece:
|
||||
parent = parent.get_parent()
|
||||
piece = parent as ModularBuilderPiece
|
||||
if piece == root:
|
||||
_collect_all_modular_pieces(root, result)
|
||||
return result
|
||||
if piece != null and _is_descendant_of_piece(piece, root):
|
||||
if not result.has(piece):
|
||||
result.append(piece)
|
||||
return result
|
||||
|
||||
func _collect_all_modular_pieces(node: Node, result: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and not result.has(node):
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_all_modular_pieces(child, result)
|
||||
|
||||
func _is_descendant_of_piece(node: Node, ancestor: Node) -> bool:
|
||||
var current := node.get_parent()
|
||||
while current != null:
|
||||
if current == ancestor:
|
||||
return true
|
||||
current = current.get_parent()
|
||||
return false
|
||||
|
||||
func _collect_piece_geometry_vertices(piece: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_SQUARE or piece.piece_type == ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
var top_y := piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
var bottom_y := -piece.thickness * 0.5 if piece.centered_origin else 0.0
|
||||
for point in _foundation_polygon(piece):
|
||||
_add_unique_vertex(vertices, piece.to_global(Vector3(point.x, top_y, point.y)))
|
||||
_add_unique_vertex(vertices, piece.to_global(Vector3(point.x, bottom_y, point.y)))
|
||||
elif piece.piece_type == ModularBuilderPiece.PieceType.FLOOR:
|
||||
for point in piece.custom_points:
|
||||
_add_unique_vertex(vertices, piece.to_global(point))
|
||||
_add_unique_vertex(vertices, piece.to_global(point - Vector3.UP * piece.thickness))
|
||||
elif piece.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
var endpoints: PackedVector3Array = piece.call("_get_centered_wall_points") if piece.center_align else piece.custom_points
|
||||
if endpoints.size() < 2:
|
||||
return
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * piece.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for height in [0.0, piece.unit_size]:
|
||||
_add_unique_vertex(vertices, piece.to_global(endpoint + Vector3.UP * height + side))
|
||||
_add_unique_vertex(vertices, piece.to_global(endpoint + Vector3.UP * height - side))
|
||||
elif piece.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL or piece.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
_add_single_gable_wall_vertices(piece, vertices)
|
||||
elif piece.piece_type == ModularBuilderPiece.PieceType.ROOF_GABLE or piece.piece_type == ModularBuilderPiece.PieceType.ROOF_SHED or piece.piece_type == ModularBuilderPiece.PieceType.ROOF_PYRAMID:
|
||||
for point in piece.custom_points:
|
||||
_add_unique_vertex(vertices, piece.to_global(point))
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.ROOF_PYRAMID and piece.custom_points.size() >= 4:
|
||||
var base_center := (piece.custom_points[0] + piece.custom_points[1] + piece.custom_points[2] + piece.custom_points[3]) * 0.25
|
||||
_add_unique_vertex(vertices, piece.to_global(base_center + Vector3.UP * piece.rise))
|
||||
|
||||
func _collect_all_connection_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
var surfaces: Array[ModularBuilderPiece] = []
|
||||
_collect_surface_pieces(root, surfaces)
|
||||
for piece in surfaces:
|
||||
var polygon := _piece_polygon_xz(piece) if piece.piece_type == ModularBuilderPiece.PieceType.FLOOR else _foundation_polygon(piece)
|
||||
var top_y := piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
var bottom_y := -piece.thickness * 0.5 if piece.centered_origin else 0.0
|
||||
for point in polygon:
|
||||
_add_unique_vertex(vertices, piece.to_global(Vector3(point.x, top_y, point.y)))
|
||||
_add_unique_vertex(vertices, piece.to_global(Vector3(point.x, bottom_y, point.y)))
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x) * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for height in [0.0, wall.unit_size]:
|
||||
_add_unique_vertex(vertices, wall.to_global(endpoint + Vector3.UP * height + side))
|
||||
_add_unique_vertex(vertices, wall.to_global(endpoint + Vector3.UP * height - side))
|
||||
_add_gable_wall_vertices(root, vertices)
|
||||
_collect_detail_connection_vertices(root, vertices)
|
||||
var roofs: Array[ModularBuilderPiece] = []
|
||||
_collect_roof_pieces(root, roofs)
|
||||
for roof in roofs:
|
||||
for point in roof.custom_points:
|
||||
_add_unique_vertex(vertices, roof.to_global(point))
|
||||
if roof.piece_type == ModularBuilderPiece.PieceType.ROOF_PYRAMID and roof.custom_points.size() >= 4:
|
||||
var base_center := (roof.custom_points[0] + roof.custom_points[1] + roof.custom_points[2] + roof.custom_points[3]) * 0.25
|
||||
_add_unique_vertex(vertices, roof.to_global(base_center + Vector3.UP * roof.rise))
|
||||
|
||||
func _collect_roof_pieces(node: Node, roofs: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and (node.piece_type == ModularBuilderPiece.PieceType.ROOF_GABLE or node.piece_type == ModularBuilderPiece.PieceType.ROOF_SHED or node.piece_type == ModularBuilderPiece.PieceType.ROOF_PYRAMID):
|
||||
roofs.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_roof_pieces(child, roofs)
|
||||
|
||||
func _collect_detail_connection_vertices(node: Node, vertices: Array[Vector3]) -> void:
|
||||
if node is ModularBuilderDetail:
|
||||
var detail := node as ModularBuilderDetail
|
||||
if detail.detail_type == ModularBuilderDetail.DetailType.PILLAR:
|
||||
var half := detail.width * 0.5
|
||||
for x in [-half, half]:
|
||||
for y in [-detail.height * 0.5, detail.height * 0.5]:
|
||||
for z in [-half, half]:
|
||||
_add_unique_vertex(vertices, detail.to_global(Vector3(x, y, z)))
|
||||
elif (detail.detail_type == ModularBuilderDetail.DetailType.FENCE or detail.detail_type == ModularBuilderDetail.DetailType.PARAPET) and detail.path_points.size() >= 2:
|
||||
for point in detail.path_points:
|
||||
_add_unique_vertex(vertices, detail.to_global(point))
|
||||
for local_point in detail.get_meta(CUSTOM_VERTICES_META, []):
|
||||
_add_unique_vertex(vertices, detail.to_global(local_point))
|
||||
for child in node.get_children():
|
||||
_collect_detail_connection_vertices(child, vertices)
|
||||
|
||||
func _collapse_merged_vertex_groups(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
if not root.has_meta(MERGED_VERTEX_GROUPS_META):
|
||||
return
|
||||
var groups: Array = root.get_meta(MERGED_VERTEX_GROUPS_META, [])
|
||||
for group_value in groups:
|
||||
if not group_value is Dictionary:
|
||||
continue
|
||||
var group: Dictionary = group_value
|
||||
var members: PackedVector3Array = group.get("members", PackedVector3Array())
|
||||
var anchor: Vector3 = group.get("position", Vector3.ZERO)
|
||||
var had_member := false
|
||||
for i in range(vertices.size() - 1, -1, -1):
|
||||
var local_vertex := root.to_local(vertices[i])
|
||||
for member in members:
|
||||
if local_vertex.distance_to(member) <= 0.025:
|
||||
vertices.remove_at(i)
|
||||
had_member = true
|
||||
break
|
||||
if had_member:
|
||||
_add_unique_vertex(vertices, root.to_global(anchor))
|
||||
|
||||
func _merge_members_for_point(root: ModularBuilderPiece, world_point: Vector3) -> PackedVector3Array:
|
||||
var local_point := root.to_local(world_point)
|
||||
var groups: Array = root.get_meta(MERGED_VERTEX_GROUPS_META, [])
|
||||
for group_value in groups:
|
||||
if not group_value is Dictionary:
|
||||
continue
|
||||
var group: Dictionary = group_value
|
||||
var anchor: Vector3 = group.get("position", Vector3.ZERO)
|
||||
if local_point.distance_to(anchor) <= 0.04:
|
||||
return group.get("members", PackedVector3Array())
|
||||
return PackedVector3Array([local_point])
|
||||
|
||||
func _finish_merge_vertices(root: ModularBuilderPiece) -> void:
|
||||
if pending_points.size() < 2:
|
||||
return
|
||||
var selected_members := _merge_members_for_point(root, pending_points[0])
|
||||
for member in _merge_members_for_point(root, pending_points[1]):
|
||||
var found := false
|
||||
for existing in selected_members:
|
||||
if existing.distance_to(member) <= 0.025:
|
||||
found = true
|
||||
break
|
||||
if not found:
|
||||
selected_members.append(member)
|
||||
if selected_members.size() < 2:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two different vertices to merge."
|
||||
update_overlays()
|
||||
return
|
||||
var old_groups: Array = root.get_meta(MERGED_VERTEX_GROUPS_META, []).duplicate(true)
|
||||
var new_groups: Array = []
|
||||
for group_value in old_groups:
|
||||
if not group_value is Dictionary:
|
||||
continue
|
||||
var group: Dictionary = group_value
|
||||
var members: PackedVector3Array = group.get("members", PackedVector3Array())
|
||||
var overlaps := false
|
||||
for member in members:
|
||||
for selected in selected_members:
|
||||
if member.distance_to(selected) <= 0.025:
|
||||
overlaps = true
|
||||
break
|
||||
if overlaps:
|
||||
break
|
||||
if not overlaps:
|
||||
new_groups.append(group)
|
||||
var center := Vector3.ZERO
|
||||
for member in selected_members:
|
||||
center += member
|
||||
center /= selected_members.size()
|
||||
new_groups.append({"members": selected_members, "position": center})
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Merge vertex markers")
|
||||
undo.add_do_method(root, "set_meta", MERGED_VERTEX_GROUPS_META, new_groups)
|
||||
undo.add_undo_method(root, "set_meta", MERGED_VERTEX_GROUPS_META, old_groups)
|
||||
undo.commit_action()
|
||||
pending_points.clear()
|
||||
status_label.text = "Markers merged. Ctrl+Z undoes the merge."
|
||||
update_overlays()
|
||||
|
||||
func _add_gable_wall_vertices(root: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
var gable_walls: Array[Node] = []
|
||||
_collect_gable_walls(root, gable_walls)
|
||||
for node in gable_walls:
|
||||
_add_single_gable_wall_vertices(node as ModularBuilderPiece, vertices)
|
||||
|
||||
func _add_single_gable_wall_vertices(gable: ModularBuilderPiece, vertices: Array[Vector3]) -> void:
|
||||
if gable.custom_points.size() < 2:
|
||||
return
|
||||
var base_points: PackedVector3Array = gable.call("_get_centered_gable_base") if gable.center_align else gable.custom_points
|
||||
var a: Vector3 = base_points[0]
|
||||
var b: Vector3 = base_points[1]
|
||||
var direction := (b - a).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * gable.thickness * 0.5
|
||||
var peak := (a + b) * 0.5 + Vector3.UP * gable.rise if gable.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL else b + Vector3.UP * gable.rise
|
||||
for endpoint in [a, b]:
|
||||
_add_unique_vertex(vertices, gable.to_global(endpoint + side))
|
||||
_add_unique_vertex(vertices, gable.to_global(endpoint - side))
|
||||
_add_unique_vertex(vertices, gable.to_global(peak + side))
|
||||
_add_unique_vertex(vertices, gable.to_global(peak - side))
|
||||
|
||||
func _collect_gable_walls(node: Node, gable_walls: Array[Node]) -> void:
|
||||
if node is ModularBuilderPiece and (node.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL or node.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT) and not gable_walls.has(node):
|
||||
gable_walls.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_gable_walls(child, gable_walls)
|
||||
|
||||
func _collect_walls(node: Node, walls: Array[Node]) -> void:
|
||||
if node is ModularBuilderPiece and node.piece_type == ModularBuilderPiece.PieceType.WALL and not walls.has(node):
|
||||
walls.append(node)
|
||||
for child in node.get_children():
|
||||
if child is ModularBuilderPiece and child.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
if not walls.has(child):
|
||||
walls.append(child)
|
||||
_collect_walls(child, walls)
|
||||
|
||||
func _add_unique_vertex(vertices: Array[Vector3], point: Vector3) -> void:
|
||||
for existing in vertices:
|
||||
if existing.distance_to(point) < 0.02:
|
||||
return
|
||||
vertices.append(point)
|
||||
|
||||
func _get_manual_vertices(piece: ModularBuilderPiece) -> Array[Vector3]:
|
||||
var result: Array[Vector3] = []
|
||||
var owners: Array[Node3D] = []
|
||||
_collect_manual_vertex_owners(piece, owners)
|
||||
for owner_node in owners:
|
||||
for local_point_value in owner_node.get_meta(CUSTOM_VERTICES_META, []):
|
||||
var local_point: Vector3 = local_point_value
|
||||
result.append(owner_node.to_global(local_point))
|
||||
return result
|
||||
|
||||
func _collect_manual_vertex_owners(node: Node, owners: Array[Node3D]) -> void:
|
||||
if node is ModularBuilderPiece or node is ModularBuilderDetail:
|
||||
owners.append(node as Node3D)
|
||||
for child in node.get_children():
|
||||
_collect_manual_vertex_owners(child, owners)
|
||||
|
||||
func _raycast_piece_mesh(root: ModularBuilderPiece, camera: Camera3D, screen_position: Vector2) -> Variant:
|
||||
return MESH_PICKER.raycast(root, camera, screen_position)
|
||||
|
||||
func _commit_manual_vertices(owner_node: Node3D, new_vertices: Array, action_name: String) -> void:
|
||||
var old_vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, []).duplicate()
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action(action_name)
|
||||
undo.add_do_method(owner_node, "set_meta", CUSTOM_VERTICES_META, new_vertices)
|
||||
undo.add_undo_method(owner_node, "set_meta", CUSTOM_VERTICES_META, old_vertices)
|
||||
undo.commit_action()
|
||||
|
||||
func _create_manual_vertex(root: ModularBuilderPiece, world_position: Vector3) -> void:
|
||||
var owner_node := _get_vertex_attachment_node(root)
|
||||
var local_position := owner_node.to_local(world_position)
|
||||
var vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, []).duplicate()
|
||||
for existing in vertices:
|
||||
var existing_position: Vector3 = existing
|
||||
if existing_position.distance_to(local_position) < 0.025:
|
||||
status_label.text = "A manual vertex already exists there."
|
||||
return
|
||||
vertices.append(local_position)
|
||||
_commit_manual_vertices(owner_node, vertices, "Create connection vertex")
|
||||
status_label.text = "Connection vertex created and linked to the selected object."
|
||||
|
||||
func _move_manual_vertex(root: ModularBuilderPiece, old_world_position: Vector3, new_world_position: Vector3) -> void:
|
||||
var owner_record := _find_manual_vertex_record(root, old_world_position)
|
||||
if owner_record.is_empty():
|
||||
pending_points.clear()
|
||||
status_label.text = "The selected vertex is no longer available."
|
||||
return
|
||||
var owner_node: Node3D = owner_record.node
|
||||
var vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, []).duplicate()
|
||||
vertices[owner_record.index] = owner_node.to_local(new_world_position)
|
||||
pending_points.clear()
|
||||
_commit_manual_vertices(owner_node, vertices, "Move connection vertex")
|
||||
status_label.text = "Vertex moved."
|
||||
|
||||
func _remove_manual_vertex(root: ModularBuilderPiece, world_position: Vector3) -> void:
|
||||
var owner_record := _find_manual_vertex_record(root, world_position)
|
||||
if owner_record.is_empty():
|
||||
return
|
||||
var owner_node: Node3D = owner_record.node
|
||||
var vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, []).duplicate()
|
||||
vertices.remove_at(owner_record.index)
|
||||
_commit_manual_vertices(owner_node, vertices, "Remove connection vertex")
|
||||
status_label.text = "Vertex removed."
|
||||
|
||||
func _get_vertex_attachment_node(root: ModularBuilderPiece) -> Node3D:
|
||||
var selected := get_editor_interface().get_selection().get_selected_nodes()
|
||||
if selected.size() == 1:
|
||||
var selected_piece := selected[0] as ModularBuilderPiece
|
||||
if selected_piece != null and (selected_piece == root or _is_descendant_of_piece(selected_piece, root)):
|
||||
return selected_piece
|
||||
if selected[0] is ModularBuilderDetail and _is_descendant_of_piece(selected[0], root):
|
||||
return selected[0] as ModularBuilderDetail
|
||||
return root
|
||||
|
||||
func _find_manual_vertex_record(root: ModularBuilderPiece, world_position: Vector3) -> Dictionary:
|
||||
var owners: Array[Node3D] = []
|
||||
_collect_manual_vertex_owners(root, owners)
|
||||
var nearest_record := {}
|
||||
var nearest_distance := 0.05
|
||||
for owner_node in owners:
|
||||
var vertices: Array = owner_node.get_meta(CUSTOM_VERTICES_META, [])
|
||||
for i in range(vertices.size()):
|
||||
var local_point: Vector3 = vertices[i]
|
||||
var distance := owner_node.to_global(local_point).distance_to(world_position)
|
||||
if distance < nearest_distance:
|
||||
nearest_distance = distance
|
||||
nearest_record = {"node": owner_node, "index": i}
|
||||
return nearest_record
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://ghgythev6vkt
|
||||
637
addons/modular_builder_en/modular_builder_plugin.gd
Normal file
|
|
@ -0,0 +1,637 @@
|
|||
@tool
|
||||
extends "res://addons/modular_builder_en/modular_builder_editor_vertices.gd"
|
||||
|
||||
const ROOF_SUPPORT = preload("res://addons/modular_builder_en/modular_roof_support.gd")
|
||||
|
||||
func _forward_3d_gui_input(camera: Camera3D, event: InputEvent) -> int:
|
||||
if active_tool != TOOL_NONE and event is InputEventKey and event.pressed and event.keycode == KEY_ESCAPE:
|
||||
pending_points.clear()
|
||||
_editor_sync_elapsed = 1.0
|
||||
status_label.text = "Current points cleared."
|
||||
update_overlays()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if _is_detail_tool(active_tool) and event is InputEventMouseMotion:
|
||||
_preview_camera = camera
|
||||
_preview_screen_position = event.position
|
||||
_update_detail_preview()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if not (event is InputEventMouseButton and event.pressed and event.button_index == MOUSE_BUTTON_LEFT):
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
_editor_sync_elapsed = 1.0
|
||||
var root := _get_selected_piece()
|
||||
if root == null:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if active_tool == TOOL_FOUNDATION:
|
||||
var side := _nearest_side_button(camera, event.position)
|
||||
if side >= 0:
|
||||
_add_foundation_at_side(root, side)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if active_tool == TOOL_NONE:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if active_tool == TOOL_CREATE_VERTEX:
|
||||
var create_position := _raycast_piece_mesh(root, camera, event.position)
|
||||
if create_position == null:
|
||||
status_label.text = "Click a structure surface to create a connection vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
_create_manual_vertex(root, create_position)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_MOVE_VERTEX:
|
||||
if pending_points.is_empty():
|
||||
var selected_vertex := _nearest_vertex(camera, event.position, _get_manual_vertices(root))
|
||||
if selected_vertex == null:
|
||||
status_label.text = "Click a blue manual vertex to select it."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
pending_points.append(selected_vertex)
|
||||
status_label.text = "Vertex selected. Click the surface where you want to move it."
|
||||
else:
|
||||
var move_position := _raycast_piece_mesh(root, camera, event.position)
|
||||
if move_position == null:
|
||||
status_label.text = "Click a structure surface to move the vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
_move_manual_vertex(root, pending_points[0], move_position)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_REMOVE_VERTEX:
|
||||
var remove_position := _nearest_vertex(camera, event.position, _get_manual_vertices(root))
|
||||
if remove_position == null:
|
||||
status_label.text = "Click a blue manual vertex to remove it."
|
||||
else:
|
||||
_remove_manual_vertex(root, remove_position)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_PILLAR:
|
||||
var pillar_vertex := _nearest_vertex(camera, event.position, _get_tool_vertices(root))
|
||||
if pillar_vertex == null:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
if event.alt_pressed:
|
||||
_create_vertical_pillar_from_vertex(root, pillar_vertex)
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
for previous_point in pending_points:
|
||||
if previous_point.distance_to(pillar_vertex) < 0.02:
|
||||
status_label.text = "Choose a different second vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
pending_points.append(pillar_vertex)
|
||||
if pending_points.size() >= 2:
|
||||
_create_pillar_from_vertices(root, pending_points[0], pending_points[1])
|
||||
else:
|
||||
status_label.text = "First vertex marked. Click another vertex to place the pillar between them."
|
||||
update_overlays()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_FENCE or active_tool == TOOL_PARAPET:
|
||||
var fence_vertex := _nearest_vertex(camera, event.position, _get_tool_vertices(root))
|
||||
if fence_vertex == null:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
for previous_point in pending_points:
|
||||
if previous_point.distance_to(fence_vertex) < 0.02:
|
||||
status_label.text = "Choose a different second vertex for the path."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
pending_points.append(fence_vertex)
|
||||
if pending_points.size() >= 2:
|
||||
_create_fence_decoration(root, pending_points[0], pending_points[1])
|
||||
else:
|
||||
status_label.text = "Start marked. Click the final fence vertex."
|
||||
update_overlays()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if _is_detail_tool(active_tool):
|
||||
if _create_detail_from_click(camera, event.position):
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
status_label.text = "Click a surface compatible with the selected tool."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
var point := _nearest_vertex(camera, event.position, _get_tool_vertices(root))
|
||||
if point == null:
|
||||
return EditorPlugin.AFTER_GUI_INPUT_PASS
|
||||
for previous_point in pending_points:
|
||||
if previous_point.distance_to(point) < 0.02:
|
||||
status_label.text = "Choose a different vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
if active_tool == TOOL_FLOOR_SQUARE and pending_points.size() == 1:
|
||||
var first_local := root.to_local(pending_points[0])
|
||||
var second_local := root.to_local(point)
|
||||
if absf(first_local.x - second_local.x) < 0.02 or absf(first_local.z - second_local.z) < 0.02:
|
||||
status_label.text = "To complete a square floor, choose the opposite diagonal vertex."
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
pending_points.append(point)
|
||||
var needed := _required_clicks()
|
||||
if pending_points.size() >= needed:
|
||||
_finish_shape(root)
|
||||
elif active_tool == TOOL_ROOF_SHED:
|
||||
status_label.text = "Low point marked. Click the high point on the other side of the roof."
|
||||
elif active_tool == TOOL_MERGE_VERTICES:
|
||||
status_label.text = "First vertex marked. Click the second to merge the markers."
|
||||
else:
|
||||
status_label.text = "Point %d of %d marked. Click the next vertex." % [pending_points.size(), needed]
|
||||
update_overlays()
|
||||
return EditorPlugin.AFTER_GUI_INPUT_STOP
|
||||
|
||||
func _forward_3d_draw_over_viewport(overlay: Control) -> void:
|
||||
pass
|
||||
|
||||
func _required_clicks() -> int:
|
||||
if active_tool == TOOL_WALL or active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT or active_tool == TOOL_MERGE_VERTICES or active_tool == TOOL_FLOOR_SQUARE or active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_SHED or active_tool == TOOL_ROOF_PYRAMID or active_tool == TOOL_STAIRS or active_tool == TOOL_FENCE:
|
||||
return 2
|
||||
return 3
|
||||
|
||||
func _finish_shape(root: ModularBuilderPiece) -> void:
|
||||
if active_tool == TOOL_MERGE_VERTICES:
|
||||
_finish_merge_vertices(root)
|
||||
return
|
||||
if active_tool == TOOL_WALL:
|
||||
_finish_wall_segments(root)
|
||||
return
|
||||
if active_tool == TOOL_GABLE_WALL or active_tool == TOOL_GABLE_WALL_RIGHT:
|
||||
_finish_gable_wall(root)
|
||||
return
|
||||
if active_tool == TOOL_ROOF_GABLE or active_tool == TOOL_ROOF_SHED or active_tool == TOOL_ROOF_PYRAMID:
|
||||
_finish_roof(root)
|
||||
return
|
||||
if active_tool == TOOL_STAIRS:
|
||||
_finish_stairs(root)
|
||||
return
|
||||
if active_tool == TOOL_FENCE:
|
||||
_finish_fence(root)
|
||||
return
|
||||
var local_points := PackedVector3Array()
|
||||
for world_point in pending_points:
|
||||
local_points.append(root.to_local(world_point))
|
||||
var piece := Node3D.new()
|
||||
piece.set_script(PIECE_SCRIPT)
|
||||
piece.set("centered_origin", true)
|
||||
piece.name = "FloorTriangle" if active_tool == TOOL_FLOOR_TRIANGLE else "FloorSquare"
|
||||
piece.set("piece_type", ModularBuilderPiece.PieceType.FLOOR)
|
||||
piece.set("thickness", root.thickness)
|
||||
if active_tool == TOOL_FLOOR_SQUARE:
|
||||
var a := local_points[0]
|
||||
var b := local_points[1]
|
||||
var left := minf(a.x, b.x)
|
||||
var right := maxf(a.x, b.x)
|
||||
var near_z := minf(a.z, b.z)
|
||||
var far_z := maxf(a.z, b.z)
|
||||
var y := (a.y + b.y) * 0.5
|
||||
local_points = PackedVector3Array([
|
||||
Vector3(left, y, near_z), Vector3(right, y, near_z),
|
||||
Vector3(right, y, far_z), Vector3(left, y, far_z)
|
||||
])
|
||||
var floor_pivot := _packed_points_bounds_center(local_points)
|
||||
floor_pivot.y -= piece.thickness * 0.5
|
||||
piece.position = floor_pivot
|
||||
for i in range(local_points.size()):
|
||||
local_points[i] -= floor_pivot
|
||||
piece.set("custom_points", local_points)
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create %s from vertices" % piece.name)
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "%s created inside ModularBuilderPiece. The tool remains active." % piece.name
|
||||
update_overlays()
|
||||
|
||||
func _finish_wall_segments(root: ModularBuilderPiece) -> void:
|
||||
var start_world := pending_points[0]
|
||||
var finish_world := pending_points[1]
|
||||
var path := finish_world - start_world
|
||||
var path_length_squared := path.length_squared()
|
||||
if path_length_squared < 0.0001:
|
||||
pending_points.clear()
|
||||
status_label.text = "Wall points are too close together."
|
||||
update_overlays()
|
||||
return
|
||||
var parameters: Array[float] = [0.0, 1.0]
|
||||
for vertex in _get_tool_vertices(root):
|
||||
var t := (vertex - start_world).dot(path) / path_length_squared
|
||||
if t <= 0.001 or t >= 0.999:
|
||||
continue
|
||||
var projected := start_world + path * t
|
||||
if vertex.distance_to(projected) > 0.02:
|
||||
continue
|
||||
var already_added := false
|
||||
for existing_t in parameters:
|
||||
if absf(existing_t - t) < 0.005:
|
||||
already_added = true
|
||||
break
|
||||
if not already_added:
|
||||
parameters.append(t)
|
||||
parameters.sort()
|
||||
var pieces: Array[ModularBuilderPiece] = []
|
||||
var centralize_wall := _gable_centralize_check.button_pressed
|
||||
for i in range(parameters.size() - 1):
|
||||
var a_world := start_world.lerp(finish_world, parameters[i])
|
||||
var b_world := start_world.lerp(finish_world, parameters[i + 1])
|
||||
pieces.append(_make_wall_segment(root, a_world, b_world, i + 1, centralize_wall))
|
||||
var scene_root := get_editor_interface().get_edited_scene_root()
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create wall in %d segments" % pieces.size())
|
||||
for piece in pieces:
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", scene_root)
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = pieces[-1]
|
||||
pending_points.clear()
|
||||
status_label.text = "Wall split into %d segments at foundation vertices." % pieces.size()
|
||||
update_overlays()
|
||||
|
||||
func _finish_gable_wall(root: ModularBuilderPiece) -> void:
|
||||
var start := root.to_local(pending_points[0])
|
||||
var finish := root.to_local(pending_points[1])
|
||||
if Vector2(start.x - finish.x, start.z - finish.z).length() < 0.02 or absf(start.y - finish.y) > 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two separate vertices at the same height for the gable base."
|
||||
update_overlays()
|
||||
return
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
var is_right_triangle := active_tool == TOOL_GABLE_WALL_RIGHT
|
||||
piece.name = "GableWallRight" if is_right_triangle else "GableWall"
|
||||
piece.piece_type = ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT if is_right_triangle else ModularBuilderPiece.PieceType.GABLE_WALL
|
||||
piece.unit_size = root.unit_size
|
||||
piece.thickness = root.thickness
|
||||
piece.centered_origin = true
|
||||
piece.center_align = _gable_centralize_check.button_pressed
|
||||
var pivot := (start + finish) * 0.5 + Vector3.UP * rise_spin.value * 0.5
|
||||
piece.position = pivot
|
||||
piece.rise = rise_spin.value
|
||||
piece.custom_points = PackedVector3Array([start - pivot, finish - pivot])
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create gable wall")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "Gable wall created. The tool remains active."
|
||||
update_overlays()
|
||||
|
||||
func _finish_roof(root: ModularBuilderPiece) -> void:
|
||||
var first := root.to_local(pending_points[0])
|
||||
var second := root.to_local(pending_points[1])
|
||||
var is_gable := active_tool == TOOL_ROOF_GABLE
|
||||
var is_pyramid := active_tool == TOOL_ROOF_PYRAMID
|
||||
var is_shed := active_tool == TOOL_ROOF_SHED
|
||||
if (is_gable or is_pyramid) and absf(first.y - second.y) > 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "For this roof, choose two diagonal points at the same height."
|
||||
update_overlays()
|
||||
return
|
||||
if is_shed and second.y <= first.y + 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose a low point first, then a higher point on the opposite side."
|
||||
update_overlays()
|
||||
return
|
||||
if is_shed and not _shed_points_are_on_opposite_sides(root, pending_points[0], pending_points[1]):
|
||||
pending_points.clear()
|
||||
status_label.text = "Roof points must be on opposite, parallel walls."
|
||||
update_overlays()
|
||||
return
|
||||
var roof_rise := rise_spin.value
|
||||
var roof_width := 0.0
|
||||
var corners := PackedVector3Array()
|
||||
if is_gable:
|
||||
var gable_result := _find_roof_between_gables(root, pending_points[0], pending_points[1])
|
||||
if not gable_result.is_empty():
|
||||
corners = gable_result.corners
|
||||
roof_rise = gable_result.rise
|
||||
elif is_pyramid:
|
||||
corners = _find_roof_corners(root, first, second)
|
||||
else:
|
||||
corners = _find_shed_roof_corners(root, first, second)
|
||||
if corners.size() == 4:
|
||||
roof_width = corners[0].distance_to(corners[1])
|
||||
if is_gable and corners.is_empty():
|
||||
corners = ROOF_SUPPORT.find_corners(root, pending_points[0], pending_points[1], _simplify_vertices_check.button_pressed)
|
||||
if corners.is_empty():
|
||||
corners = _find_roof_corners(root, first, second)
|
||||
if corners.size() != 4:
|
||||
pending_points.clear()
|
||||
status_label.text = "Could not find the other roof corners. Choose two diagonal vertices on the top edges of opposite walls."
|
||||
update_overlays()
|
||||
return
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
piece.name = "RoofGable" if is_gable else ("RoofPyramid" if is_pyramid else "RoofShed")
|
||||
piece.piece_type = ModularBuilderPiece.PieceType.ROOF_GABLE if is_gable else (ModularBuilderPiece.PieceType.ROOF_PYRAMID if is_pyramid else ModularBuilderPiece.PieceType.ROOF_SHED)
|
||||
piece.unit_size = root.unit_size
|
||||
piece.thickness = 0.05
|
||||
piece.centered_origin = true
|
||||
piece.rise = roof_rise
|
||||
if is_shed:
|
||||
piece.roof_width = roof_width
|
||||
var pivot := Vector3.ZERO
|
||||
for corner in corners:
|
||||
pivot += corner
|
||||
pivot /= corners.size()
|
||||
if is_gable or is_pyramid:
|
||||
pivot += Vector3.UP * roof_rise * 0.5
|
||||
piece.position = pivot
|
||||
if is_gable or is_pyramid:
|
||||
var local_corners := PackedVector3Array()
|
||||
for corner in corners:
|
||||
var local_corner: Vector3 = corner - pivot
|
||||
local_corner.y = -roof_rise * 0.5
|
||||
local_corners.append(local_corner)
|
||||
piece.custom_points = local_corners
|
||||
else:
|
||||
var local_corners := PackedVector3Array()
|
||||
for corner in corners:
|
||||
local_corners.append(corner - pivot)
|
||||
piece.custom_points = local_corners
|
||||
var undo := get_undo_redo()
|
||||
var roof_title := "gable roof" if is_gable else ("pyramid roof" if is_pyramid else "shed roof")
|
||||
undo.create_action("Create %s" % roof_title)
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "%s created. The tool remains active." % piece.name
|
||||
update_overlays()
|
||||
|
||||
func _finish_stairs(root: ModularBuilderPiece) -> void:
|
||||
if pending_points.size() < 2:
|
||||
return
|
||||
var low := root.to_local(pending_points[0])
|
||||
var high := root.to_local(pending_points[1])
|
||||
if high.y <= low.y + 0.05:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose the low point first, then a point at least 5 cm higher."
|
||||
update_overlays()
|
||||
return
|
||||
if Vector2(high.x - low.x, high.z - low.z).length() < 0.1:
|
||||
pending_points.clear()
|
||||
status_label.text = "Stair points need horizontal separation."
|
||||
update_overlays()
|
||||
return
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
var pivot := (low + high) * 0.5
|
||||
piece.name = "Stairs"
|
||||
piece.piece_type = ModularBuilderPiece.PieceType.STAIRS
|
||||
piece.position = pivot
|
||||
piece.unit_size = root.unit_size
|
||||
piece.thickness = 0.1
|
||||
piece.centered_origin = true
|
||||
piece.stair_count = int(stair_count_spin.value)
|
||||
piece.stair_width = stair_width_spin.value
|
||||
piece.custom_points = PackedVector3Array([low - pivot, high - pivot])
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create stairs")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "Stairs created with %d steps. The tool remains active." % piece.stair_count
|
||||
update_overlays()
|
||||
|
||||
func _finish_fence(root: ModularBuilderPiece) -> void:
|
||||
if pending_points.size() < 2:
|
||||
return
|
||||
var start := root.to_local(pending_points[0])
|
||||
var finish := root.to_local(pending_points[1])
|
||||
if start.distance_to(finish) < 0.1:
|
||||
pending_points.clear()
|
||||
status_label.text = "Choose two points far enough apart for the fence."
|
||||
update_overlays()
|
||||
return
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
var pivot := (start + finish) * 0.5
|
||||
piece.name = "Fence"
|
||||
piece.piece_type = ModularBuilderPiece.PieceType.FENCE
|
||||
piece.position = pivot
|
||||
piece.unit_size = root.unit_size
|
||||
piece.thickness = 0.08
|
||||
piece.centered_origin = true
|
||||
piece.fence_divisions = int(fence_divisions_spin.value)
|
||||
piece.fence_height = 1.0
|
||||
piece.custom_points = PackedVector3Array([start - pivot, finish - pivot])
|
||||
var undo := get_undo_redo()
|
||||
undo.create_action("Create fence")
|
||||
undo.add_do_method(root, "add_child", piece, true)
|
||||
undo.add_do_property(piece, "owner", get_editor_interface().get_edited_scene_root())
|
||||
undo.add_undo_method(root, "remove_child", piece)
|
||||
undo.commit_action()
|
||||
_last_created_piece = piece
|
||||
pending_points.clear()
|
||||
status_label.text = "Fence created with %d intermediate post(s). The tool remains active." % piece.fence_divisions
|
||||
update_overlays()
|
||||
|
||||
func _find_roof_corners(root: ModularBuilderPiece, first: Vector3, second: Vector3) -> PackedVector3Array:
|
||||
var diagonal_center := (first + second) * 0.5
|
||||
var diagonal_length := first.distance_to(second)
|
||||
var candidates: Array[Vector3] = []
|
||||
for world_point in _get_tool_vertices(root):
|
||||
var local_point := root.to_local(world_point)
|
||||
if absf(local_point.y - first.y) > 0.04:
|
||||
continue
|
||||
_add_unique_vertex(candidates, local_point)
|
||||
for adjacent in candidates:
|
||||
if adjacent.distance_to(first) < 0.02 or adjacent.distance_to(second) < 0.02:
|
||||
continue
|
||||
var opposite := diagonal_center * 2.0 - adjacent
|
||||
for other_adjacent in candidates:
|
||||
if other_adjacent.distance_to(first) < 0.02 or other_adjacent.distance_to(second) < 0.02 or other_adjacent.distance_to(adjacent) < 0.02:
|
||||
continue
|
||||
if other_adjacent.distance_to(opposite) > 0.04:
|
||||
continue
|
||||
if absf(adjacent.distance_to(other_adjacent) - diagonal_length) > 0.06:
|
||||
continue
|
||||
return PackedVector3Array([first, adjacent, second, other_adjacent])
|
||||
return PackedVector3Array()
|
||||
|
||||
func _find_shed_roof_corners(root: ModularBuilderPiece, low: Vector3, high: Vector3) -> PackedVector3Array:
|
||||
var candidates: Array[Vector3] = []
|
||||
for world_point in _get_tool_vertices(root):
|
||||
_add_unique_vertex(candidates, root.to_local(world_point))
|
||||
var match_tolerance := maxf(0.08, root.thickness * 1.5)
|
||||
var best := PackedVector3Array()
|
||||
var best_score := INF
|
||||
for low_adjacent in candidates:
|
||||
if absf(low_adjacent.y - low.y) > 0.04 or low_adjacent.distance_to(low) < 0.4:
|
||||
continue
|
||||
var target_high_adjacent := low + high - low_adjacent
|
||||
for high_adjacent in candidates:
|
||||
if absf(high_adjacent.y - high.y) > 0.04 or high_adjacent.distance_to(high) < 0.4:
|
||||
continue
|
||||
var corner_error := high_adjacent.distance_to(target_high_adjacent)
|
||||
if corner_error > match_tolerance:
|
||||
continue
|
||||
var low_edge := Vector2(low_adjacent.x - low.x, low_adjacent.z - low.z)
|
||||
var high_edge := Vector2(high.x - high_adjacent.x, high.z - high_adjacent.z)
|
||||
if low_edge.length() < 0.4 or high_edge.length() < 0.4:
|
||||
continue
|
||||
if absf(low_edge.normalized().dot(high_edge.normalized())) < 0.94:
|
||||
continue
|
||||
var edge_error := absf(low_edge.length() - high_edge.length())
|
||||
if edge_error > match_tolerance * 2.0:
|
||||
continue
|
||||
var score := corner_error + edge_error
|
||||
if score < best_score:
|
||||
best_score = score
|
||||
best = PackedVector3Array([low, low_adjacent, high_adjacent, high])
|
||||
return best
|
||||
|
||||
func _shed_points_are_on_opposite_sides(root: ModularBuilderPiece, low_world: Vector3, high_world: Vector3) -> bool:
|
||||
var low_supports := _get_roof_supports_at_point(root, low_world)
|
||||
var high_supports := _get_roof_supports_at_point(root, high_world)
|
||||
var across := root.to_local(high_world) - root.to_local(low_world)
|
||||
var across_horizontal := Vector2(across.x, across.z)
|
||||
if across_horizontal.length() < 0.1:
|
||||
return false
|
||||
across_horizontal = across_horizontal.normalized()
|
||||
for low_support in low_supports:
|
||||
for high_support in high_supports:
|
||||
if low_support.piece == high_support.piece:
|
||||
continue
|
||||
var low_direction: Vector2 = low_support.direction
|
||||
var high_direction: Vector2 = high_support.direction
|
||||
if absf(low_direction.dot(high_direction)) < 0.85:
|
||||
continue
|
||||
var side_axis := Vector2(-low_direction.y, low_direction.x)
|
||||
var center_delta: Vector3 = high_support.center - low_support.center
|
||||
var side_separation := absf(Vector2(center_delta.x, center_delta.z).dot(side_axis))
|
||||
if side_separation >= 0.4:
|
||||
return true
|
||||
return false
|
||||
|
||||
func _get_roof_supports_at_point(root: ModularBuilderPiece, world_point: Vector3) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
for node in walls:
|
||||
var wall := node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var world_a := wall.to_global(endpoints[0])
|
||||
var world_b := wall.to_global(endpoints[1])
|
||||
var local_direction := root.to_local(world_b) - root.to_local(world_a)
|
||||
var direction := Vector2(local_direction.x, local_direction.z).normalized()
|
||||
var wall_local_direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-wall_local_direction.z, 0.0, wall_local_direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
for height in [0.0, wall.unit_size]:
|
||||
var center := wall.to_global(endpoint + Vector3.UP * height)
|
||||
var support_points := [center + side, center - side]
|
||||
if _simplify_vertices_check.button_pressed:
|
||||
support_points.append(center)
|
||||
for point in support_points:
|
||||
if point.distance_to(world_point) <= 0.04:
|
||||
result.append({"piece": wall, "direction": direction, "center": root.to_local(center)})
|
||||
var gables: Array[Node] = []
|
||||
_collect_gable_walls(root, gables)
|
||||
for node in gables:
|
||||
var gable := node as ModularBuilderPiece
|
||||
if gable.custom_points.size() < 2:
|
||||
continue
|
||||
var base: PackedVector3Array = gable.call("_get_centered_gable_base") if gable.center_align else gable.custom_points
|
||||
var a: Vector3 = base[0]
|
||||
var b: Vector3 = base[1]
|
||||
var local_direction := b - a
|
||||
var direction := Vector2(local_direction.x, local_direction.z).normalized()
|
||||
var side := Vector3(-local_direction.z, 0.0, local_direction.x).normalized() * gable.thickness * 0.5
|
||||
var peak := (a + b) * 0.5 + Vector3.UP * gable.rise if gable.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL else b + Vector3.UP * gable.rise
|
||||
var support_points := [a + side, a - side, b + side, b - side, peak + side, peak - side]
|
||||
if _simplify_vertices_check.button_pressed:
|
||||
support_points.append_array([a, b, peak])
|
||||
for point_local in support_points:
|
||||
var point := gable.to_global(point_local)
|
||||
if point.distance_to(world_point) <= 0.04:
|
||||
result.append({"piece": gable, "direction": direction, "center": root.to_local(gable.to_global((a + b) * 0.5))})
|
||||
if result.is_empty() and root.has_meta(MERGED_VERTEX_GROUPS_META):
|
||||
for group_value in root.get_meta(MERGED_VERTEX_GROUPS_META, []):
|
||||
if not group_value is Dictionary:
|
||||
continue
|
||||
var group: Dictionary = group_value
|
||||
var anchor: Vector3 = group.get("position", Vector3.ZERO)
|
||||
if root.to_global(anchor).distance_to(world_point) > 0.04:
|
||||
continue
|
||||
var members: PackedVector3Array = group.get("members", PackedVector3Array())
|
||||
for member in members:
|
||||
result.append_array(_get_roof_supports_at_point(root, root.to_global(member)))
|
||||
return result
|
||||
|
||||
func _find_roof_between_gables(root: ModularBuilderPiece, first_world: Vector3, second_world: Vector3) -> Dictionary:
|
||||
var gables: Array[Node] = []
|
||||
_collect_gable_walls(root, gables)
|
||||
var first_gable: ModularBuilderPiece
|
||||
var second_gable: ModularBuilderPiece
|
||||
var first_base := PackedVector3Array()
|
||||
var second_base := PackedVector3Array()
|
||||
for node in gables:
|
||||
var gable := node as ModularBuilderPiece
|
||||
if gable.custom_points.size() < 2:
|
||||
continue
|
||||
var base: PackedVector3Array = gable.call("_get_centered_gable_base") if gable.center_align else gable.custom_points
|
||||
var a: Vector3 = base[0]
|
||||
var b: Vector3 = base[1]
|
||||
var direction := (b - a).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * gable.thickness * 0.5
|
||||
var peak := (a + b) * 0.5 + Vector3.UP * gable.rise if gable.piece_type == ModularBuilderPiece.PieceType.GABLE_WALL else b + Vector3.UP * gable.rise
|
||||
var peak_a := gable.to_global(peak + side)
|
||||
var peak_b := gable.to_global(peak - side)
|
||||
var peak_center := gable.to_global(peak)
|
||||
if first_gable == null and (peak_a.distance_to(first_world) < 0.12 or peak_b.distance_to(first_world) < 0.12 or (_simplify_vertices_check.button_pressed and peak_center.distance_to(first_world) < 0.12)):
|
||||
first_gable = gable
|
||||
first_base = base
|
||||
elif second_gable == null and gable != first_gable and (peak_a.distance_to(second_world) < 0.12 or peak_b.distance_to(second_world) < 0.12 or (_simplify_vertices_check.button_pressed and peak_center.distance_to(second_world) < 0.12)):
|
||||
second_gable = gable
|
||||
second_base = base
|
||||
if first_gable == null or second_gable == null:
|
||||
return {}
|
||||
var a0 := root.to_local(first_gable.to_global(first_base[0]))
|
||||
var a1 := root.to_local(first_gable.to_global(first_base[1]))
|
||||
var b0 := root.to_local(second_gable.to_global(second_base[0]))
|
||||
var b1 := root.to_local(second_gable.to_global(second_base[1]))
|
||||
var direct_score := a0.distance_to(b0) + a1.distance_to(b1)
|
||||
var crossed_score := a0.distance_to(b1) + a1.distance_to(b0)
|
||||
var corners := PackedVector3Array([a0, a1, b1, b0]) if direct_score <= crossed_score else PackedVector3Array([a0, a1, b0, b1])
|
||||
return {"corners": corners, "rise": (first_gable.rise + second_gable.rise) * 0.5}
|
||||
|
||||
func _make_wall_segment(root: ModularBuilderPiece, start_world: Vector3, finish_world: Vector3, index: int, centralize_wall: bool) -> ModularBuilderPiece:
|
||||
var centered_start := _center_wall_anchor(root, start_world) if centralize_wall else start_world
|
||||
var centered_finish := _center_wall_anchor(root, finish_world) if centralize_wall else finish_world
|
||||
var start := root.to_local(centered_start)
|
||||
var finish := root.to_local(centered_finish)
|
||||
var piece_node := Node3D.new()
|
||||
piece_node.set_script(PIECE_SCRIPT)
|
||||
var piece := piece_node as ModularBuilderPiece
|
||||
piece.name = "Wall_%02d" % index
|
||||
piece.set("piece_type", ModularBuilderPiece.PieceType.WALL)
|
||||
piece.set("unit_size", root.unit_size)
|
||||
piece.set("thickness", root.thickness)
|
||||
piece.set("centered_origin", true)
|
||||
piece.set("center_align", centralize_wall)
|
||||
var pivot := (start + finish) * 0.5 + Vector3.UP * root.unit_size * 0.5
|
||||
piece.position = pivot
|
||||
piece.set("custom_points", PackedVector3Array([start - pivot, finish - pivot]))
|
||||
return piece
|
||||
|
||||
func _center_wall_anchor(root: ModularBuilderPiece, world_point: Vector3) -> Vector3:
|
||||
var walls: Array[Node] = []
|
||||
_collect_walls(root, walls)
|
||||
var best_point := world_point
|
||||
var best_distance := INF
|
||||
for wall_node in walls:
|
||||
var wall := wall_node as ModularBuilderPiece
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := (endpoints[1] - endpoints[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in endpoints:
|
||||
var center := wall.to_global(endpoint + Vector3.UP * wall.unit_size)
|
||||
for surface_corner in [center + side, center - side]:
|
||||
var distance := world_point.distance_to(surface_corner)
|
||||
if distance < best_distance and distance <= wall.thickness + 0.04:
|
||||
best_distance = distance
|
||||
best_point = center
|
||||
return best_point
|
||||
1
addons/modular_builder_en/modular_builder_plugin.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b065m6e7ey5n0
|
||||
284
addons/modular_builder_en/modular_building_detail.gd
Normal file
|
|
@ -0,0 +1,284 @@
|
|||
@tool
|
||||
class_name ModularBuilderDetail
|
||||
extends Node3D
|
||||
|
||||
enum DetailType { WINDOW, DOOR, HATCH, PILLAR, FENCE, PARAPET }
|
||||
enum DetailMode { OPENING, SEPARATE_OBJECT }
|
||||
enum WindowStyle { SINGLE_PANE, CROSS_PANE }
|
||||
enum DoorStyle { FLAT, PANELED }
|
||||
|
||||
@export var detail_type: DetailType = DetailType.WINDOW:
|
||||
set(value):
|
||||
detail_type = value
|
||||
notify_property_list_changed()
|
||||
_rebuild()
|
||||
@export var detail_mode: DetailMode = DetailMode.OPENING:
|
||||
set(value):
|
||||
detail_mode = value
|
||||
notify_property_list_changed()
|
||||
_rebuild()
|
||||
@export var center_align: bool = false:
|
||||
set(value):
|
||||
center_align = value
|
||||
_apply_pillar_alignment()
|
||||
notify_property_list_changed()
|
||||
@export_storage var pillar_direction: Vector3 = Vector3.UP
|
||||
@export_storage var pillar_alignment_basis: Basis = Basis.IDENTITY
|
||||
@export var window_style: WindowStyle = WindowStyle.SINGLE_PANE:
|
||||
set(value):
|
||||
window_style = value
|
||||
_rebuild()
|
||||
@export var door_style: DoorStyle = DoorStyle.FLAT:
|
||||
set(value):
|
||||
door_style = value
|
||||
_rebuild()
|
||||
@export_range(0.01, 5.0, 0.001, "suffix:m") var width: float = 1.0:
|
||||
set(value):
|
||||
width = value
|
||||
_rebuild()
|
||||
@export_range(0.2, 5.0, 0.001, "suffix:m") var height: float = 1.0:
|
||||
set(value):
|
||||
height = value
|
||||
_rebuild()
|
||||
@export_range(0.02, 0.5, 0.001, "suffix:m") var frame_depth: float = 0.08:
|
||||
set(value):
|
||||
frame_depth = value
|
||||
_rebuild()
|
||||
@export_range(0.1, 8.0, 0.001, "suffix:m") var fence_height: float = 0.7:
|
||||
set(value):
|
||||
fence_height = value
|
||||
_rebuild()
|
||||
@export_range(0, 100, 1, "suffix: posts") var fence_divisions: int = 1:
|
||||
set(value):
|
||||
fence_divisions = value
|
||||
_rebuild()
|
||||
@export_storage var path_points: PackedVector3Array = PackedVector3Array()
|
||||
@export_range(0.01, 0.5, 0.001, "suffix:m") var frame_width: float = 0.08:
|
||||
set(value):
|
||||
frame_width = value
|
||||
_rebuild()
|
||||
@export var follow_host_thickness: bool = true:
|
||||
set(value):
|
||||
follow_host_thickness = value
|
||||
_rebuild()
|
||||
@export var surface_material: Material:
|
||||
set(value):
|
||||
surface_material = value
|
||||
_rebuild()
|
||||
@export var frame_material: Material:
|
||||
set(value):
|
||||
frame_material = value
|
||||
_rebuild()
|
||||
@export var panel_material: Material:
|
||||
set(value):
|
||||
panel_material = value
|
||||
_rebuild()
|
||||
|
||||
func _validate_property(property: Dictionary) -> void:
|
||||
var separate_object := detail_mode == DetailMode.SEPARATE_OBJECT
|
||||
var is_window := detail_type == DetailType.WINDOW
|
||||
var is_door := detail_type == DetailType.DOOR
|
||||
var has_frame_and_panel := separate_object and detail_type != DetailType.PILLAR and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
|
||||
var show_property := true
|
||||
match StringName(property.name):
|
||||
&"detail_mode":
|
||||
show_property = detail_type != DetailType.PILLAR and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
|
||||
&"center_align":
|
||||
show_property = detail_type == DetailType.PILLAR
|
||||
&"fence_height", &"fence_divisions":
|
||||
show_property = detail_type == DetailType.FENCE or detail_type == DetailType.PARAPET
|
||||
&"path_points":
|
||||
show_property = false
|
||||
&"width":
|
||||
show_property = detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
|
||||
if detail_type == DetailType.PILLAR:
|
||||
property.hint = PROPERTY_HINT_RANGE
|
||||
property.hint_string = "0.01,5.0,0.001,suffix:m"
|
||||
&"height":
|
||||
show_property = detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET
|
||||
&"window_style":
|
||||
show_property = is_window and separate_object
|
||||
&"door_style":
|
||||
show_property = is_door and separate_object
|
||||
&"frame_width", &"frame_material", &"panel_material":
|
||||
show_property = has_frame_and_panel
|
||||
&"follow_host_thickness":
|
||||
show_property = has_frame_and_panel
|
||||
&"frame_depth":
|
||||
show_property = has_frame_and_panel and not follow_host_thickness
|
||||
&"surface_material":
|
||||
show_property = separate_object
|
||||
if not show_property:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
|
||||
func _ready() -> void:
|
||||
set_notify_transform(true)
|
||||
_rebuild()
|
||||
|
||||
func _notification(what: int) -> void:
|
||||
if what == NOTIFICATION_TRANSFORM_CHANGED and is_inside_tree():
|
||||
var host := get_parent()
|
||||
if host != null and host.has_method("_rebuild"):
|
||||
host.call("_rebuild")
|
||||
|
||||
func _on_host_thickness_changed() -> void:
|
||||
_rebuild(false)
|
||||
|
||||
func _apply_pillar_alignment() -> void:
|
||||
if detail_type != DetailType.PILLAR:
|
||||
return
|
||||
var direction := pillar_direction.normalized()
|
||||
if direction.is_zero_approx():
|
||||
direction = Vector3.UP
|
||||
if center_align:
|
||||
var object_axes := pillar_alignment_basis.orthonormalized()
|
||||
var best_axis := direction
|
||||
var best_dot := -1.0
|
||||
for axis in [object_axes.x, object_axes.y, object_axes.z]:
|
||||
var normalized_axis: Vector3 = axis.normalized()
|
||||
var alignment := absf(direction.dot(normalized_axis))
|
||||
if alignment > best_dot:
|
||||
best_dot = alignment
|
||||
best_axis = normalized_axis if direction.dot(normalized_axis) >= 0.0 else -normalized_axis
|
||||
direction = best_axis
|
||||
var reference_axis := Vector3.FORWARD if absf(direction.dot(Vector3.UP)) > 0.99 else Vector3.UP
|
||||
var x_axis := reference_axis.cross(direction).normalized()
|
||||
var z_axis := x_axis.cross(direction).normalized()
|
||||
transform.basis = Basis(x_axis, direction, z_axis).orthonormalized()
|
||||
if center_align and pillar_direction.length() > 0.0:
|
||||
height = pillar_direction.length()
|
||||
|
||||
func _rebuild(notify_host: bool = true) -> void:
|
||||
if not is_inside_tree():
|
||||
return
|
||||
for child in get_children():
|
||||
if child.name == "DetailGeometry":
|
||||
remove_child(child)
|
||||
child.queue_free()
|
||||
var host := get_parent()
|
||||
if notify_host and host != null and host.has_method("_rebuild"):
|
||||
host.call("_rebuild")
|
||||
if detail_mode == DetailMode.OPENING and detail_type != DetailType.FENCE and detail_type != DetailType.PARAPET:
|
||||
return
|
||||
var holder := Node3D.new()
|
||||
holder.name = "DetailGeometry"
|
||||
add_child(holder)
|
||||
var scene_root: Node = null
|
||||
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
|
||||
scene_root = get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
holder.owner = scene_root
|
||||
var effective_depth := _effective_frame_depth()
|
||||
var frame := minf(maxf(frame_width, 0.01), minf(width, height) * 0.45)
|
||||
if detail_type == DetailType.FENCE or detail_type == DetailType.PARAPET:
|
||||
_build_fence_geometry(holder)
|
||||
return
|
||||
if detail_type == DetailType.PILLAR:
|
||||
_add_box(holder, "PillarBody", Vector3(width, height, width), Vector3.ZERO, surface_material)
|
||||
return
|
||||
if detail_type == DetailType.HATCH:
|
||||
_add_box(holder, "LeftFrame", Vector3(frame, effective_depth, height), Vector3(-width * 0.5 + frame * 0.5, 0.0, 0.0), _frame_material())
|
||||
_add_box(holder, "RightFrame", Vector3(frame, effective_depth, height), Vector3(width * 0.5 - frame * 0.5, 0.0, 0.0), _frame_material())
|
||||
_add_box(holder, "NearFrame", Vector3(width, effective_depth, frame), Vector3(0.0, 0.0, -height * 0.5 + frame * 0.5), _frame_material())
|
||||
_add_box(holder, "FarFrame", Vector3(width, effective_depth, frame), Vector3(0.0, 0.0, height * 0.5 - frame * 0.5), _frame_material())
|
||||
_add_box(holder, "HatchPanel", Vector3(width - frame * 2.0, effective_depth * 0.35, height - frame * 2.0), Vector3(0.0, effective_depth * 0.4, 0.0), _panel_material())
|
||||
return
|
||||
_add_box(holder, "LeftFrame", Vector3(frame, height, effective_depth), Vector3(-width * 0.5 + frame * 0.5, 0.0, 0.0), _frame_material())
|
||||
_add_box(holder, "RightFrame", Vector3(frame, height, effective_depth), Vector3(width * 0.5 - frame * 0.5, 0.0, 0.0), _frame_material())
|
||||
_add_box(holder, "TopFrame", Vector3(width, frame, effective_depth), Vector3(0.0, height * 0.5 - frame * 0.5, 0.0), _frame_material())
|
||||
if detail_type != DetailType.DOOR:
|
||||
_add_box(holder, "BottomFrame", Vector3(width, frame, effective_depth), Vector3(0.0, -height * 0.5 + frame * 0.5, 0.0), _frame_material())
|
||||
if detail_type == DetailType.WINDOW:
|
||||
_add_box(holder, "WindowPanel", Vector3(maxf(width - frame * 2.0, 0.001), maxf(height - frame * 2.0, 0.001), effective_depth * 0.35), Vector3.ZERO, _panel_material())
|
||||
if window_style == WindowStyle.CROSS_PANE:
|
||||
_add_box(holder, "VerticalMullion", Vector3(frame * 0.55, height - frame * 2.0, effective_depth * 0.4), Vector3.ZERO, _frame_material())
|
||||
_add_box(holder, "HorizontalMullion", Vector3(width - frame * 2.0, frame * 0.55, effective_depth * 0.4), Vector3.ZERO, _frame_material())
|
||||
elif detail_type == DetailType.DOOR:
|
||||
_add_box(holder, "DoorPanel", Vector3(width - frame * 2.0, height - frame, effective_depth * 0.45), Vector3(0.0, -frame * 0.5, 0.0), _panel_material())
|
||||
if door_style == DoorStyle.PANELED:
|
||||
var inset_depth := maxf(effective_depth * 0.04, 0.005)
|
||||
var panel_width := (width - frame * 2.0) * 0.82
|
||||
var panel_height := maxf((height - frame) * 0.28, 0.05)
|
||||
for panel_index in range(2):
|
||||
var panel_y := -frame * 0.5 + (panel_index - 0.5) * (panel_height + frame * 0.25)
|
||||
_add_box(holder, "DoorInset_%d" % panel_index, Vector3(panel_width, panel_height, inset_depth), Vector3(0.0, panel_y, effective_depth * 0.24), _frame_material())
|
||||
|
||||
func _effective_frame_depth() -> float:
|
||||
if follow_host_thickness:
|
||||
var host := get_parent()
|
||||
if host != null:
|
||||
var host_thickness = host.get("thickness")
|
||||
if host_thickness is float or host_thickness is int:
|
||||
var depth := float(host_thickness)
|
||||
if detail_type == DetailType.WINDOW or detail_type == DetailType.DOOR:
|
||||
depth *= 1.25
|
||||
return maxf(depth, 0.02)
|
||||
return frame_depth
|
||||
|
||||
func _build_fence_geometry(holder: Node3D) -> void:
|
||||
if path_points.size() < 2 or fence_height <= 0.0:
|
||||
return
|
||||
var start := path_points[0]
|
||||
var finish := path_points[1]
|
||||
var path := finish - start
|
||||
var path_length := path.length()
|
||||
if path_length < 0.05:
|
||||
return
|
||||
var path_axis := path / path_length
|
||||
var up_axis := Vector3.UP - path_axis * path_axis.dot(Vector3.UP)
|
||||
if up_axis.length_squared() < 0.001:
|
||||
up_axis = Vector3.FORWARD - path_axis * path_axis.dot(Vector3.FORWARD)
|
||||
up_axis = up_axis.normalized()
|
||||
var side_axis := path_axis.cross(up_axis).normalized()
|
||||
var post_width := 0.16 if detail_type == DetailType.PARAPET else 0.08
|
||||
var post_count := fence_divisions + 2
|
||||
var posts: Array[Vector3] = []
|
||||
for i in range(post_count):
|
||||
var base := start.lerp(finish, float(i) / float(post_count - 1))
|
||||
posts.append(base)
|
||||
_add_oriented_box(holder, "Post_%d" % i, base + up_axis * fence_height * 0.5, path_axis, up_axis, side_axis, Vector3(post_width, fence_height, post_width))
|
||||
for i in range(posts.size() - 1):
|
||||
var a := posts[i]
|
||||
var b := posts[i + 1]
|
||||
var segment := b - a
|
||||
var segment_length := segment.length()
|
||||
if detail_type == DetailType.PARAPET:
|
||||
_add_oriented_box(holder, "Rail_%d" % i, (a + b) * 0.5 + up_axis * fence_height * 0.78, segment.normalized(), up_axis, side_axis, Vector3(segment_length, post_width * 0.8, post_width * 0.8))
|
||||
else:
|
||||
for height_ratio in [0.28, 0.78]:
|
||||
_add_oriented_box(holder, "Rail_%d_%d" % [i, int(height_ratio * 100.0)], (a + b) * 0.5 + up_axis * fence_height * height_ratio, segment.normalized(), up_axis, side_axis, Vector3(segment_length, post_width, post_width))
|
||||
|
||||
func _add_oriented_box(holder: Node3D, node_name: String, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3, dimensions: Vector3) -> void:
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = node_name
|
||||
var box := BoxMesh.new()
|
||||
box.size = dimensions
|
||||
mesh_instance.mesh = box
|
||||
var basis := Basis(axis_x.normalized(), axis_y.normalized(), axis_z.normalized()).orthonormalized()
|
||||
mesh_instance.transform = Transform3D(basis, center)
|
||||
mesh_instance.material_override = surface_material
|
||||
holder.add_child(mesh_instance)
|
||||
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
|
||||
var scene_root := get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
mesh_instance.owner = scene_root
|
||||
|
||||
func _frame_material() -> Material:
|
||||
return frame_material if frame_material != null else surface_material
|
||||
|
||||
func _panel_material() -> Material:
|
||||
return panel_material if panel_material != null else surface_material
|
||||
|
||||
func _add_box(holder: Node3D, node_name: String, box_size: Vector3, box_position: Vector3, material: Material = null) -> void:
|
||||
var mesh_instance := MeshInstance3D.new()
|
||||
mesh_instance.name = node_name
|
||||
var box := BoxMesh.new()
|
||||
box.size = box_size
|
||||
mesh_instance.mesh = box
|
||||
mesh_instance.position = box_position
|
||||
mesh_instance.material_override = material if material != null else surface_material
|
||||
holder.add_child(mesh_instance)
|
||||
if Engine.is_editor_hint() and not has_meta("modular_builder_preview"):
|
||||
var scene_root := get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
mesh_instance.owner = scene_root
|
||||
1
addons/modular_builder_en/modular_building_detail.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dsttda167kuhi
|
||||
166
addons/modular_builder_en/modular_mesh_baker.gd
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
func bake(building: ModularBuilderPiece) -> ArrayMesh:
|
||||
var mesh_instances: Array[MeshInstance3D] = []
|
||||
_collect_mesh_instances(building, mesh_instances)
|
||||
var groups: Array[Dictionary] = []
|
||||
var projected_materials: Dictionary = {}
|
||||
for mesh_instance in mesh_instances:
|
||||
if not is_instance_valid(mesh_instance) or not mesh_instance.is_visible_in_tree() or _is_export_excluded(mesh_instance, building):
|
||||
continue
|
||||
var source_mesh := mesh_instance.mesh
|
||||
if source_mesh == null:
|
||||
continue
|
||||
var relative_transform := building.global_transform.affine_inverse() * mesh_instance.global_transform
|
||||
if absf(relative_transform.basis.determinant()) < 0.000001:
|
||||
continue
|
||||
var normal_transform := relative_transform.basis.inverse().transposed()
|
||||
var mirrored := relative_transform.basis.determinant() < 0.0
|
||||
for surface_index in range(source_mesh.get_surface_count()):
|
||||
var primitive_type := Mesh.PRIMITIVE_TRIANGLES
|
||||
var surface_arrays: Array
|
||||
if source_mesh is PrimitiveMesh:
|
||||
# PrimitiveMesh provides mesh arrays directly; surface_get_* methods
|
||||
# are used for other meshes.
|
||||
if surface_index > 0:
|
||||
continue
|
||||
surface_arrays = source_mesh.get_mesh_arrays()
|
||||
else:
|
||||
primitive_type = source_mesh.surface_get_primitive_type(surface_index)
|
||||
surface_arrays = source_mesh.surface_get_arrays(surface_index)
|
||||
if primitive_type != Mesh.PRIMITIVE_TRIANGLES:
|
||||
continue
|
||||
if surface_arrays.is_empty() or not surface_arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array:
|
||||
continue
|
||||
var source_vertices: PackedVector3Array = surface_arrays[Mesh.ARRAY_VERTEX]
|
||||
if source_vertices.size() < 3:
|
||||
continue
|
||||
var source_normals := PackedVector3Array()
|
||||
var source_uvs := PackedVector2Array()
|
||||
var source_colors := PackedColorArray()
|
||||
if surface_arrays[Mesh.ARRAY_NORMAL] is PackedVector3Array:
|
||||
source_normals = surface_arrays[Mesh.ARRAY_NORMAL]
|
||||
if surface_arrays[Mesh.ARRAY_TEX_UV] is PackedVector2Array:
|
||||
source_uvs = surface_arrays[Mesh.ARRAY_TEX_UV]
|
||||
if surface_arrays[Mesh.ARRAY_COLOR] is PackedColorArray:
|
||||
source_colors = surface_arrays[Mesh.ARRAY_COLOR]
|
||||
var source_indices := PackedInt32Array()
|
||||
if surface_arrays[Mesh.ARRAY_INDEX] is PackedInt32Array:
|
||||
source_indices = surface_arrays[Mesh.ARRAY_INDEX]
|
||||
var material := mesh_instance.get_active_material(surface_index)
|
||||
var force_projected_uvs := false
|
||||
var bake_material := material
|
||||
if material is StandardMaterial3D and material.uv1_triplanar:
|
||||
force_projected_uvs = true
|
||||
var material_id := material.get_instance_id()
|
||||
if not projected_materials.has(material_id):
|
||||
var material_copy := material.duplicate() as StandardMaterial3D
|
||||
material_copy.uv1_triplanar = false
|
||||
material_copy.uv1_world_triplanar = false
|
||||
projected_materials[material_id] = material_copy
|
||||
bake_material = projected_materials[material_id] as Material
|
||||
var group_index := _find_or_create_group(groups, bake_material)
|
||||
var group: Dictionary = groups[group_index]
|
||||
var group_vertices: Array = group["vertices"]
|
||||
var group_normals: Array = group["normals"]
|
||||
var group_uvs: Array = group["uvs"]
|
||||
var group_colors: Array = group["colors"]
|
||||
var has_indices := not source_indices.is_empty()
|
||||
var triangle_count := source_indices.size() if has_indices else source_vertices.size()
|
||||
for triangle_start in range(0, triangle_count - 2, 3):
|
||||
var index_a := source_indices[triangle_start] if has_indices else triangle_start
|
||||
var index_b := source_indices[triangle_start + 1] if has_indices else triangle_start + 1
|
||||
var index_c := source_indices[triangle_start + 2] if has_indices else triangle_start + 2
|
||||
if index_a >= source_vertices.size() or index_b >= source_vertices.size() or index_c >= source_vertices.size():
|
||||
continue
|
||||
if mirrored:
|
||||
var swap_index := index_b
|
||||
index_b = index_c
|
||||
index_c = swap_index
|
||||
var local_a: Vector3 = source_vertices[index_a]
|
||||
var local_b: Vector3 = source_vertices[index_b]
|
||||
var local_c: Vector3 = source_vertices[index_c]
|
||||
var baked_a: Vector3 = relative_transform * local_a
|
||||
var baked_b: Vector3 = relative_transform * local_b
|
||||
var baked_c: Vector3 = relative_transform * local_c
|
||||
var face_normal := (baked_b - baked_a).cross(baked_c - baked_a).normalized()
|
||||
if face_normal.length_squared() < 0.5:
|
||||
continue
|
||||
var baked_points := [baked_a, baked_b, baked_c]
|
||||
var source_point_indices := [index_a, index_b, index_c]
|
||||
var source_face_normal := (local_b - local_a).cross(local_c - local_a).normalized()
|
||||
for corner in range(3):
|
||||
var source_index: int = source_point_indices[corner]
|
||||
var baked_point: Vector3 = baked_points[corner]
|
||||
var baked_normal := face_normal
|
||||
if source_normals.size() == source_vertices.size():
|
||||
baked_normal = (normal_transform * source_normals[source_index]).normalized()
|
||||
if baked_normal.length_squared() < 0.5:
|
||||
baked_normal = face_normal
|
||||
var uv: Vector2
|
||||
if not force_projected_uvs and source_uvs.size() == source_vertices.size():
|
||||
uv = source_uvs[source_index]
|
||||
else:
|
||||
uv = _project_uv(source_vertices[source_index], source_face_normal)
|
||||
var color := source_colors[source_index] if source_colors.size() == source_vertices.size() else Color.WHITE
|
||||
group_vertices.append(baked_point)
|
||||
group_normals.append(baked_normal)
|
||||
group_uvs.append(uv)
|
||||
group_colors.append(color)
|
||||
groups[group_index] = group
|
||||
if groups.is_empty():
|
||||
return null
|
||||
var combined := ArrayMesh.new()
|
||||
combined.resource_name = "%s_Mesh" % building.name
|
||||
for group in groups:
|
||||
var vertices := PackedVector3Array(group["vertices"])
|
||||
if vertices.is_empty():
|
||||
continue
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = PackedVector3Array(group["normals"])
|
||||
arrays[Mesh.ARRAY_TEX_UV] = PackedVector2Array(group["uvs"])
|
||||
arrays[Mesh.ARRAY_COLOR] = PackedColorArray(group["colors"])
|
||||
var surface_tool := SurfaceTool.new()
|
||||
surface_tool.create_from_arrays(arrays, Mesh.PRIMITIVE_TRIANGLES)
|
||||
if group["material"] is Material:
|
||||
surface_tool.set_material(group["material"])
|
||||
surface_tool.generate_tangents()
|
||||
var temp_mesh := surface_tool.commit()
|
||||
if temp_mesh == null or temp_mesh.get_surface_count() == 0:
|
||||
continue
|
||||
combined.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, temp_mesh.surface_get_arrays(0))
|
||||
if group["material"] is Material:
|
||||
combined.surface_set_material(combined.get_surface_count() - 1, group["material"])
|
||||
return combined if combined.get_surface_count() > 0 else null
|
||||
|
||||
func _find_or_create_group(groups: Array[Dictionary], material: Material) -> int:
|
||||
for i in range(groups.size()):
|
||||
if groups[i]["material"] == material:
|
||||
return i
|
||||
groups.append({"material": material, "vertices": [], "normals": [], "uvs": [], "colors": []})
|
||||
return groups.size() - 1
|
||||
|
||||
func _project_uv(point: Vector3, normal: Vector3) -> Vector2:
|
||||
var absolute_normal := normal.abs()
|
||||
if absolute_normal.y >= absolute_normal.x and absolute_normal.y >= absolute_normal.z:
|
||||
return Vector2(point.x, point.z)
|
||||
if absolute_normal.x >= absolute_normal.z:
|
||||
return Vector2(point.z, point.y)
|
||||
return Vector2(point.x, point.y)
|
||||
|
||||
func _is_export_excluded(mesh_instance: MeshInstance3D, building: ModularBuilderPiece) -> bool:
|
||||
var current: Node = mesh_instance
|
||||
while current != null and current != building:
|
||||
if current.has_meta("modular_builder_preview") or current.name == "ModularBuilderMarkers":
|
||||
return true
|
||||
current = current.get_parent()
|
||||
return false
|
||||
|
||||
func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
|
||||
if node is MeshInstance3D:
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_mesh_instances(child, result)
|
||||
1
addons/modular_builder_en/modular_mesh_baker.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://bd52re1amu4r2
|
||||
55
addons/modular_builder_en/modular_mesh_picker.gd
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
static func raycast(root: Node3D, camera: Camera3D, screen_position: Vector2) -> Variant:
|
||||
var ray_origin := camera.project_ray_origin(screen_position)
|
||||
var ray_direction := camera.project_ray_normal(screen_position).normalized()
|
||||
var nearest_point: Variant = null
|
||||
var nearest_distance := INF
|
||||
var mesh_nodes: Array[MeshInstance3D] = []
|
||||
_collect_mesh_instances(root, mesh_nodes)
|
||||
for mesh_node in mesh_nodes:
|
||||
var source_mesh := mesh_node.mesh
|
||||
if source_mesh == null or not mesh_node.is_visible_in_tree():
|
||||
continue
|
||||
for surface_index in range(source_mesh.get_surface_count()):
|
||||
var arrays: Array
|
||||
if source_mesh is PrimitiveMesh:
|
||||
if surface_index > 0:
|
||||
continue
|
||||
arrays = source_mesh.get_mesh_arrays()
|
||||
else:
|
||||
if source_mesh.surface_get_primitive_type(surface_index) != Mesh.PRIMITIVE_TRIANGLES:
|
||||
continue
|
||||
arrays = source_mesh.surface_get_arrays(surface_index)
|
||||
if arrays.is_empty() or not arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array:
|
||||
continue
|
||||
var vertices: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
var indices := PackedInt32Array()
|
||||
if arrays[Mesh.ARRAY_INDEX] is PackedInt32Array:
|
||||
indices = arrays[Mesh.ARRAY_INDEX]
|
||||
var has_indices := not indices.is_empty()
|
||||
var triangle_count := indices.size() if has_indices else vertices.size()
|
||||
for triangle_start in range(0, triangle_count - 2, 3):
|
||||
var index_a := indices[triangle_start] if has_indices else triangle_start
|
||||
var index_b := indices[triangle_start + 1] if has_indices else triangle_start + 1
|
||||
var index_c := indices[triangle_start + 2] if has_indices else triangle_start + 2
|
||||
if index_a < 0 or index_b < 0 or index_c < 0 or index_a >= vertices.size() or index_b >= vertices.size() or index_c >= vertices.size():
|
||||
continue
|
||||
var vertex_a := mesh_node.to_global(vertices[index_a])
|
||||
var vertex_b := mesh_node.to_global(vertices[index_b])
|
||||
var vertex_c := mesh_node.to_global(vertices[index_c])
|
||||
var hit: Variant = Geometry3D.ray_intersects_triangle(ray_origin, ray_direction, vertex_a, vertex_b, vertex_c)
|
||||
if hit == null:
|
||||
continue
|
||||
var distance := ray_origin.distance_to(hit)
|
||||
if distance < nearest_distance:
|
||||
nearest_distance = distance
|
||||
nearest_point = hit
|
||||
return nearest_point
|
||||
|
||||
static func _collect_mesh_instances(node: Node, result: Array[MeshInstance3D]) -> void:
|
||||
if node is MeshInstance3D:
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_mesh_instances(child, result)
|
||||
1
addons/modular_builder_en/modular_mesh_picker.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://cg71aq1smh1q3
|
||||
61
addons/modular_builder_en/modular_piece.gd
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
@tool
|
||||
class_name ModularBuilderPiece
|
||||
extends "res://addons/modular_builder_en/modular_piece_geometry.gd"
|
||||
|
||||
func _ready() -> void:
|
||||
_rebuild()
|
||||
|
||||
func _validate_property(property: Dictionary) -> void:
|
||||
if property.name == &"symmetric_dimensions" and piece_type == PieceType.BUILDING:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"dimension_height" and (piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE or piece_type == PieceType.FLOOR):
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"dimension_depth" and piece_type == PieceType.WALL:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"dimension_width" or property.name == &"dimension_height" or property.name == &"dimension_depth":
|
||||
if symmetric_dimensions:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"rise" and piece_type != PieceType.GABLE_WALL and piece_type != PieceType.GABLE_WALL_RIGHT and piece_type != PieceType.ROOF_GABLE and piece_type != PieceType.ROOF_PYRAMID:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"roof_width" and piece_type != PieceType.ROOF_SHED:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"stair_count" and piece_type != PieceType.STAIRS:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"stair_width" and piece_type != PieceType.STAIRS:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"fence_divisions" and piece_type != PieceType.FENCE:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"fence_height" and piece_type != PieceType.FENCE:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
if property.name == &"center_align" and piece_type != PieceType.GABLE_WALL and piece_type != PieceType.GABLE_WALL_RIGHT and piece_type != PieceType.WALL:
|
||||
property.usage = PROPERTY_USAGE_NO_EDITOR
|
||||
|
||||
func _rebuild() -> void:
|
||||
if not is_inside_tree():
|
||||
return
|
||||
var old_collision := get_node_or_null("Collision")
|
||||
if old_collision != null:
|
||||
remove_child(old_collision)
|
||||
old_collision.queue_free()
|
||||
var mesh_instance := get_node_or_null("Geometry") as MeshInstance3D
|
||||
if piece_type == PieceType.BUILDING:
|
||||
if mesh_instance != null:
|
||||
remove_child(mesh_instance)
|
||||
mesh_instance.queue_free()
|
||||
return
|
||||
if mesh_instance == null:
|
||||
mesh_instance = MeshInstance3D.new()
|
||||
mesh_instance.name = "Geometry"
|
||||
add_child(mesh_instance)
|
||||
_set_scene_owner(mesh_instance)
|
||||
mesh_instance.mesh = _make_mesh()
|
||||
mesh_instance.material_override = surface_material
|
||||
if mesh_instance.mesh != null and mesh_instance.mesh.get_surface_count() > 0:
|
||||
_dimension_reference = mesh_instance.mesh.get_aabb().size
|
||||
_apply_dimension_scale()
|
||||
|
||||
func _set_scene_owner(node: Node) -> void:
|
||||
if Engine.is_editor_hint():
|
||||
var scene_root := get_tree().edited_scene_root
|
||||
if scene_root:
|
||||
node.owner = scene_root
|
||||
1
addons/modular_builder_en/modular_piece.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://coqicbv0jfksq
|
||||
748
addons/modular_builder_en/modular_piece_geometry.gd
Normal file
|
|
@ -0,0 +1,748 @@
|
|||
@tool
|
||||
class_name ModularBuilderPieceGeometry
|
||||
extends Node3D
|
||||
|
||||
const DETAIL_SCRIPT_PATH := "res://addons/modular_builder_en/modular_building_detail.gd"
|
||||
const DETAIL_TYPE_HATCH := 2
|
||||
const DETAIL_TYPE_PILLAR := 3
|
||||
|
||||
enum PieceType { FOUNDATION_SQUARE, FOUNDATION_TRIANGLE, FLOOR, WALL, BUILDING, GABLE_WALL, ROOF_GABLE, ROOF_SHED, GABLE_WALL_RIGHT, ROOF_PYRAMID, STAIRS, FENCE }
|
||||
|
||||
@export var piece_type: PieceType = PieceType.FOUNDATION_SQUARE:
|
||||
set(value):
|
||||
piece_type = value
|
||||
notify_property_list_changed()
|
||||
_rebuild()
|
||||
@export_range(0.5, 8.0, 0.001, "suffix:m") var unit_size: float = 2.0:
|
||||
set(value):
|
||||
unit_size = value
|
||||
_rebuild()
|
||||
@export_range(0.01, 1.0, 0.001, "suffix:m") var thickness: float = 0.2:
|
||||
set(value):
|
||||
thickness = value
|
||||
_rebuild()
|
||||
for child in get_children():
|
||||
if child.has_method("_on_host_thickness_changed"):
|
||||
child.call("_on_host_thickness_changed")
|
||||
@export_range(0.1, 8.0, 0.001, "suffix:m") var rise: float = 1.0:
|
||||
set(value):
|
||||
rise = value
|
||||
_rebuild()
|
||||
@export_range(0.01, 32.0, 0.001, "suffix:m") var roof_width: float = 2.0:
|
||||
set(value):
|
||||
roof_width = value
|
||||
_rebuild()
|
||||
@export_range(1, 100, 1, "suffix: steps") var stair_count: int = 10:
|
||||
set(value):
|
||||
stair_count = value
|
||||
_rebuild()
|
||||
@export_range(0.01, 8.0, 0.001, "suffix:m") var stair_width: float = 1.0:
|
||||
set(value):
|
||||
stair_width = value
|
||||
_rebuild()
|
||||
@export_range(0, 100, 1, "suffix: posts") var fence_divisions: int = 1:
|
||||
set(value):
|
||||
fence_divisions = value
|
||||
_rebuild()
|
||||
@export_range(0.1, 8.0, 0.001, "suffix:m") var fence_height: float = 1.0:
|
||||
set(value):
|
||||
fence_height = value
|
||||
_rebuild()
|
||||
@export var symmetric_dimensions: bool = true:
|
||||
set(value):
|
||||
if symmetric_dimensions == value:
|
||||
return
|
||||
if not value:
|
||||
_capture_mesh_dimensions()
|
||||
symmetric_dimensions = value
|
||||
notify_property_list_changed()
|
||||
if value:
|
||||
scale = Vector3.ONE
|
||||
else:
|
||||
_apply_dimension_scale()
|
||||
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_width: float = 2.0:
|
||||
set(value):
|
||||
dimension_width = value
|
||||
_apply_dimension_scale()
|
||||
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_height: float = 2.0:
|
||||
set(value):
|
||||
dimension_height = value
|
||||
_apply_dimension_scale()
|
||||
@export_range(0.01, 100.0, 0.001, "suffix:m") var dimension_depth: float = 2.0:
|
||||
set(value):
|
||||
dimension_depth = value
|
||||
_apply_dimension_scale()
|
||||
@export_storage var _dimension_reference: Vector3 = Vector3.ZERO
|
||||
@export var center_align: bool = false:
|
||||
set(value):
|
||||
center_align = value
|
||||
_rebuild()
|
||||
@export var surface_material: Material:
|
||||
set(value):
|
||||
surface_material = value
|
||||
_rebuild()
|
||||
@export var custom_points: PackedVector3Array = PackedVector3Array():
|
||||
set(value):
|
||||
custom_points = value
|
||||
_rebuild()
|
||||
@export var centered_origin: bool = false:
|
||||
set(value):
|
||||
centered_origin = value
|
||||
_rebuild()
|
||||
|
||||
func _rebuild() -> void:
|
||||
pass
|
||||
|
||||
func _capture_mesh_dimensions() -> void:
|
||||
var geometry := get_node_or_null("Geometry") as MeshInstance3D
|
||||
if geometry == null or geometry.mesh == null:
|
||||
return
|
||||
var dimensions := geometry.mesh.get_aabb().size
|
||||
if dimensions.x > 0.001:
|
||||
dimension_width = dimensions.x
|
||||
if dimensions.y > 0.001:
|
||||
dimension_height = dimensions.y
|
||||
if dimensions.z > 0.001:
|
||||
dimension_depth = dimensions.z
|
||||
_dimension_reference = dimensions
|
||||
|
||||
func _apply_dimension_scale() -> void:
|
||||
if symmetric_dimensions or _dimension_reference.x <= 0.001 or _dimension_reference.y <= 0.001 or _dimension_reference.z <= 0.001:
|
||||
return
|
||||
var target := Vector3(dimension_width, dimension_height, dimension_depth)
|
||||
if piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE or piece_type == PieceType.FLOOR:
|
||||
target.y = _dimension_reference.y
|
||||
if piece_type == PieceType.WALL:
|
||||
target.z = _dimension_reference.z
|
||||
scale = Vector3(target.x / _dimension_reference.x, target.y / _dimension_reference.y, target.z / _dimension_reference.z)
|
||||
|
||||
func _make_mesh() -> ArrayMesh:
|
||||
if piece_type == PieceType.GABLE_WALL and custom_points.size() >= 2:
|
||||
return _make_gable_wall_mesh()
|
||||
if piece_type == PieceType.GABLE_WALL_RIGHT and custom_points.size() >= 2:
|
||||
return _make_right_gable_wall_mesh()
|
||||
if (piece_type == PieceType.ROOF_GABLE or piece_type == PieceType.ROOF_SHED or piece_type == PieceType.ROOF_PYRAMID) and custom_points.size() >= 2:
|
||||
return _make_roof_mesh()
|
||||
if piece_type == PieceType.STAIRS and custom_points.size() >= 2:
|
||||
return _make_stair_mesh()
|
||||
if piece_type == PieceType.FENCE and custom_points.size() >= 2:
|
||||
return _make_fence_mesh()
|
||||
if custom_points.size() >= 2 and piece_type == PieceType.WALL:
|
||||
return _make_custom_wall_mesh()
|
||||
if custom_points.size() >= 3 and piece_type == PieceType.FLOOR:
|
||||
return _make_custom_floor_mesh()
|
||||
if custom_points.size() >= 3 and (piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE):
|
||||
return _make_custom_foundation_mesh()
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var s := unit_size
|
||||
var polygon: Array[Vector2]
|
||||
match piece_type:
|
||||
PieceType.FOUNDATION_TRIANGLE:
|
||||
polygon = [Vector2(-s * 0.5, -s * 0.5), Vector2(s * 0.5, -s * 0.5), Vector2(-s * 0.5, s * 0.5)]
|
||||
_:
|
||||
polygon = [Vector2(-s * 0.5, -s * 0.5), Vector2(s * 0.5, -s * 0.5), Vector2(s * 0.5, s * 0.5), Vector2(-s * 0.5, s * 0.5)]
|
||||
var y0 := -thickness * 0.5 if centered_origin else 0.0
|
||||
var y1 := thickness * 0.5 if centered_origin else thickness
|
||||
if piece_type == PieceType.WALL:
|
||||
polygon = [Vector2(-s * 0.5, -thickness * 0.5), Vector2(s * 0.5, -thickness * 0.5), Vector2(s * 0.5, thickness * 0.5), Vector2(-s * 0.5, thickness * 0.5)]
|
||||
y0 = -s * 0.5 if centered_origin else 0.0
|
||||
y1 = s * 0.5 if centered_origin else s
|
||||
if piece_type == PieceType.FOUNDATION_SQUARE or piece_type == PieceType.FOUNDATION_TRIANGLE:
|
||||
var hatch_mesh := _make_hatch_surface_mesh(polygon, y1, y0)
|
||||
if hatch_mesh != null:
|
||||
return hatch_mesh
|
||||
for i in range(1, polygon.size() - 1):
|
||||
_add_tri(vertices, normals, Vector3(polygon[0].x, y1, polygon[0].y), Vector3(polygon[i + 1].x, y1, polygon[i + 1].y), Vector3(polygon[i].x, y1, polygon[i].y))
|
||||
_add_tri(vertices, normals, Vector3(polygon[0].x, y0, polygon[0].y), Vector3(polygon[i].x, y0, polygon[i].y), Vector3(polygon[i + 1].x, y0, polygon[i + 1].y))
|
||||
for i in range(polygon.size()):
|
||||
var j := (i + 1) % polygon.size()
|
||||
var a := polygon[i]
|
||||
var b := polygon[j]
|
||||
_add_tri(vertices, normals, Vector3(a.x, y0, a.y), Vector3(b.x, y0, b.y), Vector3(b.x, y1, b.y))
|
||||
_add_tri(vertices, normals, Vector3(a.x, y0, a.y), Vector3(b.x, y1, b.y), Vector3(a.x, y1, a.y))
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = vertices
|
||||
arrays[Mesh.ARRAY_NORMAL] = normals
|
||||
var result := ArrayMesh.new()
|
||||
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return result
|
||||
|
||||
func _make_gable_wall_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var base_points := _get_centered_gable_base() if center_align else PackedVector3Array([custom_points[0], custom_points[1]])
|
||||
var a: Vector3 = base_points[0]
|
||||
var b: Vector3 = base_points[1]
|
||||
var direction := (b - a).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
|
||||
var peak := (a + b) * 0.5 + Vector3.UP * rise
|
||||
var front_a := a + side
|
||||
var front_b := b + side
|
||||
var front_peak := peak + side
|
||||
var back_a := a - side
|
||||
var back_b := b - side
|
||||
var back_peak := peak - side
|
||||
_add_tri(vertices, normals, front_a, front_b, front_peak)
|
||||
_add_tri(vertices, normals, back_b, back_a, back_peak)
|
||||
_add_quad(vertices, normals, front_a, back_a, back_b, front_b)
|
||||
_add_quad(vertices, normals, front_b, back_b, back_peak, front_peak)
|
||||
_add_quad(vertices, normals, front_peak, back_peak, back_a, front_a)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_right_gable_wall_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var base_points := _get_centered_gable_base() if center_align else PackedVector3Array([custom_points[0], custom_points[1]])
|
||||
var a: Vector3 = base_points[0]
|
||||
var b: Vector3 = base_points[1]
|
||||
var direction := (b - a).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
|
||||
var peak := b + Vector3.UP * rise
|
||||
var front_a := a + side
|
||||
var front_b := b + side
|
||||
var front_peak := peak + side
|
||||
var back_a := a - side
|
||||
var back_b := b - side
|
||||
var back_peak := peak - side
|
||||
_add_tri(vertices, normals, front_a, front_b, front_peak)
|
||||
_add_tri(vertices, normals, back_peak, back_b, back_a)
|
||||
_add_quad(vertices, normals, front_a, back_a, back_b, front_b)
|
||||
_add_quad(vertices, normals, front_b, back_b, back_peak, front_peak)
|
||||
_add_quad(vertices, normals, front_peak, back_peak, back_a, front_a)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_roof_mesh() -> ArrayMesh:
|
||||
if piece_type == PieceType.ROOF_SHED and custom_points.size() == 2:
|
||||
return _make_shed_roof_mesh()
|
||||
if custom_points.size() >= 4:
|
||||
return _make_oriented_roof_mesh()
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var first: Vector3 = custom_points[0]
|
||||
var second: Vector3 = custom_points[1]
|
||||
var min_x := minf(first.x, second.x)
|
||||
var max_x := maxf(first.x, second.x)
|
||||
var min_z := minf(first.z, second.z)
|
||||
var max_z := maxf(first.z, second.z)
|
||||
var span_x := max_x - min_x
|
||||
var span_z := max_z - min_z
|
||||
if span_x < 0.02 or span_z < 0.02:
|
||||
return _array_mesh(vertices, normals)
|
||||
var panels: Array = []
|
||||
if piece_type == PieceType.ROOF_GABLE:
|
||||
if span_x <= span_z:
|
||||
var ridge_x := (min_x + max_x) * 0.5
|
||||
panels.append([Vector3(min_x, 0.0, min_z), Vector3(min_x, 0.0, max_z), Vector3(ridge_x, rise, max_z), Vector3(ridge_x, rise, min_z)])
|
||||
panels.append([Vector3(ridge_x, rise, min_z), Vector3(ridge_x, rise, max_z), Vector3(max_x, 0.0, max_z), Vector3(max_x, 0.0, min_z)])
|
||||
else:
|
||||
var ridge_z := (min_z + max_z) * 0.5
|
||||
panels.append([Vector3(min_x, 0.0, min_z), Vector3(max_x, 0.0, min_z), Vector3(max_x, rise, ridge_z), Vector3(min_x, rise, ridge_z)])
|
||||
panels.append([Vector3(min_x, rise, ridge_z), Vector3(max_x, rise, ridge_z), Vector3(max_x, 0.0, max_z), Vector3(min_x, 0.0, max_z)])
|
||||
else:
|
||||
if span_x <= span_z:
|
||||
var low_x := first.x
|
||||
var high_x := second.x
|
||||
panels.append([Vector3(low_x, 0.0, min_z), Vector3(low_x, 0.0, max_z), Vector3(high_x, rise, max_z), Vector3(high_x, rise, min_z)])
|
||||
else:
|
||||
var low_z := first.z
|
||||
var high_z := second.z
|
||||
panels.append([Vector3(min_x, 0.0, low_z), Vector3(max_x, 0.0, low_z), Vector3(max_x, rise, high_z), Vector3(min_x, rise, high_z)])
|
||||
for panel in panels:
|
||||
_append_solid_panel(vertices, normals, panel)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_shed_roof_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var low: Vector3 = custom_points[0]
|
||||
var high: Vector3 = custom_points[1]
|
||||
var slope := high - low
|
||||
var horizontal := Vector3(slope.x, 0.0, slope.z)
|
||||
if horizontal.length_squared() < 0.0001 or roof_width <= 0.0:
|
||||
return _array_mesh(vertices, normals)
|
||||
var across := Vector3(horizontal.z, 0.0, -horizontal.x).normalized() * roof_width * 0.5
|
||||
var panel := [low - across, low + across, high + across, high - across]
|
||||
_append_solid_panel(vertices, normals, panel)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_oriented_roof_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var p0: Vector3 = custom_points[0]
|
||||
var p1: Vector3 = custom_points[1]
|
||||
var p2: Vector3 = custom_points[2]
|
||||
var p3: Vector3 = custom_points[3]
|
||||
var edge_01 := Vector2(p1.x - p0.x, p1.z - p0.z).length()
|
||||
var edge_12 := Vector2(p2.x - p1.x, p2.z - p1.z).length()
|
||||
var panels: Array = []
|
||||
if piece_type == PieceType.ROOF_GABLE:
|
||||
if edge_01 <= edge_12:
|
||||
var ridge_01 := (p0 + p1) * 0.5 + Vector3.UP * rise
|
||||
var ridge_23 := (p2 + p3) * 0.5 + Vector3.UP * rise
|
||||
panels.append([p0, p3, ridge_23, ridge_01])
|
||||
panels.append([ridge_01, ridge_23, p2, p1])
|
||||
else:
|
||||
var ridge_12 := (p1 + p2) * 0.5 + Vector3.UP * rise
|
||||
var ridge_30 := (p3 + p0) * 0.5 + Vector3.UP * rise
|
||||
panels.append([p0, p1, ridge_12, ridge_30])
|
||||
panels.append([ridge_30, ridge_12, p2, p3])
|
||||
elif piece_type == PieceType.ROOF_PYRAMID:
|
||||
var peak := (p0 + p1 + p2 + p3) * 0.25 + Vector3.UP * rise
|
||||
panels.append([p0, p1, peak])
|
||||
panels.append([p1, p2, peak])
|
||||
panels.append([p2, p3, peak])
|
||||
panels.append([p3, p0, peak])
|
||||
else:
|
||||
# The four points define the low and high edges. Keep their centers
|
||||
# fixed when adjusting both edge widths in the Inspector.
|
||||
var base_width := Vector2(p1.x - p0.x, p1.z - p0.z).length()
|
||||
if base_width > 0.001 and roof_width > 0.0:
|
||||
var direction := Vector3(p1.x - p0.x, 0.0, p1.z - p0.z).normalized()
|
||||
var low_center := (p0 + p1) * 0.5
|
||||
var high_center := (p2 + p3) * 0.5
|
||||
var half_width := roof_width * 0.5
|
||||
panels.append([
|
||||
low_center - direction * half_width,
|
||||
low_center + direction * half_width,
|
||||
high_center + direction * half_width,
|
||||
high_center - direction * half_width,
|
||||
])
|
||||
else:
|
||||
panels.append([p0, p1, p2, p3])
|
||||
for panel in panels:
|
||||
_append_solid_panel(vertices, normals, panel)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_stair_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var start: Vector3 = custom_points[0]
|
||||
var finish: Vector3 = custom_points[1]
|
||||
var horizontal := Vector3(finish.x - start.x, 0.0, finish.z - start.z)
|
||||
var run := horizontal.length()
|
||||
var rise_amount := finish.y - start.y
|
||||
if run < 0.05 or rise_amount <= 0.05 or stair_count < 1 or stair_width <= 0.0:
|
||||
return _array_mesh(vertices, normals)
|
||||
var forward := horizontal / run
|
||||
var width_axis := Vector3(-forward.z, 0.0, forward.x) * stair_width * 0.5
|
||||
var profile := PackedVector2Array([Vector2(0.0, 0.0), Vector2(run, 0.0), Vector2(run, rise_amount)])
|
||||
for step_index in range(stair_count - 1, -1, -1):
|
||||
var tread_start := run * float(step_index) / stair_count
|
||||
var tread_end := run * float(step_index + 1) / stair_count
|
||||
var tread_height := rise_amount * float(step_index + 1) / stair_count
|
||||
profile.append(Vector2(tread_start, tread_height))
|
||||
if step_index > 0:
|
||||
profile.append(Vector2(tread_start, rise_amount * float(step_index) / stair_count))
|
||||
var triangulation := Geometry2D.triangulate_polygon(profile)
|
||||
for side_sign in [-1.0, 1.0]:
|
||||
for triangle_index in range(0, triangulation.size(), 3):
|
||||
var a := profile[triangulation[triangle_index]]
|
||||
var b := profile[triangulation[triangle_index + 1]]
|
||||
var c := profile[triangulation[triangle_index + 2]]
|
||||
var point_a: Vector3 = start + forward * a.x + Vector3.UP * a.y + width_axis * side_sign
|
||||
var point_b: Vector3 = start + forward * b.x + Vector3.UP * b.y + width_axis * side_sign
|
||||
var point_c: Vector3 = start + forward * c.x + Vector3.UP * c.y + width_axis * side_sign
|
||||
if side_sign > 0.0:
|
||||
_add_tri(vertices, normals, point_a, point_b, point_c)
|
||||
else:
|
||||
_add_tri(vertices, normals, point_c, point_b, point_a)
|
||||
for profile_index in range(profile.size()):
|
||||
var next_index := (profile_index + 1) % profile.size()
|
||||
var a := profile[profile_index]
|
||||
var b := profile[next_index]
|
||||
var point_a_left := start + forward * a.x + Vector3.UP * a.y - width_axis
|
||||
var point_b_left := start + forward * b.x + Vector3.UP * b.y - width_axis
|
||||
var point_b_right := start + forward * b.x + Vector3.UP * b.y + width_axis
|
||||
var point_a_right := start + forward * a.x + Vector3.UP * a.y + width_axis
|
||||
_add_quad(vertices, normals, point_a_left, point_b_left, point_b_right, point_a_right)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_fence_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var start: Vector3 = custom_points[0]
|
||||
var finish: Vector3 = custom_points[1]
|
||||
var path := finish - start
|
||||
var path_length := path.length()
|
||||
if path_length < 0.05 or fence_height <= 0.0:
|
||||
return _array_mesh(vertices, normals)
|
||||
var path_axis := path / path_length
|
||||
var post_count := fence_divisions + 2
|
||||
var post_positions: Array[Vector3] = []
|
||||
for post_index in range(post_count):
|
||||
var base_position := start.lerp(finish, float(post_index) / float(post_count - 1))
|
||||
post_positions.append(base_position)
|
||||
var side_axis := path_axis.cross(Vector3.UP).normalized()
|
||||
if side_axis.length_squared() < 0.001:
|
||||
side_axis = Vector3.FORWARD
|
||||
_append_oriented_box(vertices, normals, base_position + Vector3.UP * fence_height * 0.5, path_axis, Vector3.UP, side_axis, Vector3(thickness, fence_height, thickness))
|
||||
for section_index in range(post_positions.size() - 1):
|
||||
var section_start := post_positions[section_index]
|
||||
var section_end := post_positions[section_index + 1]
|
||||
var section_axis := (section_end - section_start).normalized()
|
||||
var rail_up := Vector3.UP - section_axis * section_axis.dot(Vector3.UP)
|
||||
if rail_up.length_squared() < 0.001:
|
||||
rail_up = Vector3.FORWARD - section_axis * section_axis.dot(Vector3.FORWARD)
|
||||
rail_up = rail_up.normalized()
|
||||
var rail_side := section_axis.cross(rail_up).normalized()
|
||||
var section_length := section_start.distance_to(section_end)
|
||||
for height_ratio in [0.28, 0.78]:
|
||||
var rail_center: Vector3 = (section_start + section_end) * 0.5 + Vector3.UP * fence_height * height_ratio
|
||||
_append_oriented_box(vertices, normals, rail_center, section_axis, rail_up, rail_side, Vector3(section_length, thickness, thickness))
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _append_oriented_box(vertices: PackedVector3Array, normals: PackedVector3Array, center: Vector3, axis_x: Vector3, axis_y: Vector3, axis_z: Vector3, dimensions: Vector3) -> void:
|
||||
var x := axis_x.normalized() * dimensions.x * 0.5
|
||||
var y := axis_y.normalized() * dimensions.y * 0.5
|
||||
var z := axis_z.normalized() * dimensions.z * 0.5
|
||||
var nnn := center - x - y - z
|
||||
var pnn := center + x - y - z
|
||||
var ppn := center + x + y - z
|
||||
var npn := center - x + y - z
|
||||
var nnp := center - x - y + z
|
||||
var pnp := center + x - y + z
|
||||
var ppp := center + x + y + z
|
||||
var npp := center - x + y + z
|
||||
_add_quad(vertices, normals, nnp, pnp, ppp, npp)
|
||||
_add_quad(vertices, normals, pnn, nnn, npn, ppn)
|
||||
_add_quad(vertices, normals, npp, ppp, ppn, npn)
|
||||
_add_quad(vertices, normals, nnn, pnn, pnp, nnp)
|
||||
_add_quad(vertices, normals, nnn, nnp, npp, npn)
|
||||
_add_quad(vertices, normals, pnp, pnn, ppn, ppp)
|
||||
|
||||
func _get_centered_gable_base() -> PackedVector3Array:
|
||||
var selected_a := to_global(custom_points[0])
|
||||
var selected_b := to_global(custom_points[1])
|
||||
var search_root: Node = get_parent()
|
||||
while search_root != null and not search_root is ModularBuilderPieceGeometry:
|
||||
search_root = search_root.get_parent()
|
||||
if search_root == null:
|
||||
return PackedVector3Array([custom_points[0], custom_points[1]])
|
||||
var walls: Array[ModularBuilderPieceGeometry] = []
|
||||
_collect_centering_walls(search_root, walls)
|
||||
var centered_a := selected_a
|
||||
var centered_b := selected_b
|
||||
var nearest_a := INF
|
||||
var nearest_b := INF
|
||||
for wall in walls:
|
||||
if wall.custom_points.size() < 2:
|
||||
continue
|
||||
var wall_a := wall.to_global(wall.custom_points[0] + Vector3.UP * wall.unit_size)
|
||||
var wall_b := wall.to_global(wall.custom_points[1] + Vector3.UP * wall.unit_size)
|
||||
var tolerance: float = wall.thickness + 0.04
|
||||
for endpoint in [wall_a, wall_b]:
|
||||
var distance_a := selected_a.distance_to(endpoint)
|
||||
if distance_a < nearest_a and distance_a <= tolerance:
|
||||
nearest_a = distance_a
|
||||
centered_a = endpoint
|
||||
var distance_b := selected_b.distance_to(endpoint)
|
||||
if distance_b < nearest_b and distance_b <= tolerance:
|
||||
nearest_b = distance_b
|
||||
centered_b = endpoint
|
||||
if nearest_a < INF and nearest_b < INF and centered_a.distance_to(centered_b) >= 0.02:
|
||||
return PackedVector3Array([to_local(centered_a), to_local(centered_b)])
|
||||
return PackedVector3Array([custom_points[0], custom_points[1]])
|
||||
|
||||
func _collect_centering_walls(node: Node, walls: Array[ModularBuilderPieceGeometry]) -> void:
|
||||
if node is ModularBuilderPieceGeometry and node.piece_type == PieceType.WALL:
|
||||
walls.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_centering_walls(child, walls)
|
||||
|
||||
func _append_solid_panel(vertices: PackedVector3Array, normals: PackedVector3Array, panel: Array) -> void:
|
||||
if panel.size() < 3:
|
||||
return
|
||||
var p0: Vector3 = panel[0]
|
||||
var p1: Vector3 = panel[1]
|
||||
var p2: Vector3 = panel[2]
|
||||
var normal: Vector3 = (p1 - p0).cross(p2 - p0).normalized()
|
||||
if normal.is_zero_approx():
|
||||
return
|
||||
var half_depth: Vector3 = normal * thickness * 0.5
|
||||
var front: Array[Vector3] = []
|
||||
var back: Array[Vector3] = []
|
||||
for point in panel:
|
||||
front.append(point + half_depth)
|
||||
back.append(point - half_depth)
|
||||
for i in range(1, panel.size() - 1):
|
||||
_add_tri(vertices, normals, front[0], front[i], front[i + 1])
|
||||
_add_tri(vertices, normals, back[i + 1], back[i], back[0])
|
||||
for i in range(panel.size()):
|
||||
var next := (i + 1) % panel.size()
|
||||
_add_quad(vertices, normals, front[i], back[i], back[next], front[next])
|
||||
|
||||
func _make_custom_wall_mesh() -> ArrayMesh:
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var wall_points: PackedVector3Array = _get_centered_wall_points() if center_align else custom_points
|
||||
var a: Vector3 = wall_points[0]
|
||||
var b: Vector3 = wall_points[1]
|
||||
var direction := (b - a).normalized()
|
||||
var length := a.distance_to(b)
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * thickness * 0.5
|
||||
var height := Vector3.UP * unit_size
|
||||
var openings: Array[PackedVector2Array] = []
|
||||
for child in get_children():
|
||||
if _is_detail_node(child) and int(child.get("detail_type")) != DETAIL_TYPE_HATCH and int(child.get("detail_type")) != DETAIL_TYPE_PILLAR:
|
||||
var detail := child as Node3D
|
||||
var center := Vector2((detail.position - a).dot(direction), detail.position.y - a.y)
|
||||
var axis_x := Vector2(detail.transform.basis.x.dot(direction), detail.transform.basis.x.y).normalized()
|
||||
var axis_y := Vector2(detail.transform.basis.y.dot(direction), detail.transform.basis.y.y).normalized()
|
||||
var opening := PackedVector2Array([
|
||||
center - axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
|
||||
center + axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
|
||||
center + axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5,
|
||||
center - axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5
|
||||
])
|
||||
var wall_rect := PackedVector2Array([Vector2(0.0, 0.0), Vector2(length, 0.0), Vector2(length, unit_size), Vector2(0.0, unit_size)])
|
||||
for clipped_opening in Geometry2D.intersect_polygons(opening, wall_rect):
|
||||
if clipped_opening.size() >= 3:
|
||||
openings.append(clipped_opening)
|
||||
var xs: Array[float] = [0.0, length]
|
||||
var ys: Array[float] = [0.0, unit_size]
|
||||
for opening in openings:
|
||||
for point in opening:
|
||||
xs.append(clampf(point.x, 0.0, length))
|
||||
ys.append(clampf(point.y, 0.0, unit_size))
|
||||
xs.sort()
|
||||
ys.sort()
|
||||
for xi in range(xs.size() - 1):
|
||||
for yi in range(ys.size() - 1):
|
||||
var u0: float = xs[xi]
|
||||
var u1: float = xs[xi + 1]
|
||||
var v0: float = ys[yi]
|
||||
var v1: float = ys[yi + 1]
|
||||
var mid := Vector2((u0 + u1) * 0.5, (v0 + v1) * 0.5)
|
||||
var cell := PackedVector2Array([Vector2(u0, v0), Vector2(u1, v0), Vector2(u1, v1), Vector2(u0, v1)])
|
||||
var remaining: Array[PackedVector2Array] = [cell]
|
||||
for opening in openings:
|
||||
var next_remaining: Array[PackedVector2Array] = []
|
||||
for polygon in remaining:
|
||||
for clipped in Geometry2D.clip_polygons(polygon, opening):
|
||||
if clipped.size() >= 3:
|
||||
next_remaining.append(clipped)
|
||||
remaining = next_remaining
|
||||
for polygon in remaining:
|
||||
var indices := Geometry2D.triangulate_polygon(polygon)
|
||||
for ti in range(0, indices.size(), 3):
|
||||
var p0: Vector2 = polygon[indices[ti]]
|
||||
var p1: Vector2 = polygon[indices[ti + 1]]
|
||||
var p2: Vector2 = polygon[indices[ti + 2]]
|
||||
var f0 := a + direction * p0.x + Vector3.UP * p0.y
|
||||
var f1 := a + direction * p1.x + Vector3.UP * p1.y
|
||||
var f2 := a + direction * p2.x + Vector3.UP * p2.y
|
||||
_add_tri(vertices, normals, f0 + side, f1 + side, f2 + side)
|
||||
_add_tri(vertices, normals, f2 - side, f1 - side, f0 - side)
|
||||
# Close the wall body at its ends, top, and base.
|
||||
_add_quad(vertices, normals, a - side, a + side, a + side + height, a - side + height)
|
||||
_add_quad(vertices, normals, b + side, b - side, b - side + height, b + side + height)
|
||||
_add_quad(vertices, normals, a + side + height, b + side + height, b - side + height, a - side + height)
|
||||
_add_quad(vertices, normals, a - side, b - side, b + side, a + side)
|
||||
for opening in openings:
|
||||
for edge_index in range(opening.size()):
|
||||
var p0 := opening[edge_index]
|
||||
var p1 := opening[(edge_index + 1) % opening.size()]
|
||||
var a0 := a + direction * p0.x + Vector3.UP * p0.y
|
||||
var a1 := a + direction * p1.x + Vector3.UP * p1.y
|
||||
_add_quad(vertices, normals, a0 + side, a0 - side, a1 - side, a1 + side)
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _get_centered_wall_points() -> PackedVector3Array:
|
||||
if custom_points.size() < 2:
|
||||
return custom_points
|
||||
var selected_a := to_global(custom_points[0])
|
||||
var selected_b := to_global(custom_points[1])
|
||||
var search_root: Node = get_parent()
|
||||
while search_root != null and not search_root is ModularBuilderPieceGeometry:
|
||||
search_root = search_root.get_parent()
|
||||
if search_root == null:
|
||||
return custom_points
|
||||
var walls: Array[ModularBuilderPieceGeometry] = []
|
||||
_collect_centering_walls(search_root, walls)
|
||||
var centered_a := selected_a
|
||||
var centered_b := selected_b
|
||||
var nearest_a := INF
|
||||
var nearest_b := INF
|
||||
for wall in walls:
|
||||
if wall == self or wall.custom_points.size() < 2:
|
||||
continue
|
||||
var direction := (wall.custom_points[1] - wall.custom_points[0]).normalized()
|
||||
var side := Vector3(-direction.z, 0.0, direction.x).normalized() * wall.thickness * 0.5
|
||||
for endpoint in wall.custom_points:
|
||||
var center := wall.to_global(endpoint + Vector3.UP * wall.unit_size)
|
||||
for surface_corner in [center + side, center - side]:
|
||||
var distance_a := selected_a.distance_to(surface_corner)
|
||||
if distance_a < nearest_a and distance_a <= wall.thickness + 0.04:
|
||||
nearest_a = distance_a
|
||||
centered_a = center
|
||||
var distance_b := selected_b.distance_to(surface_corner)
|
||||
if distance_b < nearest_b and distance_b <= wall.thickness + 0.04:
|
||||
nearest_b = distance_b
|
||||
centered_b = center
|
||||
if nearest_a < INF and nearest_b < INF and centered_a.distance_to(centered_b) >= 0.02:
|
||||
return PackedVector3Array([to_local(centered_a), to_local(centered_b)])
|
||||
return custom_points
|
||||
|
||||
func _make_custom_floor_mesh() -> ArrayMesh:
|
||||
var polygon: Array[Vector2] = []
|
||||
for point in custom_points:
|
||||
polygon.append(Vector2(point.x, point.z))
|
||||
var hatch_mesh := _make_hatch_surface_mesh(polygon, custom_points[0].y, custom_points[0].y - thickness)
|
||||
if hatch_mesh != null:
|
||||
return hatch_mesh
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var points := custom_points
|
||||
var bottom: Array[Vector3] = []
|
||||
for point in points:
|
||||
bottom.append(point - Vector3.UP * thickness)
|
||||
for i in range(1, points.size() - 1):
|
||||
_add_tri(vertices, normals, points[0], points[i + 1], points[i])
|
||||
_add_tri(vertices, normals, bottom[0], bottom[i], bottom[i + 1])
|
||||
for i in range(points.size()):
|
||||
var j := (i + 1) % points.size()
|
||||
_add_quad(vertices, normals, bottom[i], bottom[j], points[j], points[i])
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _make_hatch_surface_mesh(polygon: Array[Vector2], top_y: float, bottom_y: float) -> ArrayMesh:
|
||||
var openings: Array[PackedVector2Array] = []
|
||||
for child in get_children():
|
||||
if _is_detail_node(child) and int(child.get("detail_type")) == DETAIL_TYPE_HATCH:
|
||||
var detail := child as Node3D
|
||||
var center := Vector2(detail.position.x, detail.position.z)
|
||||
var axis_x := Vector2(detail.transform.basis.x.x, detail.transform.basis.x.z).normalized()
|
||||
var axis_y := Vector2(detail.transform.basis.z.x, detail.transform.basis.z.z).normalized()
|
||||
var opening := PackedVector2Array([
|
||||
center - axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
|
||||
center + axis_x * float(detail.get("width")) * 0.5 - axis_y * float(detail.get("height")) * 0.5,
|
||||
center + axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5,
|
||||
center - axis_x * float(detail.get("width")) * 0.5 + axis_y * float(detail.get("height")) * 0.5
|
||||
])
|
||||
openings.append(opening)
|
||||
if openings.is_empty() or polygon.size() < 3:
|
||||
return null
|
||||
var min_x := polygon[0].x
|
||||
var max_x := polygon[0].x
|
||||
var min_z := polygon[0].y
|
||||
var max_z := polygon[0].y
|
||||
var xs: Array[float] = []
|
||||
var zs: Array[float] = []
|
||||
for point in polygon:
|
||||
min_x = minf(min_x, point.x)
|
||||
max_x = maxf(max_x, point.x)
|
||||
min_z = minf(min_z, point.y)
|
||||
max_z = maxf(max_z, point.y)
|
||||
xs.append(point.x)
|
||||
zs.append(point.y)
|
||||
for opening in openings:
|
||||
for point in opening:
|
||||
xs.append(clampf(point.x, min_x, max_x))
|
||||
zs.append(clampf(point.y, min_z, max_z))
|
||||
xs.sort()
|
||||
zs.sort()
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
var source_polygon := PackedVector2Array()
|
||||
for point in polygon:
|
||||
source_polygon.append(point)
|
||||
for xi in range(xs.size() - 1):
|
||||
for zi in range(zs.size() - 1):
|
||||
var x0: float = xs[xi]
|
||||
var x1: float = xs[xi + 1]
|
||||
var z0: float = zs[zi]
|
||||
var z1: float = zs[zi + 1]
|
||||
if x1 - x0 < 0.0001 or z1 - z0 < 0.0001:
|
||||
continue
|
||||
var cell := PackedVector2Array([Vector2(x0, z0), Vector2(x1, z0), Vector2(x1, z1), Vector2(x0, z1)])
|
||||
var remaining: Array[PackedVector2Array] = []
|
||||
for clipped_surface in Geometry2D.intersect_polygons(source_polygon, cell):
|
||||
remaining.append(clipped_surface)
|
||||
for opening in openings:
|
||||
var next_remaining: Array[PackedVector2Array] = []
|
||||
for surface_polygon in remaining:
|
||||
for clipped_surface in Geometry2D.clip_polygons(surface_polygon, opening):
|
||||
if clipped_surface.size() >= 3:
|
||||
next_remaining.append(clipped_surface)
|
||||
remaining = next_remaining
|
||||
for clipped_surface in remaining:
|
||||
var triangulated := Geometry2D.triangulate_polygon(clipped_surface)
|
||||
for ti in range(0, triangulated.size(), 3):
|
||||
var a: Vector2 = clipped_surface[triangulated[ti]]
|
||||
var b: Vector2 = clipped_surface[triangulated[ti + 1]]
|
||||
var c: Vector2 = clipped_surface[triangulated[ti + 2]]
|
||||
_add_tri(vertices, normals, Vector3(a.x, top_y, a.y), Vector3(c.x, top_y, c.y), Vector3(b.x, top_y, b.y))
|
||||
_add_tri(vertices, normals, Vector3(a.x, bottom_y, a.y), Vector3(b.x, bottom_y, b.y), Vector3(c.x, bottom_y, c.y))
|
||||
for i in range(polygon.size()):
|
||||
var j := (i + 1) % polygon.size()
|
||||
var a := polygon[i]
|
||||
var b := polygon[j]
|
||||
_add_quad(vertices, normals, Vector3(a.x, bottom_y, a.y), Vector3(b.x, bottom_y, b.y), Vector3(b.x, top_y, b.y), Vector3(a.x, top_y, a.y))
|
||||
for opening in openings:
|
||||
var visible_edges := Geometry2D.intersect_polygons(opening, source_polygon)
|
||||
for visible_opening in visible_edges:
|
||||
for edge_index in range(visible_opening.size()):
|
||||
var p0 := visible_opening[edge_index]
|
||||
var p1 := visible_opening[(edge_index + 1) % visible_opening.size()]
|
||||
_add_quad(vertices, normals, Vector3(p0.x, bottom_y, p0.y), Vector3(p0.x, top_y, p0.y), Vector3(p1.x, top_y, p1.y), Vector3(p1.x, bottom_y, p1.y))
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _is_detail_node(node: Node) -> bool:
|
||||
if not node is Node3D or node.get_script() == null:
|
||||
return false
|
||||
return node.get_script().resource_path == DETAIL_SCRIPT_PATH
|
||||
|
||||
func _make_custom_foundation_mesh() -> ArrayMesh:
|
||||
var polygon: Array[Vector2] = []
|
||||
for point in custom_points:
|
||||
polygon.append(Vector2(point.x, point.z))
|
||||
var bottom_y := -thickness * 0.5 if centered_origin else 0.0
|
||||
var top_y := thickness * 0.5 if centered_origin else thickness
|
||||
var hatch_mesh := _make_hatch_surface_mesh(polygon, top_y, bottom_y)
|
||||
if hatch_mesh != null:
|
||||
return hatch_mesh
|
||||
var vertices := PackedVector3Array()
|
||||
var normals := PackedVector3Array()
|
||||
for i in range(1, custom_points.size() - 1):
|
||||
var a := custom_points[0]
|
||||
var b := custom_points[i]
|
||||
var c := custom_points[i + 1]
|
||||
_add_tri(vertices, normals, Vector3(a.x, top_y, a.z), Vector3(c.x, top_y, c.z), Vector3(b.x, top_y, b.z))
|
||||
_add_tri(vertices, normals, Vector3(a.x, bottom_y, a.z), Vector3(b.x, bottom_y, b.z), Vector3(c.x, bottom_y, c.z))
|
||||
for i in range(custom_points.size()):
|
||||
var j := (i + 1) % custom_points.size()
|
||||
var a := custom_points[i]
|
||||
var b := custom_points[j]
|
||||
_add_quad(vertices, normals, Vector3(a.x, bottom_y, a.z), Vector3(b.x, bottom_y, b.z), Vector3(b.x, top_y, b.z), Vector3(a.x, top_y, a.z))
|
||||
return _array_mesh(vertices, normals)
|
||||
|
||||
func _add_quad(v: PackedVector3Array, n: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3, d: Vector3) -> void:
|
||||
_add_tri(v, n, a, b, c)
|
||||
_add_tri(v, n, a, c, d)
|
||||
|
||||
func _array_mesh(v: PackedVector3Array, n: PackedVector3Array) -> ArrayMesh:
|
||||
var arrays: Array = []
|
||||
arrays.resize(Mesh.ARRAY_MAX)
|
||||
arrays[Mesh.ARRAY_VERTEX] = v
|
||||
arrays[Mesh.ARRAY_NORMAL] = n
|
||||
var result := ArrayMesh.new()
|
||||
result.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
return result
|
||||
|
||||
func _add_tri(v: PackedVector3Array, n: PackedVector3Array, a: Vector3, b: Vector3, c: Vector3) -> void:
|
||||
var normal := (b - a).cross(c - a).normalized()
|
||||
v.append(a)
|
||||
v.append(b)
|
||||
v.append(c)
|
||||
n.append(normal)
|
||||
n.append(normal)
|
||||
n.append(normal)
|
||||
# Also generate reversed faces to show both sides.
|
||||
# This keeps floors and foundations visible from below with any material.
|
||||
v.append(c)
|
||||
v.append(b)
|
||||
v.append(a)
|
||||
n.append(-normal)
|
||||
n.append(-normal)
|
||||
n.append(-normal)
|
||||
1
addons/modular_builder_en/modular_piece_geometry.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dp1333xlylabf
|
||||
34
addons/modular_builder_en/modular_pillar_support.gd
Normal file
|
|
@ -0,0 +1,34 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
static func vertical_direction_for_piece(piece: ModularBuilderPiece, world_anchor: Vector3) -> Vector3:
|
||||
var local_anchor := piece.to_local(world_anchor)
|
||||
var low_y := 0.0
|
||||
var high_y := piece.unit_size
|
||||
match piece.piece_type:
|
||||
ModularBuilderPiece.PieceType.FOUNDATION_SQUARE, ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
low_y = -piece.thickness * 0.5 if piece.centered_origin else 0.0
|
||||
high_y = piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
ModularBuilderPiece.PieceType.FLOOR:
|
||||
high_y = piece.custom_points[0].y if not piece.custom_points.is_empty() else (piece.thickness * 0.5 if piece.centered_origin else piece.thickness)
|
||||
low_y = high_y - piece.thickness
|
||||
ModularBuilderPiece.PieceType.GABLE_WALL, ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
if piece.custom_points.size() >= 2:
|
||||
var base_points: PackedVector3Array = piece.call("_get_centered_gable_base") if piece.center_align else piece.custom_points
|
||||
low_y = base_points[0].y
|
||||
high_y = low_y + piece.rise
|
||||
_:
|
||||
if piece.custom_points.size() >= 2:
|
||||
var nearest_endpoint := piece.custom_points[0]
|
||||
var nearest_horizontal := INF
|
||||
for endpoint in piece.custom_points:
|
||||
var horizontal := Vector2(endpoint.x - local_anchor.x, endpoint.z - local_anchor.z).length_squared()
|
||||
if horizontal < nearest_horizontal:
|
||||
nearest_horizontal = horizontal
|
||||
nearest_endpoint = endpoint
|
||||
low_y = nearest_endpoint.y
|
||||
high_y = nearest_endpoint.y + piece.unit_size
|
||||
return Vector3.DOWN if absf(local_anchor.y - low_y) < absf(local_anchor.y - high_y) else Vector3.UP
|
||||
|
||||
static func vertical_direction_for_pillar(pillar: ModularBuilderDetail, world_anchor: Vector3) -> Vector3:
|
||||
return Vector3.DOWN if pillar.to_local(world_anchor).y < 0.0 else Vector3.UP
|
||||
1
addons/modular_builder_en/modular_pillar_support.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://df6psamkqmyvg
|
||||
72
addons/modular_builder_en/modular_roof_support.gd
Normal file
|
|
@ -0,0 +1,72 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
# Find the roof outline from two diagonal corners at the top
|
||||
# of parallel walls, even when the structure is rotated around Y.
|
||||
static func find_corners(root: ModularBuilderPiece, first_world: Vector3, second_world: Vector3, allow_center: bool = false) -> PackedVector3Array:
|
||||
var first_edges := _matching_top_edges(root, first_world, allow_center)
|
||||
var second_edges := _matching_top_edges(root, second_world, allow_center)
|
||||
var best_corners := PackedVector3Array()
|
||||
var best_score := INF
|
||||
var best_length := 0.0
|
||||
for first_edge in first_edges:
|
||||
for second_edge in second_edges:
|
||||
if first_edge["wall"] == second_edge["wall"]:
|
||||
continue
|
||||
var a: Vector3 = first_edge["selected"]
|
||||
var adjacent_a: Vector3 = first_edge["other"]
|
||||
var c: Vector3 = second_edge["selected"]
|
||||
var adjacent_c: Vector3 = second_edge["other"]
|
||||
var edge_a := Vector2(adjacent_a.x - a.x, adjacent_a.z - a.z)
|
||||
var edge_c := Vector2(adjacent_c.x - c.x, adjacent_c.z - c.z)
|
||||
var length_a := edge_a.length()
|
||||
var length_c := edge_c.length()
|
||||
if length_a < 0.2 or length_c < 0.2:
|
||||
continue
|
||||
if edge_a.normalized().dot(edge_c.normalized()) > -0.9:
|
||||
continue
|
||||
var center_a := (a + adjacent_a) * 0.5
|
||||
var center_c := (c + adjacent_c) * 0.5
|
||||
if Vector2(center_c.x - center_a.x, center_c.z - center_a.z).length() < 0.2:
|
||||
continue
|
||||
var diagonal_error := (a + c - adjacent_a - adjacent_c).length()
|
||||
var length_error := absf(length_a - length_c)
|
||||
var tolerance := maxf(0.12, maxf(first_edge["thickness"], second_edge["thickness"]) * 0.5)
|
||||
if diagonal_error > tolerance or length_error > tolerance:
|
||||
continue
|
||||
var score := diagonal_error + length_error
|
||||
if score < best_score - 0.01 or (absf(score - best_score) <= 0.01 and length_a > best_length):
|
||||
best_score = score
|
||||
best_length = length_a
|
||||
best_corners = PackedVector3Array([a, adjacent_a, c, adjacent_c])
|
||||
return best_corners
|
||||
|
||||
static func _matching_top_edges(root: ModularBuilderPiece, world_point: Vector3, allow_center: bool) -> Array[Dictionary]:
|
||||
var result: Array[Dictionary] = []
|
||||
var walls: Array[ModularBuilderPiece] = []
|
||||
_collect_walls(root, walls)
|
||||
for wall in walls:
|
||||
var endpoints: PackedVector3Array = wall.call("_get_centered_wall_points") if wall.center_align else wall.custom_points
|
||||
if endpoints.size() < 2:
|
||||
continue
|
||||
var direction := endpoints[1] - endpoints[0]
|
||||
var horizontal := Vector2(direction.x, direction.z)
|
||||
if horizontal.length() < 0.2:
|
||||
continue
|
||||
var side := Vector3(-horizontal.y, 0.0, horizontal.x).normalized() * wall.thickness * 0.5
|
||||
for offset in [side, -side]:
|
||||
var top_a := wall.to_global(endpoints[0] + Vector3.UP * wall.unit_size + offset)
|
||||
var top_b := wall.to_global(endpoints[1] + Vector3.UP * wall.unit_size + offset)
|
||||
var center_a := wall.to_global(endpoints[0] + Vector3.UP * wall.unit_size)
|
||||
var center_b := wall.to_global(endpoints[1] + Vector3.UP * wall.unit_size)
|
||||
if top_a.distance_to(world_point) <= 0.08 or (allow_center and center_a.distance_to(world_point) <= 0.08):
|
||||
result.append({"wall": wall, "selected": root.to_local(top_a), "other": root.to_local(top_b), "thickness": wall.thickness})
|
||||
if top_b.distance_to(world_point) <= 0.08 or (allow_center and center_b.distance_to(world_point) <= 0.08):
|
||||
result.append({"wall": wall, "selected": root.to_local(top_b), "other": root.to_local(top_a), "thickness": wall.thickness})
|
||||
return result
|
||||
|
||||
static func _collect_walls(node: Node, result: Array[ModularBuilderPiece]) -> void:
|
||||
if node is ModularBuilderPiece and node.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
result.append(node)
|
||||
for child in node.get_children():
|
||||
_collect_walls(child, result)
|
||||
1
addons/modular_builder_en/modular_roof_support.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://c3m5254irja4m
|
||||
32
addons/modular_builder_en/modular_vertex_simplifier.gd
Normal file
|
|
@ -0,0 +1,32 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
|
||||
static func piece_vertex(piece: ModularBuilderPiece, world_point: Vector3) -> Vector3:
|
||||
var local := piece.to_local(world_point)
|
||||
match piece.piece_type:
|
||||
ModularBuilderPiece.PieceType.FOUNDATION_SQUARE, ModularBuilderPiece.PieceType.FOUNDATION_TRIANGLE:
|
||||
local.y = piece.thickness * 0.5 if piece.centered_origin else piece.thickness
|
||||
ModularBuilderPiece.PieceType.FLOOR:
|
||||
if not piece.custom_points.is_empty():
|
||||
local.y = piece.custom_points[0].y
|
||||
ModularBuilderPiece.PieceType.WALL, ModularBuilderPiece.PieceType.GABLE_WALL, ModularBuilderPiece.PieceType.GABLE_WALL_RIGHT:
|
||||
var endpoints: PackedVector3Array
|
||||
if piece.piece_type == ModularBuilderPiece.PieceType.WALL:
|
||||
endpoints = piece.call("_get_centered_wall_points") if piece.center_align else piece.custom_points
|
||||
else:
|
||||
endpoints = piece.call("_get_centered_gable_base") if piece.center_align else piece.custom_points
|
||||
if endpoints.size() >= 2:
|
||||
var start := Vector2(endpoints[0].x, endpoints[0].z)
|
||||
var edge := Vector2(endpoints[1].x, endpoints[1].z) - start
|
||||
if edge.length_squared() > 0.000001:
|
||||
var fraction := clampf((Vector2(local.x, local.z) - start).dot(edge) / edge.length_squared(), 0.0, 1.0)
|
||||
var center := start + edge * fraction
|
||||
local.x = center.x
|
||||
local.z = center.y
|
||||
return piece.to_global(local)
|
||||
|
||||
static func pillar_vertex(pillar: ModularBuilderDetail, world_point: Vector3) -> Vector3:
|
||||
var local := pillar.to_local(world_point)
|
||||
local.x = 0.0
|
||||
local.z = 0.0
|
||||
return pillar.to_global(local)
|
||||
|
|
@ -0,0 +1 @@
|
|||
uid://dpixdrfnwnj5m
|
||||
7
addons/modular_builder_en/plugin.cfg
Normal file
|
|
@ -0,0 +1,7 @@
|
|||
[plugin]
|
||||
|
||||
name="Modular Builder (English)"
|
||||
description="Create snap-together modular building pieces directly in the Godot editor."
|
||||
author="Michel Savadim"
|
||||
version="0.2.0"
|
||||
script="modular_builder_plugin.gd"
|
||||
162
addons/modular_builder_en/scopedvr_export.gd
Normal file
|
|
@ -0,0 +1,162 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
## ScopedVR-Bibliothek-Export für den Modular Builder (Karte t_82d2a8dd).
|
||||
##
|
||||
## Kern-Logik des „Struktur exportieren → ScopedVR-Bibliothek"-Flows — bewusst
|
||||
## ALLES statisch und ohne Editor-UI, damit tests/test_builder_export.gd sie
|
||||
## headless prüfen kann. Der UI-Ablauf (Name-Dialog, Pipeline-Aufrufe, Status)
|
||||
## lebt in scopedvr_export_ui.gd; der Editor-Button hängt in
|
||||
## modular_builder_editor_actions.gd (_on_export_pressed → EXPORT_LIBRARY).
|
||||
##
|
||||
## Pipeline (wie Fass, docs/object_pipeline.md):
|
||||
## 1. Mesh normalisieren (Fußpunkt = y 0, XZ zentriert)
|
||||
## 2. GLB schreiben (GLTFDocument, Texturen eingebettet)
|
||||
## 3. Trimesh-Vertices (Dreiecks-Suppe, dedupliziert, 1 mm quantisiert)
|
||||
## 4. tools/objekt mesh → registry (trimesh, --budget) → client → editor
|
||||
## Die Registry bekommt "shape": "trimesh" + "vertices" + "size" (AABB) —
|
||||
## der Match-Server-Lader baut daraus ein ConcavePolygonShape3D (t_c949ebf2).
|
||||
|
||||
const QUANT := 0.001 # 1 mm — Kollision darf ~1 mm vom visuellen Mesh abweichen
|
||||
|
||||
|
||||
## Name → Objekt-ID: kleinbuchstaben, ä/ö/ü/ß aufgelöst, alles Ungültige → '_',
|
||||
## führende Ziffern weg, max. 48 Zeichen. Rückgabe "" = Name unbrauchbar.
|
||||
static func slugify(name: String) -> String:
|
||||
var s := name.strip_edges().to_lower()
|
||||
s = s.replace("ä", "ae").replace("ö", "oe").replace("ü", "ue").replace("ß", "ss")
|
||||
var out := ""
|
||||
for c in s:
|
||||
var ok := (c >= "a" and c <= "z") or (c >= "0" and c <= "9") or c == "_"
|
||||
out += c if ok else "_"
|
||||
# Führende Unterstriche/Ziffern abstreifen (IDs müssen mit Buchstabe beginnen).
|
||||
while out.length() > 0 and not (out[0] >= "a" and out[0] <= "z"):
|
||||
out = out.substr(1)
|
||||
while out.contains("__"):
|
||||
out = out.replace("__", "_")
|
||||
out = out.strip_edges()
|
||||
while out.ends_with("_"):
|
||||
out = out.substr(0, out.length() - 1)
|
||||
if out.length() > 48:
|
||||
out = out.substr(0, 48)
|
||||
if out.length() == 0 or not (out[0] >= "a" and out[0] <= "z"):
|
||||
return ""
|
||||
return out
|
||||
|
||||
|
||||
## Mesh normalisieren: Kopie, in der der Boden auf y = 0 liegt und das Haus
|
||||
## um seinen XZ-Mittelpunkt steht — der svmap-Fußpunkt-Vertrag (pos = Boden).
|
||||
## Rückgabe {mesh: ArrayMesh, offset: Vector3 (angewandter Versatz), size: Vector3 (AABB)}.
|
||||
## null im Ergebnis-Mesh = nichts zu holen (leeres Mesh).
|
||||
static func normalisiere(mesh: ArrayMesh) -> Dictionary:
|
||||
var aabb := mesh.get_aabb()
|
||||
if aabb.size.length() <= 0.0:
|
||||
return {"mesh": null, "offset": Vector3.ZERO, "size": Vector3.ZERO}
|
||||
var offset := Vector3(-aabb.get_center().x, -aabb.position.y, -aabb.get_center().z)
|
||||
var out := ArrayMesh.new()
|
||||
out.resource_name = mesh.resource_name
|
||||
for s in mesh.get_surface_count():
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays.is_empty() or not (arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array):
|
||||
continue
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
for i in verts.size():
|
||||
verts[i] = verts[i] + offset
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
var src_mat := mesh.surface_get_material(s)
|
||||
if src_mat != null:
|
||||
out.surface_set_material(out.get_surface_count() - 1, src_mat)
|
||||
return {"mesh": out, "offset": offset, "size": aabb.size}
|
||||
|
||||
|
||||
## Trimesh-Vertices (Dreiecks-Suppe, Loader-Vertrag svmap_loader._trimesh_faces):
|
||||
## je 3 Vertices ein Dreieck, aus ALLEN Oberflächen, exakte Doppel-Dreiecke
|
||||
## (quantisiert) fliegen raus — Decken/Wände aus aneinander liegenden Teilen
|
||||
## dürfen die Kollisions-Suppe nicht unnötig aufblähen.
|
||||
static func trimesh_vertices(mesh: ArrayMesh) -> PackedVector3Array:
|
||||
var seen := {}
|
||||
var out := PackedVector3Array()
|
||||
for s in mesh.get_surface_count():
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays.is_empty() or not (arrays[Mesh.ARRAY_VERTEX] is PackedVector3Array):
|
||||
continue
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
# ARRAY_INDEX ist null (nicht PackedInt32Array), wenn das Mesh nicht
|
||||
# indiziert ist — der direkte Cast bricht sonst die ganze Funktion ab.
|
||||
var idx := PackedInt32Array()
|
||||
if arrays[Mesh.ARRAY_INDEX] is PackedInt32Array:
|
||||
idx = arrays[Mesh.ARRAY_INDEX]
|
||||
var tri_count := 0
|
||||
if not idx.is_empty():
|
||||
tri_count = idx.size() / 3
|
||||
else:
|
||||
tri_count = verts.size() / 3
|
||||
for t in tri_count:
|
||||
var a: Vector3
|
||||
var b: Vector3
|
||||
var c: Vector3
|
||||
if not idx.is_empty():
|
||||
a = verts[idx[t * 3]]
|
||||
b = verts[idx[t * 3 + 1]]
|
||||
c = verts[idx[t * 3 + 2]]
|
||||
else:
|
||||
a = verts[t * 3]
|
||||
b = verts[t * 3 + 1]
|
||||
c = verts[t * 3 + 2]
|
||||
var key := _dreieck_schluessel(a, b, c)
|
||||
if key.is_empty():
|
||||
continue # Entartet (Fläche 0 in min. einer Komponente).
|
||||
if seen.has(key):
|
||||
continue
|
||||
seen[key] = true
|
||||
out.append_array(PackedVector3Array([a, b, c]))
|
||||
return out
|
||||
|
||||
|
||||
## Sortierter Quantisierungs-Schlüssel eines Dreiecks (unabhängig von der
|
||||
## Vertex-Reihenfolge) — "" bei entarteten Dreiecken (zwei identische Punkte).
|
||||
static func _dreieck_schluessel(a: Vector3, b: Vector3, c: Vector3) -> String:
|
||||
var pts := [_quant(a), _quant(b), _quant(c)]
|
||||
if pts[0] == pts[1] or pts[1] == pts[2] or pts[0] == pts[2]:
|
||||
return ""
|
||||
pts.sort()
|
||||
return "%s|%s|%s" % [pts[0], pts[1], pts[2]]
|
||||
|
||||
|
||||
static func _quant(v: Vector3) -> String:
|
||||
return "%d,%d,%d" % [roundi(v.x / QUANT), roundi(v.y / QUANT), roundi(v.z / QUANT)]
|
||||
|
||||
|
||||
## Mesh als GLB-Bytes (Texturen eingebettet, image/png) — über GLTFDocument,
|
||||
## funktioniert headless und im Editor (Probe: tests/test_builder_export.gd).
|
||||
## Leere Bytes = Export fehlgeschlagen.
|
||||
static func glb_bytes(mesh: ArrayMesh, name_hint := "Struktur") -> PackedByteArray:
|
||||
if mesh == null:
|
||||
return PackedByteArray() # Kein Mesh → Export fehlschlagen, kein Engine-Error.
|
||||
var root := Node3D.new()
|
||||
root.name = name_hint
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.name = "BakedMesh"
|
||||
mi.mesh = mesh
|
||||
root.add_child(mi)
|
||||
var state := GLTFState.new()
|
||||
var doc := GLTFDocument.new()
|
||||
var err := doc.append_from_scene(root, state)
|
||||
if err != OK:
|
||||
return PackedByteArray()
|
||||
var bytes := doc.generate_buffer(state)
|
||||
root.free()
|
||||
return bytes
|
||||
|
||||
|
||||
## GLB-Bytes grob prüfen (glTF-Magic + Version + JSON-Chunk) — reicht für die
|
||||
## Selbst-Verifikation VOR dem Pipeline-Aufruf; die Dreiecks-Zählung macht
|
||||
## tools/objekt eh noch mal selbst.
|
||||
static func glb_gueltig(bytes: PackedByteArray) -> bool:
|
||||
if bytes.size() < 20:
|
||||
return false
|
||||
if bytes.decode_u32(0) != 0x46546C67 or bytes.decode_u32(4) != 2:
|
||||
return false
|
||||
if bytes.decode_u32(16) != 0x4E4F534A:
|
||||
return false
|
||||
return 20 + bytes.decode_u32(12) <= bytes.size()
|
||||
1
addons/modular_builder_en/scopedvr_export.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://du56x33pwyb4s
|
||||
250
addons/modular_builder_en/scopedvr_export_ui.gd
Normal file
|
|
@ -0,0 +1,250 @@
|
|||
@tool
|
||||
extends RefCounted
|
||||
## UI-Ablauf des ScopedVR-Bibliothek-Exports (Karte t_82d2a8dd): Name-Dialog →
|
||||
## Mesh normalisieren → GLB + Trimesh-Vertices → tools/objekt (Match-Server-Repo)
|
||||
## → Thumbnail → Status.
|
||||
##
|
||||
## Bewusst getrennt von scopedvr_export.gd (Kern, statisch/testbar) — dieses
|
||||
## Skript braucht die Editor-Umgebung (Dialoge, OS.execute, Rendering).
|
||||
## Deutsch für Sly: alle Status-/Fehlermeldungen auf Deutsch.
|
||||
|
||||
const KERN := preload("res://addons/modular_builder_en/scopedvr_export.gd")
|
||||
const BUDGET := 20000 # Export-Strukturen (match-server BUILDER_BUDGET)
|
||||
const THUMB_SIZE := 128
|
||||
|
||||
var _plugin: EditorPlugin
|
||||
var _status: Callable
|
||||
var _building: Node3D
|
||||
var _mesh: ArrayMesh
|
||||
var _dialog: AcceptDialog
|
||||
var _name_edit: LineEdit
|
||||
|
||||
|
||||
## Startet den Flow. building = gewählte Struktur, mesh = gebackenes Mesh
|
||||
## (aus modular_mesh_baker), status = Callable(label: String) für das Dock-Statusfeld.
|
||||
func starten(plugin: EditorPlugin, building: Node3D, mesh: ArrayMesh, status: Callable) -> void:
|
||||
_plugin = plugin
|
||||
_building = building
|
||||
_mesh = mesh
|
||||
_status = status
|
||||
if _dialog != null:
|
||||
_dialog.queue_free()
|
||||
_dialog = AcceptDialog.new()
|
||||
_dialog.title = "Struktur exportieren — ScopedVR-Bibliothek"
|
||||
_dialog.ok_button_text = "Exportieren"
|
||||
var vbox := VBoxContainer.new()
|
||||
vbox.custom_minimum_size = Vector2(380, 0)
|
||||
var hinweis := Label.new()
|
||||
hinweis.text = "Name der Struktur (wird zur Objekt-ID: kleine Buchstaben,\nZahlen, Unterstrich — z. B. „Bauernhaus Rot\" → haus_bauernhaus_rot... \nrichtig: „Bauernhaus rot\" → bauernhaus_rot).\n\nDer Export durchläuft die Objekt-Pipeline (Mesh, Registry,\nClient, Editor) — der Client-Import dauert einen Moment."
|
||||
hinweis.autowrap_mode = TextServer.AUTOWRAP_WORD_SMART
|
||||
vbox.add_child(hinweis)
|
||||
_name_edit = LineEdit.new()
|
||||
_name_edit.text = str(building.name)
|
||||
vbox.add_child(_name_edit)
|
||||
_dialog.add_child(vbox)
|
||||
_dialog.confirmed.connect(_on_name_ok)
|
||||
plugin.add_child(_dialog)
|
||||
_name_edit.text_submitted.connect(func(_t: String) -> void:
|
||||
_dialog.hide()
|
||||
_on_name_ok())
|
||||
_name_edit.call_deferred("grab_focus")
|
||||
_dialog.popup_centered()
|
||||
|
||||
|
||||
func _on_name_ok() -> void:
|
||||
var name_roh := _name_edit.text
|
||||
var oid := KERN.slugify(name_roh)
|
||||
if oid.is_empty():
|
||||
_status.call("FEHLER: Aus dem Namen „%s\" wird keine gültige Objekt-ID — bitte Buchstaben verwenden." % name_roh)
|
||||
return
|
||||
_status.call("Export läuft: „%s\" → %s (Pipeline: Mesh → Registry → Client → Editor) ..." % [name_roh, oid])
|
||||
# Einen Frame abwarten, damit der Status sichtbar wird, bevor OS.execute blockiert.
|
||||
await _plugin.get_tree().process_frame
|
||||
await _export(oid, name_roh)
|
||||
|
||||
|
||||
func _export(oid: String, name_roh: String) -> void:
|
||||
var server_dir := _match_server_dir()
|
||||
if server_dir.is_empty():
|
||||
_status.call("FEHLER: Match-Server-Repo nicht gefunden — erwartet ../scopedvr-match-server neben dem Map-Editor (oder SCOPEDVR_MATCH_SERVER_DIR).")
|
||||
return
|
||||
var objekt_tool := server_dir + "/tools/objekt"
|
||||
|
||||
# 1) Normalisieren (Fußpunkt y=0, XZ zentriert).
|
||||
var norm: Dictionary = KERN.normalisiere(_mesh)
|
||||
var mesh: ArrayMesh = norm.get("mesh")
|
||||
if mesh == null:
|
||||
_status.call("FEHLER: Das gebackene Mesh ist leer — nichts zu exportieren.")
|
||||
return
|
||||
var size: Vector3 = norm["size"]
|
||||
var faces := KERN.trimesh_vertices(mesh)
|
||||
if faces.is_empty():
|
||||
_status.call("FEHLER: Keine Kollisions-Dreiecke im gebackenen Mesh.")
|
||||
return
|
||||
|
||||
# 2) GLB + Vertices temporär schreiben.
|
||||
var tmp_dir := _tmp_dir()
|
||||
var glb_path := tmp_dir + "/scopedvr_export_%s.glb" % oid
|
||||
var verts_path := tmp_dir + "/scopedvr_export_%s.json" % oid
|
||||
var glb := KERN.glb_bytes(mesh, oid)
|
||||
if glb.is_empty() or not KERN.glb_gueltig(glb):
|
||||
_status.call("FEHLER: GLB-Export fehlgeschlagen (GLTFDocument).")
|
||||
return
|
||||
var gf := FileAccess.open(glb_path, FileAccess.WRITE)
|
||||
if gf == null:
|
||||
_status.call("FEHLER: Konnte %s nicht schreiben." % glb_path)
|
||||
return
|
||||
gf.store_buffer(glb)
|
||||
gf.close()
|
||||
var vertices_liste: Array = []
|
||||
for v in faces:
|
||||
vertices_liste.append([_r(v.x), _r(v.y), _r(v.z)])
|
||||
var verts_json := JSON.stringify({
|
||||
"vertices": vertices_liste,
|
||||
"size": [_r(size.x), _r(size.y), _r(size.z)],
|
||||
})
|
||||
var vf := FileAccess.open(verts_path, FileAccess.WRITE)
|
||||
if vf == null:
|
||||
_status.call("FEHLER: Konnte %s nicht schreiben." % verts_path)
|
||||
return
|
||||
vf.store_string(verts_json)
|
||||
vf.close()
|
||||
|
||||
# 3) Pipeline-Schritte 1–2 (Mesh + Registry). Fehler = sofort raus, klare Meldung.
|
||||
var schritt1 := _lauf(objekt_tool, ["mesh", oid, glb_path, "--budget", str(BUDGET)])
|
||||
if schritt1 != "":
|
||||
_status.call(schritt1)
|
||||
return
|
||||
var schritt2 := _lauf(objekt_tool, ["registry", oid, name_roh.strip_edges(), "trimesh", "--vertices", verts_path, "--budget", str(BUDGET)])
|
||||
if schritt2 != "":
|
||||
_status.call(schritt2)
|
||||
return
|
||||
|
||||
# 4) Client-Sync (kopiert GLB + Registry in den Client, Import-Probe).
|
||||
_status.call("Export läuft: Client-Import (dauert einen Moment) ...")
|
||||
var schritt3 := _lauf(objekt_tool, ["client", oid])
|
||||
if schritt3 != "":
|
||||
_status.call(schritt3)
|
||||
return
|
||||
|
||||
# 5) Thumbnail AUS DEM ECHTEN MESH rendern (bevor der Editor-Sync kommt —
|
||||
# gen_thumbs überspringt Trimesh-Objekte mit vorhandener Datei).
|
||||
var editor_dir := _editor_dir()
|
||||
if not editor_dir.is_empty():
|
||||
var img := await _thumbnail_bilden(mesh)
|
||||
if img != null:
|
||||
DirAccess.make_dir_recursive_absolute(editor_dir + "/library/thumbs")
|
||||
img.save_png(editor_dir + "/library/thumbs/" + oid + ".png")
|
||||
|
||||
# 6) Editor-Sync (Registry-Kopie; vorhandene Thumbnails bleiben stehen).
|
||||
var schritt4 := _lauf(objekt_tool, ["editor", oid])
|
||||
if schritt4 != "":
|
||||
_status.call(schritt4)
|
||||
return
|
||||
|
||||
var vis_tris := 0
|
||||
var tris := int(faces.size() / 3)
|
||||
for s in mesh.get_surface_count():
|
||||
var arrays := mesh.surface_get_arrays(s)
|
||||
if arrays.is_empty():
|
||||
continue
|
||||
# ARRAY_INDEX ist null bei nicht-indizierten Oberflächen (kein Cast!).
|
||||
var idx := PackedInt32Array()
|
||||
if arrays[Mesh.ARRAY_INDEX] is PackedInt32Array:
|
||||
idx = arrays[Mesh.ARRAY_INDEX]
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
vis_tris += (idx.size() / 3) if not idx.is_empty() else (verts.size() / 3)
|
||||
_status.call("OK: „%s\" als '%s' exportiert (poly %d, Trimesh %d Dreiecke, Budget %d). Objekt liegt in Bibliothek + Client — Einchecken nicht vergessen (git add in 3 Repos)." % [name_roh, oid, vis_tris, tris, BUDGET])
|
||||
_aufraeumen([glb_path, verts_path])
|
||||
|
||||
|
||||
func _aufraeumen(pfade: Array) -> void:
|
||||
for p in pfade:
|
||||
if FileAccess.file_exists(p):
|
||||
DirAccess.remove_absolute(p)
|
||||
|
||||
|
||||
func _lauf(objekt_tool: String, args: PackedStringArray) -> String:
|
||||
var out := []
|
||||
var code: int = OS.execute(objekt_tool, args, out, true)
|
||||
var text := "\n".join(out)
|
||||
if code == 0:
|
||||
return ""
|
||||
# Die FEHLER-Zeile des Werkzeugs durchreichen (letzte gewinnt).
|
||||
var fehler := "Pipeline fehlgeschlagen (Exit %d)." % code
|
||||
for zeile in text.split("\n"):
|
||||
if str(zeile).begins_with("FEHLER: "):
|
||||
fehler = str(zeile)
|
||||
elif str(zeile).begins_with("FEHLER:"):
|
||||
fehler = str(zeile)
|
||||
return fehler
|
||||
|
||||
|
||||
func _match_server_dir() -> String:
|
||||
var env := OS.get_environment("SCOPEDVR_MATCH_SERVER_DIR")
|
||||
if not env.is_empty() and DirAccess.dir_exists_absolute(env):
|
||||
return env.rstrip("/")
|
||||
var root := ProjectSettings.globalize_path("res://").rstrip("/")
|
||||
var cand := root.path_join("../scopedvr-match-server")
|
||||
if DirAccess.dir_exists_absolute(cand):
|
||||
return cand
|
||||
return ""
|
||||
|
||||
|
||||
func _editor_dir() -> String:
|
||||
var env := OS.get_environment("SCOPEDVR_EDITOR_DIR")
|
||||
if not env.is_empty() and DirAccess.dir_exists_absolute(env):
|
||||
return env.rstrip("/")
|
||||
return ProjectSettings.globalize_path("res://").rstrip("/")
|
||||
|
||||
|
||||
func _tmp_dir() -> String:
|
||||
var t := OS.get_environment("TMPDIR")
|
||||
if t.is_empty():
|
||||
t = "/tmp"
|
||||
return t.rstrip("/")
|
||||
|
||||
|
||||
static func _r(v: float) -> float:
|
||||
return snappedf(v, 0.0001)
|
||||
|
||||
|
||||
## Thumbnail aus dem Mesh rendern (Editor-Rendering vorausgesetzt) —
|
||||
## gleicher Stil wie tools/gen_thumbs.gd (128 px, dunkler Hintergrund).
|
||||
func _thumbnail_bilden(mesh: ArrayMesh) -> Image:
|
||||
var vp := SubViewport.new()
|
||||
vp.size = Vector2i(THUMB_SIZE, THUMB_SIZE)
|
||||
vp.own_world_3d = true
|
||||
vp.render_target_update_mode = SubViewport.UPDATE_ONCE
|
||||
_plugin.add_child(vp)
|
||||
var env := Environment.new()
|
||||
env.background_mode = Environment.BG_COLOR
|
||||
env.background_color = Color(0.16, 0.18, 0.21)
|
||||
env.ambient_light_source = Environment.AMBIENT_SOURCE_COLOR
|
||||
env.ambient_light_color = Color(1, 1, 1)
|
||||
env.ambient_light_energy = 0.7
|
||||
var we := WorldEnvironment.new()
|
||||
we.environment = env
|
||||
vp.add_child(we)
|
||||
var sun := DirectionalLight3D.new()
|
||||
sun.rotation_degrees = Vector3(-55.0, 35.0, 0.0)
|
||||
sun.light_energy = 1.1
|
||||
vp.add_child(sun)
|
||||
var mi := MeshInstance3D.new()
|
||||
mi.mesh = mesh
|
||||
vp.add_child(mi)
|
||||
var cam := Camera3D.new()
|
||||
cam.current = true
|
||||
vp.add_child(cam)
|
||||
var aabb := mesh.get_aabb()
|
||||
var center := aabb.get_center()
|
||||
var radius := aabb.size.length() * 0.5
|
||||
var dir := Vector3(1.0, 0.65, 1.0).normalized()
|
||||
cam.position = center + dir * (radius * 2.3 + 0.3)
|
||||
cam.look_at(center, Vector3.UP)
|
||||
# Ein Frame Rendering abwarten, dann auslesen und aufräumen.
|
||||
await _plugin.get_tree().process_frame
|
||||
await _plugin.get_tree().process_frame
|
||||
var img := vp.get_texture().get_image()
|
||||
vp.queue_free()
|
||||
return img
|
||||
1
addons/modular_builder_en/scopedvr_export_ui.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://dx8fw7ovdhua0
|
||||
|
|
@ -19,7 +19,7 @@ window/stretch/mode="disabled"
|
|||
|
||||
[editor_plugins]
|
||||
|
||||
enabled=PackedStringArray()
|
||||
enabled=PackedStringArray("res://addons/modular_builder_en/plugin.cfg")
|
||||
|
||||
[rendering]
|
||||
|
||||
|
|
|
|||
141
tests/test_builder_export.gd
Normal file
|
|
@ -0,0 +1,141 @@
|
|||
extends SceneTree
|
||||
|
||||
## Unit-Tests des ScopedVR-Bibliothek-Exports (Karte t_82d2a8dd) — Kern-Logik
|
||||
## aus addons/modular_builder_en/scopedvr_export.gd, komplett headless:
|
||||
## - slugify (Umlaute, Leerzeichen, Zahlen-Anfang, Länge)
|
||||
## - normalisiere (Fußpunkt y=0, XZ zentriert, Materialien bleiben)
|
||||
## - trimesh_vertices (Dreiecks-Suppe, Dedup, Quantisierung)
|
||||
## - glb_bytes (glTF-Magic, Texturen eingebettet, Dreiecke drin)
|
||||
## Run: godot --headless --path . --script res://tests/test_builder_export.gd
|
||||
|
||||
const KERN := preload("res://addons/modular_builder_en/scopedvr_export.gd")
|
||||
|
||||
var _failures: Array[String] = []
|
||||
var _checks: int = 0
|
||||
|
||||
|
||||
func _initialize() -> void:
|
||||
_test_slugify()
|
||||
_test_normalisiere()
|
||||
_test_trimesh()
|
||||
_test_glb()
|
||||
print("--------------------------------------------------")
|
||||
print("CHECKS: %d FAILURES: %d" % [_checks, _failures.size()])
|
||||
for f in _failures:
|
||||
print("FAIL: " + f)
|
||||
quit(0 if _failures.is_empty() else 1)
|
||||
|
||||
|
||||
func _check(cond: bool, label: String) -> void:
|
||||
_checks += 1
|
||||
if cond:
|
||||
print(" ok - " + label)
|
||||
else:
|
||||
_failures.append(label)
|
||||
print(" FAIL - " + label)
|
||||
|
||||
|
||||
func _test_slugify() -> void:
|
||||
print("Testgruppe: slugify")
|
||||
_check(KERN.slugify("Bauernhaus rot") == "bauernhaus_rot", "'Bauernhaus rot' → bauernhaus_rot")
|
||||
_check(KERN.slugify("Häuschen Größe 2") == "haeuschen_groesse_2", "Umlaute werden aufgelöst (ist %s)" % KERN.slugify("Häuschen Größe 2"))
|
||||
_check(KERN.slugify("Straße") == "strasse", "'Straße' → strasse")
|
||||
_check(KERN.slugify("2 Häuser") == "haeuser", "führende Ziffern fliegen (ist %s)" % KERN.slugify("2 Häuser"))
|
||||
_check(KERN.slugify(" schon_gut ") == "schon_gut", "Whitespace/Unterstrich-Ränder weg")
|
||||
_check(KERN.slugify("!!!") == "", "nur Sonderzeichen → leer")
|
||||
_check(KERN.slugify("a".repeat(60)).length() == 48, "max. 48 Zeichen")
|
||||
_check(KERN.slugify("Bauernhaus--rot") == "bauernhaus_rot", "Doppel-Unterstrich kollabieren")
|
||||
|
||||
|
||||
func _test_normalisiere() -> void:
|
||||
print("Testgruppe: normalisiere")
|
||||
var box := BoxMesh.new()
|
||||
box.size = Vector3(2, 1, 4)
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_color = Color(1, 0, 0)
|
||||
var root_mesh := _mesh_aus_bodies([{"mesh": box, "pos": Vector3(10, 5, -20), "mat": mat}])
|
||||
var norm: Dictionary = KERN.normalisiere(root_mesh)
|
||||
var m: ArrayMesh = norm["mesh"]
|
||||
_check(m != null and m.get_surface_count() == 1, "normalisiere: 1 Oberfläche bleibt")
|
||||
var aabb := m.get_aabb()
|
||||
_check(absf(aabb.position.y) < 0.001, "normalisiere: Boden liegt auf y=0 (min.y=%s)" % aabb.position.y)
|
||||
_check(absf(aabb.get_center().x) < 0.001 and absf(aabb.get_center().z) < 0.001,
|
||||
"normalisiere: XZ zentriert (center=%s)" % aabb.get_center())
|
||||
_check(aabb.size.is_equal_approx(Vector3(2, 1, 4)), "normalisiere: Größe unverändert")
|
||||
var smat := m.surface_get_material(0)
|
||||
_check(smat != null and (smat as StandardMaterial3D).albedo_color == Color(1, 0, 0),
|
||||
"normalisiere: Material bleibt erhalten")
|
||||
|
||||
|
||||
func _test_trimesh() -> void:
|
||||
print("Testgruppe: trimesh_vertices")
|
||||
# Zwei identische Dreiecke + ein echtes: Dedup lässt 3 Vertices-Tripel → 1 übrig.
|
||||
var st := SurfaceTool.new()
|
||||
st.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
st.add_vertex(Vector3(0, 0, 0)); st.add_vertex(Vector3(1, 0, 0)); st.add_vertex(Vector3(0, 0, 1))
|
||||
st.add_vertex(Vector3(0, 0, 0)); st.add_vertex(Vector3(1, 0, 0)); st.add_vertex(Vector3(0, 0, 1))
|
||||
st.add_vertex(Vector3(0, 1, 0)); st.add_vertex(Vector3(1, 1, 0)); st.add_vertex(Vector3(1, 1, 1))
|
||||
var mesh := st.commit()
|
||||
var faces := KERN.trimesh_vertices(mesh)
|
||||
_check(faces.size() == 6, "Doppel-Dreieck dedupliziert: 9 → 6 Vertices (ist %d)" % faces.size())
|
||||
# Entartetes Dreieck (zwei gleiche Punkte) fliegt raus.
|
||||
var st2 := SurfaceTool.new()
|
||||
st2.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
st2.add_vertex(Vector3(0, 0, 0)); st2.add_vertex(Vector3(0, 0, 0)); st2.add_vertex(Vector3(1, 0, 0))
|
||||
var degeneriert := KERN.trimesh_vertices(st2.commit())
|
||||
_check(degeneriert.is_empty(), "entartetes Dreieck → leer")
|
||||
# Quantisierung: 0,2-mm-Unterschied (weit unter dem 1-mm-Quant) gilt als
|
||||
# identisch — genau auf der Bucket-Grenze (0,5 mm) wäre das nicht garantiert.
|
||||
var st3 := SurfaceTool.new()
|
||||
st3.begin(Mesh.PRIMITIVE_TRIANGLES)
|
||||
st3.add_vertex(Vector3(0, 0, 0)); st3.add_vertex(Vector3(1, 0, 0)); st3.add_vertex(Vector3(0, 0, 1))
|
||||
st3.add_vertex(Vector3(0, 0.0002, 0)); st3.add_vertex(Vector3(1, 0, 0)); st3.add_vertex(Vector3(0, 0, 1))
|
||||
_check(KERN.trimesh_vertices(st3.commit()).size() == 3, "Quantisierung 1 mm fasst 0,2-mm-Unterschied zusammen")
|
||||
|
||||
|
||||
func _test_glb() -> void:
|
||||
print("Testgruppe: glb_bytes")
|
||||
var box := BoxMesh.new()
|
||||
box.size = Vector3(1, 1, 1)
|
||||
var img := Image.create(8, 8, false, Image.FORMAT_RGB8)
|
||||
img.fill(Color(0.7, 0.3, 0.1))
|
||||
var mat := StandardMaterial3D.new()
|
||||
mat.albedo_texture = ImageTexture.create_from_image(img)
|
||||
var mesh := _mesh_aus_bodies([{"mesh": box, "pos": Vector3.ZERO, "mat": mat}])
|
||||
var bytes := KERN.glb_bytes(mesh, "testhaus")
|
||||
_check(KERN.glb_gueltig(bytes), "GLB gültig (Magic + Version + JSON-Chunk, %d Bytes)" % bytes.size())
|
||||
var json_len := bytes.decode_u32(12)
|
||||
var parsed: Variant = JSON.parse_string(bytes.slice(20, 20 + json_len).get_string_from_utf8())
|
||||
var gltf: Dictionary = parsed if parsed is Dictionary else {}
|
||||
var tris := 0
|
||||
for m in gltf.get("meshes", []):
|
||||
for p in m.get("primitives", []):
|
||||
var ai: int = int(p.get("indices", -1))
|
||||
if ai >= 0:
|
||||
tris += int(gltf["accessors"][ai].get("count", 0)) / 3
|
||||
_check(tris == 12, "GLB enthält 12 Dreiecke (Box, ist %d)" % tris)
|
||||
_check((gltf.get("images", []) as Array).size() == 1, "Textur ist EINGEBETTET (images=1)")
|
||||
_check(KERN.glb_gueltig("müll".to_utf8_buffer()) == false, "Müll-Bytes werden erkannt")
|
||||
_check(KERN.glb_bytes(null).is_empty(), "null-Mesh → leere Bytes (kein Engine-Error)")
|
||||
|
||||
|
||||
## Hilfsfunktion: baut ein ArrayMesh aus BoxMeshes an Positionen (mit Material).
|
||||
func _mesh_aus_bodies(bodies: Array) -> ArrayMesh:
|
||||
var out := ArrayMesh.new()
|
||||
for b in bodies:
|
||||
var bm: Mesh = b["mesh"]
|
||||
var st := SurfaceTool.new()
|
||||
st.create_from(bm, 0)
|
||||
if b.has("mat"):
|
||||
st.set_material(b["mat"])
|
||||
var tmp := st.commit()
|
||||
var arrays := tmp.surface_get_arrays(0)
|
||||
var verts: PackedVector3Array = arrays[Mesh.ARRAY_VERTEX]
|
||||
var off: Vector3 = b.get("pos", Vector3.ZERO)
|
||||
for i in verts.size():
|
||||
verts[i] = verts[i] + off
|
||||
arrays[Mesh.ARRAY_VERTEX] = verts
|
||||
out.add_surface_from_arrays(Mesh.PRIMITIVE_TRIANGLES, arrays)
|
||||
if b.has("mat"):
|
||||
out.surface_set_material(out.get_surface_count() - 1, b["mat"])
|
||||
return out
|
||||
1
tests/test_builder_export.gd.uid
Normal file
|
|
@ -0,0 +1 @@
|
|||
uid://b1e81c1xd6aur
|
||||
|
|
@ -73,6 +73,13 @@ func _next() -> void:
|
|||
var oid := str(ids[idx])
|
||||
var entry: Dictionary = (manifest["objects"] as Dictionary)[oid]
|
||||
var col: Dictionary = entry.get("collision", {})
|
||||
# Trimesh-Objekte (Modular-Builder-Export, t_82d2a8dd): das Thumbnail wird
|
||||
# beim Export aus dem ECHTEN Mesh gerendert (der Platzhalter hier wäre nur
|
||||
# eine Box) — existiert die Datei schon, wird sie nicht überschrieben.
|
||||
if str(col.get("shape", "box")) == "trimesh" and FileAccess.file_exists(out_dir + "/" + oid + ".png"):
|
||||
print(" thumb: %s übersprungen (echtes Thumbnail vom Export)" % oid)
|
||||
_next()
|
||||
return
|
||||
var mesh := MeshShapes.make_placeholder(str(col.get("shape", "box")), col, 1.0)
|
||||
mi.mesh = mesh
|
||||
# Kamera diagonal aufs Objekt, passend zur Mesh-Größe (Kiste nah, Turm weit).
|
||||
|
|
|
|||