FerrellSyntheticIntelligence commited on
Commit ·
d26d55f
1
Parent(s): dde6b93
Add GitHub Sponsors + Buy Me A Coffee
Browse files- .github/FUNDING.yml +2 -0
- .gitignore +5 -0
- README.md +110 -24
- app.py +35 -16
- devcore/__pycache__/builder.cpython-312.pyc +0 -0
- devcore/__pycache__/parser.cpython-312.pyc +0 -0
- devcore/builder.py +36 -43
- devcore/parser.py +13 -0
- devcore/templates/viewer.html +198 -202
.github/FUNDING.yml
ADDED
|
@@ -0,0 +1,2 @@
|
|
|
|
|
|
|
|
|
|
| 1 |
+
github: [AnonymousNomad]
|
| 2 |
+
custom: ["https://www.buymeacoffee.com/anonymousnomad"]
|
.gitignore
ADDED
|
@@ -0,0 +1,5 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
__pycache__/
|
| 2 |
+
*.pyc
|
| 3 |
+
*.pyo
|
| 4 |
+
.env
|
| 5 |
+
.DS_Store
|
README.md
CHANGED
|
@@ -1,38 +1,124 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
---
|
| 2 |
-
title: Vitalis DevCore — 3D Blueprint Builder
|
| 3 |
-
emoji: 🏗️
|
| 4 |
-
colorFrom: blue
|
| 5 |
-
colorTo: indigo
|
| 6 |
-
sdk: gradio
|
| 7 |
-
sdk_version: 6.19.0
|
| 8 |
-
app_file: app.py
|
| 9 |
-
pinned: false
|
| 10 |
-
---
|
| 11 |
|
| 12 |
-
#
|
|
|
|
|
|
|
| 13 |
|
| 14 |
-
|
| 15 |
|
| 16 |
## Features
|
| 17 |
|
| 18 |
-
|
| 19 |
-
-
|
| 20 |
-
|
| 21 |
-
|
| 22 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 23 |
|
| 24 |
-
## Example Prompts
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 25 |
|
| 26 |
-
|
| 27 |
-
|
| 28 |
-
|
| 29 |
-
|
| 30 |
-
|
|
|
|
|
|
|
| 31 |
|
| 32 |
## Architecture
|
| 33 |
|
| 34 |
```
|
| 35 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 36 |
```
|
| 37 |
|
| 38 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
<p align="center">
|
| 2 |
+
<img src="https://img.shields.io/badge/Vitalis-DevCore-7c3aed?style=for-the-badge&logo=data:image/svg+xml;base64,PHN2ZyB3aWR0aD0iNDAiIGhlaWdodD0iNDAiIHZpZXdCb3g9IjAgMCA0MCA0MCIgZmlsbD0ibm9uZSIgeG1sbnM9Imh0dHA6Ly93d3cudzMub3JnLzIwMDAvc3ZnIj48cmVjdCB4PSI0IiB5PSI0IiB3aWR0aD0iMzIiIGhlaWdodD0iMzIiIHJ4PSI0IiBzdHJva2U9IiM3YzNhZWQiIHN0cm9rZS13aWR0aD0iMiIvPjxwYXRoIGQ9Ik0xNCAxOGw0LTQgNCA0LTIgMi0yLTItMiAyeiIgZmlsbD0iIzdjM2FlZCIvPjwvc3ZnPg==" alt="Vitalis DevCore"/>
|
| 3 |
+
</p>
|
| 4 |
+
|
| 5 |
+
<h1 align="center">Vitalis DevCore — 3D Blueprint Builder</h1>
|
| 6 |
+
|
| 7 |
+
<p align="center">
|
| 8 |
+
<strong>→ Rival Codex: Natural Language → Interactive 3D Architecture</strong>
|
| 9 |
+
</p>
|
| 10 |
+
|
| 11 |
+
<p align="center">
|
| 12 |
+
<a href="https://huggingface.co/spaces/FerrellSyntheticIntelligence/Vitalis-DevCore">
|
| 13 |
+
<img src="https://img.shields.io/badge/🤗%20HF%20Spaces-Live-7c3aed?style=flat-square" alt="HF Spaces"/>
|
| 14 |
+
</a>
|
| 15 |
+
<a href="https://github.com/AnonymousNomad/Vitalis-DevCore">
|
| 16 |
+
<img src="https://img.shields.io/badge/GitHub-Source-181717?style=flat-square&logo=github" alt="GitHub"/>
|
| 17 |
+
</a>
|
| 18 |
+
<a href="https://www.buymeacoffee.com/anonymousnomad">
|
| 19 |
+
<img src="https://img.shields.io/badge/Sponsor-%E2%9D%A4%EF%B8%8F-ff69b4?style=flat-square" alt="Sponsor"/>
|
| 20 |
+
</a>
|
| 21 |
+
</p>
|
| 22 |
+
|
| 23 |
---
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 24 |
|
| 25 |
+
## Overview
|
| 26 |
+
|
| 27 |
+
Vitalis DevCore transforms natural language building descriptions into interactive 3D architectural blueprints. Describe any building in plain English — "2000 sq ft ranch with 3 bedrooms and 2 baths" — and get a fully explorable 3D model with rooms, style-specific roofs, measurement labels, and camera controls.
|
| 28 |
|
| 29 |
+
**The system is live on HuggingFace Spaces** — no GPU, no install, just describe and build.
|
| 30 |
|
| 31 |
## Features
|
| 32 |
|
| 33 |
+
| Feature | Description |
|
| 34 |
+
|---|---|
|
| 35 |
+
| **Natural Language Parsing** | Extracts dimensions, style, stories, bedrooms, bathrooms, garage, extra rooms from plain English |
|
| 36 |
+
| **Architecture Styles** | Modern, Ranch, Colonial, Victorian, Cabin, Craftsman, Farmhouse, Tudor — each with style-specific roofs |
|
| 37 |
+
| **Style-Specific Roofs** | Flat (modern), Gable (colonial/cabin), Hip (ranch), Steep-gable (victorian/tudor), Low-gable (craftsman) |
|
| 38 |
+
| **Live 3D Rendering** | Three.js in-browser — walls, floors, roof, shadows, lighting |
|
| 39 |
+
| **Room Labels** | Floating sprites showing room names |
|
| 40 |
+
| **Measurement Overlays** | Dimension labels on each room (width/depth in feet) |
|
| 41 |
+
| **Interactive Controls** | Orbit (rotate), Pan (move), Tour (auto walkthrough) |
|
| 42 |
+
| **JSON Export** | Download blueprint data as structured JSON |
|
| 43 |
+
| **Extra Room Support** | Office, Library, Deck, Loft, Garage, Basement, and more |
|
| 44 |
+
|
| 45 |
+
## How It Works
|
| 46 |
+
|
| 47 |
+
```
|
| 48 |
+
"30x40 modern 2-story with 4 bedrooms and a garage"
|
| 49 |
+
↓
|
| 50 |
+
NL Parser → Structured Blueprint JSON → Three.js Scene → Interactive 3D
|
| 51 |
+
```
|
| 52 |
+
|
| 53 |
+
## Quick Start
|
| 54 |
+
|
| 55 |
+
### Try It Live (no install)
|
| 56 |
+
1. Open [Vitalis DevCore on HF Spaces](https://huggingface.co/spaces/FerrellSyntheticIntelligence/Vitalis-DevCore)
|
| 57 |
+
2. Type a building description or click an example prompt
|
| 58 |
+
3. Click **Build Blueprint**
|
| 59 |
+
4. Use mouse to orbit/zoom/pan the 3D model
|
| 60 |
+
5. Click **Tour** for auto walkthrough or **Export** to download
|
| 61 |
|
| 62 |
+
### Example Prompts
|
| 63 |
+
```
|
| 64 |
+
"2000 sq ft ranch house with 3 bedrooms and 2 baths"
|
| 65 |
+
"modern 2-story house 30x40 with 4 bedrooms 3 bathrooms and a garage"
|
| 66 |
+
"small cabin 800 sq ft with 1 bedroom and a loft"
|
| 67 |
+
"victorian 3500 sq ft 5 bedroom 3 bath with office and library"
|
| 68 |
+
"30 feet by 50 feet modern house with 4 bedrooms and a deck"
|
| 69 |
+
```
|
| 70 |
|
| 71 |
+
### Run Locally
|
| 72 |
+
```bash
|
| 73 |
+
git clone https://github.com/AnonymousNomad/Vitalis-DevCore.git
|
| 74 |
+
cd Vitalis-DevCore
|
| 75 |
+
pip install -r requirements.txt
|
| 76 |
+
python app.py
|
| 77 |
+
```
|
| 78 |
|
| 79 |
## Architecture
|
| 80 |
|
| 81 |
```
|
| 82 |
+
devcore/
|
| 83 |
+
├── parser.py # Natural language → structured blueprint data
|
| 84 |
+
│ ├── 7 extractors (sqft, dimensions, style, stories, bedrooms, bathrooms, extras)
|
| 85 |
+
│ └── Room layout generator with smart positioning
|
| 86 |
+
��── builder.py # Blueprint → Three.js HTML with style-specific roofs
|
| 87 |
+
│ ├── Measurements overlay
|
| 88 |
+
│ └── JSON export support
|
| 89 |
+
└── templates/
|
| 90 |
+
└── viewer.html # Full Three.js scene with orbit/tour/pan controls
|
| 91 |
```
|
| 92 |
|
| 93 |
+
### Roof Styles by Architecture
|
| 94 |
+
|
| 95 |
+
| Style | Roof | Description |
|
| 96 |
+
|-------|------|-------------|
|
| 97 |
+
| Modern | Flat | Clean, minimal horizontal lines |
|
| 98 |
+
| Ranch/Hip | Hip | Sloped on all four sides |
|
| 99 |
+
| Colonial | Gable | Classic triangular gable ends |
|
| 100 |
+
| Victorian | Steep-gable | Dramatic steep pitch with dormers |
|
| 101 |
+
| Craftsman | Low-gable | Low-pitch with wide eaves |
|
| 102 |
+
| Cabin | Gable | Simple rustic gable |
|
| 103 |
+
| Farmhouse | Gable | Traditional gable |
|
| 104 |
+
|
| 105 |
+
## Sponsor
|
| 106 |
+
|
| 107 |
+
If you find this tool useful for architecture visualization or design education:
|
| 108 |
+
|
| 109 |
+
<p align="center">
|
| 110 |
+
<a href="https://www.buymeacoffee.com/anonymousnomad">
|
| 111 |
+
<img src="https://img.shields.io/badge/Buy_Me_A_Coffee-FFDD00?style=for-the-badge&logo=buy-me-a-coffee&logoColor=black" alt="Buy Me A Coffee"/>
|
| 112 |
+
</a>
|
| 113 |
+
<a href="https://github.com/sponsors/AnonymousNomad">
|
| 114 |
+
<img src="https://img.shields.io/badge/GitHub_Sponsor-%23EA4AAA?style=for-the-badge&logo=github&logoColor=white" alt="GitHub Sponsor"/>
|
| 115 |
+
</a>
|
| 116 |
+
</p>
|
| 117 |
+
|
| 118 |
+
## License
|
| 119 |
+
|
| 120 |
+
MIT — build on it, share it, make it better.
|
| 121 |
+
|
| 122 |
+
---
|
| 123 |
+
|
| 124 |
+
*Built by FerrellSyntheticIntelligence for the Vitalis ecosystem.*
|
app.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
| 1 |
import gradio as gr
|
|
|
|
| 2 |
from devcore.parser import parse_description
|
| 3 |
-
from devcore.builder import description_to_blueprint
|
| 4 |
|
| 5 |
EXAMPLE_PROMPTS = [
|
| 6 |
"2000 sq ft ranch house with 3 bedrooms and 2 baths",
|
|
@@ -16,7 +17,7 @@ EXAMPLE_PROMPTS = [
|
|
| 16 |
def build_blueprint(description):
|
| 17 |
if not description or not description.strip():
|
| 18 |
html = _render_empty("Enter a description of the building you want to visualize.")
|
| 19 |
-
return html, "No description provided.", ""
|
| 20 |
try:
|
| 21 |
html = description_to_blueprint(description)
|
| 22 |
data = parse_description(description)
|
|
@@ -29,10 +30,12 @@ def build_blueprint(description):
|
|
| 29 |
f"🛏️ {f['bedrooms']} bed · 🚿 {f['bathrooms']} bath · "
|
| 30 |
f"{'🚗 Garage' if f['garage'] else 'No garage'}"
|
| 31 |
)
|
| 32 |
-
|
|
|
|
|
|
|
| 33 |
except Exception as e:
|
| 34 |
html = _render_empty(f"Error: {e}")
|
| 35 |
-
return html, f"Failed to build: {e}", ""
|
| 36 |
|
| 37 |
|
| 38 |
def _render_empty(message):
|
|
@@ -89,30 +92,46 @@ with gr.Blocks(title="Vitalis DevCore — 3D Blueprint Builder", theme=gr.themes
|
|
| 89 |
with gr.Accordion("Blueprint Details", open=False):
|
| 90 |
details_display = gr.Markdown(label="Details")
|
| 91 |
|
|
|
|
|
|
|
| 92 |
def on_build(desc):
|
| 93 |
-
html, summary, details = build_blueprint(desc)
|
| 94 |
-
return html, summary, details
|
| 95 |
|
| 96 |
build_btn.click(
|
| 97 |
fn=on_build,
|
| 98 |
inputs=[prompt],
|
| 99 |
-
outputs=[model_display, summary_display, details_display],
|
| 100 |
)
|
| 101 |
prompt.submit(
|
| 102 |
fn=on_build,
|
| 103 |
inputs=[prompt],
|
| 104 |
-
outputs=[model_display, summary_display, details_display],
|
| 105 |
)
|
| 106 |
|
| 107 |
gr.Markdown("---")
|
| 108 |
-
gr.
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
|
| 114 |
-
|
| 115 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
|
| 117 |
|
| 118 |
if __name__ == "__main__":
|
|
|
|
| 1 |
import gradio as gr
|
| 2 |
+
import json
|
| 3 |
from devcore.parser import parse_description
|
| 4 |
+
from devcore.builder import description_to_blueprint, blueprint_to_json
|
| 5 |
|
| 6 |
EXAMPLE_PROMPTS = [
|
| 7 |
"2000 sq ft ranch house with 3 bedrooms and 2 baths",
|
|
|
|
| 17 |
def build_blueprint(description):
|
| 18 |
if not description or not description.strip():
|
| 19 |
html = _render_empty("Enter a description of the building you want to visualize.")
|
| 20 |
+
return html, "No description provided.", "", ""
|
| 21 |
try:
|
| 22 |
html = description_to_blueprint(description)
|
| 23 |
data = parse_description(description)
|
|
|
|
| 30 |
f"🛏️ {f['bedrooms']} bed · 🚿 {f['bathrooms']} bath · "
|
| 31 |
f"{'🚗 Garage' if f['garage'] else 'No garage'}"
|
| 32 |
)
|
| 33 |
+
roof = s.get("roof_style", "gable")
|
| 34 |
+
extras = f"🏗️ {roof} roof · 🖱️ Orbit/Pan/Tour modes · 📥 Export JSON"
|
| 35 |
+
return html, summary, details, extras
|
| 36 |
except Exception as e:
|
| 37 |
html = _render_empty(f"Error: {e}")
|
| 38 |
+
return html, f"Failed to build: {e}", "", ""
|
| 39 |
|
| 40 |
|
| 41 |
def _render_empty(message):
|
|
|
|
| 92 |
with gr.Accordion("Blueprint Details", open=False):
|
| 93 |
details_display = gr.Markdown(label="Details")
|
| 94 |
|
| 95 |
+
extras_display = gr.Markdown(label="Features")
|
| 96 |
+
|
| 97 |
def on_build(desc):
|
| 98 |
+
html, summary, details, extras = build_blueprint(desc)
|
| 99 |
+
return html, summary, details, extras
|
| 100 |
|
| 101 |
build_btn.click(
|
| 102 |
fn=on_build,
|
| 103 |
inputs=[prompt],
|
| 104 |
+
outputs=[model_display, summary_display, details_display, extras_display],
|
| 105 |
)
|
| 106 |
prompt.submit(
|
| 107 |
fn=on_build,
|
| 108 |
inputs=[prompt],
|
| 109 |
+
outputs=[model_display, summary_display, details_display, extras_display],
|
| 110 |
)
|
| 111 |
|
| 112 |
gr.Markdown("---")
|
| 113 |
+
with gr.Row():
|
| 114 |
+
gr.Markdown("""
|
| 115 |
+
### View Controls
|
| 116 |
+
- **Orbit** — Drag to rotate, scroll to zoom
|
| 117 |
+
- **Pan** — Drag to move view
|
| 118 |
+
- **Tour** — Auto- camera walkthrough of rooms
|
| 119 |
+
- **Export** — Download blueprint as JSON
|
| 120 |
+
|
| 121 |
+
*Style-specific roofs: gable, hip, flat, steep-gable · Measurements shown on each room · All rendering in-browser*
|
| 122 |
+
""")
|
| 123 |
+
with gr.Accordion("JSON Blueprint Data (read-only)", open=False):
|
| 124 |
+
json_display = gr.Markdown("Build a blueprint first.")
|
| 125 |
+
def show_json(desc):
|
| 126 |
+
if not desc or not desc.strip():
|
| 127 |
+
return "Enter a description."
|
| 128 |
+
try:
|
| 129 |
+
return "```json\n" + blueprint_to_json(desc) + "\n```"
|
| 130 |
+
except:
|
| 131 |
+
return "Invalid description."
|
| 132 |
+
prompt.change(show_json, inputs=[prompt], outputs=json_display)
|
| 133 |
+
|
| 134 |
+
gr.Markdown("*Vitalis DevCore — Natural Language → 3D Blueprint · Built with Three.js*")
|
| 135 |
|
| 136 |
|
| 137 |
if __name__ == "__main__":
|
devcore/__pycache__/builder.cpython-312.pyc
CHANGED
|
Binary files a/devcore/__pycache__/builder.cpython-312.pyc and b/devcore/__pycache__/builder.cpython-312.pyc differ
|
|
|
devcore/__pycache__/parser.cpython-312.pyc
CHANGED
|
Binary files a/devcore/__pycache__/parser.cpython-312.pyc and b/devcore/__pycache__/parser.cpython-312.pyc differ
|
|
|
devcore/builder.py
CHANGED
|
@@ -1,5 +1,4 @@
|
|
| 1 |
-
import json
|
| 2 |
-
import os
|
| 3 |
from devcore.parser import parse_description
|
| 4 |
|
| 5 |
TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "templates", "viewer.html")
|
|
@@ -16,13 +15,15 @@ def _build_threejs_html(data):
|
|
| 16 |
w = scene["width"]
|
| 17 |
d = scene["depth"]
|
| 18 |
s = scene["stories"]
|
|
|
|
|
|
|
|
|
|
|
|
|
| 19 |
|
| 20 |
room_meshes = []
|
| 21 |
wall_meshes = []
|
| 22 |
label_data = []
|
| 23 |
-
|
| 24 |
-
wall_height = 8 if s <= 1 else 9
|
| 25 |
-
floor_height = 0.5
|
| 26 |
|
| 27 |
for room in rooms:
|
| 28 |
rx, rz = room["x"], room["z"]
|
|
@@ -30,78 +31,70 @@ def _build_threejs_html(data):
|
|
| 30 |
floor_n = room.get("floor", 0)
|
| 31 |
cy = floor_n * (wall_height + floor_height) + floor_height / 2
|
| 32 |
exterior = room.get("exterior", False)
|
| 33 |
-
|
| 34 |
color = room["color"]
|
| 35 |
|
| 36 |
room_meshes.append({
|
| 37 |
"x": rx, "z": rz, "w": rw, "d": rd,
|
| 38 |
"y": cy, "h": floor_height,
|
| 39 |
-
"color": color,
|
| 40 |
-
"name": room["name"],
|
| 41 |
-
"exterior": exterior,
|
| 42 |
})
|
| 43 |
|
| 44 |
lx = rx + rw / 2
|
| 45 |
lz = rz + rd / 2
|
| 46 |
-
label_data.append({
|
| 47 |
-
"x": lx, "z": lz, "y": cy + 0.3,
|
| 48 |
-
"text": room["name"],
|
| 49 |
-
})
|
| 50 |
|
| 51 |
-
|
| 52 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 53 |
|
| 54 |
if not exterior:
|
| 55 |
for side in ["north", "south", "east", "west"]:
|
| 56 |
wall_meshes.append({
|
| 57 |
-
"side": side,
|
| 58 |
-
"
|
| 59 |
-
"h": wall_height,
|
| 60 |
"room_name": room["name"],
|
| 61 |
})
|
| 62 |
|
| 63 |
-
|
| 64 |
-
|
| 65 |
-
|
| 66 |
-
"d": d * 1.15,
|
| 67 |
-
"
|
| 68 |
-
"
|
| 69 |
-
"color": "#5a4a3a",
|
| 70 |
}
|
| 71 |
|
| 72 |
blueprint = {
|
| 73 |
"scene": {
|
| 74 |
-
"width": w,
|
| 75 |
-
"
|
| 76 |
-
"
|
| 77 |
-
"wall_height": wall_height,
|
| 78 |
-
"floor_height": floor_height,
|
| 79 |
},
|
| 80 |
"floors": room_meshes,
|
| 81 |
"walls": wall_meshes,
|
| 82 |
-
"roof":
|
| 83 |
"labels": label_data,
|
|
|
|
| 84 |
"summary": data["summary"],
|
| 85 |
"features": data["features"],
|
| 86 |
}
|
| 87 |
|
| 88 |
-
|
| 89 |
-
return html
|
| 90 |
|
| 91 |
|
| 92 |
def _render_html(blueprint):
|
| 93 |
bp_json = json.dumps(blueprint)
|
| 94 |
-
|
| 95 |
with open(TEMPLATE_PATH, "r") as f:
|
| 96 |
template = f.read()
|
| 97 |
-
|
| 98 |
-
html = template.replace("/* BLUEPRINT_DATA */", f"const BLUEPRINT = {bp_json};")
|
| 99 |
-
return html
|
| 100 |
|
| 101 |
|
| 102 |
-
|
| 103 |
-
|
| 104 |
-
|
| 105 |
-
with open("/tmp/test_blueprint.html", "w") as f:
|
| 106 |
-
f.write(html)
|
| 107 |
-
print("Saved to /tmp/test_blueprint.html")
|
|
|
|
| 1 |
+
import json, os
|
|
|
|
| 2 |
from devcore.parser import parse_description
|
| 3 |
|
| 4 |
TEMPLATE_PATH = os.path.join(os.path.dirname(__file__), "templates", "viewer.html")
|
|
|
|
| 15 |
w = scene["width"]
|
| 16 |
d = scene["depth"]
|
| 17 |
s = scene["stories"]
|
| 18 |
+
roof_style = scene.get("roof_style", "gable")
|
| 19 |
+
|
| 20 |
+
wall_height = 8 if s <= 1 else 9
|
| 21 |
+
floor_height = 0.5
|
| 22 |
|
| 23 |
room_meshes = []
|
| 24 |
wall_meshes = []
|
| 25 |
label_data = []
|
| 26 |
+
measurements = []
|
|
|
|
|
|
|
| 27 |
|
| 28 |
for room in rooms:
|
| 29 |
rx, rz = room["x"], room["z"]
|
|
|
|
| 31 |
floor_n = room.get("floor", 0)
|
| 32 |
cy = floor_n * (wall_height + floor_height) + floor_height / 2
|
| 33 |
exterior = room.get("exterior", False)
|
|
|
|
| 34 |
color = room["color"]
|
| 35 |
|
| 36 |
room_meshes.append({
|
| 37 |
"x": rx, "z": rz, "w": rw, "d": rd,
|
| 38 |
"y": cy, "h": floor_height,
|
| 39 |
+
"color": color, "name": room["name"], "exterior": exterior,
|
|
|
|
|
|
|
| 40 |
})
|
| 41 |
|
| 42 |
lx = rx + rw / 2
|
| 43 |
lz = rz + rd / 2
|
| 44 |
+
label_data.append({"x": lx, "z": lz, "y": cy + 0.3, "text": room["name"]})
|
|
|
|
|
|
|
|
|
|
| 45 |
|
| 46 |
+
measurements.append({
|
| 47 |
+
"x": rx + rw / 2, "z": rz - 1, "y": 0.1,
|
| 48 |
+
"text": f"{rw:.0f}'",
|
| 49 |
+
"dim": "width",
|
| 50 |
+
})
|
| 51 |
+
measurements.append({
|
| 52 |
+
"x": rx - 1, "z": rz + rd / 2, "y": 0.1,
|
| 53 |
+
"text": f"{rd:.0f}'",
|
| 54 |
+
"dim": "depth",
|
| 55 |
+
})
|
| 56 |
|
| 57 |
if not exterior:
|
| 58 |
for side in ["north", "south", "east", "west"]:
|
| 59 |
wall_meshes.append({
|
| 60 |
+
"side": side, "x": rx, "z": rz,
|
| 61 |
+
"w": rw, "d": rd, "h": wall_height,
|
|
|
|
| 62 |
"room_name": room["name"],
|
| 63 |
})
|
| 64 |
|
| 65 |
+
roof_h = 4 if roof_style == "flat" else 6
|
| 66 |
+
roof_elev = s * (wall_height + floor_height)
|
| 67 |
+
roof_info = {
|
| 68 |
+
"w": w * 1.15, "d": d * 1.15, "y": roof_elev,
|
| 69 |
+
"h": roof_h, "style": roof_style, "color": "#5a4a3a",
|
| 70 |
+
"pitch": 0.5 if roof_style == "steep-gable" else (0.3 if roof_style == "low-gable" else 0.4),
|
|
|
|
| 71 |
}
|
| 72 |
|
| 73 |
blueprint = {
|
| 74 |
"scene": {
|
| 75 |
+
"width": w, "depth": d, "stories": s,
|
| 76 |
+
"wall_height": wall_height, "floor_height": floor_height,
|
| 77 |
+
"roof_style": roof_style, "style": scene.get("style", "modern"),
|
|
|
|
|
|
|
| 78 |
},
|
| 79 |
"floors": room_meshes,
|
| 80 |
"walls": wall_meshes,
|
| 81 |
+
"roof": roof_info,
|
| 82 |
"labels": label_data,
|
| 83 |
+
"measurements": measurements,
|
| 84 |
"summary": data["summary"],
|
| 85 |
"features": data["features"],
|
| 86 |
}
|
| 87 |
|
| 88 |
+
return _render_html(blueprint)
|
|
|
|
| 89 |
|
| 90 |
|
| 91 |
def _render_html(blueprint):
|
| 92 |
bp_json = json.dumps(blueprint)
|
|
|
|
| 93 |
with open(TEMPLATE_PATH, "r") as f:
|
| 94 |
template = f.read()
|
| 95 |
+
return template.replace("/* BLUEPRINT_DATA */", f"const BLUEPRINT = {bp_json};")
|
|
|
|
|
|
|
| 96 |
|
| 97 |
|
| 98 |
+
def blueprint_to_json(text):
|
| 99 |
+
data = parse_description(text)
|
| 100 |
+
return json.dumps(data, indent=2)
|
|
|
|
|
|
|
|
|
devcore/parser.py
CHANGED
|
@@ -11,6 +11,17 @@ STYLES = {
|
|
| 11 |
"farmhouse": "farmhouse",
|
| 12 |
}
|
| 13 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 14 |
ROOM_COLORS = {
|
| 15 |
"living": "#e8dcc8", "family": "#e8dcc8",
|
| 16 |
"kitchen": "#d4e8d0", "dining": "#e0d0c0",
|
|
@@ -42,12 +53,14 @@ def parse_description(text):
|
|
| 42 |
|
| 43 |
rooms = _generate_rooms(width, depth, stories, bedrooms, bathrooms, garage, extra_rooms, style)
|
| 44 |
|
|
|
|
| 45 |
return {
|
| 46 |
"structure": {
|
| 47 |
"width": width,
|
| 48 |
"depth": depth,
|
| 49 |
"stories": stories,
|
| 50 |
"style": style,
|
|
|
|
| 51 |
"sqft": sqft or (width * depth * stories),
|
| 52 |
},
|
| 53 |
"rooms": rooms,
|
|
|
|
| 11 |
"farmhouse": "farmhouse",
|
| 12 |
}
|
| 13 |
|
| 14 |
+
ROOF_STYLES = {
|
| 15 |
+
"ranch": "hip", "rambler": "hip",
|
| 16 |
+
"colonial": "gable", "cape": "gable",
|
| 17 |
+
"modern": "flat", "contemporary": "flat",
|
| 18 |
+
"victorian": "steep-gable",
|
| 19 |
+
"craftsman": "low-gable",
|
| 20 |
+
"cabin": "gable", "cottage": "gable",
|
| 21 |
+
"tudor": "steep-gable",
|
| 22 |
+
"farmhouse": "gable",
|
| 23 |
+
}
|
| 24 |
+
|
| 25 |
ROOM_COLORS = {
|
| 26 |
"living": "#e8dcc8", "family": "#e8dcc8",
|
| 27 |
"kitchen": "#d4e8d0", "dining": "#e0d0c0",
|
|
|
|
| 53 |
|
| 54 |
rooms = _generate_rooms(width, depth, stories, bedrooms, bathrooms, garage, extra_rooms, style)
|
| 55 |
|
| 56 |
+
roof_style = ROOF_STYLES.get(style, "gable")
|
| 57 |
return {
|
| 58 |
"structure": {
|
| 59 |
"width": width,
|
| 60 |
"depth": depth,
|
| 61 |
"stories": stories,
|
| 62 |
"style": style,
|
| 63 |
+
"roof_style": roof_style,
|
| 64 |
"sqft": sqft or (width * depth * stories),
|
| 65 |
},
|
| 66 |
"rooms": rooms,
|
devcore/templates/viewer.html
CHANGED
|
@@ -5,24 +5,35 @@
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>Vitalis DevCore — 3D Blueprint</title>
|
| 7 |
<style>
|
| 8 |
-
*
|
| 9 |
-
body
|
| 10 |
-
#canvas-container
|
| 11 |
-
#info-overlay
|
| 12 |
-
#info-overlay h2
|
| 13 |
-
#info-overlay p
|
| 14 |
-
|
| 15 |
-
|
| 16 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 17 |
</style>
|
| 18 |
</head>
|
| 19 |
<body>
|
| 20 |
<div id="info-overlay">
|
| 21 |
-
<h2>🏗️ Blueprint</h2>
|
| 22 |
<p id="summary-text">Loading...</p>
|
| 23 |
<div id="features" style="margin-top:6px;"></div>
|
|
|
|
| 24 |
</div>
|
| 25 |
-
<div id="controls-hint">
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 26 |
<div id="canvas-container"></div>
|
| 27 |
|
| 28 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
@@ -31,17 +42,20 @@ body { font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, sans-
|
|
| 31 |
<script>
|
| 32 |
/* BLUEPRINT_DATA */
|
| 33 |
|
| 34 |
-
|
| 35 |
-
|
| 36 |
-
|
| 37 |
-
|
|
|
|
|
|
|
|
|
|
| 38 |
|
| 39 |
-
|
| 40 |
scene.background = new THREE.Color(0x0a0a1a);
|
| 41 |
-
scene.fog = new THREE.Fog(0x0a0a1a,
|
| 42 |
|
| 43 |
-
|
| 44 |
-
|
| 45 |
renderer.setSize(w, h);
|
| 46 |
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 47 |
renderer.shadowMap.enabled = true;
|
|
@@ -50,238 +64,220 @@ function initScene(data) {
|
|
| 50 |
renderer.toneMappingExposure = 1.2;
|
| 51 |
container.appendChild(renderer.domElement);
|
| 52 |
|
| 53 |
-
|
| 54 |
controls.enableDamping = true;
|
| 55 |
controls.dampingFactor = 0.08;
|
| 56 |
-
controls.maxPolarAngle = Math.PI / 2.
|
| 57 |
-
controls.minDistance =
|
| 58 |
-
controls.maxDistance =
|
| 59 |
|
| 60 |
// Lights
|
| 61 |
-
|
| 62 |
-
|
| 63 |
-
|
| 64 |
-
const sun = new THREE.DirectionalLight(0xffeedd, 1.2);
|
| 65 |
sun.position.set(40, 60, 30);
|
| 66 |
sun.castShadow = true;
|
| 67 |
sun.shadow.mapSize.width = 2048;
|
| 68 |
sun.shadow.mapSize.height = 2048;
|
| 69 |
-
sun.shadow.camera.near = 0.5;
|
| 70 |
-
sun.shadow.camera.far = 150;
|
| 71 |
scene.add(sun);
|
| 72 |
-
|
| 73 |
-
|
| 74 |
-
fill.position.set(-30, 20, -20);
|
| 75 |
-
scene.add(fill);
|
| 76 |
-
|
| 77 |
-
const hemi = new THREE.HemisphereLight(0x8888ff, 0x444422, 0.6);
|
| 78 |
-
scene.add(hemi);
|
| 79 |
|
| 80 |
// Ground
|
| 81 |
-
|
| 82 |
-
|
| 83 |
-
color:
|
| 84 |
-
|
| 85 |
-
|
| 86 |
-
|
| 87 |
-
|
| 88 |
-
|
| 89 |
-
|
| 90 |
-
ground.rotation.x = -Math.PI / 2;
|
| 91 |
-
ground.position.y = -0.25;
|
| 92 |
-
ground.receiveShadow = true;
|
| 93 |
-
scene.add(ground);
|
| 94 |
-
|
| 95 |
-
// Grid
|
| 96 |
-
const grid = new THREE.GridHelper(100, 40, 0x444488, 0x333366);
|
| 97 |
grid.position.y = 0.01;
|
| 98 |
scene.add(grid);
|
| 99 |
|
| 100 |
-
|
| 101 |
-
|
| 102 |
-
const cz = bp.depth / 2;
|
| 103 |
-
const sw = bp.width;
|
| 104 |
-
const sd = bp.depth;
|
| 105 |
|
| 106 |
// Foundation outline
|
| 107 |
-
|
| 108 |
-
|
| 109 |
-
|
| 110 |
-
|
| 111 |
-
|
| 112 |
-
|
| 113 |
-
const outline = new THREE.Line(outlineGeo, outlineMat);
|
| 114 |
-
outline.position.y = 0.02;
|
| 115 |
-
scene.add(outline);
|
| 116 |
|
| 117 |
// Floors
|
| 118 |
-
|
| 119 |
-
|
| 120 |
-
|
| 121 |
-
|
| 122 |
-
|
| 123 |
-
|
| 124 |
-
mat.transparent = true;
|
| 125 |
-
mat.opacity = f.exterior ? 0.5 : 0.85;
|
| 126 |
-
const mesh = new THREE.Mesh(geo, mat);
|
| 127 |
-
mesh.position.set(f.x + f.w/2, f.y, f.z + f.d/2);
|
| 128 |
-
mesh.castShadow = true;
|
| 129 |
-
mesh.receiveShadow = true;
|
| 130 |
scene.add(mesh);
|
| 131 |
-
|
| 132 |
if (!f.exterior) {
|
| 133 |
-
|
| 134 |
-
|
| 135 |
-
|
| 136 |
-
}));
|
| 137 |
-
line.position.copy(mesh.position);
|
| 138 |
-
scene.add(line);
|
| 139 |
}
|
| 140 |
});
|
| 141 |
|
| 142 |
// Walls
|
| 143 |
-
|
| 144 |
-
|
| 145 |
-
|
| 146 |
-
|
| 147 |
-
|
| 148 |
-
|
| 149 |
-
var
|
| 150 |
-
var
|
| 151 |
-
|
| 152 |
-
|
| 153 |
-
wx = w.x; wz = w.z; ww = w.w; wd = 0.15;
|
| 154 |
-
} else if (w.side === 'south') {
|
| 155 |
-
wx = w.x; wz = w.z + w.d; ww = w.w; wd = 0.15;
|
| 156 |
-
} else if (w.side === 'east') {
|
| 157 |
-
wx = w.x + w.w; wz = w.z; ww = 0.15; wd = w.d;
|
| 158 |
-
} else {
|
| 159 |
-
wx = w.x; wz = w.z; ww = 0.15; wd = w.d;
|
| 160 |
-
}
|
| 161 |
-
|
| 162 |
-
const geo = new THREE.BoxGeometry(ww, wh, wd);
|
| 163 |
-
const mat = wallMat.clone();
|
| 164 |
-
mat.color = new THREE.Color(0x6666aa);
|
| 165 |
-
const mesh = new THREE.Mesh(geo, mat);
|
| 166 |
-
mesh.position.set(wx + ww/2, wh/2, wz + wd/2);
|
| 167 |
scene.add(mesh);
|
| 168 |
});
|
| 169 |
|
| 170 |
// Roof
|
| 171 |
-
|
| 172 |
-
|
| 173 |
-
|
| 174 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 175 |
});
|
| 176 |
-
const roofGeo = new THREE.ConeGeometry(Math.max(roof.w, roof.d) * 0.7, roof.h, 4);
|
| 177 |
-
const roofMesh = new THREE.Mesh(roofGeo, roofMat);
|
| 178 |
-
roofMesh.position.set(sw/2, roof.y, sd/2);
|
| 179 |
-
roofMesh.rotation.y = Math.PI / 4;
|
| 180 |
-
roofMesh.castShadow = true;
|
| 181 |
-
scene.add(roofMesh);
|
| 182 |
-
|
| 183 |
-
// Labels using sprites
|
| 184 |
-
data.labels.forEach(function(l) {
|
| 185 |
-
const canvas = document.createElement('canvas');
|
| 186 |
-
const ctx = canvas.getContext('2d');
|
| 187 |
-
canvas.width = 512;
|
| 188 |
-
canvas.height = 128;
|
| 189 |
|
| 190 |
-
|
| 191 |
-
|
| 192 |
-
|
| 193 |
-
ctx
|
| 194 |
-
|
| 195 |
-
|
| 196 |
-
const rh = 60;
|
| 197 |
-
const rx = (canvas.width - rw) / 2;
|
| 198 |
-
const ry = (canvas.height - rh) / 2;
|
| 199 |
-
ctx.shadowBlur = 0;
|
| 200 |
ctx.fillStyle = 'rgba(20,20,50,0.5)';
|
| 201 |
-
ctx.beginPath();
|
| 202 |
-
ctx.
|
| 203 |
-
ctx.
|
| 204 |
-
ctx.strokeStyle = 'rgba(100,60,255,0.3)';
|
| 205 |
-
ctx.lineWidth = 1;
|
| 206 |
-
ctx.stroke();
|
| 207 |
-
|
| 208 |
-
ctx.shadowColor = 'rgba(0,0,0,0.6)';
|
| 209 |
-
ctx.shadowBlur = 6;
|
| 210 |
ctx.fillStyle = l.text.includes('Upper') ? 'rgba(180,180,220,0.6)' : '#ffffff';
|
| 211 |
-
ctx.font = 'bold
|
| 212 |
-
ctx.textAlign = 'center';
|
| 213 |
-
ctx.
|
| 214 |
-
|
| 215 |
-
|
| 216 |
-
|
| 217 |
-
|
| 218 |
-
|
| 219 |
-
|
| 220 |
-
map: texture,
|
| 221 |
-
transparent: true,
|
| 222 |
-
depthTest: false,
|
| 223 |
-
sizeAttenuation: true,
|
| 224 |
-
});
|
| 225 |
-
const sprite = new THREE.Sprite(spriteMat);
|
| 226 |
-
sprite.position.set(l.x, l.y + 2, l.z);
|
| 227 |
-
sprite.scale.set(8, 2, 1);
|
| 228 |
-
scene.add(sprite);
|
| 229 |
});
|
| 230 |
|
| 231 |
-
//
|
| 232 |
-
|
| 233 |
-
camera.position.set(dist
|
| 234 |
-
controls.target.set(sw
|
| 235 |
controls.update();
|
| 236 |
|
|
|
|
|
|
|
|
|
|
| 237 |
// Resize
|
| 238 |
window.addEventListener('resize', function() {
|
| 239 |
-
|
| 240 |
-
|
| 241 |
-
camera.aspect = w2 / h2;
|
| 242 |
-
camera.updateProjectionMatrix();
|
| 243 |
renderer.setSize(w2, h2);
|
| 244 |
});
|
| 245 |
|
| 246 |
-
function animate() {
|
| 247 |
-
requestAnimationFrame(animate);
|
| 248 |
-
controls.update();
|
| 249 |
-
renderer.render(scene, camera);
|
| 250 |
-
}
|
| 251 |
animate();
|
| 252 |
}
|
| 253 |
|
| 254 |
-
|
| 255 |
-
|
| 256 |
-
|
| 257 |
-
|
| 258 |
-
|
| 259 |
-
|
| 260 |
-
|
| 261 |
-
|
| 262 |
-
|
| 263 |
-
|
| 264 |
-
return this;
|
| 265 |
-
};
|
| 266 |
}
|
| 267 |
|
| 268 |
-
//
|
| 269 |
-
document.getElementById('summary-text').textContent =
|
| 270 |
-
|
|
|
|
|
|
|
| 271 |
var featHtml = '';
|
| 272 |
-
var
|
| 273 |
-
|
| 274 |
-
|
| 275 |
-
|
| 276 |
-
|
| 277 |
-
|
| 278 |
-
if (k === 'garage') val = val ? 'Yes' : 'No';
|
| 279 |
-
featHtml += '<span class="badge">' + featMap[k] + ' ' + k.charAt(0).toUpperCase() + k.slice(1) + ': ' + val + '</span>';
|
| 280 |
-
}
|
| 281 |
-
}
|
| 282 |
document.getElementById('features').innerHTML = featHtml;
|
| 283 |
|
| 284 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 285 |
</script>
|
| 286 |
</body>
|
| 287 |
</html>
|
|
|
|
| 5 |
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
| 6 |
<title>Vitalis DevCore — 3D Blueprint</title>
|
| 7 |
<style>
|
| 8 |
+
*{margin:0;padding:0;box-sizing:border-box;}
|
| 9 |
+
body{font-family:-apple-system,BlinkMacSystemFont,'Segoe UI',Roboto,sans-serif;background:#0a0a1a;overflow:hidden;color:#fff;}
|
| 10 |
+
#canvas-container{width:100vw;height:100vh;display:block;}
|
| 11 |
+
#info-overlay{position:fixed;top:16px;left:16px;background:rgba(10,10,30,0.85);border:1px solid rgba(100,60,255,0.3);border-radius:12px;padding:16px 20px;max-width:340px;font-size:13px;line-height:1.5;backdrop-filter:blur(8px);z-index:10;}
|
| 12 |
+
#info-overlay h2{font-size:16px;margin-bottom:6px;color:#a78bfa;}
|
| 13 |
+
#info-overlay p{color:#c4b5fd;margin:2px 0;font-size:12px;}
|
| 14 |
+
.badge{display:inline-block;padding:2px 8px;border-radius:6px;font-size:11px;margin:2px;background:rgba(100,60,255,0.15);color:#a78bfa;border:1px solid rgba(100,60,255,0.2);}
|
| 15 |
+
.badge-dim{background:rgba(50,180,100,0.15);color:#6ee7a7;border-color:rgba(50,180,100,0.2);}
|
| 16 |
+
#controls-hint{position:fixed;bottom:20px;left:50%;transform:translateX(-50%);background:rgba(10,10,30,0.7);border:1px solid rgba(255,255,255,0.1);border-radius:8px;padding:8px 16px;font-size:12px;color:#888;backdrop-filter:blur(4px);z-index:10;display:flex;gap:16px;}
|
| 17 |
+
#controls-hint span{cursor:pointer;padding:2px 8px;border-radius:4px;transition:background 0.2s;}
|
| 18 |
+
#controls-hint span:hover{background:rgba(100,60,255,0.2);color:#a78bfa;}
|
| 19 |
+
#controls-hint .active{background:rgba(100,60,255,0.3);color:#a78bfa;}
|
| 20 |
+
.tool-btn{position:fixed;bottom:20px;right:20px;background:rgba(100,60,255,0.2);border:1px solid rgba(100,60,255,0.3);border-radius:8px;padding:8px 14px;font-size:12px;color:#a78bfa;cursor:pointer;backdrop-filter:blur(4px);z-index:10;transition:all 0.2s;}
|
| 21 |
+
.tool-btn:hover{background:rgba(100,60,255,0.4);}
|
| 22 |
</style>
|
| 23 |
</head>
|
| 24 |
<body>
|
| 25 |
<div id="info-overlay">
|
| 26 |
+
<h2>🏗️ <span id="style-name">Blueprint</span></h2>
|
| 27 |
<p id="summary-text">Loading...</p>
|
| 28 |
<div id="features" style="margin-top:6px;"></div>
|
| 29 |
+
<div id="dimensions" style="margin-top:4px;font-size:11px;color:#6ee7a7;"></div>
|
| 30 |
</div>
|
| 31 |
+
<div id="controls-hint">
|
| 32 |
+
<span data-mode="orbit" class="active">🔄 Orbit</span>
|
| 33 |
+
<span data-mode="pan">✋ Pan</span>
|
| 34 |
+
<span data-mode="tour">🎬 Tour</span>
|
| 35 |
+
</div>
|
| 36 |
+
<button class="tool-btn" id="export-btn">📥 Export JSON</button>
|
| 37 |
<div id="canvas-container"></div>
|
| 38 |
|
| 39 |
<script src="https://cdnjs.cloudflare.com/ajax/libs/three.js/r128/three.min.js"></script>
|
|
|
|
| 42 |
<script>
|
| 43 |
/* BLUEPRINT_DATA */
|
| 44 |
|
| 45 |
+
var BP = BLUEPRINT;
|
| 46 |
+
var scene, camera, renderer, controls;
|
| 47 |
+
var tourActive = false, tourPoints = [], tourIdx = 0;
|
| 48 |
+
|
| 49 |
+
function initScene() {
|
| 50 |
+
var container = document.getElementById('canvas-container');
|
| 51 |
+
var w = container.clientWidth, h = container.clientHeight;
|
| 52 |
|
| 53 |
+
scene = new THREE.Scene();
|
| 54 |
scene.background = new THREE.Color(0x0a0a1a);
|
| 55 |
+
scene.fog = new THREE.Fog(0x0a0a1a, 120, 200);
|
| 56 |
|
| 57 |
+
camera = new THREE.PerspectiveCamera(40, w/h, 0.1, 500);
|
| 58 |
+
renderer = new THREE.WebGLRenderer({antialias:true});
|
| 59 |
renderer.setSize(w, h);
|
| 60 |
renderer.setPixelRatio(Math.min(window.devicePixelRatio, 2));
|
| 61 |
renderer.shadowMap.enabled = true;
|
|
|
|
| 64 |
renderer.toneMappingExposure = 1.2;
|
| 65 |
container.appendChild(renderer.domElement);
|
| 66 |
|
| 67 |
+
controls = new THREE.OrbitControls(camera, renderer.domElement);
|
| 68 |
controls.enableDamping = true;
|
| 69 |
controls.dampingFactor = 0.08;
|
| 70 |
+
controls.maxPolarAngle = Math.PI / 2.05;
|
| 71 |
+
controls.minDistance = 3;
|
| 72 |
+
controls.maxDistance = 120;
|
| 73 |
|
| 74 |
// Lights
|
| 75 |
+
scene.add(new THREE.AmbientLight(0x404060, 0.5));
|
| 76 |
+
var sun = new THREE.DirectionalLight(0xffeedd, 1.4);
|
|
|
|
|
|
|
| 77 |
sun.position.set(40, 60, 30);
|
| 78 |
sun.castShadow = true;
|
| 79 |
sun.shadow.mapSize.width = 2048;
|
| 80 |
sun.shadow.mapSize.height = 2048;
|
|
|
|
|
|
|
| 81 |
scene.add(sun);
|
| 82 |
+
scene.add(new THREE.DirectionalLight(0x8888ff, 0.3).position.set(-30, 20, -20));
|
| 83 |
+
scene.add(new THREE.HemisphereLight(0x8888ff, 0x444422, 0.5));
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 84 |
|
| 85 |
// Ground
|
| 86 |
+
var gnd = new THREE.Mesh(
|
| 87 |
+
new THREE.PlaneGeometry(200, 200),
|
| 88 |
+
new THREE.MeshStandardMaterial({color:0x181830,roughness:0.9,metalness:0,transparent:true,opacity:0.8})
|
| 89 |
+
);
|
| 90 |
+
gnd.rotation.x = -Math.PI/2;
|
| 91 |
+
gnd.position.y = -0.25;
|
| 92 |
+
gnd.receiveShadow = true;
|
| 93 |
+
scene.add(gnd);
|
| 94 |
+
var grid = new THREE.GridHelper(100, 40, 0x444488, 0x333366);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 95 |
grid.position.y = 0.01;
|
| 96 |
scene.add(grid);
|
| 97 |
|
| 98 |
+
var sp = BP.scene;
|
| 99 |
+
var sw = sp.width, sd = sp.depth;
|
|
|
|
|
|
|
|
|
|
| 100 |
|
| 101 |
// Foundation outline
|
| 102 |
+
var ol = new THREE.Line(
|
| 103 |
+
new THREE.BufferGeometry().setAttribute('position', new THREE.BufferAttribute(new Float32Array([0,0,0,sw,0,0,sw,0,sd,0,0,sd,0,0,0]),3)),
|
| 104 |
+
new THREE.LineBasicMaterial({color:0x6666cc,transparent:true,opacity:0.4})
|
| 105 |
+
);
|
| 106 |
+
ol.position.y = 0.02;
|
| 107 |
+
scene.add(ol);
|
|
|
|
|
|
|
|
|
|
| 108 |
|
| 109 |
// Floors
|
| 110 |
+
BP.floors.forEach(function(f) {
|
| 111 |
+
var g = new THREE.BoxGeometry(f.w, f.h, f.d);
|
| 112 |
+
var m = new THREE.MeshStandardMaterial({roughness:0.7,metalness:0.05,color:f.color||0x444466,transparent:true,opacity:f.exterior?0.4:0.8});
|
| 113 |
+
var mesh = new THREE.Mesh(g, m);
|
| 114 |
+
mesh.position.set(f.x+f.w/2, f.y, f.z+f.d/2);
|
| 115 |
+
mesh.castShadow = true; mesh.receiveShadow = true;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 116 |
scene.add(mesh);
|
|
|
|
| 117 |
if (!f.exterior) {
|
| 118 |
+
var e = new THREE.LineSegments(new THREE.EdgesGeometry(g), new THREE.LineBasicMaterial({color:0x8888cc,transparent:true,opacity:0.15}));
|
| 119 |
+
e.position.copy(mesh.position);
|
| 120 |
+
scene.add(e);
|
|
|
|
|
|
|
|
|
|
| 121 |
}
|
| 122 |
});
|
| 123 |
|
| 124 |
// Walls
|
| 125 |
+
BP.walls.forEach(function(w) {
|
| 126 |
+
var wx, wz, ww, wd;
|
| 127 |
+
if (w.side === 'north') { wx = w.x; wz = w.z; ww = w.w; wd = 0.15; }
|
| 128 |
+
else if (w.side === 'south') { wx = w.x; wz = w.z+w.d; ww = w.w; wd = 0.15; }
|
| 129 |
+
else if (w.side === 'east') { wx = w.x+w.w; wz = w.z; ww = 0.15; wd = w.d; }
|
| 130 |
+
else { wx = w.x; wz = w.z; ww = 0.15; wd = w.d; }
|
| 131 |
+
var g = new THREE.BoxGeometry(ww, w.h, wd);
|
| 132 |
+
var m = new THREE.MeshStandardMaterial({color:0x555599,roughness:0.6,metalness:0.1,transparent:true,opacity:0.1});
|
| 133 |
+
var mesh = new THREE.Mesh(g, m);
|
| 134 |
+
mesh.position.set(wx+ww/2, w.h/2, wz+wd/2);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 135 |
scene.add(mesh);
|
| 136 |
});
|
| 137 |
|
| 138 |
// Roof
|
| 139 |
+
var rf = BP.roof;
|
| 140 |
+
var roofMesh;
|
| 141 |
+
if (rf.style === 'flat') {
|
| 142 |
+
var rg = new THREE.BoxGeometry(rf.w, rf.h*0.3, rf.d);
|
| 143 |
+
var rm = new THREE.MeshStandardMaterial({color:rf.color||0x5a4a3a,roughness:0.8,metalness:0});
|
| 144 |
+
roofMesh = new THREE.Mesh(rg, rm);
|
| 145 |
+
roofMesh.position.set(sw/2, rf.y, sd/2);
|
| 146 |
+
} else if (rf.style === 'gable' || rf.style === 'steep-gable' || rf.style === 'low-gable') {
|
| 147 |
+
var shape = new THREE.Shape();
|
| 148 |
+
var pitch = rf.pitch || 0.4;
|
| 149 |
+
var ridgeH = rf.w * pitch * 0.5;
|
| 150 |
+
shape.moveTo(-rf.w/2, 0);
|
| 151 |
+
shape.lineTo(0, ridgeH);
|
| 152 |
+
shape.lineTo(rf.w/2, 0);
|
| 153 |
+
shape.lineTo(-rf.w/2, 0);
|
| 154 |
+
var extrudeSettings = {steps:1,depth:rf.d,bevelEnabled:true,bevelThickness:0.5,bevelSize:0.3,bevelSegments:2};
|
| 155 |
+
var rg = new THREE.ExtrudeGeometry(shape, extrudeSettings);
|
| 156 |
+
var rm = new THREE.MeshStandardMaterial({color:rf.color||0x5a4a3a,roughness:0.8,metalness:0,side:THREE.DoubleSide});
|
| 157 |
+
roofMesh = new THREE.Mesh(rg, rm);
|
| 158 |
+
roofMesh.position.set(sw/2, rf.y, sd/2 - rf.d/2);
|
| 159 |
+
roofMesh.rotation.x = 0;
|
| 160 |
+
} else {
|
| 161 |
+
var rg = new THREE.ConeGeometry(Math.max(rf.w,rf.d)*0.6, rf.h, 4);
|
| 162 |
+
var rm = new THREE.MeshStandardMaterial({color:rf.color||0x5a4a3a,roughness:0.8,metalness:0});
|
| 163 |
+
roofMesh = new THREE.Mesh(rg, rm);
|
| 164 |
+
roofMesh.position.set(sw/2, rf.y + rf.h/2, sd/2);
|
| 165 |
+
roofMesh.rotation.y = Math.PI/4;
|
| 166 |
+
}
|
| 167 |
+
if (roofMesh) { roofMesh.castShadow = true; scene.add(roofMesh); }
|
| 168 |
+
|
| 169 |
+
// Measurements
|
| 170 |
+
BP.measurements.forEach(function(m) {
|
| 171 |
+
var canvas = document.createElement('canvas');
|
| 172 |
+
var ctx = canvas.getContext('2d');
|
| 173 |
+
canvas.width = 256; canvas.height = 64;
|
| 174 |
+
ctx.fillStyle = 'rgba(50,180,100,0.15)';
|
| 175 |
+
ctx.beginPath(); ctx.roundRect(20,8,216,48,6); ctx.fill();
|
| 176 |
+
ctx.strokeStyle = 'rgba(50,180,100,0.4)'; ctx.lineWidth=1; ctx.stroke();
|
| 177 |
+
ctx.fillStyle = '#6ee7a7'; ctx.font = 'bold 24px monospace'; ctx.textAlign='center'; ctx.textBaseline='middle';
|
| 178 |
+
ctx.fillText(m.text, 128, 32);
|
| 179 |
+
|
| 180 |
+
var tex = new THREE.CanvasTexture(canvas);
|
| 181 |
+
tex.needsUpdate = true;
|
| 182 |
+
var sp = new THREE.Sprite(new THREE.SpriteMaterial({map:tex,transparent:true,depthTest:false,sizeAttenuation:true}));
|
| 183 |
+
sp.position.set(m.x, m.y + 0.5, m.z);
|
| 184 |
+
sp.scale.set(m.dim === 'width' ? 4 : 2.5, 1, 1);
|
| 185 |
+
scene.add(sp);
|
| 186 |
});
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 187 |
|
| 188 |
+
// Labels
|
| 189 |
+
BP.labels.forEach(function(l) {
|
| 190 |
+
var canvas = document.createElement('canvas');
|
| 191 |
+
var ctx = canvas.getContext('2d');
|
| 192 |
+
canvas.width = 512; canvas.height = 100;
|
| 193 |
+
ctx.shadowColor = 'rgba(0,0,0,0.8)'; ctx.shadowBlur = 10;
|
|
|
|
|
|
|
|
|
|
|
|
|
| 194 |
ctx.fillStyle = 'rgba(20,20,50,0.5)';
|
| 195 |
+
ctx.beginPath(); ctx.roundRect(50,12,412,76,10); ctx.fill();
|
| 196 |
+
ctx.strokeStyle = 'rgba(100,60,255,0.2)'; ctx.lineWidth=1; ctx.stroke();
|
| 197 |
+
ctx.shadowColor = 'rgba(0,0,0,0.5)'; ctx.shadowBlur = 4;
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 198 |
ctx.fillStyle = l.text.includes('Upper') ? 'rgba(180,180,220,0.6)' : '#ffffff';
|
| 199 |
+
ctx.font = 'bold 28px -apple-system, BlinkMacSystemFont, sans-serif';
|
| 200 |
+
ctx.textAlign = 'center'; ctx.textBaseline = 'middle';
|
| 201 |
+
ctx.fillText(l.text, 256, 50);
|
| 202 |
+
var tex = new THREE.CanvasTexture(canvas);
|
| 203 |
+
tex.needsUpdate = true;
|
| 204 |
+
var sp = new THREE.Sprite(new THREE.SpriteMaterial({map:tex,transparent:true,depthTest:false,sizeAttenuation:true}));
|
| 205 |
+
sp.position.set(l.x, l.y + 2.5, l.z);
|
| 206 |
+
sp.scale.set(6, 1.2, 1);
|
| 207 |
+
scene.add(sp);
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 208 |
});
|
| 209 |
|
| 210 |
+
// Position camera
|
| 211 |
+
var dist = Math.max(sw, sd) * 1.2 + 8;
|
| 212 |
+
camera.position.set(dist*0.6, dist*0.6, dist*0.7);
|
| 213 |
+
controls.target.set(sw/2, 1, sd/2);
|
| 214 |
controls.update();
|
| 215 |
|
| 216 |
+
// Tour points
|
| 217 |
+
tourPoints = BP.labels.map(function(l) { return {x:l.x, y:3, z:l.z}; });
|
| 218 |
+
|
| 219 |
// Resize
|
| 220 |
window.addEventListener('resize', function() {
|
| 221 |
+
var w2 = container.clientWidth, h2 = container.clientHeight;
|
| 222 |
+
camera.aspect = w2/h2; camera.updateProjectionMatrix();
|
|
|
|
|
|
|
| 223 |
renderer.setSize(w2, h2);
|
| 224 |
});
|
| 225 |
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 226 |
animate();
|
| 227 |
}
|
| 228 |
|
| 229 |
+
function animate() {
|
| 230 |
+
requestAnimationFrame(animate);
|
| 231 |
+
if (tourActive && tourPoints.length > 0) {
|
| 232 |
+
var p = tourPoints[tourIdx];
|
| 233 |
+
controls.target.lerp(new THREE.Vector3(p.x, p.y, p.z), 0.03);
|
| 234 |
+
var dist = controls.target.distanceTo(camera.position);
|
| 235 |
+
if (dist < 2) { tourIdx = (tourIdx + 1) % tourPoints.length; }
|
| 236 |
+
}
|
| 237 |
+
controls.update();
|
| 238 |
+
renderer.render(scene, camera);
|
|
|
|
|
|
|
| 239 |
}
|
| 240 |
|
| 241 |
+
// UI
|
| 242 |
+
document.getElementById('summary-text').textContent = BP.summary || 'No description.';
|
| 243 |
+
document.getElementById('style-name').textContent = (BP.scene.style || 'Modern').charAt(0).toUpperCase() + (BP.scene.style||'Modern').slice(1);
|
| 244 |
+
|
| 245 |
+
var feat = BP.features || {};
|
| 246 |
var featHtml = '';
|
| 247 |
+
var dimHtml = '';
|
| 248 |
+
if (feat.bedrooms !== undefined) featHtml += '<span class="badge">🛏️ ' + feat.bedrooms + ' bed</span>';
|
| 249 |
+
if (feat.bathrooms !== undefined) featHtml += '<span class="badge">🚿 ' + feat.bathrooms + ' bath</span>';
|
| 250 |
+
if (feat.stories !== undefined) featHtml += '<span class="badge">🏠 ' + feat.stories + ' story</span>';
|
| 251 |
+
if (feat.garage !== undefined) featHtml += '<span class="badge">🚗 ' + (feat.garage ? 'Garage' : 'No garage') + '</span>';
|
| 252 |
+
featHtml += '<span class="badge badge-dim">📐 ' + BP.scene.width + 'ft x ' + BP.scene.depth + 'ft</span>';
|
|
|
|
|
|
|
|
|
|
|
|
|
| 253 |
document.getElementById('features').innerHTML = featHtml;
|
| 254 |
|
| 255 |
+
var sqft = BP.scene.width * BP.scene.depth * (BP.scene.stories || 1);
|
| 256 |
+
document.getElementById('dimensions').textContent = '≈ ' + sqft + ' sq ft total';
|
| 257 |
+
|
| 258 |
+
// Control mode switching
|
| 259 |
+
document.querySelectorAll('#controls-hint span').forEach(function(el) {
|
| 260 |
+
el.addEventListener('click', function() {
|
| 261 |
+
document.querySelectorAll('#controls-hint span').forEach(function(e) { e.classList.remove('active'); });
|
| 262 |
+
el.classList.add('active');
|
| 263 |
+
var mode = el.dataset.mode;
|
| 264 |
+
if (mode === 'orbit') { tourActive = false; controls.enableRotate = true; controls.enablePan = true; }
|
| 265 |
+
else if (mode === 'pan') { tourActive = false; controls.enableRotate = false; controls.enablePan = true; }
|
| 266 |
+
else if (mode === 'tour') { tourActive = true; tourIdx = 0; controls.enableRotate = true; controls.enablePan = true; }
|
| 267 |
+
});
|
| 268 |
+
});
|
| 269 |
+
|
| 270 |
+
// Export
|
| 271 |
+
document.getElementById('export-btn').addEventListener('click', function() {
|
| 272 |
+
var json = JSON.stringify(BP, null, 2);
|
| 273 |
+
var blob = new Blob([json], {type:'application/json'});
|
| 274 |
+
var a = document.createElement('a');
|
| 275 |
+
a.href = URL.createObjectURL(blob);
|
| 276 |
+
a.download = 'blueprint.json';
|
| 277 |
+
a.click();
|
| 278 |
+
});
|
| 279 |
+
|
| 280 |
+
initScene();
|
| 281 |
</script>
|
| 282 |
</body>
|
| 283 |
</html>
|