FerrellSyntheticIntelligence commited on
Commit
d26d55f
·
1 Parent(s): dde6b93

Add GitHub Sponsors + Buy Me A Coffee

Browse files
.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
- # Vitalis DevCore — 3D Blueprint Builder
 
 
13
 
14
- Interactive 3D architectural visualizer. Describe a building in natural language and see it rendered as an explorable 3D model in your browser.
15
 
16
  ## Features
17
 
18
- - **Natural language input** — "2000 sq ft ranch house with 3 bedrooms and 2 baths"
19
- - **Real-time 3D rendering** — Three.js in-browser, fully interactive (orbit, zoom, pan)
20
- - **Structured parsing** Extracts dimensions, style, stories, rooms, features
21
- - **Live labels** Room names rendered as floating sprites
22
- - **No GPU required** All rendering client-side
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
23
 
24
- ## Example Prompts
 
 
 
 
 
 
 
25
 
26
- - `2000 sq ft ranch house with 3 bedrooms and 2 baths`
27
- - `modern 2-story house 30x40 with 4 bedrooms 3 bathrooms and a garage`
28
- - `small cabin 800 sq ft with 1 bedroom and a loft`
29
- - `victorian 3500 sq ft 5 bedroom 3 bath with office and library`
30
- - `my house is 50 feet wide and 60 feet long with 3 bedrooms`
 
 
31
 
32
  ## Architecture
33
 
34
  ```
35
- User Input → NL Parser → Blueprint JSON → Three.js HTML → Browser Render
 
 
 
 
 
 
 
 
36
  ```
37
 
38
- Built for the Vitalis ecosystem by FerrellSyntheticIntelligence.
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- return html, summary, details
 
 
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.Markdown("""
109
- ### How it works
110
- 1. **Parse** — Natural language description → structured blueprint data
111
- 2. **Generate** — Blueprint Three.js 3D scene with walls, rooms, roof, labels
112
- 3. **Render** — Interactive model with orbit controls (drag to rotate, scroll to zoom)
113
-
114
- *Built with Three.js · All rendering happens in your browser no server-side graphics needed*
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
- hw = rw / 2
52
- hd = rd / 2
 
 
 
 
 
 
 
 
53
 
54
  if not exterior:
55
  for side in ["north", "south", "east", "west"]:
56
  wall_meshes.append({
57
- "side": side,
58
- "x": rx, "z": rz, "w": rw, "d": rd,
59
- "h": wall_height,
60
  "room_name": room["name"],
61
  })
62
 
63
- roof_height = wall_height + 4
64
- roof_mesh = {
65
- "w": w * 1.15,
66
- "d": d * 1.15,
67
- "y": s * (wall_height + floor_height),
68
- "h": roof_height,
69
- "color": "#5a4a3a",
70
  }
71
 
72
  blueprint = {
73
  "scene": {
74
- "width": w,
75
- "depth": d,
76
- "stories": s,
77
- "wall_height": wall_height,
78
- "floor_height": floor_height,
79
  },
80
  "floors": room_meshes,
81
  "walls": wall_meshes,
82
- "roof": roof_mesh,
83
  "labels": label_data,
 
84
  "summary": data["summary"],
85
  "features": data["features"],
86
  }
87
 
88
- html = _render_html(blueprint)
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
- if __name__ == "__main__":
103
- html = description_to_blueprint("2000 sq ft ranch house with 3 bedrooms and 2 baths")
104
- print(f"Generated {len(html)} bytes of HTML")
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
- * { 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: 320px; 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; }
14
- #info-overlay .badge { display: inline-block; padding: 2px 8px; border-radius: 6px; font-size: 11px; margin: 2px; background: rgba(100,60,255,0.2); color: #a78bfa; border: 1px solid rgba(100,60,255,0.2); }
15
- #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; }
16
- .label-2d { position: absolute; color: rgba(255,255,255,0.6); font-size: 11px; font-weight: 500; text-shadow: 0 0 8px rgba(0,0,0,0.8); pointer-events: none; transform: translate(-50%, -50%); white-space: nowrap; }
 
 
 
 
 
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">🖱️ Drag to orbit · Scroll to zoom · Right-drag to pan</div>
 
 
 
 
 
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
- function initScene(data) {
35
- const container = document.getElementById('canvas-container');
36
- const w = container.clientWidth;
37
- const h = container.clientHeight;
 
 
 
38
 
39
- const scene = new THREE.Scene();
40
  scene.background = new THREE.Color(0x0a0a1a);
41
- scene.fog = new THREE.Fog(0x0a0a1a, 100, 200);
42
 
43
- const camera = new THREE.PerspectiveCamera(40, w / h, 0.1, 500);
44
- const renderer = new THREE.WebGLRenderer({ antialias: true });
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
- const controls = new THREE.OrbitControls(camera, renderer.domElement);
54
  controls.enableDamping = true;
55
  controls.dampingFactor = 0.08;
56
- controls.maxPolarAngle = Math.PI / 2.1;
57
- controls.minDistance = 5;
58
- controls.maxDistance = 150;
59
 
60
  // Lights
61
- const ambient = new THREE.AmbientLight(0x404060, 0.6);
62
- scene.add(ambient);
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
- const fill = new THREE.DirectionalLight(0x8888ff, 0.4);
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
- const groundGeo = new THREE.PlaneGeometry(200, 200);
82
- const groundMat = new THREE.MeshStandardMaterial({
83
- color: 0x181830,
84
- roughness: 0.9,
85
- metalness: 0.0,
86
- transparent: true,
87
- opacity: 0.8,
88
- });
89
- const ground = new THREE.Mesh(groundGeo, groundMat);
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
- const bp = data.scene;
101
- const cx = bp.width / 2;
102
- const cz = bp.depth / 2;
103
- const sw = bp.width;
104
- const sd = bp.depth;
105
 
106
  // Foundation outline
107
- const outlineMat = new THREE.LineBasicMaterial({ color: 0x6666cc, transparent: true, opacity: 0.4 });
108
- const outlineGeo = new THREE.BufferGeometry();
109
- const outlineVerts = new Float32Array([
110
- 0, 0, 0, sw, 0, 0, sw, 0, sd, 0, 0, sd, 0, 0, 0
111
- ]);
112
- outlineGeo.setAttribute('position', new THREE.BufferAttribute(outlineVerts, 3));
113
- const outline = new THREE.Line(outlineGeo, outlineMat);
114
- outline.position.y = 0.02;
115
- scene.add(outline);
116
 
117
  // Floors
118
- const floorMat = new THREE.MeshStandardMaterial({ roughness: 0.7, metalness: 0.05 });
119
-
120
- data.floors.forEach(function(f) {
121
- const geo = new THREE.BoxGeometry(f.w, f.h, f.d);
122
- const mat = floorMat.clone();
123
- mat.color = new THREE.Color(f.color || 0x444466);
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
- const edge = new THREE.EdgesGeometry(geo);
134
- const line = new THREE.LineSegments(edge, new THREE.LineBasicMaterial({
135
- color: 0x8888cc, transparent: true, opacity: 0.3
136
- }));
137
- line.position.copy(mesh.position);
138
- scene.add(line);
139
  }
140
  });
141
 
142
  // Walls
143
- const wallMat = new THREE.MeshStandardMaterial({
144
- color: 0x444488, roughness: 0.6, metalness: 0.1,
145
- transparent: true, opacity: 0.15, wireframe: false,
146
- });
147
-
148
- data.walls.forEach(function(w) {
149
- var wx, wz, ww, wd, wh = w.h;
150
- var margin = 0.05;
151
-
152
- if (w.side === 'north') {
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
- const roof = data.roof;
172
- const roofMat = new THREE.MeshStandardMaterial({
173
- color: new THREE.Color(roof.color || 0x5a4a3a),
174
- roughness: 0.8, metalness: 0.0,
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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
- ctx.shadowColor = 'rgba(0,0,0,0.8)';
191
- ctx.shadowBlur = 12;
192
-
193
- ctx.fillStyle = 'rgba(10,10,30,0.6)';
194
- const tw = ctx.measureText(l.text).width || 200;
195
- const rw = Math.min(tw + 40, 500);
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.roundRect(rx, ry, rw, rh, 8);
203
- ctx.fill();
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 32px -apple-system, BlinkMacSystemFont, sans-serif';
212
- ctx.textAlign = 'center';
213
- ctx.textBaseline = 'middle';
214
- ctx.fillText(l.text, canvas.width / 2, canvas.height / 2);
215
-
216
- const texture = new THREE.CanvasTexture(canvas);
217
- texture.needsUpdate = true;
218
-
219
- const spriteMat = new THREE.SpriteMaterial({
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
- // Camera position
232
- const dist = Math.max(sw, sd) * 1.3 + 10;
233
- camera.position.set(dist * 0.6, dist * 0.7, dist * 0.8);
234
- controls.target.set(sw / 2, 1, sd / 2);
235
  controls.update();
236
 
 
 
 
237
  // Resize
238
  window.addEventListener('resize', function() {
239
- const w2 = container.clientWidth;
240
- const h2 = container.clientHeight;
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
- // RoundRect polyfill for labels
255
- if (!CanvasRenderingContext2D.prototype.roundRect) {
256
- CanvasRenderingContext2D.prototype.roundRect = function(x, y, w, h, r) {
257
- if (r > w/2) r = w/2;
258
- if (r > h/2) r = h/2;
259
- this.moveTo(x + r, y);
260
- this.arcTo(x + w, y, x + w, y + h, r);
261
- this.arcTo(x + w, y + h, x, y + h, r);
262
- this.arcTo(x, y + h, x, y, r);
263
- this.arcTo(x, y, x + w, y, r);
264
- return this;
265
- };
266
  }
267
 
268
- // Update info overlay
269
- document.getElementById('summary-text').textContent = BLUEPRINT.summary || 'No description available.';
270
- var feat = BLUEPRINT.features || {};
 
 
271
  var featHtml = '';
272
- var featMap = {
273
- bedrooms: '🛏️', bathrooms: '🚿', stories: '🏠', garage: '🚗'
274
- };
275
- for (var k in feat) {
276
- if (featMap[k]) {
277
- var val = feat[k];
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
- initScene(BLUEPRINT);
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
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>